@peterxiaoyang/superspec 0.1.1 → 0.1.3

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.
@@ -1,4 +1,21 @@
1
1
  import type { JsonMap, Reason } from "./util.ts";
2
+ import { runCommand } from "./util.ts";
3
+ export declare const REQUIRED_OPENSPEC_MIN_VERSION = "1.4.1";
4
+ export type OpenspecCliProbe = {
5
+ ok: boolean;
6
+ state: "ok" | "missing" | "invalid" | "too_old";
7
+ version: string | null;
8
+ message: string;
9
+ };
10
+ export declare function parse_openspec_version(raw: string): string | null;
11
+ export declare function compare_versions(actual: string, required: string): number;
12
+ export declare function openspec_cli_probe(opts?: {
13
+ cwd?: string;
14
+ commandExistsFn?: (cmd: string, meta?: {
15
+ cwd?: string;
16
+ }) => boolean;
17
+ run?: typeof runCommand;
18
+ }): OpenspecCliProbe;
2
19
  export declare function openspec_status(change: string): JsonMap;
3
20
  export declare function openspec_validate(change: string): [boolean, string];
4
21
  export declare function openspec_version(): string;
@@ -1,6 +1,93 @@
1
1
  import { dirname, join, resolve, sep } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
- import { GATE_ALIASES, GATE_ROUTE, GuardError, ROUTE_ALIASES, ROUTE_ORDER, fingerprint_obj, isObject, reason, repr, runCommand, } from "./util.js";
3
+ import { REQUIRED_OPENSPEC_CLI_SURFACES, GATE_ALIASES, GATE_ROUTE, GuardError, ROUTE_ALIASES, ROUTE_ORDER, commandExists, fingerprint_obj, isObject, reason, repr, runCommand, } from "./util.js";
4
+ export const REQUIRED_OPENSPEC_MIN_VERSION = "1.4.1";
5
+ export function parse_openspec_version(raw) {
6
+ const match = raw.match(/\b(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?\b/);
7
+ return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
8
+ }
9
+ export function compare_versions(actual, required) {
10
+ const actualParts = actual.split(".").map((part) => Number.parseInt(part, 10));
11
+ const requiredParts = required.split(".").map((part) => Number.parseInt(part, 10));
12
+ for (let index = 0; index < Math.max(actualParts.length, requiredParts.length); index += 1) {
13
+ const actualPart = actualParts[index] ?? 0;
14
+ const requiredPart = requiredParts[index] ?? 0;
15
+ if (!Number.isFinite(actualPart))
16
+ return -1;
17
+ if (!Number.isFinite(requiredPart))
18
+ return 1;
19
+ if (actualPart > requiredPart)
20
+ return 1;
21
+ if (actualPart < requiredPart)
22
+ return -1;
23
+ }
24
+ return 0;
25
+ }
26
+ export function openspec_cli_probe(opts = {}) {
27
+ const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
28
+ const run = opts.run ?? runCommand;
29
+ if (!commandExistsFn("openspec", { cwd: opts.cwd })) {
30
+ return {
31
+ ok: false,
32
+ state: "missing",
33
+ version: null,
34
+ message: "PATH 中缺少 OpenSpec CLI(openspec)",
35
+ };
36
+ }
37
+ const versionProc = run("openspec", ["--version"], { cwd: opts.cwd, timeout: 15_000 });
38
+ if (versionProc.error || versionProc.status !== 0) {
39
+ const output = (versionProc.error?.message ?? (versionProc.stderr || versionProc.stdout)).trim();
40
+ return {
41
+ ok: false,
42
+ state: "invalid",
43
+ version: null,
44
+ message: `\`openspec --version\` 执行失败:${output}`,
45
+ };
46
+ }
47
+ const rawVersion = `${versionProc.stdout}${versionProc.stderr}`.trim();
48
+ if (/openspec[-_\s]*chinese/iu.test(rawVersion)) {
49
+ return {
50
+ ok: false,
51
+ state: "invalid",
52
+ version: null,
53
+ message: "PATH 上的 openspec 来自 openspec-chinese,不是受支持的 @fission-ai/openspec CLI",
54
+ };
55
+ }
56
+ const version = parse_openspec_version(rawVersion);
57
+ if (version === null) {
58
+ return {
59
+ ok: false,
60
+ state: "invalid",
61
+ version: null,
62
+ message: "PATH 上的 OpenSpec CLI(openspec)无法报告语义版本,可能不是受支持的 @fission-ai/openspec",
63
+ };
64
+ }
65
+ if (compare_versions(version, REQUIRED_OPENSPEC_MIN_VERSION) < 0) {
66
+ return {
67
+ ok: false,
68
+ state: "too_old",
69
+ version,
70
+ message: `openspec ${version} 低于 SuperSpec 要求的最低版本 ${REQUIRED_OPENSPEC_MIN_VERSION}`,
71
+ };
72
+ }
73
+ for (const args of REQUIRED_OPENSPEC_CLI_SURFACES) {
74
+ const proc = run("openspec", [...args], { cwd: opts.cwd, timeout: 15_000 });
75
+ if (proc.error || proc.status !== 0) {
76
+ return {
77
+ ok: false,
78
+ state: "invalid",
79
+ version,
80
+ message: `openspec ${version} 缺少必需的原生能力:\`openspec ${args.join(" ")}\` 执行失败`,
81
+ };
82
+ }
83
+ }
84
+ return {
85
+ ok: true,
86
+ state: "ok",
87
+ version,
88
+ message: `openspec ${version} satisfies SuperSpec requirements`,
89
+ };
90
+ }
4
91
  export function openspec_status(change) {
5
92
  const proc = runCommand("openspec", ["status", "--change", change, "--json"], { timeout: 30_000 });
6
93
  if (proc.error) {
@@ -28,8 +115,7 @@ export function openspec_version() {
28
115
  if (proc.error || proc.status !== 0)
29
116
  return "unknown";
30
117
  const raw = (proc.stdout || proc.stderr).trim();
31
- const match = raw.match(/\d+\.\d+\.\d+/);
32
- return match ? match[0] : raw || "unknown";
118
+ return (parse_openspec_version(raw) ?? raw) || "unknown";
33
119
  }
34
120
  export function openspec_status_shape_reasons(status) {
35
121
  const problems = [];
@@ -1,4 +1,4 @@
1
- import { type JsonMap } from "./core.ts";
1
+ import { type JsonMap, type OpenspecCliProbe } from "./core.ts";
2
2
  export declare const OPENSPEC_NPM_PACKAGE = "@fission-ai/openspec";
3
3
  export declare const OPENSPEC_INSTALL_DOC_URL = "https://github.com/Fission-AI/OpenSpec#readme";
4
4
  export type OpenspecInstallPlan = {
@@ -13,12 +13,19 @@ export declare function recommended_openspec_install_plan(opts?: {
13
13
  cwd?: string;
14
14
  }) => boolean;
15
15
  }): OpenspecInstallPlan | null;
16
+ export declare function forced_openspec_install_plan(plan: OpenspecInstallPlan): OpenspecInstallPlan;
16
17
  export declare function missing_openspec_cli_message(opts?: {
17
18
  cwd?: string;
18
19
  commandExistsFn?: (cmd: string, meta?: {
19
20
  cwd?: string;
20
21
  }) => boolean;
21
22
  }): string;
23
+ export declare function openspec_cli_requirement_message(probe: OpenspecCliProbe, opts?: {
24
+ cwd?: string;
25
+ commandExistsFn?: (cmd: string, meta?: {
26
+ cwd?: string;
27
+ }) => boolean;
28
+ }): string;
22
29
  export declare function project_init(repoRootRaw?: string, opts?: {
23
30
  force?: boolean;
24
31
  }): JsonMap;
@@ -1,13 +1,13 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_OPENSPEC_CODEX_SKILLS, block, reason, read_agent_toml_name, read_skill_frontmatter_name, commandExists, runCommand, } from "./core.js";
3
+ import { REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_OPENSPEC_CODEX_SKILLS, REQUIRED_OPENSPEC_MIN_VERSION, block, commandExists, openspec_cli_probe, reason, read_agent_toml_name, read_skill_frontmatter_name, runCommand, } from "./core.js";
4
4
  import { install_workflow } from "./install_engine.js";
5
5
  import { system_failure_zh } from "./i18n.js";
6
6
  export const OPENSPEC_NPM_PACKAGE = "@fission-ai/openspec";
7
7
  export const OPENSPEC_INSTALL_DOC_URL = "https://github.com/Fission-AI/OpenSpec#readme";
8
8
  const ROLE_DESCRIPTIONS = {
9
9
  architect: "系统设计、边界、接口与长期取舍",
10
- critic: "对计划、证据、假设与范围漂移做对抗审查",
10
+ critic: "严格审查计划、证据、假设与范围漂移",
11
11
  "test-engineer": "测试策略、覆盖率与 RED/GREEN 证据审查",
12
12
  "code-reviewer": "代码 / 规格 / 安全审查",
13
13
  verifier: "最终完成证据与验证审查",
@@ -34,12 +34,30 @@ export function recommended_openspec_install_plan(opts = {}) {
34
34
  }
35
35
  return null;
36
36
  }
37
+ export function forced_openspec_install_plan(plan) {
38
+ const versionedPackage = `${OPENSPEC_NPM_PACKAGE}@latest`;
39
+ const forced = {
40
+ npm: ["install", "-g", "--force", versionedPackage],
41
+ pnpm: ["add", "-g", "--force", versionedPackage],
42
+ yarn: ["global", "add", versionedPackage, "--force"],
43
+ bun: ["add", "-g", "--force", versionedPackage],
44
+ };
45
+ return { ...plan, args: forced[plan.manager], rendered: renderCommand(plan.cmd, forced[plan.manager]) };
46
+ }
37
47
  export function missing_openspec_cli_message(opts = {}) {
48
+ return openspec_cli_requirement_message({
49
+ ok: false,
50
+ state: "missing",
51
+ version: null,
52
+ message: "PATH 中缺少 OpenSpec CLI(openspec)",
53
+ }, opts);
54
+ }
55
+ export function openspec_cli_requirement_message(probe, opts = {}) {
38
56
  const plan = recommended_openspec_install_plan(opts);
39
57
  if (plan) {
40
- return `PATH 中缺少 openspec CLI。可先运行 \`${plan.rendered}\` 安装,然后重新运行 superspec init --scope project。`;
58
+ return `${probe.message}。需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}。可先运行 \`${plan.rendered}\` 安装或升级,然后重新运行 \`superspec init --scope project\`。`;
41
59
  }
42
- return `PATH 中缺少 openspec CLI。请先安装 OpenSpec CLI(${OPENSPEC_INSTALL_DOC_URL}),然后重新运行 superspec init --scope project。`;
60
+ return `${probe.message}。请先安装或升级 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}(${OPENSPEC_INSTALL_DOC_URL}),然后重新运行 \`superspec init --scope project\`。`;
43
61
  }
44
62
  function openspecSkillProblems(repoRoot) {
45
63
  const skillsRoot = join(repoRoot, ".codex", "skills");
@@ -64,8 +82,8 @@ function writeSuperSpecAgent(repoRoot, name) {
64
82
  'model_reasoning_effort = "high"',
65
83
  'developer_instructions = """',
66
84
  `你是仓库本地的 superspec ${name} native subagent。`,
67
- "遵循分配给你的 superspec gate 证据任务,引用具体文件,并把阻塞点上报主线程。",
68
- "不要用主线程自审替代必须的角色证据。",
85
+ "遵循分配给你的 superspec gate 证据任务,引用具体文件,并把未通过的问题上报主流程。",
86
+ "不要用主流程自审替代必须的角色证据。",
69
87
  '"""',
70
88
  "",
71
89
  ].join("\n"), "utf8");
@@ -82,14 +100,15 @@ function writeSuperSpecPrompt(repoRoot, name) {
82
100
  "",
83
101
  `你是仓库本地的 superspec ${name} 角色。`,
84
102
  "",
85
- "请基于具体文件证据审查提供的 superspec gate 上下文,输出简洁的通过 / 阻塞报告,引用 source anchors 与 target refs,并且不要用主线程自审替代必须的 native-subagent 证据。",
103
+ "请基于具体文件证据审查提供的 superspec gate 上下文,输出简洁的通过 / 未通过报告,引用 source anchors 与 target refs,并且不要用主流程自审替代必须的 native-subagent 证据。",
86
104
  "",
87
105
  ].join("\n"), "utf8");
88
106
  return filePath;
89
107
  }
90
108
  function ensureOpenSpecCodex(repoRoot, actions) {
91
- if (!commandExists("openspec", { cwd: repoRoot }))
92
- return [missing_openspec_cli_message({ cwd: repoRoot })];
109
+ const probe = openspec_cli_probe({ cwd: repoRoot });
110
+ if (!probe.ok)
111
+ return [openspec_cli_requirement_message(probe, { cwd: repoRoot })];
93
112
  let problems = openspecSkillProblems(repoRoot);
94
113
  if (problems.length === 0) {
95
114
  actions.push({ action: "openspec_codex_skills", status: "ok" });
@@ -0,0 +1,14 @@
1
+ import { runCommand } from "./util.ts";
2
+ type RunFn = typeof runCommand;
3
+ type CommandExistsFn = (cmd: string, meta?: {
4
+ cwd?: string;
5
+ }) => boolean;
6
+ export declare function update_self_then_rerun(opts: {
7
+ args: string[];
8
+ cwd?: string;
9
+ run?: RunFn;
10
+ commandExistsFn?: CommandExistsFn;
11
+ writeStdout?: (text: string) => void;
12
+ writeStderr?: (text: string) => void;
13
+ }): number;
14
+ export {};
@@ -0,0 +1,56 @@
1
+ import { commandExists, runCommand } from "./util.js";
2
+ const SUPERSPEC_NPM_PACKAGE = "@peterxiaoyang/superspec";
3
+ function renderCommand(cmd, args) {
4
+ return [cmd, ...args].join(" ");
5
+ }
6
+ function commandFailure(proc) {
7
+ const output = (proc.error?.message ?? (proc.stderr || proc.stdout)).trim();
8
+ if (output)
9
+ return output;
10
+ return proc.status === null ? "command failed" : `command exited with status ${proc.status}`;
11
+ }
12
+ function shouldRetrySuperSpecInstallWithForce(proc) {
13
+ const output = `${proc.error?.message ?? ""}\n${proc.stderr}\n${proc.stdout}`;
14
+ const binConflict = /\bEEXIST\b|already exists|file exists|Refusing to delete|will not overwrite|would overwrite/iu.test(output);
15
+ const superspecBin = /\bsuperspec(?:\.(?:cmd|ps1))?\b/iu.test(output);
16
+ return binConflict && superspecBin;
17
+ }
18
+ export function update_self_then_rerun(opts) {
19
+ const cwd = opts.cwd ?? process.cwd();
20
+ const run = opts.run ?? runCommand;
21
+ const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
22
+ const writeStdout = opts.writeStdout ?? ((text) => process.stdout.write(text));
23
+ const writeStderr = opts.writeStderr ?? ((text) => process.stderr.write(text));
24
+ if (!commandExistsFn("npm", { cwd })) {
25
+ writeStderr("SuperSpec update requires npm on PATH to install @peterxiaoyang/superspec@latest. Use `superspec update --local-only` to update surfaces from the currently installed package.\n");
26
+ return 1;
27
+ }
28
+ const installArgs = ["install", "-g", `${SUPERSPEC_NPM_PACKAGE}@latest`];
29
+ writeStderr(`Updating SuperSpec CLI: ${renderCommand("npm", installArgs)}\n`);
30
+ let install = run("npm", installArgs, { cwd, timeout: 300_000 });
31
+ if ((install.error || install.status !== 0) && shouldRetrySuperSpecInstallWithForce(install)) {
32
+ const forcedArgs = ["install", "-g", "--force", `${SUPERSPEC_NPM_PACKAGE}@latest`];
33
+ writeStderr(`SuperSpec CLI install hit a global bin conflict; retrying with: ${renderCommand("npm", forcedArgs)}\n`);
34
+ install = run("npm", forcedArgs, { cwd, timeout: 300_000 });
35
+ }
36
+ if (install.error || install.status !== 0) {
37
+ writeStderr(`SuperSpec CLI update failed: ${commandFailure(install)}\n`);
38
+ return 1;
39
+ }
40
+ if (!commandExistsFn("superspec", { cwd })) {
41
+ writeStderr("SuperSpec CLI update completed, but `superspec` is not resolvable from PATH. Reopen the shell or check npm global bin configuration.\n");
42
+ return 1;
43
+ }
44
+ const rerunArgs = ["update", ...opts.args, "--skip-self-update"];
45
+ const rerun = run("superspec", rerunArgs, { cwd, timeout: 300_000 });
46
+ if (rerun.stdout)
47
+ writeStdout(rerun.stdout);
48
+ if (rerun.stderr)
49
+ writeStderr(rerun.stderr);
50
+ if (rerun.error || rerun.status !== 0) {
51
+ if (rerun.error && !rerun.stderr)
52
+ writeStderr(`SuperSpec update rerun failed: ${rerun.error.message}\n`);
53
+ return rerun.status ?? 1;
54
+ }
55
+ return 0;
56
+ }
@@ -127,6 +127,7 @@ export declare function commandLookupInvocation(cmd: string, platform?: NodeJS.P
127
127
  args: string[];
128
128
  shell: boolean;
129
129
  };
130
+ export declare function selectWindowsCommandCandidate(cmd: string, whereStdout: string): string;
130
131
  export declare function windowsShellEscapeArg(arg: string): string;
131
132
  export declare function windowsCmdShimInvocation(cmdPath: string, args: string[], comspec?: string): {
132
133
  cmd: string;
package/dist/src/util.js CHANGED
@@ -78,6 +78,7 @@ export const EVIDENCE_KINDS = new Set([
78
78
  // FIX-8 (audit A-5) adds the previously anchor-less human pause points:
79
79
  // apply isolation choice, apply-phase scope expansion, and verify-failure disposition.
80
80
  export const HUMAN_CONFIRMATION_GATES = new Set([
81
+ "explore_complete",
81
82
  "design_complete",
82
83
  "invariants_reviewed",
83
84
  "archive_ready",
@@ -215,8 +216,8 @@ export const GATE_ALIASES = {
215
216
  "propose.invariants_reviewed": "invariants_reviewed",
216
217
  "propose.test_plan_drafted": "test_contract_drafted",
217
218
  "propose.tasks_mapped": "tasks_complete",
218
- "propose.apply_ready": "propose_complete",
219
- apply_ready: "propose_complete",
219
+ "propose.apply_ready": "apply_ready",
220
+ apply_ready: "apply_ready",
220
221
  };
221
222
  export const GATE_ROUTE = {
222
223
  explore_complete: "explore",
@@ -227,6 +228,7 @@ export const GATE_ROUTE = {
227
228
  test_contract_honored: "propose",
228
229
  tasks_complete: "propose",
229
230
  propose_complete: "propose",
231
+ apply_ready: "propose",
230
232
  review_complete: "review",
231
233
  verify_complete: "review",
232
234
  archive_ready: "archive",
@@ -287,6 +289,10 @@ export function decorateDecision(decision, opts = {}) {
287
289
  })
288
290
  : [];
289
291
  const nextActions = Array.isArray(decision.next_allowed_actions) ? decision.next_allowed_actions.map((item) => String(item)) : [];
292
+ const windowsPowerShellHints = windowsPowerShellCommandHints([
293
+ ...nextActions,
294
+ ...reasons.map((item) => String(item.message ?? "")),
295
+ ]);
290
296
  return {
291
297
  ...decision,
292
298
  command: command || decision.command,
@@ -297,10 +303,23 @@ export function decorateDecision(decision, opts = {}) {
297
303
  command_hint_zh: commandInfo?.hint_zh,
298
304
  block_reasons: reasons,
299
305
  next_allowed_actions_zh: nextActions.map((item) => translate_action_zh(item)),
306
+ windows_powershell_command_hints: windowsPowerShellHints.length > 0 ? windowsPowerShellHints : undefined,
300
307
  trust_warnings_zh: Array.isArray(decision.trust_warnings) ? decision.trust_warnings.map((item) => trust_warning_zh(String(item))) : [],
301
308
  workflow_terms_zh: workflow_terms_zh_for(command || undefined, String(decision.gate ?? ""), reasons.map((item) => String(item.code ?? ""))),
302
309
  };
303
310
  }
311
+ function windowsPowerShellCommandHints(texts) {
312
+ const hints = new Set();
313
+ for (const text of texts) {
314
+ for (const match of text.matchAll(/`((?:superspec|openspec)\s+[^`]+)`/giu)) {
315
+ const command = match[1]?.trim();
316
+ if (!command)
317
+ continue;
318
+ hints.add(`Windows PowerShell: use \`${command.replace(/^(superspec|openspec)\b/iu, "$1.cmd")}\``);
319
+ }
320
+ }
321
+ return [...hints];
322
+ }
304
323
  function sanitizeReasonForOutput(item) {
305
324
  const refs = Array.isArray(item.refs) ? item.refs.map((ref) => String(ref)) : [];
306
325
  return {
@@ -475,6 +494,10 @@ export function commandLookupInvocation(cmd, platform = process.platform) {
475
494
  return { cmd: "where.exe", args: [cmd], shell: false };
476
495
  return { cmd: "sh", args: ["-c", `command -v ${cmd}`], shell: false };
477
496
  }
497
+ export function selectWindowsCommandCandidate(cmd, whereStdout) {
498
+ const candidates = whereStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
499
+ return candidates.find((candidate) => /\.(?:cmd|bat)$/i.test(candidate)) ?? candidates[0] ?? cmd;
500
+ }
478
501
  function resolveWindowsCommand(cmd, cwd) {
479
502
  if (cmd.includes("\\") || cmd.includes("/") || extname(cmd))
480
503
  return cmd;
@@ -485,7 +508,7 @@ function resolveWindowsCommand(cmd, cwd) {
485
508
  });
486
509
  if (lookup.status !== 0 || typeof lookup.stdout !== "string")
487
510
  return cmd;
488
- return lookup.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? cmd;
511
+ return selectWindowsCommandCandidate(cmd, lookup.stdout);
489
512
  }
490
513
  export function windowsShellEscapeArg(arg) {
491
514
  let escaped = String(arg);
@@ -497,7 +520,7 @@ export function windowsShellEscapeArg(arg) {
497
520
  export function windowsCmdShimInvocation(cmdPath, args, comspec = "cmd.exe") {
498
521
  return {
499
522
  cmd: comspec,
500
- args: ["/d", "/s", "/c", [windowsShellEscapeArg(cmdPath), ...args.map(windowsShellEscapeArg)].join(" ")],
523
+ args: ["/d", "/c", cmdPath, ...args],
501
524
  };
502
525
  }
503
526
  function windowsCommandInvocation(cmd, args, cwd) {
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  export * from "./src/init_cli.ts";
3
3
  export * from "./src/cli.ts";
4
+ export * from "./src/doctor.ts";
5
+ export * from "./src/self_update.ts";
4
6
  export declare function main_superspec(argv?: string[]): Promise<number>;
package/dist/superspec.js CHANGED
@@ -3,8 +3,12 @@ import { realpathSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  export * from "./src/init_cli.js";
5
5
  export * from "./src/cli.js";
6
+ export * from "./src/doctor.js";
7
+ export * from "./src/self_update.js";
6
8
  import { main } from "./src/cli.js";
9
+ import { main_doctor, superspec_package_version } from "./src/doctor.js";
7
10
  import { main_init_async } from "./src/init_cli.js";
11
+ import { update_self_then_rerun } from "./src/self_update.js";
8
12
  function realpathMaybe(filePath) {
9
13
  try {
10
14
  return realpathSync(filePath);
@@ -19,14 +23,34 @@ function help() {
19
23
  "",
20
24
  "commands:",
21
25
  " init install SuperSpec Codex surfaces (asks project/user; default project)",
22
- " update update manifest-managed SuperSpec surfaces",
26
+ " update update SuperSpec CLI, then update manifest-managed surfaces",
23
27
  " uninstall remove manifest-managed SuperSpec surfaces",
24
28
  " guard run the SuperSpec guard command surface",
29
+ " doctor diagnose SuperSpec/OpenSpec/npm/PATH wiring",
30
+ " version print SuperSpec CLI version",
25
31
  "",
26
32
  "examples:",
27
33
  " superspec init --scope project",
28
34
  " superspec init --scope user",
29
35
  " superspec guard check-init --change <change>",
36
+ " superspec doctor",
37
+ "",
38
+ ].join("\n");
39
+ }
40
+ function updateHelp() {
41
+ return [
42
+ "usage: superspec update [--scope {project,user}] [--path PATH] [--codex-home PATH] [--local-only]",
43
+ "",
44
+ "updates the global SuperSpec CLI from npm, then updates manifest-managed SuperSpec surfaces.",
45
+ "",
46
+ "options:",
47
+ " --scope {project,user} update project .codex surfaces or user Codex home surfaces (default: project)",
48
+ " --project equivalent to --scope project",
49
+ " --user, --global equivalent to --scope user",
50
+ " --path PATH project root for project scope (default: current directory)",
51
+ " --codex-home PATH Codex user home for user scope (default: $CODEX_HOME or ~/.codex)",
52
+ " --local-only skip npm self-update and use the currently installed package",
53
+ " -h, --help show this help",
30
54
  "",
31
55
  ].join("\n");
32
56
  }
@@ -36,14 +60,29 @@ export async function main_superspec(argv = process.argv.slice(2)) {
36
60
  process.stdout.write(help());
37
61
  return 0;
38
62
  }
63
+ if (command === "-v" || command === "--version" || command === "version") {
64
+ process.stdout.write(`${superspec_package_version()}\n`);
65
+ return 0;
66
+ }
39
67
  if (command === "init")
40
68
  return main_init_async(rest);
41
- if (command === "update")
42
- return main_init_async([...rest, "--update"]);
69
+ if (command === "update") {
70
+ if (rest.includes("-h") || rest.includes("--help")) {
71
+ process.stdout.write(updateHelp());
72
+ return 0;
73
+ }
74
+ const localOnly = rest.includes("--local-only") || rest.includes("--skip-self-update");
75
+ const updateArgs = rest.filter((arg) => arg !== "--local-only" && arg !== "--skip-self-update");
76
+ if (!localOnly)
77
+ return update_self_then_rerun({ args: updateArgs });
78
+ return main_init_async([...updateArgs, "--update"]);
79
+ }
43
80
  if (command === "uninstall")
44
81
  return main_init_async([...rest, "--uninstall"]);
45
82
  if (command === "guard")
46
83
  return main(rest);
84
+ if (command === "doctor")
85
+ return main_doctor(rest);
47
86
  // Convenience fallback: `superspec check-init ...` behaves like `superspec guard check-init ...`.
48
87
  if (command.startsWith("check-") || command === "status" || command === "recompute" || command === "init") {
49
88
  return main(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "SuperSpec workflow package: guard runtime, generic workflow templates, and Codex adapter payload.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,7 +51,7 @@
51
51
  "scripts": {
52
52
  "build": "node build.js",
53
53
  "typecheck": "tsc --noEmit",
54
- "test": "node --test tests/test_install_engine.test.ts tests/test_real_openspec_smoke.test.ts tests/test_superspec_guard.test.ts tests/test_superspec_skills.test.ts",
54
+ "test": "node --test tests/test_install_engine.test.ts tests/test_real_openspec_smoke.test.ts tests/test_superspec_cli.test.ts tests/test_superspec_guard.test.ts tests/test_superspec_skills.test.ts",
55
55
  "prepack": "npm run build",
56
56
  "prepublishOnly": "npm run build",
57
57
  "pack:dry-run": "npm pack --dry-run"
@@ -40,7 +40,7 @@ argument-hint: "任务说明"
40
40
  - 建议必须具体且可执行。
41
41
  - 必须说明取舍。
42
42
  - 在 ralplan 共识审查中,要包含反论、张力和综合方案。
43
- - 在 `superspec-review` 中,要输出基于来源证据的架构 guidance 和升级点;最终裁决由主线程完成,不由本角色直接下判。
43
+ - 在 `superspec-review` 中,要输出基于来源证据的架构 guidance 和升级点;最终判断由主流程完成,不由本角色直接下判。
44
44
  </success_criteria>
45
45
 
46
46
  <verification_loop>
@@ -78,7 +78,7 @@ argument-hint: "任务说明"
78
78
  1. [最高优先级] - [工作量] - [影响]
79
79
  2. [下一优先级] - [工作量] - [影响]
80
80
 
81
- ## 主线程裁决建议
81
+ ## 主流程判断建议
82
82
  - 关键架构判断
83
83
  - 建议直接加载的 source refs
84
84
  - 建议升级或后续动作
@@ -6,7 +6,7 @@ argument-hint: "任务说明"
6
6
  你是 Code Reviewer。你的任务是通过系统化、带严重级别的审查来保障代码质量与安全性。
7
7
  你负责规格符合性验证、安全检查、代码质量评估、性能审视和最佳实践约束。
8
8
  你不负责直接实现修复(executor)、架构设计(architect)或编写测试(test-engineer)。
9
- 当你在 `superspec-review` 中与 `architect` / `critic` 配合时,你负责代码 / 规格 / 安全这一条审查线,需要产出带证据的 guidance 供主线程裁决,而不是自己充当最终判官。
9
+ 当你在 `superspec-review` 中与 `architect` / `critic` 配合时,你负责代码 / 规格 / 安全这一条审查线,需要产出带证据的 guidance,供主流程做最终判断,而不是自己充当最终判官。
10
10
 
11
11
  代码审查是缺陷和漏洞进入生产前的最后一道防线。之所以强调这些规则,是因为漏掉安全问题会造成真实损害,而只盯格式细枝末节会浪费所有人的时间。
12
12
  </identity>
@@ -39,8 +39,8 @@ argument-hint: "任务说明"
39
39
  <explore>
40
40
  1) 先跑 `git diff` 看最近改动,重点关注被修改的文件。
41
41
  2) 阶段 1:规格符合性(必须先通过)。检查实现是否覆盖全部要求,是否解决了正确的问题,是否有缺漏或多做,需求提出者会不会认得这是他要的东西。
42
- 3) 根因守卫(在正常质量放行前必须通过):如果新引入的 fallback / workaround 会掩盖故障、压掉证据、增加宽泛绕路,或回避修主合同,就直接驳回。要求作者回到根因修复:保留失败证据、收紧主合同、删除掩盖分支,并补上真正故障的回归覆盖。
43
- 4) 阶段 2:代码质量(只有阶段 1 和根因守卫都通过后才做)。对每个修改文件运行 `lsp_diagnostics`。使用 `ast_grep_search` 检查高风险模式,例如 `console.log`、空 `catch`、硬编码密钥、宽泛 `try/catch` fallback、静默默认值、尽力而为式绕路。然后按安全、质量、性能、最佳实践清单审查。
42
+ 3) 根因检查(在正常质量放行前必须通过):如果新引入的 fallback / workaround 会掩盖故障、压掉证据、增加宽泛绕路,或回避修主合同,就直接驳回。要求作者回到根因修复:保留失败证据、收紧主合同、删除掩盖分支,并补上真正故障的回归覆盖。
43
+ 4) 阶段 2:代码质量(只有阶段 1 和根因检查都通过后才做)。对每个修改文件运行 `lsp_diagnostics`。使用 `ast_grep_search` 检查高风险模式,例如 `console.log`、空 `catch`、硬编码密钥、宽泛 `try/catch` fallback、静默默认值、尽力而为式绕路。然后按安全、质量、性能、最佳实践清单审查。
44
44
  5) 给每个问题评严重级别,并给出修复建议。
45
45
  6) 根据最高严重级别得出总体结论。
46
46
  </explore>
@@ -53,7 +53,7 @@ argument-hint: "任务说明"
53
53
  - 每个问题都包含明确修复建议。
54
54
  - 所有修改文件都已运行 `lsp_diagnostics`,不能在有类型错误时放行。
55
55
  - guidance 包必须清晰:包括 findings、source refs、required claim ids 和建议的下一步。
56
- - 在 superspec review 中,架构问题要向 `architect` 上抛,最终裁决留给主线程。
56
+ - 在 superspec review 中,架构问题要向 `architect` 上抛,最终判断留给主流程。
57
57
  </success_criteria>
58
58
 
59
59
  <verification_loop>
@@ -71,7 +71,7 @@ argument-hint: "任务说明"
71
71
 
72
72
  <root_cause_fallback_policy>
73
73
  - 当 fallback / workaround 会掩盖真实缺陷时,要把它当成审查阻塞项:比如吞错、降级诊断、静默默认值、宽泛兼容垫片、重复的备用执行路径、绕开损坏主路径的功能开关,或没有证明主合同被修好却让故障“消失”的尽力分支。
74
- - 对这类掩盖式补丁,即使测试通过也要给出 REQUEST CHANGES。要明确说明:只要补丁压掉证据或绕开失败合同,单纯“能跑通”就不够;要求最小化的根因修复、明确的失败行为,以及没有真实修复就会失败的回归测试。
74
+ - 对这类掩盖式问题,即使测试通过也要给出 REQUEST CHANGES。要明确说明:只要问题压掉证据或绕开失败合同,单纯“能跑通”就不够;要求最小化的根因修复、明确的失败行为,以及没有真实修复就会失败的回归测试。
75
75
  - 不要无差别否定所有 fallback。若 fallback 明确说明为不可避免、被限制在已知外部/版本边界内、主路径与 fallback 路径都经过测试、失败证据仍然可见,并且没有替代可控主合同的修复,那么窄范围兼容 fallback 可以接受。
76
76
  - 需要细腻判断时,要把条件写清楚:例如“只有当这个 fallback 始终限制在 [boundary]、保持 [evidence/error] 可见,并且同时覆盖 [primary] 与 [compatibility] 行为测试时,才可以接受。”否则就建议删除 fallback / workaround,回到根因修复。
77
77
  </root_cause_fallback_policy>
@@ -114,7 +114,7 @@ argument-hint: "任务说明"
114
114
 
115
115
  ### 主线程建议
116
116
  - 推荐下一步
117
- - 需要主线程裁决的 claims
117
+ - 需要主流程判断的 claims
118
118
  - 建议主线程直接加载的 source refs
119
119
  </output_contract>
120
120
 
@@ -124,7 +124,7 @@ argument-hint: "任务说明"
124
124
  - 没有证据:没跑 `lsp_diagnostics` 就说 “looks good”。必须对修改文件跑诊断。
125
125
  - 问题描述含糊:比如只说“这里可以更好”。应改成类似:`[MEDIUM] utils.ts:42 - 函数超过 50 行,建议把 42-65 行的校验逻辑提取到 validateInput()。`
126
126
  - 严重级别膨胀:把缺失 JSDoc 评成 CRITICAL。CRITICAL 只留给安全漏洞和数据损坏风险。
127
- - 纵容掩盖式补丁:看到用 fallback、静默默认值、宽泛绕路去掩盖主路径故障却仍然放行。应要求回到根因修复,并补回归证据。
127
+ - 纵容掩盖式问题:看到用 fallback、静默默认值、宽泛绕路去掩盖主路径故障却仍然放行。应要求回到根因修复,并补回归证据。
128
128
  </anti_patterns>
129
129
 
130
130
  <scenario_handling>
@@ -142,7 +142,7 @@ argument-hint: "任务说明"
142
142
  - 我是否拦下了会掩盖故障或绕开根因修复的 fallback / workaround?
143
143
  - 我是否对所有修改文件都运行了 lsp_diagnostics?
144
144
  - 每个问题是否都有 file:line、严重级别和修复建议?
145
- - 我是否给主线程留下了足够证据,使其无需盲信我也能裁决?
145
+ - 我是否给主流程留下了足够证据,使其无需盲信我也能判断?
146
146
  - 我是否检查了安全问题(硬编码密钥、注入、XSS)?
147
147
  </final_checklist>
148
148
  </style>
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "计划与方案的对抗审查角色(深度)"
2
+ description: "计划与方案的严格审查角色(深度)"
3
3
  argument-hint: "任务说明"
4
4
  ---
5
5
  <identity>
@@ -7,7 +7,7 @@ argument-hint: "任务说明"
7
7
  </identity>
8
8
 
9
9
  <goal>
10
- 针对计划,要审查清晰度、完整性、验证方式、整体适配性、引用文件以及代表性实现路径。在 `superspec-review` 中,你输出带证据的 guidance、required claims 与 required loads,交给主线程裁决,而不是自己做最终判定。
10
+ 针对计划,要审查清晰度、完整性、验证方式、整体适配性、引用文件以及代表性实现路径。在 `superspec-review` 中,你输出带证据的 guidance、required claims 与 required loads,交给主流程做最终判断,而不是自己做最终判定。
11
11
  </goal>
12
12
 
13
13
  <language>
@@ -14,7 +14,7 @@ argument-hint: "任务说明"
14
14
  </language>
15
15
 
16
16
  <goal>
17
- 通过检查代码、diff、命令输出、诊断、测试、工件和验收口径,把 claim 变成可复现的证明,或明确的证明缺口。缺少证据不是通过;最终裁决仍由主线程负责。
17
+ 通过检查代码、diff、命令输出、诊断、测试、工件和验收口径,把 claim 变成可复现的证明,或明确的证明缺口。缺少证据不是通过;最终判断仍由主流程负责。
18
18
  </goal>
19
19
 
20
20
  <constraints>