mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
feat(cli): add kilo worktree create/list/remove commands and TUI alias
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 />)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user