deflake 1.0.2 → 1.0.4

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 (3) hide show
  1. package/cli.js +239 -7
  2. package/client.js +36 -7
  3. package/package.json +2 -2
package/cli.js CHANGED
@@ -7,26 +7,55 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const pkg = require('./package.json');
9
9
 
10
- const argv = yargs(hideBin(process.argv))
10
+ // --- DETERMINISTIC COMMAND INTERCEPTION ---
11
+ // Check for diagnostic commands before yargs or main logic even starts
12
+ const rawArgs = process.argv.slice(2);
13
+ if (rawArgs.includes('doctor') || rawArgs.includes('--doctor')) {
14
+ // We run the doctor logic and force exit to prevent any wrapper loops
15
+ const doctorArgv = yargs(hideBin(process.argv)).argv;
16
+ runDoctor(doctorArgv).then(() => process.exit(0)).catch(() => process.exit(1));
17
+ return; // Safety for some environments
18
+ }
19
+ // ------------------------------------------
20
+
21
+ const parser = yargs(hideBin(process.argv))
11
22
  .option('log', {
12
23
  alias: 'l',
13
24
  type: 'string',
14
25
  description: 'Path to the error log file',
15
- demandOption: false // No longer strictly required if running in wrapper mode
26
+ demandOption: false
16
27
  })
17
28
  .option('html', {
18
29
  alias: 'h',
19
30
  type: 'string',
20
31
  description: 'Path to the HTML snapshot',
21
- demandOption: false // No longer strictly required if auto-detected
32
+ demandOption: false
22
33
  })
23
34
  .option('api-url', {
24
35
  type: 'string',
25
36
  description: 'Override Default API URL',
26
37
  })
38
+ .option('fix', {
39
+ type: 'boolean',
40
+ description: 'Automatically apply suggested fixes',
41
+ default: false
42
+ })
43
+ .option('doctor', {
44
+ type: 'boolean',
45
+ description: 'Diagnose your DeFlake installation',
46
+ default: false
47
+ })
48
+ .command('doctor', 'Diagnose your DeFlake installation', {}, (argv) => {
49
+ runDoctor(argv).then(() => process.exit(0));
50
+ })
27
51
  .version(pkg.version)
28
- .help()
29
- .argv;
52
+ .help();
53
+
54
+ const argv = parser.argv;
55
+
56
+ // If we reach this point, it means no diagnostic command (like 'doctor') was triggered.
57
+ // We proceed with the main test wrapper logic.
58
+ main();
30
59
 
31
60
  // Helper to auto-detect artifacts (Batch Mode)
32
61
  function detectAllArtifacts(providedLog, providedHtml) {
@@ -335,6 +364,200 @@ function printDetailedFix(fixText, location, sourceCode = null) {
335
364
  * Core analysis engine for batch mode.
336
365
  * Enforces tier limits and calculates deduplicated results.
337
366
  */
367
+ async function runDoctor(argv) {
368
+ const C = {
369
+ RESET: "\x1b[0m",
370
+ BRIGHT: "\x1b[1m",
371
+ GREEN: "\x1b[32m",
372
+ YELLOW: "\x1b[33m",
373
+ RED: "\x1b[31m",
374
+ CYAN: "\x1b[36m",
375
+ GRAY: "\x1b[90m"
376
+ };
377
+
378
+ console.log(`\n${C.BRIGHT}👨‍⚕️ DeFlake Doctor - Diagnostic Tool${C.RESET}\n`);
379
+
380
+ // 1. Environment Info
381
+ console.log(`${C.BRIGHT}Checking Environment:${C.RESET}`);
382
+ console.log(` - DeFlake version: ${C.CYAN}${pkg.version}${C.RESET}`);
383
+ console.log(` - Node.js version: ${C.CYAN}${process.version}${C.RESET}`);
384
+ console.log(` - Platform: ${C.CYAN}${process.platform}${C.RESET}\n`);
385
+
386
+ // 2. Framework Detection
387
+ console.log(`${C.BRIGHT}Detecting Frameworks:${C.RESET}`);
388
+ const frameworks = [];
389
+ if (fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js')) frameworks.push('Playwright');
390
+ if (fs.existsSync('cypress.config.ts') || fs.existsSync('cypress.config.js') || fs.existsSync('cypress.json')) frameworks.push('Cypress');
391
+ if (fs.existsSync('wdio.conf.ts') || fs.existsSync('wdio.conf.js')) frameworks.push('WebdriverIO');
392
+
393
+ if (frameworks.length > 0) {
394
+ console.log(` ✅ Found: ${C.GREEN}${frameworks.join(', ')}${C.RESET}`);
395
+ } else {
396
+ console.log(` ⚠️ ${C.YELLOW}No supported frameworks detected in the current directory.${C.RESET}`);
397
+ console.log(` (Checked for Playwright, Cypress, WebdriverIO files)`);
398
+ }
399
+
400
+ // 2b. Deep Structure Detection
401
+ const structures = [];
402
+ const checkDir = (dirs) => dirs.find(d => fs.existsSync(d) && fs.statSync(d).isDirectory());
403
+
404
+ const pomDir = checkDir(['pages', 'page-objects', 'po']);
405
+ if (pomDir) structures.push(`${C.CYAN}POM${C.RESET} (${pomDir}/)`);
406
+
407
+ const utilsDir = checkDir(['utils', 'helpers', 'support/utils', 'tests/utils']);
408
+ if (utilsDir) structures.push(`${C.CYAN}Utils/Helpers${C.RESET} (${utilsDir}/)`);
409
+
410
+ const fixturesDir = checkDir(['fixtures', 'data', 'tests/fixtures', 'cypress/fixtures']);
411
+ if (fixturesDir) structures.push(`${C.CYAN}Fixtures${C.RESET} (${fixturesDir}/)`);
412
+
413
+ if (structures.length > 0) {
414
+ console.log(` 🔍 Structure: ${structures.join(', ')}`);
415
+ console.log(` ${C.GRAY}(DeFlake will prioritize analyzing these folders for robust locator fixes)${C.RESET}`);
416
+ } else {
417
+ console.log(` ⚠️ ${C.YELLOW}No standard PageObject or Utils folders detected.${C.RESET}`);
418
+ console.log(` ${C.GRAY}(DeFlake works best when it can find your reusable locators in POM or Utils)${C.RESET}`);
419
+ }
420
+
421
+ if (fs.existsSync('package.json')) {
422
+ try {
423
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
424
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
425
+ const detectedDeps = [];
426
+ if (deps['@playwright/test']) detectedDeps.push('@playwright/test');
427
+ if (deps['cypress']) detectedDeps.push('cypress');
428
+ if (deps['webdriverio']) detectedDeps.push('webdriverio');
429
+
430
+ if (detectedDeps.length > 0) {
431
+ console.log(` ✅ Installed: ${C.GREEN}${detectedDeps.join(', ')}${C.RESET}`);
432
+ }
433
+ } catch (e) {
434
+ console.log(` ❌ ${C.RED}Error reading package.json${C.RESET}`);
435
+ }
436
+ }
437
+ console.log("");
438
+
439
+ // 3. .env File Format Validation
440
+ console.log(`${C.BRIGHT}Checking .env Configuration:${C.RESET}`);
441
+ if (fs.existsSync('.env')) {
442
+ const envContent = fs.readFileSync('.env', 'utf8');
443
+ const lines = envContent.split('\n');
444
+ let hasIssues = false;
445
+
446
+ for (const line of lines) {
447
+ const trimmed = line.trim();
448
+ if (!trimmed || trimmed.startsWith('#')) continue;
449
+
450
+ // Check for 'export' prefix (shell syntax, not .env syntax)
451
+ if (trimmed.startsWith('export ')) {
452
+ console.log(` ❌ ${C.RED}Invalid format: 'export' prefix detected${C.RESET}`);
453
+ console.log(` ${C.GRAY}Found:${C.RESET} ${trimmed.substring(0, 50)}...`);
454
+ console.log(` ${C.GREEN}Fix:${C.RESET} Remove 'export ' - .env files use: KEY=value`);
455
+ hasIssues = true;
456
+ }
457
+
458
+ // Check for quoted values (can cause issues with some parsers)
459
+ if (trimmed.match(/^[A-Z_]+=["'].*["']$/)) {
460
+ console.log(` ⚠️ ${C.YELLOW}Quotes detected in value (may cause issues)${C.RESET}`);
461
+ console.log(` ${C.GRAY}Found:${C.RESET} ${trimmed.substring(0, 50)}...`);
462
+ console.log(` ${C.GREEN}Tip:${C.RESET} Try without quotes: KEY=value`);
463
+ hasIssues = true;
464
+ }
465
+ }
466
+
467
+ if (!hasIssues) {
468
+ console.log(` ✅ .env file format looks correct`);
469
+ }
470
+ } else {
471
+ console.log(` ⚠️ ${C.YELLOW}No .env file found in current directory${C.RESET}`);
472
+ console.log(` Create one with: echo 'DEFLAKE_API_KEY=your_key' > .env`);
473
+ }
474
+ console.log("");
475
+
476
+ // 4. API Key Validation
477
+ console.log(`${C.BRIGHT}Validating API Key:${C.RESET}`);
478
+ const apiKey = process.env.DEFLAKE_API_KEY;
479
+ if (apiKey) {
480
+ const maskedKey = apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 8)) + apiKey.substring(apiKey.length - 4);
481
+ console.log(` ✅ DEFLAKE_API_KEY found: ${C.GREEN}${maskedKey}${C.RESET}`);
482
+ } else {
483
+ console.log(` ❌ ${C.RED}DEFLAKE_API_KEY is missing from your environment.${C.RESET}`);
484
+ console.log(` Fix: Run 'export DEFLAKE_API_KEY=your_key' or add it to your .env file.`);
485
+ }
486
+ console.log("");
487
+
488
+ // 5. API Connectivity
489
+ console.log(`${C.BRIGHT}Checking Connectivity:${C.RESET}`);
490
+ const client = new DeFlakeClient(argv.apiUrl, apiKey);
491
+ // Debug: Show what the client is using
492
+ console.log(` ${C.GRAY}(URL: ${client.apiUrl}, Key length: ${client.apiKey ? client.apiKey.length : 0})${C.RESET}`);
493
+ try {
494
+ process.stdout.write(` ⏳ Pinging DeFlake API... `);
495
+ const usage = await client.getUsage();
496
+ if (usage) {
497
+ process.stdout.write(`\r ✅ API Connected! (Tier: ${C.GREEN}${usage.tier.toUpperCase()}${C.RESET}) \n`);
498
+ } else {
499
+ process.stdout.write(`\r ❌ ${C.RED}API Connectivity Failed (Invalid Key or Server Timeout)${C.RESET} \n`);
500
+ }
501
+ } catch (error) {
502
+ process.stdout.write(`\r ❌ ${C.RED}API Connectivity Error: ${error.message}${C.RESET}\n`);
503
+ }
504
+ console.log("");
505
+
506
+ console.log(`${C.BRIGHT}Summary:${C.RESET}`);
507
+ if (apiKey && frameworks.length > 0) {
508
+ console.log(` ✨ ${C.GREEN}${C.BRIGHT}You are ready to use DeFlake!${C.RESET}`);
509
+ } else {
510
+ console.log(` ⚠️ ${C.YELLOW}Please address the issues above to ensure DeFlake works correctly.${C.RESET}`);
511
+ }
512
+ console.log("\n" + C.GRAY + "─".repeat(50) + C.RESET + "\n");
513
+ }
514
+
515
+ async function applySelfHealing(result) {
516
+ const C = {
517
+ RESET: "\x1b[0m",
518
+ GREEN: "\x1b[32m",
519
+ RED: "\x1b[31m",
520
+ GRAY: "\x1b[90m"
521
+ };
522
+
523
+ if (!result.location || !result.location.fullRootPath || !result.fix) return;
524
+
525
+ try {
526
+ const filePath = result.location.fullRootPath;
527
+ const targetLine = parseInt(result.location.rootLine);
528
+
529
+ if (!fs.existsSync(filePath)) {
530
+ console.error(` ❌ [Self-Healing] File not found: ${filePath}`);
531
+ return;
532
+ }
533
+
534
+ let fixCode = result.fix;
535
+ try {
536
+ const parsed = JSON.parse(result.fix);
537
+ if (parsed.code) fixCode = parsed.code;
538
+ } catch (e) { }
539
+
540
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n');
541
+ const originalLineIndex = targetLine - 1;
542
+
543
+ if (originalLineIndex < 0 || originalLineIndex >= lines.length) {
544
+ console.error(` ❌ [Self-Healing] Line ${targetLine} out of bounds in ${filePath}`);
545
+ return;
546
+ }
547
+
548
+ // Apply fix: Replace the entire line or the specific locator
549
+ // For robustness, we replace the whole line but keep indentation
550
+ const originalLine = lines[originalLineIndex];
551
+ const indentation = originalLine.match(/^\s*/)[0];
552
+ lines[originalLineIndex] = indentation + fixCode.trim();
553
+
554
+ fs.writeFileSync(filePath, lines.join('\n'));
555
+ console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully patched:${C.RESET} ${path.basename(filePath)}:${targetLine}`);
556
+ } catch (error) {
557
+ console.error(` ❌ ${C.RED}[Self-Healing] Error patching file:${C.RESET} ${error.message}`);
558
+ }
559
+ }
560
+
338
561
  async function analyzeFailures(artifacts, fullLog, client) {
339
562
  const C = {
340
563
  RESET: "\x1b[0m",
@@ -406,6 +629,11 @@ async function analyzeFailures(artifacts, fullLog, client) {
406
629
 
407
630
  if (result && result.status === 'success') {
408
631
  results.push(result);
632
+
633
+ // If --fix is enabled, apply it immediately for ALL tiers
634
+ if (argv.fix) {
635
+ await applySelfHealing(result);
636
+ }
409
637
  }
410
638
  }
411
639
  console.log("\r✅ Analysis complete. \n");
@@ -433,9 +661,13 @@ async function analyzeFailures(artifacts, fullLog, client) {
433
661
  }
434
662
 
435
663
  async function main() {
664
+ const command = argv._;
665
+
666
+ // If 'doctor' was called, don't proceed to wrapper logic
667
+ if (command.includes('doctor')) return;
668
+
436
669
  console.log("🚑 DeFlake JS Client (Batch Mode)");
437
670
  const client = new DeFlakeClient(argv.apiUrl);
438
- const command = argv._;
439
671
 
440
672
  if (command.length > 0) {
441
673
  const cmd = command[0];
@@ -467,4 +699,4 @@ async function main() {
467
699
  }
468
700
  }
469
701
 
470
- main();
702
+ // main() call is now handled by the check above
package/client.js CHANGED
@@ -10,23 +10,51 @@ class DeFlakeClient {
10
10
  this.productionUrl = 'https://deflake-api.up.railway.app/api/deflake';
11
11
  this.apiUrl = apiUrl || process.env.DEFLAKE_API_URL || this.productionUrl;
12
12
  this.apiKey = apiKey || process.env.DEFLAKE_API_KEY;
13
+ this.projectName = this.detectProjectName();
13
14
 
14
15
  if (!this.apiKey) {
15
- console.error("❌ DeFlake Error: DEFLAKE_API_KEY is missing.");
16
- console.error(" To get a free key, visit: http://deflake.com/register");
17
- console.error(" Or set it in your environment: export DEFLAKE_API_KEY=your_key");
18
- process.exit(1);
16
+ // We no longer exit here to allow diagnostic tools (like 'doctor') to run.
17
+ // Validation will happen when an actual API call is attempted.
18
+ }
19
+ }
20
+
21
+ detectProjectName() {
22
+ try {
23
+ // 1. Try package.json
24
+ if (fs.existsSync('package.json')) {
25
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
26
+ if (pkg.name) return pkg.name;
27
+ }
28
+ // 2. Try git remote
29
+ const { execSync } = require('child_process');
30
+ try {
31
+ const remote = execSync('git remote get-url origin', { stdio: 'pipe' }).toString().trim();
32
+ const basename = path.basename(remote, '.git');
33
+ if (basename) return basename;
34
+ } catch (e) { }
35
+ // 3. Fallback to folder name
36
+ return path.basename(process.cwd());
37
+ } catch (e) {
38
+ return 'unknown-project';
19
39
  }
20
40
  }
21
41
 
22
42
  async getUsage() {
23
43
  try {
24
- const usageUrl = this.apiUrl.replace('/deflake', '/user/usage');
44
+ // Replace /api/deflake with /api/user/usage to avoid matching domain name
45
+ const usageUrl = this.apiUrl.replace('/api/deflake', '/api/user/usage');
25
46
  const response = await axios.get(usageUrl, {
26
- headers: { 'X-API-KEY': this.apiKey }
47
+ headers: {
48
+ 'X-API-KEY': this.apiKey,
49
+ 'X-Project-Name': this.projectName
50
+ }
27
51
  });
28
52
  return response.data;
29
53
  } catch (error) {
54
+ // Log error details for debugging (only in doctor mode, based on env var)
55
+ if (process.env.DEFLAKE_DEBUG) {
56
+ console.error('getUsage error:', error.response?.status, error.response?.data || error.message);
57
+ }
30
58
  return null; // Silent failure for usage check
31
59
  }
32
60
  }
@@ -50,7 +78,8 @@ class DeFlakeClient {
50
78
  const response = await axios.post(this.apiUrl, payload, {
51
79
  headers: {
52
80
  'Content-Type': 'application/json',
53
- 'X-API-KEY': this.apiKey
81
+ 'X-API-KEY': this.apiKey,
82
+ 'X-Project-Name': this.projectName
54
83
  }
55
84
  });
56
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {
@@ -34,4 +34,4 @@
34
34
  "dotenv": "^17.2.3",
35
35
  "yargs": "^18.0.0"
36
36
  }
37
- }
37
+ }