agents-gitflow-guard 0.0.12 → 0.0.14
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 +20 -11
- package/README.zh.md +22 -12
- package/lib/cli.mjs +1 -1
- package/lib/index.d.mts +13 -1
- package/lib/index.mjs +2 -2
- package/lib/{src-CPX1Ks_Z.mjs → src-C1CZM7Q4.mjs} +127 -16
- package/package.json +1 -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.14
|
|
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
|
|
@@ -131,7 +132,7 @@ Nobody has to remember the rules — the rules are enforced.
|
|
|
131
132
|
- **Role-driven, fully configurable**: `integration` is the only required role; `preview` / `production` / `archive` are optional arrays of branch names or regexes, each with its own update rules (`pr` / `flexible`, `mergeBy`).
|
|
132
133
|
- **Merge-by-user where it matters**: production & archive merges stay in your hands — the plugin blocks the agent from clicking merge, so your action *is* the confirmation.
|
|
133
134
|
- **Works with any naming**: branch names are mapped by your config, never hard-coded (see [Configuration](#configuration-reference)).
|
|
134
|
-
- **Fully audited**: every deny is
|
|
135
|
+
- **Fully audited**: every deny is appended to an audit log under your user state directory (`~/.local/state/gitflow-guard/`, `%LOCALAPPDATA%\gitflow-guard` on Windows) — outside the repository, never committed, and outside the agent's writable sandbox.
|
|
135
136
|
- **Platform-agnostic core**: pure local git; optionally consults `gh` (GitHub) or `glab` (GitLab) for PR/MR target resolution, and works fine without them.
|
|
136
137
|
|
|
137
138
|
---
|
|
@@ -167,7 +168,7 @@ Why this matters: branch protection answers *"can this push happen at all?"*; th
|
|
|
167
168
|
|
|
168
169
|
1. An agent calls a shell tool (`pwsh` / `bash`) with a git command.
|
|
169
170
|
2. The plugin classifies the command, resolves the branch roles from `gitflow-guard.config.json`, and applies the gate matrix.
|
|
170
|
-
3. Violation → the tool call is **denied before it runs**, with a reason and the next step. Allowed → the command proceeds; every deny is audited to
|
|
171
|
+
3. Violation → the tool call is **denied before it runs**, with a reason and the next step. Allowed → the command proceeds; every deny is audited to the user-level log (`~/.local/state/gitflow-guard/repos/<repo>-<hash>/audit.jsonl`).
|
|
171
172
|
|
|
172
173
|
No chat-confirmation or permit store: sensitive merges (production / archive) are simply **user-only** — an agent may prepare the PR/MR, but the merge click stays yours.
|
|
173
174
|
|
|
@@ -268,6 +269,14 @@ archive (optional; you archive after release)
|
|
|
268
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.
|
|
269
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.
|
|
270
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
|
+
```
|
|
271
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`.
|
|
272
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.
|
|
273
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.
|
|
@@ -293,17 +302,17 @@ The PR/MR target is resolved via `gh pr view` (GitHub) or `glab mr view` (GitLab
|
|
|
293
302
|
## Where the human stays in control
|
|
294
303
|
|
|
295
304
|
- **Production merge** and **archive** are user-only by default: an agent may help prepare the PR/MR, but **you click the merge button** — that click *is* the confirmation. There is no separate permit store to outsource that decision.
|
|
296
|
-
- Every deny is
|
|
305
|
+
- Every deny is appended to the user-level audit log for review (`gitflow-guard audit`).
|
|
297
306
|
|
|
298
307
|
---
|
|
299
308
|
## Installation in detail
|
|
300
309
|
|
|
301
|
-
**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).
|
|
302
311
|
|
|
303
312
|
**From the npm registry** — the standard path, already covered in [Quick Start](#quick-start--30-seconds-to-a-guarded-repo):
|
|
304
313
|
|
|
305
314
|
```bash
|
|
306
|
-
dsh plugin --profile web add agents-gitflow-guard@0.0.
|
|
315
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.14 # pin recommended, see note above
|
|
307
316
|
```
|
|
308
317
|
|
|
309
318
|
Then restart DSH. Upgrades are the same command, followed by another restart.
|
|
@@ -403,7 +412,7 @@ What remains **locally non-defensible**: direct forge-API calls (`gh api repos/
|
|
|
403
412
|
|
|
404
413
|
### Why can't the agent just merge into production/archive itself?
|
|
405
414
|
|
|
406
|
-
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.
|
|
407
416
|
|
|
408
417
|
---
|
|
409
418
|
|
|
@@ -462,7 +471,7 @@ If it saves your team from a shortcut gone wrong, the coffee button at the top o
|
|
|
462
471
|
## Roadmap
|
|
463
472
|
|
|
464
473
|
- **i18n — localized block messages** ✅ (0.0.3): English by default, `"locale": "zh"` for Chinese.
|
|
465
|
-
- **v2 — audit sync**: sync
|
|
474
|
+
- **v2 — audit sync**: sync the user-level audit log across machines (audit is local-only today).
|
|
466
475
|
- **v2 — more pre-built templates**: ready-made config templates for common flows (solo `develop`, multi-env enterprise) as community-contributed presets.
|
|
467
476
|
- **v2 — CI hard-gating research**: whether `pr checks` could become a real gate without hurting the platform-agnostic core.
|
|
468
477
|
|
|
@@ -482,7 +491,7 @@ The plugin is free and open source (MIT). If it saves you and your team from a s
|
|
|
482
491
|
|
|
483
492
|
```bash
|
|
484
493
|
npm install
|
|
485
|
-
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
|
|
486
495
|
npm run typecheck # tsc --noEmit, 0 errors
|
|
487
496
|
npm run build # tsdown → lib/ (CLI and plugin share the build)
|
|
488
497
|
npm run verify:matrix # continuous cross-agent regression: DSH logic + zh-locale regression + Claude Code / Codex / OpenCode / Antigravity hook wiring
|
|
@@ -490,7 +499,7 @@ npm run verify:matrix # continuous cross-agent regression: DSH logic + zh-locale
|
|
|
490
499
|
|
|
491
500
|
**Rule**: any logic change must pass a 0-error build + all green tests + a green `verify:matrix` before done.
|
|
492
501
|
|
|
493
|
-
**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.)
|
|
494
503
|
|
|
495
504
|
---
|
|
496
505
|
|
|
@@ -498,4 +507,4 @@ npm run verify:matrix # continuous cross-agent regression: DSH logic + zh-locale
|
|
|
498
507
|
|
|
499
508
|
[MIT](LICENSE) © FeatureAgents
|
|
500
509
|
|
|
501
|
-
|
|
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.14
|
|
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
|
|
@@ -128,7 +129,7 @@ AI 编码 agent 在你的仓库里工作。它通过系统提示词、项目智
|
|
|
128
129
|
- **角色驱动、完全可配**:`integration` 是唯一必填;`preview` / `production` / `archive` 是可选数组(精确名或正则),每个角色独立 `update`(`pr` / `flexible`)与 `mergeBy`。
|
|
129
130
|
- **在关键处保留人的操作权**:生产与归档合并始终在你手上——插件阻止 agent 点击合并,于是你的动作*就是*确认。
|
|
130
131
|
- **任何命名都行**:分支名全由配置映射,绝无硬编码(见[配置参考](#配置参考))。
|
|
131
|
-
-
|
|
132
|
+
- **全程审计**:每次拦截都追加到用户级状态目录(macOS/Linux `~/.local/state/gitflow-guard/`,Windows `%LOCALAPPDATA%\gitflow-guard`)下的审计日志——在仓库外、绝不进版本库,且位于 agent 可写沙箱之外。
|
|
132
133
|
- **平台无关核心**:纯本地 git;可选调用 `gh`(GitHub)或 `glab`(GitLab)做 PR/MR 目标解析,没有它们照样工作。
|
|
133
134
|
|
|
134
135
|
---
|
|
@@ -164,7 +165,7 @@ AI 编码 agent 在你的仓库里工作。它通过系统提示词、项目智
|
|
|
164
165
|
|
|
165
166
|
1. agent 调用 shell 工具(`pwsh`/`bash`)执行一条 git 命令。
|
|
166
167
|
2. 插件分类该命令,从 `gitflow-guard.config.json` 解析分支角色,套用门禁矩阵。
|
|
167
|
-
3. 违规 → 工具调用在**运行前被拒绝**,附原因和下一步;放行 →
|
|
168
|
+
3. 违规 → 工具调用在**运行前被拒绝**,附原因和下一步;放行 → 命令照常执行,每次拦截都写入用户级日志(`~/.local/state/gitflow-guard/repos/<repo>-<hash>/audit.jsonl`)。
|
|
168
169
|
|
|
169
170
|
没有聊天确认、也没有特许库:敏感合并(生产/归档)就是**仅用户**——agent 可以帮你准备 PR/MR,但点合并的始终是你。
|
|
170
171
|
|
|
@@ -265,6 +266,13 @@ archive(可选, 发布后你亲手归档)
|
|
|
265
266
|
- 每条分支条目是精确名或正则(自动识别)。**正则安全**:分支正则由项目作者提供并按原样编译——`featurePattern` 与分支条目请避免灾难性回溯写法(如 `(\w+)+` 这类嵌套量词)。
|
|
266
267
|
- **文案语言**:默认英文;加 `"locale": "zh"` 切中文,或给任意 `gitflow-guard` 子命令传 `--locale <en|zh>`(优先级:CLI 旗标 > 项目配置 > 英文)。全部用户可见文案都跟随 locale——包括 `--help`、未知子命令提示、审计为空的提示等 CLI 框架文案。
|
|
267
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
|
+
```
|
|
268
276
|
- **未注册语言**:拦截路径对未注册的 `"locale"` 静默回退英文(设计如此——hook 不因文案缺失卡死),笔误因此容易被忽略;一行告警在 `gitflow-guard status` 中可见。
|
|
269
277
|
- **校验**:`integration` 必填;角色条目重叠会被拒;非法正则会报错。**任何错误都会让该项目的插件禁用并上报**(而不是用半吊子配置)。
|
|
270
278
|
- **strict 模式**:默认配置损坏时 stderr 告警一次后放行(fail-open,避免一个笔误卡死工具管道);`"strict": true` 把配置异常与内部错误翻转为**拦截**(fail-closed)——供高风险仓库选用。文件不存在或显式 `enabled: false` 两种模式下都保持静默。
|
|
@@ -279,7 +287,7 @@ archive(可选, 发布后你亲手归档)
|
|
|
279
287
|
| 直推 / 强推 / 删除 integration / preview / production / archive | 🚫 拦(integration/preview 配 `flexible` 时直推放行) |
|
|
280
288
|
| PR/MR: feature → integration / preview | ✅ 放行 |
|
|
281
289
|
| PR/MR: feature → production | ✅ 可创建;**合并被拦**(你在 UI 合并) |
|
|
282
|
-
| 指向 archive 的 PR/MR |
|
|
290
|
+
| 指向 archive 的 PR/MR | ✅ 可创建;🚫 合并被拦(你在 UI 合并) |
|
|
283
291
|
| 在 integration / preview 上 `git merge feature/x`(本地) | 🚫 拦(须 PR/MR);`update: flexible` 则放行 |
|
|
284
292
|
| 串联命令(`checkout develop && merge feature/x`) | 🚫 拦——逐段模拟分支切换,无法绕序 |
|
|
285
293
|
|
|
@@ -290,17 +298,17 @@ PR/MR 目标通过 `gh pr view`(GitHub)或 `glab mr view`(GitLab)解析;没有
|
|
|
290
298
|
## 人保持控制权的地方
|
|
291
299
|
|
|
292
300
|
- **生产合并与归档**默认仅用户:agent 可以帮你准备 PR/MR,但**合并按钮由你点**——那个点击*就是*确认。没有独立特许库能把这决定外包出去。
|
|
293
|
-
-
|
|
301
|
+
- 每次拦截都追加到用户级审计日志供查阅(`gitflow-guard audit`)。
|
|
294
302
|
|
|
295
303
|
---
|
|
296
304
|
## 安装详解
|
|
297
305
|
|
|
298
|
-
**前置**:一个可用的 [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 安装,同样需要运行时)。
|
|
299
307
|
|
|
300
308
|
**从 npm registry**——标准路径,已在[快速开始](#快速开始30-秒用上)覆盖:
|
|
301
309
|
|
|
302
310
|
```bash
|
|
303
|
-
dsh plugin --profile web add agents-gitflow-guard@0.0.
|
|
311
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.14 # 建议锁版本, 见上文提示
|
|
304
312
|
```
|
|
305
313
|
|
|
306
314
|
然后重启 DSH。升级用同一命令,再重启一次。
|
|
@@ -400,7 +408,7 @@ hooks:
|
|
|
400
408
|
|
|
401
409
|
### 为什么 agent 不能自己合并进生产/归档?
|
|
402
410
|
|
|
403
|
-
|
|
411
|
+
因为门禁把那些动作判定为**仅用户**。插件对生产的*合并*、归档的*合并*一律拦截——*建 PR/MR 允许*,agent 仍可替你起草 develop→main 归档 PR。但合并本身只有一条路径:**你**亲手点合并——不存在 agent 能用来给自己授权的特许、令牌或聊天消息。
|
|
404
412
|
|
|
405
413
|
---
|
|
406
414
|
|
|
@@ -448,7 +456,7 @@ MIT,免费,无条件。随便用、随便改、随便发,唯一义务是保留
|
|
|
448
456
|
| **integration** | 集成分支,唯一必填角色(`branches.integration`);feature 经 PR/MR 合入;受保护 |
|
|
449
457
|
| **preview** | 可选环境终点分支(`branches.preview`,数组);只走 PR/MR 更新 |
|
|
450
458
|
| **production** | 可选生产分支(`branches.production`,数组);PR/MR + 合并仅限用户 |
|
|
451
|
-
| **archive** | `branches.archive
|
|
459
|
+
| **archive** | 可选的发布后归档分支(`branches.archive`,数组);允许 agent 创建指向它的 PR/MR,合并仍限用户亲手 |
|
|
452
460
|
| **feature 分支** | 你的工作分支,由 `featurePattern` 识别;自由区 |
|
|
453
461
|
| **门禁矩阵** | 把每条被分类的命令映射为放行/拦截的判定表 |
|
|
454
462
|
| **pre-execute** | 工具管线中拦截发生的钩子——在命令运行之前 |
|
|
@@ -459,7 +467,7 @@ MIT,免费,无条件。随便用、随便改、随便发,唯一义务是保留
|
|
|
459
467
|
## 路线图
|
|
460
468
|
|
|
461
469
|
- **i18n——拦截文案本地化** ✅(0.0.3):默认英文,`"locale": "zh"` 切中文。
|
|
462
|
-
- **v2
|
|
470
|
+
- **v2——审计同步**:跨机器同步用户级审计日志(现仅本地)。
|
|
463
471
|
- **v2——更多预制模板**:常用流程(solo `develop`、多环境企业)的现成配置模板,由社区贡献。
|
|
464
472
|
- **v2——CI 硬门槛研究**:`pr checks` 能否在不伤平台无关核心的前提下变成真实门槛。
|
|
465
473
|
|
|
@@ -479,7 +487,7 @@ MIT,免费,无条件。随便用、随便改、随便发,唯一义务是保留
|
|
|
479
487
|
|
|
480
488
|
```bash
|
|
481
489
|
npm install
|
|
482
|
-
npm test # 单测: classify / gate / config / cli / repo / platform
|
|
490
|
+
npm test # 单测: classify / gate / config / cli / repo / platform / i18n / index / accuracy-audit
|
|
483
491
|
npm run typecheck # tsc --noEmit, 0 Error
|
|
484
492
|
npm run build # tsdown → lib/(CLI 与插件共用)
|
|
485
493
|
npm run verify:matrix # 连续复测矩阵: DSH 逻辑 + zh 文案回归 + Claude Code / Codex / OpenCode / Antigravity hook 编码
|
|
@@ -487,10 +495,12 @@ npm run verify:matrix # 连续复测矩阵: DSH 逻辑 + zh 文案回归 + Claud
|
|
|
487
495
|
|
|
488
496
|
**铁律**:任何逻辑改动必须 0 Error 构建 + 单测全绿 + 连续复测矩阵(`verify:matrix`)全绿后才算完成。
|
|
489
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 清单,见该节例外说明。)
|
|
499
|
+
|
|
490
500
|
---
|
|
491
501
|
|
|
492
502
|
## 许可证
|
|
493
503
|
|
|
494
504
|
[MIT](LICENSE) © FeatureAgents
|
|
495
505
|
|
|
496
|
-
|
|
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
|
|
1
|
+
import { _ as classify, c as currentBranch, d as loadConfig, f as roleMatches, g as resolveLocale, i as formatDeny, l as findRepoRoot, m as makeT, o as stateDir, r as evaluateCommand, u as gitRunner } from "./src-C1CZM7Q4.mjs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
//#region src/platform.ts
|
package/lib/index.d.mts
CHANGED
|
@@ -11,6 +11,7 @@ type I18nVars = Record<string, string>;
|
|
|
11
11
|
type Entry = (v: I18nVars) => string;
|
|
12
12
|
/** 一门语言的文案字典: key 集合必须与内置 en 完全一致(registerLocale / 加载期校验) */
|
|
13
13
|
type Dict = Record<string, Entry>;
|
|
14
|
+
declare const MESSAGE_KEYS: readonly string[];
|
|
14
15
|
/**
|
|
15
16
|
* 注册一门新语言(运行时扩展点, P2-2): key 集合必须与内置 en 完全一致, 否则抛英文异常。
|
|
16
17
|
* 注册后 makeT/resolveLocale 即接受该 locale; 未注册的 locale 一律回退英文。
|
|
@@ -52,6 +53,17 @@ interface AuditEntry {
|
|
|
52
53
|
role?: string;
|
|
53
54
|
reason?: string;
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* 用户级运行时状态根目录(仓库外): macOS/Linux 走 XDG state, Windows 走 %LOCALAPPDATA%。
|
|
58
|
+
* GITFLOW_GUARD_STATE_ROOT 显式覆盖所有平台默认值(测试/特殊部署用)。
|
|
59
|
+
*/
|
|
60
|
+
declare function userStateRoot(): string;
|
|
61
|
+
/**
|
|
62
|
+
* 仓库运行时状态目录(审计流水等), 键为「仓库名-真实路径哈希」。
|
|
63
|
+
* 刻意放在仓库外、且在 agent 文件沙箱(workspace-write)可写区之外:
|
|
64
|
+
* 凡 agent 可写之处的状态都可能被 agent 伪造而自我授权, 存仓库外才堵住这条路;
|
|
65
|
+
* 附带收益: 重克隆/移动 .git 不丢历史。
|
|
66
|
+
*/
|
|
55
67
|
declare function stateDir(repoRoot: string): string;
|
|
56
68
|
/** 审计留痕; 失败不阻断门禁 */
|
|
57
69
|
declare function appendAudit(repoRoot: string, entry: AuditEntry): Promise<void>;
|
|
@@ -60,4 +72,4 @@ declare function evaluateCommand(command: string, opts: EvaluateOptions): Promis
|
|
|
60
72
|
declare function formatDeny(locale: Locale, why: string, next: string): string;
|
|
61
73
|
declare function apply(ctx: Context, pluginConfig?: PluginConfig): void;
|
|
62
74
|
//#endregion
|
|
63
|
-
export { AuditEntry, type Dict, EvaluateOptions, EvaluateResult, PluginConfig, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir };
|
|
75
|
+
export { AuditEntry, type Dict, EvaluateOptions, EvaluateResult, MESSAGE_KEYS, PluginConfig, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir, userStateRoot };
|
package/lib/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as name, i as formatDeny, n as apply, o as stateDir, p as
|
|
2
|
-
export { appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir };
|
|
1
|
+
import { a as name, h as registerLocale, i as formatDeny, n as apply, o as stateDir, p as MESSAGE_KEYS, r as evaluateCommand, s as userStateRoot, t as appendAudit } from "./src-C1CZM7Q4.mjs";
|
|
2
|
+
export { MESSAGE_KEYS, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir, userStateRoot };
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, join } from "node:path";
|
|
3
6
|
import { execFile } from "node:child_process";
|
|
4
7
|
//#region src/classify.ts
|
|
5
8
|
/** 拆分命令为多段(&& / || / | / 分号 / 换行), 每段独立分类; 引号内的分隔符不算 */
|
|
@@ -174,10 +177,30 @@ function classifyGit(args, ctx) {
|
|
|
174
177
|
if (sub === "push") return parsePush(rest, ctx);
|
|
175
178
|
if (sub === "pull") return parsePull(rest);
|
|
176
179
|
if (sub === "merge") return parseMerge(rest);
|
|
177
|
-
if (sub === "branch") return parseBranch(rest);
|
|
180
|
+
if (sub === "branch") return parseBranch(rest, ctx);
|
|
178
181
|
if (sub === "checkout" || sub === "switch") return parseCheckout(rest);
|
|
179
182
|
if (sub === "send-pack") return parseSendPack(rest);
|
|
180
183
|
if (sub === "update-ref") return parseUpdateRef(rest);
|
|
184
|
+
if (sub === "reset" || sub === "filter-branch") return [{ kind: "ref-move" }];
|
|
185
|
+
if (sub === "rebase") return parseRebase(rest);
|
|
186
|
+
if (sub === "commit") return parseCommit(rest);
|
|
187
|
+
return [{ kind: "other" }];
|
|
188
|
+
}
|
|
189
|
+
/** rebase 移动当前分支 ref; abort/continue/skip 等恢复类旗标不移动(放行, 避免把用户困在中途态) */
|
|
190
|
+
function parseRebase(args) {
|
|
191
|
+
const RESUME = /* @__PURE__ */ new Set([
|
|
192
|
+
"--abort",
|
|
193
|
+
"--continue",
|
|
194
|
+
"--skip",
|
|
195
|
+
"--quit",
|
|
196
|
+
"--edit-todo"
|
|
197
|
+
]);
|
|
198
|
+
if (args.some((a) => RESUME.has(a))) return [{ kind: "other" }];
|
|
199
|
+
return [{ kind: "ref-move" }];
|
|
200
|
+
}
|
|
201
|
+
/** commit 仅 --amend 改写当前分支 tip; 普通提交不移动既有 ref */
|
|
202
|
+
function parseCommit(args) {
|
|
203
|
+
if (args.some((a) => a === "--amend")) return [{ kind: "ref-move" }];
|
|
181
204
|
return [{ kind: "other" }];
|
|
182
205
|
}
|
|
183
206
|
/** 剥离子命令前的全局选项(-C <path> / -c <k=v> / --git-dir 等), 否则 git -C . push 会被判 other */
|
|
@@ -388,12 +411,60 @@ function parseMerge(args) {
|
|
|
388
411
|
source: args.find((a, i) => !a.startsWith("-") && args[i - 1] !== "-m" && args[i - 1] !== "--message") ?? null
|
|
389
412
|
}];
|
|
390
413
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
414
|
+
/**
|
|
415
|
+
* git branch 全旗标扫描(旧实现只读 args[0..1], `-d --force develop` 这类组合长旗标会漏):
|
|
416
|
+
* - 删除(-d/-D/--delete, 可与 --force 组合): 逐个分支名 → branch-delete
|
|
417
|
+
* - 改名(-m/-M/--move): 移动受保护 ref(源)或覆盖受保护名(目标)→ 按 ref-update 同级处理
|
|
418
|
+
* - 强制复位(-f/--force 单独使用): git branch -f <name> <commit> 静默移动分支指针 → ref-update
|
|
419
|
+
*/
|
|
420
|
+
function parseBranch(args, ctx) {
|
|
421
|
+
let deleteFlag = false;
|
|
422
|
+
let force = false;
|
|
423
|
+
let move = false;
|
|
424
|
+
const names = [];
|
|
425
|
+
for (const a of args) if (a.startsWith("--")) {
|
|
426
|
+
if (a === "--delete") deleteFlag = true;
|
|
427
|
+
else if (a === "--force") force = true;
|
|
428
|
+
else if (a === "--move") move = true;
|
|
429
|
+
} else if (a.startsWith("-") && a.length > 1) {
|
|
430
|
+
for (const ch of a.slice(1)) if (ch === "d") deleteFlag = true;
|
|
431
|
+
else if (ch === "D") {
|
|
432
|
+
deleteFlag = true;
|
|
433
|
+
force = true;
|
|
434
|
+
} else if (ch === "m") move = true;
|
|
435
|
+
else if (ch === "M") {
|
|
436
|
+
move = true;
|
|
437
|
+
force = true;
|
|
438
|
+
} else if (ch === "f") force = true;
|
|
439
|
+
} else names.push(a);
|
|
440
|
+
if (move) {
|
|
441
|
+
const from = names.length >= 2 ? names[0] : ctx.currentBranch ?? null;
|
|
442
|
+
const to = names.length >= 1 ? names[names.length - 1] : null;
|
|
443
|
+
const out = [];
|
|
444
|
+
if (from != null) out.push({
|
|
445
|
+
kind: "ref-update",
|
|
446
|
+
branch: from,
|
|
447
|
+
delete: false
|
|
448
|
+
});
|
|
449
|
+
if (to != null && to !== from) out.push({
|
|
450
|
+
kind: "ref-update",
|
|
451
|
+
branch: to,
|
|
452
|
+
delete: false
|
|
453
|
+
});
|
|
454
|
+
return out.length > 0 ? out : [{ kind: "other" }];
|
|
455
|
+
}
|
|
456
|
+
if (deleteFlag) {
|
|
457
|
+
if (names.length === 0) return [{ kind: "other" }];
|
|
458
|
+
return names.map((branch) => ({
|
|
459
|
+
kind: "branch-delete",
|
|
460
|
+
branch,
|
|
461
|
+
force
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
if (force && names.length >= 1) return [{
|
|
465
|
+
kind: "ref-update",
|
|
466
|
+
branch: names[0],
|
|
467
|
+
delete: false
|
|
397
468
|
}];
|
|
398
469
|
return [{ kind: "other" }];
|
|
399
470
|
}
|
|
@@ -457,6 +528,8 @@ const en = {
|
|
|
457
528
|
"denyDeleteOrForce.next": () => "Delete/force-push on a feature branch outside the protected branches; protected branches are managed by you.",
|
|
458
529
|
"refUpdateProtected.why": (v) => `Protected branch "${v.branch}" forbids direct ref updates (update-ref)`,
|
|
459
530
|
"refUpdateProtected.next": () => "Update protected branches via PR/MR; they are managed by you.",
|
|
531
|
+
"refMoveProtected.why": () => "Rewriting history on a protected branch (reset / rebase / commit --amend / filter-branch) is not allowed",
|
|
532
|
+
"refMoveProtected.next": () => "Do history rewrites on a feature branch; protected branches advance via PR/MR and are managed by you.",
|
|
460
533
|
"pushAll.why": () => "--all/--mirror push would include protected branches",
|
|
461
534
|
"pushAll.next": () => "Push branch by branch with an explicit refspec.",
|
|
462
535
|
"pushDetached.why": () => "Cannot determine the push target (detached HEAD?)",
|
|
@@ -532,6 +605,8 @@ const zh = {
|
|
|
532
605
|
"denyDeleteOrForce.next": () => "删除/强推请到受保护分支外的 feature 分支上操作; 受保护分支由用户亲手管理",
|
|
533
606
|
"refUpdateProtected.why": (v) => `受保护分支「${v.branch}」禁止直接改写 refs(update-ref)`,
|
|
534
607
|
"refUpdateProtected.next": () => "请通过 PR/MR 更新受保护分支; 受保护分支由用户亲手管理",
|
|
608
|
+
"refMoveProtected.why": () => "受保护分支禁止本地改写历史(reset / rebase / commit --amend / filter-branch)",
|
|
609
|
+
"refMoveProtected.next": () => "历史改写请在 feature 分支上进行; 受保护分支仅经 PR/MR 推进, 由用户亲手管理",
|
|
535
610
|
"pushAll.why": () => "--all/--mirror 推送会包含受保护分支",
|
|
536
611
|
"pushAll.next": () => "请逐分支推送并显式指定 refspec",
|
|
537
612
|
"pushDetached.why": () => "无法确定推送目标分支(可能处于 detached HEAD)",
|
|
@@ -656,7 +731,7 @@ function roleMatches(branch, role) {
|
|
|
656
731
|
return role.branches.some((spec) => matchBranchSpec(branch, spec));
|
|
657
732
|
}
|
|
658
733
|
/** 规范化用户输入的某个角色: 数组 或 {branches:[...], update?, mergeBy?} */
|
|
659
|
-
function normalizeRole(raw, defaultUpdate, defaultMergeBy) {
|
|
734
|
+
function normalizeRole(raw, roleName, defaultUpdate, defaultMergeBy) {
|
|
660
735
|
const errors = [];
|
|
661
736
|
let arr;
|
|
662
737
|
let update = void 0;
|
|
@@ -673,6 +748,11 @@ function normalizeRole(raw, defaultUpdate, defaultMergeBy) {
|
|
|
673
748
|
};
|
|
674
749
|
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
750
|
const role = { branches: (Array.isArray(arr) ? arr : []).filter((x) => typeof x === "string" && x !== "") };
|
|
751
|
+
for (const spec of role.branches) try {
|
|
752
|
+
new RegExp(`^(?:${spec})$`);
|
|
753
|
+
} catch {
|
|
754
|
+
errors.push(`branches.${roleName} entry is not a valid regex: ${spec}`);
|
|
755
|
+
}
|
|
676
756
|
if (update === void 0 || update === "pr" || update === "flexible") role.update = update === void 0 ? defaultUpdate : update;
|
|
677
757
|
else errors.push("update must be \"pr\" or \"flexible\"");
|
|
678
758
|
if (mergeBy === void 0 || mergeBy === "user" || mergeBy === "anyone") role.mergeBy = mergeBy === void 0 ? defaultMergeBy : mergeBy;
|
|
@@ -711,22 +791,22 @@ function mergeConfig(raw) {
|
|
|
711
791
|
} else if (r.locale !== void 0) errors.push("locale must be a string");
|
|
712
792
|
const b = r.branches ?? {};
|
|
713
793
|
if ("integration" in b) {
|
|
714
|
-
const { role, errors: e } = normalizeRole(b.integration, "pr", "anyone");
|
|
794
|
+
const { role, errors: e } = normalizeRole(b.integration, "integration", "pr", "anyone");
|
|
715
795
|
config.branches.integration = role;
|
|
716
796
|
errors.push(...e);
|
|
717
797
|
} else errors.push("branches.integration is required");
|
|
718
798
|
if (b.preview !== void 0) {
|
|
719
|
-
const { role, errors: e } = normalizeRole(b.preview, "pr", "anyone");
|
|
799
|
+
const { role, errors: e } = normalizeRole(b.preview, "preview", "pr", "anyone");
|
|
720
800
|
config.branches.preview = role;
|
|
721
801
|
errors.push(...e);
|
|
722
802
|
}
|
|
723
803
|
if (b.production !== void 0) {
|
|
724
|
-
const { role, errors: e } = normalizeRole(b.production, "pr", "user");
|
|
804
|
+
const { role, errors: e } = normalizeRole(b.production, "production", "pr", "user");
|
|
725
805
|
config.branches.production = role;
|
|
726
806
|
errors.push(...e);
|
|
727
807
|
}
|
|
728
808
|
if (b.archive !== void 0) {
|
|
729
|
-
const { role, errors: e } = normalizeRole(b.archive, "pr", "user");
|
|
809
|
+
const { role, errors: e } = normalizeRole(b.archive, "archive", "pr", "user");
|
|
730
810
|
config.branches.archive = role;
|
|
731
811
|
errors.push(...e);
|
|
732
812
|
}
|
|
@@ -741,7 +821,7 @@ function mergeConfig(raw) {
|
|
|
741
821
|
...strict !== void 0 ? { strict } : {}
|
|
742
822
|
};
|
|
743
823
|
}
|
|
744
|
-
/** 配置校验:
|
|
824
|
+
/** 配置校验: 角色分支重叠等(角色条目正则合法性已在 normalizeRole 预编译报错) */
|
|
745
825
|
function validateConfig(config) {
|
|
746
826
|
const errors = [];
|
|
747
827
|
if (config.branches.integration.branches.length === 0) errors.push("branches.integration.branches is required");
|
|
@@ -843,6 +923,7 @@ function decide(classified, facts, config, t = defaultT) {
|
|
|
843
923
|
case "pr-merge": return decidePrMerge(classified, facts, config, t);
|
|
844
924
|
case "branch-delete": return isProtected(roleOfBranch(classified.branch, config)) ? deny(t("denyDeleteOrForce.why", { branch: classified.branch ?? "" }), t("denyDeleteOrForce.next")) : { kind: "allow" };
|
|
845
925
|
case "ref-update": return classified.branch != null && isProtected(roleOfBranch(classified.branch, config)) ? deny(t("refUpdateProtected.why", { branch: classified.branch }), t("refUpdateProtected.next")) : { kind: "allow" };
|
|
926
|
+
case "ref-move": return isProtected(roleOfBranch(facts.currentBranch, config)) ? deny(t("refMoveProtected.why"), t("refMoveProtected.next")) : { kind: "allow" };
|
|
846
927
|
case "guard-cli": return { kind: "allow" };
|
|
847
928
|
case "checkout": return { kind: "allow" };
|
|
848
929
|
default: return { kind: "allow" };
|
|
@@ -1034,8 +1115,38 @@ function resolvePrTarget(info, config) {
|
|
|
1034
1115
|
//#endregion
|
|
1035
1116
|
//#region src/index.ts
|
|
1036
1117
|
const name = "gitflow-guard";
|
|
1118
|
+
/**
|
|
1119
|
+
* 用户级运行时状态根目录(仓库外): macOS/Linux 走 XDG state, Windows 走 %LOCALAPPDATA%。
|
|
1120
|
+
* GITFLOW_GUARD_STATE_ROOT 显式覆盖所有平台默认值(测试/特殊部署用)。
|
|
1121
|
+
*/
|
|
1122
|
+
function userStateRoot() {
|
|
1123
|
+
const override = process.env.GITFLOW_GUARD_STATE_ROOT?.trim();
|
|
1124
|
+
if (override) return override;
|
|
1125
|
+
if (process.platform === "win32") {
|
|
1126
|
+
const local = process.env.LOCALAPPDATA?.trim() || join(homedir(), "AppData", "Local");
|
|
1127
|
+
return join(local, "gitflow-guard");
|
|
1128
|
+
}
|
|
1129
|
+
const xdg = process.env.XDG_STATE_HOME?.trim();
|
|
1130
|
+
return join(xdg || join(homedir(), ".local", "state"), "gitflow-guard");
|
|
1131
|
+
}
|
|
1132
|
+
function canonicalRepoRoot(repoRoot) {
|
|
1133
|
+
try {
|
|
1134
|
+
return realpathSync(repoRoot);
|
|
1135
|
+
} catch {
|
|
1136
|
+
return repoRoot;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* 仓库运行时状态目录(审计流水等), 键为「仓库名-真实路径哈希」。
|
|
1141
|
+
* 刻意放在仓库外、且在 agent 文件沙箱(workspace-write)可写区之外:
|
|
1142
|
+
* 凡 agent 可写之处的状态都可能被 agent 伪造而自我授权, 存仓库外才堵住这条路;
|
|
1143
|
+
* 附带收益: 重克隆/移动 .git 不丢历史。
|
|
1144
|
+
*/
|
|
1037
1145
|
function stateDir(repoRoot) {
|
|
1038
|
-
|
|
1146
|
+
const real = canonicalRepoRoot(repoRoot);
|
|
1147
|
+
const hash = createHash("sha256").update(real).digest("hex").slice(0, 12);
|
|
1148
|
+
const name = basename(real).replace(/[^\w.-]+/g, "-") || "repo";
|
|
1149
|
+
return join(userStateRoot(), "repos", `${name}-${hash}`);
|
|
1039
1150
|
}
|
|
1040
1151
|
/** 审计留痕; 失败不阻断门禁 */
|
|
1041
1152
|
async function appendAudit(repoRoot, entry) {
|
|
@@ -1164,4 +1275,4 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
1164
1275
|
});
|
|
1165
1276
|
}
|
|
1166
1277
|
//#endregion
|
|
1167
|
-
export { name as a,
|
|
1278
|
+
export { classify as _, name as a, currentBranch as c, loadConfig as d, roleMatches as f, resolveLocale as g, registerLocale as h, formatDeny as i, findRepoRoot as l, makeT as m, apply as n, stateDir as o, MESSAGE_KEYS as p, evaluateCommand as r, userStateRoot as s, appendAudit as t, gitRunner as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-gitflow-guard",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
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",
|
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
|