@coderook/cli 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/api.js +1 -0
- package/dist/cli/src/cli.js +113 -25
- package/dist/cli/src/git_remote.js +100 -15
- package/dist/cli/src/project_commands.js +46 -5
- 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 +18 -0
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "coderook",
|
|
3
3
|
"displayName": "CodeRook",
|
|
4
4
|
"description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, from Claude Code.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.27.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/api.js
CHANGED
|
@@ -251,6 +251,7 @@ async function versions(repositoryId) {
|
|
|
251
251
|
: [],
|
|
252
252
|
authorName: String(row.author?.displayName ?? ""),
|
|
253
253
|
name: row.name == null ? null : String(row.name),
|
|
254
|
+
notes: row.notes == null ? null : String(row.notes),
|
|
254
255
|
/*
|
|
255
256
|
Falls back to the older field, so this keeps working against a service
|
|
256
257
|
that has not been updated yet rather than reporting everything private.
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -93,18 +93,62 @@ const done = (line) => {
|
|
|
93
93
|
node_process_1.default.stdout.write("\r");
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
|
-
|
|
96
|
+
/**
|
|
97
|
+
* The flags a command declares as taking a value.
|
|
98
|
+
*
|
|
99
|
+
* Read from the option's own help text, which already says so: a value is
|
|
100
|
+
* written as a placeholder after the name, as in `--name <text>`, and a
|
|
101
|
+
* switch has nothing after it. So there is no second list to keep in step
|
|
102
|
+
* with the first — the thing already written for the reader is the thing
|
|
103
|
+
* the parser uses.
|
|
104
|
+
*/
|
|
105
|
+
function valueTaking(spec) {
|
|
106
|
+
const names = new Set();
|
|
107
|
+
for (const option of spec?.options ?? []) {
|
|
108
|
+
if (!/[<[]/.test(option.flags))
|
|
109
|
+
continue;
|
|
110
|
+
for (const match of option.flags.matchAll(/--?([A-Za-z0-9][\w-]*)/g)) {
|
|
111
|
+
names.add(match[1]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return names;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* What was typed, split into positions and flags.
|
|
118
|
+
*
|
|
119
|
+
* A flag takes the word after it only when the command says it takes one.
|
|
120
|
+
* Guessing from the shape of the next word — anything not starting with a
|
|
121
|
+
* dash — meant `cbx take-down 1 --yes my-project` read the project name as
|
|
122
|
+
* the value of `--yes` and then reported that `--yes` had not been given,
|
|
123
|
+
* having also swallowed the argument that said which project. Every switch
|
|
124
|
+
* followed by a positional had the same fault.
|
|
125
|
+
*/
|
|
126
|
+
function parse(argv, spec) {
|
|
97
127
|
const positional = [];
|
|
98
128
|
const flags = new Map();
|
|
129
|
+
const takesValue = valueTaking(spec);
|
|
99
130
|
for (let at = 0; at < argv.length; at += 1) {
|
|
100
131
|
const item = argv[at];
|
|
101
132
|
if (!item.startsWith("-")) {
|
|
102
133
|
positional.push(item);
|
|
103
134
|
continue;
|
|
104
135
|
}
|
|
105
|
-
|
|
136
|
+
/* `--name=value` says so itself and needs no declaration. */
|
|
137
|
+
const written = item.replace(/^--?/, "");
|
|
138
|
+
const split = written.indexOf("=");
|
|
139
|
+
if (split > 0) {
|
|
140
|
+
flags.set(written.slice(0, split), written.slice(split + 1));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const name = written;
|
|
106
144
|
const next = argv[at + 1];
|
|
107
|
-
|
|
145
|
+
/*
|
|
146
|
+
An undeclared flag keeps the old guess. Commands that never declared
|
|
147
|
+
their options rely on it, and narrowing that here would trade one
|
|
148
|
+
quiet misreading for another.
|
|
149
|
+
*/
|
|
150
|
+
const wants = takesValue.size ? takesValue.has(name) : true;
|
|
151
|
+
if (wants && next !== undefined && !next.startsWith("-")) {
|
|
108
152
|
flags.set(name, next);
|
|
109
153
|
at += 1;
|
|
110
154
|
}
|
|
@@ -398,7 +442,7 @@ async function commandImport(parsed) {
|
|
|
398
442
|
flags: submitFlags,
|
|
399
443
|
});
|
|
400
444
|
if (code === 0 && plan.temporary) {
|
|
401
|
-
console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
|
|
445
|
+
console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
|
|
402
446
|
` +
|
|
403
447
|
`Run ${accent("cbx get " + plan.name)} anywhere to fetch it fresh.`));
|
|
404
448
|
}
|
|
@@ -526,17 +570,17 @@ async function commandSubmit(parsed) {
|
|
|
526
570
|
*/
|
|
527
571
|
const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
|
|
528
572
|
if (shielded.length && !hasFlag(parsed, "allow-private")) {
|
|
529
|
-
console.log(red(`
|
|
573
|
+
console.log(red(`
|
|
530
574
|
${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
|
|
531
575
|
for (const finding of shielded) {
|
|
532
576
|
console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
|
|
533
577
|
}
|
|
534
|
-
console.log(`
|
|
578
|
+
console.log(`
|
|
535
579
|
Nothing was sent. To leave them behind:`);
|
|
536
580
|
for (const finding of shielded) {
|
|
537
581
|
console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
|
|
538
582
|
}
|
|
539
|
-
console.error(`
|
|
583
|
+
console.error(`
|
|
540
584
|
Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
541
585
|
return 1;
|
|
542
586
|
}
|
|
@@ -571,7 +615,7 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
|
571
615
|
const pasted = await (0, worktree_js_1.detectPastedCredentials)(folder, files.map((file) => file.path));
|
|
572
616
|
const stillPasted = pasted.filter((finding) => !exposed.includes(finding.path));
|
|
573
617
|
if (stillPasted.length) {
|
|
574
|
-
console.log(red(`
|
|
618
|
+
console.log(red(`
|
|
575
619
|
${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
|
|
576
620
|
for (const finding of stillPasted.slice(0, 20)) {
|
|
577
621
|
console.log(` ${finding.path}`);
|
|
@@ -586,11 +630,11 @@ ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s
|
|
|
586
630
|
console.log(dim(` …and ${stillPasted.length - 20} more`));
|
|
587
631
|
}
|
|
588
632
|
if (!hasFlag(parsed, "allow-secrets")) {
|
|
589
|
-
console.error(`
|
|
633
|
+
console.error(`
|
|
590
634
|
Nothing was sent. Move the key into an environment variable, and if` +
|
|
591
635
|
` it has ever been published, replace it at the service that issued` +
|
|
592
636
|
` it — a key that has leaked stays leaked.` +
|
|
593
|
-
`
|
|
637
|
+
`
|
|
594
638
|
Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
595
639
|
return 1;
|
|
596
640
|
}
|
|
@@ -688,7 +732,7 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
688
732
|
const failure = (0, publish_js_1.classifyPublishFailure)(error);
|
|
689
733
|
const text = error instanceof Error ? error.message : String(error);
|
|
690
734
|
if (failure?.kind === "interrupted") {
|
|
691
|
-
console.error(red(`
|
|
735
|
+
console.error(red(`
|
|
692
736
|
The connection failed: ${text}`));
|
|
693
737
|
console.error(`Your work may already have been saved. Run the same command again —` +
|
|
694
738
|
` it will not create a second version.`);
|
|
@@ -890,7 +934,7 @@ async function commandGet(parsed) {
|
|
|
890
934
|
`changed while the interrupted fetch was stopped:`));
|
|
891
935
|
for (const file of changedInTheGap.slice(0, 20))
|
|
892
936
|
console.error(` ${file.path}`);
|
|
893
|
-
console.error(`
|
|
937
|
+
console.error(`
|
|
894
938
|
Save them with ${accent("cbx submit")}, or finish the fetch and ` +
|
|
895
939
|
`discard them with ${accent("cbx get --replace")}.`);
|
|
896
940
|
return 1;
|
|
@@ -1122,8 +1166,8 @@ async function suggestRules(folder, apply) {
|
|
|
1122
1166
|
const addition = (0, detect_js_1.rulesFromSuggestions)(recommended);
|
|
1123
1167
|
const shared = rules.shared.trimEnd();
|
|
1124
1168
|
await (0, worktree_js_1.writeRules)(folder, {
|
|
1125
|
-
shared: shared ? `${shared}
|
|
1126
|
-
|
|
1169
|
+
shared: shared ? `${shared}
|
|
1170
|
+
|
|
1127
1171
|
${addition}` : addition,
|
|
1128
1172
|
local: rules.local,
|
|
1129
1173
|
});
|
|
@@ -1250,7 +1294,7 @@ async function commandMerges(parsed) {
|
|
|
1250
1294
|
const counts = merge.conflicts;
|
|
1251
1295
|
console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
|
|
1252
1296
|
}
|
|
1253
|
-
console.log(dim(`
|
|
1297
|
+
console.log(dim(`
|
|
1254
1298
|
Run cbx merge <reference> to look at one.`));
|
|
1255
1299
|
return 0;
|
|
1256
1300
|
}
|
|
@@ -1316,7 +1360,7 @@ async function commandMerge(parsed) {
|
|
|
1316
1360
|
}
|
|
1317
1361
|
}
|
|
1318
1362
|
const now = await (0, api_js_1.mergeTrack)(summary.id);
|
|
1319
|
-
console.log(`
|
|
1363
|
+
console.log(`
|
|
1320
1364
|
${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
1321
1365
|
(now.provisional.ready
|
|
1322
1366
|
? "ready to apply"
|
|
@@ -1331,19 +1375,19 @@ ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
|
1331
1375
|
return 1;
|
|
1332
1376
|
}
|
|
1333
1377
|
const applied = await (0, api_js_1.applyMerge)(summary.id);
|
|
1334
|
-
console.log(`
|
|
1378
|
+
console.log(`
|
|
1335
1379
|
Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
1336
1380
|
console.log(dim("Run cbx get to bring it down to this folder."));
|
|
1337
1381
|
return 0;
|
|
1338
1382
|
}
|
|
1339
1383
|
if (!decision) {
|
|
1340
|
-
console.log(dim(`
|
|
1384
|
+
console.log(dim(`
|
|
1341
1385
|
--mine keeps yours, --theirs keeps what was already saved,` +
|
|
1342
|
-
` --drop removes the file.
|
|
1386
|
+
` --drop removes the file.
|
|
1343
1387
|
Add --path <file> for one file, then --apply when ready.`));
|
|
1344
1388
|
}
|
|
1345
1389
|
else if (now.provisional.ready) {
|
|
1346
|
-
console.log(dim(`
|
|
1390
|
+
console.log(dim(`
|
|
1347
1391
|
Run cbx merge ${now.mergeTrack.reference} --apply to publish it.`));
|
|
1348
1392
|
}
|
|
1349
1393
|
return 0;
|
|
@@ -1758,8 +1802,20 @@ const SPECS = [
|
|
|
1758
1802
|
group: "Your projects",
|
|
1759
1803
|
summary: "what has been saved to a project",
|
|
1760
1804
|
usage: "versions [project]",
|
|
1761
|
-
detail: "Newest first. Run it inside a linked folder to leave the name out
|
|
1762
|
-
|
|
1805
|
+
detail: "Newest first. Run it inside a linked folder to leave the name out.\n\n" +
|
|
1806
|
+
"A save is a commit; a commit somebody named is a version, and versions\n" +
|
|
1807
|
+
"are the only thing the public side shows. Both are listed together\n" +
|
|
1808
|
+
"unless you ask for one lane — the same three views the website has.",
|
|
1809
|
+
options: [
|
|
1810
|
+
{ flags: "--limit <n>", description: "how many to show (default 20)" },
|
|
1811
|
+
{ flags: "--versions", description: "only the ones that were named" },
|
|
1812
|
+
{ flags: "--commits", description: "only the ones that were not" },
|
|
1813
|
+
{ flags: "--notes", description: "print what each one says about itself" },
|
|
1814
|
+
],
|
|
1815
|
+
examples: [
|
|
1816
|
+
"cbx versions --versions",
|
|
1817
|
+
"cbx versions my-project --commits",
|
|
1818
|
+
],
|
|
1763
1819
|
run: project_commands_js_1.commandVersions,
|
|
1764
1820
|
},
|
|
1765
1821
|
{
|
|
@@ -1844,6 +1900,8 @@ const SPECS = [
|
|
|
1844
1900
|
options: [
|
|
1845
1901
|
{ flags: "--name <text>", description: "what to call it" },
|
|
1846
1902
|
{ flags: "--notes <text>", description: "what changed" },
|
|
1903
|
+
{ flags: "--notes-file <path>", description: "what changed, from a file" },
|
|
1904
|
+
{ flags: "--edit", description: "write what changed in your editor" },
|
|
1847
1905
|
],
|
|
1848
1906
|
examples: [
|
|
1849
1907
|
"cbx promote",
|
|
@@ -1867,6 +1925,34 @@ const SPECS = [
|
|
|
1867
1925
|
that works until somebody scripts it. This marks a save — pins it,
|
|
1868
1926
|
hides it, labels it — which is what it does anyway.
|
|
1869
1927
|
*/
|
|
1928
|
+
name: "notes",
|
|
1929
|
+
aliases: ["release-notes"],
|
|
1930
|
+
group: "Your projects",
|
|
1931
|
+
summary: "read or write what a version says about itself",
|
|
1932
|
+
usage: "notes [n] [project]",
|
|
1933
|
+
detail: "The release info the website shows on a version, and the same field\n" +
|
|
1934
|
+
"`--notes` writes when promoting. With nothing to write it prints what\n" +
|
|
1935
|
+
"is there, which the terminal could not do before: `cbx releases` shows\n" +
|
|
1936
|
+
"the first line and stops.\n\n" +
|
|
1937
|
+
"Written in Markdown, because that is what the page renders. --edit\n" +
|
|
1938
|
+
"opens $EDITOR (or $VISUAL, or notepad on Windows) the way git does,\n" +
|
|
1939
|
+
"which is the only comfortable way to write more than a sentence.",
|
|
1940
|
+
options: [
|
|
1941
|
+
{ flags: "--edit", description: "write it in your editor" },
|
|
1942
|
+
{ flags: "--notes-file <path>", description: "take the text from a file" },
|
|
1943
|
+
{ flags: "--notes <text>", description: "set it in one line" },
|
|
1944
|
+
{ flags: "--clear", description: "remove what is there" },
|
|
1945
|
+
{ flags: "--project <name>", description: "which project" },
|
|
1946
|
+
],
|
|
1947
|
+
examples: [
|
|
1948
|
+
"cbx notes",
|
|
1949
|
+
"cbx notes 41",
|
|
1950
|
+
"cbx notes 41 --edit",
|
|
1951
|
+
"cbx notes 41 --notes-file RELEASE.md",
|
|
1952
|
+
],
|
|
1953
|
+
run: version_commands_js_1.commandNotes,
|
|
1954
|
+
},
|
|
1955
|
+
{
|
|
1870
1956
|
name: "mark",
|
|
1871
1957
|
aliases: ["set"],
|
|
1872
1958
|
group: "Your projects",
|
|
@@ -1879,11 +1965,13 @@ const SPECS = [
|
|
|
1879
1965
|
{ flags: "--name <text>", description: "give it a code name" },
|
|
1880
1966
|
{ flags: "--unname", description: "take the name off again" },
|
|
1881
1967
|
{ flags: "--notes <text>", description: "what changed" },
|
|
1968
|
+
{ flags: "--notes-file <path>", description: "what changed, from a file" },
|
|
1969
|
+
{ flags: "--edit", description: "write what changed in your editor" },
|
|
1882
1970
|
{ flags: "--hide", description: "nobody outside the project can read it" },
|
|
1883
1971
|
{ flags: "--show", description: "anybody can read it" },
|
|
1884
1972
|
{
|
|
1885
1973
|
flags: "--visibility <who>",
|
|
1886
|
-
description: "
|
|
1974
|
+
description: "public, or private to hold it back",
|
|
1887
1975
|
},
|
|
1888
1976
|
{ flags: "--pin", description: "keep it at the top of the list" },
|
|
1889
1977
|
{ flags: "--unpin", description: "put it back in order" },
|
|
@@ -1892,7 +1980,7 @@ const SPECS = [
|
|
|
1892
1980
|
],
|
|
1893
1981
|
examples: [
|
|
1894
1982
|
"cbx mark --name v2.1 --pin",
|
|
1895
|
-
"cbx mark 41 --visibility
|
|
1983
|
+
"cbx mark 41 --visibility private",
|
|
1896
1984
|
'cbx mark 41 --labels "shipped,client work"',
|
|
1897
1985
|
],
|
|
1898
1986
|
run: version_commands_js_1.commandVersion,
|
|
@@ -2249,7 +2337,7 @@ async function main(argv) {
|
|
|
2249
2337
|
if (spec.deprecatedBy) {
|
|
2250
2338
|
console.error(dim(`"${name}" is now "${spec.deprecatedBy}". The old name still works.`));
|
|
2251
2339
|
}
|
|
2252
|
-
return spec.run(parse(rest));
|
|
2340
|
+
return spec.run(parse(rest, spec));
|
|
2253
2341
|
}
|
|
2254
2342
|
/**
|
|
2255
2343
|
* 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
|
|
@@ -131,10 +131,39 @@ async function commandVersions(parsed) {
|
|
|
131
131
|
console.log(dim("Nothing has been saved to this project yet."));
|
|
132
132
|
return 0;
|
|
133
133
|
}
|
|
134
|
+
/*
|
|
135
|
+
The same two lanes the website shows.
|
|
136
|
+
|
|
137
|
+
A version is a commit somebody named, so the lanes are a split of one
|
|
138
|
+
history rather than two histories — asking for commits gives the saves
|
|
139
|
+
still waiting on that decision, and asking for versions gives the ones
|
|
140
|
+
the public side can see. Neither contains the other; with no flag you get
|
|
141
|
+
both, which is what this always did.
|
|
142
|
+
*/
|
|
143
|
+
const onlyVersions = parsed.flags.get("versions") === true;
|
|
144
|
+
const onlyCommits = parsed.flags.get("commits") === true;
|
|
145
|
+
if (onlyVersions && onlyCommits) {
|
|
146
|
+
console.error(red("Pick one: --versions or --commits, or neither for both."));
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
const lane = saved.filter((version) => {
|
|
150
|
+
if (onlyVersions)
|
|
151
|
+
return Boolean(version.name);
|
|
152
|
+
if (onlyCommits)
|
|
153
|
+
return !version.name;
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
if (!lane.length) {
|
|
157
|
+
console.log(dim(onlyVersions
|
|
158
|
+
? "Nothing has been made a version yet."
|
|
159
|
+
: "Every save here has been made a version."));
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
const withNotes = parsed.flags.get("notes") === true;
|
|
134
163
|
const asked = Number(parsed.flags.get("limit") ?? 20);
|
|
135
164
|
const limit = Number.isFinite(asked) && asked > 0 ? asked : 20;
|
|
136
165
|
console.log(bold(project.name));
|
|
137
|
-
for (const version of
|
|
166
|
+
for (const version of lane.slice(0, limit)) {
|
|
138
167
|
const when = version.createdAt
|
|
139
168
|
? new Date(version.createdAt).toLocaleString()
|
|
140
169
|
: "";
|
|
@@ -155,8 +184,11 @@ async function commandVersions(parsed) {
|
|
|
155
184
|
version.state === "held" ? red("held") : "",
|
|
156
185
|
version.state === "declined" ? dim("declined") : "",
|
|
157
186
|
version.removedAt ? dim("taken down") : "",
|
|
158
|
-
|
|
159
|
-
|
|
187
|
+
/*
|
|
188
|
+
Said only of a version, because only a version is offered to anybody.
|
|
189
|
+
A commit nobody has promoted is not hidden — it was never on offer.
|
|
190
|
+
*/
|
|
191
|
+
version.name && version.visibility === "private" ? dim("hidden") : "",
|
|
160
192
|
...version.labels.map((label) => accent(label.name)),
|
|
161
193
|
].filter(Boolean);
|
|
162
194
|
console.log(` ${accent(`v${version.sequence}`).padEnd(16)} ${when.padEnd(22)}` +
|
|
@@ -164,9 +196,18 @@ async function commandVersions(parsed) {
|
|
|
164
196
|
(marks.length ? ` ${marks.join(" ")}` : ""));
|
|
165
197
|
if (version.message)
|
|
166
198
|
console.log(` ${dim(version.message)}`);
|
|
199
|
+
/*
|
|
200
|
+
Indented under the save it belongs to rather than printed flat, so a
|
|
201
|
+
note of several paragraphs still reads as part of one row.
|
|
202
|
+
*/
|
|
203
|
+
if (withNotes && version.notes?.trim()) {
|
|
204
|
+
for (const line of version.notes.trim().split(/\r?\n/)) {
|
|
205
|
+
console.log(` ${line}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
167
208
|
}
|
|
168
|
-
if (
|
|
169
|
-
console.log(dim(` … ${
|
|
209
|
+
if (lane.length > limit) {
|
|
210
|
+
console.log(dim(` … ${lane.length - limit} older. Use --limit to see more.`));
|
|
170
211
|
}
|
|
171
212
|
return 0;
|
|
172
213
|
}
|
|
@@ -16,6 +16,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.split = split;
|
|
19
|
+
exports.commandNotes = commandNotes;
|
|
19
20
|
exports.commandVersion = commandVersion;
|
|
20
21
|
exports.commandLabels = commandLabels;
|
|
21
22
|
exports.commandHeld = commandHeld;
|
|
@@ -26,6 +27,10 @@ exports.commandUndo = commandUndo;
|
|
|
26
27
|
const api_js_1 = require("./api.js");
|
|
27
28
|
const project_commands_js_1 = require("./project_commands.js");
|
|
28
29
|
const promises_1 = require("node:readline/promises");
|
|
30
|
+
const node_child_process_1 = require("node:child_process");
|
|
31
|
+
const node_fs_1 = require("node:fs");
|
|
32
|
+
const node_os_1 = require("node:os");
|
|
33
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
29
34
|
const node_process_1 = __importDefault(require("node:process"));
|
|
30
35
|
/* Written out rather than imported: the same four escapes every other command
|
|
31
36
|
file in here declares for itself, and a shared module for four one-line
|
|
@@ -34,6 +39,7 @@ const dim = (value) => `[2m${value}[0m`;
|
|
|
34
39
|
const bold = (value) => `[1m${value}[0m`;
|
|
35
40
|
const red = (value) => `[31m${value}[0m`;
|
|
36
41
|
const accent = (value) => `[33m${value}[0m`;
|
|
42
|
+
const green = (value) => `[32m${value}[0m`;
|
|
37
43
|
/**
|
|
38
44
|
* Which of `[n] [project]` the arguments actually were.
|
|
39
45
|
*
|
|
@@ -112,6 +118,148 @@ function colourOf(given) {
|
|
|
112
118
|
function paint(label) {
|
|
113
119
|
return accent(label.name);
|
|
114
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Release info, from wherever the writer keeps it.
|
|
123
|
+
*
|
|
124
|
+
* `--notes "..."` is fine for a sentence and hopeless for anything real: a
|
|
125
|
+
* release note has paragraphs and lists in it, and the shell trick people
|
|
126
|
+
* reach for — `--notes "$(cat NOTES.md)"` — is not a thing in PowerShell or
|
|
127
|
+
* cmd, which is most of this product's users. So the text can come from a
|
|
128
|
+
* file, or from the editor already set up for writing commit messages.
|
|
129
|
+
*
|
|
130
|
+
* Returns undefined when none of them were asked for, which is different from
|
|
131
|
+
* an empty string: one means leave it alone, the other means clear it.
|
|
132
|
+
*/
|
|
133
|
+
function notesFrom(parsed) {
|
|
134
|
+
const inline = parsed.flags.get("notes");
|
|
135
|
+
const file = parsed.flags.get("notes-file");
|
|
136
|
+
const edit = parsed.flags.get("edit") === true;
|
|
137
|
+
const chosen = [
|
|
138
|
+
typeof inline === "string",
|
|
139
|
+
typeof file === "string",
|
|
140
|
+
edit,
|
|
141
|
+
].filter(Boolean).length;
|
|
142
|
+
if (chosen > 1) {
|
|
143
|
+
throw new Error("Use one of --notes, --notes-file or --edit, not several.");
|
|
144
|
+
}
|
|
145
|
+
if (typeof inline === "string")
|
|
146
|
+
return inline;
|
|
147
|
+
if (typeof file === "string") {
|
|
148
|
+
try {
|
|
149
|
+
return (0, node_fs_1.readFileSync)(file, "utf8");
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
throw new Error(`Could not read ${file}.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/*
|
|
156
|
+
--edit is answered by the caller, which has the existing text to seed the
|
|
157
|
+
editor with. Nothing to report from here.
|
|
158
|
+
*/
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Write in the editor this person already uses, the way git does.
|
|
163
|
+
*
|
|
164
|
+
* The comment lines are stripped, so the instructions at the bottom of the
|
|
165
|
+
* file cannot end up published as part of the release. Quitting without
|
|
166
|
+
* saving leaves the notes untouched rather than clearing them, because an
|
|
167
|
+
* empty buffer is far more often a change of mind than an instruction.
|
|
168
|
+
*/
|
|
169
|
+
function writeInEditor(existing) {
|
|
170
|
+
const editor = node_process_1.default.env.CODEROOK_EDITOR ??
|
|
171
|
+
node_process_1.default.env.VISUAL ??
|
|
172
|
+
node_process_1.default.env.EDITOR ??
|
|
173
|
+
(node_process_1.default.platform === "win32" ? "notepad" : "nano");
|
|
174
|
+
const directory = (0, node_fs_1.mkdtempSync)(node_path_1.default.join((0, node_os_1.tmpdir)(), "cbx-notes-"));
|
|
175
|
+
const file = node_path_1.default.join(directory, "RELEASE_NOTES.md");
|
|
176
|
+
try {
|
|
177
|
+
(0, node_fs_1.writeFileSync)(file, `${existing}\n\n` +
|
|
178
|
+
"# Write what changed in this version, in Markdown.\n" +
|
|
179
|
+
"# Lines starting with # in the first column are removed.\n" +
|
|
180
|
+
"# Save an empty file to leave the notes as they were.\n", "utf8");
|
|
181
|
+
/*
|
|
182
|
+
No shell. Handing the arguments to one concatenates them into a command
|
|
183
|
+
line without escaping, which makes the editor setting — and the path
|
|
184
|
+
beside it — a place to hide a second command. An editor is a program
|
|
185
|
+
and some arguments, so it is split here and run directly, which also
|
|
186
|
+
means a path with a space in it survives.
|
|
187
|
+
*/
|
|
188
|
+
const parts = editor.match(/"[^"]+"|\S+/g) ?? [editor];
|
|
189
|
+
const unquote = (value) => value.replace(/^"|"$/g, "");
|
|
190
|
+
const run = (0, node_child_process_1.spawnSync)(unquote(parts[0] ?? editor), [...parts.slice(1).map(unquote), file], { stdio: "inherit" });
|
|
191
|
+
if (run.status !== 0)
|
|
192
|
+
return null;
|
|
193
|
+
const written = (0, node_fs_1.readFileSync)(file, "utf8")
|
|
194
|
+
.split(/\r?\n/)
|
|
195
|
+
.filter((line) => !/^#\s/.test(line) && line !== "#")
|
|
196
|
+
.join("\n")
|
|
197
|
+
.trim();
|
|
198
|
+
return written ? written : null;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
(0, node_fs_1.rmSync)(directory, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Read or write what a version says about itself.
|
|
209
|
+
*
|
|
210
|
+
* The website grew a place to write this and the terminal could only set it
|
|
211
|
+
* as a one-line flag while promoting, and could not read it back at all —
|
|
212
|
+
* `cbx releases` prints the first line and stops. Somebody working here could
|
|
213
|
+
* publish release info but never check what they had published.
|
|
214
|
+
*/
|
|
215
|
+
async function commandNotes(parsed) {
|
|
216
|
+
const which = split(parsed);
|
|
217
|
+
const project = await (0, project_commands_js_1.resolveProject)(which.project);
|
|
218
|
+
if (!project)
|
|
219
|
+
return 1;
|
|
220
|
+
const target = await pick(project.id, which.n);
|
|
221
|
+
if (!target)
|
|
222
|
+
return 1;
|
|
223
|
+
if (parsed.flags.get("clear") === true) {
|
|
224
|
+
await (0, api_js_1.changeVersion)(project.id, target.id, { notes: null });
|
|
225
|
+
console.log(green(`Cleared the notes on v${target.sequence}.`));
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
let next;
|
|
229
|
+
try {
|
|
230
|
+
next = notesFrom(parsed);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
if (parsed.flags.get("edit") === true) {
|
|
237
|
+
const written = writeInEditor(target.notes ?? "");
|
|
238
|
+
if (written === null) {
|
|
239
|
+
console.log(dim("Left as it was."));
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
next = written;
|
|
243
|
+
}
|
|
244
|
+
if (next === undefined) {
|
|
245
|
+
/* Nothing to write, so this is a read. */
|
|
246
|
+
const text = (target.notes ?? "").trim();
|
|
247
|
+
if (!text) {
|
|
248
|
+
console.log(dim(`v${target.sequence} has no notes.`) +
|
|
249
|
+
dim(" Write some with --edit, --notes-file or --notes"));
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
console.log(bold(`v${target.sequence}${target.name ? ` ${target.name}` : ""}`));
|
|
253
|
+
console.log(text);
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
const text = (next ?? "").trim();
|
|
257
|
+
await (0, api_js_1.changeVersion)(project.id, target.id, { notes: text ? next : null });
|
|
258
|
+
console.log(green(text
|
|
259
|
+
? `Wrote the notes on v${target.sequence}.`
|
|
260
|
+
: `Cleared the notes on v${target.sequence}.`));
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
115
263
|
async function commandVersion(parsed) {
|
|
116
264
|
const which = split(parsed);
|
|
117
265
|
const project = await (0, project_commands_js_1.resolveProject)(which.project);
|
|
@@ -126,16 +274,27 @@ async function commandVersion(parsed) {
|
|
|
126
274
|
patch.name = name;
|
|
127
275
|
if (parsed.flags.get("unname") === true)
|
|
128
276
|
patch.name = null;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
277
|
+
try {
|
|
278
|
+
const notes = notesFrom(parsed);
|
|
279
|
+
if (notes !== undefined)
|
|
280
|
+
patch.notes = notes;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
if (parsed.flags.get("edit") === true) {
|
|
287
|
+
const written = writeInEditor(target.notes ?? "");
|
|
288
|
+
if (written !== null)
|
|
289
|
+
patch.notes = written;
|
|
290
|
+
}
|
|
132
291
|
if (parsed.flags.get("hide") === true)
|
|
133
292
|
patch.visibility = "private";
|
|
134
293
|
if (parsed.flags.get("show") === true)
|
|
135
294
|
patch.visibility = "public";
|
|
136
295
|
const visibility = parsed.flags.get("visibility");
|
|
137
296
|
if (typeof visibility === "string") {
|
|
138
|
-
if (!["private", "
|
|
297
|
+
if (!["private", "public"].includes(visibility)) {
|
|
139
298
|
console.error(red("Visibility is private, unlisted or public."));
|
|
140
299
|
return 1;
|
|
141
300
|
}
|
|
@@ -229,6 +229,7 @@ class Downloader {
|
|
|
229
229
|
...(row.objectId ? { objectId: String(row.objectId) } : {}),
|
|
230
230
|
sha256: String(row.sha256 ?? ""),
|
|
231
231
|
sourceSize: Number(row.sourceSize ?? 0),
|
|
232
|
+
...(row.sealed === true ? { sealed: true } : {}),
|
|
232
233
|
...(packed?.objectId
|
|
233
234
|
? {
|
|
234
235
|
pack: {
|
|
@@ -298,7 +299,7 @@ class Downloader {
|
|
|
298
299
|
const packed = new Map();
|
|
299
300
|
for (const file of files) {
|
|
300
301
|
const id = file.pack?.objectId;
|
|
301
|
-
if (!id)
|
|
302
|
+
if (!id || file.sealed)
|
|
302
303
|
continue;
|
|
303
304
|
const group = packed.get(id) ?? [];
|
|
304
305
|
group.push(file);
|
|
@@ -323,7 +324,7 @@ class Downloader {
|
|
|
323
324
|
for (const file of files) {
|
|
324
325
|
this.check();
|
|
325
326
|
progress(file.path);
|
|
326
|
-
const packId = file.pack?.objectId;
|
|
327
|
+
const packId = file.sealed ? undefined : file.pack?.objectId;
|
|
327
328
|
if (packId && restoredPacks.has(packId))
|
|
328
329
|
continue;
|
|
329
330
|
if (packId) {
|
|
@@ -372,7 +373,14 @@ class Downloader {
|
|
|
372
373
|
}
|
|
373
374
|
async fetchInto(repositoryId, versionId, file, target) {
|
|
374
375
|
const whole = (0, node_crypto_1.createHash)("sha256");
|
|
375
|
-
|
|
376
|
+
/*
|
|
377
|
+
A sealed file is read whole, by path, whatever shape it is stored in.
|
|
378
|
+
|
|
379
|
+
Its pieces and its object are the covered bytes; only the per-path route
|
|
380
|
+
puts the real value back. Reading it any other way fetches something
|
|
381
|
+
that cannot match the digest this file was listed with.
|
|
382
|
+
*/
|
|
383
|
+
const pieces = file.sealed ? [] : (file.chunks ?? []);
|
|
376
384
|
// Captured so the generators below do not need `this` rebound.
|
|
377
385
|
const request = (route) => this.request(route);
|
|
378
386
|
const check = () => this.check();
|
|
@@ -399,7 +407,7 @@ class Downloader {
|
|
|
399
407
|
}
|
|
400
408
|
})()
|
|
401
409
|
: (async function* () {
|
|
402
|
-
const reply = await request(file.objectId
|
|
410
|
+
const reply = await request(file.objectId && !file.sealed
|
|
403
411
|
? `/v1/repositories/${repositoryId}/objects/${file.objectId}`
|
|
404
412
|
: `/v1/repositories/${repositoryId}/versions/${versionId}/file` +
|
|
405
413
|
`?path=${encodeURIComponent(file.path)}`);
|
|
@@ -456,7 +464,8 @@ class Downloader {
|
|
|
456
464
|
let bytes = 0;
|
|
457
465
|
const packed = new Map();
|
|
458
466
|
for (const file of files) {
|
|
459
|
-
|
|
467
|
+
/* Sealed members never come out of the pack — see `sealed` above. */
|
|
468
|
+
const id = file.sealed ? undefined : file.pack?.objectId;
|
|
460
469
|
if (!id)
|
|
461
470
|
continue;
|
|
462
471
|
const group = packed.get(id) ?? [];
|
|
@@ -479,7 +488,7 @@ class Downloader {
|
|
|
479
488
|
if (parts.some((part) => part === ".." || part.includes("\0"))) {
|
|
480
489
|
throw new Error(`That version contains an unsafe path: ${file.path}`);
|
|
481
490
|
}
|
|
482
|
-
const packId = file.pack?.objectId;
|
|
491
|
+
const packId = file.sealed ? undefined : file.pack?.objectId;
|
|
483
492
|
if (packId && restoredPacks.has(packId))
|
|
484
493
|
continue;
|
|
485
494
|
if (packId) {
|
|
@@ -66,6 +66,24 @@ class Tracks {
|
|
|
66
66
|
const body = await this.call(`/v1/repositories/${repositoryId}/versions`);
|
|
67
67
|
return body.versions ?? [];
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* What the account says about this project, as distinct from this folder.
|
|
71
|
+
*
|
|
72
|
+
* The local record knows where a folder saves to and nothing about how the
|
|
73
|
+
* project is published — so the history window could show which saves were
|
|
74
|
+
* public without being able to say whether the project itself was, which is
|
|
75
|
+
* the half that decides whether anybody can reach them.
|
|
76
|
+
*/
|
|
77
|
+
async project(repositoryId) {
|
|
78
|
+
try {
|
|
79
|
+
const body = await this.call(`/v1/repositories/${repositoryId}`);
|
|
80
|
+
return { visibility: body.visibility ?? null };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* Not knowing is not worth failing the window for. */
|
|
84
|
+
return { visibility: null };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
69
87
|
async labels(repositoryId) {
|
|
70
88
|
const body = await this.call(`/v1/repositories/${repositoryId}/labels`);
|
|
71
89
|
return body.labels ?? [];
|
|
@@ -1163,6 +1163,10 @@ class Uploader {
|
|
|
1163
1163
|
...(request.track ? { track: request.track } : {}),
|
|
1164
1164
|
...(request.allowIgnored ? { allowIgnored: true } : {}),
|
|
1165
1165
|
...(request.allowSecrets ? { allowSecrets: true } : {}),
|
|
1166
|
+
...(request.linesAdded === undefined ? {} : { linesAdded: request.linesAdded }),
|
|
1167
|
+
...(request.linesRemoved === undefined
|
|
1168
|
+
? {}
|
|
1169
|
+
: { linesRemoved: request.linesRemoved }),
|
|
1166
1170
|
/*
|
|
1167
1171
|
Names this attempt so a retry after a lost connection is answered
|
|
1168
1172
|
with the version already made, rather than making a second one.
|
|
@@ -999,9 +999,26 @@ async function uploadConcerns(root, include) {
|
|
|
999
999
|
* matches no service's format and so is invisible to the pattern scan — the
|
|
1000
1000
|
* only thing that identifies it is the name of the file it is sitting in.
|
|
1001
1001
|
*/
|
|
1002
|
+
/**
|
|
1003
|
+
* The `.env.<something>` files that exist to be committed.
|
|
1004
|
+
*
|
|
1005
|
+
* `.env.example` is the file a project is *supposed* to publish — it is the
|
|
1006
|
+
* documentation of which variables exist, with the values left empty. Refusing
|
|
1007
|
+
* to send it, and then advising it be added to .gitignore, is advice that
|
|
1008
|
+
* breaks the project for the next person who clones it.
|
|
1009
|
+
*
|
|
1010
|
+
* The same list the service uses when it decides what to cover, so a file is
|
|
1011
|
+
* not a template in one half of the system and a credential in the other.
|
|
1012
|
+
*/
|
|
1013
|
+
const TEMPLATE_SUFFIXES = [".example", ".sample", ".template", ".dist", ".defaults"];
|
|
1014
|
+
function isTemplateName(name) {
|
|
1015
|
+
return TEMPLATE_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
1016
|
+
}
|
|
1002
1017
|
function isCredentialByName(relativePath) {
|
|
1003
1018
|
const parts = relativePath.split("/");
|
|
1004
1019
|
const name = (parts[parts.length - 1] ?? "").toLowerCase();
|
|
1020
|
+
if (isTemplateName(name))
|
|
1021
|
+
return false;
|
|
1005
1022
|
return (SECRET_NAMES.has(name) ||
|
|
1006
1023
|
name.startsWith(".env.") ||
|
|
1007
1024
|
SECRET_SUFFIXES.some((suffix) => name.endsWith(suffix)) ||
|
|
@@ -1032,14 +1049,15 @@ async function detectSecrets(root) {
|
|
|
1032
1049
|
continue;
|
|
1033
1050
|
}
|
|
1034
1051
|
seen += 1;
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1052
|
+
/*
|
|
1053
|
+
Asked of the one function rather than repeated here. These were two
|
|
1054
|
+
copies of the same rule, which is how `.env.example` came to be
|
|
1055
|
+
refused by the command line long after the service had learned that
|
|
1056
|
+
templates are not credentials.
|
|
1057
|
+
*/
|
|
1058
|
+
const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
|
|
1059
|
+
if (isCredentialByName(relative))
|
|
1060
|
+
found.push(relative);
|
|
1043
1061
|
}
|
|
1044
1062
|
}
|
|
1045
1063
|
return found;
|