mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
fix(cli): make dev-setup work from local builds and hide it in releases
The `Installation.isLocal()` gate in dev-setup rejected locally-built binaries because KILO_CHANNEL bakes the git branch name, not "local". `detectRepo()` also failed inside a Bun single-file executable where `import.meta.url` resolves to a `/$bunfs/` virtual path. Introduce a build-time `KILO_BUILD_KIND` flag (source/release) derived from `Script.release`, guard dev-setup/dev-alias registration on it, and rewrite `detectRepo()` to try KILO_DEV_REPO, `import.meta.url` (skipping bunfs), `process.execPath`, then `process.cwd()`.
This commit is contained in:
@@ -235,6 +235,7 @@ for (const item of targets) {
|
||||
KILO_RIPGREP_WORKER_PATH: rgPath,
|
||||
KILO_CHANNEL: `'${Script.channel}'`,
|
||||
KILO_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
|
||||
KILO_BUILD_KIND: Script.release ? `'release'` : `'source'`, // kilocode_change
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { UninstallCommand } from "./cli/cmd/uninstall"
|
||||
import { ModelsCommand } from "./cli/cmd/models"
|
||||
import { UI } from "./cli/ui"
|
||||
import { Installation } from "./installation"
|
||||
import { InstallationVersion } from "./installation/version"
|
||||
import { InstallationBuildKind, InstallationVersion } from "./installation/version" // kilocode_change - add InstallationBuildKind
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { FormatError } from "./cli/error"
|
||||
import { ServeCommand } from "./cli/cmd/serve"
|
||||
@@ -237,11 +237,15 @@ let cli = yargs(args) // kilocode_change
|
||||
.command(SessionCommand)
|
||||
.command(RemoteCommand) // kilocode_change
|
||||
.command(ConfigCLICommand) // kilocode_change
|
||||
.command(DevSetupCommand) // kilocode_change
|
||||
.command(DevAliasCommand) // kilocode_change
|
||||
.command(PluginCommand)
|
||||
.command(DbCommand)
|
||||
|
||||
// kilocode_change start - dev-only commands are hidden from release builds
|
||||
if (InstallationBuildKind !== "release") {
|
||||
cli = cli.command(DevSetupCommand).command(DevAliasCommand)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - registered after initial chain to avoid self-referential type error
|
||||
cli = cli.command(createHelpCommand(() => cli))
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
declare global {
|
||||
const KILO_VERSION: string
|
||||
const KILO_CHANNEL: string
|
||||
const KILO_BUILD_KIND: string // kilocode_change
|
||||
}
|
||||
|
||||
export const InstallationVersion = typeof KILO_VERSION === "string" ? KILO_VERSION : "local"
|
||||
export const InstallationChannel = typeof KILO_CHANNEL === "string" ? KILO_CHANNEL : "local"
|
||||
export const InstallationLocal = InstallationChannel === "local"
|
||||
// kilocode_change start - distinguish release builds from source / local builds
|
||||
export const InstallationBuildKind: "source" | "release" =
|
||||
typeof KILO_BUILD_KIND === "string" && KILO_BUILD_KIND === "release" ? "release" : "source"
|
||||
// kilocode_change end
|
||||
|
||||
@@ -4,7 +4,6 @@ import { existsSync } from "fs"
|
||||
import { mkdir } from "fs/promises"
|
||||
import { fileURLToPath } from "url"
|
||||
import { cmd } from "@/cli/cmd/cmd"
|
||||
import { Installation } from "@/installation"
|
||||
import { UI } from "@/cli/ui"
|
||||
|
||||
type Shell = "zsh" | "bash" | "fish" | "powershell"
|
||||
@@ -43,13 +42,8 @@ export const DevSetupCommand = cmd({
|
||||
default: false,
|
||||
}),
|
||||
handler: async (args) => {
|
||||
if (!Installation.isLocal()) {
|
||||
UI.error("dev-setup only works when running from a source checkout (./bin/kilodev)")
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const repo = await detectRepo()
|
||||
const repo = await safeDetectRepo()
|
||||
if (!repo) return
|
||||
const shell = args.shell ? parse(args.shell) : detectShell()
|
||||
const snippet = aliasLine(shell, repo)
|
||||
|
||||
@@ -137,16 +131,22 @@ export const DevAliasCommand = cmd({
|
||||
default: "zsh",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
if (!Installation.isLocal()) {
|
||||
UI.error("dev-alias only works when running from a source checkout (./bin/kilodev)")
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
const repo = await detectRepo()
|
||||
const repo = await safeDetectRepo()
|
||||
if (!repo) return
|
||||
process.stdout.write(aliasLine(parse(args.shell), repo) + "\n")
|
||||
},
|
||||
})
|
||||
|
||||
async function safeDetectRepo(): Promise<string | undefined> {
|
||||
try {
|
||||
return await detectRepo()
|
||||
} catch (err) {
|
||||
UI.error(err instanceof Error ? err.message : String(err))
|
||||
process.exitCode = 1
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const { TEXT_HIGHLIGHT: H, TEXT_NORMAL: N, TEXT_DIM: D, TEXT_SUCCESS: S, TEXT_NORMAL_BOLD: B } = UI.Style
|
||||
const Style = UI.Style
|
||||
|
||||
@@ -262,15 +262,56 @@ function stamp(): string {
|
||||
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
async function detectRepo(): Promise<string> {
|
||||
const hint = process.env.KILO_DEV_REPO
|
||||
if (hint) return hint
|
||||
const walk = async (dir: string): Promise<string> => {
|
||||
// Walk up from `start` until we find a directory containing
|
||||
// packages/opencode/package.json. Exported for test coverage.
|
||||
export async function findRepoFrom(start: string): Promise<string | undefined> {
|
||||
let dir = start
|
||||
while (true) {
|
||||
const candidate = path.join(dir, "packages", "opencode", "package.json")
|
||||
if (await Bun.file(candidate).exists()) return dir
|
||||
const parent = path.dirname(dir)
|
||||
if (parent === dir) throw new Error("cannot locate repo root")
|
||||
return walk(parent)
|
||||
if (parent === dir) return undefined
|
||||
dir = parent
|
||||
}
|
||||
return walk(path.dirname(fileURLToPath(import.meta.url)))
|
||||
}
|
||||
|
||||
// bunfs single-file executable virtual roots — `import.meta.url` points here,
|
||||
// which is not a real on-disk path and must be skipped.
|
||||
function isBunfsPath(p: string): boolean {
|
||||
if (p.startsWith("/$bunfs/")) return true
|
||||
// Windows bunfs root, e.g. `B:/~BUN/root/...`. Drive letter may be any case.
|
||||
if (/^[A-Za-z]:[\\/]~BUN[\\/]/.test(p)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// Exported for test coverage.
|
||||
export async function detectRepo(): Promise<string> {
|
||||
const hint = process.env.KILO_DEV_REPO
|
||||
if (hint) return hint
|
||||
|
||||
const candidates: string[] = []
|
||||
|
||||
const meta = (() => {
|
||||
try {
|
||||
return fileURLToPath(import.meta.url)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
if (meta && !isBunfsPath(meta)) candidates.push(path.dirname(meta))
|
||||
|
||||
// process.execPath points at the binary itself; walk up from its directory.
|
||||
// For a local build it's <repo>/packages/opencode/dist/<target>/bin/kilo, so
|
||||
// findRepoFrom eventually hits the repo's packages/opencode/package.json.
|
||||
if (process.execPath) candidates.push(path.dirname(process.execPath))
|
||||
candidates.push(process.cwd())
|
||||
|
||||
for (const start of candidates) {
|
||||
const found = await findRepoFrom(start)
|
||||
if (found) return found
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"cannot locate kilocode source checkout; set KILO_DEV_REPO=/path/to/kilocode or run ./bin/kilodev dev-setup from the repo",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { ConfigCommand as ConfigCLICommand } from "../cli/cmd/config"
|
||||
import { PluginCommand } from "../cli/cmd/plug"
|
||||
import { DevSetupCommand, DevAliasCommand } from "./cli/dev-setup"
|
||||
import { HelpCommand } from "./help-command"
|
||||
import { InstallationBuildKind } from "../installation/version"
|
||||
|
||||
// Synthetic entry for the yargs built-in .completion() command so that
|
||||
// generateHelp --all and cli-reference.md include it automatically.
|
||||
@@ -34,6 +35,12 @@ const CompletionCommand = {
|
||||
handler: () => {},
|
||||
}
|
||||
|
||||
// Dev-only commands are spread in conditionally so release builds omit them
|
||||
// from `kilo help --all` and the docs table. They're also guarded the same way
|
||||
// at the yargs registration site in src/index.ts, so the commands-in-sync
|
||||
// regex in test/kilocode/help.test.ts sees DevSetup/DevAlias on neither side.
|
||||
const dev = InstallationBuildKind === "release" ? [] : [DevSetupCommand, DevAliasCommand]
|
||||
|
||||
export const commands = [
|
||||
AcpCommand,
|
||||
McpCommand,
|
||||
@@ -56,8 +63,7 @@ export const commands = [
|
||||
RemoteCommand,
|
||||
DbCommand,
|
||||
ConfigCLICommand,
|
||||
DevSetupCommand,
|
||||
DevAliasCommand,
|
||||
...dev,
|
||||
PluginCommand,
|
||||
HelpCommand,
|
||||
CompletionCommand,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { detectRepo, findRepoFrom } from "../../../src/kilocode/cli/dev-setup"
|
||||
|
||||
// Simulate a repo root by writing the sentinel file detectRepo looks for.
|
||||
async function makeSynthRepo(dir: string) {
|
||||
const pkg = path.join(dir, "packages", "opencode")
|
||||
await fs.mkdir(pkg, { recursive: true })
|
||||
await Bun.write(path.join(pkg, "package.json"), JSON.stringify({ name: "synth" }))
|
||||
return dir
|
||||
}
|
||||
|
||||
async function withEnv<T>(name: string, value: string | undefined, fn: () => Promise<T>): Promise<T> {
|
||||
const prev = process.env[name]
|
||||
if (value === undefined) delete process.env[name]
|
||||
else process.env[name] = value
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env[name]
|
||||
else process.env[name] = prev
|
||||
}
|
||||
}
|
||||
|
||||
describe("findRepoFrom", () => {
|
||||
test("returns the repo root when started from a nested path", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await makeSynthRepo(tmp.path)
|
||||
const nested = path.join(tmp.path, "packages", "opencode", "src", "kilocode", "cli")
|
||||
await fs.mkdir(nested, { recursive: true })
|
||||
|
||||
const found = await findRepoFrom(nested)
|
||||
expect(found).toBe(tmp.path)
|
||||
})
|
||||
|
||||
test("returns the repo root when started from the root itself", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await makeSynthRepo(tmp.path)
|
||||
|
||||
const found = await findRepoFrom(tmp.path)
|
||||
expect(found).toBe(tmp.path)
|
||||
})
|
||||
|
||||
test("returns undefined when no sentinel is found up to filesystem root", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
// No packages/opencode/package.json anywhere under tmp, and the filesystem
|
||||
// above tmp does not contain a kilocode repo at its own root.
|
||||
const found = await findRepoFrom(tmp.path)
|
||||
expect(found).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectRepo", () => {
|
||||
test("honours KILO_DEV_REPO without touching the filesystem", async () => {
|
||||
// Pointing at a path that does not exist — the hint must be returned as-is
|
||||
// (this proves no walk happens when KILO_DEV_REPO is set).
|
||||
const hint = "/definitely/does/not/exist/kilo-repo"
|
||||
const got = await withEnv("KILO_DEV_REPO", hint, () => detectRepo())
|
||||
expect(got).toBe(hint)
|
||||
})
|
||||
|
||||
test("KILO_DEV_REPO wins over the filesystem walk", async () => {
|
||||
// Build a synthetic repo, point KILO_DEV_REPO at it, and run from an
|
||||
// unrelated cwd. Even though the walk from cwd (inside the real kilo
|
||||
// checkout) would succeed, the hint must take priority.
|
||||
await using tmp = await tmpdir()
|
||||
await makeSynthRepo(tmp.path)
|
||||
|
||||
const got = await withEnv("KILO_DEV_REPO", tmp.path, () => detectRepo())
|
||||
expect(got).toBe(tmp.path)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user