import { execSync } from 'child_process' import * as fs from 'fs' import * as path from 'path' /** * Interface for zzup manifest structure */ export interface zzupManifest { schema: string name: string description?: string sourceDir: string targetDir?: string author?: string homepage?: string dependencies?: Record options?: Record } /** * Default manifest path within the image */ export const MANIFEST_PATH = '/.manifest.json' /** * Extract manifest from an image * @param imageName Image name * @param tag Tag * @returns The manifest object or null if not found */ export async function extractManifest( imageName: string, tag: string ): Promise { // Create a temporary container console.log(`๐Ÿ”„ Creating temporary container to extract manifest...`) try { // Create a container without running it (using a non-destructive command) const containerIdBuffer = await new Promise((resolve, reject) => { try { // Use execSync for simpler error handling const buffer = execSync( `docker create --entrypoint "/bin/sh" ${imageName}:${tag} -c "exit 0"`, { stdio: ['ignore', 'pipe', 'pipe'], } ) resolve(buffer) } catch (error) { console.error(`Failed to create container: ${error}`) reject(error) } }).catch(error => { console.error(`Error creating container: ${error}`) return null }) if (!containerIdBuffer) { console.error(`Could not create container for ${imageName}:${tag}`) return null } const containerId = containerIdBuffer.toString().trim() console.log(`โœ… Created temporary container: ${containerId}`) // Create a temporary directory for the extracted manifest const tempDir = fs.mkdtempSync( path.join(fs.realpathSync(path.join(process.cwd())), 'zzup-manifest-') ) const manifestDestPath = path.join(tempDir, 'manifest.json') try { // Extract the manifest file from the container try { execSync( `docker cp ${containerId}:${MANIFEST_PATH} ${manifestDestPath}`, { stdio: ['ignore', 'pipe', 'pipe'], } ) console.log(`โœ… Copied manifest from container`) } catch (error) { // If the file doesn't exist, this is not a fatal error console.log(`No manifest found in container at ${MANIFEST_PATH}`) } // Check if the manifest was successfully extracted let manifest: zzupManifest | null = null if (fs.existsSync(manifestDestPath)) { try { const manifestContent = fs.readFileSync(manifestDestPath, 'utf8') manifest = JSON.parse(manifestContent) as zzupManifest console.log(`โœ… Found manifest in image ${imageName}:${tag}`) } catch (error) { console.error(`Parsing manifest: ${error}`) } } else { console.warn(`No manifest found in image ${imageName}:${tag}`) } return manifest } finally { // Clean up the temporary container console.log(`๐Ÿงน Cleaning up temporary container...`) try { execSync(`docker rm ${containerId}`, { stdio: 'pipe' }) console.log(`โœ… Container removed`) } catch (error) { console.warn(`Warning: Failed to remove container: ${error}`) } // Clean up the temporary directory try { if (fs.existsSync(manifestDestPath)) { fs.unlinkSync(manifestDestPath) } fs.rmdirSync(tempDir) } catch (error) { console.warn(`Error cleaning up temporary files: ${error}`) } } } catch (error) { console.error(`Extracting manifest: ${error}`) return null } } /** * Validate a zzup manifest * @param manifest Manifest to validate * @returns True if the manifest is valid, false otherwise */ export function validateManifest(manifest: zzupManifest): boolean { // Check for required fields const missingFields = [] if (!manifest.schema) { missingFields.push('schema') } if (!manifest.name) { missingFields.push('name') } if (!manifest.sourceDir) { missingFields.push('sourceDir') } if (missingFields.length > 0) { console.error( `Invalid manifest: missing required fields: ${missingFields.join(', ')}` ) console.error( `Required manifest format should include: schema, name, and sourceDir` ) console.error(`Example valid manifest: { "schema": "1.0", "name": "example-tool", "sourceDir": "/opt/content/", "targetDir": "example-tool" }`) return false } // Check if the schema is supported const supportedSchemas = ['1.0'] if (!supportedSchemas.includes(manifest.schema)) { console.error(`Unsupported manifest schema: ${manifest.schema}`) console.error(`Supported schemas are: ${supportedSchemas.join(', ')}`) return false } return true } /** * Get manifest properties or fallback to provided values * @param manifest Manifest object * @param imageName Image name (used as fallback for targetDir) * @param sourceDir Source directory (fallback if not in manifest) * @param targetDir Target directory (fallback if not in manifest) * @returns Object with resolved sourceDir and targetDir */ export function getManifestProperties( manifest: zzupManifest | null, imageName: string, sourceDir?: string, targetDir?: string ): { sourceDir: string; targetDir: string } { // If we have a valid manifest, use its values as defaults let resolvedSourceDir = '' let resolvedTargetDir = '' if (manifest) { // Use manifest sourceDir if no override provided resolvedSourceDir = sourceDir || manifest.sourceDir // Use manifest targetDir if no override provided resolvedTargetDir = targetDir || manifest.targetDir || imageName.split('/').pop() || '' } else { // No manifest, use provided values or defaults if (!sourceDir) { throw new Error( `No sourceDir provided and no manifest found in image ${imageName}` ) } resolvedSourceDir = sourceDir resolvedTargetDir = targetDir || imageName.split('/').pop() || '' } return { sourceDir: resolvedSourceDir, targetDir: resolvedTargetDir, } }