@girardmedia/bootspring 1.1.0

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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +255 -0
  3. package/agents/README.md +93 -0
  4. package/agents/api-expert/context.md +416 -0
  5. package/agents/architecture-expert/context.md +454 -0
  6. package/agents/backend-expert/context.md +483 -0
  7. package/agents/code-review-expert/context.md +365 -0
  8. package/agents/database-expert/context.md +250 -0
  9. package/agents/devops-expert/context.md +446 -0
  10. package/agents/frontend-expert/context.md +364 -0
  11. package/agents/index.js +140 -0
  12. package/agents/performance-expert/context.md +377 -0
  13. package/agents/security-expert/context.md +343 -0
  14. package/agents/testing-expert/context.md +414 -0
  15. package/agents/ui-ux-expert/context.md +448 -0
  16. package/agents/vercel-expert/context.md +426 -0
  17. package/bin/bootspring.js +310 -0
  18. package/cli/agent.js +337 -0
  19. package/cli/context.js +194 -0
  20. package/cli/dashboard.js +150 -0
  21. package/cli/generate.js +294 -0
  22. package/cli/init.js +410 -0
  23. package/cli/loop.js +421 -0
  24. package/cli/mcp.js +241 -0
  25. package/cli/memory.js +303 -0
  26. package/cli/orchestrator.js +400 -0
  27. package/cli/plugin.js +451 -0
  28. package/cli/quality.js +332 -0
  29. package/cli/skill.js +369 -0
  30. package/cli/task.js +628 -0
  31. package/cli/telemetry.js +114 -0
  32. package/cli/todo.js +614 -0
  33. package/cli/update.js +312 -0
  34. package/core/config.js +245 -0
  35. package/core/context.js +329 -0
  36. package/core/entitlements.js +209 -0
  37. package/core/index.js +43 -0
  38. package/core/policies.js +68 -0
  39. package/core/telemetry.js +247 -0
  40. package/core/utils.js +380 -0
  41. package/dashboard/server.js +818 -0
  42. package/docs/integrations/claude-code.md +42 -0
  43. package/docs/integrations/codex.md +42 -0
  44. package/docs/mcp-api-platform.md +102 -0
  45. package/generators/generate.js +598 -0
  46. package/generators/index.js +18 -0
  47. package/hooks/context-detector.js +177 -0
  48. package/hooks/index.js +35 -0
  49. package/hooks/prompt-enhancer.js +289 -0
  50. package/intelligence/git-memory.js +551 -0
  51. package/intelligence/index.js +59 -0
  52. package/intelligence/orchestrator.js +964 -0
  53. package/intelligence/prd.js +447 -0
  54. package/intelligence/recommendation-weights.json +18 -0
  55. package/intelligence/recommendations.js +234 -0
  56. package/mcp/capabilities.js +71 -0
  57. package/mcp/contracts/mcp-contract.v1.json +497 -0
  58. package/mcp/registry.js +213 -0
  59. package/mcp/response-formatter.js +462 -0
  60. package/mcp/server.js +99 -0
  61. package/mcp/tools/agent-tool.js +137 -0
  62. package/mcp/tools/capabilities-tool.js +54 -0
  63. package/mcp/tools/context-tool.js +49 -0
  64. package/mcp/tools/dashboard-tool.js +58 -0
  65. package/mcp/tools/generate-tool.js +46 -0
  66. package/mcp/tools/loop-tool.js +134 -0
  67. package/mcp/tools/memory-tool.js +180 -0
  68. package/mcp/tools/orchestrator-tool.js +232 -0
  69. package/mcp/tools/plugin-tool.js +76 -0
  70. package/mcp/tools/quality-tool.js +47 -0
  71. package/mcp/tools/skill-tool.js +233 -0
  72. package/mcp/tools/telemetry-tool.js +95 -0
  73. package/mcp/tools/todo-tool.js +133 -0
  74. package/package.json +98 -0
  75. package/plugins/index.js +141 -0
  76. package/quality/index.js +380 -0
  77. package/quality/lint-budgets.json +19 -0
  78. package/skills/index.js +787 -0
  79. package/skills/patterns/README.md +163 -0
  80. package/skills/patterns/api/route-handler.md +217 -0
  81. package/skills/patterns/api/server-action.md +249 -0
  82. package/skills/patterns/auth/clerk.md +132 -0
  83. package/skills/patterns/database/prisma.md +180 -0
  84. package/skills/patterns/payments/stripe.md +272 -0
  85. package/skills/patterns/security/validation.md +268 -0
  86. package/skills/patterns/testing/vitest.md +307 -0
  87. package/templates/bootspring.config.js +83 -0
  88. package/templates/mcp.json +9 -0
package/cli/quality.js ADDED
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Bootspring Quality Command
3
+ * Run quality gates and checks
4
+ *
5
+ * @package bootspring
6
+ * @command quality
7
+ */
8
+
9
+ const { execSync, spawn } = require('child_process');
10
+ const path = require('path');
11
+ const config = require('../core/config');
12
+ const utils = require('../core/utils');
13
+
14
+ /**
15
+ * Quality gate definitions
16
+ */
17
+ const GATES = {
18
+ 'pre-commit': {
19
+ name: 'Pre-Commit',
20
+ description: 'Quick checks before committing code',
21
+ checks: [
22
+ {
23
+ name: 'TypeScript',
24
+ command: 'npx tsc --noEmit',
25
+ description: 'Type checking'
26
+ },
27
+ {
28
+ name: 'ESLint',
29
+ command: 'npx eslint . --ext .ts,.tsx,.js,.jsx --max-warnings 0',
30
+ description: 'Linting'
31
+ },
32
+ {
33
+ name: 'Prettier',
34
+ command: 'npx prettier --check .',
35
+ description: 'Format checking'
36
+ }
37
+ ]
38
+ },
39
+ 'pre-push': {
40
+ name: 'Pre-Push',
41
+ description: 'Thorough checks before pushing to remote',
42
+ checks: [
43
+ {
44
+ name: 'TypeScript',
45
+ command: 'npx tsc --noEmit',
46
+ description: 'Type checking'
47
+ },
48
+ {
49
+ name: 'ESLint',
50
+ command: 'npx eslint . --ext .ts,.tsx,.js,.jsx',
51
+ description: 'Linting'
52
+ },
53
+ {
54
+ name: 'Unit Tests',
55
+ command: 'npm test',
56
+ description: 'Running tests'
57
+ },
58
+ {
59
+ name: 'Build',
60
+ command: 'npm run build',
61
+ description: 'Production build'
62
+ }
63
+ ]
64
+ },
65
+ 'pre-deploy': {
66
+ name: 'Pre-Deploy',
67
+ description: 'Full audit before deployment',
68
+ checks: [
69
+ {
70
+ name: 'TypeScript',
71
+ command: 'npx tsc --noEmit',
72
+ description: 'Type checking'
73
+ },
74
+ {
75
+ name: 'ESLint',
76
+ command: 'npx eslint . --ext .ts,.tsx,.js,.jsx',
77
+ description: 'Linting'
78
+ },
79
+ {
80
+ name: 'Unit Tests',
81
+ command: 'npm test',
82
+ description: 'Running tests'
83
+ },
84
+ {
85
+ name: 'Build',
86
+ command: 'npm run build',
87
+ description: 'Production build'
88
+ },
89
+ {
90
+ name: 'Security Audit',
91
+ command: 'npm audit --audit-level=high',
92
+ description: 'Checking for vulnerabilities'
93
+ }
94
+ ]
95
+ }
96
+ };
97
+
98
+ /**
99
+ * Run a single check
100
+ * @param {object} check - Check definition
101
+ * @param {string} projectRoot - Project root path
102
+ * @returns {object} Check result
103
+ */
104
+ function runCheck(check, projectRoot) {
105
+ const spinner = utils.createSpinner(`${check.name}: ${check.description}`).start();
106
+
107
+ try {
108
+ execSync(check.command, {
109
+ cwd: projectRoot,
110
+ stdio: 'pipe',
111
+ encoding: 'utf-8',
112
+ timeout: 300000 // 5 minute timeout
113
+ });
114
+
115
+ spinner.succeed(`${check.name}: passed`);
116
+ return { name: check.name, status: 'pass', message: 'Passed' };
117
+ } catch (error) {
118
+ const output = error.stdout || error.stderr || error.message;
119
+ spinner.fail(`${check.name}: failed`);
120
+
121
+ // Show truncated output
122
+ if (output) {
123
+ const lines = output.split('\n').slice(0, 10);
124
+ console.log(`${utils.COLORS.dim}${lines.join('\n')}${utils.COLORS.reset}`);
125
+ if (output.split('\n').length > 10) {
126
+ console.log(`${utils.COLORS.dim}... (truncated)${utils.COLORS.reset}`);
127
+ }
128
+ }
129
+
130
+ return { name: check.name, status: 'fail', message: output.slice(0, 500) };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Run a quality gate
136
+ * @param {string} gateName - Gate name
137
+ * @param {object} options - Options
138
+ */
139
+ async function runGate(gateName, options = {}) {
140
+ const gate = GATES[gateName];
141
+
142
+ if (!gate) {
143
+ utils.print.error(`Unknown gate: ${gateName}`);
144
+ utils.print.dim(`Available gates: ${Object.keys(GATES).join(', ')}`);
145
+ return;
146
+ }
147
+
148
+ const cfg = config.load();
149
+ const projectRoot = cfg._projectRoot;
150
+
151
+ console.log(`
152
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ ${gate.name} Quality Gate${utils.COLORS.reset}
153
+ ${utils.COLORS.dim}${gate.description}${utils.COLORS.reset}
154
+ `);
155
+
156
+ const results = [];
157
+ let passed = 0;
158
+ let failed = 0;
159
+
160
+ for (const check of gate.checks) {
161
+ if (options.skip && options.skip.includes(check.name.toLowerCase())) {
162
+ console.log(`${utils.COLORS.yellow}⊘${utils.COLORS.reset} ${check.name}: skipped`);
163
+ results.push({ name: check.name, status: 'skip', message: 'Skipped' });
164
+ continue;
165
+ }
166
+
167
+ const result = runCheck(check, projectRoot);
168
+ results.push(result);
169
+
170
+ if (result.status === 'pass') {
171
+ passed++;
172
+ } else {
173
+ failed++;
174
+ }
175
+
176
+ // Stop on first failure if strict mode
177
+ if (options.strict && result.status === 'fail') {
178
+ console.log(`\n${utils.COLORS.red}Stopping on first failure (strict mode)${utils.COLORS.reset}`);
179
+ break;
180
+ }
181
+ }
182
+
183
+ // Summary
184
+ console.log();
185
+ if (failed === 0) {
186
+ console.log(`${utils.COLORS.green}${utils.COLORS.bold}✓ All checks passed!${utils.COLORS.reset}`);
187
+ console.log(`${utils.COLORS.dim}${passed} check(s) passed${utils.COLORS.reset}`);
188
+ } else {
189
+ console.log(`${utils.COLORS.red}${utils.COLORS.bold}✗ Quality gate failed${utils.COLORS.reset}`);
190
+ console.log(`${utils.COLORS.dim}${passed} passed, ${failed} failed${utils.COLORS.reset}`);
191
+ process.exitCode = 1;
192
+ }
193
+
194
+ return { gate: gateName, passed, failed, results };
195
+ }
196
+
197
+ /**
198
+ * Run quick lint check
199
+ */
200
+ function runQuickCheck() {
201
+ const cfg = config.load();
202
+ const projectRoot = cfg._projectRoot;
203
+
204
+ console.log(`
205
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Quick Quality Check${utils.COLORS.reset}
206
+ `);
207
+
208
+ // Just run TypeScript check
209
+ const spinner = utils.createSpinner('Type checking...').start();
210
+
211
+ try {
212
+ execSync('npx tsc --noEmit', {
213
+ cwd: projectRoot,
214
+ stdio: 'pipe',
215
+ timeout: 60000
216
+ });
217
+ spinner.succeed('No type errors');
218
+ } catch (error) {
219
+ spinner.fail('Type errors found');
220
+ console.log(`${utils.COLORS.dim}Run 'npx tsc --noEmit' for details${utils.COLORS.reset}`);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Show quality status
226
+ */
227
+ function showStatus() {
228
+ const cfg = config.load();
229
+
230
+ console.log(`
231
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Quality Gate Configuration${utils.COLORS.reset}
232
+ ${utils.COLORS.dim}Project: ${cfg._projectRoot}${utils.COLORS.reset}
233
+
234
+ ${utils.COLORS.bold}Available Gates${utils.COLORS.reset}
235
+ `);
236
+
237
+ for (const [id, gate] of Object.entries(GATES)) {
238
+ console.log(` ${utils.COLORS.cyan}${id}${utils.COLORS.reset}`);
239
+ console.log(` ${gate.description}`);
240
+ console.log(` ${utils.COLORS.dim}Checks: ${gate.checks.map(c => c.name).join(', ')}${utils.COLORS.reset}`);
241
+ console.log();
242
+ }
243
+
244
+ const userConfig = cfg.quality || {};
245
+ console.log(`${utils.COLORS.bold}Project Settings${utils.COLORS.reset}`);
246
+ console.log(` Pre-commit: ${userConfig.preCommit !== false ? 'enabled' : 'disabled'}`);
247
+ console.log(` Pre-push: ${userConfig.prePush ? 'enabled' : 'disabled'}`);
248
+ console.log(` Strict mode: ${userConfig.strictMode ? 'enabled' : 'disabled'}`);
249
+ console.log();
250
+
251
+ console.log(`${utils.COLORS.bold}Git Hooks${utils.COLORS.reset}`);
252
+ console.log(` ${utils.COLORS.dim}To enable git hooks, use husky or lefthook:${utils.COLORS.reset}`);
253
+ console.log(` ${utils.COLORS.dim}npx husky add .husky/pre-commit "bootspring quality pre-commit"${utils.COLORS.reset}`);
254
+ }
255
+
256
+ /**
257
+ * Show quality help
258
+ */
259
+ function showHelp() {
260
+ console.log(`
261
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Quality${utils.COLORS.reset}
262
+ ${utils.COLORS.dim}Run quality gates and checks${utils.COLORS.reset}
263
+
264
+ ${utils.COLORS.bold}Usage:${utils.COLORS.reset}
265
+ bootspring quality <gate> [options]
266
+
267
+ ${utils.COLORS.bold}Gates:${utils.COLORS.reset}
268
+ ${utils.COLORS.cyan}pre-commit${utils.COLORS.reset} Quick checks (types, lint, format)
269
+ ${utils.COLORS.cyan}pre-push${utils.COLORS.reset} Thorough checks (tests, build)
270
+ ${utils.COLORS.cyan}pre-deploy${utils.COLORS.reset} Full audit (security, everything)
271
+
272
+ ${utils.COLORS.bold}Options:${utils.COLORS.reset}
273
+ --strict Stop on first failure
274
+ --skip <check> Skip specific check
275
+
276
+ ${utils.COLORS.bold}Commands:${utils.COLORS.reset}
277
+ ${utils.COLORS.cyan}quick${utils.COLORS.reset} Run quick type check only
278
+ ${utils.COLORS.cyan}status${utils.COLORS.reset} Show quality configuration
279
+
280
+ ${utils.COLORS.bold}Examples:${utils.COLORS.reset}
281
+ bootspring quality pre-commit
282
+ bootspring quality pre-push --strict
283
+ bootspring quality pre-deploy --skip security
284
+ bootspring quality quick
285
+ bootspring quality status
286
+ `);
287
+ }
288
+
289
+ /**
290
+ * Run quality command
291
+ */
292
+ async function run(args) {
293
+ const parsedArgs = utils.parseArgs(args);
294
+ const subcommand = parsedArgs._[0];
295
+
296
+ if (!subcommand) {
297
+ showHelp();
298
+ return;
299
+ }
300
+
301
+ switch (subcommand) {
302
+ case 'pre-commit':
303
+ case 'pre-push':
304
+ case 'pre-deploy':
305
+ await runGate(subcommand, {
306
+ strict: parsedArgs.strict,
307
+ skip: parsedArgs.skip ? [parsedArgs.skip] : []
308
+ });
309
+ break;
310
+
311
+ case 'quick':
312
+ case 'check':
313
+ runQuickCheck();
314
+ break;
315
+
316
+ case 'status':
317
+ showStatus();
318
+ break;
319
+
320
+ case 'help':
321
+ case '-h':
322
+ case '--help':
323
+ showHelp();
324
+ break;
325
+
326
+ default:
327
+ utils.print.error(`Unknown gate: ${subcommand}`);
328
+ showHelp();
329
+ }
330
+ }
331
+
332
+ module.exports = { run, GATES, runGate, runCheck };
package/cli/skill.js ADDED
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Bootspring Skill Command
3
+ * Browse and use skill patterns.
4
+ */
5
+
6
+ const utils = require('../core/utils');
7
+ const entitlements = require('../core/entitlements');
8
+ const telemetry = require('../core/telemetry');
9
+ const skills = require('../skills');
10
+
11
+ function trackTelemetry(event, payload) {
12
+ try {
13
+ telemetry.emitEvent(event, payload);
14
+ } catch {
15
+ // Do not block CLI flow on telemetry issues.
16
+ }
17
+ }
18
+
19
+ function printBuiltInSkills() {
20
+ const categories = skills.getCategories();
21
+ for (const category of categories) {
22
+ const patterns = skills.getPatternsByCategory(category);
23
+ if (patterns.length === 0) continue;
24
+
25
+ console.log(`${utils.COLORS.bold}${category}${utils.COLORS.reset}`);
26
+ for (const pattern of patterns) {
27
+ const id = `${category}/${pattern}`;
28
+ const metadata = skills.getSkillMetadata(id);
29
+ const description = metadata?.description ? ` - ${metadata.description}` : '';
30
+ console.log(` ${utils.COLORS.cyan}${id}${utils.COLORS.reset}${description}`);
31
+ }
32
+ console.log('');
33
+ }
34
+ }
35
+
36
+ function printExternalSkills(options = {}) {
37
+ const {
38
+ limit = 20,
39
+ accessOptions = {}
40
+ } = options;
41
+
42
+ if (!skills.hasExternalSkillLibrary()) {
43
+ console.log(`${utils.COLORS.dim}No external skills catalog found${utils.COLORS.reset}\n`);
44
+ return;
45
+ }
46
+
47
+ const externalIds = skills.listExternalSkills();
48
+ const { allowed: accessibleIds, denied } = entitlements.filterAccessibleSkills(externalIds, accessOptions);
49
+ const shown = accessibleIds.slice(0, limit);
50
+
51
+ if (shown.length === 0) {
52
+ console.log(`${utils.COLORS.bold}external${utils.COLORS.reset}`);
53
+ console.log(` ${utils.COLORS.dim}No external skills available for current entitlement${utils.COLORS.reset}\n`);
54
+ if (denied.length > 0) {
55
+ utils.print.dim(denied[0].reason);
56
+ }
57
+ return;
58
+ }
59
+
60
+ console.log(`${utils.COLORS.bold}external${utils.COLORS.reset}`);
61
+ for (const id of shown) {
62
+ const metadata = skills.getSkillMetadata(id);
63
+ const description = metadata?.description ? ` - ${metadata.description}` : '';
64
+ console.log(` ${utils.COLORS.yellow}${id}${utils.COLORS.reset}${description}`);
65
+ }
66
+
67
+ if (accessibleIds.length > shown.length) {
68
+ const remaining = accessibleIds.length - shown.length;
69
+ console.log(`\n${utils.COLORS.dim}${remaining} more external skills hidden.${utils.COLORS.reset}`);
70
+ console.log(`${utils.COLORS.dim}Use --limit <n> or search with --external.${utils.COLORS.reset}`);
71
+ }
72
+ if (denied.length > 0) {
73
+ console.log(`\n${utils.COLORS.dim}${denied.length} external skills locked by entitlement policy${utils.COLORS.reset}`);
74
+ }
75
+ console.log('');
76
+ }
77
+
78
+ function listSkills(options = {}) {
79
+ const {
80
+ includeExternal = false,
81
+ externalOnly = false,
82
+ limit = 20,
83
+ accessOptions = {}
84
+ } = options;
85
+
86
+ console.log(`
87
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Skills${utils.COLORS.reset}
88
+ ${utils.COLORS.dim}Built-in patterns + optional external skill catalog${utils.COLORS.reset}
89
+ `);
90
+
91
+ if (!externalOnly) {
92
+ printBuiltInSkills();
93
+ }
94
+
95
+ if (includeExternal || externalOnly) {
96
+ printExternalSkills({ limit, accessOptions });
97
+ }
98
+
99
+ const builtInCount = skills.listSkills().length;
100
+ const externalCount = skills.hasExternalSkillLibrary() ? skills.listExternalSkills().length : 0;
101
+ console.log(`${utils.COLORS.dim}${builtInCount} built-in skills available${utils.COLORS.reset}`);
102
+ if (skills.hasExternalSkillLibrary()) {
103
+ console.log(`${utils.COLORS.dim}${externalCount} external skills indexed${utils.COLORS.reset}`);
104
+ }
105
+ console.log(`${utils.COLORS.dim}Use "bootspring skill show <id>" to view a skill${utils.COLORS.reset}`);
106
+ }
107
+
108
+ function showSkill(skillId, options = {}) {
109
+ const {
110
+ includeExternal = false,
111
+ summary = false,
112
+ sections,
113
+ maxChars,
114
+ accessOptions = {}
115
+ } = options;
116
+
117
+ const metadata = skills.getSkillMetadata(skillId);
118
+ const resolvedSkillId = metadata?.id || skillId;
119
+ const access = entitlements.checkSkillAccess(resolvedSkillId, accessOptions);
120
+
121
+ if (!access.allowed) {
122
+ trackTelemetry('premium_prompted', {
123
+ capability: 'external_skill',
124
+ skillId: resolvedSkillId,
125
+ mode: access.context?.mode,
126
+ tier: access.context?.tier,
127
+ reason: access.code
128
+ });
129
+ utils.print.error(access.reason);
130
+ return;
131
+ }
132
+
133
+ const rawContent = skills.loadSkill(skillId, { includeExternal });
134
+
135
+ if (!rawContent) {
136
+ utils.print.error(`Skill not found: ${skillId}`);
137
+ utils.print.dim('Try: bootspring skill list');
138
+ utils.print.dim('Or: bootspring skill search <query> --external');
139
+ return;
140
+ }
141
+
142
+ const content = skills.formatSkillContent(rawContent, {
143
+ summary,
144
+ sections,
145
+ maxChars
146
+ });
147
+
148
+ const headerName = metadata?.name || skillId;
149
+ const source = metadata?.source || 'built-in';
150
+ if (String(source).startsWith('external')) {
151
+ trackTelemetry('premium_unlocked', {
152
+ capability: 'external_skill',
153
+ skillId: metadata?.id || skillId,
154
+ mode: access.context?.mode,
155
+ tier: access.context?.tier,
156
+ reason: access.code
157
+ });
158
+ }
159
+
160
+ console.log(`
161
+ ${utils.COLORS.cyan}${utils.COLORS.bold}${headerName}${utils.COLORS.reset}
162
+ ${utils.COLORS.dim}ID: ${metadata?.id || skillId} | Source: ${source}${utils.COLORS.reset}
163
+ `);
164
+
165
+ console.log(content);
166
+ }
167
+
168
+ function searchSkills(query, options = {}) {
169
+ const {
170
+ includeExternal = false,
171
+ externalOnly = false,
172
+ limit = 20,
173
+ accessOptions = {}
174
+ } = options;
175
+
176
+ let results = skills.searchSkills(query, {
177
+ includeExternal: includeExternal || externalOnly,
178
+ limit
179
+ });
180
+
181
+ if (externalOnly) {
182
+ results = results.filter(id => id.startsWith('external/'));
183
+ }
184
+ const filtered = entitlements.filterAccessibleSkills(results, accessOptions);
185
+ results = filtered.allowed;
186
+
187
+ console.log(`
188
+ ${utils.COLORS.cyan}${utils.COLORS.bold}Search Results${utils.COLORS.reset}
189
+ ${utils.COLORS.dim}Query: "${query}"${utils.COLORS.reset}
190
+ `);
191
+
192
+ if (results.length === 0) {
193
+ utils.print.warning('No matching skills found');
194
+ utils.print.dim('Try: bootspring skill list');
195
+ return;
196
+ }
197
+
198
+ for (const id of results) {
199
+ const metadata = skills.getSkillMetadata(id);
200
+ const source = String(metadata?.source || '').startsWith('external') ? 'external' : 'built-in';
201
+ const description = metadata?.description ? ` - ${metadata.description}` : '';
202
+ const color = source === 'external' ? utils.COLORS.yellow : utils.COLORS.cyan;
203
+ console.log(` ${color}${id}${utils.COLORS.reset} ${utils.COLORS.dim}[${source}]${utils.COLORS.reset}${description}`);
204
+ }
205
+
206
+ console.log(`\n${utils.COLORS.dim}${results.length} match(es)${utils.COLORS.reset}`);
207
+ if (filtered.denied.length > 0) {
208
+ console.log(`${utils.COLORS.dim}${filtered.denied.length} result(s) hidden by entitlement policy${utils.COLORS.reset}`);
209
+ }
210
+ }
211
+
212
+ async function syncCatalog(options = {}) {
213
+ const {
214
+ manifestUrl,
215
+ contentBaseUrl,
216
+ token,
217
+ timeoutMs,
218
+ cacheDir,
219
+ manifestPublicKey,
220
+ requireManifestSignature
221
+ } = options;
222
+
223
+ const spinner = utils.createSpinner('Syncing external skill catalog').start();
224
+ try {
225
+ const result = await skills.syncExternalCatalog({
226
+ manifestUrl,
227
+ contentBaseUrl,
228
+ token,
229
+ timeoutMs,
230
+ cacheDir,
231
+ manifestPublicKey,
232
+ requireManifestSignature
233
+ });
234
+ spinner.succeed(`Synced ${result.fetched} external skills`);
235
+ if (result.skipped.length > 0) {
236
+ utils.print.warning(`Skipped ${result.skipped.length} skill(s): ${result.skipped.join(', ')}`);
237
+ }
238
+ utils.print.dim(`Cache: ${result.manifestPath}`);
239
+ } catch (error) {
240
+ spinner.fail(`Sync failed: ${error.message}`);
241
+ }
242
+ }
243
+
244
+ function showHelp() {
245
+ console.log(`
246
+ ${utils.COLORS.cyan}${utils.COLORS.bold}Bootspring Skill Command${utils.COLORS.reset}
247
+
248
+ ${utils.COLORS.cyan}Usage:${utils.COLORS.reset}
249
+ bootspring skill <command> [args] [options]
250
+
251
+ ${utils.COLORS.cyan}Commands:${utils.COLORS.reset}
252
+ ${utils.COLORS.cyan}list${utils.COLORS.reset} List built-in skills
253
+ ${utils.COLORS.cyan}show${utils.COLORS.reset} <id> Show skill content
254
+ ${utils.COLORS.cyan}search${utils.COLORS.reset} <query> Search skills
255
+ ${utils.COLORS.cyan}sync${utils.COLORS.reset} Sync external catalog from remote manifest
256
+
257
+ ${utils.COLORS.cyan}Options:${utils.COLORS.reset}
258
+ ${utils.COLORS.cyan}--external${utils.COLORS.reset} Include external skills catalog
259
+ ${utils.COLORS.cyan}--external-only${utils.COLORS.reset} External skills only
260
+ ${utils.COLORS.cyan}--limit <n>${utils.COLORS.reset} Limit results (default: 20)
261
+ ${utils.COLORS.cyan}--summary${utils.COLORS.reset} Show concise summary for skill show
262
+ ${utils.COLORS.cyan}--sections a,b${utils.COLORS.reset} Show matching sections for skill show
263
+ ${utils.COLORS.cyan}--max-chars <n>${utils.COLORS.reset} Cap output size for skill show
264
+ ${utils.COLORS.cyan}--access-mode <mode>${utils.COLORS.reset} Access mode: local (default) or server
265
+ ${utils.COLORS.cyan}--entitled <bool>${utils.COLORS.reset} Entitlement override for external skills
266
+ ${utils.COLORS.cyan}--tier <tier>${utils.COLORS.reset} Tier override: free, pro, team, enterprise
267
+ ${utils.COLORS.cyan}--manifest-url <url>${utils.COLORS.reset} Remote manifest URL for skill sync
268
+ ${utils.COLORS.cyan}--content-base-url <url>${utils.COLORS.reset} Base URL for SKILL.md files when missing contentUrl
269
+ ${utils.COLORS.cyan}--token <value>${utils.COLORS.reset} Bearer token for remote catalog APIs
270
+ ${utils.COLORS.cyan}--cache-dir <path>${utils.COLORS.reset} Override local skill cache directory
271
+ ${utils.COLORS.cyan}--manifest-public-key <pem>${utils.COLORS.reset} Public key for manifest signature verification
272
+ ${utils.COLORS.cyan}--require-signature <bool>${utils.COLORS.reset} Require remote manifest signature validation
273
+
274
+ ${utils.COLORS.cyan}Examples:${utils.COLORS.reset}
275
+ bootspring skill list
276
+ bootspring skill list --external
277
+ bootspring skill search auth
278
+ bootspring skill search vercel --external
279
+ bootspring skill show auth/clerk
280
+ bootspring skill show external/vercel-automation
281
+ bootspring skill sync --manifest-url https://api.bootspring.com/skills/manifest.json
282
+ `);
283
+ }
284
+
285
+ /**
286
+ * Run skill command
287
+ */
288
+ async function run(args) {
289
+ const parsedArgs = utils.parseArgs(args);
290
+ const subcommand = parsedArgs._[0] || 'list';
291
+ const subargs = parsedArgs._.slice(1);
292
+
293
+ const externalOnly = Boolean(parsedArgs['external-only']);
294
+ const includeExternal = externalOnly || Boolean(parsedArgs.external);
295
+ const parsedLimit = parsedArgs.limit ? Number(parsedArgs.limit) : 20;
296
+ const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 20;
297
+ const maxChars = parsedArgs['max-chars'] ? Number(parsedArgs['max-chars']) : undefined;
298
+ const accessOptions = {
299
+ mode: parsedArgs['access-mode'],
300
+ entitled: parsedArgs.entitled,
301
+ tier: parsedArgs.tier
302
+ };
303
+
304
+ switch (subcommand) {
305
+ case 'list':
306
+ listSkills({ includeExternal, externalOnly, limit, accessOptions });
307
+ break;
308
+
309
+ case 'show':
310
+ if (!subargs[0]) {
311
+ utils.print.error('Please specify a skill ID');
312
+ utils.print.dim('Usage: bootspring skill show <id>');
313
+ return;
314
+ }
315
+ showSkill(subargs[0], {
316
+ includeExternal: true,
317
+ summary: Boolean(parsedArgs.summary),
318
+ sections: parsedArgs.sections,
319
+ maxChars,
320
+ accessOptions
321
+ });
322
+ break;
323
+
324
+ case 'search':
325
+ if (!subargs[0]) {
326
+ utils.print.error('Please specify a search query');
327
+ utils.print.dim('Usage: bootspring skill search <query>');
328
+ return;
329
+ }
330
+ searchSkills(subargs.join(' '), { includeExternal, externalOnly, limit, accessOptions });
331
+ break;
332
+
333
+ case 'sync':
334
+ await syncCatalog({
335
+ manifestUrl: parsedArgs['manifest-url'],
336
+ contentBaseUrl: parsedArgs['content-base-url'],
337
+ token: parsedArgs.token,
338
+ timeoutMs: parsedArgs.timeout,
339
+ cacheDir: parsedArgs['cache-dir'],
340
+ manifestPublicKey: parsedArgs['manifest-public-key'],
341
+ requireManifestSignature: parsedArgs['require-signature']
342
+ });
343
+ break;
344
+
345
+ case 'help':
346
+ case '-h':
347
+ case '--help':
348
+ showHelp();
349
+ break;
350
+
351
+ default:
352
+ // Shortcut: `bootspring skill auth/clerk`
353
+ showSkill(subcommand, {
354
+ includeExternal: true,
355
+ summary: Boolean(parsedArgs.summary),
356
+ sections: parsedArgs.sections,
357
+ maxChars,
358
+ accessOptions
359
+ });
360
+ }
361
+ }
362
+
363
+ module.exports = {
364
+ run,
365
+ listSkills,
366
+ showSkill,
367
+ searchSkills,
368
+ syncCatalog
369
+ };