agents-gitflow-guard 0.0.18 → 0.0.19
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 +28 -14
- package/README.zh.md +28 -14
- package/lib/cli.mjs +1 -1
- package/lib/index.mjs +1 -1
- package/lib/{src-BQYC4N6b.mjs → src-CL6uB3oo.mjs} +120 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,18 +36,26 @@ You define your own branches —
|
|
|
36
36
|
|
|
37
37
|
## Quick Start — 30 seconds to a guarded repo
|
|
38
38
|
|
|
39
|
-
**Step 1 — install
|
|
39
|
+
**Step 1 — install.** All six clients consume the same npm package `agents-gitflow-guard` — pick yours:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
#
|
|
43
|
-
dsh plugin --profile web add agents-gitflow-guard
|
|
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.18
|
|
42
|
+
# DSH — in-process plugin; restart DSH afterwards (plugins load at startup)
|
|
43
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.19
|
|
46
44
|
```
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
```bash
|
|
47
|
+
# Claude Code · Codex · OpenCode · Antigravity — standalone hooks, no DSH needed
|
|
48
|
+
npm i -g agents-gitflow-guard@0.0.19
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Pi — in-process extension
|
|
53
|
+
npm i -D agents-gitflow-guard@0.0.19
|
|
54
|
+
```
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
> **Version gotcha**: a bare `add` or unpinned `npm i` 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. (DSH users: the pnpm peer-dependency *warning* is expected — DSH supplies `@deepseek-ai/cordis` / `@deepseek-ai/dsh-tools` through its shared profile module fallback at startup; the plugin works normally.)
|
|
57
|
+
>
|
|
58
|
+
> The hook clients (Claude Code · Codex · OpenCode · Antigravity) and Pi need one wiring step after install — walk the per-agent table in [Installation in detail](#installation-in-detail).
|
|
51
59
|
|
|
52
60
|
**Step 2 — configure**, create `gitflow-guard.config.json` in your **project root**:
|
|
53
61
|
|
|
@@ -296,6 +304,12 @@ archive (optional; you archive after release)
|
|
|
296
304
|
| PR/MR into archive | ✅ create allowed; 🚫 merge blocked (you merge in UI) |
|
|
297
305
|
| local `git merge feature/x` while on integration / preview | 🚫 block (PR/MR required); `update: flexible` allows |
|
|
298
306
|
| chained commands (`checkout develop && merge feature/x`) | 🚫 blocked — branch switches are simulated per segment, no bypass |
|
|
307
|
+
| force-recreate a protected branch (`git checkout -B/-C <branch>` / `git switch -C`) | 🚫 block (direct ref-update gate) |
|
|
308
|
+
| retarget/delete a protected branch via `git symbolic-ref` | 🚫 block (direct ref-update gate) |
|
|
309
|
+
| `git cherry-pick` / `git revert` while on integration / preview / production / archive | 🚫 block (history rewrite on a protected branch); `-n` / `--no-commit` and `--abort`/`--continue`/`--skip`/`--quit` pass |
|
|
310
|
+
| `sudo`-wrapped git commands (privilege wrapper) | 🚫 wrapper peeled (`sudo -u …` included), underlying command gated |
|
|
311
|
+
|
|
312
|
+
> Two deliberate non-gates, so they don't get "closed" by accident later: `git tag -f` (moving a tag, even pointing at a protected branch) stays exempt — tags are outside the branch-role scope, same as `push --tags`; and a plain `git commit` on a protected branch stays allowed — the guard governs branch roles and merge paths, not content, and the following `git push` is still blocked (remote stays clean).
|
|
299
313
|
|
|
300
314
|
The PR/MR target is resolved via `gh pr view` (GitHub) or `glab mr view` (GitLab). Without a platform CLI, the plugin is conservative.
|
|
301
315
|
|
|
@@ -313,14 +327,14 @@ The PR/MR target is resolved via `gh pr view` (GitHub) or `glab mr view` (GitLab
|
|
|
313
327
|
|
|
314
328
|
| Agent | Install command | After that |
|
|
315
329
|
|---|---|---|
|
|
316
|
-
| DSH | `dsh plugin --profile web add agents-gitflow-guard@0.0.
|
|
317
|
-
| Claude Code · Codex · OpenCode · Antigravity | `npm i -g agents-gitflow-guard@0.0.
|
|
318
|
-
| Pi | `npm i -D agents-gitflow-guard@0.0.
|
|
330
|
+
| DSH | `dsh plugin --profile web add agents-gitflow-guard@0.0.19` | restart DSH — the plugin auto-mounts as a profile layer |
|
|
331
|
+
| Claude Code · Codex · OpenCode · Antigravity | `npm i -g agents-gitflow-guard@0.0.19` | wire a hook to the `gitflow-guard` binary (below) |
|
|
332
|
+
| Pi | `npm i -D agents-gitflow-guard@0.0.19` | copy `pi/gitflow-guard.ts` into `.pi/extensions/` (below) |
|
|
319
333
|
|
|
320
334
|
**DSH — in-process plugin** (the standard path, already covered in [Quick Start](#quick-start--30-seconds-to-a-guarded-repo)):
|
|
321
335
|
|
|
322
336
|
```bash
|
|
323
|
-
dsh plugin --profile web add agents-gitflow-guard@0.0.
|
|
337
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.19 # pin recommended, see note above
|
|
324
338
|
```
|
|
325
339
|
|
|
326
340
|
Then restart DSH. Upgrades are the same command, followed by another restart.
|
|
@@ -337,7 +351,7 @@ The package declares `dsh.bundle.patch`, so `dsh plugin add` automatically makes
|
|
|
337
351
|
**Standalone agent hooks** — Claude Code / Codex / OpenCode / Antigravity, no DSH required. Install the CLI once, then reference the `gitflow-guard` binary in each hook config:
|
|
338
352
|
|
|
339
353
|
```bash
|
|
340
|
-
npm i -g agents-gitflow-guard@0.0.
|
|
354
|
+
npm i -g agents-gitflow-guard@0.0.19 # provides the `gitflow-guard` binary
|
|
341
355
|
```
|
|
342
356
|
|
|
343
357
|
This repo ships project configs at `.claude/settings.json` (Claude Code), `.codex/hooks.json` (Codex), `.opencode/hook/hooks.yaml` (OpenCode) and `.agents/hooks.json` (Antigravity / Google); any other repo adds its own:
|
|
@@ -393,7 +407,7 @@ hooks:
|
|
|
393
407
|
Pi loads extensions in-process (no stdin payload, no subprocess hook). Install the shipped entry point into the project and keep the package in devDependencies:
|
|
394
408
|
|
|
395
409
|
```bash
|
|
396
|
-
npm i -D agents-gitflow-guard@0.0.
|
|
410
|
+
npm i -D agents-gitflow-guard@0.0.19
|
|
397
411
|
mkdir -p .pi/extensions
|
|
398
412
|
cp node_modules/agents-gitflow-guard/pi/gitflow-guard.ts .pi/extensions/gitflow-guard.ts
|
|
399
413
|
```
|
|
@@ -431,7 +445,7 @@ No. Add only the roles your flow actually has. A solo repo with just `develop` c
|
|
|
431
445
|
|
|
432
446
|
No, and it is important that you don't treat it as one. It is a workflow guard: it makes an agreed process mechanically enforceable. Text-based command recognition is inherently best-effort — an agent determined to obfuscate a command can slip past the parser.
|
|
433
447
|
|
|
434
|
-
Within its supported command forms, the role boundary is enforced locally: merging into a protected role branch (integration / preview / production / archive) requires the configured path (PR/MR, or a human merge for production/archive). Standard obfuscation wrappers are classified and blocked — shell wrappers (`sh -c` / `bash -lc`), subshells and backtick/`$()` nesting, `env`/`command`/`nohup`/`xargs` prefixes and `VAR=x` assignments, absolute paths, pipelines and `||` tails, git global options (`-C .`, `--git-dir=…`), wildcard refspecs (`refs/heads/*:refs/heads/*`), `git pull` used as fetch+merge, and the `send-pack`/`update-ref` plumbing. The executable adversarial corpus lives in `tests/accuracy-audit.spec.ts`.
|
|
448
|
+
Within its supported command forms, the role boundary is enforced locally: merging into a protected role branch (integration / preview / production / archive) requires the configured path (PR/MR, or a human merge for production/archive). Standard obfuscation wrappers are classified and blocked — shell wrappers (`sh -c` / `bash -lc`), subshells and backtick/`$()` nesting, `env`/`command`/`nohup`/`xargs`/`sudo` prefixes and `VAR=x` assignments, absolute paths, pipelines and `||` tails, git global options (`-C .`, `--git-dir=…`), wildcard refspecs (`refs/heads/*:refs/heads/*`), `git pull` used as fetch+merge, and the `send-pack`/`update-ref`/`symbolic-ref` plumbing; force-recreating a protected branch (`checkout -B`/`switch -C`) and cherry-pick/revert on a protected branch are blocked by the ref-update / ref-move gates. The executable adversarial corpus lives in `tests/accuracy-audit.spec.ts`.
|
|
435
449
|
|
|
436
450
|
What remains **locally non-defensible**: direct forge-API calls (`gh api repos/…/pulls/N/merge`, `curl`) and commands inside interpreter subprocesses (`node -e "child_process.exec('git push …')"`); arbitrarily deep quoting or encoding stays best-effort by nature. The real, non-bypassable boundary lives in branch-protection rules on your hosting service. Use both — treat this guard as instant feedback and audit trail, not as a security boundary.
|
|
437
451
|
|
package/README.zh.md
CHANGED
|
@@ -35,18 +35,26 @@
|
|
|
35
35
|
|
|
36
36
|
## 快速开始——30 秒用上
|
|
37
37
|
|
|
38
|
-
**第 1
|
|
38
|
+
**第 1 步——安装**。六个客户端吃同一个 npm 包 `agents-gitflow-guard`,按你的 agent 选一条:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
#
|
|
42
|
-
dsh plugin --profile web add agents-gitflow-guard
|
|
43
|
-
# ...或锁定已知良好版本(推荐; 同时绕开 registry 陈旧缓存)
|
|
44
|
-
dsh plugin --profile web add agents-gitflow-guard@0.0.18
|
|
41
|
+
# DSH —— 进程内插件(装完重启 DSH; 插件在进程启动时加载)
|
|
42
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.19
|
|
45
43
|
```
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
```bash
|
|
46
|
+
# Claude Code · Codex · OpenCode · Antigravity —— 独立 hook,不需要 DSH
|
|
47
|
+
npm i -g agents-gitflow-guard@0.0.19
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Pi —— 进程内扩展
|
|
52
|
+
npm i -D agents-gitflow-guard@0.0.19
|
|
53
|
+
```
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
> **版本坑**: 裸 `add` 或不带版本的 `npm i` 装的是安装时刻的 `latest`——在 npm/pnpm 注册表缓存或镜像陈旧的机器上可能拿到旧版本。看到版本不对就锁版本。(DSH 用户: pnpm 打印的 peer 依赖 *警告* 属预期——DSH 启动时经共享模块回退提供 `@deepseek-ai/cordis` / `@deepseek-ai/dsh-tools`,插件正常工作。)
|
|
56
|
+
>
|
|
57
|
+
> hook 客户端(Claude Code · Codex · OpenCode · Antigravity)和 Pi 装完还要各做一步接线——见[安装详解](#安装详解)的逐客户端表。
|
|
50
58
|
|
|
51
59
|
**第 2 步——配置**,在**项目根目录**创建 `gitflow-guard.config.json`:
|
|
52
60
|
|
|
@@ -292,6 +300,12 @@ archive(可选, 发布后你亲手归档)
|
|
|
292
300
|
| 指向 archive 的 PR/MR | ✅ 可创建;🚫 合并被拦(你在 UI 合并) |
|
|
293
301
|
| 在 integration / preview 上 `git merge feature/x`(本地) | 🚫 拦(须 PR/MR);`update: flexible` 则放行 |
|
|
294
302
|
| 串联命令(`checkout develop && merge feature/x`) | 🚫 拦——逐段模拟分支切换,无法绕序 |
|
|
303
|
+
| 强制重建受保护分支(`git checkout -B/-C <分支>` / `git switch -C`) | 🚫 拦(直改 ref-update 门禁) |
|
|
304
|
+
| 用 `git symbolic-ref` 重定向/删除受保护分支 | 🚫 拦(直改 ref-update 门禁) |
|
|
305
|
+
| 在 integration / preview / production / archive 上 `git cherry-pick` / `git revert` | 🚫 拦(受保护分支上改写历史);`-n`/`--no-commit` 与 `--abort`/`--continue`/`--skip`/`--quit` 放行 |
|
|
306
|
+
| `sudo` 包装的 git 命令(特权外壳) | 🚫 剥壳(含 `sudo -u …`)后按内层命令判定 |
|
|
307
|
+
|
|
308
|
+
> 两处**刻意不拦**的边界,防止后来者「顺手堵上」造成语义回归:`git tag -f` 移动 tag(即使指向受保护分支)维持豁免——tag 不在分支角色守卫范围,与 `push --tags` 同型;受保护分支上的普通 `git commit` 维持放行——守卫只管分支角色与合入路径、不管内容,后续 `git push` 仍被拦(远端零污染)。
|
|
295
309
|
|
|
296
310
|
PR/MR 目标通过 `gh pr view`(GitHub)或 `glab mr view`(GitLab)解析;没有平台 CLI 时插件走保守路径。
|
|
297
311
|
|
|
@@ -309,14 +323,14 @@ PR/MR 目标通过 `gh pr view`(GitHub)或 `glab mr view`(GitLab)解析;没有
|
|
|
309
323
|
|
|
310
324
|
| 客户端 | 安装命令 | 装完再做什么 |
|
|
311
325
|
|---|---|---|
|
|
312
|
-
| DSH | `dsh plugin --profile web add agents-gitflow-guard@0.0.
|
|
313
|
-
| Claude Code · Codex · OpenCode · Antigravity | `npm i -g agents-gitflow-guard@0.0.
|
|
314
|
-
| Pi | `npm i -D agents-gitflow-guard@0.0.
|
|
326
|
+
| DSH | `dsh plugin --profile web add agents-gitflow-guard@0.0.19` | 重启 DSH——插件自动挂为 profile 层 |
|
|
327
|
+
| Claude Code · Codex · OpenCode · Antigravity | `npm i -g agents-gitflow-guard@0.0.19` | 在各自 hook 配置里指向 `gitflow-guard` 二进制(见下) |
|
|
328
|
+
| Pi | `npm i -D agents-gitflow-guard@0.0.19` | 把 `pi/gitflow-guard.ts` 拷进 `.pi/extensions/`(见下) |
|
|
315
329
|
|
|
316
330
|
**DSH —— 进程内插件**(标准路径,已在[快速开始](#快速开始30-秒用上)覆盖):
|
|
317
331
|
|
|
318
332
|
```bash
|
|
319
|
-
dsh plugin --profile web add agents-gitflow-guard@0.0.
|
|
333
|
+
dsh plugin --profile web add agents-gitflow-guard@0.0.19 # 建议锁版本, 见上文提示
|
|
320
334
|
```
|
|
321
335
|
|
|
322
336
|
然后重启 DSH。升级用同一命令,再重启一次。
|
|
@@ -333,7 +347,7 @@ dsh plugin --profile web add file:/path/to/agents-gitflow-guard
|
|
|
333
347
|
**各 agent 独立 hook**——Claude Code / Codex / OpenCode / Antigravity,不依赖 DSH。全局装一次 CLI,然后引用 `gitflow-guard` 二进制:
|
|
334
348
|
|
|
335
349
|
```bash
|
|
336
|
-
npm i -g agents-gitflow-guard@0.0.
|
|
350
|
+
npm i -g agents-gitflow-guard@0.0.19 # 提供 `gitflow-guard` 二进制
|
|
337
351
|
```
|
|
338
352
|
|
|
339
353
|
本仓库已自带 `.claude/settings.json`(Claude Code)、`.codex/hooks.json`(Codex)、`.opencode/hook/hooks.yaml`(OpenCode)和 `.agents/hooks.json`(Antigravity / Google);其他仓库加自己的 hooks:
|
|
@@ -389,7 +403,7 @@ hooks:
|
|
|
389
403
|
Pi 以进程内扩展装载(没有 stdin payload,也没有子进程 hook)。把随包发布的入口装进项目、包留在 devDependencies:
|
|
390
404
|
|
|
391
405
|
```bash
|
|
392
|
-
npm i -D agents-gitflow-guard@0.0.
|
|
406
|
+
npm i -D agents-gitflow-guard@0.0.19
|
|
393
407
|
mkdir -p .pi/extensions
|
|
394
408
|
cp node_modules/agents-gitflow-guard/pi/gitflow-guard.ts .pi/extensions/gitflow-guard.ts
|
|
395
409
|
```
|
|
@@ -427,7 +441,7 @@ cp node_modules/agents-gitflow-guard/pi/gitflow-guard.ts .pi/extensions/gitflow-
|
|
|
427
441
|
|
|
428
442
|
不是,请注意别把它当安全工具。它是工作流守卫:把既定流程变成可机制执行的东西。基于文本的命令识别天然是尽力而为——铁心混淆命令的 agent 可以绕过解析器。
|
|
429
443
|
|
|
430
|
-
在其支持的命令形态内,角色边界在本地强制生效:合入受保护角色分支(integration / preview / production / archive)必须走配置好的路径(PR/MR,或生产/归档的人工合并)。常见混淆包装已纳入分类与拦截——shell 包装(`sh -c` / `bash -lc`)、子 shell 与反引号/`$()` 内嵌、`env`/`command`/`nohup`/`xargs` 前缀与 `VAR=x` 赋值、绝对路径、管道与 `||` 后半段、git 全局选项(`-C .`、`--git-dir=…`)、通配 refspec(`refs/heads/*:refs/heads/*`)、当 fetch+merge 用的 `git pull`,以及 `send-pack`/`update-ref` 等 plumbing
|
|
444
|
+
在其支持的命令形态内,角色边界在本地强制生效:合入受保护角色分支(integration / preview / production / archive)必须走配置好的路径(PR/MR,或生产/归档的人工合并)。常见混淆包装已纳入分类与拦截——shell 包装(`sh -c` / `bash -lc`)、子 shell 与反引号/`$()` 内嵌、`env`/`command`/`nohup`/`xargs`/`sudo` 前缀与 `VAR=x` 赋值、绝对路径、管道与 `||` 后半段、git 全局选项(`-C .`、`--git-dir=…`)、通配 refspec(`refs/heads/*:refs/heads/*`)、当 fetch+merge 用的 `git pull`,以及 `send-pack`/`update-ref`/`symbolic-ref` 等 plumbing;强制重建受保护分支(`checkout -B`/`switch -C`)与受保护分支上的 cherry-pick/revert 由 ref-update / ref-move 门禁拦截。可执行对抗语料见 `tests/accuracy-audit.spec.ts`。
|
|
431
445
|
|
|
432
446
|
已知**本地不可防**的通道:直连 forge API(`gh api repos/…/pulls/N/merge`、`curl`)与解释器子进程内嵌(`node -e "child_process.exec('git push …')"`);任意深度的引号/编码变换天然只能尽力而为。真正不可绕过的边界在你托管服务的分支保护设置。两边都用——把本守卫当作即时反馈与审计留痕,而不是安全边界。
|
|
433
447
|
|
package/lib/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as resolveLocale, d as gitRunner, f as loadConfig, h as makeT, i as formatDeny, l as currentBranch, o as stateDir, p as roleMatches, r as evaluateCommand, u as findRepoRoot, v as classify } from "./src-
|
|
1
|
+
import { _ as resolveLocale, d as gitRunner, f as loadConfig, h as makeT, i as formatDeny, l as currentBranch, o as stateDir, p as roleMatches, r as evaluateCommand, u as findRepoRoot, v as classify } from "./src-CL6uB3oo.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.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as name, c as createPiExtension, g as registerLocale, i as formatDeny, m as MESSAGE_KEYS, n as apply, o as stateDir, r as evaluateCommand, s as userStateRoot, t as appendAudit } from "./src-
|
|
1
|
+
import { a as name, c as createPiExtension, g as registerLocale, i as formatDeny, m as MESSAGE_KEYS, n as apply, o as stateDir, r as evaluateCommand, s as userStateRoot, t as appendAudit } from "./src-CL6uB3oo.mjs";
|
|
2
2
|
export { MESSAGE_KEYS, appendAudit, apply, createPiExtension, evaluateCommand, formatDeny, name, registerLocale, stateDir, userStateRoot };
|
|
@@ -119,10 +119,12 @@ function classifySegment(segment, ctx) {
|
|
|
119
119
|
}
|
|
120
120
|
/** 分派: 已知命令直接解析; 包装器剥壳后递归(token 只减不增, 必然终止) */
|
|
121
121
|
function classifyTokens(tokens, ctx) {
|
|
122
|
+
if (tokens.length === 0) return [{ kind: "other" }];
|
|
122
123
|
const rawCmd = tokens[0];
|
|
123
124
|
const cmd = rawCmd.includes("/") ? rawCmd.slice(rawCmd.lastIndexOf("/") + 1) : rawCmd;
|
|
124
125
|
if (SHELLS.has(cmd)) return classifyShellWrapped(tokens, ctx);
|
|
125
126
|
if (cmd === "env") return classifyTokens(stripEnvArgs(tokens.slice(1)), ctx);
|
|
127
|
+
if (cmd === "sudo") return classifyTokens(stripSudoArgs(tokens.slice(1)), ctx);
|
|
126
128
|
if (WRAPPERS.has(cmd)) return classifyTokens(stripWrapperArgs(tokens.slice(1)), ctx);
|
|
127
129
|
if (/^[\w-][\w.-]*=/.test(rawCmd)) return classifyTokens(tokens.slice(1), ctx);
|
|
128
130
|
if (cmd === "git") return classifyGit(tokens.slice(1), ctx);
|
|
@@ -168,6 +170,35 @@ function stripWrapperArgs(args) {
|
|
|
168
170
|
while (i < args.length && (args[i].startsWith("-") || /^\d+$/.test(args[i]) || /^[\w-]+=/.test(args[i]))) i++;
|
|
169
171
|
return args.slice(i);
|
|
170
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* sudo 参数剥离: 旗标与 VAR=x 赋值逐个消费; -u/-g/-p(及长旗标)消费下一个参数;
|
|
175
|
+
* `--` 之后即命令本体。仅剥壳不出新语义, 递归分类必然终止。
|
|
176
|
+
*/
|
|
177
|
+
function stripSudoArgs(args) {
|
|
178
|
+
const WITH_VALUE = /* @__PURE__ */ new Set([
|
|
179
|
+
"-u",
|
|
180
|
+
"--user",
|
|
181
|
+
"-g",
|
|
182
|
+
"--group",
|
|
183
|
+
"-p",
|
|
184
|
+
"--prompt"
|
|
185
|
+
]);
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i < args.length) {
|
|
188
|
+
const a = args[i];
|
|
189
|
+
if (a === "--") return args.slice(i + 1);
|
|
190
|
+
if (WITH_VALUE.has(a)) {
|
|
191
|
+
i += args[i + 1] != null ? 2 : 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (a.startsWith("-") || /^[\w-]+=/.test(a)) {
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
return args.slice(i);
|
|
201
|
+
}
|
|
171
202
|
/** 分词: 引号内的空格不拆分 */
|
|
172
203
|
function tokenize(segment) {
|
|
173
204
|
return segment.match(/"[^"]*"|'[^']*'|\S+/g)?.map((t) => t.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -181,6 +212,8 @@ function classifyGit(args, ctx) {
|
|
|
181
212
|
if (sub === "checkout" || sub === "switch") return parseCheckout(rest);
|
|
182
213
|
if (sub === "send-pack") return parseSendPack(rest);
|
|
183
214
|
if (sub === "update-ref") return parseUpdateRef(rest);
|
|
215
|
+
if (sub === "symbolic-ref") return parseSymbolicRef(rest);
|
|
216
|
+
if (sub === "cherry-pick" || sub === "revert") return parseCherryPickLike(rest);
|
|
184
217
|
if (sub === "reset" || sub === "filter-branch") return [{ kind: "ref-move" }];
|
|
185
218
|
if (sub === "rebase") return parseRebase(rest);
|
|
186
219
|
if (sub === "commit") return parseCommit(rest);
|
|
@@ -203,6 +236,21 @@ function parseCommit(args) {
|
|
|
203
236
|
if (args.some((a) => a === "--amend")) return [{ kind: "ref-move" }];
|
|
204
237
|
return [{ kind: "other" }];
|
|
205
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* cherry-pick/revert 会在当前分支上新提交 → 改写当前 tip, 收编为 ref-move
|
|
241
|
+
* (受保护分支上拒绝, 与 reset/rebase 同型);
|
|
242
|
+
* -n/--no-commit 只改工作树与索引(不移动 tip)与恢复类旗标(abort/continue 等)放行。
|
|
243
|
+
*/
|
|
244
|
+
function parseCherryPickLike(args) {
|
|
245
|
+
const RESUME = /* @__PURE__ */ new Set([
|
|
246
|
+
"--abort",
|
|
247
|
+
"--continue",
|
|
248
|
+
"--skip",
|
|
249
|
+
"--quit"
|
|
250
|
+
]);
|
|
251
|
+
if (args.some((a) => a === "-n" || a === "--no-commit" || RESUME.has(a))) return [{ kind: "other" }];
|
|
252
|
+
return [{ kind: "ref-move" }];
|
|
253
|
+
}
|
|
206
254
|
/** 剥离子命令前的全局选项(-C <path> / -c <k=v> / --git-dir 等), 否则 git -C . push 会被判 other */
|
|
207
255
|
function stripGlobalOptions(args) {
|
|
208
256
|
const WITH_VALUE = /* @__PURE__ */ new Set([
|
|
@@ -238,20 +286,44 @@ function stripGlobalOptions(args) {
|
|
|
238
286
|
}
|
|
239
287
|
return args.slice(i);
|
|
240
288
|
}
|
|
241
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* 分支切换: 普通切换/-b/-c(switch -c)新建 → checkout(放行, 分支状态由 evaluateCommand 模拟);
|
|
291
|
+
* -B/-C 强制重建会静默移动/重建既有 ref(可波及受保护分支), 目标名单独送 ref-update 门禁,
|
|
292
|
+
* 门禁放行后仍按 checkout 模拟切换(两段任一 deny 即整体拦截, 与 push 歧义双解释同机制)。
|
|
293
|
+
* 短旗标簇(-Bf/-bt 等)扫描 b/B/c/C 视同对应形态。
|
|
294
|
+
*/
|
|
242
295
|
function parseCheckout(args) {
|
|
243
296
|
const first = args[0];
|
|
244
297
|
if (first === "--") return [{
|
|
245
298
|
kind: "checkout",
|
|
246
299
|
branch: null
|
|
247
300
|
}];
|
|
301
|
+
const name = args[1];
|
|
302
|
+
const validName = name != null && !name.startsWith("-");
|
|
248
303
|
if (first === "-b" || first === "-B" || first === "-c" || first === "-C") {
|
|
249
|
-
|
|
304
|
+
if (!validName) return [{
|
|
305
|
+
kind: "checkout",
|
|
306
|
+
branch: null
|
|
307
|
+
}];
|
|
308
|
+
if (first === "-B" || first === "-C") return forceRecreateOut(name);
|
|
250
309
|
return [{
|
|
251
310
|
kind: "checkout",
|
|
252
|
-
branch: name
|
|
311
|
+
branch: name
|
|
253
312
|
}];
|
|
254
313
|
}
|
|
314
|
+
if (first != null && first.startsWith("-") && !first.startsWith("--") && first.length > 1) {
|
|
315
|
+
const clusterForce = first.includes("B") || first.includes("C");
|
|
316
|
+
if (clusterForce || first.includes("b") || first.includes("c")) {
|
|
317
|
+
if (!validName) return [{
|
|
318
|
+
kind: "checkout",
|
|
319
|
+
branch: null
|
|
320
|
+
}];
|
|
321
|
+
return clusterForce ? forceRecreateOut(name) : [{
|
|
322
|
+
kind: "checkout",
|
|
323
|
+
branch: name
|
|
324
|
+
}];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
255
327
|
if (first && !first.startsWith("-")) return [{
|
|
256
328
|
kind: "checkout",
|
|
257
329
|
branch: first
|
|
@@ -261,6 +333,17 @@ function parseCheckout(args) {
|
|
|
261
333
|
branch: null
|
|
262
334
|
}];
|
|
263
335
|
}
|
|
336
|
+
/** -B/-C(及含 B/C 的旗标簇)的产出: 目标 ref 送 ref-update, 再按 checkout 模拟切换 */
|
|
337
|
+
function forceRecreateOut(name) {
|
|
338
|
+
return [{
|
|
339
|
+
kind: "ref-update",
|
|
340
|
+
branch: stripRefPrefix(name),
|
|
341
|
+
delete: false
|
|
342
|
+
}, {
|
|
343
|
+
kind: "checkout",
|
|
344
|
+
branch: name
|
|
345
|
+
}];
|
|
346
|
+
}
|
|
264
347
|
function parsePush(args, ctx) {
|
|
265
348
|
let force = false;
|
|
266
349
|
let isDelete = false;
|
|
@@ -404,6 +487,36 @@ function parseUpdateRef(args) {
|
|
|
404
487
|
}
|
|
405
488
|
return [{ kind: "other" }];
|
|
406
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* git symbolic-ref 直改 symbolic refs(plumbing 绕行面):
|
|
492
|
+
* - 查询形态(symbolic-ref <name> / --short 等单参)不改变任何 ref → other;
|
|
493
|
+
* - 双参重定向(symbolic-ref <name> <ref>)把 name 指向别处, 目标名送 ref-update;
|
|
494
|
+
* - -d/--delete 删除该 ref, 同样送 ref-update。
|
|
495
|
+
*/
|
|
496
|
+
function parseSymbolicRef(args) {
|
|
497
|
+
let isDelete = false;
|
|
498
|
+
const nonFlag = [];
|
|
499
|
+
for (let i = 0; i < args.length; i++) {
|
|
500
|
+
const a = args[i];
|
|
501
|
+
if (a === "-d" || a === "--delete") {
|
|
502
|
+
isDelete = true;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (a === "-m" || a === "--message") {
|
|
506
|
+
i++;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (a.startsWith("-")) continue;
|
|
510
|
+
nonFlag.push(a);
|
|
511
|
+
}
|
|
512
|
+
if (!isDelete && nonFlag.length < 2) return [{ kind: "other" }];
|
|
513
|
+
if (isDelete && nonFlag.length === 0) return [{ kind: "other" }];
|
|
514
|
+
return [{
|
|
515
|
+
kind: "ref-update",
|
|
516
|
+
branch: stripRefPrefix(nonFlag[0]),
|
|
517
|
+
delete: isDelete
|
|
518
|
+
}];
|
|
519
|
+
}
|
|
407
520
|
function parseMerge(args) {
|
|
408
521
|
if (args.some((a) => a === "--abort")) return [{ kind: "other" }];
|
|
409
522
|
return [{
|
|
@@ -526,9 +639,9 @@ const en = {
|
|
|
526
639
|
"head.unknown": () => "current branch",
|
|
527
640
|
"denyDeleteOrForce.why": (v) => `Protected branch "${v.branch}" may not be deleted or force-pushed`,
|
|
528
641
|
"denyDeleteOrForce.next": () => "Delete/force-push on a feature branch outside the protected branches; protected branches are managed by you.",
|
|
529
|
-
"refUpdateProtected.why": (v) => `Protected branch "${v.branch}" forbids direct ref updates (update-ref)`,
|
|
642
|
+
"refUpdateProtected.why": (v) => `Protected branch "${v.branch}" forbids direct ref updates (update-ref / symbolic-ref / branch -m|-f / checkout -B)`,
|
|
530
643
|
"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",
|
|
644
|
+
"refMoveProtected.why": () => "Rewriting history on a protected branch (reset / rebase / commit --amend / filter-branch / cherry-pick / revert) is not allowed",
|
|
532
645
|
"refMoveProtected.next": () => "Do history rewrites on a feature branch; protected branches advance via PR/MR and are managed by you.",
|
|
533
646
|
"pushAll.why": () => "--all/--mirror push would include protected branches",
|
|
534
647
|
"pushAll.next": () => "Push branch by branch with an explicit refspec.",
|
|
@@ -603,9 +716,9 @@ const zh = {
|
|
|
603
716
|
"head.unknown": () => "当前分支",
|
|
604
717
|
"denyDeleteOrForce.why": (v) => `受保护分支「${v.branch}」禁止删除或强推`,
|
|
605
718
|
"denyDeleteOrForce.next": () => "删除/强推请到受保护分支外的 feature 分支上操作; 受保护分支由用户亲手管理",
|
|
606
|
-
"refUpdateProtected.why": (v) => `受保护分支「${v.branch}」禁止直接改写 refs(update-ref)`,
|
|
719
|
+
"refUpdateProtected.why": (v) => `受保护分支「${v.branch}」禁止直接改写 refs(update-ref / symbolic-ref / branch -m|-f / checkout -B)`,
|
|
607
720
|
"refUpdateProtected.next": () => "请通过 PR/MR 更新受保护分支; 受保护分支由用户亲手管理",
|
|
608
|
-
"refMoveProtected.why": () => "受保护分支禁止本地改写历史(reset / rebase / commit --amend / filter-branch)",
|
|
721
|
+
"refMoveProtected.why": () => "受保护分支禁止本地改写历史(reset / rebase / commit --amend / filter-branch / cherry-pick / revert)",
|
|
609
722
|
"refMoveProtected.next": () => "历史改写请在 feature 分支上进行; 受保护分支仅经 PR/MR 推进, 由用户亲手管理",
|
|
610
723
|
"pushAll.why": () => "--all/--mirror 推送会包含受保护分支",
|
|
611
724
|
"pushAll.next": () => "请逐分支推送并显式指定 refspec",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-gitflow-guard",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "A configurable branch-role guard for AI coding agents (DSH / Claude Code / Codex / OpenCode / Antigravity / Pi) — integration/preview/production/archive, each with its own update rules",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|