cawdev-cli 1.0.10-beta → 1.1.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 +11 -1
- package/lib/background-shells.mjs +45 -3
- package/lib/roadmap-format.mjs +31 -8
- package/lib/stage-tools.mjs +2 -0
- package/mcp/README.md +44 -30
- package/mcp/server.mjs +113 -49
- package/package.json +1 -1
- package/runner/README.md +29 -27
- package/runner/attach.mjs +19 -8
- package/runner/git-host.mjs +263 -0
- package/runner/runner.mjs +324 -117
- package/runner/split.mjs +46 -6
- package/runner/workspace-check.mjs +78 -0
package/runner/split.mjs
CHANGED
|
@@ -661,14 +661,31 @@ export function wordWrap(text, width) {
|
|
|
661
661
|
* terminal acts on. MERGED, SHIPPED and DECLINED are not columns: the work
|
|
662
662
|
* is over, the three only grow, and the console's own board folds the last
|
|
663
663
|
* two to a count (R68) for the same reason — so here all three are counts
|
|
664
|
-
* beside the project's name.
|
|
664
|
+
* beside the project's name. BACKLOG is a count too, at the head of them: it
|
|
665
|
+
* is work not yet taken up rather than work over, but a column of it would be
|
|
666
|
+
* the first to give way, and since R371 it is where a new issue lands — left
|
|
667
|
+
* out, a project's fresh bugs were on no line of the strip at all.
|
|
665
668
|
*/
|
|
666
669
|
export const BOARD_COLUMNS = [
|
|
667
670
|
['CONSIDERING', 'Considering'],
|
|
668
671
|
['PLANNED', 'Planned'],
|
|
669
672
|
['IN_DEVELOPMENT', 'In development'],
|
|
670
673
|
];
|
|
671
|
-
const FOLDED = [['MERGED', 'merged'], ['SHIPPED', 'shipped'], ['DECLINED', 'declined']];
|
|
674
|
+
const FOLDED = [['BACKLOG', 'backlog'], ['MERGED', 'merged'], ['SHIPPED', 'shipped'], ['DECLINED', 'declined']];
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* The column a card is drawn in. An issue (R85) has statuses of its own for
|
|
678
|
+
* the first two steps — NEW is reported and not yet agreed, CONFIRMED is
|
|
679
|
+
* agreed and waiting for somebody, the platform's `afterPlanning` — and they
|
|
680
|
+
* are Considering's and Planned's counterparts, so they share those columns
|
|
681
|
+
* rather than adding two more to a strip that already gives columns away.
|
|
682
|
+
*/
|
|
683
|
+
export function columnOf(status) {
|
|
684
|
+
return { NEW: 'CONSIDERING', CONFIRMED: 'PLANNED' }[status] ?? status;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Most broken first: the issues board's own order (R85), and what a column cut short keeps. */
|
|
688
|
+
const SEVERITY_ORDER = ['CRITICAL', 'MEDIUM', 'MINOR'];
|
|
672
689
|
|
|
673
690
|
/**
|
|
674
691
|
* Which column gives way first when the width runs out: the ideas, then what
|
|
@@ -701,7 +718,12 @@ const STRIP_HEAD = 2;
|
|
|
701
718
|
* column needs, no more — of exactly `width` columns. One line when there is
|
|
702
719
|
* no project at all.
|
|
703
720
|
*
|
|
704
|
-
*
|
|
721
|
+
* ISSUES SHARE THE COLUMNS (`columnOf`) and go first in each, most severe
|
|
722
|
+
* first: a column cut to `+N more` should not hide a critical defect under
|
|
723
|
+
* ideas. Their ref (`i12`) is in the colour of their severity.
|
|
724
|
+
*
|
|
725
|
+
* @param cards `[{ ref, title, slug, status, live?, severity? }]` — a card
|
|
726
|
+
* with a `severity` is an issue
|
|
705
727
|
* @param projects the slugs to show, in order; defaults to those the cards
|
|
706
728
|
* come from
|
|
707
729
|
*/
|
|
@@ -716,7 +738,7 @@ export function roadmapStrip(width, maxRows, cards, ink, projects = null) {
|
|
|
716
738
|
slug,
|
|
717
739
|
folded: FOLDED.map(([status, word]) => [word, mine.filter((c) => c.status === status).length]),
|
|
718
740
|
columns: BOARD_COLUMNS.map(([status, label]) => ({
|
|
719
|
-
status, label, cards: mine
|
|
741
|
+
status, label, cards: inColumn(mine, status),
|
|
720
742
|
})),
|
|
721
743
|
hidden: [],
|
|
722
744
|
};
|
|
@@ -796,7 +818,7 @@ export function roadmapStrip(width, maxRows, cards, ink, projects = null) {
|
|
|
796
818
|
} else if (column.cards[row]) {
|
|
797
819
|
const card = column.cards[row];
|
|
798
820
|
const mark = card.live ? ink.accent('●') : ' ';
|
|
799
|
-
cell = `${mark}${ink
|
|
821
|
+
cell = `${mark}${refInk(card, ink)(card.ref)} ${card.title ?? ''}`;
|
|
800
822
|
} else if (row === 0 && !column.cards.length) {
|
|
801
823
|
cell = ink.muted(' —');
|
|
802
824
|
}
|
|
@@ -806,6 +828,24 @@ export function roadmapStrip(width, maxRows, cards, ink, projects = null) {
|
|
|
806
828
|
return lines.map((line) => padVisible(clip(line, width), width));
|
|
807
829
|
}
|
|
808
830
|
|
|
831
|
+
/** A column's cards: its issues first, most severe first, then its roadmap cards, each in the order given. */
|
|
832
|
+
function inColumn(cards, status) {
|
|
833
|
+
const here = cards.filter((card) => columnOf(card.status) === status);
|
|
834
|
+
const rank = (card) => {
|
|
835
|
+
const at = SEVERITY_ORDER.indexOf(card.severity);
|
|
836
|
+
return at === -1 ? SEVERITY_ORDER.length : at;
|
|
837
|
+
};
|
|
838
|
+
const issues = here.filter((card) => card.severity).sort((a, b) => rank(a) - rank(b));
|
|
839
|
+
return [...issues, ...here.filter((card) => !card.severity)];
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** A card's ref in bold; an issue's in the colour of how broken it is. */
|
|
843
|
+
function refInk(card, ink) {
|
|
844
|
+
if (!card.severity) return ink.bold;
|
|
845
|
+
const hue = card.severity === 'CRITICAL' ? ink.danger : card.severity === 'MEDIUM' ? ink.warn : ink.muted;
|
|
846
|
+
return (text) => ink.bold(hue(text));
|
|
847
|
+
}
|
|
848
|
+
|
|
809
849
|
/** Clipped to `width`, with `…` where it had to stop — a title cut dead reads as the whole title. */
|
|
810
850
|
function ellipsed(text, width) {
|
|
811
851
|
return visibleWidth(text) <= width ? text : `${clip(text, width - 1)}…`;
|
|
@@ -923,7 +963,7 @@ export function cardLines(card, ink, url = null) {
|
|
|
923
963
|
field('status', [card.statusDisplay ?? card.status, kind].filter(Boolean).join(' · '));
|
|
924
964
|
field('section', card.section);
|
|
925
965
|
field('tags', card.tags?.length ? card.tags.join(', ') : null);
|
|
926
|
-
field('sprint', card.sprint ? `${card.sprint.ref ?? card.sprint.number} ${card.sprint.name ?? ''}`.trim() : null);
|
|
966
|
+
field('sprint', card.sprint ? `${card.sprint.label ?? card.sprint.ref ?? card.sprint.number} ${card.sprint.name ?? ''}`.trim() : null);
|
|
927
967
|
const item = card.workItem;
|
|
928
968
|
field('branch', card.branch ?? item?.branch);
|
|
929
969
|
field('work', item ? [item.statusDisplay ?? item.status, item.runnerName && `on ${item.runnerName}`]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { access, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether a workspace folder can be used at all, and how to say so — R377.
|
|
7
|
+
*
|
|
8
|
+
* Node reports a missing `cwd` as `spawn claude ENOENT`, which reads as "the
|
|
9
|
+
* agent CLI is not installed" and sends a person looking at their PATH. The
|
|
10
|
+
* folder is checked before it gets that far, and said in its own words.
|
|
11
|
+
*
|
|
12
|
+
* `.git` is checked with `access`, never `isDirectory()`: in a worktree —
|
|
13
|
+
* including the `-read` copy — it is a file.
|
|
14
|
+
*/
|
|
15
|
+
export async function workspaceProblem(path) {
|
|
16
|
+
try {
|
|
17
|
+
const found = await stat(path);
|
|
18
|
+
if (!found.isDirectory()) return { kind: 'not-a-directory', path };
|
|
19
|
+
} catch (failure) {
|
|
20
|
+
// ENOTDIR: a parent of it is a file, so it is no more there than if absent.
|
|
21
|
+
if (failure.code === 'ENOENT' || failure.code === 'ENOTDIR') return { kind: 'missing', path };
|
|
22
|
+
return { kind: 'unreadable', path, code: failure.code };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await access(join(path, '.git'));
|
|
26
|
+
} catch {
|
|
27
|
+
return { kind: 'not-git', path };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The sentence for a problem. `first` is the project's `workspaces[0]`.
|
|
34
|
+
* The wording must not change from one poll to the next: `noteQueued` only
|
|
35
|
+
* logs again when the text does.
|
|
36
|
+
*/
|
|
37
|
+
export function workspaceProblemSaid(slug, problem, { first = null } = {}) {
|
|
38
|
+
const { kind, path } = problem;
|
|
39
|
+
if (kind === 'missing') {
|
|
40
|
+
const base = `${slug}'s workspace ${path} does not exist on this machine. `
|
|
41
|
+
+ 'Clone the repository there, or correct "workspaces" with `cawdev config`.';
|
|
42
|
+
// `createAutoWorkspace` clones FROM the first workspace, so the advice
|
|
43
|
+
// cannot help when that is the one that is missing.
|
|
44
|
+
return path !== first
|
|
45
|
+
? `${base} Or remove it and set "autoWorkspaces" so the runner clones checkouts beside ${first} itself.`
|
|
46
|
+
: base;
|
|
47
|
+
}
|
|
48
|
+
if (kind === 'not-a-directory') {
|
|
49
|
+
return `${slug}'s workspace ${path} is a file, not a folder. `
|
|
50
|
+
+ 'Correct "workspaces" with `cawdev config`.';
|
|
51
|
+
}
|
|
52
|
+
if (kind === 'unreadable') {
|
|
53
|
+
return `${slug}'s workspace ${path} cannot be read (${problem.code ?? 'unknown error'}). `
|
|
54
|
+
+ 'Fix its permissions, or correct "workspaces" with `cawdev config`.';
|
|
55
|
+
}
|
|
56
|
+
return `${slug}'s workspace ${path} exists but is not a git checkout (no .git in it). `
|
|
57
|
+
+ 'Clone the repository there, or correct "workspaces" with `cawdev config`.';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function problemSync(cwd) {
|
|
61
|
+
if (!cwd || !existsSync(cwd)) return { kind: 'missing', path: cwd };
|
|
62
|
+
if (!statSync(cwd).isDirectory()) return { kind: 'not-a-directory', path: cwd };
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What to tell a person when `spawn` fails. Runs inside an `'error'` handler,
|
|
68
|
+
* hence the synchronous checks. `label` is "the agent", "Antigravity" or "Codex".
|
|
69
|
+
*/
|
|
70
|
+
export function spawnFailureSaid({ failure, command, cwd, slug, label, first = null }) {
|
|
71
|
+
if (failure?.code === 'ENOENT') {
|
|
72
|
+
const problem = problemSync(cwd);
|
|
73
|
+
if (problem) return workspaceProblemSaid(slug, problem, { first });
|
|
74
|
+
return `\`${command}\` not found on this runner's PATH — set "agentCommands" to its full path `
|
|
75
|
+
+ `(run \`which ${command}\` where it works).`;
|
|
76
|
+
}
|
|
77
|
+
return `Could not spawn ${label}: ${failure?.message}`;
|
|
78
|
+
}
|