@peterxiaoyang/superspec 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +302 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -238,7 +238,7 @@ superspec status
|
|
|
238
238
|
superspec update
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
这条命令会先检查 npm 上的 latest 版本;如果有新版,会自动执行全局升级并用新版 CLI 重新同步项目入口。同步内容包括补齐 `.superspec/changes` 运行时目录,并把当前 CLI 内置的 `.codex/skills/superspec-*`、`.codex/prompts/*.md`、`.codex/agents/*.toml` 同步到项目里。
|
|
242
242
|
|
|
243
243
|
## 进阶信息
|
|
244
244
|
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SuperSpec 流程引擎 — CLI 入口
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
4
6
|
import { installProject } from "./install.js";
|
|
5
7
|
import { writeSnapshot } from "./store.js";
|
|
6
8
|
import { rebuildSnapshot } from "./sync.js";
|
|
@@ -10,6 +12,7 @@ import { recordJobSubmit, recordUserDecision, jobsList, jobsPacket } from "./rec
|
|
|
10
12
|
import { recordTestRun } from "./task.js";
|
|
11
13
|
import { probeOpenSpec, openspecStatus, changeRoot } from "./openspec.js";
|
|
12
14
|
import { SUPERSPEC_VERSION } from "./version.js";
|
|
15
|
+
const PACKAGE_NAME = "@peterxiaoyang/superspec";
|
|
13
16
|
// ===== 参数解析 =====
|
|
14
17
|
function parseArgs(argv) {
|
|
15
18
|
const [command, subcommand, ...rest] = argv;
|
|
@@ -39,6 +42,279 @@ function parseFlags(args) {
|
|
|
39
42
|
}
|
|
40
43
|
return opts;
|
|
41
44
|
}
|
|
45
|
+
function parseVersion(version) {
|
|
46
|
+
const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
47
|
+
if (!match)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
major: Number(match[1]),
|
|
51
|
+
minor: Number(match[2]),
|
|
52
|
+
patch: Number(match[3]),
|
|
53
|
+
prerelease: match[4] ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function compareVersions(a, b) {
|
|
57
|
+
const left = parseVersion(a);
|
|
58
|
+
const right = parseVersion(b);
|
|
59
|
+
if (!left || !right)
|
|
60
|
+
return a.localeCompare(b);
|
|
61
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
62
|
+
if (left[key] !== right[key])
|
|
63
|
+
return left[key] - right[key];
|
|
64
|
+
}
|
|
65
|
+
if (left.prerelease === right.prerelease)
|
|
66
|
+
return 0;
|
|
67
|
+
if (left.prerelease == null)
|
|
68
|
+
return 1;
|
|
69
|
+
if (right.prerelease == null)
|
|
70
|
+
return -1;
|
|
71
|
+
return left.prerelease.localeCompare(right.prerelease);
|
|
72
|
+
}
|
|
73
|
+
function commandErrorMessage(err) {
|
|
74
|
+
if (!err || typeof err !== "object")
|
|
75
|
+
return String(err);
|
|
76
|
+
const maybe = err;
|
|
77
|
+
const stderr = maybe.stderr ? Buffer.from(maybe.stderr).toString("utf8").trim() : "";
|
|
78
|
+
const stdout = maybe.stdout ? Buffer.from(maybe.stdout).toString("utf8").trim() : "";
|
|
79
|
+
return stderr || stdout || maybe.message || String(err);
|
|
80
|
+
}
|
|
81
|
+
class SelfUpdateError extends Error {
|
|
82
|
+
phase;
|
|
83
|
+
latest;
|
|
84
|
+
constructor(phase, message, latest = null) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "SelfUpdateError";
|
|
87
|
+
this.phase = phase;
|
|
88
|
+
this.latest = latest;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isTestMode() {
|
|
92
|
+
return process.env.SUPERSPEC_TEST_MODE === "1" || process.env.NODE_ENV === "test";
|
|
93
|
+
}
|
|
94
|
+
function testEnv(name) {
|
|
95
|
+
return isTestMode() ? process.env[name] : undefined;
|
|
96
|
+
}
|
|
97
|
+
function selfUpdateError(phase, err, latest = null) {
|
|
98
|
+
return new SelfUpdateError(phase, commandErrorMessage(err), latest);
|
|
99
|
+
}
|
|
100
|
+
function attachSelfUpdateLatest(err, phase, latest) {
|
|
101
|
+
if (err instanceof SelfUpdateError) {
|
|
102
|
+
return new SelfUpdateError(err.phase, err.message, err.latest ?? latest);
|
|
103
|
+
}
|
|
104
|
+
return selfUpdateError(phase, err, latest);
|
|
105
|
+
}
|
|
106
|
+
function selfUpdateFailurePayload(err) {
|
|
107
|
+
if (err instanceof SelfUpdateError) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
message: err.message,
|
|
111
|
+
self_update: {
|
|
112
|
+
updated: false,
|
|
113
|
+
from: SUPERSPEC_VERSION,
|
|
114
|
+
to: err.latest,
|
|
115
|
+
phase: err.phase,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
message: commandErrorMessage(err),
|
|
122
|
+
self_update: {
|
|
123
|
+
updated: false,
|
|
124
|
+
from: SUPERSPEC_VERSION,
|
|
125
|
+
to: null,
|
|
126
|
+
phase: "unknown",
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function npmLatestVersion() {
|
|
131
|
+
const testError = testEnv("SUPERSPEC_TEST_NPM_VIEW_ERROR");
|
|
132
|
+
if (testError)
|
|
133
|
+
throw new SelfUpdateError("npm_view", testError);
|
|
134
|
+
const testLatest = testEnv("SUPERSPEC_TEST_LATEST_VERSION");
|
|
135
|
+
if (testLatest)
|
|
136
|
+
return testLatest;
|
|
137
|
+
try {
|
|
138
|
+
const output = execFileSync("npm", ["view", PACKAGE_NAME, "version"], { encoding: "utf8" });
|
|
139
|
+
return output.trim().replace(/^"|"$/g, "");
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
throw selfUpdateError("npm_view", err);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function installLatestGlobal() {
|
|
146
|
+
const testError = testEnv("SUPERSPEC_TEST_GLOBAL_INSTALL_ERROR");
|
|
147
|
+
if (testError)
|
|
148
|
+
throw new SelfUpdateError("global_install", testError);
|
|
149
|
+
if (testEnv("SUPERSPEC_TEST_SKIP_GLOBAL_INSTALL") === "1")
|
|
150
|
+
return;
|
|
151
|
+
try {
|
|
152
|
+
execFileSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
|
|
153
|
+
encoding: "utf8",
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
throw selfUpdateError("global_install", err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function parseSuperSpecVersion(output) {
|
|
162
|
+
const match = output.trim().match(/^SuperSpec\s+(.+)$/);
|
|
163
|
+
return match?.[1]?.trim() ?? null;
|
|
164
|
+
}
|
|
165
|
+
function pathCliVersion() {
|
|
166
|
+
const testError = testEnv("SUPERSPEC_TEST_CLI_VERSION_ERROR");
|
|
167
|
+
if (testError)
|
|
168
|
+
throw new Error(testError);
|
|
169
|
+
const testVersion = testEnv("SUPERSPEC_TEST_CLI_VERSION");
|
|
170
|
+
if (testVersion)
|
|
171
|
+
return testVersion;
|
|
172
|
+
const testOutput = testEnv("SUPERSPEC_TEST_CLI_VERSION_OUTPUT");
|
|
173
|
+
if (testOutput) {
|
|
174
|
+
const parsed = parseSuperSpecVersion(testOutput);
|
|
175
|
+
if (!parsed)
|
|
176
|
+
throw new Error(`无法解析 superspec --version 输出:${testOutput}`);
|
|
177
|
+
return parsed;
|
|
178
|
+
}
|
|
179
|
+
const output = execFileSync("superspec", ["--version"], {
|
|
180
|
+
encoding: "utf8",
|
|
181
|
+
env: process.env,
|
|
182
|
+
});
|
|
183
|
+
const parsed = parseSuperSpecVersion(output);
|
|
184
|
+
if (!parsed)
|
|
185
|
+
throw new Error(`无法解析 superspec --version 输出:${output.trim()}`);
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
function assertUpdatedCliVersion(latest) {
|
|
189
|
+
let actual;
|
|
190
|
+
try {
|
|
191
|
+
actual = pathCliVersion();
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
throw selfUpdateError("version_check", err, latest);
|
|
195
|
+
}
|
|
196
|
+
if (actual !== latest) {
|
|
197
|
+
throw new SelfUpdateError("version_mismatch", `全局 superspec 版本仍为 ${actual},期望 ${latest}。请检查 npm 全局 bin 是否在 PATH 前置。`, latest);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function rerunUpdatedCli(projectRoot, args) {
|
|
201
|
+
const testError = testEnv("SUPERSPEC_TEST_RERUN_ERROR");
|
|
202
|
+
if (testError)
|
|
203
|
+
throw new SelfUpdateError("rerun", testError);
|
|
204
|
+
const testOutput = testEnv("SUPERSPEC_TEST_RERUN_OUTPUT");
|
|
205
|
+
if (testOutput)
|
|
206
|
+
return testOutput;
|
|
207
|
+
try {
|
|
208
|
+
return execFileSync("superspec", args, {
|
|
209
|
+
cwd: projectRoot,
|
|
210
|
+
encoding: "utf8",
|
|
211
|
+
env: process.env,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
throw selfUpdateError("rerun", err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function updateSelfIfNeeded(projectRoot, rerunArgs) {
|
|
219
|
+
const latest = npmLatestVersion();
|
|
220
|
+
if (compareVersions(latest, SUPERSPEC_VERSION) <= 0)
|
|
221
|
+
return { updated: false, latest };
|
|
222
|
+
try {
|
|
223
|
+
installLatestGlobal();
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
throw attachSelfUpdateLatest(err, "global_install", latest);
|
|
227
|
+
}
|
|
228
|
+
assertUpdatedCliVersion(latest);
|
|
229
|
+
let output;
|
|
230
|
+
try {
|
|
231
|
+
output = rerunUpdatedCli(projectRoot, rerunArgs);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
throw attachSelfUpdateLatest(err, "rerun", latest);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
updated: true,
|
|
238
|
+
latest,
|
|
239
|
+
output,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function updatedCliOutput(output, latest) {
|
|
243
|
+
const trimmed = output.trim();
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(trimmed);
|
|
246
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
247
|
+
throw new Error("新版 CLI 输出不是 JSON object");
|
|
248
|
+
}
|
|
249
|
+
const payload = {
|
|
250
|
+
...parsed,
|
|
251
|
+
self_update: {
|
|
252
|
+
updated: true,
|
|
253
|
+
from: SUPERSPEC_VERSION,
|
|
254
|
+
to: latest,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
exitCode: payload.ok === false ? 1 : 0,
|
|
259
|
+
text: JSON.stringify(payload, null, 2),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
const failure = new SelfUpdateError("rerun_output", "新版 CLI 输出不是 JSON object", latest);
|
|
264
|
+
return {
|
|
265
|
+
exitCode: 1,
|
|
266
|
+
text: JSON.stringify(selfUpdateFailurePayload(failure), null, 2),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function askToUpdateSelfIfNeeded(projectRoot, rerunArgs) {
|
|
271
|
+
const assumeTty = testEnv("SUPERSPEC_TEST_ASSUME_TTY") === "1";
|
|
272
|
+
if (!assumeTty && (!process.stdin.isTTY || !process.stdout.isTTY))
|
|
273
|
+
return { updated: false, latest: null };
|
|
274
|
+
let latest;
|
|
275
|
+
try {
|
|
276
|
+
latest = npmLatestVersion();
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
console.error(`SuperSpec 检查最新版本失败:${commandErrorMessage(err)}。继续使用当前版本。`);
|
|
280
|
+
return { updated: false, latest: null };
|
|
281
|
+
}
|
|
282
|
+
if (compareVersions(latest, SUPERSPEC_VERSION) <= 0)
|
|
283
|
+
return { updated: false, latest };
|
|
284
|
+
const testAnswer = testEnv("SUPERSPEC_TEST_PROMPT_ANSWER");
|
|
285
|
+
if (testAnswer !== undefined) {
|
|
286
|
+
if (/^n(o)?$/i.test(testAnswer.trim()))
|
|
287
|
+
return { updated: false, latest };
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
291
|
+
try {
|
|
292
|
+
const answer = await rl.question(`发现 SuperSpec ${latest} 可用,当前为 ${SUPERSPEC_VERSION}。是否先升级再继续? [Y/n] `);
|
|
293
|
+
if (/^n(o)?$/i.test(answer.trim()))
|
|
294
|
+
return { updated: false, latest };
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
rl.close();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
installLatestGlobal();
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
throw attachSelfUpdateLatest(err, "global_install", latest);
|
|
305
|
+
}
|
|
306
|
+
assertUpdatedCliVersion(latest);
|
|
307
|
+
try {
|
|
308
|
+
return {
|
|
309
|
+
updated: true,
|
|
310
|
+
latest,
|
|
311
|
+
output: rerunUpdatedCli(projectRoot, rerunArgs),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
throw attachSelfUpdateLatest(err, "rerun", latest);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
42
318
|
// ===== 初始化 transition init =====
|
|
43
319
|
// ===== init/explore 现在在 transition.ts 中(走统一锁内路径)=====
|
|
44
320
|
// ===== 主分发 =====
|
|
@@ -56,7 +332,7 @@ async function main(argv) {
|
|
|
56
332
|
jobs <子命令> --change <C> 工作项管理(见下)
|
|
57
333
|
install 安装项目工作流入口
|
|
58
334
|
init --scope project install 的兼容别名
|
|
59
|
-
update
|
|
335
|
+
update 升级 CLI 到 npm latest 并同步项目工作流模板
|
|
60
336
|
version 版本号
|
|
61
337
|
|
|
62
338
|
transition 子命令:
|
|
@@ -92,19 +368,37 @@ jobs 子命令:
|
|
|
92
368
|
return 1;
|
|
93
369
|
}
|
|
94
370
|
try {
|
|
371
|
+
if (opts["skip-self-update"] !== "true") {
|
|
372
|
+
const rerunArgs = command === "init"
|
|
373
|
+
? ["init", "--scope", "project", "--skip-self-update"]
|
|
374
|
+
: ["install", "--skip-self-update"];
|
|
375
|
+
const selfUpdate = await askToUpdateSelfIfNeeded(projectRoot, rerunArgs);
|
|
376
|
+
if (selfUpdate.updated) {
|
|
377
|
+
const rerun = updatedCliOutput(selfUpdate.output, selfUpdate.latest);
|
|
378
|
+
console.log(rerun.text);
|
|
379
|
+
return rerun.exitCode;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
95
382
|
console.log(JSON.stringify(installProject(projectRoot)));
|
|
96
383
|
return 0;
|
|
97
384
|
}
|
|
98
385
|
catch (err) {
|
|
99
|
-
console.log(JSON.stringify(
|
|
100
|
-
|
|
101
|
-
message: err
|
|
102
|
-
}));
|
|
386
|
+
console.log(JSON.stringify(err instanceof SelfUpdateError
|
|
387
|
+
? selfUpdateFailurePayload(err)
|
|
388
|
+
: { ok: false, message: commandErrorMessage(err) }));
|
|
103
389
|
return 1;
|
|
104
390
|
}
|
|
105
391
|
}
|
|
106
392
|
if (command === "update") {
|
|
107
393
|
try {
|
|
394
|
+
if (opts["skip-self-update"] !== "true") {
|
|
395
|
+
const selfUpdate = updateSelfIfNeeded(projectRoot, ["update", "--skip-self-update"]);
|
|
396
|
+
if (selfUpdate.updated) {
|
|
397
|
+
const rerun = updatedCliOutput(selfUpdate.output, selfUpdate.latest);
|
|
398
|
+
console.log(rerun.text);
|
|
399
|
+
return rerun.exitCode;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
108
402
|
const result = installProject(projectRoot, { allowLegacyState: true });
|
|
109
403
|
console.log(JSON.stringify({
|
|
110
404
|
...result,
|
|
@@ -113,10 +407,9 @@ jobs 子命令:
|
|
|
113
407
|
return 0;
|
|
114
408
|
}
|
|
115
409
|
catch (err) {
|
|
116
|
-
console.log(JSON.stringify(
|
|
117
|
-
|
|
118
|
-
message: err
|
|
119
|
-
}));
|
|
410
|
+
console.log(JSON.stringify(err instanceof SelfUpdateError
|
|
411
|
+
? selfUpdateFailurePayload(err)
|
|
412
|
+
: { ok: false, message: commandErrorMessage(err) }));
|
|
120
413
|
return 1;
|
|
121
414
|
}
|
|
122
415
|
}
|