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
@@ -4,8 +4,15 @@
4
4
  * Provides specific error types for common IDE setup failures.
5
5
  * These errors carry context about what failed and why,
6
6
  * enabling better error handling and user feedback.
7
+ *
8
+ * Integration with error-codes.js:
9
+ * - All IDE errors now have errorCode, severity, category metadata
10
+ * - Use formatError() from error-codes.js for consistent display
11
+ * - isRecoverable() works with these errors
7
12
  */
8
13
 
14
+ const { ErrorCodes, Severity, Category } = require('../../../lib/error-codes');
15
+
9
16
  /**
10
17
  * Base error class for IDE-related errors.
11
18
  * All IDE errors extend this class.
@@ -15,13 +22,22 @@ class IdeError extends Error {
15
22
  * @param {string} message - Error description
16
23
  * @param {string} ideName - Name of the IDE (e.g., 'Claude Code', 'Cursor')
17
24
  * @param {Object} [context={}] - Additional context about the error
25
+ * @param {string} [errorCode='EUNKNOWN'] - Error code from error-codes.js
18
26
  */
19
- constructor(message, ideName, context = {}) {
27
+ constructor(message, ideName, context = {}, errorCode = 'EUNKNOWN') {
20
28
  super(message);
21
29
  this.name = this.constructor.name;
22
30
  this.ideName = ideName;
23
31
  this.context = context;
24
32
  Error.captureStackTrace(this, this.constructor);
33
+
34
+ // Attach error code metadata from unified error codes
35
+ const codeData = ErrorCodes[errorCode] || ErrorCodes.EUNKNOWN;
36
+ this.errorCode = codeData.code;
37
+ this.severity = codeData.severity;
38
+ this.category = codeData.category;
39
+ this.recoverable = codeData.recoverable;
40
+ this.autoFix = codeData.autoFix || null;
25
41
  }
26
42
 
27
43
  /**
@@ -31,6 +47,16 @@ class IdeError extends Error {
31
47
  getUserMessage() {
32
48
  return `${this.ideName}: ${this.message}`;
33
49
  }
50
+
51
+ /**
52
+ * Get suggested action to fix the error
53
+ * Override in subclasses for specific suggestions
54
+ * @returns {string}
55
+ */
56
+ getSuggestedAction() {
57
+ const codeData = ErrorCodes[this.errorCode] || ErrorCodes.EUNKNOWN;
58
+ return codeData.suggestedFix;
59
+ }
34
60
  }
35
61
 
36
62
  /**
@@ -44,10 +70,12 @@ class IdeConfigNotFoundError extends IdeError {
44
70
  * @param {Object} [context={}] - Additional context
45
71
  */
46
72
  constructor(ideName, configPath, context = {}) {
47
- super(`Configuration directory not found: ${configPath}`, ideName, {
48
- configPath,
49
- ...context,
50
- });
73
+ super(
74
+ `Configuration directory not found: ${configPath}`,
75
+ ideName,
76
+ { configPath, ...context },
77
+ 'ENODIR' // Use unified error code
78
+ );
51
79
  this.configPath = configPath;
52
80
  }
53
81
 
@@ -72,11 +100,23 @@ class CommandInstallationError extends IdeError {
72
100
  * @param {Object} [context={}] - Additional context
73
101
  */
74
102
  constructor(ideName, commandName, reason, context = {}) {
75
- super(`Failed to install command '${commandName}': ${reason}`, ideName, {
76
- commandName,
77
- reason,
78
- ...context,
79
- });
103
+ // Detect appropriate error code from reason
104
+ let errorCode = 'ESTATE';
105
+ if (reason.toLowerCase().includes('permission')) {
106
+ errorCode = 'EACCES';
107
+ } else if (
108
+ reason.toLowerCase().includes('not found') ||
109
+ reason.toLowerCase().includes('no such')
110
+ ) {
111
+ errorCode = 'ENOENT';
112
+ }
113
+
114
+ super(
115
+ `Failed to install command '${commandName}': ${reason}`,
116
+ ideName,
117
+ { commandName, reason, ...context },
118
+ errorCode
119
+ );
80
120
  this.commandName = commandName;
81
121
  this.reason = reason;
82
122
  }
@@ -108,11 +148,12 @@ class FilePermissionError extends IdeError {
108
148
  * @param {Object} [context={}] - Additional context
109
149
  */
110
150
  constructor(ideName, filePath, operation, context = {}) {
111
- super(`Permission denied: cannot ${operation} '${filePath}'`, ideName, {
112
- filePath,
113
- operation,
114
- ...context,
115
- });
151
+ super(
152
+ `Permission denied: cannot ${operation} '${filePath}'`,
153
+ ideName,
154
+ { filePath, operation, ...context },
155
+ 'EACCES' // Use unified error code
156
+ );
116
157
  this.filePath = filePath;
117
158
  this.operation = operation;
118
159
  }
@@ -138,14 +179,23 @@ class ContentInjectionError extends IdeError {
138
179
  * @param {Object} [context={}] - Additional context
139
180
  */
140
181
  constructor(ideName, templateFile, reason, context = {}) {
141
- super(`Content injection failed for '${templateFile}': ${reason}`, ideName, {
142
- templateFile,
143
- reason,
144
- ...context,
145
- });
182
+ super(
183
+ `Content injection failed for '${templateFile}': ${reason}`,
184
+ ideName,
185
+ { templateFile, reason, ...context },
186
+ 'ECONFIG' // Use unified error code - configuration/template issue
187
+ );
146
188
  this.templateFile = templateFile;
147
189
  this.reason = reason;
148
190
  }
191
+
192
+ /**
193
+ * Get suggested action to fix the error
194
+ * @returns {string}
195
+ */
196
+ getSuggestedAction() {
197
+ return `Check the template file '${this.templateFile}' for valid placeholders. Run "npx agileflow doctor --fix" to repair.`;
198
+ }
149
199
  }
150
200
 
151
201
  /**
@@ -160,14 +210,34 @@ class CleanupError extends IdeError {
160
210
  * @param {Object} [context={}] - Additional context
161
211
  */
162
212
  constructor(ideName, targetPath, reason, context = {}) {
163
- super(`Cleanup failed for '${targetPath}': ${reason}`, ideName, {
164
- targetPath,
165
- reason,
166
- ...context,
167
- });
213
+ // Detect appropriate error code from reason
214
+ let errorCode = 'ESTATE';
215
+ if (reason.toLowerCase().includes('lock') || reason.toLowerCase().includes('busy')) {
216
+ errorCode = 'ELOCK';
217
+ } else if (reason.toLowerCase().includes('permission')) {
218
+ errorCode = 'EACCES';
219
+ }
220
+
221
+ super(
222
+ `Cleanup failed for '${targetPath}': ${reason}`,
223
+ ideName,
224
+ { targetPath, reason, ...context },
225
+ errorCode
226
+ );
168
227
  this.targetPath = targetPath;
169
228
  this.reason = reason;
170
229
  }
230
+
231
+ /**
232
+ * Get suggested action to fix the error
233
+ * @returns {string}
234
+ */
235
+ getSuggestedAction() {
236
+ if (this.errorCode === 'ELOCK') {
237
+ return `Close any applications using files in '${this.targetPath}' and try again.`;
238
+ }
239
+ return `Check permissions on '${this.targetPath}' or remove it manually.`;
240
+ }
171
241
  }
172
242
 
173
243
  /**
@@ -181,12 +251,29 @@ class IdeDetectionError extends IdeError {
181
251
  * @param {Object} [context={}] - Additional context
182
252
  */
183
253
  constructor(ideName, reason, context = {}) {
184
- super(`IDE detection failed: ${reason}`, ideName, {
185
- reason,
186
- ...context,
187
- });
254
+ // Detect appropriate error code
255
+ let errorCode = 'ECONFLICT';
256
+ if (
257
+ reason.toLowerCase().includes('not found') ||
258
+ reason.toLowerCase().includes('not installed')
259
+ ) {
260
+ errorCode = 'ENOENT';
261
+ }
262
+
263
+ super(`IDE detection failed: ${reason}`, ideName, { reason, ...context }, errorCode);
188
264
  this.reason = reason;
189
265
  }
266
+
267
+ /**
268
+ * Get suggested action to fix the error
269
+ * @returns {string}
270
+ */
271
+ getSuggestedAction() {
272
+ if (this.errorCode === 'ENOENT') {
273
+ return `Install ${this.ideName} and run it at least once to initialize configuration.`;
274
+ }
275
+ return `Check IDE configuration and resolve any conflicts. Run "npx agileflow doctor" for details.`;
276
+ }
190
277
  }
191
278
 
192
279
  /**
@@ -220,6 +307,52 @@ function isIdeError(error) {
220
307
  return error instanceof IdeError;
221
308
  }
222
309
 
310
+ /**
311
+ * Format an IDE error for display using unified error code format
312
+ * @param {IdeError} error - IDE error to format
313
+ * @param {Object} [options={}] - Format options
314
+ * @param {boolean} [options.includeStack=false] - Include stack trace
315
+ * @param {boolean} [options.includeSuggestion=true] - Include suggested action
316
+ * @returns {string} Formatted error string
317
+ */
318
+ function formatIdeError(error, options = {}) {
319
+ const { includeStack = false, includeSuggestion = true } = options;
320
+
321
+ if (!error) return 'Unknown error';
322
+
323
+ const lines = [];
324
+
325
+ // Main error line with IDE name and error code
326
+ lines.push(`[${error.errorCode}] ${error.ideName}: ${error.message}`);
327
+
328
+ // Severity and category
329
+ lines.push(` Severity: ${error.severity} | Category: ${error.category}`);
330
+
331
+ // Suggested action (IDE-specific takes precedence)
332
+ if (includeSuggestion) {
333
+ const suggestion = error.getSuggestedAction?.() || error.suggestedFix;
334
+ if (suggestion) {
335
+ lines.push(` Fix: ${suggestion}`);
336
+ }
337
+ }
338
+
339
+ // Recoverable status
340
+ lines.push(` Recoverable: ${error.recoverable ? 'Yes' : 'No'}`);
341
+
342
+ // Auto-fix availability
343
+ if (error.autoFix) {
344
+ lines.push(` Auto-fix available: npx agileflow doctor --fix`);
345
+ }
346
+
347
+ // Stack trace
348
+ if (includeStack && error.stack) {
349
+ lines.push('');
350
+ lines.push(error.stack);
351
+ }
352
+
353
+ return lines.join('\n');
354
+ }
355
+
223
356
  module.exports = {
224
357
  IdeError,
225
358
  IdeConfigNotFoundError,
@@ -230,4 +363,5 @@ module.exports = {
230
363
  IdeDetectionError,
231
364
  withPermissionHandling,
232
365
  isIdeError,
366
+ formatIdeError,
233
367
  };
@@ -0,0 +1,186 @@
1
+ /**
2
+ * AgileFlow CLI - IDE Registry
3
+ *
4
+ * Centralized registry of supported IDEs with their metadata.
5
+ * This eliminates duplicate IDE configuration scattered across commands.
6
+ *
7
+ * Usage:
8
+ * const { IdeRegistry } = require('./lib/ide-registry');
9
+ * const configPath = IdeRegistry.getConfigPath('claude-code', projectDir);
10
+ * const displayName = IdeRegistry.getDisplayName('cursor');
11
+ */
12
+
13
+ const path = require('path');
14
+
15
+ /**
16
+ * IDE metadata definition
17
+ * @typedef {Object} IdeMetadata
18
+ * @property {string} name - Internal IDE name (e.g., 'claude-code')
19
+ * @property {string} displayName - Human-readable name (e.g., 'Claude Code')
20
+ * @property {string} configDir - Base config directory (e.g., '.claude')
21
+ * @property {string} targetSubdir - Target subdirectory for commands (e.g., 'commands/agileflow')
22
+ * @property {boolean} preferred - Whether this is a preferred IDE
23
+ * @property {string} [handler] - Handler class name (e.g., 'ClaudeCodeSetup')
24
+ */
25
+
26
+ /**
27
+ * Registry of all supported IDEs
28
+ * @type {Object.<string, IdeMetadata>}
29
+ */
30
+ const IDE_REGISTRY = {
31
+ 'claude-code': {
32
+ name: 'claude-code',
33
+ displayName: 'Claude Code',
34
+ configDir: '.claude',
35
+ targetSubdir: 'commands/agileflow', // lowercase
36
+ preferred: true,
37
+ handler: 'ClaudeCodeSetup',
38
+ },
39
+ cursor: {
40
+ name: 'cursor',
41
+ displayName: 'Cursor',
42
+ configDir: '.cursor',
43
+ targetSubdir: 'commands/AgileFlow', // PascalCase
44
+ preferred: false,
45
+ handler: 'CursorSetup',
46
+ },
47
+ windsurf: {
48
+ name: 'windsurf',
49
+ displayName: 'Windsurf',
50
+ configDir: '.windsurf',
51
+ targetSubdir: 'workflows/agileflow', // lowercase
52
+ preferred: true,
53
+ handler: 'WindsurfSetup',
54
+ },
55
+ codex: {
56
+ name: 'codex',
57
+ displayName: 'OpenAI Codex CLI',
58
+ configDir: '.codex',
59
+ targetSubdir: 'skills', // Codex uses skills directory
60
+ preferred: false,
61
+ handler: 'CodexSetup',
62
+ },
63
+ };
64
+
65
+ /**
66
+ * IDE Registry class providing centralized IDE metadata access
67
+ */
68
+ class IdeRegistry {
69
+ /**
70
+ * Get all registered IDE names
71
+ * @returns {string[]} List of IDE names
72
+ */
73
+ static getAll() {
74
+ return Object.keys(IDE_REGISTRY);
75
+ }
76
+
77
+ /**
78
+ * Get all IDE metadata
79
+ * @returns {Object.<string, IdeMetadata>} All IDE metadata
80
+ */
81
+ static getAllMetadata() {
82
+ return { ...IDE_REGISTRY };
83
+ }
84
+
85
+ /**
86
+ * Get metadata for a specific IDE
87
+ * @param {string} ideName - IDE name
88
+ * @returns {IdeMetadata|null} IDE metadata or null if not found
89
+ */
90
+ static get(ideName) {
91
+ return IDE_REGISTRY[ideName] || null;
92
+ }
93
+
94
+ /**
95
+ * Check if an IDE is registered
96
+ * @param {string} ideName - IDE name
97
+ * @returns {boolean}
98
+ */
99
+ static exists(ideName) {
100
+ return ideName in IDE_REGISTRY;
101
+ }
102
+
103
+ /**
104
+ * Get the config path for an IDE in a project
105
+ * @param {string} ideName - IDE name
106
+ * @param {string} projectDir - Project directory
107
+ * @returns {string} Full path to IDE config directory
108
+ */
109
+ static getConfigPath(ideName, projectDir) {
110
+ const ide = IDE_REGISTRY[ideName];
111
+ if (!ide) {
112
+ return '';
113
+ }
114
+ return path.join(projectDir, ide.configDir, ide.targetSubdir);
115
+ }
116
+
117
+ /**
118
+ * Get the base config directory for an IDE (e.g., .claude, .cursor)
119
+ * @param {string} ideName - IDE name
120
+ * @param {string} projectDir - Project directory
121
+ * @returns {string} Full path to base config directory
122
+ */
123
+ static getBaseDir(ideName, projectDir) {
124
+ const ide = IDE_REGISTRY[ideName];
125
+ if (!ide) {
126
+ return '';
127
+ }
128
+ return path.join(projectDir, ide.configDir);
129
+ }
130
+
131
+ /**
132
+ * Get the display name for an IDE
133
+ * @param {string} ideName - IDE name
134
+ * @returns {string} Display name or the original name if not found
135
+ */
136
+ static getDisplayName(ideName) {
137
+ const ide = IDE_REGISTRY[ideName];
138
+ return ide ? ide.displayName : ideName;
139
+ }
140
+
141
+ /**
142
+ * Get all preferred IDEs
143
+ * @returns {string[]} List of preferred IDE names
144
+ */
145
+ static getPreferred() {
146
+ return Object.entries(IDE_REGISTRY)
147
+ .filter(([, meta]) => meta.preferred)
148
+ .map(([name]) => name);
149
+ }
150
+
151
+ /**
152
+ * Validate IDE name
153
+ * @param {string} ideName - IDE name to validate
154
+ * @returns {{ok: boolean, error?: string}} Validation result
155
+ */
156
+ static validate(ideName) {
157
+ if (!ideName || typeof ideName !== 'string') {
158
+ return { ok: false, error: 'IDE name must be a non-empty string' };
159
+ }
160
+
161
+ if (!IDE_REGISTRY[ideName]) {
162
+ const validNames = Object.keys(IDE_REGISTRY).join(', ');
163
+ return {
164
+ ok: false,
165
+ error: `Unknown IDE: '${ideName}'. Valid options: ${validNames}`,
166
+ };
167
+ }
168
+
169
+ return { ok: true };
170
+ }
171
+
172
+ /**
173
+ * Get handler class name for an IDE
174
+ * @param {string} ideName - IDE name
175
+ * @returns {string|null} Handler class name or null
176
+ */
177
+ static getHandler(ideName) {
178
+ const ide = IDE_REGISTRY[ideName];
179
+ return ide ? ide.handler : null;
180
+ }
181
+ }
182
+
183
+ module.exports = {
184
+ IdeRegistry,
185
+ IDE_REGISTRY,
186
+ };
@@ -40,6 +40,9 @@ async function getLatestVersion(packageName) {
40
40
  headers: {
41
41
  'User-Agent': 'agileflow-cli',
42
42
  },
43
+ // Security: Explicitly enable TLS certificate validation
44
+ // Prevents MITM attacks on npm registry requests
45
+ rejectUnauthorized: true,
43
46
  };
44
47
 
45
48
  debugLog('Fetching version', { package: packageName, path: options.path });
@@ -70,12 +73,22 @@ async function getLatestVersion(packageName) {
70
73
  });
71
74
 
72
75
  req.on('error', err => {
73
- debugLog('Network error', { error: err.message });
76
+ // Enhanced error logging with retry guidance
77
+ const errorInfo = {
78
+ error: err.message,
79
+ code: err.code,
80
+ suggestion: 'Check network connection. If error persists, try: npm cache clean --force',
81
+ };
82
+ if (err.code === 'CERT_HAS_EXPIRED' || err.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
83
+ errorInfo.suggestion = 'TLS certificate error - check system time or update CA certificates';
84
+ }
85
+ debugLog('Network error', errorInfo);
74
86
  resolve(null);
75
87
  });
76
88
 
77
- req.setTimeout(5000, () => {
78
- debugLog('Request timeout');
89
+ // 10 second timeout for registry requests
90
+ req.setTimeout(10000, () => {
91
+ debugLog('Request timeout (10s)', { suggestion: 'npm registry may be slow. Retry later.' });
79
92
  req.destroy();
80
93
  resolve(null);
81
94
  });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * AgileFlow CLI - Self-Update Module
3
+ *
4
+ * Provides self-update capability for the CLI to ensure users
5
+ * always run the latest version.
6
+ *
7
+ * Usage:
8
+ * const { checkSelfUpdate, performSelfUpdate } = require('./lib/self-update');
9
+ * await checkSelfUpdate(options, 'update');
10
+ */
11
+
12
+ const path = require('path');
13
+ const { spawnSync } = require('node:child_process');
14
+ const semver = require('semver');
15
+ const chalk = require('chalk');
16
+ const { getLatestVersion } = require('./npm-utils');
17
+ const { info } = require('./ui');
18
+
19
+ /**
20
+ * Get the local CLI version from package.json
21
+ * @returns {string} Local CLI version
22
+ */
23
+ function getLocalVersion() {
24
+ const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
25
+ return packageJson.version;
26
+ }
27
+
28
+ /**
29
+ * Check if a self-update is needed
30
+ * @param {Object} options - Command options
31
+ * @param {boolean} [options.selfUpdate=true] - Whether self-update is enabled
32
+ * @param {boolean} [options.selfUpdated=false] - Whether already self-updated
33
+ * @returns {Promise<{needed: boolean, localVersion: string, latestVersion: string|null}>}
34
+ */
35
+ async function checkSelfUpdate(options = {}) {
36
+ const shouldCheck = options.selfUpdate !== false && !options.selfUpdated;
37
+
38
+ if (!shouldCheck) {
39
+ return {
40
+ needed: false,
41
+ localVersion: getLocalVersion(),
42
+ latestVersion: null,
43
+ };
44
+ }
45
+
46
+ const localVersion = getLocalVersion();
47
+ const latestVersion = await getLatestVersion('agileflow');
48
+
49
+ if (!latestVersion) {
50
+ return {
51
+ needed: false,
52
+ localVersion,
53
+ latestVersion: null,
54
+ };
55
+ }
56
+
57
+ const needed = semver.lt(localVersion, latestVersion);
58
+
59
+ return {
60
+ needed,
61
+ localVersion,
62
+ latestVersion,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Perform self-update by re-running with latest CLI version
68
+ * @param {string} command - Command name to re-run
69
+ * @param {Object} options - Command options
70
+ * @param {Object} versionInfo - Version info from checkSelfUpdate
71
+ * @returns {number} Exit code from spawned process
72
+ */
73
+ function performSelfUpdate(command, options, versionInfo) {
74
+ const { localVersion, latestVersion } = versionInfo;
75
+
76
+ // Display update notice
77
+ console.log(chalk.hex('#e8683a').bold('\n AgileFlow CLI Update\n'));
78
+ info(`Updating CLI from v${localVersion} to v${latestVersion}...`);
79
+ console.log(chalk.dim(' Fetching latest version from npm...\n'));
80
+
81
+ // Build the command with all current options forwarded
82
+ const args = ['agileflow@latest', command, '--self-updated'];
83
+
84
+ // Forward common options
85
+ if (options.directory) args.push('-d', options.directory);
86
+ if (options.force) args.push('--force');
87
+
88
+ // Forward command-specific options
89
+ if (command === 'update' && options.ides) {
90
+ args.push('--ides', options.ides);
91
+ }
92
+
93
+ const result = spawnSync('npx', args, {
94
+ stdio: 'inherit',
95
+ cwd: process.cwd(),
96
+ shell: process.platform === 'win32',
97
+ });
98
+
99
+ return result.status ?? 0;
100
+ }
101
+
102
+ /**
103
+ * Check and perform self-update if needed
104
+ * Returns true if the caller should exit (because update was performed)
105
+ * @param {string} command - Command name
106
+ * @param {Object} options - Command options
107
+ * @returns {Promise<boolean>} True if caller should exit
108
+ */
109
+ async function handleSelfUpdate(command, options) {
110
+ const versionInfo = await checkSelfUpdate(options);
111
+
112
+ if (versionInfo.needed) {
113
+ const exitCode = performSelfUpdate(command, options, versionInfo);
114
+ process.exit(exitCode);
115
+ return true; // Never reached, but for type safety
116
+ }
117
+
118
+ return false;
119
+ }
120
+
121
+ /**
122
+ * Middleware for self-update check
123
+ * @param {string} command - Command name
124
+ * @returns {Function} Middleware function
125
+ */
126
+ function selfUpdateMiddleware(command) {
127
+ return async (ctx, next) => {
128
+ const versionInfo = await checkSelfUpdate(ctx.options);
129
+
130
+ if (versionInfo.needed) {
131
+ const exitCode = performSelfUpdate(command, ctx.options, versionInfo);
132
+ process.exit(exitCode);
133
+ return; // Never reached
134
+ }
135
+
136
+ // Store version info in context for later use
137
+ ctx.versionInfo = versionInfo;
138
+ await next();
139
+ };
140
+ }
141
+
142
+ module.exports = {
143
+ getLocalVersion,
144
+ checkSelfUpdate,
145
+ performSelfUpdate,
146
+ handleSelfUpdate,
147
+ selfUpdateMiddleware,
148
+ };