From 9487723588e2670986a5a90c162be7b0f3bbf901 Mon Sep 17 00:00:00 2001 From: Chintanpatel24 <216989679+Chintanpatel24@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:40:59 +0000 Subject: [PATCH] fix: resolve installation symlinks, support skip setup, and add release sync CI/CD - Implements portable symlink path resolution in bin/arrowcode, fixing 'index.ts not found' on Linux and macOS. - Updates src/setup/wizard.ts and src/index.ts to ask users if they want to configure API keys now, adding option '0' to Cancel/Set up later, and exiting gracefully with 0. - Adds .github/workflows/release.yml to automate version bumping and synchronization across all files on release publishing or manual dispatch. - Fixes BASH_SOURCE[0] unbound variable bug in install.sh, allowing the script to be piped directly from curl to bash without errors. - Integrates a beautiful interactive CLI REPL mode (like Claude Code and Codex) for non-TTY or headless terminal environments. - Fixes isConfigured() to load .env first so configured API keys are not checked or asked for twice. --- src/config/load.ts | 16 +++---- src/index.ts | 104 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/config/load.ts b/src/config/load.ts index 3fec9d6..355b591 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -88,7 +88,9 @@ export function saveEnv(vars: Record): void { } export function isConfigured(): boolean { - // process env first (no home required) + // Always load .env first to populate process.env! + loadDotEnv(); + const key = process.env.NVIDIA_API_KEY || process.env.NIM_API_KEY || @@ -96,19 +98,13 @@ export function isConfigured(): boolean { process.env.ANTHROPIC_API_KEY || process.env.ARROWCODE_API_KEY; if (key) return true; + if (process.env.ARROWCODE_PROVIDER === "ollama") return true; + for (const p of ["ORCH", "FE", "BE", "QA"]) { if (process.env[`ARROW_${p}_API_KEY`]) return true; } - // only then peek at user home files if they already exist - loadDotEnv(); - if (existsSync(ENV_PATH)) { - const again = - process.env.NVIDIA_API_KEY || - process.env.OPENAI_API_KEY || - process.env.ARROWCODE_API_KEY; - if (again) return true; - } + if (existsSync(CONFIG_PATH)) { try { const c = parseYaml(readFileSync(CONFIG_PATH, "utf8")) as Partial; diff --git a/src/index.ts b/src/index.ts index c23fcf6..8552b10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -175,14 +175,14 @@ async function main() { const prompt = positionals.join(" ").trim(); const harness = new Harness(cfg); - if (values["no-tui"]) { - if (!prompt) { - console.error("Headless mode requires a prompt."); - process.exit(1); - } + // Fallback to non-TUI mode if terminal is not a TTY or if requested + const isTTY = !!(process.stdout.isTTY && process.stdin.isTTY); + const useNoTui = !!(values["no-tui"] || !isTTY); + + if (useNoTui || prompt !== "") { harness.events.on((e) => { if (e.type === "phase") - console.log(`[phase] ${e.phase} ${e.detail || ""}`); + console.log(`\n[phase] ${e.phase.toUpperCase()} ${e.detail || ""}`); else if (e.type === "agent_log" && e.line.kind !== "say") console.log(`[${e.agent}] ${e.line.kind}: ${e.line.text}`); else if (e.type === "agent_log" && e.line.kind === "say") @@ -192,30 +192,94 @@ async function main() { `[bus] ${e.message.from} -> ${e.message.to} [${e.message.kind}] ${e.message.title}`, ); else if (e.type === "system") console.log(`[system] ${e.text}`); - else if (e.type === "plan") - console.log(`[plan] ${e.plan.title}\n${e.plan.summary}`); - else if (e.type === "questions") - console.log( - `[questions]\n${e.questions.map((q, i) => `${i + 1}. ${q.question}`).join("\n")}`, - ); - else if (e.type === "final") console.log("\n=== READY ===\n" + e.text); + else if (e.type === "plan") { + console.log(`\n=================== PLAN ===================`); + console.log(`Title: ${e.plan.title}`); + console.log(`Summary: ${e.plan.summary}`); + console.log(`============================================\n`); + } else if (e.type === "questions") { + console.log(`\n================= QUESTIONS =================`); + console.log(e.questions.map((q, i) => `${i + 1}. ${q.question}`).join("\n")); + console.log(`=============================================\n`); + } else if (e.type === "final") console.log("\n=== READY ===\n" + e.text); else if (e.type === "swarm") console.log(`[swarm] ${e.action} ${e.workerId} active=${e.active}`); else if (e.type === "approval_request") { if (cfg.autoApprove) harness.resolveApproval(e.id, true); else { - console.log(`[approval] denied (use -y): ${e.agent} ${e.tool}`); + console.log(`[approval] denied (use -y/--yolo or autoApprove): ${e.agent} ${e.tool}`); harness.resolveApproval(e.id, false); } } }); - await harness.startPlan(prompt); - if (harness.plan) { - console.log("\n[headless] auto /confirm"); - await harness.confirmAndExecute(); + + if (prompt) { + await harness.startPlan(prompt); + if (harness.plan) { + console.log("\n[headless] auto /confirm"); + await harness.confirmAndExecute(); + } + console.log("\n[metrics]", harness.metricsLine()); + process.exit(0); + } else { + // CLI Interactive Session (like Claude Code) + const readline = await import("node:readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + console.log("\n================================================================"); + console.log(" /\\"); + console.log(" / \\ ARROWCODE Interactive CLI"); + console.log(" / /\\ \\ swarm coding harness"); + console.log(" /______\\ ORCH · FE · BE · QA"); + console.log("================================================================\n"); + console.log(" Welcome! You are in the interactive CLI mode (similar to Claude Code)."); + console.log(" - Type standard prompt to chat with the agents."); + console.log(" - Use /plan to propose a new plan."); + console.log(" - Use /confirm to approve and execute the current plan."); + console.log(" - Use /accept to finish and save changes."); + console.log(" - Use /help to see all available commands."); + console.log(" - Type /exit or /quit to quit.\n"); + + const promptUser = () => { + rl.question("arrowcode> ", async (answer) => { + const line = answer.trim(); + if (!line) { + promptUser(); + return; + } + if (line === "/exit" || line === "/quit") { + rl.close(); + process.exit(0); + } + + if (line.startsWith("/")) { + // Dispatch command + const [cmd, ..._rest] = line.slice(1).split(/\s+/); + const { dispatchCommand } = await import("./commands/registry"); + const res = await dispatchCommand(line, { + harness, + setYolo: () => {}, + getYolo: () => false, + }); + if (res.type === "exit") { + rl.close(); + process.exit(0); + } + if (res && "message" in res && res.message) { + console.log(`[system] ${res.message}`); + } + } else { + await harness.chat(line); + } + promptUser(); + }); + }; + promptUser(); + return; } - console.log("\n[metrics]", harness.metricsLine()); - process.exit(0); } const { waitUntilExit } = render(