@markjaquith/agency 2.4.0 → 2.6.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 +56 -20
- package/cli.ts +43 -2
- package/package.json +1 -1
- package/skills/agency/SKILL.md +41 -5
- package/src/cli.test.ts +31 -3
- package/src/commands/archive.test.ts +69 -0
- package/src/commands/archive.ts +70 -0
- package/src/commands/init.test.ts +3 -0
- package/src/commands/phase.ts +23 -1
- package/src/commands/task-phase.test.ts +46 -1
- package/src/commands/task.test.ts +94 -0
- package/src/commands/task.ts +160 -17
- package/src/commands/validate.test.ts +65 -0
- package/src/commands/validate.ts +19 -5
- package/src/commands/work.test.ts +100 -34
- package/src/commands/work.ts +30 -10
- package/src/commands/workbase.test.ts +62 -0
- package/src/commands/workbase.ts +58 -0
- package/src/services/ArchiveService.test.ts +334 -0
- package/src/services/ArchiveService.ts +246 -0
- package/src/services/PhaseService.ts +28 -0
- package/src/services/TaskPhaseService.test.ts +79 -0
- package/src/services/TaskService.ts +31 -1
- package/src/services/WorkbaseService.test.ts +99 -1
- package/src/services/WorkbaseService.ts +101 -2
- package/src/services/WorktreeService.test.ts +150 -1
- package/src/services/WorktreeService.ts +123 -1
- package/src/test-utils.ts +2 -0
- package/src/utils/progress.test.ts +37 -0
- package/src/utils/progress.ts +36 -0
- package/src/workbase/AGENTS.md +3 -0
- package/src/workbase/opencode-file.ts +37 -0
- package/src/workbase/schemas.test.ts +114 -0
- package/src/workbase/schemas.ts +18 -2
- package/src/workbase/work-target.test.ts +16 -9
- package/src/workbase/work-target.ts +37 -6
- package/src/workbase/workbase-choice.ts +71 -0
|
@@ -9,6 +9,8 @@ import { WorktreeService } from "../services/WorktreeService"
|
|
|
9
9
|
import { captureLogs } from "../test-utils"
|
|
10
10
|
import { work } from "./work"
|
|
11
11
|
import type { PickWorkTarget } from "../workbase/work-target"
|
|
12
|
+
import type { PickWorkbase } from "../workbase/workbase-choice"
|
|
13
|
+
import type { Progress } from "../utils/progress"
|
|
12
14
|
|
|
13
15
|
type ExecutionWorkspace = Effect.Effect.Success<
|
|
14
16
|
ReturnType<WorktreeService["materialize"]>
|
|
@@ -42,11 +44,15 @@ interface HarnessOptions {
|
|
|
42
44
|
readonly epicRecords?: readonly any[]
|
|
43
45
|
readonly taskRecords?: readonly any[]
|
|
44
46
|
readonly phaseRecords?: readonly any[]
|
|
47
|
+
readonly outsideWorkbase?: boolean
|
|
48
|
+
readonly registeredWorkbases?: readonly string[]
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
const createHarness = (options: HarnessOptions = {}) => {
|
|
48
52
|
const events: string[] = []
|
|
49
53
|
const probes: string[] = []
|
|
54
|
+
const statusUpdates: string[] = []
|
|
55
|
+
const progressUpdates: string[] = []
|
|
50
56
|
const launches: Array<{
|
|
51
57
|
cli: string
|
|
52
58
|
args: readonly string[]
|
|
@@ -70,7 +76,14 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
70
76
|
},
|
|
71
77
|
}
|
|
72
78
|
const workbase = {
|
|
73
|
-
discover: () =>
|
|
79
|
+
discover: (path: string) =>
|
|
80
|
+
options.outsideWorkbase && path === "/outside"
|
|
81
|
+
? Effect.fail({
|
|
82
|
+
_tag: "WorkbaseNotFoundError" as const,
|
|
83
|
+
message: "No Agency workbase found from /outside",
|
|
84
|
+
})
|
|
85
|
+
: Effect.succeed("/workbase"),
|
|
86
|
+
listRegistered: () => Effect.succeed(options.registeredWorkbases ?? []),
|
|
74
87
|
}
|
|
75
88
|
const epics = {
|
|
76
89
|
show: (id: string) =>
|
|
@@ -91,6 +104,10 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
91
104
|
: { repo: "agency", branch: `task/${id}`, base: "main" },
|
|
92
105
|
}),
|
|
93
106
|
list: () => Effect.succeed(options.taskRecords ?? []),
|
|
107
|
+
setStatus: (id: string, status: string) => {
|
|
108
|
+
statusUpdates.push(`task:${id}:${status}`)
|
|
109
|
+
return Effect.void
|
|
110
|
+
},
|
|
94
111
|
}
|
|
95
112
|
const phases = {
|
|
96
113
|
show: (taskId: string, id: string) =>
|
|
@@ -101,6 +118,10 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
101
118
|
data: { repo: "agency", branch: `task/${id}`, base: "main" },
|
|
102
119
|
}),
|
|
103
120
|
list: () => Effect.succeed(options.phaseRecords ?? []),
|
|
121
|
+
setStatus: (taskId: string, id: string, status: string) => {
|
|
122
|
+
statusUpdates.push(`phase:${taskId}:${id}:${status}`)
|
|
123
|
+
return Effect.void
|
|
124
|
+
},
|
|
104
125
|
}
|
|
105
126
|
const fs = {
|
|
106
127
|
runCommand: (args: readonly string[]) => {
|
|
@@ -119,12 +140,19 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
119
140
|
launches.push({ cli, args, cwd })
|
|
120
141
|
}
|
|
121
142
|
const defaultPick: PickWorkTarget = () => Effect.succeed(null)
|
|
143
|
+
const defaultPickWorkbase: PickWorkbase = () => Effect.succeed(null)
|
|
144
|
+
const progress: Progress = {
|
|
145
|
+
start: (message) => progressUpdates.push(`start:${message}`),
|
|
146
|
+
succeed: (message) => progressUpdates.push(`succeed:${message}`),
|
|
147
|
+
fail: (message) => progressUpdates.push(`fail:${message}`),
|
|
148
|
+
}
|
|
122
149
|
const run = (
|
|
123
150
|
commandOptions: Parameters<typeof work>[0],
|
|
124
151
|
pick: PickWorkTarget = defaultPick,
|
|
152
|
+
pickBase: PickWorkbase = defaultPickWorkbase,
|
|
125
153
|
) =>
|
|
126
154
|
Effect.runPromise(
|
|
127
|
-
work(commandOptions, launch, pick).pipe(
|
|
155
|
+
work(commandOptions, launch, pick, progress, pickBase).pipe(
|
|
128
156
|
Effect.provideService(WorktreeService, worktrees as never),
|
|
129
157
|
Effect.provideService(FileSystemService, fs as never),
|
|
130
158
|
Effect.provideService(WorkbaseService, workbase as never),
|
|
@@ -134,7 +162,15 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
134
162
|
) as Effect.Effect<void, unknown, never>,
|
|
135
163
|
)
|
|
136
164
|
|
|
137
|
-
return {
|
|
165
|
+
return {
|
|
166
|
+
events,
|
|
167
|
+
probes,
|
|
168
|
+
launches,
|
|
169
|
+
materializeOptions,
|
|
170
|
+
statusUpdates,
|
|
171
|
+
progressUpdates,
|
|
172
|
+
run,
|
|
173
|
+
}
|
|
138
174
|
}
|
|
139
175
|
|
|
140
176
|
describe("work command", () => {
|
|
@@ -146,11 +182,7 @@ describe("work command", () => {
|
|
|
146
182
|
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
147
183
|
expect(harness.launches[0]).toEqual({
|
|
148
184
|
cli: "opencode",
|
|
149
|
-
args: [
|
|
150
|
-
"opencode",
|
|
151
|
-
"--prompt",
|
|
152
|
-
"Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
|
|
153
|
-
],
|
|
185
|
+
args: ["opencode", "--continue"],
|
|
154
186
|
cwd: "/workbase/epics/delivery",
|
|
155
187
|
})
|
|
156
188
|
})
|
|
@@ -163,11 +195,7 @@ describe("work command", () => {
|
|
|
163
195
|
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
164
196
|
expect(harness.launches[0]).toEqual({
|
|
165
197
|
cli: "opencode",
|
|
166
|
-
args: [
|
|
167
|
-
"opencode",
|
|
168
|
-
"--prompt",
|
|
169
|
-
"Work on the task. Read /workbase/tasks/delivery/TASK.md.",
|
|
170
|
-
],
|
|
198
|
+
args: ["opencode", "--continue"],
|
|
171
199
|
cwd: "/workbase/tasks/delivery",
|
|
172
200
|
})
|
|
173
201
|
})
|
|
@@ -185,9 +213,10 @@ describe("work command", () => {
|
|
|
185
213
|
"probe:opencode",
|
|
186
214
|
"launch:opencode",
|
|
187
215
|
])
|
|
188
|
-
expect(harness.launches[0]?.args).
|
|
189
|
-
|
|
190
|
-
|
|
216
|
+
expect(harness.launches[0]?.args).toEqual(["opencode", "--continue"])
|
|
217
|
+
expect(harness.statusUpdates).toEqual([
|
|
218
|
+
"phase:example:implementation:working",
|
|
219
|
+
])
|
|
191
220
|
})
|
|
192
221
|
|
|
193
222
|
test("infers a single-phase task from a nested checkout directory", async () => {
|
|
@@ -199,7 +228,7 @@ describe("work command", () => {
|
|
|
199
228
|
})
|
|
200
229
|
|
|
201
230
|
expect(harness.events[0]).toBe("materialize")
|
|
202
|
-
expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/example
|
|
231
|
+
expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/example")
|
|
203
232
|
})
|
|
204
233
|
|
|
205
234
|
test("selects a target with fzf outside an entity directory", async () => {
|
|
@@ -234,8 +263,43 @@ describe("work command", () => {
|
|
|
234
263
|
"launch:opencode",
|
|
235
264
|
])
|
|
236
265
|
expect(harness.launches[0]?.cwd).toBe(
|
|
237
|
-
"/workbase/tasks/
|
|
266
|
+
"/workbase/tasks/delivery/phases/build",
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test("selects a registered workbase when local discovery fails", async () => {
|
|
271
|
+
const harness = createHarness({
|
|
272
|
+
outsideWorkbase: true,
|
|
273
|
+
registeredWorkbases: ["/first", "/workbase"],
|
|
274
|
+
})
|
|
275
|
+
const selections: string[][] = []
|
|
276
|
+
const pickBase: PickWorkbase = (workbases) => {
|
|
277
|
+
selections.push([...workbases])
|
|
278
|
+
return Effect.succeed("/workbase")
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await harness.run(
|
|
282
|
+
{ cwd: "/outside", taskId: "example", opencode: true },
|
|
283
|
+
undefined,
|
|
284
|
+
pickBase,
|
|
238
285
|
)
|
|
286
|
+
|
|
287
|
+
expect(selections).toEqual([["/first", "/workbase"]])
|
|
288
|
+
expect(harness.events).toEqual([
|
|
289
|
+
"probe:fzf",
|
|
290
|
+
"materialize",
|
|
291
|
+
"probe:opencode",
|
|
292
|
+
"launch:opencode",
|
|
293
|
+
])
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test("explains how to register a workbase when none are known", async () => {
|
|
297
|
+
const harness = createHarness({ outsideWorkbase: true })
|
|
298
|
+
|
|
299
|
+
await expect(harness.run({ cwd: "/outside" })).rejects.toThrow(
|
|
300
|
+
"agency workbase add <path>",
|
|
301
|
+
)
|
|
302
|
+
expect(harness.events).toEqual([])
|
|
239
303
|
})
|
|
240
304
|
|
|
241
305
|
test("prints the target tree when fzf is unavailable", async () => {
|
|
@@ -278,7 +342,7 @@ describe("work command", () => {
|
|
|
278
342
|
expect(harness.events).toEqual([])
|
|
279
343
|
})
|
|
280
344
|
|
|
281
|
-
test("launches OpenCode in the
|
|
345
|
+
test("launches OpenCode in the task directory and continues its session", async () => {
|
|
282
346
|
const harness = createHarness()
|
|
283
347
|
|
|
284
348
|
await harness.run({ taskId: "example", opencode: true })
|
|
@@ -291,17 +355,18 @@ describe("work command", () => {
|
|
|
291
355
|
expect(harness.launches).toEqual([
|
|
292
356
|
{
|
|
293
357
|
cli: "opencode",
|
|
294
|
-
args: [
|
|
295
|
-
|
|
296
|
-
"--prompt",
|
|
297
|
-
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
298
|
-
],
|
|
299
|
-
cwd: "/workbase/tasks/example/code/agency",
|
|
358
|
+
args: ["opencode", "--continue"],
|
|
359
|
+
cwd: "/workbase/tasks/example",
|
|
300
360
|
},
|
|
301
361
|
])
|
|
362
|
+
expect(harness.statusUpdates).toEqual(["task:example:working"])
|
|
363
|
+
expect(harness.progressUpdates).toEqual([
|
|
364
|
+
"start:Preparing workspace...",
|
|
365
|
+
"succeed:Workspace ready",
|
|
366
|
+
])
|
|
302
367
|
})
|
|
303
368
|
|
|
304
|
-
test("
|
|
369
|
+
test("continues OpenCode for a multi-phase task", async () => {
|
|
305
370
|
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
306
371
|
|
|
307
372
|
await harness.run({
|
|
@@ -310,11 +375,7 @@ describe("work command", () => {
|
|
|
310
375
|
opencode: true,
|
|
311
376
|
})
|
|
312
377
|
|
|
313
|
-
expect(harness.launches[0]?.args).toEqual([
|
|
314
|
-
"opencode",
|
|
315
|
-
"--prompt",
|
|
316
|
-
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
317
|
-
])
|
|
378
|
+
expect(harness.launches[0]?.args).toEqual(["opencode", "--continue"])
|
|
318
379
|
})
|
|
319
380
|
|
|
320
381
|
test("automatically falls back to Claude", async () => {
|
|
@@ -326,7 +387,7 @@ describe("work command", () => {
|
|
|
326
387
|
expect(harness.launches[0]).toEqual({
|
|
327
388
|
cli: "claude",
|
|
328
389
|
args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
|
|
329
|
-
cwd: "/workbase/tasks/example
|
|
390
|
+
cwd: "/workbase/tasks/example",
|
|
330
391
|
})
|
|
331
392
|
})
|
|
332
393
|
|
|
@@ -338,6 +399,7 @@ describe("work command", () => {
|
|
|
338
399
|
).rejects.toThrow("opencode CLI tool not found")
|
|
339
400
|
expect(harness.probes).toEqual(["opencode"])
|
|
340
401
|
expect(harness.launches).toEqual([])
|
|
402
|
+
expect(harness.statusUpdates).toEqual([])
|
|
341
403
|
})
|
|
342
404
|
|
|
343
405
|
test("launches explicitly requested Claude", async () => {
|
|
@@ -349,7 +411,7 @@ describe("work command", () => {
|
|
|
349
411
|
expect(harness.launches[0]).toEqual({
|
|
350
412
|
cli: "claude",
|
|
351
413
|
args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
|
|
352
|
-
cwd: "/workbase/tasks/example
|
|
414
|
+
cwd: "/workbase/tasks/example",
|
|
353
415
|
})
|
|
354
416
|
})
|
|
355
417
|
|
|
@@ -374,6 +436,10 @@ describe("work command", () => {
|
|
|
374
436
|
"materialization failed",
|
|
375
437
|
)
|
|
376
438
|
expect(harness.events).toEqual(["materialize"])
|
|
439
|
+
expect(harness.progressUpdates).toEqual([
|
|
440
|
+
"start:Preparing workspace...",
|
|
441
|
+
"fail:Workspace preparation failed",
|
|
442
|
+
])
|
|
377
443
|
})
|
|
378
444
|
|
|
379
445
|
test("respects silent and verbose logging options", async () => {
|
|
@@ -382,7 +448,7 @@ describe("work command", () => {
|
|
|
382
448
|
verboseHarness.run({ taskId: "example", verbose: true }),
|
|
383
449
|
)
|
|
384
450
|
expect(verboseLogs).toEqual([
|
|
385
|
-
"Launching opencode in /workbase/tasks/example
|
|
451
|
+
"Launching opencode in /workbase/tasks/example",
|
|
386
452
|
])
|
|
387
453
|
expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
|
|
388
454
|
|
package/src/commands/work.ts
CHANGED
|
@@ -9,12 +9,18 @@ import { TaskService } from "../services/TaskService"
|
|
|
9
9
|
import { PhaseService } from "../services/PhaseService"
|
|
10
10
|
import { createLoggers } from "../utils/effect"
|
|
11
11
|
import { execvp } from "../utils/exec"
|
|
12
|
+
import { createProgress, type Progress } from "../utils/progress"
|
|
12
13
|
import {
|
|
13
14
|
buildWorkTargetChoices,
|
|
14
15
|
pickWorkTarget,
|
|
15
16
|
type PickWorkTarget,
|
|
16
17
|
type WorkTarget,
|
|
17
18
|
} from "../workbase/work-target"
|
|
19
|
+
import {
|
|
20
|
+
pickWorkbase,
|
|
21
|
+
resolveWorkbase,
|
|
22
|
+
type PickWorkbase,
|
|
23
|
+
} from "../workbase/workbase-choice"
|
|
18
24
|
|
|
19
25
|
interface WorkOptions extends BaseCommandOptions {
|
|
20
26
|
readonly taskId?: string
|
|
@@ -35,6 +41,8 @@ export const work = (
|
|
|
35
41
|
options: WorkOptions = {},
|
|
36
42
|
launch: LaunchAgent = launchAgent,
|
|
37
43
|
pick: PickWorkTarget = pickWorkTarget,
|
|
44
|
+
progress: Progress = createProgress(options),
|
|
45
|
+
pickBase: PickWorkbase = pickWorkbase,
|
|
38
46
|
) =>
|
|
39
47
|
Effect.gen(function* () {
|
|
40
48
|
if (options.opencode && options.claude) {
|
|
@@ -56,7 +64,8 @@ export const work = (
|
|
|
56
64
|
const phases = yield* PhaseService
|
|
57
65
|
const { log, verboseLog } = createLoggers(options)
|
|
58
66
|
const cwd = options.cwd ?? process.cwd()
|
|
59
|
-
const root = yield*
|
|
67
|
+
const root = yield* resolveWorkbase(cwd, log, pickBase)
|
|
68
|
+
if (!root) return
|
|
60
69
|
|
|
61
70
|
let target: WorkTarget | null = null
|
|
62
71
|
if (options.epicId) {
|
|
@@ -155,16 +164,21 @@ export const work = (
|
|
|
155
164
|
} else {
|
|
156
165
|
const taskId = target.taskId
|
|
157
166
|
const phaseId = target.kind === "phase" ? target.phaseId : undefined
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
phaseId,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
progress.start("Preparing workspace...")
|
|
168
|
+
const workspace = yield* worktrees
|
|
169
|
+
.materialize(taskId, phaseId, root, options)
|
|
170
|
+
.pipe(
|
|
171
|
+
Effect.tap(() =>
|
|
172
|
+
Effect.sync(() => progress.succeed("Workspace ready")),
|
|
173
|
+
),
|
|
174
|
+
Effect.tapError(() =>
|
|
175
|
+
Effect.sync(() => progress.fail("Workspace preparation failed")),
|
|
176
|
+
),
|
|
177
|
+
)
|
|
164
178
|
prompt = workspace.phasePath
|
|
165
179
|
? `Start the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
|
|
166
180
|
: `Start the task. Read ${workspace.taskPath}.`
|
|
167
|
-
launchPath =
|
|
181
|
+
launchPath = dirname(target.path)
|
|
168
182
|
}
|
|
169
183
|
|
|
170
184
|
const requested = options.claude ? "claude" : "opencode"
|
|
@@ -179,9 +193,14 @@ export const work = (
|
|
|
179
193
|
if (available.exitCode !== 0) {
|
|
180
194
|
return yield* Effect.fail(new Error(`${cli} CLI tool not found`))
|
|
181
195
|
}
|
|
196
|
+
if (target.kind === "phase") {
|
|
197
|
+
yield* phases.setStatus(target.taskId, target.phaseId, "working", root)
|
|
198
|
+
} else if (target.kind === "task" && !target.multiPhase) {
|
|
199
|
+
yield* tasks.setStatus(target.taskId, "working", root)
|
|
200
|
+
}
|
|
182
201
|
|
|
183
202
|
verboseLog(`Launching ${cli} in ${launchPath}`)
|
|
184
|
-
const args = cli === "opencode" ? ["--
|
|
203
|
+
const args = cli === "opencode" ? ["--continue"] : [prompt]
|
|
185
204
|
launch(cli, [cli, ...args], launchPath)
|
|
186
205
|
})
|
|
187
206
|
|
|
@@ -189,7 +208,8 @@ export const help = `
|
|
|
189
208
|
Usage: agency work [<task-id> [phase-id] | --epic <epic-id>]
|
|
190
209
|
|
|
191
210
|
Launch an agent for the current epic, task, or phase. Outside an entity
|
|
192
|
-
directory, select one with fzf.
|
|
211
|
+
directory, select one with fzf. Outside a workbase, select a registered
|
|
212
|
+
workbase first.
|
|
193
213
|
|
|
194
214
|
Options:
|
|
195
215
|
--epic <id> Work on an epic
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
captureLogs,
|
|
5
|
+
cleanupTempDir,
|
|
6
|
+
createTempDir,
|
|
7
|
+
runTestEffect,
|
|
8
|
+
} from "../test-utils"
|
|
9
|
+
import { workbase } from "./workbase"
|
|
10
|
+
|
|
11
|
+
describe("workbase command", () => {
|
|
12
|
+
let root: string
|
|
13
|
+
let configDirectory: string
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
root = await createTempDir()
|
|
17
|
+
configDirectory = join(root, "config")
|
|
18
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(async () => cleanupTempDir(root))
|
|
22
|
+
|
|
23
|
+
test("adds and lists workbases as JSON", async () => {
|
|
24
|
+
const added = await captureLogs(() =>
|
|
25
|
+
runTestEffect(
|
|
26
|
+
workbase({
|
|
27
|
+
subcommand: "add",
|
|
28
|
+
args: [root],
|
|
29
|
+
configDirectory,
|
|
30
|
+
json: true,
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
const path = JSON.parse(added[0]!).path
|
|
35
|
+
|
|
36
|
+
const listed = await captureLogs(() =>
|
|
37
|
+
runTestEffect(
|
|
38
|
+
workbase({
|
|
39
|
+
subcommand: "list",
|
|
40
|
+
args: [],
|
|
41
|
+
configDirectory,
|
|
42
|
+
json: true,
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
expect(JSON.parse(listed[0]!)).toEqual([path])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("requires an add path", async () => {
|
|
51
|
+
await expect(
|
|
52
|
+
runTestEffect(
|
|
53
|
+
workbase({
|
|
54
|
+
subcommand: "add",
|
|
55
|
+
args: [],
|
|
56
|
+
configDirectory,
|
|
57
|
+
silent: true,
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
).rejects.toThrow("Usage: agency workbase add <path>")
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
3
|
+
import { WorkbaseService } from "../services/WorkbaseService"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
|
|
6
|
+
interface WorkbaseOptions extends BaseCommandOptions {
|
|
7
|
+
readonly subcommand?: string
|
|
8
|
+
readonly args: readonly string[]
|
|
9
|
+
readonly configDirectory?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const workbase = (options: WorkbaseOptions) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const service = yield* WorkbaseService
|
|
15
|
+
const { log } = createLoggers(options)
|
|
16
|
+
|
|
17
|
+
switch (options.subcommand) {
|
|
18
|
+
case "add": {
|
|
19
|
+
const path = options.args[0]
|
|
20
|
+
if (!path) {
|
|
21
|
+
return yield* Effect.fail(
|
|
22
|
+
new Error("Usage: agency workbase add <path>"),
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
const root = yield* service.register(path, options.configDirectory)
|
|
26
|
+
log(
|
|
27
|
+
options.json
|
|
28
|
+
? JSON.stringify({ path: root }, null, 2)
|
|
29
|
+
: `Added workbase ${root}`,
|
|
30
|
+
)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
case "list": {
|
|
34
|
+
const workbases = yield* service.listRegistered(options.configDirectory)
|
|
35
|
+
if (options.json) {
|
|
36
|
+
log(JSON.stringify(workbases, null, 2))
|
|
37
|
+
} else {
|
|
38
|
+
for (const path of workbases) log(path)
|
|
39
|
+
}
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
default:
|
|
43
|
+
return yield* Effect.fail(
|
|
44
|
+
new Error("Subcommand is required. Available: add, list"),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const help = `
|
|
50
|
+
Usage: agency workbase <subcommand>
|
|
51
|
+
|
|
52
|
+
Subcommands:
|
|
53
|
+
add <path> Register an Agency workbase
|
|
54
|
+
list List registered workbases
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--json Output results as JSON
|
|
58
|
+
`
|