@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
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Bootspring Telemetry Command
3
+ * Inspect and upload telemetry events.
4
+ */
5
+
6
+ const utils = require('../core/utils');
7
+ const telemetry = require('../core/telemetry');
8
+
9
+ function showStatus() {
10
+ const status = telemetry.getStatus();
11
+ console.log(`
12
+ ${utils.COLORS.cyan}${utils.COLORS.bold}Telemetry Status${utils.COLORS.reset}
13
+ ${utils.COLORS.dim}File: ${status.file}${utils.COLORS.reset}
14
+ ${utils.COLORS.dim}Events: ${status.count}${utils.COLORS.reset}
15
+ ${utils.COLORS.dim}Last event: ${status.lastEventAt || 'none'}${utils.COLORS.reset}
16
+ `);
17
+ }
18
+
19
+ function listEvents(options = {}) {
20
+ const records = telemetry.listEvents(options);
21
+ if (records.length === 0) {
22
+ utils.print.warning('No telemetry events found');
23
+ return;
24
+ }
25
+
26
+ records.forEach(record => {
27
+ console.log(`${utils.COLORS.cyan}${record.timestamp}${utils.COLORS.reset} ${record.event}`);
28
+ });
29
+ console.log(`\n${utils.COLORS.dim}${records.length} event(s)${utils.COLORS.reset}`);
30
+ }
31
+
32
+ async function upload(options = {}) {
33
+ const spinner = utils.createSpinner('Uploading telemetry events').start();
34
+ try {
35
+ const result = await telemetry.uploadEvents({
36
+ endpoint: options.endpoint,
37
+ token: options.token,
38
+ event: options.event,
39
+ limit: options.limit,
40
+ clearOnSuccess: options.clear
41
+ });
42
+ spinner.succeed(`Uploaded ${result.uploaded} event(s)`);
43
+ utils.print.dim(`Endpoint: ${result.endpoint}`);
44
+ utils.print.dim(`Remaining local events: ${result.remaining}`);
45
+ } catch (error) {
46
+ spinner.fail(`Upload failed: ${error.message}`);
47
+ }
48
+ }
49
+
50
+ function clear() {
51
+ const result = telemetry.clearEvents();
52
+ utils.print.success(`Cleared ${result.cleared} event(s)`);
53
+ }
54
+
55
+ function help() {
56
+ console.log(`
57
+ ${utils.COLORS.cyan}${utils.COLORS.bold}Bootspring Telemetry${utils.COLORS.reset}
58
+
59
+ ${utils.COLORS.cyan}Usage:${utils.COLORS.reset}
60
+ bootspring telemetry <command> [options]
61
+
62
+ ${utils.COLORS.cyan}Commands:${utils.COLORS.reset}
63
+ ${utils.COLORS.cyan}status${utils.COLORS.reset} Show telemetry status
64
+ ${utils.COLORS.cyan}list${utils.COLORS.reset} List telemetry events
65
+ ${utils.COLORS.cyan}upload${utils.COLORS.reset} Upload events to endpoint
66
+ ${utils.COLORS.cyan}clear${utils.COLORS.reset} Clear local event log
67
+
68
+ ${utils.COLORS.cyan}Options:${utils.COLORS.reset}
69
+ ${utils.COLORS.cyan}--event <name>${utils.COLORS.reset} Filter by event name
70
+ ${utils.COLORS.cyan}--limit <n>${utils.COLORS.reset} Limit listed/uploaded events
71
+ ${utils.COLORS.cyan}--endpoint <url>${utils.COLORS.reset} Upload endpoint override
72
+ ${utils.COLORS.cyan}--token <value>${utils.COLORS.reset} Upload bearer token
73
+ ${utils.COLORS.cyan}--clear${utils.COLORS.reset} Clear local events on successful upload
74
+ `);
75
+ }
76
+
77
+ async function run(args) {
78
+ const parsed = utils.parseArgs(args);
79
+ const command = parsed._[0] || 'status';
80
+
81
+ switch (command) {
82
+ case 'status':
83
+ showStatus();
84
+ break;
85
+ case 'list':
86
+ listEvents({
87
+ event: parsed.event,
88
+ limit: parsed.limit ? Number(parsed.limit) : undefined
89
+ });
90
+ break;
91
+ case 'upload':
92
+ await upload({
93
+ endpoint: parsed.endpoint,
94
+ token: parsed.token,
95
+ event: parsed.event,
96
+ limit: parsed.limit ? Number(parsed.limit) : undefined,
97
+ clear: Boolean(parsed.clear)
98
+ });
99
+ break;
100
+ case 'clear':
101
+ clear();
102
+ break;
103
+ case 'help':
104
+ case '--help':
105
+ case '-h':
106
+ help();
107
+ break;
108
+ default:
109
+ utils.print.error(`Unknown telemetry command: ${command}`);
110
+ help();
111
+ }
112
+ }
113
+
114
+ module.exports = { run };
package/cli/todo.js ADDED
@@ -0,0 +1,614 @@
1
+ /**
2
+ * Bootspring Todo Command
3
+ * Simple and powerful todo management
4
+ *
5
+ * @package bootspring
6
+ * @command todo
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const config = require('../core/config');
12
+ const utils = require('../core/utils');
13
+
14
+ // ============================================================================
15
+ // Utility Functions (for programmatic/test use)
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Parse todo file and return todos
20
+ * @param {string} filePath - Path to todo file
21
+ * @returns {object[]} Array of todos with { done, text } format
22
+ */
23
+ function parseTodoFile(filePath) {
24
+ try {
25
+ const content = fs.readFileSync(filePath, 'utf-8');
26
+ const todos = [];
27
+ const lines = content.split('\n');
28
+
29
+ for (const line of lines) {
30
+ const match = line.match(/^(\s*)-\s*\[([ xX])\]\s*(.+)$/);
31
+ if (match) {
32
+ todos.push({
33
+ done: match[2].toLowerCase() === 'x',
34
+ text: match[3].trim()
35
+ });
36
+ }
37
+ }
38
+
39
+ return todos;
40
+ } catch (error) {
41
+ return [];
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Write todos to file
47
+ * @param {string} filePath - Path to todo file
48
+ * @param {object[]} items - Array of todos with { done, text } format
49
+ */
50
+ function writeTodoFile(filePath, items) {
51
+ const lines = ['# Todo', ''];
52
+
53
+ for (const item of items) {
54
+ const checkbox = item.done ? '[x]' : '[ ]';
55
+ lines.push(`- ${checkbox} ${item.text}`);
56
+ }
57
+
58
+ lines.push(''); // trailing newline
59
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
60
+ }
61
+
62
+ /**
63
+ * Add a todo to file (utility function)
64
+ * @param {string} filePath - Path to todo file
65
+ * @param {string} text - Todo text
66
+ */
67
+ function addTodoUtil(filePath, text) {
68
+ const todos = parseTodoFile(filePath);
69
+ todos.push({ done: false, text });
70
+ writeTodoFile(filePath, todos);
71
+ }
72
+
73
+ /**
74
+ * Mark a todo as done (utility function)
75
+ * @param {string} filePath - Path to todo file
76
+ * @param {number} index - Zero-based index
77
+ */
78
+ function markDone(filePath, index) {
79
+ const todos = parseTodoFile(filePath);
80
+ if (index >= 0 && index < todos.length) {
81
+ todos[index].done = true;
82
+ writeTodoFile(filePath, todos);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Mark a todo as not done (utility function)
88
+ * @param {string} filePath - Path to todo file
89
+ * @param {number} index - Zero-based index
90
+ */
91
+ function markUndone(filePath, index) {
92
+ const todos = parseTodoFile(filePath);
93
+ if (index >= 0 && index < todos.length) {
94
+ todos[index].done = false;
95
+ writeTodoFile(filePath, todos);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Remove a todo by index (utility function)
101
+ * @param {string} filePath - Path to todo file
102
+ * @param {number} index - Zero-based index
103
+ */
104
+ function removeTodoUtil(filePath, index) {
105
+ const todos = parseTodoFile(filePath);
106
+ if (index >= 0 && index < todos.length) {
107
+ todos.splice(index, 1);
108
+ writeTodoFile(filePath, todos);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Clear todos (utility function)
114
+ * @param {string} filePath - Path to todo file
115
+ * @param {string} type - 'completed' or 'all'
116
+ */
117
+ function clearTodos(filePath, type = 'completed') {
118
+ const todos = parseTodoFile(filePath);
119
+
120
+ if (type === 'all') {
121
+ writeTodoFile(filePath, []);
122
+ } else if (type === 'completed') {
123
+ const remaining = todos.filter(t => !t.done);
124
+ writeTodoFile(filePath, remaining);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * List all todos from file (utility function)
130
+ * @param {string} filePath - Path to todo file
131
+ * @returns {object[]} Array of todos
132
+ */
133
+ function listTodosUtil(filePath) {
134
+ return parseTodoFile(filePath);
135
+ }
136
+
137
+ // ============================================================================
138
+ // Internal Parsing (for CLI use)
139
+ // ============================================================================
140
+
141
+ /**
142
+ * Parse todo.md file content
143
+ * @param {string} content - File content
144
+ * @returns {object[]} Array of todos with full metadata
145
+ */
146
+ function parseTodos(content) {
147
+ const todos = [];
148
+ const lines = content.split('\n');
149
+
150
+ for (let i = 0; i < lines.length; i++) {
151
+ const line = lines[i];
152
+
153
+ // Match todo items: - [ ] or - [x]
154
+ const match = line.match(/^(\s*)-\s*\[([ xX])\]\s*(.+)$/);
155
+ if (match) {
156
+ todos.push({
157
+ index: todos.length + 1,
158
+ indent: match[1].length,
159
+ completed: match[2].toLowerCase() === 'x',
160
+ text: match[3].trim(),
161
+ line: i
162
+ });
163
+ }
164
+ }
165
+
166
+ return todos;
167
+ }
168
+
169
+ /**
170
+ * Format todos for display
171
+ * @param {object[]} todos - Array of todos
172
+ * @param {object} options - Display options
173
+ */
174
+ function displayTodos(todos, options = {}) {
175
+ const { showCompleted = true, showIndex = true } = options;
176
+
177
+ const pending = todos.filter(t => !t.completed);
178
+ const completed = todos.filter(t => t.completed);
179
+
180
+ if (pending.length === 0 && completed.length === 0) {
181
+ utils.print.dim('No todos found. Add one with: bootspring todo add "Your task"');
182
+ return;
183
+ }
184
+
185
+ // Display pending
186
+ if (pending.length > 0) {
187
+ console.log(`\n${utils.COLORS.bold}Pending (${pending.length})${utils.COLORS.reset}\n`);
188
+ for (const todo of pending) {
189
+ const index = showIndex ? `${utils.COLORS.dim}${String(todo.index).padStart(2)}${utils.COLORS.reset} ` : '';
190
+ const indent = ' '.repeat(todo.indent);
191
+ console.log(`${index}${indent}${utils.COLORS.yellow}○${utils.COLORS.reset} ${todo.text}`);
192
+ }
193
+ }
194
+
195
+ // Display completed
196
+ if (showCompleted && completed.length > 0) {
197
+ console.log(`\n${utils.COLORS.bold}Completed (${completed.length})${utils.COLORS.reset}\n`);
198
+ for (const todo of completed) {
199
+ const index = showIndex ? `${utils.COLORS.dim}${String(todo.index).padStart(2)}${utils.COLORS.reset} ` : '';
200
+ const indent = ' '.repeat(todo.indent);
201
+ console.log(`${index}${indent}${utils.COLORS.green}●${utils.COLORS.reset} ${utils.COLORS.dim}${todo.text}${utils.COLORS.reset}`);
202
+ }
203
+ }
204
+
205
+ console.log();
206
+ }
207
+
208
+ /**
209
+ * Get todo file path
210
+ * @returns {string} Todo file path
211
+ */
212
+ function getTodoPath() {
213
+ const cfg = config.load();
214
+ return path.join(cfg._projectRoot, cfg.paths?.todo || 'todo.md');
215
+ }
216
+
217
+ /**
218
+ * Read todo file
219
+ * @returns {string} File content
220
+ */
221
+ function readTodoFile() {
222
+ const todoPath = getTodoPath();
223
+ if (!utils.fileExists(todoPath)) {
224
+ // Create default todo file
225
+ const defaultContent = `# Todo List\n\n## Pending\n\n## Completed\n`;
226
+ utils.writeFile(todoPath, defaultContent);
227
+ return defaultContent;
228
+ }
229
+ return utils.readFile(todoPath);
230
+ }
231
+
232
+ /**
233
+ * Save todo file content (internal CLI use)
234
+ * @param {string} content - File content
235
+ */
236
+ function saveTodoContent(content) {
237
+ const todoPath = getTodoPath();
238
+ utils.writeFile(todoPath, content);
239
+ }
240
+
241
+ /**
242
+ * List all todos
243
+ */
244
+ function listTodos(args) {
245
+ const parsedArgs = utils.parseArgs(args);
246
+ const content = readTodoFile();
247
+ const todos = parseTodos(content);
248
+
249
+ console.log(`${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Todo${utils.COLORS.reset}`);
250
+
251
+ displayTodos(todos, {
252
+ showCompleted: !parsedArgs.pending,
253
+ showIndex: true
254
+ });
255
+
256
+ // Summary
257
+ const pending = todos.filter(t => !t.completed).length;
258
+ const completed = todos.filter(t => t.completed).length;
259
+ utils.print.dim(`${pending} pending, ${completed} completed`);
260
+ }
261
+
262
+ /**
263
+ * Add a new todo
264
+ */
265
+ function addTodo(args) {
266
+ const text = args.join(' ').trim();
267
+
268
+ if (!text) {
269
+ utils.print.error('Please provide a todo text');
270
+ utils.print.dim('Usage: bootspring todo add "Your task"');
271
+ return;
272
+ }
273
+
274
+ const content = readTodoFile();
275
+ const lines = content.split('\n');
276
+
277
+ // Find the "Pending" section or add after first heading
278
+ let insertIndex = -1;
279
+ for (let i = 0; i < lines.length; i++) {
280
+ if (lines[i].match(/^##?\s*(Pending|In Progress|Todo)/i)) {
281
+ insertIndex = i + 1;
282
+ // Skip empty lines after heading
283
+ while (insertIndex < lines.length && lines[insertIndex].trim() === '') {
284
+ insertIndex++;
285
+ }
286
+ break;
287
+ }
288
+ }
289
+
290
+ // If no section found, add after first heading
291
+ if (insertIndex === -1) {
292
+ for (let i = 0; i < lines.length; i++) {
293
+ if (lines[i].startsWith('#')) {
294
+ insertIndex = i + 1;
295
+ break;
296
+ }
297
+ }
298
+ }
299
+
300
+ // If still no good place, add at the end
301
+ if (insertIndex === -1) {
302
+ insertIndex = lines.length;
303
+ }
304
+
305
+ // Insert the new todo
306
+ const newTodo = `- [ ] ${text}`;
307
+ lines.splice(insertIndex, 0, newTodo);
308
+
309
+ saveTodoContent(lines.join('\n'));
310
+
311
+ utils.print.success(`Added: ${text}`);
312
+
313
+ // Show count
314
+ const todos = parseTodos(lines.join('\n'));
315
+ const pending = todos.filter(t => !t.completed).length;
316
+ utils.print.dim(`${pending} pending todos`);
317
+ }
318
+
319
+ /**
320
+ * Mark todo as done
321
+ */
322
+ function doneTodo(args) {
323
+ const indexStr = args[0];
324
+
325
+ if (!indexStr) {
326
+ utils.print.error('Please provide a todo number');
327
+ utils.print.dim('Usage: bootspring todo done <number>');
328
+ return;
329
+ }
330
+
331
+ const index = parseInt(indexStr, 10);
332
+ if (isNaN(index) || index < 1) {
333
+ utils.print.error('Invalid todo number');
334
+ return;
335
+ }
336
+
337
+ const content = readTodoFile();
338
+ const todos = parseTodos(content);
339
+
340
+ const todo = todos.find(t => t.index === index);
341
+ if (!todo) {
342
+ utils.print.error(`Todo #${index} not found`);
343
+ return;
344
+ }
345
+
346
+ if (todo.completed) {
347
+ utils.print.warning(`Todo #${index} is already completed`);
348
+ return;
349
+ }
350
+
351
+ // Update the line
352
+ const lines = content.split('\n');
353
+ const originalLine = lines[todo.line];
354
+ lines[todo.line] = originalLine.replace('[ ]', '[x]');
355
+
356
+ saveTodoContent(lines.join('\n'));
357
+
358
+ utils.print.success(`Completed: ${todo.text}`);
359
+
360
+ // Show remaining
361
+ const remaining = todos.filter(t => !t.completed && t.index !== index).length;
362
+ utils.print.dim(`${remaining} pending todos remaining`);
363
+ }
364
+
365
+ /**
366
+ * Undo a completed todo
367
+ */
368
+ function undoTodo(args) {
369
+ const indexStr = args[0];
370
+
371
+ if (!indexStr) {
372
+ utils.print.error('Please provide a todo number');
373
+ utils.print.dim('Usage: bootspring todo undo <number>');
374
+ return;
375
+ }
376
+
377
+ const index = parseInt(indexStr, 10);
378
+ if (isNaN(index) || index < 1) {
379
+ utils.print.error('Invalid todo number');
380
+ return;
381
+ }
382
+
383
+ const content = readTodoFile();
384
+ const todos = parseTodos(content);
385
+
386
+ const todo = todos.find(t => t.index === index);
387
+ if (!todo) {
388
+ utils.print.error(`Todo #${index} not found`);
389
+ return;
390
+ }
391
+
392
+ if (!todo.completed) {
393
+ utils.print.warning(`Todo #${index} is not completed`);
394
+ return;
395
+ }
396
+
397
+ // Update the line
398
+ const lines = content.split('\n');
399
+ const originalLine = lines[todo.line];
400
+ lines[todo.line] = originalLine.replace(/\[[xX]\]/, '[ ]');
401
+
402
+ saveTodoContent(lines.join('\n'));
403
+
404
+ utils.print.success(`Reopened: ${todo.text}`);
405
+ }
406
+
407
+ /**
408
+ * Remove a todo
409
+ */
410
+ function removeTodo(args) {
411
+ const indexStr = args[0];
412
+
413
+ if (!indexStr) {
414
+ utils.print.error('Please provide a todo number');
415
+ utils.print.dim('Usage: bootspring todo remove <number>');
416
+ return;
417
+ }
418
+
419
+ const index = parseInt(indexStr, 10);
420
+ if (isNaN(index) || index < 1) {
421
+ utils.print.error('Invalid todo number');
422
+ return;
423
+ }
424
+
425
+ const content = readTodoFile();
426
+ const todos = parseTodos(content);
427
+
428
+ const todo = todos.find(t => t.index === index);
429
+ if (!todo) {
430
+ utils.print.error(`Todo #${index} not found`);
431
+ return;
432
+ }
433
+
434
+ // Remove the line
435
+ const lines = content.split('\n');
436
+ lines.splice(todo.line, 1);
437
+
438
+ saveTodoContent(lines.join('\n'));
439
+
440
+ utils.print.success(`Removed: ${todo.text}`);
441
+ }
442
+
443
+ /**
444
+ * Clear completed todos
445
+ */
446
+ function clearCompleted() {
447
+ const content = readTodoFile();
448
+ const todos = parseTodos(content);
449
+ const completedTodos = todos.filter(t => t.completed);
450
+
451
+ if (completedTodos.length === 0) {
452
+ utils.print.info('No completed todos to clear');
453
+ return;
454
+ }
455
+
456
+ // Remove completed lines (in reverse order to maintain indices)
457
+ const lines = content.split('\n');
458
+ const linesToRemove = completedTodos.map(t => t.line).sort((a, b) => b - a);
459
+
460
+ for (const lineIndex of linesToRemove) {
461
+ lines.splice(lineIndex, 1);
462
+ }
463
+
464
+ saveTodoContent(lines.join('\n'));
465
+
466
+ utils.print.success(`Cleared ${completedTodos.length} completed todos`);
467
+ }
468
+
469
+ /**
470
+ * Show todo help
471
+ */
472
+ function showHelp() {
473
+ console.log(`
474
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Todo${utils.COLORS.reset}
475
+ ${utils.COLORS.dim}Simple and powerful todo management${utils.COLORS.reset}
476
+
477
+ ${utils.COLORS.bold}Usage:${utils.COLORS.reset}
478
+ bootspring todo <command> [args]
479
+
480
+ ${utils.COLORS.bold}Commands:${utils.COLORS.reset}
481
+ ${utils.COLORS.cyan}list${utils.COLORS.reset} List all todos
482
+ ${utils.COLORS.cyan}add${utils.COLORS.reset} <text> Add a new todo
483
+ ${utils.COLORS.cyan}done${utils.COLORS.reset} <number> Mark todo as completed
484
+ ${utils.COLORS.cyan}undo${utils.COLORS.reset} <number> Reopen a completed todo
485
+ ${utils.COLORS.cyan}remove${utils.COLORS.reset} <number> Remove a todo
486
+ ${utils.COLORS.cyan}clear${utils.COLORS.reset} Clear all completed todos
487
+
488
+ ${utils.COLORS.bold}Examples:${utils.COLORS.reset}
489
+ bootspring todo add "Implement user auth"
490
+ bootspring todo done 1
491
+ bootspring todo list --pending
492
+ bootspring todo clear
493
+ `);
494
+ }
495
+
496
+ /**
497
+ * Run todo command
498
+ */
499
+ async function run(args) {
500
+ const subcommand = args[0] || 'list';
501
+ const subargs = args.slice(1);
502
+
503
+ switch (subcommand) {
504
+ case 'list':
505
+ case 'ls':
506
+ listTodos(subargs);
507
+ break;
508
+
509
+ case 'add':
510
+ case 'a':
511
+ case 'new':
512
+ addTodo(subargs);
513
+ break;
514
+
515
+ case 'done':
516
+ case 'd':
517
+ case 'complete':
518
+ case 'check':
519
+ doneTodo(subargs);
520
+ break;
521
+
522
+ case 'undo':
523
+ case 'u':
524
+ case 'reopen':
525
+ undoTodo(subargs);
526
+ break;
527
+
528
+ case 'remove':
529
+ case 'rm':
530
+ case 'delete':
531
+ removeTodo(subargs);
532
+ break;
533
+
534
+ case 'clear':
535
+ clearCompleted();
536
+ break;
537
+
538
+ case 'help':
539
+ case '-h':
540
+ case '--help':
541
+ showHelp();
542
+ break;
543
+
544
+ default:
545
+ // If it looks like a todo text, treat as add
546
+ if (subcommand && !subcommand.startsWith('-')) {
547
+ addTodo([subcommand, ...subargs]);
548
+ } else {
549
+ utils.print.error(`Unknown subcommand: ${subcommand}`);
550
+ showHelp();
551
+ }
552
+ }
553
+ }
554
+
555
+ // Smart wrapper that detects if called with file path or CLI args
556
+ function addTodoSmart(firstArg, secondArg) {
557
+ if (typeof firstArg === 'string' && typeof secondArg === 'string') {
558
+ // Utility mode: addTodo(filePath, text)
559
+ return addTodoUtil(firstArg, secondArg);
560
+ } else if (Array.isArray(firstArg)) {
561
+ // CLI mode: addTodo(args)
562
+ return addTodo(firstArg);
563
+ } else {
564
+ // Default CLI mode with single arg treated as array
565
+ return addTodo([firstArg, secondArg].filter(Boolean));
566
+ }
567
+ }
568
+
569
+ function removeTodoSmart(firstArg, secondArg) {
570
+ if (typeof firstArg === 'string' && typeof secondArg === 'number') {
571
+ // Utility mode: removeTodo(filePath, index)
572
+ return removeTodoUtil(firstArg, secondArg);
573
+ } else if (Array.isArray(firstArg)) {
574
+ // CLI mode: removeTodo(args)
575
+ return removeTodo(firstArg);
576
+ } else {
577
+ return removeTodo([firstArg]);
578
+ }
579
+ }
580
+
581
+ function listTodosSmart(firstArg) {
582
+ if (typeof firstArg === 'string' && (firstArg.includes('/') || firstArg.includes('\\'))) {
583
+ // Utility mode: listTodos(filePath)
584
+ return listTodosUtil(firstArg);
585
+ } else if (Array.isArray(firstArg)) {
586
+ // CLI mode: listTodos(args)
587
+ return listTodos(firstArg);
588
+ } else {
589
+ return listTodos([firstArg].filter(Boolean));
590
+ }
591
+ }
592
+
593
+ module.exports = {
594
+ // CLI command runner
595
+ run,
596
+
597
+ // CLI-focused internal functions
598
+ parseTodos,
599
+ doneTodo,
600
+ undoTodo,
601
+ clearCompleted,
602
+
603
+ // Utility functions (for programmatic/test use)
604
+ parseTodoFile,
605
+ writeTodoFile,
606
+ markDone,
607
+ markUndone,
608
+ clearTodos,
609
+
610
+ // Smart wrappers that work in both modes
611
+ addTodo: addTodoSmart,
612
+ removeTodo: removeTodoSmart,
613
+ listTodos: listTodosSmart
614
+ };