agileflow 2.89.3 → 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 (37) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +3 -3
  3. package/lib/placeholder-registry.js +617 -0
  4. package/lib/smart-json-file.js +205 -1
  5. package/lib/table-formatter.js +504 -0
  6. package/lib/transient-status.js +374 -0
  7. package/lib/ui-manager.js +612 -0
  8. package/lib/validate-args.js +213 -0
  9. package/lib/validate-names.js +143 -0
  10. package/lib/validate-paths.js +434 -0
  11. package/lib/validate.js +37 -737
  12. package/package.json +4 -1
  13. package/scripts/check-update.js +16 -3
  14. package/scripts/lib/sessionRegistry.js +682 -0
  15. package/scripts/session-manager.js +77 -10
  16. package/scripts/tui/App.js +176 -0
  17. package/scripts/tui/index.js +75 -0
  18. package/scripts/tui/lib/crashRecovery.js +302 -0
  19. package/scripts/tui/lib/eventStream.js +316 -0
  20. package/scripts/tui/lib/keyboard.js +252 -0
  21. package/scripts/tui/lib/loopControl.js +371 -0
  22. package/scripts/tui/panels/OutputPanel.js +278 -0
  23. package/scripts/tui/panels/SessionPanel.js +178 -0
  24. package/scripts/tui/panels/TracePanel.js +333 -0
  25. package/src/core/commands/tui.md +91 -0
  26. package/tools/cli/commands/config.js +7 -30
  27. package/tools/cli/commands/doctor.js +18 -38
  28. package/tools/cli/commands/list.js +47 -35
  29. package/tools/cli/commands/status.js +13 -37
  30. package/tools/cli/commands/uninstall.js +9 -38
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +374 -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 +16 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -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,36 +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
372
 
401
373
  /**
402
374
  * Count files in directory recursively
@@ -420,7 +392,7 @@ async function countFilesInDir(dirPath) {
420
392
  }
421
393
 
422
394
  /**
423
- * Print summary
395
+ * Print summary using formatKeyValue for consistent output
424
396
  * @param {number} issues - Issue count
425
397
  * @param {number} warnings - Warning count
426
398
  */
@@ -430,9 +402,17 @@ function printSummary(issues, warnings) {
430
402
  if (issues === 0 && warnings === 0) {
431
403
  console.log(chalk.green.bold('No issues found.\n'));
432
404
  } else if (issues === 0) {
433
- console.log(chalk.yellow(`${warnings} warning(s), no critical issues.\n`));
405
+ console.log(formatKeyValue({
406
+ Warnings: chalk.yellow(warnings),
407
+ Issues: chalk.green('0'),
408
+ }, { separator: ':', alignValues: false }));
409
+ console.log();
434
410
  } else {
435
- console.log(chalk.red(`${issues} issue(s), ${warnings} warning(s) found.\n`));
411
+ console.log(formatKeyValue({
412
+ Issues: chalk.red(issues),
413
+ Warnings: chalk.yellow(warnings),
414
+ }, { separator: ':', alignValues: false }));
415
+ console.log();
436
416
  }
437
417
  }
438
418
 
@@ -13,6 +13,7 @@ const { displayLogo, displaySection, success, warning, info } = require('../lib/
13
13
  const {
14
14
  parseFrontmatter: parseYamlFrontmatter,
15
15
  } = require('../../../scripts/lib/frontmatter-parser');
16
+ const { formatList, formatKeyValue, formatHeader, isTTY } = require('../../../lib/table-formatter');
16
17
 
17
18
  const installer = new Installer();
18
19
 
@@ -300,73 +301,84 @@ function extractFirstLine(content) {
300
301
  }
301
302
 
302
303
  /**
303
- * Display compact output
304
+ * Display compact output using formatKeyValue
304
305
  */
305
306
  function displayCompact(result, showCommands, showAgents, showSkills, showExperts) {
307
+ const data = {};
308
+
306
309
  if (showCommands && result.commands?.length > 0) {
307
- console.log(chalk.bold('Commands:'), result.commands.map(c => c.name).join(', '));
310
+ data.Commands = result.commands.map(c => c.name).join(', ');
308
311
  }
309
312
 
310
313
  if (showAgents && result.agents?.length > 0) {
311
- console.log(chalk.bold('Agents:'), result.agents.map(a => a.name).join(', '));
314
+ data.Agents = result.agents.map(a => a.name).join(', ');
312
315
  }
313
316
 
314
317
  if (showSkills && result.skills?.length > 0) {
315
- console.log(chalk.bold('Skills:'), result.skills.map(s => s.name).join(', '));
318
+ data.Skills = result.skills.map(s => s.name).join(', ');
316
319
  }
317
320
 
318
321
  if (showExperts && result.experts?.length > 0) {
319
- console.log(chalk.bold('Experts:'), result.experts.map(e => e.name).join(', '));
322
+ data.Experts = result.experts.map(e => e.name).join(', ');
323
+ }
324
+
325
+ if (Object.keys(data).length > 0) {
326
+ console.log(formatKeyValue(data, { alignValues: false }));
320
327
  }
321
328
  }
322
329
 
323
330
  /**
324
- * Display full output with descriptions
331
+ * Display full output with descriptions using formatList
325
332
  */
326
333
  function displayFull(result, showCommands, showAgents, showSkills, showExperts) {
327
- if (showCommands && result.commands?.length > 0) {
328
- displaySection(`Commands (${result.commands.length})`);
334
+ const { BRAND_HEX } = require('../../../lib/colors');
329
335
 
330
- for (const cmd of result.commands) {
331
- console.log(chalk.hex('#e8683a')(` ${cmd.name}`));
332
- console.log(chalk.dim(` ${cmd.description}`));
333
- }
336
+ if (showCommands && result.commands?.length > 0) {
337
+ console.log(formatHeader(`Commands (${result.commands.length})`));
338
+ const items = result.commands.map(cmd => ({
339
+ text: `${chalk.hex(BRAND_HEX)(cmd.name)}\n ${chalk.dim(cmd.description)}`,
340
+ status: 'active',
341
+ }));
342
+ console.log(formatList(items, { indent: ' ' }));
334
343
  }
335
344
 
336
345
  if (showAgents && result.agents?.length > 0) {
337
- displaySection(`Agents (${result.agents.length})`);
338
-
339
- for (const agent of result.agents) {
346
+ console.log(formatHeader(`Agents (${result.agents.length})`));
347
+ const items = result.agents.map(agent => {
340
348
  const modelBadge = agent.model !== 'default' ? chalk.dim(` [${agent.model}]`) : '';
341
- console.log(chalk.hex('#e8683a')(` ${agent.name}`) + modelBadge);
342
- console.log(chalk.dim(` ${agent.description}`));
343
- }
349
+ return {
350
+ text: `${chalk.hex(BRAND_HEX)(agent.name)}${modelBadge}\n ${chalk.dim(agent.description)}`,
351
+ status: 'active',
352
+ };
353
+ });
354
+ console.log(formatList(items, { indent: ' ' }));
344
355
  }
345
356
 
346
357
  if (showSkills && result.skills?.length > 0) {
347
- displaySection(`Skills (${result.skills.length})`);
348
-
349
- for (const skill of result.skills) {
350
- console.log(chalk.hex('#e8683a')(` ${skill.name}`));
351
- console.log(chalk.dim(` ${skill.description}`));
358
+ console.log(formatHeader(`Skills (${result.skills.length})`));
359
+ const items = result.skills.map(skill => {
360
+ let desc = chalk.dim(skill.description);
352
361
  if (skill.triggers?.length > 0) {
353
- console.log(
354
- chalk.dim(
355
- ` Triggers: ${skill.triggers.slice(0, 3).join(', ')}${skill.triggers.length > 3 ? '...' : ''}`
356
- )
357
- );
362
+ desc += `\n ${chalk.dim(`Triggers: ${skill.triggers.slice(0, 3).join(', ')}${skill.triggers.length > 3 ? '...' : ''}`)}`;
358
363
  }
359
- }
364
+ return {
365
+ text: `${chalk.hex(BRAND_HEX)(skill.name)}\n ${desc}`,
366
+ status: 'active',
367
+ };
368
+ });
369
+ console.log(formatList(items, { indent: ' ' }));
360
370
  }
361
371
 
362
372
  if (showExperts && result.experts?.length > 0) {
363
- displaySection(`Experts (${result.experts.length})`);
364
-
365
- for (const expert of result.experts) {
373
+ console.log(formatHeader(`Experts (${result.experts.length})`));
374
+ const items = result.experts.map(expert => {
366
375
  const versionBadge = expert.version !== 'unknown' ? chalk.dim(` v${expert.version}`) : '';
367
- console.log(chalk.hex('#e8683a')(` ${expert.name}`) + versionBadge);
368
- console.log(chalk.dim(` ${expert.description}`));
369
- }
376
+ return {
377
+ text: `${chalk.hex(BRAND_HEX)(expert.name)}${versionBadge}\n ${chalk.dim(expert.description)}`,
378
+ status: 'active',
379
+ };
380
+ });
381
+ console.log(formatList(items, { indent: ' ' }));
370
382
  }
371
383
 
372
384
  console.log(); // Final newline
@@ -11,6 +11,8 @@ const ora = require('ora');
11
11
  const { Installer } = require('../installers/core/installer');
12
12
  const { displayLogo, displaySection, success, warning, info } = require('../lib/ui');
13
13
  const { checkForUpdate } = require('../lib/version-checker');
14
+ const { IdeRegistry } = require('../lib/ide-registry');
15
+ const { formatKeyValue, formatList, isTTY } = require('../../../lib/table-formatter');
14
16
 
15
17
  const installer = new Installer();
16
18
 
@@ -33,14 +35,18 @@ module.exports = {
33
35
  process.exit(0);
34
36
  }
35
37
 
36
- // Show installation info
37
- console.log(chalk.bold('Location: '), status.path);
38
- console.log(chalk.bold('Version: '), status.version);
38
+ // Show installation info using formatKeyValue
39
+ console.log(formatKeyValue({
40
+ Location: status.path,
41
+ Version: status.version,
42
+ }));
39
43
 
40
44
  // Count installed items
41
45
  const counts = await installer.countInstalledItems(status.path);
42
46
 
43
- console.log(chalk.bold('\nCore: '), chalk.green('✓ Installed'));
47
+ console.log(formatKeyValue({
48
+ '\nCore': chalk.green('✓ Installed'),
49
+ }, { alignValues: false }));
44
50
  info(`${counts.agents} agents`);
45
51
  info(`${counts.commands} commands`);
46
52
  info(`${counts.skills} skills`);
@@ -50,13 +56,13 @@ module.exports = {
50
56
  console.log(chalk.bold('\nConfigured IDEs:'));
51
57
  for (const ide of status.ides) {
52
58
  // Check if IDE config exists
53
- const ideConfigPath = getIdeConfigPath(directory, ide);
59
+ const ideConfigPath = IdeRegistry.getConfigPath(ide, directory);
54
60
  const exists = await fs.pathExists(ideConfigPath);
55
61
 
56
62
  if (exists) {
57
- success(formatIdeName(ide));
63
+ success(IdeRegistry.getDisplayName(ide));
58
64
  } else {
59
- warning(`${formatIdeName(ide)} (config missing)`);
65
+ warning(`${IdeRegistry.getDisplayName(ide)} (config missing)`);
60
66
  }
61
67
  }
62
68
  }
@@ -87,33 +93,3 @@ module.exports = {
87
93
  },
88
94
  };
89
95
 
90
- /**
91
- * Get IDE config path
92
- * @param {string} projectDir - Project directory
93
- * @param {string} ide - IDE name
94
- * @returns {string}
95
- */
96
- function getIdeConfigPath(projectDir, ide) {
97
- const paths = {
98
- 'claude-code': '.claude/commands/agileflow',
99
- cursor: '.cursor/rules/agileflow',
100
- windsurf: '.windsurf/workflows/agileflow',
101
- };
102
-
103
- return path.join(projectDir, paths[ide] || '');
104
- }
105
-
106
- /**
107
- * Format IDE name for display
108
- * @param {string} ide - IDE name
109
- * @returns {string}
110
- */
111
- function formatIdeName(ide) {
112
- const names = {
113
- 'claude-code': 'Claude Code',
114
- cursor: 'Cursor',
115
- windsurf: 'Windsurf',
116
- };
117
-
118
- return names[ide] || ide;
119
- }
@@ -11,6 +11,7 @@ const { Installer } = require('../installers/core/installer');
11
11
  const { IdeManager } = require('../installers/ide/manager');
12
12
  const { displayLogo, displaySection, success, warning, error, confirm } = require('../lib/ui');
13
13
  const { ErrorHandler } = require('../lib/error-handler');
14
+ const { IdeRegistry } = require('../lib/ide-registry');
14
15
 
15
16
  const installer = new Installer();
16
17
  const ideManager = new IdeManager();
@@ -40,17 +41,17 @@ module.exports = {
40
41
  // Check if removing just one IDE
41
42
  if (options.ide) {
42
43
  const ideName = options.ide.toLowerCase();
43
- displaySection('Removing IDE Configuration', `IDE: ${formatIdeName(ideName)}`);
44
+ displaySection('Removing IDE Configuration', `IDE: ${IdeRegistry.getDisplayName(ideName)}`);
44
45
 
45
46
  if (!status.ides || !status.ides.includes(ideName)) {
46
- warning(`${formatIdeName(ideName)} is not configured in this installation`);
47
+ warning(`${IdeRegistry.getDisplayName(ideName)} is not configured in this installation`);
47
48
  console.log(chalk.dim(`Configured IDEs: ${(status.ides || []).join(', ') || 'none'}\n`));
48
49
  process.exit(0);
49
50
  }
50
51
 
51
52
  // Confirm removal
52
53
  if (!options.force) {
53
- const proceed = await confirm(`Remove ${formatIdeName(ideName)} configuration?`, false);
54
+ const proceed = await confirm(`Remove ${IdeRegistry.getDisplayName(ideName)} configuration?`, false);
54
55
  if (!proceed) {
55
56
  console.log(chalk.dim('\nCancelled\n'));
56
57
  process.exit(0);
@@ -60,10 +61,10 @@ module.exports = {
60
61
  console.log();
61
62
 
62
63
  // Remove the IDE configuration
63
- const configPath = getIdeConfigPath(directory, ideName);
64
+ const configPath = IdeRegistry.getConfigPath(ideName, directory);
64
65
  if (await fs.pathExists(configPath)) {
65
66
  await fs.remove(configPath);
66
- success(`Removed ${formatIdeName(ideName)} configuration`);
67
+ success(`Removed ${IdeRegistry.getDisplayName(ideName)} configuration`);
67
68
  }
68
69
 
69
70
  // Also remove spawnable agents for claude-code
@@ -87,7 +88,7 @@ module.exports = {
87
88
  success('Updated manifest');
88
89
  }
89
90
 
90
- console.log(chalk.green(`\n${formatIdeName(ideName)} has been removed.\n`));
91
+ console.log(chalk.green(`\n${IdeRegistry.getDisplayName(ideName)} has been removed.\n`));
91
92
  if (status.ides.length > 1) {
92
93
  console.log(
93
94
  chalk.dim(`Remaining IDEs: ${status.ides.filter(i => i !== ideName).join(', ')}\n`)
@@ -114,10 +115,10 @@ module.exports = {
114
115
  // Remove IDE configurations
115
116
  if (status.ides && status.ides.length > 0) {
116
117
  for (const ide of status.ides) {
117
- const configPath = getIdeConfigPath(directory, ide);
118
+ const configPath = IdeRegistry.getConfigPath(ide, directory);
118
119
  if (await fs.pathExists(configPath)) {
119
120
  await fs.remove(configPath);
120
- success(`Removed ${formatIdeName(ide)} configuration`);
121
+ success(`Removed ${IdeRegistry.getDisplayName(ide)} configuration`);
121
122
  }
122
123
  // Also remove spawnable agents for claude-code
123
124
  if (ide === 'claude-code') {
@@ -150,33 +151,3 @@ module.exports = {
150
151
  },
151
152
  };
152
153
 
153
- /**
154
- * Get IDE config path
155
- * @param {string} projectDir - Project directory
156
- * @param {string} ide - IDE name
157
- * @returns {string}
158
- */
159
- function getIdeConfigPath(projectDir, ide) {
160
- const paths = {
161
- 'claude-code': '.claude/commands/agileflow',
162
- cursor: '.cursor/rules/agileflow',
163
- windsurf: '.windsurf/workflows/agileflow',
164
- };
165
-
166
- return path.join(projectDir, paths[ide] || '');
167
- }
168
-
169
- /**
170
- * Format IDE name for display
171
- * @param {string} ide - IDE name
172
- * @returns {string}
173
- */
174
- function formatIdeName(ide) {
175
- const names = {
176
- 'claude-code': 'Claude Code',
177
- cursor: 'Cursor',
178
- windsurf: 'Windsurf',
179
- };
180
-
181
- return names[ide] || ide;
182
- }
@@ -17,6 +17,7 @@ const {
17
17
  getErrorCodeFromError,
18
18
  attachErrorCode,
19
19
  } = require('../../../../lib/error-codes');
20
+ const { setSecurePermissions, SECURE_FILE_MODE } = require('../../../../lib/smart-json-file');
20
21
 
21
22
  const TEXT_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.txt', '.json']);
22
23
 
@@ -504,6 +505,8 @@ class Installer {
504
505
  };
505
506
 
506
507
  await fs.writeFile(configPath, safeDump(config), 'utf8');
508
+ // Security: Set secure permissions (0o600) on config file
509
+ setSecurePermissions(configPath);
507
510
  return;
508
511
  }
509
512
 
@@ -531,6 +534,8 @@ class Installer {
531
534
  };
532
535
 
533
536
  await fs.writeFile(configPath, safeDump(next), 'utf8');
537
+ // Security: Set secure permissions (0o600) on config file
538
+ setSecurePermissions(configPath);
534
539
  } catch (err) {
535
540
  // If it's a typed parse error and not forcing, re-throw
536
541
  if (err.errorCode === 'EPARSE' && !options.force) {
@@ -547,6 +552,8 @@ class Installer {
547
552
  };
548
553
 
549
554
  await fs.writeFile(configPath, safeDump(config), 'utf8');
555
+ // Security: Set secure permissions (0o600) on config file
556
+ setSecurePermissions(configPath);
550
557
  }
551
558
  }
552
559
  }
@@ -578,6 +585,8 @@ class Installer {
578
585
  };
579
586
 
580
587
  await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
588
+ // Security: Set secure permissions (0o600) on manifest file
589
+ setSecurePermissions(manifestPath);
581
590
  return;
582
591
  }
583
592
 
@@ -608,6 +617,8 @@ class Installer {
608
617
  };
609
618
 
610
619
  await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
620
+ // Security: Set secure permissions (0o600) on manifest file
621
+ setSecurePermissions(manifestPath);
611
622
  } catch (err) {
612
623
  // If it's a typed parse error and not forcing, re-throw
613
624
  if (err.errorCode === 'EPARSE' && !options.force) {
@@ -627,6 +638,8 @@ class Installer {
627
638
  };
628
639
 
629
640
  await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
641
+ // Security: Set secure permissions (0o600) on manifest file
642
+ setSecurePermissions(manifestPath);
630
643
  }
631
644
  }
632
645
  }