baro-ai 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # baro
2
+
3
+ Autonomous parallel coding engine. Give it a goal, it breaks it into stories, builds a dependency DAG, and executes them in parallel using AI agents.
4
+
5
+ ```
6
+ npm install -g baro-ai
7
+ ```
8
+
9
+ ## How it works
10
+
11
+ 1. **You describe a goal** - "Add authentication with JWT and role-based access control"
12
+ 2. **AI plans the work** - Claude (or OpenAI) explores your codebase and creates a dependency graph of user stories
13
+ 3. **You review the plan** - Scrollable plan review with accept/refine/quit
14
+ 4. **Stories execute in parallel** - Independent stories run simultaneously, each with its own Claude agent
15
+ 5. **Live TUI dashboard** - Watch progress, logs, DAG visualization, and stats in real-time
16
+
17
+ ## Usage
18
+
19
+ ### Interactive mode
20
+
21
+ ```bash
22
+ baro
23
+ ```
24
+
25
+ Opens the welcome screen where you type your goal and choose a planner.
26
+
27
+ ### Direct mode
28
+
29
+ ```bash
30
+ baro "Add a REST API for user management"
31
+ ```
32
+
33
+ Skips the welcome screen and starts planning immediately.
34
+
35
+ ### Options
36
+
37
+ ```
38
+ baro [goal] [options]
39
+
40
+ Arguments:
41
+ goal Project goal (opens welcome screen if omitted)
42
+
43
+ Options:
44
+ --planner <planner> Planner to use: claude or openai (default: claude)
45
+ --cwd <path> Working directory (default: current directory)
46
+ -h, --help Print help
47
+ ```
48
+
49
+ ### Examples
50
+
51
+ ```bash
52
+ # Interactive - opens welcome screen
53
+ baro
54
+
55
+ # Plan and execute with Claude (default)
56
+ baro "Refactor the database layer to use connection pooling"
57
+
58
+ # Use OpenAI for planning
59
+ baro --planner openai "Add WebSocket support"
60
+
61
+ # Run in a specific directory
62
+ baro --cwd ~/projects/myapp "Add unit tests for all API endpoints"
63
+ ```
64
+
65
+ ## TUI Screens
66
+
67
+ ### Welcome
68
+
69
+ ASCII art logo, goal text input, and planner toggle (Claude/OpenAI).
70
+
71
+ ### Planning
72
+
73
+ Animated spinner showing planning progress with elapsed timer. The selected AI explores your codebase and generates a structured plan.
74
+
75
+ ### Review
76
+
77
+ Scrollable list of all planned stories with descriptions and dependencies. Navigate with arrow keys, accept with Enter, or quit with q.
78
+
79
+ ### Execution Dashboard
80
+
81
+ Three tabs while stories execute:
82
+
83
+ - **Dashboard** - Story list with status icons + live logs from the active agent
84
+ - **DAG** - Dependency graph visualization showing levels and connections
85
+ - **Stats** - Summary table with times, file counts, and completion stats
86
+
87
+ Keybinds: `1/2/3` switch tabs, `Tab/Shift+Tab` switch log panels, `q` quit.
88
+
89
+ ## Requirements
90
+
91
+ - [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli) installed and authenticated (for Claude planner/executor)
92
+ - Node.js 18+ (only needed if using `--planner openai`)
93
+ - macOS (arm64/x64) or Linux (x64/arm64)
94
+
95
+ ## Architecture
96
+
97
+ Baro is a Rust binary distributed via npm:
98
+
99
+ - **TUI** - ratatui-based terminal UI with 4 screens
100
+ - **Planner** - Spawns Claude CLI or OpenAI (via Node.js bridge) to generate a PRD
101
+ - **DAG Engine** - Kahn's algorithm for topological sort with level grouping
102
+ - **Executor** - Parallel story execution via tokio, one Claude agent per story
103
+ - **npm package** - `postinstall` downloads the prebuilt binary for your platform
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ # Build the Rust binary
109
+ cargo build -p baro-tui --release
110
+
111
+ # Run locally
112
+ ./target/release/baro "your goal"
113
+
114
+ # The binary is in crates/baro-tui/
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
package/bin/baro ADDED
Binary file
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/planner.ts
4
+ import { z } from "zod";
5
+ import { zodToJsonSchema } from "zod-to-json-schema";
6
+
7
+ // src/core/stream.ts
8
+ var API_URL = "https://api.openai.com/v1/responses";
9
+ var MAX_TOOL_ROUNDS = 15;
10
+ async function streamCompletion(opts) {
11
+ const apiKey = process.env.OPENAI_API_KEY;
12
+ if (!apiKey) throw new Error("OPENAI_API_KEY not set");
13
+ let input = [
14
+ ...opts.messages.map((m) => ({ role: m.role, content: m.content })),
15
+ { role: "user", content: opts.task }
16
+ ];
17
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
18
+ const body = { model: opts.model, input, stream: true };
19
+ if (opts.reasoning) body.reasoning = opts.reasoning;
20
+ if (opts.jsonSchema) {
21
+ body.text = {
22
+ format: { type: "json_schema", name: "prd_output", schema: opts.jsonSchema, strict: true }
23
+ };
24
+ }
25
+ if (opts.tools?.length) {
26
+ body.tools = opts.tools.map((t) => ({
27
+ type: "function",
28
+ name: t.name,
29
+ description: t.description,
30
+ parameters: t.parameters
31
+ }));
32
+ }
33
+ const response = await fetch(API_URL, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
36
+ body: JSON.stringify(body)
37
+ });
38
+ if (!response.ok) {
39
+ const errText = await response.text();
40
+ throw new Error(`OpenAI API error ${response.status}: ${errText}`);
41
+ }
42
+ if (!response.body) throw new Error("No response body");
43
+ const { textOutput, toolCalls, responseId } = await parseSSE(response.body, opts);
44
+ if (toolCalls.length === 0) {
45
+ return textOutput;
46
+ }
47
+ const toolResults = [];
48
+ for (const tc of toolCalls) {
49
+ const toolDef = opts.tools?.find((t) => t.name === tc.name);
50
+ if (!toolDef) continue;
51
+ opts.onToolCall?.(tc.name, tc.args);
52
+ let result;
53
+ try {
54
+ const parsed = JSON.parse(tc.args);
55
+ result = await toolDef.invoke(parsed);
56
+ } catch (err) {
57
+ result = `Error: ${err.message}`;
58
+ }
59
+ toolResults.push({
60
+ type: "function_call_output",
61
+ call_id: tc.callId,
62
+ output: typeof result === "string" ? result : JSON.stringify(result)
63
+ });
64
+ }
65
+ input = toolResults;
66
+ }
67
+ throw new Error("Too many tool calling rounds");
68
+ }
69
+ async function parseSSE(body, opts) {
70
+ let textOutput = "";
71
+ const toolCalls = [];
72
+ const toolCallArgs = /* @__PURE__ */ new Map();
73
+ let responseId = "";
74
+ const reader = body.getReader();
75
+ const decoder = new TextDecoder();
76
+ let buffer = "";
77
+ while (true) {
78
+ const { done, value } = await reader.read();
79
+ if (done) break;
80
+ buffer += decoder.decode(value, { stream: true });
81
+ const lines = buffer.split("\n");
82
+ buffer = lines.pop() ?? "";
83
+ for (const line of lines) {
84
+ if (!line.startsWith("data: ")) continue;
85
+ const data = line.slice(6).trim();
86
+ if (data === "[DONE]") continue;
87
+ try {
88
+ const ev = JSON.parse(data);
89
+ if (ev.type === "response.created") {
90
+ responseId = ev.response?.id ?? "";
91
+ } else if (ev.type === "response.output_text.delta") {
92
+ textOutput += ev.delta ?? "";
93
+ opts.onToken(ev.delta ?? "");
94
+ } else if (ev.type === "response.reasoning.delta") {
95
+ opts.onThinking?.(ev.delta ?? "");
96
+ } else if (ev.type === "response.output_item.added") {
97
+ if (ev.item?.type === "function_call") {
98
+ toolCallArgs.set(ev.output_index, {
99
+ callId: ev.item.call_id ?? "",
100
+ name: ev.item.name ?? "",
101
+ args: ""
102
+ });
103
+ }
104
+ } else if (ev.type === "response.function_call_arguments.delta") {
105
+ const tc = toolCallArgs.get(ev.output_index);
106
+ if (tc) tc.args += ev.delta ?? "";
107
+ } else if (ev.type === "response.output_item.done") {
108
+ if (ev.item?.type === "function_call") {
109
+ const tc = toolCallArgs.get(ev.output_index);
110
+ if (tc) {
111
+ toolCalls.push({ callId: tc.callId, name: tc.name, args: tc.args });
112
+ }
113
+ }
114
+ }
115
+ } catch {
116
+ }
117
+ }
118
+ }
119
+ return { textOutput, toolCalls, responseId };
120
+ }
121
+
122
+ // src/core/tools.ts
123
+ import * as fs from "fs";
124
+ import * as path from "path";
125
+ import { execSync } from "child_process";
126
+ var IGNORE = /* @__PURE__ */ new Set([
127
+ "node_modules",
128
+ ".git",
129
+ "dist",
130
+ "build",
131
+ ".next",
132
+ ".nuxt",
133
+ "coverage",
134
+ ".cache",
135
+ "__pycache__",
136
+ "target",
137
+ ".output"
138
+ ]);
139
+ var MAX_FILE_SIZE = 15e3;
140
+ function createCodebaseTools(cwd2) {
141
+ return [
142
+ {
143
+ name: "list_files",
144
+ description: "List files and directories. Use path='' for project root. Returns file names with types. Ignores node_modules, .git, etc.",
145
+ parameters: {
146
+ type: "object",
147
+ properties: {
148
+ path: { type: "string", description: "Relative path from project root. Empty for root." },
149
+ recursive: { type: "boolean", description: "List all files recursively (max 200). Default false." }
150
+ },
151
+ required: ["path"],
152
+ additionalProperties: false
153
+ },
154
+ async invoke(args2) {
155
+ const target = safePath(cwd2, args2.path || ".");
156
+ if (!target || !fs.existsSync(target)) return `Directory not found: ${args2.path}`;
157
+ if (!fs.statSync(target).isDirectory()) return `Not a directory: ${args2.path}`;
158
+ const results = [];
159
+ function walk(dir, prefix, depth) {
160
+ if (results.length >= 200 || depth > 4) return;
161
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
162
+ if (IGNORE.has(entry.name) || entry.name.startsWith(".")) continue;
163
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
164
+ if (entry.isDirectory()) {
165
+ results.push(rel + "/");
166
+ if (args2.recursive) walk(path.join(dir, entry.name), rel, depth + 1);
167
+ } else {
168
+ results.push(rel);
169
+ }
170
+ }
171
+ }
172
+ walk(target, "", 0);
173
+ return results.join("\n") || "(empty directory)";
174
+ }
175
+ },
176
+ {
177
+ name: "read_file",
178
+ description: "Read file contents. Large files truncated to ~15000 chars. Use to understand code structure, configs, etc.",
179
+ parameters: {
180
+ type: "object",
181
+ properties: {
182
+ path: { type: "string", description: "Relative path to file (e.g. 'src/index.ts')" }
183
+ },
184
+ required: ["path"],
185
+ additionalProperties: false
186
+ },
187
+ async invoke(args2) {
188
+ const target = safePath(cwd2, args2.path);
189
+ if (!target || !fs.existsSync(target)) return `File not found: ${args2.path}`;
190
+ if (fs.statSync(target).isDirectory()) return `${args2.path} is a directory. Use list_files.`;
191
+ if (fs.statSync(target).size > 5e5) return `File too large (${(fs.statSync(target).size / 1024).toFixed(0)}KB)`;
192
+ let content = fs.readFileSync(target, "utf-8");
193
+ if (content.length > MAX_FILE_SIZE) {
194
+ content = content.slice(0, MAX_FILE_SIZE) + "\n... (truncated)";
195
+ }
196
+ return content;
197
+ }
198
+ },
199
+ {
200
+ name: "grep",
201
+ description: "Search for a text pattern across project files. Returns matching lines with file paths. Ignores node_modules, .git, etc.",
202
+ parameters: {
203
+ type: "object",
204
+ properties: {
205
+ pattern: { type: "string", description: "Text to search for (case-insensitive)" },
206
+ path: { type: "string", description: "Directory to search in. Default: entire project." },
207
+ file_pattern: { type: "string", description: "File glob (e.g. '*.ts'). Default: all." }
208
+ },
209
+ required: ["pattern"],
210
+ additionalProperties: false
211
+ },
212
+ async invoke(args2) {
213
+ const searchDir = safePath(cwd2, args2.path || ".");
214
+ if (!searchDir || !fs.existsSync(searchDir)) return `Directory not found: ${args2.path}`;
215
+ try {
216
+ const excludes = Array.from(IGNORE).map((d) => `--exclude-dir=${d}`).join(" ");
217
+ const include = args2.file_pattern ? `--include='${args2.file_pattern}'` : "";
218
+ const cmd = `grep -rn -i ${excludes} ${include} --max-count=50 -- ${JSON.stringify(args2.pattern)} ${JSON.stringify(searchDir)} 2>/dev/null || true`;
219
+ const output = execSync(cmd, { encoding: "utf-8", maxBuffer: 1024 * 1024 });
220
+ const lines = output.split("\n").filter(Boolean).map(
221
+ (line) => line.startsWith(cwd2) ? line.slice(cwd2.length + 1) : line
222
+ );
223
+ return lines.slice(0, 50).join("\n") || "No matches found.";
224
+ } catch {
225
+ return "No matches found.";
226
+ }
227
+ }
228
+ },
229
+ {
230
+ name: "file_tree",
231
+ description: "Get a condensed tree view of the project structure up to 3 levels deep. Good starting point to understand the codebase.",
232
+ parameters: {
233
+ type: "object",
234
+ properties: {},
235
+ additionalProperties: false
236
+ },
237
+ async invoke() {
238
+ const lines = [path.basename(cwd2) + "/"];
239
+ function walk(dir, prefix, depth) {
240
+ if (lines.length >= 150 || depth > 3) return;
241
+ let entries;
242
+ try {
243
+ entries = fs.readdirSync(dir, { withFileTypes: true });
244
+ } catch {
245
+ return;
246
+ }
247
+ entries.sort((a, b) => {
248
+ if (a.isDirectory() && !b.isDirectory()) return -1;
249
+ if (!a.isDirectory() && b.isDirectory()) return 1;
250
+ return a.name.localeCompare(b.name);
251
+ });
252
+ for (let i = 0; i < entries.length; i++) {
253
+ if (IGNORE.has(entries[i].name) || entries[i].name.startsWith(".")) continue;
254
+ const isLast = i === entries.length - 1;
255
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
256
+ const childPrefix = isLast ? " " : "\u2502 ";
257
+ if (entries[i].isDirectory()) {
258
+ lines.push(`${prefix}${connector}${entries[i].name}/`);
259
+ walk(path.join(dir, entries[i].name), prefix + childPrefix, depth + 1);
260
+ } else {
261
+ lines.push(`${prefix}${connector}${entries[i].name}`);
262
+ }
263
+ }
264
+ }
265
+ walk(cwd2, "", 0);
266
+ return lines.join("\n");
267
+ }
268
+ }
269
+ ];
270
+ }
271
+ function safePath(cwd2, filePath) {
272
+ const resolved = path.resolve(cwd2, filePath);
273
+ if (!resolved.startsWith(path.resolve(cwd2))) return null;
274
+ return resolved;
275
+ }
276
+
277
+ // src/core/planner.ts
278
+ var StorySchema = z.object({
279
+ id: z.string().describe("Short ID like S1, S2, S3"),
280
+ priority: z.number().describe("Priority level: lower = earlier"),
281
+ title: z.string().describe("Short title for the story"),
282
+ description: z.string().describe("What needs to be implemented"),
283
+ dependsOn: z.array(z.string()).describe("IDs of stories this depends on"),
284
+ retries: z.number().describe("Retry attempts if story fails (usually 2)"),
285
+ acceptance: z.array(z.string()).describe("Testable acceptance criteria"),
286
+ tests: z.array(z.string()).describe("Test commands (e.g. ['npm test'])")
287
+ });
288
+ var PrdSchema = z.object({
289
+ project: z.string().describe("Short project name"),
290
+ branchName: z.string().describe("Git branch name (kebab-case)"),
291
+ description: z.string().describe("One-line project description"),
292
+ userStories: z.array(StorySchema).describe("User stories forming a DAG")
293
+ });
294
+ var SYSTEM_PROMPT = `You are an expert software architect. Break down project goals into concrete user stories that form a dependency DAG.
295
+
296
+ IMPORTANT: Before generating a plan, USE YOUR TOOLS to explore the existing codebase:
297
+ 1. Call file_tree to see the project structure
298
+ 2. Call read_file on key files (package.json, README, entry points, configs)
299
+ 3. Call grep to find relevant patterns
300
+ 4. THEN generate a plan that fits the existing code
301
+
302
+ Rules:
303
+ - Each story: single focused unit of work for one AI agent
304
+ - Use dependsOn for dependencies; same-priority stories with no deps run IN PARALLEL
305
+ - Keep stories small (15-60 min of work)
306
+ - Include testable acceptance criteria and test commands
307
+ - No circular dependencies
308
+ - Start with foundational stories, build up
309
+ - retries: 2-3 for most stories
310
+ - IDs: S1, S2, S3...
311
+ - branchName: kebab-case
312
+ - Build on existing code, don't recreate what exists`;
313
+ function fixSchemaForOpenAI(schema) {
314
+ if (!schema || typeof schema !== "object") return schema;
315
+ if (schema.type === "object" && schema.properties) {
316
+ schema.additionalProperties = false;
317
+ }
318
+ if (schema.properties) {
319
+ for (const key of Object.keys(schema.properties)) {
320
+ fixSchemaForOpenAI(schema.properties[key]);
321
+ }
322
+ }
323
+ if (schema.items) fixSchemaForOpenAI(schema.items);
324
+ delete schema.$schema;
325
+ return schema;
326
+ }
327
+ var Planner = class {
328
+ messages;
329
+ model;
330
+ onToken;
331
+ onToolCall;
332
+ tools;
333
+ constructor(options = {}) {
334
+ this.model = options.model ?? "gpt-5.4";
335
+ this.onToken = options.onToken;
336
+ this.onToolCall = options.onToolCall;
337
+ this.tools = options.cwd ? createCodebaseTools(options.cwd) : [];
338
+ this.messages = [{ role: "system", content: SYSTEM_PROMPT }];
339
+ }
340
+ async send(userMessage) {
341
+ const raw = zodToJsonSchema(PrdSchema, "prd");
342
+ const schema = fixSchemaForOpenAI(
343
+ raw.definitions?.prd ?? raw
344
+ );
345
+ const fullText = await streamCompletion({
346
+ model: this.model,
347
+ messages: this.messages,
348
+ task: userMessage,
349
+ jsonSchema: schema,
350
+ reasoning: { effort: "high" },
351
+ tools: this.tools.length > 0 ? this.tools : void 0,
352
+ onToken: this.onToken ?? (() => {
353
+ }),
354
+ onToolCall: this.onToolCall
355
+ });
356
+ let prd;
357
+ try {
358
+ prd = JSON.parse(fullText);
359
+ } catch {
360
+ throw new Error("Failed to parse plan output as JSON");
361
+ }
362
+ this.messages.push(
363
+ { role: "user", content: userMessage },
364
+ { role: "assistant", content: fullText }
365
+ );
366
+ return {
367
+ ...prd,
368
+ userStories: prd.userStories.map((s) => ({
369
+ ...s,
370
+ passes: false,
371
+ completedAt: null,
372
+ durationSecs: null
373
+ }))
374
+ };
375
+ }
376
+ };
377
+
378
+ // src/core/openai-planner.ts
379
+ var args = process.argv.slice(2);
380
+ var goal = "";
381
+ var cwd = process.cwd();
382
+ for (let i = 0; i < args.length; i++) {
383
+ if (args[i] === "--cwd" && args[i + 1]) {
384
+ cwd = args[++i];
385
+ } else if (!goal) {
386
+ goal = args[i];
387
+ }
388
+ }
389
+ if (!goal) {
390
+ console.error("Usage: openai-planner <goal> [--cwd <path>]");
391
+ process.exit(1);
392
+ }
393
+ async function main() {
394
+ const planner = new Planner({
395
+ cwd,
396
+ onToken: () => {
397
+ },
398
+ onToolCall: (name, args2) => {
399
+ process.stderr.write(`[openai] tool: ${name}
400
+ `);
401
+ }
402
+ });
403
+ try {
404
+ const prd = await planner.send(goal);
405
+ process.stdout.write(JSON.stringify(prd, null, 2) + "\n");
406
+ } catch (err) {
407
+ console.error(`OpenAI planner error: ${err.message}`);
408
+ process.exit(1);
409
+ }
410
+ }
411
+ main();
package/package.json CHANGED
@@ -1,29 +1,25 @@
1
1
  {
2
2
  "name": "baro-ai",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Autonomous parallel coding - plan and execute with AI",
5
5
  "type": "module",
6
6
  "bin": {
7
- "baro": "./dist/cli.js"
7
+ "baro": "./bin/baro"
8
8
  },
9
9
  "files": [
10
- "dist/",
11
10
  "bin/",
11
+ "dist/",
12
12
  "scripts/postinstall.js"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsup src/cli.tsx src/core/executor.ts --format esm --external ink --external react --external ink-spinner",
15
+ "build": "tsup src/core/openai-planner.ts --format esm",
16
16
  "postinstall": "node scripts/postinstall.js"
17
17
  },
18
18
  "dependencies": {
19
- "ink": "^5.2.0",
20
- "ink-spinner": "^5.0.0",
21
- "react": "^18.3.1",
22
19
  "zod": "^3.24.0",
23
20
  "zod-to-json-schema": "^3.25.1"
24
21
  },
25
22
  "devDependencies": {
26
- "@types/react": "^18.3.0",
27
23
  "@types/node": "^22.0.0",
28
24
  "tsup": "^8.0.2",
29
25
  "typescript": "^5.3.3"
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Postinstall script - downloads the baro-tui binary for the current platform.
3
+ * Postinstall script - downloads the baro binary for the current platform.
4
4
  * Binary is fetched from GitHub Releases.
5
5
  */
6
6
 
7
- import { execSync } from "child_process"
8
7
  import * as fs from "fs"
9
8
  import * as path from "path"
10
9
  import { fileURLToPath } from "url"
@@ -13,12 +12,12 @@ import * as https from "https"
13
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
13
  const PACKAGE_ROOT = path.resolve(__dirname, "..")
15
14
  const BIN_DIR = path.join(PACKAGE_ROOT, "bin")
16
- const BINARY_NAME = "baro-tui"
15
+ const BINARY_NAME = "baro"
17
16
  const REPO = "Lotus015/baro"
18
17
 
19
18
  function getPlatformKey() {
20
- const platform = process.platform // darwin, linux, win32
21
- const arch = process.arch // arm64, x64
19
+ const platform = process.platform
20
+ const arch = process.arch
22
21
 
23
22
  const map = {
24
23
  "darwin-arm64": "darwin-arm64",
@@ -29,9 +28,9 @@ function getPlatformKey() {
29
28
 
30
29
  const key = `${platform}-${arch}`
31
30
  if (!map[key]) {
32
- console.warn(`⚠ baro-tui: no prebuilt binary for ${key}. Execution dashboard won't be available.`)
33
- console.warn(` You can build it manually: cargo build --release in the baro repo.`)
34
- process.exit(0) // Don't fail install
31
+ console.warn(`Warning: no prebuilt baro binary for ${key}.`)
32
+ console.warn(` You can build it manually: cargo build --release -p baro-tui`)
33
+ process.exit(0)
35
34
  }
36
35
  return map[key]
37
36
  }
@@ -64,7 +63,6 @@ async function download(url, dest) {
64
63
  }
65
64
 
66
65
  async function main() {
67
- // Skip if binary already exists (e.g. local dev)
68
66
  const binaryPath = path.join(BIN_DIR, BINARY_NAME)
69
67
  if (fs.existsSync(binaryPath)) {
70
68
  return
@@ -75,19 +73,17 @@ async function main() {
75
73
 
76
74
  const url = `https://github.com/${REPO}/releases/download/v${version}/${BINARY_NAME}-${platformKey}`
77
75
 
78
- console.log(`Downloading baro-tui for ${platformKey}...`)
76
+ console.log(`Downloading baro for ${platformKey}...`)
79
77
 
80
78
  fs.mkdirSync(BIN_DIR, { recursive: true })
81
79
 
82
80
  try {
83
81
  await download(url, binaryPath)
84
82
  fs.chmodSync(binaryPath, 0o755)
85
- console.log(`✓ baro-tui installed`)
83
+ console.log(`baro installed successfully`)
86
84
  } catch (err) {
87
- console.warn(`⚠ Could not download baro-tui: ${err.message}`)
88
- console.warn(` Planning will work. Execution dashboard requires the binary.`)
89
- console.warn(` Build manually: cargo build --release`)
90
- // Don't fail the install
85
+ console.warn(`Warning: Could not download baro: ${err.message}`)
86
+ console.warn(` Build manually: cargo build --release -p baro-tui`)
91
87
  }
92
88
  }
93
89
 
@@ -1,61 +0,0 @@
1
- // src/core/cli-task.ts
2
- import { spawn } from "child_process";
3
- var CliTask = class {
4
- id;
5
- opts;
6
- constructor(opts) {
7
- this.id = opts.id;
8
- this.opts = opts;
9
- }
10
- execute() {
11
- return new Promise((resolve, reject) => {
12
- const start = Date.now();
13
- let stdout = "";
14
- let stderr = "";
15
- const proc = spawn(this.opts.command, this.opts.args, {
16
- cwd: this.opts.cwd,
17
- stdio: ["ignore", "pipe", "pipe"],
18
- env: { ...process.env }
19
- });
20
- proc.stdout.on("data", (chunk) => {
21
- const text = chunk.toString();
22
- stdout += text;
23
- if (this.opts.onStdout) {
24
- for (const line of text.split("\n").filter(Boolean)) {
25
- this.opts.onStdout(line);
26
- }
27
- }
28
- });
29
- proc.stderr.on("data", (chunk) => {
30
- const text = chunk.toString();
31
- stderr += text;
32
- if (this.opts.onStderr) {
33
- for (const line of text.split("\n").filter(Boolean)) {
34
- this.opts.onStderr(line);
35
- }
36
- }
37
- });
38
- proc.on("error", (err) => {
39
- reject(new Error(`Failed to spawn ${this.opts.command}: ${err.message}`));
40
- });
41
- proc.on("close", (code) => {
42
- const result = {
43
- stdout,
44
- stderr,
45
- exitCode: code ?? 1,
46
- durationMs: Date.now() - start
47
- };
48
- if (code === 0) resolve(result);
49
- else {
50
- const err = new Error(`${this.opts.command} exited with code ${code}`);
51
- err.result = result;
52
- reject(err);
53
- }
54
- });
55
- });
56
- }
57
- };
58
-
59
- export {
60
- CliTask
61
- };