2import fs from 'fs/promises' 3import { existsSync, readdirSync, readFileSync } from 'fs' 5import { execSync } from 'child_process' 40import { unlinkSync } from 'fs' 48const ptFileExtPattern = ptFileExtA.join('|') 49const ptPathRe = new RegExp(`(?<![">/])\\b(pt0/[\\w/.-]+(?:\\.(${ptFileExtPattern})|(?<=/path_bin/)[\\w]+))\\b(?![^<]*</a>)`, 'g') 50const linkifyPtPaths = (html) => html.replace(ptPathRe, (m) => `<a href="${getSrcFileUrl(m)}">${m}</a>`) 52const extractClusterEpPath = (content) => { 53 const match = content.match(new RegExp(`\\{cluster_name:[^}]*\\}\\s+([\\w/.-]+\\.(${ptFileExtPattern}))\\b`)) 54 return match?.[1] ?? null 57const resolveEpPublicDir = (ep) => { 59 return ep.publicDir.startsWith('/') ? ep.publicDir : pathJoin(epDir, ep.publicDir) 62const getEpFromSite = (s) => s.ep || (s.epH && Object.values(s.epH)[0]) 64const buildSiteRoutes = ({sites, epDir}) => (sites || []).filter(s => s.path && getEpFromSite(s)).map(s => ({ 66 dir: resolveEpPublicDir(getEpFromSite(s)), 69const discoverBlogposts = (blogpostDirAbs) => { 70 if (!blogpostDirAbs || !existsSync(blogpostDirAbs)) return [] 71 const entries = readdirSync(blogpostDirAbs, {withFileTypes: true}) 72 const dirPosts = entries 73 .filter(d => d.isDirectory()) 75 const dir = path.join(blogpostDirAbs, d.name) 76 const mdPath = path.join(dir, 'index.md') 77 return {slug: d.name, dir, mdPath: existsSync(mdPath) ? mdPath : null} 79 const flatPosts = entries 80 .filter(f => f.isFile() && f.name.endsWith('.md')) 81 .map(f => ({slug: f.name.slice(0, -3), dir: null, mdPath: path.join(blogpostDirAbs, f.name)})) 82 return [...dirPosts, ...flatPosts] 87import stripAnsi from 'strip-ansi' 91const helpStyleCss = `${transcriptCssVars}\n.bash-output { white-space: pre-wrap; word-wrap: break-word; line-height: 1.4; font-family: monospace; }\n.action-link { color: var(--teal); cursor: pointer; text-decoration: underline; }\n.action-link:hover, .action-link.active { color: var(--cyan); }\n#action-output { margin-top: 24px; }` 93const renderHelpForEp = (helpOutput, epName, exampleActions, templates, epPathToSlug = new Map()) => { 94 const plainHelp = stripAnsi(helpOutput) 99 const markdownStr = `<style>${helpStyleCss}</style>\n<pre class="bash-output">${htmlHelp}</pre>\n<div id="action-output"></div>` 100 const rawHtmlSuffix = exampleActions.length ? `${templates}\n${helpPageScript}` : '' 101 return {markdownStr, rawHtmlSuffix} 104const buildEpPathToSlugMap = (sites) => { 105 const map = new Map() 106 for (const site of sites || []) { 107 const ep = getEpFromSite(site) 108 const key = site.epH && Object.keys(site.epH)[0] 109 if (!ep?.importMetaUrl || !key) continue 111 map.set(epPtPath, `${key}-help`) 116const buildHelpBlogposts = async (sites) => { 117 if (!sites?.length) return [] 118 const epPathToSlug = buildEpPathToSlugMap(sites) 120 const ossTransform = (s: string) => ossTranscriptRedactFn ? ossTranscriptRedactFn(s) : s 121 const results = await Promise.all(sites.map(async (site) => { 122 const ep = getEpFromSite(site) 123 const key = site.epH && Object.keys(site.epH)[0] 124 if (!ep || !key) return null 125 const epName = ep.epName || ep.name || key 126 const helpOutput = site.noHelp ? null : await captureEpHelp(ep, { forceColor: true }) 128 if (!helpOutput && !exampleActions.length) return null 129 const templates = exampleActions.map(action => { 130 const htmlPath = path.join(bashtranscriptsDir, `${epName}-${action}.html`) 131 const content = readFileSync(htmlPath, 'utf8') 132 return `<template id="bash-${epName}-${action}">${linkifyEpHelpLinks(content, epPathToSlug)}</template>` 134 let markdownStr, rawHtmlSuffix 136 ({markdownStr, rawHtmlSuffix} = renderHelpForEp(helpOutput, epName, exampleActions, templates, epPathToSlug)) 138 let firstExample = readFileSync(path.join(bashtranscriptsDir, `${epName}-${exampleActions[0]}.html`), 'utf8') 139 const linkedEpPath = extractClusterEpPath(firstExample) 140 let linkedPage = null 142 const linkedEpExt = '.' + ptFileExtA.find(e => linkedEpPath.endsWith('.' + e)) 143 const linkedSlug = path.basename(linkedEpPath, linkedEpExt) + '-help' 144 const linkedAbsPath = path.join(process.cwd(), linkedEpPath) 145 const linkedHelpOutput = await captureEpHelp({importMetaUrl: 'file://' + linkedAbsPath}, {forceColor: true}) 146 if (linkedHelpOutput) { 147 firstExample = firstExample.replace(linkedEpPath, `<a href="/blogposts/${linkedSlug}/index.html">${linkedEpPath}</a>`) 148 // Get resource name from ep exports for bashtranscript lookup 149 let linkedResourceName = path.basename(linkedEpPath, linkedEpExt) 151 const epMod = await import(linkedAbsPath) 152 linkedResourceName = epMod.exampleName || epMod.ethStatusCfg?.execClient?.fuzzyPodName || epMod.default?.name || linkedResourceName 155 const linkedTemplates = linkedExamples.map(action => { 156 const htmlPath = path.join(bashtranscriptsDir, `${linkedResourceName}-${action}.html`) 157 const content = readFileSync(htmlPath, 'utf8') 158 return `<template id="bash-${linkedResourceName}-${action}">${linkifyEpHelpLinks(content, epPathToSlug)}</template>` 160 const {markdownStr: linkedMarkdownStr, rawHtmlSuffix: linkedRawHtmlSuffix} = renderHelpForEp(linkedHelpOutput, linkedResourceName, linkedExamples, linkedTemplates, epPathToSlug) 161 linkedPage = {slug: linkedSlug, markdownStr: ossTransform(linkedMarkdownStr), rawHtmlSuffix: ossTransform(linkedRawHtmlSuffix)} 164 markdownStr = `<style>${transcriptCssVars}\n.bash-output { white-space: pre-wrap; word-wrap: break-word; line-height: 1.4; font-family: monospace; }</style>\n<pre class="bash-output">${linkifyPtPaths(firstExample)}</pre>` 166 if (linkedPage) return [{slug: `${key}-help`, markdownStr: ossTransform(markdownStr), rawHtmlSuffix: ossTransform(rawHtmlSuffix)}, linkedPage] 168 return {slug: `${key}-help`, markdownStr: ossTransform(markdownStr), rawHtmlSuffix: ossTransform(rawHtmlSuffix)} 170 return results.flat().filter(Boolean) 175const scanWorkDirForForbidden = async (workDir, {ossPathFwd, allFiles}) => { 177 if (!ossForbidStrA) return 178 const forbiddenHits = [] 179 for (const relPath of allFiles) { 180 const displayPath = ossPathFwd(relPath) 181 if (!isTextFile(relPath) || displayPath.endsWith('.d.ts')) continue 182 const content = await fs.readFile(path.join(workDir, displayPath), 'utf8').catch(() => null) 183 if (!content) continue 184 const hit = perFileGuardNeedle({needleStrA: ossForbidStrA, path: displayPath, contents: content, raiseOnFind: false}) 185 if (hit) forbiddenHits.push(hit) 187 if (forbiddenHits.length) { 188 console.error(`\n${ossRepoGitPath}: ${forbiddenHits.length} forbidden string hit(s):`) 189 for (const h of forbiddenHits) console.error(` ${h.path}: "${h.needleStr}" in "${h.containingWord}"`) 190 throw new Error(`${ossRepoGitPath}: ${forbiddenHits.length} forbidden string(s) found`) 194const scanUploadDirForForbidden = async (uploadDir) => { 196 if (!ossForbidStrA) return 197 const forbiddenHits = [] 198 for (const relPath of await fs.readdir(uploadDir, {recursive: true})) { 200 const content = await fs.readFile(path.join(uploadDir, relPath), 'utf8').catch(() => null) 201 if (!content) continue 202 const hit = perFileGuardNeedle({needleStrA: ossForbidStrA, path: relPath, contents: content, raiseOnFind: false}) 203 if (hit) forbiddenHits.push(hit) 205 if (forbiddenHits.length) { 206 console.error(`\nuploadDir: ${forbiddenHits.length} forbidden string hit(s):`) 207 for (const h of forbiddenHits) console.error(` ${h.path}: "${h.needleStr}" in "${h.containingWord}"`) 208 throw new Error(`uploadDir: ${forbiddenHits.length} forbidden string(s) found`) 212const buildPeatGitBare = async (cacheDir, {ossReplaceFn, ossPathFwd, ossTrackedPaths, ossExcludeFn, ossAuthor, shortSha}) => { 213 const tmpWork = path.join(ptTmpDir, `peat-work-${Date.now()}`) 214 await fs.mkdir(tmpWork, {recursive: true}) 218 const allFiles = (await getTrackedFiles(ossTrackedPaths)).filter(f => !ossExcludeFn || !ossExcludeFn(f)) 219 for (const relPath of allFiles) { 220 const displayPath = ossPathFwd(relPath) 221 const destFile = path.join(tmpWork, displayPath) 222 await fs.mkdir(path.dirname(destFile), {recursive: true}) 223 const srcAbs = path.join(ptDir, relPath) 224 const fileStat = await fs.stat(srcAbs).catch(() => null) 225 if (!fileStat?.isFile()) continue 226 const basename = path.basename(relPath) 227 if (basename === '.gitignore' && ossGitignore) { 228 await fs.writeFile(destFile, ossGitignore) 229 } else if (basename === 'pnpm-workspace.yaml' && filterPnpmWs) { 230 const raw = await fs.readFile(srcAbs, 'utf8') 231 await fs.writeFile(destFile, filterPnpmWs(raw)) 232 } else if (relPath === 'opencode.json' && transformOcJson) { 233 const raw = await fs.readFile(srcAbs, 'utf8') 234 await fs.writeFile(destFile, transformOcJson(ossReplaceFn ? ossReplaceFn(raw) : raw)) 236 const raw = await fs.readFile(srcAbs, 'utf8') 237 const transformed = ossReplaceFn ? ossReplaceFn(raw) : raw 238 await fs.writeFile(destFile, transformed) 240 await fs.copyFile(srcAbs, destFile) 244 await scanWorkDirForForbidden(tmpWork, {ossPathFwd, allFiles}) 245 const reviewResult = await llmReviewDir({dir: tmpWork, shortSha, reviewName: 'ossopsec'}) 246 if (reviewResult.summary.abortedEarly || reviewResult.summary.failedChunkCount > 0) { 247 throw new Error(`oss review did not pass: ${reviewResult.summary.failedChunkCount} chunk(s) failed${reviewResult.summary.abortedEarly ? ' (aborted early)' : ''}${reviewResult.summary.firstError ? ` — ${reviewResult.summary.firstError.code}: ${reviewResult.summary.firstError.detail}` : ''}`) 249 execSync('git init', {cwd: tmpWork, stdio: 'pipe'}) 250 execSync('git add -A', {cwd: tmpWork, stdio: 'pipe'}) 251 const gitEnv = {...process.env, GIT_AUTHOR_NAME: ossEnsName, GIT_AUTHOR_EMAIL: ossEmail, GIT_COMMITTER_NAME: ossEnsName, GIT_COMMITTER_EMAIL: ossEmail} 252 const repoHeadSha = commitMineShaPrefix({cwd: tmpWork, msgBase: `peatSymposium ${shortSha}`, tgtShaPrefix: ossRepoTgtShaPrefix, gitEnv}) 253 execSync(`git clone --bare "${tmpWork}" "${cacheDir}"`, {stdio: 'pipe'}) 254 execSync('git repack -a -d', {cwd: cacheDir, stdio: 'pipe'}) 255 execSync('git update-server-info', {cwd: cacheDir, stdio: 'pipe'}) 256 await fs.rm(tmpWork, {recursive: true}) 257 return {repoHeadSha, reviewDir: reviewResult.reviewDir, reviewTotCostUsd: reviewResult.summary?.totCostUsd, reviewChunksWithFindings: reviewResult.summary?.chunksWithFindings} 260const ensurePeatGitCacheDir = async (shortSha) => { 261 const cacheDir = path.join(ptTmpDir, peatGitCacheKey(shortSha)) 262 if (!existsSync(cacheDir)) { 268 const ossHook = await (await import('../../deployF/ossHookLoaderF.mts')).getOssHook() 269 const ossAuthorResolved = ossHook?.ossAuthor || ossAuthor 270 const buildInfo = await buildPeatGitBare(cacheDir, {ossReplaceFn, ossPathFwd, ossTrackedPaths, ossExcludeFn, ossAuthor: ossAuthorResolved, shortSha}) 271 console.log(`Generated ${ossRepoGitPath}/ (head ${buildInfo.repoHeadSha.slice(0, 12)})`) 276const pruneStaleOssRepoDirs = async (destDir) => { 277 const repoBase = ossRepoName.replace(/\d+$/, '') 278 for (const f of readdirSync(destDir)) { 279 if (new RegExp(`^${repoBase}\\d+\\.git$`).test(f) && f !== ossRepoGitPath) await fs.rm(path.join(destDir, f), {recursive: true, force: true}) 283const generatePeatGit = async (destDir) => { 285 const cacheDir = await ensurePeatGitCacheDir(shortSha) 286 await pruneStaleOssRepoDirs(destDir) 287 await fs.cp(cacheDir, path.join(destDir, ossRepoGitPath), {recursive: true}) 288 try { return execSync(`git --git-dir=${cacheDir} rev-parse HEAD`, {encoding: 'utf8'}).trim().slice(0, gitShaDisplayLen) } catch { return undefined } 291let _peatGitCache = null 292const getCachedPeatGitDir = async () => { 294 const cacheDir = path.join(ptTmpDir, peatGitCacheKey(shortSha)) 295 if (_peatGitCache === cacheDir && existsSync(cacheDir)) return cacheDir 296 const dir = await ensurePeatGitCacheDir(shortSha) 301const peatGitContentType = (subPath) => { 302 if (subPath.endsWith('.pack')) return 'application/x-git-packed-objects' 303 if (subPath.endsWith('.idx')) return 'application/x-git-packed-objects-toc' 304 if (subPath === 'info/refs' || subPath === 'objects/info/packs') return 'text/plain; charset=utf-8' 305 return 'application/octet-stream' 308const buildCombinedDir = async ({root, sites, markdownPath, renderGalleryIndex, galleryJsxPath, epDir, siteStyles, blogpostsDir}) => { 309 const routes = buildSiteRoutes({sites, epDir}) 310 const blogpostPathPrefix = 'blogposts' 311 const blogposts = discoverBlogposts(blogpostsDir) 312 const affectingPaths = [ 315 path.relative(ptDir, path.dirname(galleryJsxPath)), 316 ...routes.map(r => r.dir), 317 ...(blogpostsDir ? [blogpostsDir] : []), 318 ].filter(p => p && !p.startsWith('/')) 321 const shortSha = buildSha.slice(0, 12) 325 if (existsSync(cacheDir)) { 326 console.log(`Using cached build (sha: ${shortSha})`) 330 console.log(`Building fresh (sha: ${shortSha})`) 331 await fs.rm(cacheDir, {recursive: true, force: true}) 332 await fs.mkdir(cacheDir, {recursive: true}) 334 const rootExists = await fs.stat(root).catch(() => null) 335 if (rootExists) await fs.cp(root, cacheDir, {recursive: true}) 337 for (const {prefix, dir} of routes) { 338 const destPath = path.join(cacheDir, prefix) 339 console.log(`Copying ${prefix} from ${dir}`) 340 await fs.cp(dir, destPath, {recursive: true}) 343 for (const {slug, dir, mdPath} of blogposts) { 344 const destPath = path.join(cacheDir, blogpostPathPrefix, slug) 345 await fs.mkdir(destPath, {recursive: true}) 347 const html = await renderMarkdownPage({markdownPath: mdPath, title: slug, styles: siteStyles, homeHref: '/', bodyClasses: 'blogpost-' + slug}) 348 await fs.writeFile(path.join(destPath, 'index.html'), html) 349 console.log(`Generated ${blogpostPathPrefix}/${slug}/index.html from markdown`) 352 for (const f of readdirSync(dir)) { 353 if (f === 'index.md') continue 354 await fs.cp(path.join(dir, f), path.join(destPath, f), {recursive: true}) 359 for (const {slug, markdownStr, rawHtmlSuffix} of await buildHelpBlogposts(sites)) { 360 const destPath = path.join(cacheDir, blogpostPathPrefix, slug) 361 await fs.mkdir(destPath, {recursive: true}) 362 const html = await renderMarkdownPage({markdownStr, title: slug, styles: siteStyles, homeHref: '/', rawHtmlSuffix}) 363 await fs.writeFile(path.join(destPath, 'index.html'), html) 364 console.log(`Generated ${blogpostPathPrefix}/${slug}/index.html from help`) 367 const peatGitSha = await generatePeatGit(cacheDir) 369 if (sites?.length && renderGalleryIndex) { 370 const indexPath = path.join(cacheDir, 'index.html') 371 if (!existsSync(indexPath)) { 373 const indexHtml = await renderGalleryIndex(sites, {markdownPath, peatGitSha}) 374 await fs.writeFile(indexPath, ossReplaceFn ? ossReplaceFn(indexHtml) : indexHtml) 375 console.log(`Generated gallery index.html`) 384export const eptEnsGallerySite = async ({importMetaUrl, publicDir = 'public', svcPortNo = 31281, ensName, getWallet, sites, ensNameToRegister, durationYears = 1, ipfsApiUrl, ipfsGatewayUrl, renderGalleryIndex, galleryJsxPath, galleryStylesPath, stylesPath, siteStyles, srcFilePaths, ...props}) => { 385 const fullConfig = {importMetaUrl, publicDir, svcPortNo, ensName, getWallet, sites, ensNameToRegister, durationYears, ipfsApiUrl, ipfsGatewayUrl, renderGalleryIndex, galleryJsxPath, galleryStylesPath, stylesPath, siteStyles, srcFilePaths, ...props} 389 const {publicDir: root} = setupStaticSiteCtx({importMetaUrl, publicDir, svcPortNo, ensName, getWallet, sites, ensNameToRegister, durationYears, ...props}) 392 const resolvedMarkdownPath = pathJoin(contentDir, 'index.md') 393 const blogpostsDir = pathJoin(contentDir, 'blogposts') 395 const regens = async () => { 398 const wallet = getWallet() 399 betLog({ensNameToRegister, durationYears, walletAddress: wallet.address}) 402 const verifiedOwner = await ensGetOwner({ensName: ensNameToRegister}) 403 const ownsName = verifiedOwner?.toLowerCase() === wallet.address.toLowerCase() 407 regens.cliDescript = 'register ENS name (experimental/broken)' 408 regens.cliAdvanced = true 410 const devserver = async () => { 411 const routes = buildSiteRoutes({sites, epDir}) 412 const watchFiles = [galleryJsxPath, galleryStylesPath, resolvedMarkdownPath, stylesPath].filter(Boolean) 413 const dynamicFiles = sites?.length && renderGalleryIndex && !existsSync(path.join(root, 'index.html')) ? [{ 415 generate: () => renderGalleryIndex(sites, {markdownPath: resolvedMarkdownPath, peatGitSha: 'placeholder123sha'}), 419 const blogpostPathPrefix = 'blogposts' 420 const customPagePaths = { 421 dedication: path.join(ptDir, ptAnchorPath('pt0/peatsite/DedicationPageAI.jsx')), 423 const makeBlogpostDynamicFile = ({slug, dir: _dir, mdPath}) => { 424 const customPagePath = customPagePaths[slug] 425 if (customPagePath && existsSync(customPagePath)) { 427 path: path.join(blogpostPathPrefix, slug, 'index.html'), 428 generate: async () => { 429 const mdContent = readFileSync(mdPath, 'utf8') 433 watchFiles: [customPagePath, mdPath], 436 const mdContent = readFileSync(mdPath, 'utf8') 437 const transcriptRefs = [...mdContent.matchAll(/\{\{transcript:(ses_[a-zA-Z0-9]+)(?::startAtStr=.+?)?\}\}/g)] 438 .map(m => path.join(contentDir, 'octranscripts', `${m[1]}.html`)) 440 path: path.join(blogpostPathPrefix, slug, 'index.html'), 441 generate: () => renderMarkdownPage({markdownPath: mdPath, title: slug, styles: siteStyles, homeHref: '/', bodyClasses: 'blogpost-' + slug}), 442 watchFiles: [mdPath, ...transcriptRefs], 446 const blogpostAssetDirs = [] 447 for (const bp of discoverBlogposts(blogpostsDir)) { 448 if (bp.mdPath) dynamicFiles.push(makeBlogpostDynamicFile(bp)) 449 if (bp.dir) blogpostAssetDirs.push({prefix: path.join(blogpostPathPrefix, bp.slug), dir: bp.dir}) 452 for (const {slug, markdownStr, rawHtmlSuffix} of await buildHelpBlogposts(sites)) { 454 path: path.join(blogpostPathPrefix, slug, 'index.html'), 455 generate: () => renderMarkdownPage({markdownStr, title: slug, styles: siteStyles, homeHref: '/', rawHtmlSuffix}), 462 const srcHandler = async (displayPath) => { 463 if (hasSensitive(displayPath)) return null 464 const diskPath = ossPathRev(displayPath) 466 const rawContent = await fs.readFile(path.join(ptDir, diskPath), 'utf8').catch(() => null) 467 if (rawContent == null) return null 468 const content = ossReplaceFn ? ossReplaceFn(rawContent) : rawContent 469 return renderSourcePage(ossPathFwd(diskPath), content, {contentTransform: ossReplaceFn, pathTransform: ossPathFwd, pathReverse: ossPathRev}) 471 const peatGitHandler = async (subPath) => { 472 const bareDir = await getCachedPeatGitDir() 473 if (!bareDir) return null 474 const filePath = path.join(bareDir, subPath) 475 if (!existsSync(filePath)) return null 476 return fs.readFile(filePath) 478 const onDemandHandlers = [ 479 {prefix: '/src/', handler: srcHandler}, 480 {prefix: `/${ossRepoGitPath}/`, handler: peatGitHandler, contentType: peatGitContentType}, 483 const hostname = ensName ? `${ensName.replace('.eth', '')}.localhost` : null 484 const renderEntryPtPath = 'pt0/peatsite/renderGalleryAI.mjs' 485 madgeDepFilterCtx.enterWith({dependencyFilter: () => true}) 486 const madgeH = await ptMadge([renderEntryPtPath].filter(isPtCodeFileExt)) 488 const codeWatchFiles = [...importTree].filter(isPtCodeFileExt) 489 await startDevserverWithRoutes({root, routes, dynamicFiles, codeWatchFiles, svcPortNo, hostname, blogpostsDir, makeBlogpostDynamicFile, blogpostPathPrefix, blogpostAssetDirs, onDemandHandlers}) 491 devserver.cliDescript = 'start local http-server' 494 info.cliDescript = 'show current ENS contenthash' 496 const generateSrcPagesCache = async (shortSha) => { 497 const srcCacheDir = path.join(ptTmpDir, `ens-src-${shortSha}`) 498 if (existsSync(path.join(srcCacheDir, '.complete'))) return srcCacheDir 499 await fs.rm(srcCacheDir, {recursive: true, force: true}) 500 await fs.mkdir(path.join(srcCacheDir, 'src'), {recursive: true}) 508 console.log(`Generating ${allFiles.length} src pages (sha: ${shortSha})...`) 510 for (const diskPath of allFiles) { 511 const displayPath = ossPathFwd(diskPath) 512 if (hasSensitive(displayPath)) continue 513 const rawContent = await fs.readFile(path.join(ptDir, diskPath), 'utf8').catch(() => null) 514 if (rawContent == null) continue 515 const basename = path.basename(diskPath) 516 const substituted = basename === '.gitignore' && ossGitignore ? ossGitignore 517 : basename === 'pnpm-workspace.yaml' && filterPnpmWs ? filterPnpmWs(rawContent) 519 const content = ossReplaceFn ? ossReplaceFn(substituted) : substituted 520 const html = await renderSourcePage(displayPath, content, {contentTransform: ossReplaceFn, pathTransform: ossPathFwd, pathReverse: ossPathRev}) 521 const destPath = path.join(srcCacheDir, 'src', displayPath, 'index.html') 522 await fs.mkdir(path.dirname(destPath), {recursive: true}) 523 await fs.writeFile(destPath, html) 524 if (++count % 100 === 0) console.log(` ${count}/${allFiles.length}...`) 526 await fs.writeFile(path.join(srcCacheDir, '.complete'), '') 527 console.log(`Generated ${count} src pages`) 531 const apply = async () => { 534 const uploadDir = (sites?.length || existsSync(blogpostsDir)) ? await buildCombinedDir({root, sites, markdownPath: resolvedMarkdownPath, renderGalleryIndex, galleryJsxPath, epDir, siteStyles, blogpostsDir}) : root 535 const srcCacheDir = await generateSrcPagesCache(shortSha) 536 await fs.cp(path.join(srcCacheDir, 'src'), path.join(uploadDir, 'src'), {recursive: true}) 537 await generatePeatGit(uploadDir) 538 await scanUploadDirForForbidden(uploadDir) 539 const peatGitDir = path.join(uploadDir, ossRepoGitPath) 540 const repoHeadSha = execSync(`git --git-dir=${peatGitDir} rev-parse HEAD`, {encoding: 'utf8'}).trim() 541 await doEnsApply({uploadDir, ensName, getWallet, ipfsApiUrl, ipfsGatewayUrl}) 543 await appendOssAnchor({name: ossRepoName, srcHeadSha: shortSha, repoHeadSha, tgtShaPrefix: ossRepoTgtShaPrefix, reviewName: 'ossopsec', reviewTotCostUsd: reviewSummary?.totCostUsd, reviewChunksWithFindings: reviewSummary?.chunksWithFindings, ts: new Date().toISOString()}) 545 apply.cliDescript = 'upload to IPFS and set ENS contenthash' 548 updatescreenshots.cliDescript = "capture screenshots for sites with image: 'auto'" 549 updatescreenshots.cliSchema = {only: {type: 'string' as const, desc: 'only capture this site key'}} 551 const prune = async () => { 552 if (!existsSync(bashtranscriptsDir)) { console.log('No bashtranscripts dir'); return } 553 const allFiles = readdirSync(bashtranscriptsDir).filter(f => f.endsWith('.html')) 554 const anchoredPrefixes = new Set() 555 for (const site of sites || []) { 556 const ep = getEpFromSite(site), key = site.epH && Object.keys(site.epH)[0] 557 if (!ep || !key) continue 558 const epName = ep.epName || ep.name || key 559 anchoredPrefixes.add(epName) 561 if (site.noHelp && exampleActions.length) { 562 const firstHtml = readFileSync(path.join(bashtranscriptsDir, `${epName}-${exampleActions[0]}.html`), 'utf8') 563 const linkedEpPath = extractClusterEpPath(firstHtml) 565 const linkedEpExt = '.' + ptFileExtA.find(e => linkedEpPath.endsWith('.' + e)) 566 let linkedResourceName = path.basename(linkedEpPath, linkedEpExt) 568 const epMod = await import(path.join(process.cwd(), linkedEpPath)) 569 linkedResourceName = epMod.exampleName || epMod.ethStatusCfg?.execClient?.fuzzyPodName || epMod.default?.name || linkedResourceName 571 anchoredPrefixes.add(linkedResourceName) 575 const relDir = path.relative(ptDir, bashtranscriptsDir) 576 const orphanedPtPaths = allFiles 577 .filter(f => !anchoredPrefixes.has(f.replace(/-[^-]+\.html$/, ''))) 578 .map(f => `${relDir}/${f}`) 579 if (!orphanedPtPaths.length) { console.log('No orphaned bashtranscripts'); return } 581 async () => { for (const p of orphanedPtPaths) unlinkSync(path.join(ptDir, p)) }, 582 {commitMsg: `ptPrune(bashtranscripts): ${orphanedPtPaths.length} orphaned`, relevantFilesA: orphanedPtPaths, skipViolationCheck: true} 584 for (const p of orphanedPtPaths) console.log(`deleted ${p}`) 586 prune.cliDescript = 'delete bashtranscripts not anchored by any site' 588 const runtests = async () => { 590 test('gallery-index-md-keys-match-sites', async () => { 592 const html = await renderGalleryIndex(sites, {markdownPath: resolvedMarkdownPath}) 593 if (!html || typeof html !== 'string') return {passed: false, msg: 'renderGalleryIndex returned non-string'} 594 if (!html.includes('<html')) return {passed: false, msg: 'output missing <html tag'} 595 return {passed: true, msg: `gallery index rendered (${html.length} chars)`} 597 return {passed: false, msg: err.message} 602 runtests.cliDescript = 'verify gallery site renders without errors' 604 const actionsH = {regens, apply, devserver, info, updatescreenshots, prune, runtests} 605 availActionsCtx.enterWith({...availActionsCtx.getStore(), ...actionsH}) 607 const wantsHelp = action === 'help' || !action || cliFlag('--help') 608 if (wantsHelp || !actionsH[action]) { 609 cliBase({actionNames: Object.keys(actionsH), actionsH}) 613 await actionsH[action]()