agileflow 2.90.7 → 2.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +6 -6
  3. package/lib/codebase-indexer.js +810 -0
  4. package/lib/validate-names.js +3 -3
  5. package/package.json +4 -1
  6. package/scripts/obtain-context.js +238 -0
  7. package/scripts/precompact-context.sh +13 -1
  8. package/scripts/query-codebase.js +430 -0
  9. package/scripts/tui/blessed/data/watcher.js +175 -0
  10. package/scripts/tui/blessed/index.js +244 -0
  11. package/scripts/tui/blessed/panels/output.js +95 -0
  12. package/scripts/tui/blessed/panels/sessions.js +143 -0
  13. package/scripts/tui/blessed/panels/trace.js +91 -0
  14. package/scripts/tui/blessed/ui/help.js +77 -0
  15. package/scripts/tui/blessed/ui/screen.js +52 -0
  16. package/scripts/tui/blessed/ui/statusbar.js +51 -0
  17. package/scripts/tui/blessed/ui/tabbar.js +99 -0
  18. package/scripts/tui/index.js +38 -30
  19. package/scripts/validators/README.md +143 -0
  20. package/scripts/validators/component-validator.js +212 -0
  21. package/scripts/validators/json-schema-validator.js +179 -0
  22. package/scripts/validators/markdown-validator.js +153 -0
  23. package/scripts/validators/migration-validator.js +117 -0
  24. package/scripts/validators/security-validator.js +276 -0
  25. package/scripts/validators/story-format-validator.js +176 -0
  26. package/scripts/validators/test-result-validator.js +99 -0
  27. package/scripts/validators/workflow-validator.js +240 -0
  28. package/src/core/agents/accessibility.md +6 -0
  29. package/src/core/agents/adr-writer.md +6 -0
  30. package/src/core/agents/analytics.md +6 -0
  31. package/src/core/agents/api.md +6 -0
  32. package/src/core/agents/ci.md +6 -0
  33. package/src/core/agents/codebase-query.md +237 -0
  34. package/src/core/agents/compliance.md +6 -0
  35. package/src/core/agents/configuration-damage-control.md +6 -0
  36. package/src/core/agents/configuration-visual-e2e.md +6 -0
  37. package/src/core/agents/database.md +10 -0
  38. package/src/core/agents/datamigration.md +6 -0
  39. package/src/core/agents/design.md +6 -0
  40. package/src/core/agents/devops.md +6 -0
  41. package/src/core/agents/documentation.md +6 -0
  42. package/src/core/agents/epic-planner.md +6 -0
  43. package/src/core/agents/integrations.md +6 -0
  44. package/src/core/agents/mentor.md +6 -0
  45. package/src/core/agents/mobile.md +6 -0
  46. package/src/core/agents/monitoring.md +6 -0
  47. package/src/core/agents/multi-expert.md +6 -0
  48. package/src/core/agents/performance.md +6 -0
  49. package/src/core/agents/product.md +6 -0
  50. package/src/core/agents/qa.md +6 -0
  51. package/src/core/agents/readme-updater.md +6 -0
  52. package/src/core/agents/refactor.md +6 -0
  53. package/src/core/agents/research.md +6 -0
  54. package/src/core/agents/security.md +6 -0
  55. package/src/core/agents/testing.md +10 -0
  56. package/src/core/agents/ui.md +6 -0
  57. package/src/core/commands/audit.md +401 -0
  58. package/src/core/commands/board.md +1 -0
  59. package/src/core/commands/epic.md +92 -1
  60. package/src/core/commands/help.md +1 -0
  61. package/src/core/commands/metrics.md +1 -0
  62. package/src/core/commands/research/analyze.md +1 -0
  63. package/src/core/commands/research/ask.md +2 -0
  64. package/src/core/commands/research/import.md +1 -0
  65. package/src/core/commands/research/list.md +2 -0
  66. package/src/core/commands/research/synthesize.md +584 -0
  67. package/src/core/commands/research/view.md +2 -0
  68. package/src/core/commands/status.md +126 -1
  69. package/src/core/commands/story/list.md +9 -9
  70. package/src/core/commands/story/view.md +1 -0
  71. package/src/core/experts/codebase-query/expertise.yaml +190 -0
  72. package/src/core/experts/codebase-query/question.md +73 -0
  73. package/src/core/experts/codebase-query/self-improve.md +105 -0
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * query-codebase.js
4
+ *
5
+ * Query engine for programmatic codebase searches.
6
+ * Part of the RLM-inspired Codebase Query Interface (EP-0021).
7
+ *
8
+ * Uses indexing and programmatic search instead of loading full context,
9
+ * following RLM principles: virtualize documents and query programmatically.
10
+ *
11
+ * Usage:
12
+ * node query-codebase.js --build-index # Build/rebuild index
13
+ * node query-codebase.js --query="auth files" # Search by pattern/keyword
14
+ * node query-codebase.js --deps="src/api.js" # Show dependencies
15
+ * node query-codebase.js --content="validate" # Search file content
16
+ * node query-codebase.js --tag="api" # Search by tag
17
+ * node query-codebase.js --export="login" # Find export locations
18
+ *
19
+ * Options:
20
+ * --project=<path> Project root (default: cwd)
21
+ * --budget=<chars> Token budget for output (default: 15000)
22
+ * --json Output as JSON
23
+ * --verbose Show debug info
24
+ *
25
+ * Exit codes:
26
+ * 0 = Success
27
+ * 1 = Error
28
+ * 2 = No results
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const {
34
+ buildIndex,
35
+ updateIndex,
36
+ getIndex,
37
+ queryFiles,
38
+ queryByTag,
39
+ queryByExport,
40
+ getDependencies,
41
+ } = require('../lib/codebase-indexer');
42
+
43
+ // Default configuration
44
+ const DEFAULT_BUDGET = 15000;
45
+
46
+ // Parse command line arguments
47
+ function parseArgs(argv) {
48
+ const args = {
49
+ buildIndex: false,
50
+ query: null,
51
+ deps: null,
52
+ content: null,
53
+ tag: null,
54
+ export: null,
55
+ project: process.cwd(),
56
+ budget: DEFAULT_BUDGET,
57
+ json: false,
58
+ verbose: false,
59
+ };
60
+
61
+ for (const arg of argv.slice(2)) {
62
+ if (arg === '--build-index') {
63
+ args.buildIndex = true;
64
+ } else if (arg === '--json') {
65
+ args.json = true;
66
+ } else if (arg === '--verbose') {
67
+ args.verbose = true;
68
+ } else if (arg.startsWith('--query=')) {
69
+ args.query = arg.slice(8);
70
+ } else if (arg.startsWith('--deps=')) {
71
+ args.deps = arg.slice(7);
72
+ } else if (arg.startsWith('--content=')) {
73
+ args.content = arg.slice(10);
74
+ } else if (arg.startsWith('--tag=')) {
75
+ args.tag = arg.slice(6);
76
+ } else if (arg.startsWith('--export=')) {
77
+ args.export = arg.slice(9);
78
+ } else if (arg.startsWith('--project=')) {
79
+ args.project = arg.slice(10);
80
+ } else if (arg.startsWith('--budget=')) {
81
+ args.budget = parseInt(arg.slice(9), 10) || DEFAULT_BUDGET;
82
+ }
83
+ }
84
+
85
+ return args;
86
+ }
87
+
88
+ // Search file content using grep-style regex
89
+ function queryContent(projectRoot, pattern, budget) {
90
+ const results = [];
91
+ let totalChars = 0;
92
+
93
+ // Get list of files to search
94
+ const indexResult = getIndex(projectRoot);
95
+ if (!indexResult.ok) {
96
+ return { ok: false, error: indexResult.error };
97
+ }
98
+
99
+ const regex = new RegExp(pattern, 'gi');
100
+ const files = Object.keys(indexResult.data.files);
101
+
102
+ for (const relativePath of files) {
103
+ const fullPath = path.join(projectRoot, relativePath);
104
+ const fileType = indexResult.data.files[relativePath].type;
105
+
106
+ // Only search code files
107
+ if (!['javascript', 'typescript', 'javascript-react', 'typescript-react'].includes(fileType)) {
108
+ continue;
109
+ }
110
+
111
+ try {
112
+ const content = fs.readFileSync(fullPath, 'utf8');
113
+ const lines = content.split('\n');
114
+ const matches = [];
115
+
116
+ for (let i = 0; i < lines.length; i++) {
117
+ if (regex.test(lines[i])) {
118
+ // Include context (2 lines before/after)
119
+ const start = Math.max(0, i - 2);
120
+ const end = Math.min(lines.length - 1, i + 2);
121
+ const context = lines.slice(start, end + 1).map((line, idx) => ({
122
+ lineNumber: start + idx + 1,
123
+ content: line,
124
+ isMatch: start + idx === i,
125
+ }));
126
+ matches.push({ line: i + 1, context });
127
+ }
128
+ regex.lastIndex = 0; // Reset regex state
129
+ }
130
+
131
+ if (matches.length > 0) {
132
+ const result = { file: relativePath, matches };
133
+ const resultChars = JSON.stringify(result).length;
134
+
135
+ if (totalChars + resultChars > budget) {
136
+ results.push({
137
+ file: '...',
138
+ matches: [{ line: 0, context: [{ lineNumber: 0, content: `[Truncated: budget exceeded]`, isMatch: false }] }],
139
+ });
140
+ break;
141
+ }
142
+
143
+ results.push(result);
144
+ totalChars += resultChars;
145
+ }
146
+ } catch (err) {
147
+ // Skip unreadable files
148
+ }
149
+ }
150
+
151
+ return { ok: true, data: results };
152
+ }
153
+
154
+ // Format output for human readability
155
+ function formatResults(results, type) {
156
+ const lines = [];
157
+
158
+ switch (type) {
159
+ case 'files':
160
+ lines.push(`Found ${results.length} file(s):`);
161
+ for (const file of results) {
162
+ lines.push(` ${file}`);
163
+ }
164
+ break;
165
+
166
+ case 'content':
167
+ lines.push(`Found matches in ${results.length} file(s):`);
168
+ for (const result of results) {
169
+ lines.push(`\n${result.file}:`);
170
+ for (const match of result.matches) {
171
+ for (const ctx of match.context) {
172
+ const marker = ctx.isMatch ? '>' : ' ';
173
+ lines.push(`${marker} ${ctx.lineNumber}: ${ctx.content}`);
174
+ }
175
+ lines.push(' ---');
176
+ }
177
+ }
178
+ break;
179
+
180
+ case 'deps':
181
+ lines.push('Dependencies:');
182
+ if (results.imports.length > 0) {
183
+ lines.push('\nImports:');
184
+ for (const imp of results.imports) {
185
+ lines.push(` ${imp}`);
186
+ }
187
+ }
188
+ if (results.importedBy.length > 0) {
189
+ lines.push('\nImported by:');
190
+ for (const dep of results.importedBy) {
191
+ lines.push(` ${dep}`);
192
+ }
193
+ }
194
+ break;
195
+
196
+ case 'index':
197
+ lines.push('Index Statistics:');
198
+ lines.push(` Total files: ${results.stats.total_files}`);
199
+ lines.push(` Indexed files: ${results.stats.indexed_files}`);
200
+ lines.push(` Build time: ${results.stats.build_time_ms}ms`);
201
+ lines.push(` Tags: ${Object.keys(results.tags).length}`);
202
+ lines.push(` Exports tracked: ${Object.keys(results.symbols.exports).length}`);
203
+ break;
204
+
205
+ default:
206
+ lines.push(JSON.stringify(results, null, 2));
207
+ }
208
+
209
+ return lines.join('\n');
210
+ }
211
+
212
+ // Truncate output to budget
213
+ function truncateOutput(output, budget) {
214
+ if (output.length <= budget) {
215
+ return output;
216
+ }
217
+
218
+ const truncated = output.slice(0, budget - 50);
219
+ const lastNewline = truncated.lastIndexOf('\n');
220
+ return truncated.slice(0, lastNewline) + '\n\n... [Truncated: output exceeded budget]';
221
+ }
222
+
223
+ // Main execution
224
+ async function main() {
225
+ const args = parseArgs(process.argv);
226
+
227
+ if (args.verbose) {
228
+ console.error('Args:', JSON.stringify(args, null, 2));
229
+ }
230
+
231
+ // Check project exists
232
+ if (!fs.existsSync(args.project)) {
233
+ console.error(`Error: Project not found: ${args.project}`);
234
+ process.exit(1);
235
+ }
236
+
237
+ let result;
238
+ let outputType;
239
+
240
+ try {
241
+ // Handle --build-index
242
+ if (args.buildIndex) {
243
+ if (args.verbose) console.error('Building index...');
244
+ result = buildIndex(args.project);
245
+ outputType = 'index';
246
+
247
+ if (!result.ok) {
248
+ console.error(`Error: ${result.error}`);
249
+ process.exit(1);
250
+ }
251
+
252
+ const output = args.json
253
+ ? JSON.stringify(result.data, null, 2)
254
+ : formatResults(result.data, outputType);
255
+ console.log(truncateOutput(output, args.budget));
256
+ process.exit(0);
257
+ }
258
+
259
+ // Handle --query (glob pattern search)
260
+ if (args.query) {
261
+ if (args.verbose) console.error(`Querying: ${args.query}`);
262
+
263
+ // First try to get/update index
264
+ const indexResult = updateIndex(args.project);
265
+ if (!indexResult.ok) {
266
+ console.error(`Error: ${indexResult.error}`);
267
+ process.exit(1);
268
+ }
269
+
270
+ // Interpret query
271
+ const query = args.query.toLowerCase();
272
+ let files = [];
273
+
274
+ // If query looks like a glob, use it directly
275
+ if (query.includes('*') || query.includes('/')) {
276
+ files = queryFiles(indexResult.data, args.query);
277
+ } else {
278
+ // Otherwise, search multiple ways:
279
+ // 1. Files containing query in name
280
+ files = queryFiles(indexResult.data, `**/*${args.query}*`);
281
+
282
+ // 2. Files with matching tag
283
+ const tagFiles = queryByTag(indexResult.data, query);
284
+ files = [...new Set([...files, ...tagFiles])];
285
+
286
+ // 3. Files exporting symbol
287
+ const exportFiles = queryByExport(indexResult.data, args.query);
288
+ files = [...new Set([...files, ...exportFiles])];
289
+ }
290
+
291
+ if (files.length === 0) {
292
+ console.error(`No files found matching: ${args.query}`);
293
+ process.exit(2);
294
+ }
295
+
296
+ const output = args.json
297
+ ? JSON.stringify(files, null, 2)
298
+ : formatResults(files, 'files');
299
+ console.log(truncateOutput(output, args.budget));
300
+ process.exit(0);
301
+ }
302
+
303
+ // Handle --content (grep-style search)
304
+ if (args.content) {
305
+ if (args.verbose) console.error(`Searching content: ${args.content}`);
306
+
307
+ result = queryContent(args.project, args.content, args.budget);
308
+
309
+ if (!result.ok) {
310
+ console.error(`Error: ${result.error}`);
311
+ process.exit(1);
312
+ }
313
+
314
+ if (result.data.length === 0) {
315
+ console.error(`No content matches for: ${args.content}`);
316
+ process.exit(2);
317
+ }
318
+
319
+ const output = args.json
320
+ ? JSON.stringify(result.data, null, 2)
321
+ : formatResults(result.data, 'content');
322
+ console.log(truncateOutput(output, args.budget));
323
+ process.exit(0);
324
+ }
325
+
326
+ // Handle --tag
327
+ if (args.tag) {
328
+ if (args.verbose) console.error(`Searching tag: ${args.tag}`);
329
+
330
+ const indexResult = getIndex(args.project);
331
+ if (!indexResult.ok) {
332
+ console.error(`Error: ${indexResult.error}`);
333
+ process.exit(1);
334
+ }
335
+
336
+ const files = queryByTag(indexResult.data, args.tag);
337
+
338
+ if (files.length === 0) {
339
+ console.error(`No files with tag: ${args.tag}`);
340
+ process.exit(2);
341
+ }
342
+
343
+ const output = args.json
344
+ ? JSON.stringify(files, null, 2)
345
+ : formatResults(files, 'files');
346
+ console.log(truncateOutput(output, args.budget));
347
+ process.exit(0);
348
+ }
349
+
350
+ // Handle --export
351
+ if (args.export) {
352
+ if (args.verbose) console.error(`Searching export: ${args.export}`);
353
+
354
+ const indexResult = getIndex(args.project);
355
+ if (!indexResult.ok) {
356
+ console.error(`Error: ${indexResult.error}`);
357
+ process.exit(1);
358
+ }
359
+
360
+ const files = queryByExport(indexResult.data, args.export);
361
+
362
+ if (files.length === 0) {
363
+ console.error(`No files export: ${args.export}`);
364
+ process.exit(2);
365
+ }
366
+
367
+ const output = args.json
368
+ ? JSON.stringify(files, null, 2)
369
+ : formatResults(files, 'files');
370
+ console.log(truncateOutput(output, args.budget));
371
+ process.exit(0);
372
+ }
373
+
374
+ // Handle --deps
375
+ if (args.deps) {
376
+ if (args.verbose) console.error(`Getting dependencies: ${args.deps}`);
377
+
378
+ const indexResult = getIndex(args.project);
379
+ if (!indexResult.ok) {
380
+ console.error(`Error: ${indexResult.error}`);
381
+ process.exit(1);
382
+ }
383
+
384
+ const deps = getDependencies(indexResult.data, args.deps);
385
+
386
+ if (deps.imports.length === 0 && deps.importedBy.length === 0) {
387
+ console.error(`No dependencies found for: ${args.deps}`);
388
+ process.exit(2);
389
+ }
390
+
391
+ const output = args.json
392
+ ? JSON.stringify(deps, null, 2)
393
+ : formatResults(deps, 'deps');
394
+ console.log(truncateOutput(output, args.budget));
395
+ process.exit(0);
396
+ }
397
+
398
+ // No action specified - show help
399
+ console.log(`Usage: node query-codebase.js <command>
400
+
401
+ Commands:
402
+ --build-index Build/rebuild codebase index
403
+ --query="<pattern>" Search files by pattern or keyword
404
+ --content="<regex>" Search file content (grep-style)
405
+ --tag="<tag>" Find files by tag (api, ui, auth, etc.)
406
+ --export="<name>" Find files exporting a symbol
407
+ --deps="<file>" Show file dependencies
408
+
409
+ Options:
410
+ --project=<path> Project root (default: cwd)
411
+ --budget=<chars> Output budget in characters (default: 15000)
412
+ --json Output as JSON
413
+ --verbose Show debug info
414
+
415
+ Exit codes:
416
+ 0 = Success
417
+ 1 = Error
418
+ 2 = No results
419
+ `);
420
+ process.exit(0);
421
+ } catch (err) {
422
+ console.error(`Error: ${err.message}`);
423
+ if (args.verbose) {
424
+ console.error(err.stack);
425
+ }
426
+ process.exit(1);
427
+ }
428
+ }
429
+
430
+ main();
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chokidar = require('chokidar');
6
+
7
+ /**
8
+ * DataWatcher - watches status.json and log files for changes
9
+ * Triggers refresh callback when data updates
10
+ */
11
+ class DataWatcher {
12
+ constructor(state, onUpdate) {
13
+ this.state = state;
14
+ this.onUpdate = onUpdate;
15
+ this.watcher = null;
16
+ this.cwd = process.cwd();
17
+
18
+ // Paths to watch
19
+ this.statusPath = path.join(this.cwd, 'docs/09-agents/status.json');
20
+ this.logPath = path.join(this.cwd, 'docs/09-agents/bus/log.jsonl');
21
+ this.sessionStatePath = path.join(this.cwd, '.agileflow/session-state.json');
22
+ }
23
+
24
+ start() {
25
+ // Initial load
26
+ this.loadStatus();
27
+ this.loadLogs();
28
+ this.loadSessionState();
29
+
30
+ // Set up file watcher
31
+ const watchPaths = [this.statusPath, this.logPath, this.sessionStatePath].filter(p => {
32
+ const dir = path.dirname(p);
33
+ return fs.existsSync(dir);
34
+ });
35
+
36
+ if (watchPaths.length === 0) {
37
+ // No paths to watch yet, check periodically
38
+ this.pollInterval = setInterval(() => {
39
+ this.loadStatus();
40
+ this.loadLogs();
41
+ this.loadSessionState();
42
+ this.onUpdate();
43
+ }, 2000);
44
+ return;
45
+ }
46
+
47
+ this.watcher = chokidar.watch(watchPaths, {
48
+ persistent: true,
49
+ ignoreInitial: true,
50
+ usePolling: false,
51
+ awaitWriteFinish: {
52
+ stabilityThreshold: 100,
53
+ pollInterval: 50
54
+ }
55
+ });
56
+
57
+ this.watcher.on('change', filePath => {
58
+ if (filePath.endsWith('status.json')) {
59
+ this.loadStatus();
60
+ } else if (filePath.endsWith('log.jsonl')) {
61
+ this.loadLogs();
62
+ } else if (filePath.endsWith('session-state.json')) {
63
+ this.loadSessionState();
64
+ }
65
+ this.onUpdate();
66
+ });
67
+
68
+ this.watcher.on('error', err => {
69
+ // Silently handle watcher errors
70
+ });
71
+ }
72
+
73
+ loadStatus() {
74
+ try {
75
+ if (fs.existsSync(this.statusPath)) {
76
+ const content = fs.readFileSync(this.statusPath, 'utf8');
77
+ const data = JSON.parse(content);
78
+
79
+ // Extract stories with in_progress status as "sessions"
80
+ const stories = data.stories || [];
81
+ this.state.sessions = stories
82
+ .filter(s => s.status === 'in_progress')
83
+ .map((s, i) => ({
84
+ id: `S-${i + 1}`,
85
+ story: s.id || s.title || 'Unknown',
86
+ status: 'active',
87
+ duration: this.formatDuration(s.started_at),
88
+ progress: s.progress || '--'
89
+ }));
90
+
91
+ // If no active stories, show all stories summary
92
+ if (this.state.sessions.length === 0) {
93
+ const ready = stories.filter(s => s.status === 'ready').length;
94
+ const completed = stories.filter(s => s.status === 'completed').length;
95
+ this.state.sessions = [{
96
+ id: '--',
97
+ story: `${ready} ready, ${completed} completed`,
98
+ status: 'idle',
99
+ duration: '--',
100
+ progress: '--'
101
+ }];
102
+ }
103
+ }
104
+ } catch (err) {
105
+ // Ignore parse errors
106
+ }
107
+ }
108
+
109
+ loadLogs() {
110
+ try {
111
+ if (fs.existsSync(this.logPath)) {
112
+ const content = fs.readFileSync(this.logPath, 'utf8');
113
+ const lines = content.trim().split('\n').filter(Boolean);
114
+
115
+ // Take last 100 lines
116
+ this.state.logs = lines.slice(-100).map(line => {
117
+ try {
118
+ return JSON.parse(line);
119
+ } catch {
120
+ return { message: line, level: 'info' };
121
+ }
122
+ });
123
+ }
124
+ } catch (err) {
125
+ // Ignore read errors
126
+ }
127
+ }
128
+
129
+ loadSessionState() {
130
+ try {
131
+ if (fs.existsSync(this.sessionStatePath)) {
132
+ const content = fs.readFileSync(this.sessionStatePath, 'utf8');
133
+ const data = JSON.parse(content);
134
+
135
+ // Extract traces from active commands
136
+ if (data.active_commands) {
137
+ this.state.traces = data.active_commands.map((cmd, i) => ({
138
+ action: cmd.command || cmd,
139
+ status: 'running',
140
+ duration: '--',
141
+ details: cmd.args || ''
142
+ }));
143
+ }
144
+ }
145
+ } catch (err) {
146
+ // Ignore errors
147
+ }
148
+ }
149
+
150
+ formatDuration(startedAt) {
151
+ if (!startedAt) return '--';
152
+ try {
153
+ const start = new Date(startedAt);
154
+ const now = new Date();
155
+ const diff = Math.floor((now - start) / 1000);
156
+
157
+ if (diff < 60) return `${diff}s`;
158
+ if (diff < 3600) return `${Math.floor(diff / 60)}m`;
159
+ return `${Math.floor(diff / 3600)}h ${Math.floor((diff % 3600) / 60)}m`;
160
+ } catch {
161
+ return '--';
162
+ }
163
+ }
164
+
165
+ stop() {
166
+ if (this.watcher) {
167
+ this.watcher.close();
168
+ }
169
+ if (this.pollInterval) {
170
+ clearInterval(this.pollInterval);
171
+ }
172
+ }
173
+ }
174
+
175
+ module.exports = DataWatcher;