agileflow 2.99.7 → 3.0.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 (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/cache-provider.js +155 -0
  3. package/lib/codebase-indexer.js +1 -1
  4. package/lib/content-sanitizer.js +1 -0
  5. package/lib/dashboard-protocol.js +25 -0
  6. package/lib/dashboard-server.js +184 -133
  7. package/lib/errors.js +18 -0
  8. package/lib/file-cache.js +1 -1
  9. package/lib/flag-detection.js +11 -20
  10. package/lib/git-operations.js +15 -33
  11. package/lib/merge-operations.js +40 -34
  12. package/lib/process-executor.js +199 -0
  13. package/lib/registry-cache.js +13 -47
  14. package/lib/skill-loader.js +206 -0
  15. package/lib/smart-json-file.js +2 -4
  16. package/package.json +1 -1
  17. package/scripts/agileflow-configure.js +13 -12
  18. package/scripts/agileflow-statusline.sh +30 -0
  19. package/scripts/agileflow-welcome.js +181 -212
  20. package/scripts/auto-self-improve.js +3 -3
  21. package/scripts/claude-smart.sh +67 -0
  22. package/scripts/claude-tmux.sh +248 -170
  23. package/scripts/damage-control-multi-agent.js +227 -0
  24. package/scripts/lib/bus-utils.js +471 -0
  25. package/scripts/lib/configure-detect.js +5 -6
  26. package/scripts/lib/configure-features.js +44 -0
  27. package/scripts/lib/configure-repair.js +5 -6
  28. package/scripts/lib/configure-utils.js +2 -3
  29. package/scripts/lib/context-formatter.js +87 -8
  30. package/scripts/lib/damage-control-utils.js +37 -3
  31. package/scripts/lib/file-lock.js +392 -0
  32. package/scripts/lib/ideation-index.js +2 -5
  33. package/scripts/lib/lifecycle-detector.js +123 -0
  34. package/scripts/lib/process-cleanup.js +55 -81
  35. package/scripts/lib/scale-detector.js +357 -0
  36. package/scripts/lib/signal-detectors.js +779 -0
  37. package/scripts/lib/story-state-machine.js +1 -1
  38. package/scripts/lib/sync-ideation-status.js +2 -3
  39. package/scripts/lib/task-registry.js +7 -1
  40. package/scripts/lib/team-events.js +357 -0
  41. package/scripts/messaging-bridge.js +79 -36
  42. package/scripts/migrate-ideation-index.js +37 -14
  43. package/scripts/obtain-context.js +37 -19
  44. package/scripts/ralph-loop.js +3 -4
  45. package/scripts/smart-detect.js +390 -0
  46. package/scripts/team-manager.js +174 -30
  47. package/src/core/commands/audit.md +13 -11
  48. package/src/core/commands/babysit.md +162 -115
  49. package/src/core/commands/changelog.md +21 -4
  50. package/src/core/commands/configure.md +105 -2
  51. package/src/core/commands/debt.md +12 -2
  52. package/src/core/commands/feedback.md +7 -6
  53. package/src/core/commands/ideate/history.md +1 -1
  54. package/src/core/commands/ideate/new.md +5 -5
  55. package/src/core/commands/logic/audit.md +2 -2
  56. package/src/core/commands/pr.md +7 -6
  57. package/src/core/commands/research/analyze.md +28 -20
  58. package/src/core/commands/research/ask.md +43 -0
  59. package/src/core/commands/research/import.md +29 -21
  60. package/src/core/commands/research/list.md +8 -7
  61. package/src/core/commands/research/synthesize.md +356 -20
  62. package/src/core/commands/research/view.md +8 -5
  63. package/src/core/commands/review.md +24 -6
  64. package/src/core/commands/skill/create.md +34 -0
  65. package/tools/cli/lib/docs-setup.js +4 -0
package/lib/errors.js CHANGED
@@ -538,6 +538,23 @@ function wrapSafeAsync(fn, operationName = 'operation', options = {}) {
538
538
  };
539
539
  }
540
540
 
541
+ /**
542
+ * Execute a function and return its result, or undefined if it throws.
543
+ * Logs suppressed errors when AGILEFLOW_DEBUG=1.
544
+ *
545
+ * @param {Function} fn - Function to execute
546
+ * @param {string} [label] - Optional label for debug logging
547
+ * @returns {*} Function result on success, undefined on error
548
+ */
549
+ function tryOptional(fn, label) {
550
+ try {
551
+ return fn();
552
+ } catch (err) {
553
+ debugLog('tryOptional', { label: label || 'anonymous', error: err.message });
554
+ return undefined;
555
+ }
556
+ }
557
+
541
558
  /**
542
559
  * Safely parse JSON string with optional field validation
543
560
  * @param {string} content - JSON string to parse
@@ -646,6 +663,7 @@ module.exports = {
646
663
  // Utility wrappers
647
664
  wrapSafe,
648
665
  wrapSafeAsync,
666
+ tryOptional,
649
667
 
650
668
  // Debug helper
651
669
  debugLog,
package/lib/file-cache.js CHANGED
@@ -153,7 +153,7 @@ class LRUCache {
153
153
  // Global cache instance (persists across requires in same process)
154
154
  const fileCache = new LRUCache({
155
155
  maxSize: 50,
156
- ttlMs: 30000, // 30 seconds
156
+ ttlMs: 15000, // 15 seconds
157
157
  });
158
158
 
159
159
  /**
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  const fs = require('fs');
14
- const { execFileSync } = require('child_process');
14
+ const { executeCommandSync } = require('./process-executor');
15
15
 
16
16
  /**
17
17
  * Known Claude flags that should be propagated to child sessions
@@ -218,15 +218,11 @@ function detectFromPs() {
218
218
 
219
219
  // Get command line for this PID
220
220
  let cmdline;
221
- try {
222
- cmdline = execFileSync('ps', ['-p', String(pid), '-o', 'args='], {
223
- encoding: 'utf8',
224
- stdio: ['pipe', 'pipe', 'pipe'],
225
- timeout: 1000,
226
- }).trim();
227
- } catch {
228
- break;
229
- }
221
+ const cmdResult = executeCommandSync('ps', ['-p', String(pid), '-o', 'args='], {
222
+ timeout: 1000, fallback: null,
223
+ });
224
+ if (!cmdResult.ok || cmdResult.data === null) break;
225
+ cmdline = cmdResult.data;
230
226
 
231
227
  if (!cmdline) break;
232
228
 
@@ -242,16 +238,11 @@ function detectFromPs() {
242
238
  }
243
239
 
244
240
  // Get parent PID
245
- try {
246
- const ppidStr = execFileSync('ps', ['-p', String(pid), '-o', 'ppid='], {
247
- encoding: 'utf8',
248
- stdio: ['pipe', 'pipe', 'pipe'],
249
- timeout: 1000,
250
- }).trim();
251
- pid = parseInt(ppidStr, 10);
252
- } catch {
253
- break;
254
- }
241
+ const ppidResult = executeCommandSync('ps', ['-p', String(pid), '-o', 'ppid='], {
242
+ timeout: 1000, fallback: null,
243
+ });
244
+ if (!ppidResult.ok || ppidResult.data === null) break;
245
+ pid = parseInt(ppidResult.data, 10);
255
246
  }
256
247
  } catch (e) {
257
248
  // ps command failed - ignore
@@ -4,10 +4,11 @@
4
4
  * Provides cached git operations and session phase detection for Kanban visualization.
5
5
  */
6
6
 
7
- const { execFileSync, spawnSync, spawn } = require('child_process');
7
+ const { spawnSync, spawn } = require('child_process');
8
8
  const fs = require('fs');
9
9
 
10
10
  const { getProjectRoot } = require('./paths');
11
+ const { git } = require('./process-executor');
11
12
 
12
13
  const ROOT = getProjectRoot();
13
14
 
@@ -79,16 +80,10 @@ function getCurrentBranch(cwd = ROOT) {
79
80
  const cached = gitCache.get(cacheKey);
80
81
  if (cached !== null) return cached;
81
82
 
82
- try {
83
- const branch = execFileSync('git', ['branch', '--show-current'], {
84
- cwd,
85
- encoding: 'utf8',
86
- }).trim();
87
- gitCache.set(cacheKey, branch);
88
- return branch;
89
- } catch (e) {
90
- return 'unknown';
91
- }
83
+ const result = git(['branch', '--show-current'], { cwd, fallback: 'unknown' });
84
+ const branch = result.data;
85
+ gitCache.set(cacheKey, branch);
86
+ return branch;
92
87
  }
93
88
 
94
89
  /**
@@ -180,28 +175,15 @@ function getSessionPhase(session) {
180
175
 
181
176
  try {
182
177
  const mainBranch = getMainBranch(sessionPath);
183
- let commitCount;
184
- try {
185
- commitCount = execFileSync('git', ['rev-list', '--count', `${mainBranch}..HEAD`], {
186
- cwd: sessionPath,
187
- encoding: 'utf8',
188
- stdio: ['pipe', 'pipe', 'pipe'],
189
- }).trim();
190
- } catch {
191
- commitCount = '0';
192
- }
193
- const commits = parseInt(commitCount, 10);
194
-
195
- let status = '';
196
- try {
197
- status = execFileSync('git', ['status', '--porcelain'], {
198
- cwd: sessionPath,
199
- encoding: 'utf8',
200
- stdio: ['pipe', 'pipe', 'pipe'],
201
- }).trim();
202
- } catch {
203
- // git status failed, treat as no changes
204
- }
178
+ const commitResult = git(['rev-list', '--count', `${mainBranch}..HEAD`], {
179
+ cwd: sessionPath, fallback: '0',
180
+ });
181
+ const commits = parseInt(commitResult.data, 10);
182
+
183
+ const statusResult = git(['status', '--porcelain'], {
184
+ cwd: sessionPath, fallback: '',
185
+ });
186
+ const status = statusResult.data;
205
187
 
206
188
  const phase = determinePhaseFromGitState(commits, status !== '');
207
189
  gitCache.set(cacheKey, phase);
@@ -62,7 +62,9 @@ function checkMergeability(sessionId, loadRegistry) {
62
62
  }
63
63
  );
64
64
 
65
- const [behind, ahead] = (aheadBehind.stdout || '0\t0').trim().split('\t').map(Number);
65
+ const parts = (aheadBehind.stdout || '0\t0').trim().split('\t').map(Number);
66
+ const behind = isNaN(parts[0]) ? 0 : parts[0];
67
+ const ahead = isNaN(parts[1]) ? 0 : parts[1];
66
68
 
67
69
  if (ahead === 0) {
68
70
  return {
@@ -79,46 +81,50 @@ function checkMergeability(sessionId, loadRegistry) {
79
81
 
80
82
  // Try merge --no-commit --no-ff to check for conflicts (dry run)
81
83
  const currentBranch = getCurrentBranch();
84
+ let checkedOutMain = false;
82
85
 
83
- // Checkout main in ROOT for the test merge
84
- const checkoutMain = spawnSync('git', ['checkout', mainBranch], {
85
- cwd: ROOT,
86
- encoding: 'utf8',
87
- });
86
+ try {
87
+ // Checkout main in ROOT for the test merge
88
+ const checkoutMain = spawnSync('git', ['checkout', mainBranch], {
89
+ cwd: ROOT,
90
+ encoding: 'utf8',
91
+ });
88
92
 
89
- if (checkoutMain.status !== 0) {
90
- return {
91
- success: false,
92
- error: `Failed to checkout ${mainBranch}: ${checkoutMain.stderr}`,
93
- };
94
- }
93
+ if (checkoutMain.status !== 0) {
94
+ return {
95
+ success: false,
96
+ error: `Failed to checkout ${mainBranch}: ${checkoutMain.stderr}`,
97
+ };
98
+ }
99
+ checkedOutMain = true;
95
100
 
96
- // Try the merge
97
- const testMerge = spawnSync('git', ['merge', '--no-commit', '--no-ff', branchName], {
98
- cwd: ROOT,
99
- encoding: 'utf8',
100
- });
101
+ // Try the merge
102
+ const testMerge = spawnSync('git', ['merge', '--no-commit', '--no-ff', branchName], {
103
+ cwd: ROOT,
104
+ encoding: 'utf8',
105
+ });
101
106
 
102
- const hasConflicts = testMerge.status !== 0;
107
+ const hasConflicts = testMerge.status !== 0;
103
108
 
104
- // Abort the test merge
105
- spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
109
+ // Abort the test merge
110
+ spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
106
111
 
107
- // Go back to original branch if different
108
- if (currentBranch && currentBranch !== mainBranch) {
109
- spawnSync('git', ['checkout', currentBranch], { cwd: ROOT, encoding: 'utf8' });
112
+ return {
113
+ success: true,
114
+ mergeable: !hasConflicts,
115
+ branchName,
116
+ mainBranch,
117
+ commitsAhead: ahead,
118
+ commitsBehind: behind,
119
+ hasConflicts,
120
+ conflictDetails: hasConflicts ? testMerge.stderr : null,
121
+ };
122
+ } finally {
123
+ // Always restore original branch
124
+ if (checkedOutMain && currentBranch && currentBranch !== mainBranch) {
125
+ spawnSync('git', ['checkout', currentBranch], { cwd: ROOT, encoding: 'utf8' });
126
+ }
110
127
  }
111
-
112
- return {
113
- success: true,
114
- mergeable: !hasConflicts,
115
- branchName,
116
- mainBranch,
117
- commitsAhead: ahead,
118
- commitsBehind: behind,
119
- hasConflicts,
120
- conflictDetails: hasConflicts ? testMerge.stderr : null,
121
- };
122
128
  }
123
129
 
124
130
  /**
@@ -0,0 +1,199 @@
1
+ /**
2
+ * process-executor.js - Unified shell execution module (US-0310)
3
+ *
4
+ * Centralizes child_process usage with security-by-default:
5
+ * - Uses execFileSync/spawn (no shell) as standard pattern (US-0297)
6
+ * - Consistent Result pattern: { ok, data?, error?, exitCode?, stderr? }
7
+ * - Git shortcuts for the 76% of call sites that are git commands
8
+ *
9
+ * This module coexists with errors.js safeExec() which serves a different
10
+ * purpose (shell command strings via bash -c).
11
+ */
12
+
13
+ const { execFileSync, spawn: nodeSpawn } = require('child_process');
14
+
15
+ /**
16
+ * Execute a command synchronously without a shell.
17
+ * Uses execFileSync internally - shell metacharacters are literal, not interpreted.
18
+ *
19
+ * @param {string} cmd - Executable name (e.g., 'git', 'ps', 'node')
20
+ * @param {string[]} args - Arguments array
21
+ * @param {Object} [opts] - Options
22
+ * @param {string} [opts.cwd] - Working directory (default: process.cwd())
23
+ * @param {number} [opts.timeout] - Timeout in ms (default: 30000)
24
+ * @param {string} [opts.encoding] - Output encoding (default: 'utf8')
25
+ * @param {boolean} [opts.trim] - Trim stdout (default: true)
26
+ * @param {boolean} [opts.captureStderr] - Include stderr in result (default: false)
27
+ * @param {*} [opts.fallback] - Value to return as data on failure (makes errors non-fatal)
28
+ * @returns {{ ok: boolean, data?: string, error?: string, exitCode?: number, stderr?: string }}
29
+ */
30
+ function executeCommandSync(cmd, args = [], opts = {}) {
31
+ const {
32
+ cwd = process.cwd(),
33
+ timeout = 30000,
34
+ encoding = 'utf8',
35
+ trim = true,
36
+ captureStderr = false,
37
+ fallback,
38
+ } = opts;
39
+
40
+ try {
41
+ const result = execFileSync(cmd, args, {
42
+ cwd,
43
+ encoding,
44
+ timeout,
45
+ stdio: ['pipe', 'pipe', 'pipe'],
46
+ });
47
+
48
+ const data = trim ? result.trim() : result;
49
+ return { ok: true, data };
50
+ } catch (err) {
51
+ if (fallback !== undefined) {
52
+ return { ok: true, data: fallback };
53
+ }
54
+
55
+ const exitCode = err.status || 1;
56
+ const stderr = err.stderr ? (trim ? err.stderr.trim() : err.stderr) : undefined;
57
+ const error = `Command failed: ${cmd} ${args.join(' ')} (exit ${exitCode})`;
58
+ const result = { ok: false, error, exitCode };
59
+ if (captureStderr && stderr) {
60
+ result.stderr = stderr;
61
+ }
62
+ return result;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Execute a command asynchronously without a shell.
68
+ * Uses spawn internally. The returned Promise always resolves (never rejects).
69
+ *
70
+ * @param {string} cmd - Executable name
71
+ * @param {string[]} args - Arguments array
72
+ * @param {Object} [opts] - Options (same as executeCommandSync)
73
+ * @returns {Promise<{ ok: boolean, data?: string, error?: string, exitCode?: number, stderr?: string }>}
74
+ */
75
+ function executeCommand(cmd, args = [], opts = {}) {
76
+ const {
77
+ cwd = process.cwd(),
78
+ timeout = 30000,
79
+ trim = true,
80
+ captureStderr = false,
81
+ fallback,
82
+ } = opts;
83
+
84
+ return new Promise(resolve => {
85
+ const proc = nodeSpawn(cmd, args, {
86
+ cwd,
87
+ stdio: ['pipe', 'pipe', 'pipe'],
88
+ });
89
+
90
+ let stdout = '';
91
+ let stderr = '';
92
+ let timedOut = false;
93
+
94
+ const timer = timeout > 0 ? setTimeout(() => {
95
+ timedOut = true;
96
+ proc.kill('SIGTERM');
97
+ }, timeout) : null;
98
+
99
+ proc.stdout.on('data', chunk => { stdout += chunk; });
100
+ proc.stderr.on('data', chunk => { stderr += chunk; });
101
+
102
+ proc.on('error', err => {
103
+ if (timer) clearTimeout(timer);
104
+ if (fallback !== undefined) {
105
+ resolve({ ok: true, data: fallback });
106
+ } else {
107
+ resolve({ ok: false, error: `Spawn error: ${err.message}`, exitCode: null });
108
+ }
109
+ });
110
+
111
+ proc.on('close', code => {
112
+ if (timer) clearTimeout(timer);
113
+
114
+ if (timedOut) {
115
+ if (fallback !== undefined) {
116
+ resolve({ ok: true, data: fallback });
117
+ } else {
118
+ resolve({ ok: false, error: `Command timed out after ${timeout}ms: ${cmd}`, exitCode: null });
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (code !== 0) {
124
+ if (fallback !== undefined) {
125
+ resolve({ ok: true, data: fallback });
126
+ } else {
127
+ const result = { ok: false, error: `Command failed: ${cmd} ${args.join(' ')} (exit ${code})`, exitCode: code };
128
+ if (captureStderr) {
129
+ result.stderr = trim ? stderr.trim() : stderr;
130
+ }
131
+ resolve(result);
132
+ }
133
+ return;
134
+ }
135
+
136
+ const data = trim ? stdout.trim() : stdout;
137
+ resolve({ ok: true, data });
138
+ });
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Spawn a background (detached, fire-and-forget) process.
144
+ *
145
+ * @param {string} cmd - Executable name
146
+ * @param {string[]} args - Arguments array
147
+ * @param {Object} [opts] - Options
148
+ * @param {string} [opts.cwd] - Working directory
149
+ * @returns {{ ok: boolean, pid?: number, error?: string }}
150
+ */
151
+ function spawnBackground(cmd, args = [], opts = {}) {
152
+ const { cwd = process.cwd() } = opts;
153
+
154
+ try {
155
+ const child = nodeSpawn(cmd, args, {
156
+ cwd,
157
+ detached: true,
158
+ stdio: 'ignore',
159
+ });
160
+ // Suppress unhandled error events (e.g., ENOENT for missing commands)
161
+ child.on('error', () => {});
162
+ child.unref();
163
+ return { ok: true, pid: child.pid };
164
+ } catch (err) {
165
+ return { ok: false, error: `Failed to spawn: ${err.message}` };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Execute a git command synchronously.
171
+ * Shortcut for executeCommandSync('git', args, opts).
172
+ *
173
+ * @param {string[]} args - Git arguments (e.g., ['branch', '--show-current'])
174
+ * @param {Object} [opts] - Same options as executeCommandSync
175
+ * @returns {{ ok: boolean, data?: string, error?: string, exitCode?: number, stderr?: string }}
176
+ */
177
+ function git(args, opts = {}) {
178
+ return executeCommandSync('git', args, opts);
179
+ }
180
+
181
+ /**
182
+ * Execute a git command asynchronously.
183
+ * Shortcut for executeCommand('git', args, opts).
184
+ *
185
+ * @param {string[]} args - Git arguments
186
+ * @param {Object} [opts] - Same options as executeCommand
187
+ * @returns {Promise<{ ok: boolean, data?: string, error?: string, exitCode?: number, stderr?: string }>}
188
+ */
189
+ function gitAsync(args, opts = {}) {
190
+ return executeCommand('git', args, opts);
191
+ }
192
+
193
+ module.exports = {
194
+ executeCommandSync,
195
+ executeCommand,
196
+ spawnBackground,
197
+ git,
198
+ gitAsync,
199
+ };
@@ -1,52 +1,30 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
3
+ const { MtimeCache } = require('./cache-provider');
4
4
 
5
5
  /**
6
6
  * Registry scanning cache
7
7
  * Caches directory scan results keyed by path + mtime.
8
8
  * TTL-based expiration (default 60s).
9
+ *
10
+ * Uses MtimeCache from cache-provider for unified caching with
11
+ * automatic mtime invalidation.
9
12
  */
10
13
 
11
- const _cache = new Map();
12
14
  const DEFAULT_TTL_MS = 60 * 1000;
13
15
 
16
+ const _cache = new MtimeCache({ maxSize: 50, ttlMs: DEFAULT_TTL_MS });
17
+
14
18
  /**
15
19
  * Get cached scan result for a directory
16
20
  * @param {string} dirPath - Directory path
17
21
  * @param {Object} [options]
18
- * @param {number} [options.ttlMs] - TTL in milliseconds (default 60000)
19
22
  * @param {boolean} [options.noCache] - Bypass cache entirely
20
23
  * @returns {*|null} Cached result or null if miss/expired
21
24
  */
22
25
  function getCached(dirPath, options = {}) {
23
- const { ttlMs = DEFAULT_TTL_MS, noCache = false } = options;
24
-
25
- if (noCache) return null;
26
-
27
- const entry = _cache.get(dirPath);
28
- if (!entry) return null;
29
-
30
- // Check TTL
31
- if (Date.now() - entry.cachedAt > ttlMs) {
32
- _cache.delete(dirPath);
33
- return null;
34
- }
35
-
36
- // Check directory mtime hasn't changed
37
- try {
38
- const stat = fs.statSync(dirPath);
39
- const currentMtime = stat.mtimeMs;
40
- if (currentMtime !== entry.mtimeMs) {
41
- _cache.delete(dirPath);
42
- return null;
43
- }
44
- } catch {
45
- _cache.delete(dirPath);
46
- return null;
47
- }
48
-
49
- return entry.data;
26
+ if (options.noCache) return null;
27
+ return _cache.get(dirPath) ?? null;
50
28
  }
51
29
 
52
30
  /**
@@ -55,16 +33,7 @@ function getCached(dirPath, options = {}) {
55
33
  * @param {*} data - Data to cache
56
34
  */
57
35
  function setCached(dirPath, data) {
58
- try {
59
- const stat = fs.statSync(dirPath);
60
- _cache.set(dirPath, {
61
- data,
62
- mtimeMs: stat.mtimeMs,
63
- cachedAt: Date.now(),
64
- });
65
- } catch {
66
- // Can't stat directory, don't cache
67
- }
36
+ _cache.set(dirPath, data);
68
37
  }
69
38
 
70
39
  /**
@@ -76,26 +45,23 @@ function clearCache() {
76
45
 
77
46
  /**
78
47
  * Get cache stats
79
- * @returns {{ size: number, keys: string[] }}
48
+ * @returns {{ size: number, hits: number, misses: number, hitRate: string }}
80
49
  */
81
50
  function getCacheStats() {
82
- return {
83
- size: _cache.size,
84
- keys: Array.from(_cache.keys()),
85
- };
51
+ return _cache.getStats();
86
52
  }
87
53
 
88
54
  /**
89
55
  * Wrap a scan function with caching
90
56
  * @param {Function} scanFn - Function that takes (dirPath, ...args) and returns results
91
57
  * @param {Object} [options]
92
- * @param {number} [options.ttlMs] - TTL in ms
58
+ * @param {boolean} [options.noCache] - Bypass cache
93
59
  * @returns {Function} Cached version of scanFn
94
60
  */
95
61
  function withCache(scanFn, options = {}) {
96
62
  return function cachedScan(dirPath, ...args) {
97
63
  const noCache = process.argv.includes('--no-cache') || options.noCache;
98
- const cached = getCached(dirPath, { ...options, noCache });
64
+ const cached = getCached(dirPath, { noCache });
99
65
  if (cached !== null) return cached;
100
66
 
101
67
  const result = scanFn(dirPath, ...args);