Skip to content
Merged
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
44 changes: 35 additions & 9 deletions apps/cli/src/self-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,42 @@ export function detectPackageManager(): 'bun' | 'npm' {

/**
* Detect whether agentv was invoked from a local project install.
* A path containing a `node_modules` segment indicates a local dependency;
* anything else (system binary, `.bun/bin`, `.nvm/.../bin`) is treated as
* global. Matches both POSIX and Windows path separators so a directory
* that merely embeds the substring (e.g., `/opt/my_node_modules_tool/`)
* isn't misclassified.
* npm global installs can also live under a `node_modules` segment, so
* the path alone is not enough. We treat `npx` cache paths as local and
* otherwise require the current working directory to be inside the package
* root before classifying it as local.
*/
export function detectInstallScopeFromPath(scriptPath: string): 'local' | 'global' {
const hasSegment =
scriptPath.includes('/node_modules/') || scriptPath.includes('\\node_modules\\');
return hasSegment ? 'local' : 'global';
export function detectInstallScopeFromPath(
scriptPath: string,
cwd = process.cwd(),
): 'local' | 'global' {
const normalizedScriptPath = scriptPath.replace(/\\/g, '/');
const normalizedCwd = cwd.replace(/\\/g, '/');

if (!normalizedScriptPath.includes('/node_modules/')) {
return 'global';
}

if (normalizedScriptPath.includes('/.npm/_npx/') || normalizedScriptPath.includes('/npm-cache/_npx/')) {
return 'local';
}

const packageRoot = normalizedScriptPath.split('/node_modules/')[0];
if (!packageRoot) {
return 'global';
}

const scriptPathComparable = process.platform === 'win32'
? normalizedScriptPath.toLowerCase()
: normalizedScriptPath;
const cwdComparable = process.platform === 'win32' ? normalizedCwd.toLowerCase() : normalizedCwd;
const packageRootComparable = process.platform === 'win32' ? packageRoot.toLowerCase() : packageRoot;

const projectOwnsPackage =
cwdComparable === packageRootComparable ||
cwdComparable.startsWith(`${packageRootComparable}/`);

return projectOwnsPackage ? 'local' : 'global';
}

export function detectInstallScope(): 'local' | 'global' {
Expand Down
43 changes: 39 additions & 4 deletions apps/cli/test/self-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ describe('detectPackageManagerFromPath', () => {

describe('detectInstallScopeFromPath', () => {
test('detects local for project node_modules path', () => {
expect(detectInstallScopeFromPath('/home/user/proj/node_modules/.bin/agentv')).toBe('local');
expect(
detectInstallScopeFromPath(
'/home/user/proj/node_modules/.bin/agentv',
'/home/user/proj',
),
).toBe('local');
});

test('detects local for nested npx cache path', () => {
Expand All @@ -51,9 +56,39 @@ describe('detectInstallScopeFromPath', () => {
});

test('detects local for Windows node_modules path', () => {
expect(detectInstallScopeFromPath('C:\\Users\\dev\\proj\\node_modules\\.bin\\agentv.cmd')).toBe(
'local',
);
expect(
detectInstallScopeFromPath(
'C:\\Users\\dev\\proj\\node_modules\\.bin\\agentv.cmd',
'C:\\Users\\dev\\proj',
),
).toBe('local');
});

test('detects global for Windows npm global install path', () => {
expect(
detectInstallScopeFromPath(
'C:\\Users\\dev\\AppData\\Roaming\\npm\\node_modules\\agentv\\dist\\cli.js',
'C:\\Users\\dev\\work\\repo',
),
).toBe('global');
});

test('detects global for Windows npm global install path from home directory', () => {
expect(
detectInstallScopeFromPath(
'C:\\Users\\dev\\AppData\\Roaming\\npm\\node_modules\\agentv\\dist\\cli.js',
'C:\\Users\\dev',
),
).toBe('global');
});

test('detects local when cwd is inside the project using the dependency', () => {
expect(
detectInstallScopeFromPath(
'C:\\Users\\dev\\repo\\node_modules\\agentv\\dist\\cli.js',
'C:\\Users\\dev\\repo\\packages\\cli',
),
).toBe('local');
});

test('treats unrelated directory containing node_modules substring as global', () => {
Expand Down
Loading