@rigxyz/tapd 0.1.0 → 0.3.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 CHANGED
@@ -18,17 +18,77 @@ import {
18
18
  readBindingConfig,
19
19
  revoke,
20
20
  start
21
- } from "./chunk-RQC73B5Y.js";
21
+ } from "./chunk-N3KRCOBG.js";
22
22
 
23
23
  // src/bin.ts
24
- import { join as pathJoin, resolve } from "path";
24
+ import { join as pathJoin, resolve as resolve2 } from "path";
25
25
  import kleur from "kleur";
26
26
 
27
+ // src/defaults.ts
28
+ var DEFAULT_RELAY_URL = "https://tap-relay.fly.dev";
29
+
30
+ // src/hub-token.ts
31
+ import { readFileSync, existsSync } from "fs";
32
+ import { homedir } from "os";
33
+ import { join as join2 } from "path";
34
+ function rigConfigPath() {
35
+ const xdg = process.env.XDG_CONFIG_HOME;
36
+ const base = xdg && xdg.trim() !== "" ? xdg : join2(homedir(), ".config");
37
+ return join2(base, "rig", "config.json");
38
+ }
39
+ var HubTokenMissingError = class extends Error {
40
+ constructor() {
41
+ super(
42
+ "no hub token \u2014 run `rig login` first, or set RIG_HUB_TOKEN. tapd needs an authenticated session to create/join bindings."
43
+ );
44
+ this.name = "HubTokenMissingError";
45
+ }
46
+ };
47
+ function readHubToken(opts = {}) {
48
+ const fromEnv = process.env.RIG_HUB_TOKEN?.trim();
49
+ if (fromEnv) return fromEnv;
50
+ const path = opts.configPath ?? rigConfigPath();
51
+ if (!existsSync(path)) throw new HubTokenMissingError();
52
+ let parsed;
53
+ try {
54
+ parsed = JSON.parse(readFileSync(path, "utf8"));
55
+ } catch {
56
+ throw new HubTokenMissingError();
57
+ }
58
+ if (parsed && typeof parsed === "object" && "hub_token" in parsed) {
59
+ const t = parsed.hub_token;
60
+ if (typeof t === "string" && t.trim() !== "") return t.trim();
61
+ }
62
+ throw new HubTokenMissingError();
63
+ }
64
+
65
+ // src/members.ts
66
+ function clientFor(rootDir, fetchImpl) {
67
+ const cfg = readBindingConfig(rootDir);
68
+ if (!cfg) throw new NotInitializedError(rootDir);
69
+ return new RelayClient({
70
+ baseUrl: cfg.relayUrl,
71
+ bindingId: cfg.bindingId,
72
+ token: cfg.token,
73
+ fetch: fetchImpl
74
+ });
75
+ }
76
+ async function list2(opts) {
77
+ const res = await clientFor(opts.rootDir, opts.fetch).listMembers();
78
+ return res.members;
79
+ }
80
+ async function remove(opts) {
81
+ return clientFor(opts.rootDir, opts.fetch).removeMember(opts.userId);
82
+ }
83
+ async function setRole(opts) {
84
+ return clientFor(opts.rootDir, opts.fetch).setMemberRole(opts.userId, opts.role);
85
+ }
86
+
27
87
  // src/pidfile.ts
28
88
  import {
29
- existsSync,
89
+ existsSync as existsSync2,
30
90
  mkdirSync,
31
- readFileSync,
91
+ readFileSync as readFileSync2,
32
92
  unlinkSync,
33
93
  writeFileSync
34
94
  } from "fs";
@@ -53,10 +113,10 @@ function isProcessAlive(pid) {
53
113
  }
54
114
  }
55
115
  function readLivePid(path) {
56
- if (!existsSync(path)) return null;
116
+ if (!existsSync2(path)) return null;
57
117
  let raw;
58
118
  try {
59
- raw = readFileSync(path, "utf8").trim();
119
+ raw = readFileSync2(path, "utf8").trim();
60
120
  } catch {
61
121
  return null;
62
122
  }
@@ -73,10 +133,10 @@ function acquire(path) {
73
133
  writeFileSync(path, String(process.pid), { mode: 384 });
74
134
  }
75
135
  function release(path) {
76
- if (!existsSync(path)) return;
136
+ if (!existsSync2(path)) return;
77
137
  let raw;
78
138
  try {
79
- raw = readFileSync(path, "utf8").trim();
139
+ raw = readFileSync2(path, "utf8").trim();
80
140
  } catch {
81
141
  return;
82
142
  }
@@ -88,12 +148,131 @@ function release(path) {
88
148
  }
89
149
  }
90
150
 
151
+ // src/history.ts
152
+ var PAGE_LIMIT = 500;
153
+ function clientFor2(rootDir, fetchImpl) {
154
+ const cfg = readBindingConfig(rootDir);
155
+ if (!cfg) throw new NotInitializedError(rootDir);
156
+ return new RelayClient({
157
+ baseUrl: cfg.relayUrl,
158
+ bindingId: cfg.bindingId,
159
+ token: cfg.token,
160
+ fetch: fetchImpl
161
+ });
162
+ }
163
+ async function history(opts) {
164
+ const client = clientFor2(opts.rootDir, opts.fetch);
165
+ const events = [];
166
+ let after = void 0;
167
+ while (true) {
168
+ const page = await client.listChanges({
169
+ after,
170
+ limit: PAGE_LIMIT,
171
+ path: opts.path,
172
+ ...opts.untilInclusive ? { untilInclusive: opts.untilInclusive } : {}
173
+ });
174
+ events.push(...page.events);
175
+ if (page.events.length < PAGE_LIMIT) break;
176
+ after = page.cursor;
177
+ }
178
+ return events.reverse();
179
+ }
180
+
181
+ // src/restore.ts
182
+ import { createHash } from "crypto";
183
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
184
+ import { dirname as dirname2, join as join3, resolve } from "path";
185
+ var PathNeverExistedError = class extends Error {
186
+ constructor(path) {
187
+ super(`No events for path "${path}" \u2014 never written through this binding.`);
188
+ this.path = path;
189
+ this.name = "PathNeverExistedError";
190
+ }
191
+ path;
192
+ };
193
+ var PathCurrentlyDeletedError = class extends Error {
194
+ constructor(path, lastCursor) {
195
+ super(
196
+ `Path "${path}" is currently deleted (last event: ${lastCursor}). Pass --as-of <cursor> with a cursor before the delete to restore an earlier version.`
197
+ );
198
+ this.path = path;
199
+ this.lastCursor = lastCursor;
200
+ this.name = "PathCurrentlyDeletedError";
201
+ }
202
+ path;
203
+ lastCursor;
204
+ };
205
+ var HashMismatchError = class extends Error {
206
+ constructor(path, expected, actual) {
207
+ super(`Hash mismatch restoring "${path}": expected ${expected}, got ${actual}`);
208
+ this.path = path;
209
+ this.expected = expected;
210
+ this.actual = actual;
211
+ this.name = "HashMismatchError";
212
+ }
213
+ path;
214
+ expected;
215
+ actual;
216
+ };
217
+ function clientFor3(rootDir, fetchImpl) {
218
+ const cfg = readBindingConfig(rootDir);
219
+ if (!cfg) throw new NotInitializedError(rootDir);
220
+ return new RelayClient({
221
+ baseUrl: cfg.relayUrl,
222
+ bindingId: cfg.bindingId,
223
+ token: cfg.token,
224
+ fetch: fetchImpl
225
+ });
226
+ }
227
+ function sha256Hex(bytes) {
228
+ return createHash("sha256").update(bytes).digest("hex");
229
+ }
230
+ async function restore(opts) {
231
+ const events = await history({
232
+ rootDir: opts.rootDir,
233
+ path: opts.path,
234
+ ...opts.asOf ? { untilInclusive: opts.asOf } : {},
235
+ ...opts.fetch ? { fetch: opts.fetch } : {}
236
+ });
237
+ if (events.length === 0) {
238
+ throw new PathNeverExistedError(opts.path);
239
+ }
240
+ if (!opts.asOf && events[0].op === "delete") {
241
+ throw new PathCurrentlyDeletedError(opts.path, events[0].id);
242
+ }
243
+ const writeEvent = events.find((e) => e.op === "write" && e.hash != null);
244
+ if (!writeEvent || !writeEvent.hash) {
245
+ throw new PathNeverExistedError(opts.path);
246
+ }
247
+ const client = clientFor3(opts.rootDir, opts.fetch);
248
+ const dl = await client.downloadUrl(writeEvent.hash);
249
+ const bytes = await client.getObjectBytes(dl.downloadUrl);
250
+ const actualHash = `sha256:${sha256Hex(bytes)}`;
251
+ if (actualHash !== writeEvent.hash) {
252
+ throw new HashMismatchError(opts.path, writeEvent.hash, actualHash);
253
+ }
254
+ const targetAbs = resolve(opts.rootDir, opts.path);
255
+ const overwrote = existsSync3(targetAbs);
256
+ mkdirSync2(dirname2(targetAbs), { recursive: true });
257
+ const tmp = `${targetAbs}.tap-restore-tmp.${Date.now().toString(36)}`;
258
+ const mode = writeEvent.executable ? 493 : 420;
259
+ writeFileSync2(tmp, bytes, { mode });
260
+ renameSync(tmp, targetAbs);
261
+ return {
262
+ path: opts.path,
263
+ restoredFromCursor: writeEvent.id,
264
+ hash: writeEvent.hash,
265
+ size: bytes.length,
266
+ overwrote
267
+ };
268
+ }
269
+
91
270
  // src/status.ts
92
- import { existsSync as existsSync2 } from "fs";
93
- import { join as join2 } from "path";
271
+ import { existsSync as existsSync4 } from "fs";
272
+ import { join as join4 } from "path";
94
273
 
95
274
  // src/version.ts
96
- var TAPD_VERSION = "0.1.0";
275
+ var TAPD_VERSION = "0.3.0";
97
276
 
98
277
  // src/status.ts
99
278
  var STATE_KEY_LAST_APPLY_ERROR = "last_apply_error";
@@ -124,7 +303,7 @@ async function status(opts) {
124
303
  if (!config) {
125
304
  throw new NotInitializedError(opts.rootDir);
126
305
  }
127
- const db = openStateDb(join2(opts.rootDir, ".rig", "tap", "state.local.db"));
306
+ const db = openStateDb(join4(opts.rootDir, ".rig", "tap", "state.local.db"));
128
307
  try {
129
308
  const cursor = db.getCursor();
130
309
  const tracked = db.listPaths().length;
@@ -135,7 +314,7 @@ async function status(opts) {
135
314
  const lastErrJson = db.getMeta(STATE_KEY_LAST_APPLY_ERROR);
136
315
  const lastErrAt = db.getMeta(STATE_KEY_LAST_APPLY_ERROR_AT);
137
316
  const lastApplyError = lastErrJson ? safeParseLastError(lastErrJson, lastErrAt) : null;
138
- const pidfilePath = join2(opts.rootDir, DAEMON_PIDFILE_REL);
317
+ const pidfilePath = join4(opts.rootDir, DAEMON_PIDFILE_REL);
139
318
  const livePid = readLivePid(pidfilePath);
140
319
  const daemon = livePid !== null ? { running: true, pid: livePid } : { running: false };
141
320
  const client = new RelayClient({
@@ -160,7 +339,7 @@ async function status(opts) {
160
339
  const pendingApplies = remoteEvents.length;
161
340
  const remoteCursor = remoteEvents.length > 0 ? remoteEvents[remoteEvents.length - 1].id : cursor;
162
341
  let pendingUploads = 0;
163
- if (existsSync2(opts.rootDir)) {
342
+ if (existsSync4(opts.rootDir)) {
164
343
  const ignore = loadIgnore(opts.rootDir);
165
344
  const diff = await computeLocalDiff({ rootDir: opts.rootDir, stateDb: db, ignore });
166
345
  pendingUploads = diff.writes.length + diff.deletes.length + diff.mkdirs.length + diff.rmdirs.length;
@@ -204,13 +383,13 @@ function safeParseLastError(json, at) {
204
383
 
205
384
  // src/uninit.ts
206
385
  import {
207
- existsSync as existsSync3,
386
+ existsSync as existsSync5,
208
387
  rmSync,
209
388
  rmdirSync,
210
389
  statSync,
211
390
  unlinkSync as unlinkSync2
212
391
  } from "fs";
213
- import { join as join3 } from "path";
392
+ import { join as join5 } from "path";
214
393
  var DAEMON_PIDFILE_REL2 = ".rig/tap/daemon.pid";
215
394
  var STATE_DIR_REL = ".rig/tap";
216
395
  var DaemonStillRunningError = class extends Error {
@@ -226,14 +405,14 @@ function uninit(opts) {
226
405
  if (!config) {
227
406
  throw new NotInitializedError(opts.rootDir);
228
407
  }
229
- const pidfilePath = join3(opts.rootDir, DAEMON_PIDFILE_REL2);
408
+ const pidfilePath = join5(opts.rootDir, DAEMON_PIDFILE_REL2);
230
409
  const livePid = readLivePid(pidfilePath);
231
410
  if (livePid !== null) {
232
411
  throw new DaemonStillRunningError(livePid);
233
412
  }
234
- const stateDbPath = join3(opts.rootDir, ".rig", "tap", "state.local.db");
413
+ const stateDbPath = join5(opts.rootDir, ".rig", "tap", "state.local.db");
235
414
  const tracked = [];
236
- if (existsSync3(stateDbPath)) {
415
+ if (existsSync5(stateDbPath)) {
237
416
  const db = openStateDb(stateDbPath);
238
417
  try {
239
418
  for (const p of db.listPaths()) {
@@ -253,7 +432,7 @@ function uninit(opts) {
253
432
  const files = tracked.filter((t) => !t.isDir);
254
433
  const dirs = tracked.filter((t) => t.isDir).sort((a, b) => b.path.split("/").length - a.path.split("/").length);
255
434
  for (const f of files) {
256
- const abs = join3(opts.rootDir, f.path);
435
+ const abs = join5(opts.rootDir, f.path);
257
436
  try {
258
437
  const st = statSync(abs);
259
438
  if (st.isFile()) {
@@ -271,7 +450,7 @@ function uninit(opts) {
271
450
  }
272
451
  }
273
452
  for (const d of dirs) {
274
- const abs = join3(opts.rootDir, d.path);
453
+ const abs = join5(opts.rootDir, d.path);
275
454
  try {
276
455
  rmdirSync(abs);
277
456
  dirsRemoved += 1;
@@ -288,15 +467,15 @@ function uninit(opts) {
288
467
  }
289
468
  }
290
469
  const cfgPath = bindingConfigFile(opts.rootDir);
291
- if (existsSync3(cfgPath)) {
470
+ if (existsSync5(cfgPath)) {
292
471
  try {
293
472
  unlinkSync2(cfgPath);
294
473
  } catch (err) {
295
474
  if (err.code !== "ENOENT") throw err;
296
475
  }
297
476
  }
298
- const stateDir = join3(opts.rootDir, STATE_DIR_REL);
299
- if (existsSync3(stateDir)) {
477
+ const stateDir = join5(opts.rootDir, STATE_DIR_REL);
478
+ if (existsSync5(stateDir)) {
300
479
  rmSync(stateDir, { recursive: true, force: true });
301
480
  }
302
481
  return {
@@ -325,32 +504,42 @@ function help() {
325
504
  ${C.bold().cyan("COMMANDS")}
326
505
  ${C.green("tapd init")} bootstrap a binding in the current directory
327
506
  ${C.green("tapd invite")} mint / list / revoke invite URLs (owner only)
507
+ ${C.green("tapd members")} list / remove / set-role for binding members (owner only for write ops)
328
508
  ${C.green("tapd join")} accept an invite URL into the current directory
329
509
  ${C.green("tapd start")} run the watch + apply daemon (foreground)
330
510
  ${C.green("tapd status")} show binding + cursor + pending remote events
511
+ ${C.green("tapd history")} list ChangeEvents for a single path (recovery)
512
+ ${C.green("tapd restore")} bring a file back from the relay's event log
331
513
  ${C.green("tapd uninit")} disconnect this checkout (optionally --purge tracked files)
332
514
  ${C.green("tapd help")} this message
333
515
 
516
+ ${C.dim("Identity:")} tapd doesn't manage its own credentials \u2014 init/join
517
+ read a Clerk JWT from RIG_HUB_TOKEN (set by rig when shelling out) or
518
+ from ~/.config/rig/config.json (set by ${C.green("rig login")}).
519
+
334
520
  ${C.dim("--version / -v")} print the tapd binary version on stdout and exit.
335
521
  ${C.dim("--json")} on init / invite / join / status / uninit emits a versioned
336
522
  JSON payload to stdout (protocolVersion: 1) for programmatic consumers (rig).
337
523
 
338
524
  ${C.bold().cyan("tapd init OPTIONS")}
339
525
  ${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
526
+ ${C.dim("--relay <url>")} override (defaults to the logged-in relay)
342
527
  ${C.dim("--device-label <l>")} default 'owner'
343
528
  ${C.dim("--dir <path>")} default cwd
344
529
 
345
530
  ${C.bold().cyan("tapd join OPTIONS")}
346
531
  ${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
532
+ relay or dashboard
533
+ ${C.dim("--anonymous")} accept a pure-capability invite without an account
351
534
  ${C.dim("--device-label <l>")} default 'device'
352
535
  ${C.dim("--dir <path>")} default cwd
353
536
 
537
+ ${C.bold().cyan("tapd members SUBCOMMANDS")}
538
+ ${C.green("tapd members list")} list members (any active member can read)
539
+ ${C.green("tapd members remove <userId>")} remove + cascade-revoke their tokens (owner)
540
+ ${C.green("tapd members set-role <userId> <role>")} change role: owner | editor | viewer (owner)
541
+ ${C.dim("--dir <path>")} default cwd
542
+
354
543
  ${C.bold().cyan("tapd invite SUBCOMMANDS")}
355
544
  ${C.green("tapd invite create")} mint a new invite + print the URL once
356
545
  ${C.dim("--ops <read,write,subscribe>")} default read,write,subscribe
@@ -409,10 +598,106 @@ function collectPathGlobs(flags2) {
409
598
  const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
410
599
  return parts.length > 0 ? parts : void 0;
411
600
  }
601
+ async function runMembers(positional2, flags2) {
602
+ const json = flags2.get("json") === true;
603
+ const sub = positional2[0];
604
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
605
+ const VALID_ROLES2 = /* @__PURE__ */ new Set(["owner", "editor", "viewer"]);
606
+ try {
607
+ if (sub === void 0 || sub === "list") {
608
+ const members = await list2({ rootDir });
609
+ if (json) {
610
+ printJson({ protocolVersion: PROTOCOL_VERSION, members });
611
+ return;
612
+ }
613
+ const C = kleur;
614
+ if (members.length === 0) {
615
+ console.log(C.dim("no members"));
616
+ return;
617
+ }
618
+ console.log(C.bold().cyan("tapd members"));
619
+ for (const m of members) {
620
+ const roleColor = m.role === "owner" ? C.yellow : m.role === "editor" ? C.green : C.dim;
621
+ const emailSuffix = m.email ? ` ${C.dim(m.email)}` : "";
622
+ console.log(
623
+ ` ${C.bold(m.userId)} ${roleColor(m.role.padEnd(7))}${emailSuffix} ${C.dim(`joined ${m.joinedAt}`)}`
624
+ );
625
+ }
626
+ return;
627
+ }
628
+ if (sub === "remove") {
629
+ const userId = positional2[1];
630
+ if (!userId) {
631
+ if (json) printJsonError("missing_argument", "usage: tapd members remove <userId>");
632
+ else console.error(kleur.red("error:"), "usage: tapd members remove <userId>");
633
+ process.exit(2);
634
+ }
635
+ const result = await remove({ rootDir, userId });
636
+ if (json) {
637
+ printJson({
638
+ protocolVersion: PROTOCOL_VERSION,
639
+ userId,
640
+ removed: result.removed,
641
+ tokensRevoked: result.tokensRevoked
642
+ });
643
+ return;
644
+ }
645
+ const C = kleur;
646
+ console.log(
647
+ `${C.green("\u2713")} removed ${C.bold(userId)} ${C.dim(`(${result.tokensRevoked} token${result.tokensRevoked === 1 ? "" : "s"} revoked)`)}`
648
+ );
649
+ return;
650
+ }
651
+ if (sub === "set-role") {
652
+ const userId = positional2[1];
653
+ const role = positional2[2];
654
+ if (!userId || !role) {
655
+ if (json) printJsonError("missing_argument", "usage: tapd members set-role <userId> <owner|editor|viewer>");
656
+ else console.error(kleur.red("error:"), "usage: tapd members set-role <userId> <owner|editor|viewer>");
657
+ process.exit(2);
658
+ }
659
+ if (!VALID_ROLES2.has(role)) {
660
+ if (json) printJsonError("invalid_role", `invalid role '${role}' (allowed: owner|editor|viewer)`);
661
+ else console.error(kleur.red("error:"), `invalid role '${role}' (allowed: owner|editor|viewer)`);
662
+ process.exit(2);
663
+ }
664
+ const result = await setRole({
665
+ rootDir,
666
+ userId,
667
+ role
668
+ });
669
+ if (json) {
670
+ printJson({
671
+ protocolVersion: PROTOCOL_VERSION,
672
+ userId,
673
+ role: result.role,
674
+ changed: result.changed
675
+ });
676
+ return;
677
+ }
678
+ const C = kleur;
679
+ console.log(`${C.green("\u2713")} ${C.bold(userId)} \u2192 ${C.bold(role)}`);
680
+ return;
681
+ }
682
+ if (json) printJsonError("unknown_subcommand", `unknown subcommand: tapd members ${sub}`);
683
+ else {
684
+ console.error(kleur.red("error:"), `unknown subcommand: tapd members ${sub}`);
685
+ console.error(" see `tapd help`");
686
+ }
687
+ process.exit(2);
688
+ } catch (err) {
689
+ if (err instanceof NotInitializedError) {
690
+ if (json) printJsonError("not_initialized", err.message);
691
+ else console.error(kleur.red("error:"), err.message);
692
+ process.exit(1);
693
+ }
694
+ throw err;
695
+ }
696
+ }
412
697
  async function runInvite(positional2, flags2) {
413
698
  const json = flags2.get("json") === true;
414
699
  const sub = positional2[0];
415
- const rootDir = resolve(flags2.get("dir") ?? process.cwd());
700
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
416
701
  try {
417
702
  if (sub === void 0 || sub === "create") {
418
703
  const ops = parseOps(flags2.get("ops"));
@@ -543,22 +828,32 @@ async function runJoin(positional2, flags2) {
543
828
  if (json) printJsonError("missing_argument", "missing invite URL");
544
829
  else {
545
830
  console.error(kleur.red("error:"), "missing invite URL");
546
- console.error(" usage: tapd join <invite-url> --user-id <u>");
831
+ console.error(" usage: tapd join <invite-url>");
547
832
  }
548
833
  process.exit(2);
549
834
  }
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);
835
+ const anonymous = flags2.get("anonymous") === true;
836
+ let accessToken = null;
837
+ if (!anonymous) {
838
+ try {
839
+ accessToken = readHubToken();
840
+ } catch (err) {
841
+ if (err instanceof HubTokenMissingError) {
842
+ if (json) printJsonError("not_logged_in", err.message);
843
+ else {
844
+ console.error(kleur.red("error:"), err.message);
845
+ console.error(kleur.dim(" pass --anonymous for a pure-capability invite, or run `rig login`."));
846
+ }
847
+ process.exit(1);
848
+ }
849
+ throw err;
850
+ }
555
851
  }
556
- const rootDir = resolve(flags2.get("dir") ?? process.cwd());
557
- const email = flags2.get("email");
852
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
558
853
  const deviceLabel = flags2.get("device-label");
559
854
  let result;
560
855
  try {
561
- result = await join({ rootDir, inviteUrl, userId, email, deviceLabel });
856
+ result = await join({ rootDir, inviteUrl, accessToken, deviceLabel });
562
857
  } catch (err) {
563
858
  if (err instanceof AlreadyJoinedError) {
564
859
  if (json) printJsonError("already_joined", err.message);
@@ -599,7 +894,7 @@ async function runJoin(positional2, flags2) {
599
894
  }
600
895
  async function runStatus(flags2) {
601
896
  const json = flags2.get("json") === true;
602
- const rootDir = resolve(flags2.get("dir") ?? process.cwd());
897
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
603
898
  let report;
604
899
  try {
605
900
  report = await status({ rootDir });
@@ -649,7 +944,7 @@ async function runStatus(flags2) {
649
944
  async function runUninit(flags2) {
650
945
  const json = flags2.get("json") === true;
651
946
  const purge = flags2.get("purge") === true;
652
- const rootDir = resolve(flags2.get("dir") ?? process.cwd());
947
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
653
948
  let result;
654
949
  try {
655
950
  result = uninit({ rootDir, purge });
@@ -706,7 +1001,7 @@ async function runUninit(flags2) {
706
1001
  }
707
1002
  }
708
1003
  async function runStart(flags2) {
709
- const rootDir = resolve(flags2.get("dir") ?? process.cwd());
1004
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
710
1005
  const pollSeconds = flags2.has("poll-seconds") ? Number(flags2.get("poll-seconds")) : void 0;
711
1006
  const pidfilePath = flags2.get("no-pidfile") === true ? null : pathJoin(rootDir, DAEMON_PIDFILE_REL3);
712
1007
  let handle = null;
@@ -750,26 +1045,136 @@ async function runStart(flags2) {
750
1045
  if (pidfilePath) console.log(` pid: ${process.pid} ${kleur.dim(`(${pidfilePath})`)}`);
751
1046
  console.log(kleur.dim(" press ctrl+c to stop"));
752
1047
  }
1048
+ async function runHistory(flags2) {
1049
+ const json = flags2.get("json") === true;
1050
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
1051
+ const path = flags2.get("path");
1052
+ const asOf = flags2.get("as-of");
1053
+ if (!path) {
1054
+ if (json) printJsonError("missing_flag", "--path is required");
1055
+ else console.error(kleur.red("error:"), "--path is required (e.g. theses/foo.md)");
1056
+ process.exit(2);
1057
+ }
1058
+ let events;
1059
+ try {
1060
+ events = await history({
1061
+ rootDir,
1062
+ path,
1063
+ ...asOf ? { untilInclusive: asOf } : {}
1064
+ });
1065
+ } catch (err) {
1066
+ if (err instanceof NotInitializedError) {
1067
+ if (json) printJsonError("not_initialized", err.message);
1068
+ else console.error(kleur.red("error:"), err.message);
1069
+ process.exit(1);
1070
+ }
1071
+ if (json) printJsonError("history_failed", err.message);
1072
+ else console.error(kleur.red("error:"), err.message);
1073
+ process.exit(1);
1074
+ }
1075
+ if (json) {
1076
+ printJson({
1077
+ protocolVersion: PROTOCOL_VERSION,
1078
+ path,
1079
+ events
1080
+ });
1081
+ return;
1082
+ }
1083
+ const C = kleur;
1084
+ console.log(C.bold().cyan("tapd history") + " " + C.dim(path));
1085
+ if (events.length === 0) {
1086
+ console.log(C.dim(" (no events \u2014 never written through this binding)"));
1087
+ return;
1088
+ }
1089
+ for (const ev of events) {
1090
+ const dot = ev.op === "delete" ? C.red("\u2022") : ev.op === "write" ? C.green("\u2022") : C.dim("\u2022");
1091
+ const sizeStr = ev.size != null ? `${ev.size}B` : "";
1092
+ const actor = ev.actorUserId ? C.dim(`by ${ev.actorUserId}`) : "";
1093
+ console.log(` ${dot} ${C.bold(ev.id.padEnd(10))} ${ev.op.padEnd(6)} ${C.dim(ev.createdAt)} ${sizeStr.padEnd(8)} ${actor}`);
1094
+ }
1095
+ }
1096
+ async function runRestore(flags2) {
1097
+ const json = flags2.get("json") === true;
1098
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
1099
+ const path = flags2.get("path");
1100
+ const asOf = flags2.get("as-of");
1101
+ if (!path) {
1102
+ if (json) printJsonError("missing_flag", "--path is required");
1103
+ else console.error(kleur.red("error:"), "--path is required");
1104
+ process.exit(2);
1105
+ }
1106
+ let result;
1107
+ try {
1108
+ result = await restore({
1109
+ rootDir,
1110
+ path,
1111
+ ...asOf ? { asOf } : {}
1112
+ });
1113
+ } catch (err) {
1114
+ if (err instanceof NotInitializedError) {
1115
+ if (json) printJsonError("not_initialized", err.message);
1116
+ else console.error(kleur.red("error:"), err.message);
1117
+ process.exit(1);
1118
+ }
1119
+ if (err instanceof PathNeverExistedError) {
1120
+ if (json) printJsonError("path_never_existed", err.message);
1121
+ else console.error(kleur.red("error:"), err.message);
1122
+ process.exit(1);
1123
+ }
1124
+ if (err instanceof PathCurrentlyDeletedError) {
1125
+ if (json) printJsonError("path_currently_deleted", err.message);
1126
+ else console.error(kleur.red("error:"), err.message);
1127
+ process.exit(1);
1128
+ }
1129
+ if (err instanceof HashMismatchError) {
1130
+ if (json) printJsonError("hash_mismatch", err.message);
1131
+ else console.error(kleur.red("error:"), err.message);
1132
+ process.exit(1);
1133
+ }
1134
+ if (json) printJsonError("restore_failed", err.message);
1135
+ else console.error(kleur.red("error:"), err.message);
1136
+ process.exit(1);
1137
+ }
1138
+ if (json) {
1139
+ printJson({
1140
+ protocolVersion: PROTOCOL_VERSION,
1141
+ ...result
1142
+ });
1143
+ return;
1144
+ }
1145
+ const C = kleur;
1146
+ console.log(
1147
+ C.green("\u2713 ") + `restored ${C.bold(path)} from ${C.bold(result.restoredFromCursor)} ` + C.dim(`(${result.size}B, hash ${result.hash.slice(0, 16)}\u2026)`)
1148
+ );
1149
+ if (result.overwrote) {
1150
+ console.log(C.dim(" (overwrote local file; daemon will resync to other devices)"));
1151
+ }
1152
+ }
753
1153
  async function runInit(flags2) {
754
1154
  const json = flags2.get("json") === true;
755
1155
  const name = flags2.get("name");
756
- const ownerUserId = flags2.get("owner-user-id");
757
1156
  if (!name) {
758
1157
  if (json) printJsonError("missing_flag", "--name is required");
759
1158
  else console.error(kleur.red("error:"), "--name is required");
760
1159
  process.exit(2);
761
1160
  }
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);
1161
+ let accessToken;
1162
+ try {
1163
+ accessToken = readHubToken();
1164
+ } catch (err) {
1165
+ if (err instanceof HubTokenMissingError) {
1166
+ if (json) printJsonError("not_logged_in", err.message);
1167
+ else console.error(kleur.red("error:"), err.message);
1168
+ process.exit(1);
1169
+ }
1170
+ throw err;
766
1171
  }
767
- const rootDir = resolve(flags2.get("dir") ?? process.cwd());
768
- const relayUrl = flags2.get("relay") ?? "http://127.0.0.1:4030";
1172
+ const relayUrl = flags2.get("relay") ?? process.env.RIG_RELAY_URL ?? DEFAULT_RELAY_URL;
1173
+ const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
769
1174
  const deviceLabel = flags2.get("device-label");
770
1175
  let result;
771
1176
  try {
772
- result = await init({ rootDir, relayUrl, ownerUserId, bindingName: name, deviceLabel });
1177
+ result = await init({ rootDir, relayUrl, accessToken, bindingName: name, deviceLabel });
773
1178
  } catch (err) {
774
1179
  if (err instanceof AlreadyInitializedError) {
775
1180
  if (json) printJsonError("already_initialized", err.message);
@@ -858,6 +1263,9 @@ if (flags.get("version") === true || command === "-v") {
858
1263
  case "invite":
859
1264
  await runInvite(positional, flags);
860
1265
  return;
1266
+ case "members":
1267
+ await runMembers(positional, flags);
1268
+ return;
861
1269
  case "join":
862
1270
  await runJoin(positional, flags);
863
1271
  return;
@@ -867,6 +1275,12 @@ if (flags.get("version") === true || command === "-v") {
867
1275
  case "status":
868
1276
  await runStatus(flags);
869
1277
  return;
1278
+ case "history":
1279
+ await runHistory(flags);
1280
+ return;
1281
+ case "restore":
1282
+ await runRestore(flags);
1283
+ return;
870
1284
  case "uninit":
871
1285
  await runUninit(flags);
872
1286
  return;