@markjaquith/agency 1.11.0 → 2.0.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 (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -415
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -0
  6. package/src/cli.test.ts +290 -0
  7. package/src/commands/description.test.ts +103 -0
  8. package/src/commands/epic.test.ts +103 -0
  9. package/src/commands/epic.ts +101 -0
  10. package/src/commands/init.test.ts +42 -290
  11. package/src/commands/init.ts +15 -123
  12. package/src/commands/phase.ts +120 -0
  13. package/src/commands/pr.test.ts +19 -170
  14. package/src/commands/pr.ts +23 -179
  15. package/src/commands/repo.test.ts +80 -0
  16. package/src/commands/repo.ts +83 -0
  17. package/src/commands/status.test.ts +35 -477
  18. package/src/commands/status.ts +24 -213
  19. package/src/commands/task-phase.test.ts +206 -0
  20. package/src/commands/task.ts +83 -1285
  21. package/src/commands/validate.test.ts +91 -0
  22. package/src/commands/validate.ts +50 -0
  23. package/src/commands/work.test.ts +172 -419
  24. package/src/commands/work.ts +47 -166
  25. package/src/services/EpicService.test.ts +71 -0
  26. package/src/services/EpicService.ts +135 -0
  27. package/src/services/FileSystemService.ts +61 -92
  28. package/src/services/PhaseService.ts +323 -0
  29. package/src/services/PullRequestService.test.ts +386 -0
  30. package/src/services/PullRequestService.ts +143 -0
  31. package/src/services/RepositoryService.test.ts +106 -0
  32. package/src/services/RepositoryService.ts +159 -0
  33. package/src/services/TaskPhaseService.test.ts +217 -0
  34. package/src/services/TaskService.ts +192 -0
  35. package/src/services/WorkbaseService.test.ts +446 -0
  36. package/src/services/WorkbaseService.ts +493 -0
  37. package/src/services/WorktreeService.test.ts +599 -0
  38. package/src/services/WorktreeService.ts +357 -0
  39. package/src/test-utils.ts +41 -438
  40. package/src/types.ts +1 -215
  41. package/src/utils/command.ts +1 -5
  42. package/src/utils/effect.test.ts +24 -0
  43. package/src/utils/effect.ts +3 -273
  44. package/src/utils/process.test.ts +0 -15
  45. package/src/utils/process.ts +1 -60
  46. package/src/workbase/frontmatter.test.ts +50 -0
  47. package/src/workbase/frontmatter.ts +77 -0
  48. package/src/workbase/repository-reference.test.ts +25 -0
  49. package/src/workbase/repository-reference.ts +18 -0
  50. package/src/workbase/schemas.test.ts +146 -0
  51. package/src/workbase/schemas.ts +78 -0
  52. package/src/workbase/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -518
  73. package/src/commands/push.ts +0 -434
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1183
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. package/templates/opencode.json +0 -4
@@ -0,0 +1,103 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { mkdir } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
5
+ import { epic } from "./epic"
6
+ import { phase } from "./phase"
7
+ import { task } from "./task"
8
+
9
+ describe("create command descriptions", () => {
10
+ let root: string
11
+
12
+ beforeEach(async () => {
13
+ root = await createTempDir()
14
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
15
+ await mkdir(join(root, "repos/agency"), { recursive: true })
16
+ })
17
+
18
+ afterEach(async () => cleanupTempDir(root))
19
+
20
+ test("persists and exposes descriptions for every body-of-work shape", async () => {
21
+ await runTestEffect(
22
+ epic({
23
+ subcommand: "create",
24
+ args: ["rollout"],
25
+ ticketUrl: "https://example.com/epic",
26
+ description: "Coordinate the rollout.",
27
+ repos: ["agency:main"],
28
+ cwd: root,
29
+ silent: true,
30
+ }),
31
+ )
32
+ await runTestEffect(
33
+ task({
34
+ subcommand: "create",
35
+ args: ["delivery"],
36
+ ticketUrl: "https://example.com/task",
37
+ description: "Deliver the rollout.",
38
+ multiPhase: true,
39
+ cwd: root,
40
+ silent: true,
41
+ }),
42
+ )
43
+ await runTestEffect(
44
+ phase({
45
+ subcommand: "create",
46
+ args: ["delivery", "contract"],
47
+ description: "Introduce the service contract.",
48
+ repo: "agency",
49
+ branch: "task/contract",
50
+ base: "main",
51
+ cwd: root,
52
+ silent: true,
53
+ }),
54
+ )
55
+
56
+ expect(
57
+ await Bun.file(join(root, "epics/rollout/EPIC.md")).text(),
58
+ ).toContain("description: Coordinate the rollout.")
59
+ expect(
60
+ await Bun.file(join(root, "tasks/delivery/TASK.md")).text(),
61
+ ).toContain("description: Deliver the rollout.")
62
+ expect(
63
+ await Bun.file(
64
+ join(root, "tasks/delivery/phases/contract/PHASE.md"),
65
+ ).text(),
66
+ ).toContain("description: Introduce the service contract.")
67
+
68
+ const output: string[] = []
69
+ const originalLog = console.log
70
+ console.log = (message?: unknown) => output.push(String(message))
71
+ try {
72
+ await runTestEffect(
73
+ epic({ subcommand: "list", args: [], cwd: root, json: true }),
74
+ )
75
+ await runTestEffect(
76
+ task({
77
+ subcommand: "show",
78
+ args: ["delivery"],
79
+ cwd: root,
80
+ json: true,
81
+ }),
82
+ )
83
+ await runTestEffect(
84
+ phase({
85
+ subcommand: "show",
86
+ args: ["delivery", "contract"],
87
+ cwd: root,
88
+ json: true,
89
+ }),
90
+ )
91
+ } finally {
92
+ console.log = originalLog
93
+ }
94
+
95
+ expect(JSON.parse(output[0]!)[0].data.description).toBe(
96
+ "Coordinate the rollout.",
97
+ )
98
+ expect(JSON.parse(output[1]!).data.description).toBe("Deliver the rollout.")
99
+ expect(JSON.parse(output[2]!).data.description).toBe(
100
+ "Introduce the service contract.",
101
+ )
102
+ })
103
+ })
@@ -0,0 +1,103 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { mkdir } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import {
5
+ captureLogs,
6
+ cleanupTempDir,
7
+ createTempDir,
8
+ runTestEffect,
9
+ } from "../test-utils"
10
+ import { epic } from "./epic"
11
+
12
+ describe("epic command", () => {
13
+ let root: string
14
+
15
+ beforeEach(async () => {
16
+ root = await createTempDir()
17
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
18
+ await mkdir(join(root, "repos/agency"), { recursive: true })
19
+ })
20
+
21
+ afterEach(async () => cleanupTempDir(root))
22
+
23
+ test("requires create arguments", async () => {
24
+ await expect(
25
+ runTestEffect(epic({ subcommand: "create", args: [], silent: true })),
26
+ ).rejects.toThrow("Usage: agency epic create")
27
+ })
28
+
29
+ test("outputs the created epic as JSON", async () => {
30
+ const logs = await captureLogs(() =>
31
+ runTestEffect(
32
+ epic({
33
+ subcommand: "create",
34
+ args: ["example"],
35
+ ticketUrl: "https://example.com/epic",
36
+ repos: ["agency:main"],
37
+ cwd: root,
38
+ json: true,
39
+ }),
40
+ ),
41
+ )
42
+
43
+ expect(JSON.parse(logs[0]!)).toEqual({
44
+ id: "example",
45
+ path: join(root, "epics/example/EPIC.md"),
46
+ data: {
47
+ ticketUrl: "https://example.com/epic",
48
+ repos: [{ repo: "agency", ref: "main" }],
49
+ tasks: [],
50
+ },
51
+ })
52
+ })
53
+
54
+ test("lists and shows stable epic JSON records", async () => {
55
+ await runTestEffect(
56
+ epic({
57
+ subcommand: "create",
58
+ args: ["example"],
59
+ ticketUrl: "https://example.com/epic",
60
+ repos: ["agency:main"],
61
+ cwd: root,
62
+ silent: true,
63
+ }),
64
+ )
65
+
66
+ const listLogs = await captureLogs(() =>
67
+ runTestEffect(
68
+ epic({ subcommand: "list", args: [], cwd: root, json: true }),
69
+ ),
70
+ )
71
+ expect(JSON.parse(listLogs[0]!)).toEqual([
72
+ {
73
+ id: "example",
74
+ path: join(root, "epics/example/EPIC.md"),
75
+ data: {
76
+ ticketUrl: "https://example.com/epic",
77
+ repos: [{ repo: "agency", ref: "main" }],
78
+ tasks: [],
79
+ },
80
+ },
81
+ ])
82
+
83
+ const showLogs = await captureLogs(() =>
84
+ runTestEffect(
85
+ epic({
86
+ subcommand: "show",
87
+ args: ["example"],
88
+ cwd: root,
89
+ json: true,
90
+ }),
91
+ ),
92
+ )
93
+ expect(JSON.parse(showLogs[0]!)).toEqual({
94
+ id: "example",
95
+ path: join(root, "epics/example/EPIC.md"),
96
+ data: {
97
+ ticketUrl: "https://example.com/epic",
98
+ repos: [{ repo: "agency", ref: "main" }],
99
+ tasks: [],
100
+ },
101
+ })
102
+ })
103
+ })
@@ -0,0 +1,101 @@
1
+ import { Effect } from "effect"
2
+ import type { BaseCommandOptions } from "../utils/command"
3
+ import { EpicService } from "../services/EpicService"
4
+ import { createLoggers } from "../utils/effect"
5
+ import { parseRepositoryReferences } from "../workbase/repository-reference"
6
+
7
+ interface EpicOptions extends BaseCommandOptions {
8
+ readonly subcommand?: string
9
+ readonly args: readonly string[]
10
+ readonly ticketUrl?: string
11
+ readonly description?: string
12
+ readonly repos?: readonly string[]
13
+ readonly json?: boolean
14
+ }
15
+
16
+ export const epic = (options: EpicOptions) =>
17
+ Effect.gen(function* () {
18
+ const epics = yield* EpicService
19
+ const { log } = createLoggers(options)
20
+ const cwd = options.cwd ?? process.cwd()
21
+
22
+ switch (options.subcommand) {
23
+ case "create": {
24
+ const id = options.args[0]
25
+ if (!id || !options.ticketUrl || !options.repos?.length) {
26
+ return yield* Effect.fail(
27
+ new Error(
28
+ "Usage: agency epic create <id> --ticket-url <url> --repo <alias>:<ref>",
29
+ ),
30
+ )
31
+ }
32
+ const record = yield* epics.create(
33
+ id,
34
+ options.ticketUrl,
35
+ parseRepositoryReferences(options.repos),
36
+ cwd,
37
+ options.description,
38
+ )
39
+ const { content: _, ...output } = record
40
+ log(
41
+ options.json
42
+ ? JSON.stringify(output, null, 2)
43
+ : `Created epic '${record.id}'`,
44
+ )
45
+ return
46
+ }
47
+
48
+ case "list": {
49
+ const records = yield* epics.list(cwd)
50
+ if (options.json) {
51
+ log(
52
+ JSON.stringify(
53
+ records.map(({ content: _, ...record }) => record),
54
+ null,
55
+ 2,
56
+ ),
57
+ )
58
+ } else {
59
+ for (const record of records) log(record.id)
60
+ }
61
+ return
62
+ }
63
+
64
+ case "show": {
65
+ const id = options.args[0]
66
+ if (!id) return yield* Effect.fail(new Error("Epic ID is required"))
67
+ const record = yield* epics.show(id, cwd)
68
+ const { content: _, ...output } = record
69
+ log(
70
+ options.json
71
+ ? JSON.stringify(output, null, 2)
72
+ : record.content.trimEnd(),
73
+ )
74
+ return
75
+ }
76
+
77
+ default:
78
+ return yield* Effect.fail(
79
+ new Error(
80
+ "Subcommand is required. Available subcommands: create, list, show",
81
+ ),
82
+ )
83
+ }
84
+ })
85
+
86
+ export const help = `
87
+ Usage: agency epic <subcommand>
88
+
89
+ Subcommands:
90
+ create <id> Create an epic
91
+ list List epics
92
+ show <id> Show an epic
93
+
94
+ Create options:
95
+ --ticket-url <url> External ticket URL
96
+ --description <text> Short description of the epic
97
+ --repo <alias>:<ref> Read-only repository reference; repeatable
98
+
99
+ Options:
100
+ --json Output results as JSON
101
+ `
@@ -1,311 +1,63 @@
1
- import { test, expect, describe } from "bun:test"
2
- import { mkdtemp, rm } from "fs/promises"
3
- import { tmpdir } from "os"
4
- import { join } from "path"
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { stat } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import {
5
+ captureLogs,
6
+ cleanupTempDir,
7
+ createTempDir,
8
+ runTestEffect,
9
+ } from "../test-utils"
5
10
  import { init } from "./init"
6
- import { initGitRepo, getGitConfig, runTestEffect } from "../test-utils"
7
11
 
8
12
  describe("init command", () => {
9
- test("initializes with template flag", async () => {
10
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
11
- try {
12
- await initGitRepo(tempDir)
13
- await runTestEffect(
14
- init({ template: "test-template", silent: true, cwd: tempDir }),
15
- )
16
- expect(await getGitConfig("agency.template", tempDir)).toBe(
17
- "test-template",
18
- )
19
- } finally {
20
- await rm(tempDir, { recursive: true, force: true })
21
- }
22
- })
23
-
24
- test("saves template name to git config", async () => {
25
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
26
- try {
27
- await initGitRepo(tempDir)
28
- await runTestEffect(
29
- init({ template: "my-template", silent: true, cwd: tempDir }),
30
- )
31
- expect(await getGitConfig("agency.template", tempDir)).toBe("my-template")
32
- } finally {
33
- await rm(tempDir, { recursive: true, force: true })
34
- }
35
- })
13
+ let parent: string
36
14
 
37
- test("accepts template names with hyphens", async () => {
38
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
39
- try {
40
- await initGitRepo(tempDir)
41
- await runTestEffect(
42
- init({ template: "my-work-template", silent: true, cwd: tempDir }),
43
- )
44
- expect(await getGitConfig("agency.template", tempDir)).toBe(
45
- "my-work-template",
46
- )
47
- } finally {
48
- await rm(tempDir, { recursive: true, force: true })
49
- }
15
+ beforeEach(async () => {
16
+ parent = await createTempDir()
50
17
  })
51
18
 
52
- test("accepts template names with underscores", async () => {
53
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
54
- try {
55
- await initGitRepo(tempDir)
56
- await runTestEffect(
57
- init({ template: "my_work_template", silent: true, cwd: tempDir }),
58
- )
59
- expect(await getGitConfig("agency.template", tempDir)).toBe(
60
- "my_work_template",
61
- )
62
- } finally {
63
- await rm(tempDir, { recursive: true, force: true })
64
- }
65
- })
66
-
67
- test("throws error when not in git repository", async () => {
68
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
69
- try {
70
- await expect(
71
- runTestEffect(init({ template: "test", silent: true, cwd: tempDir })),
72
- ).rejects.toThrow("Not in a git repository")
73
- } finally {
74
- await rm(tempDir, { recursive: true, force: true })
75
- }
19
+ afterEach(async () => {
20
+ await cleanupTempDir(parent)
76
21
  })
77
22
 
78
- test("throws error when already initialized without template flag", async () => {
79
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
80
- try {
81
- await initGitRepo(tempDir)
82
- await runTestEffect(
83
- init({ template: "first-template", silent: true, cwd: tempDir }),
84
- )
85
- await expect(
86
- runTestEffect(init({ silent: true, cwd: tempDir })),
87
- ).rejects.toThrow("Already initialized")
88
- } finally {
89
- await rm(tempDir, { recursive: true, force: true })
90
- }
91
- })
23
+ test("creates a workbase and required directories", async () => {
24
+ const root = join(parent, "workbase")
25
+ await runTestEffect(init({ path: root, silent: true }))
92
26
 
93
- test("allows re-initialization with different template", async () => {
94
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
95
- try {
96
- await initGitRepo(tempDir)
97
- await runTestEffect(
98
- init({ template: "first-template", silent: true, cwd: tempDir }),
99
- )
100
- await runTestEffect(
101
- init({ template: "second-template", silent: true, cwd: tempDir }),
102
- )
103
- expect(await getGitConfig("agency.template", tempDir)).toBe(
104
- "second-template",
105
- )
106
- } finally {
107
- await rm(tempDir, { recursive: true, force: true })
27
+ expect(await Bun.file(join(root, "agency.json")).json()).toEqual({
28
+ version: 2,
29
+ })
30
+ for (const directory of ["repos", "epics", "tasks"]) {
31
+ expect((await stat(join(root, directory))).isDirectory()).toBe(true)
108
32
  }
33
+ expect(await Bun.file(join(root, ".gitignore")).text()).toBe(
34
+ "/repos/\n/tasks/*/code/\n/tasks/*/phases/*/code/\n",
35
+ )
109
36
  })
110
37
 
111
- test("requires template name in silent mode", async () => {
112
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
113
- try {
114
- await initGitRepo(tempDir)
115
- await expect(
116
- runTestEffect(init({ silent: true, cwd: tempDir })),
117
- ).rejects.toThrow("Template name required")
118
- } finally {
119
- await rm(tempDir, { recursive: true, force: true })
120
- }
121
- })
38
+ test("preserves existing gitignore entries", async () => {
39
+ await Bun.write(join(parent, ".gitignore"), "custom\n")
40
+ await runTestEffect(init({ path: parent, silent: true }))
122
41
 
123
- test("sets config even if template directory does not exist", async () => {
124
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
125
- try {
126
- await initGitRepo(tempDir)
127
- await runTestEffect(
128
- init({ template: "nonexistent", silent: true, cwd: tempDir }),
129
- )
130
- expect(await getGitConfig("agency.template", tempDir)).toBe("nonexistent")
131
- } finally {
132
- await rm(tempDir, { recursive: true, force: true })
133
- }
42
+ expect(await Bun.file(join(parent, ".gitignore")).text()).toStartWith(
43
+ "custom\n",
44
+ )
134
45
  })
135
46
 
136
- test("works without output in silent mode", async () => {
137
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
138
- try {
139
- await initGitRepo(tempDir)
140
- await runTestEffect(
141
- init({ template: "silent-test", silent: true, cwd: tempDir }),
142
- )
143
- expect(await getGitConfig("agency.template", tempDir)).toBe("silent-test")
144
- } finally {
145
- await rm(tempDir, { recursive: true, force: true })
146
- }
147
- })
47
+ test("outputs the initialized workbase as JSON", async () => {
48
+ const root = join(parent, "json-workbase")
49
+ const logs = await captureLogs(() =>
50
+ runTestEffect(init({ path: root, json: true })),
51
+ )
148
52
 
149
- test("fails in silent mode without template", async () => {
150
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
151
- try {
152
- await initGitRepo(tempDir)
153
- await expect(
154
- runTestEffect(init({ silent: true, cwd: tempDir })),
155
- ).rejects.toThrow("Template name required")
156
- } finally {
157
- await rm(tempDir, { recursive: true, force: true })
158
- }
53
+ expect(JSON.parse(logs[0]!)).toEqual({ root })
159
54
  })
160
55
 
161
- test("works in verbose mode", async () => {
162
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
163
- try {
164
- await initGitRepo(tempDir)
165
- const originalLog = console.log
166
- console.log = () => {}
167
- try {
168
- await runTestEffect(
169
- init({
170
- template: "verbose-test",
171
- verbose: true,
172
- silent: false,
173
- cwd: tempDir,
174
- }),
175
- )
176
- expect(await getGitConfig("agency.template", tempDir)).toBe(
177
- "verbose-test",
178
- )
179
- } finally {
180
- console.log = originalLog
181
- }
182
- } finally {
183
- await rm(tempDir, { recursive: true, force: true })
184
- }
185
- })
56
+ test("rejects an existing Agency configuration", async () => {
57
+ await Bun.write(join(parent, "agency.json"), '{"version":2}\n')
186
58
 
187
- test("handles empty template name", async () => {
188
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
189
- try {
190
- await initGitRepo(tempDir)
191
- await expect(
192
- runTestEffect(init({ template: "", silent: true, cwd: tempDir })),
193
- ).rejects.toThrow("Template name required")
194
- } finally {
195
- await rm(tempDir, { recursive: true, force: true })
196
- }
197
- })
198
-
199
- test("handles template name with special characters", async () => {
200
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
201
- try {
202
- await initGitRepo(tempDir)
203
- await runTestEffect(
204
- init({ template: "test.template", silent: true, cwd: tempDir }),
205
- )
206
- expect(await getGitConfig("agency.template", tempDir)).toBe(
207
- "test.template",
208
- )
209
- } finally {
210
- await rm(tempDir, { recursive: true, force: true })
211
- }
212
- })
213
-
214
- test("handles very long template names", async () => {
215
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
216
- try {
217
- await initGitRepo(tempDir)
218
- const longName = "a".repeat(100)
219
- await runTestEffect(
220
- init({ template: longName, silent: true, cwd: tempDir }),
221
- )
222
- expect(await getGitConfig("agency.template", tempDir)).toBe(longName)
223
- } finally {
224
- await rm(tempDir, { recursive: true, force: true })
225
- }
226
- })
227
-
228
- test("changing template updates git config", async () => {
229
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
230
- try {
231
- await initGitRepo(tempDir)
232
- await runTestEffect(
233
- init({ template: "template-a", silent: true, cwd: tempDir }),
234
- )
235
- expect(await getGitConfig("agency.template", tempDir)).toBe("template-a")
236
- await runTestEffect(
237
- init({ template: "template-b", silent: true, cwd: tempDir }),
238
- )
239
- expect(await getGitConfig("agency.template", tempDir)).toBe("template-b")
240
- } finally {
241
- await rm(tempDir, { recursive: true, force: true })
242
- }
243
- })
244
-
245
- test("preserves template name across command runs", async () => {
246
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
247
- try {
248
- await initGitRepo(tempDir)
249
- await runTestEffect(
250
- init({ template: "persistent", silent: true, cwd: tempDir }),
251
- )
252
- expect(await getGitConfig("agency.template", tempDir)).toBe("persistent")
253
- expect(await getGitConfig("agency.template", tempDir)).toBe("persistent")
254
- } finally {
255
- await rm(tempDir, { recursive: true, force: true })
256
- }
257
- })
258
-
259
- test("does not create template directory during init", async () => {
260
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
261
- const configDir = await mkdtemp(join(tmpdir(), "agency-config-"))
262
- const originalConfigDir = process.env.AGENCY_CONFIG_DIR
263
- try {
264
- await initGitRepo(tempDir)
265
- process.env.AGENCY_CONFIG_DIR = configDir
266
- await runTestEffect(
267
- init({ template: "test-template", silent: true, cwd: tempDir }),
268
- )
269
- expect(
270
- await Bun.file(
271
- `${configDir}/templates/test-template/AGENTS.md`,
272
- ).exists(),
273
- ).toBe(false)
274
- } finally {
275
- if (originalConfigDir !== undefined) {
276
- process.env.AGENCY_CONFIG_DIR = originalConfigDir
277
- } else {
278
- delete process.env.AGENCY_CONFIG_DIR
279
- }
280
- await rm(tempDir, { recursive: true, force: true })
281
- await rm(configDir, { recursive: true, force: true })
282
- }
283
- })
284
-
285
- test("works when templates directory does not exist", async () => {
286
- const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
287
- const originalConfigDir = process.env.AGENCY_CONFIG_DIR
288
- try {
289
- await initGitRepo(tempDir)
290
- // Point to a non-existent config directory
291
- const configDir = join(tmpdir(), `agency-config-${Date.now()}`)
292
- process.env.AGENCY_CONFIG_DIR = configDir
293
- // Verify templates directory doesn't exist
294
- expect(await Bun.file(join(configDir, "templates")).exists()).toBe(false)
295
- // Init should work even without templates directory
296
- await runTestEffect(
297
- init({ template: "new-template", silent: true, cwd: tempDir }),
298
- )
299
- expect(await getGitConfig("agency.template", tempDir)).toBe(
300
- "new-template",
301
- )
302
- } finally {
303
- if (originalConfigDir !== undefined) {
304
- process.env.AGENCY_CONFIG_DIR = originalConfigDir
305
- } else {
306
- delete process.env.AGENCY_CONFIG_DIR
307
- }
308
- await rm(tempDir, { recursive: true, force: true })
309
- }
59
+ await expect(
60
+ runTestEffect(init({ path: parent, silent: true })),
61
+ ).rejects.toThrow("already exists")
310
62
  })
311
63
  })