Skip to content
Open
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/fix-version-ux.md
Original file line number Diff line number Diff line change
@@ -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
13 changes: 7 additions & 6 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 16 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,26 +50,28 @@ function handleCommandError(fn: () => Promise<number>) {
};
}

// 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
.name('worktree')
.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',
`
Expand Down
8 changes: 8 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
18 changes: 7 additions & 11 deletions src/utils/update-checker.ts → src/utils/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -10,12 +11,7 @@ const UpdateCheckCacheSchema = z.object({
latestVersion: z.string().optional(),
});

type UpdateCheckCache = z.infer<typeof UpdateCheckCacheSchema>;

interface PackageJson {
name: string;
version: string;
}
export type UpdateCheckCache = z.infer<typeof UpdateCheckCacheSchema>;

const CACHE_FILENAME = 'update-check.json';
const PRERELEASE_PATTERN = /-(?:alpha|beta|rc|pre|canary|next|dev)/;
Expand Down Expand Up @@ -57,7 +53,7 @@ async function readCache(): Promise<UpdateCheckCache | null> {
return data;
}

async function writeCache(cache: UpdateCheckCache): Promise<void> {
export async function writeCache(cache: UpdateCheckCache): Promise<void> {
const { error } = await tryCatch(async () => {
await ensureCacheDir();
await writeFile(cacheFile, JSON.stringify(cache, null, 2), 'utf-8');
Expand Down Expand Up @@ -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;
}
Expand All @@ -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`);
}
2 changes: 1 addition & 1 deletion tests/update-checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions tests/update-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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);
Expand Down