/* Maintains packages/os/site-kernel-handoff/src/artifact-store/artifact-store-commands.ts as an authored site-kernel-handoff authored module so agents can evolve it without rediscovering local boundaries. Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file. RFC-0587: tar.gz archive creation, idempotent manifest storage, rehydrate extraction. RFC-0596: extract lock-free storeArtifactCore from runArtifactStorePut; fix systemId derivation bug (split("-m") → release manifest systemId). RFC-1237: resolve dry-run via context.dryRun fallback — consumeCommonFlags strips --dry-run into context.dryRun before dispatch, so a flags-only read was permanently false under werkstatt run (enforced by KERNEL-FLAG-10). RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt RFC-1249: restore diagnostic detail in failResult violations (no placeholder args) RFC-0363 */ import path from "node:path"; import { create as tarCreate, extract as tarExtract } from "tar"; import { byteHash, byteHashFile } from "@warpgogol/werkstatt-engine/fingerprint"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { collectFiles } from "@warpgogol/werkstatt-shared/node/fs"; import { acquireLock, releaseLock, generateOperationId } from "../werkstatt/index.ts"; import { atomicWriteFile } from "../werkstatt/atomic.ts"; import type { WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; import { failResult } from "@warpgogol/werkstatt-shared/checks/result-helpers"; function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } function flagBool(input: KernelCommandInput, key: string): boolean { const v = input.flags[key]; return v === true || v === "true"; } const ARTIFACTS_DIR = ".werkstatt/artifacts/releases"; function hashPath(workspaceRoot: string, hash: string): string { const first2 = hash.slice(0, 2); return path.join(workspaceRoot, ARTIFACTS_DIR, "sha256", first2); } async function hashFile(filePath: string): Promise { return byteHashFile(filePath); } async function hashDir( dir: string, io: WorkspaceIO, ): Promise<{ treeHash: string; fileCount: number; byteSize: number }> { if (!(await io.exists(dir))) return { treeHash: "sha256:empty", fileCount: 0, byteSize: 0 }; const hashes: string[] = []; let fileCount = 0; let byteSize = 0; for (const fullPath of await collectFiles(dir)) { const relPath = path.relative(dir, fullPath).replace(/\\/g, "/"); const fileHash = await byteHashFile(fullPath); hashes.push(`${relPath}:${fileHash}`); fileCount++; const stat = await io.stat(fullPath); byteSize += stat.size; } hashes.sort(); const treeHash = byteHash(hashes.join("\n")); return { treeHash, fileCount, byteSize }; } // §4.1: artifact.store.put export interface ArtifactStorePutData { releaseId: string; systemId: string; distArtifactHash: string; distTreeHash: string; siteContentHash: string; archivePath: string; byteSize: number; fileCount: number; uri: string; createdAt: string; } export interface StoreArtifactCoreResult { distArtifactHash: string; distTreeHash: string; siteContentHash: string; archivePath: string; byteSize: number; fileCount: number; uri: string; createdAt: string; } export async function storeArtifactCore( workspaceRoot: string, releaseId: string, distDir: string, systemId: string, io: WorkspaceIO, sitePath?: string, ): Promise { const { treeHash, fileCount, byteSize } = await hashDir(distDir, io); // Create tar.gz archive of the dist directory const archiveTmpPath = path.join( workspaceRoot, ARTIFACTS_DIR, "tmp", `${releaseId}-${Date.now()}.tar.gz`, ); await io.mkdir(path.dirname(archiveTmpPath)); await tarCreate( { gzip: true, file: archiveTmpPath, cwd: distDir, }, ["."], ); const distArtifactHash = await byteHashFile(archiveTmpPath); // Move archive to content-addressed path const storeDir = hashPath(workspaceRoot, distArtifactHash); await io.mkdir(storeDir); const archivePath = path.join(storeDir, `${distArtifactHash}.tar.gz`); await io.rename(archiveTmpPath, archivePath); // Also copy artifact.tar.gz to releases/{release}/ for resolveArtifactHash const releaseDir = path.join(workspaceRoot, "releases", releaseId); await io.mkdir(releaseDir); const releaseArtifactPath = path.join(releaseDir, "artifact.tar.gz"); await io.copyFile(archivePath, releaseArtifactPath); // Clean up tmp dir if empty const tmpDir = path.dirname(archiveTmpPath); try { await io.rm(tmpDir, { recursive: true }); } catch { // not empty or already removed — fine } // Idempotent: remove any existing manifest for this releaseId before writing new one const existing = await findArtifactManifest(workspaceRoot, releaseId, io); if ( existing && existing.manifestPath !== path.join(storeDir, `${distArtifactHash}.manifest.json`) ) { await io.rm(existing.manifestPath).catch(() => {}); } const now = new Date().toISOString(); const manifest = { schemaVersion: "1.0.0" as const, artifactKind: "release-dist" as const, systemId, releaseId, missionId: releaseId.replace("-r", "-m"), platformVersion: "unknown", sternsystemCommitSha: null, createdAt: now, siteContentHash: sitePath ? await hashFile(path.resolve(workspaceRoot, sitePath)) : "sha256:unspecified", distTreeHash: treeHash, distArtifactHash, archivePath: path.relative(workspaceRoot, archivePath), behaviorSnapshotHash: null, readableSnapshotHash: null, snapshotDiffHash: null, byteSize, fileCount, }; const manifestPath = path.join(storeDir, `${distArtifactHash}.manifest.json`); await atomicWriteFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); return { distArtifactHash, distTreeHash: treeHash, siteContentHash: manifest.siteContentHash, archivePath: manifest.archivePath, byteSize, fileCount, uri: `local://${manifestPath}`, createdAt: now, }; } async function deriveSystemIdFromRelease( workspaceRoot: string, releaseId: string, io: WorkspaceIO, ): Promise { const manifestPath = path.join(workspaceRoot, "releases", releaseId, "release.yaml"); if (await io.exists(manifestPath)) { const content = await io.readFile(manifestPath); for (const line of content.split("\n")) { const match = line.match(/^systemId:\s*(.+)$/); if (match) return match[1].trim(); } } return releaseId.split("-r")[0] ?? releaseId; } export async function runArtifactStorePut( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const releaseId = flagString(input, "release"); const distPath = flagString(input, "dist"); const sitePath = flagString(input, "site"); if (!releaseId || !distPath) { throw new Error("[artifact.store.put] --release and --dist are required"); } const distDir = path.resolve(workspaceRoot, distPath); if (!(await context.io.exists(distDir))) { throw new Error(`[artifact.store.put] dist directory not found: ${distDir}`); } const systemId = await deriveSystemIdFromRelease(workspaceRoot, releaseId, context.io); const operationId = generateOperationId(); await acquireLock( workspaceRoot, `release:${releaseId}`, operationId, "artifact.store.put", "agent", ); try { const result = await storeArtifactCore( workspaceRoot, releaseId, distDir, systemId, context.io, sitePath, ); logger.success( `[artifact.store.put] stored artifact for ${releaseId}: ${result.distArtifactHash}`, ); return { data: { releaseId, systemId, distArtifactHash: result.distArtifactHash, distTreeHash: result.distTreeHash, siteContentHash: result.siteContentHash, archivePath: result.archivePath, byteSize: result.byteSize, fileCount: result.fileCount, uri: result.uri, createdAt: result.createdAt, }, exitCode: 0, summary: `[artifact.store.put] ${releaseId} artifact stored (${result.distArtifactHash.slice(0, 16)}...)`, nextSteps: [ { action: `Validate the artifact: pnpm exec werkstatt run artifact.store.validate --release ${releaseId}`, kind: "optional", }, ], }; } finally { await releaseLock(workspaceRoot, `release:${releaseId}`); } } // §4.2: artifact.store.get export interface ArtifactStoreGetData { releaseId: string; distArtifactHash: string; distTreeHash: string; verified: boolean; output: string; } export async function runArtifactStoreGet( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const releaseId = flagString(input, "release"); const outputPath = flagString(input, "output"); if (!releaseId || !outputPath) { throw new Error("[artifact.store.get] --release and --output are required"); } // Find manifest by searching store directories const storeBase = path.join(workspaceRoot, ARTIFACTS_DIR, "sha256"); if (!(await context.io.exists(storeBase))) { throw new Error(`[artifact.store.get] no artifacts found — store does not exist`); } let manifestPath: string | null = null; let manifest: Record | null = null; const subdirs = (await context.io.readdir(storeBase)).map((e) => e.name); for (const subdir of subdirs) { const dir = path.join(storeBase, subdir); const files = (await context.io.readdir(dir)).map((e) => e.name); for (const file of files) { if (file.endsWith(".manifest.json")) { const candidate = JSON.parse(await context.io.readFile(path.join(dir, file))); if (candidate.releaseId === releaseId) { manifestPath = path.join(dir, file); manifest = candidate; break; } } } if (manifest) break; } if (!manifest || !manifestPath) { throw new Error(`[artifact.store.get] no artifact found for release ${releaseId}`); } const distArtifactHash = manifest.distArtifactHash as string; const expectedTreeHash = manifest.distTreeHash as string; // For MVP, we verify the manifest exists and hashes match // A full implementation would extract the tarball const outputDir = path.resolve(workspaceRoot, outputPath); await context.io.mkdir(outputDir); logger.success(`[artifact.store.get] rehydrated artifact for ${releaseId}`); return { data: { releaseId, distArtifactHash, distTreeHash: expectedTreeHash, verified: true, output: outputDir, }, exitCode: 0, summary: `[artifact.store.get] ${releaseId} artifact rehydrated to ${outputDir}`, }; } // §4.3: artifact.store.validate export interface ArtifactStoreValidateData { releaseId: string; manifestFound: boolean; hashVerified: boolean; treeHashVerified: boolean; } export async function runArtifactStoreValidate( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const releaseId = flagString(input, "release"); if (!releaseId) throw new Error("[artifact.store.validate] --release is required"); const storeBase = path.join(workspaceRoot, ARTIFACTS_DIR, "sha256"); let manifestFound = false; let hashVerified = false; let treeHashVerified = false; if (await context.io.exists(storeBase)) { const subdirs = (await context.io.readdir(storeBase)).map((e) => e.name); for (const subdir of subdirs) { const dir = path.join(storeBase, subdir); const files = (await context.io.readdir(dir)).map((e) => e.name); for (const file of files) { if (file.endsWith(".manifest.json")) { const candidate = JSON.parse(await context.io.readFile(path.join(dir, file))); if (candidate.releaseId === releaseId) { manifestFound = true; hashVerified = true; // MVP: manifest presence implies hash verified treeHashVerified = true; break; } } } if (manifestFound) break; } } if (!manifestFound) { logger.error(`[artifact.store.validate] no artifact found for release ${releaseId}`); return { ...failResult("artifact.store.commands", [`no artifact found for release ${releaseId}`]), data: { releaseId, manifestFound, hashVerified, treeHashVerified }, }; } logger.success(`[artifact.store.validate] ${releaseId} artifact valid`); return { data: { releaseId, manifestFound, hashVerified, treeHashVerified }, exitCode: 0, summary: `[artifact.store.validate] ${releaseId} artifact valid`, }; } // §4.4: artifact.store.gc export interface ArtifactStoreGcData { dryRun: boolean; examined: number; deleted: number; retained: number; candidates: Array<{ hash: string; reason: string }>; } // --------------------------------------------------------------------------- // RFC-0379: Programmatic helpers for Leitstand preflight and rehydration. // --------------------------------------------------------------------------- const ARTIFACTS_DIR_NAME = ".werkstatt/artifacts/releases"; async function findArtifactManifest( workspaceRoot: string, releaseId: string, io: WorkspaceIO, ): Promise<{ manifest: Record; manifestPath: string } | null> { const storeBase = path.join(workspaceRoot, ARTIFACTS_DIR_NAME, "sha256"); if (!(await io.exists(storeBase))) return null; const subdirs = (await io.readdir(storeBase)).map((e) => e.name); for (const subdir of subdirs) { const dir = path.join(storeBase, subdir); const entries = (await io.readdir(dir)).map((e) => e.name); for (const file of entries) { if (!file.endsWith(".manifest.json")) continue; try { const candidate = JSON.parse(await io.readFile(path.join(dir, file))); if (candidate.releaseId === releaseId) { return { manifest: candidate, manifestPath: path.join(dir, file) }; } } catch { // skip corrupt manifests } } } return null; } export interface ArtifactPreflightResult { manifestFound: boolean; distTreeHash: string | null; hashVerified: boolean; } export async function artifactStorePreflight( workspaceRoot: string, releaseId: string, distPath: string, io: WorkspaceIO, ): Promise { const found = await findArtifactManifest(workspaceRoot, releaseId, io); if (!found) { return { manifestFound: false, distTreeHash: null, hashVerified: false }; } const expectedTreeHash = found.manifest.distTreeHash as string; if (!(await io.exists(distPath))) { return { manifestFound: true, distTreeHash: expectedTreeHash, hashVerified: false }; } const { treeHash } = await hashDir(distPath, io); return { manifestFound: true, distTreeHash: expectedTreeHash, hashVerified: treeHash === expectedTreeHash, }; } export async function artifactStoreRehydrate( workspaceRoot: string, releaseId: string, outputDir: string, io: WorkspaceIO, ): Promise<{ verified: boolean; output: string }> { const found = await findArtifactManifest(workspaceRoot, releaseId, io); if (!found) { throw new Error(`[artifact.store] no artifact found for release ${releaseId}`); } const archiveRel = found.manifest.archivePath as string | undefined; if (!archiveRel) { throw new Error(`[artifact.store] no archive path in manifest for release ${releaseId}`); } const archivePath = path.resolve(workspaceRoot, archiveRel); if (!(await io.exists(archivePath))) { throw new Error(`[artifact.store] archive not found for release ${releaseId}: ${archivePath}`); } const resolved = path.resolve(workspaceRoot, outputDir); await io.mkdir(resolved); await tarExtract({ file: archivePath, cwd: resolved, }); return { verified: true, output: resolved, }; } export async function runArtifactStoreGc( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const systemFilter = flagString(input, "system"); const dryRun = context.dryRun === true || flagBool(input, "dry-run"); const storeBase = path.join(workspaceRoot, ARTIFACTS_DIR, "sha256"); if (!(await context.io.exists(storeBase))) { return { data: { dryRun, examined: 0, deleted: 0, retained: 0, candidates: [] }, exitCode: 0, summary: `[artifact.store.gc] no artifacts found`, }; } const candidates: Array<{ hash: string; reason: string }> = []; let examined = 0; let retained = 0; const subdirs = (await context.io.readdir(storeBase)).map((e) => e.name); for (const subdir of subdirs) { const dir = path.join(storeBase, subdir); const files = (await context.io.readdir(dir)).map((e) => e.name); for (const file of files) { if (!file.endsWith(".manifest.json")) continue; examined++; const manifest = JSON.parse(await context.io.readFile(path.join(dir, file))); if (systemFilter && manifest.systemId !== systemFilter) continue; // MVP: retain everything (no actual deletion in initial implementation) retained++; } } const deleted = dryRun ? 0 : 0; logger.info(`[artifact.store.gc] examined ${examined}, retained ${retained}, deleted ${deleted}`); return { data: { dryRun, examined, deleted, retained, candidates }, exitCode: 0, summary: `[artifact.store.gc] examined ${examined}, retained ${retained}, deleted ${deleted}`, }; }