@girardmedia/bootspring 2.1.2 → 2.2.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.
package/cli/skill.js DELETED
@@ -1,503 +0,0 @@
1
- /**
2
- * Bootspring Skill Command
3
- * Browse and use skill patterns.
4
- */
5
-
6
- const crypto = require('crypto');
7
- const utils = require('../core/utils');
8
- const entitlements = require('../core/entitlements');
9
- const tierEnforcement = require('../core/tier-enforcement');
10
- const telemetry = require('../core/telemetry');
11
- const api = require('../core/api-client');
12
- const auth = require('../core/auth');
13
-
14
- // Note: skills module removed - all content fetched from API (thin client)
15
-
16
- function trackTelemetry(event, payload) {
17
- try {
18
- telemetry.emitEvent(event, payload);
19
- } catch {
20
- // Do not block CLI flow on telemetry issues.
21
- }
22
- }
23
-
24
- // Use centralized tier badge formatting
25
- const { formatTierBadge } = tierEnforcement;
26
-
27
- function normalizeChecksum(checksum) {
28
- const raw = String(checksum || '').trim().toLowerCase();
29
- if (!raw) return '';
30
- return raw.startsWith('sha256:') ? raw.slice(7) : raw;
31
- }
32
-
33
- function isPremiumTier(tier) {
34
- const normalized = String(tier || 'free').trim().toLowerCase();
35
- return normalized === 'pro' || normalized === 'premium';
36
- }
37
-
38
- function verifyPayloadIntegrity(resourceId, content, checksum, options = {}) {
39
- const { requireChecksum = false } = options;
40
- const expected = normalizeChecksum(checksum);
41
- if (requireChecksum && !expected) {
42
- throw new Error(`Integrity metadata missing for skill payload: ${resourceId}`);
43
- }
44
-
45
- const digest = crypto.createHash('sha256').update(String(content || ''), 'utf8').digest('hex');
46
- if (expected && expected !== digest) {
47
- throw new Error(`Integrity check failed for skill payload: ${resourceId}`);
48
- }
49
-
50
- return {
51
- algorithm: 'sha256',
52
- checksum: `sha256:${digest}`,
53
- expected: expected ? `sha256:${expected}` : null,
54
- verified: !!expected
55
- };
56
- }
57
-
58
- /**
59
- * Fetch skills list from API (thin client)
60
- */
61
- async function fetchSkillsList(options = {}) {
62
- if (!auth.isAuthenticated()) {
63
- return { error: 'auth_required' };
64
- }
65
-
66
- try {
67
- const response = await api.listSkills(options);
68
- return response;
69
- } catch (error) {
70
- if (error.status === 401) {
71
- return { error: 'auth_required' };
72
- }
73
- return { error: 'network_error', message: error.message };
74
- }
75
- }
76
-
77
- /**
78
- * Fetch skill content from API (thin client)
79
- */
80
- async function fetchSkillContent(skillId) {
81
- if (!auth.isAuthenticated()) {
82
- return { error: 'auth_required' };
83
- }
84
-
85
- try {
86
- const response = await api.getSkillContent(skillId);
87
- const skillTier = String(response?.tier || 'free').toLowerCase();
88
- const access = entitlements.checkSkillAccess(skillId, {
89
- mode: 'server',
90
- entitled: true,
91
- tier: auth.getTier(),
92
- skillTier
93
- });
94
-
95
- if (!access.allowed) {
96
- return { error: 'upgrade_required', requiredTier: 'pro', message: access.reason };
97
- }
98
-
99
- const integrity = verifyPayloadIntegrity(skillId, response?.content || '', response?.checksum, {
100
- requireChecksum: Boolean(response?.checksum)
101
- });
102
-
103
- return { ...response, integrity };
104
- } catch (error) {
105
- if (error.message && error.message.toLowerCase().includes('integrity')) {
106
- return { error: 'integrity_error', message: error.message };
107
- }
108
- if (error.status === 403) {
109
- return { error: 'upgrade_required', requiredTier: 'pro' };
110
- }
111
- if (error.status === 401) {
112
- return { error: 'auth_required' };
113
- }
114
- if (error.status === 404) {
115
- return { error: 'not_found' };
116
- }
117
- return { error: 'network_error', message: error.message };
118
- }
119
- }
120
-
121
- /**
122
- * List skills from API (thin client)
123
- */
124
- async function listSkills(options = {}) {
125
- const { tierFilter, category } = options;
126
-
127
- // Check authentication
128
- if (!auth.isAuthenticated()) {
129
- console.log(`
130
- ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Skills${utils.COLORS.reset}
131
- ${utils.COLORS.red}Authentication required${utils.COLORS.reset}
132
-
133
- ${utils.COLORS.dim}Skills are served from the API. Please log in:${utils.COLORS.reset}
134
- ${utils.COLORS.cyan}bootspring auth login${utils.COLORS.reset}
135
- `);
136
- return;
137
- }
138
-
139
- const tierLabel = tierFilter ? ` (${tierFilter} tier)` : '';
140
- console.log(`
141
- ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Skills${utils.COLORS.reset}${tierLabel}
142
- ${utils.COLORS.dim}Code patterns served from API${utils.COLORS.reset}
143
- `);
144
-
145
- const spinner = utils.createSpinner('Loading skills...');
146
- spinner.start();
147
-
148
- const result = await fetchSkillsList({ category });
149
-
150
- if (result.error === 'auth_required') {
151
- spinner.fail('Authentication required');
152
- console.log(`${utils.COLORS.dim}Run: bootspring auth login${utils.COLORS.reset}`);
153
- return;
154
- }
155
-
156
- if (result.error === 'network_error') {
157
- spinner.fail('Failed to load skills');
158
- console.log(`${utils.COLORS.dim}Check your internet connection.${utils.COLORS.reset}`);
159
- return;
160
- }
161
-
162
- spinner.succeed(`Loaded ${result.skills?.length || 0} skills`);
163
-
164
- // Group skills by category
165
- const categories = {};
166
- for (const skill of result.skills || []) {
167
- // Apply tier filter if specified
168
- if (tierFilter && skill.tier !== tierFilter) continue;
169
-
170
- if (!categories[skill.category]) {
171
- categories[skill.category] = [];
172
- }
173
- categories[skill.category].push(skill);
174
- }
175
-
176
- // Display by category
177
- for (const [cat, catSkills] of Object.entries(categories).sort()) {
178
- console.log(`\n${utils.COLORS.bold}${cat}${utils.COLORS.reset}`);
179
- for (const skill of catSkills) {
180
- const tierBadge = formatTierBadge(skill.tier);
181
- const lockIcon = skill.accessible ? '' : ` ${utils.COLORS.red}🔒${utils.COLORS.reset}`;
182
- const description = skill.description ? ` - ${skill.description}` : '';
183
- console.log(` ${utils.COLORS.cyan}${skill.id}${utils.COLORS.reset} ${tierBadge}${lockIcon}${description}`);
184
- }
185
- }
186
-
187
- const accessibleCount = (result.skills || []).filter(s => s.accessible).length;
188
- const lockedCount = (result.skills || []).length - accessibleCount;
189
-
190
- console.log(`\n${utils.COLORS.dim}${accessibleCount} skills available, ${lockedCount} locked${utils.COLORS.reset}`);
191
- console.log(`${utils.COLORS.dim}Your tier: ${result.userTier || 'free'}${utils.COLORS.reset}`);
192
- console.log(`${utils.COLORS.dim}Use "bootspring skill show <id>" to view a skill${utils.COLORS.reset}`);
193
- }
194
-
195
- /**
196
- * Show skill content from API (thin client)
197
- */
198
- async function showSkill(skillId, _options = {}) {
199
- // Check authentication first
200
- if (!auth.isAuthenticated()) {
201
- console.log(`
202
- ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Skill: ${skillId}${utils.COLORS.reset}
203
- ${utils.COLORS.red}Authentication required${utils.COLORS.reset}
204
-
205
- ${utils.COLORS.dim}Skill content is served from the API. Please log in:${utils.COLORS.reset}
206
- ${utils.COLORS.cyan}bootspring auth login${utils.COLORS.reset}
207
- `);
208
- return;
209
- }
210
-
211
- const isMCP = utils.isMCPContext();
212
-
213
- const spinner = utils.createSpinner('Loading skill...');
214
- spinner.start();
215
-
216
- const result = await fetchSkillContent(skillId);
217
-
218
- if (result.error === 'auth_required') {
219
- spinner.fail('Authentication required');
220
- console.log(`${utils.COLORS.dim}Run: bootspring auth login${utils.COLORS.reset}`);
221
- return;
222
- }
223
-
224
- if (result.error === 'upgrade_required') {
225
- spinner.fail('Upgrade required');
226
- const promptContext = tierEnforcement.getUpgradePromptContext(`skill: ${skillId}`, result.requiredTier || 'pro', {
227
- capability: 'premium_pattern',
228
- action: 'skill_show'
229
- });
230
- trackTelemetry('premium_prompted', {
231
- skillId,
232
- ...promptContext,
233
- reason: 'upgrade_required'
234
- });
235
- console.log(tierEnforcement.getUpgradePrompt(`skill: ${skillId}`, result.requiredTier || 'pro', {
236
- capability: promptContext.capability,
237
- action: promptContext.action,
238
- placement: promptContext.placement,
239
- variant: promptContext.variant
240
- }));
241
- return;
242
- }
243
-
244
- if (result.error === 'not_found') {
245
- spinner.fail('Skill not found');
246
- utils.print.dim('Try: bootspring skill list');
247
- return;
248
- }
249
-
250
- if (result.error === 'integrity_error') {
251
- spinner.fail('Integrity verification failed');
252
- console.log(`${utils.COLORS.dim}${result.message || 'Skill payload checksum mismatch.'}${utils.COLORS.reset}`);
253
- return;
254
- }
255
-
256
- if (result.error === 'network_error') {
257
- spinner.fail('Failed to load skill');
258
- console.log(`${utils.COLORS.dim}Check your internet connection.${utils.COLORS.reset}`);
259
- return;
260
- }
261
-
262
- spinner.succeed('Skill loaded');
263
-
264
- const tierBadge = formatTierBadge(result.tier || 'free');
265
-
266
- console.log(`
267
- ${utils.COLORS.cyan}${utils.COLORS.bold}${result.name || skillId}${utils.COLORS.reset} ${tierBadge}
268
- ${utils.COLORS.dim}ID: ${result.id || skillId} | Tier: ${result.tier || 'free'}${utils.COLORS.reset}
269
- `);
270
-
271
- // Track successful access
272
- trackTelemetry('skill_accessed', {
273
- skillId: result.id || skillId,
274
- tier: result.tier
275
- });
276
-
277
- const unlockedPremium = entitlements.isExternalSkill(result.id || skillId) || isPremiumTier(result.tier);
278
- if (unlockedPremium) {
279
- const promptContext = tierEnforcement.getUpgradePromptContext(`skill: ${result.id || skillId}`, String(result.tier || 'pro'), {
280
- capability: entitlements.isExternalSkill(result.id || skillId) ? 'external_skill' : 'premium_pattern',
281
- action: 'skill_show'
282
- });
283
- trackTelemetry('premium_unlocked', {
284
- skillId: result.id || skillId,
285
- skillTier: result.tier || 'free',
286
- ...promptContext
287
- });
288
- }
289
-
290
- // In MCP mode, output full content
291
- if (isMCP) {
292
- console.log(result.content);
293
- return;
294
- }
295
-
296
- // CLI mode - show content with truncation notice
297
- const contentLines = result.content.split('\n');
298
- const previewLines = contentLines.slice(0, 40);
299
- const hasMore = contentLines.length > 40;
300
-
301
- console.log(previewLines.join('\n'));
302
-
303
- if (hasMore) {
304
- console.log(`\n${utils.COLORS.dim}... (${contentLines.length - 40} more lines)${utils.COLORS.reset}`);
305
- }
306
-
307
- console.log(`
308
- ${utils.COLORS.bold}Usage:${utils.COLORS.reset}
309
- Copy the pattern above and adapt it to your project.
310
-
311
- ${utils.COLORS.yellow}${utils.COLORS.bold}Full Content in MCP Mode${utils.COLORS.reset}
312
- ${utils.COLORS.dim}With MCP integration, skills are available directly in your AI assistant.${utils.COLORS.reset}
313
- ${utils.COLORS.cyan}bootspring mcp start${utils.COLORS.reset}
314
- `);
315
- }
316
-
317
- /**
318
- * Search skills via API (thin client)
319
- */
320
- async function searchSkills(query, options = {}) {
321
- const { tierFilter } = options;
322
-
323
- // Check authentication
324
- if (!auth.isAuthenticated()) {
325
- console.log(`
326
- ${utils.COLORS.cyan}${utils.COLORS.bold}Search: "${query}"${utils.COLORS.reset}
327
- ${utils.COLORS.red}Authentication required${utils.COLORS.reset}
328
-
329
- ${utils.COLORS.dim}Skills are served from the API. Please log in:${utils.COLORS.reset}
330
- ${utils.COLORS.cyan}bootspring auth login${utils.COLORS.reset}
331
- `);
332
- return;
333
- }
334
-
335
- const spinner = utils.createSpinner('Searching...');
336
- spinner.start();
337
-
338
- const result = await fetchSkillsList({ search: query });
339
-
340
- if (result.error) {
341
- spinner.fail('Search failed');
342
- console.log(`${utils.COLORS.dim}${result.message || 'Check your internet connection.'}${utils.COLORS.reset}`);
343
- return;
344
- }
345
-
346
- spinner.succeed('Search complete');
347
-
348
- let skills = result.skills || [];
349
-
350
- // Apply tier filter if specified
351
- if (tierFilter) {
352
- skills = skills.filter(s => s.tier === tierFilter);
353
- }
354
-
355
- const tierLabel = tierFilter ? ` (${tierFilter} tier)` : '';
356
- console.log(`
357
- ${utils.COLORS.cyan}${utils.COLORS.bold}Search Results${utils.COLORS.reset}${tierLabel}
358
- ${utils.COLORS.dim}Query: "${query}"${utils.COLORS.reset}
359
- `);
360
-
361
- if (skills.length === 0) {
362
- utils.print.warning('No matching skills found');
363
- utils.print.dim('Try: bootspring skill list');
364
- return;
365
- }
366
-
367
- for (const skill of skills) {
368
- const tierBadge = formatTierBadge(skill.tier);
369
- const lockIcon = skill.accessible ? '' : ` ${utils.COLORS.red}🔒${utils.COLORS.reset}`;
370
- const description = skill.description ? ` - ${skill.description}` : '';
371
- console.log(` ${utils.COLORS.cyan}${skill.id}${utils.COLORS.reset} ${tierBadge}${lockIcon}${description}`);
372
- }
373
-
374
- const accessibleCount = skills.filter(s => s.accessible).length;
375
- const lockedCount = skills.length - accessibleCount;
376
-
377
- console.log(`\n${utils.COLORS.dim}${skills.length} match(es)${utils.COLORS.reset}`);
378
- if (lockedCount > 0) {
379
- console.log(`${utils.COLORS.dim}${lockedCount} result(s) require upgrade${utils.COLORS.reset}`);
380
- console.log(`${utils.COLORS.dim}Run: bootspring billing upgrade${utils.COLORS.reset}`);
381
- }
382
- }
383
-
384
- /**
385
- * Sync is no longer needed - thin client fetches from API
386
- * @deprecated
387
- */
388
- async function syncCatalog() {
389
- console.log(`
390
- ${utils.COLORS.cyan}${utils.COLORS.bold}Sync Not Required${utils.COLORS.reset}
391
-
392
- ${utils.COLORS.dim}Skills are now served directly from the API.${utils.COLORS.reset}
393
- ${utils.COLORS.dim}No local sync needed - content is always up to date.${utils.COLORS.reset}
394
-
395
- ${utils.COLORS.bold}To view available skills:${utils.COLORS.reset}
396
- ${utils.COLORS.cyan}bootspring skill list${utils.COLORS.reset}
397
- `);
398
- }
399
-
400
- function showHelp() {
401
- console.log(`
402
- ${utils.COLORS.cyan}${utils.COLORS.bold}Bootspring Skill Command${utils.COLORS.reset}
403
-
404
- ${utils.COLORS.cyan}Usage:${utils.COLORS.reset}
405
- bootspring skill <command> [args] [options]
406
-
407
- ${utils.COLORS.cyan}Commands:${utils.COLORS.reset}
408
- ${utils.COLORS.cyan}list${utils.COLORS.reset} List built-in skills
409
- ${utils.COLORS.cyan}show${utils.COLORS.reset} <id> Show skill content
410
- ${utils.COLORS.cyan}search${utils.COLORS.reset} <query> Search skills
411
-
412
- ${utils.COLORS.cyan}Options:${utils.COLORS.reset}
413
- ${utils.COLORS.cyan}--tier-filter <tier>${utils.COLORS.reset} Filter by tier: free or pro
414
- ${utils.COLORS.cyan}--category <cat>${utils.COLORS.reset} Filter by category
415
-
416
- ${utils.COLORS.cyan}Tier Information:${utils.COLORS.reset}
417
- Skills are labeled with their tier: ${utils.COLORS.green}[FREE]${utils.COLORS.reset} or ${utils.COLORS.yellow}[PRO]${utils.COLORS.reset}
418
- - FREE tier patterns are available to all authenticated users
419
- - PRO tier patterns require a Pro subscription
420
-
421
- ${utils.COLORS.cyan}Authentication:${utils.COLORS.reset}
422
- All skill commands require authentication.
423
- Run: ${utils.COLORS.cyan}bootspring auth login${utils.COLORS.reset}
424
-
425
- ${utils.COLORS.cyan}Examples:${utils.COLORS.reset}
426
- bootspring skill list
427
- bootspring skill list --tier-filter=free
428
- bootspring skill search auth
429
- bootspring skill show auth/clerk
430
- bootspring skill show api/route-handler
431
- `);
432
- }
433
-
434
- /**
435
- * Normalize tier filter value
436
- */
437
- function normalizeTierFilter(value) {
438
- if (!value) return undefined;
439
- const normalized = String(value).trim().toLowerCase();
440
- if (normalized === 'free' || normalized === 'pro' || normalized === 'premium') {
441
- // Treat 'premium' as 'pro' for filtering purposes
442
- return normalized === 'premium' ? 'pro' : normalized;
443
- }
444
- return undefined;
445
- }
446
-
447
- /**
448
- * Run skill command
449
- */
450
- async function run(args) {
451
- const parsedArgs = utils.parseArgs(args);
452
- const subcommand = parsedArgs._[0] || 'list';
453
- const subargs = parsedArgs._.slice(1);
454
-
455
- // Parse tier filter (for list/search filtering) separately from access tier
456
- const tierFilter = normalizeTierFilter(parsedArgs['tier-filter'] || parsedArgs['filter-tier']);
457
-
458
- switch (subcommand) {
459
- case 'list':
460
- await listSkills({ tierFilter, category: parsedArgs.category });
461
- break;
462
-
463
- case 'show':
464
- if (!subargs[0]) {
465
- utils.print.error('Please specify a skill ID');
466
- utils.print.dim('Usage: bootspring skill show <id>');
467
- return;
468
- }
469
- await showSkill(subargs[0], {});
470
- break;
471
-
472
- case 'search':
473
- if (!subargs[0]) {
474
- utils.print.error('Please specify a search query');
475
- utils.print.dim('Usage: bootspring skill search <query>');
476
- return;
477
- }
478
- await searchSkills(subargs.join(' '), { tierFilter });
479
- break;
480
-
481
- case 'sync':
482
- await syncCatalog();
483
- break;
484
-
485
- case 'help':
486
- case '-h':
487
- case '--help':
488
- showHelp();
489
- break;
490
-
491
- default:
492
- // Shortcut: `bootspring skill auth/clerk`
493
- await showSkill(subcommand, {});
494
- }
495
- }
496
-
497
- module.exports = {
498
- run,
499
- listSkills,
500
- showSkill,
501
- searchSkills,
502
- syncCatalog
503
- };