@testdriverai/agent 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.
- package/agent/interface.js +7 -1
- package/agent/lib/commands.js +8 -6
- package/agent/lib/system.js +60 -6
- package/docs/docs.json +16 -1
- package/docs/v7/ai/agent.mdx +72 -0
- package/docs/v7/ai/mcp.mdx +228 -0
- package/docs/v7/ai/skills.mdx +73 -0
- package/docs/v7/find.mdx +2 -0
- package/interfaces/cli/commands/init.js +81 -2
- package/lib/init-project.js +57 -28
- package/lib/install-clients.js +470 -0
- package/mcp-server/dist/server.mjs +245 -66
- package/mcp-server/src/server.ts +250 -32
- package/package.json +1 -1
- package/sdk.js +14 -12
|
@@ -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
|
|
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
|
);
|
package/lib/init-project.js
CHANGED
|
@@ -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
|
-
//
|
|
282
|
-
//
|
|
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
|
-
|
|
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)) {
|