Skip to content

Commit 74752a3

Browse files
Copilottheoephraim
andauthored
Add version mismatch warning between standalone binary and local node_modules install (#534)
* Initial plan * feat: add version mismatch detection between standalone binary and local node_modules install Agent-Logs-Url: https://github.com/dmno-dev/varlock/sessions/9c46e44c-4e1e-41a8-bb8b-475279f531dc Co-authored-by: theoephraim <1158956+theoephraim@users.noreply.github.com> * refactor: address code review feedback - clarify loop condition and skip warning for --version/--help Agent-Logs-Url: https://github.com/dmno-dev/varlock/sessions/9c46e44c-4e1e-41a8-bb8b-475279f531dc Co-authored-by: theoephraim <1158956+theoephraim@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: theoephraim <1158956+theoephraim@users.noreply.github.com>
1 parent 01c9a6a commit 74752a3

4 files changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"varlock": patch
3+
---
4+
5+
Add version mismatch detection between standalone binary and local node_modules install
6+
7+
When running the standalone binary (installed via homebrew/curl), varlock now checks if a different version is installed in the project's node_modules. If a version mismatch is detected, a warning is displayed suggesting users update the binary or use the locally installed version instead. This helps prevent confusing errors caused by running mismatched versions.

packages/varlock/src/cli/cli-executable.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { fmt } from './helpers/pretty-format';
77
import { trackCommand, trackInstall } from './helpers/telemetry';
88
import { InvalidEnvError } from './helpers/error-checks';
99
import { checkBunVersion } from '../lib/check-bun-version';
10+
import { checkLocalVersionMismatch } from '../lib/check-local-version';
1011
import packageJson from '../../package.json';
1112

1213
// we'll import just the spec from each, so the implementations can be lazy loaded
@@ -85,6 +86,15 @@ subCommands.set('typegen', buildLazyCommand(typegenCommandSpec, async () => awai
8586
await trackCommand('version');
8687
}
8788

89+
// warn if standalone binary version differs from local node_modules install
90+
// skip for --version/--help since those are quick informational commands
91+
if (__VARLOCK_SEA_BUILD__ && args[0] !== '--version' && args[0] !== '--help') {
92+
const versionMismatchWarning = checkLocalVersionMismatch(packageJson.version);
93+
if (versionMismatchWarning) {
94+
console.warn(`\n⚠️ ${versionMismatchWarning}\n`);
95+
}
96+
}
97+
8898
await cli(args, {
8999
// main command - triggered if you just run `varlock` with no args
90100
run: () => {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import path from 'node:path';
2+
import fs from 'node:fs';
3+
4+
/**
5+
* When running as a standalone binary (SEA build), checks if varlock is also
6+
* installed in a local node_modules directory. If found and the versions differ,
7+
* returns a warning message to alert the user about the mismatch.
8+
*
9+
* This helps prevent confusing errors when users have both a standalone binary
10+
* (e.g. installed via homebrew/curl) and a project-level npm install with
11+
* different versions.
12+
*/
13+
export function checkLocalVersionMismatch(currentVersion: string): string | undefined {
14+
// Walk up from cwd looking for node_modules/varlock/package.json
15+
let currentDir = process.cwd();
16+
while (true) {
17+
const localPkgJsonPath = path.join(currentDir, 'node_modules', 'varlock', 'package.json');
18+
if (fs.existsSync(localPkgJsonPath)) {
19+
try {
20+
const localPkgJson = JSON.parse(fs.readFileSync(localPkgJsonPath, 'utf-8'));
21+
const localVersion = localPkgJson.version;
22+
if (localVersion && localVersion !== currentVersion) {
23+
return 'Varlock version mismatch detected!\n'
24+
+ ` Standalone binary version: ${currentVersion}\n`
25+
+ ` Local installed version: ${localVersion}\n`
26+
+ 'You are running the standalone binary, but a different version of varlock is installed in this project\'s node_modules.\n'
27+
+ 'This can cause unexpected errors. Please update your standalone binary or use the locally installed version instead\n'
28+
+ '(e.g. via npx varlock, pnpm exec varlock, or bunx varlock).';
29+
}
30+
} catch {
31+
// If we can't read/parse the package.json, just skip the check
32+
}
33+
// Found node_modules/varlock - stop walking regardless of outcome
34+
break;
35+
}
36+
const parentDir = path.dirname(currentDir);
37+
if (parentDir === currentDir) break;
38+
currentDir = parentDir;
39+
}
40+
return undefined;
41+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
describe, it, expect, afterEach,
3+
} from 'vitest';
4+
import fs from 'node:fs';
5+
import path from 'node:path';
6+
import os from 'node:os';
7+
import { checkLocalVersionMismatch } from '../check-local-version';
8+
9+
describe('checkLocalVersionMismatch', () => {
10+
const tmpDirs: Array<string> = [];
11+
12+
function createTempProject(localVarlockVersion?: string): string {
13+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'varlock-test-'));
14+
tmpDirs.push(tmpDir);
15+
16+
if (localVarlockVersion) {
17+
const varlockDir = path.join(tmpDir, 'node_modules', 'varlock');
18+
fs.mkdirSync(varlockDir, { recursive: true });
19+
fs.writeFileSync(
20+
path.join(varlockDir, 'package.json'),
21+
JSON.stringify({ name: 'varlock', version: localVarlockVersion }),
22+
);
23+
}
24+
25+
return tmpDir;
26+
}
27+
28+
function withCwd(dir: string, fn: () => void) {
29+
const originalCwd = process.cwd();
30+
process.chdir(dir);
31+
try {
32+
fn();
33+
} finally {
34+
process.chdir(originalCwd);
35+
}
36+
}
37+
38+
afterEach(() => {
39+
for (const dir of tmpDirs) {
40+
fs.rmSync(dir, { recursive: true, force: true });
41+
}
42+
tmpDirs.length = 0;
43+
});
44+
45+
it('should return undefined when no node_modules/varlock exists', () => {
46+
const tmpDir = createTempProject();
47+
withCwd(tmpDir, () => {
48+
expect(checkLocalVersionMismatch('1.0.0')).toBeUndefined();
49+
});
50+
});
51+
52+
it('should return undefined when versions match', () => {
53+
const tmpDir = createTempProject('1.0.0');
54+
withCwd(tmpDir, () => {
55+
expect(checkLocalVersionMismatch('1.0.0')).toBeUndefined();
56+
});
57+
});
58+
59+
it('should return a warning when versions differ', () => {
60+
const tmpDir = createTempProject('0.6.3');
61+
withCwd(tmpDir, () => {
62+
const result = checkLocalVersionMismatch('0.4.0');
63+
expect(result).toBeDefined();
64+
expect(result).toContain('0.4.0');
65+
expect(result).toContain('0.6.3');
66+
expect(result).toContain('mismatch');
67+
});
68+
});
69+
70+
it('should include the standalone binary version in the warning', () => {
71+
const tmpDir = createTempProject('2.0.0');
72+
withCwd(tmpDir, () => {
73+
const result = checkLocalVersionMismatch('1.0.0');
74+
expect(result).toContain('Standalone binary version: 1.0.0');
75+
});
76+
});
77+
78+
it('should include the local installed version in the warning', () => {
79+
const tmpDir = createTempProject('2.0.0');
80+
withCwd(tmpDir, () => {
81+
const result = checkLocalVersionMismatch('1.0.0');
82+
expect(result).toContain('Local installed version: 2.0.0');
83+
});
84+
});
85+
86+
it('should suggest using locally installed version', () => {
87+
const tmpDir = createTempProject('2.0.0');
88+
withCwd(tmpDir, () => {
89+
const result = checkLocalVersionMismatch('1.0.0');
90+
expect(result).toContain('npx varlock');
91+
});
92+
});
93+
94+
it('should find node_modules in parent directory', () => {
95+
const tmpDir = createTempProject('2.0.0');
96+
const subDir = path.join(tmpDir, 'src', 'app');
97+
fs.mkdirSync(subDir, { recursive: true });
98+
withCwd(subDir, () => {
99+
const result = checkLocalVersionMismatch('1.0.0');
100+
expect(result).toBeDefined();
101+
expect(result).toContain('2.0.0');
102+
});
103+
});
104+
105+
it('should handle malformed package.json gracefully', () => {
106+
const tmpDir = createTempProject();
107+
const varlockDir = path.join(tmpDir, 'node_modules', 'varlock');
108+
fs.mkdirSync(varlockDir, { recursive: true });
109+
fs.writeFileSync(path.join(varlockDir, 'package.json'), 'not valid json');
110+
withCwd(tmpDir, () => {
111+
expect(checkLocalVersionMismatch('1.0.0')).toBeUndefined();
112+
});
113+
});
114+
115+
it('should handle package.json without version field', () => {
116+
const tmpDir = createTempProject();
117+
const varlockDir = path.join(tmpDir, 'node_modules', 'varlock');
118+
fs.mkdirSync(varlockDir, { recursive: true });
119+
fs.writeFileSync(path.join(varlockDir, 'package.json'), JSON.stringify({ name: 'varlock' }));
120+
withCwd(tmpDir, () => {
121+
expect(checkLocalVersionMismatch('1.0.0')).toBeUndefined();
122+
});
123+
});
124+
});

0 commit comments

Comments
 (0)