vibeshare-live 0.1.0 → 0.2.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/dist/cli.js CHANGED
@@ -1,277 +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-5OLI3WTU.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 { createInterface } from "readline";
9
- import { networkInterfaces } from "os";
10
- import { pathToFileURL } from "url";
11
- import { createHost, createRelay, SHARE_SESSION_SCOPE } from "vibelive-cli";
12
- var HOST_ID = "host";
13
- var BADGE = "\u25CF p2p \xB7 e2e \xB7 nothing stored on a server";
14
- var err = (message) => ({ cmd: "error", message });
15
- function parseArgs(argv) {
16
- const dd = argv.indexOf("--");
17
- const head = dd >= 0 ? argv.slice(0, dd) : [...argv];
18
- const command = dd >= 0 ? argv.slice(dd + 1) : [];
19
- if (head.includes("--help") || head.includes("-h")) return { cmd: "help" };
20
- if (head.includes("--version") || head.includes("-v")) return { cmd: "version" };
21
- const sub = head[0];
22
- if (sub === "stop") return { cmd: "stop" };
23
- if (sub === "viewers") return { cmd: "viewers" };
24
- if (sub === "mcp") return { cmd: "mcp" };
25
- if (head.length === 0 && command.length === 0) return { cmd: "help" };
26
- let flagTokens;
27
- if (sub === "host") {
28
- flagTokens = head.slice(1);
29
- } else if (sub === void 0 || sub.startsWith("-")) {
30
- flagTokens = head;
31
- } else {
32
- 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";
33
23
  }
34
- let access = "spectate";
35
- let expire;
36
- let pass;
37
- let name;
38
- for (let i = 0; i < flagTokens.length; i++) {
39
- const t = flagTokens[i];
40
- if (t === void 0) break;
41
- const next = flagTokens[i + 1];
42
- if (t === "--spectate") {
43
- access = "spectate";
44
- } else if (t === "--invite") {
45
- access = "invite";
46
- } else if (t === "--expire") {
47
- if (next === void 0) return err("--expire requires a value (1h | 24h)");
48
- if (next !== "1h" && next !== "24h") {
49
- return err(`--expire must be "1h" or "24h", got: ${next}`);
50
- }
51
- expire = next;
52
- i++;
53
- } else if (t === "--pass") {
54
- if (next === void 0) return err("--pass requires a value");
55
- pass = next;
56
- i++;
57
- } else if (t === "--name") {
58
- if (next === void 0) return err("--name requires a value");
59
- name = next;
60
- i++;
61
- } else {
62
- return err(`unknown flag: ${t}`);
63
- }
64
- }
65
- if (command.length === 0) {
66
- return err('vibeshare needs a command after "--", e.g. `vibeshare --invite -- claude`');
67
- }
68
- return { cmd: "host", command, access, expire, pass, name };
69
- }
70
- var HELP = `vibeshare ${VERSION} \u2014 share your live agent session by URL
71
-
72
- USAGE
73
- vibeshare [--spectate|--invite] [--expire 1h|24h] [--pass <p>] -- <command...>
74
- vibeshare host [the flags above] -- <command...>
75
- Host a session running <command> (e.g. \`vibeshare --invite -- claude\`)
76
- and print its share URL. You start as the driver.
24
+ };
25
+ var USAGE = `vibeshare \u2014 share your live agent coding session by URL
77
26
 
78
- --spectate link holders get read-only access (default)
79
- --invite link holders may request to join; approve to let them drive
80
- --expire auto-revoke the share after 1h or 24h
81
- --pass <p> require a passphrase on top of the share URL
82
- --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
83
31
 
84
- vibeshare stop stop the foreground host session (also: /stop inside it)
85
- vibeshare viewers (in-foreground) list viewers / pending join requests
86
- vibeshare mcp run the vibeshare MCP server on stdio
87
- vibeshare --version
88
- 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
89
47
 
90
- v0 is foreground, host-authoritative, local/LAN. The share URL points at your own
91
- domain (vibeshare.stream); the live stream runs over a self-hostable e2e relay.
92
- ${BADGE}
93
- `;
94
- function printHelp(stream = process.stdout) {
95
- stream.write(HELP);
96
- }
97
- function lanAddress() {
98
- const nets = networkInterfaces();
99
- for (const list of Object.values(nets)) {
100
- if (!list) continue;
101
- for (const n of list) {
102
- 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}`);
103
79
  }
80
+ if (actions > 1) throw new CliUsageError("use only one of --approve / --deny / --kick");
81
+ return out;
104
82
  }
105
- return null;
106
- }
107
- async function runHost(p) {
108
- const host = createHost({ command: p.command });
109
- const relay = await createRelay({
83
+ if (sub === "start") rest.shift();
84
+ const options = {
85
+ access: "spectate",
86
+ expiry: "stop",
110
87
  port: 0,
111
- hostHandle: host,
112
- initialDriver: HOST_ID,
113
- hostParticipantName: p.name ?? HOST_ID
114
- });
115
- relay.consent.grant(SHARE_SESSION_SCOPE);
116
- let expired = false;
117
- const share = createShare({
118
- session: relay,
119
- access: p.access,
120
- expiry: p.expire,
121
- passphrase: p.pass,
122
- onRevoke: (reason) => {
123
- if (reason === "expired") expired = true;
124
- }
125
- });
126
- host.onOutput((entry) => process.stdout.write(entry.text));
127
- const accessLabel = p.access + (p.pass ? " \xB7 passphrase" : "") + (p.expire ? ` \xB7 expires ${p.expire}` : " \xB7 no expiry");
128
- process.stderr.write(`vibeshare \u2014 sharing ${p.command.join(" ")}
129
- `);
130
- process.stderr.write(` share: ${share.url}
131
- `);
132
- process.stderr.write(` access: ${accessLabel}
133
- `);
134
- process.stderr.write(` relay: ${relay.url}
135
- `);
136
- const lan = lanAddress();
137
- if (lan) {
138
- process.stderr.write(` lan: ws://${lan}:${relay.port}
139
- `);
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;
140
117
  }
141
- process.stderr.write(` ${BADGE}
142
- `);
143
- process.stderr.write(
144
- ` you are the driver. /release to hand off, /drive to take back, /viewers, /approve <id>, /stop to end.
145
- `
146
- );
147
- const rl = createInterface({ input: process.stdin, terminal: false });
148
- const printViewers = () => {
149
- const roster = share.viewers();
150
- if (roster.viewers.length === 0 && roster.pending.length === 0) {
151
- process.stderr.write(" (no viewers connected)\n");
152
- return;
153
- }
154
- for (const v of roster.viewers) {
155
- process.stderr.write(` \u25CF ${v.name} [${v.role}]
156
- `);
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;
157
162
  }
158
- for (const v of roster.pending) {
159
- process.stderr.write(` \u2026 ${v.name} [wants to join \u2014 /approve ${v.id}]
160
- `);
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
+ }
161
189
  }
162
190
  };
163
- rl.on("line", (line) => {
164
- if (line === "/stop" || line === "/quit") {
165
- void shutdown(0);
166
- return;
167
- }
168
- if (line === "/viewers") {
169
- printViewers();
170
- return;
171
- }
172
- if (line.startsWith("/approve ")) {
173
- const id = line.slice("/approve ".length).trim();
174
- const ok = share.approve(id);
175
- process.stderr.write(ok ? ` promoted ${id}.
176
- ` : ` can't promote ${id} (unknown / spectate-only).
177
- `);
178
- return;
179
- }
180
- if (line === "/release") {
181
- relay.localReleaseControl(HOST_ID);
182
- process.stderr.write(" control released.\n");
183
- return;
184
- }
185
- if (line === "/drive") {
186
- relay.localRequestControl(HOST_ID);
187
- process.stderr.write(" control requested.\n");
188
- 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);
189
202
  }
190
- if (relay.arbiter.isDriver(HOST_ID)) {
191
- host.sendInput(`${line}
192
- `);
193
- } else {
194
- 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;
195
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"));
196
259
  });
197
- rl.on("SIGINT", () => void shutdown(0));
198
260
  let shuttingDown = false;
199
- const shutdown = async (code2) => {
200
- if (shuttingDown) return;
201
- shuttingDown = true;
202
- rl.close();
203
- host.kill();
204
- await share.revoke();
205
- process.exit(code2);
206
- };
207
- process.on("SIGINT", () => void shutdown(130));
208
- process.on("SIGTERM", () => void shutdown(143));
209
- if (p.expire !== void 0) {
210
- void promiseTrue(() => share.revoked).then(async () => {
211
- if (expired) {
212
- process.stderr.write(" share expired \u2014 tearing down.\n");
213
- await shutdown(0);
214
- }
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));
215
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 };
216
308
  }
217
- const code = await host.exited;
218
- await shutdown(code ?? 0);
219
- return code ?? 0;
309
+ return { ok: true, data: body };
220
310
  }
221
- function promiseTrue(pred) {
222
- return new Promise((resolve) => {
223
- const tick = () => {
224
- if (pred()) resolve();
225
- else setTimeout(tick, 250);
226
- };
227
- tick();
228
- });
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;
229
324
  }
230
- async function runStop() {
231
- process.stderr.write(
232
- "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)}`
233
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`);
234
373
  return 0;
235
374
  }
236
- async function runViewers() {
237
- process.stderr.write(
238
- "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"
239
- );
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}`);
240
388
  return 0;
241
389
  }
242
- async function main(argv = process.argv.slice(2)) {
243
- const parsed = parseArgs(argv);
244
- 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) {
245
404
  case "help":
246
- printHelp();
405
+ io.out(USAGE);
247
406
  return 0;
248
407
  case "version":
249
- process.stdout.write(`${VERSION}
250
- `);
408
+ io.out(`vibeshare ${VERSION}`);
251
409
  return 0;
252
- case "mcp": {
253
- const { runMcpStdio } = await import("./mcp.js");
254
- await runMcpStdio();
255
- return 0;
256
- }
257
- case "stop":
258
- return runStop();
410
+ case "start":
411
+ return startShare(command.options, io);
259
412
  case "viewers":
260
- return runViewers();
261
- case "host":
262
- return runHost(parsed);
263
- case "error":
264
- process.stderr.write(`vibeshare: ${parsed.message}
265
- `);
266
- printHelp(process.stderr);
267
- return 2;
413
+ return viewersCommand(command, io);
414
+ case "stop":
415
+ return stopCommand(command, io);
268
416
  }
269
417
  }
270
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
271
- 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
+ );
272
427
  }
273
428
  export {
274
- main,
275
- parseArgs,
276
- printHelp
429
+ CliUsageError,
430
+ parseArgv,
431
+ run
277
432
  };