@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.
@@ -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 > exports.DELTA_MAX_FILE_BYTES || operationCount > 1_000_000) {
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
+ }
@@ -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)
@@ -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.plural = exports.short = exports.accent = exports.green = exports.red = exports.bold = exports.dim = void 0;
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 ESC = String.fromCharCode(27);
21
- const paint = (code) => (value) => `${ESC}[${code}m${value}${ESC}[0m`;
22
- exports.dim = paint("2");
23
- exports.bold = paint("1");
24
- exports.red = paint("31");
25
- exports.green = paint("32");
26
- exports.accent = paint("33");
27
- const short = (id) => id.slice(0, 10);
28
- exports.short = short;
29
- const plural = (count, one, many = `${one}s`) => `${count.toLocaleString()} ${count === 1 ? one : many}`;
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, exports.bold)(node_path_1.default.basename(repository.root))} on ${(0, exports.accent)(now.line)}` +
46
- (now.head ? (0, exports.dim)(` last saved ${(0, exports.short)(now.head.id)}: ${now.head.message.split("\n")[0]}`) : (0, exports.dim)(" nothing saved yet")));
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, exports.accent)((0, exports.short)(now.merging.theirs))}: ` +
50
- (left ? (0, exports.red)(`${(0, exports.plural)(left, "file")} still ${left === 1 ? "has" : "have"} conflict markers`) : (0, exports.green)("no conflict markers left")));
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, exports.red)("conflict")} ${conflict.path} ${(0, exports.dim)(how)}`);
46
+ console.log(` ${(0, paint_js_1.red)("conflict")} ${conflict.path} ${(0, paint_js_1.dim)(how)}`);
60
47
  }
61
- console.log((0, exports.dim)(`Finish with cbx save, take a side with cbx merge --mine or --theirs, or cbx merge --cancel.`));
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
- console.log(`${(0, exports.plural)(count, "file")} changed since the last save:`);
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.1",
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",