mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
perf(cli): optimize startup time
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Improve CLI cold and warm startup time.
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user