@peterxiaoyang/superspec 0.1.0 → 0.1.1

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,25 @@
1
+ import { runCommand } 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
+ confirm?: (question: string) => Promise<boolean>;
20
+ run?: typeof runCommand;
21
+ writeStderr?: (text: string) => void;
22
+ }): Promise<"not-needed" | "installed" | "skipped" | "failed">;
1
23
  export declare function main_init(argv?: string[]): number;
2
24
  export declare function main_init_async(argv?: string[]): Promise<number>;
25
+ 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 } from "./core.js";
2
+ import { 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,14 @@ 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
+ }
50
59
  function engineDecision(gate, projectRoot, result, nextActions) {
51
60
  if (result.problems.length > 0) {
52
61
  const decision = block("project", gate, result.problems.map((item) => reason(`${gate}_failed`, item)));
@@ -68,12 +77,12 @@ async function promptInstallScope() {
68
77
  const rl = createInterface({ input: process.stdin, output: process.stderr });
69
78
  try {
70
79
  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")
80
+ const answer = (await rl.question("请选择安装范围:project 还是 user[project] ")).trim().toLowerCase();
81
+ if (answer === "" || answer === "project" || answer === "p" || answer === "1" || answer === "项目")
73
82
  return "project";
74
- if (answer === "user" || answer === "u" || answer === "2" || answer === "global" || answer === "g")
83
+ if (answer === "user" || answer === "u" || answer === "2" || answer === "global" || answer === "g" || answer === "用户")
75
84
  return "user";
76
- process.stderr.write("Please answer project or user.\n");
85
+ process.stderr.write("请输入 project user。\n");
77
86
  }
78
87
  }
79
88
  finally {
@@ -83,6 +92,56 @@ async function promptInstallScope() {
83
92
  function canPrompt() {
84
93
  return Boolean(process.stdin.isTTY && process.stderr.isTTY);
85
94
  }
95
+ async function promptYesNo(question, defaultYes = true) {
96
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
97
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
98
+ try {
99
+ for (;;) {
100
+ const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
101
+ if (answer === "")
102
+ return defaultYes;
103
+ if (["y", "yes", "是", "好", "确认"].includes(answer))
104
+ return true;
105
+ if (["n", "no", "否", "不", "取消"].includes(answer))
106
+ return false;
107
+ process.stderr.write("请输入 y / n,或输入 是 / 否。\n");
108
+ }
109
+ }
110
+ finally {
111
+ rl.close();
112
+ }
113
+ }
114
+ export async function maybe_install_missing_openspec(opts) {
115
+ const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
116
+ if (opts.scope !== "project" || opts.mode !== "install")
117
+ return "not-needed";
118
+ if (commandExistsFn("openspec", { cwd: opts.cwd }))
119
+ return "not-needed";
120
+ if (!(opts.interactive ?? canPrompt()))
121
+ return "skipped";
122
+ const plan = recommended_openspec_install_plan({ cwd: opts.cwd, commandExistsFn });
123
+ if (plan === null)
124
+ return "skipped";
125
+ const confirm = opts.confirm ?? ((question) => promptYesNo(question));
126
+ const accepted = await confirm(`未检测到 openspec CLI。是否现在尝试自动安装?\n将执行:${plan.rendered}`);
127
+ if (!accepted)
128
+ return "skipped";
129
+ const writeStderr = opts.writeStderr ?? ((text) => {
130
+ process.stderr.write(text);
131
+ });
132
+ writeStderr(`正在安装 OpenSpec CLI:${plan.rendered}\n`);
133
+ const proc = (opts.run ?? runCommand)(plan.cmd, plan.args, { cwd: opts.cwd, timeout: 300_000 });
134
+ if (proc.error || proc.status !== 0) {
135
+ writeStderr(`自动安装 OpenSpec CLI 失败:${commandFailure(proc)}\n`);
136
+ return "failed";
137
+ }
138
+ if (!commandExistsFn("openspec", { cwd: opts.cwd })) {
139
+ writeStderr("安装命令已完成,但当前 PATH 里仍未检测到 `openspec`。请重新打开终端或确认全局 bin 已在 PATH 中,然后重新运行 superspec init。\n");
140
+ return "failed";
141
+ }
142
+ writeStderr("OpenSpec CLI 安装完成,继续执行 superspec init。\n");
143
+ return "installed";
144
+ }
86
145
  function run_init(args, scope) {
87
146
  const targetRoot = scope === "user" ? args.codexHome : args.path;
88
147
  const gatePrefix = scope === "user" ? "user" : "project";
@@ -107,7 +166,7 @@ function run_init(args, scope) {
107
166
  }
108
167
  summary.install_scope = scope;
109
168
  summary.install_root = targetRoot;
110
- printDecision(summary);
169
+ printDecision(summary, { command: "init" });
111
170
  return summary.allowed ? 0 : 1;
112
171
  }
113
172
  export function main_init(argv = process.argv.slice(2)) {
@@ -122,7 +181,7 @@ export function main_init(argv = process.argv.slice(2)) {
122
181
  catch (err) {
123
182
  const change = "project";
124
183
  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]));
184
+ printDecision(block(change, "guard_error", [errReason]), { command: "init" });
126
185
  return 2;
127
186
  }
128
187
  }
@@ -134,12 +193,13 @@ export async function main_init_async(argv = process.argv.slice(2)) {
134
193
  }
135
194
  const args = parse_init_argv(argv);
136
195
  const scope = args.scope ?? (canPrompt() ? await promptInstallScope() : "project");
196
+ await maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
137
197
  return run_init(args, scope);
138
198
  }
139
199
  catch (err) {
140
200
  const change = "project";
141
201
  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]));
202
+ printDecision(block(change, "guard_error", [errReason]), { command: "init" });
143
203
  return 2;
144
204
  }
145
205
  }
@@ -1,4 +1,24 @@
1
1
  import { type JsonMap } 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 missing_openspec_cli_message(opts?: {
17
+ cwd?: string;
18
+ commandExistsFn?: (cmd: string, meta?: {
19
+ cwd?: string;
20
+ }) => boolean;
21
+ }): string;
2
22
  export declare function project_init(repoRootRaw?: string, opts?: {
3
23
  force?: boolean;
4
24
  }): JsonMap;
@@ -2,15 +2,44 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "no
2
2
  import { join, resolve } from "node:path";
3
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";
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 missing_openspec_cli_message(opts = {}) {
38
+ const plan = recommended_openspec_install_plan(opts);
39
+ if (plan) {
40
+ return `PATH 中缺少 openspec CLI。可先运行 \`${plan.rendered}\` 安装,然后重新运行 superspec init --scope project。`;
41
+ }
42
+ return `PATH 中缺少 openspec CLI。请先安装 OpenSpec CLI(${OPENSPEC_INSTALL_DOC_URL}),然后重新运行 superspec init --scope project。`;
14
43
  }
15
44
  function openspecSkillProblems(repoRoot) {
16
45
  const skillsRoot = join(repoRoot, ".codex", "skills");
@@ -34,9 +63,9 @@ function writeSuperSpecAgent(repoRoot, name) {
34
63
  `description = "${ROLE_DESCRIPTIONS[name] ?? `superspec ${name} role`}"`,
35
64
  'model_reasoning_effort = "high"',
36
65
  '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.",
66
+ `你是仓库本地的 superspec ${name} native subagent。`,
67
+ "遵循分配给你的 superspec gate 证据任务,引用具体文件,并把阻塞点上报主线程。",
68
+ "不要用主线程自审替代必须的角色证据。",
40
69
  '"""',
41
70
  "",
42
71
  ].join("\n"), "utf8");
@@ -51,16 +80,16 @@ function writeSuperSpecPrompt(repoRoot, name) {
51
80
  'argument-hint: "superspec gate evidence task"',
52
81
  "---",
53
82
  "",
54
- `You are the repo-local superspec ${name} role.`,
83
+ `你是仓库本地的 superspec ${name} 角色。`,
55
84
  "",
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.",
85
+ "请基于具体文件证据审查提供的 superspec gate 上下文,输出简洁的通过 / 阻塞报告,引用 source anchors target refs,并且不要用主线程自审替代必须的 native-subagent 证据。",
57
86
  "",
58
87
  ].join("\n"), "utf8");
59
88
  return filePath;
60
89
  }
61
90
  function ensureOpenSpecCodex(repoRoot, actions) {
62
91
  if (!commandExists("openspec", { cwd: repoRoot }))
63
- return ["openspec CLI is not available in PATH"];
92
+ return [missing_openspec_cli_message({ cwd: repoRoot })];
64
93
  let problems = openspecSkillProblems(repoRoot);
65
94
  if (problems.length === 0) {
66
95
  actions.push({ action: "openspec_codex_skills", status: "ok" });
@@ -71,10 +100,10 @@ function ensureOpenSpecCodex(repoRoot, actions) {
71
100
  action: "openspec init --tools codex .",
72
101
  status: init.status === 0 ? "updated" : "failed",
73
102
  refs: problems,
74
- detail: init.status === 0 ? undefined : (init.stderr || init.stdout).trim(),
103
+ detail: init.status === 0 ? undefined : commandFailure(init),
75
104
  });
76
105
  if (init.error || init.status !== 0)
77
- return [`openspec init failed: ${commandFailure(init)}`];
106
+ return [`openspec init 执行失败:${commandFailure(init)}`];
78
107
  problems = openspecSkillProblems(repoRoot);
79
108
  if (problems.length === 0)
80
109
  return [];
@@ -83,12 +112,12 @@ function ensureOpenSpecCodex(repoRoot, actions) {
83
112
  action: "openspec update --force .",
84
113
  status: update.status === 0 ? "updated" : "failed",
85
114
  refs: problems,
86
- detail: update.status === 0 ? undefined : (update.stderr || update.stdout).trim(),
115
+ detail: update.status === 0 ? undefined : commandFailure(update),
87
116
  });
88
117
  if (update.error || update.status !== 0)
89
- return [`openspec update failed: ${commandFailure(update)}`];
118
+ return [`openspec update 执行失败:${commandFailure(update)}`];
90
119
  problems = openspecSkillProblems(repoRoot);
91
- return problems.length === 0 ? [] : [`OpenSpec Codex skills remain missing/invalid: ${problems.join(", ")}`];
120
+ return problems.length === 0 ? [] : [`OpenSpec 配套技能文件仍然缺失或无效:${problems.join(", ")}`];
92
121
  }
93
122
  function ensureSuperSpecRoles(repoRoot, actions) {
94
123
  const problems = [];
@@ -99,14 +128,14 @@ function ensureSuperSpecRoles(repoRoot, actions) {
99
128
  created.push(writeSuperSpecAgent(repoRoot, name));
100
129
  }
101
130
  else if (read_agent_toml_name(agentPath) !== name) {
102
- problems.push(`invalid agent ${agentPath}`);
131
+ problems.push(`agent 文件无效:${agentPath}`);
103
132
  }
104
133
  const promptPath = join(repoRoot, ".codex", "prompts", `${name}.md`);
105
134
  if (!existsSync(promptPath) || !statSync(promptPath).isFile()) {
106
135
  created.push(writeSuperSpecPrompt(repoRoot, name));
107
136
  }
108
137
  else if (!readFileSync(promptPath, "utf8").trim()) {
109
- problems.push(`empty prompt ${promptPath}`);
138
+ problems.push(`prompt 文件为空:${promptPath}`);
110
139
  }
111
140
  }
112
141
  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;
package/dist/src/util.js CHANGED
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
3
3
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
+ import { action_detail_zh, action_label_zh, action_status_zh, command_zh, decision_zh, gate_zh, reason_message_zh, reason_zh, translate_action_zh, trust_warning_zh, workflow_terms_zh_for } from "./i18n.js";
5
6
  export const SCHEMA_VERSION = 1;
6
7
  export const GUARD_VERSION = "superspec-guard@1";
7
8
  export const CONFIG_FILENAME = "config.yaml";
@@ -234,7 +235,8 @@ export class GuardError extends Error {
234
235
  }
235
236
  export const runtime = {};
236
237
  export function reason(code, message, refs = null) {
237
- return { code, message, refs: refs ?? [] };
238
+ const zh = reason_zh(code);
239
+ return { code, message, refs: refs ?? [], label_zh: zh.label_zh, hint_zh: zh.hint_zh };
238
240
  }
239
241
  export function pinned_ref_key(item) {
240
242
  return `${String(item.path)}\u0000${String(item.blob_sha)}`;
@@ -273,8 +275,85 @@ export function block(change, gate, reasons, opts = {}) {
273
275
  trust_warnings: trustWarnings(),
274
276
  };
275
277
  }
276
- export function printDecision(decision) {
277
- process.stdout.write(`${JSON.stringify(decision, null, 2)}\n`);
278
+ export function decorateDecision(decision, opts = {}) {
279
+ const gateInfo = gate_zh(String(decision.gate ?? ""));
280
+ const command = opts.command ?? (typeof decision.command === "string" ? String(decision.command) : "");
281
+ const commandInfo = command ? command_zh(command) : null;
282
+ const reasons = Array.isArray(decision.block_reasons)
283
+ ? decision.block_reasons.map((item) => {
284
+ const base = item && typeof item === "object" ? { ...item } : { code: String(item), message: "", refs: [] };
285
+ const zh = reason_zh(String(base.code ?? ""));
286
+ return { ...base, label_zh: zh.label_zh, hint_zh: zh.hint_zh };
287
+ })
288
+ : [];
289
+ const nextActions = Array.isArray(decision.next_allowed_actions) ? decision.next_allowed_actions.map((item) => String(item)) : [];
290
+ return {
291
+ ...decision,
292
+ command: command || decision.command,
293
+ decision_zh: decision_zh(String(decision.decision ?? "")),
294
+ gate_label_zh: gateInfo.label_zh,
295
+ gate_hint_zh: gateInfo.hint_zh,
296
+ command_label_zh: commandInfo?.label_zh,
297
+ command_hint_zh: commandInfo?.hint_zh,
298
+ block_reasons: reasons,
299
+ next_allowed_actions_zh: nextActions.map((item) => translate_action_zh(item)),
300
+ trust_warnings_zh: Array.isArray(decision.trust_warnings) ? decision.trust_warnings.map((item) => trust_warning_zh(String(item))) : [],
301
+ workflow_terms_zh: workflow_terms_zh_for(command || undefined, String(decision.gate ?? ""), reasons.map((item) => String(item.code ?? ""))),
302
+ };
303
+ }
304
+ function sanitizeReasonForOutput(item) {
305
+ const refs = Array.isArray(item.refs) ? item.refs.map((ref) => String(ref)) : [];
306
+ return {
307
+ ...item,
308
+ refs,
309
+ message: String(item.message ?? ""),
310
+ message_zh: reason_message_zh(String(item.code ?? ""), String(item.message ?? ""), refs),
311
+ };
312
+ }
313
+ function sanitizeDecisionForOutput(decision) {
314
+ const reasons = Array.isArray(decision.block_reasons) ? decision.block_reasons.map((item) => sanitizeReasonForOutput(item)) : [];
315
+ const nextActions = Array.isArray(decision.next_allowed_actions)
316
+ ? decision.next_allowed_actions.map((item) => String(item))
317
+ : [];
318
+ const nextActionsZh = Array.isArray(decision.next_allowed_actions_zh)
319
+ ? decision.next_allowed_actions_zh.map((item) => String(item))
320
+ : [];
321
+ const trustWarnings = Array.isArray(decision.trust_warnings)
322
+ ? decision.trust_warnings.map((item) => String(item))
323
+ : [];
324
+ const trustWarningsZh = Array.isArray(decision.trust_warnings_zh)
325
+ ? decision.trust_warnings_zh.map((item) => String(item))
326
+ : [];
327
+ const actions = Array.isArray(decision.actions)
328
+ ? decision.actions.map((item) => {
329
+ const base = item && typeof item === "object" ? { ...item } : { action: String(item) };
330
+ const action = String(base.action ?? "");
331
+ const status = typeof base.status === "string" ? String(base.status) : "";
332
+ const detail = typeof base.detail === "string" ? String(base.detail) : "";
333
+ return {
334
+ ...base,
335
+ action,
336
+ status: status || base.status,
337
+ detail: detail || base.detail,
338
+ action_zh: action_label_zh(action),
339
+ status_zh: status ? action_status_zh(status) : undefined,
340
+ detail_zh: detail ? action_detail_zh(detail) : undefined,
341
+ };
342
+ })
343
+ : decision.actions;
344
+ return {
345
+ ...decision,
346
+ block_reasons: reasons,
347
+ next_allowed_actions: nextActions,
348
+ next_allowed_actions_zh: nextActionsZh,
349
+ trust_warnings: trustWarnings,
350
+ trust_warnings_zh: trustWarningsZh,
351
+ actions,
352
+ };
353
+ }
354
+ export function printDecision(decision, opts = {}) {
355
+ const decorated = decorateDecision(decision, opts);
356
+ process.stdout.write(`${JSON.stringify(sanitizeDecisionForOutput(decorated), null, 2)}\n`);
278
357
  }
279
358
  export function runCommand(cmd, args, opts = {}) {
280
359
  const platform = opts.platform ?? process.platform;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "SuperSpec workflow package: guard runtime, generic workflow templates, and Codex adapter payload.",
5
5
  "repository": {
6
6
  "type": "git",