@fasttest-ai/qa-agent 0.4.3 → 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.
- package/README.md +1 -27
- package/bin/qa-agent.js +4 -0
- package/dist/cli.js +33 -194
- package/dist/index.js +64 -1906
- package/dist/install.js +39 -570
- package/package.json +5 -2
- package/dist/actions.d.ts +0 -41
- package/dist/actions.js +0 -224
- package/dist/actions.js.map +0 -1
- package/dist/browser.d.ts +0 -77
- package/dist/browser.js +0 -312
- package/dist/browser.js.map +0 -1
- package/dist/cli.d.ts +0 -19
- package/dist/cli.js.map +0 -1
- package/dist/cloud.d.ts +0 -302
- package/dist/cloud.js +0 -261
- package/dist/cloud.js.map +0 -1
- package/dist/config.d.ts +0 -21
- package/dist/config.js +0 -49
- package/dist/config.js.map +0 -1
- package/dist/healer.d.ts +0 -32
- package/dist/healer.js +0 -316
- package/dist/healer.js.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.js.map +0 -1
- package/dist/install.d.ts +0 -11
- package/dist/install.js.map +0 -1
- package/dist/runner.d.ts +0 -90
- package/dist/runner.js +0 -700
- package/dist/runner.js.map +0 -1
- package/dist/variables.d.ts +0 -30
- package/dist/variables.js +0 -104
- package/dist/variables.js.map +0 -1
package/dist/install.js
CHANGED
|
@@ -1,411 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
const IS_WINDOWS = platform() === "win32";
|
|
18
|
-
const IS_MAC = platform() === "darwin";
|
|
19
|
-
const MCP_SERVER_NAME = "fasttest";
|
|
20
|
-
const NPX_CMD = IS_WINDOWS ? "npx.cmd" : "npx";
|
|
21
|
-
const MCP_COMMAND = "npx";
|
|
22
|
-
const MCP_ARGS = ["-y", "@fasttest-ai/qa-agent@latest"];
|
|
23
|
-
const IDE_CONFIGS = [
|
|
24
|
-
{
|
|
25
|
-
id: "claude-code",
|
|
26
|
-
label: "Claude Code",
|
|
27
|
-
globalConfigPath: "", // Uses CLI, not direct file write
|
|
28
|
-
format: "cli",
|
|
29
|
-
hasPermissions: true,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: "cursor",
|
|
33
|
-
label: "Cursor",
|
|
34
|
-
globalConfigPath: join(homedir(), ".cursor", "mcp.json"),
|
|
35
|
-
format: "json-mcpServers",
|
|
36
|
-
hasPermissions: false,
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: "windsurf",
|
|
40
|
-
label: "Windsurf",
|
|
41
|
-
globalConfigPath: join(homedir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
42
|
-
format: "json-mcpServers",
|
|
43
|
-
hasPermissions: false,
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
id: "vscode",
|
|
47
|
-
label: "VS Code / Copilot",
|
|
48
|
-
globalConfigPath: IS_MAC
|
|
49
|
-
? join(homedir(), "Library", "Application Support", "Code", "User", "mcp.json")
|
|
50
|
-
: IS_WINDOWS
|
|
51
|
-
? join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Code", "User", "mcp.json")
|
|
52
|
-
: join(homedir(), ".config", "Code", "User", "mcp.json"),
|
|
53
|
-
format: "json-servers",
|
|
54
|
-
hasPermissions: false,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: "codex",
|
|
58
|
-
label: "Codex",
|
|
59
|
-
globalConfigPath: join(homedir(), ".codex", "config.toml"),
|
|
60
|
-
format: "toml",
|
|
61
|
-
hasPermissions: false,
|
|
62
|
-
},
|
|
63
|
-
];
|
|
64
|
-
function parseArgs() {
|
|
65
|
-
const action = process.argv[2];
|
|
66
|
-
const rest = process.argv.slice(3);
|
|
67
|
-
let ide = null;
|
|
68
|
-
let scope = "user";
|
|
69
|
-
let skipPermissions = false;
|
|
70
|
-
for (let i = 0; i < rest.length; i++) {
|
|
71
|
-
if (rest[i] === "--ide" && rest[i + 1]) {
|
|
72
|
-
ide = rest[++i];
|
|
73
|
-
}
|
|
74
|
-
else if (rest[i] === "--scope" && rest[i + 1]) {
|
|
75
|
-
scope = rest[++i];
|
|
76
|
-
}
|
|
77
|
-
else if (rest[i] === "--skip-permissions") {
|
|
78
|
-
skipPermissions = true;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return { action, ide, scope, skipPermissions };
|
|
82
|
-
}
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Interactive IDE picker
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
function askQuestion(prompt) {
|
|
87
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
88
|
-
return new Promise((resolve) => {
|
|
89
|
-
rl.question(prompt, (answer) => {
|
|
90
|
-
rl.close();
|
|
91
|
-
resolve(answer.trim());
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
async function pickIde() {
|
|
96
|
-
console.log(" Which IDE do you use?\n");
|
|
97
|
-
IDE_CONFIGS.forEach((ide, i) => {
|
|
98
|
-
console.log(` ${i + 1}. ${ide.label}`);
|
|
99
|
-
});
|
|
100
|
-
console.log();
|
|
101
|
-
const answer = await askQuestion(" > ");
|
|
102
|
-
const idx = parseInt(answer, 10) - 1;
|
|
103
|
-
if (idx >= 0 && idx < IDE_CONFIGS.length) {
|
|
104
|
-
return IDE_CONFIGS[idx].id;
|
|
105
|
-
}
|
|
106
|
-
// Try matching by name
|
|
107
|
-
const match = IDE_CONFIGS.find((c) => c.id === answer.toLowerCase() || c.label.toLowerCase() === answer.toLowerCase());
|
|
108
|
-
if (match)
|
|
109
|
-
return match.id;
|
|
110
|
-
console.log(" Invalid selection, defaulting to Claude Code.\n");
|
|
111
|
-
return "claude-code";
|
|
112
|
-
}
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
// Claude Code — CLI-based registration + permission management
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
function isClaudeCliAvailable() {
|
|
117
|
-
try {
|
|
118
|
-
execFileSync("claude", ["--version"], { stdio: "pipe", shell: IS_WINDOWS });
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function registerClaudeCode(scope) {
|
|
126
|
-
const args = [
|
|
127
|
-
"mcp", "add", "--scope", scope,
|
|
128
|
-
MCP_SERVER_NAME, "--",
|
|
129
|
-
MCP_COMMAND, ...MCP_ARGS,
|
|
130
|
-
];
|
|
131
|
-
try {
|
|
132
|
-
execFileSync("claude", args, { stdio: "inherit", shell: IS_WINDOWS });
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
try {
|
|
137
|
-
execFileSync("claude", ["mcp", "remove", "--scope", scope, MCP_SERVER_NAME], {
|
|
138
|
-
stdio: "pipe", shell: IS_WINDOWS,
|
|
139
|
-
});
|
|
140
|
-
execFileSync("claude", args, { stdio: "inherit", shell: IS_WINDOWS });
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
function removeClaudeCode(scope) {
|
|
149
|
-
try {
|
|
150
|
-
execFileSync("claude", ["mcp", "remove", "--scope", scope, MCP_SERVER_NAME], {
|
|
151
|
-
stdio: "inherit", shell: IS_WINDOWS,
|
|
152
|
-
});
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
160
|
-
const CLAUDE_SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
|
|
161
|
-
const PERMISSION_ENTRY = "mcp__fasttest";
|
|
162
|
-
function addClaudePermissions() {
|
|
163
|
-
if (!existsSync(CLAUDE_DIR))
|
|
164
|
-
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
165
|
-
let settings = {};
|
|
166
|
-
if (existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
167
|
-
try {
|
|
168
|
-
settings = JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
const backup = CLAUDE_SETTINGS_PATH + ".bak";
|
|
172
|
-
writeFileSync(backup, readFileSync(CLAUDE_SETTINGS_PATH));
|
|
173
|
-
console.log(` Warning: ${CLAUDE_SETTINGS_PATH} was corrupted. Backed up to ${backup}`);
|
|
174
|
-
settings = {};
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (!settings.permissions)
|
|
178
|
-
settings.permissions = {};
|
|
179
|
-
if (!Array.isArray(settings.permissions.allow))
|
|
180
|
-
settings.permissions.allow = [];
|
|
181
|
-
if (settings.permissions.allow.includes(PERMISSION_ENTRY)) {
|
|
182
|
-
return { added: false, alreadyExists: true };
|
|
183
|
-
}
|
|
184
|
-
settings.permissions.allow.push(PERMISSION_ENTRY);
|
|
185
|
-
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
186
|
-
return { added: true, alreadyExists: false };
|
|
187
|
-
}
|
|
188
|
-
function removeClaudePermissions() {
|
|
189
|
-
if (!existsSync(CLAUDE_SETTINGS_PATH))
|
|
190
|
-
return false;
|
|
191
|
-
try {
|
|
192
|
-
const settings = JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
193
|
-
const allow = settings.permissions?.allow;
|
|
194
|
-
if (!Array.isArray(allow))
|
|
195
|
-
return false;
|
|
196
|
-
const idx = allow.indexOf(PERMISSION_ENTRY);
|
|
197
|
-
if (idx === -1)
|
|
198
|
-
return false;
|
|
199
|
-
allow.splice(idx, 1);
|
|
200
|
-
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
// Claude Code — /ftest slash command
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
211
|
-
const CLAUDE_COMMANDS_DIR = join(process.cwd(), ".claude", "commands");
|
|
212
|
-
const COMMAND_FILES = ["ftest.md", "qa.md"];
|
|
213
|
-
function installClaudeCommands() {
|
|
214
|
-
let installed = 0;
|
|
215
|
-
let updated = 0;
|
|
216
|
-
for (const file of COMMAND_FILES) {
|
|
217
|
-
const src = join(__dirname, "..", "commands", file);
|
|
218
|
-
const dest = join(CLAUDE_COMMANDS_DIR, file);
|
|
219
|
-
if (!existsSync(src))
|
|
220
|
-
continue;
|
|
221
|
-
if (existsSync(dest)) {
|
|
222
|
-
copyFileSync(src, dest);
|
|
223
|
-
updated++;
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
mkdirSync(CLAUDE_COMMANDS_DIR, { recursive: true });
|
|
227
|
-
copyFileSync(src, dest);
|
|
228
|
-
installed++;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return { installed, updated };
|
|
232
|
-
}
|
|
233
|
-
function removeClaudeCommands() {
|
|
234
|
-
let removed = 0;
|
|
235
|
-
for (const file of COMMAND_FILES) {
|
|
236
|
-
const dest = join(CLAUDE_COMMANDS_DIR, file);
|
|
237
|
-
if (!existsSync(dest))
|
|
238
|
-
continue;
|
|
239
|
-
try {
|
|
240
|
-
unlinkSync(dest);
|
|
241
|
-
removed++;
|
|
242
|
-
}
|
|
243
|
-
catch { /* ignore */ }
|
|
244
|
-
}
|
|
245
|
-
return removed;
|
|
246
|
-
}
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
// JSON config writers (Cursor, Windsurf, VS Code)
|
|
249
|
-
// ---------------------------------------------------------------------------
|
|
250
|
-
/** Cursor & Windsurf: { "mcpServers": { "fasttest": { "command": ..., "args": ... } } } */
|
|
251
|
-
function writeMcpServersJson(configPath) {
|
|
252
|
-
const dir = join(configPath, "..");
|
|
253
|
-
if (!existsSync(dir))
|
|
254
|
-
mkdirSync(dir, { recursive: true });
|
|
255
|
-
let config = {};
|
|
256
|
-
if (existsSync(configPath)) {
|
|
257
|
-
try {
|
|
258
|
-
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
const backup = configPath + ".bak";
|
|
262
|
-
writeFileSync(backup, readFileSync(configPath));
|
|
263
|
-
console.log(` Warning: ${configPath} was corrupted. Backed up to ${backup}`);
|
|
264
|
-
config = {};
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
268
|
-
config.mcpServers = {};
|
|
269
|
-
}
|
|
270
|
-
config.mcpServers[MCP_SERVER_NAME] = {
|
|
271
|
-
command: MCP_COMMAND,
|
|
272
|
-
args: [...MCP_ARGS],
|
|
273
|
-
};
|
|
274
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
/** VS Code: { "servers": { "fasttest": { "type": "stdio", "command": ..., "args": ... } } } */
|
|
278
|
-
function writeVscodeJson(configPath) {
|
|
279
|
-
const dir = join(configPath, "..");
|
|
280
|
-
if (!existsSync(dir))
|
|
281
|
-
mkdirSync(dir, { recursive: true });
|
|
282
|
-
let config = {};
|
|
283
|
-
if (existsSync(configPath)) {
|
|
284
|
-
try {
|
|
285
|
-
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
const backup = configPath + ".bak";
|
|
289
|
-
writeFileSync(backup, readFileSync(configPath));
|
|
290
|
-
console.log(` Warning: ${configPath} was corrupted. Backed up to ${backup}`);
|
|
291
|
-
config = {};
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (!config.servers || typeof config.servers !== "object") {
|
|
295
|
-
config.servers = {};
|
|
296
|
-
}
|
|
297
|
-
config.servers[MCP_SERVER_NAME] = {
|
|
298
|
-
type: "stdio",
|
|
299
|
-
command: MCP_COMMAND,
|
|
300
|
-
args: [...MCP_ARGS],
|
|
301
|
-
};
|
|
302
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
// ---------------------------------------------------------------------------
|
|
306
|
-
// Codex TOML config writer
|
|
307
|
-
// ---------------------------------------------------------------------------
|
|
308
|
-
function writeCodexToml(configPath) {
|
|
309
|
-
const dir = join(configPath, "..");
|
|
310
|
-
if (!existsSync(dir))
|
|
311
|
-
mkdirSync(dir, { recursive: true });
|
|
312
|
-
let content = "";
|
|
313
|
-
if (existsSync(configPath)) {
|
|
314
|
-
content = readFileSync(configPath, "utf-8");
|
|
315
|
-
}
|
|
316
|
-
const sectionHeader = `[mcp_servers.${MCP_SERVER_NAME}]`;
|
|
317
|
-
if (content.includes(sectionHeader)) {
|
|
318
|
-
// Replace existing section — find from header to next section or EOF
|
|
319
|
-
const regex = new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?(?=\\n\\[|$)`);
|
|
320
|
-
content = content.replace(regex, buildCodexSection());
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
// Append
|
|
324
|
-
if (content.length > 0 && !content.endsWith("\n"))
|
|
325
|
-
content += "\n";
|
|
326
|
-
content += "\n" + buildCodexSection() + "\n";
|
|
327
|
-
}
|
|
328
|
-
writeFileSync(configPath, content);
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
|
-
function buildCodexSection() {
|
|
332
|
-
return `[mcp_servers.${MCP_SERVER_NAME}]\ncommand = "${MCP_COMMAND}"\nargs = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]`;
|
|
333
|
-
}
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
// Remove from JSON configs
|
|
336
|
-
// ---------------------------------------------------------------------------
|
|
337
|
-
function removeFromMcpServersJson(configPath) {
|
|
338
|
-
if (!existsSync(configPath))
|
|
339
|
-
return false;
|
|
340
|
-
try {
|
|
341
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
342
|
-
const servers = config.mcpServers;
|
|
343
|
-
if (!servers || !(MCP_SERVER_NAME in servers))
|
|
344
|
-
return false;
|
|
345
|
-
delete servers[MCP_SERVER_NAME];
|
|
346
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
function removeFromVscodeJson(configPath) {
|
|
354
|
-
if (!existsSync(configPath))
|
|
355
|
-
return false;
|
|
356
|
-
try {
|
|
357
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
358
|
-
const servers = config.servers;
|
|
359
|
-
if (!servers || !(MCP_SERVER_NAME in servers))
|
|
360
|
-
return false;
|
|
361
|
-
delete servers[MCP_SERVER_NAME];
|
|
362
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
function removeFromCodexToml(configPath) {
|
|
370
|
-
if (!existsSync(configPath))
|
|
371
|
-
return false;
|
|
372
|
-
try {
|
|
373
|
-
let content = readFileSync(configPath, "utf-8");
|
|
374
|
-
const sectionHeader = `[mcp_servers.${MCP_SERVER_NAME}]`;
|
|
375
|
-
if (!content.includes(sectionHeader))
|
|
376
|
-
return false;
|
|
377
|
-
const regex = new RegExp(`\\n?\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?(?=\\n\\[|$)`);
|
|
378
|
-
content = content.replace(regex, "");
|
|
379
|
-
writeFileSync(configPath, content);
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
catch {
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
// Playwright browser installation
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
function installPlaywrightBrowsers() {
|
|
390
|
-
try {
|
|
391
|
-
execFileSync(NPX_CMD, ["playwright", "install", "--with-deps", "chromium"], {
|
|
392
|
-
stdio: "inherit",
|
|
393
|
-
});
|
|
394
|
-
console.log(" Chromium installed");
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
console.log(" Warning: Could not install Playwright browsers automatically.");
|
|
398
|
-
console.log(" Run manually: npx playwright install --with-deps chromium");
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
// ---------------------------------------------------------------------------
|
|
402
|
-
// Install
|
|
403
|
-
// ---------------------------------------------------------------------------
|
|
404
|
-
function registerMcpServer(ide, scope) {
|
|
405
|
-
switch (ide.format) {
|
|
406
|
-
case "cli":
|
|
407
|
-
if (!isClaudeCliAvailable()) {
|
|
408
|
-
console.log(`
|
|
1
|
+
import{execFileSync as y}from"node:child_process";import{createInterface as M}from"node:readline";import{existsSync as a,mkdirSync as b,readFileSync as u,writeFileSync as f,unlinkSync as N,copyFileSync as _}from"node:fs";import{homedir as g,platform as j}from"node:os";import{join as i,dirname as O}from"node:path";import{fileURLToPath as T}from"node:url";var C=j()==="win32",D=j()==="darwin",l="fasttest",F=C?"npx.cmd":"npx",$="npx",I=["-y","@fasttest-ai/qa-agent@latest"],v=[{id:"claude-code",label:"Claude Code",globalConfigPath:"",format:"cli",hasPermissions:!0},{id:"cursor",label:"Cursor",globalConfigPath:i(g(),".cursor","mcp.json"),format:"json-mcpServers",hasPermissions:!1},{id:"windsurf",label:"Windsurf",globalConfigPath:i(g(),".codeium","windsurf","mcp_config.json"),format:"json-mcpServers",hasPermissions:!1},{id:"vscode",label:"VS Code / Copilot",globalConfigPath:D?i(g(),"Library","Application Support","Code","User","mcp.json"):C?i(process.env.APPDATA??i(g(),"AppData","Roaming"),"Code","User","mcp.json"):i(g(),".config","Code","User","mcp.json"),format:"json-servers",hasPermissions:!1},{id:"codex",label:"Codex",globalConfigPath:i(g(),".codex","config.toml"),format:"toml",hasPermissions:!1},{id:"antigravity",label:"Antigravity",globalConfigPath:i(g(),".gemini","antigravity","mcp_config.json"),format:"json-mcpServers",hasPermissions:!1}];function J(){let e=process.argv[2],o=process.argv.slice(3),s=null,n="user",r=!1;for(let t=0;t<o.length;t++)o[t]==="--ide"&&o[t+1]?s=o[++t]:o[t]==="--scope"&&o[t+1]?n=o[++t]:o[t]==="--skip-permissions"&&(r=!0);return{action:e,ide:s,scope:n,skipPermissions:r}}function L(e){let o=M({input:process.stdin,output:process.stdout});return new Promise(s=>{o.question(e,n=>{o.close(),s(n.trim())})})}async function R(){console.log(` Which IDE do you use?
|
|
2
|
+
`),v.forEach((n,r)=>{console.log(` ${r+1}. ${n.label}`)}),console.log();let e=await L(" > "),o=parseInt(e,10)-1;if(o>=0&&o<v.length)return v[o].id;let s=v.find(n=>n.id===e.toLowerCase()||n.label.toLowerCase()===e.toLowerCase());return s?s.id:(console.log(` Invalid selection, defaulting to Claude Code.
|
|
3
|
+
`),"claude-code")}function U(){try{return y("claude",["--version"],{stdio:"pipe",shell:C}),!0}catch{return!1}}function W(e){let o=["mcp","add","--scope",e,l,"--",$,...I];try{return y("claude",o,{stdio:"inherit",shell:C}),!0}catch{try{return y("claude",["mcp","remove","--scope",e,l],{stdio:"pipe",shell:C}),y("claude",o,{stdio:"inherit",shell:C}),!0}catch{return!1}}}function G(e){try{return y("claude",["mcp","remove","--scope",e,l],{stdio:"inherit",shell:C}),!0}catch{return!1}}var P=i(g(),".claude"),d=i(P,"settings.json"),h="mcp__fasttest";function q(){a(P)||b(P,{recursive:!0});let e={};if(a(d))try{e=JSON.parse(u(d,"utf-8"))}catch{let o=d+".bak";f(o,u(d)),console.log(` Warning: ${d} was corrupted. Backed up to ${o}`),e={}}return e.permissions||(e.permissions={}),Array.isArray(e.permissions.allow)||(e.permissions.allow=[]),e.permissions.allow.includes(h)?{added:!1,alreadyExists:!0}:(e.permissions.allow.push(h),f(d,JSON.stringify(e,null,2)+`
|
|
4
|
+
`),{added:!0,alreadyExists:!1})}function H(){if(!a(d))return!1;try{let e=JSON.parse(u(d,"utf-8")),o=e.permissions?.allow;if(!Array.isArray(o))return!1;let s=o.indexOf(h);return s===-1?!1:(o.splice(s,1),f(d,JSON.stringify(e,null,2)+`
|
|
5
|
+
`),!0)}catch{return!1}}var B=O(T(import.meta.url)),w=i(process.cwd(),".claude","commands"),E=["ftest.md","qa.md"];function V(){let e=0,o=0;for(let s of E){let n=i(B,"..","commands",s),r=i(w,s);a(n)&&(a(r)?(_(n,r),o++):(b(w,{recursive:!0}),_(n,r),e++))}return{installed:e,updated:o}}function Y(){let e=0;for(let o of E){let s=i(w,o);if(a(s))try{N(s),e++}catch{}}return e}function K(e){let o=i(e,"..");a(o)||b(o,{recursive:!0});let s={};if(a(e))try{s=JSON.parse(u(e,"utf-8"))}catch{let n=e+".bak";f(n,u(e)),console.log(` Warning: ${e} was corrupted. Backed up to ${n}`),s={}}return(!s.mcpServers||typeof s.mcpServers!="object")&&(s.mcpServers={}),s.mcpServers[l]={command:$,args:[...I]},f(e,JSON.stringify(s,null,2)+`
|
|
6
|
+
`),!0}function Q(e){let o=i(e,"..");a(o)||b(o,{recursive:!0});let s={};if(a(e))try{s=JSON.parse(u(e,"utf-8"))}catch{let n=e+".bak";f(n,u(e)),console.log(` Warning: ${e} was corrupted. Backed up to ${n}`),s={}}return(!s.servers||typeof s.servers!="object")&&(s.servers={}),s.servers[l]={type:"stdio",command:$,args:[...I]},f(e,JSON.stringify(s,null,2)+`
|
|
7
|
+
`),!0}function X(e){let o=i(e,"..");a(o)||b(o,{recursive:!0});let s="";a(e)&&(s=u(e,"utf-8"));let n=`[mcp_servers.${l}]`;if(s.includes(n)){let r=new RegExp(`\\[mcp_servers\\.${l}\\][\\s\\S]*?(?=\\n\\[|$)`);s=s.replace(r,k())}else s.length>0&&!s.endsWith(`
|
|
8
|
+
`)&&(s+=`
|
|
9
|
+
`),s+=`
|
|
10
|
+
`+k()+`
|
|
11
|
+
`;return f(e,s),!0}function k(){return`[mcp_servers.${l}]
|
|
12
|
+
command = "${$}"
|
|
13
|
+
args = [${I.map(e=>`"${e}"`).join(", ")}]`}function z(e){if(!a(e))return!1;try{let o=JSON.parse(u(e,"utf-8")),s=o.mcpServers;return!s||!(l in s)?!1:(delete s[l],f(e,JSON.stringify(o,null,2)+`
|
|
14
|
+
`),!0)}catch{return!1}}function Z(e){if(!a(e))return!1;try{let o=JSON.parse(u(e,"utf-8")),s=o.servers;return!s||!(l in s)?!1:(delete s[l],f(e,JSON.stringify(o,null,2)+`
|
|
15
|
+
`),!0)}catch{return!1}}function ee(e){if(!a(e))return!1;try{let o=u(e,"utf-8"),s=`[mcp_servers.${l}]`;if(!o.includes(s))return!1;let n=new RegExp(`\\n?\\[mcp_servers\\.${l}\\][\\s\\S]*?(?=\\n\\[|$)`);return o=o.replace(n,""),f(e,o),!0}catch{return!1}}function se(){if(process.env.FASTTEST_SKIP_PLAYWRIGHT==="1"){console.log(" Skipping Playwright install (FASTTEST_SKIP_PLAYWRIGHT=1)");return}try{y(F,["playwright","install","--with-deps","chromium"],{stdio:"inherit"}),console.log(" Chromium installed")}catch{console.log(" Warning: Could not install Playwright browsers automatically."),console.log(" Run manually: npx playwright install --with-deps chromium")}}function oe(e,o){switch(e.format){case"cli":return U()?W(o):(console.log(`
|
|
409
16
|
Claude Code CLI not found in PATH.
|
|
410
17
|
|
|
411
18
|
Install Claude Code first:
|
|
@@ -420,171 +27,33 @@ function registerMcpServer(ide, scope) {
|
|
|
420
27
|
}
|
|
421
28
|
}
|
|
422
29
|
}
|
|
423
|
-
`);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
function removeMcpServer(ide, scope) {
|
|
436
|
-
switch (ide.format) {
|
|
437
|
-
case "cli":
|
|
438
|
-
return removeClaudeCode(scope);
|
|
439
|
-
case "json-mcpServers":
|
|
440
|
-
return removeFromMcpServersJson(ide.globalConfigPath);
|
|
441
|
-
case "json-servers":
|
|
442
|
-
return removeFromVscodeJson(ide.globalConfigPath);
|
|
443
|
-
case "toml":
|
|
444
|
-
return removeFromCodexToml(ide.globalConfigPath);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
async function install(config) {
|
|
448
|
-
console.log("\n FastTest Agent Installer\n");
|
|
449
|
-
const ideId = config.ide ?? (await pickIde());
|
|
450
|
-
const ide = IDE_CONFIGS.find((c) => c.id === ideId);
|
|
451
|
-
if (!ide) {
|
|
452
|
-
console.log(` Unknown IDE: ${ideId}`);
|
|
453
|
-
console.log(` Supported: ${IDE_CONFIGS.map((c) => c.id).join(", ")}`);
|
|
454
|
-
process.exit(1);
|
|
455
|
-
}
|
|
456
|
-
console.log(`\n Installing for ${ide.label}...\n`);
|
|
457
|
-
// Calculate total steps
|
|
458
|
-
const isClaudeCode = ide.id === "claude-code";
|
|
459
|
-
const hasPermissions = ide.hasPermissions && !config.skipPermissions;
|
|
460
|
-
let totalSteps = 2; // MCP registration + Playwright
|
|
461
|
-
if (hasPermissions)
|
|
462
|
-
totalSteps++;
|
|
463
|
-
if (isClaudeCode)
|
|
464
|
-
totalSteps++; // /ftest command
|
|
465
|
-
let step = 1;
|
|
466
|
-
// Step 1: Register MCP server
|
|
467
|
-
console.log(` [${step}/${totalSteps}] Registering MCP server...`);
|
|
468
|
-
const registered = registerMcpServer(ide, config.scope);
|
|
469
|
-
if (registered) {
|
|
470
|
-
const location = ide.format === "cli"
|
|
471
|
-
? `(scope: ${config.scope})`
|
|
472
|
-
: ide.globalConfigPath;
|
|
473
|
-
console.log(` MCP server "${MCP_SERVER_NAME}" registered ${location}`);
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
if (ide.format === "cli") {
|
|
477
|
-
process.exit(1);
|
|
478
|
-
}
|
|
479
|
-
console.log(" Warning: Could not register MCP server.");
|
|
480
|
-
}
|
|
481
|
-
step++;
|
|
482
|
-
// Step 2: Pre-approve tools (Claude Code only)
|
|
483
|
-
if (hasPermissions) {
|
|
484
|
-
console.log(` [${step}/${totalSteps}] Pre-approving tools...`);
|
|
485
|
-
const { added, alreadyExists } = addClaudePermissions();
|
|
486
|
-
if (added) {
|
|
487
|
-
console.log(` Added ${PERMISSION_ENTRY} to ${CLAUDE_SETTINGS_PATH}`);
|
|
488
|
-
}
|
|
489
|
-
else if (alreadyExists) {
|
|
490
|
-
console.log(` Already configured (${PERMISSION_ENTRY})`);
|
|
491
|
-
}
|
|
492
|
-
step++;
|
|
493
|
-
}
|
|
494
|
-
// Step 3: Install /ftest slash command (Claude Code only)
|
|
495
|
-
if (isClaudeCode) {
|
|
496
|
-
console.log(` [${step}/${totalSteps}] Installing /ftest and /qa commands...`);
|
|
497
|
-
const { installed, updated } = installClaudeCommands();
|
|
498
|
-
if (installed > 0) {
|
|
499
|
-
console.log(` Added ${installed} command(s) to ${CLAUDE_COMMANDS_DIR}`);
|
|
500
|
-
}
|
|
501
|
-
if (updated > 0) {
|
|
502
|
-
console.log(` Updated ${updated} command(s)`);
|
|
503
|
-
}
|
|
504
|
-
if (installed === 0 && updated === 0) {
|
|
505
|
-
console.log(" Warning: Could not install commands (source files missing)");
|
|
506
|
-
}
|
|
507
|
-
step++;
|
|
508
|
-
}
|
|
509
|
-
// Step 4: Install Playwright browsers
|
|
510
|
-
console.log(` [${step}/${totalSteps}] Installing Playwright browsers...`);
|
|
511
|
-
installPlaywrightBrowsers();
|
|
512
|
-
if (isClaudeCode) {
|
|
513
|
-
console.log(`
|
|
514
|
-
Done! Open ${ide.label} and try:
|
|
30
|
+
`),!1);case"json-mcpServers":return K(e.globalConfigPath);case"json-servers":return Q(e.globalConfigPath);case"toml":return X(e.globalConfigPath)}}function ne(e,o){switch(e.format){case"cli":return G(o);case"json-mcpServers":return z(e.globalConfigPath);case"json-servers":return Z(e.globalConfigPath);case"toml":return ee(e.globalConfigPath)}}async function te(e){console.log(`
|
|
31
|
+
FastTest Agent Installer
|
|
32
|
+
`);let o=e.ide??await R(),s=v.find(c=>c.id===o);s||(console.log(` Unknown IDE: ${o}`),console.log(` Supported: ${v.map(c=>c.id).join(", ")}`),process.exit(1)),console.log(`
|
|
33
|
+
Installing for ${s.label}...
|
|
34
|
+
`);let n=s.id==="claude-code",r=s.hasPermissions&&!e.skipPermissions,t=2;r&&t++,n&&t++;let m=1;if(console.log(` [${m}/${t}] Registering MCP server...`),oe(s,e.scope)){let c=s.format==="cli"?`(scope: ${e.scope})`:s.globalConfigPath;console.log(` MCP server "${l}" registered ${c}`)}else s.format==="cli"&&process.exit(1),console.log(" Warning: Could not register MCP server.");if(m++,r){console.log(` [${m}/${t}] Pre-approving tools...`);let{added:c,alreadyExists:S}=q();c?console.log(` Added ${h} to ${d}`):S&&console.log(` Already configured (${h})`),m++}if(n){console.log(` [${m}/${t}] Installing /ftest and /qa commands...`);let{installed:c,updated:S}=V();c>0&&console.log(` Added ${c} command(s) to ${w}`),S>0&&console.log(` Updated ${S} command(s)`),c===0&&S===0&&console.log(" Warning: Could not install commands (source files missing)"),m++}console.log(` [${m}/${t}] Installing Playwright browsers...`),se();let x=` Optional: Add this to your project's ${{"claude-code":"CLAUDE.md",cursor:".cursor/rules",windsurf:".windsurfrules",vscode:".github/copilot-instructions.md",codex:"AGENTS.md",antigravity:"GEMINI.md"}[s.id]} to auto-test after building features:
|
|
35
|
+
|
|
36
|
+
## Testing
|
|
37
|
+
After implementing a feature, verify it works by running:
|
|
38
|
+
\`ftest <app-url> <what to test>\`
|
|
39
|
+
or use \`vibe shield <app-url>\` to generate a full regression suite.`;console.log(n?`
|
|
40
|
+
Done! Open ${s.label} and try:
|
|
515
41
|
|
|
516
42
|
/ftest http://localhost:3000 login flow
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
Done! Open ${ide.label} and try:
|
|
43
|
+
|
|
44
|
+
${x}
|
|
45
|
+
`:`
|
|
46
|
+
Done! Open ${s.label} and try:
|
|
522
47
|
|
|
523
48
|
"ftest my app at http://localhost:3000"
|
|
524
49
|
"ftest explore http://localhost:3000"
|
|
525
50
|
"ftest chaos http://localhost:3000"
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const ide = IDE_CONFIGS.find((c) => c.id === ideId);
|
|
536
|
-
if (!ide) {
|
|
537
|
-
console.log(` Unknown IDE: ${ideId}`);
|
|
538
|
-
process.exit(1);
|
|
539
|
-
}
|
|
540
|
-
console.log(`\n Uninstalling from ${ide.label}...\n`);
|
|
541
|
-
const isClaudeCode = ide.id === "claude-code";
|
|
542
|
-
let totalSteps = 1; // MCP removal
|
|
543
|
-
if (ide.hasPermissions)
|
|
544
|
-
totalSteps++;
|
|
545
|
-
if (isClaudeCode)
|
|
546
|
-
totalSteps++;
|
|
547
|
-
let step = 1;
|
|
548
|
-
console.log(` [${step}/${totalSteps}] Removing MCP server...`);
|
|
549
|
-
const removed = removeMcpServer(ide, config.scope);
|
|
550
|
-
if (removed) {
|
|
551
|
-
console.log(` MCP server "${MCP_SERVER_NAME}" removed`);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
console.log(" MCP server was not registered (nothing to remove)");
|
|
555
|
-
}
|
|
556
|
-
step++;
|
|
557
|
-
if (ide.hasPermissions) {
|
|
558
|
-
console.log(` [${step}/${totalSteps}] Removing tool permissions...`);
|
|
559
|
-
const permRemoved = removeClaudePermissions();
|
|
560
|
-
if (permRemoved) {
|
|
561
|
-
console.log(` Removed ${PERMISSION_ENTRY} from ${CLAUDE_SETTINGS_PATH}`);
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
console.log(" Permission was not present (nothing to remove)");
|
|
565
|
-
}
|
|
566
|
-
step++;
|
|
567
|
-
}
|
|
568
|
-
if (isClaudeCode) {
|
|
569
|
-
console.log(` [${step}/${totalSteps}] Removing /ftest and /qa commands...`);
|
|
570
|
-
const cmdRemoved = removeClaudeCommands();
|
|
571
|
-
if (cmdRemoved > 0) {
|
|
572
|
-
console.log(` Removed ${cmdRemoved} command(s)`);
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
console.log(" Commands were not installed (nothing to remove)");
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
console.log(`\n FastTest has been uninstalled from ${ide.label}.\n`);
|
|
579
|
-
}
|
|
580
|
-
// ---------------------------------------------------------------------------
|
|
581
|
-
// Main
|
|
582
|
-
// ---------------------------------------------------------------------------
|
|
583
|
-
const config = parseArgs();
|
|
584
|
-
if (config.action === "uninstall") {
|
|
585
|
-
await uninstall(config);
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
await install(config);
|
|
589
|
-
}
|
|
590
|
-
//# sourceMappingURL=install.js.map
|
|
51
|
+
|
|
52
|
+
${x}
|
|
53
|
+
`)}async function re(e){console.log(`
|
|
54
|
+
FastTest Agent Uninstaller
|
|
55
|
+
`);let o=e.ide??await R(),s=v.find(p=>p.id===o);s||(console.log(` Unknown IDE: ${o}`),process.exit(1)),console.log(`
|
|
56
|
+
Uninstalling from ${s.label}...
|
|
57
|
+
`);let n=s.id==="claude-code",r=1;s.hasPermissions&&r++,n&&r++;let t=1;console.log(` [${t}/${r}] Removing MCP server...`);let m=ne(s,e.scope);if(console.log(m?` MCP server "${l}" removed`:" MCP server was not registered (nothing to remove)"),t++,s.hasPermissions){console.log(` [${t}/${r}] Removing tool permissions...`);let p=H();console.log(p?` Removed ${h} from ${d}`:" Permission was not present (nothing to remove)"),t++}if(n){console.log(` [${t}/${r}] Removing /ftest and /qa commands...`);let p=Y();p>0?console.log(` Removed ${p} command(s)`):console.log(" Commands were not installed (nothing to remove)")}console.log(`
|
|
58
|
+
FastTest has been uninstalled from ${s.label}.
|
|
59
|
+
`)}var A=J();A.action==="uninstall"?await re(A):await te(A);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fasttest-ai/qa-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "FastTest Agent — MCP server that turns your coding agent into a QA engineer. Test, explore, and break web apps using Playwright.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -33,8 +33,10 @@
|
|
|
33
33
|
},
|
|
34
34
|
"main": "./dist/index.js",
|
|
35
35
|
"scripts": {
|
|
36
|
-
"build": "
|
|
36
|
+
"build": "rm -rf dist && node esbuild.config.mjs",
|
|
37
|
+
"build:dev": "tsc",
|
|
37
38
|
"dev": "tsc --watch",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
38
40
|
"start": "node dist/index.js",
|
|
39
41
|
"ci": "node dist/cli.js"
|
|
40
42
|
},
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"@types/node": "^22.0.0",
|
|
49
|
+
"esbuild": "^0.24.0",
|
|
47
50
|
"typescript": "^5.7.0"
|
|
48
51
|
},
|
|
49
52
|
"engines": {
|