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.
Files changed (4) hide show
  1. package/README.md +6 -3
  2. package/cli.js +143 -73
  3. package/client.js +5 -3
  4. 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
- if (res?.status === 'success') {
105
- if (argv.fix) {
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
- printFix(res.fix, loc);
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 ❌ ${C.RED}Error: ${e.message}${C.RESET}`);
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) return false;
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
- // πŸ›‘ SAFETY GUARD: Prevent top-level await in class bodies
133
- if (cleanLine.includes('await') && !content.includes('async')) {
134
- console.log(` ⚠️ Blocked illegal await in non-async scope: ${path.basename(loc.path)}`);
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
- fs.writeFileSync(loc.path, content);
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
- } catch(e) { return false; }
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
- const m = text.match(/at\s+(.*?\.ts):(\d+):(\d+)/);
164
- return m ? { path: path.resolve(m[1]), line: m[2] } : null;
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πŸ‘¨β€βš•οΈ ${C.BRIGHT}DeFlake Doctor - Diagnostic Tool${C.RESET}\n`);
177
- const dirs = ['test-results', 'playwright-report'];
178
- let error = false;
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
- for (const d of dirs) {
181
- if (!fs.existsSync(d)) continue;
182
- process.stdout.write(` Checking ${d.padEnd(20)}... `);
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
- fs.accessSync(d, fs.constants.R_OK);
185
- const checkDeep = (p) => {
186
- fs.accessSync(p, fs.constants.R_OK);
187
- if (fs.statSync(p).isDirectory()) {
188
- fs.readdirSync(p).forEach(f => checkDeep(path.join(p, f)));
189
- }
190
- };
191
- checkDeep(d);
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(`${C.RED}Access Denied${C.RESET}`);
195
- error = true;
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 (error) {
200
- console.log(`\n ${C.YELLOW}⚠️ Files are locked!${C.RESET}`);
201
- const cmd = process.platform === 'win32' ? `icacls . /grant \${env:USERNAME}:(OI)(CI)F /T` : `sudo chown -R $(whoami) .`;
202
- console.log(` ${C.BRIGHT}Fix:${C.RESET} ${C.CYAN}${cmd}${C.RESET}`);
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 ✨ ${C.GREEN}All checks passed. System is ready.${C.RESET}`);
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
- const logContent = fs.readFileSync(logPath, 'utf8');
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.2.17",
3
+ "version": "1.2.25",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {