Skip to content

Commit 59d3e80

Browse files
cameroncookeclaude
andcommitted
fix(cli): Allow hyphens in semver identifiers and check brew exit code
Fix parseVersion regex to accept hyphens in prerelease and build-metadata identifiers as required by SemVer 2.0.0, so versions like 1.0.0-alpha-1 or 1.0.0-rc-1+build-hash no longer fail to parse. Check the exit code of `brew info` before attempting to parse its stdout. Homebrew exits 0 for missing formulas (a quirk the existing empty-formulae path handles), but other failure modes like a broken brew install or permission errors exit non-zero with empty stdout — those now surface a specific exit-code error instead of a misleading "invalid JSON output". Reported in PR review by Cursor Bugbot and Sentry. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d6bc364 commit 59d3e80

File tree

2 files changed

+38
-1
lines changed

2 files changed

+38
-1
lines changed

src/cli/commands/__tests__/upgrade.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,24 @@ describe('upgrade command', () => {
202202
});
203203
});
204204

205+
it('parses prerelease with hyphenated identifier', () => {
206+
expect(parseVersion('1.0.0-alpha-1')).toEqual({
207+
major: 1,
208+
minor: 0,
209+
patch: 0,
210+
prerelease: 'alpha-1',
211+
});
212+
});
213+
214+
it('parses hyphenated prerelease with hyphenated build metadata', () => {
215+
expect(parseVersion('1.0.0-rc-1+build-hash')).toEqual({
216+
major: 1,
217+
minor: 0,
218+
patch: 0,
219+
prerelease: 'rc-1',
220+
});
221+
});
222+
205223
it.each([['not-a-version'], ['1.2'], [''], ['1.2.3.4'], ['abc.def.ghi']])(
206224
'returns undefined for malformed input %j',
207225
(input) => {
@@ -941,6 +959,19 @@ describe('upgrade command', () => {
941959
expect(collectStderr(stderrSpy)).toContain('invalid JSON');
942960
});
943961

962+
it('homebrew: exits 1 when brew info exits non-zero', async () => {
963+
const deps = channelDeps(homebrewMethod(), {
964+
stdout: '',
965+
stderr: 'Error: Permission denied',
966+
exitCode: 1,
967+
});
968+
969+
const code = await runUpgradeCommand({ check: false, yes: false }, deps);
970+
expect(code).toBe(1);
971+
expect(collectStderr(stderrSpy)).toContain('Homebrew');
972+
expect(collectStderr(stderrSpy)).toContain('exited with code 1');
973+
});
974+
944975
it('npm-global: exits 1 when npm view exits non-zero', async () => {
945976
const deps = channelDeps(npmGlobalMethod(), {
946977
stdout: '',

src/cli/commands/upgrade.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export interface UpgradeOptions {
6969

7070
export function parseVersion(raw: string): ParsedVersion | undefined {
7171
const stripped = raw.startsWith('v') ? raw.slice(1) : raw;
72-
const match = stripped.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.]+))?(?:\+[a-zA-Z0-9.]+)?$/);
72+
const match = stripped.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+[a-zA-Z0-9.-]+)?$/);
7373
if (!match) return undefined;
7474
return {
7575
major: Number(match[1]),
@@ -258,6 +258,12 @@ async function fetchLatestVersionFromHomebrew(
258258
throw new Error(`couldn't determine latest version from Homebrew: ${reason}`);
259259
}
260260

261+
if (result.exitCode !== 0) {
262+
throw new Error(
263+
`couldn't determine latest version from Homebrew: command exited with code ${result.exitCode}`,
264+
);
265+
}
266+
261267
let data: unknown;
262268
try {
263269
data = JSON.parse(result.stdout);

0 commit comments

Comments
 (0)