diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 32d4db2..fb36841 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1,3 +1,4 @@ + import { Command } from 'commander'; import { resolveProjectConfig, @@ -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 ', 'Deployment network. CLI value overrides stellarforge.config.json.', @@ -52,19 +57,27 @@ 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 ` or set a named identity in stellarforge.config.json.', ); @@ -72,11 +85,15 @@ export function createDeployCommand( 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, + }); }); } diff --git a/src/commands/dev.ts b/src/commands/dev.ts index aba5ec5..b80dda1 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -1,9 +1,13 @@ + 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 { @@ -11,18 +15,31 @@ export interface DevCommandOptions extends DevOrchestratorOptions { 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, + }); }); } diff --git a/src/commands/root.ts b/src/commands/root.ts index b6cab2d..3539b85 100644 --- a/src/commands/root.ts +++ b/src/commands/root.ts @@ -1,3 +1,4 @@ + import { Command } from 'commander'; import { resolveCliVersion } from '../version.js'; import { createDeployCommand } from './deploy.js'; @@ -6,19 +7,32 @@ 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( @@ -26,29 +40,34 @@ export function createRootCommand(version = resolveCliVersion()): Command { ) .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.`,