import type { Argv } from "yargs"
import { UI } from "../ui"
import { formatCliHelpBannerLogoBlock } from "./tui/component/banner-logo"
import * as prompts from "@clack/prompts"
import { Installation } from "../../installation"
import { Global } from "@opencode-ai/core/global"
import fs from "fs/promises"
import path from "path"
import os from "os"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
interface UninstallArgs {
keepConfig: boolean
keepData: boolean
dryRun: boolean
force: boolean
}
interface RemovalTargets {
directories: Array<{ path: string; label: string; keep: boolean }>
shellConfig: string | null
binary: string | null
}
export const UninstallCommand = {
command: "uninstall",
describe: "uninstall deveco and remove all related files",
builder: (yargs: Argv) =>
yargs
.option("keep-config", {
alias: "c",
type: "boolean",
describe: "keep configuration files",
default: false,
})
.option("keep-data", {
alias: "d",
type: "boolean",
describe: "keep session data and snapshots",
default: false,
})
.option("dry-run", {
type: "boolean",
describe: "show what would be removed without removing",
default: false,
})
.option("force", {
alias: "f",
type: "boolean",
describe: "skip confirmation prompts",
default: false,
}),
handler: async (args: UninstallArgs) => {
UI.empty()
const cols = process.stderr.columns ?? process.stdout.columns
UI.println(formatCliHelpBannerLogoBlock(cols))
UI.empty()
prompts.intro("Uninstall DevEco Code")
const method = await Installation.method()
prompts.log.info(`Installation method: ${method}`)
const targets = await collectRemovalTargets(args, method)
await showRemovalSummary(targets, method)
if (!args.force && !args.dryRun) {
const confirm = await prompts.confirm({
message: "Are you sure you want to uninstall?",
initialValue: false,
})
if (!confirm || prompts.isCancel(confirm)) {
prompts.outro("Cancelled")
return
}
}
if (args.dryRun) {
prompts.log.warn("Dry run - no changes made")
prompts.outro("Done")
return
}
await executeUninstall(method, targets)
prompts.outro("Done")
},
}
async function collectRemovalTargets(args: UninstallArgs, method: Installation.Method): Promise<RemovalTargets> {
const directories: RemovalTargets["directories"] = [
{ path: Global.Path.data, label: "Data", keep: args.keepData },
{ path: Global.Path.cache, label: "Cache", keep: false },
{ path: Global.Path.config, label: "Config", keep: args.keepConfig },
{ path: Global.Path.state, label: "State", keep: false },
]
return { directories, shellConfig: null, binary: null }
}
async function showRemovalSummary(targets: RemovalTargets, method: Installation.Method) {
prompts.log.message("The following will be removed:")
for (const dir of targets.directories) {
const exists = await fs
.access(dir.path)
.then(() => true)
.catch(() => false)
if (!exists) continue
const size = await getDirectorySize(dir.path)
const sizeStr = formatSize(size)
const status = dir.keep ? UI.Style.TEXT_DIM + "(keeping)" : ""
const prefix = dir.keep ? "○" : "✓"
prompts.log.info(` ${prefix} ${dir.label}: ${shortenPath(dir.path)} ${UI.Style.TEXT_DIM}(${sizeStr})${status}`)
}
if (targets.binary) {
prompts.log.info(` ✓ Binary: ${shortenPath(targets.binary)}`)
}
if (targets.shellConfig) {
prompts.log.info(` ✓ Shell PATH in ${shortenPath(targets.shellConfig)}`)
}
if (method !== "unknown") {
const cmds: Record<string, string> = {
npm: "npm uninstall -g @deveco/deveco-code",
pnpm: "pnpm uninstall -g @deveco/deveco-code",
bun: "bun remove -g @deveco/deveco-code",
}
prompts.log.info(` ✓ Package: ${cmds[method] || method}`)
}
}
async function executeUninstall(method: Installation.Method, targets: RemovalTargets) {
const spinner = prompts.spinner()
const errors: string[] = []
for (const dir of targets.directories) {
if (dir.keep) {
prompts.log.step(`Skipping ${dir.label} (--keep-${dir.label.toLowerCase()})`)
continue
}
const exists = await fs
.access(dir.path)
.then(() => true)
.catch(() => false)
if (!exists) continue
spinner.start(`Removing ${dir.label}...`)
const err = await fs.rm(dir.path, { recursive: true, force: true }).catch((e) => e)
if (err) {
spinner.stop(`Failed to remove ${dir.label}`, 1)
errors.push(`${dir.label}: ${err.message}`)
continue
}
spinner.stop(`Removed ${dir.label}`)
}
if (method !== "unknown") {
const cmds: Record<string, string[]> = {
npm: ["npm", "uninstall", "-g", "@deveco/deveco-code"],
pnpm: ["pnpm", "uninstall", "-g", "@deveco/deveco-code"],
bun: ["bun", "remove", "-g", "@deveco/deveco-code"],
}
const cmd = cmds[method]
if (cmd) {
spinner.start(`Running ${cmd.join(" ")}...`)
const result = await Process.run(cmd, { nothrow: true })
if (result.code !== 0) {
spinner.stop(`Package manager uninstall failed: exit code ${result.code}`, 1)
prompts.log.warn(`You may need to run manually: ${cmd.join(" ")}`)
} else {
spinner.stop("Package removed")
}
}
}
if (errors.length > 0) {
UI.empty()
prompts.log.warn("Some operations failed:")
for (const err of errors) {
prompts.log.error(` ${err}`)
}
}
UI.empty()
prompts.log.success("Thank you for using DevEco Code!")
}
async function getDirectorySize(dir: string): Promise<number> {
let total = 0
const walk = async (current: string) => {
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => [])
for (const entry of entries) {
const full = path.join(current, entry.name)
if (entry.isDirectory()) {
await walk(full)
continue
}
if (entry.isFile()) {
const stat = await fs.stat(full).catch(() => null)
if (stat) total += stat.size
}
}
}
await walk(dir)
return total
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
}
function shortenPath(p: string): string {
const home = os.homedir()
if (p.startsWith(home)) {
return p.replace(home, "~")
}
return p
}