deflake 1.0.0

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 (4) hide show
  1. package/README.md +34 -0
  2. package/cli.js +468 -0
  3. package/client.js +75 -0
  4. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # DeFlake (JavaScript Client) ā„ļø
2
+
3
+ > **Stop Flaky Tests. Start Auto-Healing.**
4
+ > Official Node.js client for DeFlake.
5
+
6
+ ## šŸš€ Installation
7
+
8
+ ```bash
9
+ npm install deflake --save-dev
10
+ ```
11
+
12
+ ## šŸ”§ Usage
13
+
14
+ ### Wrapper Mode (Zero Config)
15
+ Simply prepend `npx deflake` to your test run command:
16
+
17
+ ```bash
18
+ # Playwright
19
+ export DEFLAKE_API_KEY="your-api-key"
20
+ npx deflake npx playwright test
21
+
22
+ # Cypress
23
+ npx deflake npx cypress run
24
+ ```
25
+
26
+ ### Manual Mode
27
+ Analyze an existing error log and HTML snapshot:
28
+
29
+ ```bash
30
+ npx deflake --log error.log --html playwright-report/index.html
31
+ ```
32
+
33
+ ## šŸ“„ License
34
+ ISC
package/cli.js ADDED
@@ -0,0 +1,468 @@
1
+ #!/usr/bin/env node
2
+ const yargs = require('yargs/yargs');
3
+ const { hideBin } = require('yargs/helpers');
4
+ const DeFlakeClient = require('./client');
5
+ const { spawn } = require('child_process');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const argv = yargs(hideBin(process.argv))
10
+ .option('log', {
11
+ alias: 'l',
12
+ type: 'string',
13
+ description: 'Path to the error log file',
14
+ demandOption: false // No longer strictly required if running in wrapper mode
15
+ })
16
+ .option('html', {
17
+ alias: 'h',
18
+ type: 'string',
19
+ description: 'Path to the HTML snapshot',
20
+ demandOption: false // No longer strictly required if auto-detected
21
+ })
22
+ .option('api-url', {
23
+ type: 'string',
24
+ description: 'Override Default API URL',
25
+ })
26
+ .help()
27
+ .argv;
28
+
29
+ // Helper to auto-detect artifacts (Batch Mode)
30
+ function detectAllArtifacts(providedLog, providedHtml) {
31
+ // If user provided explicit paths, use them as a single item list
32
+ if (providedHtml || providedLog) {
33
+ return [{
34
+ logPath: providedLog,
35
+ htmlPath: providedHtml,
36
+ id: 'manual-input',
37
+ name: 'Manual Input'
38
+ }];
39
+ }
40
+
41
+ const detected = [];
42
+
43
+ // 1. Scan test-results
44
+ if (fs.existsSync('test-results')) {
45
+ function findFiles(dir) {
46
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
47
+ for (const entry of entries) {
48
+ const res = path.resolve(dir, entry.name);
49
+ if (entry.isDirectory()) {
50
+ findFiles(res);
51
+ } else {
52
+ // Unique ID for this test failure is the parent folder name
53
+ const folderName = path.basename(path.dirname(res));
54
+
55
+ // We only want ONE artifact per test folder.
56
+ // Priority: error-context.md > index.html > trace.zip
57
+
58
+ // But we can't easily dedup here without a map.
59
+ // Let's just collect all candidates and filter later.
60
+ }
61
+ }
62
+ }
63
+
64
+ // Better approach: Iterate folders in test-results
65
+ const testFolders = fs.readdirSync('test-results', { withFileTypes: true })
66
+ .filter(dirent => dirent.isDirectory())
67
+ .map(dirent => path.resolve('test-results', dirent.name));
68
+
69
+ for (const folder of testFolders) {
70
+ let artifact = null;
71
+ let type = '';
72
+
73
+ const mdPath = path.join(folder, 'error-context.md');
74
+ const htmlPath = path.join(folder, 'trace.html'); // Some reporters use this
75
+
76
+ // Check for error-context.md (High Quality)
77
+ if (fs.existsSync(mdPath)) {
78
+ artifact = mdPath;
79
+ type = 'md';
80
+ }
81
+ // Else check for any HTML that isn't trace viewer boilerplate
82
+ else {
83
+ // simple fallback
84
+ }
85
+
86
+ if (artifact) {
87
+ detected.push({
88
+ logPath: null, // Log is usually embedded or passed via stdout
89
+ htmlPath: artifact,
90
+ id: path.basename(folder),
91
+ name: path.basename(folder)
92
+ });
93
+ }
94
+ }
95
+ }
96
+
97
+ // Fallback: If no granular results, check global report
98
+ if (detected.length === 0) {
99
+ if (fs.existsSync('playwright-report/index.html')) {
100
+ detected.push({
101
+ logPath: providedLog,
102
+ htmlPath: 'playwright-report/index.html',
103
+ id: 'global-report',
104
+ name: 'Global Report'
105
+ });
106
+ }
107
+ }
108
+
109
+ return detected;
110
+ }
111
+
112
+ function stripAnsi(str) {
113
+ return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
114
+ }
115
+
116
+ function parsePlaywrightLogs(fullLog) {
117
+ // Strip ANSI codes to reliable regex matching
118
+ const cleanLog = stripAnsi(fullLog);
119
+ const errorBlocks = [];
120
+
121
+ // Regex to find start of a test failure: " 1) [chromium] › ..."
122
+ const errorHeaderRegex = /^\s*(\d+)\)\s+\[(.*?)\]\s+/gm;
123
+ let match;
124
+
125
+ // Find all start indices
126
+ const indices = [];
127
+ while ((match = errorHeaderRegex.exec(cleanLog)) !== null) {
128
+ indices.push({ index: match.index, browser: match[2] }); // Group 2 is browser
129
+ }
130
+
131
+ // Slice log into blocks
132
+ for (let i = 0; i < indices.length; i++) {
133
+ const start = indices[i].index;
134
+ const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
135
+ const content = cleanLog.slice(start, end);
136
+ errorBlocks.push({
137
+ browser: indices[i].browser, // e.g. 'chromium'
138
+ content: content
139
+ });
140
+ }
141
+
142
+ return errorBlocks;
143
+ }
144
+
145
+ async function runHealer(logContent, htmlPath, apiUrl, testName) {
146
+ // Check file size limit (Basic check to avoid 429s on huge files)
147
+ if (htmlPath && fs.existsSync(htmlPath)) {
148
+ const stats = fs.statSync(htmlPath);
149
+ if (stats.size > 5 * 1024 * 1024) { // 5MB limit warning
150
+ console.warn(`āš ļø Warning: Artifact for ${testName} is large (` + (stats.size / 1024 / 1024).toFixed(2) + "MB).");
151
+ }
152
+ }
153
+
154
+ const client = new DeFlakeClient(apiUrl);
155
+
156
+ // Pass log CONTENT (string) directly if it came from wrapper, or file path if from args
157
+ let finalLogPath = argv.log;
158
+ let effectiveLogContent = logContent;
159
+
160
+ // Robustness: If logContent is empty but we have an MD artifact, read it
161
+ if (!effectiveLogContent && htmlPath && htmlPath.endsWith('.md') && fs.existsSync(htmlPath)) {
162
+ try {
163
+ effectiveLogContent = fs.readFileSync(htmlPath, 'utf8');
164
+ } catch (e) { }
165
+ }
166
+
167
+ if (effectiveLogContent) {
168
+ // We have content (from stdio or artifact), write to temp file
169
+ finalLogPath = '.deflake-error.log';
170
+ fs.writeFileSync(finalLogPath, effectiveLogContent);
171
+ } else if (!finalLogPath && fs.existsSync('error.log')) {
172
+ // Fallback to auto-detected error.log if nothing else
173
+ finalLogPath = 'error.log';
174
+ }
175
+
176
+ // Validation before calling client
177
+ if (!finalLogPath) {
178
+ // Create a dummy log if we have HTML but absolutely no log, to prevent client crash
179
+ if (htmlPath) {
180
+ console.log(" (Creating temporary log from available context)");
181
+ finalLogPath = '.deflake-error.log';
182
+ fs.writeFileSync(finalLogPath, "Log missing. Refer to HTML/MD snapshot.");
183
+ effectiveLogContent = "Log missing.";
184
+ } else {
185
+ console.error(`āŒ [${testName}] Error: No log file or content available.`);
186
+ return;
187
+ }
188
+ }
189
+
190
+ // 🧠 SMART CONTEXT: Extract the failing source code if possible
191
+ const failureLoc = extractFailureLocation(effectiveLogContent || fs.readFileSync(finalLogPath, 'utf8'));
192
+
193
+ let sourceCodeContent = null;
194
+ if (failureLoc && failureLoc.fullRootPath) {
195
+ try {
196
+ if (fs.existsSync(failureLoc.fullRootPath)) {
197
+ sourceCodeContent = fs.readFileSync(failureLoc.fullRootPath, 'utf8');
198
+ }
199
+ } catch (e) { }
200
+ }
201
+
202
+ if (!failureLoc) {
203
+ // Silent warning for batch mode - don't clutter output
204
+ }
205
+
206
+ try {
207
+ const result = await client.heal(finalLogPath, htmlPath, failureLoc, sourceCodeContent);
208
+
209
+ if (result && result.status === 'success') {
210
+ // Return structured object for grouping
211
+ return {
212
+ testName,
213
+ location: failureLoc,
214
+ sourceCode: sourceCodeContent,
215
+ fix: result.fix,
216
+ status: 'success'
217
+ };
218
+ }
219
+ // Return original result so main() can check for QUOTA_EXCEEDED
220
+ return result || { status: 'error', detail: 'UNKNOWN' };
221
+ } catch (error) {
222
+ return { status: 'error', detail: error.message };
223
+ }
224
+ }
225
+
226
+ // ... (extractFailureLocation and printDetailedFix remain mostly same, just slight tweaks for robustness) ...
227
+ // 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.
228
+ // To avoid massive token usage, I will use the existing helper functions but just update Main.
229
+
230
+ // WAIT, I must provide the implementations of extractFailureLocation and printDetailedFix if I replace the whole file or large chunks.
231
+ // The previous tool call view_file shows I have the whole content. I will rewrite the whole file to be safe and clean.
232
+
233
+ function extractFailureLocation(logText) {
234
+ if (!logText) return null;
235
+ const loc = {
236
+ specFile: null,
237
+ testLine: null,
238
+ rootFile: null,
239
+ fullRootPath: null,
240
+ rootLine: null,
241
+ stepLine: null
242
+ };
243
+
244
+ const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+›\s+(.*?.spec.ts):(\d+):\d+\s+›/m);
245
+ if (testMatch) {
246
+ loc.specFile = testMatch[1];
247
+ loc.testLine = testMatch[2];
248
+ }
249
+
250
+ const stackRegex = /at\s+(?:.*? \()?((?:[a-zA-Z]:\\|[\/~]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?/g;
251
+ let match;
252
+ let foundRoot = false;
253
+
254
+ while ((match = stackRegex.exec(logText)) !== null) {
255
+ const file = match[1];
256
+ const line = match[2];
257
+ if (!foundRoot && !file.includes('node_modules')) {
258
+ loc.rootFile = file.split('/').pop();
259
+ loc.fullRootPath = path.resolve(process.cwd(), file);
260
+ loc.rootLine = line;
261
+ foundRoot = true;
262
+ }
263
+ if (loc.specFile && file.endsWith(loc.specFile)) {
264
+ loc.stepLine = line;
265
+ }
266
+ }
267
+
268
+ // Fallback: If header regex failed but we found a root file that looks like a test
269
+ if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.'))) {
270
+ loc.specFile = loc.rootFile;
271
+ loc.testLine = loc.rootLine;
272
+ }
273
+
274
+ if (loc.specFile || loc.rootFile) return loc;
275
+ return null;
276
+ }
277
+
278
+ function printDetailedFix(fixText, location, sourceCode = null) {
279
+ const C = {
280
+ RESET: "\x1b[0m",
281
+ BRIGHT: "\x1b[1m",
282
+ RED: "\x1b[31m",
283
+ GREEN: "\x1b[32m",
284
+ YELLOW: "\x1b[33m",
285
+ CYAN: "\x1b[36m",
286
+ BLUE: "\x1b[34m",
287
+ GRAY: "\x1b[90m",
288
+ WHITE: "\x1b[37m"
289
+ };
290
+
291
+ let fixCode = fixText;
292
+ let explanation = null;
293
+
294
+ try {
295
+ const parsed = JSON.parse(fixText);
296
+ if (parsed.code) {
297
+ fixCode = parsed.code;
298
+ explanation = parsed.reason;
299
+ }
300
+ } catch (e) { }
301
+
302
+ console.log("\n" + C.GRAY + "─".repeat(50) + C.RESET);
303
+ if (location) {
304
+ // Label is default color, Value is colored
305
+ if (location.rootFile && location.rootLine) {
306
+ console.log(`šŸ’„ Runtime Error: ${C.RED}${location.rootFile}:${location.rootLine}${C.RESET}`);
307
+ }
308
+ if (location.specFile) {
309
+ // Calculate a nice label for the target
310
+ let targetLabel = location.specFile;
311
+ if (location.testLine) targetLabel += `:${location.testLine}`;
312
+ console.log(`šŸŽÆ Fix Target: ${C.CYAN}${targetLabel} (Definition)${C.RESET}`);
313
+ }
314
+ }
315
+ console.log(C.GRAY + "─".repeat(50) + C.RESET);
316
+
317
+ console.log(`${C.GREEN}${C.BRIGHT}✨ DEFLAKE SUGGESTION:${C.RESET}`);
318
+ if (explanation) console.log(`${C.GRAY}// ${explanation}${C.RESET}`);
319
+
320
+ // Print code with simple coloring
321
+ fixCode.split('\n').forEach(line => {
322
+ let colored = line
323
+ .replace(/(\/\/.*)/g, `${C.GRAY}$1${C.RESET}`)
324
+ .replace(/\b(const|let|var|await|async|function|return)\b/g, `${C.YELLOW}$1${C.RESET}`)
325
+ .replace(/('.*?')|(".*?")|(`.*?`)/g, `${C.GREEN}$1${C.RESET}`)
326
+ .replace(/(\.click|\.fill|\.locator)/g, `${C.CYAN}$1${C.RESET}`);
327
+ console.log(" " + colored);
328
+ });
329
+ console.log(C.GRAY + "─".repeat(50) + C.RESET);
330
+ }
331
+
332
+ /**
333
+ * Core analysis engine for batch mode.
334
+ * Enforces tier limits and calculates deduplicated results.
335
+ */
336
+ async function analyzeFailures(artifacts, fullLog, client) {
337
+ const C = {
338
+ RESET: "\x1b[0m",
339
+ YELLOW: "\x1b[33m",
340
+ RED: "\x1b[31m",
341
+ GRAY: "\x1b[90m"
342
+ };
343
+
344
+ if (artifacts.length === 0) {
345
+ console.log("āš ļø No error artifacts found.");
346
+ return;
347
+ }
348
+
349
+ // 1. Check Quota / Tier
350
+ const usage = await client.getUsage();
351
+ let limit = 100; // Default safety cap
352
+ let tier = 'unknown';
353
+
354
+ if (usage) {
355
+ tier = usage.tier || 'free';
356
+ if (tier === 'free') limit = 5;
357
+ else if (tier === 'pro') limit = 50;
358
+ else if (tier === 'master' || tier === 'byok') limit = 100;
359
+
360
+ console.log(`šŸŽ« Subscription: ${tier.toUpperCase()} | Quota: ${usage.usage}/${usage.limit}`);
361
+ }
362
+
363
+ if (artifacts.length > limit) {
364
+ console.log(`${C.YELLOW}āš ļø Detected ${artifacts.length} failures, but your ${tier} plan limits batch analysis to ${limit} unique fixes.${C.RESET}`);
365
+ console.log(` (Processing the first ${limit}...)\n`);
366
+ }
367
+
368
+ const errorBlocks = parsePlaywrightLogs(fullLog || "");
369
+ const results = [];
370
+ const processLimit = Math.min(artifacts.length, limit);
371
+ const batchArtifacts = artifacts.slice(0, processLimit);
372
+
373
+ console.log(`šŸ” Analyzing ${batchArtifacts.length} failure(s)...`);
374
+
375
+ for (const art of batchArtifacts) {
376
+ let specificLog = fullLog;
377
+ if (art.htmlPath && art.htmlPath.endsWith('.md') && errorBlocks.length > 0) {
378
+ let bestMatch = null;
379
+ let bestScore = -1;
380
+ const artifactTokens = art.name.toLowerCase().replace(/[-_]/g, ' ').split(' ').filter(w => w.length > 3 && w !== 'chromium' && w !== 'firefox' && w !== 'webkit');
381
+ const browser = art.name.toLowerCase().includes('firefox') ? 'firefox' : art.name.toLowerCase().includes('webkit') ? 'webkit' : 'chromium';
382
+ const candidates = errorBlocks.filter(b => b.browser.toLowerCase().includes(browser));
383
+
384
+ for (const block of candidates) {
385
+ let score = 0;
386
+ const blockLower = block.content.toLowerCase();
387
+ for (const token of artifactTokens) { if (blockLower.includes(token)) score++; }
388
+ if (score > bestScore) { bestScore = score; bestMatch = block; }
389
+ }
390
+ if (bestMatch && bestScore > 0) {
391
+ specificLog = bestMatch.content;
392
+ const idx = errorBlocks.indexOf(bestMatch);
393
+ if (idx > -1) errorBlocks.splice(idx, 1);
394
+ }
395
+ }
396
+
397
+ process.stdout.write(`\rā³ Analyzing ${art.name}... `);
398
+ const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name);
399
+
400
+ if (result && result.status === 'error' && result.detail === 'QUOTA_EXCEEDED') {
401
+ console.log(`\n${C.RED}šŸ›‘ Quota exceeded! Stopping.${C.RESET}`);
402
+ break;
403
+ }
404
+
405
+ if (result && result.status === 'success') {
406
+ results.push(result);
407
+ }
408
+ }
409
+ console.log("\rāœ… Analysis complete. \n");
410
+
411
+ // GROUPING & PRINTING
412
+ const groups = {};
413
+ for (const res of results) {
414
+ if (!res.location) continue;
415
+ const key = `${res.location.rootFile}:${res.location.rootLine}|${JSON.stringify(res.fix)}`;
416
+ if (!groups[key]) {
417
+ groups[key] = { location: res.location, sourceCode: res.sourceCode, fix: res.fix, tests: [] };
418
+ }
419
+ groups[key].tests.push(res.testName);
420
+ }
421
+
422
+ const sortedKeys = Object.keys(groups).sort((a, b) => {
423
+ const lineA = parseInt(groups[a].location?.rootLine) || 0;
424
+ const lineB = parseInt(groups[b].location?.rootLine) || 0;
425
+ return lineA - lineB;
426
+ });
427
+
428
+ for (const key of sortedKeys) {
429
+ printDetailedFix(groups[key].fix, groups[key].location, groups[key].sourceCode);
430
+ }
431
+ }
432
+
433
+ async function main() {
434
+ console.log("šŸš‘ DeFlake JS Client (Batch Mode)");
435
+ const client = new DeFlakeClient(argv.apiUrl);
436
+ const command = argv._;
437
+
438
+ if (command.length > 0) {
439
+ const cmd = command[0];
440
+ const args = command.slice(1);
441
+ console.log(`šŸš€ Running command: ${cmd} ${args.join(' ')}`);
442
+
443
+ const child = spawn(cmd, args, { shell: true, stdio: 'pipe' });
444
+ let stdout = '';
445
+ let stderr = '';
446
+
447
+ child.stdout.on('data', (data) => { process.stdout.write(data); stdout += data.toString(); });
448
+ child.stderr.on('data', (data) => { process.stderr.write(data); stderr += data.toString(); });
449
+
450
+ child.on('close', async (code) => {
451
+ if (code !== 0) {
452
+ console.log(`\nšŸ”“ Command failed with code ${code}. Activating DeFlake...`);
453
+ const artifacts = detectAllArtifacts(null, argv.html);
454
+ await analyzeFailures(artifacts, stdout + "\n" + stderr, client);
455
+ process.exit(code);
456
+ } else {
457
+ console.log("\n🟢 Command passed successfully.");
458
+ process.exit(0);
459
+ }
460
+ });
461
+ } else {
462
+ const artifacts = detectAllArtifacts(argv.log, argv.html);
463
+ const fullLog = argv.log && fs.existsSync(argv.log) ? fs.readFileSync(argv.log, 'utf8') : null;
464
+ await analyzeFailures(artifacts, fullLog, client);
465
+ }
466
+ }
467
+
468
+ main();
package/client.js ADDED
@@ -0,0 +1,75 @@
1
+ const axios = require('axios');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ require('dotenv').config();
5
+
6
+ class DeFlakeClient {
7
+ constructor(apiUrl, apiKey) {
8
+ // Use environment variable first, then fallback to a placeholder production URL
9
+ // User can still pass a custom URL for local development/B2B setups
10
+ this.productionUrl = 'https://deflake-api.up.railway.app/api/deflake';
11
+ this.apiUrl = apiUrl || process.env.DEFLAKE_API_URL || this.productionUrl;
12
+ this.apiKey = apiKey || process.env.DEFLAKE_API_KEY;
13
+
14
+ 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);
19
+ }
20
+ }
21
+
22
+ async getUsage() {
23
+ try {
24
+ const usageUrl = this.apiUrl.replace('/deflake', '/user/usage');
25
+ const response = await axios.get(usageUrl, {
26
+ headers: { 'X-API-KEY': this.apiKey }
27
+ });
28
+ return response.data;
29
+ } catch (error) {
30
+ return null; // Silent failure for usage check
31
+ }
32
+ }
33
+
34
+ async heal(logPath, htmlPath, failureLocation = null, sourceCode = null) {
35
+ try {
36
+ // ... (rest of the check logic) ...
37
+ if (!fs.existsSync(logPath)) throw new Error(`Log file not found: ${logPath}`);
38
+ if (!fs.existsSync(htmlPath)) throw new Error(`HTML file not found: ${htmlPath}`);
39
+
40
+ const logContent = fs.readFileSync(logPath, 'utf8');
41
+ const htmlContent = fs.readFileSync(htmlPath, 'utf8');
42
+
43
+ const payload = {
44
+ error_log: logContent,
45
+ html_snapshot: htmlContent,
46
+ failing_line: failureLocation ? `Line ${failureLocation.rootLine}` : null,
47
+ source_code: sourceCode
48
+ };
49
+
50
+ const response = await axios.post(this.apiUrl, payload, {
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'X-API-KEY': this.apiKey
54
+ }
55
+ });
56
+
57
+ return response.data;
58
+
59
+ } catch (error) {
60
+ if (error.response) {
61
+ if (error.response.status === 402) {
62
+ return { status: 'error', detail: 'QUOTA_EXCEEDED' };
63
+ }
64
+ let detail = error.response.data.detail || error.response.statusText;
65
+ if (typeof detail === 'object') detail = JSON.stringify(detail);
66
+ console.error(`āŒ API Error: ${error.response.status} - ${detail}`);
67
+ } else {
68
+ console.error(`āŒ Client Error: ${error.message}`);
69
+ }
70
+ return null; // Return null instead of exiting process
71
+ }
72
+ }
73
+ }
74
+
75
+ module.exports = DeFlakeClient;
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "deflake",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
+ "main": "client.js",
6
+ "bin": {
7
+ "deflake": "cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "playwright",
14
+ "cypress",
15
+ "webdriverio",
16
+ "ai",
17
+ "self-healing",
18
+ "automation",
19
+ "testing"
20
+ ],
21
+ "author": "VicQA Automation Cloud",
22
+ "license": "ISC",
23
+ "type": "commonjs",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/vicqaautomation-cloud/DeFlake.git"
27
+ },
28
+ "homepage": "https://github.com/vicqaautomation-cloud/DeFlake#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/vicqaautomation-cloud/DeFlake/issues"
31
+ },
32
+ "dependencies": {
33
+ "axios": "^1.13.2",
34
+ "dotenv": "^17.2.3",
35
+ "yargs": "^18.0.0"
36
+ }
37
+ }