deflake 1.0.1 → 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 +241 -7
  2. package/client.js +36 -7
  3. package/package.json +2 -2
package/cli.js CHANGED
@@ -5,26 +5,57 @@ const DeFlakeClient = require('./client');
5
5
  const { spawn } = require('child_process');
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const pkg = require('./package.json');
9
+
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
+ // ------------------------------------------
8
20
 
9
- const argv = yargs(hideBin(process.argv))
21
+ const parser = yargs(hideBin(process.argv))
10
22
  .option('log', {
11
23
  alias: 'l',
12
24
  type: 'string',
13
25
  description: 'Path to the error log file',
14
- demandOption: false // No longer strictly required if running in wrapper mode
26
+ demandOption: false
15
27
  })
16
28
  .option('html', {
17
29
  alias: 'h',
18
30
  type: 'string',
19
31
  description: 'Path to the HTML snapshot',
20
- demandOption: false // No longer strictly required if auto-detected
32
+ demandOption: false
21
33
  })
22
34
  .option('api-url', {
23
35
  type: 'string',
24
36
  description: 'Override Default API URL',
25
37
  })
26
- .help()
27
- .argv;
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
+ })
51
+ .version(pkg.version)
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();
28
59
 
29
60
  // Helper to auto-detect artifacts (Batch Mode)
30
61
  function detectAllArtifacts(providedLog, providedHtml) {
@@ -333,6 +364,200 @@ function printDetailedFix(fixText, location, sourceCode = null) {
333
364
  * Core analysis engine for batch mode.
334
365
  * Enforces tier limits and calculates deduplicated results.
335
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
+
336
561
  async function analyzeFailures(artifacts, fullLog, client) {
337
562
  const C = {
338
563
  RESET: "\x1b[0m",
@@ -404,6 +629,11 @@ async function analyzeFailures(artifacts, fullLog, client) {
404
629
 
405
630
  if (result && result.status === 'success') {
406
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
+ }
407
637
  }
408
638
  }
409
639
  console.log("\r✅ Analysis complete. \n");
@@ -431,9 +661,13 @@ async function analyzeFailures(artifacts, fullLog, client) {
431
661
  }
432
662
 
433
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
+
434
669
  console.log("🚑 DeFlake JS Client (Batch Mode)");
435
670
  const client = new DeFlakeClient(argv.apiUrl);
436
- const command = argv._;
437
671
 
438
672
  if (command.length > 0) {
439
673
  const cmd = command[0];
@@ -465,4 +699,4 @@ async function main() {
465
699
  }
466
700
  }
467
701
 
468
- 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.1",
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
+ }