@boopkit/cli 0.3.2 → 0.4.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.
- package/README.md +2 -2
- package/dist/capability-versions.json +16 -16
- package/dist/commands/init.js +2 -2
- package/dist/commands/loop.js +18 -7
- package/dist/commands/update.js +11 -1
- package/dist/lib/execute.js +1 -4
- package/dist/lib/manifest.d.ts +1 -1
- package/dist/lib/preflight.js +0 -8
- package/dist/lib/scaffold.d.ts +12 -5
- package/dist/lib/scaffold.js +65 -100
- package/dist/lib/update.d.ts +5 -1
- package/dist/lib/update.js +72 -6
- package/package.json +3 -3
- package/src/templates/kernel/AGENTS.md.hbs +2 -3
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ A **capability** is a composable feature bundle. Each capability package ships:
|
|
|
61
61
|
- **Middleware hooks** — lines wired into `src/server.ts` at the right phase (imports, pre-routes, routes, error-handling)
|
|
62
62
|
- **Scripts** — added to `package.json` (e.g. `db:migrate`, `tailwind:build`)
|
|
63
63
|
- **Post-install steps** — human/agent-readable instructions printed by `boop status`
|
|
64
|
-
- **Agent skills** —
|
|
64
|
+
- **Agent skills** — skill folders (each containing a `SKILL.md`) that teach AI agents the conventions this capability uses
|
|
65
65
|
|
|
66
66
|
Capabilities can declare `requires` relationships (e.g. `users-and-accounts` requires `database`, `services`, and `web`). `boop add` resolves the dependency graph and installs everything needed.
|
|
67
67
|
|
|
@@ -138,7 +138,7 @@ Flags:
|
|
|
138
138
|
|
|
139
139
|
Boop is built for agent-assisted work — the skills and conventions ship with the code so your agent has real guidance, not just a blank repo. Every scaffolded project includes:
|
|
140
140
|
|
|
141
|
-
- **Skills** — every capability installs
|
|
141
|
+
- **Skills** — every capability installs skill folders into `skills/`. These are focused, task-specific instructions (each folder contains a `SKILL.md`, e.g. `skills/rest-api-design/SKILL.md`, `skills/tdd-light/SKILL.md`) that tell an agent the conventions this project uses.
|
|
142
142
|
- **`boop status`** — after `boop init` or `boop add`, run `boop status`. It prints every capability's post-install steps. Hand this to your agent and say "finish setup."
|
|
143
143
|
- **`boop loop`** — Uses Claude for 'unattended work', building working code and UI from your specifications.
|
|
144
144
|
- `prefix` mode: Plan work as a tree of issues, then `boop loop --prefix <id>` processes them one at a time, each in its own Claude Code session. Dry-run by default; budget-capped; resumes unfinished work. See [docs/loop.md](docs/loop.md).
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@boopkit/cap-api": "0.
|
|
3
|
-
"@boopkit/cap-background-jobs": "0.
|
|
4
|
-
"@boopkit/cap-cloud-storage": "0.
|
|
5
|
-
"@boopkit/cap-coding": "0.
|
|
6
|
-
"@boopkit/cap-database": "0.
|
|
7
|
-
"@boopkit/cap-email": "0.
|
|
8
|
-
"@boopkit/cap-hello": "0.
|
|
9
|
-
"@boopkit/cap-noticeboard": "0.
|
|
10
|
-
"@boopkit/cap-product-design": "0.
|
|
11
|
-
"@boopkit/cap-project-mgmt": "0.
|
|
12
|
-
"@boopkit/cap-services": "0.
|
|
13
|
-
"@boopkit/cap-skills": "0.
|
|
14
|
-
"@boopkit/cap-testing": "0.
|
|
15
|
-
"@boopkit/cap-users": "0.
|
|
16
|
-
"@boopkit/cap-web": "0.
|
|
17
|
-
"@boopkit/logger": "0.
|
|
2
|
+
"@boopkit/cap-api": "0.4.0",
|
|
3
|
+
"@boopkit/cap-background-jobs": "0.4.0",
|
|
4
|
+
"@boopkit/cap-cloud-storage": "0.4.0",
|
|
5
|
+
"@boopkit/cap-coding": "0.4.0",
|
|
6
|
+
"@boopkit/cap-database": "0.4.0",
|
|
7
|
+
"@boopkit/cap-email": "0.4.1",
|
|
8
|
+
"@boopkit/cap-hello": "0.4.0",
|
|
9
|
+
"@boopkit/cap-noticeboard": "0.4.0",
|
|
10
|
+
"@boopkit/cap-product-design": "0.4.0",
|
|
11
|
+
"@boopkit/cap-project-mgmt": "0.4.0",
|
|
12
|
+
"@boopkit/cap-services": "0.4.0",
|
|
13
|
+
"@boopkit/cap-skills": "0.4.0",
|
|
14
|
+
"@boopkit/cap-testing": "0.4.0",
|
|
15
|
+
"@boopkit/cap-users": "0.4.1",
|
|
16
|
+
"@boopkit/cap-web": "0.4.1",
|
|
17
|
+
"@boopkit/logger": "0.4.0"
|
|
18
18
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -245,6 +245,8 @@ function runInit(name, options) {
|
|
|
245
245
|
}
|
|
246
246
|
// --- Inject CSS imports for capability partials ---
|
|
247
247
|
(0, scaffold_1.injectCssImports)(targetDir, cssCapMap, { skipBackup: true });
|
|
248
|
+
// --- Symlink skills/* into .claude/skills/ for Claude Code auto-discovery ---
|
|
249
|
+
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
248
250
|
// --- Merge capability scripts into package.json ---
|
|
249
251
|
if (Object.keys(allScripts).length > 0) {
|
|
250
252
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
@@ -263,8 +265,6 @@ function runInit(name, options) {
|
|
|
263
265
|
if (fs.existsSync(profileRefDir)) {
|
|
264
266
|
(0, safe_io_1.safeCopyDir)(profileRefDir, path.join(targetDir, 'reference'), { onConflict: 'skip' });
|
|
265
267
|
}
|
|
266
|
-
// --- Generate skills index from installed skill files ---
|
|
267
|
-
(0, scaffold_1.generateSkillsIndex)(targetDir);
|
|
268
268
|
// --- Build capabilityConfig for capabilities with env vars ---
|
|
269
269
|
const capabilityConfig = {};
|
|
270
270
|
const envEntries = [];
|
package/dist/commands/loop.js
CHANGED
|
@@ -332,7 +332,7 @@ ${issue.description}
|
|
|
332
332
|
## Instructions
|
|
333
333
|
1. Read AGENTS.md for project orientation
|
|
334
334
|
2. Claim the issue: bd update ${issue.id} --claim
|
|
335
|
-
3. Read relevant skills from skills/
|
|
335
|
+
3. Read relevant skills from skills/ (list folders, read each SKILL.md) before making changes
|
|
336
336
|
4. Implement the changes following TDD-Lite: observe, implement, verify
|
|
337
337
|
5. Run tests if applicable: npm test
|
|
338
338
|
6. Commit your changes with a message referencing ${issue.id}
|
|
@@ -566,10 +566,21 @@ async function runLoop(options) {
|
|
|
566
566
|
return;
|
|
567
567
|
}
|
|
568
568
|
if (flow && !fs.existsSync(flow)) {
|
|
569
|
-
console.error(`\x1b[31mERR\x1b[0m --flow
|
|
569
|
+
console.error(`\x1b[31mERR\x1b[0m --flow path not found: ${flow}`);
|
|
570
570
|
process.exitCode = 1;
|
|
571
571
|
return;
|
|
572
572
|
}
|
|
573
|
+
// If --flow points to a directory (e.g. a skill folder), resolve to its SKILL.md
|
|
574
|
+
let resolvedFlow = flow;
|
|
575
|
+
if (flow && fs.statSync(flow).isDirectory()) {
|
|
576
|
+
const skillPath = path.join(flow, 'SKILL.md');
|
|
577
|
+
if (!fs.existsSync(skillPath)) {
|
|
578
|
+
console.error(`\x1b[31mERR\x1b[0m --flow directory has no SKILL.md: ${flow}`);
|
|
579
|
+
process.exitCode = 1;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
resolvedFlow = skillPath;
|
|
583
|
+
}
|
|
573
584
|
const dryRun = !options.run;
|
|
574
585
|
const maxIssues = parseInt(options.max || '0', 10);
|
|
575
586
|
const budget = options.budget || '5.00';
|
|
@@ -586,12 +597,12 @@ async function runLoop(options) {
|
|
|
586
597
|
const logDir = path.join(projectDir, 'labs', 'claude-runs');
|
|
587
598
|
fs.mkdirSync(logDir, { recursive: true });
|
|
588
599
|
// Planning mode dry-run
|
|
589
|
-
if (dryRun && work &&
|
|
600
|
+
if (dryRun && work && resolvedFlow) {
|
|
590
601
|
const workContent = fs.readFileSync(work, 'utf8');
|
|
591
|
-
const flowContent = fs.readFileSync(
|
|
602
|
+
const flowContent = fs.readFileSync(resolvedFlow, 'utf8');
|
|
592
603
|
console.log('[bd-loop] PLANNING MODE (dry-run)');
|
|
593
604
|
console.log(`[bd-loop] Work file: ${work}`);
|
|
594
|
-
console.log(`[bd-loop] Flow file: ${flow}`);
|
|
605
|
+
console.log(`[bd-loop] Flow file: ${resolvedFlow}${resolvedFlow !== flow ? ` (resolved from ${flow})` : ''}`);
|
|
595
606
|
console.log('[bd-loop] Planning prompt that would be sent:');
|
|
596
607
|
console.log('---');
|
|
597
608
|
console.log(buildPlanningPrompt(workContent, flowContent, prefix));
|
|
@@ -600,10 +611,10 @@ async function runLoop(options) {
|
|
|
600
611
|
return;
|
|
601
612
|
}
|
|
602
613
|
// Planning phase: create feature tree before looping
|
|
603
|
-
if (work &&
|
|
614
|
+
if (work && resolvedFlow) {
|
|
604
615
|
const planResult = await planWork({
|
|
605
616
|
workPath: work,
|
|
606
|
-
flowPath:
|
|
617
|
+
flowPath: resolvedFlow,
|
|
607
618
|
prefix,
|
|
608
619
|
budget,
|
|
609
620
|
model,
|
package/dist/commands/update.js
CHANGED
|
@@ -40,6 +40,7 @@ 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 scaffold_1 = require("../lib/scaffold");
|
|
43
44
|
function runUpdate(capabilityName, options) {
|
|
44
45
|
const targetDir = process.cwd();
|
|
45
46
|
// Must be in a boop project
|
|
@@ -108,7 +109,8 @@ function runUpdate(capabilityName, options) {
|
|
|
108
109
|
const icon = action.action === 'overwrite' ? '\x1b[32m↑\x1b[0m'
|
|
109
110
|
: action.action === 'skip' ? '\x1b[33m◐\x1b[0m'
|
|
110
111
|
: action.action === 'add' ? '\x1b[32m+\x1b[0m'
|
|
111
|
-
: '\x1b[
|
|
112
|
+
: action.action === 'migrate' ? '\x1b[36m↗\x1b[0m'
|
|
113
|
+
: '\x1b[90m⚠\x1b[0m';
|
|
112
114
|
console.log(` ${icon} ${action.relativePath} — ${action.reason}`);
|
|
113
115
|
}
|
|
114
116
|
}
|
|
@@ -127,6 +129,7 @@ function runUpdate(capabilityName, options) {
|
|
|
127
129
|
forcedOverwrite: [],
|
|
128
130
|
skippedDeleted: [],
|
|
129
131
|
pruned: [],
|
|
132
|
+
migrated: [],
|
|
130
133
|
unchangedCount: plan.unchangedCount,
|
|
131
134
|
});
|
|
132
135
|
continue;
|
|
@@ -134,6 +137,10 @@ function runUpdate(capabilityName, options) {
|
|
|
134
137
|
const result = (0, update_1.executeUpdatePlan)(plan, { force: options.force, restore: options.restore, prune: options.prune });
|
|
135
138
|
results.push(result);
|
|
136
139
|
}
|
|
140
|
+
// Refresh .claude/skills/ symlinks for any new skill folders added by this update
|
|
141
|
+
if (!options.dryRun) {
|
|
142
|
+
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
143
|
+
}
|
|
137
144
|
// Output
|
|
138
145
|
if (options.json) {
|
|
139
146
|
console.log(JSON.stringify({ capabilities: results }, null, 2));
|
|
@@ -144,6 +151,9 @@ function runUpdate(capabilityName, options) {
|
|
|
144
151
|
const summary = (0, update_1.formatUpdateSummary)(result);
|
|
145
152
|
console.log(`${result.capability}: ${summary}`);
|
|
146
153
|
// Show details for non-trivial updates
|
|
154
|
+
for (const f of result.migrated || []) {
|
|
155
|
+
console.log(` \x1b[36m↗\x1b[0m ${f} (migrated to folder)`);
|
|
156
|
+
}
|
|
147
157
|
for (const f of result.overwritten) {
|
|
148
158
|
if (result.forcedOverwrite.includes(f)) {
|
|
149
159
|
console.log(` \x1b[33m⚠\x1b[0m ${f} (force-overwritten, backup saved)`);
|
package/dist/lib/execute.js
CHANGED
|
@@ -100,6 +100,7 @@ function executeTask(task, manifest) {
|
|
|
100
100
|
switch (task.op) {
|
|
101
101
|
case 'copy-files': {
|
|
102
102
|
(0, scaffold_1.copyCapabilityFiles)(targetDir, capabilityName);
|
|
103
|
+
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
103
104
|
break;
|
|
104
105
|
}
|
|
105
106
|
case 'merge-deps': {
|
|
@@ -154,10 +155,6 @@ function executeTask(task, manifest) {
|
|
|
154
155
|
(0, scaffold_1.injectCssImports)(targetDir, data.cssCapMap, { skipBackup: true });
|
|
155
156
|
break;
|
|
156
157
|
}
|
|
157
|
-
case 'generate-skills-index': {
|
|
158
|
-
(0, scaffold_1.generateSkillsIndex)(targetDir);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
158
|
case 'update-manifest': {
|
|
162
159
|
const data = task.data;
|
|
163
160
|
const projectManifest = (0, project_1.readProjectManifest)(targetDir);
|
package/dist/lib/manifest.d.ts
CHANGED
|
@@ -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' | '
|
|
10
|
+
export type ManifestOp = '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;
|
package/dist/lib/preflight.js
CHANGED
|
@@ -329,14 +329,6 @@ function planAdd(targetDir, capabilityName) {
|
|
|
329
329
|
data: { cssCapMap },
|
|
330
330
|
});
|
|
331
331
|
}
|
|
332
|
-
// Task: regenerate skills index
|
|
333
|
-
tasks.push({
|
|
334
|
-
op: 'generate-skills-index',
|
|
335
|
-
source: capabilityName,
|
|
336
|
-
target: path.join(targetDir, 'skills', 'index.md'),
|
|
337
|
-
detail: 'Regenerate skills/index.md',
|
|
338
|
-
status: 'pending',
|
|
339
|
-
});
|
|
340
332
|
// Task: update manifest
|
|
341
333
|
tasks.push({
|
|
342
334
|
op: 'update-manifest',
|
package/dist/lib/scaffold.d.ts
CHANGED
|
@@ -36,6 +36,18 @@ export declare function hashFile(filePath: string): string;
|
|
|
36
36
|
* Returns list of copied file paths relative to targetDir, and file hashes.
|
|
37
37
|
*/
|
|
38
38
|
export declare function copyCapabilityFiles(targetDir: string, capabilityName: string, searchPaths?: string[]): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Symlink each skill folder under `<targetDir>/skills/` into
|
|
41
|
+
* `<targetDir>/.claude/skills/` so Claude Code's built-in Skill tool
|
|
42
|
+
* auto-discovers them alongside any plugin-installed skills.
|
|
43
|
+
*
|
|
44
|
+
* Per-folder symlinks (rather than a single dir-level symlink) so existing
|
|
45
|
+
* `.claude/skills/<other>` entries (Claude Code plugins, user-authored skills)
|
|
46
|
+
* are preserved.
|
|
47
|
+
*
|
|
48
|
+
* Idempotent. Returns the relative paths of symlinks created or already present.
|
|
49
|
+
*/
|
|
50
|
+
export declare function linkSkillsToClaude(targetDir: string): string[];
|
|
39
51
|
/**
|
|
40
52
|
* Merge dependencies into an existing package.json.
|
|
41
53
|
* Does not overwrite existing dependency versions.
|
|
@@ -83,9 +95,4 @@ interface CapabilityHookEntry {
|
|
|
83
95
|
* prevent arbitrary code injection into scaffolded projects.
|
|
84
96
|
*/
|
|
85
97
|
export declare function collectHooks(capabilities: CapabilityHookEntry[]): HookVariables;
|
|
86
|
-
/**
|
|
87
|
-
* Generate skills/index.md from the .skill.md files present in the project.
|
|
88
|
-
* Overwrites any existing index.md.
|
|
89
|
-
*/
|
|
90
|
-
export declare function generateSkillsIndex(targetDir: string): void;
|
|
91
98
|
export {};
|
package/dist/lib/scaffold.js
CHANGED
|
@@ -40,11 +40,11 @@ exports.renderTemplate = renderTemplate;
|
|
|
40
40
|
exports.scaffoldKernel = scaffoldKernel;
|
|
41
41
|
exports.hashFile = hashFile;
|
|
42
42
|
exports.copyCapabilityFiles = copyCapabilityFiles;
|
|
43
|
+
exports.linkSkillsToClaude = linkSkillsToClaude;
|
|
43
44
|
exports.getBoopkitVersion = getBoopkitVersion;
|
|
44
45
|
exports.mergePackageJson = mergePackageJson;
|
|
45
46
|
exports.injectCssImports = injectCssImports;
|
|
46
47
|
exports.collectHooks = collectHooks;
|
|
47
|
-
exports.generateSkillsIndex = generateSkillsIndex;
|
|
48
48
|
const fs = __importStar(require("fs"));
|
|
49
49
|
const path = __importStar(require("path"));
|
|
50
50
|
const crypto = __importStar(require("crypto"));
|
|
@@ -139,6 +139,19 @@ function copyCapabilityFiles(targetDir, capabilityName, searchPaths) {
|
|
|
139
139
|
// Fallback to source tree
|
|
140
140
|
return copyFromSourceTree(targetDir, capabilityName);
|
|
141
141
|
}
|
|
142
|
+
function walkDir(dir) {
|
|
143
|
+
const results = [];
|
|
144
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
145
|
+
const fullPath = path.join(dir, entry.name);
|
|
146
|
+
if (entry.isDirectory()) {
|
|
147
|
+
results.push(...walkDir(fullPath));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
results.push(fullPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
142
155
|
/**
|
|
143
156
|
* Copy capability files from a package directory.
|
|
144
157
|
*/
|
|
@@ -153,10 +166,14 @@ function copyFromPackage(targetDir, capabilityName, pkgDir, boopkit) {
|
|
|
153
166
|
(0, safe_io_1.safeCopyDir)(skillsDir, destSkillsDir, { onConflict: 'skip' });
|
|
154
167
|
const entries = fs.readdirSync(skillsDir);
|
|
155
168
|
for (const entry of entries) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
const srcEntryPath = path.join(skillsDir, entry);
|
|
170
|
+
if (fs.statSync(srcEntryPath).isDirectory()) {
|
|
171
|
+
const files = walkDir(path.join(destSkillsDir, entry));
|
|
172
|
+
for (const filePath of files) {
|
|
173
|
+
const rel = path.join('skills', entry, path.relative(path.join(destSkillsDir, entry), filePath));
|
|
174
|
+
copied.push(rel);
|
|
175
|
+
fileHashes[rel] = hashFile(filePath);
|
|
176
|
+
}
|
|
160
177
|
}
|
|
161
178
|
}
|
|
162
179
|
}
|
|
@@ -182,6 +199,42 @@ function copyFromPackage(targetDir, capabilityName, pkgDir, boopkit) {
|
|
|
182
199
|
}
|
|
183
200
|
return copied;
|
|
184
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Symlink each skill folder under `<targetDir>/skills/` into
|
|
204
|
+
* `<targetDir>/.claude/skills/` so Claude Code's built-in Skill tool
|
|
205
|
+
* auto-discovers them alongside any plugin-installed skills.
|
|
206
|
+
*
|
|
207
|
+
* Per-folder symlinks (rather than a single dir-level symlink) so existing
|
|
208
|
+
* `.claude/skills/<other>` entries (Claude Code plugins, user-authored skills)
|
|
209
|
+
* are preserved.
|
|
210
|
+
*
|
|
211
|
+
* Idempotent. Returns the relative paths of symlinks created or already present.
|
|
212
|
+
*/
|
|
213
|
+
function linkSkillsToClaude(targetDir) {
|
|
214
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
215
|
+
if (!fs.existsSync(skillsDir))
|
|
216
|
+
return [];
|
|
217
|
+
const claudeSkillsDir = path.join(targetDir, '.claude', 'skills');
|
|
218
|
+
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
219
|
+
const linked = [];
|
|
220
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
221
|
+
if (!entry.isDirectory())
|
|
222
|
+
continue;
|
|
223
|
+
const linkPath = path.join(claudeSkillsDir, entry.name);
|
|
224
|
+
const target = path.join('..', '..', 'skills', entry.name);
|
|
225
|
+
const rel = path.join('.claude', 'skills', entry.name);
|
|
226
|
+
const existing = fs.lstatSync(linkPath, { throwIfNoEntry: false });
|
|
227
|
+
if (existing) {
|
|
228
|
+
if (existing.isSymbolicLink() && fs.readlinkSync(linkPath) === target) {
|
|
229
|
+
linked.push(rel);
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
fs.symlinkSync(target, linkPath);
|
|
234
|
+
linked.push(rel);
|
|
235
|
+
}
|
|
236
|
+
return linked;
|
|
237
|
+
}
|
|
185
238
|
/**
|
|
186
239
|
* Copy capability files from the capabilities/ source tree (test fixtures only).
|
|
187
240
|
*/
|
|
@@ -198,7 +251,13 @@ function copyFromSourceTree(targetDir, capabilityName) {
|
|
|
198
251
|
(0, safe_io_1.safeCopyDir)(skillsDir, destSkillsDir, { onConflict: 'skip' });
|
|
199
252
|
const entries = fs.readdirSync(skillsDir);
|
|
200
253
|
for (const entry of entries) {
|
|
201
|
-
|
|
254
|
+
const srcEntryPath = path.join(skillsDir, entry);
|
|
255
|
+
if (fs.statSync(srcEntryPath).isDirectory()) {
|
|
256
|
+
const files = walkDir(path.join(destSkillsDir, entry));
|
|
257
|
+
for (const filePath of files) {
|
|
258
|
+
copied.push(path.join('skills', entry, path.relative(path.join(destSkillsDir, entry), filePath)));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
202
261
|
}
|
|
203
262
|
}
|
|
204
263
|
// Scaffold files declared in the capability manifest
|
|
@@ -481,97 +540,3 @@ function collectHooks(capabilities) {
|
|
|
481
540
|
hookPostRoutes: renderPhase('postRoutes'),
|
|
482
541
|
};
|
|
483
542
|
}
|
|
484
|
-
/**
|
|
485
|
-
* Preamble for generated skills/index.md.
|
|
486
|
-
* Edit here to change the header text in every generated project.
|
|
487
|
-
*/
|
|
488
|
-
const SKILLS_INDEX_PREAMBLE = [
|
|
489
|
-
'# About Skills',
|
|
490
|
-
'## Writing new skills',
|
|
491
|
-
'When writing a new skill, create a markdown file in this directory using the pattern `<topic>.skill.md`.',
|
|
492
|
-
'Each skill is a concise set of concrete steps that an AI agent can take to achieve a specific goal or perform a specific task.',
|
|
493
|
-
'Preface the skill with "When you ... " to help the agent understand when to use it.',
|
|
494
|
-
'',
|
|
495
|
-
'## Current skills',
|
|
496
|
-
];
|
|
497
|
-
/**
|
|
498
|
-
* Category display names for skill file prefixes.
|
|
499
|
-
* Unknown prefixes get title-cased automatically.
|
|
500
|
-
*/
|
|
501
|
-
const SKILL_CATEGORIES = {
|
|
502
|
-
code: 'Code',
|
|
503
|
-
design: 'Design',
|
|
504
|
-
product: 'Product',
|
|
505
|
-
proj: 'Project',
|
|
506
|
-
reqs: 'Requirements and Docs',
|
|
507
|
-
skills: 'Skills',
|
|
508
|
-
test: 'Testing',
|
|
509
|
-
web: 'Web',
|
|
510
|
-
world: 'World',
|
|
511
|
-
};
|
|
512
|
-
/**
|
|
513
|
-
* Extract the H1 title and first content line from a skill markdown file.
|
|
514
|
-
*/
|
|
515
|
-
function parseSkillFile(content) {
|
|
516
|
-
const lines = content.split('\n');
|
|
517
|
-
let title = '';
|
|
518
|
-
let description = '';
|
|
519
|
-
for (const line of lines) {
|
|
520
|
-
const trimmed = line.trim();
|
|
521
|
-
if (!title && trimmed.startsWith('# ')) {
|
|
522
|
-
title = trimmed.slice(2).trim();
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
if (title && !description && trimmed.length > 0 && !trimmed.startsWith('#')) {
|
|
526
|
-
description = trimmed;
|
|
527
|
-
break;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
return { title: title || 'Untitled', description };
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Generate skills/index.md from the .skill.md files present in the project.
|
|
534
|
-
* Overwrites any existing index.md.
|
|
535
|
-
*/
|
|
536
|
-
function generateSkillsIndex(targetDir) {
|
|
537
|
-
const skillsDir = path.join(targetDir, 'skills');
|
|
538
|
-
if (!fs.existsSync(skillsDir))
|
|
539
|
-
return;
|
|
540
|
-
const files = fs.readdirSync(skillsDir)
|
|
541
|
-
.filter(f => f.endsWith('.skill.md'))
|
|
542
|
-
.sort();
|
|
543
|
-
if (files.length === 0)
|
|
544
|
-
return;
|
|
545
|
-
const entries = [];
|
|
546
|
-
for (const filename of files) {
|
|
547
|
-
const content = fs.readFileSync(path.join(skillsDir, filename), 'utf-8');
|
|
548
|
-
const { title, description } = parseSkillFile(content);
|
|
549
|
-
const category = filename.split('.')[0];
|
|
550
|
-
entries.push({ filename, title, description, category });
|
|
551
|
-
}
|
|
552
|
-
// Group by category
|
|
553
|
-
const grouped = new Map();
|
|
554
|
-
for (const entry of entries) {
|
|
555
|
-
const list = grouped.get(entry.category) || [];
|
|
556
|
-
list.push(entry);
|
|
557
|
-
grouped.set(entry.category, list);
|
|
558
|
-
}
|
|
559
|
-
// Sort categories by display name
|
|
560
|
-
const sortedCategories = [...grouped.keys()].sort((a, b) => {
|
|
561
|
-
const nameA = SKILL_CATEGORIES[a] || a;
|
|
562
|
-
const nameB = SKILL_CATEGORIES[b] || b;
|
|
563
|
-
return nameA.localeCompare(nameB);
|
|
564
|
-
});
|
|
565
|
-
const lines = [...SKILLS_INDEX_PREAMBLE];
|
|
566
|
-
for (const category of sortedCategories) {
|
|
567
|
-
const displayName = SKILL_CATEGORIES[category]
|
|
568
|
-
|| category.charAt(0).toUpperCase() + category.slice(1);
|
|
569
|
-
lines.push(`### ${displayName}`);
|
|
570
|
-
for (const skill of grouped.get(category)) {
|
|
571
|
-
const desc = skill.description ? ` - ${skill.description}` : '';
|
|
572
|
-
lines.push(`- [**${skill.title}**](${skill.filename})${desc}`);
|
|
573
|
-
}
|
|
574
|
-
lines.push('');
|
|
575
|
-
}
|
|
576
|
-
fs.writeFileSync(path.join(skillsDir, 'index.md'), lines.join('\n'), 'utf-8');
|
|
577
|
-
}
|
package/dist/lib/update.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface FileUpdateAction {
|
|
|
10
10
|
/** Relative path from project root */
|
|
11
11
|
relativePath: string;
|
|
12
12
|
/** What will happen to this file */
|
|
13
|
-
action: 'overwrite' | 'skip' | 'add' | 'deprecate';
|
|
13
|
+
action: 'overwrite' | 'skip' | 'add' | 'deprecate' | 'migrate';
|
|
14
14
|
/** Why this action was chosen */
|
|
15
15
|
reason: string;
|
|
16
16
|
/** Stored hash from config */
|
|
@@ -19,6 +19,8 @@ export interface FileUpdateAction {
|
|
|
19
19
|
currentHash?: string;
|
|
20
20
|
/** New hash from package */
|
|
21
21
|
newHash?: string;
|
|
22
|
+
/** For migrate actions: the old file path being replaced */
|
|
23
|
+
oldPath?: string;
|
|
22
24
|
}
|
|
23
25
|
export interface UpdatePlan {
|
|
24
26
|
capability: string;
|
|
@@ -46,6 +48,8 @@ export interface UpdateResult {
|
|
|
46
48
|
skippedDeleted: string[];
|
|
47
49
|
/** Files removed from disk via --prune */
|
|
48
50
|
pruned: string[];
|
|
51
|
+
/** Files migrated from old flat format to new folder format */
|
|
52
|
+
migrated: string[];
|
|
49
53
|
/** Count of files unchanged in both package and locally */
|
|
50
54
|
unchangedCount: number;
|
|
51
55
|
}
|
package/dist/lib/update.js
CHANGED
|
@@ -92,15 +92,19 @@ function planCapabilityUpdate(targetDir, capabilityName, projectManifest) {
|
|
|
92
92
|
collectFiles(scaffoldDir, scaffoldDir, packageFiles);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
// Collect files from package skills dir
|
|
95
|
+
// Collect files from package skills dir (both flat files and folder-based skills)
|
|
96
96
|
if (boopkit.skillsDir) {
|
|
97
97
|
const skillsDir = path.join(pkgDir, boopkit.skillsDir);
|
|
98
98
|
if (fs.existsSync(skillsDir)) {
|
|
99
99
|
const entries = fs.readdirSync(skillsDir);
|
|
100
100
|
for (const entry of entries) {
|
|
101
101
|
const srcPath = path.join(skillsDir, entry);
|
|
102
|
-
if (fs.statSync(srcPath).
|
|
103
|
-
|
|
102
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
103
|
+
const subFiles = new Map();
|
|
104
|
+
collectFiles(srcPath, srcPath, subFiles);
|
|
105
|
+
for (const [subRel, subAbs] of subFiles) {
|
|
106
|
+
packageFiles.set(path.join('skills', entry, subRel), subAbs);
|
|
107
|
+
}
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
}
|
|
@@ -184,6 +188,53 @@ function planCapabilityUpdate(targetDir, capabilityName, projectManifest) {
|
|
|
184
188
|
});
|
|
185
189
|
}
|
|
186
190
|
}
|
|
191
|
+
// Apply skill migration logic: upgrade deprecate+add pairs to migrate actions
|
|
192
|
+
const capJsonPath = path.join(pkgDir, 'capability.json');
|
|
193
|
+
if (fs.existsSync(capJsonPath)) {
|
|
194
|
+
const capJson = JSON.parse(fs.readFileSync(capJsonPath, 'utf-8'));
|
|
195
|
+
const migrations = capJson.skill_migrations;
|
|
196
|
+
if (migrations) {
|
|
197
|
+
for (const [newSkillName, oldFlatFilename] of Object.entries(migrations)) {
|
|
198
|
+
const oldRelPath = path.join('skills', oldFlatFilename);
|
|
199
|
+
const newPrefix = path.join('skills', newSkillName) + path.sep;
|
|
200
|
+
const oldStoredHash = storedHashes[oldRelPath];
|
|
201
|
+
if (!oldStoredHash)
|
|
202
|
+
continue;
|
|
203
|
+
const oldFilePath = path.join(targetDir, oldRelPath);
|
|
204
|
+
const oldState = compareFileHash(oldFilePath, oldStoredHash);
|
|
205
|
+
if (oldState === 'modified') {
|
|
206
|
+
// User modified the old file — skip new folder files, keep old file
|
|
207
|
+
for (const action of actions) {
|
|
208
|
+
if (action.relativePath.startsWith(newPrefix) || action.relativePath === path.join('skills', newSkillName)) {
|
|
209
|
+
action.action = 'skip';
|
|
210
|
+
action.reason = `Skipped: old skill file '${oldRelPath}' was locally modified`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Change deprecate to skip for the old file
|
|
214
|
+
const oldAction = actions.find(a => a.relativePath === oldRelPath);
|
|
215
|
+
if (oldAction) {
|
|
216
|
+
oldAction.action = 'skip';
|
|
217
|
+
oldAction.reason = 'Locally modified (migration skipped)';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (oldState === 'match') {
|
|
221
|
+
// Old file unmodified — convert deprecate to migrate
|
|
222
|
+
const oldAction = actions.find(a => a.relativePath === oldRelPath);
|
|
223
|
+
if (oldAction) {
|
|
224
|
+
oldAction.action = 'migrate';
|
|
225
|
+
oldAction.reason = `Migrated to folder: skills/${newSkillName}/`;
|
|
226
|
+
}
|
|
227
|
+
// Tag new folder files with the old path they're replacing
|
|
228
|
+
for (const action of actions) {
|
|
229
|
+
if (action.relativePath.startsWith(newPrefix) || action.relativePath === path.join('skills', newSkillName)) {
|
|
230
|
+
action.oldPath = oldRelPath;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// oldState === 'missing': old file already gone, add new files normally
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
187
238
|
const upToDate = actions.length === 0;
|
|
188
239
|
return {
|
|
189
240
|
capability: capabilityName,
|
|
@@ -210,6 +261,7 @@ function executeUpdatePlan(plan, options = {}) {
|
|
|
210
261
|
forcedOverwrite: [],
|
|
211
262
|
skippedDeleted: [],
|
|
212
263
|
pruned: [],
|
|
264
|
+
migrated: [],
|
|
213
265
|
unchangedCount: plan.unchangedCount,
|
|
214
266
|
};
|
|
215
267
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(plan.packageDir, 'package.json'), 'utf-8'));
|
|
@@ -294,6 +346,17 @@ function executeUpdatePlan(plan, options = {}) {
|
|
|
294
346
|
}
|
|
295
347
|
break;
|
|
296
348
|
}
|
|
349
|
+
case 'migrate': {
|
|
350
|
+
const oldFilePath = path.join(plan.targetDir, action.relativePath);
|
|
351
|
+
if (fs.existsSync(oldFilePath)) {
|
|
352
|
+
(0, safe_io_1.createBackup)(oldFilePath);
|
|
353
|
+
fs.unlinkSync(oldFilePath);
|
|
354
|
+
}
|
|
355
|
+
delete newHashes[action.relativePath];
|
|
356
|
+
result.migrated.push(action.relativePath);
|
|
357
|
+
console.warn(`\x1b[33mWARN\x1b[0m Migrated skill: ${action.relativePath} → ${action.reason}`);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
297
360
|
}
|
|
298
361
|
}
|
|
299
362
|
// Update .boop/config.json
|
|
@@ -314,10 +377,10 @@ function executeUpdatePlan(plan, options = {}) {
|
|
|
314
377
|
* Resolve the source path for a relative file path from the package.
|
|
315
378
|
*/
|
|
316
379
|
function resolveSourcePath(pkgDir, boopkit, relativePath) {
|
|
317
|
-
// Skills are stored as skills/<filename> in relPath
|
|
380
|
+
// Skills are stored as skills/<filename> or skills/<folder>/<file> in relPath
|
|
318
381
|
if (relativePath.startsWith('skills/') && boopkit.skillsDir) {
|
|
319
|
-
const
|
|
320
|
-
const srcPath = path.join(pkgDir, boopkit.skillsDir,
|
|
382
|
+
const subPath = relativePath.slice('skills/'.length);
|
|
383
|
+
const srcPath = path.join(pkgDir, boopkit.skillsDir, subPath);
|
|
321
384
|
return fs.existsSync(srcPath) ? srcPath : null;
|
|
322
385
|
}
|
|
323
386
|
// Scaffold files
|
|
@@ -348,6 +411,9 @@ function collectFiles(baseDir, dir, result) {
|
|
|
348
411
|
*/
|
|
349
412
|
function formatUpdateSummary(result) {
|
|
350
413
|
const parts = [];
|
|
414
|
+
if (result.migrated && result.migrated.length > 0) {
|
|
415
|
+
parts.push(`migrated ${result.migrated.length} skill${result.migrated.length === 1 ? '' : 's'}`);
|
|
416
|
+
}
|
|
351
417
|
const normalOverwrites = result.overwritten.length - result.forcedOverwrite.length;
|
|
352
418
|
if (normalOverwrites > 0) {
|
|
353
419
|
parts.push(`Updated ${normalOverwrites} file${normalOverwrites === 1 ? '' : 's'}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boopkit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "boop: the opinionated bootstrap for founder friendly, AI-first web apps - code plus agent skills, composable from day one.",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"workspaces": [
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc && node tools/generate-version-manifest.js",
|
|
22
22
|
"build:caps": "./tools/build-caps.sh",
|
|
23
|
-
"publish"
|
|
24
|
-
"release"
|
|
23
|
+
"publish": "./tools/publish-caps.sh",
|
|
24
|
+
"release": "./tools/publish-caps.sh --release",
|
|
25
25
|
"dev": "tsx src/cli.ts",
|
|
26
26
|
"start": "node dist/cli.js",
|
|
27
27
|
"test": "jest"
|
|
@@ -9,8 +9,7 @@ Don't confirm a hypothesis (or take action) without observation - logs, tests, a
|
|
|
9
9
|
|
|
10
10
|
## How to work in this project
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
1. **Check skills first.** Before coding, reviewing, testing, designing, debugging, filing issues, or proposing any non-trivial change, check [skills/index.md](skills/index.md) for a matching skill and follow its process. Do not rely on general knowledge when a project skill exists.
|
|
12
|
+
1. **Prefer project skills over general knowledge.** When a skill matches your task — coding, reviewing, testing, designing, debugging, filing issues — follow its `SKILL.md` instead of working from memory. Skills live in `skills/` (each is a folder with a `SKILL.md`) and are also exposed at `.claude/skills/` for tooling that auto-discovers there. If your tooling does not auto-surface skills, list `skills/` and read each `SKILL.md`'s frontmatter (`name`, `description`) to find a match.
|
|
14
13
|
2. **Use boop for capabilities.** Run `boop caps` to see what's available. To add features like database, auth, or other services and infrastructure, run `boop add <capability>` — don't install or wire things manually.
|
|
15
14
|
3. **Check project status.** If the system isn't working or new capabilities need to be added run `boop status` to see what's configured and what needs attention.
|
|
16
15
|
4. **Run `boop --help`** for the full command list.
|
|
@@ -21,7 +20,7 @@ Read [reference/boop-guide.ref.md](reference/boop-guide.ref.md) to understand ho
|
|
|
21
20
|
|
|
22
21
|
## Project layout
|
|
23
22
|
- `src/` — source code
|
|
24
|
-
- `skills/` — process skills for agents (
|
|
23
|
+
- `skills/` — process skills for agents (each skill is a folder with a `SKILL.md`); also linked from `.claude/skills/` for Claude Code's built-in skill discovery
|
|
25
24
|
- `reference/` — architecture and coding standards ("what exists now")
|
|
26
25
|
- `specs/` — design docs and specifications ("future state")
|
|
27
26
|
- `tests/` — `npm test` (Jest)
|