@qa-gentic/stlc-agents 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.
Files changed (36) hide show
  1. package/README.md +310 -0
  2. package/bin/postinstall.js +78 -0
  3. package/bin/qa-stlc.js +89 -0
  4. package/package.json +48 -0
  5. package/skills/qa-stlc/AGENT-BEHAVIOR.md +383 -0
  6. package/skills/qa-stlc/deduplication-protocol.md +303 -0
  7. package/skills/qa-stlc/generate-gherkin.md +550 -0
  8. package/skills/qa-stlc/generate-playwright-code.md +464 -0
  9. package/skills/qa-stlc/generate-test-cases.md +176 -0
  10. package/skills/qa-stlc/write-helix-files.md +374 -0
  11. package/src/boilerplate-bundle.js +66 -0
  12. package/src/cmd-init.js +92 -0
  13. package/src/cmd-mcp-config.js +177 -0
  14. package/src/cmd-scaffold.js +130 -0
  15. package/src/cmd-skills.js +124 -0
  16. package/src/cmd-verify.js +129 -0
  17. package/src/stlc_agents/__init__.py +0 -0
  18. package/src/stlc_agents/agent_gherkin_generator/__init__.py +0 -0
  19. package/src/stlc_agents/agent_gherkin_generator/server.py +502 -0
  20. package/src/stlc_agents/agent_gherkin_generator/tools/__init__.py +0 -0
  21. package/src/stlc_agents/agent_gherkin_generator/tools/ado_gherkin.py +854 -0
  22. package/src/stlc_agents/agent_helix_writer/__init__.py +0 -0
  23. package/src/stlc_agents/agent_helix_writer/server.py +529 -0
  24. package/src/stlc_agents/agent_helix_writer/tools/__init__.py +0 -0
  25. package/src/stlc_agents/agent_helix_writer/tools/boilerplate.py +70 -0
  26. package/src/stlc_agents/agent_helix_writer/tools/helix_write.py +796 -0
  27. package/src/stlc_agents/agent_playwright_generator/__init__.py +0 -0
  28. package/src/stlc_agents/agent_playwright_generator/server.py +2610 -0
  29. package/src/stlc_agents/agent_playwright_generator/tools/__init__.py +0 -0
  30. package/src/stlc_agents/agent_playwright_generator/tools/ado_attach.py +62 -0
  31. package/src/stlc_agents/agent_test_case_manager/__init__.py +0 -0
  32. package/src/stlc_agents/agent_test_case_manager/server.py +483 -0
  33. package/src/stlc_agents/agent_test_case_manager/tools/__init__.py +0 -0
  34. package/src/stlc_agents/agent_test_case_manager/tools/ado_workitem.py +302 -0
  35. package/src/stlc_agents/shared/__init__.py +0 -0
  36. package/src/stlc_agents/shared/auth.py +119 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * cmd-init.js — `qa-stlc init`
3
+ *
4
+ * Full bootstrap: install Python agents + skills + MCP config.
5
+ */
6
+ "use strict";
7
+
8
+ const { execSync, spawnSync } = require("child_process");
9
+ const path = require("path");
10
+ const fs = require("fs");
11
+
12
+ const cmdSkills = require("./cmd-skills");
13
+ const cmdMcpConfig = require("./cmd-mcp-config");
14
+
15
+ const C = {
16
+ reset: "\x1b[0m", bold: "\x1b[1m",
17
+ green: "\x1b[32m", cyan: "\x1b[36m",
18
+ yellow: "\x1b[33m", red: "\x1b[31m", dim: "\x1b[2m",
19
+ };
20
+ const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
21
+ const info = (m) => console.log(`${C.cyan}→${C.reset} ${m}`);
22
+ const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
23
+ const die = (m) => { console.error(`${C.red}✗${C.reset} ${m}`); process.exit(1); };
24
+
25
+ module.exports = async function init(opts) {
26
+ console.log(`\n${C.bold}QA STLC Agents — init${C.reset}\n`);
27
+
28
+ // ── 1. Check Python ──────────────────────────────────────────────────────
29
+ const python = opts.python || "python3";
30
+ info(`Checking Python (${python})…`);
31
+ const pyCheck = spawnSync(python, ["--version"], { encoding: "utf8" });
32
+ if (pyCheck.status !== 0) {
33
+ die(`Python not found at '${python}'. Install Python 3.10+ or pass --python <path>.`);
34
+ }
35
+ const pyVersion = (pyCheck.stdout || pyCheck.stderr || "").trim();
36
+ const match = pyVersion.match(/Python (\d+)\.(\d+)/);
37
+ if (!match || parseInt(match[1]) < 3 || parseInt(match[2]) < 10) {
38
+ die(`Python 3.10+ required, found: ${pyVersion}`);
39
+ }
40
+ ok(`${pyVersion} found.`);
41
+
42
+ // ── 2. pip install qa-gentic-stlc-agents ────────────────────────────────────────
43
+ info("Installing qa-gentic-stlc-agents (pip)…");
44
+ const pip = spawnSync(python, ["-m", "pip", "install", "qa-gentic-stlc-agents>=1.0.1", "--quiet"], {
45
+ stdio: "inherit",
46
+ encoding: "utf8",
47
+ });
48
+ if (pip.status !== 0) {
49
+ die("pip install failed. Run manually: pip install qa-gentic-stlc-agents");
50
+ }
51
+ ok("qa-gentic-stlc-agents installed.");
52
+
53
+ // ── 3. Install skills ────────────────────────────────────────────────────
54
+ info("Installing skills…");
55
+ const skillTarget = opts.vscode ? "both" : "claude";
56
+ await cmdSkills({ target: skillTarget });
57
+
58
+ // ── 4. Write MCP config ──────────────────────────────────────────────────
59
+ info("Writing MCP config…");
60
+ await cmdMcpConfig({
61
+ vscode: opts.vscode || false,
62
+ print: false,
63
+ python: python,
64
+ playwrightPort: "8931",
65
+ });
66
+
67
+ // ── 5. Done ──────────────────────────────────────────────────────────────
68
+ const mcpLocation = opts.vscode ? ".vscode/mcp.json" : ".mcp.json";
69
+ const skillsLocation = opts.vscode
70
+ ? ".github/copilot-instructions/ (reload VS Code window to activate)"
71
+ : ".claude/skills/";
72
+
73
+ console.log(`
74
+ ${C.bold}Setup complete.${C.reset}
75
+
76
+ ${C.dim}MCP config :${C.reset} ${mcpLocation}
77
+ ${C.dim}Skills :${C.reset} ${skillsLocation}
78
+
79
+ ${C.bold}Start Playwright MCP${C.reset} ${C.dim}(keep running in a separate terminal):${C.reset}
80
+
81
+ ${C.cyan}npx @playwright/mcp@latest --port 8931${C.reset}
82
+
83
+ ${C.bold}STLC Workflow${C.reset} ${C.dim}— use agents in this order:${C.reset}
84
+
85
+ ${C.dim}1 →${C.reset} ${C.yellow}qa-test-case-manager${C.reset} ${C.dim}ADO work item ID → create manual test cases in ADO${C.reset}
86
+ ${C.dim}2 →${C.reset} ${C.yellow}qa-gherkin-generator${C.reset} ${C.dim}Epic / Feature / PBI → BDD .feature file attached to ADO${C.reset}
87
+ ${C.dim}3 →${C.reset} ${C.yellow}qa-playwright-generator${C.reset} ${C.dim}Gherkin + live page snapshot → self-healing Playwright TypeScript${C.reset}
88
+ ${C.dim}4 →${C.reset} ${C.yellow}qa-helix-writer${C.reset} ${C.dim}Generated files → merged into Helix-QA project on disk${C.reset}
89
+
90
+ ${C.dim}Skills reference all four steps — ask your agent: "@generate-playwright-code"${C.reset}
91
+ `);
92
+ };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * cmd-mcp-config.js — `qa-stlc mcp-config`
3
+ *
4
+ * Generates .mcp.json (Claude Code) or .vscode/mcp.json (GitHub Copilot / VS Code).
5
+ */
6
+ "use strict";
7
+
8
+ const path = require("path");
9
+ const fs = require("fs");
10
+ const { spawnSync } = require("child_process");
11
+
12
+ const C = {
13
+ reset: "\x1b[0m", bold: "\x1b[1m",
14
+ green: "\x1b[32m", cyan: "\x1b[36m",
15
+ yellow: "\x1b[33m", red: "\x1b[31m", dim: "\x1b[2m",
16
+ };
17
+ const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
18
+ const info = (m) => console.log(`${C.cyan}→${C.reset} ${m}`);
19
+ const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
20
+
21
+ const CWD = process.cwd();
22
+ const IS_WIN = process.platform === "win32";
23
+ const EXT = IS_WIN ? ".exe" : "";
24
+
25
+ const AGENT_NAMES = [
26
+ "qa-test-case-manager",
27
+ "qa-gherkin-generator",
28
+ "qa-playwright-generator",
29
+ "qa-helix-writer",
30
+ ];
31
+
32
+ /**
33
+ * Locate the binary for an agent.
34
+ * Priority: .venv → --python dir → system PATH → python sys.executable dir → macOS framework dirs
35
+ */
36
+ function findBinary(name, pythonBin) {
37
+ // 1. .venv in cwd
38
+ const venvBin = IS_WIN
39
+ ? path.join(CWD, ".venv", "Scripts", name + EXT)
40
+ : path.join(CWD, ".venv", "bin", name);
41
+ if (fs.existsSync(venvBin)) return venvBin;
42
+
43
+ // 2. dir of --python flag binary
44
+ if (pythonBin && pythonBin !== "python3" && pythonBin !== "python") {
45
+ const candidate = path.join(path.dirname(pythonBin), name + EXT);
46
+ if (fs.existsSync(candidate)) return candidate;
47
+ }
48
+
49
+ // 3. system PATH via which/where
50
+ const which = spawnSync(IS_WIN ? "where" : "which", [name], { encoding: "utf8" });
51
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim().split("\n")[0].trim();
52
+
53
+ // 4. ask Python where its own bin dir is (catches framework installs not on PATH)
54
+ for (const py of ["python3", "python"]) {
55
+ const r = spawnSync(py, ["-c", "import sys, os; print(os.path.dirname(sys.executable))"], { encoding: "utf8" });
56
+ if (r.status === 0 && r.stdout.trim()) {
57
+ const candidate = path.join(r.stdout.trim(), name + EXT);
58
+ if (fs.existsSync(candidate)) return candidate;
59
+ }
60
+ }
61
+
62
+ // 5. macOS Python.org framework fallback
63
+ if (!IS_WIN) {
64
+ const frameworkDirs = [
65
+ "/Library/Frameworks/Python.framework/Versions/3.13/bin",
66
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin",
67
+ "/Library/Frameworks/Python.framework/Versions/3.11/bin",
68
+ "/Library/Frameworks/Python.framework/Versions/3.10/bin",
69
+ ];
70
+ for (const dir of frameworkDirs) {
71
+ const candidate = path.join(dir, name);
72
+ if (fs.existsSync(candidate)) return candidate;
73
+ }
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ function buildClaudeConfig(pythonBin, playwrightPort) {
80
+ const servers = {};
81
+ const missing = [];
82
+
83
+ for (const name of AGENT_NAMES) {
84
+ const bin = findBinary(name, pythonBin);
85
+ if (bin) {
86
+ servers[name] = { command: bin };
87
+ } else {
88
+ missing.push(name);
89
+ servers[name] = { command: `/path/to/.venv/bin/${name}`, "_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents" };
90
+ }
91
+ }
92
+
93
+ servers["playwright"] = {
94
+ type: "url",
95
+ url: `ws://localhost:${playwrightPort}`,
96
+ };
97
+
98
+ return { config: { mcpServers: servers }, missing };
99
+ }
100
+
101
+ function buildVscodeConfig(pythonBin, playwrightPort) {
102
+ const servers = {};
103
+ const missing = [];
104
+
105
+ for (const name of AGENT_NAMES) {
106
+ const bin = findBinary(name, pythonBin);
107
+ if (bin) {
108
+ servers[name] = { command: bin };
109
+ } else {
110
+ missing.push(name);
111
+ servers[name] = { command: `/path/to/.venv/bin/${name}`, "_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents" };
112
+ }
113
+ }
114
+
115
+ servers["playwright"] = {
116
+ type: "http",
117
+ url: `http://localhost:${playwrightPort}/mcp`,
118
+ };
119
+
120
+ return { config: { servers }, missing };
121
+ }
122
+
123
+ function printNextSteps(mode, playwrightPort) {
124
+ const isVscode = mode === "vscode";
125
+ console.log(`
126
+ ${C.dim}Start Playwright MCP before running generation workflows:${C.reset}
127
+ npx @playwright/mcp@latest --port ${playwrightPort}
128
+ ${C.dim}headless (CI): npx @playwright/mcp@latest --headless --port ${playwrightPort}${C.reset}
129
+
130
+ ${isVscode
131
+ ? `Reload VS Code window — all 5 MCP servers will appear in the MCP panel.`
132
+ : `In Claude Code, run /mcp to verify all 5 servers are loaded.`
133
+ }
134
+ `);
135
+ }
136
+
137
+ module.exports = async function mcpConfig(opts) {
138
+ const useVscode = opts.vscode || false;
139
+ const printOnly = opts.print || false;
140
+ const pythonBin = opts.python || "python3";
141
+ const playwrightPort = opts.playwrightPort || "8931";
142
+
143
+ if (printOnly) {
144
+ const { config: claudeCfg } = buildClaudeConfig(pythonBin, playwrightPort);
145
+ const { config: vscodeCfg } = buildVscodeConfig(pythonBin, playwrightPort);
146
+ console.log("\n=== Claude Code (.mcp.json) ===");
147
+ console.log(JSON.stringify(claudeCfg, null, 2));
148
+ console.log("\n=== VS Code (.vscode/mcp.json) ===");
149
+ console.log(JSON.stringify(vscodeCfg, null, 2));
150
+ printNextSteps("both", playwrightPort);
151
+ return;
152
+ }
153
+
154
+ if (useVscode) {
155
+ const { config, missing } = buildVscodeConfig(pythonBin, playwrightPort);
156
+ const dir = path.join(CWD, ".vscode");
157
+ fs.mkdirSync(dir, { recursive: true });
158
+ const out = path.join(dir, "mcp.json");
159
+ fs.writeFileSync(out, JSON.stringify(config, null, 2) + "\n", "utf8");
160
+ ok(`Written → .vscode/mcp.json`);
161
+ if (missing.length) {
162
+ warn(`${missing.length} agent(s) not found — run: pip install qa-gentic-stlc-agents`);
163
+ missing.forEach((m) => warn(` missing: ${m}`));
164
+ }
165
+ printNextSteps("vscode", playwrightPort);
166
+ } else {
167
+ const { config, missing } = buildClaudeConfig(pythonBin, playwrightPort);
168
+ const out = path.join(CWD, ".mcp.json");
169
+ fs.writeFileSync(out, JSON.stringify(config, null, 2) + "\n", "utf8");
170
+ ok(`Written → .mcp.json`);
171
+ if (missing.length) {
172
+ warn(`${missing.length} agent(s) not found — run: pip install qa-gentic-stlc-agents`);
173
+ missing.forEach((m) => warn(` missing: ${m}`));
174
+ }
175
+ printNextSteps("claude", playwrightPort);
176
+ }
177
+ };
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * cmd-scaffold.js — qa-stlc scaffold
5
+ *
6
+ * Writes the embedded Healix QA framework boilerplate into a new project directory
7
+ * and performs post-scaffold customisation (project name replacement, etc.).
8
+ * All framework files are bundled in boilerplate-bundle.js — no filesystem
9
+ * dependency on the boilerplate/ directory at runtime.
10
+ *
11
+ * Usage:
12
+ * qa-stlc scaffold # scaffold into ./my-qa-project
13
+ * qa-stlc scaffold --name acme-tests # custom project name
14
+ * qa-stlc scaffold --dir /path/to/target # custom output directory
15
+ * qa-stlc scaffold --no-install # skip npm install
16
+ */
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+ const cp = require("child_process");
21
+
22
+ const BUNDLE = require("./boilerplate-bundle");
23
+
24
+ /**
25
+ * Write all bundle entries to the target directory.
26
+ * Skips entries whose content is empty (directory markers like .gitkeep).
27
+ */
28
+ function writeBundleToDir(bundle, dest) {
29
+ for (const [relPath, content] of Object.entries(bundle)) {
30
+ const destPath = path.join(dest, relPath);
31
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
32
+ if (relPath.endsWith(".gitkeep")) {
33
+ // Just ensure the directory exists — don't write .gitkeep files
34
+ continue;
35
+ }
36
+ fs.writeFileSync(destPath, content, "utf-8");
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Replace all occurrences of a string in a file (binary-safe via Buffer → string).
42
+ */
43
+ function replaceInFile(filePath, search, replacement) {
44
+ try {
45
+ const content = fs.readFileSync(filePath, "utf-8");
46
+ if (!content.includes(search)) return;
47
+ fs.writeFileSync(filePath, content.split(search).join(replacement), "utf-8");
48
+ } catch {
49
+ // Binary or unreadable files — skip silently
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Walk a directory and apply replaceInFile to every text file.
55
+ */
56
+ function replaceInDir(dir, search, replacement) {
57
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
58
+ const fullPath = path.join(dir, entry.name);
59
+ if (entry.isDirectory()) {
60
+ replaceInDir(fullPath, search, replacement);
61
+ } else if (entry.isFile()) {
62
+ replaceInFile(fullPath, search, replacement);
63
+ }
64
+ }
65
+ }
66
+
67
+ module.exports = async function scaffold(options) {
68
+ const projectName = options.name || "my-qa-project";
69
+ const targetDir = path.resolve(options.dir || projectName);
70
+ const runInstall = options.install !== false;
71
+
72
+ // Guard: target must not already exist (or be empty)
73
+ if (fs.existsSync(targetDir)) {
74
+ const entries = fs.readdirSync(targetDir).filter(f => f !== ".git");
75
+ if (entries.length > 0) {
76
+ console.error(`\n ❌ Target directory is not empty: ${targetDir}`);
77
+ console.error(` Use --dir to choose a different location.\n`);
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ console.log(`\n 🚀 Scaffolding QA framework boilerplate`);
83
+ console.log(` Name : ${projectName}`);
84
+ console.log(` Target : ${targetDir}\n`);
85
+
86
+ // 1. Write embedded bundle
87
+ console.log(" 📁 Writing framework files…");
88
+ writeBundleToDir(BUNDLE, targetDir);
89
+
90
+ // 2. Replace {{PROJECT_NAME}} placeholder
91
+ console.log(" ✏️ Applying project name…");
92
+ replaceInDir(targetDir, "{{PROJECT_NAME}}", projectName);
93
+
94
+ // 3. Copy .env.example → .env (if not already present)
95
+ const envExample = path.join(targetDir, ".env.example");
96
+ const envFile = path.join(targetDir, ".env");
97
+ if (fs.existsSync(envExample) && !fs.existsSync(envFile)) {
98
+ fs.copyFileSync(envExample, envFile);
99
+ console.log(" 📋 Created .env from .env.example");
100
+ }
101
+
102
+ // 4. npm install
103
+ if (runInstall) {
104
+ console.log(" 📦 Installing npm dependencies (this may take a minute)…");
105
+ try {
106
+ cp.execSync("npm install", { cwd: targetDir, stdio: "inherit" });
107
+ } catch {
108
+ console.warn("\n ⚠️ npm install exited with an error — check output above.\n");
109
+ }
110
+ }
111
+
112
+ // 5. Print next steps
113
+ console.log("");
114
+ console.log(" ✅ Scaffold complete!\n");
115
+ console.log(" Next steps:");
116
+ console.log(` cd ${path.relative(process.cwd(), targetDir) || "."}`);
117
+ if (!runInstall) console.log(" npm install");
118
+ console.log(" npx playwright install chromium");
119
+ console.log(" # Edit .env — set BASE_URL and (optionally) AI_API_KEY");
120
+ console.log(" npm test");
121
+ console.log("");
122
+ console.log(" To add a new page:");
123
+ console.log(" cp src/templates/_template.feature src/test/features/my-page.feature");
124
+ console.log(" cp src/templates/_template.locators.ts src/locators/my-page.locators.ts");
125
+ console.log(" cp src/templates/_template.steps.ts src/test/steps/my-page.steps.ts");
126
+ console.log(" cp src/templates/_TemplatePage.ts src/pages/MyPage.ts");
127
+ console.log("");
128
+ console.log(" Docs: https://github.com/your-org/stlc-agents#scaffold");
129
+ console.log("");
130
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * cmd-skills.js — `qa-stlc skills`
3
+ *
4
+ * Copies skill markdown files into the correct directory for each coding agent:
5
+ *
6
+ * claude → .claude/skills/ + .claude/AGENT-BEHAVIOR.md
7
+ * vscode → .github/copilot-instructions/
8
+ * cursor → .cursor/rules/ (one file per skill)
9
+ * windsurf → .windsurf/rules/
10
+ * both → claude + vscode
11
+ * print → stdout only
12
+ */
13
+ "use strict";
14
+
15
+ const path = require("path");
16
+ const fs = require("fs");
17
+
18
+ const C = {
19
+ reset: "\x1b[0m", bold: "\x1b[1m",
20
+ green: "\x1b[32m", yellow: "\x1b[33m", dim: "\x1b[2m",
21
+ };
22
+ const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
23
+ const info = (m) => console.log(` ${C.dim}${m}${C.reset}`);
24
+ const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
25
+
26
+ // Resolve the skills bundled with this npm package
27
+ const PKG_ROOT = path.resolve(__dirname, "..");
28
+ const SKILLS_DIR = path.join(PKG_ROOT, "skills", "qa-stlc");
29
+ const BEHAVIOR_MD = path.join(SKILLS_DIR, "AGENT-BEHAVIOR.md");
30
+
31
+ /** Copy a file, creating parent dirs as needed. */
32
+ function cp(src, dest) {
33
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
34
+ fs.copyFileSync(src, dest);
35
+ }
36
+
37
+ /** List all .md files in SKILLS_DIR (flat). */
38
+ function skillFiles() {
39
+ return fs.readdirSync(SKILLS_DIR)
40
+ .filter((f) => f.endsWith(".md") && f !== "AGENT-BEHAVIOR.md")
41
+ .map((f) => path.join(SKILLS_DIR, f));
42
+ }
43
+
44
+ const CWD = process.cwd();
45
+
46
+ function installClaude() {
47
+ const dest = path.join(CWD, ".claude", "skills");
48
+ for (const src of skillFiles()) {
49
+ cp(src, path.join(dest, path.basename(src)));
50
+ }
51
+ cp(BEHAVIOR_MD, path.join(CWD, ".claude", "AGENT-BEHAVIOR.md"));
52
+ ok(`Skills installed → .claude/skills/`);
53
+ info("AGENT-BEHAVIOR.md → .claude/AGENT-BEHAVIOR.md");
54
+ skillFiles().forEach((f) => info(path.basename(f)));
55
+ printPlaywrightHint();
56
+ }
57
+
58
+ function installVscode() {
59
+ const dest = path.join(CWD, ".github", "copilot-instructions");
60
+ for (const src of skillFiles()) {
61
+ cp(src, path.join(dest, path.basename(src)));
62
+ }
63
+ cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
64
+ ok(`Skills installed → .github/copilot-instructions/`);
65
+ info("Add to .github/copilot-instructions.md:");
66
+ info(" @.github/copilot-instructions/AGENT-BEHAVIOR.md");
67
+ printPlaywrightHint();
68
+ }
69
+
70
+ function installCursor() {
71
+ const dest = path.join(CWD, ".cursor", "rules");
72
+ for (const src of skillFiles()) {
73
+ cp(src, path.join(dest, path.basename(src)));
74
+ }
75
+ cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
76
+ ok(`Skills installed → .cursor/rules/`);
77
+ printPlaywrightHint();
78
+ }
79
+
80
+ function installWindsurf() {
81
+ const dest = path.join(CWD, ".windsurf", "rules");
82
+ for (const src of skillFiles()) {
83
+ cp(src, path.join(dest, path.basename(src)));
84
+ }
85
+ cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
86
+ ok(`Skills installed → .windsurf/rules/`);
87
+ printPlaywrightHint();
88
+ }
89
+
90
+ function printSkills() {
91
+ console.log("\nAvailable skills:\n");
92
+ skillFiles().forEach((f) => console.log(` ${path.basename(f)}`));
93
+ console.log(` AGENT-BEHAVIOR.md`);
94
+ }
95
+
96
+ function printPlaywrightHint() {
97
+ console.log(`
98
+ ${C.dim}Start Playwright MCP before running generation workflows:${C.reset}
99
+ npx @playwright/mcp@latest --port 8931
100
+ `);
101
+ }
102
+
103
+ module.exports = async function skills(opts) {
104
+ const target = (opts.target || "claude").toLowerCase();
105
+
106
+ if (!fs.existsSync(SKILLS_DIR)) {
107
+ console.error(`Skills directory not found: ${SKILLS_DIR}`);
108
+ console.error("Try reinstalling: npm install -g @qa-stlc/agents");
109
+ process.exit(1);
110
+ }
111
+
112
+ switch (target) {
113
+ case "claude": installClaude(); break;
114
+ case "vscode": installVscode(); break;
115
+ case "cursor": installCursor(); break;
116
+ case "windsurf": installWindsurf(); break;
117
+ case "both": installClaude(); installVscode(); break;
118
+ case "print": printSkills(); break;
119
+ default:
120
+ warn(`Unknown target: ${target}`);
121
+ warn("Valid targets: claude, vscode, cursor, windsurf, both, print");
122
+ process.exit(1);
123
+ }
124
+ };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * cmd-verify.js — `qa-stlc verify`
3
+ *
4
+ * Quick sanity checks:
5
+ * 1. Are the four Python MCP agents on PATH (or in .venv)?
6
+ * 2. Is Playwright MCP reachable on the expected port?
7
+ * 3. Is MSAL auth cache present?
8
+ */
9
+ "use strict";
10
+
11
+ const path = require("path");
12
+ const fs = require("fs");
13
+ const http = require("http");
14
+ const https = require("https");
15
+ const { spawnSync } = require("child_process");
16
+ const os = require("os");
17
+
18
+ const C = {
19
+ reset: "\x1b[0m", bold: "\x1b[1m",
20
+ green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", dim: "\x1b[2m",
21
+ };
22
+ const ok = (m) => console.log(` ${C.green}✓${C.reset} ${m}`);
23
+ const fail = (m) => console.log(` ${C.red}✗${C.reset} ${m}`);
24
+ const warn = (m) => console.log(` ${C.yellow}⚠${C.reset} ${m}`);
25
+ const head = (m) => console.log(`\n${C.bold}${m}${C.reset}`);
26
+
27
+ const CWD = process.cwd();
28
+ const IS_WIN = process.platform === "win32";
29
+ const EXT = IS_WIN ? ".exe" : "";
30
+
31
+ const AGENT_NAMES = [
32
+ "qa-test-case-manager",
33
+ "qa-gherkin-generator",
34
+ "qa-playwright-generator",
35
+ "qa-helix-writer",
36
+ ];
37
+
38
+ function findBinary(name) {
39
+ // .venv first
40
+ const venvBin = IS_WIN
41
+ ? path.join(CWD, ".venv", "Scripts", name + EXT)
42
+ : path.join(CWD, ".venv", "bin", name);
43
+ if (fs.existsSync(venvBin)) return venvBin;
44
+ // system PATH
45
+ const w = spawnSync(IS_WIN ? "where" : "which", [name], { encoding: "utf8" });
46
+ if (w.status === 0 && w.stdout.trim()) return w.stdout.trim().split("\n")[0].trim();
47
+ return null;
48
+ }
49
+
50
+ function checkPort(port) {
51
+ return new Promise((resolve) => {
52
+ const req = http.get(`http://localhost:${port}/`, (res) => {
53
+ res.destroy();
54
+ resolve(true);
55
+ });
56
+ req.setTimeout(2000, () => { req.destroy(); resolve(false); });
57
+ req.on("error", () => resolve(false));
58
+ });
59
+ }
60
+
61
+ module.exports = async function verify(opts) {
62
+ const playwrightPort = opts.playwrightPort || "8931";
63
+ let allOk = true;
64
+
65
+ // ── 1. Python agents ──────────────────────────────────────────────────────
66
+ head("Python MCP Agents");
67
+ for (const name of AGENT_NAMES) {
68
+ const bin = findBinary(name);
69
+ if (bin) {
70
+ ok(`${name} ${C.dim}${bin}${C.reset}`);
71
+ } else {
72
+ fail(`${name} — not found`);
73
+ allOk = false;
74
+ }
75
+ }
76
+ if (!allOk) {
77
+ console.log(`\n ${C.yellow}Fix: pip install qa-gentic-stlc-agents${C.reset}`);
78
+ }
79
+
80
+ // ── 2. Playwright MCP ─────────────────────────────────────────────────────
81
+ head("Playwright MCP");
82
+ const pwReachable = await checkPort(playwrightPort);
83
+ if (pwReachable) {
84
+ ok(`Reachable on port ${playwrightPort}`);
85
+ } else {
86
+ warn(`Not reachable on port ${playwrightPort} ${C.dim}(start it with: npx @playwright/mcp@latest --port ${playwrightPort})${C.reset}`);
87
+ // Don't fail — it's optional until generation time
88
+ }
89
+
90
+ // ── 3. MSAL token cache ───────────────────────────────────────────────────
91
+ head("Azure DevOps Auth (MSAL)");
92
+ const msalCache = path.join(os.homedir(), ".msal-cache", "msal-cache.json");
93
+ if (fs.existsSync(msalCache)) {
94
+ const stat = fs.statSync(msalCache);
95
+ ok(`Token cache found ${C.dim}${msalCache} (${Math.round(stat.size / 1024)} KB)${C.reset}`);
96
+ } else {
97
+ warn(`No MSAL cache at ${msalCache}`);
98
+ warn(`A browser will open the first time an agent calls Azure DevOps.`);
99
+ warn(`Set AZURE_CLIENT_ID + AZURE_CLIENT_SECRET + AZURE_TENANT_ID for CI/CD.`);
100
+ }
101
+
102
+ // ── 4. MCP config files ───────────────────────────────────────────────────
103
+ head("MCP Config");
104
+ const mcpJson = path.join(CWD, ".mcp.json");
105
+ const vscodeMcp = path.join(CWD, ".vscode", "mcp.json");
106
+ if (fs.existsSync(mcpJson)) ok(`.mcp.json present`);
107
+ else warn(`.mcp.json missing — run: qa-stlc mcp-config`);
108
+ if (fs.existsSync(vscodeMcp)) ok(`.vscode/mcp.json present`);
109
+ else warn(`.vscode/mcp.json missing — run: qa-stlc mcp-config --vscode`);
110
+
111
+ // ── 5. Skills ─────────────────────────────────────────────────────────────
112
+ head("Skills");
113
+ const claudeSkills = path.join(CWD, ".claude", "skills");
114
+ const copilotSkills = path.join(CWD, ".github", "copilot-instructions");
115
+ if (fs.existsSync(claudeSkills)) {
116
+ const files = fs.readdirSync(claudeSkills).filter((f) => f.endsWith(".md"));
117
+ ok(`Claude Code skills (${files.length} files) ${C.dim}${claudeSkills}${C.reset}`);
118
+ } else {
119
+ warn(`No Claude Code skills — run: qa-stlc skills`);
120
+ }
121
+ if (fs.existsSync(copilotSkills)) {
122
+ const files = fs.readdirSync(copilotSkills).filter((f) => f.endsWith(".md"));
123
+ ok(`Copilot instructions (${files.length} files) ${C.dim}${copilotSkills}${C.reset}`);
124
+ } else {
125
+ warn(`No Copilot instructions — run: qa-stlc skills --target vscode`);
126
+ }
127
+
128
+ console.log();
129
+ };
File without changes