diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index e3eb43d92..e2f3a9e48 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -9,6 +9,11 @@ import os from "os" import { Filesystem } from "../../util/filesystem" import { Process } from "../../util/process" +// altimate_change start — shell config markers (current + legacy for migration) +const SHELL_MARKERS = ["# altimate-code", "# opencode"] +const BIN_PATHS = [".altimate-code/bin", ".opencode/bin"] +// altimate_change end + interface UninstallArgs { keepConfig: boolean keepData: boolean @@ -180,13 +185,15 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar if (method !== "curl" && method !== "unknown") { const cmds: Record = { - npm: ["npm", "uninstall", "-g", "opencode-ai"], - pnpm: ["pnpm", "uninstall", "-g", "opencode-ai"], - bun: ["bun", "remove", "-g", "opencode-ai"], - yarn: ["yarn", "global", "remove", "opencode-ai"], - brew: ["brew", "uninstall", "opencode"], + // altimate_change start — correct package names + npm: ["npm", "uninstall", "-g", "@altimateai/altimate-code"], + pnpm: ["pnpm", "uninstall", "-g", "@altimateai/altimate-code"], + bun: ["bun", "remove", "-g", "@altimateai/altimate-code"], + yarn: ["yarn", "global", "remove", "@altimateai/altimate-code"], + brew: ["brew", "uninstall", "altimate-code"], choco: ["choco", "uninstall", "opencode"], scoop: ["scoop", "uninstall", "opencode"], + // altimate_change end } const cmd = cmds[method] @@ -215,7 +222,7 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar prompts.log.info(` rm "${targets.binary}"`) const binDir = path.dirname(targets.binary) - if (binDir.includes(".opencode")) { + if (BIN_PATHS.some((p) => binDir.includes(p.split("/")[0]))) { prompts.log.info(` rmdir "${binDir}" 2>/dev/null`) } } @@ -266,7 +273,7 @@ async function getShellConfigFile(): Promise { if (!exists) continue const content = await Filesystem.readText(file).catch(() => "") - if (content.includes("# opencode") || content.includes(".opencode/bin")) { + if (SHELL_MARKERS.some((m) => content.includes(m)) || BIN_PATHS.some((p) => content.includes(p))) { return file } } @@ -284,22 +291,24 @@ async function cleanShellConfig(file: string) { for (const line of lines) { const trimmed = line.trim() - if (trimmed === "# opencode") { + // altimate_change start — handle both current (altimate-code) and legacy (opencode) markers + if (SHELL_MARKERS.includes(trimmed)) { skip = true continue } if (skip) { skip = false - if (trimmed.includes(".opencode/bin") || trimmed.includes("fish_add_path")) { + if (BIN_PATHS.some((p) => trimmed.includes(p)) || trimmed.includes("fish_add_path")) { continue } } if ( - (trimmed.startsWith("export PATH=") && trimmed.includes(".opencode/bin")) || - (trimmed.startsWith("fish_add_path") && trimmed.includes(".opencode")) + (trimmed.startsWith("export PATH=") && BIN_PATHS.some((p) => trimmed.includes(p))) || + (trimmed.startsWith("fish_add_path") && BIN_PATHS.some((p) => trimmed.includes(p.split("/")[0]))) ) { + // altimate_change end continue } diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 8dc6f48f1..8c822c1ed 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -25,6 +25,20 @@ declare global { export namespace Installation { const log = Log.create({ service: "installation" }) + // altimate_change start — fetch with timeout to prevent hanging + const FETCH_TIMEOUT_MS = 10_000 + + async function fetchWithTimeout(url: string, opts?: RequestInit): Promise { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) + try { + return await fetch(url, { ...opts, signal: controller.signal }) + } finally { + clearTimeout(timeout) + } + } + // altimate_change end + async function text(cmd: string[], opts: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) { return Process.text(cmd, { cwd: opts.cwd, @@ -34,10 +48,41 @@ export namespace Installation { } async function upgradeCurl(target: string) { - const body = await fetch("https://altimate.ai/install").then((res) => { - if (!res.ok) throw new Error(res.statusText) - return res.text() - }) + // altimate_change start — use repo-hosted install script (altimate.ai/install returns HTML) + const installUrls = [ + "https://raw.githubusercontent.com/AltimateAI/altimate-code/main/install", + "https://altimate.ai/install", + ] + let body = "" + for (const installUrl of installUrls) { + try { + const res = await fetchWithTimeout(installUrl) + if (!res.ok) continue + const text = await res.text() + if (text.trimStart().startsWith(" { if (!res.ok) throw new Error(`GitHub releases API: ${res.status} ${res.statusText}`) return res.json() @@ -307,6 +359,12 @@ export namespace Installation { } if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") { + // altimate_change start — skip registry check for local channel + if (CHANNEL === "local") { + log.info("skipping version check for local channel") + return VERSION + } + // altimate_change end const registry = await iife(async () => { const r = (await text(["npm", "config", "get", "registry"])).trim() const reg = r || "https://registry.npmjs.org" @@ -314,7 +372,7 @@ export namespace Installation { }) const channel = CHANNEL // altimate_change start — npm package name for version check - return fetch(`${registry}/@altimateai/altimate-code/${channel}`) + return fetchWithTimeout(`${registry}/@altimateai/altimate-code/${channel}`) // altimate_change end .then((res) => { if (!res.ok) throw new Error(res.statusText) @@ -324,7 +382,7 @@ export namespace Installation { } if (detectedMethod === "choco") { - return fetch( + return fetchWithTimeout( "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version", { headers: { Accept: "application/json;odata=verbose" } }, ) @@ -332,25 +390,47 @@ export namespace Installation { if (!res.ok) throw new Error(res.statusText) return res.json() }) - .then((data: any) => data.d.results[0].Version) + .then((data: any) => { + // altimate_change start — guard against empty results + const results = data?.d?.results + if (!results || !Array.isArray(results) || results.length === 0) { + throw new Error("Chocolatey package 'opencode' not found or returned empty results") + } + return results[0].Version + // altimate_change end + }) } if (detectedMethod === "scoop") { - return fetch("https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", { + return fetchWithTimeout("https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", { headers: { Accept: "application/json" }, }) .then((res) => { - if (!res.ok) throw new Error(res.statusText) + if (!res.ok) throw new Error(`Scoop manifest fetch failed: ${res.status}`) return res.json() }) - .then((data: any) => data.version) + .then((data: any) => { + // altimate_change start — guard against missing version field + if (!data?.version) { + throw new Error("Scoop manifest for 'opencode' missing version field") + } + return data.version + // altimate_change end + }) } - return fetch("https://api.github.com/repos/AltimateAI/altimate-code/releases/latest") + // altimate_change start — fallback to GitHub releases with safe access + return fetchWithTimeout("https://api.github.com/repos/AltimateAI/altimate-code/releases/latest") .then((res) => { - if (!res.ok) throw new Error(res.statusText) + if (!res.ok) throw new Error(`GitHub releases API returned ${res.status}`) return res.json() }) - .then((data: any) => data.tag_name.replace(/^v/, "")) + .then((data: any) => { + if (!data?.tag_name) { + throw new Error("No releases found for AltimateAI/altimate-code") + } + return data.tag_name.replace(/^v/, "") + }) + // altimate_change end } }