ccqa 0.12.0 → 0.13.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/dist/bin/ccqa.mjs CHANGED
@@ -3199,6 +3199,44 @@ function resolveProjectOrThrow(project, cwd) {
3199
3199
  */
3200
3200
  const hubUrlOption = ["--hub-url <url>", "ccqa hub base URL (or CCQA_HUB_URL)."];
3201
3201
  const hubTokenOption = ["--hub-token <token>", "ccqa hub bearer token (or CCQA_HUB_TOKEN)."];
3202
+ const hubHeaderOption = [
3203
+ "--hub-header <header>",
3204
+ "Extra header sent with every hub request, as \"key:value\" (or CCQA_HUB_HEADER). Repeatable. For infra that gates the hub behind a header check (e.g. a load balancer bypass rule).",
3205
+ collectHubHeader,
3206
+ []
3207
+ ];
3208
+ /** commander `--hub-header` accumulator: collects repeated flags into an array. */
3209
+ function collectHubHeader(value, previous) {
3210
+ return [...previous, value];
3211
+ }
3212
+ /**
3213
+ * Parse `"key:value"` entries (from `--hub-header`, repeatable) into a
3214
+ * headers map. The value may itself contain `:` (e.g. a URL), so only the
3215
+ * first colon is treated as the separator. Throws on an entry with no colon.
3216
+ */
3217
+ function parseHubHeaders(entries) {
3218
+ const headers = {};
3219
+ for (const entry of entries) {
3220
+ const i = entry.indexOf(":");
3221
+ if (i < 0) throw new Error(`invalid --hub-header (expected "key:value"): ${entry}`);
3222
+ const key = entry.slice(0, i).trim();
3223
+ const value = entry.slice(i + 1).trim();
3224
+ if (!key) throw new Error(`invalid --hub-header (expected "key:value"): ${entry}`);
3225
+ headers[key] = value;
3226
+ }
3227
+ return headers;
3228
+ }
3229
+ /**
3230
+ * Resolve custom hub headers from flags, falling back to `CCQA_HUB_HEADER`
3231
+ * (a single `"key:value"` entry) when no `--hub-header` flag was given.
3232
+ * CI setups typically pass exactly one gateway header via env; `--hub-header`
3233
+ * is repeatable for the (rarer) local/multi-header case.
3234
+ */
3235
+ function resolveHubHeaders(hubHeader) {
3236
+ if (hubHeader && hubHeader.length > 0) return parseHubHeaders(hubHeader);
3237
+ const envHeader = process.env.CCQA_HUB_HEADER;
3238
+ if (envHeader) return parseHubHeaders([envHeader]);
3239
+ }
3202
3240
  /** Thrown by `requireHubClient` when the URL and/or token can't be resolved from flags/env. */
3203
3241
  var HubConnectionError = class extends Error {
3204
3242
  constructor(message) {
@@ -3215,9 +3253,11 @@ function resolveHubClient(opts) {
3215
3253
  const baseUrl = opts.hubUrl ?? process.env.CCQA_HUB_URL;
3216
3254
  const token = opts.hubToken ?? process.env.CCQA_HUB_TOKEN;
3217
3255
  if (!baseUrl || !token) return null;
3256
+ const headers = resolveHubHeaders(opts.hubHeader);
3218
3257
  return createHubClient({
3219
3258
  baseUrl: baseUrl.replace(/\/+$/, ""),
3220
- token
3259
+ token,
3260
+ ...headers ? { headers } : {}
3221
3261
  });
3222
3262
  }
3223
3263
  /** Same as `resolveHubClient`, but throws `HubConnectionError` instead of returning `null`. */
@@ -3286,7 +3326,7 @@ function addProfileOption(command) {
3286
3326
  * options so help text and behaviour don't drift.
3287
3327
  */
3288
3328
  function addHubOptions(command) {
3289
- return command.option(...hubUrlOption).option(...hubTokenOption);
3329
+ return command.option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption);
3290
3330
  }
3291
3331
  /**
3292
3332
  * CLI wrapper around `resolveProfileEnv`: on failure, print the error and
@@ -5218,7 +5258,8 @@ async function executeRun(targets, opts) {
5218
5258
  project: projectForProfile,
5219
5259
  cwd,
5220
5260
  hubUrl: opts.hubUrl,
5221
- hubToken: opts.hubToken
5261
+ hubToken: opts.hubToken,
5262
+ hubHeader: opts.hubHeader
5222
5263
  });
5223
5264
  } else await resolveProfileEnv({
5224
5265
  profile: void 0,
@@ -5236,6 +5277,7 @@ async function executeRun(targets, opts) {
5236
5277
  hubCtx = resolveHubContext({
5237
5278
  hubUrl: opts.hubUrl,
5238
5279
  hubToken: opts.hubToken,
5280
+ hubHeader: opts.hubHeader,
5239
5281
  project: opts.project,
5240
5282
  cwd
5241
5283
  });
@@ -8597,7 +8639,8 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
8597
8639
  project,
8598
8640
  cwd: cwdForProfile,
8599
8641
  hubUrl: opts.hubUrl,
8600
- hubToken: opts.hubToken
8642
+ hubToken: opts.hubToken,
8643
+ hubHeader: opts.hubHeader
8601
8644
  });
8602
8645
  else await applyProfileFromOption({
8603
8646
  profile: void 0,
@@ -8606,7 +8649,8 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
8606
8649
  });
8607
8650
  const hubClientForTrace = resolveHubClient({
8608
8651
  hubUrl: opts.hubUrl,
8609
- hubToken: opts.hubToken
8652
+ hubToken: opts.hubToken,
8653
+ hubHeader: opts.hubHeader
8610
8654
  });
8611
8655
  const hubContext = hubClientForTrace && project ? {
8612
8656
  hub: hubClientForTrace,
@@ -9325,7 +9369,7 @@ Return the spec keys that might be affected by any of the new files. Conservativ
9325
9369
  //#endregion
9326
9370
  //#region src/cli/drift.ts
9327
9371
  const DEFAULT_CONCURRENCY = 3;
9328
- const driftCommand = addLanguageOption(new Command("drift").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Standalone spec ↔ codebase static audit. Use for PR checks where the browser isn't run. For run-time audit with a structured report, see `ccqa run --report`.").option("--format <fmt>", "Output format: text | json | github", "text").option("--severity <level>", "Exit non-zero on this severity or higher: warn | error", "error").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--changed", "Restrict drift checks to specs whose relatedPaths intersect the git diff against --base (or, in CI, $GITHUB_BASE_REF, else origin/main). New files are routed to specs via a single lightweight Claude call.").option("--base <ref>", "Base ref to diff against when --changed is set. Defaults to $GITHUB_BASE_REF (CI) or origin/main.").option("--push", "Push the drift result to a ccqa hub as a run (kind: drift).").option("--project <name>", "Logical project name for the pushed run. Defaults to the current directory's name.").option(...hubUrlOption).option(...hubTokenOption)).action(async (specPath, opts) => {
9372
+ const driftCommand = addLanguageOption(new Command("drift").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Standalone spec ↔ codebase static audit. Use for PR checks where the browser isn't run. For run-time audit with a structured report, see `ccqa run --report`.").option("--format <fmt>", "Output format: text | json | github", "text").option("--severity <level>", "Exit non-zero on this severity or higher: warn | error", "error").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--changed", "Restrict drift checks to specs whose relatedPaths intersect the git diff against --base (or, in CI, $GITHUB_BASE_REF, else origin/main). New files are routed to specs via a single lightweight Claude call.").option("--base <ref>", "Base ref to diff against when --changed is set. Defaults to $GITHUB_BASE_REF (CI) or origin/main.").option("--push", "Push the drift result to a ccqa hub as a run (kind: drift).").option("--project <name>", "Logical project name for the pushed run. Defaults to the current directory's name.").option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption)).action(async (specPath, opts) => {
9329
9373
  const format = parseFormat(opts.format);
9330
9374
  const threshold = parseSeverity(opts.severity);
9331
9375
  const concurrency = parseConcurrency(opts.concurrency);
@@ -11597,7 +11641,7 @@ const CSS = `
11597
11641
  order decides). */
11598
11642
  .chip.kind-chip { color: var(--violet); background: var(--violet-bg); border-color: var(--violet-border); font-family: var(--font); margin-left: 6px; }
11599
11643
  .chip.drift-count-chip { color: var(--amber); background: var(--amber-bg); border-color: var(--amber-border); margin-left: 6px; }
11600
- .chip.drift-errors-chip { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-border); }
11644
+ .chip.drift-errors-chip { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-border); margin-left: 6px; }
11601
11645
  .drift-meta-box { display: flex; flex-direction: column; gap: 4px; }
11602
11646
  .drift-meta-chips { display: flex; gap: 6px; }
11603
11647
  .specs { display: inline-flex; align-items: center; gap: 9px; }
@@ -181,6 +181,12 @@ type LabelsExport = z.infer<typeof LabelsExportSchema>;
181
181
  interface HubClientOptions {
182
182
  baseUrl: string;
183
183
  token: string;
184
+ /**
185
+ * Extra headers sent on every request, ahead of any per-call headers and
186
+ * the `Authorization` header (e.g. an infra gateway/ALB bypass header in
187
+ * CI — never overrides `Authorization`).
188
+ */
189
+ headers?: Record<string, string>;
184
190
  /** Override for testing; defaults to the global `fetch`. */
185
191
  fetchImpl?: typeof fetch;
186
192
  }
@@ -51,6 +51,7 @@ function createHubClient(opts) {
51
51
  ...init,
52
52
  signal,
53
53
  headers: {
54
+ ...opts.headers,
54
55
  ...init.headers,
55
56
  Authorization: `Bearer ${opts.token}`
56
57
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {