🌳
pt0/peatsite/binF/eptEnsGallerySiteAI.mts
1// @ts-nocheck
2import fs from 'fs/promises'
3import { existsSync, readdirSync, readFileSync } from 'fs'
4import path from 'path'
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) => {
58 const epDir = getParentDir(getImportMetaUrlPath(ep.importMetaUrl))
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 => ({
65 prefix: '/' + s.path,
66 dir: resolveEpPublicDir(getEpFromSite(s)),
67}))
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())
74 .map(d => {
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}
78 })
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)
95 let htmlHelp = ansiToHtml(helpOutput)
96 htmlHelp = linkifyEpPath(htmlHelp, plainHelp)
97 htmlHelp = linkifyActions(htmlHelp, plainHelp, epName, exampleActions)
98 htmlHelp = linkifyEpHelpLinks(htmlHelp, epPathToSlug)
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
110 const epPtPath = path.relative(ptDir, getImportMetaUrlPath(ep.importMetaUrl))
111 map.set(epPtPath, `${key}-help`)
112 }
113 return map
116const buildHelpBlogposts = async (sites) => {
117 if (!sites?.length) return []
118 const epPathToSlug = buildEpPathToSlugMap(sites)
119 const ossTranscriptRedactFn = await getOssTranscriptRedactFn()
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 })
127 const exampleActions = getBashExamples(epName)
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>`
133 }).join('\n')
134 let markdownStr, rawHtmlSuffix
135 if (helpOutput) {
136 ({markdownStr, rawHtmlSuffix} = renderHelpForEp(helpOutput, epName, exampleActions, templates, epPathToSlug))
137 } else {
138 let firstExample = readFileSync(path.join(bashtranscriptsDir, `${epName}-${exampleActions[0]}.html`), 'utf8')
139 const linkedEpPath = extractClusterEpPath(firstExample)
140 let linkedPage = null
141 if (linkedEpPath) {
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)
150 try {
151 const epMod = await import(linkedAbsPath)
152 linkedResourceName = epMod.exampleName || epMod.ethStatusCfg?.execClient?.fuzzyPodName || epMod.default?.name || linkedResourceName
153 } catch {}
154 const linkedExamples = getBashExamples(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>`
159 }).join('\n')
160 const {markdownStr: linkedMarkdownStr, rawHtmlSuffix: linkedRawHtmlSuffix} = renderHelpForEp(linkedHelpOutput, linkedResourceName, linkedExamples, linkedTemplates, epPathToSlug)
161 linkedPage = {slug: linkedSlug, markdownStr: ossTransform(linkedMarkdownStr), rawHtmlSuffix: ossTransform(linkedRawHtmlSuffix)}
162 }
163 }
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>`
165 rawHtmlSuffix = ''
166 if (linkedPage) return [{slug: `${key}-help`, markdownStr: ossTransform(markdownStr), rawHtmlSuffix: ossTransform(rawHtmlSuffix)}, linkedPage]
167 }
168 return {slug: `${key}-help`, markdownStr: ossTransform(markdownStr), rawHtmlSuffix: ossTransform(rawHtmlSuffix)}
169 }))
170 return results.flat().filter(Boolean)
173const peatGitCacheKey = (shortSha) => `peat-git-${shortSha}-${ossRepoTgtShaPrefix || 'nomine'}`
175const scanWorkDirForForbidden = async (workDir, {ossPathFwd, allFiles}) => {
176 const ossForbidStrA = await getOssForbidStrA()
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)
186 }
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`)
191 }
194const scanUploadDirForForbidden = async (uploadDir) => {
195 const ossForbidStrA = await getOssForbidStrA()
196 if (!ossForbidStrA) return
197 const forbiddenHits = []
198 for (const relPath of await fs.readdir(uploadDir, {recursive: true})) {
199 if (!isTextFile(relPath)) continue
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)
204 }
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`)
209 }
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})
215 const ossGitignore = await getOssGitignoreContent()
216 const filterPnpmWs = await getFilterOssPnpmWorkspace()
217 const transformOcJson = await getTransformOssOpencodeJson()
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))
235 } else if (isTextFile(relPath)) {
236 const raw = await fs.readFile(srcAbs, 'utf8')
237 const transformed = ossReplaceFn ? ossReplaceFn(raw) : raw
238 await fs.writeFile(destFile, transformed)
239 } else {
240 await fs.copyFile(srcAbs, destFile)
241 }
242 }
243 pnpmInstallSync({lockfileOnly: true, cwd: tmpWork})
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}` : ''}`)
248 }
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)) {
263 console.log(`Generating ${ossRepoGitPath} (sha: ${shortSha})...`)
264 const ossReplaceFn = await getOssReplaceFn()
265 const { forward: ossPathFwd } = await getOssPathTransformers()
266 const ossTrackedPaths = await getOssTrackedPaths() || ['pt0/']
267 const ossExcludeFn = await getOssExcludeFn()
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)})`)
272 }
273 return cacheDir
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})
280 }
283const generatePeatGit = async (destDir) => {
284 const shortSha = (await getHeadGitSha()).slice(0, 12)
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 () => {
293 const shortSha = (await getHeadGitSha()).slice(0, 12)
294 const cacheDir = path.join(ptTmpDir, peatGitCacheKey(shortSha))
295 if (_peatGitCache === cacheDir && existsSync(cacheDir)) return cacheDir
296 const dir = await ensurePeatGitCacheDir(shortSha)
297 _peatGitCache = dir
298 return dir
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 = [
313 root,
314 markdownPath,
315 path.relative(ptDir, path.dirname(galleryJsxPath)),
316 ...routes.map(r => r.dir),
317 ...(blogpostsDir ? [blogpostsDir] : []),
318 ].filter(p => p && !p.startsWith('/'))
320 const buildSha = await getHeadGitSha({filterPathList: affectingPaths})
321 const shortSha = buildSha.slice(0, 12)
323 const cacheDir = `${ptTmpDir}/ens-site-${shortSha}-${ossRepoName}`
325 if (existsSync(cacheDir)) {
326 console.log(`Using cached build (sha: ${shortSha})`)
327 return cacheDir
328 }
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})
341 }
343 for (const {slug, dir, mdPath} of blogposts) {
344 const destPath = path.join(cacheDir, blogpostPathPrefix, slug)
345 await fs.mkdir(destPath, {recursive: true})
346 if (mdPath) {
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`)
350 }
351 if (dir) {
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})
355 }
356 }
357 }
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`)
365 }
367 const peatGitSha = await generatePeatGit(cacheDir)
369 if (sites?.length && renderGalleryIndex) {
370 const indexPath = path.join(cacheDir, 'index.html')
371 if (!existsSync(indexPath)) {
372 const ossReplaceFn = await getOssReplaceFn()
373 const indexHtml = await renderGalleryIndex(sites, {markdownPath, peatGitSha})
374 await fs.writeFile(indexPath, ossReplaceFn ? ossReplaceFn(indexHtml) : indexHtml)
375 console.log(`Generated gallery index.html`)
376 }
377 }
379 return cacheDir
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}
386 if (shouldReturnConfig(importMetaUrl)) return fullConfig
388 const action = props.action ?? getAction()
389 const {publicDir: root} = setupStaticSiteCtx({importMetaUrl, publicDir, svcPortNo, ensName, getWallet, sites, ensNameToRegister, durationYears, ...props})
390 const epDir = getParentDir(getImportMetaUrlPath(importMetaUrl))
391 const contentDir = getParentDir(epDir)
392 const resolvedMarkdownPath = pathJoin(contentDir, 'index.md')
393 const blogpostsDir = pathJoin(contentDir, 'blogposts')
395 const regens = async () => {
396 assertNonEmptyString(ensNameToRegister)
397 assertDefined(getWallet)
398 const wallet = getWallet()
399 betLog({ensNameToRegister, durationYears, walletAddress: wallet.address})
400 const result = await ensCommitAndRegister({ensName: ensNameToRegister, wallet, durationYears})
401 betLog({result})
402 const verifiedOwner = await ensGetOwner({ensName: ensNameToRegister})
403 const ownsName = verifiedOwner?.toLowerCase() === wallet.address.toLowerCase()
404 betLog({verifiedOwner, ownsName})
405 assertTruthy(ownsName)
406 }
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')) ? [{
414 path: 'index.html',
415 generate: () => renderGalleryIndex(sites, {markdownPath: resolvedMarkdownPath, peatGitSha: 'placeholder123sha'}),
416 watchFiles,
417 }] : []
419 const blogpostPathPrefix = 'blogposts'
420 const customPagePaths = {
421 dedication: path.join(ptDir, ptAnchorPath('pt0/peatsite/DedicationPageAI.jsx')),
422 }
423 const makeBlogpostDynamicFile = ({slug, dir: _dir, mdPath}) => {
424 const customPagePath = customPagePaths[slug]
425 if (customPagePath && existsSync(customPagePath)) {
426 return {
427 path: path.join(blogpostPathPrefix, slug, 'index.html'),
428 generate: async () => {
429 const mdContent = readFileSync(mdPath, 'utf8')
430 const htmlContent = (await import('marked')).marked.parse(preserveLeadingSpaces(mdContent))
431 return renderJsxToHtml(customPagePath, 'DedicationPage', {htmlContent})
432 },
433 watchFiles: [customPagePath, mdPath],
434 }
435 }
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`))
439 return {
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],
443 }
444 }
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})
450 }
452 for (const {slug, markdownStr, rawHtmlSuffix} of await buildHelpBlogposts(sites)) {
453 dynamicFiles.push({
454 path: path.join(blogpostPathPrefix, slug, 'index.html'),
455 generate: () => renderMarkdownPage({markdownStr, title: slug, styles: siteStyles, homeHref: '/', rawHtmlSuffix}),
456 })
457 }
459 const ossReplaceFn = await getOssReplaceFn()
460 const ossTrackedPaths = await getOssTrackedPaths()
461 const { forward: ossPathFwd, reverse: ossPathRev, hasSensitive } = await getOssPathTransformers()
462 const srcHandler = async (displayPath) => {
463 if (hasSensitive(displayPath)) return null
464 const diskPath = ossPathRev(displayPath)
465 if (!isOssTrackedPath(ossTrackedPaths, diskPath)) return null
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})
470 }
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)
477 }
478 const onDemandHandlers = [
479 {prefix: '/src/', handler: srcHandler},
480 {prefix: `/${ossRepoGitPath}/`, handler: peatGitHandler, contentType: peatGitContentType},
481 ]
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))
487 const importTree = getImportTreeFromMadgeH(renderEntryPtPath, madgeH)
488 const codeWatchFiles = [...importTree].filter(isPtCodeFileExt)
489 await startDevserverWithRoutes({root, routes, dynamicFiles, codeWatchFiles, svcPortNo, hostname, blogpostsDir, makeBlogpostDynamicFile, blogpostPathPrefix, blogpostAssetDirs, onDemandHandlers})
490 }
491 devserver.cliDescript = 'start local http-server'
493 const info = async () => doEnsInfo({ensName})
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})
501 const ossReplaceFn = await getOssReplaceFn()
502 const ossGitignore = await getOssGitignoreContent()
503 const filterPnpmWs = await getFilterOssPnpmWorkspace()
504 const ossTrackedPaths = await getOssTrackedPaths() || ['pt0/']
505 const ossExcludeFn = await getOssExcludeFn()
506 const { forward: ossPathFwd, reverse: ossPathRev, hasSensitive } = await getOssPathTransformers()
507 const allFiles = (await getTrackedFiles(ossTrackedPaths)).filter(f => isTextFile(f) && (!ossExcludeFn || !ossExcludeFn(f)))
508 console.log(`Generating ${allFiles.length} src pages (sha: ${shortSha})...`)
509 let count = 0
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)
518 : 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}...`)
525 }
526 await fs.writeFile(path.join(srcCacheDir, '.complete'), '')
527 console.log(`Generated ${count} src pages`)
528 return srcCacheDir
529 }
531 const apply = async () => {
532 const shortSha = (await getHeadGitSha()).slice(0, 12)
533 await assertOssBakeable({name: ossRepoName, wipCfgVar: 'releaseRepoWipNo'})
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})
542 const reviewSummary = await getOssReviewSummary('ossopsec', shortSha)
543 await appendOssAnchor({name: ossRepoName, srcHeadSha: shortSha, repoHeadSha, tgtShaPrefix: ossRepoTgtShaPrefix, reviewName: 'ossopsec', reviewTotCostUsd: reviewSummary?.totCostUsd, reviewChunksWithFindings: reviewSummary?.chunksWithFindings, ts: new Date().toISOString()})
544 }
545 apply.cliDescript = 'upload to IPFS and set ENS contenthash'
547 const updatescreenshots = async () => updateScreenshots(sites, root, {only: cliArg('only')})
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)
560 const exampleActions = getBashExamples(epName)
561 if (site.noHelp && exampleActions.length) {
562 const firstHtml = readFileSync(path.join(bashtranscriptsDir, `${epName}-${exampleActions[0]}.html`), 'utf8')
563 const linkedEpPath = extractClusterEpPath(firstHtml)
564 if (linkedEpPath) {
565 const linkedEpExt = '.' + ptFileExtA.find(e => linkedEpPath.endsWith('.' + e))
566 let linkedResourceName = path.basename(linkedEpPath, linkedEpExt)
567 try {
568 const epMod = await import(path.join(process.cwd(), linkedEpPath))
569 linkedResourceName = epMod.exampleName || epMod.ethStatusCfg?.execClient?.fuzzyPodName || epMod.default?.name || linkedResourceName
570 } catch {}
571 anchoredPrefixes.add(linkedResourceName)
572 }
573 }
574 }
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}
583 )
584 for (const p of orphanedPtPaths) console.log(`deleted ${p}`)
585 }
586 prune.cliDescript = 'delete bashtranscripts not anchored by any site'
588 const runtests = async () => {
589 const {tests, test} = createTestCollector()
590 test('gallery-index-md-keys-match-sites', async () => {
591 try {
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)`}
596 } catch (err) {
597 return {passed: false, msg: err.message}
598 }
599 })
600 await runTestsStandalone({tests, suiteName: 'peatsiteGallery', ep: importMetaUrl, failFast: false})
601 }
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})
610 return
611 }
613 await actionsH[action]()