@markjaquith/agency 2.20.0 → 2.21.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 +23 -0
- package/cli.ts +13 -0
- package/package.json +1 -1
- package/src/cli-parser.test.ts +45 -1
- package/src/cli-parser.ts +158 -5
- package/src/cli.test.ts +62 -0
- package/src/commands/epic.ts +51 -1
- package/src/commands/phase.ts +94 -1
- package/src/commands/task.ts +108 -1
- package/src/services/GraphMutationService.test.ts +336 -0
- package/src/services/GraphMutationService.ts +952 -0
- package/src/services/WorkbaseService.ts +3 -37
- package/src/test-utils.ts +2 -0
- package/src/workbase/dependency-graph.ts +51 -0
package/README.md
CHANGED
|
@@ -408,6 +408,9 @@ agency epic create <id> --ticket-url <url> [--description <text>] [--json]
|
|
|
408
408
|
--repo <alias>:<ref> [--repo <alias>:<ref>...]
|
|
409
409
|
agency epic list [filters] [--json]
|
|
410
410
|
agency epic show <id> [--json]
|
|
411
|
+
agency epic update <id> [--ticket-url <url>] [--description <text>]
|
|
412
|
+
[--clear-description] [--repo <alias>:<ref>...] [--json]
|
|
413
|
+
agency epic rename <id> <new-id> [--json]
|
|
411
414
|
```
|
|
412
415
|
|
|
413
416
|
Creating a task with `--epic <id>` adds the task to the epic and writes the task
|
|
@@ -463,8 +466,18 @@ Inspect tasks:
|
|
|
463
466
|
agency task list [filters] [--json]
|
|
464
467
|
agency task show <id> [--json]
|
|
465
468
|
agency task status <id> <open|done|dropped> [--json]
|
|
469
|
+
agency task update <id> [metadata options] [--json]
|
|
470
|
+
agency task rename <id> <new-id> [--json]
|
|
471
|
+
agency task move <id> (--epic <epic-id> | --no-epic) [--json]
|
|
472
|
+
agency task dependency <add|remove> <task-id> <dependency-id> [--json]
|
|
466
473
|
```
|
|
467
474
|
|
|
475
|
+
Task updates can replace or clear descriptions, tickets, repository references,
|
|
476
|
+
and pull request URLs, or replace writable repository, branch, and base metadata.
|
|
477
|
+
Execution metadata changes refuse to run while code is materialized. Moving a
|
|
478
|
+
task with scoped incoming or outgoing dependencies also refuses until those
|
|
479
|
+
dependencies are removed.
|
|
480
|
+
|
|
468
481
|
To add a phase to an existing single-phase task, name the phase that will own
|
|
469
482
|
the task's current execution fields with `--first-phase`:
|
|
470
483
|
|
|
@@ -490,8 +503,18 @@ agency phase create <task-id> <phase-id>
|
|
|
490
503
|
agency phase list <task-id> [filters] [--json]
|
|
491
504
|
agency phase show <task-id> <phase-id> [--json]
|
|
492
505
|
agency phase status <task-id> <phase-id> <open|done|dropped> [--json]
|
|
506
|
+
agency phase update <task-id> <phase-id> [metadata options] [--json]
|
|
507
|
+
agency phase rename <task-id> <phase-id> <new-id> [--json]
|
|
508
|
+
agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id>
|
|
509
|
+
[--json]
|
|
493
510
|
```
|
|
494
511
|
|
|
512
|
+
Dependency additions append without reordering existing declarations and reject
|
|
513
|
+
unknown IDs, self-dependencies, and cycles. Rename operations update structured
|
|
514
|
+
references as one rollback-capable mutation and refuse when a materialized
|
|
515
|
+
worktree would make the directory move unsafe. Mutation JSON includes changed
|
|
516
|
+
paths and the focused validation scope.
|
|
517
|
+
|
|
495
518
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
496
519
|
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
497
520
|
before launch. Use claims for coordinated ownership and the status subcommands
|
package/cli.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { GraphService } from "./src/services/GraphService"
|
|
|
38
38
|
import { ClaimService } from "./src/services/ClaimService"
|
|
39
39
|
import { SyncService } from "./src/services/SyncService"
|
|
40
40
|
import { ReadinessService } from "./src/services/ReadinessService"
|
|
41
|
+
import { GraphMutationService } from "./src/services/GraphMutationService"
|
|
41
42
|
import {
|
|
42
43
|
claimCommand,
|
|
43
44
|
claimHelp,
|
|
@@ -68,6 +69,7 @@ const CliLayer = Layer.mergeAll(
|
|
|
68
69
|
ClaimService.Default,
|
|
69
70
|
SyncService.Default,
|
|
70
71
|
ReadinessService.Default,
|
|
72
|
+
GraphMutationService.Default,
|
|
71
73
|
)
|
|
72
74
|
|
|
73
75
|
/**
|
|
@@ -235,6 +237,7 @@ const commands: Record<string, Command> = {
|
|
|
235
237
|
args: args.slice(1),
|
|
236
238
|
ticketUrl: options["ticket-url"],
|
|
237
239
|
description: options.description,
|
|
240
|
+
clearDescription: options["clear-description"],
|
|
238
241
|
repos: options.repo,
|
|
239
242
|
json: options.json,
|
|
240
243
|
statuses: options.status,
|
|
@@ -278,10 +281,14 @@ const commands: Record<string, Command> = {
|
|
|
278
281
|
subcommand: args[0],
|
|
279
282
|
args: args.slice(1),
|
|
280
283
|
description: options.description,
|
|
284
|
+
clearDescription: options["clear-description"],
|
|
281
285
|
repo: options.repo?.[0],
|
|
282
286
|
references: options.reference,
|
|
283
287
|
branch: options.branch,
|
|
284
288
|
base: options.base,
|
|
289
|
+
clearReferences: options["clear-references"],
|
|
290
|
+
prUrl: options["pr-url"],
|
|
291
|
+
clearPr: options["clear-pr"],
|
|
285
292
|
dependsOn: options["depends-on"],
|
|
286
293
|
firstPhase: options["first-phase"],
|
|
287
294
|
json: options.json,
|
|
@@ -381,12 +388,18 @@ const commands: Record<string, Command> = {
|
|
|
381
388
|
subcommand: args[0],
|
|
382
389
|
args: args.slice(1),
|
|
383
390
|
ticketUrl: options["ticket-url"],
|
|
391
|
+
clearTicket: options["clear-ticket"],
|
|
384
392
|
description: options.description,
|
|
393
|
+
clearDescription: options["clear-description"],
|
|
385
394
|
epic: options.epic,
|
|
386
395
|
repo: options.repo?.[0],
|
|
387
396
|
references: options.reference,
|
|
388
397
|
branch: options.branch,
|
|
389
398
|
base: options.base,
|
|
399
|
+
clearReferences: options["clear-references"],
|
|
400
|
+
prUrl: options["pr-url"],
|
|
401
|
+
clearPr: options["clear-pr"],
|
|
402
|
+
noEpic: options["no-epic"],
|
|
390
403
|
multiPhase: options["multi-phase"],
|
|
391
404
|
json: options.json,
|
|
392
405
|
statuses: options.status,
|
package/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -387,7 +387,51 @@ describe("strict CLI parsing", () => {
|
|
|
387
387
|
)
|
|
388
388
|
expectUsageError(
|
|
389
389
|
["task", "crate"],
|
|
390
|
-
"agency task <new|create|list|show|status>",
|
|
390
|
+
"agency task <new|create|list|show|status|update|rename|move|dependency>",
|
|
391
|
+
)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test("parses graph mutation commands and rejects unsafe ambiguity", () => {
|
|
395
|
+
expect(
|
|
396
|
+
parseCli([
|
|
397
|
+
"task",
|
|
398
|
+
"update",
|
|
399
|
+
"example",
|
|
400
|
+
"--description",
|
|
401
|
+
"Revised",
|
|
402
|
+
"--reference",
|
|
403
|
+
"docs:main",
|
|
404
|
+
"--reference",
|
|
405
|
+
"api:main",
|
|
406
|
+
"--clear-pr",
|
|
407
|
+
]),
|
|
408
|
+
).toMatchObject({
|
|
409
|
+
args: ["update", "example"],
|
|
410
|
+
values: {
|
|
411
|
+
description: "Revised",
|
|
412
|
+
reference: ["docs:main", "api:main"],
|
|
413
|
+
"clear-pr": true,
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
expect(
|
|
417
|
+
parseCli(["task", "move", "example", "--no-epic"]).values,
|
|
418
|
+
).toMatchObject({
|
|
419
|
+
"no-epic": true,
|
|
420
|
+
})
|
|
421
|
+
expect(parseCli(["phase", "rename", "task", "old", "new"]).args).toEqual([
|
|
422
|
+
"rename",
|
|
423
|
+
"task",
|
|
424
|
+
"old",
|
|
425
|
+
"new",
|
|
426
|
+
])
|
|
427
|
+
expect(() => parseCli(["task", "update", "example"])).toThrow(
|
|
428
|
+
"At least one update option",
|
|
429
|
+
)
|
|
430
|
+
expect(() =>
|
|
431
|
+
parseCli(["task", "move", "example", "--epic", "one", "--no-epic"]),
|
|
432
|
+
).toThrow("cannot be combined")
|
|
433
|
+
expect(() => parseCli(["task", "dependency", "replace", "a", "b"])).toThrow(
|
|
434
|
+
"must be 'add' or 'remove'",
|
|
391
435
|
)
|
|
392
436
|
})
|
|
393
437
|
|
package/src/cli-parser.ts
CHANGED
|
@@ -90,6 +90,15 @@ const phaseCreateOptions = {
|
|
|
90
90
|
"first-phase": { type: "string" },
|
|
91
91
|
} satisfies OptionConfig
|
|
92
92
|
|
|
93
|
+
const mutationOptions = {
|
|
94
|
+
"clear-description": { type: "boolean" },
|
|
95
|
+
"clear-ticket": { type: "boolean" },
|
|
96
|
+
"clear-references": { type: "boolean" },
|
|
97
|
+
"pr-url": { type: "string" },
|
|
98
|
+
"clear-pr": { type: "boolean" },
|
|
99
|
+
"no-epic": { type: "boolean" },
|
|
100
|
+
} satisfies OptionConfig
|
|
101
|
+
|
|
93
102
|
const claimOptions = {
|
|
94
103
|
...outputOptions,
|
|
95
104
|
claimant: { type: "string" },
|
|
@@ -200,8 +209,13 @@ const commands = {
|
|
|
200
209
|
},
|
|
201
210
|
},
|
|
202
211
|
epic: {
|
|
203
|
-
usage: "agency epic <create|list|show>",
|
|
204
|
-
options: {
|
|
212
|
+
usage: "agency epic <create|list|show|update|rename>",
|
|
213
|
+
options: {
|
|
214
|
+
...createOptions,
|
|
215
|
+
...viewOptions,
|
|
216
|
+
...mutationOptions,
|
|
217
|
+
epic: { type: "string" },
|
|
218
|
+
},
|
|
205
219
|
subcommands: {
|
|
206
220
|
create: {
|
|
207
221
|
usage:
|
|
@@ -226,11 +240,38 @@ const commands = {
|
|
|
226
240
|
maxArgs: 1,
|
|
227
241
|
options: ["json", "epic"],
|
|
228
242
|
},
|
|
243
|
+
update: {
|
|
244
|
+
usage: "agency epic update <id> [options] [--json]",
|
|
245
|
+
minArgs: 1,
|
|
246
|
+
maxArgs: 1,
|
|
247
|
+
options: [
|
|
248
|
+
"description",
|
|
249
|
+
"clear-description",
|
|
250
|
+
"ticket-url",
|
|
251
|
+
"repo",
|
|
252
|
+
"json",
|
|
253
|
+
],
|
|
254
|
+
repeatable: ["repo"],
|
|
255
|
+
conflicts: [["description", "clear-description"]],
|
|
256
|
+
},
|
|
257
|
+
rename: {
|
|
258
|
+
usage: "agency epic rename <id> <new-id> [--json]",
|
|
259
|
+
minArgs: 2,
|
|
260
|
+
maxArgs: 2,
|
|
261
|
+
options: ["json"],
|
|
262
|
+
},
|
|
229
263
|
},
|
|
230
264
|
},
|
|
231
265
|
task: {
|
|
232
|
-
usage:
|
|
233
|
-
|
|
266
|
+
usage:
|
|
267
|
+
"agency task <new|create|list|show|status|update|rename|move|dependency>",
|
|
268
|
+
options: {
|
|
269
|
+
...taskCreateOptions,
|
|
270
|
+
...viewOptions,
|
|
271
|
+
...mutationOptions,
|
|
272
|
+
"pr-url": { type: "string" },
|
|
273
|
+
task: { type: "string" },
|
|
274
|
+
},
|
|
234
275
|
subcommands: {
|
|
235
276
|
new: {
|
|
236
277
|
usage: "agency task new [id] [options]",
|
|
@@ -287,13 +328,61 @@ const commands = {
|
|
|
287
328
|
maxArgs: 2,
|
|
288
329
|
options: ["json", "task"],
|
|
289
330
|
},
|
|
331
|
+
update: {
|
|
332
|
+
usage: "agency task update <id> [options] [--json]",
|
|
333
|
+
minArgs: 1,
|
|
334
|
+
maxArgs: 1,
|
|
335
|
+
options: [
|
|
336
|
+
"ticket-url",
|
|
337
|
+
"clear-ticket",
|
|
338
|
+
"description",
|
|
339
|
+
"clear-description",
|
|
340
|
+
"repo",
|
|
341
|
+
"reference",
|
|
342
|
+
"clear-references",
|
|
343
|
+
"branch",
|
|
344
|
+
"base",
|
|
345
|
+
"pr-url",
|
|
346
|
+
"clear-pr",
|
|
347
|
+
"json",
|
|
348
|
+
],
|
|
349
|
+
repeatable: ["reference"],
|
|
350
|
+
conflicts: [
|
|
351
|
+
["ticket-url", "clear-ticket"],
|
|
352
|
+
["description", "clear-description"],
|
|
353
|
+
["reference", "clear-references"],
|
|
354
|
+
["pr-url", "clear-pr"],
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
rename: {
|
|
358
|
+
usage: "agency task rename <id> <new-id> [--json]",
|
|
359
|
+
minArgs: 2,
|
|
360
|
+
maxArgs: 2,
|
|
361
|
+
options: ["json"],
|
|
362
|
+
},
|
|
363
|
+
move: {
|
|
364
|
+
usage: "agency task move <id> (--epic <id> | --no-epic) [--json]",
|
|
365
|
+
minArgs: 1,
|
|
366
|
+
maxArgs: 1,
|
|
367
|
+
options: ["epic", "no-epic", "json"],
|
|
368
|
+
conflicts: [["epic", "no-epic"]],
|
|
369
|
+
},
|
|
370
|
+
dependency: {
|
|
371
|
+
usage:
|
|
372
|
+
"agency task dependency <add|remove> <task-id> <dependency-id> [--json]",
|
|
373
|
+
minArgs: 3,
|
|
374
|
+
maxArgs: 3,
|
|
375
|
+
options: ["json"],
|
|
376
|
+
},
|
|
290
377
|
},
|
|
291
378
|
},
|
|
292
379
|
phase: {
|
|
293
|
-
usage: "agency phase <create|list|show|status>",
|
|
380
|
+
usage: "agency phase <create|list|show|status|update|rename|dependency>",
|
|
294
381
|
options: {
|
|
295
382
|
...phaseCreateOptions,
|
|
296
383
|
...viewOptions,
|
|
384
|
+
...mutationOptions,
|
|
385
|
+
"pr-url": { type: "string" },
|
|
297
386
|
task: { type: "string" },
|
|
298
387
|
phase: { type: "string" },
|
|
299
388
|
},
|
|
@@ -338,6 +427,42 @@ const commands = {
|
|
|
338
427
|
maxArgs: 3,
|
|
339
428
|
options: ["json", "task", "phase"],
|
|
340
429
|
},
|
|
430
|
+
update: {
|
|
431
|
+
usage: "agency phase update <task-id> <phase-id> [options] [--json]",
|
|
432
|
+
minArgs: 2,
|
|
433
|
+
maxArgs: 2,
|
|
434
|
+
options: [
|
|
435
|
+
"description",
|
|
436
|
+
"clear-description",
|
|
437
|
+
"repo",
|
|
438
|
+
"reference",
|
|
439
|
+
"clear-references",
|
|
440
|
+
"branch",
|
|
441
|
+
"base",
|
|
442
|
+
"pr-url",
|
|
443
|
+
"clear-pr",
|
|
444
|
+
"json",
|
|
445
|
+
],
|
|
446
|
+
repeatable: ["reference"],
|
|
447
|
+
conflicts: [
|
|
448
|
+
["description", "clear-description"],
|
|
449
|
+
["reference", "clear-references"],
|
|
450
|
+
["pr-url", "clear-pr"],
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
rename: {
|
|
454
|
+
usage: "agency phase rename <task-id> <phase-id> <new-id> [--json]",
|
|
455
|
+
minArgs: 3,
|
|
456
|
+
maxArgs: 3,
|
|
457
|
+
options: ["json"],
|
|
458
|
+
},
|
|
459
|
+
dependency: {
|
|
460
|
+
usage:
|
|
461
|
+
"agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id> [--json]",
|
|
462
|
+
minArgs: 4,
|
|
463
|
+
maxArgs: 4,
|
|
464
|
+
options: ["json"],
|
|
465
|
+
},
|
|
341
466
|
},
|
|
342
467
|
},
|
|
343
468
|
claim: {
|
|
@@ -977,6 +1102,34 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
977
1102
|
) {
|
|
978
1103
|
validateTaskCreate(parsed.values, spec, subcommand === "create")
|
|
979
1104
|
}
|
|
1105
|
+
if (["task", "phase"].includes(commandName) && subcommand === "dependency") {
|
|
1106
|
+
if (!["add", "remove"].includes(commandPositionals[0] ?? "")) {
|
|
1107
|
+
throw usageError(
|
|
1108
|
+
"Dependency operation must be 'add' or 'remove'.",
|
|
1109
|
+
spec.usage,
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (commandName === "task" && subcommand === "move") {
|
|
1114
|
+
if (
|
|
1115
|
+
parsed.values.epic === undefined &&
|
|
1116
|
+
parsed.values["no-epic"] === undefined
|
|
1117
|
+
) {
|
|
1118
|
+
throw usageError(
|
|
1119
|
+
"One of '--epic' or '--no-epic' is required.",
|
|
1120
|
+
spec.usage,
|
|
1121
|
+
)
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (
|
|
1125
|
+
["epic", "task", "phase"].includes(commandName) &&
|
|
1126
|
+
subcommand === "update"
|
|
1127
|
+
) {
|
|
1128
|
+
const mutationNames = (spec.options ?? []).filter((name) => name !== "json")
|
|
1129
|
+
if (!mutationNames.some((name) => parsed.values[name] !== undefined)) {
|
|
1130
|
+
throw usageError("At least one update option is required.", spec.usage)
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
980
1133
|
if (commandName === "graph") {
|
|
981
1134
|
validateGraphOptions(parsed.values, spec)
|
|
982
1135
|
}
|
package/src/cli.test.ts
CHANGED
|
@@ -166,6 +166,68 @@ describe("CLI", () => {
|
|
|
166
166
|
expect(finished.claim).toMatchObject({ state: "finished", outcome: "done" })
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
test("routes graph mutations through structured output", async () => {
|
|
170
|
+
const root = await createTempDir()
|
|
171
|
+
tempDirs.push(root)
|
|
172
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
173
|
+
await mkdir(join(root, "repos", "agency"), { recursive: true })
|
|
174
|
+
parseJson(
|
|
175
|
+
await runCli(
|
|
176
|
+
[
|
|
177
|
+
"epic",
|
|
178
|
+
"create",
|
|
179
|
+
"delivery",
|
|
180
|
+
"--ticket-url",
|
|
181
|
+
"https://example.com/delivery",
|
|
182
|
+
"--repo",
|
|
183
|
+
"agency:main",
|
|
184
|
+
"--json",
|
|
185
|
+
],
|
|
186
|
+
root,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
for (const id of ["first", "second"]) {
|
|
190
|
+
parseJson(
|
|
191
|
+
await runCli(
|
|
192
|
+
[
|
|
193
|
+
"task",
|
|
194
|
+
"create",
|
|
195
|
+
id,
|
|
196
|
+
"--repo",
|
|
197
|
+
"agency",
|
|
198
|
+
"--epic",
|
|
199
|
+
"delivery",
|
|
200
|
+
"--json",
|
|
201
|
+
],
|
|
202
|
+
root,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const updated = parseJson(
|
|
208
|
+
await runCli(
|
|
209
|
+
["task", "update", "second", "--description", "Revised", "--json"],
|
|
210
|
+
root,
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
expect(updated).toMatchObject({
|
|
214
|
+
operation: "task.update",
|
|
215
|
+
entity: { kind: "task", id: "second" },
|
|
216
|
+
validation: { valid: true, scope: ["tasks/second/TASK.md"] },
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const dependency = parseJson(
|
|
220
|
+
await runCli(
|
|
221
|
+
["task", "dependency", "add", "second", "first", "--json"],
|
|
222
|
+
root,
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
expect(dependency).toMatchObject({
|
|
226
|
+
operation: "task.dependency.add",
|
|
227
|
+
changedPaths: ["epics/delivery/EPIC.md"],
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
169
231
|
test("emits one versioned error envelope for usage and command failures", async () => {
|
|
170
232
|
const usage = await runCli(["unknown", "--json"])
|
|
171
233
|
expect(usage.exitCode).toBe(1)
|
package/src/commands/epic.ts
CHANGED
|
@@ -5,12 +5,14 @@ import { createLoggers } from "../utils/effect"
|
|
|
5
5
|
import { formatTable } from "../utils/table"
|
|
6
6
|
import { getWorkViews } from "../work-view"
|
|
7
7
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
8
|
+
import { GraphMutationService } from "../services/GraphMutationService"
|
|
8
9
|
|
|
9
10
|
interface EpicOptions extends BaseCommandOptions {
|
|
10
11
|
readonly subcommand?: string
|
|
11
12
|
readonly args: readonly string[]
|
|
12
13
|
readonly ticketUrl?: string
|
|
13
14
|
readonly description?: string
|
|
15
|
+
readonly clearDescription?: boolean
|
|
14
16
|
readonly repos?: readonly string[]
|
|
15
17
|
readonly json?: boolean
|
|
16
18
|
readonly statuses?: readonly string[]
|
|
@@ -23,6 +25,7 @@ interface EpicOptions extends BaseCommandOptions {
|
|
|
23
25
|
export const epic = (options: EpicOptions) =>
|
|
24
26
|
Effect.gen(function* () {
|
|
25
27
|
const epics = yield* EpicService
|
|
28
|
+
const mutations = yield* GraphMutationService
|
|
26
29
|
const { log } = createLoggers(options)
|
|
27
30
|
const cwd = options.cwd ?? process.cwd()
|
|
28
31
|
|
|
@@ -105,10 +108,49 @@ export const epic = (options: EpicOptions) =>
|
|
|
105
108
|
return
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
case "update": {
|
|
112
|
+
const id = options.args[0]
|
|
113
|
+
if (!id) return yield* Effect.fail(new Error("Epic ID is required"))
|
|
114
|
+
const output = yield* mutations.updateEpic(
|
|
115
|
+
id,
|
|
116
|
+
{
|
|
117
|
+
description: options.clearDescription ? null : options.description,
|
|
118
|
+
ticketUrl: options.ticketUrl,
|
|
119
|
+
repos:
|
|
120
|
+
options.repos === undefined
|
|
121
|
+
? undefined
|
|
122
|
+
: parseRepositoryReferences(options.repos),
|
|
123
|
+
},
|
|
124
|
+
cwd,
|
|
125
|
+
)
|
|
126
|
+
log(
|
|
127
|
+
options.json
|
|
128
|
+
? JSON.stringify(output, null, 2)
|
|
129
|
+
: `Updated epic '${id}'`,
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "rename": {
|
|
135
|
+
const [id, newId] = options.args
|
|
136
|
+
if (!id || !newId) {
|
|
137
|
+
return yield* Effect.fail(
|
|
138
|
+
new Error("Epic ID and new ID are required"),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
const output = yield* mutations.renameEpic(id, newId, cwd)
|
|
142
|
+
log(
|
|
143
|
+
options.json
|
|
144
|
+
? JSON.stringify(output, null, 2)
|
|
145
|
+
: `Renamed epic '${id}' to '${newId}'`,
|
|
146
|
+
)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
108
150
|
default:
|
|
109
151
|
return yield* Effect.fail(
|
|
110
152
|
new Error(
|
|
111
|
-
"Subcommand is required. Available subcommands: create, list, show",
|
|
153
|
+
"Subcommand is required. Available subcommands: create, list, show, update, rename",
|
|
112
154
|
),
|
|
113
155
|
)
|
|
114
156
|
}
|
|
@@ -121,12 +163,20 @@ Subcommands:
|
|
|
121
163
|
create <id> Create an epic
|
|
122
164
|
list List epics
|
|
123
165
|
show <id> Show an epic
|
|
166
|
+
update <id> Update epic metadata
|
|
167
|
+
rename <id> <new-id> Rename an epic and update task references
|
|
124
168
|
|
|
125
169
|
Create options:
|
|
126
170
|
--ticket-url <url> External ticket URL
|
|
127
171
|
--description <text> Short description of the epic
|
|
128
172
|
--repo <alias>:<ref> Read-only repository reference; repeatable
|
|
129
173
|
|
|
174
|
+
Update options:
|
|
175
|
+
--ticket-url <url> Replace the external ticket URL
|
|
176
|
+
--description <text> Replace the description
|
|
177
|
+
--clear-description Remove the description
|
|
178
|
+
--repo <alias>:<ref> Replace repository references; repeatable
|
|
179
|
+
|
|
130
180
|
Options:
|
|
131
181
|
--json Output results as JSON
|
|
132
182
|
--status <status> Filter list by status; repeatable
|
package/src/commands/phase.ts
CHANGED
|
@@ -5,15 +5,20 @@ import { createLoggers } from "../utils/effect"
|
|
|
5
5
|
import { formatTable } from "../utils/table"
|
|
6
6
|
import { getWorkViews } from "../work-view"
|
|
7
7
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
8
|
+
import { GraphMutationService } from "../services/GraphMutationService"
|
|
8
9
|
|
|
9
10
|
interface PhaseOptions extends BaseCommandOptions {
|
|
10
11
|
readonly subcommand?: string
|
|
11
12
|
readonly args: readonly string[]
|
|
12
13
|
readonly description?: string
|
|
14
|
+
readonly clearDescription?: boolean
|
|
13
15
|
readonly repo?: string
|
|
14
16
|
readonly references?: readonly string[]
|
|
15
17
|
readonly branch?: string
|
|
16
18
|
readonly base?: string
|
|
19
|
+
readonly clearReferences?: boolean
|
|
20
|
+
readonly prUrl?: string
|
|
21
|
+
readonly clearPr?: boolean
|
|
17
22
|
readonly dependsOn?: readonly string[]
|
|
18
23
|
readonly firstPhase?: string
|
|
19
24
|
readonly json?: boolean
|
|
@@ -27,6 +32,7 @@ interface PhaseOptions extends BaseCommandOptions {
|
|
|
27
32
|
export const phase = (options: PhaseOptions) =>
|
|
28
33
|
Effect.gen(function* () {
|
|
29
34
|
const phases = yield* PhaseService
|
|
35
|
+
const mutations = yield* GraphMutationService
|
|
30
36
|
const { log } = createLoggers(options)
|
|
31
37
|
const cwd = options.cwd ?? process.cwd()
|
|
32
38
|
const [taskId, phaseId] = options.args
|
|
@@ -153,10 +159,84 @@ export const phase = (options: PhaseOptions) =>
|
|
|
153
159
|
)
|
|
154
160
|
return
|
|
155
161
|
}
|
|
162
|
+
case "update": {
|
|
163
|
+
if (!taskId || !phaseId) {
|
|
164
|
+
return yield* Effect.fail(
|
|
165
|
+
new Error("Task ID and phase ID are required"),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
const output = yield* mutations.updatePhase(
|
|
169
|
+
taskId,
|
|
170
|
+
phaseId,
|
|
171
|
+
{
|
|
172
|
+
description: options.clearDescription ? null : options.description,
|
|
173
|
+
repo: options.repo,
|
|
174
|
+
repos: options.clearReferences
|
|
175
|
+
? null
|
|
176
|
+
: options.references === undefined
|
|
177
|
+
? undefined
|
|
178
|
+
: parseRepositoryReferences(options.references),
|
|
179
|
+
branch: options.branch,
|
|
180
|
+
base: options.base,
|
|
181
|
+
pr: options.clearPr ? null : options.prUrl,
|
|
182
|
+
},
|
|
183
|
+
cwd,
|
|
184
|
+
)
|
|
185
|
+
log(
|
|
186
|
+
options.json
|
|
187
|
+
? JSON.stringify(output, null, 2)
|
|
188
|
+
: `Updated phase '${phaseId}'`,
|
|
189
|
+
)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
case "rename": {
|
|
193
|
+
const newId = options.args[2]
|
|
194
|
+
if (!taskId || !phaseId || !newId) {
|
|
195
|
+
return yield* Effect.fail(
|
|
196
|
+
new Error("Task ID, phase ID, and new ID are required"),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
const output = yield* mutations.renamePhase(taskId, phaseId, newId, cwd)
|
|
200
|
+
log(
|
|
201
|
+
options.json
|
|
202
|
+
? JSON.stringify(output, null, 2)
|
|
203
|
+
: `Renamed phase '${phaseId}' to '${newId}'`,
|
|
204
|
+
)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
case "dependency": {
|
|
208
|
+
const [operation, dependencyTaskId, dependencyPhaseId, dependencyId] =
|
|
209
|
+
options.args
|
|
210
|
+
if (
|
|
211
|
+
(operation !== "add" && operation !== "remove") ||
|
|
212
|
+
!dependencyTaskId ||
|
|
213
|
+
!dependencyPhaseId ||
|
|
214
|
+
!dependencyId
|
|
215
|
+
) {
|
|
216
|
+
return yield* Effect.fail(
|
|
217
|
+
new Error(
|
|
218
|
+
"Usage: agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id>",
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
const output = yield* mutations.mutatePhaseDependency(
|
|
223
|
+
operation,
|
|
224
|
+
dependencyTaskId,
|
|
225
|
+
dependencyPhaseId,
|
|
226
|
+
dependencyId,
|
|
227
|
+
cwd,
|
|
228
|
+
)
|
|
229
|
+
log(
|
|
230
|
+
options.json
|
|
231
|
+
? JSON.stringify(output, null, 2)
|
|
232
|
+
: `${operation === "add" ? "Added" : "Removed"} dependency '${dependencyId}' ${operation === "add" ? "to" : "from"} phase '${dependencyPhaseId}'`,
|
|
233
|
+
)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
156
236
|
default:
|
|
157
237
|
return yield* Effect.fail(
|
|
158
238
|
new Error(
|
|
159
|
-
"Subcommand is required. Available: create, list, show, status",
|
|
239
|
+
"Subcommand is required. Available: create, list, show, status, update, rename, dependency",
|
|
160
240
|
),
|
|
161
241
|
)
|
|
162
242
|
}
|
|
@@ -171,6 +251,11 @@ Subcommands:
|
|
|
171
251
|
show <task> <phase> Show a phase
|
|
172
252
|
status <task> <phase> <status>
|
|
173
253
|
Set open, done, or dropped
|
|
254
|
+
update <task> <phase> Update phase metadata
|
|
255
|
+
rename <task> <phase> <new-id>
|
|
256
|
+
Rename a phase and update dependencies
|
|
257
|
+
dependency <operation> <task> <phase> <dependency>
|
|
258
|
+
Add or remove a phase dependency
|
|
174
259
|
|
|
175
260
|
Create options:
|
|
176
261
|
--description <text> Short description of the phase
|
|
@@ -182,6 +267,14 @@ Create options:
|
|
|
182
267
|
--depends-on <id> Phase dependency; repeatable
|
|
183
268
|
--first-phase <id> Existing execution phase ID when converting a task
|
|
184
269
|
|
|
270
|
+
Update options:
|
|
271
|
+
--description <text> / --clear-description
|
|
272
|
+
--repo <alias> Replace the writable repository
|
|
273
|
+
--reference <alias>:<ref> / --clear-references
|
|
274
|
+
--branch <name> Replace the working branch
|
|
275
|
+
--base <name> Replace the base branch
|
|
276
|
+
--pr-url <url> / --clear-pr
|
|
277
|
+
|
|
185
278
|
Options:
|
|
186
279
|
--json Output results as JSON
|
|
187
280
|
--status <status> Filter list by status; repeatable
|