@coderook/cli 0.46.1 → 0.46.3
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 +583 -70
- package/dist/cbx/src/local/history.js +27 -4
- package/dist/cbx/src/local/remote.js +15 -0
- package/dist/cbx/src/local/repository.js +90 -11
- package/dist/cbx/src/local/snapshot.js +110 -9
- package/dist/cbx/src/local/store.js +365 -9
- package/dist/cbx/src/local/store_v2.js +269 -0
- package/dist/cbx/src/shared/delta.js +109 -4
- package/dist/cbx/src/shared/native_engine.js +23 -0
- package/dist/cli/src/bin.js +21 -0
- package/dist/cli/src/cli.js +13 -0
- package/dist/cli/src/local_status.js +18 -33
- package/dist/cli/src/paint.js +16 -0
- package/dist/cli/src/status_lines.js +26 -0
- package/engine-bin/darwin-arm64/cbx-engine +0 -0
- package/engine-bin/darwin-x64/cbx-engine +0 -0
- package/engine-bin/linux-arm64/cbx-engine +0 -0
- package/engine-bin/linux-x64/cbx-engine +0 -0
- package/engine-bin/win32-x64/cbx-engine.exe +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.V2IndexFile = exports.V2_PACK_BYTES = exports.MAX_CHAIN = exports.LARGE_WINDOW_LOG = exports.LARGE_BLOCK_BYTES = exports.SOLID_BLOCK_BYTES = exports.SOLID_OBJECT_LIMIT = exports.KIND_DELTA_ZSTD = exports.KIND_DELTA = exports.KIND_SOLID = exports.KIND_WHOLE_ZSTD = exports.KIND_WHOLE = void 0;
|
|
4
|
+
exports.zstdAt = zstdAt;
|
|
5
|
+
exports.compresses = compresses;
|
|
6
|
+
exports.unzstd = unzstd;
|
|
7
|
+
exports.encodeIndex = encodeIndex;
|
|
8
|
+
exports.decodeIndex = decodeIndex;
|
|
9
|
+
exports.solidKey = solidKey;
|
|
10
|
+
exports.sha256Hex = sha256Hex;
|
|
11
|
+
/**
|
|
12
|
+
* How a cbx-local 2 history keeps objects on disk (engine/V2-STORE.md).
|
|
13
|
+
*
|
|
14
|
+
* A v2 history holds exactly the objects a v1 history holds, under the same
|
|
15
|
+
* names; only the way each is kept changes. A v2 pack is a run of records,
|
|
16
|
+
* each one of:
|
|
17
|
+
*
|
|
18
|
+
* whole the object's bytes, as they are or zstd-compressed
|
|
19
|
+
* solid many small objects joined and compressed as one block, so what
|
|
20
|
+
* files share (licence headers, imports, near-copies) is stored once
|
|
21
|
+
* delta an exact COPY/INSERT patch (CRP2, the format the service applies)
|
|
22
|
+
* against another object, as it is or zstd-compressed
|
|
23
|
+
*
|
|
24
|
+
* Every object read is rebuilt and checked against its name before it is
|
|
25
|
+
* used, delta bases included, so damage anywhere is refused rather than
|
|
26
|
+
* passed on.
|
|
27
|
+
*
|
|
28
|
+
* .cbx/objects/v2/<id>.pack records, written whole then renamed
|
|
29
|
+
* .cbx/objects/v2/<id>.idx binary index, sorted by name with a fan-out
|
|
30
|
+
*/
|
|
31
|
+
const node_crypto_1 = require("node:crypto");
|
|
32
|
+
const node_zlib_1 = require("node:zlib");
|
|
33
|
+
exports.KIND_WHOLE = 1;
|
|
34
|
+
exports.KIND_WHOLE_ZSTD = 2;
|
|
35
|
+
exports.KIND_SOLID = 3;
|
|
36
|
+
exports.KIND_DELTA = 4;
|
|
37
|
+
exports.KIND_DELTA_ZSTD = 5;
|
|
38
|
+
/** Objects at most this big wait to be joined into a solid block. */
|
|
39
|
+
exports.SOLID_OBJECT_LIMIT = 256 * 1024;
|
|
40
|
+
/** A solid block of small objects holds this much before it is compressed. */
|
|
41
|
+
exports.SOLID_BLOCK_BYTES = 16 * 1024 * 1024;
|
|
42
|
+
/*
|
|
43
|
+
Larger objects that compress are joined too, in bigger blocks with a window
|
|
44
|
+
as big as the block, so one program can be kept as what it shares with
|
|
45
|
+
another: CMake's fourteen largest files took 31.96 MB compressed one by one
|
|
46
|
+
and 23.89 MB together. Git finds the same thing with its deltas after gc.
|
|
47
|
+
*/
|
|
48
|
+
exports.LARGE_BLOCK_BYTES = 64 * 1024 * 1024;
|
|
49
|
+
exports.LARGE_WINDOW_LOG = 26;
|
|
50
|
+
/** A delta may stand on at most this many others; past that the object is kept whole. */
|
|
51
|
+
exports.MAX_CHAIN = 12;
|
|
52
|
+
/** A pack is written once the objects waiting for it reach this much. */
|
|
53
|
+
exports.V2_PACK_BYTES = 128 * 1024 * 1024;
|
|
54
|
+
function zstdAt(raw, level, long = false, windowLog, checksum = false) {
|
|
55
|
+
return (0, node_zlib_1.zstdCompressSync)(raw, {
|
|
56
|
+
params: {
|
|
57
|
+
[node_zlib_1.constants.ZSTD_c_compressionLevel]: level,
|
|
58
|
+
...(long ? { [node_zlib_1.constants.ZSTD_c_enableLongDistanceMatching]: 1 } : {}),
|
|
59
|
+
...(windowLog ? { [node_zlib_1.constants.ZSTD_c_windowLog]: windowLog } : {}),
|
|
60
|
+
/*
|
|
61
|
+
Blocks carry zstd's own checksum: four bytes, and damage to a block is
|
|
62
|
+
refused as damage even where it happens not to change the bytes. The
|
|
63
|
+
digest of every object read is checked either way.
|
|
64
|
+
*/
|
|
65
|
+
...(checksum ? { [node_zlib_1.constants.ZSTD_c_checksumFlag]: 1 } : {}),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Whether bytes are worth compressing, judged from a 64 KiB sample of their middle. */
|
|
70
|
+
function compresses(raw) {
|
|
71
|
+
const sample = raw.length > 131072 ? raw.subarray((raw.length >> 1) - 32768, (raw.length >> 1) + 32768) : raw;
|
|
72
|
+
return zstdAt(sample, 1).length < sample.length * 0.92;
|
|
73
|
+
}
|
|
74
|
+
function unzstd(stored) {
|
|
75
|
+
return (0, node_zlib_1.zstdDecompressSync)(stored);
|
|
76
|
+
}
|
|
77
|
+
const INDEX_MAGIC = Buffer.from("CBXI", "latin1");
|
|
78
|
+
const INDEX_VERSION = 2;
|
|
79
|
+
const ENTRY_BYTES = 32 + 1 + 1 + 4 + 4 + 4 + 4;
|
|
80
|
+
const FANOUT = 256;
|
|
81
|
+
/* Below this many entries the fan-out table costs more than it saves, and is left out. */
|
|
82
|
+
const FANOUT_FROM = 1024;
|
|
83
|
+
const FLAG_FANOUT = 1;
|
|
84
|
+
/**
|
|
85
|
+
* The index of one v2 pack: a 16-byte header, a 256-entry fan-out by first
|
|
86
|
+
* byte (as Git's pack index has) when there are enough entries to need one,
|
|
87
|
+
* fixed 50-byte entries sorted by name, and the names delta bases refer to,
|
|
88
|
+
* once each. A delta's `inner` field is the position of its base in that
|
|
89
|
+
* table.
|
|
90
|
+
*/
|
|
91
|
+
function encodeIndex(entries) {
|
|
92
|
+
const names = [...entries.keys()].sort();
|
|
93
|
+
const bases = [];
|
|
94
|
+
const baseAt = new Map();
|
|
95
|
+
for (const name of names) {
|
|
96
|
+
const base = entries.get(name).base;
|
|
97
|
+
if (base && !baseAt.has(base)) {
|
|
98
|
+
baseAt.set(base, bases.length);
|
|
99
|
+
bases.push(base);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const fanout = names.length >= FANOUT_FROM;
|
|
103
|
+
const entriesAt = 16 + (fanout ? FANOUT * 4 : 0);
|
|
104
|
+
const out = Buffer.alloc(entriesAt + names.length * ENTRY_BYTES + bases.length * 32);
|
|
105
|
+
INDEX_MAGIC.copy(out, 0);
|
|
106
|
+
out.writeUInt8(INDEX_VERSION, 4);
|
|
107
|
+
out.writeUInt8(fanout ? FLAG_FANOUT : 0, 5);
|
|
108
|
+
out.writeUInt32LE(names.length, 8);
|
|
109
|
+
out.writeUInt32LE(bases.length, 12);
|
|
110
|
+
if (fanout) {
|
|
111
|
+
const counts = new Array(FANOUT).fill(0);
|
|
112
|
+
for (const name of names)
|
|
113
|
+
counts[Number.parseInt(name.slice(0, 2), 16)] += 1;
|
|
114
|
+
let running = 0;
|
|
115
|
+
for (let byte = 0; byte < FANOUT; byte += 1) {
|
|
116
|
+
running += counts[byte];
|
|
117
|
+
out.writeUInt32LE(running, 16 + byte * 4);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let at = entriesAt;
|
|
121
|
+
for (const name of names) {
|
|
122
|
+
const entry = entries.get(name);
|
|
123
|
+
Buffer.from(name, "hex").copy(out, at);
|
|
124
|
+
out.writeUInt8(entry.kind, at + 32);
|
|
125
|
+
out.writeUInt8(entry.chain, at + 33);
|
|
126
|
+
out.writeUInt32LE(entry.offset, at + 34);
|
|
127
|
+
out.writeUInt32LE(entry.length, at + 38);
|
|
128
|
+
out.writeUInt32LE(entry.base ? baseAt.get(entry.base) : entry.inner, at + 42);
|
|
129
|
+
out.writeUInt32LE(entry.raw, at + 46);
|
|
130
|
+
at += ENTRY_BYTES;
|
|
131
|
+
}
|
|
132
|
+
for (const base of bases) {
|
|
133
|
+
Buffer.from(base, "hex").copy(out, at);
|
|
134
|
+
at += 32;
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function decodeIndex(bytes) {
|
|
139
|
+
if (bytes.length < 16 || !bytes.subarray(0, 4).equals(INDEX_MAGIC)) {
|
|
140
|
+
throw new Error("A v2 pack index is damaged");
|
|
141
|
+
}
|
|
142
|
+
if (bytes.readUInt8(4) !== INDEX_VERSION)
|
|
143
|
+
throw new Error("A v2 pack index is of a version this cbx does not read");
|
|
144
|
+
const count = bytes.readUInt32LE(8);
|
|
145
|
+
const baseCount = bytes.readUInt32LE(12);
|
|
146
|
+
const entriesAt = 16 + ((bytes.readUInt8(5) & FLAG_FANOUT) !== 0 ? FANOUT * 4 : 0);
|
|
147
|
+
const basesAt = entriesAt + count * ENTRY_BYTES;
|
|
148
|
+
if (bytes.length !== basesAt + baseCount * 32)
|
|
149
|
+
throw new Error("A v2 pack index is damaged");
|
|
150
|
+
const bases = [];
|
|
151
|
+
for (let index = 0; index < baseCount; index += 1) {
|
|
152
|
+
bases.push(bytes.subarray(basesAt + index * 32, basesAt + index * 32 + 32).toString("hex"));
|
|
153
|
+
}
|
|
154
|
+
const entries = new Map();
|
|
155
|
+
for (let index = 0; index < count; index += 1) {
|
|
156
|
+
const at = entriesAt + index * ENTRY_BYTES;
|
|
157
|
+
const kind = bytes.readUInt8(at + 32);
|
|
158
|
+
const inner = bytes.readUInt32LE(at + 42);
|
|
159
|
+
const delta = kind === exports.KIND_DELTA || kind === exports.KIND_DELTA_ZSTD;
|
|
160
|
+
if (delta && inner >= baseCount)
|
|
161
|
+
throw new Error("A v2 pack index is damaged");
|
|
162
|
+
entries.set(bytes.subarray(at, at + 32).toString("hex"), {
|
|
163
|
+
kind,
|
|
164
|
+
chain: bytes.readUInt8(at + 33),
|
|
165
|
+
offset: bytes.readUInt32LE(at + 34),
|
|
166
|
+
length: bytes.readUInt32LE(at + 38),
|
|
167
|
+
inner: delta ? 0 : inner,
|
|
168
|
+
raw: bytes.readUInt32LE(at + 46),
|
|
169
|
+
...(delta ? { base: bases[inner] } : {}),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return entries;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The order small objects are joined in: by extension, then name, so files
|
|
176
|
+
* of one kind sit together where the compressor can see what they share.
|
|
177
|
+
*/
|
|
178
|
+
function solidKey(filePath) {
|
|
179
|
+
if (!filePath)
|
|
180
|
+
return "\u{10ffff}";
|
|
181
|
+
const name = filePath.slice(filePath.lastIndexOf("/") + 1);
|
|
182
|
+
const dot = name.lastIndexOf(".");
|
|
183
|
+
const extension = dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
|
|
184
|
+
return `${extension}\u0000${name.toLowerCase()}\u0000${filePath}`;
|
|
185
|
+
}
|
|
186
|
+
function sha256Hex(bytes) {
|
|
187
|
+
return (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* One v2 index, searched where it lies rather than decoded whole: its
|
|
191
|
+
* entries are sorted by name, so finding one is a binary search over the
|
|
192
|
+
* bytes. A status needs a handful of objects, and decoding every entry of
|
|
193
|
+
* every index into a map of strings was its largest single cost in a v2
|
|
194
|
+
* history (15 ms of 270 for 8,500 files).
|
|
195
|
+
*/
|
|
196
|
+
class V2IndexFile {
|
|
197
|
+
pack;
|
|
198
|
+
count;
|
|
199
|
+
bytes;
|
|
200
|
+
entriesAt;
|
|
201
|
+
basesAt;
|
|
202
|
+
fanout;
|
|
203
|
+
baseCount;
|
|
204
|
+
constructor(pack, bytes) {
|
|
205
|
+
if (bytes.length < 16 || !bytes.subarray(0, 4).equals(INDEX_MAGIC)) {
|
|
206
|
+
throw new Error("A v2 pack index is damaged");
|
|
207
|
+
}
|
|
208
|
+
if (bytes.readUInt8(4) !== INDEX_VERSION)
|
|
209
|
+
throw new Error("A v2 pack index is of a version this cbx does not read");
|
|
210
|
+
this.pack = pack;
|
|
211
|
+
this.bytes = bytes;
|
|
212
|
+
this.count = bytes.readUInt32LE(8);
|
|
213
|
+
this.baseCount = bytes.readUInt32LE(12);
|
|
214
|
+
this.fanout = (bytes.readUInt8(5) & FLAG_FANOUT) !== 0;
|
|
215
|
+
this.entriesAt = 16 + (this.fanout ? FANOUT * 4 : 0);
|
|
216
|
+
this.basesAt = this.entriesAt + this.count * ENTRY_BYTES;
|
|
217
|
+
if (bytes.length !== this.basesAt + this.baseCount * 32)
|
|
218
|
+
throw new Error("A v2 pack index is damaged");
|
|
219
|
+
}
|
|
220
|
+
find(digest) {
|
|
221
|
+
const key = Buffer.from(digest, "hex");
|
|
222
|
+
if (key.length !== 32)
|
|
223
|
+
return undefined;
|
|
224
|
+
let low = 0;
|
|
225
|
+
let high = this.count;
|
|
226
|
+
if (this.fanout) {
|
|
227
|
+
const first = key[0];
|
|
228
|
+
low = first === 0 ? 0 : this.bytes.readUInt32LE(16 + (first - 1) * 4);
|
|
229
|
+
high = this.bytes.readUInt32LE(16 + first * 4);
|
|
230
|
+
}
|
|
231
|
+
while (low < high) {
|
|
232
|
+
const middle = (low + high) >> 1;
|
|
233
|
+
const at = this.entriesAt + middle * ENTRY_BYTES;
|
|
234
|
+
const order = key.compare(this.bytes, at, at + 32);
|
|
235
|
+
if (order === 0)
|
|
236
|
+
return this.entryAt(at);
|
|
237
|
+
if (order < 0)
|
|
238
|
+
high = middle;
|
|
239
|
+
else
|
|
240
|
+
low = middle + 1;
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
entryAt(at) {
|
|
245
|
+
const kind = this.bytes.readUInt8(at + 32);
|
|
246
|
+
const inner = this.bytes.readUInt32LE(at + 42);
|
|
247
|
+
const delta = kind === exports.KIND_DELTA || kind === exports.KIND_DELTA_ZSTD;
|
|
248
|
+
if (delta && inner >= this.baseCount)
|
|
249
|
+
throw new Error("A v2 pack index is damaged");
|
|
250
|
+
return {
|
|
251
|
+
kind,
|
|
252
|
+
chain: this.bytes.readUInt8(at + 33),
|
|
253
|
+
offset: this.bytes.readUInt32LE(at + 34),
|
|
254
|
+
length: this.bytes.readUInt32LE(at + 38),
|
|
255
|
+
inner: delta ? 0 : inner,
|
|
256
|
+
raw: this.bytes.readUInt32LE(at + 46),
|
|
257
|
+
...(delta
|
|
258
|
+
? { base: this.bytes.subarray(this.basesAt + inner * 32, this.basesAt + inner * 32 + 32).toString("hex") }
|
|
259
|
+
: {}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
*names() {
|
|
263
|
+
for (let index = 0; index < this.count; index += 1) {
|
|
264
|
+
const at = this.entriesAt + index * ENTRY_BYTES;
|
|
265
|
+
yield this.bytes.subarray(at, at + 32).toString("hex");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
exports.V2IndexFile = V2IndexFile;
|
|
@@ -18,6 +18,7 @@ exports.deltaBlockSize = deltaBlockSize;
|
|
|
18
18
|
exports.estimateDeltaSignatureBytes = estimateDeltaSignatureBytes;
|
|
19
19
|
exports.createDeltaSignature = createDeltaSignature;
|
|
20
20
|
exports.parseDeltaSignature = parseDeltaSignature;
|
|
21
|
+
exports.createExactDeltaPatch = createExactDeltaPatch;
|
|
21
22
|
exports.createDeltaPatch = createDeltaPatch;
|
|
22
23
|
exports.deltaPatchTargetSize = deltaPatchTargetSize;
|
|
23
24
|
exports.applyDeltaPatch = applyDeltaPatch;
|
|
@@ -313,6 +314,108 @@ function encodePatchV2(targetSize, operations) {
|
|
|
313
314
|
}
|
|
314
315
|
return out;
|
|
315
316
|
}
|
|
317
|
+
/* Bytes a run must share with the base before it is worth a COPY over a literal. */
|
|
318
|
+
const EXACT_MIN_COPY = 12;
|
|
319
|
+
const EXACT_WINDOW = 16;
|
|
320
|
+
function windowKey(bytes, at) {
|
|
321
|
+
let hash = 0x811c9dc5;
|
|
322
|
+
for (let index = 0; index < EXACT_WINDOW; index += 1) {
|
|
323
|
+
hash = Math.imul(hash ^ (bytes[at + index] ?? 0), 0x01000193);
|
|
324
|
+
}
|
|
325
|
+
return hash >>> 0;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Build a V2 patch from the base bytes themselves, for a client that still
|
|
329
|
+
* holds the version it is editing, the way Git's sender holds the objects the
|
|
330
|
+
* receiver has. No signature is fetched, and copies start and end on any
|
|
331
|
+
* byte, so a one-byte edit costs a few bytes instead of a whole block.
|
|
332
|
+
*
|
|
333
|
+
* The result is the same CRP2 format the receiver-signature path produces:
|
|
334
|
+
* any service that applies those applies this, and verifies the digest after.
|
|
335
|
+
*/
|
|
336
|
+
function createExactDeltaPatch(base, target,
|
|
337
|
+
/* The transport's limit unless said; a local history keeps larger pieces as deltas too. */
|
|
338
|
+
maxBytes = exports.DELTA_MAX_FILE_BYTES) {
|
|
339
|
+
if (target.byteLength > maxBytes || base.byteLength > maxBytes) {
|
|
340
|
+
throw new Error("Delta target exceeds the bounded transport limit");
|
|
341
|
+
}
|
|
342
|
+
const operations = [];
|
|
343
|
+
/* The common case is one edited region: keep both ends whole. */
|
|
344
|
+
const limit = Math.min(base.byteLength, target.byteLength);
|
|
345
|
+
let prefix = 0;
|
|
346
|
+
while (prefix < limit && base[prefix] === target[prefix])
|
|
347
|
+
prefix += 1;
|
|
348
|
+
let suffix = 0;
|
|
349
|
+
while (suffix < limit - prefix &&
|
|
350
|
+
base[base.byteLength - 1 - suffix] === target[target.byteLength - 1 - suffix])
|
|
351
|
+
suffix += 1;
|
|
352
|
+
if (prefix >= EXACT_MIN_COPY)
|
|
353
|
+
appendOperation(operations, { kind: "copy", offset: 0, length: prefix });
|
|
354
|
+
else
|
|
355
|
+
prefix = 0;
|
|
356
|
+
if (suffix < EXACT_MIN_COPY)
|
|
357
|
+
suffix = 0;
|
|
358
|
+
const baseEnd = base.byteLength - suffix;
|
|
359
|
+
const targetEnd = target.byteLength - suffix;
|
|
360
|
+
/* Every window of the base's middle, by the offset it starts at. */
|
|
361
|
+
const index = new Map();
|
|
362
|
+
/*
|
|
363
|
+
Every offset for a small middle; every sixteenth past 256 KiB, as Git's
|
|
364
|
+
delta indexes, so an 8 MiB base is half a million entries, not eight
|
|
365
|
+
million. The target is still scanned at every byte, so moved text is found.
|
|
366
|
+
*/
|
|
367
|
+
const middle = baseEnd - prefix;
|
|
368
|
+
const stride = middle > 8 * 1024 * 1024 ? EXACT_WINDOW * 4 : middle > 256 * 1024 ? EXACT_WINDOW : 1;
|
|
369
|
+
for (let at = prefix; at + EXACT_WINDOW <= baseEnd; at += stride) {
|
|
370
|
+
const key = windowKey(base, at);
|
|
371
|
+
const list = index.get(key);
|
|
372
|
+
if (!list)
|
|
373
|
+
index.set(key, [at]);
|
|
374
|
+
else if (list.length < 16)
|
|
375
|
+
list.push(at);
|
|
376
|
+
}
|
|
377
|
+
let cursor = prefix;
|
|
378
|
+
let literalStart = prefix;
|
|
379
|
+
while (cursor + EXACT_WINDOW <= targetEnd) {
|
|
380
|
+
const candidates = index.get(windowKey(target, cursor));
|
|
381
|
+
let bestOffset = -1;
|
|
382
|
+
let bestLength = 0;
|
|
383
|
+
let bestBack = 0;
|
|
384
|
+
for (const offset of candidates ?? []) {
|
|
385
|
+
let length = 0;
|
|
386
|
+
while (cursor + length < targetEnd &&
|
|
387
|
+
offset + length < baseEnd &&
|
|
388
|
+
base[offset + length] === target[cursor + length])
|
|
389
|
+
length += 1;
|
|
390
|
+
let back = 0;
|
|
391
|
+
while (back < cursor - literalStart &&
|
|
392
|
+
back < offset - prefix &&
|
|
393
|
+
base[offset - back - 1] === target[cursor - back - 1])
|
|
394
|
+
back += 1;
|
|
395
|
+
if (length + back > bestLength + bestBack) {
|
|
396
|
+
bestOffset = offset;
|
|
397
|
+
bestLength = length;
|
|
398
|
+
bestBack = back;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (bestOffset >= 0 && bestLength + bestBack >= EXACT_MIN_COPY) {
|
|
402
|
+
appendOperation(operations, { kind: "literal", bytes: target.slice(literalStart, cursor - bestBack) });
|
|
403
|
+
appendOperation(operations, {
|
|
404
|
+
kind: "copy",
|
|
405
|
+
offset: bestOffset - bestBack,
|
|
406
|
+
length: bestLength + bestBack,
|
|
407
|
+
});
|
|
408
|
+
cursor += bestLength;
|
|
409
|
+
literalStart = cursor;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
cursor += 1;
|
|
413
|
+
}
|
|
414
|
+
appendOperation(operations, { kind: "literal", bytes: target.slice(literalStart, targetEnd) });
|
|
415
|
+
if (suffix)
|
|
416
|
+
appendOperation(operations, { kind: "copy", offset: baseEnd, length: suffix });
|
|
417
|
+
return encodePatchV2(target.byteLength, operations);
|
|
418
|
+
}
|
|
316
419
|
/** Build COPY/INSERT operations from receiver signatures, without base bytes. */
|
|
317
420
|
function createDeltaPatch(target, signature) {
|
|
318
421
|
if (target.byteLength > exports.DELTA_MAX_FILE_BYTES) {
|
|
@@ -394,9 +497,11 @@ function deltaPatchTargetSize(patch) {
|
|
|
394
497
|
}
|
|
395
498
|
return size;
|
|
396
499
|
}
|
|
397
|
-
function applyDeltaPatch(base, patch
|
|
500
|
+
function applyDeltaPatch(base, patch,
|
|
501
|
+
/* The transport's limit unless said; see createExactDeltaPatch. */
|
|
502
|
+
maxBytes = exports.DELTA_MAX_FILE_BYTES) {
|
|
398
503
|
if (sameMagic(patch, PATCH_MAGIC_V2)) {
|
|
399
|
-
return applyDeltaPatchV2(base, patch);
|
|
504
|
+
return applyDeltaPatchV2(base, patch, maxBytes);
|
|
400
505
|
}
|
|
401
506
|
if (patch.byteLength < PATCH_HEADER_V1_BYTES ||
|
|
402
507
|
!sameMagic(patch, PATCH_MAGIC_V1)) {
|
|
@@ -454,12 +559,12 @@ function applyDeltaPatch(base, patch) {
|
|
|
454
559
|
}
|
|
455
560
|
return out;
|
|
456
561
|
}
|
|
457
|
-
function applyDeltaPatchV2(base, patch) {
|
|
562
|
+
function applyDeltaPatchV2(base, patch, maxBytes = exports.DELTA_MAX_FILE_BYTES) {
|
|
458
563
|
const targetField = readVarUint(patch, 4);
|
|
459
564
|
const targetSize = targetField.value;
|
|
460
565
|
const countField = readVarUint(patch, targetField.next);
|
|
461
566
|
const operationCount = countField.value;
|
|
462
|
-
if (targetSize >
|
|
567
|
+
if (targetSize > maxBytes || operationCount > 1_000_000) {
|
|
463
568
|
throw new Error("Delta patch dimensions are invalid");
|
|
464
569
|
}
|
|
465
570
|
const out = new Uint8Array(targetSize);
|
|
@@ -11,6 +11,7 @@ exports.engineStore = engineStore;
|
|
|
11
11
|
exports.engineMeasure = engineMeasure;
|
|
12
12
|
exports.engineScan = engineScan;
|
|
13
13
|
exports.resetEngineForTests = resetEngineForTests;
|
|
14
|
+
exports.engineQuickStatus = engineQuickStatus;
|
|
14
15
|
/**
|
|
15
16
|
* The native byte engine (cbx/engine, Rust), when there is one.
|
|
16
17
|
*
|
|
@@ -287,3 +288,25 @@ function resetEngineForTests() {
|
|
|
287
288
|
located = undefined;
|
|
288
289
|
broken = null;
|
|
289
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* A quiet status answered by the engine alone (engine/src/quick.rs): "clean"
|
|
293
|
+
* only when a cbx-local 2 history's folder is exactly the line's newest save,
|
|
294
|
+
* judged from the stamp status leaves in the index and a walk of the folder.
|
|
295
|
+
* Anything else, and an engine too old to know the command, is null or
|
|
296
|
+
* "unknown", and the full status answers. Never marks the engine broken: an
|
|
297
|
+
* older engine still scans for that full status.
|
|
298
|
+
*/
|
|
299
|
+
async function engineQuickStatus(from) {
|
|
300
|
+
if (!enginePath() || broken || process.env.CBX_ENGINE_QUICK === "off" || !usable())
|
|
301
|
+
return null;
|
|
302
|
+
const answer = await run(["quick-status", from], "");
|
|
303
|
+
if (answer.code !== 0)
|
|
304
|
+
return null;
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(answer.stdout);
|
|
307
|
+
return parsed.protocol === PROTOCOL ? parsed : null;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
package/dist/cli/src/bin.js
CHANGED
|
@@ -57,6 +57,27 @@ async function quickStatus() {
|
|
|
57
57
|
return null;
|
|
58
58
|
if (await (0, reminder_gate_js_1.reminderDue)("status"))
|
|
59
59
|
return null;
|
|
60
|
+
/*
|
|
61
|
+
A folder that is exactly its last save is answered by the engine alone,
|
|
62
|
+
from the stamp the last status left (engine/src/quick.rs): one walk and
|
|
63
|
+
four small reads, and none of the history code loaded here. Anything it
|
|
64
|
+
cannot vouch for is answered below as before.
|
|
65
|
+
*/
|
|
66
|
+
const { engineQuickStatus } = await Promise.resolve().then(() => __importStar(require("../../cbx/src/shared/native_engine.js")));
|
|
67
|
+
const quick = await engineQuickStatus(process.cwd());
|
|
68
|
+
if (quick?.answer === "clean" || quick?.answer === "changed") {
|
|
69
|
+
const { accent, bold, dim, short } = await Promise.resolve().then(() => __importStar(require("./paint.js")));
|
|
70
|
+
console.log(`${bold(quick.project)} on ${accent(quick.line)}` +
|
|
71
|
+
dim(` last saved ${short(quick.head)}: ${quick.message.split("\n")[0]}`));
|
|
72
|
+
if (quick.answer === "clean") {
|
|
73
|
+
console.log("Nothing has changed since the last save.");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const { printChangedSummary } = await Promise.resolve().then(() => __importStar(require("./status_lines.js")));
|
|
77
|
+
printChangedSummary({ added: quick.added, modified: quick.modified, deleted: quick.deleted });
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
60
81
|
const { Repository } = await Promise.resolve().then(() => __importStar(require("../../cbx/src/local/repository.js")));
|
|
61
82
|
const repository = await Repository.find(process.cwd());
|
|
62
83
|
if (!repository)
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -1267,14 +1267,27 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
1267
1267
|
command now does it itself, a few times, before giving up.
|
|
1268
1268
|
*/
|
|
1269
1269
|
const resumeAfter = [10, 30, 60];
|
|
1270
|
+
/*
|
|
1271
|
+
The project an attempt created, for the attempts after it. A first save
|
|
1272
|
+
creates its project while planning, and the project stays once anything
|
|
1273
|
+
has been sent so a retry can reuse what arrived; but the retry used to ask
|
|
1274
|
+
to create it again under the same name and was refused, so a dropped
|
|
1275
|
+
connection during a first save could never be picked up.
|
|
1276
|
+
*/
|
|
1277
|
+
let createdRepositoryId = null;
|
|
1270
1278
|
for (let resumed = 0;; resumed += 1) {
|
|
1271
1279
|
try {
|
|
1280
|
+
if (createdRepositoryId && !uploadRequest.repositoryId) {
|
|
1281
|
+
Object.assign(uploadRequest, { repositoryId: createdRepositoryId });
|
|
1282
|
+
}
|
|
1272
1283
|
const plan = await uploader.plan(uploadRequest, (progress) => {
|
|
1273
1284
|
line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
|
|
1274
1285
|
`${"plan".padEnd(7)} ${progress.files}/${progress.totalFiles} ` +
|
|
1275
1286
|
dim(progress.path.slice(-40)));
|
|
1276
1287
|
});
|
|
1277
1288
|
done(line);
|
|
1289
|
+
if (plan.repositoryCreated)
|
|
1290
|
+
createdRepositoryId = plan.repositoryId;
|
|
1278
1291
|
console.log(`${accent("Upload plan")} ${bytes(plan.sourceBytes)} selected → ` +
|
|
1279
1292
|
`${bytes(plan.compactedBytes)} compacted; ${bytes(plan.chargeableBytes)} new storage.`);
|
|
1280
1293
|
console.log(plan.allowance.exempt
|
|
@@ -3,8 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
exports.printChanges = printChanges;
|
|
6
|
+
exports.short = exports.red = exports.printChanges = exports.plural = exports.green = exports.dim = exports.bold = exports.accent = void 0;
|
|
8
7
|
exports.printLocalStatus = printLocalStatus;
|
|
9
8
|
/**
|
|
10
9
|
* What `cbx status` prints in a folder with a local history, kept apart from
|
|
@@ -17,37 +16,25 @@ exports.printLocalStatus = printLocalStatus;
|
|
|
17
16
|
*/
|
|
18
17
|
const node_path_1 = __importDefault(require("node:path"));
|
|
19
18
|
const history_js_1 = require("../../cbx/src/local/history.js");
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
exports
|
|
23
|
-
exports
|
|
24
|
-
exports
|
|
25
|
-
exports
|
|
26
|
-
exports
|
|
27
|
-
const
|
|
28
|
-
exports.
|
|
29
|
-
|
|
30
|
-
exports.plural = plural;
|
|
31
|
-
function printChanges(changes, limit = 50) {
|
|
32
|
-
const rows = [
|
|
33
|
-
...changes.added.map((file) => [(0, exports.green)("added "), file]),
|
|
34
|
-
...changes.modified.map((file) => [(0, exports.accent)("changed "), file]),
|
|
35
|
-
...changes.deleted.map((file) => [(0, exports.red)("deleted "), file]),
|
|
36
|
-
].sort((left, right) => (left[1] < right[1] ? -1 : left[1] > right[1] ? 1 : 0));
|
|
37
|
-
for (const [mark, file] of rows.slice(0, limit))
|
|
38
|
-
console.log(` ${mark}${file}`);
|
|
39
|
-
if (rows.length > limit)
|
|
40
|
-
console.log((0, exports.dim)(` … and ${rows.length - limit} more`));
|
|
41
|
-
}
|
|
19
|
+
const paint_js_1 = require("./paint.js");
|
|
20
|
+
Object.defineProperty(exports, "accent", { enumerable: true, get: function () { return paint_js_1.accent; } });
|
|
21
|
+
Object.defineProperty(exports, "bold", { enumerable: true, get: function () { return paint_js_1.bold; } });
|
|
22
|
+
Object.defineProperty(exports, "dim", { enumerable: true, get: function () { return paint_js_1.dim; } });
|
|
23
|
+
Object.defineProperty(exports, "green", { enumerable: true, get: function () { return paint_js_1.green; } });
|
|
24
|
+
Object.defineProperty(exports, "red", { enumerable: true, get: function () { return paint_js_1.red; } });
|
|
25
|
+
Object.defineProperty(exports, "short", { enumerable: true, get: function () { return paint_js_1.short; } });
|
|
26
|
+
const status_lines_js_1 = require("./status_lines.js");
|
|
27
|
+
Object.defineProperty(exports, "plural", { enumerable: true, get: function () { return status_lines_js_1.plural; } });
|
|
28
|
+
Object.defineProperty(exports, "printChanges", { enumerable: true, get: function () { return status_lines_js_1.printChanges; } });
|
|
42
29
|
/** The status of a local history, printed. */
|
|
43
30
|
async function printLocalStatus(repository) {
|
|
44
31
|
const now = await (0, history_js_1.status)(repository);
|
|
45
|
-
console.log(`${(0,
|
|
46
|
-
(now.head ? (0,
|
|
32
|
+
console.log(`${(0, paint_js_1.bold)(node_path_1.default.basename(repository.root))} on ${(0, paint_js_1.accent)(now.line)}` +
|
|
33
|
+
(now.head ? (0, paint_js_1.dim)(` last saved ${(0, paint_js_1.short)(now.head.id)}: ${now.head.message.split("\n")[0]}`) : (0, paint_js_1.dim)(" nothing saved yet")));
|
|
47
34
|
if (now.merging) {
|
|
48
35
|
const left = now.merging.unresolved.length;
|
|
49
|
-
console.log(`Merging ${(0,
|
|
50
|
-
(left ? (0,
|
|
36
|
+
console.log(`Merging ${(0, paint_js_1.accent)((0, paint_js_1.short)(now.merging.theirs))}: ` +
|
|
37
|
+
(left ? (0, paint_js_1.red)(`${(0, status_lines_js_1.plural)(left, "file")} still ${left === 1 ? "has" : "have"} conflict markers`) : (0, paint_js_1.green)("no conflict markers left")));
|
|
51
38
|
for (const conflict of now.merging.conflicts) {
|
|
52
39
|
const how = conflict.kind === "text"
|
|
53
40
|
? now.merging.unresolved.includes(conflict.path) ? "markers in it" : "edited"
|
|
@@ -56,17 +43,15 @@ async function printLocalStatus(repository) {
|
|
|
56
43
|
: conflict.kind === "deleted-here"
|
|
57
44
|
? "deleted here, changed there; their copy is in the folder"
|
|
58
45
|
: "changed here, deleted there; this copy is in the folder";
|
|
59
|
-
console.log(` ${(0,
|
|
46
|
+
console.log(` ${(0, paint_js_1.red)("conflict")} ${conflict.path} ${(0, paint_js_1.dim)(how)}`);
|
|
60
47
|
}
|
|
61
|
-
console.log((0,
|
|
48
|
+
console.log((0, paint_js_1.dim)(`Finish with cbx save, take a side with cbx merge --mine or --theirs, or cbx merge --cancel.`));
|
|
62
49
|
}
|
|
63
50
|
const count = (0, history_js_1.changeCount)(now.changes);
|
|
64
51
|
if (!count) {
|
|
65
52
|
console.log("Nothing has changed since the last save.");
|
|
66
53
|
return 0;
|
|
67
54
|
}
|
|
68
|
-
|
|
69
|
-
printChanges(now.changes);
|
|
70
|
-
console.log((0, exports.dim)(`cbx diff shows the lines; cbx save -m "…" records them.`));
|
|
55
|
+
(0, status_lines_js_1.printChangedSummary)(now.changes);
|
|
71
56
|
return 0;
|
|
72
57
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.short = exports.accent = exports.green = exports.red = exports.bold = exports.dim = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The colours cbx prints in, on their own so the quickest paths (bin.ts)
|
|
6
|
+
* can print exactly what the full commands print without loading them.
|
|
7
|
+
*/
|
|
8
|
+
const ESC = String.fromCharCode(27);
|
|
9
|
+
const paint = (code) => (value) => `${ESC}[${code}m${value}${ESC}[0m`;
|
|
10
|
+
exports.dim = paint("2");
|
|
11
|
+
exports.bold = paint("1");
|
|
12
|
+
exports.red = paint("31");
|
|
13
|
+
exports.green = paint("32");
|
|
14
|
+
exports.accent = paint("33");
|
|
15
|
+
const short = (id) => id.slice(0, 10);
|
|
16
|
+
exports.short = short;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.plural = void 0;
|
|
4
|
+
exports.printChanges = printChanges;
|
|
5
|
+
exports.printChangedSummary = printChangedSummary;
|
|
6
|
+
const paint_js_1 = require("./paint.js");
|
|
7
|
+
const plural = (count, one, many = `${one}s`) => `${count.toLocaleString()} ${count === 1 ? one : many}`;
|
|
8
|
+
exports.plural = plural;
|
|
9
|
+
function printChanges(changes, limit = 50) {
|
|
10
|
+
const rows = [
|
|
11
|
+
...changes.added.map((file) => [(0, paint_js_1.green)("added "), file]),
|
|
12
|
+
...changes.modified.map((file) => [(0, paint_js_1.accent)("changed "), file]),
|
|
13
|
+
...changes.deleted.map((file) => [(0, paint_js_1.red)("deleted "), file]),
|
|
14
|
+
].sort((left, right) => (left[1] < right[1] ? -1 : left[1] > right[1] ? 1 : 0));
|
|
15
|
+
for (const [mark, file] of rows.slice(0, limit))
|
|
16
|
+
console.log(` ${mark}${file}`);
|
|
17
|
+
if (rows.length > limit)
|
|
18
|
+
console.log((0, paint_js_1.dim)(` … and ${rows.length - limit} more`));
|
|
19
|
+
}
|
|
20
|
+
/** Everything after the first line when something changed. */
|
|
21
|
+
function printChangedSummary(changes) {
|
|
22
|
+
const count = changes.added.length + changes.modified.length + changes.deleted.length;
|
|
23
|
+
console.log(`${(0, exports.plural)(count, "file")} changed since the last save:`);
|
|
24
|
+
printChanges(changes);
|
|
25
|
+
console.log((0, paint_js_1.dim)(`cbx diff shows the lines; cbx save -m "…" records them.`));
|
|
26
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.3",
|
|
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",
|