/** * init-app.tsx — Ink application wrapper for `woco init`. * * Wires together auto-detection, the InitForm component, and file writing. * This component: * 1. Auto-detects project name, base branch, build/install commands * 2. Renders the InitForm with detected defaults * 3. On confirm, calls writeInitFiles and installs the agent template * 4. On cancel, exits cleanly * * Usage from cmdInit: * render() */ import React, { useState, useCallback, useMemo } from "react"; import { Box, Text, useApp, render } from "ink"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { InitForm, type InitFormDefaults } from "./init-form"; import { detectProjectName, detectBaseBranch, detectBuildCommand, detectInstallCommand, } from "./init-detect"; import { writeInitFiles } from "./init-writer"; import { loadConfig } from "../config"; import { renderAgentTemplate } from "../lib/templates"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface InitAppProps { /** Absolute path to the project root. */ projectRoot: string; /** Whether to overwrite existing config. */ force: boolean; } type InitState = | { phase: "form" } | { phase: "done"; createdFiles: string[] } | { phase: "error"; message: string } | { phase: "cancelled" }; // --------------------------------------------------------------------------- // InitApp // --------------------------------------------------------------------------- /** * InitApp — top-level Ink component for `woco init`. * * Auto-detects project settings, shows confirmation form, writes files. */ export function InitApp({ projectRoot, force }: InitAppProps): React.ReactElement { const app = useApp(); // Auto-detect defaults (computed once on mount) const detected = useMemo( (): InitFormDefaults => ({ baseBranch: detectBaseBranch(projectRoot), buildCommand: detectBuildCommand(projectRoot), installCommand: detectInstallCommand(projectRoot), }), [projectRoot], ); const projectName = useMemo( () => detectProjectName(projectRoot), [projectRoot], ); const [state, setState] = useState({ phase: "form" }); const handleConfirm = useCallback( (values: InitFormDefaults) => { try { const result = writeInitFiles(projectRoot, values, force); // Install agent template try { installAgentTemplate(projectRoot); result.createdFiles.push(".opencode/agents/generalist-agent.md"); } catch { // Non-fatal — agent template install is optional } setState({ phase: "done", createdFiles: result.createdFiles }); // Exit after a short delay to let the user see the result setTimeout(() => app.exit(), 500); } catch (err: any) { setState({ phase: "error", message: err.message }); setTimeout(() => app.exit(), 1000); } }, [projectRoot, force, app], ); const handleCancel = useCallback(() => { setState({ phase: "cancelled" }); setTimeout(() => app.exit(), 200); }, [app]); // --- Render based on state --- if (state.phase === "done") { return ( ✓ Project initialized! {state.createdFiles.map((file, idx) => ( Created {file} ))} Run woco help to see available commands. ); } if (state.phase === "error") { return ( ✗ Init failed: {state.message} ); } if (state.phase === "cancelled") { return ( Init cancelled. ); } return ( ); } // --------------------------------------------------------------------------- // Render entry point // --------------------------------------------------------------------------- /** * Mount the InitApp and wait for it to exit. * * This is the entry point called by cmdInit. It renders the Ink app, * waits for the user to confirm/cancel, then resolves. */ export async function renderInitApp(props: InitAppProps): Promise { const instance = render(); await instance.waitUntilExit(); } // --------------------------------------------------------------------------- // Agent template helper // --------------------------------------------------------------------------- /** * Install the generalist agent template into .opencode/agents/. * Uses the project's loaded config and the template renderer to create * the agent definition file. */ function installAgentTemplate(projectRoot: string): void { const config = loadConfig(projectRoot); const agentDir = resolve(projectRoot, ".opencode", "agents"); const agentDefPath = resolve(agentDir, `${config.agent.name}.md`); mkdirSync(agentDir, { recursive: true }); const template = renderAgentTemplate(config, projectRoot); writeFileSync(agentDefPath, template, "utf-8"); }