specsmd 0.1.22 → 0.1.24

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.
@@ -0,0 +1,387 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+
5
+ const {
6
+ normalizeStatus,
7
+ normalizeMode,
8
+ normalizeScope,
9
+ normalizeComplexity,
10
+ normalizeState,
11
+ deriveIntentStatus,
12
+ calculateStats,
13
+ parseDependencies,
14
+ buildPendingItems,
15
+ normalizeRunWorkItem
16
+ } = require('./model');
17
+
18
+ const STANDARD_TYPES = [
19
+ 'constitution',
20
+ 'tech-stack',
21
+ 'coding-standards',
22
+ 'testing-standards',
23
+ 'system-architecture'
24
+ ];
25
+
26
+ function parseFrontmatter(content) {
27
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
28
+ if (!match) {
29
+ return {};
30
+ }
31
+
32
+ try {
33
+ return yaml.load(match[1]) || {};
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function readFileSafe(filePath) {
40
+ try {
41
+ return fs.readFileSync(filePath, 'utf8');
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function listSubdirectories(dirPath) {
48
+ try {
49
+ return fs.readdirSync(dirPath, { withFileTypes: true })
50
+ .filter((entry) => entry.isDirectory())
51
+ .map((entry) => entry.name)
52
+ .sort((a, b) => a.localeCompare(b));
53
+ } catch {
54
+ return [];
55
+ }
56
+ }
57
+
58
+ function listMarkdownFiles(dirPath) {
59
+ try {
60
+ return fs.readdirSync(dirPath)
61
+ .filter((file) => file.endsWith('.md'))
62
+ .sort((a, b) => a.localeCompare(b));
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ function parseRunLog(runLogPath) {
69
+ const content = readFileSafe(runLogPath);
70
+ if (!content) {
71
+ return {
72
+ scope: undefined,
73
+ workItems: [],
74
+ currentItem: null,
75
+ startedAt: undefined,
76
+ completedAt: undefined
77
+ };
78
+ }
79
+
80
+ const frontmatter = parseFrontmatter(content);
81
+ const workItemsRaw = Array.isArray(frontmatter.work_items)
82
+ ? frontmatter.work_items
83
+ : (Array.isArray(frontmatter.workItems) ? frontmatter.workItems : []);
84
+
85
+ const workItems = workItemsRaw
86
+ .map((item) => normalizeRunWorkItem(item))
87
+ .filter((item) => item.id !== '');
88
+
89
+ return {
90
+ scope: normalizeScope(frontmatter.scope),
91
+ workItems,
92
+ currentItem: typeof frontmatter.current_item === 'string'
93
+ ? frontmatter.current_item
94
+ : (typeof frontmatter.currentItem === 'string' ? frontmatter.currentItem : null),
95
+ startedAt: typeof frontmatter.started === 'string' ? frontmatter.started : undefined,
96
+ completedAt: typeof frontmatter.completed === 'string'
97
+ ? frontmatter.completed
98
+ : undefined
99
+ };
100
+ }
101
+
102
+ function scanWorkItems(intentPath, intentId, stateWorkItems, warnings) {
103
+ const workItemsPath = path.join(intentPath, 'work-items');
104
+ const fileWorkItemIds = listMarkdownFiles(workItemsPath)
105
+ .filter((file) => !file.endsWith('-design.md'))
106
+ .map((file) => file.replace(/\.md$/, ''));
107
+
108
+ const stateWorkItemIds = (stateWorkItems || []).map((item) => item.id).filter(Boolean);
109
+ const uniqueIds = Array.from(new Set([...fileWorkItemIds, ...stateWorkItemIds])).sort((a, b) => a.localeCompare(b));
110
+
111
+ const stateMap = new Map((stateWorkItems || []).map((item) => [item.id, item]));
112
+
113
+ return uniqueIds.map((workItemId) => {
114
+ const stateItem = stateMap.get(workItemId);
115
+ const filePath = path.join(workItemsPath, `${workItemId}.md`);
116
+
117
+ let frontmatter = {};
118
+ if (fs.existsSync(filePath)) {
119
+ frontmatter = parseFrontmatter(readFileSafe(filePath) || '');
120
+ } else if (stateItem) {
121
+ warnings.push(`Work item ${intentId}/${workItemId} exists in state.yaml but markdown file is missing.`);
122
+ }
123
+
124
+ const dependencies = parseDependencies(frontmatter.depends_on ?? frontmatter.dependencies);
125
+
126
+ return {
127
+ id: workItemId,
128
+ intentId,
129
+ title: typeof frontmatter.title === 'string' ? frontmatter.title : workItemId,
130
+ status: normalizeStatus(stateItem?.status || frontmatter.status) || 'pending',
131
+ mode: normalizeMode(stateItem?.mode || frontmatter.mode) || 'confirm',
132
+ complexity: normalizeComplexity(frontmatter.complexity) || 'medium',
133
+ filePath,
134
+ description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
135
+ dependencies,
136
+ createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
137
+ completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
138
+ };
139
+ });
140
+ }
141
+
142
+ function scanIntents(rootPath, normalizedState, warnings) {
143
+ const intentsPath = path.join(rootPath, 'intents');
144
+ const dirIntentIds = listSubdirectories(intentsPath);
145
+ const stateIntentIds = (normalizedState.intents || []).map((intent) => intent.id).filter(Boolean);
146
+ const uniqueIntentIds = Array.from(new Set([...dirIntentIds, ...stateIntentIds])).sort((a, b) => a.localeCompare(b));
147
+
148
+ const stateIntentMap = new Map((normalizedState.intents || []).map((intent) => [intent.id, intent]));
149
+
150
+ return uniqueIntentIds.map((intentId) => {
151
+ const stateIntent = stateIntentMap.get(intentId);
152
+ const intentPath = path.join(intentsPath, intentId);
153
+ const briefPath = path.join(intentPath, 'brief.md');
154
+
155
+ let frontmatter = {};
156
+ if (fs.existsSync(briefPath)) {
157
+ frontmatter = parseFrontmatter(readFileSafe(briefPath) || '');
158
+ } else if (stateIntent) {
159
+ warnings.push(`Intent ${intentId} exists in state.yaml but brief.md is missing.`);
160
+ }
161
+
162
+ const workItems = scanWorkItems(
163
+ intentPath,
164
+ intentId,
165
+ stateIntent?.workItems || [],
166
+ warnings
167
+ );
168
+
169
+ return {
170
+ id: intentId,
171
+ title: typeof frontmatter.title === 'string'
172
+ ? frontmatter.title
173
+ : (stateIntent?.title || intentId),
174
+ status: deriveIntentStatus(stateIntent?.status, workItems),
175
+ filePath: briefPath,
176
+ description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
177
+ workItems,
178
+ createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
179
+ completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
180
+ };
181
+ });
182
+ }
183
+
184
+ function scanRuns(rootPath, normalizedState) {
185
+ const runsPath = path.join(rootPath, 'runs');
186
+ const runDirs = listSubdirectories(runsPath).filter((name) => name.startsWith('run-'));
187
+
188
+ const stateActiveMap = new Map((normalizedState.runs?.active || []).map((run) => [run.id, run]));
189
+ const stateCompletedMap = new Map((normalizedState.runs?.completed || []).map((run) => [run.id, run]));
190
+
191
+ const runIds = Array.from(new Set([
192
+ ...runDirs,
193
+ ...Array.from(stateActiveMap.keys()),
194
+ ...Array.from(stateCompletedMap.keys())
195
+ ])).sort((a, b) => a.localeCompare(b));
196
+
197
+ return runIds.map((runId) => {
198
+ const folderPath = path.join(runsPath, runId);
199
+ const runLogPath = path.join(folderPath, 'run.md');
200
+ const parsedRunLog = parseRunLog(runLogPath);
201
+
202
+ const stateActiveRun = stateActiveMap.get(runId);
203
+ const stateCompletedRun = stateCompletedMap.get(runId);
204
+
205
+ const workItems = (stateActiveRun?.workItems && stateActiveRun.workItems.length > 0)
206
+ ? stateActiveRun.workItems
207
+ : ((stateCompletedRun?.workItems && stateCompletedRun.workItems.length > 0)
208
+ ? stateCompletedRun.workItems
209
+ : parsedRunLog.workItems);
210
+
211
+ const completedAt = stateCompletedRun?.completed || parsedRunLog.completedAt || undefined;
212
+
213
+ return {
214
+ id: runId,
215
+ scope: stateActiveRun?.scope || parsedRunLog.scope || 'single',
216
+ workItems,
217
+ currentItem: stateActiveRun?.currentItem || parsedRunLog.currentItem || null,
218
+ folderPath,
219
+ startedAt: stateActiveRun?.started || parsedRunLog.startedAt || '',
220
+ completedAt: completedAt === 'null' ? undefined : completedAt,
221
+ hasPlan: fs.existsSync(path.join(folderPath, 'plan.md')),
222
+ hasWalkthrough: fs.existsSync(path.join(folderPath, 'walkthrough.md')),
223
+ hasTestReport: fs.existsSync(path.join(folderPath, 'test-report.md'))
224
+ };
225
+ });
226
+ }
227
+
228
+ function scanStandards(rootPath) {
229
+ const standardsPath = path.join(rootPath, 'standards');
230
+
231
+ return STANDARD_TYPES
232
+ .filter((type) => fs.existsSync(path.join(standardsPath, `${type}.md`)))
233
+ .map((type) => ({
234
+ type,
235
+ filePath: path.join(standardsPath, `${type}.md`),
236
+ scope: 'root'
237
+ }));
238
+ }
239
+
240
+ function buildActiveRuns(runs, normalizedState) {
241
+ const byId = new Map((runs || []).map((run) => [run.id, run]));
242
+
243
+ return (normalizedState.runs?.active || [])
244
+ .map((active) => byId.get(active.id) || null)
245
+ .filter(Boolean);
246
+ }
247
+
248
+ function buildCompletedRuns(runs) {
249
+ return (runs || [])
250
+ .filter((run) => run.completedAt != null)
251
+ .sort((a, b) => {
252
+ const aTime = a.completedAt ? Date.parse(a.completedAt) : 0;
253
+ const bTime = b.completedAt ? Date.parse(b.completedAt) : 0;
254
+ if (bTime !== aTime) {
255
+ return bTime - aTime;
256
+ }
257
+ return b.id.localeCompare(a.id);
258
+ });
259
+ }
260
+
261
+ function createUninitializedSnapshot(workspacePath, rootPath) {
262
+ return {
263
+ flow: 'fire',
264
+ isProject: true,
265
+ initialized: false,
266
+ workspacePath,
267
+ rootPath,
268
+ version: '0.0.0',
269
+ project: null,
270
+ workspace: null,
271
+ intents: [],
272
+ runs: [],
273
+ activeRuns: [],
274
+ completedRuns: [],
275
+ pendingItems: [],
276
+ standards: scanStandards(rootPath),
277
+ stats: {
278
+ totalIntents: 0,
279
+ completedIntents: 0,
280
+ inProgressIntents: 0,
281
+ pendingIntents: 0,
282
+ blockedIntents: 0,
283
+ totalWorkItems: 0,
284
+ completedWorkItems: 0,
285
+ inProgressWorkItems: 0,
286
+ pendingWorkItems: 0,
287
+ blockedWorkItems: 0,
288
+ totalRuns: 0,
289
+ completedRuns: 0,
290
+ activeRunsCount: 0
291
+ },
292
+ warnings: ['FIRE folder exists but state.yaml has not been created yet.'],
293
+ generatedAt: new Date().toISOString()
294
+ };
295
+ }
296
+
297
+ function parseStateFile(statePath) {
298
+ const content = readFileSafe(statePath);
299
+ if (content == null) {
300
+ throw new Error('Unable to read state.yaml');
301
+ }
302
+
303
+ const parsed = yaml.load(content);
304
+ if (!parsed || typeof parsed !== 'object') {
305
+ throw new Error('state.yaml is empty or invalid.');
306
+ }
307
+
308
+ return parsed;
309
+ }
310
+
311
+ function parseFireDashboard(workspacePath) {
312
+ const rootPath = path.join(workspacePath, '.specs-fire');
313
+
314
+ if (!fs.existsSync(rootPath) || !fs.statSync(rootPath).isDirectory()) {
315
+ return {
316
+ ok: false,
317
+ error: {
318
+ code: 'FIRE_NOT_FOUND',
319
+ message: `No FIRE workspace found at ${rootPath}`,
320
+ hint: 'Install FIRE flow or run this command from a FIRE project root.'
321
+ }
322
+ };
323
+ }
324
+
325
+ const statePath = path.join(rootPath, 'state.yaml');
326
+ if (!fs.existsSync(statePath)) {
327
+ return {
328
+ ok: true,
329
+ snapshot: createUninitializedSnapshot(workspacePath, rootPath)
330
+ };
331
+ }
332
+
333
+ let rawState;
334
+ try {
335
+ rawState = parseStateFile(statePath);
336
+ } catch (error) {
337
+ return {
338
+ ok: false,
339
+ error: {
340
+ code: 'STATE_PARSE_ERROR',
341
+ message: `Failed to parse ${statePath}`,
342
+ details: error.message,
343
+ path: statePath
344
+ }
345
+ };
346
+ }
347
+
348
+ const warnings = [];
349
+ const normalizedState = normalizeState(rawState);
350
+ const intents = scanIntents(rootPath, normalizedState, warnings);
351
+ const runs = scanRuns(rootPath, normalizedState);
352
+ const activeRuns = buildActiveRuns(runs, normalizedState);
353
+ const completedRuns = buildCompletedRuns(runs);
354
+ const standards = scanStandards(rootPath);
355
+ const pendingItems = buildPendingItems(intents);
356
+ const stats = calculateStats(intents, runs, activeRuns);
357
+
358
+ return {
359
+ ok: true,
360
+ snapshot: {
361
+ flow: 'fire',
362
+ isProject: true,
363
+ initialized: true,
364
+ workspacePath,
365
+ rootPath,
366
+ version: normalizedState.project?.fireVersion || '0.0.0',
367
+ project: normalizedState.project,
368
+ workspace: normalizedState.workspace,
369
+ intents,
370
+ runs,
371
+ activeRuns,
372
+ completedRuns,
373
+ pendingItems,
374
+ standards,
375
+ stats,
376
+ warnings,
377
+ generatedAt: new Date().toISOString()
378
+ }
379
+ };
380
+ }
381
+
382
+ module.exports = {
383
+ STANDARD_TYPES,
384
+ parseFrontmatter,
385
+ parseRunLog,
386
+ parseFireDashboard
387
+ };
@@ -0,0 +1,86 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const SUPPORTED_FLOWS = ['fire', 'aidlc', 'simple'];
5
+
6
+ function directoryExists(dirPath) {
7
+ try {
8
+ return fs.statSync(dirPath).isDirectory();
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ function getFlowMarkerPath(workspacePath, flow) {
15
+ switch (flow) {
16
+ case 'fire':
17
+ return path.join(workspacePath, '.specs-fire');
18
+ case 'aidlc':
19
+ return path.join(workspacePath, 'memory-bank');
20
+ case 'simple':
21
+ return path.join(workspacePath, 'specs');
22
+ default:
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function detectAvailableFlows(workspacePath) {
28
+ return SUPPORTED_FLOWS.filter((flow) => {
29
+ const markerPath = getFlowMarkerPath(workspacePath, flow);
30
+ return markerPath && directoryExists(markerPath);
31
+ });
32
+ }
33
+
34
+ function detectFlow(workspacePath, explicitFlow) {
35
+ const availableFlows = detectAvailableFlows(workspacePath);
36
+
37
+ if (explicitFlow) {
38
+ if (!SUPPORTED_FLOWS.includes(explicitFlow)) {
39
+ const valid = SUPPORTED_FLOWS.join(', ');
40
+ throw new Error(`Invalid flow \"${explicitFlow}\". Valid options: ${valid}`);
41
+ }
42
+
43
+ const markerPath = getFlowMarkerPath(workspacePath, explicitFlow);
44
+ const exists = markerPath ? directoryExists(markerPath) : false;
45
+
46
+ return {
47
+ flow: explicitFlow,
48
+ source: 'flag',
49
+ markerPath,
50
+ detected: exists,
51
+ availableFlows,
52
+ warning: exists
53
+ ? undefined
54
+ : `Flow \"${explicitFlow}\" was selected explicitly but ${markerPath} was not found.`
55
+ };
56
+ }
57
+
58
+ for (const flow of SUPPORTED_FLOWS) {
59
+ const markerPath = getFlowMarkerPath(workspacePath, flow);
60
+ if (markerPath && directoryExists(markerPath)) {
61
+ return {
62
+ flow,
63
+ source: 'auto',
64
+ markerPath,
65
+ detected: true,
66
+ availableFlows
67
+ };
68
+ }
69
+ }
70
+
71
+ return {
72
+ flow: null,
73
+ source: 'auto',
74
+ markerPath: null,
75
+ detected: false,
76
+ availableFlows
77
+ };
78
+ }
79
+
80
+ module.exports = {
81
+ SUPPORTED_FLOWS,
82
+ directoryExists,
83
+ getFlowMarkerPath,
84
+ detectAvailableFlows,
85
+ detectFlow
86
+ };
@@ -0,0 +1,134 @@
1
+ const path = require('path');
2
+ const { detectFlow } = require('./flow-detect');
3
+ const { parseFireDashboard } = require('./fire/parser');
4
+ const { formatDashboardText } = require('./tui/renderer');
5
+ const { createDashboardApp } = require('./tui/app');
6
+
7
+ function parseRefreshMs(raw) {
8
+ const parsed = Number.parseInt(String(raw || '1000'), 10);
9
+ if (Number.isNaN(parsed)) {
10
+ return 1000;
11
+ }
12
+
13
+ return Math.max(200, Math.min(parsed, 5000));
14
+ }
15
+
16
+ function getUnsupportedFlowMessage(flow) {
17
+ if (flow === 'aidlc') {
18
+ return 'AI-DLC dashboard is coming soon. FIRE dashboard is available now.';
19
+ }
20
+
21
+ if (flow === 'simple') {
22
+ return 'Simple flow dashboard is coming soon. FIRE dashboard is available now.';
23
+ }
24
+
25
+ return `Flow \"${flow}\" dashboard is not available yet.`;
26
+ }
27
+
28
+ async function runFireDashboard(options) {
29
+ const workspacePath = path.resolve(options.path || process.cwd());
30
+ const rootPath = path.join(workspacePath, '.specs-fire');
31
+ const watchEnabled = options.watch !== false;
32
+ const refreshMs = parseRefreshMs(options.refreshMs);
33
+
34
+ const parseSnapshot = async () => parseFireDashboard(workspacePath);
35
+
36
+ const initialResult = await parseSnapshot();
37
+
38
+ if (!watchEnabled) {
39
+ if (!initialResult.ok) {
40
+ const output = formatDashboardText({
41
+ snapshot: null,
42
+ error: initialResult.error,
43
+ flow: 'fire',
44
+ workspacePath,
45
+ view: 'runs',
46
+ runFilter: 'all',
47
+ watchEnabled: false,
48
+ watchStatus: 'off',
49
+ showHelp: true,
50
+ lastRefreshAt: new Date().toISOString(),
51
+ width: process.stdout.columns || 120
52
+ });
53
+ console.log(output);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+
58
+ const output = formatDashboardText({
59
+ snapshot: initialResult.snapshot,
60
+ error: null,
61
+ flow: 'fire',
62
+ workspacePath,
63
+ view: 'runs',
64
+ runFilter: 'all',
65
+ watchEnabled: false,
66
+ watchStatus: 'off',
67
+ showHelp: true,
68
+ lastRefreshAt: new Date().toISOString(),
69
+ width: process.stdout.columns || 120
70
+ });
71
+ console.log(output);
72
+ return;
73
+ }
74
+
75
+ const ink = await import('ink');
76
+ const reactNamespace = await import('react');
77
+ const React = reactNamespace.default || reactNamespace;
78
+
79
+ const App = createDashboardApp({
80
+ React,
81
+ ink,
82
+ parseSnapshot,
83
+ workspacePath,
84
+ rootPath,
85
+ flow: 'fire',
86
+ refreshMs,
87
+ watchEnabled,
88
+ initialSnapshot: initialResult.ok ? initialResult.snapshot : null,
89
+ initialError: initialResult.ok ? null : initialResult.error
90
+ });
91
+
92
+ const { waitUntilExit } = ink.render(React.createElement(App), {
93
+ exitOnCtrlC: true
94
+ });
95
+
96
+ await waitUntilExit();
97
+ }
98
+
99
+ async function run(options = {}) {
100
+ const workspacePath = path.resolve(options.path || process.cwd());
101
+
102
+ let detection;
103
+ try {
104
+ detection = detectFlow(workspacePath, options.flow);
105
+ } catch (error) {
106
+ console.error(error.message);
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+
111
+ if (!detection.flow) {
112
+ console.error('No supported flow detected. Expected one of: .specs-fire, memory-bank, specs');
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+
117
+ if (detection.warning) {
118
+ console.warn(`Warning: ${detection.warning}`);
119
+ }
120
+
121
+ if (detection.flow !== 'fire') {
122
+ console.log(getUnsupportedFlowMessage(detection.flow));
123
+ return;
124
+ }
125
+
126
+ await runFireDashboard(options);
127
+ }
128
+
129
+ module.exports = {
130
+ run,
131
+ runFireDashboard,
132
+ parseRefreshMs,
133
+ getUnsupportedFlowMessage
134
+ };
@@ -0,0 +1,113 @@
1
+ const chokidar = require('chokidar');
2
+ const path = require('path');
3
+
4
+ function createDebouncedTrigger(callback, delayMs) {
5
+ let timeoutId = null;
6
+
7
+ const trigger = () => {
8
+ if (timeoutId != null) {
9
+ clearTimeout(timeoutId);
10
+ }
11
+
12
+ timeoutId = setTimeout(() => {
13
+ timeoutId = null;
14
+ callback();
15
+ }, delayMs);
16
+ };
17
+
18
+ const cancel = () => {
19
+ if (timeoutId != null) {
20
+ clearTimeout(timeoutId);
21
+ timeoutId = null;
22
+ }
23
+ };
24
+
25
+ return {
26
+ trigger,
27
+ cancel,
28
+ isPending: () => timeoutId != null
29
+ };
30
+ }
31
+
32
+ function createWatchRuntime(options) {
33
+ const {
34
+ rootPath,
35
+ onRefresh,
36
+ onError,
37
+ debounceMs = 250
38
+ } = options;
39
+
40
+ if (!rootPath || typeof rootPath !== 'string') {
41
+ throw new Error('rootPath is required for watch runtime');
42
+ }
43
+
44
+ if (typeof onRefresh !== 'function') {
45
+ throw new Error('onRefresh callback is required for watch runtime');
46
+ }
47
+
48
+ const reportError = typeof onError === 'function' ? onError : () => {};
49
+
50
+ let watcher = null;
51
+ let started = false;
52
+ const debounced = createDebouncedTrigger(onRefresh, debounceMs);
53
+
54
+ const watchTargets = [
55
+ path.join(rootPath, 'state.yaml'),
56
+ path.join(rootPath, 'intents'),
57
+ path.join(rootPath, 'runs'),
58
+ path.join(rootPath, 'standards')
59
+ ];
60
+
61
+ function start() {
62
+ if (started) {
63
+ return;
64
+ }
65
+
66
+ started = true;
67
+
68
+ try {
69
+ watcher = chokidar.watch(watchTargets, {
70
+ persistent: true,
71
+ ignoreInitial: true,
72
+ awaitWriteFinish: {
73
+ stabilityThreshold: 150,
74
+ pollInterval: 50
75
+ },
76
+ depth: 10
77
+ });
78
+
79
+ watcher.on('all', () => {
80
+ debounced.trigger();
81
+ });
82
+
83
+ watcher.on('error', (error) => {
84
+ reportError(error);
85
+ });
86
+ } catch (error) {
87
+ reportError(error);
88
+ }
89
+ }
90
+
91
+ async function close() {
92
+ debounced.cancel();
93
+
94
+ if (watcher) {
95
+ const activeWatcher = watcher;
96
+ watcher = null;
97
+ started = false;
98
+ await activeWatcher.close();
99
+ }
100
+ }
101
+
102
+ return {
103
+ start,
104
+ close,
105
+ isActive: () => started,
106
+ hasPendingRefresh: () => debounced.isPending()
107
+ };
108
+ }
109
+
110
+ module.exports = {
111
+ createDebouncedTrigger,
112
+ createWatchRuntime
113
+ };