import { execSync } from 'child_process' import * as fs from 'fs' import * as path from 'path' import { extractManifest, getManifestProperties, validateManifest, } from './manifest' /** * Check if Docker is available on the system * @returns true if Docker is available, false otherwise */ export function isDockerAvailable(): boolean { try { execSync('docker --version', { stdio: 'pipe' }) return true } catch (error) { return false } } /** * Process a single OCI image - pull and extract contents * @param imageName Name of the OCI image * @param tag Tag for the image * @param sourceDir Optional source directory override (if not specified, will use manifest) * @param targetDir Optional target directory override (if not specified, will use manifest) */ export async function processImage( imageName: string, tag: string, sourceDir?: string, targetDir?: string ) { console.log(`๐Ÿ”„ Processing image ${imageName}:${tag}...`) // Check if Docker is available if (!isDockerAvailable()) { console.error('โŒ Docker is not available or not installed.') console.error( 'Please make sure Docker is installed and running on your system.' ) throw new Error('Docker is not available') } try { // Pull the image console.log(`๐Ÿ”„ Pulling image ${imageName}:${tag}...`) execSync(`docker pull ${imageName}:${tag}`, { stdio: 'inherit' }) console.log(`โœ… Successfully pulled image ${imageName}:${tag}`) // Extract and validate the manifest const manifest = await extractManifest(imageName, tag) if (!manifest) { throw new Error( `No .manifest.json found in image ${imageName}:${tag}. Image is not zzup compatible.` ) } // Validate the manifest before proceeding if (!validateManifest(manifest)) { // The validateManifest function now logs detailed errors to the console, // so we just need to throw a more informative error that includes: // 1. Which image had the problem // 2. A hint that more detailed info was already logged throw new Error( `Invalid .manifest.json in image ${imageName}:${tag}. Check the error messages above for details on required fields and format.` ) } // Get the source and target directories from the manifest or from the provided values const { sourceDir: resolvedSourceDir, targetDir: resolvedTargetDir } = getManifestProperties(manifest, imageName, sourceDir, targetDir) if (!resolvedSourceDir) { throw new Error( `No source directory specified in manifest or parameters for image ${imageName}:${tag}` ) } // Display manifest information if available console.log(`๐Ÿ“„ Manifest information:`) console.log(` Name: ${manifest.name}`) if (manifest.description) console.log(` Description: ${manifest.description}`) console.log(` Source directory: ${resolvedSourceDir}`) console.log(` Target directory: ${resolvedTargetDir}`) if (manifest.author) console.log(` Author: ${manifest.author}`) if (manifest.homepage) console.log(` Homepage: ${manifest.homepage}`) // Normalize source directory path (ensure it ends with slash) const normalizedSourceDir = resolvedSourceDir.endsWith('/') ? resolvedSourceDir : `${resolvedSourceDir}/` // Make sure the target directory exists const absoluteTargetDir = path.resolve(resolvedTargetDir) if (!fs.existsSync(absoluteTargetDir)) { fs.mkdirSync(absoluteTargetDir, { recursive: true }) console.log(`Created target directory: ${absoluteTargetDir}`) } // Create a temporary container (with a dummy entrypoint since it might be a scratch image) console.log(`๐Ÿ”„ Creating temporary container...`) const containerIdBuffer = execSync( `docker create --entrypoint "true" ${imageName}:${tag}` ) const containerId = containerIdBuffer.toString().trim() console.log(`โœ… Created container with ID: ${containerId}`) try { // Check if source directory exists by listing files in the export console.log( `๐Ÿ”„ Checking files in the ${normalizedSourceDir} directory...` ) let hasSourceDir = false try { const checkCmd = `docker export "${containerId}" | tar t ${normalizedSourceDir} 2>/dev/null` execSync(checkCmd, { stdio: 'pipe' }) hasSourceDir = true } catch (error) { console.error( `โŒ Source directory '${normalizedSourceDir}' does not exist in the container.` ) console.log(`Available directories in the container root:`) try { const listCmd = `docker export "${containerId}" | tar t | grep -v '/$' | head -20` const rootContents = execSync(listCmd, { encoding: 'utf8' }) console.log(rootContents) } catch (e) { console.log('Could not list container contents.') } throw new Error( `Source directory '${normalizedSourceDir}' not found in container` ) } if (hasSourceDir) { // Remove existing directory if it exists and create a fresh one if ( fs.existsSync(absoluteTargetDir) && fs.readdirSync(absoluteTargetDir).length > 0 ) { console.log(`๐Ÿ”„ Removing existing content in ${absoluteTargetDir}...`) fs.rmSync(absoluteTargetDir, { recursive: true, force: true }) fs.mkdirSync(absoluteTargetDir, { recursive: true }) } // Extract files from the container using export and tar console.log( `๐Ÿ”„ Extracting ${normalizedSourceDir} from container to ${absoluteTargetDir}...` ) // Calculate the strip components level based on the source directory path const stripComponents = normalizedSourceDir .split('/') .filter(Boolean).length const exportCmd = `docker export "${containerId}" | tar x --strip-components=${stripComponents} -C "${absoluteTargetDir}" ${normalizedSourceDir}` execSync(exportCmd, { stdio: 'inherit' }) // Verify the extraction worked const fileCount = fs.readdirSync(absoluteTargetDir).length console.log(`โœ… Extracted ${fileCount} items to ${absoluteTargetDir}`) } } catch (error: any) { const errorMessage = error instanceof Error ? error.message : String(error) console.error(`Error: ${errorMessage}`) throw error } finally { // Clean up the container try { console.log(`๐Ÿงน Cleaning up temporary container...`) execSync(`docker rm "${containerId}"`, { stdio: 'pipe' }) console.log(`โœ… Container removed`) } catch (error) { console.error(`Warning: Failed to remove container: ${error}`) } } console.log(`โœ… OCI Image processed successfully!`) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) console.error(`[Error] ${errorMessage}`) throw error } } /** * Validates that an image exists in the configuration * @param imageName Name of the image to check * @param availableImages Array of available image names * @returns true if valid, false if invalid */ export function validateImageExists( imageName: string, availableImages: string[] ): boolean { return availableImages.includes(imageName) }