- ciRunsUnconditionally never looked at the workflow-level `on:` trigger, so a
workflow switched to `on: release` still contained the step and reported as
wired while no PR ever ran the validator. It now requires `push` or
`pull_request`, handling all three shapes `on:` takes. The positive fixtures
had no trigger at all, so they could not assert the "on every push" half of
the contract; they carry one now, alongside a release-only negative. Verified
by mutation.
- Stale JSDoc from the round-16 version change: `probeClaudeVersion` is now
generic in its parser's result rather than hard-coding `number[]`,
`validatorCommand` documents `{ parts, prerelease }`, and `runValidator`
documents the `platform` option it already accepted. Annotations that lie make
valid call sites look type-invalid.
Signed-off-by: cliffhall <cliff@futurescale.com>
68 lines
2.6 KiB
JavaScript
68 lines
2.6 KiB
JavaScript
// Spawning the `claude` CLI from the root scripts (#2163, Copilot).
|
|
//
|
|
// On Windows an npm-installed `claude` is a `.cmd` shim, and Node has refused
|
|
// shell-free `.cmd`/`.bat` spawns since the CVE-2024-27980 hardening — so a
|
|
// plain `spawnSync("claude", …)` reports ENOENT there even though the CLI is
|
|
// installed. Every consumer of that would then be wrong in a *quiet* way:
|
|
// `verify:skills` would skip its authoritative hand-off, `verify:skills:cli`
|
|
// would download the pinned package it already has (and fail offline), and
|
|
// `skills:eval` would report "not on PATH".
|
|
//
|
|
// The shell that fixes it introduces the #1939 problem — `cmd.exe` re-parses a
|
|
// space-joined string, so any argument holding a metacharacter becomes syntax —
|
|
// so arguments go through the same `winShellArgs` quoting the npm/npx call
|
|
// sites use. This is deliberately the ONLY place that decides either question.
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { winShellArgs } from "./win-shell-args.mjs";
|
|
|
|
/**
|
|
* `spawnSync` arguments for a `claude` invocation, correct on every platform.
|
|
*
|
|
* @param {string[]} args
|
|
* @param {object} [options] Passed through to `spawnSync`.
|
|
* @param {string} [platform] Defaults to the current platform; injectable for tests.
|
|
* @returns {{ command: string, args: string[], options: object }}
|
|
*/
|
|
export function claudeSpawnArgs(
|
|
args,
|
|
options = {},
|
|
platform = process.platform,
|
|
) {
|
|
return {
|
|
command: "claude",
|
|
args: winShellArgs(args, platform),
|
|
options: { ...options, shell: platform === "win32" },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Read the installed CLI's version, or null when there is no usable one.
|
|
*
|
|
* "Usable" deliberately includes parsing the output: a `claude` that answers
|
|
* `--version` with something unrecognizable cannot be trusted to be the pinned
|
|
* one either, and the callers all treat null as "use the pinned package".
|
|
*
|
|
* Generic in the parser's result: this helper only distinguishes "parsed" from
|
|
* "did not", so the shape is whatever the caller's parser returns — today
|
|
* `{ parts, prerelease }` from `parseClaudeVersion`. Hard-coding `number[]`
|
|
* here made valid callers look type-invalid (Copilot).
|
|
*
|
|
* @template T
|
|
* @param {(text: string) => T | null} parseVersion
|
|
* @param {{ spawn?: typeof spawnSync, platform?: string }} [io]
|
|
* @returns {T | null}
|
|
*/
|
|
export function probeClaudeVersion(
|
|
parseVersion,
|
|
{ spawn = spawnSync, platform } = {},
|
|
) {
|
|
const { command, args, options } = claudeSpawnArgs(
|
|
["--version"],
|
|
{ encoding: "utf8" },
|
|
platform,
|
|
);
|
|
const res = spawn(command, args, options);
|
|
if (res.error || res.status !== 0) return null;
|
|
return parseVersion(res.stdout ?? "");
|
|
}
|