@jojonax/codex-copilot 1.5.5 → 1.6.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/LICENSE +21 -21
- package/README.md +144 -44
- package/bin/cli.js +189 -182
- package/package.json +39 -39
- package/src/commands/evolve.js +316 -316
- package/src/commands/fix.js +447 -447
- package/src/commands/init.js +298 -298
- package/src/commands/reset.js +61 -61
- package/src/commands/retry.js +190 -190
- package/src/commands/run.js +958 -958
- package/src/commands/skip.js +62 -62
- package/src/commands/status.js +95 -95
- package/src/commands/usage.js +361 -361
- package/src/utils/automator.js +279 -279
- package/src/utils/checkpoint.js +246 -246
- package/src/utils/detect-prd.js +137 -137
- package/src/utils/git.js +388 -388
- package/src/utils/github.js +486 -486
- package/src/utils/json.js +220 -220
- package/src/utils/logger.js +41 -41
- package/src/utils/prompt.js +49 -49
- package/src/utils/provider.js +770 -769
- package/src/utils/self-heal.js +330 -330
- package/src/utils/shell-bootstrap.js +404 -0
- package/src/utils/update-check.js +103 -103
package/src/utils/json.js
CHANGED
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Safe JSON read/write utilities
|
|
3
|
-
*
|
|
4
|
-
* Provides resilient JSON file operations with:
|
|
5
|
-
* - Auto-repair of common corruption patterns (trailing commas, BOM, control chars)
|
|
6
|
-
* - Truncation recovery via progressive trimming and bracket closure
|
|
7
|
-
* - Atomic writes (temp file + rename) to prevent corruption on crash
|
|
8
|
-
* - Backup creation on auto-repair
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync, writeFileSync, existsSync, renameSync, copyFileSync } from 'fs';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Read and parse a JSON file with auto-repair on corruption.
|
|
15
|
-
* Creates a .bak backup before overwriting with repaired content.
|
|
16
|
-
* @param {string} filePath - Path to JSON file
|
|
17
|
-
* @returns {any} Parsed JSON data
|
|
18
|
-
* @throws {Error} If JSON is unrecoverable
|
|
19
|
-
*/
|
|
20
|
-
export function readJSON(filePath) {
|
|
21
|
-
let raw = readFileSync(filePath, 'utf-8');
|
|
22
|
-
// Strip BOM if present
|
|
23
|
-
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
24
|
-
try {
|
|
25
|
-
return JSON.parse(raw);
|
|
26
|
-
} catch (firstErr) {
|
|
27
|
-
const data = tryRepair(raw);
|
|
28
|
-
if (data !== null) {
|
|
29
|
-
// Save repaired version and backup original
|
|
30
|
-
try {
|
|
31
|
-
copyFileSync(filePath, filePath + '.bak');
|
|
32
|
-
writeJSON(filePath, data);
|
|
33
|
-
} catch { /* best effort */ }
|
|
34
|
-
return data;
|
|
35
|
-
}
|
|
36
|
-
throw new Error(`Invalid JSON in ${filePath}: ${firstErr.message}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Multi-strategy JSON repair pipeline.
|
|
42
|
-
* @param {string} raw - Raw file content
|
|
43
|
-
* @returns {any|null} Parsed data or null if all strategies fail
|
|
44
|
-
*/
|
|
45
|
-
function tryRepair(raw) {
|
|
46
|
-
// Strategy 0: Strip git conflict markers (<<<<<<, ======, >>>>>>)
|
|
47
|
-
// Keep the "ours" side (before =======) and discard "theirs" (after =======)
|
|
48
|
-
let cleaned = raw.replace(
|
|
49
|
-
/^<{4,}[^\n]*\n((?:(?!^={4,})[\s\S])*?)^={4,}[^\n]*\n(?:(?:(?!^>{4,})[\s\S])*?)^>{4,}[^\n]*/gm,
|
|
50
|
-
'$1'
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// Strategy 1: Basic cleanup (trailing commas, control chars)
|
|
54
|
-
cleaned = cleaned
|
|
55
|
-
.replace(/,\s*([}\]])/g, '$1')
|
|
56
|
-
.replace(/[\x00-\x08\x0E-\x1F]/g, '')
|
|
57
|
-
.trim();
|
|
58
|
-
|
|
59
|
-
let result = tryParse(cleaned);
|
|
60
|
-
if (result !== null) return result;
|
|
61
|
-
|
|
62
|
-
// Strategy 2: Truncation recovery — find the last complete } or ] and close
|
|
63
|
-
// Work backwards: try truncating at each } or ] from the end
|
|
64
|
-
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
65
|
-
if (cleaned[i] === '}' || cleaned[i] === ']') {
|
|
66
|
-
let candidate = cleaned.substring(0, i + 1);
|
|
67
|
-
candidate = closeBrackets(candidate);
|
|
68
|
-
result = tryParse(candidate);
|
|
69
|
-
if (result !== null) return result;
|
|
70
|
-
|
|
71
|
-
// Also try stripping garbage prefix (start from first open bracket)
|
|
72
|
-
const firstOpen = findFirstOpen(candidate);
|
|
73
|
-
if (firstOpen > 0) {
|
|
74
|
-
let stripped = candidate.substring(firstOpen);
|
|
75
|
-
stripped = closeBrackets(stripped);
|
|
76
|
-
result = tryParse(stripped);
|
|
77
|
-
if (result !== null) return result;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Strategy 3: Extract {…} or […] from garbage-surrounded content
|
|
83
|
-
const firstOpen = findFirstOpen(cleaned);
|
|
84
|
-
if (firstOpen >= 0) {
|
|
85
|
-
// Find the last } or ] in the string
|
|
86
|
-
const lastClose = Math.max(cleaned.lastIndexOf('}'), cleaned.lastIndexOf(']'));
|
|
87
|
-
if (lastClose > firstOpen) {
|
|
88
|
-
let candidate = cleaned.substring(firstOpen, lastClose + 1);
|
|
89
|
-
candidate = closeBrackets(candidate);
|
|
90
|
-
result = tryParse(candidate);
|
|
91
|
-
if (result !== null) return result;
|
|
92
|
-
}
|
|
93
|
-
// Also try from firstOpen to end with bracket closing
|
|
94
|
-
let candidate = cleaned.substring(firstOpen);
|
|
95
|
-
candidate = closeBrackets(candidate);
|
|
96
|
-
result = tryParse(candidate);
|
|
97
|
-
if (result !== null) return result;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Find the first { or [ in a string
|
|
105
|
-
*/
|
|
106
|
-
function findFirstOpen(s) {
|
|
107
|
-
for (let i = 0; i < s.length; i++) {
|
|
108
|
-
if (s[i] === '{' || s[i] === '[') return i;
|
|
109
|
-
}
|
|
110
|
-
return -1;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Close unmatched brackets/braces in a JSON string.
|
|
115
|
-
* Properly handles strings (doesn't count brackets inside quoted strings).
|
|
116
|
-
*/
|
|
117
|
-
function closeBrackets(s) {
|
|
118
|
-
const stack = [];
|
|
119
|
-
let inString = false;
|
|
120
|
-
let escaped = false;
|
|
121
|
-
|
|
122
|
-
for (let i = 0; i < s.length; i++) {
|
|
123
|
-
const ch = s[i];
|
|
124
|
-
if (escaped) { escaped = false; continue; }
|
|
125
|
-
if (ch === '\\' && inString) { escaped = true; continue; }
|
|
126
|
-
if (ch === '"') { inString = !inString; continue; }
|
|
127
|
-
if (inString) continue;
|
|
128
|
-
if (ch === '{') stack.push('}');
|
|
129
|
-
else if (ch === '[') stack.push(']');
|
|
130
|
-
else if (ch === '}' || ch === ']') {
|
|
131
|
-
if (stack.length > 0) stack.pop();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Append closers in reverse order
|
|
136
|
-
while (stack.length > 0) s += stack.pop();
|
|
137
|
-
return s;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Try to parse JSON, return data or null.
|
|
142
|
-
*/
|
|
143
|
-
function tryParse(s) {
|
|
144
|
-
try {
|
|
145
|
-
return JSON.parse(s);
|
|
146
|
-
} catch {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Write JSON data to file atomically (temp file + rename).
|
|
153
|
-
* Prevents corruption if process is killed mid-write.
|
|
154
|
-
* @param {string} filePath - Path to JSON file
|
|
155
|
-
* @param {any} data - Data to serialize
|
|
156
|
-
*/
|
|
157
|
-
export function writeJSON(filePath, data) {
|
|
158
|
-
const tempPath = filePath + '.tmp';
|
|
159
|
-
const content = JSON.stringify(data, null, 2);
|
|
160
|
-
|
|
161
|
-
// B4: Disk space protection — catch ENOSPC/EDQUOT errors with clear message
|
|
162
|
-
try {
|
|
163
|
-
writeFileSync(tempPath, content);
|
|
164
|
-
} catch (writeErr) {
|
|
165
|
-
if (writeErr.code === 'ENOSPC' || writeErr.code === 'EDQUOT') {
|
|
166
|
-
throw new Error(`Disk full — cannot write ${filePath}. Free up space and retry.`);
|
|
167
|
-
}
|
|
168
|
-
if (writeErr.code === 'EACCES' || writeErr.code === 'EPERM') {
|
|
169
|
-
throw new Error(`Permission denied — cannot write ${filePath}. Check file/directory permissions.`);
|
|
170
|
-
}
|
|
171
|
-
throw writeErr;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
renameSync(tempPath, filePath);
|
|
175
|
-
|
|
176
|
-
// Write-back verification: ensure what we wrote is valid and readable
|
|
177
|
-
try {
|
|
178
|
-
const readBack = readFileSync(filePath, 'utf-8');
|
|
179
|
-
JSON.parse(readBack);
|
|
180
|
-
} catch (verifyErr) {
|
|
181
|
-
// Write succeeded but verification failed — attempt direct overwrite
|
|
182
|
-
writeFileSync(filePath, content);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Deep repair of a severely corrupted JSON file.
|
|
188
|
-
* Tries readJSON's repair pipeline, then falls back to .bak file.
|
|
189
|
-
* Used by the fix command when readJSON fails.
|
|
190
|
-
* @param {string} filePath - Path to corrupted JSON file
|
|
191
|
-
* @returns {any} Parsed JSON data
|
|
192
|
-
* @throws {Error} If all repair strategies fail
|
|
193
|
-
*/
|
|
194
|
-
export function repairRawJSON(filePath) {
|
|
195
|
-
let raw = readFileSync(filePath, 'utf-8');
|
|
196
|
-
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
197
|
-
|
|
198
|
-
// Try the full repair pipeline
|
|
199
|
-
const data = tryRepair(raw);
|
|
200
|
-
if (data !== null) {
|
|
201
|
-
try {
|
|
202
|
-
copyFileSync(filePath, filePath + '.bak');
|
|
203
|
-
writeJSON(filePath, data);
|
|
204
|
-
} catch { /* best effort */ }
|
|
205
|
-
return data;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Try to recover from .bak file
|
|
209
|
-
const bakPath = filePath + '.bak';
|
|
210
|
-
if (existsSync(bakPath)) {
|
|
211
|
-
try {
|
|
212
|
-
const bakRaw = readFileSync(bakPath, 'utf-8');
|
|
213
|
-
const bakData = tryRepair(bakRaw) ?? JSON.parse(bakRaw);
|
|
214
|
-
writeJSON(filePath, bakData);
|
|
215
|
-
return bakData;
|
|
216
|
-
} catch { /* backup also corrupt */ }
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
throw new Error(`All repair strategies failed for ${filePath}`);
|
|
220
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Safe JSON read/write utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides resilient JSON file operations with:
|
|
5
|
+
* - Auto-repair of common corruption patterns (trailing commas, BOM, control chars)
|
|
6
|
+
* - Truncation recovery via progressive trimming and bracket closure
|
|
7
|
+
* - Atomic writes (temp file + rename) to prevent corruption on crash
|
|
8
|
+
* - Backup creation on auto-repair
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, copyFileSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read and parse a JSON file with auto-repair on corruption.
|
|
15
|
+
* Creates a .bak backup before overwriting with repaired content.
|
|
16
|
+
* @param {string} filePath - Path to JSON file
|
|
17
|
+
* @returns {any} Parsed JSON data
|
|
18
|
+
* @throws {Error} If JSON is unrecoverable
|
|
19
|
+
*/
|
|
20
|
+
export function readJSON(filePath) {
|
|
21
|
+
let raw = readFileSync(filePath, 'utf-8');
|
|
22
|
+
// Strip BOM if present
|
|
23
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
} catch (firstErr) {
|
|
27
|
+
const data = tryRepair(raw);
|
|
28
|
+
if (data !== null) {
|
|
29
|
+
// Save repaired version and backup original
|
|
30
|
+
try {
|
|
31
|
+
copyFileSync(filePath, filePath + '.bak');
|
|
32
|
+
writeJSON(filePath, data);
|
|
33
|
+
} catch { /* best effort */ }
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Invalid JSON in ${filePath}: ${firstErr.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Multi-strategy JSON repair pipeline.
|
|
42
|
+
* @param {string} raw - Raw file content
|
|
43
|
+
* @returns {any|null} Parsed data or null if all strategies fail
|
|
44
|
+
*/
|
|
45
|
+
function tryRepair(raw) {
|
|
46
|
+
// Strategy 0: Strip git conflict markers (<<<<<<, ======, >>>>>>)
|
|
47
|
+
// Keep the "ours" side (before =======) and discard "theirs" (after =======)
|
|
48
|
+
let cleaned = raw.replace(
|
|
49
|
+
/^<{4,}[^\n]*\n((?:(?!^={4,})[\s\S])*?)^={4,}[^\n]*\n(?:(?:(?!^>{4,})[\s\S])*?)^>{4,}[^\n]*/gm,
|
|
50
|
+
'$1'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Strategy 1: Basic cleanup (trailing commas, control chars)
|
|
54
|
+
cleaned = cleaned
|
|
55
|
+
.replace(/,\s*([}\]])/g, '$1')
|
|
56
|
+
.replace(/[\x00-\x08\x0E-\x1F]/g, '')
|
|
57
|
+
.trim();
|
|
58
|
+
|
|
59
|
+
let result = tryParse(cleaned);
|
|
60
|
+
if (result !== null) return result;
|
|
61
|
+
|
|
62
|
+
// Strategy 2: Truncation recovery — find the last complete } or ] and close
|
|
63
|
+
// Work backwards: try truncating at each } or ] from the end
|
|
64
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
65
|
+
if (cleaned[i] === '}' || cleaned[i] === ']') {
|
|
66
|
+
let candidate = cleaned.substring(0, i + 1);
|
|
67
|
+
candidate = closeBrackets(candidate);
|
|
68
|
+
result = tryParse(candidate);
|
|
69
|
+
if (result !== null) return result;
|
|
70
|
+
|
|
71
|
+
// Also try stripping garbage prefix (start from first open bracket)
|
|
72
|
+
const firstOpen = findFirstOpen(candidate);
|
|
73
|
+
if (firstOpen > 0) {
|
|
74
|
+
let stripped = candidate.substring(firstOpen);
|
|
75
|
+
stripped = closeBrackets(stripped);
|
|
76
|
+
result = tryParse(stripped);
|
|
77
|
+
if (result !== null) return result;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Strategy 3: Extract {…} or […] from garbage-surrounded content
|
|
83
|
+
const firstOpen = findFirstOpen(cleaned);
|
|
84
|
+
if (firstOpen >= 0) {
|
|
85
|
+
// Find the last } or ] in the string
|
|
86
|
+
const lastClose = Math.max(cleaned.lastIndexOf('}'), cleaned.lastIndexOf(']'));
|
|
87
|
+
if (lastClose > firstOpen) {
|
|
88
|
+
let candidate = cleaned.substring(firstOpen, lastClose + 1);
|
|
89
|
+
candidate = closeBrackets(candidate);
|
|
90
|
+
result = tryParse(candidate);
|
|
91
|
+
if (result !== null) return result;
|
|
92
|
+
}
|
|
93
|
+
// Also try from firstOpen to end with bracket closing
|
|
94
|
+
let candidate = cleaned.substring(firstOpen);
|
|
95
|
+
candidate = closeBrackets(candidate);
|
|
96
|
+
result = tryParse(candidate);
|
|
97
|
+
if (result !== null) return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find the first { or [ in a string
|
|
105
|
+
*/
|
|
106
|
+
function findFirstOpen(s) {
|
|
107
|
+
for (let i = 0; i < s.length; i++) {
|
|
108
|
+
if (s[i] === '{' || s[i] === '[') return i;
|
|
109
|
+
}
|
|
110
|
+
return -1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Close unmatched brackets/braces in a JSON string.
|
|
115
|
+
* Properly handles strings (doesn't count brackets inside quoted strings).
|
|
116
|
+
*/
|
|
117
|
+
function closeBrackets(s) {
|
|
118
|
+
const stack = [];
|
|
119
|
+
let inString = false;
|
|
120
|
+
let escaped = false;
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < s.length; i++) {
|
|
123
|
+
const ch = s[i];
|
|
124
|
+
if (escaped) { escaped = false; continue; }
|
|
125
|
+
if (ch === '\\' && inString) { escaped = true; continue; }
|
|
126
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
127
|
+
if (inString) continue;
|
|
128
|
+
if (ch === '{') stack.push('}');
|
|
129
|
+
else if (ch === '[') stack.push(']');
|
|
130
|
+
else if (ch === '}' || ch === ']') {
|
|
131
|
+
if (stack.length > 0) stack.pop();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Append closers in reverse order
|
|
136
|
+
while (stack.length > 0) s += stack.pop();
|
|
137
|
+
return s;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Try to parse JSON, return data or null.
|
|
142
|
+
*/
|
|
143
|
+
function tryParse(s) {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(s);
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Write JSON data to file atomically (temp file + rename).
|
|
153
|
+
* Prevents corruption if process is killed mid-write.
|
|
154
|
+
* @param {string} filePath - Path to JSON file
|
|
155
|
+
* @param {any} data - Data to serialize
|
|
156
|
+
*/
|
|
157
|
+
export function writeJSON(filePath, data) {
|
|
158
|
+
const tempPath = filePath + '.tmp';
|
|
159
|
+
const content = JSON.stringify(data, null, 2);
|
|
160
|
+
|
|
161
|
+
// B4: Disk space protection — catch ENOSPC/EDQUOT errors with clear message
|
|
162
|
+
try {
|
|
163
|
+
writeFileSync(tempPath, content);
|
|
164
|
+
} catch (writeErr) {
|
|
165
|
+
if (writeErr.code === 'ENOSPC' || writeErr.code === 'EDQUOT') {
|
|
166
|
+
throw new Error(`Disk full — cannot write ${filePath}. Free up space and retry.`);
|
|
167
|
+
}
|
|
168
|
+
if (writeErr.code === 'EACCES' || writeErr.code === 'EPERM') {
|
|
169
|
+
throw new Error(`Permission denied — cannot write ${filePath}. Check file/directory permissions.`);
|
|
170
|
+
}
|
|
171
|
+
throw writeErr;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
renameSync(tempPath, filePath);
|
|
175
|
+
|
|
176
|
+
// Write-back verification: ensure what we wrote is valid and readable
|
|
177
|
+
try {
|
|
178
|
+
const readBack = readFileSync(filePath, 'utf-8');
|
|
179
|
+
JSON.parse(readBack);
|
|
180
|
+
} catch (verifyErr) {
|
|
181
|
+
// Write succeeded but verification failed — attempt direct overwrite
|
|
182
|
+
writeFileSync(filePath, content);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Deep repair of a severely corrupted JSON file.
|
|
188
|
+
* Tries readJSON's repair pipeline, then falls back to .bak file.
|
|
189
|
+
* Used by the fix command when readJSON fails.
|
|
190
|
+
* @param {string} filePath - Path to corrupted JSON file
|
|
191
|
+
* @returns {any} Parsed JSON data
|
|
192
|
+
* @throws {Error} If all repair strategies fail
|
|
193
|
+
*/
|
|
194
|
+
export function repairRawJSON(filePath) {
|
|
195
|
+
let raw = readFileSync(filePath, 'utf-8');
|
|
196
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
197
|
+
|
|
198
|
+
// Try the full repair pipeline
|
|
199
|
+
const data = tryRepair(raw);
|
|
200
|
+
if (data !== null) {
|
|
201
|
+
try {
|
|
202
|
+
copyFileSync(filePath, filePath + '.bak');
|
|
203
|
+
writeJSON(filePath, data);
|
|
204
|
+
} catch { /* best effort */ }
|
|
205
|
+
return data;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Try to recover from .bak file
|
|
209
|
+
const bakPath = filePath + '.bak';
|
|
210
|
+
if (existsSync(bakPath)) {
|
|
211
|
+
try {
|
|
212
|
+
const bakRaw = readFileSync(bakPath, 'utf-8');
|
|
213
|
+
const bakData = tryRepair(bakRaw) ?? JSON.parse(bakRaw);
|
|
214
|
+
writeJSON(filePath, bakData);
|
|
215
|
+
return bakData;
|
|
216
|
+
} catch { /* backup also corrupt */ }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new Error(`All repair strategies failed for ${filePath}`);
|
|
220
|
+
}
|
package/src/utils/logger.js
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logger utility - Unified terminal output styles
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const COLORS = {
|
|
6
|
-
green: '\x1b[32m',
|
|
7
|
-
yellow: '\x1b[33m',
|
|
8
|
-
red: '\x1b[31m',
|
|
9
|
-
cyan: '\x1b[36m',
|
|
10
|
-
dim: '\x1b[2m',
|
|
11
|
-
bold: '\x1b[1m',
|
|
12
|
-
reset: '\x1b[0m',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const log = {
|
|
16
|
-
info: (msg) => console.log(` ${COLORS.green}✔${COLORS.reset} ${msg}`),
|
|
17
|
-
warn: (msg) => console.log(` ${COLORS.yellow}⚠${COLORS.reset} ${msg}`),
|
|
18
|
-
error: (msg) => console.log(` ${COLORS.red}✗${COLORS.reset} ${msg}`),
|
|
19
|
-
step: (msg) => console.log(` ${COLORS.cyan}▶${COLORS.reset} ${msg}`),
|
|
20
|
-
dim: (msg) => console.log(` ${COLORS.dim}${msg}${COLORS.reset}`),
|
|
21
|
-
title: (msg) => console.log(` ${COLORS.bold}${msg}${COLORS.reset}`),
|
|
22
|
-
blank: () => console.log(''),
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Display a progress bar
|
|
27
|
-
*/
|
|
28
|
-
export function progressBar(current, total, label = '') {
|
|
29
|
-
const width = 30;
|
|
30
|
-
if (total <= 0) {
|
|
31
|
-
const bar = '░'.repeat(width);
|
|
32
|
-
console.log(` ${COLORS.cyan}[${bar}]${COLORS.reset} 0% ${label}`);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const rawRatio = current / total;
|
|
36
|
-
const ratio = Number.isNaN(rawRatio) ? 0 : Math.max(0, Math.min(1, rawRatio));
|
|
37
|
-
const filled = Math.round(ratio * width);
|
|
38
|
-
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
39
|
-
const pct = Math.round(ratio * 100);
|
|
40
|
-
console.log(` ${COLORS.cyan}[${bar}]${COLORS.reset} ${pct}% ${label}`);
|
|
41
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility - Unified terminal output styles
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const COLORS = {
|
|
6
|
+
green: '\x1b[32m',
|
|
7
|
+
yellow: '\x1b[33m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
11
|
+
bold: '\x1b[1m',
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const log = {
|
|
16
|
+
info: (msg) => console.log(` ${COLORS.green}✔${COLORS.reset} ${msg}`),
|
|
17
|
+
warn: (msg) => console.log(` ${COLORS.yellow}⚠${COLORS.reset} ${msg}`),
|
|
18
|
+
error: (msg) => console.log(` ${COLORS.red}✗${COLORS.reset} ${msg}`),
|
|
19
|
+
step: (msg) => console.log(` ${COLORS.cyan}▶${COLORS.reset} ${msg}`),
|
|
20
|
+
dim: (msg) => console.log(` ${COLORS.dim}${msg}${COLORS.reset}`),
|
|
21
|
+
title: (msg) => console.log(` ${COLORS.bold}${msg}${COLORS.reset}`),
|
|
22
|
+
blank: () => console.log(''),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Display a progress bar
|
|
27
|
+
*/
|
|
28
|
+
export function progressBar(current, total, label = '') {
|
|
29
|
+
const width = 30;
|
|
30
|
+
if (total <= 0) {
|
|
31
|
+
const bar = '░'.repeat(width);
|
|
32
|
+
console.log(` ${COLORS.cyan}[${bar}]${COLORS.reset} 0% ${label}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const rawRatio = current / total;
|
|
36
|
+
const ratio = Number.isNaN(rawRatio) ? 0 : Math.max(0, Math.min(1, rawRatio));
|
|
37
|
+
const filled = Math.round(ratio * width);
|
|
38
|
+
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
39
|
+
const pct = Math.round(ratio * 100);
|
|
40
|
+
console.log(` ${COLORS.cyan}[${bar}]${COLORS.reset} ${pct}% ${label}`);
|
|
41
|
+
}
|
package/src/utils/prompt.js
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive terminal utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createInterface } from 'readline';
|
|
6
|
-
|
|
7
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Ask a question and wait for user input
|
|
11
|
-
*/
|
|
12
|
-
export function ask(question) {
|
|
13
|
-
return new Promise((resolve) => {
|
|
14
|
-
rl.question(` ${question} `, (answer) => {
|
|
15
|
-
resolve(answer.trim());
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Yes/No confirmation
|
|
22
|
-
*/
|
|
23
|
-
export async function confirm(question, defaultYes = true) {
|
|
24
|
-
const hint = defaultYes ? '(Y/n)' : '(y/N)';
|
|
25
|
-
const answer = await ask(`${question} ${hint}:`);
|
|
26
|
-
if (!answer) return defaultYes;
|
|
27
|
-
return answer.toLowerCase().startsWith('y');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Select from a list
|
|
32
|
-
*/
|
|
33
|
-
export async function select(question, choices) {
|
|
34
|
-
console.log(` ${question}`);
|
|
35
|
-
for (let i = 0; i < choices.length; i++) {
|
|
36
|
-
console.log(` ${i + 1}. ${choices[i].label}`);
|
|
37
|
-
}
|
|
38
|
-
const answer = await ask('Enter number:');
|
|
39
|
-
const idx = parseInt(answer) - 1;
|
|
40
|
-
if (idx >= 0 && idx < choices.length) return choices[idx];
|
|
41
|
-
return choices[0]; // Default to first
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Close readline interface
|
|
46
|
-
*/
|
|
47
|
-
export function closePrompt() {
|
|
48
|
-
rl.close();
|
|
49
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Interactive terminal utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ask a question and wait for user input
|
|
11
|
+
*/
|
|
12
|
+
export function ask(question) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(` ${question} `, (answer) => {
|
|
15
|
+
resolve(answer.trim());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Yes/No confirmation
|
|
22
|
+
*/
|
|
23
|
+
export async function confirm(question, defaultYes = true) {
|
|
24
|
+
const hint = defaultYes ? '(Y/n)' : '(y/N)';
|
|
25
|
+
const answer = await ask(`${question} ${hint}:`);
|
|
26
|
+
if (!answer) return defaultYes;
|
|
27
|
+
return answer.toLowerCase().startsWith('y');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Select from a list
|
|
32
|
+
*/
|
|
33
|
+
export async function select(question, choices) {
|
|
34
|
+
console.log(` ${question}`);
|
|
35
|
+
for (let i = 0; i < choices.length; i++) {
|
|
36
|
+
console.log(` ${i + 1}. ${choices[i].label}`);
|
|
37
|
+
}
|
|
38
|
+
const answer = await ask('Enter number:');
|
|
39
|
+
const idx = parseInt(answer) - 1;
|
|
40
|
+
if (idx >= 0 && idx < choices.length) return choices[idx];
|
|
41
|
+
return choices[0]; // Default to first
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Close readline interface
|
|
46
|
+
*/
|
|
47
|
+
export function closePrompt() {
|
|
48
|
+
rl.close();
|
|
49
|
+
}
|