Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 72 additions & 19 deletions packages/opencode/src/cli/cmd/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,46 @@ export const UninstallCommand = {
const method = await Installation.method()
prompts.log.info(`Installation method: ${method}`)

// altimate_change start — #1305: refuse BEFORE removing anything when we cannot tell what
// installed this binary.
//
// `unknown` means detection could not confirm an owner. The removal targets below always
// include data, config, cache and state, while the binary and the package-manager entry
// are only removed for a known method — so proceeding here wiped everything the user
// cares about and left the installation running, with no indication that had happened.
// Data loss with nothing uninstalled is strictly worse than declining.
if (method === "unknown") {
const win = process.platform === "win32"
const standalone = win ? "%USERPROFILE%\\.altimate\\bin" : "~/.altimate/bin"
prompts.log.error(`Cannot determine how altimate was installed (running from ${process.execPath}).`)
prompts.log.info("Uninstalling now would delete your data and config while leaving the program installed.")
prompts.log.info("Remove the program with whichever tool installed it — each has its own syntax:")
prompts.log.info(" npm: npm uninstall -g altimate-code")
prompts.log.info(" pnpm: pnpm uninstall -g altimate-code")
prompts.log.info(" bun: bun remove -g altimate-code")
prompts.log.info(" yarn: yarn global remove altimate-code")
prompts.log.info(" Homebrew: brew uninstall altimate-code")
prompts.log.info(` installer: delete the binary from ${standalone}`)
prompts.log.info("If you installed the scoped package, use @altimateai/altimate-code as the name instead.")
// Do not tell the user to "re-run" this command: once the package is gone, so is the
// binary that would run it. Name the directories so data can be cleaned up by hand.
prompts.log.info("Then delete these directories to remove data, config, cache and state:")
for (const dir of [Global.Path.data, Global.Path.config, Global.Path.cache, Global.Path.state]) {
prompts.log.info(` ${dir}`)
}
prompts.outro("Nothing was removed")
return
}
// altimate_change end

const targets = await collectRemovalTargets(args, method)

await showRemovalSummary(targets, method)
// altimate_change start — #1305: the package the MANAGER confirms owns this binary.
// publish.ts ships both a scoped and an unscoped wrapper; removing the wrong one removes
// nothing while uninstall goes on to delete config and cache.
const pkg = (await Installation.packageName()) ?? "@altimateai/altimate-code"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: If the second ownership lookup fails or changes after method() succeeds, this fallback silently targets the scoped package instead of the verified package. Fail closed when packageName() is missing, or reuse the owner from the initial verification, before deleting data and running the package manager.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/cmd/uninstall.ts, line 90:

<comment>If the second ownership lookup fails or changes after `method()` succeeds, this fallback silently targets the scoped package instead of the verified package. Fail closed when `packageName()` is missing, or reuse the owner from the initial verification, before deleting data and running the package manager.</comment>

<file context>
@@ -62,9 +62,34 @@ export const UninstallCommand = {
+    // altimate_change start — #1305: the package the MANAGER confirms owns this binary.
+    // publish.ts ships both a scoped and an unscoped wrapper; removing the wrong one removes
+    // nothing while uninstall goes on to delete config and cache.
+    const pkg = (await Installation.packageName()) ?? "@altimateai/altimate-code"
+    await showRemovalSummary(targets, method, pkg)
+    // altimate_change end
</file context>

await showRemovalSummary(targets, method, pkg)
// altimate_change end

if (!args.force && !args.dryRun) {
const confirm = await prompts.confirm({
Expand All @@ -83,7 +120,10 @@ export const UninstallCommand = {
return
}

await executeUninstall(method, targets)
// altimate_change start — #1305: pass the verified package name through so removal
// targets the wrapper the user actually installed.
await executeUninstall(method, targets, pkg)
// altimate_change end

prompts.outro("Done")
},
Expand All @@ -103,7 +143,10 @@ async function collectRemovalTargets(args: UninstallArgs, method: Installation.M
return { directories, shellConfig, binary }
}

async function showRemovalSummary(targets: RemovalTargets, method: Installation.Method) {
// altimate_change start — #1305: takes the verified package name so the summary prints the
// command that will actually run.
async function showRemovalSummary(targets: RemovalTargets, method: Installation.Method, pkg: string) {
// altimate_change end
prompts.log.message("The following will be removed:")

for (const dir of targets.directories) {
Expand All @@ -130,20 +173,26 @@ async function showRemovalSummary(targets: RemovalTargets, method: Installation.
}

if (method !== "curl" && method !== "unknown") {
// altimate_change start — #1305: these targeted upstream's `opencode-ai` / `opencode`,
// so an uninstall could remove an unrelated upstream package while leaving Altimate
// installed. scoop/choco are omitted: Installation.method() no longer returns them
// (their commands still reference upstream identities), so they are unreachable here.
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",
choco: "choco uninstall opencode",
scoop: "scoop uninstall opencode",
npm: `npm uninstall -g ${pkg}`,
pnpm: `pnpm uninstall -g ${pkg}`,
bun: `bun remove -g ${pkg}`,
yarn: `yarn global remove ${pkg}`,
brew: "brew uninstall altimate-code",
}
// altimate_change end
prompts.log.info(` ✓ Package: ${cmds[method] || method}`)
}
}

async function executeUninstall(method: Installation.Method, targets: RemovalTargets) {
// altimate_change start — #1305: takes the verified package name so removal targets the
// wrapper the user actually installed.
async function executeUninstall(method: Installation.Method, targets: RemovalTargets, pkg: string) {
// altimate_change end
const spinner = prompts.spinner()
const errors: string[] = []

Expand Down Expand Up @@ -181,22 +230,26 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar
}

if (method !== "curl" && method !== "unknown") {
// altimate_change start — #1305: Altimate package identities, not upstream's.
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"],
choco: ["choco", "uninstall", "opencode"],
scoop: ["scoop", "uninstall", "opencode"],
npm: ["npm", "uninstall", "-g", pkg],
pnpm: ["pnpm", "uninstall", "-g", pkg],
bun: ["bun", "remove", "-g", pkg],
yarn: ["yarn", "global", "remove", pkg],
brew: ["brew", "uninstall", "altimate-code"],
}
// altimate_change end

const cmd = cmds[method]
if (cmd) {
spinner.start(`Running ${cmd.join(" ")}...`)
const result = await Process.run(method === "choco" ? ["choco", "uninstall", "opencode", "-y", "-r"] : cmd, {
// altimate_change start — #1305: the choco special-case here passed a hardcoded
// `["choco","uninstall","opencode",...]`; choco is no longer a reachable method (see
// the command map above), so the branch is gone and `cmd` is used directly.
const result = await Process.run(cmd, {
nothrow: true,
})
// altimate_change end
if (result.code !== 0) {
spinner.stop(`Package manager uninstall failed: exit code ${result.code}`, 1)
const text = `${result.stdout.toString("utf8")}\n${result.stderr.toString("utf8")}`
Expand Down
48 changes: 31 additions & 17 deletions packages/opencode/src/cli/cmd/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export const UpgradeCommand = {
alias: "m",
describe: "installation method to use",
type: "string",
choices: ["curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"],
// altimate_change start — #1305: keep in step with UNSUPPORTED_UPGRADE_METHODS.
// choco/scoop were offered here but Installation.upgrade() always refuses them, so
// selecting either could only fail.
choices: ["curl", "npm", "pnpm", "bun", "brew"],
// altimate_change end
})
},
handler: async (args: { target?: string; method?: string }) => {
Expand All @@ -46,23 +50,33 @@ export const UpgradeCommand = {
// altimate_change end
const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") {
// altimate_change start — branding
prompts.log.error(`altimate is installed to ${process.execPath} and may be managed by a package manager`)
// altimate_change end
const install = await prompts.select({
message: "Install anyways?",
options: [
{ label: "Yes", value: true },
{ label: "No", value: false },
],
initialValue: false,
})
if (!install) {
prompts.outro("Done")
return
}
// altimate_change start — #1305: stop instead of offering a choice that cannot work.
// `Installation.upgrade()` refuses every method in UNSUPPORTED_UPGRADE_METHODS, so the
// old "Install anyways?" prompt ended in `UpgradeFailedError: Unknown installation
// method` whichever way the user answered — and detection now returns `unknown` for
// anything it cannot verify, which made that dead end much more common.
if (Installation.UNSUPPORTED_UPGRADE_METHODS.includes(method)) {
prompts.log.error(
method === "unknown"
? `Cannot determine how altimate was installed (running from ${process.execPath}).`
: `Upgrading a ${method} installation is not supported.`,
)
prompts.log.info("Upgrade with whichever tool installed it:")
prompts.log.info(" npm: npm install -g altimate-code@latest")
prompts.log.info(" pnpm: pnpm install -g altimate-code@latest")
prompts.log.info(" bun: bun install -g altimate-code@latest")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When method is yarn, this recovery message gives no Yarn command even though Yarn installs are detected and routed here. Add yarn global add altimate-code@latest to the manual upgrade options.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/cmd/upgrade.ts, line 67:

<comment>When `method` is `yarn`, this recovery message gives no Yarn command even though Yarn installs are detected and routed here. Add `yarn global add altimate-code@latest` to the manual upgrade options.</comment>

<file context>
@@ -57,10 +61,17 @@ export const UpgradeCommand = {
+      prompts.log.info("Upgrade with whichever tool installed it:")
+      prompts.log.info("  npm:       npm install -g altimate-code@latest")
+      prompts.log.info("  pnpm:      pnpm install -g altimate-code@latest")
+      prompts.log.info("  bun:       bun install -g altimate-code@latest")
+      prompts.log.info("  Homebrew:  brew upgrade altimate-code")
+      prompts.log.info(
</file context>
Suggested change
prompts.log.info(" bun: bun install -g altimate-code@latest")
prompts.log.info(" bun: bun install -g altimate-code@latest")
prompts.log.info(" yarn: yarn global add altimate-code@latest")

prompts.log.info(" Homebrew: brew upgrade altimate-code")
prompts.log.info(
process.platform === "win32"
? " installer: irm https://www.altimate.sh/install.ps1 | iex"
: " installer: curl -fsSL https://www.altimate.sh/install | bash",
)
prompts.log.info("If you installed the scoped package, use @altimateai/altimate-code as the name instead.")
prompts.log.info("Or force a specific manager with --method <npm|pnpm|bun|brew|curl>.")
prompts.outro("Done")
return
}
// altimate_change end
prompts.log.info("Using method: " + method)
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()

Expand Down
Loading
Loading