@peterxiaoyang/superspec 0.1.0 → 0.1.2

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,2 +1,27 @@
1
+ import { runCommand, type OpenspecCliProbe } from "./core.ts";
2
+ import { type InstallScope } from "./install_engine.ts";
3
+ type InitArgs = {
4
+ path: string;
5
+ codexHome: string;
6
+ mode: "install" | "update" | "uninstall";
7
+ scope: InstallScope | null;
8
+ dryRun: boolean;
9
+ force: boolean;
10
+ };
11
+ export declare function maybe_install_missing_openspec(opts: {
12
+ cwd: string;
13
+ scope: InstallScope;
14
+ mode: InitArgs["mode"];
15
+ interactive?: boolean;
16
+ commandExistsFn?: (cmd: string, meta?: {
17
+ cwd?: string;
18
+ }) => boolean;
19
+ probeOpenspecFn?: (meta?: {
20
+ cwd?: string;
21
+ }) => OpenspecCliProbe;
22
+ run?: typeof runCommand;
23
+ writeStderr?: (text: string) => void;
24
+ }): "not-needed" | "installed" | "skipped" | "failed";
1
25
  export declare function main_init(argv?: string[]): number;
2
26
  export declare function main_init_async(argv?: string[]): Promise<number>;
27
+ export {};
@@ -1,14 +1,15 @@
1
- import { block, GuardError, printDecision, reason } from "./core.js";
2
- import { project_init } from "./project_init.js";
1
+ import { block, commandExists, GuardError, printDecision, reason, runCommand, openspec_cli_probe, REQUIRED_OPENSPEC_MIN_VERSION, } from "./core.js";
2
+ import { forced_openspec_install_plan, project_init, recommended_openspec_install_plan } from "./project_init.js";
3
3
  import { install_workflow, uninstall_workflow, update_workflow } from "./install_engine.js";
4
4
  import { homedir } from "node:os";
5
5
  import { resolve } from "node:path";
6
6
  import { createInterface } from "node:readline/promises";
7
+ import { system_failure_zh } from "./i18n.js";
7
8
  function usage() {
8
9
  return "usage: superspec init [-h] [--scope {project,user}] [--path PATH] [--codex-home PATH] [--create] [--update] [--uninstall] [--dry-run] [--force]\n";
9
10
  }
10
11
  function help() {
11
- return `${usage()}\noptional arguments:\n -h, --help show this help message and exit\n --scope {project,user} install into this project's .codex directory or into the Codex user home (default: project)\n --project shortcut for --scope project\n --user shortcut for --scope user\n --global compatibility alias for --user\n --path PATH project root for --scope project (default: current directory)\n --codex-home PATH Codex user home for --scope user (default: $CODEX_HOME or ~/.codex)\n --create accepted for compatibility; init creates missing surfaces by default\n --update manifest-driven update of managed SuperSpec surfaces (user-modified files kept, new version written to *.new)\n --uninstall manifest-driven removal of managed SuperSpec surfaces (.superspec data and preexisting/user-modified files are kept)\n --dry-run with --uninstall: print what would be removed without touching files\n --force with install: overwrite pre-existing different files (backs up *.bak)\n`;
12
+ return `${usage()}\n可选参数:\n -h, --help 显示帮助并退出\n --scope {project,user} 安装到当前项目的 .codex 目录,或安装到用户级 Codex 目录(默认:project)\n --project 等价于 --scope project\n --user 等价于 --scope user\n --global 兼容别名,等价于 --user\n --path PATH --scope project 时使用的项目根目录(默认:当前目录)\n --codex-home PATH --scope user 时使用的 Codex 用户目录(默认:$CODEX_HOME ~/.codex)\n --create 兼容参数;init 默认就会创建缺失内容\n --update manifest 更新 SuperSpec 管理的文件;用户改动文件保留,新的版本写入 *.new\n --uninstall manifest 卸载 SuperSpec 管理的文件;.superspec 数据与既有/用户改动文件会保留\n --dry-run 配合 --uninstall 时只预览将删除的文件,不实际修改\n --force 安装时覆盖已有且内容不同的文件,并保留 *.bak 备份\n`;
12
13
  }
13
14
  function parse_init_argv(argv) {
14
15
  const getValue = (flag) => {
@@ -17,19 +18,19 @@ function parse_init_argv(argv) {
17
18
  return undefined;
18
19
  const value = argv[idx + 1];
19
20
  if (value === undefined || value.startsWith("--"))
20
- throw new GuardError(`${flag} requires a value`);
21
+ throw new GuardError(`${flag} 缺少取值`);
21
22
  return value;
22
23
  };
23
24
  const update = argv.includes("--update");
24
25
  const uninstall = argv.includes("--uninstall");
25
26
  if (update && uninstall)
26
- throw new GuardError("--update and --uninstall are mutually exclusive");
27
+ throw new GuardError("--update --uninstall 不能同时使用");
27
28
  const scopeFlag = getValue("--scope");
28
29
  if (scopeFlag !== undefined && !["project", "user", "global"].includes(scopeFlag))
29
- throw new GuardError("--scope must be project or user");
30
+ throw new GuardError("--scope 只能是 project user");
30
31
  const userShortcut = argv.includes("--user") || argv.includes("--global");
31
32
  if (userShortcut && argv.includes("--project"))
32
- throw new GuardError("--user/--global and --project are mutually exclusive");
33
+ throw new GuardError("--user/--global --project 不能同时使用");
33
34
  let scope = scopeFlag === "global" ? "user" : scopeFlag ?? null;
34
35
  if (userShortcut)
35
36
  scope = "user";
@@ -47,6 +48,20 @@ function parse_init_argv(argv) {
47
48
  function joinHomeCodex() {
48
49
  return `${homedir()}/.codex`;
49
50
  }
51
+ function commandFailure(proc) {
52
+ const output = (proc.error?.message ?? (proc.stderr || proc.stdout)).trim();
53
+ if (output)
54
+ return system_failure_zh(output, `安装命令执行失败(退出状态码 ${proc.status ?? "未知"})。`);
55
+ if (proc.status !== null)
56
+ return `安装命令执行失败(退出状态码 ${proc.status})。`;
57
+ return "安装命令执行失败,请查看终端日志后重试。";
58
+ }
59
+ function shouldRetryOpenSpecInstallWithForce(proc) {
60
+ const output = `${proc.error?.message ?? ""}\n${proc.stderr}\n${proc.stdout}`;
61
+ const binConflict = /\bEEXIST\b|already exists|file exists|Refusing to delete|will not overwrite|would overwrite/iu.test(output);
62
+ const openspecBin = /\bopenspec(?:\.(?:cmd|ps1))?\b/iu.test(output);
63
+ return binConflict && openspecBin;
64
+ }
50
65
  function engineDecision(gate, projectRoot, result, nextActions) {
51
66
  if (result.problems.length > 0) {
52
67
  const decision = block("project", gate, result.problems.map((item) => reason(`${gate}_failed`, item)));
@@ -68,12 +83,12 @@ async function promptInstallScope() {
68
83
  const rl = createInterface({ input: process.stdin, output: process.stderr });
69
84
  try {
70
85
  for (;;) {
71
- const answer = (await rl.question("Install SuperSpec Codex surfaces to project or user? [project] ")).trim().toLowerCase();
72
- if (answer === "" || answer === "project" || answer === "p" || answer === "1")
86
+ const answer = (await rl.question("请选择安装范围:project 还是 user[project] ")).trim().toLowerCase();
87
+ if (answer === "" || answer === "project" || answer === "p" || answer === "1" || answer === "项目")
73
88
  return "project";
74
- if (answer === "user" || answer === "u" || answer === "2" || answer === "global" || answer === "g")
89
+ if (answer === "user" || answer === "u" || answer === "2" || answer === "global" || answer === "g" || answer === "用户")
75
90
  return "user";
76
- process.stderr.write("Please answer project or user.\n");
91
+ process.stderr.write("请输入 project user。\n");
77
92
  }
78
93
  }
79
94
  finally {
@@ -83,6 +98,60 @@ async function promptInstallScope() {
83
98
  function canPrompt() {
84
99
  return Boolean(process.stdin.isTTY && process.stderr.isTTY);
85
100
  }
101
+ export function maybe_install_missing_openspec(opts) {
102
+ const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
103
+ if (opts.mode !== "install")
104
+ return "not-needed";
105
+ const run = opts.run ?? runCommand;
106
+ const probeOpenspecFn = opts.probeOpenspecFn ?? ((meta) => openspec_cli_probe({ cwd: meta?.cwd, commandExistsFn, run }));
107
+ const before = probeOpenspecFn({ cwd: opts.cwd });
108
+ if (before.ok)
109
+ return "not-needed";
110
+ const writeStderr = opts.writeStderr ?? ((text) => {
111
+ process.stderr.write(text);
112
+ });
113
+ const plan = recommended_openspec_install_plan({ cwd: opts.cwd, commandExistsFn });
114
+ if (plan === null) {
115
+ writeStderr(`${before.message}。SuperSpec 需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION},但未找到 npm、pnpm、yarn 或 bun,无法自动安装。\n`);
116
+ return "skipped";
117
+ }
118
+ writeStderr(`${before.message}。SuperSpec 需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION},将自动安装或升级。\n`);
119
+ writeStderr(`正在安装 OpenSpec CLI:${plan.rendered}\n`);
120
+ let proc = run(plan.cmd, plan.args, { cwd: opts.cwd, timeout: 300_000 });
121
+ if ((proc.error || proc.status !== 0) && shouldRetryOpenSpecInstallWithForce(proc)) {
122
+ const forcedPlan = forced_openspec_install_plan(plan);
123
+ writeStderr(`OpenSpec CLI 安装遇到全局 bin 冲突,正在覆盖重试:${forcedPlan.rendered}\n`);
124
+ proc = run(forcedPlan.cmd, forcedPlan.args, { cwd: opts.cwd, timeout: 300_000 });
125
+ }
126
+ if (proc.error || proc.status !== 0) {
127
+ writeStderr(`自动安装 OpenSpec CLI 失败:${commandFailure(proc)}\n`);
128
+ return "failed";
129
+ }
130
+ const after = probeOpenspecFn({ cwd: opts.cwd });
131
+ if (!after.ok) {
132
+ writeStderr(`安装命令已完成,但当前 PATH 里的 openspec 仍不可用:${after.message}。请重新打开终端或确认全局 bin 已在 PATH 中,然后重新运行 superspec init;Windows PowerShell 请运行 superspec.cmd init。\n`);
133
+ return "failed";
134
+ }
135
+ writeStderr("OpenSpec CLI 安装或升级完成,继续执行 superspec init。\n");
136
+ return "installed";
137
+ }
138
+ function openspecPreflightBlocked(args, scope, installResult) {
139
+ const targetRoot = scope === "user" ? args.codexHome : args.path;
140
+ const code = installResult === "skipped" ? "openspec_auto_install_unavailable" : "openspec_auto_install_failed";
141
+ const message = installResult === "skipped"
142
+ ? `OpenSpec CLI 不满足 SuperSpec 要求,且未找到可用包管理器自动安装 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}。`
143
+ : `OpenSpec CLI 自动安装或升级 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION} 未完成。`;
144
+ const decision = block("project", "openspec_preflight", [reason(code, message)], {
145
+ next_actions: [`install or upgrade @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}, then rerun \`superspec init --scope ${scope}\``],
146
+ });
147
+ printDecision({
148
+ ...decision,
149
+ project_root: args.path,
150
+ install_scope: scope,
151
+ install_root: targetRoot,
152
+ }, { command: "init" });
153
+ return 1;
154
+ }
86
155
  function run_init(args, scope) {
87
156
  const targetRoot = scope === "user" ? args.codexHome : args.path;
88
157
  const gatePrefix = scope === "user" ? "user" : "project";
@@ -107,7 +176,7 @@ function run_init(args, scope) {
107
176
  }
108
177
  summary.install_scope = scope;
109
178
  summary.install_root = targetRoot;
110
- printDecision(summary);
179
+ printDecision(summary, { command: "init" });
111
180
  return summary.allowed ? 0 : 1;
112
181
  }
113
182
  export function main_init(argv = process.argv.slice(2)) {
@@ -117,12 +186,16 @@ export function main_init(argv = process.argv.slice(2)) {
117
186
  return 0;
118
187
  }
119
188
  const args = parse_init_argv(argv);
120
- return run_init(args, args.scope ?? "project");
189
+ const scope = args.scope ?? "project";
190
+ const openspecInstall = maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
191
+ if (openspecInstall === "failed" || openspecInstall === "skipped")
192
+ return openspecPreflightBlocked(args, scope, openspecInstall);
193
+ return run_init(args, scope);
121
194
  }
122
195
  catch (err) {
123
196
  const change = "project";
124
197
  const errReason = err instanceof GuardError ? reason("guard_error", err.message) : reason("guard_internal_error", `${err.name}: ${err.message}`);
125
- printDecision(block(change, "guard_error", [errReason]));
198
+ printDecision(block(change, "guard_error", [errReason]), { command: "init" });
126
199
  return 2;
127
200
  }
128
201
  }
@@ -134,12 +207,15 @@ export async function main_init_async(argv = process.argv.slice(2)) {
134
207
  }
135
208
  const args = parse_init_argv(argv);
136
209
  const scope = args.scope ?? (canPrompt() ? await promptInstallScope() : "project");
210
+ const openspecInstall = maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
211
+ if (openspecInstall === "failed" || openspecInstall === "skipped")
212
+ return openspecPreflightBlocked(args, scope, openspecInstall);
137
213
  return run_init(args, scope);
138
214
  }
139
215
  catch (err) {
140
216
  const change = "project";
141
217
  const errReason = err instanceof GuardError ? reason("guard_error", err.message) : reason("guard_internal_error", `${err.name}: ${err.message}`);
142
- printDecision(block(change, "guard_error", [errReason]));
218
+ printDecision(block(change, "guard_error", [errReason]), { command: "init" });
143
219
  return 2;
144
220
  }
145
221
  }
@@ -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,31 @@
1
- import { type JsonMap } from "./core.ts";
1
+ import { type JsonMap, type OpenspecCliProbe } from "./core.ts";
2
+ export declare const OPENSPEC_NPM_PACKAGE = "@fission-ai/openspec";
3
+ export declare const OPENSPEC_INSTALL_DOC_URL = "https://github.com/Fission-AI/OpenSpec#readme";
4
+ export type OpenspecInstallPlan = {
5
+ manager: "npm" | "pnpm" | "yarn" | "bun";
6
+ cmd: string;
7
+ args: string[];
8
+ rendered: string;
9
+ };
10
+ export declare function recommended_openspec_install_plan(opts?: {
11
+ cwd?: string;
12
+ commandExistsFn?: (cmd: string, meta?: {
13
+ cwd?: string;
14
+ }) => boolean;
15
+ }): OpenspecInstallPlan | null;
16
+ export declare function forced_openspec_install_plan(plan: OpenspecInstallPlan): OpenspecInstallPlan;
17
+ export declare function missing_openspec_cli_message(opts?: {
18
+ cwd?: string;
19
+ commandExistsFn?: (cmd: string, meta?: {
20
+ cwd?: string;
21
+ }) => boolean;
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;
2
29
  export declare function project_init(repoRootRaw?: string, opts?: {
3
30
  force?: boolean;
4
31
  }): JsonMap;
@@ -1,16 +1,63 @@
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
+ import { system_failure_zh } from "./i18n.js";
6
+ export const OPENSPEC_NPM_PACKAGE = "@fission-ai/openspec";
7
+ export const OPENSPEC_INSTALL_DOC_URL = "https://github.com/Fission-AI/OpenSpec#readme";
5
8
  const ROLE_DESCRIPTIONS = {
6
- architect: "System design, boundaries, interfaces, and long-horizon tradeoffs",
7
- critic: "Critical review of plans, evidence, assumptions, and scope drift",
8
- "test-engineer": "Test strategy, coverage, and RED/GREEN evidence review",
9
- "code-reviewer": "Code/spec/security review lane for the code-review workflow",
10
- verifier: "Final completion evidence and verification review",
9
+ architect: "系统设计、边界、接口与长期取舍",
10
+ critic: "严格审查计划、证据、假设与范围漂移",
11
+ "test-engineer": "测试策略、覆盖率与 RED/GREEN 证据审查",
12
+ "code-reviewer": "代码 / 规格 / 安全审查",
13
+ verifier: "最终完成证据与验证审查",
11
14
  };
12
15
  function commandFailure(proc) {
13
- return (proc.error?.message ?? (proc.stderr || proc.stdout)).trim();
16
+ return system_failure_zh((proc.error?.message ?? (proc.stderr || proc.stdout)).trim(), proc.status !== null && proc.status !== undefined ? `命令执行失败(退出状态码 ${proc.status})。` : "命令执行失败,请查看终端日志后重试。");
17
+ }
18
+ function renderCommand(cmd, args) {
19
+ return [cmd, ...args].join(" ");
20
+ }
21
+ export function recommended_openspec_install_plan(opts = {}) {
22
+ const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
23
+ const versionedPackage = `${OPENSPEC_NPM_PACKAGE}@latest`;
24
+ const candidates = [
25
+ { manager: "npm", cmd: "npm", args: ["install", "-g", versionedPackage] },
26
+ { manager: "pnpm", cmd: "pnpm", args: ["add", "-g", versionedPackage] },
27
+ { manager: "yarn", cmd: "yarn", args: ["global", "add", versionedPackage] },
28
+ { manager: "bun", cmd: "bun", args: ["add", "-g", versionedPackage] },
29
+ ];
30
+ for (const candidate of candidates) {
31
+ if (!commandExistsFn(candidate.cmd, { cwd: opts.cwd }))
32
+ continue;
33
+ return { ...candidate, rendered: renderCommand(candidate.cmd, candidate.args) };
34
+ }
35
+ return null;
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
+ }
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 = {}) {
56
+ const plan = recommended_openspec_install_plan(opts);
57
+ if (plan) {
58
+ return `${probe.message}。需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}。可先运行 \`${plan.rendered}\` 安装或升级,然后重新运行 \`superspec init --scope project\`。`;
59
+ }
60
+ return `${probe.message}。请先安装或升级 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}(${OPENSPEC_INSTALL_DOC_URL}),然后重新运行 \`superspec init --scope project\`。`;
14
61
  }
15
62
  function openspecSkillProblems(repoRoot) {
16
63
  const skillsRoot = join(repoRoot, ".codex", "skills");
@@ -34,9 +81,9 @@ function writeSuperSpecAgent(repoRoot, name) {
34
81
  `description = "${ROLE_DESCRIPTIONS[name] ?? `superspec ${name} role`}"`,
35
82
  'model_reasoning_effort = "high"',
36
83
  'developer_instructions = """',
37
- `You are the repo-local superspec ${name} native subagent.`,
38
- "Follow the assigned superspec gate evidence task, cite concrete files, and report blockers upward.",
39
- "Do not substitute main-thread self-review for required role evidence.",
84
+ `你是仓库本地的 superspec ${name} native subagent。`,
85
+ "遵循分配给你的 superspec gate 证据任务,引用具体文件,并把未通过的问题上报主流程。",
86
+ "不要用主流程自审替代必须的角色证据。",
40
87
  '"""',
41
88
  "",
42
89
  ].join("\n"), "utf8");
@@ -51,16 +98,17 @@ function writeSuperSpecPrompt(repoRoot, name) {
51
98
  'argument-hint: "superspec gate evidence task"',
52
99
  "---",
53
100
  "",
54
- `You are the repo-local superspec ${name} role.`,
101
+ `你是仓库本地的 superspec ${name} 角色。`,
55
102
  "",
56
- "Review the provided superspec gate context with concrete file-backed evidence. Produce a concise pass/block report, cite source anchors and target refs, and do not replace required native-subagent evidence with main-thread self-review.",
103
+ "请基于具体文件证据审查提供的 superspec gate 上下文,输出简洁的通过 / 未通过报告,引用 source anchors target refs,并且不要用主流程自审替代必须的 native-subagent 证据。",
57
104
  "",
58
105
  ].join("\n"), "utf8");
59
106
  return filePath;
60
107
  }
61
108
  function ensureOpenSpecCodex(repoRoot, actions) {
62
- if (!commandExists("openspec", { cwd: repoRoot }))
63
- return ["openspec CLI is not available in PATH"];
109
+ const probe = openspec_cli_probe({ cwd: repoRoot });
110
+ if (!probe.ok)
111
+ return [openspec_cli_requirement_message(probe, { cwd: repoRoot })];
64
112
  let problems = openspecSkillProblems(repoRoot);
65
113
  if (problems.length === 0) {
66
114
  actions.push({ action: "openspec_codex_skills", status: "ok" });
@@ -71,10 +119,10 @@ function ensureOpenSpecCodex(repoRoot, actions) {
71
119
  action: "openspec init --tools codex .",
72
120
  status: init.status === 0 ? "updated" : "failed",
73
121
  refs: problems,
74
- detail: init.status === 0 ? undefined : (init.stderr || init.stdout).trim(),
122
+ detail: init.status === 0 ? undefined : commandFailure(init),
75
123
  });
76
124
  if (init.error || init.status !== 0)
77
- return [`openspec init failed: ${commandFailure(init)}`];
125
+ return [`openspec init 执行失败:${commandFailure(init)}`];
78
126
  problems = openspecSkillProblems(repoRoot);
79
127
  if (problems.length === 0)
80
128
  return [];
@@ -83,12 +131,12 @@ function ensureOpenSpecCodex(repoRoot, actions) {
83
131
  action: "openspec update --force .",
84
132
  status: update.status === 0 ? "updated" : "failed",
85
133
  refs: problems,
86
- detail: update.status === 0 ? undefined : (update.stderr || update.stdout).trim(),
134
+ detail: update.status === 0 ? undefined : commandFailure(update),
87
135
  });
88
136
  if (update.error || update.status !== 0)
89
- return [`openspec update failed: ${commandFailure(update)}`];
137
+ return [`openspec update 执行失败:${commandFailure(update)}`];
90
138
  problems = openspecSkillProblems(repoRoot);
91
- return problems.length === 0 ? [] : [`OpenSpec Codex skills remain missing/invalid: ${problems.join(", ")}`];
139
+ return problems.length === 0 ? [] : [`OpenSpec 配套技能文件仍然缺失或无效:${problems.join(", ")}`];
92
140
  }
93
141
  function ensureSuperSpecRoles(repoRoot, actions) {
94
142
  const problems = [];
@@ -99,14 +147,14 @@ function ensureSuperSpecRoles(repoRoot, actions) {
99
147
  created.push(writeSuperSpecAgent(repoRoot, name));
100
148
  }
101
149
  else if (read_agent_toml_name(agentPath) !== name) {
102
- problems.push(`invalid agent ${agentPath}`);
150
+ problems.push(`agent 文件无效:${agentPath}`);
103
151
  }
104
152
  const promptPath = join(repoRoot, ".codex", "prompts", `${name}.md`);
105
153
  if (!existsSync(promptPath) || !statSync(promptPath).isFile()) {
106
154
  created.push(writeSuperSpecPrompt(repoRoot, name));
107
155
  }
108
156
  else if (!readFileSync(promptPath, "utf8").trim()) {
109
- problems.push(`empty prompt ${promptPath}`);
157
+ problems.push(`prompt 文件为空:${promptPath}`);
110
158
  }
111
159
  }
112
160
  actions.push({
@@ -1,3 +1,4 @@
1
+ import { type WorkflowTermHint } from "./i18n.ts";
1
2
  export declare const SCHEMA_VERSION = 1;
2
3
  export declare const GUARD_VERSION = "superspec-guard@1";
3
4
  export declare const CONFIG_FILENAME = "config.yaml";
@@ -12,6 +13,9 @@ export type Reason = {
12
13
  code: string;
13
14
  message: string;
14
15
  refs: string[];
16
+ label_zh?: string;
17
+ hint_zh?: string;
18
+ message_zh?: string;
15
19
  };
16
20
  export type Decision = {
17
21
  allowed: boolean;
@@ -24,6 +28,16 @@ export type Decision = {
24
28
  block_reasons: Reason[];
25
29
  next_allowed_actions: string[];
26
30
  trust_warnings: string[];
31
+ actions?: JsonMap[];
32
+ decision_zh?: string;
33
+ gate_label_zh?: string;
34
+ gate_hint_zh?: string;
35
+ command?: string;
36
+ command_label_zh?: string;
37
+ command_hint_zh?: string;
38
+ next_allowed_actions_zh?: string[];
39
+ trust_warnings_zh?: string[];
40
+ workflow_terms_zh?: WorkflowTermHint[];
27
41
  };
28
42
  export type TaskInfo = {
29
43
  task_id: string;
@@ -82,7 +96,12 @@ export declare function block(change: string, gate: string, reasons: Reason[], o
82
96
  gate_summary?: Record<string, any>;
83
97
  next_actions?: string[];
84
98
  }): Decision;
85
- export declare function printDecision(decision: JsonMap): void;
99
+ export declare function decorateDecision(decision: JsonMap, opts?: {
100
+ command?: string;
101
+ }): JsonMap;
102
+ export declare function printDecision(decision: JsonMap, opts?: {
103
+ command?: string;
104
+ }): void;
86
105
  export declare function runCommand(cmd: string, args: string[], opts?: {
87
106
  cwd?: string;
88
107
  timeout?: number;
@@ -108,6 +127,7 @@ export declare function commandLookupInvocation(cmd: string, platform?: NodeJS.P
108
127
  args: string[];
109
128
  shell: boolean;
110
129
  };
130
+ export declare function selectWindowsCommandCandidate(cmd: string, whereStdout: string): string;
111
131
  export declare function windowsShellEscapeArg(arg: string): string;
112
132
  export declare function windowsCmdShimInvocation(cmdPath: string, args: string[], comspec?: string): {
113
133
  cmd: string;