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,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
+ };
@@ -0,0 +1,491 @@
1
+ /**
2
+ * AgileFlow CLI - Validation Middleware
3
+ *
4
+ * Provides middleware layer for input validation that runs
5
+ * before command action handlers.
6
+ *
7
+ * Usage:
8
+ * const { withValidation, schemas } = require('./lib/validation-middleware');
9
+ * module.exports = {
10
+ * name: 'mycommand',
11
+ * validate: {
12
+ * directory: schemas.path,
13
+ * ide: schemas.ide,
14
+ * },
15
+ * action: withValidation(async (options) => { ... })
16
+ * };
17
+ */
18
+
19
+ const path = require('path');
20
+ const { validatePath, isValidStoryId, isValidEpicId } = require('../../../lib/validate');
21
+ const { IdeRegistry } = require('./ide-registry');
22
+
23
+ /**
24
+ * Validation schema definition
25
+ * @typedef {Object} ValidationSchema
26
+ * @property {string} type - Schema type (path, ide, storyId, epicId, string, number)
27
+ * @property {boolean} [required=false] - Whether the field is required
28
+ * @property {*} [default] - Default value if not provided
29
+ * @property {Function} [validate] - Custom validation function
30
+ * @property {string} [errorMessage] - Custom error message
31
+ */
32
+
33
+ /**
34
+ * Validation result
35
+ * @typedef {Object} ValidationResult
36
+ * @property {boolean} ok - Whether validation passed
37
+ * @property {Object} [data] - Validated and transformed data
38
+ * @property {string[]} [errors] - List of validation errors
39
+ */
40
+
41
+ /**
42
+ * Pre-defined validation schemas for common types
43
+ */
44
+ const schemas = {
45
+ /**
46
+ * Path schema - validates directory/file paths
47
+ * Automatically resolves to absolute path and validates against traversal
48
+ */
49
+ path: {
50
+ type: 'path',
51
+ required: false,
52
+ default: '.',
53
+ errorMessage: 'Invalid path',
54
+ },
55
+
56
+ /**
57
+ * Required path schema
58
+ */
59
+ pathRequired: {
60
+ type: 'path',
61
+ required: true,
62
+ errorMessage: 'Path is required',
63
+ },
64
+
65
+ /**
66
+ * IDE schema - validates IDE name against registry
67
+ */
68
+ ide: {
69
+ type: 'ide',
70
+ required: false,
71
+ errorMessage: 'Invalid IDE name',
72
+ },
73
+
74
+ /**
75
+ * Required IDE schema
76
+ */
77
+ ideRequired: {
78
+ type: 'ide',
79
+ required: true,
80
+ errorMessage: 'IDE name is required',
81
+ },
82
+
83
+ /**
84
+ * Story ID schema - validates US-XXXX format
85
+ */
86
+ storyId: {
87
+ type: 'storyId',
88
+ required: false,
89
+ errorMessage: 'Invalid story ID (expected US-XXXX)',
90
+ },
91
+
92
+ /**
93
+ * Required story ID schema
94
+ */
95
+ storyIdRequired: {
96
+ type: 'storyId',
97
+ required: true,
98
+ errorMessage: 'Story ID is required (expected US-XXXX)',
99
+ },
100
+
101
+ /**
102
+ * Epic ID schema - validates EP-XXXX format
103
+ */
104
+ epicId: {
105
+ type: 'epicId',
106
+ required: false,
107
+ errorMessage: 'Invalid epic ID (expected EP-XXXX)',
108
+ },
109
+
110
+ /**
111
+ * Required epic ID schema
112
+ */
113
+ epicIdRequired: {
114
+ type: 'epicId',
115
+ required: true,
116
+ errorMessage: 'Epic ID is required (expected EP-XXXX)',
117
+ },
118
+
119
+ /**
120
+ * String schema with optional length constraints
121
+ * @param {Object} options - Schema options
122
+ * @returns {ValidationSchema}
123
+ */
124
+ string: (options = {}) => ({
125
+ type: 'string',
126
+ required: options.required || false,
127
+ minLength: options.minLength,
128
+ maxLength: options.maxLength,
129
+ pattern: options.pattern,
130
+ errorMessage: options.errorMessage || 'Invalid string value',
131
+ }),
132
+
133
+ /**
134
+ * Number schema with optional range constraints
135
+ * @param {Object} options - Schema options
136
+ * @returns {ValidationSchema}
137
+ */
138
+ number: (options = {}) => ({
139
+ type: 'number',
140
+ required: options.required || false,
141
+ min: options.min,
142
+ max: options.max,
143
+ integer: options.integer || false,
144
+ errorMessage: options.errorMessage || 'Invalid number value',
145
+ }),
146
+
147
+ /**
148
+ * Boolean schema
149
+ */
150
+ boolean: {
151
+ type: 'boolean',
152
+ required: false,
153
+ errorMessage: 'Invalid boolean value',
154
+ },
155
+
156
+ /**
157
+ * Enum schema
158
+ * @param {string[]} values - Allowed values
159
+ * @param {Object} options - Schema options
160
+ * @returns {ValidationSchema}
161
+ */
162
+ enum: (values, options = {}) => ({
163
+ type: 'enum',
164
+ values,
165
+ required: options.required || false,
166
+ errorMessage: options.errorMessage || `Invalid value (expected: ${values.join(', ')})`,
167
+ }),
168
+ };
169
+
170
+ /**
171
+ * Validate a single field against its schema
172
+ * @param {string} fieldName - Name of the field
173
+ * @param {*} value - Value to validate
174
+ * @param {ValidationSchema} schema - Schema to validate against
175
+ * @param {string} baseDir - Base directory for path validation
176
+ * @returns {ValidationResult}
177
+ */
178
+ function validateField(fieldName, value, schema, baseDir) {
179
+ const errors = [];
180
+
181
+ // Check required
182
+ if (schema.required && (value === undefined || value === null || value === '')) {
183
+ return {
184
+ ok: false,
185
+ errors: [schema.errorMessage || `${fieldName} is required`],
186
+ };
187
+ }
188
+
189
+ // Return default if not provided
190
+ if (value === undefined || value === null || value === '') {
191
+ return {
192
+ ok: true,
193
+ data: schema.default,
194
+ };
195
+ }
196
+
197
+ // Validate by type
198
+ switch (schema.type) {
199
+ case 'path': {
200
+ // Convert to string if not already
201
+ const pathValue = String(value);
202
+
203
+ // Resolve path relative to current directory
204
+ const resolvedPath = path.resolve(pathValue);
205
+
206
+ // Validate path is within safe bounds
207
+ const pathResult = validatePath(pathValue, baseDir, {
208
+ allowSymlinks: false,
209
+ mustExist: false,
210
+ });
211
+
212
+ if (!pathResult.ok) {
213
+ return {
214
+ ok: false,
215
+ errors: [pathResult.error?.message || schema.errorMessage || 'Invalid path'],
216
+ };
217
+ }
218
+
219
+ return {
220
+ ok: true,
221
+ data: resolvedPath,
222
+ };
223
+ }
224
+
225
+ case 'ide': {
226
+ // Normalize IDE name to lowercase before validation
227
+ const normalizedIde = String(value).toLowerCase();
228
+ const validation = IdeRegistry.validate(normalizedIde);
229
+ if (!validation.ok) {
230
+ return {
231
+ ok: false,
232
+ errors: [validation.error || schema.errorMessage],
233
+ };
234
+ }
235
+ return {
236
+ ok: true,
237
+ data: normalizedIde,
238
+ };
239
+ }
240
+
241
+ case 'storyId': {
242
+ // Normalize story ID to uppercase before validation
243
+ const normalizedStoryId = String(value).toUpperCase();
244
+ if (!isValidStoryId(normalizedStoryId)) {
245
+ return {
246
+ ok: false,
247
+ errors: [schema.errorMessage || 'Invalid story ID (expected US-XXXX)'],
248
+ };
249
+ }
250
+ return {
251
+ ok: true,
252
+ data: normalizedStoryId,
253
+ };
254
+ }
255
+
256
+ case 'epicId': {
257
+ // Normalize epic ID to uppercase before validation
258
+ const normalizedEpicId = String(value).toUpperCase();
259
+ if (!isValidEpicId(normalizedEpicId)) {
260
+ return {
261
+ ok: false,
262
+ errors: [schema.errorMessage || 'Invalid epic ID (expected EP-XXXX)'],
263
+ };
264
+ }
265
+ return {
266
+ ok: true,
267
+ data: normalizedEpicId,
268
+ };
269
+ }
270
+
271
+ case 'string': {
272
+ if (typeof value !== 'string') {
273
+ return {
274
+ ok: false,
275
+ errors: [schema.errorMessage || `${fieldName} must be a string`],
276
+ };
277
+ }
278
+
279
+ if (schema.minLength && value.length < schema.minLength) {
280
+ return {
281
+ ok: false,
282
+ errors: [`${fieldName} must be at least ${schema.minLength} characters`],
283
+ };
284
+ }
285
+
286
+ if (schema.maxLength && value.length > schema.maxLength) {
287
+ return {
288
+ ok: false,
289
+ errors: [`${fieldName} must be at most ${schema.maxLength} characters`],
290
+ };
291
+ }
292
+
293
+ if (schema.pattern && !schema.pattern.test(value)) {
294
+ return {
295
+ ok: false,
296
+ errors: [schema.errorMessage || `${fieldName} has invalid format`],
297
+ };
298
+ }
299
+
300
+ return {
301
+ ok: true,
302
+ data: value,
303
+ };
304
+ }
305
+
306
+ case 'number': {
307
+ const num = parseFloat(value);
308
+
309
+ if (isNaN(num)) {
310
+ return {
311
+ ok: false,
312
+ errors: [schema.errorMessage || `${fieldName} must be a number`],
313
+ };
314
+ }
315
+
316
+ if (schema.integer && !Number.isInteger(num)) {
317
+ return {
318
+ ok: false,
319
+ errors: [`${fieldName} must be an integer`],
320
+ };
321
+ }
322
+
323
+ if (schema.min !== undefined && num < schema.min) {
324
+ return {
325
+ ok: false,
326
+ errors: [`${fieldName} must be at least ${schema.min}`],
327
+ };
328
+ }
329
+
330
+ if (schema.max !== undefined && num > schema.max) {
331
+ return {
332
+ ok: false,
333
+ errors: [`${fieldName} must be at most ${schema.max}`],
334
+ };
335
+ }
336
+
337
+ return {
338
+ ok: true,
339
+ data: num,
340
+ };
341
+ }
342
+
343
+ case 'boolean': {
344
+ const boolValue = value === true || value === 'true' || value === '1';
345
+ return {
346
+ ok: true,
347
+ data: boolValue,
348
+ };
349
+ }
350
+
351
+ case 'enum': {
352
+ if (!schema.values.includes(value)) {
353
+ return {
354
+ ok: false,
355
+ errors: [schema.errorMessage || `Invalid value for ${fieldName}`],
356
+ };
357
+ }
358
+ return {
359
+ ok: true,
360
+ data: value,
361
+ };
362
+ }
363
+
364
+ default:
365
+ // Unknown type - pass through
366
+ return {
367
+ ok: true,
368
+ data: value,
369
+ };
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Validate all fields in options against their schemas
375
+ * @param {Object} options - Command options
376
+ * @param {Object} validationSchemas - Schema definitions
377
+ * @param {string} [baseDir=process.cwd()] - Base directory for path validation
378
+ * @returns {ValidationResult}
379
+ */
380
+ function validateOptions(options, validationSchemas, baseDir = process.cwd()) {
381
+ const errors = [];
382
+ const validatedData = { ...options };
383
+
384
+ for (const [fieldName, schema] of Object.entries(validationSchemas)) {
385
+ const value = options[fieldName];
386
+ const result = validateField(fieldName, value, schema, baseDir);
387
+
388
+ if (!result.ok) {
389
+ errors.push(...result.errors);
390
+ } else {
391
+ validatedData[fieldName] = result.data;
392
+ }
393
+ }
394
+
395
+ if (errors.length > 0) {
396
+ return { ok: false, errors };
397
+ }
398
+
399
+ return { ok: true, data: validatedData };
400
+ }
401
+
402
+ /**
403
+ * Create a validation wrapper for command actions
404
+ * @param {Function} action - Original command action
405
+ * @param {Object} validationSchemas - Schema definitions
406
+ * @returns {Function} Wrapped action with validation
407
+ */
408
+ function createValidationWrapper(action, validationSchemas) {
409
+ return async function validatedAction(...args) {
410
+ // Get options from the last argument (Commander.js pattern)
411
+ const options = args[args.length - 1];
412
+
413
+ // Validate options
414
+ const result = validateOptions(options, validationSchemas);
415
+
416
+ if (!result.ok) {
417
+ const { ErrorHandler } = require('./error-handler');
418
+ const handler = new ErrorHandler('validation');
419
+
420
+ // Format errors nicely
421
+ const errorList = result.errors.map(e => ` • ${e}`).join('\n');
422
+ handler.warning(
423
+ 'Input validation failed',
424
+ `Please fix the following:\n${errorList}`,
425
+ 'Check command help with --help'
426
+ );
427
+ return;
428
+ }
429
+
430
+ // Replace options with validated data
431
+ args[args.length - 1] = { ...options, ...result.data };
432
+
433
+ // Call original action
434
+ return action.apply(this, args);
435
+ };
436
+ }
437
+
438
+ /**
439
+ * Decorator to add validation to a command action
440
+ * @param {Object} validationSchemas - Schema definitions
441
+ * @returns {Function} Decorator function
442
+ */
443
+ function withValidation(validationSchemas) {
444
+ return function (action) {
445
+ return createValidationWrapper(action, validationSchemas);
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Shorthand to wrap an action directly with schemas
451
+ * @param {Function} action - Action function
452
+ * @param {Object} validationSchemas - Schema definitions
453
+ * @returns {Function} Wrapped action
454
+ */
455
+ function validated(action, validationSchemas) {
456
+ return createValidationWrapper(action, validationSchemas);
457
+ }
458
+
459
+ /**
460
+ * Validate path argument automatically
461
+ * Use as middleware before any path-based command
462
+ * @param {string} pathValue - Path value from options
463
+ * @param {string} fieldName - Name of the path field
464
+ * @returns {ValidationResult}
465
+ */
466
+ function validatePathArgument(pathValue, fieldName = 'path') {
467
+ return validateField(fieldName, pathValue, schemas.path, process.cwd());
468
+ }
469
+
470
+ /**
471
+ * Format validation errors for display
472
+ * @param {string[]} errors - List of errors
473
+ * @returns {string} Formatted error message
474
+ */
475
+ function formatValidationErrors(errors) {
476
+ if (errors.length === 1) {
477
+ return errors[0];
478
+ }
479
+ return errors.map((e, i) => `${i + 1}. ${e}`).join('\n');
480
+ }
481
+
482
+ module.exports = {
483
+ schemas,
484
+ validateField,
485
+ validateOptions,
486
+ createValidationWrapper,
487
+ withValidation,
488
+ validated,
489
+ validatePathArgument,
490
+ formatValidationErrors,
491
+ };