diff --git a/README.md b/README.md index 3c06eb6..ecbf6d2 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ Choose a provider before first use. The local Lima provider is the default: ```bash +./rootcell --init-env macos-lima + # Store the default Bedrock provider key in Keychain. security add-generic-password -a "$USER" -s aws-bedrock-api-key -w "" @@ -141,19 +143,13 @@ notes. ### AWS EC2 -Create or edit the instance `.env` before the first run: +Initialize the instance `.env` before the first run: ```bash -mkdir -p instances/aws-dev -cp .env.defaults instances/aws-dev/.env -cat >> instances/aws-dev/.env <<'EOF' -ROOTCELL_VM_PROVIDER=aws-ec2 -ROOTCELL_AWS_PROFILE=your-profile -ROOTCELL_AWS_REGION=us-east-1 -ROOTCELL_AWS_CONTROL_CIDR=auto -EOF - -./rootcell --instance aws-dev +./rootcell -i aws-dev --init-env aws-ec2 +./rootcell -i aws-dev edit env + +./rootcell -i aws-dev ``` See [AWS EC2 provider](src/rootcell/providers/aws-ec2/README.md) for Terraform @@ -187,6 +183,7 @@ state root. ./rootcell # open a bash shell inside the agent VM ./rootcell pi # run pi directly ./rootcell -- nix flake update # run any command inside the agent VM +./rootcell edit env # edit the instance .env in $EDITOR ./rootcell edit http # edit the HTTPS allowlist in $EDITOR ./rootcell edit dns # edit the DNS allowlist in $EDITOR ./rootcell edit ssh # edit the SSH allowlist in $EDITOR @@ -199,8 +196,11 @@ state root. ./rootcell spy # tail formatted Bedrock Runtime traffic ./rootcell spy --raw # include sanitized raw JSON bodies too ./rootcell spy --tui # browse Bedrock Runtime traffic interactively +./rootcell -i aws-dev --init-env aws-ec2 # initialize a provider-specific instance .env +./rootcell -i local --init-env macos-lima # initialize an explicit local Lima .env ./rootcell --instance dev # open the dev instance shell +./rootcell --instance dev edit env # edit the dev instance environment ./rootcell --instance dev edit dns # edit the dev instance DNS allowlist ./rootcell --instance dev allow # reload only the dev instance allowlists ``` @@ -379,8 +379,25 @@ same instance settings. ### Environment -`./rootcell` seeds `/.env` from `.env.defaults` on first run. Edit -that file for instance-local settings such as: +Use `./rootcell -i --init-env ` to create the selected +instance directory, seed allowlists and secret mappings, and write a +provider-specific `/.env`: + +```bash +./rootcell -i local --init-env macos-lima +./rootcell -i aws-dev --init-env aws-ec2 +``` + +The supported provider types are `macos-lima` and `aws-ec2`. `macos-lima` +writes `ROOTCELL_VM_PROVIDER=lima`; `aws-ec2` writes `ROOTCELL_VM_PROVIDER=aws-ec2` +plus `ROOTCELL_AWS_PROFILE`, `ROOTCELL_AWS_REGION`, and +`ROOTCELL_AWS_CONTROL_CIDR`. The AWS profile and region default from your +current host environment when available, otherwise to `default` and `us-east-1`. + +Normal `./rootcell` entry also seeds `/.env` from `.env.defaults` +on first run if it does not already exist. Edit that file for instance-local +settings such as these, or run `./rootcell -i edit env` to open it in +`$EDITOR`: ```sh ROOTCELL_VM_PROVIDER=lima diff --git a/src/rootcell/args.ts b/src/rootcell/args.ts index 723e918..895d08c 100644 --- a/src/rootcell/args.ts +++ b/src/rootcell/args.ts @@ -4,8 +4,11 @@ import { isRootcellSubcommand, ROOTCELL_SUBCOMMANDS, type RootcellSubcommand } f import { listRootcellInstanceNames, validateInstanceName } from "./instance.ts"; import { parseSchema } from "./schema.ts"; import { + ParsedRootcellInitEnvArgsSchema, ParsedRootcellHandledArgsSchema, ParsedRootcellRunArgsSchema, + ROOTCELL_INIT_ENV_PROVIDER_TYPES, + RootcellInitEnvProviderTypeSchema, SpyOptionsSchema, type ParsedRootcellArgs, type SpyOptions, @@ -15,6 +18,7 @@ const DEFAULT_SPY_OPTIONS: SpyOptions = { raw: false, dedupe: true, tui: false } interface GlobalArgs { readonly instance?: string | readonly string[]; + readonly initEnv?: string | readonly string[]; } interface GuestArgs extends GlobalArgs { @@ -29,7 +33,7 @@ interface SpyArgs extends GlobalArgs { } interface EditArgs extends GlobalArgs { - readonly allowlist?: string; + readonly target?: string; } type ParserArgv = Argv; @@ -127,6 +131,11 @@ function createParser(args: readonly string[]): Argv { default: "default", normalize: false, }) + .option("init-env", { + choices: ROOTCELL_INIT_ENV_PROVIDER_TYPES, + describe: "initialize the selected instance environment for a provider", + type: "string", + }) // yargs' completion request flag is normally implicit. With // unknown-options-as-args enabled for command pass-through, it must be // declared so completion requests still reach yargs. @@ -141,12 +150,12 @@ function createParser(args: readonly string[]): Argv { .command(...rootcellSubcommand("stop")) .command(...rootcellSubcommand("remove")) .command( - "edit ", + "edit ", subcommandDescription("edit"), (argv: ParserArgv) => argv - .positional("allowlist", { - choices: ["http", "https", "dns", "ssh"], - describe: "allowlist to edit", + .positional("target", { + choices: ["env", "http", "https", "dns", "ssh"], + describe: "instance file to edit", type: "string", }) .demandCommand(0, 0) @@ -187,9 +196,11 @@ function createParser(args: readonly string[]): Argv { .example("$0", "open an interactive shell inside the agent VM") .example("$0 pi", "run pi inside the agent VM") .example("$0 -- nix flake update", "run any command inside the agent VM") + .example("$0 edit env", "edit the default instance environment in $EDITOR") .example("$0 edit http", "edit the HTTPS allowlist for the default instance") .example("$0 --instance dev edit dns", "edit the DNS allowlist for the dev instance") .example("$0 --instance dev allow", "reload allowlists for the dev instance") + .example("$0 --instance aws-dev --init-env aws-ec2", "initialize an AWS EC2 instance environment") .example("$0 list", "list rootcell VMs and their current state") .example("$0 stop --instance dev", "stop the dev instance VMs") .example("$0 remove --instance dev", "delete the dev instance VM state") @@ -212,8 +223,20 @@ export function parseRootcellArgs(args: readonly string[]): ParsedRootcellArgs { } const subcommand = parsedSubcommand(argv); + const initEnv = lastString(argv.initEnv); + if (initEnv !== undefined) { + if (subcommand !== undefined || stringArray(argv.command).length > 0 || stringArray(argv["--"]).length > 0) { + throw new Error("--init-env cannot be combined with a rootcell command"); + } + return parseSchema(ParsedRootcellInitEnvArgsSchema, { + kind: "init-env", + instanceName: instanceName(argv), + providerType: parseSchema(RootcellInitEnvProviderTypeSchema, initEnv, "invalid --init-env provider"), + }, "invalid parsed rootcell args"); + } + if (subcommand !== undefined) { - const rest = subcommand === "edit" ? [argString((argv as ArgumentsCamelCase).allowlist)] : []; + const rest = subcommand === "edit" ? [argString((argv as ArgumentsCamelCase).target)] : []; return parseSchema(ParsedRootcellRunArgsSchema, { kind: "run", instanceName: instanceName(argv), @@ -259,11 +282,11 @@ function firstRootcellToken(args: readonly string[]): string | undefined { if (arg === undefined || arg === "--") { return undefined; } - if (arg === "--instance" || arg === "-i" || arg === "--get-yargs-completions") { + if (arg === "--instance" || arg === "-i" || arg === "--init-env" || arg === "--get-yargs-completions") { index += 1; continue; } - if (arg.startsWith("--instance=") || (arg.startsWith("-i") && arg.length > 2)) { + if (arg.startsWith("--instance=") || arg.startsWith("--init-env=") || (arg.startsWith("-i") && arg.length > 2)) { continue; } if (arg.startsWith("-")) { diff --git a/src/rootcell/init-env.ts b/src/rootcell/init-env.ts new file mode 100644 index 0000000..119e75d --- /dev/null +++ b/src/rootcell/init-env.ts @@ -0,0 +1,92 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { instancePaths, seedRootcellInstanceFiles } from "./instance.ts"; +import { + ROOTCELL_AWS_CONTROL_CIDR_ENV, + ROOTCELL_AWS_PROFILE_ENV, + ROOTCELL_AWS_REGION_ENV, + ROOTCELL_VM_PROVIDER_ENV, +} from "./providers/aws-ec2-config.ts"; +import type { RootcellInitEnvProviderType } from "./types.ts"; + +interface EnvAssignment { + readonly key: string; + readonly value: string; + readonly overwrite?: boolean; +} + +export interface InitEnvResult { + readonly envPath: string; + readonly providerType: RootcellInitEnvProviderType; +} + +export function initRootcellInstanceEnv( + repoDir: string, + instanceName: string, + providerType: RootcellInitEnvProviderType, + log: (message: string) => void, + env: NodeJS.ProcessEnv = process.env, +): InitEnvResult { + seedRootcellInstanceFiles(repoDir, instanceName, log, env); + const paths = instancePaths(repoDir, instanceName, env); + const existing = existsSync(paths.envPath) ? readFileSync(paths.envPath, "utf8") : ""; + const content = upsertEnvAssignments( + existing, + providerEnvAssignments(providerType, env), + `# Provider initialized by rootcell --init-env ${providerType}.`, + ); + if (content !== existing) { + writeFileSync(paths.envPath, content, { encoding: "utf8", mode: 0o600 }); + } + log(`initialized ${instanceName} environment for ${providerType} at ${paths.envPath}`); + return { envPath: paths.envPath, providerType }; +} + +function providerEnvAssignments(providerType: RootcellInitEnvProviderType, env: NodeJS.ProcessEnv): readonly EnvAssignment[] { + if (providerType === "aws-ec2") { + return [ + { key: ROOTCELL_VM_PROVIDER_ENV, value: "aws-ec2", overwrite: true }, + { key: ROOTCELL_AWS_PROFILE_ENV, value: env[ROOTCELL_AWS_PROFILE_ENV] ?? env.AWS_PROFILE ?? "default" }, + { key: ROOTCELL_AWS_REGION_ENV, value: env[ROOTCELL_AWS_REGION_ENV] ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1" }, + { key: ROOTCELL_AWS_CONTROL_CIDR_ENV, value: env[ROOTCELL_AWS_CONTROL_CIDR_ENV] ?? "auto" }, + ]; + } + return [ + { key: ROOTCELL_VM_PROVIDER_ENV, value: "lima", overwrite: true }, + ]; +} + +function upsertEnvAssignments( + text: string, + assignments: readonly EnvAssignment[], + comment: string, +): string { + const pending = new Map(assignments.map((assignment) => [assignment.key, assignment])); + const lines = text.length === 0 ? [] : text.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n"); + const nextLines = lines.map((line) => { + if (line.startsWith("#")) { + return line; + } + const equalsAt = line.indexOf("="); + const key = equalsAt === -1 ? line : line.slice(0, equalsAt); + const assignment = pending.get(key); + if (assignment === undefined) { + return line; + } + pending.delete(key); + return assignment.overwrite === true ? `${key}=${assignment.value}` : line; + }); + + if (pending.size > 0) { + if (nextLines.length > 0 && nextLines[nextLines.length - 1] !== "") { + nextLines.push(""); + } + nextLines.push(comment); + for (const assignment of assignments) { + if (pending.has(assignment.key)) { + nextLines.push(`${assignment.key}=${assignment.value}`); + } + } + } + + return `${nextLines.join("\n")}\n`; +} diff --git a/src/rootcell/metadata.ts b/src/rootcell/metadata.ts index bd0ea92..83d38c6 100644 --- a/src/rootcell/metadata.ts +++ b/src/rootcell/metadata.ts @@ -7,7 +7,7 @@ export const ROOTCELL_SUBCOMMANDS: readonly SubcommandMetadata[] = [ { name: "list", description: "list rootcell VMs and their current state" }, { name: "stop", description: "stop the selected rootcell instance VMs" }, { name: "remove", description: "stop the selected instance and delete VM state" }, - { name: "edit", description: "open an allowlist in $EDITOR" }, + { name: "edit", description: "open an instance config file in $EDITOR" }, { name: "provision", description: "re-copy files and rebuild both VMs" }, { name: "allow", description: "hot-reload allowlists into the firewall VM" }, { name: "pubkey", description: "print the agent VM SSH public key" }, diff --git a/src/rootcell/providers/aws-ec2/README.md b/src/rootcell/providers/aws-ec2/README.md index 7c7d6c2..af0183c 100644 --- a/src/rootcell/providers/aws-ec2/README.md +++ b/src/rootcell/providers/aws-ec2/README.md @@ -6,7 +6,14 @@ AWS infrastructure with a generated Terraform module. ## Required Instance Environment -Set these in the instance `.env` before first use: +Initialize the instance `.env` before first use: + +```sh +./rootcell -i aws-dev --init-env aws-ec2 +./rootcell -i aws-dev edit env +``` + +The command writes these provider settings: ```sh ROOTCELL_VM_PROVIDER=aws-ec2 diff --git a/src/rootcell/providers/macos-lima-user-v2/README.md b/src/rootcell/providers/macos-lima-user-v2/README.md index 7b89eaa..e798be6 100644 --- a/src/rootcell/providers/macos-lima-user-v2/README.md +++ b/src/rootcell/providers/macos-lima-user-v2/README.md @@ -10,8 +10,13 @@ HTTPS, SSH, and the host control path through the firewall. ## Required Instance Environment -The Lima provider is the default. This is optional, but useful when you want the -instance `.env` to be explicit: +The Lima provider is the default. To make an instance `.env` explicit, run: + +```sh +./rootcell -i local --init-env macos-lima +``` + +That command writes: ```sh ROOTCELL_VM_PROVIDER=lima diff --git a/src/rootcell/rootcell.test.ts b/src/rootcell/rootcell.test.ts index 24c6cc3..da13433 100644 --- a/src/rootcell/rootcell.test.ts +++ b/src/rootcell/rootcell.test.ts @@ -4,6 +4,7 @@ import { parseRootcellArgs } from "./args.ts"; import { ROOTCELL_SUBCOMMANDS } from "./metadata.ts"; import { loadDotEnv, parseSecretMappings } from "./env.ts"; import { resolveHostTool } from "./host-tools.ts"; +import { initRootcellInstanceEnv } from "./init-env.ts"; import { buildConfig, formatVmList, rootcellMain } from "./rootcell.ts"; import { deriveVmNames, instancePaths, listRootcellVmInstanceNames, loadRootcellInstance, seedRootcellInstanceFiles } from "./instance.ts"; import { runCapture } from "./process.ts"; @@ -114,7 +115,17 @@ describe("rootcell argument parsing", () => { }); }); - test("parses edit allowlist subcommands", () => { + test("parses edit subcommands", () => { + const env = runArgs(["edit", "env"]); + expectRunArgs(env); + expect(env).toEqual({ + kind: "run", + instanceName: "default", + subcommand: "edit", + rest: ["env"], + spyOptions: { raw: false, dedupe: true, tui: false }, + }); + const http = runArgs(["edit", "http"]); expectRunArgs(http); expect(http).toEqual({ @@ -224,6 +235,21 @@ describe("rootcell argument parsing", () => { expect(() => parseRootcellArgs(["spy", "--bogus"])).toThrow("Unknown argument: bogus"); }); + test("parses init-env provider mode", () => { + expect(parseRootcellArgs(["-i", "aws-test", "--init-env", "aws-ec2"])).toEqual({ + kind: "init-env", + instanceName: "aws-test", + providerType: "aws-ec2", + }); + expect(parseRootcellArgs(["--instance=local", "--init-env", "macos-lima"])).toEqual({ + kind: "init-env", + instanceName: "local", + providerType: "macos-lima", + }); + expect(() => parseRootcellArgs(["--init-env", "aws-ec2", "list"])).toThrow("--init-env cannot be combined"); + expect(() => parseRootcellArgs(["--init-env", "unknown"])).toThrow(); + }); + test("rejects unknown rootcell flags before commands", () => { expect(() => parseRootcellArgs(["--bogus", "provision"])).toThrow("Unknown argument: bogus"); expect(() => parseRootcellArgs(["--raw", "spy"])).toThrow("Unknown argument: raw"); @@ -1410,6 +1436,80 @@ describe("instance state", () => { rmSync(repo, { recursive: true, force: true }); } }); + + test("initializes AWS EC2 instance environment", () => { + const repo = makeInstanceRepo(); + try { + const result = initRootcellInstanceEnv(repo, "aws-test", "aws-ec2", ignoreLog, { + AWS_PROFILE: "sandbox", + AWS_REGION: "us-west-2", + }); + + expect(result.envPath).toBe(join(repo, "instances", "aws-test", ".env")); + expect(readFileSync(result.envPath, "utf8")).toBe([ + "AWS_REGION=us-east-1", + "", + "# Provider initialized by rootcell --init-env aws-ec2.", + "ROOTCELL_VM_PROVIDER=aws-ec2", + "ROOTCELL_AWS_PROFILE=sandbox", + "ROOTCELL_AWS_REGION=us-west-2", + "ROOTCELL_AWS_CONTROL_CIDR=auto", + "", + ].join("\n")); + expect(readFileSync(join(repo, "instances", "aws-test", "secrets.env"), "utf8")).toContain("macos-keychain"); + expect(readFileSync(join(repo, "instances", "aws-test", "proxy", "allowed-dns.txt"), "utf8")).toBe("\n"); + } finally { + rmSync(repo, { recursive: true, force: true }); + } + }); + + test("initializes explicit macOS Lima instance environment", () => { + const repo = makeInstanceRepo(); + try { + const result = initRootcellInstanceEnv(repo, "local", "macos-lima", ignoreLog, {}); + + expect(readFileSync(result.envPath, "utf8")).toBe([ + "AWS_REGION=us-east-1", + "", + "# Provider initialized by rootcell --init-env macos-lima.", + "ROOTCELL_VM_PROVIDER=lima", + "", + ].join("\n")); + } finally { + rmSync(repo, { recursive: true, force: true }); + } + }); + + test("init-env preserves existing AWS provider settings", () => { + const repo = makeInstanceRepo(); + try { + seedRootcellInstanceFiles(repo, "aws-test", ignoreLog, {}); + const envPath = join(repo, "instances", "aws-test", ".env"); + writeFileSync(envPath, [ + "ROOTCELL_VM_PROVIDER=lima", + "ROOTCELL_AWS_PROFILE=prod", + "ROOTCELL_AWS_REGION=eu-central-1", + "", + ].join("\n"), "utf8"); + + initRootcellInstanceEnv(repo, "aws-test", "aws-ec2", ignoreLog, { + AWS_PROFILE: "sandbox", + AWS_REGION: "us-west-2", + }); + + expect(readFileSync(envPath, "utf8")).toBe([ + "ROOTCELL_VM_PROVIDER=aws-ec2", + "ROOTCELL_AWS_PROFILE=prod", + "ROOTCELL_AWS_REGION=eu-central-1", + "", + "# Provider initialized by rootcell --init-env aws-ec2.", + "ROOTCELL_AWS_CONTROL_CIDR=auto", + "", + ].join("\n")); + } finally { + rmSync(repo, { recursive: true, force: true }); + } + }); }); describe("rootcell edit command", () => { @@ -1444,6 +1544,37 @@ describe("rootcell edit command", () => { rmSync(repo, { recursive: true, force: true }); } }); + + test("opens the selected instance environment in EDITOR", async () => { + const repo = makeInstanceRepo(); + const oldRootcellStateDir = process.env.ROOTCELL_STATE_DIR; + const oldEditor = process.env.EDITOR; + const oldRecord = process.env.ROOTCELL_EDITOR_RECORD; + try { + mkdirSync(join(repo, "src", "bin"), { recursive: true }); + writeFileSync(join(repo, "flake.nix"), "{}\n", "utf8"); + + const editor = join(repo, "editor.sh"); + const record = join(repo, "opened.txt"); + writeFileSync(editor, "#!/bin/sh\nprintf '%s\\n' \"$1\" > \"$ROOTCELL_EDITOR_RECORD\"\n", "utf8"); + chmodSync(editor, 0o700); + + process.env.ROOTCELL_STATE_DIR = join(repo, ".state"); + process.env.EDITOR = editor; + process.env.ROOTCELL_EDITOR_RECORD = record; + + const status = await rootcellMain(["--instance", "dev", "edit", "env"], join(repo, "src", "bin", "rootcell.ts")); + + expect(status).toBe(0); + expect(readFileSync(record, "utf8").trim()).toBe(join(repo, ".state", "dev", ".env")); + expect(readFileSync(join(repo, ".state", "dev", ".env"), "utf8")).toBe("AWS_REGION=us-east-1\n"); + } finally { + restoreEnv("ROOTCELL_STATE_DIR", oldRootcellStateDir); + restoreEnv("EDITOR", oldEditor); + restoreEnv("ROOTCELL_EDITOR_RECORD", oldRecord); + rmSync(repo, { recursive: true, force: true }); + } + }); }); describe("reload helper", () => { diff --git a/src/rootcell/rootcell.ts b/src/rootcell/rootcell.ts index 927a4b3..dd7672f 100644 --- a/src/rootcell/rootcell.ts +++ b/src/rootcell/rootcell.ts @@ -10,6 +10,7 @@ import { dirname, join, resolve } from "node:path"; import { parseRootcellArgs } from "./args.ts"; import { loadDotEnv, nixString, parseSecretMappings } from "./env.ts"; import { DEFAULT_IMAGE_MANIFEST_URL } from "./images.ts"; +import { initRootcellInstanceEnv } from "./init-env.ts"; import { deriveVmNames, instancePaths, @@ -28,13 +29,15 @@ import { RootcellConfigSchema, type RootcellConfig, type RootcellInstance, type const GUEST_USER = "luser"; -const EDIT_ALLOWLIST_FILES = { +const EDIT_PROXY_FILES = { http: "allowed-https.txt", https: "allowed-https.txt", dns: "allowed-dns.txt", ssh: "allowed-ssh.txt", } as const; +const EDIT_TARGETS = ["env", "http", "https", "dns", "ssh"] as const; + const VM_FILES: VmFileSet = { agent: [ "flake.nix", @@ -948,19 +951,18 @@ function runEditCommand( instanceName: string, editTarget: string | undefined, ): number { - const file = editTarget === undefined ? undefined : EDIT_ALLOWLIST_FILES[editTarget as keyof typeof EDIT_ALLOWLIST_FILES]; - if (file === undefined) { - log(`unknown allowlist '${editTarget ?? ""}' (expected http, dns, or ssh)`); + if (!isEditTarget(editTarget)) { + log(`unknown edit target '${editTarget ?? ""}' (expected ${EDIT_TARGETS.join(", ")})`); return 2; } const editor = env.EDITOR; if (editor === undefined || editor.length === 0) { - log("EDITOR is not set; set EDITOR to edit allowlists."); + log("EDITOR is not set; set EDITOR to edit instance files."); return 1; } seedRootcellInstanceFiles(repoDir, instanceName, log, env); - const path = join(instancePaths(repoDir, instanceName, env).proxyDir, file); + const path = editPath(instancePaths(repoDir, instanceName, env), editTarget); log(`opening ${path}`); return runInherited("sh", ["-c", "exec $EDITOR \"$1\"", "sh", path], { allowFailure: true, @@ -968,6 +970,17 @@ function runEditCommand( }).status; } +function isEditTarget(value: string | undefined): value is (typeof EDIT_TARGETS)[number] { + return EDIT_TARGETS.some((target) => target === value); +} + +function editPath(paths: ReturnType, target: (typeof EDIT_TARGETS)[number]): string { + if (target === "env") { + return paths.envPath; + } + return join(paths.proxyDir, EDIT_PROXY_FILES[target]); +} + function missingVmEntries(instanceName: string): readonly VmListEntry[] { const vmNames = deriveVmNames(instanceName); return [ @@ -991,6 +1004,10 @@ export async function rootcellMain(args: readonly string[], importMetaPath: stri } try { + if (parsed.kind === "init-env") { + initRootcellInstanceEnv(repoDir, parsed.instanceName, parsed.providerType, log, process.env); + return 0; + } if (parsed.subcommand === "list") { return await runListCommand(repoDir, process.env, parsed.instanceName, hasInstanceFlag(args)); } diff --git a/src/rootcell/types.ts b/src/rootcell/types.ts index c2871a5..c6f0972 100644 --- a/src/rootcell/types.ts +++ b/src/rootcell/types.ts @@ -11,6 +11,12 @@ export const RootcellVmProviderIdSchema = z.enum(["lima", "aws-ec2"]); export type RootcellVmProviderId = z.infer; +export const RootcellInitEnvProviderTypeSchema = z.enum(["macos-lima", "aws-ec2"]); + +export const ROOTCELL_INIT_ENV_PROVIDER_TYPES = RootcellInitEnvProviderTypeSchema.options; + +export type RootcellInitEnvProviderType = z.infer; + const AwsControlCidrSchema = z.string().refine( (value) => value === "auto" || isIpv4Cidr(value), { message: "must be 'auto' or an IPv4 CIDR block" }, @@ -126,12 +132,21 @@ export const ParsedRootcellHandledArgsSchema = z.object({ export type ParsedRootcellHandledArgs = Readonly>; +export const ParsedRootcellInitEnvArgsSchema = z.object({ + kind: z.literal("init-env"), + instanceName: NonEmptyStringSchema, + providerType: RootcellInitEnvProviderTypeSchema, +}); + +export type ParsedRootcellInitEnvArgs = Readonly>; + export const ParsedRootcellArgsSchema = z.discriminatedUnion("kind", [ ParsedRootcellRunArgsSchema, ParsedRootcellHandledArgsSchema, + ParsedRootcellInitEnvArgsSchema, ]); -export type ParsedRootcellArgs = ParsedRootcellRunArgs | ParsedRootcellHandledArgs; +export type ParsedRootcellArgs = ParsedRootcellRunArgs | ParsedRootcellHandledArgs | ParsedRootcellInitEnvArgs; export const InstanceStateSchema = z.object({ schemaVersion: z.literal(1),