/*
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}`,
};
}