@coderook/cli 0.25.4 → 0.27.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,652 @@
1
+ "use strict";
2
+ /**
3
+ * Arranging a project's versions from the terminal.
4
+ *
5
+ * Every one of these things could already be done — from the website, and only
6
+ * from the website. That is the gap this closes: naming worked here, hiding
7
+ * did not, and colouring and pinning existed nowhere. A person who works in a
8
+ * terminal should not have to open a browser to say which version matters.
9
+ *
10
+ * One command does the changing, because the service takes one patch: `cbx
11
+ * version 12 --name v2.1 --hide --pin` is a single request that either happened
12
+ * or did not, rather than three that can half-happen.
13
+ */
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.split = split;
19
+ exports.commandNotes = commandNotes;
20
+ exports.commandVersion = commandVersion;
21
+ exports.commandLabels = commandLabels;
22
+ exports.commandHeld = commandHeld;
23
+ exports.commandReview = commandReview;
24
+ exports.commandTakeDown = commandTakeDown;
25
+ exports.commandPromote = commandPromote;
26
+ exports.commandUndo = commandUndo;
27
+ const api_js_1 = require("./api.js");
28
+ const project_commands_js_1 = require("./project_commands.js");
29
+ const promises_1 = require("node:readline/promises");
30
+ const node_child_process_1 = require("node:child_process");
31
+ const node_fs_1 = require("node:fs");
32
+ const node_os_1 = require("node:os");
33
+ const node_path_1 = __importDefault(require("node:path"));
34
+ const node_process_1 = __importDefault(require("node:process"));
35
+ /* Written out rather than imported: the same four escapes every other command
36
+ file in here declares for itself, and a shared module for four one-line
37
+ functions would be the only thing they all depend on. */
38
+ const dim = (value) => `${value}`;
39
+ const bold = (value) => `${value}`;
40
+ const red = (value) => `${value}`;
41
+ const accent = (value) => `${value}`;
42
+ const green = (value) => `${value}`;
43
+ /**
44
+ * Which of `[n] [project]` the arguments actually were.
45
+ *
46
+ * Both are optional and both are positional, so `cbx undo blog` has to be read
47
+ * as a project and `cbx undo 41` as a save — the shape of the word is the only
48
+ * thing that separates them. A save is digits, optionally with a leading v;
49
+ * anything else is a project name.
50
+ *
51
+ * That leaves a project whose whole name is digits unreachable this way. It
52
+ * still works as `--project 41`, and reading a bare number as a save is the
53
+ * one that comes up.
54
+ */
55
+ function split(parsed) {
56
+ const named = parsed.flags.get("project") ?? parsed.flags.get("p");
57
+ const flagged = typeof named === "string" && named ? named : undefined;
58
+ const [first, second] = parsed.positional;
59
+ if (second !== undefined)
60
+ return { n: first, project: flagged ?? second };
61
+ if (first === undefined)
62
+ return { project: flagged };
63
+ if (flagged)
64
+ return { n: first, project: flagged };
65
+ return /^v?\d+$/i.test(first) ? { n: first } : { project: first };
66
+ }
67
+ /** The version somebody meant, which is usually the newest one. */
68
+ async function pick(repositoryId, wanted) {
69
+ const all = await (0, api_js_1.versions)(repositoryId);
70
+ if (!all.length) {
71
+ console.error(red("This project has no versions yet."));
72
+ return null;
73
+ }
74
+ /*
75
+ The newest by number, not by position.
76
+
77
+ The list arrives newest first today, and relying on that is how three
78
+ callers started reporting a pinned old save as the current one when the
79
+ order changed for a while. Asking for the largest number cannot be broken
80
+ by how the list happens to be sorted.
81
+ */
82
+ if (!wanted) {
83
+ return all.reduce((newest, one) => (one.sequence > newest.sequence ? one : newest));
84
+ }
85
+ const sequence = wanted.replace(/^v/i, "");
86
+ const found = all.find((one) => String(one.sequence) === sequence);
87
+ if (!found) {
88
+ console.error(red(`This project has no version ${wanted}.`));
89
+ return null;
90
+ }
91
+ return found;
92
+ }
93
+ const SWATCH = {
94
+ red: "#e0574a",
95
+ amber: "#e8a13f",
96
+ yellow: "#e8d13f",
97
+ green: "#5fcf8d",
98
+ blue: "#5aa9e6",
99
+ purple: "#a98ae0",
100
+ pink: "#e68ab8",
101
+ grey: "#8c968f",
102
+ gray: "#8c968f",
103
+ };
104
+ /**
105
+ * A colour, given either as a name or as hex.
106
+ *
107
+ * Names because nobody remembers hex, hex because somebody will want their own
108
+ * exact one and being told "pick from these eight" is the kind of small refusal
109
+ * that makes a tool feel like it is arguing.
110
+ */
111
+ function colourOf(given) {
112
+ const value = given.trim().toLowerCase();
113
+ if (SWATCH[value])
114
+ return SWATCH[value];
115
+ const hex = value.startsWith("#") ? value : `#${value}`;
116
+ return /^#[0-9a-f]{6}$/.test(hex) ? hex : null;
117
+ }
118
+ function paint(label) {
119
+ return accent(label.name);
120
+ }
121
+ /**
122
+ * Release info, from wherever the writer keeps it.
123
+ *
124
+ * `--notes "..."` is fine for a sentence and hopeless for anything real: a
125
+ * release note has paragraphs and lists in it, and the shell trick people
126
+ * reach for — `--notes "$(cat NOTES.md)"` — is not a thing in PowerShell or
127
+ * cmd, which is most of this product's users. So the text can come from a
128
+ * file, or from the editor already set up for writing commit messages.
129
+ *
130
+ * Returns undefined when none of them were asked for, which is different from
131
+ * an empty string: one means leave it alone, the other means clear it.
132
+ */
133
+ function notesFrom(parsed) {
134
+ const inline = parsed.flags.get("notes");
135
+ const file = parsed.flags.get("notes-file");
136
+ const edit = parsed.flags.get("edit") === true;
137
+ const chosen = [
138
+ typeof inline === "string",
139
+ typeof file === "string",
140
+ edit,
141
+ ].filter(Boolean).length;
142
+ if (chosen > 1) {
143
+ throw new Error("Use one of --notes, --notes-file or --edit, not several.");
144
+ }
145
+ if (typeof inline === "string")
146
+ return inline;
147
+ if (typeof file === "string") {
148
+ try {
149
+ return (0, node_fs_1.readFileSync)(file, "utf8");
150
+ }
151
+ catch {
152
+ throw new Error(`Could not read ${file}.`);
153
+ }
154
+ }
155
+ /*
156
+ --edit is answered by the caller, which has the existing text to seed the
157
+ editor with. Nothing to report from here.
158
+ */
159
+ return undefined;
160
+ }
161
+ /**
162
+ * Write in the editor this person already uses, the way git does.
163
+ *
164
+ * The comment lines are stripped, so the instructions at the bottom of the
165
+ * file cannot end up published as part of the release. Quitting without
166
+ * saving leaves the notes untouched rather than clearing them, because an
167
+ * empty buffer is far more often a change of mind than an instruction.
168
+ */
169
+ function writeInEditor(existing) {
170
+ const editor = node_process_1.default.env.CODEROOK_EDITOR ??
171
+ node_process_1.default.env.VISUAL ??
172
+ node_process_1.default.env.EDITOR ??
173
+ (node_process_1.default.platform === "win32" ? "notepad" : "nano");
174
+ const directory = (0, node_fs_1.mkdtempSync)(node_path_1.default.join((0, node_os_1.tmpdir)(), "cbx-notes-"));
175
+ const file = node_path_1.default.join(directory, "RELEASE_NOTES.md");
176
+ try {
177
+ (0, node_fs_1.writeFileSync)(file, `${existing}\n\n` +
178
+ "# Write what changed in this version, in Markdown.\n" +
179
+ "# Lines starting with # in the first column are removed.\n" +
180
+ "# Save an empty file to leave the notes as they were.\n", "utf8");
181
+ /*
182
+ No shell. Handing the arguments to one concatenates them into a command
183
+ line without escaping, which makes the editor setting — and the path
184
+ beside it — a place to hide a second command. An editor is a program
185
+ and some arguments, so it is split here and run directly, which also
186
+ means a path with a space in it survives.
187
+ */
188
+ const parts = editor.match(/"[^"]+"|\S+/g) ?? [editor];
189
+ const unquote = (value) => value.replace(/^"|"$/g, "");
190
+ const run = (0, node_child_process_1.spawnSync)(unquote(parts[0] ?? editor), [...parts.slice(1).map(unquote), file], { stdio: "inherit" });
191
+ if (run.status !== 0)
192
+ return null;
193
+ const written = (0, node_fs_1.readFileSync)(file, "utf8")
194
+ .split(/\r?\n/)
195
+ .filter((line) => !/^#\s/.test(line) && line !== "#")
196
+ .join("\n")
197
+ .trim();
198
+ return written ? written : null;
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ finally {
204
+ (0, node_fs_1.rmSync)(directory, { recursive: true, force: true });
205
+ }
206
+ }
207
+ /**
208
+ * Read or write what a version says about itself.
209
+ *
210
+ * The website grew a place to write this and the terminal could only set it
211
+ * as a one-line flag while promoting, and could not read it back at all —
212
+ * `cbx releases` prints the first line and stops. Somebody working here could
213
+ * publish release info but never check what they had published.
214
+ */
215
+ async function commandNotes(parsed) {
216
+ const which = split(parsed);
217
+ const project = await (0, project_commands_js_1.resolveProject)(which.project);
218
+ if (!project)
219
+ return 1;
220
+ const target = await pick(project.id, which.n);
221
+ if (!target)
222
+ return 1;
223
+ if (parsed.flags.get("clear") === true) {
224
+ await (0, api_js_1.changeVersion)(project.id, target.id, { notes: null });
225
+ console.log(green(`Cleared the notes on v${target.sequence}.`));
226
+ return 0;
227
+ }
228
+ let next;
229
+ try {
230
+ next = notesFrom(parsed);
231
+ }
232
+ catch (error) {
233
+ console.error(red(error instanceof Error ? error.message : String(error)));
234
+ return 1;
235
+ }
236
+ if (parsed.flags.get("edit") === true) {
237
+ const written = writeInEditor(target.notes ?? "");
238
+ if (written === null) {
239
+ console.log(dim("Left as it was."));
240
+ return 0;
241
+ }
242
+ next = written;
243
+ }
244
+ if (next === undefined) {
245
+ /* Nothing to write, so this is a read. */
246
+ const text = (target.notes ?? "").trim();
247
+ if (!text) {
248
+ console.log(dim(`v${target.sequence} has no notes.`) +
249
+ dim(" Write some with --edit, --notes-file or --notes"));
250
+ return 0;
251
+ }
252
+ console.log(bold(`v${target.sequence}${target.name ? ` ${target.name}` : ""}`));
253
+ console.log(text);
254
+ return 0;
255
+ }
256
+ const text = (next ?? "").trim();
257
+ await (0, api_js_1.changeVersion)(project.id, target.id, { notes: text ? next : null });
258
+ console.log(green(text
259
+ ? `Wrote the notes on v${target.sequence}.`
260
+ : `Cleared the notes on v${target.sequence}.`));
261
+ return 0;
262
+ }
263
+ async function commandVersion(parsed) {
264
+ const which = split(parsed);
265
+ const project = await (0, project_commands_js_1.resolveProject)(which.project);
266
+ if (!project)
267
+ return 1;
268
+ const target = await pick(project.id, which.n);
269
+ if (!target)
270
+ return 1;
271
+ const patch = {};
272
+ const name = parsed.flags.get("name");
273
+ if (typeof name === "string")
274
+ patch.name = name;
275
+ if (parsed.flags.get("unname") === true)
276
+ patch.name = null;
277
+ try {
278
+ const notes = notesFrom(parsed);
279
+ if (notes !== undefined)
280
+ patch.notes = notes;
281
+ }
282
+ catch (error) {
283
+ console.error(red(error instanceof Error ? error.message : String(error)));
284
+ return 1;
285
+ }
286
+ if (parsed.flags.get("edit") === true) {
287
+ const written = writeInEditor(target.notes ?? "");
288
+ if (written !== null)
289
+ patch.notes = written;
290
+ }
291
+ if (parsed.flags.get("hide") === true)
292
+ patch.visibility = "private";
293
+ if (parsed.flags.get("show") === true)
294
+ patch.visibility = "public";
295
+ const visibility = parsed.flags.get("visibility");
296
+ if (typeof visibility === "string") {
297
+ if (!["private", "public"].includes(visibility)) {
298
+ console.error(red("Visibility is private, unlisted or public."));
299
+ return 1;
300
+ }
301
+ patch.visibility = visibility;
302
+ }
303
+ if (parsed.flags.get("pin") === true)
304
+ patch.pinned = true;
305
+ if (parsed.flags.get("unpin") === true)
306
+ patch.pinned = false;
307
+ const labelNames = parsed.flags.get("labels");
308
+ if (typeof labelNames === "string") {
309
+ const known = await (0, api_js_1.projectLabels)(project.id);
310
+ const wanted = labelNames
311
+ .split(",")
312
+ .map((one) => one.trim())
313
+ .filter(Boolean);
314
+ const ids = [];
315
+ for (const one of wanted) {
316
+ const found = known.find((label) => label.name.toLowerCase() === one.toLowerCase());
317
+ if (!found) {
318
+ console.error(red(`This project has no "${one}" label.`) +
319
+ dim(` Make one with cbx labels add ${one} green`));
320
+ return 1;
321
+ }
322
+ ids.push(found.id);
323
+ }
324
+ patch.labelIds = ids;
325
+ }
326
+ if (parsed.flags.get("clear-labels") === true)
327
+ patch.labelIds = [];
328
+ if (!Object.keys(patch).length) {
329
+ console.error(red("Say what to change.") +
330
+ dim(" --name, --notes, --hide, --show, --visibility, --pin, --labels"));
331
+ return 1;
332
+ }
333
+ try {
334
+ await (0, api_js_1.changeVersion)(project.id, target.id, patch);
335
+ }
336
+ catch (error) {
337
+ console.error(red(error instanceof Error ? error.message : String(error)));
338
+ return 1;
339
+ }
340
+ const said = [];
341
+ if (patch.name !== undefined) {
342
+ said.push(patch.name ? `named ${accent(patch.name)}` : "name removed");
343
+ }
344
+ if (patch.visibility)
345
+ said.push(patch.visibility);
346
+ if (patch.pinned !== undefined)
347
+ said.push(patch.pinned ? "pinned" : "unpinned");
348
+ if (patch.labelIds) {
349
+ said.push(patch.labelIds.length ? "labelled" : "labels cleared");
350
+ }
351
+ console.log(`${bold(`v${target.sequence}`)} ${said.join(", ")}.`);
352
+ return 0;
353
+ }
354
+ async function commandLabels(parsed) {
355
+ const action = parsed.positional[0] ?? "list";
356
+ if (action === "list") {
357
+ const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[1]);
358
+ if (!project)
359
+ return 1;
360
+ const labels = await (0, api_js_1.projectLabels)(project.id);
361
+ if (!labels.length) {
362
+ console.log(dim("No labels yet. Make one with cbx labels add shipped green"));
363
+ return 0;
364
+ }
365
+ console.log(bold(project.name));
366
+ for (const label of labels) {
367
+ console.log(` ${paint(label).padEnd(28)} ${dim(label.colour)}` +
368
+ (label.description ? ` ${dim(label.description)}` : ""));
369
+ }
370
+ return 0;
371
+ }
372
+ if (action === "add") {
373
+ const name = parsed.positional[1];
374
+ const colour = parsed.positional[2] ?? "grey";
375
+ if (!name) {
376
+ console.error(red("Name the label: cbx labels add shipped green"));
377
+ return 1;
378
+ }
379
+ const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[3]);
380
+ if (!project)
381
+ return 1;
382
+ const hex = colourOf(colour);
383
+ if (!hex) {
384
+ console.error(red(`"${colour}" is not a colour.`) +
385
+ dim(` Try one of ${Object.keys(SWATCH).slice(0, 8).join(", ")}, or #rrggbb`));
386
+ return 1;
387
+ }
388
+ try {
389
+ const made = await (0, api_js_1.createProjectLabel)(project.id, name, hex);
390
+ console.log(`Added ${paint(made)} ${dim(made.colour)}.`);
391
+ }
392
+ catch (error) {
393
+ console.error(red(error instanceof Error ? error.message : String(error)));
394
+ return 1;
395
+ }
396
+ return 0;
397
+ }
398
+ if (action === "remove" || action === "rm") {
399
+ const name = parsed.positional[1];
400
+ if (!name) {
401
+ console.error(red("Name the label to remove: cbx labels remove shipped"));
402
+ return 1;
403
+ }
404
+ const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[2]);
405
+ if (!project)
406
+ return 1;
407
+ const labels = await (0, api_js_1.projectLabels)(project.id);
408
+ const found = labels.find((one) => one.name.toLowerCase() === name.toLowerCase());
409
+ if (!found) {
410
+ console.error(red(`This project has no "${name}" label.`));
411
+ return 1;
412
+ }
413
+ await (0, api_js_1.deleteProjectLabel)(project.id, found.id);
414
+ console.log(`Removed ${found.name}. ` +
415
+ dim("Versions that wore it keep everything else about them."));
416
+ return 0;
417
+ }
418
+ console.error(red(`Unknown: cbx labels ${action}`) + dim(" Try list, add or remove"));
419
+ return 1;
420
+ }
421
+ /**
422
+ * The versions a project is holding until somebody says yes.
423
+ *
424
+ * Listed first rather than requiring a version number, because the person
425
+ * running this is usually asking "is there anything waiting for me" rather
426
+ * than acting on one they already know about.
427
+ */
428
+ async function commandHeld(parsed) {
429
+ const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[0]);
430
+ if (!project)
431
+ return 1;
432
+ const waiting = (await (0, api_js_1.versions)(project.id)).filter((one) => one.state === "held");
433
+ if (!waiting.length) {
434
+ console.log(dim("Nothing is waiting for a decision."));
435
+ return 0;
436
+ }
437
+ console.log(bold(`${waiting.length} waiting on ${project.name}`));
438
+ for (const one of waiting) {
439
+ console.log(` ${accent(`v${one.sequence}`).padEnd(16)} ${one.authorName.padEnd(20)} ` +
440
+ dim(one.message));
441
+ }
442
+ console.log(dim(`\n cbx review v${waiting[0].sequence} --approve`));
443
+ return 0;
444
+ }
445
+ async function commandReview(parsed) {
446
+ const which = split(parsed);
447
+ const approve = parsed.flags.get("approve") === true;
448
+ const decline = parsed.flags.get("decline") === true;
449
+ if (approve === decline) {
450
+ console.error(red("Say which: --approve or --decline."));
451
+ return 1;
452
+ }
453
+ const project = await (0, project_commands_js_1.resolveProject)(which.project);
454
+ if (!project)
455
+ return 1;
456
+ const target = await pick(project.id, which.n);
457
+ if (!target)
458
+ return 1;
459
+ if (target.state !== "held") {
460
+ console.error(red(`v${target.sequence} is not waiting for a decision.`));
461
+ return 1;
462
+ }
463
+ const note = parsed.flags.get("note");
464
+ try {
465
+ await (0, api_js_1.reviewVersion)(project.id, target.id, approve ? "approve" : "decline", typeof note === "string" ? note : null);
466
+ }
467
+ catch (error) {
468
+ console.error(red(error instanceof Error ? error.message : String(error)));
469
+ return 1;
470
+ }
471
+ console.log(approve
472
+ ? `${bold(`v${target.sequence}`)} approved. It is now the current version.`
473
+ : `${bold(`v${target.sequence}`)} declined. It stays in the history as a version that was not accepted.`);
474
+ return 0;
475
+ }
476
+ /**
477
+ * Take a version's content down.
478
+ *
479
+ * Asks first, and says exactly what will survive, because this is the one
480
+ * command here that destroys something. The version itself stays — the list
481
+ * keeps a gap saying what went and who removed it — and that distinction is
482
+ * the difference between this and rewriting history.
483
+ */
484
+ async function commandTakeDown(parsed) {
485
+ const which = split(parsed);
486
+ const project = await (0, project_commands_js_1.resolveProject)(which.project);
487
+ if (!project)
488
+ return 1;
489
+ const target = await pick(project.id, which.n);
490
+ if (!target)
491
+ return 1;
492
+ const reason = parsed.flags.get("reason");
493
+ if (parsed.flags.get("yes") !== true) {
494
+ console.log(red(`This removes the files in v${target.sequence}. They do not come back.`));
495
+ console.log(dim(` The version stays in the history as a gap saying it was removed.\n` +
496
+ ` Add --yes when you are sure.`));
497
+ return 1;
498
+ }
499
+ try {
500
+ await (0, api_js_1.removeVersionContent)(project.id, target.id, typeof reason === "string" ? reason : null);
501
+ }
502
+ catch (error) {
503
+ console.error(red(error instanceof Error ? error.message : String(error)));
504
+ return 1;
505
+ }
506
+ console.log(`${bold(`v${target.sequence}`)} taken down.`);
507
+ return 0;
508
+ }
509
+ /**
510
+ * Turning a commit into a version.
511
+ *
512
+ * A project's history is its working history — most of it says "fix that
513
+ * file", and none of that is anybody else's business. A version is a commit
514
+ * somebody decided was worth showing, and this is where that decision gets
515
+ * made: the last ten, pick one, name it.
516
+ *
517
+ * Named rather than numbered on purpose. A version people fetch is "v2.1" or
518
+ * "the one for the client", and inventing a second counter beside the commit
519
+ * numbers would give everybody two numbers to hold and tell them nothing.
520
+ */
521
+ async function commandPromote(parsed) {
522
+ const which = split(parsed);
523
+ const project = await (0, project_commands_js_1.resolveProject)(which.project);
524
+ if (!project)
525
+ return 1;
526
+ const all = await (0, api_js_1.versions)(project.id);
527
+ if (!all.length) {
528
+ console.error(red("This project has nothing saved yet."));
529
+ return 1;
530
+ }
531
+ const named = parsed.flags.get("name");
532
+ const notes = parsed.flags.get("notes");
533
+ /* A number given outright skips the list, for anybody scripting this. */
534
+ const asked = which.n;
535
+ let target = asked
536
+ ? all.find((one) => String(one.sequence) === asked.replace(/^v/i, ""))
537
+ : null;
538
+ if (asked && !target) {
539
+ console.error(red(`This project has no ${asked}.`));
540
+ return 1;
541
+ }
542
+ if (!target) {
543
+ const recent = all.filter((one) => one.state === "verified").slice(0, 10);
544
+ if (!recent.length) {
545
+ console.error(red("Nothing here can be promoted yet."));
546
+ return 1;
547
+ }
548
+ console.log(bold(`Recent commits on ${project.name}`));
549
+ recent.forEach((one, at) => {
550
+ const when = one.createdAt
551
+ ? new Date(one.createdAt).toLocaleString()
552
+ : "";
553
+ const already = one.name ? accent(` → ${one.name}`) : "";
554
+ console.log(` ${String(at + 1).padStart(2)}. ${accent(`v${one.sequence}`).padEnd(16)}` +
555
+ `${when.padEnd(22)}${dim(one.message)}${already}`);
556
+ });
557
+ const prompt = (0, promises_1.createInterface)({
558
+ input: node_process_1.default.stdin,
559
+ output: node_process_1.default.stdout,
560
+ });
561
+ const typed = await prompt.question("\nWhich one? (1-" + recent.length + ", or blank to stop) ");
562
+ prompt.close();
563
+ const choice = Number(typed.trim());
564
+ if (!typed.trim()) {
565
+ console.log(dim("Nothing promoted."));
566
+ return 0;
567
+ }
568
+ if (!Number.isInteger(choice) || choice < 1 || choice > recent.length) {
569
+ console.error(red("That was not one of the numbers listed."));
570
+ return 1;
571
+ }
572
+ target = recent[choice - 1];
573
+ }
574
+ let versionName = typeof named === "string" ? named.trim() : "";
575
+ if (!versionName) {
576
+ const prompt = (0, promises_1.createInterface)({
577
+ input: node_process_1.default.stdin,
578
+ output: node_process_1.default.stdout,
579
+ });
580
+ const typed = await prompt.question(`Call it what? (blank for v${target.sequence}.0) `);
581
+ prompt.close();
582
+ versionName = typed.trim() || `v${target.sequence}.0`;
583
+ }
584
+ try {
585
+ await (0, api_js_1.changeVersion)(project.id, target.id, {
586
+ name: versionName,
587
+ ...(typeof notes === "string" ? { notes } : {}),
588
+ });
589
+ }
590
+ catch (error) {
591
+ console.error(red(error instanceof Error ? error.message : String(error)));
592
+ return 1;
593
+ }
594
+ console.log(`${bold(versionName)} is now a version. ` +
595
+ dim("It is what the public side of this project offers."));
596
+ return 0;
597
+ }
598
+ /**
599
+ * Putting the project back where it was.
600
+ *
601
+ * Hiding a bad push stops strangers reading it and leaves everybody on the
602
+ * team standing on it. This moves the project itself, which is the half that
603
+ * was missing — and it destroys nothing: the commits that get passed over stay
604
+ * in the history with their numbers, and a later save carries on from here.
605
+ */
606
+ async function commandUndo(parsed) {
607
+ const which = split(parsed);
608
+ const project = await (0, project_commands_js_1.resolveProject)(which.project);
609
+ if (!project)
610
+ return 1;
611
+ const all = await (0, api_js_1.versions)(project.id);
612
+ if (all.length < 2) {
613
+ console.error(red("There is nothing before this to go back to."));
614
+ return 1;
615
+ }
616
+ let to = null;
617
+ const asked = which.n;
618
+ if (asked) {
619
+ const wanted = all.find((one) => String(one.sequence) === asked.replace(/^v/i, ""));
620
+ if (!wanted) {
621
+ console.error(red(`This project has no ${asked}.`));
622
+ return 1;
623
+ }
624
+ to = wanted.id;
625
+ }
626
+ try {
627
+ const undone = await (0, api_js_1.undoTo)(project.id, to);
628
+ console.log(`${bold(project.name)} is back on ${accent(`v${undone.to.sequence}`)}.`);
629
+ if (undone.skipped.length) {
630
+ /*
631
+ Named rather than counted. "3 commits passed over" tells somebody
632
+ nothing they can act on; the numbers let them look at one, or promote
633
+ one, or put the head back where it was.
634
+ */
635
+ console.log(dim(` Passed over: ${undone.skipped
636
+ .map((one) => `v${one.sequence}${one.name ? ` (${one.name})` : ""}`)
637
+ .join(", ")}`));
638
+ console.log(dim(" They are still here. Nothing was deleted and nothing renumbered."));
639
+ const stillPublic = undone.skipped.filter((one) => one.name);
640
+ if (stillPublic.length) {
641
+ console.log(dim(` ${stillPublic.map((one) => one.name).join(", ")} ` +
642
+ `${stillPublic.length === 1 ? "is" : "are"} still a published version — ` +
643
+ `use cbx mark to take ${stillPublic.length === 1 ? "it" : "them"} down.`));
644
+ }
645
+ }
646
+ }
647
+ catch (error) {
648
+ console.error(red(error instanceof Error ? error.message : String(error)));
649
+ return 1;
650
+ }
651
+ return 0;
652
+ }