agents-gitflow-guard 0.0.18 → 0.0.20
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 +90 -42
- package/README.zh.md +90 -42
- package/lib/cli.mjs +377 -4
- package/lib/index.mjs +1 -1
- package/lib/{src-BQYC4N6b.mjs → src-DPJRoEJq.mjs} +216 -28
- package/package.json +7 -3
|
@@ -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.",
|
|
@@ -575,6 +688,31 @@ const en = {
|
|
|
575
688
|
"cli.auditEmpty": () => " No audit entries yet",
|
|
576
689
|
"cli.checkInternalError": (v) => `[gitflow-guard] check internal error, allowed through: ${v.msg}`,
|
|
577
690
|
"cli.guardDisabledInvalidConfig": (v) => `[gitflow-guard] guard disabled: invalid config: ${v.err}`,
|
|
691
|
+
"cli.wireUnknownClient": (v) => `unknown client: ${v.client} (expected dsh|claude|codex|opencode|antigravity|pi)`,
|
|
692
|
+
"cli.wireNeedRepo": () => "project scope needs a git repository — run inside a repo or pass --repo <path>",
|
|
693
|
+
"cli.wireScopeAsk": () => "Scope — project (this repo only) or global (all repos on this machine)? [project/global] ",
|
|
694
|
+
"cli.wireScopeInvalid": () => "invalid scope (expected project or global)",
|
|
695
|
+
"cli.wireTarget": (v) => `${v.client}: wiring → ${v.path}`,
|
|
696
|
+
"cli.wireAlready": (v) => `${v.client}: hook already wired (${v.path})`,
|
|
697
|
+
"cli.wireCreated": (v) => `${v.client}: hook written → ${v.path}`,
|
|
698
|
+
"cli.wireRemoved": (v) => `${v.client}: hook removed → ${v.path}`,
|
|
699
|
+
"cli.wireNotWired": (v) => `${v.client}: no hook entry found (${v.path})`,
|
|
700
|
+
"cli.wireConfirmWrite": (v) => `Write ${v.path}? [y/N] `,
|
|
701
|
+
"cli.wireRefuseGlobal": () => "Refusing to modify a global config without confirmation — pass --yes to allow",
|
|
702
|
+
"cli.wireDshGuide": () => "DSH is an in-process plugin — no hook file to write. Mount it with: dsh plugin --profile web add agents-gitflow-guard",
|
|
703
|
+
"cli.wirePiGuide": () => "Pi is an in-process extension — no hook file to write. Copy pi/gitflow-guard.ts into .pi/extensions/ and list it in .pi/settings.json (see README).",
|
|
704
|
+
"cli.wireExperimental": (v) => `${v.client}: experimental support — verify the hook on a real device before relying on it`,
|
|
705
|
+
"cli.wireDryRunAdd": (v) => `${v.client}: [dry-run] would add hook → ${v.path}`,
|
|
706
|
+
"cli.wireDryRunRemove": (v) => `${v.client}: [dry-run] would remove hook → ${v.path}`,
|
|
707
|
+
"cli.wireDryRunNoOp": (v) => `${v.client}: [dry-run] nothing to do (${v.path})`,
|
|
708
|
+
"cli.statusUsingDefaults": () => "Config: built-in defaults (no gitflow-guard.config.json) — integration=develop, archive=main",
|
|
709
|
+
"cli.statusMainProtected": () => " main is protected by default. Trunk / single-branch users: create gitflow-guard.config.json with \"enabled\": false, or map your own branches.",
|
|
710
|
+
"cli.statusWireHints": () => "Wiring:",
|
|
711
|
+
"cli.statusWireHint": (v) => ` ${v.client}: not wired — run: gitflow-guard wire --client ${v.client}`,
|
|
712
|
+
"cli.setupIntro": () => "gitflow-guard setup — wire one client for this project. (Ctrl+C to cancel)",
|
|
713
|
+
"cli.setupClientAsk": () => "Which client? [dsh|claude|codex|opencode|antigravity|pi] ",
|
|
714
|
+
"cli.setupClientInvalid": () => "invalid client (expected dsh|claude|codex|opencode|antigravity|pi)",
|
|
715
|
+
"cli.setupNoTty": () => "setup needs an interactive terminal — use: gitflow-guard wire --client <name> --yes",
|
|
578
716
|
"guardStrictConfigBroken.why": () => "Guard config is invalid while strict mode is enabled",
|
|
579
717
|
"guardStrictConfigBroken.next": () => "Fix gitflow-guard.config.json (or remove \"strict\": true) before retrying.",
|
|
580
718
|
"guardStrictInternalError.why": (v) => `Guard internal error while strict mode is enabled: ${v.msg}`,
|
|
@@ -585,13 +723,18 @@ Usage:
|
|
|
585
723
|
gitflow-guard status [--repo <path>] [--locale <en|zh>]
|
|
586
724
|
gitflow-guard audit [--lines <count>] [--repo <path>] [--locale <en|zh>]
|
|
587
725
|
gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <path>] [--locale <en|zh>]
|
|
726
|
+
gitflow-guard wire --client <dsh|claude|codex|opencode|antigravity|pi> [--project|--global] [--unwire] [--dry-run] [--yes] [--repo <path>] [--locale <en|zh>]
|
|
727
|
+
gitflow-guard setup [--repo <path>] [--locale <en|zh>]
|
|
588
728
|
gitflow-guard --help
|
|
589
729
|
|
|
590
730
|
Notes:
|
|
591
731
|
status/audit are read-only; the agent can self-inspect.
|
|
592
732
|
--locale overrides the message language for this invocation (flag > project config > English).
|
|
593
733
|
check reads the hook payload on stdin (platform-specific protocol: claude/opencode exit 2,
|
|
594
|
-
codex/antigravity JSON on stdout) and is meant for pre/post hooks of AI agents
|
|
734
|
+
codex/antigravity JSON on stdout) and is meant for pre/post hooks of AI agents.
|
|
735
|
+
wire writes each client's hook config into the project (default) or global scope; dsh/pi are
|
|
736
|
+
in-process and only print guidance. No config file needed — built-in defaults (develop+main)
|
|
737
|
+
apply out of the box; create gitflow-guard.config.json to override, or set "enabled": false to turn off.`
|
|
595
738
|
};
|
|
596
739
|
const zh = {
|
|
597
740
|
"role.integration": () => "集成分支",
|
|
@@ -603,9 +746,9 @@ const zh = {
|
|
|
603
746
|
"head.unknown": () => "当前分支",
|
|
604
747
|
"denyDeleteOrForce.why": (v) => `受保护分支「${v.branch}」禁止删除或强推`,
|
|
605
748
|
"denyDeleteOrForce.next": () => "删除/强推请到受保护分支外的 feature 分支上操作; 受保护分支由用户亲手管理",
|
|
606
|
-
"refUpdateProtected.why": (v) => `受保护分支「${v.branch}」禁止直接改写 refs(update-ref)`,
|
|
749
|
+
"refUpdateProtected.why": (v) => `受保护分支「${v.branch}」禁止直接改写 refs(update-ref / symbolic-ref / branch -m|-f / checkout -B)`,
|
|
607
750
|
"refUpdateProtected.next": () => "请通过 PR/MR 更新受保护分支; 受保护分支由用户亲手管理",
|
|
608
|
-
"refMoveProtected.why": () => "受保护分支禁止本地改写历史(reset / rebase / commit --amend / filter-branch)",
|
|
751
|
+
"refMoveProtected.why": () => "受保护分支禁止本地改写历史(reset / rebase / commit --amend / filter-branch / cherry-pick / revert)",
|
|
609
752
|
"refMoveProtected.next": () => "历史改写请在 feature 分支上进行; 受保护分支仅经 PR/MR 推进, 由用户亲手管理",
|
|
610
753
|
"pushAll.why": () => "--all/--mirror 推送会包含受保护分支",
|
|
611
754
|
"pushAll.next": () => "请逐分支推送并显式指定 refspec",
|
|
@@ -652,6 +795,31 @@ const zh = {
|
|
|
652
795
|
"cli.auditEmpty": () => " 暂无审计记录",
|
|
653
796
|
"cli.checkInternalError": (v) => `[gitflow-guard] check 内部错误, 已放行: ${v.msg}`,
|
|
654
797
|
"cli.guardDisabledInvalidConfig": (v) => `[gitflow-guard] 守卫未启用: 配置无效: ${v.err}`,
|
|
798
|
+
"cli.wireUnknownClient": (v) => `未知客户端: ${v.client}(应为 dsh|claude|codex|opencode|antigravity|pi)`,
|
|
799
|
+
"cli.wireNeedRepo": () => "项目级作用域需要一个 git 仓库 — 请在仓库内运行, 或传 --repo <路径>",
|
|
800
|
+
"cli.wireScopeAsk": () => "作用域 — project(仅当前仓库) 还是 global(本机所有仓库)? [project/global] ",
|
|
801
|
+
"cli.wireScopeInvalid": () => "无效作用域(应为 project 或 global)",
|
|
802
|
+
"cli.wireTarget": (v) => `${v.client}: 接线 → ${v.path}`,
|
|
803
|
+
"cli.wireAlready": (v) => `${v.client}: hook 已接线(${v.path})`,
|
|
804
|
+
"cli.wireCreated": (v) => `${v.client}: hook 已写入 → ${v.path}`,
|
|
805
|
+
"cli.wireRemoved": (v) => `${v.client}: hook 已移除 → ${v.path}`,
|
|
806
|
+
"cli.wireNotWired": (v) => `${v.client}: 未找到 hook 条目(${v.path})`,
|
|
807
|
+
"cli.wireConfirmWrite": (v) => `写入 ${v.path}? [y/N] `,
|
|
808
|
+
"cli.wireRefuseGlobal": () => "拒绝在未确认时改动全局配置 — 传 --yes 允许",
|
|
809
|
+
"cli.wireDshGuide": () => "DSH 是进程内插件, 无需写入 hook 文件。挂载: dsh plugin --profile web add agents-gitflow-guard",
|
|
810
|
+
"cli.wirePiGuide": () => "Pi 是进程内扩展, 无需写入 hook 文件。把 pi/gitflow-guard.ts 拷到 .pi/extensions/ 并在 .pi/settings.json 登记(见 README)",
|
|
811
|
+
"cli.wireExperimental": (v) => `${v.client}: 实验支持 — 请在真机核验后再依赖它`,
|
|
812
|
+
"cli.wireDryRunAdd": (v) => `${v.client}: [dry-run] 将添加 hook → ${v.path}`,
|
|
813
|
+
"cli.wireDryRunRemove": (v) => `${v.client}: [dry-run] 将移除 hook → ${v.path}`,
|
|
814
|
+
"cli.wireDryRunNoOp": (v) => `${v.client}: [dry-run] 无需改动(${v.path})`,
|
|
815
|
+
"cli.statusUsingDefaults": () => "配置: 内置默认(无 gitflow-guard.config.json)— integration=develop, archive=main",
|
|
816
|
+
"cli.statusMainProtected": () => " main 默认受保护。Trunk/单分支用户: 创建 gitflow-guard.config.json 写 \"enabled\": false, 或自行映射分支",
|
|
817
|
+
"cli.statusWireHints": () => "接线:",
|
|
818
|
+
"cli.statusWireHint": (v) => ` ${v.client}: 未接线 — 运行: gitflow-guard wire --client ${v.client}`,
|
|
819
|
+
"cli.setupIntro": () => "gitflow-guard setup — 为本项目接线一个客户端。(Ctrl+C 取消)",
|
|
820
|
+
"cli.setupClientAsk": () => "选哪个客户端? [dsh|claude|codex|opencode|antigravity|pi] ",
|
|
821
|
+
"cli.setupClientInvalid": () => "无效客户端(应为 dsh|claude|codex|opencode|antigravity|pi)",
|
|
822
|
+
"cli.setupNoTty": () => "setup 需要交互终端 — 请用: gitflow-guard wire --client <名字> --yes",
|
|
655
823
|
"guardStrictConfigBroken.why": () => "守卫配置无效, 且已启用 strict 模式",
|
|
656
824
|
"guardStrictConfigBroken.next": () => "请先修复 gitflow-guard.config.json(或移除 \"strict\": true)后重试",
|
|
657
825
|
"guardStrictInternalError.why": (v) => `守卫内部错误, 且已启用 strict 模式: ${v.msg}`,
|
|
@@ -662,13 +830,17 @@ const zh = {
|
|
|
662
830
|
gitflow-guard status [--repo <路径>] [--locale <en|zh>]
|
|
663
831
|
gitflow-guard audit [--lines <数量>] [--repo <路径>] [--locale <en|zh>]
|
|
664
832
|
gitflow-guard check [--platform <auto|claude|codex|opencode|antigravity>] [--command "<cmd>"] [--repo <路径>] [--locale <en|zh>]
|
|
833
|
+
gitflow-guard wire --client <dsh|claude|codex|opencode|antigravity|pi> [--project|--global] [--unwire] [--dry-run] [--yes] [--repo <路径>] [--locale <en|zh>]
|
|
834
|
+
gitflow-guard setup [--repo <路径>] [--locale <en|zh>]
|
|
665
835
|
gitflow-guard --help
|
|
666
836
|
|
|
667
837
|
说明:
|
|
668
838
|
status/audit 只读, agent 可自查。
|
|
669
839
|
--locale 可临时覆盖本次调用的文案语言(旗标 > 项目配置 > 英文)。
|
|
670
840
|
check 读 stdin hook payload 做门禁(平台协议: claude/opencode exit 2, codex/antigravity stdout JSON),
|
|
671
|
-
供 Claude Code / Codex / OpenCode 等 agent 的 pre/post hook
|
|
841
|
+
供 Claude Code / Codex / OpenCode 等 agent 的 pre/post hook 调用。
|
|
842
|
+
wire 把各客户端默认 hook 写入工程(默认)或全局作用域; dsh/pi 为进程内接入, 仅打印引导。
|
|
843
|
+
无需配置文件 — 内置默认(develop+main)开箱即用; 建 gitflow-guard.config.json 可覆盖, 或写 "enabled": false 关闭。`
|
|
672
844
|
};
|
|
673
845
|
/** 内置文案注册表: en 为兜底语言; 下游可经 registerLocale 追加 */
|
|
674
846
|
const dicts = /* @__PURE__ */ new Map([["en", en], ["zh", zh]]);
|
|
@@ -708,10 +880,26 @@ function resolveLocale(v) {
|
|
|
708
880
|
//#endregion
|
|
709
881
|
//#region src/config.ts
|
|
710
882
|
const CONFIG_FILE = "gitflow-guard.config.json";
|
|
711
|
-
/**
|
|
883
|
+
/**
|
|
884
|
+
* 内置默认配置(零门槛开箱即用): 没有 gitflow-guard.config.json 也生效。
|
|
885
|
+
* 默认保护 develop(integration, 只走 PR/MR) + main(archive, 归档合并在人)。
|
|
886
|
+
* 用户 config 存在时按字段深度合并覆盖——只写想改的字段, 其余沿用默认。
|
|
887
|
+
*/
|
|
712
888
|
const DEFAULT_CONFIG = {
|
|
713
|
-
enabled:
|
|
889
|
+
enabled: true,
|
|
714
890
|
featurePattern: "feature/[\\w-]+",
|
|
891
|
+
branches: {
|
|
892
|
+
integration: {
|
|
893
|
+
branches: ["develop"],
|
|
894
|
+
update: "pr",
|
|
895
|
+
mergeBy: "anyone"
|
|
896
|
+
},
|
|
897
|
+
archive: {
|
|
898
|
+
branches: ["main"],
|
|
899
|
+
update: "pr",
|
|
900
|
+
mergeBy: "user"
|
|
901
|
+
}
|
|
902
|
+
},
|
|
715
903
|
ci: { enabled: true },
|
|
716
904
|
locale: "en"
|
|
717
905
|
};
|
|
@@ -769,32 +957,30 @@ function mergeConfig(raw) {
|
|
|
769
957
|
if (typeof raw !== "object" || raw === null) return {
|
|
770
958
|
config: null,
|
|
771
959
|
errors: ["Config file must be a JSON object"],
|
|
772
|
-
warnings
|
|
960
|
+
warnings,
|
|
961
|
+
usingDefaults: false
|
|
773
962
|
};
|
|
774
963
|
const r = raw;
|
|
775
964
|
const strict = r.strict === true ? true : r.strict === false ? false : void 0;
|
|
776
965
|
if (r.strict !== void 0 && typeof r.strict !== "boolean") errors.push("strict must be a boolean");
|
|
777
|
-
const config =
|
|
778
|
-
...DEFAULT_CONFIG,
|
|
779
|
-
ci: { ...DEFAULT_CONFIG.ci },
|
|
780
|
-
branches: { integration: {
|
|
781
|
-
branches: [],
|
|
782
|
-
update: "pr",
|
|
783
|
-
mergeBy: "anyone"
|
|
784
|
-
} }
|
|
785
|
-
};
|
|
966
|
+
const config = structuredClone(DEFAULT_CONFIG);
|
|
786
967
|
if (typeof r.enabled === "boolean") config.enabled = r.enabled;
|
|
787
968
|
if (typeof r.featurePattern === "string" && r.featurePattern !== "") config.featurePattern = r.featurePattern;
|
|
788
969
|
if (typeof r.locale === "string" && r.locale !== "") {
|
|
789
970
|
config.locale = r.locale;
|
|
790
971
|
if (resolveLocale(r.locale) !== r.locale) warnings.push(`unknown locale "${r.locale}"; falling back to en`);
|
|
791
972
|
} else if (r.locale !== void 0) errors.push("locale must be a string");
|
|
973
|
+
if (typeof r.ci === "object" && r.ci !== null) {
|
|
974
|
+
const ci = r.ci;
|
|
975
|
+
if (typeof ci.enabled === "boolean") config.ci.enabled = ci.enabled;
|
|
976
|
+
}
|
|
977
|
+
if (r.branches !== void 0 && (typeof r.branches !== "object" || r.branches === null)) errors.push("branches must be an object");
|
|
792
978
|
const b = r.branches ?? {};
|
|
793
979
|
if ("integration" in b) {
|
|
794
980
|
const { role, errors: e } = normalizeRole(b.integration, "integration", "pr", "anyone");
|
|
795
981
|
config.branches.integration = role;
|
|
796
982
|
errors.push(...e);
|
|
797
|
-
}
|
|
983
|
+
}
|
|
798
984
|
if (b.preview !== void 0) {
|
|
799
985
|
const { role, errors: e } = normalizeRole(b.preview, "preview", "pr", "anyone");
|
|
800
986
|
config.branches.preview = role;
|
|
@@ -810,14 +996,13 @@ function mergeConfig(raw) {
|
|
|
810
996
|
config.branches.archive = role;
|
|
811
997
|
errors.push(...e);
|
|
812
998
|
}
|
|
813
|
-
const ci = r.ci ?? {};
|
|
814
|
-
if (typeof ci.enabled === "boolean") config.ci.enabled = ci.enabled;
|
|
815
999
|
if (strict !== void 0) config.strict = strict;
|
|
816
1000
|
errors.push(...validateConfig(config));
|
|
817
1001
|
return {
|
|
818
1002
|
config: errors.length > 0 ? null : config,
|
|
819
1003
|
errors,
|
|
820
1004
|
warnings,
|
|
1005
|
+
usingDefaults: false,
|
|
821
1006
|
...strict !== void 0 ? { strict } : {}
|
|
822
1007
|
};
|
|
823
1008
|
}
|
|
@@ -847,21 +1032,23 @@ function validateConfig(config) {
|
|
|
847
1032
|
}
|
|
848
1033
|
return errors;
|
|
849
1034
|
}
|
|
850
|
-
/** 从项目根加载配置; 无文件 =
|
|
1035
|
+
/** 从项目根加载配置; 无文件 = 使用内置默认配置(开箱即用, develop/main 已受保护) */
|
|
851
1036
|
async function loadConfig(repoRoot) {
|
|
852
1037
|
let text;
|
|
853
1038
|
try {
|
|
854
1039
|
text = await readFile(join(repoRoot, CONFIG_FILE), "utf8");
|
|
855
1040
|
} catch (e) {
|
|
856
1041
|
if (e.code === "ENOENT") return {
|
|
857
|
-
config:
|
|
1042
|
+
config: structuredClone(DEFAULT_CONFIG),
|
|
858
1043
|
errors: [],
|
|
859
|
-
warnings: []
|
|
1044
|
+
warnings: [],
|
|
1045
|
+
usingDefaults: true
|
|
860
1046
|
};
|
|
861
1047
|
return {
|
|
862
1048
|
config: null,
|
|
863
1049
|
errors: [`Failed to read config file: ${e.message}`],
|
|
864
|
-
warnings: []
|
|
1050
|
+
warnings: [],
|
|
1051
|
+
usingDefaults: false
|
|
865
1052
|
};
|
|
866
1053
|
}
|
|
867
1054
|
let raw;
|
|
@@ -873,6 +1060,7 @@ async function loadConfig(repoRoot) {
|
|
|
873
1060
|
config: null,
|
|
874
1061
|
errors: [`Failed to read config file: ${e.message}`],
|
|
875
1062
|
warnings: [],
|
|
1063
|
+
usingDefaults: false,
|
|
876
1064
|
...strict ? { strict } : {}
|
|
877
1065
|
};
|
|
878
1066
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-gitflow-guard",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
|
3
|
+
"version": "0.0.20",
|
|
4
|
+
"description": "A configurable branch-role guard for AI coding agents (DSH / Claude Code / Codex / OpenCode / Antigravity / Pi) — zero-config defaults (develop+main) out of the box, deep-merge override, and per-client native hooks via gitflow-guard wire/setup; integration/preview/production/archive, each with its own update rules",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -42,7 +42,11 @@
|
|
|
42
42
|
"git",
|
|
43
43
|
"workflow",
|
|
44
44
|
"guard",
|
|
45
|
-
"agent"
|
|
45
|
+
"agent",
|
|
46
|
+
"zero-config",
|
|
47
|
+
"onboarding",
|
|
48
|
+
"wire",
|
|
49
|
+
"setup"
|
|
46
50
|
],
|
|
47
51
|
"repository": {
|
|
48
52
|
"type": "git",
|