@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/LICENSE +21 -0
- package/dist/bin.js +465 -51
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-RQC73B5Y.js → chunk-N3KRCOBG.js} +39 -15
- package/dist/chunk-N3KRCOBG.js.map +1 -0
- package/dist/index.d.ts +35 -13
- package/dist/index.js +1 -1
- package/package.json +23 -14
- package/dist/chunk-RQC73B5Y.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -18,17 +18,77 @@ import {
|
|
|
18
18
|
readBindingConfig,
|
|
19
19
|
revoke,
|
|
20
20
|
start
|
|
21
|
-
} from "./chunk-
|
|
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 (!
|
|
116
|
+
if (!existsSync2(path)) return null;
|
|
57
117
|
let raw;
|
|
58
118
|
try {
|
|
59
|
-
raw =
|
|
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 (!
|
|
136
|
+
if (!existsSync2(path)) return;
|
|
77
137
|
let raw;
|
|
78
138
|
try {
|
|
79
|
-
raw =
|
|
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
|
|
93
|
-
import { join as
|
|
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.
|
|
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(
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
413
|
+
const stateDbPath = join5(opts.rootDir, ".rig", "tap", "state.local.db");
|
|
235
414
|
const tracked = [];
|
|
236
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 =
|
|
299
|
-
if (
|
|
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("--
|
|
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
|
|
348
|
-
${C.dim("--
|
|
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 =
|
|
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>
|
|
831
|
+
console.error(" usage: tapd join <invite-url>");
|
|
547
832
|
}
|
|
548
833
|
process.exit(2);
|
|
549
834
|
}
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
|
768
|
-
const
|
|
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,
|
|
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;
|