@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.
- package/README.md +310 -0
- package/bin/postinstall.js +78 -0
- package/bin/qa-stlc.js +89 -0
- package/package.json +48 -0
- package/skills/qa-stlc/AGENT-BEHAVIOR.md +383 -0
- package/skills/qa-stlc/deduplication-protocol.md +303 -0
- package/skills/qa-stlc/generate-gherkin.md +550 -0
- package/skills/qa-stlc/generate-playwright-code.md +464 -0
- package/skills/qa-stlc/generate-test-cases.md +176 -0
- package/skills/qa-stlc/write-helix-files.md +374 -0
- package/src/boilerplate-bundle.js +66 -0
- package/src/cmd-init.js +92 -0
- package/src/cmd-mcp-config.js +177 -0
- package/src/cmd-scaffold.js +130 -0
- package/src/cmd-skills.js +124 -0
- package/src/cmd-verify.js +129 -0
- package/src/stlc_agents/__init__.py +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__init__.py +0 -0
- package/src/stlc_agents/agent_gherkin_generator/server.py +502 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__init__.py +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/ado_gherkin.py +854 -0
- package/src/stlc_agents/agent_helix_writer/__init__.py +0 -0
- package/src/stlc_agents/agent_helix_writer/server.py +529 -0
- package/src/stlc_agents/agent_helix_writer/tools/__init__.py +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/boilerplate.py +70 -0
- package/src/stlc_agents/agent_helix_writer/tools/helix_write.py +796 -0
- package/src/stlc_agents/agent_playwright_generator/__init__.py +0 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +2610 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__init__.py +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/ado_attach.py +62 -0
- package/src/stlc_agents/agent_test_case_manager/__init__.py +0 -0
- package/src/stlc_agents/agent_test_case_manager/server.py +483 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__init__.py +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/ado_workitem.py +302 -0
- package/src/stlc_agents/shared/__init__.py +0 -0
- package/src/stlc_agents/shared/auth.py +119 -0
package/src/cmd-init.js
ADDED
|
@@ -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
|
|
File without changes
|