agileflow 2.89.1 → 2.89.3
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/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +449 -0
- package/lib/validate.js +165 -11
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/tools/cli/commands/config.js +3 -3
- package/tools/cli/commands/doctor.js +30 -2
- package/tools/cli/commands/list.js +2 -2
- package/tools/cli/commands/uninstall.js +3 -3
- package/tools/cli/installers/core/installer.js +62 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-registry.js - Event Bus for Session Registry
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates all registry I/O operations and emits events for
|
|
5
|
+
* session lifecycle changes. Reduces code duplication in session-manager.js.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Centralized load/save with caching
|
|
9
|
+
* - Event emission for session changes
|
|
10
|
+
* - Batch operations support
|
|
11
|
+
* - Audit trail logging
|
|
12
|
+
*
|
|
13
|
+
* Events:
|
|
14
|
+
* - 'registered': Session was registered { sessionId, session }
|
|
15
|
+
* - 'unregistered': Session was unregistered { sessionId }
|
|
16
|
+
* - 'updated': Session was updated { sessionId, changes }
|
|
17
|
+
* - 'loaded': Registry was loaded { sessionCount }
|
|
18
|
+
* - 'saved': Registry was saved { sessionCount }
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* const { SessionRegistry } = require('./session-registry');
|
|
22
|
+
* const registry = new SessionRegistry('/path/to/project');
|
|
23
|
+
*
|
|
24
|
+
* registry.on('registered', ({ sessionId }) => {
|
|
25
|
+
* console.log(`Session ${sessionId} registered`);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* registry.registerSession(1, { worktree: '/path' });
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const EventEmitter = require('events');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const SmartJsonFile = require('./smart-json-file');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Session Registry Event Bus
|
|
38
|
+
* @extends EventEmitter
|
|
39
|
+
*/
|
|
40
|
+
class SessionRegistry extends EventEmitter {
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} projectRoot - Project root directory
|
|
43
|
+
* @param {Object} [options={}] - Configuration options
|
|
44
|
+
* @param {string} [options.sessionsDir] - Override sessions directory
|
|
45
|
+
* @param {number} [options.cacheTTL=10000] - Cache TTL in ms
|
|
46
|
+
* @param {boolean} [options.auditLog=false] - Enable audit logging
|
|
47
|
+
*/
|
|
48
|
+
constructor(projectRoot, options = {}) {
|
|
49
|
+
super();
|
|
50
|
+
|
|
51
|
+
this.projectRoot = projectRoot;
|
|
52
|
+
this.sessionsDir = options.sessionsDir || path.join(projectRoot, '.agileflow', 'sessions');
|
|
53
|
+
this.registryPath = path.join(this.sessionsDir, 'registry.json');
|
|
54
|
+
this.cacheTTL = options.cacheTTL || 10000; // 10 second cache
|
|
55
|
+
this.auditLog = options.auditLog || false;
|
|
56
|
+
|
|
57
|
+
// Cache
|
|
58
|
+
this._cache = null;
|
|
59
|
+
this._cacheTime = 0;
|
|
60
|
+
|
|
61
|
+
// Batch mode
|
|
62
|
+
this._batchMode = false;
|
|
63
|
+
this._batchChanges = [];
|
|
64
|
+
|
|
65
|
+
// Create SmartJsonFile for safe I/O
|
|
66
|
+
this._jsonFile = new SmartJsonFile(this.registryPath, {
|
|
67
|
+
retries: 3,
|
|
68
|
+
backoff: 100,
|
|
69
|
+
createDir: true,
|
|
70
|
+
defaultValue: this._createDefaultRegistry(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create default empty registry
|
|
76
|
+
* @returns {Object}
|
|
77
|
+
* @private
|
|
78
|
+
*/
|
|
79
|
+
_createDefaultRegistry() {
|
|
80
|
+
return {
|
|
81
|
+
schema_version: '1.0.0',
|
|
82
|
+
next_id: 1,
|
|
83
|
+
project_name: path.basename(this.projectRoot),
|
|
84
|
+
sessions: {},
|
|
85
|
+
created: new Date().toISOString(),
|
|
86
|
+
updated: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ensure sessions directory exists
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
_ensureDir() {
|
|
95
|
+
if (!fs.existsSync(this.sessionsDir)) {
|
|
96
|
+
fs.mkdirSync(this.sessionsDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Log audit event if enabled
|
|
102
|
+
* @param {string} action - Action name
|
|
103
|
+
* @param {Object} details - Action details
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
_auditLog(action, details) {
|
|
107
|
+
if (!this.auditLog) return;
|
|
108
|
+
|
|
109
|
+
const logPath = path.join(this.sessionsDir, 'audit.log');
|
|
110
|
+
const entry = {
|
|
111
|
+
timestamp: new Date().toISOString(),
|
|
112
|
+
action,
|
|
113
|
+
...details,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + '\n');
|
|
118
|
+
} catch {
|
|
119
|
+
// Ignore audit log errors
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load registry (with caching)
|
|
125
|
+
* @param {boolean} [force=false] - Force reload ignoring cache
|
|
126
|
+
* @returns {Promise<Object>} Registry data
|
|
127
|
+
*/
|
|
128
|
+
async load(force = false) {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
|
|
131
|
+
// Return cached if valid and not forced
|
|
132
|
+
if (!force && this._cache && now - this._cacheTime < this.cacheTTL) {
|
|
133
|
+
return this._cache;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._ensureDir();
|
|
137
|
+
const result = await this._jsonFile.read();
|
|
138
|
+
|
|
139
|
+
if (result.ok) {
|
|
140
|
+
this._cache = result.data;
|
|
141
|
+
this._cacheTime = now;
|
|
142
|
+
this.emit('loaded', { sessionCount: Object.keys(result.data.sessions || {}).length });
|
|
143
|
+
return result.data;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Return default on error
|
|
147
|
+
const defaultRegistry = this._createDefaultRegistry();
|
|
148
|
+
this._cache = defaultRegistry;
|
|
149
|
+
this._cacheTime = now;
|
|
150
|
+
return defaultRegistry;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Load registry synchronously (for compatibility)
|
|
155
|
+
* @returns {Object} Registry data
|
|
156
|
+
*/
|
|
157
|
+
loadSync() {
|
|
158
|
+
this._ensureDir();
|
|
159
|
+
const result = this._jsonFile.readSync();
|
|
160
|
+
|
|
161
|
+
if (result.ok) {
|
|
162
|
+
this._cache = result.data;
|
|
163
|
+
this._cacheTime = Date.now();
|
|
164
|
+
return result.data;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return this._createDefaultRegistry();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Save registry
|
|
172
|
+
* @param {Object} registry - Registry data to save
|
|
173
|
+
* @returns {Promise<{ok: boolean, error?: Error}>}
|
|
174
|
+
*/
|
|
175
|
+
async save(registry) {
|
|
176
|
+
registry.updated = new Date().toISOString();
|
|
177
|
+
const result = await this._jsonFile.write(registry);
|
|
178
|
+
|
|
179
|
+
if (result.ok) {
|
|
180
|
+
this._cache = registry;
|
|
181
|
+
this._cacheTime = Date.now();
|
|
182
|
+
this._auditLog('save', { sessionCount: Object.keys(registry.sessions || {}).length });
|
|
183
|
+
this.emit('saved', { sessionCount: Object.keys(registry.sessions || {}).length });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Save registry synchronously (for compatibility)
|
|
191
|
+
* @param {Object} registry - Registry data to save
|
|
192
|
+
* @returns {{ok: boolean, error?: Error}}
|
|
193
|
+
*/
|
|
194
|
+
saveSync(registry) {
|
|
195
|
+
registry.updated = new Date().toISOString();
|
|
196
|
+
const result = this._jsonFile.writeSync(registry);
|
|
197
|
+
|
|
198
|
+
if (result.ok) {
|
|
199
|
+
this._cache = registry;
|
|
200
|
+
this._cacheTime = Date.now();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clear cache
|
|
208
|
+
*/
|
|
209
|
+
clearCache() {
|
|
210
|
+
this._cache = null;
|
|
211
|
+
this._cacheTime = 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get a session by ID
|
|
216
|
+
* @param {number|string} sessionId - Session ID
|
|
217
|
+
* @returns {Promise<Object|null>}
|
|
218
|
+
*/
|
|
219
|
+
async getSession(sessionId) {
|
|
220
|
+
const registry = await this.load();
|
|
221
|
+
return registry.sessions?.[sessionId] || null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get all sessions
|
|
226
|
+
* @returns {Promise<Object>} Map of sessionId -> session
|
|
227
|
+
*/
|
|
228
|
+
async getAllSessions() {
|
|
229
|
+
const registry = await this.load();
|
|
230
|
+
return registry.sessions || {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get next available session ID
|
|
235
|
+
* @returns {Promise<number>}
|
|
236
|
+
*/
|
|
237
|
+
async getNextId() {
|
|
238
|
+
const registry = await this.load();
|
|
239
|
+
return registry.next_id || 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Register a new session
|
|
244
|
+
* @param {number|string} sessionId - Session ID
|
|
245
|
+
* @param {Object} sessionData - Session data
|
|
246
|
+
* @returns {Promise<{ok: boolean, error?: Error}>}
|
|
247
|
+
*/
|
|
248
|
+
async registerSession(sessionId, sessionData) {
|
|
249
|
+
const registry = await this.load(true); // Force reload for freshness
|
|
250
|
+
|
|
251
|
+
registry.sessions = registry.sessions || {};
|
|
252
|
+
registry.sessions[sessionId] = {
|
|
253
|
+
...sessionData,
|
|
254
|
+
registered_at: new Date().toISOString(),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Update next_id if needed
|
|
258
|
+
const numericId = parseInt(sessionId, 10);
|
|
259
|
+
if (!isNaN(numericId) && numericId >= registry.next_id) {
|
|
260
|
+
registry.next_id = numericId + 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const result = await this.save(registry);
|
|
264
|
+
|
|
265
|
+
if (result.ok) {
|
|
266
|
+
this._auditLog('register', { sessionId, sessionData });
|
|
267
|
+
this.emit('registered', { sessionId, session: registry.sessions[sessionId] });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Unregister a session
|
|
275
|
+
* @param {number|string} sessionId - Session ID
|
|
276
|
+
* @returns {Promise<{ok: boolean, found: boolean, error?: Error}>}
|
|
277
|
+
*/
|
|
278
|
+
async unregisterSession(sessionId) {
|
|
279
|
+
const registry = await this.load(true);
|
|
280
|
+
|
|
281
|
+
if (!registry.sessions || !registry.sessions[sessionId]) {
|
|
282
|
+
return { ok: true, found: false };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
delete registry.sessions[sessionId];
|
|
286
|
+
const result = await this.save(registry);
|
|
287
|
+
|
|
288
|
+
if (result.ok) {
|
|
289
|
+
this._auditLog('unregister', { sessionId });
|
|
290
|
+
this.emit('unregistered', { sessionId });
|
|
291
|
+
return { ok: true, found: true };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { ...result, found: true };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Update a session
|
|
299
|
+
* @param {number|string} sessionId - Session ID
|
|
300
|
+
* @param {Object} updates - Fields to update
|
|
301
|
+
* @returns {Promise<{ok: boolean, found: boolean, error?: Error}>}
|
|
302
|
+
*/
|
|
303
|
+
async updateSession(sessionId, updates) {
|
|
304
|
+
const registry = await this.load(true);
|
|
305
|
+
|
|
306
|
+
if (!registry.sessions || !registry.sessions[sessionId]) {
|
|
307
|
+
return { ok: false, found: false, error: new Error(`Session ${sessionId} not found`) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
registry.sessions[sessionId] = {
|
|
311
|
+
...registry.sessions[sessionId],
|
|
312
|
+
...updates,
|
|
313
|
+
updated_at: new Date().toISOString(),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const result = await this.save(registry);
|
|
317
|
+
|
|
318
|
+
if (result.ok) {
|
|
319
|
+
this._auditLog('update', { sessionId, updates });
|
|
320
|
+
this.emit('updated', { sessionId, changes: updates });
|
|
321
|
+
return { ok: true, found: true };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { ...result, found: true };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Start batch mode - changes are queued until commitBatch()
|
|
329
|
+
*/
|
|
330
|
+
startBatch() {
|
|
331
|
+
this._batchMode = true;
|
|
332
|
+
this._batchChanges = [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Add operation to batch
|
|
337
|
+
* @param {string} operation - 'register', 'unregister', 'update'
|
|
338
|
+
* @param {Object} params - Operation parameters
|
|
339
|
+
*/
|
|
340
|
+
addToBatch(operation, params) {
|
|
341
|
+
if (!this._batchMode) {
|
|
342
|
+
throw new Error('Not in batch mode. Call startBatch() first.');
|
|
343
|
+
}
|
|
344
|
+
this._batchChanges.push({ operation, params });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Commit all batched changes in one write
|
|
349
|
+
* @returns {Promise<{ok: boolean, applied: number, error?: Error}>}
|
|
350
|
+
*/
|
|
351
|
+
async commitBatch() {
|
|
352
|
+
if (!this._batchMode) {
|
|
353
|
+
return { ok: false, applied: 0, error: new Error('Not in batch mode') };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const registry = await this.load(true);
|
|
357
|
+
let applied = 0;
|
|
358
|
+
|
|
359
|
+
for (const change of this._batchChanges) {
|
|
360
|
+
const { operation, params } = change;
|
|
361
|
+
|
|
362
|
+
switch (operation) {
|
|
363
|
+
case 'register': {
|
|
364
|
+
registry.sessions = registry.sessions || {};
|
|
365
|
+
registry.sessions[params.sessionId] = {
|
|
366
|
+
...params.sessionData,
|
|
367
|
+
registered_at: new Date().toISOString(),
|
|
368
|
+
};
|
|
369
|
+
const numericId = parseInt(params.sessionId, 10);
|
|
370
|
+
if (!isNaN(numericId) && numericId >= registry.next_id) {
|
|
371
|
+
registry.next_id = numericId + 1;
|
|
372
|
+
}
|
|
373
|
+
applied++;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case 'unregister': {
|
|
377
|
+
if (registry.sessions?.[params.sessionId]) {
|
|
378
|
+
delete registry.sessions[params.sessionId];
|
|
379
|
+
applied++;
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case 'update': {
|
|
384
|
+
if (registry.sessions?.[params.sessionId]) {
|
|
385
|
+
registry.sessions[params.sessionId] = {
|
|
386
|
+
...registry.sessions[params.sessionId],
|
|
387
|
+
...params.updates,
|
|
388
|
+
updated_at: new Date().toISOString(),
|
|
389
|
+
};
|
|
390
|
+
applied++;
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const result = await this.save(registry);
|
|
398
|
+
|
|
399
|
+
// Clear batch state
|
|
400
|
+
this._batchMode = false;
|
|
401
|
+
this._batchChanges = [];
|
|
402
|
+
|
|
403
|
+
if (result.ok) {
|
|
404
|
+
this._auditLog('batch', { applied });
|
|
405
|
+
return { ok: true, applied };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { ...result, applied };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Cancel batch mode without saving
|
|
413
|
+
*/
|
|
414
|
+
cancelBatch() {
|
|
415
|
+
this._batchMode = false;
|
|
416
|
+
this._batchChanges = [];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Count sessions by status
|
|
421
|
+
* @returns {Promise<{total: number, active: number, inactive: number}>}
|
|
422
|
+
*/
|
|
423
|
+
async countSessions() {
|
|
424
|
+
const registry = await this.load();
|
|
425
|
+
const sessions = Object.values(registry.sessions || {});
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
total: sessions.length,
|
|
429
|
+
active: sessions.filter(s => s.status === 'active').length,
|
|
430
|
+
inactive: sessions.filter(s => s.status !== 'active').length,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Clean up stale sessions
|
|
436
|
+
* @param {Function} isAlive - Function to check if session is alive (sessionId) => boolean
|
|
437
|
+
* @returns {Promise<{ok: boolean, cleaned: number}>}
|
|
438
|
+
*/
|
|
439
|
+
async cleanupStaleSessions(isAlive) {
|
|
440
|
+
const registry = await this.load(true);
|
|
441
|
+
let cleaned = 0;
|
|
442
|
+
|
|
443
|
+
for (const [sessionId, session] of Object.entries(registry.sessions || {})) {
|
|
444
|
+
if (!isAlive(sessionId, session)) {
|
|
445
|
+
delete registry.sessions[sessionId];
|
|
446
|
+
cleaned++;
|
|
447
|
+
this._auditLog('cleanup', { sessionId });
|
|
448
|
+
this.emit('unregistered', { sessionId });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (cleaned > 0) {
|
|
453
|
+
const result = await this.save(registry);
|
|
454
|
+
return { ok: result.ok, cleaned };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { ok: true, cleaned: 0 };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = { SessionRegistry };
|