@girardmedia/bootspring 2.0.21 → 2.0.23

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 (159) hide show
  1. package/bin/bootspring.js +5 -0
  2. package/cli/org.js +474 -0
  3. package/cli/preseed/index.js +16 -0
  4. package/cli/preseed/interactive.js +143 -0
  5. package/cli/preseed/templates.js +227 -0
  6. package/cli/preseed.js +9 -301
  7. package/cli/seed/builders/ai-context-builder.js +85 -0
  8. package/cli/seed/builders/index.js +13 -0
  9. package/cli/seed/builders/seed-builder.js +272 -0
  10. package/cli/seed/extractors/content-extractors.js +383 -0
  11. package/cli/seed/extractors/index.js +47 -0
  12. package/cli/seed/extractors/metadata-extractors.js +167 -0
  13. package/cli/seed/extractors/section-extractor.js +54 -0
  14. package/cli/seed/extractors/stack-extractors.js +228 -0
  15. package/cli/seed/index.js +18 -0
  16. package/cli/seed/utils/folder-structure.js +84 -0
  17. package/cli/seed/utils/index.js +11 -0
  18. package/cli/seed.js +23 -1074
  19. package/core/api-client.js +77 -0
  20. package/core/entitlements.js +36 -0
  21. package/core/organizations.js +223 -0
  22. package/core/policies.js +51 -6
  23. package/core/policy-matrix.js +303 -0
  24. package/core/project-context.js +1 -0
  25. package/dist/cli/index.d.ts +3 -0
  26. package/dist/cli/index.js +3220 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/context-McpJQa_2.d.ts +5710 -0
  29. package/dist/core/index.d.ts +635 -0
  30. package/dist/core/index.js +2593 -0
  31. package/dist/core/index.js.map +1 -0
  32. package/dist/index-QqbeEiDm.d.ts +857 -0
  33. package/dist/index-UiYCgwiH.d.ts +174 -0
  34. package/dist/index.d.ts +453 -0
  35. package/dist/index.js +44228 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mcp/index.d.ts +1 -0
  38. package/dist/mcp/index.js +41173 -0
  39. package/dist/mcp/index.js.map +1 -0
  40. package/generators/index.ts +82 -0
  41. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  42. package/intelligence/orchestrator/config/index.js +23 -0
  43. package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
  44. package/intelligence/orchestrator/config/phases.js +111 -0
  45. package/intelligence/orchestrator/config/remediation.js +150 -0
  46. package/intelligence/orchestrator/config/workflows.js +168 -0
  47. package/intelligence/orchestrator/core/index.js +16 -0
  48. package/intelligence/orchestrator/core/state-manager.js +88 -0
  49. package/intelligence/orchestrator/core/telemetry.js +24 -0
  50. package/intelligence/orchestrator/index.js +17 -0
  51. package/intelligence/orchestrator.js +17 -512
  52. package/mcp/contracts/mcp-contract.v1.json +1 -1
  53. package/package.json +16 -3
  54. package/src/cli/agent.ts +703 -0
  55. package/src/cli/analyze.ts +640 -0
  56. package/src/cli/audit.ts +707 -0
  57. package/src/cli/auth.ts +930 -0
  58. package/src/cli/billing.ts +364 -0
  59. package/src/cli/build.ts +1089 -0
  60. package/src/cli/business.ts +508 -0
  61. package/src/cli/checkpoint-utils.ts +236 -0
  62. package/src/cli/checkpoint.ts +757 -0
  63. package/src/cli/cloud-sync.ts +534 -0
  64. package/src/cli/content.ts +273 -0
  65. package/src/cli/context.ts +667 -0
  66. package/src/cli/dashboard.ts +133 -0
  67. package/src/cli/deploy.ts +704 -0
  68. package/src/cli/doctor.ts +480 -0
  69. package/src/cli/fundraise.ts +494 -0
  70. package/src/cli/generate.ts +346 -0
  71. package/src/cli/github-cmd.ts +566 -0
  72. package/src/cli/health.ts +599 -0
  73. package/src/cli/index.ts +113 -0
  74. package/src/cli/init.ts +838 -0
  75. package/src/cli/legal.ts +495 -0
  76. package/src/cli/log.ts +316 -0
  77. package/src/cli/loop.ts +1660 -0
  78. package/src/cli/manager.ts +878 -0
  79. package/src/cli/mcp.ts +275 -0
  80. package/src/cli/memory.ts +346 -0
  81. package/src/cli/metrics.ts +590 -0
  82. package/src/cli/monitor.ts +960 -0
  83. package/src/cli/mvp.ts +662 -0
  84. package/src/cli/onboard.ts +663 -0
  85. package/src/cli/orchestrator.ts +622 -0
  86. package/src/cli/plugin.ts +483 -0
  87. package/src/cli/prd.ts +671 -0
  88. package/src/cli/preseed-start.ts +1633 -0
  89. package/src/cli/preseed.ts +2434 -0
  90. package/src/cli/project.ts +526 -0
  91. package/src/cli/quality.ts +885 -0
  92. package/src/cli/security.ts +1079 -0
  93. package/src/cli/seed.ts +1224 -0
  94. package/src/cli/skill.ts +537 -0
  95. package/src/cli/suggest.ts +1225 -0
  96. package/src/cli/switch.ts +518 -0
  97. package/src/cli/task.ts +780 -0
  98. package/src/cli/telemetry.ts +172 -0
  99. package/src/cli/todo.ts +627 -0
  100. package/src/cli/types.ts +15 -0
  101. package/src/cli/update.ts +334 -0
  102. package/src/cli/visualize.ts +609 -0
  103. package/src/cli/watch.ts +895 -0
  104. package/src/cli/workspace.ts +709 -0
  105. package/src/core/action-recorder.ts +673 -0
  106. package/src/core/analyze-workflow.ts +1453 -0
  107. package/src/core/api-client.ts +1120 -0
  108. package/src/core/audit-workflow.ts +1681 -0
  109. package/src/core/auth.ts +471 -0
  110. package/src/core/build-orchestrator.ts +509 -0
  111. package/src/core/build-state.ts +621 -0
  112. package/src/core/checkpoint-engine.ts +482 -0
  113. package/src/core/config.ts +1285 -0
  114. package/src/core/context-loader.ts +694 -0
  115. package/src/core/context.ts +410 -0
  116. package/src/core/deploy-workflow.ts +1085 -0
  117. package/src/core/entitlements.ts +322 -0
  118. package/src/core/github-sync.ts +720 -0
  119. package/src/core/index.ts +981 -0
  120. package/src/core/ingest.ts +1186 -0
  121. package/src/core/metrics-engine.ts +886 -0
  122. package/src/core/mvp.ts +847 -0
  123. package/src/core/onboard-workflow.ts +1293 -0
  124. package/src/core/policies.ts +81 -0
  125. package/src/core/preseed-workflow.ts +1163 -0
  126. package/src/core/preseed.ts +1826 -0
  127. package/src/core/project-context.ts +380 -0
  128. package/src/core/project-state.ts +699 -0
  129. package/src/core/r2-sync.ts +691 -0
  130. package/src/core/scaffold.ts +1715 -0
  131. package/src/core/session.ts +286 -0
  132. package/src/core/task-extractor.ts +799 -0
  133. package/src/core/telemetry.ts +371 -0
  134. package/src/core/tier-enforcement.ts +737 -0
  135. package/src/core/utils.ts +437 -0
  136. package/src/index.ts +29 -0
  137. package/src/intelligence/agent-collab.ts +2376 -0
  138. package/src/intelligence/auto-suggest.ts +713 -0
  139. package/src/intelligence/content-gen.ts +1351 -0
  140. package/src/intelligence/cross-project.ts +1692 -0
  141. package/src/intelligence/git-memory.ts +529 -0
  142. package/src/intelligence/index.ts +318 -0
  143. package/src/intelligence/orchestrator.ts +534 -0
  144. package/src/intelligence/prd.ts +466 -0
  145. package/src/intelligence/recommendations.ts +982 -0
  146. package/src/intelligence/workflow-composer.ts +1472 -0
  147. package/src/mcp/capabilities.ts +233 -0
  148. package/src/mcp/index.ts +37 -0
  149. package/src/mcp/registry.ts +1268 -0
  150. package/src/mcp/response-formatter.ts +797 -0
  151. package/src/mcp/server.ts +240 -0
  152. package/src/types/agent.ts +69 -0
  153. package/src/types/config.ts +86 -0
  154. package/src/types/context.ts +77 -0
  155. package/src/types/index.ts +53 -0
  156. package/src/types/mcp.ts +91 -0
  157. package/src/types/skills.ts +47 -0
  158. package/src/types/workflow.ts +155 -0
  159. package/generators/index.js +0 -18
@@ -0,0 +1,895 @@
1
+ /**
2
+ * Bootspring Watch Command
3
+ * Real-time file watching and sync for todos, tasks, and workflows
4
+ *
5
+ * @package bootspring
6
+ * @command watch
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { EventEmitter } from 'events';
12
+
13
+ // Interfaces
14
+ interface ConfigModule {
15
+ load: () => { _projectRoot: string };
16
+ }
17
+
18
+ interface UtilsModule {
19
+ parseArgs: (args: string[]) => ParsedArgs;
20
+ }
21
+
22
+ interface TelemetryModule {
23
+ emitEvent: (event: string, payload: Record<string, unknown>) => void;
24
+ }
25
+
26
+ interface CheckpointEngineModule {
27
+ getCheckpointStatus: (projectRoot: string) => CheckpointStatus;
28
+ syncCheckpoints: (projectRoot: string) => { success: boolean };
29
+ }
30
+
31
+ interface CheckpointUtilsModule {
32
+ pushCheckpointsToServer: (projectRoot: string, options: PushOptions) => Promise<{ success: boolean }>;
33
+ }
34
+
35
+ interface ParsedArgs {
36
+ _: string[];
37
+ help?: boolean;
38
+ h?: boolean;
39
+ todos?: boolean;
40
+ workflows?: boolean;
41
+ prd?: boolean;
42
+ checkpoints?: boolean;
43
+ verbose?: boolean;
44
+ debounce?: string;
45
+ [key: string]: string | boolean | string[] | undefined;
46
+ }
47
+
48
+ interface Todo {
49
+ id: string;
50
+ line: number;
51
+ completed: boolean;
52
+ text: string;
53
+ }
54
+
55
+ interface TodoChange {
56
+ type: 'completed' | 'added' | 'removed' | 'reopened';
57
+ todo: Todo;
58
+ }
59
+
60
+ interface TodoStats {
61
+ pending: number;
62
+ completed: number;
63
+ total: number;
64
+ progress: number;
65
+ }
66
+
67
+ interface WatcherOptions {
68
+ debounceMs?: number | undefined;
69
+ watchTodos?: boolean | undefined;
70
+ watchWorkflows?: boolean | undefined;
71
+ watchPRD?: boolean | undefined;
72
+ watchCheckpoints?: boolean | undefined;
73
+ }
74
+
75
+ interface WatcherState {
76
+ todos: Todo[];
77
+ workflows: Record<string, WorkflowState>;
78
+ prd: PRD | null;
79
+ checkpoints: CheckpointStatus | null;
80
+ }
81
+
82
+ interface WorkflowState {
83
+ status?: string | undefined;
84
+ phases?: Record<string, { status: string }> | undefined;
85
+ }
86
+
87
+ interface PRD {
88
+ name?: string | undefined;
89
+ stories?: Story[] | undefined;
90
+ }
91
+
92
+ interface Story {
93
+ id?: string | undefined;
94
+ title: string;
95
+ status: string;
96
+ }
97
+
98
+ interface CheckpointStatus {
99
+ exists: boolean;
100
+ percentage?: number | undefined;
101
+ completed?: number | undefined;
102
+ total?: number | undefined;
103
+ checkpoints?: Checkpoint[] | undefined;
104
+ }
105
+
106
+ interface Checkpoint {
107
+ id: string;
108
+ label: string;
109
+ completed: boolean;
110
+ }
111
+
112
+ interface PushOptions {
113
+ quiet?: boolean | undefined;
114
+ autoSync?: boolean | undefined;
115
+ skipMetrics?: boolean | undefined;
116
+ }
117
+
118
+ interface RunWatchOptions {
119
+ watchTodos?: boolean | undefined;
120
+ watchWorkflows?: boolean | undefined;
121
+ watchPRD?: boolean | undefined;
122
+ watchCheckpoints?: boolean | undefined;
123
+ verbose?: boolean | undefined;
124
+ debounceMs?: number | undefined;
125
+ }
126
+
127
+ // Lazy load modules
128
+ const config = require('../core/config') as ConfigModule;
129
+ const utils = require('../core/utils') as UtilsModule;
130
+ const telemetry = require('../core/telemetry') as TelemetryModule;
131
+ const checkpointEngine = require('../core/checkpoint-engine') as CheckpointEngineModule;
132
+
133
+ // Colors
134
+ const c = {
135
+ reset: '\x1b[0m',
136
+ bold: '\x1b[1m',
137
+ dim: '\x1b[2m',
138
+ green: '\x1b[32m',
139
+ blue: '\x1b[34m',
140
+ yellow: '\x1b[33m',
141
+ cyan: '\x1b[36m',
142
+ red: '\x1b[31m',
143
+ magenta: '\x1b[35m'
144
+ };
145
+
146
+ /**
147
+ * Parse todo.md content
148
+ */
149
+ function parseTodoFile(content: string): Todo[] {
150
+ const todos: Todo[] = [];
151
+ const lines = content.split('\n');
152
+
153
+ for (let i = 0; i < lines.length; i++) {
154
+ const line = lines[i];
155
+ if (!line) continue;
156
+ const match = line.match(/^- \[([ xX])\] (.+)$/);
157
+ if (match) {
158
+ todos.push({
159
+ id: `todo-${i}`,
160
+ line: i + 1,
161
+ completed: match[1]?.toLowerCase() === 'x',
162
+ text: match[2]?.trim() || ''
163
+ });
164
+ }
165
+ }
166
+
167
+ return todos;
168
+ }
169
+
170
+ /**
171
+ * Compare todo states and find changes
172
+ */
173
+ function diffTodos(oldTodos: Todo[], newTodos: Todo[]): TodoChange[] {
174
+ const changes: TodoChange[] = [];
175
+
176
+ // Map old todos by text for comparison
177
+ const oldMap = new Map(oldTodos.map(t => [t.text, t]));
178
+ const newMap = new Map(newTodos.map(t => [t.text, t]));
179
+
180
+ // Find completed todos
181
+ for (const newTodo of newTodos) {
182
+ const oldTodo = oldMap.get(newTodo.text);
183
+ if (oldTodo && !oldTodo.completed && newTodo.completed) {
184
+ changes.push({ type: 'completed', todo: newTodo });
185
+ }
186
+ if (oldTodo && oldTodo.completed && !newTodo.completed) {
187
+ changes.push({ type: 'reopened', todo: newTodo });
188
+ }
189
+ }
190
+
191
+ // Find added todos
192
+ for (const newTodo of newTodos) {
193
+ if (!oldMap.has(newTodo.text)) {
194
+ changes.push({ type: 'added', todo: newTodo });
195
+ }
196
+ }
197
+
198
+ // Find removed todos
199
+ for (const oldTodo of oldTodos) {
200
+ if (!newMap.has(oldTodo.text)) {
201
+ changes.push({ type: 'removed', todo: oldTodo });
202
+ }
203
+ }
204
+
205
+ return changes;
206
+ }
207
+
208
+ /**
209
+ * Get todo statistics
210
+ */
211
+ function getTodoStats(todos: Todo[]): TodoStats {
212
+ const pending = todos.filter(t => !t.completed).length;
213
+ const completed = todos.filter(t => t.completed).length;
214
+ return {
215
+ pending,
216
+ completed,
217
+ total: todos.length,
218
+ progress: todos.length > 0 ? Math.round((completed / todos.length) * 100) : 0
219
+ };
220
+ }
221
+
222
+ /**
223
+ * File watcher class
224
+ */
225
+ class FileWatcher extends EventEmitter {
226
+ private projectRoot: string;
227
+ private options: Required<WatcherOptions>;
228
+ private watchers: fs.FSWatcher[];
229
+ private state: WatcherState;
230
+ private debounceTimers: Map<string, NodeJS.Timeout>;
231
+
232
+ constructor(projectRoot: string, options: WatcherOptions = {}) {
233
+ super();
234
+ this.projectRoot = projectRoot;
235
+ this.options = {
236
+ debounceMs: options.debounceMs || 300,
237
+ watchTodos: options.watchTodos !== false,
238
+ watchWorkflows: options.watchWorkflows !== false,
239
+ watchPRD: options.watchPRD !== false,
240
+ watchCheckpoints: options.watchCheckpoints !== false
241
+ };
242
+
243
+ this.watchers = [];
244
+ this.state = {
245
+ todos: [],
246
+ workflows: {},
247
+ prd: null,
248
+ checkpoints: null
249
+ };
250
+ this.debounceTimers = new Map();
251
+ }
252
+
253
+ /**
254
+ * Start watching files
255
+ */
256
+ start(): void {
257
+ const todoPath = path.join(this.projectRoot, 'todo.md');
258
+ const bootspringDir = path.join(this.projectRoot, '.bootspring');
259
+ const prdPath = path.join(this.projectRoot, 'tasks', 'prd.json');
260
+
261
+ // Watch todo.md
262
+ if (this.options.watchTodos && fs.existsSync(todoPath)) {
263
+ this.watchFile(todoPath, 'todo');
264
+ this.loadTodoState(todoPath);
265
+ }
266
+
267
+ // Watch .bootspring directory for workflow changes
268
+ if (this.options.watchWorkflows && fs.existsSync(bootspringDir)) {
269
+ this.watchDirectory(bootspringDir, 'workflow');
270
+ this.loadWorkflowStates();
271
+ }
272
+
273
+ // Watch PRD
274
+ if (this.options.watchPRD && fs.existsSync(prdPath)) {
275
+ this.watchFile(prdPath, 'prd');
276
+ this.loadPRDState(prdPath);
277
+ }
278
+
279
+ // Watch planning directory for checkpoint-related files
280
+ const planningDir = path.join(this.projectRoot, 'planning');
281
+ if (this.options.watchCheckpoints && fs.existsSync(planningDir)) {
282
+ this.watchDirectory(planningDir, 'checkpoint');
283
+ this.loadCheckpointState();
284
+ }
285
+
286
+ this.emit('started');
287
+ }
288
+
289
+ /**
290
+ * Watch a single file
291
+ */
292
+ private watchFile(filePath: string, type: string): void {
293
+ try {
294
+ const watcher = fs.watch(filePath, (eventType) => {
295
+ if (eventType === 'change') {
296
+ this.debouncedHandle(filePath, type);
297
+ }
298
+ });
299
+
300
+ this.watchers.push(watcher);
301
+ } catch (err) {
302
+ this.emit('error', { type, error: err });
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Watch a directory
308
+ */
309
+ private watchDirectory(dirPath: string, type: string): void {
310
+ try {
311
+ const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
312
+ if (!filename) return;
313
+
314
+ // For checkpoints, watch .md files; for workflows, watch .json files
315
+ const isRelevant = type === 'checkpoint'
316
+ ? filename.endsWith('.md') || filename.endsWith('.json')
317
+ : filename.endsWith('.json');
318
+
319
+ if (isRelevant) {
320
+ const fullPath = path.join(dirPath, filename);
321
+ this.debouncedHandle(fullPath, type);
322
+ }
323
+ });
324
+
325
+ this.watchers.push(watcher);
326
+ } catch (err) {
327
+ this.emit('error', { type, error: err });
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Debounced change handler
333
+ */
334
+ private debouncedHandle(filePath: string, type: string): void {
335
+ const key = `${type}:${filePath}`;
336
+
337
+ const existingTimer = this.debounceTimers.get(key);
338
+ if (existingTimer) {
339
+ clearTimeout(existingTimer);
340
+ }
341
+
342
+ this.debounceTimers.set(key, setTimeout(() => {
343
+ this.handleChange(filePath, type);
344
+ this.debounceTimers.delete(key);
345
+ }, this.options.debounceMs));
346
+ }
347
+
348
+ /**
349
+ * Handle file change
350
+ */
351
+ private handleChange(filePath: string, type: string): void {
352
+ try {
353
+ switch (type) {
354
+ case 'todo':
355
+ this.handleTodoChange(filePath);
356
+ break;
357
+ case 'workflow':
358
+ this.handleWorkflowChange(filePath);
359
+ break;
360
+ case 'prd':
361
+ this.handlePRDChange(filePath);
362
+ break;
363
+ case 'checkpoint':
364
+ this.handleCheckpointChange(filePath);
365
+ break;
366
+ }
367
+ } catch (err) {
368
+ this.emit('error', { type, filePath, error: err });
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Load initial todo state
374
+ */
375
+ private loadTodoState(filePath: string): void {
376
+ try {
377
+ if (fs.existsSync(filePath)) {
378
+ const content = fs.readFileSync(filePath, 'utf-8');
379
+ this.state.todos = parseTodoFile(content);
380
+ }
381
+ } catch {
382
+ // Ignore errors
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Handle todo file changes
388
+ */
389
+ private handleTodoChange(filePath: string): void {
390
+ if (!fs.existsSync(filePath)) {
391
+ this.emit('todo:deleted');
392
+ this.state.todos = [];
393
+ return;
394
+ }
395
+
396
+ const content = fs.readFileSync(filePath, 'utf-8');
397
+ const newTodos = parseTodoFile(content);
398
+ const changes = diffTodos(this.state.todos, newTodos);
399
+ const stats = getTodoStats(newTodos);
400
+
401
+ this.state.todos = newTodos;
402
+
403
+ if (changes.length > 0) {
404
+ this.emit('todo:changed', { changes, stats, todos: newTodos });
405
+
406
+ // Emit specific events
407
+ for (const change of changes) {
408
+ this.emit(`todo:${change.type}`, change.todo);
409
+ }
410
+
411
+ // Track in telemetry
412
+ telemetry.emitEvent('watch:todo:change', {
413
+ changes: changes.map(c => ({ type: c.type, text: c.todo.text.slice(0, 50) })),
414
+ stats
415
+ });
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Load initial workflow states
421
+ */
422
+ private loadWorkflowStates(): void {
423
+ const workflowNames = ['onboard', 'analyze', 'audit', 'preseed', 'seed', 'deploy', 'loop'];
424
+
425
+ for (const name of workflowNames) {
426
+ const stateFile = path.join(this.projectRoot, '.bootspring', name, 'workflow-state.json');
427
+ if (fs.existsSync(stateFile)) {
428
+ try {
429
+ this.state.workflows[name] = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
430
+ } catch {
431
+ // Ignore errors
432
+ }
433
+ }
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Handle workflow state changes
439
+ */
440
+ private handleWorkflowChange(filePath: string): void {
441
+ // Extract workflow name from path
442
+ const relativePath = path.relative(path.join(this.projectRoot, '.bootspring'), filePath);
443
+ const workflowName = relativePath.split(path.sep)[0];
444
+
445
+ if (!workflowName) return;
446
+
447
+ if (!fs.existsSync(filePath)) {
448
+ this.emit('workflow:deleted', { workflow: workflowName });
449
+ delete this.state.workflows[workflowName];
450
+ return;
451
+ }
452
+
453
+ try {
454
+ const newState = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as WorkflowState;
455
+ const oldState = this.state.workflows[workflowName];
456
+
457
+ this.state.workflows[workflowName] = newState;
458
+
459
+ // Detect phase changes
460
+ if (oldState && newState.phases) {
461
+ for (const [phaseName, phase] of Object.entries(newState.phases)) {
462
+ const oldPhase = oldState.phases?.[phaseName];
463
+ if (oldPhase && oldPhase.status !== phase.status) {
464
+ this.emit('workflow:phase', {
465
+ workflow: workflowName,
466
+ phase: phaseName,
467
+ oldStatus: oldPhase.status,
468
+ newStatus: phase.status
469
+ });
470
+ }
471
+ }
472
+ }
473
+
474
+ this.emit('workflow:changed', { workflow: workflowName, state: newState });
475
+
476
+ // Track in telemetry
477
+ telemetry.emitEvent('watch:workflow:change', {
478
+ workflow: workflowName,
479
+ status: newState.status
480
+ });
481
+ } catch {
482
+ // Ignore parse errors
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Load initial PRD state
488
+ */
489
+ private loadPRDState(filePath: string): void {
490
+ try {
491
+ if (fs.existsSync(filePath)) {
492
+ this.state.prd = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
493
+ }
494
+ } catch {
495
+ // Ignore errors
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Handle PRD changes
501
+ */
502
+ private handlePRDChange(filePath: string): void {
503
+ if (!fs.existsSync(filePath)) {
504
+ this.emit('prd:deleted');
505
+ this.state.prd = null;
506
+ return;
507
+ }
508
+
509
+ try {
510
+ const newPRD = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as PRD;
511
+ const oldPRD = this.state.prd;
512
+
513
+ // Detect story status changes
514
+ if (oldPRD && newPRD.stories) {
515
+ const oldStoryMap = new Map((oldPRD.stories || []).map(s => [s.id || s.title, s]));
516
+
517
+ for (const story of newPRD.stories) {
518
+ const oldStory = oldStoryMap.get(story.id || story.title);
519
+ if (oldStory && oldStory.status !== story.status) {
520
+ this.emit('prd:story', {
521
+ story,
522
+ oldStatus: oldStory.status,
523
+ newStatus: story.status
524
+ });
525
+ }
526
+ }
527
+ }
528
+
529
+ this.state.prd = newPRD;
530
+ this.emit('prd:changed', { prd: newPRD });
531
+
532
+ // Track in telemetry
533
+ const completed = newPRD.stories?.filter(s => s.status === 'complete').length || 0;
534
+ telemetry.emitEvent('watch:prd:change', {
535
+ name: newPRD.name,
536
+ stories: newPRD.stories?.length || 0,
537
+ completed
538
+ });
539
+ } catch {
540
+ // Ignore parse errors
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Load initial checkpoint state
546
+ */
547
+ private loadCheckpointState(): void {
548
+ try {
549
+ const status = checkpointEngine.getCheckpointStatus(this.projectRoot);
550
+ if (status.exists) {
551
+ this.state.checkpoints = status;
552
+ }
553
+ } catch {
554
+ // Ignore errors
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Handle checkpoint-related file changes
560
+ * Syncs checkpoints when planning files change and auto-pushes to server
561
+ */
562
+ private handleCheckpointChange(_filePath: string): void {
563
+ try {
564
+ // Sync checkpoints when any planning file changes
565
+ checkpointEngine.syncCheckpoints(this.projectRoot);
566
+ const newStatus = checkpointEngine.getCheckpointStatus(this.projectRoot);
567
+ const oldStatus = this.state.checkpoints;
568
+
569
+ if (newStatus.exists) {
570
+ // Check for newly completed checkpoints
571
+ if (oldStatus && oldStatus.checkpoints) {
572
+ for (const checkpoint of newStatus.checkpoints || []) {
573
+ const oldCheckpoint = oldStatus.checkpoints.find(c => c.id === checkpoint.id);
574
+ if (oldCheckpoint && !oldCheckpoint.completed && checkpoint.completed) {
575
+ this.emit('checkpoint:completed', {
576
+ checkpoint,
577
+ file: _filePath
578
+ });
579
+ }
580
+ }
581
+ }
582
+
583
+ // Check if percentage changed
584
+ if (!oldStatus || oldStatus.percentage !== newStatus.percentage) {
585
+ this.emit('checkpoint:progress', {
586
+ oldPercentage: oldStatus?.percentage || 0,
587
+ newPercentage: newStatus.percentage,
588
+ completed: newStatus.completed,
589
+ total: newStatus.total
590
+ });
591
+
592
+ // Auto-push to dashboard if authenticated
593
+ this.autoPushCheckpoints();
594
+ }
595
+
596
+ this.state.checkpoints = newStatus;
597
+ this.emit('checkpoint:changed', { status: newStatus });
598
+
599
+ // Track in telemetry
600
+ telemetry.emitEvent('watch:checkpoint:change', {
601
+ completed: newStatus.completed,
602
+ total: newStatus.total,
603
+ percentage: newStatus.percentage
604
+ });
605
+ }
606
+ } catch {
607
+ // Ignore errors
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Auto-push checkpoints to dashboard (non-blocking)
613
+ * Only pushes if authenticated and project is selected
614
+ */
615
+ private async autoPushCheckpoints(): Promise<void> {
616
+ try {
617
+ const checkpointUtils = require('./checkpoint-utils') as CheckpointUtilsModule;
618
+ const result = await checkpointUtils.pushCheckpointsToServer(this.projectRoot, {
619
+ quiet: true,
620
+ autoSync: true,
621
+ skipMetrics: true // Skip heavy metrics collection in watch mode
622
+ });
623
+
624
+ if (result.success) {
625
+ this.emit('checkpoint:synced', { success: true });
626
+ }
627
+ } catch {
628
+ // Silent fail - auto-push is best-effort
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Stop watching
634
+ */
635
+ stop(): void {
636
+ for (const watcher of this.watchers) {
637
+ watcher.close();
638
+ }
639
+ this.watchers = [];
640
+
641
+ for (const timer of this.debounceTimers.values()) {
642
+ clearTimeout(timer);
643
+ }
644
+ this.debounceTimers.clear();
645
+
646
+ this.emit('stopped');
647
+ }
648
+
649
+ /**
650
+ * Get current state
651
+ */
652
+ getState(): WatcherState {
653
+ return { ...this.state };
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Create notification message
659
+ */
660
+ function formatNotification(type: string, data: Record<string, unknown>): string {
661
+ const timestamp = new Date().toLocaleTimeString();
662
+
663
+ switch (type) {
664
+ case 'todo:completed':
665
+ return `${c.green}✓${c.reset} [${c.dim}${timestamp}${c.reset}] Todo completed: ${data.text}`;
666
+
667
+ case 'todo:added':
668
+ return `${c.blue}+${c.reset} [${c.dim}${timestamp}${c.reset}] Todo added: ${data.text}`;
669
+
670
+ case 'todo:removed':
671
+ return `${c.red}-${c.reset} [${c.dim}${timestamp}${c.reset}] Todo removed: ${data.text}`;
672
+
673
+ case 'todo:reopened':
674
+ return `${c.yellow}○${c.reset} [${c.dim}${timestamp}${c.reset}] Todo reopened: ${data.text}`;
675
+
676
+ case 'workflow:phase':
677
+ return `${c.cyan}◆${c.reset} [${c.dim}${timestamp}${c.reset}] ${data.workflow}/${data.phase}: ${data.oldStatus} → ${data.newStatus}`;
678
+
679
+ case 'workflow:changed':
680
+ return `${c.cyan}↻${c.reset} [${c.dim}${timestamp}${c.reset}] Workflow updated: ${data.workflow}`;
681
+
682
+ case 'prd:story': {
683
+ const story = data.story as Story;
684
+ return `${c.magenta}◇${c.reset} [${c.dim}${timestamp}${c.reset}] Story "${story.title}": ${data.oldStatus} → ${data.newStatus}`;
685
+ }
686
+
687
+ case 'prd:changed': {
688
+ const prd = data.prd as PRD;
689
+ return `${c.magenta}↻${c.reset} [${c.dim}${timestamp}${c.reset}] PRD updated: ${prd.name}`;
690
+ }
691
+
692
+ case 'checkpoint:completed': {
693
+ const checkpoint = data.checkpoint as Checkpoint;
694
+ return `${c.green}★${c.reset} [${c.dim}${timestamp}${c.reset}] Checkpoint completed: ${checkpoint.label}`;
695
+ }
696
+
697
+ case 'checkpoint:progress':
698
+ return `${c.cyan}▲${c.reset} [${c.dim}${timestamp}${c.reset}] Progress: ${data.oldPercentage}% → ${data.newPercentage}% (${data.completed}/${data.total})`;
699
+
700
+ case 'checkpoint:synced':
701
+ return `${c.green}↑${c.reset} [${c.dim}${timestamp}${c.reset}] Synced to dashboard`;
702
+
703
+ default:
704
+ return `${c.dim}[${timestamp}]${c.reset} ${type}: ${JSON.stringify(data)}`;
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Draw status bar
710
+ */
711
+ function drawStatusBar(state: WatcherState): string {
712
+ const todoStats = getTodoStats(state.todos);
713
+ const workflowCount = Object.keys(state.workflows).length;
714
+ const prdName = state.prd?.name || 'None';
715
+ const checkpointProgress = state.checkpoints?.percentage || 0;
716
+
717
+ const parts = [
718
+ `${c.cyan}Todos:${c.reset} ${todoStats.pending}/${todoStats.total}`,
719
+ `${c.cyan}Workflows:${c.reset} ${workflowCount}`,
720
+ `${c.cyan}Checkpoints:${c.reset} ${checkpointProgress}%`,
721
+ `${c.cyan}PRD:${c.reset} ${prdName}`
722
+ ];
723
+
724
+ return `${c.dim}${parts.join(' │ ')}${c.reset}`;
725
+ }
726
+
727
+ /**
728
+ * Run interactive watch mode
729
+ */
730
+ async function runWatchMode(projectRoot: string, options: RunWatchOptions = {}): Promise<void> {
731
+ const watcher = new FileWatcher(projectRoot, options);
732
+
733
+ console.log(`
734
+ ${c.cyan}${c.bold}⚡ Bootspring Watch Mode${c.reset}
735
+ ${c.dim}Monitoring files for changes...${c.reset}
736
+ ${c.dim}Press Ctrl+C to exit${c.reset}
737
+ `);
738
+
739
+ // Set up event handlers
740
+ watcher.on('started', () => {
741
+ console.log(drawStatusBar(watcher.getState()));
742
+ console.log();
743
+ });
744
+
745
+ watcher.on('todo:completed', (todo: Todo) => {
746
+ console.log(formatNotification('todo:completed', todo as unknown as Record<string, unknown>));
747
+ });
748
+
749
+ watcher.on('todo:added', (todo: Todo) => {
750
+ console.log(formatNotification('todo:added', todo as unknown as Record<string, unknown>));
751
+ });
752
+
753
+ watcher.on('todo:removed', (todo: Todo) => {
754
+ console.log(formatNotification('todo:removed', todo as unknown as Record<string, unknown>));
755
+ });
756
+
757
+ watcher.on('todo:reopened', (todo: Todo) => {
758
+ console.log(formatNotification('todo:reopened', todo as unknown as Record<string, unknown>));
759
+ });
760
+
761
+ watcher.on('workflow:phase', (data: Record<string, unknown>) => {
762
+ console.log(formatNotification('workflow:phase', data));
763
+ });
764
+
765
+ watcher.on('workflow:changed', (data: Record<string, unknown>) => {
766
+ if (options.verbose) {
767
+ console.log(formatNotification('workflow:changed', data));
768
+ }
769
+ });
770
+
771
+ watcher.on('prd:story', (data: Record<string, unknown>) => {
772
+ console.log(formatNotification('prd:story', data));
773
+ });
774
+
775
+ watcher.on('prd:changed', (data: Record<string, unknown>) => {
776
+ if (options.verbose) {
777
+ console.log(formatNotification('prd:changed', data));
778
+ }
779
+ });
780
+
781
+ watcher.on('checkpoint:completed', (data: Record<string, unknown>) => {
782
+ console.log(formatNotification('checkpoint:completed', data));
783
+ });
784
+
785
+ watcher.on('checkpoint:progress', (data: Record<string, unknown>) => {
786
+ console.log(formatNotification('checkpoint:progress', data));
787
+ });
788
+
789
+ watcher.on('checkpoint:synced', (data: Record<string, unknown>) => {
790
+ console.log(formatNotification('checkpoint:synced', data));
791
+ });
792
+
793
+ watcher.on('error', (err: { type: string; error?: Error }) => {
794
+ console.log(`${c.red}Error:${c.reset} ${err.type} - ${err.error?.message || 'Unknown error'}`);
795
+ });
796
+
797
+ // Start watching
798
+ watcher.start();
799
+
800
+ // Handle exit
801
+ process.on('SIGINT', () => {
802
+ console.log(`\n${c.dim}Stopping watch mode...${c.reset}`);
803
+ watcher.stop();
804
+ process.exit(0);
805
+ });
806
+
807
+ // Keep process alive
808
+ return new Promise(() => {});
809
+ }
810
+
811
+ /**
812
+ * Show help
813
+ */
814
+ function showHelp(): void {
815
+ console.log(`
816
+ ${c.cyan}${c.bold}⚡ Bootspring Watch${c.reset}
817
+ ${c.dim}Real-time file watching and sync${c.reset}
818
+
819
+ ${c.bold}Usage:${c.reset}
820
+ bootspring watch [options]
821
+
822
+ ${c.bold}Options:${c.reset}
823
+ --todos Watch only todo.md
824
+ --workflows Watch only workflow states
825
+ --prd Watch only PRD file
826
+ --checkpoints Watch only checkpoints
827
+ --verbose Show all change events
828
+ --debounce <ms> Debounce interval (default: 300)
829
+
830
+ ${c.bold}Watched Files:${c.reset}
831
+ - todo.md Task changes
832
+ - .bootspring/*/workflow-state.json Workflow phase changes
833
+ - tasks/prd.json Story status changes
834
+ - planning/*.md Checkpoint progress
835
+
836
+ ${c.bold}Events:${c.reset}
837
+ ${c.green}✓${c.reset} Todo completed
838
+ ${c.blue}+${c.reset} Todo added
839
+ ${c.red}-${c.reset} Todo removed
840
+ ${c.cyan}◆${c.reset} Workflow phase change
841
+ ${c.magenta}◇${c.reset} PRD story change
842
+ ${c.green}★${c.reset} Checkpoint completed
843
+ ${c.cyan}▲${c.reset} Checkpoint progress
844
+
845
+ ${c.bold}Examples:${c.reset}
846
+ bootspring watch # Watch all files
847
+ bootspring watch --todos # Watch only todos
848
+ bootspring watch --verbose # Show all events
849
+ `);
850
+ }
851
+
852
+ /**
853
+ * Run watch command
854
+ */
855
+ export async function run(args: string[]): Promise<void> {
856
+ const parsedArgs = utils.parseArgs(args);
857
+ const subcommand = parsedArgs._[0];
858
+
859
+ if (subcommand === 'help' || parsedArgs.help || parsedArgs.h) {
860
+ showHelp();
861
+ return;
862
+ }
863
+
864
+ const cfg = config.load();
865
+ const projectRoot = cfg._projectRoot;
866
+
867
+ const options: RunWatchOptions = {
868
+ watchTodos: !parsedArgs.workflows && !parsedArgs.prd,
869
+ watchWorkflows: !parsedArgs.todos && !parsedArgs.prd,
870
+ watchPRD: !parsedArgs.todos && !parsedArgs.workflows,
871
+ verbose: parsedArgs.verbose,
872
+ debounceMs: parseInt(parsedArgs.debounce as string, 10) || 300
873
+ };
874
+
875
+ // If specific flags are set, only watch those
876
+ if (parsedArgs.todos) {
877
+ options.watchTodos = true;
878
+ options.watchWorkflows = false;
879
+ options.watchPRD = false;
880
+ }
881
+ if (parsedArgs.workflows) {
882
+ options.watchTodos = false;
883
+ options.watchWorkflows = true;
884
+ options.watchPRD = false;
885
+ }
886
+ if (parsedArgs.prd) {
887
+ options.watchTodos = false;
888
+ options.watchWorkflows = false;
889
+ options.watchPRD = true;
890
+ }
891
+
892
+ await runWatchMode(projectRoot, options);
893
+ }
894
+
895
+ export { FileWatcher, parseTodoFile, diffTodos, getTodoStats };