@peripheries/engine 0.5.0 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peripheries/engine",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Build, check, knowledge and deploy for peripheries component repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{spawnSync}from"node:child_process";import{existsSync}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{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;const external=[...EXTERNALS,...config.shared];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}"`);console.log(`
2
+ import{spawnSync}from"node:child_process";import{existsSync}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{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
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:{"@":resolve(REPO_ROOT,"src")}},optimizeDeps:{exclude:external},build:{lib:{entry,formats:["es"],fileName:()=>"index.js"},rollupOptions:{external},outDir:resolve(REPO_ROOT,lineage.dist),emptyOutDir:true,sourcemap:true}});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(", ")}`);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const VERSION=parseFlagValue(argv,"version")??process.env.VERSION??null;if(!VERSION){console.error("[cutover] --version <v> is required.");process.exit(2)}const DRY_RUN=argv.includes("--dry-run");const explicitBundle=parseFlagValue(argv,"bundle");const wantsPublic=argv.includes("--public");if(wantsPublic&&explicitBundle&&explicitBundle!=="public"){console.error(`[cutover] --public conflicts with --bundle ${explicitBundle}. Pass one.`);process.exit(2)}const MAIN_LINEAGE="main";const PUBLIC_LINEAGE="public";const BUNDLE_NAME=explicitBundle??(wantsPublic?PUBLIC_LINEAGE:MAIN_LINEAGE);console.log(`
2
+ import{lineageConfig}from"../lib/config.mjs";import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const VERSION=parseFlagValue(argv,"version")??process.env.VERSION??null;if(!VERSION){console.error("[cutover] --version <v> is required.");process.exit(2)}const DRY_RUN=argv.includes("--dry-run");const explicitBundle=parseFlagValue(argv,"bundle");const wantsPublic=argv.includes("--public");if(wantsPublic&&explicitBundle&&explicitBundle!=="public"){console.error(`[cutover] --public conflicts with --bundle ${explicitBundle}. Pass one.`);process.exit(2)}const MAIN_LINEAGE="main";const PUBLIC_LINEAGE="public";const BUNDLE_NAME=explicitBundle??(wantsPublic?PUBLIC_LINEAGE:MAIN_LINEAGE);console.log(`
3
3
  \u{1F3AF} cutover \u2014 env=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}
4
4
  `);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[cutover] Could not resolve org "${ORG_SLUG}":`,orgError?.message??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;console.log(` org_id=${ORG_ID}`);const{data:bundleRow,error:bundleError}=await supabase.from("component_bundle").select("id, status, exports").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(bundleError){console.error("[cutover] Failed to fetch component_bundle:",bundleError.message);process.exit(1)}if(!bundleRow){console.error(`[cutover] No component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}). Has upload-bundle.mjs + register-components.mjs been run for this version?`);process.exit(1)}const BUNDLE_ID=bundleRow.id;console.log(` bundle_id=${BUNDLE_ID} status='${bundleRow.status}' exports=${bundleRow.exports?.length??0}`);if(bundleRow.status!=="registered"){console.error(`[cutover] Bundle is in status='${bundleRow.status}'. cutover requires 'registered'.
5
5
  `+(bundleRow.status==="uploaded"?" \u2192 Run register-components.mjs for this version first.\n":bundleRow.status==="cutover"?" \u2192 This bundle is already active. Target a different version to roll forward/back.\n":" \u2192 Investigate the bundle row directly.\n"));process.exit(1)}await warnLiveDrift();if(BUNDLE_NAME===PUBLIC_LINEAGE){await cutoverPublic()}else if(BUNDLE_NAME!==MAIN_LINEAGE){await cutoverProject()}else{await cutoverMain()}async function cutoverMain(){const{data:templateRow,error:templateError}=await supabase.from("org_template").select("id, remote_version, remote_url, previous_version").eq("org_id",ORG_ID).eq("is_archived",false).single();if(templateError||!templateRow?.id){console.error(`[cutover] No active org_template for org "${ORG_SLUG}":`,templateError?.message??"not found");process.exit(1)}let previousBundleId=null;if(templateRow.remote_version){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",MAIN_LINEAGE).eq("version",templateRow.remote_version).maybeSingle();previousBundleId=prev?.id??null}const newRemoteUrl=`/api/remotes/${ORG_SLUG}/${VERSION}/index.js`;console.log(`
@@ -8,12 +8,12 @@ import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../li
8
8
  `)}async function cutoverPublic(){const{data:templateRow,error:templateError}=await supabase.from("org_template").select("id, public_remote_version").eq("org_id",ORG_ID).eq("is_archived",false).single();if(templateError||!templateRow?.id){console.error(`[cutover] No active org_template for org "${ORG_SLUG}":`,templateError?.message??"not found");process.exit(1)}let previousBundleId=null;if(templateRow.public_remote_version){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",PUBLIC_LINEAGE).eq("version",templateRow.public_remote_version).maybeSingle();previousBundleId=prev?.id??null}console.log(`
9
9
  org_template (public lineage):`);console.log(` public_remote_version: ${templateRow.public_remote_version??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_public: false \u2192 true`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` component_bundle_lineage (public):`);console.log(` active_version: ${templateRow.public_remote_version??"(none)"} \u2192 ${VERSION}`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,is_public:true,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}const{error:templateUpdateError}=await supabase.from("org_template").update({public_remote_version:VERSION,updated_at:nowIso}).eq("id",templateRow.id);if(templateUpdateError){console.error("[cutover] Failed to update org_template.public_remote_version:",templateUpdateError.message);process.exit(1)}await setLiveBundle(PUBLIC_LINEAGE);await writeLineagePointer({bundleName:PUBLIC_LINEAGE,previousVersion:templateRow.public_remote_version??null,storageBucket:PUBLIC_BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
10
10
  \u2705 Promoted public bundle ${BUNDLE_ID} for org "${ORG_SLUG}".
11
- `)}async function cutoverProject(){const{data:lineage}=await supabase.from("component_bundle_lineage").select("active_version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).maybeSingle();const previousVersion=lineage?.active_version??null;let previousBundleId=null;if(previousVersion){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",previousVersion).maybeSingle();previousBundleId=prev?.id??null}console.log(`
12
- component_bundle_lineage (${BUNDLE_NAME}):`);console.log(` active_version: ${previousVersion??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` org_template: (untouched \u2014 project lineages have no org-wide pointer)`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}await setLiveBundle(BUNDLE_NAME);await writeLineagePointer({bundleName:BUNDLE_NAME,previousVersion,storageBucket:BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
11
+ `)}async function cutoverProject(){const LINEAGE=await lineageConfig(BUNDLE_NAME);const shared=LINEAGE.kind==="shared"?{specifier:LINEAGE.specifier,audience:"platform"}:null;const{data:lineage}=await supabase.from("component_bundle_lineage").select("active_version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).maybeSingle();const previousVersion=lineage?.active_version??null;let previousBundleId=null;if(previousVersion){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",previousVersion).maybeSingle();previousBundleId=prev?.id??null}console.log(`
12
+ component_bundle_lineage (${BUNDLE_NAME}):`);console.log(` active_version: ${previousVersion??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` org_template: (untouched \u2014 project lineages have no org-wide pointer)`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,previous_bundle_id:previousBundleId,...shared?{is_public:true}:{}}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}await setLiveBundle(BUNDLE_NAME);await writeLineagePointer({bundleName:BUNDLE_NAME,previousVersion,storageBucket:shared?PUBLIC_BUNDLE_BUCKET:BUNDLE_BUCKET,extra:shared??{}});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
13
13
  \u2705 Promoted "${BUNDLE_NAME}" bundle ${BUNDLE_ID} for org "${ORG_SLUG}" to version ${VERSION}.
14
14
  `);console.log(` Point a template at it with project_template.bundle_name = '${BUNDLE_NAME}'
15
15
  (upload-project-template.mjs writes this from template.json's "bundle_name" key).
16
- `)}async function setLiveBundle(bundleName){const{error:clearError}=await supabase.from("component_bundle").update({is_live:false}).eq("org_id",ORG_ID).eq("bundle_name",bundleName).eq("is_live",true);if(clearError){console.error(`[cutover] Failed to clear is_live for lineage "${bundleName}":`,clearError.message);process.exit(1)}const{error:setError}=await supabase.from("component_bundle").update({is_live:true}).eq("id",BUNDLE_ID);if(setError){console.error(`[cutover] Cleared the previous live bundle but failed to set is_live on ${BUNDLE_ID}:`,setError.message);console.error(` Lineage "${bundleName}" now has NO live bundle. Fix with:`);console.error(` update component_bundle set is_live = true where id = '${BUNDLE_ID}';`);process.exit(1)}}async function writeLineagePointer({bundleName,previousVersion,storageBucket}){const{error}=await supabase.from("component_bundle_lineage").upsert({org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:bundleName,active_version:VERSION,previous_version:previousVersion,storage_bucket:storageBucket,updated_at:new Date().toISOString()},{onConflict:"org_id,bundle_name"});if(error){console.error("[cutover] Failed to write component_bundle_lineage:",error.message);console.error(` The bundle status was updated but the lineage pointer was not \u2014 a project_template
16
+ `)}async function setLiveBundle(bundleName){const{error:clearError}=await supabase.from("component_bundle").update({is_live:false}).eq("org_id",ORG_ID).eq("bundle_name",bundleName).eq("is_live",true);if(clearError){console.error(`[cutover] Failed to clear is_live for lineage "${bundleName}":`,clearError.message);process.exit(1)}const{error:setError}=await supabase.from("component_bundle").update({is_live:true}).eq("id",BUNDLE_ID);if(setError){console.error(`[cutover] Cleared the previous live bundle but failed to set is_live on ${BUNDLE_ID}:`,setError.message);console.error(` Lineage "${bundleName}" now has NO live bundle. Fix with:`);console.error(` update component_bundle set is_live = true where id = '${BUNDLE_ID}';`);process.exit(1)}}async function writeLineagePointer({bundleName,previousVersion,storageBucket,extra={}}){const{error}=await supabase.from("component_bundle_lineage").upsert({org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:bundleName,active_version:VERSION,previous_version:previousVersion,storage_bucket:storageBucket,...extra,updated_at:new Date().toISOString()},{onConflict:"org_id,bundle_name"});if(error){console.error("[cutover] Failed to write component_bundle_lineage:",error.message);console.error(` The bundle status was updated but the lineage pointer was not \u2014 a project_template
17
17
  referencing "${bundleName}" will fail its FK until this row exists.`);process.exit(1)}}async function retirePreviousBundle(previousBundleId,nowIso){if(!previousBundleId||previousBundleId===BUNDLE_ID)return;const{error}=await supabase.from("component_bundle").update({status:"rolled_back",rolled_back_at:nowIso}).eq("id",previousBundleId).eq("status","cutover");if(error){console.warn(`[cutover] Promoted, but failed to retire previous bundle ${previousBundleId}:`,error.message)}}async function warnLiveDrift(){const{data:drifted,error}=await supabase.from("component_bundle").select("version, cutover_at").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("status","cutover").eq("is_live",false).order("cutover_at",{ascending:false});if(error){console.warn(`[cutover] Could not check for is_live drift: ${error.message}`);return}if(!drifted?.length)return;const sample=drifted.slice(0,5).map(r=>`${r.version} (cutover_at=${r.cutover_at??"null"})`).join(", ");console.warn(`
18
18
  \u26A0\uFE0F ${drifted.length} bundle(s) in lineage "${BUNDLE_NAME}" have status='cutover' but is_live=false:
19
19
  ${sample}${drifted.length>5?", \u2026":""}
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync,existsSync,statSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";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 DEFAULT_BUCKET=IS_PUBLIC?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;async function uploadOne(localPath,remotePath,contentType){const body=readFileSync(localPath);if(dryRun){console.log(`[upload-bundle] would upload ${STORAGE_BUCKET}/${remotePath} (${body.byteLength} bytes)`);return}const{error}=await supabase.storage.from(STORAGE_BUCKET).upload(remotePath,body,{contentType,upsert:false});if(error){console.error(`[upload-bundle] Failed to upload ${remotePath}:`,error);process.exit(1)}console.log(`[upload-bundle] \u2191 ${remotePath} (${body.byteLength} bytes)`)}const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;await uploadOne(bundlePath,`${remoteBase}/index.js`,"application/javascript");const mapPath=resolve(distDir,"index.js.map");if(existsSync(mapPath)){await uploadOne(mapPath,`${remoteBase}/index.js.map`,"application/json")}const contractsPath=resolve(distDir,"contracts.json");if(existsSync(contractsPath)){await uploadOne(contractsPath,`${remoteBase}/contracts.json`,"application/json")}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`,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,...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.`);
2
+ import{readFileSync,existsSync,statSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";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;async function uploadOne(localPath,remotePath,contentType){const body=readFileSync(localPath);if(dryRun){console.log(`[upload-bundle] would upload ${STORAGE_BUCKET}/${remotePath} (${body.byteLength} bytes)`);return}const{error}=await supabase.storage.from(STORAGE_BUCKET).upload(remotePath,body,{contentType,upsert:false});if(error){console.error(`[upload-bundle] Failed to upload ${remotePath}:`,error);process.exit(1)}console.log(`[upload-bundle] \u2191 ${remotePath} (${body.byteLength} bytes)`)}const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;await uploadOne(bundlePath,`${remoteBase}/index.js`,"application/javascript");const mapPath=resolve(distDir,"index.js.map");if(existsSync(mapPath)){await uploadOne(mapPath,`${remoteBase}/index.js.map`,"application/json")}const contractsPath=resolve(distDir,"contracts.json");if(existsSync(contractsPath)){await uploadOne(contractsPath,`${remoteBase}/contracts.json`,"application/json")}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,...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
@@ -1,4 +1,4 @@
1
- export type LineageKind = 'main' | 'public' | 'page' | 'form';
1
+ export type LineageKind = 'main' | 'public' | 'page' | 'form' | 'shared';
2
2
 
3
3
  export type LineageSpec =
4
4
  | string
@@ -8,6 +8,8 @@ export type LineageSpec =
8
8
  entry?: string;
9
9
  sourceRoot?: string | null;
10
10
  dist?: string;
11
+ specifier?: string;
12
+ shared?: string[];
11
13
  build?: { script: string; env?: Record<string, string> };
12
14
  };
13
15
 
@@ -37,6 +39,8 @@ export type Lineage = {
37
39
  sourceRoot: string | null;
38
40
  dist: string;
39
41
  build?: { script: string; env?: Record<string, string> };
42
+ specifier?: string;
43
+ shared?: string[];
40
44
  };
41
45
 
42
46
  export const DEFAULT_CHECKS: string[];
@@ -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","knowledge"];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}/`:`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}});if(lineages[0]?.name!==MAIN_LINEAGE)throw new Error(`[engine] ${origin}: lineages must start with "main"`);const gated=raw.checks?.gated??{};const checks={run:raw.checks?.run??DEFAULT_CHECKS,peerRepos:raw.checks?.peerRepos??[],gated:{projectTemplates:gated.projectTemplates??[],orgTemplates:gated.orgTemplates??[]}};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,harness}}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,defineConfig,lineageConfig,lineageDefaults,loadConfig};
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","knowledge"];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??[]}};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,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,defineConfig,lineageConfig,lineageDefaults,loadConfig,normalizeForTest};
@@ -1,4 +1,4 @@
1
- import{existsSync,readdirSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toDisplayName}from"./jsdoc-parser.mjs";import{pathToFileURL}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"./root.mjs";function registryOf(entry){const out={};for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(file&&hasComponentJSDoc(file,name))out[name]={props_schema:parseJSDocFile(file,name).props_schema??void 0}}return out}const repoRoot=REPO_ROOT_;function readDoc(path){const text=readFileSync(path,"utf8");const m=text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);if(!m)throw new Error(`[knowledge] ${path} has no front matter`);const meta={};for(const line of m[1].split("\n")){const kv=line.match(/^([\w-]+):\s*(.*)$/);if(!kv)continue;const raw=kv[2].trim();try{meta[kv[1]]=JSON.parse(raw)}catch{meta[kv[1]]=raw}}return{meta,body:m[2].trim()}}const docsIn=dir=>existsSync(resolve(repoRoot,dir))?readdirSync(resolve(repoRoot,dir)).filter(f=>f.endsWith(".md")).sort().map(f=>({key:f.replace(/\.md$/,""),...readDoc(resolve(repoRoot,dir,f))})):[];const str=v=>typeof v==="string"?v:void 0;const strs=v=>Array.isArray(v)?v.map(String):[];function oneLine(text,fallback){const first=(text??"").split(/(?<=[.!?])\s+/)[0]?.trim()||fallback;if(first.length<=200)return first;return`${first.slice(0,199).replace(/\s+\S*$/,"")}\u2026`}function isPlainJson(v){if(v===null||["string","number","boolean"].includes(typeof v))return true;if(Array.isArray(v))return v.every(isPlainJson);if(typeof v!=="object"||Object.getPrototypeOf(v)!==Object.prototype||"$$typeof"in v)return false;return Object.values(v).every(x=>x===void 0||isPlainJson(x))}async function fixtureExamples(componentFile){const base=componentFile.replace(/\.tsx?$/,"");const file=[".fixtures.ts",".fixtures.tsx"].map(ext=>base+ext).find(f=>existsSync(f));if(!file)return[];const mod=await import(file);return(mod.scenarios??[]).filter(s=>["cell","whole-page"].includes(s.surface??"cell")&&s.props&&Object.keys(s.props).length>0&&isPlainJson(s.props)).map(s=>({name:s.name,props:JSON.parse(JSON.stringify(s.props))})).filter(s=>JSON.stringify(s.props).length<4e3).sort((a,b)=>JSON.stringify(a.props).length-JSON.stringify(b.props).length).slice(0,3)}const holdsReference=v=>typeof v==="string"&&(v.startsWith("@")||v.includes("${"))||(Array.isArray(v)?v.some(holdsReference):typeof v==="object"&&v!==null&&Object.values(v).some(holdsReference));async function loadVocabulary(){const file=["src/catalog/vocabulary.ts","src/catalog/vocabulary.tsx"].map(f=>resolve(repoRoot,f)).find(f=>existsSync(f));if(!file)return{};return(await import(pathToFileURL(file).href)).VOCABULARY??{}}async function buildKnowledge(bundle,org){const VOCABULARY=await loadVocabulary();const lineage=`${org}/${bundle}`;const entry=bundle==="main"?"src/index.ts":`src/index.${bundle}.ts`;const scopeLineage=`${org}/main`;const scopeRegistry=bundle==="main"?void 0:registryOf("src/index.ts");const bundleDocPath=resolve(repoRoot,"src/catalog",`${bundle}.md`);if(!existsSync(bundleDocPath))throw new Error(`[knowledge] no src/catalog/${bundle}.md: every lineage says how to compose a page from it.`);const bundleDoc=readDoc(bundleDocPath);const kits=docsIn("src/catalog/kits").filter(k=>(str(k.meta.bundle)??"main")===bundle);const patterns=docsIn("src/catalog/patterns").filter(p=>(str(p.meta.bundle)??"main")===bundle);const notPageCellFamilies=new Set(strs(bundleDoc.meta.not_page_cell_families));const pageCellExceptions=new Set(strs(bundleDoc.meta.page_cells));const notPageCells=new Set(strs(bundleDoc.meta.not_page_cells));const rows=[];const links=[];const registry={};rows.push({kind:"bundle",key:bundle,title:str(bundleDoc.meta.title)??lineage,summary:str(bundleDoc.meta.summary)??"",body:bundleDoc.body,attributes:{kits:kits.map(k=>k.key),patterns:patterns.map(p=>p.key)}});for(const kit of kits){const vocabulary=str(kit.meta.vocabulary);if(vocabulary&&!VOCABULARY[vocabulary])throw new Error(`[knowledge] kit ${kit.key} names vocabulary "${vocabulary}", which has no generator`);rows.push({kind:"kit",key:kit.key,title:str(kit.meta.title)??kit.key,summary:str(kit.meta.summary)??"",body:kit.body,tags:strs(kit.meta.tags),sort:Number(kit.meta.sort??0),attributes:{families:strs(kit.meta.families),...vocabulary?{vocabulary:VOCABULARY[vocabulary]()}:{}}})}const kitOfFamily=new Map(kits.flatMap(k=>strs(k.meta.families).map(f=>[f,k.key])));const cellId=name=>name.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!file||!hasComponentJSDoc(file,name))continue;const j=parseJSDocFile(file,name);registry[name]={props_schema:j.props_schema??void 0};const family=j.family??"other";const pageCell=j.page_cell??(notPageCells.has(name)?false:pageCellExceptions.has(name)||!notPageCellFamilies.has(family));const weight=e=>(holdsReference(e)?1e6:0)+JSON.stringify(e).length;const examples=[...j.usage_examples??[]].sort((a,b)=>weight(a)-weight(b));const snippets=examples.length?examples.map((props,i)=>({title:`${name}, example ${i+1}`,cells:[{id:cellId(name),component:name,props}]})):(await fixtureExamples(file)).map(f=>({title:`${name}, ${f.name}`,cells:[{id:cellId(name),component:name,props:f.props}]}));const useCases=j.use_cases?.length?`
1
+ import{existsSync,readdirSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toDisplayName}from"./jsdoc-parser.mjs";import{pathToFileURL}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"./root.mjs";function registryOf(entry){const out={};for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(file&&hasComponentJSDoc(file,name))out[name]={props_schema:parseJSDocFile(file,name).props_schema??void 0}}return out}const repoRoot=REPO_ROOT_;function readDoc(path){const text=readFileSync(path,"utf8");const m=text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);if(!m)throw new Error(`[knowledge] ${path} has no front matter`);const meta={};for(const line of m[1].split("\n")){const kv=line.match(/^([\w-]+):\s*(.*)$/);if(!kv)continue;const raw=kv[2].trim();try{meta[kv[1]]=JSON.parse(raw)}catch{meta[kv[1]]=raw}}return{meta,body:m[2].trim()}}const docsIn=dir=>existsSync(resolve(repoRoot,dir))?readdirSync(resolve(repoRoot,dir)).filter(f=>f.endsWith(".md")).sort().map(f=>({key:f.replace(/\.md$/,""),...readDoc(resolve(repoRoot,dir,f))})):[];const str=v=>typeof v==="string"?v:void 0;const strs=v=>Array.isArray(v)?v.map(String):[];function oneLine(text,fallback){const first=(text??"").split(/(?<=[.!?])\s+/)[0]?.trim()||fallback;if(first.length<=200)return first;return`${first.slice(0,199).replace(/\s+\S*$/,"")}\u2026`}function isPlainJson(v){if(v===null||["string","number","boolean"].includes(typeof v))return true;if(Array.isArray(v))return v.every(isPlainJson);if(typeof v!=="object"||Object.getPrototypeOf(v)!==Object.prototype||"$$typeof"in v)return false;return Object.values(v).every(x=>x===void 0||isPlainJson(x))}async function fixtureExamples(componentFile){const base=componentFile.replace(/\.tsx?$/,"");const file=[".fixtures.ts",".fixtures.tsx"].map(ext=>base+ext).find(f=>existsSync(f));if(!file)return[];const mod=await import(file);return(mod.scenarios??[]).filter(s=>["cell","whole-page"].includes(s.surface??"cell")&&s.props&&Object.keys(s.props).length>0&&isPlainJson(s.props)).map(s=>({name:s.name,props:JSON.parse(JSON.stringify(s.props))})).filter(s=>JSON.stringify(s.props).length<4e3).sort((a,b)=>JSON.stringify(a.props).length-JSON.stringify(b.props).length).slice(0,3)}const holdsReference=v=>typeof v==="string"&&(v.startsWith("@")||v.includes("${"))||(Array.isArray(v)?v.some(holdsReference):typeof v==="object"&&v!==null&&Object.values(v).some(holdsReference));async function loadVocabulary(){const file=["src/catalog/vocabulary.ts","src/catalog/vocabulary.tsx"].map(f=>resolve(repoRoot,f)).find(f=>existsSync(f));if(!file)return{};return(await import(pathToFileURL(file).href)).VOCABULARY??{}}async function buildKnowledge(bundle,org){const VOCABULARY=await loadVocabulary();const lineage=`${org}/${bundle}`;const entry=bundle==="main"?"src/index.ts":`src/index.${bundle}.ts`;const scopeLineage=`${org}/main`;const scopeRegistry=bundle==="main"||!existsSync(resolve(repoRoot,"src/index.ts"))?void 0:registryOf("src/index.ts");const bundleDocPath=resolve(repoRoot,"src/catalog",`${bundle}.md`);if(!existsSync(bundleDocPath))throw new Error(`[knowledge] no src/catalog/${bundle}.md: every lineage says how to compose a page from it.`);const bundleDoc=readDoc(bundleDocPath);const kits=docsIn("src/catalog/kits").filter(k=>(str(k.meta.bundle)??"main")===bundle);const patterns=docsIn("src/catalog/patterns").filter(p=>(str(p.meta.bundle)??"main")===bundle);const notPageCellFamilies=new Set(strs(bundleDoc.meta.not_page_cell_families));const pageCellExceptions=new Set(strs(bundleDoc.meta.page_cells));const notPageCells=new Set(strs(bundleDoc.meta.not_page_cells));const rows=[];const links=[];const registry={};rows.push({kind:"bundle",key:bundle,title:str(bundleDoc.meta.title)??lineage,summary:str(bundleDoc.meta.summary)??"",body:bundleDoc.body,attributes:{kits:kits.map(k=>k.key),patterns:patterns.map(p=>p.key)}});for(const kit of kits){const vocabulary=str(kit.meta.vocabulary);if(vocabulary&&!VOCABULARY[vocabulary])throw new Error(`[knowledge] kit ${kit.key} names vocabulary "${vocabulary}", which has no generator`);rows.push({kind:"kit",key:kit.key,title:str(kit.meta.title)??kit.key,summary:str(kit.meta.summary)??"",body:kit.body,tags:strs(kit.meta.tags),sort:Number(kit.meta.sort??0),attributes:{families:strs(kit.meta.families),...vocabulary?{vocabulary:VOCABULARY[vocabulary]()}:{}}})}const kitOfFamily=new Map(kits.flatMap(k=>strs(k.meta.families).map(f=>[f,k.key])));const cellId=name=>name.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!file||!hasComponentJSDoc(file,name))continue;const j=parseJSDocFile(file,name);registry[name]={props_schema:j.props_schema??void 0};const family=j.family??"other";const pageCell=j.page_cell??(notPageCells.has(name)?false:pageCellExceptions.has(name)||!notPageCellFamilies.has(family));const weight=e=>(holdsReference(e)?1e6:0)+JSON.stringify(e).length;const examples=[...j.usage_examples??[]].sort((a,b)=>weight(a)-weight(b));const snippets=examples.length?examples.map((props,i)=>({title:`${name}, example ${i+1}`,cells:[{id:cellId(name),component:name,props}]})):(await fixtureExamples(file)).map(f=>({title:`${name}, ${f.name}`,cells:[{id:cellId(name),component:name,props:f.props}]}));const useCases=j.use_cases?.length?`
2
2
 
3
3
  Use it for:
4
4
  ${j.use_cases.map(u=>`- ${u}`).join("\n")}`:"";rows.push({kind:"component",key:name,title:toDisplayName(name),summary:oneLine(j.summary??j.description,`${toDisplayName(name)}.`),body:`${j.agent_instructions??j.description??""}${useCases}`.trim()||null,tags:j.tags??[],snippets,page_cell:pageCell,attributes:{family,...j.print_policy||j.size?{capabilities:{...j.print_policy?{print_policy:j.print_policy}:{},...j.size?{size:true}:{}}}:{}}});const kit=j.kit??kitOfFamily.get(family);if(kit)links.push({from:{kind:"component",key:name},relation:"part_of",to:{kind:"kit",key:kit}});for(const l of j.links??[])links.push({from:{kind:"component",key:name},relation:l.relation,to:{kind:"component",key:l.to},note:l.note})}for(const pattern of patterns){const slug=str(pattern.meta.page);if(!slug)throw new Error(`[knowledge] pattern ${pattern.key} names no page`);const dir=slug.includes("/")?resolve(repoRoot,"src",slug):resolve(repoRoot,"src/pages",slug);const page=JSON.parse(readFileSync(resolve(dir,"page.json"),"utf8"));const mock=existsSync(resolve(dir,"mock.json"))?JSON.parse(readFileSync(resolve(dir,"mock.json"),"utf8")):{};const data={...mock.data??{}};delete data.params;rows.push({kind:"pattern",key:pattern.key,title:str(pattern.meta.title)??pattern.key,summary:str(pattern.meta.summary)??"",body:pattern.body,tags:strs(pattern.meta.tags),sort:Number(pattern.meta.sort??0),snippets:[{title:str(pattern.meta.title)??pattern.key,intent:str(pattern.meta.summary),cells:page.content.cells,layout:page.layout,data}],attributes:{layout:page.layout?.type??"stack",...page.layout?.profile?{profile:page.layout.profile}:{},page:slug}});for(const component of new Set([...page.content.cells.map(c=>c.component).filter(c=>Boolean(c)),...strs(pattern.meta.uses)])){const inMain=scopeRegistry&&!registry[component]&&scopeRegistry[component];links.push({from:{kind:"pattern",key:pattern.key},relation:"uses",to:{...inMain?{lineage:scopeLineage}:{},kind:"component",key:component}})}for(const kit of strs(pattern.meta.kits))links.push({from:{kind:"pattern",key:pattern.key},relation:"uses",to:{kind:"kit",key:kit}})}return{lineage,rows,links,registry,...scopeRegistry?{scopeLineage,scopeRegistry}:{}}}export{buildKnowledge,holdsReference};