@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/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
+ }
@@ -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
+ }
@@ -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
+ }