From c5b21cc439f5cbd4bb1af5e1b3c11cfec026a756 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 17:45:04 +0100 Subject: [PATCH 1/7] feat: weekly versions check Signed-off-by: David Dal Busco --- src/commands/status.ts | 2 +- src/commands/version.ts | 66 +++------------- src/configs/cli.state.config.ts | 2 +- src/configs/cli.versions.config.ts | 20 +++++ src/index.ts | 6 +- src/services/assets/deploy.services.ts | 2 +- .../version/version.check.services.ts | 79 +++++++++++++++++++ .../{ => version}/version.services.ts | 43 +++++++++- src/types/cli/cli.versions.ts | 14 ++++ src/utils/pm.utils.ts | 13 +++ src/version.ts | 11 +++ 11 files changed, 197 insertions(+), 61 deletions(-) create mode 100644 src/configs/cli.versions.config.ts create mode 100644 src/services/version/version.check.services.ts rename src/services/{ => version}/version.services.ts (71%) create mode 100644 src/types/cli/cli.versions.ts create mode 100644 src/version.ts diff --git a/src/commands/status.ts b/src/commands/status.ts index 760fde90..685e09e1 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -12,7 +12,7 @@ import { ORBITER_WASM_NAME, SATELLITE_WASM_NAME } from '../constants/constants'; -import {checkVersion, getSatelliteVersion} from '../services/version.services'; +import {checkVersion, getSatelliteVersion} from '../services/version/version.services'; import type {AssetKey} from '../types/asset-key'; import {toAssetKeys} from '../utils/asset-key.utils'; import {orbiterKey, satelliteKey} from '../utils/cli.config.utils'; diff --git a/src/commands/version.ts b/src/commands/version.ts index c93725c3..f73e0c10 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -1,15 +1,14 @@ -import {isEmptyString, isNullish} from '@dfinity/utils'; -import {green, red} from 'kleur'; -import {clean} from 'semver'; +import {isEmptyString} from '@dfinity/utils'; +import {green} from 'kleur'; import {version as cliCurrentVersion} from '../../package.json'; -import { - githubCliLastRelease, - githubJunoDockerLastRelease, - type GithubLastReleaseResult -} from '../rest/github.rest'; +import {githubCliLastRelease, githubJunoDockerLastRelease} from '../rest/github.rest'; import {findEmulatorVersion} from '../services/emulator/version.services'; -import {checkVersion, type CheckVersionResult} from '../services/version.services'; -import {detectPackageManager} from '../utils/pm.utils'; +import { + buildVersionFromGitHub, + checkVersion, + type CheckVersionResult +} from '../services/version/version.services'; +import {pmInstallHint} from '../utils/pm.utils'; export const version = async () => { const check = await cliVersion(); @@ -23,7 +22,7 @@ export const version = async () => { const cliVersion = async (): Promise => { const result = await buildVersionFromGitHub({ - release: 'CLI', + logReleaseOnError: () => 'CLI', releaseFn: githubCliLastRelease }); @@ -37,23 +36,10 @@ const cliVersion = async (): Promise => { currentVersion: cliCurrentVersion, latestVersion, displayHint: 'CLI', - commandLineHint: installHint() + commandLineHint: pmInstallHint() }); }; -const installHint = (): string => { - const pm = detectPackageManager(); - - switch (pm) { - case 'yarn': - return 'yarn global add @junobuild/cli'; - case 'pnpm': - return 'pnpm add -g @junobuild/cli'; - default: - return 'npm i -g @junobuild/cli'; - } -}; - const emulatorVersion = async () => { const emulatorResult = await findEmulatorVersion(); @@ -64,7 +50,7 @@ const emulatorVersion = async () => { const {version: emulatorCurrentVersion} = emulatorResult; const result = await buildVersionFromGitHub({ - release: 'Juno Docker', + logReleaseOnError: () => 'Juno Docker', releaseFn: githubJunoDockerLastRelease }); @@ -88,31 +74,3 @@ const emulatorVersion = async () => { displayHint: 'Emulator' }); }; - -const buildVersionFromGitHub = async ({ - releaseFn, - release -}: { - releaseFn: () => Promise; - release: 'CLI' | 'Juno Docker'; -}): Promise<{result: 'success'; latestVersion: string} | {result: 'error'}> => { - const githubRelease = await releaseFn(); - - if (githubRelease.status === 'error') { - console.log(red(`Cannot fetch the last version of ${release} on GitHub 😢.`)); - return {result: 'error'}; - } - - const { - release: {tag_name} - } = githubRelease; - - const latestVersion = clean(tag_name); - - if (isNullish(latestVersion)) { - console.log(red(`Cannot extract version from the ${release} release. Reach out Juno❗️`)); - return {result: 'error'}; - } - - return {result: 'success', latestVersion}; -}; diff --git a/src/configs/cli.state.config.ts b/src/configs/cli.state.config.ts index bec9ec63..711525a1 100644 --- a/src/configs/cli.state.config.ts +++ b/src/configs/cli.state.config.ts @@ -7,7 +7,7 @@ import { type CliStateSatelliteAppliedConfigHashes } from '../types/cli/cli.state'; -export const getStateConfig = (): Conf => +const getStateConfig = (): Conf => new Conf({projectName: ENV.config.projectStateName}); export const getLatestAppliedConfig = ({ diff --git a/src/configs/cli.versions.config.ts b/src/configs/cli.versions.config.ts new file mode 100644 index 00000000..b93d9824 --- /dev/null +++ b/src/configs/cli.versions.config.ts @@ -0,0 +1,20 @@ +import Conf from 'conf'; +import {CachedVersions} from '../types/cli/cli.versions'; + +const getVersionConfig = (): Conf => + new Conf({projectName: 'juno-cli-state'}); + +export const getCachedVersions = (): Conf => getVersionConfig(); + +export const updateLastCheckToNow = () => { + const config = getVersionConfig(); + config.set('lastCheck', new Date()); +}; + +export const saveCachedVersions = (versions: Omit) => { + const config = getVersionConfig(); + config.set({ + lastCheck: new Date(), + ...versions + }); +}; diff --git a/src/index.ts b/src/index.ts index aacc688d..0398bff1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ import {logHelpUpgrade} from './help/upgrade.help'; import {logHelpVersion} from './help/version.help'; import {logHelpWhoAmI} from './help/whoami.help'; import {checkNodeVersion} from './utils/env.utils'; +import {checkWeeklyVersions} from './version'; export const run = async () => { const {valid} = checkNodeVersion(); @@ -192,11 +193,14 @@ export const run = async () => { break; case 'help': console.log(help); - break; + process.exit(0); default: console.log(red('Unknown command.')); console.log(help); + process.exit(-1); } + + await checkWeeklyVersions({cmd}); }; // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/src/services/assets/deploy.services.ts b/src/services/assets/deploy.services.ts index ce1be5b9..908e9d19 100644 --- a/src/services/assets/deploy.services.ts +++ b/src/services/assets/deploy.services.ts @@ -7,7 +7,7 @@ import {clearProposalStagedAssets} from '../changes/changes.clear.services'; import {applyConfig} from '../config/apply.services'; import {init} from '../config/init.services'; import {links} from '../links.services'; -import {getSatelliteVersion} from '../version.services'; +import {getSatelliteVersion} from '../version/version.services'; import {parseBatchSize} from './_args.services'; import {deployImmediate} from './_deploy/deploy.individual.services'; import {deployWithProposal as executeDeployWithProposal} from './_deploy/deploy.with-proposal.services'; diff --git a/src/services/version/version.check.services.ts b/src/services/version/version.check.services.ts new file mode 100644 index 00000000..d577f02b --- /dev/null +++ b/src/services/version/version.check.services.ts @@ -0,0 +1,79 @@ +import {isNullish, nonNullish} from '@dfinity/utils'; +import {compare} from 'semver'; +import {version as cliCurrentVersion} from '../../../package.json'; +import { + getCachedVersions, + saveCachedVersions, + updateLastCheckToNow +} from '../../configs/cli.versions.config'; +import {githubCliLastRelease} from '../../rest/github.rest'; +import {pmInstallHint} from '../../utils/pm.utils'; +import {buildVersionFromGitHub, checkVersion} from './version.services'; + +const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; + +export const checkVersions = async () => { + const cachedVersions = getCachedVersions(); + + const lastCheck = cachedVersions.get('lastCheck'); + + if (isNullish(lastCheck)) { + saveCachedVersions({ + cli: { + local: cliCurrentVersion + } + }); + return; + } + + const cachedCliLocalVersion = cachedVersions.get('cli')?.local; + + // The version was never cached or the developer upgraded since the last check. + // We assume they are on the latest version. If not, the next weekly check will catch it. + if (isNullish(cachedCliLocalVersion) || compare(cliCurrentVersion, cachedCliLocalVersion) > 0) { + saveCachedVersions({ + cli: { + local: cliCurrentVersion + } + }); + return; + } + + const checkIsDue = new Date(lastCheck.getTime() + ONE_WEEK_MS).getTime() <= new Date().getTime(); + + const cachedCliRemoteVersion = cachedVersions.get('cli')?.remote; + + // The weekly check is not due and the current version of the CLI is up to date + if ( + !checkIsDue && + nonNullish(cachedCliRemoteVersion) && + compare(cliCurrentVersion, cachedCliRemoteVersion) >= 0 + ) { + return; + } + + const result = await buildVersionFromGitHub({ + releaseFn: githubCliLastRelease + }); + + if (result.result === 'error') { + updateLastCheckToNow(); + return; + } + + const {latestVersion: remoteCliVersion} = result; + + saveCachedVersions({ + cli: { + local: cliCurrentVersion, + remote: remoteCliVersion + } + }); + + checkVersion({ + currentVersion: cliCurrentVersion, + latestVersion: remoteCliVersion, + displayHint: 'CLI', + commandLineHint: pmInstallHint() + }); +}; diff --git a/src/services/version.services.ts b/src/services/version/version.services.ts similarity index 71% rename from src/services/version.services.ts rename to src/services/version/version.services.ts index 3fb877ce..7e9aa3cd 100644 --- a/src/services/version.services.ts +++ b/src/services/version/version.services.ts @@ -7,9 +7,10 @@ import { import {JUNO_PACKAGE_SATELLITE_ID} from '@junobuild/config'; import {cyan, green, red, yellow} from 'kleur'; import ora from 'ora'; -import {compare} from 'semver'; -import type {SatelliteParametersWithId} from '../types/satellite'; -import {assertConfigAndLoadSatelliteContext} from '../utils/juno.config.utils'; +import {clean, compare} from 'semver'; +import {GithubLastReleaseResult} from '../../rest/github.rest'; +import type {SatelliteParametersWithId} from '../../types/satellite'; +import {assertConfigAndLoadSatelliteContext} from '../../utils/juno.config.utils'; export const getSatelliteVersion = async (): Promise< {result: 'success'; version: string} | {result: 'error'} @@ -112,3 +113,39 @@ export const checkVersion = ({ return {diff: 'outdated'}; }; + +export const buildVersionFromGitHub = async ({ + releaseFn, + logReleaseOnError +}: { + releaseFn: () => Promise; + logReleaseOnError?: () => 'CLI' | 'Juno Docker'; +}): Promise<{result: 'success'; latestVersion: string} | {result: 'error'}> => { + const githubRelease = await releaseFn(); + + if (githubRelease.status === 'error') { + if (nonNullish(logReleaseOnError)) { + console.log(red(`Cannot fetch the last version of ${logReleaseOnError()} on GitHub 😢.`)); + } + + return {result: 'error'}; + } + + const { + release: {tag_name} + } = githubRelease; + + const latestVersion = clean(tag_name); + + if (isNullish(latestVersion)) { + if (nonNullish(logReleaseOnError)) { + console.log( + red(`Cannot extract version from the ${logReleaseOnError()} release. Reach out Juno❗️`) + ); + } + + return {result: 'error'}; + } + + return {result: 'success', latestVersion}; +}; diff --git a/src/types/cli/cli.versions.ts b/src/types/cli/cli.versions.ts new file mode 100644 index 00000000..946810f4 --- /dev/null +++ b/src/types/cli/cli.versions.ts @@ -0,0 +1,14 @@ +import {j} from '@junobuild/schema'; + +const CachedVersionSchema = j.strictObject({ + local: j.string(), + remote: j.string().optional() +}); + +const CachedVersionsSchema = j.strictObject({ + lastCheck: j.date().optional(), + cli: CachedVersionSchema.optional(), + emulator: CachedVersionSchema.optional() +}); + +export type CachedVersions = j.infer; diff --git a/src/utils/pm.utils.ts b/src/utils/pm.utils.ts index 46f7e878..0a1c0eaa 100644 --- a/src/utils/pm.utils.ts +++ b/src/utils/pm.utils.ts @@ -23,3 +23,16 @@ export const detectPackageManager = (): PackageManager | undefined => { return undefined; }; + +export const pmInstallHint = (): string => { + const pm = detectPackageManager(); + + switch (pm) { + case 'yarn': + return 'yarn global add @junobuild/cli'; + case 'pnpm': + return 'pnpm add -g @junobuild/cli'; + default: + return 'npm i -g @junobuild/cli'; + } +} \ No newline at end of file diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 00000000..10457c9e --- /dev/null +++ b/src/version.ts @@ -0,0 +1,11 @@ +import {checkVersions} from './services/version/version.check.services'; +import {isHeadless} from './utils/process.utils'; + +export const checkWeeklyVersions = async ({cmd}: {cmd: string}) => { + // No check if used in headless mode + if (isHeadless()) { + return; + } + + await checkVersions(); +}; From 4877b1d266806d05029bc409cf097a3ba76eb50e Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 18:02:39 +0100 Subject: [PATCH 2/7] feat: iso date Signed-off-by: David Dal Busco --- src/configs/cli.versions.config.ts | 6 +++--- src/services/version/version.check.services.ts | 14 ++++++++++++-- src/services/version/version.services.ts | 9 +++++++-- src/types/cli/cli.versions.ts | 2 +- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/configs/cli.versions.config.ts b/src/configs/cli.versions.config.ts index b93d9824..d4a935eb 100644 --- a/src/configs/cli.versions.config.ts +++ b/src/configs/cli.versions.config.ts @@ -2,19 +2,19 @@ import Conf from 'conf'; import {CachedVersions} from '../types/cli/cli.versions'; const getVersionConfig = (): Conf => - new Conf({projectName: 'juno-cli-state'}); + new Conf({projectName: 'juno-cli-versions'}); export const getCachedVersions = (): Conf => getVersionConfig(); export const updateLastCheckToNow = () => { const config = getVersionConfig(); - config.set('lastCheck', new Date()); + config.set('lastCheck', new Date().toISOString()); }; export const saveCachedVersions = (versions: Omit) => { const config = getVersionConfig(); config.set({ - lastCheck: new Date(), + lastCheck: new Date().toISOString(), ...versions }); }; diff --git a/src/services/version/version.check.services.ts b/src/services/version/version.check.services.ts index d577f02b..2f5c0bea 100644 --- a/src/services/version/version.check.services.ts +++ b/src/services/version/version.check.services.ts @@ -39,7 +39,8 @@ export const checkVersions = async () => { return; } - const checkIsDue = new Date(lastCheck.getTime() + ONE_WEEK_MS).getTime() <= new Date().getTime(); + const checkIsDue = + new Date(new Date(lastCheck).getTime() + ONE_WEEK_MS).getTime() <= new Date().getTime(); const cachedCliRemoteVersion = cachedVersions.get('cli')?.remote; @@ -70,10 +71,19 @@ export const checkVersions = async () => { } }); + console.log( + '\n────────────────────────────────────────────────────────────────────────────────────────────────────' + ); + checkVersion({ currentVersion: cliCurrentVersion, latestVersion: remoteCliVersion, displayHint: 'CLI', - commandLineHint: pmInstallHint() + commandLineHint: pmInstallHint(), + logUpToDate: false }); + + console.log( + '────────────────────────────────────────────────────────────────────────────────────────────────────' + ); }; diff --git a/src/services/version/version.services.ts b/src/services/version/version.services.ts index 7e9aa3cd..fbad8b43 100644 --- a/src/services/version/version.services.ts +++ b/src/services/version/version.services.ts @@ -86,17 +86,22 @@ export const checkVersion = ({ currentVersion, latestVersion, displayHint, - commandLineHint + commandLineHint, + logUpToDate = true }: { currentVersion: string; latestVersion: string; displayHint: string; commandLineHint?: string; + logUpToDate?: boolean; }): CheckVersionResult => { const diff = compare(currentVersion, latestVersion); if (diff === 0) { - console.log(`Your ${displayHint} (${green(`v${currentVersion}`)}) is up-to-date.`); + if (logUpToDate) { + console.log(`Your ${displayHint} (${green(`v${currentVersion}`)}) is up-to-date.`); + } + return {diff: 'up-to-date'}; } diff --git a/src/types/cli/cli.versions.ts b/src/types/cli/cli.versions.ts index 946810f4..72205546 100644 --- a/src/types/cli/cli.versions.ts +++ b/src/types/cli/cli.versions.ts @@ -6,7 +6,7 @@ const CachedVersionSchema = j.strictObject({ }); const CachedVersionsSchema = j.strictObject({ - lastCheck: j.date().optional(), + lastCheck: j.iso.datetime(), cli: CachedVersionSchema.optional(), emulator: CachedVersionSchema.optional() }); From 387f8374a553cdb7d429363d6abb84513dac6b82 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 18:13:59 +0100 Subject: [PATCH 3/7] feat: same version pattern Signed-off-by: David Dal Busco --- src/configs/cli.versions.config.ts | 16 ++++++--- .../version/version.check.services.ts | 34 ++++++++----------- src/types/cli/cli.versions.ts | 5 +-- src/version.ts | 1 + 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/configs/cli.versions.config.ts b/src/configs/cli.versions.config.ts index d4a935eb..da6da36b 100644 --- a/src/configs/cli.versions.config.ts +++ b/src/configs/cli.versions.config.ts @@ -1,19 +1,25 @@ import Conf from 'conf'; -import {CachedVersions} from '../types/cli/cli.versions'; +import {CachedVersion, CachedVersions} from '../types/cli/cli.versions'; const getVersionConfig = (): Conf => new Conf({projectName: 'juno-cli-versions'}); export const getCachedVersions = (): Conf => getVersionConfig(); -export const updateLastCheckToNow = () => { +export const updateCliLastCheckToNow = () => { const config = getVersionConfig(); - config.set('lastCheck', new Date().toISOString()); + + const currentCli = config.get('cli'); + + config.set('cli', { + lastCheck: new Date().toISOString(), + ...(currentCli ?? {}) + }); }; -export const saveCachedVersions = (versions: Omit) => { +export const saveCliCachedVersions = (versions: Omit) => { const config = getVersionConfig(); - config.set({ + config.set('cli', { lastCheck: new Date().toISOString(), ...versions }); diff --git a/src/services/version/version.check.services.ts b/src/services/version/version.check.services.ts index 2f5c0bea..6ab96842 100644 --- a/src/services/version/version.check.services.ts +++ b/src/services/version/version.check.services.ts @@ -3,8 +3,8 @@ import {compare} from 'semver'; import {version as cliCurrentVersion} from '../../../package.json'; import { getCachedVersions, - saveCachedVersions, - updateLastCheckToNow + saveCliCachedVersions, + updateCliLastCheckToNow } from '../../configs/cli.versions.config'; import {githubCliLastRelease} from '../../rest/github.rest'; import {pmInstallHint} from '../../utils/pm.utils'; @@ -15,26 +15,24 @@ const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; export const checkVersions = async () => { const cachedVersions = getCachedVersions(); - const lastCheck = cachedVersions.get('lastCheck'); + const cachedCli = cachedVersions.get('cli'); + + const lastCheck = cachedCli?.lastCheck; if (isNullish(lastCheck)) { - saveCachedVersions({ - cli: { - local: cliCurrentVersion - } + saveCliCachedVersions({ + local: cliCurrentVersion }); return; } - const cachedCliLocalVersion = cachedVersions.get('cli')?.local; + const cachedCliLocalVersion = cachedCli?.local; // The version was never cached or the developer upgraded since the last check. // We assume they are on the latest version. If not, the next weekly check will catch it. if (isNullish(cachedCliLocalVersion) || compare(cliCurrentVersion, cachedCliLocalVersion) > 0) { - saveCachedVersions({ - cli: { - local: cliCurrentVersion - } + saveCliCachedVersions({ + local: cliCurrentVersion }); return; } @@ -42,7 +40,7 @@ export const checkVersions = async () => { const checkIsDue = new Date(new Date(lastCheck).getTime() + ONE_WEEK_MS).getTime() <= new Date().getTime(); - const cachedCliRemoteVersion = cachedVersions.get('cli')?.remote; + const cachedCliRemoteVersion = cachedCli?.remote; // The weekly check is not due and the current version of the CLI is up to date if ( @@ -58,17 +56,15 @@ export const checkVersions = async () => { }); if (result.result === 'error') { - updateLastCheckToNow(); + updateCliLastCheckToNow(); return; } const {latestVersion: remoteCliVersion} = result; - saveCachedVersions({ - cli: { - local: cliCurrentVersion, - remote: remoteCliVersion - } + saveCliCachedVersions({ + local: cliCurrentVersion, + remote: remoteCliVersion }); console.log( diff --git a/src/types/cli/cli.versions.ts b/src/types/cli/cli.versions.ts index 72205546..78c2442a 100644 --- a/src/types/cli/cli.versions.ts +++ b/src/types/cli/cli.versions.ts @@ -1,14 +1,15 @@ import {j} from '@junobuild/schema'; const CachedVersionSchema = j.strictObject({ - local: j.string(), + lastCheck: j.iso.datetime(), + local: j.string().optional(), remote: j.string().optional() }); const CachedVersionsSchema = j.strictObject({ - lastCheck: j.iso.datetime(), cli: CachedVersionSchema.optional(), emulator: CachedVersionSchema.optional() }); +export type CachedVersion = j.infer; export type CachedVersions = j.infer; diff --git a/src/version.ts b/src/version.ts index 10457c9e..74d3b318 100644 --- a/src/version.ts +++ b/src/version.ts @@ -7,5 +7,6 @@ export const checkWeeklyVersions = async ({cmd}: {cmd: string}) => { return; } + // {withEmulator: ["functions", "fn"].includes(cmd)} await checkVersions(); }; From 06ad87b87afb95296d33f0a3df939c18a62843e8 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 18:42:16 +0100 Subject: [PATCH 4/7] feat: check emulator version Signed-off-by: David Dal Busco --- src/configs/cli.versions.config.ts | 18 +- .../version/version.check.services.ts | 157 +++++++++++++----- src/services/version/version.services.ts | 22 ++- src/version.ts | 9 +- 4 files changed, 156 insertions(+), 50 deletions(-) diff --git a/src/configs/cli.versions.config.ts b/src/configs/cli.versions.config.ts index da6da36b..49d9c4fb 100644 --- a/src/configs/cli.versions.config.ts +++ b/src/configs/cli.versions.config.ts @@ -6,20 +6,26 @@ const getVersionConfig = (): Conf => export const getCachedVersions = (): Conf => getVersionConfig(); -export const updateCliLastCheckToNow = () => { +export const updateLastCheckToNow = ({key}: {key: keyof CachedVersions}) => { const config = getVersionConfig(); - const currentCli = config.get('cli'); + const currentVersions = config.get(key); - config.set('cli', { + config.set(key, { lastCheck: new Date().toISOString(), - ...(currentCli ?? {}) + ...(currentVersions ?? {}) }); }; -export const saveCliCachedVersions = (versions: Omit) => { +export const saveCachedVersions = ({ + key, + versions +}: { + key: keyof CachedVersions; + versions: Omit; +}) => { const config = getVersionConfig(); - config.set('cli', { + config.set(key, { lastCheck: new Date().toISOString(), ...versions }); diff --git a/src/services/version/version.check.services.ts b/src/services/version/version.check.services.ts index 6ab96842..c1000c4f 100644 --- a/src/services/version/version.check.services.ts +++ b/src/services/version/version.check.services.ts @@ -1,38 +1,118 @@ import {isNullish, nonNullish} from '@dfinity/utils'; +import ora from 'ora'; import {compare} from 'semver'; import {version as cliCurrentVersion} from '../../../package.json'; import { getCachedVersions, - saveCliCachedVersions, - updateCliLastCheckToNow + saveCachedVersions, + updateLastCheckToNow } from '../../configs/cli.versions.config'; -import {githubCliLastRelease} from '../../rest/github.rest'; +import { + githubCliLastRelease, + githubJunoDockerLastRelease, + GithubLastReleaseResult +} from '../../rest/github.rest'; +import {CachedVersions} from '../../types/cli/cli.versions'; import {pmInstallHint} from '../../utils/pm.utils'; -import {buildVersionFromGitHub, checkVersion} from './version.services'; +import {findEmulatorVersion} from '../emulator/version.services'; +import { + buildVersionFromGitHub, + BuildVersionFromGitHubResult, + checkVersion +} from './version.services'; const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; -export const checkVersions = async () => { +export const checkCliVersion = async () => { + const checkVersionFn = ({latestVersion}: {latestVersion: string}) => { + checkVersion({ + currentVersion: cliCurrentVersion, + latestVersion, + displayHint: 'CLI', + commandLineHint: pmInstallHint(), + logUpToDate: false, + logSpacer: true + }); + }; + + await check({ + key: 'cli', + currentVersion: cliCurrentVersion, + releaseFn: githubCliLastRelease, + checkVersionFn + }); +}; + +export const checkEmulatorVersion = async () => { + const emulatorResult = await findEmulatorVersion(); + + if (emulatorResult.status !== 'success') { + return; + } + + const {version: emulatorCurrentVersion} = emulatorResult; + + // We fetched the emulator but the version is null which could happen has providing the metadata + // was patched in Juno Docker v0.6.3 + if (isNullish(emulatorCurrentVersion)) { + return; + } + + const checkVersionFn = ({latestVersion}: {latestVersion: string}) => { + checkVersion({ + currentVersion: emulatorCurrentVersion, + latestVersion, + displayHint: 'Emulator', + logUpToDate: false, + logSpacer: true + }); + }; + + await check({ + key: 'emulator', + currentVersion: emulatorCurrentVersion, + releaseFn: githubJunoDockerLastRelease, + checkVersionFn + }); +}; + +const check = async ({ + key, + currentVersion, + releaseFn, + checkVersionFn +}: { + key: keyof CachedVersions; + currentVersion: string; + releaseFn: () => Promise; + checkVersionFn: (params: {latestVersion: string}) => void; +}) => { const cachedVersions = getCachedVersions(); - const cachedCli = cachedVersions.get('cli'); + const cachedInfo = cachedVersions.get(key); - const lastCheck = cachedCli?.lastCheck; + const lastCheck = cachedInfo?.lastCheck; if (isNullish(lastCheck)) { - saveCliCachedVersions({ - local: cliCurrentVersion + saveCachedVersions({ + key, + versions: { + local: currentVersion + } }); return; } - const cachedCliLocalVersion = cachedCli?.local; + const cachedLocalVersion = cachedInfo?.local; // The version was never cached or the developer upgraded since the last check. // We assume they are on the latest version. If not, the next weekly check will catch it. - if (isNullish(cachedCliLocalVersion) || compare(cliCurrentVersion, cachedCliLocalVersion) > 0) { - saveCliCachedVersions({ - local: cliCurrentVersion + if (isNullish(cachedLocalVersion) || compare(currentVersion, cachedLocalVersion) > 0) { + saveCachedVersions({ + key, + versions: { + local: currentVersion + } }); return; } @@ -40,46 +120,45 @@ export const checkVersions = async () => { const checkIsDue = new Date(new Date(lastCheck).getTime() + ONE_WEEK_MS).getTime() <= new Date().getTime(); - const cachedCliRemoteVersion = cachedCli?.remote; + const cachedRemoteVersion = cachedInfo?.remote; // The weekly check is not due and the current version of the CLI is up to date if ( !checkIsDue && - nonNullish(cachedCliRemoteVersion) && - compare(cliCurrentVersion, cachedCliRemoteVersion) >= 0 + nonNullish(cachedRemoteVersion) && + compare(currentVersion, cachedRemoteVersion) >= 0 ) { return; } - const result = await buildVersionFromGitHub({ - releaseFn: githubCliLastRelease - }); + const loadVersionWithGitHub = async (): Promise => { + const spinner = ora(`Two secs, fetching ${key} latest version...`).start(); + + try { + return await buildVersionFromGitHub({ + releaseFn + }); + } finally { + spinner.stop(); + } + }; + + const result = await loadVersionWithGitHub(); if (result.result === 'error') { - updateCliLastCheckToNow(); + updateLastCheckToNow({key}); return; } - const {latestVersion: remoteCliVersion} = result; + const {latestVersion} = result; - saveCliCachedVersions({ - local: cliCurrentVersion, - remote: remoteCliVersion - }); - - console.log( - '\n────────────────────────────────────────────────────────────────────────────────────────────────────' - ); - - checkVersion({ - currentVersion: cliCurrentVersion, - latestVersion: remoteCliVersion, - displayHint: 'CLI', - commandLineHint: pmInstallHint(), - logUpToDate: false + saveCachedVersions({ + key, + versions: { + local: currentVersion, + remote: latestVersion + } }); - console.log( - '────────────────────────────────────────────────────────────────────────────────────────────────────' - ); + checkVersionFn({latestVersion}); }; diff --git a/src/services/version/version.services.ts b/src/services/version/version.services.ts index fbad8b43..3f056ed0 100644 --- a/src/services/version/version.services.ts +++ b/src/services/version/version.services.ts @@ -87,17 +87,23 @@ export const checkVersion = ({ latestVersion, displayHint, commandLineHint, - logUpToDate = true + logUpToDate = true, + logSpacer = false }: { currentVersion: string; latestVersion: string; displayHint: string; commandLineHint?: string; logUpToDate?: boolean; + logSpacer?: boolean; }): CheckVersionResult => { const diff = compare(currentVersion, latestVersion); if (diff === 0) { + if (logSpacer) { + console.log(''); + } + if (logUpToDate) { console.log(`Your ${displayHint} (${green(`v${currentVersion}`)}) is up-to-date.`); } @@ -106,10 +112,18 @@ export const checkVersion = ({ } if (diff === 1) { + if (logSpacer) { + console.log(''); + } + console.log(yellow(`Your ${displayHint} version is more recent than the latest available 🤔.`)); return {diff: 'error'}; } + if (logSpacer) { + console.log(''); + } + console.log( `Your ${displayHint} (${yellow(`v${currentVersion}`)}) is behind the latest version (${green( `v${latestVersion}` @@ -119,13 +133,17 @@ export const checkVersion = ({ return {diff: 'outdated'}; }; +export type BuildVersionFromGitHubResult = + | {result: 'success'; latestVersion: string} + | {result: 'error'}; + export const buildVersionFromGitHub = async ({ releaseFn, logReleaseOnError }: { releaseFn: () => Promise; logReleaseOnError?: () => 'CLI' | 'Juno Docker'; -}): Promise<{result: 'success'; latestVersion: string} | {result: 'error'}> => { +}): Promise => { const githubRelease = await releaseFn(); if (githubRelease.status === 'error') { diff --git a/src/version.ts b/src/version.ts index 74d3b318..231f3051 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,4 @@ -import {checkVersions} from './services/version/version.check.services'; +import {checkCliVersion, checkEmulatorVersion} from './services/version/version.check.services'; import {isHeadless} from './utils/process.utils'; export const checkWeeklyVersions = async ({cmd}: {cmd: string}) => { @@ -7,6 +7,9 @@ export const checkWeeklyVersions = async ({cmd}: {cmd: string}) => { return; } - // {withEmulator: ["functions", "fn"].includes(cmd)} - await checkVersions(); + await checkCliVersion(); + + if (['functions', 'fn'].includes(cmd)) { + await checkEmulatorVersion(); + } }; From c58409bfe3f0b2e7d17007d7b77c8b1b2e1e078f Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 18:44:41 +0100 Subject: [PATCH 5/7] feat: no fetch if not needed Signed-off-by: David Dal Busco --- src/services/version/version.check.services.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/services/version/version.check.services.ts b/src/services/version/version.check.services.ts index c1000c4f..3373a8c3 100644 --- a/src/services/version/version.check.services.ts +++ b/src/services/version/version.check.services.ts @@ -122,12 +122,15 @@ const check = async ({ const cachedRemoteVersion = cachedInfo?.remote; - // The weekly check is not due and the current version of the CLI is up to date - if ( - !checkIsDue && - nonNullish(cachedRemoteVersion) && - compare(currentVersion, cachedRemoteVersion) >= 0 - ) { + // The weekly check is not due and we got a remote version in cache + if (!checkIsDue && nonNullish(cachedRemoteVersion)) { + // The current version is newer, we assume the dev use the latest + if (compare(currentVersion, cachedRemoteVersion) >= 0) { + return; + } + + // Behind but check not due, compare cached remote version + checkVersionFn({latestVersion: cachedRemoteVersion}); return; } From c5e82dae1ee8bad019a3b5b5941044bdb73736ee Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 20:37:18 +0100 Subject: [PATCH 6/7] feat: fine tuning Signed-off-by: David Dal Busco --- src/services/version/version.check.services.ts | 2 +- src/version.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/services/version/version.check.services.ts b/src/services/version/version.check.services.ts index 3373a8c3..73197f7c 100644 --- a/src/services/version/version.check.services.ts +++ b/src/services/version/version.check.services.ts @@ -124,7 +124,7 @@ const check = async ({ // The weekly check is not due and we got a remote version in cache if (!checkIsDue && nonNullish(cachedRemoteVersion)) { - // The current version is newer, we assume the dev use the latest + // The current version is newer or equals, we assume the dev use the latest if (compare(currentVersion, cachedRemoteVersion) >= 0) { return; } diff --git a/src/version.ts b/src/version.ts index 231f3051..1f5924d4 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,12 +1,18 @@ import {checkCliVersion, checkEmulatorVersion} from './services/version/version.check.services'; import {isHeadless} from './utils/process.utils'; -export const checkWeeklyVersions = async ({cmd}: {cmd: string}) => { +export const checkWeeklyVersions = async ({cmd, args}: {cmd: string; args?: string[]}) => { // No check if used in headless mode if (isHeadless()) { return; } + const [subCommand] = args ?? []; + + if (cmd === 'emulator' && ['start', 'wait'].includes(subCommand)) { + return; + } + await checkCliVersion(); if (['functions', 'fn'].includes(cmd)) { From a685e0d35812c08e7793e9e575fee5c11b18d13a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 20:41:13 +0100 Subject: [PATCH 7/7] chore: lint Signed-off-by: David Dal Busco --- src/configs/cli.versions.config.ts | 2 +- src/index.ts | 1 + src/services/version/version.check.services.ts | 6 +++--- src/services/version/version.services.ts | 2 +- src/types/cli/cli.versions.ts | 4 ++-- src/utils/pm.utils.ts | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/configs/cli.versions.config.ts b/src/configs/cli.versions.config.ts index 49d9c4fb..655050a4 100644 --- a/src/configs/cli.versions.config.ts +++ b/src/configs/cli.versions.config.ts @@ -1,5 +1,5 @@ import Conf from 'conf'; -import {CachedVersion, CachedVersions} from '../types/cli/cli.versions'; +import {type CachedVersion, type CachedVersions} from '../types/cli/cli.versions'; const getVersionConfig = (): Conf => new Conf({projectName: 'juno-cli-versions'}); diff --git a/src/index.ts b/src/index.ts index 0398bff1..c3d9de77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -194,6 +194,7 @@ export const run = async () => { case 'help': console.log(help); process.exit(0); + break; default: console.log(red('Unknown command.')); console.log(help); diff --git a/src/services/version/version.check.services.ts b/src/services/version/version.check.services.ts index 73197f7c..35830f55 100644 --- a/src/services/version/version.check.services.ts +++ b/src/services/version/version.check.services.ts @@ -10,14 +10,14 @@ import { import { githubCliLastRelease, githubJunoDockerLastRelease, - GithubLastReleaseResult + type GithubLastReleaseResult } from '../../rest/github.rest'; -import {CachedVersions} from '../../types/cli/cli.versions'; +import {type CachedVersions} from '../../types/cli/cli.versions'; import {pmInstallHint} from '../../utils/pm.utils'; import {findEmulatorVersion} from '../emulator/version.services'; import { buildVersionFromGitHub, - BuildVersionFromGitHubResult, + type BuildVersionFromGitHubResult, checkVersion } from './version.services'; diff --git a/src/services/version/version.services.ts b/src/services/version/version.services.ts index 3f056ed0..18f41dd2 100644 --- a/src/services/version/version.services.ts +++ b/src/services/version/version.services.ts @@ -8,7 +8,7 @@ import {JUNO_PACKAGE_SATELLITE_ID} from '@junobuild/config'; import {cyan, green, red, yellow} from 'kleur'; import ora from 'ora'; import {clean, compare} from 'semver'; -import {GithubLastReleaseResult} from '../../rest/github.rest'; +import {type GithubLastReleaseResult} from '../../rest/github.rest'; import type {SatelliteParametersWithId} from '../../types/satellite'; import {assertConfigAndLoadSatelliteContext} from '../../utils/juno.config.utils'; diff --git a/src/types/cli/cli.versions.ts b/src/types/cli/cli.versions.ts index 78c2442a..365665a4 100644 --- a/src/types/cli/cli.versions.ts +++ b/src/types/cli/cli.versions.ts @@ -1,12 +1,12 @@ import {j} from '@junobuild/schema'; -const CachedVersionSchema = j.strictObject({ +export const CachedVersionSchema = j.strictObject({ lastCheck: j.iso.datetime(), local: j.string().optional(), remote: j.string().optional() }); -const CachedVersionsSchema = j.strictObject({ +export const CachedVersionsSchema = j.strictObject({ cli: CachedVersionSchema.optional(), emulator: CachedVersionSchema.optional() }); diff --git a/src/utils/pm.utils.ts b/src/utils/pm.utils.ts index 0a1c0eaa..657288f6 100644 --- a/src/utils/pm.utils.ts +++ b/src/utils/pm.utils.ts @@ -35,4 +35,4 @@ export const pmInstallHint = (): string => { default: return 'npm i -g @junobuild/cli'; } -} \ No newline at end of file +};