Skip to content

Commit b5b470f

Browse files
committed
feat(cli/update): handle various package managers
1 parent e3ec0e5 commit b5b470f

File tree

3 files changed

+130
-66
lines changed

3 files changed

+130
-66
lines changed

genkit-tools/cli/src/commands/update.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import * as os from 'os';
2323
import * as path from 'path';
2424
import { readConfig, writeConfig } from '../utils/config';
2525
import { detectCLIRuntime } from '../utils/runtime-detector';
26-
import { runningFromNpmLocally } from '../utils/utils';
26+
import { detectGlobalPackageManager, PACKAGE_MANAGERS, runningFromNpmLocally } from '../utils/package-manager-detector';
2727
import { version as currentVersion, name } from '../utils/version';
2828

2929
interface UpdateOptions {
@@ -204,12 +204,12 @@ function getPlatformInfo(): { platform: string; arch: string } {
204204
*/
205205
async function downloadAndInstall(
206206
version: string,
207-
force: boolean
208207
): Promise<void> {
209208
const { platform, arch } = getPlatformInfo();
210209
const execPath = process.execPath;
211210
const backupPath = `${execPath}.backup`;
212211
const runtime = detectCLIRuntime();
212+
const pm = await detectGlobalPackageManager() || PACKAGE_MANAGERS.npm;
213213

214214
// If not running from a binary, we should install using npm
215215
if (!runtime.isCompiledBinary) {
@@ -218,7 +218,7 @@ async function downloadAndInstall(
218218
if (await runningFromNpmLocally()) {
219219
command = `npm install ${name}@${version}`;
220220
} else {
221-
command = `npm install -g ${name}@${version}`;
221+
command = `${pm?.globalInstallCommand} ${name}@${version}`;
222222
}
223223

224224
logger.info('Running using npm, downloading using npm...');
@@ -320,9 +320,6 @@ export async function showUpdateNotification(): Promise<void> {
320320
const result = await checkForUpdates();
321321

322322
if (result.hasUpdate) {
323-
const runtime = detectCLIRuntime();
324-
const isBinary = runtime.isCompiledBinary;
325-
326323
// Merge all notification lines into a single message for concise output
327324
console.log(
328325
`\n${clc.yellow('📦 Update available:')} ${clc.bold(result.currentVersion)}${clc.bold(clc.green(result.latestVersion))}\n` +
@@ -421,7 +418,7 @@ export const update = new Command('update')
421418
}
422419
}
423420

424-
await downloadAndInstall(version, options.force || false);
421+
await downloadAndInstall(version);
425422
} catch (error: any) {
426423
logger.error(`${clc.red('Update failed:')} ${error.message}`);
427424
process.exit(1);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { exec } from 'child_process';
18+
import * as path from 'path';
19+
20+
interface PackageManager {
21+
type: string;
22+
localInstallCommand: string;
23+
globalInstallCommand: string;
24+
globalRootCommand: string;
25+
}
26+
27+
export const PACKAGE_MANAGERS: Record<string, PackageManager> = {
28+
npm: {
29+
type: 'npm',
30+
localInstallCommand: 'npm install',
31+
globalInstallCommand: 'npm install -g',
32+
globalRootCommand: 'npm root -g',
33+
},
34+
pnpm: {
35+
type: 'pnpm',
36+
localInstallCommand: 'pnpm install',
37+
globalInstallCommand: 'pnpm install -g',
38+
globalRootCommand: 'pnpm root -g',
39+
},
40+
yarn: {
41+
type: 'yarn',
42+
localInstallCommand: 'yarn install',
43+
globalInstallCommand: 'yarn install -g',
44+
globalRootCommand: 'yarn global bin',
45+
},
46+
};
47+
48+
/**
49+
* Caches the package manager type to avoid multiple calls to the same command.
50+
*/
51+
let detectedPackageManager: PackageManager | undefined;
52+
53+
/**
54+
* Detects which global package manager (npm, pnpm, yarn) was used to install the CLI,
55+
* based on the location of the entry script.
56+
*
57+
* @returns The detected global package manager, or undefined if not found.
58+
*/
59+
export async function detectGlobalPackageManager(): Promise<PackageManager | undefined> {
60+
// Return cached value if available
61+
if (detectedPackageManager) {
62+
return detectedPackageManager;
63+
}
64+
65+
const entryScript = process.argv[1] ? path.resolve(process.argv[1]) : '';
66+
67+
for (const key of Object.keys(PACKAGE_MANAGERS)) {
68+
const pm = PACKAGE_MANAGERS[key];
69+
const globalPath = await getGlobalPath(pm.globalRootCommand);
70+
if (globalPath && entryScript.startsWith(globalPath)) {
71+
detectedPackageManager = pm;
72+
break;
73+
}
74+
}
75+
76+
return detectedPackageManager;
77+
78+
function getGlobalPath(command: string): Promise<string | undefined> {
79+
return new Promise((resolve) => {
80+
exec(command, (error, stdout) => {
81+
if (error) {
82+
resolve(undefined);
83+
} else {
84+
resolve(stdout.toString().trim());
85+
}
86+
});
87+
});
88+
}
89+
}
90+
91+
/**
92+
* Determines if the CLI is running from a local npm install (e.g., npx, local node_modules/.bin)
93+
* vs a global npm install. This checks the location of the entry script (process.argv[1])
94+
* to see if it resides within the global node_modules directory.
95+
*
96+
* Returns:
97+
* - true: if running from a local install (including npx, local node_modules, or dev)
98+
* - false: if running from a global npm install
99+
*
100+
* Note: This is a heuristic and may not be 100% accurate in all edge cases.
101+
*/
102+
export async function runningFromNpmLocally(): Promise<boolean> {
103+
const pm = await detectGlobalPackageManager();
104+
const globalRootCmd = pm ? pm.globalRootCommand : PACKAGE_MANAGERS.npm.globalRootCommand;
105+
return new Promise((resolve) => {
106+
exec(globalRootCmd, (error, stdout) => {
107+
if (error) {
108+
// If we can't determine, assume local for safety
109+
resolve(true);
110+
return;
111+
}
112+
const globalNodeModules = stdout.toString().trim();
113+
114+
// process.argv[1] is the entry script (e.g., .../node_modules/.bin/genkit or .../bin/genkit.js)
115+
const entryScript = process.argv[1] ? path.resolve(process.argv[1]) : '';
116+
117+
// If the entry script is inside the global node_modules directory, it's global
118+
if (entryScript && entryScript.startsWith(globalNodeModules)) {
119+
resolve(false); // running globally, not locally
120+
} else {
121+
// Otherwise, assume it's local (e.g., npx, local install, or dev)
122+
resolve(true);
123+
}
124+
});
125+
});
126+
}

genkit-tools/cli/src/utils/utils.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)