@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.
- package/bin/bootspring.js +5 -0
- package/cli/org.js +474 -0
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/preseed.js +9 -301
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/cli/seed.js +23 -1074
- package/core/api-client.js +77 -0
- package/core/entitlements.js +36 -0
- package/core/organizations.js +223 -0
- package/core/policies.js +51 -6
- package/core/policy-matrix.js +303 -0
- package/core/project-context.js +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +23 -0
- package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/intelligence/orchestrator.js +17 -512
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- package/generators/index.js +0 -18
package/src/cli/watch.ts
ADDED
|
@@ -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 };
|