@coderook/cli 0.36.0 → 0.38.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/.claude-plugin/plugin.json +1 -1
- package/README.md +90 -22
- package/dist/cli/src/actions_command.js +254 -0
- package/dist/cli/src/api.js +32 -15
- package/dist/cli/src/build_command.js +8 -0
- package/dist/cli/src/cli.js +297 -44
- package/dist/cli/src/community_command.js +136 -0
- package/dist/cli/src/config.js +2 -1
- package/dist/cli/src/issue_command.js +186 -0
- package/dist/cli/src/pr_command.js +318 -0
- package/dist/cli/src/private_command.js +65 -0
- package/dist/cli/src/project_commands.js +2 -1
- package/dist/cli/src/runner.js +108 -9
- package/dist/cli/src/service_commands.js +121 -5
- package/dist/cli/src/verify_command.js +135 -0
- package/dist/cli/src/workspace_command.js +198 -0
- package/package.json +66 -66
- package/skills/coderook/SKILL.md +413 -371
- package/dist/cli/src/github_bridge.js +0 -403
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commandFork = commandFork;
|
|
4
|
+
exports.commandSecurity = commandSecurity;
|
|
5
|
+
/**
|
|
6
|
+
* `cbx fork` — your own copy of somebody's project, and offering work back.
|
|
7
|
+
* `cbx security` — private vulnerability reports.
|
|
8
|
+
*
|
|
9
|
+
* cbx fork sam/editor [--name my-editor] copy it into your account
|
|
10
|
+
* cbx fork offer [line] --title "…" offer this fork's line to the original as a pull request
|
|
11
|
+
* cbx fork sync bring the original's latest into the "upstream" line
|
|
12
|
+
*
|
|
13
|
+
* cbx security report sam/editor --title "…" [--severity high] [--body "…"]
|
|
14
|
+
* cbx security list | view S-3 | reply S-3 -m "…" | mark S-3 fixed
|
|
15
|
+
*/
|
|
16
|
+
const api_js_1 = require("./api.js");
|
|
17
|
+
const colour_js_1 = require("./colour.js");
|
|
18
|
+
const project_commands_js_1 = require("./project_commands.js");
|
|
19
|
+
function flagText(parsed, ...names) {
|
|
20
|
+
for (const name of names) {
|
|
21
|
+
const value = parsed.flags.get(name);
|
|
22
|
+
if (typeof value === "string")
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
async function commandFork(parsed) {
|
|
28
|
+
const sub = parsed.positional[0] ?? "";
|
|
29
|
+
if (sub === "offer" || sub === "sync") {
|
|
30
|
+
const project = await (0, project_commands_js_1.resolveProject)(flagText(parsed, "project"));
|
|
31
|
+
if (!project)
|
|
32
|
+
return 1;
|
|
33
|
+
if (sub === "sync") {
|
|
34
|
+
const result = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/sync-upstream`, { method: "POST" });
|
|
35
|
+
console.log(`${(0, colour_js_1.green)("Brought in.")} The original's latest is v${result.sequence} on the ${(0, colour_js_1.accent)("upstream")} line.`);
|
|
36
|
+
console.log((0, colour_js_1.dim)(" Merge it into yours when you are ready: cbx propose upstream, or on the website."));
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
const line = parsed.positional[1] ?? "main";
|
|
40
|
+
const result = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/upstream-pulls`, { method: "POST", body: { line, title: flagText(parsed, "title", "m", "message") ?? `Changes from ${line}`, body: flagText(parsed, "body") ?? null } });
|
|
41
|
+
console.log(result.updated
|
|
42
|
+
? `${(0, colour_js_1.green)("Updated")} ${result.pull.reference} with your newest save on ${line}.`
|
|
43
|
+
: `${(0, colour_js_1.green)("Opened")} ${result.pull.reference} on the original, from ${result.line}.`);
|
|
44
|
+
console.log((0, colour_js_1.dim)(" Its runs there wait for the project's approval and get none of its secrets."));
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
const reference = sub;
|
|
48
|
+
if (!reference || !reference.includes("/")) {
|
|
49
|
+
console.error((0, colour_js_1.red)("Whose project? Try: cbx fork sam/editor (or: cbx fork offer, cbx fork sync)"));
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const source = await (0, api_js_1.findProject)(reference);
|
|
53
|
+
if (!source) {
|
|
54
|
+
console.error((0, colour_js_1.red)(`No project ${reference} that you can see.`));
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const fork = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(source.id)}/fork`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
body: flagText(parsed, "name") ? { slug: flagText(parsed, "name") } : {},
|
|
60
|
+
});
|
|
61
|
+
console.log(`${(0, colour_js_1.green)("Forked")} ${reference} as ${(0, colour_js_1.bold)(fork.slug)} (private, v${fork.sequence}).`);
|
|
62
|
+
console.log((0, colour_js_1.dim)(` Get it: cbx get ${fork.slug} Offer changes back: cbx fork offer <line> --title "…"`));
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
async function commandSecurity(parsed) {
|
|
66
|
+
const sub = parsed.positional[0] ?? "list";
|
|
67
|
+
if (sub === "report") {
|
|
68
|
+
const reference = parsed.positional[1];
|
|
69
|
+
const target = reference ? await (0, api_js_1.findProject)(reference) : await (0, project_commands_js_1.resolveProject)(flagText(parsed, "project"));
|
|
70
|
+
if (!target) {
|
|
71
|
+
console.error((0, colour_js_1.red)(reference ? `No project ${reference} that you can see.` : "Which project? cbx security report sam/editor --title \"…\""));
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const title = flagText(parsed, "title", "m", "message");
|
|
75
|
+
if (!title) {
|
|
76
|
+
console.error((0, colour_js_1.red)('Say what is wrong: --title "Path traversal in export"'));
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
const created = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(target.id)}/security-reports`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: { title, body: flagText(parsed, "body") ?? "", severity: flagText(parsed, "severity") ?? "unknown" },
|
|
82
|
+
});
|
|
83
|
+
console.log(`${(0, colour_js_1.green)("Sent privately")} as S-${created.number}. Only the project's owner and admins can read it; you hear when they reply.`);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const project = await (0, project_commands_js_1.resolveProject)(flagText(parsed, "project"));
|
|
87
|
+
if (!project)
|
|
88
|
+
return 1;
|
|
89
|
+
const base = `/v1/repositories/${encodeURIComponent(project.id)}/security-reports`;
|
|
90
|
+
if (sub === "list") {
|
|
91
|
+
const { reports } = await (0, api_js_1.callApi)(base);
|
|
92
|
+
if (!reports.length) {
|
|
93
|
+
console.log((0, colour_js_1.dim)(`No security reports on ${project.slug} that you can see.`));
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
for (const report of reports) {
|
|
97
|
+
console.log(` ${(0, colour_js_1.accent)(`S-${report.number}`)} ${report.title} ${(0, colour_js_1.dim)(`${report.severity} · ${report.state} · ${report.reporter ?? "?"}`)}`);
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
const number = Number((parsed.positional[1] ?? "").replace(/^S-/i, ""));
|
|
102
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
103
|
+
console.error((0, colour_js_1.red)(`Which report? cbx security ${sub} S-3`));
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
if (sub === "view") {
|
|
107
|
+
const report = await (0, api_js_1.callApi)(`${base}/${number}`);
|
|
108
|
+
console.log(`${(0, colour_js_1.bold)(`S-${report.number} ${report.title}`)} ${(0, colour_js_1.dim)(`${report.severity} · ${report.state} · from ${report.reporter ?? "?"}`)}`);
|
|
109
|
+
console.log(report.body || (0, colour_js_1.dim)("(no details)"));
|
|
110
|
+
for (const comment of report.comments)
|
|
111
|
+
console.log(`\n${(0, colour_js_1.accent)(comment.author ?? "Somebody")} ${(0, colour_js_1.dim)(new Date(comment.createdAt).toLocaleString())}\n${comment.body}`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
if (sub === "reply") {
|
|
115
|
+
const body = flagText(parsed, "m", "message");
|
|
116
|
+
if (!body) {
|
|
117
|
+
console.error((0, colour_js_1.red)('Say something: cbx security reply S-3 -m "Confirmed, fixing"'));
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
await (0, api_js_1.callApi)(`${base}/${number}/comments`, { method: "POST", body: { body } });
|
|
121
|
+
console.log((0, colour_js_1.green)(`Replied on S-${number}.`));
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (sub === "mark") {
|
|
125
|
+
const state = parsed.positional[2];
|
|
126
|
+
if (!state || !["open", "accepted", "fixed", "declined"].includes(state)) {
|
|
127
|
+
console.error((0, colour_js_1.red)("Mark it open, accepted, fixed or declined: cbx security mark S-3 fixed"));
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
await (0, api_js_1.callApi)(`${base}/${number}`, { method: "PATCH", body: { state } });
|
|
131
|
+
console.log((0, colour_js_1.green)(`S-${number} is ${state}. Whoever reported it is told.`));
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
console.error((0, colour_js_1.red)(`cbx security has no "${sub}". `) + "Try: report, list, view, reply, mark");
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
package/dist/cli/src/config.js
CHANGED
|
@@ -32,7 +32,8 @@ const node_os_1 = require("node:os");
|
|
|
32
32
|
const node_path_1 = __importDefault(require("node:path"));
|
|
33
33
|
exports.DEFAULT_API = "https://api.coderook.com";
|
|
34
34
|
function apiOrigin() {
|
|
35
|
-
|
|
35
|
+
/* CODEROOK_API is what a run is handed (with CODEROOK_TOKEN), so cbx inside a run needs no setup. */
|
|
36
|
+
return (process.env.CODEROOK_API_URL || process.env.CODEROOK_API || exports.DEFAULT_API).replace(/\/+$/, "");
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
39
|
* The website that goes with the service, for a link somebody can open.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commandIssue = commandIssue;
|
|
4
|
+
/**
|
|
5
|
+
* `cbx issue` — one issue at a time, from the terminal.
|
|
6
|
+
*
|
|
7
|
+
* cbx issue view 12 the issue, its labels, its timeline and comments
|
|
8
|
+
* cbx issue comment 12 -m "…" reply
|
|
9
|
+
* cbx issue close 12 [--reason not-planned | --duplicate-of 9]
|
|
10
|
+
* cbx issue reopen 12
|
|
11
|
+
* cbx issue label 12 bug,ui [--remove]
|
|
12
|
+
* cbx issue assign 12 sam | none
|
|
13
|
+
* cbx issue labels [--starter] the project's labels
|
|
14
|
+
* cbx issue rules the project's issue rules
|
|
15
|
+
*
|
|
16
|
+
* Listing and opening issues stays with `cbx issues` and `cbx issues --new`.
|
|
17
|
+
* Inside a run, CODEROOK_TOKEN and CODEROOK_PROJECT make these work with no
|
|
18
|
+
* setup, so a workflow can comment on or label the issue that started it.
|
|
19
|
+
*/
|
|
20
|
+
const api_js_1 = require("./api.js");
|
|
21
|
+
const colour_js_1 = require("./colour.js");
|
|
22
|
+
const project_commands_js_1 = require("./project_commands.js");
|
|
23
|
+
function text(parsed) {
|
|
24
|
+
const message = parsed.flags.get("m") ?? parsed.flags.get("message");
|
|
25
|
+
return typeof message === "string" && message.trim() ? message : null;
|
|
26
|
+
}
|
|
27
|
+
function eventLine(event) {
|
|
28
|
+
const who = event.actor ?? "A rule";
|
|
29
|
+
const detail = event.detail;
|
|
30
|
+
switch (event.kind) {
|
|
31
|
+
case "closed":
|
|
32
|
+
return `${who} closed it (${String(detail.reason ?? "completed").replace("_", " ")})`;
|
|
33
|
+
case "marked_duplicate":
|
|
34
|
+
return `${who} closed it as a duplicate of #${String(detail.duplicateOf)}`;
|
|
35
|
+
case "reopened":
|
|
36
|
+
return `${who} reopened it`;
|
|
37
|
+
case "labeled":
|
|
38
|
+
return `${who} added ${String(detail.label)}`;
|
|
39
|
+
case "unlabeled":
|
|
40
|
+
return `${who} removed ${String(detail.label)}`;
|
|
41
|
+
case "assigned":
|
|
42
|
+
return `${who} assigned it`;
|
|
43
|
+
case "unassigned":
|
|
44
|
+
return `${who} removed the assignee`;
|
|
45
|
+
case "renamed":
|
|
46
|
+
return `${who} renamed it from "${String(detail.from)}"`;
|
|
47
|
+
case "referenced":
|
|
48
|
+
return `${who} mentioned it in ${detail.sourceKind === "pull" ? String(detail.reference) : `#${String(detail.number)}`}`;
|
|
49
|
+
default:
|
|
50
|
+
return `${who} ${event.kind}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function commandIssue(parsed) {
|
|
54
|
+
const sub = parsed.positional[0] ?? "";
|
|
55
|
+
const named = parsed.flags.get("project");
|
|
56
|
+
const project = await (0, project_commands_js_1.resolveProject)(typeof named === "string" ? named : undefined);
|
|
57
|
+
if (!project)
|
|
58
|
+
return 1;
|
|
59
|
+
const base = `/v1/repositories/${encodeURIComponent(project.id)}`;
|
|
60
|
+
if (sub === "labels") {
|
|
61
|
+
if (parsed.flags.has("starter")) {
|
|
62
|
+
const { added } = await (0, api_js_1.callApi)(`${base}/issue-labels/starter`, { method: "POST" });
|
|
63
|
+
console.log((0, colour_js_1.green)(added ? `Added ${added} labels.` : "The usual labels are already there."));
|
|
64
|
+
}
|
|
65
|
+
const { labels } = await (0, api_js_1.callApi)(`${base}/issue-labels`);
|
|
66
|
+
if (!labels.length) {
|
|
67
|
+
console.log(`${(0, colour_js_1.bold)(project.slug)} has no labels. ${(0, colour_js_1.dim)("Add the usual set: cbx issue labels --starter")}`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
for (const label of labels) {
|
|
71
|
+
console.log(` ${(0, colour_js_1.accent)(label.name.padEnd(20))} ${(0, colour_js_1.dim)(`${label.colour} ${label.open} open${label.description ? ` ${label.description}` : ""}`)}`);
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (sub === "rules") {
|
|
76
|
+
const { rules } = await (0, api_js_1.callApi)(`${base}/issue-rules`);
|
|
77
|
+
if (!rules.length) {
|
|
78
|
+
console.log(`${(0, colour_js_1.bold)(project.slug)} has no issue rules. ${(0, colour_js_1.dim)("Add them in the project's Settings, under Issue rules.")}`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
for (const rule of rules) {
|
|
82
|
+
console.log(` ${rule.enabled ? (0, colour_js_1.green)("on ") : (0, colour_js_1.dim)("off")} ${(0, colour_js_1.accent)(rule.kind.padEnd(12))} ${(0, colour_js_1.dim)(JSON.stringify(rule.config))}`);
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const number = Number((parsed.positional[1] ?? "").replace(/^#/, ""));
|
|
87
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
88
|
+
console.error((0, colour_js_1.red)(`Which issue? Try: cbx issue ${sub || "view"} 12`));
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
const at = `${base}/issues/${number}`;
|
|
92
|
+
switch (sub) {
|
|
93
|
+
case "view": {
|
|
94
|
+
const thread = await (0, api_js_1.callApi)(at);
|
|
95
|
+
const issue = thread.issue;
|
|
96
|
+
console.log(`${(0, colour_js_1.bold)(`#${issue.number} ${issue.title}`)} ${issue.state === "open" ? (0, colour_js_1.green)("open") : (0, colour_js_1.dim)(`closed${issue.closeReason ? ` (${issue.closeReason.replace("_", " ")}${issue.duplicateOf ? ` of #${issue.duplicateOf}` : ""})` : ""}`)}`);
|
|
97
|
+
if (issue.labels?.length)
|
|
98
|
+
console.log((0, colour_js_1.dim)(` labels: ${issue.labels.map((label) => label.name).join(", ")}`));
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(issue.body || (0, colour_js_1.dim)("(no description)"));
|
|
101
|
+
const stream = [
|
|
102
|
+
...thread.comments.map((comment) => ({ at: comment.createdAt, line: `\n${(0, colour_js_1.accent)(comment.automated ? "Project rule" : comment.author.displayName)} ${(0, colour_js_1.dim)(new Date(comment.createdAt).toLocaleString())}\n${comment.body}` })),
|
|
103
|
+
...(thread.events ?? []).filter((event) => event.kind !== "opened").map((event) => ({ at: event.createdAt, line: (0, colour_js_1.dim)(` · ${eventLine(event)}, ${new Date(event.createdAt).toLocaleString()}`) })),
|
|
104
|
+
].sort((a, b) => new Date(a.at).getTime() - new Date(b.at).getTime());
|
|
105
|
+
for (const item of stream)
|
|
106
|
+
console.log(item.line);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
case "comment": {
|
|
110
|
+
const body = text(parsed);
|
|
111
|
+
if (!body) {
|
|
112
|
+
console.error((0, colour_js_1.red)('Say something: cbx issue comment 12 -m "Fixed in v41"'));
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
await (0, api_js_1.callApi)(`${at}/comments`, { method: "POST", body: { body } });
|
|
116
|
+
console.log((0, colour_js_1.green)(`Replied on #${number}.`));
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
case "close": {
|
|
120
|
+
const duplicate = parsed.flags.get("duplicate-of");
|
|
121
|
+
const reasonFlag = parsed.flags.get("reason");
|
|
122
|
+
const reason = typeof duplicate === "string"
|
|
123
|
+
? "duplicate"
|
|
124
|
+
: typeof reasonFlag === "string"
|
|
125
|
+
? reasonFlag.replace("-", "_")
|
|
126
|
+
: "completed";
|
|
127
|
+
if (!["completed", "not_planned", "duplicate"].includes(reason)) {
|
|
128
|
+
console.error((0, colour_js_1.red)("A reason is completed, not-planned, or --duplicate-of <n>."));
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
await (0, api_js_1.callApi)(at, {
|
|
132
|
+
method: "PATCH",
|
|
133
|
+
body: {
|
|
134
|
+
state: "closed",
|
|
135
|
+
reason,
|
|
136
|
+
...(typeof duplicate === "string" ? { duplicateOf: Number(duplicate.replace(/^#/, "")) } : {}),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const body = text(parsed);
|
|
140
|
+
if (body)
|
|
141
|
+
await (0, api_js_1.callApi)(`${at}/comments`, { method: "POST", body: { body } });
|
|
142
|
+
console.log((0, colour_js_1.green)(`Closed #${number}${reason === "completed" ? "" : ` (${reason.replace("_", " ")})`}.`));
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
case "reopen":
|
|
146
|
+
await (0, api_js_1.callApi)(at, { method: "PATCH", body: { state: "open" } });
|
|
147
|
+
console.log((0, colour_js_1.green)(`Reopened #${number}.`));
|
|
148
|
+
return 0;
|
|
149
|
+
case "label": {
|
|
150
|
+
const names = (parsed.positional[2] ?? "").split(",").map((one) => one.trim()).filter(Boolean);
|
|
151
|
+
if (!names.length) {
|
|
152
|
+
console.error((0, colour_js_1.red)("Which labels? cbx issue label 12 bug,ui"));
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
const remove = parsed.flags.has("remove");
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
await (0, api_js_1.callApi)(`${at}/labels/${encodeURIComponent(name)}`, { method: remove ? "DELETE" : "PUT" });
|
|
158
|
+
}
|
|
159
|
+
console.log((0, colour_js_1.green)(`${remove ? "Removed" : "Added"} ${names.join(", ")} ${remove ? "from" : "on"} #${number}.`));
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
case "assign": {
|
|
163
|
+
const who = (parsed.positional[2] ?? "").replace(/^@/, "").trim();
|
|
164
|
+
if (!who) {
|
|
165
|
+
console.error((0, colour_js_1.red)("To whom? cbx issue assign 12 sam (or none)"));
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
let assigneeUserId = null;
|
|
169
|
+
if (who !== "none") {
|
|
170
|
+
const { people } = await (0, api_js_1.callApi)(`${base}/assignable`);
|
|
171
|
+
const person = people.find((one) => (one.username ?? "").toLowerCase() === who.toLowerCase() || one.name.toLowerCase() === who.toLowerCase());
|
|
172
|
+
if (!person) {
|
|
173
|
+
console.error((0, colour_js_1.red)(`${who} is not on ${project.name}. It can go to: ${people.map((one) => one.username ?? one.name).join(", ")}`));
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
assigneeUserId = person.id;
|
|
177
|
+
}
|
|
178
|
+
await (0, api_js_1.callApi)(at, { method: "PATCH", body: { assigneeUserId } });
|
|
179
|
+
console.log((0, colour_js_1.green)(assigneeUserId ? `#${number} is ${who}'s now.` : `#${number} is nobody's now.`));
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
default:
|
|
183
|
+
console.error((0, colour_js_1.red)(`cbx issue has no "${sub}". `) + "Try: view, comment, close, reopen, label, assign, labels, rules");
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commandPr = commandPr;
|
|
4
|
+
/**
|
|
5
|
+
* `cbx pr` — a project's pull requests, from the command line.
|
|
6
|
+
*
|
|
7
|
+
* A pull request is a proposal: a line offered to another line (`cbx propose`
|
|
8
|
+
* opens one). This is the rest of what the website's Pull requests tab does —
|
|
9
|
+
* the list, one pull request's conversation, files and checks, commenting,
|
|
10
|
+
* reviewing, labels, drafts, closing and reopening, and merging — so the work
|
|
11
|
+
* can be reviewed without leaving the terminal.
|
|
12
|
+
*/
|
|
13
|
+
const api_js_1 = require("./api.js");
|
|
14
|
+
const colour_js_1 = require("./colour.js");
|
|
15
|
+
const project_commands_js_1 = require("./project_commands.js");
|
|
16
|
+
const flag = (parsed, ...names) => {
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
const value = parsed.flags.get(name);
|
|
19
|
+
if (typeof value === "string")
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
};
|
|
24
|
+
function who(person) {
|
|
25
|
+
if (!person)
|
|
26
|
+
return "somebody";
|
|
27
|
+
return person.username ? `@${person.username}` : person.displayName;
|
|
28
|
+
}
|
|
29
|
+
function stateWord(state) {
|
|
30
|
+
if (state === "open")
|
|
31
|
+
return (0, colour_js_1.green)("open");
|
|
32
|
+
if (state === "merged")
|
|
33
|
+
return (0, colour_js_1.accent)("merged");
|
|
34
|
+
if (state === "closed")
|
|
35
|
+
return (0, colour_js_1.red)("closed");
|
|
36
|
+
return (0, colour_js_1.dim)(state);
|
|
37
|
+
}
|
|
38
|
+
function checksWord(checks) {
|
|
39
|
+
if (!checks.total)
|
|
40
|
+
return "";
|
|
41
|
+
if (checks.failing)
|
|
42
|
+
return (0, colour_js_1.red)(`✗ ${checks.failing} failing`);
|
|
43
|
+
if (checks.pending)
|
|
44
|
+
return (0, colour_js_1.accent)(`● ${checks.pending} running`);
|
|
45
|
+
return (0, colour_js_1.green)(`✓ ${checks.passing} passing`);
|
|
46
|
+
}
|
|
47
|
+
/** The pull request a person named (M-3, or 3), on the project they named or this folder's. */
|
|
48
|
+
async function find(parsed, at) {
|
|
49
|
+
const wanted = parsed.positional[at];
|
|
50
|
+
if (!wanted) {
|
|
51
|
+
console.error((0, colour_js_1.red)("Which pull request? Name it like M-3."));
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const project = await (0, project_commands_js_1.resolveProject)(flag(parsed, "project"));
|
|
55
|
+
if (!project)
|
|
56
|
+
return null;
|
|
57
|
+
const reference = /^\d+$/.test(wanted) ? `M-${wanted}` : wanted.toUpperCase();
|
|
58
|
+
const { pulls } = await (0, api_js_1.callApi)(`/v1/repositories/${project.id}/pulls?state=all`);
|
|
59
|
+
const found = pulls.find((one) => one.reference.toUpperCase() === reference);
|
|
60
|
+
if (!found) {
|
|
61
|
+
console.error((0, colour_js_1.red)(`${project.name} has no pull request ${reference}.`));
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return { id: found.id, reference: found.reference, projectId: project.id };
|
|
65
|
+
}
|
|
66
|
+
async function list(parsed) {
|
|
67
|
+
const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[1] ?? flag(parsed, "project"));
|
|
68
|
+
if (!project)
|
|
69
|
+
return 1;
|
|
70
|
+
const query = new URLSearchParams();
|
|
71
|
+
const state = parsed.flags.has("all") ? "all" : (flag(parsed, "state") ?? "open");
|
|
72
|
+
query.set("state", state);
|
|
73
|
+
if (parsed.flags.has("mine"))
|
|
74
|
+
query.set("review", "me");
|
|
75
|
+
const label = flag(parsed, "label");
|
|
76
|
+
if (label)
|
|
77
|
+
query.set("label", label);
|
|
78
|
+
const author = flag(parsed, "author");
|
|
79
|
+
if (author)
|
|
80
|
+
query.set("author", author);
|
|
81
|
+
const { pulls, counts } = await (0, api_js_1.callApi)(`/v1/repositories/${project.id}/pulls?${query}`);
|
|
82
|
+
console.log(`${(0, colour_js_1.bold)(project.name)} ${(0, colour_js_1.dim)(`· ${counts.open} open · ${counts.merged} merged · ${counts.closed} closed`)}`);
|
|
83
|
+
if (!pulls.length) {
|
|
84
|
+
console.log((0, colour_js_1.dim)(state === "open" ? " No open pull requests. Offer a line with cbx propose." : " Nothing here."));
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
for (const pull of pulls) {
|
|
88
|
+
const flags = [
|
|
89
|
+
checksWord(pull.checks),
|
|
90
|
+
pull.conflicts ? (0, colour_js_1.accent)(`${pull.conflicts} to settle`) : "",
|
|
91
|
+
pull.changesRequested ? (0, colour_js_1.red)("changes asked") : pull.approvals ? (0, colour_js_1.green)(`${pull.approvals} approved`) : "",
|
|
92
|
+
pull.comments ? (0, colour_js_1.dim)(`${pull.comments} comment${pull.comments === 1 ? "" : "s"}`) : "",
|
|
93
|
+
pull.labels.length ? (0, colour_js_1.dim)(pull.labels.map((one) => one.name).join(", ")) : "",
|
|
94
|
+
].filter(Boolean);
|
|
95
|
+
console.log(` ${(0, colour_js_1.accent)(pull.reference.padEnd(6))} ${stateWord(pull.state).padEnd(16)} ${pull.title}`);
|
|
96
|
+
console.log(` ${(0, colour_js_1.dim)(`${pull.from} → ${pull.into} · ${who(pull.author)}`)}${flags.length ? ` ${flags.join(" ")}` : ""}`);
|
|
97
|
+
}
|
|
98
|
+
console.log("");
|
|
99
|
+
console.log((0, colour_js_1.dim)(" Read one with: cbx pr view M-3"));
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
async function view(parsed) {
|
|
103
|
+
const found = await find(parsed, 1);
|
|
104
|
+
if (!found)
|
|
105
|
+
return 1;
|
|
106
|
+
const pull = await (0, api_js_1.callApi)(`/v1/merge-tracks/${found.id}/pull`);
|
|
107
|
+
console.log(`${(0, colour_js_1.accent)(pull.reference)} ${(0, colour_js_1.bold)(pull.title)} ${stateWord(pull.state)}`);
|
|
108
|
+
console.log((0, colour_js_1.dim)(` ${who(pull.author)} wants to merge ${pull.from} into ${pull.into}`));
|
|
109
|
+
if (pull.labels.length)
|
|
110
|
+
console.log((0, colour_js_1.dim)(` Labels: ${pull.labels.map((one) => one.name).join(", ")}`));
|
|
111
|
+
if (pull.linkedIssues.length)
|
|
112
|
+
console.log((0, colour_js_1.dim)(` Closes: ${pull.linkedIssues.map((one) => `#${one.number}`).join(", ")}`));
|
|
113
|
+
console.log("");
|
|
114
|
+
console.log(pull.body?.trim() || (0, colour_js_1.dim)(" No description."));
|
|
115
|
+
console.log("");
|
|
116
|
+
if (pull.reviews.length || pull.requested.length) {
|
|
117
|
+
console.log((0, colour_js_1.bold)("Reviews"));
|
|
118
|
+
for (const review of pull.reviews) {
|
|
119
|
+
const verdict = review.dismissed
|
|
120
|
+
? (0, colour_js_1.accent)("approval no longer counts")
|
|
121
|
+
: review.verdict === "approve"
|
|
122
|
+
? (0, colour_js_1.green)("approved")
|
|
123
|
+
: review.verdict === "request_changes"
|
|
124
|
+
? (0, colour_js_1.red)("asked for changes")
|
|
125
|
+
: "commented";
|
|
126
|
+
console.log(` ${who(review.reviewer)} ${verdict}${review.body ? (0, colour_js_1.dim)(` — ${review.body}`) : ""}`);
|
|
127
|
+
}
|
|
128
|
+
for (const asked of pull.requested.filter((one) => !one.reviewed)) {
|
|
129
|
+
console.log(` @${asked.reviewer?.username ?? "somebody"} ${(0, colour_js_1.dim)("asked, not yet reviewed")}`);
|
|
130
|
+
}
|
|
131
|
+
console.log("");
|
|
132
|
+
}
|
|
133
|
+
if (pull.checks.length) {
|
|
134
|
+
console.log((0, colour_js_1.bold)("Checks"));
|
|
135
|
+
for (const check of pull.checks) {
|
|
136
|
+
const mark = check.state === "passing" ? (0, colour_js_1.green)("✓") : check.state === "failing" ? (0, colour_js_1.red)("✗") : check.state === "skipped" ? (0, colour_js_1.dim)("–") : (0, colour_js_1.accent)("●");
|
|
137
|
+
console.log(` ${mark} ${check.name}${check.required ? (0, colour_js_1.dim)(" (required)") : ""}${check.detail ? (0, colour_js_1.dim)(` — ${check.detail}`) : ""}`);
|
|
138
|
+
}
|
|
139
|
+
console.log("");
|
|
140
|
+
}
|
|
141
|
+
const roots = pull.comments.filter((comment) => !comment.replyTo);
|
|
142
|
+
if (roots.length) {
|
|
143
|
+
console.log((0, colour_js_1.bold)("Conversation"));
|
|
144
|
+
for (const root of roots) {
|
|
145
|
+
const where = root.path ? (0, colour_js_1.dim)(` on ${root.path}${root.line ? `:${root.line}` : ""}${root.resolved ? " (resolved)" : ""}`) : "";
|
|
146
|
+
console.log(` ${who(root.author)}${where}: ${root.body ?? (0, colour_js_1.dim)("(deleted)")}`);
|
|
147
|
+
for (const reply of pull.comments.filter((comment) => comment.replyTo === root.id)) {
|
|
148
|
+
console.log(` ${who(reply.author)}: ${reply.body ?? (0, colour_js_1.dim)("(deleted)")}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
console.log("");
|
|
152
|
+
}
|
|
153
|
+
if (pull.state === "open" || pull.state === "draft") {
|
|
154
|
+
if (pull.blockers.length) {
|
|
155
|
+
console.log((0, colour_js_1.bold)((0, colour_js_1.accent)("Merging is blocked")));
|
|
156
|
+
for (const blocker of pull.blockers)
|
|
157
|
+
console.log(` · ${blocker.text}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.log((0, colour_js_1.green)("Ready to merge.") + (0, colour_js_1.dim)(` cbx pr merge ${pull.reference}`));
|
|
161
|
+
}
|
|
162
|
+
if (pull.unresolvedThreads)
|
|
163
|
+
console.log((0, colour_js_1.dim)(` ${pull.unresolvedThreads} conversation(s) on lines not yet resolved.`));
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
async function files(parsed) {
|
|
168
|
+
const found = await find(parsed, 1);
|
|
169
|
+
if (!found)
|
|
170
|
+
return 1;
|
|
171
|
+
const answer = await (0, api_js_1.callApi)(`/v1/merge-tracks/${found.id}/files`);
|
|
172
|
+
for (const file of answer.files) {
|
|
173
|
+
const mark = file.kind === "added" ? (0, colour_js_1.green)("A") : file.kind === "removed" ? (0, colour_js_1.red)("D") : (0, colour_js_1.accent)("M");
|
|
174
|
+
console.log(` ${mark} ${file.path}${file.viewed ? (0, colour_js_1.dim)(" (viewed)") : ""}`);
|
|
175
|
+
}
|
|
176
|
+
console.log((0, colour_js_1.dim)(` ${answer.files.length} file${answer.files.length === 1 ? "" : "s"} changed`));
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
async function comment(parsed) {
|
|
180
|
+
const found = await find(parsed, 1);
|
|
181
|
+
if (!found)
|
|
182
|
+
return 1;
|
|
183
|
+
const body = flag(parsed, "message", "m") ?? parsed.positional.slice(2).join(" ");
|
|
184
|
+
if (!body.trim()) {
|
|
185
|
+
console.error((0, colour_js_1.red)('Say something: cbx pr comment M-3 -m "Looks good"'));
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
const at = flag(parsed, "line");
|
|
189
|
+
const [path, line] = at ? [at.slice(0, at.lastIndexOf(":")), Number(at.slice(at.lastIndexOf(":") + 1))] : [undefined, undefined];
|
|
190
|
+
await (0, api_js_1.callApi)(`/v1/merge-tracks/${found.id}/comments`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
body: { body, ...(path && line ? { path, line, side: "new" } : {}) },
|
|
193
|
+
});
|
|
194
|
+
console.log(`${(0, colour_js_1.green)("Commented")} on ${found.reference}${path ? ` (${path}:${line})` : ""}.`);
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
async function review(parsed, verdict) {
|
|
198
|
+
const found = await find(parsed, 1);
|
|
199
|
+
if (!found)
|
|
200
|
+
return 1;
|
|
201
|
+
const body = flag(parsed, "message", "m");
|
|
202
|
+
if (verdict === "request_changes" && !body) {
|
|
203
|
+
console.error((0, colour_js_1.red)('Say what needs changing: cbx pr request-changes M-3 -m "…"'));
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
await (0, api_js_1.callApi)(`/v1/merge-tracks/${found.id}/review`, { method: "PUT", body: body ? { verdict, body } : { verdict } });
|
|
207
|
+
console.log(`${verdict === "approve" ? (0, colour_js_1.green)("Approved") : verdict === "request_changes" ? (0, colour_js_1.red)("Asked for changes on") : "Reviewed"} ${found.reference}.`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
async function edit(parsed, change, said) {
|
|
211
|
+
const found = await find(parsed, 1);
|
|
212
|
+
if (!found)
|
|
213
|
+
return 1;
|
|
214
|
+
await (0, api_js_1.callApi)(`/v1/merge-tracks/${found.id}/pull`, { method: "PATCH", body: change });
|
|
215
|
+
console.log(`${found.reference} ${said}.`);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
async function commandPr(parsed) {
|
|
219
|
+
const sub = parsed.positional[0] ?? "list";
|
|
220
|
+
switch (sub) {
|
|
221
|
+
case "list":
|
|
222
|
+
return list(parsed);
|
|
223
|
+
case "view":
|
|
224
|
+
return view(parsed);
|
|
225
|
+
case "files":
|
|
226
|
+
return files(parsed);
|
|
227
|
+
case "comment":
|
|
228
|
+
return comment(parsed);
|
|
229
|
+
case "approve":
|
|
230
|
+
return review(parsed, "approve");
|
|
231
|
+
case "request-changes":
|
|
232
|
+
return review(parsed, "request_changes");
|
|
233
|
+
case "ready":
|
|
234
|
+
return edit(parsed, { draft: false }, "is ready for review");
|
|
235
|
+
case "draft":
|
|
236
|
+
return edit(parsed, { draft: true }, "is a draft again");
|
|
237
|
+
case "title": {
|
|
238
|
+
const title = flag(parsed, "message", "m") ?? parsed.positional.slice(2).join(" ");
|
|
239
|
+
if (!title.trim()) {
|
|
240
|
+
console.error((0, colour_js_1.red)('Give the new title: cbx pr title M-3 "Better title"'));
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
return edit(parsed, { title }, "renamed");
|
|
244
|
+
}
|
|
245
|
+
case "retarget": {
|
|
246
|
+
const into = parsed.positional[2];
|
|
247
|
+
if (!into) {
|
|
248
|
+
console.error((0, colour_js_1.red)("Into which line? cbx pr retarget M-3 release"));
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
return edit(parsed, { into }, `now lands on ${into}`);
|
|
252
|
+
}
|
|
253
|
+
case "label": {
|
|
254
|
+
const found = await find(parsed, 1);
|
|
255
|
+
if (!found)
|
|
256
|
+
return 1;
|
|
257
|
+
const labels = (parsed.positional[2] ?? "").split(",").map((one) => one.trim()).filter(Boolean);
|
|
258
|
+
const saved = await (0, api_js_1.callApi)(`/v1/merge-tracks/${found.id}/labels`, { method: "PUT", body: { labels } });
|
|
259
|
+
console.log(`${found.reference} labels: ${saved.labels.join(", ") || (0, colour_js_1.dim)("none")}`);
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
case "ask": {
|
|
263
|
+
const found = await find(parsed, 1);
|
|
264
|
+
if (!found)
|
|
265
|
+
return 1;
|
|
266
|
+
const names = parsed.positional.slice(2).flatMap((one) => one.split(",")).map((one) => one.trim()).filter(Boolean);
|
|
267
|
+
const asked = await (0, api_js_1.requestReviews)(found.id, names);
|
|
268
|
+
console.log(`Asked ${asked.map((person) => person.displayName).join(", ") || "nobody new"} to review ${found.reference}.`);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
case "close": {
|
|
272
|
+
const found = await find(parsed, 1);
|
|
273
|
+
if (!found)
|
|
274
|
+
return 1;
|
|
275
|
+
await (0, api_js_1.cancelMerge)(found.id);
|
|
276
|
+
console.log(`${found.reference} closed without merging. The line and its saves are untouched.`);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
case "reopen": {
|
|
280
|
+
const found = await find(parsed, 1);
|
|
281
|
+
if (!found)
|
|
282
|
+
return 1;
|
|
283
|
+
await (0, api_js_1.callApi)(`/v1/merge-tracks/${found.id}/reopen`, { method: "POST" });
|
|
284
|
+
console.log(`${found.reference} reopened.`);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
case "merge": {
|
|
288
|
+
const found = await find(parsed, 1);
|
|
289
|
+
if (!found)
|
|
290
|
+
return 1;
|
|
291
|
+
/* --squash lands it as one save, without the line's own saves in the target's history. */
|
|
292
|
+
const result = parsed.flags.has("squash")
|
|
293
|
+
? await (0, api_js_1.callApi)(`/v1/merge-tracks/${encodeURIComponent(found.id)}/apply?squash=true`, { method: "POST" })
|
|
294
|
+
: await (0, api_js_1.applyMerge)(found.id);
|
|
295
|
+
console.log(`${(0, colour_js_1.green)(parsed.flags.has("squash") ? "Squashed and merged" : "Merged")} ${found.reference} as version ${result.version.sequence}.` +
|
|
296
|
+
(result.closedIssues?.length ? ` Closed ${result.closedIssues.map((n) => `#${n}`).join(", ")}.` : ""));
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
case "auto-merge": {
|
|
300
|
+
/* Merge by itself once nothing stands in the way (backend merge_rules.ts); --off stops it. */
|
|
301
|
+
const found = await find(parsed, 1);
|
|
302
|
+
if (!found)
|
|
303
|
+
return 1;
|
|
304
|
+
const enabled = !parsed.flags.has("off");
|
|
305
|
+
await (0, api_js_1.callApi)(`/v1/merge-tracks/${encodeURIComponent(found.id)}/auto-merge`, {
|
|
306
|
+
method: "PUT",
|
|
307
|
+
body: { enabled, squash: parsed.flags.has("squash") },
|
|
308
|
+
});
|
|
309
|
+
console.log(enabled
|
|
310
|
+
? `${(0, colour_js_1.green)("Set.")} ${found.reference} merges by itself once nothing stands in the way${parsed.flags.has("squash") ? ", squashed" : ""}.`
|
|
311
|
+
: `${(0, colour_js_1.green)("Off.")} ${found.reference} waits for somebody to merge it.`);
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
default:
|
|
315
|
+
console.error((0, colour_js_1.red)(`cbx pr has no "${sub}". `) + "See: " + (0, colour_js_1.accent)("cbx help pr"));
|
|
316
|
+
return 1;
|
|
317
|
+
}
|
|
318
|
+
}
|