import { Hype } from '@because/hype' import { define, stylesToCSS } from '@because/forge' import { baseStyles, ToolScript, theme } from '@because/toes/tools' import { readdir, readlink, stat } from 'fs/promises' import { join } from 'path' import type { Child } from 'hono/jsx' const APPS_DIR = process.env.APPS_DIR! const TOES_URL = process.env.TOES_URL! const app = new Hype({ prettyHTML: false }) const Container = define('Container', { fontFamily: theme('fonts-sans'), padding: '20px', paddingTop: 0, maxWidth: '800px', margin: '0 auto', color: theme('colors-text'), }) const Header = define('Header', { marginBottom: '20px', paddingBottom: '10px', borderBottom: `2px solid ${theme('colors-border')}`, }) const Title = define('Title', { margin: 0, fontSize: '24px', fontWeight: 'bold', }) const Subtitle = define('Subtitle', { color: theme('colors-textMuted'), fontSize: '18px', marginTop: '5px', }) const VersionList = define('VersionList', { listStyle: 'none', padding: 0, margin: '20px 0', border: `1px solid ${theme('colors-border')}`, borderRadius: theme('radius-md'), overflow: 'hidden', }) const VersionItem = define('VersionItem', { padding: '12px 15px', borderBottom: `1px solid ${theme('colors-border')}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', states: { ':last-child': { borderBottom: 'none', }, ':hover': { backgroundColor: theme('colors-bgHover'), }, }, }) const VersionLink = define('VersionLink', { base: 'a', textDecoration: 'none', color: theme('colors-link'), fontFamily: theme('fonts-mono'), fontSize: '15px', cursor: 'pointer', states: { ':hover': { textDecoration: 'underline', }, }, }) const Badge = define('Badge', { fontSize: '12px', padding: '2px 8px', borderRadius: theme('radius-md'), backgroundColor: theme('colors-bgElement'), color: theme('colors-statusRunning'), fontWeight: 'bold', }) const ErrorBox = define('ErrorBox', { color: theme('colors-error'), padding: '20px', backgroundColor: theme('colors-bgElement'), borderRadius: theme('radius-md'), margin: '20px 0', }) interface LayoutProps { title: string subtitle?: string children: Child } function Layout({ title, subtitle, children }: LayoutProps) { return ( {title}
Versions
{children}
) } app.get('/ok', c => c.text('ok')) app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8', })) async function getVersions(appPath: string): Promise<{ name: string; isCurrent: boolean }[]> { const entries = await readdir(appPath, { withFileTypes: true }) let currentTarget = '' try { currentTarget = await readlink(join(appPath, 'current')) } catch { } return entries .filter(e => e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name)) .map(e => ({ name: e.name, isCurrent: e.name === currentTarget })) .sort((a, b) => b.name.localeCompare(a.name)) } function formatTimestamp(ts: string): string { return `${ts.slice(0, 4)}-${ts.slice(4, 6)}-${ts.slice(6, 8)} ${ts.slice(9, 11)}:${ts.slice(11, 13)}:${ts.slice(13, 15)}` } app.get('/', async c => { const appName = c.req.query('app') if (!appName) { return c.html( Please specify an app name with ?app=<name> ) } const appPath = join(APPS_DIR, appName) try { await stat(appPath) } catch { return c.html( App "{appName}" not found ) } const versions = await getVersions(appPath) if (versions.length === 0) { return c.html( No versions found ) } return c.html( {versions.map(v => ( {formatTimestamp(v.name)} {v.isCurrent && current} ))} ) }) export default app.defaults