/**
* 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");
}