#!/usr/bin/env node import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createServer } from 'node:net'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const enabled = process.env.NIANCODE_OPENCODE_REAL_SMOKE === '1' || process.env.OPENCODE_REAL_SMOKE === '1'; function skip(reason) { console.log(`[opencode-real-smoke] skipped: ${reason}`); process.exit(0); } if (!enabled) { skip('set NIANCODE_OPENCODE_REAL_SMOKE=1 to launch a real opencode runtime'); } function resolveOpencodeBin() { const explicit = process.env.NIANCODE_OPENCODE_BIN || process.env.OPENCODE_BIN; if (explicit) return explicit; const binName = process.platform === 'win32' ? 'opencode.cmd' : 'opencode'; const localBin = join(repoRoot, 'node_modules', '.bin', binName); if (existsSync(localBin)) return localBin; return binName; } async function resolveConfigContent() { if (process.env.OPENCODE_CONFIG_CONTENT) return process.env.OPENCODE_CONFIG_CONTENT; if (process.env.OPENCODE_REAL_SMOKE_CONFIG) return process.env.OPENCODE_REAL_SMOKE_CONFIG; if (process.env.OPENCODE_REAL_SMOKE_CONFIG_FILE) { return await readFile(resolve(process.env.OPENCODE_REAL_SMOKE_CONFIG_FILE), 'utf8'); } return null; } async function getFreePort() { return await new Promise((resolvePort, reject) => { const server = createServer(); server.once('error', reject); server.listen(0, '127.0.0.1', () => { const address = server.address(); const port = typeof address === 'object' && address ? address.port : null; server.close(() => { if (typeof port === 'number') { resolvePort(port); return; } reject(new Error('Unable to reserve a local smoke-test port')); }); }); }); } function waitForListening(proc, timeoutMs) { return awaitableProcessOutput(proc, timeoutMs, (text) => { const match = text.match(/opencode server listening\s+on\s+(https?:\/\/[^\s]+)/); return match?.[1] ?? null; }); } function awaitableProcessOutput(proc, timeoutMs, resolveFromText) { return new Promise((resolveUrl, reject) => { let settled = false; let stderr = ''; const finish = (callback) => { if (settled) return; settled = true; clearTimeout(timer); callback(); }; const timer = setTimeout(() => { finish(() => reject(new Error(`Timed out waiting for opencode server after ${timeoutMs}ms${stderr ? `\n${stderr}` : ''}`))); }, timeoutMs); proc.stdout?.on('data', (chunk) => { const text = chunk.toString(); process.stdout.write(text); const result = resolveFromText(text); if (result) finish(() => resolveUrl(result)); }); proc.stderr?.on('data', (chunk) => { const text = chunk.toString(); stderr += text; process.stderr.write(text); }); proc.on('error', (error) => finish(() => reject(error))); proc.on('exit', (code) => { finish(() => reject(new Error(`opencode exited before it became ready: ${code ?? 'unknown'}${stderr ? `\n${stderr}` : ''}`))); }); }); } function sessionIdOf(session) { if (!session || typeof session !== 'object') return null; const value = session.id ?? session.sessionID ?? session.sessionId; return typeof value === 'string' && value.trim() ? value : null; } async function requestJson(url, init = {}) { const response = await fetch(url, { ...init, headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}), }, }); if (!response.ok) { const body = await response.text().catch(() => ''); throw new Error(`opencode request failed ${response.status} ${response.statusText}${body ? `\n${body}` : ''}`); } if (response.status === 204) return null; return await response.json(); } async function createProjectDir() { if (process.env.OPENCODE_REAL_SMOKE_PROJECT) { return { path: resolve(process.env.OPENCODE_REAL_SMOKE_PROJECT), cleanup: async () => {}, }; } const path = await mkdtemp(join(tmpdir(), 'niancode-opencode-smoke-')); await writeFile(join(path, 'README.md'), '# Makelore opencode real-runtime smoke\n'); return { path, cleanup: async () => { await rm(path, { recursive: true, force: true }); }, }; } const configContent = await resolveConfigContent(); if (!configContent && process.env.OPENCODE_REAL_SMOKE_ALLOW_LOCAL_CONFIG !== '1') { skip('provide OPENCODE_CONFIG_CONTENT, OPENCODE_REAL_SMOKE_CONFIG, or OPENCODE_REAL_SMOKE_CONFIG_FILE'); } JSON.parse(configContent ?? '{}'); const binPath = resolveOpencodeBin(); const port = Number(process.env.OPENCODE_REAL_SMOKE_PORT) || await getFreePort(); const startupTimeoutMs = Number(process.env.OPENCODE_REAL_SMOKE_STARTUP_TIMEOUT_MS) || 20_000; const apiTimeoutMs = Number(process.env.OPENCODE_REAL_SMOKE_API_TIMEOUT_MS) || 60_000; const project = await createProjectDir(); const proc = spawn(binPath, ['serve', '--hostname=127.0.0.1', `--port=${port}`], { env: { ...process.env, ...(configContent ? { OPENCODE_CONFIG_CONTENT: configContent } : {}), }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: process.platform === 'win32' && /\.cmd$/i.test(binPath), }); try { const baseUrl = await waitForListening(proc, startupTimeoutMs); const directoryQuery = `directory=${encodeURIComponent(project.path)}`; const scopedHeaders = { 'x-opencode-directory': encodeURIComponent(project.path) }; const sessions = await requestJson(`${baseUrl}/session?${directoryQuery}`, { signal: AbortSignal.timeout(apiTimeoutMs), headers: scopedHeaders, }); if (!Array.isArray(sessions)) { throw new Error('Expected /session to return an array'); } const session = await requestJson(`${baseUrl}/session?${directoryQuery}`, { method: 'POST', body: JSON.stringify({}), signal: AbortSignal.timeout(apiTimeoutMs), headers: scopedHeaders, }); const sessionId = sessionIdOf(session); if (!sessionId) { throw new Error('Created opencode session did not include an id'); } if (process.env.OPENCODE_REAL_SMOKE_PROMPT) { await requestJson(`${baseUrl}/session/${encodeURIComponent(sessionId)}/message?${directoryQuery}`, { method: 'POST', body: JSON.stringify({ parts: [ { type: 'text', text: process.env.OPENCODE_REAL_SMOKE_PROMPT }, ], }), signal: AbortSignal.timeout(apiTimeoutMs), headers: scopedHeaders, }); } console.log(`[opencode-real-smoke] ok: started ${baseUrl}, listed ${sessions.length} session(s), created ${sessionId}`); } finally { proc.kill(); await project.cleanup(); }