From 8e2fda870aabcb856ba52872b9f848242e95acd8 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sat, 27 Dec 2025 02:07:55 +0900 Subject: [PATCH] feat: add get-local-version CLI command for version checking (#262) - Add new CLI command 'get-local-version' to display current version and check for updates - Reuses existing version checking infrastructure from auto-update-checker - Supports both human-readable and JSON output formats - Handles edge cases: local dev mode, pinned versions, network errors - Provides colored terminal output with picocolors - Closes #260 Co-authored-by: sisyphus-dev-ai --- src/cli/get-local-version/formatter.ts | 66 ++++++++++++++++ src/cli/get-local-version/index.ts | 104 +++++++++++++++++++++++++ src/cli/get-local-version/types.ts | 14 ++++ src/cli/index.ts | 28 +++++++ 4 files changed, 212 insertions(+) create mode 100644 src/cli/get-local-version/formatter.ts create mode 100644 src/cli/get-local-version/index.ts create mode 100644 src/cli/get-local-version/types.ts diff --git a/src/cli/get-local-version/formatter.ts b/src/cli/get-local-version/formatter.ts new file mode 100644 index 0000000..b65f22b --- /dev/null +++ b/src/cli/get-local-version/formatter.ts @@ -0,0 +1,66 @@ +import color from "picocolors" +import type { VersionInfo } from "./types" + +const SYMBOLS = { + check: color.green("✓"), + cross: color.red("✗"), + arrow: color.cyan("→"), + info: color.blue("ℹ"), + warn: color.yellow("⚠"), + pin: color.magenta("📌"), + dev: color.cyan("🔧"), +} + +export function formatVersionOutput(info: VersionInfo): string { + const lines: string[] = [] + + lines.push("") + lines.push(color.bold(color.white("oh-my-opencode Version Information"))) + lines.push(color.dim("─".repeat(50))) + lines.push("") + + if (info.currentVersion) { + lines.push(` Current Version: ${color.cyan(info.currentVersion)}`) + } else { + lines.push(` Current Version: ${color.dim("unknown")}`) + } + + if (!info.isLocalDev && info.latestVersion) { + lines.push(` Latest Version: ${color.cyan(info.latestVersion)}`) + } + + lines.push("") + + switch (info.status) { + case "up-to-date": + lines.push(` ${SYMBOLS.check} ${color.green("You're up to date!")}`) + break + case "outdated": + lines.push(` ${SYMBOLS.warn} ${color.yellow("Update available")}`) + lines.push(` ${color.dim("Run:")} ${color.cyan("cd ~/.config/opencode && bun update oh-my-opencode")}`) + break + case "local-dev": + lines.push(` ${SYMBOLS.dev} ${color.cyan("Running in local development mode")}`) + lines.push(` ${color.dim("Using file:// protocol from config")}`) + break + case "pinned": + lines.push(` ${SYMBOLS.pin} ${color.magenta(`Version pinned to ${info.pinnedVersion}`)}`) + lines.push(` ${color.dim("Update check skipped for pinned versions")}`) + break + case "error": + lines.push(` ${SYMBOLS.cross} ${color.red("Unable to check for updates")}`) + lines.push(` ${color.dim("Network error or npm registry unavailable")}`) + break + case "unknown": + lines.push(` ${SYMBOLS.info} ${color.yellow("Version information unavailable")}`) + break + } + + lines.push("") + + return lines.join("\n") +} + +export function formatJsonOutput(info: VersionInfo): string { + return JSON.stringify(info, null, 2) +} diff --git a/src/cli/get-local-version/index.ts b/src/cli/get-local-version/index.ts new file mode 100644 index 0000000..06a2936 --- /dev/null +++ b/src/cli/get-local-version/index.ts @@ -0,0 +1,104 @@ +import { getCachedVersion, getLatestVersion, isLocalDevMode, findPluginEntry } from "../../hooks/auto-update-checker/checker" +import type { GetLocalVersionOptions, VersionInfo } from "./types" +import { formatVersionOutput, formatJsonOutput } from "./formatter" + +export async function getLocalVersion(options: GetLocalVersionOptions = {}): Promise { + const directory = options.directory ?? process.cwd() + + try { + if (isLocalDevMode(directory)) { + const currentVersion = getCachedVersion() + const info: VersionInfo = { + currentVersion, + latestVersion: null, + isUpToDate: false, + isLocalDev: true, + isPinned: false, + pinnedVersion: null, + status: "local-dev", + } + + console.log(options.json ? formatJsonOutput(info) : formatVersionOutput(info)) + return 0 + } + + const pluginInfo = findPluginEntry(directory) + if (pluginInfo?.isPinned) { + const info: VersionInfo = { + currentVersion: pluginInfo.pinnedVersion, + latestVersion: null, + isUpToDate: false, + isLocalDev: false, + isPinned: true, + pinnedVersion: pluginInfo.pinnedVersion, + status: "pinned", + } + + console.log(options.json ? formatJsonOutput(info) : formatVersionOutput(info)) + return 0 + } + + const currentVersion = getCachedVersion() + if (!currentVersion) { + const info: VersionInfo = { + currentVersion: null, + latestVersion: null, + isUpToDate: false, + isLocalDev: false, + isPinned: false, + pinnedVersion: null, + status: "unknown", + } + + console.log(options.json ? formatJsonOutput(info) : formatVersionOutput(info)) + return 1 + } + + const latestVersion = await getLatestVersion() + + if (!latestVersion) { + const info: VersionInfo = { + currentVersion, + latestVersion: null, + isUpToDate: false, + isLocalDev: false, + isPinned: false, + pinnedVersion: null, + status: "error", + } + + console.log(options.json ? formatJsonOutput(info) : formatVersionOutput(info)) + return 0 + } + + const isUpToDate = currentVersion === latestVersion + const info: VersionInfo = { + currentVersion, + latestVersion, + isUpToDate, + isLocalDev: false, + isPinned: false, + pinnedVersion: null, + status: isUpToDate ? "up-to-date" : "outdated", + } + + console.log(options.json ? formatJsonOutput(info) : formatVersionOutput(info)) + return 0 + + } catch (error) { + const info: VersionInfo = { + currentVersion: null, + latestVersion: null, + isUpToDate: false, + isLocalDev: false, + isPinned: false, + pinnedVersion: null, + status: "error", + } + + console.log(options.json ? formatJsonOutput(info) : formatVersionOutput(info)) + return 1 + } +} + +export * from "./types" diff --git a/src/cli/get-local-version/types.ts b/src/cli/get-local-version/types.ts new file mode 100644 index 0000000..a791774 --- /dev/null +++ b/src/cli/get-local-version/types.ts @@ -0,0 +1,14 @@ +export interface VersionInfo { + currentVersion: string | null + latestVersion: string | null + isUpToDate: boolean + isLocalDev: boolean + isPinned: boolean + pinnedVersion: string | null + status: "up-to-date" | "outdated" | "local-dev" | "pinned" | "error" | "unknown" +} + +export interface GetLocalVersionOptions { + directory?: string + json?: boolean +} diff --git a/src/cli/index.ts b/src/cli/index.ts index edbe768..6301e39 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,8 +2,10 @@ import { Command } from "commander" import { install } from "./install" import { run } from "./run" +import { getLocalVersion } from "./get-local-version" import type { InstallArgs } from "./types" import type { RunOptions } from "./run" +import type { GetLocalVersionOptions } from "./get-local-version/types" const packageJson = await import("../../package.json") const VERSION = packageJson.version @@ -73,6 +75,32 @@ Unlike 'opencode run', this command waits until: process.exit(exitCode) }) +program + .command("get-local-version") + .description("Show current installed version and check for updates") + .option("-d, --directory ", "Working directory to check config from") + .option("--json", "Output in JSON format for scripting") + .addHelpText("after", ` +Examples: + $ bunx oh-my-opencode get-local-version + $ bunx oh-my-opencode get-local-version --json + $ bunx oh-my-opencode get-local-version --directory /path/to/project + +This command shows: + - Current installed version + - Latest available version on npm + - Whether you're up to date + - Special modes (local dev, pinned version) +`) + .action(async (options) => { + const versionOptions: GetLocalVersionOptions = { + directory: options.directory, + json: options.json ?? false, + } + const exitCode = await getLocalVersion(versionOptions) + process.exit(exitCode) + }) + program .command("version") .description("Show version information")