@coderook/cli 0.26.0 → 0.28.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/dist/cli/src/api.js +156 -1
- package/dist/cli/src/attach_command.js +217 -0
- package/dist/cli/src/cli.js +238 -27
- package/dist/cli/src/git_remote.js +100 -15
- package/dist/cli/src/project_commands.js +46 -5
- package/dist/cli/src/service_commands.js +201 -1
- package/dist/cli/src/version_commands.js +163 -4
- package/dist/desktop-app/src/main/download.js +15 -6
- package/dist/desktop-app/src/main/tracks.js +171 -6
- package/dist/desktop-app/src/main/upload.js +4 -0
- package/dist/desktop-app/src/main/worktree.js +26 -8
- package/package.json +1 -1
package/dist/cli/src/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ 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
27
|
const publish_js_1 = require("./publish.js");
|
|
28
|
+
const attach_command_js_1 = require("./attach_command.js");
|
|
28
29
|
const project_commands_js_1 = require("./project_commands.js");
|
|
29
30
|
const track_commands_js_1 = require("./track_commands.js");
|
|
30
31
|
const tracks_js_1 = require("../../desktop-app/src/main/tracks.js");
|
|
@@ -93,18 +94,62 @@ const done = (line) => {
|
|
|
93
94
|
node_process_1.default.stdout.write("\r");
|
|
94
95
|
}
|
|
95
96
|
};
|
|
96
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The flags a command declares as taking a value.
|
|
99
|
+
*
|
|
100
|
+
* Read from the option's own help text, which already says so: a value is
|
|
101
|
+
* written as a placeholder after the name, as in `--name <text>`, and a
|
|
102
|
+
* switch has nothing after it. So there is no second list to keep in step
|
|
103
|
+
* with the first — the thing already written for the reader is the thing
|
|
104
|
+
* the parser uses.
|
|
105
|
+
*/
|
|
106
|
+
function valueTaking(spec) {
|
|
107
|
+
const names = new Set();
|
|
108
|
+
for (const option of spec?.options ?? []) {
|
|
109
|
+
if (!/[<[]/.test(option.flags))
|
|
110
|
+
continue;
|
|
111
|
+
for (const match of option.flags.matchAll(/--?([A-Za-z0-9][\w-]*)/g)) {
|
|
112
|
+
names.add(match[1]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return names;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* What was typed, split into positions and flags.
|
|
119
|
+
*
|
|
120
|
+
* A flag takes the word after it only when the command says it takes one.
|
|
121
|
+
* Guessing from the shape of the next word — anything not starting with a
|
|
122
|
+
* dash — meant `cbx take-down 1 --yes my-project` read the project name as
|
|
123
|
+
* the value of `--yes` and then reported that `--yes` had not been given,
|
|
124
|
+
* having also swallowed the argument that said which project. Every switch
|
|
125
|
+
* followed by a positional had the same fault.
|
|
126
|
+
*/
|
|
127
|
+
function parse(argv, spec) {
|
|
97
128
|
const positional = [];
|
|
98
129
|
const flags = new Map();
|
|
130
|
+
const takesValue = valueTaking(spec);
|
|
99
131
|
for (let at = 0; at < argv.length; at += 1) {
|
|
100
132
|
const item = argv[at];
|
|
101
133
|
if (!item.startsWith("-")) {
|
|
102
134
|
positional.push(item);
|
|
103
135
|
continue;
|
|
104
136
|
}
|
|
105
|
-
|
|
137
|
+
/* `--name=value` says so itself and needs no declaration. */
|
|
138
|
+
const written = item.replace(/^--?/, "");
|
|
139
|
+
const split = written.indexOf("=");
|
|
140
|
+
if (split > 0) {
|
|
141
|
+
flags.set(written.slice(0, split), written.slice(split + 1));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const name = written;
|
|
106
145
|
const next = argv[at + 1];
|
|
107
|
-
|
|
146
|
+
/*
|
|
147
|
+
An undeclared flag keeps the old guess. Commands that never declared
|
|
148
|
+
their options rely on it, and narrowing that here would trade one
|
|
149
|
+
quiet misreading for another.
|
|
150
|
+
*/
|
|
151
|
+
const wants = takesValue.size ? takesValue.has(name) : true;
|
|
152
|
+
if (wants && next !== undefined && !next.startsWith("-")) {
|
|
108
153
|
flags.set(name, next);
|
|
109
154
|
at += 1;
|
|
110
155
|
}
|
|
@@ -398,7 +443,7 @@ async function commandImport(parsed) {
|
|
|
398
443
|
flags: submitFlags,
|
|
399
444
|
});
|
|
400
445
|
if (code === 0 && plan.temporary) {
|
|
401
|
-
console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
|
|
446
|
+
console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
|
|
402
447
|
` +
|
|
403
448
|
`Run ${accent("cbx get " + plan.name)} anywhere to fetch it fresh.`));
|
|
404
449
|
}
|
|
@@ -526,17 +571,17 @@ async function commandSubmit(parsed) {
|
|
|
526
571
|
*/
|
|
527
572
|
const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
|
|
528
573
|
if (shielded.length && !hasFlag(parsed, "allow-private")) {
|
|
529
|
-
console.log(red(`
|
|
574
|
+
console.log(red(`
|
|
530
575
|
${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
|
|
531
576
|
for (const finding of shielded) {
|
|
532
577
|
console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
|
|
533
578
|
}
|
|
534
|
-
console.log(`
|
|
579
|
+
console.log(`
|
|
535
580
|
Nothing was sent. To leave them behind:`);
|
|
536
581
|
for (const finding of shielded) {
|
|
537
582
|
console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
|
|
538
583
|
}
|
|
539
|
-
console.error(`
|
|
584
|
+
console.error(`
|
|
540
585
|
Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
541
586
|
return 1;
|
|
542
587
|
}
|
|
@@ -571,7 +616,7 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
|
571
616
|
const pasted = await (0, worktree_js_1.detectPastedCredentials)(folder, files.map((file) => file.path));
|
|
572
617
|
const stillPasted = pasted.filter((finding) => !exposed.includes(finding.path));
|
|
573
618
|
if (stillPasted.length) {
|
|
574
|
-
console.log(red(`
|
|
619
|
+
console.log(red(`
|
|
575
620
|
${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
|
|
576
621
|
for (const finding of stillPasted.slice(0, 20)) {
|
|
577
622
|
console.log(` ${finding.path}`);
|
|
@@ -586,11 +631,11 @@ ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s
|
|
|
586
631
|
console.log(dim(` …and ${stillPasted.length - 20} more`));
|
|
587
632
|
}
|
|
588
633
|
if (!hasFlag(parsed, "allow-secrets")) {
|
|
589
|
-
console.error(`
|
|
634
|
+
console.error(`
|
|
590
635
|
Nothing was sent. Move the key into an environment variable, and if` +
|
|
591
636
|
` it has ever been published, replace it at the service that issued` +
|
|
592
637
|
` it — a key that has leaked stays leaked.` +
|
|
593
|
-
`
|
|
638
|
+
`
|
|
594
639
|
Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
595
640
|
return 1;
|
|
596
641
|
}
|
|
@@ -649,6 +694,48 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
649
694
|
...(hasFlag(parsed, "allow-secrets") ? { allowSecrets: true } : {}),
|
|
650
695
|
...(link ? { known: link.local ?? link.manifest ?? {} } : {}),
|
|
651
696
|
});
|
|
697
|
+
/*
|
|
698
|
+
Whether this is walking into a merge, asked before anything is sent.
|
|
699
|
+
|
|
700
|
+
The service has offered this since Merge Tracks shipped and nothing ever
|
|
701
|
+
asked, so the first anybody heard that their upload would be diverted was
|
|
702
|
+
after it had finished — which on a slow line is the worst possible moment
|
|
703
|
+
to find out. It is advice: the answer can be stale by the time the
|
|
704
|
+
publish lands and the publish path decides for real, so this says what is
|
|
705
|
+
coming and gets out of the way.
|
|
706
|
+
*/
|
|
707
|
+
if (link?.repositoryId && link.baseVersionId) {
|
|
708
|
+
try {
|
|
709
|
+
const ahead = await (0, api_js_1.collisionCheck)(link.repositoryId, link.baseVersionId, files.filter((file) => !file.deleted).map((file) => file.path));
|
|
710
|
+
if (ahead.collidingPaths.length) {
|
|
711
|
+
console.log(accent("Heads up") +
|
|
712
|
+
` somebody has saved since you last caught up, and ` +
|
|
713
|
+
`${ahead.collidingPaths.length} of your file` +
|
|
714
|
+
`${ahead.collidingPaths.length === 1 ? "" : "s"} ` +
|
|
715
|
+
`${ahead.collidingPaths.length === 1 ? "is" : "are"} among what they changed.`);
|
|
716
|
+
for (const path of ahead.collidingPaths.slice(0, 5)) {
|
|
717
|
+
console.log(dim(` ${path}`));
|
|
718
|
+
}
|
|
719
|
+
if (ahead.collidingPaths.length > 5) {
|
|
720
|
+
console.log(dim(` and ${ahead.collidingPaths.length - 5} more`));
|
|
721
|
+
}
|
|
722
|
+
console.log(dim(" Your upload will be kept whole and put on a merge for you to settle."));
|
|
723
|
+
console.log(dim(" Run cbx get first if you would rather build on theirs."));
|
|
724
|
+
console.log("");
|
|
725
|
+
}
|
|
726
|
+
else if (ahead.behind) {
|
|
727
|
+
console.log(dim("Somebody has saved since you last caught up, but not to any file " +
|
|
728
|
+
"you changed — this will combine on its own."));
|
|
729
|
+
console.log("");
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
/*
|
|
734
|
+
Advice that cannot be fetched is not a reason to refuse a publish.
|
|
735
|
+
The service checks properly at the point it matters.
|
|
736
|
+
*/
|
|
737
|
+
}
|
|
738
|
+
}
|
|
652
739
|
let result;
|
|
653
740
|
try {
|
|
654
741
|
const plan = await uploader.plan(uploadRequest, (progress) => {
|
|
@@ -688,7 +775,7 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
688
775
|
const failure = (0, publish_js_1.classifyPublishFailure)(error);
|
|
689
776
|
const text = error instanceof Error ? error.message : String(error);
|
|
690
777
|
if (failure?.kind === "interrupted") {
|
|
691
|
-
console.error(red(`
|
|
778
|
+
console.error(red(`
|
|
692
779
|
The connection failed: ${text}`));
|
|
693
780
|
console.error(`Your work may already have been saved. Run the same command again —` +
|
|
694
781
|
` it will not create a second version.`);
|
|
@@ -890,7 +977,7 @@ async function commandGet(parsed) {
|
|
|
890
977
|
`changed while the interrupted fetch was stopped:`));
|
|
891
978
|
for (const file of changedInTheGap.slice(0, 20))
|
|
892
979
|
console.error(` ${file.path}`);
|
|
893
|
-
console.error(`
|
|
980
|
+
console.error(`
|
|
894
981
|
Save them with ${accent("cbx submit")}, or finish the fetch and ` +
|
|
895
982
|
`discard them with ${accent("cbx get --replace")}.`);
|
|
896
983
|
return 1;
|
|
@@ -1122,8 +1209,8 @@ async function suggestRules(folder, apply) {
|
|
|
1122
1209
|
const addition = (0, detect_js_1.rulesFromSuggestions)(recommended);
|
|
1123
1210
|
const shared = rules.shared.trimEnd();
|
|
1124
1211
|
await (0, worktree_js_1.writeRules)(folder, {
|
|
1125
|
-
shared: shared ? `${shared}
|
|
1126
|
-
|
|
1212
|
+
shared: shared ? `${shared}
|
|
1213
|
+
|
|
1127
1214
|
${addition}` : addition,
|
|
1128
1215
|
local: rules.local,
|
|
1129
1216
|
});
|
|
@@ -1250,7 +1337,7 @@ async function commandMerges(parsed) {
|
|
|
1250
1337
|
const counts = merge.conflicts;
|
|
1251
1338
|
console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
|
|
1252
1339
|
}
|
|
1253
|
-
console.log(dim(`
|
|
1340
|
+
console.log(dim(`
|
|
1254
1341
|
Run cbx merge <reference> to look at one.`));
|
|
1255
1342
|
return 0;
|
|
1256
1343
|
}
|
|
@@ -1316,7 +1403,7 @@ async function commandMerge(parsed) {
|
|
|
1316
1403
|
}
|
|
1317
1404
|
}
|
|
1318
1405
|
const now = await (0, api_js_1.mergeTrack)(summary.id);
|
|
1319
|
-
console.log(`
|
|
1406
|
+
console.log(`
|
|
1320
1407
|
${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
1321
1408
|
(now.provisional.ready
|
|
1322
1409
|
? "ready to apply"
|
|
@@ -1331,19 +1418,19 @@ ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
|
1331
1418
|
return 1;
|
|
1332
1419
|
}
|
|
1333
1420
|
const applied = await (0, api_js_1.applyMerge)(summary.id);
|
|
1334
|
-
console.log(`
|
|
1421
|
+
console.log(`
|
|
1335
1422
|
Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
1336
1423
|
console.log(dim("Run cbx get to bring it down to this folder."));
|
|
1337
1424
|
return 0;
|
|
1338
1425
|
}
|
|
1339
1426
|
if (!decision) {
|
|
1340
|
-
console.log(dim(`
|
|
1427
|
+
console.log(dim(`
|
|
1341
1428
|
--mine keeps yours, --theirs keeps what was already saved,` +
|
|
1342
|
-
` --drop removes the file.
|
|
1429
|
+
` --drop removes the file.
|
|
1343
1430
|
Add --path <file> for one file, then --apply when ready.`));
|
|
1344
1431
|
}
|
|
1345
1432
|
else if (now.provisional.ready) {
|
|
1346
|
-
console.log(dim(`
|
|
1433
|
+
console.log(dim(`
|
|
1347
1434
|
Run cbx merge ${now.mergeTrack.reference} --apply to publish it.`));
|
|
1348
1435
|
}
|
|
1349
1436
|
return 0;
|
|
@@ -1758,8 +1845,20 @@ const SPECS = [
|
|
|
1758
1845
|
group: "Your projects",
|
|
1759
1846
|
summary: "what has been saved to a project",
|
|
1760
1847
|
usage: "versions [project]",
|
|
1761
|
-
detail: "Newest first. Run it inside a linked folder to leave the name out
|
|
1762
|
-
|
|
1848
|
+
detail: "Newest first. Run it inside a linked folder to leave the name out.\n\n" +
|
|
1849
|
+
"A save is a commit; a commit somebody named is a version, and versions\n" +
|
|
1850
|
+
"are the only thing the public side shows. Both are listed together\n" +
|
|
1851
|
+
"unless you ask for one lane — the same three views the website has.",
|
|
1852
|
+
options: [
|
|
1853
|
+
{ flags: "--limit <n>", description: "how many to show (default 20)" },
|
|
1854
|
+
{ flags: "--versions", description: "only the ones that were named" },
|
|
1855
|
+
{ flags: "--commits", description: "only the ones that were not" },
|
|
1856
|
+
{ flags: "--notes", description: "print what each one says about itself" },
|
|
1857
|
+
],
|
|
1858
|
+
examples: [
|
|
1859
|
+
"cbx versions --versions",
|
|
1860
|
+
"cbx versions my-project --commits",
|
|
1861
|
+
],
|
|
1763
1862
|
run: project_commands_js_1.commandVersions,
|
|
1764
1863
|
},
|
|
1765
1864
|
{
|
|
@@ -1791,11 +1890,21 @@ const SPECS = [
|
|
|
1791
1890
|
group: "Your projects",
|
|
1792
1891
|
summary: "issues on a project, or open one",
|
|
1793
1892
|
usage: "issues [project]",
|
|
1893
|
+
detail: "Labels are given when the issue is opened rather than added\n" +
|
|
1894
|
+
"afterwards, and any name that does not exist yet is created.\n\n" +
|
|
1895
|
+
"--version says which save it is about. \"It broke\" and \"it broke in\n" +
|
|
1896
|
+
"v41\" are different reports, and the second is the one somebody can\n" +
|
|
1897
|
+
"act on.",
|
|
1794
1898
|
options: [
|
|
1795
1899
|
{ flags: '--new "<title>"', description: "open a new issue" },
|
|
1796
1900
|
{ flags: "--body <text>", description: "the description for a new one" },
|
|
1901
|
+
{ flags: "--labels <a,b>", description: "labels for a new one, comma separated" },
|
|
1902
|
+
{ flags: "--version <n>", description: "which save it is about" },
|
|
1903
|
+
],
|
|
1904
|
+
examples: [
|
|
1905
|
+
'cbx issues my-project --new "Crash on export"',
|
|
1906
|
+
'cbx issues --new "Installer will not run" --labels bug,windows --version 41',
|
|
1797
1907
|
],
|
|
1798
|
-
examples: ['cbx issues my-project --new "Crash on export"'],
|
|
1799
1908
|
run: service_commands_js_1.commandIssues,
|
|
1800
1909
|
},
|
|
1801
1910
|
{
|
|
@@ -1806,6 +1915,37 @@ const SPECS = [
|
|
|
1806
1915
|
usage: "releases [project]",
|
|
1807
1916
|
run: service_commands_js_1.commandReleases,
|
|
1808
1917
|
},
|
|
1918
|
+
{
|
|
1919
|
+
/*
|
|
1920
|
+
The service has been able to notify something else since Merge Tracks
|
|
1921
|
+
shipped — queue an event, sign it, deliver it after the request that
|
|
1922
|
+
caused it — and nothing could reach it, so no project could be told to
|
|
1923
|
+
tell a build box or a status page anything.
|
|
1924
|
+
*/
|
|
1925
|
+
name: "hooks",
|
|
1926
|
+
aliases: ["webhooks"],
|
|
1927
|
+
group: "Your projects",
|
|
1928
|
+
summary: "where this project tells something else what happened",
|
|
1929
|
+
usage: "hooks [project]",
|
|
1930
|
+
detail: "A signed POST to an address of yours when something happens here.\n" +
|
|
1931
|
+
"The signing secret is generated by the service and shown once, when\n" +
|
|
1932
|
+
"the hook is added — no route hands it back afterwards.\n\n" +
|
|
1933
|
+
"Events: version.published, merge.opened, merge.applied,\n" +
|
|
1934
|
+
"check.reported, change_request.opened, change_request.reviewed.\n" +
|
|
1935
|
+
"Naming none of them sends all of them.",
|
|
1936
|
+
options: [
|
|
1937
|
+
{ flags: "--add <url>", description: "notify this address; https only" },
|
|
1938
|
+
{ flags: "--events <a,b>", description: "only these, comma separated" },
|
|
1939
|
+
{ flags: "--remove <id>", description: "stop notifying it" },
|
|
1940
|
+
{ flags: "--project <name>", description: "which project" },
|
|
1941
|
+
],
|
|
1942
|
+
examples: [
|
|
1943
|
+
"cbx hooks",
|
|
1944
|
+
"cbx hooks --add https://example.com/coderook",
|
|
1945
|
+
"cbx hooks --add https://example.com/builds --events version.published",
|
|
1946
|
+
],
|
|
1947
|
+
run: service_commands_js_1.commandHooks,
|
|
1948
|
+
},
|
|
1809
1949
|
{
|
|
1810
1950
|
/*
|
|
1811
1951
|
The half that hiding never covered.
|
|
@@ -1844,6 +1984,8 @@ const SPECS = [
|
|
|
1844
1984
|
options: [
|
|
1845
1985
|
{ flags: "--name <text>", description: "what to call it" },
|
|
1846
1986
|
{ flags: "--notes <text>", description: "what changed" },
|
|
1987
|
+
{ flags: "--notes-file <path>", description: "what changed, from a file" },
|
|
1988
|
+
{ flags: "--edit", description: "write what changed in your editor" },
|
|
1847
1989
|
],
|
|
1848
1990
|
examples: [
|
|
1849
1991
|
"cbx promote",
|
|
@@ -1867,6 +2009,58 @@ const SPECS = [
|
|
|
1867
2009
|
that works until somebody scripts it. This marks a save — pins it,
|
|
1868
2010
|
hides it, labels it — which is what it does anyway.
|
|
1869
2011
|
*/
|
|
2012
|
+
name: "attach",
|
|
2013
|
+
group: "Your projects",
|
|
2014
|
+
summary: "put a file people can download on a version",
|
|
2015
|
+
usage: "attach <file> [n] [project]",
|
|
2016
|
+
detail: "A version can carry downloads — an installer, a build, a changelog —\n" +
|
|
2017
|
+
"and until now the only thing that could put one there was a workflow\n" +
|
|
2018
|
+
"run. This attaches a file from this machine.\n\n" +
|
|
2019
|
+
"Attaching is not publishing. A download on a commit is reachable by\n" +
|
|
2020
|
+
"nobody until that commit is made a version; on a version already\n" +
|
|
2021
|
+
"offered it is public the moment it lands.\n\n" +
|
|
2022
|
+
"Large files go up in parts, so a 95 MB installer works the same way a\n" +
|
|
2023
|
+
"text file does.",
|
|
2024
|
+
options: [
|
|
2025
|
+
{ flags: "--name <text>", description: "call it something else on the version" },
|
|
2026
|
+
{ flags: "--project <name>", description: "which project" },
|
|
2027
|
+
],
|
|
2028
|
+
examples: [
|
|
2029
|
+
"cbx attach ./release/Installer-1.0.exe",
|
|
2030
|
+
"cbx attach ./release/app.dmg 41",
|
|
2031
|
+
'cbx attach ./notes.pdf --name "Release notes.pdf"',
|
|
2032
|
+
],
|
|
2033
|
+
run: attach_command_js_1.commandAttach,
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
name: "notes",
|
|
2037
|
+
aliases: ["release-notes"],
|
|
2038
|
+
group: "Your projects",
|
|
2039
|
+
summary: "read or write what a version says about itself",
|
|
2040
|
+
usage: "notes [n] [project]",
|
|
2041
|
+
detail: "The release info the website shows on a version, and the same field\n" +
|
|
2042
|
+
"`--notes` writes when promoting. With nothing to write it prints what\n" +
|
|
2043
|
+
"is there, which the terminal could not do before: `cbx releases` shows\n" +
|
|
2044
|
+
"the first line and stops.\n\n" +
|
|
2045
|
+
"Written in Markdown, because that is what the page renders. --edit\n" +
|
|
2046
|
+
"opens $EDITOR (or $VISUAL, or notepad on Windows) the way git does,\n" +
|
|
2047
|
+
"which is the only comfortable way to write more than a sentence.",
|
|
2048
|
+
options: [
|
|
2049
|
+
{ flags: "--edit", description: "write it in your editor" },
|
|
2050
|
+
{ flags: "--notes-file <path>", description: "take the text from a file" },
|
|
2051
|
+
{ flags: "--notes <text>", description: "set it in one line" },
|
|
2052
|
+
{ flags: "--clear", description: "remove what is there" },
|
|
2053
|
+
{ flags: "--project <name>", description: "which project" },
|
|
2054
|
+
],
|
|
2055
|
+
examples: [
|
|
2056
|
+
"cbx notes",
|
|
2057
|
+
"cbx notes 41",
|
|
2058
|
+
"cbx notes 41 --edit",
|
|
2059
|
+
"cbx notes 41 --notes-file RELEASE.md",
|
|
2060
|
+
],
|
|
2061
|
+
run: version_commands_js_1.commandNotes,
|
|
2062
|
+
},
|
|
2063
|
+
{
|
|
1870
2064
|
name: "mark",
|
|
1871
2065
|
aliases: ["set"],
|
|
1872
2066
|
group: "Your projects",
|
|
@@ -1879,11 +2073,13 @@ const SPECS = [
|
|
|
1879
2073
|
{ flags: "--name <text>", description: "give it a code name" },
|
|
1880
2074
|
{ flags: "--unname", description: "take the name off again" },
|
|
1881
2075
|
{ flags: "--notes <text>", description: "what changed" },
|
|
2076
|
+
{ flags: "--notes-file <path>", description: "what changed, from a file" },
|
|
2077
|
+
{ flags: "--edit", description: "write what changed in your editor" },
|
|
1882
2078
|
{ flags: "--hide", description: "nobody outside the project can read it" },
|
|
1883
2079
|
{ flags: "--show", description: "anybody can read it" },
|
|
1884
2080
|
{
|
|
1885
2081
|
flags: "--visibility <who>",
|
|
1886
|
-
description: "
|
|
2082
|
+
description: "public, or private to hold it back",
|
|
1887
2083
|
},
|
|
1888
2084
|
{ flags: "--pin", description: "keep it at the top of the list" },
|
|
1889
2085
|
{ flags: "--unpin", description: "put it back in order" },
|
|
@@ -1892,7 +2088,7 @@ const SPECS = [
|
|
|
1892
2088
|
],
|
|
1893
2089
|
examples: [
|
|
1894
2090
|
"cbx mark --name v2.1 --pin",
|
|
1895
|
-
"cbx mark 41 --visibility
|
|
2091
|
+
"cbx mark 41 --visibility private",
|
|
1896
2092
|
'cbx mark 41 --labels "shipped,client work"',
|
|
1897
2093
|
],
|
|
1898
2094
|
run: version_commands_js_1.commandVersion,
|
|
@@ -1994,7 +2190,22 @@ const SPECS = [
|
|
|
1994
2190
|
summary: "the automations a project has",
|
|
1995
2191
|
usage: "actions [project]",
|
|
1996
2192
|
detail: "Shows what each action runs, what it runs on, and how many of its\n" +
|
|
1997
|
-
"runs have passed. Use `cbx runner` to execute them on this machine
|
|
2193
|
+
"runs have passed. Use `cbx runner` to execute them on this machine.\n\n" +
|
|
2194
|
+
"An action that keeps files can hand them straight to the version it\n" +
|
|
2195
|
+
"ran against, so a passing build becomes a download without anybody\n" +
|
|
2196
|
+
"opening the website. --ship turns that on for one action.\n\n" +
|
|
2197
|
+
"Attaching is publishing: what an action ships onto a public version\n" +
|
|
2198
|
+
"is a download anybody can take, from the moment the run passes.",
|
|
2199
|
+
options: [
|
|
2200
|
+
{ flags: "--ship <action>", description: "its builds become downloads on the version" },
|
|
2201
|
+
{ flags: "--no-ship <action>", description: "keep its files without attaching them" },
|
|
2202
|
+
{ flags: "--project <name>", description: "which project" },
|
|
2203
|
+
],
|
|
2204
|
+
examples: [
|
|
2205
|
+
"cbx actions",
|
|
2206
|
+
'cbx actions --ship "Windows installer"',
|
|
2207
|
+
'cbx actions --no-ship "Windows installer"',
|
|
2208
|
+
],
|
|
1998
2209
|
run: service_commands_js_1.commandWorkflows,
|
|
1999
2210
|
},
|
|
2000
2211
|
{
|
|
@@ -2249,7 +2460,7 @@ async function main(argv) {
|
|
|
2249
2460
|
if (spec.deprecatedBy) {
|
|
2250
2461
|
console.error(dim(`"${name}" is now "${spec.deprecatedBy}". The old name still works.`));
|
|
2251
2462
|
}
|
|
2252
|
-
return spec.run(parse(rest));
|
|
2463
|
+
return spec.run(parse(rest, spec));
|
|
2253
2464
|
}
|
|
2254
2465
|
/**
|
|
2255
2466
|
* Set the status and let Node wind down on its own.
|
|
@@ -47,9 +47,19 @@ exports.main = main;
|
|
|
47
47
|
* `coderook://` URLs, which is what lets every editor's built-in git panel work
|
|
48
48
|
* with CodeRook without any per-editor work.
|
|
49
49
|
*
|
|
50
|
-
* Both directions are implemented: `push` publishes
|
|
51
|
-
* `import` rebuilds a git history from
|
|
52
|
-
* work.
|
|
50
|
+
* Both directions are implemented: `push` publishes each commit as a save, and
|
|
51
|
+
* `import` rebuilds a git history from those saves so `git clone` and `git
|
|
52
|
+
* fetch` work.
|
|
53
|
+
*
|
|
54
|
+
* The vocabulary matters here, because the two products do not agree by
|
|
55
|
+
* accident. A pushed commit becomes a *commit* in CodeRook — a save, inside
|
|
56
|
+
* the project, that nobody outside can see. Tagging it names it, and a named
|
|
57
|
+
* save is a *version*, which is the only thing the public side offers. That
|
|
58
|
+
* is the same distinction git already draws between a commit and a tag, so
|
|
59
|
+
* the mapping is one-to-one rather than an approximation.
|
|
60
|
+
*
|
|
61
|
+
* A save also carries a description of its own, separate from its message.
|
|
62
|
+
* That travels as a git note on `refs/notes/coderook` in both directions.
|
|
53
63
|
*
|
|
54
64
|
* ## Why the `push` capability rather than `export`
|
|
55
65
|
*
|
|
@@ -143,6 +153,26 @@ function git(args) {
|
|
|
143
153
|
}
|
|
144
154
|
return result.stdout;
|
|
145
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Where CodeRook keeps a save's description inside a git repository.
|
|
158
|
+
*
|
|
159
|
+
* `refs/notes/coderook` rather than git's default `refs/notes/commits`,
|
|
160
|
+
* because the default is shared ground — anything else attaching notes writes
|
|
161
|
+
* there too, and a fetch that overwrote somebody's own notes with ours would
|
|
162
|
+
* be taking something that was not offered.
|
|
163
|
+
*/
|
|
164
|
+
const NOTES_REF = "refs/notes/coderook";
|
|
165
|
+
/**
|
|
166
|
+
* The description attached to a commit, if there is one.
|
|
167
|
+
*
|
|
168
|
+
* Empty rather than throwing for every ordinary reason it can be missing: no
|
|
169
|
+
* notes ref at all, no note on this commit, or a repository where notes have
|
|
170
|
+
* never been used. None of those is a problem worth stopping a push for.
|
|
171
|
+
*/
|
|
172
|
+
function noteOn(sha) {
|
|
173
|
+
const result = (0, node_child_process_1.spawnSync)("git", ["notes", `--ref=${NOTES_REF}`, "show", sha], { encoding: "utf8", maxBuffer: 1024 * 1024 * 8 });
|
|
174
|
+
return result.status === 0 ? (result.stdout ?? "").trim() : "";
|
|
175
|
+
}
|
|
146
176
|
/** Run git for its exit status alone. False rather than throwing. */
|
|
147
177
|
function gitOk(args) {
|
|
148
178
|
return ((0, node_child_process_1.spawnSync)("git", args, { encoding: "utf8", stdio: "ignore" }).status === 0);
|
|
@@ -665,6 +695,40 @@ async function doImport(refs, url) {
|
|
|
665
695
|
say(` ${done}/${fresh.length} versions`);
|
|
666
696
|
}
|
|
667
697
|
}
|
|
698
|
+
/*
|
|
699
|
+
What each save says about itself, as git notes.
|
|
700
|
+
|
|
701
|
+
A CodeRook save carries a description separate from its message — the
|
|
702
|
+
thing the website shows as release info and `cbx notes` writes — and a
|
|
703
|
+
fetch used to drop all of it except on the few versions that were also
|
|
704
|
+
releases. Git's answer for text attached to a commit without changing
|
|
705
|
+
the commit is a note, so that is where it goes: `git log
|
|
706
|
+
--notes=coderook` shows them, and pushing them back returns them.
|
|
707
|
+
|
|
708
|
+
Written as one commit on its own ref after every version commit exists,
|
|
709
|
+
because a note has to name a commit that is already in the stream.
|
|
710
|
+
*/
|
|
711
|
+
const described = fresh.filter((version) => (version.notes ?? "").trim());
|
|
712
|
+
if (described.length) {
|
|
713
|
+
send(`commit ${(0, git_history_js_1.importRef)(NOTES_REF)}`);
|
|
714
|
+
send(`committer CodeRook <noreply@coderook.com> ` +
|
|
715
|
+
`${(0, git_history_js_1.stamp)(new Date().toISOString())} +0000`);
|
|
716
|
+
const why = Buffer.from("What each save says about itself\n", "utf8");
|
|
717
|
+
send(`data ${why.length}`);
|
|
718
|
+
node_process_1.default.stdout.write(why);
|
|
719
|
+
node_process_1.default.stdout.write("\n");
|
|
720
|
+
for (const version of described) {
|
|
721
|
+
const mark = commitMark.get(version.id);
|
|
722
|
+
if (mark === undefined)
|
|
723
|
+
continue;
|
|
724
|
+
const body = Buffer.from(`${(version.notes ?? "").trim()}\n`, "utf8");
|
|
725
|
+
send(`N inline :${mark}`);
|
|
726
|
+
send(`data ${body.length}`);
|
|
727
|
+
node_process_1.default.stdout.write(body);
|
|
728
|
+
node_process_1.default.stdout.write("\n");
|
|
729
|
+
}
|
|
730
|
+
say(` ${described.length} description${described.length === 1 ? "" : "s"} as git notes`);
|
|
731
|
+
}
|
|
668
732
|
/* Every ref git asked for, named outright now the commits exist. */
|
|
669
733
|
settleRefs();
|
|
670
734
|
/*
|
|
@@ -856,7 +920,7 @@ async function doPush(requests, url) {
|
|
|
856
920
|
const existing = await releases(repositoryId).catch(() => []);
|
|
857
921
|
const already = existing.find((release) => release.versionId === versionId && release.name !== tag);
|
|
858
922
|
if (already) {
|
|
859
|
-
say(` note: v${already.sequence} was already released as "${already.name}".
|
|
923
|
+
say(` note: v${already.sequence} was already released as "${already.name}".
|
|
860
924
|
` +
|
|
861
925
|
` A version carries one name, so it is now "${tag}".`);
|
|
862
926
|
}
|
|
@@ -1098,10 +1162,10 @@ async function doPush(requests, url) {
|
|
|
1098
1162
|
happened and go looking for a way to undo it.
|
|
1099
1163
|
*/
|
|
1100
1164
|
send(`error ${request.dst} the connection failed part way through`);
|
|
1101
|
-
say(`
|
|
1102
|
-
${published} of ${commits.length} commits were published.
|
|
1165
|
+
say(`
|
|
1166
|
+
${published} of ${commits.length} commits were published.
|
|
1103
1167
|
` +
|
|
1104
|
-
` Push again — commits already published are not sent twice.
|
|
1168
|
+
` Push again — commits already published are not sent twice.
|
|
1105
1169
|
`);
|
|
1106
1170
|
throw error;
|
|
1107
1171
|
}
|
|
@@ -1113,8 +1177,8 @@ async function doPush(requests, url) {
|
|
|
1113
1177
|
anyway.
|
|
1114
1178
|
*/
|
|
1115
1179
|
send(`error ${request.dst} this commit carries a credential`);
|
|
1116
|
-
say(`
|
|
1117
|
-
${failure.message}
|
|
1180
|
+
say(`
|
|
1181
|
+
${failure.message}
|
|
1118
1182
|
`);
|
|
1119
1183
|
/*
|
|
1120
1184
|
Deleting the file in a later commit does not help, and saying
|
|
@@ -1124,14 +1188,14 @@ async function doPush(requests, url) {
|
|
|
1124
1188
|
The history has to lose it.
|
|
1125
1189
|
*/
|
|
1126
1190
|
say(` A later commit that deletes it is not enough — every commit` +
|
|
1127
|
-
` being pushed
|
|
1191
|
+
` being pushed
|
|
1128
1192
|
becomes a version, and the one that added` +
|
|
1129
|
-
` the key still carries it.
|
|
1193
|
+
` the key still carries it.
|
|
1130
1194
|
` +
|
|
1131
1195
|
` Rewrite it out (git rebase -i, or git commit --amend if it` +
|
|
1132
|
-
` is the last one),
|
|
1196
|
+
` is the last one),
|
|
1133
1197
|
or publish this deliberately with` +
|
|
1134
|
-
` cbx submit --allow-secrets.
|
|
1198
|
+
` cbx submit --allow-secrets.
|
|
1135
1199
|
`);
|
|
1136
1200
|
/*
|
|
1137
1201
|
Reported already, and in more detail than the wrapper can. The
|
|
@@ -1141,8 +1205,8 @@ async function doPush(requests, url) {
|
|
|
1141
1205
|
}
|
|
1142
1206
|
if (failure?.kind === "conflict") {
|
|
1143
1207
|
send(`error ${request.dst} somebody published to "${branch}" while this push was running`);
|
|
1144
|
-
say(`
|
|
1145
|
-
git fetch, then push again.
|
|
1208
|
+
say(`
|
|
1209
|
+
git fetch, then push again.
|
|
1146
1210
|
`);
|
|
1147
1211
|
throw error;
|
|
1148
1212
|
}
|
|
@@ -1154,6 +1218,27 @@ async function doPush(requests, url) {
|
|
|
1154
1218
|
}
|
|
1155
1219
|
currentRepositoryId = result.repositoryId;
|
|
1156
1220
|
baseVersionId = result.versionId;
|
|
1221
|
+
/*
|
|
1222
|
+
A note on the commit becomes what the version says about itself.
|
|
1223
|
+
|
|
1224
|
+
CodeRook lets a save be described before anybody decides to publish
|
|
1225
|
+
it, and git has exactly one place for text attached to a commit
|
|
1226
|
+
without altering it. Sent after the publish rather than with it,
|
|
1227
|
+
because a note is not part of what was saved and a failure to
|
|
1228
|
+
record one must not fail the push.
|
|
1229
|
+
*/
|
|
1230
|
+
const note = noteOn(sha);
|
|
1231
|
+
if (note) {
|
|
1232
|
+
try {
|
|
1233
|
+
const { changeVersion } = await Promise.resolve().then(() => __importStar(require("./api.js")));
|
|
1234
|
+
await changeVersion(result.repositoryId, result.versionId, {
|
|
1235
|
+
notes: note,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
say(` note: the description on ${sha.slice(0, 8)} could not be saved`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1157
1242
|
/*
|
|
1158
1243
|
The version and the commit it came from are the same thing in this
|
|
1159
1244
|
repository from here on. Recorded so a later fetch does not rebuild
|