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.
- package/CHANGELOG.md +10 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/smart-json-file.js +228 -1
- package/lib/table-formatter.js +519 -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 +37 -737
- package/package.json +3 -1
- package/scripts/check-update.js +17 -3
- package/scripts/lib/sessionRegistry.js +678 -0
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +151 -0
- package/scripts/tui/index.js +31 -0
- package/scripts/tui/lib/crashRecovery.js +304 -0
- package/scripts/tui/lib/eventStream.js +309 -0
- package/scripts/tui/lib/keyboard.js +261 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +242 -0
- package/scripts/tui/panels/SessionPanel.js +170 -0
- package/scripts/tui/panels/TracePanel.js +298 -0
- package/scripts/tui/simple-tui.js +390 -0
- package/tools/cli/commands/config.js +7 -31
- package/tools/cli/commands/doctor.js +28 -39
- package/tools/cli/commands/list.js +47 -35
- package/tools/cli/commands/status.js +20 -38
- package/tools/cli/commands/tui.js +59 -0
- package/tools/cli/commands/uninstall.js +12 -39
- package/tools/cli/installers/core/installer.js +13 -0
- package/tools/cli/lib/command-context.js +382 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +17 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
package/lib/smart-json-file.js
CHANGED
|
@@ -42,6 +42,101 @@ function debugLog(operation, details) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Security: Secure file permission mode for sensitive config files
|
|
46
|
+
// 0o600 = owner read/write only (no group or world access)
|
|
47
|
+
const SECURE_FILE_MODE = 0o600;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if file permissions are too permissive (security risk)
|
|
51
|
+
* @param {number} mode - File mode (from fs.statSync)
|
|
52
|
+
* @returns {{ok: boolean, warning?: string}} Check result
|
|
53
|
+
*/
|
|
54
|
+
function checkFilePermissions(mode) {
|
|
55
|
+
// Skip permission checks on Windows (different permission model)
|
|
56
|
+
if (process.platform === 'win32') {
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Extract permission bits (last 9 bits)
|
|
61
|
+
const permissions = mode & 0o777;
|
|
62
|
+
|
|
63
|
+
// Check for group/world readable/writable (security risk)
|
|
64
|
+
const groupRead = permissions & 0o040;
|
|
65
|
+
const groupWrite = permissions & 0o020;
|
|
66
|
+
const worldRead = permissions & 0o004;
|
|
67
|
+
const worldWrite = permissions & 0o002;
|
|
68
|
+
|
|
69
|
+
if (worldWrite) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
warning:
|
|
73
|
+
'File is world-writable (mode: ' +
|
|
74
|
+
permissions.toString(8) +
|
|
75
|
+
'). Security risk - others can modify.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (worldRead) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
warning:
|
|
83
|
+
'File is world-readable (mode: ' +
|
|
84
|
+
permissions.toString(8) +
|
|
85
|
+
'). May expose sensitive config.',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (groupWrite) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
warning:
|
|
93
|
+
'File is group-writable (mode: ' +
|
|
94
|
+
permissions.toString(8) +
|
|
95
|
+
'). Consider restricting to 0600.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (groupRead) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
warning:
|
|
103
|
+
'File is group-readable (mode: ' +
|
|
104
|
+
permissions.toString(8) +
|
|
105
|
+
'). Consider restricting to 0600.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { ok: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set secure permissions on a file (0o600 - owner only)
|
|
114
|
+
* @param {string} filePath - Path to the file
|
|
115
|
+
* @returns {{ok: boolean, error?: Error}}
|
|
116
|
+
*/
|
|
117
|
+
function setSecurePermissions(filePath) {
|
|
118
|
+
// Skip on Windows
|
|
119
|
+
if (process.platform === 'win32') {
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
fs.chmodSync(filePath, SECURE_FILE_MODE);
|
|
125
|
+
debugLog('setSecurePermissions', { filePath, mode: SECURE_FILE_MODE.toString(8) });
|
|
126
|
+
return { ok: true };
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const error = createTypedError(
|
|
129
|
+
`Failed to set secure permissions on ${filePath}: ${err.message}`,
|
|
130
|
+
'EPERM',
|
|
131
|
+
{
|
|
132
|
+
cause: err,
|
|
133
|
+
context: { filePath, mode: SECURE_FILE_MODE },
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
return { ok: false, error };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
45
140
|
/**
|
|
46
141
|
* Generate a unique temporary file path
|
|
47
142
|
* @param {string} filePath - Original file path
|
|
@@ -78,6 +173,8 @@ class SmartJsonFile {
|
|
|
78
173
|
* @param {number} [options.spaces=2] - JSON indentation spaces
|
|
79
174
|
* @param {Function} [options.schema] - Optional validation function (throws on invalid)
|
|
80
175
|
* @param {*} [options.defaultValue] - Default value if file doesn't exist
|
|
176
|
+
* @param {boolean} [options.secureMode=false] - Enforce 0o600 permissions on write
|
|
177
|
+
* @param {boolean} [options.warnInsecure=false] - Warn if file has insecure permissions on read
|
|
81
178
|
*/
|
|
82
179
|
constructor(filePath, options = {}) {
|
|
83
180
|
if (!filePath || typeof filePath !== 'string') {
|
|
@@ -99,10 +196,12 @@ class SmartJsonFile {
|
|
|
99
196
|
this.spaces = options.spaces ?? 2;
|
|
100
197
|
this.schema = options.schema ?? null;
|
|
101
198
|
this.defaultValue = options.defaultValue;
|
|
199
|
+
this.secureMode = options.secureMode ?? false;
|
|
200
|
+
this.warnInsecure = options.warnInsecure ?? false;
|
|
102
201
|
|
|
103
202
|
debugLog('constructor', {
|
|
104
203
|
filePath,
|
|
105
|
-
options: { retries: this.retries, backoff: this.backoff },
|
|
204
|
+
options: { retries: this.retries, backoff: this.backoff, secureMode: this.secureMode },
|
|
106
205
|
});
|
|
107
206
|
}
|
|
108
207
|
|
|
@@ -133,6 +232,21 @@ class SmartJsonFile {
|
|
|
133
232
|
// Read file
|
|
134
233
|
const content = fs.readFileSync(this.filePath, 'utf8');
|
|
135
234
|
|
|
235
|
+
// Security: Check file permissions if warnInsecure is enabled
|
|
236
|
+
if (this.warnInsecure) {
|
|
237
|
+
try {
|
|
238
|
+
const stats = fs.statSync(this.filePath);
|
|
239
|
+
const permCheck = checkFilePermissions(stats.mode);
|
|
240
|
+
if (!permCheck.ok) {
|
|
241
|
+
debugLog('read', { security: 'insecure permissions', warning: permCheck.warning });
|
|
242
|
+
// Log warning to stderr (non-blocking)
|
|
243
|
+
console.error(`[Security Warning] ${this.filePath}: ${permCheck.warning}`);
|
|
244
|
+
}
|
|
245
|
+
} catch (statErr) {
|
|
246
|
+
debugLog('read', { security: 'could not check permissions', error: statErr.message });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
136
250
|
// Parse JSON
|
|
137
251
|
let data;
|
|
138
252
|
try {
|
|
@@ -242,6 +356,16 @@ class SmartJsonFile {
|
|
|
242
356
|
|
|
243
357
|
// Atomic rename
|
|
244
358
|
fs.renameSync(tempPath, this.filePath);
|
|
359
|
+
|
|
360
|
+
// Security: Set secure permissions if secureMode is enabled
|
|
361
|
+
if (this.secureMode) {
|
|
362
|
+
const permResult = setSecurePermissions(this.filePath);
|
|
363
|
+
if (!permResult.ok) {
|
|
364
|
+
debugLog('write', { status: 'warning', security: 'failed to set secure permissions' });
|
|
365
|
+
// Don't fail the write, just warn
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
245
369
|
debugLog('write', { status: 'success' });
|
|
246
370
|
|
|
247
371
|
return { ok: true };
|
|
@@ -424,6 +548,17 @@ class SmartJsonFile {
|
|
|
424
548
|
fs.writeFileSync(tempPath, content, 'utf8');
|
|
425
549
|
fs.renameSync(tempPath, this.filePath);
|
|
426
550
|
|
|
551
|
+
// Security: Set secure permissions if secureMode is enabled
|
|
552
|
+
if (this.secureMode) {
|
|
553
|
+
const permResult = setSecurePermissions(this.filePath);
|
|
554
|
+
if (!permResult.ok) {
|
|
555
|
+
debugLog('writeSync', {
|
|
556
|
+
status: 'warning',
|
|
557
|
+
security: 'failed to set secure permissions',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
427
562
|
return { ok: true };
|
|
428
563
|
} catch (err) {
|
|
429
564
|
// Clean up temp file
|
|
@@ -446,4 +581,96 @@ class SmartJsonFile {
|
|
|
446
581
|
}
|
|
447
582
|
}
|
|
448
583
|
|
|
584
|
+
// Default max age for temp files (24 hours in milliseconds)
|
|
585
|
+
const DEFAULT_TEMP_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Clean up orphaned temp files in a directory
|
|
589
|
+
*
|
|
590
|
+
* Removes files matching the temp file pattern that are older than maxAge.
|
|
591
|
+
* Pattern: .{basename}.{timestamp}.{random}.{ext}.tmp
|
|
592
|
+
*
|
|
593
|
+
* @param {string} directory - Directory to clean
|
|
594
|
+
* @param {Object} [options={}] - Cleanup options
|
|
595
|
+
* @param {number} [options.maxAgeMs=86400000] - Max age in ms (default: 24 hours)
|
|
596
|
+
* @param {boolean} [options.dryRun=false] - Don't delete, just report
|
|
597
|
+
* @returns {{ok: boolean, cleaned: string[], errors: string[]}}
|
|
598
|
+
*/
|
|
599
|
+
function cleanupTempFiles(directory, options = {}) {
|
|
600
|
+
const { maxAgeMs = DEFAULT_TEMP_MAX_AGE_MS, dryRun = false } = options;
|
|
601
|
+
|
|
602
|
+
const cleaned = [];
|
|
603
|
+
const errors = [];
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
if (!fs.existsSync(directory)) {
|
|
607
|
+
return { ok: true, cleaned, errors };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const now = Date.now();
|
|
611
|
+
const entries = fs.readdirSync(directory);
|
|
612
|
+
|
|
613
|
+
// Pattern: .{basename}.{timestamp}.{random}.{ext}.tmp
|
|
614
|
+
const tempFilePattern = /^\.[^.]+\.\d+\.[a-z0-9]+\.[^.]+\.tmp$/;
|
|
615
|
+
|
|
616
|
+
for (const entry of entries) {
|
|
617
|
+
// Check if it matches temp file pattern
|
|
618
|
+
if (!tempFilePattern.test(entry)) continue;
|
|
619
|
+
|
|
620
|
+
const filePath = path.join(directory, entry);
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const stat = fs.statSync(filePath);
|
|
624
|
+
|
|
625
|
+
// Skip if not a file
|
|
626
|
+
if (!stat.isFile()) continue;
|
|
627
|
+
|
|
628
|
+
// Check age
|
|
629
|
+
const age = now - stat.mtimeMs;
|
|
630
|
+
if (age < maxAgeMs) continue;
|
|
631
|
+
|
|
632
|
+
// Delete the temp file
|
|
633
|
+
if (!dryRun) {
|
|
634
|
+
fs.unlinkSync(filePath);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
cleaned.push(filePath);
|
|
638
|
+
debugLog('cleanupTempFiles', {
|
|
639
|
+
action: dryRun ? 'would delete' : 'deleted',
|
|
640
|
+
filePath,
|
|
641
|
+
ageHours: Math.round(age / 3600000),
|
|
642
|
+
});
|
|
643
|
+
} catch (err) {
|
|
644
|
+
errors.push(`${filePath}: ${err.message}`);
|
|
645
|
+
debugLog('cleanupTempFiles', { action: 'error', filePath, error: err.message });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return { ok: errors.length === 0, cleaned, errors };
|
|
650
|
+
} catch (err) {
|
|
651
|
+
errors.push(`Directory read error: ${err.message}`);
|
|
652
|
+
return { ok: false, cleaned, errors };
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Clean up temp files in the directory of a specific JSON file
|
|
658
|
+
*
|
|
659
|
+
* @param {string} filePath - Path to the JSON file
|
|
660
|
+
* @param {Object} [options={}] - Cleanup options
|
|
661
|
+
* @returns {{ok: boolean, cleaned: string[], errors: string[]}}
|
|
662
|
+
*/
|
|
663
|
+
function cleanupTempFilesFor(filePath, options = {}) {
|
|
664
|
+
const directory = path.dirname(filePath);
|
|
665
|
+
return cleanupTempFiles(directory, options);
|
|
666
|
+
}
|
|
667
|
+
|
|
449
668
|
module.exports = SmartJsonFile;
|
|
669
|
+
|
|
670
|
+
// Export helper functions for external use
|
|
671
|
+
module.exports.SECURE_FILE_MODE = SECURE_FILE_MODE;
|
|
672
|
+
module.exports.checkFilePermissions = checkFilePermissions;
|
|
673
|
+
module.exports.setSecurePermissions = setSecurePermissions;
|
|
674
|
+
module.exports.cleanupTempFiles = cleanupTempFiles;
|
|
675
|
+
module.exports.cleanupTempFilesFor = cleanupTempFilesFor;
|
|
676
|
+
module.exports.DEFAULT_TEMP_MAX_AGE_MS = DEFAULT_TEMP_MAX_AGE_MS;
|