chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* qodercli command resolution (#6263).
|
||||
*
|
||||
* Extracted from `qoderCli.ts` (frozen at the file-size baseline) so the
|
||||
* Windows-aware resolution logic can grow without bloating the transport module.
|
||||
*
|
||||
* The bare `"qodercli"` name does not resolve on Windows, where npm installs the
|
||||
* CLI as a `qodercli.cmd` wrapper under `%APPDATA%\npm` (a user-PATH directory)
|
||||
* that `spawn` cannot find with `shell:false` and an unenriched env. OmniRoute
|
||||
* already has a Windows-aware resolver for this exact tool in `cliRuntime.ts`, so
|
||||
* we reuse it: `getCliRuntimeStatus("qoder")` returns an absolute `.cmd`/`.exe`
|
||||
* `commandPath`, and `shouldUseShellForCommand()` tells us whether it needs cmd.exe.
|
||||
*/
|
||||
import {
|
||||
getCliRuntimeStatus,
|
||||
getKnownToolPaths,
|
||||
shouldUseShellForCommand,
|
||||
} from "@/shared/services/cliRuntime";
|
||||
|
||||
export function getQoderCliCommand(): string {
|
||||
const explicit = String(process.env.CLI_QODER_BIN || "").trim();
|
||||
return explicit || "qodercli";
|
||||
}
|
||||
|
||||
export type QoderCliInvocation = { command: string; useShell: boolean };
|
||||
|
||||
// Resolving through cliRuntime does synchronous fs walks plus a `--version`
|
||||
// healthcheck spawn; memoize the result so we don't repeat that on every chat /
|
||||
// quota request. The install location is effectively static for a running host.
|
||||
const QODER_RESOLVE_TTL_MS = 5 * 60 * 1000;
|
||||
// Keyed on the fallback command (which folds in CLI_QODER_BIN) so changing the
|
||||
// override — or a test pointing at a fresh stub — invalidates a stale entry
|
||||
// instead of spawning a since-deleted binary.
|
||||
let qoderInvocationCache: (QoderCliInvocation & { key: string; expiresAt: number }) | null = null;
|
||||
|
||||
/** Test-only: drop the memoized qodercli command resolution. */
|
||||
export function __clearQoderCliInvocationCache(): void {
|
||||
qoderInvocationCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the exact command + shell flag to spawn qodercli. `deps` is injectable
|
||||
* for unit tests; production uses the real cliRuntime exports.
|
||||
*/
|
||||
export async function resolveQoderCliInvocation(
|
||||
explicitCommand?: string | null,
|
||||
deps: {
|
||||
getStatus?: typeof getCliRuntimeStatus;
|
||||
shouldUseShell?: typeof shouldUseShellForCommand;
|
||||
} = {}
|
||||
): Promise<QoderCliInvocation> {
|
||||
const explicit = String(explicitCommand || "").trim();
|
||||
const getStatus = deps.getStatus || getCliRuntimeStatus;
|
||||
const shouldUseShell = deps.shouldUseShell || shouldUseShellForCommand;
|
||||
// Only the default path is cached; an explicit per-call command or an injected
|
||||
// resolver (tests) always resolves fresh and never touches the shared cache.
|
||||
const cacheable = !explicit && !deps.getStatus && !deps.shouldUseShell;
|
||||
const fallback = explicit || getQoderCliCommand();
|
||||
|
||||
if (
|
||||
cacheable &&
|
||||
qoderInvocationCache &&
|
||||
qoderInvocationCache.key === fallback &&
|
||||
qoderInvocationCache.expiresAt > Date.now()
|
||||
) {
|
||||
return { command: qoderInvocationCache.command, useShell: qoderInvocationCache.useShell };
|
||||
}
|
||||
|
||||
let command = fallback;
|
||||
try {
|
||||
const status = await getStatus("qoder");
|
||||
if (status && status.installed && status.commandPath) {
|
||||
command = status.commandPath;
|
||||
}
|
||||
} catch {
|
||||
/* fall back to the bare/explicit command — spawn will surface a real ENOENT */
|
||||
}
|
||||
|
||||
const invocation: QoderCliInvocation = { command, useShell: shouldUseShell(command) };
|
||||
if (cacheable) {
|
||||
qoderInvocationCache = {
|
||||
...invocation,
|
||||
key: fallback,
|
||||
expiresAt: Date.now() + QODER_RESOLVE_TTL_MS,
|
||||
};
|
||||
}
|
||||
return invocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the operator-facing "qodercli not found" error, listing the paths the
|
||||
* resolver searched plus the `CLI_QODER_BIN` override hint (#6263).
|
||||
*/
|
||||
export function buildQoderCliNotFoundHint(runError: string): string {
|
||||
let searchedHint = "";
|
||||
try {
|
||||
const candidates = getKnownToolPaths("qoder");
|
||||
if (candidates.length > 0) {
|
||||
searchedHint = ` Searched: ${candidates.slice(0, 6).join(", ")}.`;
|
||||
}
|
||||
} catch {
|
||||
/* best-effort — the path list is only advisory for the error message */
|
||||
}
|
||||
return (
|
||||
`Qoder CLI (qodercli) was not found on the OmniRoute host (${runError}).` +
|
||||
searchedHint +
|
||||
" Install it from https://qoder.com, or set CLI_QODER_BIN to the absolute path " +
|
||||
"of the qodercli binary (e.g. %APPDATA%\\npm\\qodercli.cmd on Windows). " +
|
||||
"PAT auth is driven through the local qodercli binary."
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user