@coderook/cli 0.35.0 → 0.37.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.
@@ -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
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commandPrivate = commandPrivate;
4
+ /**
5
+ * `cbx private` — files and folders only the project's own people may see.
6
+ *
7
+ * Patterns in the shape of a .gitignore, kept on the project rather than in a
8
+ * file inside it, so they cover every version from the moment they are set,
9
+ * old ones included, and a save can never undo them. Visitors, the public
10
+ * API, git clone, zips and Pages leave those files out; members see them.
11
+ */
12
+ const api_js_1 = require("./api.js");
13
+ const colour_js_1 = require("./colour.js");
14
+ const project_commands_js_1 = require("./project_commands.js");
15
+ async function commandPrivate(parsed) {
16
+ const sub = parsed.positional[0] ?? "list";
17
+ const named = parsed.flags.get("project");
18
+ const project = await (0, project_commands_js_1.resolveProject)(typeof named === "string" ? named : undefined);
19
+ if (!project)
20
+ return 1;
21
+ const current = await (0, api_js_1.getPrivatePaths)(project.id);
22
+ if (sub === "list") {
23
+ if (!current.length) {
24
+ console.log(`Nothing in ${(0, colour_js_1.bold)(project.slug)} is private on its own.`);
25
+ console.log((0, colour_js_1.dim)(` Keep files to the project's people: cbx private add "design/" "*.psd"`));
26
+ return 0;
27
+ }
28
+ console.log((0, colour_js_1.bold)(`Private in ${project.slug}`) + (0, colour_js_1.dim)(" — only the project's people see these"));
29
+ for (const pattern of current)
30
+ console.log(` ${pattern}`);
31
+ return 0;
32
+ }
33
+ const given = parsed.positional.slice(1);
34
+ if (!given.length) {
35
+ console.error((0, colour_js_1.red)(`Name at least one pattern: cbx private ${sub} "design/"`));
36
+ return 1;
37
+ }
38
+ let next;
39
+ if (sub === "add") {
40
+ next = [...current, ...given.filter((one) => !current.includes(one))];
41
+ }
42
+ else if (sub === "remove") {
43
+ const missing = given.filter((one) => !current.includes(one));
44
+ if (missing.length) {
45
+ console.error((0, colour_js_1.red)(`Not in the list: ${missing.join(", ")}. See: cbx private list`));
46
+ return 1;
47
+ }
48
+ next = current.filter((one) => !given.includes(one));
49
+ }
50
+ else if (sub === "set") {
51
+ next = given;
52
+ }
53
+ else {
54
+ console.error((0, colour_js_1.red)(`cbx private has no "${sub}". `) + "See: " + (0, colour_js_1.accent)("cbx help private"));
55
+ return 1;
56
+ }
57
+ const saved = await (0, api_js_1.putPrivatePaths)(project.id, next);
58
+ console.log(`${(0, colour_js_1.green)("Saved.")} ${saved.length ? `Private in ${(0, colour_js_1.bold)(project.slug)}:` : `Nothing in ${project.slug} is private on its own now.`}`);
59
+ for (const pattern of saved)
60
+ console.log(` ${pattern}`);
61
+ if (sub !== "remove" && saved.length) {
62
+ console.log((0, colour_js_1.dim)(" Every version obeys this now, old ones included. Members still see everything."));
63
+ }
64
+ return 0;
65
+ }
@@ -42,7 +42,8 @@ function bytes(value) {
42
42
  * its name every time.
43
43
  */
44
44
  async function resolveProject(reference) {
45
- const wanted = reference ?? (await (0, config_js_1.readLink)(node_process_1.default.cwd()))?.slug;
45
+ /* Inside a run, the project it is running for (CODEROOK_PROJECT, handed to every run). */
46
+ const wanted = reference ?? (await (0, config_js_1.readLink)(node_process_1.default.cwd()))?.slug ?? node_process_1.default.env.CODEROOK_PROJECT?.trim();
46
47
  if (!wanted) {
47
48
  console.error((0, colour_js_1.red)("Which project? Name one, or run this inside a linked folder."));
48
49
  return null;