import { existsSync } from 'node:fs'; import { mkdir, readFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import { resolveProjectWorkspace } from './workspace.js'; import { writeTextFileAtomic } from './atomic-write.js'; import type { AssetTagEntry } from './prompt-rules.js'; export interface EnvironmentAssetEntry { name: string; description: string; plateUrl: string; plateRef: string; } export interface EnvironmentAssetsArtifact { schemaVersion: 1; projectSlug: string; generatedAt: string; environments: EnvironmentAssetEntry[]; } export interface EnvironmentAssetsLookup { environments: EnvironmentAssetEntry[]; /** name (lowercased) -> AssetTagEntry, ready for buildAssetTagLookup's environmentsByName. */ environmentEntryByName: Map; } export function environmentAssetsPathFor(root: string, slug: string): string { return join(resolveProjectWorkspace(slug, root).projectDir, 'artifacts', 'environment-assets.json'); } export async function writeEnvironmentAssets( root: string, slug: string, artifact: EnvironmentAssetsArtifact, ): Promise { const path = environmentAssetsPathFor(root, slug); await mkdir(dirname(path), { recursive: true }); await writeTextFileAtomic(path, JSON.stringify(artifact, null, 2) + '\n'); } /** * Read `artifacts/environment-assets.json`. Returns an empty lookup when absent * (graceful — `@location` tags then fall through to strip-and-warn). The * `environmentEntryByName` map is the AssetTagEntry shape `buildAssetTagLookup` * consumes via `environmentsByName`. */ export async function readEnvironmentAssets(root: string, slug: string): Promise { const path = environmentAssetsPathFor(root, slug); if (!existsSync(path)) { return { environments: [], environmentEntryByName: new Map() }; } const parsed = JSON.parse(await readFile(path, 'utf-8')) as Partial; const environments = Array.isArray(parsed.environments) ? parsed.environments : []; const environmentEntryByName = new Map(); for (const env of environments) { if (!env?.name) continue; // Wire the HOSTED plate URL (public R2) as the reference — providers // (seedance-direct etc.) require a real HTTP/HTTPS URL or Asset:// URI and // reject the internal `gobananas://` plateRef. Fall back to plateRef only if // no hosted URL was captured. const referencePath = env.plateUrl || env.plateRef; environmentEntryByName.set(env.name.toLowerCase(), { descriptor: env.description ?? env.name, ...(referencePath ? { referencePath } : {}), }); } return { environments, environmentEntryByName }; }