diff --git a/packages/schema/package.json b/packages/schema/package.json index 5265796b9..75b71d619 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -108,6 +108,7 @@ "semver": "^7.5.2", "sleep-promise": "^9.1.0", "strip-color": "^0.1.0", + "terminal-link": "^2.0.0", "tiny-invariant": "^1.3.1", "ts-morph": "^16.0.0", "ts-pattern": "^4.3.0", diff --git a/packages/schema/src/cli/actions/generate.ts b/packages/schema/src/cli/actions/generate.ts index 44ad55612..d697504ee 100644 --- a/packages/schema/src/cli/actions/generate.ts +++ b/packages/schema/src/cli/actions/generate.ts @@ -10,6 +10,7 @@ import { getZenStackPackages, loadDocument, requiredPrismaVersion, + showNotification, } from '../cli-util'; import { PluginRunner, PluginRunnerOptions } from '../plugin-runner'; @@ -22,6 +23,7 @@ type Options = { withPlugins?: string[]; withoutPlugins?: string[]; defaultPlugins: boolean; + offline?: boolean; }; /** @@ -48,11 +50,19 @@ export async function generate(projectPath: string, options: Options) { await runPlugins(options); - if (options.versionCheck) { - // note that we can't run plugins and do version check concurrently because - // plugins are CPU-bound and can cause version check to false timeout - await checkNewVersion(); + // note that we can't run online jobs concurrently with plugins because + // plugins are CPU-bound and can cause false timeout + const postJobs: Promise[] = []; + + if (options.versionCheck && !options.offline) { + postJobs.push(checkNewVersion()); + } + + if (!options.offline) { + postJobs.push(showNotification()); } + + await Promise.all(postJobs); } async function runPlugins(options: Options) { diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 6db9eaaf3..43216656e 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -6,8 +6,10 @@ import { getDocument, LangiumDocument, LangiumDocuments, linkContentToContainer import { NodeFileSystem } from 'langium/node'; import path from 'path'; import semver from 'semver'; +import terminalLink from 'terminal-link'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { URI } from 'vscode-uri'; +import { z } from 'zod'; import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../language-server/constants'; import { ZModelFormatter } from '../language-server/zmodel-formatter'; import { createZModelServices, ZModelServices } from '../language-server/zmodel-module'; @@ -20,6 +22,8 @@ import { CliError } from './cli-error'; export const requiredPrismaVersion = '4.8.0'; const CHECK_VERSION_TIMEOUT = 1000; +const FETCH_CLI_CONFIG_TIMEOUT = 500; +const CLI_CONFIG_ENDPOINT = 'https://zenstack.dev/config/cli.json'; /** * Loads a zmodel document from a file. @@ -364,3 +368,33 @@ async function relinkAll(model: Model, services: ZModelServices) { return newDoc.parseResult.value as Model; } + +export async function showNotification() { + try { + const fetchResult = await fetch(CLI_CONFIG_ENDPOINT, { + headers: { accept: 'application/json' }, + signal: AbortSignal.timeout(FETCH_CLI_CONFIG_TIMEOUT), + }); + + if (!fetchResult.ok) { + return; + } + + const data = await fetchResult.json(); + const schema = z.object({ + notifications: z.array(z.object({ title: z.string(), url: z.string().url(), active: z.boolean() })), + }); + const parseResult = schema.safeParse(data); + + if (parseResult.success) { + const activeItems = parseResult.data.notifications.filter((item) => item.active); + // return a random active item + if (activeItems.length > 0) { + const item = activeItems[Math.floor(Math.random() * activeItems.length)]; + console.log(terminalLink(item.title, item.url)); + } + } + } catch { + // noop + } +} diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index 20207772a..e8773fddd 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -97,7 +97,8 @@ export function createProgram() { 'pnpm', ]); const noVersionCheckOption = new Option('--no-version-check', 'do not check for new version'); - const noDependencyCheck = new Option('--no-dependency-check', 'do not check if dependencies are installed'); + const noDependencyCheckOption = new Option('--no-dependency-check', 'do not check if dependencies are installed'); + const offlineOption = new Option('--offline', 'run in offline mode'); program .command('info') @@ -125,7 +126,8 @@ export function createProgram() { .addOption(new Option('--no-default-plugins', 'do not run default plugins')) .addOption(new Option('--no-compile', 'do not compile the output of core plugins')) .addOption(noVersionCheckOption) - .addOption(noDependencyCheck) + .addOption(noDependencyCheckOption) + .addOption(offlineOption) .action(generateAction); program diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfff08d8f..71318ecb6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -528,6 +528,9 @@ importers: strip-color: specifier: ^0.1.0 version: 0.1.0 + terminal-link: + specifier: ^2.0.0 + version: 2.1.1 tiny-invariant: specifier: ^1.3.1 version: 1.3.3