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.
- package/cli.js +239 -7
- package/client.js +36 -7
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
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
|
+
}
|