diff --git a/.changeset/cli-upgrade-command.md b/.changeset/cli-upgrade-command.md new file mode 100644 index 0000000..00d542b --- /dev/null +++ b/.changeset/cli-upgrade-command.md @@ -0,0 +1,14 @@ +--- +"sideshow": minor +--- + +Add `sideshow upgrade` to self-update the CLI to the latest npm release. + +- `sideshow upgrade` fetches the latest published version and, when newer, + runs `npm install -g sideshow@` for you. +- `--check` reports whether an update is available without installing; + `--dry-run` prints the install command instead of running it. +- It refuses on a development checkout (a published package ships no `.git`) + with a `git pull && npm install` hint, so it never clobbers a source tree. +- `sideshow version` now points users at `sideshow upgrade` instead of the + manual `npm install -g sideshow`, and both share one cached registry check. diff --git a/bin/sideshow.js b/bin/sideshow.js index 6c9058f..7f03c20 100755 --- a/bin/sideshow.js +++ b/bin/sideshow.js @@ -136,6 +136,9 @@ usage: sideshow setup print the AGENTS.md integration block sideshow agent-howto print current agent how-to sideshow version show version and check for updates + sideshow upgrade [--check] [--dry-run] update the CLI to the latest npm release + --check report whether an update is available, don't install + --dry-run print the install command instead of running it sideshow mcp run the stdio MCP server (for agent configs) flags: @@ -603,6 +606,34 @@ function writeUpdateCache(version) { } catch {} } +// Resolve the latest published version from the npm registry. Uses the 24h disk +// cache unless `force` is set (the `upgrade` command wants a fresh answer before +// installing). Returns null when offline / timed out / the registry errors, so +// callers stay best-effort. +async function fetchLatestVersion({ force = false } = {}) { + if (!force) { + const cached = readUpdateCache(); + if (cached) return cached; + } + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), 3000); + try { + const res = await fetch("https://registry.npmjs.org/sideshow/latest", { signal: ctrl.signal }); + if (res.ok) { + const pkg = await res.json(); + if (typeof pkg.version === "string") { + writeUpdateCache(pkg.version); + return pkg.version; + } + } + } catch { + // Offline / timed out / registry error — caller treats null as "unknown". + } finally { + clearTimeout(timer); + } + return null; +} + // One comment → one line (one monitor notification). Newlines are collapsed so // a multi-line comment stays a single notification. function watchLine(c) { @@ -1602,35 +1633,52 @@ const commands = { parse(); console.log(`sideshow ${PKG_VERSION}`); try { - const cached = readUpdateCache(); - let latest = cached; - if (!latest) { - const ctrl = new AbortController(); - const timer = setTimeout(() => ctrl.abort(), 3000); - try { - const res = await fetch("https://registry.npmjs.org/sideshow/latest", { - signal: ctrl.signal, - }); - clearTimeout(timer); - if (res.ok) { - const pkg = await res.json(); - if (typeof pkg.version === "string") { - latest = pkg.version; - writeUpdateCache(latest); - } - } - } catch { - // Offline / timed out — skip silently. - } - } + const latest = await fetchLatestVersion(); if (latest && versionGt(latest, PKG_VERSION)) { console.log(`\nUpdate available: ${PKG_VERSION} → ${latest}`); - console.log(`Run: npm install -g sideshow`); + console.log(`Run: sideshow upgrade`); } } catch { // Never let the update check fail the command. } }, + + // Self-update the globally installed CLI to the latest npm release. Refuses on + // a development checkout (a published package ships no .git) — there you + // upgrade with git. --check reports without installing; --dry-run prints the + // exact install command instead of running it. + async upgrade() { + const { values: flags } = parse({ + options: { check: { type: "boolean" }, "dry-run": { type: "boolean" } }, + }); + const latest = await fetchLatestVersion({ force: true }); + if (!latest) fail("could not reach the npm registry — check your connection and retry"); + if (!versionGt(latest, PKG_VERSION)) { + console.log(`sideshow ${PKG_VERSION} is already up to date.`); + return; + } + if (flags.check) { + console.log(`Update available: ${PKG_VERSION} → ${latest}`); + console.log(`Run: sideshow upgrade`); + return; + } + if (existsSync(join(ROOT, ".git"))) { + fail(`development checkout at ${ROOT} — upgrade with: git pull && npm install`); + } + const spec = `sideshow@${latest}`; + if (flags["dry-run"]) { + console.log(`npm install -g ${spec}`); + return; + } + console.log(`Upgrading sideshow ${PKG_VERSION} → ${latest}…`); + const npm = process.platform === "win32" ? "npm.cmd" : "npm"; + try { + execFileSync(npm, ["install", "-g", spec], { stdio: "inherit" }); + } catch { + fail(`upgrade failed — try manually: npm install -g ${spec}`); + } + console.log(`\nUpgraded to sideshow ${latest}.`); + }, }; async function fetchTextWithFallback(path, localFile) { diff --git a/package-lock.json b/package-lock.json index dcabd98..18e9d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sideshow", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sideshow", - "version": "0.8.0", + "version": "0.9.0", "license": "MIT", "dependencies": { "@hono/node-server": "^1.14.0", diff --git a/server/index.ts b/server/index.ts index db522b4..67e0938 100644 --- a/server/index.ts +++ b/server/index.ts @@ -79,7 +79,7 @@ const app = createApp({ // SIDESHOW_VERSION fakes the running version (manual testing of the // notice); set it to the empty string to disable the update check version: process.env.SIDESHOW_VERSION ?? (JSON.parse(pkgJson) as { version: string }).version, - upgradeCommand: "npm install -g sideshow", + upgradeCommand: "sideshow upgrade", }); const port = Number(process.env.PORT ?? 8228);