agents-gitflow-guard 0.0.11 → 0.0.13
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 +23 -12
- package/README.zh.md +26 -13
- package/lib/cli.mjs +43 -21
- package/lib/index.d.mts +19 -3
- package/lib/index.mjs +2 -2
- package/lib/{src-DTg5WMog.mjs → src-BHyQJ6hX.mjs} +285 -182
- package/package.json +9 -1
- package/patch.yml +2 -2
package/README.md
CHANGED
|
@@ -42,10 +42,11 @@ 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.
|
|
45
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.13
|
|
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).
|
|
49
|
+
|
|
49
50
|
**Step 2 — configure**, create `gitflow-guard.config.json` in your **project root**:
|
|
50
51
|
|
|
51
52
|
```jsonc
|
|
@@ -256,7 +257,7 @@ archive (optional; you archive after release)
|
|
|
256
257
|
"production": { "branches": ["prd"], "update": "pr", "mergeBy": "user" }, // optional
|
|
257
258
|
"archive": ["main"] // optional
|
|
258
259
|
},
|
|
259
|
-
"locale": "en", // optional: message language ('en'
|
|
260
|
+
"locale": "en", // optional: message language — any registered locale ('en'/'zh' built-in); unknown values warn in status and fall back to English
|
|
260
261
|
"strict": false, // optional: fail-closed — invalid config / internal errors block instead of warn-and-allow
|
|
261
262
|
"ci": { "enabled": true } // optional: gh pr checks logged as reference
|
|
262
263
|
}
|
|
@@ -265,8 +266,18 @@ archive (optional; you archive after release)
|
|
|
265
266
|
- Roles accept either an **array** (shorthand) or an **object** `{ branches, update?, mergeBy? }`.
|
|
266
267
|
- `update`: `pr` (default) = updates only via PR/MR; `flexible` = allow direct/local merges (small teams).
|
|
267
268
|
- `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.
|
|
269
|
+
- 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.
|
|
270
|
+
- **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.
|
|
271
|
+
- **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.
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
import { registerLocale, MESSAGE_KEYS } from 'agents-gitflow-guard'
|
|
275
|
+
// MESSAGE_KEYS lists every key a dictionary must define (same set as built-in English);
|
|
276
|
+
// registration throws if a key is missing or extra.
|
|
277
|
+
const fr = { /* one entry per MESSAGE_KEYS, e.g. */ 'deny.header': ({ why }) => `[gitflow-guard] bloqué : ${why}` }
|
|
278
|
+
registerLocale('fr', fr)
|
|
279
|
+
```
|
|
280
|
+
- **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
281
|
- **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
282
|
- **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
283
|
|
|
@@ -296,12 +307,12 @@ The PR/MR target is resolved via `gh pr view` (GitHub) or `glab mr view` (GitLab
|
|
|
296
307
|
---
|
|
297
308
|
## Installation in detail
|
|
298
309
|
|
|
299
|
-
**Prerequisite**: a working [DSH](https://github.com/deepseek-ai/deepseek-harness) installation.
|
|
310
|
+
**Prerequisite**: a working [DSH](https://github.com/deepseek-ai/deepseek-harness) installation and **Node.js ≥ 22** on your `PATH` (matches the package `engines` floor and the lowest CI matrix tier — standalone hook users bypass npm but still need the runtime).
|
|
300
311
|
|
|
301
312
|
**From the npm registry** — the standard path, already covered in [Quick Start](#quick-start--30-seconds-to-a-guarded-repo):
|
|
302
313
|
|
|
303
314
|
```bash
|
|
304
|
-
dsh plugin --profile web add agents-gitflow-guard@0.0.
|
|
315
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.13 # pin recommended, see note above
|
|
305
316
|
```
|
|
306
317
|
|
|
307
318
|
Then restart DSH. Upgrades are the same command, followed by another restart.
|
|
@@ -401,7 +412,7 @@ What remains **locally non-defensible**: direct forge-API calls (`gh api repos/
|
|
|
401
412
|
|
|
402
413
|
### Why can't the agent just merge into production/archive itself?
|
|
403
414
|
|
|
404
|
-
Because the gate classifies those as **user-only** actions.
|
|
415
|
+
Because the gate classifies those as **user-only** actions. The plugin denies the *merge* for production and for archive — creating a PR/MR stays allowed, so an agent can still draft a `develop` → `main` archive PR for you. The merge itself, however, has exactly one path: **you** clicking it — there is no permit, token, or chat message an agent could use to confer that power on itself.
|
|
405
416
|
|
|
406
417
|
---
|
|
407
418
|
|
|
@@ -421,7 +432,7 @@ The blocks are reserved for: (1) direct writes to protected role branches, and (
|
|
|
421
432
|
|
|
422
433
|
### What if my config has a mistake?
|
|
423
434
|
|
|
424
|
-
|
|
435
|
+
A half-guessed setup is never applied by accident: any validation error disables the guard for that project and reports the errors.
|
|
425
436
|
|
|
426
437
|
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
438
|
|
|
@@ -480,15 +491,15 @@ The plugin is free and open source (MIT). If it saves you and your team from a s
|
|
|
480
491
|
|
|
481
492
|
```bash
|
|
482
493
|
npm install
|
|
483
|
-
npm test # unit tests: classify / gate / config / cli / repo / platform
|
|
494
|
+
npm test # unit tests: classify / gate / config / cli / repo / platform / i18n / index / accuracy-audit
|
|
484
495
|
npm run typecheck # tsc --noEmit, 0 errors
|
|
485
496
|
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 /
|
|
497
|
+
npm run verify:matrix # continuous cross-agent regression: DSH logic + zh-locale regression + Claude Code / Codex / OpenCode / Antigravity hook wiring
|
|
487
498
|
```
|
|
488
499
|
|
|
489
500
|
**Rule**: any logic change must pass a 0-error build + all green tests + a green `verify:matrix` before done.
|
|
490
501
|
|
|
491
|
-
**Adding a new agent client** (e.g.
|
|
502
|
+
**Adding a new agent client** (e.g. Cursor / Windsurf): all of these must change in one commit — `src/platform.ts` (+tests, `HookPlatform` union), a repo hook config beside `.claude/settings.json` / `.codex/hooks.json`, `.agents/hooks/references/<tool>.md`, `scripts/verify-matrix.mjs`, the README hook section and the top tagline, `package.json` description/keywords, and `CHANGELOG`. Done only when `npm run verify:matrix` is green. (Same checklist in [AGENTS.md](AGENTS.md) §8.)
|
|
492
503
|
|
|
493
504
|
---
|
|
494
505
|
|
|
@@ -496,4 +507,4 @@ npm run verify:matrix # continuous cross-agent regression: DSH logic + Claude Co
|
|
|
496
507
|
|
|
497
508
|
[MIT](LICENSE) © FeatureAgents
|
|
498
509
|
|
|
499
|
-
|
|
510
|
+
Historical v0 design decisions (Chinese; superseded by the role-driven model shipped in 0.0.2 — current behavior is documented in this README): [docs/design.md](docs/design.md).
|
package/README.zh.md
CHANGED
|
@@ -41,10 +41,11 @@
|
|
|
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.
|
|
44
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.13
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
> **版本坑**: 裸 `add` 装的是安装时刻的 `latest`——在 npm/pnpm 注册表缓存或镜像陈旧的机器上可能拿到旧版本。看到版本不对就锁版本。pnpm 打印的 peer 依赖 *警告* 属预期: DSH 启动时经共享模块回退提供 `@deepseek-ai/cordis` / `@deepseek-ai/dsh-tools`(插件正常工作)。
|
|
48
|
+
|
|
48
49
|
**第 2 步——配置**,在**项目根目录**创建 `gitflow-guard.config.json`:
|
|
49
50
|
|
|
50
51
|
```jsonc
|
|
@@ -253,7 +254,7 @@ archive(可选, 发布后你亲手归档)
|
|
|
253
254
|
"production": { "branches": ["prd"], "update": "pr", "mergeBy": "user" }, // 可选
|
|
254
255
|
"archive": ["main"] // 可选
|
|
255
256
|
},
|
|
256
|
-
"locale": "en", // 可选:
|
|
257
|
+
"locale": "en", // 可选: 文案语言——任意已注册 locale('en'/'zh' 内置); 未注册值在 status 告警并回退英文
|
|
257
258
|
"strict": false, // 可选: fail-closed —— 配置异常/内部错误改为拦截, 而非告警放行
|
|
258
259
|
"ci": { "enabled": true } // 可选: gh pr checks 作参考日志
|
|
259
260
|
}
|
|
@@ -262,8 +263,17 @@ archive(可选, 发布后你亲手归档)
|
|
|
262
263
|
- 每个角色既可用**数组**(简写),也可用**对象** `{ branches, update?, mergeBy? }`。
|
|
263
264
|
- `update`:`pr`(默认)= 只能 PR/MR 合入;`flexible` = 允许直推/本地合入(小团队)。
|
|
264
265
|
- `mergeBy`(生产):`user`(默认)= 只能你点合并;`anyone` = 放行 PR 合并。
|
|
265
|
-
- 每条分支条目是精确名或正则(自动识别)。
|
|
266
|
-
- **文案语言**:默认英文;加 `"locale": "zh"`
|
|
266
|
+
- 每条分支条目是精确名或正则(自动识别)。**正则安全**:分支正则由项目作者提供并按原样编译——`featurePattern` 与分支条目请避免灾难性回溯写法(如 `(\w+)+` 这类嵌套量词)。
|
|
267
|
+
- **文案语言**:默认英文;加 `"locale": "zh"` 切中文,或给任意 `gitflow-guard` 子命令传 `--locale <en|zh>`(优先级:CLI 旗标 > 项目配置 > 英文)。全部用户可见文案都跟随 locale——包括 `--help`、未知子命令提示、审计为空的提示等 CLI 框架文案。
|
|
268
|
+
- **自定义语言**:下游包可在运行时追加语言——`import { registerLocale } from 'agents-gitflow-guard'`,调用 `registerLocale('fr', frDict)` 注册一份与内置英文键完全一致的字典(注册时校验),再在项目配置写 `"locale": "fr"` 即生效。
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
import { registerLocale, MESSAGE_KEYS } from 'agents-gitflow-guard'
|
|
272
|
+
// MESSAGE_KEYS 列出字典必须覆盖的全部键(与内置英文同一键集);缺键/多键注册即抛错。
|
|
273
|
+
const fr = { /* 每个 MESSAGE_KEYS 一条, 如 */ 'deny.header': ({ why }) => `[gitflow-guard] bloqué : ${why}` }
|
|
274
|
+
registerLocale('fr', fr)
|
|
275
|
+
```
|
|
276
|
+
- **未注册语言**:拦截路径对未注册的 `"locale"` 静默回退英文(设计如此——hook 不因文案缺失卡死),笔误因此容易被忽略;一行告警在 `gitflow-guard status` 中可见。
|
|
267
277
|
- **校验**:`integration` 必填;角色条目重叠会被拒;非法正则会报错。**任何错误都会让该项目的插件禁用并上报**(而不是用半吊子配置)。
|
|
268
278
|
- **strict 模式**:默认配置损坏时 stderr 告警一次后放行(fail-open,避免一个笔误卡死工具管道);`"strict": true` 把配置异常与内部错误翻转为**拦截**(fail-closed)——供高风险仓库选用。文件不存在或显式 `enabled: false` 两种模式下都保持静默。
|
|
269
279
|
|
|
@@ -277,7 +287,7 @@ archive(可选, 发布后你亲手归档)
|
|
|
277
287
|
| 直推 / 强推 / 删除 integration / preview / production / archive | 🚫 拦(integration/preview 配 `flexible` 时直推放行) |
|
|
278
288
|
| PR/MR: feature → integration / preview | ✅ 放行 |
|
|
279
289
|
| PR/MR: feature → production | ✅ 可创建;**合并被拦**(你在 UI 合并) |
|
|
280
|
-
| 指向 archive 的 PR/MR |
|
|
290
|
+
| 指向 archive 的 PR/MR | ✅ 可创建;🚫 合并被拦(你在 UI 合并) |
|
|
281
291
|
| 在 integration / preview 上 `git merge feature/x`(本地) | 🚫 拦(须 PR/MR);`update: flexible` 则放行 |
|
|
282
292
|
| 串联命令(`checkout develop && merge feature/x`) | 🚫 拦——逐段模拟分支切换,无法绕序 |
|
|
283
293
|
|
|
@@ -293,12 +303,12 @@ PR/MR 目标通过 `gh pr view`(GitHub)或 `glab mr view`(GitLab)解析;没有
|
|
|
293
303
|
---
|
|
294
304
|
## 安装详解
|
|
295
305
|
|
|
296
|
-
**前置**:一个可用的 [DSH](https://github.com/deepseek-ai/deepseek-harness)
|
|
306
|
+
**前置**:一个可用的 [DSH](https://github.com/deepseek-ai/deepseek-harness) 安装,且 `PATH` 上有 **Node.js ≥ 22**(与包 `engines` 及 CI 矩阵最低档一致——独立 hook 用户不经 npm 安装,同样需要运行时)。
|
|
297
307
|
|
|
298
308
|
**从 npm registry**——标准路径,已在[快速开始](#快速开始30-秒用上)覆盖:
|
|
299
309
|
|
|
300
310
|
```bash
|
|
301
|
-
dsh plugin --profile web add agents-gitflow-guard@0.0.
|
|
311
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.13 # 建议锁版本, 见上文提示
|
|
302
312
|
```
|
|
303
313
|
|
|
304
314
|
然后重启 DSH。升级用同一命令,再重启一次。
|
|
@@ -398,7 +408,7 @@ hooks:
|
|
|
398
408
|
|
|
399
409
|
### 为什么 agent 不能自己合并进生产/归档?
|
|
400
410
|
|
|
401
|
-
|
|
411
|
+
因为门禁把那些动作判定为**仅用户**。插件对生产的*合并*、归档的*合并*一律拦截——*建 PR/MR 允许*,agent 仍可替你起草 develop→main 归档 PR。但合并本身只有一条路径:**你**亲手点合并——不存在 agent 能用来给自己授权的特许、令牌或聊天消息。
|
|
402
412
|
|
|
403
413
|
---
|
|
404
414
|
|
|
@@ -418,7 +428,7 @@ hooks:
|
|
|
418
428
|
|
|
419
429
|
### 配置写错了会怎样?
|
|
420
430
|
|
|
421
|
-
|
|
431
|
+
半吊子配置绝不会意外生效:任何校验错误都会让该项目的守卫禁用并上报错误。
|
|
422
432
|
|
|
423
433
|
常见错误:`integration` 缺失(必填)、同一个分支被配到两个角色里(显式拒绝)、`featurePattern` 写不成合法正则(报错)。失败提示很明确,文件又是一个 JSON 对象,通常三十秒改好。
|
|
424
434
|
|
|
@@ -446,7 +456,7 @@ MIT,免费,无条件。随便用、随便改、随便发,唯一义务是保留
|
|
|
446
456
|
| **integration** | 集成分支,唯一必填角色(`branches.integration`);feature 经 PR/MR 合入;受保护 |
|
|
447
457
|
| **preview** | 可选环境终点分支(`branches.preview`,数组);只走 PR/MR 更新 |
|
|
448
458
|
| **production** | 可选生产分支(`branches.production`,数组);PR/MR + 合并仅限用户 |
|
|
449
|
-
| **archive** | `branches.archive
|
|
459
|
+
| **archive** | 可选的发布后归档分支(`branches.archive`,数组);允许 agent 创建指向它的 PR/MR,合并仍限用户亲手 |
|
|
450
460
|
| **feature 分支** | 你的工作分支,由 `featurePattern` 识别;自由区 |
|
|
451
461
|
| **门禁矩阵** | 把每条被分类的命令映射为放行/拦截的判定表 |
|
|
452
462
|
| **pre-execute** | 工具管线中拦截发生的钩子——在命令运行之前 |
|
|
@@ -477,12 +487,15 @@ MIT,免费,无条件。随便用、随便改、随便发,唯一义务是保留
|
|
|
477
487
|
|
|
478
488
|
```bash
|
|
479
489
|
npm install
|
|
480
|
-
npm test # 单测: classify / gate / config / cli / repo / platform
|
|
490
|
+
npm test # 单测: classify / gate / config / cli / repo / platform / i18n / index / accuracy-audit
|
|
481
491
|
npm run typecheck # tsc --noEmit, 0 Error
|
|
482
492
|
npm run build # tsdown → lib/(CLI 与插件共用)
|
|
493
|
+
npm run verify:matrix # 连续复测矩阵: DSH 逻辑 + zh 文案回归 + Claude Code / Codex / OpenCode / Antigravity hook 编码
|
|
483
494
|
```
|
|
484
495
|
|
|
485
|
-
**铁律**:任何逻辑改动必须 0 Error 构建 +
|
|
496
|
+
**铁律**:任何逻辑改动必须 0 Error 构建 + 单测全绿 + 连续复测矩阵(`verify:matrix`)全绿后才算完成。
|
|
497
|
+
|
|
498
|
+
**接入新的 agent 客户端**(如 Cursor / Windsurf):以下各项必须在同一个 commit 内完成——`src/platform.ts`(含测试与 `HookPlatform` 联合类型)、`.claude/settings.json` / `.codex/hooks.json` 旁新增一份仓库级 hook 配置、`.agents/hooks/references/<tool>.md`、`scripts/verify-matrix.mjs`、README 双语 hook 段与开头宣传语、`package.json` 的 description/keywords,以及 `CHANGELOG`。`npm run verify:matrix` 全绿才算完成。(同一清单见 [AGENTS.md](AGENTS.md) §8;DSH 为进程内插件不走 stdin-hook 清单,见该节例外说明。)
|
|
486
499
|
|
|
487
500
|
---
|
|
488
501
|
|
|
@@ -490,4 +503,4 @@ npm run build # tsdown → lib/(CLI 与插件共用)
|
|
|
490
503
|
|
|
491
504
|
[MIT](LICENSE) © FeatureAgents
|
|
492
505
|
|
|
493
|
-
|
|
506
|
+
v0 历史设计决策(中文;已被 0.0.2 角色驱动模型取代——现行行为以本 README 为准):[docs/design.md](docs/design.md)。
|
package/lib/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as findRepoRoot, d as
|
|
1
|
+
import { c as findRepoRoot, d as roleMatches, g as classify, h as resolveLocale, i as formatDeny, l as gitRunner, o as stateDir, p as makeT, r as evaluateCommand, s as currentBranch, u as loadConfig } from "./src-BHyQJ6hX.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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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).
|
|
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(
|
|
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(
|
|
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
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
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(
|
|
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,22 @@
|
|
|
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
|
+
declare const MESSAGE_KEYS: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* 注册一门新语言(运行时扩展点, P2-2): key 集合必须与内置 en 完全一致, 否则抛英文异常。
|
|
17
|
+
* 注册后 makeT/resolveLocale 即接受该 locale; 未注册的 locale 一律回退英文。
|
|
18
|
+
*/
|
|
19
|
+
declare function registerLocale(name: string, dict: Dict): void;
|
|
6
20
|
//#endregion
|
|
7
21
|
//#region src/index.d.ts
|
|
8
22
|
declare const name = "gitflow-guard";
|
|
@@ -19,6 +33,8 @@ interface EvaluateOptions {
|
|
|
19
33
|
glabRunner?: Runner;
|
|
20
34
|
/** 当前分支(缺省时内部查询) */
|
|
21
35
|
currentBranch?: string | null;
|
|
36
|
+
/** 覆盖文案 locale(CLI --locale 旗标用, P2-1); 缺省按项目 config.locale 解析 */
|
|
37
|
+
locale?: Locale;
|
|
22
38
|
}
|
|
23
39
|
interface EvaluateResult {
|
|
24
40
|
outcome: 'allow' | 'deny' | 'skipped';
|
|
@@ -45,4 +61,4 @@ declare function evaluateCommand(command: string, opts: EvaluateOptions): Promis
|
|
|
45
61
|
declare function formatDeny(locale: Locale, why: string, next: string): string;
|
|
46
62
|
declare function apply(ctx: Context, pluginConfig?: PluginConfig): void;
|
|
47
63
|
//#endregion
|
|
48
|
-
export { AuditEntry, EvaluateOptions, EvaluateResult, PluginConfig, appendAudit, apply, evaluateCommand, formatDeny, name, stateDir };
|
|
64
|
+
export { AuditEntry, type Dict, EvaluateOptions, EvaluateResult, MESSAGE_KEYS, 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-
|
|
2
|
-
export { appendAudit, apply, evaluateCommand, formatDeny, name, stateDir };
|
|
1
|
+
import { a as name, f as MESSAGE_KEYS, i as formatDeny, m as registerLocale, n as apply, o as stateDir, r as evaluateCommand, t as appendAudit } from "./src-BHyQJ6hX.mjs";
|
|
2
|
+
export { MESSAGE_KEYS, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir };
|
|
@@ -174,10 +174,30 @@ function classifyGit(args, ctx) {
|
|
|
174
174
|
if (sub === "push") return parsePush(rest, ctx);
|
|
175
175
|
if (sub === "pull") return parsePull(rest);
|
|
176
176
|
if (sub === "merge") return parseMerge(rest);
|
|
177
|
-
if (sub === "branch") return parseBranch(rest);
|
|
177
|
+
if (sub === "branch") return parseBranch(rest, ctx);
|
|
178
178
|
if (sub === "checkout" || sub === "switch") return parseCheckout(rest);
|
|
179
179
|
if (sub === "send-pack") return parseSendPack(rest);
|
|
180
180
|
if (sub === "update-ref") return parseUpdateRef(rest);
|
|
181
|
+
if (sub === "reset" || sub === "filter-branch") return [{ kind: "ref-move" }];
|
|
182
|
+
if (sub === "rebase") return parseRebase(rest);
|
|
183
|
+
if (sub === "commit") return parseCommit(rest);
|
|
184
|
+
return [{ kind: "other" }];
|
|
185
|
+
}
|
|
186
|
+
/** rebase 移动当前分支 ref; abort/continue/skip 等恢复类旗标不移动(放行, 避免把用户困在中途态) */
|
|
187
|
+
function parseRebase(args) {
|
|
188
|
+
const RESUME = /* @__PURE__ */ new Set([
|
|
189
|
+
"--abort",
|
|
190
|
+
"--continue",
|
|
191
|
+
"--skip",
|
|
192
|
+
"--quit",
|
|
193
|
+
"--edit-todo"
|
|
194
|
+
]);
|
|
195
|
+
if (args.some((a) => RESUME.has(a))) return [{ kind: "other" }];
|
|
196
|
+
return [{ kind: "ref-move" }];
|
|
197
|
+
}
|
|
198
|
+
/** commit 仅 --amend 改写当前分支 tip; 普通提交不移动既有 ref */
|
|
199
|
+
function parseCommit(args) {
|
|
200
|
+
if (args.some((a) => a === "--amend")) return [{ kind: "ref-move" }];
|
|
181
201
|
return [{ kind: "other" }];
|
|
182
202
|
}
|
|
183
203
|
/** 剥离子命令前的全局选项(-C <path> / -c <k=v> / --git-dir 等), 否则 git -C . push 会被判 other */
|
|
@@ -388,12 +408,60 @@ function parseMerge(args) {
|
|
|
388
408
|
source: args.find((a, i) => !a.startsWith("-") && args[i - 1] !== "-m" && args[i - 1] !== "--message") ?? null
|
|
389
409
|
}];
|
|
390
410
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
411
|
+
/**
|
|
412
|
+
* git branch 全旗标扫描(旧实现只读 args[0..1], `-d --force develop` 这类组合长旗标会漏):
|
|
413
|
+
* - 删除(-d/-D/--delete, 可与 --force 组合): 逐个分支名 → branch-delete
|
|
414
|
+
* - 改名(-m/-M/--move): 移动受保护 ref(源)或覆盖受保护名(目标)→ 按 ref-update 同级处理
|
|
415
|
+
* - 强制复位(-f/--force 单独使用): git branch -f <name> <commit> 静默移动分支指针 → ref-update
|
|
416
|
+
*/
|
|
417
|
+
function parseBranch(args, ctx) {
|
|
418
|
+
let deleteFlag = false;
|
|
419
|
+
let force = false;
|
|
420
|
+
let move = false;
|
|
421
|
+
const names = [];
|
|
422
|
+
for (const a of args) if (a.startsWith("--")) {
|
|
423
|
+
if (a === "--delete") deleteFlag = true;
|
|
424
|
+
else if (a === "--force") force = true;
|
|
425
|
+
else if (a === "--move") move = true;
|
|
426
|
+
} else if (a.startsWith("-") && a.length > 1) {
|
|
427
|
+
for (const ch of a.slice(1)) if (ch === "d") deleteFlag = true;
|
|
428
|
+
else if (ch === "D") {
|
|
429
|
+
deleteFlag = true;
|
|
430
|
+
force = true;
|
|
431
|
+
} else if (ch === "m") move = true;
|
|
432
|
+
else if (ch === "M") {
|
|
433
|
+
move = true;
|
|
434
|
+
force = true;
|
|
435
|
+
} else if (ch === "f") force = true;
|
|
436
|
+
} else names.push(a);
|
|
437
|
+
if (move) {
|
|
438
|
+
const from = names.length >= 2 ? names[0] : ctx.currentBranch ?? null;
|
|
439
|
+
const to = names.length >= 1 ? names[names.length - 1] : null;
|
|
440
|
+
const out = [];
|
|
441
|
+
if (from != null) out.push({
|
|
442
|
+
kind: "ref-update",
|
|
443
|
+
branch: from,
|
|
444
|
+
delete: false
|
|
445
|
+
});
|
|
446
|
+
if (to != null && to !== from) out.push({
|
|
447
|
+
kind: "ref-update",
|
|
448
|
+
branch: to,
|
|
449
|
+
delete: false
|
|
450
|
+
});
|
|
451
|
+
return out.length > 0 ? out : [{ kind: "other" }];
|
|
452
|
+
}
|
|
453
|
+
if (deleteFlag) {
|
|
454
|
+
if (names.length === 0) return [{ kind: "other" }];
|
|
455
|
+
return names.map((branch) => ({
|
|
456
|
+
kind: "branch-delete",
|
|
457
|
+
branch,
|
|
458
|
+
force
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
if (force && names.length >= 1) return [{
|
|
462
|
+
kind: "ref-update",
|
|
463
|
+
branch: names[0],
|
|
464
|
+
delete: false
|
|
397
465
|
}];
|
|
398
466
|
return [{ kind: "other" }];
|
|
399
467
|
}
|
|
@@ -444,166 +512,6 @@ function guardSub(args) {
|
|
|
444
512
|
return "other";
|
|
445
513
|
}
|
|
446
514
|
//#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
515
|
//#region src/i18n.ts
|
|
608
516
|
const en = {
|
|
609
517
|
"role.integration": () => "integration branch",
|
|
@@ -617,6 +525,8 @@ const en = {
|
|
|
617
525
|
"denyDeleteOrForce.next": () => "Delete/force-push on a feature branch outside the protected branches; protected branches are managed by you.",
|
|
618
526
|
"refUpdateProtected.why": (v) => `Protected branch "${v.branch}" forbids direct ref updates (update-ref)`,
|
|
619
527
|
"refUpdateProtected.next": () => "Update protected branches via PR/MR; they are managed by you.",
|
|
528
|
+
"refMoveProtected.why": () => "Rewriting history on a protected branch (reset / rebase / commit --amend / filter-branch) is not allowed",
|
|
529
|
+
"refMoveProtected.next": () => "Do history rewrites on a feature branch; protected branches advance via PR/MR and are managed by you.",
|
|
620
530
|
"pushAll.why": () => "--all/--mirror push would include protected branches",
|
|
621
531
|
"pushAll.next": () => "Push branch by branch with an explicit refspec.",
|
|
622
532
|
"pushDetached.why": () => "Cannot determine the push target (detached HEAD?)",
|
|
@@ -650,6 +560,7 @@ const en = {
|
|
|
650
560
|
"cli.statusTitle": (v) => `[gitflow-guard] status — ${v.repo}`,
|
|
651
561
|
"cli.statusDisabled": () => "Config: not enabled (no gitflow-guard.config.json or enabled=false)",
|
|
652
562
|
"cli.statusConfigError": (v) => ` config error: ${v.err}`,
|
|
563
|
+
"cli.statusConfigWarning": (v) => ` config warning: ${v.warn}`,
|
|
653
564
|
"cli.statusEnabled": (v) => `Config: enabled | featurePattern: ${v.pattern}`,
|
|
654
565
|
"cli.statusIntegration": (v) => `Integration: ${v.list} (update=${v.mode})`,
|
|
655
566
|
"cli.statusPreview": (v) => `Preview: ${v.list} (update=${v.mode})`,
|
|
@@ -668,13 +579,14 @@ const en = {
|
|
|
668
579
|
"usage.text": () => `gitflow-guard — GitFlow guard CLI
|
|
669
580
|
|
|
670
581
|
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>]
|
|
582
|
+
gitflow-guard status [--repo <path>] [--locale <en|zh>]
|
|
583
|
+
gitflow-guard audit [--lines <count>] [--repo <path>] [--locale <en|zh>]
|
|
584
|
+
gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <path>] [--locale <en|zh>]
|
|
674
585
|
gitflow-guard --help
|
|
675
586
|
|
|
676
587
|
Notes:
|
|
677
588
|
status/audit are read-only; the agent can self-inspect.
|
|
589
|
+
--locale overrides the message language for this invocation (flag > project config > English).
|
|
678
590
|
check reads the hook payload on stdin (platform-specific protocol: claude/opencode exit 2,
|
|
679
591
|
codex/antigravity JSON on stdout) and is meant for pre/post hooks of AI agents.`
|
|
680
592
|
};
|
|
@@ -690,6 +602,8 @@ const zh = {
|
|
|
690
602
|
"denyDeleteOrForce.next": () => "删除/强推请到受保护分支外的 feature 分支上操作; 受保护分支由用户亲手管理",
|
|
691
603
|
"refUpdateProtected.why": (v) => `受保护分支「${v.branch}」禁止直接改写 refs(update-ref)`,
|
|
692
604
|
"refUpdateProtected.next": () => "请通过 PR/MR 更新受保护分支; 受保护分支由用户亲手管理",
|
|
605
|
+
"refMoveProtected.why": () => "受保护分支禁止本地改写历史(reset / rebase / commit --amend / filter-branch)",
|
|
606
|
+
"refMoveProtected.next": () => "历史改写请在 feature 分支上进行; 受保护分支仅经 PR/MR 推进, 由用户亲手管理",
|
|
693
607
|
"pushAll.why": () => "--all/--mirror 推送会包含受保护分支",
|
|
694
608
|
"pushAll.next": () => "请逐分支推送并显式指定 refspec",
|
|
695
609
|
"pushDetached.why": () => "无法确定推送目标分支(可能处于 detached HEAD)",
|
|
@@ -723,6 +637,7 @@ const zh = {
|
|
|
723
637
|
"cli.statusTitle": (v) => `[gitflow-guard] status — ${v.repo}`,
|
|
724
638
|
"cli.statusDisabled": () => "配置: 未启用(不存在 gitflow-guard.config.json 或 enabled=false)",
|
|
725
639
|
"cli.statusConfigError": (v) => ` 配置错误: ${v.err}`,
|
|
640
|
+
"cli.statusConfigWarning": (v) => ` 配置警告: ${v.warn}`,
|
|
726
641
|
"cli.statusEnabled": (v) => `配置: 已启用 | featurePattern: ${v.pattern}`,
|
|
727
642
|
"cli.statusIntegration": (v) => `集成分支: ${v.list} (update=${v.mode})`,
|
|
728
643
|
"cli.statusPreview": (v) => `预览分支: ${v.list} (update=${v.mode})`,
|
|
@@ -741,24 +656,38 @@ const zh = {
|
|
|
741
656
|
"usage.text": () => `gitflow-guard — GitFlow 流程守卫 CLI
|
|
742
657
|
|
|
743
658
|
用法:
|
|
744
|
-
gitflow-guard status [--repo <路径>]
|
|
745
|
-
gitflow-guard audit [--lines <数量>] [--repo <路径>]
|
|
746
|
-
gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <路径>]
|
|
659
|
+
gitflow-guard status [--repo <路径>] [--locale <en|zh>]
|
|
660
|
+
gitflow-guard audit [--lines <数量>] [--repo <路径>] [--locale <en|zh>]
|
|
661
|
+
gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <路径>] [--locale <en|zh>]
|
|
747
662
|
gitflow-guard --help
|
|
748
663
|
|
|
749
664
|
说明:
|
|
750
665
|
status/audit 只读, agent 可自查。
|
|
666
|
+
--locale 可临时覆盖本次调用的文案语言(旗标 > 项目配置 > 英文)。
|
|
751
667
|
check 读 stdin hook payload 做门禁(平台协议: claude/opencode exit 2, codex/antigravity stdout JSON),
|
|
752
668
|
供 Claude Code / Codex / OpenCode 等 agent 的 pre/post hook 调用。`
|
|
753
669
|
};
|
|
670
|
+
/** 内置文案注册表: en 为兜底语言; 下游可经 registerLocale 追加 */
|
|
671
|
+
const dicts = /* @__PURE__ */ new Map([["en", en], ["zh", zh]]);
|
|
754
672
|
const MESSAGE_KEYS = Object.keys(en);
|
|
755
|
-
/**
|
|
756
|
-
|
|
673
|
+
/** 字典键一致性校验(与内置 en 完全一致), 失败抛英文异常(P0-2: 异常信息遵循语言规范) */
|
|
674
|
+
function assertDictKeys(name, dict) {
|
|
675
|
+
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`);
|
|
676
|
+
}
|
|
677
|
+
assertDictKeys("zh", zh);
|
|
757
678
|
/**
|
|
758
|
-
*
|
|
679
|
+
* 注册一门新语言(运行时扩展点, P2-2): key 集合必须与内置 en 完全一致, 否则抛英文异常。
|
|
680
|
+
* 注册后 makeT/resolveLocale 即接受该 locale; 未注册的 locale 一律回退英文。
|
|
681
|
+
*/
|
|
682
|
+
function registerLocale(name, dict) {
|
|
683
|
+
assertDictKeys(name, dict);
|
|
684
|
+
dicts.set(name, dict);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* 生成翻译函数。未注册 locale / 未知 key 均回退英文(开发/防御性)。
|
|
759
688
|
*/
|
|
760
689
|
function makeT(locale) {
|
|
761
|
-
const dict = locale
|
|
690
|
+
const dict = dicts.get(locale) ?? en;
|
|
762
691
|
return (key, vars = {}) => {
|
|
763
692
|
const entry = dict[key] ?? en[key];
|
|
764
693
|
if (!entry) return key;
|
|
@@ -769,9 +698,182 @@ function makeT(locale) {
|
|
|
769
698
|
}
|
|
770
699
|
};
|
|
771
700
|
}
|
|
772
|
-
/** 解析配置里的 locale 值:
|
|
701
|
+
/** 解析配置里的 locale 值: 白名单语义 = 已注册语言原样通过, 其余(含未定义)一律英文(P2-2 后白名单随注册表扩展) */
|
|
773
702
|
function resolveLocale(v) {
|
|
774
|
-
return v === "
|
|
703
|
+
return typeof v === "string" && dicts.has(v) ? v : "en";
|
|
704
|
+
}
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/config.ts
|
|
707
|
+
const CONFIG_FILE = "gitflow-guard.config.json";
|
|
708
|
+
/** 默认配置(分支角色必须由项目显式配置, 无默认) */
|
|
709
|
+
const DEFAULT_CONFIG = {
|
|
710
|
+
enabled: false,
|
|
711
|
+
featurePattern: "feature/[\\w-]+",
|
|
712
|
+
ci: { enabled: true },
|
|
713
|
+
locale: "en"
|
|
714
|
+
};
|
|
715
|
+
const REGEX_CHARS = /[\\^$.*+?()[\]{}|]/;
|
|
716
|
+
/** 一条分支条目: 含正则元字符按正则对待, 否则精确匹配 */
|
|
717
|
+
function matchBranchSpec(branch, spec) {
|
|
718
|
+
if (REGEX_CHARS.test(spec)) try {
|
|
719
|
+
return new RegExp(`^(?:${spec})$`).test(branch);
|
|
720
|
+
} catch {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
return branch === spec;
|
|
724
|
+
}
|
|
725
|
+
/** 判断分支是否命中某个角色(任一分支条目) */
|
|
726
|
+
function roleMatches(branch, role) {
|
|
727
|
+
if (!branch) return false;
|
|
728
|
+
return role.branches.some((spec) => matchBranchSpec(branch, spec));
|
|
729
|
+
}
|
|
730
|
+
/** 规范化用户输入的某个角色: 数组 或 {branches:[...], update?, mergeBy?} */
|
|
731
|
+
function normalizeRole(raw, roleName, defaultUpdate, defaultMergeBy) {
|
|
732
|
+
const errors = [];
|
|
733
|
+
let arr;
|
|
734
|
+
let update = void 0;
|
|
735
|
+
let mergeBy = void 0;
|
|
736
|
+
if (Array.isArray(raw)) arr = raw;
|
|
737
|
+
else if (typeof raw === "object" && raw !== null) {
|
|
738
|
+
const o = raw;
|
|
739
|
+
arr = o.branches;
|
|
740
|
+
update = o.update;
|
|
741
|
+
mergeBy = o.mergeBy;
|
|
742
|
+
} else return {
|
|
743
|
+
role: { branches: [] },
|
|
744
|
+
errors: ["Branch role must be an array or { branches: [...] }"]
|
|
745
|
+
};
|
|
746
|
+
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");
|
|
747
|
+
const role = { branches: (Array.isArray(arr) ? arr : []).filter((x) => typeof x === "string" && x !== "") };
|
|
748
|
+
for (const spec of role.branches) try {
|
|
749
|
+
new RegExp(`^(?:${spec})$`);
|
|
750
|
+
} catch {
|
|
751
|
+
errors.push(`branches.${roleName} entry is not a valid regex: ${spec}`);
|
|
752
|
+
}
|
|
753
|
+
if (update === void 0 || update === "pr" || update === "flexible") role.update = update === void 0 ? defaultUpdate : update;
|
|
754
|
+
else errors.push("update must be \"pr\" or \"flexible\"");
|
|
755
|
+
if (mergeBy === void 0 || mergeBy === "user" || mergeBy === "anyone") role.mergeBy = mergeBy === void 0 ? defaultMergeBy : mergeBy;
|
|
756
|
+
else errors.push("mergeBy must be \"user\" or \"anyone\"");
|
|
757
|
+
return {
|
|
758
|
+
role,
|
|
759
|
+
errors
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
/** 合并默认值并校验; 任何校验错误都会导致未启用(strict 位仍从原文提取, 供 fail-closed 判定) */
|
|
763
|
+
function mergeConfig(raw) {
|
|
764
|
+
const errors = [];
|
|
765
|
+
const warnings = [];
|
|
766
|
+
if (typeof raw !== "object" || raw === null) return {
|
|
767
|
+
config: null,
|
|
768
|
+
errors: ["Config file must be a JSON object"],
|
|
769
|
+
warnings
|
|
770
|
+
};
|
|
771
|
+
const r = raw;
|
|
772
|
+
const strict = r.strict === true ? true : r.strict === false ? false : void 0;
|
|
773
|
+
if (r.strict !== void 0 && typeof r.strict !== "boolean") errors.push("strict must be a boolean");
|
|
774
|
+
const config = {
|
|
775
|
+
...DEFAULT_CONFIG,
|
|
776
|
+
ci: { ...DEFAULT_CONFIG.ci },
|
|
777
|
+
branches: { integration: {
|
|
778
|
+
branches: [],
|
|
779
|
+
update: "pr",
|
|
780
|
+
mergeBy: "anyone"
|
|
781
|
+
} }
|
|
782
|
+
};
|
|
783
|
+
if (typeof r.enabled === "boolean") config.enabled = r.enabled;
|
|
784
|
+
if (typeof r.featurePattern === "string" && r.featurePattern !== "") config.featurePattern = r.featurePattern;
|
|
785
|
+
if (typeof r.locale === "string" && r.locale !== "") {
|
|
786
|
+
config.locale = r.locale;
|
|
787
|
+
if (resolveLocale(r.locale) !== r.locale) warnings.push(`unknown locale "${r.locale}"; falling back to en`);
|
|
788
|
+
} else if (r.locale !== void 0) errors.push("locale must be a string");
|
|
789
|
+
const b = r.branches ?? {};
|
|
790
|
+
if ("integration" in b) {
|
|
791
|
+
const { role, errors: e } = normalizeRole(b.integration, "integration", "pr", "anyone");
|
|
792
|
+
config.branches.integration = role;
|
|
793
|
+
errors.push(...e);
|
|
794
|
+
} else errors.push("branches.integration is required");
|
|
795
|
+
if (b.preview !== void 0) {
|
|
796
|
+
const { role, errors: e } = normalizeRole(b.preview, "preview", "pr", "anyone");
|
|
797
|
+
config.branches.preview = role;
|
|
798
|
+
errors.push(...e);
|
|
799
|
+
}
|
|
800
|
+
if (b.production !== void 0) {
|
|
801
|
+
const { role, errors: e } = normalizeRole(b.production, "production", "pr", "user");
|
|
802
|
+
config.branches.production = role;
|
|
803
|
+
errors.push(...e);
|
|
804
|
+
}
|
|
805
|
+
if (b.archive !== void 0) {
|
|
806
|
+
const { role, errors: e } = normalizeRole(b.archive, "archive", "pr", "user");
|
|
807
|
+
config.branches.archive = role;
|
|
808
|
+
errors.push(...e);
|
|
809
|
+
}
|
|
810
|
+
const ci = r.ci ?? {};
|
|
811
|
+
if (typeof ci.enabled === "boolean") config.ci.enabled = ci.enabled;
|
|
812
|
+
if (strict !== void 0) config.strict = strict;
|
|
813
|
+
errors.push(...validateConfig(config));
|
|
814
|
+
return {
|
|
815
|
+
config: errors.length > 0 ? null : config,
|
|
816
|
+
errors,
|
|
817
|
+
warnings,
|
|
818
|
+
...strict !== void 0 ? { strict } : {}
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** 配置校验: 角色分支重叠等(角色条目正则合法性已在 normalizeRole 预编译报错) */
|
|
822
|
+
function validateConfig(config) {
|
|
823
|
+
const errors = [];
|
|
824
|
+
if (config.branches.integration.branches.length === 0) errors.push("branches.integration.branches is required");
|
|
825
|
+
try {
|
|
826
|
+
new RegExp(config.featurePattern);
|
|
827
|
+
} catch {
|
|
828
|
+
errors.push(`featurePattern is not a valid regex: ${config.featurePattern}`);
|
|
829
|
+
}
|
|
830
|
+
const allRoles = [
|
|
831
|
+
"integration",
|
|
832
|
+
"preview",
|
|
833
|
+
"production",
|
|
834
|
+
"archive"
|
|
835
|
+
];
|
|
836
|
+
for (let i = 0; i < allRoles.length; i++) {
|
|
837
|
+
const a = config.branches[allRoles[i]];
|
|
838
|
+
if (!a) continue;
|
|
839
|
+
for (let j = i + 1; j < allRoles.length; j++) {
|
|
840
|
+
const bb = config.branches[allRoles[j]];
|
|
841
|
+
if (!bb) continue;
|
|
842
|
+
if (a.branches.some((s) => bb.branches.includes(s))) errors.push(`branches.${allRoles[i]} and branches.${allRoles[j]} share the same entries`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return errors;
|
|
846
|
+
}
|
|
847
|
+
/** 从项目根加载配置; 无文件 = 未启用(opt-in) */
|
|
848
|
+
async function loadConfig(repoRoot) {
|
|
849
|
+
let text;
|
|
850
|
+
try {
|
|
851
|
+
text = await readFile(join(repoRoot, CONFIG_FILE), "utf8");
|
|
852
|
+
} catch (e) {
|
|
853
|
+
if (e.code === "ENOENT") return {
|
|
854
|
+
config: null,
|
|
855
|
+
errors: [],
|
|
856
|
+
warnings: []
|
|
857
|
+
};
|
|
858
|
+
return {
|
|
859
|
+
config: null,
|
|
860
|
+
errors: [`Failed to read config file: ${e.message}`],
|
|
861
|
+
warnings: []
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
let raw;
|
|
865
|
+
try {
|
|
866
|
+
raw = JSON.parse(text);
|
|
867
|
+
} catch (e) {
|
|
868
|
+
const strict = /"strict"\s*:\s*true/.test(text) || void 0;
|
|
869
|
+
return {
|
|
870
|
+
config: null,
|
|
871
|
+
errors: [`Failed to read config file: ${e.message}`],
|
|
872
|
+
warnings: [],
|
|
873
|
+
...strict ? { strict } : {}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
return mergeConfig(raw);
|
|
775
877
|
}
|
|
776
878
|
//#endregion
|
|
777
879
|
//#region src/gate.ts
|
|
@@ -818,6 +920,7 @@ function decide(classified, facts, config, t = defaultT) {
|
|
|
818
920
|
case "pr-merge": return decidePrMerge(classified, facts, config, t);
|
|
819
921
|
case "branch-delete": return isProtected(roleOfBranch(classified.branch, config)) ? deny(t("denyDeleteOrForce.why", { branch: classified.branch ?? "" }), t("denyDeleteOrForce.next")) : { kind: "allow" };
|
|
820
922
|
case "ref-update": return classified.branch != null && isProtected(roleOfBranch(classified.branch, config)) ? deny(t("refUpdateProtected.why", { branch: classified.branch }), t("refUpdateProtected.next")) : { kind: "allow" };
|
|
923
|
+
case "ref-move": return isProtected(roleOfBranch(facts.currentBranch, config)) ? deny(t("refMoveProtected.why"), t("refMoveProtected.next")) : { kind: "allow" };
|
|
821
924
|
case "guard-cli": return { kind: "allow" };
|
|
822
925
|
case "checkout": return { kind: "allow" };
|
|
823
926
|
default: return { kind: "allow" };
|
|
@@ -1030,7 +1133,7 @@ async function evaluateCommand(command, opts) {
|
|
|
1030
1133
|
segmentCount: 0,
|
|
1031
1134
|
locale: "en"
|
|
1032
1135
|
};
|
|
1033
|
-
const locale = resolveLocale(config.locale);
|
|
1136
|
+
const locale = opts.locale != null ? resolveLocale(opts.locale) : resolveLocale(config.locale);
|
|
1034
1137
|
const t = makeT(locale);
|
|
1035
1138
|
const branch = opts.currentBranch ?? await currentBranch(runner, opts.repoRoot);
|
|
1036
1139
|
const env = {
|
|
@@ -1133,10 +1236,10 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
1133
1236
|
};
|
|
1134
1237
|
return next();
|
|
1135
1238
|
} catch (e) {
|
|
1136
|
-
ctx.logger?.warn?.(`gitflow-guard:
|
|
1239
|
+
ctx.logger?.warn?.(`gitflow-guard: gate internal error, allowed through: ${e.message}`);
|
|
1137
1240
|
return next();
|
|
1138
1241
|
}
|
|
1139
1242
|
});
|
|
1140
1243
|
}
|
|
1141
1244
|
//#endregion
|
|
1142
|
-
export { name as a, findRepoRoot as c,
|
|
1245
|
+
export { name as a, findRepoRoot as c, roleMatches as d, MESSAGE_KEYS as f, classify as g, resolveLocale as h, formatDeny as i, gitRunner as l, registerLocale as m, apply as n, stateDir as o, makeT 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.
|
|
3
|
+
"version": "0.0.13",
|
|
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"
|
package/patch.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# agents-gitflow-guard
|
|
2
|
-
#
|
|
1
|
+
# agents-gitflow-guard DSH mount layer (applied into the profile after `dsh plugin add`)
|
|
2
|
+
# Rows here can be overridden per-id in the profile's cordis.patch.yml
|
|
3
3
|
|
|
4
4
|
- insert:
|
|
5
5
|
- id: gitflow-guard
|