// src/installer.ts import { copy } from "jsr:@std/fs/copy"; import { ensureDir } from "jsr:@std/fs/ensure-dir"; import { join, resolve } from "jsr:@std/path"; async function runCmd( cmd: string, args: string[], cwd: string, ): Promise { const command = new Deno.Command(cmd, { args, cwd, stdout: "piped", stderr: "piped", }); const output = await command.output(); if (output.code !== 0) { const errText = new TextDecoder().decode(output.stderr).trim(); throw new Error( `Command '${cmd} ${args.join(" ")}' failed in ${cwd}: ${errText}`, ); } return new TextDecoder().decode(output.stdout).trim(); } export async function main(args: string[] = Deno.args) { const rawTarget = args[0] ?? Deno.cwd(); const targetDir = resolve(rawTarget); const engineRoot = resolve(new URL(".", import.meta.url).pathname, ".."); console.log(`-> Installing Agent Forum from ${engineRoot} to ${targetDir}`); // 1. Verify target is a git repository await runCmd("git", ["rev-parse", "--show-toplevel"], targetDir); // 2. Scaffolding directories const targetForum = join(targetDir, ".forum"); await ensureDir(join(targetForum, "src")); await ensureDir(join(targetForum, "hooks")); await ensureDir(join(targetForum, "config")); // 3. Copy engine source and hooks await copy(join(engineRoot, "src"), join(targetForum, "src"), { overwrite: true, }); await copy(join(engineRoot, "src", "hooks"), join(targetForum, "hooks"), { overwrite: true, }); // 4. Configure git hooks path await runCmd("git", ["config", "core.hooksPath", ".forum/hooks"], targetDir); // 5. Append managed block to target .gitignore const gitignorePath = join(targetDir, ".gitignore"); let gitignoreContent = ""; try { gitignoreContent = await Deno.readTextFile(gitignorePath); } catch { // File does not exist yet } const managedHeader = "# === AGENT-FORUM MANAGED BLOCK ==="; const managedFooter = "# === END AGENT-FORUM MANAGED BLOCK ==="; const block = [ managedHeader, ".forum/ast/", ".forum/security/", ".forum/reports/", ".forum/worktree_meta/", managedFooter, ].join("\n"); if (!gitignoreContent.includes(managedHeader)) { const updated = gitignoreContent ? `${gitignoreContent.trimEnd()}\n\n${block}\n` : `${block}\n`; await Deno.writeTextFile(gitignorePath, updated); } // 5.5 Create agent-forum configuration in the target root if it does not exist const configPath = join(targetDir, "agent-forum.json"); try { await Deno.stat(configPath); console.log( "-> Existing agent-forum.json found in repository root. Preserving it.", ); } catch { console.log("-> Creating default agent-forum.json configuration..."); const defaultConfig = { "stryker": { "ignoreTests": ["test/integration/"], "showProgress": true, }, }; await Deno.writeTextFile( configPath, JSON.stringify(defaultConfig, null, 2), ); } // Cleanup old config if it exists in .forum/ const oldConfigPath = join(targetForum, "agent-forum.json"); try { await Deno.stat(oldConfigPath); await Deno.remove(oldConfigPath); console.log("-> Cleaned up old agent-forum.json from .forum/ directory."); } catch { // Old config does not exist, nothing to clean up } // 6. Bootstrap meta-state branch await runCmd( "deno", ["run", "-A", join(targetForum, "src", "cli.ts"), "bootstrap"], targetDir, ); // 7. Sync Vector Database console.log("-> Synchronizing Vector Databases..."); await runCmd( "deno", [ "run", "-A", join(targetForum, "src", "cli.ts"), "replay", "--sync", ], targetDir, ); console.log("✅ Agent Forum installed successfully."); } if (import.meta.main) { await main(Deno.args); }