@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 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** — `.skill.md` files that teach AI agents the conventions this capability uses
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 `.skill.md` files into `skills/`. These are focused, task-specific instructions (e.g. `design.rest-api-design.skill.md`, `test.tdd-light.skill.md`) that tell an agent the conventions this project uses.
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.1",
3
- "@boopkit/cap-background-jobs": "0.3.1",
4
- "@boopkit/cap-cloud-storage": "0.3.1",
5
- "@boopkit/cap-coding": "0.3.1",
6
- "@boopkit/cap-database": "0.3.1",
7
- "@boopkit/cap-email": "0.3.1",
8
- "@boopkit/cap-hello": "0.3.1",
9
- "@boopkit/cap-noticeboard": "0.3.1",
10
- "@boopkit/cap-product-design": "0.3.1",
11
- "@boopkit/cap-project-mgmt": "0.3.1",
12
- "@boopkit/cap-services": "0.3.1",
13
- "@boopkit/cap-skills": "0.3.1",
14
- "@boopkit/cap-testing": "0.3.1",
15
- "@boopkit/cap-users": "0.3.1",
16
- "@boopkit/cap-web": "0.3.1",
17
- "@boopkit/logger": "0.3.1"
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
  }
@@ -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 = [];
@@ -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/index.md before making changes
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 file not found: ${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 && flow) {
600
+ if (dryRun && work && resolvedFlow) {
590
601
  const workContent = fs.readFileSync(work, 'utf8');
591
- const flowContent = fs.readFileSync(flow, 'utf8');
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 && flow) {
614
+ if (work && resolvedFlow) {
604
615
  const planResult = await planWork({
605
616
  workPath: work,
606
- flowPath: flow,
617
+ flowPath: resolvedFlow,
607
618
  prefix,
608
619
  budget,
609
620
  model,
@@ -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[90m⚠\x1b[0m';
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)`);
@@ -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);
@@ -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' | 'generate-skills-index' | 'update-manifest' | 'run-command';
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;
@@ -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',
@@ -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 {};
@@ -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 destPath = path.join(destSkillsDir, entry);
157
- copied.push(path.join('skills', entry));
158
- if (fs.existsSync(destPath)) {
159
- fileHashes[path.join('skills', entry)] = hashFile(destPath);
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
- copied.push(path.join('skills', entry));
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
- }
@@ -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
  }
@@ -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).isFile()) {
103
- packageFiles.set(path.join('skills', entry), srcPath);
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 filename = relativePath.slice('skills/'.length);
320
- const srcPath = path.join(pkgDir, boopkit.skillsDir, filename);
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.2",
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" : "./tools/publish-caps.sh",
24
- "release" : "./tools/publish-caps.sh --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
- 0. **Read the skills index now.** Open [skills/index.md](skills/index.md) before doing anything else in this session. You need to know what skills exist before you can apply rule 1.
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 (read `skills/index.md`)
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)