Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cli-version-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sideshow": patch
---

Add `sideshow --version`, `-V`, and `version` subcommand. Prints the installed version and checks the npm registry for updates (best-effort, 3 s timeout, 24 h disk cache).
81 changes: 80 additions & 1 deletion bin/sideshow.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { parseArgs } from "node:util";
const BASE = (process.env.SIDESHOW_URL ?? "http://localhost:8228").replace(/\/$/, "");
const TOKEN = process.env.SIDESHOW_TOKEN;
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const PKG_VERSION = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
// This script's own path — used to register the Stop hook so it works whether
// or not `sideshow` is on PATH (a fresh clone, an npx run, a global install).
const SELF = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -115,8 +116,12 @@ usage:
sideshow guide print the design contract for posts
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 mcp run the stdio MCP server (for agent configs)

flags:
--version, -V print version and exit

environment:
SIDESHOW_URL server base URL (default http://localhost:8228; set to a
deployed instance, e.g. https://sideshow.you.workers.dev)
Expand Down Expand Up @@ -471,6 +476,43 @@ async function publishSurface(parts, flags) {

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Semver greater-than for plain x.y.z (mirrors server/app.ts versionGt).
function versionGt(a, b) {
const pa = a.split("-")[0].split(".").map(Number);
const pb = b.split("-")[0].split(".").map(Number);
for (let i = 0; i < 3; i++) {
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
if (d !== 0) return d > 0;
}
return false;
}

// Disk-cached update check so `sideshow version` doesn't hit the registry every
// time. TTL = 24 hours; stale/missing/corrupt cache is silently ignored.
const UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;

function updateCachePath() {
const dir = join(tmpdir(), `sideshow-${userInfo().username}`);
mkdirSync(dir, { recursive: true, mode: 0o700 });
return join(dir, "update-check.json");
}

function readUpdateCache() {
try {
const data = JSON.parse(readFileSync(updateCachePath(), "utf8"));
if (Date.now() - data.at < UPDATE_CACHE_TTL_MS && typeof data.version === "string") {
return data.version;
}
} catch {}
return null;
}

function writeUpdateCache(version) {
try {
writeFileSync(updateCachePath(), JSON.stringify({ at: Date.now(), version }));
} catch {}
}

// One comment → one line (one monitor notification). Newlines are collapsed so
// a multi-line comment stays a single notification.
function watchLine(c) {
Expand Down Expand Up @@ -1345,6 +1387,41 @@ const commands = {
parse();
console.log(await fetchTextWithFallback("/agent-howto", join(ROOT, "guide", "AGENT_HOWTO.md")));
},

// Print the running version and check for updates (non-blocking, best-effort).
async version() {
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.
}
}
if (latest && versionGt(latest, PKG_VERSION)) {
console.log(`\nUpdate available: ${PKG_VERSION} → ${latest}`);
console.log(`Run: npm install -g sideshow`);
}
} catch {
// Never let the update check fail the command.
}
},
};

async function fetchTextWithFallback(path, localFile) {
Expand All @@ -1355,7 +1432,9 @@ async function fetchTextWithFallback(path, localFile) {
return readFileSync(localFile, "utf8");
}

if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
if (cmd === "--version" || cmd === "-V") {
await commands.version();
} else if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
console.log(HELP);
} else if (commands[cmd]) {
await commands[cmd]();
Expand Down
16 changes: 16 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ const post = (url: string, body: unknown) =>
body: JSON.stringify(body),
}).then((r) => r.json() as Promise<any>);

// --- version ---

for (const flag of ["--version", "-V", "version"]) {
test(`${flag} prints the version`, async () => {
const { code, stdout } = await run(...(flag.startsWith("-") ? [flag] : [flag]));
assert.equal(code, 0);
assert.match(stdout, /^sideshow \d+\.\d+\.\d+/);
});
}

test("version runs end-to-end (update check is best-effort)", async () => {
const { code, stdout } = await run("version");
assert.equal(code, 0);
assert.match(stdout, /^sideshow \d+\.\d+\.\d+/);
});

// None of these reach the network: --help and option errors resolve in
// parsing, before any request (no server needs to be running).

Expand Down
Loading