@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.
@@ -2,27 +2,29 @@
2
2
  /**
3
3
  * postinstall.js — Auto-install Python MCP servers after npm install -g
4
4
  *
5
- * npm 7+ captures all stdout/stderr from lifecycle scripts for global installs,
6
- * so normal process.stdout / process.stderr writes are swallowed.
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
- * Fix: write directly to /dev/tty (Unix) which bypasses npm's pipe entirely.
9
- * Falls back to process.stderr (visible for local installs / Windows CI).
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
- let _tty = null;
22
- try {
23
- _tty = fs.openSync("/dev/tty", "w");
24
- } catch (_) { /* Windows or no TTY — use stderr fallback */ }
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 = _write;
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 pythonCandidates) {
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
- if (!python) {
74
- warn("Python not found skipping pip install.");
75
- warn("After installing Python 3.10+, run inside your project:");
76
- warn(" pip install qa-gentic-stlc-agents");
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
- // ── 2. pip install (non-interactive, pipe output to stderr) ────────────────
79
- info("Installing qa-gentic-stlc-agents via pip…");
80
- const pip = spawnSync(
81
- python,
82
- ["-m", "pip", "install", "qa-gentic-stlc-agents", "--upgrade", "--quiet"],
83
- { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }
84
- );
85
- if (pip.status === 0) {
86
- ok("qa-gentic-stlc-agents installed.");
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
- // Route pip's output through _write (/dev/tty) — process.stderr is swallowed
89
- // by npm 7+ for global lifecycle scripts, so direct stderr writes vanish.
90
- const combined = `${pip.stdout || ""}${pip.stderr || ""}`;
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
- // ── 3. Activate cost tracking on MCP servers ─────────────────────────────
106
- info("Activating cost tracking on MCP servers...");
107
- const costPatch = spawnSync(
108
- python,
109
- ["-m", "stlc_agents.shared.install_hook"],
110
- { encoding: "utf8", stdio: "pipe" }
111
- );
112
- if (costPatch.status === 0) {
113
- // Print each output line through our TTY writer
114
- (costPatch.stdout || "").split("\n").filter(Boolean).forEach((l) => console.log(l));
115
- ok("Cost tracking active — every tool call will log tokens + cost");
116
- } else {
117
- warn("Cost tracking patch skipped (run manually: python -m stlc_agents.shared.install_hook)");
118
- if (costPatch.stderr) console.log(d(costPatch.stderr.slice(0, 300)));
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
- // ── 4. Print next-step instructions ──────────────────────────────────────────
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.29",
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.28",
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 [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
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 [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
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