Files
openclaw/src/cli/worktrees-cli.ts
2026-07-14 23:06:26 -07:00

137 lines
4.6 KiB
TypeScript

import type { Command } from "commander";
import { getTerminalTableWidth, renderTable } from "../../packages/terminal-core/src/table.js";
import { createManagedWorktreeOwnerProtection } from "../agents/worktrees/owner-protection.js";
import { managedWorktrees, resolveWorktreeCleanupLimits } from "../agents/worktrees/service.js";
import type { ManagedWorktreeRecord } from "../agents/worktrees/types.js";
import { getRuntimeConfig } from "../config/config.js";
import { defaultRuntime } from "../runtime.js";
import { applyParentDefaultHelpAction } from "./program/parent-default-help.js";
type JsonOption = { json?: boolean };
function printJson(value: unknown): void {
// writeJson targets process.stdout directly; runtime.log routes through the console
// logger and never reaches piped stdout, which breaks `--json | jq` consumers.
defaultRuntime.writeJson(value);
}
function printRecord(record: ManagedWorktreeRecord, json: boolean): void {
if (json) {
printJson(record);
return;
}
defaultRuntime.log(`${record.id}\t${record.path}`);
}
export function registerWorktreesCli(program: Command): void {
const worktrees = program
.command("worktrees")
.description("Create, inspect, restore, and clean up managed worktrees");
worktrees
.command("list")
.description("List active and restorable managed worktrees")
.option("--json", "Output JSON", false)
.action(async (opts: JsonOption) => {
const records = await managedWorktrees.list();
if (opts.json) {
printJson({ worktrees: records });
return;
}
if (records.length === 0) {
defaultRuntime.log("No managed worktrees.");
return;
}
defaultRuntime.log(
renderTable({
width: getTerminalTableWidth(),
columns: [
{ key: "ID", header: "ID", minWidth: 16, flex: true },
{ key: "Repo", header: "Repo", minWidth: 18, flex: true },
{ key: "Branch", header: "Branch", minWidth: 18, flex: true },
{ key: "Status", header: "Status", minWidth: 10 },
],
rows: records.map((record) => ({
ID: record.id,
Repo: record.repoRoot,
Branch: record.branch,
Status: record.removedAt ? "restorable" : "active",
})),
}).trimEnd(),
);
});
worktrees
.command("create")
.description("Create a managed worktree")
.argument("<repoRoot>", "Source git checkout")
.option("--name <name>", "Managed worktree name")
.option("--base-ref <ref>", "Git ref to branch from")
.option("--json", "Output JSON", false)
.action(async (repoRoot: string, opts: JsonOption & { name?: string; baseRef?: string }) => {
printRecord(
await managedWorktrees.create({
repoRoot,
name: opts.name,
baseRef: opts.baseRef,
ownerKind: "manual",
}),
opts.json === true,
);
});
worktrees
.command("remove")
.description("Snapshot and remove a managed worktree")
.argument("<id>", "Managed worktree id")
.option("--force", "Remove even if snapshot creation fails", false)
.option("--json", "Output JSON", false)
.action(async (id: string, opts: JsonOption & { force?: boolean }) => {
const result = await managedWorktrees.remove({
id,
reason: "manual-delete",
force: opts.force,
});
if (opts.json) {
printJson(result);
} else {
defaultRuntime.log(
result.snapshotError
? `Removed ${id} without a snapshot: ${result.snapshotError}`
: `Removed ${id}.`,
);
}
});
worktrees
.command("restore")
.description("Restore a managed worktree from its snapshot")
.argument("<id>", "Managed worktree id")
.option("--json", "Output JSON", false)
.action(async (id: string, opts: JsonOption) => {
printRecord(await managedWorktrees.restore({ id }), opts.json === true);
});
worktrees
.command("gc")
.description("Run managed worktree cleanup now")
.option("--json", "Output JSON", false)
.action(async (opts: JsonOption) => {
const cfg = getRuntimeConfig();
const limits = resolveWorktreeCleanupLimits(cfg.worktrees);
const result = await managedWorktrees.gc({
limits,
shouldProtectOwner: createManagedWorktreeOwnerProtection(cfg),
});
if (opts.json) {
printJson(result);
} else {
defaultRuntime.log(
`Removed ${result.removed.length}; deleted ${result.orphansDeleted} orphans; pruned ${result.snapshotsPruned} snapshots.`,
);
}
});
applyParentDefaultHelpAction(worktrees);
}