@peripheries/engine 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine.mjs +2 -2
- package/package.json +1 -1
- package/src/commands/build.mjs +2 -2
- package/src/commands/check-inline-assets.mjs +5 -0
- package/src/commands/upload-bundle.mjs +1 -1
- package/src/index.d.mts +5 -0
- package/src/lib/assets.mjs +3 -0
- package/src/lib/config.mjs +1 -1
- package/src/lib/run.mjs +3 -1
- package/src/template/cli.mjs +1 -1
package/bin/engine.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{COMMANDS,runCommand}from"../src/lib/run.mjs";const[command,...args]=process.argv.slice(2);if(!command||command==="--help"||command==="-h"||!(command in COMMANDS)){const known=Object.keys(COMMANDS).join(", ");if(command&&!(command in COMMANDS)&&command!=="--help"&&command!=="-h")console.error(`[engine] unknown command "${command}"`);console.log(`usage: engine <command> [args]
|
|
2
|
+
import{COMMANDS,commandUsage,runCommand}from"../src/lib/run.mjs";const[command,...args]=process.argv.slice(2);if(!command||command==="--help"||command==="-h"||!(command in COMMANDS)){const known=Object.keys(COMMANDS).join(", ");if(command&&!(command in COMMANDS)&&command!=="--help"&&command!=="-h")console.error(`[engine] unknown command "${command}"`);console.log(`usage: engine <command> [args]
|
|
3
3
|
commands: ${known}
|
|
4
4
|
|
|
5
|
-
engine deploy [--develop|--prod] [--lineage <name>] [--skip-build]`);process.exit(command&&!(command in COMMANDS)&&command!=="--help"&&command!=="-h"?2:0)}process.exit(runCommand(command,args));
|
|
5
|
+
engine deploy [--develop|--prod] [--lineage <name>] [--skip-build]`);process.exit(command&&!(command in COMMANDS)&&command!=="--help"&&command!=="-h"?2:0)}if(command!=="template"&&(args.includes("--help")||args.includes("-h"))){console.log(commandUsage(command));process.exit(0)}process.exit(runCommand(command,args));
|
package/package.json
CHANGED
package/src/commands/build.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawnSync}from"node:child_process";import{existsSync,readdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{createRequire}from"node:module";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue}from"../lib/env.mjs";import{layerCss,layerFor}from"../lib/layers.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{runCommand}from"../lib/run.mjs";import{EXTERNALS}from"./externals.mjs";const argv=process.argv.slice(2);const only=parseFlagValue(argv,"lineage");const config=await loadConfig();const lineages=only?config.lineages.filter(l=>l.name===only):config.lineages;if(only&&lineages.length===0)throw new Error(`[build] --lineage ${only} is not in the config's lineages`);const fromRepo=createRequire(resolve(REPO_ROOT,"package.json"));const importFromRepo=name=>import(pathToFileURL(fromRepo.resolve(name)).href);function check(label,status){if(status!==0){console.error(`[build] ${label} failed (exit ${status}).`);process.exit(status??1)}}if(!argv.includes("--no-typecheck")&&existsSync(resolve(REPO_ROOT,"tsconfig.build.json"))){console.log("[build] typecheck: tsc -p tsconfig.build.json --noEmit");const tsc=fromRepo.resolve("typescript/bin/tsc");check("typecheck",spawnSync(process.execPath,[tsc,"-p","tsconfig.build.json","--noEmit"],{cwd:REPO_ROOT,stdio:"inherit"}).status)}process.env.NODE_ENV="production";const{build}=await importFromRepo("vite");const react=(await importFromRepo("@vitejs/plugin-react")).default;for(const lineage of lineages){const entry=resolve(REPO_ROOT,lineage.entry);if(!existsSync(entry))throw new Error(`[build] no barrel at ${lineage.entry} for lineage "${lineage.name}"`);const external=[...new Set([...EXTERNALS,...config.shared,...lineage.shared??[]])].filter(s=>s!==lineage.specifier);console.log(`
|
|
3
|
-
[build] ${lineage.name}: ${lineage.entry} \u2192 ${lineage.dist}/index.js`);await build({configFile:false,mode:"production",root:REPO_ROOT,logLevel:"warn",plugins:[react()],define:{"process.env.NODE_ENV":JSON.stringify("production"),__OA_BUNDLE_NAME__:JSON.stringify(config.org)},resolve:{alias:
|
|
2
|
+
import{spawnSync}from"node:child_process";import{existsSync,readdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{createRequire}from"node:module";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{assetAliases,assetFileNames,assetsPlugin}from"../lib/assets.mjs";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue}from"../lib/env.mjs";import{layerCss,layerFor}from"../lib/layers.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{runCommand}from"../lib/run.mjs";import{EXTERNALS}from"./externals.mjs";const argv=process.argv.slice(2);const only=parseFlagValue(argv,"lineage");const config=await loadConfig();const lineages=only?config.lineages.filter(l=>l.name===only):config.lineages;if(only&&lineages.length===0)throw new Error(`[build] --lineage ${only} is not in the config's lineages`);const fromRepo=createRequire(resolve(REPO_ROOT,"package.json"));const importFromRepo=name=>import(pathToFileURL(fromRepo.resolve(name)).href);function check(label,status){if(status!==0){console.error(`[build] ${label} failed (exit ${status}).`);process.exit(status??1)}}if(!argv.includes("--no-typecheck")&&existsSync(resolve(REPO_ROOT,"tsconfig.build.json"))){console.log("[build] typecheck: tsc -p tsconfig.build.json --noEmit");const tsc=fromRepo.resolve("typescript/bin/tsc");check("typecheck",spawnSync(process.execPath,[tsc,"-p","tsconfig.build.json","--noEmit"],{cwd:REPO_ROOT,stdio:"inherit"}).status)}process.env.NODE_ENV="production";const{build}=await importFromRepo("vite");const react=(await importFromRepo("@vitejs/plugin-react")).default;for(const lineage of lineages){const entry=resolve(REPO_ROOT,lineage.entry);if(!existsSync(entry))throw new Error(`[build] no barrel at ${lineage.entry} for lineage "${lineage.name}"`);const external=[...new Set([...EXTERNALS,...config.shared,...lineage.shared??[]])].filter(s=>s!==lineage.specifier);console.log(`
|
|
3
|
+
[build] ${lineage.name}: ${lineage.entry} \u2192 ${lineage.dist}/index.js`);await build({configFile:false,mode:"production",root:REPO_ROOT,base:"./",logLevel:"warn",plugins:[assetsPlugin({lineage:lineage.name,limits:config.assets}),react()],define:{"process.env.NODE_ENV":JSON.stringify("production"),__OA_BUNDLE_NAME__:JSON.stringify(config.org)},resolve:{alias:assetAliases(resolve(REPO_ROOT,"src"))},optimizeDeps:{exclude:external},build:{lib:{entry,formats:["es"],fileName:()=>"index.js"},rollupOptions:{external,output:{assetFileNames}},outDir:resolve(REPO_ROOT,lineage.dist),emptyOutDir:true,sourcemap:true}});await writeLayeredCss(lineage);check(`${lineage.name} contracts`,runCommand("contracts",["--entry",lineage.entry,"--out",`${lineage.dist}/contracts.json`]));check(`${lineage.name} knowledge`,runCommand("knowledge",["--bundle",lineage.name]))}console.log(`
|
|
4
4
|
[build] \u2705 ${lineages.map(l=>l.name).join(", ")}`);async function writeLayeredCss(lineage){const dist=resolve(REPO_ROOT,lineage.dist);const files=readdirSync(dist).filter(f=>f.endsWith(".css"));if(files.length===0)return;const css=files.map(f=>readFileSync(resolve(dist,f),"utf8")).join("\n");for(const f of files)rmSync(resolve(dist,f));const postcss=(await importFromRepo("postcss")).default;const layer=layerFor(lineage);writeFileSync(resolve(dist,"index.css"),layerCss(postcss,css,layer));console.log(`[build] ${lineage.name}: index.css in @layer ${layer}`)}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{existsSync,readFileSync}from"node:fs";import{relative,resolve}from"node:path";import{formatBytes,inlinedDataUris}from"../lib/assets.mjs";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue}from"../lib/env.mjs";import{REPO_ROOT}from"../lib/root.mjs";const INLINE_THRESHOLD=4*1024;const argv=process.argv.slice(2);const threshold=Number(parseFlagValue(argv,"threshold")??INLINE_THRESHOLD);if(!Number.isFinite(threshold)||threshold<0){console.error("[check-inline-assets] --threshold must be a number of bytes");process.exit(2)}const{lineages}=await loadConfig();const built=lineages.flatMap(l=>["index.js","index.css"].map(f=>resolve(REPO_ROOT,l.dist,f))).filter(f=>existsSync(f));if(built.length===0){const msg="[check-inline-assets] no built lineage found (each lineage's index.js).";if(argv.includes("--require-build")){console.error(`${msg} Run \`engine build\` first.`);process.exit(1)}console.warn(`${msg} Skipping (pass --require-build to make this fatal).`);process.exit(0)}let findings=0;for(const file of built){const hits=inlinedDataUris(readFileSync(file,"utf8"),threshold);const rel=relative(REPO_ROOT,file);for(const h of hits){findings++;console.error(`[check-inline-assets] ${rel}: an inlined ${h.mime} data: URI of ${formatBytes(h.bytes)} (over ${formatBytes(threshold)})`)}}if(findings){console.error(`
|
|
3
|
+
Import the image or font as a file instead: \`import url from './photo.jpg'\`,
|
|
4
|
+
\`new URL('./photo.jpg', import.meta.url)\` or CSS \`url(./font.woff2)\` (no \`?inline\`).
|
|
5
|
+
\`engine build\` emits it into the lineage's assets/ and \`engine upload\` ships it beside index.js.`);process.exit(1)}console.log(`[check-inline-assets] OK \u2014 ${built.length} built file(s), no image or font data: URI over ${formatBytes(threshold)}`);export{INLINE_THRESHOLD};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{
|
|
2
|
+
import{existsSync,statSync}from"node:fs";import{resolve}from"node:path";import{bundleFiles,metadataFiles,uploadBundleFiles}from"../lib/assets.mjs";import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";import{lineageConfig}from"../lib/config.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const BUNDLE_NAME=parseFlagValue(argv,"bundle")??"main";const dryRun=argv.includes("--dry-run");const IS_MAIN=BUNDLE_NAME==="main";const IS_PUBLIC=BUNDLE_NAME==="public";const IS_ORG_WIDE=IS_MAIN||IS_PUBLIC;const IS_SHARED=(await lineageConfig(BUNDLE_NAME)).kind==="shared";const DEFAULT_BUCKET=IS_PUBLIC||IS_SHARED?PUBLIC_BUNDLE_BUCKET:BUNDLE_BUCKET;const STORAGE_BUCKET=process.env.STORAGE_BUCKET||DEFAULT_BUCKET;if(STORAGE_BUCKET!==DEFAULT_BUCKET){console.error(`[upload-bundle] STORAGE_BUCKET must be "${DEFAULT_BUCKET}" for lineage "${BUNDLE_NAME}", got "${STORAGE_BUCKET}".`);process.exit(2)}const VERSION=process.env.VERSION||`v_${target}_${new Date().toISOString().replace(/[-:T]/g,"").replace(/\..+$/,"").slice(0,14)}`;if(!process.env.VERSION){console.log(`[upload-bundle] VERSION auto-generated: ${VERSION}`);process.env.VERSION=VERSION}const distDir=resolve(REPO_ROOT_,(await lineageConfig(BUNDLE_NAME)).dist);const bundlePath=resolve(distDir,"index.js");if(!existsSync(bundlePath)){console.error(`[upload-bundle] ${bundlePath} does not exist. `+(IS_MAIN?"Did `npm run build` run?":`Did \`OA_PROJECT_BUNDLE=${BUNDLE_NAME} npm run build:bundle\` run?`));process.exit(1)}console.log(`[upload-bundle] lineage=${BUNDLE_NAME} dist=${distDir.split("/").slice(-2).join("/")}`);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[upload-bundle] Could not resolve org_id for org_slug="${ORG_SLUG}":`,orgError??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;const files=bundleFiles(distDir);try{await uploadBundleFiles({bucket:supabase.storage.from(STORAGE_BUCKET),remoteBase,files,dryRun})}catch(e){console.error(e.message);process.exit(1)}const mapPath=resolve(distDir,"index.js.map");const{data:existingBundle,error:existingBundleError}=await supabase.from("component_bundle").select("id, status").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(existingBundleError){console.error(`[upload-bundle] Failed to look up existing component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}):`,existingBundleError);process.exit(1)}const bundleBytes=statSync(bundlePath).size;const hasSourcemap=existsSync(mapPath);const bundlePayload={org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:BUNDLE_NAME,version:VERSION,status:"uploaded",storage_bucket:STORAGE_BUCKET,storage_path:`${remoteBase}/index.js`,...IS_SHARED?{is_public:true}:{},uploaded_at:new Date().toISOString(),registered_at:null,cutover_at:null,git_sha:process.env.DEPLOY_SHA??process.env.GITHUB_SHA??null,git_ref:process.env.DEPLOY_REF??process.env.GITHUB_REF??null,git_repo:process.env.GITHUB_REPOSITORY??null,process_id:process.env.PROCESS_ID||null,metadata:{bundle_bytes:bundleBytes,has_sourcemap:hasSourcemap,bundle_scope:BUNDLE_NAME,files:metadataFiles(files),...process.env.GITHUB_RUN_ID?{github_run_id:process.env.GITHUB_RUN_ID}:{},...process.env.GITHUB_RUN_ATTEMPT?{github_run_attempt:process.env.GITHUB_RUN_ATTEMPT}:{}}};if(dryRun){console.log(`[upload-bundle] dry-run \u2014 would ${existingBundle?.id?`UPDATE ${existingBundle.id}`:"INSERT"} component_bundle:`);console.log(JSON.stringify(bundlePayload,null,2));console.log("[upload-bundle] dry-run \u2014 nothing written.")}else if(existingBundle?.id){const{error:updateError}=await supabase.from("component_bundle").update(bundlePayload).eq("id",existingBundle.id);if(updateError){console.error(`[upload-bundle] Failed to update component_bundle row:`,updateError);process.exit(1)}console.log(`[upload-bundle] ~ component_bundle ${existingBundle.id} reset to status='uploaded' (was '${existingBundle.status}').`)}else{const{data:inserted,error:insertError}=await supabase.from("component_bundle").insert(bundlePayload).select("id").single();if(insertError){console.error(`[upload-bundle] Failed to insert component_bundle row:`,insertError);process.exit(1)}console.log(`[upload-bundle] + component_bundle ${inserted.id} created (status='uploaded').`)}console.log(`[upload-bundle] Done \u2014 target=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}. Run register-components.mjs next.`);
|
package/src/index.d.mts
CHANGED
|
@@ -27,6 +27,10 @@ export type EngineConfig = {
|
|
|
27
27
|
overflow?: 'warn' | 'error';
|
|
28
28
|
output?: string;
|
|
29
29
|
alias?: Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
assets?: {
|
|
32
|
+
maxAssetBytes?: number;
|
|
33
|
+
maxTotalBytes?: number;
|
|
30
34
|
};
|
|
31
35
|
checks?: {
|
|
32
36
|
run?: string[];
|
|
@@ -73,6 +77,7 @@ export type LoadedConfig = {
|
|
|
73
77
|
};
|
|
74
78
|
shadows: Record<string, string>;
|
|
75
79
|
harness: { wrapper: string | null; widths: number[]; container: number; overflow: 'warn' | 'error'; output: string; alias: Record<string, string> };
|
|
80
|
+
assets: { maxAssetBytes: number; maxTotalBytes: number };
|
|
76
81
|
};
|
|
77
82
|
export function loadConfig(): Promise<LoadedConfig>;
|
|
78
83
|
export function lineageConfig(name: string): Promise<Lineage>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{existsSync,readdirSync,readFileSync,statSync}from"node:fs";import{extname,join,resolve}from"node:path";const ASSETS_DIR="assets";const ASSET_TYPES={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".avif":"image/avif",".gif":"image/gif",".svg":"image/svg+xml",".woff2":"font/woff2",".woff":"font/woff",".ttf":"font/ttf",".otf":"font/otf"};const DEFAULT_ASSET_LIMITS={maxAssetBytes:1024*1024,maxTotalBytes:8*1024*1024};const EXT_RE=Object.keys(ASSET_TYPES).map(e=>e.slice(1)).join("|");function svgProblems(text){const out=[];if(/<script\b/i.test(text))out.push("a <script> element");if(/<foreignObject\b/i.test(text))out.push("a <foreignObject> element");const handler=text.match(/<[^>]*?[\s"'/](on[a-z]+)\s*=/i);if(handler)out.push(`an event-handler attribute (${handler[1]})`);if(/javascript\s*:/i.test(text))out.push("a javascript: URL");return out}function assetProblems(name,source,limits=DEFAULT_ASSET_LIMITS){const ext=extname(name).toLowerCase();if(!ASSET_TYPES[ext]){return[`${ext||"a file without an extension"} is not an allowed asset type (images: png, jpg, jpeg, webp, avif, gif, svg; fonts: woff2, woff, ttf, otf)`]}const out=[];const bytes=typeof source==="string"?Buffer.byteLength(source):source.byteLength;if(bytes>limits.maxAssetBytes)out.push(`${formatBytes(bytes)} is over the ${formatBytes(limits.maxAssetBytes)} cap per asset (assets.maxAssetBytes)`);if(ext===".svg"){for(const p of svgProblems(typeof source==="string"?source:Buffer.from(source).toString("utf8")))out.push(`the SVG holds ${p}`)}return out}function lineageAssetProblems(assets,limits=DEFAULT_ASSET_LIMITS){const out=[];let total=0;for(const a of assets){const from=a.originalFileNames?.length?` (from ${a.originalFileNames.join(", ")})`:"";for(const p of assetProblems(a.fileName,a.source,limits))out.push(`${a.fileName}${from}: ${p}`);total+=typeof a.source==="string"?Buffer.byteLength(a.source):a.source.byteLength}if(total>limits.maxTotalBytes)out.push(`the lineage's assets total ${formatBytes(total)}, over the ${formatBytes(limits.maxTotalBytes)} cap (assets.maxTotalBytes)`);return out}function formatBytes(n){if(n>=1024*1024)return`${(n/(1024*1024)).toFixed(n%(1024*1024)?1:0)} MB`;if(n>=1024)return`${(n/1024).toFixed(n%1024?1:0)} KB`;return`${n} B`}function assetAliases(srcDir){return[{find:new RegExp(`^@/([^?#]*\\.(?:${EXT_RE}))$`,"i"),replacement:`${srcDir}/$1?no-inline`},{find:/^@\/([^?#]+)\?url$/i,replacement:`${srcDir}/$1?url&no-inline`},{find:"@",replacement:srcDir},{find:new RegExp(`^(?![a-z][a-z0-9+.-]*:|//)([^?#]+\\.(?:${EXT_RE}))$`,"i"),replacement:"$1?no-inline"},{find:/^(?![a-z][a-z0-9+.-]*:|\/\/)([^?#]+)\?url$/i,replacement:"$1?url&no-inline"}]}const META_URL_RE=new RegExp(`(new\\s+URL\\(\\s*)(['"\`])(\\.{1,2}/[^'"\`?#$]*\\.(?:${EXT_RE}))\\2(\\s*,\\s*import\\.meta\\.url\\s*\\))`,"gi");function tagImportMetaUrls(code){if(!code.includes("import.meta.url"))return null;const out=code.replace(META_URL_RE,(_,open,q,url,close)=>`${open}${q}${url}?no-inline${q}${close}`);return out===code?null:out}function assetFileNames(info){const name=info.names?.[0]??info.name??"";return name.endsWith(".css")?"[name][extname]":`${ASSETS_DIR}/[name]-[hash][extname]`}function assetsPlugin({lineage,limits=DEFAULT_ASSET_LIMITS}){return{name:"engine:assets",enforce:"pre",transform(code,id){const file=id.split("?")[0];if(file.includes("/node_modules/")||!/\.[cm]?[jt]sx?$/.test(file))return null;const out=tagImportMetaUrls(code);return out===null?null:{code:out,map:null}},generateBundle(_,bundle){const assets=Object.values(bundle).filter(o=>o.type==="asset"&&o.fileName.startsWith(`${ASSETS_DIR}/`));const problems=lineageAssetProblems(assets,limits);if(problems.length){this.error(`[build] ${lineage}: ${problems.length} asset problem(s):
|
|
2
|
+
\xB7 ${problems.join("\n \xB7 ")}`)}}}}function bundleFiles(distDir){const files=[{local:resolve(distDir,"index.js"),remote:"index.js",contentType:"application/javascript",listed:true}];const optional=[["index.css","text/css",true],["index.js.map","application/json",false],["contracts.json","application/json",false]];for(const[name,contentType,listed]of optional){const local=resolve(distDir,name);if(existsSync(local))files.push({local,remote:name,contentType,listed})}const assetsDir=resolve(distDir,ASSETS_DIR);if(existsSync(assetsDir)){for(const name of readdirSync(assetsDir).sort()){const local=join(assetsDir,name);if(!statSync(local).isFile())continue;const contentType=ASSET_TYPES[extname(name).toLowerCase()];if(!contentType)throw new Error(`[upload-bundle] ${ASSETS_DIR}/${name} is not an allowed asset type; rebuild with \`engine build\``);files.push({local,remote:`${ASSETS_DIR}/${name}`,contentType,listed:true})}}return files}function metadataFiles(files){return files.filter(f=>f.listed).map(f=>f.remote)}async function uploadBundleFiles({bucket,remoteBase,files,dryRun=false,log=console.log}){for(const f of files){const body=readFileSync(f.local);const path=`${remoteBase}/${f.remote}`;if(dryRun){log(`[upload-bundle] would upload ${path} (${body.byteLength} bytes, ${f.contentType})`);continue}const{error}=await bucket.upload(path,body,{contentType:f.contentType,upsert:false});if(error)throw new Error(`[upload-bundle] Failed to upload ${path}: ${error.message??error}`);log(`[upload-bundle] \u2191 ${path} (${body.byteLength} bytes)`)}}function inlinedDataUris(text,threshold){const out=[];const re=/data:((?:image|font)\/[a-z0-9.+-]+|application\/(?:font-[a-z0-9]+|x-font-[a-z0-9]+))((?:;[a-z0-9-]+=[^;,]*)*)(;base64)?,/gi;let m;while(m=re.exec(text)){const start=m.index+m[0].length;let end=start;while(end<text.length&&!`"'\`)
|
|
3
|
+
`.includes(text[end]))end++;const payload=text.slice(start,end);const bytes=m[3]?Math.floor(payload.replace(/=+$/,"").length*3/4):decodedLength(payload);if(bytes>threshold)out.push({mime:m[1].toLowerCase(),bytes});re.lastIndex=end}return out}function decodedLength(s){try{return Buffer.byteLength(decodeURIComponent(s))}catch{return s.length}}export{ASSETS_DIR,ASSET_TYPES,DEFAULT_ASSET_LIMITS,assetAliases,assetFileNames,assetProblems,assetsPlugin,bundleFiles,formatBytes,inlinedDataUris,lineageAssetProblems,metadataFiles,svgProblems,tagImportMetaUrls,uploadBundleFiles};
|
package/src/lib/config.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{REPO_ROOT}from"./root.mjs";const MAIN_LINEAGE="main";const DEFAULT_CHECKS=["validate-externals","check-wired","check-routes","check-design-refs","check-skills","check-work-orders","check-cutover-ownership","check-fixtures","check-tokens","check-css-scope","check-inline-styles","check-shadowing","check-theme","knowledge"];function budgetVerdict(name,count,budget){if(budget===void 0||budget===null)return{ok:true,message:null};if(count>budget)return{ok:false,message:`${count} finding(s), over the budget of ${budget}: the count may only fall (checks.budgets['${name}'])`};if(count<budget)return{ok:true,message:`${count} finding(s), under the budget of ${budget}: lower checks.budgets['${name}'] to ${count}`};return{ok:true,message:`${count} finding(s), at the budget`}}const PUBLIC_LINEAGE="public";const CONFIG_FILES=["engine.config.ts","engine.config.mts","engine.config.mjs","engine.config.js"];function defineConfig(config){return config}function lineageDefaults(name,kind){if(name===MAIN_LINEAGE){return{name,kind:"main",entry:"src/index.ts",sourceRoot:null,dist:"dist"}}if(name===PUBLIC_LINEAGE){return{name,kind:"public",entry:"src/index.public.ts",sourceRoot:"components/public/",dist:"dist-public"}}const k=kind??"page";return{name,kind:k,entry:`src/index.${name}.ts`,sourceRoot:k==="form"?`components/form/${name}/`:k==="shared"?`${name}/`:`components/project_page/${name}/`,dist:`dist-bundles/${name}`}}function normalize(raw,origin){if(!raw||typeof raw!=="object")throw new Error(`[engine] ${origin} must export a config object`);if(typeof raw.org!=="string"||!raw.org)throw new Error(`[engine] ${origin}: \`org\` (the org slug) is required`);const list=raw.lineages??[MAIN_LINEAGE];const lineages=list.map(l=>{const spec=typeof l==="string"?{name:l}:l;if(!spec?.name)throw new Error(`[engine] ${origin}: every lineage needs a name`);return{...lineageDefaults(spec.name,spec.kind),...spec}});const mainAt=lineages.findIndex(l=>l.name===MAIN_LINEAGE);if(mainAt>0)throw new Error(`[engine] ${origin}: main must be the first lineage`);if(mainAt===-1&&!lineages.every(l=>l.kind==="shared")){throw new Error(`[engine] ${origin}: lineages must start with "main", unless every lineage is shared`)}for(const l of lineages){if(l.kind==="shared"&&!/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test(l.specifier??"")){throw new Error(`[engine] ${origin}: shared lineage "${l.name}" needs a specifier like "@stack/${l.name}"`)}}const gated=raw.checks?.gated??{};const checks={run:raw.checks?.run??DEFAULT_CHECKS,peerRepos:raw.checks?.peerRepos??[],gated:{projectTemplates:gated.projectTemplates??[],orgTemplates:gated.orgTemplates??[]},budgets:raw.checks?.budgets??{},propsSchema:{families:raw.checks?.propsSchema?.families??null,names:raw.checks?.propsSchema?.names??[],floor:raw.checks?.propsSchema?.floor??1,propless:raw.checks?.propsSchema?.propless??[]}};const shadows=raw.shadows??{};const shared=raw.shared??[];const harness={wrapper:raw.harness?.wrapper??null,widths:raw.harness?.widths??[390,768,1280],container:raw.harness?.container??320,overflow:raw.harness?.overflow??"warn",output:raw.harness?.output??"test/visual/__output__",alias:raw.harness?.alias??{}};return{org:raw.org,lineages,checks,shared,shadows,harness}}const normalizeForTest=raw=>normalize(raw,"test");let cached;async function loadConfig(){if(cached)return cached;const file=CONFIG_FILES.map(f=>resolve(REPO_ROOT,f)).find(f=>existsSync(f));if(file){const mod=await import(pathToFileURL(file).href);cached=normalize(mod.default??mod,file);return cached}const pkg=JSON.parse(readFileSync(resolve(REPO_ROOT,"package.json"),"utf8"));let org=pkg.oaBundle?.name;const orgJson=resolve(REPO_ROOT,"template/config/org.json");if(existsSync(orgJson))org=JSON.parse(readFileSync(orgJson,"utf8")).slug??org;const lineages=(pkg.oaBundle?.lineages??[MAIN_LINEAGE]).map(n=>n==="workbooks"?{name:n,kind:"form"}:n);cached=normalize({org,lineages},"package.json oaBundle");return cached}async function lineageConfig(name){const{lineages}=await loadConfig();return lineages.find(l=>l.name===name)??lineageDefaults(name)}export{DEFAULT_CHECKS,MAIN_LINEAGE,PUBLIC_LINEAGE,budgetVerdict,defineConfig,lineageConfig,lineageDefaults,loadConfig,normalizeForTest};
|
|
1
|
+
import{existsSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{DEFAULT_ASSET_LIMITS}from"./assets.mjs";import{REPO_ROOT}from"./root.mjs";const MAIN_LINEAGE="main";const DEFAULT_CHECKS=["validate-externals","check-wired","check-routes","check-design-refs","check-skills","check-work-orders","check-cutover-ownership","check-fixtures","check-tokens","check-css-scope","check-inline-styles","check-shadowing","check-theme","check-inline-assets","knowledge"];function budgetVerdict(name,count,budget){if(budget===void 0||budget===null)return{ok:true,message:null};if(count>budget)return{ok:false,message:`${count} finding(s), over the budget of ${budget}: the count may only fall (checks.budgets['${name}'])`};if(count<budget)return{ok:true,message:`${count} finding(s), under the budget of ${budget}: lower checks.budgets['${name}'] to ${count}`};return{ok:true,message:`${count} finding(s), at the budget`}}const PUBLIC_LINEAGE="public";const CONFIG_FILES=["engine.config.ts","engine.config.mts","engine.config.mjs","engine.config.js"];function defineConfig(config){return config}function lineageDefaults(name,kind){if(name===MAIN_LINEAGE){return{name,kind:"main",entry:"src/index.ts",sourceRoot:null,dist:"dist"}}if(name===PUBLIC_LINEAGE){return{name,kind:"public",entry:"src/index.public.ts",sourceRoot:"components/public/",dist:"dist-public"}}const k=kind??"page";return{name,kind:k,entry:`src/index.${name}.ts`,sourceRoot:k==="form"?`components/form/${name}/`:k==="shared"?`${name}/`:`components/project_page/${name}/`,dist:`dist-bundles/${name}`}}function normalize(raw,origin){if(!raw||typeof raw!=="object")throw new Error(`[engine] ${origin} must export a config object`);if(typeof raw.org!=="string"||!raw.org)throw new Error(`[engine] ${origin}: \`org\` (the org slug) is required`);const list=raw.lineages??[MAIN_LINEAGE];const lineages=list.map(l=>{const spec=typeof l==="string"?{name:l}:l;if(!spec?.name)throw new Error(`[engine] ${origin}: every lineage needs a name`);return{...lineageDefaults(spec.name,spec.kind),...spec}});const mainAt=lineages.findIndex(l=>l.name===MAIN_LINEAGE);if(mainAt>0)throw new Error(`[engine] ${origin}: main must be the first lineage`);if(mainAt===-1&&!lineages.every(l=>l.kind==="shared")){throw new Error(`[engine] ${origin}: lineages must start with "main", unless every lineage is shared`)}for(const l of lineages){if(l.kind==="shared"&&!/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test(l.specifier??"")){throw new Error(`[engine] ${origin}: shared lineage "${l.name}" needs a specifier like "@stack/${l.name}"`)}}const gated=raw.checks?.gated??{};const checks={run:raw.checks?.run??DEFAULT_CHECKS,peerRepos:raw.checks?.peerRepos??[],gated:{projectTemplates:gated.projectTemplates??[],orgTemplates:gated.orgTemplates??[]},budgets:raw.checks?.budgets??{},propsSchema:{families:raw.checks?.propsSchema?.families??null,names:raw.checks?.propsSchema?.names??[],floor:raw.checks?.propsSchema?.floor??1,propless:raw.checks?.propsSchema?.propless??[]}};const shadows=raw.shadows??{};const shared=raw.shared??[];const harness={wrapper:raw.harness?.wrapper??null,widths:raw.harness?.widths??[390,768,1280],container:raw.harness?.container??320,overflow:raw.harness?.overflow??"warn",output:raw.harness?.output??"test/visual/__output__",alias:raw.harness?.alias??{}};const assets={maxAssetBytes:raw.assets?.maxAssetBytes??DEFAULT_ASSET_LIMITS.maxAssetBytes,maxTotalBytes:raw.assets?.maxTotalBytes??DEFAULT_ASSET_LIMITS.maxTotalBytes};for(const[k,v]of Object.entries(assets)){if(!Number.isInteger(v)||v<=0)throw new Error(`[engine] ${origin}: assets.${k} must be a positive number of bytes`)}return{org:raw.org,lineages,checks,shared,shadows,harness,assets}}const normalizeForTest=raw=>normalize(raw,"test");let cached;async function loadConfig(){if(cached)return cached;const file=CONFIG_FILES.map(f=>resolve(REPO_ROOT,f)).find(f=>existsSync(f));if(file){const mod=await import(pathToFileURL(file).href);cached=normalize(mod.default??mod,file);return cached}const pkg=JSON.parse(readFileSync(resolve(REPO_ROOT,"package.json"),"utf8"));let org=pkg.oaBundle?.name;const orgJson=resolve(REPO_ROOT,"template/config/org.json");if(existsSync(orgJson))org=JSON.parse(readFileSync(orgJson,"utf8")).slug??org;const lineages=(pkg.oaBundle?.lineages??[MAIN_LINEAGE]).map(n=>n==="workbooks"?{name:n,kind:"form"}:n);cached=normalize({org,lineages},"package.json oaBundle");return cached}async function lineageConfig(name){const{lineages}=await loadConfig();return lineages.find(l=>l.name===name)??lineageDefaults(name)}export{DEFAULT_CHECKS,MAIN_LINEAGE,PUBLIC_LINEAGE,budgetVerdict,defineConfig,lineageConfig,lineageDefaults,loadConfig,normalizeForTest};
|
package/src/lib/run.mjs
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
import{spawnSync}from"node:child_process";import{existsSync}from"node:fs";import{dirname,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT}from"./root.mjs";const COMMANDS_DIR=resolve(dirname(fileURLToPath(import.meta.url)),"..","commands");const TSX=import.meta.resolve("tsx/esm");const COMMANDS={build:"build.mjs",deploy:"deploy.mjs",check:"check.mjs",harness:"harness.mjs","test-visual":"test-visual.mjs",template:"template.mjs",upload:"upload-bundle.mjs",register:"register-components.mjs",cutover:"cutover.mjs",knowledge:"emit-knowledge.mjs",contracts:"emit-contracts.mjs","bundle-size":"bundle-size.mjs","check-bundle-leak":"check-bundle-leak.mjs","validate-externals":"validate-externals.mjs","check-wired":"check-wired-bindings.mjs","check-routes":"check-template-routes.mjs","check-design-refs":"check-design-refs.mjs","check-skills":"check-skills.mjs","check-work-orders":"check-work-orders.mjs","check-cutover-ownership":"check-cutover-ownership.mjs","check-fixtures":"check-fixtures.mjs","check-props-schema":"check-props-schema.mjs","check-tokens":"check-tokens.mjs","check-css-scope":"check-css-scope.mjs","check-inline-styles":"check-inline-styles.mjs","check-shadowing":"check-shadowing.mjs","check-theme":"check-theme.mjs"};function commandFile(name){if(!COMMANDS[name])return null;const file=resolve(COMMANDS_DIR,COMMANDS[name]);const ts=file.replace(/\.mjs$/,".mts");return existsSync(file)?file:existsSync(ts)?ts:null}function runCommand(name,args=[],env={}){const file=commandFile(name);if(!file)throw new Error(`[engine] unknown command "${name}"`);const r=spawnSync(process.execPath,["--import",TSX,file,...args],{cwd:REPO_ROOT,stdio:"inherit",env:{...process.env,ENGINE_ROOT:REPO_ROOT,...env}});return r.status??1}
|
|
1
|
+
import{spawnSync}from"node:child_process";import{existsSync,readFileSync}from"node:fs";import{dirname,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT}from"./root.mjs";const COMMANDS_DIR=resolve(dirname(fileURLToPath(import.meta.url)),"..","commands");const TSX=import.meta.resolve("tsx/esm");const COMMANDS={build:"build.mjs",deploy:"deploy.mjs",check:"check.mjs",harness:"harness.mjs","test-visual":"test-visual.mjs",template:"template.mjs",upload:"upload-bundle.mjs",register:"register-components.mjs",cutover:"cutover.mjs",knowledge:"emit-knowledge.mjs",contracts:"emit-contracts.mjs","bundle-size":"bundle-size.mjs","check-bundle-leak":"check-bundle-leak.mjs","validate-externals":"validate-externals.mjs","check-wired":"check-wired-bindings.mjs","check-routes":"check-template-routes.mjs","check-design-refs":"check-design-refs.mjs","check-skills":"check-skills.mjs","check-work-orders":"check-work-orders.mjs","check-cutover-ownership":"check-cutover-ownership.mjs","check-fixtures":"check-fixtures.mjs","check-props-schema":"check-props-schema.mjs","check-tokens":"check-tokens.mjs","check-css-scope":"check-css-scope.mjs","check-inline-styles":"check-inline-styles.mjs","check-shadowing":"check-shadowing.mjs","check-theme":"check-theme.mjs","check-inline-assets":"check-inline-assets.mjs"};function commandFile(name){if(!COMMANDS[name])return null;const file=resolve(COMMANDS_DIR,COMMANDS[name]);const ts=file.replace(/\.mjs$/,".mts");return existsSync(file)?file:existsSync(ts)?ts:null}function runCommand(name,args=[],env={}){const file=commandFile(name);if(!file)throw new Error(`[engine] unknown command "${name}"`);const r=spawnSync(process.execPath,["--import",TSX,file,...args],{cwd:REPO_ROOT,stdio:"inherit",env:{...process.env,ENGINE_ROOT:REPO_ROOT,...env}});return r.status??1}function commandUsage(name){const fallback=`usage: engine ${name} [args]`;const file=commandFile(name);if(!file)return fallback;const lines=readFileSync(file,"utf8").split("\n");let i=lines[0]?.startsWith("#!")?1:0;const out=[];if(lines[i]?.trim().startsWith("/**")){for(i+=1;i<lines.length&&!lines[i].includes("*/");i++)out.push(lines[i].replace(/^\s*\*\s?/,""))}else{for(;i<lines.length&&lines[i].startsWith("//");i++)out.push(lines[i].replace(/^\/\/\s?/,""))}const text=out.join("\n").trim();return text?`engine ${name}
|
|
2
|
+
|
|
3
|
+
${text}`:fallback}export{COMMANDS,commandFile,commandUsage,runCommand};
|
package/src/template/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import{execSync}from"node:child_process";function parseArgs(argv){const flags=ne
|
|
|
8
8
|
npm run template -- sync --layer form-templates --env ${this.options.targetEnv}`});skipped++;continue}const{data:existingRows,error:existingError}=await this.options.supabase.from("client_form").select("id").eq("client_id",clientId).eq("template_id",templateId).limit(1);if(existingError){throw new Error(`client_form lookup failed: ${existingError.message}`)}const existing=(existingRows??[])[0];let replaced=null;if(existing&&this.options.replaceExisting){const{count,error:countError}=await this.options.supabase.from("client_input").select("id",{count:"exact",head:true}).eq("form_id",existing.id);if(countError)throw new Error(`client_input count failed: ${countError.message}`);if(this.options.dryRun){entities.push({entity:workbook.template_name,action:"updated",id:existing.id,reason:`dry run \u2014 would DELETE this workbook and its ${count??0} answer(s), then re-seed it`});updated++;continue}const{error:inputsError}=await this.options.supabase.from("client_input").delete().eq("form_id",existing.id);if(inputsError)throw new Error(`client_input delete failed: ${inputsError.message}`);const{error:formError}=await this.options.supabase.from("client_form").delete().eq("id",existing.id);if(formError)throw new Error(`client_form delete failed: ${formError.message}`);replaced={answers:count??0}}else if(existing){entities.push({entity:workbook.template_name,action:"skipped",id:existing.id,reason:"workbook already exists \u2014 member progress is never overwritten"});skipped++;continue}if(this.options.dryRun){entities.push({entity:workbook.template_name,action:"created",id:null,reason:"dry run"});created++;continue}const completed=workbook.completed===true;const now=new Date().toISOString();const{data:inserted,error:insertError}=await this.options.supabase.from("client_form").insert({client_id:clientId,template_id:templateId,org_id:this.options.orgId,project_id:projectId,status:workbook.status??(completed?"completed":"in_progress"),progress:workbook.progress??{},started_at:now,completed_at:completed?now:null}).select("id").single();if(insertError)throw new Error(`client_form insert failed: ${insertError.message}`);const formId=inserted.id;if(replaced)updated++;else created++;const answerCount=await this.seedAnswers(formId,templateId,workbook);entities.push({entity:`${workbook.template_name} (${completed?"completed":workbook.status??"in_progress"}, ${answerCount} answer(s))`,action:replaced?"updated":"created",id:formId,...replaced?{reason:`replaced an existing workbook and its ${replaced.answers} answer(s)`}:{}})}}catch(error){errors.push({entity:file,error:error instanceof Error?error.message:String(error)})}}return{layer:this.def.name,created,updated,skipped,entities,errors,duration_ms:Date.now()-started}}static envelope(fieldType,raw){if(fieldType==="number"){const n=typeof raw==="number"?raw:Number(raw);if(Number.isNaN(n)){throw new Error(`field_type "number" but value "${raw}" is not a number`)}return{data:n,type:fieldType}}return{data:String(raw),type:fieldType}}async seedAnswers(formId,templateId,workbook){let count=0;for(const answer of workbook.answers??[]){const field=await this.resolveField(templateId,answer.field_name);if(!field){console.warn(`[template] \u26A0 ${workbook.template_name}: no client_input_field named "${answer.field_name}" \u2014 answer skipped. Field names come from the form-templates layer; check the workbook definition.`);continue}if(field.sharedFrom){console.log(`[template] ${workbook.template_name}: "${answer.field_name}" resolved from shared template "${field.sharedFrom}"`)}if(!answer.revision){const{data:priorRows,error:priorError}=await this.options.supabase.from("client_input").select("id").eq("form_id",formId).eq("field_id",field.id).limit(1);if(priorError)throw new Error(`client_input lookup failed: ${priorError.message}`);if((priorRows??[]).length)continue}let value;try{value=_DevWorkbookManager.envelope(field.fieldType,answer.value)}catch(error2){throw new Error(`${workbook.template_name}.${answer.field_name}: ${error2 instanceof Error?error2.message:String(error2)}`)}const{error}=await this.options.supabase.from("client_input").insert({form_id:formId,field_id:field.id,org_id:this.options.orgId,value});if(error)throw new Error(`client_input insert failed: ${error.message}`);count++}return count}};var CUTOVER_OWNED_COLUMNS=["remote_version","previous_version","remote_url","remote_storage_bucket"];var LAYOUT_SYNC_OWNED=["name","description"];var OrgManager=class extends LayerManager{get shape(){return{fields:{slug:{type:"string",required:true,minLength:1},name:{type:"string",required:true,minLength:1},advisor:{type:"boolean"},advisor_org_slug:{type:"string"},bundle_name:{type:"string"},asset_bucket:{type:"string"},description:{type:"string"}},ignored:["_comment"]}}naturalKeyOf(def){return def.slug}naturalKeyFilter(def){return{metadata_org_slug:def.slug}}toRow(){throw new Error("OrgManager writes via syncAll()")}files(){return["org.json"]}async fetchDbKeys(){const config=loadOrgConfig();const org=await findOrgBySlug(this.options.supabase,config.slug);return org?new Map([[config.slug,org.id]]):new Map}async syncAll(){const started=Date.now();const entities=[];const errors=[];const config=loadOrgConfig();const dryRun=this.options.dryRun;try{const existingOrg=await findOrgBySlug(this.options.supabase,config.slug);let advisorOrgId=null;if(!config.advisor&&config.advisor_org_slug){const advisorOrg=await findOrgBySlug(this.options.supabase,config.advisor_org_slug);if(advisorOrg){advisorOrgId=advisorOrg.id;this.mappings.set("org",config.advisor_org_slug,advisorOrg.id)}else{entities.push({entity:`advisor:${config.advisor_org_slug}`,action:"skipped",id:null,reason:`advisor org "${config.advisor_org_slug}" not found in ${this.options.targetEnv} \u2014 seed the advisor org repo first, then re-run. Advisor-scoped lookups stay unresolved until then.`})}}const orgMetadata={...existingOrg?{}:{seeded:true,created_at:new Date().toISOString()},org_slug:config.slug,organization_type:config.advisor?"advisor":"client",...config.advisor_org_slug?{advisor_org_slug:config.advisor_org_slug}:{},...advisorOrgId?{advisor_org_id:advisorOrgId}:{}};let orgId;if(existingOrg){orgId=existingOrg.id;if(!dryRun){const{data:current}=await this.options.supabase.from("organization").select("metadata").eq("id",orgId).single();const merged={...current?.metadata??{},...orgMetadata};const{error}=await this.options.supabase.from("organization").update({advisor:config.advisor,metadata:merged}).eq("id",orgId);if(error)throw new Error(`organization update failed: ${error.message}`)}entities.push({entity:`organization:${config.slug}`,action:"updated",id:orgId})}else if(dryRun){orgId="(new)";entities.push({entity:`organization:${config.slug}`,action:"created",id:orgId})}else{const{data,error}=await this.options.supabase.from("organization").insert([{name:config.name,advisor:config.advisor,created_by:"00000000-0000-0000-0000-000000000000",metadata:orgMetadata}]).select("id").single();if(error)throw new Error(`organization insert failed: ${error.message}`);orgId=data.id;entities.push({entity:`organization:${config.slug}`,action:"created",id:orgId})}if(!dryRun&&orgId!=="(new)")this.mappings.set("org",config.slug,orgId);const{data:templateRows,error:templateError}=await this.options.supabase.from("org_template").select("id, metadata").eq("org_id",orgId==="(new)"?"00000000-0000-0000-0000-000000000000":orgId).eq("is_archived",false).limit(2);if(templateError)throw new Error(`org_template lookup failed: ${templateError.message}`);const templates=templateRows??[];if(templates.length>1){throw new Error(`${templates.length} non-archived org_template rows for org ${orgId} \u2014 ambiguous. Fix the DB first.`)}const existingTemplate=templates[0];if(existingTemplate){const patch={advisor_org_id:advisorOrgId,is_published:true,is_archived:false,metadata:{...existingTemplate.metadata??{},org_slug:config.slug,organization_type:config.advisor?"advisor":"client",...config.advisor_org_slug?{advisor_org_slug:config.advisor_org_slug}:{}},updated_at:new Date().toISOString()};const leaked=[...CUTOVER_OWNED_COLUMNS,...LAYOUT_SYNC_OWNED,"remote_name","remote_enabled"].filter(c=>c in patch);if(leaked.length){throw new Error(`BUG: org layer patch contains non-owned column(s): ${leaked.join(", ")}`)}if(!dryRun){const{error}=await this.options.supabase.from("org_template").update(patch).eq("id",existingTemplate.id);if(error)throw new Error(`org_template update failed: ${error.message}`)}entities.push({entity:"org_template",action:"updated",id:existingTemplate.id});if(!dryRun)this.mappings.set("org_template",config.slug,existingTemplate.id)}else if(dryRun){entities.push({entity:"org_template",action:"created",id:"(new)"})}else{const{data,error}=await this.options.supabase.from("org_template").insert([{org_id:orgId,advisor_org_id:advisorOrgId,name:config.name,description:config.description??null,is_published:true,is_archived:false,remote_name:config.slug,remote_enabled:true,metadata:{org_slug:config.slug,organization_type:config.advisor?"advisor":"client",...config.advisor_org_slug?{advisor_org_slug:config.advisor_org_slug}:{},created_by:"template-cli"}}]).select("id").single();if(error)throw new Error(`org_template insert failed: ${error.message}`);const templateId=data.id;entities.push({entity:"org_template",action:"created",id:templateId});this.mappings.set("org_template",config.slug,templateId)}}catch(error){errors.push({entity:"org",error:error instanceof Error?error.message:String(error)})}return{layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started}}};import{existsSync as existsSync6,readFileSync as readFileSync3}from"node:fs";import{relative}from"node:path";var COLUMNS="id, org_id, org_slug, bundle_name, version, status, is_live, uploaded_at";async function resolveBundle(supabase,orgId,orgSlug,options={}){const bundleName=options.bundleName??"main";const version=options.version??null;if(version){const{data,error}=await supabase.from("component_bundle").select(COLUMNS).eq("org_id",orgId).eq("bundle_name",bundleName).eq("version",version).limit(2);if(error)throw new Error(`component_bundle lookup failed: ${error.message}`);const rows=data??[];if(rows.length>1){throw new Error(`${rows.length} bundles match org=${orgSlug} bundle=${bundleName} version=${version} \u2014 ambiguous.`)}const row2=rows[0];if(!row2){throw new Error(`no component_bundle for org=${orgSlug} bundle=${bundleName} version=${version}.`)}return{...row2,resolved_by:"pin"}}const{data:liveRows,error:liveError}=await supabase.from("component_bundle").select(COLUMNS).eq("org_id",orgId).eq("bundle_name",bundleName).eq("is_live",true).limit(2);if(liveError)throw new Error(`component_bundle lookup failed: ${liveError.message}`);const live=liveRows??[];if(live.length>1){throw new Error(`${live.length} bundles in lineage "${bundleName}" (org=${orgSlug}) have is_live=true \u2014 exactly one row may be live. Re-run cutover from whichever repo owns this org's deploy pipeline (this one if it has scripts/cutover.mjs):
|
|
9
9
|
npm run deploy:cutover -- --org ${orgSlug} --version <v> --<env>`)}if(live[0]){return{...live[0],resolved_by:"is_live"}}const{data:latestRows,error:latestError}=await supabase.from("component_bundle").select(COLUMNS).eq("org_id",orgId).eq("bundle_name",bundleName).order("uploaded_at",{ascending:false}).limit(1);if(latestError)throw new Error(`component_bundle lookup failed: ${latestError.message}`);const latest=(latestRows??[])[0];if(!latest){throw new Error(`no component_bundle found for org=${orgSlug} bundle=${bundleName}. Run \`npm run deploy:upload\` first.`)}const row=latest;console.warn(`[template] \u26A0 no is_live bundle in lineage "${bundleName}" (org=${orgSlug}) \u2014 falling back to the most recently uploaded bundle (${row.version}). The host serves ONLY the is_live row, so this metadata may describe components the host is not running. Pin with --version, or promote one first from THIS repo:
|
|
10
10
|
npm run deploy:cutover -- --org ${orgSlug} --version ${row.version} --<env>`);return{...latest,resolved_by:"latest-uploaded"}}async function lineageExists(supabase,orgId,bundleName){const{data,error}=await supabase.from("component_bundle_lineage").select("bundle_name").eq("org_id",orgId).eq("bundle_name",bundleName).limit(1);if(error)throw new Error(`component_bundle_lineage lookup failed: ${error.message}`);return(data??[]).length>0}async function resolveBundleIfAny(supabase,orgId,orgSlug,options={}){try{return await resolveBundle(supabase,orgId,orgSlug,options)}catch(error){const message=error instanceof Error?error.message:String(error);if(!options.version&&message.startsWith("no component_bundle found for org="))return null;throw error}}var HOST_NATIVE_COMPONENTS=new Set(["ProjectPageByType","ProjectPageById","ProjectPageRenderer","IndividualPageViewer","CellRenderer","CellsRenderer"]);var REGISTRY_COLUMNS="component_name, description, props_schema, delivery_mode, exposed_export, is_active";async function fetchRegistry(supabase,bundleId,componentNames){if(!bundleId||componentNames.length===0)return new Map;const{data,error}=await supabase.from("component_registry").select(REGISTRY_COLUMNS).eq("bundle_id",bundleId).in("component_name",componentNames);if(error)throw new Error(`component_registry lookup failed: ${error.message}`);const rows=data??[];return new Map(rows.map(row=>[row.component_name,row]))}function missingBundleComponents(componentNames,registry){return componentNames.filter(n=>!registry.has(n)&&!HOST_NATIVE_COMPONENTS.has(n))}var SHELL_BLOCK_KEYS=["tab_bar","app_bar","chat_rail","more_menu"];function collectFromLayout(layout,out){if(!layout||typeof layout!=="object")return;const l=layout;if(l.base_route?.component)out.add(l.base_route.component);const visit=route=>{if(route?.component)out.add(route.component);if(route?.right_sidebar?.component)out.add(route.right_sidebar.component);route?.children?.forEach(visit)};[...l.whitelisted_routes??[],...l.routes??[]].forEach(visit);for(const region of Object.values(l.regions??{})){for(const band of["header","body","footer"]){const name=region?.[band]?.component;if(name&&!name.startsWith("host:"))out.add(name)}}for(const nav of l.sidebar?.navigation??[]){if(nav?.component)out.add(nav.component)}if(l.right_sidebar?.component)out.add(l.right_sidebar.component);for(const key of SHELL_BLOCK_KEYS){const block=l[key];if(block?.component)out.add(block.component)}}function collectComponentNames(payload){const out=new Set;collectFromLayout(payload.layout,out);for(const tier of["public","portal","platform","agency","aux"]){collectFromLayout(payload[`${tier}_layout`],out)}return[...out]}function collectCellComponents(cells){return[...new Set(cells.map(c=>c?.component).filter(c=>Boolean(c)))]}function extractRegistryMeta(row){const props=row.props_schema??{};return{description:row.description??null,declared_data:props.data??null,declared_actions:props.actions??null,declared_props:props.properties??null,contract_version:props.contract_version??null,delivery_mode:row.delivery_mode??null,exposed_export:row.exposed_export??null}}function metaFor(component,registry){if(!component)return null;const row=registry.get(component);return row?extractRegistryMeta(row):null}function inlineRouteRegistryMeta(routes,registry){if(!Array.isArray(routes))return routes;return routes.map(route=>{const children=Array.isArray(route?.children)?{children:inlineRouteRegistryMeta(route.children,registry)}:{};if(!route?.component)return{...route,...children};const next={...route,...children,registry_meta:metaFor(route.component,registry)};const sidebar=route.right_sidebar;if(sidebar?.component){next.right_sidebar={...sidebar,registry_meta:metaFor(sidebar.component,registry)}}return next})}function inlineNavRegistryMeta(navItems,registry){if(!Array.isArray(navItems))return navItems;return navItems.map(item=>item?.component?{...item,registry_meta:metaFor(item.component,registry)}:item)}function annotateLayout(layout,registry){if(!layout||typeof layout!=="object")return layout;const l=layout;return{...l,...l.sidebar?{sidebar:{...l.sidebar,navigation:inlineNavRegistryMeta(l.sidebar.navigation,registry)}}:{},...l.whitelisted_routes?{whitelisted_routes:inlineRouteRegistryMeta(l.whitelisted_routes,registry)}:{},...l.routes?{routes:inlineRouteRegistryMeta(l.routes,registry)}:{}}}function buildTemplateMeta(input){const{sourcePath,bundle,componentNames,registry,generator}=input;return{source:sourcePath,generated_at:new Date().toISOString(),generator,bundle_id:bundle.id,bundle_version:bundle.version,bundle_org_slug:bundle.org_slug,bundle_status:bundle.status,bundle_resolved_by:bundle.resolved_by,component_count:componentNames.length,missing_components:missingBundleComponents(componentNames,registry),host_native_components:componentNames.filter(n=>HOST_NATIVE_COMPONENTS.has(n))}}function stripHarnessOnly(value){if(Array.isArray(value))return value.map(v=>stripHarnessOnly(v));if(value&&typeof value==="object"){const out={};for(const[key,v]of Object.entries(value)){if(key.startsWith("_harness_")||key.startsWith("_mock"))continue;out[key]=stripHarnessOnly(v)}return out}return value}var GENERATOR="template/cli sync --layer org-template-layout";var LAYOUT_COLUMNS=["public_layout","portal_layout","platform_layout","agency_layout","aux_layout"];var CUTOVER_OWNED_COLUMNS2=["remote_name","remote_version","previous_version","remote_url","remote_storage_bucket","remote_enabled"];var OrgTemplateLayoutManager=class extends LayerManager{get shape(){return{fields:{name:{type:"string",required:true,minLength:1},description:{type:"string"}},allowUnknown:true}}naturalKeyOf(def){return def.name}naturalKeyFilter(){return{metadata_org_slug:loadOrgConfig().slug}}toRow(){throw new Error("OrgTemplateLayoutManager writes via syncAll()")}source(){const config=loadTemplatesConfig().org_template;if(!config)return null;return{slug:config.slug,bundleName:config.bundle_name??"main",layoutBundles:config.layout_bundles??{},path:orgTemplatePath(config.slug)}}bundleFor(column){const source=this.source();return source?.layoutBundles[column]??source?.bundleName??"main"}files(){const source=this.source();return source&&existsSync6(source.path)?[`src/org_templates/${source.slug}/template.json`]:[]}read(path){const parsed=JSON.parse(readFileSync3(path,"utf8"));const file=parsed;if(!file||typeof file.name!=="string"){throw new Error(`${path}: template.json must have at least { "name": "..." }`)}return file}fileKeys(){const source=this.source();if(!source||!existsSync6(source.path))return[];try{return[this.read(source.path).name]}catch{return[]}}async fetchDbKeys(){const source=this.source();if(!source||!existsSync6(source.path))return new Map;const{data,error}=await this.options.supabase.from("org_template").select("id, name").eq("org_id",this.options.orgId).eq("is_archived",false).limit(2);if(error)throw new Error(`org_template lookup failed: ${error.message}`);const rows=data??[];return new Map(rows.map(r=>[r.name,r.id]))}validateAll(){const errors=[];const warnings=[];const source=this.source();if(!source){return{layer:this.def.name,filesChecked:0,errors,warnings}}const label=`src/org_templates/${source.slug}/template.json`;if(!existsSync6(source.path)){errors.push({file:label,message:`declared in config/templates.json but not found on disk`});return{layer:this.def.name,filesChecked:0,errors,warnings}}let file;try{file=this.read(source.path)}catch(error){errors.push({file:label,message:error instanceof Error?error.message:String(error)});return{layer:this.def.name,filesChecked:1,errors,warnings}}const declared=LAYOUT_COLUMNS.filter(c=>file[c]!==void 0);if(declared.length===0){warnings.push({file:label,message:"declares no *_layout column \u2014 nothing to sync"})}const droppable=Object.keys(file).filter(k=>!["name","description","is_published","is_archived","metadata"].includes(k)&&!LAYOUT_COLUMNS.includes(k));for(const key of droppable){warnings.push({file:label,message:`"${key}" has no org_template column and is not written (harness-only or dead config)`})}if(collectComponentNames(file).length===0){warnings.push({file:label,message:"no component references found in any layout"})}return{layer:this.def.name,filesChecked:1,errors,warnings}}async syncAll(){const started=Date.now();const entities=[];const errors=[];const source=this.source();const done=()=>({layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started});if(!source){entities.push({entity:"org-template-layout",action:"skipped",id:null,reason:"no org_template declared in config/templates.json"});return done()}try{const file=this.read(source.path);const cleaned=stripHarnessOnly(file);const presentColumns=LAYOUT_COLUMNS.filter(c=>cleaned[c]!==void 0);const neededLineages=[...new Set([source.bundleName,...presentColumns.map(c=>this.bundleFor(c))])].sort();const bundles=new Map;for(const lineage of neededLineages){const resolved=await resolveBundleIfAny(this.options.supabase,this.options.orgId,this.options.orgSlug,{version:lineage===source.bundleName?this.options.versionPin??null:null,bundleName:lineage});if(!resolved){entities.push({entity:`org_template:${cleaned.name}`,action:"skipped",id:null,reason:`no component_bundle in lineage "${lineage}" yet \u2014 layouts inline registry metadata from the bundle the host serves. Deploy (upload \u2192 register \u2192 host cutover), then re-run this layer.`});return done()}bundles.set(lineage,resolved)}const bundle=bundles.get(source.bundleName);const componentNames=collectComponentNames(cleaned);const registries=new Map;for(const lineage of neededLineages){const names=new Set;for(const column of presentColumns){if(this.bundleFor(column)!==lineage)continue;for(const n of collectComponentNames({[column]:cleaned[column]}))names.add(n)}const target=bundles.get(lineage);registries.set(lineage,await fetchRegistry(this.options.supabase,target.id,[...names]));const missing=missingBundleComponents([...names],registries.get(lineage));if(missing.length){console.warn(`[template] \u26A0 ${missing.length} component(s) not in ${lineage} bundle ${target.version}: ${missing.join(", ")}. Routes referencing them land with registry_meta=null.`)}}const registry=new Map;for(const r of registries.values())for(const[k,v]of r)registry.set(k,v);const{data:rows,error:fetchError}=await this.options.supabase.from("org_template").select("id, name, metadata").eq("org_id",this.options.orgId).eq("is_archived",false).limit(2);if(fetchError)throw new Error(`org_template lookup failed: ${fetchError.message}`);const existing=rows??[];if(existing.length>1){throw new Error(`${existing.length} non-archived org_template rows for this org \u2014 ambiguous. Fix the DB first.`)}const row=existing[0];if(!row){throw new Error(`no org_template row for "${this.options.orgSlug}". Run \`sync --layer org\` first \u2014 that layer owns row creation, this one owns the layout columns.`)}const patch={name:cleaned.name,updated_at:new Date().toISOString()};if(cleaned.description!==void 0)patch.description=cleaned.description;if(cleaned.is_published!==void 0)patch.is_published=cleaned.is_published;if(cleaned.is_archived!==void 0)patch.is_archived=cleaned.is_archived;for(const column of presentColumns){patch[column]=annotateLayout(cleaned[column],registries.get(this.bundleFor(column)))}patch.metadata={...row.metadata??{},...cleaned.metadata??{},org_slug:row.metadata?.org_slug??cleaned.metadata?.org_slug??this.options.orgSlug,bundle_refs:cleaned.metadata?.bundle_refs??[bundle.org_slug],template_meta:{...buildTemplateMeta({sourcePath:relative(REPO_ROOT,source.path),bundle,componentNames,registry,generator:GENERATOR}),layout_bundles:Object.fromEntries(presentColumns.map(column=>{const lineage=this.bundleFor(column);return[column,{bundle_name:lineage,version:bundles.get(lineage).version}]}))}};const leaked=CUTOVER_OWNED_COLUMNS2.filter(c=>c in patch);if(leaked.length){throw new Error(`BUG: cutover-owned column(s) in patch: ${leaked.join(", ")}`)}const untouched=LAYOUT_COLUMNS.filter(c=>!(c in patch));if(untouched.length){console.log(` layout columns preserved (absent from template.json): ${untouched.join(", ")}`)}if(!this.options.dryRun){const{error}=await this.options.supabase.from("org_template").update(patch).eq("id",row.id);if(error)throw new Error(`org_template update failed: ${error.message}`);this.mappings.set("org_template",this.options.orgSlug,row.id)}entities.push({entity:`org_template:${cleaned.name} (bundle ${bundle.version} via ${bundle.resolved_by})`,action:"updated",id:row.id})}catch(error){errors.push({entity:`src/org_templates/${source.slug}/template.json`,error:error instanceof Error?error.message:String(error)})}return done()}async healthChecks(){const source=this.source();if(!source||!existsSync6(source.path))return[];const{data,error}=await this.options.supabase.from("org_template").select("metadata").eq("org_id",this.options.orgId).eq("is_archived",false).limit(1);if(error){return[{name:"layout-bundle",severity:"warn",message:`lookup failed: ${error.message}`}]}const meta=(data??[])[0]?.metadata;const templateMeta=meta?.template_meta;if(!templateMeta){return[{name:"layout-bundle",severity:"warn",message:"org_template has no metadata.template_meta \u2014 layouts have never been synced from src/"}]}let live;try{live=await resolveBundle(this.options.supabase,this.options.orgId,this.options.orgSlug,{bundleName:source.bundleName})}catch(error2){return[{name:"layout-bundle",severity:"warn",message:error2 instanceof Error?error2.message:String(error2)}]}const recorded=templateMeta.bundle_version;if(recorded&&live.resolved_by==="is_live"&&recorded!==live.version){return[{name:"layout-bundle",severity:"warn",message:`layouts were annotated from bundle ${recorded}, but the host serves ${live.version}. Re-run \`sync --layer org-template-layout\` so registry_meta describes what is running.`}]}return[{name:"layout-bundle",severity:"ok",message:`layouts annotated from ${recorded??"(unknown)"}; host serves ${live.version} (${live.resolved_by})`}]}};import{existsSync as existsSync7,readFileSync as readFileSync4}from"node:fs";import{join as join6}from"node:path";var CONFIG_FILE=join6(CONFIG_DIR,"public.json");var DOMAIN_SHAPE={fields:{domain_pattern:{type:"string",required:true,minLength:1},is_wildcard:{type:"boolean",default:false},priority:{type:"number",default:100},is_active:{type:"boolean",default:true}},ignored:["_comment"]};var OrgTemplatePublicManager=class extends LayerManager{get shape(){return DOMAIN_SHAPE}naturalKeyOf(def){return def.domain_pattern}naturalKeyFilter(){throw new Error("OrgTemplatePublicManager writes via syncAll()")}toRow(){throw new Error("OrgTemplatePublicManager writes via syncAll()")}config(){if(!existsSync7(CONFIG_FILE))return null;const parsed=JSON.parse(readFileSync4(CONFIG_FILE,"utf8"));const outer=validateShape(parsed,{fields:{advisor_org_slug:{type:"string",required:true,minLength:1},domains:{type:"array",required:true},branding:{type:"object"},metadata:{type:"object"}},ignored:["_comment"]});if(!outer.ok||!outer.value){throw new Error(`template/config/public.json is invalid:
|
|
11
|
-
- ${outer.errors.join("\n - ")}`)}const domains=[];outer.value.domains.forEach((raw,index)=>{const result=validateShape(raw,DOMAIN_SHAPE);if(!result.ok||!result.value){throw new Error(`public.json domains[${index}] is invalid: ${result.errors.join("; ")}`)}domains.push(result.value)});const full=parsed;return{advisor_org_slug:outer.value.advisor_org_slug,domains,branding:full.branding,metadata:full.metadata}}publicLayout(){const configured=loadTemplatesConfig().org_template;if(!configured)return null;const path=orgTemplatePath(configured.slug);if(!existsSync7(path))return null;const parsed=JSON.parse(readFileSync4(path,"utf8"));return parsed.public_layout??null}files(){const config=this.config();if(!config)return[];const filter=this.options.nameFilter;const domains=config.domains.map(d=>d.domain_pattern);return filter?domains.filter(d=>d===filter):domains}fileKeys(){return this.files()}async advisorOrgId(slug){if(slug===loadOrgConfig().slug)return this.options.orgId;const org=await findOrgBySlug(this.options.supabase,slug);if(!org){throw new Error(`public.json advisor_org_slug "${slug}" resolves to no organization. Seed that org first.`)}return org.id}validateAll(){const errors=[];const warnings=[];let config;try{config=this.config()}catch(error){errors.push({file:"config/public.json",message:error instanceof Error?error.message:String(error)});return{layer:this.def.name,filesChecked:1,errors,warnings}}if(!config)return{layer:this.def.name,filesChecked:0,errors,warnings};const seen=new Set;for(const domain of config.domains){if(seen.has(domain.domain_pattern)){errors.push({file:"config/public.json",message:`domain_pattern "${domain.domain_pattern}" is declared twice`})}seen.add(domain.domain_pattern);if(domain.domain_pattern.includes("*")&&domain.is_wildcard!==true){warnings.push({file:"config/public.json",message:`"${domain.domain_pattern}" contains * but is_wildcard is false \u2014 it will only ever match literally`})}}if(!this.publicLayout()){warnings.push({file:"config/public.json",message:"the configured org template declares no public_layout \u2014 every domain row would be written with a null layout, and the anonymous path renders nothing"})}return{layer:this.def.name,filesChecked:1,errors,warnings}}async syncAll(){const started=Date.now();const entities=[];const errors=[];const done=()=>({layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started});let config;try{config=this.config()}catch(error){errors.push({entity:"config/public.json",error:error instanceof Error?error.message:String(error)});return done()}if(!config){entities.push({entity:"org-template-public",action:"skipped",id:null,reason:"no template/config/public.json \u2014 this org publishes no public site"});return done()}const layout=this.publicLayout();const selected=new Set(this.files());let advisorOrgId;try{advisorOrgId=await this.advisorOrgId(config.advisor_org_slug)}catch(error){errors.push({entity:"advisor_org_slug",error:error instanceof Error?error.message:String(error)});return done()}for(const domain of config.domains){if(!selected.has(domain.domain_pattern))continue;try{const outcome=await upsertByNaturalKey(this.options.supabase,"org_template_public",{advisor_org_id:advisorOrgId,domain_pattern:domain.domain_pattern},{advisor_org_id:advisorOrgId,domain_pattern:domain.domain_pattern,is_wildcard:domain.is_wildcard??false,priority:domain.priority??100,is_active:domain.is_active??true,public_layout:layout,branding:config.branding??null,metadata:config.metadata??null,updated_at:new Date().toISOString()},{dryRun:this.options.dryRun});entities.push({entity:domain.domain_pattern,action:outcome.action,id:outcome.id})}catch(error){errors.push({entity:domain.domain_pattern,error:error instanceof Error?error.message:String(error)})}}return done()}async fetchDbKeys(){const config=this.config();if(!config)return new Map;const advisorOrgId=await this.advisorOrgId(config.advisor_org_slug);const{data,error}=await this.options.supabase.from("org_template_public").select("id, domain_pattern").eq("advisor_org_id",advisorOrgId);if(error)throw new Error(`org_template_public: ${error.message}`);const keys=new Map;for(const row of data??[]){keys.set(row.domain_pattern,row.id)}return keys}async healthChecks(){const config=this.config();if(!config)return[];const live=config.domains.filter(d=>d.is_active!==false);const exact=live.filter(d=>d.is_wildcard!==true);if(live.length===0){return[{name:"public-domains",severity:"fail",message:"every domain in public.json is is_active:false \u2014 the public site resolves nowhere"}]}if(exact.length===0){return[{name:"public-domains",severity:"warn",message:`${live.length} active domain(s), all wildcards \u2014 preview URLs resolve but the production hostname does not`}]}return[{name:"public-domains",severity:"ok",message:`${exact.length} exact + ${live.length-exact.length} wildcard domain(s) active`}]}};import{readFileSync as readFileSync5}from"node:fs";import{relative as relative2}from"node:path";var GENERATOR2="template/cli sync --layer page-templates";var PageTemplateManager=class extends LayerManager{get shape(){return{fields:{name:{type:"string",required:true,minLength:1},status:{type:"string",default:"active"},page_type:{type:"string"},content:{type:"object",required:true},layout:{type:"object"},metadata:{type:"object"}},allowUnknown:true}}naturalKeyOf(def){return def.page_type??def.name}naturalKeyFilter(def){return def.page_type?{org_id:this.options.orgId,page_type:def.page_type,is_template:true}:{org_id:this.options.orgId,name:def.name,is_template:true}}toRow(){throw new Error("PageTemplateManager writes via syncAll()")}sources(){const filter=this.options.nameFilter;const out=[];for(const template of loadTemplatesConfig().project_templates){if(template.skip_pages)continue;for(const page of projectTemplatePages(template.slug)){const label=`${template.slug}/${page.page}`;if(filter&&filter!==page.page&&filter!==label&&filter!==template.slug)continue;out.push({label,projectSlug:template.slug,bundleName:template.bundle_name??"main",path:page.path})}}return out}files(){return this.sources().map(s=>`src/project_templates/${s.projectSlug}/pages/${s.label.split("/")[1]}/page.json`)}read(path){const parsed=JSON.parse(readFileSync5(path,"utf8"));const file=parsed;if(!file?.name||!Array.isArray(file.content?.cells)){throw new Error(`${path}: page.json needs { name, content: { cells: [...] } }`)}return file}fileKeys(){const keys=[];for(const source of this.sources()){try{const file=this.read(source.path);keys.push(file.page_type??file.name)}catch{}}return keys}async fetchDbKeys(){const{data,error}=await this.options.supabase.from("project_page").select("id, name, page_type").eq("org_id",this.options.orgId).eq("is_template",true);if(error)throw new Error(`project_page lookup failed: ${error.message}`);const out=new Map;this.duplicateDbKeys.clear();for(const row of data??[]){const key=row.page_type??row.name;if(out.has(key))this.duplicateDbKeys.add(key);out.set(key,row.id)}return out}validateAll(){const errors=[];const warnings=[];const byPageType=new Map;let checked=0;for(const source of this.sources()){const label=`${source.label}/page.json`;let file;try{file=this.read(source.path)}catch(error){errors.push({file:label,message:error instanceof Error?error.message:String(error)});continue}checked++;if(!file.page_type){warnings.push({file:label,message:"no page_type \u2014 the row can only be addressed by name, and project_template routes address pages by params.pageType"})}else{const previous=byPageType.get(file.page_type);if(previous){errors.push({file:label,message:`page_type "${file.page_type}" is also declared by ${previous}`})}else{byPageType.set(file.page_type,label)}}const cells=file.content.cells??[];if(cells.length===0){warnings.push({file:label,message:"no cells \u2014 the page renders empty"})}if(!file.layout){warnings.push({file:label,message:"no layout \u2014 one is derived from cell order on write"})}}return{layer:this.def.name,filesChecked:checked,errors,warnings}}async syncAll(){const started=Date.now();const entities=[];const errors=[];const bundles=new Map;const bundleFor=async source=>{if(bundles.has(source.bundleName))return bundles.get(source.bundleName)??null;const resolved=await resolveBundleIfAny(this.options.supabase,this.options.orgId,this.options.orgSlug,{version:this.options.versionPin??null,bundleName:source.bundleName});bundles.set(source.bundleName,resolved);return resolved};for(const source of this.sources()){try{const file=stripHarnessOnly(this.read(source.path));const cells=file.content.cells??[];const bundle=await bundleFor(source);if(!bundle){entities.push({entity:source.label,action:"skipped",id:null,reason:`no component_bundle in lineage "${source.bundleName}" yet \u2014 cells inline registry metadata from it. Deploy the bundle, then re-run.`});continue}const componentNames=collectCellComponents(cells);const registry=await fetchRegistry(this.options.supabase,bundle.id,componentNames);const missing=missingBundleComponents(componentNames,registry);if(missing.length){console.warn(`[template] \u26A0 ${source.label}: ${missing.length} cell component(s) not in bundle ${bundle.version}: ${missing.join(", ")}`)}const naturalKey=this.naturalKeyFilter(file);let existingMetadata={};let lookup=this.options.supabase.from("project_page").select("id, metadata").eq("org_id",this.options.orgId).eq("is_template",true);lookup=file.page_type?lookup.eq("page_type",file.page_type):lookup.eq("name",file.name);const{data:existingRows,error:readError}=await lookup.limit(2);if(readError)throw new Error(`project_page lookup failed: ${readError.message}`);const existing=existingRows??[];if(existing[0])existingMetadata=existing[0].metadata??{};const row={org_id:this.options.orgId,project_id:null,is_template:true,name:file.name,status:file.status??"active",page_type:file.page_type??null,content:file.content,layout:file.layout??{type:"grid",structure:cells.map(c=>({cellId:c.id}))},metadata:{...existingMetadata,...file.metadata??{},bundle_refs:[bundle.org_slug],template_meta:{source:relative2(REPO_ROOT,source.path),generated_at:new Date().toISOString(),generator:GENERATOR2,bundle_id:bundle.id,bundle_version:bundle.version,bundle_org_slug:bundle.org_slug,bundle_status:bundle.status,bundle_resolved_by:bundle.resolved_by,project_template:source.projectSlug,cell_count:cells.length,missing_components:missing}},updated_at:new Date().toISOString()};const outcome=await upsertByNaturalKey(this.options.supabase,"project_page",naturalKey,row,{dryRun:this.options.dryRun});if(!this.options.dryRun){this.mappings.set("project_page",file.page_type??file.name,outcome.id)}entities.push({entity:`${source.label} \u2192 ${file.page_type??file.name} (${cells.length} cells)`,action:outcome.action,id:outcome.id})}catch(error){errors.push({entity:source.label,error:error instanceof Error?error.message:String(error)})}}return{layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started}}async healthChecks(){const{data,error}=await this.options.supabase.from("project_page").select("id, name, page_type, created_at").eq("org_id",this.options.orgId).eq("is_template",true);if(error){return[{name:"page-template-dupes",severity:"warn",message:`lookup failed: ${error.message}`}]}const rows=data??[];const groups=new Map;for(const row of rows){const key=row.page_type??`name:${row.name}`;groups.set(key,[...groups.get(key)??[],row])}const duplicated=[...groups.entries()].filter(([,group])=>group.length>1);if(duplicated.length===0){return[{name:"page-template-dupes",severity:"ok",message:`${groups.size} page template(s), one row each`}]}return[{name:"page-template-dupes",severity:"fail",message:`${duplicated.length} page type(s) have more than one template row \u2014 sync refuses to write them, and a page-builder agent cannot tell which is current. Created by scripts/upload-page-template.mjs, which upserts on a freshly generated id`,detail:duplicated.map(([key,group])=>`${key}: ${group.length} rows \u2014 ${group.map(r=>`${r.id.slice(0,8)} (${r.created_at.slice(0,10)})`).join(", ")}`)}]}};import{existsSync as existsSync8,readFileSync as readFileSync6}from"node:fs";import{join as join7,relative as relative3}from"node:path";var GENERATOR3="template/cli sync --layer project-templates";var ProjectTemplateManager=class extends LayerManager{get shape(){return{fields:{name:{type:"string",required:true,minLength:1},description:{type:"string"},project_type:{type:"string"},slug:{type:"string",minLength:1},processes:{type:"array"},layout:{type:"object"},metadata:{type:"object"}},allowUnknown:true}}naturalKeyOf(def){return def.name}naturalKeyFilter(def){return{org_id:this.options.orgId,name:def.name}}toRow(){throw new Error("ProjectTemplateManager writes via syncAll()")}sources(){const filter=this.options.nameFilter;return loadTemplatesConfig().project_templates.filter(t=>!filter||t.slug===filter).map(t=>({slug:t.slug,bundleName:t.bundle_name??"main",path:join7(projectTemplateDir(t.slug),"template.json")}))}files(){return this.sources().filter(s=>existsSync8(s.path)).map(s=>`src/project_templates/${s.slug}/template.json`)}read(path){const parsed=JSON.parse(readFileSync6(path,"utf8"));const file=parsed;if(!file||typeof file.name!=="string"){throw new Error(`${path}: template.json must have at least { "name": "..." }`)}return file}fileKeys(){const keys=[];for(const source of this.sources()){if(!existsSync8(source.path))continue;try{keys.push(this.read(source.path).name)}catch{}}return keys}async fetchDbKeys(){const{data,error}=await this.options.supabase.from("project_template").select("id, name").eq("org_id",this.options.orgId);if(error)throw new Error(`project_template lookup failed: ${error.message}`);const out=new Map;for(const row of data??[]){out.set(row.name,row.id)}return out}validateAll(){const errors=[];const warnings=[];let checked=0;for(const source of this.sources()){const label=`src/project_templates/${source.slug}/template.json`;if(!existsSync8(source.path)){errors.push({file:label,message:"declared in config/templates.json but not on disk"});continue}checked++;let file;try{file=this.read(source.path)}catch(error){errors.push({file:label,message:error instanceof Error?error.message:String(error)});continue}if(!file.project_type){warnings.push({file:label,message:"no project_type \u2014 page-type routing needs one (host doc 07/08)"})}if(!file.layout){warnings.push({file:label,message:"no layout \u2014 the project renders no routes"})}if(collectComponentNames(file).length===0){warnings.push({file:label,message:"layout references no components"})}}return{layer:this.def.name,filesChecked:checked,errors,warnings}}async syncAll(){const started=Date.now();const entities=[];const errors=[];for(const source of this.sources()){const label=`src/project_templates/${source.slug}/template.json`;try{if(!existsSync8(source.path)){throw new Error("declared in config/templates.json but not on disk")}const cleaned=stripHarnessOnly(this.read(source.path));const bundle=await resolveBundleIfAny(this.options.supabase,this.options.orgId,this.options.orgSlug,{version:this.options.versionPin??null,bundleName:source.bundleName});if(!bundle){entities.push({entity:cleaned.name,action:"skipped",id:null,reason:`no component_bundle in lineage "${source.bundleName}" yet \u2014 routes inline registry metadata from it, and project_template.bundle_name has an FK to component_bundle_lineage, which host cutover creates. Deploy the bundle, then re-run.`});continue}const componentNames=collectComponentNames(cleaned);const registry=await fetchRegistry(this.options.supabase,bundle.id,componentNames);const missing=missingBundleComponents(componentNames,registry);if(missing.length){console.warn(`[template] \u26A0 ${source.slug}: ${missing.length} component(s) not in bundle ${bundle.version}: ${missing.join(", ")}`)}const{data:existingRows,error:readError}=await this.options.supabase.from("project_template").select("id, metadata, bundle_name").eq("org_id",this.options.orgId).eq("name",cleaned.name).limit(2);if(readError)throw new Error(`project_template lookup failed: ${readError.message}`);const existing=existingRows??[];if(existing.length>1){throw new Error(`${existing.length} project_template rows named "${cleaned.name}" for this org \u2014 ambiguous, refusing to write. Ids: ${existing.map(r=>r.id).join(", ")}.`)}const layout=cleaned.layout?{...cleaned.layout,whitelisted_routes:inlineRouteRegistryMeta(cleaned.layout.whitelisted_routes,registry),...cleaned.layout.routes?{routes:inlineRouteRegistryMeta(cleaned.layout.routes,registry)}:{}}:null;const row={org_id:this.options.orgId,name:cleaned.name,description:cleaned.description??null,project_type:cleaned.project_type??null,...cleaned.slug!==void 0?{slug:cleaned.slug}:{},...cleaned.processes!==void 0?{processes:cleaned.processes}:{},bundle_name:await lineageExists(this.options.supabase,this.options.orgId,source.bundleName)?source.bundleName:existing[0]?.bundle_name??null,layout,metadata:{...existing[0]?.metadata??{},...cleaned.metadata??{},bundle_refs:cleaned.metadata?.bundle_refs??[bundle.org_slug],template_meta:buildTemplateMeta({sourcePath:relative3(REPO_ROOT,source.path),bundle,componentNames,registry,generator:GENERATOR3})},updated_at:new Date().toISOString()};const outcome=await upsertByNaturalKey(this.options.supabase,"project_template",{org_id:this.options.orgId,name:cleaned.name},row,{dryRun:this.options.dryRun});if(!this.options.dryRun){this.mappings.set("project_template",cleaned.name,outcome.id)}entities.push({entity:`${cleaned.name} (${source.bundleName}/${bundle.version} via ${bundle.resolved_by})`,action:outcome.action,id:outcome.id})}catch(error){errors.push({entity:label,error:error instanceof Error?error.message:String(error)})}}return{layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started}}async healthChecks(){const checks=[];for(const source of this.sources()){if(!existsSync8(source.path))continue;let file;try{file=this.read(source.path)}catch{continue}const routes=file.layout?.whitelisted_routes??[];const referenced=[...new Set(routes.map(r=>r.params?.pageType).filter(t=>Boolean(t)))];if(referenced.length===0)continue;const{data,error}=await this.options.supabase.from("project_page").select("page_type").eq("org_id",this.options.orgId).eq("is_template",true).in("page_type",referenced);if(error){checks.push({name:"route-page-types",severity:"warn",message:`page-type lookup failed: ${error.message}`});continue}const present=new Set((data??[]).map(r=>r.page_type));const absent=referenced.filter(t=>!present.has(t));checks.push(absent.length===0?{name:"route-page-types",severity:"ok",message:`${source.slug}: all ${referenced.length} routed page type(s) have a template`}:{name:"route-page-types",severity:"fail",message:`${source.slug}: ${absent.length} routed page type(s) have NO template row \u2014 those routes render nothing for a client`,detail:absent})}return checks}};var ProjectManager=class extends LayerManager{get shape(){return{fields:{name:{type:"string",required:true,minLength:1},description:{type:"string"},status:{type:"string",default:"active"},project_template_name:{type:"string",required:true,minLength:1},clients:{type:"array",default:[]},metadata:{type:"object"}},ignored:["_comment"]}}naturalKeyOf(def){return def.name}naturalKeyFilter(def){return{org_id:this.options.orgId,name:def.name}}toRow(){throw new Error("ProjectManager writes via syncAll()")}async anyBundleDeployed(){const{data,error}=await this.options.supabase.from("component_bundle").select("id").eq("org_id",this.options.orgId).limit(1);if(error)throw new Error(`component_bundle lookup failed: ${error.message}`);return(data??[]).length>0}async resolveTemplateId(name){const{data,error}=await this.options.supabase.from("project_template").select("id").eq("org_id",this.options.orgId).eq("name",name).limit(2);if(error)throw new Error(`project_template lookup failed: ${error.message}`);const rows=data??[];if(rows.length>1){throw new Error(`${rows.length} project_template rows named "${name}" \u2014 ambiguous.`)}if(!rows[0]){if(!await this.anyBundleDeployed())return null;throw new Error(`no project_template named "${name}" for this org, and this org HAS deployed bundles \u2014 so this is a missing or misnamed template, not an undeployed environment. That row is owned by the project-templates LAYER, not this one:
|
|
11
|
+
- ${outer.errors.join("\n - ")}`)}const domains=[];outer.value.domains.forEach((raw,index)=>{const result=validateShape(raw,DOMAIN_SHAPE);if(!result.ok||!result.value){throw new Error(`public.json domains[${index}] is invalid: ${result.errors.join("; ")}`)}domains.push(result.value)});const full=parsed;return{advisor_org_slug:outer.value.advisor_org_slug,domains,branding:full.branding,metadata:full.metadata}}publicLayout(){const configured=loadTemplatesConfig().org_template;if(!configured)return null;const path=orgTemplatePath(configured.slug);if(!existsSync7(path))return null;const parsed=JSON.parse(readFileSync4(path,"utf8"));return parsed.public_layout??null}files(){const config=this.config();if(!config)return[];const filter=this.options.nameFilter;const domains=config.domains.map(d=>d.domain_pattern);return filter?domains.filter(d=>d===filter):domains}fileKeys(){return this.files()}async advisorOrgId(slug){if(slug===loadOrgConfig().slug)return this.options.orgId;const org=await findOrgBySlug(this.options.supabase,slug);if(!org){throw new Error(`public.json advisor_org_slug "${slug}" resolves to no organization. Seed that org first.`)}return org.id}validateAll(){const errors=[];const warnings=[];let config;try{config=this.config()}catch(error){errors.push({file:"config/public.json",message:error instanceof Error?error.message:String(error)});return{layer:this.def.name,filesChecked:1,errors,warnings}}if(!config)return{layer:this.def.name,filesChecked:0,errors,warnings};const seen=new Set;for(const domain of config.domains){if(seen.has(domain.domain_pattern)){errors.push({file:"config/public.json",message:`domain_pattern "${domain.domain_pattern}" is declared twice`})}seen.add(domain.domain_pattern);if(domain.domain_pattern.includes("*")&&domain.is_wildcard!==true){warnings.push({file:"config/public.json",message:`"${domain.domain_pattern}" contains * but is_wildcard is false \u2014 it will only ever match literally`})}}if(!this.publicLayout()){warnings.push({file:"config/public.json",message:"the configured org template declares no public_layout \u2014 every domain row would be written with a null layout, and the anonymous path renders nothing"})}return{layer:this.def.name,filesChecked:1,errors,warnings}}async syncAll(){const started=Date.now();const entities=[];const errors=[];const done=()=>({layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started});let config;try{config=this.config()}catch(error){errors.push({entity:"config/public.json",error:error instanceof Error?error.message:String(error)});return done()}if(!config){entities.push({entity:"org-template-public",action:"skipped",id:null,reason:"no template/config/public.json \u2014 this org publishes no public site"});return done()}const layout=this.publicLayout();const selected=new Set(this.files());let advisorOrgId;try{advisorOrgId=await this.advisorOrgId(config.advisor_org_slug)}catch(error){errors.push({entity:"advisor_org_slug",error:error instanceof Error?error.message:String(error)});return done()}for(const domain of config.domains){if(!selected.has(domain.domain_pattern))continue;try{const outcome=await upsertByNaturalKey(this.options.supabase,"org_template_public",{advisor_org_id:advisorOrgId,domain_pattern:domain.domain_pattern},{advisor_org_id:advisorOrgId,domain_pattern:domain.domain_pattern,is_wildcard:domain.is_wildcard??false,priority:domain.priority??100,is_active:domain.is_active??true,public_layout:layout,branding:config.branding??null,metadata:config.metadata??null,updated_at:new Date().toISOString()},{dryRun:this.options.dryRun});entities.push({entity:domain.domain_pattern,action:outcome.action,id:outcome.id})}catch(error){errors.push({entity:domain.domain_pattern,error:error instanceof Error?error.message:String(error)})}}return done()}async fetchDbKeys(){const config=this.config();if(!config)return new Map;const advisorOrgId=await this.advisorOrgId(config.advisor_org_slug);const{data,error}=await this.options.supabase.from("org_template_public").select("id, domain_pattern").eq("advisor_org_id",advisorOrgId);if(error)throw new Error(`org_template_public: ${error.message}`);const keys=new Map;for(const row of data??[]){keys.set(row.domain_pattern,row.id)}return keys}async healthChecks(){const config=this.config();if(!config)return[];const live=config.domains.filter(d=>d.is_active!==false);const exact=live.filter(d=>d.is_wildcard!==true);if(live.length===0){return[{name:"public-domains",severity:"fail",message:"every domain in public.json is is_active:false \u2014 the public site resolves nowhere"}]}if(exact.length===0){return[{name:"public-domains",severity:"warn",message:`${live.length} active domain(s), all wildcards \u2014 preview URLs resolve but the production hostname does not`}]}return[{name:"public-domains",severity:"ok",message:`${exact.length} exact + ${live.length-exact.length} wildcard domain(s) active`}]}};import{readFileSync as readFileSync5}from"node:fs";import{relative as relative2}from"node:path";var GENERATOR2="template/cli sync --layer page-templates";var PageTemplateManager=class extends LayerManager{get shape(){return{fields:{name:{type:"string",required:true,minLength:1},status:{type:"string",default:"active"},page_type:{type:"string"},content:{type:"object",required:true},layout:{type:"object"},metadata:{type:"object"}},allowUnknown:true}}naturalKeyOf(def){return def.page_type??def.name}naturalKeyFilter(def){return def.page_type?{org_id:this.options.orgId,page_type:def.page_type,is_template:true}:{org_id:this.options.orgId,name:def.name,is_template:true}}toRow(){throw new Error("PageTemplateManager writes via syncAll()")}sources(){const filter=this.options.nameFilter;const out=[];for(const template of loadTemplatesConfig().project_templates){if(template.skip_pages)continue;for(const page of projectTemplatePages(template.slug)){const label=`${template.slug}/${page.page}`;if(filter&&filter!==page.page&&filter!==label&&filter!==template.slug)continue;out.push({label,projectSlug:template.slug,bundleName:template.bundle_name??"main",path:page.path})}}return out}files(){return this.sources().map(s=>`src/project_templates/${s.projectSlug}/pages/${s.label.split("/")[1]}/page.json`)}read(path){const parsed=JSON.parse(readFileSync5(path,"utf8"));const file=parsed;if(!file?.name||!Array.isArray(file.content?.cells)){throw new Error(`${path}: page.json needs { name, content: { cells: [...] } }`)}return file}fileKeys(){const keys=[];for(const source of this.sources()){try{const file=this.read(source.path);keys.push(file.page_type??file.name)}catch{}}return keys}async fetchDbKeys(){const{data,error}=await this.options.supabase.from("project_page").select("id, name, page_type").eq("org_id",this.options.orgId).eq("is_template",true);if(error)throw new Error(`project_page lookup failed: ${error.message}`);const out=new Map;this.duplicateDbKeys.clear();for(const row of data??[]){const key=row.page_type??row.name;if(out.has(key))this.duplicateDbKeys.add(key);out.set(key,row.id)}return out}validateAll(){const errors=[];const warnings=[];const byPageType=new Map;let checked=0;for(const source of this.sources()){const label=`${source.label}/page.json`;let file;try{file=this.read(source.path)}catch(error){errors.push({file:label,message:error instanceof Error?error.message:String(error)});continue}checked++;if(!file.page_type){warnings.push({file:label,message:"no page_type \u2014 the row can only be addressed by name, and project_template routes address pages by params.pageType"})}else{const previous=byPageType.get(file.page_type);if(previous){errors.push({file:label,message:`page_type "${file.page_type}" is also declared by ${previous}`})}else{byPageType.set(file.page_type,label)}}const cells=file.content.cells??[];if(cells.length===0){warnings.push({file:label,message:"no cells \u2014 the page renders empty"})}if(!file.layout){warnings.push({file:label,message:"no layout \u2014 one is derived from cell order on write"})}}return{layer:this.def.name,filesChecked:checked,errors,warnings}}async syncAll(){const started=Date.now();const entities=[];const errors=[];const bundles=new Map;const bundleFor=async source=>{if(bundles.has(source.bundleName))return bundles.get(source.bundleName)??null;const resolved=await resolveBundleIfAny(this.options.supabase,this.options.orgId,this.options.orgSlug,{version:this.options.versionPin??null,bundleName:source.bundleName});bundles.set(source.bundleName,resolved);return resolved};for(const source of this.sources()){try{const file=stripHarnessOnly(this.read(source.path));const cells=file.content.cells??[];const bundle=await bundleFor(source);if(!bundle){entities.push({entity:source.label,action:"skipped",id:null,reason:`no component_bundle in lineage "${source.bundleName}" yet \u2014 cells inline registry metadata from it. Deploy the bundle, then re-run.`});continue}const componentNames=collectCellComponents(cells);const registry=await fetchRegistry(this.options.supabase,bundle.id,componentNames);const missing=missingBundleComponents(componentNames,registry);if(missing.length){console.warn(`[template] \u26A0 ${source.label}: ${missing.length} cell component(s) not in bundle ${bundle.version}: ${missing.join(", ")}`)}const naturalKey=this.naturalKeyFilter(file);let existingMetadata={};let lookup=this.options.supabase.from("project_page").select("id, metadata").eq("org_id",this.options.orgId).eq("is_template",true);lookup=file.page_type?lookup.eq("page_type",file.page_type):lookup.eq("name",file.name);const{data:existingRows,error:readError}=await lookup.limit(2);if(readError)throw new Error(`project_page lookup failed: ${readError.message}`);const existing=existingRows??[];if(existing[0])existingMetadata=existing[0].metadata??{};const row={org_id:this.options.orgId,project_id:null,is_template:true,name:file.name,status:file.status??"active",page_type:file.page_type??null,content:file.content,layout:file.layout??{type:"grid",structure:cells.map(c=>({cellId:c.id}))},metadata:{...existingMetadata,...file.metadata??{},bundle_refs:[bundle.org_slug],template_meta:{source:relative2(REPO_ROOT,source.path),generated_at:new Date().toISOString(),generator:GENERATOR2,bundle_id:bundle.id,bundle_version:bundle.version,bundle_org_slug:bundle.org_slug,bundle_status:bundle.status,bundle_resolved_by:bundle.resolved_by,project_template:source.projectSlug,cell_count:cells.length,missing_components:missing}},updated_at:new Date().toISOString()};const outcome=await upsertByNaturalKey(this.options.supabase,"project_page",naturalKey,row,{dryRun:this.options.dryRun});if(!this.options.dryRun){this.mappings.set("project_page",file.page_type??file.name,outcome.id)}entities.push({entity:`${source.label} \u2192 ${file.page_type??file.name} (${cells.length} cells)`,action:outcome.action,id:outcome.id})}catch(error){errors.push({entity:source.label,error:error instanceof Error?error.message:String(error)})}}return{layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started}}async healthChecks(){const{data,error}=await this.options.supabase.from("project_page").select("id, name, page_type, created_at").eq("org_id",this.options.orgId).eq("is_template",true);if(error){return[{name:"page-template-dupes",severity:"warn",message:`lookup failed: ${error.message}`}]}const rows=data??[];const groups=new Map;for(const row of rows){const key=row.page_type??`name:${row.name}`;groups.set(key,[...groups.get(key)??[],row])}const duplicated=[...groups.entries()].filter(([,group])=>group.length>1);if(duplicated.length===0){return[{name:"page-template-dupes",severity:"ok",message:`${groups.size} page template(s), one row each`}]}return[{name:"page-template-dupes",severity:"fail",message:`${duplicated.length} page type(s) have more than one template row \u2014 sync refuses to write them, and a page-builder agent cannot tell which is current. Created by scripts/upload-page-template.mjs, which upserts on a freshly generated id`,detail:duplicated.map(([key,group])=>`${key}: ${group.length} rows \u2014 ${group.map(r=>`${r.id.slice(0,8)} (${r.created_at.slice(0,10)})`).join(", ")}`)}]}};import{existsSync as existsSync8,readFileSync as readFileSync6}from"node:fs";import{join as join7,relative as relative3}from"node:path";var GENERATOR3="template/cli sync --layer project-templates";var ProjectTemplateManager=class extends LayerManager{get shape(){return{fields:{name:{type:"string",required:true,minLength:1},description:{type:"string"},project_type:{type:"string"},slug:{type:"string",minLength:1},processes:{type:"array"},audience:{type:"enum",values:["client","internal"]},layout:{type:"object"},metadata:{type:"object"}},allowUnknown:true}}naturalKeyOf(def){return def.name}naturalKeyFilter(def){return{org_id:this.options.orgId,name:def.name}}toRow(){throw new Error("ProjectTemplateManager writes via syncAll()")}sources(){const filter=this.options.nameFilter;return loadTemplatesConfig().project_templates.filter(t=>!filter||t.slug===filter).map(t=>({slug:t.slug,bundleName:t.bundle_name??"main",path:join7(projectTemplateDir(t.slug),"template.json")}))}files(){return this.sources().filter(s=>existsSync8(s.path)).map(s=>`src/project_templates/${s.slug}/template.json`)}read(path){const parsed=JSON.parse(readFileSync6(path,"utf8"));const file=parsed;if(!file||typeof file.name!=="string"){throw new Error(`${path}: template.json must have at least { "name": "..." }`)}return file}fileKeys(){const keys=[];for(const source of this.sources()){if(!existsSync8(source.path))continue;try{keys.push(this.read(source.path).name)}catch{}}return keys}async fetchDbKeys(){const{data,error}=await this.options.supabase.from("project_template").select("id, name").eq("org_id",this.options.orgId);if(error)throw new Error(`project_template lookup failed: ${error.message}`);const out=new Map;for(const row of data??[]){out.set(row.name,row.id)}return out}validateAll(){const errors=[];const warnings=[];let checked=0;for(const source of this.sources()){const label=`src/project_templates/${source.slug}/template.json`;if(!existsSync8(source.path)){errors.push({file:label,message:"declared in config/templates.json but not on disk"});continue}checked++;let file;try{file=this.read(source.path)}catch(error){errors.push({file:label,message:error instanceof Error?error.message:String(error)});continue}if(!file.project_type){warnings.push({file:label,message:"no project_type \u2014 page-type routing needs one (host doc 07/08)"})}if(!file.layout){warnings.push({file:label,message:"no layout \u2014 the project renders no routes"})}if(collectComponentNames(file).length===0){warnings.push({file:label,message:"layout references no components"})}}return{layer:this.def.name,filesChecked:checked,errors,warnings}}async syncAll(){const started=Date.now();const entities=[];const errors=[];for(const source of this.sources()){const label=`src/project_templates/${source.slug}/template.json`;try{if(!existsSync8(source.path)){throw new Error("declared in config/templates.json but not on disk")}const cleaned=stripHarnessOnly(this.read(source.path));const bundle=await resolveBundleIfAny(this.options.supabase,this.options.orgId,this.options.orgSlug,{version:this.options.versionPin??null,bundleName:source.bundleName});if(!bundle){entities.push({entity:cleaned.name,action:"skipped",id:null,reason:`no component_bundle in lineage "${source.bundleName}" yet \u2014 routes inline registry metadata from it, and project_template.bundle_name has an FK to component_bundle_lineage, which host cutover creates. Deploy the bundle, then re-run.`});continue}const componentNames=collectComponentNames(cleaned);const registry=await fetchRegistry(this.options.supabase,bundle.id,componentNames);const missing=missingBundleComponents(componentNames,registry);if(missing.length){console.warn(`[template] \u26A0 ${source.slug}: ${missing.length} component(s) not in bundle ${bundle.version}: ${missing.join(", ")}`)}const{data:existingRows,error:readError}=await this.options.supabase.from("project_template").select("id, metadata, bundle_name").eq("org_id",this.options.orgId).eq("name",cleaned.name).limit(2);if(readError)throw new Error(`project_template lookup failed: ${readError.message}`);const existing=existingRows??[];if(existing.length>1){throw new Error(`${existing.length} project_template rows named "${cleaned.name}" for this org \u2014 ambiguous, refusing to write. Ids: ${existing.map(r=>r.id).join(", ")}.`)}const layout=cleaned.layout?{...cleaned.layout,whitelisted_routes:inlineRouteRegistryMeta(cleaned.layout.whitelisted_routes,registry),...cleaned.layout.routes?{routes:inlineRouteRegistryMeta(cleaned.layout.routes,registry)}:{}}:null;const row={org_id:this.options.orgId,name:cleaned.name,description:cleaned.description??null,project_type:cleaned.project_type??null,...cleaned.slug!==void 0?{slug:cleaned.slug}:{},...cleaned.processes!==void 0?{processes:cleaned.processes}:{},...cleaned.audience!==void 0?{audience:cleaned.audience}:{},bundle_name:await lineageExists(this.options.supabase,this.options.orgId,source.bundleName)?source.bundleName:existing[0]?.bundle_name??null,layout,metadata:{...existing[0]?.metadata??{},...cleaned.metadata??{},bundle_refs:cleaned.metadata?.bundle_refs??[bundle.org_slug],template_meta:buildTemplateMeta({sourcePath:relative3(REPO_ROOT,source.path),bundle,componentNames,registry,generator:GENERATOR3})},updated_at:new Date().toISOString()};const outcome=await upsertByNaturalKey(this.options.supabase,"project_template",{org_id:this.options.orgId,name:cleaned.name},row,{dryRun:this.options.dryRun});if(!this.options.dryRun){this.mappings.set("project_template",cleaned.name,outcome.id)}entities.push({entity:`${cleaned.name} (${source.bundleName}/${bundle.version} via ${bundle.resolved_by})`,action:outcome.action,id:outcome.id})}catch(error){errors.push({entity:label,error:error instanceof Error?error.message:String(error)})}}return{layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started}}async healthChecks(){const checks=[];for(const source of this.sources()){if(!existsSync8(source.path))continue;let file;try{file=this.read(source.path)}catch{continue}const routes=file.layout?.whitelisted_routes??[];const referenced=[...new Set(routes.map(r=>r.params?.pageType).filter(t=>Boolean(t)))];if(referenced.length===0)continue;const{data,error}=await this.options.supabase.from("project_page").select("page_type").eq("org_id",this.options.orgId).eq("is_template",true).in("page_type",referenced);if(error){checks.push({name:"route-page-types",severity:"warn",message:`page-type lookup failed: ${error.message}`});continue}const present=new Set((data??[]).map(r=>r.page_type));const absent=referenced.filter(t=>!present.has(t));checks.push(absent.length===0?{name:"route-page-types",severity:"ok",message:`${source.slug}: all ${referenced.length} routed page type(s) have a template`}:{name:"route-page-types",severity:"fail",message:`${source.slug}: ${absent.length} routed page type(s) have NO template row \u2014 those routes render nothing for a client`,detail:absent})}return checks}};var ProjectManager=class extends LayerManager{get shape(){return{fields:{name:{type:"string",required:true,minLength:1},description:{type:"string"},status:{type:"string",default:"active"},project_template_name:{type:"string",required:true,minLength:1},clients:{type:"array",default:[]},metadata:{type:"object"}},ignored:["_comment"]}}naturalKeyOf(def){return def.name}naturalKeyFilter(def){return{org_id:this.options.orgId,name:def.name}}toRow(){throw new Error("ProjectManager writes via syncAll()")}async anyBundleDeployed(){const{data,error}=await this.options.supabase.from("component_bundle").select("id").eq("org_id",this.options.orgId).limit(1);if(error)throw new Error(`component_bundle lookup failed: ${error.message}`);return(data??[]).length>0}async resolveTemplateId(name){const{data,error}=await this.options.supabase.from("project_template").select("id").eq("org_id",this.options.orgId).eq("name",name).limit(2);if(error)throw new Error(`project_template lookup failed: ${error.message}`);const rows=data??[];if(rows.length>1){throw new Error(`${rows.length} project_template rows named "${name}" \u2014 ambiguous.`)}if(!rows[0]){if(!await this.anyBundleDeployed())return null;throw new Error(`no project_template named "${name}" for this org, and this org HAS deployed bundles \u2014 so this is a missing or misnamed template, not an undeployed environment. That row is owned by the project-templates LAYER, not this one:
|
|
12
12
|
npm run template -- sync --layer project-templates --env ${this.options.targetEnv}`)}return rows[0].id}async resolveClientId(reference){const bySlug=await this.options.supabase.from("client").select("id").eq("org_id",this.options.orgId).eq("data->>slug",reference).limit(2);if(bySlug.error)throw new Error(`client lookup failed: ${bySlug.error.message}`);const slugRows=bySlug.data??[];if(slugRows.length>1){throw new Error(`${slugRows.length} clients have data->>slug="${reference}" \u2014 ambiguous.`)}if(slugRows[0])return slugRows[0].id;const byName=await this.options.supabase.from("client").select("id").eq("org_id",this.options.orgId).eq("name",reference).limit(2);if(byName.error)throw new Error(`client lookup failed: ${byName.error.message}`);const nameRows=byName.data??[];if(nameRows.length>1){throw new Error(`${nameRows.length} clients named "${reference}" \u2014 ambiguous.`)}return nameRows[0]?.id??null}async fetchDbKeys(){const{data,error}=await this.options.supabase.from("project").select("id, name").eq("org_id",this.options.orgId);if(error)throw new Error(`project lookup failed: ${error.message}`);const out=new Map;for(const row of data??[]){if(row.name)out.set(row.name,row.id)}return out}async syncAll(){const started=Date.now();const entities=[];const errors=[];if(this.options.targetEnv==="prod"){return{layer:this.def.name,created:0,updated:0,skipped:1,entities:[{entity:"projects",action:"skipped",id:null,reason:"dev fixtures never run against prod"}],errors,duration_ms:Date.now()-started}}for(const file of this.files()){try{const def=this.loadFile(file);const templateId=await this.resolveTemplateId(def.project_template_name);if(templateId===null){entities.push({entity:def.name,action:"skipped",id:null,reason:`project_template "${def.project_template_name}" does not exist yet \u2014 this org has no deployed bundle, so the project-templates layer skipped too. Deploy (upload \u2192 register \u2192 host cutover), re-run project-templates, then re-run this layer.`});continue}const outcome=await upsertByNaturalKey(this.options.supabase,"project",{org_id:this.options.orgId,name:def.name},{org_id:this.options.orgId,name:def.name,description:def.description??null,status:def.status,project_template_id:templateId,metadata:def.metadata??{}},{dryRun:this.options.dryRun});let linked=0;const unresolved=[];for(const reference of def.clients){const clientId=await this.resolveClientId(reference);if(!clientId){unresolved.push(reference);continue}if(this.options.dryRun){linked++;continue}const{data:existing}=await this.options.supabase.from("client_project").select("id").eq("client_id",clientId).eq("project_id",outcome.id).maybeSingle();if(!existing){const{error}=await this.options.supabase.from("client_project").insert({client_id:clientId,project_id:outcome.id,org_id:this.options.orgId});if(error)throw new Error(`client_project insert failed: ${error.message}`)}linked++}if(unresolved.length){console.warn(`[template] \u26A0 project "${def.name}": could not resolve client(s) ${unresolved.join(", ")} \u2014 seed dev-users first. The project exists but those clients cannot open it.`)}if(!this.options.dryRun)this.mappings.set("project",def.name,outcome.id);entities.push({entity:`${def.name} (${linked}/${def.clients.length} client link(s))`,action:outcome.action,id:outcome.id})}catch(error){errors.push({entity:file,error:error instanceof Error?error.message:String(error)})}}return{layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started}}};import{existsSync as existsSync9,readFileSync as readFileSync7,statSync}from"node:fs";import{extname,join as join8,resolve as resolve3}from"node:path";var MIME_TYPES={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".mp4":"video/mp4",".webm":"video/webm",".pdf":"application/pdf",".json":"application/json",".txt":"text/plain",".csv":"text/csv"};var ASSET_SHAPE={fields:{bucket:{type:"string",required:true,minLength:1},entity_type:{type:"any"},root:{type:"enum",values:["layer","repo"],default:"layer"},local_path:{type:"string",required:true,minLength:1},storage_path:{type:"string",required:true,minLength:1},advisor_slug:{type:"string"},description:{type:"string"}}};var StorageAssetManager=class extends LayerManager{get shape(){return{fields:{org_slug:{type:"string",required:true,minLength:1},assets:{type:"array",required:true}},ignored:["_comment"]}}naturalKeyOf(def){return def.org_slug}naturalKeyFilter(){return{}}toRow(){throw new Error("StorageAssetManager uploads files; it writes no table row")}files(){return existsSync9(join8(DATA_DIR,"storage","manifest.json"))?["manifest.json"]:[]}manifest(){return this.loadFile("manifest.json")}assets(){const errors=[];const out=[];const manifest=this.manifest();manifest.assets.forEach((raw,index)=>{const result=validateShape(raw,ASSET_SHAPE);if(!result.ok||!result.value){errors.push(`assets[${index}]: ${result.errors.join("; ")}`);return}out.push(result.value)});return{assets:out,errors}}localFile(asset){return asset.root==="repo"?resolve3(REPO_ROOT,asset.local_path):resolve3(DATA_DIR,"storage",asset.local_path)}resolvePath(asset){if(asset.entity_type==="advisor"){if(!asset.advisor_slug){throw new Error(`advisor_slug required for entity_type=advisor (${asset.storage_path})`)}const advisorId=this.mappings.getId("advisor",asset.advisor_slug);if(!advisorId){throw new Error(`cannot resolve advisor_id for slug "${asset.advisor_slug}" \u2014 seed dev-users first`)}return`${this.options.orgId}/advisors/${advisorId}/${asset.storage_path}`}if(asset.entity_type==="org"||asset.entity_type==="advisor_org"){return`${this.options.orgId}/${asset.storage_path}`}return asset.storage_path}mimeType(filePath){return MIME_TYPES[extname(filePath).toLowerCase()]??"application/octet-stream"}validateAll(){const errors=[];const warnings=[];if(this.files().length===0){return{layer:this.def.name,filesChecked:0,errors,warnings}}let assets=[];try{const parsed=this.assets();assets=parsed.assets;for(const message of parsed.errors)errors.push({file:"manifest.json",message})}catch(error){errors.push({file:"manifest.json",message:error instanceof Error?error.message:String(error)});return{layer:this.def.name,filesChecked:1,errors,warnings}}const destinations=new Map;for(const asset of assets){const local=this.localFile(asset);if(!existsSync9(local)){errors.push({file:"manifest.json",message:`file not found: ${asset.local_path}`});continue}if(statSync(local).size===0){warnings.push({file:"manifest.json",message:`empty file: ${asset.local_path}`})}const destination=`${asset.bucket}:${asset.storage_path}`;const previous=destinations.get(destination);if(previous){errors.push({file:"manifest.json",message:`${destination} is written by both "${previous}" and "${asset.local_path}"`})}else{destinations.set(destination,asset.local_path)}}return{layer:this.def.name,filesChecked:1,errors,warnings}}async syncAll(){const started=Date.now();const entities=[];const errors=[];if(this.files().length===0){return{layer:this.def.name,created:0,updated:0,skipped:0,entities,errors,duration_ms:Date.now()-started}}let assets;try{const parsed=this.assets();assets=parsed.assets;for(const message of parsed.errors)errors.push({entity:"manifest.json",error:message})}catch(error){return{layer:this.def.name,created:0,updated:0,skipped:0,entities,errors:[{entity:"manifest.json",error:error instanceof Error?error.message:String(error)}],duration_ms:Date.now()-started}}const existing=await this.listBucketObjects(assets);for(const asset of assets){const key=`${asset.bucket}:${asset.storage_path}`;try{const local=this.localFile(asset);if(!existsSync9(local)){errors.push({entity:key,error:`file not found: ${asset.local_path}`});continue}const storagePath=this.resolvePath(asset);const alreadyThere=existing.has(`${asset.bucket}:${storagePath}`);if(this.options.dryRun){entities.push({entity:key,action:alreadyThere?"updated":"created",id:storagePath});continue}const{error}=await this.options.supabase.storage.from(asset.bucket).upload(storagePath,readFileSync7(local),{contentType:this.mimeType(local),upsert:true,cacheControl:"3600"});if(error){errors.push({entity:key,error:error.message});continue}this.mappings.set("storage_asset",key,storagePath);entities.push({entity:key,action:alreadyThere?"updated":"created",id:storagePath})}catch(error){errors.push({entity:key,error:error instanceof Error?error.message:String(error)})}}return{layer:this.def.name,created:entities.filter(e=>e.action==="created").length,updated:entities.filter(e=>e.action==="updated").length,skipped:entities.filter(e=>e.action==="skipped").length,entities,errors,duration_ms:Date.now()-started}}async listBucketObjects(assets){const folders=new Set;for(const asset of assets){let resolved;try{resolved=this.resolvePath(asset)}catch{continue}const slash=resolved.lastIndexOf("/");folders.add(`${asset.bucket}:${slash===-1?"":resolved.slice(0,slash)}`)}const found=new Set;for(const folder of folders){const separator=folder.indexOf(":");const bucket=folder.slice(0,separator);const prefix=folder.slice(separator+1);const{data,error}=await this.options.supabase.storage.from(bucket).list(prefix,{limit:1e3});if(error)continue;for(const entry of data??[]){if(entry.name)found.add(`${bucket}:${prefix?`${prefix}/${entry.name}`:entry.name}`)}}return found}async fetchDbKeys(){if(this.files().length===0)return new Map;const{assets}=this.assets();const existing=await this.listBucketObjects(assets);const out=new Map;for(const entry of existing)out.set(entry,entry);return out}async status(){const base={layer:this.def.name,kind:this.def.kind,implemented:this.def.implemented,files:0,dbRows:null,mappedIds:this.mappingEntities().reduce((sum,e)=>sum+this.mappings.count(e),0),missingInDb:[],missingInFiles:[],unmappedInDb:[]};if(this.files().length===0)return base;try{const{assets}=this.assets();base.files=assets.length;const declared=new Map;for(const asset of assets)declared.set(`${asset.bucket}:${this.resolvePath(asset)}`,asset.storage_path);const present=await this.listBucketObjects(assets);base.dbRows=[...declared.keys()].filter(d=>present.has(d)).length;base.missingInDb=[...declared.entries()].filter(([full])=>!present.has(full)).map(([,short])=>short).sort();base.missingInFiles=[...present].filter(p=>!declared.has(p)).sort()}catch(error){base.error=error instanceof Error?error.message:String(error)}return base}async healthChecks(){if(this.files().length===0)return[];const{assets}=this.assets();const declared=new Set;for(const asset of assets){try{declared.add(`${asset.bucket}:${this.resolvePath(asset)}`)}catch{}}const present=await this.listBucketObjects(assets);const missing=[...declared].filter(d=>!present.has(d)).sort();const extra=[...present].filter(p=>!declared.has(p)).sort();const checks=[missing.length===0?{name:"storage-declared",severity:"ok",message:`all ${declared.size} declared asset(s) present in the bucket`}:{name:"storage-declared",severity:"warn",message:`${missing.length} declared asset(s) not in the bucket \u2014 run sync`,detail:missing}];if(extra.length>0){checks.push({name:"storage-unmanaged",severity:"warn",message:`${extra.length} object(s) in the manifest's folders are declared by no asset. Uploaded out of band, or left from an uploader that used different filenames. This layer never deletes.`,detail:extra})}return checks}};var LAYER_ORDER=["org","org-template-layout","org-template-public","event-types","data-types","feed-producers","agent-templates","form-templates","project-templates","page-templates","storage","dev-users","projects","dev-workbooks","dev-client-data"];var LAYERS={org:{name:"org",kind:"data",table:"organization + org_template",naturalKey:"metadata->>org_slug; org_template (org_id, is_archived=false)",mappingEntity:"org",sourceDir:"config",envs:["local","develop","prod"],implemented:true,notes:"Owns identity columns only: org_id, advisor_org_id, is_published/is_archived, metadata.org_slug. name/description are written on INSERT only \u2014 the layout sync owns them after that."},"org-template-layout":{name:"org-template-layout",kind:"data",table:"org_template.*_layout",naturalKey:"metadata->>org_slug (read-modify-write)",mappingEntity:"org_template",sourceDir:null,envs:["local","develop","prod"],implemented:true,notes:"Source stays in src/org_templates/<slug>/template.json (D-T3); config/templates.json names which slug and which bundle lineage. Owns the *_layout columns PRESENT in the file \u2014 an absent one is left untouched. Never creates the row; `sync --layer org` does."},"org-template-public":{name:"org-template-public",kind:"data",table:"org_template_public",naturalKey:"(advisor_org_id, domain_pattern)",mappingEntity:"org_template_public",sourceDir:null,envs:["local","develop","prod"],implemented:true,notes:"Advisor orgs only \u2014 the anonymous hostname\u2192layout lookup. Reads config/public.json for domains/branding and takes public_layout from src/org_templates/<slug>/template.json, so the layout is single-sourced rather than duplicated as the host did."},"event-types":{name:"event-types",kind:"data",table:"event_type",naturalKey:"(org_id, name) \u2014 NOT unique-constrained in the DB",mappingEntity:"event_type",sourceDir:"data/shared/event-types",envs:["local","develop","prod"],implemented:true,notes:"SHARED data (its hashes ship with the engine): org-scoped rows, identical in every org. data/event-types/ holds org-specific additions and is not hashed. category/display_order are in the files but are not columns \u2014 deliberately not written."},"data-types":{name:"data-types",kind:"data",table:"client_data_type",naturalKey:"(org_id, name) \u2014 NOT unique-constrained in the DB",mappingEntity:"client_data_type",sourceDir:"data/data-types",envs:["local","develop","prod"],implemented:true},"feed-producers":{name:"feed-producers",kind:"data",table:"feed_producer",naturalKey:"(org_id, key) \u2014 UNIQUE",mappingEntity:"feed_producer",sourceDir:"data/feed-producers",envs:["local","develop","prod"],implemented:true},"agent-templates":{name:"agent-templates",kind:"data",table:"agent",naturalKey:"(org_id, name, is_template=true)",mappingEntity:"agent",sourceDir:"data/agent-templates",envs:["local","develop","prod"],implemented:true,notes:"Org-scoped rows only \u2014 core (org_id IS NULL) agents stay in BEA (D-T9)."},"form-templates":{name:"form-templates",kind:"data",table:"client_form_template",naturalKey:"(org_id, name) + nested page/section/field keys",mappingEntity:"form_template",sourceDir:"data/form-templates",envs:["local","develop","prod"],implemented:true,notes:"Writes 5 mapping entities (form_template/page/section/field + shared_field). data/shared-fields.json is synced first so `shared: true` fields reuse the canonical uuid."},"project-templates":{name:"project-templates",kind:"data",table:"project_template",naturalKey:"(org_id, name) (read-modify-write)",mappingEntity:"project_template",sourceDir:null,envs:["local","develop","prod"],implemented:true,notes:"Source stays in src/project_templates/<slug>/template.json (D-T3). Writes bundle_name, which the .mjs uploader never did, and resolves registry metadata against the is_live bundle."},"page-templates":{name:"page-templates",kind:"data",table:"project_page",naturalKey:"(org_id, page_type, is_template=true) \u2014 name only when page_type is null",mappingEntity:"project_page",sourceDir:null,envs:["local","develop","prod"],implemented:true,notes:"Every pages/*/page.json under each configured project template, in one run. Replaces scripts/upload-page-template.mjs, which upserted on a freshly generated id and so created a duplicate template row on every re-run."},storage:{name:"storage",kind:"data",table:"storage.objects",naturalKey:"bucket + resolved path (org_id prefix from slug, never hardcoded)",mappingEntity:"storage_asset",sourceDir:"data/storage",envs:["local","develop","prod"],implemented:true,notes:'ONE manifest replaces three disagreeing uploaders. Branding SVGs are single-homed in assets/branding/ via root:"repo" \u2014 never copied into template/data/.'},"dev-users":{name:"dev-users",kind:"seed",table:"auth.users + org_member + advisor/client",naturalKey:"email; (user_id, org_id); (advisor_id, org_id)",mappingEntity:"client",mappingEntities:["advisor"],sourceDir:"seed/dev-users",envs:["local","develop"],implemented:true,notes:"Advisors live in the OTA repo, client users here (D-T4). org_member and advisor have NO unique constraint on (user_id, org_id) \u2014 duplicates break role resolution, so doctor checks."},projects:{name:"projects",kind:"seed",table:"project + client_project",naturalKey:"(org_id, name)",mappingEntity:"project",sourceDir:"seed/projects",envs:["local","develop"],implemented:true,notes:"NEW content for block-plans, not a port \u2014 the legacy fixtures resolved template rows no file recreates (D-T10). The project_template row is owned by the project-templates layer."},"dev-workbooks":{name:"dev-workbooks",kind:"seed",table:"client_form + client_input",naturalKey:"(client_id, template_id); client_input (form_id, field_id)",mappingEntity:null,sourceDir:"seed/dev-workbooks",envs:["local","develop"],implemented:true,notes:"CHECK-THEN-INSERT ONLY \u2014 these rows carry member progress, so an existing workbook is never overwritten (the converger objection in HOST-025/026). Definitions belong to form-templates; this layer only records that a member started one. client_input is APPEND-ONLY with no unique constraint on (form_id, field_id), so existence is checked here, not by the database."},"dev-client-data":{name:"dev-client-data",kind:"seed",table:"client_data",naturalKey:"(client_id, data_type_id, name)",mappingEntity:null,sourceDir:"seed/dev-client-data",envs:["local","develop"],implemented:true,notes:`CHECK-THEN-INSERT ONLY \u2014 client_data is append-only artefact history (the host inserts a new "Client Profile" row per save rather than updating), so a converger here would either revert a member's current profile or delete the history it versions. client_data_type rows belong to the data-types layer and are resolved by exact name, never created.`}};var FACTORIES={org:(options,mappings)=>new OrgManager(LAYERS.org,options,mappings),"org-template-layout":(options,mappings)=>new OrgTemplateLayoutManager(LAYERS["org-template-layout"],options,mappings),"project-templates":(options,mappings)=>new ProjectTemplateManager(LAYERS["project-templates"],options,mappings),"page-templates":(options,mappings)=>new PageTemplateManager(LAYERS["page-templates"],options,mappings),"org-template-public":(options,mappings)=>new OrgTemplatePublicManager(LAYERS["org-template-public"],options,mappings),"event-types":(options,mappings)=>new EventTypeManager(LAYERS["event-types"],options,mappings),"data-types":(options,mappings)=>new DataTypeManager(LAYERS["data-types"],options,mappings),"feed-producers":(options,mappings)=>new FeedProducerManager(LAYERS["feed-producers"],options,mappings),"agent-templates":(options,mappings)=>new AgentTemplateManager(LAYERS["agent-templates"],options,mappings),"form-templates":(options,mappings)=>new FormTemplateManager(LAYERS["form-templates"],options,mappings),storage:(options,mappings)=>new StorageAssetManager(LAYERS.storage,options,mappings),"dev-users":(options,mappings)=>new DevUserManager(LAYERS["dev-users"],options,mappings),projects:(options,mappings)=>new ProjectManager(LAYERS.projects,options,mappings),"dev-workbooks":(options,mappings)=>new DevWorkbookManager(LAYERS["dev-workbooks"],options,mappings),"dev-client-data":(options,mappings)=>new DevClientDataManager(LAYERS["dev-client-data"],options,mappings)};function isLayerName(value){return value in LAYERS}function assertLayerName(value){if(!isLayerName(value)){throw new Error(`unknown layer "${value}". Known layers: ${LAYER_ORDER.join(", ")}. Run \`npm run template -- list\` to see which are ported.`)}return value}function createManager(layer,options,mappings){const factory=FACTORIES[layer];return factory?factory(options,mappings):null}function resolveLayers(owned,options){const ordered=LAYER_ORDER.filter(l=>owned.includes(l));const selected=options.layer?ordered.filter(l=>l===options.layer):ordered;return selected.filter(l=>LAYERS[l].envs.includes(options.targetEnv))}import{existsSync as existsSync10,mkdirSync,readFileSync as readFileSync8,renameSync,writeFileSync}from"node:fs";import{join as join9}from"node:path";var MAPPING_ENTITIES=["org","org_template","org_template_public","client_data_type","event_type","feed_producer","agent","form_template","form_page","form_section","form_field","shared_field","project_template","project_page","project","client","client_form","advisor","storage_asset","process","system","skill"];var ENTITY_SET=new Set(MAPPING_ENTITIES);function isMappingEntity(value){return ENTITY_SET.has(value)}function assertMappingEntity(value){if(!isMappingEntity(value)){throw new Error(`"${value}" is not a canonical mapping entity. Add it to MAPPING_ENTITIES in template/lib/entities.ts rather than inventing a spelling \u2014 two spellings of one entity means two mapping files, and the second one starts empty.`)}return value}function mappingFileName(entity,targetEnv){return`${entity}.mapping.${targetEnv}.json`}var META_KEYS=new Set(["_comment","_generated"]);var MappingStore=class{constructor(mappingsDir,targetEnv,now=()=>new Date().toISOString()){this.mappingsDir=mappingsDir;this.targetEnv=targetEnv;this.now=now}mappingsDir;targetEnv;now;cache=new Map;dirty=new Set;load(entity){const cached=this.cache.get(entity);if(cached)return cached;const path=join9(this.mappingsDir,mappingFileName(entity,this.targetEnv));const data={};if(existsSync10(path)){try{const parsed=JSON.parse(readFileSync8(path,"utf8"));for(const[key,value]of Object.entries(parsed)){if(!META_KEYS.has(key))data[key]=value}}catch(error){throw new Error(`mapping file ${path} is not valid JSON \u2014 fix or delete it before seeding. (${error instanceof Error?error.message:String(error)})`)}}this.cache.set(entity,data);return data}get(entityName,naturalKey){const entity=assertMappingEntity(entityName);return this.load(entity)[naturalKey]}getId(entityName,naturalKey){const value=this.get(entityName,naturalKey);return typeof value==="string"?value:void 0}set(entityName,naturalKey,value){const entity=assertMappingEntity(entityName);if(value===void 0||value===null){throw new Error(`refusing to map ${entity}."${naturalKey}" to ${String(value)}`)}const mapping=this.load(entity);if(mapping[naturalKey]===value)return;mapping[naturalKey]=value;this.dirty.add(entity)}keys(entityName){const entity=assertMappingEntity(entityName);return Object.keys(this.load(entity)).sort()}count(entityName){return this.keys(entityName).length}hasChanges(){return this.dirty.size>0}changedEntities(){return[...this.dirty].sort()}flushedEntities(){return[...this.lastFlushed].sort()}lastFlushed=[];flush(){this.lastFlushed=[...this.dirty].sort();if(this.dirty.size===0)return[];if(!existsSync10(this.mappingsDir))mkdirSync(this.mappingsDir,{recursive:true});const written=[];for(const entity of this.dirty){const mapping=this.cache.get(entity)??{};const ordered={};for(const key of Object.keys(mapping).sort())ordered[key]=mapping[key];const body={_comment:"Auto-generated by template/cli (seed | sync | pull). Commit this file.",_generated:this.now(),...ordered};const path=join9(this.mappingsDir,mappingFileName(entity,this.targetEnv));const temp=`${path}.tmp`;writeFileSync(temp,`${JSON.stringify(body,null,2)}
|
|
13
13
|
`,"utf8");renameSync(temp,path);written.push(path)}this.dirty.clear();return written}};function loadLayersConfig(){const path=join10(CONFIG_DIR,"layers.json");if(!existsSync11(path))return{layers:[...LAYER_ORDER],bundleRefFloor:1};const parsed=JSON.parse(readFileSync9(path,"utf8"));const result=validateShape(parsed,{fields:{layers:{type:"array",required:true},bundle_ref_floor:{type:"number",required:false}},ignored:["_comment"]});if(!result.ok||!result.value){throw new Error(`template/config/layers.json is invalid:
|
|
14
14
|
- ${result.errors.join("\n - ")}`)}return{layers:result.value.layers.map(l=>assertLayerName(String(l))),bundleRefFloor:result.value.bundle_ref_floor??1}}async function buildContext(args){const org=loadOrgConfig();const targetEnv=args.targetEnv;const{layers:ownedLayers,bundleRefFloor}=loadLayersConfig();const mappings=new MappingStore(MAPPINGS_DIR,targetEnv);let supabase=null;let orgId=null;if(args.offline){loadEnv(targetEnv,REPO_ROOT)}else{supabase=createSupabaseClient(targetEnv,REPO_ROOT);const resolved=await findOrgBySlug(supabase,org.slug);orgId=resolved?.id??mappings.getId("org",org.slug)??null}return{args,org,targetEnv,repoRoot:REPO_ROOT,supabase,get orgId(){return orgId},mappings,ownedLayers,bundleRefFloor,async refreshOrgId(){if(!supabase)return null;const resolved=await findOrgBySlug(supabase,org.slug);orgId=resolved?.id??mappings.getId("org",org.slug)??null;return orgId},managerOptions(request={}){if(!supabase)throw new Error("this command needs a database connection (remove --offline)");if(!orgId&&!request.allowMissingOrg){throw new Error(`organization "${org.slug}" does not exist in ${targetEnv}. Run \`npm run template -- seed --env ${targetEnv} --layer org\` to create it, or check that this is the environment you meant.`)}return{supabase,orgSlug:org.slug,orgId:orgId??"",targetEnv,repoRoot:REPO_ROOT,dryRun:args.dryRun,nameFilter:args.name,versionPin:args.version,replaceExisting:args.flags.has("replace-existing")}}}}import{createHash}from"node:crypto";import{existsSync as existsSync12,readdirSync as readdirSync3,readFileSync as readFileSync10,statSync as statSync2}from"node:fs";import{join as join11,relative as relative4}from"node:path";var shared_data_default={files:{"data/shared/event-types/call.json":"068fbc183cafe31337b97567ab44488966ff7f0e9a70201f610fda343ba383d7","data/shared/event-types/meeting.json":"6e244514140faa6e9e62a6cad989d67a9f32f6ff574391d83c5431cbce819791","data/shared/event-types/note.json":"094af621fcc63acd6a59e1f9826239f9a1fbacd0bec85dc8061cfc4e6cd83e82"}};var SHARED_DATA_DIR=join11("data","shared");function walkFiles(dir,keep){if(!existsSync12(dir))return[];const out=[];for(const name of readdirSync3(dir)){const full=join11(dir,name);if(statSync2(full).isDirectory())out.push(...walkFiles(full,keep));else if(keep(name))out.push(relative4(TEMPLATE_ROOT,full).split("\\").join("/"))}return out.sort()}function listSharedDataFiles(){return walkFiles(join11(TEMPLATE_ROOT,SHARED_DATA_DIR),n=>n.endsWith(".json"))}function computeSharedDataHashes(){const out={};for(const f of listSharedDataFiles())out[f]=createHash("sha256").update(readFileSync10(join11(TEMPLATE_ROOT,f))).digest("hex");return out}function sharedDataDriftCheck(){const expected=shared_data_default.files;const actual=computeSharedDataHashes();const problems=[...Object.keys(expected).filter(f=>actual[f]&&actual[f]!==expected[f]).map(f=>`changed: ${f}`),...Object.keys(expected).filter(f=>!actual[f]).map(f=>`missing: ${f}`),...Object.keys(actual).filter(f=>!expected[f]).map(f=>`not in the engine's shared data: ${f}`)];if(problems.length===0){return{name:"shared-data-drift",severity:"ok",message:`all ${Object.keys(actual).length} shared data file(s) match the engine's`}}return{name:"shared-data-drift",severity:"fail",message:`${problems.length} file(s) under template/${SHARED_DATA_DIR} differ from the engine's shared data. Every org seeds the same definitions, so they must be identical; change them in the engine, then here.`,detail:problems}}import{existsSync as existsSync13,readdirSync as readdirSync4,readFileSync as readFileSync11}from"node:fs";import{join as join12}from"node:path";function packageBundleName(){const path=join12(REPO_ROOT,"package.json");if(!existsSync13(path))return null;try{const parsed=JSON.parse(readFileSync11(path,"utf8"));const raw=parsed.oaBundle;const value=typeof raw==="string"?raw:raw?.name;return value?{where:"package.json oaBundle",value}:null}catch{return null}}function configFiles(){const out=[];for(const entry of readdirSync4(REPO_ROOT)){if(/^vite\.config.*\.ts$/.test(entry))out.push(join12(REPO_ROOT,entry))}const harnessDir=join12(REPO_ROOT,"test","visual");if(existsSync13(harnessDir)){for(const entry of readdirSync4(harnessDir)){if(/\.config\.ts$/.test(entry))out.push(join12(harnessDir,entry))}}return out.sort()}function inlinedNamespaces(){const pattern=/__OA_BUNDLE_NAME__\s*:\s*JSON\.stringify\(\s*['"]([^'"]+)['"]\s*\)/g;const out=[];for(const file of configFiles()){let text;try{text=readFileSync11(file,"utf8")}catch{continue}for(const match of text.matchAll(pattern)){out.push({where:file.slice(REPO_ROOT.length+1),value:match[1]})}}return out}function identityChecks(targetEnv){const org=loadOrgConfig();const checks=[];const sources=[packageBundleName(),...inlinedNamespaces()].filter(s=>s!==null);const distinct=[...new Set(sources.map(s=>s.value))];if(sources.length===0){checks.push({name:"identity-bundle-name",severity:"warn",message:'no bundle namespace found in package.json or any vite config \u2014 the theme namespace falls back to "oa-bundle"'})}else if(distinct.length>1){checks.push({name:"identity-bundle-name",severity:"fail",message:`this repo builds ${distinct.length} different [data-oa-bundle] namespaces \u2014 every bundle an org ships must use the same one, or a page mixing them loses styling on all but one`,detail:sources.map(s=>`${s.where}: "${s.value}"`)})}else if(distinct[0]!==org.slug){checks.push({name:"identity-bundle-name",severity:"warn",message:`the bundle namespace is "${distinct[0]}" but this org is "${org.slug}" (${sources.map(s=>s.where).join(", ")}). It only has to be constant across this org's lineages, but matching the slug is how you get that for free. Changing it requires rebuilding and redeploying every lineage together.`})}else{checks.push({name:"identity-bundle-name",severity:"ok",message:`bundle namespace "${distinct[0]}" matches the org slug, consistent across ${sources.length} source(s)`})}const envSlug=process.env.ORG_SLUG??null;const viteSlug=process.env.VITE_ORG_SLUG??null;const mismatched=[...envSlug&&envSlug!==org.slug?[`ORG_SLUG=${envSlug}`]:[],...viteSlug&&viteSlug!==org.slug?[`VITE_ORG_SLUG=${viteSlug}`]:[]];if(mismatched.length>0){checks.push({name:"identity-env-slug",severity:"fail",message:`.env.${targetEnv} names a different org than config/org.json ("${org.slug}"): ${mismatched.join(", ")}. scripts/*.mjs deploy into ORG_SLUG, so bundles and registry rows would land on that org while this CLI configures "${org.slug}".`})}else if(!envSlug){checks.push({name:"identity-env-slug",severity:"warn",message:`.env.${targetEnv} sets no ORG_SLUG, so every deploy script needs --org typed by hand \u2014 which is exactly how a wrong slug gets in. Add ORG_SLUG=${org.slug}.`})}else{checks.push({name:"identity-env-slug",severity:"ok",message:`.env.${targetEnv} ORG_SLUG = "${envSlug}"`})}return checks}function renderSeedReport(report){const lines=[];const mode=report.dryRun?" (dry run \u2014 nothing written)":"";lines.push("");lines.push(`${report.command}: ${report.orgSlug} \u2192 ${report.targetEnv}${mode}`);if(report.orgId)lines.push(` org_id: ${report.orgId}`);lines.push("");for(const layer of report.layers){const counts=`created ${layer.created}, updated ${layer.updated}, skipped ${layer.skipped}`;const status=layer.errors.length>0?"FAILED":"ok";lines.push(` ${layer.layer.padEnd(22)} ${status.padEnd(7)} ${counts} (${layer.duration_ms}ms)`);for(const error of layer.errors){lines.push(` \u2717 ${error.entity}: ${error.error}`)}}const totalErrors=report.layers.reduce((sum,l)=>sum+l.errors.length,0);lines.push("");lines.push(report.success?` \u2713 ${report.layers.length} layer(s) in ${report.totalDuration_ms}ms`:` \u2717 ${totalErrors} error(s) across ${report.layers.length} layer(s)`);lines.push("");return lines.join("\n")}function renderStatus(statuses,header){const lines=["",header,""];lines.push(` ${"layer".padEnd(22)} ${"kind".padEnd(5)} ${"files".padStart(5)} ${"db".padStart(5)} ${"mapped".padStart(6)} state`);for(const s of statuses){const db=s.dbRows===null?"\u2014":String(s.dbRows);const notes=[];if(!s.implemented)notes.push("not ported (host-owned)");if(s.error)notes.push(`ERROR: ${s.error}`);if(s.missingInDb.length)notes.push(`${s.missingInDb.length} file(s) not in DB`);if(s.missingInFiles.length)notes.push(`${s.missingInFiles.length} DB row(s) without a file`);if(s.unmappedInDb.length)notes.push(`${s.unmappedInDb.length} unmapped`);if(!notes.length)notes.push("in sync");lines.push(` ${s.layer.padEnd(22)} ${s.kind.padEnd(5)} ${String(s.files).padStart(5)} ${db.padStart(5)} ${String(s.mappedIds).padStart(6)} ${notes.join("; ")}`)}const detail=statuses.filter(s=>s.missingInDb.length||s.missingInFiles.length||s.unmappedInDb.length);if(detail.length){lines.push("");lines.push(" drift detail");for(const s of detail){if(s.missingInDb.length)lines.push(` ${s.layer} \u2014 in files, not in DB: ${s.missingInDb.join(", ")}`);if(s.missingInFiles.length)lines.push(` ${s.layer} \u2014 in DB, no file: ${s.missingInFiles.join(", ")}`);if(s.unmappedInDb.length)lines.push(` ${s.layer} \u2014 unmapped: ${s.unmappedInDb.join(", ")}`)}}lines.push("");return lines.join("\n")}function renderValidation(results){const lines=[""];let errors=0;for(const result of results){const count=result.errors.length;errors+=count;const state=count===0?"ok":`${count} error(s)`;lines.push(` ${result.layer.padEnd(22)} ${String(result.filesChecked).padStart(3)} file(s) ${state}`);for(const issue of result.errors)lines.push(` \u2717 ${issue.file}: ${issue.message}`);for(const issue of result.warnings)lines.push(` \u26A0 ${issue.file}: ${issue.message}`)}lines.push("");lines.push(errors===0?" \u2713 all files valid":` \u2717 ${errors} error(s)`);lines.push("");return lines.join("\n")}function emit(json,payload,human){console.log(json?JSON.stringify(payload,null,2):human)}async function checkAdvisorLinkage(ctx,supabase){const org=await findOrgBySlug(supabase,ctx.org.slug);if(!org){return{name:"advisor-linkage",severity:"fail",message:`organization "${ctx.org.slug}" not found in ${ctx.targetEnv}`}}if(ctx.org.advisor){return org.advisor?{name:"advisor-linkage",severity:"ok",message:"advisor org, flag set correctly"}:{name:"advisor-linkage",severity:"fail",message:"config says advisor org but organization.advisor is false"}}const expected=ctx.org.advisor_org_slug;if(!expected){return{name:"advisor-linkage",severity:"ok",message:"client org with no advisor configured"}}const advisorOrg=await findOrgBySlug(supabase,expected);if(!advisorOrg){return{name:"advisor-linkage",severity:"warn",message:`advisor org "${expected}" does not exist in ${ctx.targetEnv} yet \u2014 seed it first`}}if(org.advisorOrgId!==advisorOrg.id){return{name:"advisor-linkage",severity:"fail",message:`organization.metadata.advisor_org_id is ${org.advisorOrgId??"null"} but "${expected}" is ${advisorOrg.id} \u2014 advisor-scoped lookups will return nothing`}}return{name:"advisor-linkage",severity:"ok",message:`linked to ${expected} (${advisorOrg.id})`}}async function checkDuplicateDataTypes(ctx,supabase){const{data,error}=await supabase.from("client_data_type").select("id, name").eq("org_id",ctx.orgId);if(error){return{name:"duplicate-rows",severity:"warn",message:`lookup failed: ${error.message}`}}const byName=new Map;for(const row of data??[]){byName.set(row.name,[...byName.get(row.name)??[],row.id])}const dupes=[...byName.entries()].filter(([,ids])=>ids.length>1);return dupes.length===0?{name:"duplicate-rows",severity:"ok",message:`client_data_type: ${byName.size} distinct name(s), no duplicates`}:{name:"duplicate-rows",severity:"fail",message:`client_data_type has ${dupes.length} duplicated name(s) \u2014 sync will refuse to write them`,detail:dupes.map(([name,ids])=>`${name}: ${ids.join(", ")}`)}}async function checkDuplicateMemberships(ctx,supabase){const detail=[];const{data:members,error:memberError}=await supabase.from("org_member").select("id, user_id").eq("org_id",ctx.orgId);if(memberError){return{name:"duplicate-memberships",severity:"warn",message:`org_member lookup failed: ${memberError.message}`}}const memberCounts=new Map;for(const row of members??[]){memberCounts.set(row.user_id,(memberCounts.get(row.user_id)??0)+1)}for(const[userId,count]of memberCounts){if(count>1)detail.push(`org_member: user ${userId} has ${count} rows`)}const{data:advisors,error:advisorError}=await supabase.from("advisor").select("id, user_id").eq("org_id",ctx.orgId);if(!advisorError){const advisorCounts=new Map;for(const row of advisors??[]){advisorCounts.set(row.user_id,(advisorCounts.get(row.user_id)??0)+1)}for(const[userId,count]of advisorCounts){if(count>1)detail.push(`advisor: user ${userId} has ${count} rows`)}}return detail.length===0?{name:"duplicate-memberships",severity:"ok",message:`${memberCounts.size} org member(s), no duplicate rows`}:{name:"duplicate-memberships",severity:"fail",message:"duplicate membership rows \u2014 role resolution uses .maybeSingle() and will throw, hanging login on a portal\u2192platform redirect loop",detail}}async function checkLiveBundle(ctx,supabase){const{data:bundles,error:bundleError}=await supabase.from("component_bundle").select("id, version, bundle_name, is_live, status").eq("org_id",ctx.orgId).eq("bundle_name",ctx.org.bundle_name).eq("is_live",true);if(bundleError){return{name:"live-bundle",severity:"warn",message:`lookup failed: ${bundleError.message}`}}const live=bundles??[];if(live.length===0){return{name:"live-bundle",severity:"warn",message:`no is_live bundle in lineage "${ctx.org.bundle_name}" \u2014 the host has nothing to serve`}}if(live.length>1){return{name:"live-bundle",severity:"fail",message:`${live.length} bundles are is_live in lineage "${ctx.org.bundle_name}" \u2014 exactly one may be`,detail:live.map(b=>b.version)}}const liveVersion=live[0].version;const{data:templates,error:templateError}=await supabase.from("org_template").select("id, remote_version").eq("metadata->>org_slug",ctx.org.slug);if(templateError){return{name:"live-bundle",severity:"warn",message:`is_live=${liveVersion}; org_template lookup failed: ${templateError.message}`}}const rows=templates??[];const pointer=rows[0]?.remote_version??null;if(pointer&&pointer!==liveVersion){return{name:"live-bundle",severity:"warn",message:`org_template.remote_version=${pointer} but is_live=${liveVersion}. The host serves the is_live row. Reconcile from THIS repo:
|