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.
@@ -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
- /** 分支切换: 门禁放行, 分支状态由 evaluateCommand 按段模拟 */
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
- const name = args[1];
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 && !name.startsWith("-") ? name : null
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: false,
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
- } else errors.push("branches.integration is required");
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
- /** 从项目根加载配置; 无文件 = 未启用(opt-in) */
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: null,
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.18",
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",