From 79cc459bf004af10bb059c2411edf5a9650b2cc0 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 14:47:44 +0100 Subject: [PATCH 1/3] feat: shows emulator version Signed-off-by: David Dal Busco --- src/commands/version.ts | 88 +++++++++++++++++++---- src/constants/constants.ts | 1 + src/rest/github.rest.ts | 31 +++++--- src/services/emulator/version.services.ts | 11 +++ src/services/version.services.ts | 12 +++- src/utils/runner.utils.ts | 15 ++++ 6 files changed, 131 insertions(+), 27 deletions(-) create mode 100644 src/services/emulator/version.services.ts diff --git a/src/commands/version.ts b/src/commands/version.ts index a34a71c6..26e49a90 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -2,31 +2,38 @@ import {isNullish} from '@dfinity/utils'; import {red} from 'kleur'; import {clean} from 'semver'; import {version as cliCurrentVersion} from '../../package.json'; -import {githubCliLastRelease} from '../rest/github.rest'; -import {checkVersion} from '../services/version.services'; +import { + githubCliLastRelease, + githubJunoDockerLastRelease, + GithubLastReleaseResult +} from '../rest/github.rest'; +import {checkVersion, CheckVersionResult} from '../services/version.services'; import {detectPackageManager} from '../utils/pm.utils'; -export const version = async () => { - await cliVersion(); -}; +import {readEmulatorConfig} from '../configs/emulator.config'; -const cliVersion = async () => { - const githubRelease = await githubCliLastRelease(); +export const version = async () => { + const check = await cliVersion(); - if (githubRelease === undefined) { - console.log(red('Cannot fetch last release version of Juno on GitHub 😢.')); + if (check.diff === 'error') { return; } - const {tag_name} = githubRelease; + await emulatorVersion(); +}; - const latestVersion = clean(tag_name); +const cliVersion = async (): Promise => { + const result = await buildVersionFromGitHub({ + release: 'CLI', + releaseFn: githubCliLastRelease + }); - if (isNullish(latestVersion)) { - console.log(red(`Cannot extract version from release. Reach out Juno❗️`)); - return; + if (result.result === 'error') { + return {diff: 'error'}; } - checkVersion({ + const {latestVersion} = result; + + return checkVersion({ currentVersion: cliCurrentVersion, latestVersion, displayHint: 'CLI', @@ -46,3 +53,54 @@ const installHint = (): string => { return 'npm i -g @junobuild/cli'; } }; + +const emulatorVersion = async (): Promise => { + const parsedResult = await readEmulatorConfig(); + + if (!parsedResult.success) { + return {diff: "error"}; + } + + + + const result = await buildVersionFromGitHub({ + release: 'Juno Docker', + releaseFn: githubJunoDockerLastRelease + }); + + if (result.result === 'error') { + return {diff: 'error'}; + } + + const {latestVersion} = result; + + +}; + +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/constants/constants.ts b/src/constants/constants.ts index 32cd4fdb..6905e7e8 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -6,6 +6,7 @@ export const ORBITER_WASM_NAME = 'orbiter'; export const NODE_VERSION = 20; export const JUNO_CDN_URL = 'https://cdn.juno.build'; export const GITHUB_API_CLI_URL = 'https://api.github.com/repos/junobuild/cli'; +export const GITHUB_API_JUNO_DOCKER_URL = 'https://api.github.com/repos/junobuild/juno-docker'; /** * Revoked principals that must not be used. diff --git a/src/rest/github.rest.ts b/src/rest/github.rest.ts index c92b1e7c..2501984a 100644 --- a/src/rest/github.rest.ts +++ b/src/rest/github.rest.ts @@ -1,4 +1,4 @@ -import {GITHUB_API_CLI_URL} from '../constants/constants'; +import {GITHUB_API_CLI_URL, GITHUB_API_JUNO_DOCKER_URL} from '../constants/constants'; export interface GitHubAsset { url: string; // 'https://api.github.com/repos/peterpeterparker/dummy/releases/assets/91555492' @@ -38,16 +38,29 @@ const GITHUB_API_HEADERS: RequestInit = { } }; -const githubLastRelease = async (apiUrl: string): Promise => { - const response = await fetch(`${apiUrl}/releases/latest`, GITHUB_API_HEADERS); +export type GithubLastReleaseResult = + | {status: 'success'; release: GitHubRelease} + | {status: 'error'; err?: unknown}; - if (!response.ok) { - return undefined; - } +const githubLastRelease = async ( + apiUrl: string +): Promise<{status: 'success'; release: GitHubRelease} | {status: 'error'; err?: unknown}> => { + try { + const response = await fetch(`${apiUrl}/releases/latest`, GITHUB_API_HEADERS); + + if (!response.ok) { + return {status: 'error'}; + } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return await response.json(); + const release = await response.json(); + return {status: 'success', release}; + } catch (err: unknown) { + return {status: 'error', err}; + } }; -export const githubCliLastRelease = async (): Promise => +export const githubCliLastRelease = async (): Promise => await githubLastRelease(GITHUB_API_CLI_URL); + +export const githubJunoDockerLastRelease = async (): Promise => + await githubLastRelease(GITHUB_API_JUNO_DOCKER_URL); diff --git a/src/services/emulator/version.services.ts b/src/services/emulator/version.services.ts new file mode 100644 index 00000000..15ebc101 --- /dev/null +++ b/src/services/emulator/version.services.ts @@ -0,0 +1,11 @@ +import {readEmulatorConfig} from '../../configs/emulator.config'; + +export const findEmulatorVersion = async () => { + const parsedResult = await readEmulatorConfig(); + + if (!parsedResult.success) { + return {diff: 'error'}; + } + + const {config} = parsedResult; +} \ No newline at end of file diff --git a/src/services/version.services.ts b/src/services/version.services.ts index 45b7a4db..a4affb83 100644 --- a/src/services/version.services.ts +++ b/src/services/version.services.ts @@ -77,6 +77,10 @@ const loadSatelliteVersion = async ({ return {result: 'success', version: legacyVersion}; }; +export interface CheckVersionResult { + diff: 'up-to-date' | 'outdated' | 'error'; +} + export const checkVersion = ({ currentVersion, latestVersion, @@ -87,17 +91,17 @@ export const checkVersion = ({ latestVersion: string; displayHint: string; commandLineHint: string; -}) => { +}): CheckVersionResult => { const diff = compare(currentVersion, latestVersion); if (diff === 0) { console.log(`Your ${displayHint} (${green(`v${currentVersion}`)}) is up-to-date.`); - return; + return {diff: 'up-to-date'}; } if (diff === 1) { console.log(yellow(`Your ${displayHint} version is more recent than the latest available 🤔.`)); - return; + return {diff: 'error'}; } console.log( @@ -105,4 +109,6 @@ export const checkVersion = ({ `v${latestVersion}` )}) available. Run ${cyan(commandLineHint)} to update it.` ); + + return {diff: 'outdated'}; }; diff --git a/src/utils/runner.utils.ts b/src/utils/runner.utils.ts index 1847cc81..6a7e5184 100644 --- a/src/utils/runner.utils.ts +++ b/src/utils/runner.utils.ts @@ -87,3 +87,18 @@ export const isContainerRunning = async ({ return {err}; } }; + +export const inspectImage = async ({ + runner +}: Pick) => { + try { + await spawn({ + command: runner, + args: ['ps', '--quiet'], + silentOut: true + }); + } catch (_e: unknown) { + console.log(red(`It looks like ${runner} does not appear to be running.`)); + process.exit(1); + } +}; \ No newline at end of file From 935dcd34a40f000727a983e208eda16750c27fa4 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 15:16:33 +0100 Subject: [PATCH 2/3] feat: read emulator version Signed-off-by: David Dal Busco --- src/commands/version.ts | 30 ++++++++++++++++------- src/services/emulator/version.services.ts | 29 +++++++++++++++++++--- src/services/version.services.ts | 4 +-- src/utils/runner.utils.ts | 28 +++++++++++++++------ 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/commands/version.ts b/src/commands/version.ts index 26e49a90..78a71383 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -1,5 +1,5 @@ -import {isNullish} from '@dfinity/utils'; -import {red} from 'kleur'; +import {isEmptyString, isNullish} from '@dfinity/utils'; +import {green, red} from 'kleur'; import {clean} from 'semver'; import {version as cliCurrentVersion} from '../../package.json'; import { @@ -7,9 +7,9 @@ import { githubJunoDockerLastRelease, GithubLastReleaseResult } from '../rest/github.rest'; +import {findEmulatorVersion} from '../services/emulator/version.services'; import {checkVersion, CheckVersionResult} from '../services/version.services'; import {detectPackageManager} from '../utils/pm.utils'; -import {readEmulatorConfig} from '../configs/emulator.config'; export const version = async () => { const check = await cliVersion(); @@ -54,14 +54,14 @@ const installHint = (): string => { } }; -const emulatorVersion = async (): Promise => { - const parsedResult = await readEmulatorConfig(); +const emulatorVersion = async () => { + const emulatorResult = await findEmulatorVersion(); - if (!parsedResult.success) { - return {diff: "error"}; + if (emulatorResult.status !== 'success') { + return; } - + const {version: emulatorCurrentVersion} = emulatorResult; const result = await buildVersionFromGitHub({ release: 'Juno Docker', @@ -69,12 +69,24 @@ const emulatorVersion = async (): Promise => { }); if (result.result === 'error') { - return {diff: 'error'}; + return; } const {latestVersion} = result; + // Images prior to v0.6.3 lacked proper metadata in org.opencontainers.image.version. + // Earlier releases contained invalid values such as "0-arm64", while v0.6.2 returned an empty string. + // Note: sanitizing the version read via docker/podman inspect causes these cases to resolve to null. + if (isEmptyString(emulatorCurrentVersion)) { + console.log(`Your Emulator is behind the latest version (${green(`v${latestVersion}`)}).`); + return; + } + checkVersion({ + currentVersion: emulatorCurrentVersion, + latestVersion, + displayHint: 'Emulator' + }); }; const buildVersionFromGitHub = async ({ diff --git a/src/services/emulator/version.services.ts b/src/services/emulator/version.services.ts index 15ebc101..d3dce41a 100644 --- a/src/services/emulator/version.services.ts +++ b/src/services/emulator/version.services.ts @@ -1,11 +1,32 @@ +import {notEmptyString} from '@dfinity/utils'; +import {clean} from 'semver'; import {readEmulatorConfig} from '../../configs/emulator.config'; +import {inspectImageVersion} from '../../utils/runner.utils'; -export const findEmulatorVersion = async () => { +export const findEmulatorVersion = async (): Promise< + | {status: 'skipped'} + | {status: 'error'; err: unknown} + | {status: 'success'; version: string | undefined | null} +> => { const parsedResult = await readEmulatorConfig(); if (!parsedResult.success) { - return {diff: 'error'}; + return {status: 'skipped'}; } - const {config} = parsedResult; -} \ No newline at end of file + const { + config: {derivedConfig} + } = parsedResult; + + const inspectResult = await inspectImageVersion(derivedConfig); + + if ('err' in inspectResult) { + return {status: 'error', err: inspectResult.err}; + } + + const {version: versionText} = inspectResult; + + const version = notEmptyString(versionText) ? clean(versionText) : undefined; + + return {status: 'success', version}; +}; diff --git a/src/services/version.services.ts b/src/services/version.services.ts index a4affb83..3fb877ce 100644 --- a/src/services/version.services.ts +++ b/src/services/version.services.ts @@ -90,7 +90,7 @@ export const checkVersion = ({ currentVersion: string; latestVersion: string; displayHint: string; - commandLineHint: string; + commandLineHint?: string; }): CheckVersionResult => { const diff = compare(currentVersion, latestVersion); @@ -107,7 +107,7 @@ export const checkVersion = ({ console.log( `Your ${displayHint} (${yellow(`v${currentVersion}`)}) is behind the latest version (${green( `v${latestVersion}` - )}) available. Run ${cyan(commandLineHint)} to update it.` + )}).${nonNullish(commandLineHint) ? ` Run ${cyan(commandLineHint)} to update it.` : ''}` ); return {diff: 'outdated'}; diff --git a/src/utils/runner.utils.ts b/src/utils/runner.utils.ts index 6a7e5184..8f483c99 100644 --- a/src/utils/runner.utils.ts +++ b/src/utils/runner.utils.ts @@ -88,17 +88,29 @@ export const isContainerRunning = async ({ } }; -export const inspectImage = async ({ - runner -}: Pick) => { +export const inspectImageVersion = async ({ + runner, + image +}: Pick): Promise< + {version: string} | {err: unknown} +> => { try { + let output = ''; + await spawn({ command: runner, - args: ['ps', '--quiet'], + args: [ + 'inspect', + '--format', + '{{ index .Config.Labels "org.opencontainers.image.version"}}', + image + ], + stdout: (o) => (output += o), silentOut: true }); - } catch (_e: unknown) { - console.log(red(`It looks like ${runner} does not appear to be running.`)); - process.exit(1); + + return {version: output.trim()}; + } catch (err: unknown) { + return {err}; } -}; \ No newline at end of file +}; From 60712823427cb8bf18bed69c040a15732c87fc4e Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 16 Mar 2026 15:18:41 +0100 Subject: [PATCH 3/3] chore: lint Signed-off-by: David Dal Busco --- src/commands/version.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/version.ts b/src/commands/version.ts index 78a71383..c93725c3 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -5,10 +5,10 @@ import {version as cliCurrentVersion} from '../../package.json'; import { githubCliLastRelease, githubJunoDockerLastRelease, - GithubLastReleaseResult + type GithubLastReleaseResult } from '../rest/github.rest'; import {findEmulatorVersion} from '../services/emulator/version.services'; -import {checkVersion, CheckVersionResult} from '../services/version.services'; +import {checkVersion, type CheckVersionResult} from '../services/version.services'; import {detectPackageManager} from '../utils/pm.utils'; export const version = async () => {