@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/README.md
CHANGED
|
@@ -148,7 +148,7 @@ Adding a capability installs its skills into your project's `skills/` directory.
|
|
|
148
148
|
- **`coding`** — boop-coding-beads-flow, boop-coding-development, boop-coding-commit-changes, boop-coding-review-changes, boop-coding-debugging
|
|
149
149
|
- **`product-design`** — boop-product-design-engineering-design, boop-product-design-feature-documentation, boop-product-design-feature-requirements, boop-product-design-reference-integration, boop-product-design-thought-partnership, boop-writing-technical-prose, boop-writing-documents, boop-writing-semantic-line-breaks
|
|
150
150
|
- **`project-mgmt`** — boop-project-mgmt-beads-planning, boop-project-mgmt-engineering-planning
|
|
151
|
-
- **`skills`** — boop-skill-authoring, boop-skills-discovery, boop-skills-agent-surface-design, boop-get-smart, boop-so-what, boop-skills-hyperlinking
|
|
151
|
+
- **`skills`** — boop-skill-authoring, boop-skills-discovery, boop-skills-agent-surface-design, boop-get-smart, boop-so-what, boop-skills-hyperlinking, boop-skills-edit-triage
|
|
152
152
|
- **`testing`** — boop-testing-tdd-light, boop-testing-testing-express-web, boop-testing-testing
|
|
153
153
|
- **`ux`** — boop-ux-purpose-judgement, boop-ux-visual-judgement, boop-ux-eval-and-refine-workflow, boop-ux-app-ui-dev-workflow
|
|
154
154
|
- **`workflows`** — boop-feature-dev-workflow, boop-light-dev-workflow, boop-refactor-dev-workflow, boop-test-audit-workflow, boop-architecture-audit-workflow, boop-issue-sweep-workflow, boop-website-dev-workflow, boop-design-handoff-workflow, boop-knowledge-backfill, boop-stray-disposition
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@boopkit/cap-api": "0.4.
|
|
3
|
-
"@boopkit/cap-background-jobs": "0.4.
|
|
4
|
-
"@boopkit/cap-cloud-storage": "0.4.
|
|
5
|
-
"@boopkit/cap-coding": "0.4.
|
|
6
|
-
"@boopkit/cap-database": "0.4.
|
|
7
|
-
"@boopkit/cap-email": "0.4.
|
|
2
|
+
"@boopkit/cap-api": "0.4.5",
|
|
3
|
+
"@boopkit/cap-background-jobs": "0.4.3",
|
|
4
|
+
"@boopkit/cap-cloud-storage": "0.4.1",
|
|
5
|
+
"@boopkit/cap-coding": "0.4.6",
|
|
6
|
+
"@boopkit/cap-database": "0.4.4",
|
|
7
|
+
"@boopkit/cap-email": "0.4.3",
|
|
8
8
|
"@boopkit/cap-hello": "0.4.1",
|
|
9
9
|
"@boopkit/cap-noticeboard": "0.4.1",
|
|
10
10
|
"@boopkit/cap-oauth-client": "0.1.1",
|
|
11
|
-
"@boopkit/cap-oauth-server": "0.1.
|
|
12
|
-
"@boopkit/cap-product-design": "0.6.
|
|
13
|
-
"@boopkit/cap-project-mgmt": "0.4.
|
|
14
|
-
"@boopkit/cap-services": "0.4.
|
|
15
|
-
"@boopkit/cap-skills": "0.5.
|
|
16
|
-
"@boopkit/cap-testing": "0.5.
|
|
17
|
-
"@boopkit/cap-users": "0.8.
|
|
18
|
-
"@boopkit/cap-ux": "0.2.
|
|
19
|
-
"@boopkit/cap-web": "0.5.
|
|
20
|
-
"@boopkit/cap-workflows": "0.2.
|
|
11
|
+
"@boopkit/cap-oauth-server": "0.1.4",
|
|
12
|
+
"@boopkit/cap-product-design": "0.6.9",
|
|
13
|
+
"@boopkit/cap-project-mgmt": "0.4.11",
|
|
14
|
+
"@boopkit/cap-services": "0.4.3",
|
|
15
|
+
"@boopkit/cap-skills": "0.5.6",
|
|
16
|
+
"@boopkit/cap-testing": "0.5.7",
|
|
17
|
+
"@boopkit/cap-users": "0.8.1",
|
|
18
|
+
"@boopkit/cap-ux": "0.2.3",
|
|
19
|
+
"@boopkit/cap-web": "0.5.5",
|
|
20
|
+
"@boopkit/cap-workflows": "0.2.2",
|
|
21
21
|
"@boopkit/logger": "0.4.0"
|
|
22
22
|
}
|
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,7 @@ const list_1 = require("./commands/list");
|
|
|
43
43
|
const add_1 = require("./commands/add");
|
|
44
44
|
const adopt_1 = require("./commands/adopt");
|
|
45
45
|
const update_1 = require("./commands/update");
|
|
46
|
+
const migrate_skill_edits_1 = require("./commands/migrate-skill-edits");
|
|
46
47
|
const loop_1 = require("./commands/loop");
|
|
47
48
|
const loop_merge_1 = require("./commands/loop-merge");
|
|
48
49
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -95,6 +96,12 @@ program
|
|
|
95
96
|
.option('--fix-scripts', 'Rewrite package.json scripts that contradict shipped runner settings')
|
|
96
97
|
.option('--json', 'Output structured JSON')
|
|
97
98
|
.action(update_1.runUpdate);
|
|
99
|
+
program
|
|
100
|
+
.command('migrate-skill-edits')
|
|
101
|
+
.description('Write a decision plan to .boop/skill-edits/ for each skill whose SKILL.md this project edited. ' +
|
|
102
|
+
'Reports by default: changes no skill, PROJECT.md or config')
|
|
103
|
+
.option('--run', "Apply each decided plan: move its rules into PROJECT.md, back up SKILL.md, restore boop's copy and record its hash")
|
|
104
|
+
.action((options) => (options.run ? (0, migrate_skill_edits_1.runApplySkillEdits)() : (0, migrate_skill_edits_1.runMigrateSkillEdits)()));
|
|
98
105
|
program
|
|
99
106
|
.command('adopt')
|
|
100
107
|
.description('Copy package source into the project for full ownership')
|
package/dist/commands/init.js
CHANGED
|
@@ -367,8 +367,9 @@ function runInit(name, options, deps = {}) {
|
|
|
367
367
|
}
|
|
368
368
|
// --- Inject CSS imports for capability partials ---
|
|
369
369
|
(0, scaffold_1.injectCssImports)(targetDir, cssCapMap, { skipBackup: true });
|
|
370
|
-
// --- Symlink skills/* into .claude/skills/ for Claude Code auto-discovery ---
|
|
370
|
+
// --- Symlink skills/* into .claude/skills/ for Claude Code auto-discovery, and give each a PROJECT.md ---
|
|
371
371
|
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
372
|
+
(0, scaffold_1.createSkillProjectFiles)(targetDir, capSearchPaths);
|
|
372
373
|
// --- Merge capability scripts into package.json ---
|
|
373
374
|
if (Object.keys(allScripts).length > 0) {
|
|
374
375
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
@@ -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,27 @@ 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 && plan.scriptActions.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));
|
|
124
|
+
}
|
|
125
|
+
for (const line of scriptLines(plan.scriptActions)) {
|
|
126
|
+
console.log(line);
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
if (stale.length > 0) {
|
|
@@ -135,16 +145,21 @@ function runUpdate(capabilityName, options) {
|
|
|
135
145
|
skippedDeleted: [],
|
|
136
146
|
pruned: [],
|
|
137
147
|
migrated: [],
|
|
148
|
+
scriptsAdded: [],
|
|
149
|
+
scriptsKept: plan.scriptActions.filter(a => a.action === 'keep'),
|
|
138
150
|
unchangedCount: plan.unchangedCount,
|
|
151
|
+
instructions: [],
|
|
139
152
|
});
|
|
140
153
|
continue;
|
|
141
154
|
}
|
|
142
155
|
const result = (0, update_1.executeUpdatePlan)(plan, { force: options.force, restore: options.restore, prune: options.prune });
|
|
143
156
|
results.push(result);
|
|
144
157
|
}
|
|
145
|
-
// Refresh .claude/skills/ symlinks for any new skill folders added by this update
|
|
158
|
+
// Refresh .claude/skills/ symlinks for any new skill folders added by this update,
|
|
159
|
+
// and put back any PROJECT.md the project has deleted
|
|
146
160
|
if (!options.dryRun) {
|
|
147
161
|
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
162
|
+
(0, scaffold_1.createSkillProjectFiles)(targetDir);
|
|
148
163
|
}
|
|
149
164
|
// A script that contradicts the settings a capability now ships survives the
|
|
150
165
|
// update, because scripts are only merged when absent. Say so, or fix it on
|
|
@@ -164,9 +179,11 @@ function runUpdate(capabilityName, options) {
|
|
|
164
179
|
return;
|
|
165
180
|
}
|
|
166
181
|
console.log(`\n${'─'.repeat(40)}`);
|
|
167
|
-
|
|
182
|
+
results.forEach((result, i) => {
|
|
168
183
|
const summary = (0, update_1.formatUpdateSummary)(result);
|
|
169
184
|
console.log(`${result.capability}: ${summary}`);
|
|
185
|
+
const actionFor = (f) => plans[i].actions.find(a => a.relativePath === f);
|
|
186
|
+
const instructionFor = (f) => result.instructions.find(entry => entry.relativePath === f)?.instruction;
|
|
170
187
|
// Show details for non-trivial updates
|
|
171
188
|
for (const f of result.migrated || []) {
|
|
172
189
|
console.log(` \x1b[36m↗\x1b[0m ${f} (migrated to folder)`);
|
|
@@ -178,12 +195,21 @@ function runUpdate(capabilityName, options) {
|
|
|
178
195
|
else {
|
|
179
196
|
console.log(` \x1b[32m↑\x1b[0m ${f}`);
|
|
180
197
|
}
|
|
198
|
+
const instruction = instructionFor(f);
|
|
199
|
+
if (instruction)
|
|
200
|
+
console.log(instructionLine(instruction));
|
|
181
201
|
}
|
|
182
202
|
for (const f of result.skipped) {
|
|
203
|
+
const action = actionFor(f);
|
|
204
|
+
const instruction = instructionFor(f);
|
|
183
205
|
if (result.skippedDeleted?.includes(f)) {
|
|
184
206
|
console.log(` \x1b[36mℹ\x1b[0m ${f} (locally deleted, skipped — use --restore to re-add)`);
|
|
185
207
|
}
|
|
186
|
-
else {
|
|
208
|
+
else if (instruction) {
|
|
209
|
+
console.log(` \x1b[33m◐\x1b[0m ${f} (${skipLabel(action)})`);
|
|
210
|
+
console.log(instructionLine(instruction));
|
|
211
|
+
}
|
|
212
|
+
else if (!action || !isEditDoingItsJob(action)) {
|
|
187
213
|
console.log(` \x1b[33m◐\x1b[0m ${f} (locally modified, skipped)`);
|
|
188
214
|
}
|
|
189
215
|
}
|
|
@@ -198,7 +224,10 @@ function runUpdate(capabilityName, options) {
|
|
|
198
224
|
console.log(` \x1b[33m⚠\x1b[0m ${f} (deprecated — removed from package, use --prune to delete)`);
|
|
199
225
|
}
|
|
200
226
|
}
|
|
201
|
-
|
|
227
|
+
for (const line of scriptLines([...(result.scriptsAdded || []), ...(result.scriptsKept || [])])) {
|
|
228
|
+
console.log(line);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
202
231
|
for (const fixed of fixedScripts) {
|
|
203
232
|
console.log(`\n\x1b[32m✓\x1b[0m package.json script "${fixed.name}" rewritten: ${fixed.command} → ${fixed.rewritten}`);
|
|
204
233
|
}
|
|
@@ -206,6 +235,54 @@ function runUpdate(capabilityName, options) {
|
|
|
206
235
|
console.log(`\n${(0, stale_scripts_1.formatStaleScriptWarning)(remainingStale)}`);
|
|
207
236
|
}
|
|
208
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* How the report shows the npm scripts a capability declares: an added one reads
|
|
240
|
+
* like a new file, and a kept one shows the command the capability ships, so a
|
|
241
|
+
* project whose own command has fallen behind can see that it has. The label
|
|
242
|
+
* claims only that the commands differ: with no record of the command boop wrote,
|
|
243
|
+
* a command an older capability version shipped looks just like a customisation.
|
|
244
|
+
*/
|
|
245
|
+
function scriptLines(actions) {
|
|
246
|
+
const lines = [];
|
|
247
|
+
for (const action of actions) {
|
|
248
|
+
if (action.action === 'add') {
|
|
249
|
+
lines.push(` \x1b[32m+\x1b[0m npm run ${action.name} (new script)`);
|
|
250
|
+
lines.push(instructionLine(action.command));
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
lines.push(` \x1b[33m◐\x1b[0m npm run ${action.name} (your command differs; kept)`);
|
|
254
|
+
lines.push(instructionLine(`this capability ships: ${action.command}`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return lines;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* A reference page or code file the project edited, and boop has not, is doing
|
|
261
|
+
* what those files are for, so the report does not list it.
|
|
262
|
+
*/
|
|
263
|
+
function isEditDoingItsJob(action) {
|
|
264
|
+
return action.action === 'skip' && action.drift !== undefined && !action.instruction;
|
|
265
|
+
}
|
|
266
|
+
/** What the report says happened to a file the update skipped */
|
|
267
|
+
function skipLabel(action) {
|
|
268
|
+
if (action?.drift === 'project-only')
|
|
269
|
+
return 'edited here, skipped';
|
|
270
|
+
if (action?.drift === 'both-changed') {
|
|
271
|
+
return action.fileKind === 'skill'
|
|
272
|
+
? 'edited here, and boop has changes waiting, skipped'
|
|
273
|
+
: 'changed here and in boop, skipped';
|
|
274
|
+
}
|
|
275
|
+
if (action?.replacedBy)
|
|
276
|
+
return 'old single-file skill, edited here, skipped';
|
|
277
|
+
if (action?.oldPath)
|
|
278
|
+
return `not installed while ${action.oldPath} is edited, skipped`;
|
|
279
|
+
if (action?.instruction)
|
|
280
|
+
return 'not tracked by boop, skipped';
|
|
281
|
+
return 'locally modified, skipped';
|
|
282
|
+
}
|
|
283
|
+
function instructionLine(instruction) {
|
|
284
|
+
return ` → ${instruction}`;
|
|
285
|
+
}
|
|
209
286
|
/**
|
|
210
287
|
* Read the project's package.json scripts and return those that contradict the
|
|
211
288
|
* 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;
|