@jphutchins/code-review 0.1.0-alpha.22 → 0.1.0-alpha.24
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 +2 -0
- package/dist/index.js +312 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ npx @jphutchins/code-review <subcommand>
|
|
|
39
39
|
| --- | --- |
|
|
40
40
|
| `post` | Post a complete review (inline comments + sticky summary) from findings + envelope + diff — the one-call path |
|
|
41
41
|
| `gather` | Resolve the PR from the CI head SHA and gather the review inputs (diff with git-diff fallback, PR context, prior bot review, failing-job logs) into the workspace for the agent |
|
|
42
|
+
| `parse-command` | Resolve a PR's head from its number and parse a ChatOps trigger comment (`/code-review [24m] [$1.00] <instructions>`) into review overrides — the on-demand comment trigger |
|
|
43
|
+
| `react` | Add/remove a GitHub comment reaction — the ChatOps acknowledgement (👀 on receipt, 🚀 on completion) |
|
|
42
44
|
| `render` | Render the sticky-comment markdown from findings + usage + prices |
|
|
43
45
|
| `inline` | Build the GitHub reviews `comments[]` payload from findings + diff (in-diff validation; strays demote to the summary) |
|
|
44
46
|
| `adapt` | Map a native agent-CLI result envelope onto the abstract result envelope (`src/schema.ts`) |
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
3
|
import { readFileSync, writeFileSync, statSync, copyFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
4
5
|
import { resolve as resolve$1, join, dirname, basename, extname } from 'path';
|
|
5
6
|
import { Eta } from 'eta';
|
|
6
7
|
import parseDiff from 'parse-diff';
|
|
@@ -459,12 +460,12 @@ var sumTranscriptUsage = (entries) => {
|
|
|
459
460
|
},
|
|
460
461
|
{ totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
|
|
461
462
|
);
|
|
462
|
-
const models = [...summed.totals].map(([model,
|
|
463
|
+
const models = [...summed.totals].map(([model, t6]) => ({
|
|
463
464
|
model,
|
|
464
|
-
input_tokens:
|
|
465
|
-
output_tokens:
|
|
466
|
-
cache_read_tokens:
|
|
467
|
-
cache_write_tokens:
|
|
465
|
+
input_tokens: t6.input,
|
|
466
|
+
output_tokens: t6.output,
|
|
467
|
+
cache_read_tokens: t6.cacheRead,
|
|
468
|
+
cache_write_tokens: t6.cacheWrite
|
|
468
469
|
}));
|
|
469
470
|
const bounds = entries.reduce(
|
|
470
471
|
(acc, entry) => {
|
|
@@ -801,7 +802,7 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
|
801
802
|
const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
|
|
802
803
|
if (WRITE_TOOLS.has(toolName)) {
|
|
803
804
|
const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
|
|
804
|
-
if (typeof fp === "string" && targets.some((
|
|
805
|
+
if (typeof fp === "string" && targets.some((t6) => fp === t6 || basename(fp) === basename(t6)))
|
|
805
806
|
return true;
|
|
806
807
|
}
|
|
807
808
|
if (toolName === "Bash") {
|
|
@@ -897,9 +898,9 @@ var parseWallMs = (raw) => {
|
|
|
897
898
|
};
|
|
898
899
|
var parseEpochSecMs = (raw) => {
|
|
899
900
|
if (raw === void 0) return null;
|
|
900
|
-
const
|
|
901
|
-
if (!/^\d+$/.test(
|
|
902
|
-
const n = Number.parseInt(
|
|
901
|
+
const t6 = raw.trim();
|
|
902
|
+
if (!/^\d+$/.test(t6)) return null;
|
|
903
|
+
const n = Number.parseInt(t6, 10);
|
|
903
904
|
return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
|
|
904
905
|
};
|
|
905
906
|
var anchoredElapsedMs = (src) => {
|
|
@@ -1314,8 +1315,8 @@ var priorBotCommentIds = (raw, botLogin) => {
|
|
|
1314
1315
|
const nodes = conn?.nodes;
|
|
1315
1316
|
if (!Array.isArray(nodes)) return { ids: [], truncated };
|
|
1316
1317
|
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
1317
|
-
const ids = nodes.flatMap((
|
|
1318
|
-
const cnodes =
|
|
1318
|
+
const ids = nodes.flatMap((t6) => {
|
|
1319
|
+
const cnodes = t6.comments?.nodes;
|
|
1319
1320
|
return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
|
|
1320
1321
|
});
|
|
1321
1322
|
return { ids, truncated };
|
|
@@ -1579,6 +1580,190 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1579
1580
|
}
|
|
1580
1581
|
}
|
|
1581
1582
|
};
|
|
1583
|
+
var DURATION_RE = /^(\d+)(h|m|s)$/;
|
|
1584
|
+
var USD_RE = /^\$(\d+(?:\.\d+)?)$/;
|
|
1585
|
+
var toSeconds = (n, unit) => unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
|
|
1586
|
+
var stripTrigger = (body, trigger) => {
|
|
1587
|
+
const trimmed = body.replace(/^\s+/, "");
|
|
1588
|
+
if (!trimmed.startsWith(trigger)) return null;
|
|
1589
|
+
const after = trimmed.slice(trigger.length);
|
|
1590
|
+
return after === "" || /^\s/.test(after) ? after : null;
|
|
1591
|
+
};
|
|
1592
|
+
var scanLeading = (s, acc) => {
|
|
1593
|
+
const m = /^(\s*)(\S+)([\s\S]*)$/.exec(s);
|
|
1594
|
+
if (m === null) return { ...acc, rest: "" };
|
|
1595
|
+
const [, , token = "", tail = ""] = m;
|
|
1596
|
+
const dm = DURATION_RE.exec(token);
|
|
1597
|
+
if (dm && acc.durationSec === null)
|
|
1598
|
+
return scanLeading(tail, {
|
|
1599
|
+
...acc,
|
|
1600
|
+
durationSec: toSeconds(Number.parseInt(dm[1] ?? "", 10), dm[2] ?? "s")
|
|
1601
|
+
});
|
|
1602
|
+
const um = USD_RE.exec(token);
|
|
1603
|
+
if (um && acc.usd === null)
|
|
1604
|
+
return scanLeading(tail, { ...acc, usd: Number.parseFloat(um[1] ?? "") });
|
|
1605
|
+
return { ...acc, rest: s };
|
|
1606
|
+
};
|
|
1607
|
+
var clampDuration = (requested, ceiling) => requested !== null && ceiling !== null && requested > ceiling ? {
|
|
1608
|
+
value: ceiling,
|
|
1609
|
+
notes: [
|
|
1610
|
+
`requested duration ${String(requested)}s exceeds the ${String(ceiling)}s ceiling \u2014 clamped to ${String(ceiling)}s`
|
|
1611
|
+
]
|
|
1612
|
+
} : { value: requested, notes: [] };
|
|
1613
|
+
var clampUsd = (requested, ceiling) => requested !== null && ceiling !== null && requested > ceiling ? {
|
|
1614
|
+
value: ceiling,
|
|
1615
|
+
notes: [
|
|
1616
|
+
`requested $${requested.toFixed(2)} exceeds the $${ceiling.toFixed(2)} ceiling \u2014 clamped to $${ceiling.toFixed(2)}`
|
|
1617
|
+
]
|
|
1618
|
+
} : { value: requested, notes: [] };
|
|
1619
|
+
var capInstructions = (text, maxLen) => text.length > maxLen ? {
|
|
1620
|
+
value: text.slice(0, maxLen),
|
|
1621
|
+
notes: [
|
|
1622
|
+
`instructions truncated from ${String(text.length)} to ${String(maxLen)} characters`
|
|
1623
|
+
]
|
|
1624
|
+
} : { value: text, notes: [] };
|
|
1625
|
+
var parseCommandArgs = (body, options) => {
|
|
1626
|
+
const afterTrigger = stripTrigger(body, options.trigger);
|
|
1627
|
+
if (afterTrigger === null) return { kind: "not-a-command" };
|
|
1628
|
+
const scan = scanLeading(afterTrigger, { durationSec: null, usd: null });
|
|
1629
|
+
const duration = clampDuration(scan.durationSec, options.maxDurationSec);
|
|
1630
|
+
const usd = clampUsd(scan.usd, options.maxUsd);
|
|
1631
|
+
const instructions = capInstructions(scan.rest.trim(), options.maxInstructionsLen);
|
|
1632
|
+
return {
|
|
1633
|
+
kind: "command",
|
|
1634
|
+
args: {
|
|
1635
|
+
durationSec: duration.value,
|
|
1636
|
+
usd: usd.value,
|
|
1637
|
+
instructions: instructions.value,
|
|
1638
|
+
notes: [...duration.notes, ...usd.notes, ...instructions.notes]
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
};
|
|
1642
|
+
var PrHeadCodec = t.type({
|
|
1643
|
+
head_sha: t.string,
|
|
1644
|
+
head_ref: t.string,
|
|
1645
|
+
head_repo: t.union([t.string, t.null]),
|
|
1646
|
+
state: t.string
|
|
1647
|
+
});
|
|
1648
|
+
var resolvePrHead = async (repo, prNumber, ghApi) => {
|
|
1649
|
+
const stdout = await ghApi([
|
|
1650
|
+
`repos/${repo}/pulls/${String(prNumber)}`,
|
|
1651
|
+
"--jq",
|
|
1652
|
+
"{head_sha: .head.sha, head_ref: .head.ref, head_repo: .head.repo.full_name, state: .state}"
|
|
1653
|
+
]);
|
|
1654
|
+
const decoded = PrHeadCodec.decode(JSON.parse(stdout));
|
|
1655
|
+
if (decoded._tag === "Left") {
|
|
1656
|
+
throw new Error(`PR head for #${String(prNumber)} did not match the expected shape`);
|
|
1657
|
+
}
|
|
1658
|
+
return decoded.right;
|
|
1659
|
+
};
|
|
1660
|
+
var parseCommand = async (input, ghApi = runGhApi) => {
|
|
1661
|
+
const parse = parseCommandArgs(input.body, input.options);
|
|
1662
|
+
if (parse.kind === "not-a-command") {
|
|
1663
|
+
return {
|
|
1664
|
+
kind: "skip",
|
|
1665
|
+
reason: `comment does not begin with the trigger "${input.options.trigger}"`
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
const head = await resolvePrHead(input.repo, input.prNumber, ghApi).catch(
|
|
1669
|
+
(err) => err instanceof Error ? err : new Error(String(err))
|
|
1670
|
+
);
|
|
1671
|
+
if (head instanceof Error) {
|
|
1672
|
+
return {
|
|
1673
|
+
kind: "skip",
|
|
1674
|
+
reason: `could not resolve PR #${String(input.prNumber)}: ${head.message}`
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
if (head.state !== "open") {
|
|
1678
|
+
return {
|
|
1679
|
+
kind: "skip",
|
|
1680
|
+
reason: `PR #${String(input.prNumber)} is not open (state: ${head.state})`
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
kind: "run",
|
|
1685
|
+
headSha: head.head_sha,
|
|
1686
|
+
headBranch: head.head_ref,
|
|
1687
|
+
headRepo: head.head_repo ?? input.repo,
|
|
1688
|
+
args: parse.args
|
|
1689
|
+
};
|
|
1690
|
+
};
|
|
1691
|
+
var safeHeredocDelim = (instructions, randomHex, attemptsLeft = 8) => {
|
|
1692
|
+
const candidate = `GHOUT_${randomHex()}`;
|
|
1693
|
+
if (!instructions.split("\n").includes(candidate)) return candidate;
|
|
1694
|
+
if (attemptsLeft <= 0) throw new Error("could not derive a collision-free heredoc delimiter");
|
|
1695
|
+
return safeHeredocDelim(instructions, randomHex, attemptsLeft - 1);
|
|
1696
|
+
};
|
|
1697
|
+
var renderCommandOutputs = (result, delim) => {
|
|
1698
|
+
if (result.kind === "skip") return "should_run=false\n";
|
|
1699
|
+
const { headSha, headBranch, headRepo, args } = result;
|
|
1700
|
+
return `${[
|
|
1701
|
+
"should_run=true",
|
|
1702
|
+
`head_sha=${headSha}`,
|
|
1703
|
+
`head_branch=${headBranch}`,
|
|
1704
|
+
`head_repo=${headRepo}`,
|
|
1705
|
+
`duration=${args.durationSec === null ? "" : `${String(args.durationSec)}s`}`,
|
|
1706
|
+
`usd=${args.usd === null ? "" : args.usd.toFixed(2)}`,
|
|
1707
|
+
`instructions<<${delim}`,
|
|
1708
|
+
args.instructions,
|
|
1709
|
+
delim
|
|
1710
|
+
].join("\n")}
|
|
1711
|
+
`;
|
|
1712
|
+
};
|
|
1713
|
+
var REACTIONS = [
|
|
1714
|
+
"+1",
|
|
1715
|
+
"-1",
|
|
1716
|
+
"laugh",
|
|
1717
|
+
"confused",
|
|
1718
|
+
"heart",
|
|
1719
|
+
"hooray",
|
|
1720
|
+
"rocket",
|
|
1721
|
+
"eyes"
|
|
1722
|
+
];
|
|
1723
|
+
var isReaction = (s) => REACTIONS.includes(s);
|
|
1724
|
+
var ReactionCodec = t.type({ id: t.number, content: t.string });
|
|
1725
|
+
var reactionsPath = (repo, commentId) => `repos/${repo}/issues/comments/${String(commentId)}/reactions`;
|
|
1726
|
+
var removeReactions = async (repo, commentId, content, ghApi) => {
|
|
1727
|
+
const stdout = await ghApi([
|
|
1728
|
+
reactionsPath(repo, commentId),
|
|
1729
|
+
"--paginate",
|
|
1730
|
+
"--jq",
|
|
1731
|
+
".[] | {id, content}"
|
|
1732
|
+
]);
|
|
1733
|
+
for (const line of stdout.split("\n").filter((l) => l.trim() !== "")) {
|
|
1734
|
+
const parsed = tryParseJson(line);
|
|
1735
|
+
const decoded = parsed.ok ? ReactionCodec.decode(parsed.value) : void 0;
|
|
1736
|
+
if (decoded === void 0 || decoded._tag === "Left") {
|
|
1737
|
+
process.stderr.write("code-review react: could not decode a reaction entry \u2014 skipping\n");
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
if (decoded.right.content !== content) continue;
|
|
1741
|
+
await ghApi([
|
|
1742
|
+
"--method",
|
|
1743
|
+
"DELETE",
|
|
1744
|
+
`${reactionsPath(repo, commentId)}/${String(decoded.right.id)}`
|
|
1745
|
+
]).catch(
|
|
1746
|
+
(err) => process.stderr.write(
|
|
1747
|
+
`code-review react: could not remove reaction ${String(decoded.right.id)} (${errMsg(err)}) \u2014 skipping
|
|
1748
|
+
`
|
|
1749
|
+
)
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
var react = async (input, ghApi = runGhApi) => {
|
|
1754
|
+
if (input.add !== void 0) {
|
|
1755
|
+
await ghApi([
|
|
1756
|
+
"--method",
|
|
1757
|
+
"POST",
|
|
1758
|
+
reactionsPath(input.repo, input.commentId),
|
|
1759
|
+
"-f",
|
|
1760
|
+
`content=${input.add}`
|
|
1761
|
+
]);
|
|
1762
|
+
}
|
|
1763
|
+
if (input.remove !== void 0) {
|
|
1764
|
+
await removeReactions(input.repo, input.commentId, input.remove, ghApi);
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1582
1767
|
var renderOutputs = (result) => {
|
|
1583
1768
|
switch (result.kind) {
|
|
1584
1769
|
case "skip":
|
|
@@ -3095,6 +3280,120 @@ var postCmd = defineCommand({
|
|
|
3095
3280
|
});
|
|
3096
3281
|
}
|
|
3097
3282
|
});
|
|
3283
|
+
var requireCeilingSec = (raw) => {
|
|
3284
|
+
if (raw === void 0) return null;
|
|
3285
|
+
const ms = parseWallMs(raw);
|
|
3286
|
+
if (ms === null)
|
|
3287
|
+
return fail(`--max-duration must be a duration like 60m, 3600s, or 1h (got "${raw}")`);
|
|
3288
|
+
return Math.floor(ms / 1e3);
|
|
3289
|
+
};
|
|
3290
|
+
var requireCeilingUsd = (raw) => {
|
|
3291
|
+
if (raw === void 0) return null;
|
|
3292
|
+
const n = Number.parseFloat(raw.replace(/^\$/, ""));
|
|
3293
|
+
if (!Number.isFinite(n) || n < 0) fail(`--max-usd must be a non-negative number (got "${raw}")`);
|
|
3294
|
+
return n;
|
|
3295
|
+
};
|
|
3296
|
+
var requireMaxInstructions = (raw) => {
|
|
3297
|
+
if (raw === void 0) return 4e3;
|
|
3298
|
+
if (!/^\d+$/.test(raw)) fail(`--max-instructions must be a non-negative integer (got "${raw}")`);
|
|
3299
|
+
return Number.parseInt(raw, 10);
|
|
3300
|
+
};
|
|
3301
|
+
var requirePositiveInt = (raw, flag) => {
|
|
3302
|
+
const n = Number.parseInt(raw, 10);
|
|
3303
|
+
return Number.isInteger(n) && n > 0 && /^\d+$/.test(raw) ? n : fail(`${flag} must be a positive integer; got "${raw}"`);
|
|
3304
|
+
};
|
|
3305
|
+
var parseCommandCmd = defineCommand({
|
|
3306
|
+
meta: {
|
|
3307
|
+
name: "parse-command",
|
|
3308
|
+
description: `Resolve a PR's head (SHA/branch/repo) from its NUMBER via the API and parse a ChatOps trigger comment ("/code-review [24m] [$1.00] <instructions>") into $GITHUB_OUTPUT lines. The untrusted comment is parsed here in type-safe code, never in workflow bash, and the head is resolved from the trusted PR number, never from the comment text. Emits should_run=false (and nothing else) when the comment is not the trigger, the PR is closed, or resolution fails.`
|
|
3309
|
+
},
|
|
3310
|
+
args: {
|
|
3311
|
+
repo: { type: "string", description: "Repository (owner/name)", required: true },
|
|
3312
|
+
pr: {
|
|
3313
|
+
type: "string",
|
|
3314
|
+
description: "PR number (from github.event.issue.number \u2014 trusted event data)",
|
|
3315
|
+
required: true
|
|
3316
|
+
},
|
|
3317
|
+
"comment-body": {
|
|
3318
|
+
type: "string",
|
|
3319
|
+
description: "The comment body to parse (default: the CODE_REVIEW_COMMENT_BODY env var \u2014 the safe way to pass untrusted text without shell interpolation)"
|
|
3320
|
+
},
|
|
3321
|
+
trigger: {
|
|
3322
|
+
type: "string",
|
|
3323
|
+
description: 'Trigger token the comment must begin with (default: "/code-review")'
|
|
3324
|
+
},
|
|
3325
|
+
"max-duration": {
|
|
3326
|
+
type: "string",
|
|
3327
|
+
description: "Ceiling the requested duration is clamped to (e.g. 60m); omit for no clamp \u2014 a comment could then request an unbounded wall, so set this"
|
|
3328
|
+
},
|
|
3329
|
+
"max-usd": {
|
|
3330
|
+
type: "string",
|
|
3331
|
+
description: "Ceiling the requested USD budget is clamped to (e.g. 5); omit for no clamp"
|
|
3332
|
+
},
|
|
3333
|
+
"max-instructions": {
|
|
3334
|
+
type: "string",
|
|
3335
|
+
description: "Max characters of free-form instructions kept (default: 4000)"
|
|
3336
|
+
}
|
|
3337
|
+
},
|
|
3338
|
+
run: async ({ args }) => {
|
|
3339
|
+
const body = args["comment-body"] || process.env["CODE_REVIEW_COMMENT_BODY"] || "";
|
|
3340
|
+
const result = await parseCommand({
|
|
3341
|
+
repo: args.repo,
|
|
3342
|
+
prNumber: requirePositiveInt(args.pr, "--pr"),
|
|
3343
|
+
body,
|
|
3344
|
+
options: {
|
|
3345
|
+
trigger: args.trigger || "/code-review",
|
|
3346
|
+
maxDurationSec: requireCeilingSec(args["max-duration"]),
|
|
3347
|
+
maxUsd: requireCeilingUsd(args["max-usd"]),
|
|
3348
|
+
maxInstructionsLen: requireMaxInstructions(args["max-instructions"])
|
|
3349
|
+
}
|
|
3350
|
+
});
|
|
3351
|
+
if (result.kind === "skip") {
|
|
3352
|
+
process.stderr.write(`code-review parse-command: not running \u2014 ${result.reason}
|
|
3353
|
+
`);
|
|
3354
|
+
process.stdout.write(renderCommandOutputs(result, "UNUSED"));
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
for (const note of result.args.notes)
|
|
3358
|
+
process.stderr.write(`code-review parse-command: ${note}
|
|
3359
|
+
`);
|
|
3360
|
+
const delim = safeHeredocDelim(result.args.instructions, () => randomBytes(16).toString("hex"));
|
|
3361
|
+
process.stdout.write(renderCommandOutputs(result, delim));
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3364
|
+
var requireReaction = (name) => isReaction(name) ? name : fail(`Unknown reaction "${name}" \u2014 one of: ${REACTIONS.join(", ")}`);
|
|
3365
|
+
var reactCmd = defineCommand({
|
|
3366
|
+
meta: {
|
|
3367
|
+
name: "react",
|
|
3368
|
+
description: "Add and/or remove a GitHub reaction on a PR/issue comment \u2014 the ChatOps acknowledgement (\u{1F440} on receipt, swapped to \u{1F680} on completion). Cosmetic: warns and exits 0 on any API error so a reaction never fails the job."
|
|
3369
|
+
},
|
|
3370
|
+
args: {
|
|
3371
|
+
repo: { type: "string", description: "Repository (owner/name)", required: true },
|
|
3372
|
+
"comment-id": {
|
|
3373
|
+
type: "string",
|
|
3374
|
+
description: "Comment id to react on (github.event.comment.id)",
|
|
3375
|
+
required: true
|
|
3376
|
+
},
|
|
3377
|
+
add: { type: "string", description: `Reaction to add: ${REACTIONS.join(" | ")}` },
|
|
3378
|
+
remove: {
|
|
3379
|
+
type: "string",
|
|
3380
|
+
description: "Reaction (of the token owner) to remove after adding \u2014 for the \u{1F440}\u2192\u{1F680} swap"
|
|
3381
|
+
}
|
|
3382
|
+
},
|
|
3383
|
+
run: async ({ args }) => {
|
|
3384
|
+
const commentId = requirePositiveInt(args["comment-id"], "--comment-id");
|
|
3385
|
+
const add = args.add ? requireReaction(args.add) : void 0;
|
|
3386
|
+
const remove = args.remove ? requireReaction(args.remove) : void 0;
|
|
3387
|
+
if (add === void 0 && remove === void 0)
|
|
3388
|
+
fail("react: nothing to do \u2014 pass --add and/or --remove");
|
|
3389
|
+
await react({ repo: args.repo, commentId, add, remove }).catch(
|
|
3390
|
+
(err) => process.stderr.write(
|
|
3391
|
+
`code-review react: reaction update failed (${errMsg(err)}) \u2014 continuing (reactions are cosmetic)
|
|
3392
|
+
`
|
|
3393
|
+
)
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
});
|
|
3098
3397
|
var main = defineCommand({
|
|
3099
3398
|
meta: {
|
|
3100
3399
|
name: "code-review",
|
|
@@ -3103,6 +3402,8 @@ var main = defineCommand({
|
|
|
3103
3402
|
},
|
|
3104
3403
|
subCommands: {
|
|
3105
3404
|
gather: gatherCmd,
|
|
3405
|
+
"parse-command": parseCommandCmd,
|
|
3406
|
+
react: reactCmd,
|
|
3106
3407
|
render: renderCmd,
|
|
3107
3408
|
inline: inlineCmd,
|
|
3108
3409
|
post: postCmd,
|