deflake 1.2.13 → 1.2.17

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 (2) hide show
  1. package/cli.js +138 -1008
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -2,8 +2,7 @@
2
2
  const yargs = require('yargs/yargs');
3
3
  const { hideBin } = require('yargs/helpers');
4
4
  const DeFlakeClient = require('./client');
5
- const Migrator = require('./lib/migrator');
6
- const { spawn } = require('child_process');
5
+ const { spawn, execSync } = require('child_process');
7
6
  const fs = require('fs');
8
7
  const path = require('path');
9
8
  const pkg = require('./package.json');
@@ -21,1060 +20,191 @@ const C = {
21
20
  WHITE: "\x1b[37m"
22
21
  };
23
22
 
24
- // --- DETERMINISTIC COMMAND INTERCEPTION ---
25
- // Check for diagnostic commands before yargs or main logic even starts
26
23
  const rawArgs = process.argv.slice(2);
27
- if (rawArgs.includes('doctor') || rawArgs.includes('--doctor')) {
28
- // We run the doctor logic and force exit to prevent any wrapper loops
29
- const doctorArgv = yargs(hideBin(process.argv)).argv;
30
- runDoctor(doctorArgv).then(() => process.exit(0)).catch((err) => {
31
- console.error(`${C.RED}Doctor failed:${C.RESET} ${err.message}`);
32
- process.exit(1);
33
- });
34
- return; // Safety for some environments
24
+ if (rawArgs.includes('doctor')) {
25
+ runDoctor().then(() => process.exit(0));
26
+ } else {
27
+ main();
35
28
  }
36
- // ------------------------------------------
37
-
38
- const parser = yargs(hideBin(process.argv))
39
- .option('log', {
40
- alias: 'l',
41
- type: 'string',
42
- description: 'Path to the error log file',
43
- demandOption: false
44
- })
45
- .option('html', {
46
- alias: 'h',
47
- type: 'string',
48
- description: 'Path to the HTML snapshot',
49
- demandOption: false
50
- })
51
- .option('api-url', {
52
- type: 'string',
53
- description: 'Override Default API URL',
54
- })
55
- .option('fix', {
56
- type: 'boolean',
57
- description: 'Automatically apply suggested fixes',
58
- default: false
59
- })
60
- .option('doctor', {
61
- type: 'boolean',
62
- description: 'Diagnose your DeFlake installation',
63
- default: false
64
- })
65
- .option('report', {
66
- type: 'boolean',
67
- description: 'Automatically open the HTML report after fixing',
68
- default: true
69
- })
70
- .command('doctor', 'Diagnose your DeFlake installation', {}, (argv) => {
71
- runDoctor(argv).then(() => process.exit(0));
72
- })
73
- .command('migrate', 'Migrate Cypress tests to Playwright', {
74
- from: { type: 'string', default: 'cypress', description: 'Source framework' },
75
- to: { type: 'string', default: 'playwright', description: 'Target framework' },
76
- path: { type: 'string', demandOption: true, description: 'Path to Cypress tests' },
77
- output: { type: 'string', description: 'Path to output Playwright tests' },
78
- ai: { type: 'boolean', default: false, description: 'Enable AI-powered refinement' }
79
- }, async (argv) => {
80
- try {
81
- const client = new DeFlakeClient(argv['api-url']);
82
- const migrator = new Migrator({ ...argv, client });
83
- await migrator.run();
84
- process.exit(0);
85
- } catch (err) {
86
- console.error(`\x1b[31mMigration failed:\x1b[0m ${err.message}`);
87
- process.exit(1);
88
- }
89
- })
90
- .version(pkg.version)
91
- .help();
92
-
93
- const argv = parser.argv;
94
-
95
- // If we reach this point, it means no diagnostic command (like 'doctor') was triggered.
96
- // We proceed with the main test wrapper logic.
97
- main();
98
-
99
- // Helper to auto-detect artifacts (Batch Mode)
100
- function detectAllArtifacts(providedLog, providedHtml) {
101
- // If user provided explicit paths, use them as a single item list
102
- if (providedHtml || providedLog) {
103
- return [{
104
- logPath: providedLog,
105
- htmlPath: providedHtml,
106
- id: 'manual-input',
107
- name: 'Manual Input'
108
- }];
109
- }
110
-
111
- const detected = [];
112
29
 
113
- // 1. Scan test-results
114
- if (fs.existsSync('test-results')) {
115
- function findFiles(dir) {
116
- const entries = fs.readdirSync(dir, { withFileTypes: true });
117
- for (const entry of entries) {
118
- const res = path.resolve(dir, entry.name);
119
- if (entry.isDirectory()) {
120
- findFiles(res);
121
- } else {
122
- // Unique ID for this test failure is the parent folder name
123
- const folderName = path.basename(path.dirname(res));
124
-
125
- // We only want ONE artifact per test folder.
126
- // Priority: error-context.md > index.html > trace.zip
127
-
128
- // But we can't easily dedup here without a map.
129
- // Let's just collect all candidates and filter later.
130
- }
131
- }
132
- }
133
-
134
- // Better approach: Iterate folders in test-results
135
- const testFolders = fs.readdirSync('test-results', { withFileTypes: true })
136
- .filter(dirent => dirent.isDirectory())
137
- .map(dirent => path.resolve('test-results', dirent.name));
138
-
139
- for (const folder of testFolders) {
140
- let artifact = null;
141
- let type = '';
30
+ async function main() {
31
+ const parser = yargs(hideBin(process.argv))
32
+ .option('fix', { type: 'boolean', default: false })
33
+ .option('report', { type: 'boolean', default: true })
34
+ .help();
35
+
36
+ const argv = parser.argv;
37
+ const commandToRun = argv._.join(' ');
38
+
39
+ console.log(`šŸš‘ DeFlake JS Client (Batch Mode) - ${C.CYAN}v${pkg.version}${C.RESET}`);
40
+ const client = new DeFlakeClient();
41
+ const fw = DeFlakeClient.detectFramework();
42
+ console.log(`šŸ” Detected Framework: ${C.GREEN}${fw.toUpperCase()}${C.RESET}`);
142
43
 
143
- const mdPath = path.join(folder, 'error-context.md');
144
- const htmlPath = path.join(folder, 'trace.html'); // Some reporters use this
44
+ if (commandToRun) {
45
+ const { code } = await runNative(commandToRun);
145
46
 
146
- // Check for error-context.md (High Quality)
147
- if (fs.existsSync(mdPath)) {
148
- artifact = mdPath;
149
- type = 'md';
150
- }
151
- // Else check for any HTML that isn't trace viewer boilerplate
152
- else {
153
- // simple fallback
154
- }
47
+ if (code !== 0) {
48
+ console.log(`\nšŸ”“ Command failed with code ${code}. Activating DeFlake...`);
49
+ const artifacts = detectFailures();
50
+ const applied = await analyzeAndFix(artifacts, client, argv);
155
51
 
156
- if (artifact) {
157
- detected.push({
158
- logPath: null, // Log is usually embedded or passed via stdout
159
- htmlPath: artifact,
160
- id: path.basename(folder),
161
- name: path.basename(folder)
162
- });
52
+ if (applied > 0 && argv.fix) {
53
+ console.log(`\n${C.BRIGHT}šŸ’‰ Fixes applied. Re-running tests to verify...${C.RESET}`);
54
+ await runNative(commandToRun);
163
55
  }
164
56
  }
165
57
  }
166
58
 
167
- // 2. Scan cypress/screenshots (Cypress default)
168
- if (fs.existsSync('cypress/screenshots')) {
169
- const screenshotFiles = fs.readdirSync('cypress/screenshots', { recursive: true })
170
- .filter(f => f.endsWith('.png'))
171
- .map(f => path.resolve('cypress/screenshots', f));
172
-
173
- for (const screenshot of screenshotFiles) {
174
- detected.push({
175
- logPath: providedLog,
176
- htmlPath: screenshot,
177
- id: path.basename(screenshot),
178
- name: path.basename(screenshot)
179
- });
180
- }
181
- }
182
-
183
- // Fallback: If no granular results, check global report
184
- if (detected.length === 0) {
185
- if (fs.existsSync('playwright-report/index.html')) {
186
- detected.push({
187
- logPath: providedLog,
188
- htmlPath: 'playwright-report/index.html',
189
- id: 'global-report',
190
- name: 'Global Report'
191
- });
192
- }
193
- }
194
-
195
- return detected;
59
+ if (argv.report) showReport();
196
60
  }
197
61
 
198
- function stripAnsi(str) {
199
- return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
200
- }
201
-
202
- function parsePlaywrightLogs(fullLog) {
203
- const cleanLog = stripAnsi(fullLog);
204
- const errorBlocks = [];
205
- const errorHeaderRegex = /^\s*(\d+)\)\s+\[(.*?)\]\s+/gm;
206
- let match;
207
-
208
- const indices = [];
209
- while ((match = errorHeaderRegex.exec(cleanLog)) !== null) {
210
- indices.push({ index: match.index, name: match[0].trim() });
211
- }
212
-
213
- for (let i = 0; i < indices.length; i++) {
214
- const start = indices[i].index;
215
- const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
216
- errorBlocks.push({
217
- name: indices[i].name,
218
- content: cleanLog.slice(start, end)
219
- });
220
- }
221
- return errorBlocks;
222
- }
223
-
224
- function parseCypressLogs(fullLog) {
225
- const cleanLog = stripAnsi(fullLog);
226
- const specBlocks = [];
227
- const specHeaderRegex = /Running:\s+([^\s]+)\s+\(\d+\s+of\s+\d+\)/g;
228
- let match;
229
-
230
- const indices = [];
231
- while ((match = specHeaderRegex.exec(cleanLog)) !== null) {
232
- indices.push({ index: match.index, spec: match[1] });
233
- }
234
-
235
- for (let i = 0; i < indices.length; i++) {
236
- const start = indices[i].index;
237
- const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
238
- specBlocks.push({
239
- spec: indices[i].spec,
240
- content: cleanLog.slice(start, end)
241
- });
242
- }
243
- return specBlocks;
244
- }
245
-
246
- function parsePytestLogs(fullLog) {
247
- const cleanLog = stripAnsi(fullLog);
248
- const errorBlocks = [];
249
- const testHeaderRegex = /_{10,}\s+(.*?)\s+_{10,}/g;
250
- let match;
251
-
252
- const indices = [];
253
- while ((match = testHeaderRegex.exec(cleanLog)) !== null) {
254
- indices.push({ index: match.index, name: match[1] });
255
- }
256
-
257
- for (let i = 0; i < indices.length; i++) {
258
- const start = indices[i].index;
259
- const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
260
- errorBlocks.push({
261
- name: indices[i].name,
262
- content: cleanLog.slice(start, end)
263
- });
264
- }
265
- return errorBlocks;
62
+ async function runNative(fullCommand) {
63
+ return new Promise((resolve) => {
64
+ console.log(`šŸš€ Running command: ${C.CYAN}${fullCommand}${C.RESET}`);
65
+ const child = spawn(fullCommand, { shell: true, stdio: 'inherit' });
66
+ child.on('close', (code) => resolve({ code }));
67
+ });
266
68
  }
267
69
 
268
- async function runHealer(logContent, htmlPath, apiUrl, testName, applyFix = false) {
269
- // Check file size limit (Basic check to avoid 429s on huge files)
270
- if (htmlPath && fs.existsSync(htmlPath)) {
271
- const stats = fs.statSync(htmlPath);
272
- if (stats.size > 5 * 1024 * 1024) { // 5MB limit warning
273
- console.warn(`āš ļø Warning: Artifact for ${testName} is large (` + (stats.size / 1024 / 1024).toFixed(2) + "MB).");
274
- }
275
- }
276
-
277
- const client = new DeFlakeClient(apiUrl);
278
-
279
- // Pass log CONTENT (string) directly if it came from wrapper, or file path if from args
280
- let finalLogPath = argv.log;
281
- let effectiveLogContent = logContent;
282
-
283
- // Robustness: If logContent is empty but we have an MD artifact, read it
284
- if (!effectiveLogContent && htmlPath && htmlPath.endsWith('.md') && fs.existsSync(htmlPath)) {
285
- try {
286
- effectiveLogContent = fs.readFileSync(htmlPath, 'utf8');
287
- } catch (e) { }
288
- }
289
-
290
- if (effectiveLogContent) {
291
- // We have content (from stdio or artifact), write to temp file
292
- finalLogPath = '.deflake-error.log';
293
- fs.writeFileSync(finalLogPath, effectiveLogContent);
294
- } else if (!finalLogPath && fs.existsSync('error.log')) {
295
- // Fallback to auto-detected error.log if nothing else
296
- finalLogPath = 'error.log';
297
- }
298
-
299
- // Validation before calling client
300
- if (!finalLogPath) {
301
- // Create a dummy log if we have HTML but absolutely no log, to prevent client crash
302
- if (htmlPath) {
303
- console.log(" (Creating temporary log from available context)");
304
- finalLogPath = '.deflake-error.log';
305
- fs.writeFileSync(finalLogPath, "Log missing. Refer to HTML/MD snapshot.");
306
- effectiveLogContent = "Log missing.";
307
- } else {
308
- console.error(`āŒ [${testName}] Error: No log file or content available.`);
309
- return;
310
- }
311
- }
312
-
313
- // 🧠 SMART CONTEXT: Extract the failing source code if possible
314
- const failureLoc = extractFailureLocation(effectiveLogContent || fs.readFileSync(finalLogPath, 'utf8'));
315
-
316
- let sourceCodeContent = null;
317
- if (failureLoc && failureLoc.fullRootPath) {
70
+ function detectFailures() {
71
+ const results = [];
72
+ ['test-results', 'playwright-report'].forEach(dir => {
73
+ if (!fs.existsSync(dir)) return;
318
74
  try {
319
- if (fs.existsSync(failureLoc.fullRootPath)) {
320
- sourceCodeContent = fs.readFileSync(failureLoc.fullRootPath, 'utf8');
321
- }
322
- } catch (e) { }
323
- }
324
-
325
- if (!failureLoc) {
326
- // Silent warning for batch mode - don't clutter output
327
- }
328
-
329
- try {
330
- const result = await client.heal(finalLogPath, htmlPath, failureLoc, sourceCodeContent, applyFix);
331
-
332
- if (result && result.status === 'success') {
333
- // Return structured object for grouping
334
- return {
335
- testName,
336
- location: failureLoc,
337
- sourceCode: sourceCodeContent,
338
- fix: result.fix,
339
- status: 'success'
340
- };
341
- }
342
- // Return original result so main() can check for QUOTA_EXCEEDED
343
- return result || { status: 'error', detail: 'UNKNOWN' };
344
- } catch (error) {
345
- return { status: 'error', detail: error.message };
346
- }
347
- }
348
-
349
- // ... (extractFailureLocation and printDetailedFix remain mostly same, just slight tweaks for robustness) ...
350
- // Actually, I need to include them in the replace or user 'multi_replace' but this is 'replace_file' so I must provide full content or precise chunks.
351
- // To avoid massive token usage, I will use the existing helper functions but just update Main.
352
-
353
- // WAIT, I must provide the implementations of extractFailureLocation and printDetailedFix if I replace the whole file or large chunks.
354
- // The previous tool call view_file shows I have the whole content. I will rewrite the whole file to be safe and clean.
355
-
356
- function extractFailureLocation(logText) {
357
- if (!logText) return null;
358
- const loc = {
359
- specFile: null,
360
- testLine: null,
361
- rootFile: null,
362
- fullRootPath: null,
363
- rootLine: null,
364
- stepLine: null
365
- };
366
-
367
- const projectName = path.basename(process.cwd());
368
-
369
- // Updated regex to be more flexible with arrows and spaces, and support .cy/.py files
370
- const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(.*?\.(?:spec|cy|py)\.(?:ts|js|py)?):(\d+):(\d+)/m) ||
371
- logText.match(/^(.*?\.py):(\d+):/m); // Pytest direct format
372
-
373
- if (testMatch) {
374
- loc.specFile = testMatch[1];
375
- loc.testLine = testMatch[2];
376
- }
377
-
378
- // Stack Trace Regex - Modified to handle Cypress URLs, webpack paths, and Python "File" entries
379
- const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?|File "(.+?)", line (\d+)/g;
380
- let match;
381
- let foundRoot = false;
382
-
383
- while ((match = stackRegex.exec(logText)) !== null) {
384
- let file = match[1] || match[4];
385
- const line = match[2] || match[5];
386
-
387
- if (!file) continue;
388
-
389
- // Clean Cypress/Browser URLs to just the relative path if possible
390
- if (file.includes('__cypress/runner')) continue;
391
-
392
- if (file.includes('webpack:///')) {
393
- file = file.split('webpack:///')[1];
394
- } else if (file.includes('webpack://')) {
395
- file = file.split('webpack://')[1];
396
- }
397
-
398
- // Strip project name if it's the first segment (common in Cypress/Webpack logs)
399
- file = file.replace(/^\.\//, '');
400
- if (file.startsWith(projectName + '/')) {
401
- file = file.substring(projectName.length + 1);
402
- }
403
- file = file.replace(/^\.\//, '');
404
-
405
- if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner') && !file.includes('python')) {
406
- loc.rootFile = file.split(/[/\\\\]/).pop();
407
- loc.fullRootPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
408
- loc.rootLine = line;
409
- foundRoot = true;
410
- }
411
-
412
- if (loc.specFile && file.endsWith(loc.specFile)) {
413
- loc.stepLine = line;
414
- }
415
- }
416
-
417
- // Fallback: If header regex failed but we found a root file that looks like a test
418
- if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.') || loc.rootFile.includes('.cy.') || loc.rootFile.endsWith('.py'))) {
419
- loc.specFile = loc.rootFile;
420
- loc.testLine = loc.rootLine;
421
- }
422
-
423
- if (loc.specFile || loc.rootFile) return loc;
424
- return null;
425
- }
426
-
427
- // --- COLORS Moved to top ---
428
-
429
- function printDetailedFix(fixText, location, sourceCode = null, isApplied = false) {
430
- let patches = [];
431
- let explanation = null;
432
-
433
- try {
434
- // Strip markdown if present
435
- const cleaned = fixText.replace(/```json\n?/, '').replace(/```/, '').trim();
436
- const parsed = JSON.parse(cleaned);
437
- explanation = parsed.explanation || parsed.reason;
438
- if (parsed.patches && Array.isArray(parsed.patches)) {
439
- patches = parsed.patches;
440
- } else if (parsed.code) {
441
- patches = [{
442
- file: location ? (location.specFile || location.rootFile) : 'unknown',
443
- line: location ? (location.testLine || location.rootLine) : 0,
444
- action: 'REPLACE',
445
- new_line: parsed.code
446
- }];
447
- }
448
- } catch (e) {
449
- // If it looks like JSON but couldn't be parsed, try harder or fallback
450
- if (fixText.trim().startsWith('{')) {
451
- try {
452
- const match = fixText.match(/\{[\s\S]*\}/);
453
- if (match) {
454
- const parsed = JSON.parse(match[0]);
455
- explanation = parsed.explanation || parsed.reason;
456
- patches = parsed.patches || (parsed.code ? [{ file: 'target', line: 0, action: 'REPLACE', new_line: parsed.code }] : []);
457
- }
458
- } catch (e2) {}
459
- }
460
-
461
- if (patches.length === 0) {
462
- patches = [{
463
- file: location ? (location.specFile || location.rootFile) : 'unknown',
464
- line: location ? (location.testLine || location.rootLine) : 0,
465
- action: 'REPLACE',
466
- new_line: fixText
467
- }];
468
- }
469
- }
470
-
471
- console.log("\n" + C.GRAY + "─".repeat(50) + C.RESET);
472
-
473
- if (isApplied) {
474
- console.log(`${C.GREEN}${C.BRIGHT}āœ… FIX APPLIED:${C.RESET}`);
475
- if (location) {
476
- const fileLabel = location.specFile || location.rootFile;
477
- let lineLabel = location.testLine || location.rootLine;
478
- if (fileLabel) {
479
- console.log(`${C.BRIGHT}šŸ“„ File:${C.RESET} ${fileLabel}:${lineLabel}`);
480
- }
481
- }
482
-
483
- if (explanation) {
484
- console.log(`\n${C.BRIGHT}ā„¹ļø Reason:${C.RESET} ${explanation}`);
485
- }
486
-
487
- for (const patch of patches) {
488
- console.log(C.GRAY + "─".repeat(20) + C.RESET);
489
- console.log(`${C.BRIGHT}šŸ“„ File:${C.RESET} ${path.basename(patch.file)}:${patch.line} [${patch.action}]`);
490
- console.log(`\n${C.GREEN}${C.BRIGHT}🟢 NEW:${C.RESET}\n ${C.GREEN}${patch.new_line.trim()}${C.RESET}`);
491
- }
492
- } else {
493
- console.log(`${C.CYAN}${C.BRIGHT}šŸ’” SUGGESTED FIX:${C.RESET}`);
494
- if (explanation) {
495
- console.log(`\n${C.BRIGHT}ā„¹ļø Reason:${C.RESET} ${explanation}`);
496
- }
497
- for (const patch of patches) {
498
- console.log(` - ${C.CYAN}${path.basename(patch.file)}:${patch.line}${C.RESET}: ${patch.new_line.trim()}`);
499
- }
500
- }
501
- }
502
-
503
- /**
504
- * Core analysis engine for batch mode.
505
- * Enforces tier limits and calculates deduplicated results.
506
- */
507
- async function runDoctor(argv) {
508
- console.log(`\n${C.BRIGHT}šŸ‘Øā€āš•ļø DeFlake Doctor - Diagnostic Tool${C.RESET}\n`);
509
-
510
- // 1. Environment Info & Version Check
511
- console.log(`${C.BRIGHT}Checking Environment:${C.RESET}`);
512
- console.log(` - DeFlake version: ${C.CYAN}${pkg.version}${C.RESET}`);
513
-
514
- // Check for updates from npm
515
- try {
516
- const https = require('https');
517
- const latestVersion = await new Promise((resolve, reject) => {
518
- const req = https.get('https://registry.npmjs.org/deflake/latest', { timeout: 3000 }, (res) => {
519
- let data = '';
520
- res.on('data', chunk => data += chunk);
521
- res.on('end', () => {
522
- try {
523
- resolve(JSON.parse(data).version);
524
- } catch (e) { resolve(null); }
525
- });
75
+ const files = fs.readdirSync(dir, { recursive: true });
76
+ files.forEach(f => {
77
+ const fullPath = path.join(dir, f);
78
+ if (f.endsWith('error-context.md')) {
79
+ results.push({ name: path.basename(path.dirname(fullPath)), htmlPath: fullPath });
80
+ }
526
81
  });
527
- req.on('error', () => resolve(null));
528
- req.on('timeout', () => { req.destroy(); resolve(null); });
529
- });
530
-
531
- if (latestVersion && latestVersion !== pkg.version) {
532
- // Compare versions
533
- const current = pkg.version.split('.').map(Number);
534
- const latest = latestVersion.split('.').map(Number);
535
- const isOutdated = latest[0] > current[0] ||
536
- (latest[0] === current[0] && latest[1] > current[1]) ||
537
- (latest[0] === current[0] && latest[1] === current[1] && latest[2] > current[2]);
538
-
539
- if (isOutdated) {
540
- console.log(` āš ļø ${C.YELLOW}Update available: ${pkg.version} → ${latestVersion}${C.RESET}`);
541
- console.log(` ${C.GREEN}Run: npx deflake@${latestVersion} doctor${C.RESET}`);
542
- }
543
- }
544
- } catch (e) {
545
- // Silent failure - version check is optional
546
- }
547
-
548
- console.log(` - Node.js version: ${C.CYAN}${process.version}${C.RESET}`);
549
- console.log(` - Platform: ${C.CYAN}${process.platform}${C.RESET}\n`);
550
-
551
- // 2. Framework Detection
552
- console.log(`${C.BRIGHT}Detecting Frameworks:${C.RESET}`);
553
- const activeFramework = DeFlakeClient.detectFramework();
82
+ } catch (e) {}
83
+ });
84
+ return results;
85
+ }
554
86
 
555
- if (activeFramework !== 'generic') {
556
- const capitalized = activeFramework.charAt(0).toUpperCase() + activeFramework.slice(1);
557
- console.log(` āœ… Active: ${C.GREEN}${C.BRIGHT}${capitalized}${C.RESET}`);
558
- } else {
559
- console.log(` āš ļø ${C.YELLOW}No supported frameworks detected in the current directory.${C.RESET}`);
560
- console.log(` (Checked for Playwright, Cypress, WebdriverIO, and Selenium Python files)`);
87
+ async function analyzeAndFix(artifacts, client, argv) {
88
+ if (artifacts.length === 0) {
89
+ console.log(` ${C.YELLOW}āš ļø No failure artifacts detected. Run 'npx deflake doctor' to check permissions.${C.RESET}`);
90
+ return 0;
561
91
  }
562
92
 
563
- // 2b. Deep Structure Detection
564
- const structures = [];
565
- const checkDir = (dirs) => dirs.find(d => fs.existsSync(d) && fs.statSync(d).isDirectory());
93
+ console.log(`šŸ” Analyzing ${artifacts.length} failure(s)...`);
94
+ let count = 0;
566
95
 
567
- const pomDir = checkDir(['pages', 'page-objects', 'po']);
568
- if (pomDir) structures.push(`${C.CYAN}POM${C.RESET} (${pomDir}/)`);
569
-
570
- const utilsDir = checkDir(['utils', 'helpers', 'support/utils', 'tests/utils']);
571
- if (utilsDir) structures.push(`${C.CYAN}Utils/Helpers${C.RESET} (${utilsDir}/)`);
572
-
573
- const fixturesDir = checkDir(['fixtures', 'data', 'tests/fixtures', 'cypress/fixtures']);
574
- if (fixturesDir) structures.push(`${C.CYAN}Fixtures${C.RESET} (${fixturesDir}/)`);
575
-
576
- if (structures.length > 0) {
577
- console.log(` šŸ” Structure: ${structures.join(', ')}`);
578
- console.log(` ${C.GRAY}(DeFlake will prioritize analyzing these folders for robust locator fixes)${C.RESET}`);
579
- } else {
580
- console.log(` āš ļø ${C.YELLOW}No standard PageObject or Utils folders detected.${C.RESET}`);
581
- console.log(` ${C.GRAY}(DeFlake works best when it can find your reusable locators in POM or Utils)${C.RESET}`);
582
- }
583
-
584
- if (fs.existsSync('package.json')) {
96
+ for (const art of artifacts) {
97
+ process.stdout.write(`ā³ Analyzing ${art.name}...`);
585
98
  try {
586
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
587
- const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
588
- const detectedDeps = [];
589
-
590
- // Core frameworks
591
- if (deps['@playwright/test']) detectedDeps.push(`${C.CYAN}@playwright/test${C.RESET}`);
592
- if (deps['cypress']) detectedDeps.push(`${C.CYAN}cypress${C.RESET}`);
593
- if (deps['webdriverio'] || deps['@wdio/cli']) detectedDeps.push(`${C.CYAN}webdriverio${C.RESET}`);
594
-
595
- // Common plugins
596
- if (deps['dotenv']) detectedDeps.push('dotenv');
597
- if (deps['typescript']) detectedDeps.push('typescript');
598
-
599
- if (detectedDeps.length > 0) {
600
- console.log(` šŸ“¦ Installed: ${detectedDeps.join(', ')}`);
99
+ const content = fs.readFileSync(art.htmlPath, 'utf8');
100
+ const loc = extractLoc(content);
101
+ const source = loc && fs.existsSync(loc.path) ? fs.readFileSync(loc.path, 'utf8') : null;
102
+
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
+ }
108
+ process.stdout.write(`\rāœ… Analysis complete for ${art.name}\n`);
109
+ printFix(res.fix, loc);
601
110
  }
602
111
  } catch (e) {
603
- console.log(` āŒ ${C.RED}Error reading package.json${C.RESET}`);
604
- }
605
- }
606
- console.log("");
607
-
608
- // 3. .env File Format Validation
609
- console.log(`${C.BRIGHT}Checking .env Configuration:${C.RESET}`);
610
- if (fs.existsSync('.env')) {
611
- const envContent = fs.readFileSync('.env', 'utf8');
612
- const lines = envContent.split('\n');
613
- let hasIssues = false;
614
-
615
- for (const line of lines) {
616
- const trimmed = line.trim();
617
- if (!trimmed || trimmed.startsWith('#')) continue;
618
-
619
- // Check for 'export' prefix (shell syntax, not .env syntax)
620
- if (trimmed.startsWith('export ')) {
621
- console.log(` āŒ ${C.RED}Invalid format: 'export' prefix detected${C.RESET}`);
622
- console.log(` ${C.GRAY}Found:${C.RESET} ${trimmed.substring(0, 50)}...`);
623
- console.log(` ${C.GREEN}Fix:${C.RESET} Remove 'export ' - .env files use: KEY=value`);
624
- hasIssues = true;
625
- }
626
-
627
- // Check for quoted values (can cause issues with some parsers)
628
- if (trimmed.match(/^[A-Z_]+=["'].*["']$/)) {
629
- console.log(` āš ļø ${C.YELLOW}Quotes detected in value (may cause issues)${C.RESET}`);
630
- console.log(` ${C.GRAY}Found:${C.RESET} ${trimmed.substring(0, 50)}...`);
631
- console.log(` ${C.GREEN}Tip:${C.RESET} Try without quotes: KEY=value`);
632
- hasIssues = true;
633
- }
112
+ console.log(`\n āŒ ${C.RED}Error: ${e.message}${C.RESET}`);
634
113
  }
635
-
636
- if (!hasIssues) {
637
- console.log(` āœ… .env file format looks correct`);
638
- }
639
- } else {
640
- console.log(` āš ļø ${C.YELLOW}No .env file found in current directory${C.RESET}`);
641
- console.log(` Create one with: echo 'DEFLAKE_API_KEY=your_key' > .env`);
642
- }
643
- console.log("");
644
-
645
- // 4. API Key Validation
646
- console.log(`${C.BRIGHT}Validating API Key:${C.RESET}`);
647
- const apiKey = process.env.DEFLAKE_API_KEY;
648
- if (apiKey) {
649
- const maskedKey = apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 8)) + apiKey.substring(apiKey.length - 4);
650
- console.log(` āœ… DEFLAKE_API_KEY found: ${C.GREEN}${maskedKey}${C.RESET}`);
651
- } else {
652
- console.log(` āŒ ${C.RED}DEFLAKE_API_KEY is missing from your environment.${C.RESET}`);
653
- console.log(` Fix: Run 'export DEFLAKE_API_KEY=your_key' or add it to your .env file.`);
654
- }
655
- console.log("");
656
-
657
- // 5. API Connectivity
658
- console.log(`${C.BRIGHT}Checking Connectivity:${C.RESET}`);
659
- const client = new DeFlakeClient(argv.apiUrl, apiKey);
660
-
661
- try {
662
- process.stdout.write(` ā³ Pinging DeFlake API... `);
663
- const result = await client.getUsage();
664
-
665
- if (result.status === 'success') {
666
- const usage = result.data;
667
- process.stdout.write(`\r āœ… API Connected! (Tier: ${C.GREEN}${usage.tier.toUpperCase()}${C.RESET}) \n`);
668
- const u = usage;
669
- const remaining = u.limit - u.usage;
670
- console.log(` Fix Quota: ${C.GREEN}${remaining}/${u.limit} remaining${C.RESET}`);
671
- console.log(` Analysis: ${C.GREEN}FREE${C.RESET} (Pay-per-fix model enabled)`);
672
- } else {
673
- process.stdout.write(`\r āŒ ${C.RED}API Connectivity Failed${C.RESET} \n`);
674
- if (result.code === 401 || result.code === 403) {
675
- console.log(` Reason: API Key is invalid (Status ${result.code}).`);
676
- } else {
677
- console.log(` Reason: ${result.message} (Code: ${result.code || 'UNKNOWN'})`);
678
- }
679
- console.log(` API URL: ${client.apiUrl}`);
680
- }
681
- } catch (error) {
682
- process.stdout.write(`\r āŒ ${C.RED}API Connectivity Error: ${error.message}${C.RESET}\n`);
683
114
  }
684
- console.log("");
685
-
686
- console.log(`${C.BRIGHT}Summary:${C.RESET}`);
687
- if (apiKey && activeFramework !== 'generic') {
688
- console.log(` ✨ ${C.GREEN}${C.BRIGHT}You are ready to use DeFlake!${C.RESET}`);
689
- } else {
690
- console.log(` āš ļø ${C.YELLOW}Please address the issues above to ensure DeFlake works correctly.${C.RESET}`);
691
- }
692
- console.log("\n" + C.GRAY + "─".repeat(50) + C.RESET + "\n");
115
+ return count;
693
116
  }
694
117
 
695
- async function applySelfHealing(result) {
696
- if (!result.location || !result.location.fullRootPath || !result.fix) return;
697
-
118
+ async function applyFix(res, loc) {
119
+ if (!loc?.path) return false;
698
120
  try {
699
- const filePath = result.location.fullRootPath;
700
- const targetLine = parseInt(result.location.rootLine);
701
-
702
- if (!fs.existsSync(filePath)) {
703
- console.error(` āŒ [Self-Healing] File not found: ${filePath}`);
704
- return;
705
- }
706
-
707
- let patches = [];
708
- try {
709
- const parsed = JSON.parse(result.fix);
710
- if (parsed.patches && Array.isArray(parsed.patches)) {
711
- patches = parsed.patches;
712
- } else if (parsed.code) {
713
- patches = [{
714
- file: filePath,
715
- line: targetLine,
716
- action: 'REPLACE',
717
- new_line: parsed.code
718
- }];
719
- }
720
- } catch (e) {
721
- // Fallback for raw string fixes
722
- patches = [{
723
- file: filePath,
724
- line: targetLine,
725
- action: 'REPLACE',
726
- new_line: result.fix
727
- }];
728
- }
729
-
730
- const backups = new Map();
121
+ const patches = JSON.parse(res.fix).patches || [];
122
+ const original = fs.readFileSync(loc.path, 'utf8');
123
+ let content = original;
731
124
 
732
- try {
733
- for (const patch of patches) {
734
- const pFile = patch.file || filePath;
735
- const pLine = parseInt(patch.line || targetLine);
736
- const pAction = patch.action || 'REPLACE';
737
- let pNew = patch.new_line;
738
-
739
- if (!fs.existsSync(pFile)) continue;
740
-
741
- // Create backup if not already done
742
- if (!backups.has(pFile)) {
743
- backups.set(pFile, fs.readFileSync(pFile, 'utf8'));
125
+ for (const p of patches) {
126
+ const lines = content.split('\n');
127
+ const idx = p.line - 1;
128
+ if (idx >= 0 && idx < lines.length) {
129
+ const indent = lines[idx].match(/^\s*/)[0];
130
+ 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;
744
136
  }
745
137
 
746
- const currentLines = fs.readFileSync(pFile, 'utf8').split('\n');
747
- const originalLineIndex = pLine - 1;
748
-
749
- if (originalLineIndex < 0 || originalLineIndex >= currentLines.length) continue;
750
-
751
- const originalLine = currentLines[originalLineIndex];
752
- const indentation = originalLine.match(/^\s*/)[0];
753
-
754
- // CRITICAL: Prevent injecting JSON metadata into the code
755
- if (pNew.trim().startsWith('{') && pNew.includes('"patches"') && pNew.includes(':')) {
756
- console.log(` āŒ ${C.RED}[Self-Healing] Detected attempted JSON injection. Aborting patch for ${path.basename(pFile)}.${C.RESET}`);
757
- continue;
758
- }
759
-
760
- const finalNewLine = indentation + pNew.trim();
761
-
762
- // DUPLICATE PROTECTION: Don't insert/replace if code already exists
763
- if (currentLines.some(l => l.trim() === pNew.trim())) {
764
- console.log(` ā„¹ļø ${C.GRAY}[Self-Healing] Skipping duplicate patch for:${C.RESET} ${path.basename(pFile)}`);
765
- continue;
766
- }
767
-
768
- if (pAction === 'INSERT_AFTER') {
769
- currentLines.splice(pLine, 0, finalNewLine);
770
- fs.writeFileSync(pFile, currentLines.join('\n'));
771
- console.log(` āœ… ${C.GREEN}[Self-Healing] Successfully inserted at:${C.RESET} ${path.basename(pFile)}:${pLine}`);
772
- } else {
773
- currentLines[originalLineIndex] = finalNewLine;
774
- fs.writeFileSync(pFile, currentLines.join('\n'));
775
- console.log(` āœ… ${C.GREEN}[Self-Healing] Successfully patched:${C.RESET} ${path.basename(pFile)}:${pLine}`);
776
- }
777
-
778
- // SYNTAX VALIDATION: Rollback if syntax is broken
779
- if (pFile.endsWith('.ts') || pFile.endsWith('.js')) {
780
- try {
781
- const { execSync } = require('child_process');
782
- execSync(`node --check "${pFile}"`, { stdio: 'ignore' });
783
- } catch (err) {
784
- console.log(` āš ļø ${C.YELLOW}[Self-Healing] Patch broke syntax in ${path.basename(pFile)}. Rolling back...${C.RESET}`);
785
- fs.writeFileSync(pFile, backups.get(pFile));
786
- }
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');
787
142
  }
788
143
  }
789
- } catch (patchError) {
790
- console.error(` āŒ ${C.RED}[Self-Healing] Error applying patches: ${patchError.message}${C.RESET}`);
791
- // Emergency rollback for all involved files
792
- for (const [file, content] of backups) {
793
- fs.writeFileSync(file, content);
794
- }
795
144
  }
796
- } catch (error) {
797
- console.error(` āŒ ${C.RED}[Self-Healing] Error in self-healing logic:${C.RESET} ${error.message}`);
798
- }
799
- }
800
-
801
- /**
802
- * Automagically opens the framework's native HTML report.
803
- */
804
- function showFrameworkReport() {
805
- const framework = DeFlakeClient.detectFramework();
806
- console.log(`\n${C.CYAN}šŸ“Š Opening HTML Report for ${framework.toUpperCase()}...${C.RESET}`);
807
-
808
- let command = '';
809
- const opener = process.platform === 'win32' ? 'start' : 'open';
810
-
811
- if (framework === 'playwright') {
812
- // Playwright default report port is 9323. Try to close previous instance if on Mac/Linux
145
+
146
+ fs.writeFileSync(loc.path, content);
147
+
148
+ // 🧪 SMARTER SYNTAX CHECK
813
149
  try {
814
- if (process.platform !== 'win32') {
815
- const pid = require('child_process').execSync('lsof -t -i:9323').toString().trim();
816
- if (pid) {
817
- require('child_process').execSync(`kill ${pid}`);
818
- }
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");
819
153
  }
820
- } catch (e) {}
821
- command = 'npx playwright show-report';
822
- } else if (framework === 'cypress') {
823
- // Broad search for common Cypress HTML reports
824
- const candidates = [
825
- 'cypress/reports/html/index.html',
826
- 'cypress/reports/index.html',
827
- 'mochawesome-report/mochawesome.html',
828
- 'reports/index.html'
829
- ];
830
-
831
- const found = candidates.find(p => fs.existsSync(p));
832
- if (found) {
833
- command = `${opener} ${found}`;
834
- } else {
835
- console.log(`${C.GRAY}ā„¹ļø Cypress HTML report not found. Verify your reporter configuration (e.g. mochawesome).${C.RESET}`);
836
- console.log(` ${C.GRAY}Looked in: ${candidates.join(', ')}${C.RESET}`);
837
- return;
154
+ return true;
155
+ } catch(e) {
156
+ fs.writeFileSync(loc.path, original);
157
+ return false;
838
158
  }
839
- } else if (framework === 'webdriverio') {
840
- // WDIO usually uses Allure or spec-reporter
841
- if (fs.existsSync('allure-results')) {
842
- console.log(` ${C.GRAY}Allure results detected. Attempting to serve...${C.RESET}`);
843
- command = 'npx allure serve allure-results';
844
- } else if (fs.existsSync('reports/html/index.html')) {
845
- command = `${opener} reports/html/index.html`;
846
- } else {
847
- console.log(`${C.GRAY}ā„¹ļø WebdriverIO HTML report not found. If you use Allure, ensure allure-results folder exists.${C.RESET}`);
848
- return;
849
- }
850
- }
851
-
852
- if (command) {
853
- // Use inherit if it's a server (like allure serve) or a long-running process
854
- const isServer = command.includes('serve') || command.includes('show-report');
855
- spawn(command, { shell: true, stdio: isServer ? 'inherit' : 'ignore' });
856
- }
159
+ } catch(e) { return false; }
857
160
  }
858
161
 
859
- async function analyzeFailures(artifacts, fullLog, client) {
860
- if (artifacts.length === 0) {
861
- console.log("āš ļø No error artifacts found.");
862
- return;
863
- }
864
-
865
- // 1. Check Quota / Tier
866
- const result = await client.getUsage();
867
- let limit = 100; // Default safety cap
868
- let tier = 'unknown';
869
-
870
- if (result.status === 'success') {
871
- const usage = result.data;
872
- tier = usage.tier || 'free';
873
- if (tier === 'free') limit = 10;
874
- else if (tier === 'pro') limit = 50;
875
- else if (tier === 'master' || tier === 'byok') limit = 100;
876
-
877
- console.log(`šŸŽ« Subscription: ${tier.toUpperCase()} | Monthly Usage: ${usage.usage}/${usage.limit}`);
878
- }
879
-
880
- if (artifacts.length > limit) {
881
- console.log(`${C.YELLOW}āš ļø Detected ${artifacts.length} failures, but your ${tier} plan limits batch analysis to ${limit} unique fixes.${C.RESET}`);
882
- console.log(` (Processing the first ${limit}...)\n`);
883
- }
884
-
885
- const framework = DeFlakeClient.detectFramework();
886
- const results = [];
887
- const processLimit = Math.min(artifacts.length, limit);
888
- const batchArtifacts = artifacts.slice(0, processLimit);
889
-
890
- console.log(`šŸ” Analyzing ${batchArtifacts.length} failure(s)...`);
891
-
892
- const playwrightBlocks = (framework === 'playwright') ? parsePlaywrightLogs(fullLog || "") : [];
893
- const cypressBlocks = (framework === 'cypress') ? parseCypressLogs(fullLog || "") : [];
894
- const pytestBlocks = (framework === 'selenium-python') ? parsePytestLogs(fullLog || "") : [];
895
-
896
- for (const art of batchArtifacts) {
897
- let specificLog = fullLog;
898
-
899
- if (framework === 'cypress' && cypressBlocks.length > 0) {
900
- const match = cypressBlocks.find(b => art.htmlPath && art.htmlPath.includes(b.spec));
901
- if (match) specificLog = match.content;
902
- } else if (framework === 'playwright' && playwrightBlocks.length > 0) {
903
- // Heuristic matching for Playwright error blocks
904
- let bestMatch = null;
905
- let bestScore = -1;
906
- const artifactTokens = art.name.toLowerCase().replace(/[-_]/g, ' ').split(' ').filter(w => w.length > 3);
907
-
908
- for (const block of playwrightBlocks) {
909
- let score = 0;
910
- const blockLower = block.content.toLowerCase();
911
- for (const token of artifactTokens) { if (blockLower.includes(token)) score++; }
912
- if (score > bestScore) { bestScore = score; bestMatch = block; }
913
- }
914
- if (bestMatch && bestScore > 0) {
915
- specificLog = bestMatch.content;
916
- }
917
- } else if (framework === 'selenium-python' && pytestBlocks.length > 0) {
918
- // Match pytest block by test name heuristic
919
- const match = pytestBlocks.find(b => art.name && b.name.includes(art.name.replace('.png', '')));
920
- if (match) specificLog = match.content;
921
- }
922
-
923
- const displayName = (art.name || 'Unknown Artifact').substring(0, 40);
924
- process.stdout.write(`\rā³ Analyzing ${displayName}... `);
925
-
926
- try {
927
- const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name, argv.fix);
928
-
929
- if (result && result.status === 'success') {
930
- results.push(result);
931
- } else {
932
- console.log(`\n ${C.RED}āŒ Analysis failed for ${displayName}:${C.RESET} ${result?.detail || 'Check server status'}`);
933
- }
934
- } catch (err) {
935
- console.log(`\n ${C.RED}āŒ Error analyzing ${displayName}:${C.RESET} ${err.message}`);
936
- }
937
- }
938
- console.log("\rāœ… Analysis complete. ");
939
-
940
- // GROUPING & PRINTING
941
- const groups = {};
942
- for (const res of results) {
943
- let fixCode = res.fix;
944
- try {
945
- const p = JSON.parse(res.fix);
946
- if (p.code) fixCode = p.code;
947
- } catch (e) { }
948
-
949
- const locId = res.location ? `${res.location.rootFile}:${res.location.rootLine}` : `unknown-${res.testName}`;
950
- const key = `${locId}|${fixCode.trim()}`;
951
-
952
- if (!groups[key]) {
953
- groups[key] = { ...res, count: 0, locations: [] };
954
- }
955
- groups[key].count++;
956
- if (res.location) {
957
- groups[key].locations.push(res.location);
958
- }
959
- }
960
-
961
- const finalGroups = Object.values(groups);
962
- for (const group of finalGroups) {
963
- if (group.count > 1) {
964
- console.log(`${C.GRAY}ā„¹ļø Suggested for ${group.count} similar failures:${C.RESET}`);
965
- }
966
-
967
- // APPLY FIX (To all instances in the group)
968
- if (argv.fix) {
969
- for (const loc of group.locations) {
970
- await applySelfHealing({ ...group, location: loc });
971
- }
972
- }
973
-
974
- printDetailedFix(group.fix, group.location, group.source_code, argv.fix);
975
- }
976
-
977
- // SUMMARY
978
- console.log(`\n${C.BRIGHT}šŸ“Š DeFlake Summary:${C.RESET}`);
979
- console.log(` - Failures analyzed: ${batchArtifacts.length}`);
980
- console.log(` - Fixes suggested: ${results.length}`);
981
-
982
- try {
983
- // Fetch updated usage for clearer reporting
984
- const updatedUsage = await client.getUsage();
985
- if (updatedUsage.status === 'success') {
986
- const u = updatedUsage.data;
987
- const remaining = u.limit - u.usage;
988
- console.log(` - Fix Quota: ${C.GREEN}${remaining}/${u.limit} remaining (Analysis is FREE)${C.RESET}`);
989
- }
990
- } catch (e) {
991
- // usage fetch failed is non-critical
992
- }
993
-
994
- if (results.length === 0) {
995
- console.log(`${C.GRAY}ā„¹ļø DeFlake analyzed the logs but couldn't find a confident fix for these errors.${C.RESET}`);
996
- if (!fullLog) {
997
- console.log(`${C.YELLOW}āš ļø Tip: Ensure your test runner output is being captured correctly.${C.RESET}`);
998
- }
999
- } else if (!argv.fix) {
1000
- console.log(`\n${C.BRIGHT}šŸ’” Tip: Use ${C.CYAN}--fix${C.RESET}${C.BRIGHT} to automatically apply these suggested fixes next time.${C.RESET}`);
1001
- }
1002
-
1003
-
1004
- return results.length;
162
+ 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;
1005
165
  }
1006
166
 
1007
- /**
1008
- * Helper to run a shell command and capture output.
1009
- */
1010
- async function runCommand(cmd, args) {
1011
- return new Promise((resolve) => {
1012
- console.log(`šŸš€ Running command: ${cmd} ${args.join(' ')}`);
1013
- const child = spawn(cmd, args, { shell: true, stdio: 'pipe' });
1014
- let stdout = '';
1015
- let stderr = '';
1016
-
1017
- child.stdout.on('data', (data) => { process.stdout.write(data); stdout += data.toString(); });
1018
- child.stderr.on('data', (data) => { process.stderr.write(data); stderr += data.toString(); });
1019
-
1020
- child.on('close', (code) => {
1021
- resolve({ code, output: stdout + "\n" + stderr });
1022
- });
1023
- });
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) {}
1024
173
  }
1025
174
 
1026
- async function main() {
1027
- const command = argv._ || [];
1028
- let finalExitCode = 0;
175
+ 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;
1029
179
 
1030
- // If 'doctor' or 'migrate' was called, don't proceed to wrapper logic
1031
- if (command.includes('doctor') || command.includes('migrate')) return;
1032
-
1033
- console.log(`šŸš‘ DeFlake JS Client (Batch Mode) - ${C.CYAN}v${pkg.version}${C.RESET}`);
1034
- const client = new DeFlakeClient(argv.apiUrl);
1035
- const fw = client.framework.charAt(0).toUpperCase() + client.framework.slice(1);
1036
- console.log(`šŸ” Detected Framework: ${C.GREEN}${fw}${C.RESET}`);
1037
-
1038
- if (command.length > 0) {
1039
- const cmd = command[0];
1040
- const args = command.slice(1);
1041
-
1042
- let { code, output } = await runCommand(cmd, args);
1043
-
1044
- if (code !== 0) {
1045
- console.log(`\nšŸ”“ Command failed with code ${code}. Activating DeFlake...`);
1046
- const artifacts = detectAllArtifacts(null, argv.html);
1047
- const fixesApplied = await analyzeFailures(artifacts, output, client);
1048
-
1049
- let outcomeCode = code;
1050
- // AUTO-VERIFICATION
1051
- if (fixesApplied > 0 && argv.fix) {
1052
- console.log(`\n${C.BRIGHT}šŸ’‰ Fixes applied. Re-running tests to verify...${C.RESET}`);
1053
- const secondRun = await runCommand(cmd, args);
1054
- outcomeCode = secondRun.code;
1055
- if (secondRun.code === 0) {
1056
- console.log(`\n${C.GREEN}${C.BRIGHT}āœ… All tests passed after DeFlake healing!${C.RESET}`);
1057
- } else {
1058
- console.log(`\n${C.YELLOW}āš ļø Some tests still failing after fixes. Check the report for details.${C.RESET}`);
180
+ for (const d of dirs) {
181
+ if (!fs.existsSync(d)) continue;
182
+ process.stdout.write(` Checking ${d.padEnd(20)}... `);
183
+ 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)));
1059
189
  }
1060
- }
1061
- finalExitCode = outcomeCode;
1062
- } else {
1063
- console.log("\n🟢 Command passed successfully.");
1064
- finalExitCode = 0;
190
+ };
191
+ checkDeep(d);
192
+ console.log(`${C.GREEN}Healthy${C.RESET}`);
193
+ } catch (e) {
194
+ console.log(`${C.RED}Access Denied${C.RESET}`);
195
+ error = true;
1065
196
  }
1066
- } else {
1067
- const artifacts = detectAllArtifacts(argv.log, argv.html);
1068
- const fullLog = argv.log && fs.existsSync(argv.log) ? fs.readFileSync(argv.log, 'utf8') : null;
1069
- await analyzeFailures(artifacts, fullLog, client);
1070
197
  }
1071
198
 
1072
- // FINAL REPORT TRIGGER
1073
- if (argv.report) {
1074
- showFrameworkReport();
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}`);
203
+ } else {
204
+ console.log(`\n ✨ ${C.GREEN}All checks passed. System is ready.${C.RESET}`);
1075
205
  }
1076
-
1077
- process.exit(finalExitCode);
1078
206
  }
1079
207
 
1080
- // main() call is now handled by the check above
208
+ function showReport() {
209
+ spawn('npx playwright show-report', { shell: true, stdio: 'inherit' }).on('error', () => {});
210
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.2.13",
3
+ "version": "1.2.17",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {