import * as fs from 'fs' import * as yaml from 'js-yaml' // Interface for a single OCI image configuration export interface OCIImageConfig { tag: string // Current tag sourceDir?: string // Directory in the container (optional if using manifest) targetDir?: string // Directory on the host (optional if using manifest) } // Interface for the whole zzup.yaml configuration file export interface zzupConfig { [imageName: string]: OCIImageConfig | string // Can be an object or just a tag string } // Normalized zzupConfig where all values are OCIImageConfig objects export interface NormalizedzzupConfig { [imageName: string]: OCIImageConfig } // Possible config file paths export const CONFIG_PATHS = ['zzup.yaml', 'zup.yaml'] as const /** * Migrate from zup.yaml to zzup.yaml if needed * @returns The path of the active config file */ export function migrateConfigIfNeeded(): string { if (fs.existsSync('zup.yaml') && !fs.existsSync('zzup.yaml')) { console.log('Migrating from zup.yaml to zzup.yaml...') fs.renameSync('zup.yaml', 'zzup.yaml') return 'zzup.yaml' } return findConfigPath() || 'zzup.yaml' } /** * Find the active config file path * @returns The path of the active config file or null if none exists */ export function findConfigPath(): string | null { const existingFiles = CONFIG_PATHS.filter(path => fs.existsSync(path)) if (existingFiles.length === 0) { return null } if (existingFiles.length > 1) { throw new Error( `Multiple configuration files found: ${existingFiles.join(', ')}. Please use only one of them.` ) } return existingFiles[0] } /** * Load the zzup configuration from file * @param configPath Path to the configuration file * @returns The loaded configuration */ export function loadConfig(configPath?: string): NormalizedzzupConfig { try { const activePath = configPath || findConfigPath() if (!activePath) { return {} } const fileContents = fs.readFileSync(activePath, 'utf8') const rawConfig = yaml.load(fileContents) as any // If the old format is detected (images property), convert it if (rawConfig && 'images' in rawConfig) { const oldConfig = rawConfig as { images: { [key: string]: OCIImageConfig | string } } return normalizeConfig(oldConfig.images) } return normalizeConfig(rawConfig || {}) } catch (error) { console.error(`Error loading configuration: ${error}`) return {} } } /** * Normalize the config to ensure all values are OCIImageConfig objects * @param config Raw config that might contain string values * @returns Normalized config where all values are OCIImageConfig objects */ function normalizeConfig(config: { [key: string]: OCIImageConfig | string }): NormalizedzzupConfig { const normalizedConfig: NormalizedzzupConfig = {} Object.entries(config).forEach(([imageName, value]) => { if (typeof value === 'string') { // Convert shorthand format (just tag) to full format normalizedConfig[imageName] = { tag: value } } else if (value && typeof value === 'object') { // Already in object format normalizedConfig[imageName] = value } else { // Skip invalid entries console.warn(`Invalid configuration for image ${imageName}, skipping`) } }) return normalizedConfig } /** * Save the zzup configuration to file * @param config Configuration to save * @param configPath Path to save the configuration to */ export function saveConfig( config: NormalizedzzupConfig, configPath: string = CONFIG_PATHS[0] ): void { try { // Convert all entries to shorthand format if possible const simplifiedConfig: { [key: string]: OCIImageConfig | string } = {} Object.entries(config).forEach(([imageName, value]) => { // If only tag is present, use the shorthand format if (Object.keys(value).length === 1 && 'tag' in value) { simplifiedConfig[imageName] = value.tag } else { simplifiedConfig[imageName] = value } }) const yamlStr = yaml.dump(simplifiedConfig, { indent: 2, lineWidth: 120, }) fs.writeFileSync(configPath, yamlStr, 'utf8') } catch (error) { console.error(`Error saving configuration to ${configPath}: ${error}`) throw error } } /** * Get an image configuration by name * @param config zzupConfig object * @param imageName Name of the image to find * @returns The image configuration if found, or undefined */ export function getImageConfig( config: NormalizedzzupConfig, imageName: string ): (OCIImageConfig & { name: string }) | undefined { if (config[imageName]) { return { name: imageName, ...config[imageName], } } return undefined } /** * Update an existing image configuration or add a new one * @param config zzupConfig object * @param imageConfig Image configuration to update * @returns Updated zzupConfig */ export function updateImageConfig( config: NormalizedzzupConfig, imageConfig: OCIImageConfig & { name: string } ): NormalizedzzupConfig { const newConfig = { ...config } const { name, ...configWithoutName } = imageConfig newConfig[imageConfig.name] = configWithoutName return newConfig } /** * Initialize a new config file * @param configPath Path to create the config file * @returns Created zzupConfig */ export function initConfig( configPath: string = CONFIG_PATHS[0] ): NormalizedzzupConfig { const config: NormalizedzzupConfig = {} saveConfig(config, configPath) return config }