feat(cli): add kilo worktree create/list/remove commands and TUI alias

This commit is contained in:
Bruno Agatao
2026-08-06 16:23:43 +02:00
parent 7c25f5b5a9
commit b57cfdce43
4 changed files with 86 additions and 1 deletions
@@ -94,7 +94,9 @@ function waitForWorktreeEvent(
return { promise: deferred.promise, cancel: cleanup }
}
async function resolveWorktree(name: string, root: string, timeoutMs = 10 * 60_000) {
// Exported for `kilo worktree create` (worktree.ts), which calls this and
// exits instead of going on to launch the TUI.
export async function resolveWorktree(name: string, root: string, timeoutMs = 10 * 60_000) {
const { Worktree } = await import("@/worktree")
const { GlobalBus } = await import("@/bus/global")
const { InstanceState } = await import("@/effect/instance-state")
@@ -0,0 +1,80 @@
// kilocode_change - new file
// `kilo worktree list`/`remove`: CLI-side counterpart to `kilo --worktree <name>`
// (tui-worktree.ts) and the TUI's `/worktree` alias for the workspaces dialog
// (packages/tui/src/app.tsx). All three go through the same `Worktree.Service`.
import path from "path"
import { Effect } from "effect"
import { cmd } from "@/cli/cmd/cmd"
import { CliError, effectCmd, fail } from "@/cli/effect-cmd"
import { UI } from "@/cli/ui"
import { errorMessage } from "@/util/error"
import { slugify } from "@/kilocode/cli/cmd/tui-worktree"
import { Worktree } from "@/worktree"
const wrapErr = (message: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(Effect.mapError((error) => new CliError({ message: `${message}: ${errorMessage(error)}` })))
const listWorktrees = Worktree.Service.use((svc) => svc.list()).pipe(wrapErr("Failed to list worktrees"))
export const WorktreeCommand = cmd({
command: "worktree",
describe: "manage git worktrees",
builder: (yargs) =>
yargs.command(WorktreeCreateCommand).command(WorktreeListCommand).command(WorktreeRemoveCommand).demandCommand(),
async handler() {},
})
export const WorktreeCreateCommand = cmd({
command: "create <name>",
describe: "create (or reuse) a git worktree by name",
builder: (yargs) => yargs.positional("name", { type: "string", demandOption: true }),
async handler(args) {
// Plain cmd(), not effectCmd(): resolveWorktree loads/disposes its own
// instance context (it's shared with `kilo --worktree`'s pre-TUI-launch
// path in tui-worktree.ts), so it can't run inside effectCmd's own.
const { resolveWorktree } = await import("@/kilocode/cli/cmd/tui-worktree")
await resolveWorktree(args.name, process.cwd()).catch((error) => {
UI.error(errorMessage(error))
process.exitCode = 1
})
},
})
export const WorktreeListCommand = effectCmd({
command: "list",
describe: "list git worktrees for the current project",
handler: Effect.fn("Cli.worktree.list")(function* () {
const list = yield* listWorktrees
if (!list.length) {
UI.println("No worktrees found.")
return
}
for (const w of list) UI.println(`${w.name}${w.branch ? ` (${w.branch})` : ""} ${w.directory}`)
}),
})
export const WorktreeRemoveCommand = effectCmd({
command: "remove <name>",
describe: "remove a git worktree by name",
builder: (yargs) => yargs.positional("name", { type: "string", demandOption: true }),
handler: Effect.fn("Cli.worktree.remove")(function* (args) {
const slug = slugify(args.name)
if (!slug) {
yield* fail(`Invalid worktree name "${args.name}"`)
return
}
const list = yield* listWorktrees
// Matches the reuse logic in tui-worktree.ts: list() remaps `name` to the
// project ID when a worktree's basename collides with the primary
// checkout's, so also match on the directory basename.
const found = list.find((w) => w.name.toLowerCase() === slug || path.basename(w.directory).toLowerCase() === slug)
if (!found) {
yield* fail(`No worktree named "${args.name}" found.`)
return
}
yield* Worktree.Service.use((svc) => svc.remove({ directory: found.directory })).pipe(
wrapErr(`Failed to remove worktree "${args.name}"`),
)
UI.println(`Removed worktree "${found.name}" at ${found.directory}`)
}),
})
@@ -11,6 +11,7 @@ import { DaemonCommand } from "@/kilocode/cli/cmd/daemon"
import { DevSetupCommand, DevAliasCommand } from "@/kilocode/cli/dev-setup"
import { RemoteCommand } from "@/cli/cmd/remote"
import { ConfigCommand as ConfigCLICommand } from "@/cli/cmd/config"
import { WorktreeCommand } from "@/kilocode/cli/cmd/worktree"
const log = Log.create({ service: "kilocode.cli" })
@@ -56,6 +57,7 @@ export namespace KiloCli {
.command(RemoteCommand)
.command(DaemonCommand)
.command(ConfigCLICommand)
.command(WorktreeCommand)
if (InstallationBuildKind !== "release") cli.command(DevSetupCommand).command(DevAliasCommand)
// Safe self-reference: `cli` is a typed parameter and yargs `.command()` returns the same
// instance, so the help command can resolve the fully-built root at handler time. This also
+1
View File
@@ -627,6 +627,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
category: "Workspace",
hidden: !Flag.KILO_EXPERIMENTAL_WORKSPACES,
slashName: "workspaces",
slashAliases: ["worktree", "worktrees"], // kilocode_change - `kilo --worktree` worktrees are workspaces too
run: () => {
dialog.replace(() => <DialogWorkspaceList />)
},