Files
2026-08-27 21:09:14 +00:00

127 lines
3.8 KiB
TypeScript

#!/usr/bin/env bun
import { $ } from "bun";
import { parseArgs } from "node:util";
import { installBareAgent, installTartAgent } from "./lib/agent";
import { bake } from "./lib/bake";
import { ciUserExists, enableAutoLogin, ensureCiUser } from "./lib/ci-user";
import { config } from "./lib/config";
import {
bootstrapToolchain,
brewInstall,
disableRemoteManagement,
hardenSshd,
installBuildkiteAgent,
installSelf,
joinTailnet,
setHostname,
tailnetSummary,
} from "./lib/host";
import { consoleUser, fail, step } from "./lib/shell";
const usage = `usage:
main.ts provision <hostname> <tart|bare> [--tags <tailscale tags>] converge a freshly imaged host
main.ts setup-user create the auto-login ${config.ciUser} user
main.ts bake [--base <image>] [--ref <bun ref>] build ${config.tart.image} (run as ${config.ciUser})
main.ts install-agent [--release N] [--spawn N] write agent config and launchd jobs
provision, setup-user and install-agent need passwordless sudo.`;
const { positionals, values } = parseArgs({
allowPositionals: true,
options: {
base: { type: "string", default: config.tart.baseRemote },
ref: { type: "string", default: config.bun.ref },
release: { type: "string", default: String(config.tart.guestRelease) },
spawn: { type: "string", default: String(config.tart.spawn) },
tags: { type: "string" },
},
});
const [subcommand, ...args] = positionals;
const agentOptions = { release: Number(values.release), spawn: Number(values.spawn) };
switch (subcommand) {
case "provision":
await provision(args[0] ?? fail(usage), parseMode(args[1]));
break;
case "setup-user":
await setupUser();
break;
case "bake":
await bake({ base: values.base!, ref: values.ref! });
break;
case "install-agent":
await installTartAgent(agentOptions);
break;
default:
fail(usage);
}
function parseMode(mode: string | undefined): "tart" | "bare" {
if (mode === "tart" || mode === "bare") return mode;
return fail(usage);
}
async function setupUser(): Promise<void> {
await ensureCiUser();
await enableAutoLogin();
}
async function provision(name: string, mode: "tart" | "bare"): Promise<void> {
step("remote management off, sshd key-only");
await disableRemoteManagement();
await hardenSshd();
step(`hostname ${name}`);
await setHostname(name);
step("tailscale");
await joinTailnet(name, values.tags);
step(`buildkite-agent ${config.buildkiteAgent.version}`);
await installBuildkiteAgent();
step(`install scripts to ${config.installDir}`);
await installSelf();
if (mode === "tart") await provisionTart();
else await provisionBare();
step("done");
console.log(`tailscale: ${await tailnetSummary()}`);
}
async function provisionTart(): Promise<void> {
if (process.arch !== "arm64") fail("tart mode needs Apple Silicon; use bare on Intel");
const user = config.ciUser;
const main = `${config.installDir}/main.ts`;
step("tart");
await brewInstall("cirruslabs/cli/tart");
step(`${user} user with auto-login`);
const existed = await ciUserExists();
await setupUser();
if ((await consoleUser()) !== user) {
console.log(
`${existed ? "" : `created ${user}; `}reboot so ${user} owns the console session, then re-run this command:`,
);
console.log(" sudo shutdown -r now");
return;
}
step(`bake ${config.tart.image} as ${user}`);
await $`sudo -u ${user} -H /usr/local/bin/bun ${main} bake --base ${values.base!} --ref ${values.ref!}`;
step("agent");
await installTartAgent(agentOptions);
}
async function provisionBare(): Promise<void> {
step("toolchain (scripts/bootstrap.sh)");
await bootstrapToolchain();
step("agent (scripts/agent.mjs)");
await installBareAgent();
}