@boopkit/cli 0.6.13 → 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.
- package/README.md +1 -1
- package/dist/capability-versions.json +16 -16
- package/dist/cli.js +7 -0
- package/dist/commands/init.js +2 -1
- package/dist/commands/migrate-skill-edits.d.ts +24 -0
- package/dist/commands/migrate-skill-edits.js +190 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.js +13 -0
- package/dist/commands/update.js +84 -7
- package/dist/lib/apply-skill-edits.d.ts +76 -0
- package/dist/lib/apply-skill-edits.js +307 -0
- package/dist/lib/execute.js +4 -12
- package/dist/lib/original-text.d.ts +43 -0
- package/dist/lib/original-text.js +165 -0
- package/dist/lib/scaffold.d.ts +53 -0
- package/dist/lib/scaffold.js +134 -1
- package/dist/lib/script-merge.d.ts +55 -0
- package/dist/lib/script-merge.js +144 -0
- package/dist/lib/skill-edits.d.ts +88 -0
- package/dist/lib/skill-edits.js +293 -0
- package/dist/lib/stale-scripts.d.ts +3 -2
- package/dist/lib/stale-scripts.js +3 -2
- package/dist/lib/update.d.ts +45 -2
- package/dist/lib/update.js +207 -44
- package/docs/skills.md +1 -0
- package/package.json +1 -1
- package/src/templates/skill-project/PROJECT.md +28 -0
- package/dist/commands/loop.js.orig-pre-maxbuffer +0 -1455
- package/dist/commands/status.test.d.ts +0 -1
- package/dist/commands/status.test.js +0 -118
- package/dist/lib/project.test.d.ts +0 -1
- package/dist/lib/project.test.js +0 -118
package/dist/lib/scaffold.js
CHANGED
|
@@ -41,8 +41,13 @@ exports.getTemplatesDir = getTemplatesDir;
|
|
|
41
41
|
exports.profileHasTemplates = profileHasTemplates;
|
|
42
42
|
exports.scaffoldKernel = scaffoldKernel;
|
|
43
43
|
exports.hashFile = hashFile;
|
|
44
|
+
exports.hashContent = hashContent;
|
|
44
45
|
exports.copyCapabilityFiles = copyCapabilityFiles;
|
|
45
46
|
exports.linkSkillsToClaude = linkSkillsToClaude;
|
|
47
|
+
exports.installedSkillNames = installedSkillNames;
|
|
48
|
+
exports.createSkillProjectFiles = createSkillProjectFiles;
|
|
49
|
+
exports.hasOfferedRules = hasOfferedRules;
|
|
50
|
+
exports.readSkillProjectTemplate = readSkillProjectTemplate;
|
|
46
51
|
exports.getBoopkitVersion = getBoopkitVersion;
|
|
47
52
|
exports.formatUnpinnedDependencyWarning = formatUnpinnedDependencyWarning;
|
|
48
53
|
exports.mergePackageJson = mergePackageJson;
|
|
@@ -56,6 +61,7 @@ const config_1 = require("./config");
|
|
|
56
61
|
const capability_1 = require("./capability");
|
|
57
62
|
const safe_io_1 = require("./safe-io");
|
|
58
63
|
const install_layout_1 = require("./install-layout");
|
|
64
|
+
const project_1 = require("./project");
|
|
59
65
|
/**
|
|
60
66
|
* Render a single Handlebars template string with variables.
|
|
61
67
|
*
|
|
@@ -135,7 +141,13 @@ function scaffoldKernel(targetDir, variables, overwrite = false, templatesDir =
|
|
|
135
141
|
* Compute SHA-256 hash of a file's content.
|
|
136
142
|
*/
|
|
137
143
|
function hashFile(filePath) {
|
|
138
|
-
|
|
144
|
+
return hashContent(fs.readFileSync(filePath));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The hash `hashFile` records, for content that is not on disk — a past version
|
|
148
|
+
* read from git, or a file inside a downloaded package.
|
|
149
|
+
*/
|
|
150
|
+
function hashContent(content) {
|
|
139
151
|
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
140
152
|
return `sha256:${hash}`;
|
|
141
153
|
}
|
|
@@ -243,6 +255,127 @@ function linkSkillsToClaude(targetDir, only) {
|
|
|
243
255
|
}
|
|
244
256
|
return linked;
|
|
245
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* The names of the skill folders under `<targetDir>/skills/` that boop installed.
|
|
260
|
+
*
|
|
261
|
+
* A project's `skills/` is not all boop's: a project writes skills of its own
|
|
262
|
+
* beside the ones its capabilities ship, and those folders are already entirely
|
|
263
|
+
* the project's file. Two records say which are boop's, and the union is taken
|
|
264
|
+
* because neither is complete on its own:
|
|
265
|
+
*
|
|
266
|
+
* - the `fileHashes` a capability stored in `.boop/config.json`, which is the
|
|
267
|
+
* record of what was actually copied into this project;
|
|
268
|
+
* - the skill placements of an installed capability that has no stored hashes —
|
|
269
|
+
* a manifest written before `capabilityMeta` existed, or one whose hash
|
|
270
|
+
* storage was skipped.
|
|
271
|
+
*
|
|
272
|
+
* Returns null when the directory holds no `.boop/config.json`, i.e. nothing
|
|
273
|
+
* here is a boop project and no folder can be attributed either way.
|
|
274
|
+
*/
|
|
275
|
+
function installedSkillNames(targetDir, searchPaths) {
|
|
276
|
+
const manifest = (0, project_1.readProjectManifest)(targetDir);
|
|
277
|
+
if (!manifest)
|
|
278
|
+
return null;
|
|
279
|
+
const names = new Set();
|
|
280
|
+
const addDest = (dest) => {
|
|
281
|
+
// Stored destinations are POSIX, but tolerate a manifest written on Windows.
|
|
282
|
+
const [top, skillName] = dest.split(/[\\/]/);
|
|
283
|
+
if (top === 'skills' && skillName)
|
|
284
|
+
names.add(skillName);
|
|
285
|
+
};
|
|
286
|
+
const meta = manifest.capabilityMeta ?? {};
|
|
287
|
+
for (const entry of Object.values(meta)) {
|
|
288
|
+
for (const dest of Object.keys(entry?.fileHashes ?? {}))
|
|
289
|
+
addDest(dest);
|
|
290
|
+
}
|
|
291
|
+
// Walking a package's files is the expensive half, so it runs only for a
|
|
292
|
+
// capability whose hashes are missing — where it is the only record left.
|
|
293
|
+
for (const capability of manifest.capabilities ?? []) {
|
|
294
|
+
if (Object.keys(meta[capability]?.fileHashes ?? {}).length > 0)
|
|
295
|
+
continue;
|
|
296
|
+
for (const placement of (0, install_layout_1.capabilityPlacements)(capability, searchPaths ?? [targetDir])) {
|
|
297
|
+
if (placement.kind === 'skill')
|
|
298
|
+
addDest(placement.dest);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return names;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Give every boop-installed skill folder under `<targetDir>/skills/` that lacks
|
|
305
|
+
* one a `PROJECT.md`, where the project records how it applies the skill. Each
|
|
306
|
+
* boop `SKILL.md` opens by pointing at it; the file being present is the
|
|
307
|
+
* backstop for an agent that skipped the pointer.
|
|
308
|
+
*
|
|
309
|
+
* Only a folder boop installed gets one. A skill the project wrote itself is
|
|
310
|
+
* already the project's file outright, so a `PROJECT.md` beside it separates
|
|
311
|
+
* nothing — and because this runs on every init, add and update, a project
|
|
312
|
+
* could never delete the stub it left there.
|
|
313
|
+
*
|
|
314
|
+
* For the same reason it takes back a stub an earlier boop wrote beside a
|
|
315
|
+
* project-owned skill, but only while the file's bytes are still exactly the
|
|
316
|
+
* template's: anything the project typed into a `PROJECT.md` keeps it.
|
|
317
|
+
*
|
|
318
|
+
* The file is the project's: boop never hashes it, so `boop update` can never
|
|
319
|
+
* report it, and an existing one is never opened for writing.
|
|
320
|
+
*
|
|
321
|
+
* Returns the relative paths of the files created and of the stubs removed.
|
|
322
|
+
*/
|
|
323
|
+
function createSkillProjectFiles(targetDir, searchPaths) {
|
|
324
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
325
|
+
if (!fs.existsSync(skillsDir))
|
|
326
|
+
return { created: [], removed: [] };
|
|
327
|
+
const installed = installedSkillNames(targetDir, searchPaths);
|
|
328
|
+
if (installed === null)
|
|
329
|
+
return { created: [], removed: [] };
|
|
330
|
+
const template = readSkillProjectTemplate();
|
|
331
|
+
const created = [];
|
|
332
|
+
const removed = [];
|
|
333
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
334
|
+
// A symlinked skill folder is someone else's. In the boop repository each one
|
|
335
|
+
// points into a capability package, from which a PROJECT.md would ship to every project.
|
|
336
|
+
if (!entry.isDirectory())
|
|
337
|
+
continue;
|
|
338
|
+
const filePath = path.join(skillsDir, entry.name, 'PROJECT.md');
|
|
339
|
+
const relPath = path.join('skills', entry.name, 'PROJECT.md');
|
|
340
|
+
const existing = fs.lstatSync(filePath, { throwIfNoEntry: false });
|
|
341
|
+
if (!installed.has(entry.name)) {
|
|
342
|
+
if (existing?.isFile() && fs.readFileSync(filePath, 'utf-8') === template) {
|
|
343
|
+
fs.unlinkSync(filePath);
|
|
344
|
+
removed.push(relPath);
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (existing)
|
|
349
|
+
continue;
|
|
350
|
+
fs.writeFileSync(filePath, template, { flag: 'wx' });
|
|
351
|
+
created.push(relPath);
|
|
352
|
+
}
|
|
353
|
+
return { created, removed };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Whether a skill's PROJECT.md has rules under "Offered to boop" — anything in
|
|
357
|
+
* that section beyond the template's own explanation of it.
|
|
358
|
+
*/
|
|
359
|
+
function hasOfferedRules(projectFilePath) {
|
|
360
|
+
if (!fs.existsSync(projectFilePath))
|
|
361
|
+
return false;
|
|
362
|
+
const section = offeredToBoopSection(fs.readFileSync(projectFilePath, 'utf-8'));
|
|
363
|
+
if (section === null)
|
|
364
|
+
return false;
|
|
365
|
+
const explanation = new Set((offeredToBoopSection(readSkillProjectTemplate()) ?? '').split('\n').map(line => line.trim()));
|
|
366
|
+
return section.split('\n').some(line => line.trim() !== '' && !explanation.has(line.trim()));
|
|
367
|
+
}
|
|
368
|
+
function offeredToBoopSection(text) {
|
|
369
|
+
const heading = /^##[ \t]+Offered to boop[ \t]*$/m.exec(text);
|
|
370
|
+
if (!heading)
|
|
371
|
+
return null;
|
|
372
|
+
const rest = text.slice(heading.index + heading[0].length);
|
|
373
|
+
const nextHeading = rest.search(/^#{1,2}[ \t]/m);
|
|
374
|
+
return nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
|
375
|
+
}
|
|
376
|
+
function readSkillProjectTemplate() {
|
|
377
|
+
return fs.readFileSync(path.join((0, config_1.getBoopRoot)(), 'src', 'templates', 'skill-project', 'PROJECT.md'), 'utf-8');
|
|
378
|
+
}
|
|
246
379
|
/**
|
|
247
380
|
* Copy capability files from the capabilities/ source tree (test fixtures only).
|
|
248
381
|
*/
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge the npm scripts a capability declares into a project's package.json
|
|
3
|
+
* during `boop update`.
|
|
4
|
+
*
|
|
5
|
+
* The case this exists for (boop-ldou): a capability ships a new command as two
|
|
6
|
+
* things — a file under `scaffold/`, and a `scripts` entry in `capability.json`.
|
|
7
|
+
* `boop add` merges those scripts at install time (see `merge-scripts` in
|
|
8
|
+
* lib/execute.ts), but `boop update` only ever copied files, so an upgraded
|
|
9
|
+
* project received `src/scripts/migrate-credentials.ts` and no way to run it by
|
|
10
|
+
* name. Every document, post-install note and status hint that said
|
|
11
|
+
* `npm run credentials:migrate` was right for a freshly scaffolded project and
|
|
12
|
+
* wrong for every upgraded one — precisely the projects a migration tool is for.
|
|
13
|
+
*
|
|
14
|
+
* The merge is additive, like `boop add`'s: a script is written only when the
|
|
15
|
+
* project has no script of that name. A name the project already uses is left
|
|
16
|
+
* exactly as it stands, because a command somebody customised is theirs, and
|
|
17
|
+
* clobbering it would be worse than leaving it alone — the same restraint
|
|
18
|
+
* `rewriteStaleRunnerScripts` shows when a command has changed underneath it.
|
|
19
|
+
* What boop does do is say so, so a kept command that no longer matches what the
|
|
20
|
+
* capability ships is visible rather than silent.
|
|
21
|
+
*/
|
|
22
|
+
import { CapabilityScript } from './config';
|
|
23
|
+
export interface ScriptUpdateAction {
|
|
24
|
+
/** The script's key in package.json, e.g. `credentials:migrate`. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** The command the capability declares. */
|
|
27
|
+
command: string;
|
|
28
|
+
/** `add` writes it; `keep` leaves the project's own command in place. */
|
|
29
|
+
action: 'add' | 'keep';
|
|
30
|
+
/** Why this action was chosen. */
|
|
31
|
+
reason: string;
|
|
32
|
+
/** The command the project has today, when there is one. */
|
|
33
|
+
currentCommand?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pure: given what a capability declares and what the project's package.json
|
|
37
|
+
* holds, decide what happens to each script.
|
|
38
|
+
*
|
|
39
|
+
* A script whose command already matches what the capability ships is not
|
|
40
|
+
* returned at all — there is nothing to do and nothing worth reporting.
|
|
41
|
+
*/
|
|
42
|
+
export declare function planScriptUpdates(declared: Record<string, CapabilityScript> | undefined, projectScripts: Record<string, string> | undefined): ScriptUpdateAction[];
|
|
43
|
+
/**
|
|
44
|
+
* Write the `add` actions into the project's package.json, and return the ones
|
|
45
|
+
* actually written.
|
|
46
|
+
*
|
|
47
|
+
* Re-reads the file rather than trusting the plan: an action is applied only
|
|
48
|
+
* while the name is still free, so a script written between planning and here
|
|
49
|
+
* survives. A missing or unreadable package.json is not an error — scaffold
|
|
50
|
+
* files have already been delivered by the time this runs, and failing the whole
|
|
51
|
+
* update over the script merge would be worse than reporting nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applyScriptUpdates(targetDir: string, actions: ScriptUpdateAction[]): ScriptUpdateAction[];
|
|
54
|
+
/** The project's package.json scripts, or undefined when there is no readable one. */
|
|
55
|
+
export declare function readProjectScripts(targetDir: string): Record<string, string> | undefined;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Merge the npm scripts a capability declares into a project's package.json
|
|
4
|
+
* during `boop update`.
|
|
5
|
+
*
|
|
6
|
+
* The case this exists for (boop-ldou): a capability ships a new command as two
|
|
7
|
+
* things — a file under `scaffold/`, and a `scripts` entry in `capability.json`.
|
|
8
|
+
* `boop add` merges those scripts at install time (see `merge-scripts` in
|
|
9
|
+
* lib/execute.ts), but `boop update` only ever copied files, so an upgraded
|
|
10
|
+
* project received `src/scripts/migrate-credentials.ts` and no way to run it by
|
|
11
|
+
* name. Every document, post-install note and status hint that said
|
|
12
|
+
* `npm run credentials:migrate` was right for a freshly scaffolded project and
|
|
13
|
+
* wrong for every upgraded one — precisely the projects a migration tool is for.
|
|
14
|
+
*
|
|
15
|
+
* The merge is additive, like `boop add`'s: a script is written only when the
|
|
16
|
+
* project has no script of that name. A name the project already uses is left
|
|
17
|
+
* exactly as it stands, because a command somebody customised is theirs, and
|
|
18
|
+
* clobbering it would be worse than leaving it alone — the same restraint
|
|
19
|
+
* `rewriteStaleRunnerScripts` shows when a command has changed underneath it.
|
|
20
|
+
* What boop does do is say so, so a kept command that no longer matches what the
|
|
21
|
+
* capability ships is visible rather than silent.
|
|
22
|
+
*/
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.planScriptUpdates = planScriptUpdates;
|
|
58
|
+
exports.applyScriptUpdates = applyScriptUpdates;
|
|
59
|
+
exports.readProjectScripts = readProjectScripts;
|
|
60
|
+
const fs = __importStar(require("fs"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
/**
|
|
63
|
+
* Pure: given what a capability declares and what the project's package.json
|
|
64
|
+
* holds, decide what happens to each script.
|
|
65
|
+
*
|
|
66
|
+
* A script whose command already matches what the capability ships is not
|
|
67
|
+
* returned at all — there is nothing to do and nothing worth reporting.
|
|
68
|
+
*/
|
|
69
|
+
function planScriptUpdates(declared, projectScripts) {
|
|
70
|
+
if (!declared)
|
|
71
|
+
return [];
|
|
72
|
+
const actions = [];
|
|
73
|
+
for (const [name, script] of Object.entries(declared)) {
|
|
74
|
+
const command = typeof script === 'string' ? script : script?.cmd;
|
|
75
|
+
if (!command)
|
|
76
|
+
continue;
|
|
77
|
+
const currentCommand = projectScripts?.[name];
|
|
78
|
+
if (currentCommand === undefined) {
|
|
79
|
+
actions.push({ name, command, action: 'add', reason: 'New script from capability' });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (currentCommand === command)
|
|
83
|
+
continue;
|
|
84
|
+
actions.push({
|
|
85
|
+
name,
|
|
86
|
+
command,
|
|
87
|
+
action: 'keep',
|
|
88
|
+
reason: 'Project has its own command for this script',
|
|
89
|
+
currentCommand,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return actions;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Write the `add` actions into the project's package.json, and return the ones
|
|
96
|
+
* actually written.
|
|
97
|
+
*
|
|
98
|
+
* Re-reads the file rather than trusting the plan: an action is applied only
|
|
99
|
+
* while the name is still free, so a script written between planning and here
|
|
100
|
+
* survives. A missing or unreadable package.json is not an error — scaffold
|
|
101
|
+
* files have already been delivered by the time this runs, and failing the whole
|
|
102
|
+
* update over the script merge would be worse than reporting nothing.
|
|
103
|
+
*/
|
|
104
|
+
function applyScriptUpdates(targetDir, actions) {
|
|
105
|
+
const additions = actions.filter(a => a.action === 'add');
|
|
106
|
+
if (additions.length === 0)
|
|
107
|
+
return [];
|
|
108
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
109
|
+
if (!fs.existsSync(pkgPath))
|
|
110
|
+
return [];
|
|
111
|
+
let pkg;
|
|
112
|
+
try {
|
|
113
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
if (!pkg.scripts)
|
|
119
|
+
pkg.scripts = {};
|
|
120
|
+
const applied = [];
|
|
121
|
+
for (const action of additions) {
|
|
122
|
+
if (action.name in pkg.scripts)
|
|
123
|
+
continue;
|
|
124
|
+
pkg.scripts[action.name] = action.command;
|
|
125
|
+
applied.push(action);
|
|
126
|
+
}
|
|
127
|
+
if (applied.length > 0) {
|
|
128
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
129
|
+
}
|
|
130
|
+
return applied;
|
|
131
|
+
}
|
|
132
|
+
/** The project's package.json scripts, or undefined when there is no readable one. */
|
|
133
|
+
function readProjectScripts(targetDir) {
|
|
134
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
135
|
+
if (!fs.existsSync(pkgPath))
|
|
136
|
+
return undefined;
|
|
137
|
+
try {
|
|
138
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
139
|
+
return pkg.scripts;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mechanical half of splitting a drifted skill: find each skill whose
|
|
3
|
+
* SKILL.md the project edited, recover the text boop installed, list every
|
|
4
|
+
* edit, and write one plan per skill for an agent to decide. Nothing here
|
|
5
|
+
* decides where an edit belongs.
|
|
6
|
+
*/
|
|
7
|
+
import { FindOriginalTextOptions } from './original-text';
|
|
8
|
+
import { BoopManifest } from './project';
|
|
9
|
+
/** Where plans are written, relative to the project. Everything in `.boop/` but config.json is gitignored. */
|
|
10
|
+
export declare const SKILL_EDITS_DIR: string;
|
|
11
|
+
/** One continuous change between a base text and the project's copy, as line ranges. */
|
|
12
|
+
export interface Edit {
|
|
13
|
+
/** Lines [baseStart, baseEnd) of the base text that the project replaced */
|
|
14
|
+
baseStart: number;
|
|
15
|
+
baseEnd: number;
|
|
16
|
+
/** Lines [projectStart, projectEnd) of the project's copy that replace them */
|
|
17
|
+
projectStart: number;
|
|
18
|
+
projectEnd: number;
|
|
19
|
+
}
|
|
20
|
+
/** A blank decision, for the agent following the triage skill to fill in. */
|
|
21
|
+
export interface EditDecision {
|
|
22
|
+
/** keep | offer | drop */
|
|
23
|
+
decision: null;
|
|
24
|
+
/** For a kept edit: adds | narrows | overrides */
|
|
25
|
+
kind: null;
|
|
26
|
+
/** For a kept or offered edit: the edit rewritten as a rule that stands alone */
|
|
27
|
+
rule: null;
|
|
28
|
+
/** For a dropped edit: why */
|
|
29
|
+
reason: null;
|
|
30
|
+
}
|
|
31
|
+
export interface SkillEditPlanData {
|
|
32
|
+
skill: string;
|
|
33
|
+
capability: string;
|
|
34
|
+
path: string;
|
|
35
|
+
original: {
|
|
36
|
+
source: 'none';
|
|
37
|
+
} | ({
|
|
38
|
+
source: 'git';
|
|
39
|
+
commit: string;
|
|
40
|
+
}) | ({
|
|
41
|
+
source: 'npm';
|
|
42
|
+
package: string;
|
|
43
|
+
version: string;
|
|
44
|
+
});
|
|
45
|
+
/** True when the original could not be found, so every difference from boop's copy needs a decision */
|
|
46
|
+
comparedWithoutOriginal: boolean;
|
|
47
|
+
/** Whether boop has changed the skill since the project installed it; unknown without the original */
|
|
48
|
+
boopChanged: boolean | null;
|
|
49
|
+
/** As they were when the plan was written, so applying it can refuse a plan made for other text */
|
|
50
|
+
hashes: {
|
|
51
|
+
installed: string;
|
|
52
|
+
project: string;
|
|
53
|
+
boop: string;
|
|
54
|
+
};
|
|
55
|
+
decisions: Record<string, EditDecision>;
|
|
56
|
+
}
|
|
57
|
+
export type SkillEditOutcome = {
|
|
58
|
+
kind: 'written';
|
|
59
|
+
skill: string;
|
|
60
|
+
planPath: string;
|
|
61
|
+
edits: number;
|
|
62
|
+
boopChanged: boolean | null;
|
|
63
|
+
comparedWithoutOriginal: boolean;
|
|
64
|
+
replacedStale: boolean;
|
|
65
|
+
} | {
|
|
66
|
+
kind: 'kept';
|
|
67
|
+
skill: string;
|
|
68
|
+
planPath: string;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'hand-split';
|
|
71
|
+
skill: string;
|
|
72
|
+
relativePath: string;
|
|
73
|
+
upstreamPath?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Find every skill edited in SKILL.md across the project's capabilities and
|
|
77
|
+
* write a plan for each to `.boop/skill-edits/<skill-name>.md`.
|
|
78
|
+
*
|
|
79
|
+
* A plan already written for the current project copy and boop copy is left
|
|
80
|
+
* alone, because it may hold decisions. One written for other text is backed
|
|
81
|
+
* up and rewritten. A skill boop has no stored hash for gets no plan.
|
|
82
|
+
*/
|
|
83
|
+
export declare function writeSkillEditPlans(projectDir: string, manifest: BoopManifest, options?: FindOriginalTextOptions): SkillEditOutcome[];
|
|
84
|
+
/**
|
|
85
|
+
* The edits that turn `base` into `project`: each maximal run of lines that
|
|
86
|
+
* are not part of a longest common subsequence of the two.
|
|
87
|
+
*/
|
|
88
|
+
export declare function diffLines(base: string[], project: string[]): Edit[];
|