vibeshare-live 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pooria Arab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # vibeshare
2
+
3
+ Share your live agent coding session by URL — traces.com-style, open source, CLI-first. Spectate read-only or invite into the vibelive multiplayer engine.
4
+
5
+ Part of the **Vibe Suite** — companion tools for agentic coding CLIs (Claude Code, Codex, Gemini, Grok/pi, Kimi). Ships as **CLI + npm package + MCP server**.
6
+
7
+ **Local-first: runs on your own machine, no data out** (consent model in `@pooriaarab/vibe-core`).
8
+
9
+ > Status: **v0** — the share/access layer over [`@pooriaarab/vibelive`](https://www.npmjs.com/package/@pooriaarab/vibelive) is implemented (CLI, library, MCP). Local/LAN, host-authoritative. E2e relay fan-out to ~1000 spectators is on the vibelive roadmap.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g vibeshare-live
15
+ # or use it inline:
16
+ npx vibeshare-live -- claude
17
+ ```
18
+
19
+ It pulls in `@pooriaarab/vibelive` (the multiplayer engine) and `@pooriaarab/vibe-core` (the consent ledger).
20
+
21
+ ## Quick start
22
+
23
+ Host your agent and hand someone a link:
24
+
25
+ ```bash
26
+ vibeshare --invite -- claude
27
+ ```
28
+
29
+ You'll get back something like:
30
+
31
+ ```
32
+ vibeshare — sharing claude
33
+ share: https://vibeshare.stream/s/a5e3a635-0bb7-4689-973c-5fe2d7de6207
34
+ access: invite · no expiry
35
+ relay: ws://localhost:55410
36
+ ● p2p · e2e · nothing stored on a server
37
+ ```
38
+
39
+ Send them the `share` URL. They spectate read-only; with `--invite` they can request to join and you `/approve <id>` to let them drive. Slash commands inside the host session: `/drive` `/release` `/viewers` `/approve <id>` `/stop`.
40
+
41
+ | Flag | Meaning |
42
+ | --- | --- |
43
+ | `--spectate` | link holders get read-only access (default) |
44
+ | `--invite` | link holders may request to join; you approve to let them drive |
45
+ | `--expire 1h\|24h` | auto-revoke the share after this duration |
46
+ | `--pass <p>` | require a passphrase on top of the share URL |
47
+
48
+ `vibeshare --version` · `vibeshare --help` · `vibeshare mcp`
49
+
50
+ ## Your own domain, not vibe.live
51
+
52
+ The share URL reads `vibeshare.stream/s/<id>` — **vibeshare's own domain** (available $4/yr), not `vibe.live`. The link is a capability URL: the id is the only thing that grants access (122 bits of entropy), so it's unguessable. Optional `--pass` adds a second factor.
53
+
54
+ The actual live stream runs over the local/LAN WebSocket relay that vibelive starts on your machine. The `vibeshare.stream` URL is the identity layer; the bytes flow over a self-hostable relay.
55
+
56
+ ## Peer-to-peer, end-to-end encrypted, nothing stored on a server
57
+
58
+ The session stream runs **peer-to-peer / via a dumb e2e-encrypted relay** that forwards opaque blobs. The relay cannot decrypt anything — nothing readable is ever stored on a server, and the relay is self-hostable. This is vibelive's transport (§1–2 of its tech spec); vibeshare is the URL/access layer on top. Fanning session output off your machine is gated behind the `share:session` consent scope.
59
+
60
+ ## As a library
61
+
62
+ ```ts
63
+ import { createHost, createRelay } from '@pooriaarab/vibelive';
64
+ import { createShare } from 'vibeshare-live';
65
+
66
+ const host = createHost({ command: ['claude'] });
67
+ const relay = await createRelay({ port: 0, hostHandle: host, initialDriver: 'host' });
68
+
69
+ const share = createShare({
70
+ session: relay,
71
+ access: 'spectate', // 'spectate' (read-only) | 'invite' (can join to drive)
72
+ expiry: '1h', // optional: '1h' | '24h' | number(ms)
73
+ passphrase: 'sekret', // optional second factor
74
+ });
75
+
76
+ console.log(share.url); // https://vibeshare.stream/s/<id>
77
+ const { viewers, pending } = share.viewers();
78
+ await share.revoke(); // tear down (also fires on expiry)
79
+ ```
80
+
81
+ Spectators are enforced read-only at the access gate: a spectator's control request never reaches vibelive's `WriteArbiter`, so they can never obtain the write token — the same "never two concurrent writers" invariant vibelive holds, extended to the share.
82
+
83
+ ## As an MCP server
84
+
85
+ Wire it into an agent that speaks MCP and it can offer "share this session?":
86
+
87
+ ```jsonc
88
+ // .mcp.json or your client's MCP config
89
+ {
90
+ "mcpServers": {
91
+ "vibeshare": { "command": "vibeshare", "args": ["mcp"] }
92
+ }
93
+ }
94
+ ```
95
+
96
+ Tools: `create_share` (host a command, mint a share URL) and `viewers` (list active shares + their audiences).
97
+
98
+ ## Prototype
99
+
100
+ Interactive, self-contained UX prototype (no build, no network): open [`docs/prototype.html`](docs/prototype.html) in a browser.
101
+
102
+ ## License
103
+
104
+ MIT.
@@ -0,0 +1,233 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/access.ts
8
+ var DEFAULT_ACCESS = "spectate";
9
+ function createAccessGate(options) {
10
+ const access = options.access ?? DEFAULT_ACCESS;
11
+ const arbiter = options.arbiter;
12
+ const passphrase = options.passphrase;
13
+ const roles = /* @__PURE__ */ new Map();
14
+ const hasPassphrase = typeof passphrase === "string" && passphrase.length > 0;
15
+ return {
16
+ access,
17
+ hasPassphrase,
18
+ admit({ id }, input) {
19
+ if (hasPassphrase && input !== passphrase) return false;
20
+ if (!roles.has(id)) roles.set(id, "spectator");
21
+ return true;
22
+ },
23
+ remove(id) {
24
+ roles.delete(id);
25
+ arbiter.leave(id);
26
+ },
27
+ has: (id) => roles.has(id),
28
+ role: (id) => roles.get(id),
29
+ promote(id) {
30
+ if (access !== "invite") return false;
31
+ if (!roles.has(id)) return false;
32
+ roles.set(id, "participant");
33
+ return true;
34
+ },
35
+ requestControl(id) {
36
+ const role = roles.get(id);
37
+ if (role === void 0) return { ok: false, reason: "unknown" };
38
+ if (role === "participant") {
39
+ const state = arbiter.requestControl(id);
40
+ return { ok: true, state };
41
+ }
42
+ return { ok: false, reason: access === "invite" ? "not-promoted" : "spectator" };
43
+ }
44
+ };
45
+ }
46
+
47
+ // src/url.ts
48
+ var SHARE_ORIGIN = "https://vibeshare.stream";
49
+ var SHARE_PATH_PREFIX = "/s/";
50
+ function newShareId() {
51
+ const uuid = globalThis.crypto?.randomUUID?.();
52
+ if (uuid) return uuid;
53
+ const bytes = new Uint8Array(16);
54
+ globalThis.crypto.getRandomValues(bytes);
55
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
56
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
57
+ }
58
+ function buildShareUrl(id, origin = SHARE_ORIGIN) {
59
+ return `${origin}${SHARE_PATH_PREFIX}${id}`;
60
+ }
61
+ var ShareUrlParseError = class extends Error {
62
+ constructor(message) {
63
+ super(message);
64
+ this.name = "ShareUrlParseError";
65
+ }
66
+ };
67
+ function parseShareUrl(url) {
68
+ if (typeof url !== "string" || url.length === 0) {
69
+ throw new ShareUrlParseError("share url must be a non-empty string");
70
+ }
71
+ const idx = url.indexOf(SHARE_PATH_PREFIX);
72
+ if (idx < 0) {
73
+ throw new ShareUrlParseError(`not a vibeshare share url (missing "${SHARE_PATH_PREFIX}"): ${url}`);
74
+ }
75
+ const tail = url.slice(idx + SHARE_PATH_PREFIX.length);
76
+ const end = tail.search(/[/?#]/);
77
+ const id = end < 0 ? tail : tail.slice(0, end);
78
+ if (id.length === 0) {
79
+ throw new ShareUrlParseError(`share url has an empty id: ${url}`);
80
+ }
81
+ return { id };
82
+ }
83
+
84
+ // src/share.ts
85
+ import { SHARE_SESSION_SCOPE as VIBELIVE_SHARE_SCOPE } from "vibelive-cli";
86
+ var SHARE_SESSION_SCOPE = VIBELIVE_SHARE_SCOPE;
87
+ function parseExpiry(spec) {
88
+ if (spec === void 0) return null;
89
+ if (typeof spec === "number") {
90
+ if (!Number.isFinite(spec) || spec <= 0) {
91
+ throw new RangeError(`expiry duration must be a positive finite number (ms), got: ${spec}`);
92
+ }
93
+ return Math.floor(spec);
94
+ }
95
+ switch (spec) {
96
+ case "1h":
97
+ return 60 * 60 * 1e3;
98
+ case "24h":
99
+ return 24 * 60 * 60 * 1e3;
100
+ default: {
101
+ const exhaustive = spec;
102
+ throw new RangeError(`unknown expiry preset: ${String(exhaustive)}`);
103
+ }
104
+ }
105
+ }
106
+ var ConsentError = class extends Error {
107
+ constructor() {
108
+ super(`consent for "${SHARE_SESSION_SCOPE}" is required to share a session \u2014 grant it first`);
109
+ this.name = "ConsentError";
110
+ }
111
+ };
112
+ var HOST_PARTICIPANT_NAME = "host";
113
+ function isLocalHostParticipant(p) {
114
+ return p.ws === void 0;
115
+ }
116
+ function participantName(p, fallback) {
117
+ return p.name && p.name.length > 0 ? p.name : fallback;
118
+ }
119
+ function createShare(options) {
120
+ const relay = options.session;
121
+ const consent = options.consent ?? relay.consent;
122
+ if (!consent.allows(SHARE_SESSION_SCOPE)) {
123
+ throw new ConsentError();
124
+ }
125
+ const id = newShareId();
126
+ const url = buildShareUrl(id);
127
+ const gate = createAccessGate({
128
+ arbiter: relay.arbiter,
129
+ access: options.access,
130
+ passphrase: options.passphrase
131
+ });
132
+ const firstSeen = /* @__PURE__ */ new Map();
133
+ const noteSeen = (pid) => {
134
+ let t = firstSeen.get(pid);
135
+ if (t === void 0) {
136
+ t = Date.now();
137
+ firstSeen.set(pid, t);
138
+ }
139
+ return t;
140
+ };
141
+ let revoked = false;
142
+ let revokeReason;
143
+ let expiryTimer;
144
+ const fireRevoke = (reason) => {
145
+ if (revoked) return Promise.resolve();
146
+ revoked = true;
147
+ revokeReason = reason;
148
+ if (expiryTimer !== void 0) {
149
+ clearTimeout(expiryTimer);
150
+ expiryTimer = void 0;
151
+ }
152
+ try {
153
+ options.onRevoke?.(reason);
154
+ } catch {
155
+ }
156
+ return relay.close().catch(() => {
157
+ });
158
+ };
159
+ const expiryMs = parseExpiry(options.expiry);
160
+ if (expiryMs !== null) {
161
+ expiryTimer = setTimeout(() => {
162
+ void fireRevoke("expired");
163
+ }, expiryMs);
164
+ expiryTimer.unref?.();
165
+ }
166
+ const toViewer = (p) => {
167
+ const pid = p.id;
168
+ if (!gate.has(pid)) {
169
+ gate.admit({ id: pid, name: participantName(p, pid) });
170
+ }
171
+ const role = gate.role(pid) ?? "spectator";
172
+ return {
173
+ id: pid,
174
+ name: participantName(p, pid),
175
+ role,
176
+ joinedAt: noteSeen(pid)
177
+ };
178
+ };
179
+ return {
180
+ id,
181
+ url,
182
+ access: gate.access,
183
+ gate,
184
+ relayUrl: relay.url,
185
+ get revoked() {
186
+ return revoked;
187
+ },
188
+ viewers() {
189
+ const connected = [];
190
+ const pending = [];
191
+ for (const p of relay.participants) {
192
+ if (isLocalHostParticipant(p)) continue;
193
+ const v = toViewer(p);
194
+ if (gate.access === "invite" && v.role !== "participant") {
195
+ pending.push(v);
196
+ } else {
197
+ connected.push(v);
198
+ }
199
+ }
200
+ return { viewers: connected, pending };
201
+ },
202
+ approve(viewerId) {
203
+ return gate.promote(viewerId);
204
+ },
205
+ removeViewer(viewerId) {
206
+ gate.remove(viewerId);
207
+ },
208
+ revoke() {
209
+ return fireRevoke(revokeReason === "expired" ? "expired" : "manual");
210
+ }
211
+ };
212
+ }
213
+
214
+ // src/version.ts
215
+ var VERSION = "0.1.0";
216
+
217
+ export {
218
+ __export,
219
+ DEFAULT_ACCESS,
220
+ createAccessGate,
221
+ SHARE_ORIGIN,
222
+ SHARE_PATH_PREFIX,
223
+ newShareId,
224
+ buildShareUrl,
225
+ ShareUrlParseError,
226
+ parseShareUrl,
227
+ SHARE_SESSION_SCOPE,
228
+ parseExpiry,
229
+ ConsentError,
230
+ HOST_PARTICIPANT_NAME,
231
+ createShare,
232
+ VERSION
233
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { A as AccessMode, E as ExpirySpec } from './share-CoX608sF.js';
3
+ import 'vibelive-cli';
4
+ import '@pooriaarab/vibe-core';
5
+
6
+ type ParsedCommand = {
7
+ readonly cmd: 'help';
8
+ } | {
9
+ readonly cmd: 'version';
10
+ } | {
11
+ readonly cmd: 'mcp';
12
+ } | {
13
+ readonly cmd: 'stop';
14
+ } | {
15
+ readonly cmd: 'viewers';
16
+ } | {
17
+ readonly cmd: 'host';
18
+ readonly command: readonly string[];
19
+ readonly access: AccessMode;
20
+ readonly expire?: ExpirySpec;
21
+ readonly pass?: string;
22
+ readonly name?: string;
23
+ } | {
24
+ readonly cmd: 'error';
25
+ readonly message: string;
26
+ };
27
+ /**
28
+ * Parse vibeshare argv (`process.argv.slice(2)`). Pure — no IO.
29
+ *
30
+ * Everything after a bare `--` is the opaque wrapped command and is never scanned
31
+ * for vibeshare flags, so `vibeshare -- claude --help` wraps `claude --help`
32
+ * rather than printing vibeshare's own help. Both `vibeshare -- <cmd>` and the
33
+ * explicit `vibeshare host -- <cmd>` forms are accepted.
34
+ */
35
+ declare function parseArgs(argv: readonly string[]): ParsedCommand;
36
+ declare function printHelp(stream?: {
37
+ write(s: string): boolean;
38
+ }): void;
39
+ /** CLI entrypoint. Returns the desired exit code (does not call exit itself). */
40
+ declare function main(argv?: readonly string[]): Promise<number>;
41
+
42
+ export { main, parseArgs, printHelp };
package/dist/cli.js ADDED
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ VERSION,
4
+ createShare
5
+ } from "./chunk-5OLI3WTU.js";
6
+
7
+ // 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}`);
33
+ }
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.
77
+
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
83
+
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
89
+
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;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+ async function runHost(p) {
108
+ const host = createHost({ command: p.command });
109
+ const relay = await createRelay({
110
+ 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
+ `);
140
+ }
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
+ `);
157
+ }
158
+ for (const v of roster.pending) {
159
+ process.stderr.write(` \u2026 ${v.name} [wants to join \u2014 /approve ${v.id}]
160
+ `);
161
+ }
162
+ };
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;
189
+ }
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");
195
+ }
196
+ });
197
+ rl.on("SIGINT", () => void shutdown(0));
198
+ 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
+ }
215
+ });
216
+ }
217
+ const code = await host.exited;
218
+ await shutdown(code ?? 0);
219
+ return code ?? 0;
220
+ }
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
+ });
229
+ }
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"
233
+ );
234
+ return 0;
235
+ }
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
+ );
240
+ return 0;
241
+ }
242
+ async function main(argv = process.argv.slice(2)) {
243
+ const parsed = parseArgs(argv);
244
+ switch (parsed.cmd) {
245
+ case "help":
246
+ printHelp();
247
+ return 0;
248
+ case "version":
249
+ process.stdout.write(`${VERSION}
250
+ `);
251
+ 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();
259
+ 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;
268
+ }
269
+ }
270
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
271
+ void main().then((code) => process.exit(code));
272
+ }
273
+ export {
274
+ main,
275
+ parseArgs,
276
+ printHelp
277
+ };