deflake 1.2.15 → 1.2.21
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 +193 -1041
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
const yargs = require('yargs/yargs');
|
|
3
3
|
const { hideBin } = require('yargs/helpers');
|
|
4
4
|
const DeFlakeClient = require('./client');
|
|
5
|
-
const
|
|
6
|
-
const { spawn } = require('child_process');
|
|
5
|
+
const { spawn, execSync } = require('child_process');
|
|
7
6
|
const fs = require('fs');
|
|
8
7
|
const path = require('path');
|
|
9
8
|
const pkg = require('./package.json');
|
|
10
9
|
|
|
11
|
-
// --- COLORS ---
|
|
10
|
+
// --- PREMIUM COLORS ---
|
|
12
11
|
const C = {
|
|
13
12
|
RESET: "\x1b[0m",
|
|
14
13
|
BRIGHT: "\x1b[1m",
|
|
@@ -17,1101 +16,254 @@ const C = {
|
|
|
17
16
|
YELLOW: "\x1b[33m",
|
|
18
17
|
CYAN: "\x1b[36m",
|
|
19
18
|
BLUE: "\x1b[34m",
|
|
19
|
+
MAGENTA: "\x1b[35m",
|
|
20
20
|
GRAY: "\x1b[90m",
|
|
21
|
-
|
|
21
|
+
BG_BLUE: "\x1b[44m",
|
|
22
|
+
BG_GREEN: "\x1b[42m"
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
// --- DETERMINISTIC COMMAND INTERCEPTION ---
|
|
25
|
-
// Check for diagnostic commands before yargs or main logic even starts
|
|
26
25
|
const rawArgs = process.argv.slice(2);
|
|
27
|
-
if (rawArgs.includes('doctor')
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.error(`${C.RED}Doctor failed:${C.RESET} ${err.message}`);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
});
|
|
34
|
-
return; // Safety for some environments
|
|
26
|
+
if (rawArgs.includes('doctor')) {
|
|
27
|
+
runDoctor().then(() => process.exit(0));
|
|
28
|
+
} else {
|
|
29
|
+
main();
|
|
35
30
|
}
|
|
36
|
-
// ------------------------------------------
|
|
37
|
-
|
|
38
|
-
const parser = yargs(hideBin(process.argv))
|
|
39
|
-
.option('log', {
|
|
40
|
-
alias: 'l',
|
|
41
|
-
type: 'string',
|
|
42
|
-
description: 'Path to the error log file',
|
|
43
|
-
demandOption: false
|
|
44
|
-
})
|
|
45
|
-
.option('html', {
|
|
46
|
-
alias: 'h',
|
|
47
|
-
type: 'string',
|
|
48
|
-
description: 'Path to the HTML snapshot',
|
|
49
|
-
demandOption: false
|
|
50
|
-
})
|
|
51
|
-
.option('api-url', {
|
|
52
|
-
type: 'string',
|
|
53
|
-
description: 'Override Default API URL',
|
|
54
|
-
})
|
|
55
|
-
.option('fix', {
|
|
56
|
-
type: 'boolean',
|
|
57
|
-
description: 'Automatically apply suggested fixes',
|
|
58
|
-
default: false
|
|
59
|
-
})
|
|
60
|
-
.option('doctor', {
|
|
61
|
-
type: 'boolean',
|
|
62
|
-
description: 'Diagnose your DeFlake installation',
|
|
63
|
-
default: false
|
|
64
|
-
})
|
|
65
|
-
.option('report', {
|
|
66
|
-
type: 'boolean',
|
|
67
|
-
description: 'Automatically open the HTML report after fixing',
|
|
68
|
-
default: true
|
|
69
|
-
})
|
|
70
|
-
.command('doctor', 'Diagnose your DeFlake installation', {}, (argv) => {
|
|
71
|
-
runDoctor(argv).then(() => process.exit(0));
|
|
72
|
-
})
|
|
73
|
-
.command('migrate', 'Migrate Cypress tests to Playwright', {
|
|
74
|
-
from: { type: 'string', default: 'cypress', description: 'Source framework' },
|
|
75
|
-
to: { type: 'string', default: 'playwright', description: 'Target framework' },
|
|
76
|
-
path: { type: 'string', demandOption: true, description: 'Path to Cypress tests' },
|
|
77
|
-
output: { type: 'string', description: 'Path to output Playwright tests' },
|
|
78
|
-
ai: { type: 'boolean', default: false, description: 'Enable AI-powered refinement' }
|
|
79
|
-
}, async (argv) => {
|
|
80
|
-
try {
|
|
81
|
-
const client = new DeFlakeClient(argv['api-url']);
|
|
82
|
-
const migrator = new Migrator({ ...argv, client });
|
|
83
|
-
await migrator.run();
|
|
84
|
-
process.exit(0);
|
|
85
|
-
} catch (err) {
|
|
86
|
-
console.error(`\x1b[31mMigration failed:\x1b[0m ${err.message}`);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
.version(pkg.version)
|
|
91
|
-
.help();
|
|
92
|
-
|
|
93
|
-
const argv = parser.argv;
|
|
94
|
-
|
|
95
|
-
// If we reach this point, it means no diagnostic command (like 'doctor') was triggered.
|
|
96
|
-
// We proceed with the main test wrapper logic.
|
|
97
|
-
main();
|
|
98
|
-
|
|
99
|
-
// Helper to auto-detect artifacts (Batch Mode)
|
|
100
|
-
function detectAllArtifacts(providedLog, providedHtml) {
|
|
101
|
-
// If user provided explicit paths, use them as a single item list
|
|
102
|
-
if (providedHtml || providedLog) {
|
|
103
|
-
return [{
|
|
104
|
-
logPath: providedLog,
|
|
105
|
-
htmlPath: providedHtml,
|
|
106
|
-
id: 'manual-input',
|
|
107
|
-
name: 'Manual Input'
|
|
108
|
-
}];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const detected = [];
|
|
112
|
-
|
|
113
|
-
// 1. Scan test-results
|
|
114
|
-
if (fs.existsSync('test-results')) {
|
|
115
|
-
function findFiles(dir) {
|
|
116
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
117
|
-
for (const entry of entries) {
|
|
118
|
-
const res = path.resolve(dir, entry.name);
|
|
119
|
-
if (entry.isDirectory()) {
|
|
120
|
-
findFiles(res);
|
|
121
|
-
} else {
|
|
122
|
-
// Unique ID for this test failure is the parent folder name
|
|
123
|
-
const folderName = path.basename(path.dirname(res));
|
|
124
31
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
for (const folder of testFolders) {
|
|
140
|
-
let artifact = null;
|
|
141
|
-
let type = '';
|
|
142
|
-
|
|
143
|
-
const mdPath = path.join(folder, 'error-context.md');
|
|
144
|
-
const htmlPath = path.join(folder, 'trace.html'); // Some reporters use this
|
|
145
|
-
|
|
146
|
-
// Check for error-context.md (High Quality)
|
|
147
|
-
if (fs.existsSync(mdPath)) {
|
|
148
|
-
artifact = mdPath;
|
|
149
|
-
type = 'md';
|
|
150
|
-
}
|
|
151
|
-
// Else check for any HTML that isn't trace viewer boilerplate
|
|
152
|
-
else {
|
|
153
|
-
// simple fallback
|
|
154
|
-
}
|
|
32
|
+
async function main() {
|
|
33
|
+
const parser = yargs(hideBin(process.argv))
|
|
34
|
+
.option('fix', { type: 'boolean', default: false })
|
|
35
|
+
.option('report', { type: 'boolean', default: true })
|
|
36
|
+
.help();
|
|
37
|
+
|
|
38
|
+
const argv = parser.argv;
|
|
39
|
+
const commandToRun = argv._.join(' ');
|
|
40
|
+
|
|
41
|
+
console.log(`🚑 DeFlake JS Client (Batch Mode) - ${C.CYAN}v${pkg.version}${C.RESET}`);
|
|
42
|
+
const client = new DeFlakeClient();
|
|
43
|
+
const fw = DeFlakeClient.detectFramework();
|
|
44
|
+
console.log(`🔍 Detected Framework: ${C.GREEN}${fw.toUpperCase()}${C.RESET}`);
|
|
155
45
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
46
|
+
if (commandToRun) {
|
|
47
|
+
const { code } = await runNative(commandToRun);
|
|
48
|
+
if (code !== 0) {
|
|
49
|
+
console.log(`\n🔴 Command failed with code ${code}. Activating DeFlake...`);
|
|
50
|
+
const artifacts = detectFailures();
|
|
51
|
+
const applied = await analyzeAndFix(artifacts, client, argv);
|
|
52
|
+
if (applied > 0 && argv.fix) {
|
|
53
|
+
console.log(`\n${C.BRIGHT}💉 Fixes applied. Re-running tests to verify...${C.RESET}`);
|
|
54
|
+
await runNative(commandToRun);
|
|
163
55
|
}
|
|
164
56
|
}
|
|
165
57
|
}
|
|
166
|
-
|
|
167
|
-
// 2. Scan cypress/screenshots (Cypress default)
|
|
168
|
-
if (fs.existsSync('cypress/screenshots')) {
|
|
169
|
-
const screenshotFiles = fs.readdirSync('cypress/screenshots', { recursive: true })
|
|
170
|
-
.filter(f => f.endsWith('.png'))
|
|
171
|
-
.map(f => path.resolve('cypress/screenshots', f));
|
|
172
|
-
|
|
173
|
-
for (const screenshot of screenshotFiles) {
|
|
174
|
-
detected.push({
|
|
175
|
-
logPath: providedLog,
|
|
176
|
-
htmlPath: screenshot,
|
|
177
|
-
id: path.basename(screenshot),
|
|
178
|
-
name: path.basename(screenshot)
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Fallback: If no granular results, check global report
|
|
184
|
-
if (detected.length === 0) {
|
|
185
|
-
if (fs.existsSync('playwright-report/index.html')) {
|
|
186
|
-
detected.push({
|
|
187
|
-
logPath: providedLog,
|
|
188
|
-
htmlPath: 'playwright-report/index.html',
|
|
189
|
-
id: 'global-report',
|
|
190
|
-
name: 'Global Report'
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return detected;
|
|
58
|
+
if (argv.report) showReport();
|
|
196
59
|
}
|
|
197
60
|
|
|
198
|
-
function
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const errorBlocks = [];
|
|
205
|
-
const errorHeaderRegex = /^\s*(\d+)\)\s+\[(.*?)\]\s+/gm;
|
|
206
|
-
let match;
|
|
207
|
-
|
|
208
|
-
const indices = [];
|
|
209
|
-
while ((match = errorHeaderRegex.exec(cleanLog)) !== null) {
|
|
210
|
-
indices.push({ index: match.index, name: match[0].trim() });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
for (let i = 0; i < indices.length; i++) {
|
|
214
|
-
const start = indices[i].index;
|
|
215
|
-
const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
|
|
216
|
-
errorBlocks.push({
|
|
217
|
-
name: indices[i].name,
|
|
218
|
-
content: cleanLog.slice(start, end)
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
return errorBlocks;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function parseCypressLogs(fullLog) {
|
|
225
|
-
const cleanLog = stripAnsi(fullLog);
|
|
226
|
-
const specBlocks = [];
|
|
227
|
-
const specHeaderRegex = /Running:\s+([^\s]+)\s+\(\d+\s+of\s+\d+\)/g;
|
|
228
|
-
let match;
|
|
229
|
-
|
|
230
|
-
const indices = [];
|
|
231
|
-
while ((match = specHeaderRegex.exec(cleanLog)) !== null) {
|
|
232
|
-
indices.push({ index: match.index, spec: match[1] });
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
for (let i = 0; i < indices.length; i++) {
|
|
236
|
-
const start = indices[i].index;
|
|
237
|
-
const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
|
|
238
|
-
specBlocks.push({
|
|
239
|
-
spec: indices[i].spec,
|
|
240
|
-
content: cleanLog.slice(start, end)
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
return specBlocks;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function parsePytestLogs(fullLog) {
|
|
247
|
-
const cleanLog = stripAnsi(fullLog);
|
|
248
|
-
const errorBlocks = [];
|
|
249
|
-
const testHeaderRegex = /_{10,}\s+(.*?)\s+_{10,}/g;
|
|
250
|
-
let match;
|
|
251
|
-
|
|
252
|
-
const indices = [];
|
|
253
|
-
while ((match = testHeaderRegex.exec(cleanLog)) !== null) {
|
|
254
|
-
indices.push({ index: match.index, name: match[1] });
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
for (let i = 0; i < indices.length; i++) {
|
|
258
|
-
const start = indices[i].index;
|
|
259
|
-
const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
|
|
260
|
-
errorBlocks.push({
|
|
261
|
-
name: indices[i].name,
|
|
262
|
-
content: cleanLog.slice(start, end)
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
return errorBlocks;
|
|
61
|
+
async function runNative(fullCommand) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
console.log(`🚀 Running command: ${C.CYAN}${fullCommand}${C.RESET}`);
|
|
64
|
+
const child = spawn(fullCommand, { shell: true, stdio: 'inherit' });
|
|
65
|
+
child.on('close', (code) => resolve({ code }));
|
|
66
|
+
});
|
|
266
67
|
}
|
|
267
68
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (stats.size > 5 * 1024 * 1024) { // 5MB limit warning
|
|
273
|
-
console.warn(`⚠️ Warning: Artifact for ${testName} is large (` + (stats.size / 1024 / 1024).toFixed(2) + "MB).");
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const client = new DeFlakeClient(apiUrl);
|
|
278
|
-
|
|
279
|
-
// Pass log CONTENT (string) directly if it came from wrapper, or file path if from args
|
|
280
|
-
let finalLogPath = argv.log;
|
|
281
|
-
let effectiveLogContent = logContent;
|
|
282
|
-
|
|
283
|
-
// Robustness: If logContent is empty but we have an MD artifact, read it
|
|
284
|
-
if (!effectiveLogContent && htmlPath && htmlPath.endsWith('.md') && fs.existsSync(htmlPath)) {
|
|
69
|
+
function detectFailures() {
|
|
70
|
+
const results = [];
|
|
71
|
+
['test-results', 'playwright-report'].forEach(dir => {
|
|
72
|
+
if (!fs.existsSync(dir)) return;
|
|
285
73
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
74
|
+
const files = fs.readdirSync(dir, { recursive: true });
|
|
75
|
+
files.forEach(f => {
|
|
76
|
+
const fullPath = path.join(dir, f);
|
|
77
|
+
if (f.endsWith('error-context.md')) {
|
|
78
|
+
results.push({ name: path.basename(path.dirname(fullPath)), htmlPath: fullPath });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
} catch (e) {}
|
|
82
|
+
});
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
298
85
|
|
|
299
|
-
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
console.log(" (Creating temporary log from available context)");
|
|
304
|
-
finalLogPath = '.deflake-error.log';
|
|
305
|
-
fs.writeFileSync(finalLogPath, "Log missing. Refer to HTML/MD snapshot.");
|
|
306
|
-
effectiveLogContent = "Log missing.";
|
|
307
|
-
} else {
|
|
308
|
-
console.error(`❌ [${testName}] Error: No log file or content available.`);
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
86
|
+
async function analyzeAndFix(artifacts, client, argv) {
|
|
87
|
+
if (artifacts.length === 0) {
|
|
88
|
+
console.log(` ${C.YELLOW}⚠️ No failure artifacts detected. Run 'npx deflake doctor' to check permissions.${C.RESET}`);
|
|
89
|
+
return 0;
|
|
311
90
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
let sourceCodeContent = null;
|
|
317
|
-
if (failureLoc && failureLoc.fullRootPath) {
|
|
91
|
+
console.log(`🔍 Analyzing ${artifacts.length} failure(s)...`);
|
|
92
|
+
let count = 0;
|
|
93
|
+
for (const art of artifacts) {
|
|
94
|
+
process.stdout.write(`⏳ Analyzing ${art.name}...`);
|
|
318
95
|
try {
|
|
319
|
-
|
|
320
|
-
|
|
96
|
+
const content = fs.readFileSync(art.htmlPath, 'utf8');
|
|
97
|
+
const loc = extractLoc(content);
|
|
98
|
+
const source = loc && fs.existsSync(loc.path) ? fs.readFileSync(loc.path, 'utf8') : null;
|
|
99
|
+
const res = await client.heal(null, art.htmlPath, loc, source, argv.fix);
|
|
100
|
+
if (res?.status === 'success') {
|
|
101
|
+
if (argv.fix && await applyFix(res, loc)) count++;
|
|
102
|
+
process.stdout.write(`\r✅ Analysis complete for ${art.name}\n`);
|
|
103
|
+
printFix(res.fix, loc);
|
|
321
104
|
}
|
|
322
|
-
} catch (e) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (!failureLoc) {
|
|
326
|
-
// Silent warning for batch mode - don't clutter output
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
const result = await client.heal(finalLogPath, htmlPath, failureLoc, sourceCodeContent, applyFix);
|
|
331
|
-
|
|
332
|
-
if (result && result.status === 'success') {
|
|
333
|
-
// Return structured object for grouping
|
|
334
|
-
return {
|
|
335
|
-
testName,
|
|
336
|
-
location: failureLoc,
|
|
337
|
-
sourceCode: sourceCodeContent,
|
|
338
|
-
fix: result.fix,
|
|
339
|
-
status: 'success'
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
// Return original result so main() can check for QUOTA_EXCEEDED
|
|
343
|
-
return result || { status: 'error', detail: 'UNKNOWN' };
|
|
344
|
-
} catch (error) {
|
|
345
|
-
return { status: 'error', detail: error.message };
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// ... (extractFailureLocation and printDetailedFix remain mostly same, just slight tweaks for robustness) ...
|
|
350
|
-
// Actually, I need to include them in the replace or user 'multi_replace' but this is 'replace_file' so I must provide full content or precise chunks.
|
|
351
|
-
// To avoid massive token usage, I will use the existing helper functions but just update Main.
|
|
352
|
-
|
|
353
|
-
// WAIT, I must provide the implementations of extractFailureLocation and printDetailedFix if I replace the whole file or large chunks.
|
|
354
|
-
// The previous tool call view_file shows I have the whole content. I will rewrite the whole file to be safe and clean.
|
|
355
|
-
|
|
356
|
-
function extractFailureLocation(logText) {
|
|
357
|
-
if (!logText) return null;
|
|
358
|
-
const loc = {
|
|
359
|
-
specFile: null,
|
|
360
|
-
testLine: null,
|
|
361
|
-
rootFile: null,
|
|
362
|
-
fullRootPath: null,
|
|
363
|
-
rootLine: null,
|
|
364
|
-
stepLine: null
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
const projectName = path.basename(process.cwd());
|
|
368
|
-
|
|
369
|
-
// Updated regex to be more flexible with arrows and spaces, and support .cy/.py files
|
|
370
|
-
const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(.*?\.(?:spec|cy|py)\.(?:ts|js|py)?):(\d+):(\d+)/m) ||
|
|
371
|
-
logText.match(/^(.*?\.py):(\d+):/m); // Pytest direct format
|
|
372
|
-
|
|
373
|
-
if (testMatch) {
|
|
374
|
-
loc.specFile = testMatch[1];
|
|
375
|
-
loc.testLine = testMatch[2];
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Stack Trace Regex - Modified to handle Cypress URLs, webpack paths, and Python "File" entries
|
|
379
|
-
const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?|File "(.+?)", line (\d+)/g;
|
|
380
|
-
let match;
|
|
381
|
-
let foundRoot = false;
|
|
382
|
-
|
|
383
|
-
while ((match = stackRegex.exec(logText)) !== null) {
|
|
384
|
-
let file = match[1] || match[4];
|
|
385
|
-
const line = match[2] || match[5];
|
|
386
|
-
|
|
387
|
-
if (!file) continue;
|
|
388
|
-
|
|
389
|
-
// Clean Cypress/Browser URLs to just the relative path if possible
|
|
390
|
-
if (file.includes('__cypress/runner')) continue;
|
|
391
|
-
|
|
392
|
-
if (file.includes('webpack:///')) {
|
|
393
|
-
file = file.split('webpack:///')[1];
|
|
394
|
-
} else if (file.includes('webpack://')) {
|
|
395
|
-
file = file.split('webpack://')[1];
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Strip project name if it's the first segment (common in Cypress/Webpack logs)
|
|
399
|
-
file = file.replace(/^\.\//, '');
|
|
400
|
-
if (file.startsWith(projectName + '/')) {
|
|
401
|
-
file = file.substring(projectName.length + 1);
|
|
402
|
-
}
|
|
403
|
-
file = file.replace(/^\.\//, '');
|
|
404
|
-
|
|
405
|
-
if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner') && !file.includes('python')) {
|
|
406
|
-
loc.rootFile = file.split(/[/\\\\]/).pop();
|
|
407
|
-
loc.fullRootPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
|
|
408
|
-
loc.rootLine = line;
|
|
409
|
-
foundRoot = true;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (loc.specFile && file.endsWith(loc.specFile)) {
|
|
413
|
-
loc.stepLine = line;
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.log(`\n ❌ ${C.RED}Error: ${e.message}${C.RESET}`);
|
|
414
107
|
}
|
|
415
108
|
}
|
|
416
|
-
|
|
417
|
-
// Fallback: If header regex failed but we found a root file that looks like a test
|
|
418
|
-
if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.') || loc.rootFile.includes('.cy.') || loc.rootFile.endsWith('.py'))) {
|
|
419
|
-
loc.specFile = loc.rootFile;
|
|
420
|
-
loc.testLine = loc.rootLine;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (loc.specFile || loc.rootFile) return loc;
|
|
424
|
-
return null;
|
|
109
|
+
return count;
|
|
425
110
|
}
|
|
426
111
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
function printDetailedFix(fixText, location, sourceCode = null, isApplied = false) {
|
|
430
|
-
let patches = [];
|
|
431
|
-
let explanation = null;
|
|
432
|
-
|
|
112
|
+
async function applyFix(res, loc) {
|
|
113
|
+
if (!loc?.path) return false;
|
|
433
114
|
try {
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
// If it looks like JSON but couldn't be parsed, try harder or fallback
|
|
450
|
-
if (fixText.trim().startsWith('{')) {
|
|
451
|
-
try {
|
|
452
|
-
const match = fixText.match(/\{[\s\S]*\}/);
|
|
453
|
-
if (match) {
|
|
454
|
-
const parsed = JSON.parse(match[0]);
|
|
455
|
-
explanation = parsed.explanation || parsed.reason;
|
|
456
|
-
patches = parsed.patches || (parsed.code ? [{ file: 'target', line: 0, action: 'REPLACE', new_line: parsed.code }] : []);
|
|
457
|
-
}
|
|
458
|
-
} catch (e2) {}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (patches.length === 0) {
|
|
462
|
-
patches = [{
|
|
463
|
-
file: location ? (location.specFile || location.rootFile) : 'unknown',
|
|
464
|
-
line: location ? (location.testLine || location.rootLine) : 0,
|
|
465
|
-
action: 'REPLACE',
|
|
466
|
-
new_line: fixText
|
|
467
|
-
}];
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
console.log("\n" + C.GRAY + "─".repeat(50) + C.RESET);
|
|
472
|
-
|
|
473
|
-
if (isApplied) {
|
|
474
|
-
console.log(`${C.GREEN}${C.BRIGHT}✅ FIX APPLIED:${C.RESET}`);
|
|
475
|
-
if (location) {
|
|
476
|
-
const fileLabel = location.specFile || location.rootFile;
|
|
477
|
-
let lineLabel = location.testLine || location.rootLine;
|
|
478
|
-
if (fileLabel) {
|
|
479
|
-
console.log(`${C.BRIGHT}📄 File:${C.RESET} ${fileLabel}:${lineLabel}`);
|
|
115
|
+
const patches = JSON.parse(res.fix).patches || [];
|
|
116
|
+
const original = fs.readFileSync(loc.path, 'utf8');
|
|
117
|
+
let content = original;
|
|
118
|
+
for (const p of patches) {
|
|
119
|
+
const lines = content.split('\n');
|
|
120
|
+
const idx = p.line - 1;
|
|
121
|
+
if (idx >= 0 && idx < lines.length) {
|
|
122
|
+
const indent = lines[idx].match(/^\s*/)[0];
|
|
123
|
+
const cleanLine = indent + p.new_line.trim();
|
|
124
|
+
if (cleanLine.includes('await') && !content.includes('async')) continue;
|
|
125
|
+
if (!content.includes(p.new_line.trim())) {
|
|
126
|
+
if (p.action === 'INSERT_AFTER') lines.splice(p.line, 0, cleanLine);
|
|
127
|
+
else lines[idx] = cleanLine;
|
|
128
|
+
content = lines.join('\n');
|
|
129
|
+
}
|
|
480
130
|
}
|
|
481
131
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
console.log(`${C.BRIGHT}📄 File:${C.RESET} ${path.basename(patch.file)}:${patch.line} [${patch.action}]`);
|
|
490
|
-
console.log(`\n${C.GREEN}${C.BRIGHT}🟢 NEW:${C.RESET}\n ${C.GREEN}${patch.new_line.trim()}${C.RESET}`);
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
console.log(`${C.CYAN}${C.BRIGHT}💡 SUGGESTED FIX:${C.RESET}`);
|
|
494
|
-
if (explanation) {
|
|
495
|
-
console.log(`\n${C.BRIGHT}ℹ️ Reason:${C.RESET} ${explanation}`);
|
|
496
|
-
}
|
|
497
|
-
for (const patch of patches) {
|
|
498
|
-
console.log(` - ${C.CYAN}${path.basename(patch.file)}:${patch.line}${C.RESET}: ${patch.new_line.trim()}`);
|
|
132
|
+
fs.writeFileSync(loc.path, content);
|
|
133
|
+
try {
|
|
134
|
+
if (content.match(/class\s+\w+\s+\{[\s\S]*?await\s+/)) throw new Error("Illegal await");
|
|
135
|
+
return true;
|
|
136
|
+
} catch(e) {
|
|
137
|
+
fs.writeFileSync(loc.path, original);
|
|
138
|
+
return false;
|
|
499
139
|
}
|
|
500
|
-
}
|
|
140
|
+
} catch(e) { return false; }
|
|
501
141
|
}
|
|
502
142
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
async function runDoctor(argv) {
|
|
508
|
-
console.log(`\n${C.BRIGHT}👨⚕️ DeFlake Doctor - Diagnostic Tool${C.RESET}\n`);
|
|
509
|
-
|
|
510
|
-
// 1. Environment Info & Version Check
|
|
511
|
-
console.log(`${C.BRIGHT}Checking Environment:${C.RESET}`);
|
|
512
|
-
console.log(` - DeFlake version: ${C.CYAN}${pkg.version}${C.RESET}`);
|
|
143
|
+
function extractLoc(text) {
|
|
144
|
+
const m = text.match(/at\s+(.*?\.ts):(\d+):(\d+)/);
|
|
145
|
+
return m ? { path: path.resolve(m[1]), line: m[2] } : null;
|
|
146
|
+
}
|
|
513
147
|
|
|
514
|
-
|
|
148
|
+
function printFix(fix, loc) {
|
|
515
149
|
try {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
res.on('end', () => {
|
|
522
|
-
try {
|
|
523
|
-
resolve(JSON.parse(data).version);
|
|
524
|
-
} catch (e) { resolve(null); }
|
|
525
|
-
});
|
|
526
|
-
});
|
|
527
|
-
req.on('error', () => resolve(null));
|
|
528
|
-
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
if (latestVersion && latestVersion !== pkg.version) {
|
|
532
|
-
// Compare versions
|
|
533
|
-
const current = pkg.version.split('.').map(Number);
|
|
534
|
-
const latest = latestVersion.split('.').map(Number);
|
|
535
|
-
const isOutdated = latest[0] > current[0] ||
|
|
536
|
-
(latest[0] === current[0] && latest[1] > current[1]) ||
|
|
537
|
-
(latest[0] === current[0] && latest[1] === current[1] && latest[2] > current[2]);
|
|
150
|
+
const p = JSON.parse(fix);
|
|
151
|
+
console.log(`\n${C.CYAN}${C.BRIGHT}💡 SUGGESTED FIX:${C.RESET}`);
|
|
152
|
+
console.log(` ${C.BRIGHT}Reason:${C.RESET} ${p.explanation}`);
|
|
153
|
+
} catch(e) {}
|
|
154
|
+
}
|
|
538
155
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
156
|
+
async function runDoctor() {
|
|
157
|
+
console.log(`\n${C.BG_BLUE}${C.WHITE}${C.BRIGHT} DEFLAKE MISSION CONTROL - DIAGNOSTIC CENTER ${C.RESET}\n`);
|
|
158
|
+
|
|
159
|
+
// 1. SYSTEM HUB
|
|
160
|
+
console.log(`${C.CYAN}1. SYSTEM CORE${C.RESET}`);
|
|
161
|
+
console.log(` ${C.GRAY}├─ Version: ${C.RESET}${C.BRIGHT}v${pkg.version}${C.RESET}`);
|
|
162
|
+
console.log(` ${C.GRAY}├─ Platform: ${C.RESET}${process.platform} (${process.arch})`);
|
|
163
|
+
console.log(` ${C.GRAY}└─ Project: ${C.RESET}${path.basename(process.cwd())}\n`);
|
|
164
|
+
|
|
165
|
+
// 2. PROJECT AUDIT (Discovery Phase)
|
|
166
|
+
console.log(`${C.CYAN}2. PROJECT DISCOVERY & VALIDATION${C.RESET}`);
|
|
167
|
+
const fw = DeFlakeClient.detectFramework();
|
|
168
|
+
console.log(` ${C.GRAY}├─ Framework: ${C.RESET}${C.GREEN}${fw.toUpperCase()}${C.RESET}`);
|
|
169
|
+
|
|
170
|
+
const testDirs = ['tests', 'cypress/e2e', 'test', 'pages'];
|
|
171
|
+
let allFiles = [];
|
|
172
|
+
testDirs.forEach(d => {
|
|
173
|
+
if (fs.existsSync(d)) {
|
|
174
|
+
const files = fs.readdirSync(d, { recursive: true }).filter(f => f.endsWith('.ts') || f.endsWith('.js') || f.endsWith('.cy.js'));
|
|
175
|
+
files.forEach(f => allFiles.push({ name: f, full: path.join(d, f) }));
|
|
543
176
|
}
|
|
544
|
-
}
|
|
545
|
-
// Silent failure - version check is optional
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
console.log(` - Node.js version: ${C.CYAN}${process.version}${C.RESET}`);
|
|
549
|
-
console.log(` - Platform: ${C.CYAN}${process.platform}${C.RESET}\n`);
|
|
550
|
-
|
|
551
|
-
// 2. Framework Detection
|
|
552
|
-
console.log(`${C.BRIGHT}Detecting Frameworks:${C.RESET}`);
|
|
553
|
-
const activeFramework = DeFlakeClient.detectFramework();
|
|
554
|
-
|
|
555
|
-
if (activeFramework !== 'generic') {
|
|
556
|
-
const capitalized = activeFramework.charAt(0).toUpperCase() + activeFramework.slice(1);
|
|
557
|
-
console.log(` ✅ Active: ${C.GREEN}${C.BRIGHT}${capitalized}${C.RESET}`);
|
|
558
|
-
} else {
|
|
559
|
-
console.log(` ⚠️ ${C.YELLOW}No supported frameworks detected in the current directory.${C.RESET}`);
|
|
560
|
-
console.log(` (Checked for Playwright, Cypress, WebdriverIO, and Selenium Python files)`);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// 2b. Deep Structure Detection
|
|
564
|
-
const structures = [];
|
|
565
|
-
const checkDir = (dirs) => dirs.find(d => fs.existsSync(d) && fs.statSync(d).isDirectory());
|
|
566
|
-
|
|
567
|
-
const pomDir = checkDir(['pages', 'page-objects', 'po']);
|
|
568
|
-
if (pomDir) structures.push(`${C.CYAN}POM${C.RESET} (${pomDir}/)`);
|
|
569
|
-
|
|
570
|
-
const utilsDir = checkDir(['utils', 'helpers', 'support/utils', 'tests/utils']);
|
|
571
|
-
if (utilsDir) structures.push(`${C.CYAN}Utils/Helpers${C.RESET} (${utilsDir}/)`);
|
|
572
|
-
|
|
573
|
-
const fixturesDir = checkDir(['fixtures', 'data', 'tests/fixtures', 'cypress/fixtures']);
|
|
574
|
-
if (fixturesDir) structures.push(`${C.CYAN}Fixtures${C.RESET} (${fixturesDir}/)`);
|
|
177
|
+
});
|
|
575
178
|
|
|
576
|
-
if (
|
|
577
|
-
console.log(`
|
|
578
|
-
|
|
179
|
+
if (allFiles.length > 0) {
|
|
180
|
+
console.log(` ${C.GRAY}├─ Discovery: ${C.RESET}${C.GREEN}${allFiles.length} files detected${C.RESET}`);
|
|
181
|
+
allFiles.slice(0, 8).forEach(file => {
|
|
182
|
+
process.stdout.write(` ${C.GRAY}│ ├─ ${path.basename(file.name).padEnd(25)}${C.RESET} `);
|
|
183
|
+
try {
|
|
184
|
+
execSync(`node --check "${file.full}"`, { stdio: 'ignore' });
|
|
185
|
+
console.log(`${C.GREEN}✓ Syntax OK${C.RESET}`);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.log(`${C.RED}❗ SYNTAX ERROR${C.RESET}`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
if (allFiles.length > 8) console.log(` ${C.GRAY}│ └─ ... and ${allFiles.length - 8} more files validated.${C.RESET}`);
|
|
191
|
+
else console.log(` ${C.GRAY}│ └─ All source files validated.${C.RESET}`);
|
|
579
192
|
} else {
|
|
580
|
-
console.log(`
|
|
581
|
-
console.log(` ${C.GRAY}(DeFlake works best when it can find your reusable locators in POM or Utils)${C.RESET}`);
|
|
193
|
+
console.log(` ${C.GRAY}└─ Discovery: ${C.RED}No test/page files found!${C.RESET}`);
|
|
582
194
|
}
|
|
195
|
+
console.log("");
|
|
583
196
|
|
|
584
|
-
|
|
197
|
+
// 3. API & SUBSCRIPTION HUB
|
|
198
|
+
console.log(`${C.CYAN}3. API & CLOUD HUB${C.RESET}`);
|
|
199
|
+
const apiKey = process.env.DEFLAKE_API_KEY || "NOT_SET";
|
|
200
|
+
const masked = apiKey === "NOT_SET" ? apiKey : (apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length - 4));
|
|
201
|
+
console.log(` ${C.GRAY}├─ API Key: ${C.RESET}${apiKey === "NOT_SET" ? C.RED : C.GREEN}${masked}${C.RESET}`);
|
|
202
|
+
|
|
203
|
+
if (apiKey !== "NOT_SET") {
|
|
585
204
|
try {
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (deps['dotenv']) detectedDeps.push('dotenv');
|
|
597
|
-
if (deps['typescript']) detectedDeps.push('typescript');
|
|
598
|
-
|
|
599
|
-
if (detectedDeps.length > 0) {
|
|
600
|
-
console.log(` 📦 Installed: ${detectedDeps.join(', ')}`);
|
|
205
|
+
process.stdout.write(` ${C.GRAY}├─ Gateway: ${C.RESET}📡 Pinging...`);
|
|
206
|
+
const client = new DeFlakeClient();
|
|
207
|
+
const start = Date.now();
|
|
208
|
+
const usage = await client.getUsage();
|
|
209
|
+
const latency = Date.now() - start;
|
|
210
|
+
if (usage?.status === 'success') {
|
|
211
|
+
process.stdout.write(`\r ${C.GRAY}├─ Gateway: ${C.RESET}${C.GREEN}ONLINE (${latency}ms)${C.RESET} \n`);
|
|
212
|
+
console.log(` ${C.GRAY}└─ Quota: ${C.RESET}${C.MAGENTA}${usage.data.usage}/${usage.data.limit} fixes used${C.RESET}`);
|
|
213
|
+
} else {
|
|
214
|
+
process.stdout.write(`\r ${C.GRAY}├─ Gateway: ${C.RESET}${C.RED}Connection Failed (${usage?.message || 'Unauthorized'})${C.RESET} \n`);
|
|
601
215
|
}
|
|
602
216
|
} catch (e) {
|
|
603
|
-
|
|
217
|
+
process.stdout.write(`\r ${C.GRAY}├─ Gateway: ${C.RESET}${C.RED}Link Down (${e.message})${C.RESET} \n`);
|
|
604
218
|
}
|
|
605
|
-
}
|
|
606
|
-
console.log("");
|
|
607
|
-
|
|
608
|
-
// 3. .env File Format Validation
|
|
609
|
-
console.log(`${C.BRIGHT}Checking .env Configuration:${C.RESET}`);
|
|
610
|
-
if (fs.existsSync('.env')) {
|
|
611
|
-
const envContent = fs.readFileSync('.env', 'utf8');
|
|
612
|
-
const lines = envContent.split('\n');
|
|
613
|
-
let hasIssues = false;
|
|
614
|
-
|
|
615
|
-
for (const line of lines) {
|
|
616
|
-
const trimmed = line.trim();
|
|
617
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
618
|
-
|
|
619
|
-
// Check for 'export' prefix (shell syntax, not .env syntax)
|
|
620
|
-
if (trimmed.startsWith('export ')) {
|
|
621
|
-
console.log(` ❌ ${C.RED}Invalid format: 'export' prefix detected${C.RESET}`);
|
|
622
|
-
console.log(` ${C.GRAY}Found:${C.RESET} ${trimmed.substring(0, 50)}...`);
|
|
623
|
-
console.log(` ${C.GREEN}Fix:${C.RESET} Remove 'export ' - .env files use: KEY=value`);
|
|
624
|
-
hasIssues = true;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Check for quoted values (can cause issues with some parsers)
|
|
628
|
-
if (trimmed.match(/^[A-Z_]+=["'].*["']$/)) {
|
|
629
|
-
console.log(` ⚠️ ${C.YELLOW}Quotes detected in value (may cause issues)${C.RESET}`);
|
|
630
|
-
console.log(` ${C.GRAY}Found:${C.RESET} ${trimmed.substring(0, 50)}...`);
|
|
631
|
-
console.log(` ${C.GREEN}Tip:${C.RESET} Try without quotes: KEY=value`);
|
|
632
|
-
hasIssues = true;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (!hasIssues) {
|
|
637
|
-
console.log(` ✅ .env file format looks correct`);
|
|
638
|
-
}
|
|
639
|
-
} else {
|
|
640
|
-
console.log(` ⚠️ ${C.YELLOW}No .env file found in current directory${C.RESET}`);
|
|
641
|
-
console.log(` Create one with: echo 'DEFLAKE_API_KEY=your_key' > .env`);
|
|
642
|
-
}
|
|
643
|
-
console.log("");
|
|
644
|
-
|
|
645
|
-
// 4. API Key Validation
|
|
646
|
-
console.log(`${C.BRIGHT}Validating API Key:${C.RESET}`);
|
|
647
|
-
const apiKey = process.env.DEFLAKE_API_KEY;
|
|
648
|
-
if (apiKey) {
|
|
649
|
-
const maskedKey = apiKey.substring(0, 4) + '*'.repeat(Math.max(0, apiKey.length - 8)) + apiKey.substring(apiKey.length - 4);
|
|
650
|
-
console.log(` ✅ DEFLAKE_API_KEY found: ${C.GREEN}${maskedKey}${C.RESET}`);
|
|
651
219
|
} else {
|
|
652
|
-
console.log(`
|
|
653
|
-
console.log(` Fix: Run 'export DEFLAKE_API_KEY=your_key' or add it to your .env file.`);
|
|
220
|
+
console.log(` ${C.GRAY}└─ Status: ${C.RED}Awaiting credentials...${C.RESET}`);
|
|
654
221
|
}
|
|
655
222
|
console.log("");
|
|
656
223
|
|
|
657
|
-
//
|
|
658
|
-
console.log(`${C.
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
process.stdout.write(` ⏳ Pinging DeFlake API... `);
|
|
663
|
-
const result = await client.getUsage();
|
|
664
|
-
|
|
665
|
-
if (result.status === 'success') {
|
|
666
|
-
const usage = result.data;
|
|
667
|
-
process.stdout.write(`\r ✅ API Connected! (Tier: ${C.GREEN}${usage.tier.toUpperCase()}${C.RESET}) \n`);
|
|
668
|
-
const u = usage;
|
|
669
|
-
const remaining = u.limit - u.usage;
|
|
670
|
-
console.log(` Fix Quota: ${C.GREEN}${remaining}/${u.limit} remaining${C.RESET}`);
|
|
671
|
-
console.log(` Analysis: ${C.GREEN}FREE${C.RESET} (Pay-per-fix model enabled)`);
|
|
672
|
-
} else {
|
|
673
|
-
process.stdout.write(`\r ❌ ${C.RED}API Connectivity Failed${C.RESET} \n`);
|
|
674
|
-
if (result.code === 401 || result.code === 403) {
|
|
675
|
-
console.log(` Reason: API Key is invalid (Status ${result.code}).`);
|
|
676
|
-
} else {
|
|
677
|
-
console.log(` Reason: ${result.message} (Code: ${result.code || 'UNKNOWN'})`);
|
|
678
|
-
}
|
|
679
|
-
console.log(` API URL: ${client.apiUrl}`);
|
|
680
|
-
}
|
|
681
|
-
} catch (error) {
|
|
682
|
-
process.stdout.write(`\r ❌ ${C.RED}API Connectivity Error: ${error.message}${C.RESET}\n`);
|
|
683
|
-
}
|
|
684
|
-
// 6. Artifact & Permission Check
|
|
685
|
-
console.log(`${C.BRIGHT}Checking Artifact Permissions:${C.RESET}`);
|
|
686
|
-
const criticalDirs = [
|
|
687
|
-
{ path: 'test-results', label: 'Playwright Failures' },
|
|
688
|
-
{ path: 'playwright-report', label: 'Playwright Report' },
|
|
689
|
-
{ path: 'cypress/screenshots', label: 'Cypress Screenshots' },
|
|
690
|
-
{ path: 'cypress/videos', label: 'Cypress Videos' }
|
|
224
|
+
// 4. THE LIBERATION AUDIT (Permission Status)
|
|
225
|
+
console.log(`${C.CYAN}4. LIBERATION AUDIT (Permission Status)${C.RESET}`);
|
|
226
|
+
const artDirs = [
|
|
227
|
+
{ path: 'test-results', label: 'Test Results' },
|
|
228
|
+
{ path: 'playwright-report', label: 'HTML Report' }
|
|
691
229
|
];
|
|
230
|
+
let allLiberated = true;
|
|
231
|
+
let totalItems = 0;
|
|
692
232
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
233
|
+
for (const d of artDirs) {
|
|
234
|
+
if (fs.existsSync(d.path)) {
|
|
235
|
+
process.stdout.write(` ${C.GRAY}├─ ${d.label.padEnd(16)}: ${C.RESET}🔍 Checking...`);
|
|
236
|
+
let dItems = 0;
|
|
696
237
|
try {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
238
|
+
const scan = (p) => {
|
|
239
|
+
dItems++;
|
|
240
|
+
totalItems++;
|
|
241
|
+
fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK);
|
|
242
|
+
if (fs.statSync(p).isDirectory()) fs.readdirSync(p).forEach(f => scan(path.join(p, f)));
|
|
243
|
+
};
|
|
244
|
+
scan(d.path);
|
|
245
|
+
process.stdout.write(`\r ${C.GRAY}├─ ${d.label.padEnd(16)}: ${C.RESET}${C.GREEN}${C.BRIGHT}[LIBERADOS]${C.RESET}${C.GREEN} (${dItems} ítems accesibles)${C.RESET} \n`);
|
|
701
246
|
} catch (e) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
permsIssue = true;
|
|
247
|
+
process.stdout.write(`\r ${C.GRAY}├─ ${d.label.padEnd(16)}: ${C.RESET}${C.RED}${C.BRIGHT}[BLOQUEADOS]${C.RESET}${C.RED} (Error en: ${path.basename(e.path)})${C.RESET} \n`);
|
|
248
|
+
allLiberated = false;
|
|
705
249
|
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
if (permsIssue) {
|
|
710
|
-
console.log(`\n ${C.YELLOW}⚠️ Permission bottleneck detected!${C.RESET}`);
|
|
711
|
-
if (process.platform === 'win32') {
|
|
712
|
-
console.log(` ${C.BRIGHT}Windows Fix:${C.RESET} Run this in PowerShell as Admin:`);
|
|
713
|
-
console.log(` ${C.CYAN}icacls . /grant \${env:USERNAME}:(OI)(CI)F /T${C.RESET}`);
|
|
714
250
|
} else {
|
|
715
|
-
console.log(`
|
|
716
|
-
console.log(` ${C.CYAN}sudo chown -R $(whoami) .${C.RESET}`);
|
|
251
|
+
console.log(` ${C.GRAY}├─ ${d.label.padEnd(16)}: ${C.RESET}${C.GRAY}Sin carpeta (Aún no hay resultados)${C.RESET}`);
|
|
717
252
|
}
|
|
718
|
-
} else {
|
|
719
|
-
console.log(` ✅ All critical directories are accessible`);
|
|
720
253
|
}
|
|
721
|
-
console.log("");
|
|
722
254
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
255
|
+
if (!allLiberated) {
|
|
256
|
+
console.log(`\n ${C.BG_BLUE}${C.WHITE}${C.BRIGHT} ACCIÓN REQUERIDA: LIBERAR PERMISOS ${C.RESET}`);
|
|
257
|
+
const fix = process.platform === 'win32' ? 'icacls . /grant ${env:USERNAME}:(OI)(CI)F /T' : 'sudo chown -R $(whoami) .';
|
|
258
|
+
console.log(` Ejecuta esto en tu terminal: ${C.CYAN}${fix}${C.RESET}`);
|
|
726
259
|
} else {
|
|
727
|
-
console.log(`
|
|
260
|
+
console.log(` ${C.GRAY}└─ Auditoría: ${C.RESET}${C.GREEN}${totalItems} archivos verificados con éxito.${C.RESET}`);
|
|
261
|
+
console.log(`\n${C.BG_GREEN}${C.WHITE}${C.BRIGHT} ESTADO DEL SISTEMA: LIBERADO Y OPERATIVO ${C.RESET}`);
|
|
262
|
+
console.log(` Ya puedes correr: ${C.CYAN}npx deflake --fix --report npx playwright test${C.RESET}`);
|
|
728
263
|
}
|
|
729
264
|
console.log("\n" + C.GRAY + "─".repeat(50) + C.RESET + "\n");
|
|
730
265
|
}
|
|
731
266
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
try {
|
|
736
|
-
const filePath = result.location.fullRootPath;
|
|
737
|
-
const targetLine = parseInt(result.location.rootLine);
|
|
738
|
-
|
|
739
|
-
if (!fs.existsSync(filePath)) {
|
|
740
|
-
console.error(` ❌ [Self-Healing] File not found: ${filePath}`);
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
let patches = [];
|
|
745
|
-
try {
|
|
746
|
-
const parsed = JSON.parse(result.fix);
|
|
747
|
-
if (parsed.patches && Array.isArray(parsed.patches)) {
|
|
748
|
-
patches = parsed.patches;
|
|
749
|
-
} else if (parsed.code) {
|
|
750
|
-
patches = [{
|
|
751
|
-
file: filePath,
|
|
752
|
-
line: targetLine,
|
|
753
|
-
action: 'REPLACE',
|
|
754
|
-
new_line: parsed.code
|
|
755
|
-
}];
|
|
756
|
-
}
|
|
757
|
-
} catch (e) {
|
|
758
|
-
// Fallback for raw string fixes
|
|
759
|
-
patches = [{
|
|
760
|
-
file: filePath,
|
|
761
|
-
line: targetLine,
|
|
762
|
-
action: 'REPLACE',
|
|
763
|
-
new_line: result.fix
|
|
764
|
-
}];
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const backups = new Map();
|
|
768
|
-
|
|
769
|
-
try {
|
|
770
|
-
for (const patch of patches) {
|
|
771
|
-
const pFile = patch.file || filePath;
|
|
772
|
-
const pLine = parseInt(patch.line || targetLine);
|
|
773
|
-
const pAction = patch.action || 'REPLACE';
|
|
774
|
-
let pNew = patch.new_line;
|
|
775
|
-
|
|
776
|
-
if (!fs.existsSync(pFile)) continue;
|
|
777
|
-
|
|
778
|
-
// Create backup if not already done
|
|
779
|
-
if (!backups.has(pFile)) {
|
|
780
|
-
backups.set(pFile, fs.readFileSync(pFile, 'utf8'));
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const currentLines = fs.readFileSync(pFile, 'utf8').split('\n');
|
|
784
|
-
const originalLineIndex = pLine - 1;
|
|
785
|
-
|
|
786
|
-
if (originalLineIndex < 0 || originalLineIndex >= currentLines.length) continue;
|
|
787
|
-
|
|
788
|
-
const originalLine = currentLines[originalLineIndex];
|
|
789
|
-
const indentation = originalLine.match(/^\s*/)[0];
|
|
790
|
-
|
|
791
|
-
// CRITICAL: Prevent injecting JSON metadata into the code
|
|
792
|
-
if (pNew.trim().startsWith('{') && pNew.includes('"patches"') && pNew.includes(':')) {
|
|
793
|
-
console.log(` ❌ ${C.RED}[Self-Healing] Detected attempted JSON injection. Aborting patch for ${path.basename(pFile)}.${C.RESET}`);
|
|
794
|
-
continue;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const finalNewLine = indentation + pNew.trim();
|
|
798
|
-
|
|
799
|
-
// DUPLICATE PROTECTION: Don't insert/replace if code already exists
|
|
800
|
-
if (currentLines.some(l => l.trim() === pNew.trim())) {
|
|
801
|
-
console.log(` ℹ️ ${C.GRAY}[Self-Healing] Skipping duplicate patch for:${C.RESET} ${path.basename(pFile)}`);
|
|
802
|
-
continue;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
if (pAction === 'INSERT_AFTER') {
|
|
806
|
-
currentLines.splice(pLine, 0, finalNewLine);
|
|
807
|
-
fs.writeFileSync(pFile, currentLines.join('\n'));
|
|
808
|
-
console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully inserted at:${C.RESET} ${path.basename(pFile)}:${pLine}`);
|
|
809
|
-
} else {
|
|
810
|
-
currentLines[originalLineIndex] = finalNewLine;
|
|
811
|
-
fs.writeFileSync(pFile, currentLines.join('\n'));
|
|
812
|
-
console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully patched:${C.RESET} ${path.basename(pFile)}:${pLine}`);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// SYNTAX VALIDATION: Rollback if syntax is broken
|
|
816
|
-
if (pFile.endsWith('.ts') || pFile.endsWith('.js')) {
|
|
817
|
-
try {
|
|
818
|
-
const { execSync } = require('child_process');
|
|
819
|
-
execSync(`node --check "${pFile}"`, { stdio: 'ignore' });
|
|
820
|
-
} catch (err) {
|
|
821
|
-
console.log(` ⚠️ ${C.YELLOW}[Self-Healing] Patch broke syntax in ${path.basename(pFile)}. Rolling back...${C.RESET}`);
|
|
822
|
-
fs.writeFileSync(pFile, backups.get(pFile));
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
} catch (patchError) {
|
|
827
|
-
console.error(` ❌ ${C.RED}[Self-Healing] Error applying patches: ${patchError.message}${C.RESET}`);
|
|
828
|
-
// Emergency rollback for all involved files
|
|
829
|
-
for (const [file, content] of backups) {
|
|
830
|
-
fs.writeFileSync(file, content);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
} catch (error) {
|
|
834
|
-
console.error(` ❌ ${C.RED}[Self-Healing] Error in self-healing logic:${C.RESET} ${error.message}`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Automagically opens the framework's native HTML report.
|
|
840
|
-
*/
|
|
841
|
-
function showFrameworkReport() {
|
|
842
|
-
const framework = DeFlakeClient.detectFramework();
|
|
843
|
-
console.log(`\n${C.CYAN}📊 Opening HTML Report for ${framework.toUpperCase()}...${C.RESET}`);
|
|
844
|
-
|
|
845
|
-
let command = '';
|
|
846
|
-
const opener = process.platform === 'win32' ? 'start' : 'open';
|
|
847
|
-
|
|
848
|
-
if (framework === 'playwright') {
|
|
849
|
-
// Playwright default report port is 9323. Try to close previous instance if on Mac/Linux
|
|
850
|
-
try {
|
|
851
|
-
if (process.platform !== 'win32') {
|
|
852
|
-
const pid = require('child_process').execSync('lsof -t -i:9323').toString().trim();
|
|
853
|
-
if (pid) {
|
|
854
|
-
require('child_process').execSync(`kill ${pid}`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
} catch (e) {}
|
|
858
|
-
command = 'npx playwright show-report';
|
|
859
|
-
} else if (framework === 'cypress') {
|
|
860
|
-
// Broad search for common Cypress HTML reports
|
|
861
|
-
const candidates = [
|
|
862
|
-
'cypress/reports/html/index.html',
|
|
863
|
-
'cypress/reports/index.html',
|
|
864
|
-
'mochawesome-report/mochawesome.html',
|
|
865
|
-
'reports/index.html'
|
|
866
|
-
];
|
|
867
|
-
|
|
868
|
-
const found = candidates.find(p => fs.existsSync(p));
|
|
869
|
-
if (found) {
|
|
870
|
-
command = `${opener} ${found}`;
|
|
871
|
-
} else {
|
|
872
|
-
console.log(`${C.GRAY}ℹ️ Cypress HTML report not found. Verify your reporter configuration (e.g. mochawesome).${C.RESET}`);
|
|
873
|
-
console.log(` ${C.GRAY}Looked in: ${candidates.join(', ')}${C.RESET}`);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
} else if (framework === 'webdriverio') {
|
|
877
|
-
// WDIO usually uses Allure or spec-reporter
|
|
878
|
-
if (fs.existsSync('allure-results')) {
|
|
879
|
-
console.log(` ${C.GRAY}Allure results detected. Attempting to serve...${C.RESET}`);
|
|
880
|
-
command = 'npx allure serve allure-results';
|
|
881
|
-
} else if (fs.existsSync('reports/html/index.html')) {
|
|
882
|
-
command = `${opener} reports/html/index.html`;
|
|
883
|
-
} else {
|
|
884
|
-
console.log(`${C.GRAY}ℹ️ WebdriverIO HTML report not found. If you use Allure, ensure allure-results folder exists.${C.RESET}`);
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
if (command) {
|
|
890
|
-
// Use inherit if it's a server (like allure serve) or a long-running process
|
|
891
|
-
const isServer = command.includes('serve') || command.includes('show-report');
|
|
892
|
-
spawn(command, { shell: true, stdio: isServer ? 'inherit' : 'ignore' });
|
|
893
|
-
}
|
|
267
|
+
function showReport() {
|
|
268
|
+
spawn('npx playwright show-report', { shell: true, stdio: 'inherit' }).on('error', () => {});
|
|
894
269
|
}
|
|
895
|
-
|
|
896
|
-
async function analyzeFailures(artifacts, fullLog, client) {
|
|
897
|
-
if (artifacts.length === 0) {
|
|
898
|
-
console.log("⚠️ No error artifacts found.");
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// 1. Check Quota / Tier
|
|
903
|
-
const result = await client.getUsage();
|
|
904
|
-
let limit = 100; // Default safety cap
|
|
905
|
-
let tier = 'unknown';
|
|
906
|
-
|
|
907
|
-
if (result.status === 'success') {
|
|
908
|
-
const usage = result.data;
|
|
909
|
-
tier = usage.tier || 'free';
|
|
910
|
-
if (tier === 'free') limit = 10;
|
|
911
|
-
else if (tier === 'pro') limit = 50;
|
|
912
|
-
else if (tier === 'master' || tier === 'byok') limit = 100;
|
|
913
|
-
|
|
914
|
-
console.log(`🎫 Subscription: ${tier.toUpperCase()} | Monthly Usage: ${usage.usage}/${usage.limit}`);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
if (artifacts.length > limit) {
|
|
918
|
-
console.log(`${C.YELLOW}⚠️ Detected ${artifacts.length} failures, but your ${tier} plan limits batch analysis to ${limit} unique fixes.${C.RESET}`);
|
|
919
|
-
console.log(` (Processing the first ${limit}...)\n`);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const framework = DeFlakeClient.detectFramework();
|
|
923
|
-
const results = [];
|
|
924
|
-
const processLimit = Math.min(artifacts.length, limit);
|
|
925
|
-
const batchArtifacts = artifacts.slice(0, processLimit);
|
|
926
|
-
|
|
927
|
-
console.log(`🔍 Analyzing ${batchArtifacts.length} failure(s)...`);
|
|
928
|
-
|
|
929
|
-
const playwrightBlocks = (framework === 'playwright') ? parsePlaywrightLogs(fullLog || "") : [];
|
|
930
|
-
const cypressBlocks = (framework === 'cypress') ? parseCypressLogs(fullLog || "") : [];
|
|
931
|
-
const pytestBlocks = (framework === 'selenium-python') ? parsePytestLogs(fullLog || "") : [];
|
|
932
|
-
|
|
933
|
-
for (const art of batchArtifacts) {
|
|
934
|
-
let specificLog = fullLog;
|
|
935
|
-
|
|
936
|
-
if (framework === 'cypress' && cypressBlocks.length > 0) {
|
|
937
|
-
const match = cypressBlocks.find(b => art.htmlPath && art.htmlPath.includes(b.spec));
|
|
938
|
-
if (match) specificLog = match.content;
|
|
939
|
-
} else if (framework === 'playwright' && playwrightBlocks.length > 0) {
|
|
940
|
-
// Heuristic matching for Playwright error blocks
|
|
941
|
-
let bestMatch = null;
|
|
942
|
-
let bestScore = -1;
|
|
943
|
-
const artifactTokens = art.name.toLowerCase().replace(/[-_]/g, ' ').split(' ').filter(w => w.length > 3);
|
|
944
|
-
|
|
945
|
-
for (const block of playwrightBlocks) {
|
|
946
|
-
let score = 0;
|
|
947
|
-
const blockLower = block.content.toLowerCase();
|
|
948
|
-
for (const token of artifactTokens) { if (blockLower.includes(token)) score++; }
|
|
949
|
-
if (score > bestScore) { bestScore = score; bestMatch = block; }
|
|
950
|
-
}
|
|
951
|
-
if (bestMatch && bestScore > 0) {
|
|
952
|
-
specificLog = bestMatch.content;
|
|
953
|
-
}
|
|
954
|
-
} else if (framework === 'selenium-python' && pytestBlocks.length > 0) {
|
|
955
|
-
// Match pytest block by test name heuristic
|
|
956
|
-
const match = pytestBlocks.find(b => art.name && b.name.includes(art.name.replace('.png', '')));
|
|
957
|
-
if (match) specificLog = match.content;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const displayName = (art.name || 'Unknown Artifact').substring(0, 40);
|
|
961
|
-
process.stdout.write(`\r⏳ Analyzing ${displayName}... `);
|
|
962
|
-
|
|
963
|
-
try {
|
|
964
|
-
const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name, argv.fix);
|
|
965
|
-
|
|
966
|
-
if (result && result.status === 'success') {
|
|
967
|
-
results.push(result);
|
|
968
|
-
} else {
|
|
969
|
-
console.log(`\n ${C.RED}❌ Analysis failed for ${displayName}:${C.RESET} ${result?.detail || 'Check server status'}`);
|
|
970
|
-
}
|
|
971
|
-
} catch (err) {
|
|
972
|
-
console.log(`\n ${C.RED}❌ Error analyzing ${displayName}:${C.RESET} ${err.message}`);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
console.log("\r✅ Analysis complete. ");
|
|
976
|
-
|
|
977
|
-
// GROUPING & PRINTING
|
|
978
|
-
const groups = {};
|
|
979
|
-
for (const res of results) {
|
|
980
|
-
let fixCode = res.fix;
|
|
981
|
-
try {
|
|
982
|
-
const p = JSON.parse(res.fix);
|
|
983
|
-
if (p.code) fixCode = p.code;
|
|
984
|
-
} catch (e) { }
|
|
985
|
-
|
|
986
|
-
const locId = res.location ? `${res.location.rootFile}:${res.location.rootLine}` : `unknown-${res.testName}`;
|
|
987
|
-
const key = `${locId}|${fixCode.trim()}`;
|
|
988
|
-
|
|
989
|
-
if (!groups[key]) {
|
|
990
|
-
groups[key] = { ...res, count: 0, locations: [] };
|
|
991
|
-
}
|
|
992
|
-
groups[key].count++;
|
|
993
|
-
if (res.location) {
|
|
994
|
-
groups[key].locations.push(res.location);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const finalGroups = Object.values(groups);
|
|
999
|
-
for (const group of finalGroups) {
|
|
1000
|
-
if (group.count > 1) {
|
|
1001
|
-
console.log(`${C.GRAY}ℹ️ Suggested for ${group.count} similar failures:${C.RESET}`);
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// APPLY FIX (To all instances in the group)
|
|
1005
|
-
if (argv.fix) {
|
|
1006
|
-
for (const loc of group.locations) {
|
|
1007
|
-
await applySelfHealing({ ...group, location: loc });
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
printDetailedFix(group.fix, group.location, group.source_code, argv.fix);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// SUMMARY
|
|
1015
|
-
console.log(`\n${C.BRIGHT}📊 DeFlake Summary:${C.RESET}`);
|
|
1016
|
-
console.log(` - Failures analyzed: ${batchArtifacts.length}`);
|
|
1017
|
-
console.log(` - Fixes suggested: ${results.length}`);
|
|
1018
|
-
|
|
1019
|
-
try {
|
|
1020
|
-
// Fetch updated usage for clearer reporting
|
|
1021
|
-
const updatedUsage = await client.getUsage();
|
|
1022
|
-
if (updatedUsage.status === 'success') {
|
|
1023
|
-
const u = updatedUsage.data;
|
|
1024
|
-
const remaining = u.limit - u.usage;
|
|
1025
|
-
console.log(` - Fix Quota: ${C.GREEN}${remaining}/${u.limit} remaining (Analysis is FREE)${C.RESET}`);
|
|
1026
|
-
}
|
|
1027
|
-
} catch (e) {
|
|
1028
|
-
// usage fetch failed is non-critical
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
if (results.length === 0) {
|
|
1032
|
-
console.log(`${C.GRAY}ℹ️ DeFlake analyzed the logs but couldn't find a confident fix for these errors.${C.RESET}`);
|
|
1033
|
-
if (!fullLog) {
|
|
1034
|
-
console.log(`${C.YELLOW}⚠️ Tip: Ensure your test runner output is being captured correctly.${C.RESET}`);
|
|
1035
|
-
}
|
|
1036
|
-
} else if (!argv.fix) {
|
|
1037
|
-
console.log(`\n${C.BRIGHT}💡 Tip: Use ${C.CYAN}--fix${C.RESET}${C.BRIGHT} to automatically apply these suggested fixes next time.${C.RESET}`);
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
return results.length;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* Helper to run a shell command and capture output.
|
|
1046
|
-
*/
|
|
1047
|
-
async function runCommand(cmd, args) {
|
|
1048
|
-
return new Promise((resolve) => {
|
|
1049
|
-
console.log(`🚀 Running command: ${cmd} ${args.join(' ')}`);
|
|
1050
|
-
const child = spawn(cmd, args, { shell: true, stdio: 'pipe' });
|
|
1051
|
-
let stdout = '';
|
|
1052
|
-
let stderr = '';
|
|
1053
|
-
|
|
1054
|
-
child.stdout.on('data', (data) => { process.stdout.write(data); stdout += data.toString(); });
|
|
1055
|
-
child.stderr.on('data', (data) => { process.stderr.write(data); stderr += data.toString(); });
|
|
1056
|
-
|
|
1057
|
-
child.on('close', (code) => {
|
|
1058
|
-
resolve({ code, output: stdout + "\n" + stderr });
|
|
1059
|
-
});
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
async function main() {
|
|
1064
|
-
const command = argv._ || [];
|
|
1065
|
-
let finalExitCode = 0;
|
|
1066
|
-
|
|
1067
|
-
// If 'doctor' or 'migrate' was called, don't proceed to wrapper logic
|
|
1068
|
-
if (command.includes('doctor') || command.includes('migrate')) return;
|
|
1069
|
-
|
|
1070
|
-
console.log(`🚑 DeFlake JS Client (Batch Mode) - ${C.CYAN}v${pkg.version}${C.RESET}`);
|
|
1071
|
-
const client = new DeFlakeClient(argv.apiUrl);
|
|
1072
|
-
const fw = client.framework.charAt(0).toUpperCase() + client.framework.slice(1);
|
|
1073
|
-
console.log(`🔍 Detected Framework: ${C.GREEN}${fw}${C.RESET}`);
|
|
1074
|
-
|
|
1075
|
-
if (command.length > 0) {
|
|
1076
|
-
const cmd = command[0];
|
|
1077
|
-
const args = command.slice(1);
|
|
1078
|
-
|
|
1079
|
-
let { code, output } = await runCommand(cmd, args);
|
|
1080
|
-
|
|
1081
|
-
if (code !== 0) {
|
|
1082
|
-
console.log(`\n🔴 Command failed with code ${code}. Activating DeFlake...`);
|
|
1083
|
-
const artifacts = detectAllArtifacts(null, argv.html);
|
|
1084
|
-
const fixesApplied = await analyzeFailures(artifacts, output, client);
|
|
1085
|
-
|
|
1086
|
-
let outcomeCode = code;
|
|
1087
|
-
// AUTO-VERIFICATION
|
|
1088
|
-
if (fixesApplied > 0 && argv.fix) {
|
|
1089
|
-
console.log(`\n${C.BRIGHT}💉 Fixes applied. Re-running tests to verify...${C.RESET}`);
|
|
1090
|
-
const secondRun = await runCommand(cmd, args);
|
|
1091
|
-
outcomeCode = secondRun.code;
|
|
1092
|
-
if (secondRun.code === 0) {
|
|
1093
|
-
console.log(`\n${C.GREEN}${C.BRIGHT}✅ All tests passed after DeFlake healing!${C.RESET}`);
|
|
1094
|
-
} else {
|
|
1095
|
-
console.log(`\n${C.YELLOW}⚠️ Some tests still failing after fixes. Check the report for details.${C.RESET}`);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
finalExitCode = outcomeCode;
|
|
1099
|
-
} else {
|
|
1100
|
-
console.log("\n🟢 Command passed successfully.");
|
|
1101
|
-
finalExitCode = 0;
|
|
1102
|
-
}
|
|
1103
|
-
} else {
|
|
1104
|
-
const artifacts = detectAllArtifacts(argv.log, argv.html);
|
|
1105
|
-
const fullLog = argv.log && fs.existsSync(argv.log) ? fs.readFileSync(argv.log, 'utf8') : null;
|
|
1106
|
-
await analyzeFailures(artifacts, fullLog, client);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// FINAL REPORT TRIGGER
|
|
1110
|
-
if (argv.report) {
|
|
1111
|
-
showFrameworkReport();
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
process.exit(finalExitCode);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// main() call is now handled by the check above
|