import { assertPublicFileTarget } from '../fleet/private-config.js'; import { db } from '../db/index.js'; import { blobSize, getFileBlob } from '../db/file-blobs.js'; import type { LaunchRecipePlan } from '../fleet/launch-recipes.js'; import type { StagedFileChange } from '../fleet/staged-files.js'; /** Build the replacement with the exact requested additions, updates and removals. */ export function applyActivationFiles( recipe: LaunchRecipePlan, operationId: string, includeContent = false ): void { const changes = db() .prepare( `SELECT f.*, ${blobSize('f.content_hash')} AS size, v.version, v.release_version FROM staged_file_changes f JOIN provisioning_sources s ON s.kind=f.source_kind AND s.source_id=f.source_id AND s.server=f.server LEFT JOIN script_versions v ON v.id=f.script_version_id WHERE s.operation_id=? AND f.state='pending' ORDER BY f.id` ) .all(operationId) as (StagedFileChange & { content_hash: string | null; size: number; version: number | null; release_version: string | null; })[]; for (const change of changes) { assertPublicFileTarget(change.server, change.kind, change.rel_path); if (change.kind !== 'script' && change.kind !== 'config' && change.kind !== 'plugin') throw new Error('File activation requires a runtime file kind.'); recipe.files = recipe.files.filter( (file) => file.kind !== change.kind || file.path !== change.rel_path ); if (!change.content_hash) continue; recipe.files.push({ kind: change.kind, name: change.rel_path, path: change.rel_path, versionId: change.script_version_id, version: change.version, releaseVersion: change.release_version, sha256: change.content_hash, size: change.size, ...(includeContent ? { content: getFileBlob(change.content_hash)!.toString('base64') } : {}) }); } }