papermark 0.0.1 → 0.2.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/README.md +85 -5
- package/dist/index.js +2216 -0
- package/dist/index.js.map +1 -0
- package/docs/CONTRACT_V1.md +108 -0
- package/openai.yaml +56 -0
- package/package.json +56 -10
- package/skills/papermark/SKILL.md +99 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command2 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/analytics.ts
|
|
7
|
+
import chalk2 from "chalk";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import Conf from "conf";
|
|
12
|
+
var store = new Conf({
|
|
13
|
+
projectName: "papermark",
|
|
14
|
+
configFileMode: 384,
|
|
15
|
+
defaults: {
|
|
16
|
+
apiUrl: "https://api.papermark.com"
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
var credentialsFile;
|
|
20
|
+
function readCredentialsFile() {
|
|
21
|
+
if (credentialsFile !== void 0) return credentialsFile;
|
|
22
|
+
const path = process.env.PAPERMARK_CREDENTIALS_FILE;
|
|
23
|
+
if (!path) return credentialsFile = null;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
+
if (parsed && typeof parsed === "object") {
|
|
27
|
+
return credentialsFile = {
|
|
28
|
+
token: typeof parsed.token === "string" ? parsed.token : void 0,
|
|
29
|
+
apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : typeof parsed.api_url === "string" ? parsed.api_url : void 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
process.stderr.write(
|
|
34
|
+
`warning: could not read PAPERMARK_CREDENTIALS_FILE (${path}): ${err instanceof Error ? err.message : String(err)}
|
|
35
|
+
`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return credentialsFile = null;
|
|
39
|
+
}
|
|
40
|
+
function envOr(name) {
|
|
41
|
+
const v = process.env[name];
|
|
42
|
+
return v && v.length > 0 ? v : void 0;
|
|
43
|
+
}
|
|
44
|
+
function getToken() {
|
|
45
|
+
return envOr("PAPERMARK_TOKEN") ?? readCredentialsFile()?.token ?? store.get("token");
|
|
46
|
+
}
|
|
47
|
+
function getTokenSource() {
|
|
48
|
+
if (envOr("PAPERMARK_TOKEN")) return "env";
|
|
49
|
+
if (readCredentialsFile()?.token) return "credentials-file";
|
|
50
|
+
if (store.get("token")) return "config";
|
|
51
|
+
return "none";
|
|
52
|
+
}
|
|
53
|
+
function setToken(token) {
|
|
54
|
+
store.set("token", token);
|
|
55
|
+
}
|
|
56
|
+
function clearToken() {
|
|
57
|
+
store.delete("token");
|
|
58
|
+
}
|
|
59
|
+
function getApiUrl() {
|
|
60
|
+
return envOr("PAPERMARK_API_URL") ?? readCredentialsFile()?.apiUrl ?? store.get("apiUrl") ?? "https://api.papermark.com";
|
|
61
|
+
}
|
|
62
|
+
function setApiUrl(url) {
|
|
63
|
+
store.set("apiUrl", url);
|
|
64
|
+
}
|
|
65
|
+
function getAll() {
|
|
66
|
+
return { token: store.get("token"), apiUrl: store.get("apiUrl") };
|
|
67
|
+
}
|
|
68
|
+
function configPath() {
|
|
69
|
+
return store.path;
|
|
70
|
+
}
|
|
71
|
+
function deleteKey(key) {
|
|
72
|
+
store.delete(key);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/debug.ts
|
|
76
|
+
var enabled = process.env.PAPERMARK_DEBUG === "1" || process.env.PAPERMARK_DEBUG === "true";
|
|
77
|
+
function debug(category, ...parts) {
|
|
78
|
+
if (!enabled) return;
|
|
79
|
+
const line = parts.map((p) => typeof p === "string" ? p : JSON.stringify(p)).join(" ");
|
|
80
|
+
process.stderr.write(`[papermark:${category}] ${line}
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/runtime.ts
|
|
85
|
+
var current = {
|
|
86
|
+
json: !process.stdout.isTTY,
|
|
87
|
+
dryRun: false,
|
|
88
|
+
color: process.stdout.isTTY && !process.env.NO_COLOR
|
|
89
|
+
};
|
|
90
|
+
function setRuntime(patch) {
|
|
91
|
+
current = { ...current, ...patch };
|
|
92
|
+
}
|
|
93
|
+
function getRuntime() {
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/client.ts
|
|
98
|
+
var ApiError = class extends Error {
|
|
99
|
+
code;
|
|
100
|
+
status;
|
|
101
|
+
docUrl;
|
|
102
|
+
details;
|
|
103
|
+
constructor(status, body) {
|
|
104
|
+
const message = typeof body === "string" ? body : body.message;
|
|
105
|
+
super(message);
|
|
106
|
+
this.status = status;
|
|
107
|
+
if (typeof body === "string") {
|
|
108
|
+
this.code = "unknown";
|
|
109
|
+
} else {
|
|
110
|
+
this.code = body.code;
|
|
111
|
+
this.docUrl = body.doc_url;
|
|
112
|
+
this.details = body.details;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var AuthRequiredError = class extends Error {
|
|
117
|
+
constructor() {
|
|
118
|
+
super("No API token found. Run `papermark login` first.");
|
|
119
|
+
this.name = "AuthRequiredError";
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var NetworkError = class extends Error {
|
|
123
|
+
url;
|
|
124
|
+
cause;
|
|
125
|
+
constructor(url, cause) {
|
|
126
|
+
const reason = extractNetworkReason(cause);
|
|
127
|
+
super(`Network request to ${url} failed: ${reason}`);
|
|
128
|
+
this.name = "NetworkError";
|
|
129
|
+
this.url = url;
|
|
130
|
+
this.cause = cause;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
function extractNetworkReason(err) {
|
|
134
|
+
if (err && typeof err === "object" && "cause" in err) {
|
|
135
|
+
const c = err.cause;
|
|
136
|
+
if (c && typeof c === "object") {
|
|
137
|
+
const code = c.code;
|
|
138
|
+
const msg = c.message;
|
|
139
|
+
if (code || msg) return `${code ?? ""}${code && msg ? " \u2014 " : ""}${msg ?? ""}`.trim();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (err instanceof Error) return err.message;
|
|
143
|
+
return String(err);
|
|
144
|
+
}
|
|
145
|
+
function buildUrl(path, query) {
|
|
146
|
+
const base = getApiUrl().replace(/\/$/, "");
|
|
147
|
+
let fullPath = path.startsWith("/") ? path : `/${path}`;
|
|
148
|
+
if (base.endsWith("/api") && fullPath.startsWith("/api/")) {
|
|
149
|
+
fullPath = fullPath.slice(4);
|
|
150
|
+
}
|
|
151
|
+
const url = new URL(base + fullPath);
|
|
152
|
+
if (query) {
|
|
153
|
+
for (const [k, v] of Object.entries(query)) {
|
|
154
|
+
if (v !== void 0 && v !== null && v !== "") {
|
|
155
|
+
url.searchParams.set(k, String(v));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return url.toString();
|
|
160
|
+
}
|
|
161
|
+
async function request(path, opts = {}) {
|
|
162
|
+
const headers = {
|
|
163
|
+
Accept: "application/json",
|
|
164
|
+
...opts.headers ?? {}
|
|
165
|
+
};
|
|
166
|
+
if (!opts.anonymous) {
|
|
167
|
+
const token = getToken();
|
|
168
|
+
if (token) {
|
|
169
|
+
headers.Authorization = `Bearer ${token}`;
|
|
170
|
+
} else if (!getRuntime().dryRun) {
|
|
171
|
+
throw new AuthRequiredError();
|
|
172
|
+
} else {
|
|
173
|
+
headers.Authorization = "Bearer <no-token>";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let body;
|
|
177
|
+
if (opts.body !== void 0) {
|
|
178
|
+
if (opts.body instanceof Uint8Array || opts.body instanceof ArrayBuffer) {
|
|
179
|
+
body = opts.body;
|
|
180
|
+
} else {
|
|
181
|
+
body = JSON.stringify(opts.body);
|
|
182
|
+
headers["Content-Type"] = "application/json";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const url = buildUrl(path, opts.query);
|
|
186
|
+
const method = opts.method ?? (opts.body ? "POST" : "GET");
|
|
187
|
+
if (getRuntime().dryRun) {
|
|
188
|
+
const redactedHeaders = { ...headers };
|
|
189
|
+
if (redactedHeaders.Authorization) {
|
|
190
|
+
redactedHeaders.Authorization = "Bearer ***redacted***";
|
|
191
|
+
}
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
`[dry-run] ${method} ${url}
|
|
194
|
+
` + Object.entries(redactedHeaders).map(([k, v]) => `[dry-run] ${k}: ${v}`).join("\n") + (body ? `
|
|
195
|
+
[dry-run] body: ${truncate(String(body), 500)}` : "") + "\n"
|
|
196
|
+
);
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
debug("http", method, url, body ? "(body)" : "");
|
|
200
|
+
const started = Date.now();
|
|
201
|
+
let res;
|
|
202
|
+
try {
|
|
203
|
+
res = await fetch(url, { method, headers, body });
|
|
204
|
+
} catch (err) {
|
|
205
|
+
debug("http", "network error after", `${Date.now() - started}ms`, String(err));
|
|
206
|
+
throw new NetworkError(url, err);
|
|
207
|
+
}
|
|
208
|
+
debug("http", method, url, `\u2192 ${res.status}`, `(${Date.now() - started}ms)`);
|
|
209
|
+
if (res.status === 204) return void 0;
|
|
210
|
+
const text = await res.text();
|
|
211
|
+
const parsed = text ? safeParseJson(text) : null;
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
214
|
+
throw new ApiError(res.status, parsed.error);
|
|
215
|
+
}
|
|
216
|
+
const looksLikeHtml = text.trimStart().toLowerCase().startsWith("<");
|
|
217
|
+
const msg = looksLikeHtml ? `HTTP ${res.status} (server returned HTML, not JSON \u2014 is the dev server healthy?)` : text || `HTTP ${res.status}`;
|
|
218
|
+
throw new ApiError(res.status, msg);
|
|
219
|
+
}
|
|
220
|
+
if (parsed === null && text.trim() !== "") {
|
|
221
|
+
const looksLikeHtml = text.trimStart().toLowerCase().startsWith("<");
|
|
222
|
+
const msg = looksLikeHtml ? `HTTP ${res.status} (server returned HTML on a 2xx \u2014 is the API URL pointing at the right host?)` : `HTTP ${res.status} (response was not JSON: ${truncate(text, 200)})`;
|
|
223
|
+
throw new ApiError(res.status, msg);
|
|
224
|
+
}
|
|
225
|
+
return parsed ?? void 0;
|
|
226
|
+
}
|
|
227
|
+
function safeParseJson(s) {
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(s);
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function truncate(s, n) {
|
|
235
|
+
return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
|
|
236
|
+
}
|
|
237
|
+
var api = {
|
|
238
|
+
// Documents
|
|
239
|
+
listDocuments: (query) => request("/v1/documents", { query }),
|
|
240
|
+
getDocument: (id) => request(`/v1/documents/${id}`),
|
|
241
|
+
searchDocuments: (q, limit) => request("/v1/documents/search", { query: { q, limit } }),
|
|
242
|
+
updateDocument: (id, body) => request(`/v1/documents/${id}`, { method: "PATCH", body }),
|
|
243
|
+
deleteDocument: (id) => request(`/v1/documents/${id}`, { method: "DELETE" }),
|
|
244
|
+
uploadUrl: (body) => request("/v1/documents/upload-url", { method: "POST", body }),
|
|
245
|
+
createDocument: (body) => request("/v1/documents", { method: "POST", body }),
|
|
246
|
+
listDocumentVersions: (id) => request(`/v1/documents/${id}/versions`),
|
|
247
|
+
addDocumentVersion: (id, body) => request(`/v1/documents/${id}/versions`, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
body
|
|
250
|
+
}),
|
|
251
|
+
getDocumentVersion: (id, vid) => request(`/v1/documents/${id}/versions/${vid}`),
|
|
252
|
+
promoteDocumentVersion: (id, vid) => request(`/v1/documents/${id}/versions/${vid}`, {
|
|
253
|
+
method: "PATCH",
|
|
254
|
+
body: { is_primary: true }
|
|
255
|
+
}),
|
|
256
|
+
// Folders
|
|
257
|
+
listFolders: (query) => request("/v1/folders", { query }),
|
|
258
|
+
createFolder: (body) => request("/v1/folders", { method: "POST", body }),
|
|
259
|
+
getFolder: (id) => request(`/v1/folders/${id}`),
|
|
260
|
+
updateFolder: (id, body) => request(`/v1/folders/${id}`, { method: "PATCH", body }),
|
|
261
|
+
deleteFolder: (id, opts) => request(`/v1/folders/${id}`, {
|
|
262
|
+
method: "DELETE",
|
|
263
|
+
query: opts?.cascade ? { cascade: "true" } : void 0
|
|
264
|
+
}),
|
|
265
|
+
moveFolder: (id, body) => request(`/v1/folders/${id}/move`, { method: "POST", body }),
|
|
266
|
+
// Links
|
|
267
|
+
listLinks: (query) => request("/v1/links", { query }),
|
|
268
|
+
getLink: (id) => request(`/v1/links/${id}`),
|
|
269
|
+
createLink: (body) => request("/v1/links", { method: "POST", body }),
|
|
270
|
+
updateLink: (id, body) => request(`/v1/links/${id}`, { method: "PATCH", body }),
|
|
271
|
+
deleteLink: (id) => request(`/v1/links/${id}`, { method: "DELETE" }),
|
|
272
|
+
// Views
|
|
273
|
+
listViews: (linkId, query) => request(`/v1/links/${linkId}/views`, { query }),
|
|
274
|
+
// Datarooms
|
|
275
|
+
listDatarooms: (query) => request("/v1/datarooms", { query }),
|
|
276
|
+
getDataroom: (id) => request(`/v1/datarooms/${id}`),
|
|
277
|
+
createDataroom: (body) => request("/v1/datarooms", { method: "POST", body }),
|
|
278
|
+
updateDataroom: (id, body) => request(`/v1/datarooms/${id}`, { method: "PATCH", body }),
|
|
279
|
+
deleteDataroom: (id) => request(`/v1/datarooms/${id}`, { method: "DELETE" }),
|
|
280
|
+
listDataroomDocuments: (id, query) => request(`/v1/datarooms/${id}/documents`, { query }),
|
|
281
|
+
attachDataroomDocument: (id, body) => request(`/v1/datarooms/${id}/documents`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
body
|
|
284
|
+
}),
|
|
285
|
+
// Dataroom folders
|
|
286
|
+
listDataroomFolders: (id, query) => request(`/v1/datarooms/${id}/folders`, { query }),
|
|
287
|
+
createDataroomFolder: (id, body) => request(`/v1/datarooms/${id}/folders`, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
body
|
|
290
|
+
}),
|
|
291
|
+
getDataroomFolder: (id, fid) => request(`/v1/datarooms/${id}/folders/${fid}`),
|
|
292
|
+
updateDataroomFolder: (id, fid, body) => request(`/v1/datarooms/${id}/folders/${fid}`, {
|
|
293
|
+
method: "PATCH",
|
|
294
|
+
body
|
|
295
|
+
}),
|
|
296
|
+
deleteDataroomFolder: (id, fid, opts) => request(`/v1/datarooms/${id}/folders/${fid}`, {
|
|
297
|
+
method: "DELETE",
|
|
298
|
+
query: opts?.cascade ? { cascade: "true" } : void 0
|
|
299
|
+
}),
|
|
300
|
+
moveDataroomFolder: (id, fid, body) => request(`/v1/datarooms/${id}/folders/${fid}/move`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
body
|
|
303
|
+
}),
|
|
304
|
+
// Visitors
|
|
305
|
+
listVisitors: (query) => request("/v1/visitors", { query }),
|
|
306
|
+
getVisitor: (id) => request(`/v1/visitors/${id}`),
|
|
307
|
+
listVisitorViews: (id, query) => request(`/v1/visitors/${id}/views`, { query }),
|
|
308
|
+
// Analytics
|
|
309
|
+
documentAnalytics: (id, query) => request(`/v1/analytics/documents/${id}`, { query }),
|
|
310
|
+
linkAnalytics: (id, query) => request(`/v1/analytics/links/${id}`, { query }),
|
|
311
|
+
dataroomAnalytics: (id, query) => request(`/v1/analytics/datarooms/${id}`, { query }),
|
|
312
|
+
viewAnalytics: (id) => request(`/v1/analytics/views/${id}`)
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/output.ts
|
|
316
|
+
import chalk from "chalk";
|
|
317
|
+
|
|
318
|
+
// src/exit.ts
|
|
319
|
+
var ExitCode = {
|
|
320
|
+
Ok: 0,
|
|
321
|
+
/** Upstream API returned 4xx/5xx that isn't auth or validation. */
|
|
322
|
+
ApiError: 1,
|
|
323
|
+
/** No token, expired token, invalid token, missing scope. */
|
|
324
|
+
AuthError: 2,
|
|
325
|
+
/** Client-side input validation, or server-side 422. */
|
|
326
|
+
ValidationError: 3,
|
|
327
|
+
/** Network/DNS/connection refused, bad URL, etc. */
|
|
328
|
+
NetworkError: 4,
|
|
329
|
+
/** Unexpected — bug in the CLI itself. */
|
|
330
|
+
Internal: 5
|
|
331
|
+
};
|
|
332
|
+
var EXIT_CODE_LEGEND = {
|
|
333
|
+
[ExitCode.Ok]: "success",
|
|
334
|
+
[ExitCode.ApiError]: "API error (4xx/5xx)",
|
|
335
|
+
[ExitCode.AuthError]: "authentication error",
|
|
336
|
+
[ExitCode.ValidationError]: "validation error",
|
|
337
|
+
[ExitCode.NetworkError]: "network error",
|
|
338
|
+
[ExitCode.Internal]: "internal CLI error"
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// src/output.ts
|
|
342
|
+
var ValidationError = class extends Error {
|
|
343
|
+
details;
|
|
344
|
+
constructor(message, details) {
|
|
345
|
+
super(message);
|
|
346
|
+
this.name = "ValidationError";
|
|
347
|
+
this.details = details;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function formatError(err) {
|
|
351
|
+
if (err instanceof AuthRequiredError) {
|
|
352
|
+
const message = sanitizeForTerminal(err.message);
|
|
353
|
+
return {
|
|
354
|
+
human: chalk.red("\u2717 ") + message,
|
|
355
|
+
json: {
|
|
356
|
+
error: {
|
|
357
|
+
code: "NO_TOKEN",
|
|
358
|
+
message: err.message,
|
|
359
|
+
retryable: false,
|
|
360
|
+
hint: "Run `papermark login` to authenticate."
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
exitCode: ExitCode.AuthError
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (err instanceof NetworkError) {
|
|
367
|
+
const message = sanitizeForTerminal(err.message);
|
|
368
|
+
return {
|
|
369
|
+
human: chalk.red("\u2717 ") + message + chalk.dim(
|
|
370
|
+
"\n Hint: check `papermark config show` and your internet connection."
|
|
371
|
+
),
|
|
372
|
+
json: {
|
|
373
|
+
error: {
|
|
374
|
+
code: "NETWORK_ERROR",
|
|
375
|
+
message: err.message,
|
|
376
|
+
retryable: true,
|
|
377
|
+
hint: "check `papermark config show` and your internet connection"
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
exitCode: ExitCode.NetworkError
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (err instanceof ValidationError) {
|
|
384
|
+
const message = sanitizeForTerminal(err.message);
|
|
385
|
+
return {
|
|
386
|
+
human: chalk.red("\u2717 ") + message,
|
|
387
|
+
json: {
|
|
388
|
+
error: {
|
|
389
|
+
code: "VALIDATION",
|
|
390
|
+
message: err.message,
|
|
391
|
+
retryable: false,
|
|
392
|
+
...err.details ? { details: err.details } : {}
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
exitCode: ExitCode.ValidationError
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (err instanceof ApiError) {
|
|
399
|
+
const hint = hintForApiError(err);
|
|
400
|
+
const safeMessage = sanitizeForTerminal(err.message);
|
|
401
|
+
const safeDocUrl = err.docUrl ? sanitizeForTerminal(err.docUrl) : "";
|
|
402
|
+
const safeDetails = err.details && typeof err.details === "object" ? sanitizeForTerminal(JSON.stringify(err.details)) : "";
|
|
403
|
+
const docs = safeDocUrl ? chalk.dim(`
|
|
404
|
+
See: ${safeDocUrl}`) : "";
|
|
405
|
+
const hintLine = hint ? chalk.dim(`
|
|
406
|
+
Hint: ${hint}`) : "";
|
|
407
|
+
const details = safeDetails ? chalk.dim("\n " + safeDetails) : "";
|
|
408
|
+
return {
|
|
409
|
+
human: chalk.red(`\u2717 [${err.status} ${err.code}] `) + safeMessage + hintLine + docs + details,
|
|
410
|
+
json: {
|
|
411
|
+
error: {
|
|
412
|
+
code: err.code,
|
|
413
|
+
message: err.message,
|
|
414
|
+
status: err.status,
|
|
415
|
+
retryable: isRetryableStatus(err.status),
|
|
416
|
+
...hint ? { hint } : {},
|
|
417
|
+
...err.docUrl ? { doc_url: err.docUrl } : {},
|
|
418
|
+
...err.details ? { details: err.details } : {}
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
exitCode: exitCodeForApiError(err)
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
425
|
+
return {
|
|
426
|
+
human: chalk.red("\u2717 ") + sanitizeForTerminal(rawMessage),
|
|
427
|
+
json: { error: { code: "INTERNAL", message: rawMessage, retryable: false } },
|
|
428
|
+
exitCode: ExitCode.Internal
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function isRetryableStatus(status) {
|
|
432
|
+
if (status === 429) return true;
|
|
433
|
+
if (status >= 500 && status < 600) return true;
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
function hintForApiError(err) {
|
|
437
|
+
if (err.status === 401) return "Run `papermark login` to re-authenticate.";
|
|
438
|
+
if (err.status === 403) {
|
|
439
|
+
return "Token is missing a required scope. Re-login to refresh grants.";
|
|
440
|
+
}
|
|
441
|
+
if (err.status === 429) {
|
|
442
|
+
return "Rate limit exceeded. Check X-RateLimit-* headers and back off.";
|
|
443
|
+
}
|
|
444
|
+
if (err.status === 422) return "Check the input shape \u2014 details below.";
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
function exitCodeForApiError(err) {
|
|
448
|
+
if (err.status === 401) return ExitCode.AuthError;
|
|
449
|
+
if (err.status === 403) return ExitCode.AuthError;
|
|
450
|
+
if (err.status === 422) return ExitCode.ValidationError;
|
|
451
|
+
return ExitCode.ApiError;
|
|
452
|
+
}
|
|
453
|
+
function die(err) {
|
|
454
|
+
const fe = formatError(err);
|
|
455
|
+
if (getRuntime().json) {
|
|
456
|
+
process.stderr.write(
|
|
457
|
+
JSON.stringify({ ok: false, error: fe.json.error }) + "\n"
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
process.stderr.write(fe.human + "\n");
|
|
461
|
+
}
|
|
462
|
+
process.exit(fe.exitCode);
|
|
463
|
+
}
|
|
464
|
+
function emit(payload, human) {
|
|
465
|
+
const rt = getRuntime();
|
|
466
|
+
if (rt.dryRun) return;
|
|
467
|
+
if (rt.json) {
|
|
468
|
+
process.stdout.write(JSON.stringify(wrapSuccess(payload), null, 2) + "\n");
|
|
469
|
+
} else {
|
|
470
|
+
process.stdout.write(human() + "\n");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function wrapSuccess(payload) {
|
|
474
|
+
if (payload !== null && typeof payload === "object" && "data" in payload && "next_cursor" in payload && Array.isArray(payload.data)) {
|
|
475
|
+
const p = payload;
|
|
476
|
+
return { ok: true, data: p.data, meta: { next_cursor: p.next_cursor } };
|
|
477
|
+
}
|
|
478
|
+
return { ok: true, data: payload };
|
|
479
|
+
}
|
|
480
|
+
function table(rows, columns) {
|
|
481
|
+
if (rows.length === 0) return chalk.dim("(no rows)");
|
|
482
|
+
const widths = columns.map(
|
|
483
|
+
(c) => Math.max(c.length, ...rows.map((r) => (r[c] ?? "").length))
|
|
484
|
+
);
|
|
485
|
+
const pad = (s, n) => s + " ".repeat(Math.max(0, n - s.length));
|
|
486
|
+
const header = columns.map((c, i) => chalk.bold(pad(c, widths[i]))).join(" ");
|
|
487
|
+
const sep = columns.map((_, i) => "\u2500".repeat(widths[i])).join(" ");
|
|
488
|
+
const body = rows.map(
|
|
489
|
+
(r) => columns.map((c, i) => pad(r[c] ?? "", widths[i])).join(" ")
|
|
490
|
+
);
|
|
491
|
+
return [header, chalk.dim(sep), ...body].join("\n");
|
|
492
|
+
}
|
|
493
|
+
function json(obj) {
|
|
494
|
+
return JSON.stringify(obj, null, 2);
|
|
495
|
+
}
|
|
496
|
+
function sanitizeForTerminal(input) {
|
|
497
|
+
return input.replace(/\x1B\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1B\][^\x07\x1B]*(\x07|\x1B\\)/g, "").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "").replace(/\x1B/g, "").replace(/[\x80-\x9F]/g, "").replace(/[\u202A-\u202E\u2066-\u2069\u200E\u200F]/g, "").replace(/[\u200B-\u200D\u2060]/g, "");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/commands/analytics.ts
|
|
501
|
+
function registerAnalyticsCommands(program2) {
|
|
502
|
+
const analytics = program2.command("analytics").description("Aggregate analytics (Tinybird-backed, tighter rate limit)");
|
|
503
|
+
analytics.command("document <id>").description(
|
|
504
|
+
"Document analytics: total views, unique viewers, total read time, per-page average duration."
|
|
505
|
+
).option("--since <unix-ms>", "window start (default: 30 days ago)", (v) => Number(v)).option("--until <unix-ms>", "window end (default: now)", (v) => Number(v)).option("--json", "output raw JSON").action(
|
|
506
|
+
async (id, opts) => {
|
|
507
|
+
if (opts.json) setRuntime({ json: true });
|
|
508
|
+
try {
|
|
509
|
+
const stats = await api.documentAnalytics(id, {
|
|
510
|
+
since: opts.since,
|
|
511
|
+
until: opts.until
|
|
512
|
+
});
|
|
513
|
+
emit(stats, () => formatDocumentAnalytics(stats));
|
|
514
|
+
} catch (err) {
|
|
515
|
+
die(err);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
analytics.command("link <id>").description("Aggregate analytics for a single share link.").option("--since <unix-ms>", "window start (default: 30 days ago)", (v) => Number(v)).option("--until <unix-ms>", "window end (default: now)", (v) => Number(v)).option("--json", "output raw JSON").action(
|
|
520
|
+
async (id, opts) => {
|
|
521
|
+
if (opts.json) setRuntime({ json: true });
|
|
522
|
+
try {
|
|
523
|
+
const stats = await api.linkAnalytics(id, {
|
|
524
|
+
since: opts.since,
|
|
525
|
+
until: opts.until
|
|
526
|
+
});
|
|
527
|
+
emit(stats, () => formatLinkAnalytics(stats));
|
|
528
|
+
} catch (err) {
|
|
529
|
+
die(err);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
analytics.command("dataroom <id>").description("Aggregate analytics for a dataroom (alias of `papermark dr stats`).").option("--since <unix-ms>", "window start (default: 30 days ago)", (v) => Number(v)).option("--until <unix-ms>", "window end (default: now)", (v) => Number(v)).option("--json", "output raw JSON").action(
|
|
534
|
+
async (id, opts) => {
|
|
535
|
+
if (opts.json) setRuntime({ json: true });
|
|
536
|
+
try {
|
|
537
|
+
const stats = await api.dataroomAnalytics(id, {
|
|
538
|
+
since: opts.since,
|
|
539
|
+
until: opts.until
|
|
540
|
+
});
|
|
541
|
+
emit(stats, () => formatDataroomAnalytics(stats));
|
|
542
|
+
} catch (err) {
|
|
543
|
+
die(err);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
analytics.command("view <id>").description(
|
|
548
|
+
"Per-view breakdown: page-by-page time spent, total duration, viewer geo + client."
|
|
549
|
+
).option("--json", "output raw JSON").action(async (id, opts) => {
|
|
550
|
+
if (opts.json) setRuntime({ json: true });
|
|
551
|
+
try {
|
|
552
|
+
const stats = await api.viewAnalytics(id);
|
|
553
|
+
emit(stats, () => json(stats));
|
|
554
|
+
} catch (err) {
|
|
555
|
+
die(err);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function formatDocumentAnalytics(stats) {
|
|
560
|
+
const minutes = Math.round(stats.total_duration_seconds / 60);
|
|
561
|
+
const lines = [
|
|
562
|
+
`${chalk2.bold("Document:")} ${stats.document_id}`,
|
|
563
|
+
`${chalk2.bold("Total views:")} ${stats.total_views}`,
|
|
564
|
+
`${chalk2.bold("Unique viewers:")} ${stats.unique_viewers}`,
|
|
565
|
+
`${chalk2.bold("Total read time:")} ${minutes} min (${stats.total_duration_seconds.toFixed(1)} s)`,
|
|
566
|
+
chalk2.dim(`Window: ${stats.since.slice(0, 10)} \u2192 ${stats.until.slice(0, 10)}`)
|
|
567
|
+
];
|
|
568
|
+
if (stats.avg_page_duration_seconds.length > 0) {
|
|
569
|
+
const rows = stats.avg_page_duration_seconds.map((p) => ({
|
|
570
|
+
page: String(p.page_number),
|
|
571
|
+
avg_seconds: p.avg_duration_seconds.toFixed(1)
|
|
572
|
+
}));
|
|
573
|
+
lines.push("", chalk2.bold("Per-page average duration:"));
|
|
574
|
+
lines.push(table(rows, ["page", "avg_seconds"]));
|
|
575
|
+
}
|
|
576
|
+
return lines.join("\n");
|
|
577
|
+
}
|
|
578
|
+
function formatLinkAnalytics(stats) {
|
|
579
|
+
const minutes = Math.round(stats.total_duration_seconds / 60);
|
|
580
|
+
return [
|
|
581
|
+
`${chalk2.bold("Link:")} ${stats.link_id}`,
|
|
582
|
+
`${chalk2.bold("Total views:")} ${stats.total_views}`,
|
|
583
|
+
`${chalk2.bold("Unique viewers:")} ${stats.unique_viewers}`,
|
|
584
|
+
`${chalk2.bold("Total read time:")} ${minutes} min (${stats.total_duration_seconds.toFixed(1)} s)`,
|
|
585
|
+
chalk2.dim(`Window: ${stats.since.slice(0, 10)} \u2192 ${stats.until.slice(0, 10)}`)
|
|
586
|
+
].join("\n");
|
|
587
|
+
}
|
|
588
|
+
function formatDataroomAnalytics(stats) {
|
|
589
|
+
const minutes = Math.round(stats.total_duration_seconds / 60);
|
|
590
|
+
return [
|
|
591
|
+
`${chalk2.bold("Dataroom:")} ${stats.dataroom_id}`,
|
|
592
|
+
`${chalk2.bold("Total views:")} ${stats.total_views}`,
|
|
593
|
+
`${chalk2.bold("Unique viewers:")} ${stats.unique_viewers}`,
|
|
594
|
+
`${chalk2.bold("Total read time:")} ${minutes} min (${stats.total_duration_seconds.toFixed(1)} s)`,
|
|
595
|
+
chalk2.dim(`Window: ${stats.since.slice(0, 10)} \u2192 ${stats.until.slice(0, 10)}`)
|
|
596
|
+
].join("\n");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/commands/auth.ts
|
|
600
|
+
import chalk3 from "chalk";
|
|
601
|
+
import { Command } from "commander";
|
|
602
|
+
import ora from "ora";
|
|
603
|
+
|
|
604
|
+
// src/oauth.ts
|
|
605
|
+
import * as oauth from "oauth4webapi";
|
|
606
|
+
import open from "open";
|
|
607
|
+
var CLIENT_ID = "papermark-cli";
|
|
608
|
+
var DEFAULT_SCOPES = [
|
|
609
|
+
"openid",
|
|
610
|
+
"offline_access",
|
|
611
|
+
"documents.read",
|
|
612
|
+
"documents.write",
|
|
613
|
+
"links.read",
|
|
614
|
+
"links.write",
|
|
615
|
+
"datarooms.read",
|
|
616
|
+
"datarooms.write",
|
|
617
|
+
"analytics.read",
|
|
618
|
+
"visitors.read"
|
|
619
|
+
];
|
|
620
|
+
function deriveIssuer(apiUrl = getApiUrl()) {
|
|
621
|
+
const url = new URL(apiUrl);
|
|
622
|
+
if (url.pathname === "/api" || url.pathname === "/api/") {
|
|
623
|
+
return new URL(`${url.origin}`);
|
|
624
|
+
}
|
|
625
|
+
if (url.hostname.startsWith("api.")) {
|
|
626
|
+
return new URL(`${url.protocol}//${url.host.replace(/^api\./, "app.")}`);
|
|
627
|
+
}
|
|
628
|
+
return new URL(url.origin);
|
|
629
|
+
}
|
|
630
|
+
async function discover(issuer) {
|
|
631
|
+
const response = await oauth.discoveryRequest(issuer, {
|
|
632
|
+
algorithm: "oidc",
|
|
633
|
+
...allowHttpIfLocal(issuer)
|
|
634
|
+
});
|
|
635
|
+
return oauth.processDiscoveryResponse(issuer, response);
|
|
636
|
+
}
|
|
637
|
+
function allowHttpIfLocal(u) {
|
|
638
|
+
const isLocal = u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname.endsWith(".local"));
|
|
639
|
+
return isLocal ? { [oauth.allowInsecureRequests]: true } : {};
|
|
640
|
+
}
|
|
641
|
+
async function requestDeviceAuthorization(as, scopes) {
|
|
642
|
+
if (!as.device_authorization_endpoint) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
"Authorization server does not advertise a device authorization endpoint."
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const body = new URLSearchParams({
|
|
648
|
+
client_id: CLIENT_ID,
|
|
649
|
+
scope: scopes.join(" ")
|
|
650
|
+
});
|
|
651
|
+
const json2 = await postFormJson(
|
|
652
|
+
as.device_authorization_endpoint,
|
|
653
|
+
body,
|
|
654
|
+
"device authorization"
|
|
655
|
+
);
|
|
656
|
+
return json2;
|
|
657
|
+
}
|
|
658
|
+
async function postFormJson(endpoint, body, what) {
|
|
659
|
+
let res;
|
|
660
|
+
try {
|
|
661
|
+
res = await fetch(endpoint, {
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers: {
|
|
664
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
665
|
+
Accept: "application/json"
|
|
666
|
+
},
|
|
667
|
+
body
|
|
668
|
+
});
|
|
669
|
+
} catch (err) {
|
|
670
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
671
|
+
throw new Error(
|
|
672
|
+
`${what}: network request to ${endpoint} failed (${reason}). Check connectivity and that the OAuth server is reachable.`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const text = await res.text();
|
|
676
|
+
let parsed = null;
|
|
677
|
+
try {
|
|
678
|
+
parsed = text ? JSON.parse(text) : null;
|
|
679
|
+
} catch {
|
|
680
|
+
parsed = null;
|
|
681
|
+
}
|
|
682
|
+
if (!res.ok) {
|
|
683
|
+
if (parsed) {
|
|
684
|
+
const e = new Error(
|
|
685
|
+
`${what} failed: ${res.status} ${JSON.stringify(parsed)}`
|
|
686
|
+
);
|
|
687
|
+
e.body = parsed;
|
|
688
|
+
throw e;
|
|
689
|
+
}
|
|
690
|
+
const snippet = text ? text.slice(0, 200) : "(empty body)";
|
|
691
|
+
throw new Error(
|
|
692
|
+
`${what} failed: HTTP ${res.status} (non-JSON response: ${snippet})`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (!parsed) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`${what} succeeded but returned a non-JSON body \u2014 is the endpoint correct? URL: ${endpoint}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return parsed;
|
|
701
|
+
}
|
|
702
|
+
async function pollForToken(as, deviceCode, {
|
|
703
|
+
interval = 5,
|
|
704
|
+
expiresIn = 600,
|
|
705
|
+
onTick
|
|
706
|
+
} = {}) {
|
|
707
|
+
if (!as.token_endpoint) throw new Error("No token_endpoint in discovery");
|
|
708
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
709
|
+
let wait = Math.max(interval, 1);
|
|
710
|
+
while (Date.now() < deadline) {
|
|
711
|
+
await new Promise((r) => setTimeout(r, wait * 1e3));
|
|
712
|
+
onTick?.();
|
|
713
|
+
const body = new URLSearchParams({
|
|
714
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
715
|
+
device_code: deviceCode,
|
|
716
|
+
client_id: CLIENT_ID
|
|
717
|
+
});
|
|
718
|
+
let parsed;
|
|
719
|
+
try {
|
|
720
|
+
parsed = await postFormJson(as.token_endpoint, body, "token request");
|
|
721
|
+
} catch (err) {
|
|
722
|
+
const errBody = err.body;
|
|
723
|
+
const errCode = errBody?.error;
|
|
724
|
+
if (errCode === "authorization_pending") continue;
|
|
725
|
+
if (errCode === "slow_down") {
|
|
726
|
+
wait += 5;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
throw err;
|
|
730
|
+
}
|
|
731
|
+
return parsed;
|
|
732
|
+
}
|
|
733
|
+
throw new Error("Device code expired before authorization completed.");
|
|
734
|
+
}
|
|
735
|
+
async function tryOpenBrowser(url) {
|
|
736
|
+
try {
|
|
737
|
+
await open(url);
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/commands/auth.ts
|
|
743
|
+
function registerAuthCommands(program2) {
|
|
744
|
+
program2.command("login").description("Sign in to Papermark via the browser (OAuth device flow)").option(
|
|
745
|
+
"--token <token>",
|
|
746
|
+
"paste an existing API token non-interactively (skip OAuth)"
|
|
747
|
+
).option(
|
|
748
|
+
"--scopes <list>",
|
|
749
|
+
"comma-separated scopes to request (defaults to read+write on documents/links plus analytics.read)"
|
|
750
|
+
).option("--no-browser", "do not auto-open a browser; print the URL only").action(
|
|
751
|
+
async (opts) => {
|
|
752
|
+
try {
|
|
753
|
+
if (opts.token) {
|
|
754
|
+
return await loginWithToken(opts.token);
|
|
755
|
+
}
|
|
756
|
+
const scopes = opts.scopes ? opts.scopes.split(/[\s,]+/).filter(Boolean) : DEFAULT_SCOPES;
|
|
757
|
+
return await loginWithDeviceFlow(scopes, {
|
|
758
|
+
openBrowser: opts.browser !== false
|
|
759
|
+
});
|
|
760
|
+
} catch (err) {
|
|
761
|
+
die(err);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
);
|
|
765
|
+
program2.command("logout").description("Remove the stored API token").action(() => {
|
|
766
|
+
clearToken();
|
|
767
|
+
emit({ removed: true }, () => chalk3.green("\u2713") + " Token removed.");
|
|
768
|
+
});
|
|
769
|
+
program2.command("whoami").description("Show the active token and API URL").action(() => {
|
|
770
|
+
const token = getToken();
|
|
771
|
+
if (!token) {
|
|
772
|
+
die(new AuthRequiredError());
|
|
773
|
+
}
|
|
774
|
+
const partial = `${token.slice(0, 11)}...${token.slice(-4)}`;
|
|
775
|
+
emit(
|
|
776
|
+
{
|
|
777
|
+
apiUrl: getApiUrl(),
|
|
778
|
+
token: partial,
|
|
779
|
+
source: getTokenSource(),
|
|
780
|
+
config: configPath()
|
|
781
|
+
},
|
|
782
|
+
() => [
|
|
783
|
+
`${chalk3.bold("API URL:")} ${getApiUrl()}`,
|
|
784
|
+
`${chalk3.bold("Token: ")} ${partial}`,
|
|
785
|
+
`${chalk3.bold("Source: ")} ${getTokenSource()}`,
|
|
786
|
+
`${chalk3.bold("Config: ")} ${configPath()}`
|
|
787
|
+
].join("\n")
|
|
788
|
+
);
|
|
789
|
+
});
|
|
790
|
+
program2.command("auth").description("Auth helpers (export, set, status)").addCommand(buildAuthExport()).addCommand(buildAuthSet());
|
|
791
|
+
}
|
|
792
|
+
function buildAuthExport() {
|
|
793
|
+
return new Command("export").description(
|
|
794
|
+
"Print current credentials as JSON. Useful for CI: redirect into a file and set PAPERMARK_CREDENTIALS_FILE."
|
|
795
|
+
).option(
|
|
796
|
+
"--unmasked",
|
|
797
|
+
"include the full token (default: mask to first-6 + last-4 chars)"
|
|
798
|
+
).action((opts) => {
|
|
799
|
+
const token = getToken();
|
|
800
|
+
if (!token) {
|
|
801
|
+
die(new AuthRequiredError());
|
|
802
|
+
}
|
|
803
|
+
const payload = {
|
|
804
|
+
token: opts.unmasked ? token : `${token.slice(0, 8)}***${token.slice(-4)}`,
|
|
805
|
+
apiUrl: getApiUrl(),
|
|
806
|
+
source: getTokenSource()
|
|
807
|
+
};
|
|
808
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
809
|
+
if (!opts.unmasked) {
|
|
810
|
+
process.stderr.write(
|
|
811
|
+
chalk3.dim(
|
|
812
|
+
"Token is masked. Pass --unmasked to include the full value (e.g. for writing to a secrets file).\n"
|
|
813
|
+
)
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function buildAuthSet() {
|
|
819
|
+
return new Command("set").description(
|
|
820
|
+
"Set credentials from stdin. Pipes a token (or JSON blob with {token, apiUrl}) into the CLI's config without leaving a tempfile on disk or exposing the value via `ps` / shell history. Ideal for CI runners pulling from a secret manager."
|
|
821
|
+
).option(
|
|
822
|
+
"--stdin",
|
|
823
|
+
"read the token (or JSON blob with --json) from stdin until EOF"
|
|
824
|
+
).option(
|
|
825
|
+
"--json",
|
|
826
|
+
"interpret stdin as a JSON object of shape {token, apiUrl?} instead of a bare token"
|
|
827
|
+
).action(async (opts) => {
|
|
828
|
+
if (!opts.stdin) {
|
|
829
|
+
die(
|
|
830
|
+
new ValidationError(
|
|
831
|
+
"auth set currently requires --stdin. Pipe the token via stdin."
|
|
832
|
+
)
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (process.stdin.isTTY) {
|
|
836
|
+
die(
|
|
837
|
+
new ValidationError(
|
|
838
|
+
"stdin is a TTY \u2014 pipe the token in (e.g. `op read op://vault/papermark | papermark auth set --stdin`)."
|
|
839
|
+
)
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
const raw = await readStdin();
|
|
843
|
+
if (!raw.trim()) {
|
|
844
|
+
die(
|
|
845
|
+
new ValidationError(
|
|
846
|
+
"No data on stdin. Pipe a token, or exit with Ctrl-D."
|
|
847
|
+
)
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
let token;
|
|
851
|
+
let apiUrl;
|
|
852
|
+
if (opts.json) {
|
|
853
|
+
let parsed;
|
|
854
|
+
try {
|
|
855
|
+
parsed = JSON.parse(raw);
|
|
856
|
+
} catch (parseErr) {
|
|
857
|
+
die(
|
|
858
|
+
new ValidationError("stdin was not valid JSON.", {
|
|
859
|
+
parseError: parseErr instanceof Error ? parseErr.message : String(parseErr)
|
|
860
|
+
})
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (!parsed || typeof parsed !== "object") {
|
|
864
|
+
die(
|
|
865
|
+
new ValidationError(
|
|
866
|
+
'Expected a JSON object like {"token":"..."}.'
|
|
867
|
+
)
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const p = parsed;
|
|
871
|
+
if (typeof p.token !== "string" || !p.token) {
|
|
872
|
+
die(
|
|
873
|
+
new ValidationError('JSON is missing a non-empty "token" field.')
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
token = p.token;
|
|
877
|
+
if (typeof p.apiUrl === "string") apiUrl = p.apiUrl;
|
|
878
|
+
else if (typeof p.api_url === "string") apiUrl = p.api_url;
|
|
879
|
+
} else {
|
|
880
|
+
token = raw.trim();
|
|
881
|
+
}
|
|
882
|
+
const prevToken = getToken();
|
|
883
|
+
const prevApiUrl = getApiUrl();
|
|
884
|
+
setToken(token);
|
|
885
|
+
if (apiUrl) setApiUrl(apiUrl);
|
|
886
|
+
try {
|
|
887
|
+
await api.listDocuments({ limit: 1 });
|
|
888
|
+
} catch (err) {
|
|
889
|
+
if (prevToken) setToken(prevToken);
|
|
890
|
+
else clearToken();
|
|
891
|
+
if (apiUrl) setApiUrl(prevApiUrl);
|
|
892
|
+
die(err);
|
|
893
|
+
}
|
|
894
|
+
const partial = `${token.slice(0, 8)}***${token.slice(-4)}`;
|
|
895
|
+
emit(
|
|
896
|
+
{ token: partial, source: getTokenSource(), config: configPath() },
|
|
897
|
+
() => chalk3.green("\u2713") + ` Token stored (${partial}). Config: ${chalk3.dim(configPath())}`
|
|
898
|
+
);
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
function readStdin() {
|
|
902
|
+
return new Promise((resolve, reject) => {
|
|
903
|
+
const chunks = [];
|
|
904
|
+
process.stdin.on("data", (c) => chunks.push(c));
|
|
905
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
906
|
+
process.stdin.on("error", reject);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
async function loginWithToken(token) {
|
|
910
|
+
const prevToken = getToken();
|
|
911
|
+
setToken(token.trim());
|
|
912
|
+
try {
|
|
913
|
+
await api.listDocuments({ limit: 1 });
|
|
914
|
+
} catch (err) {
|
|
915
|
+
if (prevToken) setToken(prevToken);
|
|
916
|
+
else clearToken();
|
|
917
|
+
if (err instanceof NetworkError) {
|
|
918
|
+
writeErrorWithHint(
|
|
919
|
+
err,
|
|
920
|
+
`
|
|
921
|
+
Current API URL: ${getApiUrl()}
|
|
922
|
+
If you're running Papermark locally, point the CLI at it:
|
|
923
|
+
papermark config set api-url http://localhost:3000/api`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
throw err;
|
|
927
|
+
}
|
|
928
|
+
emit(
|
|
929
|
+
{ apiUrl: getApiUrl(), config: configPath() },
|
|
930
|
+
() => chalk3.green("\u2713") + ` Logged in at ${chalk3.bold(getApiUrl())}. Config: ${chalk3.dim(configPath())}`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
function writeErrorWithHint(err, hint) {
|
|
934
|
+
const fe = formatError(err);
|
|
935
|
+
if (getRuntime().json) {
|
|
936
|
+
process.stderr.write(
|
|
937
|
+
JSON.stringify({ ok: false, error: fe.json.error }) + "\n"
|
|
938
|
+
);
|
|
939
|
+
} else {
|
|
940
|
+
process.stderr.write(fe.human + chalk3.dim(hint) + "\n");
|
|
941
|
+
}
|
|
942
|
+
process.exit(fe.exitCode);
|
|
943
|
+
}
|
|
944
|
+
async function loginWithDeviceFlow(scopes, { openBrowser }) {
|
|
945
|
+
const issuer = deriveIssuer();
|
|
946
|
+
const issuerDisplay = issuer.toString().replace(/\/$/, "");
|
|
947
|
+
const discoverSpinner = ora(`Contacting ${issuerDisplay}`).start();
|
|
948
|
+
let as;
|
|
949
|
+
try {
|
|
950
|
+
as = await discover(issuer);
|
|
951
|
+
} catch (err) {
|
|
952
|
+
discoverSpinner.fail();
|
|
953
|
+
writeErrorWithHint(
|
|
954
|
+
err,
|
|
955
|
+
`
|
|
956
|
+
Fetched ${issuerDisplay}/.well-known/openid-configuration.
|
|
957
|
+
If this isn't a Papermark deployment, paste an API token instead:
|
|
958
|
+
papermark login --token pm_live_xxx`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
discoverSpinner.succeed(`Discovered ${as.issuer}`);
|
|
962
|
+
const authSpinner = ora("Requesting device authorization").start();
|
|
963
|
+
let auth;
|
|
964
|
+
try {
|
|
965
|
+
auth = await requestDeviceAuthorization(as, scopes);
|
|
966
|
+
} catch (err) {
|
|
967
|
+
authSpinner.fail();
|
|
968
|
+
throw err;
|
|
969
|
+
}
|
|
970
|
+
authSpinner.succeed("Got a user code");
|
|
971
|
+
const target = auth.verification_uri_complete ?? auth.verification_uri;
|
|
972
|
+
console.log("");
|
|
973
|
+
console.log(" " + chalk3.bold("Open this URL in your browser:"));
|
|
974
|
+
console.log(" " + chalk3.cyan(target));
|
|
975
|
+
console.log(" " + chalk3.bold("And enter this code if prompted:"));
|
|
976
|
+
console.log(" " + chalk3.bold(chalk3.yellow(auth.user_code)));
|
|
977
|
+
console.log("");
|
|
978
|
+
if (openBrowser) {
|
|
979
|
+
await tryOpenBrowser(target);
|
|
980
|
+
}
|
|
981
|
+
const pollSpinner = ora("Waiting for you to approve in the browser\u2026").start();
|
|
982
|
+
let token;
|
|
983
|
+
try {
|
|
984
|
+
token = await pollForToken(as, auth.device_code, {
|
|
985
|
+
interval: auth.interval ?? 5,
|
|
986
|
+
expiresIn: auth.expires_in
|
|
987
|
+
});
|
|
988
|
+
} catch (err) {
|
|
989
|
+
pollSpinner.fail();
|
|
990
|
+
throw err;
|
|
991
|
+
}
|
|
992
|
+
pollSpinner.succeed("Access token granted");
|
|
993
|
+
const prevToken = getToken();
|
|
994
|
+
setToken(token.access_token);
|
|
995
|
+
try {
|
|
996
|
+
await api.listDocuments({ limit: 1 });
|
|
997
|
+
} catch (err) {
|
|
998
|
+
if (prevToken) setToken(prevToken);
|
|
999
|
+
else clearToken();
|
|
1000
|
+
throw err;
|
|
1001
|
+
}
|
|
1002
|
+
const expiresIn = token.expires_in ?? null;
|
|
1003
|
+
const expiryLabel = formatExpiry(expiresIn);
|
|
1004
|
+
emit(
|
|
1005
|
+
{
|
|
1006
|
+
apiUrl: getApiUrl(),
|
|
1007
|
+
config: configPath(),
|
|
1008
|
+
...token.scope ? { scopes: token.scope.split(" ") } : {},
|
|
1009
|
+
...expiresIn != null ? { expires_in: expiresIn } : {}
|
|
1010
|
+
},
|
|
1011
|
+
() => {
|
|
1012
|
+
const lines = [
|
|
1013
|
+
chalk3.green("\u2713") + ` Logged in at ${chalk3.bold(getApiUrl())}. Config: ${chalk3.dim(configPath())}`
|
|
1014
|
+
];
|
|
1015
|
+
if (token.scope) lines.push(chalk3.dim(` Scopes: ${token.scope}`));
|
|
1016
|
+
if (expiryLabel) {
|
|
1017
|
+
lines.push(
|
|
1018
|
+
chalk3.dim(
|
|
1019
|
+
` Please note: this key will expire after ${expiryLabel}, at which point you'll need to re-authenticate.`
|
|
1020
|
+
)
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
return lines.join("\n");
|
|
1024
|
+
}
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
function formatExpiry(expiresInSeconds) {
|
|
1028
|
+
if (expiresInSeconds == null) return null;
|
|
1029
|
+
const days = Math.round(expiresInSeconds / 86400);
|
|
1030
|
+
if (days >= 2) return `${days} days`;
|
|
1031
|
+
const hours = Math.round(expiresInSeconds / 3600);
|
|
1032
|
+
if (hours >= 2) return `${hours} hours`;
|
|
1033
|
+
return `${expiresInSeconds}s`;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// src/commands/config.ts
|
|
1037
|
+
import chalk4 from "chalk";
|
|
1038
|
+
var KNOWN_KEYS = ["api-url"];
|
|
1039
|
+
function isKnown(key) {
|
|
1040
|
+
return KNOWN_KEYS.includes(key);
|
|
1041
|
+
}
|
|
1042
|
+
function registerConfigCommands(program2) {
|
|
1043
|
+
const cfg = program2.command("config").description("Manage CLI configuration");
|
|
1044
|
+
cfg.command("get [key]").description("Print one or all config values").action((key) => {
|
|
1045
|
+
const all = getAll();
|
|
1046
|
+
if (!key) {
|
|
1047
|
+
console.log(`${chalk4.dim("# " + configPath())}`);
|
|
1048
|
+
console.log(`api-url ${all.apiUrl ?? ""}`);
|
|
1049
|
+
console.log(`token ${all.token ? "(set)" : "(not set)"}`);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (!isKnown(key)) return die(new Error(`Unknown key: ${key}`));
|
|
1053
|
+
if (key === "api-url") console.log(all.apiUrl ?? "");
|
|
1054
|
+
});
|
|
1055
|
+
cfg.command("set <key> <value>").description(`Set a config value. Known keys: ${KNOWN_KEYS.join(", ")}`).action((key, value) => {
|
|
1056
|
+
if (!isKnown(key)) return die(new Error(`Unknown key: ${key}`));
|
|
1057
|
+
if (key === "api-url") setApiUrl(value);
|
|
1058
|
+
console.log(chalk4.green("\u2713") + ` ${key} = ${value}`);
|
|
1059
|
+
});
|
|
1060
|
+
cfg.command("unset <key>").description("Clear a config value").action((key) => {
|
|
1061
|
+
if (!isKnown(key)) return die(new Error(`Unknown key: ${key}`));
|
|
1062
|
+
if (key === "api-url") deleteKey("apiUrl");
|
|
1063
|
+
console.log(chalk4.green("\u2713") + ` ${key} unset`);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/commands/datarooms.ts
|
|
1068
|
+
import chalk5 from "chalk";
|
|
1069
|
+
function registerDataroomsCommands(program2) {
|
|
1070
|
+
const dr = program2.command("datarooms").alias("dr").description("Manage datarooms");
|
|
1071
|
+
dr.command("list").description("List datarooms").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("-q, --query <substring>", "filter by dataroom name").option("--json", "output raw JSON (same as --json on the root command)").action(
|
|
1072
|
+
async (opts) => {
|
|
1073
|
+
if (opts.json) setRuntime({ json: true });
|
|
1074
|
+
try {
|
|
1075
|
+
const res = await api.listDatarooms({
|
|
1076
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1077
|
+
cursor: opts.cursor,
|
|
1078
|
+
query: opts.query
|
|
1079
|
+
});
|
|
1080
|
+
emit(res, () => {
|
|
1081
|
+
const rows = res.data.map((d) => ({
|
|
1082
|
+
id: d.pid,
|
|
1083
|
+
name: truncate2(d.name, 30),
|
|
1084
|
+
docs: String(d.document_count),
|
|
1085
|
+
folders: String(d.folder_count),
|
|
1086
|
+
created: d.created.slice(0, 10)
|
|
1087
|
+
}));
|
|
1088
|
+
const out = table(rows, ["id", "name", "docs", "folders", "created"]);
|
|
1089
|
+
return res.next_cursor ? out + chalk5.dim(`
|
|
1090
|
+
|
|
1091
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
1092
|
+
});
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
die(err);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
);
|
|
1098
|
+
dr.command("get <id>").description(
|
|
1099
|
+
"Fetch one dataroom. `id` accepts the internal cuid or the `dr_...` public id."
|
|
1100
|
+
).action(async (id) => {
|
|
1101
|
+
try {
|
|
1102
|
+
const room = await api.getDataroom(id);
|
|
1103
|
+
emit(room, () => json(room));
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
die(err);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
dr.command("create").description("Create a new (empty) dataroom").requiredOption("-n, --name <name>", "dataroom name").option(
|
|
1109
|
+
"--internal-name <slug>",
|
|
1110
|
+
"private alias visible only to your team"
|
|
1111
|
+
).option("-d, --description <text>", "human-readable description").action(
|
|
1112
|
+
async (opts) => {
|
|
1113
|
+
try {
|
|
1114
|
+
const room = await api.createDataroom({
|
|
1115
|
+
name: opts.name,
|
|
1116
|
+
internal_name: opts.internalName,
|
|
1117
|
+
description: opts.description
|
|
1118
|
+
});
|
|
1119
|
+
emit(
|
|
1120
|
+
room,
|
|
1121
|
+
() => chalk5.green("\u2713") + ` Dataroom created: ${chalk5.bold(room.pid)}
|
|
1122
|
+
` + json(room)
|
|
1123
|
+
);
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
die(err);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
);
|
|
1129
|
+
dr.command("update <id>").description(
|
|
1130
|
+
"Update a dataroom. `id` accepts the internal cuid or the `dr_...` public id."
|
|
1131
|
+
).option("-n, --name <name>").option("--internal-name <slug>", "set or clear (use empty string)").option("-d, --description <text>", "set or clear (use empty string)").option("--bulk-download <on|off>", "allow or block bulk download").action(
|
|
1132
|
+
async (id, opts) => {
|
|
1133
|
+
try {
|
|
1134
|
+
const body = {};
|
|
1135
|
+
if (opts.name !== void 0) body.name = opts.name;
|
|
1136
|
+
if (opts.internalName !== void 0)
|
|
1137
|
+
body.internal_name = opts.internalName === "" ? null : opts.internalName;
|
|
1138
|
+
if (opts.description !== void 0)
|
|
1139
|
+
body.description = opts.description === "" ? null : opts.description;
|
|
1140
|
+
const tri = (v, flag) => {
|
|
1141
|
+
if (v === void 0) return void 0;
|
|
1142
|
+
const norm = v.toLowerCase();
|
|
1143
|
+
if (norm === "on") return true;
|
|
1144
|
+
if (norm === "off") return false;
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
`Invalid value for ${flag}: ${JSON.stringify(v)} (expected "on" or "off").`
|
|
1147
|
+
);
|
|
1148
|
+
};
|
|
1149
|
+
const bulk = tri(opts.bulkDownload, "--bulk-download");
|
|
1150
|
+
if (bulk !== void 0) body.allow_bulk_download = bulk;
|
|
1151
|
+
if (Object.keys(body).length === 0) {
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
"Pass at least one field to update (e.g. --name, --description, --bulk-download off)."
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
const room = await api.updateDataroom(id, body);
|
|
1157
|
+
emit(
|
|
1158
|
+
room,
|
|
1159
|
+
() => chalk5.green("\u2713") + ` Dataroom updated: ${chalk5.bold(room.pid)}
|
|
1160
|
+
` + json(room)
|
|
1161
|
+
);
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
die(err);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
1167
|
+
dr.command("links <id>").description(
|
|
1168
|
+
"List share links scoped to a dataroom. `id` accepts the internal cuid or the `dr_...` public id."
|
|
1169
|
+
).option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON (same as --json on the root command)").action(
|
|
1170
|
+
async (id, opts) => {
|
|
1171
|
+
if (opts.json) setRuntime({ json: true });
|
|
1172
|
+
try {
|
|
1173
|
+
const res = await api.listLinks({
|
|
1174
|
+
dataroom_id: id,
|
|
1175
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1176
|
+
cursor: opts.cursor
|
|
1177
|
+
});
|
|
1178
|
+
emit(res, () => {
|
|
1179
|
+
const rows = res.data.map((l) => ({
|
|
1180
|
+
id: l.id,
|
|
1181
|
+
name: truncate2(l.name ?? "", 30),
|
|
1182
|
+
url: truncate2(l.url, 45),
|
|
1183
|
+
password: l.is_password_protected ? "yes" : "",
|
|
1184
|
+
expires: l.expires_at ? l.expires_at.slice(0, 10) : ""
|
|
1185
|
+
}));
|
|
1186
|
+
const out = table(rows, ["id", "name", "url", "password", "expires"]);
|
|
1187
|
+
return res.next_cursor ? out + chalk5.dim(`
|
|
1188
|
+
|
|
1189
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
1190
|
+
});
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
die(err);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
dr.command("viewers <id>").description(
|
|
1197
|
+
"List persistent visitors (Viewer rows) tied to a dataroom. `id` accepts the internal cuid or the `dr_...` public id."
|
|
1198
|
+
).option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--email <email>", "filter to a specific email (exact match)").option("--json", "output raw JSON (same as --json on the root command)").action(
|
|
1199
|
+
async (id, opts) => {
|
|
1200
|
+
if (opts.json) setRuntime({ json: true });
|
|
1201
|
+
try {
|
|
1202
|
+
const res = await api.listVisitors({
|
|
1203
|
+
dataroom_id: id,
|
|
1204
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1205
|
+
cursor: opts.cursor,
|
|
1206
|
+
email: opts.email
|
|
1207
|
+
});
|
|
1208
|
+
emit(res, () => {
|
|
1209
|
+
const rows = res.data.map((v) => ({
|
|
1210
|
+
id: v.id,
|
|
1211
|
+
email: sanitizeForTerminal(v.email),
|
|
1212
|
+
verified: v.verified ? "yes" : "",
|
|
1213
|
+
views: String(v.total_views),
|
|
1214
|
+
last_view: v.last_viewed_at?.slice(0, 10) ?? ""
|
|
1215
|
+
}));
|
|
1216
|
+
const out = table(rows, ["id", "email", "verified", "views", "last_view"]);
|
|
1217
|
+
return res.next_cursor ? out + chalk5.dim(`
|
|
1218
|
+
|
|
1219
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
1220
|
+
});
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
die(err);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
);
|
|
1226
|
+
dr.command("stats <id>").alias("analytics").description(
|
|
1227
|
+
"Aggregate analytics for a dataroom: total views, unique viewers, total read time. Backed by Tinybird \u2014 subject to a stricter rate limit. `id` accepts the internal cuid or the `dr_...` public id."
|
|
1228
|
+
).option(
|
|
1229
|
+
"--since <unix-ms>",
|
|
1230
|
+
"window start (default: 30 days ago)",
|
|
1231
|
+
(v) => Number(v)
|
|
1232
|
+
).option(
|
|
1233
|
+
"--until <unix-ms>",
|
|
1234
|
+
"window end (default: now)",
|
|
1235
|
+
(v) => Number(v)
|
|
1236
|
+
).option("--json", "output raw JSON (same as --json on the root command)").action(
|
|
1237
|
+
async (id, opts) => {
|
|
1238
|
+
if (opts.json) setRuntime({ json: true });
|
|
1239
|
+
try {
|
|
1240
|
+
const stats = await api.dataroomAnalytics(id, {
|
|
1241
|
+
since: opts.since,
|
|
1242
|
+
until: opts.until
|
|
1243
|
+
});
|
|
1244
|
+
emit(stats, () => {
|
|
1245
|
+
const minutes = Math.round(stats.total_duration_seconds / 60);
|
|
1246
|
+
return [
|
|
1247
|
+
`${chalk5.bold("Dataroom:")} ${stats.dataroom_id}`,
|
|
1248
|
+
`${chalk5.bold("Total views:")} ${stats.total_views}`,
|
|
1249
|
+
`${chalk5.bold("Unique viewers:")} ${stats.unique_viewers}`,
|
|
1250
|
+
`${chalk5.bold("Total read time:")} ${minutes} min (${stats.total_duration_seconds.toFixed(1)} s)`,
|
|
1251
|
+
chalk5.dim(
|
|
1252
|
+
`Window: ${stats.since.slice(0, 10)} \u2192 ${stats.until.slice(0, 10)}`
|
|
1253
|
+
)
|
|
1254
|
+
].join("\n");
|
|
1255
|
+
});
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
die(err);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
);
|
|
1261
|
+
dr.command("delete <id>").description(
|
|
1262
|
+
"Delete a dataroom. `id` accepts the internal cuid or the `dr_...` public id."
|
|
1263
|
+
).action(async (id) => {
|
|
1264
|
+
try {
|
|
1265
|
+
const deleted = await api.deleteDataroom(id);
|
|
1266
|
+
emit(
|
|
1267
|
+
deleted,
|
|
1268
|
+
() => chalk5.green("\u2713") + ` Dataroom ${id} deleted.`
|
|
1269
|
+
);
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
die(err);
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
dr.command("attach <id>").description(
|
|
1275
|
+
"Attach an existing document to a dataroom. The document stays in the team library; this just adds it to the dataroom contents."
|
|
1276
|
+
).requiredOption("--document <id>", "document id to attach").option(
|
|
1277
|
+
"--folder-id <fid>",
|
|
1278
|
+
"place under a specific dataroom folder (must belong to the same dataroom)"
|
|
1279
|
+
).action(
|
|
1280
|
+
async (id, opts) => {
|
|
1281
|
+
try {
|
|
1282
|
+
const item = await api.attachDataroomDocument(id, {
|
|
1283
|
+
document_id: opts.document,
|
|
1284
|
+
folder_id: opts.folderId
|
|
1285
|
+
});
|
|
1286
|
+
emit(
|
|
1287
|
+
item,
|
|
1288
|
+
() => chalk5.green("\u2713") + ` Attached document ${chalk5.bold(item.document_id)} as item ${chalk5.bold(item.id)}.
|
|
1289
|
+
` + json(item)
|
|
1290
|
+
);
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
die(err);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
);
|
|
1296
|
+
const drFolders = dr.command("folders").description("Manage folders inside a dataroom");
|
|
1297
|
+
drFolders.command("list <id>").description("List folders in a dataroom").option(
|
|
1298
|
+
"--parent <fid>",
|
|
1299
|
+
"restrict to direct children of this folder (use 'root' for top-level)"
|
|
1300
|
+
).option("-l, --limit <n>", "max results (1-100)", "50").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON").action(
|
|
1301
|
+
async (id, opts) => {
|
|
1302
|
+
if (opts.json) setRuntime({ json: true });
|
|
1303
|
+
try {
|
|
1304
|
+
const res = await api.listDataroomFolders(id, {
|
|
1305
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1306
|
+
cursor: opts.cursor,
|
|
1307
|
+
parent_id: opts.parent
|
|
1308
|
+
});
|
|
1309
|
+
emit(res, () => {
|
|
1310
|
+
const rows = res.data.map((f) => ({
|
|
1311
|
+
id: f.id,
|
|
1312
|
+
name: truncate2(f.name, 30),
|
|
1313
|
+
path: truncate2(f.path, 40),
|
|
1314
|
+
docs: String(f.document_count),
|
|
1315
|
+
children: String(f.child_folder_count)
|
|
1316
|
+
}));
|
|
1317
|
+
const out = table(rows, ["id", "name", "path", "docs", "children"]);
|
|
1318
|
+
return res.next_cursor ? out + chalk5.dim(`
|
|
1319
|
+
|
|
1320
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
1321
|
+
});
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
die(err);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
);
|
|
1327
|
+
drFolders.command("create <id>").description("Create a folder inside a dataroom").requiredOption("-n, --name <name>", "folder name").option("--parent <fid>", "parent folder id (omit for dataroom root)").option("--icon <icon>", "icon name (e.g. folder, briefcase, lock)").option("--color <color>", "color token (gray, red, orange, yellow, green, blue, black)").action(
|
|
1328
|
+
async (id, opts) => {
|
|
1329
|
+
try {
|
|
1330
|
+
const f = await api.createDataroomFolder(id, {
|
|
1331
|
+
name: opts.name,
|
|
1332
|
+
parent_id: opts.parent ?? null,
|
|
1333
|
+
icon: opts.icon,
|
|
1334
|
+
color: opts.color
|
|
1335
|
+
});
|
|
1336
|
+
emit(
|
|
1337
|
+
f,
|
|
1338
|
+
() => chalk5.green("\u2713") + ` Dataroom folder created: ${chalk5.bold(f.id)}
|
|
1339
|
+
` + json(f)
|
|
1340
|
+
);
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
die(err);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
);
|
|
1346
|
+
drFolders.command("get <id> <fid>").description("Fetch one dataroom folder").action(async (id, fid) => {
|
|
1347
|
+
try {
|
|
1348
|
+
const f = await api.getDataroomFolder(id, fid);
|
|
1349
|
+
emit(f, () => json(f));
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
die(err);
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
drFolders.command("update <id> <fid>").description("Update a dataroom folder").option("-n, --name <name>").option("--icon <icon>", "icon name (empty string to clear)").option("--color <color>", "color token (empty string to clear)").action(
|
|
1355
|
+
async (id, fid, opts) => {
|
|
1356
|
+
try {
|
|
1357
|
+
const body = {};
|
|
1358
|
+
if (opts.name !== void 0) body.name = opts.name;
|
|
1359
|
+
if (opts.icon !== void 0)
|
|
1360
|
+
body.icon = opts.icon === "" ? null : opts.icon;
|
|
1361
|
+
if (opts.color !== void 0)
|
|
1362
|
+
body.color = opts.color === "" ? null : opts.color;
|
|
1363
|
+
if (Object.keys(body).length === 0) {
|
|
1364
|
+
throw new Error(
|
|
1365
|
+
"Pass at least one of --name, --icon, --color to update."
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
const f = await api.updateDataroomFolder(id, fid, body);
|
|
1369
|
+
emit(
|
|
1370
|
+
f,
|
|
1371
|
+
() => chalk5.green("\u2713") + ` Dataroom folder updated: ${chalk5.bold(f.id)}
|
|
1372
|
+
` + json(f)
|
|
1373
|
+
);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
die(err);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
);
|
|
1379
|
+
drFolders.command("delete <id> <fid>").description("Delete a dataroom folder").option(
|
|
1380
|
+
"--cascade",
|
|
1381
|
+
"also delete nested folders + dataroom-document attachments",
|
|
1382
|
+
false
|
|
1383
|
+
).action(
|
|
1384
|
+
async (id, fid, opts) => {
|
|
1385
|
+
try {
|
|
1386
|
+
const deleted = await api.deleteDataroomFolder(id, fid, {
|
|
1387
|
+
cascade: opts.cascade
|
|
1388
|
+
});
|
|
1389
|
+
emit(
|
|
1390
|
+
deleted,
|
|
1391
|
+
() => chalk5.green("\u2713") + ` Dataroom folder ${fid} deleted.`
|
|
1392
|
+
);
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
die(err);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
);
|
|
1398
|
+
drFolders.command("move <id> <fid>").description("Move a dataroom folder under a different parent").option(
|
|
1399
|
+
"--parent <fid>",
|
|
1400
|
+
"target parent folder id; pass empty string for the dataroom root"
|
|
1401
|
+
).action(
|
|
1402
|
+
async (id, fid, opts) => {
|
|
1403
|
+
try {
|
|
1404
|
+
if (opts.parent === void 0) {
|
|
1405
|
+
throw new Error(
|
|
1406
|
+
"Pass --parent <fid> (use empty string to move to the root)."
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
const parent_id = opts.parent === "" ? null : opts.parent;
|
|
1410
|
+
const f = await api.moveDataroomFolder(id, fid, { parent_id });
|
|
1411
|
+
emit(
|
|
1412
|
+
f,
|
|
1413
|
+
() => chalk5.green("\u2713") + ` Folder moved. New path: ${chalk5.bold(f.path)}
|
|
1414
|
+
` + json(f)
|
|
1415
|
+
);
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
die(err);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
);
|
|
1421
|
+
dr.command("documents <id>").description(
|
|
1422
|
+
"List documents inside a dataroom. `id` accepts the internal cuid or the `dr_...` public id."
|
|
1423
|
+
).option("-l, --limit <n>", "max results (1-100)", "50").option("-c, --cursor <id>", "pagination cursor").option("--folder <id>", "restrict to a folder").option("--json", "output raw JSON (same as --json on the root command)").action(
|
|
1424
|
+
async (id, opts) => {
|
|
1425
|
+
if (opts.json) setRuntime({ json: true });
|
|
1426
|
+
try {
|
|
1427
|
+
const res = await api.listDataroomDocuments(id, {
|
|
1428
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1429
|
+
cursor: opts.cursor,
|
|
1430
|
+
folder_id: opts.folder
|
|
1431
|
+
});
|
|
1432
|
+
emit(res, () => {
|
|
1433
|
+
const rows = res.data.map((it) => ({
|
|
1434
|
+
id: it.id,
|
|
1435
|
+
document: truncate2(it.document_name, 32),
|
|
1436
|
+
folder: truncate2(it.folder_path ?? "/", 64),
|
|
1437
|
+
type: it.content_type ?? "",
|
|
1438
|
+
pages: it.num_pages?.toString() ?? ""
|
|
1439
|
+
}));
|
|
1440
|
+
const out = table(rows, ["id", "document", "folder", "type", "pages"]);
|
|
1441
|
+
return res.next_cursor ? out + chalk5.dim(`
|
|
1442
|
+
|
|
1443
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
1444
|
+
});
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
die(err);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
function truncate2(s, n) {
|
|
1452
|
+
const clean = sanitizeForTerminal(s);
|
|
1453
|
+
return clean.length > n ? clean.slice(0, n - 1) + "\u2026" : clean;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/commands/doctor.ts
|
|
1457
|
+
import chalk6 from "chalk";
|
|
1458
|
+
function registerDoctorCommand(program2) {
|
|
1459
|
+
program2.command("doctor").description(
|
|
1460
|
+
"Run health checks against the CLI config + Papermark API. Returns a `checks` array (JSON) and a non-zero exit if anything fails \u2014 useful as a CI preflight before running real commands."
|
|
1461
|
+
).action(async () => {
|
|
1462
|
+
const checks = [];
|
|
1463
|
+
const token = getToken();
|
|
1464
|
+
checks.push({
|
|
1465
|
+
name: "auth.present",
|
|
1466
|
+
ok: Boolean(token),
|
|
1467
|
+
detail: token ? `token found (source: ${getTokenSource()})` : "no token configured",
|
|
1468
|
+
hint: token ? void 0 : "Run `papermark login` or set PAPERMARK_TOKEN."
|
|
1469
|
+
});
|
|
1470
|
+
const apiUrl = getApiUrl();
|
|
1471
|
+
checks.push({
|
|
1472
|
+
name: "config.api_url",
|
|
1473
|
+
ok: Boolean(apiUrl),
|
|
1474
|
+
detail: apiUrl
|
|
1475
|
+
});
|
|
1476
|
+
if (token) {
|
|
1477
|
+
try {
|
|
1478
|
+
await api.listDocuments({ limit: 1 });
|
|
1479
|
+
checks.push({
|
|
1480
|
+
name: "api.reachable",
|
|
1481
|
+
ok: true,
|
|
1482
|
+
detail: `GET /v1/documents?limit=1 \u2192 200 at ${apiUrl}`
|
|
1483
|
+
});
|
|
1484
|
+
checks.push({
|
|
1485
|
+
name: "api.auth",
|
|
1486
|
+
ok: true,
|
|
1487
|
+
detail: "token accepted"
|
|
1488
|
+
});
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
if (err instanceof NetworkError) {
|
|
1491
|
+
checks.push({
|
|
1492
|
+
name: "network.api",
|
|
1493
|
+
ok: false,
|
|
1494
|
+
detail: err.message,
|
|
1495
|
+
hint: "Check your internet connection and the configured api-url."
|
|
1496
|
+
});
|
|
1497
|
+
} else if (err instanceof ApiError) {
|
|
1498
|
+
const authIssue = err.status === 401 || err.status === 403;
|
|
1499
|
+
checks.push({
|
|
1500
|
+
name: "api.reachable",
|
|
1501
|
+
ok: true,
|
|
1502
|
+
detail: `HTTP ${err.status} ${err.code}: ${err.message}`
|
|
1503
|
+
});
|
|
1504
|
+
checks.push({
|
|
1505
|
+
name: "api.auth",
|
|
1506
|
+
ok: !authIssue,
|
|
1507
|
+
detail: authIssue ? `Token rejected: HTTP ${err.status} ${err.code}` : `non-auth API error: HTTP ${err.status} ${err.code}`,
|
|
1508
|
+
hint: authIssue ? "Run `papermark login` to refresh." : void 0
|
|
1509
|
+
});
|
|
1510
|
+
} else {
|
|
1511
|
+
checks.push({
|
|
1512
|
+
name: "api.reachable",
|
|
1513
|
+
ok: false,
|
|
1514
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
checks.push({
|
|
1520
|
+
name: "api.reachable",
|
|
1521
|
+
ok: false,
|
|
1522
|
+
detail: "skipped (no token)"
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
const allOk = checks.every((c) => c.ok);
|
|
1526
|
+
emit({ ok: allOk, checks }, () => {
|
|
1527
|
+
const lines = checks.map((c) => {
|
|
1528
|
+
const glyph = c.ok ? chalk6.green("\u2713") : chalk6.red("\u2717");
|
|
1529
|
+
const hint = c.hint ? chalk6.dim(`
|
|
1530
|
+
Hint: ${c.hint}`) : "";
|
|
1531
|
+
return `${glyph} ${chalk6.bold(c.name)} ${chalk6.dim(c.detail)}${hint}`;
|
|
1532
|
+
});
|
|
1533
|
+
const footer = allOk ? chalk6.green("\nAll checks passed.") : chalk6.red("\nOne or more checks failed.");
|
|
1534
|
+
return lines.join("\n") + footer;
|
|
1535
|
+
});
|
|
1536
|
+
if (!allOk) {
|
|
1537
|
+
const anyNetworkFailure = checks.some(
|
|
1538
|
+
(c) => !c.ok && c.name.startsWith("network")
|
|
1539
|
+
);
|
|
1540
|
+
const anyAuthFailure = checks.some(
|
|
1541
|
+
(c) => !c.ok && (c.name.startsWith("auth") || c.name === "api.auth")
|
|
1542
|
+
);
|
|
1543
|
+
process.exit(
|
|
1544
|
+
anyNetworkFailure ? ExitCode.NetworkError : anyAuthFailure ? ExitCode.AuthError : ExitCode.ApiError
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/commands/documents.ts
|
|
1551
|
+
import { createReadStream } from "fs";
|
|
1552
|
+
import { stat } from "fs/promises";
|
|
1553
|
+
import { basename } from "path";
|
|
1554
|
+
import { Readable } from "stream";
|
|
1555
|
+
import chalk7 from "chalk";
|
|
1556
|
+
import { lookup as lookupMimeType } from "mime-types";
|
|
1557
|
+
import ora2 from "ora";
|
|
1558
|
+
function registerDocumentsCommands(program2) {
|
|
1559
|
+
const docs = program2.command("documents").alias("doc").description("Manage documents");
|
|
1560
|
+
docs.command("list").description("List documents").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON (same as --json on the root command)").action(async (opts) => {
|
|
1561
|
+
if (opts.json) setRuntime({ json: true });
|
|
1562
|
+
try {
|
|
1563
|
+
const res = await api.listDocuments({
|
|
1564
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1565
|
+
cursor: opts.cursor
|
|
1566
|
+
});
|
|
1567
|
+
emit(res, () => {
|
|
1568
|
+
const rows = res.data.map((d) => ({
|
|
1569
|
+
id: d.id,
|
|
1570
|
+
name: truncate3(d.name, 40),
|
|
1571
|
+
type: d.type ?? "",
|
|
1572
|
+
pages: d.num_pages?.toString() ?? "",
|
|
1573
|
+
created: d.created.slice(0, 10)
|
|
1574
|
+
}));
|
|
1575
|
+
const out = table(rows, ["id", "name", "type", "pages", "created"]);
|
|
1576
|
+
return res.next_cursor ? out + chalk7.dim(`
|
|
1577
|
+
|
|
1578
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
1579
|
+
});
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
die(err);
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
docs.command("get <id>").description("Fetch one document").action(async (id) => {
|
|
1585
|
+
try {
|
|
1586
|
+
const doc = await api.getDocument(id);
|
|
1587
|
+
emit(doc, () => json(doc));
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
die(err);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
docs.command("search <query>").description("Search documents by name").option("-l, --limit <n>", "max results (1-100)", "25").option("--json", "output raw JSON (same as --json on the root command)").action(async (query, opts) => {
|
|
1593
|
+
if (opts.json) setRuntime({ json: true });
|
|
1594
|
+
try {
|
|
1595
|
+
const res = await api.searchDocuments(
|
|
1596
|
+
query,
|
|
1597
|
+
opts.limit ? Number(opts.limit) : void 0
|
|
1598
|
+
);
|
|
1599
|
+
emit(res, () => {
|
|
1600
|
+
const rows = res.data.map((d) => ({
|
|
1601
|
+
id: d.id,
|
|
1602
|
+
name: truncate3(d.name, 40),
|
|
1603
|
+
type: d.type ?? "",
|
|
1604
|
+
created: d.created.slice(0, 10)
|
|
1605
|
+
}));
|
|
1606
|
+
return table(rows, ["id", "name", "type", "created"]);
|
|
1607
|
+
});
|
|
1608
|
+
} catch (err) {
|
|
1609
|
+
die(err);
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
docs.command("update <id>").description("Rename a document or move it to a different team-library folder").option("-n, --name <name>", "new document name").option("--folder-id <id>", "move into folder id (use empty string for root)").action(
|
|
1613
|
+
async (id, opts) => {
|
|
1614
|
+
try {
|
|
1615
|
+
const body = {};
|
|
1616
|
+
if (opts.name !== void 0) body.name = opts.name;
|
|
1617
|
+
if (opts.folderId !== void 0) {
|
|
1618
|
+
body.folder_id = opts.folderId === "" ? null : opts.folderId;
|
|
1619
|
+
}
|
|
1620
|
+
if (Object.keys(body).length === 0) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
"Pass at least one of --name or --folder-id to update."
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
const doc = await api.updateDocument(id, body);
|
|
1626
|
+
emit(
|
|
1627
|
+
doc,
|
|
1628
|
+
() => chalk7.green("\u2713") + ` Document updated: ${chalk7.bold(doc.id)}
|
|
1629
|
+
` + json(doc)
|
|
1630
|
+
);
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
die(err);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
);
|
|
1636
|
+
docs.command("delete <id>").description("Delete a document").action(async (id) => {
|
|
1637
|
+
try {
|
|
1638
|
+
const deleted = await api.deleteDocument(id);
|
|
1639
|
+
emit(
|
|
1640
|
+
deleted,
|
|
1641
|
+
() => chalk7.green("\u2713") + ` Document ${id} deleted.`
|
|
1642
|
+
);
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
die(err);
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
docs.command("upload <file>").description("Upload a local file and create a document").option("-n, --name <name>", "override document name (default: filename)").option("--link", "also create a default share link for this document").option("--folder-id <id>", "place in folder by id (create folders via `papermark folders create`)").action(
|
|
1648
|
+
async (filePath, opts) => {
|
|
1649
|
+
const spinner = ora2();
|
|
1650
|
+
try {
|
|
1651
|
+
const stats = await stat(filePath);
|
|
1652
|
+
if (!stats.isFile()) throw new Error(`${filePath} is not a file.`);
|
|
1653
|
+
const fileName = opts.name ?? basename(filePath);
|
|
1654
|
+
const contentType = lookupMimeType(filePath) || "application/octet-stream";
|
|
1655
|
+
spinner.start("Requesting upload URL");
|
|
1656
|
+
const upload = await api.uploadUrl({
|
|
1657
|
+
fileName,
|
|
1658
|
+
contentType,
|
|
1659
|
+
contentLength: stats.size
|
|
1660
|
+
});
|
|
1661
|
+
spinner.succeed("Got presigned URL");
|
|
1662
|
+
spinner.start(`Uploading ${humanSize(stats.size)}`);
|
|
1663
|
+
const fileStream = Readable.toWeb(createReadStream(filePath));
|
|
1664
|
+
const putRes = await fetch(upload.upload_url, {
|
|
1665
|
+
method: "PUT",
|
|
1666
|
+
headers: {
|
|
1667
|
+
...upload.required_headers,
|
|
1668
|
+
"Content-Length": String(stats.size)
|
|
1669
|
+
},
|
|
1670
|
+
body: fileStream,
|
|
1671
|
+
duplex: "half"
|
|
1672
|
+
});
|
|
1673
|
+
if (!putRes.ok) {
|
|
1674
|
+
const errText = await putRes.text().catch(() => "");
|
|
1675
|
+
throw new Error(
|
|
1676
|
+
`Upload failed: HTTP ${putRes.status} ${putRes.statusText}${errText ? "\n" + errText.slice(0, 500) : ""}`
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
spinner.succeed("Uploaded to storage");
|
|
1680
|
+
spinner.start("Creating document");
|
|
1681
|
+
const doc = await api.createDocument({
|
|
1682
|
+
name: fileName,
|
|
1683
|
+
upload_id: upload.upload_id,
|
|
1684
|
+
folder_id: opts.folderId,
|
|
1685
|
+
create_link: opts.link ?? false
|
|
1686
|
+
});
|
|
1687
|
+
spinner.succeed(`Document created: ${chalk7.bold(doc.id)}`);
|
|
1688
|
+
let link = null;
|
|
1689
|
+
let linkWarning = null;
|
|
1690
|
+
if (opts.link) {
|
|
1691
|
+
try {
|
|
1692
|
+
const links = await api.listLinks({
|
|
1693
|
+
document_id: doc.id,
|
|
1694
|
+
limit: 1
|
|
1695
|
+
});
|
|
1696
|
+
link = links.data[0] ?? null;
|
|
1697
|
+
if (!link) {
|
|
1698
|
+
linkWarning = "--link was set but no link was returned. Try `papermark links list --document " + doc.id + "`.";
|
|
1699
|
+
}
|
|
1700
|
+
} catch (warnErr) {
|
|
1701
|
+
linkWarning = `Document created, but listing the share link failed: ${warnErr instanceof Error ? warnErr.message : String(warnErr)}`;
|
|
1702
|
+
}
|
|
1703
|
+
if (linkWarning) {
|
|
1704
|
+
process.stderr.write(chalk7.yellow("! ") + linkWarning + "\n");
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
emit({ document: doc, link }, () => {
|
|
1708
|
+
const blocks = [json(doc)];
|
|
1709
|
+
if (link) {
|
|
1710
|
+
blocks.push(
|
|
1711
|
+
chalk7.green("\u2713") + " Share link: " + chalk7.bold(link.url)
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
return blocks.join("\n");
|
|
1715
|
+
});
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
spinner.fail();
|
|
1718
|
+
die(err);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
);
|
|
1722
|
+
const versions = docs.command("versions").description("Manage document versions");
|
|
1723
|
+
versions.command("list <id>").description("List versions of a document, primary first").option("--json", "output raw JSON").action(async (id, opts) => {
|
|
1724
|
+
if (opts.json) setRuntime({ json: true });
|
|
1725
|
+
try {
|
|
1726
|
+
const res = await api.listDocumentVersions(id);
|
|
1727
|
+
emit(res, () => {
|
|
1728
|
+
const rows = res.data.map((v) => ({
|
|
1729
|
+
id: v.id,
|
|
1730
|
+
version: String(v.version_number),
|
|
1731
|
+
primary: v.is_primary ? "\u2605" : "",
|
|
1732
|
+
type: v.type ?? "",
|
|
1733
|
+
pages: v.num_pages?.toString() ?? "",
|
|
1734
|
+
size: v.file_size !== null ? humanSize(v.file_size) : "",
|
|
1735
|
+
created: v.created.slice(0, 10)
|
|
1736
|
+
}));
|
|
1737
|
+
return table(rows, [
|
|
1738
|
+
"id",
|
|
1739
|
+
"version",
|
|
1740
|
+
"primary",
|
|
1741
|
+
"type",
|
|
1742
|
+
"pages",
|
|
1743
|
+
"size",
|
|
1744
|
+
"created"
|
|
1745
|
+
]);
|
|
1746
|
+
});
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
die(err);
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
versions.command("get <id> <vid>").description("Fetch a single document version").action(async (id, vid) => {
|
|
1752
|
+
try {
|
|
1753
|
+
const v = await api.getDocumentVersion(id, vid);
|
|
1754
|
+
emit(v, () => json(v));
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
die(err);
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
versions.command("add <id> <file>").description("Upload a new version of an existing document").action(async (id, filePath) => {
|
|
1760
|
+
const spinner = ora2();
|
|
1761
|
+
try {
|
|
1762
|
+
const stats = await stat(filePath);
|
|
1763
|
+
if (!stats.isFile()) throw new Error(`${filePath} is not a file.`);
|
|
1764
|
+
const fileName = basename(filePath);
|
|
1765
|
+
const contentType = lookupMimeType(filePath) || "application/octet-stream";
|
|
1766
|
+
spinner.start("Requesting upload URL");
|
|
1767
|
+
const upload = await api.uploadUrl({
|
|
1768
|
+
fileName,
|
|
1769
|
+
contentType,
|
|
1770
|
+
contentLength: stats.size
|
|
1771
|
+
});
|
|
1772
|
+
spinner.succeed("Got presigned URL");
|
|
1773
|
+
spinner.start(`Uploading ${humanSize(stats.size)}`);
|
|
1774
|
+
const fileStream = Readable.toWeb(createReadStream(filePath));
|
|
1775
|
+
const putRes = await fetch(upload.upload_url, {
|
|
1776
|
+
method: "PUT",
|
|
1777
|
+
headers: {
|
|
1778
|
+
...upload.required_headers,
|
|
1779
|
+
"Content-Length": String(stats.size)
|
|
1780
|
+
},
|
|
1781
|
+
body: fileStream,
|
|
1782
|
+
duplex: "half"
|
|
1783
|
+
});
|
|
1784
|
+
if (!putRes.ok) {
|
|
1785
|
+
const errText = await putRes.text().catch(() => "");
|
|
1786
|
+
throw new Error(
|
|
1787
|
+
`Upload failed: HTTP ${putRes.status} ${putRes.statusText}${errText ? "\n" + errText.slice(0, 500) : ""}`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
spinner.succeed("Uploaded to storage");
|
|
1791
|
+
spinner.start("Adding version");
|
|
1792
|
+
const v = await api.addDocumentVersion(id, { upload_id: upload.upload_id });
|
|
1793
|
+
spinner.succeed(`Version ${v.version_number} added: ${chalk7.bold(v.id)}`);
|
|
1794
|
+
emit(v, () => json(v));
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
spinner.fail();
|
|
1797
|
+
die(err);
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
versions.command("promote <id> <vid>").description("Promote a version to primary").action(async (id, vid) => {
|
|
1801
|
+
try {
|
|
1802
|
+
const v = await api.promoteDocumentVersion(id, vid);
|
|
1803
|
+
emit(
|
|
1804
|
+
v,
|
|
1805
|
+
() => chalk7.green("\u2713") + ` Version ${v.version_number} promoted to primary.
|
|
1806
|
+
` + json(v)
|
|
1807
|
+
);
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
die(err);
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
function truncate3(s, n) {
|
|
1814
|
+
const clean = sanitizeForTerminal(s);
|
|
1815
|
+
return clean.length > n ? clean.slice(0, n - 1) + "\u2026" : clean;
|
|
1816
|
+
}
|
|
1817
|
+
function humanSize(bytes) {
|
|
1818
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
1819
|
+
let n = bytes;
|
|
1820
|
+
let i = 0;
|
|
1821
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
1822
|
+
n /= 1024;
|
|
1823
|
+
i++;
|
|
1824
|
+
}
|
|
1825
|
+
return `${n.toFixed(i === 0 ? 0 : 1)}${units[i]}`;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// src/commands/folders.ts
|
|
1829
|
+
import chalk8 from "chalk";
|
|
1830
|
+
function registerFoldersCommands(program2) {
|
|
1831
|
+
const folders = program2.command("folders").description("Manage team-library folders");
|
|
1832
|
+
folders.command("list").description("List folders in the team library").option(
|
|
1833
|
+
"--parent <id>",
|
|
1834
|
+
"restrict to direct children of this folder (use 'root' for top-level)"
|
|
1835
|
+
).option("-l, --limit <n>", "max results (1-100)", "50").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON").action(
|
|
1836
|
+
async (opts) => {
|
|
1837
|
+
if (opts.json) setRuntime({ json: true });
|
|
1838
|
+
try {
|
|
1839
|
+
const res = await api.listFolders({
|
|
1840
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1841
|
+
cursor: opts.cursor,
|
|
1842
|
+
parent_id: opts.parent
|
|
1843
|
+
});
|
|
1844
|
+
emit(res, () => {
|
|
1845
|
+
const rows = res.data.map((f) => ({
|
|
1846
|
+
id: f.id,
|
|
1847
|
+
name: truncate4(f.name, 30),
|
|
1848
|
+
path: truncate4(f.path, 40),
|
|
1849
|
+
docs: String(f.document_count),
|
|
1850
|
+
children: String(f.child_folder_count)
|
|
1851
|
+
}));
|
|
1852
|
+
const out = table(rows, ["id", "name", "path", "docs", "children"]);
|
|
1853
|
+
return res.next_cursor ? out + chalk8.dim(`
|
|
1854
|
+
|
|
1855
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
1856
|
+
});
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
die(err);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
);
|
|
1862
|
+
folders.command("create").description("Create a folder in the team library").requiredOption("-n, --name <name>", "folder name").option("--parent <id>", "parent folder id (omit for library root)").option("--icon <icon>", "icon name (e.g. folder, briefcase, lock)").option("--color <color>", "color token (gray, red, orange, yellow, green, blue, black)").action(
|
|
1863
|
+
async (opts) => {
|
|
1864
|
+
try {
|
|
1865
|
+
const f = await api.createFolder({
|
|
1866
|
+
name: opts.name,
|
|
1867
|
+
parent_id: opts.parent ?? null,
|
|
1868
|
+
icon: opts.icon,
|
|
1869
|
+
color: opts.color
|
|
1870
|
+
});
|
|
1871
|
+
emit(
|
|
1872
|
+
f,
|
|
1873
|
+
() => chalk8.green("\u2713") + ` Folder created: ${chalk8.bold(f.id)}
|
|
1874
|
+
` + json(f)
|
|
1875
|
+
);
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
die(err);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
);
|
|
1881
|
+
folders.command("get <id>").description("Fetch one folder").action(async (id) => {
|
|
1882
|
+
try {
|
|
1883
|
+
const f = await api.getFolder(id);
|
|
1884
|
+
emit(f, () => json(f));
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
die(err);
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
folders.command("update <id>").description("Update a folder").option("-n, --name <name>").option("--icon <icon>", "icon name (empty string to clear)").option("--color <color>", "color token (empty string to clear)").action(
|
|
1890
|
+
async (id, opts) => {
|
|
1891
|
+
try {
|
|
1892
|
+
const body = {};
|
|
1893
|
+
if (opts.name !== void 0) body.name = opts.name;
|
|
1894
|
+
if (opts.icon !== void 0)
|
|
1895
|
+
body.icon = opts.icon === "" ? null : opts.icon;
|
|
1896
|
+
if (opts.color !== void 0)
|
|
1897
|
+
body.color = opts.color === "" ? null : opts.color;
|
|
1898
|
+
if (Object.keys(body).length === 0) {
|
|
1899
|
+
throw new Error(
|
|
1900
|
+
"Pass at least one of --name, --icon, --color to update."
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
const f = await api.updateFolder(id, body);
|
|
1904
|
+
emit(
|
|
1905
|
+
f,
|
|
1906
|
+
() => chalk8.green("\u2713") + ` Folder updated: ${chalk8.bold(f.id)}
|
|
1907
|
+
` + json(f)
|
|
1908
|
+
);
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
die(err);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
);
|
|
1914
|
+
folders.command("delete <id>").description("Delete a folder").option(
|
|
1915
|
+
"--cascade",
|
|
1916
|
+
"also delete nested folders and every document inside them",
|
|
1917
|
+
false
|
|
1918
|
+
).action(async (id, opts) => {
|
|
1919
|
+
try {
|
|
1920
|
+
const deleted = await api.deleteFolder(id, { cascade: opts.cascade });
|
|
1921
|
+
emit(
|
|
1922
|
+
deleted,
|
|
1923
|
+
() => chalk8.green("\u2713") + ` Folder ${id} deleted.`
|
|
1924
|
+
);
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
die(err);
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
folders.command("move <id>").description("Move a folder under a different parent").option(
|
|
1930
|
+
"--parent <id>",
|
|
1931
|
+
"target parent folder id; pass empty string to move to the library root"
|
|
1932
|
+
).action(async (id, opts) => {
|
|
1933
|
+
try {
|
|
1934
|
+
if (opts.parent === void 0) {
|
|
1935
|
+
throw new Error(
|
|
1936
|
+
"Pass --parent <id> (use empty string to move to the root)."
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
const parent_id = opts.parent === "" ? null : opts.parent;
|
|
1940
|
+
const f = await api.moveFolder(id, { parent_id });
|
|
1941
|
+
emit(
|
|
1942
|
+
f,
|
|
1943
|
+
() => chalk8.green("\u2713") + ` Folder moved. New path: ${chalk8.bold(f.path)}
|
|
1944
|
+
` + json(f)
|
|
1945
|
+
);
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
die(err);
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
function truncate4(s, n) {
|
|
1952
|
+
const clean = sanitizeForTerminal(s);
|
|
1953
|
+
return clean.length > n ? clean.slice(0, n - 1) + "\u2026" : clean;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// src/commands/links.ts
|
|
1957
|
+
import chalk9 from "chalk";
|
|
1958
|
+
function registerLinksCommands(program2) {
|
|
1959
|
+
const links = program2.command("links").description("Manage share links");
|
|
1960
|
+
links.command("list").description("List share links").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--document <id>", "filter by document id").option("--dataroom <id>", "filter by dataroom id").option("--json", "output raw JSON (same as --json on the root command)").action(
|
|
1961
|
+
async (opts) => {
|
|
1962
|
+
if (opts.json) setRuntime({ json: true });
|
|
1963
|
+
try {
|
|
1964
|
+
const res = await api.listLinks({
|
|
1965
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1966
|
+
cursor: opts.cursor,
|
|
1967
|
+
document_id: opts.document,
|
|
1968
|
+
dataroom_id: opts.dataroom
|
|
1969
|
+
});
|
|
1970
|
+
emit(res, () => {
|
|
1971
|
+
const rows = res.data.map((l) => ({
|
|
1972
|
+
id: l.id,
|
|
1973
|
+
name: truncate5(l.name ?? "", 30),
|
|
1974
|
+
url: truncate5(l.url, 45),
|
|
1975
|
+
password: l.is_password_protected ? "yes" : "",
|
|
1976
|
+
expires: l.expires_at ? l.expires_at.slice(0, 10) : ""
|
|
1977
|
+
}));
|
|
1978
|
+
const out = table(rows, ["id", "name", "url", "password", "expires"]);
|
|
1979
|
+
return res.next_cursor ? out + chalk9.dim(`
|
|
1980
|
+
|
|
1981
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
1982
|
+
});
|
|
1983
|
+
} catch (err) {
|
|
1984
|
+
die(err);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
);
|
|
1988
|
+
links.command("get <id>").description("Fetch one share link").action(async (id) => {
|
|
1989
|
+
try {
|
|
1990
|
+
const link = await api.getLink(id);
|
|
1991
|
+
emit(link, () => json(link));
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
die(err);
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
links.command("create").description("Create a share link for a document or dataroom").option("--document <id>", "document id").option("--dataroom <id>", "dataroom id").option("-n, --name <name>", "link name").option("--expires <iso-date>", "expiration date (ISO 8601)").option("--password <pw>", "require a password to view").option("--email-protected", "require email before viewing", false).option("--allow-download", "allow viewer to download the file", false).action(
|
|
1997
|
+
async (opts) => {
|
|
1998
|
+
try {
|
|
1999
|
+
if (!opts.document && !opts.dataroom) {
|
|
2000
|
+
throw new Error("Either --document <id> or --dataroom <id> is required.");
|
|
2001
|
+
}
|
|
2002
|
+
const link = await api.createLink({
|
|
2003
|
+
document_id: opts.document,
|
|
2004
|
+
dataroom_id: opts.dataroom,
|
|
2005
|
+
name: opts.name,
|
|
2006
|
+
expires_at: opts.expires,
|
|
2007
|
+
password: opts.password,
|
|
2008
|
+
email_protected: opts.emailProtected,
|
|
2009
|
+
allow_download: opts.allowDownload
|
|
2010
|
+
});
|
|
2011
|
+
emit(
|
|
2012
|
+
link,
|
|
2013
|
+
() => chalk9.green("\u2713") + " Link created\n" + chalk9.bold("URL: ") + link.url + "\n" + json(link)
|
|
2014
|
+
);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
die(err);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
);
|
|
2020
|
+
links.command("update <id>").description("Update an existing share link").option("-n, --name <name>").option("--expires <iso-date>", "expiration date (ISO 8601), or empty string to clear").option("--password <pw>", "set a password (empty string to clear)").option("--email-protected <on|off>").option("--email-authenticated <on|off>").option("--allow-download <on|off>").option(
|
|
2021
|
+
"--allow-list <items>",
|
|
2022
|
+
"comma-separated email/domain allow list (e.g. '@acme.com,bob@x.com')"
|
|
2023
|
+
).option("--deny-list <items>", "comma-separated email/domain deny list").option("--watermark <on|off>", "enable watermark overlay").option("--screenshot-protection <on|off>", "blur on screenshot").action(
|
|
2024
|
+
async (id, opts) => {
|
|
2025
|
+
try {
|
|
2026
|
+
const tri = (v, flag) => {
|
|
2027
|
+
if (v === void 0) return void 0;
|
|
2028
|
+
const norm = v.toLowerCase();
|
|
2029
|
+
if (norm === "on") return true;
|
|
2030
|
+
if (norm === "off") return false;
|
|
2031
|
+
throw new Error(
|
|
2032
|
+
`Invalid value for ${flag}: ${JSON.stringify(v)} (expected "on" or "off").`
|
|
2033
|
+
);
|
|
2034
|
+
};
|
|
2035
|
+
const list = (v) => v === void 0 ? void 0 : v === "" ? [] : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2036
|
+
const body = {};
|
|
2037
|
+
if (opts.name !== void 0) body.name = opts.name;
|
|
2038
|
+
if (opts.expires !== void 0)
|
|
2039
|
+
body.expires_at = opts.expires === "" ? null : opts.expires;
|
|
2040
|
+
if (opts.password !== void 0)
|
|
2041
|
+
body.password = opts.password === "" ? null : opts.password;
|
|
2042
|
+
const ep = tri(opts.emailProtected, "--email-protected");
|
|
2043
|
+
if (ep !== void 0) body.email_protected = ep;
|
|
2044
|
+
const ea = tri(opts.emailAuthenticated, "--email-authenticated");
|
|
2045
|
+
if (ea !== void 0) body.email_authenticated = ea;
|
|
2046
|
+
const ad = tri(opts.allowDownload, "--allow-download");
|
|
2047
|
+
if (ad !== void 0) body.allow_download = ad;
|
|
2048
|
+
const al = list(opts.allowList);
|
|
2049
|
+
if (al !== void 0) body.allow_list = al;
|
|
2050
|
+
const dl = list(opts.denyList);
|
|
2051
|
+
if (dl !== void 0) body.deny_list = dl;
|
|
2052
|
+
const wm = tri(opts.watermark, "--watermark");
|
|
2053
|
+
if (wm !== void 0) body.enable_watermark = wm;
|
|
2054
|
+
const sp = tri(opts.screenshotProtection, "--screenshot-protection");
|
|
2055
|
+
if (sp !== void 0) body.enable_screenshot_protection = sp;
|
|
2056
|
+
if (Object.keys(body).length === 0) {
|
|
2057
|
+
throw new Error(
|
|
2058
|
+
"Pass at least one field to update (e.g. --name, --expires, --password)."
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const link = await api.updateLink(id, body);
|
|
2062
|
+
emit(
|
|
2063
|
+
link,
|
|
2064
|
+
() => chalk9.green("\u2713") + ` Link updated: ${chalk9.bold(link.id)}
|
|
2065
|
+
` + json(link)
|
|
2066
|
+
);
|
|
2067
|
+
} catch (err) {
|
|
2068
|
+
die(err);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
);
|
|
2072
|
+
links.command("delete <id>").description("Revoke a share link").action(async (id) => {
|
|
2073
|
+
try {
|
|
2074
|
+
const deleted = await api.deleteLink(id);
|
|
2075
|
+
emit(
|
|
2076
|
+
deleted,
|
|
2077
|
+
() => chalk9.green("\u2713") + ` Link ${id} revoked.`
|
|
2078
|
+
);
|
|
2079
|
+
} catch (err) {
|
|
2080
|
+
die(err);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
function truncate5(s, n) {
|
|
2085
|
+
const clean = sanitizeForTerminal(s);
|
|
2086
|
+
return clean.length > n ? clean.slice(0, n - 1) + "\u2026" : clean;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/commands/views.ts
|
|
2090
|
+
import chalk10 from "chalk";
|
|
2091
|
+
function registerViewsCommands(program2) {
|
|
2092
|
+
const views = program2.command("views").description("Read link view analytics");
|
|
2093
|
+
views.command("list").description("List views for a share link").requiredOption("--link <id>", "link id").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON (same as --json on the root command)").action(
|
|
2094
|
+
async (opts) => {
|
|
2095
|
+
if (opts.json) setRuntime({ json: true });
|
|
2096
|
+
try {
|
|
2097
|
+
const res = await api.listViews(opts.link, {
|
|
2098
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
2099
|
+
cursor: opts.cursor
|
|
2100
|
+
});
|
|
2101
|
+
emit(res, () => {
|
|
2102
|
+
const rows = res.data.map((v) => ({
|
|
2103
|
+
id: v.id,
|
|
2104
|
+
viewer: sanitizeForTerminal(v.viewer_email ?? ""),
|
|
2105
|
+
type: v.view_type ?? "",
|
|
2106
|
+
viewed: v.viewed_at.replace("T", " ").slice(0, 16),
|
|
2107
|
+
downloaded: v.downloaded_at ? v.downloaded_at.slice(0, 10) : ""
|
|
2108
|
+
}));
|
|
2109
|
+
const out = table(rows, ["id", "viewer", "type", "viewed", "downloaded"]);
|
|
2110
|
+
return res.next_cursor ? out + chalk10.dim(`
|
|
2111
|
+
|
|
2112
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
2113
|
+
});
|
|
2114
|
+
} catch (err) {
|
|
2115
|
+
die(err);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// src/commands/visitors.ts
|
|
2122
|
+
import chalk11 from "chalk";
|
|
2123
|
+
function registerVisitorsCommands(program2) {
|
|
2124
|
+
const visitors = program2.command("visitors").description("Manage persistent visitors (Viewer rows)");
|
|
2125
|
+
visitors.command("list").description("List visitors").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--email <email>", "exact email match (case-insensitive)").option("--dataroom <id>", "filter by dataroom id").option("--json", "output raw JSON").action(
|
|
2126
|
+
async (opts) => {
|
|
2127
|
+
if (opts.json) setRuntime({ json: true });
|
|
2128
|
+
try {
|
|
2129
|
+
const res = await api.listVisitors({
|
|
2130
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
2131
|
+
cursor: opts.cursor,
|
|
2132
|
+
email: opts.email,
|
|
2133
|
+
dataroom_id: opts.dataroom
|
|
2134
|
+
});
|
|
2135
|
+
emit(res, () => {
|
|
2136
|
+
const rows = res.data.map((v) => ({
|
|
2137
|
+
id: v.id,
|
|
2138
|
+
email: sanitizeForTerminal(v.email),
|
|
2139
|
+
verified: v.verified ? "yes" : "",
|
|
2140
|
+
views: String(v.total_views),
|
|
2141
|
+
last_view: v.last_viewed_at?.slice(0, 10) ?? ""
|
|
2142
|
+
}));
|
|
2143
|
+
const out = table(rows, ["id", "email", "verified", "views", "last_view"]);
|
|
2144
|
+
return res.next_cursor ? out + chalk11.dim(`
|
|
2145
|
+
|
|
2146
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
2147
|
+
});
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
die(err);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
);
|
|
2153
|
+
visitors.command("get <id>").description("Fetch one visitor").action(async (id) => {
|
|
2154
|
+
try {
|
|
2155
|
+
const v = await api.getVisitor(id);
|
|
2156
|
+
emit(v, () => json(v));
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
die(err);
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
visitors.command("views <id>").description("List view events tied to a visitor").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON").action(
|
|
2162
|
+
async (id, opts) => {
|
|
2163
|
+
if (opts.json) setRuntime({ json: true });
|
|
2164
|
+
try {
|
|
2165
|
+
const res = await api.listVisitorViews(id, {
|
|
2166
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
2167
|
+
cursor: opts.cursor
|
|
2168
|
+
});
|
|
2169
|
+
emit(res, () => {
|
|
2170
|
+
const rows = res.data.map((v) => ({
|
|
2171
|
+
id: v.id,
|
|
2172
|
+
link: v.link_id,
|
|
2173
|
+
type: v.view_type,
|
|
2174
|
+
viewed: v.viewed_at.replace("T", " ").slice(0, 16),
|
|
2175
|
+
downloaded: v.downloaded_at ? v.downloaded_at.slice(0, 10) : ""
|
|
2176
|
+
}));
|
|
2177
|
+
const out = table(rows, ["id", "link", "type", "viewed", "downloaded"]);
|
|
2178
|
+
return res.next_cursor ? out + chalk11.dim(`
|
|
2179
|
+
|
|
2180
|
+
More results: --cursor ${res.next_cursor}`) : out;
|
|
2181
|
+
});
|
|
2182
|
+
} catch (err) {
|
|
2183
|
+
die(err);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/index.ts
|
|
2190
|
+
var program = new Command2();
|
|
2191
|
+
program.name("papermark").description("Papermark CLI \u2014 manage documents, links, and views").version("0.1.0").option(
|
|
2192
|
+
"--json",
|
|
2193
|
+
"emit machine-readable JSON on stdout (auto-enabled when piped)"
|
|
2194
|
+
).option(
|
|
2195
|
+
"--dry-run",
|
|
2196
|
+
"print the HTTP request that would be sent and exit (token redacted)"
|
|
2197
|
+
).option("--no-color", "disable ANSI color (same as NO_COLOR=1)").hook("preAction", (thisCommand) => {
|
|
2198
|
+
const opts = thisCommand.opts();
|
|
2199
|
+
setRuntime({
|
|
2200
|
+
json: Boolean(opts.json) || !process.stdout.isTTY,
|
|
2201
|
+
dryRun: Boolean(opts.dryRun),
|
|
2202
|
+
color: opts.color !== false && !process.env.NO_COLOR
|
|
2203
|
+
});
|
|
2204
|
+
});
|
|
2205
|
+
registerAuthCommands(program);
|
|
2206
|
+
registerDoctorCommand(program);
|
|
2207
|
+
registerDocumentsCommands(program);
|
|
2208
|
+
registerFoldersCommands(program);
|
|
2209
|
+
registerLinksCommands(program);
|
|
2210
|
+
registerViewsCommands(program);
|
|
2211
|
+
registerDataroomsCommands(program);
|
|
2212
|
+
registerVisitorsCommands(program);
|
|
2213
|
+
registerAnalyticsCommands(program);
|
|
2214
|
+
registerConfigCommands(program);
|
|
2215
|
+
program.parseAsync(process.argv);
|
|
2216
|
+
//# sourceMappingURL=index.js.map
|