updates.ts•7.28 kB
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GenkitToolsError } from '@genkit-ai/tools-common/manager';
import { getUserSettings, logger } from '@genkit-ai/tools-common/utils';
import axios, { AxiosInstance } from 'axios';
import * as clc from 'colorette';
import { arch, platform } from 'os';
import semver from 'semver';
import { UPDATE_NOTIFICATIONS_OPT_OUT_CONFIG_TAG } from '../commands/config';
import { detectCLIRuntime } from '../utils/runtime-detector';
import {
version as currentVersion,
name as packageName,
} from '../utils/version';
const GCS_BUCKET_URL = 'https://storage.googleapis.com/genkit-assets-cli';
const CLI_DOCS_URL = 'https://genkit.dev/docs/devtools/';
const AXIOS_INSTANCE: AxiosInstance = axios.create({
timeout: 3000,
});
/**
* Interface for update check result
*/
export interface UpdateCheckResult {
hasUpdate: boolean;
currentVersion: string;
latestVersion: string;
}
/**
* Returns the current CLI version, normalized.
*/
export function getCurrentVersion(): string {
return normalizeVersion(currentVersion);
}
/**
* Normalizes a version string by removing a leading 'v' if present.
* @param version - The version string to normalize
* @returns The normalized version string
*/
function normalizeVersion(version: string): string {
return version.replace(/^v/, '');
}
/**
* Interface for the Google Cloud Storage latest.json response
*/
interface GCSLatestResponse {
channel: string;
latestVersion: string;
lastUpdated: string;
platforms: Record<
string,
{
url: string;
version: string;
versionedUrl: string;
}
>;
}
/**
* Interface for npm registry response
*/
interface NpmRegistryResponse {
'dist-tags': {
latest: string;
[key: string]: string;
};
versions: Record<string, unknown>;
}
/**
* Fetches the latest release data from GCS.
*/
async function getGCSLatestData(): Promise<GCSLatestResponse> {
const response = await AXIOS_INSTANCE.get(`${GCS_BUCKET_URL}/latest.json`);
if (response.status !== 200) {
throw new GenkitToolsError(
`Failed to fetch GCS latest.json: ${response.statusText}`
);
}
return response.data as GCSLatestResponse;
}
/**
* Gets the latest CLI version from npm registry for non-binary installations.
* @param ignoreRC - If true, ignore prerelease versions (default: true)
*/
export async function getLatestVersionFromNpm(
ignoreRC: boolean = true
): Promise<string | null> {
try {
const response = await AXIOS_INSTANCE.get(
`https://registry.npmjs.org/${packageName}`
);
if (response.status !== 200) {
throw new GenkitToolsError(
`Failed to fetch npm versions: ${response.statusText}`
);
}
const data: NpmRegistryResponse = response.data;
// Prefer dist-tags.latest if valid and not a prerelease (if ignoreRC)
const latest = data['dist-tags']?.latest;
if (latest) {
const clean = normalizeVersion(latest);
if (semver.valid(clean) && (!ignoreRC || !semver.prerelease(clean))) {
return clean;
}
}
// Fallback: find the highest valid version in versions
const versions = Object.keys(data.versions)
.map(normalizeVersion)
.filter((v) => semver.valid(v) && (!ignoreRC || !semver.prerelease(v)));
if (versions.length === 0) {
return null;
}
// Sort by semver descending (newest first)
versions.sort(semver.rcompare);
return versions[0];
} catch (error: unknown) {
if (error instanceof GenkitToolsError) {
throw error;
}
throw new GenkitToolsError(
`Failed to fetch npm versions: ${(error as Error)?.message ?? String(error)}`
);
}
}
/**
* Checks if update notifications are disabled via environment variable or user config.
*/
function isUpdateNotificationsDisabled(): boolean {
if (process.env.GENKIT_CLI_DISABLE_UPDATE_NOTIFICATIONS === 'true') {
return true;
}
const userSettings = getUserSettings();
return Boolean(userSettings[UPDATE_NOTIFICATIONS_OPT_OUT_CONFIG_TAG]);
}
/**
* Gets the latest version and update message for compiled binary installations.
*/
async function getBinaryUpdateInfo(): Promise<string | null> {
const gcsLatestData = await getGCSLatestData();
const machine = `${platform}-${arch}`;
const platformData = gcsLatestData.platforms[machine];
if (!platformData) {
logger.debug(`No update information for platform: ${machine}`);
return null;
}
const latestVersion = normalizeVersion(gcsLatestData.latestVersion);
return latestVersion;
}
/**
* Gets the latest version and update message for npm installations.
*/
async function getNpmUpdateInfo(): Promise<string | null> {
const latestVersion = await getLatestVersionFromNpm();
if (!latestVersion) {
logger.debug('No available versions found from npm.');
return null;
}
return latestVersion;
}
/**
* Shows an update notification if a new version is available.
* This function is designed to be called from the CLI entry point.
* It can be disabled by the user's configuration or environment variable.
*/
export async function showUpdateNotification(): Promise<void> {
try {
if (isUpdateNotificationsDisabled()) {
return;
}
const { isCompiledBinary } = detectCLIRuntime();
const updateInfo = isCompiledBinary
? await getBinaryUpdateInfo()
: await getNpmUpdateInfo();
if (!updateInfo) {
return;
}
const latestVersion = updateInfo;
const current = normalizeVersion(currentVersion);
if (!semver.valid(latestVersion) || !semver.valid(current)) {
logger.debug(
`Invalid semver: current=${current}, latest=${latestVersion}`
);
return;
}
if (!semver.gt(latestVersion, current)) {
return;
}
// Determine install method and update command for message
const installMethod = isCompiledBinary
? 'installer script'
: 'your package manager';
const updateCommand = isCompiledBinary
? 'curl -sL cli.genkit.dev | upgrade=true bash'
: 'npm install -g genkit-cli';
const updateNotificationMessage =
`Update available ${clc.gray(`v${current}`)} → ${clc.green(`v${latestVersion}`)}\n` +
`To update to the latest version using ${installMethod}, run\n${clc.cyan(updateCommand)}\n` +
`For other CLI management options, visit ${CLI_DOCS_URL}\n` +
`${clc.dim('Run')} ${clc.bold('genkit config set updateNotificationsOptOut true')} ${clc.dim('to disable these notifications')}\n`;
logger.info(`\n${updateNotificationMessage}`);
} catch (e) {
// Silently fail - update notifications shouldn't break the CLI
logger.debug('Failed to show update notification', e);
}
}