#!/usr/bin/env bun // One version for storebound: the npm CLI and the desktop app are one product and are released together (0.8.0 was // the first release with one number). The authority is `version` in the root package.json; it is copied into the // agent plugin manifests (written by tools/gen-adapters.ts), apps/desktop/package.json, src-tauri/tauri.conf.json, // src-tauri/Cargo.toml and the app's entry in src-tauri/Cargo.lock. `storebound desktop` installs, opens or updates the // app of exactly this version. // bun tools/desktop-version.ts check: exit 1 when any copy drifted (CI runs this) // bun tools/desktop-version.ts --set 0.8.0 write the new version into package.json and every copy // bun tools/desktop-version.ts --print print the version import { readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; export const ROOT = join(import.meta.dir, '..'); const TAURI = 'apps/desktop/src-tauri'; export const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/; export interface Copy { file: string; read(text: string): string | null; write(text: string, v: string): string } /** The first `"version": "..."` of a JSON file (top level in package.json and the manifests, the plugin entry of * marketplace.json). */ const jsonVersion: Pick = { read: t => /"version"\s*:\s*"([^"]*)"/.exec(t)?.[1] ?? null, write: (t, v) => t.replace(/("version"\s*:\s*)"[^"]*"/, `$1"${v}"`), }; /** The authority first, then every copy. */ export const COPIES: Copy[] = [ { file: 'package.json', ...jsonVersion }, { file: '.claude-plugin/plugin.json', ...jsonVersion }, { file: '.claude-plugin/marketplace.json', ...jsonVersion }, { file: '.codex-plugin/plugin.json', ...jsonVersion }, { file: 'gemini-extension.json', ...jsonVersion }, { file: 'apps/desktop/package.json', ...jsonVersion }, { file: `${TAURI}/tauri.conf.json`, ...jsonVersion }, { file: `${TAURI}/Cargo.toml`, // the [package] table's version (the first `version =` line of the file, before any dependency table) read: t => /^\[package\][^[]*?^version\s*=\s*"([^"]+)"/ms.exec(t)?.[1] ?? null, write: (t, v) => t.replace(/^(\[package\][^[]*?^version\s*=\s*)"[^"]+"/ms, `$1"${v}"`), }, { file: `${TAURI}/Cargo.lock`, read: t => /^name = "storebound-desktop"\nversion = "([^"]+)"/m.exec(t)?.[1] ?? null, write: (t, v) => t.replace(/^(name = "storebound-desktop"\nversion = )"[^"]+"/m, `$1"${v}"`), }, ]; export interface Drift { file: string; found: string | null } export function versions(root = ROOT): { version: string | null; drift: Drift[] } { const [authority, ...copies] = COPIES.map(c => ({ file: c.file, found: c.read(readFileSync(join(root, c.file), 'utf8')) })); const version = authority!.found; return { version, drift: copies.filter(c => c.found !== version) }; } export function setVersion(v: string, root = ROOT): void { if (!SEMVER.test(v)) throw new Error(`not a version: ${v}`); for (const c of COPIES) { const p = join(root, c.file); const before = readFileSync(p, 'utf8'); const after = c.write(before, v); if (c.read(after) !== v) throw new Error(`could not set the version in ${c.file}`); if (after !== before) writeFileSync(p, after); } } if (import.meta.main) { const argv = process.argv.slice(2); const i = argv.indexOf('--set'); if (i >= 0) { setVersion(argv[i + 1] ?? ''); console.log(`version ${argv[i + 1]} written to ${COPIES.map(c => c.file).join(', ')}\nnext: a CHANGELOG section for it; the npm package and the desktop app are released together (docs/desktop-release.md)`); process.exit(0); } const { version, drift } = versions(); if (argv.includes('--print')) { console.log(version ?? ''); process.exit(version ? 0 : 1); } if (!version || !SEMVER.test(version)) { console.error('version: package.json has no valid "version"'); process.exit(1); } for (const d of drift) console.error(`version drift: ${d.file} has ${d.found ?? 'no version'}, package.json version is ${version}`); if (drift.length) { console.error('fix: bun tools/desktop-version.ts --set ' + version); process.exit(1); } console.log(`version ${version}: the CLI, the plugin manifests and the desktop app (${COPIES.length - 1} copies) match`); }