@markjaquith/agency 1.11.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +302 -113
- package/cli.ts +123 -415
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +258 -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 +172 -419
- package/src/commands/work.ts +47 -166
- 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/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 -518
- package/src/commands/push.ts +0 -434
- 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 -1183
- 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,311 +0,0 @@
|
|
|
1
|
-
import { join, dirname, resolve, relative } from "node:path"
|
|
2
|
-
import { Data, Effect } from "effect"
|
|
3
|
-
import { Schema } from "@effect/schema"
|
|
4
|
-
import { AgencyMetadata } from "../schemas"
|
|
5
|
-
import { FileSystemService } from "./FileSystemService"
|
|
6
|
-
import { GitService } from "./GitService"
|
|
7
|
-
import { expandGlobs } from "../utils/glob"
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Error type for AgencyMetadata operations
|
|
11
|
-
*/
|
|
12
|
-
class AgencyMetadataError extends Data.TaggedError("AgencyMetadataError")<{
|
|
13
|
-
message: string
|
|
14
|
-
cause?: unknown
|
|
15
|
-
}> {}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Parse and validate agency.json content from a JSON string.
|
|
19
|
-
* Returns null if the content is invalid.
|
|
20
|
-
*/
|
|
21
|
-
const parseAgencyMetadata = (content: string) =>
|
|
22
|
-
Effect.gen(function* () {
|
|
23
|
-
const data = yield* Effect.try({
|
|
24
|
-
try: () => JSON.parse(content),
|
|
25
|
-
catch: () => new Error("Failed to parse agency.json"),
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
// Validate version
|
|
29
|
-
if (typeof data.version !== "number" || data.version !== 1) {
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Parse and validate using Effect schema
|
|
34
|
-
const metadata = yield* Effect.try({
|
|
35
|
-
try: () => Schema.decodeUnknownSync(AgencyMetadata)(data),
|
|
36
|
-
catch: () => new Error("Invalid agency.json format"),
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
return metadata
|
|
40
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(null)))
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Resolve symlink targets for a list of files.
|
|
44
|
-
* For each file, if it's a symlink, also adds the target path to the result.
|
|
45
|
-
* This ensures that when filtering files from git history, both the symlink
|
|
46
|
-
* and its target are filtered (important when CLAUDE.md → AGENTS.md, for example).
|
|
47
|
-
*/
|
|
48
|
-
const resolveSymlinkTargets = (
|
|
49
|
-
fs: FileSystemService,
|
|
50
|
-
gitRoot: string,
|
|
51
|
-
files: string[],
|
|
52
|
-
) =>
|
|
53
|
-
Effect.gen(function* () {
|
|
54
|
-
const result = new Set<string>(files)
|
|
55
|
-
|
|
56
|
-
for (const file of files) {
|
|
57
|
-
const fullPath = join(gitRoot, file)
|
|
58
|
-
const target = yield* fs.readSymlinkTarget(fullPath)
|
|
59
|
-
|
|
60
|
-
if (target) {
|
|
61
|
-
// The target path could be relative or absolute
|
|
62
|
-
// If relative, it's relative to the symlink's location
|
|
63
|
-
// We need to normalize it to be relative to gitRoot
|
|
64
|
-
let targetPath: string
|
|
65
|
-
if (target.startsWith("/")) {
|
|
66
|
-
// Absolute path - make it relative to gitRoot
|
|
67
|
-
targetPath = relative(gitRoot, target)
|
|
68
|
-
} else {
|
|
69
|
-
// Relative path - resolve from the symlink's directory
|
|
70
|
-
const symlinkDir = dirname(fullPath)
|
|
71
|
-
const absoluteTarget = resolve(symlinkDir, target)
|
|
72
|
-
targetPath = relative(gitRoot, absoluteTarget)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Only add if it's within the gitRoot (not escaping the repo)
|
|
76
|
-
if (!targetPath.startsWith("..")) {
|
|
77
|
-
result.add(targetPath)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return Array.from(result)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Service for managing agency.json metadata operations
|
|
87
|
-
*/
|
|
88
|
-
export class AgencyMetadataService extends Effect.Service<AgencyMetadataService>()(
|
|
89
|
-
"AgencyMetadataService",
|
|
90
|
-
{
|
|
91
|
-
sync: () => ({
|
|
92
|
-
/**
|
|
93
|
-
* Read agency.json from disk in the repository root.
|
|
94
|
-
* Returns null if the file doesn't exist or is invalid.
|
|
95
|
-
*/
|
|
96
|
-
readFromDisk: (gitRoot: string) =>
|
|
97
|
-
Effect.gen(function* () {
|
|
98
|
-
const fs = yield* FileSystemService
|
|
99
|
-
const metadataPath = join(gitRoot, "agency.json")
|
|
100
|
-
|
|
101
|
-
const exists = yield* fs.exists(metadataPath)
|
|
102
|
-
if (!exists) {
|
|
103
|
-
return null
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const content = yield* fs.readFile(metadataPath)
|
|
107
|
-
return yield* parseAgencyMetadata(content)
|
|
108
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(null))),
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Read agency.json from a specific git branch using git show.
|
|
112
|
-
* Returns null if the file doesn't exist or is invalid.
|
|
113
|
-
*/
|
|
114
|
-
readFromBranch: (gitRoot: string, branch: string) =>
|
|
115
|
-
Effect.gen(function* () {
|
|
116
|
-
const git = yield* GitService
|
|
117
|
-
|
|
118
|
-
// Try to read agency.json from the branch using git show
|
|
119
|
-
const content = yield* git.getFileAtRef(
|
|
120
|
-
gitRoot,
|
|
121
|
-
branch,
|
|
122
|
-
"agency.json",
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
if (!content) {
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return yield* parseAgencyMetadata(content)
|
|
130
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(null))),
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Write agency.json to disk in the repository root.
|
|
134
|
-
*/
|
|
135
|
-
write: (gitRoot: string, metadata: AgencyMetadata) =>
|
|
136
|
-
Effect.gen(function* () {
|
|
137
|
-
const fs = yield* FileSystemService
|
|
138
|
-
const metadataPath = join(gitRoot, "agency.json")
|
|
139
|
-
|
|
140
|
-
const content = JSON.stringify(metadata, null, 2) + "\n"
|
|
141
|
-
yield* fs.writeFile(metadataPath, content).pipe(
|
|
142
|
-
Effect.mapError(
|
|
143
|
-
(error) =>
|
|
144
|
-
new AgencyMetadataError({
|
|
145
|
-
message: `Failed to write agency.json: ${error}`,
|
|
146
|
-
cause: error,
|
|
147
|
-
}),
|
|
148
|
-
),
|
|
149
|
-
)
|
|
150
|
-
}),
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Get list of files to filter during PR/merge operations.
|
|
154
|
-
* Always includes TASK.md, AGENCY.md, and agency.json, plus any injectedFiles from metadata.
|
|
155
|
-
*/
|
|
156
|
-
getFilesToFilter: (gitRoot: string) =>
|
|
157
|
-
Effect.gen(function* () {
|
|
158
|
-
const fs = yield* FileSystemService
|
|
159
|
-
const metadataPath = join(gitRoot, "agency.json")
|
|
160
|
-
|
|
161
|
-
const exists = yield* fs.exists(metadataPath)
|
|
162
|
-
const baseFiles = ["TASK.md", "AGENCY.md", "agency.json"]
|
|
163
|
-
|
|
164
|
-
if (!exists) {
|
|
165
|
-
// Resolve symlinks even for base files
|
|
166
|
-
return yield* resolveSymlinkTargets(fs, gitRoot, baseFiles)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const content = yield* fs
|
|
170
|
-
.readFile(metadataPath)
|
|
171
|
-
.pipe(Effect.catchAll(() => Effect.succeed("")))
|
|
172
|
-
|
|
173
|
-
if (!content) {
|
|
174
|
-
return yield* resolveSymlinkTargets(fs, gitRoot, baseFiles)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const metadata = yield* parseAgencyMetadata(content)
|
|
178
|
-
|
|
179
|
-
if (!metadata) {
|
|
180
|
-
return yield* resolveSymlinkTargets(fs, gitRoot, baseFiles)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Expand any glob patterns in injectedFiles to actual file paths
|
|
184
|
-
const expandedFiles = yield* Effect.tryPromise({
|
|
185
|
-
try: () =>
|
|
186
|
-
expandGlobs([...baseFiles, ...metadata.injectedFiles], gitRoot),
|
|
187
|
-
catch: () => new Error("Failed to expand glob patterns"),
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
// Resolve symlinks and add their targets
|
|
191
|
-
return yield* resolveSymlinkTargets(fs, gitRoot, expandedFiles)
|
|
192
|
-
}).pipe(
|
|
193
|
-
Effect.catchAll(() =>
|
|
194
|
-
Effect.succeed(["TASK.md", "AGENCY.md", "agency.json"]),
|
|
195
|
-
),
|
|
196
|
-
),
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Get the configured base branch from agency.json metadata.
|
|
200
|
-
* Returns null if no metadata exists or no base branch is configured.
|
|
201
|
-
*/
|
|
202
|
-
getBaseBranch: (gitRoot: string) =>
|
|
203
|
-
Effect.gen(function* () {
|
|
204
|
-
const fs = yield* FileSystemService
|
|
205
|
-
const metadataPath = join(gitRoot, "agency.json")
|
|
206
|
-
|
|
207
|
-
const exists = yield* fs.exists(metadataPath)
|
|
208
|
-
if (!exists) {
|
|
209
|
-
return null
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const content = yield* fs
|
|
213
|
-
.readFile(metadataPath)
|
|
214
|
-
.pipe(Effect.catchAll(() => Effect.succeed("")))
|
|
215
|
-
|
|
216
|
-
if (!content) {
|
|
217
|
-
return null
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const metadata = yield* parseAgencyMetadata(content)
|
|
221
|
-
return metadata?.baseBranch || null
|
|
222
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(null))),
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Set the base branch in agency.json metadata.
|
|
226
|
-
*/
|
|
227
|
-
setBaseBranch: (gitRoot: string, baseBranch: string) =>
|
|
228
|
-
Effect.gen(function* () {
|
|
229
|
-
const fs = yield* FileSystemService
|
|
230
|
-
const metadataPath = join(gitRoot, "agency.json")
|
|
231
|
-
|
|
232
|
-
const exists = yield* fs.exists(metadataPath).pipe(
|
|
233
|
-
Effect.mapError(
|
|
234
|
-
(error) =>
|
|
235
|
-
new AgencyMetadataError({
|
|
236
|
-
message: `Failed to check agency.json: ${error}`,
|
|
237
|
-
cause: error,
|
|
238
|
-
}),
|
|
239
|
-
),
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
if (!exists) {
|
|
243
|
-
return yield* Effect.fail(
|
|
244
|
-
new AgencyMetadataError({
|
|
245
|
-
message:
|
|
246
|
-
"agency.json not found. Please run 'agency task' first to initialize backpack files.",
|
|
247
|
-
}),
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const content = yield* fs.readFile(metadataPath).pipe(
|
|
252
|
-
Effect.mapError(
|
|
253
|
-
(error) =>
|
|
254
|
-
new AgencyMetadataError({
|
|
255
|
-
message: `Failed to read agency.json: ${error}`,
|
|
256
|
-
cause: error,
|
|
257
|
-
}),
|
|
258
|
-
),
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
const metadata = yield* parseAgencyMetadata(content).pipe(
|
|
262
|
-
Effect.flatMap((m) =>
|
|
263
|
-
m
|
|
264
|
-
? Effect.succeed(m)
|
|
265
|
-
: Effect.fail(
|
|
266
|
-
new AgencyMetadataError({
|
|
267
|
-
message:
|
|
268
|
-
"agency.json is invalid. Please run 'agency task' first to initialize backpack files.",
|
|
269
|
-
}),
|
|
270
|
-
),
|
|
271
|
-
),
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
// Create a new metadata instance with the updated baseBranch
|
|
275
|
-
const updatedMetadata = new AgencyMetadata({
|
|
276
|
-
version: metadata.version,
|
|
277
|
-
injectedFiles: metadata.injectedFiles,
|
|
278
|
-
template: metadata.template,
|
|
279
|
-
createdAt: metadata.createdAt,
|
|
280
|
-
baseBranch,
|
|
281
|
-
emitBranch: metadata.emitBranch,
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
const outputContent = JSON.stringify(updatedMetadata, null, 2) + "\n"
|
|
285
|
-
yield* fs.writeFile(metadataPath, outputContent).pipe(
|
|
286
|
-
Effect.mapError(
|
|
287
|
-
(error) =>
|
|
288
|
-
new AgencyMetadataError({
|
|
289
|
-
message: `Failed to write agency.json: ${error}`,
|
|
290
|
-
cause: error,
|
|
291
|
-
}),
|
|
292
|
-
),
|
|
293
|
-
)
|
|
294
|
-
return void 0
|
|
295
|
-
}),
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Parse and validate agency.json content from a JSON string.
|
|
299
|
-
* Returns null if the content is invalid.
|
|
300
|
-
*/
|
|
301
|
-
parse: parseAgencyMetadata,
|
|
302
|
-
}),
|
|
303
|
-
},
|
|
304
|
-
) {}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Parse and validate agency.json content from a JSON string.
|
|
308
|
-
* Returns null if the content is invalid.
|
|
309
|
-
* This is exported as a standalone function for convenience since it has no dependencies.
|
|
310
|
-
*/
|
|
311
|
-
export { parseAgencyMetadata }
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
import { join } from "path"
|
|
4
|
-
import {
|
|
5
|
-
createTempDir,
|
|
6
|
-
cleanupTempDir,
|
|
7
|
-
initGitRepo,
|
|
8
|
-
runTestEffect,
|
|
9
|
-
fileExists,
|
|
10
|
-
readFile,
|
|
11
|
-
createFile,
|
|
12
|
-
} from "../test-utils"
|
|
13
|
-
import { ClaudeService } from "./ClaudeService"
|
|
14
|
-
|
|
15
|
-
describe("ClaudeService", () => {
|
|
16
|
-
let tempDir: string
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
tempDir = await createTempDir()
|
|
20
|
-
await initGitRepo(tempDir)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await cleanupTempDir(tempDir)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe("claudeFileExists", () => {
|
|
28
|
-
test("returns false when CLAUDE.md does not exist", async () => {
|
|
29
|
-
const result = await runTestEffect(
|
|
30
|
-
Effect.gen(function* () {
|
|
31
|
-
const claudeService = yield* ClaudeService
|
|
32
|
-
return yield* claudeService.claudeFileExists(tempDir)
|
|
33
|
-
}),
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
expect(result).toBe(false)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test("returns true when CLAUDE.md exists", async () => {
|
|
40
|
-
await createFile(tempDir, "CLAUDE.md", "# Claude Code\n")
|
|
41
|
-
|
|
42
|
-
const result = await runTestEffect(
|
|
43
|
-
Effect.gen(function* () {
|
|
44
|
-
const claudeService = yield* ClaudeService
|
|
45
|
-
return yield* claudeService.claudeFileExists(tempDir)
|
|
46
|
-
}),
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
expect(result).toBe(true)
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
describe("hasAgencySection", () => {
|
|
54
|
-
test("returns false when content does not have agency section", async () => {
|
|
55
|
-
const content = "# Claude Code\n\nSome instructions\n"
|
|
56
|
-
|
|
57
|
-
const result = await runTestEffect(
|
|
58
|
-
Effect.gen(function* () {
|
|
59
|
-
const claudeService = yield* ClaudeService
|
|
60
|
-
return yield* claudeService.hasAgencySection(content)
|
|
61
|
-
}),
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
expect(result).toBe(false)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test("returns true when content has both @AGENCY.md and @TASK.md", async () => {
|
|
68
|
-
const content = `# Claude Code
|
|
69
|
-
|
|
70
|
-
Some instructions
|
|
71
|
-
|
|
72
|
-
## Agency
|
|
73
|
-
|
|
74
|
-
@AGENCY.md
|
|
75
|
-
@TASK.md
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
const result = await runTestEffect(
|
|
79
|
-
Effect.gen(function* () {
|
|
80
|
-
const claudeService = yield* ClaudeService
|
|
81
|
-
return yield* claudeService.hasAgencySection(content)
|
|
82
|
-
}),
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
expect(result).toBe(true)
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
describe("injectAgencySection", () => {
|
|
90
|
-
test("creates CLAUDE.md with agency section when file does not exist", async () => {
|
|
91
|
-
const result = await runTestEffect(
|
|
92
|
-
Effect.gen(function* () {
|
|
93
|
-
const claudeService = yield* ClaudeService
|
|
94
|
-
return yield* claudeService.injectAgencySection(tempDir)
|
|
95
|
-
}),
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
expect(result.created).toBe(true)
|
|
99
|
-
expect(result.modified).toBe(true)
|
|
100
|
-
|
|
101
|
-
const claudePath = join(tempDir, "CLAUDE.md")
|
|
102
|
-
expect(await fileExists(claudePath)).toBe(true)
|
|
103
|
-
|
|
104
|
-
const content = await readFile(claudePath)
|
|
105
|
-
expect(content).toContain("@AGENCY.md")
|
|
106
|
-
expect(content).toContain("@TASK.md")
|
|
107
|
-
expect(content.indexOf("@AGENCY.md")).toBeLessThan(
|
|
108
|
-
content.indexOf("@TASK.md"),
|
|
109
|
-
)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test("appends agency section when file exists without references", async () => {
|
|
113
|
-
const initialContent = "# Claude Code\n\nSome existing instructions\n"
|
|
114
|
-
await createFile(tempDir, "CLAUDE.md", initialContent)
|
|
115
|
-
|
|
116
|
-
const result = await runTestEffect(
|
|
117
|
-
Effect.gen(function* () {
|
|
118
|
-
const claudeService = yield* ClaudeService
|
|
119
|
-
return yield* claudeService.injectAgencySection(tempDir)
|
|
120
|
-
}),
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
expect(result.created).toBe(false)
|
|
124
|
-
expect(result.modified).toBe(true)
|
|
125
|
-
|
|
126
|
-
const content = await readFile(join(tempDir, "CLAUDE.md"))
|
|
127
|
-
expect(content).toContain("Some existing instructions")
|
|
128
|
-
expect(content).toContain("@AGENCY.md")
|
|
129
|
-
expect(content).toContain("@TASK.md")
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
test("does not modify file when agency section already exists in correct order", async () => {
|
|
133
|
-
const contentWithSection = `# Claude Code
|
|
134
|
-
|
|
135
|
-
Some instructions
|
|
136
|
-
|
|
137
|
-
## Agency
|
|
138
|
-
|
|
139
|
-
@AGENCY.md
|
|
140
|
-
@TASK.md
|
|
141
|
-
`
|
|
142
|
-
await createFile(tempDir, "CLAUDE.md", contentWithSection)
|
|
143
|
-
|
|
144
|
-
const result = await runTestEffect(
|
|
145
|
-
Effect.gen(function* () {
|
|
146
|
-
const claudeService = yield* ClaudeService
|
|
147
|
-
return yield* claudeService.injectAgencySection(tempDir)
|
|
148
|
-
}),
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
expect(result.created).toBe(false)
|
|
152
|
-
expect(result.modified).toBe(false)
|
|
153
|
-
|
|
154
|
-
const content = await readFile(join(tempDir, "CLAUDE.md"))
|
|
155
|
-
expect(content).toBe(contentWithSection)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
test("re-adds references when they exist in wrong order", async () => {
|
|
159
|
-
const contentWithWrongOrder = `# Claude Code
|
|
160
|
-
|
|
161
|
-
Some instructions
|
|
162
|
-
|
|
163
|
-
@TASK.md
|
|
164
|
-
@AGENCY.md
|
|
165
|
-
`
|
|
166
|
-
await createFile(tempDir, "CLAUDE.md", contentWithWrongOrder)
|
|
167
|
-
|
|
168
|
-
const result = await runTestEffect(
|
|
169
|
-
Effect.gen(function* () {
|
|
170
|
-
const claudeService = yield* ClaudeService
|
|
171
|
-
return yield* claudeService.injectAgencySection(tempDir)
|
|
172
|
-
}),
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
expect(result.created).toBe(false)
|
|
176
|
-
expect(result.modified).toBe(true)
|
|
177
|
-
|
|
178
|
-
const content = await readFile(join(tempDir, "CLAUDE.md"))
|
|
179
|
-
// Should have the section appended with correct order
|
|
180
|
-
expect(content).toContain("@AGENCY.md")
|
|
181
|
-
expect(content).toContain("@TASK.md")
|
|
182
|
-
})
|
|
183
|
-
})
|
|
184
|
-
})
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect"
|
|
2
|
-
import { FileSystemService } from "./FileSystemService"
|
|
3
|
-
|
|
4
|
-
const AGENCY_SECTION = `
|
|
5
|
-
## Agency
|
|
6
|
-
|
|
7
|
-
@AGENCY.md
|
|
8
|
-
@TASK.md`
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Service for handling CLAUDE.md files with @-reference injection.
|
|
12
|
-
*/
|
|
13
|
-
export class ClaudeService extends Effect.Service<ClaudeService>()(
|
|
14
|
-
"ClaudeService",
|
|
15
|
-
{
|
|
16
|
-
sync: () => ({
|
|
17
|
-
/**
|
|
18
|
-
* Check if CLAUDE.md exists in the git root.
|
|
19
|
-
*/
|
|
20
|
-
claudeFileExists: (gitRoot: string) =>
|
|
21
|
-
Effect.gen(function* () {
|
|
22
|
-
const fs = yield* FileSystemService
|
|
23
|
-
const claudePath = `${gitRoot}/CLAUDE.md`
|
|
24
|
-
return yield* fs.exists(claudePath)
|
|
25
|
-
}),
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Check if the CLAUDE.md file already contains the agency section.
|
|
29
|
-
*/
|
|
30
|
-
hasAgencySection: (content: string) =>
|
|
31
|
-
Effect.sync(() => {
|
|
32
|
-
// Check if both @AGENCY.md and @TASK.md exist in the content
|
|
33
|
-
return content.includes("@AGENCY.md") && content.includes("@TASK.md")
|
|
34
|
-
}),
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Inject or ensure the agency section exists in CLAUDE.md.
|
|
38
|
-
* If the file doesn't exist, create it with the basic template.
|
|
39
|
-
* If it exists but doesn't have the references, append them.
|
|
40
|
-
* Returns true if the file was modified, false if no changes needed.
|
|
41
|
-
*/
|
|
42
|
-
injectAgencySection: (gitRoot: string) =>
|
|
43
|
-
Effect.gen(function* () {
|
|
44
|
-
const fs = yield* FileSystemService
|
|
45
|
-
const claudePath = `${gitRoot}/CLAUDE.md`
|
|
46
|
-
|
|
47
|
-
// Check if file exists
|
|
48
|
-
const exists = yield* fs.exists(claudePath)
|
|
49
|
-
|
|
50
|
-
if (!exists) {
|
|
51
|
-
// Create new CLAUDE.md with basic template
|
|
52
|
-
const content = `# Claude Code Instructions
|
|
53
|
-
|
|
54
|
-
This project uses the agency CLI for managing development tasks and templates.
|
|
55
|
-
${AGENCY_SECTION}
|
|
56
|
-
`
|
|
57
|
-
yield* fs.writeFile(claudePath, content)
|
|
58
|
-
return { modified: true, created: true }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Read existing content
|
|
62
|
-
const content = yield* fs.readFile(claudePath)
|
|
63
|
-
|
|
64
|
-
// Check if agency section already exists
|
|
65
|
-
const hasSection = yield* Effect.sync(() => {
|
|
66
|
-
return (
|
|
67
|
-
content.includes("@AGENCY.md") && content.includes("@TASK.md")
|
|
68
|
-
)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
if (hasSection) {
|
|
72
|
-
// Check if they're in the correct order
|
|
73
|
-
const agencyIndex = content.indexOf("@AGENCY.md")
|
|
74
|
-
const taskIndex = content.indexOf("@TASK.md")
|
|
75
|
-
|
|
76
|
-
if (agencyIndex < taskIndex) {
|
|
77
|
-
// Already has the section in correct order
|
|
78
|
-
return { modified: false, created: false }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// They exist but in wrong order - we'll re-add them
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Append the agency section
|
|
85
|
-
const newContent = content.trimEnd() + "\n" + AGENCY_SECTION + "\n"
|
|
86
|
-
yield* fs.writeFile(claudePath, newContent)
|
|
87
|
-
return { modified: true, created: false }
|
|
88
|
-
}),
|
|
89
|
-
}),
|
|
90
|
-
},
|
|
91
|
-
) {}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { Effect, Data } from "effect"
|
|
2
|
-
import { Schema } from "@effect/schema"
|
|
3
|
-
import { mkdir } from "node:fs/promises"
|
|
4
|
-
import { AgencyConfig } from "../schemas"
|
|
5
|
-
import { getAgencyConfigDir, getAgencyConfigPath } from "../utils/paths"
|
|
6
|
-
|
|
7
|
-
// Error types for Config operations
|
|
8
|
-
class ConfigError extends Data.TaggedError("ConfigError")<{
|
|
9
|
-
message: string
|
|
10
|
-
cause?: unknown
|
|
11
|
-
}> {}
|
|
12
|
-
|
|
13
|
-
class ConfigWriteError extends Data.TaggedError("ConfigWriteError")<{
|
|
14
|
-
path: string
|
|
15
|
-
cause?: unknown
|
|
16
|
-
}> {}
|
|
17
|
-
|
|
18
|
-
const DEFAULT_CONFIG: AgencyConfig = new AgencyConfig({
|
|
19
|
-
sourceBranchPattern: "agency--%branch%",
|
|
20
|
-
emitBranch: "%branch%",
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// Config Service using Effect.Service pattern
|
|
24
|
-
export class ConfigService extends Effect.Service<ConfigService>()(
|
|
25
|
-
"ConfigService",
|
|
26
|
-
{
|
|
27
|
-
sync: () => ({
|
|
28
|
-
getConfigDir: () => Effect.sync(() => getAgencyConfigDir()),
|
|
29
|
-
|
|
30
|
-
getConfigPath: () => Effect.sync(() => getAgencyConfigPath()),
|
|
31
|
-
|
|
32
|
-
loadConfig: (configPath?: string) =>
|
|
33
|
-
Effect.gen(function* () {
|
|
34
|
-
const path =
|
|
35
|
-
configPath || (yield* Effect.sync(() => getAgencyConfigPath()))
|
|
36
|
-
|
|
37
|
-
// Check if file exists
|
|
38
|
-
const file = Bun.file(path)
|
|
39
|
-
const exists = yield* Effect.tryPromise({
|
|
40
|
-
try: () => file.exists(),
|
|
41
|
-
catch: () =>
|
|
42
|
-
new ConfigError({
|
|
43
|
-
message: `Failed to check if config exists at ${path}`,
|
|
44
|
-
}),
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
if (!exists) {
|
|
48
|
-
return DEFAULT_CONFIG
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Try to read and parse the config
|
|
52
|
-
const data = yield* Effect.tryPromise({
|
|
53
|
-
try: () => file.json(),
|
|
54
|
-
catch: (error) =>
|
|
55
|
-
new ConfigError({
|
|
56
|
-
message: `Failed to read config file at ${path}`,
|
|
57
|
-
cause: error,
|
|
58
|
-
}),
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Parse with schema, but fall back to defaults on error
|
|
62
|
-
const parsed = yield* Effect.try({
|
|
63
|
-
try: () => Schema.decodeUnknownSync(AgencyConfig)(data),
|
|
64
|
-
catch: (error) =>
|
|
65
|
-
new ConfigError({
|
|
66
|
-
message: `Invalid config format at ${path}`,
|
|
67
|
-
cause: error,
|
|
68
|
-
}),
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// If we get here, parsing succeeded
|
|
72
|
-
return parsed || DEFAULT_CONFIG
|
|
73
|
-
}),
|
|
74
|
-
|
|
75
|
-
saveConfig: (config: AgencyConfig, configPath?: string) =>
|
|
76
|
-
Effect.gen(function* () {
|
|
77
|
-
const path =
|
|
78
|
-
configPath || (yield* Effect.sync(() => getAgencyConfigPath()))
|
|
79
|
-
|
|
80
|
-
// Ensure the config directory exists
|
|
81
|
-
const configDir = yield* Effect.sync(() => getAgencyConfigDir())
|
|
82
|
-
|
|
83
|
-
yield* Effect.tryPromise({
|
|
84
|
-
try: () => mkdir(configDir, { recursive: true }),
|
|
85
|
-
catch: (error) =>
|
|
86
|
-
new ConfigWriteError({
|
|
87
|
-
path: configDir,
|
|
88
|
-
cause: error,
|
|
89
|
-
}),
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// Encode and write the config
|
|
93
|
-
const encoded = yield* Effect.try({
|
|
94
|
-
try: () => Schema.encodeSync(AgencyConfig)(config),
|
|
95
|
-
catch: (error) =>
|
|
96
|
-
new ConfigWriteError({
|
|
97
|
-
path,
|
|
98
|
-
cause: error,
|
|
99
|
-
}),
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
yield* Effect.tryPromise({
|
|
103
|
-
try: () => Bun.write(path, JSON.stringify(encoded, null, 2) + "\n"),
|
|
104
|
-
catch: (error) =>
|
|
105
|
-
new ConfigWriteError({
|
|
106
|
-
path,
|
|
107
|
-
cause: error,
|
|
108
|
-
}),
|
|
109
|
-
})
|
|
110
|
-
}),
|
|
111
|
-
|
|
112
|
-
getDefaultConfig: () => Effect.succeed(DEFAULT_CONFIG),
|
|
113
|
-
}),
|
|
114
|
-
},
|
|
115
|
-
) {}
|