@markjaquith/agency 2.25.0 → 2.27.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 +24 -2
- package/cli.ts +21 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +10 -1
- package/src/cli-parser.test.ts +47 -0
- package/src/cli-parser.ts +110 -2
- package/src/cli.test.ts +34 -0
- package/src/commands/repo.test.ts +44 -0
- package/src/commands/repo.ts +107 -1
- package/src/commands/workbase.test.ts +47 -0
- package/src/commands/workbase.ts +52 -1
- package/src/commands/worktree.ts +138 -0
- package/src/services/FileSystemService.ts +26 -0
- package/src/services/RepositoryService.test.ts +129 -1
- package/src/services/RepositoryService.ts +181 -0
- package/src/services/WorkbaseService.test.ts +46 -0
- package/src/services/WorkbaseService.ts +94 -25
- package/src/services/WorktreeService.test.ts +453 -2
- package/src/services/WorktreeService.ts +1510 -655
package/README.md
CHANGED
|
@@ -379,14 +379,24 @@ conditions remain visible in `warnings` or `unresolved` with a suggested action.
|
|
|
379
379
|
agency init [path] [--json]
|
|
380
380
|
agency workbase add <path> [--name <name>] [--json]
|
|
381
381
|
agency workbase list [--json]
|
|
382
|
+
agency workbase show <id|name|path> [--json]
|
|
383
|
+
agency workbase name <id|name|path> <name> [--json]
|
|
384
|
+
agency workbase name <id|name|path> --clear [--json]
|
|
382
385
|
agency workbase remove <id|name|path> [--json]
|
|
383
386
|
agency workbase prune [--json]
|
|
384
|
-
agency workbase default [<id|name> | --clear] [--json]
|
|
387
|
+
agency workbase default [<id|name|path> | --clear] [--json]
|
|
385
388
|
agency integration status [--json]
|
|
386
389
|
agency integration sync [--json]
|
|
387
390
|
agency repo add <alias> <remote> [--json]
|
|
388
391
|
agency repo link <alias> <path> [--json]
|
|
389
392
|
agency repo list [--json]
|
|
393
|
+
agency repo show <alias> [--json]
|
|
394
|
+
agency repo fetch <alias> [--json]
|
|
395
|
+
agency repo remove <alias> [--json]
|
|
396
|
+
agency repo unlink <alias> [--json]
|
|
397
|
+
agency repo rename <alias> <new-alias> [--json]
|
|
398
|
+
agency repo remote <alias> [remote] [--json]
|
|
399
|
+
agency repo verify <alias> [--json]
|
|
390
400
|
```
|
|
391
401
|
|
|
392
402
|
Registered workbases are stored in
|
|
@@ -395,7 +405,9 @@ Each registration has a stable ID and may have a unique name. A default workbase
|
|
|
395
405
|
is used when the current directory is outside every workbase. `prune` removes
|
|
396
406
|
registrations whose workbase configuration no longer exists.
|
|
397
407
|
`repo add` creates a bare clone. `repo link` creates a symlink to an existing Git
|
|
398
|
-
repository. Alias names are then used by all documents and commands.
|
|
408
|
+
repository. Alias names are then used by all documents and commands. Remove,
|
|
409
|
+
unlink, and rename refuse aliases referenced by active work or backed by linked
|
|
410
|
+
worktrees, and report each blocker.
|
|
399
411
|
|
|
400
412
|
Commands that print Agency-owned results accept `--json`, including initialization,
|
|
401
413
|
integration inspection/sync, repository mutations, entity creation/list/show,
|
|
@@ -598,6 +610,7 @@ for restoration. Archived IDs are reserved until restored.
|
|
|
598
610
|
```text
|
|
599
611
|
agency work [<directory> | --epic <epic-id>] [--runner <name>] [--print-command]
|
|
600
612
|
agency work prepare [target] [--dry-run] [--json]
|
|
613
|
+
agency worktree <list|inspect|prepare|remove|rebuild|repair>
|
|
601
614
|
agency pr create <task-id> [phase-id] [--draft] [--json]
|
|
602
615
|
```
|
|
603
616
|
|
|
@@ -617,6 +630,15 @@ Its JSON result includes document and checkout paths, resolved commits, actions,
|
|
|
617
630
|
and Git operations. Use `--dry-run` to report planned fetch, branch, and worktree
|
|
618
631
|
changes without applying them.
|
|
619
632
|
|
|
633
|
+
`agency worktree list` and `inspect` report each declared checkout's expected and
|
|
634
|
+
registered path, branch, commit, Agency owner, dirtiness, and conflicts. `prepare`
|
|
635
|
+
is the explicit lifecycle form of `agency work prepare`. `remove`, `rebuild`, and
|
|
636
|
+
`repair` preflight every writable and reference checkout before changing any of
|
|
637
|
+
them and accept `--dry-run`. Removal preserves branches. Rebuild rejects dirty or
|
|
638
|
+
conflicting worktrees. Repair is deliberately conservative: it repairs safe Git
|
|
639
|
+
registration issues and materializes missing checkouts, but never switches a
|
|
640
|
+
branch, resets a commit, or discards uncommitted work.
|
|
641
|
+
|
|
620
642
|
Epic and multi-phase task targets launch orchestration agents beside their
|
|
621
643
|
documents. Single-phase tasks and phases fetch repositories, create or reuse
|
|
622
644
|
worktrees under `code/`, and launch an execution agent in the writable checkout
|
package/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { init, help as initHelp } from "./src/commands/init"
|
|
|
7
7
|
import { task, help as taskHelp } from "./src/commands/task"
|
|
8
8
|
import { pr, help as prHelp } from "./src/commands/pr"
|
|
9
9
|
import { work, workPrepare, help as workHelp } from "./src/commands/work"
|
|
10
|
+
import { worktree, help as worktreeHelp } from "./src/commands/worktree"
|
|
10
11
|
import { status, help as statusHelp } from "./src/commands/status"
|
|
11
12
|
import { validate, help as validateHelp } from "./src/commands/validate"
|
|
12
13
|
import { context, help as contextHelp } from "./src/commands/context"
|
|
@@ -470,6 +471,25 @@ const commands: Record<string, Command> = {
|
|
|
470
471
|
)
|
|
471
472
|
},
|
|
472
473
|
},
|
|
474
|
+
worktree: {
|
|
475
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
476
|
+
if (options.help) {
|
|
477
|
+
console.log(worktreeHelp)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
await runCommand(
|
|
481
|
+
worktree({
|
|
482
|
+
subcommand: args[0],
|
|
483
|
+
args: args.slice(1),
|
|
484
|
+
dryRun: options["dry-run"],
|
|
485
|
+
json: options.json,
|
|
486
|
+
silent: options.silent,
|
|
487
|
+
verbose: options.verbose,
|
|
488
|
+
cwd: options.cwd,
|
|
489
|
+
}),
|
|
490
|
+
)
|
|
491
|
+
},
|
|
492
|
+
},
|
|
473
493
|
next: {
|
|
474
494
|
run: async (_args: string[], options: Record<string, any>) => {
|
|
475
495
|
if (options.help) return console.log(nextHelp)
|
|
@@ -607,6 +627,7 @@ Commands:
|
|
|
607
627
|
archive <type> Archive a work item
|
|
608
628
|
task <subcommand> Manage tasks
|
|
609
629
|
work [directory|task] Work on an epic, task, or phase
|
|
630
|
+
worktree <subcommand> Inspect and maintain managed worktrees
|
|
610
631
|
next List or select ready execution units
|
|
611
632
|
pr create Create a pull request for an execution unit
|
|
612
633
|
repo <subcommand> Manage workbase repositories
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -65,7 +65,9 @@ Register and name known workbases so commands can select one from anywhere:
|
|
|
65
65
|
```bash
|
|
66
66
|
agency workbase add <path> [--name <name>]
|
|
67
67
|
agency workbase list
|
|
68
|
-
agency workbase
|
|
68
|
+
agency workbase show <id|name|path>
|
|
69
|
+
agency workbase name <id|name|path> <name> | --clear
|
|
70
|
+
agency workbase default [<id|name|path> | --clear]
|
|
69
71
|
agency workbase remove <id|name|path>
|
|
70
72
|
agency workbase prune
|
|
71
73
|
```
|
|
@@ -85,6 +87,13 @@ Link an existing local Git repository:
|
|
|
85
87
|
|
|
86
88
|
```bash
|
|
87
89
|
agency repo link <alias> <path>
|
|
90
|
+
agency repo show <alias>
|
|
91
|
+
agency repo fetch <alias>
|
|
92
|
+
agency repo remove <alias>
|
|
93
|
+
agency repo unlink <alias>
|
|
94
|
+
agency repo rename <alias> <new-alias>
|
|
95
|
+
agency repo remote <alias> [remote]
|
|
96
|
+
agency repo verify <alias>
|
|
88
97
|
```
|
|
89
98
|
|
|
90
99
|
Use aliases, never absolute paths or Git URLs, in epic/task/phase frontmatter.
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -121,6 +121,23 @@ describe("strict CLI parsing", () => {
|
|
|
121
121
|
expect(parseCli(["status", "--no-pr"]).values["no-pr"]).toBe(true)
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
+
test("parses addressable resource maintenance commands", () => {
|
|
125
|
+
for (const args of [
|
|
126
|
+
["repo", "show", "agency", "--json"],
|
|
127
|
+
["repo", "fetch", "agency"],
|
|
128
|
+
["repo", "remove", "agency"],
|
|
129
|
+
["repo", "unlink", "agency"],
|
|
130
|
+
["repo", "rename", "agency", "renamed"],
|
|
131
|
+
["repo", "remote", "agency", "https://example.com/repo.git"],
|
|
132
|
+
["repo", "verify", "agency"],
|
|
133
|
+
["workbase", "show", "primary", "--json"],
|
|
134
|
+
["workbase", "name", "primary", "renamed"],
|
|
135
|
+
["workbase", "name", "primary", "--clear"],
|
|
136
|
+
]) {
|
|
137
|
+
expect(() => parseCli(args)).not.toThrow()
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
124
141
|
test("validates view filter values and conflicts", () => {
|
|
125
142
|
expect(() => parseCli(["epic", "list", "--status", "invalid"])).toThrow(
|
|
126
143
|
"Invalid '--status' value",
|
|
@@ -387,6 +404,36 @@ describe("strict CLI parsing", () => {
|
|
|
387
404
|
)
|
|
388
405
|
})
|
|
389
406
|
|
|
407
|
+
test("parses worktree lifecycle commands and selectors", () => {
|
|
408
|
+
expect(parseCli(["worktree", "list", "--json"])).toMatchObject({
|
|
409
|
+
commandName: "worktree",
|
|
410
|
+
args: ["list"],
|
|
411
|
+
values: { json: true },
|
|
412
|
+
})
|
|
413
|
+
expect(
|
|
414
|
+
parseCli([
|
|
415
|
+
"worktree",
|
|
416
|
+
"rebuild",
|
|
417
|
+
"--task",
|
|
418
|
+
"example",
|
|
419
|
+
"--phase",
|
|
420
|
+
"verify",
|
|
421
|
+
"--dry-run",
|
|
422
|
+
]),
|
|
423
|
+
).toMatchObject({
|
|
424
|
+
commandName: "worktree",
|
|
425
|
+
args: ["rebuild", "example", "verify"],
|
|
426
|
+
values: { task: "example", phase: "verify", "dry-run": true },
|
|
427
|
+
})
|
|
428
|
+
expectUsageError(
|
|
429
|
+
["worktree", "inspect", "example", "--dry-run"],
|
|
430
|
+
"agency worktree inspect",
|
|
431
|
+
)
|
|
432
|
+
expect(() => parseCli(["worktree", "repair", "--phase", "verify"])).toThrow(
|
|
433
|
+
"requires '--task'",
|
|
434
|
+
)
|
|
435
|
+
})
|
|
436
|
+
|
|
390
437
|
test("accepts runner selection and command inspection for work", () => {
|
|
391
438
|
expect(
|
|
392
439
|
parseCli(["work", "example", "--runner", "custom", "--print-command"]),
|
package/src/cli-parser.ts
CHANGED
|
@@ -127,7 +127,7 @@ const commands = {
|
|
|
127
127
|
},
|
|
128
128
|
},
|
|
129
129
|
workbase: {
|
|
130
|
-
usage: "agency workbase <add|list|remove|prune|default>",
|
|
130
|
+
usage: "agency workbase <add|list|show|name|remove|prune|default>",
|
|
131
131
|
options: {
|
|
132
132
|
...outputOptions,
|
|
133
133
|
name: { type: "string" },
|
|
@@ -146,6 +146,18 @@ const commands = {
|
|
|
146
146
|
maxArgs: 0,
|
|
147
147
|
options: ["json"],
|
|
148
148
|
},
|
|
149
|
+
show: {
|
|
150
|
+
usage: "agency workbase show <selector> [--json]",
|
|
151
|
+
minArgs: 1,
|
|
152
|
+
maxArgs: 1,
|
|
153
|
+
options: ["json"],
|
|
154
|
+
},
|
|
155
|
+
name: {
|
|
156
|
+
usage: "agency workbase name <selector> <name> | --clear [--json]",
|
|
157
|
+
minArgs: 1,
|
|
158
|
+
maxArgs: 2,
|
|
159
|
+
options: ["json", "clear"],
|
|
160
|
+
},
|
|
149
161
|
remove: {
|
|
150
162
|
usage: "agency workbase remove <selector> [--json]",
|
|
151
163
|
minArgs: 1,
|
|
@@ -186,7 +198,8 @@ const commands = {
|
|
|
186
198
|
},
|
|
187
199
|
},
|
|
188
200
|
repo: {
|
|
189
|
-
usage:
|
|
201
|
+
usage:
|
|
202
|
+
"agency repo <add|link|list|show|fetch|remove|unlink|rename|remote|verify>",
|
|
190
203
|
options: outputOptions,
|
|
191
204
|
subcommands: {
|
|
192
205
|
add: {
|
|
@@ -207,6 +220,48 @@ const commands = {
|
|
|
207
220
|
maxArgs: 0,
|
|
208
221
|
options: ["json"],
|
|
209
222
|
},
|
|
223
|
+
show: {
|
|
224
|
+
usage: "agency repo show <alias> [--json]",
|
|
225
|
+
minArgs: 1,
|
|
226
|
+
maxArgs: 1,
|
|
227
|
+
options: ["json"],
|
|
228
|
+
},
|
|
229
|
+
fetch: {
|
|
230
|
+
usage: "agency repo fetch <alias> [--json]",
|
|
231
|
+
minArgs: 1,
|
|
232
|
+
maxArgs: 1,
|
|
233
|
+
options: ["json"],
|
|
234
|
+
},
|
|
235
|
+
remove: {
|
|
236
|
+
usage: "agency repo remove <alias> [--json]",
|
|
237
|
+
minArgs: 1,
|
|
238
|
+
maxArgs: 1,
|
|
239
|
+
options: ["json"],
|
|
240
|
+
},
|
|
241
|
+
unlink: {
|
|
242
|
+
usage: "agency repo unlink <alias> [--json]",
|
|
243
|
+
minArgs: 1,
|
|
244
|
+
maxArgs: 1,
|
|
245
|
+
options: ["json"],
|
|
246
|
+
},
|
|
247
|
+
rename: {
|
|
248
|
+
usage: "agency repo rename <alias> <new-alias> [--json]",
|
|
249
|
+
minArgs: 2,
|
|
250
|
+
maxArgs: 2,
|
|
251
|
+
options: ["json"],
|
|
252
|
+
},
|
|
253
|
+
remote: {
|
|
254
|
+
usage: "agency repo remote <alias> [remote] [--json]",
|
|
255
|
+
minArgs: 1,
|
|
256
|
+
maxArgs: 2,
|
|
257
|
+
options: ["json"],
|
|
258
|
+
},
|
|
259
|
+
verify: {
|
|
260
|
+
usage: "agency repo verify <alias> [--json]",
|
|
261
|
+
minArgs: 1,
|
|
262
|
+
maxArgs: 1,
|
|
263
|
+
options: ["json"],
|
|
264
|
+
},
|
|
210
265
|
},
|
|
211
266
|
},
|
|
212
267
|
epic: {
|
|
@@ -616,6 +671,56 @@ const commands = {
|
|
|
616
671
|
},
|
|
617
672
|
},
|
|
618
673
|
},
|
|
674
|
+
worktree: {
|
|
675
|
+
usage: "agency worktree <list|inspect|prepare|remove|rebuild|repair>",
|
|
676
|
+
options: {
|
|
677
|
+
...outputOptions,
|
|
678
|
+
...entitySelectorOptions,
|
|
679
|
+
"dry-run": { type: "boolean" },
|
|
680
|
+
},
|
|
681
|
+
subcommands: {
|
|
682
|
+
list: {
|
|
683
|
+
usage: "agency worktree list [--json]",
|
|
684
|
+
minArgs: 0,
|
|
685
|
+
maxArgs: 0,
|
|
686
|
+
options: ["json"],
|
|
687
|
+
},
|
|
688
|
+
inspect: {
|
|
689
|
+
usage: "agency worktree inspect <task-id> [phase-id] [--json]",
|
|
690
|
+
minArgs: 1,
|
|
691
|
+
maxArgs: 2,
|
|
692
|
+
options: ["json", "task", "phase"],
|
|
693
|
+
},
|
|
694
|
+
prepare: {
|
|
695
|
+
usage:
|
|
696
|
+
"agency worktree prepare <task-id> [phase-id] [--dry-run] [--json]",
|
|
697
|
+
minArgs: 1,
|
|
698
|
+
maxArgs: 2,
|
|
699
|
+
options: ["dry-run", "json", "task", "phase"],
|
|
700
|
+
},
|
|
701
|
+
remove: {
|
|
702
|
+
usage:
|
|
703
|
+
"agency worktree remove <task-id> [phase-id] [--dry-run] [--json]",
|
|
704
|
+
minArgs: 1,
|
|
705
|
+
maxArgs: 2,
|
|
706
|
+
options: ["dry-run", "json", "task", "phase"],
|
|
707
|
+
},
|
|
708
|
+
rebuild: {
|
|
709
|
+
usage:
|
|
710
|
+
"agency worktree rebuild <task-id> [phase-id] [--dry-run] [--json]",
|
|
711
|
+
minArgs: 1,
|
|
712
|
+
maxArgs: 2,
|
|
713
|
+
options: ["dry-run", "json", "task", "phase"],
|
|
714
|
+
},
|
|
715
|
+
repair: {
|
|
716
|
+
usage:
|
|
717
|
+
"agency worktree repair <task-id> [phase-id] [--dry-run] [--json]",
|
|
718
|
+
minArgs: 1,
|
|
719
|
+
maxArgs: 2,
|
|
720
|
+
options: ["dry-run", "json", "task", "phase"],
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
},
|
|
619
724
|
work: {
|
|
620
725
|
usage:
|
|
621
726
|
"agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] | agency work prepare [target] [--dry-run] [--json]",
|
|
@@ -847,6 +952,9 @@ const targetSlots = (
|
|
|
847
952
|
: subcommand === "phase"
|
|
848
953
|
? ["task", "phase"]
|
|
849
954
|
: []
|
|
955
|
+
if (commandName === "worktree" && subcommand !== "list") {
|
|
956
|
+
return ["task", "phase"]
|
|
957
|
+
}
|
|
850
958
|
if (commandName === "pr" && subcommand === "create") return ["task", "phase"]
|
|
851
959
|
return []
|
|
852
960
|
}
|
package/src/cli.test.ts
CHANGED
|
@@ -692,6 +692,40 @@ status: open
|
|
|
692
692
|
await runCli(["task", "show", "example", "--json"], root),
|
|
693
693
|
)
|
|
694
694
|
expect(task.data.status).toBe("open")
|
|
695
|
+
|
|
696
|
+
const listed = parseJson(await runCli(["worktree", "list", "--json"], root))
|
|
697
|
+
expect(listed).toEqual([
|
|
698
|
+
expect.objectContaining({
|
|
699
|
+
owner: expect.objectContaining({ kind: "task", taskId: "example" }),
|
|
700
|
+
checkouts: [
|
|
701
|
+
expect.objectContaining({
|
|
702
|
+
repo: "agency",
|
|
703
|
+
kind: "writable",
|
|
704
|
+
registered: true,
|
|
705
|
+
actualBranch: "feat/example",
|
|
706
|
+
actualCommit: expect.stringMatching(/^[0-9a-f]{40}$/),
|
|
707
|
+
dirty: false,
|
|
708
|
+
}),
|
|
709
|
+
],
|
|
710
|
+
}),
|
|
711
|
+
])
|
|
712
|
+
const rebuild = parseJson(
|
|
713
|
+
await runCli(
|
|
714
|
+
["worktree", "rebuild", "example", "--dry-run", "--json"],
|
|
715
|
+
root,
|
|
716
|
+
),
|
|
717
|
+
)
|
|
718
|
+
expect(rebuild).toMatchObject({
|
|
719
|
+
operation: "rebuild",
|
|
720
|
+
dryRun: true,
|
|
721
|
+
actions: [
|
|
722
|
+
`remove ${join(workbaseRoot, "tasks/example/code/agency")}`,
|
|
723
|
+
`create ${join(workbaseRoot, "tasks/example/code/agency")}`,
|
|
724
|
+
],
|
|
725
|
+
})
|
|
726
|
+
expect(
|
|
727
|
+
await Bun.file(join(root, "tasks/example/code/agency/README.md")).text(),
|
|
728
|
+
).toBe("example\n")
|
|
695
729
|
})
|
|
696
730
|
|
|
697
731
|
test("envelopes help and version output in machine mode", async () => {
|
|
@@ -52,6 +52,21 @@ describe("repo command", () => {
|
|
|
52
52
|
])
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
+
test("shows a repository by alias as JSON", async () => {
|
|
56
|
+
const logs = await captureLogs(() =>
|
|
57
|
+
runTestEffect(
|
|
58
|
+
repo({
|
|
59
|
+
subcommand: "show",
|
|
60
|
+
args: ["agency"],
|
|
61
|
+
cwd: root,
|
|
62
|
+
json: true,
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(JSON.parse(logs[0]!).alias).toBe("agency")
|
|
68
|
+
})
|
|
69
|
+
|
|
55
70
|
test("outputs a linked repository as JSON", async () => {
|
|
56
71
|
const target = join(root, "source")
|
|
57
72
|
await mkdir(target)
|
|
@@ -77,4 +92,33 @@ describe("repo command", () => {
|
|
|
77
92
|
path: join(root, "repos/linked"),
|
|
78
93
|
})
|
|
79
94
|
})
|
|
95
|
+
|
|
96
|
+
test("unlinks a linked repository by alias", async () => {
|
|
97
|
+
const target = join(root, "unlink-source")
|
|
98
|
+
await mkdir(target)
|
|
99
|
+
const git = Bun.spawn(["git", "init", target], {
|
|
100
|
+
stdout: "ignore",
|
|
101
|
+
stderr: "ignore",
|
|
102
|
+
})
|
|
103
|
+
expect(await git.exited).toBe(0)
|
|
104
|
+
await runTestEffect(
|
|
105
|
+
repo({
|
|
106
|
+
subcommand: "link",
|
|
107
|
+
args: ["linked", target],
|
|
108
|
+
cwd: root,
|
|
109
|
+
silent: true,
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
await runTestEffect(
|
|
114
|
+
repo({
|
|
115
|
+
subcommand: "unlink",
|
|
116
|
+
args: ["linked"],
|
|
117
|
+
cwd: root,
|
|
118
|
+
silent: true,
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
expect(await Bun.file(join(target, ".git/HEAD")).exists()).toBe(true)
|
|
123
|
+
})
|
|
80
124
|
})
|
package/src/commands/repo.ts
CHANGED
|
@@ -9,6 +9,9 @@ interface RepoOptions extends BaseCommandOptions {
|
|
|
9
9
|
readonly json?: boolean
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const requireArg = (args: readonly string[], index: number, usage: string) =>
|
|
13
|
+
args[index] ? Effect.succeed(args[index]) : Effect.fail(new Error(usage))
|
|
14
|
+
|
|
12
15
|
export const repo = (options: RepoOptions) =>
|
|
13
16
|
Effect.gen(function* () {
|
|
14
17
|
const repositories = yield* RepositoryService
|
|
@@ -61,10 +64,106 @@ export const repo = (options: RepoOptions) =>
|
|
|
61
64
|
return
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
case "show": {
|
|
68
|
+
const alias = yield* requireArg(
|
|
69
|
+
options.args,
|
|
70
|
+
0,
|
|
71
|
+
"Usage: agency repo show <alias>",
|
|
72
|
+
)
|
|
73
|
+
const item = yield* repositories.show(alias, cwd)
|
|
74
|
+
log(
|
|
75
|
+
options.json
|
|
76
|
+
? JSON.stringify(item, null, 2)
|
|
77
|
+
: `${item.alias}\t${item.kind}\t${item.target ?? item.remote ?? item.path}`,
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case "fetch": {
|
|
83
|
+
const alias = yield* requireArg(
|
|
84
|
+
options.args,
|
|
85
|
+
0,
|
|
86
|
+
"Usage: agency repo fetch <alias>",
|
|
87
|
+
)
|
|
88
|
+
const item = yield* repositories.fetch(alias, cwd)
|
|
89
|
+
log(options.json ? JSON.stringify(item, null, 2) : `Fetched '${alias}'`)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "remove":
|
|
94
|
+
case "unlink": {
|
|
95
|
+
const alias = yield* requireArg(
|
|
96
|
+
options.args,
|
|
97
|
+
0,
|
|
98
|
+
`Usage: agency repo ${options.subcommand} <alias>`,
|
|
99
|
+
)
|
|
100
|
+
const item = yield* repositories[options.subcommand](alias, cwd)
|
|
101
|
+
log(
|
|
102
|
+
options.json
|
|
103
|
+
? JSON.stringify(item, null, 2)
|
|
104
|
+
: `${options.subcommand === "unlink" ? "Unlinked" : "Removed"} '${alias}'`,
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case "rename": {
|
|
110
|
+
const alias = yield* requireArg(
|
|
111
|
+
options.args,
|
|
112
|
+
0,
|
|
113
|
+
"Usage: agency repo rename <alias> <new-alias>",
|
|
114
|
+
)
|
|
115
|
+
const newAlias = yield* requireArg(
|
|
116
|
+
options.args,
|
|
117
|
+
1,
|
|
118
|
+
"Usage: agency repo rename <alias> <new-alias>",
|
|
119
|
+
)
|
|
120
|
+
const item = yield* repositories.rename(alias, newAlias, cwd)
|
|
121
|
+
log(
|
|
122
|
+
options.json
|
|
123
|
+
? JSON.stringify(item, null, 2)
|
|
124
|
+
: `Renamed '${alias}' to '${newAlias}'`,
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case "remote": {
|
|
130
|
+
const alias = yield* requireArg(
|
|
131
|
+
options.args,
|
|
132
|
+
0,
|
|
133
|
+
"Usage: agency repo remote <alias> [remote]",
|
|
134
|
+
)
|
|
135
|
+
const item = yield* repositories.remote(alias, options.args[1], cwd)
|
|
136
|
+
log(
|
|
137
|
+
options.json
|
|
138
|
+
? JSON.stringify(item, null, 2)
|
|
139
|
+
: (item.remote ?? "No origin remote configured"),
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "verify": {
|
|
145
|
+
const alias = yield* requireArg(
|
|
146
|
+
options.args,
|
|
147
|
+
0,
|
|
148
|
+
"Usage: agency repo verify <alias>",
|
|
149
|
+
)
|
|
150
|
+
const report = yield* repositories.verify(alias, cwd)
|
|
151
|
+
if (options.json) log(JSON.stringify(report, null, 2))
|
|
152
|
+
if (!report.valid) {
|
|
153
|
+
return yield* Effect.fail(
|
|
154
|
+
new Error(
|
|
155
|
+
`Repository '${alias}' verification failed:\n${report.issues.map((issue) => `- ${issue}`).join("\n")}`,
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
if (!options.json) log(`Verified repository '${alias}'`)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
64
163
|
default:
|
|
65
164
|
return yield* Effect.fail(
|
|
66
165
|
new Error(
|
|
67
|
-
"Subcommand is required. Available subcommands: add, link, list",
|
|
166
|
+
"Subcommand is required. Available subcommands: add, link, list, show, fetch, remove, unlink, rename, remote, verify",
|
|
68
167
|
),
|
|
69
168
|
)
|
|
70
169
|
}
|
|
@@ -77,6 +176,13 @@ Subcommands:
|
|
|
77
176
|
add <alias> <remote> Create a bare clone
|
|
78
177
|
link <alias> <path> Link an existing Git repository
|
|
79
178
|
list List repository aliases
|
|
179
|
+
show <alias> Show a repository alias
|
|
180
|
+
fetch <alias> Fetch and prune a repository
|
|
181
|
+
remove <alias> Remove an unused repository alias
|
|
182
|
+
unlink <alias> Remove an unused linked alias
|
|
183
|
+
rename <old> <new> Rename an unused repository alias
|
|
184
|
+
remote <alias> [url] Show or update the origin remote
|
|
185
|
+
verify <alias> Verify repository operation
|
|
80
186
|
|
|
81
187
|
Options:
|
|
82
188
|
--json Output repository aliases as JSON
|
|
@@ -108,6 +108,53 @@ describe("workbase command", () => {
|
|
|
108
108
|
).toEqual({ version: 2, workbases: [] })
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
+
test("names, shows, and clears a workbase name", async () => {
|
|
112
|
+
const added = await captureLogs(() =>
|
|
113
|
+
runTestEffect(
|
|
114
|
+
workbase({
|
|
115
|
+
subcommand: "add",
|
|
116
|
+
args: [root],
|
|
117
|
+
configDirectory,
|
|
118
|
+
json: true,
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
const registration = JSON.parse(added[0]!)
|
|
123
|
+
await runTestEffect(
|
|
124
|
+
workbase({
|
|
125
|
+
subcommand: "name",
|
|
126
|
+
args: [registration.id, "primary"],
|
|
127
|
+
configDirectory,
|
|
128
|
+
silent: true,
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const shown = await captureLogs(() =>
|
|
133
|
+
runTestEffect(
|
|
134
|
+
workbase({
|
|
135
|
+
subcommand: "show",
|
|
136
|
+
args: ["primary"],
|
|
137
|
+
configDirectory,
|
|
138
|
+
json: true,
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
expect(JSON.parse(shown[0]!).name).toBe("primary")
|
|
143
|
+
|
|
144
|
+
await runTestEffect(
|
|
145
|
+
workbase({
|
|
146
|
+
subcommand: "name",
|
|
147
|
+
args: [registration.id],
|
|
148
|
+
clear: true,
|
|
149
|
+
configDirectory,
|
|
150
|
+
silent: true,
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
expect(
|
|
154
|
+
await Bun.file(join(configDirectory, "agency/workbases.json")).json(),
|
|
155
|
+
).toEqual({ version: 2, workbases: [registration] })
|
|
156
|
+
})
|
|
157
|
+
|
|
111
158
|
test("requires an add path", async () => {
|
|
112
159
|
await expect(
|
|
113
160
|
runTestEffect(
|