import { readdir, mkdir } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { createRouter } from '../../create-router'; import { DATA_PATH, getResourcesDir } from '../../data-path'; type ResourceCredentials = { apiKey?: string; username?: string; password?: string; }; export type ResourceConnectionConfig = { url: string; credentials?: ResourceCredentials; }; type ResourcesConfig = Record; type Resource = { id: string; name: string; subtitle: string; type: string; port: string | null; path: string | null; description: string; installCommand: string | null; uninstallCommand: string | null; manageCommand: string | null; verifyCommand: string | null; updateCommand: string | null; installed: boolean; version: string | null; connectionConfig: ResourceConnectionConfig | null; }; const stripBackticks = (value: string) => value.replace(/^`(.+)`$/, '$1'); const resolveCommand = (command: string): string => { return command.replace(/\$DATA_PATH/g, DATA_PATH); }; function parseResourceFile( filename: string, content: string, ): Omit { const id = filename .replace(/^SERVICE_/, '') .replace(/\.md$/, '') .toLowerCase() .replace(/_/g, '-'); const headingMatch = content.match(/^#\s+(.+?)\s+—\s+(.+)$/m); const name = headingMatch?.[1] ?? id; const subtitle = headingMatch?.[2] ?? ''; const field = (key: string): string | null => { const match = content.match(new RegExp(`^-\\s+\\*\\*${key}:\\*\\*\\s+(.+)$`, 'm')); return match?.[1]?.trim() ?? null; }; const rawType = field('Type') ?? 'native'; const rawPort = field('Port'); const port = rawPort && !rawPort.startsWith('none') ? rawPort : null; const rawPath = field('Path'); return { id, name, subtitle, type: rawType, port, path: rawPath ? stripBackticks(rawPath) : null, description: field('Description') ?? '', installCommand: field('Install') ? resolveCommand(stripBackticks(field('Install')!)) : null, uninstallCommand: field('Uninstall') ? resolveCommand(stripBackticks(field('Uninstall')!)) : null, manageCommand: field('Manage') ? resolveCommand(stripBackticks(field('Manage')!)) : null, verifyCommand: field('Verify') ? resolveCommand(stripBackticks(field('Verify')!)) : null, updateCommand: field('Update') ? resolveCommand(stripBackticks(field('Update')!)) : null, }; } const CONFIG_FILENAME = 'resources-config.json'; const getConfigPath = () => join(getResourcesDir(), CONFIG_FILENAME); export async function readConfig(): Promise { const path = getConfigPath(); if (!existsSync(path)) return {}; const text = await Bun.file(path).text(); return JSON.parse(text) as ResourcesConfig; } async function writeConfig(config: ResourcesConfig): Promise { const dir = getResourcesDir(); if (!existsSync(dir)) await mkdir(dir, { recursive: true }); await Bun.write(getConfigPath(), JSON.stringify(config, null, 2)); } const CHECK_TIMEOUT_MS = 3_000; async function checkPort(port: number): Promise { try { const socket = await Bun.connect({ hostname: '127.0.0.1', port, socket: { data() {}, open(s) { s.end(); }, error() {}, }, }); socket.end(); return true; } catch { return false; } } async function checkVerifyCommand(command: string): Promise<{ installed: boolean; version: string | null }> { try { const proc = Bun.spawn(['sh', '-c', command], { stdout: 'pipe', stderr: 'pipe' }); const timeout = new Promise((_, reject) => setTimeout(() => { proc.kill(); reject(new Error('timeout')); }, CHECK_TIMEOUT_MS), ); const result = Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]); const [stdout, stderr] = await Promise.race([result, timeout]); if (proc.exitCode !== 0) return { installed: false, version: null }; const output = (stdout + stderr).trim(); const versionMatch = output.match(/(\d+\.\d+[\w.-]*)/); return { installed: true, version: versionMatch?.[1] ?? null }; } catch { return { installed: false, version: null }; } } function extractFirstPort(portStr: string): number | null { const match = portStr.match(/(\d+)/); return match ? parseInt(match[1]!, 10) : null; } async function checkUrl(url: string): Promise { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS); await fetch(url, { signal: controller.signal }); clearTimeout(timeout); return true; } catch { return false; } } type CheckResourceStatusParams = { resource: Omit; configUrl?: string; }; async function checkResourceStatus({ resource, configUrl, }: CheckResourceStatusParams): Promise<{ installed: boolean; version: string | null }> { if (resource.path) { const fullPath = `${getResourcesDir()}/${resource.path}`; return { installed: existsSync(fullPath), version: null }; } if (resource.port) { if (configUrl) { const reachable = await checkUrl(configUrl); if (reachable) return { installed: true, version: null }; } const port = extractFirstPort(resource.port); if (port) { const reachable = await checkPort(port); return { installed: reachable, version: null }; } } if (resource.verifyCommand) { return checkVerifyCommand(resource.verifyCommand); } return { installed: false, version: null }; } function buildConnectionConfig( resource: Omit, config: ResourcesConfig, ): ResourceConnectionConfig | null { if (!resource.port) return null; if (config[resource.id]) return config[resource.id]!; const port = extractFirstPort(resource.port); return { url: `http://127.0.0.1:${port ?? resource.port}` }; } async function loadResources(): Promise { const dir = getResourcesDir(); if (!existsSync(dir)) return []; const [files, config] = await Promise.all([readdir(dir), readConfig()]); const serviceFiles = files.filter((f) => f.startsWith('SERVICE_') && f.endsWith('.md')); const parsed = await Promise.all( serviceFiles.map(async (filename) => { const content = await Bun.file(`${dir}/${filename}`).text(); return parseResourceFile(filename, content); }), ); const resources = await Promise.all( parsed.map(async (r) => { const configUrl = config[r.id]?.url; const status = await checkResourceStatus({ resource: r, configUrl }); const connectionConfig = buildConnectionConfig(r, config); return { ...r, ...status, connectionConfig }; }), ); return resources; } export const resourcesRouter = createRouter(); resourcesRouter.get('/', async (ctx) => { const resources = await loadResources(); return ctx.json(resources); }); resourcesRouter.get('/config', async (ctx) => { const config = await readConfig(); return ctx.json(config); }); resourcesRouter.patch('/config/:id', async (ctx) => { const id = ctx.req.param('id'); const body = await ctx.req.json>(); const config = await readConfig(); const existing = config[id] ?? { url: '' }; config[id] = { ...existing, ...body }; await writeConfig(config); return ctx.json(config[id]); }); resourcesRouter.post('/:id/ping', async (ctx) => { const id = ctx.req.param('id'); const body = await ctx.req.json<{ url?: string }>().catch((): { url?: string } => ({})); const config = await readConfig(); const resources = await loadResources(); const resource = resources.find((r) => r.id === id); if (!resource) return ctx.json({ error: 'Resource not found' }, 404); const url = body.url ?? config[id]?.url ?? resource.connectionConfig?.url; if (!url) return ctx.json({ error: 'No URL configured' }, 400); try { const start = performance.now(); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS); await fetch(url, { signal: controller.signal }); clearTimeout(timeout); const latencyMs = Math.round(performance.now() - start); return ctx.json({ reachable: true, latencyMs }); } catch { return ctx.json({ reachable: false, latencyMs: null }); } }); resourcesRouter.post('/error-log', async (ctx) => { const body = await ctx.req.json<{ command: string; output: string; exitCode: number }>(); const timestamp = Date.now(); const filePath = `/tmp/officer-error-${timestamp}.md`; const md = [ `# Command Failed (exit code ${body.exitCode})`, '', '```', body.command, '```', '', '## Output', '', '```', body.output, '```', ].join('\n'); await Bun.write(filePath, md); return ctx.json({ filePath }); }); resourcesRouter.get('/:id', async (ctx) => { const resources = await loadResources(); const resource = resources.find((r) => r.id === ctx.req.param('id')); if (!resource) return ctx.json({ error: 'Resource not found' }, 404); return ctx.json(resource); });