@fasttest-ai/qa-agent 0.4.2 → 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/dist/install.js CHANGED
@@ -1,370 +1,18 @@
1
- /**
2
- * FastTest Agent installer supports Claude Code, Cursor, Windsurf, VS Code, and Codex.
3
- * Registers the MCP server, sets up permissions (where supported), and installs Playwright.
4
- *
5
- * Usage:
6
- * npx @fasttest-ai/qa-agent install # Interactive IDE picker
7
- * npx @fasttest-ai/qa-agent install --ide cursor # Skip prompt
8
- * npx @fasttest-ai/qa-agent install --ide claude-code --scope project
9
- * npx @fasttest-ai/qa-agent uninstall
10
- */
11
- import { execFileSync } from "node:child_process";
12
- import { createInterface } from "node:readline";
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
- import { homedir, platform } from "node:os";
15
- import { join } from "node:path";
16
- const IS_WINDOWS = platform() === "win32";
17
- const IS_MAC = platform() === "darwin";
18
- const MCP_SERVER_NAME = "fasttest";
19
- const NPX_CMD = IS_WINDOWS ? "npx.cmd" : "npx";
20
- const MCP_COMMAND = "npx";
21
- const MCP_ARGS = ["-y", "@fasttest-ai/qa-agent@latest"];
22
- const IDE_CONFIGS = [
23
- {
24
- id: "claude-code",
25
- label: "Claude Code",
26
- globalConfigPath: "", // Uses CLI, not direct file write
27
- format: "cli",
28
- hasPermissions: true,
29
- },
30
- {
31
- id: "cursor",
32
- label: "Cursor",
33
- globalConfigPath: join(homedir(), ".cursor", "mcp.json"),
34
- format: "json-mcpServers",
35
- hasPermissions: false,
36
- },
37
- {
38
- id: "windsurf",
39
- label: "Windsurf",
40
- globalConfigPath: join(homedir(), ".codeium", "windsurf", "mcp_config.json"),
41
- format: "json-mcpServers",
42
- hasPermissions: false,
43
- },
44
- {
45
- id: "vscode",
46
- label: "VS Code / Copilot",
47
- globalConfigPath: IS_MAC
48
- ? join(homedir(), "Library", "Application Support", "Code", "User", "mcp.json")
49
- : IS_WINDOWS
50
- ? join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Code", "User", "mcp.json")
51
- : join(homedir(), ".config", "Code", "User", "mcp.json"),
52
- format: "json-servers",
53
- hasPermissions: false,
54
- },
55
- {
56
- id: "codex",
57
- label: "Codex",
58
- globalConfigPath: join(homedir(), ".codex", "config.toml"),
59
- format: "toml",
60
- hasPermissions: false,
61
- },
62
- ];
63
- function parseArgs() {
64
- const action = process.argv[2];
65
- const rest = process.argv.slice(3);
66
- let ide = null;
67
- let scope = "user";
68
- let skipPermissions = false;
69
- for (let i = 0; i < rest.length; i++) {
70
- if (rest[i] === "--ide" && rest[i + 1]) {
71
- ide = rest[++i];
72
- }
73
- else if (rest[i] === "--scope" && rest[i + 1]) {
74
- scope = rest[++i];
75
- }
76
- else if (rest[i] === "--skip-permissions") {
77
- skipPermissions = true;
78
- }
79
- }
80
- return { action, ide, scope, skipPermissions };
81
- }
82
- // ---------------------------------------------------------------------------
83
- // Interactive IDE picker
84
- // ---------------------------------------------------------------------------
85
- function askQuestion(prompt) {
86
- const rl = createInterface({ input: process.stdin, output: process.stdout });
87
- return new Promise((resolve) => {
88
- rl.question(prompt, (answer) => {
89
- rl.close();
90
- resolve(answer.trim());
91
- });
92
- });
93
- }
94
- async function pickIde() {
95
- console.log(" Which IDE do you use?\n");
96
- IDE_CONFIGS.forEach((ide, i) => {
97
- console.log(` ${i + 1}. ${ide.label}`);
98
- });
99
- console.log();
100
- const answer = await askQuestion(" > ");
101
- const idx = parseInt(answer, 10) - 1;
102
- if (idx >= 0 && idx < IDE_CONFIGS.length) {
103
- return IDE_CONFIGS[idx].id;
104
- }
105
- // Try matching by name
106
- const match = IDE_CONFIGS.find((c) => c.id === answer.toLowerCase() || c.label.toLowerCase() === answer.toLowerCase());
107
- if (match)
108
- return match.id;
109
- console.log(" Invalid selection, defaulting to Claude Code.\n");
110
- return "claude-code";
111
- }
112
- // ---------------------------------------------------------------------------
113
- // Claude Code — CLI-based registration + permission management
114
- // ---------------------------------------------------------------------------
115
- function isClaudeCliAvailable() {
116
- try {
117
- execFileSync("claude", ["--version"], { stdio: "pipe", shell: IS_WINDOWS });
118
- return true;
119
- }
120
- catch {
121
- return false;
122
- }
123
- }
124
- function registerClaudeCode(scope) {
125
- const args = [
126
- "mcp", "add", "--scope", scope,
127
- MCP_SERVER_NAME, "--",
128
- MCP_COMMAND, ...MCP_ARGS,
129
- ];
130
- try {
131
- execFileSync("claude", args, { stdio: "inherit", shell: IS_WINDOWS });
132
- return true;
133
- }
134
- catch {
135
- try {
136
- execFileSync("claude", ["mcp", "remove", "--scope", scope, MCP_SERVER_NAME], {
137
- stdio: "pipe", shell: IS_WINDOWS,
138
- });
139
- execFileSync("claude", args, { stdio: "inherit", shell: IS_WINDOWS });
140
- return true;
141
- }
142
- catch {
143
- return false;
144
- }
145
- }
146
- }
147
- function removeClaudeCode(scope) {
148
- try {
149
- execFileSync("claude", ["mcp", "remove", "--scope", scope, MCP_SERVER_NAME], {
150
- stdio: "inherit", shell: IS_WINDOWS,
151
- });
152
- return true;
153
- }
154
- catch {
155
- return false;
156
- }
157
- }
158
- const CLAUDE_DIR = join(homedir(), ".claude");
159
- const CLAUDE_SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
160
- const PERMISSION_ENTRY = "mcp__fasttest";
161
- function addClaudePermissions() {
162
- if (!existsSync(CLAUDE_DIR))
163
- mkdirSync(CLAUDE_DIR, { recursive: true });
164
- let settings = {};
165
- if (existsSync(CLAUDE_SETTINGS_PATH)) {
166
- try {
167
- settings = JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
168
- }
169
- catch {
170
- const backup = CLAUDE_SETTINGS_PATH + ".bak";
171
- writeFileSync(backup, readFileSync(CLAUDE_SETTINGS_PATH));
172
- console.log(` Warning: ${CLAUDE_SETTINGS_PATH} was corrupted. Backed up to ${backup}`);
173
- settings = {};
174
- }
175
- }
176
- if (!settings.permissions)
177
- settings.permissions = {};
178
- if (!Array.isArray(settings.permissions.allow))
179
- settings.permissions.allow = [];
180
- if (settings.permissions.allow.includes(PERMISSION_ENTRY)) {
181
- return { added: false, alreadyExists: true };
182
- }
183
- settings.permissions.allow.push(PERMISSION_ENTRY);
184
- writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
185
- return { added: true, alreadyExists: false };
186
- }
187
- function removeClaudePermissions() {
188
- if (!existsSync(CLAUDE_SETTINGS_PATH))
189
- return false;
190
- try {
191
- const settings = JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
192
- const allow = settings.permissions?.allow;
193
- if (!Array.isArray(allow))
194
- return false;
195
- const idx = allow.indexOf(PERMISSION_ENTRY);
196
- if (idx === -1)
197
- return false;
198
- allow.splice(idx, 1);
199
- writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
200
- return true;
201
- }
202
- catch {
203
- return false;
204
- }
205
- }
206
- // ---------------------------------------------------------------------------
207
- // JSON config writers (Cursor, Windsurf, VS Code)
208
- // ---------------------------------------------------------------------------
209
- /** Cursor & Windsurf: { "mcpServers": { "fasttest": { "command": ..., "args": ... } } } */
210
- function writeMcpServersJson(configPath) {
211
- const dir = join(configPath, "..");
212
- if (!existsSync(dir))
213
- mkdirSync(dir, { recursive: true });
214
- let config = {};
215
- if (existsSync(configPath)) {
216
- try {
217
- config = JSON.parse(readFileSync(configPath, "utf-8"));
218
- }
219
- catch {
220
- const backup = configPath + ".bak";
221
- writeFileSync(backup, readFileSync(configPath));
222
- console.log(` Warning: ${configPath} was corrupted. Backed up to ${backup}`);
223
- config = {};
224
- }
225
- }
226
- if (!config.mcpServers || typeof config.mcpServers !== "object") {
227
- config.mcpServers = {};
228
- }
229
- config.mcpServers[MCP_SERVER_NAME] = {
230
- command: MCP_COMMAND,
231
- args: [...MCP_ARGS],
232
- };
233
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
234
- return true;
235
- }
236
- /** VS Code: { "servers": { "fasttest": { "type": "stdio", "command": ..., "args": ... } } } */
237
- function writeVscodeJson(configPath) {
238
- const dir = join(configPath, "..");
239
- if (!existsSync(dir))
240
- mkdirSync(dir, { recursive: true });
241
- let config = {};
242
- if (existsSync(configPath)) {
243
- try {
244
- config = JSON.parse(readFileSync(configPath, "utf-8"));
245
- }
246
- catch {
247
- const backup = configPath + ".bak";
248
- writeFileSync(backup, readFileSync(configPath));
249
- console.log(` Warning: ${configPath} was corrupted. Backed up to ${backup}`);
250
- config = {};
251
- }
252
- }
253
- if (!config.servers || typeof config.servers !== "object") {
254
- config.servers = {};
255
- }
256
- config.servers[MCP_SERVER_NAME] = {
257
- type: "stdio",
258
- command: MCP_COMMAND,
259
- args: [...MCP_ARGS],
260
- };
261
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
262
- return true;
263
- }
264
- // ---------------------------------------------------------------------------
265
- // Codex TOML config writer
266
- // ---------------------------------------------------------------------------
267
- function writeCodexToml(configPath) {
268
- const dir = join(configPath, "..");
269
- if (!existsSync(dir))
270
- mkdirSync(dir, { recursive: true });
271
- let content = "";
272
- if (existsSync(configPath)) {
273
- content = readFileSync(configPath, "utf-8");
274
- }
275
- const sectionHeader = `[mcp_servers.${MCP_SERVER_NAME}]`;
276
- if (content.includes(sectionHeader)) {
277
- // Replace existing section — find from header to next section or EOF
278
- const regex = new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?(?=\\n\\[|$)`);
279
- content = content.replace(regex, buildCodexSection());
280
- }
281
- else {
282
- // Append
283
- if (content.length > 0 && !content.endsWith("\n"))
284
- content += "\n";
285
- content += "\n" + buildCodexSection() + "\n";
286
- }
287
- writeFileSync(configPath, content);
288
- return true;
289
- }
290
- function buildCodexSection() {
291
- return `[mcp_servers.${MCP_SERVER_NAME}]\ncommand = "${MCP_COMMAND}"\nargs = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]`;
292
- }
293
- // ---------------------------------------------------------------------------
294
- // Remove from JSON configs
295
- // ---------------------------------------------------------------------------
296
- function removeFromMcpServersJson(configPath) {
297
- if (!existsSync(configPath))
298
- return false;
299
- try {
300
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
301
- const servers = config.mcpServers;
302
- if (!servers || !(MCP_SERVER_NAME in servers))
303
- return false;
304
- delete servers[MCP_SERVER_NAME];
305
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
306
- return true;
307
- }
308
- catch {
309
- return false;
310
- }
311
- }
312
- function removeFromVscodeJson(configPath) {
313
- if (!existsSync(configPath))
314
- return false;
315
- try {
316
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
317
- const servers = config.servers;
318
- if (!servers || !(MCP_SERVER_NAME in servers))
319
- return false;
320
- delete servers[MCP_SERVER_NAME];
321
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
322
- return true;
323
- }
324
- catch {
325
- return false;
326
- }
327
- }
328
- function removeFromCodexToml(configPath) {
329
- if (!existsSync(configPath))
330
- return false;
331
- try {
332
- let content = readFileSync(configPath, "utf-8");
333
- const sectionHeader = `[mcp_servers.${MCP_SERVER_NAME}]`;
334
- if (!content.includes(sectionHeader))
335
- return false;
336
- const regex = new RegExp(`\\n?\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?(?=\\n\\[|$)`);
337
- content = content.replace(regex, "");
338
- writeFileSync(configPath, content);
339
- return true;
340
- }
341
- catch {
342
- return false;
343
- }
344
- }
345
- // ---------------------------------------------------------------------------
346
- // Playwright browser installation
347
- // ---------------------------------------------------------------------------
348
- function installPlaywrightBrowsers() {
349
- try {
350
- execFileSync(NPX_CMD, ["playwright", "install", "--with-deps", "chromium"], {
351
- stdio: "inherit",
352
- });
353
- console.log(" Chromium installed");
354
- }
355
- catch {
356
- console.log(" Warning: Could not install Playwright browsers automatically.");
357
- console.log(" Run manually: npx playwright install --with-deps chromium");
358
- }
359
- }
360
- // ---------------------------------------------------------------------------
361
- // Install
362
- // ---------------------------------------------------------------------------
363
- function registerMcpServer(ide, scope) {
364
- switch (ide.format) {
365
- case "cli":
366
- if (!isClaudeCliAvailable()) {
367
- 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(`
368
16
  Claude Code CLI not found in PATH.
369
17
 
370
18
  Install Claude Code first:
@@ -379,122 +27,33 @@ function registerMcpServer(ide, scope) {
379
27
  }
380
28
  }
381
29
  }
382
- `);
383
- return false;
384
- }
385
- return registerClaudeCode(scope);
386
- case "json-mcpServers":
387
- return writeMcpServersJson(ide.globalConfigPath);
388
- case "json-servers":
389
- return writeVscodeJson(ide.globalConfigPath);
390
- case "toml":
391
- return writeCodexToml(ide.globalConfigPath);
392
- }
393
- }
394
- function removeMcpServer(ide, scope) {
395
- switch (ide.format) {
396
- case "cli":
397
- return removeClaudeCode(scope);
398
- case "json-mcpServers":
399
- return removeFromMcpServersJson(ide.globalConfigPath);
400
- case "json-servers":
401
- return removeFromVscodeJson(ide.globalConfigPath);
402
- case "toml":
403
- return removeFromCodexToml(ide.globalConfigPath);
404
- }
405
- }
406
- async function install(config) {
407
- console.log("\n FastTest Agent Installer\n");
408
- const ideId = config.ide ?? (await pickIde());
409
- const ide = IDE_CONFIGS.find((c) => c.id === ideId);
410
- if (!ide) {
411
- console.log(` Unknown IDE: ${ideId}`);
412
- console.log(` Supported: ${IDE_CONFIGS.map((c) => c.id).join(", ")}`);
413
- process.exit(1);
414
- }
415
- console.log(`\n Installing for ${ide.label}...\n`);
416
- // Step 1: Register MCP server
417
- const totalSteps = ide.hasPermissions && !config.skipPermissions ? 3 : 2;
418
- let step = 1;
419
- console.log(` [${step}/${totalSteps}] Registering MCP server...`);
420
- const registered = registerMcpServer(ide, config.scope);
421
- if (registered) {
422
- const location = ide.format === "cli"
423
- ? `(scope: ${config.scope})`
424
- : ide.globalConfigPath;
425
- console.log(` MCP server "${MCP_SERVER_NAME}" registered ${location}`);
426
- }
427
- else {
428
- if (ide.format === "cli") {
429
- process.exit(1);
430
- }
431
- console.log(" Warning: Could not register MCP server.");
432
- }
433
- step++;
434
- // Step 2: Pre-approve tools (Claude Code only)
435
- if (ide.hasPermissions && !config.skipPermissions) {
436
- console.log(` [${step}/${totalSteps}] Pre-approving tools...`);
437
- const { added, alreadyExists } = addClaudePermissions();
438
- if (added) {
439
- console.log(` Added ${PERMISSION_ENTRY} to ${CLAUDE_SETTINGS_PATH}`);
440
- }
441
- else if (alreadyExists) {
442
- console.log(` Already configured (${PERMISSION_ENTRY})`);
443
- }
444
- step++;
445
- }
446
- // Step 3: Install Playwright browsers
447
- console.log(` [${step}/${totalSteps}] Installing Playwright browsers...`);
448
- installPlaywrightBrowsers();
449
- console.log(`
450
- 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:
41
+
42
+ /ftest http://localhost:3000 login flow
43
+
44
+ ${x}
45
+ `:`
46
+ Done! Open ${s.label} and try:
47
+
48
+ "ftest my app at http://localhost:3000"
49
+ "ftest explore http://localhost:3000"
50
+ "ftest chaos http://localhost:3000"
451
51
 
452
- "test my app at http://localhost:3000"
453
- `);
454
- }
455
- // ---------------------------------------------------------------------------
456
- // Uninstall
457
- // ---------------------------------------------------------------------------
458
- async function uninstall(config) {
459
- console.log("\n FastTest Agent Uninstaller\n");
460
- const ideId = config.ide ?? (await pickIde());
461
- const ide = IDE_CONFIGS.find((c) => c.id === ideId);
462
- if (!ide) {
463
- console.log(` Unknown IDE: ${ideId}`);
464
- process.exit(1);
465
- }
466
- console.log(`\n Uninstalling from ${ide.label}...\n`);
467
- const totalSteps = ide.hasPermissions ? 2 : 1;
468
- let step = 1;
469
- console.log(` [${step}/${totalSteps}] Removing MCP server...`);
470
- const removed = removeMcpServer(ide, config.scope);
471
- if (removed) {
472
- console.log(` MCP server "${MCP_SERVER_NAME}" removed`);
473
- }
474
- else {
475
- console.log(" MCP server was not registered (nothing to remove)");
476
- }
477
- step++;
478
- if (ide.hasPermissions) {
479
- console.log(` [${step}/${totalSteps}] Removing tool permissions...`);
480
- const permRemoved = removeClaudePermissions();
481
- if (permRemoved) {
482
- console.log(` Removed ${PERMISSION_ENTRY} from ${CLAUDE_SETTINGS_PATH}`);
483
- }
484
- else {
485
- console.log(" Permission was not present (nothing to remove)");
486
- }
487
- }
488
- console.log(`\n FastTest has been uninstalled from ${ide.label}.\n`);
489
- }
490
- // ---------------------------------------------------------------------------
491
- // Main
492
- // ---------------------------------------------------------------------------
493
- const config = parseArgs();
494
- if (config.action === "uninstall") {
495
- await uninstall(config);
496
- }
497
- else {
498
- await install(config);
499
- }
500
- //# sourceMappingURL=install.js.map
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.4.2",
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": "tsc",
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": {
@@ -51,7 +54,8 @@
51
54
  },
52
55
  "files": [
53
56
  "dist",
54
- "bin"
57
+ "bin",
58
+ "commands"
55
59
  ],
56
60
  "license": "Apache-2.0"
57
61
  }
package/dist/actions.d.ts DELETED
@@ -1,41 +0,0 @@
1
- /**
2
- * Browser action executor — navigate, click, fill, screenshot, etc.
3
- */
4
- import type { Page } from "playwright";
5
- export interface ActionResult {
6
- success: boolean;
7
- error?: string;
8
- data?: Record<string, unknown>;
9
- }
10
- export declare function navigate(page: Page, url: string): Promise<ActionResult>;
11
- export declare function click(page: Page, selector: string): Promise<ActionResult>;
12
- export declare function fill(page: Page, selector: string, value: string): Promise<ActionResult>;
13
- export declare function hover(page: Page, selector: string): Promise<ActionResult>;
14
- export declare function selectOption(page: Page, selector: string, value: string): Promise<ActionResult>;
15
- export declare function waitFor(page: Page, selector: string, timeoutMs?: number): Promise<ActionResult>;
16
- export declare function screenshot(page: Page, fullPage?: boolean): Promise<string>;
17
- export declare function getSnapshot(page: Page): Promise<Record<string, unknown>>;
18
- export declare function goBack(page: Page): Promise<ActionResult>;
19
- export declare function goForward(page: Page): Promise<ActionResult>;
20
- export declare function pressKey(page: Page, key: string): Promise<ActionResult>;
21
- export declare function uploadFile(page: Page, selector: string, filePaths: string[]): Promise<ActionResult>;
22
- export declare function evaluate(page: Page, expression: string): Promise<ActionResult>;
23
- export declare function drag(page: Page, sourceSelector: string, targetSelector: string): Promise<ActionResult>;
24
- export declare function resize(page: Page, width: number, height: number): Promise<ActionResult>;
25
- export declare function fillForm(page: Page, fields: Record<string, string>): Promise<ActionResult>;
26
- export type AssertionType = "element_visible" | "element_hidden" | "text_contains" | "text_equals" | "url_contains" | "url_equals" | "element_count" | "attribute_value";
27
- export interface AssertionParams {
28
- type: AssertionType;
29
- selector?: string;
30
- text?: string;
31
- url?: string;
32
- count?: number;
33
- attribute?: string;
34
- value?: string;
35
- }
36
- export interface AssertionResult {
37
- pass: boolean;
38
- actual?: string | number | boolean;
39
- error?: string;
40
- }
41
- export declare function assertPage(page: Page, params: AssertionParams): Promise<AssertionResult>;