@testdriverai/mcp 7.9.103-canary → 7.9.104-test

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,6 +1,7 @@
1
1
  const BaseCommand = require("../lib/base.js");
2
2
  const { createCommandDefinitions } = require("../../../agent/interface.js");
3
3
  const { initProject } = require("../../../lib/init-project.js");
4
+ const { CLIENTS, detectClients } = require("../../../lib/install-clients.js");
4
5
  const fs = require("fs");
5
6
  const path = require("path");
6
7
  const chalk = require("chalk");
@@ -22,13 +23,24 @@ const POLL_TIMEOUT = 900000; // 15 minutes
22
23
  */
23
24
  class InitCommand extends BaseCommand {
24
25
  async run() {
25
- await this.parse(InitCommand);
26
+ const { flags } = await this.parse(InitCommand);
26
27
 
27
28
  console.log(chalk.cyan("\nšŸš€ Initializing TestDriver project...\n"));
28
29
 
29
30
  // Prompt for API key first
30
31
  const apiKey = await this.promptForApiKey();
31
32
 
33
+ // Determine which AI clients to wire up: from --client flag, or interactively.
34
+ let clients;
35
+ if (flags.client) {
36
+ clients = flags.client
37
+ .split(",")
38
+ .map((c) => c.trim())
39
+ .filter(Boolean);
40
+ } else {
41
+ clients = await this.promptForClients();
42
+ }
43
+
32
44
  // Helper to print progress messages with appropriate colors
33
45
  const printProgress = (msg) => {
34
46
  if (msg.startsWith("āœ“")) {
@@ -47,6 +59,7 @@ class InitCommand extends BaseCommand {
47
59
  targetDir: process.cwd(),
48
60
  apiKey: apiKey,
49
61
  skipInstall: false,
62
+ clients: clients && clients.length ? clients : undefined,
50
63
  onProgress: printProgress,
51
64
  });
52
65
 
@@ -159,6 +172,72 @@ class InitCommand extends BaseCommand {
159
172
  }
160
173
  }
161
174
 
175
+ /**
176
+ * Prompt the user to select which AI clients to install TestDriver into.
177
+ * Returns an array of client ids (possibly empty).
178
+ * @returns {Promise<string[]>}
179
+ */
180
+ async promptForClients() {
181
+ const ids = Object.keys(CLIENTS);
182
+ const detected = new Set(detectClients(process.cwd()));
183
+
184
+ // Non-interactive stdin (CI, piped input, no TTY): don't prompt — fall back
185
+ // to detected clients so init can still scaffold all files unattended.
186
+ if (!process.stdin.isTTY) {
187
+ return [...detected];
188
+ }
189
+
190
+ console.log(chalk.cyan("\n Which AI clients should TestDriver be installed into?\n"));
191
+ ids.forEach((id, i) => {
192
+ const c = CLIENTS[id];
193
+ const tag = detected.has(id) ? chalk.green(" (detected)") : "";
194
+ const web = c.type === "web" ? chalk.gray(" — web, manual steps") : "";
195
+ console.log(` ${chalk.cyan(i + 1)}. ${c.label}${tag}${web}`);
196
+ });
197
+ console.log(` ${chalk.cyan("a")}. All of the above`);
198
+ console.log(
199
+ chalk.gray(
200
+ "\n Enter numbers separated by commas (e.g. 1,3), 'a' for all, or press Enter to skip.\n",
201
+ ),
202
+ );
203
+
204
+ const answer = await new Promise((resolve) => {
205
+ const rl = readline.createInterface({
206
+ input: process.stdin,
207
+ output: process.stdout,
208
+ });
209
+ // Default to detected clients if any, else skip.
210
+ const defaultHint = detected.size
211
+ ? ` [${[...detected].map((d) => ids.indexOf(d) + 1).join(",")}]`
212
+ : "";
213
+ let answered = false;
214
+ rl.question(` Select clients${defaultHint}: `, (a) => {
215
+ answered = true;
216
+ rl.close();
217
+ resolve(a.trim());
218
+ });
219
+ // If stdin closes (EOF) before a line is entered, the question callback
220
+ // never fires — resolve empty so init doesn't hang.
221
+ rl.on("close", () => {
222
+ if (!answered) resolve("");
223
+ });
224
+ });
225
+
226
+ if (!answer) {
227
+ // Enter pressed: use detected clients if any, otherwise none.
228
+ return [...detected];
229
+ }
230
+ if (answer.toLowerCase() === "a" || answer.toLowerCase() === "all") {
231
+ return ["all"];
232
+ }
233
+ const selected = answer
234
+ .split(",")
235
+ .map((s) => parseInt(s.trim(), 10))
236
+ .filter((n) => Number.isInteger(n) && n >= 1 && n <= ids.length)
237
+ .map((n) => ids[n - 1]);
238
+ return selected;
239
+ }
240
+
162
241
  /**
163
242
  * Browser-based login flow using device code
164
243
  * @returns {Promise<string>} The API key
@@ -411,7 +490,7 @@ class InitCommand extends BaseCommand {
411
490
  console.log(" 2. Use AI agents to write tests:");
412
491
  console.log(chalk.gray(" Open VSCode/Cursor and use @testdriver agent\n"));
413
492
  console.log(" 3. MCP server configured:");
414
- console.log(chalk.gray(" TestDriver tools available via MCP in .vscode/mcp.json\n"));
493
+ console.log(chalk.gray(" TestDriver tools are now available in your selected AI client(s)\n"));
415
494
  console.log(
416
495
  " 4. For CI/CD, add TD_API_KEY to your GitHub repository secrets",
417
496
  );
@@ -39,6 +39,7 @@ function runInstall(cmd, args, cwd, label) {
39
39
  * @param {string} [options.apiKey] - TestDriver API key (will be saved to .env)
40
40
  * @param {boolean} [options.skipInstall=false] - Skip npm install step
41
41
  * @param {boolean} [options.interactive=false] - Whether to prompt for missing values (CLI mode)
42
+ * @param {string[]} [options.clients] - AI client ids to wire up (e.g. ["claude-code","cursor"] or ["all"]). When set, native per-client config is written instead of the interactive add-mcp flow.
42
43
  * @param {function} [options.onProgress] - Callback for progress updates (receives message string)
43
44
  * @returns {Promise<{success: boolean, results: string[], errors: string[]}>}
44
45
  */
@@ -277,12 +278,64 @@ jobs:
277
278
  progress("⊘ GitHub workflow already exists");
278
279
  }
279
280
 
281
+ // Resolve the bundled agent/skills source dirs once; reused by the
282
+ // client-aware installer (step 6) and the legacy copy steps (8 & 9).
283
+ const resolveSource = (...candidates) =>
284
+ candidates.find((c) => fs.existsSync(c)) || null;
285
+ const skillsSourceDir = resolveSource(
286
+ path.join(targetDir, "node_modules", "testdriverai", "ai", "skills"),
287
+ path.join(__dirname, "..", "ai", "skills"),
288
+ );
289
+ const agentsSourceDir = resolveSource(
290
+ path.join(targetDir, "node_modules", "testdriverai", "ai", "agents"),
291
+ path.join(__dirname, "..", "ai", "agents"),
292
+ );
293
+
280
294
  // 6. Setup MCP configuration
281
- // When triggered from VS Code extension, create .vscode/mcp.json silently
282
- // When triggered from CLI, use interactive add-mcp for user to select their MCP client
295
+ // Priority:
296
+ // a) options.clients set -> write native config per selected client
297
+ // b) TD_INIT_SOURCE=vscode -> create .vscode/mcp.json silently
298
+ // c) otherwise (CLI) -> interactive add-mcp client picker
283
299
  const isVscodeInit = process.env.TD_INIT_SOURCE === "vscode";
284
-
285
- if (isVscodeInit) {
300
+ const requestedClients =
301
+ Array.isArray(options.clients) && options.clients.length
302
+ ? options.clients
303
+ : null;
304
+
305
+ if (requestedClients) {
306
+ const {
307
+ installClient,
308
+ resolveClientIds,
309
+ } = require("./install-clients.js");
310
+ const { valid, unknown } = resolveClientIds(requestedClients);
311
+ for (const id of unknown) {
312
+ progress(`⚠ Unknown client "${id}" - skipping`);
313
+ }
314
+ for (const id of valid) {
315
+ try {
316
+ const result = installClient(id, {
317
+ targetDir,
318
+ apiKey: options.apiKey,
319
+ agentSource: agentsSourceDir,
320
+ skillsSource: skillsSourceDir,
321
+ });
322
+ progress(`šŸ”§ ${result.label}:`);
323
+ for (const step of result.steps) {
324
+ if (step.status === "manual") {
325
+ progress(` ℹ ${step.message}`);
326
+ } else if (step.status === "error") {
327
+ progress(` ⚠ ${step.message}`);
328
+ } else if (step.status === "skipped") {
329
+ progress(` ⊘ ${step.message || step.file}`);
330
+ } else {
331
+ progress(` āœ“ ${step.message || step.file}`);
332
+ }
333
+ }
334
+ } catch (err) {
335
+ progress(`⚠ Failed to configure ${id}: ${err.message}`);
336
+ }
337
+ }
338
+ } else if (isVscodeInit) {
286
339
  // VS Code extension: create .vscode/mcp.json directly
287
340
  const vscodeDir = path.join(targetDir, ".vscode");
288
341
  if (!fs.existsSync(vscodeDir)) {
@@ -365,18 +418,6 @@ jobs:
365
418
 
366
419
  // 8. Copy TestDriver skills
367
420
  const skillsDestDir = path.join(targetDir, ".github", "skills");
368
- const possibleSkillsSources = [
369
- path.join(targetDir, "node_modules", "testdriverai", "ai", "skills"),
370
- path.join(__dirname, "..", "ai", "skills"),
371
- ];
372
-
373
- let skillsSourceDir = null;
374
- for (const source of possibleSkillsSources) {
375
- if (fs.existsSync(source)) {
376
- skillsSourceDir = source;
377
- break;
378
- }
379
- }
380
421
 
381
422
  if (skillsSourceDir) {
382
423
  if (!fs.existsSync(skillsDestDir)) {
@@ -412,18 +453,6 @@ jobs:
412
453
 
413
454
  // 9. Copy TestDriver agents
414
455
  const agentsDestDir = path.join(targetDir, ".github", "agents");
415
- const possibleAgentsSources = [
416
- path.join(targetDir, "node_modules", "testdriverai", "ai", "agents"),
417
- path.join(__dirname, "..", "ai", "agents"),
418
- ];
419
-
420
- let agentsSourceDir = null;
421
- for (const source of possibleAgentsSources) {
422
- if (fs.existsSync(source)) {
423
- agentsSourceDir = source;
424
- break;
425
- }
426
- }
427
456
 
428
457
  if (agentsSourceDir) {
429
458
  if (!fs.existsSync(agentsDestDir)) {