@coderook/cli 0.29.0 → 0.29.1

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.29.0",
5
+ "version": "0.29.1",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -661,6 +661,16 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
661
661
  localPath: folder,
662
662
  changed: files.filter((file) => !file.deleted).map((file) => file.path),
663
663
  deleted: files.filter((file) => file.deleted).map((file) => file.path),
664
+ /*
665
+ Nothing is left out, and `changed` above is not the whole of it.
666
+
667
+ `submit` sends whatever the rules allow — there is no per-file choosing
668
+ here — but it was passing the listed rows as the selection, and that list
669
+ stops at twenty thousand. A folder of twenty-one thousand files saved
670
+ twenty thousand of them and said "Saved v1" without mentioning the rest.
671
+ An empty exclusion list is how "all of it" is said.
672
+ */
673
+ excluded: [],
664
674
  message,
665
675
  /*
666
676
  A folder's name is the right default and the wrong answer for import,
@@ -8,6 +8,7 @@ function uploadRequestFor(input) {
8
8
  localPath: input.localPath,
9
9
  // Both lists. See the note at the top of this file.
10
10
  include: [...input.changed, ...input.deleted],
11
+ ...(input.excluded === undefined ? {} : { excluded: input.excluded }),
11
12
  deletions: input.deleted,
12
13
  message: input.message,
13
14
  projectName: input.projectName,
@@ -424,60 +424,98 @@ class Uploader {
424
424
  // 1. Measure and hash. This is what makes an unchanged file free.
425
425
  const declarations = [];
426
426
  let totalBytes = 0;
427
- for (const [index, file] of sending.entries()) {
428
- this.check();
429
- const full = node_path_1.default.join(request.localPath, file.path);
430
- let size;
431
- try {
432
- size = (await (0, promises_1.stat)(full)).size;
433
- }
434
- catch (error) {
435
- // Unreadable now, though it was listed a moment ago. Skipping it
436
- // would publish a version quietly missing a file the person chose.
437
- vanished.push(file.path);
438
- readFailures.set(file.path, error instanceof Error ? error.message : String(error));
439
- continue;
440
- }
441
- report({
442
- stage: "hash",
443
- files: index,
444
- totalFiles: sending.length,
445
- bytes: totalBytes,
446
- totalBytes: 0,
447
- path: file.path,
448
- percent: Math.round((index / sending.length) * 20),
449
- bytesPerSecond: 0,
450
- });
451
- /*
452
- Hashing is the first thing that opens the file, and opening is what
453
- fails on a file another program holds — a database in use, a browser
454
- profile's LOCK, an antivirus mid-scan. On Windows those still answer
455
- stat() perfectly well, so the check three lines above lets them
456
- through and the read below was the one that actually broke.
457
-
458
- It was unguarded, so the person got a raw "EBUSY: resource busy or
459
- locked" naming a path they never chose to think about, instead of the
460
- sentence this code already writes for exactly this situation. The
461
- outcome was always going to be a refusal; what was missing was it
462
- being legible.
463
- */
464
- let digest;
465
- try {
466
- digest = await (0, profile_js_1.timed)("hash files", () => digestOf(full));
427
+ /*
428
+ Hashed several at a time, and reported by how many are done.
429
+
430
+ Each file here is an open, a read and a close, and on Windows every open
431
+ is also an antivirus scan — so this stage is waiting, not computing.
432
+ Taken strictly one after another, a folder of twenty thousand small
433
+ files spent a quarter of an hour in a step that moves no data at all,
434
+ and the bar sat on 0% for the first thousand of them because it only
435
+ ever spanned a fifth of itself across the whole stage.
436
+
437
+ Overlapping the waits collapses them into one wait. The results are
438
+ written into their own slots rather than pushed, because which disk read
439
+ finishes first must not decide the order of a version's file list.
440
+ */
441
+ const HASH_LANES = 8;
442
+ const measured = new Array(sending.length).fill(null);
443
+ let hashed = 0;
444
+ let cursor = 0;
445
+ /** Throttled: twenty thousand sends is its own cost, and nobody reads them. */
446
+ let told = 0;
447
+ const lane = async () => {
448
+ for (;;) {
449
+ const index = cursor;
450
+ cursor += 1;
451
+ if (index >= sending.length)
452
+ return;
453
+ const file = sending[index];
454
+ this.check();
455
+ const full = node_path_1.default.join(request.localPath, file.path);
456
+ let size;
457
+ try {
458
+ size = (await (0, promises_1.stat)(full)).size;
459
+ }
460
+ catch (error) {
461
+ // Unreadable now, though it was listed a moment ago. Skipping it
462
+ // would publish a version quietly missing a file the person chose.
463
+ vanished.push(file.path);
464
+ readFailures.set(file.path, error instanceof Error ? error.message : String(error));
465
+ hashed += 1;
466
+ continue;
467
+ }
468
+ /*
469
+ Hashing is the first thing that opens the file, and opening is what
470
+ fails on a file another program holds — a database in use, a browser
471
+ profile's LOCK, an antivirus mid-scan. On Windows those still answer
472
+ stat() perfectly well, so the check above lets them through and the
473
+ read below is the one that actually breaks.
474
+
475
+ It was unguarded, so the person got a raw "EBUSY: resource busy or
476
+ locked" naming a path they never chose to think about, instead of
477
+ the sentence this code already writes for exactly this situation.
478
+ */
479
+ let digest;
480
+ try {
481
+ digest = await (0, profile_js_1.timed)("hash files", () => digestOf(full));
482
+ }
483
+ catch (error) {
484
+ vanished.push(file.path);
485
+ readFailures.set(file.path, error instanceof Error ? error.message : String(error));
486
+ hashed += 1;
487
+ continue;
488
+ }
489
+ measured[index] = {
490
+ path: file.path,
491
+ full,
492
+ size,
493
+ sha256: digest,
494
+ mediaType: mediaTypeOf(file.path),
495
+ };
496
+ hashed += 1;
497
+ const now = Date.now();
498
+ if (now - told > 80 || hashed === sending.length) {
499
+ told = now;
500
+ report({
501
+ stage: "hash",
502
+ files: hashed,
503
+ totalFiles: sending.length,
504
+ bytes: 0,
505
+ totalBytes: 0,
506
+ path: file.path,
507
+ percent: Math.round((hashed / sending.length) * 20),
508
+ bytesPerSecond: 0,
509
+ });
510
+ }
467
511
  }
468
- catch (error) {
469
- vanished.push(file.path);
470
- readFailures.set(file.path, error instanceof Error ? error.message : String(error));
512
+ };
513
+ await Promise.all(Array.from({ length: Math.min(HASH_LANES, sending.length) }, () => lane()));
514
+ for (const one of measured) {
515
+ if (!one)
471
516
  continue;
472
- }
473
- declarations.push({
474
- path: file.path,
475
- full,
476
- size,
477
- sha256: digest,
478
- mediaType: mediaTypeOf(file.path),
479
- });
480
- totalBytes += size;
517
+ declarations.push(one);
518
+ totalBytes += one.size;
481
519
  }
482
520
  /*
483
521
  A deletion-only save has no file to hash or upload, but it is still a
@@ -1060,7 +1098,16 @@ class Uploader {
1060
1098
  totalFiles: declarations.length,
1061
1099
  bytes: sentBytes,
1062
1100
  totalBytes,
1063
- path: "Recording the version",
1101
+ /*
1102
+ A save, not a version.
1103
+
1104
+ Sending produces a commit; it becomes a version when somebody names
1105
+ it, which happens later and on purpose. This line said "Recording the
1106
+ version" while doing neither — it was the last wording left over from
1107
+ before the two were separate, and it sat on the one screen a person
1108
+ watches to the end.
1109
+ */
1110
+ path: "Writing down what was sent",
1064
1111
  percent: 96,
1065
1112
  bytesPerSecond: rate(),
1066
1113
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
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",