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
@@ -21,6 +21,15 @@ const {
21
21
  const { IdeManager } = require('../installers/ide/manager');
22
22
  const { getCurrentVersion } = require('../lib/version-checker');
23
23
  const { ErrorHandler } = require('../lib/error-handler');
24
+ const {
25
+ ErrorCodes,
26
+ getErrorCodeFromError,
27
+ getSuggestedFix,
28
+ isRecoverable,
29
+ } = require('../../../lib/error-codes');
30
+ const { safeDump } = require('../../../lib/yaml-utils');
31
+ const { IdeRegistry } = require('../lib/ide-registry');
32
+ const { formatKeyValue, formatList, isTTY } = require('../../../lib/table-formatter');
24
33
 
25
34
  const installer = new Installer();
26
35
 
@@ -88,6 +97,7 @@ module.exports = {
88
97
  issues++;
89
98
  repairs.push({
90
99
  type: 'missing-manifest',
100
+ errorCode: 'ENOENT',
91
101
  message: 'Recreate missing manifest.yaml',
92
102
  fix: async () => {
93
103
  info('Recreating manifest.yaml...');
@@ -95,7 +105,6 @@ module.exports = {
95
105
  const cfgDir = path.join(status.path, '_cfg');
96
106
  await fs.ensureDir(cfgDir);
97
107
 
98
- const yaml = require('js-yaml');
99
108
  const manifest = {
100
109
  version: packageJson.version,
101
110
  installed_at: new Date().toISOString(),
@@ -107,7 +116,7 @@ module.exports = {
107
116
  docs_folder: 'docs',
108
117
  };
109
118
 
110
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
119
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
111
120
  success('Created manifest.yaml');
112
121
  },
113
122
  });
@@ -142,6 +151,7 @@ module.exports = {
142
151
  warnings++;
143
152
  repairs.push({
144
153
  type: 'invalid-file-index',
154
+ errorCode: 'EPARSE',
145
155
  message: 'Recreate files.json safe-update index',
146
156
  fix: async () => {
147
157
  await createProtectedFileIndex(status.path, fileIndexPath);
@@ -154,6 +164,7 @@ module.exports = {
154
164
  warnings++;
155
165
  repairs.push({
156
166
  type: 'missing-file-index',
167
+ errorCode: 'ENOENT',
157
168
  message: 'Create files.json safe-update index',
158
169
  fix: async () => {
159
170
  await createProtectedFileIndex(status.path, fileIndexPath);
@@ -196,6 +207,7 @@ module.exports = {
196
207
  if (missingCore) {
197
208
  repairs.push({
198
209
  type: 'missing-core',
210
+ errorCode: 'EEMPTYDIR',
199
211
  message: 'Reinstall missing core content',
200
212
  fix: async () => {
201
213
  info('Reinstalling core content...');
@@ -229,8 +241,8 @@ module.exports = {
229
241
  ideManager.setDocsFolder(status.docsFolder || 'docs');
230
242
 
231
243
  for (const ide of status.ides) {
232
- const configPath = getIdeConfigPath(directory, ide);
233
- const ideName = formatIdeName(ide);
244
+ const configPath = IdeRegistry.getConfigPath(ide, directory);
245
+ const ideName = IdeRegistry.getDisplayName(ide);
234
246
 
235
247
  if (await fs.pathExists(configPath)) {
236
248
  // Count files in config
@@ -241,6 +253,7 @@ module.exports = {
241
253
  warnings++;
242
254
  repairs.push({
243
255
  type: 'missing-ide-config',
256
+ errorCode: 'ENODIR',
244
257
  message: `Reinstall ${ideName} configuration`,
245
258
  fix: async () => {
246
259
  info(`Reinstalling ${ideName} configuration...`);
@@ -254,19 +267,20 @@ module.exports = {
254
267
 
255
268
  // Check for orphaned configs
256
269
  console.log(chalk.bold('\nOrphan Check:'));
257
- const allIdes = ['claude-code', 'cursor', 'windsurf'];
270
+ const allIdes = IdeRegistry.getAll();
258
271
  let orphansFound = false;
259
272
 
260
273
  for (const ide of allIdes) {
261
274
  if (!status.ides || !status.ides.includes(ide)) {
262
- const configPath = getIdeConfigPath(directory, ide);
275
+ const configPath = IdeRegistry.getConfigPath(ide, directory);
263
276
  if (await fs.pathExists(configPath)) {
264
- const ideName = formatIdeName(ide);
277
+ const ideName = IdeRegistry.getDisplayName(ide);
265
278
  warning(`${ideName}: Config exists but not in manifest`);
266
279
  orphansFound = true;
267
280
  warnings++;
268
281
  repairs.push({
269
282
  type: 'orphaned-config',
283
+ errorCode: 'ECONFLICT',
270
284
  message: `Remove orphaned ${ideName} configuration`,
271
285
  fix: async () => {
272
286
  info(`Removing orphaned ${ideName} configuration...`);
@@ -291,7 +305,13 @@ module.exports = {
291
305
  try {
292
306
  await repair.fix();
293
307
  } catch (err) {
308
+ // Use error codes for better diagnosis
309
+ const codeData = getErrorCodeFromError(err);
294
310
  error(`Failed to ${repair.message.toLowerCase()}: ${err.message}`);
311
+ if (codeData.code !== 'EUNKNOWN') {
312
+ console.log(chalk.dim(` Error code: ${codeData.code}`));
313
+ console.log(chalk.dim(` Suggestion: ${codeData.suggestedFix}`));
314
+ }
295
315
  }
296
316
  }
297
317
 
@@ -300,6 +320,16 @@ module.exports = {
300
320
  } else if (repairs.length > 0 && !options.fix) {
301
321
  console.log();
302
322
  info(`Found ${repairs.length} fixable issue(s). Run with --fix to auto-repair.`);
323
+
324
+ // Show summary of fixable issues with error codes
325
+ console.log(chalk.bold('\nFixable Issues:'));
326
+ for (const repair of repairs) {
327
+ const errorCode = repair.errorCode || 'ECONFIG';
328
+ const codeData = ErrorCodes[errorCode] || ErrorCodes.ECONFIG;
329
+ console.log(
330
+ ` ${chalk.yellow('!')} ${repair.message} ${chalk.dim(`[${codeData.code}]`)}`
331
+ );
332
+ }
303
333
  }
304
334
 
305
335
  // Print summary
@@ -339,36 +369,6 @@ function compareVersions(a, b) {
339
369
  return 0;
340
370
  }
341
371
 
342
- /**
343
- * Get IDE config path
344
- * @param {string} projectDir - Project directory
345
- * @param {string} ide - IDE name
346
- * @returns {string}
347
- */
348
- function getIdeConfigPath(projectDir, ide) {
349
- const paths = {
350
- 'claude-code': '.claude/commands/agileflow',
351
- cursor: '.cursor/rules/agileflow',
352
- windsurf: '.windsurf/workflows/agileflow',
353
- };
354
-
355
- return path.join(projectDir, paths[ide] || '');
356
- }
357
-
358
- /**
359
- * Format IDE name for display
360
- * @param {string} ide - IDE name
361
- * @returns {string}
362
- */
363
- function formatIdeName(ide) {
364
- const names = {
365
- 'claude-code': 'Claude Code',
366
- cursor: 'Cursor',
367
- windsurf: 'Windsurf',
368
- };
369
-
370
- return names[ide] || ide;
371
- }
372
372
 
373
373
  /**
374
374
  * Count files in directory recursively
@@ -392,7 +392,7 @@ async function countFilesInDir(dirPath) {
392
392
  }
393
393
 
394
394
  /**
395
- * Print summary
395
+ * Print summary using formatKeyValue for consistent output
396
396
  * @param {number} issues - Issue count
397
397
  * @param {number} warnings - Warning count
398
398
  */
@@ -402,9 +402,17 @@ function printSummary(issues, warnings) {
402
402
  if (issues === 0 && warnings === 0) {
403
403
  console.log(chalk.green.bold('No issues found.\n'));
404
404
  } else if (issues === 0) {
405
- 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();
406
410
  } else {
407
- 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();
408
416
  }
409
417
  }
410
418
 
@@ -7,12 +7,13 @@
7
7
  const chalk = require('chalk');
8
8
  const path = require('node:path');
9
9
  const fs = require('fs-extra');
10
- const yaml = require('js-yaml');
10
+ const { safeLoad } = require('../../../lib/yaml-utils');
11
11
  const { Installer } = require('../installers/core/installer');
12
12
  const { displayLogo, displaySection, success, warning, info } = require('../lib/ui');
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
 
@@ -233,7 +234,7 @@ async function listExperts(agileflowPath) {
233
234
 
234
235
  try {
235
236
  const content = await fs.readFile(expertiseFile, 'utf8');
236
- const parsed = yaml.load(content);
237
+ const parsed = safeLoad(content);
237
238
 
238
239
  experts.push({
239
240
  name: entry.name,
@@ -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
@@ -78,16 +79,16 @@ module.exports = {
78
79
  // Update the manifest to remove this IDE
79
80
  const manifestPath = path.join(status.path, '_cfg', 'manifest.yaml');
80
81
  if (await fs.pathExists(manifestPath)) {
81
- const yaml = require('js-yaml');
82
+ const { safeLoad, safeDump } = require('../../../lib/yaml-utils');
82
83
  const manifestContent = await fs.readFile(manifestPath, 'utf8');
83
- const manifest = yaml.load(manifestContent);
84
+ const manifest = safeLoad(manifestContent);
84
85
  manifest.ides = (manifest.ides || []).filter(ide => ide !== ideName);
85
86
  manifest.updated_at = new Date().toISOString();
86
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
87
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
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
- }