@coderook/cli 0.34.5 → 0.34.6
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.
|
@@ -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, with cbx or plain git, from Claude Code.",
|
|
5
|
-
"version": "0.34.
|
|
5
|
+
"version": "0.34.6",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
|
@@ -21,6 +21,7 @@ exports.LICENCE_FILE = exports.DEFAULT_LICENCE = exports.LICENCES = void 0;
|
|
|
21
21
|
exports.licenceById = licenceById;
|
|
22
22
|
exports.licenceText = licenceText;
|
|
23
23
|
exports.detectLicence = detectLicence;
|
|
24
|
+
exports.existingLicence = existingLicence;
|
|
24
25
|
exports.licenceNewProject = licenceNewProject;
|
|
25
26
|
const promises_1 = require("node:fs/promises");
|
|
26
27
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -281,6 +282,23 @@ function detectLicence(text) {
|
|
|
281
282
|
*
|
|
282
283
|
* Returns the licence written, or null when nothing was.
|
|
283
284
|
*/
|
|
285
|
+
/*
|
|
286
|
+
The names a licence goes by, as a file or as a folder of them.
|
|
287
|
+
|
|
288
|
+
Only the exact name LICENSE was looked for, which missed nearly every way a
|
|
289
|
+
project actually carries its licence — LICENSE.md, LICENCE.txt, COPYING,
|
|
290
|
+
the LICENSE-MIT and LICENSE-APACHE pair, a licenses folder. Found by saving
|
|
291
|
+
a copy of Blender: its licences live in a folder called `license`, which on
|
|
292
|
+
Windows answers to "LICENSE" as well, so the check opened a directory and
|
|
293
|
+
the save failed. Had the folder been named anything else, the save would
|
|
294
|
+
have put an MIT licence beside Blender's GPL one.
|
|
295
|
+
*/
|
|
296
|
+
const LICENCE_NAME = /^(licen[cs]es?|copying|copyright|unlicen[cs]e)([-._ ].*)?$/i;
|
|
297
|
+
/** The licence file or folder this folder already has, if any. */
|
|
298
|
+
async function existingLicence(folder) {
|
|
299
|
+
const names = await (0, promises_1.readdir)(folder).catch(() => []);
|
|
300
|
+
return names.find((name) => LICENCE_NAME.test(name)) ?? null;
|
|
301
|
+
}
|
|
284
302
|
async function licenceNewProject(folder, holder, skip = false) {
|
|
285
303
|
/*
|
|
286
304
|
No name, no licence. A copyright line naming nobody is worse than the
|
|
@@ -289,10 +307,19 @@ async function licenceNewProject(folder, holder, skip = false) {
|
|
|
289
307
|
*/
|
|
290
308
|
if (skip || !holder.trim())
|
|
291
309
|
return null;
|
|
310
|
+
/* Any licence at all is somebody's decision, whatever it is called. */
|
|
311
|
+
if (await existingLicence(folder))
|
|
312
|
+
return null;
|
|
292
313
|
const target = node_path_1.default.join(folder, exports.LICENCE_FILE);
|
|
293
|
-
|
|
294
|
-
|
|
314
|
+
/*
|
|
315
|
+
A nudge, never a reason to fail. A folder that cannot be written to is
|
|
316
|
+
saved without one, exactly as it would have been before this existed.
|
|
317
|
+
*/
|
|
318
|
+
try {
|
|
319
|
+
await (0, promises_1.writeFile)(target, licenceText(exports.DEFAULT_LICENCE, holder.trim()), { encoding: "utf8", flag: "wx" });
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
295
322
|
return null;
|
|
296
|
-
|
|
323
|
+
}
|
|
297
324
|
return exports.DEFAULT_LICENCE;
|
|
298
325
|
}
|
|
@@ -932,6 +932,11 @@ class Uploader {
|
|
|
932
932
|
objects are immutable and content-addressed, so a retry reuses them.
|
|
933
933
|
*/
|
|
934
934
|
(0, faults_js_1.maybeFail)("submit:after-objects");
|
|
935
|
+
const counted = request.countLines
|
|
936
|
+
? await (0, profile_js_1.timed)("count the changed lines", () => request.countLines()).catch(() => null)
|
|
937
|
+
: null;
|
|
938
|
+
const linesAdded = counted ? counted.added : request.linesAdded;
|
|
939
|
+
const linesRemoved = counted ? counted.removed : request.linesRemoved;
|
|
935
940
|
const completed = await (0, profile_js_1.timed)("publish the version", () => this.call(`/v1/repositories/${repositoryId}/versions`, {
|
|
936
941
|
method: "POST",
|
|
937
942
|
contentType: "application/json",
|
|
@@ -950,10 +955,8 @@ class Uploader {
|
|
|
950
955
|
...(request.allowIgnored ? { allowIgnored: true } : {}),
|
|
951
956
|
...(this.trackedByGit.length ? { trackedByGit: this.trackedByGit } : {}),
|
|
952
957
|
...(request.allowSecrets ? { allowSecrets: true } : {}),
|
|
953
|
-
...(
|
|
954
|
-
...(
|
|
955
|
-
? {}
|
|
956
|
-
: { linesRemoved: request.linesRemoved }),
|
|
958
|
+
...(linesAdded === undefined ? {} : { linesAdded }),
|
|
959
|
+
...(linesRemoved === undefined ? {} : { linesRemoved }),
|
|
957
960
|
/*
|
|
958
961
|
Names this attempt so a retry after a lost connection is answered
|
|
959
962
|
with the version already made, rather than making a second one.
|
|
@@ -1155,6 +1158,22 @@ class Uploader {
|
|
|
1155
1158
|
question rather than a transfer.
|
|
1156
1159
|
*/
|
|
1157
1160
|
const inFlightByDigest = new Map();
|
|
1161
|
+
/*
|
|
1162
|
+
What the account already holds, asked once for the whole save.
|
|
1163
|
+
|
|
1164
|
+
Asked per section, the answer changed under the save's feet. The plan
|
|
1165
|
+
stores nothing, so every section of the plan heard the same answer; the
|
|
1166
|
+
send stores as it goes and runs sections side by side, so a later
|
|
1167
|
+
section holding a copy of a file an earlier one had just sent heard
|
|
1168
|
+
"already held" and left it out of its pack. The pack's bytes, and so its
|
|
1169
|
+
digest, were then not the pack the plan had priced, and the service
|
|
1170
|
+
refused it as "a file that was not part of what it asked to send". A
|
|
1171
|
+
copy of Blender 4.5 — 326 contents present more than once — failed this
|
|
1172
|
+
way every time. One answer, taken before anything is sent, is the answer
|
|
1173
|
+
the plan was given.
|
|
1174
|
+
*/
|
|
1175
|
+
let heldOnce = null;
|
|
1176
|
+
const heldAtStart = () => (heldOnce ??= (0, profile_js_1.timed)("ask what is already held", () => this.storedInBulk(repositoryId, everyDeclaration.map((one) => one.sha256))));
|
|
1158
1177
|
const runSection = async (section) => {
|
|
1159
1178
|
const declarations = section.files.map((file) => declarationByPath.get(file.path));
|
|
1160
1179
|
/*
|
|
@@ -1214,7 +1233,7 @@ class Uploader {
|
|
|
1214
1233
|
asking took longer than the transfer. The answer per digest is a boolean
|
|
1215
1234
|
and an identifier, so the whole question fits in one request.
|
|
1216
1235
|
*/
|
|
1217
|
-
const alreadyHeld = await (
|
|
1236
|
+
const alreadyHeld = await heldAtStart();
|
|
1218
1237
|
/*
|
|
1219
1238
|
The small files go first, packed together.
|
|
1220
1239
|
|
|
@@ -33,6 +33,7 @@ exports.detectPrivateDirectories = detectPrivateDirectories;
|
|
|
33
33
|
exports.detectPastedCredentials = detectPastedCredentials;
|
|
34
34
|
exports.uploadConcerns = uploadConcerns;
|
|
35
35
|
exports.isCredentialByName = isCredentialByName;
|
|
36
|
+
exports.holdsOnlyCertificates = holdsOnlyCertificates;
|
|
36
37
|
exports.detectSecrets = detectSecrets;
|
|
37
38
|
/** Reading a project folder: changed files, diffs, and rule measurement. */
|
|
38
39
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -1737,6 +1738,13 @@ onScanned) {
|
|
|
1737
1738
|
const wanted = include.filter((relative) => (0, secret_patterns_js_1.worthReading)(relative.slice(relative.lastIndexOf("/") + 1)));
|
|
1738
1739
|
let cursor = 0;
|
|
1739
1740
|
let stop = false;
|
|
1741
|
+
/*
|
|
1742
|
+
Every file looked at, read or passed over for its size. Reporting only
|
|
1743
|
+
those read left the count short of its own total: a refused save of
|
|
1744
|
+
Blender stopped on "3700 of 3747 files", which reads as a scan that gave
|
|
1745
|
+
up rather than one that finished.
|
|
1746
|
+
*/
|
|
1747
|
+
let looked = 0;
|
|
1740
1748
|
const lane = async () => {
|
|
1741
1749
|
for (;;) {
|
|
1742
1750
|
if (stop)
|
|
@@ -1745,6 +1753,9 @@ onScanned) {
|
|
|
1745
1753
|
cursor += 1;
|
|
1746
1754
|
if (at >= wanted.length)
|
|
1747
1755
|
return;
|
|
1756
|
+
looked += 1;
|
|
1757
|
+
if (onScanned && looked % 50 === 0)
|
|
1758
|
+
onScanned(looked, wanted.length);
|
|
1748
1759
|
if (found.length >= MOST_FINDINGS) {
|
|
1749
1760
|
stop = true;
|
|
1750
1761
|
return;
|
|
@@ -1768,8 +1779,6 @@ onScanned) {
|
|
|
1768
1779
|
}
|
|
1769
1780
|
read += 1;
|
|
1770
1781
|
bytes += contents.byteLength;
|
|
1771
|
-
if (onScanned && read % 50 === 0)
|
|
1772
|
-
onScanned(read, wanted.length);
|
|
1773
1782
|
if (!(0, secret_patterns_js_1.looksLikeText)(contents))
|
|
1774
1783
|
continue;
|
|
1775
1784
|
const credentials = (0, secret_patterns_js_1.findCredentials)(contents.toString("utf8"));
|
|
@@ -1778,6 +1787,8 @@ onScanned) {
|
|
|
1778
1787
|
}
|
|
1779
1788
|
};
|
|
1780
1789
|
await Promise.all(Array.from({ length: Math.min(LANES, wanted.length) }, () => lane()));
|
|
1790
|
+
if (onScanned && wanted.length)
|
|
1791
|
+
onScanned(looked, wanted.length);
|
|
1781
1792
|
/*
|
|
1782
1793
|
`stop` is also set by the findings ceiling, which is a different statement
|
|
1783
1794
|
from running out of budget — forty findings is plenty to act on, and the
|
|
@@ -1847,6 +1858,38 @@ function isCredentialByName(relativePath) {
|
|
|
1847
1858
|
SECRET_SUFFIXES.some((suffix) => name.endsWith(suffix)) ||
|
|
1848
1859
|
parts.slice(0, -1).includes("secrets"));
|
|
1849
1860
|
}
|
|
1861
|
+
/*
|
|
1862
|
+
A `.pem` is a container, not a kind of file.
|
|
1863
|
+
|
|
1864
|
+
It holds a private key as readily as a certificate, and the name cannot
|
|
1865
|
+
tell the two apart — so every one was refused. That refused Blender: its
|
|
1866
|
+
bundled Python carries `certifi/cacert.pem` twice, Mozilla's list of the
|
|
1867
|
+
certificate authorities a program should trust, which ships inside every
|
|
1868
|
+
pip there is and is public by design. A certificate is the half of a key
|
|
1869
|
+
pair that exists to be handed out.
|
|
1870
|
+
|
|
1871
|
+
So a `.pem` is opened and judged by its blocks. Only certificates (and
|
|
1872
|
+
revocation lists) clear it; a private key anywhere in it, a block of any
|
|
1873
|
+
other kind, no blocks at all, or a file too large or unreadable to look
|
|
1874
|
+
at all leave it a credential, as before.
|
|
1875
|
+
*/
|
|
1876
|
+
const CERTIFICATE_BUNDLE_LIMIT = 4 * 1024 * 1024;
|
|
1877
|
+
async function holdsOnlyCertificates(full) {
|
|
1878
|
+
try {
|
|
1879
|
+
const info = await (0, promises_1.stat)(full);
|
|
1880
|
+
if (info.size > CERTIFICATE_BUNDLE_LIMIT)
|
|
1881
|
+
return false;
|
|
1882
|
+
const text = await (0, promises_1.readFile)(full, "latin1");
|
|
1883
|
+
if (/PRIVATE KEY/i.test(text))
|
|
1884
|
+
return false;
|
|
1885
|
+
const blocks = text.match(/-----BEGIN [A-Z0-9 ]+-----/g) ?? [];
|
|
1886
|
+
return (blocks.length > 0 &&
|
|
1887
|
+
blocks.every((block) => /^-----BEGIN (TRUSTED |X509 )?(CERTIFICATE|CRL)-----$/.test(block)));
|
|
1888
|
+
}
|
|
1889
|
+
catch {
|
|
1890
|
+
return false;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1850
1893
|
async function detectSecrets(root) {
|
|
1851
1894
|
const found = [];
|
|
1852
1895
|
let seen = 0;
|
|
@@ -1896,8 +1939,12 @@ async function detectSecrets(root) {
|
|
|
1896
1939
|
that templates are not credentials.
|
|
1897
1940
|
*/
|
|
1898
1941
|
const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
|
|
1899
|
-
if (isCredentialByName(relative))
|
|
1900
|
-
|
|
1942
|
+
if (!isCredentialByName(relative))
|
|
1943
|
+
continue;
|
|
1944
|
+
if (entry.name.toLowerCase().endsWith(".pem") && (await holdsOnlyCertificates(full))) {
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
found.push(relative);
|
|
1901
1948
|
}
|
|
1902
1949
|
}
|
|
1903
1950
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.6",
|
|
4
4
|
"description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://coderook.com",
|