ktx/scripts/refresh-uv-manifest.mjs
Andrey Avtomonov feb0818444
feat(cli): self-provision pinned uv and defer MCP Python runtime install (#297)
Fixes a production crash-loop (PostHog issue 019eb68e): ktx mcp start
--foreground on a uv-less container eagerly installed the managed Python
runtime at boot, failed, and was restarted by its supervisor every ~62s
(122 exceptions from one install).

- MCP server factory now wires a lazy semantic-layer compute port that
  defers the runtime install to the first call, mirroring the already-lazy
  SQL-analysis port; the server boots and serves non-Python tools without
  the runtime.
- ktx no longer requires uv on PATH: it downloads its own pinned,
  sha256-verified uv build under the runtime root (KTX_RUNTIME_ROOT aware),
  always musl-static on Linux. PATH uv is never consulted.
- uv is acquired before the version dir is wiped, so a failed download
  cannot destroy an existing runtime.
- Acquisition failures (offline, intercepted download, unsupported
  platform) throw KtxExpectedError and stay out of Error Tracking; a
  missing binary inside a checksum-verified archive remains a plain Error.
- scripts/refresh-uv-manifest.mjs regenerates the pinned manifest
  (packages/cli/src/managed-uv-release.ts) on uv bumps.
- Setup consent prompt now discloses the uv download; docs updated.
2026-06-12 16:31:06 +00:00

95 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
import { writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const manifestModulePath = path.join(scriptDir, '..', 'packages', 'cli', 'src', 'managed-uv-release.ts');
// Linux always uses the musl-static build: it runs on both glibc and musl
// distributions, so the CLI never has to detect libc at runtime.
const PLATFORM_ARTIFACTS = {
'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz',
'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz',
'linux-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz',
'linux-x64': 'uv-x86_64-unknown-linux-musl.tar.gz',
'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip',
'win32-x64': 'uv-x86_64-pc-windows-msvc.zip',
};
function parseSha256File(contents, file) {
const hash = contents.trim().split(/\s+/)[0]?.replace(/^\*/, '');
if (!/^[a-f0-9]{64}$/.test(hash ?? '')) {
throw new Error(`Unexpected sha256 file contents for ${file}: ${contents.slice(0, 120)}`);
}
return hash;
}
export function renderUvReleaseModule(version, artifacts) {
const keys = Object.keys(PLATFORM_ARTIFACTS);
for (const key of keys) {
if (!artifacts[key]?.file || !artifacts[key]?.sha256) {
throw new Error(`Missing artifact entry for ${key}`);
}
}
const entries = keys
.map(
(key) =>
` '${key}': { file: '${artifacts[key].file}', sha256: '${artifacts[key].sha256}' }, // pragma: allowlist secret`,
)
.join('\n');
return `// Generated by scripts/refresh-uv-manifest.mjs. Do not edit by hand.
// Regenerate with: node scripts/refresh-uv-manifest.mjs [<uv-version>]
export type ManagedUvPlatformKey =
${keys.map((key) => ` | '${key}'`).join('\n')};
export interface ManagedUvArtifact {
file: string;
sha256: string;
}
export const MANAGED_UV_VERSION = '${version}';
export const MANAGED_UV_ARTIFACTS: Record<ManagedUvPlatformKey, ManagedUvArtifact> = {
${entries}
};
`;
}
export async function refreshUvManifest(options = {}) {
const fetchImpl = options.fetch ?? fetch;
const writeFile = options.writeFile ?? writeFileSync;
const log = options.log ?? console.log;
const outputPath = options.outputPath ?? manifestModulePath;
let version = options.version;
if (!version) {
const response = await fetchImpl('https://api.github.com/repos/astral-sh/uv/releases/latest');
if (!response.ok) {
throw new Error(`Failed to resolve latest uv release: HTTP ${response.status}`);
}
version = (await response.json()).tag_name;
}
const artifacts = {};
for (const [key, file] of Object.entries(PLATFORM_ARTIFACTS)) {
const url = `https://github.com/astral-sh/uv/releases/download/${version}/${file}.sha256`;
const response = await fetchImpl(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
}
artifacts[key] = { file, sha256: parseSha256File(await response.text(), file) };
}
writeFile(outputPath, renderUvReleaseModule(version, artifacts));
log(`Pinned uv ${version} into ${outputPath}`);
return { version, artifacts };
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
refreshUvManifest({ version: process.argv[2] }).catch((error) => {
console.error(error);
process.exit(1);
});
}