@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
@@ -1,164 +0,0 @@
1
- import { Effect, Data, pipe } from "effect"
2
- import { spawnProcess } from "../utils/process"
3
-
4
- // Error types for FilterRepo operations
5
- class FilterRepoError extends Data.TaggedError("FilterRepoError")<{
6
- message: string
7
- cause?: unknown
8
- }> {}
9
-
10
- class FilterRepoNotInstalledError extends Data.TaggedError(
11
- "FilterRepoNotInstalledError",
12
- )<{
13
- message: string
14
- }> {}
15
-
16
- /**
17
- * Service for git-filter-repo operations.
18
- * git-filter-repo is an external Python tool for rewriting git history.
19
- */
20
- export class FilterRepoService extends Effect.Service<FilterRepoService>()(
21
- "FilterRepoService",
22
- {
23
- sync: () => ({
24
- /**
25
- * Check if git-filter-repo is installed.
26
- * @returns true if installed, false otherwise
27
- */
28
- isInstalled: () =>
29
- pipe(
30
- spawnProcess(["which", "git-filter-repo"]),
31
- Effect.map((result) => result.exitCode === 0),
32
- Effect.catchAll(() => Effect.succeed(false)),
33
- ),
34
-
35
- /**
36
- * Run git-filter-repo with the given arguments.
37
- * @param gitRoot - The git repository root
38
- * @param args - Arguments to pass to git-filter-repo
39
- * @param options - Additional options
40
- * @returns Object with exitCode, stdout, stderr
41
- */
42
- run: (
43
- gitRoot: string,
44
- args: readonly string[],
45
- options?: {
46
- readonly env?: Record<string, string>
47
- readonly verboseLog?: (message: string) => void
48
- readonly streamOutput?: boolean
49
- readonly progressIntervalMs?: number
50
- },
51
- ) =>
52
- Effect.gen(function* () {
53
- // First check if git-filter-repo is installed
54
- const installed = yield* pipe(
55
- spawnProcess(["which", "git-filter-repo"]),
56
- Effect.map((result) => result.exitCode === 0),
57
- Effect.catchAll(() => Effect.succeed(false)),
58
- )
59
-
60
- if (!installed) {
61
- return yield* Effect.fail(
62
- new FilterRepoNotInstalledError({
63
- message:
64
- "git-filter-repo is not installed. Install it with: pip install git-filter-repo",
65
- }),
66
- )
67
- }
68
-
69
- const fullArgs = ["git-filter-repo", ...args]
70
- const verboseLog = options?.verboseLog ?? (() => {})
71
-
72
- verboseLog(`Running ${fullArgs.join(" ")} in ${gitRoot}`)
73
- if (options?.streamOutput) {
74
- verboseLog("Streaming git-filter-repo output directly")
75
- }
76
-
77
- const result = yield* pipe(
78
- spawnProcess(fullArgs, {
79
- cwd: gitRoot,
80
- env: options?.env,
81
- stdout: options?.streamOutput ? "inherit" : "pipe",
82
- stderr: options?.streamOutput ? "inherit" : "pipe",
83
- onProgress: options?.streamOutput
84
- ? ({ elapsedMs, pid }) => {
85
- const elapsedSeconds = Math.round(elapsedMs / 1000)
86
- verboseLog(
87
- `git-filter-repo still running after ${elapsedSeconds}s${pid ? ` (pid ${pid})` : ""}`,
88
- )
89
- }
90
- : undefined,
91
- progressIntervalMs: options?.progressIntervalMs,
92
- }),
93
- Effect.mapError(
94
- (error) =>
95
- new FilterRepoError({
96
- message: `git-filter-repo failed: ${error.stderr}`,
97
- cause: error,
98
- }),
99
- ),
100
- )
101
-
102
- verboseLog(`git-filter-repo exited with code ${result.exitCode}`)
103
-
104
- if (result.exitCode !== 0) {
105
- return yield* Effect.fail(
106
- new FilterRepoError({
107
- message: `git-filter-repo failed with exit code ${result.exitCode}: ${result.stderr}`,
108
- }),
109
- )
110
- }
111
-
112
- return result
113
- }),
114
-
115
- /**
116
- * Filter files from repository history.
117
- * This is a higher-level operation that handles common filtering patterns.
118
- * @param gitRoot - The git repository root
119
- * @param options - Filtering options
120
- */
121
- filterFiles: (
122
- gitRoot: string,
123
- options: {
124
- readonly refs?: string
125
- readonly pathsToRemove?: readonly string[]
126
- readonly pathRenames?: readonly { from: string; to: string }[]
127
- readonly force?: boolean
128
- },
129
- ) =>
130
- Effect.gen(function* () {
131
- const args: string[] = []
132
-
133
- if (options.refs) {
134
- args.push("--refs", options.refs)
135
- }
136
-
137
- if (options.pathsToRemove) {
138
- for (const path of options.pathsToRemove) {
139
- args.push("--invert-paths", "--path", path)
140
- }
141
- }
142
-
143
- if (options.pathRenames) {
144
- for (const rename of options.pathRenames) {
145
- args.push("--path-rename", `${rename.from}:${rename.to}`)
146
- }
147
- }
148
-
149
- if (options.force) {
150
- args.push("--force")
151
- }
152
-
153
- // Always use these safety options
154
- args.push("--prune-empty=always")
155
-
156
- const filterRepo = yield* FilterRepoService
157
-
158
- return yield* filterRepo.run(gitRoot, args, {
159
- env: { GIT_CONFIG_GLOBAL: "" },
160
- })
161
- }),
162
- }),
163
- },
164
- ) {}
@@ -1,432 +0,0 @@
1
- import { test, expect, describe, beforeEach, afterEach } from "bun:test"
2
- import { join } from "path"
3
- import { Effect } from "effect"
4
- import { FormatterService } from "../services/FormatterService"
5
- import { createTempDir, cleanupTempDir, runTestEffect } from "../test-utils"
6
-
7
- describe("FormatterService", () => {
8
- let tempDir: string
9
-
10
- beforeEach(async () => {
11
- tempDir = await createTempDir()
12
- })
13
-
14
- afterEach(async () => {
15
- await cleanupTempDir(tempDir)
16
- })
17
-
18
- describe("detectPackageManager", () => {
19
- test("returns null when no package.json exists", async () => {
20
- const result = await runTestEffect(
21
- Effect.gen(function* () {
22
- const service = yield* FormatterService
23
- return yield* service.detectPackageManager(tempDir)
24
- }),
25
- )
26
- expect(result).toBeNull()
27
- })
28
-
29
- test("detects bun from bun.lockb", async () => {
30
- await Bun.write(join(tempDir, "package.json"), "{}")
31
- await Bun.write(join(tempDir, "bun.lockb"), "")
32
-
33
- const result = await runTestEffect(
34
- Effect.gen(function* () {
35
- const service = yield* FormatterService
36
- return yield* service.detectPackageManager(tempDir)
37
- }),
38
- )
39
- expect(result).toBe("bun")
40
- })
41
-
42
- test("detects bun from bun.lock", async () => {
43
- await Bun.write(join(tempDir, "package.json"), "{}")
44
- await Bun.write(join(tempDir, "bun.lock"), "")
45
-
46
- const result = await runTestEffect(
47
- Effect.gen(function* () {
48
- const service = yield* FormatterService
49
- return yield* service.detectPackageManager(tempDir)
50
- }),
51
- )
52
- expect(result).toBe("bun")
53
- })
54
-
55
- test("detects yarn from yarn.lock", async () => {
56
- await Bun.write(join(tempDir, "package.json"), "{}")
57
- await Bun.write(join(tempDir, "yarn.lock"), "")
58
-
59
- const result = await runTestEffect(
60
- Effect.gen(function* () {
61
- const service = yield* FormatterService
62
- return yield* service.detectPackageManager(tempDir)
63
- }),
64
- )
65
- expect(result).toBe("yarn")
66
- })
67
-
68
- test("detects pnpm from pnpm-lock.yaml", async () => {
69
- await Bun.write(join(tempDir, "package.json"), "{}")
70
- await Bun.write(join(tempDir, "pnpm-lock.yaml"), "")
71
-
72
- const result = await runTestEffect(
73
- Effect.gen(function* () {
74
- const service = yield* FormatterService
75
- return yield* service.detectPackageManager(tempDir)
76
- }),
77
- )
78
- expect(result).toBe("pnpm")
79
- })
80
-
81
- test("detects npm from package-lock.json", async () => {
82
- await Bun.write(join(tempDir, "package.json"), "{}")
83
- await Bun.write(join(tempDir, "package-lock.json"), "{}")
84
-
85
- const result = await runTestEffect(
86
- Effect.gen(function* () {
87
- const service = yield* FormatterService
88
- return yield* service.detectPackageManager(tempDir)
89
- }),
90
- )
91
- expect(result).toBe("npm")
92
- })
93
-
94
- test("defaults to npm when package.json exists but no lock file", async () => {
95
- await Bun.write(join(tempDir, "package.json"), "{}")
96
-
97
- const result = await runTestEffect(
98
- Effect.gen(function* () {
99
- const service = yield* FormatterService
100
- return yield* service.detectPackageManager(tempDir)
101
- }),
102
- )
103
- expect(result).toBe("npm")
104
- })
105
-
106
- test("bun.lockb takes priority over yarn.lock", async () => {
107
- await Bun.write(join(tempDir, "package.json"), "{}")
108
- await Bun.write(join(tempDir, "bun.lockb"), "")
109
- await Bun.write(join(tempDir, "yarn.lock"), "")
110
-
111
- const result = await runTestEffect(
112
- Effect.gen(function* () {
113
- const service = yield* FormatterService
114
- return yield* service.detectPackageManager(tempDir)
115
- }),
116
- )
117
- expect(result).toBe("bun")
118
- })
119
- })
120
-
121
- describe("detectFormatter", () => {
122
- test("returns null when no package.json exists", async () => {
123
- const result = await runTestEffect(
124
- Effect.gen(function* () {
125
- const service = yield* FormatterService
126
- return yield* service.detectFormatter(tempDir)
127
- }),
128
- )
129
- expect(result).toBeNull()
130
- })
131
-
132
- test("detects prettier in devDependencies", async () => {
133
- await Bun.write(
134
- join(tempDir, "package.json"),
135
- JSON.stringify({
136
- devDependencies: { prettier: "^3.0.0" },
137
- }),
138
- )
139
-
140
- const result = await runTestEffect(
141
- Effect.gen(function* () {
142
- const service = yield* FormatterService
143
- return yield* service.detectFormatter(tempDir)
144
- }),
145
- )
146
- expect(result).toBe("prettier")
147
- })
148
-
149
- test("detects prettier in dependencies", async () => {
150
- await Bun.write(
151
- join(tempDir, "package.json"),
152
- JSON.stringify({
153
- dependencies: { prettier: "^3.0.0" },
154
- }),
155
- )
156
-
157
- const result = await runTestEffect(
158
- Effect.gen(function* () {
159
- const service = yield* FormatterService
160
- return yield* service.detectFormatter(tempDir)
161
- }),
162
- )
163
- expect(result).toBe("prettier")
164
- })
165
-
166
- test("detects oxfmt in devDependencies", async () => {
167
- await Bun.write(
168
- join(tempDir, "package.json"),
169
- JSON.stringify({
170
- devDependencies: { oxfmt: "^0.1.0" },
171
- }),
172
- )
173
-
174
- const result = await runTestEffect(
175
- Effect.gen(function* () {
176
- const service = yield* FormatterService
177
- return yield* service.detectFormatter(tempDir)
178
- }),
179
- )
180
- expect(result).toBe("oxfmt")
181
- })
182
-
183
- test("oxfmt takes priority over prettier", async () => {
184
- await Bun.write(
185
- join(tempDir, "package.json"),
186
- JSON.stringify({
187
- devDependencies: { prettier: "^3.0.0", oxfmt: "^0.1.0" },
188
- }),
189
- )
190
-
191
- const result = await runTestEffect(
192
- Effect.gen(function* () {
193
- const service = yield* FormatterService
194
- return yield* service.detectFormatter(tempDir)
195
- }),
196
- )
197
- expect(result).toBe("oxfmt")
198
- })
199
-
200
- test("returns null when no formatter is in dependencies", async () => {
201
- await Bun.write(
202
- join(tempDir, "package.json"),
203
- JSON.stringify({
204
- devDependencies: { typescript: "^5.0.0" },
205
- }),
206
- )
207
-
208
- const result = await runTestEffect(
209
- Effect.gen(function* () {
210
- const service = yield* FormatterService
211
- return yield* service.detectFormatter(tempDir)
212
- }),
213
- )
214
- expect(result).toBeNull()
215
- })
216
-
217
- test("handles malformed package.json gracefully", async () => {
218
- await Bun.write(join(tempDir, "package.json"), "not valid json")
219
-
220
- const result = await runTestEffect(
221
- Effect.gen(function* () {
222
- const service = yield* FormatterService
223
- return yield* service.detectFormatter(tempDir)
224
- }),
225
- )
226
- expect(result).toBeNull()
227
- })
228
- })
229
-
230
- describe("buildFormatterCommand", () => {
231
- test("builds prettier command with bun", async () => {
232
- const result = await runTestEffect(
233
- Effect.gen(function* () {
234
- const service = yield* FormatterService
235
- return yield* service.buildFormatterCommand("prettier", "bun", [
236
- "/path/to/file.md",
237
- ])
238
- }),
239
- )
240
- expect(result).toEqual([
241
- "bun",
242
- "x",
243
- "prettier",
244
- "--write",
245
- "/path/to/file.md",
246
- ])
247
- })
248
-
249
- test("builds prettier command with yarn", async () => {
250
- const result = await runTestEffect(
251
- Effect.gen(function* () {
252
- const service = yield* FormatterService
253
- return yield* service.buildFormatterCommand("prettier", "yarn", [
254
- "/path/to/file.md",
255
- ])
256
- }),
257
- )
258
- expect(result).toEqual([
259
- "yarn",
260
- "dlx",
261
- "prettier",
262
- "--write",
263
- "/path/to/file.md",
264
- ])
265
- })
266
-
267
- test("builds prettier command with pnpm", async () => {
268
- const result = await runTestEffect(
269
- Effect.gen(function* () {
270
- const service = yield* FormatterService
271
- return yield* service.buildFormatterCommand("prettier", "pnpm", [
272
- "/path/to/file.md",
273
- ])
274
- }),
275
- )
276
- expect(result).toEqual([
277
- "pnpm",
278
- "exec",
279
- "prettier",
280
- "--write",
281
- "/path/to/file.md",
282
- ])
283
- })
284
-
285
- test("builds prettier command with npm", async () => {
286
- const result = await runTestEffect(
287
- Effect.gen(function* () {
288
- const service = yield* FormatterService
289
- return yield* service.buildFormatterCommand("prettier", "npm", [
290
- "/path/to/file.md",
291
- ])
292
- }),
293
- )
294
- expect(result).toEqual(["npx", "prettier", "--write", "/path/to/file.md"])
295
- })
296
-
297
- test("builds oxfmt command with bun", async () => {
298
- const result = await runTestEffect(
299
- Effect.gen(function* () {
300
- const service = yield* FormatterService
301
- return yield* service.buildFormatterCommand("oxfmt", "bun", [
302
- "/path/to/file.json",
303
- ])
304
- }),
305
- )
306
- expect(result).toEqual(["bun", "x", "oxfmt", "/path/to/file.json"])
307
- })
308
-
309
- test("handles multiple files", async () => {
310
- const result = await runTestEffect(
311
- Effect.gen(function* () {
312
- const service = yield* FormatterService
313
- return yield* service.buildFormatterCommand("prettier", "bun", [
314
- "/path/to/file.md",
315
- "/path/to/file.json",
316
- ])
317
- }),
318
- )
319
- expect(result).toEqual([
320
- "bun",
321
- "x",
322
- "prettier",
323
- "--write",
324
- "/path/to/file.md",
325
- "/path/to/file.json",
326
- ])
327
- })
328
-
329
- test("returns null for empty file list", async () => {
330
- const result = await runTestEffect(
331
- Effect.gen(function* () {
332
- const service = yield* FormatterService
333
- return yield* service.buildFormatterCommand("prettier", "bun", [])
334
- }),
335
- )
336
- expect(result).toBeNull()
337
- })
338
- })
339
-
340
- describe("formatFiles", () => {
341
- test("silently does nothing when no package.json exists", async () => {
342
- const logs: string[] = []
343
- const verboseLog = (msg: string) => logs.push(msg)
344
-
345
- await runTestEffect(
346
- Effect.gen(function* () {
347
- const service = yield* FormatterService
348
- yield* service.formatFiles(tempDir, ["AGENTS.md"], verboseLog)
349
- }),
350
- )
351
-
352
- expect(logs.some((l) => l.includes("No formatter"))).toBe(true)
353
- })
354
-
355
- test("silently does nothing when no formatter in package.json", async () => {
356
- await Bun.write(
357
- join(tempDir, "package.json"),
358
- JSON.stringify({ devDependencies: { typescript: "^5.0.0" } }),
359
- )
360
-
361
- const logs: string[] = []
362
- const verboseLog = (msg: string) => logs.push(msg)
363
-
364
- await runTestEffect(
365
- Effect.gen(function* () {
366
- const service = yield* FormatterService
367
- yield* service.formatFiles(tempDir, ["AGENTS.md"], verboseLog)
368
- }),
369
- )
370
-
371
- expect(logs.some((l) => l.includes("No formatter"))).toBe(true)
372
- })
373
-
374
- test("filters to only md/json/jsonc files", async () => {
375
- const logs: string[] = []
376
- const verboseLog = (msg: string) => logs.push(msg)
377
-
378
- await runTestEffect(
379
- Effect.gen(function* () {
380
- const service = yield* FormatterService
381
- yield* service.formatFiles(
382
- tempDir,
383
- ["somefile.ts", "other.txt"],
384
- verboseLog,
385
- )
386
- }),
387
- )
388
-
389
- expect(logs.some((l) => l.includes("No formattable files"))).toBe(true)
390
- })
391
-
392
- test("does nothing with empty file list", async () => {
393
- const logs: string[] = []
394
- const verboseLog = (msg: string) => logs.push(msg)
395
-
396
- await runTestEffect(
397
- Effect.gen(function* () {
398
- const service = yield* FormatterService
399
- yield* service.formatFiles(tempDir, [], verboseLog)
400
- }),
401
- )
402
-
403
- expect(logs.some((l) => l.includes("No files to format"))).toBe(true)
404
- })
405
-
406
- test("silently handles formatter command failure", async () => {
407
- // Set up a project with prettier but no actual prettier installed
408
- await Bun.write(
409
- join(tempDir, "package.json"),
410
- JSON.stringify({ devDependencies: { prettier: "^3.0.0" } }),
411
- )
412
- await Bun.write(join(tempDir, "bun.lockb"), "")
413
- await Bun.write(join(tempDir, "AGENTS.md"), "# Test")
414
-
415
- const logs: string[] = []
416
- const verboseLog = (msg: string) => logs.push(msg)
417
-
418
- // Should not throw even though prettier isn't actually installed
419
- await runTestEffect(
420
- Effect.gen(function* () {
421
- const service = yield* FormatterService
422
- yield* service.formatFiles(tempDir, ["AGENTS.md"], verboseLog)
423
- }),
424
- )
425
-
426
- // Should have detected the formatter and attempted to run it
427
- expect(logs.some((l) => l.includes("Detected formatter: prettier"))).toBe(
428
- true,
429
- )
430
- })
431
- })
432
- })