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.
@@ -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
- await this.setupPackageJson();
20
- await this.createVitestExample();
21
- await this.createGitHubWorkflow();
22
- await this.createGitignore();
23
- await this.createVscodeMcpConfig();
24
- await this.createVscodeExtensions();
25
- await this.installDependencies();
26
- await this.copySkills();
27
- await this.createAgents();
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
- console.log(chalk.green("\nāœ… Project initialized successfully!\n"));
31
- this.printNextSteps();
32
- process.exit(0);
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
- // Save to .env
81
- const envContent = fs.existsSync(envPath)
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
  */
@@ -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 = JSON.parse(await httpsGet(submitUrl));
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 = JSON.parse(
309
+ const resultResp = safeParseJson(
248
310
  await httpsGet(
249
311
  "https://2captcha.com/res.php?key=" +
250
312
  config.apiKey +