@boopkit/cli 0.6.14 → 0.6.15

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.
@@ -52,6 +52,7 @@ const capability_1 = require("./capability");
52
52
  const project_1 = require("./project");
53
53
  const safe_io_1 = require("./safe-io");
54
54
  const install_layout_1 = require("./install-layout");
55
+ const script_merge_1 = require("./script-merge");
55
56
  /** The skill that splits a drifted SKILL.md — keep in step with the folder name boop-rm2l.8 gives it */
56
57
  exports.TRIAGE_SKILL = 'boop-skills-edit-triage';
57
58
  /** What to do about skills edited in SKILL.md, for a report that counts them */
@@ -87,6 +88,10 @@ function planCapabilityUpdate(targetDir, capabilityName, projectManifest) {
87
88
  if (!boopkit?.capability)
88
89
  return null;
89
90
  const newVersion = pkgJson.version || '0.0.0';
91
+ // The capability as this package declares it: the npm scripts merged below,
92
+ // and the skill migrations applied further down.
93
+ const capJsonPath = path.join(pkgDir, 'capability.json');
94
+ const capJson = fs.existsSync(capJsonPath) ? JSON.parse(fs.readFileSync(capJsonPath, 'utf-8')) : {};
90
95
  const meta = projectManifest.capabilityMeta?.[capabilityName];
91
96
  const storedHashes = meta?.fileHashes || {};
92
97
  const actions = [];
@@ -215,60 +220,59 @@ function planCapabilityUpdate(targetDir, capabilityName, projectManifest) {
215
220
  }
216
221
  }
217
222
  // Apply skill migration logic: upgrade deprecate+add pairs to migrate actions
218
- const capJsonPath = path.join(pkgDir, 'capability.json');
219
- if (fs.existsSync(capJsonPath)) {
220
- const capJson = JSON.parse(fs.readFileSync(capJsonPath, 'utf-8'));
221
- const migrations = capJson.skill_migrations;
222
- if (migrations) {
223
- for (const [newSkillName, oldFlatFilename] of Object.entries(migrations)) {
224
- const oldRelPath = path.join('skills', oldFlatFilename);
225
- const newPrefix = path.join('skills', newSkillName) + path.sep;
226
- const oldStoredHash = storedHashes[oldRelPath];
227
- if (!oldStoredHash)
228
- continue;
229
- const oldFilePath = path.join(targetDir, oldRelPath);
230
- const oldState = compareFileHash(oldFilePath, oldStoredHash);
231
- if (oldState === 'modified') {
232
- // User modified the old file — keep it, and don't install the folder that replaces it.
233
- // A folder file already installed has its own comparison, so only an install is held back.
234
- for (const action of actions) {
235
- if (action.action === 'add' && (action.relativePath.startsWith(newPrefix) || action.relativePath === path.join('skills', newSkillName))) {
236
- action.action = 'skip';
237
- action.reason = `Skipped: old skill file '${oldRelPath}' was locally modified`;
238
- action.oldPath = oldRelPath;
239
- action.instruction = `boop installs this once ${oldRelPath} is gone; see what to do about that file.`;
240
- }
241
- }
242
- // Change deprecate to skip for the old file
243
- const oldAction = actions.find(a => a.relativePath === oldRelPath);
244
- if (oldAction) {
245
- const projectFile = path.join('skills', newSkillName, 'PROJECT.md');
246
- oldAction.action = 'skip';
247
- oldAction.reason = 'Locally modified (migration skipped)';
248
- oldAction.replacedBy = newSkillName;
249
- oldAction.instruction = `This is the old single-file form of the ${newSkillName} skill, and this project edited it. ` +
250
- `Move the rules this project needs from it into ${projectFile}, following the ${exports.TRIAGE_SKILL} skill, then delete it and run \`boop update\`.`;
223
+ const migrations = capJson.skill_migrations;
224
+ if (migrations) {
225
+ for (const [newSkillName, oldFlatFilename] of Object.entries(migrations)) {
226
+ const oldRelPath = path.join('skills', oldFlatFilename);
227
+ const newPrefix = path.join('skills', newSkillName) + path.sep;
228
+ const oldStoredHash = storedHashes[oldRelPath];
229
+ if (!oldStoredHash)
230
+ continue;
231
+ const oldFilePath = path.join(targetDir, oldRelPath);
232
+ const oldState = compareFileHash(oldFilePath, oldStoredHash);
233
+ if (oldState === 'modified') {
234
+ // User modified the old file — keep it, and don't install the folder that replaces it.
235
+ // A folder file already installed has its own comparison, so only an install is held back.
236
+ for (const action of actions) {
237
+ if (action.action === 'add' && (action.relativePath.startsWith(newPrefix) || action.relativePath === path.join('skills', newSkillName))) {
238
+ action.action = 'skip';
239
+ action.reason = `Skipped: old skill file '${oldRelPath}' was locally modified`;
240
+ action.oldPath = oldRelPath;
241
+ action.instruction = `boop installs this once ${oldRelPath} is gone; see what to do about that file.`;
251
242
  }
252
243
  }
253
- else if (oldState === 'match') {
254
- // Old file unmodified — convert deprecate to migrate
255
- const oldAction = actions.find(a => a.relativePath === oldRelPath);
256
- if (oldAction) {
257
- oldAction.action = 'migrate';
258
- oldAction.reason = `Migrated to folder: skills/${newSkillName}/`;
259
- }
260
- // Tag new folder files with the old path they're replacing
261
- for (const action of actions) {
262
- if (action.relativePath.startsWith(newPrefix) || action.relativePath === path.join('skills', newSkillName)) {
263
- action.oldPath = oldRelPath;
264
- }
244
+ // Change deprecate to skip for the old file
245
+ const oldAction = actions.find(a => a.relativePath === oldRelPath);
246
+ if (oldAction) {
247
+ const projectFile = path.join('skills', newSkillName, 'PROJECT.md');
248
+ oldAction.action = 'skip';
249
+ oldAction.reason = 'Locally modified (migration skipped)';
250
+ oldAction.replacedBy = newSkillName;
251
+ oldAction.instruction = `This is the old single-file form of the ${newSkillName} skill, and this project edited it. ` +
252
+ `Move the rules this project needs from it into ${projectFile}, following the ${exports.TRIAGE_SKILL} skill, then delete it and run \`boop update\`.`;
253
+ }
254
+ }
255
+ else if (oldState === 'match') {
256
+ // Old file unmodified — convert deprecate to migrate
257
+ const oldAction = actions.find(a => a.relativePath === oldRelPath);
258
+ if (oldAction) {
259
+ oldAction.action = 'migrate';
260
+ oldAction.reason = `Migrated to folder: skills/${newSkillName}/`;
261
+ }
262
+ // Tag new folder files with the old path they're replacing
263
+ for (const action of actions) {
264
+ if (action.relativePath.startsWith(newPrefix) || action.relativePath === path.join('skills', newSkillName)) {
265
+ action.oldPath = oldRelPath;
265
266
  }
266
267
  }
267
- // oldState === 'missing': old file already gone, add new files normally
268
268
  }
269
+ // oldState === 'missing': old file already gone, add new files normally
269
270
  }
270
271
  }
271
- const upToDate = actions.length === 0;
272
+ // A capability that ships a new command ships it as a file and a script; an
273
+ // update that delivered only the file left nothing able to run it (boop-ldou).
274
+ const scriptActions = (0, script_merge_1.planScriptUpdates)(capJson.scripts, (0, script_merge_1.readProjectScripts)(targetDir));
275
+ const upToDate = actions.length === 0 && !scriptActions.some(a => a.action === 'add');
272
276
  return {
273
277
  capability: capabilityName,
274
278
  targetDir,
@@ -276,6 +280,7 @@ function planCapabilityUpdate(targetDir, capabilityName, projectManifest) {
276
280
  currentVersion: meta?.version,
277
281
  newVersion,
278
282
  actions,
283
+ scriptActions,
279
284
  unchangedCount,
280
285
  upToDate,
281
286
  };
@@ -295,6 +300,8 @@ function executeUpdatePlan(plan, options = {}) {
295
300
  skippedDeleted: [],
296
301
  pruned: [],
297
302
  migrated: [],
303
+ scriptsAdded: [],
304
+ scriptsKept: [],
298
305
  unchangedCount: plan.unchangedCount,
299
306
  instructions: [],
300
307
  };
@@ -411,6 +418,11 @@ function executeUpdatePlan(plan, options = {}) {
411
418
  }
412
419
  }
413
420
  }
421
+ // Merge the capability's npm scripts, so a command it ships as file + script
422
+ // arrives whole. Additive only: a name the project already uses is left alone.
423
+ const scriptActions = plan.scriptActions ?? [];
424
+ result.scriptsAdded = (0, script_merge_1.applyScriptUpdates)(plan.targetDir, scriptActions);
425
+ result.scriptsKept = scriptActions.filter(a => a.action === 'keep');
414
426
  // Update .boop/config.json
415
427
  if (manifest) {
416
428
  if (!manifest.capabilityMeta) {
@@ -531,6 +543,14 @@ function formatUpdateSummary(result) {
531
543
  if (result.added.length > 0) {
532
544
  parts.push(`added ${result.added.length} new file${result.added.length === 1 ? '' : 's'}`);
533
545
  }
546
+ const scriptsAdded = result.scriptsAdded?.length || 0;
547
+ if (scriptsAdded > 0) {
548
+ parts.push(`added ${scriptsAdded} script${scriptsAdded === 1 ? '' : 's'}`);
549
+ }
550
+ const scriptsKept = result.scriptsKept?.length || 0;
551
+ if (scriptsKept > 0) {
552
+ parts.push(`kept ${scriptsKept} customised script${scriptsKept === 1 ? '' : 's'}`);
553
+ }
534
554
  if (result.pruned && result.pruned.length > 0) {
535
555
  parts.push(`pruned ${result.pruned.length} deprecated file${result.pruned.length === 1 ? '' : 's'}`);
536
556
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boopkit/cli",
3
- "version": "0.6.14",
3
+ "version": "0.6.15",
4
4
  "description": "boop: the opinionated bootstrap for founder friendly, AI-first web apps - code plus agent skills, composable from day one.",
5
5
  "main": "dist/cli.js",
6
6
  "workspaces": [