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 <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
66
src/cli/get-local-version/formatter.ts
Normal file
66
src/cli/get-local-version/formatter.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
104
src/cli/get-local-version/index.ts
Normal file
104
src/cli/get-local-version/index.ts
Normal file
@@ -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<number> {
|
||||||
|
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"
|
||||||
14
src/cli/get-local-version/types.ts
Normal file
14
src/cli/get-local-version/types.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
import { Command } from "commander"
|
import { Command } from "commander"
|
||||||
import { install } from "./install"
|
import { install } from "./install"
|
||||||
import { run } from "./run"
|
import { run } from "./run"
|
||||||
|
import { getLocalVersion } from "./get-local-version"
|
||||||
import type { InstallArgs } from "./types"
|
import type { InstallArgs } from "./types"
|
||||||
import type { RunOptions } from "./run"
|
import type { RunOptions } from "./run"
|
||||||
|
import type { GetLocalVersionOptions } from "./get-local-version/types"
|
||||||
|
|
||||||
const packageJson = await import("../../package.json")
|
const packageJson = await import("../../package.json")
|
||||||
const VERSION = packageJson.version
|
const VERSION = packageJson.version
|
||||||
@@ -73,6 +75,32 @@ Unlike 'opencode run', this command waits until:
|
|||||||
process.exit(exitCode)
|
process.exit(exitCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("get-local-version")
|
||||||
|
.description("Show current installed version and check for updates")
|
||||||
|
.option("-d, --directory <path>", "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
|
program
|
||||||
.command("version")
|
.command("version")
|
||||||
.description("Show version information")
|
.description("Show version information")
|
||||||
|
|||||||
Reference in New Issue
Block a user