@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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/cli.ts +569 -0
- package/index.ts +1 -0
- package/package.json +65 -0
- package/src/commands/base.test.ts +198 -0
- package/src/commands/base.ts +198 -0
- package/src/commands/clean.test.ts +299 -0
- package/src/commands/clean.ts +320 -0
- package/src/commands/emit.test.ts +412 -0
- package/src/commands/emit.ts +521 -0
- package/src/commands/emitted.test.ts +226 -0
- package/src/commands/emitted.ts +57 -0
- package/src/commands/init.test.ts +311 -0
- package/src/commands/init.ts +140 -0
- package/src/commands/merge.test.ts +365 -0
- package/src/commands/merge.ts +253 -0
- package/src/commands/pull.test.ts +385 -0
- package/src/commands/pull.ts +205 -0
- package/src/commands/push.test.ts +394 -0
- package/src/commands/push.ts +346 -0
- package/src/commands/save.test.ts +247 -0
- package/src/commands/save.ts +162 -0
- package/src/commands/source.test.ts +195 -0
- package/src/commands/source.ts +72 -0
- package/src/commands/status.test.ts +489 -0
- package/src/commands/status.ts +258 -0
- package/src/commands/switch.test.ts +194 -0
- package/src/commands/switch.ts +84 -0
- package/src/commands/task-branching.test.ts +334 -0
- package/src/commands/task-edit.test.ts +141 -0
- package/src/commands/task-main.test.ts +872 -0
- package/src/commands/task.ts +712 -0
- package/src/commands/tasks.test.ts +335 -0
- package/src/commands/tasks.ts +155 -0
- package/src/commands/template-delete.test.ts +178 -0
- package/src/commands/template-delete.ts +98 -0
- package/src/commands/template-list.test.ts +135 -0
- package/src/commands/template-list.ts +87 -0
- package/src/commands/template-view.test.ts +158 -0
- package/src/commands/template-view.ts +86 -0
- package/src/commands/template.test.ts +32 -0
- package/src/commands/template.ts +96 -0
- package/src/commands/use.test.ts +87 -0
- package/src/commands/use.ts +97 -0
- package/src/commands/work.test.ts +462 -0
- package/src/commands/work.ts +193 -0
- package/src/errors.ts +17 -0
- package/src/schemas.ts +33 -0
- package/src/services/AgencyMetadataService.ts +287 -0
- package/src/services/ClaudeService.test.ts +184 -0
- package/src/services/ClaudeService.ts +91 -0
- package/src/services/ConfigService.ts +115 -0
- package/src/services/FileSystemService.ts +222 -0
- package/src/services/GitService.ts +751 -0
- package/src/services/OpencodeService.ts +263 -0
- package/src/services/PromptService.ts +183 -0
- package/src/services/TemplateService.ts +75 -0
- package/src/test-utils.ts +362 -0
- package/src/types/native-exec.d.ts +8 -0
- package/src/types.ts +216 -0
- package/src/utils/colors.ts +178 -0
- package/src/utils/command.ts +17 -0
- package/src/utils/effect.ts +281 -0
- package/src/utils/exec.ts +48 -0
- package/src/utils/paths.ts +51 -0
- package/src/utils/pr-branch.test.ts +372 -0
- package/src/utils/pr-branch.ts +473 -0
- package/src/utils/process.ts +110 -0
- package/src/utils/spinner.ts +82 -0
- package/templates/AGENCY.md +20 -0
- package/templates/AGENTS.md +11 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/TASK.md +5 -0
- 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
|
+
})
|