agents-gitflow-guard 0.0.1 → 0.0.3

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,104 @@
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 resolveLocale, f as loadConfig, i as formatDeny, l as gitRunner, m as classify, o as stateDir, p as roleMatches, r as evaluateCommand, s as currentBranch, u as makeT } from "./src-LvZrsHEn.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(用户终端专属)
6
-
7
- 用法:
8
- gitflow-guard permit <feature> [--kind early-pr|confirm|trunk-pr] [--ttl <分钟>] [--repo <路径>]
9
- gitflow-guard confirm <feature> [--ttl <分钟>] [--repo <路径>]
10
- gitflow-guard status [--repo <路径>]
11
- gitflow-guard audit [--lines <数量>] [--repo <路径>]
12
- gitflow-guard --help
13
-
14
- 说明:
15
- permit/confirm 是用户专属授权操作, agent 执行会被插件拦截。
16
- status/audit 只读, agent 可自查。`;
17
87
  function parseFlags(argv) {
18
- const positional = [];
19
88
  const flags = {};
20
89
  for (let i = 0; i < argv.length; i++) {
21
90
  const a = argv[i];
22
91
  const next = () => argv[++i];
23
92
  if (a === "--repo") flags.repo = next();
24
- else if (a === "--kind") flags.kind = next();
25
- else if (a === "--ttl") flags.ttl = Number(next());
26
93
  else if (a === "--lines") flags.lines = Number(next());
94
+ else if (a === "--platform") flags.platform = next();
95
+ else if (a === "--command") flags.command = next();
27
96
  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
97
  else if (a.startsWith("--lines=")) flags.lines = Number(a.slice(8));
31
- else positional.push(a);
98
+ else if (a.startsWith("--platform=")) flags.platform = a.slice(11);
99
+ else if (a.startsWith("--command=")) flags.command = a.slice(10);
32
100
  }
33
- return {
34
- positional,
35
- flags
36
- };
101
+ return flags;
37
102
  }
38
103
  async function resolveRepo(flags) {
39
104
  if (flags.repo) return flags.repo;
@@ -41,111 +106,79 @@ async function resolveRepo(flags) {
41
106
  }
42
107
  async function main(argv, opts = {}) {
43
108
  const runner = opts.runner ?? gitRunner;
109
+ const usage = makeT("en")("usage.text");
44
110
  const [cmd, ...rest] = argv;
45
111
  if (cmd === "--help" || cmd === "help" || cmd === void 0) {
46
- console.log(USAGE);
112
+ console.log(usage);
47
113
  return 0;
48
114
  }
49
- const { positional, flags } = parseFlags(rest);
115
+ const flags = parseFlags(rest);
50
116
  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
117
  if (cmd === "status") return await status(flags, runner);
57
118
  if (cmd === "audit") return await audit(flags);
58
- console.error(`[gitflow-guard] 未知子命令: ${cmd ?? ""}\n\n${USAGE}`);
119
+ if (cmd === "check") return await check(flags);
120
+ console.error(`${makeT("en")("cli.unknownCommand", { cmd: cmd ?? "" })}\n\n${usage}`);
59
121
  return 1;
60
122
  } catch (e) {
61
123
  console.error(`[gitflow-guard] ${e.message}`);
62
124
  return 1;
63
125
  }
64
126
  }
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
127
  async function status(flags, runner) {
98
128
  const repoRoot = await resolveRepo(flags);
99
129
  if (!repoRoot) {
100
- console.error("[gitflow-guard] 无法定位 git 仓库");
130
+ console.error(makeT("en")("cli.cannotLocate"));
101
131
  return 1;
102
132
  }
103
133
  const { config, errors } = await loadConfig(repoRoot);
104
134
  const enabled = config?.enabled === true;
105
- console.log(`[gitflow-guard] status ${repoRoot}`);
135
+ const t = makeT(resolveLocale(config?.locale));
136
+ console.log(t("cli.statusTitle", { repo: repoRoot }));
106
137
  if (!enabled) {
107
- console.log("配置: 未启用(不存在 gitflow-guard.config.json 或 enabled=false)");
108
- for (const e of errors) console.log(` 配置错误: ${e}`);
138
+ console.log(t("cli.statusDisabled"));
139
+ for (const e of errors) console.log(t("cli.statusConfigError", { err: e }));
109
140
  return 0;
110
141
  }
111
142
  const branch = await currentBranch(runner, repoRoot);
112
- console.log(`配置: 已启用(${config.mode} 模式) | 基线: ${config.branches.base} | 预览: ${config.branches.preview}${config.branches.trunk ? ` | 主干: ${config.branches.trunk}` : ""}`);
113
- console.log(`当前分支: ${branch ?? "(未知)"}`);
114
- const pattern = new RegExp(config.confirm.featurePattern);
143
+ const c = config;
144
+ console.log(t("cli.statusEnabled", { pattern: c.featurePattern }));
145
+ console.log(t("cli.statusIntegration", {
146
+ list: c.branches.integration.branches.join(", "),
147
+ mode: c.branches.integration.update || "pr"
148
+ }));
149
+ if (c.branches.preview) console.log(t("cli.statusPreview", {
150
+ list: c.branches.preview.branches.join(", "),
151
+ mode: c.branches.preview.update || "pr"
152
+ }));
153
+ if (c.branches.production) console.log(t("cli.statusProduction", {
154
+ list: c.branches.production.branches.join(", "),
155
+ mode: c.branches.production.update || "pr",
156
+ merge: c.branches.production.mergeBy || "user"
157
+ }));
158
+ if (c.branches.archive) console.log(t("cli.statusArchive", { list: c.branches.archive.branches.join(", ") }));
159
+ console.log(t("cli.statusCurrentBranch", { branch: branch ?? t("cli.statusUnknownBranch") }));
115
160
  const r = await runner.run([
116
161
  "for-each-ref",
117
162
  "--format=%(refname:short)",
118
163
  "refs/heads/"
119
164
  ], 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
- }
165
+ const localBranches = r.code === 0 ? r.stdout.split("\n").map((s) => s.trim()).filter(Boolean) : [];
166
+ const classifyBranch = (b) => {
167
+ if (c.branches.production && roleMatches(b, c.branches.production)) return "production";
168
+ if (c.branches.preview && roleMatches(b, c.branches.preview)) return "preview";
169
+ if (roleMatches(b, c.branches.integration)) return "integration";
170
+ if (c.branches.archive && roleMatches(b, c.branches.archive)) return "archive";
171
+ if (new RegExp(c.featurePattern).test(b)) return "feature";
172
+ return "other";
173
+ };
174
+ console.log(t("cli.statusLocalBranches"));
175
+ for (const b of localBranches) console.log(` ${b} → ${classifyBranch(b)}`);
143
176
  return 0;
144
177
  }
145
178
  async function audit(flags) {
146
179
  const repoRoot = await resolveRepo(flags);
147
180
  if (!repoRoot) {
148
- console.error("[gitflow-guard] 无法定位 git 仓库");
181
+ console.error(makeT("en")("cli.cannotLocate"));
149
182
  return 1;
150
183
  }
151
184
  const lines = flags.lines != null && Number.isFinite(flags.lines) && flags.lines > 0 ? Math.floor(flags.lines) : 20;
@@ -153,14 +186,56 @@ async function audit(flags) {
153
186
  const all = (await readFile(join(stateDir(repoRoot), "audit.jsonl"), "utf8")).split("\n").filter(Boolean);
154
187
  for (const line of all.slice(-lines)) try {
155
188
  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)}` : ""}`);
189
+ 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
190
  } catch {
158
191
  console.log(` ${line}`);
159
192
  }
160
193
  } catch {
161
- console.log(" 暂无审计记录");
194
+ console.log(makeT("en")("cli.auditEmpty"));
162
195
  }
163
196
  return 0;
164
197
  }
198
+ function readStdin() {
199
+ return new Promise((resolve) => {
200
+ let data = "";
201
+ process.stdin.setEncoding("utf8");
202
+ process.stdin.on("data", (chunk) => data += chunk);
203
+ process.stdin.on("end", () => resolve(data));
204
+ process.stdin.on("error", () => resolve(data));
205
+ });
206
+ }
207
+ /** check: agent hook 门禁。读 stdin hook payload(或 --command), exit 0=放行 / 2=拦截(按平台编码) */
208
+ async function check(flags) {
209
+ const platform = flags.platform ?? "auto";
210
+ try {
211
+ const raw = flags.command != null ? "" : await readStdin();
212
+ const payload = flags.command != null ? {
213
+ command: flags.command,
214
+ cwd: flags.repo,
215
+ event: "pre"
216
+ } : extractHookPayload(raw, platform);
217
+ if (!payload?.command) return 0;
218
+ const segments = classify(payload.command);
219
+ if (segments.length === 0 || segments.every((s) => s.kind === "other")) return 0;
220
+ const cwd = payload.cwd ?? process.cwd();
221
+ const repoRoot = flags.repo ?? await findRepoRoot(gitRunner, cwd);
222
+ if (!repoRoot) return 0;
223
+ const { config } = await loadConfig(repoRoot);
224
+ if (!config?.enabled) return 0;
225
+ const hookPlatform = platform === "auto" ? detectPlatform(raw) : platform;
226
+ const locale = resolveLocale(config.locale);
227
+ const result = await evaluateCommand(payload.command, { repoRoot });
228
+ if (result.outcome === "deny" && result.reason) {
229
+ const enc = encodeDeny(hookPlatform, formatDeny(locale, result.reason.why, result.reason.next));
230
+ if (enc.stdout) process.stdout.write(enc.stdout + "\n");
231
+ if (enc.stderr) process.stderr.write(enc.stderr + "\n");
232
+ return enc.exitCode;
233
+ }
234
+ return 0;
235
+ } catch (e) {
236
+ process.stderr.write(`${makeT("en")("cli.checkInternalError", { msg: e.message })}\n`);
237
+ return 0;
238
+ }
239
+ }
165
240
  //#endregion
166
241
  export { main };
package/lib/index.d.mts CHANGED
@@ -1,29 +1,24 @@
1
1
  import { t as Runner } from "./repo-DrgptHl1.mjs";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
3
  //#region src/types.d.ts
4
- /** 特许类型: P1 提前建 PR / P2 确认合入 / P3 许可 trunk PR */
5
- type PermitKind = 'early-pr' | 'confirm' | 'trunk-pr';
4
+ /** 文案语言: 默认 en; 'zh' 切中文 */
5
+ type Locale = 'en' | 'zh';
6
6
  //#endregion
7
7
  //#region src/index.d.ts
8
8
  declare const name = "gitflow-guard";
9
9
  interface PluginConfig {
10
10
  /** 拦截哪些工具的命令文本(默认 pwsh / bash) */
11
11
  toolNames?: string[];
12
- /** 特许默认有效期(毫秒; 默认不过期) */
13
- permitTtlMs?: number;
14
12
  }
15
13
  interface EvaluateOptions {
16
14
  repoRoot: string;
17
15
  runner?: Runner;
18
- /** gh 适配器执行器(测试注入; 默认真实 gh) */
16
+ /** GitHub gh 适配器执行器(测试注入; 默认真实 gh) */
19
17
  ghRunner?: Runner;
18
+ /** GitLab glab 适配器执行器(测试注入; 默认真实 glab) */
19
+ glabRunner?: Runner;
20
20
  /** 当前分支(缺省时内部查询) */
21
21
  currentBranch?: string | null;
22
- now?: () => number;
23
- }
24
- interface PendingConsume {
25
- kind: PermitKind;
26
- feature: string;
27
22
  }
28
23
  interface EvaluateResult {
29
24
  outcome: 'allow' | 'deny' | 'skipped';
@@ -32,15 +27,14 @@ interface EvaluateResult {
32
27
  next: string;
33
28
  };
34
29
  segmentCount: number;
35
- /** 放行时可能被本次动作消费的特许(动作成功后由 post-execute 消费) */
36
- pendingConsume: PendingConsume[];
30
+ /** 本次评估使用的文案语言( formatDeny/审计一致) */
31
+ locale: Locale;
37
32
  }
38
33
  interface AuditEntry {
39
34
  time: number;
40
- event: 'allow' | 'deny' | 'grant' | 'consume' | 'remind' | 'ci';
35
+ event: 'deny' | 'ci';
41
36
  command?: string;
42
- feature?: string;
43
- kind?: string;
37
+ role?: string;
44
38
  reason?: string;
45
39
  }
46
40
  declare function stateDir(repoRoot: string): string;
@@ -48,6 +42,7 @@ declare function stateDir(repoRoot: string): string;
48
42
  declare function appendAudit(repoRoot: string, entry: AuditEntry): Promise<void>;
49
43
  /** 解析一条命令: 分类 → git 事实 → 门禁 → allow/deny */
50
44
  declare function evaluateCommand(command: string, opts: EvaluateOptions): Promise<EvaluateResult>;
45
+ declare function formatDeny(locale: Locale, why: string, next: string): string;
51
46
  declare function apply(ctx: Context, pluginConfig?: PluginConfig): void;
52
47
  //#endregion
53
- export { AuditEntry, EvaluateOptions, EvaluateResult, PendingConsume, PluginConfig, appendAudit, apply, evaluateCommand, name, stateDir };
48
+ 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-LvZrsHEn.mjs";
2
+ export { appendAudit, apply, evaluateCommand, formatDeny, name, stateDir };