diff --git a/packages/cli/package.json b/packages/cli/package.json index ebae22fd..45f79a17 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,6 +38,7 @@ "mixpanel": "^0.18.1", "ora": "^5.4.1", "package-manager-detector": "^1.3.0", + "semver": "^7.7.2", "ts-pattern": "catalog:" }, "peerDependencies": { @@ -45,6 +46,7 @@ }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", + "@types/semver": "^7.7.0", "@types/tmp": "catalog:", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/runtime": "workspace:*", diff --git a/packages/cli/src/actions/info.ts b/packages/cli/src/actions/info.ts index 731bdaf8..bbea51eb 100644 --- a/packages/cli/src/actions/info.ts +++ b/packages/cli/src/actions/info.ts @@ -57,6 +57,9 @@ async function getZenStackPackages(projectPath: string): Promise !!p); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4094fbb5..f4a9c5c9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -4,7 +4,7 @@ import { Command, CommanderError, Option } from 'commander'; import * as actions from './actions'; import { CliError } from './cli-error'; import { telemetry } from './telemetry'; -import { getVersion } from './utils/version-utils'; +import { checkNewVersion, getVersion } from './utils/version-utils'; const generateAction = async (options: Parameters[0]): Promise => { await telemetry.trackCommand('generate', () => actions.generate(options)); @@ -51,10 +51,13 @@ function createProgram() { `schema file (with extension ${schemaExtensions}). Defaults to "zenstack/schema.zmodel" unless specified in package.json.`, ); + const noVersionCheckOption = new Option('--no-version-check', 'do not check for new version'); + program .command('generate') .description('Run code generation plugins.') .addOption(schemaOption) + .addOption(noVersionCheckOption) .addOption(new Option('-o, --output ', 'default output directory for code generation')) .addOption(new Option('--silent', 'suppress all output except errors').default(false)) .action(generateAction); @@ -65,6 +68,7 @@ function createProgram() { migrateCommand .command('dev') .addOption(schemaOption) + .addOption(noVersionCheckOption) .addOption(new Option('-n, --name ', 'migration name')) .addOption(new Option('--create-only', 'only create migration, do not apply')) .addOption(migrationsOption) @@ -76,12 +80,14 @@ function createProgram() { .addOption(schemaOption) .addOption(new Option('--force', 'skip the confirmation prompt')) .addOption(migrationsOption) + .addOption(noVersionCheckOption) .description('Reset your database and apply all migrations, all data will be lost.') .action((options) => migrateAction('reset', options)); migrateCommand .command('deploy') .addOption(schemaOption) + .addOption(noVersionCheckOption) .addOption(migrationsOption) .description('Deploy your pending migrations to your production/staging database.') .action((options) => migrateAction('deploy', options)); @@ -89,6 +95,7 @@ function createProgram() { migrateCommand .command('status') .addOption(schemaOption) + .addOption(noVersionCheckOption) .addOption(migrationsOption) .description('Check the status of your database migrations.') .action((options) => migrateAction('status', options)); @@ -96,6 +103,7 @@ function createProgram() { migrateCommand .command('resolve') .addOption(schemaOption) + .addOption(noVersionCheckOption) .addOption(migrationsOption) .addOption(new Option('--applied ', 'record a specific migration as applied')) .addOption(new Option('--rolled-back ', 'record a specific migration as rolled back')) @@ -108,6 +116,7 @@ function createProgram() { .command('push') .description('Push the state from your schema to your database.') .addOption(schemaOption) + .addOption(noVersionCheckOption) .addOption(new Option('--accept-data-loss', 'ignore data loss warnings')) .addOption(new Option('--force-reset', 'force a reset of the database before push')) .action((options) => dbAction('push', options)); @@ -116,20 +125,29 @@ function createProgram() { .command('info') .description('Get information of installed ZenStack packages.') .argument('[path]', 'project path', '.') + .addOption(noVersionCheckOption) .action(infoAction); program .command('init') .description('Initialize an existing project for ZenStack.') .argument('[path]', 'project path', '.') + .addOption(noVersionCheckOption) .action(initAction); program .command('check') .description('Check a ZModel schema for syntax or semantic errors.') .addOption(schemaOption) + .addOption(noVersionCheckOption) .action(checkAction); + program.hook('preAction', async (_thisCommand, actionCommand) => { + if (actionCommand.getOptionValue('versionCheck') !== false) { + await checkNewVersion(); + } + }); + return program; } diff --git a/packages/cli/src/utils/version-utils.ts b/packages/cli/src/utils/version-utils.ts index 31e7a107..ad428cdf 100644 --- a/packages/cli/src/utils/version-utils.ts +++ b/packages/cli/src/utils/version-utils.ts @@ -1,6 +1,11 @@ +import colors from 'colors'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import semver from 'semver'; + +const CHECK_VERSION_TIMEOUT = 2000; +const VERSION_CHECK_TAG = 'next'; export function getVersion() { try { @@ -11,3 +16,35 @@ export function getVersion() { return undefined; } } + +export async function checkNewVersion() { + const currVersion = getVersion(); + let latestVersion: string; + try { + latestVersion = await getLatestVersion(); + } catch { + // noop + return; + } + + if (latestVersion && currVersion && semver.gt(latestVersion, currVersion)) { + console.log(`A newer version ${colors.cyan(latestVersion)} is available.`); + } +} + +export async function getLatestVersion() { + const fetchResult = await fetch(`https://registry.npmjs.org/@zenstackhq/cli/${VERSION_CHECK_TAG}`, { + headers: { accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*' }, + signal: AbortSignal.timeout(CHECK_VERSION_TIMEOUT), + }); + + if (fetchResult.ok) { + const data: any = await fetchResult.json(); + const latestVersion = data?.version; + if (typeof latestVersion === 'string' && semver.valid(latestVersion)) { + return latestVersion; + } + } + + throw new Error('invalid npm registry response'); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e794f0cc..1a32a902 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,18 +6,33 @@ settings: catalogs: default: + '@types/node': + specifier: ^20.17.24 + version: 20.17.24 '@types/tmp': specifier: ^0.2.6 version: 0.2.6 + kysely: + specifier: ^0.27.6 + version: 0.27.6 langium: specifier: 3.5.0 version: 3.5.0 + langium-cli: + specifier: 3.5.0 + version: 3.5.0 + prisma: + specifier: ^6.10.0 + version: 6.14.0 tmp: specifier: ^0.2.3 version: 0.2.3 ts-pattern: specifier: ^5.7.1 version: 5.7.1 + typescript: + specifier: ^5.8.0 + version: 5.8.3 importers: @@ -92,6 +107,9 @@ importers: prisma: specifier: 'catalog:' version: 6.14.0(typescript@5.8.3) + semver: + specifier: ^7.7.2 + version: 7.7.2 ts-pattern: specifier: 'catalog:' version: 5.7.1 @@ -99,6 +117,9 @@ importers: '@types/better-sqlite3': specifier: ^7.6.13 version: 7.6.13 + '@types/semver': + specifier: ^7.7.0 + version: 7.7.0 '@types/tmp': specifier: 'catalog:' version: 0.2.6 @@ -1110,6 +1131,9 @@ packages: '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/sql.js@1.4.9': resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} @@ -2246,11 +2270,6 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -3123,6 +3142,8 @@ snapshots: '@types/pluralize@0.0.33': {} + '@types/semver@7.7.0': {} + '@types/sql.js@1.4.9': dependencies: '@types/emscripten': 1.40.1 @@ -4340,8 +4361,6 @@ snapshots: safe-buffer@5.2.1: {} - semver@7.6.3: {} - semver@7.7.2: {} set-function-length@1.2.2: @@ -4683,7 +4702,7 @@ snapshots: vscode-languageclient@9.0.1: dependencies: minimatch: 5.1.6 - semver: 7.6.3 + semver: 7.7.2 vscode-languageserver-protocol: 3.17.5 vscode-languageserver-protocol@3.17.5: