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.
- package/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- 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 +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- 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 +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- 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
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
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
27
|
+
// Re-export argument validators
|
|
28
|
+
const {
|
|
29
|
+
isPositiveInteger,
|
|
30
|
+
parseIntBounded,
|
|
31
|
+
isValidOption,
|
|
32
|
+
validateArgs,
|
|
33
|
+
} = require('./validate-args');
|
|
590
34
|
|
|
591
|
-
|
|
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.
|
|
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": {
|