openclaw / src /cli /fleet-cli /register.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
eb3f11e verified
Raw
History Blame Contribute Delete
8.26 kB
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<FleetRuntimeModule>(
() => import("./commands.runtime.js"),
);
function loadFleetRuntime(): Promise<FleetRuntimeModule> {
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>", "Tenant slug")
.option("--image <ref>", "Container image", "ghcr.io/openclaw/openclaw:latest")
.option(
"--runtime <runtime>",
"Container runtime (docker or podman)",
parseContainerRuntime,
"docker",
)
.option("--port <port>", "Host loopback port (default: allocate from 19100)", parsePort)
.option("--memory <limit>", "Container memory limit", "2g")
.option("--cpus <count>", "Container CPU limit", parseCpus, "2")
.option(
"--disk <size>",
"Cap the container writable layer (requires overlay2+XFS pquota, btrfs, or zfs)",
parseDisk,
)
.option(
"--network <mode>",
"Container egress network (bridge or internal)",
parseNetwork,
"bridge",
)
.option(
"--pids-limit <count>",
"Container process limit",
(value: string) => parseStrictPositiveIntOption(value, "--pids-limit"),
512,
)
.option("--env <KEY=VAL>", "Pass an environment variable to the cell", collectOption, [])
.option("--gateway-token <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>", "Tenant slug")
.option("--out <path>", "Archive output path or directory")
.option("--max-bytes <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>", "Tenant slug")
.requiredOption("--from <path>", "Fleet backup archive")
.option("--force", "Stop a running cell and replace its state", false)
.option("--max-bytes <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>", "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>", "Tenant slug")
.option("--follow", "Follow log output", false)
.option("--timestamps", "Show timestamps", false)
.option("--tail <count>", "Number of lines to show", (value: string) =>
parseStrictPositiveIntOption(value, "--tail"),
)
.option("--since <value>", "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>", "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>", "Tenant slug")
.option("--image <ref>", "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>", "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 });
});
}