Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 134
fix: add .altimate-code path detection + use actual choco error message#274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, string[]> = { | ||
| 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<string | null> { | ||
| 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 | ||
dev-punia-altimate marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Response> { | ||
| 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("<!") || text.trimStart().startsWith("<html")) continue | ||
| body = text | ||
| break | ||
| } catch { | ||
| continue | ||
| } | ||
| } | ||
| if (!body) { | ||
| throw new Error( | ||
| `Could not fetch install script from any source. ` + | ||
| `As a workaround, upgrade via npm: npm install -g @altimateai/altimate-code@${target}`, | ||
| ) | ||
| } | ||
| // Guard: Windows users should use PowerShell install | ||
| if (process.platform === "win32") { | ||
| throw new Error( | ||
| `curl install method is not supported on Windows. ` + | ||
| `Use PowerShell: irm https://altimate.ai/install.ps1 | iex\n` + | ||
| `Or npm: npm install -g @altimateai/altimate-code@${target}`, | ||
| ) | ||
| } | ||
dev-punia-altimate marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. dev-punia-altimate marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // altimate_change end | ||
| const proc = Process.spawn(["bash"], { | ||
| stdin: "pipe", | ||
| stdout: "pipe", | ||
| @@ -101,6 +146,9 @@ export namespace Installation { | ||
| export async function method() { | ||
| if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" | ||
| // altimate_change start — detect altimate-code curl installs | ||
| if (process.execPath.includes(path.join(".altimate-code", "bin"))) return "curl" | ||
| // altimate_change end | ||
| if (process.execPath.includes(path.join(".local", "bin"))) return "curl" | ||
| const exec = process.execPath.toLowerCase() | ||
| @@ -186,7 +234,9 @@ export namespace Installation { | ||
| result = await Process.run(["npm", "install", "-g", `@altimateai/altimate-code@${target}`], { nothrow: true }) | ||
| break | ||
| case "pnpm": | ||
| result = await Process.run(["pnpm", "install", "-g", `@altimateai/altimate-code@${target}`], { nothrow: true }) | ||
| // altimate_change start — pnpm needs --force to override cached version on upgrade | ||
| result = await Process.run(["pnpm", "install", "-g", "--force", `@altimateai/altimate-code@${target}`], { nothrow: true }) | ||
| // altimate_change end | ||
| break | ||
| case "bun": | ||
| result = await Process.run(["bun", "install", "-g", `@altimateai/altimate-code@${target}`], { nothrow: true }) | ||
| @@ -230,11 +280,10 @@ export namespace Installation { | ||
| default: | ||
| throw new Error(`Unknown method: ${method}`) | ||
| } | ||
| // altimate_change start — telemetry for upgrade result | ||
| // altimate_change start — telemetry for upgrade result + actual error messages | ||
| const telemetryMethod = (["npm", "bun", "brew"].includes(method) ? method : "other") as "npm" | "bun" | "brew" | "other" | ||
| if (!result || result.code !== 0) { | ||
| const stderr = | ||
| method === "choco" ? "not running from an elevated command shell" : result?.stderr.toString("utf8") || "" | ||
| const stderr = result?.stderr.toString("utf8") || "upgrade failed (unknown error)" | ||
| const T = await getTelemetry() | ||
| T.track({ | ||
| type: "upgrade_attempted", | ||
| @@ -282,19 +331,22 @@ export namespace Installation { | ||
| if (detectedMethod === "brew") { | ||
| const formula = await getBrewFormula() | ||
| if (formula.includes("/")) { | ||
| // altimate_change start — safe JSON parse for brew info | ||
| const infoJson = await text(["brew", "info", "--json=v2", formula]) | ||
| const info = JSON.parse(infoJson) | ||
| const version = info.formulae?.[0]?.versions?.stable | ||
| if (!version) throw new Error(`Could not detect version for tap formula: ${formula}`) | ||
| return version | ||
| try { | ||
| const info = JSON.parse(infoJson) | ||
| const version = info.formulae?.[0]?.versions?.stable | ||
| if (!version) throw new Error(`Could not detect version for tap formula: ${formula}`) | ||
| return version | ||
| } catch (e: any) { | ||
| throw new Error(`Failed to parse brew info for ${formula}: ${e.message}`) | ||
| } | ||
| // altimate_change end | ||
| } | ||
| // altimate_change start — brew: use GitHub releases API as source of truth | ||
| // altimate-code is NOT in core homebrew, so formulae.brew.sh will 404. | ||
| // `brew info --json=v2` returns the LOCAL cached version which can be stale | ||
| // if the tap hasn't been updated — using it would cause `latest()` to return | ||
| // the already-installed version, making the upgrade command skip silently. | ||
| // GitHub releases API is the authoritative source for the actual latest version. | ||
| return fetch("https://api.github.com/repos/AltimateAI/altimate-code/releases/latest") | ||
| return fetchWithTimeout("https://api.github.com/repos/AltimateAI/altimate-code/releases/latest") | ||
| .then((res) => { | ||
| if (!res.ok) throw new Error(`GitHub releases API: ${res.status} ${res.statusText}`) | ||
| return res.json() | ||
| @@ -307,14 +359,20 @@ 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" | ||
| return reg.endsWith("/") ? reg.slice(0, -1) : reg | ||
| }) | ||
| 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,33 +382,55 @@ 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" } }, | ||
| ) | ||
| .then((res) => { | ||
| 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 | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.