import { parseStrictFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { InvalidArgumentError, type Command } from "commander"; import { validateDiskSize } from "../../fleet/cell-profile.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; import { collectOption, parseStrictPositiveIntOption } from "../program/helpers.js"; type FleetRuntimeModule = typeof import("./commands.runtime.js"); const fleetRuntimeLoader = createLazyImportLoader( () => import("./commands.runtime.js"), ); function loadFleetRuntime(): Promise { return fleetRuntimeLoader.load(); } function parseContainerRuntime(value: string): "docker" | "podman" { if (value === "docker" || value === "podman") { return value; } throw new InvalidArgumentError("--runtime must be docker or podman."); } function parsePort(value: string): number { const port = parseStrictPositiveIntOption(value, "--port"); if (port > 65_535) { throw new InvalidArgumentError("--port must be between 1 and 65535."); } return port; } function parseCpus(value: string): string { const cpus = parseStrictFiniteNumber(value); if (cpus === undefined || cpus <= 0) { throw new InvalidArgumentError("--cpus must be a positive number."); } return value; } function parseDisk(value: string): string { try { return validateDiskSize(value); } catch (error) { throw new InvalidArgumentError( error instanceof Error ? error.message : "Invalid --disk value.", ); } } function parseNetwork(value: string): "bridge" | "internal" { if (value === "bridge" || value === "internal") { return value; } throw new InvalidArgumentError("--network must be bridge or internal."); } export function registerFleetCli(program: Command): void { const fleet = program .command("fleet") .description("Provision and manage isolated tenant cells (experimental)"); fleet .command("create") .description("Create an isolated tenant cell") .argument("", "Tenant slug") .option("--image ", "Container image", "ghcr.io/openclaw/openclaw:latest") .option( "--runtime ", "Container runtime (docker or podman)", parseContainerRuntime, "docker", ) .option("--port ", "Host loopback port (default: allocate from 19100)", parsePort) .option("--memory ", "Container memory limit", "2g") .option("--cpus ", "Container CPU limit", parseCpus, "2") .option( "--disk ", "Cap the container writable layer (requires overlay2+XFS pquota, btrfs, or zfs)", parseDisk, ) .option( "--network ", "Container egress network (bridge or internal)", parseNetwork, "bridge", ) .option( "--pids-limit ", "Container process limit", (value: string) => parseStrictPositiveIntOption(value, "--pids-limit"), 512, ) .option("--env ", "Pass an environment variable to the cell", collectOption, []) .option("--gateway-token ", "Use an existing Gateway token") .option("--no-start", "Create the container without starting it") .option("--json", "Output JSON", false) .action( async ( tenant: string, options: { image: string; runtime: "docker" | "podman"; port?: number; memory: string; cpus: string; disk?: string; network: "bridge" | "internal"; pidsLimit: number; env: string[]; gatewayToken?: string; start: boolean; json: boolean; }, ) => { const runtime = await loadFleetRuntime(); await runtime.runFleetCreateCommand({ tenant, ...options }); }, ); fleet .command("backup") .description("Back up one tenant cell as a host operator (archive contains secrets)") .argument("", "Tenant slug") .option("--out ", "Archive output path or directory") .option("--max-bytes ", "Maximum archive input bytes", (value: string) => parseStrictPositiveIntOption(value, "--max-bytes"), ) .option("--json", "Output JSON", false) .action(async (tenant: string, options: { out?: string; maxBytes?: number; json: boolean }) => { const runtime = await loadFleetRuntime(); await runtime.runFleetBackupCommand({ tenant, ...options }); }); fleet .command("restore") .description("Restore one tenant cell as a host operator (archive contains secrets)") .argument("", "Tenant slug") .requiredOption("--from ", "Fleet backup archive") .option("--force", "Stop a running cell and replace its state", false) .option("--max-bytes ", "Maximum extracted bytes", (value: string) => parseStrictPositiveIntOption(value, "--max-bytes"), ) .option("--json", "Output JSON", false) .action( async ( tenant: string, options: { from: string; force: boolean; maxBytes?: number; json: boolean }, ) => { const runtime = await loadFleetRuntime(); await runtime.runFleetRestoreCommand({ tenant, ...options }); }, ); fleet .command("doctor") .description("Audit fleet cells without changing them") .argument("[tenant]", "Tenant slug") .option("--json", "Output JSON", false) .action(async (tenant: string | undefined, options: { json: boolean }) => { const runtime = await loadFleetRuntime(); await runtime.runFleetDoctorCommand({ tenant, ...options }); }); fleet .command("list") .alias("ls") .description("List tenant cells") .option("--json", "Output JSON", false) .action(async (options: { json: boolean }) => { const runtime = await loadFleetRuntime(); await runtime.runFleetListCommand(options); }); fleet .command("status") .description("Show tenant cell status") .argument("", "Tenant slug") .option("--json", "Output JSON", false) .action(async (tenant: string, options: { json: boolean }) => { const runtime = await loadFleetRuntime(); await runtime.runFleetStatusCommand({ tenant, ...options }); }); fleet .command("logs") .description("Stream tenant cell container logs") .argument("", "Tenant slug") .option("--follow", "Follow log output", false) .option("--timestamps", "Show timestamps", false) .option("--tail ", "Number of lines to show", (value: string) => parseStrictPositiveIntOption(value, "--tail"), ) .option("--since ", "Show logs since a duration or timestamp") .action( async ( tenant: string, options: { follow: boolean; timestamps: boolean; tail?: number; since?: string }, ) => { const runtime = await loadFleetRuntime(); await runtime.runFleetLogsCommand({ tenant, ...options }); }, ); for (const action of ["start", "stop", "restart"] as const) { fleet .command(action) .description(`${action[0]?.toUpperCase()}${action.slice(1)} a tenant cell`) .argument("", "Tenant slug") .action(async (tenant: string) => { const runtime = await loadFleetRuntime(); await runtime.runFleetLifecycleCommand({ action, tenant }); }); } fleet .command("upgrade") .description("Replace a tenant cell with a freshly pulled image") .argument("", "Tenant slug") .option("--image ", "Replacement image (default: recorded image)") .action(async (tenant: string, options: { image?: string }) => { const runtime = await loadFleetRuntime(); await runtime.runFleetUpgradeCommand({ tenant, ...options }); }); fleet .command("rm") .description("Remove a tenant cell") .argument("", "Tenant slug") .option("--purge-data", "Delete the tenant data directory", false) .option("--force", "Remove a running cell", false) .action(async (tenant: string, options: { purgeData: boolean; force: boolean }) => { const runtime = await loadFleetRuntime(); await runtime.runFleetRemoveCommand({ tenant, ...options }); }); }