@boopkit/cli 0.6.12 → 0.6.14
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 +27 -6
- package/dist/capability-versions.json +16 -16
- package/dist/cli.js +9 -1
- package/dist/commands/add.js +49 -2
- package/dist/commands/init.d.ts +17 -1
- package/dist/commands/init.js +151 -26
- package/dist/commands/loop-merge.d.ts +1 -0
- package/dist/commands/loop-merge.js +4 -2
- package/dist/commands/loop.d.ts +21 -2
- package/dist/commands/loop.js +105 -16
- package/dist/commands/loop.js.orig-pre-maxbuffer +1455 -0
- 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 +55 -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 +1 -0
- package/dist/lib/original-text.d.ts +43 -0
- package/dist/lib/original-text.js +165 -0
- package/dist/lib/scaffold.d.ts +41 -7
- package/dist/lib/scaffold.js +96 -15
- package/dist/lib/skill-edits.d.ts +88 -0
- package/dist/lib/skill-edits.js +293 -0
- package/dist/lib/update.d.ts +38 -2
- package/dist/lib/update.js +149 -6
- package/docs/init.md +132 -86
- package/docs/loop.md +16 -0
- package/docs/skills.md +1 -0
- package/package.json +3 -2
- package/profiles/base/profile.md +105 -0
- package/profiles/base/templates/.github/copilot-instructions.md +3 -0
- package/profiles/base/templates/.gitignore.hbs +6 -0
- package/profiles/base/templates/AGENTS.md.hbs +50 -0
- package/profiles/base/templates/CLAUDE.md.hbs +3 -0
- package/profiles/base/templates/README.md.hbs +15 -0
- package/profiles/base/templates/docs/README.md +12 -0
- package/profiles/base/templates/package.json.hbs +7 -0
- package/profiles/base/templates/reference/README.md +16 -0
- package/profiles/base/templates/specs/README.md +12 -0
- package/profiles/development/profile.md +8 -1
- package/src/templates/skill-project/PROJECT.md +28 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ApplyDeps, ApplyResult } from '../lib/apply-skill-edits';
|
|
2
|
+
import { FindOriginalTextOptions } from '../lib/original-text';
|
|
3
|
+
import { SkillEditOutcome } from '../lib/skill-edits';
|
|
4
|
+
/** Replaceable in tests, so no test reaches the npm registry. */
|
|
5
|
+
export type MigrateSkillEditsDeps = FindOriginalTextOptions;
|
|
6
|
+
/**
|
|
7
|
+
* Write a decision plan for every skill edited in SKILL.md. Changes no skill,
|
|
8
|
+
* no PROJECT.md and no manifest: only files under `.boop/skill-edits/`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runMigrateSkillEdits(deps?: MigrateSkillEditsDeps): void;
|
|
11
|
+
/**
|
|
12
|
+
* `--run`: apply every decided plan that passes its checks, and list the rest
|
|
13
|
+
* with the reason each was refused. A drop is applied only once a person at a
|
|
14
|
+
* terminal agrees to it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runApplySkillEdits(deps?: ApplyDeps): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Put a yes-or-no question to a person. Anything but y or yes is no, and so is
|
|
19
|
+
* ending input instead of answering (Ctrl+D), which would otherwise reject at a
|
|
20
|
+
* terminal and never settle on a stream.
|
|
21
|
+
*/
|
|
22
|
+
export declare function askYesNo(question: string, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<boolean>;
|
|
23
|
+
export declare function formatApplyResult(result: ApplyResult): string;
|
|
24
|
+
export declare function formatOutcomes(outcomes: SkillEditOutcome[]): string;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runMigrateSkillEdits = runMigrateSkillEdits;
|
|
37
|
+
exports.runApplySkillEdits = runApplySkillEdits;
|
|
38
|
+
exports.askYesNo = askYesNo;
|
|
39
|
+
exports.formatApplyResult = formatApplyResult;
|
|
40
|
+
exports.formatOutcomes = formatOutcomes;
|
|
41
|
+
const readline = __importStar(require("readline/promises"));
|
|
42
|
+
const apply_skill_edits_1 = require("../lib/apply-skill-edits");
|
|
43
|
+
const project_1 = require("../lib/project");
|
|
44
|
+
const skill_edits_1 = require("../lib/skill-edits");
|
|
45
|
+
const update_1 = require("../lib/update");
|
|
46
|
+
/**
|
|
47
|
+
* Write a decision plan for every skill edited in SKILL.md. Changes no skill,
|
|
48
|
+
* no PROJECT.md and no manifest: only files under `.boop/skill-edits/`.
|
|
49
|
+
*/
|
|
50
|
+
function runMigrateSkillEdits(deps = {}) {
|
|
51
|
+
const projectDir = process.cwd();
|
|
52
|
+
const manifest = (0, project_1.readProjectManifest)(projectDir);
|
|
53
|
+
if (!manifest) {
|
|
54
|
+
console.error('\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run `boop init` first.');
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const outcomes = (0, skill_edits_1.writeSkillEditPlans)(projectDir, manifest, deps);
|
|
59
|
+
console.log(formatOutcomes(outcomes));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `--run`: apply every decided plan that passes its checks, and list the rest
|
|
63
|
+
* with the reason each was refused. A drop is applied only once a person at a
|
|
64
|
+
* terminal agrees to it.
|
|
65
|
+
*/
|
|
66
|
+
async function runApplySkillEdits(deps = terminalDeps()) {
|
|
67
|
+
const projectDir = process.cwd();
|
|
68
|
+
const manifest = (0, project_1.readProjectManifest)(projectDir);
|
|
69
|
+
if (!manifest) {
|
|
70
|
+
console.error('\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run `boop init` first.');
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const result = await (0, apply_skill_edits_1.applySkillEditPlans)(projectDir, manifest, deps);
|
|
75
|
+
console.log(formatApplyResult(result));
|
|
76
|
+
if (result.outcomes.some(o => o.kind !== 'applied'))
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
function terminalDeps() {
|
|
80
|
+
return {
|
|
81
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
82
|
+
confirm: question => askYesNo(question),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Put a yes-or-no question to a person. Anything but y or yes is no, and so is
|
|
87
|
+
* ending input instead of answering (Ctrl+D), which would otherwise reject at a
|
|
88
|
+
* terminal and never settle on a stream.
|
|
89
|
+
*/
|
|
90
|
+
async function askYesNo(question, input = process.stdin, output = process.stdout) {
|
|
91
|
+
const prompt = readline.createInterface({ input, output });
|
|
92
|
+
const closed = new Promise(resolve => prompt.once('close', () => resolve('')));
|
|
93
|
+
try {
|
|
94
|
+
const answer = await Promise.race([prompt.question(`${question} [y/N] `).catch(() => ''), closed]);
|
|
95
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
prompt.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function formatApplyResult(result) {
|
|
102
|
+
const lines = [];
|
|
103
|
+
const applied = result.outcomes.filter((o) => o.kind === 'applied');
|
|
104
|
+
const notApplied = result.outcomes.filter(o => o.kind !== 'applied');
|
|
105
|
+
if (result.outcomes.length === 0) {
|
|
106
|
+
lines.push(`No plans in ${skill_edits_1.SKILL_EDITS_DIR}/ to apply. Run \`boop migrate-skill-edits\` to write them.`);
|
|
107
|
+
}
|
|
108
|
+
if (applied.length > 0) {
|
|
109
|
+
lines.push(`Applied ${plural(applied.length, 'plan')}:`);
|
|
110
|
+
for (const plan of applied) {
|
|
111
|
+
lines.push(` \x1b[32m✓\x1b[0m ${plan.skillPath}: restored boop's copy; the edited copy is at ${plan.backupPath}`);
|
|
112
|
+
for (const rule of plan.rules) {
|
|
113
|
+
lines.push(` ${rule.edit}: moved to ${plan.projectFile}, under ${rule.heading}${rule.reason ? ` — ${rule.reason}` : ''}`);
|
|
114
|
+
}
|
|
115
|
+
for (const drop of plan.drops)
|
|
116
|
+
lines.push(` Dropped ${drop.edit}: ${drop.reason}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (notApplied.length > 0) {
|
|
120
|
+
if (lines.length > 0)
|
|
121
|
+
lines.push('');
|
|
122
|
+
lines.push(`Applied nothing from ${notApplied.length === 1 ? 'this plan' : `these ${notApplied.length} plans`}:`);
|
|
123
|
+
for (const plan of notApplied) {
|
|
124
|
+
lines.push(` \x1b[31m✗\x1b[0m ${plan.planPath}`);
|
|
125
|
+
if (plan.kind === 'refused') {
|
|
126
|
+
for (const reason of plan.reasons)
|
|
127
|
+
lines.push(` ${reason}`);
|
|
128
|
+
}
|
|
129
|
+
else if (plan.kind === 'needs-person') {
|
|
130
|
+
lines.push(' It drops edits, and a drop needs a person to agree to it at a yes-or-no prompt, which this shell cannot show. ' +
|
|
131
|
+
'A person must confirm this by running `boop migrate-skill-edits --run` themselves. The drops:', ...dropLines(plan.drops));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
lines.push(' A person declined these drops. Change their decisions in the plan and run again:', ...dropLines(plan.declined));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (result.recordedHashes.length > 0) {
|
|
139
|
+
if (lines.length > 0)
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push(`Recorded the hash of ${plural(result.recordedHashes.length, 'skill')} that already held boop's copy:`);
|
|
142
|
+
for (const skill of result.recordedHashes)
|
|
143
|
+
lines.push(` · ${skill}`);
|
|
144
|
+
}
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
function dropLines(drops) {
|
|
148
|
+
return drops.map(drop => ` ${drop.edit}: ${drop.reason}`);
|
|
149
|
+
}
|
|
150
|
+
function plural(count, noun) {
|
|
151
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
152
|
+
}
|
|
153
|
+
function formatOutcomes(outcomes) {
|
|
154
|
+
const written = outcomes.filter((o) => o.kind === 'written');
|
|
155
|
+
const kept = outcomes.filter(o => o.kind === 'kept');
|
|
156
|
+
const handSplit = outcomes.filter((o) => o.kind === 'hand-split');
|
|
157
|
+
if (outcomes.length === 0)
|
|
158
|
+
return 'No skill is edited in SKILL.md, so there is nothing to migrate.';
|
|
159
|
+
const lines = [];
|
|
160
|
+
if (written.length > 0) {
|
|
161
|
+
lines.push(`Wrote ${written.length} plan${written.length === 1 ? '' : 's'} to ${skill_edits_1.SKILL_EDITS_DIR}/:`);
|
|
162
|
+
for (const plan of written) {
|
|
163
|
+
const notes = [`${plan.edits} edit${plan.edits === 1 ? '' : 's'}`];
|
|
164
|
+
if (plan.boopChanged)
|
|
165
|
+
notes.push('boop has changes waiting');
|
|
166
|
+
if (plan.replacedStale)
|
|
167
|
+
notes.push('replaced a plan written for older text; the old one is beside it as .bak');
|
|
168
|
+
lines.push(` \x1b[33m◐\x1b[0m ${plan.planPath} (${notes.join(', ')})`);
|
|
169
|
+
if (plan.comparedWithoutOriginal) {
|
|
170
|
+
lines.push(" Could not find the text boop installed, in git history or on npm. The plan compares this project's copy " +
|
|
171
|
+
"with what boop ships now, which cannot tell the project's edits from boop's changes, so every difference needs a decision.");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (kept.length > 0) {
|
|
176
|
+
lines.push(`Left ${kept.length} plan${kept.length === 1 ? '' : 's'} as they were, because the skill and boop's copy have not changed since they were written:`);
|
|
177
|
+
for (const plan of kept)
|
|
178
|
+
lines.push(` · ${plan.planPath}`);
|
|
179
|
+
}
|
|
180
|
+
if (handSplit.length > 0) {
|
|
181
|
+
lines.push(`Split ${handSplit.length === 1 ? 'this skill' : 'these skills'} by hand. boop has no record of installing ${handSplit.length === 1 ? 'it' : 'them'}, so there is no original to compare against:`);
|
|
182
|
+
for (const skill of handSplit) {
|
|
183
|
+
lines.push(` \x1b[33m◐\x1b[0m ${skill.relativePath}${skill.upstreamPath ? ` (boop's current copy is at ${skill.upstreamPath})` : ''}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (written.length + kept.length > 0) {
|
|
187
|
+
lines.push('', `Next: record a decision for every edit in each plan, following the ${update_1.TRIAGE_SKILL} skill.`);
|
|
188
|
+
}
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
|
@@ -11,6 +11,14 @@ export interface CapabilityStatus {
|
|
|
11
11
|
nextSteps?: string[];
|
|
12
12
|
optionalSteps?: string[];
|
|
13
13
|
reachable?: boolean;
|
|
14
|
+
/** Skills whose SKILL.md this project has edited, which no boop update can improve until the edits move out */
|
|
15
|
+
skillEdits?: SkillEditsStatus;
|
|
16
|
+
}
|
|
17
|
+
export interface SkillEditsStatus {
|
|
18
|
+
edited: number;
|
|
19
|
+
/** How many of the edited skills boop has also changed since they were installed */
|
|
20
|
+
changesWaiting: number;
|
|
21
|
+
instruction: string;
|
|
14
22
|
}
|
|
15
23
|
export interface StatusData {
|
|
16
24
|
name: string;
|
package/dist/commands/status.js
CHANGED
|
@@ -45,6 +45,7 @@ const env_1 = require("../lib/env");
|
|
|
45
45
|
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
|
+
const update_1 = require("../lib/update");
|
|
48
49
|
function formatStatus(manifest) {
|
|
49
50
|
const caps = manifest.capabilities.length > 0
|
|
50
51
|
? manifest.capabilities.join(', ')
|
|
@@ -156,6 +157,13 @@ function getStatusData(manifest, dir) {
|
|
|
156
157
|
capabilities[capName] = { status: 'pending', type: capType, env: envStatus, nextSteps };
|
|
157
158
|
}
|
|
158
159
|
}
|
|
160
|
+
// The one check no capability declares: it comes from the same hash comparison `boop update` makes
|
|
161
|
+
for (const capName of manifest.capabilities) {
|
|
162
|
+
const counts = (0, update_1.countSkillEdits)(dir, capName, manifest);
|
|
163
|
+
if (counts) {
|
|
164
|
+
capabilities[capName].skillEdits = { ...counts, instruction: update_1.SKILL_EDITS_INSTRUCTION };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
159
167
|
return { name: manifest.name, profile: manifest.profile, capabilities };
|
|
160
168
|
}
|
|
161
169
|
// Re-export for backward compatibility in tests
|
|
@@ -248,6 +256,11 @@ function formatCapabilityLine(capName, cap) {
|
|
|
248
256
|
}
|
|
249
257
|
}
|
|
250
258
|
}
|
|
259
|
+
if (cap.skillEdits) {
|
|
260
|
+
const { edited, changesWaiting, instruction } = cap.skillEdits;
|
|
261
|
+
lines.push(` \x1b[33m◐\x1b[0m ${edited} skill${edited === 1 ? '' : 's'} edited in SKILL.md, ${changesWaiting} with boop changes waiting`);
|
|
262
|
+
lines.push(` → ${instruction}`);
|
|
263
|
+
}
|
|
251
264
|
return lines;
|
|
252
265
|
}
|
|
253
266
|
/**
|
package/dist/commands/update.js
CHANGED
|
@@ -103,17 +103,24 @@ function runUpdate(capabilityName, options) {
|
|
|
103
103
|
}
|
|
104
104
|
for (const plan of plans) {
|
|
105
105
|
console.log(`\n${plan.capability}:`);
|
|
106
|
-
|
|
106
|
+
// Recording a hash for a file that already holds what boop ships changes nothing the project can see
|
|
107
|
+
const visible = plan.actions.filter(a => a.action !== 'record-hash');
|
|
108
|
+
if (visible.length === 0) {
|
|
107
109
|
console.log(' Already up to date');
|
|
108
110
|
continue;
|
|
109
111
|
}
|
|
110
|
-
for (const action of
|
|
112
|
+
for (const action of visible) {
|
|
113
|
+
if (isEditDoingItsJob(action))
|
|
114
|
+
continue;
|
|
111
115
|
const icon = action.action === 'overwrite' ? '\x1b[32m↑\x1b[0m'
|
|
112
116
|
: action.action === 'skip' ? '\x1b[33m◐\x1b[0m'
|
|
113
117
|
: action.action === 'add' ? '\x1b[32m+\x1b[0m'
|
|
114
118
|
: action.action === 'migrate' ? '\x1b[36m↗\x1b[0m'
|
|
115
119
|
: '\x1b[90m⚠\x1b[0m';
|
|
116
|
-
|
|
120
|
+
const label = action.action === 'skip' && (action.drift || action.instruction) ? skipLabel(action) : action.reason;
|
|
121
|
+
console.log(` ${icon} ${action.relativePath} — ${label}`);
|
|
122
|
+
if (action.instruction)
|
|
123
|
+
console.log(instructionLine(action.instruction));
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
if (stale.length > 0) {
|
|
@@ -136,15 +143,18 @@ function runUpdate(capabilityName, options) {
|
|
|
136
143
|
pruned: [],
|
|
137
144
|
migrated: [],
|
|
138
145
|
unchangedCount: plan.unchangedCount,
|
|
146
|
+
instructions: [],
|
|
139
147
|
});
|
|
140
148
|
continue;
|
|
141
149
|
}
|
|
142
150
|
const result = (0, update_1.executeUpdatePlan)(plan, { force: options.force, restore: options.restore, prune: options.prune });
|
|
143
151
|
results.push(result);
|
|
144
152
|
}
|
|
145
|
-
// Refresh .claude/skills/ symlinks for any new skill folders added by this update
|
|
153
|
+
// Refresh .claude/skills/ symlinks for any new skill folders added by this update,
|
|
154
|
+
// and put back any PROJECT.md the project has deleted
|
|
146
155
|
if (!options.dryRun) {
|
|
147
156
|
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
157
|
+
(0, scaffold_1.createSkillProjectFiles)(targetDir);
|
|
148
158
|
}
|
|
149
159
|
// A script that contradicts the settings a capability now ships survives the
|
|
150
160
|
// update, because scripts are only merged when absent. Say so, or fix it on
|
|
@@ -164,9 +174,11 @@ function runUpdate(capabilityName, options) {
|
|
|
164
174
|
return;
|
|
165
175
|
}
|
|
166
176
|
console.log(`\n${'─'.repeat(40)}`);
|
|
167
|
-
|
|
177
|
+
results.forEach((result, i) => {
|
|
168
178
|
const summary = (0, update_1.formatUpdateSummary)(result);
|
|
169
179
|
console.log(`${result.capability}: ${summary}`);
|
|
180
|
+
const actionFor = (f) => plans[i].actions.find(a => a.relativePath === f);
|
|
181
|
+
const instructionFor = (f) => result.instructions.find(entry => entry.relativePath === f)?.instruction;
|
|
170
182
|
// Show details for non-trivial updates
|
|
171
183
|
for (const f of result.migrated || []) {
|
|
172
184
|
console.log(` \x1b[36m↗\x1b[0m ${f} (migrated to folder)`);
|
|
@@ -178,12 +190,21 @@ function runUpdate(capabilityName, options) {
|
|
|
178
190
|
else {
|
|
179
191
|
console.log(` \x1b[32m↑\x1b[0m ${f}`);
|
|
180
192
|
}
|
|
193
|
+
const instruction = instructionFor(f);
|
|
194
|
+
if (instruction)
|
|
195
|
+
console.log(instructionLine(instruction));
|
|
181
196
|
}
|
|
182
197
|
for (const f of result.skipped) {
|
|
198
|
+
const action = actionFor(f);
|
|
199
|
+
const instruction = instructionFor(f);
|
|
183
200
|
if (result.skippedDeleted?.includes(f)) {
|
|
184
201
|
console.log(` \x1b[36mℹ\x1b[0m ${f} (locally deleted, skipped — use --restore to re-add)`);
|
|
185
202
|
}
|
|
186
|
-
else {
|
|
203
|
+
else if (instruction) {
|
|
204
|
+
console.log(` \x1b[33m◐\x1b[0m ${f} (${skipLabel(action)})`);
|
|
205
|
+
console.log(instructionLine(instruction));
|
|
206
|
+
}
|
|
207
|
+
else if (!action || !isEditDoingItsJob(action)) {
|
|
187
208
|
console.log(` \x1b[33m◐\x1b[0m ${f} (locally modified, skipped)`);
|
|
188
209
|
}
|
|
189
210
|
}
|
|
@@ -198,7 +219,7 @@ function runUpdate(capabilityName, options) {
|
|
|
198
219
|
console.log(` \x1b[33m⚠\x1b[0m ${f} (deprecated — removed from package, use --prune to delete)`);
|
|
199
220
|
}
|
|
200
221
|
}
|
|
201
|
-
}
|
|
222
|
+
});
|
|
202
223
|
for (const fixed of fixedScripts) {
|
|
203
224
|
console.log(`\n\x1b[32m✓\x1b[0m package.json script "${fixed.name}" rewritten: ${fixed.command} → ${fixed.rewritten}`);
|
|
204
225
|
}
|
|
@@ -206,6 +227,33 @@ function runUpdate(capabilityName, options) {
|
|
|
206
227
|
console.log(`\n${(0, stale_scripts_1.formatStaleScriptWarning)(remainingStale)}`);
|
|
207
228
|
}
|
|
208
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* A reference page or code file the project edited, and boop has not, is doing
|
|
232
|
+
* what those files are for, so the report does not list it.
|
|
233
|
+
*/
|
|
234
|
+
function isEditDoingItsJob(action) {
|
|
235
|
+
return action.action === 'skip' && action.drift !== undefined && !action.instruction;
|
|
236
|
+
}
|
|
237
|
+
/** What the report says happened to a file the update skipped */
|
|
238
|
+
function skipLabel(action) {
|
|
239
|
+
if (action?.drift === 'project-only')
|
|
240
|
+
return 'edited here, skipped';
|
|
241
|
+
if (action?.drift === 'both-changed') {
|
|
242
|
+
return action.fileKind === 'skill'
|
|
243
|
+
? 'edited here, and boop has changes waiting, skipped'
|
|
244
|
+
: 'changed here and in boop, skipped';
|
|
245
|
+
}
|
|
246
|
+
if (action?.replacedBy)
|
|
247
|
+
return 'old single-file skill, edited here, skipped';
|
|
248
|
+
if (action?.oldPath)
|
|
249
|
+
return `not installed while ${action.oldPath} is edited, skipped`;
|
|
250
|
+
if (action?.instruction)
|
|
251
|
+
return 'not tracked by boop, skipped';
|
|
252
|
+
return 'locally modified, skipped';
|
|
253
|
+
}
|
|
254
|
+
function instructionLine(instruction) {
|
|
255
|
+
return ` → ${instruction}`;
|
|
256
|
+
}
|
|
209
257
|
/**
|
|
210
258
|
* Read the project's package.json scripts and return those that contradict the
|
|
211
259
|
* runner settings the capabilities now ship. Missing or unreadable package.json
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The applying half of `boop migrate-skill-edits`: check each decided plan,
|
|
3
|
+
* move its kept and offered rules into PROJECT.md, hand SKILL.md back to boop,
|
|
4
|
+
* and record boop's hash, so the skill reads as unmodified at once.
|
|
5
|
+
*/
|
|
6
|
+
import { BoopManifest } from './project';
|
|
7
|
+
/**
|
|
8
|
+
* The block every boop SKILL.md opens with. Keep in step with POINTER_BLOCK in
|
|
9
|
+
* tools/audit-skill-links.js, which holds the shipped skills to it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const POINTER_BLOCK: string;
|
|
12
|
+
/** The PROJECT.md heading each kind of rule goes under */
|
|
13
|
+
export declare const RULE_HEADINGS: {
|
|
14
|
+
readonly adds: "Adds";
|
|
15
|
+
readonly narrows: "Narrows";
|
|
16
|
+
readonly overrides: "Overrides";
|
|
17
|
+
readonly offer: "Offered to boop";
|
|
18
|
+
};
|
|
19
|
+
export interface ApplyDeps {
|
|
20
|
+
/** Whether a person is at a terminal to answer the prompt a drop needs */
|
|
21
|
+
interactive: boolean;
|
|
22
|
+
/** Ask a yes-or-no question; resolves true for yes */
|
|
23
|
+
confirm: (question: string) => Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
export interface Drop {
|
|
26
|
+
edit: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Rule {
|
|
30
|
+
edit: string;
|
|
31
|
+
heading: string;
|
|
32
|
+
rule: string;
|
|
33
|
+
/** What the rewritten rule left out, when the triage skill recorded it */
|
|
34
|
+
reason?: string;
|
|
35
|
+
}
|
|
36
|
+
export type PlanOutcome = {
|
|
37
|
+
kind: 'applied';
|
|
38
|
+
planPath: string;
|
|
39
|
+
skillPath: string;
|
|
40
|
+
projectFile: string;
|
|
41
|
+
backupPath: string;
|
|
42
|
+
rules: Rule[];
|
|
43
|
+
drops: Drop[];
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'refused';
|
|
46
|
+
planPath: string;
|
|
47
|
+
reasons: string[];
|
|
48
|
+
}
|
|
49
|
+
/** Checks passed, but it drops edits and nobody is at a terminal to agree */
|
|
50
|
+
| {
|
|
51
|
+
kind: 'needs-person';
|
|
52
|
+
planPath: string;
|
|
53
|
+
drops: Drop[];
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'declined';
|
|
56
|
+
planPath: string;
|
|
57
|
+
declined: Drop[];
|
|
58
|
+
};
|
|
59
|
+
export interface ApplyResult {
|
|
60
|
+
outcomes: PlanOutcome[];
|
|
61
|
+
/** Skills that already held boop's copy, whose hash this run recorded */
|
|
62
|
+
recordedHashes: string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check every plan in `.boop/skill-edits/`, apply each one that passes, and
|
|
66
|
+
* record the hash of every skill that already holds boop's copy. A plan that
|
|
67
|
+
* fails any check, or whose drops nobody confirms, has nothing applied.
|
|
68
|
+
*/
|
|
69
|
+
export declare function applySkillEditPlans(projectDir: string, manifest: BoopManifest, deps: ApplyDeps): Promise<ApplyResult>;
|
|
70
|
+
/** Whether a SKILL.md's body, below its frontmatter, opens with the pointer block. */
|
|
71
|
+
export declare function opensWithPointer(markdown: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Append a rule to the end of a section of PROJECT.md, adding the heading at
|
|
74
|
+
* the end of the file when the project has removed it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function addRule(text: string, heading: string, rule: string): string;
|