agileflow 2.89.2 → 2.90.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 (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/content-sanitizer.js +463 -0
  4. package/lib/error-codes.js +544 -0
  5. package/lib/errors.js +336 -5
  6. package/lib/feedback.js +561 -0
  7. package/lib/path-resolver.js +396 -0
  8. package/lib/placeholder-registry.js +617 -0
  9. package/lib/session-registry.js +461 -0
  10. package/lib/smart-json-file.js +653 -0
  11. package/lib/table-formatter.js +504 -0
  12. package/lib/transient-status.js +374 -0
  13. package/lib/ui-manager.js +612 -0
  14. package/lib/validate-args.js +213 -0
  15. package/lib/validate-names.js +143 -0
  16. package/lib/validate-paths.js +434 -0
  17. package/lib/validate.js +38 -584
  18. package/package.json +4 -1
  19. package/scripts/agileflow-configure.js +40 -1440
  20. package/scripts/agileflow-welcome.js +2 -1
  21. package/scripts/check-update.js +16 -3
  22. package/scripts/lib/configure-detect.js +383 -0
  23. package/scripts/lib/configure-features.js +811 -0
  24. package/scripts/lib/configure-repair.js +314 -0
  25. package/scripts/lib/configure-utils.js +115 -0
  26. package/scripts/lib/frontmatter-parser.js +3 -3
  27. package/scripts/lib/sessionRegistry.js +682 -0
  28. package/scripts/obtain-context.js +417 -113
  29. package/scripts/ralph-loop.js +1 -1
  30. package/scripts/session-manager.js +77 -10
  31. package/scripts/tui/App.js +176 -0
  32. package/scripts/tui/index.js +75 -0
  33. package/scripts/tui/lib/crashRecovery.js +302 -0
  34. package/scripts/tui/lib/eventStream.js +316 -0
  35. package/scripts/tui/lib/keyboard.js +252 -0
  36. package/scripts/tui/lib/loopControl.js +371 -0
  37. package/scripts/tui/panels/OutputPanel.js +278 -0
  38. package/scripts/tui/panels/SessionPanel.js +178 -0
  39. package/scripts/tui/panels/TracePanel.js +333 -0
  40. package/src/core/commands/tui.md +91 -0
  41. package/tools/cli/commands/config.js +10 -33
  42. package/tools/cli/commands/doctor.js +48 -40
  43. package/tools/cli/commands/list.js +49 -37
  44. package/tools/cli/commands/status.js +13 -37
  45. package/tools/cli/commands/uninstall.js +12 -41
  46. package/tools/cli/installers/core/installer.js +75 -12
  47. package/tools/cli/installers/ide/_interface.js +238 -0
  48. package/tools/cli/installers/ide/codex.js +2 -2
  49. package/tools/cli/installers/ide/manager.js +15 -0
  50. package/tools/cli/lib/command-context.js +374 -0
  51. package/tools/cli/lib/config-manager.js +394 -0
  52. package/tools/cli/lib/content-injector.js +69 -16
  53. package/tools/cli/lib/ide-errors.js +163 -29
  54. package/tools/cli/lib/ide-registry.js +186 -0
  55. package/tools/cli/lib/npm-utils.js +16 -3
  56. package/tools/cli/lib/self-update.js +148 -0
  57. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -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 };