agileflow 2.89.1 → 2.89.3
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/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/session-registry.js +461 -0
- package/lib/smart-json-file.js +449 -0
- package/lib/validate.js +165 -11
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- 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/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/tools/cli/commands/config.js +3 -3
- package/tools/cli/commands/doctor.js +30 -2
- package/tools/cli/commands/list.js +2 -2
- package/tools/cli/commands/uninstall.js +3 -3
- package/tools/cli/installers/core/installer.js +62 -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/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
package/lib/errors.js
CHANGED
|
@@ -21,11 +21,214 @@ const { execSync } = require('child_process');
|
|
|
21
21
|
// Debug logging (opt-in via AGILEFLOW_DEBUG=1)
|
|
22
22
|
const DEBUG = process.env.AGILEFLOW_DEBUG === '1';
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Patterns for detecting secrets that should be redacted from debug output.
|
|
26
|
+
* Each pattern captures the sensitive portion for replacement.
|
|
27
|
+
* ORDER MATTERS: More specific patterns should come before generic ones.
|
|
28
|
+
*/
|
|
29
|
+
const SECRET_PATTERNS = [
|
|
30
|
+
// JWT tokens (header.payload.signature format) - MUST be before generic token pattern
|
|
31
|
+
{
|
|
32
|
+
pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
|
33
|
+
replacement: '***JWT_REDACTED***',
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// NPM tokens - MUST be before generic token pattern
|
|
37
|
+
{ pattern: /\bnpm_[A-Za-z0-9]{36}/g, replacement: '***NPM_TOKEN_REDACTED***' },
|
|
38
|
+
{
|
|
39
|
+
pattern: /\/\/registry\.npmjs\.org\/:_authToken=([A-Za-z0-9_-]+)/g,
|
|
40
|
+
replacement: '//registry.npmjs.org/:_authToken=***REDACTED***',
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// GitHub tokens - MUST be before generic token pattern
|
|
44
|
+
{ pattern: /\b(ghp_[A-Za-z0-9]{36})/g, replacement: '***GITHUB_TOKEN_REDACTED***' },
|
|
45
|
+
{ pattern: /\b(github_pat_[A-Za-z0-9_]{22,})/g, replacement: '***GITHUB_PAT_REDACTED***' },
|
|
46
|
+
|
|
47
|
+
// AWS keys
|
|
48
|
+
{ pattern: /\b(AKIA[A-Z0-9]{16})/g, replacement: '***AWS_KEY_REDACTED***' },
|
|
49
|
+
{
|
|
50
|
+
pattern: /\b(aws_secret_access_key)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})["']?/gi,
|
|
51
|
+
replacement: '$1=***REDACTED***',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Bearer tokens
|
|
55
|
+
{ pattern: /\bBearer\s+([A-Za-z0-9_.-]{20,})/gi, replacement: 'Bearer ***REDACTED***' },
|
|
56
|
+
|
|
57
|
+
// Git URLs with credentials
|
|
58
|
+
{ pattern: /https?:\/\/([^:@\s]+):([^@\s]+)@/g, replacement: 'https://$1:***REDACTED***@' },
|
|
59
|
+
|
|
60
|
+
// Private keys (just detect, don't try to capture whole key)
|
|
61
|
+
{
|
|
62
|
+
pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/g,
|
|
63
|
+
replacement: '***PRIVATE_KEY_START_REDACTED***',
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Generic environment variable patterns (ALL_CAPS_WITH_SECRET_KEYWORD)
|
|
67
|
+
{
|
|
68
|
+
pattern:
|
|
69
|
+
/\b([A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)[A-Z_]*)\s*[:=]\s*["']?([^\s"',}]{8,})["']?/g,
|
|
70
|
+
replacement: '$1=***REDACTED***',
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// API keys and tokens (generic patterns) - lowercase, after env vars
|
|
74
|
+
{
|
|
75
|
+
pattern: /\b(api[_-]?key)\s*[:=]\s*["']?([A-Za-z0-9_-]{16,})["']?/gi,
|
|
76
|
+
replacement: '$1=***REDACTED***',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
pattern: /\b(token)\s*[:=]\s*["']?([A-Za-z0-9_-]{16,})["']?/gi,
|
|
80
|
+
replacement: '$1=***REDACTED***',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
pattern: /\b(secret)\s*[:=]\s*["']?([A-Za-z0-9_-]{16,})["']?/gi,
|
|
84
|
+
replacement: '$1=***REDACTED***',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
pattern: /\b(password)\s*[:=]\s*["']?([^\s"',}]{4,})["']?/gi,
|
|
88
|
+
replacement: '$1=***REDACTED***',
|
|
89
|
+
},
|
|
90
|
+
{ pattern: /\b(passwd)\s*[:=]\s*["']?([^\s"',}]{4,})["']?/gi, replacement: '$1=***REDACTED***' },
|
|
91
|
+
{
|
|
92
|
+
pattern: /\b(credentials?)\s*[:=]\s*["']?([^\s"',}]{4,})["']?/gi,
|
|
93
|
+
replacement: '$1=***REDACTED***',
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// JSON-style "key":"value" patterns
|
|
97
|
+
{
|
|
98
|
+
pattern: /"(api[_-]?key|token|secret|password|passwd|credentials?)"\s*:\s*"([^"]{8,})"/gi,
|
|
99
|
+
replacement: '"$1":"***REDACTED***"',
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sanitize debug output by redacting sensitive information.
|
|
105
|
+
* This function should be applied to all debug output that might contain secrets.
|
|
106
|
+
*
|
|
107
|
+
* @param {string|any} input - String or object to sanitize
|
|
108
|
+
* @returns {{ ok: boolean, sanitized: string, redactionCount: number }}
|
|
109
|
+
*/
|
|
110
|
+
function sanitizeDebugOutput(input) {
|
|
111
|
+
// Handle non-string inputs by stringifying
|
|
112
|
+
let str;
|
|
113
|
+
if (typeof input === 'string') {
|
|
114
|
+
str = input;
|
|
115
|
+
} else if (input === null || input === undefined) {
|
|
116
|
+
return { ok: true, sanitized: String(input), redactionCount: 0 };
|
|
117
|
+
} else {
|
|
118
|
+
try {
|
|
119
|
+
str = JSON.stringify(input);
|
|
120
|
+
} catch {
|
|
121
|
+
str = String(input);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let redactionCount = 0;
|
|
126
|
+
let result = str;
|
|
127
|
+
|
|
128
|
+
for (const { pattern, replacement } of SECRET_PATTERNS) {
|
|
129
|
+
// Reset lastIndex for global patterns
|
|
130
|
+
pattern.lastIndex = 0;
|
|
131
|
+
|
|
132
|
+
// Count matches before replacing
|
|
133
|
+
const matches = result.match(pattern);
|
|
134
|
+
if (matches) {
|
|
135
|
+
redactionCount += matches.length;
|
|
136
|
+
result = result.replace(pattern, replacement);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
sanitized: result,
|
|
143
|
+
redactionCount,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
24
147
|
function debugLog(operation, details) {
|
|
25
148
|
if (DEBUG) {
|
|
26
149
|
const timestamp = new Date().toISOString();
|
|
27
|
-
|
|
150
|
+
// Sanitize details to prevent secret leakage
|
|
151
|
+
const sanitized = sanitizeDebugOutput(details);
|
|
152
|
+
console.error(`[${timestamp}] [errors.js] ${operation}:`, sanitized.sanitized);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Shell metacharacters that could enable command injection.
|
|
158
|
+
* These are dangerous in shell contexts:
|
|
159
|
+
* - ; | & - Command chaining
|
|
160
|
+
* - $ ` - Command substitution
|
|
161
|
+
* - ( ) { } - Subshells and brace expansion
|
|
162
|
+
* - < > - Redirects
|
|
163
|
+
* - \n \r - Newline injection
|
|
164
|
+
* - \ - Escape sequences
|
|
165
|
+
*/
|
|
166
|
+
const SHELL_DANGEROUS_CHARS = /[;|&$`(){}<>\n\r\\]/;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Sanitize a string for safe use in shell commands.
|
|
170
|
+
* Rejects strings containing shell metacharacters.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} str - String to sanitize
|
|
173
|
+
* @param {object} options - Optional settings
|
|
174
|
+
* @param {boolean} options.allowSpaces - Allow spaces in string (default: true)
|
|
175
|
+
* @param {string} options.context - Context description for error messages (default: 'argument')
|
|
176
|
+
* @returns {{ ok: boolean, sanitized?: string, error?: string, detected?: string }}
|
|
177
|
+
*/
|
|
178
|
+
function sanitizeForShell(str, options = {}) {
|
|
179
|
+
const { allowSpaces = true, context = 'argument' } = options;
|
|
180
|
+
|
|
181
|
+
// Must be a string
|
|
182
|
+
if (typeof str !== 'string') {
|
|
183
|
+
return { ok: false, error: `Input must be a string, got ${typeof str}` };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Empty strings are valid
|
|
187
|
+
if (str.length === 0) {
|
|
188
|
+
return { ok: true, sanitized: str };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check for dangerous shell metacharacters
|
|
192
|
+
const match = str.match(SHELL_DANGEROUS_CHARS);
|
|
193
|
+
if (match) {
|
|
194
|
+
const detected = match[0];
|
|
195
|
+
const charName = getCharName(detected);
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
error: `Unsafe characters in ${context}: contains ${charName}`,
|
|
199
|
+
detected,
|
|
200
|
+
};
|
|
28
201
|
}
|
|
202
|
+
|
|
203
|
+
// Optionally disallow spaces
|
|
204
|
+
if (!allowSpaces && /\s/.test(str)) {
|
|
205
|
+
return { ok: false, error: `Spaces not allowed in ${context}` };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { ok: true, sanitized: str };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get human-readable name for a shell character
|
|
213
|
+
*/
|
|
214
|
+
function getCharName(char) {
|
|
215
|
+
const names = {
|
|
216
|
+
';': 'semicolon (command separator)',
|
|
217
|
+
'|': 'pipe (command chaining)',
|
|
218
|
+
'&': 'ampersand (background/chaining)',
|
|
219
|
+
$: 'dollar sign (variable/command substitution)',
|
|
220
|
+
'`': 'backtick (command substitution)',
|
|
221
|
+
'(': 'open parenthesis (subshell)',
|
|
222
|
+
')': 'close parenthesis (subshell)',
|
|
223
|
+
'{': 'open brace (brace expansion)',
|
|
224
|
+
'}': 'close brace (brace expansion)',
|
|
225
|
+
'<': 'less than (input redirect)',
|
|
226
|
+
'>': 'greater than (output redirect)',
|
|
227
|
+
'\n': 'newline (command injection)',
|
|
228
|
+
'\r': 'carriage return (command injection)',
|
|
229
|
+
'\\': 'backslash (escape sequence)',
|
|
230
|
+
};
|
|
231
|
+
return names[char] || `character '${char}'`;
|
|
29
232
|
}
|
|
30
233
|
|
|
31
234
|
/**
|
|
@@ -160,10 +363,20 @@ function safeWriteFile(filePath, content, options = {}) {
|
|
|
160
363
|
* @param {string} options.cwd - Working directory
|
|
161
364
|
* @param {number} options.timeout - Timeout in ms (default: 30000)
|
|
162
365
|
* @param {boolean} options.silent - Suppress stderr (default: false)
|
|
366
|
+
* @param {boolean} options.sanitize - Validate command for shell injection (default: false)
|
|
163
367
|
* @returns {{ ok: boolean, data?: string, error?: string, exitCode?: number }}
|
|
164
368
|
*/
|
|
165
369
|
function safeExec(command, options = {}) {
|
|
166
|
-
const { cwd = process.cwd(), timeout = 30000, silent = false } = options;
|
|
370
|
+
const { cwd = process.cwd(), timeout = 30000, silent = false, sanitize = false } = options;
|
|
371
|
+
|
|
372
|
+
// Optional sanitization check
|
|
373
|
+
if (sanitize) {
|
|
374
|
+
const sanitizeResult = sanitizeForShell(command, { context: 'command' });
|
|
375
|
+
if (!sanitizeResult.ok) {
|
|
376
|
+
debugLog('safeExec', { command: command.slice(0, 50), error: sanitizeResult.error });
|
|
377
|
+
return { ok: false, error: sanitizeResult.error, detected: sanitizeResult.detected };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
167
380
|
|
|
168
381
|
try {
|
|
169
382
|
const output = execSync(command, {
|
|
@@ -245,9 +458,13 @@ function wrapSafe(fn, operationName = 'operation') {
|
|
|
245
458
|
* Wrap an async function to catch errors and return Result
|
|
246
459
|
* @param {Function} fn - Async function to wrap
|
|
247
460
|
* @param {string} operationName - Name for error messages
|
|
248
|
-
* @
|
|
461
|
+
* @param {object} [options] - Optional settings
|
|
462
|
+
* @param {boolean} [options.attachErrorCode=false] - Attach error code metadata
|
|
463
|
+
* @returns {Function} - Wrapped async function returning { ok, data?, error?, errorCode?, suggestedFix? }
|
|
249
464
|
*/
|
|
250
|
-
function wrapSafeAsync(fn, operationName = 'operation') {
|
|
465
|
+
function wrapSafeAsync(fn, operationName = 'operation', options = {}) {
|
|
466
|
+
const { attachErrorCode = false } = options;
|
|
467
|
+
|
|
251
468
|
return async function (...args) {
|
|
252
469
|
try {
|
|
253
470
|
const result = await fn.apply(this, args);
|
|
@@ -255,11 +472,117 @@ function wrapSafeAsync(fn, operationName = 'operation') {
|
|
|
255
472
|
} catch (err) {
|
|
256
473
|
const error = `${operationName} failed: ${err.message}`;
|
|
257
474
|
debugLog('wrapSafeAsync', { operationName, error: err.message });
|
|
258
|
-
|
|
475
|
+
|
|
476
|
+
const result = { ok: false, error };
|
|
477
|
+
|
|
478
|
+
// Optionally attach error code metadata
|
|
479
|
+
if (attachErrorCode) {
|
|
480
|
+
try {
|
|
481
|
+
const { getErrorCodeFromError } = require('./error-codes');
|
|
482
|
+
const codeData = getErrorCodeFromError(err);
|
|
483
|
+
result.errorCode = codeData.code;
|
|
484
|
+
result.severity = codeData.severity;
|
|
485
|
+
result.category = codeData.category;
|
|
486
|
+
result.recoverable = codeData.recoverable;
|
|
487
|
+
result.suggestedFix = codeData.suggestedFix;
|
|
488
|
+
result.autoFix = codeData.autoFix;
|
|
489
|
+
} catch {
|
|
490
|
+
// error-codes not available, skip
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return result;
|
|
259
495
|
}
|
|
260
496
|
};
|
|
261
497
|
}
|
|
262
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Safely parse JSON string with optional field validation
|
|
501
|
+
* @param {string} content - JSON string to parse
|
|
502
|
+
* @param {object} options - Optional settings
|
|
503
|
+
* @param {string[]} options.requiredFields - Array of field names that must exist
|
|
504
|
+
* @param {object} options.schema - Simple schema: { fieldName: 'string' | 'number' | 'boolean' | 'object' | 'array' }
|
|
505
|
+
* @returns {{ ok: boolean, data?: any, error?: string, missingFields?: string[], invalidFields?: string[] }}
|
|
506
|
+
*/
|
|
507
|
+
function safeParseJSON(content, options = {}) {
|
|
508
|
+
const { requiredFields = [], schema = {} } = options;
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
if (typeof content !== 'string') {
|
|
512
|
+
return { ok: false, error: 'Content must be a string' };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const data = JSON.parse(content);
|
|
516
|
+
debugLog('safeParseJSON', { status: 'parsed' });
|
|
517
|
+
|
|
518
|
+
// Validate required fields
|
|
519
|
+
if (requiredFields.length > 0) {
|
|
520
|
+
const missingFields = requiredFields.filter(field => !(field in data));
|
|
521
|
+
if (missingFields.length > 0) {
|
|
522
|
+
const error = `Missing required fields: ${missingFields.join(', ')}`;
|
|
523
|
+
debugLog('safeParseJSON', { error, missingFields });
|
|
524
|
+
return { ok: false, error, missingFields };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Validate field types from schema
|
|
529
|
+
if (Object.keys(schema).length > 0) {
|
|
530
|
+
const invalidFields = [];
|
|
531
|
+
for (const [field, expectedType] of Object.entries(schema)) {
|
|
532
|
+
if (field in data) {
|
|
533
|
+
const actualType = Array.isArray(data[field]) ? 'array' : typeof data[field];
|
|
534
|
+
if (actualType !== expectedType) {
|
|
535
|
+
invalidFields.push(`${field}: expected ${expectedType}, got ${actualType}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (invalidFields.length > 0) {
|
|
540
|
+
const error = `Invalid field types: ${invalidFields.join('; ')}`;
|
|
541
|
+
debugLog('safeParseJSON', { error, invalidFields });
|
|
542
|
+
return { ok: false, error, invalidFields };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return { ok: true, data };
|
|
547
|
+
} catch (err) {
|
|
548
|
+
const error = `JSON parse error: ${err.message}`;
|
|
549
|
+
debugLog('safeParseJSON', { error: err.message });
|
|
550
|
+
return { ok: false, error };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Safely read and parse JSON file with validation
|
|
556
|
+
* @param {string} filePath - Absolute path to JSON file
|
|
557
|
+
* @param {object} options - Optional settings
|
|
558
|
+
* @param {*} options.defaultValue - Value to return if file doesn't exist
|
|
559
|
+
* @param {string[]} options.requiredFields - Array of field names that must exist
|
|
560
|
+
* @param {object} options.schema - Simple schema for type validation
|
|
561
|
+
* @returns {{ ok: boolean, data?: any, error?: string, missingFields?: string[], invalidFields?: string[] }}
|
|
562
|
+
*/
|
|
563
|
+
function safeReadJSONWithValidation(filePath, options = {}) {
|
|
564
|
+
const { defaultValue, requiredFields, schema } = options;
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
if (!fs.existsSync(filePath)) {
|
|
568
|
+
if (defaultValue !== undefined) {
|
|
569
|
+
debugLog('safeReadJSONWithValidation', { filePath, status: 'missing, using default' });
|
|
570
|
+
return { ok: true, data: defaultValue };
|
|
571
|
+
}
|
|
572
|
+
const error = `File not found: ${filePath}`;
|
|
573
|
+
debugLog('safeReadJSONWithValidation', { filePath, error });
|
|
574
|
+
return { ok: false, error };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
578
|
+
return safeParseJSON(content, { requiredFields, schema });
|
|
579
|
+
} catch (err) {
|
|
580
|
+
const error = `Failed to read JSON from ${filePath}: ${err.message}`;
|
|
581
|
+
debugLog('safeReadJSONWithValidation', { filePath, error: err.message });
|
|
582
|
+
return { ok: false, error };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
263
586
|
module.exports = {
|
|
264
587
|
// Core safe operations
|
|
265
588
|
safeReadJSON,
|
|
@@ -270,6 +593,14 @@ module.exports = {
|
|
|
270
593
|
safeExists,
|
|
271
594
|
safeMkdir,
|
|
272
595
|
|
|
596
|
+
// JSON parsing with validation
|
|
597
|
+
safeParseJSON,
|
|
598
|
+
safeReadJSONWithValidation,
|
|
599
|
+
|
|
600
|
+
// Security functions
|
|
601
|
+
sanitizeForShell,
|
|
602
|
+
sanitizeDebugOutput,
|
|
603
|
+
|
|
273
604
|
// Utility wrappers
|
|
274
605
|
wrapSafe,
|
|
275
606
|
wrapSafeAsync,
|