Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
883bd36
feat(cli): add update command
CorieW Aug 8, 2025
ce2ba7b
feat(cli/update): force flag
CorieW Aug 8, 2025
c56eaa0
feat(cli/update): check flag
CorieW Aug 8, 2025
bfdbd57
feat(cli/update): list flag
CorieW Aug 8, 2025
6edd65f
fix(cli/update): don't show RC versions in list
CorieW Aug 10, 2025
bb6aad5
feat(cli/update): add node support, not just binary
CorieW Aug 10, 2025
99dc301
feat(cli/update): installing a specific version
CorieW Aug 10, 2025
25d5a5a
feat: add notifications, fixed npm update, and improved code
CorieW Aug 11, 2025
dfa1ce5
chore: improve the code and receieve list of version from different p…
CorieW Aug 11, 2025
5aa35d9
Merge branch 'main' of https://github.com/firebase/genkit into @inver…
CorieW Aug 11, 2025
f861d3a
chore(cli/update): code adjustments
CorieW Aug 11, 2025
a62c010
chore(cli): format
CorieW Aug 11, 2025
76cf3cc
fix(cli/update): version problem
CorieW Aug 11, 2025
e314b8d
fix(cli/update): issue that potentially could block event loop
CorieW Aug 11, 2025
23f97bb
chore(cli/update): code improvements
CorieW Aug 11, 2025
755d49a
fix(cli/update): fix updating issues and minor tweaks to console prin…
CorieW Aug 11, 2025
71347b1
fix: remove list flag as not necessary
CorieW Aug 12, 2025
268fc19
fix(cli/update): small logging issue
CorieW Aug 12, 2025
e3ec0e5
fix(cli): runningFromNpmLocally func
CorieW Aug 12, 2025
b5b470f
feat(cli/update): handle various package managers
CorieW Aug 12, 2025
eb311a0
feat(cli/update): instead of detecting package manager and global/loc…
CorieW Aug 12, 2025
d277237
fix: remove --force for --reinstall, as clearer
CorieW Aug 13, 2025
38b9765
fix: only inquire package manager when non-binary
CorieW Aug 13, 2025
9e65048
feat(cli/update): improve version not found error message, and throw …
CorieW Aug 14, 2025
afa24ad
chore(cli/update): improve some logs
CorieW Aug 14, 2025
ff4b2c8
feat(cli): add --no-update-notification flag
CorieW Aug 14, 2025
9a285ca
feat(cli/update): clear message for not finding update version for bi…
CorieW Aug 14, 2025
f4ec5b5
feat(cli/update): add some tests
CorieW Aug 15, 2025
7f95f8f
feat(cli/update): added more tests and improved robustness to error
CorieW Aug 15, 2025
40a593a
Merge branch '@invertase/cli-add-update-cmd' of https://github.com/fi…
CorieW Aug 15, 2025
46c3e78
feat(cli/update): when fail to update via package manager, suggest al…
CorieW Aug 16, 2025
859656b
feat(cli/update): add update alternative for failed binary update
CorieW Aug 18, 2025
d987d4d
chore: format
CorieW Aug 18, 2025
26b80ac
fix(cli/update): modify update notification silencing method
CorieW Aug 18, 2025
8ea0df1
fix(cli/update): tests and feedback
CorieW Aug 18, 2025
8496f24
chore(cli/update): adjust for feedback and format
CorieW Aug 19, 2025
d1eca4b
chore(cli/update): adjust for feedback
CorieW Aug 19, 2025
c5dd947
fix(cli/update): try and fix inquirer testing issue
CorieW Aug 19, 2025
d348927
chore: format
CorieW Aug 19, 2025
a3ca3d0
fix(cli/update): failing tests
CorieW Aug 19, 2025
adfa234
feat(cli): add update check
CorieW Aug 21, 2025
efee7c0
chore(cli): address feedback and format
CorieW Aug 21, 2025
c4c7de2
chore(cli): addressed feedback
CorieW Aug 26, 2025
310c411
chore: format
CorieW Aug 26, 2025
7626166
chore(cli): minor tweak for improved clarity
CorieW Aug 26, 2025
a7dd14a
Merge branch 'main' into @invertase/cli-add-update-checks
CorieW Aug 26, 2025
0fb6d93
chore: format
CorieW Aug 26, 2025
98273e5
Merge branch '@invertase/cli-add-update-checks' of https://github.com…
CorieW Aug 26, 2025
c2dc593
fix(cli): test
CorieW Aug 26, 2025
2297cd4
Merge branch 'main' into @invertase/cli-add-update-checks
pavelgj Aug 29, 2025
ecde314
remove '1.23.x' from go test matrix
pavelgj Aug 29, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.23.x', '1.24.x']
go-version: ['1.24.x']
steps:
- name: Checkout Repo
uses: actions/checkout@main
Expand Down
7 changes: 4 additions & 3 deletions genkit-tools/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"build:watch": "tsc --watch",
"compile:bun": "bun build src/bin/genkit.ts --compile --outfile dist/bin/genkit --minify",
"test": "jest --verbose",
"genversion": "genversion -esf src/utils/version.ts"
"genversion": "genversion -esf --property name,version src/utils/version.ts"
},
"repository": {
"type": "git",
Expand All @@ -40,13 +40,14 @@
"get-port": "5.1.1",
"@inquirer/prompts": "^7.8.0",
"open": "^6.3.0",
"ora": "^5.4.1"
"ora": "^5.4.1",
"semver": "^7.7.2"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/inquirer": "^8.1.3",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.19",
"@types/semver": "^7.7.0",
"bun-types": "^1.2.16",
"genversion": "^3.2.0",
"jest": "^29.7.0",
Expand Down
17 changes: 17 additions & 0 deletions genkit-tools/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { start } from './commands/start';
import { uiStart } from './commands/ui-start';
import { uiStop } from './commands/ui-stop';
import { showUpdateNotification } from './utils/updates';
import { version } from './utils/version';

/**
Expand Down Expand Up @@ -66,6 +67,7 @@ export async function startCLI(): Promise<void> {
.name('genkit')
.description('Genkit CLI')
.version(version)
.option('--no-update-notification', 'Do not show update notification')
.hook('preAction', async (_, actionCommand) => {
await notifyAnalyticsIfFirstRun();

Expand All @@ -87,6 +89,21 @@ export async function startCLI(): Promise<void> {
await record(new RunCommandEvent(commandName));
});

// Check for updates and show notification if available,
// unless --no-update-notification is set
// Run this synchronously to ensure it shows before command execution
const hasNoUpdateNotification = process.argv.includes(
'--no-update-notification'
);
if (!hasNoUpdateNotification) {
try {
await showUpdateNotification();
} catch (e) {
logger.debug('Failed to show update notification', e);
// Silently ignore errors - update notifications shouldn't break the CLI
}
}

// When running as a spawned UI server process, argv[1] will be '__server-harness'
// instead of a normal command. This allows the same binary to serve both CLI and server roles.
if (process.argv[2] === SERVER_HARNESS_COMMAND) {
Expand Down
12 changes: 12 additions & 0 deletions genkit-tools/cli/src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
import * as clc from 'colorette';
import { Command } from 'commander';

export const UPDATE_NOTIFICATIONS_OPT_OUT_CONFIG_TAG =
'updateNotificationsOptOut';

const CONFIG_TAGS: Record<
string,
(value: string) => string | boolean | number
Expand All @@ -38,6 +41,15 @@ const CONFIG_TAGS: Record<
return o;
}
},
[UPDATE_NOTIFICATIONS_OPT_OUT_CONFIG_TAG]: (value) => {
let o: boolean | undefined;
try {
o = JSON.parse(value);
} finally {
if (typeof o !== 'boolean') throw new Error('Expected boolean');
return o;
}
},
};

export const config = new Command('config');
Expand Down
249 changes: 249 additions & 0 deletions genkit-tools/cli/src/utils/updates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/**
* 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 | uninstall=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);
}
}
Loading
Loading