@boopkit/cli 0.8.0 → 0.8.1

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.
Files changed (39) hide show
  1. package/README.md +8 -6
  2. package/dist/capability-catalog.json +10 -3
  3. package/dist/capability-versions.json +11 -10
  4. package/dist/cli.js +0 -0
  5. package/dist/commands/add.js +35 -7
  6. package/dist/commands/adopt.js +1 -1
  7. package/dist/commands/loop-merge.d.ts +11 -0
  8. package/dist/commands/loop-merge.js +135 -14
  9. package/dist/commands/loop.js +1 -1
  10. package/dist/commands/migrate-skill-edits.js +2 -2
  11. package/dist/commands/status.d.ts +10 -0
  12. package/dist/commands/status.js +27 -2
  13. package/dist/commands/update.js +157 -21
  14. package/dist/lib/capability.js +4 -1
  15. package/dist/lib/execute.js +19 -2
  16. package/dist/lib/install-layout.d.ts +5 -0
  17. package/dist/lib/install-layout.js +8 -0
  18. package/dist/lib/manifest.d.ts +1 -1
  19. package/dist/lib/moves.d.ts +88 -0
  20. package/dist/lib/moves.js +381 -0
  21. package/dist/lib/original-text.d.ts +2 -0
  22. package/dist/lib/original-text.js +15 -7
  23. package/dist/lib/preflight.d.ts +14 -1
  24. package/dist/lib/preflight.js +53 -17
  25. package/dist/lib/project.d.ts +17 -0
  26. package/dist/lib/project.js +26 -0
  27. package/dist/lib/scaffold.d.ts +12 -1
  28. package/dist/lib/scaffold.js +43 -4
  29. package/dist/lib/update.d.ts +36 -2
  30. package/dist/lib/update.js +82 -4
  31. package/docs/init.md +22 -0
  32. package/docs/loop.md +11 -3
  33. package/docs/skills.md +17 -13
  34. package/package.json +1 -1
  35. package/profiles/base/profile.md +13 -10
  36. package/profiles/development/profile.md +10 -8
  37. package/profiles/web-app/profile.md +6 -4
  38. package/profiles/web-app/reference/message-queue-patterns.ref.md +8 -13
  39. package/src/templates/kernel/tests/app.test.ts.hbs +17 -0
@@ -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
- const manifest = (0, project_1.readProjectManifest)(targetDir);
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
- // Plan all updates
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 plan = (0, update_1.planCapabilityUpdate)(targetDir, cap, manifest);
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[90m⚠\x1b[0m';
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
- capability: plan.capability,
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
- // and put back any PROJECT.md the project has deleted
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
- results.forEach((result, i) => {
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 actionFor = (f) => plans[i].actions.find(a => a.relativePath === f);
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') {
@@ -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
@@ -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
- (0, scaffold_1.copyCapabilityFiles)(targetDir, capabilityName);
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;
@@ -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))
@@ -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;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The move pass of `boop update`: files a capability stopped shipping because
3
+ * another capability ships them now (specs/skill-moves.spec.md).
4
+ *
5
+ * The capability the files leave declares where they went, in `file_moves`, and
6
+ * names any capability split out of it in `split_into`. The pass reads those
7
+ * declarations from every installed capability, settles each moved path the
8
+ * project holds a stored hash for, and runs before any capability is planned:
9
+ * a move is a rename on disk plus a transfer of stored hashes, and everything
10
+ * after that is the receiving capability's ordinary update.
11
+ */
12
+ import { BoopManifest } from './project';
13
+ import { FileInstruction, FileUpdateAction } from './update';
14
+ export interface FileMoveDeclaration {
15
+ capability: string;
16
+ /** Where the files land; the key when absent */
17
+ path?: string;
18
+ }
19
+ export type MoveState = 'will-move' | 'waiting-for-capability' | 'waiting-for-package' | 'kept-for-project-content' | 'split-will-add';
20
+ /**
21
+ * What the pass decided for one declared key the project holds paths under.
22
+ *
23
+ * - `move`: rename on disk and transfer the hashes (cases 1 to 4 and 10).
24
+ * - `remove`: delete the old skill folder, which holds nothing of the project's (cases 5 and 7).
25
+ * - `merge`: the receiving capability already installed the new folder; delete the old one,
26
+ * carrying its `PROJECT.md` across when that holds rules (case 9).
27
+ * - `hold`: change nothing, and say what would let the move happen (cases 6, 8, 9, and a split that cannot be added).
28
+ * - `deprecate`: a file outside a skill folder, whose receiving capability the project does not install.
29
+ */
30
+ export interface PlannedMove {
31
+ fromCapability: string;
32
+ /** The declared key; a folder ends in `/` */
33
+ from: string;
34
+ toCapability: string;
35
+ to: string;
36
+ isSkillFolder: boolean;
37
+ outcome: 'move' | 'remove' | 'merge' | 'hold' | 'deprecate';
38
+ state: MoveState;
39
+ /** True when the old folder's PROJECT.md holds more than the template */
40
+ projectFileCarried?: boolean;
41
+ /** For a hold: what would let the move happen */
42
+ instruction?: string;
43
+ /** The stored paths under the key, and their hashes */
44
+ storedHashes: Record<string, string>;
45
+ }
46
+ export interface MovePassPlan {
47
+ moves: PlannedMove[];
48
+ /** Capabilities split out of an installed one, which the update adds before moving files into them */
49
+ splitsToAdd: {
50
+ capability: string;
51
+ splitFrom: string;
52
+ }[];
53
+ }
54
+ export interface MoveResult {
55
+ /** The capability the files left */
56
+ capability: string;
57
+ moved: {
58
+ from: string;
59
+ to: string;
60
+ toCapability: string;
61
+ }[];
62
+ removed: string[];
63
+ instructions: FileInstruction[];
64
+ }
65
+ /**
66
+ * Settle every declared move the project holds paths for. Writes nothing.
67
+ *
68
+ * `updating` is the capabilities this update was asked for: a move runs when
69
+ * either end of it is among them. `unaddable` names split capabilities the
70
+ * update tried and failed to add, with the reason, so their moves wait.
71
+ */
72
+ export declare function planMoves(targetDir: string, manifest: BoopManifest, updating: string[], options?: {
73
+ unaddable?: Record<string, string>;
74
+ }): MovePassPlan;
75
+ /**
76
+ * The actions a planned move adds to the leaving capability's plan: one per
77
+ * stored path for a move, removal or deprecation, and one for the whole key
78
+ * for a hold, which changes nothing and carries the instruction.
79
+ */
80
+ export declare function moveActions(move: PlannedMove): FileUpdateAction[];
81
+ /**
82
+ * Carry out every planned `move`, `remove` and `merge`, and write the manifest
83
+ * once afterwards. A hold or deprecation is left to the leaving capability's
84
+ * plan. Returns what happened, by the capability the files left.
85
+ */
86
+ export declare function executeMoves(targetDir: string, moves: PlannedMove[]): Map<string, MoveResult>;
87
+ /** The paths that arrive in each capability by a planned move, for the add and plan that must leave them out */
88
+ export declare function arrivingPaths(moves: PlannedMove[], capability: string): string[];