@markjaquith/agency 2.24.0 → 2.26.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 +14 -2
- package/cli.ts +10 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +10 -1
- package/src/cli-parser.test.ts +26 -0
- package/src/cli-parser.ts +57 -2
- package/src/commands/archive.test.ts +22 -0
- package/src/commands/archive.ts +3 -1
- 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/services/ArchiveService.test.ts +59 -0
- package/src/services/ArchiveService.ts +282 -113
- package/src/services/LifecycleTransaction.test.ts +107 -0
- package/src/services/LifecycleTransaction.ts +302 -0
- package/src/services/PhaseService.ts +156 -48
- package/src/services/RepositoryService.test.ts +129 -1
- package/src/services/RepositoryService.ts +181 -0
- package/src/services/TaskPhaseService.test.ts +47 -1
- package/src/services/TaskService.ts +28 -4
- package/src/services/WorkbaseService.test.ts +46 -0
- package/src/services/WorkbaseService.ts +94 -25
- package/src/services/WorktreeLock.ts +60 -0
- package/src/services/WorktreeService.test.ts +174 -1
- package/src/services/WorktreeService.ts +972 -523
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,
|
package/cli.ts
CHANGED
|
@@ -683,6 +683,7 @@ try {
|
|
|
683
683
|
}
|
|
684
684
|
if (error instanceof Error) {
|
|
685
685
|
let message = error.message
|
|
686
|
+
let details: any = error
|
|
686
687
|
|
|
687
688
|
// Handle Effect FiberFailure errors that wrap tagged errors
|
|
688
689
|
// When the message is generic "An error has occurred", try to extract the actual error
|
|
@@ -695,6 +696,7 @@ try {
|
|
|
695
696
|
const cause = (error as any)[causeSymbol]
|
|
696
697
|
if (cause && cause._tag === "Fail" && cause.failure) {
|
|
697
698
|
const failure = cause.failure
|
|
699
|
+
details = failure
|
|
698
700
|
// Try common error message patterns
|
|
699
701
|
message =
|
|
700
702
|
failure.message ||
|
|
@@ -705,6 +707,14 @@ try {
|
|
|
705
707
|
}
|
|
706
708
|
}
|
|
707
709
|
}
|
|
710
|
+
for (const [field, label] of [
|
|
711
|
+
["completed", "Completed"],
|
|
712
|
+
["rolledBack", "Rolled back"],
|
|
713
|
+
["manualRecovery", "Manual recovery"],
|
|
714
|
+
] as const) {
|
|
715
|
+
if (Array.isArray(details[field]) && details[field].length > 0)
|
|
716
|
+
message += `\n${label}: ${details[field].join("; ")}`
|
|
717
|
+
}
|
|
708
718
|
|
|
709
719
|
console.error(`ⓘ ${message}`)
|
|
710
720
|
} else {
|
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",
|
|
@@ -292,6 +309,15 @@ describe("strict CLI parsing", () => {
|
|
|
292
309
|
)
|
|
293
310
|
})
|
|
294
311
|
|
|
312
|
+
test("accepts archive dry-run", () => {
|
|
313
|
+
expect(parseCli(["archive", "task", "example", "--dry-run"])).toMatchObject(
|
|
314
|
+
{
|
|
315
|
+
commandName: "archive",
|
|
316
|
+
values: { "dry-run": true },
|
|
317
|
+
},
|
|
318
|
+
)
|
|
319
|
+
})
|
|
320
|
+
|
|
295
321
|
test("validates revision-guarded claim operations", () => {
|
|
296
322
|
const revision = "0".repeat(64)
|
|
297
323
|
expect(
|
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: {
|
|
@@ -63,6 +63,28 @@ describe("archive command", () => {
|
|
|
63
63
|
})
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
+
test("preflights an archive without moving the task", async () => {
|
|
67
|
+
const logs = await captureLogs(() =>
|
|
68
|
+
runTestEffect(
|
|
69
|
+
archive({
|
|
70
|
+
type: "task",
|
|
71
|
+
args: ["example"],
|
|
72
|
+
cwd: root,
|
|
73
|
+
dryRun: true,
|
|
74
|
+
json: true,
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect(JSON.parse(logs[0]!).dryRun).toBe(true)
|
|
80
|
+
expect(await Bun.file(join(root, "tasks/example/TASK.md")).exists()).toBe(
|
|
81
|
+
true,
|
|
82
|
+
)
|
|
83
|
+
expect(await Bun.file(join(root, "archive/tasks/example")).exists()).toBe(
|
|
84
|
+
false,
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
66
88
|
test("requires a supported work item type", async () => {
|
|
67
89
|
await expect(
|
|
68
90
|
runTestEffect(archive({ args: [], cwd: root, silent: true })),
|
package/src/commands/archive.ts
CHANGED
|
@@ -112,7 +112,9 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
112
112
|
log(
|
|
113
113
|
options.json
|
|
114
114
|
? JSON.stringify(result, null, 2)
|
|
115
|
-
:
|
|
115
|
+
: result.dryRun
|
|
116
|
+
? `Would archive ${result.kind} '${result.id}' to ${result.path}`
|
|
117
|
+
: `Archived ${result.kind} '${result.id}' to ${result.path}`,
|
|
116
118
|
)
|
|
117
119
|
})
|
|
118
120
|
|
|
@@ -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(
|
package/src/commands/workbase.ts
CHANGED
|
@@ -51,6 +51,54 @@ export const workbase = (options: WorkbaseOptions) =>
|
|
|
51
51
|
}
|
|
52
52
|
return
|
|
53
53
|
}
|
|
54
|
+
case "show": {
|
|
55
|
+
const selector = options.args[0]
|
|
56
|
+
if (!selector) {
|
|
57
|
+
return yield* Effect.fail(
|
|
58
|
+
new Error("Usage: agency workbase show <selector>"),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
const entry = yield* service.showRegistered(
|
|
62
|
+
selector,
|
|
63
|
+
options.configDirectory,
|
|
64
|
+
options.cwd,
|
|
65
|
+
)
|
|
66
|
+
log(
|
|
67
|
+
options.json
|
|
68
|
+
? JSON.stringify(entry, null, 2)
|
|
69
|
+
: `${entry.name ?? entry.id}\t${entry.path}`,
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
case "name": {
|
|
74
|
+
const selector = options.args[0]
|
|
75
|
+
const name = options.clear ? null : options.args[1]
|
|
76
|
+
if (
|
|
77
|
+
!selector ||
|
|
78
|
+
name === undefined ||
|
|
79
|
+
(options.clear && options.args[1] !== undefined)
|
|
80
|
+
) {
|
|
81
|
+
return yield* Effect.fail(
|
|
82
|
+
new Error(
|
|
83
|
+
"Usage: agency workbase name <selector> <name> | --clear",
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
const entry = yield* service.nameRegistered(
|
|
88
|
+
selector,
|
|
89
|
+
name,
|
|
90
|
+
options.configDirectory,
|
|
91
|
+
options.cwd,
|
|
92
|
+
)
|
|
93
|
+
log(
|
|
94
|
+
options.json
|
|
95
|
+
? JSON.stringify(entry, null, 2)
|
|
96
|
+
: name === null
|
|
97
|
+
? `Cleared name for ${entry.id}`
|
|
98
|
+
: `Named workbase ${name}`,
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
54
102
|
case "remove": {
|
|
55
103
|
const entry = yield* service.removeRegistered(
|
|
56
104
|
options.args[0]!,
|
|
@@ -89,6 +137,7 @@ export const workbase = (options: WorkbaseOptions) =>
|
|
|
89
137
|
const entry = yield* service.setDefault(
|
|
90
138
|
selector,
|
|
91
139
|
options.configDirectory,
|
|
140
|
+
options.cwd,
|
|
92
141
|
)
|
|
93
142
|
log(
|
|
94
143
|
options.json
|
|
@@ -102,7 +151,7 @@ export const workbase = (options: WorkbaseOptions) =>
|
|
|
102
151
|
default:
|
|
103
152
|
return yield* Effect.fail(
|
|
104
153
|
new Error(
|
|
105
|
-
"Subcommand is required. Available: add, list, remove, prune, default",
|
|
154
|
+
"Subcommand is required. Available: add, list, show, name, remove, prune, default",
|
|
106
155
|
),
|
|
107
156
|
)
|
|
108
157
|
}
|
|
@@ -114,6 +163,8 @@ Usage: agency workbase <subcommand>
|
|
|
114
163
|
Subcommands:
|
|
115
164
|
add <path> Register an Agency workbase
|
|
116
165
|
list List registered workbases
|
|
166
|
+
show <selector> Show a registered workbase
|
|
167
|
+
name <selector> Set or clear a registered workbase name
|
|
117
168
|
remove <selector> Remove a registered workbase
|
|
118
169
|
prune Remove registrations whose paths no longer exist
|
|
119
170
|
default [selector] Show or set the default workbase
|
|
@@ -850,4 +850,63 @@ describe("ArchiveService", () => {
|
|
|
850
850
|
),
|
|
851
851
|
).rejects.toThrow("is archived; restore it")
|
|
852
852
|
})
|
|
853
|
+
|
|
854
|
+
test("preflights every phase worktree before removing any of them", async () => {
|
|
855
|
+
await runTestEffect(
|
|
856
|
+
TaskService.pipe(
|
|
857
|
+
Effect.flatMap((service) =>
|
|
858
|
+
service.create(
|
|
859
|
+
{
|
|
860
|
+
id: "multi-dirty",
|
|
861
|
+
ticketUrl: "https://example.com/task",
|
|
862
|
+
multiPhase: true,
|
|
863
|
+
},
|
|
864
|
+
root,
|
|
865
|
+
),
|
|
866
|
+
),
|
|
867
|
+
),
|
|
868
|
+
)
|
|
869
|
+
for (const id of ["clean", "dirty"]) {
|
|
870
|
+
await runTestEffect(
|
|
871
|
+
PhaseService.pipe(
|
|
872
|
+
Effect.flatMap((service) =>
|
|
873
|
+
service.create(
|
|
874
|
+
{
|
|
875
|
+
taskId: "multi-dirty",
|
|
876
|
+
id,
|
|
877
|
+
repo: "agency",
|
|
878
|
+
branch: `task/${id}`,
|
|
879
|
+
base: "main",
|
|
880
|
+
},
|
|
881
|
+
root,
|
|
882
|
+
),
|
|
883
|
+
),
|
|
884
|
+
),
|
|
885
|
+
)
|
|
886
|
+
await runTestEffect(
|
|
887
|
+
WorktreeService.pipe(
|
|
888
|
+
Effect.flatMap((service) =>
|
|
889
|
+
service.materialize("multi-dirty", id, root),
|
|
890
|
+
),
|
|
891
|
+
),
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
await Bun.write(
|
|
895
|
+
join(root, "tasks/multi-dirty/phases/dirty/code/agency/dirty.txt"),
|
|
896
|
+
"keep me\n",
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
await expect(
|
|
900
|
+
runTestEffect(
|
|
901
|
+
ArchiveService.pipe(
|
|
902
|
+
Effect.flatMap((service) => service.archiveTask("multi-dirty", root)),
|
|
903
|
+
),
|
|
904
|
+
),
|
|
905
|
+
).rejects.toThrow("uncommitted changes")
|
|
906
|
+
expect(
|
|
907
|
+
await Bun.file(
|
|
908
|
+
join(root, "tasks/multi-dirty/phases/clean/code/agency/README.md"),
|
|
909
|
+
).exists(),
|
|
910
|
+
).toBe(true)
|
|
911
|
+
})
|
|
853
912
|
})
|