deflake 1.0.2 → 1.0.5

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 +274 -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,235 @@ 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 & Version Check
381
+ console.log(`${C.BRIGHT}Checking Environment:${C.RESET}`);
382
+ console.log(` - DeFlake version: ${C.CYAN}${pkg.version}${C.RESET}`);
383
+
384
+ // Check for updates from npm
385
+ try {
386
+ const https = require('https');
387
+ const latestVersion = await new Promise((resolve, reject) => {
388
+ const req = https.get('https://registry.npmjs.org/deflake/latest', { timeout: 3000 }, (res) => {
389
+ let data = '';
390
+ res.on('data', chunk => data += chunk);
391
+ res.on('end', () => {
392
+ try {
393
+ resolve(JSON.parse(data).version);
394
+ } catch (e) { resolve(null); }
395
+ });
396
+ });
397
+ req.on('error', () => resolve(null));
398
+ req.on('timeout', () => { req.destroy(); resolve(null); });
399
+ });
400
+
401
+ if (latestVersion && latestVersion !== pkg.version) {
402
+ // Compare versions
403
+ const current = pkg.version.split('.').map(Number);
404
+ const latest = latestVersion.split('.').map(Number);
405
+ const isOutdated = latest[0] > current[0] ||
406
+ (latest[0] === current[0] && latest[1] > current[1]) ||
407
+ (latest[0] === current[0] && latest[1] === current[1] && latest[2] > current[2]);
408
+
409
+ if (isOutdated) {
410
+ console.log(` ⚠️ ${C.YELLOW}Update available: ${pkg.version} → ${latestVersion}${C.RESET}`);
411
+ console.log(` ${C.GREEN}Run: npx deflake@${latestVersion} doctor${C.RESET}`);
412
+ }
413
+ }
414
+ } catch (e) {
415
+ // Silent failure - version check is optional
416
+ }
417
+
418
+ console.log(` - Node.js version: ${C.CYAN}${process.version}${C.RESET}`);
419
+ console.log(` - Platform: ${C.CYAN}${process.platform}${C.RESET}\n`);
420
+
421
+ // 2. Framework Detection
422
+ console.log(`${C.BRIGHT}Detecting Frameworks:${C.RESET}`);
423
+ const frameworks = [];
424
+ if (fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js')) frameworks.push('Playwright');
425
+ if (fs.existsSync('cypress.config.ts') || fs.existsSync('cypress.config.js') || fs.existsSync('cypress.json')) frameworks.push('Cypress');
426
+ if (fs.existsSync('wdio.conf.ts') || fs.existsSync('wdio.conf.js')) frameworks.push('WebdriverIO');
427
+
428
+ if (frameworks.length > 0) {
429
+ console.log(` ✅ Found: ${C.GREEN}${frameworks.join(', ')}${C.RESET}`);
430
+ } else {
431
+ console.log(` ⚠️ ${C.YELLOW}No supported frameworks detected in the current directory.${C.RESET}`);
432
+ console.log(` (Checked for Playwright, Cypress, WebdriverIO files)`);
433
+ }
434
+
435
+ // 2b. Deep Structure Detection
436
+ const structures = [];
437
+ const checkDir = (dirs) => dirs.find(d => fs.existsSync(d) && fs.statSync(d).isDirectory());
438
+
439
+ const pomDir = checkDir(['pages', 'page-objects', 'po']);
440
+ if (pomDir) structures.push(`${C.CYAN}POM${C.RESET} (${pomDir}/)`);
441
+
442
+ const utilsDir = checkDir(['utils', 'helpers', 'support/utils', 'tests/utils']);
443
+ if (utilsDir) structures.push(`${C.CYAN}Utils/Helpers${C.RESET} (${utilsDir}/)`);
444
+
445
+ const fixturesDir = checkDir(['fixtures', 'data', 'tests/fixtures', 'cypress/fixtures']);
446
+ if (fixturesDir) structures.push(`${C.CYAN}Fixtures${C.RESET} (${fixturesDir}/)`);
447
+
448
+ if (structures.length > 0) {
449
+ console.log(` 🔍 Structure: ${structures.join(', ')}`);
450
+ console.log(` ${C.GRAY}(DeFlake will prioritize analyzing these folders for robust locator fixes)${C.RESET}`);
451
+ } else {
452
+ console.log(` ⚠️ ${C.YELLOW}No standard PageObject or Utils folders detected.${C.RESET}`);
453
+ console.log(` ${C.GRAY}(DeFlake works best when it can find your reusable locators in POM or Utils)${C.RESET}`);
454
+ }
455
+
456
+ if (fs.existsSync('package.json')) {
457
+ try {
458
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
459
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
460
+ const detectedDeps = [];
461
+ if (deps['@playwright/test']) detectedDeps.push('@playwright/test');
462
+ if (deps['cypress']) detectedDeps.push('cypress');
463
+ if (deps['webdriverio']) detectedDeps.push('webdriverio');
464
+
465
+ if (detectedDeps.length > 0) {
466
+ console.log(` ✅ Installed: ${C.GREEN}${detectedDeps.join(', ')}${C.RESET}`);
467
+ }
468
+ } catch (e) {
469
+ console.log(` ❌ ${C.RED}Error reading package.json${C.RESET}`);
470
+ }
471
+ }
472
+ console.log("");
473
+
474
+ // 3. .env File Format Validation
475
+ console.log(`${C.BRIGHT}Checking .env Configuration:${C.RESET}`);
476
+ if (fs.existsSync('.env')) {
477
+ const envContent = fs.readFileSync('.env', 'utf8');
478
+ const lines = envContent.split('\n');
479
+ let hasIssues = false;
480
+
481
+ for (const line of lines) {
482
+ const trimmed = line.trim();
483
+ if (!trimmed || trimmed.startsWith('#')) continue;
484
+
485
+ // Check for 'export' prefix (shell syntax, not .env syntax)
486
+ if (trimmed.startsWith('export ')) {
487
+ console.log(` ❌ ${C.RED}Invalid format: 'export' prefix detected${C.RESET}`);
488
+ console.log(` ${C.GRAY}Found:${C.RESET} ${trimmed.substring(0, 50)}...`);
489
+ console.log(` ${C.GREEN}Fix:${C.RESET} Remove 'export ' - .env files use: KEY=value`);
490
+ hasIssues = true;
491
+ }
492
+
493
+ // Check for quoted values (can cause issues with some parsers)
494
+ if (trimmed.match(/^[A-Z_]+=["'].*["']$/)) {
495
+ console.log(` ⚠️ ${C.YELLOW}Quotes detected in value (may cause issues)${C.RESET}`);
496
+ console.log(` ${C.GRAY}Found:${C.RESET} ${trimmed.substring(0, 50)}...`);
497
+ console.log(` ${C.GREEN}Tip:${C.RESET} Try without quotes: KEY=value`);
498
+ hasIssues = true;
499
+ }
500
+ }
501
+
502
+ if (!hasIssues) {
503
+ console.log(` ✅ .env file format looks correct`);
504
+ }
505
+ } else {
506
+ console.log(` ⚠️ ${C.YELLOW}No .env file found in current directory${C.RESET}`);
507
+ console.log(` Create one with: echo 'DEFLAKE_API_KEY=your_key' > .env`);
508
+ }
509
+ console.log("");
510
+
511
+ // 4. API Key Validation
512
+ console.log(`${C.BRIGHT}Validating API Key:${C.RESET}`);
513
+ const apiKey = process.env.DEFLAKE_API_KEY;
514
+ if (apiKey) {
515
+ const maskedKey = apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 8)) + apiKey.substring(apiKey.length - 4);
516
+ console.log(` ✅ DEFLAKE_API_KEY found: ${C.GREEN}${maskedKey}${C.RESET}`);
517
+ } else {
518
+ console.log(` ❌ ${C.RED}DEFLAKE_API_KEY is missing from your environment.${C.RESET}`);
519
+ console.log(` Fix: Run 'export DEFLAKE_API_KEY=your_key' or add it to your .env file.`);
520
+ }
521
+ console.log("");
522
+
523
+ // 5. API Connectivity
524
+ console.log(`${C.BRIGHT}Checking Connectivity:${C.RESET}`);
525
+ const client = new DeFlakeClient(argv.apiUrl, apiKey);
526
+ // Debug: Show what the client is using
527
+ console.log(` ${C.GRAY}(URL: ${client.apiUrl}, Key length: ${client.apiKey ? client.apiKey.length : 0})${C.RESET}`);
528
+ try {
529
+ process.stdout.write(` ⏳ Pinging DeFlake API... `);
530
+ const usage = await client.getUsage();
531
+ if (usage) {
532
+ process.stdout.write(`\r ✅ API Connected! (Tier: ${C.GREEN}${usage.tier.toUpperCase()}${C.RESET}) \n`);
533
+ } else {
534
+ process.stdout.write(`\r ❌ ${C.RED}API Connectivity Failed (Invalid Key or Server Timeout)${C.RESET} \n`);
535
+ }
536
+ } catch (error) {
537
+ process.stdout.write(`\r ❌ ${C.RED}API Connectivity Error: ${error.message}${C.RESET}\n`);
538
+ }
539
+ console.log("");
540
+
541
+ console.log(`${C.BRIGHT}Summary:${C.RESET}`);
542
+ if (apiKey && frameworks.length > 0) {
543
+ console.log(` ✨ ${C.GREEN}${C.BRIGHT}You are ready to use DeFlake!${C.RESET}`);
544
+ } else {
545
+ console.log(` ⚠️ ${C.YELLOW}Please address the issues above to ensure DeFlake works correctly.${C.RESET}`);
546
+ }
547
+ console.log("\n" + C.GRAY + "─".repeat(50) + C.RESET + "\n");
548
+ }
549
+
550
+ async function applySelfHealing(result) {
551
+ const C = {
552
+ RESET: "\x1b[0m",
553
+ GREEN: "\x1b[32m",
554
+ RED: "\x1b[31m",
555
+ GRAY: "\x1b[90m"
556
+ };
557
+
558
+ if (!result.location || !result.location.fullRootPath || !result.fix) return;
559
+
560
+ try {
561
+ const filePath = result.location.fullRootPath;
562
+ const targetLine = parseInt(result.location.rootLine);
563
+
564
+ if (!fs.existsSync(filePath)) {
565
+ console.error(` ❌ [Self-Healing] File not found: ${filePath}`);
566
+ return;
567
+ }
568
+
569
+ let fixCode = result.fix;
570
+ try {
571
+ const parsed = JSON.parse(result.fix);
572
+ if (parsed.code) fixCode = parsed.code;
573
+ } catch (e) { }
574
+
575
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n');
576
+ const originalLineIndex = targetLine - 1;
577
+
578
+ if (originalLineIndex < 0 || originalLineIndex >= lines.length) {
579
+ console.error(` ❌ [Self-Healing] Line ${targetLine} out of bounds in ${filePath}`);
580
+ return;
581
+ }
582
+
583
+ // Apply fix: Replace the entire line or the specific locator
584
+ // For robustness, we replace the whole line but keep indentation
585
+ const originalLine = lines[originalLineIndex];
586
+ const indentation = originalLine.match(/^\s*/)[0];
587
+ lines[originalLineIndex] = indentation + fixCode.trim();
588
+
589
+ fs.writeFileSync(filePath, lines.join('\n'));
590
+ console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully patched:${C.RESET} ${path.basename(filePath)}:${targetLine}`);
591
+ } catch (error) {
592
+ console.error(` ❌ ${C.RED}[Self-Healing] Error patching file:${C.RESET} ${error.message}`);
593
+ }
594
+ }
595
+
338
596
  async function analyzeFailures(artifacts, fullLog, client) {
339
597
  const C = {
340
598
  RESET: "\x1b[0m",
@@ -406,6 +664,11 @@ async function analyzeFailures(artifacts, fullLog, client) {
406
664
 
407
665
  if (result && result.status === 'success') {
408
666
  results.push(result);
667
+
668
+ // If --fix is enabled, apply it immediately for ALL tiers
669
+ if (argv.fix) {
670
+ await applySelfHealing(result);
671
+ }
409
672
  }
410
673
  }
411
674
  console.log("\r✅ Analysis complete. \n");
@@ -433,9 +696,13 @@ async function analyzeFailures(artifacts, fullLog, client) {
433
696
  }
434
697
 
435
698
  async function main() {
699
+ const command = argv._;
700
+
701
+ // If 'doctor' was called, don't proceed to wrapper logic
702
+ if (command.includes('doctor')) return;
703
+
436
704
  console.log("🚑 DeFlake JS Client (Batch Mode)");
437
705
  const client = new DeFlakeClient(argv.apiUrl);
438
- const command = argv._;
439
706
 
440
707
  if (command.length > 0) {
441
708
  const cmd = command[0];
@@ -467,4 +734,4 @@ async function main() {
467
734
  }
468
735
  }
469
736
 
470
- main();
737
+ // 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.5",
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
+ }