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.
- package/cli.js +241 -7
- package/client.js +36 -7
- 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
|
|
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
|
|
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
|
|
32
|
+
demandOption: false
|
|
21
33
|
})
|
|
22
34
|
.option('api-url', {
|
|
23
35
|
type: 'string',
|
|
24
36
|
description: 'Override Default API URL',
|
|
25
37
|
})
|
|
26
|
-
.
|
|
27
|
-
|
|
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
|
-
|
|
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
|
+
}
|