@rigxyz/tapd 0.1.0 → 0.4.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 +698 -82
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-RQC73B5Y.js → chunk-YPZ2UNI5.js} +729 -189
- package/dist/chunk-YPZ2UNI5.js.map +1 -0
- package/dist/index.d.ts +345 -31
- package/dist/index.js +35 -3
- package/package.json +1 -1
- package/dist/chunk-RQC73B5Y.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -2,33 +2,99 @@
|
|
|
2
2
|
import {
|
|
3
3
|
AlreadyInitializedError,
|
|
4
4
|
AlreadyJoinedError,
|
|
5
|
+
ImportTokenScopeError,
|
|
6
|
+
ImportValidationFailed,
|
|
5
7
|
InvalidInviteUrlError,
|
|
6
8
|
NotInitializedError,
|
|
7
9
|
RelayClient,
|
|
8
10
|
STATE_KEY_CONFLICT_COUNT,
|
|
11
|
+
addImport,
|
|
9
12
|
bindingConfigFile,
|
|
10
13
|
computeLocalDiff,
|
|
11
14
|
findConflictSidecars,
|
|
15
|
+
importStateDbPath,
|
|
12
16
|
init,
|
|
13
17
|
join,
|
|
14
18
|
list,
|
|
15
19
|
loadIgnore,
|
|
20
|
+
loadImportMounts,
|
|
16
21
|
mint,
|
|
17
22
|
openStateDb,
|
|
18
23
|
readBindingConfig,
|
|
24
|
+
removeImport,
|
|
19
25
|
revoke,
|
|
20
26
|
start
|
|
21
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-YPZ2UNI5.js";
|
|
22
28
|
|
|
23
29
|
// src/bin.ts
|
|
24
|
-
import { join as pathJoin, resolve } from "path";
|
|
30
|
+
import { join as pathJoin, resolve as resolve2 } from "path";
|
|
25
31
|
import kleur from "kleur";
|
|
26
32
|
|
|
33
|
+
// src/defaults.ts
|
|
34
|
+
var DEFAULT_RELAY_URL = "https://tap-relay.fly.dev";
|
|
35
|
+
|
|
36
|
+
// src/hub-token.ts
|
|
37
|
+
import { readFileSync, existsSync } from "fs";
|
|
38
|
+
import { homedir } from "os";
|
|
39
|
+
import { join as join2 } from "path";
|
|
40
|
+
function rigConfigPath() {
|
|
41
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
42
|
+
const base = xdg && xdg.trim() !== "" ? xdg : join2(homedir(), ".config");
|
|
43
|
+
return join2(base, "rig", "config.json");
|
|
44
|
+
}
|
|
45
|
+
var HubTokenMissingError = class extends Error {
|
|
46
|
+
constructor() {
|
|
47
|
+
super(
|
|
48
|
+
"no hub token \u2014 run `rig login` first, or set RIG_HUB_TOKEN. tapd needs an authenticated session to create/join bindings."
|
|
49
|
+
);
|
|
50
|
+
this.name = "HubTokenMissingError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function readHubToken(opts = {}) {
|
|
54
|
+
const fromEnv = process.env.RIG_HUB_TOKEN?.trim();
|
|
55
|
+
if (fromEnv) return fromEnv;
|
|
56
|
+
const path = opts.configPath ?? rigConfigPath();
|
|
57
|
+
if (!existsSync(path)) throw new HubTokenMissingError();
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
61
|
+
} catch {
|
|
62
|
+
throw new HubTokenMissingError();
|
|
63
|
+
}
|
|
64
|
+
if (parsed && typeof parsed === "object" && "hub_token" in parsed) {
|
|
65
|
+
const t = parsed.hub_token;
|
|
66
|
+
if (typeof t === "string" && t.trim() !== "") return t.trim();
|
|
67
|
+
}
|
|
68
|
+
throw new HubTokenMissingError();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/members.ts
|
|
72
|
+
function clientFor(rootDir, fetchImpl) {
|
|
73
|
+
const cfg = readBindingConfig(rootDir);
|
|
74
|
+
if (!cfg) throw new NotInitializedError(rootDir);
|
|
75
|
+
return new RelayClient({
|
|
76
|
+
baseUrl: cfg.relayUrl,
|
|
77
|
+
bindingId: cfg.bindingId,
|
|
78
|
+
token: cfg.token,
|
|
79
|
+
fetch: fetchImpl
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function list2(opts) {
|
|
83
|
+
const res = await clientFor(opts.rootDir, opts.fetch).listMembers();
|
|
84
|
+
return res.members;
|
|
85
|
+
}
|
|
86
|
+
async function remove(opts) {
|
|
87
|
+
return clientFor(opts.rootDir, opts.fetch).removeMember(opts.userId);
|
|
88
|
+
}
|
|
89
|
+
async function setRole(opts) {
|
|
90
|
+
return clientFor(opts.rootDir, opts.fetch).setMemberRole(opts.userId, opts.role);
|
|
91
|
+
}
|
|
92
|
+
|
|
27
93
|
// src/pidfile.ts
|
|
28
94
|
import {
|
|
29
|
-
existsSync,
|
|
95
|
+
existsSync as existsSync2,
|
|
30
96
|
mkdirSync,
|
|
31
|
-
readFileSync,
|
|
97
|
+
readFileSync as readFileSync2,
|
|
32
98
|
unlinkSync,
|
|
33
99
|
writeFileSync
|
|
34
100
|
} from "fs";
|
|
@@ -53,10 +119,10 @@ function isProcessAlive(pid) {
|
|
|
53
119
|
}
|
|
54
120
|
}
|
|
55
121
|
function readLivePid(path) {
|
|
56
|
-
if (!
|
|
122
|
+
if (!existsSync2(path)) return null;
|
|
57
123
|
let raw;
|
|
58
124
|
try {
|
|
59
|
-
raw =
|
|
125
|
+
raw = readFileSync2(path, "utf8").trim();
|
|
60
126
|
} catch {
|
|
61
127
|
return null;
|
|
62
128
|
}
|
|
@@ -73,10 +139,10 @@ function acquire(path) {
|
|
|
73
139
|
writeFileSync(path, String(process.pid), { mode: 384 });
|
|
74
140
|
}
|
|
75
141
|
function release(path) {
|
|
76
|
-
if (!
|
|
142
|
+
if (!existsSync2(path)) return;
|
|
77
143
|
let raw;
|
|
78
144
|
try {
|
|
79
|
-
raw =
|
|
145
|
+
raw = readFileSync2(path, "utf8").trim();
|
|
80
146
|
} catch {
|
|
81
147
|
return;
|
|
82
148
|
}
|
|
@@ -88,12 +154,131 @@ function release(path) {
|
|
|
88
154
|
}
|
|
89
155
|
}
|
|
90
156
|
|
|
157
|
+
// src/history.ts
|
|
158
|
+
var PAGE_LIMIT = 500;
|
|
159
|
+
function clientFor2(rootDir, fetchImpl) {
|
|
160
|
+
const cfg = readBindingConfig(rootDir);
|
|
161
|
+
if (!cfg) throw new NotInitializedError(rootDir);
|
|
162
|
+
return new RelayClient({
|
|
163
|
+
baseUrl: cfg.relayUrl,
|
|
164
|
+
bindingId: cfg.bindingId,
|
|
165
|
+
token: cfg.token,
|
|
166
|
+
fetch: fetchImpl
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function history(opts) {
|
|
170
|
+
const client = clientFor2(opts.rootDir, opts.fetch);
|
|
171
|
+
const events = [];
|
|
172
|
+
let after = void 0;
|
|
173
|
+
while (true) {
|
|
174
|
+
const page = await client.listChanges({
|
|
175
|
+
after,
|
|
176
|
+
limit: PAGE_LIMIT,
|
|
177
|
+
path: opts.path,
|
|
178
|
+
...opts.untilInclusive ? { untilInclusive: opts.untilInclusive } : {}
|
|
179
|
+
});
|
|
180
|
+
events.push(...page.events);
|
|
181
|
+
if (page.events.length < PAGE_LIMIT) break;
|
|
182
|
+
after = page.cursor;
|
|
183
|
+
}
|
|
184
|
+
return events.reverse();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/restore.ts
|
|
188
|
+
import { createHash } from "crypto";
|
|
189
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
190
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
191
|
+
var PathNeverExistedError = class extends Error {
|
|
192
|
+
constructor(path) {
|
|
193
|
+
super(`No events for path "${path}" \u2014 never written through this binding.`);
|
|
194
|
+
this.path = path;
|
|
195
|
+
this.name = "PathNeverExistedError";
|
|
196
|
+
}
|
|
197
|
+
path;
|
|
198
|
+
};
|
|
199
|
+
var PathCurrentlyDeletedError = class extends Error {
|
|
200
|
+
constructor(path, lastCursor) {
|
|
201
|
+
super(
|
|
202
|
+
`Path "${path}" is currently deleted (last event: ${lastCursor}). Pass --as-of <cursor> with a cursor before the delete to restore an earlier version.`
|
|
203
|
+
);
|
|
204
|
+
this.path = path;
|
|
205
|
+
this.lastCursor = lastCursor;
|
|
206
|
+
this.name = "PathCurrentlyDeletedError";
|
|
207
|
+
}
|
|
208
|
+
path;
|
|
209
|
+
lastCursor;
|
|
210
|
+
};
|
|
211
|
+
var HashMismatchError = class extends Error {
|
|
212
|
+
constructor(path, expected, actual) {
|
|
213
|
+
super(`Hash mismatch restoring "${path}": expected ${expected}, got ${actual}`);
|
|
214
|
+
this.path = path;
|
|
215
|
+
this.expected = expected;
|
|
216
|
+
this.actual = actual;
|
|
217
|
+
this.name = "HashMismatchError";
|
|
218
|
+
}
|
|
219
|
+
path;
|
|
220
|
+
expected;
|
|
221
|
+
actual;
|
|
222
|
+
};
|
|
223
|
+
function clientFor3(rootDir, fetchImpl) {
|
|
224
|
+
const cfg = readBindingConfig(rootDir);
|
|
225
|
+
if (!cfg) throw new NotInitializedError(rootDir);
|
|
226
|
+
return new RelayClient({
|
|
227
|
+
baseUrl: cfg.relayUrl,
|
|
228
|
+
bindingId: cfg.bindingId,
|
|
229
|
+
token: cfg.token,
|
|
230
|
+
fetch: fetchImpl
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function sha256Hex(bytes) {
|
|
234
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
235
|
+
}
|
|
236
|
+
async function restore(opts) {
|
|
237
|
+
const events = await history({
|
|
238
|
+
rootDir: opts.rootDir,
|
|
239
|
+
path: opts.path,
|
|
240
|
+
...opts.asOf ? { untilInclusive: opts.asOf } : {},
|
|
241
|
+
...opts.fetch ? { fetch: opts.fetch } : {}
|
|
242
|
+
});
|
|
243
|
+
if (events.length === 0) {
|
|
244
|
+
throw new PathNeverExistedError(opts.path);
|
|
245
|
+
}
|
|
246
|
+
if (!opts.asOf && events[0].op === "delete") {
|
|
247
|
+
throw new PathCurrentlyDeletedError(opts.path, events[0].id);
|
|
248
|
+
}
|
|
249
|
+
const writeEvent = events.find((e) => e.op === "write" && e.hash != null);
|
|
250
|
+
if (!writeEvent || !writeEvent.hash) {
|
|
251
|
+
throw new PathNeverExistedError(opts.path);
|
|
252
|
+
}
|
|
253
|
+
const client = clientFor3(opts.rootDir, opts.fetch);
|
|
254
|
+
const dl = await client.downloadUrl(writeEvent.hash);
|
|
255
|
+
const bytes = await client.getObjectBytes(dl.downloadUrl);
|
|
256
|
+
const actualHash = `sha256:${sha256Hex(bytes)}`;
|
|
257
|
+
if (actualHash !== writeEvent.hash) {
|
|
258
|
+
throw new HashMismatchError(opts.path, writeEvent.hash, actualHash);
|
|
259
|
+
}
|
|
260
|
+
const targetAbs = resolve(opts.rootDir, opts.path);
|
|
261
|
+
const overwrote = existsSync3(targetAbs);
|
|
262
|
+
mkdirSync2(dirname2(targetAbs), { recursive: true });
|
|
263
|
+
const tmp = `${targetAbs}.tap-restore-tmp.${Date.now().toString(36)}`;
|
|
264
|
+
const mode = writeEvent.executable ? 493 : 420;
|
|
265
|
+
writeFileSync2(tmp, bytes, { mode });
|
|
266
|
+
renameSync(tmp, targetAbs);
|
|
267
|
+
return {
|
|
268
|
+
path: opts.path,
|
|
269
|
+
restoredFromCursor: writeEvent.id,
|
|
270
|
+
hash: writeEvent.hash,
|
|
271
|
+
size: bytes.length,
|
|
272
|
+
overwrote
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
91
276
|
// src/status.ts
|
|
92
|
-
import { existsSync as
|
|
93
|
-
import { join as
|
|
277
|
+
import { existsSync as existsSync4 } from "fs";
|
|
278
|
+
import { join as join4 } from "path";
|
|
94
279
|
|
|
95
280
|
// src/version.ts
|
|
96
|
-
var TAPD_VERSION = "0.
|
|
281
|
+
var TAPD_VERSION = "0.4.0";
|
|
97
282
|
|
|
98
283
|
// src/status.ts
|
|
99
284
|
var STATE_KEY_LAST_APPLY_ERROR = "last_apply_error";
|
|
@@ -105,9 +290,9 @@ function statusToJson(report) {
|
|
|
105
290
|
return {
|
|
106
291
|
protocolVersion: STATUS_PROTOCOL_VERSION,
|
|
107
292
|
version: TAPD_VERSION,
|
|
108
|
-
bindingId: report.config
|
|
109
|
-
relayUrl: report.config
|
|
110
|
-
deviceId: report.config
|
|
293
|
+
bindingId: report.config?.bindingId ?? null,
|
|
294
|
+
relayUrl: report.config?.relayUrl ?? null,
|
|
295
|
+
deviceId: report.config?.deviceId ?? null,
|
|
111
296
|
daemon: report.daemon,
|
|
112
297
|
cursor: report.cursor,
|
|
113
298
|
remoteCursor: report.remoteCursor,
|
|
@@ -116,15 +301,67 @@ function statusToJson(report) {
|
|
|
116
301
|
pendingUploads: report.pendingUploads,
|
|
117
302
|
conflicts: report.conflicts,
|
|
118
303
|
lastApplyError: report.lastApplyError,
|
|
119
|
-
offline: report.offline
|
|
304
|
+
offline: report.offline,
|
|
305
|
+
mounts: report.mounts
|
|
120
306
|
};
|
|
121
307
|
}
|
|
308
|
+
function readMounts(rootDir) {
|
|
309
|
+
const loaded = loadImportMounts(rootDir);
|
|
310
|
+
const mounts = [];
|
|
311
|
+
const readyPaths = [];
|
|
312
|
+
for (const spec of loaded.ready) {
|
|
313
|
+
let cursor;
|
|
314
|
+
const dbPath = importStateDbPath(rootDir, spec.name);
|
|
315
|
+
if (existsSync4(dbPath)) {
|
|
316
|
+
const db = openStateDb(dbPath);
|
|
317
|
+
try {
|
|
318
|
+
cursor = db.getCursor();
|
|
319
|
+
} finally {
|
|
320
|
+
db.close();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
mounts.push({
|
|
324
|
+
name: spec.name,
|
|
325
|
+
path: spec.path,
|
|
326
|
+
state: "ok",
|
|
327
|
+
bindingId: spec.bindingId,
|
|
328
|
+
...cursor ? { cursor } : {}
|
|
329
|
+
});
|
|
330
|
+
readyPaths.push(spec.path);
|
|
331
|
+
}
|
|
332
|
+
for (const b of loaded.broken) {
|
|
333
|
+
mounts.push({ name: b.name, path: b.path, state: "misconfigured", reason: b.reason });
|
|
334
|
+
}
|
|
335
|
+
mounts.sort((a, b) => a.name.localeCompare(b.name));
|
|
336
|
+
return { mounts, readyPaths };
|
|
337
|
+
}
|
|
122
338
|
async function status(opts) {
|
|
123
339
|
const config = readBindingConfig(opts.rootDir);
|
|
340
|
+
const { mounts, readyPaths } = readMounts(opts.rootDir);
|
|
341
|
+
const pidfilePath = join4(opts.rootDir, DAEMON_PIDFILE_REL);
|
|
342
|
+
const livePid = readLivePid(pidfilePath);
|
|
343
|
+
const daemon = livePid !== null ? { running: true, pid: livePid } : { running: false };
|
|
124
344
|
if (!config) {
|
|
125
|
-
|
|
345
|
+
if (mounts.length === 0) {
|
|
346
|
+
throw new NotInitializedError(opts.rootDir);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
config: null,
|
|
350
|
+
cursor: "chg_0",
|
|
351
|
+
remoteCursor: "chg_0",
|
|
352
|
+
trackedPaths: 0,
|
|
353
|
+
pendingPeek: [],
|
|
354
|
+
pendingApplies: 0,
|
|
355
|
+
pendingUploads: 0,
|
|
356
|
+
conflicts: [],
|
|
357
|
+
conflictCount: 0,
|
|
358
|
+
lastApplyError: null,
|
|
359
|
+
daemon,
|
|
360
|
+
offline: false,
|
|
361
|
+
mounts
|
|
362
|
+
};
|
|
126
363
|
}
|
|
127
|
-
const db = openStateDb(
|
|
364
|
+
const db = openStateDb(join4(opts.rootDir, ".rig", "tap", "state.local.db"));
|
|
128
365
|
try {
|
|
129
366
|
const cursor = db.getCursor();
|
|
130
367
|
const tracked = db.listPaths().length;
|
|
@@ -135,9 +372,6 @@ async function status(opts) {
|
|
|
135
372
|
const lastErrJson = db.getMeta(STATE_KEY_LAST_APPLY_ERROR);
|
|
136
373
|
const lastErrAt = db.getMeta(STATE_KEY_LAST_APPLY_ERROR_AT);
|
|
137
374
|
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
375
|
const client = new RelayClient({
|
|
142
376
|
baseUrl: config.relayUrl,
|
|
143
377
|
bindingId: config.bindingId,
|
|
@@ -160,9 +394,14 @@ async function status(opts) {
|
|
|
160
394
|
const pendingApplies = remoteEvents.length;
|
|
161
395
|
const remoteCursor = remoteEvents.length > 0 ? remoteEvents[remoteEvents.length - 1].id : cursor;
|
|
162
396
|
let pendingUploads = 0;
|
|
163
|
-
if (
|
|
397
|
+
if (existsSync4(opts.rootDir)) {
|
|
164
398
|
const ignore = loadIgnore(opts.rootDir);
|
|
165
|
-
const diff = await computeLocalDiff({
|
|
399
|
+
const diff = await computeLocalDiff({
|
|
400
|
+
rootDir: opts.rootDir,
|
|
401
|
+
stateDb: db,
|
|
402
|
+
ignore,
|
|
403
|
+
excludePaths: readyPaths
|
|
404
|
+
});
|
|
166
405
|
pendingUploads = diff.writes.length + diff.deletes.length + diff.mkdirs.length + diff.rmdirs.length;
|
|
167
406
|
}
|
|
168
407
|
const conflicts = findConflictSidecars(opts.rootDir);
|
|
@@ -178,7 +417,8 @@ async function status(opts) {
|
|
|
178
417
|
conflictCount,
|
|
179
418
|
lastApplyError,
|
|
180
419
|
daemon,
|
|
181
|
-
offline
|
|
420
|
+
offline,
|
|
421
|
+
mounts
|
|
182
422
|
};
|
|
183
423
|
} finally {
|
|
184
424
|
db.close();
|
|
@@ -204,13 +444,13 @@ function safeParseLastError(json, at) {
|
|
|
204
444
|
|
|
205
445
|
// src/uninit.ts
|
|
206
446
|
import {
|
|
207
|
-
existsSync as
|
|
447
|
+
existsSync as existsSync5,
|
|
208
448
|
rmSync,
|
|
209
449
|
rmdirSync,
|
|
210
450
|
statSync,
|
|
211
451
|
unlinkSync as unlinkSync2
|
|
212
452
|
} from "fs";
|
|
213
|
-
import { join as
|
|
453
|
+
import { join as join5 } from "path";
|
|
214
454
|
var DAEMON_PIDFILE_REL2 = ".rig/tap/daemon.pid";
|
|
215
455
|
var STATE_DIR_REL = ".rig/tap";
|
|
216
456
|
var DaemonStillRunningError = class extends Error {
|
|
@@ -226,14 +466,14 @@ function uninit(opts) {
|
|
|
226
466
|
if (!config) {
|
|
227
467
|
throw new NotInitializedError(opts.rootDir);
|
|
228
468
|
}
|
|
229
|
-
const pidfilePath =
|
|
469
|
+
const pidfilePath = join5(opts.rootDir, DAEMON_PIDFILE_REL2);
|
|
230
470
|
const livePid = readLivePid(pidfilePath);
|
|
231
471
|
if (livePid !== null) {
|
|
232
472
|
throw new DaemonStillRunningError(livePid);
|
|
233
473
|
}
|
|
234
|
-
const stateDbPath =
|
|
474
|
+
const stateDbPath = join5(opts.rootDir, ".rig", "tap", "state.local.db");
|
|
235
475
|
const tracked = [];
|
|
236
|
-
if (
|
|
476
|
+
if (existsSync5(stateDbPath)) {
|
|
237
477
|
const db = openStateDb(stateDbPath);
|
|
238
478
|
try {
|
|
239
479
|
for (const p of db.listPaths()) {
|
|
@@ -253,7 +493,7 @@ function uninit(opts) {
|
|
|
253
493
|
const files = tracked.filter((t) => !t.isDir);
|
|
254
494
|
const dirs = tracked.filter((t) => t.isDir).sort((a, b) => b.path.split("/").length - a.path.split("/").length);
|
|
255
495
|
for (const f of files) {
|
|
256
|
-
const abs =
|
|
496
|
+
const abs = join5(opts.rootDir, f.path);
|
|
257
497
|
try {
|
|
258
498
|
const st = statSync(abs);
|
|
259
499
|
if (st.isFile()) {
|
|
@@ -271,7 +511,7 @@ function uninit(opts) {
|
|
|
271
511
|
}
|
|
272
512
|
}
|
|
273
513
|
for (const d of dirs) {
|
|
274
|
-
const abs =
|
|
514
|
+
const abs = join5(opts.rootDir, d.path);
|
|
275
515
|
try {
|
|
276
516
|
rmdirSync(abs);
|
|
277
517
|
dirsRemoved += 1;
|
|
@@ -288,15 +528,15 @@ function uninit(opts) {
|
|
|
288
528
|
}
|
|
289
529
|
}
|
|
290
530
|
const cfgPath = bindingConfigFile(opts.rootDir);
|
|
291
|
-
if (
|
|
531
|
+
if (existsSync5(cfgPath)) {
|
|
292
532
|
try {
|
|
293
533
|
unlinkSync2(cfgPath);
|
|
294
534
|
} catch (err) {
|
|
295
535
|
if (err.code !== "ENOENT") throw err;
|
|
296
536
|
}
|
|
297
537
|
}
|
|
298
|
-
const stateDir =
|
|
299
|
-
if (
|
|
538
|
+
const stateDir = join5(opts.rootDir, STATE_DIR_REL);
|
|
539
|
+
if (existsSync5(stateDir)) {
|
|
300
540
|
rmSync(stateDir, { recursive: true, force: true });
|
|
301
541
|
}
|
|
302
542
|
return {
|
|
@@ -325,32 +565,43 @@ function help() {
|
|
|
325
565
|
${C.bold().cyan("COMMANDS")}
|
|
326
566
|
${C.green("tapd init")} bootstrap a binding in the current directory
|
|
327
567
|
${C.green("tapd invite")} mint / list / revoke invite URLs (owner only)
|
|
568
|
+
${C.green("tapd members")} list / remove / set-role for binding members (owner only for write ops)
|
|
328
569
|
${C.green("tapd join")} accept an invite URL into the current directory
|
|
570
|
+
${C.green("tapd import")} mount another binding read-only into a subdir (meta-rig)
|
|
329
571
|
${C.green("tapd start")} run the watch + apply daemon (foreground)
|
|
330
572
|
${C.green("tapd status")} show binding + cursor + pending remote events
|
|
573
|
+
${C.green("tapd history")} list ChangeEvents for a single path (recovery)
|
|
574
|
+
${C.green("tapd restore")} bring a file back from the relay's event log
|
|
331
575
|
${C.green("tapd uninit")} disconnect this checkout (optionally --purge tracked files)
|
|
332
576
|
${C.green("tapd help")} this message
|
|
333
577
|
|
|
578
|
+
${C.dim("Identity:")} tapd doesn't manage its own credentials \u2014 init/join
|
|
579
|
+
read a Clerk JWT from RIG_HUB_TOKEN (set by rig when shelling out) or
|
|
580
|
+
from ~/.config/rig/config.json (set by ${C.green("rig login")}).
|
|
581
|
+
|
|
334
582
|
${C.dim("--version / -v")} print the tapd binary version on stdout and exit.
|
|
335
583
|
${C.dim("--json")} on init / invite / join / status / uninit emits a versioned
|
|
336
584
|
JSON payload to stdout (protocolVersion: 1) for programmatic consumers (rig).
|
|
337
585
|
|
|
338
586
|
${C.bold().cyan("tapd init OPTIONS")}
|
|
339
587
|
${C.dim("--name <n>")} binding display name (required)
|
|
340
|
-
${C.dim("--
|
|
341
|
-
${C.dim("--relay <url>")} default http://127.0.0.1:4030
|
|
588
|
+
${C.dim("--relay <url>")} override (defaults to the logged-in relay)
|
|
342
589
|
${C.dim("--device-label <l>")} default 'owner'
|
|
343
590
|
${C.dim("--dir <path>")} default cwd
|
|
344
591
|
|
|
345
592
|
${C.bold().cyan("tapd join OPTIONS")}
|
|
346
593
|
${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
|
|
594
|
+
relay or dashboard
|
|
595
|
+
${C.dim("--anonymous")} accept a pure-capability invite without an account
|
|
351
596
|
${C.dim("--device-label <l>")} default 'device'
|
|
352
597
|
${C.dim("--dir <path>")} default cwd
|
|
353
598
|
|
|
599
|
+
${C.bold().cyan("tapd members SUBCOMMANDS")}
|
|
600
|
+
${C.green("tapd members list")} list members (any active member can read)
|
|
601
|
+
${C.green("tapd members remove <userId>")} remove + cascade-revoke their tokens (owner)
|
|
602
|
+
${C.green("tapd members set-role <userId> <role>")} change role: owner | editor | viewer (owner)
|
|
603
|
+
${C.dim("--dir <path>")} default cwd
|
|
604
|
+
|
|
354
605
|
${C.bold().cyan("tapd invite SUBCOMMANDS")}
|
|
355
606
|
${C.green("tapd invite create")} mint a new invite + print the URL once
|
|
356
607
|
${C.dim("--ops <read,write,subscribe>")} default read,write,subscribe
|
|
@@ -409,10 +660,106 @@ function collectPathGlobs(flags2) {
|
|
|
409
660
|
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
410
661
|
return parts.length > 0 ? parts : void 0;
|
|
411
662
|
}
|
|
663
|
+
async function runMembers(positional2, flags2) {
|
|
664
|
+
const json = flags2.get("json") === true;
|
|
665
|
+
const sub = positional2[0];
|
|
666
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
667
|
+
const VALID_ROLES2 = /* @__PURE__ */ new Set(["owner", "editor", "viewer"]);
|
|
668
|
+
try {
|
|
669
|
+
if (sub === void 0 || sub === "list") {
|
|
670
|
+
const members = await list2({ rootDir });
|
|
671
|
+
if (json) {
|
|
672
|
+
printJson({ protocolVersion: PROTOCOL_VERSION, members });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const C = kleur;
|
|
676
|
+
if (members.length === 0) {
|
|
677
|
+
console.log(C.dim("no members"));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
console.log(C.bold().cyan("tapd members"));
|
|
681
|
+
for (const m of members) {
|
|
682
|
+
const roleColor = m.role === "owner" ? C.yellow : m.role === "editor" ? C.green : C.dim;
|
|
683
|
+
const emailSuffix = m.email ? ` ${C.dim(m.email)}` : "";
|
|
684
|
+
console.log(
|
|
685
|
+
` ${C.bold(m.userId)} ${roleColor(m.role.padEnd(7))}${emailSuffix} ${C.dim(`joined ${m.joinedAt}`)}`
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (sub === "remove") {
|
|
691
|
+
const userId = positional2[1];
|
|
692
|
+
if (!userId) {
|
|
693
|
+
if (json) printJsonError("missing_argument", "usage: tapd members remove <userId>");
|
|
694
|
+
else console.error(kleur.red("error:"), "usage: tapd members remove <userId>");
|
|
695
|
+
process.exit(2);
|
|
696
|
+
}
|
|
697
|
+
const result = await remove({ rootDir, userId });
|
|
698
|
+
if (json) {
|
|
699
|
+
printJson({
|
|
700
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
701
|
+
userId,
|
|
702
|
+
removed: result.removed,
|
|
703
|
+
tokensRevoked: result.tokensRevoked
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const C = kleur;
|
|
708
|
+
console.log(
|
|
709
|
+
`${C.green("\u2713")} removed ${C.bold(userId)} ${C.dim(`(${result.tokensRevoked} token${result.tokensRevoked === 1 ? "" : "s"} revoked)`)}`
|
|
710
|
+
);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (sub === "set-role") {
|
|
714
|
+
const userId = positional2[1];
|
|
715
|
+
const role = positional2[2];
|
|
716
|
+
if (!userId || !role) {
|
|
717
|
+
if (json) printJsonError("missing_argument", "usage: tapd members set-role <userId> <owner|editor|viewer>");
|
|
718
|
+
else console.error(kleur.red("error:"), "usage: tapd members set-role <userId> <owner|editor|viewer>");
|
|
719
|
+
process.exit(2);
|
|
720
|
+
}
|
|
721
|
+
if (!VALID_ROLES2.has(role)) {
|
|
722
|
+
if (json) printJsonError("invalid_role", `invalid role '${role}' (allowed: owner|editor|viewer)`);
|
|
723
|
+
else console.error(kleur.red("error:"), `invalid role '${role}' (allowed: owner|editor|viewer)`);
|
|
724
|
+
process.exit(2);
|
|
725
|
+
}
|
|
726
|
+
const result = await setRole({
|
|
727
|
+
rootDir,
|
|
728
|
+
userId,
|
|
729
|
+
role
|
|
730
|
+
});
|
|
731
|
+
if (json) {
|
|
732
|
+
printJson({
|
|
733
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
734
|
+
userId,
|
|
735
|
+
role: result.role,
|
|
736
|
+
changed: result.changed
|
|
737
|
+
});
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const C = kleur;
|
|
741
|
+
console.log(`${C.green("\u2713")} ${C.bold(userId)} \u2192 ${C.bold(role)}`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (json) printJsonError("unknown_subcommand", `unknown subcommand: tapd members ${sub}`);
|
|
745
|
+
else {
|
|
746
|
+
console.error(kleur.red("error:"), `unknown subcommand: tapd members ${sub}`);
|
|
747
|
+
console.error(" see `tapd help`");
|
|
748
|
+
}
|
|
749
|
+
process.exit(2);
|
|
750
|
+
} catch (err) {
|
|
751
|
+
if (err instanceof NotInitializedError) {
|
|
752
|
+
if (json) printJsonError("not_initialized", err.message);
|
|
753
|
+
else console.error(kleur.red("error:"), err.message);
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
throw err;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
412
759
|
async function runInvite(positional2, flags2) {
|
|
413
760
|
const json = flags2.get("json") === true;
|
|
414
761
|
const sub = positional2[0];
|
|
415
|
-
const rootDir =
|
|
762
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
416
763
|
try {
|
|
417
764
|
if (sub === void 0 || sub === "create") {
|
|
418
765
|
const ops = parseOps(flags2.get("ops"));
|
|
@@ -543,22 +890,32 @@ async function runJoin(positional2, flags2) {
|
|
|
543
890
|
if (json) printJsonError("missing_argument", "missing invite URL");
|
|
544
891
|
else {
|
|
545
892
|
console.error(kleur.red("error:"), "missing invite URL");
|
|
546
|
-
console.error(" usage: tapd join <invite-url>
|
|
893
|
+
console.error(" usage: tapd join <invite-url>");
|
|
547
894
|
}
|
|
548
895
|
process.exit(2);
|
|
549
896
|
}
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
897
|
+
const anonymous = flags2.get("anonymous") === true;
|
|
898
|
+
let accessToken = null;
|
|
899
|
+
if (!anonymous) {
|
|
900
|
+
try {
|
|
901
|
+
accessToken = readHubToken();
|
|
902
|
+
} catch (err) {
|
|
903
|
+
if (err instanceof HubTokenMissingError) {
|
|
904
|
+
if (json) printJsonError("not_logged_in", err.message);
|
|
905
|
+
else {
|
|
906
|
+
console.error(kleur.red("error:"), err.message);
|
|
907
|
+
console.error(kleur.dim(" pass --anonymous for a pure-capability invite, or run `rig login`."));
|
|
908
|
+
}
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
throw err;
|
|
912
|
+
}
|
|
555
913
|
}
|
|
556
|
-
const rootDir =
|
|
557
|
-
const email = flags2.get("email");
|
|
914
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
558
915
|
const deviceLabel = flags2.get("device-label");
|
|
559
916
|
let result;
|
|
560
917
|
try {
|
|
561
|
-
result = await join({ rootDir, inviteUrl,
|
|
918
|
+
result = await join({ rootDir, inviteUrl, accessToken, deviceLabel });
|
|
562
919
|
} catch (err) {
|
|
563
920
|
if (err instanceof AlreadyJoinedError) {
|
|
564
921
|
if (json) printJsonError("already_joined", err.message);
|
|
@@ -597,9 +954,132 @@ async function runJoin(positional2, flags2) {
|
|
|
597
954
|
console.log("");
|
|
598
955
|
console.log(C.dim(" Next: `tapd start` (initial apply pulls the remote manifest)."));
|
|
599
956
|
}
|
|
957
|
+
async function runImport(positional2, flags2) {
|
|
958
|
+
const json = flags2.get("json") === true;
|
|
959
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
960
|
+
const sub = positional2[0];
|
|
961
|
+
if (sub === "remove") {
|
|
962
|
+
const name = positional2[1];
|
|
963
|
+
if (!name) {
|
|
964
|
+
if (json) printJsonError("missing_argument", "usage: tapd import remove <name>");
|
|
965
|
+
else console.error(kleur.red("error:"), "usage: tapd import remove <name>");
|
|
966
|
+
process.exit(2);
|
|
967
|
+
}
|
|
968
|
+
let accessToken2 = null;
|
|
969
|
+
try {
|
|
970
|
+
accessToken2 = readHubToken();
|
|
971
|
+
} catch {
|
|
972
|
+
accessToken2 = null;
|
|
973
|
+
}
|
|
974
|
+
const result2 = await removeImport({
|
|
975
|
+
rootDir,
|
|
976
|
+
name,
|
|
977
|
+
purge: flags2.get("purge") === true,
|
|
978
|
+
accessToken: accessToken2
|
|
979
|
+
});
|
|
980
|
+
if (json) {
|
|
981
|
+
printJson({ protocolVersion: PROTOCOL_VERSION, ...result2, name });
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const C2 = kleur;
|
|
985
|
+
if (!result2.removed) {
|
|
986
|
+
console.log(C2.dim(`no import named "${name}"`));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
console.log(`${C2.green("\u2713")} removed import ${C2.bold(name)}${result2.purged ? C2.dim(" (purged files)") : ""}`);
|
|
990
|
+
switch (result2.revoke) {
|
|
991
|
+
case "revoked":
|
|
992
|
+
console.log(C2.dim(" grant relinquished on the relay."));
|
|
993
|
+
break;
|
|
994
|
+
case "not-creator":
|
|
995
|
+
console.log(
|
|
996
|
+
C2.yellow(" note:") + " the grant belongs to another user and remains active \u2014 only its owner can revoke it."
|
|
997
|
+
);
|
|
998
|
+
break;
|
|
999
|
+
case "no-token":
|
|
1000
|
+
console.log(C2.yellow(" note:") + " not signed in \u2014 the grant was not revoked. Run `rig login`, then re-remove, to relinquish it.");
|
|
1001
|
+
break;
|
|
1002
|
+
case "offline":
|
|
1003
|
+
console.log(C2.yellow(" note:") + " relay unreachable \u2014 local mount removed, grant not yet revoked.");
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
console.log(C2.dim(" restart the daemon to apply: `tapd start`"));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const inviteUrl = sub;
|
|
1010
|
+
const mountPath = flags2.get("path");
|
|
1011
|
+
if (!inviteUrl || !mountPath) {
|
|
1012
|
+
const msg = "usage: tapd import <invite-url> --path <subdir> [--name <name>]";
|
|
1013
|
+
if (json) printJsonError("missing_argument", msg);
|
|
1014
|
+
else console.error(kleur.red("error:"), msg);
|
|
1015
|
+
process.exit(2);
|
|
1016
|
+
}
|
|
1017
|
+
const anonymous = flags2.get("anonymous") === true;
|
|
1018
|
+
let accessToken = null;
|
|
1019
|
+
if (!anonymous) {
|
|
1020
|
+
try {
|
|
1021
|
+
accessToken = readHubToken();
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
if (err instanceof HubTokenMissingError) {
|
|
1024
|
+
if (json) printJsonError("not_logged_in", err.message);
|
|
1025
|
+
else {
|
|
1026
|
+
console.error(kleur.red("error:"), err.message);
|
|
1027
|
+
console.error(kleur.dim(" pass --anonymous for a pure-capability invite, or run `rig login`."));
|
|
1028
|
+
}
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
throw err;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
let result;
|
|
1035
|
+
try {
|
|
1036
|
+
result = await addImport({
|
|
1037
|
+
rootDir,
|
|
1038
|
+
inviteUrl,
|
|
1039
|
+
path: mountPath,
|
|
1040
|
+
name: flags2.get("name"),
|
|
1041
|
+
accessToken,
|
|
1042
|
+
deviceLabel: flags2.get("device-label")
|
|
1043
|
+
});
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
if (err instanceof ImportValidationFailed) {
|
|
1046
|
+
if (json) printJsonError("import_invalid", err.message);
|
|
1047
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1048
|
+
process.exit(2);
|
|
1049
|
+
}
|
|
1050
|
+
if (err instanceof ImportTokenScopeError) {
|
|
1051
|
+
if (json) printJsonError("insufficient_ops", err.message);
|
|
1052
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
if (err instanceof InvalidInviteUrlError) {
|
|
1056
|
+
if (json) printJsonError("invalid_invite_url", err.message);
|
|
1057
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1058
|
+
process.exit(2);
|
|
1059
|
+
}
|
|
1060
|
+
throw err;
|
|
1061
|
+
}
|
|
1062
|
+
if (json) {
|
|
1063
|
+
printJson({ protocolVersion: PROTOCOL_VERSION, ...result });
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const C = kleur;
|
|
1067
|
+
console.log(C.bold().cyan("tapd import"));
|
|
1068
|
+
console.log(` name: ${C.bold(result.name)}`);
|
|
1069
|
+
console.log(` mount: ${result.path} ${C.dim("(read-only mirror)")}`);
|
|
1070
|
+
console.log(` binding: ${result.bindingId}`);
|
|
1071
|
+
console.log(` ops: ${result.ops.join(", ")}`);
|
|
1072
|
+
console.log(C.dim(` Token stored in ${rootDir}/.rig/tap/imports/${result.name}.local.json (mode 0600).`));
|
|
1073
|
+
if (result.importerBindingVerified === false) {
|
|
1074
|
+
console.log(
|
|
1075
|
+
C.yellow(" note:") + " recorded as a personal import (you \u2192 source); the rig-level edge needs membership in this meta-rig."
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
console.log(C.dim(" Next: (re)start the daemon to begin mirroring \u2014 `tapd start`."));
|
|
1079
|
+
}
|
|
600
1080
|
async function runStatus(flags2) {
|
|
601
1081
|
const json = flags2.get("json") === true;
|
|
602
|
-
const rootDir =
|
|
1082
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
603
1083
|
let report;
|
|
604
1084
|
try {
|
|
605
1085
|
report = await status({ rootDir });
|
|
@@ -619,37 +1099,51 @@ async function runStatus(flags2) {
|
|
|
619
1099
|
}
|
|
620
1100
|
const C = kleur;
|
|
621
1101
|
console.log(C.bold().cyan("tapd status"));
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1102
|
+
if (report.config) {
|
|
1103
|
+
console.log(` binding: ${C.bold(report.config.bindingId)}`);
|
|
1104
|
+
console.log(` relay: ${report.config.relayUrl}`);
|
|
1105
|
+
console.log(` device: ${C.bold(report.config.deviceId)}`);
|
|
1106
|
+
console.log(` cursor: ${report.cursor}`);
|
|
1107
|
+
console.log(` tracked: ${report.trackedPaths} paths`);
|
|
1108
|
+
if (report.pendingPeek.length === 0) {
|
|
1109
|
+
console.log(` pending: ${C.green("up to date")}`);
|
|
1110
|
+
} else {
|
|
1111
|
+
console.log(` pending: ${C.yellow(`${report.pendingPeek.length}+ events behind`)}`);
|
|
1112
|
+
for (const ev of report.pendingPeek) {
|
|
1113
|
+
console.log(C.dim(` ${ev.op.padEnd(6)} ${ev.path} (${ev.id})`));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (report.conflictCount > 0) {
|
|
1117
|
+
console.log(
|
|
1118
|
+
` conflicts: ${C.yellow(String(report.conflictCount))} ${C.dim("(sidecars at *.conflict-from.*)")}`
|
|
1119
|
+
);
|
|
1120
|
+
} else {
|
|
1121
|
+
console.log(` conflicts: ${C.dim("none")}`);
|
|
1122
|
+
}
|
|
1123
|
+
if (report.lastApplyError) {
|
|
1124
|
+
const e = report.lastApplyError;
|
|
1125
|
+
console.log(
|
|
1126
|
+
` last err: ${C.red(e.reason)} ${C.dim(`(${e.op} ${e.path} @ ${e.eventId}${e.at ? `, ${e.at}` : ""})`)}`
|
|
1127
|
+
);
|
|
633
1128
|
}
|
|
634
|
-
}
|
|
635
|
-
if (report.conflictCount > 0) {
|
|
636
|
-
console.log(
|
|
637
|
-
` conflicts: ${C.yellow(String(report.conflictCount))} ${C.dim("(sidecars at *.conflict-from.*)")}`
|
|
638
|
-
);
|
|
639
1129
|
} else {
|
|
640
|
-
console.log(
|
|
1130
|
+
console.log(C.dim(" (import-only meta-rig \u2014 no primary binding)"));
|
|
641
1131
|
}
|
|
642
|
-
if (report.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
);
|
|
1132
|
+
if (report.mounts.length > 0) {
|
|
1133
|
+
console.log(` imports:`);
|
|
1134
|
+
for (const m of report.mounts) {
|
|
1135
|
+
if (m.state === "ok") {
|
|
1136
|
+
console.log(` ${C.green("\u25CF")} ${C.bold(m.name)} \u2192 ${m.path}${m.cursor ? C.dim(` (${m.cursor})`) : ""}`);
|
|
1137
|
+
} else {
|
|
1138
|
+
console.log(` ${C.yellow("\u25CB")} ${C.bold(m.name)} ${C.yellow("misconfigured")} ${C.dim(`(${m.reason ?? "unknown"})`)}`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
647
1141
|
}
|
|
648
1142
|
}
|
|
649
1143
|
async function runUninit(flags2) {
|
|
650
1144
|
const json = flags2.get("json") === true;
|
|
651
1145
|
const purge = flags2.get("purge") === true;
|
|
652
|
-
const rootDir =
|
|
1146
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
653
1147
|
let result;
|
|
654
1148
|
try {
|
|
655
1149
|
result = uninit({ rootDir, purge });
|
|
@@ -706,7 +1200,7 @@ async function runUninit(flags2) {
|
|
|
706
1200
|
}
|
|
707
1201
|
}
|
|
708
1202
|
async function runStart(flags2) {
|
|
709
|
-
const rootDir =
|
|
1203
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
710
1204
|
const pollSeconds = flags2.has("poll-seconds") ? Number(flags2.get("poll-seconds")) : void 0;
|
|
711
1205
|
const pidfilePath = flags2.get("no-pidfile") === true ? null : pathJoin(rootDir, DAEMON_PIDFILE_REL3);
|
|
712
1206
|
let handle = null;
|
|
@@ -750,26 +1244,136 @@ async function runStart(flags2) {
|
|
|
750
1244
|
if (pidfilePath) console.log(` pid: ${process.pid} ${kleur.dim(`(${pidfilePath})`)}`);
|
|
751
1245
|
console.log(kleur.dim(" press ctrl+c to stop"));
|
|
752
1246
|
}
|
|
1247
|
+
async function runHistory(flags2) {
|
|
1248
|
+
const json = flags2.get("json") === true;
|
|
1249
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
1250
|
+
const path = flags2.get("path");
|
|
1251
|
+
const asOf = flags2.get("as-of");
|
|
1252
|
+
if (!path) {
|
|
1253
|
+
if (json) printJsonError("missing_flag", "--path is required");
|
|
1254
|
+
else console.error(kleur.red("error:"), "--path is required (e.g. theses/foo.md)");
|
|
1255
|
+
process.exit(2);
|
|
1256
|
+
}
|
|
1257
|
+
let events;
|
|
1258
|
+
try {
|
|
1259
|
+
events = await history({
|
|
1260
|
+
rootDir,
|
|
1261
|
+
path,
|
|
1262
|
+
...asOf ? { untilInclusive: asOf } : {}
|
|
1263
|
+
});
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
if (err instanceof NotInitializedError) {
|
|
1266
|
+
if (json) printJsonError("not_initialized", err.message);
|
|
1267
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
}
|
|
1270
|
+
if (json) printJsonError("history_failed", err.message);
|
|
1271
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
if (json) {
|
|
1275
|
+
printJson({
|
|
1276
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1277
|
+
path,
|
|
1278
|
+
events
|
|
1279
|
+
});
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const C = kleur;
|
|
1283
|
+
console.log(C.bold().cyan("tapd history") + " " + C.dim(path));
|
|
1284
|
+
if (events.length === 0) {
|
|
1285
|
+
console.log(C.dim(" (no events \u2014 never written through this binding)"));
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
for (const ev of events) {
|
|
1289
|
+
const dot = ev.op === "delete" ? C.red("\u2022") : ev.op === "write" ? C.green("\u2022") : C.dim("\u2022");
|
|
1290
|
+
const sizeStr = ev.size != null ? `${ev.size}B` : "";
|
|
1291
|
+
const actor = ev.actorUserId ? C.dim(`by ${ev.actorUserId}`) : "";
|
|
1292
|
+
console.log(` ${dot} ${C.bold(ev.id.padEnd(10))} ${ev.op.padEnd(6)} ${C.dim(ev.createdAt)} ${sizeStr.padEnd(8)} ${actor}`);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
async function runRestore(flags2) {
|
|
1296
|
+
const json = flags2.get("json") === true;
|
|
1297
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
1298
|
+
const path = flags2.get("path");
|
|
1299
|
+
const asOf = flags2.get("as-of");
|
|
1300
|
+
if (!path) {
|
|
1301
|
+
if (json) printJsonError("missing_flag", "--path is required");
|
|
1302
|
+
else console.error(kleur.red("error:"), "--path is required");
|
|
1303
|
+
process.exit(2);
|
|
1304
|
+
}
|
|
1305
|
+
let result;
|
|
1306
|
+
try {
|
|
1307
|
+
result = await restore({
|
|
1308
|
+
rootDir,
|
|
1309
|
+
path,
|
|
1310
|
+
...asOf ? { asOf } : {}
|
|
1311
|
+
});
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
if (err instanceof NotInitializedError) {
|
|
1314
|
+
if (json) printJsonError("not_initialized", err.message);
|
|
1315
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
if (err instanceof PathNeverExistedError) {
|
|
1319
|
+
if (json) printJsonError("path_never_existed", err.message);
|
|
1320
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
}
|
|
1323
|
+
if (err instanceof PathCurrentlyDeletedError) {
|
|
1324
|
+
if (json) printJsonError("path_currently_deleted", err.message);
|
|
1325
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
}
|
|
1328
|
+
if (err instanceof HashMismatchError) {
|
|
1329
|
+
if (json) printJsonError("hash_mismatch", err.message);
|
|
1330
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1331
|
+
process.exit(1);
|
|
1332
|
+
}
|
|
1333
|
+
if (json) printJsonError("restore_failed", err.message);
|
|
1334
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1335
|
+
process.exit(1);
|
|
1336
|
+
}
|
|
1337
|
+
if (json) {
|
|
1338
|
+
printJson({
|
|
1339
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1340
|
+
...result
|
|
1341
|
+
});
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const C = kleur;
|
|
1345
|
+
console.log(
|
|
1346
|
+
C.green("\u2713 ") + `restored ${C.bold(path)} from ${C.bold(result.restoredFromCursor)} ` + C.dim(`(${result.size}B, hash ${result.hash.slice(0, 16)}\u2026)`)
|
|
1347
|
+
);
|
|
1348
|
+
if (result.overwrote) {
|
|
1349
|
+
console.log(C.dim(" (overwrote local file; daemon will resync to other devices)"));
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
753
1352
|
async function runInit(flags2) {
|
|
754
1353
|
const json = flags2.get("json") === true;
|
|
755
1354
|
const name = flags2.get("name");
|
|
756
|
-
const ownerUserId = flags2.get("owner-user-id");
|
|
757
1355
|
if (!name) {
|
|
758
1356
|
if (json) printJsonError("missing_flag", "--name is required");
|
|
759
1357
|
else console.error(kleur.red("error:"), "--name is required");
|
|
760
1358
|
process.exit(2);
|
|
761
1359
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1360
|
+
let accessToken;
|
|
1361
|
+
try {
|
|
1362
|
+
accessToken = readHubToken();
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
if (err instanceof HubTokenMissingError) {
|
|
1365
|
+
if (json) printJsonError("not_logged_in", err.message);
|
|
1366
|
+
else console.error(kleur.red("error:"), err.message);
|
|
1367
|
+
process.exit(1);
|
|
1368
|
+
}
|
|
1369
|
+
throw err;
|
|
766
1370
|
}
|
|
767
|
-
const
|
|
768
|
-
const
|
|
1371
|
+
const relayUrl = flags2.get("relay") ?? process.env.RIG_RELAY_URL ?? DEFAULT_RELAY_URL;
|
|
1372
|
+
const rootDir = resolve2(flags2.get("dir") ?? process.cwd());
|
|
769
1373
|
const deviceLabel = flags2.get("device-label");
|
|
770
1374
|
let result;
|
|
771
1375
|
try {
|
|
772
|
-
result = await init({ rootDir, relayUrl,
|
|
1376
|
+
result = await init({ rootDir, relayUrl, accessToken, bindingName: name, deviceLabel });
|
|
773
1377
|
} catch (err) {
|
|
774
1378
|
if (err instanceof AlreadyInitializedError) {
|
|
775
1379
|
if (json) printJsonError("already_initialized", err.message);
|
|
@@ -858,15 +1462,27 @@ if (flags.get("version") === true || command === "-v") {
|
|
|
858
1462
|
case "invite":
|
|
859
1463
|
await runInvite(positional, flags);
|
|
860
1464
|
return;
|
|
1465
|
+
case "members":
|
|
1466
|
+
await runMembers(positional, flags);
|
|
1467
|
+
return;
|
|
861
1468
|
case "join":
|
|
862
1469
|
await runJoin(positional, flags);
|
|
863
1470
|
return;
|
|
1471
|
+
case "import":
|
|
1472
|
+
await runImport(positional, flags);
|
|
1473
|
+
return;
|
|
864
1474
|
case "start":
|
|
865
1475
|
await runStart(flags);
|
|
866
1476
|
return;
|
|
867
1477
|
case "status":
|
|
868
1478
|
await runStatus(flags);
|
|
869
1479
|
return;
|
|
1480
|
+
case "history":
|
|
1481
|
+
await runHistory(flags);
|
|
1482
|
+
return;
|
|
1483
|
+
case "restore":
|
|
1484
|
+
await runRestore(flags);
|
|
1485
|
+
return;
|
|
870
1486
|
case "uninit":
|
|
871
1487
|
await runUninit(flags);
|
|
872
1488
|
return;
|