agents-gitflow-guard 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,7 +42,7 @@ You define your own branches —
42
42
  # installs the latest release
43
43
  dsh plugin --profile web add agents-gitflow-guard
44
44
  # ...or pin an exact known-good version (recommended; also bypasses stale registry caches)
45
- dsh plugin --profile web add agents-gitflow-guard@0.0.9
45
+ dsh plugin --profile web add agents-gitflow-guard@0.0.12
46
46
  ```
47
47
 
48
48
  > **Version gotcha**: a bare `add` resolves whatever `latest` is at install time — on machines behind a stale npm/pnpm registry cache or mirror it may install an old version. If the installed version looks wrong, pin it explicitly. The peer-dependency *warning* pnpm may print is expected: DSH supplies `@deepseek-ai/cordis` / `@deepseek-ai/dsh-tools` through its shared profile module fallback at startup (the plugin works normally).
@@ -256,7 +256,7 @@ archive (optional; you archive after release)
256
256
  "production": { "branches": ["prd"], "update": "pr", "mergeBy": "user" }, // optional
257
257
  "archive": ["main"] // optional
258
258
  },
259
- "locale": "en", // optional: message language ('en' default, or 'zh')
259
+ "locale": "en", // optional: message language — any registered locale ('en'/'zh' built-in); unknown values warn in status and fall back to English
260
260
  "strict": false, // optional: fail-closed — invalid config / internal errors block instead of warn-and-allow
261
261
  "ci": { "enabled": true } // optional: gh pr checks logged as reference
262
262
  }
@@ -265,8 +265,10 @@ archive (optional; you archive after release)
265
265
  - Roles accept either an **array** (shorthand) or an **object** `{ branches, update?, mergeBy? }`.
266
266
  - `update`: `pr` (default) = updates only via PR/MR; `flexible` = allow direct/local merges (small teams).
267
267
  - `mergeBy` (production): `user` (default) = only you click merge; `anyone` = allow PR merge through.
268
- - Each branch entry is an exact name or a regex (auto-detected).
269
- - **Language**: messages are English by default; add `"locale": "zh"` for Chinese.
268
+ - Each branch entry is an exact name or a regex (auto-detected). **Regex safety**: branch patterns are authored by you and compiled as-is — avoid catastrophic-backtracking constructs (e.g. nested quantifiers like `(\w+)+`) in `featurePattern` and branch entries.
269
+ - **Language**: messages are English by default; add `"locale": "zh"` for Chinese, or pass `--locale <en|zh>` to any `gitflow-guard` subcommand (priority: CLI flag > project config > English). All user-facing text follows the locale — including CLI framework messages such as `--help`, unknown-command notices, and the empty-audit line.
270
+ - **Custom locales**: downstream packages can add a language at runtime — `import { registerLocale } from 'agents-gitflow-guard'`, call `registerLocale('fr', frDict)` with a dictionary covering exactly the same keys as built-in English (validated on registration), then set `"locale": "fr"` in the project config to activate it.
271
+ - **Unknown locales**: an unregistered `"locale"` value falls back to English during interception (by design — hooks never stall on wording), so a typo is easy to miss; the one-line warning shows up in `gitflow-guard status`.
270
272
  - **Validation**: `integration` is required; overlapping role entries are rejected; invalid regex is rejected. **Any error disables the plugin for that project** (reported) rather than applying a half-guessed setup.
271
273
  - **Strict mode**: by default a broken config warns on stderr once and lets the command pass (fail-open, so a typo can't wedge your tooling). `"strict": true` flips config errors and internal errors to **block** (fail-closed) — for high-risk repos. A missing file or explicit `enabled: false` stays silent either way.
272
274
 
@@ -301,7 +303,7 @@ The PR/MR target is resolved via `gh pr view` (GitHub) or `glab mr view` (GitLab
301
303
  **From the npm registry** — the standard path, already covered in [Quick Start](#quick-start--30-seconds-to-a-guarded-repo):
302
304
 
303
305
  ```bash
304
- dsh plugin --profile web add agents-gitflow-guard@0.0.9 # pin recommended, see note above
306
+ dsh plugin --profile web add agents-gitflow-guard@0.0.12 # pin recommended, see note above
305
307
  ```
306
308
 
307
309
  Then restart DSH. Upgrades are the same command, followed by another restart.
@@ -421,7 +423,7 @@ The blocks are reserved for: (1) direct writes to protected role branches, and (
421
423
 
422
424
  ### What if my config has a mistake?
423
425
 
424
- The plugin prefers failing closed: any validation error disables the guard for that project and reports the errors, so a half-guessed setup never applies by accident.
426
+ A half-guessed setup is never applied by accident: any validation error disables the guard for that project and reports the errors.
425
427
 
426
428
  Common mistakes: missing `integration` (required), overlapping a branch across two roles (rejected explicitly), and a `featurePattern` that doesn't compile (rejected as invalid regex). The failure is loud and the file is one JSON object, so the fix is usually a thirty-second correction.
427
429
 
@@ -483,7 +485,7 @@ npm install
483
485
  npm test # unit tests: classify / gate / config / cli / repo / platform
484
486
  npm run typecheck # tsc --noEmit, 0 errors
485
487
  npm run build # tsdown → lib/ (CLI and plugin share the build)
486
- npm run verify:matrix # continuous cross-agent regression: DSH logic + Claude Code / Codex / antigravity hook wiring
488
+ npm run verify:matrix # continuous cross-agent regression: DSH logic + zh-locale regression + Claude Code / Codex / OpenCode / Antigravity hook wiring
487
489
  ```
488
490
 
489
491
  **Rule**: any logic change must pass a 0-error build + all green tests + a green `verify:matrix` before done.
package/README.zh.md CHANGED
@@ -41,7 +41,7 @@
41
41
  # 安装最新版
42
42
  dsh plugin --profile web add agents-gitflow-guard
43
43
  # ...或锁定已知良好版本(推荐; 同时绕开 registry 陈旧缓存)
44
- dsh plugin --profile web add agents-gitflow-guard@0.0.9
44
+ dsh plugin --profile web add agents-gitflow-guard@0.0.12
45
45
  ```
46
46
 
47
47
  > **版本坑**: 裸 `add` 装的是安装时刻的 `latest`——在 npm/pnpm 注册表缓存或镜像陈旧的机器上可能拿到旧版本。看到版本不对就锁版本。pnpm 打印的 peer 依赖 *警告* 属预期: DSH 启动时经共享模块回退提供 `@deepseek-ai/cordis` / `@deepseek-ai/dsh-tools`(插件正常工作)。
@@ -253,7 +253,7 @@ archive(可选, 发布后你亲手归档)
253
253
  "production": { "branches": ["prd"], "update": "pr", "mergeBy": "user" }, // 可选
254
254
  "archive": ["main"] // 可选
255
255
  },
256
- "locale": "en", // 可选: 文案语言('en' 默认, 或 'zh')
256
+ "locale": "en", // 可选: 文案语言——任意已注册 locale('en'/'zh' 内置); 未注册值在 status 告警并回退英文
257
257
  "strict": false, // 可选: fail-closed —— 配置异常/内部错误改为拦截, 而非告警放行
258
258
  "ci": { "enabled": true } // 可选: gh pr checks 作参考日志
259
259
  }
@@ -262,8 +262,10 @@ archive(可选, 发布后你亲手归档)
262
262
  - 每个角色既可用**数组**(简写),也可用**对象** `{ branches, update?, mergeBy? }`。
263
263
  - `update`:`pr`(默认)= 只能 PR/MR 合入;`flexible` = 允许直推/本地合入(小团队)。
264
264
  - `mergeBy`(生产):`user`(默认)= 只能你点合并;`anyone` = 放行 PR 合并。
265
- - 每条分支条目是精确名或正则(自动识别)。
266
- - **文案语言**:默认英文;加 `"locale": "zh"` 切中文。
265
+ - 每条分支条目是精确名或正则(自动识别)。**正则安全**:分支正则由项目作者提供并按原样编译——`featurePattern` 与分支条目请避免灾难性回溯写法(如 `(\w+)+` 这类嵌套量词)
266
+ - **文案语言**:默认英文;加 `"locale": "zh"` 切中文,或给任意 `gitflow-guard` 子命令传 `--locale <en|zh>`(优先级:CLI 旗标 > 项目配置 > 英文)。全部用户可见文案都跟随 locale——包括 `--help`、未知子命令提示、审计为空的提示等 CLI 框架文案。
267
+ - **自定义语言**:下游包可在运行时追加语言——`import { registerLocale } from 'agents-gitflow-guard'`,调用 `registerLocale('fr', frDict)` 注册一份与内置英文键完全一致的字典(注册时校验),再在项目配置写 `"locale": "fr"` 即生效。
268
+ - **未注册语言**:拦截路径对未注册的 `"locale"` 静默回退英文(设计如此——hook 不因文案缺失卡死),笔误因此容易被忽略;一行告警在 `gitflow-guard status` 中可见。
267
269
  - **校验**:`integration` 必填;角色条目重叠会被拒;非法正则会报错。**任何错误都会让该项目的插件禁用并上报**(而不是用半吊子配置)。
268
270
  - **strict 模式**:默认配置损坏时 stderr 告警一次后放行(fail-open,避免一个笔误卡死工具管道);`"strict": true` 把配置异常与内部错误翻转为**拦截**(fail-closed)——供高风险仓库选用。文件不存在或显式 `enabled: false` 两种模式下都保持静默。
269
271
 
@@ -298,7 +300,7 @@ PR/MR 目标通过 `gh pr view`(GitHub)或 `glab mr view`(GitLab)解析;没有
298
300
  **从 npm registry**——标准路径,已在[快速开始](#快速开始30-秒用上)覆盖:
299
301
 
300
302
  ```bash
301
- dsh plugin --profile web add agents-gitflow-guard@0.0.9 # 建议锁版本, 见上文提示
303
+ dsh plugin --profile web add agents-gitflow-guard@0.0.12 # 建议锁版本, 见上文提示
302
304
  ```
303
305
 
304
306
  然后重启 DSH。升级用同一命令,再重启一次。
@@ -418,7 +420,7 @@ hooks:
418
420
 
419
421
  ### 配置写错了会怎样?
420
422
 
421
- 插件偏好 fail-closed:任何校验错误都会让该项目的守卫禁用并上报错误,半吊子配置绝不会意外生效。
423
+ 半吊子配置绝不会意外生效:任何校验错误都会让该项目的守卫禁用并上报错误。
422
424
 
423
425
  常见错误:`integration` 缺失(必填)、同一个分支被配到两个角色里(显式拒绝)、`featurePattern` 写不成合法正则(报错)。失败提示很明确,文件又是一个 JSON 对象,通常三十秒改好。
424
426
 
@@ -480,9 +482,10 @@ npm install
480
482
  npm test # 单测: classify / gate / config / cli / repo / platform
481
483
  npm run typecheck # tsc --noEmit, 0 Error
482
484
  npm run build # tsdown → lib/(CLI 与插件共用)
485
+ npm run verify:matrix # 连续复测矩阵: DSH 逻辑 + zh 文案回归 + Claude Code / Codex / OpenCode / Antigravity hook 编码
483
486
  ```
484
487
 
485
- **铁律**:任何逻辑改动必须 0 Error 构建 + 单测全绿后才算完成。
488
+ **铁律**:任何逻辑改动必须 0 Error 构建 + 单测全绿 + 连续复测矩阵(`verify:matrix`)全绿后才算完成。
486
489
 
487
490
  ---
488
491
 
package/lib/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as findRepoRoot, d as resolveLocale, f as loadConfig, i as formatDeny, l as gitRunner, m as classify, o as stateDir, p as roleMatches, r as evaluateCommand, s as currentBranch, u as makeT } from "./src-DTg5WMog.mjs";
1
+ import { c as findRepoRoot, d as roleMatches, f as makeT, h as classify, i as formatDeny, l as gitRunner, m as resolveLocale, o as stateDir, r as evaluateCommand, s as currentBranch, u as loadConfig } from "./src-CPX1Ks_Z.mjs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  //#region src/platform.ts
@@ -94,31 +94,45 @@ function parseFlags(argv) {
94
94
  else if (a === "--lines") flags.lines = Number(next());
95
95
  else if (a === "--platform") flags.platform = next();
96
96
  else if (a === "--command") flags.command = next();
97
+ else if (a === "--locale") flags.locale = next();
97
98
  else if (a.startsWith("--repo=")) flags.repo = a.slice(7);
98
99
  else if (a.startsWith("--lines=")) flags.lines = Number(a.slice(8));
99
100
  else if (a.startsWith("--platform=")) flags.platform = a.slice(11);
100
101
  else if (a.startsWith("--command=")) flags.command = a.slice(10);
102
+ else if (a.startsWith("--locale=")) flags.locale = a.slice(9);
101
103
  }
102
104
  return flags;
103
105
  }
104
- async function resolveRepo(flags) {
106
+ async function resolveRepo(flags, runner) {
105
107
  if (flags.repo) return flags.repo;
106
- return await findRepoRoot(gitRunner, process.cwd());
108
+ return await findRepoRoot(runner, process.cwd());
109
+ }
110
+ /** CLI 文案 locale 解析(P2-1): --locale 旗标 > 项目 config.locale > en; 白名单外一律 en */
111
+ function cliLocale(flags, configLocale) {
112
+ return resolveLocale(flags.locale ?? configLocale);
113
+ }
114
+ /** 无 config 在手时的 locale 解析(help / unknownCommand 等框架路径): 旗标优先, 否则定位仓库读配置 */
115
+ async function resolveFrameworkLocale(flags, runner) {
116
+ if (flags.locale != null) return resolveLocale(flags.locale);
117
+ const repoRoot = await resolveRepo(flags, runner);
118
+ if (!repoRoot) return "en";
119
+ const { config } = await loadConfig(repoRoot);
120
+ return resolveLocale(config?.locale);
107
121
  }
108
122
  async function main(argv, opts = {}) {
109
123
  const runner = opts.runner ?? gitRunner;
110
- const usage = makeT("en")("usage.text");
111
124
  const [cmd, ...rest] = argv;
125
+ const flags = parseFlags(rest);
112
126
  if (cmd === "--help" || cmd === "help" || cmd === void 0) {
113
- console.log(usage);
127
+ console.log(makeT(await resolveFrameworkLocale(flags, runner))("usage.text"));
114
128
  return 0;
115
129
  }
116
- const flags = parseFlags(rest);
117
130
  try {
118
131
  if (cmd === "status") return await status(flags, runner);
119
132
  if (cmd === "audit") return await audit(flags);
120
133
  if (cmd === "check") return await check(flags);
121
- console.error(`${makeT("en")("cli.unknownCommand", { cmd: cmd ?? "" })}\n\n${usage}`);
134
+ const t = makeT(await resolveFrameworkLocale(flags, runner));
135
+ console.error(`${t("cli.unknownCommand", { cmd: cmd ?? "" })}\n\n${t("usage.text")}`);
122
136
  return 1;
123
137
  } catch (e) {
124
138
  console.error(`[gitflow-guard] ${e.message}`);
@@ -126,20 +140,22 @@ async function main(argv, opts = {}) {
126
140
  }
127
141
  }
128
142
  async function status(flags, runner) {
129
- const repoRoot = await resolveRepo(flags);
143
+ const repoRoot = await resolveRepo(flags, runner);
130
144
  if (!repoRoot) {
131
- console.error(makeT("en")("cli.cannotLocate"));
145
+ console.error(makeT(resolveLocale(flags.locale))("cli.cannotLocate"));
132
146
  return 1;
133
147
  }
134
- const { config, errors } = await loadConfig(repoRoot);
148
+ const { config, errors, warnings } = await loadConfig(repoRoot);
135
149
  const enabled = config?.enabled === true;
136
- const t = makeT(resolveLocale(config?.locale));
150
+ const t = makeT(cliLocale(flags, config?.locale));
137
151
  console.log(t("cli.statusTitle", { repo: repoRoot }));
138
152
  if (!enabled) {
139
153
  console.log(t("cli.statusDisabled"));
140
154
  for (const e of errors) console.log(t("cli.statusConfigError", { err: e }));
155
+ for (const w of warnings) console.log(t("cli.statusConfigWarning", { warn: w }));
141
156
  return 0;
142
157
  }
158
+ for (const w of warnings) console.log(t("cli.statusConfigWarning", { warn: w }));
143
159
  const branch = await currentBranch(runner, repoRoot);
144
160
  const c = config;
145
161
  console.log(t("cli.statusEnabled", { pattern: c.featurePattern }));
@@ -177,22 +193,24 @@ async function status(flags, runner) {
177
193
  return 0;
178
194
  }
179
195
  async function audit(flags) {
180
- const repoRoot = await resolveRepo(flags);
196
+ const repoRoot = await resolveRepo(flags, gitRunner);
181
197
  if (!repoRoot) {
182
- console.error(makeT("en")("cli.cannotLocate"));
198
+ console.error(makeT(resolveLocale(flags.locale))("cli.cannotLocate"));
183
199
  return 1;
184
200
  }
201
+ const { config } = await loadConfig(repoRoot);
202
+ const t = makeT(cliLocale(flags, config?.locale));
185
203
  const lines = flags.lines != null && Number.isFinite(flags.lines) && flags.lines > 0 ? Math.floor(flags.lines) : 20;
186
204
  try {
187
205
  const all = (await readFile(join(stateDir(repoRoot), "audit.jsonl"), "utf8")).split("\n").filter(Boolean);
188
206
  for (const line of all.slice(-lines)) try {
189
207
  const e = JSON.parse(line);
190
- console.log(` ${new Date(e.time).toLocaleString()} ${e.event} ${e.role ?? ""}${e.command ? ` | ${e.command.slice(0, 80)}` : ""}${e.reason ? ` | ${e.reason.slice(0, 60)}` : ""}`);
208
+ console.log(` ${new Date(e.time).toISOString()} ${e.event} ${e.role ?? ""}${e.command ? ` | ${e.command.slice(0, 80)}` : ""}${e.reason ? ` | ${e.reason.slice(0, 60)}` : ""}`);
191
209
  } catch {
192
210
  console.log(` ${line}`);
193
211
  }
194
212
  } catch {
195
- console.log(makeT("en")("cli.auditEmpty"));
213
+ console.log(t("cli.auditEmpty"));
196
214
  }
197
215
  return 0;
198
216
  }
@@ -217,8 +235,10 @@ async function check(flags) {
217
235
  const platformFlag = flags.platform ?? "auto";
218
236
  let raw = "";
219
237
  let strict = false;
238
+ let denyPlatform = "claude";
220
239
  try {
221
240
  raw = flags.command != null ? "" : await readStdin();
241
+ denyPlatform = platformFlag === "auto" ? detectPlatform(raw) : platformFlag;
222
242
  const payload = flags.command != null ? {
223
243
  command: flags.command,
224
244
  cwd: flags.repo,
@@ -235,21 +255,23 @@ async function check(flags) {
235
255
  if (!loaded.config?.enabled) {
236
256
  if (loaded.errors.length > 0) {
237
257
  const t = makeT("en");
238
- if (strict) return emitDeny(platformFlag === "auto" ? detectPlatform(raw) : platformFlag, t("guardStrictConfigBroken.why"), t("guardStrictConfigBroken.next"));
258
+ if (strict) return emitDeny(denyPlatform, t("guardStrictConfigBroken.why"), t("guardStrictConfigBroken.next"));
239
259
  process.stderr.write(`${t("cli.guardDisabledInvalidConfig", { err: loaded.errors.join("; ") })}\n`);
240
260
  }
241
261
  return 0;
242
262
  }
243
263
  const config = loaded.config;
244
- const hookPlatform = platformFlag === "auto" ? detectPlatform(raw) : platformFlag;
245
- const locale = resolveLocale(config.locale);
246
- const result = await evaluateCommand(payload.command, { repoRoot });
247
- if (result.outcome === "deny" && result.reason) return emitDeny(hookPlatform, result.reason.why, result.reason.next, locale);
264
+ const locale = flags.locale != null ? resolveLocale(flags.locale) : resolveLocale(config.locale);
265
+ const result = await evaluateCommand(payload.command, {
266
+ repoRoot,
267
+ locale
268
+ });
269
+ if (result.outcome === "deny" && result.reason) return emitDeny(denyPlatform, result.reason.why, result.reason.next, locale);
248
270
  return 0;
249
271
  } catch (e) {
250
272
  if (strict) {
251
273
  const t = makeT("en");
252
- return emitDeny(platformFlag === "auto" ? detectPlatform(raw) : platformFlag, t("guardStrictInternalError.why", { msg: e.message }), t("guardStrictInternalError.next"));
274
+ return emitDeny(denyPlatform, t("guardStrictInternalError.why", { msg: e.message }), t("guardStrictInternalError.next"));
253
275
  }
254
276
  process.stderr.write(`${makeT("en")("cli.checkInternalError", { msg: e.message })}\n`);
255
277
  return 0;
package/lib/index.d.mts CHANGED
@@ -1,8 +1,21 @@
1
1
  import { t as Runner } from "./repo-DrgptHl1.mjs";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
3
  //#region src/types.d.ts
4
- /** 文案语言: 默认 en; 'zh' 切中文 */
5
- type Locale = 'en' | 'zh';
4
+ /** 文案语言: 默认 en; 'zh' 切中文; 可经 registerLocale 运行时扩展(保留字面量提示的宽字符串) */
5
+ type Locale = 'en' | 'zh' | (string & {});
6
+ //#endregion
7
+ //#region src/i18n.d.ts
8
+ /** 可插值变量(分支名/角色名等) */
9
+ type I18nVars = Record<string, string>;
10
+ /** 单一文案条目: (vars) => 最终文本 */
11
+ type Entry = (v: I18nVars) => string;
12
+ /** 一门语言的文案字典: key 集合必须与内置 en 完全一致(registerLocale / 加载期校验) */
13
+ type Dict = Record<string, Entry>;
14
+ /**
15
+ * 注册一门新语言(运行时扩展点, P2-2): key 集合必须与内置 en 完全一致, 否则抛英文异常。
16
+ * 注册后 makeT/resolveLocale 即接受该 locale; 未注册的 locale 一律回退英文。
17
+ */
18
+ declare function registerLocale(name: string, dict: Dict): void;
6
19
  //#endregion
7
20
  //#region src/index.d.ts
8
21
  declare const name = "gitflow-guard";
@@ -19,6 +32,8 @@ interface EvaluateOptions {
19
32
  glabRunner?: Runner;
20
33
  /** 当前分支(缺省时内部查询) */
21
34
  currentBranch?: string | null;
35
+ /** 覆盖文案 locale(CLI --locale 旗标用, P2-1); 缺省按项目 config.locale 解析 */
36
+ locale?: Locale;
22
37
  }
23
38
  interface EvaluateResult {
24
39
  outcome: 'allow' | 'deny' | 'skipped';
@@ -45,4 +60,4 @@ declare function evaluateCommand(command: string, opts: EvaluateOptions): Promis
45
60
  declare function formatDeny(locale: Locale, why: string, next: string): string;
46
61
  declare function apply(ctx: Context, pluginConfig?: PluginConfig): void;
47
62
  //#endregion
48
- export { AuditEntry, EvaluateOptions, EvaluateResult, PluginConfig, appendAudit, apply, evaluateCommand, formatDeny, name, stateDir };
63
+ export { AuditEntry, type Dict, EvaluateOptions, EvaluateResult, PluginConfig, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir };
package/lib/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as name, i as formatDeny, n as apply, o as stateDir, r as evaluateCommand, t as appendAudit } from "./src-DTg5WMog.mjs";
2
- export { appendAudit, apply, evaluateCommand, formatDeny, name, stateDir };
1
+ import { a as name, i as formatDeny, n as apply, o as stateDir, p as registerLocale, r as evaluateCommand, t as appendAudit } from "./src-CPX1Ks_Z.mjs";
2
+ export { appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir };
@@ -444,166 +444,6 @@ function guardSub(args) {
444
444
  return "other";
445
445
  }
446
446
  //#endregion
447
- //#region src/config.ts
448
- const CONFIG_FILE = "gitflow-guard.config.json";
449
- /** 默认配置(分支角色必须由项目显式配置, 无默认) */
450
- const DEFAULT_CONFIG = {
451
- enabled: false,
452
- featurePattern: "feature/[\\w-]+",
453
- ci: { enabled: true },
454
- locale: "en"
455
- };
456
- const REGEX_CHARS = /[\\^$.*+?()[\]{}|]/;
457
- /** 一条分支条目: 含正则元字符按正则对待, 否则精确匹配 */
458
- function matchBranchSpec(branch, spec) {
459
- if (REGEX_CHARS.test(spec)) try {
460
- return new RegExp(`^(?:${spec})$`).test(branch);
461
- } catch {
462
- return false;
463
- }
464
- return branch === spec;
465
- }
466
- /** 判断分支是否命中某个角色(任一分支条目) */
467
- function roleMatches(branch, role) {
468
- if (!branch) return false;
469
- return role.branches.some((spec) => matchBranchSpec(branch, spec));
470
- }
471
- /** 规范化用户输入的某个角色: 数组 或 {branches:[...], update?, mergeBy?} */
472
- function normalizeRole(raw, defaultUpdate, defaultMergeBy) {
473
- const errors = [];
474
- let arr;
475
- let update = void 0;
476
- let mergeBy = void 0;
477
- if (Array.isArray(raw)) arr = raw;
478
- else if (typeof raw === "object" && raw !== null) {
479
- const o = raw;
480
- arr = o.branches;
481
- update = o.update;
482
- mergeBy = o.mergeBy;
483
- } else return {
484
- role: { branches: [] },
485
- errors: ["Branch role must be an array or { branches: [...] }"]
486
- };
487
- if (!Array.isArray(arr) || arr.length === 0 || !arr.every((x) => typeof x === "string" && x !== "")) errors.push("branches must be a non-empty array of strings");
488
- const role = { branches: (Array.isArray(arr) ? arr : []).filter((x) => typeof x === "string" && x !== "") };
489
- if (update === void 0 || update === "pr" || update === "flexible") role.update = update === void 0 ? defaultUpdate : update;
490
- else errors.push("update must be \"pr\" or \"flexible\"");
491
- if (mergeBy === void 0 || mergeBy === "user" || mergeBy === "anyone") role.mergeBy = mergeBy === void 0 ? defaultMergeBy : mergeBy;
492
- else errors.push("mergeBy must be \"user\" or \"anyone\"");
493
- return {
494
- role,
495
- errors
496
- };
497
- }
498
- /** 合并默认值并校验; 任何校验错误都会导致未启用(strict 位仍从原文提取, 供 fail-closed 判定) */
499
- function mergeConfig(raw) {
500
- const errors = [];
501
- if (typeof raw !== "object" || raw === null) return {
502
- config: null,
503
- errors: ["Config file must be a JSON object"]
504
- };
505
- const r = raw;
506
- const strict = r.strict === true ? true : r.strict === false ? false : void 0;
507
- if (r.strict !== void 0 && typeof r.strict !== "boolean") errors.push("strict must be a boolean");
508
- const config = {
509
- ...DEFAULT_CONFIG,
510
- ci: { ...DEFAULT_CONFIG.ci },
511
- branches: { integration: {
512
- branches: [],
513
- update: "pr",
514
- mergeBy: "anyone"
515
- } }
516
- };
517
- if (typeof r.enabled === "boolean") config.enabled = r.enabled;
518
- if (typeof r.featurePattern === "string" && r.featurePattern !== "") config.featurePattern = r.featurePattern;
519
- if (r.locale === "en" || r.locale === "zh") config.locale = r.locale;
520
- else if (r.locale !== void 0) errors.push("locale must be \"en\" or \"zh\"");
521
- const b = r.branches ?? {};
522
- if ("integration" in b) {
523
- const { role, errors: e } = normalizeRole(b.integration, "pr", "anyone");
524
- config.branches.integration = role;
525
- errors.push(...e);
526
- } else errors.push("branches.integration is required");
527
- if (b.preview !== void 0) {
528
- const { role, errors: e } = normalizeRole(b.preview, "pr", "anyone");
529
- config.branches.preview = role;
530
- errors.push(...e);
531
- }
532
- if (b.production !== void 0) {
533
- const { role, errors: e } = normalizeRole(b.production, "pr", "user");
534
- config.branches.production = role;
535
- errors.push(...e);
536
- }
537
- if (b.archive !== void 0) {
538
- const { role, errors: e } = normalizeRole(b.archive, "pr", "user");
539
- config.branches.archive = role;
540
- errors.push(...e);
541
- }
542
- const ci = r.ci ?? {};
543
- if (typeof ci.enabled === "boolean") config.ci.enabled = ci.enabled;
544
- if (strict !== void 0) config.strict = strict;
545
- errors.push(...validateConfig(config));
546
- return {
547
- config: errors.length > 0 ? null : config,
548
- errors,
549
- ...strict !== void 0 ? { strict } : {}
550
- };
551
- }
552
- /** 配置校验: 角色分支重叠 / 正则合法等 */
553
- function validateConfig(config) {
554
- const errors = [];
555
- if (config.branches.integration.branches.length === 0) errors.push("branches.integration.branches is required");
556
- try {
557
- new RegExp(config.featurePattern);
558
- } catch {
559
- errors.push(`featurePattern is not a valid regex: ${config.featurePattern}`);
560
- }
561
- const allRoles = [
562
- "integration",
563
- "preview",
564
- "production",
565
- "archive"
566
- ];
567
- for (let i = 0; i < allRoles.length; i++) {
568
- const a = config.branches[allRoles[i]];
569
- if (!a) continue;
570
- for (let j = i + 1; j < allRoles.length; j++) {
571
- const bb = config.branches[allRoles[j]];
572
- if (!bb) continue;
573
- if (a.branches.some((s) => bb.branches.includes(s))) errors.push(`branches.${allRoles[i]} and branches.${allRoles[j]} share the same entries`);
574
- }
575
- }
576
- return errors;
577
- }
578
- /** 从项目根加载配置; 无文件 = 未启用(opt-in) */
579
- async function loadConfig(repoRoot) {
580
- let text;
581
- try {
582
- text = await readFile(join(repoRoot, CONFIG_FILE), "utf8");
583
- } catch (e) {
584
- if (e.code === "ENOENT") return {
585
- config: null,
586
- errors: []
587
- };
588
- return {
589
- config: null,
590
- errors: [`Failed to read config file: ${e.message}`]
591
- };
592
- }
593
- let raw;
594
- try {
595
- raw = JSON.parse(text);
596
- } catch (e) {
597
- const strict = /"strict"\s*:\s*true/.test(text) || void 0;
598
- return {
599
- config: null,
600
- errors: [`Failed to read config file: ${e.message}`],
601
- ...strict ? { strict } : {}
602
- };
603
- }
604
- return mergeConfig(raw);
605
- }
606
- //#endregion
607
447
  //#region src/i18n.ts
608
448
  const en = {
609
449
  "role.integration": () => "integration branch",
@@ -650,6 +490,7 @@ const en = {
650
490
  "cli.statusTitle": (v) => `[gitflow-guard] status — ${v.repo}`,
651
491
  "cli.statusDisabled": () => "Config: not enabled (no gitflow-guard.config.json or enabled=false)",
652
492
  "cli.statusConfigError": (v) => ` config error: ${v.err}`,
493
+ "cli.statusConfigWarning": (v) => ` config warning: ${v.warn}`,
653
494
  "cli.statusEnabled": (v) => `Config: enabled | featurePattern: ${v.pattern}`,
654
495
  "cli.statusIntegration": (v) => `Integration: ${v.list} (update=${v.mode})`,
655
496
  "cli.statusPreview": (v) => `Preview: ${v.list} (update=${v.mode})`,
@@ -668,13 +509,14 @@ const en = {
668
509
  "usage.text": () => `gitflow-guard — GitFlow guard CLI
669
510
 
670
511
  Usage:
671
- gitflow-guard status [--repo <path>]
672
- gitflow-guard audit [--lines <count>] [--repo <path>]
673
- gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <path>]
512
+ gitflow-guard status [--repo <path>] [--locale <en|zh>]
513
+ gitflow-guard audit [--lines <count>] [--repo <path>] [--locale <en|zh>]
514
+ gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <path>] [--locale <en|zh>]
674
515
  gitflow-guard --help
675
516
 
676
517
  Notes:
677
518
  status/audit are read-only; the agent can self-inspect.
519
+ --locale overrides the message language for this invocation (flag > project config > English).
678
520
  check reads the hook payload on stdin (platform-specific protocol: claude/opencode exit 2,
679
521
  codex/antigravity JSON on stdout) and is meant for pre/post hooks of AI agents.`
680
522
  };
@@ -723,6 +565,7 @@ const zh = {
723
565
  "cli.statusTitle": (v) => `[gitflow-guard] status — ${v.repo}`,
724
566
  "cli.statusDisabled": () => "配置: 未启用(不存在 gitflow-guard.config.json 或 enabled=false)",
725
567
  "cli.statusConfigError": (v) => ` 配置错误: ${v.err}`,
568
+ "cli.statusConfigWarning": (v) => ` 配置警告: ${v.warn}`,
726
569
  "cli.statusEnabled": (v) => `配置: 已启用 | featurePattern: ${v.pattern}`,
727
570
  "cli.statusIntegration": (v) => `集成分支: ${v.list} (update=${v.mode})`,
728
571
  "cli.statusPreview": (v) => `预览分支: ${v.list} (update=${v.mode})`,
@@ -741,24 +584,38 @@ const zh = {
741
584
  "usage.text": () => `gitflow-guard — GitFlow 流程守卫 CLI
742
585
 
743
586
  用法:
744
- gitflow-guard status [--repo <路径>]
745
- gitflow-guard audit [--lines <数量>] [--repo <路径>]
746
- gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <路径>]
587
+ gitflow-guard status [--repo <路径>] [--locale <en|zh>]
588
+ gitflow-guard audit [--lines <数量>] [--repo <路径>] [--locale <en|zh>]
589
+ gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <路径>] [--locale <en|zh>]
747
590
  gitflow-guard --help
748
591
 
749
592
  说明:
750
593
  status/audit 只读, agent 可自查。
594
+ --locale 可临时覆盖本次调用的文案语言(旗标 > 项目配置 > 英文)。
751
595
  check 读 stdin hook payload 做门禁(平台协议: claude/opencode exit 2, codex/antigravity stdout JSON),
752
596
  供 Claude Code / Codex / OpenCode 等 agent 的 pre/post hook 调用。`
753
597
  };
598
+ /** 内置文案注册表: en 为兜底语言; 下游可经 registerLocale 追加 */
599
+ const dicts = /* @__PURE__ */ new Map([["en", en], ["zh", zh]]);
754
600
  const MESSAGE_KEYS = Object.keys(en);
755
- /** key 合法性与 en/zh 双字典完整性检查(开发期一次) */
756
- if (Object.keys(zh).length !== MESSAGE_KEYS.length || MESSAGE_KEYS.some((k) => !(k in zh))) throw new Error("i18n: en/zh 字典键不一致");
601
+ /** 字典键一致性校验(与内置 en 完全一致), 失败抛英文异常(P0-2: 异常信息遵循语言规范) */
602
+ function assertDictKeys(name, dict) {
603
+ if (Object.keys(dict).length !== MESSAGE_KEYS.length || MESSAGE_KEYS.some((k) => !(k in dict))) throw new Error(`i18n: locale "${name}" dictionary keys mismatch the built-in "en" dictionary`);
604
+ }
605
+ assertDictKeys("zh", zh);
757
606
  /**
758
- * 生成翻译函数。未知 key 回退英文原文(开发/防御性)。
607
+ * 注册一门新语言(运行时扩展点, P2-2): key 集合必须与内置 en 完全一致, 否则抛英文异常。
608
+ * 注册后 makeT/resolveLocale 即接受该 locale; 未注册的 locale 一律回退英文。
609
+ */
610
+ function registerLocale(name, dict) {
611
+ assertDictKeys(name, dict);
612
+ dicts.set(name, dict);
613
+ }
614
+ /**
615
+ * 生成翻译函数。未注册 locale / 未知 key 均回退英文(开发/防御性)。
759
616
  */
760
617
  function makeT(locale) {
761
- const dict = locale === "zh" ? zh : en;
618
+ const dict = dicts.get(locale) ?? en;
762
619
  return (key, vars = {}) => {
763
620
  const entry = dict[key] ?? en[key];
764
621
  if (!entry) return key;
@@ -769,9 +626,177 @@ function makeT(locale) {
769
626
  }
770
627
  };
771
628
  }
772
- /** 解析配置里的 locale 值: 'zh' 视为中文, 其余(含未定义)一律英文 */
629
+ /** 解析配置里的 locale 值: 白名单语义 = 已注册语言原样通过, 其余(含未定义)一律英文(P2-2 后白名单随注册表扩展) */
773
630
  function resolveLocale(v) {
774
- return v === "zh" ? "zh" : "en";
631
+ return typeof v === "string" && dicts.has(v) ? v : "en";
632
+ }
633
+ //#endregion
634
+ //#region src/config.ts
635
+ const CONFIG_FILE = "gitflow-guard.config.json";
636
+ /** 默认配置(分支角色必须由项目显式配置, 无默认) */
637
+ const DEFAULT_CONFIG = {
638
+ enabled: false,
639
+ featurePattern: "feature/[\\w-]+",
640
+ ci: { enabled: true },
641
+ locale: "en"
642
+ };
643
+ const REGEX_CHARS = /[\\^$.*+?()[\]{}|]/;
644
+ /** 一条分支条目: 含正则元字符按正则对待, 否则精确匹配 */
645
+ function matchBranchSpec(branch, spec) {
646
+ if (REGEX_CHARS.test(spec)) try {
647
+ return new RegExp(`^(?:${spec})$`).test(branch);
648
+ } catch {
649
+ return false;
650
+ }
651
+ return branch === spec;
652
+ }
653
+ /** 判断分支是否命中某个角色(任一分支条目) */
654
+ function roleMatches(branch, role) {
655
+ if (!branch) return false;
656
+ return role.branches.some((spec) => matchBranchSpec(branch, spec));
657
+ }
658
+ /** 规范化用户输入的某个角色: 数组 或 {branches:[...], update?, mergeBy?} */
659
+ function normalizeRole(raw, defaultUpdate, defaultMergeBy) {
660
+ const errors = [];
661
+ let arr;
662
+ let update = void 0;
663
+ let mergeBy = void 0;
664
+ if (Array.isArray(raw)) arr = raw;
665
+ else if (typeof raw === "object" && raw !== null) {
666
+ const o = raw;
667
+ arr = o.branches;
668
+ update = o.update;
669
+ mergeBy = o.mergeBy;
670
+ } else return {
671
+ role: { branches: [] },
672
+ errors: ["Branch role must be an array or { branches: [...] }"]
673
+ };
674
+ if (!Array.isArray(arr) || arr.length === 0 || !arr.every((x) => typeof x === "string" && x !== "")) errors.push("branches must be a non-empty array of strings");
675
+ const role = { branches: (Array.isArray(arr) ? arr : []).filter((x) => typeof x === "string" && x !== "") };
676
+ if (update === void 0 || update === "pr" || update === "flexible") role.update = update === void 0 ? defaultUpdate : update;
677
+ else errors.push("update must be \"pr\" or \"flexible\"");
678
+ if (mergeBy === void 0 || mergeBy === "user" || mergeBy === "anyone") role.mergeBy = mergeBy === void 0 ? defaultMergeBy : mergeBy;
679
+ else errors.push("mergeBy must be \"user\" or \"anyone\"");
680
+ return {
681
+ role,
682
+ errors
683
+ };
684
+ }
685
+ /** 合并默认值并校验; 任何校验错误都会导致未启用(strict 位仍从原文提取, 供 fail-closed 判定) */
686
+ function mergeConfig(raw) {
687
+ const errors = [];
688
+ const warnings = [];
689
+ if (typeof raw !== "object" || raw === null) return {
690
+ config: null,
691
+ errors: ["Config file must be a JSON object"],
692
+ warnings
693
+ };
694
+ const r = raw;
695
+ const strict = r.strict === true ? true : r.strict === false ? false : void 0;
696
+ if (r.strict !== void 0 && typeof r.strict !== "boolean") errors.push("strict must be a boolean");
697
+ const config = {
698
+ ...DEFAULT_CONFIG,
699
+ ci: { ...DEFAULT_CONFIG.ci },
700
+ branches: { integration: {
701
+ branches: [],
702
+ update: "pr",
703
+ mergeBy: "anyone"
704
+ } }
705
+ };
706
+ if (typeof r.enabled === "boolean") config.enabled = r.enabled;
707
+ if (typeof r.featurePattern === "string" && r.featurePattern !== "") config.featurePattern = r.featurePattern;
708
+ if (typeof r.locale === "string" && r.locale !== "") {
709
+ config.locale = r.locale;
710
+ if (resolveLocale(r.locale) !== r.locale) warnings.push(`unknown locale "${r.locale}"; falling back to en`);
711
+ } else if (r.locale !== void 0) errors.push("locale must be a string");
712
+ const b = r.branches ?? {};
713
+ if ("integration" in b) {
714
+ const { role, errors: e } = normalizeRole(b.integration, "pr", "anyone");
715
+ config.branches.integration = role;
716
+ errors.push(...e);
717
+ } else errors.push("branches.integration is required");
718
+ if (b.preview !== void 0) {
719
+ const { role, errors: e } = normalizeRole(b.preview, "pr", "anyone");
720
+ config.branches.preview = role;
721
+ errors.push(...e);
722
+ }
723
+ if (b.production !== void 0) {
724
+ const { role, errors: e } = normalizeRole(b.production, "pr", "user");
725
+ config.branches.production = role;
726
+ errors.push(...e);
727
+ }
728
+ if (b.archive !== void 0) {
729
+ const { role, errors: e } = normalizeRole(b.archive, "pr", "user");
730
+ config.branches.archive = role;
731
+ errors.push(...e);
732
+ }
733
+ const ci = r.ci ?? {};
734
+ if (typeof ci.enabled === "boolean") config.ci.enabled = ci.enabled;
735
+ if (strict !== void 0) config.strict = strict;
736
+ errors.push(...validateConfig(config));
737
+ return {
738
+ config: errors.length > 0 ? null : config,
739
+ errors,
740
+ warnings,
741
+ ...strict !== void 0 ? { strict } : {}
742
+ };
743
+ }
744
+ /** 配置校验: 角色分支重叠 / 正则合法等 */
745
+ function validateConfig(config) {
746
+ const errors = [];
747
+ if (config.branches.integration.branches.length === 0) errors.push("branches.integration.branches is required");
748
+ try {
749
+ new RegExp(config.featurePattern);
750
+ } catch {
751
+ errors.push(`featurePattern is not a valid regex: ${config.featurePattern}`);
752
+ }
753
+ const allRoles = [
754
+ "integration",
755
+ "preview",
756
+ "production",
757
+ "archive"
758
+ ];
759
+ for (let i = 0; i < allRoles.length; i++) {
760
+ const a = config.branches[allRoles[i]];
761
+ if (!a) continue;
762
+ for (let j = i + 1; j < allRoles.length; j++) {
763
+ const bb = config.branches[allRoles[j]];
764
+ if (!bb) continue;
765
+ if (a.branches.some((s) => bb.branches.includes(s))) errors.push(`branches.${allRoles[i]} and branches.${allRoles[j]} share the same entries`);
766
+ }
767
+ }
768
+ return errors;
769
+ }
770
+ /** 从项目根加载配置; 无文件 = 未启用(opt-in) */
771
+ async function loadConfig(repoRoot) {
772
+ let text;
773
+ try {
774
+ text = await readFile(join(repoRoot, CONFIG_FILE), "utf8");
775
+ } catch (e) {
776
+ if (e.code === "ENOENT") return {
777
+ config: null,
778
+ errors: [],
779
+ warnings: []
780
+ };
781
+ return {
782
+ config: null,
783
+ errors: [`Failed to read config file: ${e.message}`],
784
+ warnings: []
785
+ };
786
+ }
787
+ let raw;
788
+ try {
789
+ raw = JSON.parse(text);
790
+ } catch (e) {
791
+ const strict = /"strict"\s*:\s*true/.test(text) || void 0;
792
+ return {
793
+ config: null,
794
+ errors: [`Failed to read config file: ${e.message}`],
795
+ warnings: [],
796
+ ...strict ? { strict } : {}
797
+ };
798
+ }
799
+ return mergeConfig(raw);
775
800
  }
776
801
  //#endregion
777
802
  //#region src/gate.ts
@@ -1030,7 +1055,7 @@ async function evaluateCommand(command, opts) {
1030
1055
  segmentCount: 0,
1031
1056
  locale: "en"
1032
1057
  };
1033
- const locale = resolveLocale(config.locale);
1058
+ const locale = opts.locale != null ? resolveLocale(opts.locale) : resolveLocale(config.locale);
1034
1059
  const t = makeT(locale);
1035
1060
  const branch = opts.currentBranch ?? await currentBranch(runner, opts.repoRoot);
1036
1061
  const env = {
@@ -1133,10 +1158,10 @@ function apply(ctx, pluginConfig = {}) {
1133
1158
  };
1134
1159
  return next();
1135
1160
  } catch (e) {
1136
- ctx.logger?.warn?.(`gitflow-guard: 门禁内部错误, 已放行: ${e.message}`);
1161
+ ctx.logger?.warn?.(`gitflow-guard: gate internal error, allowed through: ${e.message}`);
1137
1162
  return next();
1138
1163
  }
1139
1164
  });
1140
1165
  }
1141
1166
  //#endregion
1142
- export { name as a, findRepoRoot as c, resolveLocale as d, loadConfig as f, formatDeny as i, gitRunner as l, classify as m, apply as n, stateDir as o, roleMatches as p, evaluateCommand as r, currentBranch as s, appendAudit as t, makeT as u };
1167
+ export { name as a, findRepoRoot as c, roleMatches as d, makeT as f, classify as h, formatDeny as i, gitRunner as l, resolveLocale as m, apply as n, stateDir as o, registerLocale as p, evaluateCommand as r, currentBranch as s, appendAudit as t, loadConfig as u };
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "agents-gitflow-guard",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "A configurable branch-role guard for AI coding agents (DSH / Claude Code / Codex / OpenCode / Antigravity) — integration/preview/production/archive, each with its own update rules",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": false,
7
8
  "main": "lib/index.mjs",
8
9
  "types": "lib/index.d.mts",
9
10
  "exports": {
@@ -42,6 +43,13 @@
42
43
  "type": "git",
43
44
  "url": "https://github.com/FeatureAgents/AgentsGitFlowController.git"
44
45
  },
46
+ "bugs": {
47
+ "url": "https://github.com/FeatureAgents/AgentsGitFlowController/issues"
48
+ },
49
+ "homepage": "https://github.com/FeatureAgents/AgentsGitFlowController#readme",
50
+ "engines": {
51
+ "node": ">=22"
52
+ },
45
53
  "dsh": {
46
54
  "bundle": {
47
55
  "patch": "./patch.yml"