dw-mc 0.5.1 → 0.6.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/dist/bin.js CHANGED
@@ -708,6 +708,92 @@ const tidy = Effect.fn("store.tidy")(function* (directory, upTo) {
708
708
  }
709
709
  });
710
710
  //#endregion
711
+ //#region src/adapters/picker.ts
712
+ /**
713
+ * Turns quitting into an answer rather than a failure.
714
+ *
715
+ * Bailing out of a prompt gives `None`, so no caller has to catch an error to
716
+ * learn that I walked away. The prompt itself decides what a `Some` carries.
717
+ */
718
+ const orNone = (prompt) => Effect.catchTag(prompt, "QuitError", () => Effect.succeedNone);
719
+ /**
720
+ * What a prompt looks like in this tool: the marker the rows already use, and
721
+ * the same colour for the choice I am standing on.
722
+ *
723
+ * It is set here rather than at each prompt, because this module is the only
724
+ * thing that opens one and four prompts that themed themselves would be four
725
+ * looks.
726
+ */
727
+ const theme = (paint) => paint === plain ? {
728
+ prefix: "▸",
729
+ pointer: "●"
730
+ } : {
731
+ prefix: "▸",
732
+ pointer: "●",
733
+ primaryColor: "cyan",
734
+ mutedColor: "gray"
735
+ };
736
+ /**
737
+ * What the keyboard does, said under the question.
738
+ *
739
+ * It rides in the message rather than being printed above the prompt, so it
740
+ * leaves with the prompt: a hint that outlives the answer is scrollback I did
741
+ * not ask for.
742
+ */
743
+ const moves = "↑↓ move · enter choose · q quit";
744
+ const asked$1 = (paint, message) => `${message}\n${paint.dim(moves)}`;
745
+ /** Asks which one of `choices` to act on. */
746
+ const pick = (message, choices) => Effect.flatMap(Paint, (paint) => orNone(Effect.asSome(Prompt.Select({
747
+ message: asked$1(paint, message),
748
+ choices,
749
+ theme: theme(paint)
750
+ }))));
751
+ /**
752
+ * Asks which of `choices` to act on, as many as I like.
753
+ *
754
+ * Nothing is selected to begin with, so what reaches the caller is what I
755
+ * picked rather than what I failed to unpick. Quitting is not the same as
756
+ * picking nothing: it gives `None`.
757
+ */
758
+ const choose = (message, choices) => Effect.flatMap(Paint, (paint) => orNone(Effect.asSome(Prompt.MultiSelect({
759
+ message: `${message}\n${paint.dim("↑↓ move · space pick · enter confirm · q quit")}`,
760
+ choices,
761
+ theme: theme(paint)
762
+ }))));
763
+ /**
764
+ * Asks a yes-or-no question about something that cannot be taken back.
765
+ *
766
+ * It starts on no, and walking away is no as well: the answer this returns is
767
+ * the one I typed, and every other way out of the prompt leaves the thing
768
+ * undone. A confirmation that defaulted to yes would be one keystroke, which is
769
+ * exactly what it exists to stop being.
770
+ */
771
+ const confirm = (message) => Effect.flatMap(Paint, (paint) => Effect.map(orNone(Effect.asSome(Prompt.Confirm({
772
+ message,
773
+ initial: false,
774
+ theme: theme(paint)
775
+ }))), Option.getOrElse(() => false)));
776
+ /**
777
+ * Asks for a line of prose, where having nothing to say is the ordinary answer.
778
+ *
779
+ * An empty line is no note, and that is not a failure: the prompt is optional
780
+ * by design. Quitting is the one thing it does not swallow. Ctrl-C part way
781
+ * through a list of notes means I want out of the whole command, and a prompt
782
+ * that turned it into "no note" would walk me through the rest of the list and
783
+ * then act on findings I was no longer sure about.
784
+ */
785
+ const note = (message) => Effect.map(Prompt.String({ message }), (text) => text.trim() === "" ? Option.none() : Option.some(text.trim()));
786
+ /**
787
+ * How wide the screen is, or zero where there is no screen to measure.
788
+ *
789
+ * A prompt has to fit its row on one line: a row that wraps takes the list's
790
+ * alignment with it. Nothing is piping into a prompt, so zero means the writing
791
+ * is going somewhere that does not wrap either.
792
+ */
793
+ const width = Effect.gen(function* () {
794
+ return yield* (yield* Terminal.Terminal).columns;
795
+ });
796
+ //#endregion
711
797
  //#region src/adapters/heartbeat.ts
712
798
  /** The frames of the spinner, in the order they turn. */
713
799
  const frames = [
@@ -1373,92 +1459,6 @@ const prune = Effect.fn("git.prune")(function* (clone) {
1373
1459
  ]));
1374
1460
  });
1375
1461
  //#endregion
1376
- //#region src/adapters/picker.ts
1377
- /**
1378
- * Turns quitting into an answer rather than a failure.
1379
- *
1380
- * Bailing out of a prompt gives `None`, so no caller has to catch an error to
1381
- * learn that I walked away. The prompt itself decides what a `Some` carries.
1382
- */
1383
- const orNone = (prompt) => Effect.catchTag(prompt, "QuitError", () => Effect.succeedNone);
1384
- /**
1385
- * What a prompt looks like in this tool: the marker the rows already use, and
1386
- * the same colour for the choice I am standing on.
1387
- *
1388
- * It is set here rather than at each prompt, because this module is the only
1389
- * thing that opens one and four prompts that themed themselves would be four
1390
- * looks.
1391
- */
1392
- const theme = (paint) => paint === plain ? {
1393
- prefix: "▸",
1394
- pointer: "●"
1395
- } : {
1396
- prefix: "▸",
1397
- pointer: "●",
1398
- primaryColor: "cyan",
1399
- mutedColor: "gray"
1400
- };
1401
- /**
1402
- * What the keyboard does, said under the question.
1403
- *
1404
- * It rides in the message rather than being printed above the prompt, so it
1405
- * leaves with the prompt: a hint that outlives the answer is scrollback I did
1406
- * not ask for.
1407
- */
1408
- const moves = "↑↓ move · enter choose · q quit";
1409
- const asked$1 = (paint, message) => `${message}\n${paint.dim(moves)}`;
1410
- /** Asks which one of `choices` to act on. */
1411
- const pick = (message, choices) => Effect.flatMap(Paint, (paint) => orNone(Effect.asSome(Prompt.Select({
1412
- message: asked$1(paint, message),
1413
- choices,
1414
- theme: theme(paint)
1415
- }))));
1416
- /**
1417
- * Asks which of `choices` to act on, as many as I like.
1418
- *
1419
- * Nothing is selected to begin with, so what reaches the caller is what I
1420
- * picked rather than what I failed to unpick. Quitting is not the same as
1421
- * picking nothing: it gives `None`.
1422
- */
1423
- const choose = (message, choices) => Effect.flatMap(Paint, (paint) => orNone(Effect.asSome(Prompt.MultiSelect({
1424
- message: `${message}\n${paint.dim("↑↓ move · space pick · enter confirm · q quit")}`,
1425
- choices,
1426
- theme: theme(paint)
1427
- }))));
1428
- /**
1429
- * Asks a yes-or-no question about something that cannot be taken back.
1430
- *
1431
- * It starts on no, and walking away is no as well: the answer this returns is
1432
- * the one I typed, and every other way out of the prompt leaves the thing
1433
- * undone. A confirmation that defaulted to yes would be one keystroke, which is
1434
- * exactly what it exists to stop being.
1435
- */
1436
- const confirm = (message) => Effect.flatMap(Paint, (paint) => Effect.map(orNone(Effect.asSome(Prompt.Confirm({
1437
- message,
1438
- initial: false,
1439
- theme: theme(paint)
1440
- }))), Option.getOrElse(() => false)));
1441
- /**
1442
- * Asks for a line of prose, where having nothing to say is the ordinary answer.
1443
- *
1444
- * An empty line is no note, and that is not a failure: the prompt is optional
1445
- * by design. Quitting is the one thing it does not swallow. Ctrl-C part way
1446
- * through a list of notes means I want out of the whole command, and a prompt
1447
- * that turned it into "no note" would walk me through the rest of the list and
1448
- * then act on findings I was no longer sure about.
1449
- */
1450
- const note = (message) => Effect.map(Prompt.String({ message }), (text) => text.trim() === "" ? Option.none() : Option.some(text.trim()));
1451
- /**
1452
- * How wide the screen is, or zero where there is no screen to measure.
1453
- *
1454
- * A prompt has to fit its row on one line: a row that wraps takes the list's
1455
- * alignment with it. Nothing is piping into a prompt, so zero means the writing
1456
- * is going somewhere that does not wrap either.
1457
- */
1458
- const width = Effect.gen(function* () {
1459
- return yield* (yield* Terminal.Terminal).columns;
1460
- });
1461
- //#endregion
1462
1462
  //#region src/cli/table.ts
1463
1463
  /**
1464
1464
  * The rows of a table, padded so the columns line up and with the trailing
@@ -1545,7 +1545,7 @@ const plan = (inventory) => {
1545
1545
  };
1546
1546
  };
1547
1547
  /** What the whole state directory weighs: the clones, the checkouts and the records. */
1548
- const everything = (inventory) => sum([
1548
+ const everything$1 = (inventory) => sum([
1549
1549
  ...inventory.clones.map((it) => it.size),
1550
1550
  ...inventory.cuttings.map((it) => it.size),
1551
1551
  inventory.records.size
@@ -1772,6 +1772,10 @@ const PrView = Schema.fromJsonString(Schema.Struct({
1772
1772
  reviewDecision: Schema.String,
1773
1773
  statusCheckRollup: Schema.NullOr(Schema.Array(CheckEntry))
1774
1774
  }));
1775
+ /**
1776
+ * The fields one `gh pr view` asks for, named so a test can spell the vector it
1777
+ * expects without copying the list and watching it drift.
1778
+ */
1775
1779
  const viewFields = "number,title,url,isDraft,headRefOid,headRefName,baseRefName,author,isCrossRepository,mergeable,reviewDecision,statusCheckRollup";
1776
1780
  /**
1777
1781
  * Everything about one pull request that arrives without paging through it:
@@ -2536,7 +2540,7 @@ const skippedSince = (asked, docsOnly) => {
2536
2540
  return changed === null || worthRerunning(changed, docsOnly) ? null : asked.last.head;
2537
2541
  };
2538
2542
  /** What a run was opened on, as the report says it. */
2539
- const askedOf$1 = (run) => run.command === null ? "the tool's own prompt" : [run.command, run.effort].filter((part) => part !== null).join(" ");
2543
+ const askedOf$2 = (run) => run.command === null ? "the tool's own prompt" : [run.command, run.effort].filter((part) => part !== null).join(" ");
2540
2544
  /**
2541
2545
  * The report as it is written down: what it is of, then what the run said.
2542
2546
  *
@@ -2548,7 +2552,7 @@ const reportDocument = (run, title, prose) => [
2548
2552
  `# ${run.repo}#${run.number} ${title}`,
2549
2553
  "",
2550
2554
  `- head: ${run.head}`,
2551
- `- run: ${askedOf$1(run)}`,
2555
+ `- run: ${askedOf$2(run)}`,
2552
2556
  `- ran: ${DateTime.formatIso(run.ranAt)}`,
2553
2557
  "",
2554
2558
  prose.trim(),
@@ -2811,7 +2815,7 @@ const shown = (threads, options) => {
2811
2815
  };
2812
2816
  //#endregion
2813
2817
  //#region src/cli/comments.ts
2814
- const allFlag = Flag.Boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("Print the whole conversation, including what is resolved, outdated and already answered"));
2818
+ const allFlag$1 = Flag.Boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("Print the whole conversation, including what is resolved, outdated and already answered"));
2815
2819
  /** Where a thread hangs: a line of the diff, or the pull request itself. */
2816
2820
  const where$1 = (thread) => thread.path === null ? "Conversation" : thread.line === null ? thread.path : `${thread.path}:${thread.line}`;
2817
2821
  /**
@@ -2879,7 +2883,7 @@ const nothing$1 = (facts, all) => {
2879
2883
  */
2880
2884
  const comments = Command.make("comments", {
2881
2885
  pr: prArgument,
2882
- all: allFlag
2886
+ all: allFlag$1
2883
2887
  }, Effect.fn("comments")(function* ({ all, pr }) {
2884
2888
  const { number, repo } = yield* forPr(pr);
2885
2889
  const facts = yield* swept(repo, number);
@@ -3942,6 +3946,45 @@ const merge = Command.make("merge", { pr: prArgument }, Effect.fn("merge")(funct
3942
3946
  yield* Console.log(`The squash subject is the pull request title: ${view.title}`);
3943
3947
  }, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Squash-merge a Ready, stamped pull request of mine and delete its branch"));
3944
3948
  //#endregion
3949
+ //#region src/domain/coverage.ts
3950
+ const one = (repo, registered) => ({
3951
+ _tag: "covers",
3952
+ repos: [repo],
3953
+ leftOut: registered.length - 1
3954
+ });
3955
+ /**
3956
+ * The repositories a sweep covers.
3957
+ *
3958
+ * Standing in a registered repository is asking for that one, because that is
3959
+ * the table I want while I work there. Standing anywhere else asks for nothing
3960
+ * in particular, so it gets every registered repository, as it would have
3961
+ * before a sweep could be narrowed at all.
3962
+ *
3963
+ * `here` is the repository the working directory is in, and is only read when
3964
+ * neither flag decides.
3965
+ */
3966
+ const covered = (asked, here, registered) => {
3967
+ if (asked.repo !== void 0 && asked.all) return {
3968
+ _tag: "refused",
3969
+ why: "--repo names one repository and --all asks for every one. Pass one of them."
3970
+ };
3971
+ if (asked.repo !== void 0) {
3972
+ if (registered.includes(asked.repo)) return one(asked.repo, registered);
3973
+ return {
3974
+ _tag: "refused",
3975
+ why: registered.length === 0 ? `${asked.repo} is not registered. Run dw-mc init inside it to register it.` : `${asked.repo} is not registered. Name one of ${registered.join(", ")}, or run dw-mc init inside it.`
3976
+ };
3977
+ }
3978
+ if (!asked.all && here !== void 0 && registered.includes(here)) return one(here, registered);
3979
+ return {
3980
+ _tag: "covers",
3981
+ repos: registered,
3982
+ leftOut: 0
3983
+ };
3984
+ };
3985
+ /** Whether the working directory decides the coverage, which is the one case worth asking `gh` where it is. */
3986
+ const asksWhereIAm = (asked, registered) => asked.repo === void 0 && !asked.all && registered.length > 0;
3987
+ //#endregion
3945
3988
  //#region src/domain/flaky.ts
3946
3989
  /**
3947
3990
  * The failures that are flaky wherever they appear: a machine, a network or a
@@ -4233,6 +4276,13 @@ const recordConflict = Effect.fn("rebase.recordConflict")(function* (repo, numbe
4233
4276
  });
4234
4277
  //#endregion
4235
4278
  //#region src/cli/sweep.ts
4279
+ /** What the picker asks a sweep for: every registered repository, wherever I stand. */
4280
+ const everything = {
4281
+ repo: void 0,
4282
+ all: true
4283
+ };
4284
+ /** The repository I stand in, or none where the working directory is not one. */
4285
+ const whereIAm = currentRepo.pipe(Effect.asSome, Effect.catchTag("NoRepository", () => Effect.succeedNone), Effect.map(Option.getOrUndefined));
4236
4286
  const writtenBy = (comments, login) => comments.filter((comment) => comment.login === login).map((comment) => comment.at);
4237
4287
  const byHumansOtherThan = (comments, login) => comments.filter((comment) => !comment.bot && comment.login !== login).map((comment) => comment.at);
4238
4288
  /**
@@ -4306,7 +4356,8 @@ const saying$1 = (swept) => (since) => [
4306
4356
  since
4307
4357
  ].join(" · ");
4308
4358
  /**
4309
- * One pass over every tracked PR, and nothing else: a sweep only ever reads.
4359
+ * One pass over the tracked PRs of the repositories `asked` covers, and nothing
4360
+ * else: a sweep only ever reads.
4310
4361
  *
4311
4362
  * Every repository and every pull request is read on its own, so one of them
4312
4363
  * failing costs me its rows and leaves the rest of the table standing. What
@@ -4316,11 +4367,15 @@ const saying$1 = (swept) => (since) => [
4316
4367
  * that is worth saying is the caller's, which is why it is handed a count and
4317
4368
  * not a sentence.
4318
4369
  */
4319
- const sweep = Effect.fn("sweep")(function* (report) {
4370
+ const sweep = Effect.fn("sweep")(function* (asked, report) {
4320
4371
  const file = Option.getOrElse(yield* read, () => ({}));
4321
- const repos = Object.keys(file.repos ?? {}).toSorted();
4372
+ const registered = Object.keys(file.repos ?? {}).toSorted();
4373
+ const coverage = covered(asked, asksWhereIAm(asked, registered) ? yield* whereIAm : void 0, registered);
4374
+ if (coverage._tag === "refused") return yield* new CliError.UserError({ cause: coverage.why });
4375
+ const { repos, leftOut } = coverage;
4322
4376
  if (repos.length === 0) return {
4323
4377
  repos,
4378
+ leftOut,
4324
4379
  facts: [],
4325
4380
  troubles: []
4326
4381
  };
@@ -4356,6 +4411,7 @@ const sweep = Effect.fn("sweep")(function* (report) {
4356
4411
  }), { concurrency }));
4357
4412
  return {
4358
4413
  repos,
4414
+ leftOut,
4359
4415
  facts: swept.got,
4360
4416
  troubles: [...found.troubles, ...swept.troubles]
4361
4417
  };
@@ -4367,7 +4423,25 @@ const sweep = Effect.fn("sweep")(function* (report) {
4367
4423
  * three times over. It gives the heartbeat no aside, so a piped `dw-mc status`
4368
4424
  * prints exactly what it printed before there was a heartbeat at all.
4369
4425
  */
4370
- const sweeping = beating((since) => `sweeping · ${since}`, (says) => sweep((swept) => says(saying$1(swept))));
4426
+ const sweeping = (asked) => beating((since) => `sweeping · ${since}`, (says) => sweep(asked, (swept) => says(saying$1(swept))));
4427
+ /** `--repo`, for a command that sweeps: the one registered repository to cover. */
4428
+ const repoFlag = Flag.String("repo").pipe(Flag.withDescription("Cover this registered repository only, as owner/name, wherever I stand"), Flag.optional);
4429
+ /** `--all`, for a command that sweeps: every registered repository, even from inside one. */
4430
+ const allFlag = Flag.Boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("Cover every registered repository, and not only the one I stand in"));
4431
+ /** What the two flags ask a sweep to cover. */
4432
+ const askedOf$1 = (flags) => ({
4433
+ repo: Option.getOrUndefined(flags.repo),
4434
+ all: flags.all
4435
+ });
4436
+ /**
4437
+ * The word a narrowed report owes me about what it left out, so a table of one
4438
+ * repository is not read as the whole picture.
4439
+ */
4440
+ const printLeftOut = Effect.fn("sweep.printLeftOut")(function* (report) {
4441
+ if (report.leftOut === 0) return;
4442
+ yield* Console.log("");
4443
+ yield* Console.log(`Only ${report.repos.join(", ")}. --all covers all ${report.repos.length + report.leftOut} registered repositories.`);
4444
+ });
4371
4445
  /** What a sweep could not read, under a heading, so the table above it stands alone. */
4372
4446
  const printTroubles = Effect.fn("sweep.printTroubles")(function* (troubles) {
4373
4447
  if (troubles.length === 0) return;
@@ -4376,16 +4450,20 @@ const printTroubles = Effect.fn("sweep.printTroubles")(function* (troubles) {
4376
4450
  for (const trouble of troubles) yield* Console.log(` ${trouble.where} ${trouble.detail}`);
4377
4451
  });
4378
4452
  /**
4379
- * Refreshes what mission control knows about every tracked PR.
4453
+ * Refreshes what mission control knows about the tracked PRs it covers.
4380
4454
  *
4381
4455
  * `dw-mc status` does this too, so this command is for the pass on its own:
4382
4456
  * warming the state directory, or seeing what GitHub would not answer.
4383
4457
  */
4384
- const sweepCommand = Command.make("sweep", {}, Effect.fn("sweep.command")(function* () {
4385
- const report = yield* sweeping;
4458
+ const sweepCommand = Command.make("sweep", {
4459
+ repo: repoFlag,
4460
+ all: allFlag
4461
+ }, Effect.fn("sweep.command")(function* (flags) {
4462
+ const report = yield* sweeping(askedOf$1(flags));
4386
4463
  yield* Console.log(report.repos.length === 0 ? "No repositories registered. Run dw-mc init inside a repository to register it." : `Swept ${count(report.facts.length, "pull request")} across ${repositories(report.repos.length)}`);
4464
+ yield* printLeftOut(report);
4387
4465
  yield* printTroubles(report.troubles);
4388
- }, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Refresh what mission control knows about every tracked pull request"));
4466
+ }, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Refresh what mission control knows about the tracked pull requests of the repository I stand in, or of every one"));
4389
4467
  //#endregion
4390
4468
  //#region src/domain/pick.ts
4391
4469
  /**
@@ -4603,7 +4681,7 @@ const where = (facts) => `${facts.repo}#${facts.number}`;
4603
4681
  * and a merge must never cost only that (ADR 0008).
4604
4682
  */
4605
4683
  const picker = (dispatch) => Effect.fn("pick")(function* () {
4606
- const report = yield* sweeping;
4684
+ const report = yield* sweeping(everything);
4607
4685
  if (report.repos.length === 0) {
4608
4686
  yield* Console.log("No repositories registered. Run dw-mc init inside a repository to register it.");
4609
4687
  return;
@@ -5316,8 +5394,11 @@ const lines = (grouped, stamped, paint) => {
5316
5394
  *
5317
5395
  * It sweeps first, every time: a table I read is never one I forgot to refresh.
5318
5396
  */
5319
- const status = Command.make("status", {}, Effect.fn("status")(function* () {
5320
- const report = yield* sweeping;
5397
+ const status = Command.make("status", {
5398
+ repo: repoFlag,
5399
+ all: allFlag
5400
+ }, Effect.fn("status")(function* (flags) {
5401
+ const report = yield* sweeping(askedOf$1(flags));
5321
5402
  if (report.repos.length === 0) {
5322
5403
  yield* Console.log("No repositories registered. Run dw-mc init inside a repository to register it.");
5323
5404
  return;
@@ -5325,8 +5406,9 @@ const status = Command.make("status", {}, Effect.fn("status")(function* () {
5325
5406
  const grouped = group(report.facts);
5326
5407
  if (grouped.length === 0) yield* Console.log("No open pull requests.");
5327
5408
  for (const line of lines(grouped, yield* stampedAmong(report.facts), yield* Paint)) yield* Console.log(line);
5409
+ yield* printLeftOut(report);
5328
5410
  yield* printTroubles(report.troubles);
5329
- }, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Show which bucket every tracked pull request sits in, and which ones I have stamped"));
5411
+ }, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Show which bucket every tracked pull request of the repository I stand in, or of every one, sits in, and which ones I have stamped"));
5330
5412
  //#endregion
5331
5413
  //#region src/cli/uninstall.ts
5332
5414
  const configFlag = Flag.Boolean("config").pipe(Flag.withDefault(false), Flag.withDescription("Take the configuration file too, and not only the state"));
@@ -5382,7 +5464,7 @@ const uninstall = Command.make("uninstall", {
5382
5464
  }] : [])).pipe(Effect.map((found_) => found_.flat()));
5383
5465
  yield* Effect.forEach(removes({
5384
5466
  directory: found.directory,
5385
- size: weight(everything(found))
5467
+ size: weight(everything$1(found))
5386
5468
  }, alsoConfig && configured ? file : void 0, paint), (line) => Console.log(line));
5387
5469
  if (holds.length > 0) {
5388
5470
  yield* Effect.forEach(held(holds, paint), (line) => Console.log(line));
@@ -5409,7 +5491,7 @@ const uninstall = Command.make("uninstall", {
5409
5491
  * Running from source leaves the constant undeclared rather than undefined, so
5410
5492
  * the check has to be `typeof` and the fallback is what a test reads.
5411
5493
  */
5412
- const version = "0.5.1";
5494
+ const version = "0.6.0";
5413
5495
  /** Where the project lives, printed beside the version in the header. */
5414
5496
  const projectUrl = "github.com/dominikwozniak/dw-mc";
5415
5497
  const subcommands = [