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.
- package/CHANGELOG.md +10 -0
- package/lib/cache-provider.js +155 -0
- package/lib/codebase-indexer.js +1 -1
- package/lib/content-sanitizer.js +1 -0
- package/lib/dashboard-protocol.js +25 -0
- package/lib/dashboard-server.js +184 -133
- package/lib/errors.js +18 -0
- package/lib/file-cache.js +1 -1
- package/lib/flag-detection.js +11 -20
- package/lib/git-operations.js +15 -33
- package/lib/merge-operations.js +40 -34
- package/lib/process-executor.js +199 -0
- package/lib/registry-cache.js +13 -47
- package/lib/skill-loader.js +206 -0
- package/lib/smart-json-file.js +2 -4
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +13 -12
- package/scripts/agileflow-statusline.sh +30 -0
- package/scripts/agileflow-welcome.js +181 -212
- package/scripts/auto-self-improve.js +3 -3
- package/scripts/claude-smart.sh +67 -0
- package/scripts/claude-tmux.sh +248 -170
- package/scripts/damage-control-multi-agent.js +227 -0
- package/scripts/lib/bus-utils.js +471 -0
- package/scripts/lib/configure-detect.js +5 -6
- package/scripts/lib/configure-features.js +44 -0
- package/scripts/lib/configure-repair.js +5 -6
- package/scripts/lib/configure-utils.js +2 -3
- package/scripts/lib/context-formatter.js +87 -8
- package/scripts/lib/damage-control-utils.js +37 -3
- package/scripts/lib/file-lock.js +392 -0
- package/scripts/lib/ideation-index.js +2 -5
- package/scripts/lib/lifecycle-detector.js +123 -0
- package/scripts/lib/process-cleanup.js +55 -81
- package/scripts/lib/scale-detector.js +357 -0
- package/scripts/lib/signal-detectors.js +779 -0
- package/scripts/lib/story-state-machine.js +1 -1
- package/scripts/lib/sync-ideation-status.js +2 -3
- package/scripts/lib/task-registry.js +7 -1
- package/scripts/lib/team-events.js +357 -0
- package/scripts/messaging-bridge.js +79 -36
- package/scripts/migrate-ideation-index.js +37 -14
- package/scripts/obtain-context.js +37 -19
- package/scripts/ralph-loop.js +3 -4
- package/scripts/smart-detect.js +390 -0
- package/scripts/team-manager.js +174 -30
- package/src/core/commands/audit.md +13 -11
- package/src/core/commands/babysit.md +162 -115
- package/src/core/commands/changelog.md +21 -4
- package/src/core/commands/configure.md +105 -2
- package/src/core/commands/debt.md +12 -2
- package/src/core/commands/feedback.md +7 -6
- package/src/core/commands/ideate/history.md +1 -1
- package/src/core/commands/ideate/new.md +5 -5
- package/src/core/commands/logic/audit.md +2 -2
- package/src/core/commands/pr.md +7 -6
- package/src/core/commands/research/analyze.md +28 -20
- package/src/core/commands/research/ask.md +43 -0
- package/src/core/commands/research/import.md +29 -21
- package/src/core/commands/research/list.md +8 -7
- package/src/core/commands/research/synthesize.md +356 -20
- package/src/core/commands/research/view.md +8 -5
- package/src/core/commands/review.md +24 -6
- package/src/core/commands/skill/create.md +34 -0
- 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
package/lib/flag-detection.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const fs = require('fs');
|
|
14
|
-
const {
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
package/lib/git-operations.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* Provides cached git operations and session phase detection for Kanban visualization.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const {
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
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);
|
package/lib/merge-operations.js
CHANGED
|
@@ -62,7 +62,9 @@ function checkMergeability(sessionId, loadRegistry) {
|
|
|
62
62
|
}
|
|
63
63
|
);
|
|
64
64
|
|
|
65
|
-
const
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
107
|
+
const hasConflicts = testMerge.status !== 0;
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
// Abort the test merge
|
|
110
|
+
spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
};
|
package/lib/registry-cache.js
CHANGED
|
@@ -1,52 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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, {
|
|
64
|
+
const cached = getCached(dirPath, { noCache });
|
|
99
65
|
if (cached !== null) return cached;
|
|
100
66
|
|
|
101
67
|
const result = scanFn(dirPath, ...args);
|