testdriverai 7.2.78 ā 7.2.80
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/agent/index.js +42 -1
- package/agent/lib/debugger-server.js +6 -0
- package/ai/agents/testdriver.md +72 -5
- package/interfaces/cli/commands/init.js +47 -479
- package/lib/captcha/solver.js +64 -2
- package/lib/init-project.js +426 -0
- package/mcp-server/dist/server.mjs +118 -3
- package/package.json +3 -2
- package/sdk.js +12 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const BaseCommand = require("../lib/base.js");
|
|
2
2
|
const { createCommandDefinitions } = require("../../../agent/interface.js");
|
|
3
|
+
const { initProject } = require("../../../lib/init-project.js");
|
|
3
4
|
const fs = require("fs");
|
|
4
5
|
const path = require("path");
|
|
5
6
|
const chalk = require("chalk");
|
|
6
|
-
const { execSync } = require("child_process");
|
|
7
7
|
const readline = require("readline");
|
|
8
8
|
const os = require("os");
|
|
9
|
+
const { execSync } = require("child_process");
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Init command - scaffolds Vitest SDK example tests for TestDriver
|
|
@@ -16,24 +17,52 @@ class InitCommand extends BaseCommand {
|
|
|
16
17
|
|
|
17
18
|
console.log(chalk.cyan("\nš Initializing TestDriver project...\n"));
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
await this.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
await
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
await this.promptForApiKey();
|
|
20
|
+
// Prompt for API key first
|
|
21
|
+
const apiKey = await this.promptForApiKey();
|
|
22
|
+
|
|
23
|
+
// Run the shared init logic
|
|
24
|
+
const result = await initProject({
|
|
25
|
+
targetDir: process.cwd(),
|
|
26
|
+
apiKey: apiKey,
|
|
27
|
+
skipInstall: false,
|
|
28
|
+
});
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// Print results
|
|
31
|
+
for (const msg of result.results) {
|
|
32
|
+
if (msg.startsWith("ā")) {
|
|
33
|
+
console.log(chalk.green(` ${msg}`));
|
|
34
|
+
} else if (msg.startsWith("ā ") || msg.startsWith("ā¹")) {
|
|
35
|
+
console.log(chalk.yellow(` ${msg}`));
|
|
36
|
+
} else if (msg.startsWith("ā")) {
|
|
37
|
+
console.log(chalk.gray(` ${msg}`));
|
|
38
|
+
} else {
|
|
39
|
+
console.log(` ${msg}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Print errors if any
|
|
44
|
+
for (const err of result.errors) {
|
|
45
|
+
console.log(chalk.yellow(` ā ļø ${err}`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Handle shell profile for API key (CLI-specific feature)
|
|
49
|
+
if (apiKey && apiKey.trim()) {
|
|
50
|
+
this.addToShellProfile("TD_API_KEY", apiKey.trim());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (result.success) {
|
|
54
|
+
console.log(chalk.green("\nā
Project initialized successfully!\n"));
|
|
55
|
+
this.printNextSteps();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
} else {
|
|
58
|
+
console.log(chalk.red("\nā Project initialization completed with errors.\n"));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
33
61
|
}
|
|
34
62
|
|
|
35
63
|
/**
|
|
36
64
|
* Prompt user for API key and save to .env
|
|
65
|
+
* @returns {Promise<string|null>} The API key or null if skipped
|
|
37
66
|
*/
|
|
38
67
|
async promptForApiKey() {
|
|
39
68
|
const envPath = path.join(process.cwd(), ".env");
|
|
@@ -45,7 +74,7 @@ class InitCommand extends BaseCommand {
|
|
|
45
74
|
console.log(
|
|
46
75
|
chalk.gray("\n API key already configured in .env, skipping...\n"),
|
|
47
76
|
);
|
|
48
|
-
return;
|
|
77
|
+
return null;
|
|
49
78
|
}
|
|
50
79
|
}
|
|
51
80
|
|
|
@@ -77,17 +106,8 @@ class InitCommand extends BaseCommand {
|
|
|
77
106
|
);
|
|
78
107
|
|
|
79
108
|
if (apiKey && apiKey.trim()) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
? fs.readFileSync(envPath, "utf8") + "\n"
|
|
83
|
-
: "";
|
|
84
|
-
|
|
85
|
-
fs.writeFileSync(envPath, envContent + `TD_API_KEY=${apiKey.trim()}\n`);
|
|
86
|
-
process.env.TD_API_KEY = apiKey.trim();
|
|
87
|
-
console.log(chalk.green("\n ā API key saved to .env\n"));
|
|
88
|
-
|
|
89
|
-
// Also persist to shell profile so it's available in all terminals
|
|
90
|
-
this.addToShellProfile("TD_API_KEY", apiKey.trim());
|
|
109
|
+
console.log(chalk.green("\n ā API key will be saved\n"));
|
|
110
|
+
return apiKey.trim();
|
|
91
111
|
} else {
|
|
92
112
|
console.log(
|
|
93
113
|
chalk.yellow(
|
|
@@ -95,6 +115,7 @@ class InitCommand extends BaseCommand {
|
|
|
95
115
|
),
|
|
96
116
|
);
|
|
97
117
|
console.log(chalk.gray(" TD_API_KEY=your_api_key\n"));
|
|
118
|
+
return null;
|
|
98
119
|
}
|
|
99
120
|
}
|
|
100
121
|
|
|
@@ -227,459 +248,6 @@ class InitCommand extends BaseCommand {
|
|
|
227
248
|
});
|
|
228
249
|
}
|
|
229
250
|
|
|
230
|
-
/**
|
|
231
|
-
* Setup package.json if it doesn't exist
|
|
232
|
-
*/
|
|
233
|
-
async setupPackageJson() {
|
|
234
|
-
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
235
|
-
|
|
236
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
237
|
-
console.log(chalk.gray(" Creating package.json..."));
|
|
238
|
-
|
|
239
|
-
const packageJson = {
|
|
240
|
-
name: path.basename(process.cwd()),
|
|
241
|
-
version: "1.0.0",
|
|
242
|
-
description: "TestDriver.ai test suite",
|
|
243
|
-
type: "module",
|
|
244
|
-
scripts: {
|
|
245
|
-
test: "vitest run",
|
|
246
|
-
"test:watch": "vitest",
|
|
247
|
-
"test:ui": "vitest --ui",
|
|
248
|
-
},
|
|
249
|
-
keywords: ["testdriver", "testing", "e2e"],
|
|
250
|
-
author: "",
|
|
251
|
-
license: "ISC",
|
|
252
|
-
engines: {
|
|
253
|
-
node: ">=20.19.0",
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
fs.writeFileSync(
|
|
258
|
-
packageJsonPath,
|
|
259
|
-
JSON.stringify(packageJson, null, 2) + "\n",
|
|
260
|
-
);
|
|
261
|
-
console.log(chalk.green(` Created package.json`));
|
|
262
|
-
} else {
|
|
263
|
-
console.log(chalk.gray(" package.json already exists, skipping..."));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Create a Vitest SDK example
|
|
269
|
-
*/
|
|
270
|
-
async createVitestExample() {
|
|
271
|
-
const testDir = path.join(process.cwd(), "tests");
|
|
272
|
-
const testFile = path.join(testDir, "example.test.js");
|
|
273
|
-
const loginSnippetFile = path.join(testDir, "login.js");
|
|
274
|
-
const configFile = path.join(process.cwd(), "vitest.config.js");
|
|
275
|
-
|
|
276
|
-
// Create test directory if it doesn't exist
|
|
277
|
-
if (!fs.existsSync(testDir)) {
|
|
278
|
-
fs.mkdirSync(testDir, { recursive: true });
|
|
279
|
-
console.log(chalk.gray(` Created directory: ${testDir}`));
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Create login snippet file
|
|
283
|
-
const loginSnippetContent = `/**
|
|
284
|
-
* Login snippet - reusable login function
|
|
285
|
-
*
|
|
286
|
-
* This demonstrates how to create reusable test snippets that can be
|
|
287
|
-
* imported and used across multiple test files.
|
|
288
|
-
*/
|
|
289
|
-
export async function login(testdriver) {
|
|
290
|
-
|
|
291
|
-
// The password is displayed on screen, have TestDriver extract it
|
|
292
|
-
const password = await testdriver.extract('the password');
|
|
293
|
-
|
|
294
|
-
// Find the username field
|
|
295
|
-
const usernameField = await testdriver.find(
|
|
296
|
-
'username input'
|
|
297
|
-
);
|
|
298
|
-
await usernameField.click();
|
|
299
|
-
|
|
300
|
-
// Type username
|
|
301
|
-
await testdriver.type('standard_user');
|
|
302
|
-
|
|
303
|
-
// Enter password form earlier
|
|
304
|
-
// Marked as secret so it's not logged or stored
|
|
305
|
-
await testdriver.pressKeys(['tab']);
|
|
306
|
-
await testdriver.type(password, { secret: true });
|
|
307
|
-
|
|
308
|
-
// Submit the form
|
|
309
|
-
await testdriver.find('submit button on the login form').click();
|
|
310
|
-
}
|
|
311
|
-
`;
|
|
312
|
-
|
|
313
|
-
fs.writeFileSync(loginSnippetFile, loginSnippetContent);
|
|
314
|
-
console.log(chalk.green(` Created login snippet: ${loginSnippetFile}`));
|
|
315
|
-
|
|
316
|
-
// Create example Vitest test that uses the login snippet
|
|
317
|
-
const vitestContent = `import { test, expect } from 'vitest';
|
|
318
|
-
import { TestDriver } from 'testdriverai/vitest/hooks';
|
|
319
|
-
import { login } from './login.js';
|
|
320
|
-
|
|
321
|
-
test('should login and add item to cart', async (context) => {
|
|
322
|
-
|
|
323
|
-
// Create TestDriver instance - automatically connects to sandbox
|
|
324
|
-
const testdriver = TestDriver(context);
|
|
325
|
-
|
|
326
|
-
// Launch chrome and navigate to demo app
|
|
327
|
-
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
|
|
328
|
-
|
|
329
|
-
// Use the login snippet to handle authentication
|
|
330
|
-
// This demonstrates how to reuse test logic across multiple tests
|
|
331
|
-
await login(testdriver);
|
|
332
|
-
|
|
333
|
-
// Add item to cart
|
|
334
|
-
const addToCartButton = await testdriver.find(
|
|
335
|
-
'add to cart button under TestDriver Hat'
|
|
336
|
-
);
|
|
337
|
-
await addToCartButton.click();
|
|
338
|
-
|
|
339
|
-
// Open cart
|
|
340
|
-
const cartButton = await testdriver.find(
|
|
341
|
-
'cart button in the top right corner'
|
|
342
|
-
);
|
|
343
|
-
await cartButton.click();
|
|
344
|
-
|
|
345
|
-
// Verify item in cart
|
|
346
|
-
const result = await testdriver.assert('There is an item in the cart');
|
|
347
|
-
expect(result).toBeTruthy();
|
|
348
|
-
|
|
349
|
-
});
|
|
350
|
-
`;
|
|
351
|
-
|
|
352
|
-
fs.writeFileSync(testFile, vitestContent);
|
|
353
|
-
console.log(chalk.green(` Created test file: ${testFile}`));
|
|
354
|
-
|
|
355
|
-
// Create vitest config if it doesn't exist
|
|
356
|
-
if (!fs.existsSync(configFile)) {
|
|
357
|
-
const configContent = `import { defineConfig } from 'vitest/config';
|
|
358
|
-
import TestDriver from 'testdriverai/vitest';
|
|
359
|
-
|
|
360
|
-
// Note: dotenv is loaded automatically by the TestDriver SDK
|
|
361
|
-
export default defineConfig({
|
|
362
|
-
test: {
|
|
363
|
-
testTimeout: 300000,
|
|
364
|
-
hookTimeout: 300000,
|
|
365
|
-
reporters: [
|
|
366
|
-
'default',
|
|
367
|
-
TestDriver(),
|
|
368
|
-
],
|
|
369
|
-
setupFiles: ['testdriverai/vitest/setup'],
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
`;
|
|
373
|
-
|
|
374
|
-
fs.writeFileSync(configFile, configContent);
|
|
375
|
-
console.log(chalk.green(` Created config file: ${configFile}`));
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Create or update .gitignore to include .env
|
|
381
|
-
*/
|
|
382
|
-
async createGitignore() {
|
|
383
|
-
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
384
|
-
|
|
385
|
-
let gitignoreContent = "";
|
|
386
|
-
if (fs.existsSync(gitignorePath)) {
|
|
387
|
-
gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
|
|
388
|
-
|
|
389
|
-
// Check if .env is already in .gitignore
|
|
390
|
-
if (gitignoreContent.includes(".env")) {
|
|
391
|
-
console.log(chalk.gray(" .env already in .gitignore, skipping..."));
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Add common ignores including .env
|
|
397
|
-
const ignoresToAdd = [
|
|
398
|
-
"",
|
|
399
|
-
"# TestDriver.ai",
|
|
400
|
-
".env",
|
|
401
|
-
"node_modules/",
|
|
402
|
-
"test-results/",
|
|
403
|
-
"*.log",
|
|
404
|
-
];
|
|
405
|
-
|
|
406
|
-
const newContent = gitignoreContent.trim()
|
|
407
|
-
? gitignoreContent + "\n" + ignoresToAdd.join("\n") + "\n"
|
|
408
|
-
: ignoresToAdd.join("\n") + "\n";
|
|
409
|
-
|
|
410
|
-
fs.writeFileSync(gitignorePath, newContent);
|
|
411
|
-
console.log(chalk.green(" Updated .gitignore"));
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Create GitHub Actions workflow
|
|
416
|
-
*/
|
|
417
|
-
async createGitHubWorkflow() {
|
|
418
|
-
const workflowDir = path.join(process.cwd(), ".github", "workflows");
|
|
419
|
-
const workflowFile = path.join(workflowDir, "testdriver.yml");
|
|
420
|
-
|
|
421
|
-
// Create .github/workflows directory if it doesn't exist
|
|
422
|
-
if (!fs.existsSync(workflowDir)) {
|
|
423
|
-
fs.mkdirSync(workflowDir, { recursive: true });
|
|
424
|
-
console.log(chalk.gray(` Created directory: ${workflowDir}`));
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (!fs.existsSync(workflowFile)) {
|
|
428
|
-
const workflowContent = `name: TestDriver.ai Tests
|
|
429
|
-
|
|
430
|
-
on:
|
|
431
|
-
push:
|
|
432
|
-
branches: [ main, master ]
|
|
433
|
-
pull_request:
|
|
434
|
-
branches: [ main, master ]
|
|
435
|
-
|
|
436
|
-
jobs:
|
|
437
|
-
test:
|
|
438
|
-
runs-on: ubuntu-latest
|
|
439
|
-
|
|
440
|
-
steps:
|
|
441
|
-
- uses: actions/checkout@v4
|
|
442
|
-
|
|
443
|
-
- name: Setup Node.js
|
|
444
|
-
uses: actions/setup-node@v4
|
|
445
|
-
with:
|
|
446
|
-
node-version: '20'
|
|
447
|
-
cache: 'npm'
|
|
448
|
-
|
|
449
|
-
- name: Install dependencies
|
|
450
|
-
run: npm ci
|
|
451
|
-
|
|
452
|
-
- name: Run TestDriver.ai tests
|
|
453
|
-
env:
|
|
454
|
-
TD_API_KEY: \${{ secrets.TD_API_KEY }}
|
|
455
|
-
run: npx vitest run
|
|
456
|
-
|
|
457
|
-
- name: Upload test results
|
|
458
|
-
if: always()
|
|
459
|
-
uses: actions/upload-artifact@v4
|
|
460
|
-
with:
|
|
461
|
-
name: test-results
|
|
462
|
-
path: test-results/
|
|
463
|
-
retention-days: 30
|
|
464
|
-
`;
|
|
465
|
-
|
|
466
|
-
fs.writeFileSync(workflowFile, workflowContent);
|
|
467
|
-
console.log(chalk.green(` Created GitHub workflow: ${workflowFile}`));
|
|
468
|
-
} else {
|
|
469
|
-
console.log(chalk.gray(" GitHub workflow already exists, skipping..."));
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Create VSCode MCP configuration
|
|
475
|
-
*/
|
|
476
|
-
async createVscodeMcpConfig() {
|
|
477
|
-
const vscodeDir = path.join(process.cwd(), ".vscode");
|
|
478
|
-
const mcpConfigFile = path.join(vscodeDir, "mcp.json");
|
|
479
|
-
|
|
480
|
-
// Create .vscode directory if it doesn't exist
|
|
481
|
-
if (!fs.existsSync(vscodeDir)) {
|
|
482
|
-
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
483
|
-
console.log(chalk.gray(` Created directory: ${vscodeDir}`));
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (!fs.existsSync(mcpConfigFile)) {
|
|
487
|
-
const mcpConfig = {
|
|
488
|
-
inputs: [
|
|
489
|
-
{
|
|
490
|
-
type: "promptString",
|
|
491
|
-
id: "testdriver-api-key",
|
|
492
|
-
description: "TestDriver API Key From https://console.testdriver.ai/team",
|
|
493
|
-
password: true,
|
|
494
|
-
},
|
|
495
|
-
],
|
|
496
|
-
servers: {
|
|
497
|
-
testdriver: {
|
|
498
|
-
command: "npx",
|
|
499
|
-
args: ["-p", "testdriverai@beta", "testdriverai-mcp"],
|
|
500
|
-
env: {
|
|
501
|
-
TD_API_KEY: "${input:testdriver-api-key}",
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
},
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
fs.writeFileSync(
|
|
508
|
-
mcpConfigFile,
|
|
509
|
-
JSON.stringify(mcpConfig, null, 2) + "\n",
|
|
510
|
-
);
|
|
511
|
-
console.log(chalk.green(` Created MCP config: ${mcpConfigFile}`));
|
|
512
|
-
} else {
|
|
513
|
-
console.log(chalk.gray(" MCP config already exists, skipping..."));
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Create VSCode extensions recommendations
|
|
519
|
-
*/
|
|
520
|
-
async createVscodeExtensions() {
|
|
521
|
-
const vscodeDir = path.join(process.cwd(), ".vscode");
|
|
522
|
-
const extensionsFile = path.join(vscodeDir, "extensions.json");
|
|
523
|
-
|
|
524
|
-
// Create .vscode directory if it doesn't exist
|
|
525
|
-
if (!fs.existsSync(vscodeDir)) {
|
|
526
|
-
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
527
|
-
console.log(chalk.gray(` Created directory: ${vscodeDir}`));
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (!fs.existsSync(extensionsFile)) {
|
|
531
|
-
const extensionsConfig = {
|
|
532
|
-
recommendations: [
|
|
533
|
-
"vitest.explorer",
|
|
534
|
-
],
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
fs.writeFileSync(
|
|
538
|
-
extensionsFile,
|
|
539
|
-
JSON.stringify(extensionsConfig, null, 2) + "\n",
|
|
540
|
-
);
|
|
541
|
-
console.log(chalk.green(` Created extensions config: ${extensionsFile}`));
|
|
542
|
-
} else {
|
|
543
|
-
console.log(chalk.gray(" Extensions config already exists, skipping..."));
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Copy TestDriver skills from the package to the project
|
|
549
|
-
*/
|
|
550
|
-
async copySkills() {
|
|
551
|
-
const skillsDestDir = path.join(process.cwd(), ".github", "skills");
|
|
552
|
-
|
|
553
|
-
// Try to find skills in node_modules
|
|
554
|
-
const possibleSkillsSources = [
|
|
555
|
-
path.join(process.cwd(), "node_modules", "testdriverai", "ai", "skills"),
|
|
556
|
-
path.join(__dirname, "..", "..", "..", "ai", "skills"),
|
|
557
|
-
];
|
|
558
|
-
|
|
559
|
-
let skillsSourceDir = null;
|
|
560
|
-
for (const source of possibleSkillsSources) {
|
|
561
|
-
if (fs.existsSync(source)) {
|
|
562
|
-
skillsSourceDir = source;
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (!skillsSourceDir) {
|
|
568
|
-
console.log(chalk.yellow(" ā ļø Skills directory not found, skipping skills copy..."));
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Create .github/skills directory if it doesn't exist
|
|
573
|
-
if (!fs.existsSync(skillsDestDir)) {
|
|
574
|
-
fs.mkdirSync(skillsDestDir, { recursive: true });
|
|
575
|
-
console.log(chalk.gray(` Created directory: ${skillsDestDir}`));
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Copy all skill directories
|
|
579
|
-
const skillDirs = fs.readdirSync(skillsSourceDir);
|
|
580
|
-
let copiedCount = 0;
|
|
581
|
-
|
|
582
|
-
for (const skillDir of skillDirs) {
|
|
583
|
-
const sourcePath = path.join(skillsSourceDir, skillDir);
|
|
584
|
-
const destPath = path.join(skillsDestDir, skillDir);
|
|
585
|
-
|
|
586
|
-
if (fs.statSync(sourcePath).isDirectory()) {
|
|
587
|
-
// Create skill directory
|
|
588
|
-
if (!fs.existsSync(destPath)) {
|
|
589
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Copy SKILL.md file
|
|
593
|
-
const skillFile = path.join(sourcePath, "SKILL.md");
|
|
594
|
-
if (fs.existsSync(skillFile)) {
|
|
595
|
-
fs.copyFileSync(skillFile, path.join(destPath, "SKILL.md"));
|
|
596
|
-
copiedCount++;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
console.log(chalk.green(` Copied ${copiedCount} skills to ${skillsDestDir}`));
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Copy TestDriver agents to .github/agents
|
|
606
|
-
*/
|
|
607
|
-
async createAgents() {
|
|
608
|
-
const agentsDestDir = path.join(process.cwd(), ".github", "agents");
|
|
609
|
-
|
|
610
|
-
// Try to find agents in node_modules or local package
|
|
611
|
-
const possibleAgentsSources = [
|
|
612
|
-
path.join(process.cwd(), "node_modules", "testdriverai", "ai", "agents"),
|
|
613
|
-
path.join(__dirname, "..", "..", "..", "ai", "agents"),
|
|
614
|
-
];
|
|
615
|
-
|
|
616
|
-
let agentsSourceDir = null;
|
|
617
|
-
for (const source of possibleAgentsSources) {
|
|
618
|
-
if (fs.existsSync(source)) {
|
|
619
|
-
agentsSourceDir = source;
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (!agentsSourceDir) {
|
|
625
|
-
console.log(chalk.yellow(" ā ļø Agents directory not found, skipping agents copy..."));
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Create .github/agents directory if it doesn't exist
|
|
630
|
-
if (!fs.existsSync(agentsDestDir)) {
|
|
631
|
-
fs.mkdirSync(agentsDestDir, { recursive: true });
|
|
632
|
-
console.log(chalk.gray(` Created directory: ${agentsDestDir}`));
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Copy agent files with .agent.md extension
|
|
636
|
-
const agentFiles = fs.readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
|
|
637
|
-
let copiedCount = 0;
|
|
638
|
-
|
|
639
|
-
for (const agentFile of agentFiles) {
|
|
640
|
-
const sourcePath = path.join(agentsSourceDir, agentFile);
|
|
641
|
-
const agentName = agentFile.replace(".md", "");
|
|
642
|
-
const destPath = path.join(agentsDestDir, `${agentName}.agent.md`);
|
|
643
|
-
|
|
644
|
-
if (!fs.existsSync(destPath)) {
|
|
645
|
-
fs.copyFileSync(sourcePath, destPath);
|
|
646
|
-
copiedCount++;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (copiedCount > 0) {
|
|
651
|
-
console.log(chalk.green(` Copied ${copiedCount} agent(s) to ${agentsDestDir}`));
|
|
652
|
-
} else {
|
|
653
|
-
console.log(chalk.gray(" Agents already exist, skipping..."));
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Install dependencies
|
|
659
|
-
*/
|
|
660
|
-
async installDependencies() {
|
|
661
|
-
console.log(chalk.cyan("\n Installing dependencies...\n"));
|
|
662
|
-
|
|
663
|
-
try {
|
|
664
|
-
execSync(
|
|
665
|
-
"npm install -D vitest testdriverai@beta && npm install dotenv",
|
|
666
|
-
{
|
|
667
|
-
cwd: process.cwd(),
|
|
668
|
-
stdio: "inherit",
|
|
669
|
-
},
|
|
670
|
-
);
|
|
671
|
-
console.log(chalk.green("\n Dependencies installed successfully!"));
|
|
672
|
-
} catch (error) {
|
|
673
|
-
console.log(
|
|
674
|
-
chalk.yellow(
|
|
675
|
-
"\nā ļø Failed to install dependencies automatically. Please run:",
|
|
676
|
-
),
|
|
677
|
-
);
|
|
678
|
-
console.log(chalk.gray(" npm install -D vitest testdriverai@beta"));
|
|
679
|
-
console.log(chalk.gray(" npm install dotenv\n"));
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
251
|
/**
|
|
684
252
|
* Print next steps
|
|
685
253
|
*/
|
package/lib/captcha/solver.js
CHANGED
|
@@ -28,6 +28,68 @@ function httpsGet(url) {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Safely parse JSON response that may contain extra characters or multiple objects.
|
|
33
|
+
* The 2captcha API sometimes returns concatenated JSON or has trailing characters.
|
|
34
|
+
* @param {string} text - The response text to parse
|
|
35
|
+
* @returns {object} The parsed JSON object
|
|
36
|
+
*/
|
|
37
|
+
function safeParseJson(text) {
|
|
38
|
+
// Trim whitespace first
|
|
39
|
+
const trimmed = text.trim();
|
|
40
|
+
|
|
41
|
+
// Try standard parsing first
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(trimmed);
|
|
44
|
+
} catch {
|
|
45
|
+
// If standard parsing fails, try to extract the first valid JSON object
|
|
46
|
+
const jsonStart = trimmed.indexOf("{");
|
|
47
|
+
if (jsonStart === -1) {
|
|
48
|
+
throw new Error("No JSON object found in response: " + trimmed);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Find the matching closing brace by counting braces
|
|
52
|
+
let braceCount = 0;
|
|
53
|
+
let inString = false;
|
|
54
|
+
let escape = false;
|
|
55
|
+
|
|
56
|
+
for (let i = jsonStart; i < trimmed.length; i++) {
|
|
57
|
+
const char = trimmed[i];
|
|
58
|
+
|
|
59
|
+
if (escape) {
|
|
60
|
+
escape = false;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (char === "\\") {
|
|
65
|
+
escape = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (char === '"') {
|
|
70
|
+
inString = !inString;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!inString) {
|
|
75
|
+
if (char === "{") {
|
|
76
|
+
braceCount++;
|
|
77
|
+
} else if (char === "}") {
|
|
78
|
+
braceCount--;
|
|
79
|
+
if (braceCount === 0) {
|
|
80
|
+
// Found the end of the first complete JSON object
|
|
81
|
+
const jsonStr = trimmed.substring(jsonStart, i + 1);
|
|
82
|
+
return JSON.parse(jsonStr);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If we get here, we couldn't find a complete JSON object
|
|
89
|
+
throw new Error("Invalid JSON in response: " + trimmed);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
31
93
|
// Auto-detection script that runs in the browser context
|
|
32
94
|
const detectCaptchaScript = `
|
|
33
95
|
(function() {
|
|
@@ -232,7 +294,7 @@ const checkSuccessScript = `(function() {
|
|
|
232
294
|
|
|
233
295
|
// Submit to 2captcha
|
|
234
296
|
console.log("SUBMITTING...");
|
|
235
|
-
const submitResp =
|
|
297
|
+
const submitResp = safeParseJson(await httpsGet(submitUrl));
|
|
236
298
|
if (submitResp.status !== 1) {
|
|
237
299
|
throw new Error("Submit failed: " + JSON.stringify(submitResp));
|
|
238
300
|
}
|
|
@@ -244,7 +306,7 @@ const checkSuccessScript = `(function() {
|
|
|
244
306
|
const maxAttempts = Math.ceil(config.timeout / config.pollInterval);
|
|
245
307
|
for (let i = 0; i < maxAttempts; i++) {
|
|
246
308
|
await sleep(config.pollInterval);
|
|
247
|
-
const resultResp =
|
|
309
|
+
const resultResp = safeParseJson(
|
|
248
310
|
await httpsGet(
|
|
249
311
|
"https://2captcha.com/res.php?key=" +
|
|
250
312
|
config.apiKey +
|