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.
Files changed (37) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +3 -3
  3. package/lib/placeholder-registry.js +617 -0
  4. package/lib/smart-json-file.js +205 -1
  5. package/lib/table-formatter.js +504 -0
  6. package/lib/transient-status.js +374 -0
  7. package/lib/ui-manager.js +612 -0
  8. package/lib/validate-args.js +213 -0
  9. package/lib/validate-names.js +143 -0
  10. package/lib/validate-paths.js +434 -0
  11. package/lib/validate.js +37 -737
  12. package/package.json +4 -1
  13. package/scripts/check-update.js +16 -3
  14. package/scripts/lib/sessionRegistry.js +682 -0
  15. package/scripts/session-manager.js +77 -10
  16. package/scripts/tui/App.js +176 -0
  17. package/scripts/tui/index.js +75 -0
  18. package/scripts/tui/lib/crashRecovery.js +302 -0
  19. package/scripts/tui/lib/eventStream.js +316 -0
  20. package/scripts/tui/lib/keyboard.js +252 -0
  21. package/scripts/tui/lib/loopControl.js +371 -0
  22. package/scripts/tui/panels/OutputPanel.js +278 -0
  23. package/scripts/tui/panels/SessionPanel.js +178 -0
  24. package/scripts/tui/panels/TracePanel.js +333 -0
  25. package/src/core/commands/tui.md +91 -0
  26. package/tools/cli/commands/config.js +7 -30
  27. package/tools/cli/commands/doctor.js +18 -38
  28. package/tools/cli/commands/list.js +47 -35
  29. package/tools/cli/commands/status.js +13 -37
  30. package/tools/cli/commands/uninstall.js +9 -38
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +374 -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 +16 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * AgileFlow CLI - Argument Validation Utilities
3
+ *
4
+ * Validation utilities for command-line arguments and options.
5
+ */
6
+
7
+ const {
8
+ isValidBranchName,
9
+ isValidStoryId,
10
+ isValidEpicId,
11
+ isValidFeatureName,
12
+ isValidProfileName,
13
+ isValidCommandName,
14
+ isValidSessionNickname,
15
+ } = require('./validate-names');
16
+
17
+ /**
18
+ * Validate that a value is a positive integer within bounds.
19
+ * @param {any} val - Value to validate
20
+ * @param {number} min - Minimum allowed value (inclusive)
21
+ * @param {number} max - Maximum allowed value (inclusive)
22
+ * @returns {boolean} True if valid positive integer in range
23
+ */
24
+ function isPositiveInteger(val, min = 1, max = Number.MAX_SAFE_INTEGER) {
25
+ const num = typeof val === 'string' ? parseInt(val, 10) : val;
26
+ if (!Number.isInteger(num)) return false;
27
+ return num >= min && num <= max;
28
+ }
29
+
30
+ /**
31
+ * Parse and validate an integer with bounds.
32
+ * @param {any} val - Value to parse
33
+ * @param {number} defaultVal - Default if invalid
34
+ * @param {number} min - Minimum allowed value
35
+ * @param {number} max - Maximum allowed value
36
+ * @returns {number} Parsed integer or default
37
+ */
38
+ function parseIntBounded(val, defaultVal, min = 1, max = Number.MAX_SAFE_INTEGER) {
39
+ const num = typeof val === 'string' ? parseInt(val, 10) : val;
40
+ if (!Number.isInteger(num) || num < min || num > max) {
41
+ return defaultVal;
42
+ }
43
+ return num;
44
+ }
45
+
46
+ /**
47
+ * Validate an option key against an allowed whitelist.
48
+ * @param {string} key - Option key to validate
49
+ * @param {string[]} allowedKeys - Array of allowed keys
50
+ * @returns {boolean} True if key is in whitelist
51
+ */
52
+ function isValidOption(key, allowedKeys) {
53
+ if (!key || typeof key !== 'string') return false;
54
+ if (!Array.isArray(allowedKeys)) return false;
55
+ return allowedKeys.includes(key);
56
+ }
57
+
58
+ /**
59
+ * Validate and sanitize CLI arguments for known option types.
60
+ * Returns validated args object or null if critical validation fails.
61
+ *
62
+ * @param {string[]} args - Raw process.argv slice
63
+ * @param {Object} schema - Schema defining expected options
64
+ * @returns {{ ok: boolean, data?: Object, error?: string }}
65
+ *
66
+ * @example
67
+ * const schema = {
68
+ * branch: { type: 'branchName', required: true },
69
+ * max: { type: 'positiveInt', min: 1, max: 100, default: 20 },
70
+ * profile: { type: 'profileName', default: 'default' }
71
+ * };
72
+ * const result = validateArgs(args, schema);
73
+ */
74
+ function validateArgs(args, schema) {
75
+ const result = {};
76
+ const errors = [];
77
+
78
+ // Parse args into key-value pairs
79
+ const parsed = {};
80
+ for (let i = 0; i < args.length; i++) {
81
+ const arg = args[i];
82
+ if (arg.startsWith('--')) {
83
+ const [key, ...valueParts] = arg.slice(2).split('=');
84
+ const value = valueParts.length > 0 ? valueParts.join('=') : args[++i];
85
+ parsed[key] = value;
86
+ }
87
+ }
88
+
89
+ // Validate each schema field
90
+ for (const [field, config] of Object.entries(schema)) {
91
+ const value = parsed[field];
92
+
93
+ // Check required
94
+ if (config.required && (value === undefined || value === null)) {
95
+ errors.push(`Missing required option: --${field}`);
96
+ continue;
97
+ }
98
+
99
+ // Use default if not provided
100
+ if (value === undefined || value === null) {
101
+ if (config.default !== undefined) {
102
+ result[field] = config.default;
103
+ }
104
+ continue;
105
+ }
106
+
107
+ // Validate by type
108
+ switch (config.type) {
109
+ case 'branchName':
110
+ if (!isValidBranchName(value)) {
111
+ errors.push(`Invalid branch name: ${value}`);
112
+ } else {
113
+ result[field] = value;
114
+ }
115
+ break;
116
+
117
+ case 'storyId':
118
+ if (!isValidStoryId(value)) {
119
+ errors.push(`Invalid story ID: ${value} (expected US-XXXX)`);
120
+ } else {
121
+ result[field] = value;
122
+ }
123
+ break;
124
+
125
+ case 'epicId':
126
+ if (!isValidEpicId(value)) {
127
+ errors.push(`Invalid epic ID: ${value} (expected EP-XXXX)`);
128
+ } else {
129
+ result[field] = value;
130
+ }
131
+ break;
132
+
133
+ case 'featureName':
134
+ if (!isValidFeatureName(value)) {
135
+ errors.push(`Invalid feature name: ${value}`);
136
+ } else {
137
+ result[field] = value;
138
+ }
139
+ break;
140
+
141
+ case 'profileName':
142
+ if (!isValidProfileName(value)) {
143
+ errors.push(`Invalid profile name: ${value}`);
144
+ } else {
145
+ result[field] = value;
146
+ }
147
+ break;
148
+
149
+ case 'commandName':
150
+ if (!isValidCommandName(value)) {
151
+ errors.push(`Invalid command name: ${value}`);
152
+ } else {
153
+ result[field] = value;
154
+ }
155
+ break;
156
+
157
+ case 'sessionNickname':
158
+ if (!isValidSessionNickname(value)) {
159
+ errors.push(`Invalid session nickname: ${value}`);
160
+ } else {
161
+ result[field] = value;
162
+ }
163
+ break;
164
+
165
+ case 'positiveInt': {
166
+ const min = config.min || 1;
167
+ const max = config.max || Number.MAX_SAFE_INTEGER;
168
+ if (!isPositiveInteger(value, min, max)) {
169
+ errors.push(`Invalid integer for ${field}: ${value} (expected ${min}-${max})`);
170
+ result[field] = config.default;
171
+ } else {
172
+ result[field] = parseInt(value, 10);
173
+ }
174
+ break;
175
+ }
176
+
177
+ case 'enum':
178
+ if (!config.values.includes(value)) {
179
+ errors.push(
180
+ `Invalid value for ${field}: ${value} (expected: ${config.values.join(', ')})`
181
+ );
182
+ } else {
183
+ result[field] = value;
184
+ }
185
+ break;
186
+
187
+ case 'boolean':
188
+ result[field] = value === 'true' || value === '1' || value === true;
189
+ break;
190
+
191
+ case 'string':
192
+ // Basic string - just store it (caller should validate further if needed)
193
+ result[field] = String(value);
194
+ break;
195
+
196
+ default:
197
+ result[field] = value;
198
+ }
199
+ }
200
+
201
+ if (errors.length > 0) {
202
+ return { ok: false, error: errors.join('; ') };
203
+ }
204
+
205
+ return { ok: true, data: result };
206
+ }
207
+
208
+ module.exports = {
209
+ isPositiveInteger,
210
+ parseIntBounded,
211
+ isValidOption,
212
+ validateArgs,
213
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * AgileFlow CLI - Name Validation Utilities
3
+ *
4
+ * Validation patterns for names, IDs, and identifiers.
5
+ * Pre-compiled regex patterns for optimal performance.
6
+ */
7
+
8
+ /**
9
+ * Pre-compiled validation patterns for common input types.
10
+ * All patterns use strict whitelisting approach.
11
+ * Compiled once at module load for performance.
12
+ */
13
+ const PATTERNS = {
14
+ // Git branch: alphanumeric, underscores, hyphens, forward slashes
15
+ // Examples: main, feature/US-0001, session-1, my_branch
16
+ branchName: /^[a-zA-Z0-9][a-zA-Z0-9_/-]*$/,
17
+
18
+ // Story ID: US-0001 to US-99999
19
+ storyId: /^US-\d{4,5}$/,
20
+
21
+ // Epic ID: EP-0001 to EP-99999
22
+ epicId: /^EP-\d{4,5}$/,
23
+
24
+ // Feature name: lowercase with hyphens, starts with letter
25
+ // Examples: damage-control, status-line, archival
26
+ featureName: /^[a-z][a-z0-9-]*$/,
27
+
28
+ // Profile name: alphanumeric with underscores/hyphens, starts with letter
29
+ // Examples: default, my-profile, dev_config
30
+ profileName: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
31
+
32
+ // Command name: alphanumeric with hyphens/colons, starts with letter
33
+ // Examples: babysit, story:list, agileflow:configure
34
+ commandName: /^[a-zA-Z][a-zA-Z0-9:-]*$/,
35
+
36
+ // Session nickname: alphanumeric with hyphens/underscores
37
+ // Examples: auth-work, feature_1, main
38
+ sessionNickname: /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
39
+
40
+ // Merge strategy: squash or merge
41
+ // Examples: squash, merge
42
+ mergeStrategy: /^(squash|merge)$/,
43
+ };
44
+
45
+ /**
46
+ * Validate a git branch name.
47
+ * @param {string} name - Branch name to validate
48
+ * @returns {boolean} True if valid
49
+ */
50
+ function isValidBranchName(name) {
51
+ if (!name || typeof name !== 'string') return false;
52
+ if (name.length > 255) return false; // Git limit
53
+ if (name.startsWith('-')) return false; // Prevent flag injection
54
+ if (name.includes('..')) return false; // Prevent path traversal
55
+ if (name.endsWith('.lock')) return false; // Reserved
56
+ return PATTERNS.branchName.test(name);
57
+ }
58
+
59
+ /**
60
+ * Validate a story ID (US-XXXX format).
61
+ * @param {string} id - Story ID to validate
62
+ * @returns {boolean} True if valid
63
+ */
64
+ function isValidStoryId(id) {
65
+ if (!id || typeof id !== 'string') return false;
66
+ return PATTERNS.storyId.test(id);
67
+ }
68
+
69
+ /**
70
+ * Validate an epic ID (EP-XXXX format).
71
+ * @param {string} id - Epic ID to validate
72
+ * @returns {boolean} True if valid
73
+ */
74
+ function isValidEpicId(id) {
75
+ if (!id || typeof id !== 'string') return false;
76
+ return PATTERNS.epicId.test(id);
77
+ }
78
+
79
+ /**
80
+ * Validate a feature name.
81
+ * @param {string} name - Feature name to validate
82
+ * @returns {boolean} True if valid
83
+ */
84
+ function isValidFeatureName(name) {
85
+ if (!name || typeof name !== 'string') return false;
86
+ if (name.length > 50) return false;
87
+ return PATTERNS.featureName.test(name);
88
+ }
89
+
90
+ /**
91
+ * Validate a profile name.
92
+ * @param {string} name - Profile name to validate
93
+ * @returns {boolean} True if valid
94
+ */
95
+ function isValidProfileName(name) {
96
+ if (!name || typeof name !== 'string') return false;
97
+ if (name.length > 50) return false;
98
+ return PATTERNS.profileName.test(name);
99
+ }
100
+
101
+ /**
102
+ * Validate a command name.
103
+ * @param {string} name - Command name to validate
104
+ * @returns {boolean} True if valid
105
+ */
106
+ function isValidCommandName(name) {
107
+ if (!name || typeof name !== 'string') return false;
108
+ if (name.length > 100) return false;
109
+ return PATTERNS.commandName.test(name);
110
+ }
111
+
112
+ /**
113
+ * Validate a session nickname.
114
+ * @param {string} name - Nickname to validate
115
+ * @returns {boolean} True if valid
116
+ */
117
+ function isValidSessionNickname(name) {
118
+ if (!name || typeof name !== 'string') return false;
119
+ if (name.length > 50) return false;
120
+ return PATTERNS.sessionNickname.test(name);
121
+ }
122
+
123
+ /**
124
+ * Validate a merge strategy (squash or merge).
125
+ * @param {string} strategy - Strategy to validate
126
+ * @returns {boolean} True if valid
127
+ */
128
+ function isValidMergeStrategy(strategy) {
129
+ if (!strategy || typeof strategy !== 'string') return false;
130
+ return PATTERNS.mergeStrategy.test(strategy);
131
+ }
132
+
133
+ module.exports = {
134
+ PATTERNS,
135
+ isValidBranchName,
136
+ isValidStoryId,
137
+ isValidEpicId,
138
+ isValidFeatureName,
139
+ isValidProfileName,
140
+ isValidCommandName,
141
+ isValidSessionNickname,
142
+ isValidMergeStrategy,
143
+ };