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.
Files changed (4) hide show
  1. package/README.md +6 -3
  2. package/cli.js +98 -87
  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
@@ -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
- if (res?.status === 'success') {
101
- if (argv.fix && await applyFix(res, loc)) count++;
104
+
105
+ if (res && res.status === 'success') {
102
106
  process.stdout.write(`\rβœ… Analysis complete for ${art.name}\n`);
103
- 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`);
104
121
  }
105
122
  } catch (e) {
106
- console.log(`\n ❌ ${C.RED}Error: ${e.message}${C.RESET}`);
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) return false;
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 (cleanLine.includes('await') && !content.includes('async')) continue;
125
- if (!content.includes(p.new_line.trim())) {
126
- if (p.action === 'INSERT_AFTER') lines.splice(p.line, 0, cleanLine);
127
- else lines[idx] = cleanLine;
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
- fs.writeFileSync(loc.path, content);
133
- try {
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
- } 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
+ }
141
169
  }
142
170
 
143
171
  function extractLoc(text) {
144
- const m = text.match(/at\s+(.*?\.ts):(\d+):(\d+)/);
145
- return m ? { path: path.resolve(m[1]), line: m[2] } : null;
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 CENTER ${C.RESET}\n`);
178
+ console.log(`\n${C.BG_BLUE}${C.WHITE}${C.BRIGHT} DEFLAKE MISSION CONTROL - DEEP DIAGNOSTIC ${C.RESET}\n`);
158
179
 
159
- // 1. SYSTEM HUB
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 (Discovery Phase)
166
- console.log(`${C.CYAN}2. PROJECT DISCOVERY & VALIDATION${C.RESET}`);
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 > 0) {
180
- console.log(` ${C.GRAY}β”œβ”€ Discovery: ${C.RESET}${C.GREEN}${allFiles.length} files detected${C.RESET}`);
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 & SUBSCRIPTION HUB
198
- console.log(`${C.CYAN}3. API & CLOUD HUB${C.RESET}`);
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
- process.stdout.write(`\r ${C.GRAY}β”œβ”€ Gateway: ${C.RESET}${C.GREEN}ONLINE (${latency}ms)${C.RESET} \n`);
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
- process.stdout.write(`\r ${C.GRAY}β”œβ”€ Gateway: ${C.RESET}${C.RED}Connection Failed (${usage?.message || 'Unauthorized'})${C.RESET} \n`);
215
+ console.log(` ${C.GRAY}└─ Connection: ${C.RESET}${C.RED}OFFLINE (Unauthorized)${C.RESET}`);
215
216
  }
216
217
  } catch (e) {
217
- process.stdout.write(`\r ${C.GRAY}β”œβ”€ Gateway: ${C.RESET}${C.RED}Link Down (${e.message})${C.RESET} \n`);
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}└─ Status: ${C.RED}Awaiting credentials...${C.RESET}`);
221
+ console.log(` ${C.GRAY}└─ API Key: ${C.RED}MISSING${C.RESET}`);
221
222
  }
222
223
  console.log("");
223
224
 
224
- // 4. THE LIBERATION AUDIT (Permission Status)
225
- console.log(`${C.CYAN}4. LIBERATION AUDIT (Permission Status)${C.RESET}`);
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 allLiberated = true;
231
- let totalItems = 0;
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}πŸ” Checking...`);
236
- let dItems = 0;
236
+ process.stdout.write(` ${C.GRAY}β”œβ”€ ${d.label.padEnd(16)}: ${C.RESET}πŸ” Scanning Deep...`);
237
237
  try {
238
- const scan = (p) => {
239
- dItems++;
240
- totalItems++;
241
- fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK);
242
- if (fs.statSync(p).isDirectory()) fs.readdirSync(p).forEach(f => scan(path.join(p, f)));
243
- };
244
- scan(d.path);
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
- process.stdout.write(`\r ${C.GRAY}β”œβ”€ ${d.label.padEnd(16)}: ${C.RESET}${C.RED}${C.BRIGHT}[BLOQUEADOS]${C.RESET}${C.RED} (Error en: ${path.basename(e.path)})${C.RESET} \n`);
248
- allLiberated = false;
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}Sin carpeta (AΓΊn no hay resultados)${C.RESET}`);
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 (!allLiberated) {
256
- console.log(`\n ${C.BG_BLUE}${C.WHITE}${C.BRIGHT} ACCIΓ“N REQUERIDA: LIBERAR PERMISOS ${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}`);
257
266
  const fix = process.platform === 'win32' ? 'icacls . /grant ${env:USERNAME}:(OI)(CI)F /T' : 'sudo chown -R $(whoami) .';
258
- console.log(` Ejecuta esto en tu terminal: ${C.CYAN}${fix}${C.RESET}`);
267
+ console.log(` Ejecuta: ${C.CYAN}${fix}${C.RESET}`);
259
268
  } else {
260
- console.log(` ${C.GRAY}└─ AuditorΓ­a: ${C.RESET}${C.GREEN}${totalItems} archivos verificados con Γ©xito.${C.RESET}`);
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(50) + C.RESET + "\n");
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
- 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.21",
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": {