agileflow 2.89.3 → 2.90.1

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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/placeholder-registry.js +617 -0
  3. package/lib/smart-json-file.js +228 -1
  4. package/lib/table-formatter.js +519 -0
  5. package/lib/transient-status.js +374 -0
  6. package/lib/ui-manager.js +612 -0
  7. package/lib/validate-args.js +213 -0
  8. package/lib/validate-names.js +143 -0
  9. package/lib/validate-paths.js +434 -0
  10. package/lib/validate.js +37 -737
  11. package/package.json +3 -1
  12. package/scripts/check-update.js +17 -3
  13. package/scripts/lib/sessionRegistry.js +678 -0
  14. package/scripts/session-manager.js +77 -10
  15. package/scripts/tui/App.js +151 -0
  16. package/scripts/tui/index.js +31 -0
  17. package/scripts/tui/lib/crashRecovery.js +304 -0
  18. package/scripts/tui/lib/eventStream.js +309 -0
  19. package/scripts/tui/lib/keyboard.js +261 -0
  20. package/scripts/tui/lib/loopControl.js +371 -0
  21. package/scripts/tui/panels/OutputPanel.js +242 -0
  22. package/scripts/tui/panels/SessionPanel.js +170 -0
  23. package/scripts/tui/panels/TracePanel.js +298 -0
  24. package/scripts/tui/simple-tui.js +390 -0
  25. package/tools/cli/commands/config.js +7 -31
  26. package/tools/cli/commands/doctor.js +28 -39
  27. package/tools/cli/commands/list.js +47 -35
  28. package/tools/cli/commands/status.js +20 -38
  29. package/tools/cli/commands/tui.js +59 -0
  30. package/tools/cli/commands/uninstall.js +12 -39
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +382 -0
  33. package/tools/cli/lib/config-manager.js +394 -0
  34. package/tools/cli/lib/ide-registry.js +186 -0
  35. package/tools/cli/lib/npm-utils.js +17 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * AgileFlow Simple TUI - Terminal User Interface
6
+ *
7
+ * A simple terminal-based dashboard that works without React/ink dependencies.
8
+ * Uses ANSI escape codes for styling and keyboard input.
9
+ *
10
+ * Usage:
11
+ * node scripts/tui/simple-tui.js
12
+ * npx agileflow tui
13
+ *
14
+ * Key bindings:
15
+ * q - Quit TUI
16
+ * s - Start loop on current story
17
+ * p - Pause active loop
18
+ * r - Resume paused loop
19
+ * t - Toggle trace panel
20
+ * 1-9 - Switch session focus
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const readline = require('readline');
26
+
27
+ // ANSI escape codes
28
+ const ANSI = {
29
+ clear: '\x1b[2J',
30
+ home: '\x1b[H',
31
+ reset: '\x1b[0m',
32
+ bold: '\x1b[1m',
33
+ dim: '\x1b[2m',
34
+ italic: '\x1b[3m',
35
+ underline: '\x1b[4m',
36
+ // Colors
37
+ black: '\x1b[30m',
38
+ red: '\x1b[31m',
39
+ green: '\x1b[32m',
40
+ yellow: '\x1b[33m',
41
+ blue: '\x1b[34m',
42
+ magenta: '\x1b[35m',
43
+ cyan: '\x1b[36m',
44
+ white: '\x1b[37m',
45
+ gray: '\x1b[90m',
46
+ // Backgrounds
47
+ bgBlack: '\x1b[40m',
48
+ bgRed: '\x1b[41m',
49
+ bgGreen: '\x1b[42m',
50
+ bgYellow: '\x1b[43m',
51
+ bgBlue: '\x1b[44m',
52
+ bgCyan: '\x1b[46m',
53
+ // Cursor
54
+ hideCursor: '\x1b[?25l',
55
+ showCursor: '\x1b[?25h',
56
+ saveCursor: '\x1b[s',
57
+ restoreCursor: '\x1b[u',
58
+ };
59
+
60
+ // Get project root
61
+ function getProjectRoot() {
62
+ return process.cwd();
63
+ }
64
+
65
+ // Get sessions from registry
66
+ function getSessions() {
67
+ try {
68
+ const registryPath = path.join(getProjectRoot(), '.agileflow', 'sessions', 'registry.json');
69
+ if (!fs.existsSync(registryPath)) {
70
+ return [];
71
+ }
72
+ const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
73
+ const sessions = data.sessions || {};
74
+ // Convert object to array (registry stores sessions as object with ID keys)
75
+ if (Array.isArray(sessions)) {
76
+ return sessions;
77
+ }
78
+ return Object.entries(sessions).map(([id, session]) => ({
79
+ id,
80
+ ...session,
81
+ }));
82
+ } catch (e) {
83
+ return [];
84
+ }
85
+ }
86
+
87
+ // Get loop status
88
+ function getLoopStatus() {
89
+ try {
90
+ const statePath = path.join(getProjectRoot(), 'docs', '09-agents', 'session-state.json');
91
+ if (!fs.existsSync(statePath)) {
92
+ return { active: false };
93
+ }
94
+ const data = JSON.parse(fs.readFileSync(statePath, 'utf8'));
95
+ const loop = data.ralph_loop;
96
+ if (!loop || !loop.enabled) {
97
+ return { active: false };
98
+ }
99
+ return {
100
+ active: true,
101
+ paused: loop.paused || false,
102
+ epic: loop.epic,
103
+ currentStory: loop.current_story,
104
+ iteration: loop.iteration || 0,
105
+ maxIterations: loop.max_iterations || 20,
106
+ };
107
+ } catch (e) {
108
+ return { active: false };
109
+ }
110
+ }
111
+
112
+ // Get recent agent events
113
+ function getAgentEvents(limit = 10) {
114
+ try {
115
+ const logPath = path.join(getProjectRoot(), 'docs', '09-agents', 'bus', 'log.jsonl');
116
+ if (!fs.existsSync(logPath)) {
117
+ return [];
118
+ }
119
+ const content = fs.readFileSync(logPath, 'utf8');
120
+ const lines = content.trim().split('\n').filter(Boolean);
121
+ const events = [];
122
+ for (const line of lines.slice(-limit)) {
123
+ try {
124
+ events.push(JSON.parse(line));
125
+ } catch (e) {
126
+ // Skip invalid JSON
127
+ }
128
+ }
129
+ return events;
130
+ } catch (e) {
131
+ return [];
132
+ }
133
+ }
134
+
135
+ // Draw box with border
136
+ function drawBox(x, y, width, height, title = '', color = 'cyan') {
137
+ const colorCode = ANSI[color] || ANSI.cyan;
138
+ const lines = [];
139
+
140
+ // Top border
141
+ lines.push(`${colorCode}+${'─'.repeat(width - 2)}+${ANSI.reset}`);
142
+
143
+ // Title if provided
144
+ if (title) {
145
+ const titleStr = ` ${title} `;
146
+ const paddingLeft = Math.floor((width - 2 - titleStr.length) / 2);
147
+ const paddingRight = width - 2 - titleStr.length - paddingLeft;
148
+ lines[0] = `${colorCode}+${'─'.repeat(paddingLeft)}${ANSI.bold}${titleStr}${ANSI.reset}${colorCode}${'─'.repeat(paddingRight)}+${ANSI.reset}`;
149
+ }
150
+
151
+ // Content lines
152
+ for (let i = 0; i < height - 2; i++) {
153
+ lines.push(`${colorCode}│${ANSI.reset}${' '.repeat(width - 2)}${colorCode}│${ANSI.reset}`);
154
+ }
155
+
156
+ // Bottom border
157
+ lines.push(`${colorCode}+${'─'.repeat(width - 2)}+${ANSI.reset}`);
158
+
159
+ return lines;
160
+ }
161
+
162
+ // Pad string to width
163
+ function pad(str, width, align = 'left') {
164
+ const s = String(str).slice(0, width);
165
+ const padding = width - s.length;
166
+ if (align === 'right') return ' '.repeat(padding) + s;
167
+ if (align === 'center') return ' '.repeat(Math.floor(padding / 2)) + s + ' '.repeat(Math.ceil(padding / 2));
168
+ return s + ' '.repeat(padding);
169
+ }
170
+
171
+ // Progress bar
172
+ function progressBar(value, max, width = 20) {
173
+ const percent = Math.min(100, Math.max(0, (value / max) * 100));
174
+ const filled = Math.round((percent / 100) * width);
175
+ const empty = width - filled;
176
+
177
+ let color = ANSI.red;
178
+ if (percent >= 80) color = ANSI.green;
179
+ else if (percent >= 50) color = ANSI.yellow;
180
+
181
+ return `${color}${'█'.repeat(filled)}${ANSI.dim}${'░'.repeat(empty)}${ANSI.reset} ${percent.toFixed(0)}%`;
182
+ }
183
+
184
+ // Main TUI class
185
+ class SimpleTUI {
186
+ constructor() {
187
+ this.running = false;
188
+ this.showTrace = true;
189
+ this.lastUpdate = new Date();
190
+ this.messages = [];
191
+ }
192
+
193
+ start() {
194
+ this.running = true;
195
+
196
+ // Set up terminal
197
+ process.stdout.write(ANSI.clear + ANSI.home + ANSI.hideCursor);
198
+
199
+ // Enable raw mode for keyboard input
200
+ if (process.stdin.isTTY) {
201
+ readline.emitKeypressEvents(process.stdin);
202
+ process.stdin.setRawMode(true);
203
+ }
204
+
205
+ // Handle keyboard input
206
+ process.stdin.on('keypress', (str, key) => {
207
+ this.handleKey(key);
208
+ });
209
+
210
+ // Handle terminal resize
211
+ process.stdout.on('resize', () => {
212
+ this.render();
213
+ });
214
+
215
+ // Initial render
216
+ this.render();
217
+
218
+ // Update loop
219
+ this.updateInterval = setInterval(() => {
220
+ this.render();
221
+ }, 2000);
222
+
223
+ this.addMessage('TUI', 'Dashboard started');
224
+ }
225
+
226
+ stop() {
227
+ this.running = false;
228
+
229
+ if (this.updateInterval) {
230
+ clearInterval(this.updateInterval);
231
+ }
232
+
233
+ // Restore terminal
234
+ process.stdout.write(ANSI.showCursor + ANSI.clear + ANSI.home);
235
+
236
+ if (process.stdin.isTTY) {
237
+ process.stdin.setRawMode(false);
238
+ }
239
+
240
+ console.log('AgileFlow TUI closed.');
241
+ process.exit(0);
242
+ }
243
+
244
+ handleKey(key) {
245
+ if (!key) return;
246
+
247
+ const k = key.name || key.sequence;
248
+
249
+ switch (k) {
250
+ case 'q':
251
+ case 'escape':
252
+ this.stop();
253
+ break;
254
+ case 't':
255
+ this.showTrace = !this.showTrace;
256
+ this.addMessage('TUI', `Trace panel ${this.showTrace ? 'shown' : 'hidden'}`);
257
+ this.render();
258
+ break;
259
+ case 's':
260
+ this.addMessage('TUI', 'Start loop requested (not implemented)');
261
+ this.render();
262
+ break;
263
+ case 'p':
264
+ this.addMessage('TUI', 'Pause requested (not implemented)');
265
+ this.render();
266
+ break;
267
+ case 'r':
268
+ this.addMessage('TUI', 'Resume requested (not implemented)');
269
+ this.render();
270
+ break;
271
+ case 'c':
272
+ if (key.ctrl) {
273
+ this.stop();
274
+ }
275
+ break;
276
+ }
277
+ }
278
+
279
+ addMessage(agent, message) {
280
+ const timestamp = new Date().toLocaleTimeString();
281
+ this.messages.push({ timestamp, agent, message });
282
+ if (this.messages.length > 50) {
283
+ this.messages.shift();
284
+ }
285
+ }
286
+
287
+ render() {
288
+ const width = process.stdout.columns || 80;
289
+ const height = process.stdout.rows || 24;
290
+ const output = [];
291
+
292
+ // Clear screen
293
+ output.push(ANSI.clear + ANSI.home);
294
+
295
+ // Header
296
+ const title = ' AgileFlow TUI ';
297
+ const headerPadding = Math.floor((width - title.length) / 2);
298
+ output.push(`${ANSI.bgCyan}${ANSI.black}${'═'.repeat(headerPadding)}${ANSI.bold}${title}${ANSI.reset}${ANSI.bgCyan}${ANSI.black}${'═'.repeat(width - headerPadding - title.length)}${ANSI.reset}`);
299
+ output.push('');
300
+
301
+ // Calculate panel widths
302
+ const leftWidth = Math.floor(width * 0.4);
303
+ const rightWidth = width - leftWidth - 1;
304
+ const panelHeight = height - 6; // Leave room for header and footer
305
+
306
+ // Get data
307
+ const sessions = getSessions();
308
+ const loopStatus = getLoopStatus();
309
+ const agentEvents = getAgentEvents(8);
310
+
311
+ // Build left panel (sessions)
312
+ output.push(`${ANSI.cyan}${ANSI.bold}┌─ SESSIONS ─${'─'.repeat(leftWidth - 14)}┐${ANSI.reset}`);
313
+
314
+ if (sessions.length === 0) {
315
+ output.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}No active sessions${ANSI.reset}${' '.repeat(leftWidth - 21)}${ANSI.cyan}│${ANSI.reset}`);
316
+ } else {
317
+ for (const session of sessions.slice(0, 5)) {
318
+ const indicator = session.current ? `${ANSI.green}>` : ' ';
319
+ const name = `Session ${session.id}${session.is_main ? ' [main]' : ''}`;
320
+ const branch = session.branch || 'unknown';
321
+ const story = session.story || 'none';
322
+
323
+ output.push(`${ANSI.cyan}│${ANSI.reset} ${indicator} ${ANSI.bold}${pad(name, leftWidth - 6)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
324
+ output.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}Branch:${ANSI.reset} ${ANSI.cyan}${pad(branch, leftWidth - 12)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
325
+ output.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}Story:${ANSI.reset} ${ANSI.yellow}${pad(story, leftWidth - 12)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
326
+ }
327
+ }
328
+
329
+ // Fill remaining space in left panel
330
+ const usedRows = sessions.length === 0 ? 1 : Math.min(sessions.length, 5) * 3;
331
+ const remainingRows = Math.max(0, panelHeight - usedRows - 2);
332
+ for (let i = 0; i < remainingRows; i++) {
333
+ output.push(`${ANSI.cyan}│${ANSI.reset}${' '.repeat(leftWidth - 2)}${ANSI.cyan}│${ANSI.reset}`);
334
+ }
335
+
336
+ output.push(`${ANSI.cyan}└${'─'.repeat(leftWidth - 2)}┘${ANSI.reset}`);
337
+
338
+ // Move cursor to right panel position and draw
339
+ // For simplicity, we'll draw the right panel below the left panel
340
+ output.push('');
341
+ output.push(`${ANSI.green}${ANSI.bold}┌─ AGENT OUTPUT ─${'─'.repeat(width - 19)}┐${ANSI.reset}`);
342
+
343
+ if (agentEvents.length === 0 && this.messages.length === 0) {
344
+ output.push(`${ANSI.green}│${ANSI.reset} ${ANSI.dim}Waiting for agent activity...${ANSI.reset}${' '.repeat(width - 34)}${ANSI.green}│${ANSI.reset}`);
345
+ } else {
346
+ // Show recent events
347
+ const allMessages = [...agentEvents.map(e => ({
348
+ timestamp: e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : '',
349
+ agent: e.agent || 'unknown',
350
+ message: e.message || (e.event === 'iteration' ? `Iteration ${e.iter}` : e.event || JSON.stringify(e))
351
+ })), ...this.messages].slice(-8);
352
+
353
+ for (const msg of allMessages) {
354
+ const line = `[${msg.timestamp}] [${ANSI.cyan}${msg.agent}${ANSI.reset}] ${msg.message}`;
355
+ const cleanLine = `[${msg.timestamp}] [${msg.agent}] ${msg.message}`;
356
+ const padding = width - cleanLine.length - 4;
357
+ output.push(`${ANSI.green}│${ANSI.reset} ${line}${' '.repeat(Math.max(0, padding))}${ANSI.green}│${ANSI.reset}`);
358
+ }
359
+ }
360
+
361
+ output.push(`${ANSI.green}└${'─'.repeat(width - 2)}┘${ANSI.reset}`);
362
+
363
+ // Loop status (if active)
364
+ if (loopStatus.active) {
365
+ output.push('');
366
+ const statusIcon = loopStatus.paused ? `${ANSI.yellow}||${ANSI.reset}` : `${ANSI.green}>${ANSI.reset}`;
367
+ output.push(`${statusIcon} ${ANSI.bold}Loop:${ANSI.reset} ${loopStatus.epic || 'unknown'} | Story: ${loopStatus.currentStory || 'none'} | ${progressBar(loopStatus.iteration, loopStatus.maxIterations, 15)}`);
368
+ }
369
+
370
+ // Footer with key bindings
371
+ output.push('');
372
+ output.push(`${ANSI.dim}[Q]uit [S]tart [P]ause [R]esume [T]race [1-9]Sessions ${ANSI.reset}${ANSI.cyan}AgileFlow v2.90.0${ANSI.reset}`);
373
+
374
+ // Output everything
375
+ process.stdout.write(output.join('\n'));
376
+ }
377
+ }
378
+
379
+ // Main entry point
380
+ function main() {
381
+ const tui = new SimpleTUI();
382
+ tui.start();
383
+ }
384
+
385
+ // Run if executed directly
386
+ if (require.main === module) {
387
+ main();
388
+ }
389
+
390
+ module.exports = { SimpleTUI, main };
@@ -12,6 +12,7 @@ const { Installer } = require('../installers/core/installer');
12
12
  const { IdeManager } = require('../installers/ide/manager');
13
13
  const { displayLogo, displaySection, success, warning, error, info } = require('../lib/ui');
14
14
  const { ErrorHandler } = require('../lib/error-handler');
15
+ const { IdeRegistry } = require('../lib/ide-registry');
15
16
 
16
17
  const installer = new Installer();
17
18
  const ideManager = new IdeManager();
@@ -209,11 +210,12 @@ async function handleSet(directory, status, manifestPath, key, value) {
209
210
 
210
211
  case 'ides': {
211
212
  const newIdes = value.split(',').map(ide => ide.trim());
212
- const validIdes = ['claude-code', 'cursor', 'windsurf'];
213
+ const validIdes = IdeRegistry.getAll();
213
214
 
214
215
  // Validate IDEs
215
216
  for (const ide of newIdes) {
216
- if (!validIdes.includes(ide)) {
217
+ const validation = IdeRegistry.validate(ide);
218
+ if (!validation.ok) {
217
219
  handler.warning(
218
220
  `Invalid IDE: ${ide}`,
219
221
  `Valid IDEs: ${validIdes.join(', ')}`,
@@ -261,10 +263,10 @@ async function handleSet(directory, status, manifestPath, key, value) {
261
263
  // Remove old IDE configs
262
264
  for (const ide of oldIdes) {
263
265
  if (!manifest.ides.includes(ide)) {
264
- const configPath = getIdeConfigPath(directory, ide);
266
+ const configPath = IdeRegistry.getConfigPath(ide, directory);
265
267
  if (await fs.pathExists(configPath)) {
266
268
  await fs.remove(configPath);
267
- info(`Removed ${formatIdeName(ide)} configuration`);
269
+ info(`Removed ${IdeRegistry.getDisplayName(ide)} configuration`);
268
270
  }
269
271
  }
270
272
  }
@@ -272,7 +274,7 @@ async function handleSet(directory, status, manifestPath, key, value) {
272
274
  // Add new IDE configs
273
275
  for (const ide of manifest.ides) {
274
276
  await ideManager.setup(ide, directory, status.path);
275
- success(`Updated ${formatIdeName(ide)} configuration`);
277
+ success(`Updated ${IdeRegistry.getDisplayName(ide)} configuration`);
276
278
  }
277
279
 
278
280
  console.log();
@@ -281,29 +283,3 @@ async function handleSet(directory, status, manifestPath, key, value) {
281
283
 
282
284
  console.log();
283
285
  }
284
-
285
- /**
286
- * Get IDE config path
287
- */
288
- function getIdeConfigPath(projectDir, ide) {
289
- const paths = {
290
- 'claude-code': '.claude/commands/agileflow',
291
- cursor: '.cursor/rules/agileflow',
292
- windsurf: '.windsurf/workflows/agileflow',
293
- };
294
-
295
- return path.join(projectDir, paths[ide] || '');
296
- }
297
-
298
- /**
299
- * Format IDE name for display
300
- */
301
- function formatIdeName(ide) {
302
- const names = {
303
- 'claude-code': 'Claude Code',
304
- cursor: 'Cursor',
305
- windsurf: 'Windsurf',
306
- };
307
-
308
- return names[ide] || ide;
309
- }
@@ -28,6 +28,8 @@ const {
28
28
  isRecoverable,
29
29
  } = require('../../../lib/error-codes');
30
30
  const { safeDump } = require('../../../lib/yaml-utils');
31
+ const { IdeRegistry } = require('../lib/ide-registry');
32
+ const { formatKeyValue, formatList, isTTY } = require('../../../lib/table-formatter');
31
33
 
32
34
  const installer = new Installer();
33
35
 
@@ -239,8 +241,8 @@ module.exports = {
239
241
  ideManager.setDocsFolder(status.docsFolder || 'docs');
240
242
 
241
243
  for (const ide of status.ides) {
242
- const configPath = getIdeConfigPath(directory, ide);
243
- const ideName = formatIdeName(ide);
244
+ const configPath = IdeRegistry.getConfigPath(ide, directory);
245
+ const ideName = IdeRegistry.getDisplayName(ide);
244
246
 
245
247
  if (await fs.pathExists(configPath)) {
246
248
  // Count files in config
@@ -265,14 +267,14 @@ module.exports = {
265
267
 
266
268
  // Check for orphaned configs
267
269
  console.log(chalk.bold('\nOrphan Check:'));
268
- const allIdes = ['claude-code', 'cursor', 'windsurf'];
270
+ const allIdes = IdeRegistry.getAll();
269
271
  let orphansFound = false;
270
272
 
271
273
  for (const ide of allIdes) {
272
274
  if (!status.ides || !status.ides.includes(ide)) {
273
- const configPath = getIdeConfigPath(directory, ide);
275
+ const configPath = IdeRegistry.getConfigPath(ide, directory);
274
276
  if (await fs.pathExists(configPath)) {
275
- const ideName = formatIdeName(ide);
277
+ const ideName = IdeRegistry.getDisplayName(ide);
276
278
  warning(`${ideName}: Config exists but not in manifest`);
277
279
  orphansFound = true;
278
280
  warnings++;
@@ -367,37 +369,6 @@ function compareVersions(a, b) {
367
369
  return 0;
368
370
  }
369
371
 
370
- /**
371
- * Get IDE config path
372
- * @param {string} projectDir - Project directory
373
- * @param {string} ide - IDE name
374
- * @returns {string}
375
- */
376
- function getIdeConfigPath(projectDir, ide) {
377
- const paths = {
378
- 'claude-code': '.claude/commands/agileflow',
379
- cursor: '.cursor/rules/agileflow',
380
- windsurf: '.windsurf/workflows/agileflow',
381
- };
382
-
383
- return path.join(projectDir, paths[ide] || '');
384
- }
385
-
386
- /**
387
- * Format IDE name for display
388
- * @param {string} ide - IDE name
389
- * @returns {string}
390
- */
391
- function formatIdeName(ide) {
392
- const names = {
393
- 'claude-code': 'Claude Code',
394
- cursor: 'Cursor',
395
- windsurf: 'Windsurf',
396
- };
397
-
398
- return names[ide] || ide;
399
- }
400
-
401
372
  /**
402
373
  * Count files in directory recursively
403
374
  * @param {string} dirPath - Directory path
@@ -420,7 +391,7 @@ async function countFilesInDir(dirPath) {
420
391
  }
421
392
 
422
393
  /**
423
- * Print summary
394
+ * Print summary using formatKeyValue for consistent output
424
395
  * @param {number} issues - Issue count
425
396
  * @param {number} warnings - Warning count
426
397
  */
@@ -430,9 +401,27 @@ function printSummary(issues, warnings) {
430
401
  if (issues === 0 && warnings === 0) {
431
402
  console.log(chalk.green.bold('No issues found.\n'));
432
403
  } else if (issues === 0) {
433
- console.log(chalk.yellow(`${warnings} warning(s), no critical issues.\n`));
404
+ console.log(
405
+ formatKeyValue(
406
+ {
407
+ Warnings: chalk.yellow(warnings),
408
+ Issues: chalk.green('0'),
409
+ },
410
+ { separator: ':', alignValues: false }
411
+ )
412
+ );
413
+ console.log();
434
414
  } else {
435
- console.log(chalk.red(`${issues} issue(s), ${warnings} warning(s) found.\n`));
415
+ console.log(
416
+ formatKeyValue(
417
+ {
418
+ Issues: chalk.red(issues),
419
+ Warnings: chalk.yellow(warnings),
420
+ },
421
+ { separator: ':', alignValues: false }
422
+ )
423
+ );
424
+ console.log();
436
425
  }
437
426
  }
438
427