From fa7410a56cd5a60962c3bd2f79c9e58e196d92a1 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:04:55 +0800 Subject: [PATCH] fix(cli): preserve numeric-looking version strings Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- cli/src/index.ts | 32 ++++++++++++++++++-- cli/test/integration/version-option.test.ts | 33 +++++++++++++++++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/cli/src/index.ts b/cli/src/index.ts index 0fd549d8..6b27bb87 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -27,6 +27,26 @@ function toArray(val: string | string[] | undefined): string[] | undefined { return Array.isArray(val) ? val : [val] } +/** Read a string option before cac/mri coerces numeric-looking values to numbers. */ +function rawStringOption(argv: string[], name: string): string | undefined { + const optionWithEquals = `${name}=` + const end = argv.indexOf('--') + const args = end === -1 ? argv : argv.slice(0, end) + let value: string | undefined + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]! + if (argument === name) { + value = args[index + 1] + index += 1 + } else if (argument.startsWith(optionWithEquals)) { + value = argument.slice(optionWithEquals.length) + } + } + + return value +} + async function runCommand(action: () => Promise, json = false): Promise { try { const output = await action() @@ -246,7 +266,11 @@ cli .option('--token ', 'API token') .option('--json', 'Output JSON') .action((slug: string, options: InstallCommandOptions & { agent?: string | string[] }) => { - return runCommand(() => installCommand(slug, { ...options, agent: toArray(options.agent) }), Boolean(options.json)) + return runCommand(() => installCommand(slug, { + ...options, + version: rawStringOption(process.argv.slice(2), '--version'), + agent: toArray(options.agent) + }), Boolean(options.json)) }) cli @@ -262,7 +286,11 @@ cli .option('--json', 'Output JSON') .action((action: string, coordinate: string, options: SuiteCommandOptions & { agent?: string | string[] }) => { return runCommand( - () => suiteCommand(action, coordinate, { ...options, agent: toArray(options.agent) }), + () => suiteCommand(action, coordinate, { + ...options, + version: rawStringOption(process.argv.slice(2), '--version'), + agent: toArray(options.agent) + }), Boolean(options.json) ) }) diff --git a/cli/test/integration/version-option.test.ts b/cli/test/integration/version-option.test.ts index 9a8fcf26..185b65e9 100644 --- a/cli/test/integration/version-option.test.ts +++ b/cli/test/integration/version-option.test.ts @@ -43,9 +43,36 @@ describe('--version parsing', () => { expect(registry.received.resolve?.version).toBe(TIMESTAMP_VERSION) }) + test('install preserves trailing zeros with the --version=value form', async () => { + const env = await createTempHome() + const registry = await startFakeRegistry({ + token: 'sk_ok', + skills: [{ + namespace: 'global', + slug: 'timestamped-equals', + version: TIMESTAMP_VERSION, + zipBytes: zipSync({ 'SKILL.md': strToU8('# timestamped equals') }) + }] + }) + stopServer = registry.stop + const installDir = join(env.cwd, 'skills-equals') + await mkdir(installDir, { recursive: true }) + + const result = await runCli([ + 'install', '@global/timestamped-equals', + `--version=${TIMESTAMP_VERSION}`, + '--dir', installDir, + '--registry', registry.url, + '--token', 'sk_ok' + ], { HOME: env.home, USERPROFILE: env.home }) + + expect(result.exitCode).toBe(0) + expect(registry.received.resolve?.version).toBe(TIMESTAMP_VERSION) + }) + test('suite install preserves trailing zeros in a numeric-looking version', async () => { const env = await createTempHome() - let requestedVersion: string | null = null + const received = { version: null as string | null } const server = Bun.serve({ port: 0, fetch(request) { @@ -54,7 +81,7 @@ describe('--version parsing', () => { return Response.json({ apiBase: '/api/v1', capabilities: ['skill-suite-v1'] }) } if (url.pathname === '/api/v1/suites/global/starter-pack/install-plan') { - requestedVersion = url.searchParams.get('version') + received.version = url.searchParams.get('version') return Response.json({ code: 404, message: 'stop after capturing version' }, { status: 404 }) } return Response.json({ code: 404, message: 'not found' }, { status: 404 }) @@ -73,6 +100,6 @@ describe('--version parsing', () => { ], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).not.toBe(0) - expect(requestedVersion).toBe(TIMESTAMP_VERSION) + expect(received.version).toBe(TIMESTAMP_VERSION) }) })