agileflow 2.89.3 → 2.90.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/placeholder-registry.js +617 -0
  3. package/lib/smart-json-file.js +228 -1
  4. package/lib/table-formatter.js +519 -0
  5. package/lib/transient-status.js +374 -0
  6. package/lib/ui-manager.js +612 -0
  7. package/lib/validate-args.js +213 -0
  8. package/lib/validate-names.js +143 -0
  9. package/lib/validate-paths.js +434 -0
  10. package/lib/validate.js +37 -737
  11. package/package.json +3 -1
  12. package/scripts/check-update.js +17 -3
  13. package/scripts/lib/sessionRegistry.js +678 -0
  14. package/scripts/session-manager.js +77 -10
  15. package/scripts/tui/App.js +151 -0
  16. package/scripts/tui/index.js +31 -0
  17. package/scripts/tui/lib/crashRecovery.js +304 -0
  18. package/scripts/tui/lib/eventStream.js +309 -0
  19. package/scripts/tui/lib/keyboard.js +261 -0
  20. package/scripts/tui/lib/loopControl.js +371 -0
  21. package/scripts/tui/panels/OutputPanel.js +242 -0
  22. package/scripts/tui/panels/SessionPanel.js +170 -0
  23. package/scripts/tui/panels/TracePanel.js +298 -0
  24. package/scripts/tui/simple-tui.js +390 -0
  25. package/tools/cli/commands/config.js +7 -31
  26. package/tools/cli/commands/doctor.js +28 -39
  27. package/tools/cli/commands/list.js +47 -35
  28. package/tools/cli/commands/status.js +20 -38
  29. package/tools/cli/commands/tui.js +59 -0
  30. package/tools/cli/commands/uninstall.js +12 -39
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +382 -0
  33. package/tools/cli/lib/config-manager.js +394 -0
  34. package/tools/cli/lib/ide-registry.js +186 -0
  35. package/tools/cli/lib/npm-utils.js +17 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -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,23 @@ 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 =
84
+ 'TLS certificate error - check system time or update CA certificates';
85
+ }
86
+ debugLog('Network error', errorInfo);
74
87
  resolve(null);
75
88
  });
76
89
 
77
- req.setTimeout(5000, () => {
78
- debugLog('Request timeout');
90
+ // 10 second timeout for registry requests
91
+ req.setTimeout(10000, () => {
92
+ debugLog('Request timeout (10s)', { suggestion: 'npm registry may be slow. Retry later.' });
79
93
  req.destroy();
80
94
  resolve(null);
81
95
  });