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.
- package/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- 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(
|
|
48
|
-
configPath
|
|
49
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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(
|
|
112
|
-
filePath
|
|
113
|
-
|
|
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(
|
|
142
|
-
templateFile
|
|
143
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
+
};
|