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
package/lib/validate.js CHANGED
@@ -3,596 +3,47 @@
3
3
  *
4
4
  * Centralized validation patterns and helpers to prevent
5
5
  * command injection, path traversal, and invalid input handling.
6
- */
7
-
8
- const path = require('node:path');
9
- const fs = require('node:fs');
10
-
11
- /**
12
- * Validation patterns for common input types.
13
- * All patterns use strict whitelisting approach.
14
- */
15
- const PATTERNS = {
16
- // Git branch: alphanumeric, underscores, hyphens, forward slashes
17
- // Examples: main, feature/US-0001, session-1, my_branch
18
- branchName: /^[a-zA-Z0-9][a-zA-Z0-9_/-]*$/,
19
-
20
- // Story ID: US-0001 to US-99999
21
- storyId: /^US-\d{4,5}$/,
22
-
23
- // Epic ID: EP-0001 to EP-99999
24
- epicId: /^EP-\d{4,5}$/,
25
-
26
- // Feature name: lowercase with hyphens, starts with letter
27
- // Examples: damage-control, status-line, archival
28
- featureName: /^[a-z][a-z0-9-]*$/,
29
-
30
- // Profile name: alphanumeric with underscores/hyphens, starts with letter
31
- // Examples: default, my-profile, dev_config
32
- profileName: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
33
-
34
- // Command name: alphanumeric with hyphens/colons, starts with letter
35
- // Examples: babysit, story:list, agileflow:configure
36
- commandName: /^[a-zA-Z][a-zA-Z0-9:-]*$/,
37
-
38
- // Session nickname: alphanumeric with hyphens/underscores
39
- // Examples: auth-work, feature_1, main
40
- sessionNickname: /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
41
-
42
- // Merge strategy: squash or merge
43
- // Examples: squash, merge
44
- mergeStrategy: /^(squash|merge)$/,
45
- };
46
-
47
- /**
48
- * Validate a git branch name.
49
- * @param {string} name - Branch name to validate
50
- * @returns {boolean} True if valid
51
- */
52
- function isValidBranchName(name) {
53
- if (!name || typeof name !== 'string') return false;
54
- if (name.length > 255) return false; // Git limit
55
- if (name.startsWith('-')) return false; // Prevent flag injection
56
- if (name.includes('..')) return false; // Prevent path traversal
57
- if (name.endsWith('.lock')) return false; // Reserved
58
- return PATTERNS.branchName.test(name);
59
- }
60
-
61
- /**
62
- * Validate a story ID (US-XXXX format).
63
- * @param {string} id - Story ID to validate
64
- * @returns {boolean} True if valid
65
- */
66
- function isValidStoryId(id) {
67
- if (!id || typeof id !== 'string') return false;
68
- return PATTERNS.storyId.test(id);
69
- }
70
-
71
- /**
72
- * Validate an epic ID (EP-XXXX format).
73
- * @param {string} id - Epic ID to validate
74
- * @returns {boolean} True if valid
75
- */
76
- function isValidEpicId(id) {
77
- if (!id || typeof id !== 'string') return false;
78
- return PATTERNS.epicId.test(id);
79
- }
80
-
81
- /**
82
- * Validate a feature name.
83
- * @param {string} name - Feature name to validate
84
- * @returns {boolean} True if valid
85
- */
86
- function isValidFeatureName(name) {
87
- if (!name || typeof name !== 'string') return false;
88
- if (name.length > 50) return false;
89
- return PATTERNS.featureName.test(name);
90
- }
91
-
92
- /**
93
- * Validate a profile name.
94
- * @param {string} name - Profile name to validate
95
- * @returns {boolean} True if valid
96
- */
97
- function isValidProfileName(name) {
98
- if (!name || typeof name !== 'string') return false;
99
- if (name.length > 50) return false;
100
- return PATTERNS.profileName.test(name);
101
- }
102
-
103
- /**
104
- * Validate a command name.
105
- * @param {string} name - Command name to validate
106
- * @returns {boolean} True if valid
107
- */
108
- function isValidCommandName(name) {
109
- if (!name || typeof name !== 'string') return false;
110
- if (name.length > 100) return false;
111
- return PATTERNS.commandName.test(name);
112
- }
113
-
114
- /**
115
- * Validate a session nickname.
116
- * @param {string} name - Nickname to validate
117
- * @returns {boolean} True if valid
118
- */
119
- function isValidSessionNickname(name) {
120
- if (!name || typeof name !== 'string') return false;
121
- if (name.length > 50) return false;
122
- return PATTERNS.sessionNickname.test(name);
123
- }
124
-
125
- /**
126
- * Validate a merge strategy (squash or merge).
127
- * @param {string} strategy - Strategy to validate
128
- * @returns {boolean} True if valid
129
- */
130
- function isValidMergeStrategy(strategy) {
131
- if (!strategy || typeof strategy !== 'string') return false;
132
- return PATTERNS.mergeStrategy.test(strategy);
133
- }
134
-
135
- /**
136
- * Validate that a value is a positive integer within bounds.
137
- * @param {any} val - Value to validate
138
- * @param {number} min - Minimum allowed value (inclusive)
139
- * @param {number} max - Maximum allowed value (inclusive)
140
- * @returns {boolean} True if valid positive integer in range
141
- */
142
- function isPositiveInteger(val, min = 1, max = Number.MAX_SAFE_INTEGER) {
143
- const num = typeof val === 'string' ? parseInt(val, 10) : val;
144
- if (!Number.isInteger(num)) return false;
145
- return num >= min && num <= max;
146
- }
147
-
148
- /**
149
- * Parse and validate an integer with bounds.
150
- * @param {any} val - Value to parse
151
- * @param {number} defaultVal - Default if invalid
152
- * @param {number} min - Minimum allowed value
153
- * @param {number} max - Maximum allowed value
154
- * @returns {number} Parsed integer or default
155
- */
156
- function parseIntBounded(val, defaultVal, min = 1, max = Number.MAX_SAFE_INTEGER) {
157
- const num = typeof val === 'string' ? parseInt(val, 10) : val;
158
- if (!Number.isInteger(num) || num < min || num > max) {
159
- return defaultVal;
160
- }
161
- return num;
162
- }
163
-
164
- /**
165
- * Validate an option key against an allowed whitelist.
166
- * @param {string} key - Option key to validate
167
- * @param {string[]} allowedKeys - Array of allowed keys
168
- * @returns {boolean} True if key is in whitelist
169
- */
170
- function isValidOption(key, allowedKeys) {
171
- if (!key || typeof key !== 'string') return false;
172
- if (!Array.isArray(allowedKeys)) return false;
173
- return allowedKeys.includes(key);
174
- }
175
-
176
- /**
177
- * Validate and sanitize CLI arguments for known option types.
178
- * Returns validated args object or null if critical validation fails.
179
- *
180
- * @param {string[]} args - Raw process.argv slice
181
- * @param {Object} schema - Schema defining expected options
182
- * @returns {{ ok: boolean, data?: Object, error?: string }}
183
- *
184
- * @example
185
- * const schema = {
186
- * branch: { type: 'branchName', required: true },
187
- * max: { type: 'positiveInt', min: 1, max: 100, default: 20 },
188
- * profile: { type: 'profileName', default: 'default' }
189
- * };
190
- * const result = validateArgs(args, schema);
191
- */
192
- function validateArgs(args, schema) {
193
- const result = {};
194
- const errors = [];
195
-
196
- // Parse args into key-value pairs
197
- const parsed = {};
198
- for (let i = 0; i < args.length; i++) {
199
- const arg = args[i];
200
- if (arg.startsWith('--')) {
201
- const [key, ...valueParts] = arg.slice(2).split('=');
202
- const value = valueParts.length > 0 ? valueParts.join('=') : args[++i];
203
- parsed[key] = value;
204
- }
205
- }
206
-
207
- // Validate each schema field
208
- for (const [field, config] of Object.entries(schema)) {
209
- const value = parsed[field];
210
-
211
- // Check required
212
- if (config.required && (value === undefined || value === null)) {
213
- errors.push(`Missing required option: --${field}`);
214
- continue;
215
- }
216
-
217
- // Use default if not provided
218
- if (value === undefined || value === null) {
219
- if (config.default !== undefined) {
220
- result[field] = config.default;
221
- }
222
- continue;
223
- }
224
-
225
- // Validate by type
226
- switch (config.type) {
227
- case 'branchName':
228
- if (!isValidBranchName(value)) {
229
- errors.push(`Invalid branch name: ${value}`);
230
- } else {
231
- result[field] = value;
232
- }
233
- break;
234
-
235
- case 'storyId':
236
- if (!isValidStoryId(value)) {
237
- errors.push(`Invalid story ID: ${value} (expected US-XXXX)`);
238
- } else {
239
- result[field] = value;
240
- }
241
- break;
242
-
243
- case 'epicId':
244
- if (!isValidEpicId(value)) {
245
- errors.push(`Invalid epic ID: ${value} (expected EP-XXXX)`);
246
- } else {
247
- result[field] = value;
248
- }
249
- break;
250
-
251
- case 'featureName':
252
- if (!isValidFeatureName(value)) {
253
- errors.push(`Invalid feature name: ${value}`);
254
- } else {
255
- result[field] = value;
256
- }
257
- break;
258
-
259
- case 'profileName':
260
- if (!isValidProfileName(value)) {
261
- errors.push(`Invalid profile name: ${value}`);
262
- } else {
263
- result[field] = value;
264
- }
265
- break;
266
-
267
- case 'commandName':
268
- if (!isValidCommandName(value)) {
269
- errors.push(`Invalid command name: ${value}`);
270
- } else {
271
- result[field] = value;
272
- }
273
- break;
274
-
275
- case 'sessionNickname':
276
- if (!isValidSessionNickname(value)) {
277
- errors.push(`Invalid session nickname: ${value}`);
278
- } else {
279
- result[field] = value;
280
- }
281
- break;
282
-
283
- case 'positiveInt': {
284
- const min = config.min || 1;
285
- const max = config.max || Number.MAX_SAFE_INTEGER;
286
- if (!isPositiveInteger(value, min, max)) {
287
- errors.push(`Invalid integer for ${field}: ${value} (expected ${min}-${max})`);
288
- result[field] = config.default;
289
- } else {
290
- result[field] = parseInt(value, 10);
291
- }
292
- break;
293
- }
294
-
295
- case 'enum':
296
- if (!config.values.includes(value)) {
297
- errors.push(
298
- `Invalid value for ${field}: ${value} (expected: ${config.values.join(', ')})`
299
- );
300
- } else {
301
- result[field] = value;
302
- }
303
- break;
304
-
305
- case 'boolean':
306
- result[field] = value === 'true' || value === '1' || value === true;
307
- break;
308
-
309
- case 'string':
310
- // Basic string - just store it (caller should validate further if needed)
311
- result[field] = String(value);
312
- break;
313
-
314
- default:
315
- result[field] = value;
316
- }
317
- }
318
-
319
- if (errors.length > 0) {
320
- return { ok: false, error: errors.join('; ') };
321
- }
322
-
323
- return { ok: true, data: result };
324
- }
325
-
326
- // =============================================================================
327
- // Path Traversal Protection
328
- // =============================================================================
329
-
330
- /**
331
- * Path validation error with context.
332
- */
333
- class PathValidationError extends Error {
334
- /**
335
- * @param {string} message - Error message
336
- * @param {string} inputPath - The problematic path
337
- * @param {string} reason - Reason for rejection
338
- */
339
- constructor(message, inputPath, reason) {
340
- super(message);
341
- this.name = 'PathValidationError';
342
- this.inputPath = inputPath;
343
- this.reason = reason;
344
- Error.captureStackTrace(this, this.constructor);
345
- }
346
- }
347
-
348
- /**
349
- * Validate that a path is safe and within the allowed base directory.
350
- * Prevents path traversal attacks by:
351
- * 1. Resolving the path to absolute form
352
- * 2. Ensuring it stays within the base directory
353
- * 3. Rejecting symbolic links (optional)
354
- *
355
- * @param {string} inputPath - The path to validate (can be relative or absolute)
356
- * @param {string} baseDir - The allowed base directory (must be absolute)
357
- * @param {Object} options - Validation options
358
- * @param {boolean} [options.allowSymlinks=false] - Allow symbolic links
359
- * @param {boolean} [options.mustExist=false] - Path must exist on filesystem
360
- * @returns {{ ok: boolean, resolvedPath?: string, error?: PathValidationError }}
361
- *
362
- * @example
363
- * // Validate a file path within project directory
364
- * const result = validatePath('./config.yaml', '/home/user/project');
365
- * if (result.ok) {
366
- * console.log('Safe path:', result.resolvedPath);
367
- * }
368
- *
369
- * @example
370
- * // Reject path traversal attempt
371
- * const result = validatePath('../../../etc/passwd', '/home/user/project');
372
- * // result.ok === false
373
- * // result.error.reason === 'path_traversal'
374
- */
375
- function validatePath(inputPath, baseDir, options = {}) {
376
- const { allowSymlinks = false, mustExist = false } = options;
377
-
378
- // Input validation
379
- if (!inputPath || typeof inputPath !== 'string') {
380
- return {
381
- ok: false,
382
- error: new PathValidationError(
383
- 'Path is required and must be a string',
384
- String(inputPath),
385
- 'invalid_input'
386
- ),
387
- };
388
- }
389
-
390
- if (!baseDir || typeof baseDir !== 'string') {
391
- return {
392
- ok: false,
393
- error: new PathValidationError(
394
- 'Base directory is required and must be a string',
395
- inputPath,
396
- 'invalid_base'
397
- ),
398
- };
399
- }
400
-
401
- // Base directory must be absolute
402
- if (!path.isAbsolute(baseDir)) {
403
- return {
404
- ok: false,
405
- error: new PathValidationError(
406
- 'Base directory must be an absolute path',
407
- inputPath,
408
- 'relative_base'
409
- ),
410
- };
411
- }
412
-
413
- // Normalize the base directory
414
- const normalizedBase = path.resolve(baseDir);
415
-
416
- // Resolve the input path relative to base directory
417
- let resolvedPath;
418
- if (path.isAbsolute(inputPath)) {
419
- resolvedPath = path.resolve(inputPath);
420
- } else {
421
- resolvedPath = path.resolve(normalizedBase, inputPath);
422
- }
423
-
424
- // Check for path traversal: resolved path must start with base directory
425
- // Add trailing separator to prevent prefix attacks (e.g., /home/user vs /home/user2)
426
- const baseWithSep = normalizedBase.endsWith(path.sep)
427
- ? normalizedBase
428
- : normalizedBase + path.sep;
429
-
430
- const isWithinBase = resolvedPath === normalizedBase || resolvedPath.startsWith(baseWithSep);
431
-
432
- if (!isWithinBase) {
433
- return {
434
- ok: false,
435
- error: new PathValidationError(
436
- `Path escapes base directory: ${inputPath}`,
437
- inputPath,
438
- 'path_traversal'
439
- ),
440
- };
441
- }
442
-
443
- // Check if path exists (if required)
444
- if (mustExist) {
445
- try {
446
- fs.accessSync(resolvedPath);
447
- } catch {
448
- return {
449
- ok: false,
450
- error: new PathValidationError(
451
- `Path does not exist: ${resolvedPath}`,
452
- inputPath,
453
- 'not_found'
454
- ),
455
- };
456
- }
457
- }
458
-
459
- // Check for symbolic links (if not allowed)
460
- if (!allowSymlinks) {
461
- try {
462
- const stats = fs.lstatSync(resolvedPath);
463
- if (stats.isSymbolicLink()) {
464
- return {
465
- ok: false,
466
- error: new PathValidationError(
467
- `Symbolic links are not allowed: ${inputPath}`,
468
- inputPath,
469
- 'symlink_rejected'
470
- ),
471
- };
472
- }
473
- } catch {
474
- // Path doesn't exist yet, which is fine if mustExist is false
475
- // Check parent directories for symlinks
476
- const parts = path.relative(normalizedBase, resolvedPath).split(path.sep);
477
- let currentPath = normalizedBase;
478
-
479
- for (const part of parts) {
480
- currentPath = path.join(currentPath, part);
481
- try {
482
- const stats = fs.lstatSync(currentPath);
483
- if (stats.isSymbolicLink()) {
484
- return {
485
- ok: false,
486
- error: new PathValidationError(
487
- `Path contains symbolic link: ${currentPath}`,
488
- inputPath,
489
- 'symlink_in_path'
490
- ),
491
- };
492
- }
493
- } catch {
494
- // Part of path doesn't exist, stop checking
495
- break;
496
- }
497
- }
498
- }
499
- }
500
-
501
- return {
502
- ok: true,
503
- resolvedPath,
504
- };
505
- }
506
-
507
- /**
508
- * Synchronous version that throws on invalid paths.
509
- * Use when you want exceptions rather than result objects.
510
6
  *
511
- * @param {string} inputPath - The path to validate
512
- * @param {string} baseDir - The allowed base directory
513
- * @param {Object} options - Validation options
514
- * @returns {string} The validated absolute path
515
- * @throws {PathValidationError} If path is invalid
7
+ * This module re-exports from split validation modules for backward compatibility.
8
+ * For better performance, import directly from:
9
+ * - validate-names.js - Name/ID validation patterns
10
+ * - validate-args.js - CLI argument validation
11
+ * - validate-paths.js - Path traversal protection
516
12
  */
517
- function validatePathSync(inputPath, baseDir, options = {}) {
518
- const result = validatePath(inputPath, baseDir, options);
519
- if (!result.ok) {
520
- throw result.error;
521
- }
522
- return result.resolvedPath;
523
- }
524
13
 
525
- /**
526
- * Check if a path contains dangerous patterns without resolving.
527
- * Useful for quick pre-validation before expensive operations.
528
- *
529
- * @param {string} inputPath - The path to check
530
- * @returns {{ safe: boolean, reason?: string }}
531
- */
532
- function hasUnsafePathPatterns(inputPath) {
533
- if (!inputPath || typeof inputPath !== 'string') {
534
- return { safe: false, reason: 'invalid_input' };
535
- }
536
-
537
- // Check for null bytes (can bypass security in some systems)
538
- if (inputPath.includes('\0')) {
539
- return { safe: false, reason: 'null_byte' };
540
- }
541
-
542
- // Check for obvious traversal patterns
543
- if (inputPath.includes('..')) {
544
- return { safe: false, reason: 'dot_dot_sequence' };
545
- }
546
-
547
- // Check for absolute paths on Unix when expecting relative
548
- if (inputPath.startsWith('/') && !path.isAbsolute(inputPath)) {
549
- return { safe: false, reason: 'unexpected_absolute' };
550
- }
551
-
552
- // Check for Windows-style absolute paths
553
- if (/^[a-zA-Z]:/.test(inputPath)) {
554
- return { safe: false, reason: 'windows_absolute' };
555
- }
556
-
557
- return { safe: true };
558
- }
559
-
560
- /**
561
- * Sanitize a filename by removing dangerous characters.
562
- * Does NOT validate the full path - use with validatePath().
563
- *
564
- * @param {string} filename - The filename to sanitize
565
- * @param {Object} options - Sanitization options
566
- * @param {string} [options.replacement='_'] - Character to replace with
567
- * @param {number} [options.maxLength=255] - Maximum filename length
568
- * @returns {string} Sanitized filename
569
- */
570
- function sanitizeFilename(filename, options = {}) {
571
- const { replacement = '_', maxLength = 255 } = options;
572
-
573
- if (!filename || typeof filename !== 'string') {
574
- return '';
575
- }
576
-
577
- // Remove or replace dangerous characters
578
- let sanitized = filename
579
- .replace(/[<>:"/\\|?*\x00-\x1f]/g, replacement) // Control chars and reserved
580
- .replace(/\.{2,}/g, replacement) // Multiple dots
581
- .replace(/^\.+/, replacement) // Leading dots
582
- .replace(/^-+/, replacement); // Leading dashes (prevent flag injection)
14
+ // Re-export name validators
15
+ const {
16
+ PATTERNS,
17
+ isValidBranchName,
18
+ isValidStoryId,
19
+ isValidEpicId,
20
+ isValidFeatureName,
21
+ isValidProfileName,
22
+ isValidCommandName,
23
+ isValidSessionNickname,
24
+ isValidMergeStrategy,
25
+ } = require('./validate-names');
583
26
 
584
- // Truncate if too long
585
- if (sanitized.length > maxLength) {
586
- const ext = path.extname(sanitized);
587
- const base = path.basename(sanitized, ext);
588
- sanitized = base.slice(0, maxLength - ext.length) + ext;
589
- }
27
+ // Re-export argument validators
28
+ const {
29
+ isPositiveInteger,
30
+ parseIntBounded,
31
+ isValidOption,
32
+ validateArgs,
33
+ } = require('./validate-args');
590
34
 
591
- return sanitized;
592
- }
35
+ // Re-export path validators
36
+ const {
37
+ PathValidationError,
38
+ checkSymlinkChainDepth,
39
+ validatePath,
40
+ validatePathSync,
41
+ hasUnsafePathPatterns,
42
+ sanitizeFilename,
43
+ } = require('./validate-paths');
593
44
 
594
45
  module.exports = {
595
- // Patterns and basic validators
46
+ // Patterns and basic validators (from validate-names.js)
596
47
  PATTERNS,
597
48
  isValidBranchName,
598
49
  isValidStoryId,
@@ -602,15 +53,18 @@ module.exports = {
602
53
  isValidCommandName,
603
54
  isValidSessionNickname,
604
55
  isValidMergeStrategy,
56
+
57
+ // Argument validators (from validate-args.js)
605
58
  isPositiveInteger,
606
59
  parseIntBounded,
607
60
  isValidOption,
608
61
  validateArgs,
609
62
 
610
- // Path traversal protection
63
+ // Path traversal protection (from validate-paths.js)
611
64
  PathValidationError,
612
65
  validatePath,
613
66
  validatePathSync,
614
67
  hasUnsafePathPatterns,
615
68
  sanitizeFilename,
69
+ checkSymlinkChainDepth,
616
70
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.89.2",
3
+ "version": "2.90.0",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -56,9 +56,12 @@
56
56
  "chalk": "^4.1.2",
57
57
  "commander": "^12.1.0",
58
58
  "fs-extra": "^11.2.0",
59
+ "ink": "^4.4.1",
60
+ "ink-spinner": "^5.0.0",
59
61
  "inquirer": "^8.2.6",
60
62
  "js-yaml": "^4.1.0",
61
63
  "ora": "^5.4.1",
64
+ "react": "^18.2.0",
62
65
  "semver": "^7.6.3"
63
66
  },
64
67
  "optionalDependencies": {