Files
gstack/runtime/runtime-bootstrap.mjs
T
Sinabina dbf94804ad fix: make runtime release six-platform safe
Use an allowed local file URL for the browser smoke test, centralize keyless archive signing after native builds so Windows ARM64 does not require an unavailable Cosign binary, and advance the immutable bootstrap channel to RC2.
2026-07-20 16:58:31 -07:00

556 lines
26 KiB
JavaScript

#!/usr/bin/env node
// Dependency-free bootstrap copied into each standards-installed GStack skill.
// It installs only the optional local runtime; host skill placement remains the
// responsibility of the Agent Skills installer.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { createHash } from "node:crypto";
import { constants as fsConstants, createReadStream } from "node:fs";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.2";
export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]),
"browser-visible": Object.freeze([]),
design: Object.freeze([]),
pdf: Object.freeze(["browser", "diagram"]),
diagram: Object.freeze(["browser"]),
ios: Object.freeze([]),
});
export const COMPONENT_DEPENDENCIES = Object.freeze({
core: Object.freeze([]),
"browser-code": Object.freeze(["core"]),
"browser-headless": Object.freeze(["browser-code"]),
"browser-visible": Object.freeze(["browser-code"]),
design: Object.freeze(["core"]),
diagram: Object.freeze(["browser-headless"]),
pdf: Object.freeze(["diagram"]),
ios: Object.freeze(["core"]),
});
export const CAPABILITY_COMPONENTS = Object.freeze({
browser: Object.freeze(["browser-code", "browser-headless"]),
"browser-visible": Object.freeze(["browser-code", "browser-visible"]),
design: Object.freeze(["design"]),
diagram: Object.freeze(["diagram"]),
pdf: Object.freeze(["pdf"]),
ios: Object.freeze(["ios"]),
});
const ALLOWED_DOWNLOAD_HOSTS = new Set([
"github.com",
"objects.githubusercontent.com",
"release-assets.githubusercontent.com",
]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) {
const io = {
stdout: options.stdout ?? process.stdout,
stderr: options.stderr ?? process.stderr,
};
try {
const parsed = parseArgs(argv);
if (parsed.help) {
io.stdout.write(usage());
return 0;
}
if (!["preview", "install"].includes(parsed.action)) {
throw bootstrapError("Expected `preview` or `install`", "BOOTSTRAP_USAGE");
}
const platform = options.platform ?? process.platform;
if (parsed.capabilities.includes("ios") && platform !== "darwin") {
throw bootstrapError("The physical-iOS capability is available only on macOS", "BOOTSTRAP_PLATFORM_UNSUPPORTED");
}
if (parsed.source) {
if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0;
}
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false });
}
const fetch_ = options.fetch ?? globalThis.fetch;
if (typeof fetch_ !== "function") throw bootstrapError("Node 18+ with fetch is required", "BOOTSTRAP_NODE_UNSUPPORTED");
const target = platformTarget(
platform,
options.arch ?? process.arch,
options.libc ?? detectLinuxLibc(platform),
);
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan);
if (parsed.action === "preview") return 0;
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after reviewing this exact component plan", "BOOTSTRAP_CONSENT_REQUIRED");
const temporary = await fs.mkdtemp(path.join(options.tmpDir ?? os.tmpdir(), "gstack-bootstrap-"));
try {
const root = path.join(temporary, "merged", "gstack");
await fs.mkdir(root, { recursive: true, mode: 0o700 });
const claimedFiles = new Set();
if (reusable) await seedReusableRuntime(reusable, root, claimedFiles);
for (const item of plan.downloads) {
const archive = path.join(temporary, `${item.component}.tar.gz`);
await downloadVerified(fetch_, item.artifact.url, archive, item.artifact.sha256, item.artifact.bytes);
io.stdout.write(`Verified SHA-256 for ${item.component} (${target}).\n`);
await verifyCosignWhenAvailable(archive, item.artifact, path.join(temporary, item.component), { ...options, fetch: fetch_, ...io });
const extracted = path.join(temporary, "extracted", item.component);
await fs.mkdir(extracted, { recursive: true, mode: 0o700 });
await extractTarSafely(archive, extracted, options);
const componentRoot = safeArtifactRoot(extracted, item.artifact.root ?? "gstack");
await assertNoLinks(componentRoot);
await mergeComponentRoot(componentRoot, root, claimedFiles, item.component);
}
return await installFromSource(root, parsed, { ...options, ...io, prepared: true, version: manifest.version });
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
} catch (error) {
io.stderr.write(`gstack bootstrap: ${error?.message ?? error}\n`);
return 1;
}
}
function parseArgs(argv) {
const result = { action: null, capabilities: [], source: null, home: null, yes: false, json: false, help: false };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (["-h", "--help"].includes(arg)) result.help = true;
else if (arg === "--yes") result.yes = true;
else if (arg === "--json") result.json = true;
else if (!result.action && !arg.startsWith("-")) result.action = arg;
else if (["--capability", "--source", "--home"].includes(arg)) {
const value = argv[++index];
if (!value || value.startsWith("--")) throw bootstrapError(`${arg} requires a value`, "BOOTSTRAP_USAGE");
if (arg === "--capability") result.capabilities.push(value);
else if (arg === "--source") result.source = value;
else result.home = value;
} else throw bootstrapError(`Unknown option: ${arg}`, "BOOTSTRAP_USAGE");
}
if (result.help) return result;
if (result.action === "preview" && result.yes) throw bootstrapError("preview cannot be combined with --yes", "BOOTSTRAP_USAGE");
if (!result.capabilities.length) throw bootstrapError("At least one --capability is required", "BOOTSTRAP_USAGE");
result.capabilities = [...new Set(result.capabilities)].sort();
for (const capability of result.capabilities) {
if (!CAPABILITIES.has(capability)) throw bootstrapError(`Unknown capability: ${capability}`, "BOOTSTRAP_USAGE");
}
const expanded = new Set(result.capabilities);
const pending = [...expanded];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!expanded.has(dependency)) {
expanded.add(dependency);
pending.push(dependency);
}
}
}
result.capabilities = [...expanded].sort();
return result;
}
function validateManifest(manifest, target) {
if (manifest?.schemaVersion !== BOOTSTRAP_SCHEMA_VERSION || manifest?.version !== BOOTSTRAP_RUNTIME_VERSION ||
manifest?.skillApi !== "2.0" || typeof manifest?.targets !== "object" ||
!sameGraph(manifest.capabilityComponents, CAPABILITY_COMPONENTS) ||
!sameGraph(manifest.componentDependencies, COMPONENT_DEPENDENCIES)) {
throw bootstrapError("Official runtime manifest is incompatible", "BOOTSTRAP_MANIFEST_INVALID");
}
const targetRecord = manifest.targets[target];
const expected = Object.keys(COMPONENT_DEPENDENCIES)
.filter((component) => component !== "ios" || target.startsWith("darwin-"))
.sort();
if (!targetRecord || typeof targetRecord.components !== "object" ||
JSON.stringify(Object.keys(targetRecord.components).sort()) !== JSON.stringify(expected)) {
throw bootstrapError(`No valid official runtime artifact for ${target}`, "BOOTSTRAP_ARTIFACT_UNAVAILABLE");
}
for (const [component, artifact] of Object.entries(targetRecord.components)) {
if (!artifact || artifact.format !== "tar.gz" || !/^[a-f0-9]{64}$/.test(artifact.sha256) ||
!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 1 || artifact.bytes > 2 * 1024 * 1024 * 1024) {
throw bootstrapError(`Invalid ${component} artifact for ${target}`, "BOOTSTRAP_ARTIFACT_UNAVAILABLE");
}
assertOfficialReleaseAssetUrl(artifact.url);
if (artifact.cosignBundleUrl) {
assertOfficialReleaseAssetUrl(artifact.cosignBundleUrl);
if (artifact.certificateIdentity !== OFFICIAL_CERTIFICATE_IDENTITY ||
artifact.certificateOidcIssuer !== GITHUB_OIDC_ISSUER) {
throw bootstrapError("Cosign metadata does not bind the official GStack release workflow", "BOOTSTRAP_MANIFEST_INVALID");
}
} else if (artifact.certificateIdentity || artifact.certificateOidcIssuer) {
throw bootstrapError("Cosign certificate metadata requires a bundle URL", "BOOTSTRAP_MANIFEST_INVALID");
}
}
return targetRecord;
}
function sameGraph(actual, expected) {
if (!actual || typeof actual !== "object" || Array.isArray(actual)) return false;
const normalize = (graph) => Object.fromEntries(Object.entries(graph)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, values]) => [key, Array.isArray(values) ? [...values].sort() : values]));
return JSON.stringify(normalize(actual)) === JSON.stringify(normalize(expected));
}
function selectedComponents(capabilities) {
const selected = new Set(["core"]);
for (const capability of capabilities) {
for (const component of CAPABILITY_COMPONENTS[capability] ?? []) selected.add(component);
}
const pending = [...selected];
while (pending.length) {
for (const dependency of COMPONENT_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort();
}
function buildComponentPlan(manifest, target, capabilities, reusable) {
const components = selectedComponents(capabilities);
const retained = new Set(reusable?.components ?? []);
const downloads = components
.filter((component) => !retained.has(component))
.map((component) => ({ component, artifact: manifest.targets[target].components[component] }));
const downloadBytes = downloads.reduce((total, item) => total + item.artifact.bytes, 0);
return {
target,
version: manifest.version,
capabilities,
components,
reusedComponents: components.filter((component) => retained.has(component)),
downloads,
downloadBytes,
};
}
function printComponentPlan(stdout, plan) {
stdout.write(`GStack optional runtime ${plan.version} for ${plan.target}\n`);
stdout.write(`Capabilities: ${plan.capabilities.join(", ")}\n`);
stdout.write(`Components: ${plan.components.join(", ")}\n`);
if (plan.reusedComponents.length) stdout.write(`Reusing: ${plan.reusedComponents.join(", ")}\n`);
stdout.write(`Download: ${plan.downloadBytes} bytes across ${plan.downloads.length} component(s)\n`);
}
async function inspectReusableRuntime(home, version) {
const versions = path.join(home, "versions");
const pointer = JSON.parse(await fs.readFile(path.join(versions, "current.json"), "utf8"));
if (pointer?.schemaVersion !== 2 || pointer?.status !== "active" ||
typeof pointer.current !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(pointer.current)) return null;
const root = path.join(versions, pointer.current);
const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
await assertNoLinks(root);
const files = [];
const seen = new Set();
for (const entry of bundle.files) {
const relative = entry?.path;
if (typeof relative !== "string" || !relative || relative.includes("\\") || path.posix.isAbsolute(relative) ||
path.posix.normalize(relative) !== relative || relative.split("/").includes("..") || seen.has(relative) ||
!Number.isSafeInteger(entry.size) || entry.size < 0 || !/^[a-f0-9]{64}$/.test(entry.sha256)) return null;
seen.add(relative);
const file = path.join(root, ...relative.split("/"));
const fileStat = await fs.lstat(file).catch(() => null);
if (!fileStat?.isFile() || fileStat.isSymbolicLink() || fileStat.size !== entry.size ||
await sha256File(file) !== entry.sha256) return null;
files.push(relative);
}
return { root, components, files };
}
async function seedReusableRuntime(reusable, destination, claimedFiles) {
for (const relative of reusable.files) {
if (claimedFiles.has(relative)) throw bootstrapError(`Runtime components overlap at ${relative}`, "BOOTSTRAP_MANIFEST_INVALID");
claimedFiles.add(relative);
const target = path.join(destination, ...relative.split("/"));
await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
await fs.copyFile(path.join(reusable.root, ...relative.split("/")), target, fsConstants.COPYFILE_EXCL);
}
}
function sha256File(file) {
return new Promise((resolve, reject) => {
const hash = createHash("sha256");
const stream = createReadStream(file);
stream.on("error", reject);
stream.on("data", (chunk) => hash.update(chunk));
stream.on("end", () => resolve(hash.digest("hex")));
});
}
async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url);
if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value;
}
async function downloadVerified(fetch_, url, destination, expectedSha256, expectedBytes) {
const response = await fetch_(url, { redirect: "follow" });
assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Artifact download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED");
const hash = createHash("sha256");
const file = await fs.open(destination, "wx", 0o600);
let total = 0;
try {
if (response.body?.getReader) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > 2 * 1024 * 1024 * 1024) throw bootstrapError("Runtime artifact exceeds the 2 GiB safety limit", "BOOTSTRAP_DOWNLOAD_FAILED");
hash.update(value);
await file.write(value);
}
} else {
const bytes = new Uint8Array(await response.arrayBuffer());
total = bytes.byteLength;
hash.update(bytes);
await file.write(bytes);
}
} catch (error) {
await file.close();
await fs.rm(destination, { force: true });
throw error;
}
await file.close();
if (total !== expectedBytes) {
await fs.rm(destination, { force: true });
throw bootstrapError(`Runtime artifact size mismatch (expected ${expectedBytes}, received ${total})`, "BOOTSTRAP_INTEGRITY_FAILED");
}
const actual = hash.digest("hex");
if (actual !== expectedSha256) {
await fs.rm(destination, { force: true });
throw bootstrapError("Runtime artifact SHA-256 mismatch", "BOOTSTRAP_INTEGRITY_FAILED");
}
}
async function verifyCosignWhenAvailable(archive, artifact, temporary, options) {
if (!artifact.cosignBundleUrl) {
options.stdout.write("No Cosign bundle declared; continuing with verified release-manifest SHA-256.\n");
return;
}
const available = await run(options.cosignCommand ?? "cosign", ["version"], { capture: true }).then(() => true, () => false);
if (!available) {
options.stdout.write("Cosign metadata is available but Cosign is not installed; SHA-256 verification succeeded.\n");
return;
}
const bundle = path.join(temporary, "cosign.bundle");
const response = await options.fetch(artifact.cosignBundleUrl, { redirect: "follow" });
assertFinalDownloadUrl(response.url || artifact.cosignBundleUrl);
if (!response.ok) throw bootstrapError("Cosign bundle download failed", "BOOTSTRAP_ATTESTATION_FAILED");
await fs.writeFile(bundle, new Uint8Array(await response.arrayBuffer()), { mode: 0o600, flag: "wx" });
const args = ["verify-blob", "--bundle", bundle];
if (artifact.certificateIdentity) args.push("--certificate-identity", artifact.certificateIdentity);
if (artifact.certificateOidcIssuer) args.push("--certificate-oidc-issuer", artifact.certificateOidcIssuer);
args.push(archive);
await run(options.cosignCommand ?? "cosign", args);
options.stdout.write("Verified Cosign release attestation.\n");
}
async function extractTarSafely(archive, destination, options) {
const tar = options.tarCommand ?? "tar";
const listing = await run(tar, ["-tzf", archive], { capture: true });
for (const name of listing.stdout.split(/\r?\n/).filter(Boolean)) {
const normalized = name.replaceAll("\\", "/");
if (normalized.includes("\0") || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) ||
normalized.split("/").includes("..")) {
throw bootstrapError("Runtime archive contains an unsafe path", "BOOTSTRAP_ARCHIVE_UNSAFE");
}
}
const verbose = await run(tar, ["-tvzf", archive], { capture: true });
for (const line of verbose.stdout.split(/\r?\n/).filter(Boolean)) {
if (!/^[-d]/.test(line)) {
throw bootstrapError("Runtime archive contains a link or special-file entry", "BOOTSTRAP_ARCHIVE_UNSAFE");
}
}
await run(tar, ["-xzf", archive, "-C", destination]);
}
async function installFromSource(source, parsed, options) {
const physical = await fs.realpath(path.resolve(source));
const installer = path.join(physical, "runtime", "install.js");
const stat = await fs.lstat(installer).catch(() => null);
if (!stat?.isFile() || stat.isSymbolicLink()) throw bootstrapError("Source does not contain a safe runtime installer", "BOOTSTRAP_SOURCE_INVALID");
const args = [installer, "--source", physical, "--install-now", "--yes", "--capabilities", parsed.capabilities.join(",")];
if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared");
await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0;
}
function platformTarget(platform, arch, libc) {
if (!["darwin", "linux", "win32"].includes(platform) || !["arm64", "x64"].includes(arch)) {
throw bootstrapError(`Unsupported platform: ${platform}-${arch}`, "BOOTSTRAP_PLATFORM_UNSUPPORTED");
}
if (platform === "linux" && libc !== "glibc") {
throw bootstrapError(
"Official GStack runtime artifacts currently require glibc Linux; pure Agent Skills remain portable and the reviewed-source fallback may be used explicitly",
"BOOTSTRAP_PLATFORM_UNSUPPORTED",
);
}
return `${platform === "win32" ? "windows" : platform}-${arch}`;
}
function detectLinuxLibc(platform) {
if (platform !== "linux") return null;
const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
return report?.header?.glibcVersionRuntime ? "glibc" : "musl";
}
function safeArtifactRoot(extracted, relative) {
if (typeof relative !== "string" || !relative || path.isAbsolute(relative) || relative.split(/[\\/]/).includes("..")) {
throw bootstrapError("Manifest contains an unsafe artifact root", "BOOTSTRAP_MANIFEST_INVALID");
}
const target = path.resolve(extracted, relative);
if (path.relative(extracted, target).startsWith(`..${path.sep}`)) throw bootstrapError("Artifact root escaped extraction", "BOOTSTRAP_ARCHIVE_UNSAFE");
return target;
}
async function mergeComponentRoot(source, destination, claimedFiles, component) {
async function visit(relative = "") {
for (const entry of await fs.readdir(path.join(source, relative), { withFileTypes: true })) {
const child = relative ? `${relative}/${entry.name}` : entry.name;
const from = path.join(source, ...child.split("/"));
const to = path.join(destination, ...child.split("/"));
if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) {
throw bootstrapError(`Runtime component ${component} contains a link or special file`, "BOOTSTRAP_ARCHIVE_UNSAFE");
}
if (entry.isDirectory()) {
await fs.mkdir(to, { recursive: true, mode: 0o700 });
await visit(child);
} else {
if (claimedFiles.has(child)) {
throw bootstrapError(`Runtime components overlap at ${child}`, "BOOTSTRAP_MANIFEST_INVALID");
}
claimedFiles.add(child);
await fs.mkdir(path.dirname(to), { recursive: true, mode: 0o700 });
await fs.copyFile(from, to, fsConstants.COPYFILE_EXCL);
}
}
}
await visit();
}
async function assertNoLinks(root) {
const pending = [root];
while (pending.length) {
const target = pending.pop();
const stat = await fs.lstat(target);
if (stat.isSymbolicLink()) throw bootstrapError("Runtime archive contains a symbolic link", "BOOTSTRAP_ARCHIVE_UNSAFE");
if (stat.isDirectory()) for (const child of await fs.readdir(target)) pending.push(path.join(target, child));
}
}
function assertOfficialUrl(value, options = {}) {
const url = new URL(value);
if (url.protocol !== "https:" || url.username || url.password || !ALLOWED_DOWNLOAD_HOSTS.has(url.hostname)) {
throw bootstrapError("Bootstrap downloads are restricted to official GitHub release hosts", "BOOTSTRAP_URL_BLOCKED");
}
if (options.manifest && url.hostname !== "github.com") throw bootstrapError("Manifest must come from the official GitHub release", "BOOTSTRAP_URL_BLOCKED");
}
function assertOfficialReleaseAssetUrl(value) {
assertOfficialUrl(value);
const url = new URL(value);
if (url.hostname !== "github.com" || !url.pathname.startsWith(OFFICIAL_RELEASE_PREFIX)) {
throw bootstrapError("Runtime assets must come from the official versioned GStack release", "BOOTSTRAP_URL_BLOCKED");
}
}
function assertFinalDownloadUrl(value) {
assertOfficialUrl(value);
}
function run(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { shell: false, windowsHide: true, stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit" });
let stdout = "";
let stderr = "";
child.stdout?.setEncoding("utf8");
child.stderr?.setEncoding("utf8");
child.stdout?.on("data", (chunk) => { stdout += chunk; });
child.stderr?.on("data", (chunk) => { stderr += chunk; });
child.once("error", reject);
child.once("close", (code) => code === 0 ? resolve({ stdout, stderr }) : reject(bootstrapError(`${command} failed (${code})`, "BOOTSTRAP_COMMAND_FAILED")));
});
}
function bootstrapError(message, code) {
const error = new Error(message);
error.code = code;
return error;
}
function formatBytes(bytes) {
const units = ["B", "KiB", "MiB", "GiB"];
let value = bytes;
let index = 0;
while (value >= 1024 && index < units.length - 1) {
value /= 1024;
index += 1;
}
return `${value.toFixed(index === 0 ? 0 : value >= 10 ? 1 : 2)} ${units[index]} (${bytes} bytes)`;
}
function usage() {
return "Usage: node runtime-bootstrap.mjs install --capability <name> [--capability <name>...]\n" +
" node runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name>\n\n" +
"Downloads only a versioned official GStack runtime release and never enrolls a coding host.\n" +
"--source is a developer-only fallback for a checkout you have reviewed and trust.\n";
}
async function isDirectExecution() {
if (!process.argv[1]) return false;
const [modulePath, invokedPath] = await Promise.all([
fs.realpath(fileURLToPath(import.meta.url)),
fs.realpath(path.resolve(process.argv[1])).catch(() => path.resolve(process.argv[1])),
]);
return modulePath === invokedPath;
}
if (await isDirectExecution()) {
process.exitCode = await main();
}