diff --git a/package-lock.json b/package-lock.json index c5ff968..a499e30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ }, "packages/cli": { "name": "@double-coding/flow2spec", - "version": "3.6.5", + "version": "3.6.6", "license": "ISC", "dependencies": { "@double-coding/flow2spec-core": "^3.8.2" diff --git a/packages/cli/cli.js b/packages/cli/cli.js index 1e154c6..fac1a46 100755 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -64,22 +64,47 @@ function runCommandSync(command, commandArgs, options = {}) { }); } +/** + * 判断版本是否落在 CLI 声明的 caret 兼容范围内(CLI 只接受 ^x.y.z)。 + */ +function satisfiesCoreRange(version, range) { + const match = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(String(range || "").trim()); + if (!match) throw new Error(`Core 兼容范围必须是 ^x.y.z 形式,收到:${range || ""}`); + const [major, minor, patch] = match.slice(1).map(Number); + const lower = `${major}.${minor}.${patch}`; + const upper = major > 0 ? `${major + 1}.0.0` : minor > 0 ? `0.${minor + 1}.0` : `0.0.${patch + 1}`; + return compareVersions(version, lower) >= 0 && compareVersions(version, upper) < 0; +} + +/** + * 查询兼容范围内最新稳定 Core。 + * + * 不能在 argv 里拼 `@^x.y.z`:Windows 下命令经 cmd.exe 执行(shell:true 才能找到 npm.cmd), + * 参数只拼接不转义,`^` 会被 cmd 当转义符吃掉、退化成精确版本查询,导致目标永远等于当前基线。 + * 改为列全部版本后在 JS 里按范围过滤,再对选中的精确版本取 templateVersion。 + */ function queryLatestCoreMetadata(range = coreRange) { - const output = runCommandSync( - "npm", - ["view", `${CORE_PACKAGE}@${range}`, "version", "templateVersion", "--json", "--registry=https://registry.npmjs.org"], - { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] }, - ); - const candidates = [].concat(JSON.parse(output)).filter((item) => /^\d+\.\d+\.\d+$/.test(typeof item === "string" ? item : item.version)); - candidates.sort((a, b) => compareVersions(typeof a === "string" ? a : a.version, typeof b === "string" ? b : b.version)); - const metadata = candidates.pop(); - if (!metadata) throw new Error(`没有找到兼容 ${range} 的稳定 Core 版本`); - return { - version: typeof metadata === "string" ? metadata : metadata.version, - templateVersion: typeof metadata === "string" - ? metadata - : metadata.templateVersion || metadata.version, - }; + const registry = ["--registry=https://registry.npmjs.org"]; + const query = (args) => runCommandSync("npm", [...args, ...registry], { + encoding: "utf8", + timeout: 5000, + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + const candidates = [].concat(JSON.parse(query(["view", CORE_PACKAGE, "versions", "--json"]))) + .map((value) => String(value)) + .filter((value) => /^\d+\.\d+\.\d+$/.test(value)) + .filter((value) => satisfiesCoreRange(value, range)); + candidates.sort(compareVersions); + const version = candidates.pop(); + if (!version) throw new Error(`没有找到兼容 ${range} 的稳定 Core 版本`); + let templateVersion = ""; + try { + const parsed = JSON.parse(query(["view", `${CORE_PACKAGE}@${version}`, "templateVersion", "--json"])); + if (typeof parsed === "string") templateVersion = parsed; + } catch { + // 老版本可能没有 templateVersion 字段,回落到 Core 版本本身。 + } + return { version, templateVersion: templateVersion || version }; } function updateCheckCacheFile() { diff --git a/packages/cli/package.json b/packages/cli/package.json index 1414763..4ada85c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@double-coding/flow2spec", - "version": "3.6.5", + "version": "3.6.6", "description": "在业务仓库初始化文档驱动、可写回知识库的 AI 协作骨架", "homepage": "https://github.com/double-coding-lab/Flow2Spec#readme", "repository": { diff --git a/scripts/test-cli-update.js b/scripts/test-cli-update.js index db33ebf..755907b 100644 --- a/scripts/test-cli-update.js +++ b/scripts/test-cli-update.js @@ -46,9 +46,11 @@ if(a[0]==='root') console.log(e.MOCK_GLOBAL); else if(a[0]==='view') { if(a[1]===cli) console.log('3.6.6'); else if(a[1].startsWith(cli+'@')) console.log(JSON.stringify({[core]:'^4.0.0'})); - else if(a[1]===core+'@^3.8.2') console.log(JSON.stringify([{version:'3.8.2',templateVersion:'3.8.1'},{version:'3.9.0',templateVersion:'3.9.0'},{version:'3.10.0-beta.1'}])); - else if(a[1]===core+'@^4.0.0') console.log(JSON.stringify({version:'4.1.0',templateVersion:'4.0.0'})); - else if(a[1]===core+'@^0.2.3') console.log(JSON.stringify([{version:'0.2.4'},{version:'0.2.5-beta.1'}])); + else if(a[1]===core&&a[2]==='versions') console.log(JSON.stringify(['0.2.4','0.2.5-beta.1','3.8.2','3.9.0','3.10.0-beta.1','4.1.0'])); + else if(a[1]===core+'@3.8.2') console.log(JSON.stringify('3.8.1')); + else if(a[1]===core+'@3.9.0') console.log(JSON.stringify('3.9.0')); + else if(a[1]===core+'@4.1.0') console.log(JSON.stringify('4.0.0')); + else if(a[1]===core+'@0.2.4') console.log(''); else throw Error('Unexpected query '+a.join(' ')); } else if(a[0]==='uninstall') { if(e.MOCK_FAIL) process.exit(1); } else if(a[0]==='install') { @@ -73,13 +75,17 @@ try { const check = run("--check"); assert.strictEqual(check.status, 0, check.stderr); assert.match(check.stdout, /Core:\s+3.8.2 -> 3.9.0/); + assert.match(check.stdout, /Template:\s+3.8.1 -> 3.9.0/); assert.match(check.stdout, /兼容范围 \^3.8.2/); assert.doesNotMatch(check.stdout, /4.1.0|3.10.0-beta/); assert.match(check.stdout, /update --cli 一键更新/); const zero = run("--check", { MOCK_RANGE: "^0.2.3" }); assert.strictEqual(zero.status, 0, zero.stderr); assert.match(zero.stdout, /Core:\s+3.8.2 -> 0.2.4/); - assert(zero.calls.some(c => c.args[1] === `${coreName}@^0.2.3`)); + assert(zero.calls.some(c => c.args[1] === coreName && c.args[2] === "versions")); + // Windows 下参数经 cmd.exe 执行且不转义,`^` 会被吃掉(范围退化成精确版本),故任何调用都不得传 `^`。 + const caretFree = [...check.calls, ...zero.calls]; + assert(!caretFree.some(c => c.args.some(arg => typeof arg === "string" && arg.includes("^"))), "CLI 不得把 caret 范围拼进命令行参数"); const core = run("--core"); assert.strictEqual(core.status, 0, core.stderr); assert.deepStrictEqual(core.calls.filter(c => c.args[0] === "install").map(c => c.args), [["install", "-g", `${cliName}@3.6.5`]]);