@boopkit/cli 0.8.0 → 0.8.2
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 -6
- package/dist/capability-catalog.json +10 -3
- package/dist/capability-versions.json +13 -12
- package/dist/cli.js +0 -0
- package/dist/commands/add.js +35 -7
- package/dist/commands/adopt.js +1 -1
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +12 -5
- package/dist/commands/loop-merge.d.ts +11 -0
- package/dist/commands/loop-merge.js +135 -14
- package/dist/commands/loop.d.ts +5 -1
- package/dist/commands/loop.js +38 -19
- package/dist/commands/migrate-skill-edits.js +2 -2
- package/dist/commands/status.d.ts +10 -0
- package/dist/commands/status.js +27 -2
- package/dist/commands/update.js +157 -21
- package/dist/lib/capability.js +4 -1
- package/dist/lib/execute.js +30 -3
- package/dist/lib/install-layout.d.ts +5 -0
- package/dist/lib/install-layout.js +8 -0
- package/dist/lib/manifest.d.ts +1 -1
- package/dist/lib/moves.d.ts +88 -0
- package/dist/lib/moves.js +381 -0
- package/dist/lib/original-text.d.ts +2 -0
- package/dist/lib/original-text.js +15 -7
- package/dist/lib/preflight.d.ts +14 -1
- package/dist/lib/preflight.js +53 -17
- package/dist/lib/project.d.ts +17 -0
- package/dist/lib/project.js +26 -0
- package/dist/lib/scaffold.d.ts +12 -1
- package/dist/lib/scaffold.js +43 -4
- package/dist/lib/stale-scripts.js +54 -14
- package/dist/lib/update.d.ts +36 -2
- package/dist/lib/update.js +82 -4
- package/docs/init.md +22 -0
- package/docs/loop.md +13 -3
- package/docs/skills.md +23 -15
- package/package.json +1 -1
- package/profiles/base/profile.md +13 -10
- package/profiles/development/profile.md +10 -8
- package/profiles/web-app/profile.md +6 -4
- package/profiles/web-app/reference/message-queue-patterns.ref.md +8 -13
- package/src/templates/kernel/tests/app.test.ts.hbs +17 -0
package/dist/commands/loop.js
CHANGED
|
@@ -57,12 +57,14 @@ exports.buildPrompt = buildPrompt;
|
|
|
57
57
|
exports.shouldRotateSession = shouldRotateSession;
|
|
58
58
|
exports.sessionDidWork = sessionDidWork;
|
|
59
59
|
exports.splitStreamLines = splitStreamLines;
|
|
60
|
+
exports.sessionEnv = sessionEnv;
|
|
60
61
|
exports.runLoop = runLoop;
|
|
61
62
|
const child_process_1 = require("child_process");
|
|
62
63
|
const fs = __importStar(require("fs"));
|
|
63
64
|
const path = __importStar(require("path"));
|
|
64
65
|
const crypto_1 = require("crypto");
|
|
65
66
|
const string_decoder_1 = require("string_decoder");
|
|
67
|
+
const init_1 = require("./init");
|
|
66
68
|
/**
|
|
67
69
|
* Output cap for every `bd … --json` read the loop makes.
|
|
68
70
|
*
|
|
@@ -131,13 +133,22 @@ function readReadyQueue(execSyncFn) {
|
|
|
131
133
|
/**
|
|
132
134
|
* Hard requirement: beads must run in Dolt server mode so concurrent worktrees
|
|
133
135
|
* share one issue DB over TCP. Reads `.beads/metadata.json` and throws an
|
|
134
|
-
* actionable error on embedded/missing — call before any `bd` work.
|
|
136
|
+
* actionable error on embedded/missing — call before any `bd` work. A project
|
|
137
|
+
* with no `.beads/` at all (one started with `boop add`) is told the `bd init`
|
|
138
|
+
* command `boop init` would have run. The messages name no skill file, because
|
|
139
|
+
* an installed project may not have the one boop's own repository does.
|
|
135
140
|
*/
|
|
136
141
|
function assertDoltServerMode(projectDir, deps) {
|
|
137
142
|
const existsSyncFn = deps?.existsSyncFn || fs.existsSync;
|
|
138
143
|
const readFileFn = deps?.readFileFn || ((p, enc) => fs.readFileSync(p, enc));
|
|
139
|
-
const
|
|
140
|
-
const
|
|
144
|
+
const beadsDir = path.join(projectDir, '.beads');
|
|
145
|
+
const metaPath = path.join(beadsDir, 'metadata.json');
|
|
146
|
+
if (!existsSyncFn(beadsDir)) {
|
|
147
|
+
const initCommand = `bd ${(0, init_1.bdInitArgs)(path.basename(path.resolve(projectDir))).join(' ')}`;
|
|
148
|
+
throw new Error(`beads is not set up in ${projectDir}: there is no .beads folder. ` +
|
|
149
|
+
`The loop tracks its work in beads, running in Dolt server mode. To set it up, run this from the project folder:\n ${initCommand}`);
|
|
150
|
+
}
|
|
151
|
+
const fix = 'Switch this project\'s beads to Dolt server mode before running the loop.';
|
|
141
152
|
if (!existsSyncFn(metaPath)) {
|
|
142
153
|
throw new Error(`beads metadata not found at ${metaPath}. The loop requires Dolt server mode for concurrent worktrees. ${fix}`);
|
|
143
154
|
}
|
|
@@ -500,8 +511,10 @@ async function planWork(opts) {
|
|
|
500
511
|
const prompt = buildPlanningPrompt(workContent, flowContent, opts.prefix);
|
|
501
512
|
const sessionId = (0, crypto_1.randomUUID)();
|
|
502
513
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
503
|
-
|
|
504
|
-
const
|
|
514
|
+
// A spec-driven run usually has no prefix, so fall back to the spec's base name.
|
|
515
|
+
const runName = opts.prefix || path.basename(opts.workPath, path.extname(opts.workPath));
|
|
516
|
+
const logFile = path.join(opts.logDir, `_planning_${runName}_${timestamp}.log`);
|
|
517
|
+
const readableLogFile = path.join(opts.logDir, `_planning_${runName}_${timestamp}.readable.log`);
|
|
505
518
|
// Write readable log header
|
|
506
519
|
fs.writeFileSync(readableLogFile, `=== Planning Session ===\nSession: ${sessionId}\nStarted: ${new Date().toISOString()}\n\n`);
|
|
507
520
|
console.log(`\x1b[32m[bd-loop]\x1b[0m 📋 Starting planning phase...`);
|
|
@@ -915,7 +928,7 @@ function readInstalledSkills(projectDir) {
|
|
|
915
928
|
/**
|
|
916
929
|
* The installed skills an issue description names, by bare name or by path, in
|
|
917
930
|
* the order each is first mentioned. A name only matches whole, so
|
|
918
|
-
* boop-testing-testing does not match inside boop-testing-testing-
|
|
931
|
+
* boop-testing-testing does not match inside boop-testing-testing-integration.
|
|
919
932
|
*/
|
|
920
933
|
function namedSkills(description, installed) {
|
|
921
934
|
const found = [];
|
|
@@ -1075,6 +1088,24 @@ function splitStreamLines(carry, chunk) {
|
|
|
1075
1088
|
const rest = pieces.pop() ?? '';
|
|
1076
1089
|
return { lines: pieces.filter(Boolean), rest };
|
|
1077
1090
|
}
|
|
1091
|
+
// Propagate the chosen model beyond this session via the environment.
|
|
1092
|
+
// `--model` (in args) sets this session; ANTHROPIC_MODEL additionally
|
|
1093
|
+
// (a) reaches a nested `boop loop` — child processes inherit the env — so it
|
|
1094
|
+
// doesn't reset to the default, and (b) is honored by any sub-agent whose
|
|
1095
|
+
// definition opts into `model: inherit`. We deliberately do NOT set
|
|
1096
|
+
// CLAUDE_CODE_SUBAGENT_MODEL: sub-agents keep their own declared model, so a
|
|
1097
|
+
// deliberately-cheap agent (e.g. Explore on Haiku) stays cheap. The invoking
|
|
1098
|
+
// agent can still override a sub-agent's model per-invocation when it needs
|
|
1099
|
+
// to. See resolveModel().
|
|
1100
|
+
//
|
|
1101
|
+
// CLAUDE_CODE_ENTRYPOINT marks the session as machine-driven, so its transcript
|
|
1102
|
+
// is not mistaken for one the operator typed into. CLAUDE_CODE_DISABLE_AUTO_MEMORY
|
|
1103
|
+
// keeps the operator's auto-memory index, written for their interactive session,
|
|
1104
|
+
// out of a coding session (honoured by Claude Code 2.1.296).
|
|
1105
|
+
function sessionEnv(model, env = process.env) {
|
|
1106
|
+
const machineDriven = { ...env, CLAUDE_CODE_ENTRYPOINT: 'boop-loop', CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1' };
|
|
1107
|
+
return model ? { ...machineDriven, ANTHROPIC_MODEL: model } : machineDriven;
|
|
1108
|
+
}
|
|
1078
1109
|
function runClaude(prompt, opts, logFile, readableLogFile) {
|
|
1079
1110
|
return new Promise((resolve) => {
|
|
1080
1111
|
const args = [
|
|
@@ -1105,19 +1136,7 @@ function runClaude(prompt, opts, logFile, readableLogFile) {
|
|
|
1105
1136
|
const readableStream = readableLogFile
|
|
1106
1137
|
? fs.createWriteStream(readableLogFile, { flags: 'a' })
|
|
1107
1138
|
: null;
|
|
1108
|
-
|
|
1109
|
-
// `--model` (in args) sets this session; ANTHROPIC_MODEL additionally
|
|
1110
|
-
// (a) reaches a nested `boop loop` — child processes inherit the env — so it
|
|
1111
|
-
// doesn't reset to the default, and (b) is honored by any sub-agent whose
|
|
1112
|
-
// definition opts into `model: inherit`. We deliberately do NOT set
|
|
1113
|
-
// CLAUDE_CODE_SUBAGENT_MODEL: sub-agents keep their own declared model, so a
|
|
1114
|
-
// deliberately-cheap agent (e.g. Explore on Haiku) stays cheap. The invoking
|
|
1115
|
-
// agent can still override a sub-agent's model per-invocation when it needs
|
|
1116
|
-
// to. See resolveModel().
|
|
1117
|
-
const childEnv = opts.model
|
|
1118
|
-
? { ...process.env, ANTHROPIC_MODEL: opts.model }
|
|
1119
|
-
: process.env;
|
|
1120
|
-
const child = (0, child_process_1.spawn)(claudeBin, args, { cwd: opts.cwd, stdio: ['pipe', 'pipe', 'pipe'], env: childEnv });
|
|
1139
|
+
const child = (0, child_process_1.spawn)(claudeBin, args, { cwd: opts.cwd, stdio: ['pipe', 'pipe', 'pipe'], env: sessionEnv(opts.model) });
|
|
1121
1140
|
let limitHit = false;
|
|
1122
1141
|
// Track context-window pressure from outside the agent (spec: the loop reads
|
|
1123
1142
|
// the stream it already parses; the agent is never told). Peak occupancy over
|
|
@@ -51,7 +51,7 @@ function runMigrateSkillEdits(deps = {}) {
|
|
|
51
51
|
const projectDir = process.cwd();
|
|
52
52
|
const manifest = (0, project_1.readProjectManifest)(projectDir);
|
|
53
53
|
if (!manifest) {
|
|
54
|
-
console.error('\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run `boop init` first.');
|
|
54
|
+
console.error('\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run `boop init` in an empty folder, or `boop add <capability>` in an existing project, first.');
|
|
55
55
|
process.exitCode = 1;
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
@@ -67,7 +67,7 @@ async function runApplySkillEdits(deps = terminalDeps()) {
|
|
|
67
67
|
const projectDir = process.cwd();
|
|
68
68
|
const manifest = (0, project_1.readProjectManifest)(projectDir);
|
|
69
69
|
if (!manifest) {
|
|
70
|
-
console.error('\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run `boop init` first.');
|
|
70
|
+
console.error('\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run `boop init` in an empty folder, or `boop add <capability>` in an existing project, first.');
|
|
71
71
|
process.exitCode = 1;
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BoopManifest } from '../lib/project';
|
|
2
|
+
import { MoveState } from '../lib/moves';
|
|
2
3
|
export interface CapabilityStatusEnvVar {
|
|
3
4
|
required?: string;
|
|
4
5
|
set: boolean;
|
|
@@ -13,6 +14,15 @@ export interface CapabilityStatus {
|
|
|
13
14
|
reachable?: boolean;
|
|
14
15
|
/** Skills whose SKILL.md this project has edited, which no boop update can improve until the edits move out */
|
|
15
16
|
skillEdits?: SkillEditsStatus;
|
|
17
|
+
/** Paths leaving this capability for another, which the move pass of `boop update` has not settled */
|
|
18
|
+
moves?: PendingMove[];
|
|
19
|
+
}
|
|
20
|
+
export interface PendingMove {
|
|
21
|
+
path: string;
|
|
22
|
+
toCapability: string;
|
|
23
|
+
toPath: string;
|
|
24
|
+
state: MoveState;
|
|
25
|
+
instruction: string;
|
|
16
26
|
}
|
|
17
27
|
export interface SkillEditsStatus {
|
|
18
28
|
edited: number;
|
package/dist/commands/status.js
CHANGED
|
@@ -46,6 +46,7 @@ const capability_1 = require("../lib/capability");
|
|
|
46
46
|
const interpolate_1 = require("../lib/interpolate");
|
|
47
47
|
const db_status_1 = require("../lib/db-status");
|
|
48
48
|
const update_1 = require("../lib/update");
|
|
49
|
+
const moves_1 = require("../lib/moves");
|
|
49
50
|
function formatStatus(manifest) {
|
|
50
51
|
const caps = manifest.capabilities.length > 0
|
|
51
52
|
? manifest.capabilities.join(', ')
|
|
@@ -104,7 +105,7 @@ function getStatusData(manifest, dir) {
|
|
|
104
105
|
const isSet = setKeys.has(varName) || hasLiteralDefault;
|
|
105
106
|
envStatus[varName] = { required: decl.required, set: isSet };
|
|
106
107
|
if (!isSet) {
|
|
107
|
-
const isRequired = decl.required
|
|
108
|
+
const isRequired = decl.required === 'boot' || decl.required === 'runtime';
|
|
108
109
|
if (isRequired) {
|
|
109
110
|
allRequiredSet = false;
|
|
110
111
|
// Look for a setupCheck hint for this env var
|
|
@@ -164,6 +165,21 @@ function getStatusData(manifest, dir) {
|
|
|
164
165
|
capabilities[capName].skillEdits = { ...counts, instruction: update_1.SKILL_EDITS_INSTRUCTION };
|
|
165
166
|
}
|
|
166
167
|
}
|
|
168
|
+
// Every move the move pass finds, listed under the capability the files are leaving
|
|
169
|
+
for (const move of (0, moves_1.planMoves)(dir, manifest, manifest.capabilities).moves) {
|
|
170
|
+
const entry = capabilities[move.fromCapability];
|
|
171
|
+
if (!entry)
|
|
172
|
+
continue;
|
|
173
|
+
const instruction = move.instruction
|
|
174
|
+
?? (move.state === 'split-will-add' ? `\`boop update\` adds ${move.toCapability}.` : 'Run `boop update`.');
|
|
175
|
+
entry.moves = [...(entry.moves ?? []), {
|
|
176
|
+
path: move.from,
|
|
177
|
+
toCapability: move.toCapability,
|
|
178
|
+
toPath: move.to,
|
|
179
|
+
state: move.state,
|
|
180
|
+
instruction,
|
|
181
|
+
}];
|
|
182
|
+
}
|
|
167
183
|
return { name: manifest.name, profile: manifest.profile, capabilities };
|
|
168
184
|
}
|
|
169
185
|
// Re-export for backward compatibility in tests
|
|
@@ -261,6 +277,15 @@ function formatCapabilityLine(capName, cap) {
|
|
|
261
277
|
lines.push(` \x1b[33m◐\x1b[0m ${edited} skill${edited === 1 ? '' : 's'} edited in SKILL.md, ${changesWaiting} with boop changes waiting`);
|
|
262
278
|
lines.push(` → ${instruction}`);
|
|
263
279
|
}
|
|
280
|
+
for (const move of cap.moves ?? []) {
|
|
281
|
+
const from = move.path.replace(/\/$/, '');
|
|
282
|
+
const as = move.toPath.endsWith('/') ? ` as ${path.basename(move.toPath)}` : '';
|
|
283
|
+
const label = move.state === 'split-will-add'
|
|
284
|
+
? `${move.toCapability} split from ${capName}: ${from} moves into it`
|
|
285
|
+
: `${from} moved to ${move.toCapability}${as}, waiting`;
|
|
286
|
+
lines.push(` \x1b[33m◐\x1b[0m ${label}`);
|
|
287
|
+
lines.push(` → ${move.instruction}`);
|
|
288
|
+
}
|
|
264
289
|
return lines;
|
|
265
290
|
}
|
|
266
291
|
/**
|
|
@@ -301,7 +326,7 @@ async function runStatus(options) {
|
|
|
301
326
|
throw err;
|
|
302
327
|
}
|
|
303
328
|
if (!manifest) {
|
|
304
|
-
console.error('Not a boop project (no .boop/config.json found). Run `boop init` to create one.');
|
|
329
|
+
console.error('Not a boop project (no .boop/config.json found). Run `boop init` in an empty folder, or `boop add <capability>` in an existing project, to create one.');
|
|
305
330
|
process.exitCode = 1;
|
|
306
331
|
return;
|
|
307
332
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -40,14 +40,18 @@ const child_process_1 = require("child_process");
|
|
|
40
40
|
const project_1 = require("../lib/project");
|
|
41
41
|
const capability_1 = require("../lib/capability");
|
|
42
42
|
const update_1 = require("../lib/update");
|
|
43
|
+
const moves_1 = require("../lib/moves");
|
|
44
|
+
const capability_2 = require("../lib/capability");
|
|
45
|
+
const preflight_1 = require("../lib/preflight");
|
|
46
|
+
const execute_1 = require("../lib/execute");
|
|
43
47
|
const scaffold_1 = require("../lib/scaffold");
|
|
44
48
|
const stale_scripts_1 = require("../lib/stale-scripts");
|
|
45
49
|
function runUpdate(capabilityName, options) {
|
|
46
50
|
const targetDir = process.cwd();
|
|
47
51
|
// Must be in a boop project
|
|
48
|
-
|
|
52
|
+
let manifest = (0, project_1.readProjectManifest)(targetDir);
|
|
49
53
|
if (!manifest) {
|
|
50
|
-
console.error(`\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run \`boop init\` first.`);
|
|
54
|
+
console.error(`\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run \`boop init\` in an empty folder, or \`boop add <capability>\` in an existing project, first.`);
|
|
51
55
|
process.exitCode = 1;
|
|
52
56
|
return;
|
|
53
57
|
}
|
|
@@ -76,10 +80,44 @@ function runUpdate(capabilityName, options) {
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
|
-
//
|
|
83
|
+
// Step 2: the move pass, against the freshly installed packages. A path a
|
|
84
|
+
// capability stopped shipping because another ships it now is settled before
|
|
85
|
+
// either capability is planned (specs/skill-moves.spec.md).
|
|
86
|
+
let movePlan = (0, moves_1.planMoves)(targetDir, manifest, capsToUpdate);
|
|
87
|
+
const capabilitiesAdded = [];
|
|
88
|
+
let moveResults = new Map();
|
|
89
|
+
if (!options.dryRun) {
|
|
90
|
+
// Step 3: add each capability split out of an installed one, then settle the moves again with it installed
|
|
91
|
+
if (movePlan.splitsToAdd.length > 0) {
|
|
92
|
+
const unaddable = {};
|
|
93
|
+
for (const split of movePlan.splitsToAdd) {
|
|
94
|
+
const reason = addSplitCapability(targetDir, split.capability, (0, moves_1.arrivingPaths)(movePlan.moves, split.capability), options);
|
|
95
|
+
if (reason) {
|
|
96
|
+
unaddable[split.capability] = reason;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
capabilitiesAdded.push(split);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
manifest = (0, project_1.readProjectManifest)(targetDir);
|
|
103
|
+
movePlan = (0, moves_1.planMoves)(targetDir, manifest, [...capsToUpdate, ...capabilitiesAdded.map(a => a.capability)], { unaddable });
|
|
104
|
+
}
|
|
105
|
+
// Step 4: renames, hash transfers and removals, with the manifest written once
|
|
106
|
+
moveResults = (0, moves_1.executeMoves)(targetDir, movePlan.moves);
|
|
107
|
+
manifest = (0, project_1.readProjectManifest)(targetDir);
|
|
108
|
+
}
|
|
109
|
+
// Step 5: plan each capability against the manifest the moves wrote. What a
|
|
110
|
+
// move did not carry out (a hold, a deprecation) belongs to the leaving
|
|
111
|
+
// capability's plan; in a dry run, every move does, and the receiving
|
|
112
|
+
// capability leaves the arriving paths to it.
|
|
80
113
|
const plans = [];
|
|
81
|
-
for (const cap of capsToUpdate) {
|
|
82
|
-
const
|
|
114
|
+
for (const cap of [...capsToUpdate, ...capabilitiesAdded.map(a => a.capability)]) {
|
|
115
|
+
const pending = movePlan.moves.filter(m => m.fromCapability === cap
|
|
116
|
+
&& (options.dryRun || m.outcome === 'hold' || m.outcome === 'deprecate'));
|
|
117
|
+
const plan = (0, update_1.planCapabilityUpdate)(targetDir, cap, manifest, {
|
|
118
|
+
moves: pending.flatMap(moves_1.moveActions),
|
|
119
|
+
arriving: options.dryRun ? (0, moves_1.arrivingPaths)(movePlan.moves, cap) : [],
|
|
120
|
+
});
|
|
83
121
|
if (plan) {
|
|
84
122
|
plans.push(plan);
|
|
85
123
|
}
|
|
@@ -101,6 +139,9 @@ function runUpdate(capabilityName, options) {
|
|
|
101
139
|
console.log(JSON.stringify({ capabilities: plans, staleScripts: stale }, null, 2));
|
|
102
140
|
return;
|
|
103
141
|
}
|
|
142
|
+
for (const split of movePlan.splitsToAdd) {
|
|
143
|
+
console.log(`\nwould add capability ${split.capability} (split from ${split.splitFrom})`);
|
|
144
|
+
}
|
|
104
145
|
for (const plan of plans) {
|
|
105
146
|
console.log(`\n${plan.capability}:`);
|
|
106
147
|
// Recording a hash for a file that already holds what boop ships changes nothing the project can see
|
|
@@ -115,8 +156,9 @@ function runUpdate(capabilityName, options) {
|
|
|
115
156
|
const icon = action.action === 'overwrite' ? '\x1b[32m↑\x1b[0m'
|
|
116
157
|
: action.action === 'skip' ? '\x1b[33m◐\x1b[0m'
|
|
117
158
|
: action.action === 'add' ? '\x1b[32m+\x1b[0m'
|
|
118
|
-
: action.action === 'migrate' ? '\x1b[36m↗\x1b[0m'
|
|
119
|
-
: '\x1b[
|
|
159
|
+
: action.action === 'migrate' || action.action === 'move' ? '\x1b[36m↗\x1b[0m'
|
|
160
|
+
: action.action === 'remove' ? '\x1b[31m✕\x1b[0m'
|
|
161
|
+
: '\x1b[90m⚠\x1b[0m';
|
|
120
162
|
const label = action.action === 'skip' && (action.drift || action.instruction) ? skipLabel(action) : action.reason;
|
|
121
163
|
console.log(` ${icon} ${action.relativePath} — ${label}`);
|
|
122
164
|
if (action.instruction)
|
|
@@ -136,28 +178,31 @@ function runUpdate(capabilityName, options) {
|
|
|
136
178
|
for (const plan of plans) {
|
|
137
179
|
if (plan.upToDate) {
|
|
138
180
|
results.push({
|
|
139
|
-
|
|
140
|
-
overwritten: [],
|
|
141
|
-
skipped: [],
|
|
142
|
-
added: [],
|
|
143
|
-
deprecated: [],
|
|
144
|
-
forcedOverwrite: [],
|
|
145
|
-
skippedDeleted: [],
|
|
146
|
-
pruned: [],
|
|
147
|
-
migrated: [],
|
|
148
|
-
scriptsAdded: [],
|
|
181
|
+
...emptyResult(plan.capability),
|
|
149
182
|
scriptsKept: plan.scriptActions.filter(a => a.action === 'keep'),
|
|
150
183
|
unchangedCount: plan.unchangedCount,
|
|
151
|
-
instructions: [],
|
|
152
184
|
});
|
|
153
185
|
continue;
|
|
154
186
|
}
|
|
155
187
|
const result = (0, update_1.executeUpdatePlan)(plan, { force: options.force, restore: options.restore, prune: options.prune });
|
|
156
188
|
results.push(result);
|
|
157
189
|
}
|
|
190
|
+
// Each executed move is reported under the capability the files left
|
|
191
|
+
for (const moveResult of moveResults.values()) {
|
|
192
|
+
let result = results.find(r => r.capability === moveResult.capability);
|
|
193
|
+
if (!result) {
|
|
194
|
+
result = emptyResult(moveResult.capability);
|
|
195
|
+
results.push(result);
|
|
196
|
+
}
|
|
197
|
+
result.moved.push(...moveResult.moved);
|
|
198
|
+
result.removed.push(...moveResult.removed);
|
|
199
|
+
result.instructions.push(...moveResult.instructions);
|
|
200
|
+
}
|
|
158
201
|
// Refresh .claude/skills/ symlinks for any new skill folders added by this update,
|
|
159
|
-
//
|
|
202
|
+
// put back any PROJECT.md the project has deleted, and bring each capability's
|
|
203
|
+
// stored env settings in line with what its package declares now
|
|
160
204
|
if (!options.dryRun) {
|
|
205
|
+
(0, update_1.refreshCapabilityEnv)(targetDir, plans.map(p => p.capability));
|
|
161
206
|
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
162
207
|
(0, scaffold_1.createSkillProjectFiles)(targetDir);
|
|
163
208
|
}
|
|
@@ -173,17 +218,25 @@ function runUpdate(capabilityName, options) {
|
|
|
173
218
|
if (options.json) {
|
|
174
219
|
console.log(JSON.stringify({
|
|
175
220
|
capabilities: results,
|
|
221
|
+
capabilitiesAdded,
|
|
176
222
|
staleScripts: remainingStale,
|
|
177
223
|
fixedScripts,
|
|
178
224
|
}, null, 2));
|
|
179
225
|
return;
|
|
180
226
|
}
|
|
181
227
|
console.log(`\n${'─'.repeat(40)}`);
|
|
182
|
-
|
|
228
|
+
for (const added of capabilitiesAdded) {
|
|
229
|
+
console.log(`added capability ${added.capability} (split from ${added.splitFrom})`);
|
|
230
|
+
}
|
|
231
|
+
results.forEach((result) => {
|
|
183
232
|
const summary = (0, update_1.formatUpdateSummary)(result);
|
|
184
233
|
console.log(`${result.capability}: ${summary}`);
|
|
185
|
-
const
|
|
234
|
+
const plan = plans.find(p => p.capability === result.capability);
|
|
235
|
+
const actionFor = (f) => plan?.actions.find(a => a.relativePath === f);
|
|
186
236
|
const instructionFor = (f) => result.instructions.find(entry => entry.relativePath === f)?.instruction;
|
|
237
|
+
for (const line of moveLines(result, movePlan)) {
|
|
238
|
+
console.log(line);
|
|
239
|
+
}
|
|
187
240
|
// Show details for non-trivial updates
|
|
188
241
|
for (const f of result.migrated || []) {
|
|
189
242
|
console.log(` \x1b[36m↗\x1b[0m ${f} (migrated to folder)`);
|
|
@@ -217,9 +270,13 @@ function runUpdate(capabilityName, options) {
|
|
|
217
270
|
console.log(` \x1b[32m+\x1b[0m ${f} (new)`);
|
|
218
271
|
}
|
|
219
272
|
for (const f of result.deprecated) {
|
|
273
|
+
const movedTo = actionFor(f)?.toCapability;
|
|
220
274
|
if (result.pruned?.includes(f)) {
|
|
221
275
|
console.log(` \x1b[31m✕\x1b[0m ${f} (pruned, backup saved)`);
|
|
222
276
|
}
|
|
277
|
+
else if (movedTo) {
|
|
278
|
+
console.log(` \x1b[33m⚠\x1b[0m ${f} (deprecated — now ships in ${movedTo}, which this project does not install; use --prune to delete)`);
|
|
279
|
+
}
|
|
223
280
|
else {
|
|
224
281
|
console.log(` \x1b[33m⚠\x1b[0m ${f} (deprecated — removed from package, use --prune to delete)`);
|
|
225
282
|
}
|
|
@@ -235,6 +292,81 @@ function runUpdate(capabilityName, options) {
|
|
|
235
292
|
console.log(`\n${(0, stale_scripts_1.formatStaleScriptWarning)(remainingStale)}`);
|
|
236
293
|
}
|
|
237
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* The report's lines for the moves the update carried out from one capability:
|
|
297
|
+
* each folder or file moved, with where it went, and each skill folder removed
|
|
298
|
+
* because the capability that ships it now is not installed here.
|
|
299
|
+
*/
|
|
300
|
+
function moveLines(result, movePlan) {
|
|
301
|
+
const lines = [];
|
|
302
|
+
const instructionFor = (f) => result.instructions.find(entry => entry.relativePath === f)?.instruction;
|
|
303
|
+
for (const moved of result.moved) {
|
|
304
|
+
const planned = movePlan.moves.find(m => m.fromCapability === result.capability && m.from === moved.from);
|
|
305
|
+
const carried = planned?.projectFileCarried ? '; PROJECT.md carried across' : '';
|
|
306
|
+
lines.push(` \x1b[36m↗\x1b[0m ${moved.from} → ${moved.to} (${moved.toCapability}${carried})`);
|
|
307
|
+
const instruction = instructionFor(moved.from);
|
|
308
|
+
if (instruction)
|
|
309
|
+
lines.push(instructionLine(instruction));
|
|
310
|
+
}
|
|
311
|
+
for (const removed of result.removed) {
|
|
312
|
+
const planned = movePlan.moves.find(m => m.fromCapability === result.capability && m.from === removed);
|
|
313
|
+
const toCapability = planned?.toCapability;
|
|
314
|
+
const name = path.basename(planned?.to ?? removed);
|
|
315
|
+
lines.push(` \x1b[31m✕\x1b[0m ${removed} (removed: now ships in ${toCapability} as ${name}, which this project does not install; \`boop add ${toCapability}\` brings it back)`);
|
|
316
|
+
}
|
|
317
|
+
return lines;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Install a capability split out of an installed one, and add it as `boop add`
|
|
321
|
+
* would, except that the paths arriving by move are not copied: the move
|
|
322
|
+
* decides what happens to them. Returns why it could not be added, or null.
|
|
323
|
+
*/
|
|
324
|
+
function addSplitCapability(targetDir, capability, arriving, options) {
|
|
325
|
+
if (!(0, capability_1.getPackageDir)(capability, [targetDir])) {
|
|
326
|
+
if (options.skipNpm) {
|
|
327
|
+
return `its package is not installed, and --skip-npm kept boop from installing it`;
|
|
328
|
+
}
|
|
329
|
+
const packageName = (0, capability_2.getCatalogEntry)(capability)?.package ?? `@boopkit/cap-${capability}`;
|
|
330
|
+
const spec = options.tag ? `${packageName}@${options.tag}` : packageName;
|
|
331
|
+
try {
|
|
332
|
+
(0, child_process_1.execSync)(`npm install ${spec}`, { cwd: targetDir, stdio: options.json ? 'pipe' : 'inherit' });
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return `npm install ${spec} failed`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const { manifest } = (0, preflight_1.planAdd)(targetDir, capability, { excludePaths: arriving });
|
|
340
|
+
(0, execute_1.executeManifest)(manifest, { install: !options.skipNpm });
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
if (err instanceof preflight_1.PreflightError) {
|
|
345
|
+
return err.checks.filter(c => !c.passed).map(c => c.message).join('; ');
|
|
346
|
+
}
|
|
347
|
+
throw err;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function emptyResult(capability) {
|
|
351
|
+
return {
|
|
352
|
+
capability,
|
|
353
|
+
overwritten: [],
|
|
354
|
+
skipped: [],
|
|
355
|
+
added: [],
|
|
356
|
+
deprecated: [],
|
|
357
|
+
forcedOverwrite: [],
|
|
358
|
+
skippedDeleted: [],
|
|
359
|
+
pruned: [],
|
|
360
|
+
migrated: [],
|
|
361
|
+
scriptsAdded: [],
|
|
362
|
+
scriptsKept: [],
|
|
363
|
+
unchangedCount: 0,
|
|
364
|
+
instructions: [],
|
|
365
|
+
moved: [],
|
|
366
|
+
removed: [],
|
|
367
|
+
movesWaiting: [],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
238
370
|
/**
|
|
239
371
|
* How the report shows the npm scripts a capability declares: an added one reads
|
|
240
372
|
* like a new file, and a kept one shows the command the capability ships, so a
|
|
@@ -265,6 +397,10 @@ function isEditDoingItsJob(action) {
|
|
|
265
397
|
}
|
|
266
398
|
/** What the report says happened to a file the update skipped */
|
|
267
399
|
function skipLabel(action) {
|
|
400
|
+
if (action?.movedTo) {
|
|
401
|
+
const as = action.movedTo.endsWith('/') ? ` as ${path.basename(action.movedTo)}` : '';
|
|
402
|
+
return `moved to ${action.toCapability}${as}, waiting`;
|
|
403
|
+
}
|
|
268
404
|
if (action?.drift === 'project-only')
|
|
269
405
|
return 'edited here, skipped';
|
|
270
406
|
if (action?.drift === 'both-changed') {
|
package/dist/lib/capability.js
CHANGED
|
@@ -105,8 +105,11 @@ function getCapabilityFromPackage(name, searchPaths) {
|
|
|
105
105
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
106
106
|
if (!pkgJson.boopkit?.capability)
|
|
107
107
|
return null;
|
|
108
|
+
// A capability ships as the package it sits in, so its package.json name is
|
|
109
|
+
// the package when capability.json does not say. Five skill capabilities
|
|
110
|
+
// never said, and `boop add` then never made them a project dependency.
|
|
108
111
|
const capJson = JSON.parse(fs.readFileSync(capJsonPath, 'utf-8'));
|
|
109
|
-
return capJson;
|
|
112
|
+
return { ...capJson, package: capJson.package ?? pkgJson.name };
|
|
110
113
|
}
|
|
111
114
|
/**
|
|
112
115
|
* Scan node_modules/@boopkit/cap-* to find a package whose capability.json
|
package/dist/lib/execute.js
CHANGED
|
@@ -62,7 +62,7 @@ function executeManifest(manifest, options = {}) {
|
|
|
62
62
|
// Back up each mutable target file once before executing any tasks
|
|
63
63
|
const backedUp = new Set();
|
|
64
64
|
for (const task of manifest.tasks) {
|
|
65
|
-
if (task.op === 'copy-files' || task.op === 'run-command')
|
|
65
|
+
if (task.op === 'copy-files' || task.op === 'run-command' || task.op === 'install-package')
|
|
66
66
|
continue;
|
|
67
67
|
if (task.target && fs.existsSync(task.target) && !backedUp.has(task.target)) {
|
|
68
68
|
(0, safe_io_1.createBackup)(task.target);
|
|
@@ -99,8 +99,25 @@ function executeManifest(manifest, options = {}) {
|
|
|
99
99
|
function executeTask(task, manifest) {
|
|
100
100
|
const { targetDir, capability: capabilityName } = manifest;
|
|
101
101
|
switch (task.op) {
|
|
102
|
+
case 'create-package-json': {
|
|
103
|
+
const data = task.data;
|
|
104
|
+
fs.writeFileSync(task.target, (0, scaffold_1.renderBasePackageJson)(data.projectName), 'utf-8');
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'create-manifest': {
|
|
108
|
+
const data = task.data;
|
|
109
|
+
(0, project_1.writeProjectManifest)(targetDir, data.manifest);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'install-package': {
|
|
113
|
+
const data = task.data;
|
|
114
|
+
console.error(`Running ${data.command}...`);
|
|
115
|
+
(0, child_process_1.execSync)(data.command, { cwd: targetDir, stdio: 'pipe' });
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
102
118
|
case 'copy-files': {
|
|
103
|
-
|
|
119
|
+
const data = task.data;
|
|
120
|
+
(0, scaffold_1.copyCapabilityFiles)(targetDir, capabilityName, undefined, data.exclude);
|
|
104
121
|
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
105
122
|
(0, scaffold_1.createSkillProjectFiles)(targetDir);
|
|
106
123
|
break;
|
|
@@ -169,7 +186,17 @@ function executeTask(task, manifest) {
|
|
|
169
186
|
case 'run-command': {
|
|
170
187
|
const data = task.data;
|
|
171
188
|
console.log(`\nRunning ${data.command}...`);
|
|
172
|
-
|
|
189
|
+
// stderr is captured rather than inherited so that a failure's warning
|
|
190
|
+
// carries the command's own error text; on success it is passed through.
|
|
191
|
+
const result = (0, child_process_1.spawnSync)(data.command, { cwd: targetDir, shell: true, stdio: ['inherit', 'inherit', 'pipe'] });
|
|
192
|
+
if (result.error)
|
|
193
|
+
throw result.error;
|
|
194
|
+
const stderr = result.stderr.toString();
|
|
195
|
+
if (result.status !== 0) {
|
|
196
|
+
const exit = result.signal ? `signal ${result.signal}` : `exit code ${result.status}`;
|
|
197
|
+
throw new Error(`Command failed (${exit}): ${data.command}\n${stderr}`);
|
|
198
|
+
}
|
|
199
|
+
process.stderr.write(stderr);
|
|
173
200
|
break;
|
|
174
201
|
}
|
|
175
202
|
}
|
|
@@ -52,6 +52,11 @@ export declare function describePlacementOwner(placement: Placement): string;
|
|
|
52
52
|
* has no need of npm module resolution (and must not require a build).
|
|
53
53
|
*/
|
|
54
54
|
export declare function capabilityPlacementsFromDir(pkgDir: string): Placement[];
|
|
55
|
+
/**
|
|
56
|
+
* Whether a project-relative path is one of `paths`, or lies inside one that
|
|
57
|
+
* names a folder (ends in `/`).
|
|
58
|
+
*/
|
|
59
|
+
export declare function isUnderAny(relPath: string, paths: string[]): boolean;
|
|
55
60
|
/**
|
|
56
61
|
* Every file a capability ships and where it lands in a project.
|
|
57
62
|
*
|
|
@@ -53,6 +53,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
53
53
|
exports.PlacementCollisionError = void 0;
|
|
54
54
|
exports.describePlacementOwner = describePlacementOwner;
|
|
55
55
|
exports.capabilityPlacementsFromDir = capabilityPlacementsFromDir;
|
|
56
|
+
exports.isUnderAny = isUnderAny;
|
|
56
57
|
exports.capabilityPlacements = capabilityPlacements;
|
|
57
58
|
exports.profilePlacements = profilePlacements;
|
|
58
59
|
exports.skillDirectoryPlacements = skillDirectoryPlacements;
|
|
@@ -160,6 +161,13 @@ function capabilityPlacementsFromDir(pkgDir) {
|
|
|
160
161
|
}
|
|
161
162
|
return placements;
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Whether a project-relative path is one of `paths`, or lies inside one that
|
|
166
|
+
* names a folder (ends in `/`).
|
|
167
|
+
*/
|
|
168
|
+
function isUnderAny(relPath, paths) {
|
|
169
|
+
return paths.some(p => p.endsWith('/') ? relPath.startsWith(p) : relPath === p);
|
|
170
|
+
}
|
|
163
171
|
function isCapabilityPackage(pkgDir) {
|
|
164
172
|
const pkgJsonPath = path.join(pkgDir, 'package.json');
|
|
165
173
|
if (!fs.existsSync(pkgJsonPath))
|
package/dist/lib/manifest.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Phase 2 (execute) walks the manifest, performing each operation and
|
|
8
8
|
* updating task status.
|
|
9
9
|
*/
|
|
10
|
-
export type ManifestOp = 'copy-files' | 'merge-deps' | 'merge-scripts' | 'link-package' | 'inject-hook' | 'inject-css' | 'set-env' | 'update-manifest' | 'run-command';
|
|
10
|
+
export type ManifestOp = 'create-package-json' | 'create-manifest' | 'install-package' | 'copy-files' | 'merge-deps' | 'merge-scripts' | 'link-package' | 'inject-hook' | 'inject-css' | 'set-env' | 'update-manifest' | 'run-command';
|
|
11
11
|
export type TaskStatus = 'pending' | 'done' | 'failed' | 'skipped';
|
|
12
12
|
export interface ManifestTask {
|
|
13
13
|
op: ManifestOp;
|