deflake 1.2.21 β 1.2.25
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/README.md +6 -3
- package/cli.js +98 -87
- package/client.js +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,13 +22,16 @@ npx deflake npx playwright test
|
|
|
22
22
|
npx deflake npx cypress run
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
### π Diagnostics (Doctor Mode)
|
|
26
|
-
Validate your environment, API key, and quota without running tests:
|
|
27
|
-
|
|
28
25
|
```bash
|
|
29
26
|
npx deflake doctor
|
|
30
27
|
```
|
|
31
28
|
|
|
29
|
+
### π macOS Permission Requirements
|
|
30
|
+
On macOS, if your project folder is located in **Documents** or **Desktop**, DeFlake may be blocked from reading test results.
|
|
31
|
+
|
|
32
|
+
* **Move Project**: Move your folder out of Documents (e.g., to `/Users/hugo/code/`).
|
|
33
|
+
* **Full Disk Access**: Grant "Full Disk Access" to your Terminal/iTerm in *System Settings > Privacy & Security*.
|
|
34
|
+
|
|
32
35
|
### Manual Mode
|
|
33
36
|
Analyze an existing error log and HTML/Image snapshot:
|
|
34
37
|
|
package/cli.js
CHANGED
|
@@ -18,8 +18,10 @@ const C = {
|
|
|
18
18
|
BLUE: "\x1b[34m",
|
|
19
19
|
MAGENTA: "\x1b[35m",
|
|
20
20
|
GRAY: "\x1b[90m",
|
|
21
|
+
WHITE: "\x1b[37m",
|
|
21
22
|
BG_BLUE: "\x1b[44m",
|
|
22
|
-
BG_GREEN: "\x1b[42m"
|
|
23
|
+
BG_GREEN: "\x1b[42m",
|
|
24
|
+
BG_RED: "\x1b[41m"
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
const rawArgs = process.argv.slice(2);
|
|
@@ -78,7 +80,9 @@ function detectFailures() {
|
|
|
78
80
|
results.push({ name: path.basename(path.dirname(fullPath)), htmlPath: fullPath });
|
|
79
81
|
}
|
|
80
82
|
});
|
|
81
|
-
} catch (e) {
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.log(`\nβ οΈ Scan Error in ${dir}: ${C.RED}${e.message}${C.RESET}`);
|
|
85
|
+
}
|
|
82
86
|
});
|
|
83
87
|
return results;
|
|
84
88
|
}
|
|
@@ -97,73 +101,90 @@ async function analyzeAndFix(artifacts, client, argv) {
|
|
|
97
101
|
const loc = extractLoc(content);
|
|
98
102
|
const source = loc && fs.existsSync(loc.path) ? fs.readFileSync(loc.path, 'utf8') : null;
|
|
99
103
|
const res = await client.heal(null, art.htmlPath, loc, source, argv.fix);
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
|
|
105
|
+
if (res && res.status === 'success') {
|
|
102
106
|
process.stdout.write(`\rβ
Analysis complete for ${art.name}\n`);
|
|
103
|
-
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(res.fix);
|
|
109
|
+
if (parsed.explanation) console.log(` ${C.CYAN}${C.BRIGHT}π§ Insight:${C.RESET} ${C.GRAY}${parsed.explanation}${C.RESET}`);
|
|
110
|
+
if (argv.fix && parsed.patches && parsed.patches.length > 0) {
|
|
111
|
+
if (await applyFix(res, loc)) {
|
|
112
|
+
count++;
|
|
113
|
+
console.log(` ${C.GREEN}π Fix applied to ${path.basename(loc.path)}:${loc.line}${C.RESET}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.log(` ${C.CYAN}${C.BRIGHT}π§ Insight:${C.RESET} ${res.fix}`);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
process.stdout.write(`\rβ Analysis incomplete for ${art.name}: ${res?.message || 'Check logs'}\n`);
|
|
104
121
|
}
|
|
105
122
|
} catch (e) {
|
|
106
|
-
console.log(`\n
|
|
123
|
+
console.log(`\n β ${C.RED}Error: ${e.message}${C.RESET}`);
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
126
|
return count;
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
async function applyFix(res, loc) {
|
|
113
|
-
if (!loc?.path)
|
|
130
|
+
if (!loc?.path) {
|
|
131
|
+
console.log(` ${C.YELLOW}β οΈ Cannot apply fix: failure location not detected.${C.RESET}`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
114
134
|
try {
|
|
115
135
|
const patches = JSON.parse(res.fix).patches || [];
|
|
136
|
+
if (patches.length === 0) {
|
|
137
|
+
console.log(` ${C.YELLOW}β οΈ No patches provided by AI.${C.RESET}`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
116
140
|
const original = fs.readFileSync(loc.path, 'utf8');
|
|
117
141
|
let content = original;
|
|
142
|
+
let appliedCount = 0;
|
|
118
143
|
for (const p of patches) {
|
|
119
144
|
const lines = content.split('\n');
|
|
120
145
|
const idx = p.line - 1;
|
|
121
146
|
if (idx >= 0 && idx < lines.length) {
|
|
122
147
|
const indent = lines[idx].match(/^\s*/)[0];
|
|
123
148
|
const cleanLine = indent + p.new_line.trim();
|
|
124
|
-
if
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
content = lines.join('\n');
|
|
149
|
+
// Safety: skip if the new line is already present (dedup)
|
|
150
|
+
if (content.includes(p.new_line.trim())) {
|
|
151
|
+
console.log(` ${C.GRAY}βοΈ Skipped (already present): ${p.new_line.trim().substring(0, 60)}...${C.RESET}`);
|
|
152
|
+
continue;
|
|
129
153
|
}
|
|
154
|
+
if (p.action === 'INSERT_AFTER') lines.splice(p.line, 0, cleanLine);
|
|
155
|
+
else lines[idx] = cleanLine;
|
|
156
|
+
content = lines.join('\n');
|
|
157
|
+
appliedCount++;
|
|
130
158
|
}
|
|
131
159
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (content.match(/class\s+\w+\s+\{[\s\S]*?await\s+/)) throw new Error("Illegal await");
|
|
160
|
+
if (appliedCount > 0) {
|
|
161
|
+
fs.writeFileSync(loc.path, content);
|
|
135
162
|
return true;
|
|
136
|
-
} catch(e) {
|
|
137
|
-
fs.writeFileSync(loc.path, original);
|
|
138
|
-
return false;
|
|
139
163
|
}
|
|
140
|
-
|
|
164
|
+
return false;
|
|
165
|
+
} catch(e) {
|
|
166
|
+
console.log(` ${C.RED}β Patch error: ${e.message}${C.RESET}`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
141
169
|
}
|
|
142
170
|
|
|
143
171
|
function extractLoc(text) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function printFix(fix, loc) {
|
|
149
|
-
try {
|
|
150
|
-
const p = JSON.parse(fix);
|
|
151
|
-
console.log(`\n${C.CYAN}${C.BRIGHT}π‘ SUGGESTED FIX:${C.RESET}`);
|
|
152
|
-
console.log(` ${C.BRIGHT}Reason:${C.RESET} ${p.explanation}`);
|
|
153
|
-
} catch(e) {}
|
|
172
|
+
// Support .ts, .js, .cy.ts, .cy.js, .spec.ts, .spec.js
|
|
173
|
+
const m = text.match(/at\s+(.*?\.(?:cy\.)?(?:spec\.)?(?:ts|js)):(\d+):(\d+)/);
|
|
174
|
+
return m ? { path: path.resolve(m[1]), line: parseInt(m[2]) } : null;
|
|
154
175
|
}
|
|
155
176
|
|
|
156
177
|
async function runDoctor() {
|
|
157
|
-
console.log(`\n${C.BG_BLUE}${C.WHITE}${C.BRIGHT} DEFLAKE MISSION CONTROL - DIAGNOSTIC
|
|
178
|
+
console.log(`\n${C.BG_BLUE}${C.WHITE}${C.BRIGHT} DEFLAKE MISSION CONTROL - DEEP DIAGNOSTIC ${C.RESET}\n`);
|
|
158
179
|
|
|
159
|
-
// 1. SYSTEM
|
|
180
|
+
// 1. SYSTEM CORE
|
|
160
181
|
console.log(`${C.CYAN}1. SYSTEM CORE${C.RESET}`);
|
|
161
182
|
console.log(` ${C.GRAY}ββ Version: ${C.RESET}${C.BRIGHT}v${pkg.version}${C.RESET}`);
|
|
162
183
|
console.log(` ${C.GRAY}ββ Platform: ${C.RESET}${process.platform} (${process.arch})`);
|
|
163
184
|
console.log(` ${C.GRAY}ββ Project: ${C.RESET}${path.basename(process.cwd())}\n`);
|
|
164
185
|
|
|
165
|
-
// 2. PROJECT AUDIT
|
|
166
|
-
console.log(`${C.CYAN}2. PROJECT
|
|
186
|
+
// 2. PROJECT AUDIT
|
|
187
|
+
console.log(`${C.CYAN}2. PROJECT AUDIT${C.RESET}`);
|
|
167
188
|
const fw = DeFlakeClient.detectFramework();
|
|
168
189
|
console.log(` ${C.GRAY}ββ Framework: ${C.RESET}${C.GREEN}${fw.toUpperCase()}${C.RESET}`);
|
|
169
190
|
|
|
@@ -175,95 +196,85 @@ async function runDoctor() {
|
|
|
175
196
|
files.forEach(f => allFiles.push({ name: f, full: path.join(d, f) }));
|
|
176
197
|
}
|
|
177
198
|
});
|
|
178
|
-
|
|
179
|
-
if (allFiles.length
|
|
180
|
-
|
|
181
|
-
allFiles.slice(0, 8).forEach(file => {
|
|
182
|
-
process.stdout.write(` ${C.GRAY}β ββ ${path.basename(file.name).padEnd(25)}${C.RESET} `);
|
|
183
|
-
try {
|
|
184
|
-
execSync(`node --check "${file.full}"`, { stdio: 'ignore' });
|
|
185
|
-
console.log(`${C.GREEN}β Syntax OK${C.RESET}`);
|
|
186
|
-
} catch (e) {
|
|
187
|
-
console.log(`${C.RED}β SYNTAX ERROR${C.RESET}`);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
if (allFiles.length > 8) console.log(` ${C.GRAY}β ββ ... and ${allFiles.length - 8} more files validated.${C.RESET}`);
|
|
191
|
-
else console.log(` ${C.GRAY}β ββ All source files validated.${C.RESET}`);
|
|
192
|
-
} else {
|
|
193
|
-
console.log(` ${C.GRAY}ββ Discovery: ${C.RED}No test/page files found!${C.RESET}`);
|
|
194
|
-
}
|
|
199
|
+
console.log(` ${C.GRAY}ββ Discovery: ${C.RESET}${C.GREEN}${allFiles.length} files matched${C.RESET}`);
|
|
200
|
+
if (allFiles.length === 0) console.log(` ${C.GRAY}ββ ${C.RED}Warning: No test files found in expected directories.${C.RESET}`);
|
|
201
|
+
else console.log(` ${C.GRAY}ββ All source folders are accessible.${C.RESET}`);
|
|
195
202
|
console.log("");
|
|
196
203
|
|
|
197
|
-
// 3. API
|
|
198
|
-
console.log(`${C.CYAN}3. API
|
|
204
|
+
// 3. API CONNECTIVITY
|
|
205
|
+
console.log(`${C.CYAN}3. API CONNECTIVITY${C.RESET}`);
|
|
199
206
|
const apiKey = process.env.DEFLAKE_API_KEY || "NOT_SET";
|
|
200
|
-
const masked = apiKey === "NOT_SET" ? apiKey : (apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length - 4));
|
|
201
|
-
console.log(` ${C.GRAY}ββ API Key: ${C.RESET}${apiKey === "NOT_SET" ? C.RED : C.GREEN}${masked}${C.RESET}`);
|
|
202
|
-
|
|
203
207
|
if (apiKey !== "NOT_SET") {
|
|
204
208
|
try {
|
|
205
|
-
process.stdout.write(` ${C.GRAY}ββ Gateway: ${C.RESET}π‘ Pinging...`);
|
|
206
209
|
const client = new DeFlakeClient();
|
|
207
|
-
const start = Date.now();
|
|
208
210
|
const usage = await client.getUsage();
|
|
209
|
-
const latency = Date.now() - start;
|
|
210
211
|
if (usage?.status === 'success') {
|
|
211
|
-
|
|
212
|
-
console.log(` ${C.GRAY}ββ Quota: ${C.RESET}${C.MAGENTA}${usage.data.usage}/${usage.data.limit} fixes used${C.RESET}`);
|
|
212
|
+
console.log(` ${C.GRAY}ββ Connection: ${C.RESET}${C.GREEN}ONLINE${C.RESET}`);
|
|
213
|
+
console.log(` ${C.GRAY}ββ Quota: ${C.RESET}${C.MAGENTA}${usage.data.usage}/${usage.data.limit} monthly fixes used${C.RESET}`);
|
|
213
214
|
} else {
|
|
214
|
-
|
|
215
|
+
console.log(` ${C.GRAY}ββ Connection: ${C.RESET}${C.RED}OFFLINE (Unauthorized)${C.RESET}`);
|
|
215
216
|
}
|
|
216
217
|
} catch (e) {
|
|
217
|
-
|
|
218
|
+
console.log(` ${C.GRAY}ββ Connection: ${C.RESET}${C.RED}OFFLINE (${e.message})${C.RESET}`);
|
|
218
219
|
}
|
|
219
220
|
} else {
|
|
220
|
-
console.log(` ${C.GRAY}ββ
|
|
221
|
+
console.log(` ${C.GRAY}ββ API Key: ${C.RED}MISSING${C.RESET}`);
|
|
221
222
|
}
|
|
222
223
|
console.log("");
|
|
223
224
|
|
|
224
|
-
// 4.
|
|
225
|
-
console.log(`${C.CYAN}4.
|
|
225
|
+
// 4. DEEP ACCESS AUDIT (The Truth Teller)
|
|
226
|
+
console.log(`${C.CYAN}4. DEEP ACCESS AUDIT (The Truth Teller)${C.RESET}`);
|
|
226
227
|
const artDirs = [
|
|
227
228
|
{ path: 'test-results', label: 'Test Results' },
|
|
228
229
|
{ path: 'playwright-report', label: 'HTML Report' }
|
|
229
230
|
];
|
|
230
|
-
let
|
|
231
|
-
let
|
|
231
|
+
let allPristine = true;
|
|
232
|
+
let sandboxed = false;
|
|
232
233
|
|
|
233
234
|
for (const d of artDirs) {
|
|
234
235
|
if (fs.existsSync(d.path)) {
|
|
235
|
-
process.stdout.write(` ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}π
|
|
236
|
-
let dItems = 0;
|
|
236
|
+
process.stdout.write(` ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}π Scanning Deep...`);
|
|
237
237
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
process.stdout.write(`\r ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}${C.GREEN}${C.BRIGHT}[LIBERADOS]${C.RESET}${C.GREEN} (${dItems} Γtems accesibles)${C.RESET} \n`);
|
|
238
|
+
// A shallow accessSync is NOT enough for Mac Sandbox. Must list and stat.
|
|
239
|
+
const items = fs.readdirSync(d.path);
|
|
240
|
+
if (items.length > 0) {
|
|
241
|
+
process.stdout.write(`\r ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}${C.GREEN}${C.BRIGHT}[LIBERADOS]${C.RESET}${C.GREEN} (${items.length} top-level objects)${C.RESET} \n`);
|
|
242
|
+
} else {
|
|
243
|
+
process.stdout.write(`\r ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}${C.GRAY}Vacia (No hay resultados)${C.RESET} \n`);
|
|
244
|
+
}
|
|
246
245
|
} catch (e) {
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
if (e.code === 'EPERM' || e.message.includes('Operation not permitted')) {
|
|
247
|
+
process.stdout.write(`\r ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}${C.BG_RED}${C.WHITE} BLOQUEADO POR MAC PRIVACY ${C.RESET} \n`);
|
|
248
|
+
sandboxed = true;
|
|
249
|
+
} else {
|
|
250
|
+
process.stdout.write(`\r ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}${C.RED}[ERROR] (${e.code})${C.RESET} \n`);
|
|
251
|
+
}
|
|
252
|
+
allPristine = false;
|
|
249
253
|
}
|
|
250
254
|
} else {
|
|
251
|
-
console.log(` ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}${C.GRAY}
|
|
255
|
+
console.log(` ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}${C.GRAY}No detectada${C.RESET}`);
|
|
252
256
|
}
|
|
253
257
|
}
|
|
254
258
|
|
|
255
|
-
if (
|
|
256
|
-
console.log(`\n ${C.
|
|
259
|
+
if (sandboxed) {
|
|
260
|
+
console.log(`\n ${C.BG_RED}${C.WHITE}${C.BRIGHT} ALERTA: MAC SANDBOX DETECTADO ${C.RESET}`);
|
|
261
|
+
console.log(` ${C.BRIGHT}Tu proyecto estΓ‘ en Documents/Desktop y Mac bloquea el acceso profundo.${C.RESET}`);
|
|
262
|
+
console.log(` ${C.CYAN}SOLUCIΓN 1:${C.RESET} Dale "Full Disk Access" a tu Terminal en Settings > Privacy.`);
|
|
263
|
+
console.log(` ${C.CYAN}SOLUCIΓN 2:${C.RESET} Mueve el proyecto fuera de Documents (ej: /Users/hugo/code/...)`);
|
|
264
|
+
} else if (!allPristine) {
|
|
265
|
+
console.log(`\n ${C.YELLOW}β οΈ PERMISOS DE ARCHIVO REQUERIDOS:${C.RESET}`);
|
|
257
266
|
const fix = process.platform === 'win32' ? 'icacls . /grant ${env:USERNAME}:(OI)(CI)F /T' : 'sudo chown -R $(whoami) .';
|
|
258
|
-
console.log(` Ejecuta
|
|
267
|
+
console.log(` Ejecuta: ${C.CYAN}${fix}${C.RESET}`);
|
|
259
268
|
} else {
|
|
260
|
-
console.log(
|
|
261
|
-
console.log(`\n${C.BG_GREEN}${C.WHITE}${C.BRIGHT} ESTADO DEL SISTEMA: LIBERADO Y OPERATIVO ${C.RESET}`);
|
|
262
|
-
console.log(` Ya puedes correr: ${C.CYAN}npx deflake --fix --report npx playwright test${C.RESET}`);
|
|
269
|
+
console.log(`\n${C.BG_GREEN}${C.WHITE}${C.BRIGHT} SISTEMA SALUDABLE Y LIBERADO ${C.RESET}`);
|
|
263
270
|
}
|
|
264
|
-
console.log("\n" + C.GRAY + "β".repeat(
|
|
271
|
+
console.log("\n" + C.GRAY + "β".repeat(55) + C.RESET + "\n");
|
|
265
272
|
}
|
|
266
273
|
|
|
267
274
|
function showReport() {
|
|
275
|
+
// Kill any existing report server to prevent EADDRINUSE
|
|
276
|
+
try {
|
|
277
|
+
execSync('lsof -ti:9323 | xargs kill -9 2>/dev/null', { stdio: 'ignore' });
|
|
278
|
+
} catch (e) { /* no process to kill, that's fine */ }
|
|
268
279
|
spawn('npx playwright show-report', { shell: true, stdio: 'inherit' }).on('error', () => {});
|
|
269
280
|
}
|
package/client.js
CHANGED
|
@@ -105,11 +105,13 @@ class DeFlakeClient {
|
|
|
105
105
|
|
|
106
106
|
async heal(logPath, htmlPath, failureLocation = null, sourceCode = null, applyFix = false) {
|
|
107
107
|
try {
|
|
108
|
-
// ... (rest of the check logic) ...
|
|
109
|
-
if (!fs.existsSync(logPath)) throw new Error(`Log file not found: ${logPath}`);
|
|
110
108
|
if (!fs.existsSync(htmlPath)) throw new Error(`HTML file not found: ${htmlPath}`);
|
|
111
109
|
|
|
112
|
-
|
|
110
|
+
// logPath is optional β when null, we use htmlPath as the primary error context
|
|
111
|
+
let logContent = '';
|
|
112
|
+
if (logPath && fs.existsSync(logPath)) {
|
|
113
|
+
logContent = fs.readFileSync(logPath, 'utf8');
|
|
114
|
+
}
|
|
113
115
|
|
|
114
116
|
// Handle binary artifacts (Screenshots) vs Text artifacts (HTML/MD)
|
|
115
117
|
const isImage = htmlPath.toLowerCase().endsWith('.png');
|