Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 40 additions & 15 deletions packages/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || "<empty>"}`);
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() {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
14 changes: 10 additions & 4 deletions scripts/test-cli-update.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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`]]);
Expand Down
Loading