agents-gitflow-guard 0.0.1 → 0.0.2

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/lib/cli.mjs CHANGED
@@ -1,39 +1,115 @@
1
- import { a as stateDir, c as gitRunner, d as loadConfig, l as isAncestor, o as currentBranch, s as findRepoRoot, t as appendAudit, u as openPermitStore } from "./src-DWE1n9Zh.mjs";
1
+ import { c as findRepoRoot, d as roleMatches, f as classify, i as formatDeny, l as gitRunner, o as stateDir, r as evaluateCommand, s as currentBranch, u as loadConfig } from "./src-Dv6jKDsO.mjs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
+ //#region src/platform.ts
5
+ function str(v) {
6
+ return typeof v === "string" ? v : "";
7
+ }
8
+ function eventFrom(hookEventName) {
9
+ if (hookEventName === "PostToolUse") return "post";
10
+ if (hookEventName === "PostToolUseFailure") return "post-failure";
11
+ return "pre";
12
+ }
13
+ function parseRaw(raw) {
14
+ if (!raw) return null;
15
+ try {
16
+ const j = JSON.parse(raw);
17
+ if (typeof j !== "object" || j === null) return null;
18
+ return j;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ /** 从 stdin JSON 提取 command/cwd/event; 无法识别返回 null(放行) */
24
+ function extractHookPayload(raw, platform = "auto") {
25
+ const j = parseRaw(raw);
26
+ if (!j) return null;
27
+ const plat = platform === "auto" ? detectPlatform(raw) : platform;
28
+ let command = "";
29
+ let cwd = "";
30
+ if (plat === "claude" || plat === "codex") {
31
+ command = str(j.tool_input?.command);
32
+ cwd = str(j.cwd);
33
+ } else if (plat === "copilot") {
34
+ command = str(j.tool_input?.command) || str(j.toolArgs?.command);
35
+ cwd = str(j.cwd);
36
+ } else if (plat === "antigravity") {
37
+ command = str(j.toolCall?.args?.CommandLine);
38
+ cwd = str(j.cwd);
39
+ }
40
+ if (!command) return null;
41
+ return {
42
+ command,
43
+ cwd: cwd || void 0,
44
+ toolUseId: str(j.tool_use_id) || void 0,
45
+ event: eventFrom(j.hook_event_name)
46
+ };
47
+ }
48
+ /** 按 payload 判别平台: 非空 turn_id→codex, toolCall→antigravity, 其余→claude */
49
+ function detectPlatform(raw) {
50
+ const j = parseRaw(raw);
51
+ if (!j) return "claude";
52
+ if (j.turn_id) return "codex";
53
+ if (j.toolCall) return "antigravity";
54
+ return "claude";
55
+ }
56
+ /** deny 编码: 各平台的拦截协议(exit 码 + stdout/stderr) */
57
+ function encodeDeny(platform, reason) {
58
+ switch (platform) {
59
+ case "claude": return {
60
+ exitCode: 2,
61
+ stderr: reason
62
+ };
63
+ case "codex": return {
64
+ exitCode: 0,
65
+ stdout: JSON.stringify({ hookSpecificOutput: {
66
+ hookEventName: "PreToolUse",
67
+ permissionDecision: "deny",
68
+ permissionDecisionReason: reason
69
+ } }),
70
+ stderr: reason
71
+ };
72
+ case "antigravity": return {
73
+ exitCode: 0,
74
+ stdout: JSON.stringify({
75
+ decision: "block",
76
+ reason
77
+ })
78
+ };
79
+ case "copilot": return {
80
+ exitCode: 2,
81
+ stderr: reason
82
+ };
83
+ }
84
+ }
85
+ //#endregion
4
86
  //#region src/cli.ts
5
- const USAGE = `gitflow-guard — GitFlow 流程守卫 CLI(用户终端专属)
87
+ const USAGE = `gitflow-guard — GitFlow 流程守卫 CLI
6
88
 
7
89
  用法:
8
- gitflow-guard permit <feature> [--kind early-pr|confirm|trunk-pr] [--ttl <分钟>] [--repo <路径>]
9
- gitflow-guard confirm <feature> [--ttl <分钟>] [--repo <路径>]
10
90
  gitflow-guard status [--repo <路径>]
11
91
  gitflow-guard audit [--lines <数量>] [--repo <路径>]
92
+ gitflow-guard check [--platform <claude|auto>] [--command "<cmd>"] [--repo <路径>]
12
93
  gitflow-guard --help
13
94
 
14
95
  说明:
15
- permit/confirm 是用户专属授权操作, agent 执行会被插件拦截。
16
- status/audit 只读, agent 可自查。`;
96
+ status/audit 只读, agent 可自查。
97
+ check 读 stdin hook payload 做门禁(exit 0=放行 / 2=拦截), 供 Claude Code 等 agent 的 pre/post hook 调用。`;
17
98
  function parseFlags(argv) {
18
- const positional = [];
19
99
  const flags = {};
20
100
  for (let i = 0; i < argv.length; i++) {
21
101
  const a = argv[i];
22
102
  const next = () => argv[++i];
23
103
  if (a === "--repo") flags.repo = next();
24
- else if (a === "--kind") flags.kind = next();
25
- else if (a === "--ttl") flags.ttl = Number(next());
26
104
  else if (a === "--lines") flags.lines = Number(next());
105
+ else if (a === "--platform") flags.platform = next();
106
+ else if (a === "--command") flags.command = next();
27
107
  else if (a.startsWith("--repo=")) flags.repo = a.slice(7);
28
- else if (a.startsWith("--kind=")) flags.kind = a.slice(7);
29
- else if (a.startsWith("--ttl=")) flags.ttl = Number(a.slice(6));
30
108
  else if (a.startsWith("--lines=")) flags.lines = Number(a.slice(8));
31
- else positional.push(a);
109
+ else if (a.startsWith("--platform=")) flags.platform = a.slice(11);
110
+ else if (a.startsWith("--command=")) flags.command = a.slice(10);
32
111
  }
33
- return {
34
- positional,
35
- flags
36
- };
112
+ return flags;
37
113
  }
38
114
  async function resolveRepo(flags) {
39
115
  if (flags.repo) return flags.repo;
@@ -46,15 +122,11 @@ async function main(argv, opts = {}) {
46
122
  console.log(USAGE);
47
123
  return 0;
48
124
  }
49
- const { positional, flags } = parseFlags(rest);
125
+ const flags = parseFlags(rest);
50
126
  try {
51
- if (cmd === "permit") return await permit(positional, flags, runner);
52
- if (cmd === "confirm") return await permit(positional, {
53
- ...flags,
54
- kind: "confirm"
55
- }, runner);
56
127
  if (cmd === "status") return await status(flags, runner);
57
128
  if (cmd === "audit") return await audit(flags);
129
+ if (cmd === "check") return await check(flags);
58
130
  console.error(`[gitflow-guard] 未知子命令: ${cmd ?? ""}\n\n${USAGE}`);
59
131
  return 1;
60
132
  } catch (e) {
@@ -62,38 +134,6 @@ async function main(argv, opts = {}) {
62
134
  return 1;
63
135
  }
64
136
  }
65
- async function permit(positional, flags, runner) {
66
- const feature = positional[0];
67
- if (!feature) {
68
- console.error("[gitflow-guard] 用法: gitflow-guard permit <feature> [--kind early-pr|confirm|trunk-pr]");
69
- return 1;
70
- }
71
- const kind = flags.kind ?? "confirm";
72
- if (kind !== "early-pr" && kind !== "confirm" && kind !== "trunk-pr") {
73
- console.error("[gitflow-guard] --kind 必须是 early-pr / confirm / trunk-pr");
74
- return 1;
75
- }
76
- const ttlMs = flags.ttl != null && Number.isFinite(flags.ttl) && flags.ttl > 0 ? flags.ttl * 6e4 : void 0;
77
- const repoRoot = await resolveRepo(flags);
78
- if (!repoRoot) {
79
- console.error("[gitflow-guard] 无法定位 git 仓库(当前目录不在仓库内, 或用 --repo 指定)");
80
- return 1;
81
- }
82
- const { config } = await loadConfig(repoRoot);
83
- if (!config?.enabled) {
84
- console.error(`[gitflow-guard] 项目未启用 gitflow-guard(${join(repoRoot, "gitflow-guard.config.json")} 不存在或 enabled=false)`);
85
- return 1;
86
- }
87
- const granted = await (await openPermitStore(join(stateDir(repoRoot), "state.json"))).grant(kind, feature, ttlMs != null ? { ttlMs } : void 0);
88
- await appendAudit(repoRoot, {
89
- time: Date.now(),
90
- event: "grant",
91
- kind,
92
- feature
93
- });
94
- console.log(`[gitflow-guard] 已授权: ${kind} → ${feature}${granted.expiresAt ? `(有效期至 ${new Date(granted.expiresAt).toLocaleString()})` : "(长期有效)"}`);
95
- return 0;
96
- }
97
137
  async function status(flags, runner) {
98
138
  const repoRoot = await resolveRepo(flags);
99
139
  if (!repoRoot) {
@@ -109,37 +149,29 @@ async function status(flags, runner) {
109
149
  return 0;
110
150
  }
111
151
  const branch = await currentBranch(runner, repoRoot);
112
- console.log(`配置: 已启用(${config.mode} 模式) | 基线: ${config.branches.base} | 预览: ${config.branches.preview}${config.branches.trunk ? ` | 主干: ${config.branches.trunk}` : ""}`);
152
+ const c = config;
153
+ console.log(`配置: 已启用 | featurePattern: ${c.featurePattern}`);
154
+ console.log(`集成分支: ${c.branches.integration.branches.join(", ")} (update=${c.branches.integration.update || "pr"})`);
155
+ if (c.branches.preview) console.log(`预览分支: ${c.branches.preview.branches.join(", ")} (update=${c.branches.preview.update || "pr"})`);
156
+ if (c.branches.production) console.log(`生产分支: ${c.branches.production.branches.join(", ")} (update=${c.branches.production.update || "pr"}, 合并=${c.branches.production.mergeBy || "user"})`);
157
+ if (c.branches.archive) console.log(`归档分支: ${c.branches.archive.branches.join(", ")}`);
113
158
  console.log(`当前分支: ${branch ?? "(未知)"}`);
114
- const pattern = new RegExp(config.confirm.featurePattern);
115
159
  const r = await runner.run([
116
160
  "for-each-ref",
117
161
  "--format=%(refname:short)",
118
162
  "refs/heads/"
119
163
  ], repoRoot);
120
- const features = (r.code === 0 ? r.stdout.split("\n").map((s) => s.trim()).filter(Boolean) : []).filter((b) => pattern.test(b));
121
- const store = await openPermitStore(join(stateDir(repoRoot), "state.json"));
122
- const inPreview = [];
123
- for (const f of features) if (await isAncestor(runner, repoRoot, f, config.branches.preview)) inPreview.push(f);
124
- console.log(`预览分支(${config.branches.preview})包含的 feature:`);
125
- for (const f of inPreview) console.log(` ✓ ${f}`);
126
- if (inPreview.length === 0) console.log(" (无)");
127
- console.log("feature 状态一览:");
128
- for (const f of features) {
129
- const confirmed = store.hasValid("confirm", f);
130
- const early = store.hasValid("early-pr", f);
131
- const trunk = store.hasValid("trunk-pr", f);
132
- const mark = (ok) => ok ? "✓" : "✗";
133
- console.log(` ${f}: 已合预览 ${mark(inPreview.includes(f))} | 已确认(P2) ${mark(confirmed)} | P1 ${early ? "✓" : "-"} | P3 ${trunk ? "✓" : "-"}`);
134
- }
135
- const permits = store.list();
136
- if (permits.length > 0) {
137
- console.log("特许记录:");
138
- for (const p of permits) {
139
- const state = p.used ? "已使用" : p.expiresAt && p.expiresAt <= Date.now() ? "已过期" : "未使用";
140
- console.log(` ${p.kind} ${p.feature} (${state})`);
141
- }
142
- }
164
+ const localBranches = r.code === 0 ? r.stdout.split("\n").map((s) => s.trim()).filter(Boolean) : [];
165
+ const classifyBranch = (b) => {
166
+ if (c.branches.production && roleMatches(b, c.branches.production)) return "production";
167
+ if (c.branches.preview && roleMatches(b, c.branches.preview)) return "preview";
168
+ if (roleMatches(b, c.branches.integration)) return "integration";
169
+ if (c.branches.archive && roleMatches(b, c.branches.archive)) return "archive";
170
+ if (new RegExp(c.featurePattern).test(b)) return "feature";
171
+ return "other";
172
+ };
173
+ console.log("本地分支(按角色):");
174
+ for (const b of localBranches) console.log(` ${b} → ${classifyBranch(b)}`);
143
175
  return 0;
144
176
  }
145
177
  async function audit(flags) {
@@ -153,7 +185,7 @@ async function audit(flags) {
153
185
  const all = (await readFile(join(stateDir(repoRoot), "audit.jsonl"), "utf8")).split("\n").filter(Boolean);
154
186
  for (const line of all.slice(-lines)) try {
155
187
  const e = JSON.parse(line);
156
- console.log(` ${new Date(e.time).toLocaleString()} ${e.event} ${e.kind ?? ""} ${e.feature ?? ""}${e.command ? ` | ${e.command.slice(0, 80)}` : ""}`);
188
+ console.log(` ${new Date(e.time).toLocaleString()} ${e.event} ${e.role ?? ""}${e.command ? ` | ${e.command.slice(0, 80)}` : ""}${e.reason ? ` | ${e.reason.slice(0, 60)}` : ""}`);
157
189
  } catch {
158
190
  console.log(` ${line}`);
159
191
  }
@@ -162,5 +194,46 @@ async function audit(flags) {
162
194
  }
163
195
  return 0;
164
196
  }
197
+ function readStdin() {
198
+ return new Promise((resolve) => {
199
+ let data = "";
200
+ process.stdin.setEncoding("utf8");
201
+ process.stdin.on("data", (chunk) => data += chunk);
202
+ process.stdin.on("end", () => resolve(data));
203
+ process.stdin.on("error", () => resolve(data));
204
+ });
205
+ }
206
+ /** check: agent hook 门禁。读 stdin hook payload(或 --command), exit 0=放行 / 2=拦截(按平台编码) */
207
+ async function check(flags) {
208
+ const platform = flags.platform ?? "auto";
209
+ try {
210
+ const raw = flags.command != null ? "" : await readStdin();
211
+ const payload = flags.command != null ? {
212
+ command: flags.command,
213
+ cwd: flags.repo,
214
+ event: "pre"
215
+ } : extractHookPayload(raw, platform);
216
+ if (!payload?.command) return 0;
217
+ const segments = classify(payload.command);
218
+ if (segments.length === 0 || segments.every((s) => s.kind === "other")) return 0;
219
+ const cwd = payload.cwd ?? process.cwd();
220
+ const repoRoot = flags.repo ?? await findRepoRoot(gitRunner, cwd);
221
+ if (!repoRoot) return 0;
222
+ const { config } = await loadConfig(repoRoot);
223
+ if (!config?.enabled) return 0;
224
+ const hookPlatform = platform === "auto" ? detectPlatform(raw) : platform;
225
+ const result = await evaluateCommand(payload.command, { repoRoot });
226
+ if (result.outcome === "deny" && result.reason) {
227
+ const enc = encodeDeny(hookPlatform, formatDeny(result.reason.why, result.reason.next));
228
+ if (enc.stdout) process.stdout.write(enc.stdout + "\n");
229
+ if (enc.stderr) process.stderr.write(enc.stderr + "\n");
230
+ return enc.exitCode;
231
+ }
232
+ return 0;
233
+ } catch (e) {
234
+ process.stderr.write(`[gitflow-guard] check 内部错误, 已放行: ${e.message}\n`);
235
+ return 0;
236
+ }
237
+ }
165
238
  //#endregion
166
239
  export { main };
package/lib/index.d.mts CHANGED
@@ -1,29 +1,20 @@
1
1
  import { t as Runner } from "./repo-DrgptHl1.mjs";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
- //#region src/types.d.ts
4
- /** 特许类型: P1 提前建 PR / P2 确认合入 / P3 许可 trunk PR */
5
- type PermitKind = 'early-pr' | 'confirm' | 'trunk-pr';
6
- //#endregion
7
3
  //#region src/index.d.ts
8
4
  declare const name = "gitflow-guard";
9
5
  interface PluginConfig {
10
6
  /** 拦截哪些工具的命令文本(默认 pwsh / bash) */
11
7
  toolNames?: string[];
12
- /** 特许默认有效期(毫秒; 默认不过期) */
13
- permitTtlMs?: number;
14
8
  }
15
9
  interface EvaluateOptions {
16
10
  repoRoot: string;
17
11
  runner?: Runner;
18
- /** gh 适配器执行器(测试注入; 默认真实 gh) */
12
+ /** GitHub gh 适配器执行器(测试注入; 默认真实 gh) */
19
13
  ghRunner?: Runner;
14
+ /** GitLab glab 适配器执行器(测试注入; 默认真实 glab) */
15
+ glabRunner?: Runner;
20
16
  /** 当前分支(缺省时内部查询) */
21
17
  currentBranch?: string | null;
22
- now?: () => number;
23
- }
24
- interface PendingConsume {
25
- kind: PermitKind;
26
- feature: string;
27
18
  }
28
19
  interface EvaluateResult {
29
20
  outcome: 'allow' | 'deny' | 'skipped';
@@ -32,15 +23,12 @@ interface EvaluateResult {
32
23
  next: string;
33
24
  };
34
25
  segmentCount: number;
35
- /** 放行时可能被本次动作消费的特许(动作成功后由 post-execute 消费) */
36
- pendingConsume: PendingConsume[];
37
26
  }
38
27
  interface AuditEntry {
39
28
  time: number;
40
- event: 'allow' | 'deny' | 'grant' | 'consume' | 'remind' | 'ci';
29
+ event: 'deny' | 'ci';
41
30
  command?: string;
42
- feature?: string;
43
- kind?: string;
31
+ role?: string;
44
32
  reason?: string;
45
33
  }
46
34
  declare function stateDir(repoRoot: string): string;
@@ -48,6 +36,7 @@ declare function stateDir(repoRoot: string): string;
48
36
  declare function appendAudit(repoRoot: string, entry: AuditEntry): Promise<void>;
49
37
  /** 解析一条命令: 分类 → git 事实 → 门禁 → allow/deny */
50
38
  declare function evaluateCommand(command: string, opts: EvaluateOptions): Promise<EvaluateResult>;
39
+ declare function formatDeny(why: string, next: string): string;
51
40
  declare function apply(ctx: Context, pluginConfig?: PluginConfig): void;
52
41
  //#endregion
53
- export { AuditEntry, EvaluateOptions, EvaluateResult, PendingConsume, PluginConfig, appendAudit, apply, evaluateCommand, name, stateDir };
42
+ export { AuditEntry, EvaluateOptions, EvaluateResult, PluginConfig, appendAudit, apply, evaluateCommand, formatDeny, name, stateDir };
package/lib/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as stateDir, i as name, n as apply, r as evaluateCommand, t as appendAudit } from "./src-DWE1n9Zh.mjs";
2
- export { appendAudit, apply, evaluateCommand, name, stateDir };
1
+ import { a as name, i as formatDeny, n as apply, o as stateDir, r as evaluateCommand, t as appendAudit } from "./src-Dv6jKDsO.mjs";
2
+ export { appendAudit, apply, evaluateCommand, formatDeny, name, stateDir };