agileflow 2.90.7 → 2.92.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/README.md +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +818 -0
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate-names.js +3 -3
- package/lib/validate.js +116 -52
- package/package.json +4 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +435 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +43 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +118 -1048
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +36 -11
- package/scripts/query-codebase.js +538 -0
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +220 -42
- package/scripts/spawn-parallel.js +651 -0
- package/scripts/tui/blessed/data/watcher.js +180 -0
- package/scripts/tui/blessed/index.js +244 -0
- package/scripts/tui/blessed/panels/output.js +101 -0
- package/scripts/tui/blessed/panels/sessions.js +150 -0
- package/scripts/tui/blessed/panels/trace.js +97 -0
- package/scripts/tui/blessed/ui/help.js +77 -0
- package/scripts/tui/blessed/ui/screen.js +52 -0
- package/scripts/tui/blessed/ui/statusbar.js +47 -0
- package/scripts/tui/blessed/ui/tabbar.js +99 -0
- package/scripts/tui/index.js +38 -30
- package/scripts/validators/README.md +143 -0
- package/scripts/validators/component-validator.js +239 -0
- package/scripts/validators/json-schema-validator.js +186 -0
- package/scripts/validators/markdown-validator.js +152 -0
- package/scripts/validators/migration-validator.js +129 -0
- package/scripts/validators/security-validator.js +380 -0
- package/scripts/validators/story-format-validator.js +197 -0
- package/scripts/validators/test-result-validator.js +114 -0
- package/scripts/validators/workflow-validator.js +247 -0
- package/src/core/agents/accessibility.md +6 -0
- package/src/core/agents/adr-writer.md +6 -0
- package/src/core/agents/analytics.md +6 -0
- package/src/core/agents/api.md +6 -0
- package/src/core/agents/ci.md +6 -0
- package/src/core/agents/codebase-query.md +261 -0
- package/src/core/agents/compliance.md +6 -0
- package/src/core/agents/configuration-damage-control.md +6 -0
- package/src/core/agents/configuration-visual-e2e.md +6 -0
- package/src/core/agents/database.md +10 -0
- package/src/core/agents/datamigration.md +6 -0
- package/src/core/agents/design.md +6 -0
- package/src/core/agents/devops.md +6 -0
- package/src/core/agents/documentation.md +6 -0
- package/src/core/agents/epic-planner.md +6 -0
- package/src/core/agents/integrations.md +6 -0
- package/src/core/agents/mentor.md +6 -0
- package/src/core/agents/mobile.md +6 -0
- package/src/core/agents/monitoring.md +6 -0
- package/src/core/agents/multi-expert.md +6 -0
- package/src/core/agents/performance.md +6 -0
- package/src/core/agents/product.md +6 -0
- package/src/core/agents/qa.md +6 -0
- package/src/core/agents/readme-updater.md +6 -0
- package/src/core/agents/refactor.md +6 -0
- package/src/core/agents/research.md +6 -0
- package/src/core/agents/security.md +6 -0
- package/src/core/agents/testing.md +10 -0
- package/src/core/agents/ui.md +6 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/audit.md +401 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +205 -1
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +76 -0
- package/src/core/commands/metrics.md +1 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/research/analyze.md +1 -0
- package/src/core/commands/research/ask.md +2 -0
- package/src/core/commands/research/import.md +1 -0
- package/src/core/commands/research/list.md +2 -0
- package/src/core/commands/research/synthesize.md +584 -0
- package/src/core/commands/research/view.md +2 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +113 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +200 -1
- package/src/core/commands/story/list.md +9 -9
- package/src/core/commands/story/view.md +1 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/experts/codebase-query/expertise.yaml +190 -0
- package/src/core/experts/codebase-query/question.md +73 -0
- package/src/core/experts/codebase-query/self-improve.md +105 -0
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +86 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +114 -1
- package/tools/cli/lib/ui.js +14 -25
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Correlation ID Management
|
|
3
|
+
*
|
|
4
|
+
* Generates and propagates trace IDs and session IDs for event correlation.
|
|
5
|
+
* Enables filtering and tracing events across complex workflows.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// Global context for correlation IDs
|
|
13
|
+
let currentContext = {
|
|
14
|
+
traceId: null,
|
|
15
|
+
sessionId: null,
|
|
16
|
+
spanId: null,
|
|
17
|
+
parentSpanId: null,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a unique trace ID (16 hex chars, like OpenTelemetry standard)
|
|
22
|
+
* @returns {string} Unique trace ID
|
|
23
|
+
*/
|
|
24
|
+
function generateTraceId() {
|
|
25
|
+
return crypto.randomBytes(8).toString('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate a span ID (8 hex chars)
|
|
30
|
+
* @returns {string} Unique span ID
|
|
31
|
+
*/
|
|
32
|
+
function generateSpanId() {
|
|
33
|
+
return crypto.randomBytes(4).toString('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a session ID based on timestamp and random component
|
|
38
|
+
* Format: session_YYYYMMDD_HHMMSS_XXXX
|
|
39
|
+
* @returns {string} Session ID
|
|
40
|
+
*/
|
|
41
|
+
function generateSessionId() {
|
|
42
|
+
const now = new Date();
|
|
43
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
44
|
+
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
45
|
+
const random = crypto.randomBytes(2).toString('hex');
|
|
46
|
+
return `session_${date}_${time}_${random}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get current correlation context
|
|
51
|
+
* @returns {{ traceId: string | null, sessionId: string | null, spanId: string | null }}
|
|
52
|
+
*/
|
|
53
|
+
function getContext() {
|
|
54
|
+
return { ...currentContext };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set correlation context
|
|
59
|
+
* @param {Object} context
|
|
60
|
+
* @param {string} [context.traceId] - Trace ID
|
|
61
|
+
* @param {string} [context.sessionId] - Session ID
|
|
62
|
+
* @param {string} [context.spanId] - Span ID
|
|
63
|
+
* @param {string} [context.parentSpanId] - Parent span ID
|
|
64
|
+
*/
|
|
65
|
+
function setContext(context) {
|
|
66
|
+
if (context.traceId !== undefined) currentContext.traceId = context.traceId;
|
|
67
|
+
if (context.sessionId !== undefined) currentContext.sessionId = context.sessionId;
|
|
68
|
+
if (context.spanId !== undefined) currentContext.spanId = context.spanId;
|
|
69
|
+
if (context.parentSpanId !== undefined) currentContext.parentSpanId = context.parentSpanId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start a new trace (generates new trace_id)
|
|
74
|
+
* @param {Object} [options]
|
|
75
|
+
* @param {string} [options.sessionId] - Existing session ID to use
|
|
76
|
+
* @returns {{ traceId: string, sessionId: string, spanId: string }}
|
|
77
|
+
*/
|
|
78
|
+
function startTrace(options = {}) {
|
|
79
|
+
const traceId = generateTraceId();
|
|
80
|
+
const sessionId = options.sessionId || currentContext.sessionId || generateSessionId();
|
|
81
|
+
const spanId = generateSpanId();
|
|
82
|
+
|
|
83
|
+
setContext({
|
|
84
|
+
traceId,
|
|
85
|
+
sessionId,
|
|
86
|
+
spanId,
|
|
87
|
+
parentSpanId: null,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { traceId, sessionId, spanId };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Start a new span within current trace
|
|
95
|
+
* @returns {{ traceId: string, sessionId: string, spanId: string, parentSpanId: string | null }}
|
|
96
|
+
*/
|
|
97
|
+
function startSpan() {
|
|
98
|
+
const parentSpanId = currentContext.spanId;
|
|
99
|
+
const spanId = generateSpanId();
|
|
100
|
+
|
|
101
|
+
setContext({
|
|
102
|
+
spanId,
|
|
103
|
+
parentSpanId,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
traceId: currentContext.traceId,
|
|
108
|
+
sessionId: currentContext.sessionId,
|
|
109
|
+
spanId,
|
|
110
|
+
parentSpanId,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Inject correlation IDs into an event object
|
|
116
|
+
* @param {Object} event - Event to inject into
|
|
117
|
+
* @returns {Object} Event with correlation IDs
|
|
118
|
+
*/
|
|
119
|
+
function injectCorrelation(event) {
|
|
120
|
+
const ctx = getContext();
|
|
121
|
+
return {
|
|
122
|
+
...event,
|
|
123
|
+
...(ctx.traceId && { trace_id: ctx.traceId }),
|
|
124
|
+
...(ctx.sessionId && { session_id: ctx.sessionId }),
|
|
125
|
+
...(ctx.spanId && { span_id: ctx.spanId }),
|
|
126
|
+
...(ctx.parentSpanId && { parent_span_id: ctx.parentSpanId }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear correlation context (for testing or new sessions)
|
|
132
|
+
*/
|
|
133
|
+
function clearContext() {
|
|
134
|
+
currentContext = {
|
|
135
|
+
traceId: null,
|
|
136
|
+
sessionId: null,
|
|
137
|
+
spanId: null,
|
|
138
|
+
parentSpanId: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Load session ID from session-state.json if available
|
|
144
|
+
* @param {string} projectDir - Project directory
|
|
145
|
+
* @returns {string | null} Session ID or null
|
|
146
|
+
*/
|
|
147
|
+
function loadSessionId(projectDir) {
|
|
148
|
+
try {
|
|
149
|
+
const sessionStatePath = path.join(projectDir, 'docs', '09-agents', 'session-state.json');
|
|
150
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
151
|
+
const data = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
152
|
+
return data.session_id || null;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Ignore errors
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Save session ID to session-state.json
|
|
162
|
+
* @param {string} projectDir - Project directory
|
|
163
|
+
* @param {string} sessionId - Session ID to save
|
|
164
|
+
*/
|
|
165
|
+
function saveSessionId(projectDir, sessionId) {
|
|
166
|
+
try {
|
|
167
|
+
const sessionStatePath = path.join(projectDir, 'docs', '09-agents', 'session-state.json');
|
|
168
|
+
let data = {};
|
|
169
|
+
|
|
170
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
171
|
+
data = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
data.session_id = sessionId;
|
|
175
|
+
data.updated_at = new Date().toISOString();
|
|
176
|
+
|
|
177
|
+
const dir = path.dirname(sessionStatePath);
|
|
178
|
+
if (!fs.existsSync(dir)) {
|
|
179
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(data, null, 2) + '\n');
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore errors - session ID is not critical
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Initialize correlation context for a project
|
|
190
|
+
* Loads existing session ID or creates new one
|
|
191
|
+
* @param {string} projectDir - Project directory
|
|
192
|
+
* @param {Object} [options]
|
|
193
|
+
* @param {boolean} [options.newTrace=true] - Start new trace
|
|
194
|
+
* @returns {{ traceId: string, sessionId: string }}
|
|
195
|
+
*/
|
|
196
|
+
function initializeForProject(projectDir, options = {}) {
|
|
197
|
+
const { newTrace = true } = options;
|
|
198
|
+
|
|
199
|
+
// Load or create session ID
|
|
200
|
+
let sessionId = loadSessionId(projectDir);
|
|
201
|
+
if (!sessionId) {
|
|
202
|
+
sessionId = generateSessionId();
|
|
203
|
+
saveSessionId(projectDir, sessionId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setContext({ sessionId });
|
|
207
|
+
|
|
208
|
+
if (newTrace) {
|
|
209
|
+
const trace = startTrace({ sessionId });
|
|
210
|
+
return { traceId: trace.traceId, sessionId };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
traceId: currentContext.traceId || generateTraceId(),
|
|
215
|
+
sessionId,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Extract correlation IDs from an event
|
|
221
|
+
* @param {Object} event - Event object
|
|
222
|
+
* @returns {{ traceId: string | null, sessionId: string | null, spanId: string | null }}
|
|
223
|
+
*/
|
|
224
|
+
function extractCorrelation(event) {
|
|
225
|
+
return {
|
|
226
|
+
traceId: event.trace_id || null,
|
|
227
|
+
sessionId: event.session_id || null,
|
|
228
|
+
spanId: event.span_id || null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Filter events by trace ID
|
|
234
|
+
* @param {Array} events - Array of events
|
|
235
|
+
* @param {string} traceId - Trace ID to filter by
|
|
236
|
+
* @returns {Array} Filtered events
|
|
237
|
+
*/
|
|
238
|
+
function filterByTraceId(events, traceId) {
|
|
239
|
+
return events.filter(e => e.trace_id === traceId);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Filter events by session ID
|
|
244
|
+
* @param {Array} events - Array of events
|
|
245
|
+
* @param {string} sessionId - Session ID to filter by
|
|
246
|
+
* @returns {Array} Filtered events
|
|
247
|
+
*/
|
|
248
|
+
function filterBySessionId(events, sessionId) {
|
|
249
|
+
return events.filter(e => e.session_id === sessionId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
// ID generation
|
|
254
|
+
generateTraceId,
|
|
255
|
+
generateSpanId,
|
|
256
|
+
generateSessionId,
|
|
257
|
+
|
|
258
|
+
// Context management
|
|
259
|
+
getContext,
|
|
260
|
+
setContext,
|
|
261
|
+
clearContext,
|
|
262
|
+
startTrace,
|
|
263
|
+
startSpan,
|
|
264
|
+
|
|
265
|
+
// Event injection/extraction
|
|
266
|
+
injectCorrelation,
|
|
267
|
+
extractCorrelation,
|
|
268
|
+
|
|
269
|
+
// Project integration
|
|
270
|
+
initializeForProject,
|
|
271
|
+
loadSessionId,
|
|
272
|
+
saveSessionId,
|
|
273
|
+
|
|
274
|
+
// Filtering
|
|
275
|
+
filterByTraceId,
|
|
276
|
+
filterBySessionId,
|
|
277
|
+
};
|
package/lib/error-codes.js
CHANGED
|
@@ -524,6 +524,48 @@ function formatError(error, options = {}) {
|
|
|
524
524
|
return lines.join('\n');
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Create an error result with standardized metadata
|
|
529
|
+
* This factory produces Result objects compatible with result-schema.js
|
|
530
|
+
* @param {string} errorCode - Error code (e.g., 'ENOENT', 'ECONFIG')
|
|
531
|
+
* @param {string} [message] - Optional custom error message
|
|
532
|
+
* @param {Object} [options] - Additional options
|
|
533
|
+
* @param {Object} [options.context] - Additional context about the error
|
|
534
|
+
* @param {Error} [options.cause] - Original error that caused this failure
|
|
535
|
+
* @returns {{ ok: false, error: string, errorCode: string, severity: string, category: string, recoverable: boolean, suggestedFix: string, autoFix: string|null, context?: Object, cause?: Error }}
|
|
536
|
+
*/
|
|
537
|
+
function createErrorResult(errorCode, message, options = {}) {
|
|
538
|
+
const codeData = ErrorCodes[errorCode] || ErrorCodes.EUNKNOWN;
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
ok: false,
|
|
542
|
+
error: message || codeData.message,
|
|
543
|
+
errorCode: codeData.code,
|
|
544
|
+
severity: codeData.severity,
|
|
545
|
+
category: codeData.category,
|
|
546
|
+
recoverable: codeData.recoverable,
|
|
547
|
+
suggestedFix: codeData.suggestedFix,
|
|
548
|
+
autoFix: codeData.autoFix || null,
|
|
549
|
+
context: options.context,
|
|
550
|
+
cause: options.cause,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Create a success result
|
|
556
|
+
* @template T
|
|
557
|
+
* @param {T} data - The success data
|
|
558
|
+
* @param {Object} [meta] - Optional metadata
|
|
559
|
+
* @returns {{ ok: true, data: T }}
|
|
560
|
+
*/
|
|
561
|
+
function createSuccessResult(data, meta = {}) {
|
|
562
|
+
return {
|
|
563
|
+
ok: true,
|
|
564
|
+
data,
|
|
565
|
+
...meta,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
527
569
|
module.exports = {
|
|
528
570
|
// Enums
|
|
529
571
|
Severity,
|
|
@@ -541,4 +583,8 @@ module.exports = {
|
|
|
541
583
|
getSuggestedFix,
|
|
542
584
|
getAutoFix,
|
|
543
585
|
formatError,
|
|
586
|
+
|
|
587
|
+
// Result factories (for result-schema.js compatibility)
|
|
588
|
+
createErrorResult,
|
|
589
|
+
createSuccessResult,
|
|
544
590
|
};
|
package/lib/errors.js
CHANGED
|
@@ -236,10 +236,13 @@ function getCharName(char) {
|
|
|
236
236
|
* @param {string} filePath - Absolute path to JSON file
|
|
237
237
|
* @param {object} options - Optional settings
|
|
238
238
|
* @param {*} options.defaultValue - Value to return if file doesn't exist (makes missing file not an error)
|
|
239
|
-
* @
|
|
239
|
+
* @param {boolean} options.hidePathInError - Hide file path in error messages (security: default false)
|
|
240
|
+
* @param {string} options.context - Context for error messages (alternative to exposing path)
|
|
241
|
+
* @returns {{ ok: boolean, data?: any, error?: string, errorCode?: string }}
|
|
240
242
|
*/
|
|
241
243
|
function safeReadJSON(filePath, options = {}) {
|
|
242
|
-
const { defaultValue } = options;
|
|
244
|
+
const { defaultValue, hidePathInError = false, context } = options;
|
|
245
|
+
const pathDisplay = hidePathInError ? context || 'configuration file' : filePath;
|
|
243
246
|
|
|
244
247
|
try {
|
|
245
248
|
if (!fs.existsSync(filePath)) {
|
|
@@ -247,19 +250,58 @@ function safeReadJSON(filePath, options = {}) {
|
|
|
247
250
|
debugLog('safeReadJSON', { filePath, status: 'missing, using default' });
|
|
248
251
|
return { ok: true, data: defaultValue };
|
|
249
252
|
}
|
|
250
|
-
const error = `File not found: ${
|
|
253
|
+
const error = `File not found: ${pathDisplay}`;
|
|
251
254
|
debugLog('safeReadJSON', { filePath, error });
|
|
252
|
-
return { ok: false, error };
|
|
255
|
+
return { ok: false, error, errorCode: 'ENOENT' };
|
|
253
256
|
}
|
|
254
257
|
|
|
255
258
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
259
|
+
|
|
260
|
+
// Handle empty files gracefully
|
|
261
|
+
if (!content.trim()) {
|
|
262
|
+
if (defaultValue !== undefined) {
|
|
263
|
+
debugLog('safeReadJSON', { filePath, status: 'empty, using default' });
|
|
264
|
+
return { ok: true, data: defaultValue };
|
|
265
|
+
}
|
|
266
|
+
const error = `File is empty: ${pathDisplay}`;
|
|
267
|
+
debugLog('safeReadJSON', { filePath, error: 'empty file' });
|
|
268
|
+
return { ok: false, error, errorCode: 'EPARSE' };
|
|
269
|
+
}
|
|
270
|
+
|
|
256
271
|
const data = JSON.parse(content);
|
|
257
272
|
debugLog('safeReadJSON', { filePath, status: 'success' });
|
|
258
273
|
return { ok: true, data };
|
|
259
274
|
} catch (err) {
|
|
260
|
-
|
|
275
|
+
// Detect specific JSON parse errors
|
|
276
|
+
let errorCode = 'EPARSE';
|
|
277
|
+
let errorMessage = `Failed to parse JSON`;
|
|
278
|
+
|
|
279
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
280
|
+
errorCode = 'EACCES';
|
|
281
|
+
errorMessage = `Permission denied reading file`;
|
|
282
|
+
} else if (err.message && err.message.includes('Unexpected')) {
|
|
283
|
+
// JSON syntax error - don't expose full content
|
|
284
|
+
errorMessage = `Invalid JSON syntax`;
|
|
285
|
+
if (err.message.includes('position')) {
|
|
286
|
+
// Extract position info but not content
|
|
287
|
+
const posMatch = err.message.match(/position (\d+)/);
|
|
288
|
+
if (posMatch) {
|
|
289
|
+
errorMessage = `Invalid JSON syntax near position ${posMatch[1]}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} else if (err.message && err.message.includes('token')) {
|
|
293
|
+
errorMessage = `Invalid JSON: unexpected token`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Add context if not hiding path
|
|
297
|
+
if (!hidePathInError) {
|
|
298
|
+
errorMessage = `${errorMessage} in ${pathDisplay}`;
|
|
299
|
+
} else if (context) {
|
|
300
|
+
errorMessage = `${errorMessage} in ${context}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
261
303
|
debugLog('safeReadJSON', { filePath, error: err.message });
|
|
262
|
-
return { ok: false, error };
|
|
304
|
+
return { ok: false, error: errorMessage, errorCode };
|
|
263
305
|
}
|
|
264
306
|
}
|
|
265
307
|
|
package/lib/file-cache.js
CHANGED
|
@@ -336,6 +336,180 @@ function readProjectFiles(rootDir, options = {}) {
|
|
|
336
336
|
};
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
// =============================================================================
|
|
340
|
+
// Command Caching (for git and other shell commands)
|
|
341
|
+
// =============================================================================
|
|
342
|
+
|
|
343
|
+
const { execSync } = require('child_process');
|
|
344
|
+
|
|
345
|
+
// Separate cache for command output with shorter TTL
|
|
346
|
+
const commandCache = new LRUCache({
|
|
347
|
+
maxSize: 50,
|
|
348
|
+
ttlMs: 30000, // 30 seconds default
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Execute and cache a shell command
|
|
353
|
+
* @param {string} command - Shell command to execute
|
|
354
|
+
* @param {Object} [options]
|
|
355
|
+
* @param {string} [options.cwd] - Working directory
|
|
356
|
+
* @param {boolean} [options.force=false] - Skip cache and force execution
|
|
357
|
+
* @param {number} [options.ttlMs] - Custom TTL for this command
|
|
358
|
+
* @param {string} [options.cacheKey] - Custom cache key (default: auto-generated)
|
|
359
|
+
* @returns {{ ok: boolean, data?: string, error?: string, cached?: boolean }}
|
|
360
|
+
*/
|
|
361
|
+
function execCached(command, options = {}) {
|
|
362
|
+
const { cwd = process.cwd(), force = false, ttlMs, cacheKey } = options;
|
|
363
|
+
const key = cacheKey || `cmd:${command}:${cwd}`;
|
|
364
|
+
|
|
365
|
+
// Check cache first (unless force)
|
|
366
|
+
if (!force) {
|
|
367
|
+
const cached = commandCache.get(key);
|
|
368
|
+
if (cached !== undefined) {
|
|
369
|
+
return { ok: true, data: cached, cached: true };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Execute command
|
|
374
|
+
try {
|
|
375
|
+
const output = execSync(command, {
|
|
376
|
+
cwd,
|
|
377
|
+
encoding: 'utf8',
|
|
378
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
379
|
+
timeout: 10000, // 10 second timeout
|
|
380
|
+
}).trim();
|
|
381
|
+
|
|
382
|
+
// Cache the result
|
|
383
|
+
commandCache.set(key, output, ttlMs);
|
|
384
|
+
|
|
385
|
+
return { ok: true, data: output, cached: false };
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// Cache errors briefly to avoid repeated failures
|
|
388
|
+
const errMsg = error.message || 'Command failed';
|
|
389
|
+
return { ok: false, error: errMsg, cached: false };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Execute and cache a git command
|
|
395
|
+
* Helper with git-specific cache key format
|
|
396
|
+
* @param {string} gitCommand - Git subcommand (e.g., 'status --short')
|
|
397
|
+
* @param {Object} [options]
|
|
398
|
+
* @param {string} [options.cwd] - Working directory
|
|
399
|
+
* @param {boolean} [options.force=false] - Skip cache
|
|
400
|
+
* @param {number} [options.ttlMs=30000] - TTL (default 30s)
|
|
401
|
+
* @returns {{ ok: boolean, data?: string, error?: string, cached?: boolean }}
|
|
402
|
+
*/
|
|
403
|
+
function gitCached(gitCommand, options = {}) {
|
|
404
|
+
const { cwd = process.cwd(), ttlMs = 30000, force = false } = options;
|
|
405
|
+
const command = `git ${gitCommand}`;
|
|
406
|
+
const cacheKey = `git:${gitCommand}:${cwd}`;
|
|
407
|
+
|
|
408
|
+
return execCached(command, {
|
|
409
|
+
cwd,
|
|
410
|
+
force,
|
|
411
|
+
ttlMs,
|
|
412
|
+
cacheKey,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Common git commands with caching
|
|
418
|
+
*/
|
|
419
|
+
const gitCommands = {
|
|
420
|
+
/**
|
|
421
|
+
* Get current branch name (cached)
|
|
422
|
+
* @param {string} [cwd] - Working directory
|
|
423
|
+
* @param {Object} [options]
|
|
424
|
+
* @returns {{ ok: boolean, data?: string, cached?: boolean }}
|
|
425
|
+
*/
|
|
426
|
+
branch(cwd, options = {}) {
|
|
427
|
+
return gitCached('branch --show-current', { cwd, ...options });
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Get short status (cached)
|
|
432
|
+
* @param {string} [cwd] - Working directory
|
|
433
|
+
* @param {Object} [options]
|
|
434
|
+
* @returns {{ ok: boolean, data?: string, cached?: boolean }}
|
|
435
|
+
*/
|
|
436
|
+
status(cwd, options = {}) {
|
|
437
|
+
return gitCached('status --short', { cwd, ...options });
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get recent commits (cached)
|
|
442
|
+
* @param {string} [cwd] - Working directory
|
|
443
|
+
* @param {Object} [options]
|
|
444
|
+
* @param {number} [options.count=5] - Number of commits
|
|
445
|
+
* @returns {{ ok: boolean, data?: string, cached?: boolean }}
|
|
446
|
+
*/
|
|
447
|
+
log(cwd, options = {}) {
|
|
448
|
+
const { count = 5, ...rest } = options;
|
|
449
|
+
return gitCached(`log -${count} --oneline`, { cwd, ...rest });
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get diff summary (cached with shorter TTL)
|
|
454
|
+
* @param {string} [cwd] - Working directory
|
|
455
|
+
* @param {Object} [options]
|
|
456
|
+
* @returns {{ ok: boolean, data?: string, cached?: boolean }}
|
|
457
|
+
*/
|
|
458
|
+
diff(cwd, options = {}) {
|
|
459
|
+
return gitCached('diff --stat', { cwd, ttlMs: 15000, ...options });
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get last commit short hash (cached)
|
|
464
|
+
* @param {string} [cwd] - Working directory
|
|
465
|
+
* @param {Object} [options]
|
|
466
|
+
* @returns {{ ok: boolean, data?: string, cached?: boolean }}
|
|
467
|
+
*/
|
|
468
|
+
commitHash(cwd, options = {}) {
|
|
469
|
+
return gitCached('log -1 --format="%h"', { cwd, ...options });
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get last commit message (cached)
|
|
474
|
+
* @param {string} [cwd] - Working directory
|
|
475
|
+
* @param {Object} [options]
|
|
476
|
+
* @returns {{ ok: boolean, data?: string, cached?: boolean }}
|
|
477
|
+
*/
|
|
478
|
+
commitMessage(cwd, options = {}) {
|
|
479
|
+
return gitCached('log -1 --format="%s"', { cwd, ...options });
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Invalidate all git caches for a directory
|
|
485
|
+
* Call this after git operations that modify state
|
|
486
|
+
* @param {string} [cwd] - Working directory
|
|
487
|
+
*/
|
|
488
|
+
function invalidateGitCache(cwd = process.cwd()) {
|
|
489
|
+
const prefix = `git:`;
|
|
490
|
+
const suffix = `:${cwd}`;
|
|
491
|
+
for (const key of commandCache.cache.keys()) {
|
|
492
|
+
if (key.startsWith(prefix) && key.endsWith(suffix)) {
|
|
493
|
+
commandCache.delete(key);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Get command cache statistics
|
|
500
|
+
* @returns {Object} Cache stats
|
|
501
|
+
*/
|
|
502
|
+
function getCommandCacheStats() {
|
|
503
|
+
return commandCache.getStats();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Clear command cache
|
|
508
|
+
*/
|
|
509
|
+
function clearCommandCache() {
|
|
510
|
+
commandCache.clear();
|
|
511
|
+
}
|
|
512
|
+
|
|
339
513
|
module.exports = {
|
|
340
514
|
// Core LRU Cache class (for custom usage)
|
|
341
515
|
LRUCache,
|
|
@@ -356,4 +530,12 @@ module.exports = {
|
|
|
356
530
|
readMetadata,
|
|
357
531
|
readRegistry,
|
|
358
532
|
readProjectFiles,
|
|
533
|
+
|
|
534
|
+
// Command caching
|
|
535
|
+
execCached,
|
|
536
|
+
gitCached,
|
|
537
|
+
gitCommands,
|
|
538
|
+
invalidateGitCache,
|
|
539
|
+
getCommandCacheStats,
|
|
540
|
+
clearCommandCache,
|
|
359
541
|
};
|