diff --git a/CHANGELOG.md b/CHANGELOG.md index 90fb010..28f8c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] - 2026-07-19 + +### Added + +- `ccal version` (and `--version` / `-v`) prints the installed version. +- `ccal update` checks npm for a newer release and self-updates via + `npm install -g` when one is available. + ## [1.1.0] - 2026-07-19 ### Added @@ -35,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and common install locations. - Permissive CORS for browser and webview clients. -[Unreleased]: https://github.com/rousan/ccal/compare/v1.1.0...HEAD +[Unreleased]: https://github.com/rousan/ccal/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/rousan/ccal/releases/tag/v1.2.0 [1.1.0]: https://github.com/rousan/ccal/releases/tag/v1.1.0 [1.0.0]: https://github.com/rousan/ccal/releases/tag/v1.0.0 diff --git a/README.md b/README.md index d94fc57..35192bd 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,14 @@ provider with base URL `http://127.0.0.1:8787/v1`, any API key, and pick one of one response out — claude's streaming JSON output is translated back into OpenAI chunks (assistant text plus compact one-line notes for any tool calls). +## Commands + +``` +ccal serve [options] Start the OpenAI-compatible server +ccal update Update ccal to the latest published version +ccal version Print the installed version (also: --version, -v) +``` + ## Configuration ``` diff --git a/package.json b/package.json index 10c54c0..45b63c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rousan/ccal", - "version": "1.1.0", + "version": "1.2.0", "description": "Claude Code Adapter for LLMs — an OpenAI-compatible HTTP server that proxies to the local `claude` CLI.", "type": "module", "license": "MIT", diff --git a/src/cli.ts b/src/cli.ts index 1c4d503..e2ae357 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,11 +7,63 @@ // We keep argument parsing deliberately small and dependency-free — the surface // is tiny, so a hand-rolled parser is clearer than pulling in a CLI framework. +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; + import { serve } from "@hono/node-server"; import { createServer } from "./server.js"; import { checkClaude } from "./claude-binary.js"; +// The installed version, read from the package manifest (one dir up from dist/). +const VERSION = ( + createRequire(import.meta.url)("../package.json") as { version: string } +).version; + +/** GitHub release notes URL for a version, shown after a successful update. */ +const releaseUrl = (v: string) => + `https://github.com/rousan/ccal/releases/tag/v${v}`; + +/** Latest published version on npm, or null if the registry is unreachable. */ +async function latestVersion(): Promise { + try { + const res = await fetch("https://registry.npmjs.org/@rousan%2fccal/latest", { + headers: { accept: "application/json" }, + }); + if (!res.ok) return null; + const body = (await res.json()) as { version?: string }; + return body.version ?? null; + } catch { + return null; + } +} + +/** Compare npm's latest against the running version and self-update if newer. */ +async function runUpdate(): Promise { + console.log(`Installed: ccal ${VERSION}`); + const latest = await latestVersion(); + if (!latest) { + fail("Couldn't reach the npm registry to check for updates."); + } + if (latest === VERSION) { + console.log("ccal is already up to date."); + return; + } + console.log(`Updating ccal ${VERSION} -> ${latest}...`); + const result = spawnSync("npm", ["install", "-g", `@rousan/ccal@${latest}`], { + stdio: "inherit", + }); + if (result.status === 0) { + console.log(`Updated to ccal ${latest}.`); + console.log(`Release notes: ${releaseUrl(latest)}`); + } else { + fail( + "Automatic update failed. Update manually with:\n" + + " npm install -g @rousan/ccal@latest", + ); + } +} + // Parsed options for the `serve` subcommand. interface ServeOptions { port: number; @@ -95,9 +147,11 @@ function printHelp(): void { "An OpenAI-compatible HTTP server that proxies to the local `claude` CLI.", "", "Usage:", - " ccal serve [options]", + " ccal serve [options] Start the OpenAI-compatible server", + " ccal update Update ccal to the latest published version", + " ccal version Print the installed version", "", - "Options:", + "Serve options:", " --port Port to bind (default: 8787)", " --host Host to bind (default: 127.0.0.1)", " --cwd Working directory for the claude process", @@ -110,7 +164,7 @@ function printHelp(): void { ); } -function main(): void { +async function main(): Promise { const argv = process.argv.slice(2); if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") { @@ -119,8 +173,21 @@ function main(): void { } const command = argv[0]; + + if (command === "version" || command === "--version" || command === "-v") { + console.log(`ccal ${VERSION}`); + return; + } + + if (command === "update") { + await runUpdate(); + return; + } + if (command !== "serve") { - fail(`Unknown command: ${command}. Try 'ccal serve'.`); + fail( + `Unknown command: ${command}. Try 'ccal serve', 'ccal update', or 'ccal version'.`, + ); } // Allow `ccal serve --help` to print usage rather than start the server. @@ -156,4 +223,4 @@ function main(): void { }); } -main(); +main().catch((err) => fail(err instanceof Error ? err.message : String(err)));