@a5c-ai/git-a5c 1.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 +32 -0
- package/dist/args.d.ts +39 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +79 -0
- package/dist/args.js.map +1 -0
- package/dist/bin/git-a5c.d.ts +3 -0
- package/dist/bin/git-a5c.d.ts.map +1 -0
- package/dist/bin/git-a5c.js +12 -0
- package/dist/bin/git-a5c.js.map +1 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +61 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/block.d.ts +3 -0
- package/dist/commands/block.d.ts.map +1 -0
- package/dist/commands/block.js +33 -0
- package/dist/commands/block.js.map +1 -0
- package/dist/commands/gate.d.ts +3 -0
- package/dist/commands/gate.d.ts.map +1 -0
- package/dist/commands/gate.js +31 -0
- package/dist/commands/gate.js.map +1 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +35 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/hooks.d.ts +3 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +40 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/issue.d.ts +3 -0
- package/dist/commands/issue.d.ts.map +1 -0
- package/dist/commands/issue.js +134 -0
- package/dist/commands/issue.js.map +1 -0
- package/dist/commands/journal.d.ts +3 -0
- package/dist/commands/journal.d.ts.map +1 -0
- package/dist/commands/journal.js +78 -0
- package/dist/commands/journal.js.map +1 -0
- package/dist/commands/ops.d.ts +3 -0
- package/dist/commands/ops.d.ts.map +1 -0
- package/dist/commands/ops.js +39 -0
- package/dist/commands/ops.js.map +1 -0
- package/dist/commands/pr.d.ts +3 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +136 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +17 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/types.d.ts +17 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +20 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/webhook.d.ts +3 -0
- package/dist/commands/webhook.d.ts.map +1 -0
- package/dist/commands/webhook.js +61 -0
- package/dist/commands/webhook.js.map +1 -0
- package/dist/git.d.ts +5 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +35 -0
- package/dist/git.js.map +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +83 -0
- package/dist/run.js.map +1 -0
- package/dist/time.d.ts +2 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +16 -0
- package/dist/time.js.map +1 -0
- package/package.json +24 -0
- package/src/args.ts +87 -0
- package/src/bin/git-a5c.ts +14 -0
- package/src/commands/agent.ts +72 -0
- package/src/commands/block.ts +34 -0
- package/src/commands/gate.ts +32 -0
- package/src/commands/help.ts +38 -0
- package/src/commands/hooks.ts +40 -0
- package/src/commands/issue.ts +146 -0
- package/src/commands/journal.ts +75 -0
- package/src/commands/ops.ts +41 -0
- package/src/commands/pr.ts +147 -0
- package/src/commands/status.ts +18 -0
- package/src/commands/types.ts +20 -0
- package/src/commands/verify.ts +20 -0
- package/src/commands/webhook.ts +63 -0
- package/src/git.ts +38 -0
- package/src/run.ts +99 -0
- package/src/time.ts +16 -0
- package/test/_util.ts +88 -0
- package/test/cli.flow.integration.test.ts +86 -0
- package/test/cli.snapshots.test.ts +50 -0
- package/test/cli.webhook.integration.test.ts +62 -0
- package/test/cli.write.integration.test.ts +70 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CommandArgs } from "./types.js";
|
|
2
|
+
import { parseSinceToEpochMs } from "../time.js";
|
|
3
|
+
|
|
4
|
+
function typeMatches(kind: string, patterns: string[] | undefined): boolean {
|
|
5
|
+
if (!patterns || patterns.length === 0) return true;
|
|
6
|
+
return patterns.some((p) => (p.endsWith(".*") ? kind.startsWith(p.slice(0, -2)) : kind === p));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function entityMatches(ev: any, entity: string | undefined): boolean {
|
|
10
|
+
if (!entity) return true;
|
|
11
|
+
const kind = ev.kind as string;
|
|
12
|
+
const payload = ev.payload ?? {};
|
|
13
|
+
|
|
14
|
+
if (kind === "issue.event.created") return payload.issueId === entity;
|
|
15
|
+
if (kind.startsWith("comment.")) return payload.entity?.id === entity;
|
|
16
|
+
if (kind.startsWith("pr.")) return payload.prKey === entity;
|
|
17
|
+
if (kind === "dep.changed" || kind === "gate.changed") return payload.entity?.id === entity;
|
|
18
|
+
if (kind.startsWith("agent.")) return payload.entity?.id === entity;
|
|
19
|
+
if (kind.startsWith("ops.")) return payload.entity?.id === entity;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function handleJournal(args: CommandArgs): Promise<number | undefined> {
|
|
24
|
+
if (args.positionals[0] !== "journal") return;
|
|
25
|
+
|
|
26
|
+
const limit = Number.isFinite(args.flags.limit as any) ? (args.flags.limit as number) : 20;
|
|
27
|
+
const sinceMs = args.flags.since ? parseSinceToEpochMs(args.flags.since, args.nowMs()) : undefined;
|
|
28
|
+
const events = [...args.snap.collabEvents, ...(args.snap.inbox?.events ?? [])]
|
|
29
|
+
.map((e: any) => ({
|
|
30
|
+
time: (e.event as any).time as string,
|
|
31
|
+
actor: (e.event as any).actor as string,
|
|
32
|
+
kind: e.kind,
|
|
33
|
+
id: (e.event as any).id as string,
|
|
34
|
+
payload: (e.event as any).payload as any
|
|
35
|
+
}))
|
|
36
|
+
.filter((e: any) => typeMatches(e.kind, args.flags.types))
|
|
37
|
+
.filter((e: any) => entityMatches({ kind: e.kind, payload: e.payload }, args.flags.entity))
|
|
38
|
+
.filter((e: any) => {
|
|
39
|
+
if (!sinceMs) return true;
|
|
40
|
+
const t = Date.parse(e.time);
|
|
41
|
+
return Number.isFinite(t) && t >= sinceMs;
|
|
42
|
+
})
|
|
43
|
+
.sort((a: any, b: any) => (a.time < b.time ? 1 : a.time > b.time ? -1 : a.id < b.id ? 1 : -1))
|
|
44
|
+
.slice(0, limit);
|
|
45
|
+
|
|
46
|
+
if (args.flags.json) {
|
|
47
|
+
const base = events.map(({ payload: _p, ...rest }: any) => rest);
|
|
48
|
+
if (!args.flags.active) {
|
|
49
|
+
args.io.writeLine(args.io.out, JSON.stringify(base, null, 2));
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const now = args.nowMs();
|
|
53
|
+
const byAgent = new Map<string, any>();
|
|
54
|
+
for (const e of events) {
|
|
55
|
+
if (e.kind !== "agent.heartbeat.created") continue;
|
|
56
|
+
const agentId = String(e.payload?.agentId ?? "");
|
|
57
|
+
if (!agentId) continue;
|
|
58
|
+
byAgent.set(agentId, e);
|
|
59
|
+
}
|
|
60
|
+
const active = [...byAgent.values()].filter((e) => {
|
|
61
|
+
const ttlSeconds = Number(e.payload?.ttlSeconds ?? 0);
|
|
62
|
+
const t = Date.parse(e.time);
|
|
63
|
+
return Number.isFinite(t) && ttlSeconds > 0 && t + ttlSeconds * 1000 >= now;
|
|
64
|
+
});
|
|
65
|
+
args.io.writeLine(args.io.out, JSON.stringify({ events: base, activeAgents: active }, null, 2));
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const e of events) {
|
|
70
|
+
args.io.writeLine(args.io.out, `${e.time} ${e.actor} ${e.kind} ${e.id}`);
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CommandArgs } from "./types.js";
|
|
2
|
+
import { git, gitConfigGet } from "../git.js";
|
|
3
|
+
import { HlcClock, loadHlcState, saveHlcState, stageFiles, writeOpsBuild, writeOpsDeploy, writeOpsTest } from "@a5cforge/sdk";
|
|
4
|
+
|
|
5
|
+
export async function handleOps(args: CommandArgs): Promise<number | undefined> {
|
|
6
|
+
if (args.positionals[0] !== "ops") return;
|
|
7
|
+
const sub = args.positionals[1];
|
|
8
|
+
if (sub !== "deploy" && sub !== "build" && sub !== "test") {
|
|
9
|
+
args.io.writeLine(args.io.err, "usage: git a5c ops deploy|build|test --entity <id> [--artifact ...] [--rev ...] [--env ...]");
|
|
10
|
+
return 2;
|
|
11
|
+
}
|
|
12
|
+
const entityId = args.flags.entity;
|
|
13
|
+
if (!entityId) {
|
|
14
|
+
args.io.writeLine(args.io.err, "missing --entity");
|
|
15
|
+
return 2;
|
|
16
|
+
}
|
|
17
|
+
const actor = process.env.A5C_ACTOR ?? (await gitConfigGet(args.repoRoot, "user.name")) ?? "unknown";
|
|
18
|
+
const persisted = (await loadHlcState(actor)) ?? { wallMs: 0, counter: 0 };
|
|
19
|
+
const clock = new HlcClock(persisted);
|
|
20
|
+
let nonce = 0;
|
|
21
|
+
const ctx = { repoRoot: args.repoRoot, actor, clock, nextNonce: () => String(++nonce).padStart(4, "0") };
|
|
22
|
+
const time = new Date(args.nowMs()).toISOString();
|
|
23
|
+
const entity = { type: entityId.startsWith("pr-") ? "pr" : "issue", id: entityId } as const;
|
|
24
|
+
const artifact = args.flags.artifact ? { name: args.flags.artifact, uri: args.flags.artifact } : undefined;
|
|
25
|
+
const res =
|
|
26
|
+
sub === "build"
|
|
27
|
+
? await writeOpsBuild(ctx, { entity, status: args.flags.message as any, artifact, time })
|
|
28
|
+
: sub === "test"
|
|
29
|
+
? await writeOpsTest(ctx, { entity, status: args.flags.message as any, artifact, time })
|
|
30
|
+
: await writeOpsDeploy(ctx, { entity, status: args.flags.message as any, artifact, time });
|
|
31
|
+
await saveHlcState(actor, clock.now());
|
|
32
|
+
if (args.flags.stageOnly || args.flags.commit) await stageFiles(args.repoRoot, [res.path]);
|
|
33
|
+
if (args.flags.commit) {
|
|
34
|
+
const msg = args.flags.message ?? `a5c: ops ${sub} ${entityId}`;
|
|
35
|
+
await git(["-c", "user.name=a5c", "-c", "user.email=a5c@example.invalid", "commit", "-m", msg], args.repoRoot);
|
|
36
|
+
}
|
|
37
|
+
args.io.writeLine(args.io.out, res.path);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { CommandArgs } from "./types.js";
|
|
2
|
+
import { git, gitConfigGet } from "../git.js";
|
|
3
|
+
import {
|
|
4
|
+
HlcClock,
|
|
5
|
+
UlidGenerator,
|
|
6
|
+
loadHlcState,
|
|
7
|
+
saveHlcState,
|
|
8
|
+
stageFiles,
|
|
9
|
+
listPRs,
|
|
10
|
+
renderPR,
|
|
11
|
+
writePrProposal,
|
|
12
|
+
writePrRequest,
|
|
13
|
+
writePrEvent
|
|
14
|
+
} from "@a5cforge/sdk";
|
|
15
|
+
|
|
16
|
+
export async function handlePr(args: CommandArgs): Promise<number | undefined> {
|
|
17
|
+
if (args.positionals[0] !== "pr") return;
|
|
18
|
+
const sub = args.positionals[1];
|
|
19
|
+
|
|
20
|
+
if (sub === "list") {
|
|
21
|
+
const keys = listPRs(args.snap);
|
|
22
|
+
if (args.flags.json) args.io.writeLine(args.io.out, JSON.stringify(keys, null, 2));
|
|
23
|
+
else keys.forEach((k) => args.io.writeLine(args.io.out, k));
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (sub === "show") {
|
|
28
|
+
const key = args.positionals[2];
|
|
29
|
+
if (!key) throw new Error("missing prKey");
|
|
30
|
+
const pr = renderPR(args.snap, key);
|
|
31
|
+
if (!pr) {
|
|
32
|
+
args.io.writeLine(args.io.err, `not found: ${key}`);
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
if (args.flags.json) args.io.writeLine(args.io.out, JSON.stringify(pr, null, 2));
|
|
36
|
+
else {
|
|
37
|
+
args.io.writeLine(args.io.out, `${pr.prKey}: ${pr.title}`);
|
|
38
|
+
args.io.writeLine(args.io.out, `base: ${pr.baseRef}`);
|
|
39
|
+
if (pr.headRef) args.io.writeLine(args.io.out, `head: ${pr.headRef}`);
|
|
40
|
+
args.io.writeLine(args.io.out, `events: ${pr.events.length}`);
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (sub === "propose") {
|
|
46
|
+
const baseRef = args.flags.base;
|
|
47
|
+
const headRef = args.flags.head;
|
|
48
|
+
const title = args.flags.title;
|
|
49
|
+
if (!baseRef || !headRef || !title) {
|
|
50
|
+
args.io.writeLine(args.io.err, "missing --base, --head, or --title");
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
const actor = process.env.A5C_ACTOR ?? (await gitConfigGet(args.repoRoot, "user.name")) ?? "unknown";
|
|
54
|
+
const prKey = args.flags.id ?? `pr-${new UlidGenerator().generate()}`;
|
|
55
|
+
const time = new Date(args.nowMs()).toISOString();
|
|
56
|
+
const persisted = (await loadHlcState(actor)) ?? { wallMs: 0, counter: 0 };
|
|
57
|
+
const clock = new HlcClock(persisted);
|
|
58
|
+
let nonce = 0;
|
|
59
|
+
const ctx = { repoRoot: args.repoRoot, actor, clock, nextNonce: () => String(++nonce).padStart(4, "0") };
|
|
60
|
+
const res = await writePrProposal(ctx, { prKey, baseRef, headRef, title, body: args.flags.body, time });
|
|
61
|
+
await saveHlcState(actor, clock.now());
|
|
62
|
+
if (args.flags.stageOnly || args.flags.commit) await stageFiles(args.repoRoot, [res.path]);
|
|
63
|
+
if (args.flags.commit) {
|
|
64
|
+
const msg = args.flags.message ?? `a5c: pr propose ${prKey}`;
|
|
65
|
+
await git(["-c", "user.name=a5c", "-c", "user.email=a5c@example.invalid", "commit", "-m", msg], args.repoRoot);
|
|
66
|
+
}
|
|
67
|
+
args.io.writeLine(args.io.out, prKey);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (sub === "request") {
|
|
72
|
+
const baseRef = args.flags.base;
|
|
73
|
+
const title = args.flags.title;
|
|
74
|
+
if (!baseRef || !title) {
|
|
75
|
+
args.io.writeLine(args.io.err, "missing --base or --title");
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
const actor = process.env.A5C_ACTOR ?? (await gitConfigGet(args.repoRoot, "user.name")) ?? "unknown";
|
|
79
|
+
const prKey = args.flags.id ?? `pr-${new UlidGenerator().generate()}`;
|
|
80
|
+
const time = new Date(args.nowMs()).toISOString();
|
|
81
|
+
const persisted = (await loadHlcState(actor)) ?? { wallMs: 0, counter: 0 };
|
|
82
|
+
const clock = new HlcClock(persisted);
|
|
83
|
+
let nonce = 0;
|
|
84
|
+
const ctx = { repoRoot: args.repoRoot, actor, clock, nextNonce: () => String(++nonce).padStart(4, "0") };
|
|
85
|
+
const res = await writePrRequest(ctx, { prKey, baseRef, title, body: args.flags.body, time });
|
|
86
|
+
await saveHlcState(actor, clock.now());
|
|
87
|
+
if (args.flags.stageOnly || args.flags.commit) await stageFiles(args.repoRoot, [res.path]);
|
|
88
|
+
if (args.flags.commit) {
|
|
89
|
+
const msg = args.flags.message ?? `a5c: pr request ${prKey}`;
|
|
90
|
+
await git(["-c", "user.name=a5c", "-c", "user.email=a5c@example.invalid", "commit", "-m", msg], args.repoRoot);
|
|
91
|
+
}
|
|
92
|
+
args.io.writeLine(args.io.out, prKey);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (sub === "claim") {
|
|
97
|
+
const prKey = args.positionals[2];
|
|
98
|
+
const headRef = args.flags.headRef;
|
|
99
|
+
if (!prKey || !headRef) {
|
|
100
|
+
args.io.writeLine(args.io.err, "missing prKey or --head-ref");
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
const actor = process.env.A5C_ACTOR ?? (await gitConfigGet(args.repoRoot, "user.name")) ?? "unknown";
|
|
104
|
+
const time = new Date(args.nowMs()).toISOString();
|
|
105
|
+
const persisted = (await loadHlcState(actor)) ?? { wallMs: 0, counter: 0 };
|
|
106
|
+
const clock = new HlcClock(persisted);
|
|
107
|
+
let nonce = 0;
|
|
108
|
+
const ctx = { repoRoot: args.repoRoot, actor, clock, nextNonce: () => String(++nonce).padStart(4, "0") };
|
|
109
|
+
const res = await writePrEvent(ctx, { prKey, action: "claim", headRef, message: args.flags.message as any, time });
|
|
110
|
+
await saveHlcState(actor, clock.now());
|
|
111
|
+
if (args.flags.stageOnly || args.flags.commit) await stageFiles(args.repoRoot, [res.path]);
|
|
112
|
+
if (args.flags.commit) {
|
|
113
|
+
const msg = args.flags.message ?? `a5c: pr claim ${prKey}`;
|
|
114
|
+
await git(["-c", "user.name=a5c", "-c", "user.email=a5c@example.invalid", "commit", "-m", msg], args.repoRoot);
|
|
115
|
+
}
|
|
116
|
+
args.io.writeLine(args.io.out, res.path);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (sub === "bind-head") {
|
|
121
|
+
const prKey = args.positionals[2];
|
|
122
|
+
const headRef = args.flags.headRef;
|
|
123
|
+
if (!prKey || !headRef) {
|
|
124
|
+
args.io.writeLine(args.io.err, "missing prKey or --head-ref");
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
const actor = process.env.A5C_ACTOR ?? (await gitConfigGet(args.repoRoot, "user.name")) ?? "unknown";
|
|
128
|
+
const time = new Date(args.nowMs()).toISOString();
|
|
129
|
+
const persisted = (await loadHlcState(actor)) ?? { wallMs: 0, counter: 0 };
|
|
130
|
+
const clock = new HlcClock(persisted);
|
|
131
|
+
let nonce = 0;
|
|
132
|
+
const ctx = { repoRoot: args.repoRoot, actor, clock, nextNonce: () => String(++nonce).padStart(4, "0") };
|
|
133
|
+
const res = await writePrEvent(ctx, { prKey, action: "bindHead", headRef, message: args.flags.message as any, time });
|
|
134
|
+
await saveHlcState(actor, clock.now());
|
|
135
|
+
if (args.flags.stageOnly || args.flags.commit) await stageFiles(args.repoRoot, [res.path]);
|
|
136
|
+
if (args.flags.commit) {
|
|
137
|
+
const msg = args.flags.message ?? `a5c: pr bind-head ${prKey}`;
|
|
138
|
+
await git(["-c", "user.name=a5c", "-c", "user.email=a5c@example.invalid", "commit", "-m", msg], args.repoRoot);
|
|
139
|
+
}
|
|
140
|
+
args.io.writeLine(args.io.out, res.path);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CommandArgs } from "./types.js";
|
|
2
|
+
import { listIssues, listPRs } from "@a5cforge/sdk";
|
|
3
|
+
|
|
4
|
+
export function handleStatus(args: CommandArgs): number | undefined {
|
|
5
|
+
if (args.positionals[0] !== "status") return;
|
|
6
|
+
const issues = listIssues(args.snap).length;
|
|
7
|
+
const prs = listPRs(args.snap).length;
|
|
8
|
+
if (args.flags.json) {
|
|
9
|
+
args.io.writeLine(args.io.out, JSON.stringify({ treeish: args.treeish, issues, prs }, null, 2));
|
|
10
|
+
} else {
|
|
11
|
+
args.io.writeLine(args.io.out, `treeish: ${args.treeish}`);
|
|
12
|
+
args.io.writeLine(args.io.out, `issues: ${issues}`);
|
|
13
|
+
args.io.writeLine(args.io.out, `prs: ${prs}`);
|
|
14
|
+
}
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ParsedArgs } from "../args.js";
|
|
2
|
+
|
|
3
|
+
export type CommandIO = {
|
|
4
|
+
out: (s: string) => void;
|
|
5
|
+
err: (s: string) => void;
|
|
6
|
+
writeLine: (write: (s: string) => void, s?: string) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type CommandArgs = {
|
|
10
|
+
repoRoot: string;
|
|
11
|
+
treeish: string;
|
|
12
|
+
flags: ParsedArgs["flags"];
|
|
13
|
+
positionals: string[];
|
|
14
|
+
repo: any;
|
|
15
|
+
snap: any;
|
|
16
|
+
nowMs: () => number;
|
|
17
|
+
io: CommandIO;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CommandArgs } from "./types.js";
|
|
2
|
+
import { verify } from "@a5cforge/sdk";
|
|
3
|
+
|
|
4
|
+
export function handleVerify(args: CommandArgs): number | undefined {
|
|
5
|
+
if (args.positionals[0] !== "verify") return;
|
|
6
|
+
const v = verify(args.snap);
|
|
7
|
+
if (args.flags.json) {
|
|
8
|
+
args.io.writeLine(args.io.out, JSON.stringify(v, null, 2));
|
|
9
|
+
} else {
|
|
10
|
+
const counts = v.reduce<Record<string, number>>((acc, x: any) => {
|
|
11
|
+
acc[x.status] = (acc[x.status] ?? 0) + 1;
|
|
12
|
+
return acc;
|
|
13
|
+
}, {});
|
|
14
|
+
args.io.writeLine(args.io.out, `events: ${v.length}`);
|
|
15
|
+
for (const k of Object.keys(counts).sort()) args.io.writeLine(args.io.out, `${k}: ${counts[k]}`);
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CommandArgs } from "./types.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function handleWebhook(args: CommandArgs): Promise<number | undefined> {
|
|
5
|
+
if (args.positionals[0] !== "webhook") return;
|
|
6
|
+
|
|
7
|
+
const sub = args.positionals[1];
|
|
8
|
+
if (sub === "status") {
|
|
9
|
+
try {
|
|
10
|
+
const commitOid = await args.repo.git.revParse(args.treeish);
|
|
11
|
+
const raw = await args.repo.git.readBlob(commitOid, ".collab/webhooks.json");
|
|
12
|
+
const cfg = JSON.parse(raw.toString("utf8"));
|
|
13
|
+
const endpoints = Array.isArray(cfg?.endpoints) ? cfg.endpoints : [];
|
|
14
|
+
if (args.flags.json) args.io.writeLine(args.io.out, JSON.stringify({ schema: cfg?.schema, endpoints }, null, 2));
|
|
15
|
+
else {
|
|
16
|
+
args.io.writeLine(args.io.out, `schema: ${cfg?.schema ?? "?"}`);
|
|
17
|
+
args.io.writeLine(args.io.out, `endpoints: ${endpoints.length}`);
|
|
18
|
+
for (const e of endpoints) {
|
|
19
|
+
args.io.writeLine(args.io.out, `- ${e.id}: ${e.url} (${(e.events ?? []).join(",")}) ${e.enabled === false ? "[disabled]" : ""}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
} catch {
|
|
24
|
+
args.io.writeLine(args.io.err, `no webhooks config at .collab/webhooks.json in ${args.treeish}`);
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (sub === "test") {
|
|
30
|
+
const url = args.flags.url;
|
|
31
|
+
if (!url) {
|
|
32
|
+
args.io.writeLine(args.io.err, "usage: git a5c webhook test --url <url> [--type <type>]");
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
const type = args.flags.type ?? "git.ref.updated";
|
|
36
|
+
const envelope = {
|
|
37
|
+
schema: "a5cforge/v1",
|
|
38
|
+
type,
|
|
39
|
+
id: `cli:${Date.now()}`,
|
|
40
|
+
time: new Date(args.nowMs()).toISOString(),
|
|
41
|
+
repo: { id: path.basename(args.repoRoot), path: args.repoRoot },
|
|
42
|
+
source: { serverId: "cli", keyId: undefined },
|
|
43
|
+
data: { note: "test" }
|
|
44
|
+
};
|
|
45
|
+
const r = await fetch(String(url), {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "content-type": "application/json" },
|
|
48
|
+
body: JSON.stringify(envelope, null, 2) + "\n"
|
|
49
|
+
});
|
|
50
|
+
const text = await r.text();
|
|
51
|
+
if (args.flags.json) args.io.writeLine(args.io.out, JSON.stringify({ status: r.status, body: text }, null, 2));
|
|
52
|
+
else {
|
|
53
|
+
args.io.writeLine(args.io.out, `status: ${r.status}`);
|
|
54
|
+
args.io.writeLine(args.io.out, text.trim());
|
|
55
|
+
}
|
|
56
|
+
return r.ok ? 0 : 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
args.io.writeLine(args.io.err, "usage: git a5c webhook status|test ...");
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export async function git(args: string[], cwd: string): Promise<string> {
|
|
4
|
+
return await new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
6
|
+
const out: Buffer[] = [];
|
|
7
|
+
const err: Buffer[] = [];
|
|
8
|
+
child.stdout.on("data", (d: Buffer) => out.push(Buffer.from(d)));
|
|
9
|
+
child.stderr.on("data", (d: Buffer) => err.push(Buffer.from(d)));
|
|
10
|
+
child.on("error", reject);
|
|
11
|
+
child.on("close", (code: number | null) => {
|
|
12
|
+
if (code === 0) return resolve(Buffer.concat(out).toString("utf8"));
|
|
13
|
+
reject(new Error(`git ${args.join(" ")} failed (code=${code}): ${Buffer.concat(err).toString("utf8")}`));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function detectRepoRoot(cwd: string): Promise<string> {
|
|
19
|
+
const out = await git(["rev-parse", "--show-toplevel"], cwd);
|
|
20
|
+
return out.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function gitConfigGet(cwd: string, key: string): Promise<string | undefined> {
|
|
24
|
+
try {
|
|
25
|
+
const out = await git(["config", "--get", key], cwd);
|
|
26
|
+
const v = out.trim();
|
|
27
|
+
return v.length ? v : undefined;
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function gitPath(cwd: string, name: string): Promise<string> {
|
|
34
|
+
const out = await git(["rev-parse", "--git-path", name], cwd);
|
|
35
|
+
return out.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { detectRepoRoot } from "./git.js";
|
|
2
|
+
import { parseArgs } from "./args.js";
|
|
3
|
+
import { createLogger, loadSnapshot, openRepo, parseLogLevel } from "@a5cforge/sdk";
|
|
4
|
+
import type { CommandArgs } from "./commands/types.js";
|
|
5
|
+
import { handleHelp } from "./commands/help.js";
|
|
6
|
+
import { handleWebhook } from "./commands/webhook.js";
|
|
7
|
+
import { handleStatus } from "./commands/status.js";
|
|
8
|
+
import { handleIssue } from "./commands/issue.js";
|
|
9
|
+
import { handlePr } from "./commands/pr.js";
|
|
10
|
+
import { handleBlock } from "./commands/block.js";
|
|
11
|
+
import { handleGate } from "./commands/gate.js";
|
|
12
|
+
import { handleAgent } from "./commands/agent.js";
|
|
13
|
+
import { handleOps } from "./commands/ops.js";
|
|
14
|
+
import { handleHooks } from "./commands/hooks.js";
|
|
15
|
+
import { handleVerify } from "./commands/verify.js";
|
|
16
|
+
import { handleJournal } from "./commands/journal.js";
|
|
17
|
+
|
|
18
|
+
export type RunOptions = {
|
|
19
|
+
cwd?: string;
|
|
20
|
+
stdout?: (s: string) => void;
|
|
21
|
+
stderr?: (s: string) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function writeLine(write: (s: string) => void, s = "") {
|
|
25
|
+
write(s + "\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nowMs(): number {
|
|
29
|
+
// Test hook for deterministic journal/active: set A5C_NOW_ISO=...
|
|
30
|
+
const iso = process.env.A5C_NOW_ISO;
|
|
31
|
+
if (iso) {
|
|
32
|
+
const ms = Date.parse(iso);
|
|
33
|
+
if (Number.isFinite(ms)) return ms;
|
|
34
|
+
}
|
|
35
|
+
return Date.now();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runCli(argv: string[], opts: RunOptions = {}): Promise<number> {
|
|
39
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
40
|
+
const out = opts.stdout ?? ((s) => process.stdout.write(s));
|
|
41
|
+
const err = opts.stderr ?? ((s) => process.stderr.write(s));
|
|
42
|
+
const log = createLogger({ base: { component: "cli" }, level: parseLogLevel(process.env.A5C_LOG_LEVEL ?? "silent") });
|
|
43
|
+
|
|
44
|
+
const { flags, positionals } = parseArgs(argv);
|
|
45
|
+
const cmd = positionals[0] ?? "help";
|
|
46
|
+
|
|
47
|
+
let repoRoot: string;
|
|
48
|
+
try {
|
|
49
|
+
repoRoot = typeof flags.repo === "string" ? flags.repo : await detectRepoRoot(cwd);
|
|
50
|
+
} catch {
|
|
51
|
+
writeLine(err, "not a git repository (use --repo <path>)");
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
54
|
+
const treeish = typeof flags.treeish === "string" ? flags.treeish : "HEAD";
|
|
55
|
+
|
|
56
|
+
log.debug("start", { cmd, treeish, repoRoot });
|
|
57
|
+
const repo = await openRepo(repoRoot);
|
|
58
|
+
const snap = await loadSnapshot({ git: repo.git, treeish, inboxRefs: flags.inboxRefs });
|
|
59
|
+
|
|
60
|
+
const baseArgs: CommandArgs = {
|
|
61
|
+
repoRoot,
|
|
62
|
+
treeish,
|
|
63
|
+
flags,
|
|
64
|
+
positionals,
|
|
65
|
+
repo,
|
|
66
|
+
snap,
|
|
67
|
+
nowMs,
|
|
68
|
+
io: { out, err, writeLine }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handlers: Array<() => number | undefined | Promise<number | undefined>> = [
|
|
72
|
+
() => handleHelp(baseArgs),
|
|
73
|
+
() => handleWebhook(baseArgs),
|
|
74
|
+
() => handleStatus(baseArgs),
|
|
75
|
+
() => handleIssue(baseArgs),
|
|
76
|
+
() => handlePr(baseArgs),
|
|
77
|
+
() => handleBlock(baseArgs),
|
|
78
|
+
() => handleGate(baseArgs),
|
|
79
|
+
() => handleAgent(baseArgs),
|
|
80
|
+
() => handleOps(baseArgs),
|
|
81
|
+
() => handleHooks(baseArgs),
|
|
82
|
+
() => handleVerify(baseArgs),
|
|
83
|
+
() => handleJournal(baseArgs)
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (const h of handlers) {
|
|
87
|
+
const r = await h();
|
|
88
|
+
if (r !== undefined) {
|
|
89
|
+
log.debug("done", { code: r });
|
|
90
|
+
return r;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
writeLine(err, `unknown command: ${cmd}`);
|
|
95
|
+
log.warn("unknown_command", { cmd });
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
package/src/time.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function parseSinceToEpochMs(since: string, nowMs: number): number {
|
|
2
|
+
// Accept:
|
|
3
|
+
// - ISO timestamp
|
|
4
|
+
// - durations like 2h, 15m, 30s, 7d
|
|
5
|
+
const iso = Date.parse(since);
|
|
6
|
+
if (Number.isFinite(iso)) return iso;
|
|
7
|
+
|
|
8
|
+
const m = /^(\d+)(s|m|h|d)$/.exec(since.trim());
|
|
9
|
+
if (!m) throw new Error(`Invalid --since: ${since}`);
|
|
10
|
+
const n = Number(m[1]);
|
|
11
|
+
const unit = m[2];
|
|
12
|
+
const mult = unit === "s" ? 1000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000;
|
|
13
|
+
return nowMs - n * mult;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
package/test/_util.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export function run(cmd: string, args: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
10
|
+
const out: Buffer[] = [];
|
|
11
|
+
const err: Buffer[] = [];
|
|
12
|
+
child.stdout.on("data", (d) => out.push(Buffer.from(d)));
|
|
13
|
+
child.stderr.on("data", (d) => err.push(Buffer.from(d)));
|
|
14
|
+
child.on("error", reject);
|
|
15
|
+
child.on("close", (code) => {
|
|
16
|
+
const stdout = Buffer.concat(out).toString("utf8");
|
|
17
|
+
const stderr = Buffer.concat(err).toString("utf8");
|
|
18
|
+
if (code === 0) return resolve({ stdout, stderr });
|
|
19
|
+
reject(new Error(`${cmd} ${args.join(" ")} failed (code=${code}): ${stderr}`));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function copyDir(src: string, dst: string) {
|
|
25
|
+
await fs.mkdir(dst, { recursive: true });
|
|
26
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
const s = path.join(src, e.name);
|
|
29
|
+
const d = path.join(dst, e.name);
|
|
30
|
+
if (e.isDirectory()) await copyDir(s, d);
|
|
31
|
+
else if (e.isFile()) await fs.copyFile(s, d);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function makeRepoFromFixture(fixtureName: string): Promise<string> {
|
|
36
|
+
const root = path.resolve(import.meta.dirname, "../../..");
|
|
37
|
+
const fixture = path.join(root, "fixtures", fixtureName);
|
|
38
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), `a5cforge-cli-${fixtureName}-`));
|
|
39
|
+
await copyDir(fixture, dir);
|
|
40
|
+
await run("git", ["init", "-q", "-b", "main"], dir);
|
|
41
|
+
await run("git", ["add", "-A"], dir);
|
|
42
|
+
await run("git", ["add", "-f", ".collab"], dir);
|
|
43
|
+
await run("git", ["-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-q", "-m", "fixture"], dir);
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function makeEmptyRepo(tmpPrefix = "a5cforge-cli-write-"): Promise<string> {
|
|
48
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), tmpPrefix));
|
|
49
|
+
await run("git", ["init", "-q", "-b", "main"], dir);
|
|
50
|
+
await run("git", ["-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-q", "-m", "init"], dir);
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function listenOnce(handler: (req: http.IncomingMessage, body: Buffer) => Promise<void> | void): Promise<{ url: string; close: () => Promise<void> }> {
|
|
55
|
+
const srv = http.createServer(async (req, res) => {
|
|
56
|
+
const chunks: Buffer[] = [];
|
|
57
|
+
req.on("data", (d) => chunks.push(Buffer.from(d)));
|
|
58
|
+
req.on("end", async () => {
|
|
59
|
+
try {
|
|
60
|
+
await handler(req, Buffer.concat(chunks));
|
|
61
|
+
res.statusCode = 200;
|
|
62
|
+
res.setHeader("content-type", "text/plain");
|
|
63
|
+
res.end("ok\n");
|
|
64
|
+
} catch (e: any) {
|
|
65
|
+
res.statusCode = 500;
|
|
66
|
+
res.setHeader("content-type", "text/plain");
|
|
67
|
+
res.end(String(e?.message ?? e));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
srv.on("error", reject);
|
|
74
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
75
|
+
const addr = srv.address();
|
|
76
|
+
if (!addr || typeof addr === "string") return reject(new Error("bad listen addr"));
|
|
77
|
+
resolve({
|
|
78
|
+
url: `http://127.0.0.1:${addr.port}`,
|
|
79
|
+
close: async () =>
|
|
80
|
+
await new Promise<void>((r) => {
|
|
81
|
+
srv.close(() => r());
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|