diff --git a/.changeset/fix-kilo-npm-upgrade.md b/.changeset/fix-kilo-npm-upgrade.md new file mode 100644 index 00000000000..307a40eb8f5 --- /dev/null +++ b/.changeset/fix-kilo-npm-upgrade.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix upgrades to resolve Kilo CLI packages and releases instead of OpenCode packages and versions. diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md index e6bc537b981..2f760b9bdb9 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md @@ -650,7 +650,7 @@ Positionals: Options: --help Show help [boolean] --version Show version number [boolean] - -m, --method installation method to use [string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"] + -m, --method installation method to use [string] [choices: "curl", "npm", "yarn", "pnpm", "bun", "brew", "choco", "scoop"] ``` ## kilo uninstall diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index 5ae4764c680..a1311dad23c 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -122,6 +122,8 @@ - +- + - @@ -143,8 +145,6 @@ - -- - - - diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index 6b329814434..18c5ca466eb 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -8,6 +8,7 @@ import path from "path" import os from "os" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" +import { Brew as KiloBrew } from "@/kilocode/installation" // kilocode_change interface UninstallArgs { keepConfig: boolean @@ -133,7 +134,7 @@ async function showRemovalSummary(targets: RemovalTargets, method: Installation. pnpm: "pnpm uninstall -g @kilocode/cli", // kilocode_change bun: "bun remove -g @kilocode/cli", // kilocode_change yarn: "yarn global remove @kilocode/cli", // kilocode_change - brew: "brew uninstall opencode", + brew: `brew uninstall ${KiloBrew.name}`, // kilocode_change choco: "choco uninstall kilo", // kilocode_change scoop: "scoop uninstall kilo", // kilocode_change } @@ -184,7 +185,7 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar pnpm: ["pnpm", "uninstall", "-g", "@kilocode/cli"], // kilocode_change bun: ["bun", "remove", "-g", "@kilocode/cli"], // kilocode_change yarn: ["yarn", "global", "remove", "@kilocode/cli"], // kilocode_change - brew: ["brew", "uninstall", "opencode"], + brew: ["brew", "uninstall", KiloBrew.name], // kilocode_change choco: ["choco", "uninstall", "kilo"], // kilocode_change scoop: ["scoop", "uninstall", "kilo"], // kilocode_change } diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts index 8aa6049d2cc..67bd30b3e64 100644 --- a/packages/opencode/src/cli/cmd/upgrade.ts +++ b/packages/opencode/src/cli/cmd/upgrade.ts @@ -17,7 +17,7 @@ export const UpgradeCommand = { alias: "m", describe: "installation method to use", type: "string", - choices: ["curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"], + choices: ["curl", "npm", "yarn", "pnpm", "bun", "brew", "choco", "scoop"], // kilocode_change }) }, handler: async (args: { target?: string; method?: string }) => { @@ -43,7 +43,7 @@ export const UpgradeCommand = { } } prompts.log.info("Using method: " + method) - const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest() + const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest(method) // kilocode_change if (InstallationVersion === target) { prompts.log.warn(`kilo upgrade skipped: ${target} is already installed`) // kilocode_change diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index f177ce841a2..6a2f0b8e728 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -9,8 +9,8 @@ export async function upgrade() { const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())) if (config.autoupdate === false || Flag.KILO_DISABLE_AUTOUPDATE) return const method = await Installation.method() - // kilocode_change start - only auto-upgrade for npm/pnpm/bun (we only publish @kilocode/cli via npm registry) - if (method !== "npm" && method !== "pnpm" && method !== "bun") return + // kilocode_change start - only auto-upgrade for npm/yarn/pnpm/bun (we only publish @kilocode/cli via npm registry) + if (method !== "npm" && method !== "yarn" && method !== "pnpm" && method !== "bun") return // kilocode_change end const latest = await Installation.latest(method).catch(() => {}) if (!latest) return diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 9b0e06c4af5..d262663f64f 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -11,6 +11,9 @@ import { makeRuntime } from "@opencode-ai/core/effect/runtime" import semver from "semver" import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { NpmConfig } from "@opencode-ai/core/npm-config" +// kilocode_change start +import { Brew as KiloBrew, Choco as KiloChoco, Npm as KiloNpm, Release as KiloRelease, Scoop as KiloScoop } from "@/kilocode/installation" +// kilocode_change end const log = Log.create({ service: "installation" }) @@ -51,7 +54,7 @@ export const Info = Schema.Struct({ export type Info = Schema.Schema.Type export function userAgent(client = "cli") { - return `opencode/${InstallationChannel}/${InstallationVersion}/${client}` + return `kilo/${InstallationChannel}/${InstallationVersion}/${client}` // kilocode_change } export const USER_AGENT = userAgent() @@ -129,15 +132,15 @@ export const layer: Layer.Layer text(["yarn", "global", "list"]) }, { name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) }, { name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) }, - { name: "brew", command: () => text(["brew", "list", "--formula", "opencode"]) }, - { name: "scoop", command: () => text(["scoop", "list", "opencode"]) }, - { name: "choco", command: () => text(["choco", "list", "--limit-output", "opencode"]) }, + { name: "brew", command: () => text(["brew", "list", "--formula", KiloBrew.formula]) }, // kilocode_change + { name: "scoop", command: () => text(["scoop", "list", KiloScoop.name]) }, // kilocode_change + { name: "choco", command: () => text(["choco", "list", "--limit-output", KiloChoco.name]) }, // kilocode_change ] checks.sort((a, b) => { @@ -186,8 +190,16 @@ export const layer: Layer.Layer { Effect.gen(function* () { const result = yield* Installation.Service.use((svc) => svc.latest("npm")) expect(result).toBe("1.5.0") - expect(npmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) + expect(npmCalls).toContain(`https://registry.npmjs.org/@kilocode%2fcli/${InstallationChannel}`) // kilocode_change }), ) @@ -96,7 +96,7 @@ describe("installation", () => { Effect.gen(function* () { const result = yield* Installation.Service.use((svc) => svc.latest("bun")) expect(result).toBe("1.6.0") - expect(bunCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) + expect(bunCalls).toContain(`https://registry.npmjs.org/@kilocode%2fcli/${InstallationChannel}`) // kilocode_change }), ) @@ -110,7 +110,7 @@ describe("installation", () => { Effect.gen(function* () { const result = yield* Installation.Service.use((svc) => svc.latest("pnpm")) expect(result).toBe("1.7.0") - expect(pnpmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) + expect(pnpmCalls).toContain(`https://registry.npmjs.org/@kilocode%2fcli/${InstallationChannel}`) // kilocode_change }), ) @@ -135,8 +135,8 @@ describe("installation", () => { () => jsonResponse({ versions: { stable: "2.0.0" } }), (cmd, args) => { // getBrewFormula: return core formula (no tap) - if (cmd === "brew" && args.includes("--formula") && args.includes("anomalyco/tap/opencode")) return "" - if (cmd === "brew" && args.includes("--formula") && args.includes("opencode")) return "opencode" + if (cmd === "brew" && args.includes("--formula") && args.includes("Kilo-Org/tap/kilo")) return "" // kilocode_change + if (cmd === "brew" && args.includes("--formula") && args.includes("kilo")) return "kilo" // kilocode_change return "" }, ), @@ -154,7 +154,7 @@ describe("installation", () => { testLayer( () => jsonResponse({}), // HTTP not used for tap formula (cmd, args) => { - if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) return "opencode" + if (cmd === "brew" && args.includes("Kilo-Org/tap/kilo") && args.includes("--formula")) return "kilo" // kilocode_change if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson return "" }, diff --git a/packages/opencode/test/kilocode/installation/upgrade.test.ts b/packages/opencode/test/kilocode/installation/upgrade.test.ts new file mode 100644 index 00000000000..b2ecaf48f44 --- /dev/null +++ b/packages/opencode/test/kilocode/installation/upgrade.test.ts @@ -0,0 +1,221 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer, Stream } from "effect" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { InstallationChannel } from "@opencode-ai/core/installation/version" +import { AppProcess } from "@opencode-ai/core/process" +import { Installation } from "../../../src/installation" +import { testEffect } from "../../lib/effect" + +const encoder = new TextEncoder() + +function json(body: unknown) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" }, + }) +} + +function http(handler: (request: HttpClientRequest.HttpClientRequest) => Response = () => json({})) { + const client = HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler(request)))) + return Layer.succeed(HttpClient.HttpClient, client) +} + +function spawner(handler: (cmd: string, args: readonly string[]) => string) { + const child = ChildProcessSpawner.make((command) => { + const std = ChildProcess.isStandardCommand(command) ? command : undefined + const output = handler(std?.command ?? "", std?.args ?? []) + return Effect.succeed( + ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(0), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), + isRunning: Effect.succeed(false), + kill: () => Effect.void, + stdin: { [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") } as never, + stdout: output ? Stream.make(encoder.encode(output)) : Stream.empty, + stderr: Stream.empty, + all: Stream.empty, + getInputFd: () => ({ [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") }) as never, + getOutputFd: () => Stream.empty, + unref: Effect.succeed(Effect.void), + }), + ) + }) + return Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, child) +} + +function layer( + handler: (cmd: string, args: readonly string[]) => string, + request?: (request: HttpClientRequest.HttpClientRequest) => Response, +) { + const proc = AppProcess.layer.pipe(Layer.provide(spawner(handler))) + return Installation.layer.pipe(Layer.provide(http(request)), Layer.provide(proc)) +} + +describe("Kilo installation upgrade", () => { + const release: string[] = [] + testEffect( + layer( + () => "", + (request) => { + release.push(request.url) + return json({ tag_name: "v8.8.8" }) + }, + ), + ).effect("reads fallback versions from Kilo GitHub releases", () => + Effect.gen(function* () { + const result = yield* Installation.Service.use((svc) => svc.latest("unknown")) + expect(result).toBe("8.8.8") + expect(release).toContain("https://api.github.com/repos/Kilo-Org/kilocode/releases/latest") + }), + ) + + const urls: string[] = [] + testEffect( + layer( + () => "", + (request) => { + urls.push(request.url) + return json({ version: "8.8.8" }) + }, + ), + ).effect("reads yarn versions from the Kilo package registry", () => + Effect.gen(function* () { + const result = yield* Installation.Service.use((svc) => svc.latest("yarn")) + expect(result).toBe("8.8.8") + expect(urls).toContain(`https://registry.npmjs.org/@kilocode%2fcli/${InstallationChannel}`) + }), + ) + + testEffect( + layer((cmd, args) => { + if (cmd === "npm" && args.includes("list")) return "@kilocode/cli@7.3.45" + return "" + }), + ).effect("detects npm installs from the Kilo package", () => + Effect.gen(function* () { + const result = yield* Installation.Service.use((svc) => svc.method()) + expect(result).toBe("npm") + }), + ) + + const choco: string[] = [] + testEffect( + layer( + () => "", + (request) => { + choco.push(request.url) + return json({ d: { results: [{ Version: "8.8.8" }] } }) + }, + ), + ).effect("reads choco versions from the Kilo package", () => + Effect.gen(function* () { + const result = yield* Installation.Service.use((svc) => svc.latest("choco")) + expect(result).toBe("8.8.8") + expect(choco).toContain( + "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27kilo%27%20and%20IsLatestVersion&$select=Version", + ) + }), + ) + + const scoop: string[] = [] + testEffect( + layer( + () => "", + (request) => { + scoop.push(request.url) + return json({ version: "8.8.8" }) + }, + ), + ).effect("reads scoop versions from the Kilo manifest", () => + Effect.gen(function* () { + const result = yield* Installation.Service.use((svc) => svc.latest("scoop")) + expect(result).toBe("8.8.8") + expect(scoop).toContain("https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/kilo.json") + }), + ) + + const calls: string[] = [] + const upgrade = layer((cmd, args) => { + calls.push([cmd, ...args].join(" ")) + return "" + }) + + testEffect(upgrade).effect("installs the Kilo npm package", () => + Effect.gen(function* () { + yield* Installation.Service.use((svc) => svc.upgrade("npm", "9.9.9")) + expect(calls).toContain("npm install -g @kilocode/cli@9.9.9") + }), + ) + + testEffect(upgrade).effect("installs the Kilo yarn package", () => + Effect.gen(function* () { + yield* Installation.Service.use((svc) => svc.upgrade("yarn", "9.9.9")) + expect(calls).toContain("yarn global add @kilocode/cli@9.9.9") + }), + ) + + testEffect(upgrade).effect("installs the Kilo pnpm package", () => + Effect.gen(function* () { + yield* Installation.Service.use((svc) => svc.upgrade("pnpm", "9.9.9")) + expect(calls).toContain("pnpm install -g @kilocode/cli@9.9.9") + }), + ) + + testEffect(upgrade).effect("installs the Kilo bun package", () => + Effect.gen(function* () { + yield* Installation.Service.use((svc) => svc.upgrade("bun", "9.9.9")) + expect(calls).toContain("bun install -g @kilocode/cli@9.9.9") + }), + ) + + const brew: string[] = [] + const brewer = layer((cmd, args) => { + brew.push([cmd, ...args].join(" ")) + if (cmd === "brew" && args.includes("list")) return "kilo" + if (cmd === "brew" && args.includes("--repo")) return "/tmp/kilo-homebrew-tap" + return "" + }) + + testEffect(brewer).effect("upgrades the Kilo brew formula", () => + Effect.gen(function* () { + yield* Installation.Service.use((svc) => svc.upgrade("brew", "9.9.9")) + expect(brew).toContain("brew tap Kilo-Org/tap") + expect(brew).toContain("brew upgrade Kilo-Org/tap/kilo") + }), + ) + + testEffect(upgrade).effect("upgrades the Kilo choco package", () => + Effect.gen(function* () { + yield* Installation.Service.use((svc) => svc.upgrade("choco", "9.9.9")) + expect(calls).toContain("choco upgrade kilo --version=9.9.9 -y") + }), + ) + + testEffect(upgrade).effect("installs the Kilo scoop package", () => + Effect.gen(function* () { + yield* Installation.Service.use((svc) => svc.upgrade("scoop", "9.9.9")) + expect(calls).toContain("scoop install kilo@9.9.9") + }), + ) + + const curl: string[] = [] + testEffect( + layer( + (cmd, args) => { + curl.push([cmd, ...args].join(" ")) + return "" + }, + (request) => { + curl.push(request.url) + return new Response("#!/usr/bin/env bash", { status: 200 }) + }, + ), + ).effect("uses the Kilo install script for curl upgrades", () => + Effect.gen(function* () { + yield* Installation.Service.use((svc) => svc.upgrade("curl", "9.9.9")) + expect(curl).toContain("https://kilo.ai/install") + expect(curl).toContain("bash") + }), + ) +})