@markjaquith/agency 1.11.1 → 2.1.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 (132) hide show
  1. package/README.md +313 -117
  2. package/cli.ts +124 -420
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +266 -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 +326 -399
  24. package/src/commands/work.ts +153 -153
  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/work-target.test.ts +77 -0
  53. package/src/workbase/work-target.ts +180 -0
  54. package/src/workbase/worktree-command.test.ts +61 -0
  55. package/src/workbase/worktree-command.ts +52 -0
  56. package/src/commands/base.test.ts +0 -198
  57. package/src/commands/base.ts +0 -198
  58. package/src/commands/clean.test.ts +0 -299
  59. package/src/commands/clean.ts +0 -274
  60. package/src/commands/completions.test.ts +0 -51
  61. package/src/commands/completions.ts +0 -308
  62. package/src/commands/emit.integration.test.ts +0 -549
  63. package/src/commands/emit.test.ts +0 -576
  64. package/src/commands/emit.ts +0 -700
  65. package/src/commands/emitted.test.ts +0 -226
  66. package/src/commands/emitted.ts +0 -57
  67. package/src/commands/loop.test.ts +0 -621
  68. package/src/commands/loop.ts +0 -406
  69. package/src/commands/merge.integration.test.ts +0 -195
  70. package/src/commands/merge.test.ts +0 -246
  71. package/src/commands/merge.ts +0 -244
  72. package/src/commands/pull.test.ts +0 -395
  73. package/src/commands/pull.ts +0 -205
  74. package/src/commands/push.test.ts +0 -641
  75. package/src/commands/push.ts +0 -482
  76. package/src/commands/rebase.test.ts +0 -521
  77. package/src/commands/rebase.ts +0 -232
  78. package/src/commands/save.test.ts +0 -247
  79. package/src/commands/save.ts +0 -162
  80. package/src/commands/source.test.ts +0 -195
  81. package/src/commands/source.ts +0 -72
  82. package/src/commands/switch.test.ts +0 -194
  83. package/src/commands/switch.ts +0 -84
  84. package/src/commands/task-branching.test.ts +0 -633
  85. package/src/commands/task-continue.test.ts +0 -311
  86. package/src/commands/task-edit.test.ts +0 -235
  87. package/src/commands/task-main.test.ts +0 -1066
  88. package/src/commands/task-squash.test.ts +0 -266
  89. package/src/commands/tasks.test.ts +0 -391
  90. package/src/commands/tasks.ts +0 -161
  91. package/src/commands/template-delete.test.ts +0 -178
  92. package/src/commands/template-delete.ts +0 -98
  93. package/src/commands/template-list.test.ts +0 -135
  94. package/src/commands/template-list.ts +0 -87
  95. package/src/commands/template-view.test.ts +0 -158
  96. package/src/commands/template-view.ts +0 -86
  97. package/src/commands/template.test.ts +0 -32
  98. package/src/commands/template.ts +0 -96
  99. package/src/commands/use.test.ts +0 -87
  100. package/src/commands/use.ts +0 -97
  101. package/src/constants.ts +0 -10
  102. package/src/errors.ts +0 -17
  103. package/src/schemas.ts +0 -33
  104. package/src/services/AgencyMetadataService.ts +0 -311
  105. package/src/services/ClaudeService.test.ts +0 -184
  106. package/src/services/ClaudeService.ts +0 -91
  107. package/src/services/ConfigService.ts +0 -115
  108. package/src/services/FilterRepoService.ts +0 -164
  109. package/src/services/FormatterService.test.ts +0 -432
  110. package/src/services/FormatterService.ts +0 -219
  111. package/src/services/GitService.ts +0 -1187
  112. package/src/services/MockFilterRepoService.ts +0 -140
  113. package/src/services/OpencodeService.ts +0 -263
  114. package/src/services/PromptService.ts +0 -183
  115. package/src/services/TemplateService.ts +0 -81
  116. package/src/types/native-exec.d.ts +0 -8
  117. package/src/utils/colors.ts +0 -178
  118. package/src/utils/git-path.ts +0 -6
  119. package/src/utils/glob.test.ts +0 -161
  120. package/src/utils/glob.ts +0 -78
  121. package/src/utils/paths.ts +0 -51
  122. package/src/utils/pr-branch.test.ts +0 -376
  123. package/src/utils/pr-branch.ts +0 -508
  124. package/src/utils/spinner.test.ts +0 -45
  125. package/src/utils/spinner.ts +0 -96
  126. package/src/utils/task-parser.test.ts +0 -172
  127. package/src/utils/task-parser.ts +0 -93
  128. package/templates/AGENCY.md +0 -22
  129. package/templates/AGENTS.md +0 -11
  130. package/templates/CLAUDE.md +0 -3
  131. package/templates/TASK.md +0 -5
  132. 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
  })