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/README.md +152 -209
- package/README.zh.md +182 -239
- package/lib/cli.mjs +153 -80
- package/lib/index.d.mts +7 -18
- package/lib/index.mjs +2 -2
- package/lib/src-Dv6jKDsO.mjs +716 -0
- package/package.json +7 -7
- package/lib/src-DWE1n9Zh.mjs +0 -813
package/lib/cli.mjs
CHANGED
|
@@ -1,39 +1,115 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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.
|
|
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: '
|
|
29
|
+
event: 'deny' | 'ci';
|
|
41
30
|
command?: string;
|
|
42
|
-
|
|
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,
|
|
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
|
|
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 };
|