@rigxyz/tapd 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/dist/bin.js ADDED
@@ -0,0 +1,886 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AlreadyInitializedError,
4
+ AlreadyJoinedError,
5
+ InvalidInviteUrlError,
6
+ NotInitializedError,
7
+ RelayClient,
8
+ STATE_KEY_CONFLICT_COUNT,
9
+ bindingConfigFile,
10
+ computeLocalDiff,
11
+ findConflictSidecars,
12
+ init,
13
+ join,
14
+ list,
15
+ loadIgnore,
16
+ mint,
17
+ openStateDb,
18
+ readBindingConfig,
19
+ revoke,
20
+ start
21
+ } from "./chunk-RQC73B5Y.js";
22
+
23
+ // src/bin.ts
24
+ import { join as pathJoin, resolve } from "path";
25
+ import kleur from "kleur";
26
+
27
+ // src/pidfile.ts
28
+ import {
29
+ existsSync,
30
+ mkdirSync,
31
+ readFileSync,
32
+ unlinkSync,
33
+ writeFileSync
34
+ } from "fs";
35
+ import { dirname } from "path";
36
+ var PidfileLockError = class extends Error {
37
+ constructor(pidfilePath, pid) {
38
+ super(`pidfile ${pidfilePath} held by live process ${pid}`);
39
+ this.pidfilePath = pidfilePath;
40
+ this.pid = pid;
41
+ this.name = "PidfileLockError";
42
+ }
43
+ pidfilePath;
44
+ pid;
45
+ };
46
+ function isProcessAlive(pid) {
47
+ try {
48
+ process.kill(pid, 0);
49
+ return true;
50
+ } catch (err) {
51
+ const code = err.code;
52
+ return code === "EPERM";
53
+ }
54
+ }
55
+ function readLivePid(path) {
56
+ if (!existsSync(path)) return null;
57
+ let raw;
58
+ try {
59
+ raw = readFileSync(path, "utf8").trim();
60
+ } catch {
61
+ return null;
62
+ }
63
+ const pid = Number.parseInt(raw, 10);
64
+ if (!Number.isInteger(pid) || pid <= 0) return null;
65
+ return isProcessAlive(pid) ? pid : null;
66
+ }
67
+ function acquire(path) {
68
+ const livePid = readLivePid(path);
69
+ if (livePid !== null) {
70
+ throw new PidfileLockError(path, livePid);
71
+ }
72
+ mkdirSync(dirname(path), { recursive: true });
73
+ writeFileSync(path, String(process.pid), { mode: 384 });
74
+ }
75
+ function release(path) {
76
+ if (!existsSync(path)) return;
77
+ let raw;
78
+ try {
79
+ raw = readFileSync(path, "utf8").trim();
80
+ } catch {
81
+ return;
82
+ }
83
+ const pid = Number.parseInt(raw, 10);
84
+ if (pid !== process.pid) return;
85
+ try {
86
+ unlinkSync(path);
87
+ } catch {
88
+ }
89
+ }
90
+
91
+ // src/status.ts
92
+ import { existsSync as existsSync2 } from "fs";
93
+ import { join as join2 } from "path";
94
+
95
+ // src/version.ts
96
+ var TAPD_VERSION = "0.1.0";
97
+
98
+ // src/status.ts
99
+ var STATE_KEY_LAST_APPLY_ERROR = "last_apply_error";
100
+ var STATE_KEY_LAST_APPLY_ERROR_AT = "last_apply_error_at";
101
+ var DAEMON_PIDFILE_REL = ".rig/tap/daemon.pid";
102
+ var PENDING_PROBE_LIMIT = 100;
103
+ var STATUS_PROTOCOL_VERSION = 1;
104
+ function statusToJson(report) {
105
+ return {
106
+ protocolVersion: STATUS_PROTOCOL_VERSION,
107
+ version: TAPD_VERSION,
108
+ bindingId: report.config.bindingId,
109
+ relayUrl: report.config.relayUrl,
110
+ deviceId: report.config.deviceId,
111
+ daemon: report.daemon,
112
+ cursor: report.cursor,
113
+ remoteCursor: report.remoteCursor,
114
+ trackedPaths: report.trackedPaths,
115
+ pendingApplies: report.pendingApplies,
116
+ pendingUploads: report.pendingUploads,
117
+ conflicts: report.conflicts,
118
+ lastApplyError: report.lastApplyError,
119
+ offline: report.offline
120
+ };
121
+ }
122
+ async function status(opts) {
123
+ const config = readBindingConfig(opts.rootDir);
124
+ if (!config) {
125
+ throw new NotInitializedError(opts.rootDir);
126
+ }
127
+ const db = openStateDb(join2(opts.rootDir, ".rig", "tap", "state.local.db"));
128
+ try {
129
+ const cursor = db.getCursor();
130
+ const tracked = db.listPaths().length;
131
+ const conflictCount = Number.parseInt(
132
+ db.getMeta(STATE_KEY_CONFLICT_COUNT) ?? "0",
133
+ 10
134
+ );
135
+ const lastErrJson = db.getMeta(STATE_KEY_LAST_APPLY_ERROR);
136
+ const lastErrAt = db.getMeta(STATE_KEY_LAST_APPLY_ERROR_AT);
137
+ const lastApplyError = lastErrJson ? safeParseLastError(lastErrJson, lastErrAt) : null;
138
+ const pidfilePath = join2(opts.rootDir, DAEMON_PIDFILE_REL);
139
+ const livePid = readLivePid(pidfilePath);
140
+ const daemon = livePid !== null ? { running: true, pid: livePid } : { running: false };
141
+ const client = new RelayClient({
142
+ baseUrl: config.relayUrl,
143
+ bindingId: config.bindingId,
144
+ token: config.token,
145
+ fetch: opts.fetch
146
+ });
147
+ let remoteEvents = [];
148
+ let offline = false;
149
+ try {
150
+ const probe = await client.listChanges({
151
+ after: cursor,
152
+ limit: PENDING_PROBE_LIMIT
153
+ });
154
+ remoteEvents = probe.events;
155
+ } catch {
156
+ offline = true;
157
+ }
158
+ const peekLimit = opts.peekLimit ?? 5;
159
+ const pendingPeek = remoteEvents.slice(0, peekLimit);
160
+ const pendingApplies = remoteEvents.length;
161
+ const remoteCursor = remoteEvents.length > 0 ? remoteEvents[remoteEvents.length - 1].id : cursor;
162
+ let pendingUploads = 0;
163
+ if (existsSync2(opts.rootDir)) {
164
+ const ignore = loadIgnore(opts.rootDir);
165
+ const diff = await computeLocalDiff({ rootDir: opts.rootDir, stateDb: db, ignore });
166
+ pendingUploads = diff.writes.length + diff.deletes.length + diff.mkdirs.length + diff.rmdirs.length;
167
+ }
168
+ const conflicts = findConflictSidecars(opts.rootDir);
169
+ return {
170
+ config,
171
+ cursor,
172
+ remoteCursor,
173
+ trackedPaths: tracked,
174
+ pendingPeek,
175
+ pendingApplies,
176
+ pendingUploads,
177
+ conflicts,
178
+ conflictCount,
179
+ lastApplyError,
180
+ daemon,
181
+ offline
182
+ };
183
+ } finally {
184
+ db.close();
185
+ }
186
+ }
187
+ function safeParseLastError(json, at) {
188
+ try {
189
+ const parsed = JSON.parse(json);
190
+ if (typeof parsed.eventId !== "string" || typeof parsed.path !== "string" || typeof parsed.op !== "string" || typeof parsed.reason !== "string") {
191
+ return null;
192
+ }
193
+ return {
194
+ eventId: parsed.eventId,
195
+ path: parsed.path,
196
+ op: parsed.op,
197
+ reason: parsed.reason,
198
+ at: at ?? ""
199
+ };
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ // src/uninit.ts
206
+ import {
207
+ existsSync as existsSync3,
208
+ rmSync,
209
+ rmdirSync,
210
+ statSync,
211
+ unlinkSync as unlinkSync2
212
+ } from "fs";
213
+ import { join as join3 } from "path";
214
+ var DAEMON_PIDFILE_REL2 = ".rig/tap/daemon.pid";
215
+ var STATE_DIR_REL = ".rig/tap";
216
+ var DaemonStillRunningError = class extends Error {
217
+ constructor(pid) {
218
+ super(`daemon still running (pid ${pid}); stop it first`);
219
+ this.pid = pid;
220
+ this.name = "DaemonStillRunningError";
221
+ }
222
+ pid;
223
+ };
224
+ function uninit(opts) {
225
+ const config = readBindingConfig(opts.rootDir);
226
+ if (!config) {
227
+ throw new NotInitializedError(opts.rootDir);
228
+ }
229
+ const pidfilePath = join3(opts.rootDir, DAEMON_PIDFILE_REL2);
230
+ const livePid = readLivePid(pidfilePath);
231
+ if (livePid !== null) {
232
+ throw new DaemonStillRunningError(livePid);
233
+ }
234
+ const stateDbPath = join3(opts.rootDir, ".rig", "tap", "state.local.db");
235
+ const tracked = [];
236
+ if (existsSync3(stateDbPath)) {
237
+ const db = openStateDb(stateDbPath);
238
+ try {
239
+ for (const p of db.listPaths()) {
240
+ const entry = db.getPath(p);
241
+ if (!entry) continue;
242
+ tracked.push({ path: p, isDir: entry.lastSeenHash === null });
243
+ }
244
+ } finally {
245
+ db.close();
246
+ }
247
+ }
248
+ let filesDeleted = 0;
249
+ let dirsRemoved = 0;
250
+ let alreadyGone = 0;
251
+ const skippedNonEmptyDirs = [];
252
+ if (opts.purge) {
253
+ const files = tracked.filter((t) => !t.isDir);
254
+ const dirs = tracked.filter((t) => t.isDir).sort((a, b) => b.path.split("/").length - a.path.split("/").length);
255
+ for (const f of files) {
256
+ const abs = join3(opts.rootDir, f.path);
257
+ try {
258
+ const st = statSync(abs);
259
+ if (st.isFile()) {
260
+ unlinkSync2(abs);
261
+ filesDeleted += 1;
262
+ } else {
263
+ skippedNonEmptyDirs.push(f.path);
264
+ }
265
+ } catch (err) {
266
+ if (err.code === "ENOENT") {
267
+ alreadyGone += 1;
268
+ } else {
269
+ throw err;
270
+ }
271
+ }
272
+ }
273
+ for (const d of dirs) {
274
+ const abs = join3(opts.rootDir, d.path);
275
+ try {
276
+ rmdirSync(abs);
277
+ dirsRemoved += 1;
278
+ } catch (err) {
279
+ const code = err.code;
280
+ if (code === "ENOENT") {
281
+ alreadyGone += 1;
282
+ } else if (code === "ENOTEMPTY" || code === "EEXIST") {
283
+ skippedNonEmptyDirs.push(d.path);
284
+ } else {
285
+ throw err;
286
+ }
287
+ }
288
+ }
289
+ }
290
+ const cfgPath = bindingConfigFile(opts.rootDir);
291
+ if (existsSync3(cfgPath)) {
292
+ try {
293
+ unlinkSync2(cfgPath);
294
+ } catch (err) {
295
+ if (err.code !== "ENOENT") throw err;
296
+ }
297
+ }
298
+ const stateDir = join3(opts.rootDir, STATE_DIR_REL);
299
+ if (existsSync3(stateDir)) {
300
+ rmSync(stateDir, { recursive: true, force: true });
301
+ }
302
+ return {
303
+ bindingId: config.bindingId,
304
+ trackedPaths: tracked.length,
305
+ filesDeleted,
306
+ dirsRemoved,
307
+ alreadyGone,
308
+ skippedNonEmptyDirs
309
+ };
310
+ }
311
+
312
+ // src/bin.ts
313
+ var DAEMON_PIDFILE_REL3 = ".rig/tap/daemon.pid";
314
+ var PROTOCOL_VERSION = 1;
315
+ function printJson(payload) {
316
+ process.stdout.write(JSON.stringify(payload) + "\n");
317
+ }
318
+ function printJsonError(code, message) {
319
+ printJson({ protocolVersion: PROTOCOL_VERSION, error: { code, message } });
320
+ }
321
+ function help() {
322
+ const C = kleur;
323
+ console.log(`${C.bold().cyan("tapd")} ${C.dim("\u2014 local daemon for hosted Tap sync")}
324
+
325
+ ${C.bold().cyan("COMMANDS")}
326
+ ${C.green("tapd init")} bootstrap a binding in the current directory
327
+ ${C.green("tapd invite")} mint / list / revoke invite URLs (owner only)
328
+ ${C.green("tapd join")} accept an invite URL into the current directory
329
+ ${C.green("tapd start")} run the watch + apply daemon (foreground)
330
+ ${C.green("tapd status")} show binding + cursor + pending remote events
331
+ ${C.green("tapd uninit")} disconnect this checkout (optionally --purge tracked files)
332
+ ${C.green("tapd help")} this message
333
+
334
+ ${C.dim("--version / -v")} print the tapd binary version on stdout and exit.
335
+ ${C.dim("--json")} on init / invite / join / status / uninit emits a versioned
336
+ JSON payload to stdout (protocolVersion: 1) for programmatic consumers (rig).
337
+
338
+ ${C.bold().cyan("tapd init OPTIONS")}
339
+ ${C.dim("--name <n>")} binding display name (required)
340
+ ${C.dim("--owner-user-id <u>")} placeholder user id (Phase 3 sources from Clerk)
341
+ ${C.dim("--relay <url>")} default http://127.0.0.1:4030
342
+ ${C.dim("--device-label <l>")} default 'owner'
343
+ ${C.dim("--dir <path>")} default cwd
344
+
345
+ ${C.bold().cyan("tapd join OPTIONS")}
346
+ ${C.dim("<invite-url>")} positional \u2014 full URL from \`rig invite\` /
347
+ relay (.../v1/invites/<secret>/accept)
348
+ ${C.dim("--user-id <u>")} Phase 3 placeholder until Clerk
349
+ ${C.dim("--email <e>")} optional; required if the invite has
350
+ an emailConstraint
351
+ ${C.dim("--device-label <l>")} default 'device'
352
+ ${C.dim("--dir <path>")} default cwd
353
+
354
+ ${C.bold().cyan("tapd invite SUBCOMMANDS")}
355
+ ${C.green("tapd invite create")} mint a new invite + print the URL once
356
+ ${C.dim("--ops <read,write,subscribe>")} default read,write,subscribe
357
+ ${C.dim("--role <owner|editor|viewer>")} omit for pure-capability collab
358
+ ${C.dim("--path-glob <g>")} repeatable; restrict by path
359
+ ${C.dim("--ttl <duration>")} e.g. 24h, 7d, 30m (omit for never)
360
+ ${C.dim("--max-uses <n>")} default unlimited; 1 = single-use
361
+ ${C.dim("--email <e>")} require accepter to claim this email
362
+ ${C.dim("--label <l>")} show in list output
363
+ ${C.dim("--dir <path>")} default cwd
364
+ ${C.green("tapd invite list")} show outstanding invites + status
365
+ ${C.green("tapd invite revoke <id>")} revoke an invite AND its issued tokens
366
+
367
+ ${C.bold().cyan("tapd start OPTIONS")}
368
+ ${C.dim("--dir <path>")} default cwd
369
+ ${C.dim("--poll-seconds <n>")} default 3
370
+ ${C.dim("--no-pidfile")} don't write .rig/tap/daemon.pid (default: write)
371
+ `);
372
+ }
373
+ function parseTtl(raw) {
374
+ if (!raw) return void 0;
375
+ const m = /^(\d+)([smhd])?$/.exec(raw.trim());
376
+ if (!m) {
377
+ console.error(kleur.red("error:"), `invalid --ttl: ${raw} (expected e.g. 30m, 12h, 7d)`);
378
+ process.exit(2);
379
+ }
380
+ const n = Number(m[1]);
381
+ const unit = m[2] ?? "s";
382
+ const factor = unit === "d" ? 86400 : unit === "h" ? 3600 : unit === "m" ? 60 : 1;
383
+ return n * factor;
384
+ }
385
+ var VALID_OPS = ["read", "write", "subscribe"];
386
+ var VALID_ROLES = ["owner", "editor", "viewer"];
387
+ function parseOps(raw) {
388
+ if (!raw) return ["read", "write", "subscribe"];
389
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
390
+ for (const p of parts) {
391
+ if (!VALID_OPS.includes(p)) {
392
+ console.error(kleur.red("error:"), `invalid --ops value '${p}' (allowed: ${VALID_OPS.join(",")})`);
393
+ process.exit(2);
394
+ }
395
+ }
396
+ return parts;
397
+ }
398
+ function parseRole(raw) {
399
+ if (raw === void 0) return void 0;
400
+ if (!VALID_ROLES.includes(raw)) {
401
+ console.error(kleur.red("error:"), `invalid --role '${raw}' (allowed: ${VALID_ROLES.join("|")})`);
402
+ process.exit(2);
403
+ }
404
+ return raw;
405
+ }
406
+ function collectPathGlobs(flags2) {
407
+ const raw = flags2.get("path-glob");
408
+ if (typeof raw !== "string") return void 0;
409
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
410
+ return parts.length > 0 ? parts : void 0;
411
+ }
412
+ async function runInvite(positional2, flags2) {
413
+ const json = flags2.get("json") === true;
414
+ const sub = positional2[0];
415
+ const rootDir = resolve(flags2.get("dir") ?? process.cwd());
416
+ try {
417
+ if (sub === void 0 || sub === "create") {
418
+ const ops = parseOps(flags2.get("ops"));
419
+ const role = parseRole(flags2.get("role"));
420
+ const pathGlobs = collectPathGlobs(flags2);
421
+ const ttlSeconds = parseTtl(flags2.get("ttl"));
422
+ const maxUsesRaw = flags2.get("max-uses");
423
+ const maxUses = typeof maxUsesRaw === "string" ? Number(maxUsesRaw) : void 0;
424
+ if (maxUses !== void 0 && (!Number.isInteger(maxUses) || maxUses < 1)) {
425
+ if (json) printJsonError("invalid_flag", "--max-uses must be an integer \u2265 1");
426
+ else console.error(kleur.red("error:"), "--max-uses must be an integer \u2265 1");
427
+ process.exit(2);
428
+ }
429
+ const emailConstraint = flags2.get("email");
430
+ const label = flags2.get("label");
431
+ const result = await mint({
432
+ rootDir,
433
+ ops,
434
+ role: role ?? null,
435
+ pathGlobs,
436
+ ttlSeconds,
437
+ maxUses,
438
+ emailConstraint,
439
+ label
440
+ });
441
+ if (json) {
442
+ printJson({
443
+ protocolVersion: PROTOCOL_VERSION,
444
+ invite: result.invite,
445
+ url: result.url
446
+ });
447
+ return;
448
+ }
449
+ const C = kleur;
450
+ console.log(C.bold().cyan("tapd invite create"));
451
+ console.log(` id: ${C.bold(result.invite.id)}`);
452
+ console.log(` ops: ${result.invite.ops.join(", ")}`);
453
+ console.log(` role: ${result.invite.role ?? C.dim("pure-capability")}`);
454
+ if (result.invite.pathGlobs.length > 0) {
455
+ console.log(` scope: ${result.invite.pathGlobs.join(" | ")}`);
456
+ }
457
+ if (result.invite.expiresAt) {
458
+ console.log(` expires: ${result.invite.expiresAt}`);
459
+ } else {
460
+ console.log(` expires: ${C.dim("never")}`);
461
+ }
462
+ console.log(
463
+ ` uses: ${C.dim(`${result.invite.useCount}/${result.invite.maxUses ?? "\u221E"}`)}`
464
+ );
465
+ if (result.invite.emailConstraint) {
466
+ console.log(` email: ${result.invite.emailConstraint}`);
467
+ }
468
+ if (result.invite.label) {
469
+ console.log(` label: ${result.invite.label}`);
470
+ }
471
+ console.log("");
472
+ console.log(C.bold(" Invite URL (share once):"));
473
+ console.log(` ${C.green(result.url)}`);
474
+ console.log(C.dim(" Recipients run: tapd join <url> --user-id <u>"));
475
+ return;
476
+ }
477
+ if (sub === "list") {
478
+ const invites = await list({ rootDir });
479
+ if (json) {
480
+ printJson({ protocolVersion: PROTOCOL_VERSION, invites });
481
+ return;
482
+ }
483
+ const C = kleur;
484
+ if (invites.length === 0) {
485
+ console.log(C.dim("no invites"));
486
+ return;
487
+ }
488
+ console.log(C.bold().cyan("tapd invite list"));
489
+ for (const i of invites) {
490
+ const status2 = i.revokedAt ? C.red("revoked") : i.expiresAt && new Date(i.expiresAt).getTime() < Date.now() ? C.dim("expired") : i.maxUses !== null && i.useCount >= i.maxUses ? C.dim("exhausted") : C.green("active");
491
+ const roleLabel = i.role ?? "pure-cap";
492
+ const useFrac = `${i.useCount}/${i.maxUses ?? "\u221E"}`;
493
+ const labelSuffix = i.label ? ` "${i.label}"` : "";
494
+ console.log(
495
+ ` ${C.bold(i.id)} ${status2} ${roleLabel.padEnd(8)} uses ${useFrac}${labelSuffix}`
496
+ );
497
+ }
498
+ return;
499
+ }
500
+ if (sub === "revoke") {
501
+ const inviteId = positional2[1];
502
+ if (!inviteId) {
503
+ if (json) printJsonError("missing_argument", "usage: tapd invite revoke <inviteId>");
504
+ else console.error(kleur.red("error:"), "usage: tapd invite revoke <inviteId>");
505
+ process.exit(2);
506
+ }
507
+ const result = await revoke({ rootDir, inviteId });
508
+ if (json) {
509
+ printJson({
510
+ protocolVersion: PROTOCOL_VERSION,
511
+ inviteId,
512
+ alreadyRevoked: result.alreadyRevoked
513
+ });
514
+ return;
515
+ }
516
+ const C = kleur;
517
+ if (result.alreadyRevoked) {
518
+ console.log(`${C.dim("\u2713")} ${inviteId} ${C.dim("(was already revoked)")}`);
519
+ } else {
520
+ console.log(`${C.green("\u2713")} revoked ${C.bold(inviteId)} (issued tokens revoked too)`);
521
+ }
522
+ return;
523
+ }
524
+ if (json) printJsonError("unknown_subcommand", `unknown subcommand: tapd invite ${sub}`);
525
+ else {
526
+ console.error(kleur.red("error:"), `unknown subcommand: tapd invite ${sub}`);
527
+ console.error(" see `tapd help`");
528
+ }
529
+ process.exit(2);
530
+ } catch (err) {
531
+ if (err instanceof NotInitializedError) {
532
+ if (json) printJsonError("not_initialized", err.message);
533
+ else console.error(kleur.red("error:"), err.message);
534
+ process.exit(1);
535
+ }
536
+ throw err;
537
+ }
538
+ }
539
+ async function runJoin(positional2, flags2) {
540
+ const json = flags2.get("json") === true;
541
+ const inviteUrl = positional2[0];
542
+ if (!inviteUrl) {
543
+ if (json) printJsonError("missing_argument", "missing invite URL");
544
+ else {
545
+ console.error(kleur.red("error:"), "missing invite URL");
546
+ console.error(" usage: tapd join <invite-url> --user-id <u>");
547
+ }
548
+ process.exit(2);
549
+ }
550
+ const userId = flags2.get("user-id");
551
+ if (!userId) {
552
+ if (json) printJsonError("missing_flag", "--user-id is required (placeholder until Clerk)");
553
+ else console.error(kleur.red("error:"), "--user-id is required (Phase 3 placeholder until Clerk)");
554
+ process.exit(2);
555
+ }
556
+ const rootDir = resolve(flags2.get("dir") ?? process.cwd());
557
+ const email = flags2.get("email");
558
+ const deviceLabel = flags2.get("device-label");
559
+ let result;
560
+ try {
561
+ result = await join({ rootDir, inviteUrl, userId, email, deviceLabel });
562
+ } catch (err) {
563
+ if (err instanceof AlreadyJoinedError) {
564
+ if (json) printJsonError("already_joined", err.message);
565
+ else {
566
+ console.error(kleur.red("error:"), err.message);
567
+ console.error(kleur.dim(" to sync this rig, run `tapd start`."));
568
+ }
569
+ process.exit(1);
570
+ }
571
+ if (err instanceof InvalidInviteUrlError) {
572
+ if (json) printJsonError("invalid_invite_url", err.message);
573
+ else console.error(kleur.red("error:"), err.message);
574
+ process.exit(2);
575
+ }
576
+ throw err;
577
+ }
578
+ if (json) {
579
+ printJson({
580
+ protocolVersion: PROTOCOL_VERSION,
581
+ bindingId: result.bindingId,
582
+ device: { id: result.device.id, label: result.device.label },
583
+ tokenSecret: result.tokenSecret,
584
+ becameMember: result.becameMember
585
+ });
586
+ return;
587
+ }
588
+ const C = kleur;
589
+ console.log(C.bold().cyan("tapd join"));
590
+ console.log(` binding: ${C.bold(result.bindingId)}`);
591
+ console.log(` device: ${C.bold(result.device.id)} ${C.dim(result.device.label)}`);
592
+ console.log(` role: ${result.becameMember ? C.green("member") : C.dim("pure-capability")}`);
593
+ console.log("");
594
+ console.log(C.bold(" Capability token (shown once):"));
595
+ console.log(` ${C.green(result.tokenSecret)}`);
596
+ console.log(C.dim(` Stored in ${rootDir}/.rig/tap-binding.local.json (mode 0600).`));
597
+ console.log("");
598
+ console.log(C.dim(" Next: `tapd start` (initial apply pulls the remote manifest)."));
599
+ }
600
+ async function runStatus(flags2) {
601
+ const json = flags2.get("json") === true;
602
+ const rootDir = resolve(flags2.get("dir") ?? process.cwd());
603
+ let report;
604
+ try {
605
+ report = await status({ rootDir });
606
+ } catch (err) {
607
+ if (err instanceof NotInitializedError) {
608
+ if (json) printJsonError("not_initialized", err.message);
609
+ else console.error(kleur.red("error:"), err.message);
610
+ process.exit(1);
611
+ }
612
+ if (json) printJsonError("status_failed", err.message);
613
+ else console.error(kleur.red("error:"), err.message);
614
+ process.exit(1);
615
+ }
616
+ if (json) {
617
+ printJson(statusToJson(report));
618
+ return;
619
+ }
620
+ const C = kleur;
621
+ console.log(C.bold().cyan("tapd status"));
622
+ console.log(` binding: ${C.bold(report.config.bindingId)}`);
623
+ console.log(` relay: ${report.config.relayUrl}`);
624
+ console.log(` device: ${C.bold(report.config.deviceId)}`);
625
+ console.log(` cursor: ${report.cursor}`);
626
+ console.log(` tracked: ${report.trackedPaths} paths`);
627
+ if (report.pendingPeek.length === 0) {
628
+ console.log(` pending: ${C.green("up to date")}`);
629
+ } else {
630
+ console.log(` pending: ${C.yellow(`${report.pendingPeek.length}+ events behind`)}`);
631
+ for (const ev of report.pendingPeek) {
632
+ console.log(C.dim(` ${ev.op.padEnd(6)} ${ev.path} (${ev.id})`));
633
+ }
634
+ }
635
+ if (report.conflictCount > 0) {
636
+ console.log(
637
+ ` conflicts: ${C.yellow(String(report.conflictCount))} ${C.dim("(sidecars at *.conflict-from.*)")}`
638
+ );
639
+ } else {
640
+ console.log(` conflicts: ${C.dim("none")}`);
641
+ }
642
+ if (report.lastApplyError) {
643
+ const e = report.lastApplyError;
644
+ console.log(
645
+ ` last err: ${C.red(e.reason)} ${C.dim(`(${e.op} ${e.path} @ ${e.eventId}${e.at ? `, ${e.at}` : ""})`)}`
646
+ );
647
+ }
648
+ }
649
+ async function runUninit(flags2) {
650
+ const json = flags2.get("json") === true;
651
+ const purge = flags2.get("purge") === true;
652
+ const rootDir = resolve(flags2.get("dir") ?? process.cwd());
653
+ let result;
654
+ try {
655
+ result = uninit({ rootDir, purge });
656
+ } catch (err) {
657
+ if (err instanceof DaemonStillRunningError) {
658
+ if (json) printJsonError("daemon_still_running", err.message);
659
+ else {
660
+ console.error(kleur.red("error:"), err.message);
661
+ console.error(kleur.dim(" stop it with `kill <pid>` or send SIGTERM, then re-run."));
662
+ }
663
+ process.exit(1);
664
+ }
665
+ if (err instanceof NotInitializedError) {
666
+ if (json) printJsonError("not_initialized", err.message);
667
+ else console.error(kleur.red("error:"), err.message);
668
+ process.exit(1);
669
+ }
670
+ throw err;
671
+ }
672
+ if (json) {
673
+ printJson({
674
+ protocolVersion: PROTOCOL_VERSION,
675
+ bindingId: result.bindingId,
676
+ purged: purge,
677
+ trackedPaths: result.trackedPaths,
678
+ filesDeleted: result.filesDeleted,
679
+ dirsRemoved: result.dirsRemoved,
680
+ alreadyGone: result.alreadyGone,
681
+ skippedNonEmptyDirs: result.skippedNonEmptyDirs
682
+ });
683
+ return;
684
+ }
685
+ const C = kleur;
686
+ console.log(C.bold().cyan("tapd uninit"));
687
+ console.log(` binding: ${C.bold(result.bindingId)} ${C.green("disconnected")}`);
688
+ if (purge) {
689
+ console.log(` purge: ${result.filesDeleted} files + ${result.dirsRemoved} dirs removed`);
690
+ if (result.alreadyGone > 0) {
691
+ console.log(` skipped: ${C.dim(`${result.alreadyGone} already gone`)}`);
692
+ }
693
+ if (result.skippedNonEmptyDirs.length > 0) {
694
+ console.log(
695
+ ` kept: ${C.yellow(`${result.skippedNonEmptyDirs.length} non-empty dirs (untracked content inside)`)}`
696
+ );
697
+ for (const d of result.skippedNonEmptyDirs.slice(0, 5)) {
698
+ console.log(C.dim(` ${d}`));
699
+ }
700
+ if (result.skippedNonEmptyDirs.length > 5) {
701
+ console.log(C.dim(` \u2026 and ${result.skippedNonEmptyDirs.length - 5} more`));
702
+ }
703
+ }
704
+ } else {
705
+ console.log(C.dim(` ${result.trackedPaths} tracked paths left in place (pass --purge to remove)`));
706
+ }
707
+ }
708
+ async function runStart(flags2) {
709
+ const rootDir = resolve(flags2.get("dir") ?? process.cwd());
710
+ const pollSeconds = flags2.has("poll-seconds") ? Number(flags2.get("poll-seconds")) : void 0;
711
+ const pidfilePath = flags2.get("no-pidfile") === true ? null : pathJoin(rootDir, DAEMON_PIDFILE_REL3);
712
+ let handle = null;
713
+ let stopping = false;
714
+ const cleanup = async () => {
715
+ if (stopping) return;
716
+ stopping = true;
717
+ console.log("\nstopping\u2026");
718
+ try {
719
+ if (handle) await handle.stop();
720
+ } finally {
721
+ if (pidfilePath) release(pidfilePath);
722
+ }
723
+ process.exit(0);
724
+ };
725
+ process.on("SIGINT", cleanup);
726
+ process.on("SIGTERM", cleanup);
727
+ if (pidfilePath) {
728
+ try {
729
+ acquire(pidfilePath);
730
+ } catch (err) {
731
+ if (err instanceof PidfileLockError) {
732
+ console.error(kleur.red("error:"), err.message);
733
+ console.error(kleur.dim(` another \`tapd start\` is already running (pid ${err.pid}).`));
734
+ console.error(kleur.dim(" stop it with `kill <pid>` or pass --no-pidfile to override."));
735
+ process.exit(1);
736
+ }
737
+ throw err;
738
+ }
739
+ }
740
+ try {
741
+ handle = await start({ rootDir, pollSeconds });
742
+ } catch (err) {
743
+ if (pidfilePath) release(pidfilePath);
744
+ console.error(kleur.red("error:"), err.message);
745
+ process.exit(1);
746
+ }
747
+ console.log(kleur.bold().cyan("tapd start"));
748
+ console.log(` dir: ${rootDir}`);
749
+ console.log(` poll: ${pollSeconds ?? 3}s`);
750
+ if (pidfilePath) console.log(` pid: ${process.pid} ${kleur.dim(`(${pidfilePath})`)}`);
751
+ console.log(kleur.dim(" press ctrl+c to stop"));
752
+ }
753
+ async function runInit(flags2) {
754
+ const json = flags2.get("json") === true;
755
+ const name = flags2.get("name");
756
+ const ownerUserId = flags2.get("owner-user-id");
757
+ if (!name) {
758
+ if (json) printJsonError("missing_flag", "--name is required");
759
+ else console.error(kleur.red("error:"), "--name is required");
760
+ process.exit(2);
761
+ }
762
+ if (!ownerUserId) {
763
+ if (json) printJsonError("missing_flag", "--owner-user-id is required (placeholder until Clerk)");
764
+ else console.error(kleur.red("error:"), "--owner-user-id is required (placeholder until Phase 3 / Clerk)");
765
+ process.exit(2);
766
+ }
767
+ const rootDir = resolve(flags2.get("dir") ?? process.cwd());
768
+ const relayUrl = flags2.get("relay") ?? "http://127.0.0.1:4030";
769
+ const deviceLabel = flags2.get("device-label");
770
+ let result;
771
+ try {
772
+ result = await init({ rootDir, relayUrl, ownerUserId, bindingName: name, deviceLabel });
773
+ } catch (err) {
774
+ if (err instanceof AlreadyInitializedError) {
775
+ if (json) printJsonError("already_initialized", err.message);
776
+ else {
777
+ console.error(kleur.red("error:"), err.message);
778
+ console.error(kleur.dim(" to resume an incomplete init or sync this rig, run `tapd start`."));
779
+ }
780
+ process.exit(1);
781
+ }
782
+ throw err;
783
+ }
784
+ if (json) {
785
+ printJson({
786
+ protocolVersion: PROTOCOL_VERSION,
787
+ bindingId: result.binding.id,
788
+ bindingName: result.binding.name,
789
+ deviceId: result.device.id,
790
+ deviceLabel: result.device.label,
791
+ relayUrl,
792
+ ownerSecret: result.ownerSecret,
793
+ uploadedHashes: result.uploadedHashes,
794
+ reusedHashes: result.reusedHashes,
795
+ submittedEvents: result.submittedEvents,
796
+ cursor: result.cursor,
797
+ warnings: result.warnings
798
+ });
799
+ return;
800
+ }
801
+ const C = kleur;
802
+ console.log(C.bold().cyan("tapd init"));
803
+ console.log(` binding: ${C.bold(result.binding.id)} ${C.dim(result.binding.name)}`);
804
+ console.log(` device: ${C.bold(result.device.id)} ${C.dim(result.device.label)}`);
805
+ console.log(` relay: ${relayUrl}`);
806
+ console.log(` uploaded: ${result.uploadedHashes} new + ${result.reusedHashes} reused`);
807
+ console.log(` events: ${result.submittedEvents} submitted, cursor ${result.cursor}`);
808
+ if (result.warnings.length > 0) {
809
+ console.log(C.yellow(` warnings: ${result.warnings.length}`));
810
+ for (const w of result.warnings.slice(0, 10)) {
811
+ console.log(C.dim(` ${w.reason.padEnd(13)} ${w.path}`));
812
+ }
813
+ if (result.warnings.length > 10) {
814
+ console.log(C.dim(` \u2026 and ${result.warnings.length - 10} more`));
815
+ }
816
+ }
817
+ console.log("");
818
+ console.log(C.bold(" Owner capability token (shown once):"));
819
+ console.log(` ${C.green(result.ownerSecret)}`);
820
+ console.log(C.dim(` Stored in ${rootDir}/.rig/tap-binding.local.json (mode 0600).`));
821
+ }
822
+ function parseFlagsWithPositional(argv) {
823
+ const flags2 = /* @__PURE__ */ new Map();
824
+ const positional2 = [];
825
+ let command2;
826
+ for (let i = 0; i < argv.length; i++) {
827
+ const a = argv[i];
828
+ if (a.startsWith("--")) {
829
+ const eq = a.indexOf("=");
830
+ if (eq >= 0) flags2.set(a.slice(2, eq), a.slice(eq + 1));
831
+ else {
832
+ const next = argv[i + 1];
833
+ if (next && !next.startsWith("-")) {
834
+ flags2.set(a.slice(2), next);
835
+ i++;
836
+ } else {
837
+ flags2.set(a.slice(2), true);
838
+ }
839
+ }
840
+ } else if (!command2) {
841
+ command2 = a;
842
+ } else {
843
+ positional2.push(a);
844
+ }
845
+ }
846
+ return { command: command2, positional: positional2, flags: flags2 };
847
+ }
848
+ var { command, positional, flags } = parseFlagsWithPositional(process.argv.slice(2));
849
+ if (flags.get("version") === true || command === "-v") {
850
+ process.stdout.write(TAPD_VERSION + "\n");
851
+ process.exit(0);
852
+ }
853
+ (async () => {
854
+ switch (command) {
855
+ case "init":
856
+ await runInit(flags);
857
+ return;
858
+ case "invite":
859
+ await runInvite(positional, flags);
860
+ return;
861
+ case "join":
862
+ await runJoin(positional, flags);
863
+ return;
864
+ case "start":
865
+ await runStart(flags);
866
+ return;
867
+ case "status":
868
+ await runStatus(flags);
869
+ return;
870
+ case "uninit":
871
+ await runUninit(flags);
872
+ return;
873
+ case "help":
874
+ case void 0:
875
+ help();
876
+ return;
877
+ default:
878
+ console.error(`unknown command: ${command}`);
879
+ help();
880
+ process.exit(2);
881
+ }
882
+ })().catch((err) => {
883
+ console.error(kleur.red("fatal:"), err);
884
+ process.exit(1);
885
+ });
886
+ //# sourceMappingURL=bin.js.map