/** * Generic artifact materializer — the single writer all file-shaped artifact * features route through. Given the resolved contract for an engine, it either * mirrors folders/files into a native directory (pruning only Clopen-managed * slugs) or rewrites a marker-delimited preamble block in a shared memory file. * * This replaces the per-feature sync copies: Skills, Commands and Subagents all * call {@link materializeArtifacts} with their own item set and (optionally) a * custom preamble builder, so behavior stays identical across features. */ import { join } from 'path'; import { mkdir, readdir, readFile, rm, writeFile, cp, stat } from 'node:fs/promises'; import { resolveArtifact } from './matrix'; import { markersForType, writeManagedBlock } from './markers'; import type { ArtifactContext, ArtifactType, ManagedArtifact } from './types'; async function pathExists(path: string): Promise { try { await stat(path); return true; } catch { return false; } } /** * Write only when the bytes differ. * * This runs at every stream start, and a no-op rewrite still moves mtime. The * Open Code pool fingerprints these files to decide whether its baked config * changed, so an unconditional write made every single turn spawn a fresh * `opencode serve` process for a config that was byte-identical. */ async function writeFileIfChanged(filePath: string, content: string): Promise { try { if ((await readFile(filePath, 'utf8')) === content) return; } catch { /* missing or unreadable — fall through and write */ } await writeFile(filePath, content, 'utf8'); } /** Every file under `dir`, keyed by path relative to it. Missing dir → empty map. */ async function fileStats(dir: string): Promise> { const out = new Map(); const walk = async (current: string, prefix: string): Promise => { let entries; try { entries = await readdir(current, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const relative = prefix ? `${prefix}/${entry.name}` : entry.name; if (entry.isDirectory()) { await walk(join(current, entry.name), relative); continue; } try { const info = await stat(join(current, entry.name)); out.set(relative, { size: info.size, mtimeMs: info.mtimeMs }); } catch { /* raced away */ } } }; await walk(dir, ''); return out; } /** True when `destDir` already holds a current copy — same files, same sizes, none of them older than the source. */ async function folderInSync(sourceDir: string, destDir: string): Promise { const [source, dest] = await Promise.all([fileStats(sourceDir), fileStats(destDir)]); if (source.size === 0 || source.size !== dest.size) return false; for (const [relative, sourceInfo] of source) { const destInfo = dest.get(relative); if (!destInfo || destInfo.size !== sourceInfo.size || destInfo.mtimeMs < sourceInfo.mtimeMs) return false; } return true; } /** Mirror one canonical folder into a destination dir under `/`, skipping the copy when it is already current. */ async function mirrorFolder(sourceDir: string, destDir: string, slug: string): Promise { if (!(await pathExists(sourceDir))) return; const dest = join(destDir, slug); if (await folderInSync(sourceDir, dest)) return; await rm(dest, { recursive: true, force: true }); await mkdir(destDir, { recursive: true }); await cp(sourceDir, dest, { recursive: true }); } /** Default synthetic preamble — a generic "these artifacts exist" block. */ function defaultPreamble(type: ArtifactType, items: ManagedArtifact[]): string { if (items.length === 0) return ''; const label = type === 'command' ? 'Commands' : type === 'subagent' ? 'Subagents' : 'Items'; const lines = [`# Available ${label}`, '']; for (const item of items) lines.push(`- **${item.name}** — ${item.description}`); return lines.join('\n'); } export interface MaterializeInput { /** Items that should exist after the sync (enabled set). */ enabled: ManagedArtifact[]; /** Every slug this feature manages — used to prune safely without touching user files. */ managedSlugs: string[]; /** Optional custom synthetic preamble (Skills keeps its exact original text). */ buildPreamble?: (items: ManagedArtifact[]) => string; } /** * Materialize a feature's artifacts for one engine. Safe to call at every stream * start: native dirs are reconciled and synthetic blocks are rewritten in place. */ export async function materializeArtifacts( // `permission` is excluded alongside `mcp`/`instruction`: it has no managed // block id because its enforcement is a runtime hook, and its optional on-disk // file is written by `backend/permissions/materialize.ts`, not here. type: Exclude, ctx: ArtifactContext, input: MaterializeInput ): Promise { const resolution = resolveArtifact(type, ctx); if (!resolution.supported) return; const target = resolution.locateEffective(ctx); if (!target) return; const enabledSlugs = new Set(input.enabled.map(i => i.slug)); const managedSlugs = new Set(input.managedSlugs); if (resolution.format === 'folder-md' || resolution.format === 'single-md') { await mkdir(target, { recursive: true }); // Prune stale copies. In an EXCLUSIVE (Clopen-owned isolated) dir we remove // anything not currently enabled — this is what reclaims artifacts whose DB // row was deleted (their slug is gone from managedSlugs). In a SHARED dir we // only prune known managed slugs, never the user's own files. for (const entry of await readdir(target, { withFileTypes: true })) { if (entry.name.startsWith('.')) continue; const slug = resolution.format === 'single-md' ? entry.name.replace(/\.md$/, '') : entry.name; const stale = resolution.exclusive ? !enabledSlugs.has(slug) : managedSlugs.has(slug) && !enabledSlugs.has(slug); if (stale) { await rm(join(target, entry.name), { recursive: true, force: true }); } } for (const item of input.enabled) { if (resolution.format === 'folder-md') { if (item.sourceDir) await mirrorFolder(item.sourceDir, target, item.slug); } else if (item.document != null) { await writeFileIfChanged(join(target, `${item.slug}.md`), item.document); } } // A type that is NATIVE for this engine must not ALSO leave a stale synthetic // managed block in the memory file. Engines that USED to be synthetic for a // type before a native dir was added (OpenCode subagents/commands, Codex // commands) still carry an orphaned `CLOPEN:` block in AGENTS.md that // the native sync never touches — so a deleted/renamed artifact would linger // there forever (e.g. a removed subagent still "available"). Strip it here; // writeManagedBlock is a no-op when no such block exists. const staleMemoryFile = resolveArtifact('instruction', ctx).locateEffective(ctx); if (staleMemoryFile) { await writeManagedBlock(staleMemoryFile, '', markersForType(type)); } return; } // preamble-region → managed block inside the engine memory file. const build = input.buildPreamble ?? ((items) => defaultPreamble(type, items)); await writeManagedBlock(target, build(input.enabled), markersForType(type)); }