Skip to content
5 changes: 5 additions & 0 deletions .changeset/fix-cli-console-port-mismatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Restart the daemon when `kilo console` or `kilo daemon start` receives explicit network options that don't match the running daemon, instead of silently ignoring the requested settings.
32 changes: 28 additions & 4 deletions packages/opencode/src/cli/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,27 @@ const options = {

export type NetworkOptions = InferredOptionTypes<typeof options>

// kilocode_change start
const flags = [
["port", "--port"],
["hostname", "--hostname"],
["mdns", "--mdns"],
["mdnsDomain", "--mdns-domain"],
["cors", "--cors"],
] as const
export type NetworkOption = (typeof flags)[number][0]

export function explicitNetworkOptions(argv = process.argv) {
const index = argv.indexOf("--")
const args = index === -1 ? argv : argv.slice(0, index)
return flags.flatMap(([name, flag]) =>
args.some((arg) => arg === flag || arg.startsWith(`${flag}=`) || (name === "mdns" && arg === "--no-mdns"))
? [name]
: [],
)
}
// kilocode_change end

export function withNetworkOptions<T>(yargs: Argv<T>) {
return yargs.options(options)
}
Expand All @@ -42,10 +63,13 @@ export const resolveNetworkOptions = Effect.fn("Cli.resolveNetworkOptions")(func
})

export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: Config.Info) {
const portExplicitlySet = process.argv.includes("--port")
const hostnameExplicitlySet = process.argv.includes("--hostname")
const mdnsExplicitlySet = process.argv.includes("--mdns")
const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain")
// kilocode_change start
const explicit = explicitNetworkOptions()
const portExplicitlySet = explicit.includes("port")
const hostnameExplicitlySet = explicit.includes("hostname")
const mdnsExplicitlySet = explicit.includes("mdns")
const mdnsDomainExplicitlySet = explicit.includes("mdnsDomain")
// kilocode_change end
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
const mdnsDomain = mdnsDomainExplicitlySet ? args["mdns-domain"] : (config?.server?.mdnsDomain ?? args["mdns-domain"])
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
Expand Down
7 changes: 4 additions & 3 deletions packages/opencode/src/kilocode/cli/cmd/console.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import open from "open"
import { cmd } from "@/cli/cmd/cmd"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { explicitNetworkOptions, withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { AppRuntime } from "@/effect/app-runtime"
import { Daemon } from "@/kilocode/daemon/daemon"
import { warnPort } from "@/kilocode/cli/port-warning"
Expand Down Expand Up @@ -43,8 +43,9 @@ export const KiloConsoleCommand = cmd({
handler: async (args) => {
const opts = await AppRuntime.runPromise(resolveNetworkOptions(args))
warnPort(opts.port)
const result = await Daemon.start(opts)
const state = result.state
const daemon = await Daemon.ensure(opts, explicitNetworkOptions())
if (daemon.restarted) console.warn("Restarted the Kilo daemon to apply the requested network options")
const state = daemon.result.state
if (!state) throw new Error("Kilo daemon did not provide connection state")

const url = publicUrl(state)
Expand Down
13 changes: 10 additions & 3 deletions packages/opencode/src/kilocode/cli/cmd/daemon.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Argv } from "yargs"
import { cmd } from "@/cli/cmd/cmd"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { explicitNetworkOptions, withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { AppRuntime } from "@/effect/app-runtime"
import { Daemon } from "@/kilocode/daemon/daemon"
import { warnPort } from "@/kilocode/cli/port-warning"
Expand Down Expand Up @@ -62,12 +62,19 @@ const StartCommand = cmd({
handler: async (args) => {
const opts = await AppRuntime.runPromise(resolveNetworkOptions(args))
warnPort(opts.port)
const result = await Daemon.start(opts)
const daemon = await Daemon.ensure(opts, explicitNetworkOptions())
const result = daemon.result
if (args.json) {
print(result, true)
return
}
console.log(result.reused ? "kilo daemon already running" : "kilo daemon started")
console.log(
result.reused
? "kilo daemon already running"
: daemon.restarted
? "kilo daemon restarted"
: "kilo daemon started",
)
print(result)
},
})
Expand Down
66 changes: 53 additions & 13 deletions packages/opencode/src/kilocode/daemon/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
const lock = "kilocode-daemon"
export const PortRange = { start: 4097, end: 4116 } as const

export const Network = z.object({
hostname: z.string(),
port: z.number().int().nonnegative(),
mdns: z.boolean(),
mdnsDomain: z.string(),
cors: z.array(z.string()).transform((items) => [...new Set(items)].sort()),
})
export type Network = z.infer<typeof Network>
export type NetworkOption = keyof Network

export const State = z.object({
pid: z.number().int().positive(),
hostname: z.string(),
Expand All @@ -26,6 +36,7 @@
version: z.string(),
startedAt: z.string(),
log: z.string(),
options: Network.optional(),
})
export type State = z.infer<typeof State>

Expand All @@ -44,12 +55,7 @@
})
export type Status = z.infer<typeof Status>

export type Options = {
hostname: string
port: number
mdns?: boolean
mdnsDomain?: string
cors?: string[]
export type Options = Network & {
command?: string[]
env?: NodeJS.ProcessEnv
timeout?: number
Expand All @@ -60,6 +66,11 @@
reused: boolean
}

export type Ensure = {
result: Start
restarted: boolean
}

export type Stop = Status & {
stopped: boolean
}
Expand Down Expand Up @@ -157,13 +168,31 @@
return { running: true, stale: false, state, health: probe, file: file() }
}

export async function start(input: Options): Promise<Start> {
function compatible(state: State, input: Options, explicit: readonly NetworkOption[]) {
const options = Network.parse(input)
return explicit.every((name) => {
if (name === "hostname") return state.hostname === options.hostname
if (name === "port") return options.port === 0 || state.port === options.port
if (name === "mdns" && state.hostname !== options.hostname) return false
if (!state.options) return false
if (name === "cors") return state.options.cors.join("\n") === options.cors.join("\n")
return state.options[name] === options[name]
})
}

async function run(input: Options, explicit: readonly NetworkOption[] = [], force = false): Promise<Ensure> {
return await Flock.withLock(
lock,
async () => {
const current = await status()
if (current.running) return { ...current, started: false, reused: true }
if (current.stale && current.state) await terminate(current.state.pid, true)
const restarted = current.running && !!current.state && (force || !compatible(current.state, input, explicit))
if (current.running && !restarted) {
return { result: { ...current, started: false, reused: true }, restarted: false }
}
if (current.state && (current.stale || restarted)) {
await terminate(current.state.pid, current.stale)
if (alive(current.state.pid)) await terminate(current.state.pid, true)
}
await clear()
const password = "kilo"
const token = auth(password)
Expand All @@ -182,15 +211,24 @@
version: InstallationVersion,
startedAt: new Date().toISOString(),
log: out,
options: Network.parse(input),
}
await write(state)
const next = await status()
return { ...next, started: true, reused: false, state }
return { result: { ...next, started: true, reused: false, state }, restarted }
},
{ dir: path.join(root(), "locks"), timeoutMs: 15_000, staleMs: 30_000 },
)
}

export async function start(input: Options): Promise<Start> {
return (await run(input)).result
}

export async function ensure(input: Options, explicit: readonly NetworkOption[]): Promise<Ensure> {
return await run(input, explicit)
}

export async function stop(): Promise<Stop> {
return await Flock.withLock(
lock,
Expand All @@ -209,8 +247,7 @@
}

export async function restart(input: Options): Promise<Start> {
await stop()
return await start(input)
return (await run(input, [], true)).result
}

export function command(
Expand Down Expand Up @@ -289,7 +326,10 @@
})
const failure = new Promise<never>((_, reject) => child.once("error", reject))
child.unref()
return await Promise.race([wait(out, child.pid, input.timeout ?? 10_000), failure])
return await Promise.race([wait(out, child.pid, input.timeout ?? 10_000), failure]).catch(async (err) => {
if (child.pid && alive(child.pid)) await terminate(child.pid, true)
throw err
})
} finally {
await Promise.all([stdout.close(), stderr.close()])
}
Expand All @@ -314,7 +354,7 @@
while (true) {
const match = await line(out)
if (match) return { pid, hostname: match.hostname, port: match.port }
if (!alive(pid)) throw new Error(`Daemon exited before listening. Log: ${out}`)

Check failure on line 357 in packages/opencode/src/kilocode/daemon/daemon.ts

View workflow job for this annotation

GitHub Actions / unit (linux)

error: Daemon exited before listening. Log: /tmp/opencode-test-bsjqdh3ev1v/log/daemon.log

at wait (/home/runner/_work/kilocode/kilocode/packages/opencode/src/kilocode/daemon/daemon.ts:357:34)

Check failure on line 357 in packages/opencode/src/kilocode/daemon/daemon.ts

View workflow job for this annotation

GitHub Actions / unit (linux)

error: Daemon exited before listening. Log: /tmp/opencode-test-ecaojw94q98/log/daemon.log

at wait (/home/runner/_work/kilocode/kilocode/packages/opencode/src/kilocode/daemon/daemon.ts:357:34)
if (Date.now() - started > timeout) throw new Error(`Timed out waiting for daemon. Log: ${out}`)
await sleep(100)
}
Expand Down
Loading
Loading