diff --git a/.changeset/fix-version-ux.md b/.changeset/fix-version-ux.md new file mode 100644 index 0000000..57bd952 --- /dev/null +++ b/.changeset/fix-version-ux.md @@ -0,0 +1,5 @@ +--- +"@fnebenfuehr/worktree-cli": patch +--- + +Fix version command UX: remove help text display, suggest `worktree update` instead of `npm update -g`, consolidate update checking code, and make fresh version checks update the cache diff --git a/src/commands/update.ts b/src/commands/update.ts index c2e2d4b..e26e809 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,12 +1,8 @@ import { spawn } from 'node:child_process'; +import type { PackageJson } from '@/lib/types'; import { WorktreeError } from '@/utils/errors'; import { intro, log, outro, spinner } from '@/utils/prompts'; -import { fetchLatestVersion, isNewerVersion } from '@/utils/update-checker'; - -interface PackageJson { - name: string; - version: string; -} +import { fetchLatestVersion, isNewerVersion, writeCache } from '@/utils/update'; /** * Runs npm update -g for the package and returns the new version @@ -145,6 +141,11 @@ export async function getVersionInfo( const latest = await fetchLatestVersion(pkg.name); const updateAvailable = latest ? isNewerVersion(pkg.version, latest) : false; + await writeCache({ + lastCheck: Date.now(), + latestVersion: latest || undefined, + }); + return { current: pkg.version, latest, diff --git a/src/index.ts b/src/index.ts index 5a5e780..9cf9293 100755 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ import { switchCommand } from '@/commands/switch'; import { getVersionInfo, updateCommand } from '@/commands/update'; import { UserCancelledError, WorktreeError } from '@/utils/errors'; import { log } from '@/utils/prompts'; -import { checkForUpdates } from '@/utils/update-checker'; +import { checkForUpdates } from '@/utils/update'; import packageJson from '../package.json'; const VERSION = packageJson.version; @@ -50,6 +50,21 @@ function handleCommandError(fn: () => Promise) { }; } +// Handle version flag before Commander parses +if (process.argv.includes('--version') || process.argv.includes('-v')) { + console.log(VERSION); + try { + const info = await getVersionInfo(packageJson); + if (info.updateAvailable && info.latest) { + console.log(`\nUpdate available: ${info.current} → ${info.latest}`); + console.log(`Run: worktree update`); + } + } catch { + // Silently ignore update check errors + } + process.exit(0); +} + const program = new Command(); program @@ -57,19 +72,6 @@ program .description('A modern CLI tool for managing git worktrees with ease') .option('-v, --version', 'Show version') .option('--verbose', 'Enable verbose output') - .on('option:version', async () => { - console.log(VERSION); - try { - const info = await getVersionInfo(packageJson); - if (info.updateAvailable && info.latest) { - console.log(`\nUpdate available: ${info.current} → ${info.latest}`); - console.log(`Run: npm update -g ${packageJson.name}`); - } - } catch { - // Silently ignore update check errors - } - process.exit(0); - }) .addHelpText( 'after', ` diff --git a/src/lib/types.ts b/src/lib/types.ts index 5a3cace..65d1b8f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2,6 +2,14 @@ * Shared types for the worktree CLI library */ +/** + * Package.json structure for update checking + */ +export interface PackageJson { + name: string; + version: string; +} + /** * Information about a git worktree */ diff --git a/src/utils/update-checker.ts b/src/utils/update.ts similarity index 89% rename from src/utils/update-checker.ts rename to src/utils/update.ts index 5c61a9a..f2c9b6f 100644 --- a/src/utils/update-checker.ts +++ b/src/utils/update.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { homedir, tmpdir } from 'node:os'; import { join } from 'node:path'; import { z } from 'zod'; +import type { PackageJson } from '@/lib/types'; import { log } from '@/utils/prompts'; import { tryCatch } from '@/utils/try-catch'; @@ -10,12 +11,7 @@ const UpdateCheckCacheSchema = z.object({ latestVersion: z.string().optional(), }); -type UpdateCheckCache = z.infer; - -interface PackageJson { - name: string; - version: string; -} +export type UpdateCheckCache = z.infer; const CACHE_FILENAME = 'update-check.json'; const PRERELEASE_PATTERN = /-(?:alpha|beta|rc|pre|canary|next|dev)/; @@ -57,7 +53,7 @@ async function readCache(): Promise { return data; } -async function writeCache(cache: UpdateCheckCache): Promise { +export async function writeCache(cache: UpdateCheckCache): Promise { const { error } = await tryCatch(async () => { await ensureCacheDir(); await writeFile(cacheFile, JSON.stringify(cache, null, 2), 'utf-8'); @@ -140,7 +136,7 @@ export async function checkForUpdates(pkg: PackageJson, checkIntervalMs: number) if (cache && now - cache.lastCheck < checkIntervalMs) { if (cache.latestVersion && isNewerVersion(pkg.version, cache.latestVersion)) { - displayUpdateMessage(pkg.version, cache.latestVersion, pkg.name); + displayUpdateMessage(pkg.version, cache.latestVersion); } return; } @@ -153,10 +149,10 @@ export async function checkForUpdates(pkg: PackageJson, checkIntervalMs: number) }); if (latestVersion && isNewerVersion(pkg.version, latestVersion)) { - displayUpdateMessage(pkg.version, latestVersion, pkg.name); + displayUpdateMessage(pkg.version, latestVersion); } } -function displayUpdateMessage(current: string, latest: string, packageName: string): void { - log.info(`Update available: ${current} → ${latest}\nRun: npm update -g ${packageName}`); +function displayUpdateMessage(current: string, latest: string): void { + log.info(`Update available: ${current} → ${latest}\nRun: worktree update`); } diff --git a/tests/update-checker.test.ts b/tests/update-checker.test.ts index b0f14f6..83e0ef3 100644 --- a/tests/update-checker.test.ts +++ b/tests/update-checker.test.ts @@ -3,7 +3,7 @@ import { rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import * as prompts from '@/utils/prompts'; -import { checkForUpdates, setCacheDir } from '@/utils/update-checker'; +import { checkForUpdates, setCacheDir } from '@/utils/update'; describe('update-checker', () => { let originalFetch: typeof globalThis.fetch; diff --git a/tests/update-command.test.ts b/tests/update-command.test.ts index 7dbf078..01be499 100644 --- a/tests/update-command.test.ts +++ b/tests/update-command.test.ts @@ -6,7 +6,7 @@ import { spawn } from 'bun'; import * as updateModule from '@/commands/update'; import { getVersionInfo, updateCommand } from '@/commands/update'; import * as prompts from '@/utils/prompts'; -import { setCacheDir } from '@/utils/update-checker'; +import { setCacheDir } from '@/utils/update'; const CLI_PATH = join(import.meta.dir, '../src/index.ts'); @@ -164,17 +164,17 @@ describe('update command', () => { describe('update-checker exports', () => { test('fetchLatestVersion is exported', async () => { - const { fetchLatestVersion } = await import('@/utils/update-checker'); + const { fetchLatestVersion } = await import('@/utils/update'); expect(typeof fetchLatestVersion).toBe('function'); }); test('isNewerVersion is exported', async () => { - const { isNewerVersion } = await import('@/utils/update-checker'); + const { isNewerVersion } = await import('@/utils/update'); expect(typeof isNewerVersion).toBe('function'); }); test('isNewerVersion correctly compares versions', async () => { - const { isNewerVersion } = await import('@/utils/update-checker'); + const { isNewerVersion } = await import('@/utils/update'); expect(isNewerVersion('1.0.0', '2.0.0')).toBe(true); expect(isNewerVersion('1.0.0', '1.1.0')).toBe(true);