@markjaquith/agency 0.5.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/cli.ts +569 -0
  4. package/index.ts +1 -0
  5. package/package.json +65 -0
  6. package/src/commands/base.test.ts +198 -0
  7. package/src/commands/base.ts +198 -0
  8. package/src/commands/clean.test.ts +299 -0
  9. package/src/commands/clean.ts +320 -0
  10. package/src/commands/emit.test.ts +412 -0
  11. package/src/commands/emit.ts +521 -0
  12. package/src/commands/emitted.test.ts +226 -0
  13. package/src/commands/emitted.ts +57 -0
  14. package/src/commands/init.test.ts +311 -0
  15. package/src/commands/init.ts +140 -0
  16. package/src/commands/merge.test.ts +365 -0
  17. package/src/commands/merge.ts +253 -0
  18. package/src/commands/pull.test.ts +385 -0
  19. package/src/commands/pull.ts +205 -0
  20. package/src/commands/push.test.ts +394 -0
  21. package/src/commands/push.ts +346 -0
  22. package/src/commands/save.test.ts +247 -0
  23. package/src/commands/save.ts +162 -0
  24. package/src/commands/source.test.ts +195 -0
  25. package/src/commands/source.ts +72 -0
  26. package/src/commands/status.test.ts +489 -0
  27. package/src/commands/status.ts +258 -0
  28. package/src/commands/switch.test.ts +194 -0
  29. package/src/commands/switch.ts +84 -0
  30. package/src/commands/task-branching.test.ts +334 -0
  31. package/src/commands/task-edit.test.ts +141 -0
  32. package/src/commands/task-main.test.ts +872 -0
  33. package/src/commands/task.ts +712 -0
  34. package/src/commands/tasks.test.ts +335 -0
  35. package/src/commands/tasks.ts +155 -0
  36. package/src/commands/template-delete.test.ts +178 -0
  37. package/src/commands/template-delete.ts +98 -0
  38. package/src/commands/template-list.test.ts +135 -0
  39. package/src/commands/template-list.ts +87 -0
  40. package/src/commands/template-view.test.ts +158 -0
  41. package/src/commands/template-view.ts +86 -0
  42. package/src/commands/template.test.ts +32 -0
  43. package/src/commands/template.ts +96 -0
  44. package/src/commands/use.test.ts +87 -0
  45. package/src/commands/use.ts +97 -0
  46. package/src/commands/work.test.ts +462 -0
  47. package/src/commands/work.ts +193 -0
  48. package/src/errors.ts +17 -0
  49. package/src/schemas.ts +33 -0
  50. package/src/services/AgencyMetadataService.ts +287 -0
  51. package/src/services/ClaudeService.test.ts +184 -0
  52. package/src/services/ClaudeService.ts +91 -0
  53. package/src/services/ConfigService.ts +115 -0
  54. package/src/services/FileSystemService.ts +222 -0
  55. package/src/services/GitService.ts +751 -0
  56. package/src/services/OpencodeService.ts +263 -0
  57. package/src/services/PromptService.ts +183 -0
  58. package/src/services/TemplateService.ts +75 -0
  59. package/src/test-utils.ts +362 -0
  60. package/src/types/native-exec.d.ts +8 -0
  61. package/src/types.ts +216 -0
  62. package/src/utils/colors.ts +178 -0
  63. package/src/utils/command.ts +17 -0
  64. package/src/utils/effect.ts +281 -0
  65. package/src/utils/exec.ts +48 -0
  66. package/src/utils/paths.ts +51 -0
  67. package/src/utils/pr-branch.test.ts +372 -0
  68. package/src/utils/pr-branch.ts +473 -0
  69. package/src/utils/process.ts +110 -0
  70. package/src/utils/spinner.ts +82 -0
  71. package/templates/AGENCY.md +20 -0
  72. package/templates/AGENTS.md +11 -0
  73. package/templates/CLAUDE.md +3 -0
  74. package/templates/TASK.md +5 -0
  75. package/templates/opencode.json +4 -0
@@ -0,0 +1,489 @@
1
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test"
2
+ import { join } from "path"
3
+ import { status } from "./status"
4
+ import {
5
+ createTempDir,
6
+ cleanupTempDir,
7
+ initGitRepo,
8
+ getCurrentBranch,
9
+ createCommit,
10
+ initAgency,
11
+ runTestEffect,
12
+ } from "../test-utils"
13
+ import { writeAgencyMetadata } from "../types"
14
+
15
+ async function createBranch(cwd: string, branchName: string): Promise<void> {
16
+ await Bun.spawn(["git", "checkout", "-b", branchName], {
17
+ cwd,
18
+ stdout: "pipe",
19
+ stderr: "pipe",
20
+ }).exited
21
+ }
22
+
23
+ async function checkoutBranch(cwd: string, branchName: string): Promise<void> {
24
+ await Bun.spawn(["git", "checkout", branchName], {
25
+ cwd,
26
+ stdout: "pipe",
27
+ stderr: "pipe",
28
+ }).exited
29
+ }
30
+
31
+ describe("status command", () => {
32
+ let tempDir: string
33
+ let originalCwd: string
34
+
35
+ beforeEach(async () => {
36
+ tempDir = await createTempDir()
37
+ originalCwd = process.cwd()
38
+ process.chdir(tempDir)
39
+
40
+ // Set config path to non-existent file to use defaults
41
+ process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
42
+
43
+ // Initialize git repo
44
+ await initGitRepo(tempDir)
45
+ await createCommit(tempDir, "Initial commit")
46
+
47
+ // Rename to main if needed
48
+ const currentBranch = await getCurrentBranch(tempDir)
49
+ if (currentBranch === "master") {
50
+ await Bun.spawn(["git", "branch", "-m", "main"], {
51
+ cwd: tempDir,
52
+ stdout: "pipe",
53
+ stderr: "pipe",
54
+ }).exited
55
+ }
56
+ })
57
+
58
+ afterEach(async () => {
59
+ process.chdir(originalCwd)
60
+ delete process.env.AGENCY_CONFIG_PATH
61
+ await cleanupTempDir(tempDir)
62
+ })
63
+
64
+ describe("basic functionality", () => {
65
+ test("shows not initialized when agency.json is missing", async () => {
66
+ let output = ""
67
+ const originalLog = console.log
68
+ console.log = (msg: string) => {
69
+ output += msg + "\n"
70
+ }
71
+
72
+ await runTestEffect(status({}))
73
+
74
+ console.log = originalLog
75
+ expect(output).toContain("Not initialized")
76
+ expect(output).toContain("agency task")
77
+ })
78
+
79
+ test("shows initialized when agency.json exists", async () => {
80
+ // Initialize agency
81
+ await initAgency(tempDir, "test-template")
82
+
83
+ // Create feature branch with agency.json
84
+ await createBranch(tempDir, "feature")
85
+ await writeAgencyMetadata(tempDir, {
86
+ version: 1,
87
+ injectedFiles: ["AGENTS.md"],
88
+ template: "test-template",
89
+ createdAt: new Date().toISOString(),
90
+ } as any)
91
+
92
+ let output = ""
93
+ const originalLog = console.log
94
+ console.log = (msg: string) => {
95
+ output += msg + "\n"
96
+ }
97
+
98
+ await runTestEffect(status({}))
99
+
100
+ console.log = originalLog
101
+ expect(output).toContain("Current branch:")
102
+ expect(output).toContain("Branch type: Source branch")
103
+ expect(output).toContain("Template:")
104
+ })
105
+
106
+ test("shows source branch type when on source branch", async () => {
107
+ await initAgency(tempDir, "test-template")
108
+ await createBranch(tempDir, "feature")
109
+ await writeAgencyMetadata(tempDir, {
110
+ version: 1,
111
+ injectedFiles: [],
112
+ template: "test-template",
113
+ createdAt: new Date().toISOString(),
114
+ } as any)
115
+
116
+ let output = ""
117
+ const originalLog = console.log
118
+ console.log = (msg: string) => {
119
+ output += msg + "\n"
120
+ }
121
+
122
+ await runTestEffect(status({}))
123
+
124
+ console.log = originalLog
125
+ expect(output).toContain("Branch type:")
126
+ expect(output).toContain("Source branch")
127
+ })
128
+
129
+ test("shows emit branch type when on emit branch", async () => {
130
+ await initAgency(tempDir, "test-template")
131
+
132
+ // Create source branch with agency.json
133
+ await createBranch(tempDir, "agency/feature")
134
+ await writeAgencyMetadata(tempDir, {
135
+ version: 1,
136
+ injectedFiles: [],
137
+ template: "test-template",
138
+ emitBranch: "feature",
139
+ createdAt: new Date().toISOString(),
140
+ } as any)
141
+ // Stage and commit agency.json
142
+ await Bun.spawn(["git", "add", "agency.json"], {
143
+ cwd: tempDir,
144
+ stdout: "pipe",
145
+ stderr: "pipe",
146
+ }).exited
147
+ await createCommit(tempDir, "Feature work")
148
+
149
+ // Create and switch to emit branch
150
+ await createBranch(tempDir, "feature")
151
+
152
+ let output = ""
153
+ const originalLog = console.log
154
+ console.log = (msg: string) => {
155
+ output += msg + "\n"
156
+ }
157
+
158
+ await runTestEffect(status({}))
159
+
160
+ console.log = originalLog
161
+ expect(output).toContain("Branch type:")
162
+ expect(output).toContain("Emit branch")
163
+ })
164
+
165
+ test("shows corresponding branch when it exists", async () => {
166
+ await initAgency(tempDir, "test-template")
167
+ await createBranch(tempDir, "agency/feature")
168
+ await writeAgencyMetadata(tempDir, {
169
+ version: 1,
170
+ injectedFiles: [],
171
+ template: "test-template",
172
+ emitBranch: "feature",
173
+ createdAt: new Date().toISOString(),
174
+ } as any)
175
+
176
+ // Stage and commit agency.json
177
+ await Bun.spawn(["git", "add", "agency.json"], {
178
+ cwd: tempDir,
179
+ stdout: "pipe",
180
+ stderr: "pipe",
181
+ }).exited
182
+ await createCommit(tempDir, "Set up branch")
183
+
184
+ // Create emit branch
185
+ await createBranch(tempDir, "feature")
186
+ await checkoutBranch(tempDir, "agency/feature")
187
+
188
+ let output = ""
189
+ const originalLog = console.log
190
+ console.log = (msg: string) => {
191
+ output += msg + "\n"
192
+ }
193
+
194
+ await runTestEffect(status({}))
195
+
196
+ console.log = originalLog
197
+ expect(output).toContain("Emit branch: ")
198
+ expect(output).toContain("feature")
199
+ })
200
+
201
+ test("shows backpack files", async () => {
202
+ await initAgency(tempDir, "test-template")
203
+ await createBranch(tempDir, "feature")
204
+ await writeAgencyMetadata(tempDir, {
205
+ version: 1,
206
+ injectedFiles: ["AGENTS.md", "opencode.json"],
207
+ template: "test-template",
208
+ createdAt: new Date().toISOString(),
209
+ } as any)
210
+
211
+ let output = ""
212
+ const originalLog = console.log
213
+ console.log = (msg: string) => {
214
+ output += msg + "\n"
215
+ }
216
+
217
+ await runTestEffect(status({}))
218
+
219
+ console.log = originalLog
220
+ expect(output).toContain("Backpack:")
221
+ // Base backpack files (TASK.md, AGENCY.md, agency.json) + injected files (AGENTS.md, opencode.json)
222
+ expect(output).toContain("TASK.md")
223
+ expect(output).toContain("AGENCY.md")
224
+ expect(output).toContain("agency.json")
225
+ expect(output).toContain("AGENTS.md")
226
+ expect(output).toContain("opencode.json")
227
+ })
228
+
229
+ test("shows base branch when set", async () => {
230
+ await initAgency(tempDir, "test-template")
231
+ await createBranch(tempDir, "feature")
232
+ await writeAgencyMetadata(tempDir, {
233
+ version: 1,
234
+ injectedFiles: [],
235
+ template: "test-template",
236
+ baseBranch: "main",
237
+ createdAt: new Date().toISOString(),
238
+ } as any)
239
+
240
+ let output = ""
241
+ const originalLog = console.log
242
+ console.log = (msg: string) => {
243
+ output += msg + "\n"
244
+ }
245
+
246
+ await runTestEffect(status({}))
247
+
248
+ console.log = originalLog
249
+ expect(output).toContain("Base branch:")
250
+ expect(output).toContain("main")
251
+ })
252
+
253
+ test("shows template name", async () => {
254
+ await initAgency(tempDir, "my-custom-template")
255
+ await createBranch(tempDir, "feature")
256
+ await writeAgencyMetadata(tempDir, {
257
+ version: 1,
258
+ injectedFiles: [],
259
+ template: "my-custom-template",
260
+ createdAt: new Date().toISOString(),
261
+ } as any)
262
+
263
+ let output = ""
264
+ const originalLog = console.log
265
+ console.log = (msg: string) => {
266
+ output += msg + "\n"
267
+ }
268
+
269
+ await runTestEffect(status({}))
270
+
271
+ console.log = originalLog
272
+ expect(output).toContain("Template:")
273
+ expect(output).toContain("my-custom-template")
274
+ })
275
+ })
276
+
277
+ describe("JSON output", () => {
278
+ test("outputs valid JSON with --json flag", async () => {
279
+ await initAgency(tempDir, "test-template")
280
+ await createBranch(tempDir, "feature")
281
+ await writeAgencyMetadata(tempDir, {
282
+ version: 1,
283
+ injectedFiles: ["AGENTS.md"],
284
+ baseBranch: "main",
285
+ template: "test-template",
286
+ createdAt: new Date().toISOString(),
287
+ } as any)
288
+
289
+ let output = ""
290
+ const originalLog = console.log
291
+ console.log = (msg: string) => {
292
+ output += msg + "\n"
293
+ }
294
+
295
+ await runTestEffect(status({ json: true }))
296
+
297
+ console.log = originalLog
298
+
299
+ const data = JSON.parse(output.trim())
300
+ expect(data.initialized).toBe(true)
301
+ expect(data.branchType).toBe("source")
302
+ expect(data.currentBranch).toBe("feature")
303
+ expect(data.template).toBe("test-template")
304
+ // managedFiles includes base backpack files + injected files
305
+ expect(data.managedFiles).toContain("TASK.md")
306
+ expect(data.managedFiles).toContain("AGENCY.md")
307
+ expect(data.managedFiles).toContain("agency.json")
308
+ expect(data.managedFiles).toContain("AGENTS.md")
309
+ expect(data.baseBranch).toBe("main")
310
+ })
311
+
312
+ test("JSON output contains all expected fields", async () => {
313
+ await initAgency(tempDir, "test-template")
314
+ await createBranch(tempDir, "feature")
315
+ await writeAgencyMetadata(tempDir, {
316
+ version: 1,
317
+ injectedFiles: [],
318
+ template: "test-template",
319
+ createdAt: new Date().toISOString(),
320
+ } as any)
321
+
322
+ let output = ""
323
+ const originalLog = console.log
324
+ console.log = (msg: string) => {
325
+ output += msg + "\n"
326
+ }
327
+
328
+ await runTestEffect(status({ json: true }))
329
+
330
+ console.log = originalLog
331
+
332
+ const data = JSON.parse(output.trim())
333
+ expect(data).toHaveProperty("initialized")
334
+ expect(data).toHaveProperty("branchType")
335
+ expect(data).toHaveProperty("currentBranch")
336
+ expect(data).toHaveProperty("sourceBranch")
337
+ expect(data).toHaveProperty("emitBranch")
338
+ expect(data).toHaveProperty("correspondingBranchExists")
339
+ expect(data).toHaveProperty("template")
340
+ expect(data).toHaveProperty("managedFiles")
341
+ expect(data).toHaveProperty("baseBranch")
342
+ expect(data).toHaveProperty("createdAt")
343
+ })
344
+
345
+ test("JSON output shows not initialized state", async () => {
346
+ let output = ""
347
+ const originalLog = console.log
348
+ console.log = (msg: string) => {
349
+ output += msg + "\n"
350
+ }
351
+
352
+ await runTestEffect(status({ json: true }))
353
+
354
+ console.log = originalLog
355
+
356
+ const data = JSON.parse(output.trim())
357
+ expect(data.initialized).toBe(false)
358
+ expect(data.branchType).toBe("neither")
359
+ })
360
+ })
361
+
362
+ describe("error handling", () => {
363
+ test("throws error when not in a git repository", async () => {
364
+ const nonGitDir = await createTempDir()
365
+ process.chdir(nonGitDir)
366
+
367
+ await expect(runTestEffect(status({}))).rejects.toThrow(
368
+ "Not in a git repository",
369
+ )
370
+
371
+ await cleanupTempDir(nonGitDir)
372
+ })
373
+ })
374
+
375
+ describe("silent mode", () => {
376
+ test("silent flag suppresses output", async () => {
377
+ const originalLog = console.log
378
+ let logCalled = false
379
+ console.log = () => {
380
+ logCalled = true
381
+ }
382
+
383
+ await runTestEffect(status({ silent: true }))
384
+
385
+ console.log = originalLog
386
+ expect(logCalled).toBe(false)
387
+ })
388
+ })
389
+
390
+ describe("emit branch behavior", () => {
391
+ test("reads metadata from source branch when on emit branch without agency.json on disk", async () => {
392
+ await initAgency(tempDir, "test-template")
393
+
394
+ // Create source branch (agency/feature) with agency.json committed
395
+ await createBranch(tempDir, "agency/feature")
396
+ await writeAgencyMetadata(tempDir, {
397
+ version: 1,
398
+ injectedFiles: ["AGENTS.md", "opencode.json"],
399
+ template: "test-template",
400
+ baseBranch: "main",
401
+ emitBranch: "feature",
402
+ createdAt: new Date().toISOString(),
403
+ } as any)
404
+ // Stage and commit agency.json
405
+ await Bun.spawn(["git", "add", "agency.json"], {
406
+ cwd: tempDir,
407
+ stdout: "pipe",
408
+ stderr: "pipe",
409
+ }).exited
410
+ await createCommit(tempDir, "Feature work")
411
+
412
+ // Create emit branch and remove agency.json from working tree
413
+ await createBranch(tempDir, "feature")
414
+ await Bun.spawn(["rm", "agency.json"], {
415
+ cwd: tempDir,
416
+ stdout: "pipe",
417
+ stderr: "pipe",
418
+ }).exited
419
+
420
+ let output = ""
421
+ const originalLog = console.log
422
+ console.log = (msg: string) => {
423
+ output += msg + "\n"
424
+ }
425
+
426
+ await runTestEffect(status({ json: true }))
427
+
428
+ console.log = originalLog
429
+
430
+ const data = JSON.parse(output.trim())
431
+ // Verify that we're recognized as on an emit branch
432
+ expect(data.branchType).toBe("emit")
433
+ expect(data.initialized).toBe(true)
434
+ // Verify metadata is correctly read from source branch
435
+ expect(data.baseBranch).toBe("main")
436
+ expect(data.managedFiles).toContain("AGENTS.md")
437
+ expect(data.managedFiles).toContain("opencode.json")
438
+ expect(data.sourceBranch).toBe("agency/feature")
439
+ })
440
+
441
+ test("shows correct backpack when on emit branch", async () => {
442
+ await initAgency(tempDir, "test-template")
443
+
444
+ // Create source branch (agency/feature) with agency.json committed
445
+ await createBranch(tempDir, "agency/feature")
446
+ await writeAgencyMetadata(tempDir, {
447
+ version: 1,
448
+ injectedFiles: ["AGENTS.md"],
449
+ template: "test-template",
450
+ emitBranch: "feature",
451
+ createdAt: new Date().toISOString(),
452
+ } as any)
453
+ // Stage and commit agency.json
454
+ await Bun.spawn(["git", "add", "agency.json"], {
455
+ cwd: tempDir,
456
+ stdout: "pipe",
457
+ stderr: "pipe",
458
+ }).exited
459
+ await createCommit(tempDir, "Feature work")
460
+
461
+ // Create emit branch and remove agency.json from disk to simulate clean emit branch
462
+ await createBranch(tempDir, "feature")
463
+ await Bun.spawn(["rm", "agency.json"], {
464
+ cwd: tempDir,
465
+ stdout: "pipe",
466
+ stderr: "pipe",
467
+ }).exited
468
+
469
+ let output = ""
470
+ const originalLog = console.log
471
+ console.log = (msg: string) => {
472
+ output += msg + "\n"
473
+ }
474
+
475
+ await runTestEffect(status({ json: true }))
476
+
477
+ console.log = originalLog
478
+
479
+ const data = JSON.parse(output.trim())
480
+ expect(data.branchType).toBe("emit")
481
+ expect(data.initialized).toBe(true)
482
+ // Backpack should include injected files from source branch
483
+ expect(data.managedFiles).toContain("AGENTS.md")
484
+ expect(data.managedFiles).toContain("TASK.md")
485
+ expect(data.managedFiles).toContain("AGENCY.md")
486
+ expect(data.managedFiles).toContain("agency.json")
487
+ })
488
+ })
489
+ })