@coderook/cli 0.34.3 → 0.34.4
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/cbx/src/core/upload.js +158 -121
- package/dist/cli/src/network.js +25 -5
- 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, with cbx or plain git, from Claude Code.",
|
|
5
|
-
"version": "0.34.
|
|
5
|
+
"version": "0.34.4",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
|
@@ -1081,6 +1081,16 @@ class Uploader {
|
|
|
1081
1081
|
shown = count;
|
|
1082
1082
|
return shown;
|
|
1083
1083
|
};
|
|
1084
|
+
/*
|
|
1085
|
+
Identical files are one upload.
|
|
1086
|
+
|
|
1087
|
+
A folder holding two copies of the same file declares it twice, and both
|
|
1088
|
+
copies used to go up in full: side by side in one batch, or in two lanes
|
|
1089
|
+
at the same moment. The second copy now waits for the first to finish,
|
|
1090
|
+
after which the service already holds the bytes and the copy costs a
|
|
1091
|
+
question rather than a transfer.
|
|
1092
|
+
*/
|
|
1093
|
+
const sending = new Map();
|
|
1084
1094
|
const runSection = async (section) => {
|
|
1085
1095
|
const declarations = section.files.map((file) => declarationByPath.get(file.path));
|
|
1086
1096
|
/*
|
|
@@ -1151,6 +1161,17 @@ class Uploader {
|
|
|
1151
1161
|
*/
|
|
1152
1162
|
const missing = declarations.filter((one) => !alreadyHeld.has(one.sha256));
|
|
1153
1163
|
const smallOnes = missing.filter((one) => one.size <= BATCH_FILE_LIMIT);
|
|
1164
|
+
/* The first file with each digest is sent; the rest are counted when it lands. */
|
|
1165
|
+
const copiesOf = new Map();
|
|
1166
|
+
const smallUnique = smallOnes.filter((one) => {
|
|
1167
|
+
const copies = copiesOf.get(one.sha256);
|
|
1168
|
+
if (copies) {
|
|
1169
|
+
copies.push(one);
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
copiesOf.set(one.sha256, []);
|
|
1173
|
+
return true;
|
|
1174
|
+
});
|
|
1154
1175
|
// 3. Send the objects.
|
|
1155
1176
|
/*
|
|
1156
1177
|
What was sent, in whichever shape it was sent. A small file is one
|
|
@@ -1177,7 +1198,7 @@ class Uploader {
|
|
|
1177
1198
|
(0, profile_js_1.counted)("files sent as deltas", 1);
|
|
1178
1199
|
}
|
|
1179
1200
|
}
|
|
1180
|
-
const smallForPacking =
|
|
1201
|
+
const smallForPacking = smallUnique.filter((one) => !batched.has(one.sha256));
|
|
1181
1202
|
if (smallForPacking.length > 1) {
|
|
1182
1203
|
let pack = [];
|
|
1183
1204
|
let packBytes = 0;
|
|
@@ -1281,6 +1302,8 @@ class Uploader {
|
|
|
1281
1302
|
if (!batched.has(digest) && !packedInto.has(digest))
|
|
1282
1303
|
continue;
|
|
1283
1304
|
progress.finished(item.declaration.size, exactWireBytes === null ? item.encoded.body.byteLength : 0);
|
|
1305
|
+
for (const copy of copiesOf.get(digest) ?? [])
|
|
1306
|
+
progress.finished(copy.size, 0);
|
|
1284
1307
|
}
|
|
1285
1308
|
if (exactWireBytes !== null)
|
|
1286
1309
|
progress.finished(0, exactWireBytes);
|
|
@@ -1336,121 +1359,51 @@ class Uploader {
|
|
|
1336
1359
|
const queue = declarations.entries();
|
|
1337
1360
|
const runWorker = async () => {
|
|
1338
1361
|
for (const [index, declaration] of queue) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
percent: 20 + Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1348
|
-
bytesPerSecond: progress.rate(),
|
|
1349
|
-
});
|
|
1350
|
-
/*
|
|
1351
|
-
Content the service already holds does not need sending. Asking costs
|
|
1352
|
-
one round trip and saves the whole file — which after an interrupted
|
|
1353
|
-
save is the difference between re-uploading a project and re-uploading
|
|
1354
|
-
nothing.
|
|
1355
|
-
*/
|
|
1356
|
-
/*
|
|
1357
|
-
A large file goes up in pieces, cut where its own content says.
|
|
1358
|
-
|
|
1359
|
-
This is what CBX is for and what it was never doing on the wire. The
|
|
1360
|
-
boundaries come from the same chunker the .cbx bundle uses, so an
|
|
1361
|
-
edit in the middle of a large file leaves the chunks either side of it
|
|
1362
|
-
untouched — and an untouched chunk is one the service already holds,
|
|
1363
|
-
which costs a question rather than a transfer. A small file is left
|
|
1364
|
-
whole: cutting it up would trade one request for several and save
|
|
1365
|
-
nothing.
|
|
1366
|
-
*/
|
|
1367
|
-
const chunkProfile = (await this.microchunkAllowed())
|
|
1368
|
-
? (0, chunking_js_1.microchunkProfileForFileSize)(declaration.size)
|
|
1369
|
-
: (0, chunking_js_1.profileForFileSize)(declaration.size);
|
|
1370
|
-
if (chunkProfile &&
|
|
1371
|
-
declaration.size >= CHUNK_THRESHOLD &&
|
|
1372
|
-
(await this.chunkingAllowed())) {
|
|
1373
|
-
const pieces = await this.putChunked(repositoryId, declaration, chunkProfile, (offset) => {
|
|
1374
|
-
progress.lane(index, offset);
|
|
1375
|
-
report({
|
|
1376
|
-
stage: "upload",
|
|
1377
|
-
files: advance(index),
|
|
1378
|
-
totalFiles: everyDeclaration.length,
|
|
1379
|
-
bytes: progress.bytes(),
|
|
1380
|
-
totalBytes,
|
|
1381
|
-
path: declaration.path,
|
|
1382
|
-
percent: 20 +
|
|
1383
|
-
Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1384
|
-
bytesPerSecond: progress.rate(),
|
|
1385
|
-
});
|
|
1386
|
-
});
|
|
1387
|
-
progress.laneDone(index);
|
|
1388
|
-
/*
|
|
1389
|
-
A chunk is not a file. Counting every reused piece here made a
|
|
1390
|
-
one-file save report a negative number of sent files. The selected
|
|
1391
|
-
file counts as already stored only when every one of its pieces was
|
|
1392
|
-
reused and no payload crossed the wire.
|
|
1393
|
-
*/
|
|
1394
|
-
if (pieces.sentBytes === 0)
|
|
1395
|
-
progress.alreadyHeld();
|
|
1396
|
-
uploaded.push({
|
|
1397
|
-
path: declaration.path,
|
|
1398
|
-
sha256: declaration.sha256,
|
|
1399
|
-
chunks: pieces.chunks,
|
|
1400
|
-
sourceSize: declaration.size,
|
|
1401
|
-
storedSize: pieces.chunks.reduce((sum, one) => sum + one.storedSize, 0),
|
|
1402
|
-
mediaType: declaration.mediaType,
|
|
1403
|
-
});
|
|
1404
|
-
progress.finished(declaration.size, pieces.sentBytes);
|
|
1405
|
-
continue;
|
|
1362
|
+
const earlier = sending.get(declaration.sha256);
|
|
1363
|
+
if (earlier)
|
|
1364
|
+
await earlier;
|
|
1365
|
+
let release = () => { };
|
|
1366
|
+
if (!earlier) {
|
|
1367
|
+
sending.set(declaration.sha256, new Promise((done) => {
|
|
1368
|
+
release = done;
|
|
1369
|
+
}));
|
|
1406
1370
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
storedSize: fromPack.storedSize,
|
|
1416
|
-
mediaType: declaration.mediaType,
|
|
1417
|
-
});
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
const fromBatch = batched.get(declaration.sha256);
|
|
1421
|
-
if (fromBatch) {
|
|
1422
|
-
uploaded.push({
|
|
1371
|
+
try {
|
|
1372
|
+
this.check();
|
|
1373
|
+
report({
|
|
1374
|
+
stage: "upload",
|
|
1375
|
+
files: advance(index),
|
|
1376
|
+
totalFiles: everyDeclaration.length,
|
|
1377
|
+
bytes: progress.bytes(),
|
|
1378
|
+
totalBytes,
|
|
1423
1379
|
path: declaration.path,
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
storedSize: fromBatch.size,
|
|
1427
|
-
mediaType: declaration.mediaType,
|
|
1380
|
+
percent: 20 + Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1381
|
+
bytesPerSecond: progress.rate(),
|
|
1428
1382
|
});
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1431
|
-
const alreadyStored = alreadyHeld.get(declaration.sha256) ??
|
|
1432
1383
|
/*
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1384
|
+
Content the service already holds does not need sending. Asking costs
|
|
1385
|
+
one round trip and saves the whole file — which after an interrupted
|
|
1386
|
+
save is the difference between re-uploading a project and re-uploading
|
|
1387
|
+
nothing.
|
|
1436
1388
|
*/
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1389
|
+
/*
|
|
1390
|
+
A large file goes up in pieces, cut where its own content says.
|
|
1391
|
+
|
|
1392
|
+
This is what CBX is for and what it was never doing on the wire. The
|
|
1393
|
+
boundaries come from the same chunker the .cbx bundle uses, so an
|
|
1394
|
+
edit in the middle of a large file leaves the chunks either side of it
|
|
1395
|
+
untouched — and an untouched chunk is one the service already holds,
|
|
1396
|
+
which costs a question rather than a transfer. A small file is left
|
|
1397
|
+
whole: cutting it up would trade one request for several and save
|
|
1398
|
+
nothing.
|
|
1399
|
+
*/
|
|
1400
|
+
const chunkProfile = (await this.microchunkAllowed())
|
|
1401
|
+
? (0, chunking_js_1.microchunkProfileForFileSize)(declaration.size)
|
|
1402
|
+
: (0, chunking_js_1.profileForFileSize)(declaration.size);
|
|
1403
|
+
if (chunkProfile &&
|
|
1404
|
+
declaration.size >= CHUNK_THRESHOLD &&
|
|
1405
|
+
(await this.chunkingAllowed())) {
|
|
1406
|
+
const pieces = await this.putChunked(repositoryId, declaration, chunkProfile, (offset) => {
|
|
1454
1407
|
progress.lane(index, offset);
|
|
1455
1408
|
report({
|
|
1456
1409
|
stage: "upload",
|
|
@@ -1463,16 +1416,100 @@ class Uploader {
|
|
|
1463
1416
|
Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1464
1417
|
bytesPerSecond: progress.rate(),
|
|
1465
1418
|
});
|
|
1466
|
-
})
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1419
|
+
});
|
|
1420
|
+
progress.laneDone(index);
|
|
1421
|
+
/*
|
|
1422
|
+
A chunk is not a file. Counting every reused piece here made a
|
|
1423
|
+
one-file save report a negative number of sent files. The selected
|
|
1424
|
+
file counts as already stored only when every one of its pieces was
|
|
1425
|
+
reused and no payload crossed the wire.
|
|
1426
|
+
*/
|
|
1427
|
+
if (pieces.sentBytes === 0)
|
|
1428
|
+
progress.alreadyHeld();
|
|
1429
|
+
uploaded.push({
|
|
1430
|
+
path: declaration.path,
|
|
1431
|
+
sha256: declaration.sha256,
|
|
1432
|
+
chunks: pieces.chunks,
|
|
1433
|
+
sourceSize: declaration.size,
|
|
1434
|
+
storedSize: pieces.chunks.reduce((sum, one) => sum + one.storedSize, 0),
|
|
1435
|
+
mediaType: declaration.mediaType,
|
|
1436
|
+
});
|
|
1437
|
+
progress.finished(declaration.size, pieces.sentBytes);
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
const fromPack = packedInto.get(declaration.sha256);
|
|
1441
|
+
if (fromPack) {
|
|
1442
|
+
uploaded.push({
|
|
1443
|
+
path: declaration.path,
|
|
1444
|
+
sha256: declaration.sha256,
|
|
1445
|
+
pack: fromPack,
|
|
1446
|
+
sourceSize: declaration.size,
|
|
1447
|
+
/* Its share of the pack, so the version's totals stay honest. */
|
|
1448
|
+
storedSize: fromPack.storedSize,
|
|
1449
|
+
mediaType: declaration.mediaType,
|
|
1450
|
+
});
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
const fromBatch = batched.get(declaration.sha256);
|
|
1454
|
+
if (fromBatch) {
|
|
1455
|
+
uploaded.push({
|
|
1456
|
+
path: declaration.path,
|
|
1457
|
+
objectId: fromBatch.objectId,
|
|
1458
|
+
sourceSize: declaration.size,
|
|
1459
|
+
storedSize: fromBatch.size,
|
|
1460
|
+
mediaType: declaration.mediaType,
|
|
1461
|
+
});
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const alreadyStored = alreadyHeld.get(declaration.sha256) ??
|
|
1465
|
+
/*
|
|
1466
|
+
Only asked individually when the batch did not cover it — a digest
|
|
1467
|
+
that changed after the bulk question, because the file was rewritten
|
|
1468
|
+
while this upload was running.
|
|
1469
|
+
*/
|
|
1470
|
+
(await this.findStored(repositoryId, declaration));
|
|
1471
|
+
if (alreadyStored)
|
|
1472
|
+
progress.alreadyHeld();
|
|
1473
|
+
const result = alreadyStored ??
|
|
1474
|
+
(declaration.size <= DIRECT_LIMIT
|
|
1475
|
+
? await (0, profile_js_1.timed)("send files one at a time", () => this.putDirect(repositoryId, declaration)).catch((error) => {
|
|
1476
|
+
// Naming the file turns "it failed" into something actionable.
|
|
1477
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1478
|
+
throw new Error(`${declaration.path}: ${reason}`);
|
|
1479
|
+
})
|
|
1480
|
+
: await this.putMultipart(repositoryId, declaration, (offset) => {
|
|
1481
|
+
/*
|
|
1482
|
+
Recorded before the report reads it. Without this the bar does
|
|
1483
|
+
not move at all during a multipart file: `bytes()` sums the
|
|
1484
|
+
settled total and the open lanes, and this lane's offset is
|
|
1485
|
+
only in the second of those once it has been set.
|
|
1486
|
+
*/
|
|
1487
|
+
progress.lane(index, offset);
|
|
1488
|
+
report({
|
|
1489
|
+
stage: "upload",
|
|
1490
|
+
files: advance(index),
|
|
1491
|
+
totalFiles: everyDeclaration.length,
|
|
1492
|
+
bytes: progress.bytes(),
|
|
1493
|
+
totalBytes,
|
|
1494
|
+
path: declaration.path,
|
|
1495
|
+
percent: 20 +
|
|
1496
|
+
Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1497
|
+
bytesPerSecond: progress.rate(),
|
|
1498
|
+
});
|
|
1499
|
+
}));
|
|
1500
|
+
progress.laneDone(index);
|
|
1501
|
+
uploaded.push({
|
|
1502
|
+
path: declaration.path,
|
|
1503
|
+
objectId: result.objectId,
|
|
1504
|
+
sourceSize: declaration.size,
|
|
1505
|
+
storedSize: result.size,
|
|
1506
|
+
mediaType: declaration.mediaType,
|
|
1507
|
+
});
|
|
1508
|
+
progress.finished(declaration.size, alreadyStored ? 0 : declaration.size);
|
|
1509
|
+
}
|
|
1510
|
+
finally {
|
|
1511
|
+
release();
|
|
1512
|
+
}
|
|
1476
1513
|
}
|
|
1477
1514
|
};
|
|
1478
1515
|
await Promise.all(Array.from({ length: Math.min(UPLOAD_LANES, declarations.length) }, () => runWorker()));
|
package/dist/cli/src/network.js
CHANGED
|
@@ -60,17 +60,37 @@ function describeError(error) {
|
|
|
60
60
|
return "CodeRook did not answer in time. Check the connection, then try again.";
|
|
61
61
|
}
|
|
62
62
|
const cause = error.cause;
|
|
63
|
-
if (error.message === "fetch failed"
|
|
64
|
-
|
|
63
|
+
if (error.message === "fetch failed") {
|
|
64
|
+
/*
|
|
65
|
+
Said in words first, with the code after for whoever needs it. The most
|
|
66
|
+
common one, ENOTFOUND, is simply being offline: the name could not be
|
|
67
|
+
looked up. And a failure that carries no code at all used to fall
|
|
68
|
+
through to the bare "fetch failed" it exists to replace.
|
|
69
|
+
*/
|
|
70
|
+
const attempts = cause?.errors?.length ? cause.errors : cause ? [cause] : [];
|
|
71
|
+
const codes = [...new Set(attempts.map((one) => one.code).filter(Boolean))];
|
|
72
|
+
const said = codes.map((code) => PLAIN[code]).find(Boolean);
|
|
65
73
|
const named = attempts
|
|
66
74
|
.map((one) => [one.code, one.address].filter(Boolean).join(" "))
|
|
67
75
|
.filter(Boolean);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
const detail = named.length ? [...new Set(named)].join(", ") : cause?.message ?? "";
|
|
77
|
+
return (`Could not reach CodeRook${said ? `: ${said}` : ""}` +
|
|
78
|
+
(detail ? ` (${detail})` : "") +
|
|
79
|
+
". Check the connection, then try again.");
|
|
71
80
|
}
|
|
72
81
|
return error.message;
|
|
73
82
|
}
|
|
83
|
+
/** What each network failure means, for somebody who has not met the code. */
|
|
84
|
+
const PLAIN = {
|
|
85
|
+
ENOTFOUND: "its address could not be looked up, which usually means this computer is offline",
|
|
86
|
+
EAI_AGAIN: "its address could not be looked up, which usually means this computer is offline",
|
|
87
|
+
ECONNREFUSED: "the connection was refused",
|
|
88
|
+
ECONNRESET: "the connection was dropped",
|
|
89
|
+
ETIMEDOUT: "the connection timed out",
|
|
90
|
+
ENETUNREACH: "there is no route to the internet from here",
|
|
91
|
+
EHOSTUNREACH: "there is no route to the internet from here",
|
|
92
|
+
UND_ERR_CONNECT_TIMEOUT: "the connection timed out",
|
|
93
|
+
};
|
|
74
94
|
/** Wait, for a retry. */
|
|
75
95
|
function pause(milliseconds) {
|
|
76
96
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.4",
|
|
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",
|