@coderook/cli 0.23.0 → 0.25.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 +378 -220
- package/dist/cli/src/api.js +41 -2
- package/dist/cli/src/cli.js +271 -78
- package/dist/cli/src/forge_url.js +68 -0
- package/dist/cli/src/git_history.js +188 -0
- package/dist/cli/src/git_remote.js +1217 -0
- package/dist/cli/src/git_remote_bin.js +22 -0
- package/dist/cli/src/help.js +8 -8
- package/dist/cli/src/licence_commands.js +5 -5
- package/dist/cli/src/mcp.js +1 -1
- package/dist/cli/src/publish.js +37 -0
- package/dist/cli/src/registry.js +1 -1
- package/dist/cli/src/runner.js +2 -2
- package/dist/cli/src/service_commands.js +53 -3
- package/dist/cli/src/skill_command.js +3 -3
- package/dist/cli/src/track_commands.js +7 -7
- package/dist/cli/src/transfer_command.js +331 -0
- package/dist/desktop-app/src/main/tracks.js +12 -2
- package/package.json +7 -4
- package/skills/coderook/SKILL.md +130 -130
package/dist/cli/src/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* cbx — the CodeBox engine from the command line, talking to CodeRook.
|
|
9
9
|
*
|
|
10
10
|
* The same engines the desktop application uses — the same scanner, the same
|
|
11
11
|
* ignore rules, the same upload and download — with a terminal in front of
|
|
@@ -24,8 +24,11 @@ const api_js_1 = require("./api.js");
|
|
|
24
24
|
const registry_js_1 = require("./registry.js");
|
|
25
25
|
const help_js_1 = require("./help.js");
|
|
26
26
|
const progress_js_1 = require("./progress.js");
|
|
27
|
+
const publish_js_1 = require("./publish.js");
|
|
27
28
|
const project_commands_js_1 = require("./project_commands.js");
|
|
28
29
|
const track_commands_js_1 = require("./track_commands.js");
|
|
30
|
+
const tracks_js_1 = require("../../desktop-app/src/main/tracks.js");
|
|
31
|
+
const transfer_command_js_1 = require("./transfer_command.js");
|
|
29
32
|
const mcp_js_1 = require("./mcp.js");
|
|
30
33
|
const skill_command_js_1 = require("./skill_command.js");
|
|
31
34
|
const licence_commands_js_1 = require("./licence_commands.js");
|
|
@@ -43,7 +46,7 @@ const runner_js_1 = require("./runner.js");
|
|
|
43
46
|
const config_js_1 = require("./config.js");
|
|
44
47
|
/*
|
|
45
48
|
Read from the package rather than written twice. A hardcoded copy had
|
|
46
|
-
already drifted from the published version, which makes `
|
|
49
|
+
already drifted from the published version, which makes `cbx doctor`
|
|
47
50
|
worse than useless when working out what somebody is actually running.
|
|
48
51
|
*/
|
|
49
52
|
const VERSION = (() => {
|
|
@@ -297,7 +300,7 @@ async function commandStatus(parsed) {
|
|
|
297
300
|
if (stale.length) {
|
|
298
301
|
console.log(dim(`${stale.length} file${stale.length === 1 ? "" : "s"} in the saved version ` +
|
|
299
302
|
`${stale.length === 1 ? "is" : "are"} newer than the cop${stale.length === 1 ? "y" : "ies"} here ` +
|
|
300
|
-
`— run ${accent("
|
|
303
|
+
`— run ${accent("cbx get")} to bring ${stale.length === 1 ? "it" : "them"} down.`));
|
|
301
304
|
for (const [path] of stale.slice(0, 10))
|
|
302
305
|
console.log(dim(` behind ${path}`));
|
|
303
306
|
}
|
|
@@ -328,7 +331,7 @@ async function commandImport(parsed) {
|
|
|
328
331
|
if (!url) {
|
|
329
332
|
console.error(red("Nothing to import from."));
|
|
330
333
|
console.error("Give the address of a repository, for example:");
|
|
331
|
-
console.error(` ${accent("
|
|
334
|
+
console.error(` ${accent("cbx import https://github.com/owner/project")}`);
|
|
332
335
|
return 1;
|
|
333
336
|
}
|
|
334
337
|
if (!(0, import_command_js_1.looksLikeRepositoryUrl)(url)) {
|
|
@@ -408,7 +411,7 @@ async function commandImport(parsed) {
|
|
|
408
411
|
if (code === 0 && plan.temporary) {
|
|
409
412
|
console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
|
|
410
413
|
` +
|
|
411
|
-
`Run ${accent("
|
|
414
|
+
`Run ${accent("cbx get " + plan.name)} anywhere to fetch it fresh.`));
|
|
412
415
|
}
|
|
413
416
|
return code;
|
|
414
417
|
}
|
|
@@ -455,7 +458,23 @@ async function commandSubmit(parsed) {
|
|
|
455
458
|
const rules = importing
|
|
456
459
|
? { shared: "", local: "" }
|
|
457
460
|
: await (0, worktree_js_1.readRules)(folder);
|
|
458
|
-
|
|
461
|
+
/*
|
|
462
|
+
Whether a file that is no longer here means "delete it".
|
|
463
|
+
|
|
464
|
+
The default says no, and that is the right default: a missing path is
|
|
465
|
+
usually an unmounted drive, a folder moved while something was open, or a
|
|
466
|
+
scan that ran mid-copy — and the cost of guessing wrong is somebody's work
|
|
467
|
+
removed from the one place it was safe. So the saved copy is left alone
|
|
468
|
+
(docs/UPLOAD_POLICY.md).
|
|
469
|
+
|
|
470
|
+
It also meant nothing on this side could ever remove a file. Publishing
|
|
471
|
+
from git can, because there the question does not arise: `git rm` is a
|
|
472
|
+
recorded act, so the deletion is known rather than inferred. That left the
|
|
473
|
+
remote helper as the only route to something the tool itself could not do,
|
|
474
|
+
which is the wrong way round — so this is that capability, asked for
|
|
475
|
+
plainly.
|
|
476
|
+
*/
|
|
477
|
+
const files = await (0, worktree_js_1.changedFiles)(folder, rules, baseline, hasFlag(parsed, "sync") ? "synchronize" : "add-and-update");
|
|
459
478
|
/*
|
|
460
479
|
An upgraded folder with no materialisation record and something that
|
|
461
480
|
differs from its recorded version is ambiguous in the one way that
|
|
@@ -478,8 +497,8 @@ async function commandSubmit(parsed) {
|
|
|
478
497
|
console.error(`${files.length} file${files.length === 1 ? " differs" : "s differ"} here:`);
|
|
479
498
|
for (const file of files.slice(0, 20))
|
|
480
499
|
console.error(` ${file.path}`);
|
|
481
|
-
console.error(`\nIf that is your work, send it with ${accent("
|
|
482
|
-
`\nIf it is an old copy, take theirs with ${accent("
|
|
500
|
+
console.error(`\nIf that is your work, send it with ${accent("cbx submit --force")}.` +
|
|
501
|
+
`\nIf it is an old copy, take theirs with ${accent("cbx get --replace")}.`);
|
|
483
502
|
return 1;
|
|
484
503
|
}
|
|
485
504
|
}
|
|
@@ -493,7 +512,7 @@ async function commandSubmit(parsed) {
|
|
|
493
512
|
console.log(behindOn
|
|
494
513
|
? `Nothing to submit; your own work is all saved. ${behindOn} file` +
|
|
495
514
|
`${behindOn === 1 ? "" : "s"} here ${behindOn === 1 ? "is" : "are"} ` +
|
|
496
|
-
`behind the saved version — run ${accent("
|
|
515
|
+
`behind the saved version — run ${accent("cbx get")}.`
|
|
497
516
|
: "Nothing to submit; this folder matches the saved version.");
|
|
498
517
|
return 0;
|
|
499
518
|
}
|
|
@@ -591,18 +610,23 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
591
610
|
if (added) {
|
|
592
611
|
console.log(`${accent("Added an MIT licence")}, so other people may use this.`);
|
|
593
612
|
console.log(dim(` Change it with `) +
|
|
594
|
-
accent("
|
|
613
|
+
accent("cbx licence <name>") +
|
|
595
614
|
dim(`, or remove it with `) +
|
|
596
|
-
accent("
|
|
615
|
+
accent("cbx licence none") +
|
|
597
616
|
dim("."));
|
|
598
617
|
console.log("");
|
|
599
618
|
}
|
|
600
619
|
const line = progressLine();
|
|
601
620
|
const uploader = new upload_js_1.Uploader(config_js_1.credentials);
|
|
602
|
-
|
|
621
|
+
/*
|
|
622
|
+
Built by the shared description, so this and the git remote helper cannot
|
|
623
|
+
drift. They already had: the helper listed deletions in `deletions` alone
|
|
624
|
+
and published versions that still held the deleted file.
|
|
625
|
+
*/
|
|
626
|
+
const uploadRequest = (0, publish_js_1.uploadRequestFor)({
|
|
603
627
|
localPath: folder,
|
|
604
|
-
|
|
605
|
-
|
|
628
|
+
changed: files.filter((file) => !file.deleted).map((file) => file.path),
|
|
629
|
+
deleted: files.filter((file) => file.deleted).map((file) => file.path),
|
|
606
630
|
message,
|
|
607
631
|
/*
|
|
608
632
|
A folder's name is the right default and the wrong answer for import,
|
|
@@ -625,11 +649,8 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
625
649
|
would reject the publication and the import would be lossy.
|
|
626
650
|
*/
|
|
627
651
|
...(hasFlag(parsed, "allow-ignored") ? { allowIgnored: true } : {}),
|
|
628
|
-
...(link?.baseVersionId
|
|
629
|
-
? { expectedHeadVersionId: link.baseVersionId }
|
|
630
|
-
: {}),
|
|
631
652
|
...(link ? { known: link.local ?? link.manifest ?? {} } : {}),
|
|
632
|
-
};
|
|
653
|
+
});
|
|
633
654
|
let result;
|
|
634
655
|
try {
|
|
635
656
|
const plan = await uploader.plan(uploadRequest, (progress) => {
|
|
@@ -660,23 +681,22 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
660
681
|
}
|
|
661
682
|
catch (error) {
|
|
662
683
|
done(line);
|
|
663
|
-
const code = error.code;
|
|
664
|
-
const text = error instanceof Error ? error.message : String(error);
|
|
665
684
|
/*
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
difference between a person retrying and a person wondering.
|
|
685
|
+
Classified by the shared description rather than by a private regular
|
|
686
|
+
expression, so a push and a submit disagree about nothing — including
|
|
687
|
+
what counts as "the connection failed" and what that means for whether
|
|
688
|
+
the work was saved.
|
|
671
689
|
*/
|
|
672
|
-
|
|
690
|
+
const failure = (0, publish_js_1.classifyPublishFailure)(error);
|
|
691
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
692
|
+
if (failure?.kind === "interrupted") {
|
|
673
693
|
console.error(red(`
|
|
674
694
|
The connection failed: ${text}`));
|
|
675
695
|
console.error(`Your work may already have been saved. Run the same command again —` +
|
|
676
696
|
` it will not create a second version.`);
|
|
677
697
|
return 1;
|
|
678
698
|
}
|
|
679
|
-
if (
|
|
699
|
+
if (failure?.kind === "conflict") {
|
|
680
700
|
/*
|
|
681
701
|
Somebody else saved to this project first. Everything that did not
|
|
682
702
|
overlap has already been combined by the service; what is left is a
|
|
@@ -684,7 +704,7 @@ The connection failed: ${text}`));
|
|
|
684
704
|
at it rather than to try again harder.
|
|
685
705
|
*/
|
|
686
706
|
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
687
|
-
console.error(`\nRun ${accent("
|
|
707
|
+
console.error(`\nRun ${accent("cbx get")} to bring the latest version down, then` +
|
|
688
708
|
` submit again. Nothing was changed on your account.`);
|
|
689
709
|
return 1;
|
|
690
710
|
}
|
|
@@ -704,8 +724,8 @@ The connection failed: ${text}`));
|
|
|
704
724
|
for (const conflict of result.mergeTrack.conflicts.slice(0, 20)) {
|
|
705
725
|
console.log(` ${red(conflict.path)} ${dim(conflict.kind)}`);
|
|
706
726
|
}
|
|
707
|
-
console.log(`\nRun ${accent(`
|
|
708
|
-
` or ${accent("
|
|
727
|
+
console.log(`\nRun ${accent(`cbx merge ${result.mergeTrack.reference}`)} to decide,` +
|
|
728
|
+
` or ${accent("cbx merges")} to see everything waiting.`);
|
|
709
729
|
return 0;
|
|
710
730
|
}
|
|
711
731
|
/*
|
|
@@ -714,9 +734,29 @@ The connection failed: ${text}`));
|
|
|
714
734
|
attempt name exists to make survivable.
|
|
715
735
|
*/
|
|
716
736
|
(0, faults_js_1.maybeFail)("submit:after-commit");
|
|
737
|
+
/*
|
|
738
|
+
What the project is actually called, not what this folder is called.
|
|
739
|
+
|
|
740
|
+
A first publish recorded the folder's own name, which is right only when
|
|
741
|
+
nothing renamed the project — and two things routinely do: `--name`, and
|
|
742
|
+
the service turning a display name into a slug. When they differ, every
|
|
743
|
+
command that finds the project through this folder looks up a name that
|
|
744
|
+
does not exist: `tracks`, `versions`, `issues`, `releases`, `people`,
|
|
745
|
+
`watch`, `delete` and the rest all answered "No project matching …" in a
|
|
746
|
+
folder that had just published to it successfully.
|
|
747
|
+
|
|
748
|
+
Asked of the service rather than guessed, and only on a first publish,
|
|
749
|
+
because that is the only time this is not already known.
|
|
750
|
+
*/
|
|
751
|
+
let recordedSlug = link?.slug;
|
|
752
|
+
if (!recordedSlug) {
|
|
753
|
+
recordedSlug = await (0, api_js_2.projects)()
|
|
754
|
+
.then((all) => all.find((candidate) => candidate.id === result.repositoryId)?.slug)
|
|
755
|
+
.catch(() => undefined);
|
|
756
|
+
}
|
|
717
757
|
await (0, config_js_1.writeLink)(folder, {
|
|
718
758
|
repositoryId: result.repositoryId,
|
|
719
|
-
slug:
|
|
759
|
+
slug: recordedSlug ?? uploadRequest.projectName ?? node_path_1.default.basename(folder),
|
|
720
760
|
sequence: result.sequence,
|
|
721
761
|
// What this folder is now working from, so the next submit can say so.
|
|
722
762
|
versionId: result.versionId,
|
|
@@ -815,8 +855,8 @@ async function commandGet(parsed) {
|
|
|
815
855
|
console.error(red("This folder was connected by an older version of CodeRook,"));
|
|
816
856
|
console.error("which did not record what it had received, so changed files here");
|
|
817
857
|
console.error("cannot be told apart from copies left behind by a merge.\n");
|
|
818
|
-
console.error(`Save them with ${accent("
|
|
819
|
-
`take the\nsaved version exactly with ${accent("
|
|
858
|
+
console.error(`Save them with ${accent("cbx submit")} if they are your work, or ` +
|
|
859
|
+
`take the\nsaved version exactly with ${accent("cbx get --replace")}.`);
|
|
820
860
|
return 1;
|
|
821
861
|
}
|
|
822
862
|
}
|
|
@@ -846,15 +886,15 @@ async function commandGet(parsed) {
|
|
|
846
886
|
incoming.has(file.path) &&
|
|
847
887
|
edited.some((one) => one.path === file.path));
|
|
848
888
|
if (!changedInTheGap.length) {
|
|
849
|
-
return fetchInto(link.repositoryId, folder, link.slug, link.local ?? null, "replace");
|
|
889
|
+
return fetchInto(link.repositoryId, folder, link.slug, link.local ?? null, "replace", await (0, track_commands_js_1.trackFor)(folder));
|
|
850
890
|
}
|
|
851
891
|
console.error(red(`${changedInTheGap.length} file${changedInTheGap.length === 1 ? "" : "s"} ` +
|
|
852
892
|
`changed while the interrupted fetch was stopped:`));
|
|
853
893
|
for (const file of changedInTheGap.slice(0, 20))
|
|
854
894
|
console.error(` ${file.path}`);
|
|
855
895
|
console.error(`
|
|
856
|
-
Save them with ${accent("
|
|
857
|
-
`discard them with ${accent("
|
|
896
|
+
Save them with ${accent("cbx submit")}, or finish the fetch and ` +
|
|
897
|
+
`discard them with ${accent("cbx get --replace")}.`);
|
|
858
898
|
return 1;
|
|
859
899
|
}
|
|
860
900
|
/*
|
|
@@ -874,8 +914,8 @@ Save them with ${accent("coderook submit")}, or finish the fetch and ` +
|
|
|
874
914
|
console.error(` ${file.path}`);
|
|
875
915
|
if (atRisk.length > 20)
|
|
876
916
|
console.error(dim(` …and ${atRisk.length - 20} more`));
|
|
877
|
-
console.error(`\nSave them first with ${accent("
|
|
878
|
-
`with ${accent("
|
|
917
|
+
console.error(`\nSave them first with ${accent("cbx submit")}, or discard them ` +
|
|
918
|
+
`with ${accent("cbx get --replace")}.`);
|
|
879
919
|
return 1;
|
|
880
920
|
}
|
|
881
921
|
}
|
|
@@ -885,12 +925,14 @@ Save them with ${accent("coderook submit")}, or finish the fetch and ` +
|
|
|
885
925
|
what the version actually changed is written over — unless somebody asks
|
|
886
926
|
for an exact copy, which is how a damaged folder is repaired.
|
|
887
927
|
*/
|
|
888
|
-
return fetchInto(link.repositoryId, folder, link.slug, link.local ?? null, replacing ? "replace" : "reconcile"
|
|
928
|
+
return fetchInto(link.repositoryId, folder, link.slug, link.local ?? null, replacing ? "replace" : "reconcile",
|
|
929
|
+
// The line this folder saves to, so catching up brings that line down.
|
|
930
|
+
await (0, track_commands_js_1.trackFor)(folder));
|
|
889
931
|
}
|
|
890
932
|
async function commandClone(parsed) {
|
|
891
933
|
const reference = parsed.positional[0];
|
|
892
934
|
if (!reference) {
|
|
893
|
-
console.error(red("Which project? Try:
|
|
935
|
+
console.error(red("Which project? Try: cbx clone <project>"));
|
|
894
936
|
return 1;
|
|
895
937
|
}
|
|
896
938
|
const project = await (0, api_js_2.findProject)(reference);
|
|
@@ -903,7 +945,34 @@ async function commandClone(parsed) {
|
|
|
903
945
|
return 1;
|
|
904
946
|
}
|
|
905
947
|
const destination = node_path_1.default.resolve(parsed.positional[1] ?? project.slug);
|
|
906
|
-
|
|
948
|
+
/*
|
|
949
|
+
A clone opens on the project's default line, or on the one named.
|
|
950
|
+
|
|
951
|
+
It used to take whichever version was newest across every line, which is
|
|
952
|
+
only the same thing while a project has one. As soon as somebody pushed to
|
|
953
|
+
a branch, that branch held the newest version — so cloning the project
|
|
954
|
+
handed you their unfinished work under the impression it was the project.
|
|
955
|
+
Nothing said so: the fetch reported a version number and the files looked
|
|
956
|
+
like a project, just not the one anybody asked for.
|
|
957
|
+
|
|
958
|
+
Naming a line here also removes the clone-switch-fetch dance that was the
|
|
959
|
+
only way to get a branch before, which is what exposed the same fault in
|
|
960
|
+
`get`.
|
|
961
|
+
*/
|
|
962
|
+
const wanted = flagText(parsed, "track") ?? project.defaultBranch;
|
|
963
|
+
/*
|
|
964
|
+
Falling back rather than failing when the default line does not exist.
|
|
965
|
+
`defaultBranch` is a property of the project and a line can be renamed out
|
|
966
|
+
from under it; refusing to clone at all in that case would be a worse
|
|
967
|
+
answer than the old behaviour. An explicitly named line is still refused,
|
|
968
|
+
because there the person said which one they meant.
|
|
969
|
+
*/
|
|
970
|
+
const lines = await new tracks_js_1.Tracks(config_js_1.credentials).list(project.id);
|
|
971
|
+
const usable = lines.some((line) => line.kind === "line" && line.name === wanted && line.headVersionId);
|
|
972
|
+
if (!usable && !flagText(parsed, "track")) {
|
|
973
|
+
console.log(dim(`This project has no line called ${wanted}; taking its newest version.`));
|
|
974
|
+
}
|
|
975
|
+
return fetchInto(project.id, destination, project.slug, null, "replace", usable || flagText(parsed, "track") ? wanted : undefined);
|
|
907
976
|
}
|
|
908
977
|
async function fetchInto(repositoryId, destination, slug,
|
|
909
978
|
/*
|
|
@@ -912,9 +981,45 @@ async function fetchInto(repositoryId, destination, slug,
|
|
|
912
981
|
is removed, because "missing from the version" cannot then be told from
|
|
913
982
|
"never came from the version at all".
|
|
914
983
|
*/
|
|
915
|
-
held, mode = "replace"
|
|
984
|
+
held, mode = "replace",
|
|
985
|
+
/*
|
|
986
|
+
The line to fetch. Omitted means the project's newest version, which is
|
|
987
|
+
right for a clone and wrong for a folder that has chosen a line.
|
|
988
|
+
*/
|
|
989
|
+
trackName) {
|
|
916
990
|
const downloader = new download_js_1.Downloader(config_js_1.credentials);
|
|
917
|
-
const
|
|
991
|
+
const all = await downloader.versions(repositoryId);
|
|
992
|
+
/*
|
|
993
|
+
The head of this folder's line, not the newest version in the project.
|
|
994
|
+
|
|
995
|
+
Those are the same thing only while a project has one line. With two, the
|
|
996
|
+
newest version usually belongs to the *other* one — so switching to a line
|
|
997
|
+
and fetching brought down the line you had just left, reported success,
|
|
998
|
+
and left the folder holding the wrong contents while still claiming to
|
|
999
|
+
save to the line you asked for. The next publish would then be computed
|
|
1000
|
+
against a baseline from somewhere else.
|
|
1001
|
+
|
|
1002
|
+
Worse than a wrong answer: `track` ends by telling you to run `get`, so
|
|
1003
|
+
the command that set this up was the one recommending it.
|
|
1004
|
+
*/
|
|
1005
|
+
let latest = all[0];
|
|
1006
|
+
if (trackName) {
|
|
1007
|
+
const chosen = (await new tracks_js_1.Tracks(config_js_1.credentials).list(repositoryId)).find((candidate) => candidate.name === trackName && candidate.kind === "line");
|
|
1008
|
+
if (!chosen) {
|
|
1009
|
+
console.error(red(`This project has no line called ${trackName}.`));
|
|
1010
|
+
return 1;
|
|
1011
|
+
}
|
|
1012
|
+
if (!chosen.headVersionId) {
|
|
1013
|
+
console.error(red(`The line ${trackName} has nothing saved to it yet.`));
|
|
1014
|
+
return 1;
|
|
1015
|
+
}
|
|
1016
|
+
const head = all.find((version) => version.id === chosen.headVersionId);
|
|
1017
|
+
if (!head) {
|
|
1018
|
+
console.error(red(`Could not read the current version of ${trackName}.`));
|
|
1019
|
+
return 1;
|
|
1020
|
+
}
|
|
1021
|
+
latest = head;
|
|
1022
|
+
}
|
|
918
1023
|
if (!latest) {
|
|
919
1024
|
console.error(red("That project has no saved version."));
|
|
920
1025
|
return 1;
|
|
@@ -953,6 +1058,16 @@ held, mode = "replace") {
|
|
|
953
1058
|
// version holds and the two agree again.
|
|
954
1059
|
local: result.manifest,
|
|
955
1060
|
manifest: result.manifest,
|
|
1061
|
+
/*
|
|
1062
|
+
The line this folder is on survives being caught up.
|
|
1063
|
+
|
|
1064
|
+
This record is built fresh rather than merged, so anything not named
|
|
1065
|
+
here is dropped — and `track` was not named. Fetching therefore moved
|
|
1066
|
+
the folder back to `main` every time, silently: the files were right,
|
|
1067
|
+
the folder reported the wrong line, and the next publish went somewhere
|
|
1068
|
+
nobody chose.
|
|
1069
|
+
*/
|
|
1070
|
+
...(trackName ? { track: trackName } : {}),
|
|
956
1071
|
// Reached the end, so there is no longer a fetch in flight.
|
|
957
1072
|
});
|
|
958
1073
|
// The record is written; the run has not reported success yet.
|
|
@@ -994,7 +1109,7 @@ async function suggestRules(folder, apply) {
|
|
|
994
1109
|
const recommended = suggestions.filter((one) => one.recommended);
|
|
995
1110
|
if (!apply) {
|
|
996
1111
|
console.log("");
|
|
997
|
-
console.log(dim("Add the + ones with ") + accent("
|
|
1112
|
+
console.log(dim("Add the + ones with ") + accent("cbx ignore --suggest --apply"));
|
|
998
1113
|
return 0;
|
|
999
1114
|
}
|
|
1000
1115
|
if (!recommended.length) {
|
|
@@ -1056,7 +1171,7 @@ async function commandBundle(parsed) {
|
|
|
1056
1171
|
async function commandUnbundle(parsed) {
|
|
1057
1172
|
const source = parsed.positional[0];
|
|
1058
1173
|
if (!source) {
|
|
1059
|
-
console.error(red("Which bundle? Try:
|
|
1174
|
+
console.error(red("Which bundle? Try: cbx unbundle <file.cbx> [folder]"));
|
|
1060
1175
|
return 1;
|
|
1061
1176
|
}
|
|
1062
1177
|
const manifest = await (0, cbx_js_1.readManifest)(node_path_1.default.resolve(source));
|
|
@@ -1073,7 +1188,7 @@ async function commandUnbundle(parsed) {
|
|
|
1073
1188
|
async function commandInspect(parsed) {
|
|
1074
1189
|
const source = parsed.positional[0];
|
|
1075
1190
|
if (!source) {
|
|
1076
|
-
console.error(red("Which bundle? Try:
|
|
1191
|
+
console.error(red("Which bundle? Try: cbx inspect <file.cbx>"));
|
|
1077
1192
|
return 1;
|
|
1078
1193
|
}
|
|
1079
1194
|
const manifest = await (0, cbx_js_1.readManifest)(node_path_1.default.resolve(source));
|
|
@@ -1090,7 +1205,7 @@ async function commandInspect(parsed) {
|
|
|
1090
1205
|
}
|
|
1091
1206
|
async function commandDoctor() {
|
|
1092
1207
|
const service = await (0, api_js_2.health)().catch(() => null);
|
|
1093
|
-
console.log(`
|
|
1208
|
+
console.log(`cbx ${VERSION} · Node ${node_process_1.default.versions.node} · ${node_process_1.default.platform}`);
|
|
1094
1209
|
console.log(`Config: ${(0, config_js_1.configDirectory)()}`);
|
|
1095
1210
|
console.log(service
|
|
1096
1211
|
? `Service: ${service.status} · schema ${service.schemaVersion}` +
|
|
@@ -1134,7 +1249,7 @@ async function commandMerges(parsed) {
|
|
|
1134
1249
|
console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
|
|
1135
1250
|
}
|
|
1136
1251
|
console.log(dim(`
|
|
1137
|
-
Run
|
|
1252
|
+
Run cbx merge <reference> to look at one.`));
|
|
1138
1253
|
return 0;
|
|
1139
1254
|
}
|
|
1140
1255
|
/** Find a merge by the reference a person would type, such as M-2. */
|
|
@@ -1157,7 +1272,7 @@ async function findMerge(folder, reference) {
|
|
|
1157
1272
|
async function commandMerge(parsed) {
|
|
1158
1273
|
const reference = parsed.positional[0];
|
|
1159
1274
|
if (!reference) {
|
|
1160
|
-
console.error(red("Which merge? Try:
|
|
1275
|
+
console.error(red("Which merge? Try: cbx merge M-1"));
|
|
1161
1276
|
return 1;
|
|
1162
1277
|
}
|
|
1163
1278
|
const folder = folderFor({ ...parsed, positional: parsed.positional.slice(1) });
|
|
@@ -1216,7 +1331,7 @@ ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
|
1216
1331
|
const applied = await (0, api_js_1.applyMerge)(summary.id);
|
|
1217
1332
|
console.log(`
|
|
1218
1333
|
Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
1219
|
-
console.log(dim("Run
|
|
1334
|
+
console.log(dim("Run cbx get to bring it down to this folder."));
|
|
1220
1335
|
return 0;
|
|
1221
1336
|
}
|
|
1222
1337
|
if (!decision) {
|
|
@@ -1227,7 +1342,7 @@ Add --path <file> for one file, then --apply when ready.`));
|
|
|
1227
1342
|
}
|
|
1228
1343
|
else if (now.provisional.ready) {
|
|
1229
1344
|
console.log(dim(`
|
|
1230
|
-
Run
|
|
1345
|
+
Run cbx merge ${now.mergeTrack.reference} --apply to publish it.`));
|
|
1231
1346
|
}
|
|
1232
1347
|
return 0;
|
|
1233
1348
|
}
|
|
@@ -1247,7 +1362,7 @@ Run coderook merge ${now.mergeTrack.reference} --apply to publish it.`));
|
|
|
1247
1362
|
async function commandRunner(parsed) {
|
|
1248
1363
|
const reference = parsed.positional[0] ?? flagText(parsed, "project", "p");
|
|
1249
1364
|
if (!reference) {
|
|
1250
|
-
console.error(red("Which project?
|
|
1365
|
+
console.error(red("Which project? cbx runner <project>"));
|
|
1251
1366
|
return 1;
|
|
1252
1367
|
}
|
|
1253
1368
|
const project = await (0, api_js_2.findProject)(reference);
|
|
@@ -1393,7 +1508,7 @@ const SPECS = [
|
|
|
1393
1508
|
},
|
|
1394
1509
|
{
|
|
1395
1510
|
name: "submit",
|
|
1396
|
-
aliases: ["publish"],
|
|
1511
|
+
aliases: ["publish", "push"],
|
|
1397
1512
|
group: "Working with a folder",
|
|
1398
1513
|
summary: "send the changes as a new version",
|
|
1399
1514
|
usage: 'submit [folder] -m "…"',
|
|
@@ -1411,6 +1526,10 @@ const SPECS = [
|
|
|
1411
1526
|
flags: "--allow-secrets",
|
|
1412
1527
|
description: "send files that look like credentials, and files with keys inside",
|
|
1413
1528
|
},
|
|
1529
|
+
{
|
|
1530
|
+
flags: "--sync",
|
|
1531
|
+
description: "treat a file that is no longer here as deleted, rather than keeping the saved copy",
|
|
1532
|
+
},
|
|
1414
1533
|
{
|
|
1415
1534
|
flags: "--track <name>",
|
|
1416
1535
|
description: "save onto this line, just this once",
|
|
@@ -1421,8 +1540,8 @@ const SPECS = [
|
|
|
1421
1540
|
},
|
|
1422
1541
|
],
|
|
1423
1542
|
examples: [
|
|
1424
|
-
'
|
|
1425
|
-
'
|
|
1543
|
+
'cbx submit -m "Fix the export dialog"',
|
|
1544
|
+
'cbx submit --track spike -m "Try the other encoder"',
|
|
1426
1545
|
],
|
|
1427
1546
|
run: commandSubmit,
|
|
1428
1547
|
},
|
|
@@ -1453,11 +1572,48 @@ const SPECS = [
|
|
|
1453
1572
|
},
|
|
1454
1573
|
],
|
|
1455
1574
|
examples: [
|
|
1456
|
-
"
|
|
1457
|
-
"
|
|
1575
|
+
"cbx import https://github.com/owner/project",
|
|
1576
|
+
"cbx import git@github.com:owner/project.git ./project",
|
|
1458
1577
|
],
|
|
1459
1578
|
run: commandImport,
|
|
1460
1579
|
},
|
|
1580
|
+
{
|
|
1581
|
+
/*
|
|
1582
|
+
Distinct from `import`, which takes a snapshot. This moves the history,
|
|
1583
|
+
the branches, the tags and what the project says about itself — the
|
|
1584
|
+
things somebody leaving a host is actually worried about losing.
|
|
1585
|
+
*/
|
|
1586
|
+
name: "transfer",
|
|
1587
|
+
aliases: ["migrate"],
|
|
1588
|
+
group: "Getting started",
|
|
1589
|
+
summary: "move a whole repository here from GitHub, GitLab or Codeberg",
|
|
1590
|
+
usage: "transfer <address>",
|
|
1591
|
+
detail: "Brings the commits, every branch, the tags and the description across.\n" +
|
|
1592
|
+
"Each commit becomes a version and each branch a line, so a long history\n" +
|
|
1593
|
+
"takes a while; the count and an estimate are printed before it starts.\n\n" +
|
|
1594
|
+
"The history is published by running `git push` against the CodeRook\n" +
|
|
1595
|
+
"remote — the same route anybody else uses, deliberately, because the\n" +
|
|
1596
|
+
"claim being made is that git repositories work here.\n\n" +
|
|
1597
|
+
"A private repository needs a read token in the environment:\n" +
|
|
1598
|
+
"GITHUB_TOKEN, GITLAB_TOKEN or FORGE_TOKEN. It is never asked for on the\n" +
|
|
1599
|
+
"command line and never stored.\n\n" +
|
|
1600
|
+
"The project is created private whatever the source was, unless you say\n" +
|
|
1601
|
+
"otherwise. Pull requests, CI configuration and collaborators do not come\n" +
|
|
1602
|
+
"across; the report at the end lists what did and what did not.",
|
|
1603
|
+
options: [
|
|
1604
|
+
{ flags: "--name <name>", description: "call the project this, rather than the repository's name" },
|
|
1605
|
+
{
|
|
1606
|
+
flags: "--visibility <how>",
|
|
1607
|
+
description: "private (default), same as the source, or public",
|
|
1608
|
+
},
|
|
1609
|
+
{ flags: "--issues", description: "also bring the open issues; needs a read token" },
|
|
1610
|
+
],
|
|
1611
|
+
examples: [
|
|
1612
|
+
"cbx transfer https://github.com/owner/project",
|
|
1613
|
+
"cbx transfer https://codeberg.org/owner/project --visibility same",
|
|
1614
|
+
],
|
|
1615
|
+
run: transfer_command_js_1.commandTransfer,
|
|
1616
|
+
},
|
|
1461
1617
|
{
|
|
1462
1618
|
/*
|
|
1463
1619
|
Where the next save goes, which is a property of this folder rather
|
|
@@ -1465,14 +1621,14 @@ const SPECS = [
|
|
|
1465
1621
|
different lines, which is most of the point of having them.
|
|
1466
1622
|
*/
|
|
1467
1623
|
name: "track",
|
|
1468
|
-
aliases: ["switch"],
|
|
1624
|
+
aliases: ["switch", "checkout"],
|
|
1469
1625
|
group: "Working with a folder",
|
|
1470
1626
|
summary: "show or change the line this folder saves to",
|
|
1471
1627
|
usage: "track [name]",
|
|
1472
1628
|
detail: "With no name, prints the line this folder saves to. With one, switches\n" +
|
|
1473
1629
|
"to it. Switching says where the next save goes and nothing else: no\n" +
|
|
1474
1630
|
"files move and nothing is fetched, so it is instant and safe to change\n" +
|
|
1475
|
-
"your mind. Run `
|
|
1631
|
+
"your mind. Run `cbx get` afterwards to bring that line's files\n" +
|
|
1476
1632
|
"into the folder.\n\n" +
|
|
1477
1633
|
"A name that does not exist is refused rather than created, because a\n" +
|
|
1478
1634
|
"typo in a branch name is an ordinary thing to do and a line called\n" +
|
|
@@ -1481,23 +1637,25 @@ const SPECS = [
|
|
|
1481
1637
|
options: [
|
|
1482
1638
|
{ flags: "-n, --new", description: "start this line from where the project is now" },
|
|
1483
1639
|
],
|
|
1484
|
-
examples: ["
|
|
1640
|
+
examples: ["cbx track", "cbx track spike --new", "cbx track main"],
|
|
1485
1641
|
run: track_commands_js_1.commandTrack,
|
|
1486
1642
|
},
|
|
1487
1643
|
{
|
|
1488
1644
|
name: "tracks",
|
|
1645
|
+
aliases: ["branch", "branches"],
|
|
1489
1646
|
group: "Your projects",
|
|
1490
1647
|
summary: "the lines a project has, and any waiting on a decision",
|
|
1491
1648
|
usage: "tracks [project]",
|
|
1492
1649
|
detail: "Lists every line on the project, marking the one this folder saves to.\n" +
|
|
1493
1650
|
"Merges waiting on a decision are listed beside them rather than hidden,\n" +
|
|
1494
1651
|
"because somebody looking for where they can save needs to see the one\n" +
|
|
1495
|
-
"they cannot. Finish one with `
|
|
1496
|
-
examples: ["
|
|
1652
|
+
"they cannot. Finish one with `cbx merge`.",
|
|
1653
|
+
examples: ["cbx tracks", "cbx tracks my-project"],
|
|
1497
1654
|
run: track_commands_js_1.commandTracks,
|
|
1498
1655
|
},
|
|
1499
1656
|
{
|
|
1500
1657
|
name: "get",
|
|
1658
|
+
aliases: ["pull"],
|
|
1501
1659
|
group: "Working with a folder",
|
|
1502
1660
|
summary: "bring this folder up to date",
|
|
1503
1661
|
usage: "get [folder]",
|
|
@@ -1511,6 +1669,13 @@ const SPECS = [
|
|
|
1511
1669
|
group: "Working with a folder",
|
|
1512
1670
|
summary: "fetch a project into a new folder",
|
|
1513
1671
|
usage: "clone <project> [dir]",
|
|
1672
|
+
options: [
|
|
1673
|
+
{
|
|
1674
|
+
flags: "--track <name>",
|
|
1675
|
+
description: "fetch this line rather than the project's newest version",
|
|
1676
|
+
},
|
|
1677
|
+
],
|
|
1678
|
+
examples: ["cbx clone my-project", "cbx clone my-project --track spike"],
|
|
1514
1679
|
run: commandClone,
|
|
1515
1680
|
},
|
|
1516
1681
|
{
|
|
@@ -1540,7 +1705,7 @@ const SPECS = [
|
|
|
1540
1705
|
description: "replace a licence that is already there",
|
|
1541
1706
|
},
|
|
1542
1707
|
],
|
|
1543
|
-
examples: ["
|
|
1708
|
+
examples: ["cbx licence", "cbx licence MIT", "cbx licence Apache-2.0"],
|
|
1544
1709
|
run: licence_commands_js_1.commandLicence,
|
|
1545
1710
|
},
|
|
1546
1711
|
{
|
|
@@ -1587,6 +1752,7 @@ const SPECS = [
|
|
|
1587
1752
|
},
|
|
1588
1753
|
{
|
|
1589
1754
|
name: "versions",
|
|
1755
|
+
aliases: ["log"],
|
|
1590
1756
|
group: "Your projects",
|
|
1591
1757
|
summary: "what has been saved to a project",
|
|
1592
1758
|
usage: "versions [project]",
|
|
@@ -1612,9 +1778,9 @@ const SPECS = [
|
|
|
1612
1778
|
{ flags: "--request [off]", description: "automated calls to its endpoints" },
|
|
1613
1779
|
],
|
|
1614
1780
|
examples: [
|
|
1615
|
-
"
|
|
1616
|
-
"
|
|
1617
|
-
"
|
|
1781
|
+
"cbx ai my-project",
|
|
1782
|
+
"cbx ai my-project --read off",
|
|
1783
|
+
"cbx ai my-project --download off --request off",
|
|
1618
1784
|
],
|
|
1619
1785
|
run: project_commands_js_1.commandAi,
|
|
1620
1786
|
},
|
|
@@ -1627,16 +1793,43 @@ const SPECS = [
|
|
|
1627
1793
|
{ flags: '--new "<title>"', description: "open a new issue" },
|
|
1628
1794
|
{ flags: "--body <text>", description: "the description for a new one" },
|
|
1629
1795
|
],
|
|
1630
|
-
examples: ['
|
|
1796
|
+
examples: ['cbx issues my-project --new "Crash on export"'],
|
|
1631
1797
|
run: service_commands_js_1.commandIssues,
|
|
1632
1798
|
},
|
|
1633
1799
|
{
|
|
1634
1800
|
name: "releases",
|
|
1801
|
+
aliases: ["tags"],
|
|
1635
1802
|
group: "Your projects",
|
|
1636
1803
|
summary: "what has been released, and its files",
|
|
1637
1804
|
usage: "releases [project]",
|
|
1638
1805
|
run: service_commands_js_1.commandReleases,
|
|
1639
1806
|
},
|
|
1807
|
+
{
|
|
1808
|
+
/*
|
|
1809
|
+
A release is a version with a name on it, so this names one rather than
|
|
1810
|
+
creating anything. Pushing a git tag does the same thing by the same
|
|
1811
|
+
route — the tag is the name, the tagged commit picks the version.
|
|
1812
|
+
*/
|
|
1813
|
+
name: "release",
|
|
1814
|
+
aliases: ["tag"],
|
|
1815
|
+
group: "Your projects",
|
|
1816
|
+
summary: "give a version a name, so people know which one to fetch",
|
|
1817
|
+
usage: "release <name> [project]",
|
|
1818
|
+
detail: "Names the newest version unless you name another with --version.\n" +
|
|
1819
|
+
"Nothing is uploaded: a release is a version with a name on it, so the\n" +
|
|
1820
|
+
"version has to exist already.\n\n" +
|
|
1821
|
+
"The same thing happens when a git tag is pushed to a CodeRook remote —\n" +
|
|
1822
|
+
"`git push cbx v1.0` and `cbx release v1.0` are one action.",
|
|
1823
|
+
options: [
|
|
1824
|
+
{ flags: "--version <n>", description: "name this version instead of the newest" },
|
|
1825
|
+
{ flags: "--notes <text>", description: "what changed, for the release page" },
|
|
1826
|
+
],
|
|
1827
|
+
examples: [
|
|
1828
|
+
"cbx release v1.0",
|
|
1829
|
+
'cbx release v1.2 --version 41 --notes "Fixes the export dialog"',
|
|
1830
|
+
],
|
|
1831
|
+
run: service_commands_js_1.commandRelease,
|
|
1832
|
+
},
|
|
1640
1833
|
{
|
|
1641
1834
|
name: "actions",
|
|
1642
1835
|
aliases: ["workflows"],
|
|
@@ -1644,7 +1837,7 @@ const SPECS = [
|
|
|
1644
1837
|
summary: "the automations a project has",
|
|
1645
1838
|
usage: "actions [project]",
|
|
1646
1839
|
detail: "Shows what each action runs, what it runs on, and how many of its\n" +
|
|
1647
|
-
"runs have passed. Use `
|
|
1840
|
+
"runs have passed. Use `cbx runner` to execute them on this machine.",
|
|
1648
1841
|
run: service_commands_js_1.commandWorkflows,
|
|
1649
1842
|
},
|
|
1650
1843
|
{
|
|
@@ -1661,7 +1854,7 @@ const SPECS = [
|
|
|
1661
1854
|
description: "queued, running, passed, failed or cancelled",
|
|
1662
1855
|
},
|
|
1663
1856
|
],
|
|
1664
|
-
examples: ["
|
|
1857
|
+
examples: ["cbx runs --status failed"],
|
|
1665
1858
|
run: service_commands_js_1.commandRuns,
|
|
1666
1859
|
},
|
|
1667
1860
|
{
|
|
@@ -1672,7 +1865,7 @@ const SPECS = [
|
|
|
1672
1865
|
detail: "The output the runner sent up, in order. Anything the command wrote to\n" +
|
|
1673
1866
|
"stderr is shown in red. Exits 1 when the run failed, so this can be\n" +
|
|
1674
1867
|
"the last line of a script.",
|
|
1675
|
-
examples: ["
|
|
1868
|
+
examples: ["cbx logs 12", "cbx logs #12 my-project"],
|
|
1676
1869
|
run: service_commands_js_1.commandLogs,
|
|
1677
1870
|
},
|
|
1678
1871
|
{
|
|
@@ -1696,7 +1889,7 @@ const SPECS = [
|
|
|
1696
1889
|
{ flags: "--invite <email>", description: "ask somebody to join" },
|
|
1697
1890
|
{ flags: "--role <role>", description: "what they may do (default member)" },
|
|
1698
1891
|
],
|
|
1699
|
-
examples: ["
|
|
1892
|
+
examples: ["cbx people my-project --invite sam@example.com"],
|
|
1700
1893
|
run: service_commands_js_1.commandCollaborators,
|
|
1701
1894
|
},
|
|
1702
1895
|
{
|
|
@@ -1781,7 +1974,7 @@ const SPECS = [
|
|
|
1781
1974
|
{ flags: "--labels <a,b>", description: "what kinds of run it answers to" },
|
|
1782
1975
|
{ flags: "--poll <seconds>", description: "how long between asks" },
|
|
1783
1976
|
],
|
|
1784
|
-
examples: ["
|
|
1977
|
+
examples: ["cbx runner my-game --labels windows,signing"],
|
|
1785
1978
|
run: commandRunner,
|
|
1786
1979
|
},
|
|
1787
1980
|
{
|
|
@@ -1805,7 +1998,7 @@ const SPECS = [
|
|
|
1805
1998
|
"repository for everybody who clones it.\n" +
|
|
1806
1999
|
"\n" +
|
|
1807
2000
|
"It teaches the command line rather than the MCP server, because that\n" +
|
|
1808
|
-
"needs no configuration at all. `
|
|
2001
|
+
"needs no configuration at all. `cbx mcp` is still there when a\n" +
|
|
1809
2002
|
"structured connection is wanted.\n",
|
|
1810
2003
|
options: [
|
|
1811
2004
|
{
|
|
@@ -1813,7 +2006,7 @@ const SPECS = [
|
|
|
1813
2006
|
description: "install into this project rather than for you",
|
|
1814
2007
|
},
|
|
1815
2008
|
],
|
|
1816
|
-
examples: ["
|
|
2009
|
+
examples: ["cbx skill", "cbx skill --project"],
|
|
1817
2010
|
run: skill_command_js_1.commandSkill,
|
|
1818
2011
|
},
|
|
1819
2012
|
{
|
|
@@ -1832,11 +2025,11 @@ const SPECS = [
|
|
|
1832
2025
|
"assistant at it and it starts and stops the process itself.\n" +
|
|
1833
2026
|
"\n" +
|
|
1834
2027
|
"Claude Code:\n" +
|
|
1835
|
-
" claude mcp add coderook --
|
|
2028
|
+
" claude mcp add coderook -- cbx mcp\n" +
|
|
1836
2029
|
"\n" +
|
|
1837
2030
|
"Codex, in ~/.codex/config.toml:\n" +
|
|
1838
2031
|
" [mcp_servers.coderook]\n" +
|
|
1839
|
-
" command = '
|
|
2032
|
+
" command = 'cbx'\n" +
|
|
1840
2033
|
" args = ['mcp']\n" +
|
|
1841
2034
|
"\n" +
|
|
1842
2035
|
"Everything it offers reads. It lists projects, versions and files, shows\n" +
|
|
@@ -1846,7 +2039,7 @@ const SPECS = [
|
|
|
1846
2039
|
"\n" +
|
|
1847
2040
|
"A project whose owner has turned off machine reading is refused, in\n" +
|
|
1848
2041
|
"words rather than as a status code.\n",
|
|
1849
|
-
examples: ["claude mcp add coderook --
|
|
2042
|
+
examples: ["claude mcp add coderook -- cbx mcp"],
|
|
1850
2043
|
run: () => (0, mcp_js_1.commandMcp)(VERSION),
|
|
1851
2044
|
},
|
|
1852
2045
|
{
|
|
@@ -1869,7 +2062,7 @@ async function main(argv) {
|
|
|
1869
2062
|
return 0;
|
|
1870
2063
|
}
|
|
1871
2064
|
/*
|
|
1872
|
-
`
|
|
2065
|
+
`cbx help submit` and `cbx submit --help` reach the same page.
|
|
1873
2066
|
People reach for both, and one of them silently doing something else is
|
|
1874
2067
|
the kind of small betrayal that makes a tool feel unreliable.
|
|
1875
2068
|
*/
|