agileflow 2.89.3 → 2.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/lib/placeholder-registry.js +617 -0
- package/lib/smart-json-file.js +205 -1
- 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 +37 -737
- package/package.json +4 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- 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 +7 -30
- package/tools/cli/commands/doctor.js +18 -38
- package/tools/cli/commands/list.js +47 -35
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +9 -38
- package/tools/cli/installers/core/installer.js +13 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- 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
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Centralized configuration management with schema validation,
|
|
5
|
+
* migration support, and consistent access patterns.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { ConfigManager } = require('./lib/config-manager');
|
|
9
|
+
* const config = await ConfigManager.load(projectDir);
|
|
10
|
+
* const userName = config.get('userName');
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs-extra');
|
|
15
|
+
const { safeLoad, safeDump } = require('../../../lib/yaml-utils');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration schema definition
|
|
19
|
+
* @typedef {Object} ConfigSchema
|
|
20
|
+
* @property {string} type - Value type (string, array, boolean, number)
|
|
21
|
+
* @property {*} default - Default value
|
|
22
|
+
* @property {boolean} required - Whether the field is required
|
|
23
|
+
* @property {Function} [validate] - Custom validation function
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration schema for AgileFlow manifest
|
|
28
|
+
*/
|
|
29
|
+
const CONFIG_SCHEMA = {
|
|
30
|
+
version: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
default: '0.0.0',
|
|
33
|
+
required: true,
|
|
34
|
+
validate: v => /^\d+\.\d+\.\d+/.test(v),
|
|
35
|
+
},
|
|
36
|
+
userName: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: 'Developer',
|
|
39
|
+
required: false,
|
|
40
|
+
},
|
|
41
|
+
ides: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
default: ['claude-code'],
|
|
44
|
+
required: true,
|
|
45
|
+
validate: arr =>
|
|
46
|
+
Array.isArray(arr) &&
|
|
47
|
+
arr.every(ide => ['claude-code', 'cursor', 'windsurf', 'codex'].includes(ide)),
|
|
48
|
+
},
|
|
49
|
+
agileflowFolder: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
default: '.agileflow',
|
|
52
|
+
required: true,
|
|
53
|
+
validate: v => typeof v === 'string' && v.length > 0 && !v.includes('..'),
|
|
54
|
+
},
|
|
55
|
+
docsFolder: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
default: 'docs',
|
|
58
|
+
required: true,
|
|
59
|
+
validate: v => typeof v === 'string' && v.length > 0 && !v.includes('..'),
|
|
60
|
+
},
|
|
61
|
+
installedAt: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
default: null,
|
|
64
|
+
required: false,
|
|
65
|
+
},
|
|
66
|
+
updatedAt: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
default: null,
|
|
69
|
+
required: false,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Valid configuration keys (for external reference)
|
|
75
|
+
*/
|
|
76
|
+
const VALID_CONFIG_KEYS = Object.keys(CONFIG_SCHEMA);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* User-editable configuration keys
|
|
80
|
+
*/
|
|
81
|
+
const EDITABLE_CONFIG_KEYS = ['userName', 'ides', 'agileflowFolder', 'docsFolder'];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Configuration Manager class
|
|
85
|
+
*/
|
|
86
|
+
class ConfigManager {
|
|
87
|
+
/**
|
|
88
|
+
* Create a new ConfigManager instance
|
|
89
|
+
* @param {Object} data - Configuration data
|
|
90
|
+
* @param {string} manifestPath - Path to manifest file
|
|
91
|
+
*/
|
|
92
|
+
constructor(data = {}, manifestPath = null) {
|
|
93
|
+
this._data = { ...data };
|
|
94
|
+
this._manifestPath = manifestPath;
|
|
95
|
+
this._dirty = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load configuration from a project directory
|
|
100
|
+
* @param {string} projectDir - Project directory
|
|
101
|
+
* @param {Object} options - Load options
|
|
102
|
+
* @param {string} [options.agileflowFolder='.agileflow'] - AgileFlow folder name
|
|
103
|
+
* @returns {Promise<ConfigManager>} ConfigManager instance
|
|
104
|
+
*/
|
|
105
|
+
static async load(projectDir, options = {}) {
|
|
106
|
+
const agileflowFolder = options.agileflowFolder || '.agileflow';
|
|
107
|
+
const manifestPath = path.join(projectDir, agileflowFolder, '_cfg', 'manifest.yaml');
|
|
108
|
+
|
|
109
|
+
let data = {};
|
|
110
|
+
|
|
111
|
+
if (await fs.pathExists(manifestPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const content = await fs.readFile(manifestPath, 'utf8');
|
|
114
|
+
const parsed = safeLoad(content);
|
|
115
|
+
// Normalize keys from snake_case to camelCase
|
|
116
|
+
data = ConfigManager._normalizeKeys(parsed);
|
|
117
|
+
} catch {
|
|
118
|
+
// If manifest is corrupted, use defaults
|
|
119
|
+
data = {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Apply defaults for missing fields
|
|
124
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA)) {
|
|
125
|
+
if (data[key] === undefined && schema.default !== null) {
|
|
126
|
+
data[key] = schema.default;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return new ConfigManager(data, manifestPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Normalize keys from snake_case to camelCase
|
|
135
|
+
* @param {Object} obj - Object with snake_case keys
|
|
136
|
+
* @returns {Object} Object with camelCase keys
|
|
137
|
+
*/
|
|
138
|
+
static _normalizeKeys(obj) {
|
|
139
|
+
const keyMap = {
|
|
140
|
+
user_name: 'userName',
|
|
141
|
+
agileflow_folder: 'agileflowFolder',
|
|
142
|
+
docs_folder: 'docsFolder',
|
|
143
|
+
installed_at: 'installedAt',
|
|
144
|
+
updated_at: 'updatedAt',
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = {};
|
|
148
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
149
|
+
const normalizedKey = keyMap[key] || key;
|
|
150
|
+
result[normalizedKey] = value;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Denormalize keys from camelCase to snake_case for storage
|
|
157
|
+
* @param {Object} obj - Object with camelCase keys
|
|
158
|
+
* @returns {Object} Object with snake_case keys
|
|
159
|
+
*/
|
|
160
|
+
static _denormalizeKeys(obj) {
|
|
161
|
+
const keyMap = {
|
|
162
|
+
userName: 'user_name',
|
|
163
|
+
agileflowFolder: 'agileflow_folder',
|
|
164
|
+
docsFolder: 'docs_folder',
|
|
165
|
+
installedAt: 'installed_at',
|
|
166
|
+
updatedAt: 'updated_at',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = {};
|
|
170
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
171
|
+
const denormalizedKey = keyMap[key] || key;
|
|
172
|
+
result[denormalizedKey] = value;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get a configuration value
|
|
179
|
+
* @param {string} key - Configuration key
|
|
180
|
+
* @returns {*} Configuration value
|
|
181
|
+
*/
|
|
182
|
+
get(key) {
|
|
183
|
+
const schema = CONFIG_SCHEMA[key];
|
|
184
|
+
if (!schema) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return this._data[key] !== undefined ? this._data[key] : schema.default;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set a configuration value
|
|
192
|
+
* @param {string} key - Configuration key
|
|
193
|
+
* @param {*} value - Configuration value
|
|
194
|
+
* @returns {{ok: boolean, error?: string}} Result
|
|
195
|
+
*/
|
|
196
|
+
set(key, value) {
|
|
197
|
+
const schema = CONFIG_SCHEMA[key];
|
|
198
|
+
|
|
199
|
+
if (!schema) {
|
|
200
|
+
return { ok: false, error: `Unknown configuration key: ${key}` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!EDITABLE_CONFIG_KEYS.includes(key)) {
|
|
204
|
+
return { ok: false, error: `Configuration key '${key}' is read-only` };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Type validation
|
|
208
|
+
const typeError = this._validateType(key, value, schema);
|
|
209
|
+
if (typeError) {
|
|
210
|
+
return { ok: false, error: typeError };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Custom validation
|
|
214
|
+
if (schema.validate && !schema.validate(value)) {
|
|
215
|
+
return { ok: false, error: `Invalid value for '${key}'` };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this._data[key] = value;
|
|
219
|
+
this._dirty = true;
|
|
220
|
+
return { ok: true };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validate type of a value
|
|
225
|
+
* @param {string} key - Configuration key
|
|
226
|
+
* @param {*} value - Value to validate
|
|
227
|
+
* @param {ConfigSchema} schema - Schema definition
|
|
228
|
+
* @returns {string|null} Error message or null if valid
|
|
229
|
+
*/
|
|
230
|
+
_validateType(key, value, schema) {
|
|
231
|
+
switch (schema.type) {
|
|
232
|
+
case 'string':
|
|
233
|
+
if (typeof value !== 'string') {
|
|
234
|
+
return `'${key}' must be a string`;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case 'array':
|
|
238
|
+
if (!Array.isArray(value)) {
|
|
239
|
+
return `'${key}' must be an array`;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
case 'boolean':
|
|
243
|
+
if (typeof value !== 'boolean') {
|
|
244
|
+
return `'${key}' must be a boolean`;
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case 'number':
|
|
248
|
+
if (typeof value !== 'number') {
|
|
249
|
+
return `'${key}' must be a number`;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validate all configuration values
|
|
258
|
+
* @returns {{ok: boolean, errors: string[]}} Validation result
|
|
259
|
+
*/
|
|
260
|
+
validate() {
|
|
261
|
+
const errors = [];
|
|
262
|
+
|
|
263
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA)) {
|
|
264
|
+
const value = this._data[key];
|
|
265
|
+
|
|
266
|
+
// Check required fields
|
|
267
|
+
if (schema.required && (value === undefined || value === null)) {
|
|
268
|
+
errors.push(`Missing required field: ${key}`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Skip validation for undefined optional fields
|
|
273
|
+
if (value === undefined || value === null) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Type validation
|
|
278
|
+
const typeError = this._validateType(key, value, schema);
|
|
279
|
+
if (typeError) {
|
|
280
|
+
errors.push(typeError);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Custom validation
|
|
285
|
+
if (schema.validate && !schema.validate(value)) {
|
|
286
|
+
errors.push(`Invalid value for '${key}'`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { ok: errors.length === 0, errors };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Save configuration to manifest file
|
|
295
|
+
* @returns {Promise<{ok: boolean, error?: string}>} Save result
|
|
296
|
+
*/
|
|
297
|
+
async save() {
|
|
298
|
+
if (!this._manifestPath) {
|
|
299
|
+
return { ok: false, error: 'No manifest path set' };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Update timestamp
|
|
304
|
+
this._data.updatedAt = new Date().toISOString();
|
|
305
|
+
|
|
306
|
+
// Ensure directory exists
|
|
307
|
+
await fs.ensureDir(path.dirname(this._manifestPath));
|
|
308
|
+
|
|
309
|
+
// Denormalize keys for storage
|
|
310
|
+
const storageData = ConfigManager._denormalizeKeys(this._data);
|
|
311
|
+
|
|
312
|
+
// Write to file
|
|
313
|
+
await fs.writeFile(this._manifestPath, safeDump(storageData), 'utf8');
|
|
314
|
+
|
|
315
|
+
this._dirty = false;
|
|
316
|
+
return { ok: true };
|
|
317
|
+
} catch (err) {
|
|
318
|
+
return { ok: false, error: err.message };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get all configuration data
|
|
324
|
+
* @returns {Object} All configuration data
|
|
325
|
+
*/
|
|
326
|
+
getAll() {
|
|
327
|
+
const result = {};
|
|
328
|
+
for (const key of VALID_CONFIG_KEYS) {
|
|
329
|
+
result[key] = this.get(key);
|
|
330
|
+
}
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if configuration has unsaved changes
|
|
336
|
+
* @returns {boolean}
|
|
337
|
+
*/
|
|
338
|
+
isDirty() {
|
|
339
|
+
return this._dirty;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get the manifest file path
|
|
344
|
+
* @returns {string|null}
|
|
345
|
+
*/
|
|
346
|
+
getManifestPath() {
|
|
347
|
+
return this._manifestPath;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Migrate configuration from an older format
|
|
352
|
+
* @param {Object} oldData - Old configuration data
|
|
353
|
+
* @returns {{ok: boolean, migrated: string[]}} Migration result
|
|
354
|
+
*/
|
|
355
|
+
migrate(oldData) {
|
|
356
|
+
const migrated = [];
|
|
357
|
+
|
|
358
|
+
// Migration: rename 'name' to 'userName'
|
|
359
|
+
if (oldData.name && !this._data.userName) {
|
|
360
|
+
this._data.userName = oldData.name;
|
|
361
|
+
migrated.push('name → userName');
|
|
362
|
+
this._dirty = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Migration: normalize IDE names
|
|
366
|
+
if (this._data.ides) {
|
|
367
|
+
const normalizedIdes = this._data.ides.map(ide => ide.toLowerCase());
|
|
368
|
+
if (JSON.stringify(normalizedIdes) !== JSON.stringify(this._data.ides)) {
|
|
369
|
+
this._data.ides = normalizedIdes;
|
|
370
|
+
migrated.push('ides normalized to lowercase');
|
|
371
|
+
this._dirty = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Migration: ensure agileflowFolder starts with dot
|
|
376
|
+
if (
|
|
377
|
+
this._data.agileflowFolder &&
|
|
378
|
+
!this._data.agileflowFolder.startsWith('.') &&
|
|
379
|
+
this._data.agileflowFolder !== 'agileflow'
|
|
380
|
+
) {
|
|
381
|
+
// Only migrate if it looks like it should have a dot
|
|
382
|
+
// Don't migrate 'agileflow' to '.agileflow' automatically
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { ok: true, migrated };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = {
|
|
390
|
+
ConfigManager,
|
|
391
|
+
CONFIG_SCHEMA,
|
|
392
|
+
VALID_CONFIG_KEYS,
|
|
393
|
+
EDITABLE_CONFIG_KEYS,
|
|
394
|
+
};
|
|
@@ -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
|
});
|