@jaybeeuu/agent-cortex 2.6.0 → 2.8.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/CHANGELOG.md +269 -0
- package/README.md +102 -8
- package/agents/README.md +2 -2
- package/agents/ralph/agent.md +19 -16
- package/agents/ralph/claude/wake.md +4 -4
- package/agents/ralph/pi/wake.md +15 -20
- package/bin/agent-cortex.mjs +176 -67
- package/bin/installers/claude.mjs +33 -8
- package/bin/installers/pi.mjs +11 -1
- package/docs/skills/skill-anatomy.md +212 -0
- package/extensions/post-merge-watch/README.md +133 -0
- package/extensions/post-merge-watch/index.test.ts +247 -0
- package/extensions/post-merge-watch/index.ts +123 -0
- package/extensions/post-merge-watch/package.json +16 -0
- package/extensions/post-merge-watch/watch.test.ts +469 -0
- package/extensions/post-merge-watch/watch.ts +431 -0
- package/extensions/session-clear/README.md +74 -0
- package/extensions/session-clear/index.test.ts +524 -0
- package/extensions/session-clear/index.ts +159 -0
- package/extensions/session-clear/package.json +16 -0
- package/lib/cli.mjs +88 -1
- package/lib/harness-update.mjs +254 -0
- package/lib/harness-versions.mjs +36 -2
- package/lib/self-update.mjs +553 -0
- package/package.json +2 -1
- package/scripts/lib/docs.mjs +67 -0
- package/skills/engineering/implement-task/SKILL.md +124 -0
- package/skills/engineering/tdd/SKILL.md +2 -0
- package/skills/planning/create-task/REFERENCE.md +20 -3
- package/skills/planning/create-task/SKILL.md +1 -1
- package/skills/planning/create-task/pipeline.json +16 -0
- package/skills/planning/create-task/scripts/create-chores.mjs +51 -24
- package/skills/planning/create-task/templates/code.md +8 -3
- package/skills/planning/create-task/templates/document.md +8 -3
- package/skills/planning/create-task/templates/fix.md +8 -3
- package/skills/planning/create-task/templates/housekeeping.md +69 -0
- package/skills/planning/create-task/templates/post-merge.md +67 -0
- package/skills/planning/create-task/templates/review.md +8 -3
- package/skills/planning/create-task/templates/verify.md +8 -3
- package/skills/productivity/bd-tool/SKILL.md +55 -0
- package/skills/productivity/write-a-skill/SKILL.md +2 -2
- package/skills/review/refactor-skill/SKILL.md +2 -2
- package/skills/review/review-defects/EXAMPLES.md +122 -0
- package/skills/review/review-defects/SKILL.md +91 -0
- package/skills/style/style-tests/EXAMPLES.md +30 -0
- package/skills/style/style-tests/SKILL.md +10 -6
- package/skills/workflow/plan/SKILL.md +14 -3
- package/skills/workflow/ralph/REFERENCE.md +137 -30
- package/skills/workflow/ralph/SKILL.md +26 -16
- package/skills/workflow/ralph/scripts/post-merge-check.mjs +651 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +41 -23
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +14 -8
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +2 -2
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -2
- package/skills/workflow/run-pipeline-stage/playbooks/housekeeping.md +58 -0
- package/skills/workflow/run-pipeline-stage/playbooks/post-merge.md +54 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +34 -3
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +2 -2
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +66 -10
- package/skills/workflow/run-pipeline-stage/scripts/recover-brief.mjs +805 -0
- package/skills/workflow/run-pipeline-stage/scripts/trail-entry.mjs +702 -0
- package/skills/workflow/using-agent-skills/SKILL.md +15 -7
- package/token-map.README.md +41 -4
- package/token-map.json +16 -5
package/lib/cli.mjs
CHANGED
|
@@ -4,10 +4,14 @@ import { basename } from "node:path";
|
|
|
4
4
|
* Result of parsing the CLI argv. Every branch returns `command`; the rest are
|
|
5
5
|
* populated only for the commands that accept them.
|
|
6
6
|
* @typedef {object} ParsedArgs
|
|
7
|
-
* @property {"help" | "version" | "install" | "migrate" | "unknown"} command
|
|
7
|
+
* @property {"help" | "version" | "install" | "update" | "self-update" | "migrate" | "unknown"} command
|
|
8
8
|
* @property {boolean} [report] `version` subcommand: report installed harnesses too
|
|
9
9
|
* @property {boolean} [json] `version --json`: print the report as JSON
|
|
10
|
+
* @property {boolean} [check] `self-update --check`: report and stop
|
|
10
11
|
* @property {string | null} [harness]
|
|
12
|
+
* @property {string[]} [harnesses] `update`: the harnesses named on the command line
|
|
13
|
+
* @property {boolean} [all] `update -a, --all`: every harness that is not at the CLI version
|
|
14
|
+
* @property {boolean} [force] `update -f, --force`: re-run every installer regardless of version
|
|
11
15
|
* @property {string} [name]
|
|
12
16
|
* @property {string} [optionError]
|
|
13
17
|
* @property {boolean} [dryRun]
|
|
@@ -125,6 +129,50 @@ export function parseArgs(argv, programName = DEFAULT_PROGRAM_NAME) {
|
|
|
125
129
|
return { command: "install", harness, ...options };
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
if (command === "update") {
|
|
133
|
+
// `update <harness>...` refreshes exactly those harnesses; `-a` refreshes
|
|
134
|
+
// every harness that is not at the CLI version and `-f` forces a re-run.
|
|
135
|
+
// `-f` alone has no meaning, so it is rejected rather than ignored.
|
|
136
|
+
/** @type {string[]} */
|
|
137
|
+
const harnesses = [];
|
|
138
|
+
let all = false;
|
|
139
|
+
let force = false;
|
|
140
|
+
for (const arg of rest) {
|
|
141
|
+
if (arg === "--help" || arg === "-h") return { command: "help" };
|
|
142
|
+
if (arg === "-a" || arg === "--all") {
|
|
143
|
+
all = true;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (arg === "-f" || arg === "--force") {
|
|
147
|
+
force = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (arg.startsWith("-")) {
|
|
151
|
+
return { command: "update", optionError: `Unknown option "${arg}". Run "${programName} --help" for usage.` };
|
|
152
|
+
}
|
|
153
|
+
harnesses.push(arg);
|
|
154
|
+
}
|
|
155
|
+
if (force && !all) {
|
|
156
|
+
return { command: "update", optionError: `"--force" requires "--all". Run "${programName} --help" for usage.` };
|
|
157
|
+
}
|
|
158
|
+
return { command: "update", harnesses, all, force };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (command === "self-update") {
|
|
162
|
+
// A bare run acts; `--check` reports and stops. Nothing else is accepted,
|
|
163
|
+
// so a typo fails before the CLI touches the global install.
|
|
164
|
+
let check = false;
|
|
165
|
+
for (const arg of rest) {
|
|
166
|
+
if (arg === "--help" || arg === "-h") return { command: "help" };
|
|
167
|
+
if (arg === "--check") {
|
|
168
|
+
check = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
return { command: "self-update", optionError: `Unknown option "${arg}". Run "${programName} --help" for usage.` };
|
|
172
|
+
}
|
|
173
|
+
return { command: "self-update", check };
|
|
174
|
+
}
|
|
175
|
+
|
|
128
176
|
if (command === "migrate") {
|
|
129
177
|
// Dry run is the default, so `--apply` is the only state-changing flag;
|
|
130
178
|
// `--dry-run` stays accepted for scripts that pass it explicitly. `--dir`
|
|
@@ -239,6 +287,45 @@ Commands:
|
|
|
239
287
|
cannot register the staged package
|
|
240
288
|
(missing from PATH); default warns and
|
|
241
289
|
prints the manual commands
|
|
290
|
+
update [<harness>...] Refresh installed harnesses by re-running their
|
|
291
|
+
installers. With no argument the command lists every
|
|
292
|
+
harness with its installed version and the CLI version,
|
|
293
|
+
then asks which to refresh (a non-interactive shell
|
|
294
|
+
prints the same table and the flags below, then exits
|
|
295
|
+
non-zero without blocking).
|
|
296
|
+
Options:
|
|
297
|
+
<harness>... Refresh exactly these harnesses, no prompt
|
|
298
|
+
-a, --all Refresh every harness that is not at the
|
|
299
|
+
CLI version (not installed, stale, or
|
|
300
|
+
ahead); ignored when harnesses are named
|
|
301
|
+
-f, --force With --all: re-run every installer even
|
|
302
|
+
when the version already matches
|
|
303
|
+
--help, -h Show this help text
|
|
304
|
+
self-update Update the globally installed CLI through the manager that
|
|
305
|
+
owns this install, then refresh the harness installs with
|
|
306
|
+
the binary the update installed. A bare run acts; --check
|
|
307
|
+
only reports. The manager is inferred from the running
|
|
308
|
+
entry point's real path (pnpm's global/<v> or store link,
|
|
309
|
+
or npm's lib/node_modules), then confirmed by asking that
|
|
310
|
+
manager to list this package among its globals at the
|
|
311
|
+
version installed beside the entry point; a manager on
|
|
312
|
+
PATH is never used in its place. An owner that cannot be
|
|
313
|
+
inferred prints both managers' install commands and exits
|
|
314
|
+
1; an inferred manager that does not confirm prints its own
|
|
315
|
+
command, says nothing was updated, and exits 1. Otherwise
|
|
316
|
+
it prints the CLI version before and after the install,
|
|
317
|
+
then runs the new binary's "update -a" with this terminal,
|
|
318
|
+
so the refresh's own output is visible, and exits with the
|
|
319
|
+
refresh's exit code. The refresh is skipped after --check,
|
|
320
|
+
after a failed update, and when the installed version
|
|
321
|
+
already matches the published one; that case installs
|
|
322
|
+
nothing and points at "update -a". A refresh that cannot
|
|
323
|
+
start prints the error and the command to run by hand, and
|
|
324
|
+
still exits non-zero.
|
|
325
|
+
Options:
|
|
326
|
+
(no option) Report, update, then refresh harnesses
|
|
327
|
+
--check Report the installed and published
|
|
328
|
+
versions and stop, writing nothing
|
|
242
329
|
migrate Migrate the current beads workspace to the latest
|
|
243
330
|
agent-cortex migration version, applying versioned steps
|
|
244
331
|
in order (repo-agnostic — works in any beads workspace)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// The selection and reporting half of `ctx update`: which harnesses a refresh
|
|
2
|
+
// covers, how the table and the report read, and the interactive prompt.
|
|
3
|
+
//
|
|
4
|
+
// Delegation is not here. The caller injects `refresh(harness)`, which the CLI
|
|
5
|
+
// wires to the harness installers — so update-vs-install stays the installer's
|
|
6
|
+
// decision and this module never writes an artifact itself.
|
|
7
|
+
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { collectHarnessStatus } from "./harness-versions.mjs";
|
|
10
|
+
|
|
11
|
+
/** @typedef {import("./harness-versions.mjs").HarnessStatus} HarnessStatus */
|
|
12
|
+
|
|
13
|
+
const NOT_INSTALLED = "not installed";
|
|
14
|
+
const UNKNOWN = "unknown";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One refreshed harness in the update report.
|
|
18
|
+
* @typedef {object} UpdateEntry
|
|
19
|
+
* @property {string} harness
|
|
20
|
+
* @property {string} previous The version before the refresh, or "not installed"
|
|
21
|
+
* @property {string} next The version the installer recorded afterwards
|
|
22
|
+
* @property {string[]} artifacts What the installer wrote
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The harnesses an update covers, and the ones it leaves alone.
|
|
27
|
+
* @typedef {object} UpdatePlan
|
|
28
|
+
* @property {HarnessStatus[]} targets
|
|
29
|
+
* @property {HarnessStatus[]} skipped
|
|
30
|
+
* @property {string[]} unknown Names that are not supported harnesses
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decide which harnesses an update refreshes. An explicit name is always a
|
|
35
|
+
* target (the user asked for that installer to re-run); `--all` targets every
|
|
36
|
+
* harness that is not at the CLI version; `--all --force` targets every harness.
|
|
37
|
+
* @param {object} options
|
|
38
|
+
* @param {HarnessStatus[]} options.statuses One entry per supported harness
|
|
39
|
+
* @param {string[]} [options.harnesses] Names from the command line or the prompt
|
|
40
|
+
* @param {boolean} [options.all] `ctx update -a`
|
|
41
|
+
* @param {boolean} [options.force] `ctx update -a -f`
|
|
42
|
+
* @returns {UpdatePlan}
|
|
43
|
+
*/
|
|
44
|
+
export function selectHarnesses({ statuses, harnesses = [], all = false, force = false }) {
|
|
45
|
+
/** @type {string[]} */
|
|
46
|
+
const unknown = [];
|
|
47
|
+
/** @type {HarnessStatus[]} */
|
|
48
|
+
let targets;
|
|
49
|
+
if (harnesses.length > 0) {
|
|
50
|
+
targets = [];
|
|
51
|
+
for (const name of harnesses) {
|
|
52
|
+
const status = statuses.find((entry) => entry.harness === name);
|
|
53
|
+
if (status === undefined) {
|
|
54
|
+
if (!unknown.includes(name)) unknown.push(name);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!targets.includes(status)) targets.push(status);
|
|
58
|
+
}
|
|
59
|
+
} else if (all && force) {
|
|
60
|
+
targets = [...statuses];
|
|
61
|
+
} else if (all) {
|
|
62
|
+
targets = statuses.filter((entry) => entry.staleness !== "current");
|
|
63
|
+
} else {
|
|
64
|
+
targets = [];
|
|
65
|
+
}
|
|
66
|
+
const skipped = statuses.filter((entry) => !targets.includes(entry));
|
|
67
|
+
return { targets, skipped, unknown };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Count a noun for the report: `1 file`, `4 files`.
|
|
72
|
+
* @param {number} amount
|
|
73
|
+
* @param {string} noun Singular form
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function count(amount, noun) {
|
|
77
|
+
return `${amount} ${noun}${amount === 1 ? "" : "s"}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* What a pi refresh wrote, for the update report.
|
|
82
|
+
* @param {import("../bin/installers/pi.mjs").InstallPiResult} result
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
*/
|
|
85
|
+
export function describePiArtifacts(result) {
|
|
86
|
+
return [
|
|
87
|
+
`agents (${count(result.agents.length, "file")})`,
|
|
88
|
+
`skills (${count(result.skills.skills, "skill")}, ${count(result.skills.md, "markdown file")})`,
|
|
89
|
+
`extensions (${count(result.extensions.files, "file")})`,
|
|
90
|
+
`bundled pi packages (${result.packages.bundled.length > 0 ? result.packages.bundled.join(", ") : "none"})`,
|
|
91
|
+
`package manifest (version ${result.manifest.manifest.version})`,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What a claude refresh wrote, for the update report.
|
|
97
|
+
* @param {import("../bin/installers/claude.mjs").InstallClaudeResult} result
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
export function describeClaudeArtifacts(result) {
|
|
101
|
+
return [
|
|
102
|
+
`agents (${count(result.agents.length, "file")})`,
|
|
103
|
+
`skills (${count(result.skills.names.length, "skill")}, ${count(result.skills.md, "markdown file")})`,
|
|
104
|
+
`hooks (${count(result.hookFiles.length, "file")})`,
|
|
105
|
+
`hand-authored extras (${count(result.handAuthored.length, "file")})`,
|
|
106
|
+
`plugin manifest (version ${result.pluginVersion})`,
|
|
107
|
+
`settings (${result.settings?.action ?? "not written"})`,
|
|
108
|
+
`instructions (${result.instructions?.action ?? "not written"})`,
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Refresh every target through the injected installer and report the result.
|
|
114
|
+
* The new version is read back from the fingerprint the installer wrote, never
|
|
115
|
+
* assumed from the CLI version, so a failed materialisation shows as unknown.
|
|
116
|
+
* @param {object} options
|
|
117
|
+
* @param {string} options.cli The running CLI version
|
|
118
|
+
* @param {string} [options.home] Home directory holding the install roots
|
|
119
|
+
* @param {HarnessStatus[]} options.targets
|
|
120
|
+
* @param {(harness: string) => Promise<string[]>} options.refresh Runs one harness installer
|
|
121
|
+
* @returns {Promise<UpdateEntry[]>}
|
|
122
|
+
*/
|
|
123
|
+
export async function runUpdate({ cli, home = homedir(), targets, refresh }) {
|
|
124
|
+
/** @type {{ harness: string, previous: string, artifacts: string[] }[]} */
|
|
125
|
+
const refreshed = [];
|
|
126
|
+
for (const target of targets) {
|
|
127
|
+
const artifacts = await refresh(target.harness);
|
|
128
|
+
refreshed.push({ harness: target.harness, previous: target.installed ?? NOT_INSTALLED, artifacts });
|
|
129
|
+
}
|
|
130
|
+
if (refreshed.length === 0) return [];
|
|
131
|
+
const statuses = await collectHarnessStatus({ cli, home });
|
|
132
|
+
return refreshed.map((entry) => {
|
|
133
|
+
const status = statuses.find((candidate) => candidate.harness === entry.harness);
|
|
134
|
+
return { ...entry, next: status?.installed ?? UNKNOWN };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Render the harness table: one row per supported harness, so an absent
|
|
140
|
+
* install is visible instead of silently missing.
|
|
141
|
+
* @param {HarnessStatus[]} statuses
|
|
142
|
+
* @returns {string} Text without a trailing newline
|
|
143
|
+
*/
|
|
144
|
+
export function formatHarnessTable(statuses) {
|
|
145
|
+
/** @type {string[][]} */
|
|
146
|
+
const rows = [["harness", "installed", "cli", "status"]];
|
|
147
|
+
for (const status of statuses) {
|
|
148
|
+
rows.push([status.harness, status.installed ?? NOT_INSTALLED, status.cli, status.staleness]);
|
|
149
|
+
}
|
|
150
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
|
|
151
|
+
return rows
|
|
152
|
+
.map((row) => row.map((cell, column) => (column === widths.length - 1 ? cell : cell.padEnd(widths[column]))).join(" "))
|
|
153
|
+
.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The instructions a non-interactive `ctx update` prints instead of prompting.
|
|
158
|
+
* @param {string} programName The resolved command name
|
|
159
|
+
* @returns {string} Text without a trailing newline
|
|
160
|
+
*/
|
|
161
|
+
export function formatUpdateFlags(programName) {
|
|
162
|
+
return [
|
|
163
|
+
"No harness selected, and this shell is not interactive. Pass a selection:",
|
|
164
|
+
` ${programName} update <harness>... refresh the named harnesses`,
|
|
165
|
+
` ${programName} update -a refresh every harness that is not at the CLI version`,
|
|
166
|
+
` ${programName} update -a -f re-run every harness installer regardless of version`,
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Render the outcome of an update. Only skipped harnesses that are already at
|
|
172
|
+
* the CLI version are listed; one left alone for another reason (a stale
|
|
173
|
+
* harness beside an explicit selection) is not "already at" anything.
|
|
174
|
+
* @param {object} options
|
|
175
|
+
* @param {string} options.cli The running CLI version
|
|
176
|
+
* @param {UpdateEntry[]} options.entries
|
|
177
|
+
* @param {HarnessStatus[]} [options.skipped]
|
|
178
|
+
* @returns {string} Text without a trailing newline
|
|
179
|
+
*/
|
|
180
|
+
export function formatUpdateReport({ cli, entries, skipped = [] }) {
|
|
181
|
+
const current = skipped.filter((entry) => entry.staleness === "current");
|
|
182
|
+
/** @type {string[]} */
|
|
183
|
+
const lines = [];
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
lines.push(`${entry.harness} ${entry.previous} → ${entry.next}`);
|
|
186
|
+
for (const artifact of entry.artifacts) {
|
|
187
|
+
lines.push(` ✓ ${artifact}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (entries.length === 0) {
|
|
191
|
+
lines.push(`Nothing to update — every harness is at ${cli}.`);
|
|
192
|
+
} else if (current.length > 0) {
|
|
193
|
+
lines.push(`Skipped (already at ${cli}): ${current.map((entry) => entry.harness).join(", ")}`);
|
|
194
|
+
}
|
|
195
|
+
return lines.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Turn a prompt answer into harness names: row numbers, `a` for every row, or
|
|
200
|
+
* blank for none.
|
|
201
|
+
* @param {string} answer
|
|
202
|
+
* @param {HarnessStatus[]} statuses The rows the prompt listed, in order
|
|
203
|
+
* @returns {string[]}
|
|
204
|
+
*/
|
|
205
|
+
export function parseHarnessSelection(answer, statuses) {
|
|
206
|
+
const trimmed = answer.trim();
|
|
207
|
+
if (trimmed === "") return [];
|
|
208
|
+
if (/^(a|all)$/i.test(trimmed)) return statuses.map((status) => status.harness);
|
|
209
|
+
/** @type {string[]} */
|
|
210
|
+
const chosen = [];
|
|
211
|
+
for (const token of trimmed.split(/[,\s]+/)) {
|
|
212
|
+
const index = Number(token);
|
|
213
|
+
const status = Number.isInteger(index) ? statuses[index - 1] : undefined;
|
|
214
|
+
if (status === undefined) {
|
|
215
|
+
throw new Error(`Invalid selection "${token}". Enter row numbers 1-${statuses.length}, "a", or blank to cancel.`);
|
|
216
|
+
}
|
|
217
|
+
if (!chosen.includes(status.harness)) chosen.push(status.harness);
|
|
218
|
+
}
|
|
219
|
+
return chosen;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Ask on an interactive terminal which harnesses to refresh. The table is the
|
|
224
|
+
* list the user picks from; a closed input stream cancels the selection.
|
|
225
|
+
* @param {object} options
|
|
226
|
+
* @param {HarnessStatus[]} options.statuses
|
|
227
|
+
* @param {NodeJS.ReadableStream} [options.input]
|
|
228
|
+
* @param {NodeJS.WritableStream} [options.output]
|
|
229
|
+
* @returns {Promise<string[]>}
|
|
230
|
+
*/
|
|
231
|
+
export async function promptHarnessSelection({ statuses, input = process.stdin, output = process.stdout }) {
|
|
232
|
+
output.write(`${formatHarnessTable(statuses)}\n\n`);
|
|
233
|
+
const answer = await readLine(input, output, `Select harnesses (row numbers, "a" for all, blank to cancel): `);
|
|
234
|
+
return parseHarnessSelection(answer, statuses);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Read one line, prompting on `output` first. A stream that ends without a
|
|
239
|
+
* newline returns what it carried, so a closed stdin cancels rather than hangs.
|
|
240
|
+
* @param {NodeJS.ReadableStream} input
|
|
241
|
+
* @param {NodeJS.WritableStream} output
|
|
242
|
+
* @param {string} question
|
|
243
|
+
* @returns {Promise<string>}
|
|
244
|
+
*/
|
|
245
|
+
async function readLine(input, output, question) {
|
|
246
|
+
output.write(question);
|
|
247
|
+
let buffered = "";
|
|
248
|
+
for await (const chunk of input) {
|
|
249
|
+
buffered += String(chunk);
|
|
250
|
+
const newline = buffered.indexOf("\n");
|
|
251
|
+
if (newline !== -1) return buffered.slice(0, newline);
|
|
252
|
+
}
|
|
253
|
+
return buffered;
|
|
254
|
+
}
|
package/lib/harness-versions.mjs
CHANGED
|
@@ -29,8 +29,9 @@ const HARNESS_FINGERPRINTS = [
|
|
|
29
29
|
/**
|
|
30
30
|
* How an installed copy relates to the running CLI. `unknown` covers both a
|
|
31
31
|
* fingerprint that cannot be read and a version that is not a plain release —
|
|
32
|
-
* an unreadable install is never reported as `current`.
|
|
33
|
-
*
|
|
32
|
+
* an unreadable install is never reported as `current`. `missing` means the
|
|
33
|
+
* harness has no artifact root at all: it was never installed.
|
|
34
|
+
* @typedef {"current" | "stale" | "ahead" | "unknown" | "missing"} Staleness
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -107,6 +108,16 @@ async function readInstalledVersion(home, fingerprint) {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* One harness in the update table: unlike a report entry, every supported
|
|
113
|
+
* harness appears, so an absent install shows as `installed: null` / `missing`.
|
|
114
|
+
* @typedef {object} HarnessStatus
|
|
115
|
+
* @property {string} harness
|
|
116
|
+
* @property {string | null} installed The installed version, or null when the harness is not installed
|
|
117
|
+
* @property {string} cli The running CLI version
|
|
118
|
+
* @property {Staleness} staleness
|
|
119
|
+
*/
|
|
120
|
+
|
|
110
121
|
/**
|
|
111
122
|
* Report the running CLI version and the installed version of every harness
|
|
112
123
|
* whose artifact root exists.
|
|
@@ -130,6 +141,29 @@ export async function collectVersionReport({ cli, home = homedir() }) {
|
|
|
130
141
|
return { cli, harnesses };
|
|
131
142
|
}
|
|
132
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Read the status of every harness, including the ones that are not installed:
|
|
146
|
+
* `ctx update` selects from this full list, so a harness with no artifact root
|
|
147
|
+
* is reported as missing rather than omitted.
|
|
148
|
+
* @param {{ cli: string, home?: string }} options `home` defaults to the real
|
|
149
|
+
* home directory; tests pass a fixture directory
|
|
150
|
+
* @returns {Promise<HarnessStatus[]>}
|
|
151
|
+
*/
|
|
152
|
+
export async function collectHarnessStatus({ cli, home = homedir() }) {
|
|
153
|
+
/** @type {HarnessStatus[]} */
|
|
154
|
+
const statuses = [];
|
|
155
|
+
for (const fingerprint of HARNESS_FINGERPRINTS) {
|
|
156
|
+
const { installed, version } = await readInstalledVersion(home, fingerprint);
|
|
157
|
+
statuses.push({
|
|
158
|
+
harness: fingerprint.harness,
|
|
159
|
+
installed: version,
|
|
160
|
+
cli,
|
|
161
|
+
staleness: installed ? stalenessOf(version, cli) : "missing",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return statuses;
|
|
165
|
+
}
|
|
166
|
+
|
|
133
167
|
/**
|
|
134
168
|
* Render the report as the default, human-readable output: the CLI version on
|
|
135
169
|
* the first line, then one line per installed harness. A harness whose version
|