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
@@ -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,25 @@ 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(
40
+ formatKeyValue({
41
+ Location: status.path,
42
+ Version: status.version,
43
+ })
44
+ );
39
45
 
40
46
  // Count installed items
41
47
  const counts = await installer.countInstalledItems(status.path);
42
48
 
43
- console.log(chalk.bold('\nCore: '), chalk.green('✓ Installed'));
49
+ console.log(
50
+ formatKeyValue(
51
+ {
52
+ '\nCore': chalk.green('✓ Installed'),
53
+ },
54
+ { alignValues: false }
55
+ )
56
+ );
44
57
  info(`${counts.agents} agents`);
45
58
  info(`${counts.commands} commands`);
46
59
  info(`${counts.skills} skills`);
@@ -50,13 +63,13 @@ module.exports = {
50
63
  console.log(chalk.bold('\nConfigured IDEs:'));
51
64
  for (const ide of status.ides) {
52
65
  // Check if IDE config exists
53
- const ideConfigPath = getIdeConfigPath(directory, ide);
66
+ const ideConfigPath = IdeRegistry.getConfigPath(ide, directory);
54
67
  const exists = await fs.pathExists(ideConfigPath);
55
68
 
56
69
  if (exists) {
57
- success(formatIdeName(ide));
70
+ success(IdeRegistry.getDisplayName(ide));
58
71
  } else {
59
- warning(`${formatIdeName(ide)} (config missing)`);
72
+ warning(`${IdeRegistry.getDisplayName(ide)} (config missing)`);
60
73
  }
61
74
  }
62
75
  }
@@ -86,34 +99,3 @@ module.exports = {
86
99
  }
87
100
  },
88
101
  };
89
-
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
- }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * AgileFlow CLI - TUI Command
3
+ *
4
+ * Launches the Terminal User Interface for real-time session monitoring.
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const path = require('node:path');
9
+ const { spawn } = require('node:child_process');
10
+ const fs = require('fs-extra');
11
+
12
+ module.exports = {
13
+ name: 'tui',
14
+ description: 'Launch Terminal User Interface for session monitoring',
15
+ options: [['-d, --directory <path>', 'Project directory (default: current directory)']],
16
+ action: async options => {
17
+ try {
18
+ const directory = path.resolve(options.directory || '.');
19
+
20
+ // Check if AgileFlow is installed
21
+ const agileflowDir = path.join(directory, '.agileflow');
22
+ if (!(await fs.pathExists(agileflowDir))) {
23
+ console.error(chalk.red('Error:'), 'AgileFlow is not installed in this directory');
24
+ console.log(chalk.dim(`Run 'npx agileflow setup' first\n`));
25
+ process.exit(1);
26
+ }
27
+
28
+ // Find the TUI script
29
+ const tuiScript = path.join(__dirname, '../../..', 'scripts', 'tui', 'index.js');
30
+
31
+ if (!(await fs.pathExists(tuiScript))) {
32
+ console.error(chalk.red('Error:'), 'TUI script not found');
33
+ process.exit(1);
34
+ }
35
+
36
+ // Spawn the TUI process with inherited stdio for interactive mode
37
+ const child = spawn('node', [tuiScript], {
38
+ cwd: directory,
39
+ stdio: 'inherit',
40
+ env: { ...process.env, FORCE_COLOR: '1' },
41
+ });
42
+
43
+ child.on('error', err => {
44
+ console.error(chalk.red('Error launching TUI:'), err.message);
45
+ process.exit(1);
46
+ });
47
+
48
+ child.on('exit', code => {
49
+ process.exit(code || 0);
50
+ });
51
+ } catch (err) {
52
+ console.error(chalk.red('Error:'), err.message);
53
+ if (process.env.DEBUG) {
54
+ console.error(err.stack);
55
+ }
56
+ process.exit(1);
57
+ }
58
+ },
59
+ };
@@ -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,20 @@ 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(
55
+ `Remove ${IdeRegistry.getDisplayName(ideName)} configuration?`,
56
+ false
57
+ );
54
58
  if (!proceed) {
55
59
  console.log(chalk.dim('\nCancelled\n'));
56
60
  process.exit(0);
@@ -60,10 +64,10 @@ module.exports = {
60
64
  console.log();
61
65
 
62
66
  // Remove the IDE configuration
63
- const configPath = getIdeConfigPath(directory, ideName);
67
+ const configPath = IdeRegistry.getConfigPath(ideName, directory);
64
68
  if (await fs.pathExists(configPath)) {
65
69
  await fs.remove(configPath);
66
- success(`Removed ${formatIdeName(ideName)} configuration`);
70
+ success(`Removed ${IdeRegistry.getDisplayName(ideName)} configuration`);
67
71
  }
68
72
 
69
73
  // Also remove spawnable agents for claude-code
@@ -87,7 +91,7 @@ module.exports = {
87
91
  success('Updated manifest');
88
92
  }
89
93
 
90
- console.log(chalk.green(`\n${formatIdeName(ideName)} has been removed.\n`));
94
+ console.log(chalk.green(`\n${IdeRegistry.getDisplayName(ideName)} has been removed.\n`));
91
95
  if (status.ides.length > 1) {
92
96
  console.log(
93
97
  chalk.dim(`Remaining IDEs: ${status.ides.filter(i => i !== ideName).join(', ')}\n`)
@@ -114,10 +118,10 @@ module.exports = {
114
118
  // Remove IDE configurations
115
119
  if (status.ides && status.ides.length > 0) {
116
120
  for (const ide of status.ides) {
117
- const configPath = getIdeConfigPath(directory, ide);
121
+ const configPath = IdeRegistry.getConfigPath(ide, directory);
118
122
  if (await fs.pathExists(configPath)) {
119
123
  await fs.remove(configPath);
120
- success(`Removed ${formatIdeName(ide)} configuration`);
124
+ success(`Removed ${IdeRegistry.getDisplayName(ide)} configuration`);
121
125
  }
122
126
  // Also remove spawnable agents for claude-code
123
127
  if (ide === 'claude-code') {
@@ -149,34 +153,3 @@ module.exports = {
149
153
  }
150
154
  },
151
155
  };
152
-
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
  }