@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.
- package/README.md +313 -117
- package/cli.ts +124 -420
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +266 -0
- package/src/cli.test.ts +290 -0
- package/src/commands/description.test.ts +103 -0
- package/src/commands/epic.test.ts +103 -0
- package/src/commands/epic.ts +101 -0
- package/src/commands/init.test.ts +42 -290
- package/src/commands/init.ts +15 -123
- package/src/commands/phase.ts +120 -0
- package/src/commands/pr.test.ts +19 -170
- package/src/commands/pr.ts +23 -179
- package/src/commands/repo.test.ts +80 -0
- package/src/commands/repo.ts +83 -0
- package/src/commands/status.test.ts +35 -477
- package/src/commands/status.ts +24 -213
- package/src/commands/task-phase.test.ts +206 -0
- package/src/commands/task.ts +83 -1285
- package/src/commands/validate.test.ts +91 -0
- package/src/commands/validate.ts +50 -0
- package/src/commands/work.test.ts +326 -399
- package/src/commands/work.ts +153 -153
- package/src/services/EpicService.test.ts +71 -0
- package/src/services/EpicService.ts +135 -0
- package/src/services/FileSystemService.ts +61 -92
- package/src/services/PhaseService.ts +323 -0
- package/src/services/PullRequestService.test.ts +386 -0
- package/src/services/PullRequestService.ts +143 -0
- package/src/services/RepositoryService.test.ts +106 -0
- package/src/services/RepositoryService.ts +159 -0
- package/src/services/TaskPhaseService.test.ts +217 -0
- package/src/services/TaskService.ts +192 -0
- package/src/services/WorkbaseService.test.ts +446 -0
- package/src/services/WorkbaseService.ts +493 -0
- package/src/services/WorktreeService.test.ts +599 -0
- package/src/services/WorktreeService.ts +357 -0
- package/src/test-utils.ts +41 -438
- package/src/types.ts +1 -215
- package/src/utils/command.ts +1 -5
- package/src/utils/effect.test.ts +24 -0
- package/src/utils/effect.ts +3 -273
- package/src/utils/process.test.ts +0 -15
- package/src/utils/process.ts +1 -60
- package/src/workbase/frontmatter.test.ts +50 -0
- package/src/workbase/frontmatter.ts +77 -0
- package/src/workbase/repository-reference.test.ts +25 -0
- package/src/workbase/repository-reference.ts +18 -0
- package/src/workbase/schemas.test.ts +146 -0
- package/src/workbase/schemas.ts +78 -0
- package/src/workbase/work-target.test.ts +77 -0
- package/src/workbase/work-target.ts +180 -0
- package/src/workbase/worktree-command.test.ts +61 -0
- package/src/workbase/worktree-command.ts +52 -0
- package/src/commands/base.test.ts +0 -198
- package/src/commands/base.ts +0 -198
- package/src/commands/clean.test.ts +0 -299
- package/src/commands/clean.ts +0 -274
- package/src/commands/completions.test.ts +0 -51
- package/src/commands/completions.ts +0 -308
- package/src/commands/emit.integration.test.ts +0 -549
- package/src/commands/emit.test.ts +0 -576
- package/src/commands/emit.ts +0 -700
- package/src/commands/emitted.test.ts +0 -226
- package/src/commands/emitted.ts +0 -57
- package/src/commands/loop.test.ts +0 -621
- package/src/commands/loop.ts +0 -406
- package/src/commands/merge.integration.test.ts +0 -195
- package/src/commands/merge.test.ts +0 -246
- package/src/commands/merge.ts +0 -244
- package/src/commands/pull.test.ts +0 -395
- package/src/commands/pull.ts +0 -205
- package/src/commands/push.test.ts +0 -641
- package/src/commands/push.ts +0 -482
- package/src/commands/rebase.test.ts +0 -521
- package/src/commands/rebase.ts +0 -232
- package/src/commands/save.test.ts +0 -247
- package/src/commands/save.ts +0 -162
- package/src/commands/source.test.ts +0 -195
- package/src/commands/source.ts +0 -72
- package/src/commands/switch.test.ts +0 -194
- package/src/commands/switch.ts +0 -84
- package/src/commands/task-branching.test.ts +0 -633
- package/src/commands/task-continue.test.ts +0 -311
- package/src/commands/task-edit.test.ts +0 -235
- package/src/commands/task-main.test.ts +0 -1066
- package/src/commands/task-squash.test.ts +0 -266
- package/src/commands/tasks.test.ts +0 -391
- package/src/commands/tasks.ts +0 -161
- package/src/commands/template-delete.test.ts +0 -178
- package/src/commands/template-delete.ts +0 -98
- package/src/commands/template-list.test.ts +0 -135
- package/src/commands/template-list.ts +0 -87
- package/src/commands/template-view.test.ts +0 -158
- package/src/commands/template-view.ts +0 -86
- package/src/commands/template.test.ts +0 -32
- package/src/commands/template.ts +0 -96
- package/src/commands/use.test.ts +0 -87
- package/src/commands/use.ts +0 -97
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -17
- package/src/schemas.ts +0 -33
- package/src/services/AgencyMetadataService.ts +0 -311
- package/src/services/ClaudeService.test.ts +0 -184
- package/src/services/ClaudeService.ts +0 -91
- package/src/services/ConfigService.ts +0 -115
- package/src/services/FilterRepoService.ts +0 -164
- package/src/services/FormatterService.test.ts +0 -432
- package/src/services/FormatterService.ts +0 -219
- package/src/services/GitService.ts +0 -1187
- package/src/services/MockFilterRepoService.ts +0 -140
- package/src/services/OpencodeService.ts +0 -263
- package/src/services/PromptService.ts +0 -183
- package/src/services/TemplateService.ts +0 -81
- package/src/types/native-exec.d.ts +0 -8
- package/src/utils/colors.ts +0 -178
- package/src/utils/git-path.ts +0 -6
- package/src/utils/glob.test.ts +0 -161
- package/src/utils/glob.ts +0 -78
- package/src/utils/paths.ts +0 -51
- package/src/utils/pr-branch.test.ts +0 -376
- package/src/utils/pr-branch.ts +0 -508
- package/src/utils/spinner.test.ts +0 -45
- package/src/utils/spinner.ts +0 -96
- package/src/utils/task-parser.test.ts +0 -172
- package/src/utils/task-parser.ts +0 -93
- package/templates/AGENCY.md +0 -22
- package/templates/AGENTS.md +0 -11
- package/templates/CLAUDE.md +0 -3
- package/templates/TASK.md +0 -5
- 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
|
-
})
|