@os-eco/overstory-cli 0.6.1 → 0.6.5
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 +8 -7
- package/package.json +12 -4
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +131 -16
- package/src/agents/hooks-deployer.ts +33 -1
- package/src/agents/identity.test.ts +27 -27
- package/src/agents/identity.ts +10 -10
- package/src/agents/lifecycle.test.ts +6 -6
- package/src/agents/lifecycle.ts +2 -2
- package/src/agents/manifest.test.ts +86 -0
- package/src/agents/overlay.test.ts +9 -9
- package/src/agents/overlay.ts +4 -4
- package/src/commands/agents.test.ts +8 -8
- package/src/commands/agents.ts +62 -91
- package/src/commands/clean.test.ts +36 -51
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +133 -26
- package/src/commands/coordinator.ts +101 -64
- package/src/commands/costs.test.ts +47 -47
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +75 -95
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +18 -18
- package/src/commands/inspect.ts +55 -58
- package/src/commands/log.test.ts +26 -31
- package/src/commands/log.ts +97 -91
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.test.ts +5 -5
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +28 -66
- package/src/commands/merge.ts +21 -51
- package/src/commands/metrics.test.ts +8 -8
- package/src/commands/metrics.ts +34 -35
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +57 -62
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +19 -51
- package/src/commands/prime.ts +13 -50
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +201 -5
- package/src/commands/sling.ts +37 -64
- package/src/commands/spec.test.ts +14 -40
- package/src/commands/spec.ts +32 -101
- package/src/commands/status.test.ts +97 -1
- package/src/commands/status.ts +63 -58
- package/src/commands/stop.test.ts +22 -40
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +12 -14
- package/src/commands/supervisor.ts +144 -165
- package/src/commands/trace.test.ts +15 -15
- package/src/commands/trace.ts +59 -82
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +213 -37
- package/src/commands/worktree.ts +110 -55
- package/src/config.test.ts +96 -0
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/e2e/init-sling-lifecycle.test.ts +8 -8
- package/src/errors.ts +1 -1
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +6 -6
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +73 -7
- package/src/merge/queue.ts +17 -2
- package/src/merge/resolver.test.ts +159 -7
- package/src/merge/resolver.ts +46 -2
- package/src/metrics/store.test.ts +44 -44
- package/src/metrics/store.ts +2 -2
- package/src/metrics/summary.test.ts +35 -35
- package/src/mulch/client.test.ts +1 -1
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +2 -2
- package/src/sessions/store.test.ts +41 -4
- package/src/sessions/store.ts +13 -2
- package/src/types.ts +14 -14
- package/src/watchdog/daemon.test.ts +10 -10
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -1
- package/src/worktree/manager.test.ts +20 -20
- package/src/worktree/manager.ts +120 -4
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
package/src/commands/group.ts
CHANGED
|
@@ -8,38 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { Command } from "commander";
|
|
11
12
|
import { loadConfig } from "../config.ts";
|
|
12
13
|
import { GroupError, ValidationError } from "../errors.ts";
|
|
13
14
|
import { createTrackerClient, resolveBackend, type TrackerClient } from "../tracker/factory.ts";
|
|
14
15
|
import type { TaskGroup, TaskGroupProgress } from "../types.ts";
|
|
15
16
|
|
|
16
|
-
/** Boolean flags that do NOT consume the next arg. */
|
|
17
|
-
const BOOLEAN_FLAGS = new Set(["--json", "--help", "-h"]);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Extract positional arguments, skipping flag-value pairs.
|
|
21
|
-
*/
|
|
22
|
-
function getPositionalArgs(args: string[]): string[] {
|
|
23
|
-
const positional: string[] = [];
|
|
24
|
-
let i = 0;
|
|
25
|
-
while (i < args.length) {
|
|
26
|
-
const arg = args[i];
|
|
27
|
-
if (arg?.startsWith("-")) {
|
|
28
|
-
if (BOOLEAN_FLAGS.has(arg)) {
|
|
29
|
-
i += 1;
|
|
30
|
-
} else {
|
|
31
|
-
i += 2;
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
if (arg !== undefined) {
|
|
35
|
-
positional.push(arg);
|
|
36
|
-
}
|
|
37
|
-
i += 1;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return positional;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
17
|
/**
|
|
44
18
|
* Resolve the groups.json path from the project root.
|
|
45
19
|
*/
|
|
@@ -322,174 +296,167 @@ function printGroupProgress(progress: TaskGroupProgress): void {
|
|
|
322
296
|
}
|
|
323
297
|
}
|
|
324
298
|
|
|
325
|
-
const GROUP_HELP = `overstory group -- Manage task groups for batch coordination
|
|
326
|
-
|
|
327
|
-
Usage: overstory group <subcommand> [args...]
|
|
328
|
-
|
|
329
|
-
Subcommands:
|
|
330
|
-
create '<name>' <id1> [id2...] Create a new task group
|
|
331
|
-
status [group-id] Show progress for one or all groups
|
|
332
|
-
add <group-id> <id1> [id2...] Add issues to a group
|
|
333
|
-
remove <group-id> <id1> [id2...] Remove issues from a group
|
|
334
|
-
list List all groups (summary)
|
|
335
|
-
|
|
336
|
-
Options:
|
|
337
|
-
--json Output as JSON
|
|
338
|
-
--skip-validation Skip beads issue validation (for offline use)
|
|
339
|
-
--help, -h Show this help`;
|
|
340
|
-
|
|
341
299
|
/**
|
|
342
|
-
*
|
|
300
|
+
* Create the Commander command for `overstory group`.
|
|
343
301
|
*/
|
|
344
|
-
export
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
{ field: "name" },
|
|
302
|
+
export function createGroupCommand(): Command {
|
|
303
|
+
const cmd = new Command("group").description("Manage task groups for batch coordination");
|
|
304
|
+
|
|
305
|
+
cmd
|
|
306
|
+
.command("create")
|
|
307
|
+
.description("Create a new task group")
|
|
308
|
+
.argument("<name>", "Group name")
|
|
309
|
+
.argument("<ids...>", "Issue IDs to include")
|
|
310
|
+
.option("--json", "Output as JSON")
|
|
311
|
+
.option("--skip-validation", "Skip beads issue validation (for offline use)")
|
|
312
|
+
.action(
|
|
313
|
+
async (name: string, ids: string[], opts: { json?: boolean; skipValidation?: boolean }) => {
|
|
314
|
+
const config = await loadConfig(process.cwd());
|
|
315
|
+
const projectRoot = config.project.root;
|
|
316
|
+
const resolvedBackend = await resolveBackend(config.taskTracker.backend, projectRoot);
|
|
317
|
+
const tracker = createTrackerClient(resolvedBackend, projectRoot);
|
|
318
|
+
|
|
319
|
+
const group = await createGroup(
|
|
320
|
+
projectRoot,
|
|
321
|
+
name,
|
|
322
|
+
ids,
|
|
323
|
+
opts.skipValidation ?? false,
|
|
324
|
+
tracker,
|
|
368
325
|
);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (issueIds.length === 0) {
|
|
372
|
-
throw new ValidationError(
|
|
373
|
-
"At least one issue ID is required: overstory group create '<name>' <id1> [id2...]",
|
|
374
|
-
{ field: "issueIds" },
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
const group = await createGroup(projectRoot, name, issueIds, skipValidation, tracker);
|
|
378
|
-
if (json) {
|
|
379
|
-
process.stdout.write(`${JSON.stringify(group, null, "\t")}\n`);
|
|
380
|
-
} else {
|
|
381
|
-
process.stdout.write(`Created group "${group.name}" (${group.id})\n`);
|
|
382
|
-
process.stdout.write(` Members: ${group.memberIssueIds.join(", ")}\n`);
|
|
383
|
-
}
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
case "status": {
|
|
388
|
-
const positional = getPositionalArgs(subArgs);
|
|
389
|
-
const groupId = positional[0];
|
|
390
|
-
const groups = await loadGroups(projectRoot);
|
|
391
|
-
|
|
392
|
-
if (groupId) {
|
|
393
|
-
const group = groups.find((g) => g.id === groupId);
|
|
394
|
-
if (!group) {
|
|
395
|
-
throw new GroupError(`Group "${groupId}" not found`, { groupId });
|
|
396
|
-
}
|
|
397
|
-
const progress = await getGroupProgress(projectRoot, group, groups, tracker);
|
|
398
|
-
if (json) {
|
|
399
|
-
process.stdout.write(`${JSON.stringify(progress, null, "\t")}\n`);
|
|
326
|
+
if (opts.json) {
|
|
327
|
+
process.stdout.write(`${JSON.stringify(group, null, "\t")}\n`);
|
|
400
328
|
} else {
|
|
401
|
-
|
|
329
|
+
process.stdout.write(`Created group "${group.name}" (${group.id})\n`);
|
|
330
|
+
process.stdout.write(` Members: ${group.memberIssueIds.join(", ")}\n`);
|
|
402
331
|
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
cmd
|
|
336
|
+
.command("status")
|
|
337
|
+
.description("Show progress for one or all groups")
|
|
338
|
+
.argument("[group-id]", "Group ID (optional, shows all if omitted)")
|
|
339
|
+
.option("--json", "Output as JSON")
|
|
340
|
+
.option("--skip-validation", "Skip beads issue validation (for offline use)")
|
|
341
|
+
.action(
|
|
342
|
+
async (groupId: string | undefined, opts: { json?: boolean; skipValidation?: boolean }) => {
|
|
343
|
+
const config = await loadConfig(process.cwd());
|
|
344
|
+
const projectRoot = config.project.root;
|
|
345
|
+
const resolvedBackend = await resolveBackend(config.taskTracker.backend, projectRoot);
|
|
346
|
+
const tracker = createTrackerClient(resolvedBackend, projectRoot);
|
|
347
|
+
const json = opts.json ?? false;
|
|
348
|
+
|
|
349
|
+
const groups = await loadGroups(projectRoot);
|
|
350
|
+
|
|
351
|
+
if (groupId) {
|
|
352
|
+
const group = groups.find((g) => g.id === groupId);
|
|
353
|
+
if (!group) {
|
|
354
|
+
throw new GroupError(`Group "${groupId}" not found`, { groupId });
|
|
355
|
+
}
|
|
356
|
+
const progress = await getGroupProgress(projectRoot, group, groups, tracker);
|
|
406
357
|
if (json) {
|
|
407
|
-
process.stdout.write("
|
|
358
|
+
process.stdout.write(`${JSON.stringify(progress, null, "\t")}\n`);
|
|
408
359
|
} else {
|
|
409
|
-
|
|
360
|
+
printGroupProgress(progress);
|
|
410
361
|
}
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
const progressList: TaskGroupProgress[] = [];
|
|
414
|
-
for (const group of activeGroups) {
|
|
415
|
-
const progress = await getGroupProgress(projectRoot, group, groups, tracker);
|
|
416
|
-
progressList.push(progress);
|
|
417
|
-
}
|
|
418
|
-
if (json) {
|
|
419
|
-
process.stdout.write(`${JSON.stringify(progressList, null, "\t")}\n`);
|
|
420
362
|
} else {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
363
|
+
const activeGroups = groups.filter((g) => g.status === "active");
|
|
364
|
+
if (activeGroups.length === 0) {
|
|
365
|
+
if (json) {
|
|
366
|
+
process.stdout.write("[]\n");
|
|
367
|
+
} else {
|
|
368
|
+
process.stdout.write("No active groups\n");
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const progressList: TaskGroupProgress[] = [];
|
|
373
|
+
for (const group of activeGroups) {
|
|
374
|
+
const progress = await getGroupProgress(projectRoot, group, groups, tracker);
|
|
375
|
+
progressList.push(progress);
|
|
376
|
+
}
|
|
377
|
+
if (json) {
|
|
378
|
+
process.stdout.write(`${JSON.stringify(progressList, null, "\t")}\n`);
|
|
379
|
+
} else {
|
|
380
|
+
for (const progress of progressList) {
|
|
381
|
+
printGroupProgress(progress);
|
|
382
|
+
process.stdout.write("\n");
|
|
383
|
+
}
|
|
424
384
|
}
|
|
425
385
|
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
cmd
|
|
390
|
+
.command("add")
|
|
391
|
+
.description("Add issues to a group")
|
|
392
|
+
.argument("<group-id>", "Group ID")
|
|
393
|
+
.argument("<ids...>", "Issue IDs to add")
|
|
394
|
+
.option("--json", "Output as JSON")
|
|
395
|
+
.option("--skip-validation", "Skip beads issue validation (for offline use)")
|
|
396
|
+
.action(
|
|
397
|
+
async (
|
|
398
|
+
groupId: string,
|
|
399
|
+
ids: string[],
|
|
400
|
+
opts: { json?: boolean; skipValidation?: boolean },
|
|
401
|
+
) => {
|
|
402
|
+
const config = await loadConfig(process.cwd());
|
|
403
|
+
const projectRoot = config.project.root;
|
|
404
|
+
const resolvedBackend = await resolveBackend(config.taskTracker.backend, projectRoot);
|
|
405
|
+
const tracker = createTrackerClient(resolvedBackend, projectRoot);
|
|
406
|
+
|
|
407
|
+
const group = await addToGroup(
|
|
408
|
+
projectRoot,
|
|
409
|
+
groupId,
|
|
410
|
+
ids,
|
|
411
|
+
opts.skipValidation ?? false,
|
|
412
|
+
tracker,
|
|
437
413
|
);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
414
|
+
if (opts.json) {
|
|
415
|
+
process.stdout.write(`${JSON.stringify(group, null, "\t")}\n`);
|
|
416
|
+
} else {
|
|
417
|
+
process.stdout.write(`Added ${ids.length} issue(s) to "${group.name}"\n`);
|
|
418
|
+
process.stdout.write(` Members: ${group.memberIssueIds.join(", ")}\n`);
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
cmd
|
|
424
|
+
.command("remove")
|
|
425
|
+
.description("Remove issues from a group")
|
|
426
|
+
.argument("<group-id>", "Group ID")
|
|
427
|
+
.argument("<ids...>", "Issue IDs to remove")
|
|
428
|
+
.option("--json", "Output as JSON")
|
|
429
|
+
.action(async (groupId: string, ids: string[], opts: { json?: boolean }) => {
|
|
430
|
+
const config = await loadConfig(process.cwd());
|
|
431
|
+
const projectRoot = config.project.root;
|
|
432
|
+
|
|
433
|
+
const group = await removeFromGroup(projectRoot, groupId, ids);
|
|
434
|
+
if (opts.json) {
|
|
448
435
|
process.stdout.write(`${JSON.stringify(group, null, "\t")}\n`);
|
|
449
436
|
} else {
|
|
450
|
-
process.stdout.write(`
|
|
437
|
+
process.stdout.write(`Removed ${ids.length} issue(s) from "${group.name}"\n`);
|
|
451
438
|
process.stdout.write(` Members: ${group.memberIssueIds.join(", ")}\n`);
|
|
452
439
|
}
|
|
453
|
-
|
|
454
|
-
}
|
|
440
|
+
});
|
|
455
441
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
const issueIds = positional.slice(1);
|
|
466
|
-
if (issueIds.length === 0) {
|
|
467
|
-
throw new ValidationError(
|
|
468
|
-
"At least one issue ID is required: overstory group remove <group-id> <id1> [id2...]",
|
|
469
|
-
{ field: "issueIds" },
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
const group = await removeFromGroup(projectRoot, groupId, issueIds);
|
|
473
|
-
if (json) {
|
|
474
|
-
process.stdout.write(`${JSON.stringify(group, null, "\t")}\n`);
|
|
475
|
-
} else {
|
|
476
|
-
process.stdout.write(`Removed ${issueIds.length} issue(s) from "${group.name}"\n`);
|
|
477
|
-
process.stdout.write(` Members: ${group.memberIssueIds.join(", ")}\n`);
|
|
478
|
-
}
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
442
|
+
cmd
|
|
443
|
+
.command("list")
|
|
444
|
+
.description("List all groups (summary)")
|
|
445
|
+
.option("--json", "Output as JSON")
|
|
446
|
+
.action(async (opts: { json?: boolean }) => {
|
|
447
|
+
const config = await loadConfig(process.cwd());
|
|
448
|
+
const projectRoot = config.project.root;
|
|
481
449
|
|
|
482
|
-
case "list": {
|
|
483
450
|
const groups = await loadGroups(projectRoot);
|
|
484
451
|
if (groups.length === 0) {
|
|
485
|
-
if (json) {
|
|
452
|
+
if (opts.json) {
|
|
486
453
|
process.stdout.write("[]\n");
|
|
487
454
|
} else {
|
|
488
455
|
process.stdout.write("No groups\n");
|
|
489
456
|
}
|
|
490
|
-
|
|
457
|
+
return;
|
|
491
458
|
}
|
|
492
|
-
if (json) {
|
|
459
|
+
if (opts.json) {
|
|
493
460
|
process.stdout.write(`${JSON.stringify(groups, null, "\t")}\n`);
|
|
494
461
|
} else {
|
|
495
462
|
for (const group of groups) {
|
|
@@ -499,13 +466,36 @@ export async function groupCommand(args: string[]): Promise<void> {
|
|
|
499
466
|
);
|
|
500
467
|
}
|
|
501
468
|
}
|
|
502
|
-
|
|
503
|
-
}
|
|
469
|
+
});
|
|
504
470
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
471
|
+
return cmd;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Entry point for `overstory group <subcommand>`.
|
|
476
|
+
*/
|
|
477
|
+
export async function groupCommand(args: string[]): Promise<void> {
|
|
478
|
+
const cmd = createGroupCommand();
|
|
479
|
+
cmd.exitOverride();
|
|
480
|
+
|
|
481
|
+
if (args.length === 0) {
|
|
482
|
+
process.stdout.write(cmd.helpInformation());
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
488
|
+
} catch (err: unknown) {
|
|
489
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
490
|
+
const code = (err as { code: string }).code;
|
|
491
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (code === "commander.unknownCommand") {
|
|
495
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
496
|
+
throw new ValidationError(message, { field: "subcommand" });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
throw err;
|
|
510
500
|
}
|
|
511
501
|
}
|
|
@@ -75,7 +75,7 @@ afterEach(async () => {
|
|
|
75
75
|
describe("hooksCommand help", () => {
|
|
76
76
|
test("--help outputs help text", async () => {
|
|
77
77
|
const output = await captureStdout(() => hooksCommand(["--help"]));
|
|
78
|
-
expect(output).toContain("
|
|
78
|
+
expect(output).toContain("hooks");
|
|
79
79
|
expect(output).toContain("install");
|
|
80
80
|
expect(output).toContain("uninstall");
|
|
81
81
|
expect(output).toContain("status");
|
|
@@ -83,7 +83,7 @@ describe("hooksCommand help", () => {
|
|
|
83
83
|
|
|
84
84
|
test("empty args outputs help text", async () => {
|
|
85
85
|
const output = await captureStdout(() => hooksCommand([]));
|
|
86
|
-
expect(output).toContain("
|
|
86
|
+
expect(output).toContain("hooks");
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
test("unknown subcommand throws ValidationError", async () => {
|
package/src/commands/hooks.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { mkdir, unlink } from "node:fs/promises";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
+
import { Command } from "commander";
|
|
15
16
|
import { loadConfig } from "../config.ts";
|
|
16
17
|
import { ValidationError } from "../errors.ts";
|
|
17
18
|
|
|
@@ -63,23 +64,6 @@ export function mergeHooksByEventType(
|
|
|
63
64
|
return merged;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
const HOOKS_HELP = `overstory hooks — Manage orchestrator hooks
|
|
67
|
-
|
|
68
|
-
Usage: overstory hooks <subcommand>
|
|
69
|
-
|
|
70
|
-
Subcommands:
|
|
71
|
-
install Install orchestrator hooks to .claude/settings.local.json
|
|
72
|
-
uninstall Remove orchestrator hooks from .claude/settings.local.json
|
|
73
|
-
status Check if hooks are installed
|
|
74
|
-
|
|
75
|
-
Options:
|
|
76
|
-
--force Overwrite existing hooks in .claude/settings.local.json
|
|
77
|
-
--json Output as JSON
|
|
78
|
-
--help, -h Show this help
|
|
79
|
-
|
|
80
|
-
Hooks source: .overstory/hooks.json (generated by overstory init)
|
|
81
|
-
Hooks target: .claude/settings.local.json (read by Claude Code)`;
|
|
82
|
-
|
|
83
67
|
/**
|
|
84
68
|
* Install orchestrator hooks from .overstory/hooks.json to .claude/settings.local.json.
|
|
85
69
|
*
|
|
@@ -87,8 +71,7 @@ Hooks target: .claude/settings.local.json (read by Claude Code)`;
|
|
|
87
71
|
* .claude/settings.local.json where Claude Code discovers it. Preserves any
|
|
88
72
|
* existing non-hooks keys in the target file.
|
|
89
73
|
*/
|
|
90
|
-
async function installHooks(
|
|
91
|
-
const force = args.includes("--force");
|
|
74
|
+
async function installHooks(force: boolean): Promise<void> {
|
|
92
75
|
const cwd = process.cwd();
|
|
93
76
|
const config = await loadConfig(cwd);
|
|
94
77
|
const projectRoot = config.project.root;
|
|
@@ -147,7 +130,7 @@ async function installHooks(args: string[]): Promise<void> {
|
|
|
147
130
|
* If hooks were the only content, removes the file entirely.
|
|
148
131
|
* Otherwise, preserves other keys and only removes the hooks key.
|
|
149
132
|
*/
|
|
150
|
-
async function uninstallHooks(
|
|
133
|
+
async function uninstallHooks(): Promise<void> {
|
|
151
134
|
const cwd = process.cwd();
|
|
152
135
|
const config = await loadConfig(cwd);
|
|
153
136
|
const projectRoot = config.project.root;
|
|
@@ -188,8 +171,7 @@ async function uninstallHooks(_args: string[]): Promise<void> {
|
|
|
188
171
|
/**
|
|
189
172
|
* Show hooks installation status.
|
|
190
173
|
*/
|
|
191
|
-
async function statusHooks(
|
|
192
|
-
const json = args.includes("--json");
|
|
174
|
+
async function statusHooks(json: boolean): Promise<void> {
|
|
193
175
|
const cwd = process.cwd();
|
|
194
176
|
const config = await loadConfig(cwd);
|
|
195
177
|
const projectRoot = config.project.root;
|
|
@@ -222,32 +204,60 @@ async function statusHooks(args: string[]): Promise<void> {
|
|
|
222
204
|
}
|
|
223
205
|
}
|
|
224
206
|
|
|
207
|
+
export function createHooksCommand(): Command {
|
|
208
|
+
const cmd = new Command("hooks").description("Manage orchestrator hooks");
|
|
209
|
+
|
|
210
|
+
cmd
|
|
211
|
+
.command("install")
|
|
212
|
+
.description("Install orchestrator hooks to .claude/settings.local.json")
|
|
213
|
+
.option("--force", "Overwrite existing hooks")
|
|
214
|
+
.action(async (opts: { force?: boolean }) => {
|
|
215
|
+
await installHooks(opts.force ?? false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
cmd
|
|
219
|
+
.command("uninstall")
|
|
220
|
+
.description("Remove orchestrator hooks from .claude/settings.local.json")
|
|
221
|
+
.action(async () => {
|
|
222
|
+
await uninstallHooks();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
cmd
|
|
226
|
+
.command("status")
|
|
227
|
+
.description("Check if hooks are installed")
|
|
228
|
+
.option("--json", "Output as JSON")
|
|
229
|
+
.action(async (opts: { json?: boolean }) => {
|
|
230
|
+
await statusHooks(opts.json ?? false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return cmd;
|
|
234
|
+
}
|
|
235
|
+
|
|
225
236
|
/**
|
|
226
237
|
* Entry point for `overstory hooks <subcommand>`.
|
|
227
238
|
*/
|
|
228
239
|
export async function hooksCommand(args: string[]): Promise<void> {
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
const cmd = createHooksCommand();
|
|
241
|
+
cmd.exitOverride();
|
|
242
|
+
|
|
243
|
+
if (args.length === 0) {
|
|
244
|
+
process.stdout.write(cmd.helpInformation());
|
|
231
245
|
return;
|
|
232
246
|
}
|
|
233
247
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
throw new ValidationError(
|
|
249
|
-
`Unknown hooks subcommand: ${subcommand}. Run 'overstory hooks --help' for usage.`,
|
|
250
|
-
{ field: "subcommand", value: subcommand },
|
|
251
|
-
);
|
|
248
|
+
try {
|
|
249
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
250
|
+
} catch (err: unknown) {
|
|
251
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
252
|
+
const code = (err as { code: string }).code;
|
|
253
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (code === "commander.unknownCommand") {
|
|
257
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
258
|
+
throw new ValidationError(message, { field: "subcommand" });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
throw err;
|
|
252
262
|
}
|
|
253
263
|
}
|