ccqa 0.3.3 → 0.3.4

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/dist/bin/ccqa.mjs CHANGED
@@ -4,7 +4,7 @@ import { Command } from "commander";
4
4
  import { accessSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { access, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
7
- import { dirname, join, resolve } from "node:path";
7
+ import { delimiter, dirname, join, resolve } from "node:path";
8
8
  import { query } from "@anthropic-ai/claude-agent-sdk";
9
9
  import matter from "gray-matter";
10
10
  import { spawn } from "node:child_process";
@@ -706,10 +706,10 @@ function bundledVitestConfigPath() {
706
706
  }
707
707
  //#endregion
708
708
  //#region src/runtime/spawn-vitest.ts
709
- const require = createRequire(import.meta.url);
709
+ const require$1 = createRequire(import.meta.url);
710
710
  function resolveVitestBin() {
711
- const pkgPath = require.resolve("vitest/package.json");
712
- const pkg = require(pkgPath);
711
+ const pkgPath = require$1.resolve("vitest/package.json");
712
+ const pkg = require$1(pkgPath);
713
713
  const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.vitest;
714
714
  if (!binRel) throw new Error(`vitest package.json has no bin entry (resolved at ${pkgPath})`);
715
715
  return resolve(dirname(pkgPath), binRel);
@@ -761,6 +761,38 @@ function waitExit(child) {
761
761
  });
762
762
  }
763
763
  //#endregion
764
+ //#region src/runtime/agent-browser-bin.ts
765
+ const require = createRequire(import.meta.url);
766
+ /**
767
+ * Resolves the directory containing the `agent-browser` shim that npm/pnpm
768
+ * exposes on PATH for the peer-installed package. Used by `ccqa trace` to
769
+ * prepend this directory to PATH so the Claude subprocess can invoke
770
+ * `agent-browser ...` without requiring a global install.
771
+ *
772
+ * Returns null if agent-browser cannot be resolved (peer not installed).
773
+ */
774
+ function resolveAgentBrowserBinDir() {
775
+ let pkgJsonPath;
776
+ try {
777
+ pkgJsonPath = require.resolve("agent-browser/package.json");
778
+ } catch {
779
+ return null;
780
+ }
781
+ return join(dirname(pkgJsonPath), "node_modules", ".bin");
782
+ }
783
+ /**
784
+ * Returns a PATH string with the agent-browser shim directory prepended,
785
+ * so `agent-browser ...` resolves without a global install. Falls back to
786
+ * the original PATH when the package can't be resolved.
787
+ */
788
+ function pathWithAgentBrowserShim(currentPath) {
789
+ const path = currentPath ?? "";
790
+ const dir = resolveAgentBrowserBinDir();
791
+ if (!dir) return path;
792
+ if (path.split(delimiter).includes(dir)) return path;
793
+ return dir + delimiter + path;
794
+ }
795
+ //#endregion
764
796
  //#region src/cli/trace.ts
765
797
  const traceCommand = new Command("trace").argument("<feature/spec>", "Spec to trace (e.g. tasks/create-and-complete)").description("Run agent-browser, verify assertions, and record structured actions").action(async (specPath) => {
766
798
  const { featureName, specName } = parseSpecPath(specPath);
@@ -801,7 +833,10 @@ async function runTrace(featureName, specName) {
801
833
  "Grep",
802
834
  "Glob"
803
835
  ],
804
- env: { AGENT_BROWSER_SESSION: sessionName },
836
+ env: {
837
+ AGENT_BROWSER_SESSION: sessionName,
838
+ PATH: pathWithAgentBrowserShim(process.env["PATH"])
839
+ },
805
840
  onAbAction: (abAction) => {
806
841
  const action = parseAbAction(abAction);
807
842
  if (action) traceActions.push(action);
@@ -1214,18 +1249,36 @@ async function loadSetupScripts(setups) {
1214
1249
  return result;
1215
1250
  }
1216
1251
  /**
1217
- * Extract the test body (lines inside the first test() block) from a setup test script.
1252
+ * Extract the test body (statements inside the test callback) from a setup
1253
+ * test script.
1254
+ *
1255
+ * Locates the first arrow callback (`=> {`) after a top-level `test(` call
1256
+ * and returns the text between the matching `{` and `}`. Handles both
1257
+ * single-line and multi-line `test(...)` formatting (the latter is what
1258
+ * prettier produces).
1259
+ *
1260
+ * Brace tracking is naive (string/regex/comment literals are not parsed
1261
+ * specially), but setup test scripts are themselves generated by ccqa and
1262
+ * follow a fixed shape, so this is sufficient in practice.
1218
1263
  */
1219
1264
  function extractTestBody(script) {
1220
- const lines = script.split("\n");
1221
- const startIdx = lines.findIndex((l) => /^\s*test\(/.test(l));
1222
- if (startIdx === -1) return "";
1223
- const bodyLines = [];
1224
- for (let i = startIdx + 1; i < lines.length; i++) {
1225
- if (/^\s*\}[\s,);]/.test(lines[i])) break;
1226
- bodyLines.push(lines[i]);
1265
+ const testCallMatch = /\btest\s*\(/.exec(script);
1266
+ if (!testCallMatch) return "";
1267
+ const arrowIdx = script.indexOf("=> {", testCallMatch.index);
1268
+ if (arrowIdx === -1) return "";
1269
+ const bodyStart = arrowIdx + 4;
1270
+ let depth = 1;
1271
+ let i = bodyStart;
1272
+ for (; i < script.length; i++) {
1273
+ const ch = script[i];
1274
+ if (ch === "{") depth++;
1275
+ else if (ch === "}") {
1276
+ depth--;
1277
+ if (depth === 0) break;
1278
+ }
1227
1279
  }
1228
- return bodyLines.join("\n");
1280
+ if (depth !== 0) return "";
1281
+ return script.slice(bodyStart, i).replace(/^\n/, "").replace(/\n\s*$/, "");
1229
1282
  }
1230
1283
  function replacePlaceholders(body, params) {
1231
1284
  let result = body;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
@@ -2,7 +2,18 @@ import { createRequire } from "node:module";
2
2
  import { spawnSync } from "node:child_process";
3
3
  //#region src/runtime/test-helpers.ts
4
4
  const AB = createRequire(import.meta.url).resolve("agent-browser/bin/agent-browser.js");
5
- function spawnAB(args) {
5
+ const EAGAIN_PATTERN = /Resource temporarily unavailable|os error 35/i;
6
+ const EAGAIN_RETRIES = 3;
7
+ const EAGAIN_BACKOFF_MS = [
8
+ 200,
9
+ 500,
10
+ 1e3
11
+ ];
12
+ function sleepSync(ms) {
13
+ const buf = new SharedArrayBuffer(4);
14
+ Atomics.wait(new Int32Array(buf), 0, 0, ms);
15
+ }
16
+ function spawnABOnce(args) {
6
17
  const result = spawnSync(AB, args, { stdio: "pipe" });
7
18
  return {
8
19
  status: result.status,
@@ -10,6 +21,17 @@ function spawnAB(args) {
10
21
  stderr: result.stderr?.toString() ?? ""
11
22
  };
12
23
  }
24
+ function spawnAB(args) {
25
+ let result = spawnABOnce(args);
26
+ for (let attempt = 0; attempt < EAGAIN_RETRIES; attempt++) {
27
+ if (result.status === 0) return result;
28
+ const combined = `${result.stdout}\n${result.stderr}`;
29
+ if (!EAGAIN_PATTERN.test(combined)) return result;
30
+ sleepSync(EAGAIN_BACKOFF_MS[attempt] ?? 1e3);
31
+ result = spawnABOnce(args);
32
+ }
33
+ return result;
34
+ }
13
35
  function logStep(action, args) {
14
36
  const pretty = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
15
37
  process.stdout.write(` ▶ ${action.padEnd(14)} ${pretty}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {