@girardmedia/bootspring 2.0.18 → 2.0.21

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/bin/bootspring.js CHANGED
@@ -467,6 +467,18 @@ async function main() {
467
467
  try {
468
468
  const commandModule = require(scriptPath);
469
469
 
470
+ // Track command usage (silent, best-effort)
471
+ try {
472
+ const telemetry = require('../core/telemetry');
473
+ telemetry.emitEvent('cli.command', {
474
+ command,
475
+ subcommand: commandArgs[0] || null,
476
+ hasFlags: commandArgs.some(arg => arg.startsWith('-'))
477
+ });
478
+ } catch {
479
+ // Telemetry is optional - don't fail commands if it errors
480
+ }
481
+
470
482
  // Execute command
471
483
  if (typeof commandModule.run === 'function') {
472
484
  await commandModule.run(commandArgs);
package/cli/generate.js CHANGED
@@ -218,6 +218,28 @@ ${utils.COLORS.dim}Regenerating AI context...${utils.COLORS.reset}
218
218
  return;
219
219
  }
220
220
 
221
+ // Generate todo.md if missing (required for 100% score)
222
+ const todoPath = path.join(cfg._projectRoot, cfg.paths?.todo || 'todo.md');
223
+ if (!utils.fileExists(todoPath)) {
224
+ const todoSpinner = utils.createSpinner('Generating todo.md').start();
225
+ const todoContent = `# ${cfg.project.name} - Todo List
226
+
227
+ > Last updated: ${utils.formatDate()}
228
+
229
+ ## In Progress
230
+
231
+ ## Pending
232
+
233
+ - [ ] Review generated context
234
+ - [ ] Configure environment variables
235
+
236
+ ## Completed
237
+
238
+ `;
239
+ utils.writeFile(todoPath, todoContent);
240
+ todoSpinner.succeed('Generated todo.md');
241
+ }
242
+
221
243
  // Validate context
222
244
  const validation = context.validate({ config: cfg });
223
245
 
@@ -264,28 +286,6 @@ ${utils.COLORS.bold}Score:${utils.COLORS.reset} ${validation.score}/${validation
264
286
  mcpSpinner.succeed('Generated .mcp.json');
265
287
  }
266
288
 
267
- // Generate todo.md if missing
268
- const todoPath = path.join(cfg._projectRoot, cfg.paths?.todo || 'todo.md');
269
- if (!utils.fileExists(todoPath)) {
270
- const todoSpinner = utils.createSpinner('Generating todo.md').start();
271
- const todoContent = `# ${cfg.project.name} - Todo List
272
-
273
- > Last updated: ${utils.formatDate()}
274
-
275
- ## In Progress
276
-
277
- ## Pending
278
-
279
- - [ ] Review generated context
280
- - [ ] Configure environment variables
281
-
282
- ## Completed
283
-
284
- `;
285
- utils.writeFile(todoPath, todoContent);
286
- todoSpinner.succeed('Generated todo.md');
287
- }
288
-
289
289
  // Generate AGENTS.md for multi-tool compatibility
290
290
  const agentsPath = path.join(cfg._projectRoot, 'AGENTS.md');
291
291
  const agentsSpinner = utils.createSpinner('Generating AGENTS.md').start();
package/cli/seed.js CHANGED
@@ -1116,7 +1116,7 @@ function extractProblem(docs) {
1116
1116
  // Remove section number prefixes like "2.1 " at start of content
1117
1117
  .replace(/^[\d.]+\s+[A-Z][a-z]+\s+[a-z]+\n/m, '')
1118
1118
  // Convert bullet points to flowing text
1119
- .replace(/^[\-\*]\s+\*{0,2}([^*\n:]+):?\*{0,2}\s*/gm, '$1: ')
1119
+ .replace(/^[-*]\s+\*{0,2}([^*\n:]+):?\*{0,2}\s*/gm, '$1: ')
1120
1120
  // Remove remaining markdown
1121
1121
  .replace(/\*+/g, '')
1122
1122
  .trim();
@@ -1139,7 +1139,7 @@ function extractProblem(docs) {
1139
1139
  const cleaned = problemSection
1140
1140
  .replace(/^#{1,4}\s+[\d.]+\s+[^\n]+\n/gm, '')
1141
1141
  .replace(/^[\d.]+\s+[A-Z][a-z]+\s+[a-z]+\n/m, '')
1142
- .replace(/^[\-\*]\s+\*{0,2}([^*\n:]+):?\*{0,2}\s*/gm, '$1: ')
1142
+ .replace(/^[-*]\s+\*{0,2}([^*\n:]+):?\*{0,2}\s*/gm, '$1: ')
1143
1143
  .replace(/\*+/g, '')
1144
1144
  .trim();
1145
1145
 
@@ -1167,7 +1167,7 @@ function extractSolution(docs) {
1167
1167
  if (solutionSection) {
1168
1168
  const paragraphs = solutionSection.split(/\n\n/).filter(p => p.trim().length > 50);
1169
1169
  if (paragraphs.length > 0) {
1170
- return paragraphs[0].replace(/^[#\*\-\d\.]+\s*/gm, '').replace(/\*+/g, '').trim();
1170
+ return paragraphs[0].replace(/^[#*\d.-]+\s*/gm, '').replace(/\*+/g, '').trim();
1171
1171
  }
1172
1172
  }
1173
1173
 
@@ -1303,10 +1303,10 @@ function extractMVPFeatures(docs) {
1303
1303
  if (features.length === 0) {
1304
1304
  const journeySection = extractSection(prdDoc, 'Core\\s+User\\s+Journeys|User\\s+Journeys', { maxLength: 3000 });
1305
1305
  if (journeySection) {
1306
- const journeyHeaders = journeySection.match(/^#{1,4}\s+\d+\.\d+\s+Journey\s+\d+\s*[—\-]\s*(.+)$/gm);
1306
+ const journeyHeaders = journeySection.match(/^#{1,4}\s+\d+\.\d+\s+Journey\s+\d+\s*[—-]\s*(.+)$/gm);
1307
1307
  if (journeyHeaders) {
1308
1308
  journeyHeaders.forEach(h => {
1309
- const match = h.match(/Journey\s+\d+\s*[—\-]\s*(.+)/);
1309
+ const match = h.match(/Journey\s+\d+\s*[—-]\s*(.+)/);
1310
1310
  if (match) features.push(match[1].trim());
1311
1311
  });
1312
1312
  }
@@ -1329,10 +1329,10 @@ function extractTargetUsers(docs) {
1329
1329
 
1330
1330
  // Try AUDIENCE doc first - look for "## X.X Persona PX — Name" pattern
1331
1331
  if (audienceDoc) {
1332
- const personaMatches = audienceDoc.match(/^#{1,4}\s+\d+\.\d+\s+Persona\s+P?\d+\s*[—\-]\s*(.+)$/gm);
1332
+ const personaMatches = audienceDoc.match(/^#{1,4}\s+\d+\.\d+\s+Persona\s+P?\d+\s*[—-]\s*(.+)$/gm);
1333
1333
  if (personaMatches) {
1334
1334
  personaMatches.forEach(m => {
1335
- const match = m.match(/[—\-]\s*(.+)/);
1335
+ const match = m.match(/[—-]\s*(.+)/);
1336
1336
  if (match && match[1]) personas.push(match[1].trim());
1337
1337
  });
1338
1338
  }
@@ -1353,7 +1353,7 @@ function extractTargetUsers(docs) {
1353
1353
  const targetSection = extractSection(visionDoc, 'Target\\s+Users|Who\\s+This\\s+Is\\s+For', { maxLength: 2000 });
1354
1354
  if (targetSection) {
1355
1355
  // Look for bold text patterns like "- **AI Agencies & AI Arbitrage Specialists**"
1356
- const boldMatches = targetSection.match(/^\s*[\-\*]\s*\*{2}([^*]+)\*{2}/gm);
1356
+ const boldMatches = targetSection.match(/^\s*[-*]\s*\*{2}([^*]+)\*{2}/gm);
1357
1357
  if (boldMatches) {
1358
1358
  boldMatches.forEach(m => {
1359
1359
  const match = m.match(/\*{2}([^*]+)\*{2}/);
@@ -1367,10 +1367,10 @@ function extractTargetUsers(docs) {
1367
1367
  if (prdDoc && personas.length === 0) {
1368
1368
  const personaSection = extractSection(prdDoc, 'Personas|Who\\s+We\\s+Serve', { maxLength: 2000 });
1369
1369
  if (personaSection) {
1370
- const personaMatches = personaSection.match(/^#{1,4}\s+\d+\.\d+\s+Persona\s+[A-Z]\s*[—\-]\s*(.+)$/gm);
1370
+ const personaMatches = personaSection.match(/^#{1,4}\s+\d+\.\d+\s+Persona\s+[A-Z]\s*[—-]\s*(.+)$/gm);
1371
1371
  if (personaMatches) {
1372
1372
  personaMatches.forEach(m => {
1373
- const match = m.match(/[—\-]\s*(.+)/);
1373
+ const match = m.match(/[—-]\s*(.+)/);
1374
1374
  if (match && match[1]) personas.push(match[1].trim());
1375
1375
  });
1376
1376
  }
@@ -1394,7 +1394,7 @@ function extractDatabaseEntities(docs) {
1394
1394
  const tableSection = extractSection(techDoc, 'Key\\s+tables|Data\\s+Model|Database\\s+Schema', { maxLength: 3000 });
1395
1395
  if (tableSection) {
1396
1396
  // Extract backticked table names from bullet points in this specific section
1397
- const bulletTableMatches = tableSection.match(/^\s*[\-\*]\s*`(\w+)`/gm);
1397
+ const bulletTableMatches = tableSection.match(/^\s*[-*]\s*`(\w+)`/gm);
1398
1398
  if (bulletTableMatches) {
1399
1399
  bulletTableMatches.forEach(m => {
1400
1400
  const match = m.match(/`(\w+)`/);
@@ -1461,11 +1461,11 @@ function extractImplementationPhases(docs) {
1461
1461
 
1462
1462
  // Prioritize full phase sections: "## X. Phase N —" (with section number)
1463
1463
  // These have the actual deliverables, unlike brief summaries
1464
- const fullPhaseMatches = roadmapDoc.match(/^##\s+\d+\.\s+Phase\s+\d+\s*[—\-]+\s*(.+)$/gm);
1464
+ const fullPhaseMatches = roadmapDoc.match(/^##\s+\d+\.\s+Phase\s+\d+\s*[—-]+\s*(.+)$/gm);
1465
1465
 
1466
1466
  if (fullPhaseMatches) {
1467
1467
  fullPhaseMatches.forEach(m => {
1468
- const match = m.match(/Phase\s+(\d+)\s*[—\-]+\s*(.+)$/);
1468
+ const match = m.match(/Phase\s+(\d+)\s*[—-]+\s*(.+)$/);
1469
1469
  if (match && match[1] && match[2]) {
1470
1470
  const phaseNum = match[1];
1471
1471
  if (seenPhases.has(phaseNum)) return;
@@ -1478,7 +1478,7 @@ function extractImplementationPhases(docs) {
1478
1478
  const deliverables = [];
1479
1479
 
1480
1480
  // Find the full section for this specific phase (## X. Phase N — until next ## X. Phase)
1481
- const phaseRegex = new RegExp(`##\\s+\\d+\\.\\s+Phase\\s+${phaseNum}\\s*[—\\-][\\s\\S]*?(?=##\\s+\\d+\\.\\s+Phase\\s+\\d+|$)`, 'i');
1481
+ const phaseRegex = new RegExp(`##\\s+\\d+\\.\\s+Phase\\s+${phaseNum}\\s*[—-][\\s\\S]*?(?=##\\s+\\d+\\.\\s+Phase\\s+\\d+|$)`, 'i');
1482
1482
  const phaseSectionMatch = roadmapDoc.match(phaseRegex);
1483
1483
 
1484
1484
  if (phaseSectionMatch) {
@@ -1490,10 +1490,10 @@ function extractImplementationPhases(docs) {
1490
1490
 
1491
1491
  if (deliverablesMatch) {
1492
1492
  // Extract bullet points
1493
- const bullets = deliverablesMatch[0].match(/^[\-\*]\s+(.+)$/gm);
1493
+ const bullets = deliverablesMatch[0].match(/^[-*]\s+(.+)$/gm);
1494
1494
  if (bullets) {
1495
1495
  bullets.slice(0, 6).forEach(b => {
1496
- const text = b.replace(/^[\-\*]\s+/, '').trim();
1496
+ const text = b.replace(/^[-*]\s+/, '').trim();
1497
1497
  if (text.length > 3) deliverables.push(text);
1498
1498
  });
1499
1499
  }
@@ -1521,10 +1521,10 @@ function extractImplementationPhases(docs) {
1521
1521
  const exitMatch = phaseContent.match(exitCriteriaRegex);
1522
1522
  if (exitMatch) {
1523
1523
  // Extract checkmark items like "- ✅ Description"
1524
- const checkItems = exitMatch[0].match(/^[\-\*]\s+[✅✓]\s*(.+)$/gm);
1524
+ const checkItems = exitMatch[0].match(/^[-*]\s+[✅✓]\s*(.+)$/gm);
1525
1525
  if (checkItems) {
1526
1526
  checkItems.slice(0, 5).forEach(item => {
1527
- const text = item.replace(/^[\-\*]\s+[✅✓]\s*/, '').trim();
1527
+ const text = item.replace(/^[-*]\s+[✅✓]\s*/, '').trim();
1528
1528
  if (text.length > 5) deliverables.push(text);
1529
1529
  });
1530
1530
  }
@@ -1537,10 +1537,10 @@ function extractImplementationPhases(docs) {
1537
1537
  const goalMatch = phaseContent.match(goalRegex);
1538
1538
  if (goalMatch) {
1539
1539
  // Extract bullet points or bold items
1540
- const bullets = goalMatch[0].match(/^[\-\*]\s+(.+)$/gm);
1540
+ const bullets = goalMatch[0].match(/^[-*]\s+(.+)$/gm);
1541
1541
  if (bullets) {
1542
1542
  bullets.slice(0, 4).forEach(b => {
1543
- const text = b.replace(/^[\-\*]\s+/, '').trim();
1543
+ const text = b.replace(/^[-*]\s+/, '').trim();
1544
1544
  if (text.length > 3) deliverables.push(text);
1545
1545
  });
1546
1546
  }
@@ -1639,10 +1639,10 @@ function extractAPIRoutes(docs) {
1639
1639
 
1640
1640
  if (appStructureSection) {
1641
1641
  // Extract page routes like "- `/dashboard`" or "- `/agents`" or "- `/agents/[id]`"
1642
- const pageRoutes = appStructureSection.match(/[-*]\s*`\/([\w\/\[\]]+)`/g);
1642
+ const pageRoutes = appStructureSection.match(/[-*]\s*`\/([\w/[\]]+)`/g);
1643
1643
  if (pageRoutes) {
1644
1644
  pageRoutes.forEach(r => {
1645
- const match = r.match(/`(\/[\w\/\[\]]+)`/);
1645
+ const match = r.match(/`(\/[\w/[\]]+)`/);
1646
1646
  if (match && match[1]) {
1647
1647
  const route = match[1];
1648
1648
  // Skip if it looks like a citation URL
@@ -1673,7 +1673,7 @@ function extractAPIRoutes(docs) {
1673
1673
  }
1674
1674
 
1675
1675
  // Also look for explicit /api/ routes in backticks
1676
- const explicitRoutes = techDoc.match(/`\/api\/[\w\/\[\]-]+`/g);
1676
+ const explicitRoutes = techDoc.match(/`\/api\/[\w/[\]-]+`/g);
1677
1677
  if (explicitRoutes) {
1678
1678
  explicitRoutes.forEach(r => {
1679
1679
  const route = r.replace(/`/g, '');
@@ -1735,7 +1735,7 @@ function extractArchitecture(docs) {
1735
1735
  const archSection = extractSection(techDoc, 'Architecture|System\\s+Components', { maxLength: 2000 });
1736
1736
  if (archSection) {
1737
1737
  // Look for bold component names like "- **Component Name**"
1738
- const boldMatches = archSection.match(/^\s*[\-\*]\s+\*{2}([^*]+)\*{2}/gm);
1738
+ const boldMatches = archSection.match(/^\s*[-*]\s+\*{2}([^*]+)\*{2}/gm);
1739
1739
  if (boldMatches) {
1740
1740
  boldMatches.forEach(m => {
1741
1741
  const match = m.match(/\*{2}([^*]+)\*{2}/);
package/core/context.js CHANGED
@@ -251,8 +251,7 @@ function validate(options = {}) {
251
251
  checks.push({ name: 'Todo Tracking', status: 'pass', message: 'todo.md exists' });
252
252
  score += 1;
253
253
  } else {
254
- checks.push({ name: 'Todo Tracking', status: 'info', message: 'todo.md not found - optional' });
255
- score += 0.5;
254
+ checks.push({ name: 'Todo Tracking', status: 'fail', message: 'todo.md not found' });
256
255
  }
257
256
 
258
257
  return {
@@ -157,7 +157,7 @@ function extractFromPrd(content) {
157
157
  // Extract from Core User Journeys
158
158
  const journeySection = extractSection(content, 'Core\\s+User\\s+Journeys|User\\s+Journeys', { maxLength: 5000 });
159
159
  if (journeySection) {
160
- const journeyMatches = journeySection.matchAll(/^#{1,4}\s+\d+\.\d+\s+Journey\s+\d+\s*[—\-]\s*(.+)$/gm);
160
+ const journeyMatches = journeySection.matchAll(/^#{1,4}\s+\d+\.\d+\s+Journey\s+\d+\s*[—-]\s*(.+)$/gm);
161
161
  for (const match of journeyMatches) {
162
162
  const journeyName = match[1].trim();
163
163
 
@@ -189,7 +189,7 @@ function extractFromRoadmap(content) {
189
189
  const seenPhases = new Set();
190
190
 
191
191
  // Extract phase headers: "## X. Phase N — Name"
192
- const phaseMatches = content.matchAll(/^##\s+\d+\.\s+Phase\s+(\d+)\s*[—\-]+\s*(.+)$/gm);
192
+ const phaseMatches = content.matchAll(/^##\s+\d+\.\s+Phase\s+(\d+)\s*[—-]+\s*(.+)$/gm);
193
193
 
194
194
  for (const match of phaseMatches) {
195
195
  const phaseNum = match[1];
@@ -205,7 +205,7 @@ function extractFromRoadmap(content) {
205
205
 
206
206
  // Extract deliverables for this phase
207
207
  const phaseRegex = new RegExp(
208
- `##\\s+\\d+\\.\\s+Phase\\s+${phaseNum}\\s*[—\\-][\\s\\S]*?(?=##\\s+\\d+\\.\\s+Phase\\s+\\d+|$)`,
208
+ `##\\s+\\d+\\.\\s+Phase\\s+${phaseNum}\\s*[—-][\\s\\S]*?(?=##\\s+\\d+\\.\\s+Phase\\s+\\d+|$)`,
209
209
  'i'
210
210
  );
211
211
  const phaseSectionMatch = content.match(phaseRegex);
@@ -219,10 +219,10 @@ function extractFromRoadmap(content) {
219
219
  const deliverablesMatch = phaseContent.match(deliverablesRegex);
220
220
 
221
221
  if (deliverablesMatch) {
222
- const bullets = deliverablesMatch[0].match(/^[\-\*]\s+(.+)$/gm);
222
+ const bullets = deliverablesMatch[0].match(/^[-*]\s+(.+)$/gm);
223
223
  if (bullets) {
224
224
  bullets.slice(0, 8).forEach(b => {
225
- const text = b.replace(/^[\-\*]\s+/, '').trim();
225
+ const text = b.replace(/^[-*]\s+/, '').trim();
226
226
  if (text.length > 3) {
227
227
  deliverables.push(text);
228
228
 
@@ -269,10 +269,10 @@ function extractFromRoadmap(content) {
269
269
  const exitCriteriaRegex = /###\s+[\d.]+\s*Exit\s+Criteria[\s\S]*?(?=###|##|$)/i;
270
270
  const exitMatch = phaseContent.match(exitCriteriaRegex);
271
271
  if (exitMatch) {
272
- const checkItems = exitMatch[0].match(/^[\-\*]\s+[✅✓⬜]\s*(.+)$/gm);
272
+ const checkItems = exitMatch[0].match(/^[-*]\s+[✅✓⬜]\s*(.+)$/gm);
273
273
  if (checkItems) {
274
274
  checkItems.forEach(item => {
275
- const text = item.replace(/^[\-\*]\s+[✅✓⬜]\s*/, '').trim();
275
+ const text = item.replace(/^[-*]\s+[✅✓⬜]\s*/, '').trim();
276
276
  if (text.length > 5 && !deliverables.includes(text)) {
277
277
  deliverables.push(text);
278
278
  }
@@ -304,7 +304,7 @@ function extractFromTechnicalSpec(content) {
304
304
  const dbSection = extractSection(content, 'Data\\s+Model|Database|Key\\s+tables', { maxLength: 3000 });
305
305
  if (dbSection) {
306
306
  // Look for table names
307
- const tableMatches = dbSection.match(/^\s*[\-\*]\s*`(\w+)`/gm);
307
+ const tableMatches = dbSection.match(/^\s*[-*]\s*`(\w+)`/gm);
308
308
  if (tableMatches && tableMatches.length > 0) {
309
309
  tasks.push({
310
310
  title: 'Set up database schema with Prisma',
@@ -328,7 +328,7 @@ function extractFromTechnicalSpec(content) {
328
328
  if (apiSection) {
329
329
  // Group related API routes
330
330
  const apiGroups = new Map();
331
- const apiMatches = apiSection.matchAll(/`\/api\/([^`\/]+)/g);
331
+ const apiMatches = apiSection.matchAll(/`\/api\/([^`/]+)/g);
332
332
 
333
333
  for (const match of apiMatches) {
334
334
  const group = match[1];
@@ -446,10 +446,10 @@ function extractMvpCriteria(content) {
446
446
  const successSection = extractSection(content, 'MVP\\s+Success|Success\\s+Criteria|Definition\\s+of\\s+Done', { maxLength: 3000 });
447
447
 
448
448
  if (successSection) {
449
- const bulletMatches = successSection.match(/^[\-\*]\s+(.+)$/gm);
449
+ const bulletMatches = successSection.match(/^[-*]\s+(.+)$/gm);
450
450
  if (bulletMatches) {
451
451
  bulletMatches.forEach(b => {
452
- const text = b.replace(/^[\-\*]\s+/, '').replace(/\*+/g, '').trim();
452
+ const text = b.replace(/^[-*]\s+/, '').replace(/\*+/g, '').trim();
453
453
  if (text.length > 5) {
454
454
  criteria.push({
455
455
  name: text,
@@ -572,10 +572,10 @@ function extractAcceptanceCriteria(content, featureId) {
572
572
  const featureContent = featureMatch[0];
573
573
 
574
574
  // Look for bullet points
575
- const bulletMatches = featureContent.match(/^[\-\*]\s+(.+)$/gm);
575
+ const bulletMatches = featureContent.match(/^[-*]\s+(.+)$/gm);
576
576
  if (bulletMatches) {
577
577
  bulletMatches.slice(0, 5).forEach(b => {
578
- const text = b.replace(/^[\-\*]\s+/, '').replace(/\*+/g, '').trim();
578
+ const text = b.replace(/^[-*]\s+/, '').replace(/\*+/g, '').trim();
579
579
  if (text.length > 5 && text.length < 200) {
580
580
  criteria.push(text);
581
581
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "contractVersion": "v1",
3
3
  "packageName": "@girardmedia/bootspring",
4
- "packageVersion": "2.0.18",
4
+ "packageVersion": "2.0.21",
5
5
  "tools": [
6
6
  {
7
7
  "name": "bootspring_assist",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@girardmedia/bootspring",
3
- "version": "2.0.18",
3
+ "version": "2.0.21",
4
4
  "description": "Development scaffolding with intelligence - AI-powered context, agents, and workflows for any MCP-compatible assistant",
5
5
  "keywords": [
6
6
  "ai",