@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +302 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -238,7 +238,7 @@ superspec status
238
238
  superspec update
239
239
  ```
240
240
 
241
- 这条命令会补齐 `.superspec/changes` 运行时目录,并把当前 CLI 内置的 `.codex/skills/superspec-*`、`.codex/prompts/*.md`、`.codex/agents/*.toml` 同步到项目里。真正升级全局 npm 包仍需使用 `npm install -g @peterxiaoyang/superspec@<version>`。
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
- ok: false,
101
- message: err.message,
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
- ok: false,
118
- message: err.message,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": { "node": ">=20.19.0" },