diff --git a/src/cli.js b/src/cli.js index cb793a7d..d8c8c7ba 100644 --- a/src/cli.js +++ b/src/cli.js @@ -36,7 +36,8 @@ const sdkCommands = { build: 'builds a project', clean: 'removes previous build directories', create: 'creates a new project', - project: 'get and set tiapp.xml settings' + project: 'get and set tiapp.xml settings', + serve: 'serves a project through the Titanium Vite runtime', }; /** @@ -416,7 +417,13 @@ export class CLI { const cmd = args.pop(); args.pop(); // discard argv - // remove any trailing undefined args + // `args` are Commander's action-handler arguments, one entry per declared + // positional in order. Variadic positionals (e.g. `sdk uninstall + // [versions...]`) arrive as a nested array, which consumers like + // `sdk.js` rely on (`cli.argv._[0]` is the versions array). Flattening to + // `cmd.args` here would collapse that array into a string and break them. + // The `serve`/`build` positional platform is handled separately in + // `initBuildPlatform()` via `command.processedArgs`, not `argv._`. while (args.length && args[args.length - 1] === undefined) { args.pop(); } @@ -621,20 +628,19 @@ export class CLI { } /** - * If the current command is the "build" command, this function will check - * if the "build" command has a `--platform` option (which is should), then - * prompt for the platform is not explicitly passed in. + * If the current command supports platform-specific config, this function + * checks for the `--platform` option and prompts when missing. * - * Finally, the platform specific options and flags are added to the - * Commander.js "build" command context so that the second parse will - * pick up the newly defined options/flags. + * Finally, the platform specific options and flags are added to the command + * context so that the second parse picks up the newly defined options/flags. * * @returns {Promise} * @access private */ async initBuildPlatform() { const cmdName = this.command.name(); - if (cmdName !== 'build') { + // Commands with build-style platform branches. + if (cmdName !== 'build' && cmdName !== 'serve') { return; } @@ -643,6 +649,15 @@ export class CLI { return; } + // Support shorthand positional platform syntax, e.g. `ti serve ios`. + // Commander parses this into processedArgs via the [platform] argument + // declared in loadCommand(). + const positionalPlatform = this.command.processedArgs?.[0]; + if (!this.argv.platform && positionalPlatform && platformOption.values.includes(positionalPlatform)) { + this.debugLogger.trace(`Converting positional platform argument "${positionalPlatform}" to --platform`); + this.argv.platform = positionalPlatform; + } + // when specifying `--platform ios`, the SDK's option callback converts // it to `iphone`, however the platform config uses `ios` and we must // convert it back @@ -983,6 +998,9 @@ export class CLI { this.command.createHelp = () => { return Object.assign(new TiHelp(this, conf.platforms), this.command.configureHelp()); }; + + cmd.argument('[platform]', 'target platform'); + cmd.usage('[platform] [options]'); } applyCommandConfig(this, cmdName, cmd, conf); diff --git a/test/commands/ti-build.test.js b/test/commands/ti-build.test.js index 7fd9fbf6..5952be60 100644 --- a/test/commands/ti-build.test.js +++ b/test/commands/ti-build.test.js @@ -8,7 +8,7 @@ describe('ti build', () => { const output = stripColor(stdout); expect(output).toMatch(/Titanium Command-Line Interface/); - expect(output).toMatch(/Usage: titanium build \[options\]/); + expect(output).toMatch(/Usage: titanium build \[platform\] \[options\]/); expect(output).toMatch(/Builds an existing app or module project./); expect(output).toMatch(/Build Options:/); expect(output).toMatch(/Global Options:/); diff --git a/test/commands/ti-sdk.test.js b/test/commands/ti-sdk.test.js index 068da898..347feacd 100644 --- a/test/commands/ti-sdk.test.js +++ b/test/commands/ti-sdk.test.js @@ -334,4 +334,29 @@ describe('ti sdk', () => { expect(exitCode).toBe(0); })); }); + + // Regression guard: `sdk uninstall` takes a variadic `[versions...]` + // positional, which `sdk.js` reads as the array `cli.argv._[0]`. A parsing + // change once flattened variadic positionals into individual strings, so + // `versions.filter()` threw and the command printed usage instead of running. + // These cases reproduce that without network by uninstalling versions that + // are not installed, exercising single and multi-version parsing. + describe('uninstall', () => { + it('should parse a single version positional and report not found', initSDKHome(async ({ run }) => { + const { exitCode, stdout } = await run(['sdk', 'uninstall', '99.99.99.GA', '--force']); + + const output = stripColor(stdout); + expect(output).toMatch(/99\.99\.99\.GA\s+not found/); + expect(exitCode).toBe(0); + })); + + it('should parse multiple version positionals and report each not found', initSDKHome(async ({ run }) => { + const { exitCode, stdout } = await run(['sdk', 'uninstall', '98.0.0.GA', '99.0.0.GA', '--force']); + + const output = stripColor(stdout); + expect(output).toMatch(/98\.0\.0\.GA\s+not found/); + expect(output).toMatch(/99\.0\.0\.GA\s+not found/); + expect(exitCode).toBe(0); + })); + }); });