huddle-cli 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +357 -2
- package/dist/adapters/amp.js +40 -0
- package/dist/adapters/antigravity.js +52 -0
- package/dist/adapters/claude.js +70 -0
- package/dist/adapters/codex.js +59 -0
- package/dist/adapters/copilot.js +69 -0
- package/dist/adapters/cursor.js +85 -0
- package/dist/adapters/custom.js +340 -0
- package/dist/adapters/droid.js +57 -0
- package/dist/adapters/gemini.js +61 -0
- package/dist/adapters/goose.js +55 -0
- package/dist/adapters/index.js +135 -0
- package/dist/adapters/kiro.js +41 -0
- package/dist/adapters/opencode.js +63 -0
- package/dist/adapters/qwen.js +67 -0
- package/dist/adapters/types.js +6 -0
- package/dist/adapters/usage.js +141 -0
- package/dist/attach/connect.js +409 -0
- package/dist/attach/guard.js +64 -0
- package/dist/attach/live.js +222 -0
- package/dist/attach/plug.js +174 -0
- package/dist/attach/session.js +172 -0
- package/dist/cli.js +176 -0
- package/dist/mcp/server.js +240 -0
- package/dist/orchestrator/ask.js +51 -0
- package/dist/orchestrator/brain.js +164 -0
- package/dist/orchestrator/council.js +274 -0
- package/dist/orchestrator/handoff.js +117 -0
- package/dist/orchestrator/launcher.js +155 -0
- package/dist/orchestrator/planner.js +194 -0
- package/dist/orchestrator/qa.js +350 -0
- package/dist/orchestrator/scheduler.js +748 -0
- package/dist/orchestrator/takeover.js +85 -0
- package/dist/preflight.js +31 -0
- package/dist/relay/server.js +113 -0
- package/dist/room/db.js +617 -0
- package/dist/room/identity.js +122 -0
- package/dist/room/live.js +293 -0
- package/dist/room/progress.js +110 -0
- package/dist/room/relaylink.js +85 -0
- package/dist/room/server.js +752 -0
- package/dist/room/sqlite.js +156 -0
- package/dist/room/transport.js +107 -0
- package/dist/room/tunnel.js +85 -0
- package/dist/sandbox/sync.js +104 -0
- package/dist/sandbox/worktree.js +261 -0
- package/dist/types.js +4 -0
- package/dist/ui/app.js +1534 -0
- package/dist/ui/catalog.js +75 -0
- package/dist/ui/dashboard.js +192 -0
- package/dist/ui/doctor.js +54 -0
- package/dist/ui/logo.js +40 -0
- package/dist/ui/remote.js +332 -0
- package/dist/ui/story.js +932 -0
- package/dist/ui/term.js +114 -0
- package/dist/ui/watch.js +174 -0
- package/install.sh +44 -0
- package/package.json +60 -4
|
@@ -0,0 +1,332 @@
|
|
|
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 } from "../orchestrator/launcher.js";
|
|
26
|
+
import { removeWorktree } from "../sandbox/worktree.js";
|
|
27
|
+
import { fmtDuration, fmtTokens } from "../adapters/usage.js";
|
|
28
|
+
export const INVITE_CODE = /^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/i;
|
|
29
|
+
function git(cwd, args) {
|
|
30
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
|
|
31
|
+
return { ok: r.status === 0, out: (r.stdout ?? "").trim(), err: (r.stderr ?? "").trim() };
|
|
32
|
+
}
|
|
33
|
+
function repoRootOf(dir) {
|
|
34
|
+
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
35
|
+
return r.ok ? r.out : null;
|
|
36
|
+
}
|
|
37
|
+
function printBoard(b) {
|
|
38
|
+
console.log(`\n${C.violet("BOARD")} ${C.faint("· " + (b.session?.goal ?? "(no goal yet)"))}`);
|
|
39
|
+
for (const t of b.tasks ?? []) {
|
|
40
|
+
const w = 16;
|
|
41
|
+
const filled = Math.round(((t.progress_pct ?? 0) / 100) * w);
|
|
42
|
+
const bar = C.violet("█".repeat(filled)) + C.faint("░".repeat(w - filled));
|
|
43
|
+
const who = t.claimed_by ? `${dot(t.claimed_by)} ${t.claimed_by}` : C.faint("unclaimed");
|
|
44
|
+
console.log(` ${pad(C.faint(t.status), 12)} ${bar} ${String(t.progress_pct ?? 0).padStart(3)}% ${t.title} ${who}`);
|
|
45
|
+
}
|
|
46
|
+
const people = (b.members ?? []).map((m) => C.bold(m.github ? "@" + m.github : m.name)).join(", ");
|
|
47
|
+
console.log(C.faint(`\n in the room: host${people ? ", " + people : ""}${b.crews?.length ? " · teammates' agents: " + b.crews.join(", ") : ""}`));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The member's local stand-in for the Room: their agents' Plug talks to this
|
|
51
|
+
* (with its own secret); every verb is forwarded to the host's Room, tagged
|
|
52
|
+
* with the task that agent was assigned. Nothing else is reachable.
|
|
53
|
+
*/
|
|
54
|
+
async function startProxy(t, token, taskOf) {
|
|
55
|
+
const secret = crypto.randomBytes(24).toString("hex");
|
|
56
|
+
const server = http.createServer(async (req, res) => {
|
|
57
|
+
const url = new URL(req.url ?? "/", "http://proxy");
|
|
58
|
+
const reply = (status, body) => {
|
|
59
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
60
|
+
res.end(JSON.stringify(body));
|
|
61
|
+
};
|
|
62
|
+
if (req.headers["x-huddle-secret"] !== secret)
|
|
63
|
+
return reply(401, { error: "not this node's agent" });
|
|
64
|
+
let body = Object.fromEntries(url.searchParams);
|
|
65
|
+
if (req.method === "POST") {
|
|
66
|
+
const raw = await new Promise((r) => {
|
|
67
|
+
let d = "";
|
|
68
|
+
req.on("data", (c) => (d += c));
|
|
69
|
+
req.on("end", () => r(d));
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
body = raw ? JSON.parse(raw) : {};
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return reply(400, { error: "bad json" });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const agent = String(body.agent_id ?? req.headers["x-huddle-agent"] ?? "");
|
|
79
|
+
const task = taskOf(agent);
|
|
80
|
+
if (!task)
|
|
81
|
+
return reply(403, { error: "no task assigned to this agent" });
|
|
82
|
+
const verb = url.pathname.slice(1);
|
|
83
|
+
const r = await t.request("POST", `/agent/${verb}`, { ...body, task_id: body.task_id ?? task, for_task: task }, token);
|
|
84
|
+
reply(r.status, r.body);
|
|
85
|
+
});
|
|
86
|
+
const port = await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve(server.address().port)));
|
|
87
|
+
return { url: `http://127.0.0.1:${port}`, secret, close: () => server.close() };
|
|
88
|
+
}
|
|
89
|
+
export async function joinRoom(address, code) {
|
|
90
|
+
const t = transportFor(address);
|
|
91
|
+
code = code.toUpperCase();
|
|
92
|
+
// 1. Who are you? (GitHub-bound invites need a signature from your key.)
|
|
93
|
+
let ch;
|
|
94
|
+
try {
|
|
95
|
+
ch = await t.request("POST", "/challenge", { code });
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
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?`));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (ch.status !== 200) {
|
|
102
|
+
console.log(C.red(`✗ ${ch.body.error ?? "couldn't join"}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const join = { code, name: githubUser() ?? os.userInfo().username };
|
|
106
|
+
if (ch.body.github) {
|
|
107
|
+
console.log(C.faint(` this invite is for @${ch.body.github} — proving it with your GitHub SSH key…`));
|
|
108
|
+
const s = await signAs(ch.body.github, ch.body.nonce);
|
|
109
|
+
if (!s.ok) {
|
|
110
|
+
console.log(C.red(`✗ ${s.reason}`));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
Object.assign(join, { key: s.key, sig: s.sig });
|
|
114
|
+
}
|
|
115
|
+
const j = await t.request("POST", "/join", join);
|
|
116
|
+
if (j.status !== 200) {
|
|
117
|
+
console.log(C.red(`✗ ${j.body.error ?? "couldn't join"}`));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const token = j.body.token;
|
|
121
|
+
const room = j.body.room ?? {};
|
|
122
|
+
const me = j.body.member;
|
|
123
|
+
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)}`);
|
|
124
|
+
printBoard(j.body.board);
|
|
125
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: C.violet("room ›") + " " });
|
|
126
|
+
let asking = false;
|
|
127
|
+
const held = [];
|
|
128
|
+
const say = (line) => {
|
|
129
|
+
if (asking)
|
|
130
|
+
return void held.push(line);
|
|
131
|
+
process.stdout.write("\r\x1b[2K");
|
|
132
|
+
console.log(line);
|
|
133
|
+
rl.prompt(true);
|
|
134
|
+
};
|
|
135
|
+
const ask = (q) => new Promise((resolve) => {
|
|
136
|
+
asking = true;
|
|
137
|
+
process.stdout.write("\r\x1b[2K");
|
|
138
|
+
rl.question(q, (a) => {
|
|
139
|
+
asking = false;
|
|
140
|
+
for (const l of held.splice(0))
|
|
141
|
+
console.log(l);
|
|
142
|
+
resolve(a.trim().toLowerCase());
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
// 2. Code layer: are we in a clone of the room's repo? If not, offer to clone.
|
|
146
|
+
let root = repoRootOf(process.cwd());
|
|
147
|
+
const sameRepo = (r) => {
|
|
148
|
+
const ri = r ? remoteInfo(r) : null;
|
|
149
|
+
return !!(ri && room.remote && canonicalRemote(ri.url) === room.remote);
|
|
150
|
+
};
|
|
151
|
+
if (me.role !== "guest" && room.remote && !sameRepo(root)) {
|
|
152
|
+
const name = room.remote.split("/").pop();
|
|
153
|
+
const here = path.join(process.cwd(), name);
|
|
154
|
+
if (fs.existsSync(here) && sameRepo(repoRootOf(here))) {
|
|
155
|
+
process.chdir(here);
|
|
156
|
+
root = repoRootOf(here);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log(C.faint(`\n Your agents work in your own copy of ${room.remote}.`));
|
|
160
|
+
const a = await ask(` Clone it into ./${name}? ${C.faint("[Y/n]")} `);
|
|
161
|
+
if (a === "" || a.startsWith("y")) {
|
|
162
|
+
const cl = spawnSync("git", ["clone", "--quiet", `https://${room.remote}.git`, here], { stdio: "inherit" });
|
|
163
|
+
if (cl.status === 0) {
|
|
164
|
+
process.chdir(here);
|
|
165
|
+
root = repoRootOf(here);
|
|
166
|
+
console.log(` ${C.green("✓")} cloned into ${here}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const crewMode = me.role !== "guest" && sameRepo(root);
|
|
172
|
+
// 3. Your crew joins theirs.
|
|
173
|
+
let workers = [];
|
|
174
|
+
let proxy = null;
|
|
175
|
+
const assigned = new Map(); // local agent → task
|
|
176
|
+
let always = false;
|
|
177
|
+
if (crewMode && root) {
|
|
178
|
+
process.chdir(root);
|
|
179
|
+
ensureInit();
|
|
180
|
+
const results = await connectAgents();
|
|
181
|
+
workers = workersFrom(results);
|
|
182
|
+
proxy = await startProxy(t, token, (agent) => assigned.get(agent)?.taskId);
|
|
183
|
+
const r = await t.request("POST", "/crew", { agents: workers.map((w) => ({ id: w.id, vendor: w.adapter.vendor, prefers: w.adapter.prefers })) }, token);
|
|
184
|
+
if (r.status === 200)
|
|
185
|
+
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")}`);
|
|
186
|
+
}
|
|
187
|
+
else if (me.role !== "guest") {
|
|
188
|
+
console.log(C.faint(`\n watching only — run \`huddle join\` from your clone of ${room.remote ?? "their repo"} to bring your crew`));
|
|
189
|
+
}
|
|
190
|
+
console.log(C.faint("\n type to message the room · board · leave (or ctrl-d)\n"));
|
|
191
|
+
// 4. Doing a task the room offered — on this machine, your rules.
|
|
192
|
+
const runTask = async (p) => {
|
|
193
|
+
const w = workers.find((x) => x.id === p.agent);
|
|
194
|
+
const branch = p.branch;
|
|
195
|
+
const t0 = Date.now();
|
|
196
|
+
const deliver = (body) => t.request("POST", "/delivered", { task_id: p.task.id, ms: Date.now() - t0, ...body }, token);
|
|
197
|
+
if (!w || !root || !/^huddle\/[\w./#-]+$/.test(branch))
|
|
198
|
+
return deliver({ ok: false, reason: "no such agent here" });
|
|
199
|
+
// Worktree off the room's pushed main (or the branch itself, on a fix round).
|
|
200
|
+
const f = git(root, ["fetch", "--quiet", "origin"]);
|
|
201
|
+
if (!f.ok)
|
|
202
|
+
return deliver({ ok: false, reason: `couldn't fetch: ${f.err.split("\n")[0]}` });
|
|
203
|
+
const wtPath = path.join(root, ".huddle", "worktrees", branch.replace(/[^\w.-]/g, "_"));
|
|
204
|
+
if (!fs.existsSync(wtPath)) {
|
|
205
|
+
const hasRemoteBranch = git(root, ["rev-parse", "--verify", "--quiet", `origin/${branch}`]).ok;
|
|
206
|
+
const add = hasRemoteBranch
|
|
207
|
+
? git(root, ["worktree", "add", "-B", branch, wtPath, `origin/${branch}`])
|
|
208
|
+
: git(root, ["worktree", "add", "-B", branch, wtPath, p.base]);
|
|
209
|
+
if (!add.ok)
|
|
210
|
+
return deliver({ ok: false, reason: `couldn't prepare a worktree: ${add.err.split("\n")[0]}` });
|
|
211
|
+
}
|
|
212
|
+
say(` ${dot(w.id)} ${pad(w.id, 10)} ${C.violet("▸")} ${p.task.title} ${C.faint("· on your machine")}`);
|
|
213
|
+
const prefs = readPrefs();
|
|
214
|
+
const h = launchAgent({
|
|
215
|
+
repoRoot: root,
|
|
216
|
+
adapter: w.adapter,
|
|
217
|
+
agentId: w.id,
|
|
218
|
+
role: "worker",
|
|
219
|
+
taskId: p.task.id,
|
|
220
|
+
taskSpec: `${p.task.title}\n\n${p.task.spec}`,
|
|
221
|
+
roomUrl: proxy.url,
|
|
222
|
+
roomSecret: proxy.secret,
|
|
223
|
+
approvals: prefs.approvals ?? "auto",
|
|
224
|
+
worktree: { branch, path: wtPath },
|
|
225
|
+
});
|
|
226
|
+
assigned.set(w.id, { taskId: p.task.id, handle: h });
|
|
227
|
+
const code = await new Promise((resolve) => {
|
|
228
|
+
h.child.on("exit", resolve);
|
|
229
|
+
h.child.on("error", () => resolve(-1));
|
|
230
|
+
});
|
|
231
|
+
assigned.delete(w.id);
|
|
232
|
+
if (code !== 0) {
|
|
233
|
+
say(` ${dot(w.id)} ${pad(w.id, 10)} ${C.red("✗")} exited with ${code} ${C.faint("· log: " + h.logPath)}`);
|
|
234
|
+
return deliver({ ok: false, reason: `agent exited with code ${code}`, tokens: h.meter.tokens, cached: h.meter.cached });
|
|
235
|
+
}
|
|
236
|
+
// Commit here, push the branch; the room tests, reviews and merges it.
|
|
237
|
+
git(wtPath, ["add", "-A", "--", ".", ...h.excludes.map((e) => `:!${e}`)]);
|
|
238
|
+
git(wtPath, ["commit", "--quiet", "--allow-empty", "-m", `huddle(${p.worker}): ${p.task.title}`]);
|
|
239
|
+
const push = pushBranch(wtPath, branch);
|
|
240
|
+
if (!push.ok)
|
|
241
|
+
return deliver({ ok: false, reason: `couldn't push ${branch}: ${push.note}`, tokens: h.meter.tokens });
|
|
242
|
+
removeWorktree(root, wtPath);
|
|
243
|
+
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`)}`);
|
|
244
|
+
await deliver({ ok: true, branch, tokens: h.meter.tokens, cached: h.meter.cached });
|
|
245
|
+
};
|
|
246
|
+
// 5. Live room.
|
|
247
|
+
let leaving = false;
|
|
248
|
+
const leave = async () => {
|
|
249
|
+
if (leaving)
|
|
250
|
+
return;
|
|
251
|
+
leaving = true;
|
|
252
|
+
for (const a of assigned.values())
|
|
253
|
+
a.handle?.child?.kill("SIGTERM");
|
|
254
|
+
await t.request("POST", "/leave", {}, token).catch(() => { });
|
|
255
|
+
proxy?.close();
|
|
256
|
+
t.close();
|
|
257
|
+
process.stdout.write("\r\x1b[2K");
|
|
258
|
+
console.log(C.faint("left the room · your access is revoked"));
|
|
259
|
+
process.exit(0);
|
|
260
|
+
};
|
|
261
|
+
t.events(token, async (e) => {
|
|
262
|
+
const p = typeof e.payload === "string" ? JSON.parse(e.payload || "{}") : e.payload ?? {};
|
|
263
|
+
if (e.type === "message")
|
|
264
|
+
say(` ${C.violet("💬")} ${C.bold(p.from)} ${C.faint("›")} ${p.body}`);
|
|
265
|
+
else if (e.type === "member.joined")
|
|
266
|
+
say(` ${C.green("●")} ${C.bold(p.github ? "@" + p.github : p.name)} joined`);
|
|
267
|
+
else if (e.type === "member.left")
|
|
268
|
+
say(` ${C.faint("○")} ${C.bold(p.name)} ${p.reason === "removed" ? "was removed" : "left"}`);
|
|
269
|
+
else if (e.type === "session.created")
|
|
270
|
+
say(`\n ${C.violet("▸ new goal:")} ${p.goal}`);
|
|
271
|
+
else if (e.type === "task.claimed")
|
|
272
|
+
say(` ${dot(e.agent_id ?? "")} ${pad(e.agent_id ?? "", 14)} ${C.violet("▸")} picked up a task`);
|
|
273
|
+
else if (e.type === "task.status" && p.status === "merged")
|
|
274
|
+
say(` ${C.green("✓ merged")} ${C.faint(e.task_id)}`);
|
|
275
|
+
else if (e.type === "task.progress" && p.note)
|
|
276
|
+
say(` ${C.faint(String(p.pct).padStart(3) + "%")} ${p.note}`);
|
|
277
|
+
else if (e.type === "ask")
|
|
278
|
+
say(` ${dot(p.from)} ${C.bold(p.from)} ${C.faint("→")} ${p.to ? C.bold(p.to) : C.faint("room")} ${p.question}`);
|
|
279
|
+
else if (e.type === "answer" && p.source !== "teammate")
|
|
280
|
+
say(` ${dot(p.from)} ${C.bold(p.from)} ${C.faint("→")} ${C.bold(p.to)} ${p.answer} ${C.faint("— from shared memory")}`);
|
|
281
|
+
else if (e.type === "agent.message" && p.kind !== "question")
|
|
282
|
+
say(` ${dot(p.from)} ${C.bold(p.from)} ${C.faint("→")} ${C.bold(p.to)}${C.faint(":")} ${String(p.body).slice(0, 200)}`);
|
|
283
|
+
else if (e.type === "assign") {
|
|
284
|
+
// Rule 1: an offer is data. Only you (or your "always") turn it into work.
|
|
285
|
+
const title = p.task?.title ?? "a task";
|
|
286
|
+
let ok = always;
|
|
287
|
+
if (!ok) {
|
|
288
|
+
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]")} `);
|
|
289
|
+
if (a === "a" || a === "always")
|
|
290
|
+
always = ok = true;
|
|
291
|
+
else
|
|
292
|
+
ok = a === "" || a.startsWith("y");
|
|
293
|
+
}
|
|
294
|
+
await t.request("POST", "/assignment", { task_id: p.task.id, accept: ok }, token);
|
|
295
|
+
if (ok)
|
|
296
|
+
void runTask(p);
|
|
297
|
+
else
|
|
298
|
+
say(C.faint(` declined · it goes back on their board`));
|
|
299
|
+
rl.prompt(true);
|
|
300
|
+
}
|
|
301
|
+
}, (code) => {
|
|
302
|
+
if (leaving)
|
|
303
|
+
return;
|
|
304
|
+
process.stdout.write("\r\x1b[2K");
|
|
305
|
+
console.log(code === 4403 ? C.yellow("you were removed from the room") : C.faint("the room closed"));
|
|
306
|
+
for (const a of assigned.values())
|
|
307
|
+
a.handle?.child?.kill("SIGTERM");
|
|
308
|
+
proxy?.close();
|
|
309
|
+
process.exit(0);
|
|
310
|
+
});
|
|
311
|
+
rl.on("line", async (raw) => {
|
|
312
|
+
if (asking)
|
|
313
|
+
return;
|
|
314
|
+
const line = raw.trim();
|
|
315
|
+
if (!line)
|
|
316
|
+
return rl.prompt();
|
|
317
|
+
if (line === "leave" || line === "exit" || line === "quit")
|
|
318
|
+
return leave();
|
|
319
|
+
if (line === "board") {
|
|
320
|
+
const b = await t.request("GET", "/board", undefined, token);
|
|
321
|
+
printBoard(b.body);
|
|
322
|
+
return rl.prompt();
|
|
323
|
+
}
|
|
324
|
+
const r = await t.request("POST", "/message", { body: line }, token);
|
|
325
|
+
if (r.status !== 200)
|
|
326
|
+
say(C.red(` ✗ ${r.body.error ?? "not sent"}`));
|
|
327
|
+
rl.prompt();
|
|
328
|
+
});
|
|
329
|
+
rl.on("close", () => void leave());
|
|
330
|
+
rl.prompt();
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=remote.js.map
|