@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.
Files changed (99) hide show
  1. package/README.md +32 -0
  2. package/dist/args.d.ts +39 -0
  3. package/dist/args.d.ts.map +1 -0
  4. package/dist/args.js +79 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/bin/git-a5c.d.ts +3 -0
  7. package/dist/bin/git-a5c.d.ts.map +1 -0
  8. package/dist/bin/git-a5c.js +12 -0
  9. package/dist/bin/git-a5c.js.map +1 -0
  10. package/dist/commands/agent.d.ts +3 -0
  11. package/dist/commands/agent.d.ts.map +1 -0
  12. package/dist/commands/agent.js +61 -0
  13. package/dist/commands/agent.js.map +1 -0
  14. package/dist/commands/block.d.ts +3 -0
  15. package/dist/commands/block.d.ts.map +1 -0
  16. package/dist/commands/block.js +33 -0
  17. package/dist/commands/block.js.map +1 -0
  18. package/dist/commands/gate.d.ts +3 -0
  19. package/dist/commands/gate.d.ts.map +1 -0
  20. package/dist/commands/gate.js +31 -0
  21. package/dist/commands/gate.js.map +1 -0
  22. package/dist/commands/help.d.ts +3 -0
  23. package/dist/commands/help.d.ts.map +1 -0
  24. package/dist/commands/help.js +35 -0
  25. package/dist/commands/help.js.map +1 -0
  26. package/dist/commands/hooks.d.ts +3 -0
  27. package/dist/commands/hooks.d.ts.map +1 -0
  28. package/dist/commands/hooks.js +40 -0
  29. package/dist/commands/hooks.js.map +1 -0
  30. package/dist/commands/issue.d.ts +3 -0
  31. package/dist/commands/issue.d.ts.map +1 -0
  32. package/dist/commands/issue.js +134 -0
  33. package/dist/commands/issue.js.map +1 -0
  34. package/dist/commands/journal.d.ts +3 -0
  35. package/dist/commands/journal.d.ts.map +1 -0
  36. package/dist/commands/journal.js +78 -0
  37. package/dist/commands/journal.js.map +1 -0
  38. package/dist/commands/ops.d.ts +3 -0
  39. package/dist/commands/ops.d.ts.map +1 -0
  40. package/dist/commands/ops.js +39 -0
  41. package/dist/commands/ops.js.map +1 -0
  42. package/dist/commands/pr.d.ts +3 -0
  43. package/dist/commands/pr.d.ts.map +1 -0
  44. package/dist/commands/pr.js +136 -0
  45. package/dist/commands/pr.js.map +1 -0
  46. package/dist/commands/status.d.ts +3 -0
  47. package/dist/commands/status.d.ts.map +1 -0
  48. package/dist/commands/status.js +17 -0
  49. package/dist/commands/status.js.map +1 -0
  50. package/dist/commands/types.d.ts +17 -0
  51. package/dist/commands/types.d.ts.map +1 -0
  52. package/dist/commands/types.js +2 -0
  53. package/dist/commands/types.js.map +1 -0
  54. package/dist/commands/verify.d.ts +3 -0
  55. package/dist/commands/verify.d.ts.map +1 -0
  56. package/dist/commands/verify.js +20 -0
  57. package/dist/commands/verify.js.map +1 -0
  58. package/dist/commands/webhook.d.ts +3 -0
  59. package/dist/commands/webhook.d.ts.map +1 -0
  60. package/dist/commands/webhook.js +61 -0
  61. package/dist/commands/webhook.js.map +1 -0
  62. package/dist/git.d.ts +5 -0
  63. package/dist/git.d.ts.map +1 -0
  64. package/dist/git.js +35 -0
  65. package/dist/git.js.map +1 -0
  66. package/dist/run.d.ts +7 -0
  67. package/dist/run.d.ts.map +1 -0
  68. package/dist/run.js +83 -0
  69. package/dist/run.js.map +1 -0
  70. package/dist/time.d.ts +2 -0
  71. package/dist/time.d.ts.map +1 -0
  72. package/dist/time.js +16 -0
  73. package/dist/time.js.map +1 -0
  74. package/package.json +24 -0
  75. package/src/args.ts +87 -0
  76. package/src/bin/git-a5c.ts +14 -0
  77. package/src/commands/agent.ts +72 -0
  78. package/src/commands/block.ts +34 -0
  79. package/src/commands/gate.ts +32 -0
  80. package/src/commands/help.ts +38 -0
  81. package/src/commands/hooks.ts +40 -0
  82. package/src/commands/issue.ts +146 -0
  83. package/src/commands/journal.ts +75 -0
  84. package/src/commands/ops.ts +41 -0
  85. package/src/commands/pr.ts +147 -0
  86. package/src/commands/status.ts +18 -0
  87. package/src/commands/types.ts +20 -0
  88. package/src/commands/verify.ts +20 -0
  89. package/src/commands/webhook.ts +63 -0
  90. package/src/git.ts +38 -0
  91. package/src/run.ts +99 -0
  92. package/src/time.ts +16 -0
  93. package/test/_util.ts +88 -0
  94. package/test/cli.flow.integration.test.ts +86 -0
  95. package/test/cli.snapshots.test.ts +50 -0
  96. package/test/cli.webhook.integration.test.ts +62 -0
  97. package/test/cli.write.integration.test.ts +70 -0
  98. package/tsconfig.json +13 -0
  99. package/vitest.config.ts +20 -0
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { runCli } from "../src/run.js";
3
+ import { makeEmptyRepo } from "./_util.js";
4
+
5
+ describe("CLI real workflow (maturization)", () => {
6
+ it("can run a basic issue + pr workflow end-to-end", async () => {
7
+ const repo = await makeEmptyRepo("a5cforge-cli-flow-");
8
+ process.env.A5C_ACTOR = "alice";
9
+
10
+ // 1) Create issue
11
+ let out = "";
12
+ let code = await runCli(["issue", "new", "--repo", repo, "--id", "issue-flow-1", "--title", "Flow issue", "--commit"], {
13
+ stdout: (s) => (out += s),
14
+ stderr: () => {}
15
+ });
16
+ expect(code).toBe(0);
17
+ expect(out.trim()).toBe("issue-flow-1");
18
+
19
+ // 2) Add a comment
20
+ out = "";
21
+ code = await runCli(["issue", "comment", "issue-flow-1", "--repo", repo, "--comment-id", "c-flow-1", "-m", "hello", "--commit"], {
22
+ stdout: (s) => (out += s),
23
+ stderr: () => {}
24
+ });
25
+ expect(code).toBe(0);
26
+ expect(out.trim()).toBe("c-flow-1");
27
+
28
+ // 3) Needs-human gate
29
+ out = "";
30
+ code = await runCli(["gate", "needs-human", "issue-flow-1", "--repo", repo, "--topic", "review", "-m", "needs eyes", "--commit"], {
31
+ stdout: (s) => (out += s),
32
+ stderr: () => {}
33
+ });
34
+ expect(code).toBe(0);
35
+ expect(out).toContain(".collab/");
36
+
37
+ // 4) Claim the work (agent dispatch is what an agent would react to)
38
+ out = "";
39
+ code = await runCli(["agent", "dispatch", "--repo", repo, "--entity", "issue-flow-1", "--dispatch-id", "d-flow-1", "--task", "implement"], {
40
+ stdout: (s) => (out += s),
41
+ stderr: () => {}
42
+ });
43
+ expect(code).toBe(0);
44
+ expect(out).toContain(".collab/agents/events/");
45
+
46
+ // 5) Create PR request
47
+ out = "";
48
+ code = await runCli(["pr", "request", "--repo", repo, "--id", "pr-flow-1", "--base", "refs/heads/main", "--title", "Do the thing", "--commit"], {
49
+ stdout: (s) => (out += s),
50
+ stderr: () => {}
51
+ });
52
+ expect(code).toBe(0);
53
+ expect(out.trim()).toBe("pr-flow-1");
54
+
55
+ // 6) Sanity: status + show JSON reflect the repo
56
+ out = "";
57
+ code = await runCli(["status", "--repo", repo, "--json"], { stdout: (s) => (out += s), stderr: () => {} });
58
+ expect(code).toBe(0);
59
+ const st = JSON.parse(out);
60
+ expect(st).toMatchObject({ treeish: "HEAD", issues: 1, prs: 1 });
61
+
62
+ out = "";
63
+ code = await runCli(["issue", "show", "issue-flow-1", "--repo", repo, "--json"], { stdout: (s) => (out += s), stderr: () => {} });
64
+ expect(code).toBe(0);
65
+ const issue = JSON.parse(out);
66
+ expect(issue.issueId).toBe("issue-flow-1");
67
+ expect(issue.comments.map((c: any) => c.commentId)).toContain("c-flow-1");
68
+
69
+ out = "";
70
+ code = await runCli(["pr", "show", "pr-flow-1", "--repo", repo, "--json"], { stdout: (s) => (out += s), stderr: () => {} });
71
+ expect(code).toBe(0);
72
+ const pr = JSON.parse(out);
73
+ expect(pr).toMatchObject({ prKey: "pr-flow-1", kind: "request" });
74
+
75
+ // 7) Journal includes the new events
76
+ process.env.A5C_NOW_ISO = new Date().toISOString();
77
+ out = "";
78
+ code = await runCli(["journal", "--repo", repo, "--since", "2h", "--types", "issue.*,comment.*,pr.*"], { stdout: (s) => (out += s), stderr: () => {} });
79
+ expect(code).toBe(0);
80
+ expect(out).toContain("issue.event.created");
81
+ expect(out).toContain("comment.created");
82
+ expect(out).toContain("pr.request.created");
83
+ }, 20_000);
84
+ });
85
+
86
+
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { runCli } from "../src/run.js";
3
+ import { makeRepoFromFixture } from "./_util.js";
4
+
5
+ describe("CLI (Phase 4)", () => {
6
+ it("status (repo-basic)", async () => {
7
+ const repo = await makeRepoFromFixture("repo-basic");
8
+ let out = "";
9
+ const code = await runCli(["status", "--repo", repo], { stdout: (s) => (out += s), stderr: () => {} });
10
+ expect(code).toBe(0);
11
+ expect(out).toBe(["treeish: HEAD", "issues: 2", "prs: 2", ""].join("\n"));
12
+ });
13
+
14
+ it("issue list (repo-basic)", async () => {
15
+ const repo = await makeRepoFromFixture("repo-basic");
16
+ let out = "";
17
+ const code = await runCli(["issue", "list", "--repo", repo], { stdout: (s) => (out += s), stderr: () => {} });
18
+ expect(code).toBe(0);
19
+ expect(out).toBe(["issue-1", "issue-2", ""].join("\n"));
20
+ });
21
+
22
+ it("pr show --json (repo-basic)", async () => {
23
+ const repo = await makeRepoFromFixture("repo-basic");
24
+ let out = "";
25
+ const code = await runCli(["pr", "show", "pr-2", "--json", "--repo", repo], { stdout: (s) => (out += s), stderr: () => {} });
26
+ expect(code).toBe(0);
27
+ const pr = JSON.parse(out);
28
+ expect(pr).toMatchObject({
29
+ prKey: "pr-2",
30
+ kind: "request",
31
+ baseRef: "refs/heads/main"
32
+ });
33
+ expect(pr.events).toHaveLength(2);
34
+ });
35
+
36
+ it("journal filters by --since and --types", async () => {
37
+ const repo = await makeRepoFromFixture("repo-basic");
38
+ process.env.A5C_NOW_ISO = "2025-12-19T14:50:00Z";
39
+ let out = "";
40
+ const code = await runCli(
41
+ ["journal", "--repo", repo, "--since", "20m", "--types", "pr.*"],
42
+ { stdout: (s) => (out += s), stderr: () => {} }
43
+ );
44
+ expect(code).toBe(0);
45
+ // Should include pr-related events only, newest first.
46
+ expect(out).toContain("pr.event.created");
47
+ });
48
+ });
49
+
50
+
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ import { runCli } from "../src/run.js";
5
+ import { makeEmptyRepo, run, listenOnce } from "./_util.js";
6
+
7
+ async function makeRepoWithWebhooks(): Promise<string> {
8
+ const dir = await makeEmptyRepo("a5cforge-cli-webhook-");
9
+ await fs.mkdir(path.join(dir, ".collab"), { recursive: true });
10
+ await fs.writeFile(
11
+ path.join(dir, ".collab", "webhooks.json"),
12
+ JSON.stringify(
13
+ {
14
+ schema: "a5cforge/v1",
15
+ endpoints: [{ id: "e1", url: "https://example.invalid/webhook", events: ["git.ref.updated"], enabled: true }]
16
+ },
17
+ null,
18
+ 2
19
+ ) + "\n",
20
+ "utf8"
21
+ );
22
+ await run("git", ["add", "-A"], dir);
23
+ await run("git", ["-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-q", "-m", "add webhooks"], dir);
24
+ return dir;
25
+ }
26
+
27
+ describe("CLI webhook helpers (Phase 8)", () => {
28
+ it("webhook status reads from treeish (HEAD)", async () => {
29
+ const repo = await makeRepoWithWebhooks();
30
+ let out = "";
31
+ const code = await runCli(["webhook", "status", "--repo", repo], { stdout: (s) => (out += s), stderr: () => {} });
32
+ expect(code).toBe(0);
33
+ expect(out).toContain("schema: a5cforge/v1");
34
+ expect(out).toContain("endpoints: 1");
35
+ expect(out).toContain("- e1: https://example.invalid/webhook (git.ref.updated)");
36
+ });
37
+
38
+ it("webhook test posts an envelope to the given url", async () => {
39
+ const repo = await makeRepoWithWebhooks();
40
+ let got: any = null;
41
+ const rcv = await listenOnce((req, body) => {
42
+ expect(req.method).toBe("POST");
43
+ expect(String(req.headers["content-type"] ?? "")).toContain("application/json");
44
+ got = JSON.parse(body.toString("utf8"));
45
+ });
46
+
47
+ try {
48
+ let out = "";
49
+ const code = await runCli(["webhook", "test", "--repo", repo, "--url", `${rcv.url}/recv`, "--type", "git.ref.updated"], {
50
+ stdout: (s) => (out += s),
51
+ stderr: () => {}
52
+ });
53
+ expect(code).toBe(0);
54
+ expect(out).toContain("status: 200");
55
+ expect(got).toMatchObject({ schema: "a5cforge/v1", type: "git.ref.updated", source: { serverId: "cli" } });
56
+ } finally {
57
+ await rcv.close();
58
+ }
59
+ });
60
+ });
61
+
62
+
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ import { runCli } from "../src/run.js";
5
+ import { makeEmptyRepo, run } from "./_util.js";
6
+
7
+ describe("CLI write commands (Phase 5)", () => {
8
+ it("issue new --stage-only creates and stages a file", async () => {
9
+ const repo = await makeEmptyRepo();
10
+ let out = "";
11
+ const code = await runCli(["issue", "new", "--title", "T", "--repo", repo, "--stage-only"], {
12
+ stdout: (s) => (out += s),
13
+ stderr: () => {}
14
+ });
15
+ expect(code).toBe(0);
16
+ expect(out.trim()).toMatch(/^issue-/);
17
+ const st = (await run("git", ["status", "--porcelain"], repo)).stdout;
18
+ expect(st).toContain("A ");
19
+ expect(st).toContain(".collab/");
20
+ });
21
+
22
+ it("hooks install is idempotent", async () => {
23
+ const repo = await makeEmptyRepo();
24
+ let out1 = "";
25
+ const c1 = await runCli(["hooks", "install", "--repo", repo], { stdout: (s) => (out1 += s), stderr: () => {} });
26
+ expect(c1).toBe(0);
27
+ expect(out1.trim()).toBe("ok");
28
+
29
+ let out2 = "";
30
+ const c2 = await runCli(["hooks", "install", "--repo", repo], { stdout: (s) => (out2 += s), stderr: () => {} });
31
+ expect(c2).toBe(0);
32
+ expect(out2.trim()).toBe("ok");
33
+ });
34
+
35
+ it("hooks uninstall only removes managed hooks", async () => {
36
+ const repo = await makeEmptyRepo();
37
+ // Install managed hook first.
38
+ await runCli(["hooks", "install", "--repo", repo], { stdout: () => {}, stderr: () => {} });
39
+
40
+ // Create an unmanaged hook and ensure uninstall doesn't delete it.
41
+ const hooksDir = (await run("git", ["rev-parse", "--git-path", "hooks"], repo)).stdout.trim();
42
+ await fs.mkdir(hooksDir, { recursive: true });
43
+ const unmanaged = path.join(hooksDir, "pre-commit");
44
+ await fs.writeFile(unmanaged, "#!/bin/sh\necho unmanaged\n", "utf8");
45
+
46
+ let out = "";
47
+ const code = await runCli(["hooks", "uninstall", "--repo", repo], { stdout: (s) => (out += s), stderr: () => {} });
48
+ expect(code).toBe(0);
49
+ expect(out.trim()).toBe("ok");
50
+
51
+ const still = await fs.readFile(unmanaged, "utf8");
52
+ expect(still).toContain("unmanaged");
53
+ });
54
+
55
+ it("agent dispatch writes agent.dispatch.created event", async () => {
56
+ const repo = await makeEmptyRepo();
57
+ process.env.A5C_ACTOR = "agent1";
58
+ let out = "";
59
+ const code = await runCli(["agent", "dispatch", "--repo", repo, "--entity", "issue-1", "--stage-only"], {
60
+ stdout: (s) => (out += s),
61
+ stderr: () => {}
62
+ });
63
+ expect(code).toBe(0);
64
+ expect(out).toContain(".collab/agents/events/");
65
+ const st = (await run("git", ["status", "--porcelain"], repo)).stdout;
66
+ expect(st).toContain(".collab/agents/events/");
67
+ });
68
+ });
69
+
70
+
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true
9
+ },
10
+ "include": ["src"]
11
+ }
12
+
13
+
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ coverage: {
6
+ reportsDirectory: "coverage",
7
+ reporter: ["text", "lcov", "json-summary"],
8
+ include: ["src/**/*.ts"],
9
+ exclude: ["src/bin/**", "test/**", "**/*.d.ts"],
10
+ thresholds: {
11
+ statements: 55,
12
+ branches: 45,
13
+ functions: 65,
14
+ lines: 55
15
+ }
16
+ }
17
+ }
18
+ });
19
+
20
+