@coderook/cli 0.30.2 → 0.31.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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/cli.js +25 -1
- package/dist/cli/src/config.js +209 -22
- package/dist/cli/src/git_remote.js +105 -16
- package/dist/cli/src/publish.js +5 -0
- package/dist/cli/src/stored_zip.js +85 -0
- package/dist/desktop-app/src/main/compress.js +21 -0
- package/dist/desktop-app/src/main/detect.js +84 -15
- package/dist/desktop-app/src/main/download.js +58 -2
- package/dist/desktop-app/src/main/upload.js +187 -83
- package/dist/desktop-app/src/main/worktree.js +193 -5
- package/dist/desktop-app/src/shared/compression_policy.js +122 -5
- package/dist/desktop-app/src/shared/format.js +31 -0
- package/package.json +1 -1
|
@@ -4,6 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.IGNORE_HEADING = exports.KEEP_HEADING = exports.LOCAL_EXCLUDE_FILE = exports.IGNORE_FILE = exports.STARTER_IGNORE = exports.CODEROOK_TAIL = exports.CODEROOK_TAIL_HEADING = exports.KEPT_ANYWAY = exports.KEPT_ANYWAY_HEADING = exports.ALSO_LEFT_OUT = exports.ALSO_LEFT_OUT_HEADING = exports.STARTER_MARKER = exports.EVALUATION_FILE_LIMIT = void 0;
|
|
7
|
+
exports.ruleTarget = ruleTarget;
|
|
8
|
+
exports.keptTargets = keptTargets;
|
|
9
|
+
exports.withoutOpposites = withoutOpposites;
|
|
7
10
|
exports.starterAuthored = starterAuthored;
|
|
8
11
|
exports.missingStarterRules = missingStarterRules;
|
|
9
12
|
exports.missingEngineRules = missingEngineRules;
|
|
@@ -11,6 +14,7 @@ exports.isRepository = isRepository;
|
|
|
11
14
|
exports.migrateRetiredRules = migrateRetiredRules;
|
|
12
15
|
exports.starterFor = starterFor;
|
|
13
16
|
exports.moveRule = moveRule;
|
|
17
|
+
exports.acceptSuggestions = acceptSuggestions;
|
|
14
18
|
exports.readRules = readRules;
|
|
15
19
|
exports.writeRules = writeRules;
|
|
16
20
|
exports.collectLayers = collectLayers;
|
|
@@ -224,6 +228,93 @@ function ruleLines(text) {
|
|
|
224
228
|
.map((line) => line.trim())
|
|
225
229
|
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
226
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* What a rule line is about, with the parts that do not change the answer
|
|
233
|
+
* taken off: the `!`, and a slash at either end.
|
|
234
|
+
*
|
|
235
|
+
* `output/`, `/output/` and `!output/` all name the same folder, and deciding
|
|
236
|
+
* whether two lines disagree has to compare what they name rather than how they
|
|
237
|
+
* were spelt — the detector writes one spelling, the filter screen another, and
|
|
238
|
+
* a person a third.
|
|
239
|
+
*/
|
|
240
|
+
function ruleTarget(line) {
|
|
241
|
+
return line.trim().replace(/^!/, "").replace(/^\/+|\/+$/g, "");
|
|
242
|
+
}
|
|
243
|
+
/** Every thing this file explicitly keeps, by {@link ruleTarget}. */
|
|
244
|
+
function keptTargets(text) {
|
|
245
|
+
return new Set(ruleLines(text)
|
|
246
|
+
.filter((line) => line.startsWith("!"))
|
|
247
|
+
.map(ruleTarget));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The file without any line that says the opposite of `line` about the same
|
|
251
|
+
* thing, and without a heading that is left with nothing under it.
|
|
252
|
+
*
|
|
253
|
+
* Needed because every writer here appends. A keep has to come after whatever
|
|
254
|
+
* would take the file, and a new ignore after a keep beats it, so the last
|
|
255
|
+
* word always wins — but the earlier word stayed in the file. A real project
|
|
256
|
+
* ended up with `# Always include` / `!output/` directly above
|
|
257
|
+
* `# Suggested for this folder, and accepted.` / `output/`: correct to a
|
|
258
|
+
* matcher, and to anybody reading it, a file that says both things.
|
|
259
|
+
*
|
|
260
|
+
* Only the exact opposite goes. A broader rule — `*.log` above a kept
|
|
261
|
+
* `!important.log` — is not about the same thing and is what makes the keep
|
|
262
|
+
* mean anything, so it is left alone. Everything the removal did not touch is
|
|
263
|
+
* returned byte for byte, line endings included.
|
|
264
|
+
*/
|
|
265
|
+
function withoutOpposites(body, line) {
|
|
266
|
+
const target = ruleTarget(line);
|
|
267
|
+
const keeping = line.trim().startsWith("!");
|
|
268
|
+
const lines = body.split(NEWLINE);
|
|
269
|
+
const plain = (at) => (lines[at] ?? "").trim();
|
|
270
|
+
const isRule = (at) => plain(at).length > 0 && !plain(at).startsWith("#");
|
|
271
|
+
const drop = new Set();
|
|
272
|
+
/* Paragraphs are runs of non-blank lines; a heading belongs to its own. */
|
|
273
|
+
let start = 0;
|
|
274
|
+
while (start < lines.length) {
|
|
275
|
+
if (!plain(start)) {
|
|
276
|
+
start += 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
let end = start;
|
|
280
|
+
while (end + 1 < lines.length && plain(end + 1))
|
|
281
|
+
end += 1;
|
|
282
|
+
let touched = false;
|
|
283
|
+
for (let at = start; at <= end; at += 1) {
|
|
284
|
+
if (!isRule(at))
|
|
285
|
+
continue;
|
|
286
|
+
const other = plain(at);
|
|
287
|
+
if (ruleTarget(other) === target && other.startsWith("!") !== keeping) {
|
|
288
|
+
drop.add(at);
|
|
289
|
+
touched = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/*
|
|
293
|
+
A heading over nothing is a claim about nothing. Dropped with its
|
|
294
|
+
paragraph, and one blank line beside it, so the file does not gather
|
|
295
|
+
empty gaps where decisions used to be.
|
|
296
|
+
*/
|
|
297
|
+
if (touched) {
|
|
298
|
+
let left = false;
|
|
299
|
+
for (let at = start; at <= end; at += 1) {
|
|
300
|
+
if (isRule(at) && !drop.has(at))
|
|
301
|
+
left = true;
|
|
302
|
+
}
|
|
303
|
+
if (!left) {
|
|
304
|
+
for (let at = start; at <= end; at += 1)
|
|
305
|
+
drop.add(at);
|
|
306
|
+
if (end + 1 < lines.length && !plain(end + 1))
|
|
307
|
+
drop.add(end + 1);
|
|
308
|
+
else if (start > 0 && !plain(start - 1))
|
|
309
|
+
drop.add(start - 1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
start = end + 1;
|
|
313
|
+
}
|
|
314
|
+
if (!drop.size)
|
|
315
|
+
return body;
|
|
316
|
+
return lines.filter((_line, at) => !drop.has(at)).join(NEWLINE);
|
|
317
|
+
}
|
|
227
318
|
/**
|
|
228
319
|
* Whether this .gitignore was written by CodeRook rather than by a person.
|
|
229
320
|
*
|
|
@@ -255,7 +346,14 @@ function starterAuthored(text) {
|
|
|
255
346
|
*/
|
|
256
347
|
function missingStarterRules(text, reference = exports.STARTER_IGNORE) {
|
|
257
348
|
const present = new Set(ruleLines(text));
|
|
258
|
-
|
|
349
|
+
/*
|
|
350
|
+
Not a line the person has already answered. A starter rule for something
|
|
351
|
+
this file explicitly keeps is not missing, it was overruled — and offering
|
|
352
|
+
it back would append it after the keep, where it wins, and take out the
|
|
353
|
+
very thing somebody said to always include.
|
|
354
|
+
*/
|
|
355
|
+
const kept = keptTargets(text);
|
|
356
|
+
return ruleLines(reference).filter((line) => !present.has(line) && (line.startsWith("!") || !kept.has(ruleTarget(line))));
|
|
259
357
|
}
|
|
260
358
|
/**
|
|
261
359
|
* The rules this folder's own file is missing for the kind of project it is.
|
|
@@ -423,12 +521,23 @@ async function moveRule(root, file, keep) {
|
|
|
423
521
|
.catch(() => file.endsWith("/"));
|
|
424
522
|
const target = directory ? `${bare}/` : bare;
|
|
425
523
|
const line = keep ? `!${target}` : target;
|
|
524
|
+
/*
|
|
525
|
+
The opposite answer goes first. Moving a folder that was kept onto the
|
|
526
|
+
ignore list used to add `output/` after `!output/` and leave both, so the
|
|
527
|
+
file said both things and only the order decided which it meant.
|
|
528
|
+
*/
|
|
529
|
+
const settled = withoutOpposites(rules.shared, line);
|
|
426
530
|
/* Saying it twice changes nothing and makes the file harder to read. */
|
|
427
|
-
const already = ruleLines(
|
|
428
|
-
if (already)
|
|
429
|
-
|
|
531
|
+
const already = ruleLines(settled).some((one) => one === line);
|
|
532
|
+
if (already) {
|
|
533
|
+
if (settled === rules.shared)
|
|
534
|
+
return rules;
|
|
535
|
+
const next = { ...rules, shared: settled };
|
|
536
|
+
await writeRules(root, next);
|
|
537
|
+
return next;
|
|
538
|
+
}
|
|
430
539
|
const heading = keep ? exports.KEEP_HEADING : exports.IGNORE_HEADING;
|
|
431
|
-
let body =
|
|
540
|
+
let body = settled.replace(/\s*$/, "");
|
|
432
541
|
/*
|
|
433
542
|
A keep inside an excluded folder needs the folder opened first.
|
|
434
543
|
|
|
@@ -484,6 +593,85 @@ async function moveRule(root, file, keep) {
|
|
|
484
593
|
await writeRules(root, next);
|
|
485
594
|
return next;
|
|
486
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Take the suggestions somebody accepted, and write them.
|
|
598
|
+
*
|
|
599
|
+
* The accepted patterns and the missing starter lines are written as two
|
|
600
|
+
* labelled blocks rather than one, because they answer different questions:
|
|
601
|
+
* one is "this folder holds a cache", the other is "the rules this folder
|
|
602
|
+
* was set up with have moved on". A person reading the file later should be
|
|
603
|
+
* able to tell which was which.
|
|
604
|
+
*
|
|
605
|
+
* Lifted out of the window's event handler for the same reason `moveRule` was:
|
|
606
|
+
* inside it nothing could test it, and the defect it had — leaving the keep it
|
|
607
|
+
* had just overruled in the file — was found by a person reading their own
|
|
608
|
+
* .gitignore rather than by anything here.
|
|
609
|
+
*/
|
|
610
|
+
async function acceptSuggestions(root, patterns, starter) {
|
|
611
|
+
const rules = await readRules(root);
|
|
612
|
+
/*
|
|
613
|
+
A suggestion somebody accepts is their answer about that folder, and it
|
|
614
|
+
replaces any earlier one. A real project kept `# Always include` /
|
|
615
|
+
`!output/` directly above the `output/` the size prompt had just
|
|
616
|
+
written: right to a matcher, because the later line wins, and to
|
|
617
|
+
anybody reading the file a flat contradiction.
|
|
618
|
+
|
|
619
|
+
Starter lines are the other way round. Nobody chose those one by one,
|
|
620
|
+
so they never overrule a keep — `missingStarterRules` already leaves
|
|
621
|
+
out anything kept, and this skips it again in case the file changed
|
|
622
|
+
between the prompt being drawn and the button being pressed.
|
|
623
|
+
*/
|
|
624
|
+
let settled = rules.shared;
|
|
625
|
+
for (const pattern of patterns)
|
|
626
|
+
settled = withoutOpposites(settled, pattern);
|
|
627
|
+
const keptNow = keptTargets(settled);
|
|
628
|
+
starter = starter.filter((line) => line.startsWith("!") || !keptNow.has(ruleTarget(line)));
|
|
629
|
+
/*
|
|
630
|
+
Added to, never stacked.
|
|
631
|
+
|
|
632
|
+
The first version of this appended a fresh block every time, so a
|
|
633
|
+
folder that grew a second cache later ended up with two identical
|
|
634
|
+
headings and the same rule written twice — the same fault the template
|
|
635
|
+
appender had already been fixed for. Lines that are already in the file
|
|
636
|
+
are skipped, and a heading that is already there is extended under
|
|
637
|
+
itself rather than repeated.
|
|
638
|
+
*/
|
|
639
|
+
const has = new Set(settled
|
|
640
|
+
.split(/\r?\n/)
|
|
641
|
+
.map((line) => line.trim())
|
|
642
|
+
.filter(Boolean));
|
|
643
|
+
const add = (heading, lines) => {
|
|
644
|
+
const wanted = lines.filter((line) => !has.has(line.trim()));
|
|
645
|
+
if (!wanted.length)
|
|
646
|
+
return "";
|
|
647
|
+
for (const line of wanted)
|
|
648
|
+
has.add(line.trim());
|
|
649
|
+
return [heading, ...wanted].join("\n");
|
|
650
|
+
};
|
|
651
|
+
let shared = settled.replace(/\s*$/, "");
|
|
652
|
+
for (const [heading, lines] of [
|
|
653
|
+
[detect_js_1.SUGGESTED_HEADING, patterns],
|
|
654
|
+
[detect_js_1.STARTER_TOPUP_HEADING, starter],
|
|
655
|
+
]) {
|
|
656
|
+
const block = add(heading, lines);
|
|
657
|
+
if (!block)
|
|
658
|
+
continue;
|
|
659
|
+
const at = shared.indexOf(heading);
|
|
660
|
+
if (at === -1) {
|
|
661
|
+
shared = shared ? `${shared}\n\n${block}` : block;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
/* Extend the block that is already there, directly under its heading. */
|
|
665
|
+
const after = at + heading.length;
|
|
666
|
+
shared =
|
|
667
|
+
shared.slice(0, after) +
|
|
668
|
+
block.slice(heading.length) +
|
|
669
|
+
shared.slice(after);
|
|
670
|
+
}
|
|
671
|
+
const next = { ...rules, shared: `${shared}\n` };
|
|
672
|
+
await writeRules(root, next);
|
|
673
|
+
return next;
|
|
674
|
+
}
|
|
487
675
|
/** The two headings a move writes under, so a person can find their own edits. */
|
|
488
676
|
exports.KEEP_HEADING = "# Always include";
|
|
489
677
|
exports.IGNORE_HEADING = "# Left out from the filter screen";
|
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
* make the same decision for the same object.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.WORTHWHILE_COMPRESSION_RATIO = exports.SMALLEST_WORTH_COMPRESSING = void 0;
|
|
9
|
+
exports.GZIP_KEEP_RATE = exports.GZIP_REVIEW_EVERY = exports.SAMPLE_BYTES = exports.WORTHWHILE_COMPRESSION_RATIO = exports.SMALLEST_WORTH_COMPRESSING = void 0;
|
|
10
10
|
exports.looksCompressed = looksCompressed;
|
|
11
11
|
exports.shouldTryGzip = shouldTryGzip;
|
|
12
|
+
exports.worthSampling = worthSampling;
|
|
13
|
+
exports.sampleWindows = sampleWindows;
|
|
14
|
+
exports.sampleSaysDoNotBother = sampleSaysDoNotBother;
|
|
12
15
|
exports.gzipIsWorthKeeping = gzipIsWorthKeeping;
|
|
16
|
+
exports.freshGzipVerdict = freshGzipVerdict;
|
|
17
|
+
exports.noteChunkOutcome = noteChunkOutcome;
|
|
13
18
|
const ALREADY_COMPRESSED = new Set([
|
|
14
19
|
".7z", ".aac", ".avi", ".br", ".bz2", ".cbx", ".docx", ".flac", ".gif",
|
|
15
|
-
".gz", ".heic", ".jpeg", ".jpg", ".jxl", ".m4a", ".m4v", ".mkv",
|
|
16
|
-
".mp3", ".mp4", ".odp", ".ods", ".odt", ".ogg", ".
|
|
17
|
-
".
|
|
18
|
-
".zst",
|
|
20
|
+
".gguf", ".gz", ".heic", ".jpeg", ".jpg", ".jxl", ".m4a", ".m4v", ".mkv",
|
|
21
|
+
".mov", ".mp3", ".mp4", ".odp", ".ods", ".odt", ".ogg", ".onnx", ".opus",
|
|
22
|
+
".pdf", ".png", ".pptx", ".rar", ".safetensors", ".webm", ".webp", ".xlsx",
|
|
23
|
+
".xz", ".zip", ".zst",
|
|
19
24
|
]);
|
|
20
25
|
exports.SMALLEST_WORTH_COMPRESSING = 4 * 1024;
|
|
21
26
|
exports.WORTHWHILE_COMPRESSION_RATIO = 0.95;
|
|
@@ -30,6 +35,118 @@ function shouldTryGzip(relativePath, sourceBytes, allowGzip) {
|
|
|
30
35
|
sourceBytes >= exports.SMALLEST_WORTH_COMPRESSING &&
|
|
31
36
|
!looksCompressed(relativePath));
|
|
32
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Above this, find out by trying a little before trying all of it.
|
|
40
|
+
*
|
|
41
|
+
* The extension list above is a guess, and the cost of a miss is unbounded.
|
|
42
|
+
* A project of thirteen `.gguf` model weights — 10.6 GB, an extension nothing
|
|
43
|
+
* here had heard of — was deflated at level 6, in full, to discover that it
|
|
44
|
+
* compressed by 3.3% and the result had to be thrown away. That was 87% of the
|
|
45
|
+
* time the save spent before it sent a single byte, and the save runs the whole
|
|
46
|
+
* pipeline twice, so it was paid twice. The project never finished saving.
|
|
47
|
+
*
|
|
48
|
+
* A quarter of a megabyte is eight times deflate's window, which is enough for
|
|
49
|
+
* the answer to mean something, and 6% of a maximum chunk, which is what makes
|
|
50
|
+
* a wrong guess in the list above cheap instead of ruinous.
|
|
51
|
+
*/
|
|
52
|
+
exports.SAMPLE_BYTES = 256 * 1024;
|
|
53
|
+
/** Whether a piece is big enough that guessing wrong on it is worth avoiding. */
|
|
54
|
+
function worthSampling(sourceBytes) {
|
|
55
|
+
/*
|
|
56
|
+
Only where the sample is a real saving over the whole thing. Sampling a
|
|
57
|
+
300 KB file costs most of what compressing it would and answers a question
|
|
58
|
+
that was about to be answered properly anyway.
|
|
59
|
+
*/
|
|
60
|
+
return sourceBytes >= exports.SAMPLE_BYTES * 8;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Where to look, for a piece of this size.
|
|
64
|
+
*
|
|
65
|
+
* Two windows, not one, because a single window at the front judges the whole
|
|
66
|
+
* file by its header. A container with a text manifest and an incompressible
|
|
67
|
+
* payload would be compressed in full on the strength of its first block —
|
|
68
|
+
* which is the waste this exists to avoid — and one shaped the other way round
|
|
69
|
+
* would be abandoned despite compressing. The middle disagrees with the front
|
|
70
|
+
* in both cases, and either window arguing for compression is enough.
|
|
71
|
+
*
|
|
72
|
+
* Shared rather than chosen per caller so Desktop and the browser make the same
|
|
73
|
+
* decision about the same bytes, which is the point of this module.
|
|
74
|
+
*/
|
|
75
|
+
function sampleWindows(sourceBytes) {
|
|
76
|
+
return [
|
|
77
|
+
{ at: 0, size: exports.SAMPLE_BYTES },
|
|
78
|
+
{ at: Math.floor(sourceBytes / 2), size: exports.SAMPLE_BYTES },
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether a window that compressed this little argues against the whole piece.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately the same test {@link gzipIsWorthKeeping} applies at the end: the
|
|
85
|
+
* sample is an estimate of that answer, so estimating it against a softer
|
|
86
|
+
* threshold was the bug in the first attempt at this. A window that saved 3%
|
|
87
|
+
* would have passed a "did it compress at all" test, committed the file to a
|
|
88
|
+
* full deflate, and then had the result discarded for saving 3% — which is
|
|
89
|
+
* exactly the several minutes this was meant to stop spending.
|
|
90
|
+
*
|
|
91
|
+
* Getting it wrong costs a little space and never a wrong result: an object's
|
|
92
|
+
* identity is the digest of its original bytes, whatever travels.
|
|
93
|
+
*/
|
|
94
|
+
function sampleSaysDoNotBother(sampleBytes, compressedSampleBytes) {
|
|
95
|
+
return !gzipIsWorthKeeping(sampleBytes, compressedSampleBytes);
|
|
96
|
+
}
|
|
33
97
|
function gzipIsWorthKeeping(sourceBytes, compressedBytes) {
|
|
34
98
|
return compressedBytes < sourceBytes * exports.WORTHWHILE_COMPRESSION_RATIO;
|
|
35
99
|
}
|
|
100
|
+
/** How often a chunked file's compression is reviewed. */
|
|
101
|
+
exports.GZIP_REVIEW_EVERY = 16;
|
|
102
|
+
/** The fraction of reviewed chunks that must have kept gzip, as its divisor. */
|
|
103
|
+
exports.GZIP_KEEP_RATE = 4;
|
|
104
|
+
/** A file nothing is known about yet. */
|
|
105
|
+
function freshGzipVerdict() {
|
|
106
|
+
return { allowed: true, tried: 0, kept: 0, chunks: 0 };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Record how one chunk turned out, and decide whether to keep trying.
|
|
110
|
+
*
|
|
111
|
+
* A large file is not one object, it is hundreds: a 1 GB file at a 1 MB target
|
|
112
|
+
* is a thousand chunks, each compressed and judged on its own. So the guard
|
|
113
|
+
* that stops wasted work on a whole *file* cannot live in the piece — sampling
|
|
114
|
+
* inside a 1 MB chunk saves a fraction of a job that was already small.
|
|
115
|
+
*
|
|
116
|
+
* Thirteen `.gguf` model weights, 10.6 GB, went chunk by chunk through deflate
|
|
117
|
+
* at level 6. That was 87% of the time the save spent before it sent a byte,
|
|
118
|
+
* and a save runs the whole pipeline twice, so it was spent twice. The project
|
|
119
|
+
* never finished saving.
|
|
120
|
+
*
|
|
121
|
+
* Measured on one of those files, 24 of 208 chunks did clear the 5% threshold
|
|
122
|
+
* and keep their compression — so "give up once nothing has compressed" never
|
|
123
|
+
* fired, which was the first attempt at this. What those 24 chunks saved was
|
|
124
|
+
* 0.7% of the file. The question is a rate, not an existence.
|
|
125
|
+
*
|
|
126
|
+
* Each window is judged on itself, and the count starts again after every
|
|
127
|
+
* review. Judging the file cumulatively instead lets a compressible opening buy
|
|
128
|
+
* an arbitrarily long incompressible tail: a run of 256 chunks that compressed
|
|
129
|
+
* would carry the next thousand that did not, which is the cost this exists to
|
|
130
|
+
* bound. Starting again means the work wasted after a file changes character is
|
|
131
|
+
* never more than one window, whereever in the file that happens.
|
|
132
|
+
*
|
|
133
|
+
* Being wrong costs a little storage and never a wrong result: an object's
|
|
134
|
+
* identity is the digest of its original bytes, whatever those bytes travelled
|
|
135
|
+
* as, so chunks already sent compressed stay exactly as valid as before.
|
|
136
|
+
*/
|
|
137
|
+
function noteChunkOutcome(verdict, keptGzip) {
|
|
138
|
+
if (!verdict.allowed)
|
|
139
|
+
return;
|
|
140
|
+
verdict.chunks += 1;
|
|
141
|
+
verdict.tried += 1;
|
|
142
|
+
if (keptGzip)
|
|
143
|
+
verdict.kept += 1;
|
|
144
|
+
if (verdict.tried < exports.GZIP_REVIEW_EVERY)
|
|
145
|
+
return;
|
|
146
|
+
if (verdict.kept * exports.GZIP_KEEP_RATE < verdict.tried) {
|
|
147
|
+
verdict.allowed = false;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
verdict.tried = 0;
|
|
151
|
+
verdict.kept = 0;
|
|
152
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Number formatting in the design's phrasing, where both sides can reach it.
|
|
4
|
+
*
|
|
5
|
+
* Byte sizes started out in the renderer, because only the renderer showed
|
|
6
|
+
* them. Then the detector began writing a person-facing sentence of its own —
|
|
7
|
+
* "10.7 GB of 10.8 GB, and no source in it" — and a second formatter in the
|
|
8
|
+
* main process would have meant the same folder described two ways in two
|
|
9
|
+
* places. The renderer re-exports these, so nothing that already imports from
|
|
10
|
+
* `renderer/format` had to change.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.formatBytes = formatBytes;
|
|
14
|
+
exports.formatCount = formatCount;
|
|
15
|
+
/** Short byte text: "812 MB", "3.6 GB", "24 GB". A fraction only when there is one. */
|
|
16
|
+
function formatBytes(value) {
|
|
17
|
+
let size = value;
|
|
18
|
+
for (const unit of ["B", "KB", "MB", "GB", "TB"]) {
|
|
19
|
+
if (size < 1024 || unit === "TB") {
|
|
20
|
+
if (unit === "B")
|
|
21
|
+
return `${Math.round(size)} B`;
|
|
22
|
+
const text = size >= 100 ? size.toFixed(0) : size.toFixed(1);
|
|
23
|
+
return `${text.endsWith(".0") ? text.slice(0, -2) : text} ${unit}`;
|
|
24
|
+
}
|
|
25
|
+
size /= 1024;
|
|
26
|
+
}
|
|
27
|
+
return `${value} B`;
|
|
28
|
+
}
|
|
29
|
+
function formatCount(value) {
|
|
30
|
+
return value.toLocaleString("en-US");
|
|
31
|
+
}
|