perf(cli): optimize startup time

This commit is contained in:
marius-kilocode
2026-08-25 13:05:02 +02:00
parent 9d262793a8
commit 4ca951c885
17 changed files with 513 additions and 112 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Improve CLI cold and warm startup time.
+27 -21
View File
@@ -1,37 +1,42 @@
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import { RunCommand } from "./cli/cmd/run"
import { GenerateCommand } from "./cli/cmd/generate"
// kilocode_change - upstream account console intentionally omitted; KiloCli registers `kilo console` for local settings
import { ProvidersCommand } from "./cli/cmd/providers"
import { AgentCommand } from "./cli/cmd/agent"
import { UpgradeCommand } from "./cli/cmd/upgrade"
import { UninstallCommand } from "./cli/cmd/uninstall"
import { ModelsCommand } from "./cli/cmd/models"
import { UI } from "./cli/ui"
import { TuiThreadCommand } from "./cli/cmd/tui" // kilocode_change - yargs requires the default command builder eagerly
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { FormatError } from "./cli/error"
import { ServeCommand } from "./cli/cmd/serve"
import { DebugCommand } from "./cli/cmd/debug"
import { StatsCommand } from "./cli/cmd/stats"
import { McpCommand } from "./cli/cmd/mcp"
import { GithubCommand } from "./cli/cmd/github"
import { ExportCommand } from "./cli/cmd/export"
import { ImportCommand } from "./cli/cmd/import"
import { AttachCommand } from "./cli/cmd/attach"
import { TuiThreadCommand } from "./cli/cmd/tui"
import { AcpCommand } from "./cli/cmd/acp"
import { EOL } from "os"
// kilocode_change - upstream web command intentionally omitted; Kilo does not ship an embedded web UI
import { PrCommand } from "./cli/cmd/pr"
import { SessionCommand } from "./cli/cmd/session"
import { DbCommand } from "./cli/cmd/db"
import { errorMessage } from "./util/error"
import { PluginCommand } from "./cli/cmd/plug"
import { Heap } from "./cli/heap"
import { KiloCli } from "@/kilocode/cli/setup" // kilocode_change
import * as Log from "@opencode-ai/core/util/log" // kilocode_change
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process" // kilocode_change
// kilocode_change start - defer heavy command implementations until yargs selects them
import {
AcpCommand,
AgentCommand,
AttachCommand,
DbCommand,
DebugCommand,
ExportCommand,
GenerateCommand,
GithubCommand,
ImportCommand,
McpCommand,
ModelsCommand,
PluginCommand,
PrCommand,
ProvidersCommand,
RunCommand,
ServeCommand,
SessionCommand,
StatsCommand,
UninstallCommand,
UpgradeCommand,
waitForLazyCommands,
} from "@/kilocode/cli/lazy-commands"
// kilocode_change end
const args = hideBin(process.argv)
const metadata = ensureProcessMetadata("main") // kilocode_change - correlate logs across the CLI and TUI worker
@@ -119,6 +124,7 @@ let cli = yargs(args) // kilocode_change
// kilocode_change start - register Kilo-specific commands after the upstream chain
cli = KiloCli.register(cli)
await waitForLazyCommands() // kilocode_change - yargs completion invokes builders synchronously
cli = cli
// kilocode_change end
.fail((msg, err) => {
@@ -47,6 +47,7 @@ import { Snapshot } from "@/snapshot"
import { cumulativeSessionDiff } from "@/kilocode/session-portability/cumulative-diff"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" // kilocode_change
import { KiloShutdown } from "@/kilocode/cli/shutdown"
async function provide<R>(input: { directory: string; fn: () => R }): Promise<R> {
const { provide } = await import("@/kilocode/instance")
@@ -257,11 +258,17 @@ export namespace KiloSessions {
() => ingest.drain(),
(err) => log.warn("ingest drain failed", { err }),
)
KiloShutdown.register(drainIngest)
export async function drainIngestForShutdown() {
await drainIngest()
}
/** @internal - lifecycle regression coverage */
export function _queueIngestForTest(sessionId: string) {
return ingest.sync(sessionId, [{ type: "session_status", data: { status: "idle" } }])
}
const remoteEnabled = process.env["KILO_REMOTE"] === "1"
let remote: { conn: RemoteWS.Connection; sender: RemoteSender.Sender } | undefined
let enabling: Promise<void> | undefined
@@ -0,0 +1,30 @@
import { Account } from "@/account/account"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import { makeRuntime } from "@/effect/run-service"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect, Layer, Option } from "effect"
const account = Layer.mock(Account.Service)({
active: () => Effect.succeed(Option.none()),
})
const config = makeRuntime(Config.Service, AppNodeBuilder.build(Config.node, [[Account.node, account]]))
const auth = makeRuntime(Auth.Service, Auth.defaultLayer)
export namespace KiloCliBootstrapRuntime {
export function getGlobal() {
return config.runPromise((service) => service.getGlobal())
}
export function getAuth() {
return auth.runPromise((service) => service.get("kilo"))
}
export function setAuth(info: Auth.Info) {
return auth.runPromise((service) => service.set("kilo", info))
}
export async function dispose() {
await Promise.all([config.dispose(), auth.dispose()])
}
}
@@ -0,0 +1,172 @@
import type { Argv, ArgumentsCamelCase, CommandModule } from "yargs"
type Load<T, U> = () => Promise<CommandModule<T, U>>
const completion = process.argv.includes("--get-yargs-completions")
const tasks: Promise<void>[] = []
let selected = false
function build<T, U>(command: CommandModule<T, U>, args: Argv<T>) {
if (!command.builder) return args as unknown as Argv<U>
if (typeof command.builder === "function") return command.builder(args)
return args.options(command.builder) as unknown as Argv<U>
}
export function waitForLazyCommands() {
return Promise.all(tasks)
}
export function hasLazyCommandSelection() {
return selected
}
export function lazy<T = {}, U = {}>(input: {
command: string | readonly string[]
aliases?: string | readonly string[]
describe?: string | false
load: Load<T, U>
}): CommandModule<T, U> {
const state: { command?: CommandModule<T, U>; task?: Promise<CommandModule<T, U>> } = {}
const load = () => (state.task ??= input.load())
if (completion) {
tasks.push(
load().then((command) => {
state.command = command
}),
)
}
return {
command: input.command,
aliases: input.aliases,
describe: input.describe,
builder: ((args: Argv<T>) => {
selected = true
if (state.command) return build(state.command, args)
return load().then((command) => build(command, args))
}) as never,
async handler(args: ArgumentsCamelCase<U>) {
const command = await load()
await command.handler(args)
},
}
}
export const AcpCommand = lazy({
command: "acp",
describe: "start ACP (Agent Client Protocol) server",
load: async () => (await import("@/cli/cmd/acp")).AcpCommand,
})
export const AttachCommand = lazy({
command: "attach <url>",
describe: "attach to a running kilo server",
load: async () => (await import("@/cli/cmd/attach")).AttachCommand,
})
export const RunCommand = lazy({
command: "run [message..]",
describe: "run kilo with a message",
load: async () => (await import("@/cli/cmd/run")).RunCommand,
})
export const GenerateCommand = lazy({
command: "generate",
load: async () => (await import("@/cli/cmd/generate")).GenerateCommand,
})
export const McpCommand = lazy({
command: "mcp",
describe: "manage MCP (Model Context Protocol) servers",
load: async () => (await import("@/cli/cmd/mcp")).McpCommand,
})
export const DebugCommand = lazy({
command: "debug",
describe: "debugging and troubleshooting tools",
load: async () => (await import("@/cli/cmd/debug")).DebugCommand,
})
export const ProvidersCommand = lazy({
command: "auth",
aliases: ["providers"],
describe: "manage AI providers and credentials",
load: async () => (await import("@/cli/cmd/providers")).ProvidersCommand,
})
export const AgentCommand = lazy({
command: "agent",
describe: "manage agents",
load: async () => (await import("@/cli/cmd/agent")).AgentCommand,
})
export const UpgradeCommand = lazy({
command: "upgrade [target]",
describe: "upgrade kilo to the latest or a specific version",
load: async () => (await import("@/cli/cmd/upgrade")).UpgradeCommand,
})
export const UninstallCommand = lazy({
command: "uninstall",
describe: "uninstall kilo and remove all related files",
load: async () => (await import("@/cli/cmd/uninstall")).UninstallCommand,
})
export const ServeCommand = lazy({
command: "serve",
describe: "starts a headless kilo server",
load: async () => (await import("@/cli/cmd/serve")).ServeCommand,
})
export const ModelsCommand = lazy({
command: "models [provider]",
describe: "list all available models",
load: async () => (await import("@/cli/cmd/models")).ModelsCommand,
})
export const StatsCommand = lazy({
command: "stats",
describe: "show token usage and cost statistics",
load: async () => (await import("@/cli/cmd/stats")).StatsCommand,
})
export const ExportCommand = lazy({
command: "export [sessionID]",
describe: "export session data as JSON",
load: async () => (await import("@/cli/cmd/export")).ExportCommand,
})
export const ImportCommand = lazy({
command: "import <file>",
describe: "import session data from JSON file or URL",
load: async () => (await import("@/cli/cmd/import")).ImportCommand,
})
export const SessionCommand = lazy({
command: "session",
describe: "manage sessions",
load: async () => (await import("@/cli/cmd/session")).SessionCommand,
})
export const GithubCommand = lazy({
command: "github",
describe: "manage GitHub agent",
load: async () => (await import("@/cli/cmd/github")).GithubCommand,
})
export const PrCommand = lazy({
command: "pr",
describe: "manage pull requests",
load: async () => (await import("@/cli/cmd/pr")).PrCommand,
})
export const PluginCommand = lazy({
command: "plugin <module>",
aliases: ["plug"],
describe: "install plugin and update config",
load: async () => (await import("@/cli/cmd/plug")).PluginCommand,
})
export const DbCommand = lazy({
command: "db",
describe: "database tools",
load: async () => (await import("@/cli/cmd/db")).DbCommand,
})
@@ -0,0 +1,67 @@
import { lazy } from "@/kilocode/cli/lazy-commands"
export const KiloConsoleCommand = lazy({
command: "console",
describe: "open or stop the local Kilo Console (deprecated)",
load: async () => (await import("@/kilocode/cli/cmd/console")).KiloConsoleCommand,
})
export const CloudCommand = lazy({
command: "cloud",
describe: "run Cloud Agent tasks",
load: async () => (await import("@/kilocode/cli/cmd/cloud")).CloudCommand,
})
export const RollCallCommand = lazy({
command: "roll-call <filter>",
describe: "batch-test text models matching a filter for connectivity and latency",
load: async () => (await import("@/kilocode/cli/cmd/roll-call")).RollCallCommand,
})
export const ProfileCommand = lazy({
command: "profile",
describe: "show Kilo account profile",
load: async () => (await import("@/kilocode/cli/cmd/profile")).ProfileCommand,
})
export const RemoteCommand = lazy({
command: "remote",
describe: "enable remote connection for real-time session relay",
load: async () => (await import("@/cli/cmd/remote")).RemoteCommand,
})
export const DaemonCommand = lazy({
command: "daemon",
describe: "manage the local kilo daemon",
load: async () => (await import("@/kilocode/cli/cmd/daemon")).DaemonCommand,
})
export const ConfigCLICommand = lazy({
command: "config",
describe: "configuration tools",
load: async () => (await import("@/cli/cmd/config")).ConfigCommand,
})
export const WorktreeCommand = lazy({
command: "worktree",
describe: "manage git worktrees",
load: async () => (await import("@/kilocode/cli/cmd/worktree")).WorktreeCommand,
})
export const PtySmokeCommand = lazy({
command: "__pty-smoke",
describe: false,
load: async () => (await import("@/kilocode/cli/cmd/pty-smoke")).PtySmokeCommand,
})
export const DevSetupCommand = lazy({
command: "dev-setup",
describe: "install a `kilodev` shell alias for this checkout",
load: async () => (await import("@/kilocode/cli/dev-setup")).DevSetupCommand,
})
export const DevAliasCommand = lazy({
command: "dev-alias [shell]",
describe: false,
load: async () => (await import("@/kilocode/cli/dev-setup")).DevAliasCommand,
})
+44 -35
View File
@@ -1,39 +1,26 @@
import type { Argv } from "yargs"
import type { Auth } from "@/auth"
import * as Log from "@opencode-ai/core/util/log"
import { InstallationBuildKind, InstallationVersion } from "@opencode-ai/core/installation/version"
import { KiloShutdown } from "@/kilocode/cli/shutdown"
import { createHelpCommand } from "@/kilocode/help-command"
import { KiloConsoleCommand } from "@/kilocode/cli/cmd/console"
import { CloudCommand } from "@/kilocode/cli/cmd/cloud"
import { RollCallCommand } from "@/kilocode/cli/cmd/roll-call"
import { ProfileCommand } from "@/kilocode/cli/cmd/profile"
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"
import { PtySmokeCommand } from "@/kilocode/cli/cmd/pty-smoke"
import { hasLazyCommandSelection } from "@/kilocode/cli/lazy-commands"
import {
CloudCommand,
ConfigCLICommand,
DaemonCommand,
DevAliasCommand,
DevSetupCommand,
KiloConsoleCommand,
ProfileCommand,
PtySmokeCommand,
RemoteCommand,
RollCallCommand,
WorktreeCommand,
} from "@/kilocode/cli/lazy-kilo-commands"
const log = Log.create({ service: "kilocode.cli" })
// Process-level ingest drain for non-TUI commands (`kilo run`, etc.).
// KiloCli.shutdown() runs KiloShutdown before disposeAllInstances — preserve that order.
// Registered at setup load time (not inside shutdown()) so the task is always present.
// Dynamic import keeps setup.ts's own static import graph unchanged: consumers that load
// setup.ts under partial module mocks (e.g. cli-shutdown tests whose @/auth mock omits
// OAUTH_DUMMY_KEY) would otherwise fail to link the provider/plugin chain. Dynamic import
// returns the same in-process module singleton, so the drained queue is the one that
// received events. Task try/catch covers dynamic-import failure outside the shared drain
// guard; the drain itself never rejects.
KiloShutdown.register(async () => {
try {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
await KiloSessions.drainIngestForShutdown()
} catch (err) {
log.warn("ingest drain failed", { err })
}
})
// All Kilo-specific CLI customization lives here so the shared upstream entrypoint
// (src/index.ts) only needs a handful of thin call-sites behind kilocode_change markers.
// This keeps index.ts close to upstream and reduces merge conflicts on every sync.
@@ -46,6 +33,11 @@ KiloShutdown.register(async () => {
// top level, with implementation imports inside their handlers.
export namespace KiloCli {
let info = false
let narrow = false
export function workerTui(opts: { [key: string]: unknown }) {
return !hasLazyCommandSelection() && opts.mini !== true && !opts.worktree
}
// Register only the Kilo-specific commands. Upstream commands stay in index.ts's chain so
// upstream merges that add or remove commands keep working without touching this file.
@@ -78,6 +70,7 @@ export namespace KiloCli {
export async function bootstrap(opts: { [key: string]: unknown }): Promise<void> {
info = opts.help === true || opts.version === true
if (info) return
narrow = workerTui(opts)
const { KiloLog } = await import("@/kilocode/log")
await KiloLog.init()
@@ -93,9 +86,11 @@ export namespace KiloCli {
const { JsonMigration } = await import("@/kilocode/storage/json-migration")
await JsonMigration.bootstrap()
const { AppRuntime } = await import("@/effect/app-runtime")
const { Config } = await import("@/config/config")
const cfg = await AppRuntime.runPromise(Config.Service.use((c) => c.getGlobal()))
const runtime = narrow ? await import("@/kilocode/cli/bootstrap-runtime") : undefined
const app = narrow ? undefined : await import("@/effect/app-runtime")
const cfg = runtime
? await runtime.KiloCliBootstrapRuntime.getGlobal()
: await app!.AppRuntime.runPromise((await import("@/config/config")).Config.Service.use((c) => c.getGlobal()))
const { Global } = await import("@opencode-ai/core/global")
const { Telemetry } = await import("@kilocode/kilo-telemetry")
@@ -105,16 +100,25 @@ export namespace KiloCli {
enabled: cfg.experimental?.openTelemetry !== false,
})
const { Auth } = await import("@/auth")
const { migrateLegacyKiloAuth } = gateway
const getAuth = async () => {
if (runtime) return runtime.KiloCliBootstrapRuntime.getAuth()
const { Auth } = await import("@/auth")
return app!.AppRuntime.runPromise(Auth.Service.use((s) => s.get("kilo")))
}
const setAuth = async (auth: Auth.Info) => {
if (runtime) return runtime.KiloCliBootstrapRuntime.setAuth(auth)
const { Auth } = await import("@/auth")
return app!.AppRuntime.runPromise(Auth.Service.use((s) => s.set("kilo", auth)))
}
// Migrate legacy Kilo CLI auth (~/.kilocode/cli/config.json) into auth.json if present.
await migrateLegacyKiloAuth(
async () => (await AppRuntime.runPromise(Auth.Service.use((s) => s.get("kilo")))) !== undefined,
async (auth) => AppRuntime.runPromise(Auth.Service.use((s) => s.set("kilo", auth))),
async () => (await getAuth()) !== undefined,
setAuth,
)
const auth = await AppRuntime.runPromise(Auth.Service.use((s) => s.get("kilo")))
const auth = await getAuth()
if (auth) {
const token = auth.type === "oauth" ? auth.access : auth.key
const account = auth.type === "oauth" ? auth.accountId : undefined
@@ -146,6 +150,11 @@ export namespace KiloCli {
}
} finally {
await KiloShutdown.run()
if (narrow) {
const { KiloCliBootstrapRuntime } = await import("@/kilocode/cli/bootstrap-runtime")
await KiloCliBootstrapRuntime.dispose()
return
}
const { InstanceRuntime } = await import("@/project/instance-runtime")
await InstanceRuntime.disposeAllInstances() // safety net (no-op if already disposed)
}
@@ -1,16 +1,13 @@
import { createRequire } from "module"
import { ConfigPlugin } from "@/config/plugin"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { isIndexingPlugin } from "@kilocode/kilo-indexing/detect"
import { ensureAtomicChatPlugin, isAtomicChatPlugin } from "@/kilocode/atomic-chat-feature"
import { ensureIndexingPlugin, resolveIndexingPlugin } from "@/kilocode/indexing-feature"
import { ensureIndexingPlugin, INDEXING_PLUGIN } from "@/kilocode/indexing-feature"
type Log = {
debug: (msg: string, data?: Record<string, unknown>) => void
}
const req = createRequire(import.meta.url)
export namespace KilocodeDefaultPlugins {
export function apply<T extends { plugin?: ConfigPluginV1.Spec[]; plugin_origins?: ConfigPlugin.Origin[] }>(
cfg: T,
@@ -19,7 +16,7 @@ export namespace KilocodeDefaultPlugins {
let plugins = cfg.plugin ?? []
if (!opts.disabled) {
plugins = ensureIndexingPlugin(plugins, resolveIndexingPlugin(req, opts.log))
plugins = ensureIndexingPlugin(plugins, INDEXING_PLUGIN)
plugins = ensureAtomicChatPlugin(plugins)
}
@@ -1,4 +1,3 @@
import { pathToFileURL } from "url"
import { hasIndexingPlugin } from "@kilocode/kilo-indexing/detect"
export const INDEXING_PLUGIN = "@kilocode/kilo-indexing"
@@ -11,29 +10,10 @@ type ConfigLike = {
plugin?: readonly PluginSpec[] | null
}
type Req = {
resolve: (id: string) => string
}
type LogLike = {
debug: (msg: string, data?: Record<string, unknown>) => void
}
export function indexingEnabled(config?: ConfigLike | null): boolean {
return hasIndexingPlugin(config?.plugin ?? [])
}
export function resolveIndexingPlugin(req: Req, log?: LogLike): string {
try {
const file = req.resolve(INDEXING_PLUGIN)
return pathToFileURL(file).href
} catch (err) {
const error = err instanceof Error ? err.message : String(err)
log?.debug("failed to resolve indexing plugin package, using package marker", { error })
return INDEXING_PLUGIN
}
}
export function ensureIndexingPlugin(items: readonly PluginSpec[], plugin?: string): PluginSpec[] {
const plugins = [...items]
if (!plugin) return plugins
@@ -73,7 +73,10 @@ export async function bootstrap() {
const marker = Database.path()
if (marker === ":memory:") return
const pending = marker + ".json-migration"
if ((await Filesystem.exists(marker)) && !(await Filesystem.exists(pending))) return
const [database, retry] = await Promise.all([Filesystem.exists(marker), Filesystem.exists(pending)])
if (database && !retry) return
const storage = path.join(Global.Path.data, "storage")
if (!database && !retry && !(await Filesystem.exists(storage))) return
await Filesystem.write(pending, "1")
const tty = process.stderr.isTTY
@@ -4,7 +4,6 @@ import { KiloShutdown } from "../../src/kilocode/cli/shutdown"
const calls: string[] = []
const timeouts: Array<number | undefined> = []
let err: unknown
let drainErr: unknown
let drainCalls = 0
let exit: string | number | null | undefined
@@ -92,16 +91,6 @@ mock.module("@/kilocode/session-export", () => ({
},
}))
mock.module("@/kilo-sessions/kilo-sessions", () => ({
KiloSessions: {
async drainIngestForShutdown() {
drainCalls += 1
calls.push("drain")
if (drainErr) throw drainErr
},
},
}))
mock.module("@/kilocode/help-command", () => ({
createHelpCommand: () => ({ command: "help", handler() {} }),
}))
@@ -134,7 +123,6 @@ function registerDrain() {
KiloShutdown.register(async () => {
drainCalls += 1
calls.push("drain")
if (drainErr) throw drainErr
})
}
@@ -155,7 +143,6 @@ describe("KiloCli.shutdown", () => {
calls.length = 0
timeouts.length = 0
err = undefined
drainErr = undefined
drainCalls = 0
exit = process.exitCode
process.exitCode = undefined
@@ -165,19 +152,15 @@ describe("KiloCli.shutdown", () => {
process.exitCode = exit
})
// Must stay first: setup registers the drain task once at import; KiloShutdown.run() clears it.
// Only this test pins that one-time module-scope registration (and the drain-before-dispose
// ordering it enables). Later tests call installDrain() so they do not rely on order.
test("rejects drain without blocking dispose", async () => {
drainErr = new Error("ingest drain failed")
test("does not load unused ingest shutdown work", async () => {
process.exitCode = 0
const { KiloCli } = await import("../../src/kilocode/cli/setup")
await expect(KiloCli.shutdown()).resolves.toBeUndefined()
expect(drainCalls).toBe(1)
expect(drainCalls).toBe(0)
expect(timeouts).toEqual([2000])
expect(calls).toEqual(["track:0", "session", "telemetry", "drain", "dispose"])
expect(calls).toEqual(["track:0", "session", "telemetry", "dispose"])
expect(process.exitCode).toBe(0)
})
@@ -0,0 +1,14 @@
import { describe, expect, test } from "bun:test"
import { KiloCli } from "../../../src/kilocode/cli/setup"
describe("CLI bootstrap runtime selection", () => {
test("uses the narrow runtime for worker-backed TUI launches", () => {
expect(KiloCli.workerTui({ _: [] })).toBe(true)
expect(KiloCli.workerTui({ _: ["./project"] })).toBe(true)
})
test("keeps full bootstrap for explicit, mini, and worktree commands", () => {
expect(KiloCli.workerTui({ _: [], mini: true })).toBe(false)
expect(KiloCli.workerTui({ _: [], worktree: "feature" })).toBe(false)
})
})
@@ -0,0 +1,49 @@
import { describe, expect, test } from "bun:test"
import yargs from "yargs"
import { hasLazyCommandSelection, lazy } from "../../../src/kilocode/cli/lazy-commands"
describe("lazy CLI command", () => {
test("loads once after command selection", async () => {
const calls: string[] = []
const command = lazy({
command: "sample",
describe: "sample command",
async load() {
calls.push("load")
return {
command: "sample",
describe: "sample command",
builder: (args) => args.option("value", { type: "string", demandOption: true }),
handler: (args) => {
calls.push(String(args.value))
},
}
},
})
const cli = yargs([]).exitProcess(false).command(command)
expect(calls).toEqual([])
await cli.parseAsync(["sample", "--value", "ready"])
expect(calls).toEqual(["load", "ready"])
expect(hasLazyCommandSelection()).toBe(true)
})
test("preserves builder validation", async () => {
const command = lazy({
command: "sample",
describe: "sample command",
async load() {
return {
command: "sample",
describe: "sample command",
builder: (args) => args.option("value", { type: "string", demandOption: true }),
handler() {},
}
},
})
await expect(yargs([]).exitProcess(false).command(command).parseAsync(["sample"])).rejects.toThrow(
"Missing required argument: value",
)
})
})
@@ -0,0 +1,14 @@
import { expect } from "bun:test"
import { Effect } from "effect"
import { cliIt } from "../../lib/cli-process"
cliIt.live(
"nested lazy command completion includes subcommands",
({ opencode }) =>
Effect.gen(function* () {
const result = yield* opencode.spawn(["--get-yargs-completions", "auth", ""])
opencode.expectExit(result, 0, "auth completion")
expect(result.stdout.split("\n")).toEqual(expect.arrayContaining(["list", "login", "logout"]))
}),
60_000,
)
@@ -3,7 +3,6 @@ import {
ensureIndexingPlugin,
indexingEnabled,
INDEXING_PLUGIN,
resolveIndexingPlugin,
} from "../../src/kilocode/indexing-feature"
describe("indexing plugin helpers", () => {
@@ -28,13 +27,4 @@ describe("indexing plugin helpers", () => {
const list = ensureIndexingPlugin(["global-plugin"], undefined)
expect(list).toEqual(["global-plugin"])
})
test("falls back to package marker when resolver fails", () => {
const plugin = resolveIndexingPlugin({
resolve() {
throw new Error("missing")
},
})
expect(plugin).toBe(INDEXING_PLUGIN)
})
})
@@ -0,0 +1,59 @@
import { expect, spyOn, test } from "bun:test"
import { clearInFlightCache } from "../../../src/kilo-sessions/inflight-cache"
import { KiloShutdown } from "../../../src/kilocode/cli/shutdown"
test("KiloSessions drains queued ingest before instance disposal", async () => {
const token = process.env.KILO_API_KEY
const base = process.env.KILO_SESSION_INGEST_URL
const calls: string[] = []
let body: unknown
process.env.KILO_API_KEY = "shutdown-token"
process.env.KILO_SESSION_INGEST_URL = "https://ingest.test"
clearInFlightCache("kilo-sessions:token")
clearInFlightCache("kilo-sessions:client")
clearInFlightCache("kilo-sessions:token-valid:shutdown-token")
await KiloShutdown.run()
const request = spyOn(globalThis, "fetch").mockImplementation(
Object.assign(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
if (url.endsWith("/api/session")) {
return Response.json({ id: "remote-shutdown", ingestPath: "/api/ingest/shutdown" })
}
if (url.endsWith("/api/ingest/shutdown?v=2")) {
body = init?.body ? JSON.parse(String(init.body)) : undefined
calls.push("ingest")
return new Response("{}", { status: 200 })
}
throw new Error(`Unexpected request: ${url}`)
},
{ preconnect: globalThis.fetch.preconnect },
),
)
try {
const url = new URL("../../../src/kilo-sessions/kilo-sessions.ts", import.meta.url)
url.searchParams.set("test", crypto.randomUUID())
const { KiloSessions } = await import(url.href)
await KiloSessions.bootstrap("session-shutdown")
expect(await KiloSessions._queueIngestForTest("session-shutdown")).toBe(true)
await KiloShutdown.run()
calls.push("dispose")
expect(calls).toEqual(["ingest", "dispose"])
expect(body).toEqual({ data: [{ type: "session_status", data: { status: "idle" } }] })
} finally {
request.mockRestore()
if (token === undefined) delete process.env.KILO_API_KEY
else process.env.KILO_API_KEY = token
if (base === undefined) delete process.env.KILO_SESSION_INGEST_URL
else process.env.KILO_SESSION_INGEST_URL = base
clearInFlightCache("kilo-sessions:token")
clearInFlightCache("kilo-sessions:client")
clearInFlightCache("kilo-sessions:token-valid:shutdown-token")
await KiloShutdown.run()
}
}, 30_000)
@@ -97,6 +97,22 @@ async function createTestDb() {
return [sqlite, db, filename] as const
}
test("bootstrap leaves fresh installs for the normal database initializer", async () => {
const marker = path.join(Global.Path.data, "json-migration-fresh.db")
const storage = path.join(Global.Path.data, "storage")
const previous = Flag.KILO_DB
Flag.KILO_DB = marker
await fs.rm(storage, { recursive: true, force: true })
try {
await JsonMigration.bootstrap()
expect(await Bun.file(marker).exists()).toBe(false)
expect(await Bun.file(marker + ".json-migration").exists()).toBe(false)
} finally {
Flag.KILO_DB = previous
await Promise.all([marker, `${marker}-wal`, `${marker}-shm`].map(cleanup))
}
})
describe("JSON to SQLite migration", () => {
let storageDir: string
let sqlite: Database