@coderook/cli 0.38.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -0
- package/dist/cli/src/api.js +48 -16
- package/dist/cli/src/cli.js +395 -124
- package/dist/cli/src/community_command.js +54 -1
- package/dist/cli/src/help.js +21 -6
- package/dist/cli/src/issue_command.js +171 -5
- package/dist/cli/src/machine.js +80 -0
- package/dist/cli/src/project_commands.js +29 -26
- package/dist/cli/src/registry.js +32 -8
- package/dist/cli/src/service_commands.js +154 -0
- package/dist/cli/src/settings_commands.js +532 -0
- package/dist/cli/src/workspace_command.js +3 -0
- package/dist/cli/src/workspace_machine.js +322 -0
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +4 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.onOff = onOff;
|
|
7
|
+
exports.commandAbout = commandAbout;
|
|
8
|
+
exports.commandLineRules = commandLineRules;
|
|
9
|
+
exports.commandUploadRules = commandUploadRules;
|
|
10
|
+
exports.commandAssistant = commandAssistant;
|
|
11
|
+
exports.commandHandOver = commandHandOver;
|
|
12
|
+
/**
|
|
13
|
+
* The rest of a project's Settings, from a terminal.
|
|
14
|
+
*
|
|
15
|
+
* cbx about [project] [--name …] [--description …] General: name, description, address
|
|
16
|
+
* cbx line-rules [line] [--protect] [--approvals n] … Lines & reviews, per line
|
|
17
|
+
* cbx upload-rules [--ignore file] [--encrypt off] … Files & privacy: encrypted upload rules
|
|
18
|
+
* cbx assistant [manual|automated] [--review on] … Automation: the pull-request assistant
|
|
19
|
+
* cbx hand-over <user> [project] Danger zone: offer the project to somebody
|
|
20
|
+
*
|
|
21
|
+
* Each one shows what is set when given nothing to change, because "what is
|
|
22
|
+
* this set to" is most of what people ask, and changes only what was named.
|
|
23
|
+
* They call the routes the website's Settings screen calls, so the service
|
|
24
|
+
* decides who may change what — owners and admins, mostly — and its refusal
|
|
25
|
+
* is printed as it is.
|
|
26
|
+
*
|
|
27
|
+
* AI access is the one setting with no command here that changes it: a
|
|
28
|
+
* person changes it on the website or in the desktop app (see `cbx ai`).
|
|
29
|
+
*/
|
|
30
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
31
|
+
const node_fs_1 = require("node:fs");
|
|
32
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
33
|
+
const promises_1 = require("node:readline/promises");
|
|
34
|
+
const api_js_1 = require("./api.js");
|
|
35
|
+
const config_js_1 = require("./config.js");
|
|
36
|
+
const colour_js_1 = require("./colour.js");
|
|
37
|
+
const project_commands_js_1 = require("./project_commands.js");
|
|
38
|
+
function flagText(parsed, ...names) {
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const value = parsed.flags.get(name);
|
|
41
|
+
if (typeof value === "string")
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `on`/`off` and the other ways people say them. Undefined when the flag was
|
|
48
|
+
* not given; null when it was given and is neither.
|
|
49
|
+
*/
|
|
50
|
+
function onOff(parsed, name) {
|
|
51
|
+
const value = parsed.flags.get(name);
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (value === true)
|
|
55
|
+
return true;
|
|
56
|
+
const text = value.trim().toLowerCase();
|
|
57
|
+
if (["on", "true", "yes", "y", "1"].includes(text))
|
|
58
|
+
return true;
|
|
59
|
+
if (["off", "false", "no", "n", "0"].includes(text))
|
|
60
|
+
return false;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const repositoryRoute = (id) => `/v1/repositories/${encodeURIComponent(id)}`;
|
|
64
|
+
/** General: the project's name, description and address. */
|
|
65
|
+
async function commandAbout(parsed) {
|
|
66
|
+
const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[0] ?? flagText(parsed, "project"));
|
|
67
|
+
if (!project)
|
|
68
|
+
return 1;
|
|
69
|
+
const change = {};
|
|
70
|
+
const name = flagText(parsed, "name");
|
|
71
|
+
if (parsed.flags.has("name")) {
|
|
72
|
+
if (!name?.trim()) {
|
|
73
|
+
console.error((0, colour_js_1.red)('Give the new name: --name "Night Owl Diner"'));
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (name.trim().length > 160) {
|
|
77
|
+
console.error((0, colour_js_1.red)("A project's name is at most 160 characters."));
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
change.displayName = name.trim();
|
|
81
|
+
}
|
|
82
|
+
if (parsed.flags.has("description")) {
|
|
83
|
+
/* An empty description is allowed: it is how one is cleared. */
|
|
84
|
+
const description = flagText(parsed, "description") ?? "";
|
|
85
|
+
if (description.trim().length > 1000) {
|
|
86
|
+
console.error((0, colour_js_1.red)("A description is at most 1000 characters."));
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
change.description = description.trim();
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(change).length) {
|
|
92
|
+
await (0, api_js_1.updateProject)(project.id, change);
|
|
93
|
+
console.log((0, colour_js_1.green)(`Saved ${change.displayName ?? project.name}.`));
|
|
94
|
+
console.log("");
|
|
95
|
+
}
|
|
96
|
+
const record = await (0, api_js_1.callApi)(repositoryRoute(project.id));
|
|
97
|
+
const slug = record.slug ?? project.slug;
|
|
98
|
+
const site = (0, config_js_1.siteOrigin)();
|
|
99
|
+
const address = record.ownerHandle
|
|
100
|
+
? site
|
|
101
|
+
? `${site}/${encodeURIComponent(record.ownerHandle)}/${encodeURIComponent(slug)}`
|
|
102
|
+
: `${record.ownerHandle}/${slug}`
|
|
103
|
+
: slug;
|
|
104
|
+
console.log(` ${(0, colour_js_1.dim)("Name".padEnd(12))} ${(0, colour_js_1.bold)(record.displayName || project.name)}`);
|
|
105
|
+
console.log(` ${(0, colour_js_1.dim)("Description".padEnd(12))} ${record.description?.trim() ? record.description.trim() : (0, colour_js_1.dim)("none")}`);
|
|
106
|
+
console.log(` ${(0, colour_js_1.dim)("Address".padEnd(12))} ${address}`);
|
|
107
|
+
console.log(` ${(0, colour_js_1.dim)("Visibility".padEnd(12))} ${record.visibility ?? project.visibility}`);
|
|
108
|
+
if (!Object.keys(change).length) {
|
|
109
|
+
console.log("");
|
|
110
|
+
console.log((0, colour_js_1.dim)(' Change them: cbx about --name "…" --description "…"'));
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
function describeLine(line) {
|
|
115
|
+
const rules = [
|
|
116
|
+
line.protected ? (0, colour_js_1.green)("protected") : (0, colour_js_1.dim)("not protected"),
|
|
117
|
+
line.requiredApprovals
|
|
118
|
+
? `${line.requiredApprovals} ${line.requiredApprovals === 1 ? "approval" : "approvals"}`
|
|
119
|
+
: (0, colour_js_1.dim)("no approvals needed"),
|
|
120
|
+
line.requireCodeOwner ? "code owners must approve" : "",
|
|
121
|
+
].filter(Boolean);
|
|
122
|
+
const lines = [` ${(0, colour_js_1.accent)(line.name.padEnd(16))} ${rules.join((0, colour_js_1.dim)(" · "))}`];
|
|
123
|
+
if (line.requiredChecks.length)
|
|
124
|
+
lines.push(` ${" ".repeat(16)} ${(0, colour_js_1.dim)("checks:")} ${line.requiredChecks.join(", ")}`);
|
|
125
|
+
return lines;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Lines & reviews: protection, approvals, required checks and code owners,
|
|
129
|
+
* one line at a time. `cbx tracks` lists the lines and is left as it was;
|
|
130
|
+
* this is where their rules are read and set.
|
|
131
|
+
*/
|
|
132
|
+
async function commandLineRules(parsed) {
|
|
133
|
+
const project = await (0, project_commands_js_1.resolveProject)(flagText(parsed, "project"));
|
|
134
|
+
if (!project)
|
|
135
|
+
return 1;
|
|
136
|
+
const base = repositoryRoute(project.id);
|
|
137
|
+
const wanted = parsed.positional[0];
|
|
138
|
+
const protect = parsed.flags.has("protect");
|
|
139
|
+
const unprotect = parsed.flags.has("unprotect");
|
|
140
|
+
if (protect && unprotect) {
|
|
141
|
+
console.error((0, colour_js_1.red)("Pick one: --protect or --unprotect."));
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
let approvals;
|
|
145
|
+
if (parsed.flags.has("approvals")) {
|
|
146
|
+
approvals = Number(flagText(parsed, "approvals"));
|
|
147
|
+
if (!Number.isInteger(approvals) || approvals < 0 || approvals > 10) {
|
|
148
|
+
console.error((0, colour_js_1.red)("Approvals is a whole number from 0 to 10."));
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
let checks;
|
|
153
|
+
if (parsed.flags.has("checks")) {
|
|
154
|
+
const text = (flagText(parsed, "checks") ?? "").trim();
|
|
155
|
+
checks = text === "none" ? [] : text.split(",").map((one) => one.trim()).filter(Boolean);
|
|
156
|
+
}
|
|
157
|
+
const owners = onOff(parsed, "code-owners");
|
|
158
|
+
if (owners === null) {
|
|
159
|
+
console.error((0, colour_js_1.red)("--code-owners is on or off."));
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
const changing = protect || unprotect || approvals !== undefined || checks !== undefined || owners !== undefined;
|
|
163
|
+
const { tracks } = await (0, api_js_1.callApi)(`${base}/tracks`);
|
|
164
|
+
const lines = tracks
|
|
165
|
+
.filter((track) => (track.kind ?? "line") === "line")
|
|
166
|
+
.map((track) => ({
|
|
167
|
+
...track,
|
|
168
|
+
requiredApprovals: Number(track.requiredApprovals ?? 0),
|
|
169
|
+
requiredChecks: Array.isArray(track.requiredChecks) ? track.requiredChecks : [],
|
|
170
|
+
requireCodeOwner: track.requireCodeOwner === true,
|
|
171
|
+
}));
|
|
172
|
+
if (changing && !wanted) {
|
|
173
|
+
console.error((0, colour_js_1.red)("Which line? cbx line-rules main --protect"));
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const line = wanted ? lines.find((one) => one.name === wanted) : undefined;
|
|
177
|
+
if (wanted && !line) {
|
|
178
|
+
console.error((0, colour_js_1.red)(`No line called "${wanted}" on ${project.slug}.`) + (0, colour_js_1.dim)(" cbx tracks lists them."));
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
if (changing && line) {
|
|
182
|
+
const route = `${base}/tracks/${encodeURIComponent(line.name)}`;
|
|
183
|
+
if (protect || unprotect) {
|
|
184
|
+
await (0, api_js_1.callApi)(`${route}/protection`, { method: "PUT", body: { protected: protect } });
|
|
185
|
+
}
|
|
186
|
+
if (approvals !== undefined) {
|
|
187
|
+
await (0, api_js_1.callApi)(`${route}/review-policy`, { method: "PUT", body: { requiredApprovals: approvals } });
|
|
188
|
+
}
|
|
189
|
+
if (checks !== undefined || owners !== undefined) {
|
|
190
|
+
await (0, api_js_1.callApi)(`${route}/rules`, {
|
|
191
|
+
method: "PUT",
|
|
192
|
+
body: {
|
|
193
|
+
...(checks !== undefined ? { requiredChecks: checks } : {}),
|
|
194
|
+
...(owners !== undefined ? { requireCodeOwner: owners } : {}),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const after = (await (0, api_js_1.callApi)(`${base}/tracks`)).tracks.find((one) => one.name === line.name);
|
|
199
|
+
console.log((0, colour_js_1.green)(`Saved the rules for ${line.name}.`));
|
|
200
|
+
if (after) {
|
|
201
|
+
for (const row of describeLine({
|
|
202
|
+
...after,
|
|
203
|
+
requiredApprovals: Number(after.requiredApprovals ?? 0),
|
|
204
|
+
requiredChecks: Array.isArray(after.requiredChecks) ? after.requiredChecks : [],
|
|
205
|
+
requireCodeOwner: after.requireCodeOwner === true,
|
|
206
|
+
}))
|
|
207
|
+
console.log(row);
|
|
208
|
+
}
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
if (!lines.length) {
|
|
212
|
+
console.log((0, colour_js_1.dim)(`${project.slug} has no lines yet. The first save creates main.`));
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
console.log((0, colour_js_1.bold)(`Rules on ${project.slug}'s lines`));
|
|
216
|
+
for (const one of line ? [line] : lines)
|
|
217
|
+
for (const row of describeLine(one))
|
|
218
|
+
console.log(row);
|
|
219
|
+
/* The CODEOWNERS the service read, so "code owners must approve" names somebody. */
|
|
220
|
+
const { rules } = await (0, api_js_1.callApi)(`${base}/code-owners`).catch(() => ({ rules: [] }));
|
|
221
|
+
console.log("");
|
|
222
|
+
if (rules.length) {
|
|
223
|
+
console.log((0, colour_js_1.dim)(`Code owners, from CODEOWNERS on the default line (${rules.length}):`));
|
|
224
|
+
for (const rule of rules.slice(0, 10))
|
|
225
|
+
console.log(` ${rule.pattern.padEnd(24)} ${(0, colour_js_1.dim)(rule.owners.join(" "))}`);
|
|
226
|
+
if (rules.length > 10)
|
|
227
|
+
console.log((0, colour_js_1.dim)(` … and ${rules.length - 10} more`));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log((0, colour_js_1.dim)("No CODEOWNERS file on the default line, so nobody counts as a code owner."));
|
|
231
|
+
}
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log((0, colour_js_1.dim)("Change one: cbx line-rules main --protect --approvals 1 --checks build,test --code-owners on"));
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
/** A file's text, `none` for empty, or null with the reason printed. */
|
|
237
|
+
function rulesFrom(given, flag) {
|
|
238
|
+
if (typeof given !== "string" || !given.trim()) {
|
|
239
|
+
console.error((0, colour_js_1.red)(`--${flag} takes a file of patterns, one per line (or none to clear them).`));
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
if (given.trim() === "none")
|
|
243
|
+
return "";
|
|
244
|
+
const file = node_path_1.default.resolve(given);
|
|
245
|
+
if (!(0, node_fs_1.existsSync)(file) || !(0, node_fs_1.statSync)(file).isFile()) {
|
|
246
|
+
console.error((0, colour_js_1.red)(`No file at ${file}. --${flag} reads its patterns from a file.`));
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return (0, node_fs_1.readFileSync)(file, "utf8");
|
|
250
|
+
}
|
|
251
|
+
const MIB = 1024 * 1024;
|
|
252
|
+
/**
|
|
253
|
+
* Files & privacy → Encrypted uploads: what uploads leave out and keep,
|
|
254
|
+
* whether recognised secret files are encrypted before storage, and the size
|
|
255
|
+
* that earns a warning. The desktop app and the website's scans read the
|
|
256
|
+
* same rules.
|
|
257
|
+
*
|
|
258
|
+
* The service takes the whole set at once, so what was not named is sent
|
|
259
|
+
* back exactly as it was read.
|
|
260
|
+
*/
|
|
261
|
+
async function commandUploadRules(parsed) {
|
|
262
|
+
const project = await (0, project_commands_js_1.resolveProject)(flagText(parsed, "project") ?? parsed.positional[0]);
|
|
263
|
+
if (!project)
|
|
264
|
+
return 1;
|
|
265
|
+
const route = `${repositoryRoute(project.id)}/rules`;
|
|
266
|
+
const current = await (0, api_js_1.callApi)(route);
|
|
267
|
+
const next = { ...current };
|
|
268
|
+
let changed = false;
|
|
269
|
+
if (parsed.flags.has("ignore")) {
|
|
270
|
+
const text = rulesFrom(parsed.flags.get("ignore"), "ignore");
|
|
271
|
+
if (text === null)
|
|
272
|
+
return 1;
|
|
273
|
+
next.ignoreRules = text;
|
|
274
|
+
changed = true;
|
|
275
|
+
}
|
|
276
|
+
if (parsed.flags.has("keep")) {
|
|
277
|
+
const text = rulesFrom(parsed.flags.get("keep"), "keep");
|
|
278
|
+
if (text === null)
|
|
279
|
+
return 1;
|
|
280
|
+
next.keepRules = text;
|
|
281
|
+
changed = true;
|
|
282
|
+
}
|
|
283
|
+
const encrypt = onOff(parsed, "encrypt");
|
|
284
|
+
const hold = onOff(parsed, "hold-pushes");
|
|
285
|
+
if (encrypt === null || hold === null) {
|
|
286
|
+
console.error((0, colour_js_1.red)(`--${encrypt === null ? "encrypt" : "hold-pushes"} is on or off.`));
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
if (encrypt !== undefined) {
|
|
290
|
+
next.encryptSecretFiles = encrypt;
|
|
291
|
+
changed = true;
|
|
292
|
+
}
|
|
293
|
+
if (hold !== undefined) {
|
|
294
|
+
next.holdPushesForReview = hold;
|
|
295
|
+
changed = true;
|
|
296
|
+
}
|
|
297
|
+
if (parsed.flags.has("warn-large")) {
|
|
298
|
+
const mib = Number(flagText(parsed, "warn-large"));
|
|
299
|
+
if (!Number.isFinite(mib) || mib < 0) {
|
|
300
|
+
console.error((0, colour_js_1.red)("--warn-large is a size in MiB, 0 or more."));
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
next.warnLargeFileBytes = Math.round(mib * MIB);
|
|
304
|
+
changed = true;
|
|
305
|
+
}
|
|
306
|
+
if (changed) {
|
|
307
|
+
await (0, api_js_1.callApi)(route, {
|
|
308
|
+
method: "PUT",
|
|
309
|
+
body: {
|
|
310
|
+
ignoreRules: next.ignoreRules,
|
|
311
|
+
keepRules: next.keepRules,
|
|
312
|
+
encryptSecretFiles: next.encryptSecretFiles,
|
|
313
|
+
holdPushesForReview: next.holdPushesForReview,
|
|
314
|
+
warnLargeFileBytes: next.warnLargeFileBytes,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
console.log((0, colour_js_1.green)(`Saved ${project.name}'s upload rules. The desktop app and the website's scans use them now.`));
|
|
318
|
+
console.log("");
|
|
319
|
+
}
|
|
320
|
+
const shown = changed ? await (0, api_js_1.callApi)(route) : current;
|
|
321
|
+
const block = (title, text) => {
|
|
322
|
+
console.log((0, colour_js_1.dim)(title));
|
|
323
|
+
const rows = text.split(/\r?\n/).filter((row) => row.trim());
|
|
324
|
+
if (!rows.length)
|
|
325
|
+
console.log(` ${(0, colour_js_1.dim)("none")}`);
|
|
326
|
+
for (const row of rows)
|
|
327
|
+
console.log(` ${row}`);
|
|
328
|
+
};
|
|
329
|
+
console.log((0, colour_js_1.bold)(`Upload rules for ${project.slug}`));
|
|
330
|
+
console.log(` ${"Encrypt files recognised as secrets".padEnd(38)} ${shown.encryptSecretFiles ? (0, colour_js_1.green)("on") : (0, colour_js_1.red)("off")}`);
|
|
331
|
+
console.log(` ${"Warn about files larger than".padEnd(38)} ${Math.round(Number(shown.warnLargeFileBytes) / MIB)} MiB`);
|
|
332
|
+
console.log(` ${"Hold pushes below admin for review".padEnd(38)} ${shown.holdPushesForReview ? (0, colour_js_1.green)("on") : (0, colour_js_1.dim)("off")}`);
|
|
333
|
+
console.log("");
|
|
334
|
+
block("Ignore rules", shown.ignoreRules ?? "");
|
|
335
|
+
console.log("");
|
|
336
|
+
block("Keep rules", shown.keepRules ?? "");
|
|
337
|
+
if (!changed) {
|
|
338
|
+
console.log("");
|
|
339
|
+
console.log((0, colour_js_1.dim)(" Change them: cbx upload-rules --ignore .coderookignore --encrypt on --warn-large 512"));
|
|
340
|
+
}
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
/* -------------------------------------------------------------- assistant -- */
|
|
344
|
+
const SWITCHES = [
|
|
345
|
+
["review", "reviews each pull request"],
|
|
346
|
+
["describe", "writes the description"],
|
|
347
|
+
["suggest", "suggests changes"],
|
|
348
|
+
["change", "makes changes when asked"],
|
|
349
|
+
["approve", "approves when it finds nothing"],
|
|
350
|
+
["merge", "merges what it approved"],
|
|
351
|
+
];
|
|
352
|
+
/**
|
|
353
|
+
* Automation → PR assistant: manual (only when somebody asks on a pull
|
|
354
|
+
* request) or automated, and what it may do on its own. Changing it needs a
|
|
355
|
+
* repository admin and, for automated, a subscription on the owner's account.
|
|
356
|
+
*/
|
|
357
|
+
async function commandAssistant(parsed) {
|
|
358
|
+
/* `[mode] [project]`, both optional; a lone word that is not a mode is the project. */
|
|
359
|
+
const [first, second] = parsed.positional;
|
|
360
|
+
const isMode = first === "manual" || first === "automated";
|
|
361
|
+
if (first !== undefined && !isMode && second !== undefined) {
|
|
362
|
+
console.error((0, colour_js_1.red)(`"${first}" is not a mode. Pick manual or automated.`));
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
const mode = isMode ? first : undefined;
|
|
366
|
+
const project = await (0, project_commands_js_1.resolveProject)(flagText(parsed, "project") ?? (isMode ? second : first));
|
|
367
|
+
if (!project)
|
|
368
|
+
return 1;
|
|
369
|
+
const route = `${repositoryRoute(project.id)}/assistant`;
|
|
370
|
+
const change = {};
|
|
371
|
+
if (mode)
|
|
372
|
+
change.mode = mode;
|
|
373
|
+
for (const [name] of SWITCHES) {
|
|
374
|
+
const value = onOff(parsed, name);
|
|
375
|
+
if (value === null) {
|
|
376
|
+
console.error((0, colour_js_1.red)(`--${name} is on or off.`));
|
|
377
|
+
return 1;
|
|
378
|
+
}
|
|
379
|
+
if (value !== undefined)
|
|
380
|
+
change[name] = value;
|
|
381
|
+
}
|
|
382
|
+
let answer = await (0, api_js_1.callApi)(route);
|
|
383
|
+
if (Object.keys(change).length) {
|
|
384
|
+
/*
|
|
385
|
+
Said here rather than left to the service, which quietly turns merging
|
|
386
|
+
off when approving is off: "merge" asked for and not given is worth a
|
|
387
|
+
sentence, not a surprise later.
|
|
388
|
+
*/
|
|
389
|
+
const next = { ...answer.settings, ...change };
|
|
390
|
+
if (next.merge && !next.approve) {
|
|
391
|
+
console.error((0, colour_js_1.red)("Merging needs approving on too: add --approve on."));
|
|
392
|
+
return 1;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
await (0, api_js_1.callApi)(route, { method: "PUT", body: change });
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (error instanceof api_js_1.ServiceError && error.code === "assistant_needs_subscription") {
|
|
399
|
+
console.error((0, colour_js_1.red)(error.message));
|
|
400
|
+
console.error((0, colour_js_1.dim)(" Manual mode stays available: cbx assistant manual"));
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
console.log((0, colour_js_1.green)(`Saved ${project.name}'s pull-request assistant.`));
|
|
406
|
+
answer = await (0, api_js_1.callApi)(route);
|
|
407
|
+
}
|
|
408
|
+
const settings = answer.settings;
|
|
409
|
+
console.log("");
|
|
410
|
+
console.log(`${(0, colour_js_1.bold)(project.name)} ${(0, colour_js_1.dim)("pull-request assistant:")} ${(0, colour_js_1.accent)(settings.mode)}` +
|
|
411
|
+
(0, colour_js_1.dim)(settings.mode === "manual" ? " (works only when somebody asks on a pull request)" : " (works on its own)"));
|
|
412
|
+
for (const [name, meaning] of SWITCHES) {
|
|
413
|
+
console.log(` ${name.padEnd(9)} ${settings[name] ? (0, colour_js_1.green)("on ") : (0, colour_js_1.dim)("off")} ${(0, colour_js_1.dim)(meaning)}`);
|
|
414
|
+
}
|
|
415
|
+
console.log("");
|
|
416
|
+
if (!answer.subscribed) {
|
|
417
|
+
console.log((0, colour_js_1.dim)(" Automated mode comes with a subscription on the owner's account."));
|
|
418
|
+
}
|
|
419
|
+
if (!answer.available) {
|
|
420
|
+
console.log((0, colour_js_1.dim)(" The assistant is not available on this service right now."));
|
|
421
|
+
}
|
|
422
|
+
if (answer.allowance) {
|
|
423
|
+
console.log((0, colour_js_1.dim)(` This month: ${answer.allowance.percentUsed}% of the allowance used ` +
|
|
424
|
+
`(${answer.allowance.reviews} reviews, ${answer.allowance.changes} changes).`));
|
|
425
|
+
}
|
|
426
|
+
if (!Object.keys(change).length) {
|
|
427
|
+
console.log((0, colour_js_1.dim)(" Change it: cbx assistant automated --review on --describe on"));
|
|
428
|
+
}
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
/** A plain line typed at the terminal. */
|
|
432
|
+
async function typed(question) {
|
|
433
|
+
const prompt = (0, promises_1.createInterface)({ input: node_process_1.default.stdin, output: node_process_1.default.stdout });
|
|
434
|
+
const answer = await prompt.question(question);
|
|
435
|
+
prompt.close();
|
|
436
|
+
return answer.trim();
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Danger zone → Transfer ownership, and answering an offer.
|
|
440
|
+
*
|
|
441
|
+
* An offer, as on the website: nothing moves until the other person accepts,
|
|
442
|
+
* and it lapses after fourteen days. Once accepted it cannot be undone from
|
|
443
|
+
* this side, so it asks for TRANSFER to be typed, as the website does, unless
|
|
444
|
+
* --yes says a script has already decided.
|
|
445
|
+
*/
|
|
446
|
+
async function commandHandOver(parsed) {
|
|
447
|
+
const first = parsed.positional[0];
|
|
448
|
+
if (!first) {
|
|
449
|
+
console.error((0, colour_js_1.red)("To whom? cbx hand-over <username or email> [project]") + (0, colour_js_1.dim)(" (or: cbx hand-over offers)"));
|
|
450
|
+
return 1;
|
|
451
|
+
}
|
|
452
|
+
if (first === "offers") {
|
|
453
|
+
const { incoming, outgoing } = await (0, api_js_1.callApi)("/v1/account/transfers");
|
|
454
|
+
if (!incoming.length && !outgoing.length) {
|
|
455
|
+
console.log((0, colour_js_1.dim)("No projects are being handed to you or by you."));
|
|
456
|
+
return 0;
|
|
457
|
+
}
|
|
458
|
+
const until = (offer) => `until ${new Date(offer.expiresAt).toLocaleDateString()}`;
|
|
459
|
+
if (incoming.length) {
|
|
460
|
+
console.log((0, colour_js_1.bold)("Offered to you"));
|
|
461
|
+
for (const offer of incoming) {
|
|
462
|
+
console.log(` ${(0, colour_js_1.accent)(offer.slug.padEnd(24))} from ${offer.fromUsername ?? offer.fromName ?? "?"} ${(0, colour_js_1.dim)(`${until(offer)} ${offer.id}`)}`);
|
|
463
|
+
}
|
|
464
|
+
console.log((0, colour_js_1.dim)(" Take one: cbx hand-over accept <project> Turn one down: cbx hand-over decline <project>"));
|
|
465
|
+
}
|
|
466
|
+
if (outgoing.length) {
|
|
467
|
+
if (incoming.length)
|
|
468
|
+
console.log("");
|
|
469
|
+
console.log((0, colour_js_1.bold)("Offered by you"));
|
|
470
|
+
for (const offer of outgoing) {
|
|
471
|
+
console.log(` ${(0, colour_js_1.accent)(offer.slug.padEnd(24))} to ${offer.toUsername ?? offer.toName ?? "?"} ${(0, colour_js_1.dim)(`${until(offer)} ${offer.id}`)}`);
|
|
472
|
+
}
|
|
473
|
+
console.log((0, colour_js_1.dim)(" Withdraw one: cbx hand-over cancel <project>"));
|
|
474
|
+
}
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
if (first === "accept" || first === "decline" || first === "cancel") {
|
|
478
|
+
const wanted = parsed.positional[1]?.trim();
|
|
479
|
+
if (!wanted) {
|
|
480
|
+
console.error((0, colour_js_1.red)(`Which offer? cbx hand-over ${first} <project or offer id>`));
|
|
481
|
+
return 1;
|
|
482
|
+
}
|
|
483
|
+
const { incoming, outgoing } = await (0, api_js_1.callApi)("/v1/account/transfers");
|
|
484
|
+
const pool = first === "cancel" ? outgoing : incoming;
|
|
485
|
+
const offer = pool.find((one) => one.id === wanted || one.slug.toLowerCase() === wanted.toLowerCase());
|
|
486
|
+
if (!offer) {
|
|
487
|
+
console.error((0, colour_js_1.red)(`No offer ${first === "cancel" ? "from" : "to"} you for ${wanted}.`) + (0, colour_js_1.dim)(" cbx hand-over offers lists them."));
|
|
488
|
+
return 1;
|
|
489
|
+
}
|
|
490
|
+
const route = `/v1/account/transfers/${encodeURIComponent(offer.id)}`;
|
|
491
|
+
if (first === "accept") {
|
|
492
|
+
const taken = await (0, api_js_1.callApi)(`${route}/accept`, { method: "POST" });
|
|
493
|
+
console.log((0, colour_js_1.green)(`${taken.displayName ?? offer.projectName} is yours now.`));
|
|
494
|
+
}
|
|
495
|
+
else if (first === "decline") {
|
|
496
|
+
await (0, api_js_1.callApi)(`${route}/decline`, { method: "POST" });
|
|
497
|
+
console.log((0, colour_js_1.green)(`Turned down ${offer.projectName}. It stays where it was.`));
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
await (0, api_js_1.callApi)(route, { method: "DELETE" });
|
|
501
|
+
console.log((0, colour_js_1.green)(`Withdrew the offer of ${offer.projectName}. It stays yours.`));
|
|
502
|
+
}
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
const recipient = first.trim();
|
|
506
|
+
const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[1] ?? flagText(parsed, "project"));
|
|
507
|
+
if (!project)
|
|
508
|
+
return 1;
|
|
509
|
+
const keepAccess = !parsed.flags.has("drop-access");
|
|
510
|
+
const sure = parsed.flags.get("yes") === true || parsed.flags.get("y") === true;
|
|
511
|
+
if (!sure) {
|
|
512
|
+
if (!node_process_1.default.stdin.isTTY) {
|
|
513
|
+
console.error((0, colour_js_1.red)(`Handing ${project.name} over needs TRANSFER typed, and nobody is here to type it.`));
|
|
514
|
+
console.error((0, colour_js_1.dim)(" Run it in a terminal to be asked, or add --yes."));
|
|
515
|
+
return 1;
|
|
516
|
+
}
|
|
517
|
+
console.log(`This offers ${(0, colour_js_1.bold)(project.name)} to ${(0, colour_js_1.bold)(recipient)}.`);
|
|
518
|
+
console.log((0, colour_js_1.dim)(" Nothing moves until they accept, and the offer lapses after fourteen days."));
|
|
519
|
+
console.log((0, colour_js_1.dim)(" Once they accept, it is theirs: only they can give it back."));
|
|
520
|
+
console.log((0, colour_js_1.dim)(keepAccess ? " You keep admin access afterwards (--drop-access to give it up)." : " You give up your access the moment they accept."));
|
|
521
|
+
if ((await typed("Type TRANSFER to offer it: ")) !== "TRANSFER") {
|
|
522
|
+
console.error((0, colour_js_1.red)("That was not TRANSFER. Nothing was offered."));
|
|
523
|
+
return 1;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const offered = await (0, api_js_1.callApi)(`${repositoryRoute(project.id)}/transfer`, { method: "POST", body: { recipient, confirmation: "TRANSFER", keepAccess } });
|
|
527
|
+
console.log((0, colour_js_1.green)(`${offered.displayName} has been offered to ${offered.owner}.`) +
|
|
528
|
+
` It stays yours until they accept` +
|
|
529
|
+
(keepAccess ? "." : ", and you give up your access when they do."));
|
|
530
|
+
console.log((0, colour_js_1.dim)(` The offer lapses on ${new Date(offered.expiresAt).toLocaleDateString()}. Withdraw it: cbx hand-over cancel ${project.slug}`));
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
@@ -6,8 +6,11 @@ const promises_1 = require("node:fs/promises");
|
|
|
6
6
|
const node_readline_1 = require("node:readline");
|
|
7
7
|
const api_js_1 = require("./api.js");
|
|
8
8
|
const config_js_1 = require("./config.js");
|
|
9
|
+
const workspace_machine_js_1 = require("./workspace_machine.js");
|
|
9
10
|
async function commandWorkspace(parsed) {
|
|
10
11
|
const [operation, projectRef, workspace, runId] = parsed.positional;
|
|
12
|
+
if (operation && workspace_machine_js_1.MACHINE_OPERATIONS.has(operation))
|
|
13
|
+
return (0, workspace_machine_js_1.commandWorkspaceMachine)(parsed);
|
|
11
14
|
if (!operation || !projectRef)
|
|
12
15
|
throw new Error("Use cbx workspace <operation> <project> [workspace]. See cbx help workspace.");
|
|
13
16
|
const project = await (0, api_js_1.findProject)(projectRef);
|