deflake 1.2.17 β 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 +143 -73
- 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
|
@@ -7,7 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const pkg = require('./package.json');
|
|
9
9
|
|
|
10
|
-
// --- COLORS ---
|
|
10
|
+
// --- PREMIUM COLORS ---
|
|
11
11
|
const C = {
|
|
12
12
|
RESET: "\x1b[0m",
|
|
13
13
|
BRIGHT: "\x1b[1m",
|
|
@@ -16,8 +16,12 @@ const C = {
|
|
|
16
16
|
YELLOW: "\x1b[33m",
|
|
17
17
|
CYAN: "\x1b[36m",
|
|
18
18
|
BLUE: "\x1b[34m",
|
|
19
|
+
MAGENTA: "\x1b[35m",
|
|
19
20
|
GRAY: "\x1b[90m",
|
|
20
|
-
WHITE: "\x1b[37m"
|
|
21
|
+
WHITE: "\x1b[37m",
|
|
22
|
+
BG_BLUE: "\x1b[44m",
|
|
23
|
+
BG_GREEN: "\x1b[42m",
|
|
24
|
+
BG_RED: "\x1b[41m"
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
const rawArgs = process.argv.slice(2);
|
|
@@ -43,19 +47,16 @@ async function main() {
|
|
|
43
47
|
|
|
44
48
|
if (commandToRun) {
|
|
45
49
|
const { code } = await runNative(commandToRun);
|
|
46
|
-
|
|
47
50
|
if (code !== 0) {
|
|
48
51
|
console.log(`\nπ΄ Command failed with code ${code}. Activating DeFlake...`);
|
|
49
52
|
const artifacts = detectFailures();
|
|
50
53
|
const applied = await analyzeAndFix(artifacts, client, argv);
|
|
51
|
-
|
|
52
54
|
if (applied > 0 && argv.fix) {
|
|
53
55
|
console.log(`\n${C.BRIGHT}π Fixes applied. Re-running tests to verify...${C.RESET}`);
|
|
54
56
|
await runNative(commandToRun);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
|
-
|
|
59
60
|
if (argv.report) showReport();
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -79,7 +80,9 @@ function detectFailures() {
|
|
|
79
80
|
results.push({ name: path.basename(path.dirname(fullPath)), htmlPath: fullPath });
|
|
80
81
|
}
|
|
81
82
|
});
|
|
82
|
-
} catch (e) {
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.log(`\nβ οΈ Scan Error in ${dir}: ${C.RED}${e.message}${C.RESET}`);
|
|
85
|
+
}
|
|
83
86
|
});
|
|
84
87
|
return results;
|
|
85
88
|
}
|
|
@@ -89,122 +92,189 @@ async function analyzeAndFix(artifacts, client, argv) {
|
|
|
89
92
|
console.log(` ${C.YELLOW}β οΈ No failure artifacts detected. Run 'npx deflake doctor' to check permissions.${C.RESET}`);
|
|
90
93
|
return 0;
|
|
91
94
|
}
|
|
92
|
-
|
|
93
95
|
console.log(`π Analyzing ${artifacts.length} failure(s)...`);
|
|
94
96
|
let count = 0;
|
|
95
|
-
|
|
96
97
|
for (const art of artifacts) {
|
|
97
98
|
process.stdout.write(`β³ Analyzing ${art.name}...`);
|
|
98
99
|
try {
|
|
99
100
|
const content = fs.readFileSync(art.htmlPath, 'utf8');
|
|
100
101
|
const loc = extractLoc(content);
|
|
101
102
|
const source = loc && fs.existsSync(loc.path) ? fs.readFileSync(loc.path, 'utf8') : null;
|
|
102
|
-
|
|
103
103
|
const res = await client.heal(null, art.htmlPath, loc, source, argv.fix);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (await applyFix(res, loc)) count++;
|
|
107
|
-
}
|
|
104
|
+
|
|
105
|
+
if (res && res.status === 'success') {
|
|
108
106
|
process.stdout.write(`\rβ
Analysis complete for ${art.name}\n`);
|
|
109
|
-
|
|
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`);
|
|
110
121
|
}
|
|
111
122
|
} catch (e) {
|
|
112
|
-
console.log(`\n
|
|
123
|
+
console.log(`\n β ${C.RED}Error: ${e.message}${C.RESET}`);
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
126
|
return count;
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
async function applyFix(res, loc) {
|
|
119
|
-
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
|
+
}
|
|
120
134
|
try {
|
|
121
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
|
+
}
|
|
122
140
|
const original = fs.readFileSync(loc.path, 'utf8');
|
|
123
141
|
let content = original;
|
|
124
|
-
|
|
142
|
+
let appliedCount = 0;
|
|
125
143
|
for (const p of patches) {
|
|
126
144
|
const lines = content.split('\n');
|
|
127
145
|
const idx = p.line - 1;
|
|
128
146
|
if (idx >= 0 && idx < lines.length) {
|
|
129
147
|
const indent = lines[idx].match(/^\s*/)[0];
|
|
130
148
|
const cleanLine = indent + p.new_line.trim();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!content.includes(p.new_line.trim())) {
|
|
139
|
-
if (p.action === 'INSERT_AFTER') lines.splice(p.line, 0, cleanLine);
|
|
140
|
-
else lines[idx] = cleanLine;
|
|
141
|
-
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;
|
|
142
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++;
|
|
143
158
|
}
|
|
144
159
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// π§ͺ SMARTER SYNTAX CHECK
|
|
149
|
-
try {
|
|
150
|
-
// Check for obvious syntax errors in TS (top level await in CommonJS/Class)
|
|
151
|
-
if (content.match(/class\s+\w+\s+\{[\s\S]*?await\s+/)) {
|
|
152
|
-
throw new Error("Illegal await in class body");
|
|
153
|
-
}
|
|
160
|
+
if (appliedCount > 0) {
|
|
161
|
+
fs.writeFileSync(loc.path, content);
|
|
154
162
|
return true;
|
|
155
|
-
} catch(e) {
|
|
156
|
-
fs.writeFileSync(loc.path, original);
|
|
157
|
-
return false;
|
|
158
163
|
}
|
|
159
|
-
|
|
164
|
+
return false;
|
|
165
|
+
} catch(e) {
|
|
166
|
+
console.log(` ${C.RED}β Patch error: ${e.message}${C.RESET}`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
function extractLoc(text) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function printFix(fix, loc) {
|
|
168
|
-
try {
|
|
169
|
-
const p = JSON.parse(fix);
|
|
170
|
-
console.log(`\n${C.CYAN}${C.BRIGHT}π‘ SUGGESTED FIX:${C.RESET}`);
|
|
171
|
-
console.log(` ${C.BRIGHT}Reason:${C.RESET} ${p.explanation}`);
|
|
172
|
-
} 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;
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
async function runDoctor() {
|
|
176
|
-
console.log(`\n
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
console.log(`\n${C.BG_BLUE}${C.WHITE}${C.BRIGHT} DEFLAKE MISSION CONTROL - DEEP DIAGNOSTIC ${C.RESET}\n`);
|
|
179
|
+
|
|
180
|
+
// 1. SYSTEM CORE
|
|
181
|
+
console.log(`${C.CYAN}1. SYSTEM CORE${C.RESET}`);
|
|
182
|
+
console.log(` ${C.GRAY}ββ Version: ${C.RESET}${C.BRIGHT}v${pkg.version}${C.RESET}`);
|
|
183
|
+
console.log(` ${C.GRAY}ββ Platform: ${C.RESET}${process.platform} (${process.arch})`);
|
|
184
|
+
console.log(` ${C.GRAY}ββ Project: ${C.RESET}${path.basename(process.cwd())}\n`);
|
|
185
|
+
|
|
186
|
+
// 2. PROJECT AUDIT
|
|
187
|
+
console.log(`${C.CYAN}2. PROJECT AUDIT${C.RESET}`);
|
|
188
|
+
const fw = DeFlakeClient.detectFramework();
|
|
189
|
+
console.log(` ${C.GRAY}ββ Framework: ${C.RESET}${C.GREEN}${fw.toUpperCase()}${C.RESET}`);
|
|
190
|
+
|
|
191
|
+
const testDirs = ['tests', 'cypress/e2e', 'test', 'pages'];
|
|
192
|
+
let allFiles = [];
|
|
193
|
+
testDirs.forEach(d => {
|
|
194
|
+
if (fs.existsSync(d)) {
|
|
195
|
+
const files = fs.readdirSync(d, { recursive: true }).filter(f => f.endsWith('.ts') || f.endsWith('.js') || f.endsWith('.cy.js'));
|
|
196
|
+
files.forEach(f => allFiles.push({ name: f, full: path.join(d, f) }));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
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}`);
|
|
202
|
+
console.log("");
|
|
179
203
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
204
|
+
// 3. API CONNECTIVITY
|
|
205
|
+
console.log(`${C.CYAN}3. API CONNECTIVITY${C.RESET}`);
|
|
206
|
+
const apiKey = process.env.DEFLAKE_API_KEY || "NOT_SET";
|
|
207
|
+
if (apiKey !== "NOT_SET") {
|
|
183
208
|
try {
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log(`${C.GREEN}Healthy${C.RESET}`);
|
|
209
|
+
const client = new DeFlakeClient();
|
|
210
|
+
const usage = await client.getUsage();
|
|
211
|
+
if (usage?.status === 'success') {
|
|
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}`);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(` ${C.GRAY}ββ Connection: ${C.RESET}${C.RED}OFFLINE (Unauthorized)${C.RESET}`);
|
|
216
|
+
}
|
|
193
217
|
} catch (e) {
|
|
194
|
-
console.log(
|
|
195
|
-
|
|
218
|
+
console.log(` ${C.GRAY}ββ Connection: ${C.RESET}${C.RED}OFFLINE (${e.message})${C.RESET}`);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
console.log(` ${C.GRAY}ββ API Key: ${C.RED}MISSING${C.RESET}`);
|
|
222
|
+
}
|
|
223
|
+
console.log("");
|
|
224
|
+
|
|
225
|
+
// 4. DEEP ACCESS AUDIT (The Truth Teller)
|
|
226
|
+
console.log(`${C.CYAN}4. DEEP ACCESS AUDIT (The Truth Teller)${C.RESET}`);
|
|
227
|
+
const artDirs = [
|
|
228
|
+
{ path: 'test-results', label: 'Test Results' },
|
|
229
|
+
{ path: 'playwright-report', label: 'HTML Report' }
|
|
230
|
+
];
|
|
231
|
+
let allPristine = true;
|
|
232
|
+
let sandboxed = false;
|
|
233
|
+
|
|
234
|
+
for (const d of artDirs) {
|
|
235
|
+
if (fs.existsSync(d.path)) {
|
|
236
|
+
process.stdout.write(` ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}π Scanning Deep...`);
|
|
237
|
+
try {
|
|
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
|
+
}
|
|
245
|
+
} catch (e) {
|
|
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;
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
console.log(` ${C.GRAY}ββ ${d.label.padEnd(16)}: ${C.RESET}${C.GRAY}No detectada${C.RESET}`);
|
|
196
256
|
}
|
|
197
257
|
}
|
|
198
258
|
|
|
199
|
-
if (
|
|
200
|
-
console.log(`\n
|
|
201
|
-
|
|
202
|
-
console.log(`
|
|
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}`);
|
|
266
|
+
const fix = process.platform === 'win32' ? 'icacls . /grant ${env:USERNAME}:(OI)(CI)F /T' : 'sudo chown -R $(whoami) .';
|
|
267
|
+
console.log(` Ejecuta: ${C.CYAN}${fix}${C.RESET}`);
|
|
203
268
|
} else {
|
|
204
|
-
console.log(`\n
|
|
269
|
+
console.log(`\n${C.BG_GREEN}${C.WHITE}${C.BRIGHT} SISTEMA SALUDABLE Y LIBERADO ${C.RESET}`);
|
|
205
270
|
}
|
|
271
|
+
console.log("\n" + C.GRAY + "β".repeat(55) + C.RESET + "\n");
|
|
206
272
|
}
|
|
207
273
|
|
|
208
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 */ }
|
|
209
279
|
spawn('npx playwright show-report', { shell: true, stdio: 'inherit' }).on('error', () => {});
|
|
210
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');
|