From 35e5d67c8ae0253d7e77d7a039f8d1ce2e0a4140 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 30 Jul 2025 17:15:49 +0200 Subject: [PATCH 01/11] feat: prompt overwrite config --- package-lock.json | 29 +- package.json | 1 + src/cli/env.loader.ts | 3 +- src/commands/config.ts | 131 +-------- src/configs/cli.state.config.ts | 23 ++ src/services/config/config.services.ts | 322 +++++++++++++++++++++++ src/services/config/settings.services.ts | 81 ++++++ src/types/cli.env.ts | 2 + src/types/cli.state.ts | 21 ++ src/utils/obj.utils.ts | 5 + 10 files changed, 473 insertions(+), 145 deletions(-) create mode 100644 src/configs/cli.state.config.ts create mode 100644 src/services/config/config.services.ts create mode 100644 src/services/config/settings.services.ts create mode 100644 src/types/cli.state.ts create mode 100644 src/utils/obj.utils.ts diff --git a/package-lock.json b/package-lock.json index 26b9f4de..73e259d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@dfinity/ic-management": "^6.2.0", "@dfinity/identity": "^2.3.0", "@dfinity/principal": "^2.3.0", + "@dfinity/zod-schemas": "^1.0.0", "@junobuild/admin": "^0.6.8-next-2025-07-30.2", "@junobuild/cdn": "^0.2.1-next-2025-07-30.2", "@junobuild/cli-tools": "^0.3.1-next-2025-07-30.2", @@ -660,14 +661,13 @@ } }, "node_modules/@dfinity/zod-schemas": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@dfinity/zod-schemas/-/zod-schemas-2.0.0.tgz", - "integrity": "sha512-mvgiYCwGXgT+iFdvTFWh5Da0HCsF8VIFTIsY+uQifaf4duc3+K1nb16O7+tCzFD7Vs4ZmjImCNi+lO5GqjplNA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@dfinity/zod-schemas/-/zod-schemas-1.0.0.tgz", + "integrity": "sha512-5ApkpRO8hqTb7B9GH4H8FljY/r6hh3zpA/HFeeozIHieyebAzB748+4T9/oL6T7udkvlfWPMulbmjSHerm3B9A==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@dfinity/principal": "^2.0.0", - "zod": "^4" + "zod": "^3.25" } }, "node_modules/@esbuild/aix-ppc64": { @@ -7097,9 +7097,9 @@ } }, "node_modules/zod": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.14.tgz", - "integrity": "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "peer": true, "funding": { @@ -7503,10 +7503,9 @@ "requires": {} }, "@dfinity/zod-schemas": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@dfinity/zod-schemas/-/zod-schemas-2.0.0.tgz", - "integrity": "sha512-mvgiYCwGXgT+iFdvTFWh5Da0HCsF8VIFTIsY+uQifaf4duc3+K1nb16O7+tCzFD7Vs4ZmjImCNi+lO5GqjplNA==", - "peer": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@dfinity/zod-schemas/-/zod-schemas-1.0.0.tgz", + "integrity": "sha512-5ApkpRO8hqTb7B9GH4H8FljY/r6hh3zpA/HFeeozIHieyebAzB748+4T9/oL6T7udkvlfWPMulbmjSHerm3B9A==", "requires": {} }, "@esbuild/aix-ppc64": { @@ -11449,9 +11448,9 @@ "dev": true }, "zod": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.14.tgz", - "integrity": "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "peer": true } } diff --git a/package.json b/package.json index 220d9225..d28fb5bd 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@dfinity/ic-management": "^6.2.0", "@dfinity/identity": "^2.3.0", "@dfinity/principal": "^2.3.0", + "@dfinity/zod-schemas": "^1.0.0", "@junobuild/admin": "^0.6.8-next-2025-07-30.2", "@junobuild/cdn": "^0.2.1-next-2025-07-30.2", "@junobuild/cli-tools": "^0.3.1-next-2025-07-30.2", diff --git a/src/cli/env.loader.ts b/src/cli/env.loader.ts index 758ad8fa..f2e1a272 100644 --- a/src/cli/env.loader.ts +++ b/src/cli/env.loader.ts @@ -44,6 +44,7 @@ const loadEnvConfig = ({mode}: {mode: string | undefined}): JunoCliConfig => { return { projectName, - projectSettingsName: `${projectName}-cli-settings` + projectSettingsName: `${projectName}-cli-settings`, + projectStateName: `${projectName}-cli-state` }; }; diff --git a/src/commands/config.ts b/src/commands/config.ts index 21f1accb..bb7e64f8 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,132 +1,5 @@ -import {ICManagementCanister, LogVisibility} from '@dfinity/ic-management'; -import {Principal} from '@dfinity/principal'; -import {isNullish, nonNullish} from '@dfinity/utils'; -import { - type SatelliteParameters, - setAuthConfig, - setDatastoreConfig, - setStorageConfig -} from '@junobuild/admin'; -import type { - AuthenticationConfig, - DatastoreConfig, - ModuleSettings, - StorageConfig -} from '@junobuild/config'; -import {red} from 'kleur'; -import ora from 'ora'; -import {initAgent} from '../api/agent.api'; -import {assertConfigAndLoadSatelliteContext} from '../utils/satellite.utils'; - -type SetConfigResults = [ - PromiseSettledResult, - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - ...Array> -]; +import {config as configServices} from '../services/config/config.services'; export const config = async () => { - const {satellite, satelliteConfig} = await assertConfigAndLoadSatelliteContext(); - const {storage, authentication, datastore, settings} = satelliteConfig; - - const spinner = ora(`Configuring...`).start(); - - let results: SetConfigResults | undefined = undefined; - - try { - results = await Promise.allSettled([ - setStorageConfig({ - config: { - headers: storage?.headers ?? [], - rewrites: storage?.rewrites, - redirects: storage?.redirects, - iframe: storage?.iframe, - rawAccess: storage?.rawAccess, - maxMemorySize: storage?.maxMemorySize - }, - satellite - }), - ...(isNullish(datastore) - ? [] - : [ - setDatastoreConfig({ - config: datastore, - satellite - }) - ]), - ...(isNullish(authentication) - ? [] - : [ - setAuthConfig({ - config: authentication, - satellite - }) - ]), - ...(isNullish(settings) ? [] : [setSettings({settings, satellite})]) - ]); - } finally { - spinner.stop(); - } - - if (nonNullish(results)) { - printResults(results); - } -}; - -const printResults = (results: SetConfigResults) => { - const errors = results.filter((result) => result.status === 'rejected'); - - if (errors.length === 0) { - console.log('✅ Configuration applied.'); - return; - } - - console.log( - red(`The configuration failed with ${errors.length} error${errors.length > 1 ? 's' : ''} 😢.`) - ); - - errors.forEach((error, index) => { - console.log(`${index}:`, error.reason instanceof Error ? error.reason.message : error.reason); - }); -}; - -const setSettings = async ({ - settings, - satellite -}: { - settings: ModuleSettings; - satellite: Omit & - Required>; -}) => { - const { - freezingThreshold, - reservedCyclesLimit, - logVisibility, - heapMemoryLimit, - memoryAllocation, - computeAllocation - } = settings; - - const {satelliteId} = satellite; - - const agent = await initAgent(); - - const {updateSettings} = ICManagementCanister.create({ - agent - }); - - await updateSettings({ - canisterId: Principal.fromText(satelliteId), - settings: { - freezingThreshold, - reservedCyclesLimit, - logVisibility: isNullish(logVisibility) - ? undefined - : logVisibility === 'public' - ? LogVisibility.Public - : LogVisibility.Controllers, - wasmMemoryLimit: heapMemoryLimit, - memoryAllocation, - computeAllocation - } - }); + await configServices(); }; diff --git a/src/configs/cli.state.config.ts b/src/configs/cli.state.config.ts new file mode 100644 index 00000000..514727e7 --- /dev/null +++ b/src/configs/cli.state.config.ts @@ -0,0 +1,23 @@ +import {type PrincipalText} from '@dfinity/zod-schemas'; +import Conf from 'conf'; +import {ENV} from '../env'; +import {type CliState, type CliStateSatellite} from '../types/cli.state'; + +export const getStateConfig = (): Conf => + new Conf({projectName: ENV.config.projectStateName}); + +export const saveLastAppliedConfig = ({ + satelliteId, + lastAppliedConfig +}: {satelliteId: PrincipalText} & Pick) => { + const config = getStateConfig(); + + const satellites = config.get('satellites'); + + const updateSatellites = { + ...(satellites ?? {}), + [satelliteId]: {lastAppliedConfig} + }; + + config.set('satellites', updateSatellites); +}; diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts new file mode 100644 index 00000000..f9cc4b63 --- /dev/null +++ b/src/services/config/config.services.ts @@ -0,0 +1,322 @@ +import {isNullish, nonNullish} from '@dfinity/utils'; +import { + getAuthConfig, + getDatastoreConfig, + getStorageConfig, + setAuthConfig, + setDatastoreConfig, + setStorageConfig +} from '@junobuild/admin'; +import type { + AuthenticationConfig, + DatastoreConfig, + ModuleSettings, + SatelliteConfig, + StorageConfig +} from '@junobuild/config'; +import {red} from 'kleur'; +import ora from 'ora'; +import {getStateConfig, saveLastAppliedConfig} from '../../configs/cli.state.config'; +import { + type CliStateSatelliteAppliedConfigHashes, + type ConfigHash, + type SettingsHash +} from '../../types/cli.state'; +import type {SatelliteParametersWithId} from '../../types/satellite'; +import {objHash} from '../../utils/obj.utils'; +import {confirmAndExit} from '../../utils/prompt.utils'; +import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; +import {getSettings, setSettings} from './settings.services'; + +type SetConfigResults = [ + PromiseSettledResult, + PromiseSettledResult, + PromiseSettledResult, + PromiseSettledResult +]; + +export const config = async () => { + const {satellite, satelliteConfig} = await assertConfigAndLoadSatelliteContext(); + + const currentConfig = await loadCurrentConfig({satellite}); + const lastAppliedConfig = getLatestAppliedConfig({satellite}); + + const editConfig = await prepareConfig({ + currentConfig, + lastAppliedConfig, + satelliteConfig + }); + + const results = await applyConfig({satellite, editConfig}); + + await saveLastAppliedConfigHashes({ + results, + settings: editConfig?.settings, + satelliteId: satellite.satelliteId + }); + + printResults(results); +}; + +const saveLastAppliedConfigHashes = async ({ + results, + settings, + satelliteId +}: {results: SetConfigResults} & Pick & + Pick) => { + const [storage, datastore, auth, _] = results.filter((result) => result.status === 'fulfilled'); + + const lastAppliedConfig: CliStateSatelliteAppliedConfigHashes = { + ...(nonNullish(storage) && {storage: objHash(storage)}), + ...(nonNullish(datastore) && {datastore: objHash(datastore)}), + ...(nonNullish(auth) && {auth: objHash(auth)}), + ...(nonNullish(settings) && {settings: objHash(settings)}) + }; + + saveLastAppliedConfig({lastAppliedConfig, satelliteId}); +}; + +const printResults = (results: SetConfigResults) => { + const errors = results.filter((result) => result.status === 'rejected'); + + if (errors.length === 0) { + console.log('✅ Configuration applied.'); + return; + } + + console.log( + red(`The configuration failed with ${errors.length} error${errors.length > 1 ? 's' : ''} 😢.`) + ); + + errors.forEach((error, index) => { + console.log(`${index}:`, error.reason instanceof Error ? error.reason.message : error.reason); + }); +}; + +interface CurrentConfig { + storage: [StorageConfig, ConfigHash]; + datastore?: [DatastoreConfig, ConfigHash]; + auth?: [AuthenticationConfig, ConfigHash]; + settings: [ModuleSettings, SettingsHash]; +} + +const getCurrentConfig = async ({ + satellite +}: { + satellite: SatelliteParametersWithId; +}): Promise => { + const [storage, datastore, auth, settings] = await Promise.all([ + getStorageConfig({satellite}), + getDatastoreConfig({satellite}), + getAuthConfig({satellite}), + getSettings({satellite}) + ]); + + return { + storage: [storage, objHash(storage)], + ...(nonNullish(datastore) && {datastore: [datastore, objHash(datastore)]}), + ...(nonNullish(auth) && {auth: [auth, objHash(auth)]}), + settings: [settings, objHash(settings)] + }; +}; + +const getLatestAppliedConfig = ({ + satellite: {satelliteId} +}: { + satellite: SatelliteParametersWithId; +}): CliStateSatelliteAppliedConfigHashes | undefined => { + const satellites = getStateConfig().get('satellites'); + return satellites?.[satelliteId]?.lastAppliedConfig; +}; + +const loadCurrentConfig = async (params: { + satellite: SatelliteParametersWithId; +}): Promise => { + const spinner = ora('Loading...').start(); + + try { + return await getCurrentConfig(params); + } finally { + spinner.stop(); + } +}; + +const applyConfig = async ({ + satellite, + editConfig +}: { + satellite: SatelliteParametersWithId; + editConfig: Omit; +}): Promise => { + const spinner = ora('Configuring...').start(); + + try { + return await setConfigs({satellite, editConfig}); + } finally { + spinner.stop(); + } +}; + +const setConfigs = async ({ + satellite, + editConfig +}: { + satellite: SatelliteParametersWithId; + editConfig: Omit; +}): Promise => { + const {storage, authentication, datastore, settings} = editConfig; + + return await Promise.allSettled([ + setStorageConfig({ + config: { + headers: storage?.headers ?? [], + rewrites: storage?.rewrites, + redirects: storage?.redirects, + iframe: storage?.iframe, + rawAccess: storage?.rawAccess, + maxMemorySize: storage?.maxMemorySize + }, + satellite + }), + isNullish(datastore) + ? Promise.resolve() + : setDatastoreConfig({ + config: datastore, + satellite + }), + isNullish(authentication) + ? Promise.resolve() + : setAuthConfig({ + config: authentication, + satellite + }), + isNullish(settings) ? Promise.resolve() : setSettings({settings, satellite}) + ]); +}; + +const prepareConfig = async ({ + currentConfig, + lastAppliedConfig, + satelliteConfig +}: { + currentConfig: CurrentConfig; + lastAppliedConfig: CliStateSatelliteAppliedConfigHashes | undefined; + satelliteConfig: Omit; +}): Promise> => { + const { + storage: currentStorage, + datastore: currentDatastore, + auth: currentAuth, + settings: currentSettings + } = currentConfig; + + const isDefaultConfig = (): boolean => { + const [storage] = currentStorage; + + if (nonNullish(storage.version)) { + return false; + } + + const datastoreVersion = currentDatastore?.[0].version; + + if (nonNullish(datastoreVersion)) { + return false; + } + + const authVersion = currentAuth?.[0].version; + + if (nonNullish(authVersion)) { + return false; + } + + const [settings] = currentSettings; + return ( + isNullish(settings.computeAllocation) && + isNullish(settings.memoryAllocation) && + isNullish(settings.heapMemoryLimit) && + isNullish(settings.freezingThreshold) && + isNullish(settings.reservedCyclesLimit) && + isNullish(settings.logVisibility) + ); + }; + + const firstTime = isNullish(lastAppliedConfig); + + if (firstTime && isDefaultConfig()) { + return satelliteConfig; + } + + const {storage, datastore, authentication, settings} = satelliteConfig; + + // Extend the satellite config from the juno.config with the current versions available in the backend + // Unless the config contains manually defined versions. + const extendWithVersions = (): Omit => { + const [{version: versionStorage}] = currentStorage; + const versionDatastore = currentDatastore?.[0]?.version; + const versionAuth = currentAuth?.[0]?.version; + + return { + storage: + nonNullish(storage) && isNullish(storage.version) + ? {...storage, version: versionStorage} + : storage, + datastore: + nonNullish(datastore) && isNullish(datastore.version) + ? {...datastore, version: versionDatastore} + : datastore, + authentication: + nonNullish(authentication) && isNullish(authentication.version) + ? {...authentication, version: versionAuth} + : authentication, + settings + }; + }; + + const confirmAndExtendWithVersions = async (): Promise> => { + await confirmAndExit( + 'This action will overwrite the current configuration of the Satellite. Are you sure you want to continue?' + ); + + return extendWithVersions(); + }; + + if (firstTime) { + return await confirmAndExtendWithVersions(); + } + + const isLastAppliedConfigCurrent = (): boolean => { + const [_, storageHash] = currentStorage; + + const { + storage: lastStorageHash, + datastore: lastDatastoreHash, + auth: lastAuthHash, + settings: lastSettingsHash + } = lastAppliedConfig; + + if (storageHash !== lastStorageHash) { + return false; + } + + const datastoreHash = currentDatastore?.[1]; + + if (datastoreHash !== lastDatastoreHash) { + return false; + } + + const authHash = currentAuth?.[1]; + + if (authHash !== lastAuthHash) { + return false; + } + + const [__, settingsHash] = currentSettings; + return settingsHash === lastSettingsHash; + }; + + if (isLastAppliedConfigCurrent()) { + return extendWithVersions(); + } + + return await confirmAndExtendWithVersions(); +}; diff --git a/src/services/config/settings.services.ts b/src/services/config/settings.services.ts new file mode 100644 index 00000000..1c435bd7 --- /dev/null +++ b/src/services/config/settings.services.ts @@ -0,0 +1,81 @@ +import {ICManagementCanister, LogVisibility} from '@dfinity/ic-management'; +import {Principal} from '@dfinity/principal'; +import {isNullish} from '@dfinity/utils'; +import type {ModuleSettings} from '@junobuild/config'; +import {initAgent} from '../../api/agent.api'; +import type {SatelliteParametersWithId} from '../../types/satellite'; + +export const getSettings = async ({ + satellite +}: { + satellite: SatelliteParametersWithId; +}): Promise => { + const {satelliteId} = satellite; + + const agent = await initAgent(); + + const {canisterStatus} = ICManagementCanister.create({ + agent + }); + + const { + settings: { + freezing_threshold: freezingThreshold, + reserved_cycles_limit: reservedCyclesLimit, + wasm_memory_limit: heapMemoryLimit, + memory_allocation: memoryAllocation, + compute_allocation: computeAllocation, + log_visibility + } + } = await canisterStatus(Principal.fromText(satelliteId)); + + return { + freezingThreshold, + reservedCyclesLimit, + logVisibility: 'public' in log_visibility ? 'public' : 'controllers', + heapMemoryLimit, + memoryAllocation, + computeAllocation + }; +}; + +export const setSettings = async ({ + settings, + satellite +}: { + settings: ModuleSettings; + satellite: SatelliteParametersWithId; +}) => { + const { + freezingThreshold, + reservedCyclesLimit, + logVisibility, + heapMemoryLimit, + memoryAllocation, + computeAllocation + } = settings; + + const {satelliteId} = satellite; + + const agent = await initAgent(); + + const {updateSettings} = ICManagementCanister.create({ + agent + }); + + await updateSettings({ + canisterId: Principal.fromText(satelliteId), + settings: { + freezingThreshold, + reservedCyclesLimit, + logVisibility: isNullish(logVisibility) + ? undefined + : logVisibility === 'public' + ? LogVisibility.Public + : LogVisibility.Controllers, + wasmMemoryLimit: heapMemoryLimit, + memoryAllocation, + computeAllocation + } + }); +}; diff --git a/src/types/cli.env.ts b/src/types/cli.env.ts index 7988c1fa..c2338c1d 100644 --- a/src/types/cli.env.ts +++ b/src/types/cli.env.ts @@ -22,4 +22,6 @@ export interface JunoCliConfig { projectName: string | 'juno'; // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents projectSettingsName: string | 'juno-cli-settings'; + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + projectStateName: string | 'juno-cli-state'; } diff --git a/src/types/cli.state.ts b/src/types/cli.state.ts new file mode 100644 index 00000000..4610c0b3 --- /dev/null +++ b/src/types/cli.state.ts @@ -0,0 +1,21 @@ +import {type PrincipalText} from '@dfinity/zod-schemas'; + +export type ConfigHash = string; +export type SettingsHash = ConfigHash; + +export interface CliStateSatelliteAppliedConfigHashes { + storage?: ConfigHash; + datastore?: ConfigHash; + auth?: ConfigHash; + settings?: SettingsHash; +} + +export interface CliStateSatellite { + lastAppliedConfig: CliStateSatelliteAppliedConfigHashes; +} + +export type CliStateSatellites = Record; + +export interface CliState { + satellites: CliStateSatellites; +} diff --git a/src/utils/obj.utils.ts b/src/utils/obj.utils.ts new file mode 100644 index 00000000..1c743c56 --- /dev/null +++ b/src/utils/obj.utils.ts @@ -0,0 +1,5 @@ +import {jsonReplacer} from '@dfinity/utils'; +import {createHash} from 'node:crypto'; + +export const objHash = (obj: T): string => + createHash('sha256').update(JSON.stringify(obj, jsonReplacer)).digest('hex'); From 6f90d0258181860a790931e7147ef332fecbaebd Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 30 Jul 2025 17:21:59 +0200 Subject: [PATCH 02/11] chore: lint --- src/configs/cli.state.config.ts | 13 ++++++++++++- src/services/config/config.services.ts | 24 +++++++++--------------- src/utils/obj.utils.ts | 2 +- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/configs/cli.state.config.ts b/src/configs/cli.state.config.ts index 514727e7..9d004d2b 100644 --- a/src/configs/cli.state.config.ts +++ b/src/configs/cli.state.config.ts @@ -1,11 +1,22 @@ import {type PrincipalText} from '@dfinity/zod-schemas'; import Conf from 'conf'; import {ENV} from '../env'; -import {type CliState, type CliStateSatellite} from '../types/cli.state'; +import { + type CliState, + type CliStateSatellite, + type CliStateSatelliteAppliedConfigHashes +} from '../types/cli.state'; export const getStateConfig = (): Conf => new Conf({projectName: ENV.config.projectStateName}); +export const getLatestAppliedConfig = ({ + satelliteId +}: { + satelliteId: PrincipalText; +}): CliStateSatelliteAppliedConfigHashes | undefined => + getStateConfig().get('satellites')?.[satelliteId]?.lastAppliedConfig; + export const saveLastAppliedConfig = ({ satelliteId, lastAppliedConfig diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index f9cc4b63..2b0a9680 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -16,7 +16,7 @@ import type { } from '@junobuild/config'; import {red} from 'kleur'; import ora from 'ora'; -import {getStateConfig, saveLastAppliedConfig} from '../../configs/cli.state.config'; +import {getLatestAppliedConfig, saveLastAppliedConfig} from '../../configs/cli.state.config'; import { type CliStateSatelliteAppliedConfigHashes, type ConfigHash, @@ -30,16 +30,19 @@ import {getSettings, setSettings} from './settings.services'; type SetConfigResults = [ PromiseSettledResult, + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type PromiseSettledResult, + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type PromiseSettledResult, PromiseSettledResult ]; export const config = async () => { const {satellite, satelliteConfig} = await assertConfigAndLoadSatelliteContext(); + const {satelliteId} = satellite; const currentConfig = await loadCurrentConfig({satellite}); - const lastAppliedConfig = getLatestAppliedConfig({satellite}); + const lastAppliedConfig = getLatestAppliedConfig({satelliteId}); const editConfig = await prepareConfig({ currentConfig, @@ -49,16 +52,16 @@ export const config = async () => { const results = await applyConfig({satellite, editConfig}); - await saveLastAppliedConfigHashes({ + saveLastAppliedConfigHashes({ results, - settings: editConfig?.settings, - satelliteId: satellite.satelliteId + settings: editConfig.settings, + satelliteId }); printResults(results); }; -const saveLastAppliedConfigHashes = async ({ +const saveLastAppliedConfigHashes = ({ results, settings, satelliteId @@ -120,15 +123,6 @@ const getCurrentConfig = async ({ }; }; -const getLatestAppliedConfig = ({ - satellite: {satelliteId} -}: { - satellite: SatelliteParametersWithId; -}): CliStateSatelliteAppliedConfigHashes | undefined => { - const satellites = getStateConfig().get('satellites'); - return satellites?.[satelliteId]?.lastAppliedConfig; -}; - const loadCurrentConfig = async (params: { satellite: SatelliteParametersWithId; }): Promise => { diff --git a/src/utils/obj.utils.ts b/src/utils/obj.utils.ts index 1c743c56..65ca2349 100644 --- a/src/utils/obj.utils.ts +++ b/src/utils/obj.utils.ts @@ -1,5 +1,5 @@ import {jsonReplacer} from '@dfinity/utils'; import {createHash} from 'node:crypto'; -export const objHash = (obj: T): string => +export const objHash = (obj: unknown): string => createHash('sha256').update(JSON.stringify(obj, jsonReplacer)).digest('hex'); From f129d6b0aaf83dffd11b24ceb7fdf46e3c84bba4 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 30 Jul 2025 17:45:11 +0200 Subject: [PATCH 03/11] feat: default settings --- src/constants/settings.constants.ts | 7 +++++++ src/services/config/config.services.ts | 22 ++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 src/constants/settings.constants.ts diff --git a/src/constants/settings.constants.ts b/src/constants/settings.constants.ts new file mode 100644 index 00000000..c951c3d0 --- /dev/null +++ b/src/constants/settings.constants.ts @@ -0,0 +1,7 @@ +export const DEFAULT_SATELLITE_HEAP_WASM_MEMORY_LIMIT = 1_073_741_824n; +export const DEFAULT_SATELLITE_FREEZING_THRESHOLD = 31_104_000n; + +export const DEFAULT_RESERVED_CYCLES_LIMIT = 5_000_000_000_000n; +export const DEFAULT_LOG_VISIBILITY = "controllers"; +export const DEFAULT_MEMORY_ALLOCATION = 0n; +export const DEFAULT_COMPUTE_ALLOCATION = 0n; diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index 2b0a9680..062f30cc 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -17,6 +17,14 @@ import type { import {red} from 'kleur'; import ora from 'ora'; import {getLatestAppliedConfig, saveLastAppliedConfig} from '../../configs/cli.state.config'; +import { + DEFAULT_COMPUTE_ALLOCATION, + DEFAULT_LOG_VISIBILITY, + DEFAULT_MEMORY_ALLOCATION, + DEFAULT_RESERVED_CYCLES_LIMIT, + DEFAULT_SATELLITE_FREEZING_THRESHOLD, + DEFAULT_SATELLITE_HEAP_WASM_MEMORY_LIMIT +} from '../../constants/settings.constants'; import { type CliStateSatelliteAppliedConfigHashes, type ConfigHash, @@ -225,12 +233,12 @@ const prepareConfig = async ({ const [settings] = currentSettings; return ( - isNullish(settings.computeAllocation) && - isNullish(settings.memoryAllocation) && - isNullish(settings.heapMemoryLimit) && - isNullish(settings.freezingThreshold) && - isNullish(settings.reservedCyclesLimit) && - isNullish(settings.logVisibility) + settings.computeAllocation === DEFAULT_COMPUTE_ALLOCATION && + settings.memoryAllocation === DEFAULT_MEMORY_ALLOCATION && + settings.heapMemoryLimit === DEFAULT_SATELLITE_HEAP_WASM_MEMORY_LIMIT && + settings.freezingThreshold === DEFAULT_SATELLITE_FREEZING_THRESHOLD && + settings.reservedCyclesLimit === DEFAULT_RESERVED_CYCLES_LIMIT && + settings.logVisibility === DEFAULT_LOG_VISIBILITY ); }; @@ -274,6 +282,8 @@ const prepareConfig = async ({ return extendWithVersions(); }; + console.log(firstTime, currentConfig, lastAppliedConfig); + if (firstTime) { return await confirmAndExtendWithVersions(); } From 441c937b0b60d4efe65cf5d06c4ac0756dca75b7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 30 Jul 2025 17:47:28 +0200 Subject: [PATCH 04/11] fix: indexes --- src/services/config/config.services.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index 062f30cc..2f91ce3f 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -75,7 +75,14 @@ const saveLastAppliedConfigHashes = ({ satelliteId }: {results: SetConfigResults} & Pick & Pick) => { - const [storage, datastore, auth, _] = results.filter((result) => result.status === 'fulfilled'); + const fulfilledValue = ( + index: number + ): void | StorageConfig | DatastoreConfig | AuthenticationConfig | undefined => + results[index].status === 'fulfilled' ? results[index].value : undefined; + + const storage = fulfilledValue(0); + const datastore = fulfilledValue(1); + const auth = fulfilledValue(2); const lastAppliedConfig: CliStateSatelliteAppliedConfigHashes = { ...(nonNullish(storage) && {storage: objHash(storage)}), From bd4967aa54ee17674b60b4f8adba489bc65714f4 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 30 Jul 2025 17:52:24 +0200 Subject: [PATCH 05/11] feat: save undefined --- src/configs/cli.state.config.ts | 13 +++++++++++-- src/types/cli.state.ts | 8 ++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/configs/cli.state.config.ts b/src/configs/cli.state.config.ts index 9d004d2b..1d470fb1 100644 --- a/src/configs/cli.state.config.ts +++ b/src/configs/cli.state.config.ts @@ -19,15 +19,24 @@ export const getLatestAppliedConfig = ({ export const saveLastAppliedConfig = ({ satelliteId, - lastAppliedConfig + lastAppliedConfig: {storage, datastore, auth, settings} }: {satelliteId: PrincipalText} & Pick) => { const config = getStateConfig(); const satellites = config.get('satellites'); + const lastAppliedConfig = satellites?.[satelliteId]?.lastAppliedConfig; + const updateSatellites = { ...(satellites ?? {}), - [satelliteId]: {lastAppliedConfig} + [satelliteId]: { + lastAppliedConfig: { + storage: storage ?? lastAppliedConfig?.storage, + datastore: datastore ?? lastAppliedConfig?.datastore, + auth: auth ?? lastAppliedConfig?.auth, + settings: settings ?? lastAppliedConfig?.settings + } + } }; config.set('satellites', updateSatellites); diff --git a/src/types/cli.state.ts b/src/types/cli.state.ts index 4610c0b3..12b10ce3 100644 --- a/src/types/cli.state.ts +++ b/src/types/cli.state.ts @@ -4,10 +4,10 @@ export type ConfigHash = string; export type SettingsHash = ConfigHash; export interface CliStateSatelliteAppliedConfigHashes { - storage?: ConfigHash; - datastore?: ConfigHash; - auth?: ConfigHash; - settings?: SettingsHash; + storage: ConfigHash | undefined; + datastore: ConfigHash | undefined; + auth: ConfigHash | undefined; + settings: SettingsHash | undefined; } export interface CliStateSatellite { From 50174e94e3e4ad2ca8efc7b26a92880bdf74c9aa Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 30 Jul 2025 17:55:05 +0200 Subject: [PATCH 06/11] feat: save undefined --- src/services/config/config.services.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index 2f91ce3f..c248f2a8 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -85,10 +85,10 @@ const saveLastAppliedConfigHashes = ({ const auth = fulfilledValue(2); const lastAppliedConfig: CliStateSatelliteAppliedConfigHashes = { - ...(nonNullish(storage) && {storage: objHash(storage)}), - ...(nonNullish(datastore) && {datastore: objHash(datastore)}), - ...(nonNullish(auth) && {auth: objHash(auth)}), - ...(nonNullish(settings) && {settings: objHash(settings)}) + storage: nonNullish(storage) ? objHash(storage) : undefined, + datastore: nonNullish(datastore) ? objHash(datastore) : undefined, + auth: nonNullish(auth) ? objHash(auth) : undefined, + settings: nonNullish(settings) ? objHash(settings) : undefined }; saveLastAppliedConfig({lastAppliedConfig, satelliteId}); From 516123f46864cbb7a72f1c50e9edc842a8d6a0d2 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 30 Jul 2025 19:29:04 +0200 Subject: [PATCH 07/11] feat: debug in progress --- src/configs/cli.state.config.ts | 12 +++++++++-- src/services/config/config.services.ts | 29 +++++++++++++++----------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/configs/cli.state.config.ts b/src/configs/cli.state.config.ts index 1d470fb1..be70090c 100644 --- a/src/configs/cli.state.config.ts +++ b/src/configs/cli.state.config.ts @@ -14,8 +14,10 @@ export const getLatestAppliedConfig = ({ satelliteId }: { satelliteId: PrincipalText; -}): CliStateSatelliteAppliedConfigHashes | undefined => - getStateConfig().get('satellites')?.[satelliteId]?.lastAppliedConfig; +}): CliStateSatelliteAppliedConfigHashes | undefined => { + console.log('____>', getStateConfig().get('satellites')); + return getStateConfig().get('satellites')?.[satelliteId]?.lastAppliedConfig; +}; export const saveLastAppliedConfig = ({ satelliteId, @@ -23,8 +25,12 @@ export const saveLastAppliedConfig = ({ }: {satelliteId: PrincipalText} & Pick) => { const config = getStateConfig(); + console.log('1.', config); + const satellites = config.get('satellites'); + console.log('2.', satellites); + const lastAppliedConfig = satellites?.[satelliteId]?.lastAppliedConfig; const updateSatellites = { @@ -39,5 +45,7 @@ export const saveLastAppliedConfig = ({ } }; + console.log('///>', satelliteId, updateSatellites); + config.set('satellites', updateSatellites); }; diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index c248f2a8..1ea69f07 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -37,7 +37,8 @@ import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; import {getSettings, setSettings} from './settings.services'; type SetConfigResults = [ - PromiseSettledResult, + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + PromiseSettledResult, // eslint-disable-next-line @typescript-eslint/no-invalid-void-type PromiseSettledResult, // eslint-disable-next-line @typescript-eslint/no-invalid-void-type @@ -91,6 +92,8 @@ const saveLastAppliedConfigHashes = ({ settings: nonNullish(settings) ? objHash(settings) : undefined }; + console.log(lastAppliedConfig, results); + saveLastAppliedConfig({lastAppliedConfig, satelliteId}); }; @@ -176,17 +179,15 @@ const setConfigs = async ({ const {storage, authentication, datastore, settings} = editConfig; return await Promise.allSettled([ - setStorageConfig({ - config: { - headers: storage?.headers ?? [], - rewrites: storage?.rewrites, - redirects: storage?.redirects, - iframe: storage?.iframe, - rawAccess: storage?.rawAccess, - maxMemorySize: storage?.maxMemorySize - }, - satellite - }), + isNullish(storage) + ? Promise.resolve() + : setStorageConfig({ + config: { + ...storage, + headers: storage.headers ?? [] + }, + satellite + }), isNullish(datastore) ? Promise.resolve() : setDatastoreConfig({ @@ -219,6 +220,8 @@ const prepareConfig = async ({ settings: currentSettings } = currentConfig; + console.log('---->', lastAppliedConfig); + const isDefaultConfig = (): boolean => { const [storage] = currentStorage; @@ -264,6 +267,8 @@ const prepareConfig = async ({ const versionDatastore = currentDatastore?.[0]?.version; const versionAuth = currentAuth?.[0]?.version; + console.log('===', storage); + return { storage: nonNullish(storage) && isNullish(storage.version) From 3a785a7a4c0f59a13093005e50c93ef43549dafc Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 31 Jul 2025 09:50:00 +0200 Subject: [PATCH 08/11] feat: last applied settings --- src/services/config/config.services.ts | 31 ++++++++++++-------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index 1ea69f07..1d4d1a78 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -92,8 +92,6 @@ const saveLastAppliedConfigHashes = ({ settings: nonNullish(settings) ? objHash(settings) : undefined }; - console.log(lastAppliedConfig, results); - saveLastAppliedConfig({lastAppliedConfig, satelliteId}); }; @@ -220,7 +218,17 @@ const prepareConfig = async ({ settings: currentSettings } = currentConfig; - console.log('---->', lastAppliedConfig); + const isDefaultSettings = (): boolean => { + const [settings] = currentSettings; + return ( + settings.computeAllocation === DEFAULT_COMPUTE_ALLOCATION && + settings.memoryAllocation === DEFAULT_MEMORY_ALLOCATION && + settings.heapMemoryLimit === DEFAULT_SATELLITE_HEAP_WASM_MEMORY_LIMIT && + settings.freezingThreshold === DEFAULT_SATELLITE_FREEZING_THRESHOLD && + settings.reservedCyclesLimit === DEFAULT_RESERVED_CYCLES_LIMIT && + settings.logVisibility === DEFAULT_LOG_VISIBILITY + ); + }; const isDefaultConfig = (): boolean => { const [storage] = currentStorage; @@ -241,15 +249,7 @@ const prepareConfig = async ({ return false; } - const [settings] = currentSettings; - return ( - settings.computeAllocation === DEFAULT_COMPUTE_ALLOCATION && - settings.memoryAllocation === DEFAULT_MEMORY_ALLOCATION && - settings.heapMemoryLimit === DEFAULT_SATELLITE_HEAP_WASM_MEMORY_LIMIT && - settings.freezingThreshold === DEFAULT_SATELLITE_FREEZING_THRESHOLD && - settings.reservedCyclesLimit === DEFAULT_RESERVED_CYCLES_LIMIT && - settings.logVisibility === DEFAULT_LOG_VISIBILITY - ); + return isDefaultSettings(); }; const firstTime = isNullish(lastAppliedConfig); @@ -267,8 +267,6 @@ const prepareConfig = async ({ const versionDatastore = currentDatastore?.[0]?.version; const versionAuth = currentAuth?.[0]?.version; - console.log('===', storage); - return { storage: nonNullish(storage) && isNullish(storage.version) @@ -294,8 +292,6 @@ const prepareConfig = async ({ return extendWithVersions(); }; - console.log(firstTime, currentConfig, lastAppliedConfig); - if (firstTime) { return await confirmAndExtendWithVersions(); } @@ -327,7 +323,8 @@ const prepareConfig = async ({ } const [__, settingsHash] = currentSettings; - return settingsHash === lastSettingsHash; + + return settingsHash === lastSettingsHash || (isDefaultSettings() && isNullish(settings)); }; if (isLastAppliedConfigCurrent()) { From 39fef293631f0da9104a26b4bbc660e9b2e1d1c7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 31 Jul 2025 12:11:48 +0200 Subject: [PATCH 09/11] chore: lint --- eslint.config.mjs | 3 ++- src/configs/cli.state.config.ts | 12 ++---------- src/constants/settings.constants.ts | 2 +- src/services/config/config.services.ts | 3 --- src/types/cli.state.ts | 2 +- 5 files changed, 6 insertions(+), 16 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 413fa0d8..1d2ecafa 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -55,7 +55,8 @@ export default [ caughtErrorsIgnorePattern: '^_' } ], - 'eslint-comments/require-description': 'off' + 'eslint-comments/require-description': 'off', + '@typescript-eslint/no-invalid-void-type': 'off' } } ]; diff --git a/src/configs/cli.state.config.ts b/src/configs/cli.state.config.ts index be70090c..1d470fb1 100644 --- a/src/configs/cli.state.config.ts +++ b/src/configs/cli.state.config.ts @@ -14,10 +14,8 @@ export const getLatestAppliedConfig = ({ satelliteId }: { satelliteId: PrincipalText; -}): CliStateSatelliteAppliedConfigHashes | undefined => { - console.log('____>', getStateConfig().get('satellites')); - return getStateConfig().get('satellites')?.[satelliteId]?.lastAppliedConfig; -}; +}): CliStateSatelliteAppliedConfigHashes | undefined => + getStateConfig().get('satellites')?.[satelliteId]?.lastAppliedConfig; export const saveLastAppliedConfig = ({ satelliteId, @@ -25,12 +23,8 @@ export const saveLastAppliedConfig = ({ }: {satelliteId: PrincipalText} & Pick) => { const config = getStateConfig(); - console.log('1.', config); - const satellites = config.get('satellites'); - console.log('2.', satellites); - const lastAppliedConfig = satellites?.[satelliteId]?.lastAppliedConfig; const updateSatellites = { @@ -45,7 +39,5 @@ export const saveLastAppliedConfig = ({ } }; - console.log('///>', satelliteId, updateSatellites); - config.set('satellites', updateSatellites); }; diff --git a/src/constants/settings.constants.ts b/src/constants/settings.constants.ts index c951c3d0..33ff3ce3 100644 --- a/src/constants/settings.constants.ts +++ b/src/constants/settings.constants.ts @@ -2,6 +2,6 @@ export const DEFAULT_SATELLITE_HEAP_WASM_MEMORY_LIMIT = 1_073_741_824n; export const DEFAULT_SATELLITE_FREEZING_THRESHOLD = 31_104_000n; export const DEFAULT_RESERVED_CYCLES_LIMIT = 5_000_000_000_000n; -export const DEFAULT_LOG_VISIBILITY = "controllers"; +export const DEFAULT_LOG_VISIBILITY = 'controllers'; export const DEFAULT_MEMORY_ALLOCATION = 0n; export const DEFAULT_COMPUTE_ALLOCATION = 0n; diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index 1d4d1a78..f02babe1 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -37,11 +37,8 @@ import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; import {getSettings, setSettings} from './settings.services'; type SetConfigResults = [ - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type PromiseSettledResult, - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type PromiseSettledResult, - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type PromiseSettledResult, PromiseSettledResult ]; diff --git a/src/types/cli.state.ts b/src/types/cli.state.ts index 12b10ce3..bf17c829 100644 --- a/src/types/cli.state.ts +++ b/src/types/cli.state.ts @@ -17,5 +17,5 @@ export interface CliStateSatellite { export type CliStateSatellites = Record; export interface CliState { - satellites: CliStateSatellites; + satellites?: CliStateSatellites; } From 68b8f7190feb59fb3449bf56baae00a229c1b171 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 31 Jul 2025 12:15:29 +0200 Subject: [PATCH 10/11] docs: add comment --- src/services/config/config.services.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index f02babe1..93b40308 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -251,6 +251,9 @@ const prepareConfig = async ({ const firstTime = isNullish(lastAppliedConfig); + // If the developer runs `juno config` for the first time using the default configuration, + // there's no need to show a warning about overwriting an existing config - it's the first time + // they want to configure something. if (firstTime && isDefaultConfig()) { return satelliteConfig; } From a2c394c5b8f42b9b44ea6e3dff035a989edc3e1a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 31 Jul 2025 12:17:10 +0200 Subject: [PATCH 11/11] docs: add comment --- src/services/config/config.services.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/config/config.services.ts b/src/services/config/config.services.ts index 93b40308..403d9aa6 100644 --- a/src/services/config/config.services.ts +++ b/src/services/config/config.services.ts @@ -296,6 +296,10 @@ const prepareConfig = async ({ return await confirmAndExtendWithVersions(); } + // Checks whether the last applied config (hashes stored in the CLI state file) + // matches the current Satellite configuration. + // If they match, there's no need to warn the developer about overwriting — + // they're just updating the same options they previously applied. const isLastAppliedConfigCurrent = (): boolean => { const [_, storageHash] = currentStorage;