ccqa 0.12.0 → 0.13.1
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 +87 -11
- package/dist/hub-client/index.d.mts +6 -0
- package/dist/hub-client/index.mjs +1 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -1168,9 +1168,44 @@ function resolveModel(explicit) {
|
|
|
1168
1168
|
const envModel = process.env["CCQA_MODEL"];
|
|
1169
1169
|
return envModel && envModel.length > 0 ? envModel : void 0;
|
|
1170
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Standard Claude Code environment variables that select the API endpoint and
|
|
1173
|
+
* credentials. ccqa forwards whichever of these are set to the underlying
|
|
1174
|
+
* Claude Code process; it does not read or interpret their values.
|
|
1175
|
+
*
|
|
1176
|
+
* - `ANTHROPIC_BASE_URL` — the API endpoint to send requests to.
|
|
1177
|
+
* - `ANTHROPIC_AUTH_TOKEN` — sent as `Authorization: Bearer <token>`.
|
|
1178
|
+
* - `ANTHROPIC_API_KEY` — API key, when used instead of a token.
|
|
1179
|
+
* - `ANTHROPIC_CUSTOM_HEADERS` — extra request headers.
|
|
1180
|
+
*/
|
|
1181
|
+
const ENDPOINT_ENV_KEYS = [
|
|
1182
|
+
"ANTHROPIC_BASE_URL",
|
|
1183
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
1184
|
+
"ANTHROPIC_API_KEY",
|
|
1185
|
+
"ANTHROPIC_CUSTOM_HEADERS"
|
|
1186
|
+
];
|
|
1187
|
+
/**
|
|
1188
|
+
* Collects the endpoint/auth variables set in the current process environment
|
|
1189
|
+
* so they can be forwarded, verbatim, to every Claude Code invocation. Returns
|
|
1190
|
+
* only the keys that are actually set (non-empty), so unset variables never
|
|
1191
|
+
* override the SDK's own defaults.
|
|
1192
|
+
*/
|
|
1193
|
+
function resolveEndpointEnv() {
|
|
1194
|
+
const endpointEnv = {};
|
|
1195
|
+
for (const key of ENDPOINT_ENV_KEYS) {
|
|
1196
|
+
const value = process.env[key];
|
|
1197
|
+
if (value && value.length > 0) endpointEnv[key] = value;
|
|
1198
|
+
}
|
|
1199
|
+
return endpointEnv;
|
|
1200
|
+
}
|
|
1171
1201
|
async function invokeClaudeStreaming(options, onEvent) {
|
|
1172
1202
|
const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, maxTurns, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, relaxAbConstraints = false } = options;
|
|
1173
1203
|
const resolvedModel = resolveModel(model);
|
|
1204
|
+
const hasEndpointEnv = Object.keys(resolveEndpointEnv()).length > 0;
|
|
1205
|
+
const mergedEnv = env || hasEndpointEnv ? {
|
|
1206
|
+
...process.env,
|
|
1207
|
+
...env
|
|
1208
|
+
} : void 0;
|
|
1174
1209
|
let lastAbToolUseId = null;
|
|
1175
1210
|
const claimAbToolUse = (toolUseId) => {
|
|
1176
1211
|
if (toolUseId !== lastAbToolUseId) return false;
|
|
@@ -1185,10 +1220,7 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
1185
1220
|
allowDangerouslySkipPermissions: true,
|
|
1186
1221
|
...resolvedModel ? { model: resolvedModel } : {},
|
|
1187
1222
|
...cwd ? { cwd } : {},
|
|
1188
|
-
...
|
|
1189
|
-
...process.env,
|
|
1190
|
-
...env
|
|
1191
|
-
} } : {},
|
|
1223
|
+
...mergedEnv ? { env: mergedEnv } : {},
|
|
1192
1224
|
...disableBuiltinTools ? { tools: [] } : {},
|
|
1193
1225
|
hooks: onAbAction || onAbActionFailed ? {
|
|
1194
1226
|
PreToolUse: [{ hooks: [async (input) => {
|
|
@@ -3199,6 +3231,44 @@ function resolveProjectOrThrow(project, cwd) {
|
|
|
3199
3231
|
*/
|
|
3200
3232
|
const hubUrlOption = ["--hub-url <url>", "ccqa hub base URL (or CCQA_HUB_URL)."];
|
|
3201
3233
|
const hubTokenOption = ["--hub-token <token>", "ccqa hub bearer token (or CCQA_HUB_TOKEN)."];
|
|
3234
|
+
const hubHeaderOption = [
|
|
3235
|
+
"--hub-header <header>",
|
|
3236
|
+
"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).",
|
|
3237
|
+
collectHubHeader,
|
|
3238
|
+
[]
|
|
3239
|
+
];
|
|
3240
|
+
/** commander `--hub-header` accumulator: collects repeated flags into an array. */
|
|
3241
|
+
function collectHubHeader(value, previous) {
|
|
3242
|
+
return [...previous, value];
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Parse `"key:value"` entries (from `--hub-header`, repeatable) into a
|
|
3246
|
+
* headers map. The value may itself contain `:` (e.g. a URL), so only the
|
|
3247
|
+
* first colon is treated as the separator. Throws on an entry with no colon.
|
|
3248
|
+
*/
|
|
3249
|
+
function parseHubHeaders(entries) {
|
|
3250
|
+
const headers = {};
|
|
3251
|
+
for (const entry of entries) {
|
|
3252
|
+
const i = entry.indexOf(":");
|
|
3253
|
+
if (i < 0) throw new Error(`invalid --hub-header (expected "key:value"): ${entry}`);
|
|
3254
|
+
const key = entry.slice(0, i).trim();
|
|
3255
|
+
const value = entry.slice(i + 1).trim();
|
|
3256
|
+
if (!key) throw new Error(`invalid --hub-header (expected "key:value"): ${entry}`);
|
|
3257
|
+
headers[key] = value;
|
|
3258
|
+
}
|
|
3259
|
+
return headers;
|
|
3260
|
+
}
|
|
3261
|
+
/**
|
|
3262
|
+
* Resolve custom hub headers from flags, falling back to `CCQA_HUB_HEADER`
|
|
3263
|
+
* (a single `"key:value"` entry) when no `--hub-header` flag was given.
|
|
3264
|
+
* CI setups typically pass exactly one gateway header via env; `--hub-header`
|
|
3265
|
+
* is repeatable for the (rarer) local/multi-header case.
|
|
3266
|
+
*/
|
|
3267
|
+
function resolveHubHeaders(hubHeader) {
|
|
3268
|
+
if (hubHeader && hubHeader.length > 0) return parseHubHeaders(hubHeader);
|
|
3269
|
+
const envHeader = process.env.CCQA_HUB_HEADER;
|
|
3270
|
+
if (envHeader) return parseHubHeaders([envHeader]);
|
|
3271
|
+
}
|
|
3202
3272
|
/** Thrown by `requireHubClient` when the URL and/or token can't be resolved from flags/env. */
|
|
3203
3273
|
var HubConnectionError = class extends Error {
|
|
3204
3274
|
constructor(message) {
|
|
@@ -3215,9 +3285,11 @@ function resolveHubClient(opts) {
|
|
|
3215
3285
|
const baseUrl = opts.hubUrl ?? process.env.CCQA_HUB_URL;
|
|
3216
3286
|
const token = opts.hubToken ?? process.env.CCQA_HUB_TOKEN;
|
|
3217
3287
|
if (!baseUrl || !token) return null;
|
|
3288
|
+
const headers = resolveHubHeaders(opts.hubHeader);
|
|
3218
3289
|
return createHubClient({
|
|
3219
3290
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
3220
|
-
token
|
|
3291
|
+
token,
|
|
3292
|
+
...headers ? { headers } : {}
|
|
3221
3293
|
});
|
|
3222
3294
|
}
|
|
3223
3295
|
/** Same as `resolveHubClient`, but throws `HubConnectionError` instead of returning `null`. */
|
|
@@ -3286,7 +3358,7 @@ function addProfileOption(command) {
|
|
|
3286
3358
|
* options so help text and behaviour don't drift.
|
|
3287
3359
|
*/
|
|
3288
3360
|
function addHubOptions(command) {
|
|
3289
|
-
return command.option(...hubUrlOption).option(...hubTokenOption);
|
|
3361
|
+
return command.option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption);
|
|
3290
3362
|
}
|
|
3291
3363
|
/**
|
|
3292
3364
|
* CLI wrapper around `resolveProfileEnv`: on failure, print the error and
|
|
@@ -5218,7 +5290,8 @@ async function executeRun(targets, opts) {
|
|
|
5218
5290
|
project: projectForProfile,
|
|
5219
5291
|
cwd,
|
|
5220
5292
|
hubUrl: opts.hubUrl,
|
|
5221
|
-
hubToken: opts.hubToken
|
|
5293
|
+
hubToken: opts.hubToken,
|
|
5294
|
+
hubHeader: opts.hubHeader
|
|
5222
5295
|
});
|
|
5223
5296
|
} else await resolveProfileEnv({
|
|
5224
5297
|
profile: void 0,
|
|
@@ -5236,6 +5309,7 @@ async function executeRun(targets, opts) {
|
|
|
5236
5309
|
hubCtx = resolveHubContext({
|
|
5237
5310
|
hubUrl: opts.hubUrl,
|
|
5238
5311
|
hubToken: opts.hubToken,
|
|
5312
|
+
hubHeader: opts.hubHeader,
|
|
5239
5313
|
project: opts.project,
|
|
5240
5314
|
cwd
|
|
5241
5315
|
});
|
|
@@ -8597,7 +8671,8 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
|
|
|
8597
8671
|
project,
|
|
8598
8672
|
cwd: cwdForProfile,
|
|
8599
8673
|
hubUrl: opts.hubUrl,
|
|
8600
|
-
hubToken: opts.hubToken
|
|
8674
|
+
hubToken: opts.hubToken,
|
|
8675
|
+
hubHeader: opts.hubHeader
|
|
8601
8676
|
});
|
|
8602
8677
|
else await applyProfileFromOption({
|
|
8603
8678
|
profile: void 0,
|
|
@@ -8606,7 +8681,8 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
|
|
|
8606
8681
|
});
|
|
8607
8682
|
const hubClientForTrace = resolveHubClient({
|
|
8608
8683
|
hubUrl: opts.hubUrl,
|
|
8609
|
-
hubToken: opts.hubToken
|
|
8684
|
+
hubToken: opts.hubToken,
|
|
8685
|
+
hubHeader: opts.hubHeader
|
|
8610
8686
|
});
|
|
8611
8687
|
const hubContext = hubClientForTrace && project ? {
|
|
8612
8688
|
hub: hubClientForTrace,
|
|
@@ -9325,7 +9401,7 @@ Return the spec keys that might be affected by any of the new files. Conservativ
|
|
|
9325
9401
|
//#endregion
|
|
9326
9402
|
//#region src/cli/drift.ts
|
|
9327
9403
|
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) => {
|
|
9404
|
+
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
9405
|
const format = parseFormat(opts.format);
|
|
9330
9406
|
const threshold = parseSeverity(opts.severity);
|
|
9331
9407
|
const concurrency = parseConcurrency(opts.concurrency);
|
|
@@ -11597,7 +11673,7 @@ const CSS = `
|
|
|
11597
11673
|
order decides). */
|
|
11598
11674
|
.chip.kind-chip { color: var(--violet); background: var(--violet-bg); border-color: var(--violet-border); font-family: var(--font); margin-left: 6px; }
|
|
11599
11675
|
.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); }
|
|
11676
|
+
.chip.drift-errors-chip { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-border); margin-left: 6px; }
|
|
11601
11677
|
.drift-meta-box { display: flex; flex-direction: column; gap: 4px; }
|
|
11602
11678
|
.drift-meta-chips { display: flex; gap: 6px; }
|
|
11603
11679
|
.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
|
}
|
package/dist/package.json
CHANGED