diff --git a/packages/opencode/src/cli/cmd/cache.ts b/packages/opencode/src/cli/cmd/cache.ts new file mode 100644 index 00000000000..7985353efe5 --- /dev/null +++ b/packages/opencode/src/cli/cmd/cache.ts @@ -0,0 +1,119 @@ +import type { Argv } from "yargs" +import { cmd } from "./cmd" +import * as prompts from "@clack/prompts" +import { Global } from "../../global" +import { getDirectorySize, formatSize, shortenPath } from "../util" +import fs from "fs/promises" +import path from "path" + +const CacheCleanCommand = cmd({ + command: "clean", + describe: "remove cached plugins and packages", + builder: (yargs: Argv) => + yargs + .option("force", { + alias: "f", + type: "boolean", + describe: "skip confirmation prompt", + default: false, + }) + .option("dry-run", { + type: "boolean", + describe: "show what would be removed without removing", + default: false, + }), + async handler(args) { + const exists = await fs + .access(Global.Path.cache) + .then(() => true) + .catch(() => false) + + if (!exists) { + prompts.log.info("Cache directory does not exist") + return + } + + const size = await getDirectorySize(Global.Path.cache) + + prompts.log.info(`Cache: ${shortenPath(Global.Path.cache)} (${formatSize(size)})`) + + if (args.dryRun) { + prompts.log.warn("Dry run - no changes made") + return + } + + if (!args.force) { + const confirm = await prompts.confirm({ + message: "Remove cache directory?", + initialValue: true, + }) + if (!confirm || prompts.isCancel(confirm)) { + prompts.log.warn("Cancelled") + return + } + } + + const spinner = prompts.spinner() + spinner.start("Removing cache...") + + const err = await fs.rm(Global.Path.cache, { recursive: true, force: true }).catch((e) => e) + if (err) { + spinner.stop("Failed to remove cache", 1) + prompts.log.error(err.message) + return + } + + spinner.stop("Cache removed") + }, +}) + +const CacheInfoCommand = cmd({ + command: "info", + describe: "show cache directory information", + async handler() { + const exists = await fs + .access(Global.Path.cache) + .then(() => true) + .catch(() => false) + + prompts.log.info(`Path: ${shortenPath(Global.Path.cache)}`) + + if (!exists) { + prompts.log.info("Status: not created") + return + } + + const size = await getDirectorySize(Global.Path.cache) + prompts.log.info(`Size: ${formatSize(size)}`) + + const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json")) + const parsed = await pkgjson.json().catch(() => null) + + if (parsed?.dependencies) { + const deps = Object.entries(parsed.dependencies) + if (deps.length > 0) { + prompts.log.info(`Packages:`) + for (const [pkg, version] of deps) { + prompts.log.info(` ${pkg}@${version}`) + } + } + } + + const versionFile = Bun.file(path.join(Global.Path.cache, "version")) + const version = await versionFile.text().catch(() => null) + if (version) { + prompts.log.info(`Cache version: ${version.trim()}`) + } + }, +}) + +export const CacheCommand = cmd({ + command: "cache", + describe: "manage plugin and package cache", + builder: (yargs) => + yargs + .command(CacheCleanCommand) + .command(CacheInfoCommand) + .demandCommand(1, "Please specify a subcommand: clean or info"), + async handler() {}, +}) diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index 62210d57586..0bcf548a8be 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -3,6 +3,7 @@ import { UI } from "../ui" import * as prompts from "@clack/prompts" import { Installation } from "../../installation" import { Global } from "../../global" +import { getDirectorySize, formatSize, shortenPath } from "../util" import { $ } from "bun" import fs from "fs/promises" import path from "path" @@ -304,41 +305,3 @@ async function cleanShellConfig(file: string) { const output = filtered.join("\n") + "\n" await Bun.write(file, output) } - -async function getDirectorySize(dir: string): Promise { - 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 -} diff --git a/packages/opencode/src/cli/util.ts b/packages/opencode/src/cli/util.ts new file mode 100644 index 00000000000..b5940e6e404 --- /dev/null +++ b/packages/opencode/src/cli/util.ts @@ -0,0 +1,41 @@ +import fs from "fs/promises" +import path from "path" +import os from "os" + +export async function getDirectorySize(dir: string): Promise { + 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 +} + +export 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` +} + +export function shortenPath(p: string): string { + const home = os.homedir() + if (p.startsWith(home)) { + return p.replace(home, "~") + } + return p +} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 638ee7347db..07f54266812 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -27,6 +27,7 @@ import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" +import { CacheCommand } from "./cli/cmd/cache" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -98,6 +99,7 @@ const cli = yargs(hideBin(process.argv)) .command(GithubCommand) .command(PrCommand) .command(SessionCommand) + .command(CacheCommand) .fail((msg) => { if ( msg.startsWith("Unknown argument") ||