@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,335 @@
1
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test"
2
+ import { join } from "path"
3
+ import { tasks } from "./tasks"
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("tasks 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 no task branches when none exist", async () => {
66
+ let output = ""
67
+ const originalLog = console.log
68
+ console.log = (msg: string) => {
69
+ output += msg + "\n"
70
+ }
71
+
72
+ await runTestEffect(tasks({}))
73
+
74
+ console.log = originalLog
75
+ expect(output).toContain("No task branches found")
76
+ expect(output).toContain("agency task")
77
+ })
78
+
79
+ test("lists single task branch", 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
+ baseBranch: "main",
90
+ createdAt: new Date().toISOString(),
91
+ } as any)
92
+
93
+ // Commit agency.json
94
+ await Bun.spawn(["git", "add", "agency.json"], {
95
+ cwd: tempDir,
96
+ stdout: "pipe",
97
+ stderr: "pipe",
98
+ }).exited
99
+ await createCommit(tempDir, "Add agency.json")
100
+
101
+ let output = ""
102
+ const originalLog = console.log
103
+ console.log = (msg: string) => {
104
+ output += msg + "\n"
105
+ }
106
+
107
+ await runTestEffect(tasks({}))
108
+
109
+ console.log = originalLog
110
+ expect(output).toContain("feature")
111
+ expect(output.trim()).toBe("feature")
112
+ })
113
+
114
+ test("lists multiple task branches", async () => {
115
+ await initAgency(tempDir, "test-template")
116
+
117
+ // Create first feature branch
118
+ await createBranch(tempDir, "feature-1")
119
+ await writeAgencyMetadata(tempDir, {
120
+ version: 1,
121
+ injectedFiles: ["AGENTS.md"],
122
+ template: "test-template",
123
+ baseBranch: "main",
124
+ createdAt: new Date().toISOString(),
125
+ } as any)
126
+ await Bun.spawn(["git", "add", "agency.json"], {
127
+ cwd: tempDir,
128
+ stdout: "pipe",
129
+ stderr: "pipe",
130
+ }).exited
131
+ await createCommit(tempDir, "Add agency.json")
132
+
133
+ // Go back to main and create second feature branch
134
+ await checkoutBranch(tempDir, "main")
135
+ await createBranch(tempDir, "feature-2")
136
+ await writeAgencyMetadata(tempDir, {
137
+ version: 1,
138
+ injectedFiles: ["opencode.json"],
139
+ template: "another-template",
140
+ baseBranch: "main",
141
+ createdAt: new Date().toISOString(),
142
+ } as any)
143
+ await Bun.spawn(["git", "add", "agency.json"], {
144
+ cwd: tempDir,
145
+ stdout: "pipe",
146
+ stderr: "pipe",
147
+ }).exited
148
+ await createCommit(tempDir, "Add agency.json")
149
+
150
+ let output = ""
151
+ const originalLog = console.log
152
+ console.log = (msg: string) => {
153
+ output += msg + "\n"
154
+ }
155
+
156
+ await runTestEffect(tasks({}))
157
+
158
+ console.log = originalLog
159
+ expect(output).toContain("feature-1")
160
+ expect(output).toContain("feature-2")
161
+ const lines = output.trim().split("\n")
162
+ expect(lines.length).toBe(2)
163
+ })
164
+
165
+ test("ignores branches without agency.json", async () => {
166
+ await initAgency(tempDir, "test-template")
167
+
168
+ // Create a branch without agency.json
169
+ await createBranch(tempDir, "no-agency")
170
+ await createCommit(tempDir, "Some work")
171
+
172
+ // Go back to main and create a branch with agency.json
173
+ await checkoutBranch(tempDir, "main")
174
+ await createBranch(tempDir, "with-agency")
175
+ await writeAgencyMetadata(tempDir, {
176
+ version: 1,
177
+ injectedFiles: [],
178
+ template: "test-template",
179
+ createdAt: new Date().toISOString(),
180
+ } as any)
181
+ await Bun.spawn(["git", "add", "agency.json"], {
182
+ cwd: tempDir,
183
+ stdout: "pipe",
184
+ stderr: "pipe",
185
+ }).exited
186
+ await createCommit(tempDir, "Add agency.json")
187
+
188
+ let output = ""
189
+ const originalLog = console.log
190
+ console.log = (msg: string) => {
191
+ output += msg + "\n"
192
+ }
193
+
194
+ await runTestEffect(tasks({}))
195
+
196
+ console.log = originalLog
197
+ expect(output).toContain("with-agency")
198
+ expect(output).not.toContain("no-agency")
199
+ expect(output.trim()).toBe("with-agency")
200
+ })
201
+ })
202
+
203
+ describe("JSON output", () => {
204
+ test("outputs JSON format when --json is provided", async () => {
205
+ await initAgency(tempDir, "test-template")
206
+
207
+ await createBranch(tempDir, "feature")
208
+ const createdAt = new Date().toISOString()
209
+ await writeAgencyMetadata(tempDir, {
210
+ version: 1,
211
+ injectedFiles: ["AGENTS.md"],
212
+ template: "test-template",
213
+ baseBranch: "main",
214
+ createdAt,
215
+ } as any)
216
+ await Bun.spawn(["git", "add", "agency.json"], {
217
+ cwd: tempDir,
218
+ stdout: "pipe",
219
+ stderr: "pipe",
220
+ }).exited
221
+ await createCommit(tempDir, "Add agency.json")
222
+
223
+ let output = ""
224
+ const originalLog = console.log
225
+ console.log = (msg: string) => {
226
+ output += msg + "\n"
227
+ }
228
+
229
+ await runTestEffect(tasks({ json: true }))
230
+
231
+ console.log = originalLog
232
+
233
+ // Parse JSON output
234
+ const data = JSON.parse(output.trim())
235
+ expect(Array.isArray(data)).toBe(true)
236
+ expect(data.length).toBe(1)
237
+ expect(data[0].branch).toBe("feature")
238
+ expect(data[0].template).toBe("test-template")
239
+ expect(data[0].baseBranch).toBe("main")
240
+ expect(data[0].createdAt).toBeDefined()
241
+ })
242
+
243
+ test("outputs empty array in JSON format when no task branches exist", async () => {
244
+ let output = ""
245
+ const originalLog = console.log
246
+ console.log = (msg: string) => {
247
+ output += msg + "\n"
248
+ }
249
+
250
+ await runTestEffect(tasks({ json: true }))
251
+
252
+ console.log = originalLog
253
+
254
+ const data = JSON.parse(output.trim())
255
+ expect(Array.isArray(data)).toBe(true)
256
+ expect(data.length).toBe(0)
257
+ })
258
+ })
259
+
260
+ describe("edge cases", () => {
261
+ test("handles branches with invalid agency.json gracefully", async () => {
262
+ await initAgency(tempDir, "test-template")
263
+
264
+ // Create branch with invalid agency.json
265
+ await createBranch(tempDir, "invalid")
266
+ await Bun.write(join(tempDir, "agency.json"), "{ invalid json }")
267
+ await Bun.spawn(["git", "add", "agency.json"], {
268
+ cwd: tempDir,
269
+ stdout: "pipe",
270
+ stderr: "pipe",
271
+ }).exited
272
+ await createCommit(tempDir, "Add invalid agency.json")
273
+
274
+ // Create branch with valid agency.json
275
+ await checkoutBranch(tempDir, "main")
276
+ await createBranch(tempDir, "valid")
277
+ await writeAgencyMetadata(tempDir, {
278
+ version: 1,
279
+ injectedFiles: [],
280
+ template: "test-template",
281
+ createdAt: new Date().toISOString(),
282
+ } as any)
283
+ await Bun.spawn(["git", "add", "agency.json"], {
284
+ cwd: tempDir,
285
+ stdout: "pipe",
286
+ stderr: "pipe",
287
+ }).exited
288
+ await createCommit(tempDir, "Add valid agency.json")
289
+
290
+ let output = ""
291
+ const originalLog = console.log
292
+ console.log = (msg: string) => {
293
+ output += msg + "\n"
294
+ }
295
+
296
+ await runTestEffect(tasks({}))
297
+
298
+ console.log = originalLog
299
+ expect(output).toContain("valid")
300
+ expect(output).not.toContain("invalid")
301
+ expect(output.trim()).toBe("valid")
302
+ })
303
+
304
+ test("handles branches with old version agency.json", async () => {
305
+ await initAgency(tempDir, "test-template")
306
+
307
+ // Create branch with version 0 agency.json (future or old version)
308
+ await createBranch(tempDir, "old-version")
309
+ await Bun.write(
310
+ join(tempDir, "agency.json"),
311
+ JSON.stringify({
312
+ version: 0,
313
+ template: "old-template",
314
+ }),
315
+ )
316
+ await Bun.spawn(["git", "add", "agency.json"], {
317
+ cwd: tempDir,
318
+ stdout: "pipe",
319
+ stderr: "pipe",
320
+ }).exited
321
+ await createCommit(tempDir, "Add old version agency.json")
322
+
323
+ let output = ""
324
+ const originalLog = console.log
325
+ console.log = (msg: string) => {
326
+ output += msg + "\n"
327
+ }
328
+
329
+ await runTestEffect(tasks({}))
330
+
331
+ console.log = originalLog
332
+ expect(output).toContain("No task branches found")
333
+ })
334
+ })
335
+ })
@@ -0,0 +1,155 @@
1
+ import { Effect, DateTime } from "effect"
2
+ import { Schema } from "@effect/schema"
3
+ import type { BaseCommandOptions } from "../utils/command"
4
+ import { GitService } from "../services/GitService"
5
+ import { AgencyMetadata } from "../schemas"
6
+ import highlight from "../utils/colors"
7
+ import { createLoggers, ensureGitRepo } from "../utils/effect"
8
+
9
+ interface TasksOptions extends BaseCommandOptions {
10
+ json?: boolean
11
+ }
12
+
13
+ /**
14
+ * Task branch info for output
15
+ */
16
+ interface TaskBranchInfo {
17
+ branch: string
18
+ template: string | null
19
+ baseBranch: string | null
20
+ createdAt: string | null
21
+ }
22
+
23
+ /**
24
+ * Read agency.json metadata from a specific branch using git show.
25
+ */
26
+ const readAgencyMetadataFromBranch = (gitRoot: string, branch: string) =>
27
+ Effect.gen(function* () {
28
+ const git = yield* GitService
29
+
30
+ // Try to read agency.json from the branch using git show
31
+ const result = yield* git.runGitCommand(
32
+ ["git", "show", `${branch}:agency.json`],
33
+ gitRoot,
34
+ { captureOutput: true },
35
+ )
36
+
37
+ if (result.exitCode !== 0 || !result.stdout) {
38
+ return null
39
+ }
40
+
41
+ return yield* parseAgencyMetadata(result.stdout)
42
+ }).pipe(Effect.catchAll(() => Effect.succeed(null)))
43
+
44
+ /**
45
+ * Parse and validate agency.json content.
46
+ */
47
+ const parseAgencyMetadata = (content: string) =>
48
+ Effect.gen(function* () {
49
+ const data = yield* Effect.try({
50
+ try: () => JSON.parse(content),
51
+ catch: () => new Error("Failed to parse agency.json"),
52
+ })
53
+
54
+ // Validate version
55
+ if (typeof data.version !== "number" || data.version !== 1) {
56
+ return null
57
+ }
58
+
59
+ // Parse and validate using Effect schema
60
+ const metadata = yield* Effect.try({
61
+ try: () => Schema.decodeUnknownSync(AgencyMetadata)(data),
62
+ catch: () => new Error("Invalid agency.json format"),
63
+ })
64
+
65
+ return metadata
66
+ }).pipe(Effect.catchAll(() => Effect.succeed(null)))
67
+
68
+ /**
69
+ * Find all branches that contain an agency.json file
70
+ */
71
+ const findAllTaskBranches = (gitRoot: string) =>
72
+ Effect.gen(function* () {
73
+ const git = yield* GitService
74
+
75
+ // Get all local branches
76
+ const branchesResult = yield* git.runGitCommand(
77
+ ["git", "branch", "--format=%(refname:short)"],
78
+ gitRoot,
79
+ { captureOutput: true },
80
+ )
81
+
82
+ if (branchesResult.exitCode !== 0 || !branchesResult.stdout) {
83
+ return []
84
+ }
85
+
86
+ const branches = branchesResult.stdout
87
+ .split("\n")
88
+ .map((b) => b.trim())
89
+ .filter((b) => b.length > 0)
90
+
91
+ // For each branch, try to read agency.json
92
+ const taskBranches: TaskBranchInfo[] = []
93
+ for (const branch of branches) {
94
+ const metadata = yield* readAgencyMetadataFromBranch(gitRoot, branch)
95
+
96
+ if (metadata) {
97
+ taskBranches.push({
98
+ branch,
99
+ template: metadata.template ?? null,
100
+ baseBranch: metadata.baseBranch ?? null,
101
+ createdAt: metadata.createdAt
102
+ ? DateTime.toDateUtc(metadata.createdAt).toISOString()
103
+ : null,
104
+ })
105
+ }
106
+ }
107
+
108
+ return taskBranches
109
+ })
110
+
111
+ export const tasks = (options: TasksOptions = {}) =>
112
+ Effect.gen(function* () {
113
+ const { json = false } = options
114
+ const { log } = createLoggers(options)
115
+
116
+ const gitRoot = yield* ensureGitRepo()
117
+
118
+ // Find all branches with agency.json
119
+ const taskBranches = yield* findAllTaskBranches(gitRoot)
120
+
121
+ if (json) {
122
+ // Output JSON format
123
+ log(JSON.stringify(taskBranches, null, 2))
124
+ } else {
125
+ // Output human-readable format - just branch names
126
+ if (taskBranches.length === 0) {
127
+ log("")
128
+ log("No task branches found.")
129
+ log(
130
+ `Run ${highlight.value("agency task")} to create a task on a feature branch.`,
131
+ )
132
+ log("")
133
+ } else {
134
+ for (const task of taskBranches) {
135
+ log(task.branch)
136
+ }
137
+ }
138
+ }
139
+ })
140
+
141
+ export const help = `
142
+ Usage: agency tasks [options]
143
+
144
+ List all source branches that have agency tasks (branches containing agency.json).
145
+
146
+ This command searches through all local branches and displays those that have
147
+ been initialized with 'agency task'.
148
+
149
+ Options:
150
+ --json Output as JSON (includes metadata: template, base branch, created date)
151
+
152
+ Example:
153
+ agency tasks # List all task branches (names only)
154
+ agency tasks --json # Output as JSON with full metadata
155
+ `
@@ -0,0 +1,178 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test"
2
+ import { templateDelete } from "./template-delete"
3
+ import { mkdir, writeFile, rm } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { runTestEffect } from "../test-utils"
7
+
8
+ describe("template delete command", () => {
9
+ let testDir: string
10
+ let gitRoot: string
11
+ let templateDir: string
12
+
13
+ beforeEach(async () => {
14
+ // Create temporary test directory
15
+ testDir = join(tmpdir(), `agency-test-delete-${Date.now()}`)
16
+ await mkdir(testDir, { recursive: true })
17
+ gitRoot = join(testDir, "repo")
18
+ await mkdir(gitRoot, { recursive: true })
19
+
20
+ // Initialize git repo
21
+ await Bun.spawn(["git", "init"], {
22
+ cwd: gitRoot,
23
+ stdout: "ignore",
24
+ stderr: "ignore",
25
+ }).exited
26
+
27
+ // Set up config dir
28
+ const configDir = join(testDir, "config")
29
+ templateDir = join(configDir, "templates", "test-template")
30
+ await mkdir(templateDir, { recursive: true })
31
+
32
+ // Set environment variable for config
33
+ process.env.AGENCY_CONFIG_DIR = configDir
34
+
35
+ // Set git config
36
+ await Bun.spawn(["git", "config", "agency.template", "test-template"], {
37
+ cwd: gitRoot,
38
+ stdout: "ignore",
39
+ stderr: "ignore",
40
+ }).exited
41
+ })
42
+
43
+ afterEach(async () => {
44
+ // Clean up
45
+ await rm(testDir, { recursive: true, force: true })
46
+ delete process.env.AGENCY_CONFIG_DIR
47
+ })
48
+
49
+ test("deletes file from template directory", async () => {
50
+ // Create test file in template
51
+ const testFile = join(templateDir, "test.md")
52
+ await writeFile(testFile, "# Test")
53
+
54
+ const originalCwd = process.cwd()
55
+ process.chdir(gitRoot)
56
+
57
+ try {
58
+ await runTestEffect(templateDelete({ files: ["test.md"], silent: true }))
59
+
60
+ // Verify file was deleted
61
+ const file = Bun.file(testFile)
62
+ expect(await file.exists()).toBe(false)
63
+ } finally {
64
+ process.chdir(originalCwd)
65
+ }
66
+ })
67
+
68
+ test("deletes multiple files", async () => {
69
+ // Create test files in template
70
+ await writeFile(join(templateDir, "file1.md"), "# File 1")
71
+ await writeFile(join(templateDir, "file2.md"), "# File 2")
72
+ await writeFile(join(templateDir, "file3.md"), "# File 3")
73
+
74
+ const originalCwd = process.cwd()
75
+ process.chdir(gitRoot)
76
+
77
+ try {
78
+ await runTestEffect(
79
+ templateDelete({
80
+ files: ["file1.md", "file2.md"],
81
+ silent: true,
82
+ }),
83
+ )
84
+
85
+ // Verify files were deleted
86
+ expect(await Bun.file(join(templateDir, "file1.md")).exists()).toBe(false)
87
+ expect(await Bun.file(join(templateDir, "file2.md")).exists()).toBe(false)
88
+ // file3.md should still exist
89
+ expect(await Bun.file(join(templateDir, "file3.md")).exists()).toBe(true)
90
+ } finally {
91
+ process.chdir(originalCwd)
92
+ }
93
+ })
94
+
95
+ test("deletes directory recursively", async () => {
96
+ // Create test directory with files
97
+ const docsDir = join(templateDir, "docs")
98
+ await mkdir(docsDir, { recursive: true })
99
+ await writeFile(join(docsDir, "README.md"), "# Docs")
100
+ await writeFile(join(docsDir, "guide.md"), "# Guide")
101
+
102
+ const originalCwd = process.cwd()
103
+ process.chdir(gitRoot)
104
+
105
+ try {
106
+ await runTestEffect(templateDelete({ files: ["docs"], silent: true }))
107
+
108
+ // Verify directory was deleted
109
+ const dir = Bun.file(docsDir)
110
+ expect(await dir.exists()).toBe(false)
111
+ } finally {
112
+ process.chdir(originalCwd)
113
+ }
114
+ })
115
+
116
+ test("handles non-existent files gracefully", async () => {
117
+ const originalCwd = process.cwd()
118
+ process.chdir(gitRoot)
119
+
120
+ try {
121
+ // Should not throw error for non-existent file
122
+ await runTestEffect(
123
+ templateDelete({ files: ["nonexistent.md"], silent: true }),
124
+ )
125
+ } finally {
126
+ process.chdir(originalCwd)
127
+ }
128
+ })
129
+
130
+ test("throws error when no files specified", async () => {
131
+ const originalCwd = process.cwd()
132
+ process.chdir(gitRoot)
133
+
134
+ try {
135
+ await expect(
136
+ runTestEffect(templateDelete({ files: [], silent: true })),
137
+ ).rejects.toThrow("No files specified")
138
+ } finally {
139
+ process.chdir(originalCwd)
140
+ }
141
+ })
142
+
143
+ test("throws error when not in git repository", async () => {
144
+ const nonGitDir = join(testDir, "non-git")
145
+ await mkdir(nonGitDir, { recursive: true })
146
+
147
+ const originalCwd = process.cwd()
148
+ process.chdir(nonGitDir)
149
+
150
+ try {
151
+ await expect(
152
+ runTestEffect(templateDelete({ files: ["test.md"], silent: true })),
153
+ ).rejects.toThrow("Not in a git repository")
154
+ } finally {
155
+ process.chdir(originalCwd)
156
+ }
157
+ })
158
+
159
+ test("throws error when template not configured", async () => {
160
+ // Remove git config
161
+ await Bun.spawn(["git", "config", "--unset", "agency.template"], {
162
+ cwd: gitRoot,
163
+ stdout: "ignore",
164
+ stderr: "ignore",
165
+ }).exited
166
+
167
+ const originalCwd = process.cwd()
168
+ process.chdir(gitRoot)
169
+
170
+ try {
171
+ await expect(
172
+ runTestEffect(templateDelete({ files: ["test.md"], silent: true })),
173
+ ).rejects.toThrow("Repository not initialized")
174
+ } finally {
175
+ process.chdir(originalCwd)
176
+ }
177
+ })
178
+ })