@coderook/cli 0.21.0 → 0.22.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.
@@ -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.21.0",
5
+ "version": "0.22.0",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -718,7 +718,14 @@ class Uploader {
718
718
  });
719
719
  });
720
720
  inFlight.delete(index);
721
- alreadyOnAccount += pieces.reused;
721
+ /*
722
+ A chunk is not a file. Counting every reused piece here made a
723
+ one-file save report a negative number of sent files. The selected
724
+ file counts as already stored only when every one of its pieces was
725
+ reused and no payload crossed the wire.
726
+ */
727
+ if (pieces.sentBytes === 0)
728
+ alreadyOnAccount += 1;
722
729
  uploaded.push({
723
730
  path: declaration.path,
724
731
  sha256: declaration.sha256,
@@ -988,7 +995,7 @@ class Uploader {
988
995
  ...(completed.repeated ? { repeated: true } : {}),
989
996
  sourceBytes: completed.version.sourceSize ?? sourceBytes,
990
997
  storedBytes: completed.version.storedSize ?? storedBytes,
991
- sentBytes,
998
+ sentBytes: transferred,
992
999
  sentFiles: uploaded.length - alreadyOnAccount,
993
1000
  reusedFiles: reused.length,
994
1001
  /** Selected, but the service already held the content. */
@@ -1301,8 +1308,8 @@ class Uploader {
1301
1308
  */
1302
1309
  const CHUNK_LANES = 4;
1303
1310
  const active = new Set();
1304
- const dispatch = async (piece, at) => {
1305
- const task = this.sendChunk(repositoryId, declaration, piece, chunks, at, note).finally(() => {
1311
+ const dispatch = async (piece, at, digest) => {
1312
+ const task = this.sendChunk(repositoryId, declaration, piece, digest, chunks, at, note).finally(() => {
1306
1313
  active.delete(task);
1307
1314
  done += piece.length;
1308
1315
  onOffset(done);
@@ -1311,6 +1318,51 @@ class Uploader {
1311
1318
  if (active.size >= CHUNK_LANES)
1312
1319
  await Promise.race(active);
1313
1320
  };
1321
+ /*
1322
+ Ask about a bounded group of chunks before sending any of them.
1323
+
1324
+ The old path asked once per chunk, with four GETs in flight. Besides
1325
+ spending one network round trip per piece, an unavailable lookup was
1326
+ deliberately treated as a miss, so a transient failure retransmitted
1327
+ every otherwise reusable chunk. The website already uses the bulk
1328
+ endpoint. Reusing it here gives Desktop and CLI the same wire behaviour
1329
+ while retaining the safe "send it if unsure" fallback.
1330
+
1331
+ A batch is no larger than the existing chunk lane count, so this does
1332
+ not increase the memory ceiling: at most four bounded pieces are held.
1333
+ */
1334
+ const batch = [];
1335
+ const flush = async () => {
1336
+ if (!batch.length)
1337
+ return;
1338
+ const ready = batch.splice(0, batch.length);
1339
+ const held = await this.storedInBulk(repositoryId, ready.map((item) => item.digest));
1340
+ for (const item of ready) {
1341
+ const stored = held.get(item.digest);
1342
+ if (stored) {
1343
+ note("reused", 0);
1344
+ chunks[item.at] = {
1345
+ objectId: stored.objectId,
1346
+ sourceSize: item.piece.length,
1347
+ storedSize: stored.size,
1348
+ };
1349
+ done += item.piece.length;
1350
+ onOffset(done);
1351
+ continue;
1352
+ }
1353
+ await dispatch(item.piece, item.at, item.digest);
1354
+ }
1355
+ await Promise.all(active);
1356
+ };
1357
+ const queue = async (piece) => {
1358
+ batch.push({
1359
+ piece,
1360
+ at: ordinal++,
1361
+ digest: (0, node_crypto_1.createHash)("sha256").update(piece).digest("hex"),
1362
+ });
1363
+ if (batch.length >= CHUNK_LANES)
1364
+ await flush();
1365
+ };
1314
1366
  let ordinal = 0;
1315
1367
  let pending = Buffer.alloc(0);
1316
1368
  for await (const block of (0, node_fs_1.createReadStream)(declaration.full, {
@@ -1321,7 +1373,7 @@ class Uploader {
1321
1373
  pending = pending.length ? Buffer.concat([pending, incoming]) : incoming;
1322
1374
  let from = 0;
1323
1375
  for (const cut of (0, chunking_js_1.cutPoints)(pending, profile)) {
1324
- await dispatch(Buffer.from(pending.subarray(from, cut)), ordinal++);
1376
+ await queue(Buffer.from(pending.subarray(from, cut)));
1325
1377
  from = cut;
1326
1378
  }
1327
1379
  pending = Buffer.from(pending.subarray(from));
@@ -1333,8 +1385,8 @@ class Uploader {
1333
1385
  Dropping it truncates every chunked file by exactly its last piece.
1334
1386
  */
1335
1387
  if (pending.length)
1336
- await dispatch(pending, ordinal++);
1337
- await Promise.all(active);
1388
+ await queue(pending);
1389
+ await flush();
1338
1390
  /*
1339
1391
  Order is the file. Every position must be filled: a hole would mean a
1340
1392
  chunk that never landed, and publishing around it would produce a version
@@ -1351,7 +1403,7 @@ class Uploader {
1351
1403
  };
1352
1404
  }
1353
1405
  /** Send one chunk, reusing it if the account already holds those bytes. */
1354
- async sendChunk(repositoryId, declaration, piece,
1406
+ async sendChunk(repositoryId, declaration, piece, digest,
1355
1407
  /*
1356
1408
  Written at a known position rather than appended. The chunks are sent
1357
1409
  several at a time and finish in whatever order the network decides, but
@@ -1362,48 +1414,27 @@ class Uploader {
1362
1414
  chunks, at, note) {
1363
1415
  {
1364
1416
  this.check();
1365
- const digest = (0, node_crypto_1.createHash)("sha256").update(piece).digest("hex");
1366
- /*
1367
- Asked per chunk rather than per file. This is the whole saving: the
1368
- parts of a large file that did not change answer "already held" and
1369
- never travel.
1370
- */
1371
- const held = await this.findStored(repositoryId, {
1372
- ...declaration,
1373
- sha256: digest,
1374
- size: piece.length,
1417
+ const encoded = await (0, compress_js_1.encodeForUpload)(new Uint8Array(piece), declaration.path, await this.gzipAllowed());
1418
+ const query = encoded.encoding === "gzip"
1419
+ ? `?kind=chunk&role=chunk&encoding=gzip` +
1420
+ `&logicalSize=${piece.length}` +
1421
+ `&storedSha256=${encoded.storedSha256}`
1422
+ : `?kind=chunk&role=chunk`;
1423
+ const stored = await this.call(`/v1/repositories/${repositoryId}/objects/${digest}${query}`, {
1424
+ method: "PUT",
1425
+ contentType: "application/octet-stream",
1426
+ body: encoded.body,
1427
+ }).catch((error) => {
1428
+ const reason = error instanceof Error ? error.message : String(error);
1429
+ throw new Error(`${declaration.path}: ${reason}`);
1375
1430
  });
1376
- if (held) {
1377
- note("reused", 0);
1378
- chunks[at] = {
1379
- objectId: held.objectId,
1380
- sourceSize: piece.length,
1381
- storedSize: held.size,
1382
- };
1383
- }
1384
- else {
1385
- const encoded = await (0, compress_js_1.encodeForUpload)(new Uint8Array(piece), declaration.path, await this.gzipAllowed());
1386
- const query = encoded.encoding === "gzip"
1387
- ? `?kind=chunk&role=chunk&encoding=gzip` +
1388
- `&logicalSize=${piece.length}` +
1389
- `&storedSha256=${encoded.storedSha256}`
1390
- : `?kind=chunk&role=chunk`;
1391
- const stored = await this.call(`/v1/repositories/${repositoryId}/objects/${digest}${query}`, {
1392
- method: "PUT",
1393
- contentType: "application/octet-stream",
1394
- body: encoded.body,
1395
- }).catch((error) => {
1396
- const reason = error instanceof Error ? error.message : String(error);
1397
- throw new Error(`${declaration.path}: ${reason}`);
1398
- });
1399
- note("sent", encoded.body.byteLength);
1400
- chunks[at] = {
1401
- objectId: stored.objectId,
1402
- sourceSize: piece.length,
1403
- /* What the service keeps, which is smaller when the piece gzipped. */
1404
- storedSize: stored.storedSize ?? stored.size,
1405
- };
1406
- }
1431
+ note("sent", encoded.body.byteLength);
1432
+ chunks[at] = {
1433
+ objectId: stored.objectId,
1434
+ sourceSize: piece.length,
1435
+ /* What the service keeps, which is smaller when the piece gzipped. */
1436
+ storedSize: stored.storedSize ?? stored.size,
1437
+ };
1407
1438
  }
1408
1439
  }
1409
1440
  async putDirect(repositoryId, declaration) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",