@qa-gentic/stlc-agents 1.0.29 → 1.0.30
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/bin/postinstall.js
CHANGED
|
@@ -2,27 +2,29 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* postinstall.js — Auto-install Python MCP servers after npm install -g
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Goals
|
|
6
|
+
* -----
|
|
7
|
+
* 1. Always finish with a working `qa-stlc-apply-cost` on PATH (or in a
|
|
8
|
+
* well-known location like ~/.local/bin), without ever surfacing raw
|
|
9
|
+
* pip / PEP 668 traceback output to the user's terminal.
|
|
10
|
+
* 2. Auto-install pipx when missing (brew on macOS, pip --user elsewhere).
|
|
11
|
+
* 3. Never run the cost-tracking patch when the Python package isn't
|
|
12
|
+
* actually installed — that produces a misleading ModuleNotFoundError.
|
|
7
13
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Interactive prompts (readline / TTY) require --foreground-scripts and are
|
|
12
|
-
* therefore NOT used here. The user is instructed to run `qa-stlc` in their
|
|
13
|
-
* project after install — that command IS interactive.
|
|
14
|
+
* npm 7+ swallows stdout/stderr from global lifecycle scripts, so all
|
|
15
|
+
* user-facing writes go straight to /dev/tty (with stderr as a fallback).
|
|
14
16
|
*/
|
|
15
17
|
"use strict";
|
|
16
18
|
|
|
17
|
-
// ── Open /dev/tty so writes go straight to the user's terminal ───────────────
|
|
18
|
-
// npm 7+ suppresses all stdout/stderr for global lifecycle scripts; /dev/tty
|
|
19
|
-
// bypasses that pipe entirely. Fall back to stderr on Windows / non-TTY CI.
|
|
20
19
|
const fs = require("fs");
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const os = require("os");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
const { spawnSync } = require("child_process");
|
|
23
|
+
const pkg = require("../package.json");
|
|
25
24
|
|
|
25
|
+
// ── Output: write to /dev/tty so npm 7+ doesn't swallow our messages ─────────
|
|
26
|
+
let _tty = null;
|
|
27
|
+
try { _tty = fs.openSync("/dev/tty", "w"); } catch (_) { /* Windows / CI */ }
|
|
26
28
|
const _write = (msg) => {
|
|
27
29
|
const line = msg + "\n";
|
|
28
30
|
if (_tty !== null) {
|
|
@@ -30,28 +32,49 @@ const _write = (msg) => {
|
|
|
30
32
|
}
|
|
31
33
|
process.stderr.write(line);
|
|
32
34
|
};
|
|
33
|
-
console.log
|
|
34
|
-
console.info = _write;
|
|
35
|
-
console.warn = _write;
|
|
36
|
-
console.error = _write;
|
|
37
|
-
|
|
38
|
-
const { spawnSync } = require("child_process");
|
|
39
|
-
const os = require("os");
|
|
40
|
-
const path = require("path");
|
|
41
|
-
const pkg = require("../package.json");
|
|
35
|
+
console.log = console.info = console.warn = console.error = _write;
|
|
42
36
|
|
|
43
37
|
const C = {
|
|
44
38
|
reset: "\x1b[0m", bold: "\x1b[1m",
|
|
45
39
|
green: "\x1b[32m", cyan: "\x1b[36m",
|
|
46
40
|
yellow: "\x1b[33m", dim: "\x1b[2m",
|
|
47
41
|
};
|
|
48
|
-
|
|
49
42
|
const b = (s) => `${C.bold}${s}${C.reset}`;
|
|
50
43
|
const ok = (s) => console.log(`${C.green}✓${C.reset} ${s}`);
|
|
51
44
|
const info = (s) => console.log(`${C.cyan}→${C.reset} ${s}`);
|
|
52
45
|
const warn = (s) => console.log(`${C.yellow}⚠${C.reset} ${s}`);
|
|
53
46
|
const d = (s) => `${C.dim}${s}${C.reset}`;
|
|
54
47
|
|
|
48
|
+
const ENTRY = "qa-stlc-apply-cost";
|
|
49
|
+
|
|
50
|
+
// Walk PATH plus the well-known pipx / pip --user dirs so we still find
|
|
51
|
+
// scripts even when the shell hasn't refreshed PATH since `pipx install`.
|
|
52
|
+
const findCommand = (cmd) => {
|
|
53
|
+
const exts = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
|
|
54
|
+
const dirs = [...(process.env.PATH || "").split(path.delimiter)];
|
|
55
|
+
dirs.push(path.join(os.homedir(), ".local", "bin"));
|
|
56
|
+
if (process.platform === "darwin") {
|
|
57
|
+
dirs.push("/opt/homebrew/bin", "/usr/local/bin");
|
|
58
|
+
}
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
if (!dir) continue;
|
|
61
|
+
for (const ext of exts) {
|
|
62
|
+
const p = path.join(dir, cmd + ext);
|
|
63
|
+
try { if (fs.existsSync(p)) return p; } catch (_) {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Fully silent spawn — captures stdout+stderr, returns combined output.
|
|
70
|
+
const trySpawn = (cmd, args) => {
|
|
71
|
+
const r = spawnSync(cmd, args, {
|
|
72
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
});
|
|
75
|
+
return { status: r.status, output: `${r.stdout || ""}${r.stderr || ""}` };
|
|
76
|
+
};
|
|
77
|
+
|
|
55
78
|
console.log(`
|
|
56
79
|
${b("QA STLC Agents")} v${pkg.version} — post-install
|
|
57
80
|
|
|
@@ -62,63 +85,133 @@ ${d("This npm package includes:")}
|
|
|
62
85
|
• Command-line tools: qa-stlc init, qa-stlc scaffold, qa-stlc skills, etc.
|
|
63
86
|
`);
|
|
64
87
|
|
|
65
|
-
// ── 1. Find Python
|
|
66
|
-
const pythonCandidates = ["python3", "python"];
|
|
88
|
+
// ── 1. Find Python ───────────────────────────────────────────────────────────
|
|
67
89
|
let python = null;
|
|
68
|
-
for (const candidate of
|
|
90
|
+
for (const candidate of ["python3", "python"]) {
|
|
69
91
|
const r = spawnSync(candidate, ["--version"], { encoding: "utf8" });
|
|
70
92
|
if (r.status === 0) { python = candidate; break; }
|
|
71
93
|
}
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
// Quick PEP 668 detector — short-circuits plain `pip install` on Homebrew /
|
|
96
|
+
// Debian Python so its traceback never reaches the terminal.
|
|
97
|
+
const isPep668 = (() => {
|
|
98
|
+
if (!python) return false;
|
|
99
|
+
const r = trySpawn(python, ["-m", "pip", "install", "--dry-run", "pip"]);
|
|
100
|
+
return /externally-managed-environment/i.test(r.output);
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
// ── 2. Make sure pipx is available (pipx handles PEP 668 cleanly) ────────────
|
|
104
|
+
const ensurePipx = () => {
|
|
105
|
+
if (findCommand("pipx")) return true;
|
|
106
|
+
|
|
107
|
+
// macOS — prefer brew (matches Homebrew Python's expectations)
|
|
108
|
+
if (process.platform === "darwin" && findCommand("brew")) {
|
|
109
|
+
info("Installing pipx via Homebrew…");
|
|
110
|
+
const r = trySpawn("brew", ["install", "pipx"]);
|
|
111
|
+
if (r.status === 0) {
|
|
112
|
+
trySpawn("pipx", ["ensurepath"]);
|
|
113
|
+
if (findCommand("pipx")) { ok("pipx installed."); return true; }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fallback — pip --user (works on Linux, Windows, and PEP 668 systems with the flag)
|
|
118
|
+
if (python) {
|
|
119
|
+
info("Installing pipx via pip --user…");
|
|
120
|
+
const args = ["-m", "pip", "install", "--user", "--quiet", "pipx"];
|
|
121
|
+
if (isPep668) args.splice(4, 0, "--break-system-packages");
|
|
122
|
+
const r = trySpawn(python, args);
|
|
123
|
+
if (r.status === 0) {
|
|
124
|
+
trySpawn(python, ["-m", "pipx", "ensurepath"]);
|
|
125
|
+
if (findCommand("pipx")) { ok("pipx installed."); return true; }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ── 3. Install qa-gentic-stlc-agents ─────────────────────────────────────────
|
|
132
|
+
let entryPath = findCommand(ENTRY);
|
|
133
|
+
let pipOk = entryPath !== null;
|
|
134
|
+
let viaTag = null;
|
|
135
|
+
|
|
136
|
+
if (pipOk) {
|
|
137
|
+
// Already installed — best-effort silent upgrade via pipx if it manages this.
|
|
138
|
+
if (findCommand("pipx")) {
|
|
139
|
+
const list = spawnSync("pipx", ["list", "--short"], { encoding: "utf8" });
|
|
140
|
+
if ((list.stdout || "").includes("qa-gentic-stlc-agents")) {
|
|
141
|
+
spawnSync("pipx", ["upgrade", "qa-gentic-stlc-agents"], { stdio: "ignore" });
|
|
142
|
+
entryPath = findCommand(ENTRY) || entryPath;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
ok("qa-gentic-stlc-agents already installed.");
|
|
77
146
|
} else {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
["
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
147
|
+
info("Installing qa-gentic-stlc-agents…");
|
|
148
|
+
|
|
149
|
+
// Attempt A — pipx (auto-installed above if missing)
|
|
150
|
+
if (ensurePipx()) {
|
|
151
|
+
const r = trySpawn("pipx", ["install", "--force", "--quiet", "qa-gentic-stlc-agents"]);
|
|
152
|
+
if (r.status === 0) { pipOk = true; viaTag = "pipx"; }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Attempt B — plain pip (only when NOT a PEP 668 environment)
|
|
156
|
+
if (!pipOk && python && !isPep668) {
|
|
157
|
+
const r = trySpawn(python, ["-m", "pip", "install", "--upgrade", "--quiet", "qa-gentic-stlc-agents"]);
|
|
158
|
+
if (r.status === 0) { pipOk = true; viaTag = "pip"; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Attempt C — pip --user --break-system-packages (PEP 668 fallback)
|
|
162
|
+
if (!pipOk && python) {
|
|
163
|
+
const r = trySpawn(python, [
|
|
164
|
+
"-m", "pip", "install",
|
|
165
|
+
"--user", "--break-system-packages",
|
|
166
|
+
"--upgrade", "--quiet", "qa-gentic-stlc-agents",
|
|
167
|
+
]);
|
|
168
|
+
if (r.status === 0) { pipOk = true; viaTag = "pip --user"; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (pipOk) {
|
|
172
|
+
ok(`qa-gentic-stlc-agents installed ${d(`(via ${viaTag})`)}`);
|
|
173
|
+
entryPath = findCommand(ENTRY);
|
|
87
174
|
} else {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
combined.split("\n").filter(Boolean).forEach((l) => console.log(d(l)));
|
|
92
|
-
warn("pip install failed.");
|
|
93
|
-
if (/externally-managed-environment/i.test(combined)) {
|
|
94
|
-
console.log("");
|
|
95
|
-
console.log(`${b("This Python is PEP 668-locked")} ${d("(Homebrew, Debian, Ubuntu).")} Install with one of:`);
|
|
96
|
-
console.log(` ${C.cyan}brew install pipx && pipx install qa-gentic-stlc-agents${C.reset} ${d("# isolated, commands on PATH")}`);
|
|
97
|
-
console.log(` ${C.cyan}python3 -m pip install --user --break-system-packages qa-gentic-stlc-agents${C.reset}`);
|
|
98
|
-
console.log(` ${C.cyan}python3 -m venv .venv && source .venv/bin/activate && pip install qa-gentic-stlc-agents${C.reset}`);
|
|
99
|
-
} else {
|
|
100
|
-
warn("Run manually: pip install qa-gentic-stlc-agents");
|
|
101
|
-
}
|
|
175
|
+
warn("Could not auto-install qa-gentic-stlc-agents. Run one of:");
|
|
176
|
+
console.log(` ${C.cyan}pipx install qa-gentic-stlc-agents${C.reset} ${d("# recommended")}`);
|
|
177
|
+
console.log(` ${C.cyan}python3 -m pip install --user --break-system-packages qa-gentic-stlc-agents${C.reset}`);
|
|
102
178
|
}
|
|
103
179
|
}
|
|
104
180
|
|
|
105
|
-
// ──
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
181
|
+
// ── 4. Activate cost tracking — only when the package is actually present ────
|
|
182
|
+
if (pipOk) {
|
|
183
|
+
info("Activating cost tracking on MCP servers…");
|
|
184
|
+
let costPatch;
|
|
185
|
+
if (entryPath) {
|
|
186
|
+
costPatch = spawnSync(entryPath, [], {
|
|
187
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
188
|
+
encoding: "utf8",
|
|
189
|
+
shell: process.platform === "win32",
|
|
190
|
+
});
|
|
191
|
+
} else if (findCommand("pipx")) {
|
|
192
|
+
// pipx-installed entry points may not yet be on this process's PATH —
|
|
193
|
+
// `pipx run` resolves them directly from pipx's venv.
|
|
194
|
+
costPatch = spawnSync("pipx", ["run", "--spec", "qa-gentic-stlc-agents", ENTRY], {
|
|
195
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
196
|
+
encoding: "utf8",
|
|
197
|
+
});
|
|
198
|
+
} else if (python) {
|
|
199
|
+
costPatch = spawnSync(python, ["-m", "stlc_agents.shared.install_hook"], {
|
|
200
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
201
|
+
encoding: "utf8",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (costPatch && costPatch.status === 0) {
|
|
206
|
+
(costPatch.stdout || "").split("\n").filter(Boolean).forEach((l) => console.log(l));
|
|
207
|
+
ok("Cost tracking active — every tool call will log tokens + cost");
|
|
208
|
+
} else {
|
|
209
|
+
// Silent — the patch is opt-in instrumentation and not load-bearing.
|
|
210
|
+
warn(`Cost tracking patch skipped — run later with: ${C.cyan}${ENTRY}${C.reset}`);
|
|
211
|
+
}
|
|
119
212
|
}
|
|
120
|
-
|
|
121
|
-
// ──
|
|
213
|
+
|
|
214
|
+
// ── 5. Next-step instructions ────────────────────────────────────────────────
|
|
122
215
|
console.log(`
|
|
123
216
|
${b("Next steps")} — run these inside your project root:
|
|
124
217
|
|
|
@@ -148,4 +241,4 @@ ${d("Docs: https://github.com/qa-gentic/stlc-agents")}
|
|
|
148
241
|
${d(" Tip: npm sometimes suppresses postinstall output for global packages.")}
|
|
149
242
|
${d(" If you missed these instructions, run qa-stlc in your project.")}
|
|
150
243
|
${d(" Or reinstall with: npm install -g --foreground-scripts @qa-gentic/stlc-agents")}
|
|
151
|
-
`);
|
|
244
|
+
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qa-gentic/stlc-agents",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"description": "QA STLC Agents — six MCP servers + skills for AI-powered test case, Gherkin, self-healing Playwright generation, Helix-QA file writing, and existing-framework migration against Azure DevOps and Jira Cloud. Includes an 8-strategy LocatorHealer with a live HealingDashboard (write-back-to-source) and a one-shot `stlc-migrate` CLI that converts an existing Playwright project into a fully agent-ready Helix-QA tree. Works with Claude Code, GitHub Copilot, Cursor, Windsurf.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"full_command_to_add_to_qa-stlc.js": "// ── cost ─────────────────────────────────────────────────────────────────\nprogram\n .command('cost')\n .description(\n 'Show token usage and cost for the current or past pipeline sessions.\\n' +\n 'Reads logs from ~/.qa-stlc/cost-*.jsonl written by the MCP servers.\\n' +\n 'Each MCP tool call logs tokens, cost, and latency automatically.'\n )\n .option('--all', 'Show all sessions (not just the last one)')\n .option('--session <id>', 'Show a specific session by its ID')\n .option('--json', 'Emit raw JSON instead of a formatted table')\n .action(cmdCost);",
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@qa-gentic/stlc-agents": "^1.0.
|
|
70
|
+
"@qa-gentic/stlc-agents": "^1.0.29",
|
|
71
71
|
"commander": "^12.0.0",
|
|
72
72
|
"which": "^4.0.0"
|
|
73
73
|
},
|
|
@@ -32,6 +32,7 @@ from mcp.server.stdio import stdio_server
|
|
|
32
32
|
from mcp import types
|
|
33
33
|
from stlc_agents.shared.auth import get_auth_headers, get_signed_in_user
|
|
34
34
|
from stlc_agents.agent_playwright_generator.tools.ado_attach import attach_file_to_work_item as _attach_file
|
|
35
|
+
from stlc_agents.shared.cost_tracker import track
|
|
35
36
|
|
|
36
37
|
load_dotenv()
|
|
37
38
|
app = Server("qa-playwright-generator")
|
|
@@ -424,6 +425,7 @@ async def list_tools() -> list[types.Tool]:
|
|
|
424
425
|
|
|
425
426
|
@app.call_tool()
|
|
426
427
|
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
428
|
+
t0 = time.monotonic()
|
|
427
429
|
try:
|
|
428
430
|
if name == "capture_app_context":
|
|
429
431
|
result = await asyncio.to_thread(
|
|
@@ -503,7 +505,7 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
503
505
|
"and retry. No files were attached to ADO."
|
|
504
506
|
),
|
|
505
507
|
}
|
|
506
|
-
return
|
|
508
|
+
return track(result, tool_name=name, server='qa-playwright-generator', t0=t0)
|
|
507
509
|
|
|
508
510
|
# ── Epic guard — attachments on Epics are invisible in ADO workflow views ──
|
|
509
511
|
wi_id = arguments["work_item_id"]
|
|
@@ -575,7 +577,7 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
575
577
|
)
|
|
576
578
|
else:
|
|
577
579
|
result = {"error": f"Unknown tool: {name}"}
|
|
578
|
-
return
|
|
580
|
+
return track(result, tool_name=name, server='qa-playwright-generator', t0=t0)
|
|
579
581
|
except Exception as exc:
|
|
580
582
|
return [types.TextContent(type="text", text=json.dumps({"error": str(exc), "tool": name}, indent=2))]
|
|
581
583
|
|