huddle-cli 0.0.0-stage → 0.1.1

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/QUICKSTART.md +102 -0
  4. package/README.md +376 -2
  5. package/dist/adapters/amp.js +40 -0
  6. package/dist/adapters/antigravity.js +54 -0
  7. package/dist/adapters/claude.js +86 -0
  8. package/dist/adapters/codex.js +64 -0
  9. package/dist/adapters/copilot.js +69 -0
  10. package/dist/adapters/cursor.js +92 -0
  11. package/dist/adapters/custom.js +342 -0
  12. package/dist/adapters/droid.js +58 -0
  13. package/dist/adapters/gemini.js +61 -0
  14. package/dist/adapters/goose.js +57 -0
  15. package/dist/adapters/index.js +161 -0
  16. package/dist/adapters/kiro.js +41 -0
  17. package/dist/adapters/opencode.js +63 -0
  18. package/dist/adapters/qwen.js +67 -0
  19. package/dist/adapters/types.js +6 -0
  20. package/dist/adapters/usage.js +141 -0
  21. package/dist/attach/connect.js +412 -0
  22. package/dist/attach/guard.js +83 -0
  23. package/dist/attach/live.js +226 -0
  24. package/dist/attach/plug.js +185 -0
  25. package/dist/attach/screen.js +150 -0
  26. package/dist/attach/session.js +177 -0
  27. package/dist/cli.js +244 -0
  28. package/dist/mcp/context-tools.js +79 -0
  29. package/dist/mcp/server.js +267 -0
  30. package/dist/orchestrator/ask.js +54 -0
  31. package/dist/orchestrator/brain.js +174 -0
  32. package/dist/orchestrator/council.js +615 -0
  33. package/dist/orchestrator/goal.js +196 -0
  34. package/dist/orchestrator/handoff.js +117 -0
  35. package/dist/orchestrator/launcher.js +171 -0
  36. package/dist/orchestrator/learn.js +105 -0
  37. package/dist/orchestrator/planner.js +212 -0
  38. package/dist/orchestrator/qa.js +398 -0
  39. package/dist/orchestrator/radar.js +719 -0
  40. package/dist/orchestrator/scheduler.js +1207 -0
  41. package/dist/orchestrator/takeover.js +240 -0
  42. package/dist/platform.js +273 -0
  43. package/dist/preflight.js +31 -0
  44. package/dist/relay/server.js +113 -0
  45. package/dist/room/context.js +1285 -0
  46. package/dist/room/contract.js +132 -0
  47. package/dist/room/db.js +680 -0
  48. package/dist/room/followup.js +233 -0
  49. package/dist/room/identity.js +122 -0
  50. package/dist/room/lessons.js +275 -0
  51. package/dist/room/live.js +348 -0
  52. package/dist/room/mail.js +212 -0
  53. package/dist/room/progress.js +149 -0
  54. package/dist/room/relaylink.js +85 -0
  55. package/dist/room/server.js +882 -0
  56. package/dist/room/sqlite.js +156 -0
  57. package/dist/room/transport.js +107 -0
  58. package/dist/room/tunnel.js +86 -0
  59. package/dist/sandbox/sync.js +104 -0
  60. package/dist/sandbox/worktree.js +392 -0
  61. package/dist/types.js +4 -0
  62. package/dist/ui/app.js +1955 -0
  63. package/dist/ui/catalog.js +113 -0
  64. package/dist/ui/dashboard.js +335 -0
  65. package/dist/ui/doctor.js +58 -0
  66. package/dist/ui/lessons.js +81 -0
  67. package/dist/ui/logo.js +40 -0
  68. package/dist/ui/paste.js +515 -0
  69. package/dist/ui/remote.js +338 -0
  70. package/dist/ui/setup.js +152 -0
  71. package/dist/ui/story.js +1529 -0
  72. package/dist/ui/term.js +249 -0
  73. package/dist/ui/watch.js +271 -0
  74. package/install.sh +44 -0
  75. package/package.json +63 -4
@@ -0,0 +1,338 @@
1
+ // Joining a teammate's room: `huddle join <address> <CODE>`.
2
+ //
3
+ // You prove who you are (your GitHub SSH key, if the invite names you), then
4
+ // your Huddle becomes a member node:
5
+ // · you see their board live and can message the room
6
+ // · if you're in a clone of the same repo, your crew joins theirs — the room
7
+ // can offer your agents tasks; YOU accept or decline each one
8
+ // · accepted work runs here, on your machine, with your accounts, your tokens
9
+ // and your approval level, in a worktree off the room's pushed main
10
+ // · the result goes back as a branch on the shared remote; the room tests,
11
+ // reviews and merges it
12
+ // Nobody can run anything on your machine without your yes. `leave` ends it.
13
+ import fs from "node:fs";
14
+ import http from "node:http";
15
+ import crypto from "node:crypto";
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ import readline from "node:readline";
19
+ import { spawnSync } from "node:child_process";
20
+ import { C, dot, pad } from "./term.js";
21
+ import { transportFor } from "../room/transport.js";
22
+ import { signAs, githubUser } from "../room/identity.js";
23
+ import { canonicalRemote, pushBranch, remoteInfo } from "../sandbox/sync.js";
24
+ import { connectAgents, readPrefs, workersFrom, ensureInit } from "./app.js";
25
+ import { launchAgent, stopChild } from "../orchestrator/launcher.js";
26
+ import { looksLikePaths, remoteScreen } from "./paste.js";
27
+ import { removeWorktree } from "../sandbox/worktree.js";
28
+ import { fmtDuration, fmtTokens } from "../adapters/usage.js";
29
+ export const INVITE_CODE = /^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/i;
30
+ function git(cwd, args) {
31
+ const r = spawnSync("git", args, { cwd, encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
32
+ return { ok: r.status === 0, out: (r.stdout ?? "").trim(), err: (r.stderr ?? "").trim() };
33
+ }
34
+ function repoRootOf(dir) {
35
+ const r = git(dir, ["rev-parse", "--show-toplevel"]);
36
+ return r.ok ? r.out : null;
37
+ }
38
+ function printBoard(b) {
39
+ console.log(`\n${C.violet("BOARD")} ${C.faint("· " + (b.session?.goal ?? "(no goal yet)"))}`);
40
+ for (const t of b.tasks ?? []) {
41
+ const w = 16;
42
+ const filled = Math.round(((t.progress_pct ?? 0) / 100) * w);
43
+ const bar = C.violet("█".repeat(filled)) + C.faint("░".repeat(w - filled));
44
+ const who = t.claimed_by ? `${dot(t.claimed_by)} ${t.claimed_by}` : C.faint("unclaimed");
45
+ console.log(` ${pad(C.faint(t.status), 12)} ${bar} ${String(t.progress_pct ?? 0).padStart(3)}% ${t.title} ${who}`);
46
+ }
47
+ const people = (b.members ?? []).map((m) => C.bold(m.github ? "@" + m.github : m.name)).join(", ");
48
+ console.log(C.faint(`\n in the room: host${people ? ", " + people : ""}${b.crews?.length ? " · teammates' agents: " + b.crews.join(", ") : ""}`));
49
+ }
50
+ /**
51
+ * The member's local stand-in for the Room: their agents' Plug talks to this
52
+ * (with its own secret); every verb is forwarded to the host's Room, tagged
53
+ * with the task that agent was assigned. Nothing else is reachable.
54
+ */
55
+ async function startProxy(t, token, taskOf) {
56
+ const secret = crypto.randomBytes(24).toString("hex");
57
+ const server = http.createServer(async (req, res) => {
58
+ const url = new URL(req.url ?? "/", "http://proxy");
59
+ const reply = (status, body) => {
60
+ res.writeHead(status, { "content-type": "application/json" });
61
+ res.end(JSON.stringify(body));
62
+ };
63
+ if (req.headers["x-huddle-secret"] !== secret)
64
+ return reply(401, { error: "not this node's agent" });
65
+ let body = Object.fromEntries(url.searchParams);
66
+ if (req.method === "POST") {
67
+ const raw = await new Promise((r) => {
68
+ let d = "";
69
+ req.on("data", (c) => (d += c));
70
+ req.on("end", () => r(d));
71
+ });
72
+ try {
73
+ body = raw ? JSON.parse(raw) : {};
74
+ }
75
+ catch {
76
+ return reply(400, { error: "bad json" });
77
+ }
78
+ }
79
+ const agent = String(body.agent_id ?? req.headers["x-huddle-agent"] ?? "");
80
+ const task = taskOf(agent);
81
+ if (!task)
82
+ return reply(403, { error: "no task assigned to this agent" });
83
+ const verb = url.pathname.slice(1);
84
+ const r = await t.request("POST", `/agent/${verb}`, { ...body, task_id: body.task_id ?? task, for_task: task }, token);
85
+ reply(r.status, r.body);
86
+ });
87
+ const port = await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve(server.address().port)));
88
+ return { url: `http://127.0.0.1:${port}`, secret, close: () => server.close() };
89
+ }
90
+ export async function joinRoom(address, code) {
91
+ const t = transportFor(address);
92
+ code = code.toUpperCase();
93
+ // 1. Who are you? (GitHub-bound invites need a signature from your key.)
94
+ let ch;
95
+ try {
96
+ ch = await t.request("POST", "/challenge", { code });
97
+ }
98
+ catch (e) {
99
+ console.log(C.red(`✗ couldn't reach ${t.describe}`) + C.faint(` — ${e?.cause?.code ?? e?.message ?? e}. Is their Huddle open? Same network / Tailscale / relay?`));
100
+ process.exit(1);
101
+ }
102
+ if (ch.status !== 200) {
103
+ console.log(C.red(`✗ ${ch.body.error ?? "couldn't join"}`));
104
+ process.exit(1);
105
+ }
106
+ const join = { code, name: githubUser() ?? os.userInfo().username };
107
+ if (ch.body.github) {
108
+ console.log(C.faint(` this invite is for @${ch.body.github} — proving it with your GitHub SSH key…`));
109
+ const s = await signAs(ch.body.github, ch.body.nonce);
110
+ if (!s.ok) {
111
+ console.log(C.red(`✗ ${s.reason}`));
112
+ process.exit(1);
113
+ }
114
+ Object.assign(join, { key: s.key, sig: s.sig });
115
+ }
116
+ const j = await t.request("POST", "/join", join);
117
+ if (j.status !== 200) {
118
+ console.log(C.red(`✗ ${j.body.error ?? "couldn't join"}`));
119
+ process.exit(1);
120
+ }
121
+ const token = j.body.token;
122
+ const room = j.body.room ?? {};
123
+ const me = j.body.member;
124
+ console.log(`${C.green("✓")} joined ${C.bold((room.host ?? "their") + "'s room")} as ${C.bold(me.github ? "@" + me.github : me.name)} ${C.faint("· " + me.role)}`);
125
+ printBoard(j.body.board);
126
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: C.violet("room ›") + " " });
127
+ let asking = false;
128
+ const held = [];
129
+ const say = (line) => {
130
+ if (asking)
131
+ return void held.push(line);
132
+ process.stdout.write("\r\x1b[2K");
133
+ console.log(line);
134
+ rl.prompt(true);
135
+ };
136
+ const ask = (q) => new Promise((resolve) => {
137
+ asking = true;
138
+ process.stdout.write("\r\x1b[2K");
139
+ rl.question(q, (a) => {
140
+ asking = false;
141
+ for (const l of held.splice(0))
142
+ console.log(l);
143
+ resolve(a.trim().toLowerCase());
144
+ });
145
+ });
146
+ // 2. Code layer: are we in a clone of the room's repo? If not, offer to clone.
147
+ let root = repoRootOf(process.cwd());
148
+ const sameRepo = (r) => {
149
+ const ri = r ? remoteInfo(r) : null;
150
+ return !!(ri && room.remote && canonicalRemote(ri.url) === room.remote);
151
+ };
152
+ if (me.role !== "guest" && room.remote && !sameRepo(root)) {
153
+ const name = room.remote.split("/").pop();
154
+ const here = path.join(process.cwd(), name);
155
+ if (fs.existsSync(here) && sameRepo(repoRootOf(here))) {
156
+ process.chdir(here);
157
+ root = repoRootOf(here);
158
+ }
159
+ else {
160
+ console.log(C.faint(`\n Your agents work in your own copy of ${room.remote}.`));
161
+ const a = await ask(` Clone it into ./${name}? ${C.faint("[Y/n]")} `);
162
+ if (a === "" || a.startsWith("y")) {
163
+ const cl = spawnSync("git", ["clone", "--quiet", `https://${room.remote}.git`, here], { stdio: "inherit" });
164
+ if (cl.status === 0) {
165
+ process.chdir(here);
166
+ root = repoRootOf(here);
167
+ console.log(` ${C.green("✓")} cloned into ${here}`);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ const crewMode = me.role !== "guest" && sameRepo(root);
173
+ // 3. Your crew joins theirs.
174
+ let workers = [];
175
+ let proxy = null;
176
+ const assigned = new Map(); // local agent → task
177
+ let always = false;
178
+ if (crewMode && root) {
179
+ process.chdir(root);
180
+ ensureInit();
181
+ const results = await connectAgents();
182
+ workers = workersFrom(results);
183
+ proxy = await startProxy(t, token, (agent) => assigned.get(agent)?.taskId);
184
+ const r = await t.request("POST", "/crew", { agents: workers.map((w) => ({ id: w.id, vendor: w.adapter.vendor, prefers: w.adapter.prefers })) }, token);
185
+ if (r.status === 200)
186
+ console.log(`\n ${C.green("⚡")} your crew is in the room: ${workers.map((w) => dot(w.id) + " " + w.id).join(" ")} ${C.faint("· you approve every task")}`);
187
+ }
188
+ else if (me.role !== "guest") {
189
+ console.log(C.faint(`\n watching only — run \`huddle join\` from your clone of ${room.remote ?? "their repo"} to bring your crew`));
190
+ }
191
+ console.log(C.faint("\n type to message the room · board · leave (or ctrl-d)\n"));
192
+ // 4. Doing a task the room offered — on this machine, your rules.
193
+ const runTask = async (p) => {
194
+ const w = workers.find((x) => x.id === p.agent);
195
+ const branch = p.branch;
196
+ const t0 = Date.now();
197
+ const deliver = (body) => t.request("POST", "/delivered", { task_id: p.task.id, ms: Date.now() - t0, ...body }, token);
198
+ if (!w || !root || !/^huddle\/[\w./#-]+$/.test(branch))
199
+ return deliver({ ok: false, reason: "no such agent here" });
200
+ // Worktree off the room's pushed main (or the branch itself, on a fix round).
201
+ const f = git(root, ["fetch", "--quiet", "origin"]);
202
+ if (!f.ok)
203
+ return deliver({ ok: false, reason: `couldn't fetch: ${f.err.split(/\r?\n/)[0]}` });
204
+ const wtPath = path.join(root, ".huddle", "worktrees", branch.replace(/[^\w.-]/g, "_"));
205
+ if (!fs.existsSync(wtPath)) {
206
+ const hasRemoteBranch = git(root, ["rev-parse", "--verify", "--quiet", `origin/${branch}`]).ok;
207
+ const add = hasRemoteBranch
208
+ ? git(root, ["worktree", "add", "-B", branch, wtPath, `origin/${branch}`])
209
+ : git(root, ["worktree", "add", "-B", branch, wtPath, p.base]);
210
+ if (!add.ok)
211
+ return deliver({ ok: false, reason: `couldn't prepare a worktree: ${add.err.split(/\r?\n/)[0]}` });
212
+ }
213
+ say(` ${dot(w.id)} ${pad(w.id, 10)} ${C.violet("▸")} ${p.task.title} ${C.faint("· on your machine")}`);
214
+ const prefs = readPrefs();
215
+ const h = launchAgent({
216
+ repoRoot: root,
217
+ adapter: w.adapter,
218
+ agentId: w.id,
219
+ role: "worker",
220
+ taskId: p.task.id,
221
+ taskSpec: `${p.task.title}\n\n${p.task.spec}`,
222
+ roomUrl: proxy.url,
223
+ roomSecret: proxy.secret,
224
+ approvals: prefs.approvals ?? "auto",
225
+ worktree: { branch, path: wtPath },
226
+ });
227
+ assigned.set(w.id, { taskId: p.task.id, handle: h });
228
+ const code = await new Promise((resolve) => {
229
+ h.child.on("exit", resolve);
230
+ h.child.on("error", () => resolve(-1));
231
+ });
232
+ assigned.delete(w.id);
233
+ if (code !== 0) {
234
+ say(` ${dot(w.id)} ${pad(w.id, 10)} ${C.red("✗")} exited with ${code} ${C.faint("· log: " + h.logPath)}`);
235
+ return deliver({ ok: false, reason: `agent exited with code ${code}`, tokens: h.meter.tokens, cached: h.meter.cached });
236
+ }
237
+ // Commit here, push the branch; the room tests, reviews and merges it.
238
+ git(wtPath, ["add", "-A", "--", ".", ...h.excludes.map((e) => `:!${e}`)]);
239
+ git(wtPath, ["commit", "--quiet", "--allow-empty", "-m", `huddle(${p.worker}): ${p.task.title}`]);
240
+ const push = pushBranch(wtPath, branch);
241
+ if (!push.ok)
242
+ return deliver({ ok: false, reason: `couldn't push ${branch}: ${push.note}`, tokens: h.meter.tokens });
243
+ removeWorktree(root, wtPath);
244
+ say(` ${dot(w.id)} ${pad(w.id, 10)} ${C.green("↑ delivered")} ${p.task.title} ${C.faint(`· ${fmtDuration(Date.now() - t0)} · ${fmtTokens(h.meter.tokens)} tok · the room will test & merge`)}`);
245
+ await deliver({ ok: true, branch, tokens: h.meter.tokens, cached: h.meter.cached });
246
+ };
247
+ // 5. Live room.
248
+ let leaving = false;
249
+ const leave = async () => {
250
+ if (leaving)
251
+ return;
252
+ leaving = true;
253
+ for (const a of assigned.values())
254
+ stopChild(a.handle?.child);
255
+ await t.request("POST", "/leave", {}, token).catch(() => { });
256
+ proxy?.close();
257
+ t.close();
258
+ process.stdout.write("\r\x1b[2K");
259
+ console.log(C.faint("left the room · your access is revoked"));
260
+ process.exit(0);
261
+ };
262
+ t.events(token, async (e) => {
263
+ const p = typeof e.payload === "string" ? JSON.parse(e.payload || "{}") : e.payload ?? {};
264
+ if (e.type === "message")
265
+ say(` ${C.violet("💬")} ${C.bold(p.from)} ${C.faint("›")} ${p.body}`);
266
+ else if (e.type === "member.joined")
267
+ say(` ${C.green("●")} ${C.bold(p.github ? "@" + p.github : p.name)} joined`);
268
+ else if (e.type === "member.left")
269
+ say(` ${C.faint("○")} ${C.bold(p.name)} ${p.reason === "removed" ? "was removed" : "left"}`);
270
+ else if (e.type === "session.created")
271
+ say(`\n ${C.violet("▸ new goal:")} ${p.goal}`);
272
+ else if (e.type === "task.claimed")
273
+ say(` ${dot(e.agent_id ?? "")} ${pad(e.agent_id ?? "", 14)} ${C.violet("▸")} picked up a task`);
274
+ else if (e.type === "task.status" && p.status === "merged")
275
+ say(` ${C.green("✓ merged")} ${C.faint(e.task_id)}`);
276
+ else if (e.type === "task.progress" && p.note)
277
+ say(` ${C.faint(String(p.pct).padStart(3) + "%")} ${p.note}`);
278
+ else if (e.type === "ask")
279
+ say(` ${dot(p.from)} ${C.bold(p.from)} ${C.faint("→")} ${p.to ? C.bold(p.to) : C.faint("room")} ${p.question}`);
280
+ else if (e.type === "answer" && p.source !== "teammate")
281
+ say(` ${dot(p.from)} ${C.bold(p.from)} ${C.faint("→")} ${C.bold(p.to)} ${p.answer} ${C.faint("— from shared memory")}`);
282
+ else if (e.type === "agent.message" && p.kind !== "question")
283
+ say(` ${dot(p.from)} ${C.bold(p.from)} ${C.faint("→")} ${C.bold(p.to)}${C.faint(":")} ${String(p.body).slice(0, 200)}`);
284
+ else if (e.type === "assign") {
285
+ // Rule 1: an offer is data. Only you (or your "always") turn it into work.
286
+ const title = p.task?.title ?? "a task";
287
+ let ok = always;
288
+ if (!ok) {
289
+ const a = await ask(`\n ${C.bold((room.host ?? "the room") + "'s room")} offers ${C.bold("“" + title + "”")} to your ${dot(p.agent)} ${p.agent}.\n Accept? ${C.faint("[Y/n/a = always this session]")} `);
290
+ if (a === "a" || a === "always")
291
+ always = ok = true;
292
+ else
293
+ ok = a === "" || a.startsWith("y");
294
+ }
295
+ await t.request("POST", "/assignment", { task_id: p.task.id, accept: ok }, token);
296
+ if (ok)
297
+ void runTask(p);
298
+ else
299
+ say(C.faint(` declined · it goes back on their board`));
300
+ rl.prompt(true);
301
+ }
302
+ }, (code) => {
303
+ if (leaving)
304
+ return;
305
+ process.stdout.write("\r\x1b[2K");
306
+ console.log(code === 4403 ? C.yellow("you were removed from the room") : C.faint("the room closed"));
307
+ for (const a of assigned.values())
308
+ stopChild(a.handle?.child);
309
+ proxy?.close();
310
+ process.exit(0);
311
+ });
312
+ rl.on("line", async (raw) => {
313
+ if (asking)
314
+ return;
315
+ const line = raw.trim();
316
+ if (!line)
317
+ return rl.prompt();
318
+ if (line === "leave" || line === "exit" || line === "quit")
319
+ return leave();
320
+ if (line === "board") {
321
+ const b = await t.request("GET", "/board", undefined, token);
322
+ printBoard(b.body);
323
+ return rl.prompt();
324
+ }
325
+ // The host's shared screen: `ctx` lists it, `add <file>` puts a text file up.
326
+ if (/^(ctx|context)$/.test(line) || /^add\s+\S/.test(line) && looksLikePaths(line.slice(3))) {
327
+ await remoteScreen((m, p, b) => t.request(m, p, b, token), line, say);
328
+ return rl.prompt();
329
+ }
330
+ const r = await t.request("POST", "/message", { body: line }, token);
331
+ if (r.status !== 200)
332
+ say(C.red(` ✗ ${r.body.error ?? "not sent"}`));
333
+ rl.prompt();
334
+ });
335
+ rl.on("close", () => void leave());
336
+ rl.prompt();
337
+ }
338
+ //# sourceMappingURL=remote.js.map
@@ -0,0 +1,152 @@
1
+ // `huddle setup`: everything a person would otherwise do by hand before the
2
+ // first goal, in one command — check Node and git, pick which agents you use,
3
+ // install the missing ones with this platform's command, then hand the
4
+ // terminal to each one's own sign-in, and check again.
5
+ //
6
+ // huddle setup asks: use each agent? install the missing ones?
7
+ // huddle setup --yes no questions for choices and installs
8
+ // (sign-ins still need you: they open a browser)
9
+ // huddle setup --agents=claude,codex only these (and they're marked "used")
10
+ //
11
+ // Huddle never sees a credential: sign-in is always the agent's own command.
12
+ import readline from "node:readline/promises";
13
+ import { spawnSync } from "node:child_process";
14
+ import { ADAPTERS, LEGACY, installCommand, loginLine } from "../adapters/index.js";
15
+ import { MIN_NODE } from "../preflight.js";
16
+ import { gitFix, nodeFix } from "../platform.js";
17
+ import { connectAgents, installAgent, installLine, runInteractive, setAgentChoice, signIn, skippedAgents } from "./app.js";
18
+ import { C, dot } from "./term.js";
19
+ function realDeps(rl) {
20
+ return {
21
+ ask: async (q, def = true) => {
22
+ if (!rl)
23
+ return def;
24
+ const a = (await rl.question(`${q} ${C.faint(def ? "[Y/n]" : "[y/N]")} `)).trim().toLowerCase();
25
+ return a === "" ? def : a.startsWith("y");
26
+ },
27
+ run: (cmd) => runInteractive(rl, cmd),
28
+ print: (l = "") => console.log(l),
29
+ probe: () => connectAgents(),
30
+ git: () => {
31
+ const r = spawnSync("git", ["--version"], { encoding: "utf8" });
32
+ return r.status === 0 ? (r.stdout.match(/\d+\.\d+(\.\d+)?/)?.[0] ?? "?") : null;
33
+ },
34
+ node: process.versions.node,
35
+ interactive: !!process.stdin.isTTY,
36
+ };
37
+ }
38
+ export async function runSetup(args, deps) {
39
+ const rl = deps ? null : process.stdin.isTTY ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
40
+ try {
41
+ return await setup(args, deps ?? realDeps(rl));
42
+ }
43
+ finally {
44
+ rl?.close();
45
+ }
46
+ }
47
+ /** The flow, with every side effect in `d` (tests pass fakes). Exit code: 0 = at least one agent ready. */
48
+ export async function setup(args, d) {
49
+ const yesAll = args.includes("--yes") || args.includes("-y");
50
+ const pick = args.find((a) => a.startsWith("--agents="))?.split("=")[1];
51
+ const only = pick?.split(",").map((s) => s.trim()).filter(Boolean);
52
+ const ask = (q, def = true) => (yesAll || !d.interactive ? Promise.resolve(def) : d.ask(q, def));
53
+ // 1. Huddle's own needs. Node is checked by preflight before we get here, but say it.
54
+ d.print(`\n${C.bold("Huddle setup")}\n`);
55
+ d.print(` ${C.green("✓")} node ${d.node}${Number(d.node.split(".")[0]) < MIN_NODE ? C.red(` — needs ${MIN_NODE}+: ${nodeFix()}`) : ""}`);
56
+ const git = d.git();
57
+ if (git)
58
+ d.print(` ${C.green("✓")} git ${git}`);
59
+ else
60
+ d.print(` ${C.red("✗")} git is missing — Huddle gives every agent its own git worktree. Install it:\n ${gitFix()}`);
61
+ if (only) {
62
+ const known = new Set(ADAPTERS.map((a) => a.id));
63
+ const unknown = only.filter((id) => !known.has(id));
64
+ if (unknown.length) {
65
+ d.print(C.red(`✗ --agents: no agent called ${unknown.join(", ")}.`) + ` Known: ${[...known].join(", ")}`);
66
+ return 1;
67
+ }
68
+ setAgentChoice(only, true);
69
+ }
70
+ // 2. Which agents you use. Asked once per agent; --yes keeps your earlier choices.
71
+ let results = await d.probe();
72
+ const inScope = (r) => (only ? only.includes(r.adapter.id) : true);
73
+ if (!only && d.interactive && !yesAll) {
74
+ d.print(C.faint("\n Pick the agents you want Huddle to use. Skipped ones are never installed, asked about, or put to work."));
75
+ const skipped = skippedAgents();
76
+ const use = [];
77
+ const skip = [];
78
+ for (const r of results) {
79
+ const id = r.adapter.id;
80
+ const was = r.state === "skipped" ? r.was : r.state;
81
+ const note = was === "ready" ? "ready" : was === "not_authed" ? "installed, not signed in" : "not installed";
82
+ (await d.ask(` Use ${dot(id)} ${C.bold(id)} ${C.faint(`(${note})`)}?`, !skipped.has(id)) ? use : skip).push(id);
83
+ }
84
+ setAgentChoice(use, true);
85
+ setAgentChoice(skip, false);
86
+ if (skip.some((id) => !skipped.has(id)) || use.some((id) => skipped.has(id)))
87
+ results = await d.probe();
88
+ }
89
+ // 3. Install what's missing, with this platform's command.
90
+ const missing = results.filter((r) => r.state === "not_installed" && inScope(r) && (only || !LEGACY.has(r.adapter.id)));
91
+ if (missing.length) {
92
+ d.print(`\n ${C.bold("Not installed:")}`);
93
+ for (const r of missing)
94
+ d.print(` ${dot(r.adapter.id)} ${C.bold(r.adapter.id.padEnd(12))} ${C.faint(installCommand(r.adapter) ? "$ " : "")}${installLine(r.adapter)}`);
95
+ const installable = missing.filter((r) => installCommand(r.adapter));
96
+ let chosen = [];
97
+ if (installable.length) {
98
+ if (!d.interactive && !yesAll) {
99
+ d.print(C.faint("\n Run those yourself, or `huddle setup --yes` to have Huddle run them."));
100
+ }
101
+ else if (await ask(`\n Install all missing (${installable.map((r) => r.adapter.id).join(", ")})?`)) {
102
+ chosen = installable.map((r) => r.adapter);
103
+ }
104
+ else {
105
+ for (const r of installable)
106
+ if (await d.ask(` Install ${r.adapter.id}?`, false))
107
+ chosen.push(r.adapter);
108
+ }
109
+ }
110
+ let installed = false;
111
+ for (const a of chosen)
112
+ installed = installAgent(d, a) || installed;
113
+ if (installed)
114
+ results = await d.probe();
115
+ }
116
+ // 4. Sign in, one agent at a time: its own login, with the terminal handed over.
117
+ const signedOut = results.filter((r) => r.state === "not_authed" && inScope(r));
118
+ let signedIn = false;
119
+ for (const r of signedOut) {
120
+ const a = r.adapter;
121
+ const line = loginLine(a);
122
+ if (!line) {
123
+ d.print(`\n ${dot(a.id)} ${C.bold(a.id)} isn't signed in — ${r.detail}`);
124
+ continue;
125
+ }
126
+ if (!d.interactive) {
127
+ d.print(`\n ${dot(a.id)} ${C.bold(a.id)} isn't signed in — run: ${C.bold(line)}`);
128
+ continue;
129
+ }
130
+ // Even with --yes: a sign-in opens a browser, so the human is needed anyway — ask.
131
+ if (await d.ask(`\n ${dot(a.id)} ${C.bold(a.id)} isn't signed in. Sign in now? ${C.faint(`($ ${line})`)}`)) {
132
+ signIn(d, a);
133
+ signedIn = true;
134
+ }
135
+ }
136
+ if (signedIn)
137
+ results = await d.probe();
138
+ // 5. Where that leaves you.
139
+ const ready = results.filter((r) => r.state === "ready");
140
+ const rest = results.filter((r) => inScope(r) && (r.state === "not_authed" || (r.state === "not_installed" && !LEGACY.has(r.adapter.id))));
141
+ d.print("");
142
+ if (ready.length)
143
+ d.print(`${C.green("✓")} ${ready.length} agent${ready.length === 1 ? "" : "s"} ready (${ready.map((r) => r.adapter.id).join(", ")}) — cd into a repo and run ${C.bold("huddle")}`);
144
+ else
145
+ d.print(`${C.yellow("No agents ready yet.")} Install or sign in to one, then run ${C.bold("huddle setup")} again.`);
146
+ if (rest.length)
147
+ d.print(C.faint(` not ready: ${rest.map((r) => r.adapter.id).join(", ")} — run huddle setup again any time`));
148
+ if (!git)
149
+ d.print(C.red(` git is still needed: ${gitFix()}`));
150
+ return ready.length && git ? 0 : 1;
151
+ }
152
+ //# sourceMappingURL=setup.js.map