vellum-cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/index.js +761 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,17 +17,41 @@ vellum push --help
|
|
|
17
17
|
Requires Node 18+. The package is self-contained (`@vellum/core` is bundled in),
|
|
18
18
|
so it has no runtime dependencies.
|
|
19
19
|
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
The normal path is `vellum login`, which signs you in through your browser and
|
|
23
|
+
stores a per-user token under `~/.config/vellum/config.json`
|
|
24
|
+
(`$XDG_CONFIG_HOME` is honored). After that, commands authenticate as you with
|
|
25
|
+
no further setup.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
vellum login # opens the browser, approve, done
|
|
29
|
+
vellum whoami # who am I logged in as?
|
|
30
|
+
vellum logout # revoke + forget this machine's token
|
|
31
|
+
```
|
|
32
|
+
|
|
20
33
|
## Configuration
|
|
21
34
|
|
|
22
|
-
|
|
35
|
+
`--url` defaults to the hosted instance (`https://vellumai.app`); override it
|
|
36
|
+
for self-hosted servers. Connection settings come from flags, falling back to
|
|
37
|
+
env vars:
|
|
38
|
+
|
|
39
|
+
| Flag | Env var | Description |
|
|
40
|
+
| ------------ | ---------------- | -------------------------------------------- |
|
|
41
|
+
| `--url` | `VELLUM_URL` | Server base URL (default `https://vellumai.app`) |
|
|
42
|
+
| `--api-key` | `VELLUM_API_KEY` | Shared owner/agent key (`X-Api-Key`) |
|
|
23
43
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| `--api-key` | `VELLUM_API_KEY` | Shared API key (`X-Api-Key`) |
|
|
44
|
+
Credential precedence for a write: an explicit `--api-key`/`VELLUM_API_KEY`
|
|
45
|
+
(the shared owner key, e.g. for agents/CI) wins; otherwise the stored
|
|
46
|
+
`vellum login` token is used.
|
|
28
47
|
|
|
29
48
|
## Commands
|
|
30
49
|
|
|
50
|
+
### `vellum login`
|
|
51
|
+
|
|
52
|
+
Authenticate this machine. Opens the browser to approve, polls until you do,
|
|
53
|
+
then stores the token. `--no-browser` just prints the URL to open manually.
|
|
54
|
+
|
|
31
55
|
### `vellum push [file]`
|
|
32
56
|
|
|
33
57
|
Create an artifact from HTML. Reads from `<file>` if given, otherwise from
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
var DEFAULT_URL = "https://vellumai.app";
|
|
8
|
+
|
|
4
9
|
class ConfigError extends Error {
|
|
5
10
|
}
|
|
6
|
-
function
|
|
7
|
-
|
|
11
|
+
function normalizeUrl(url) {
|
|
12
|
+
return url.replace(/\/+$/, "");
|
|
13
|
+
}
|
|
14
|
+
function resolveBaseUrl(flags) {
|
|
15
|
+
return normalizeUrl(flags.url ?? process.env.VELLUM_URL ?? DEFAULT_URL);
|
|
16
|
+
}
|
|
17
|
+
async function resolveCredential(flags, baseUrl) {
|
|
8
18
|
const apiKey = flags.apiKey ?? process.env.VELLUM_API_KEY;
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
19
|
+
if (apiKey)
|
|
20
|
+
return { apiKey };
|
|
21
|
+
const token = await loadToken(baseUrl);
|
|
22
|
+
if (token)
|
|
23
|
+
return { token };
|
|
24
|
+
throw new ConfigError(`Not logged in to ${baseUrl}. Run \`vellum login\` (or set VELLUM_API_KEY / pass --api-key).`);
|
|
25
|
+
}
|
|
26
|
+
function storePath() {
|
|
27
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
28
|
+
const base = xdg && xdg.trim().length > 0 ? xdg : join(homedir(), ".config");
|
|
29
|
+
return join(base, "vellum", "config.json");
|
|
30
|
+
}
|
|
31
|
+
async function readStore() {
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(storePath(), "utf8");
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
return { servers: parsed.servers ?? {} };
|
|
36
|
+
} catch {
|
|
37
|
+
return { servers: {} };
|
|
14
38
|
}
|
|
15
|
-
|
|
39
|
+
}
|
|
40
|
+
async function writeStore(store) {
|
|
41
|
+
const path = storePath();
|
|
42
|
+
await mkdir(dirname(path), { recursive: true });
|
|
43
|
+
await writeFile(path, `${JSON.stringify(store, null, 2)}
|
|
44
|
+
`, { mode: 384 });
|
|
45
|
+
}
|
|
46
|
+
async function loadToken(baseUrl) {
|
|
47
|
+
const store = await readStore();
|
|
48
|
+
return store.servers[normalizeUrl(baseUrl)]?.token;
|
|
49
|
+
}
|
|
50
|
+
async function saveToken(baseUrl, token, email) {
|
|
51
|
+
const store = await readStore();
|
|
52
|
+
store.servers[normalizeUrl(baseUrl)] = { token, email };
|
|
53
|
+
await writeStore(store);
|
|
54
|
+
}
|
|
55
|
+
async function clearToken(baseUrl) {
|
|
56
|
+
const store = await readStore();
|
|
57
|
+
const key = normalizeUrl(baseUrl);
|
|
58
|
+
const existing = store.servers[key]?.token;
|
|
59
|
+
delete store.servers[key];
|
|
60
|
+
await writeStore(store);
|
|
61
|
+
return existing;
|
|
16
62
|
}
|
|
17
63
|
|
|
18
|
-
// src/commands/
|
|
19
|
-
import { readFile } from "node:fs/promises";
|
|
64
|
+
// src/commands/comments.ts
|
|
20
65
|
import { parseArgs } from "node:util";
|
|
21
66
|
// ../core/src/client.ts
|
|
22
67
|
class VellumApiError extends Error {
|
|
@@ -33,12 +78,25 @@ class VellumApiError extends Error {
|
|
|
33
78
|
class VellumClient {
|
|
34
79
|
baseUrl;
|
|
35
80
|
apiKey;
|
|
81
|
+
token;
|
|
36
82
|
fetchImpl;
|
|
37
83
|
constructor(opts) {
|
|
38
84
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
39
85
|
this.apiKey = opts.apiKey;
|
|
86
|
+
this.token = opts.token;
|
|
40
87
|
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
41
88
|
}
|
|
89
|
+
authHeaders() {
|
|
90
|
+
if (this.token)
|
|
91
|
+
return { Authorization: `Bearer ${this.token}` };
|
|
92
|
+
if (this.apiKey)
|
|
93
|
+
return { "X-Api-Key": this.apiKey };
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
async fail(res) {
|
|
97
|
+
const code = await res.clone().json().then((b) => b.error).catch(() => res.statusText);
|
|
98
|
+
throw new VellumApiError(res.status, code);
|
|
99
|
+
}
|
|
42
100
|
async createPage(html, opts = {}) {
|
|
43
101
|
const url = new URL(`${this.baseUrl}/v1/pages`);
|
|
44
102
|
if (opts.pageId)
|
|
@@ -47,19 +105,506 @@ class VellumClient {
|
|
|
47
105
|
method: "POST",
|
|
48
106
|
headers: {
|
|
49
107
|
"Content-Type": "text/html; charset=utf-8",
|
|
50
|
-
|
|
108
|
+
...this.authHeaders()
|
|
51
109
|
},
|
|
52
110
|
body: html
|
|
53
111
|
});
|
|
54
|
-
if (!res.ok)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
await this.fail(res);
|
|
114
|
+
return await res.json();
|
|
115
|
+
}
|
|
116
|
+
async getPage(pageId) {
|
|
117
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/pages/${encodeURIComponent(pageId)}`, { headers: this.authHeaders() });
|
|
118
|
+
if (!res.ok)
|
|
119
|
+
await this.fail(res);
|
|
120
|
+
return await res.json();
|
|
121
|
+
}
|
|
122
|
+
async createComment(pageId, opts) {
|
|
123
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/pages/${encodeURIComponent(pageId)}/comments`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "Content-Type": "application/json", ...this.authHeaders() },
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
version_id: opts.versionId,
|
|
128
|
+
body: opts.body,
|
|
129
|
+
anchor: opts.anchor,
|
|
130
|
+
parent_id: opts.parentId
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok)
|
|
134
|
+
await this.fail(res);
|
|
135
|
+
return await res.json();
|
|
136
|
+
}
|
|
137
|
+
async listComments(pageId, opts = {}) {
|
|
138
|
+
const url = new URL(`${this.baseUrl}/v1/pages/${encodeURIComponent(pageId)}/comments`);
|
|
139
|
+
if (opts.versionId)
|
|
140
|
+
url.searchParams.set("version_id", opts.versionId);
|
|
141
|
+
const res = await this.fetchImpl(url, { headers: this.authHeaders() });
|
|
142
|
+
if (!res.ok)
|
|
143
|
+
await this.fail(res);
|
|
144
|
+
return await res.json();
|
|
145
|
+
}
|
|
146
|
+
async resolveComment(commentId) {
|
|
147
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/comments/${encodeURIComponent(commentId)}/resolve`, { method: "POST", headers: this.authHeaders() });
|
|
148
|
+
if (!res.ok)
|
|
149
|
+
await this.fail(res);
|
|
150
|
+
return await res.json();
|
|
151
|
+
}
|
|
152
|
+
async startCliAuth() {
|
|
153
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/start`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "Content-Type": "application/json" },
|
|
156
|
+
body: "{}"
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok)
|
|
159
|
+
await this.fail(res);
|
|
160
|
+
return await res.json();
|
|
161
|
+
}
|
|
162
|
+
async pollCliAuth(deviceCode) {
|
|
163
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/poll`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: { "Content-Type": "application/json" },
|
|
166
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok)
|
|
169
|
+
await this.fail(res);
|
|
58
170
|
return await res.json();
|
|
59
171
|
}
|
|
172
|
+
async whoami() {
|
|
173
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/whoami`, {
|
|
174
|
+
headers: this.authHeaders()
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok)
|
|
177
|
+
await this.fail(res);
|
|
178
|
+
return await res.json();
|
|
179
|
+
}
|
|
180
|
+
async logout() {
|
|
181
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/logout`, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: this.authHeaders()
|
|
184
|
+
});
|
|
185
|
+
if (!res.ok)
|
|
186
|
+
await this.fail(res);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// src/commands/comments.ts
|
|
190
|
+
var HELP = `vellum comments — list the comments on a document
|
|
191
|
+
|
|
192
|
+
Usage:
|
|
193
|
+
vellum comments <page-id> [options]
|
|
194
|
+
|
|
195
|
+
Reads are public, so no login is required. Comments print as threads; replies
|
|
196
|
+
are indented under their parent.
|
|
197
|
+
|
|
198
|
+
Options:
|
|
199
|
+
--open Show only unresolved threads
|
|
200
|
+
--version <id> Only comments anchored to this version id
|
|
201
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
202
|
+
--json Print the raw JSON response
|
|
203
|
+
-h, --help Show this help`;
|
|
204
|
+
var truncate = (s, n) => s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
205
|
+
var oneLine = (s) => s.replace(/\s+/g, " ").trim();
|
|
206
|
+
function printComment(c, indent) {
|
|
207
|
+
const who = c.author_email ?? c.author_name ?? "(unknown)";
|
|
208
|
+
const state = c.resolved ? "resolved" : "open";
|
|
209
|
+
console.log(`${indent}${c.id} [${state}] ${who}`);
|
|
210
|
+
if (c.anchor?.exact) {
|
|
211
|
+
console.log(`${indent} on “${truncate(oneLine(c.anchor.exact), 70)}”`);
|
|
212
|
+
}
|
|
213
|
+
console.log(`${indent} ${truncate(oneLine(c.body), 100)}`);
|
|
214
|
+
for (const reply of c.replies ?? []) {
|
|
215
|
+
printComment(reply, `${indent} `);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function commentsCommand(argv) {
|
|
219
|
+
const { values, positionals } = parseArgs({
|
|
220
|
+
args: argv,
|
|
221
|
+
allowPositionals: true,
|
|
222
|
+
options: {
|
|
223
|
+
open: { type: "boolean", default: false },
|
|
224
|
+
version: { type: "string" },
|
|
225
|
+
url: { type: "string" },
|
|
226
|
+
json: { type: "boolean", default: false },
|
|
227
|
+
help: { type: "boolean", short: "h", default: false }
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
if (values.help) {
|
|
231
|
+
console.log(HELP);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
const pageId = positionals[0];
|
|
235
|
+
if (!pageId) {
|
|
236
|
+
console.error(`Error: missing <page-id>.
|
|
237
|
+
`);
|
|
238
|
+
console.error(HELP);
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
242
|
+
const client2 = new VellumClient({ baseUrl });
|
|
243
|
+
let comments = await client2.listComments(pageId, {
|
|
244
|
+
versionId: values.version
|
|
245
|
+
});
|
|
246
|
+
if (values.open)
|
|
247
|
+
comments = comments.filter((c) => !c.resolved);
|
|
248
|
+
if (values.json) {
|
|
249
|
+
console.log(JSON.stringify(comments, null, 2));
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
if (comments.length === 0) {
|
|
253
|
+
console.log(values.open ? "No open comments." : "No comments.");
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
for (const c of comments)
|
|
257
|
+
printComment(c, "");
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/commands/get.ts
|
|
262
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
263
|
+
var HELP2 = `vellum get — print a document's raw HTML
|
|
264
|
+
|
|
265
|
+
Usage:
|
|
266
|
+
vellum get <page-id> [options]
|
|
267
|
+
|
|
268
|
+
Reads are public, so no login is required. Without --version, the page's
|
|
269
|
+
current version is printed.
|
|
270
|
+
|
|
271
|
+
Options:
|
|
272
|
+
--version <n> Print a specific version number (default: current)
|
|
273
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
274
|
+
-h, --help Show this help`;
|
|
275
|
+
function resolveVersion(page, requested) {
|
|
276
|
+
if (requested != null) {
|
|
277
|
+
return page.versions.find((v) => v.version_number === requested) ?? null;
|
|
278
|
+
}
|
|
279
|
+
const current = page.current_version_id ? page.versions.find((v) => v.id === page.current_version_id) : undefined;
|
|
280
|
+
return current ?? page.versions[page.versions.length - 1] ?? null;
|
|
60
281
|
}
|
|
282
|
+
async function getCommand(argv) {
|
|
283
|
+
const { values, positionals } = parseArgs2({
|
|
284
|
+
args: argv,
|
|
285
|
+
allowPositionals: true,
|
|
286
|
+
options: {
|
|
287
|
+
version: { type: "string" },
|
|
288
|
+
url: { type: "string" },
|
|
289
|
+
help: { type: "boolean", short: "h", default: false }
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
if (values.help) {
|
|
293
|
+
console.log(HELP2);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
const pageId = positionals[0];
|
|
297
|
+
if (!pageId) {
|
|
298
|
+
console.error(`Error: missing <page-id>.
|
|
299
|
+
`);
|
|
300
|
+
console.error(HELP2);
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
let version;
|
|
304
|
+
if (values.version != null) {
|
|
305
|
+
version = Number.parseInt(values.version, 10);
|
|
306
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
307
|
+
console.error("Error: --version must be a positive integer.");
|
|
308
|
+
return 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
312
|
+
const client2 = new VellumClient({ baseUrl });
|
|
313
|
+
const page = await client2.getPage(pageId);
|
|
314
|
+
const target = resolveVersion(page, version);
|
|
315
|
+
if (!target) {
|
|
316
|
+
console.error(version != null ? `Error: page has no version ${version}.` : "Error: page has no versions.");
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
const res = await fetch(target.raw_url);
|
|
320
|
+
if (!res.ok) {
|
|
321
|
+
console.error(`Error: could not fetch raw HTML (${res.status}).`);
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
process.stdout.write(await res.text());
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/commands/login.ts
|
|
329
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
330
|
+
|
|
331
|
+
// src/util/open.ts
|
|
332
|
+
import { spawn } from "node:child_process";
|
|
333
|
+
function openBrowser(url) {
|
|
334
|
+
const platform = process.platform;
|
|
335
|
+
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
336
|
+
try {
|
|
337
|
+
const child = spawn(cmd, args, {
|
|
338
|
+
stdio: "ignore",
|
|
339
|
+
detached: true
|
|
340
|
+
});
|
|
341
|
+
child.on("error", () => {});
|
|
342
|
+
child.unref();
|
|
343
|
+
} catch {}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/commands/login.ts
|
|
347
|
+
var HELP3 = `vellum login — authenticate this machine to a Vellum server
|
|
348
|
+
|
|
349
|
+
Opens your browser to approve a CLI login, then stores a token under
|
|
350
|
+
~/.config/vellum/config.json so future commands authenticate as you.
|
|
351
|
+
|
|
352
|
+
Usage:
|
|
353
|
+
vellum login [options]
|
|
354
|
+
|
|
355
|
+
Options:
|
|
356
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
357
|
+
--no-browser Don't auto-open the browser; just print the URL
|
|
358
|
+
-h, --help Show this help`;
|
|
359
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
360
|
+
async function loginCommand(argv) {
|
|
361
|
+
const { values } = parseArgs3({
|
|
362
|
+
args: argv,
|
|
363
|
+
options: {
|
|
364
|
+
url: { type: "string" },
|
|
365
|
+
"no-browser": { type: "boolean", default: false },
|
|
366
|
+
help: { type: "boolean", short: "h", default: false }
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
if (values.help) {
|
|
370
|
+
console.log(HELP3);
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
374
|
+
const client2 = new VellumClient({ baseUrl });
|
|
375
|
+
const start = await client2.startCliAuth();
|
|
376
|
+
console.log(`
|
|
377
|
+
To authorize this CLI, visit:
|
|
378
|
+
`);
|
|
379
|
+
console.log(` ${start.verification_uri_complete}
|
|
380
|
+
`);
|
|
381
|
+
console.log(`and confirm the code: ${start.user_code}
|
|
382
|
+
`);
|
|
383
|
+
if (!values["no-browser"]) {
|
|
384
|
+
openBrowser(start.verification_uri_complete);
|
|
385
|
+
console.log(`(Opening your browser…)
|
|
386
|
+
`);
|
|
387
|
+
}
|
|
388
|
+
process.stdout.write("Waiting for approval");
|
|
389
|
+
const deadline = Date.now() + start.expires_in * 1000;
|
|
390
|
+
const intervalMs = Math.max(1, start.interval) * 1000;
|
|
391
|
+
while (Date.now() < deadline) {
|
|
392
|
+
await sleep(intervalMs);
|
|
393
|
+
process.stdout.write(".");
|
|
394
|
+
const res = await client2.pollCliAuth(start.device_code);
|
|
395
|
+
if (res.status === "complete") {
|
|
396
|
+
await saveToken(baseUrl, res.token, res.email);
|
|
397
|
+
console.log(`
|
|
398
|
+
|
|
399
|
+
✓ Logged in to ${baseUrl}${res.email ? ` as ${res.email}` : ""}.`);
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
if (res.status === "expired")
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
console.error("\n\nError: login request expired. Run `vellum login` again.");
|
|
406
|
+
return 1;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/commands/logout.ts
|
|
410
|
+
import { parseArgs as parseArgs4 } from "node:util";
|
|
411
|
+
var HELP4 = `vellum logout — remove this machine's stored CLI token
|
|
412
|
+
|
|
413
|
+
Usage:
|
|
414
|
+
vellum logout [options]
|
|
415
|
+
|
|
416
|
+
Options:
|
|
417
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
418
|
+
-h, --help Show this help`;
|
|
419
|
+
async function logoutCommand(argv) {
|
|
420
|
+
const { values } = parseArgs4({
|
|
421
|
+
args: argv,
|
|
422
|
+
options: {
|
|
423
|
+
url: { type: "string" },
|
|
424
|
+
help: { type: "boolean", short: "h", default: false }
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
if (values.help) {
|
|
428
|
+
console.log(HELP4);
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
432
|
+
const token = await clearToken(baseUrl);
|
|
433
|
+
if (!token) {
|
|
434
|
+
console.log(`Not logged in to ${baseUrl}.`);
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
await new VellumClient({ baseUrl, token }).logout();
|
|
439
|
+
} catch {}
|
|
440
|
+
console.log(`✓ Logged out of ${baseUrl}.`);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/commands/markup.ts
|
|
445
|
+
import { parseArgs as parseArgs5 } from "node:util";
|
|
446
|
+
var HELP5 = `vellum markup — pin a comment to a passage of a document
|
|
447
|
+
|
|
448
|
+
Usage:
|
|
449
|
+
vellum markup <page-id> --quote "<text>" --body "<note>" [options]
|
|
450
|
+
echo "<note>" | vellum markup <page-id> --quote "<text>"
|
|
451
|
+
|
|
452
|
+
The --quote text must appear verbatim in the document; the viewer highlights it
|
|
453
|
+
and links the highlight to your note. Without --version, the page's current
|
|
454
|
+
version is used.
|
|
455
|
+
|
|
456
|
+
Options:
|
|
457
|
+
--quote <text> Passage to mark up (must match the document text)
|
|
458
|
+
--body <note> Your note (or pipe it via stdin)
|
|
459
|
+
--version <n> Pin to a specific version number (default: current)
|
|
460
|
+
--force Post even if the quote isn't found in the version
|
|
461
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
462
|
+
--api-key <key> Shared API key (env: VELLUM_API_KEY)
|
|
463
|
+
--json Print the raw JSON response
|
|
464
|
+
-h, --help Show this help`;
|
|
465
|
+
async function readStdin() {
|
|
466
|
+
const chunks = [];
|
|
467
|
+
for await (const chunk of process.stdin)
|
|
468
|
+
chunks.push(chunk);
|
|
469
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
470
|
+
}
|
|
471
|
+
function resolveVersion2(page, requested) {
|
|
472
|
+
if (requested != null) {
|
|
473
|
+
return page.versions.find((v) => v.version_number === requested) ?? null;
|
|
474
|
+
}
|
|
475
|
+
const current = page.current_version_id ? page.versions.find((v) => v.id === page.current_version_id) : undefined;
|
|
476
|
+
return current ?? page.versions[page.versions.length - 1] ?? null;
|
|
477
|
+
}
|
|
478
|
+
function extractText(html) {
|
|
479
|
+
return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
480
|
+
}
|
|
481
|
+
var collapse = (s) => s.replace(/\s+/g, " ").trim();
|
|
482
|
+
function quoteIsAnchorable(html, quote) {
|
|
483
|
+
const text = extractText(html);
|
|
484
|
+
return text.includes(quote) || collapse(text).includes(collapse(quote));
|
|
485
|
+
}
|
|
486
|
+
async function markupCommand(argv) {
|
|
487
|
+
const { values, positionals } = parseArgs5({
|
|
488
|
+
args: argv,
|
|
489
|
+
allowPositionals: true,
|
|
490
|
+
options: {
|
|
491
|
+
quote: { type: "string" },
|
|
492
|
+
body: { type: "string" },
|
|
493
|
+
version: { type: "string" },
|
|
494
|
+
force: { type: "boolean", default: false },
|
|
495
|
+
url: { type: "string" },
|
|
496
|
+
"api-key": { type: "string" },
|
|
497
|
+
json: { type: "boolean", default: false },
|
|
498
|
+
help: { type: "boolean", short: "h", default: false }
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
if (values.help) {
|
|
502
|
+
console.log(HELP5);
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
const pageId = positionals[0];
|
|
506
|
+
if (!pageId) {
|
|
507
|
+
console.error(`Error: missing <page-id>.
|
|
508
|
+
`);
|
|
509
|
+
console.error(HELP5);
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
512
|
+
const quote = (values.quote ?? "").trim();
|
|
513
|
+
if (!quote) {
|
|
514
|
+
console.error("Error: --quote is required (the passage to mark up).");
|
|
515
|
+
return 1;
|
|
516
|
+
}
|
|
517
|
+
const body = (values.body ?? await readStdin()).trim();
|
|
518
|
+
if (!body) {
|
|
519
|
+
console.error("Error: no note provided (pass --body or pipe it via stdin).");
|
|
520
|
+
return 1;
|
|
521
|
+
}
|
|
522
|
+
let version;
|
|
523
|
+
if (values.version != null) {
|
|
524
|
+
version = Number.parseInt(values.version, 10);
|
|
525
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
526
|
+
console.error("Error: --version must be a positive integer.");
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const flags = { url: values.url, apiKey: values["api-key"] };
|
|
531
|
+
const baseUrl = resolveBaseUrl(flags);
|
|
532
|
+
const credential = await resolveCredential(flags, baseUrl);
|
|
533
|
+
const client2 = new VellumClient({ baseUrl, ...credential });
|
|
534
|
+
const page = await client2.getPage(pageId);
|
|
535
|
+
const target = resolveVersion2(page, version);
|
|
536
|
+
if (!target) {
|
|
537
|
+
console.error(version != null ? `Error: page has no version ${version}.` : "Error: page has no versions to mark up.");
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
if (!values.force) {
|
|
541
|
+
const html = await fetch(target.raw_url).then((r) => r.ok ? r.text() : "");
|
|
542
|
+
if (!quoteIsAnchorable(html, quote)) {
|
|
543
|
+
console.error(`Error: --quote was not found in v${target.version_number}; the highlight
|
|
544
|
+
` + " would not appear. Check the wording, or pass --force to post anyway.");
|
|
545
|
+
return 1;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const anchor = { type: "text-quote", exact: quote };
|
|
549
|
+
const comment = await client2.createComment(pageId, {
|
|
550
|
+
versionId: target.id,
|
|
551
|
+
body,
|
|
552
|
+
anchor
|
|
553
|
+
});
|
|
554
|
+
if (values.json) {
|
|
555
|
+
console.log(JSON.stringify(comment, null, 2));
|
|
556
|
+
} else {
|
|
557
|
+
console.log("✓ markup added");
|
|
558
|
+
console.log(` on: “${quote.length > 80 ? `${quote.slice(0, 79)}…` : quote}”`);
|
|
559
|
+
console.log(` view: ${page.view_url}`);
|
|
560
|
+
}
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/commands/open.ts
|
|
565
|
+
import { parseArgs as parseArgs6 } from "node:util";
|
|
566
|
+
var HELP6 = `vellum open — print (and open) a document's view URL
|
|
567
|
+
|
|
568
|
+
Usage:
|
|
569
|
+
vellum open <page-id> [options]
|
|
570
|
+
|
|
571
|
+
Options:
|
|
572
|
+
--no-browser Just print the URL; don't open the browser
|
|
573
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
574
|
+
-h, --help Show this help`;
|
|
575
|
+
async function openCommand(argv) {
|
|
576
|
+
const { values, positionals } = parseArgs6({
|
|
577
|
+
args: argv,
|
|
578
|
+
allowPositionals: true,
|
|
579
|
+
options: {
|
|
580
|
+
"no-browser": { type: "boolean", default: false },
|
|
581
|
+
url: { type: "string" },
|
|
582
|
+
help: { type: "boolean", short: "h", default: false }
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
if (values.help) {
|
|
586
|
+
console.log(HELP6);
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
589
|
+
const pageId = positionals[0];
|
|
590
|
+
if (!pageId) {
|
|
591
|
+
console.error(`Error: missing <page-id>.
|
|
592
|
+
`);
|
|
593
|
+
console.error(HELP6);
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
596
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
597
|
+
const page = await new VellumClient({ baseUrl }).getPage(pageId);
|
|
598
|
+
console.log(page.view_url);
|
|
599
|
+
if (!values["no-browser"])
|
|
600
|
+
openBrowser(page.view_url);
|
|
601
|
+
return 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
61
604
|
// src/commands/push.ts
|
|
62
|
-
|
|
605
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
606
|
+
import { parseArgs as parseArgs7 } from "node:util";
|
|
607
|
+
var HELP7 = `vellum push — create a Vellum artifact from HTML
|
|
63
608
|
|
|
64
609
|
Usage:
|
|
65
610
|
vellum push <file.html> [options]
|
|
@@ -71,14 +616,14 @@ Options:
|
|
|
71
616
|
--api-key <key> Shared API key (env: VELLUM_API_KEY)
|
|
72
617
|
--json Print the raw JSON response
|
|
73
618
|
-h, --help Show this help`;
|
|
74
|
-
async function
|
|
619
|
+
async function readStdin2() {
|
|
75
620
|
const chunks = [];
|
|
76
621
|
for await (const chunk of process.stdin)
|
|
77
622
|
chunks.push(chunk);
|
|
78
623
|
return Buffer.concat(chunks).toString("utf8");
|
|
79
624
|
}
|
|
80
625
|
async function pushCommand(argv) {
|
|
81
|
-
const { values, positionals } =
|
|
626
|
+
const { values, positionals } = parseArgs7({
|
|
82
627
|
args: argv,
|
|
83
628
|
allowPositionals: true,
|
|
84
629
|
options: {
|
|
@@ -90,20 +635,19 @@ async function pushCommand(argv) {
|
|
|
90
635
|
}
|
|
91
636
|
});
|
|
92
637
|
if (values.help) {
|
|
93
|
-
console.log(
|
|
638
|
+
console.log(HELP7);
|
|
94
639
|
return 0;
|
|
95
640
|
}
|
|
96
641
|
const file = positionals[0];
|
|
97
|
-
const html = file ? await
|
|
642
|
+
const html = file ? await readFile2(file, "utf8") : await readStdin2();
|
|
98
643
|
if (!html.trim()) {
|
|
99
644
|
console.error("Error: no HTML provided (empty file or stdin).");
|
|
100
645
|
return 1;
|
|
101
646
|
}
|
|
102
|
-
const {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
const client2 = new VellumClient({ baseUrl, apiKey });
|
|
647
|
+
const flags = { url: values.url, apiKey: values["api-key"] };
|
|
648
|
+
const baseUrl = resolveBaseUrl(flags);
|
|
649
|
+
const credential = await resolveCredential(flags, baseUrl);
|
|
650
|
+
const client2 = new VellumClient({ baseUrl, ...credential });
|
|
107
651
|
const res = await client2.createPage(html, { pageId: values["page-id"] });
|
|
108
652
|
if (values.json) {
|
|
109
653
|
console.log(JSON.stringify(res, null, 2));
|
|
@@ -115,30 +659,218 @@ async function pushCommand(argv) {
|
|
|
115
659
|
return 0;
|
|
116
660
|
}
|
|
117
661
|
|
|
662
|
+
// src/commands/reply.ts
|
|
663
|
+
import { parseArgs as parseArgs8 } from "node:util";
|
|
664
|
+
var HELP8 = `vellum reply — reply to a comment thread
|
|
665
|
+
|
|
666
|
+
Usage:
|
|
667
|
+
vellum reply <page-id> <comment-id> --body "<note>"
|
|
668
|
+
echo "<note>" | vellum reply <page-id> <comment-id>
|
|
669
|
+
|
|
670
|
+
The reply is threaded under <comment-id> and inherits the parent's version.
|
|
671
|
+
Run \`vellum comments <page-id>\` to find the page and comment ids.
|
|
672
|
+
|
|
673
|
+
Options:
|
|
674
|
+
--body <note> Your reply (or pipe it via stdin)
|
|
675
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
676
|
+
--api-key <key> Shared API key (env: VELLUM_API_KEY)
|
|
677
|
+
--json Print the raw JSON response
|
|
678
|
+
-h, --help Show this help`;
|
|
679
|
+
async function readStdin3() {
|
|
680
|
+
const chunks = [];
|
|
681
|
+
for await (const chunk of process.stdin)
|
|
682
|
+
chunks.push(chunk);
|
|
683
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
684
|
+
}
|
|
685
|
+
function flatten(comments) {
|
|
686
|
+
const out = [];
|
|
687
|
+
const walk = (c) => {
|
|
688
|
+
out.push(c);
|
|
689
|
+
for (const reply of c.replies ?? [])
|
|
690
|
+
walk(reply);
|
|
691
|
+
};
|
|
692
|
+
for (const c of comments)
|
|
693
|
+
walk(c);
|
|
694
|
+
return out;
|
|
695
|
+
}
|
|
696
|
+
async function replyCommand(argv) {
|
|
697
|
+
const { values, positionals } = parseArgs8({
|
|
698
|
+
args: argv,
|
|
699
|
+
allowPositionals: true,
|
|
700
|
+
options: {
|
|
701
|
+
body: { type: "string" },
|
|
702
|
+
url: { type: "string" },
|
|
703
|
+
"api-key": { type: "string" },
|
|
704
|
+
json: { type: "boolean", default: false },
|
|
705
|
+
help: { type: "boolean", short: "h", default: false }
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
if (values.help) {
|
|
709
|
+
console.log(HELP8);
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
712
|
+
const [pageId, commentId] = positionals;
|
|
713
|
+
if (!pageId || !commentId) {
|
|
714
|
+
console.error("Error: usage is `vellum reply <page-id> <comment-id>`.\n");
|
|
715
|
+
console.error(HELP8);
|
|
716
|
+
return 1;
|
|
717
|
+
}
|
|
718
|
+
const body = (values.body ?? await readStdin3()).trim();
|
|
719
|
+
if (!body) {
|
|
720
|
+
console.error("Error: no note provided (pass --body or pipe it via stdin).");
|
|
721
|
+
return 1;
|
|
722
|
+
}
|
|
723
|
+
const flags = { url: values.url, apiKey: values["api-key"] };
|
|
724
|
+
const baseUrl = resolveBaseUrl(flags);
|
|
725
|
+
const credential = await resolveCredential(flags, baseUrl);
|
|
726
|
+
const client2 = new VellumClient({ baseUrl, ...credential });
|
|
727
|
+
const parent = flatten(await client2.listComments(pageId)).find((c) => c.id === commentId);
|
|
728
|
+
if (!parent) {
|
|
729
|
+
console.error(`Error: no comment ${commentId} on page ${pageId}.`);
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
732
|
+
if (!parent.version_id) {
|
|
733
|
+
console.error("Error: parent comment has no version to reply on.");
|
|
734
|
+
return 1;
|
|
735
|
+
}
|
|
736
|
+
const reply = await client2.createComment(pageId, {
|
|
737
|
+
versionId: parent.version_id,
|
|
738
|
+
body,
|
|
739
|
+
parentId: commentId
|
|
740
|
+
});
|
|
741
|
+
if (values.json) {
|
|
742
|
+
console.log(JSON.stringify(reply, null, 2));
|
|
743
|
+
} else {
|
|
744
|
+
console.log(`✓ replied to ${commentId}`);
|
|
745
|
+
}
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/commands/resolve.ts
|
|
750
|
+
import { parseArgs as parseArgs9 } from "node:util";
|
|
751
|
+
var HELP9 = `vellum resolve — mark a comment as resolved
|
|
752
|
+
|
|
753
|
+
Usage:
|
|
754
|
+
vellum resolve <comment-id> [options]
|
|
755
|
+
|
|
756
|
+
Options:
|
|
757
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
758
|
+
--api-key <key> Shared API key (env: VELLUM_API_KEY)
|
|
759
|
+
--json Print the raw JSON response
|
|
760
|
+
-h, --help Show this help`;
|
|
761
|
+
async function resolveCommand(argv) {
|
|
762
|
+
const { values, positionals } = parseArgs9({
|
|
763
|
+
args: argv,
|
|
764
|
+
allowPositionals: true,
|
|
765
|
+
options: {
|
|
766
|
+
url: { type: "string" },
|
|
767
|
+
"api-key": { type: "string" },
|
|
768
|
+
json: { type: "boolean", default: false },
|
|
769
|
+
help: { type: "boolean", short: "h", default: false }
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
if (values.help) {
|
|
773
|
+
console.log(HELP9);
|
|
774
|
+
return 0;
|
|
775
|
+
}
|
|
776
|
+
const commentId = positionals[0];
|
|
777
|
+
if (!commentId) {
|
|
778
|
+
console.error(`Error: missing <comment-id>.
|
|
779
|
+
`);
|
|
780
|
+
console.error(HELP9);
|
|
781
|
+
return 1;
|
|
782
|
+
}
|
|
783
|
+
const flags = { url: values.url, apiKey: values["api-key"] };
|
|
784
|
+
const baseUrl = resolveBaseUrl(flags);
|
|
785
|
+
const credential = await resolveCredential(flags, baseUrl);
|
|
786
|
+
const client2 = new VellumClient({ baseUrl, ...credential });
|
|
787
|
+
const comment = await client2.resolveComment(commentId);
|
|
788
|
+
if (values.json) {
|
|
789
|
+
console.log(JSON.stringify(comment, null, 2));
|
|
790
|
+
} else {
|
|
791
|
+
console.log(`✓ resolved ${comment.id}`);
|
|
792
|
+
}
|
|
793
|
+
return 0;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/commands/whoami.ts
|
|
797
|
+
import { parseArgs as parseArgs10 } from "node:util";
|
|
798
|
+
var HELP10 = `vellum whoami — show the identity the CLI is authenticated as
|
|
799
|
+
|
|
800
|
+
Usage:
|
|
801
|
+
vellum whoami [options]
|
|
802
|
+
|
|
803
|
+
Options:
|
|
804
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
805
|
+
--api-key <key> Shared API key (env: VELLUM_API_KEY)
|
|
806
|
+
-h, --help Show this help`;
|
|
807
|
+
async function whoamiCommand(argv) {
|
|
808
|
+
const { values } = parseArgs10({
|
|
809
|
+
args: argv,
|
|
810
|
+
options: {
|
|
811
|
+
url: { type: "string" },
|
|
812
|
+
"api-key": { type: "string" },
|
|
813
|
+
help: { type: "boolean", short: "h", default: false }
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
if (values.help) {
|
|
817
|
+
console.log(HELP10);
|
|
818
|
+
return 0;
|
|
819
|
+
}
|
|
820
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
821
|
+
const credential = await resolveCredential({ url: values.url, apiKey: values["api-key"] }, baseUrl);
|
|
822
|
+
if ("apiKey" in credential) {
|
|
823
|
+
console.log(`Authenticated to ${baseUrl} with a shared API key (owner).`);
|
|
824
|
+
return 0;
|
|
825
|
+
}
|
|
826
|
+
const client2 = new VellumClient({ baseUrl, token: credential.token });
|
|
827
|
+
const { email } = await client2.whoami();
|
|
828
|
+
console.log(`${email ?? "(no email on record)"} — ${baseUrl}`);
|
|
829
|
+
return 0;
|
|
830
|
+
}
|
|
831
|
+
|
|
118
832
|
// src/index.ts
|
|
119
|
-
var
|
|
833
|
+
var HELP11 = `vellum — CLI for the Vellum artifact store
|
|
120
834
|
|
|
121
835
|
Usage:
|
|
122
836
|
vellum <command> [options]
|
|
123
837
|
|
|
124
838
|
Commands:
|
|
839
|
+
login Authenticate this machine (opens your browser)
|
|
840
|
+
logout Remove this machine's stored CLI token
|
|
841
|
+
whoami Show the identity the CLI is authenticated as
|
|
125
842
|
push Create an artifact (page) from HTML (file or stdin)
|
|
843
|
+
get Print a document's raw HTML
|
|
844
|
+
open Print (and open) a document's view URL
|
|
845
|
+
markup Pin a comment to a passage of a document
|
|
846
|
+
comments List the comments on a document
|
|
847
|
+
reply Reply to a comment thread
|
|
848
|
+
resolve Mark a comment as resolved
|
|
126
849
|
|
|
127
850
|
Run 'vellum <command> --help' for command-specific options.`;
|
|
128
851
|
var commands = {
|
|
129
|
-
|
|
852
|
+
login: loginCommand,
|
|
853
|
+
logout: logoutCommand,
|
|
854
|
+
whoami: whoamiCommand,
|
|
855
|
+
push: pushCommand,
|
|
856
|
+
get: getCommand,
|
|
857
|
+
open: openCommand,
|
|
858
|
+
markup: markupCommand,
|
|
859
|
+
comments: commentsCommand,
|
|
860
|
+
reply: replyCommand,
|
|
861
|
+
resolve: resolveCommand
|
|
130
862
|
};
|
|
131
863
|
async function main() {
|
|
132
864
|
const [, , cmd, ...rest] = process.argv;
|
|
133
865
|
if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
134
|
-
console.log(
|
|
866
|
+
console.log(HELP11);
|
|
135
867
|
return cmd ? 0 : 1;
|
|
136
868
|
}
|
|
137
869
|
const handler = commands[cmd];
|
|
138
870
|
if (!handler) {
|
|
139
871
|
console.error(`Unknown command: ${cmd}
|
|
140
872
|
`);
|
|
141
|
-
console.error(
|
|
873
|
+
console.error(HELP11);
|
|
142
874
|
return 1;
|
|
143
875
|
}
|
|
144
876
|
return handler(rest);
|