vibeshare-live 0.1.1 → 0.2.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.
package/dist/cli.js CHANGED
@@ -1,278 +1,432 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ ConsentRequiredError,
4
+ LocalHttpTransport,
5
+ SHARE_SCOPE,
6
+ ShareManager,
3
7
  VERSION,
4
- createShare
5
- } from "./chunk-SBAGPPGC.js";
8
+ clearActiveShare,
9
+ listActiveShares,
10
+ loadLedger,
11
+ readActiveShare,
12
+ writeActiveShare
13
+ } from "./chunk-RY2ANOPC.js";
6
14
 
7
15
  // src/cli.ts
8
- import { realpathSync } from "fs";
9
- import { createInterface } from "readline";
10
- import { networkInterfaces } from "os";
11
- import { pathToFileURL } from "url";
12
- import { createHost, createRelay, SHARE_SESSION_SCOPE } from "vibelive";
13
- var HOST_ID = "host";
14
- var BADGE = "\u25CF p2p \xB7 e2e \xB7 nothing stored on a server";
15
- var err = (message) => ({ cmd: "error", message });
16
- function parseArgs(argv) {
17
- const dd = argv.indexOf("--");
18
- const head = dd >= 0 ? argv.slice(0, dd) : [...argv];
19
- const command = dd >= 0 ? argv.slice(dd + 1) : [];
20
- if (head.includes("--help") || head.includes("-h")) return { cmd: "help" };
21
- if (head.includes("--version") || head.includes("-v")) return { cmd: "version" };
22
- const sub = head[0];
23
- if (sub === "stop") return { cmd: "stop" };
24
- if (sub === "viewers") return { cmd: "viewers" };
25
- if (sub === "mcp") return { cmd: "mcp" };
26
- if (head.length === 0 && command.length === 0) return { cmd: "help" };
27
- let flagTokens;
28
- if (sub === "host") {
29
- flagTokens = head.slice(1);
30
- } else if (sub === void 0 || sub.startsWith("-")) {
31
- flagTokens = head;
32
- } else {
33
- return err(`unknown command: ${sub}`);
16
+ import { spawn } from "child_process";
17
+ import { createInterface } from "readline/promises";
18
+ import { badge, createHookBus, parseConfirm, watchCwd } from "@pooriaarab/vibe-core";
19
+ var CliUsageError = class extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "CliUsageError";
34
23
  }
35
- let access = "spectate";
36
- let expire;
37
- let pass;
38
- let name;
39
- for (let i = 0; i < flagTokens.length; i++) {
40
- const t = flagTokens[i];
41
- if (t === void 0) break;
42
- const next = flagTokens[i + 1];
43
- if (t === "--spectate") {
44
- access = "spectate";
45
- } else if (t === "--invite") {
46
- access = "invite";
47
- } else if (t === "--expire") {
48
- if (next === void 0) return err("--expire requires a value (1h | 24h)");
49
- if (next !== "1h" && next !== "24h") {
50
- return err(`--expire must be "1h" or "24h", got: ${next}`);
51
- }
52
- expire = next;
53
- i++;
54
- } else if (t === "--pass") {
55
- if (next === void 0) return err("--pass requires a value");
56
- pass = next;
57
- i++;
58
- } else if (t === "--name") {
59
- if (next === void 0) return err("--name requires a value");
60
- name = next;
61
- i++;
62
- } else {
63
- return err(`unknown flag: ${t}`);
64
- }
65
- }
66
- if (command.length === 0) {
67
- return err('vibeshare needs a command after "--", e.g. `vibeshare --invite -- claude`');
68
- }
69
- return { cmd: "host", command, access, expire, pass, name };
70
- }
71
- var HELP = `vibeshare ${VERSION} \u2014 share your live agent session by URL
72
-
73
- USAGE
74
- vibeshare [--spectate|--invite] [--expire 1h|24h] [--pass <p>] -- <command...>
75
- vibeshare host [the flags above] -- <command...>
76
- Host a session running <command> (e.g. \`vibeshare --invite -- claude\`)
77
- and print its share URL. You start as the driver.
24
+ };
25
+ var USAGE = `vibeshare \u2014 share your live agent coding session by URL
78
26
 
79
- --spectate link holders get read-only access (default)
80
- --invite link holders may request to join; approve to let them drive
81
- --expire auto-revoke the share after 1h or 24h
82
- --pass <p> require a passphrase on top of the share URL
83
- --name <s> display name for the host participant
27
+ usage:
28
+ vibeshare [options] [-- <cmd\u2026>] start sharing (default: your shell)
29
+ vibeshare viewers [shareId] list viewers; act on join requests
30
+ vibeshare stop [shareId] end the active share
84
31
 
85
- vibeshare stop stop the foreground host session (also: /stop inside it)
86
- vibeshare viewers (in-foreground) list viewers / pending join requests
87
- vibeshare mcp run the vibeshare MCP server on stdio
88
- vibeshare --version
89
- vibeshare --help
32
+ options:
33
+ --spectate viewers watch read-only (default)
34
+ --invite viewers may request to join as collaborators
35
+ --expire <when> 1h, 24h, 7d, \u2026 or "stop" (default: until you stop)
36
+ --pass <phrase> require a passphrase to watch
37
+ --port <n> port to serve on (default: random)
38
+ --host <addr> bind address (default: 127.0.0.1; 0.0.0.0 shares on LAN)
39
+ --name <label> what to call the session
40
+ --yes, -y grant share:session consent without prompting
41
+ --json machine-readable output (viewers)
42
+ --approve <id> approve a viewer's join request (viewers)
43
+ --deny <id> deny a join request (viewers)
44
+ --kick <id> remove a viewer (viewers)
45
+ --version, -v print version
46
+ --help, -h this help
90
47
 
91
- v0 is foreground, host-authoritative, local/LAN. The share URL points at your own
92
- domain (vibeshare.stream); the live stream runs over a self-hostable e2e relay.
93
- ${BADGE}
94
- `;
95
- function printHelp(stream = process.stdout) {
96
- stream.write(HELP);
97
- }
98
- function lanAddress() {
99
- const nets = networkInterfaces();
100
- for (const list of Object.values(nets)) {
101
- if (!list) continue;
102
- for (const n of list) {
103
- if (n && n.family === "IPv4" && !n.internal) return n.address;
48
+ local-first: the stream is served from this machine; nothing is stored on a
49
+ server. Consent scope "share:session" is recorded in ~/.vibeshare/consent.json.`;
50
+ function parseArgv(argv) {
51
+ const rest = [...argv];
52
+ const sub = rest[0];
53
+ if (sub === "--help" || sub === "-h" || sub === "help") return { cmd: "help" };
54
+ if (sub === "--version" || sub === "-v" || sub === "version") return { cmd: "version" };
55
+ if (sub === "stop") {
56
+ const extra = positionalArgs(rest.slice(1));
57
+ if (extra.length > 1) throw new CliUsageError("stop takes at most one share id");
58
+ return { cmd: "stop", ...extra[0] !== void 0 ? { share: extra[0] } : {} };
59
+ }
60
+ if (sub === "viewers") {
61
+ const args = rest.slice(1);
62
+ const out = {
63
+ cmd: "viewers",
64
+ json: false
65
+ };
66
+ let actions = 0;
67
+ for (let i = 0; i < args.length; i++) {
68
+ const a = args[i];
69
+ if (a === "--json") out.json = true;
70
+ else if (a === "--approve" || a === "--deny" || a === "--kick") {
71
+ const id = args[++i];
72
+ if (id === void 0) throw new CliUsageError(`${a} needs a viewer id`);
73
+ const key = a.slice(2);
74
+ out[key] = id;
75
+ actions++;
76
+ } else if (a.startsWith("-")) throw new CliUsageError(`unknown option for viewers: ${a}`);
77
+ else if (out.share === void 0) out.share = a;
78
+ else throw new CliUsageError(`unexpected argument: ${a}`);
104
79
  }
80
+ if (actions > 1) throw new CliUsageError("use only one of --approve / --deny / --kick");
81
+ return out;
105
82
  }
106
- return null;
107
- }
108
- async function runHost(p) {
109
- const host = createHost({ command: p.command });
110
- const relay = await createRelay({
83
+ if (sub === "start") rest.shift();
84
+ const options = {
85
+ access: "spectate",
86
+ expiry: "stop",
111
87
  port: 0,
112
- hostHandle: host,
113
- initialDriver: HOST_ID,
114
- hostParticipantName: p.name ?? HOST_ID
115
- });
116
- relay.consent.grant(SHARE_SESSION_SCOPE);
117
- let expired = false;
118
- const share = createShare({
119
- session: relay,
120
- access: p.access,
121
- expiry: p.expire,
122
- passphrase: p.pass,
123
- onRevoke: (reason) => {
124
- if (reason === "expired") expired = true;
125
- }
126
- });
127
- host.onOutput((entry) => process.stdout.write(entry.text));
128
- const accessLabel = p.access + (p.pass ? " \xB7 passphrase" : "") + (p.expire ? ` \xB7 expires ${p.expire}` : " \xB7 no expiry");
129
- process.stderr.write(`vibeshare \u2014 sharing ${p.command.join(" ")}
130
- `);
131
- process.stderr.write(` share: ${share.url}
132
- `);
133
- process.stderr.write(` access: ${accessLabel}
134
- `);
135
- process.stderr.write(` relay: ${relay.url}
136
- `);
137
- const lan = lanAddress();
138
- if (lan) {
139
- process.stderr.write(` lan: ws://${lan}:${relay.port}
140
- `);
88
+ host: "127.0.0.1",
89
+ yes: false,
90
+ command: []
91
+ };
92
+ for (let i = 0; i < rest.length; i++) {
93
+ const a = rest[i];
94
+ const value = (flag) => {
95
+ const v = rest[++i];
96
+ if (v === void 0) throw new CliUsageError(`${flag} needs a value`);
97
+ return v;
98
+ };
99
+ if (a === "--") {
100
+ options.command = rest.slice(i + 1);
101
+ break;
102
+ } else if (a === "--spectate") options.access = "spectate";
103
+ else if (a === "--invite") options.access = "invite";
104
+ else if (a === "--expire" || a === "--expiry") options.expiry = value(a);
105
+ else if (a === "--pass") options.passphrase = value(a);
106
+ else if (a === "--port") {
107
+ const n = Number(value(a));
108
+ if (!Number.isInteger(n) || n < 0 || n > 65535) throw new CliUsageError("--port must be 0\u201365535");
109
+ options.port = n;
110
+ } else if (a === "--host") options.host = value(a);
111
+ else if (a === "--name") options.name = value(a);
112
+ else if (a === "--yes" || a === "-y") options.yes = true;
113
+ else if (a === "--help" || a === "-h") return { cmd: "help" };
114
+ else if (a.startsWith("-")) throw new CliUsageError(`unknown option: ${a}`);
115
+ else options.command = rest.slice(i);
116
+ if (!a.startsWith("-")) break;
141
117
  }
142
- process.stderr.write(` ${BADGE}
143
- `);
144
- process.stderr.write(
145
- ` you are the driver. /release to hand off, /drive to take back, /viewers, /approve <id>, /stop to end.
146
- `
147
- );
148
- const rl = createInterface({ input: process.stdin, terminal: false });
149
- const printViewers = () => {
150
- const roster = share.viewers();
151
- if (roster.viewers.length === 0 && roster.pending.length === 0) {
152
- process.stderr.write(" (no viewers connected)\n");
153
- return;
154
- }
155
- for (const v of roster.viewers) {
156
- process.stderr.write(` \u25CF ${v.name} [${v.role}]
157
- `);
118
+ return { cmd: "start", options };
119
+ }
120
+ function positionalArgs(args) {
121
+ const out = [];
122
+ for (const a of args) {
123
+ if (a.startsWith("-")) throw new CliUsageError(`unknown option: ${a}`);
124
+ out.push(a);
125
+ }
126
+ return out;
127
+ }
128
+ var stdio = {
129
+ out: (t) => process.stdout.write(t + "\n"),
130
+ err: (t) => process.stderr.write(t + "\n")
131
+ };
132
+ var HOOK_KINDS = [
133
+ "task-done",
134
+ "pr-opened",
135
+ "prototype-finished",
136
+ "spec-completed",
137
+ "tests-pass",
138
+ "tests-fail",
139
+ "error",
140
+ "session-end",
141
+ "manual"
142
+ ];
143
+ async function ensureConsent(io, yes) {
144
+ const ledger = loadLedger();
145
+ if (ledger.allows(SHARE_SCOPE)) return true;
146
+ if (yes) {
147
+ ledger.grant(SHARE_SCOPE, "granted via vibeshare --yes");
148
+ return true;
149
+ }
150
+ if (!process.stdin.isTTY) {
151
+ io.err(`consent required: re-run with --yes to grant "${SHARE_SCOPE}" (recorded locally in ~/.vibeshare/consent.json)`);
152
+ return false;
153
+ }
154
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
155
+ try {
156
+ const answer = await rl.question(
157
+ `Share this session live by URL? Anyone with the link can view its output. (y/N) `
158
+ );
159
+ if (!parseConfirm(answer)) {
160
+ io.err("aborted \u2014 no consent granted");
161
+ return false;
158
162
  }
159
- for (const v of roster.pending) {
160
- process.stderr.write(` \u2026 ${v.name} [wants to join \u2014 /approve ${v.id}]
161
- `);
163
+ ledger.grant(SHARE_SCOPE, "granted via vibeshare CLI prompt");
164
+ return true;
165
+ } finally {
166
+ rl.close();
167
+ }
168
+ }
169
+ function lineBuffer(publish) {
170
+ let buf = "";
171
+ return {
172
+ push(chunk) {
173
+ buf += chunk;
174
+ let idx;
175
+ while ((idx = buf.indexOf("\n")) !== -1) {
176
+ publish(buf.slice(0, idx).replace(/\r$/, ""));
177
+ buf = buf.slice(idx + 1);
178
+ }
179
+ if (buf.length > 8192) {
180
+ publish(buf);
181
+ buf = "";
182
+ }
183
+ },
184
+ flush() {
185
+ if (buf.length > 0) {
186
+ publish(buf);
187
+ buf = "";
188
+ }
162
189
  }
163
190
  };
164
- rl.on("line", (line) => {
165
- if (line === "/stop" || line === "/quit") {
166
- void shutdown(0);
167
- return;
168
- }
169
- if (line === "/viewers") {
170
- printViewers();
171
- return;
172
- }
173
- if (line.startsWith("/approve ")) {
174
- const id = line.slice("/approve ".length).trim();
175
- const ok = share.approve(id);
176
- process.stderr.write(ok ? ` promoted ${id}.
177
- ` : ` can't promote ${id} (unknown / spectate-only).
178
- `);
179
- return;
180
- }
181
- if (line === "/release") {
182
- relay.localReleaseControl(HOST_ID);
183
- process.stderr.write(" control released.\n");
184
- return;
185
- }
186
- if (line === "/drive") {
187
- relay.localRequestControl(HOST_ID);
188
- process.stderr.write(" control requested.\n");
189
- return;
191
+ }
192
+ async function startShare(options, io) {
193
+ if (!await ensureConsent(io, options.yes)) return 1;
194
+ const bus = createHookBus({ onError: (e) => io.err(`[vibeshare] hook error: ${String(e)}`) });
195
+ const watcher = watchCwd(process.cwd(), bus);
196
+ let created = null;
197
+ const transport = new LocalHttpTransport({
198
+ host: options.host,
199
+ port: options.port,
200
+ onStopRequested: () => {
201
+ shutdown(0);
190
202
  }
191
- if (relay.arbiter.isDriver(HOST_ID)) {
192
- host.sendInput(`${line}
193
- `);
194
- } else {
195
- process.stderr.write(" (not the driver \u2014 /drive to take control)\n");
203
+ });
204
+ const manager = new ShareManager({ consent: loadLedger(), transport });
205
+ try {
206
+ created = await manager.createShare({
207
+ access: options.access,
208
+ expiry: options.expiry,
209
+ ...options.passphrase !== void 0 ? { passphrase: options.passphrase } : {},
210
+ ...options.name !== void 0 ? { name: options.name } : {},
211
+ session: options.command.length > 0 ? options.command.join(" ") : void 0
212
+ });
213
+ } catch (err) {
214
+ watcher.stop();
215
+ await transport.close();
216
+ if (err instanceof ConsentRequiredError || err instanceof Error) {
217
+ io.err(`vibeshare: ${err.message}`);
218
+ return err instanceof ConsentRequiredError ? 1 : 2;
196
219
  }
220
+ throw err;
221
+ }
222
+ for (const kind of HOOK_KINDS) {
223
+ bus.on(kind, (e) => {
224
+ created?.feed.publishEvent(e);
225
+ });
226
+ }
227
+ const record = {
228
+ id: created.share.id,
229
+ url: created.url,
230
+ port: transport.port,
231
+ hostToken: transport.hostToken,
232
+ pid: process.pid,
233
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
234
+ };
235
+ writeActiveShare(record);
236
+ const loopback = options.host === "127.0.0.1" || options.host === "localhost" || options.host === "::1";
237
+ io.out(`${badge(loopback ? "local" : "p2p")}`);
238
+ io.out(` sharing: ${created.share.name}`);
239
+ io.out(` url: ${created.url}`);
240
+ io.out(` access: ${created.share.access}${created.share.access === "spectate" ? " (read-only)" : " (viewers may request to join)"}`);
241
+ io.out(` expires: ${created.share.expiresAt ?? "until you stop"}`);
242
+ if (created.share.passphraseHash) io.out(` pass: required`);
243
+ io.out(` manage: vibeshare viewers \xB7 vibeshare stop`);
244
+ if (!loopback) io.err("note: bound to a non-loopback address \u2014 anyone who can reach this host with the link can watch.");
245
+ const cmd = options.command.length > 0 ? options.command : [process.env["SHELL"] ?? "/bin/sh"];
246
+ const child = spawn(cmd[0], cmd.slice(1), {
247
+ stdio: ["inherit", "pipe", "pipe"],
248
+ env: process.env
249
+ });
250
+ const outLines = lineBuffer((l) => created?.feed.publish(l, { stream: "stdout" }));
251
+ const errLines = lineBuffer((l) => created?.feed.publish(l, { stream: "stderr" }));
252
+ child.stdout?.on("data", (d) => {
253
+ process.stdout.write(d);
254
+ outLines.push(d.toString("utf8"));
255
+ });
256
+ child.stderr?.on("data", (d) => {
257
+ process.stderr.write(d);
258
+ errLines.push(d.toString("utf8"));
197
259
  });
198
- rl.on("SIGINT", () => void shutdown(0));
199
260
  let shuttingDown = false;
200
- const shutdown = async (code2) => {
201
- if (shuttingDown) return;
202
- shuttingDown = true;
203
- rl.close();
204
- host.kill();
205
- await share.revoke();
206
- process.exit(code2);
207
- };
208
- process.on("SIGINT", () => void shutdown(130));
209
- process.on("SIGTERM", () => void shutdown(143));
210
- if (p.expire !== void 0) {
211
- void promiseTrue(() => share.revoked).then(async () => {
212
- if (expired) {
213
- process.stderr.write(" share expired \u2014 tearing down.\n");
214
- await shutdown(0);
215
- }
261
+ const exitCode = await new Promise((resolve) => {
262
+ child.on("error", (err) => {
263
+ io.err(`vibeshare: could not start ${cmd[0]}: ${err.message}`);
264
+ resolve(2);
265
+ });
266
+ child.on("exit", (code, signal) => {
267
+ resolve(code ?? (signal !== null ? 128 : 0));
216
268
  });
269
+ process.on("SIGINT", () => shutdown2(130));
270
+ process.on("SIGTERM", () => shutdown2(143));
271
+ function shutdown2(code) {
272
+ if (shuttingDown) return;
273
+ shuttingDown = true;
274
+ child.kill("SIGTERM");
275
+ resolve(code);
276
+ }
277
+ shutdownRef = shutdown2;
278
+ });
279
+ outLines.flush();
280
+ errLines.flush();
281
+ watcher.stop();
282
+ clearActiveShare(record.id);
283
+ await manager.stopAll();
284
+ return exitCode;
285
+ }
286
+ var shutdownRef = null;
287
+ function shutdown(code) {
288
+ shutdownRef?.(code);
289
+ }
290
+ async function controlFetch(record, path, init) {
291
+ let res;
292
+ try {
293
+ res = await fetch(`http://127.0.0.1:${record.port}${path}`, {
294
+ method: init?.method ?? "GET",
295
+ headers: {
296
+ authorization: `Bearer ${record.hostToken}`,
297
+ ...init?.body !== void 0 ? { "content-type": "application/json" } : {}
298
+ },
299
+ ...init?.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
300
+ });
301
+ } catch {
302
+ return { ok: false, status: 0, message: "connection refused" };
303
+ }
304
+ const body = await res.json().catch(() => ({}));
305
+ if (!res.ok) {
306
+ const message = typeof body["error"] === "string" ? body["error"] : `HTTP ${res.status}`;
307
+ return { ok: false, status: res.status, message };
217
308
  }
218
- const code = await host.exited;
219
- await shutdown(code ?? 0);
220
- return code ?? 0;
309
+ return { ok: true, data: body };
221
310
  }
222
- function promiseTrue(pred) {
223
- return new Promise((resolve) => {
224
- const tick = () => {
225
- if (pred()) resolve();
226
- else setTimeout(tick, 250);
227
- };
228
- tick();
229
- });
311
+ function resolveRecord(shareId, io) {
312
+ if (shareId !== void 0) {
313
+ const rec = readActiveShare(shareId);
314
+ if (rec) return rec;
315
+ io.err(`no recorded share ${shareId}`);
316
+ return null;
317
+ }
318
+ const [latest] = listActiveShares();
319
+ if (!latest) {
320
+ io.err("no active vibeshare share \u2014 start one with `vibeshare`");
321
+ return null;
322
+ }
323
+ return latest;
230
324
  }
231
- async function runStop() {
232
- process.stderr.write(
233
- "vibeshare runs the host in the foreground. To stop it, switch to that terminal and\npress Ctrl+C (or type /stop). v0 does not yet daemonize or keep cross-process state.\n"
325
+ async function viewersCommand(cmd, io) {
326
+ const record = resolveRecord(cmd.share, io);
327
+ if (!record) return 1;
328
+ const action = cmd.approve !== void 0 ? "approve" : cmd.deny !== void 0 ? "deny" : cmd.kick !== void 0 ? "kick" : null;
329
+ const target = cmd.approve ?? cmd.deny ?? cmd.kick;
330
+ if (action !== null && target !== void 0) {
331
+ const res2 = await controlFetch(record, `/control/${action}`, {
332
+ method: "POST",
333
+ body: { share: record.id, viewer: target }
334
+ });
335
+ if (!res2.ok) {
336
+ if (res2.status === 0) clearActiveShare(record.id);
337
+ io.err(`vibeshare: could not ${action} ${target}: ${res2.message}`);
338
+ return 1;
339
+ }
340
+ const v = res2.data.viewer;
341
+ io.out(
342
+ action === "approve" ? `\u2713 approved ${v.name} \u2014 now a collaborator` : action === "deny" ? `\u2713 denied ${v.name} \u2014 stays a spectator` : `\u2713 kicked ${v.name}`
343
+ );
344
+ return 0;
345
+ }
346
+ const res = await controlFetch(
347
+ record,
348
+ `/control/viewers?share=${encodeURIComponent(record.id)}`
234
349
  );
350
+ if (!res.ok) {
351
+ if (res.status === 0) {
352
+ clearActiveShare(record.id);
353
+ io.err("vibeshare: the share process is not running (stale record cleaned up)");
354
+ } else {
355
+ io.err(`vibeshare: ${res.message}`);
356
+ }
357
+ return 1;
358
+ }
359
+ if (cmd.json) {
360
+ io.out(JSON.stringify(res.data, null, 2));
361
+ return 0;
362
+ }
363
+ io.out(`${record.url}`);
364
+ if (res.data.viewers.length === 0) {
365
+ io.out(" no viewers yet");
366
+ return 0;
367
+ }
368
+ for (const v of res.data.viewers) {
369
+ const pending = v.joinRequest === "pending" ? " \u23F3 requested to join \u2014 vibeshare viewers --approve " + v.id : "";
370
+ io.out(` ${v.id} ${v.name} [${v.role}]${pending}`);
371
+ }
372
+ io.out(` ${res.data.watching} watching now`);
235
373
  return 0;
236
374
  }
237
- async function runViewers() {
238
- process.stderr.write(
239
- "vibeshare runs the host in the foreground. Inside that session, type /viewers to list\nconnected viewers and pending join requests, and /approve <id> to promote one.\n"
240
- );
375
+ async function stopCommand(cmd, io) {
376
+ const record = resolveRecord(cmd.share, io);
377
+ if (!record) return 1;
378
+ const res = await controlFetch(record, "/control/stop", {
379
+ method: "POST",
380
+ body: { share: record.id }
381
+ });
382
+ clearActiveShare(record.id);
383
+ if (!res.ok) {
384
+ io.err(res.status === 0 ? "vibeshare: the share process was not running (record cleaned up)" : `vibeshare: ${res.message}`);
385
+ return res.status === 0 ? 0 : 1;
386
+ }
387
+ io.out(`\u2713 stopped ${record.url}`);
241
388
  return 0;
242
389
  }
243
- async function main(argv = process.argv.slice(2)) {
244
- const parsed = parseArgs(argv);
245
- switch (parsed.cmd) {
390
+ async function run(argv, io = stdio) {
391
+ let command;
392
+ try {
393
+ command = parseArgv(argv);
394
+ } catch (err) {
395
+ if (err instanceof CliUsageError) {
396
+ io.err(`vibeshare: ${err.message}
397
+ `);
398
+ io.err(USAGE);
399
+ return 2;
400
+ }
401
+ throw err;
402
+ }
403
+ switch (command.cmd) {
246
404
  case "help":
247
- printHelp();
405
+ io.out(USAGE);
248
406
  return 0;
249
407
  case "version":
250
- process.stdout.write(`${VERSION}
251
- `);
408
+ io.out(`vibeshare ${VERSION}`);
252
409
  return 0;
253
- case "mcp": {
254
- const { runMcpStdio } = await import("./mcp.js");
255
- await runMcpStdio();
256
- return 0;
257
- }
258
- case "stop":
259
- return runStop();
410
+ case "start":
411
+ return startShare(command.options, io);
260
412
  case "viewers":
261
- return runViewers();
262
- case "host":
263
- return runHost(parsed);
264
- case "error":
265
- process.stderr.write(`vibeshare: ${parsed.message}
266
- `);
267
- printHelp(process.stderr);
268
- return 2;
413
+ return viewersCommand(command, io);
414
+ case "stop":
415
+ return stopCommand(command, io);
269
416
  }
270
417
  }
271
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
272
- void main().then((code) => process.exit(code));
418
+ var isMain = process.argv[1] !== void 0 && import.meta.url === new URL(`file://${process.argv[1]}`).href;
419
+ if (isMain) {
420
+ run(process.argv.slice(2)).then(
421
+ (code) => process.exit(code),
422
+ (err) => {
423
+ console.error("vibeshare:", err);
424
+ process.exit(1);
425
+ }
426
+ );
273
427
  }
274
428
  export {
275
- main,
276
- parseArgs,
277
- printHelp
429
+ CliUsageError,
430
+ parseArgv,
431
+ run
278
432
  };