@qa-gentic/stlc-agents 1.0.5 → 1.0.7
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 +175 -34
- package/bin/postinstall.js +125 -44
- package/bin/qa-stlc.js +15 -8
- package/package.json +2 -2
- package/skills/{qa-stlc/AGENT-BEHAVIOR.md → AGENT-BEHAVIOR.md} +7 -6
- package/{.github/copilot-instructions/deduplication-protocol.md → skills/deduplication-protocol/SKILL.md} +16 -21
- package/skills/generate-gherkin/SKILL.md +287 -0
- package/skills/generate-gherkin/references/step-by-step.md +267 -0
- package/skills/{qa-stlc/generate-playwright-code.md → generate-playwright-code/SKILL.md} +13 -23
- package/{.github/copilot-instructions/generate-test-cases.md → skills/generate-test-cases/SKILL.md} +16 -2
- package/skills/qa-jira-manager/SKILL.md +287 -0
- package/{.github/copilot-instructions/write-helix-files.md → skills/write-helix-files/SKILL.md} +11 -17
- package/src/{boilerplate-bundle.js → cli/boilerplate-bundle.js} +8 -8
- package/src/cli/cmd-init.js +145 -0
- package/src/{cmd-mcp-config.js → cli/cmd-mcp-config.js} +72 -9
- package/src/cli/cmd-skills.js +209 -0
- package/src/{cmd-verify.js → cli/cmd-verify.js} +35 -3
- package/src/cli/prompt-integration.js +87 -0
- package/src/stlc_agents/agent_helix_writer/tools/boilerplate.py +8 -8
- package/src/stlc_agents/agent_jira_manager/__init__.py +0 -0
- package/src/stlc_agents/agent_jira_manager/server.py +500 -0
- package/src/stlc_agents/agent_jira_manager/tools/__init__.py +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/jira_workitem.py +467 -0
- package/src/stlc_agents/shared_jira/__init__.py +0 -0
- package/src/stlc_agents/shared_jira/auth.py +270 -0
- package/.github/copilot-instructions/AGENT-BEHAVIOR.md +0 -448
- package/.github/copilot-instructions/generate-gherkin.md +0 -550
- package/.github/copilot-instructions/generate-playwright-code.md +0 -464
- package/skills/qa-stlc/deduplication-protocol.md +0 -303
- package/skills/qa-stlc/generate-gherkin.md +0 -550
- package/skills/qa-stlc/generate-test-cases.md +0 -176
- package/skills/qa-stlc/write-helix-files.md +0 -374
- package/src/cmd-init.js +0 -92
- package/src/cmd-skills.js +0 -124
- /package/src/{cmd-scaffold.js → cli/cmd-scaffold.js} +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmd-init.js — `qa-stlc init`
|
|
3
|
+
*
|
|
4
|
+
* Full bootstrap: install Python agents + skills + MCP config.
|
|
5
|
+
* Accepts --integration <ado|jira|both>. When omitted, reads
|
|
6
|
+
* ~/.qa-stlc/integration (written by postinstall) or prompts interactively.
|
|
7
|
+
*/
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { execSync, spawnSync } = require("child_process");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const os = require("os");
|
|
14
|
+
|
|
15
|
+
const cmdSkills = require("./cmd-skills");
|
|
16
|
+
const cmdMcpConfig = require("./cmd-mcp-config");
|
|
17
|
+
const pickIntegration = require("./prompt-integration");
|
|
18
|
+
|
|
19
|
+
const PREF_FILE = path.join(os.homedir(), ".qa-stlc", "integration");
|
|
20
|
+
|
|
21
|
+
function readSavedIntegration() {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(PREF_FILE)) return fs.readFileSync(PREF_FILE, "utf8").trim();
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function saveIntegration(value) {
|
|
29
|
+
try {
|
|
30
|
+
fs.mkdirSync(path.dirname(PREF_FILE), { recursive: true });
|
|
31
|
+
fs.writeFileSync(PREF_FILE, value, "utf8");
|
|
32
|
+
} catch (_) {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const C = {
|
|
36
|
+
reset: "\x1b[0m", bold: "\x1b[1m",
|
|
37
|
+
green: "\x1b[32m", cyan: "\x1b[36m",
|
|
38
|
+
yellow: "\x1b[33m", red: "\x1b[31m", dim: "\x1b[2m",
|
|
39
|
+
};
|
|
40
|
+
const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
|
|
41
|
+
const info = (m) => console.log(`${C.cyan}→${C.reset} ${m}`);
|
|
42
|
+
const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
|
|
43
|
+
const die = (m) => { console.error(`${C.red}✗${C.reset} ${m}`); process.exit(1); };
|
|
44
|
+
|
|
45
|
+
module.exports = async function init(opts) {
|
|
46
|
+
console.log(`\n${C.bold}QA STLC Agents — init${C.reset}\n`);
|
|
47
|
+
|
|
48
|
+
// ── 1. Check Python ──────────────────────────────────────────────────────
|
|
49
|
+
const python = opts.python || "python3";
|
|
50
|
+
info(`Checking Python (${python})…`);
|
|
51
|
+
const pyCheck = spawnSync(python, ["--version"], { encoding: "utf8" });
|
|
52
|
+
if (pyCheck.status !== 0) {
|
|
53
|
+
die(`Python not found at '${python}'. Install Python 3.10+ or pass --python <path>.`);
|
|
54
|
+
}
|
|
55
|
+
const pyVersion = (pyCheck.stdout || pyCheck.stderr || "").trim();
|
|
56
|
+
const match = pyVersion.match(/Python (\d+)\.(\d+)/);
|
|
57
|
+
if (!match || parseInt(match[1]) < 3 || parseInt(match[2]) < 10) {
|
|
58
|
+
die(`Python 3.10+ required, found: ${pyVersion}`);
|
|
59
|
+
}
|
|
60
|
+
ok(`${pyVersion} found.`);
|
|
61
|
+
|
|
62
|
+
// ── 2. Resolve integration choice ────────────────────────────────────────
|
|
63
|
+
// Priority: --integration flag > saved preference > interactive prompt
|
|
64
|
+
let integration = (opts.integration || "").toLowerCase().trim();
|
|
65
|
+
const validIntegrations = ["ado", "jira", "both"];
|
|
66
|
+
if (integration && !validIntegrations.includes(integration)) {
|
|
67
|
+
die(`Invalid --integration value: "${integration}". Use ado, jira, or both.`);
|
|
68
|
+
}
|
|
69
|
+
if (!integration) {
|
|
70
|
+
integration = readSavedIntegration();
|
|
71
|
+
}
|
|
72
|
+
if (!integration) {
|
|
73
|
+
integration = await pickIntegration("ado");
|
|
74
|
+
}
|
|
75
|
+
saveIntegration(integration);
|
|
76
|
+
|
|
77
|
+
const needsAdo = integration === "ado" || integration === "both";
|
|
78
|
+
const needsJira = integration === "jira" || integration === "both";
|
|
79
|
+
info(`Integration: ${C.bold}${integration}${C.reset}`);
|
|
80
|
+
|
|
81
|
+
// ── 3. pip install qa-gentic-stlc-agents ──────────────────────────────────
|
|
82
|
+
info("Installing qa-gentic-stlc-agents (pip)…");
|
|
83
|
+
const pip = spawnSync(python, ["-m", "pip", "install", "qa-gentic-stlc-agents>=1.0.1", "--quiet"], {
|
|
84
|
+
stdio: "inherit",
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
});
|
|
87
|
+
if (pip.status !== 0) {
|
|
88
|
+
die("pip install failed. Run manually: pip install qa-gentic-stlc-agents");
|
|
89
|
+
}
|
|
90
|
+
ok("qa-gentic-stlc-agents installed.");
|
|
91
|
+
|
|
92
|
+
// ── 4. Install skills ─────────────────────────────────────────────────────
|
|
93
|
+
info("Installing skills…");
|
|
94
|
+
const skillTarget = opts.vscode ? "both" : "claude";
|
|
95
|
+
await cmdSkills({ target: skillTarget, integration });
|
|
96
|
+
|
|
97
|
+
// ── 5. Write MCP config ───────────────────────────────────────────────────
|
|
98
|
+
info("Writing MCP config…");
|
|
99
|
+
await cmdMcpConfig({
|
|
100
|
+
vscode: opts.vscode || false,
|
|
101
|
+
print: false,
|
|
102
|
+
python: python,
|
|
103
|
+
playwrightPort: "8931",
|
|
104
|
+
integration,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── 6. Done ───────────────────────────────────────────────────────────────
|
|
108
|
+
const mcpLocation = opts.vscode ? ".vscode/mcp.json" : ".mcp.json";
|
|
109
|
+
const skillsLocation = opts.vscode
|
|
110
|
+
? ".github/copilot-instructions/ (reload VS Code window to activate)"
|
|
111
|
+
: ".claude/skills/";
|
|
112
|
+
|
|
113
|
+
const adoSection = needsAdo ? `
|
|
114
|
+
${C.dim}ADO pipeline:${C.reset}
|
|
115
|
+
${C.dim}1 →${C.reset} ${C.yellow}qa-test-case-manager${C.reset} ${C.dim}ADO work item → manual test cases${C.reset}
|
|
116
|
+
${C.dim}2 →${C.reset} ${C.yellow}qa-gherkin-generator${C.reset} ${C.dim}Epic / Feature / PBI → .feature file${C.reset}
|
|
117
|
+
${C.dim}3 →${C.reset} ${C.yellow}qa-playwright-generator${C.reset} ${C.dim}Gherkin + live browser → self-healing Playwright${C.reset}
|
|
118
|
+
${C.dim}4 →${C.reset} ${C.yellow}qa-helix-writer${C.reset} ${C.dim}Generated files → Helix-QA project${C.reset}` : "";
|
|
119
|
+
|
|
120
|
+
const jiraSection = needsJira ? `
|
|
121
|
+
${C.dim}Jira pipeline:${C.reset}
|
|
122
|
+
${C.dim}1 →${C.reset} ${C.yellow}qa-jira-manager${C.reset} ${C.dim}Fetch Jira issue + analyse acceptance criteria${C.reset}
|
|
123
|
+
${C.dim}2 →${C.reset} ${C.yellow}qa-jira-manager${C.reset} ${C.dim}Generate test cases → create in Jira with 'is tested by' links${C.reset}
|
|
124
|
+
${C.dim}3 →${C.reset} ${C.yellow}qa-gherkin-generator${C.reset} ${C.dim}Generate Gherkin .feature file from Jira issue AC${C.reset}
|
|
125
|
+
${C.dim}4 →${C.reset} ${C.yellow}qa-playwright-generator${C.reset} ${C.dim}Gherkin + live browser → self-healing Playwright TypeScript${C.reset}
|
|
126
|
+
${C.dim}5 →${C.reset} ${C.yellow}qa-helix-writer${C.reset} ${C.dim}Generated files → Helix-QA project on disk${C.reset}
|
|
127
|
+
${C.dim} Requires: JIRA_CLIENT_ID + JIRA_CLIENT_SECRET + JIRA_CLOUD_ID in .env${C.reset}` : "";
|
|
128
|
+
|
|
129
|
+
console.log(`
|
|
130
|
+
${C.bold}Setup complete.${C.reset}
|
|
131
|
+
|
|
132
|
+
${C.dim}Integration:${C.reset} ${C.bold}${integration}${C.reset}
|
|
133
|
+
${C.dim}MCP config :${C.reset} ${mcpLocation}
|
|
134
|
+
${C.dim}Skills :${C.reset} ${skillsLocation}
|
|
135
|
+
|
|
136
|
+
${C.bold}Start Playwright MCP${C.reset} ${C.dim}(keep running in a separate terminal):${C.reset}
|
|
137
|
+
|
|
138
|
+
${C.cyan}npx @playwright/mcp@latest --port 8931${C.reset}
|
|
139
|
+
|
|
140
|
+
${C.bold}STLC Workflow:${C.reset}
|
|
141
|
+
${adoSection}${jiraSection}
|
|
142
|
+
|
|
143
|
+
${C.dim}Change integration at any time: qa-stlc init --integration <ado|jira|both>${C.reset}
|
|
144
|
+
`);
|
|
145
|
+
};
|
|
@@ -18,6 +18,19 @@ const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
|
|
|
18
18
|
const info = (m) => console.log(`${C.cyan}→${C.reset} ${m}`);
|
|
19
19
|
const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
|
|
20
20
|
|
|
21
|
+
const os = require("os");
|
|
22
|
+
const fs_ = fs; // alias — fs already required above
|
|
23
|
+
|
|
24
|
+
const PREF_FILE_MCP = require("path").join(os.homedir(), ".qa-stlc", "integration");
|
|
25
|
+
|
|
26
|
+
function readIntegrationPref() {
|
|
27
|
+
try {
|
|
28
|
+
if (fs_.existsSync(PREF_FILE_MCP))
|
|
29
|
+
return fs_.readFileSync(PREF_FILE_MCP, "utf8").trim();
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
return "ado";
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
const CWD = process.cwd();
|
|
22
35
|
const IS_WIN = process.platform === "win32";
|
|
23
36
|
const EXT = IS_WIN ? ".exe" : "";
|
|
@@ -29,6 +42,14 @@ const AGENT_NAMES = [
|
|
|
29
42
|
"qa-helix-writer",
|
|
30
43
|
];
|
|
31
44
|
|
|
45
|
+
// Jira agent — added separately because it needs env var injection
|
|
46
|
+
const JIRA_AGENT_NAME = "qa-jira-manager";
|
|
47
|
+
const JIRA_ENV_VARS = {
|
|
48
|
+
JIRA_CLIENT_ID: "${env:JIRA_CLIENT_ID}",
|
|
49
|
+
JIRA_CLIENT_SECRET: "${env:JIRA_CLIENT_SECRET}",
|
|
50
|
+
JIRA_CLOUD_ID: "${env:JIRA_CLOUD_ID}",
|
|
51
|
+
};
|
|
52
|
+
|
|
32
53
|
/**
|
|
33
54
|
* Locate the binary for an agent.
|
|
34
55
|
* Priority: .venv → --python dir → system PATH → python sys.executable dir → macOS framework dirs
|
|
@@ -76,11 +97,18 @@ function findBinary(name, pythonBin) {
|
|
|
76
97
|
return null;
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
function buildClaudeConfig(pythonBin, playwrightPort) {
|
|
100
|
+
function buildClaudeConfig(pythonBin, playwrightPort, integration) {
|
|
80
101
|
const servers = {};
|
|
81
102
|
const missing = [];
|
|
103
|
+
const needsAdo = !integration || integration === "ado" || integration === "both";
|
|
104
|
+
const needsJira = integration === "jira" || integration === "both";
|
|
82
105
|
|
|
83
|
-
for
|
|
106
|
+
// ADO agents — always included for ado/both; also included for jira because
|
|
107
|
+
// qa-gherkin-generator, qa-playwright-generator, and qa-helix-writer are shared
|
|
108
|
+
// by the Jira pipeline (Gherkin → Playwright → Helix steps run the same agents).
|
|
109
|
+
const activeNames = (needsAdo || needsJira) ? AGENT_NAMES : [];
|
|
110
|
+
|
|
111
|
+
for (const name of activeNames) {
|
|
84
112
|
const bin = findBinary(name, pythonBin);
|
|
85
113
|
if (bin) {
|
|
86
114
|
servers[name] = { command: bin };
|
|
@@ -90,6 +118,21 @@ function buildClaudeConfig(pythonBin, playwrightPort) {
|
|
|
90
118
|
}
|
|
91
119
|
}
|
|
92
120
|
|
|
121
|
+
// Jira agent — only when integration includes jira
|
|
122
|
+
if (needsJira) {
|
|
123
|
+
const jiraBin = findBinary(JIRA_AGENT_NAME, pythonBin);
|
|
124
|
+
if (jiraBin) {
|
|
125
|
+
servers[JIRA_AGENT_NAME] = { command: jiraBin, env: JIRA_ENV_VARS };
|
|
126
|
+
} else {
|
|
127
|
+
missing.push(JIRA_AGENT_NAME);
|
|
128
|
+
servers[JIRA_AGENT_NAME] = {
|
|
129
|
+
command: `/path/to/.venv/bin/${JIRA_AGENT_NAME}`,
|
|
130
|
+
env: JIRA_ENV_VARS,
|
|
131
|
+
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
93
136
|
servers["playwright"] = {
|
|
94
137
|
type: "url",
|
|
95
138
|
url: `ws://localhost:${playwrightPort}`,
|
|
@@ -98,11 +141,15 @@ function buildClaudeConfig(pythonBin, playwrightPort) {
|
|
|
98
141
|
return { config: { mcpServers: servers }, missing };
|
|
99
142
|
}
|
|
100
143
|
|
|
101
|
-
function buildVscodeConfig(pythonBin, playwrightPort) {
|
|
144
|
+
function buildVscodeConfig(pythonBin, playwrightPort, integration) {
|
|
102
145
|
const servers = {};
|
|
103
146
|
const missing = [];
|
|
147
|
+
const needsAdo = !integration || integration === "ado" || integration === "both";
|
|
148
|
+
const needsJira = integration === "jira" || integration === "both";
|
|
149
|
+
// Shared agents (Gherkin, Playwright, Helix) are needed by both ADO and Jira pipelines.
|
|
150
|
+
const activeNames = (needsAdo || needsJira) ? AGENT_NAMES : [];
|
|
104
151
|
|
|
105
|
-
for (const name of
|
|
152
|
+
for (const name of activeNames) {
|
|
106
153
|
const bin = findBinary(name, pythonBin);
|
|
107
154
|
if (bin) {
|
|
108
155
|
servers[name] = { command: bin };
|
|
@@ -112,6 +159,21 @@ function buildVscodeConfig(pythonBin, playwrightPort) {
|
|
|
112
159
|
}
|
|
113
160
|
}
|
|
114
161
|
|
|
162
|
+
// Jira agent — only when integration includes jira
|
|
163
|
+
if (needsJira) {
|
|
164
|
+
const jiraBinV = findBinary(JIRA_AGENT_NAME, pythonBin);
|
|
165
|
+
if (jiraBinV) {
|
|
166
|
+
servers[JIRA_AGENT_NAME] = { command: jiraBinV, env: JIRA_ENV_VARS };
|
|
167
|
+
} else {
|
|
168
|
+
missing.push(JIRA_AGENT_NAME);
|
|
169
|
+
servers[JIRA_AGENT_NAME] = {
|
|
170
|
+
command: `/path/to/.venv/bin/${JIRA_AGENT_NAME}`,
|
|
171
|
+
env: JIRA_ENV_VARS,
|
|
172
|
+
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
115
177
|
servers["playwright"] = {
|
|
116
178
|
type: "http",
|
|
117
179
|
url: `http://localhost:${playwrightPort}/mcp`,
|
|
@@ -139,10 +201,11 @@ module.exports = async function mcpConfig(opts) {
|
|
|
139
201
|
const printOnly = opts.print || false;
|
|
140
202
|
const pythonBin = opts.python || "python3";
|
|
141
203
|
const playwrightPort = opts.playwrightPort || "8931";
|
|
204
|
+
const integration = (opts.integration || readIntegrationPref() || "ado").toLowerCase();
|
|
142
205
|
|
|
143
206
|
if (printOnly) {
|
|
144
|
-
const { config: claudeCfg } = buildClaudeConfig(pythonBin, playwrightPort);
|
|
145
|
-
const { config: vscodeCfg } = buildVscodeConfig(pythonBin, playwrightPort);
|
|
207
|
+
const { config: claudeCfg } = buildClaudeConfig(pythonBin, playwrightPort, integration);
|
|
208
|
+
const { config: vscodeCfg } = buildVscodeConfig(pythonBin, playwrightPort, integration);
|
|
146
209
|
console.log("\n=== Claude Code (.mcp.json) ===");
|
|
147
210
|
console.log(JSON.stringify(claudeCfg, null, 2));
|
|
148
211
|
console.log("\n=== VS Code (.vscode/mcp.json) ===");
|
|
@@ -152,7 +215,7 @@ module.exports = async function mcpConfig(opts) {
|
|
|
152
215
|
}
|
|
153
216
|
|
|
154
217
|
if (useVscode) {
|
|
155
|
-
const { config, missing } = buildVscodeConfig(pythonBin, playwrightPort);
|
|
218
|
+
const { config, missing } = buildVscodeConfig(pythonBin, playwrightPort, integration);
|
|
156
219
|
const dir = path.join(CWD, ".vscode");
|
|
157
220
|
fs.mkdirSync(dir, { recursive: true });
|
|
158
221
|
const out = path.join(dir, "mcp.json");
|
|
@@ -164,7 +227,7 @@ module.exports = async function mcpConfig(opts) {
|
|
|
164
227
|
}
|
|
165
228
|
printNextSteps("vscode", playwrightPort);
|
|
166
229
|
} else {
|
|
167
|
-
const { config, missing } = buildClaudeConfig(pythonBin, playwrightPort);
|
|
230
|
+
const { config, missing } = buildClaudeConfig(pythonBin, playwrightPort, integration);
|
|
168
231
|
const out = path.join(CWD, ".mcp.json");
|
|
169
232
|
fs.writeFileSync(out, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
170
233
|
ok(`Written → .mcp.json`);
|
|
@@ -174,4 +237,4 @@ module.exports = async function mcpConfig(opts) {
|
|
|
174
237
|
}
|
|
175
238
|
printNextSteps("claude", playwrightPort);
|
|
176
239
|
}
|
|
177
|
-
};
|
|
240
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
const os = require("os");
|
|
27
|
+
const PREF_FILE_SK = require("path").join(os.homedir(), ".qa-stlc", "integration");
|
|
28
|
+
|
|
29
|
+
function readIntegrationPrefSk() {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(PREF_FILE_SK))
|
|
32
|
+
return fs.readFileSync(PREF_FILE_SK, "utf8").trim();
|
|
33
|
+
} catch (_) {}
|
|
34
|
+
return "ado";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve the skills bundled with this npm package
|
|
38
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
39
|
+
const SKILLS_DIR = path.join(PKG_ROOT, "skills");
|
|
40
|
+
const BEHAVIOR_MD = path.join(SKILLS_DIR, "AGENT-BEHAVIOR.md");
|
|
41
|
+
const AGENTS_DIR = path.join(PKG_ROOT, ".github", "agents");
|
|
42
|
+
|
|
43
|
+
/** Copy a file, creating parent dirs as needed. */
|
|
44
|
+
function cp(src, dest) {
|
|
45
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
46
|
+
fs.copyFileSync(src, dest);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* List skill directories (each containing a SKILL.md), filtered by integration.
|
|
51
|
+
* Returns objects: { name, skillMd, dir }
|
|
52
|
+
*/
|
|
53
|
+
function skillEntries(integration) {
|
|
54
|
+
const integ = integration || readIntegrationPrefSk() || "ado";
|
|
55
|
+
const includeJira = integ === "jira" || integ === "both";
|
|
56
|
+
const adoOnlySkills = new Set(["generate-test-cases"]);
|
|
57
|
+
return fs.readdirSync(SKILLS_DIR)
|
|
58
|
+
.filter((entry) => {
|
|
59
|
+
// Only directories with a SKILL.md
|
|
60
|
+
const skillMd = path.join(SKILLS_DIR, entry, "SKILL.md");
|
|
61
|
+
if (!fs.existsSync(skillMd)) return false;
|
|
62
|
+
// Jira skill — only when integration includes jira
|
|
63
|
+
if (entry === "qa-jira-manager" && !includeJira) return false;
|
|
64
|
+
// ADO-only skills — exclude when integration is jira-only
|
|
65
|
+
if (adoOnlySkills.has(entry) && integ === "jira") return false;
|
|
66
|
+
return true;
|
|
67
|
+
})
|
|
68
|
+
.map((entry) => ({
|
|
69
|
+
name: entry,
|
|
70
|
+
skillMd: path.join(SKILLS_DIR, entry, "SKILL.md"),
|
|
71
|
+
dir: path.join(SKILLS_DIR, entry),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Back-compat: returns array of SKILL.md paths (used for print/count). */
|
|
76
|
+
function skillFiles(integration) {
|
|
77
|
+
return skillEntries(integration).map((e) => e.skillMd);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const CWD = process.cwd();
|
|
81
|
+
|
|
82
|
+
/** List .agent.md files, filtered by integration. */
|
|
83
|
+
function agentFiles(integration) {
|
|
84
|
+
const integ = integration || readIntegrationPrefSk() || "ado";
|
|
85
|
+
const includeJira = integ === "jira" || integ === "both";
|
|
86
|
+
const adoOnlyAgents = new Set(["qa-test-case-manager.agent.md"]);
|
|
87
|
+
return fs.readdirSync(AGENTS_DIR)
|
|
88
|
+
.filter((f) => {
|
|
89
|
+
if (!f.endsWith(".agent.md")) return false;
|
|
90
|
+
if (f === "qa-jira-manager.agent.md" && !includeJira) return false;
|
|
91
|
+
if (adoOnlyAgents.has(f) && integ === "jira") return false;
|
|
92
|
+
return true;
|
|
93
|
+
})
|
|
94
|
+
.map((f) => path.join(AGENTS_DIR, f));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Install .agent.md files to .github/agents/ in the consumer project. */
|
|
98
|
+
function installAgents(integration) {
|
|
99
|
+
if (!fs.existsSync(AGENTS_DIR)) return;
|
|
100
|
+
const dest = path.join(CWD, ".github", "agents");
|
|
101
|
+
for (const src of agentFiles(integration)) {
|
|
102
|
+
cp(src, path.join(dest, path.basename(src)));
|
|
103
|
+
}
|
|
104
|
+
ok(`Agent files installed → .github/agents/`);
|
|
105
|
+
agentFiles(integration).forEach((f) => info(path.basename(f)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function installClaude(integration) {
|
|
109
|
+
const dest = path.join(CWD, ".claude", "skills");
|
|
110
|
+
// Copy entire skill directory (preserves references/ subdirectory)
|
|
111
|
+
for (const entry of skillEntries(integration)) {
|
|
112
|
+
const targetDir = path.join(dest, entry.name);
|
|
113
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
114
|
+
copyDirRecursive(entry.dir, targetDir);
|
|
115
|
+
}
|
|
116
|
+
cp(BEHAVIOR_MD, path.join(CWD, ".claude", "AGENT-BEHAVIOR.md"));
|
|
117
|
+
ok(`Skills installed → .claude/skills/`);
|
|
118
|
+
info("AGENT-BEHAVIOR.md → .claude/AGENT-BEHAVIOR.md");
|
|
119
|
+
skillEntries(integration).forEach((e) => info(`${e.name}/SKILL.md`));
|
|
120
|
+
installAgents(integration);
|
|
121
|
+
printPlaywrightHint();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function installVscode(integration) {
|
|
125
|
+
const dest = path.join(CWD, ".github", "copilot-instructions");
|
|
126
|
+
// Flat copy: SKILL.md → <name>.md
|
|
127
|
+
for (const entry of skillEntries(integration)) {
|
|
128
|
+
cp(entry.skillMd, path.join(dest, `${entry.name}.md`));
|
|
129
|
+
}
|
|
130
|
+
cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
|
|
131
|
+
ok(`Skills installed → .github/copilot-instructions/`);
|
|
132
|
+
info("Add to .github/copilot-instructions.md:");
|
|
133
|
+
info(" @.github/copilot-instructions/AGENT-BEHAVIOR.md");
|
|
134
|
+
installAgents(integration);
|
|
135
|
+
printPlaywrightHint();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function installCursor(integration) {
|
|
139
|
+
const dest = path.join(CWD, ".cursor", "rules");
|
|
140
|
+
// Flat copy: SKILL.md → <name>.md
|
|
141
|
+
for (const entry of skillEntries(integration)) {
|
|
142
|
+
cp(entry.skillMd, path.join(dest, `${entry.name}.md`));
|
|
143
|
+
}
|
|
144
|
+
cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
|
|
145
|
+
ok(`Skills installed → .cursor/rules/`);
|
|
146
|
+
printPlaywrightHint();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function installWindsurf(integration) {
|
|
150
|
+
const dest = path.join(CWD, ".windsurf", "rules");
|
|
151
|
+
// Flat copy: SKILL.md → <name>.md
|
|
152
|
+
for (const entry of skillEntries(integration)) {
|
|
153
|
+
cp(entry.skillMd, path.join(dest, `${entry.name}.md`));
|
|
154
|
+
}
|
|
155
|
+
cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
|
|
156
|
+
ok(`Skills installed → .windsurf/rules/`);
|
|
157
|
+
printPlaywrightHint();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function printSkills() {
|
|
161
|
+
console.log("\nAvailable skills:\n");
|
|
162
|
+
skillEntries().forEach((e) => console.log(` ${e.name}/SKILL.md`));
|
|
163
|
+
console.log(` AGENT-BEHAVIOR.md`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Recursively copy a directory tree. */
|
|
167
|
+
function copyDirRecursive(src, dest) {
|
|
168
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
169
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
170
|
+
const srcPath = path.join(src, entry.name);
|
|
171
|
+
const destPath = path.join(dest, entry.name);
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
copyDirRecursive(srcPath, destPath);
|
|
174
|
+
} else {
|
|
175
|
+
fs.copyFileSync(srcPath, destPath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function printPlaywrightHint() {
|
|
181
|
+
console.log(`
|
|
182
|
+
${C.dim}Start Playwright MCP before running generation workflows:${C.reset}
|
|
183
|
+
npx @playwright/mcp@latest --port 8931
|
|
184
|
+
`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = async function skills(opts) {
|
|
188
|
+
const target = (opts.target || "claude").toLowerCase();
|
|
189
|
+
|
|
190
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
191
|
+
console.error(`Skills directory not found: ${SKILLS_DIR}`);
|
|
192
|
+
console.error("Try reinstalling: npm install -g @qa-stlc/agents");
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const integ = opts.integration || readIntegrationPrefSk() || "ado";
|
|
197
|
+
switch (target) {
|
|
198
|
+
case "claude": installClaude(integ); break;
|
|
199
|
+
case "vscode": installVscode(integ); break;
|
|
200
|
+
case "cursor": installCursor(integ); break;
|
|
201
|
+
case "windsurf": installWindsurf(integ); break;
|
|
202
|
+
case "both": installClaude(integ); installVscode(integ); break;
|
|
203
|
+
case "print": printSkills(); break;
|
|
204
|
+
default:
|
|
205
|
+
warn(`Unknown target: ${target}`);
|
|
206
|
+
warn("Valid targets: claude, vscode, cursor, windsurf, both, print");
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
@@ -24,6 +24,16 @@ const fail = (m) => console.log(` ${C.red}✗${C.reset} ${m}`);
|
|
|
24
24
|
const warn = (m) => console.log(` ${C.yellow}⚠${C.reset} ${m}`);
|
|
25
25
|
const head = (m) => console.log(`\n${C.bold}${m}${C.reset}`);
|
|
26
26
|
|
|
27
|
+
const PREF_FILE_V = require("path").join(os.homedir(), ".qa-stlc", "integration");
|
|
28
|
+
|
|
29
|
+
function readIntegrationPrefV() {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(PREF_FILE_V))
|
|
32
|
+
return fs.readFileSync(PREF_FILE_V, "utf8").trim();
|
|
33
|
+
} catch (_) {}
|
|
34
|
+
return "ado";
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
const CWD = process.cwd();
|
|
28
38
|
const IS_WIN = process.platform === "win32";
|
|
29
39
|
const EXT = IS_WIN ? ".exe" : "";
|
|
@@ -33,6 +43,7 @@ const AGENT_NAMES = [
|
|
|
33
43
|
"qa-gherkin-generator",
|
|
34
44
|
"qa-playwright-generator",
|
|
35
45
|
"qa-helix-writer",
|
|
46
|
+
"qa-jira-manager",
|
|
36
47
|
];
|
|
37
48
|
|
|
38
49
|
function findBinary(name) {
|
|
@@ -63,8 +74,17 @@ module.exports = async function verify(opts) {
|
|
|
63
74
|
let allOk = true;
|
|
64
75
|
|
|
65
76
|
// ── 1. Python agents ──────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
const integration = (opts.integration || readIntegrationPrefV() || "ado").toLowerCase();
|
|
78
|
+
const needsAdo = integration === "ado" || integration === "both";
|
|
79
|
+
const needsJira = integration === "jira" || integration === "both";
|
|
80
|
+
// Shared agents (Gherkin, Playwright, Helix) are required by both ADO and Jira pipelines.
|
|
81
|
+
const activeNames = [
|
|
82
|
+
...((needsAdo || needsJira) ? AGENT_NAMES : []),
|
|
83
|
+
...(needsJira ? ["qa-jira-manager"] : []),
|
|
84
|
+
].filter((v, i, a) => a.indexOf(v) === i); // deduplicate for 'both' case
|
|
85
|
+
|
|
86
|
+
head(`Python MCP Agents (integration: ${C.bold}${integration}${C.reset})`);
|
|
87
|
+
for (const name of activeNames) {
|
|
68
88
|
const bin = findBinary(name);
|
|
69
89
|
if (bin) {
|
|
70
90
|
ok(`${name} ${C.dim}${bin}${C.reset}`);
|
|
@@ -99,6 +119,18 @@ module.exports = async function verify(opts) {
|
|
|
99
119
|
warn(`Set AZURE_CLIENT_ID + AZURE_CLIENT_SECRET + AZURE_TENANT_ID for CI/CD.`);
|
|
100
120
|
}
|
|
101
121
|
|
|
122
|
+
// ── 3b. Jira auth cache ──────────────────────────────────────────────────────
|
|
123
|
+
head("Jira Auth (OAuth 2.0)");
|
|
124
|
+
const jiraCache = path.join(os.homedir(), ".jira-cache", "jira-token.json");
|
|
125
|
+
if (fs.existsSync(jiraCache)) {
|
|
126
|
+
const stat = fs.statSync(jiraCache);
|
|
127
|
+
ok(`Token cache found ${C.dim}${jiraCache} (${Math.round(stat.size / 1024)} KB)${C.reset}`);
|
|
128
|
+
} else {
|
|
129
|
+
warn(`No Jira token cache at ${jiraCache}`);
|
|
130
|
+
warn(`A browser will open the first time qa-jira-manager is called.`);
|
|
131
|
+
warn(`Set JIRA_CLIENT_ID + JIRA_CLIENT_SECRET + JIRA_CLOUD_ID in .env`);
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
// ── 4. MCP config files ───────────────────────────────────────────────────
|
|
103
135
|
head("MCP Config");
|
|
104
136
|
const mcpJson = path.join(CWD, ".mcp.json");
|
|
@@ -126,4 +158,4 @@ module.exports = async function verify(opts) {
|
|
|
126
158
|
}
|
|
127
159
|
|
|
128
160
|
console.log();
|
|
129
|
-
};
|
|
161
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-integration.js — Interactive integration picker
|
|
3
|
+
*
|
|
4
|
+
* Presents a numbered menu asking the user which integration(s) they want:
|
|
5
|
+
* 1. Azure DevOps only (Agents 1–4)
|
|
6
|
+
* 2. Jira Cloud only (Full pipeline: qa-jira-manager + shared Agents 2–4)
|
|
7
|
+
* 3. Both
|
|
8
|
+
*
|
|
9
|
+
* Works in interactive terminals (TTY). Falls back to a default (ado) when
|
|
10
|
+
* stdin is not a TTY (e.g. CI, piped installs) — callers can override the
|
|
11
|
+
* default via the `fallback` argument.
|
|
12
|
+
*
|
|
13
|
+
* Returns a Promise that resolves to: "ado" | "jira" | "both"
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const pickIntegration = require("./prompt-integration");
|
|
17
|
+
* const integration = await pickIntegration(); // "ado" | "jira" | "both"
|
|
18
|
+
*/
|
|
19
|
+
"use strict";
|
|
20
|
+
|
|
21
|
+
const readline = require("readline");
|
|
22
|
+
|
|
23
|
+
const C = {
|
|
24
|
+
reset: "\x1b[0m", bold: "\x1b[1m",
|
|
25
|
+
green: "\x1b[32m", cyan: "\x1b[36m",
|
|
26
|
+
yellow: "\x1b[33m", dim: "\x1b[2m", blue: "\x1b[34m",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const MENU = `
|
|
30
|
+
${C.bold}Which project management integration do you need?${C.reset}
|
|
31
|
+
|
|
32
|
+
${C.cyan}1${C.reset} ${C.bold}Azure DevOps${C.reset} ${C.dim}(Agents 1–4: test cases, Gherkin, Playwright, Helix-QA)${C.reset}
|
|
33
|
+
${C.cyan}2${C.reset} ${C.bold}Jira Cloud${C.reset} ${C.dim}(Full pipeline: fetch issues → test cases → Gherkin → Playwright → Helix-QA)${C.reset}
|
|
34
|
+
${C.cyan}3${C.reset} ${C.bold}Both${C.reset} ${C.dim}(All five agents — use ADO and Jira side by side)${C.reset}
|
|
35
|
+
|
|
36
|
+
${C.dim}Tip: you can change this later with qa-stlc init --integration <ado|jira|both>${C.reset}
|
|
37
|
+
|
|
38
|
+
Enter 1, 2, or 3 [default: 1]: `;
|
|
39
|
+
|
|
40
|
+
const CHOICES = { "1": "ado", "2": "jira", "3": "both", "": "ado" };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} [fallback="ado"] - Value to return when stdin is not a TTY.
|
|
44
|
+
* @returns {Promise<"ado"|"jira"|"both">}
|
|
45
|
+
*/
|
|
46
|
+
module.exports = function pickIntegration(fallback = "ado") {
|
|
47
|
+
// Non-interactive environment — skip the prompt entirely
|
|
48
|
+
if (!process.stdin.isTTY) {
|
|
49
|
+
console.log(
|
|
50
|
+
`${C.dim}→ Non-interactive install — defaulting to integration: ${C.reset}${C.bold}${fallback}${C.reset}`
|
|
51
|
+
);
|
|
52
|
+
console.log(
|
|
53
|
+
`${C.dim} To change, run: qa-stlc init --integration <ado|jira|both>${C.reset}\n`
|
|
54
|
+
);
|
|
55
|
+
return Promise.resolve(fallback);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const rl = readline.createInterface({
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function ask() {
|
|
65
|
+
rl.question(MENU, (answer) => {
|
|
66
|
+
const trimmed = answer.trim();
|
|
67
|
+
const choice = CHOICES[trimmed];
|
|
68
|
+
|
|
69
|
+
if (choice) {
|
|
70
|
+
rl.close();
|
|
71
|
+
const label = choice === "ado" ? "Azure DevOps"
|
|
72
|
+
: choice === "jira" ? "Jira Cloud"
|
|
73
|
+
: "Azure DevOps + Jira Cloud";
|
|
74
|
+
console.log(`\n${C.green}✓${C.reset} Integration selected: ${C.bold}${label}${C.reset}\n`);
|
|
75
|
+
resolve(choice);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(
|
|
78
|
+
`${C.yellow} Please enter 1, 2, or 3.${C.reset}`
|
|
79
|
+
);
|
|
80
|
+
ask();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ask();
|
|
86
|
+
});
|
|
87
|
+
};
|