From a1f6ea43f9bb5cd8ab9efdaa2941fca06f97f822 Mon Sep 17 00:00:00 2001 From: liguochuan <292761894@qq.com> Date: Wed, 30 Sep 2026 10:54:03 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[tech]=20=F0=9F=90=9B=20fix(cli):=20Windows?= =?UTF-8?q?=20=E4=B8=8B=20Core=20=E8=8C=83=E5=9B=B4=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E7=9A=84=20caret=20=E8=A2=AB=20cmd=20=E5=90=83=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flow2spec update 用 `npm view @^x.y.z` 查兼容范围内最新 Core;Windows 上 参数经 cmd.exe 执行(shell:true 才能找到 npm.cmd)且只拼接不转义,`^` 被当转义符 吃掉后查询退化为精确版本,目标永远等于当前基线:update --check 永远显示已最新、 update --core 实际 no-op(Linux/macOS 不受影响,CI 在 Linux 故未暴露)。 改为 `npm view versions --json` 列全部版本,在 JS 里按 caret 范围过滤取最大, 再对选中的精确版本查 templateVersion;全程不向 argv 传特殊字符。 test-cli-update 的 mock 与断言同步,并新增「任何调用都不得传 ^」回归断言。 --- packages/cli/cli.js | 55 +++++++++++++++++++++++++++----------- scripts/test-cli-update.js | 14 +++++++--- 2 files changed, 50 insertions(+), 19 deletions(-) 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/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`]]); From ee0cae4a6d8f8af0d214eb8f9ad644c1e7e985d1 Mon Sep 17 00:00:00 2001 From: liguochuan <292761894@qq.com> Date: Wed, 30 Sep 2026 10:54:05 +0800 Subject: [PATCH 2/2] =?UTF-8?q?[tech]=20=F0=9F=94=96=20chore(release):=20c?= =?UTF-8?q?li@3.6.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core 保持 3.8.3、Template 3.8.2,CLI 兼容范围仍为 ^3.8.2。 本轮已跑 version:check / npm test / pack:check 全部通过。 --- package-lock.json | 2 +- packages/cli/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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": {