Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import { Command } from 'commander';
import {
resolveProjectConfig,
Expand Down Expand Up @@ -35,13 +36,17 @@ interface DeployCommandFlags {
export function createDeployCommand(
options: DeployCommandOptions = {},
): Command {
const cwd = options.cwd ?? process.cwd;
const output = options.output ?? new TerminalOutput();
const resolveConfig = options.resolveConfig ?? resolveProjectConfig;
const resolvePlan = options.resolvePlan ?? resolveTestnetDeploymentPlan;
const {
cwd = () => process.cwd(),
output = new TerminalOutput(),
resolveConfig = resolveProjectConfig,
resolvePlan = resolveTestnetDeploymentPlan,
} = options;

return new Command('deploy')
.description('Deploy a supported StellarForge project to Stellar Testnet.')
.description(
'Deploy a supported StellarForge project to Stellar Testnet.',
)
.option(
'--network <network>',
'Deployment network. CLI value overrides stellarforge.config.json.',
Expand All @@ -52,31 +57,43 @@ export function createDeployCommand(
)
.action(async (flags: DeployCommandFlags) => {
const projectRoot = cwd();

const overrides: ProjectConfigOverrides = {
...(flags.network === undefined ? {} : { network: flags.network }),
...(flags.source === undefined ? {} : { identity: flags.source }),
...(flags.network === undefined
? {}
: { network: flags.network }),
...(flags.source === undefined
? {}
: { identity: flags.source }),
};

const configuration = resolveConfig(projectRoot, overrides);

if (configuration.network === undefined) {
const { network, identity } = configuration;

if (network === undefined) {
throw new ValidationCliError(
'Deployment network is required. Pass `--network testnet` or set an explicit network in stellarforge.config.json.',
);
}

if (configuration.identity === undefined) {
if (identity === undefined) {
throw new ValidationCliError(
'Deployment identity is required. Pass `--source <identity>` or set a named identity in stellarforge.config.json.',
);
}

const plan = resolvePlan({
cwd: projectRoot,
network: configuration.network,
source: configuration.identity,
network,
source: identity,
});

output.info('Deploying smart contract to Stellar Testnet.');
await superviseDevProcesses(plan, { ...options, output });

await superviseDevProcesses(plan, {
...options,
output,
});
});
}
33 changes: 25 additions & 8 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@

import { Command } from 'commander';
import {
superviseDevProcesses,
type DevOrchestratorOptions,
} from '../dev/orchestrator.js';
import { resolveDevProcessPlan, type DevProcessSpec } from '../dev/project.js';
import {
resolveDevProcessPlan,
type DevProcessSpec,
} from '../dev/project.js';
import { TerminalOutput } from '../output/terminal.js';

export interface DevCommandOptions extends DevOrchestratorOptions {
readonly cwd?: () => string;
readonly resolvePlan?: (cwd: string) => readonly DevProcessSpec[];
}

export function createDevCommand(options: DevCommandOptions = {}): Command {
const cwd = options.cwd ?? process.cwd;
const output = options.output ?? new TerminalOutput();
const resolvePlan = options.resolvePlan ?? resolveDevProcessPlan;
export function createDevCommand(
options: DevCommandOptions = {},
): Command {
const {
cwd = () => process.cwd(),
resolvePlan = resolveDevProcessPlan,
output = new TerminalOutput(),
} = options;

return new Command('dev')
.description('Start development processes for a StellarForge project.')
.action(async () => {
const plan = resolvePlan(cwd());
const workingDirectory = cwd();
const plan = resolvePlan(workingDirectory);
const processCount = plan.length;
const processLabel =
processCount === 1 ? 'process' : 'processes';

output.info(
`Starting ${plan.length} development process${plan.length === 1 ? '' : 'es'}.`,
`Starting ${processCount} development ${processLabel}.`,
);
await superviseDevProcesses(plan, { ...options, output });

await superviseDevProcesses(plan, {
...options,
output,
});
});
}
59 changes: 39 additions & 20 deletions src/commands/root.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import { Command } from 'commander';
import { resolveCliVersion } from '../version.js';
import { createDeployCommand } from './deploy.js';
Expand All @@ -6,49 +7,67 @@ import { createDoctorCommand } from './doctor.js';
import { createNewCommand } from './new.js';
import { createTestCommand } from './test.js';

// Commander implements `unknownCommand()` publicly but omits it from typings.
// Commander exposes unknownCommand() at runtime but omits it from its typings.
interface UnknownCommandReporter {
unknownCommand(): never;
}

function applyUsageErrorHandling(command: Command, remediation: string): void {
function applyUsageErrorHandling(
command: Command,
remediation: string,
): void {
command
.exitOverride()
.showHelpAfterError(remediation)
.showSuggestionAfterError(true);
}

export function createRootCommand(version = resolveCliVersion()): Command {
function reportUnknownCommand(command: Command): void {
if (command.args.length === 0) {
return;
}

(command as unknown as UnknownCommandReporter).unknownCommand();
}

export function createRootCommand(
version = resolveCliVersion(),
): Command {
const program = new Command()
.name('stellarforge')
.description(
'Open infrastructure for building production-ready Stellar applications.',
)
.usage('[options] [command]')
.version(version)
// Operands reach the action so unknown commands are reported as such
// rather than as excess arguments.
// Allow operands through so the root action can report unknown commands.
.allowExcessArguments()
.action((_options: unknown, command: Command) => {
if (command.args.length > 0) {
(command as unknown as UnknownCommandReporter).unknownCommand();
}
})
.addCommand(createDoctorCommand())
.addCommand(createNewCommand())
.addCommand(createDevCommand())
.addCommand(createTestCommand())
.addCommand(createDeployCommand());

const commandNames = program.commands.map((command) => command.name());
reportUnknownCommand(command);
});

const subcommands = [
createDoctorCommand(),
createNewCommand(),
createDevCommand(),
createTestCommand(),
createDeployCommand(),
];

for (const command of subcommands) {
program.addCommand(command);
}

const commandNames = subcommands
.map((command) => command.name())
.join(', ');

applyUsageErrorHandling(
program,
`Available commands: ${commandNames.join(', ')}.\nRun 'stellarforge --help' for usage.`,
`Available commands: ${commandNames}.\nRun 'stellarforge --help' for usage.`,
);

// `addCommand` does not inherit parent settings, so subcommand usage errors
// must be configured explicitly to share the root's exit-code mapping.
for (const command of program.commands) {
for (const command of subcommands) {
applyUsageErrorHandling(
command,
`Run 'stellarforge ${command.name()} --help' for usage.`,
Expand Down