overleaf-review 0.1.2 β 0.2.1
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 +14 -2
- package/dist/cli.js +203 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,9 @@ workflow and carries the review layer Git can't represent.
|
|
|
36
36
|
(`.overleaf/reviews.md` + `.json`), so your tools have every co-author note in context.
|
|
37
37
|
- π€ **`push`** β turn local edits into **tracked-change suggestions**, mapping files to Overleaf
|
|
38
38
|
docs by path (one file, or every changed `.tex` at once). `--dry-run` previews the exact ops.
|
|
39
|
-
- π¬ **`comment` / `
|
|
39
|
+
- π¬ **`comment` / `reply` / `resolve` / `delete-comment` / `delete-message`** β full comment
|
|
40
|
+
control: start a thread, reply, resolve/reopen, delete a whole thread or a single message.
|
|
41
|
+
- β
**`accept` / `reject`** β act on your collaborators' tracked changes from the CLI.
|
|
40
42
|
- π **`login`** β validated auth stored outside your repo (chmod 600); `--browser` mode is
|
|
41
43
|
institutional-SSO friendly.
|
|
42
44
|
|
|
@@ -73,7 +75,14 @@ overleaf-review resolve --thread <id> # thread ids come from `pull`
|
|
|
73
75
|
| `pull [--out <dir>]` | Read comments + tracked changes into a sidecar |
|
|
74
76
|
| `push [--file <f>] [--doc <name>] [--dry-run]` | Send local edits as tracked-change suggestions (all changed `.tex` if no `--file`) |
|
|
75
77
|
| `comment --anchor <text> --message <text> [--doc <name>] [--nth <n>]` | Add a comment anchored on the given text |
|
|
78
|
+
| `reply --thread <id> --message <text>` | Reply to an existing comment thread |
|
|
76
79
|
| `resolve --thread <id> [--reopen]` | Resolve (or reopen) a comment thread |
|
|
80
|
+
| `delete-comment --thread <id>` | Delete a whole comment thread |
|
|
81
|
+
| `delete-message --message-id <id> [--thread <id>]` | Delete a single message within a thread |
|
|
82
|
+
| `accept --change <id> β¦` | Accept collaborators' tracked change(s) |
|
|
83
|
+
| `reject --change <id> β¦` | Reject collaborators' tracked change(s) |
|
|
84
|
+
|
|
85
|
+
Thread and change ids are listed by `pull` (in `.overleaf/reviews.md`).
|
|
77
86
|
|
|
78
87
|
## π§ How it works
|
|
79
88
|
|
|
@@ -96,10 +105,13 @@ own account and projects. Use at your own risk.
|
|
|
96
105
|
|
|
97
106
|
## πΊοΈ Roadmap
|
|
98
107
|
|
|
99
|
-
- Reply-to-thread and delete-comment
|
|
100
108
|
- A `pull` that also writes doc content (not just the review sidecar)
|
|
101
109
|
- Trusted-publishing CI
|
|
102
110
|
|
|
111
|
+
## π Changelog
|
|
112
|
+
|
|
113
|
+
Version history and notes are on the [Releases page](https://github.com/michu5696/overleaf-review/releases).
|
|
114
|
+
|
|
103
115
|
## π License
|
|
104
116
|
|
|
105
117
|
MIT Β© [Miguel Castellano](https://github.com/michu5696)
|
package/dist/cli.js
CHANGED
|
@@ -149,15 +149,15 @@ var OverleafSocket = class {
|
|
|
149
149
|
const wsUrl = `${this.baseUrl.replace(/^http/, "ws")}/socket.io/1/websocket/${sid}`;
|
|
150
150
|
if (this.debug) console.log(`[socket] transports=${transports}; ws=${wsUrl}`);
|
|
151
151
|
this.ws = new WebSocket(wsUrl, { headers: this.browserHeaders });
|
|
152
|
-
await new Promise((resolve2,
|
|
152
|
+
await new Promise((resolve2, reject2) => {
|
|
153
153
|
this.ws.once("open", () => resolve2());
|
|
154
|
-
this.ws.once("error",
|
|
154
|
+
this.ws.once("error", reject2);
|
|
155
155
|
this.ws.once("unexpected-response", (_req, response) => {
|
|
156
156
|
let errBody = "";
|
|
157
157
|
response.on("data", (chunk) => errBody += chunk.toString());
|
|
158
158
|
response.on(
|
|
159
159
|
"end",
|
|
160
|
-
() =>
|
|
160
|
+
() => reject2(
|
|
161
161
|
new Error(
|
|
162
162
|
`ws upgrade rejected: ${response.statusCode} ${response.statusMessage}
|
|
163
163
|
headers: ${JSON.stringify(response.headers)}
|
|
@@ -175,10 +175,10 @@ var OverleafSocket = class {
|
|
|
175
175
|
/** Emit an event and resolve with the server's ack payload (array of args). */
|
|
176
176
|
emit(name, args, timeoutMs = 15e3) {
|
|
177
177
|
const id = this.ackId++;
|
|
178
|
-
return new Promise((resolve2,
|
|
178
|
+
return new Promise((resolve2, reject2) => {
|
|
179
179
|
const timer = setTimeout(() => {
|
|
180
180
|
this.pendingAcks.delete(id);
|
|
181
|
-
|
|
181
|
+
reject2(new Error(`emit('${name}') timed out after ${timeoutMs}ms with no ack`));
|
|
182
182
|
}, timeoutMs);
|
|
183
183
|
this.pendingAcks.set(id, (payload) => {
|
|
184
184
|
clearTimeout(timer);
|
|
@@ -315,6 +315,34 @@ async function setThreadResolved(docId, threadId, reopen, csrf) {
|
|
|
315
315
|
}
|
|
316
316
|
return res.status;
|
|
317
317
|
}
|
|
318
|
+
async function acceptChanges(docId, changeIds, csrf) {
|
|
319
|
+
const res = await fetch(
|
|
320
|
+
`${config.baseUrl}/project/${config.projectId}/doc/${docId}/changes/accept`,
|
|
321
|
+
{
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: headers({ "X-CSRF-Token": csrf, "Content-Type": "application/json" }),
|
|
324
|
+
body: JSON.stringify({ change_ids: changeIds })
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
if (!res.ok) throw new Error(`acceptChanges ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
328
|
+
return res.status;
|
|
329
|
+
}
|
|
330
|
+
async function deleteMessage(threadId, messageId, csrf) {
|
|
331
|
+
const res = await fetch(
|
|
332
|
+
`${config.baseUrl}/project/${config.projectId}/thread/${threadId}/messages/${messageId}`,
|
|
333
|
+
{ method: "DELETE", headers: headers({ "X-CSRF-Token": csrf }) }
|
|
334
|
+
);
|
|
335
|
+
if (!res.ok) throw new Error(`deleteMessage ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
336
|
+
return res.status;
|
|
337
|
+
}
|
|
338
|
+
async function deleteThread(docId, threadId, csrf) {
|
|
339
|
+
const res = await fetch(
|
|
340
|
+
`${config.baseUrl}/project/${config.projectId}/doc/${docId}/thread/${threadId}`,
|
|
341
|
+
{ method: "DELETE", headers: headers({ "X-CSRF-Token": csrf }) }
|
|
342
|
+
);
|
|
343
|
+
if (!res.ok) throw new Error(`deleteThread ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
344
|
+
return res.status;
|
|
345
|
+
}
|
|
318
346
|
async function validateSession(baseUrl, session2) {
|
|
319
347
|
const res = await fetch(`${baseUrl}/project`, {
|
|
320
348
|
headers: { Cookie: `overleaf_session2=${session2}`, "User-Agent": UA },
|
|
@@ -379,6 +407,7 @@ async function pull(outDir = ".overleaf") {
|
|
|
379
407
|
context: lineContext(state.lines, line),
|
|
380
408
|
resolved: Boolean(thread.resolved),
|
|
381
409
|
messages: (thread.messages ?? []).map((m) => ({
|
|
410
|
+
id: m.id,
|
|
382
411
|
author: m.user?.first_name ?? members[m.user_id] ?? m.user_id,
|
|
383
412
|
email: m.user?.email,
|
|
384
413
|
content: m.content,
|
|
@@ -391,6 +420,7 @@ async function pull(outDir = ".overleaf") {
|
|
|
391
420
|
const line = offsetToLine(state.lines, ch.op.p);
|
|
392
421
|
changes.push({
|
|
393
422
|
doc: doc.path,
|
|
423
|
+
id: ch.id,
|
|
394
424
|
type: isInsert ? "insert" : "delete",
|
|
395
425
|
text: isInsert ? ch.op.i : ch.op.d,
|
|
396
426
|
line: line + 1,
|
|
@@ -418,6 +448,10 @@ function renderMarkdown(d) {
|
|
|
418
448
|
L.push(`# Overleaf review \u2014 ${d.project}`, "");
|
|
419
449
|
L.push(`_Pulled ${d.pulledAt} from project \`${d.projectId}\`._`, "");
|
|
420
450
|
L.push(`**${d.comments.length}** comment(s), **${d.changes.length}** tracked change(s).`, "");
|
|
451
|
+
L.push(
|
|
452
|
+
"_Act on these:_ `reply --thread <id> --message <text>` \xB7 `resolve --thread <id>` \xB7 `delete-comment --thread <id>` \xB7 `delete-message --message-id <id>` \xB7 `accept --change <id>` \xB7 `reject --change <id>`.",
|
|
453
|
+
""
|
|
454
|
+
);
|
|
421
455
|
const byDoc = (items) => {
|
|
422
456
|
const m = /* @__PURE__ */ new Map();
|
|
423
457
|
for (const it of items) (m.get(it.doc) ?? m.set(it.doc, []).get(it.doc)).push(it);
|
|
@@ -430,9 +464,10 @@ function renderMarkdown(d) {
|
|
|
430
464
|
for (const c of items) {
|
|
431
465
|
const flag = c.resolved ? " \u2014 \u2705 resolved" : "";
|
|
432
466
|
L.push(`#### \u{1F4AC} \u201C${c.anchor}\u201D \xB7 line ${c.line}${flag}`);
|
|
467
|
+
L.push(`\`thread ${c.threadId}\``);
|
|
433
468
|
L.push("```", c.context, "```");
|
|
434
469
|
for (const m of c.messages) {
|
|
435
|
-
L.push(`- **${m.author}**: ${m.content}
|
|
470
|
+
L.push(`- **${m.author}**: ${m.content} \`msg ${m.id}\``);
|
|
436
471
|
}
|
|
437
472
|
if (!c.messages.length) L.push("- _(no message text)_");
|
|
438
473
|
L.push("");
|
|
@@ -445,7 +480,9 @@ function renderMarkdown(d) {
|
|
|
445
480
|
L.push(`### ${doc}`, "");
|
|
446
481
|
for (const ch of items) {
|
|
447
482
|
const verb = ch.type === "insert" ? "inserted" : "deleted";
|
|
448
|
-
L.push(
|
|
483
|
+
L.push(
|
|
484
|
+
`- **${verb}** at line ${ch.line} by ${ch.author} \xB7 \`change ${ch.id}\`: \`${ch.text.replace(/\n/g, "\u23CE")}\``
|
|
485
|
+
);
|
|
449
486
|
L.push(" ```", ch.context.replace(/^/gm, " "), " ```");
|
|
450
487
|
}
|
|
451
488
|
L.push("");
|
|
@@ -603,6 +640,13 @@ async function comment(opts) {
|
|
|
603
640
|
console.log(`\u2705 Commented on "${opts.anchor}" in ${doc.name} (thread ${threadId})`);
|
|
604
641
|
}
|
|
605
642
|
|
|
643
|
+
// src/commands/reply.ts
|
|
644
|
+
async function reply(threadId, message) {
|
|
645
|
+
const csrf = await getCsrfToken();
|
|
646
|
+
await postThreadMessage(threadId, message, csrf);
|
|
647
|
+
console.log(`\u2705 Replied to thread ${threadId}`);
|
|
648
|
+
}
|
|
649
|
+
|
|
606
650
|
// src/commands/resolve.ts
|
|
607
651
|
async function resolve(threadId, reopen = false) {
|
|
608
652
|
const { socket, docs } = await openProject();
|
|
@@ -625,6 +669,95 @@ async function resolve(threadId, reopen = false) {
|
|
|
625
669
|
console.log(`\u2705 Thread ${threadId} ${reopen ? "reopened" : "resolved"}`);
|
|
626
670
|
}
|
|
627
671
|
|
|
672
|
+
// src/commands/delete-comment.ts
|
|
673
|
+
async function deleteComment(threadId) {
|
|
674
|
+
const { socket, docs } = await openProject();
|
|
675
|
+
let docId;
|
|
676
|
+
for (const doc of docs) {
|
|
677
|
+
const state = await joinDoc(socket, doc._id);
|
|
678
|
+
if ((state.ranges.comments ?? []).some((c) => c.op?.t === threadId)) {
|
|
679
|
+
docId = doc._id;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
socket.close();
|
|
684
|
+
if (!docId) throw new Error(`thread ${threadId} not found in any doc's comments`);
|
|
685
|
+
const csrf = await getCsrfToken();
|
|
686
|
+
await deleteThread(docId, threadId, csrf);
|
|
687
|
+
console.log(`\u2705 Deleted comment thread ${threadId}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/commands/delete-message.ts
|
|
691
|
+
async function deleteThreadMessage(messageId, threadId) {
|
|
692
|
+
const csrf = await getCsrfToken();
|
|
693
|
+
let tid = threadId;
|
|
694
|
+
if (!tid) {
|
|
695
|
+
const threads = await getThreads();
|
|
696
|
+
for (const [t, v] of Object.entries(threads)) {
|
|
697
|
+
if (v.messages?.some((m) => m.id === messageId)) {
|
|
698
|
+
tid = t;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (!tid) throw new Error(`message ${messageId} not found in any thread`);
|
|
704
|
+
await deleteMessage(tid, messageId, csrf);
|
|
705
|
+
console.log(`\u2705 Deleted message ${messageId} from thread ${tid}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/commands/accept.ts
|
|
709
|
+
async function accept(changeIds) {
|
|
710
|
+
const { socket, docs } = await openProject();
|
|
711
|
+
const byDoc = /* @__PURE__ */ new Map();
|
|
712
|
+
for (const doc of docs) {
|
|
713
|
+
const state = await joinDoc(socket, doc._id);
|
|
714
|
+
const here = (state.ranges.changes ?? []).filter((c) => changeIds.includes(c.id)).map((c) => c.id);
|
|
715
|
+
if (here.length) byDoc.set(doc._id, here);
|
|
716
|
+
}
|
|
717
|
+
socket.close();
|
|
718
|
+
const found = [...byDoc.values()].flat();
|
|
719
|
+
const missing = changeIds.filter((id) => !found.includes(id));
|
|
720
|
+
if (missing.length) console.log(`\u26A0\uFE0F not found (already accepted/rejected?): ${missing.join(", ")}`);
|
|
721
|
+
if (!found.length) throw new Error("no matching tracked changes found");
|
|
722
|
+
const csrf = await getCsrfToken();
|
|
723
|
+
for (const [docId, ids] of byDoc) await acceptChanges(docId, ids, csrf);
|
|
724
|
+
console.log(`\u2705 Accepted ${found.length} tracked change(s)`);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/commands/reject.ts
|
|
728
|
+
async function reject(changeIds) {
|
|
729
|
+
const { socket, docs } = await openProject();
|
|
730
|
+
const docOf = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const doc of docs) {
|
|
732
|
+
const state = await joinDoc(socket, doc._id);
|
|
733
|
+
for (const c of state.ranges.changes ?? []) {
|
|
734
|
+
if (changeIds.includes(c.id)) docOf.set(c.id, doc._id);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
let rejected = 0;
|
|
738
|
+
for (const id of changeIds) {
|
|
739
|
+
const docId = docOf.get(id);
|
|
740
|
+
if (!docId) {
|
|
741
|
+
console.log(`\u26A0\uFE0F not found (already accepted/rejected?): ${id}`);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const state = await joinDoc(socket, docId);
|
|
745
|
+
const c = (state.ranges.changes ?? []).find((x) => x.id === id);
|
|
746
|
+
if (!c) continue;
|
|
747
|
+
const op = typeof c.op?.i === "string" ? { p: c.op.p, d: c.op.i } : { p: c.op.p, i: c.op.d };
|
|
748
|
+
const update = { doc: docId, op: [op], v: state.version, meta: {} };
|
|
749
|
+
const ack = await socket.emit("applyOtUpdate", [docId, update], 15e3);
|
|
750
|
+
if (ack?.[0]) {
|
|
751
|
+
socket.close();
|
|
752
|
+
throw new Error(`Overleaf rejected the inverse op for ${id}: ${JSON.stringify(ack[0])}`);
|
|
753
|
+
}
|
|
754
|
+
rejected++;
|
|
755
|
+
}
|
|
756
|
+
socket.close();
|
|
757
|
+
if (!rejected) throw new Error("no matching tracked changes found");
|
|
758
|
+
console.log(`\u2705 Rejected ${rejected} tracked change(s)`);
|
|
759
|
+
}
|
|
760
|
+
|
|
628
761
|
// src/commands/login.ts
|
|
629
762
|
import { createInterface } from "readline/promises";
|
|
630
763
|
async function login(opts) {
|
|
@@ -684,40 +817,45 @@ function getFlag(name) {
|
|
|
684
817
|
const i = process.argv.indexOf(`--${name}`);
|
|
685
818
|
return i >= 0 ? process.argv[i + 1] : void 0;
|
|
686
819
|
}
|
|
820
|
+
function getAll(name) {
|
|
821
|
+
const out = [];
|
|
822
|
+
process.argv.forEach((a, i) => {
|
|
823
|
+
if (a === `--${name}` && process.argv[i + 1]) out.push(process.argv[i + 1]);
|
|
824
|
+
});
|
|
825
|
+
return out.flatMap((v) => v.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
826
|
+
}
|
|
687
827
|
function usage() {
|
|
688
828
|
console.log("overleaf-review \u2014 sync Overleaf review data with git\n");
|
|
689
829
|
console.log("Setup:");
|
|
690
|
-
console.log("
|
|
691
|
-
console.log("
|
|
692
|
-
console.log("
|
|
693
|
-
console.log("
|
|
694
|
-
console.log("
|
|
695
|
-
console.log("
|
|
696
|
-
console.log("
|
|
697
|
-
console.log("
|
|
698
|
-
console.log("
|
|
699
|
-
console.log("
|
|
700
|
-
console.log("
|
|
701
|
-
console.log("
|
|
702
|
-
console.log("
|
|
830
|
+
console.log(" login [--cookie <val>] [--browser] Authenticate (SSO-friendly --browser)");
|
|
831
|
+
console.log(" link --project <id> Link this repo to an Overleaf project\n");
|
|
832
|
+
console.log("Read:");
|
|
833
|
+
console.log(" pull [--out <dir>] Comments + tracked changes \u2192 sidecar\n");
|
|
834
|
+
console.log("Comments:");
|
|
835
|
+
console.log(" comment --anchor <text> --message <text> [--doc <name>] [--nth <n>]");
|
|
836
|
+
console.log(" reply --thread <id> --message <text> Reply to an existing thread");
|
|
837
|
+
console.log(" resolve --thread <id> [--reopen] Resolve/reopen a thread");
|
|
838
|
+
console.log(" delete-comment --thread <id> Delete a whole thread");
|
|
839
|
+
console.log(" delete-message --message-id <id> Delete a single message\n");
|
|
840
|
+
console.log("Tracked changes:");
|
|
841
|
+
console.log(" push [--file <f>] [--doc <name>] [--dry-run] Send local edits as suggestions");
|
|
842
|
+
console.log(" accept --change <id> [--change <id> \u2026] Accept collaborators\u2019 changes");
|
|
843
|
+
console.log(" reject --change <id> [--change <id> \u2026] Reject collaborators\u2019 changes");
|
|
844
|
+
console.log("\n(thread/change ids come from `pull`; --change accepts comma-separated lists too)");
|
|
703
845
|
}
|
|
704
846
|
async function main() {
|
|
705
847
|
const cmd = process.argv[2];
|
|
706
848
|
switch (cmd) {
|
|
707
|
-
case "login":
|
|
849
|
+
case "login":
|
|
708
850
|
await login({
|
|
709
851
|
cookie: getFlag("cookie"),
|
|
710
852
|
baseUrl: getFlag("base-url"),
|
|
711
853
|
browser: process.argv.includes("--browser")
|
|
712
854
|
});
|
|
713
855
|
break;
|
|
714
|
-
}
|
|
715
856
|
case "link": {
|
|
716
857
|
const project = getFlag("project");
|
|
717
|
-
if (!project)
|
|
718
|
-
console.error("link requires --project <id>");
|
|
719
|
-
process.exit(1);
|
|
720
|
-
}
|
|
858
|
+
if (!project) fail("link requires --project <id>");
|
|
721
859
|
link(project, getFlag("base-url"));
|
|
722
860
|
break;
|
|
723
861
|
}
|
|
@@ -729,40 +867,63 @@ async function main() {
|
|
|
729
867
|
);
|
|
730
868
|
break;
|
|
731
869
|
}
|
|
732
|
-
case "push":
|
|
870
|
+
case "push":
|
|
733
871
|
await push({ file: getFlag("file"), docName: getFlag("doc"), dryRun: process.argv.includes("--dry-run") });
|
|
734
872
|
break;
|
|
735
|
-
}
|
|
736
873
|
case "comment": {
|
|
737
874
|
const anchor = getFlag("anchor");
|
|
738
875
|
const message = getFlag("message");
|
|
739
|
-
if (!anchor || !message)
|
|
740
|
-
console.error("comment requires --anchor <text> and --message <text>");
|
|
741
|
-
process.exit(1);
|
|
742
|
-
}
|
|
876
|
+
if (!anchor || !message) fail("comment requires --anchor <text> and --message <text>");
|
|
743
877
|
const nthRaw = getFlag("nth");
|
|
744
|
-
await comment({
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
878
|
+
await comment({ docName: getFlag("doc"), anchor, message, occurrence: nthRaw ? Number(nthRaw) : void 0 });
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case "reply": {
|
|
882
|
+
const thread = getFlag("thread");
|
|
883
|
+
const message = getFlag("message");
|
|
884
|
+
if (!thread || !message) fail("reply requires --thread <id> and --message <text>");
|
|
885
|
+
await reply(thread, message);
|
|
750
886
|
break;
|
|
751
887
|
}
|
|
752
888
|
case "resolve": {
|
|
753
889
|
const thread = getFlag("thread");
|
|
754
|
-
if (!thread)
|
|
755
|
-
console.error("resolve requires --thread <id>");
|
|
756
|
-
process.exit(1);
|
|
757
|
-
}
|
|
890
|
+
if (!thread) fail("resolve requires --thread <id>");
|
|
758
891
|
await resolve(thread, process.argv.includes("--reopen"));
|
|
759
892
|
break;
|
|
760
893
|
}
|
|
894
|
+
case "delete-comment": {
|
|
895
|
+
const thread = getFlag("thread");
|
|
896
|
+
if (!thread) fail("delete-comment requires --thread <id>");
|
|
897
|
+
await deleteComment(thread);
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
case "delete-message": {
|
|
901
|
+
const messageId = getFlag("message-id");
|
|
902
|
+
if (!messageId) fail("delete-message requires --message-id <id>");
|
|
903
|
+
await deleteThreadMessage(messageId, getFlag("thread"));
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
case "accept": {
|
|
907
|
+
const ids = getAll("change");
|
|
908
|
+
if (!ids.length) fail("accept requires --change <id>");
|
|
909
|
+
await accept(ids);
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
case "reject": {
|
|
913
|
+
const ids = getAll("change");
|
|
914
|
+
if (!ids.length) fail("reject requires --change <id>");
|
|
915
|
+
await reject(ids);
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
761
918
|
default:
|
|
762
919
|
usage();
|
|
763
920
|
process.exit(cmd ? 1 : 0);
|
|
764
921
|
}
|
|
765
922
|
}
|
|
923
|
+
function fail(msg) {
|
|
924
|
+
console.error(msg);
|
|
925
|
+
process.exit(1);
|
|
926
|
+
}
|
|
766
927
|
main().then(() => process.exit(0)).catch((e) => {
|
|
767
928
|
console.error(e instanceof Error ? e.message : e);
|
|
768
929
|
process.exit(1);
|
package/package.json
CHANGED