@coderook/cli 0.34.2 → 0.34.4
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 +158 -121
- package/dist/cli/src/api.js +8 -2
- package/dist/cli/src/attach_command.js +18 -22
- package/dist/cli/src/cli.js +48 -12
- package/dist/cli/src/colour.js +16 -0
- package/dist/cli/src/help.js +12 -14
- package/dist/cli/src/licence_commands.js +34 -38
- package/dist/cli/src/network.js +25 -5
- package/dist/cli/src/project_commands.js +43 -47
- package/dist/cli/src/service_commands.js +169 -120
- package/dist/cli/src/track_commands.js +21 -25
- package/dist/cli/src/transfer_command.js +28 -32
- package/dist/cli/src/version_commands.js +66 -70
- package/package.json +1 -1
|
@@ -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, with cbx or plain git, from Claude Code.",
|
|
5
|
-
"version": "0.34.
|
|
5
|
+
"version": "0.34.4",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
|
@@ -1081,6 +1081,16 @@ class Uploader {
|
|
|
1081
1081
|
shown = count;
|
|
1082
1082
|
return shown;
|
|
1083
1083
|
};
|
|
1084
|
+
/*
|
|
1085
|
+
Identical files are one upload.
|
|
1086
|
+
|
|
1087
|
+
A folder holding two copies of the same file declares it twice, and both
|
|
1088
|
+
copies used to go up in full: side by side in one batch, or in two lanes
|
|
1089
|
+
at the same moment. The second copy now waits for the first to finish,
|
|
1090
|
+
after which the service already holds the bytes and the copy costs a
|
|
1091
|
+
question rather than a transfer.
|
|
1092
|
+
*/
|
|
1093
|
+
const sending = new Map();
|
|
1084
1094
|
const runSection = async (section) => {
|
|
1085
1095
|
const declarations = section.files.map((file) => declarationByPath.get(file.path));
|
|
1086
1096
|
/*
|
|
@@ -1151,6 +1161,17 @@ class Uploader {
|
|
|
1151
1161
|
*/
|
|
1152
1162
|
const missing = declarations.filter((one) => !alreadyHeld.has(one.sha256));
|
|
1153
1163
|
const smallOnes = missing.filter((one) => one.size <= BATCH_FILE_LIMIT);
|
|
1164
|
+
/* The first file with each digest is sent; the rest are counted when it lands. */
|
|
1165
|
+
const copiesOf = new Map();
|
|
1166
|
+
const smallUnique = smallOnes.filter((one) => {
|
|
1167
|
+
const copies = copiesOf.get(one.sha256);
|
|
1168
|
+
if (copies) {
|
|
1169
|
+
copies.push(one);
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
copiesOf.set(one.sha256, []);
|
|
1173
|
+
return true;
|
|
1174
|
+
});
|
|
1154
1175
|
// 3. Send the objects.
|
|
1155
1176
|
/*
|
|
1156
1177
|
What was sent, in whichever shape it was sent. A small file is one
|
|
@@ -1177,7 +1198,7 @@ class Uploader {
|
|
|
1177
1198
|
(0, profile_js_1.counted)("files sent as deltas", 1);
|
|
1178
1199
|
}
|
|
1179
1200
|
}
|
|
1180
|
-
const smallForPacking =
|
|
1201
|
+
const smallForPacking = smallUnique.filter((one) => !batched.has(one.sha256));
|
|
1181
1202
|
if (smallForPacking.length > 1) {
|
|
1182
1203
|
let pack = [];
|
|
1183
1204
|
let packBytes = 0;
|
|
@@ -1281,6 +1302,8 @@ class Uploader {
|
|
|
1281
1302
|
if (!batched.has(digest) && !packedInto.has(digest))
|
|
1282
1303
|
continue;
|
|
1283
1304
|
progress.finished(item.declaration.size, exactWireBytes === null ? item.encoded.body.byteLength : 0);
|
|
1305
|
+
for (const copy of copiesOf.get(digest) ?? [])
|
|
1306
|
+
progress.finished(copy.size, 0);
|
|
1284
1307
|
}
|
|
1285
1308
|
if (exactWireBytes !== null)
|
|
1286
1309
|
progress.finished(0, exactWireBytes);
|
|
@@ -1336,121 +1359,51 @@ class Uploader {
|
|
|
1336
1359
|
const queue = declarations.entries();
|
|
1337
1360
|
const runWorker = async () => {
|
|
1338
1361
|
for (const [index, declaration] of queue) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
percent: 20 + Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1348
|
-
bytesPerSecond: progress.rate(),
|
|
1349
|
-
});
|
|
1350
|
-
/*
|
|
1351
|
-
Content the service already holds does not need sending. Asking costs
|
|
1352
|
-
one round trip and saves the whole file — which after an interrupted
|
|
1353
|
-
save is the difference between re-uploading a project and re-uploading
|
|
1354
|
-
nothing.
|
|
1355
|
-
*/
|
|
1356
|
-
/*
|
|
1357
|
-
A large file goes up in pieces, cut where its own content says.
|
|
1358
|
-
|
|
1359
|
-
This is what CBX is for and what it was never doing on the wire. The
|
|
1360
|
-
boundaries come from the same chunker the .cbx bundle uses, so an
|
|
1361
|
-
edit in the middle of a large file leaves the chunks either side of it
|
|
1362
|
-
untouched — and an untouched chunk is one the service already holds,
|
|
1363
|
-
which costs a question rather than a transfer. A small file is left
|
|
1364
|
-
whole: cutting it up would trade one request for several and save
|
|
1365
|
-
nothing.
|
|
1366
|
-
*/
|
|
1367
|
-
const chunkProfile = (await this.microchunkAllowed())
|
|
1368
|
-
? (0, chunking_js_1.microchunkProfileForFileSize)(declaration.size)
|
|
1369
|
-
: (0, chunking_js_1.profileForFileSize)(declaration.size);
|
|
1370
|
-
if (chunkProfile &&
|
|
1371
|
-
declaration.size >= CHUNK_THRESHOLD &&
|
|
1372
|
-
(await this.chunkingAllowed())) {
|
|
1373
|
-
const pieces = await this.putChunked(repositoryId, declaration, chunkProfile, (offset) => {
|
|
1374
|
-
progress.lane(index, offset);
|
|
1375
|
-
report({
|
|
1376
|
-
stage: "upload",
|
|
1377
|
-
files: advance(index),
|
|
1378
|
-
totalFiles: everyDeclaration.length,
|
|
1379
|
-
bytes: progress.bytes(),
|
|
1380
|
-
totalBytes,
|
|
1381
|
-
path: declaration.path,
|
|
1382
|
-
percent: 20 +
|
|
1383
|
-
Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1384
|
-
bytesPerSecond: progress.rate(),
|
|
1385
|
-
});
|
|
1386
|
-
});
|
|
1387
|
-
progress.laneDone(index);
|
|
1388
|
-
/*
|
|
1389
|
-
A chunk is not a file. Counting every reused piece here made a
|
|
1390
|
-
one-file save report a negative number of sent files. The selected
|
|
1391
|
-
file counts as already stored only when every one of its pieces was
|
|
1392
|
-
reused and no payload crossed the wire.
|
|
1393
|
-
*/
|
|
1394
|
-
if (pieces.sentBytes === 0)
|
|
1395
|
-
progress.alreadyHeld();
|
|
1396
|
-
uploaded.push({
|
|
1397
|
-
path: declaration.path,
|
|
1398
|
-
sha256: declaration.sha256,
|
|
1399
|
-
chunks: pieces.chunks,
|
|
1400
|
-
sourceSize: declaration.size,
|
|
1401
|
-
storedSize: pieces.chunks.reduce((sum, one) => sum + one.storedSize, 0),
|
|
1402
|
-
mediaType: declaration.mediaType,
|
|
1403
|
-
});
|
|
1404
|
-
progress.finished(declaration.size, pieces.sentBytes);
|
|
1405
|
-
continue;
|
|
1362
|
+
const earlier = sending.get(declaration.sha256);
|
|
1363
|
+
if (earlier)
|
|
1364
|
+
await earlier;
|
|
1365
|
+
let release = () => { };
|
|
1366
|
+
if (!earlier) {
|
|
1367
|
+
sending.set(declaration.sha256, new Promise((done) => {
|
|
1368
|
+
release = done;
|
|
1369
|
+
}));
|
|
1406
1370
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
storedSize: fromPack.storedSize,
|
|
1416
|
-
mediaType: declaration.mediaType,
|
|
1417
|
-
});
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
const fromBatch = batched.get(declaration.sha256);
|
|
1421
|
-
if (fromBatch) {
|
|
1422
|
-
uploaded.push({
|
|
1371
|
+
try {
|
|
1372
|
+
this.check();
|
|
1373
|
+
report({
|
|
1374
|
+
stage: "upload",
|
|
1375
|
+
files: advance(index),
|
|
1376
|
+
totalFiles: everyDeclaration.length,
|
|
1377
|
+
bytes: progress.bytes(),
|
|
1378
|
+
totalBytes,
|
|
1423
1379
|
path: declaration.path,
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
storedSize: fromBatch.size,
|
|
1427
|
-
mediaType: declaration.mediaType,
|
|
1380
|
+
percent: 20 + Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1381
|
+
bytesPerSecond: progress.rate(),
|
|
1428
1382
|
});
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1431
|
-
const alreadyStored = alreadyHeld.get(declaration.sha256) ??
|
|
1432
1383
|
/*
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1384
|
+
Content the service already holds does not need sending. Asking costs
|
|
1385
|
+
one round trip and saves the whole file — which after an interrupted
|
|
1386
|
+
save is the difference between re-uploading a project and re-uploading
|
|
1387
|
+
nothing.
|
|
1436
1388
|
*/
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1389
|
+
/*
|
|
1390
|
+
A large file goes up in pieces, cut where its own content says.
|
|
1391
|
+
|
|
1392
|
+
This is what CBX is for and what it was never doing on the wire. The
|
|
1393
|
+
boundaries come from the same chunker the .cbx bundle uses, so an
|
|
1394
|
+
edit in the middle of a large file leaves the chunks either side of it
|
|
1395
|
+
untouched — and an untouched chunk is one the service already holds,
|
|
1396
|
+
which costs a question rather than a transfer. A small file is left
|
|
1397
|
+
whole: cutting it up would trade one request for several and save
|
|
1398
|
+
nothing.
|
|
1399
|
+
*/
|
|
1400
|
+
const chunkProfile = (await this.microchunkAllowed())
|
|
1401
|
+
? (0, chunking_js_1.microchunkProfileForFileSize)(declaration.size)
|
|
1402
|
+
: (0, chunking_js_1.profileForFileSize)(declaration.size);
|
|
1403
|
+
if (chunkProfile &&
|
|
1404
|
+
declaration.size >= CHUNK_THRESHOLD &&
|
|
1405
|
+
(await this.chunkingAllowed())) {
|
|
1406
|
+
const pieces = await this.putChunked(repositoryId, declaration, chunkProfile, (offset) => {
|
|
1454
1407
|
progress.lane(index, offset);
|
|
1455
1408
|
report({
|
|
1456
1409
|
stage: "upload",
|
|
@@ -1463,16 +1416,100 @@ class Uploader {
|
|
|
1463
1416
|
Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1464
1417
|
bytesPerSecond: progress.rate(),
|
|
1465
1418
|
});
|
|
1466
|
-
})
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1419
|
+
});
|
|
1420
|
+
progress.laneDone(index);
|
|
1421
|
+
/*
|
|
1422
|
+
A chunk is not a file. Counting every reused piece here made a
|
|
1423
|
+
one-file save report a negative number of sent files. The selected
|
|
1424
|
+
file counts as already stored only when every one of its pieces was
|
|
1425
|
+
reused and no payload crossed the wire.
|
|
1426
|
+
*/
|
|
1427
|
+
if (pieces.sentBytes === 0)
|
|
1428
|
+
progress.alreadyHeld();
|
|
1429
|
+
uploaded.push({
|
|
1430
|
+
path: declaration.path,
|
|
1431
|
+
sha256: declaration.sha256,
|
|
1432
|
+
chunks: pieces.chunks,
|
|
1433
|
+
sourceSize: declaration.size,
|
|
1434
|
+
storedSize: pieces.chunks.reduce((sum, one) => sum + one.storedSize, 0),
|
|
1435
|
+
mediaType: declaration.mediaType,
|
|
1436
|
+
});
|
|
1437
|
+
progress.finished(declaration.size, pieces.sentBytes);
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
const fromPack = packedInto.get(declaration.sha256);
|
|
1441
|
+
if (fromPack) {
|
|
1442
|
+
uploaded.push({
|
|
1443
|
+
path: declaration.path,
|
|
1444
|
+
sha256: declaration.sha256,
|
|
1445
|
+
pack: fromPack,
|
|
1446
|
+
sourceSize: declaration.size,
|
|
1447
|
+
/* Its share of the pack, so the version's totals stay honest. */
|
|
1448
|
+
storedSize: fromPack.storedSize,
|
|
1449
|
+
mediaType: declaration.mediaType,
|
|
1450
|
+
});
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
const fromBatch = batched.get(declaration.sha256);
|
|
1454
|
+
if (fromBatch) {
|
|
1455
|
+
uploaded.push({
|
|
1456
|
+
path: declaration.path,
|
|
1457
|
+
objectId: fromBatch.objectId,
|
|
1458
|
+
sourceSize: declaration.size,
|
|
1459
|
+
storedSize: fromBatch.size,
|
|
1460
|
+
mediaType: declaration.mediaType,
|
|
1461
|
+
});
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const alreadyStored = alreadyHeld.get(declaration.sha256) ??
|
|
1465
|
+
/*
|
|
1466
|
+
Only asked individually when the batch did not cover it — a digest
|
|
1467
|
+
that changed after the bulk question, because the file was rewritten
|
|
1468
|
+
while this upload was running.
|
|
1469
|
+
*/
|
|
1470
|
+
(await this.findStored(repositoryId, declaration));
|
|
1471
|
+
if (alreadyStored)
|
|
1472
|
+
progress.alreadyHeld();
|
|
1473
|
+
const result = alreadyStored ??
|
|
1474
|
+
(declaration.size <= DIRECT_LIMIT
|
|
1475
|
+
? await (0, profile_js_1.timed)("send files one at a time", () => this.putDirect(repositoryId, declaration)).catch((error) => {
|
|
1476
|
+
// Naming the file turns "it failed" into something actionable.
|
|
1477
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1478
|
+
throw new Error(`${declaration.path}: ${reason}`);
|
|
1479
|
+
})
|
|
1480
|
+
: await this.putMultipart(repositoryId, declaration, (offset) => {
|
|
1481
|
+
/*
|
|
1482
|
+
Recorded before the report reads it. Without this the bar does
|
|
1483
|
+
not move at all during a multipart file: `bytes()` sums the
|
|
1484
|
+
settled total and the open lanes, and this lane's offset is
|
|
1485
|
+
only in the second of those once it has been set.
|
|
1486
|
+
*/
|
|
1487
|
+
progress.lane(index, offset);
|
|
1488
|
+
report({
|
|
1489
|
+
stage: "upload",
|
|
1490
|
+
files: advance(index),
|
|
1491
|
+
totalFiles: everyDeclaration.length,
|
|
1492
|
+
bytes: progress.bytes(),
|
|
1493
|
+
totalBytes,
|
|
1494
|
+
path: declaration.path,
|
|
1495
|
+
percent: 20 +
|
|
1496
|
+
Math.round((progress.bytes() / Math.max(totalBytes, 1)) * 72),
|
|
1497
|
+
bytesPerSecond: progress.rate(),
|
|
1498
|
+
});
|
|
1499
|
+
}));
|
|
1500
|
+
progress.laneDone(index);
|
|
1501
|
+
uploaded.push({
|
|
1502
|
+
path: declaration.path,
|
|
1503
|
+
objectId: result.objectId,
|
|
1504
|
+
sourceSize: declaration.size,
|
|
1505
|
+
storedSize: result.size,
|
|
1506
|
+
mediaType: declaration.mediaType,
|
|
1507
|
+
});
|
|
1508
|
+
progress.finished(declaration.size, alreadyStored ? 0 : declaration.size);
|
|
1509
|
+
}
|
|
1510
|
+
finally {
|
|
1511
|
+
release();
|
|
1512
|
+
}
|
|
1476
1513
|
}
|
|
1477
1514
|
};
|
|
1478
1515
|
await Promise.all(Array.from({ length: Math.min(UPLOAD_LANES, declarations.length) }, () => runWorker()));
|
package/dist/cli/src/api.js
CHANGED
|
@@ -36,6 +36,7 @@ exports.createIssue = createIssue;
|
|
|
36
36
|
exports.releases = releases;
|
|
37
37
|
exports.collaborators = collaborators;
|
|
38
38
|
exports.invite = invite;
|
|
39
|
+
exports.acceptInvite = acceptInvite;
|
|
39
40
|
exports.watch = watch;
|
|
40
41
|
exports.setWatch = setWatch;
|
|
41
42
|
exports.workflows = workflows;
|
|
@@ -688,9 +689,14 @@ async function collaborators(repositoryId) {
|
|
|
688
689
|
};
|
|
689
690
|
}
|
|
690
691
|
async function invite(repositoryId, input) {
|
|
691
|
-
await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/invites`, {
|
|
692
|
+
const row = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/invites`, { method: "POST", body: input });
|
|
693
|
+
return typeof row?.inviteUrl === "string" ? { inviteUrl: row.inviteUrl } : {};
|
|
694
|
+
}
|
|
695
|
+
/** Join a project from an invite link (or the token inside it). */
|
|
696
|
+
async function acceptInvite(token) {
|
|
697
|
+
return call("/v1/invites/accept", {
|
|
692
698
|
method: "POST",
|
|
693
|
-
body:
|
|
699
|
+
body: { token },
|
|
694
700
|
});
|
|
695
701
|
}
|
|
696
702
|
async function watch(repositoryId) {
|
|
@@ -28,11 +28,7 @@ const api_js_1 = require("./api.js");
|
|
|
28
28
|
const project_commands_js_1 = require("./project_commands.js");
|
|
29
29
|
const version_commands_js_1 = require("./version_commands.js");
|
|
30
30
|
const api_js_2 = require("./api.js");
|
|
31
|
-
const
|
|
32
|
-
const bold = (value) => `[1m${value}[0m`;
|
|
33
|
-
const red = (value) => `[31m${value}[0m`;
|
|
34
|
-
const green = (value) => `[32m${value}[0m`;
|
|
35
|
-
const accent = (value) => `[33m${value}[0m`;
|
|
31
|
+
const colour_js_1 = require("./colour.js");
|
|
36
32
|
/** What the service will take in one request. */
|
|
37
33
|
const DIRECT_LIMIT = 95 * 1024 * 1024;
|
|
38
34
|
/** Comfortably under the cap, so one slow part does not hold the rest up. */
|
|
@@ -114,7 +110,7 @@ async function sendFile(repositoryId, file, size, sha256, mediaType, say) {
|
|
|
114
110
|
repositoryRole: "chunk",
|
|
115
111
|
});
|
|
116
112
|
const parts = Math.ceil(size / PART_SIZE);
|
|
117
|
-
say(dim(` ${readable(size)} in ${parts} parts`));
|
|
113
|
+
say((0, colour_js_1.dim)(` ${readable(size)} in ${parts} parts`));
|
|
118
114
|
const handle = await (0, promises_1.open)(file, "r");
|
|
119
115
|
try {
|
|
120
116
|
for (let index = 0; index < parts; index += 1) {
|
|
@@ -123,7 +119,7 @@ async function sendFile(repositoryId, file, size, sha256, mediaType, say) {
|
|
|
123
119
|
const buffer = Buffer.alloc(length);
|
|
124
120
|
await handle.read(buffer, 0, length, start);
|
|
125
121
|
await (0, api_js_1.uploadPart)(session.uploadSessionId, index + 1, buffer);
|
|
126
|
-
say(dim(` part ${index + 1}/${parts}`));
|
|
122
|
+
say((0, colour_js_1.dim)(` part ${index + 1}/${parts}`));
|
|
127
123
|
}
|
|
128
124
|
}
|
|
129
125
|
finally {
|
|
@@ -134,24 +130,24 @@ async function sendFile(repositoryId, file, size, sha256, mediaType, say) {
|
|
|
134
130
|
async function commandAttach(parsed) {
|
|
135
131
|
const file = parsed.positional[0];
|
|
136
132
|
if (!file) {
|
|
137
|
-
console.error(red("Which file?") + dim(" cbx attach ./release/Installer.exe"));
|
|
133
|
+
console.error((0, colour_js_1.red)("Which file?") + (0, colour_js_1.dim)(" cbx attach ./release/Installer.exe"));
|
|
138
134
|
return 1;
|
|
139
135
|
}
|
|
140
136
|
let size;
|
|
141
137
|
try {
|
|
142
138
|
const info = await (0, promises_1.stat)(file);
|
|
143
139
|
if (!info.isFile()) {
|
|
144
|
-
console.error(red(`${file} is not a file.`));
|
|
140
|
+
console.error((0, colour_js_1.red)(`${file} is not a file.`));
|
|
145
141
|
return 1;
|
|
146
142
|
}
|
|
147
143
|
size = info.size;
|
|
148
144
|
}
|
|
149
145
|
catch {
|
|
150
|
-
console.error(red(`There is no file at ${file}.`));
|
|
146
|
+
console.error((0, colour_js_1.red)(`There is no file at ${file}.`));
|
|
151
147
|
return 1;
|
|
152
148
|
}
|
|
153
149
|
if (size === 0) {
|
|
154
|
-
console.error(red("That file is empty."));
|
|
150
|
+
console.error((0, colour_js_1.red)("That file is empty."));
|
|
155
151
|
return 1;
|
|
156
152
|
}
|
|
157
153
|
/*
|
|
@@ -168,26 +164,26 @@ async function commandAttach(parsed) {
|
|
|
168
164
|
return 1;
|
|
169
165
|
const all = await (0, api_js_2.versions)(project.id);
|
|
170
166
|
if (!all.length) {
|
|
171
|
-
console.error(red("This project has no versions yet."));
|
|
167
|
+
console.error((0, colour_js_1.red)("This project has no versions yet."));
|
|
172
168
|
return 1;
|
|
173
169
|
}
|
|
174
170
|
const target = which.n
|
|
175
171
|
? all.find((one) => String(one.sequence) === which.n.replace(/^v/i, ""))
|
|
176
172
|
: all.reduce((newest, one) => (one.sequence > newest.sequence ? one : newest));
|
|
177
173
|
if (!target) {
|
|
178
|
-
console.error(red(`This project has no version ${which.n}.`));
|
|
174
|
+
console.error((0, colour_js_1.red)(`This project has no version ${which.n}.`));
|
|
179
175
|
return 1;
|
|
180
176
|
}
|
|
181
177
|
const named = parsed.flags.get("name");
|
|
182
178
|
const name = typeof named === "string" && named.trim() ? named.trim() : node_path_1.default.basename(file);
|
|
183
|
-
console.log(`${bold(name)} ${dim(`(${readable(size)})`)} → ${accent(`v${target.sequence}`)} of ${project.name}`);
|
|
179
|
+
console.log(`${(0, colour_js_1.bold)(name)} ${(0, colour_js_1.dim)(`(${readable(size)})`)} → ${(0, colour_js_1.accent)(`v${target.sequence}`)} of ${project.name}`);
|
|
184
180
|
let objectId;
|
|
185
181
|
try {
|
|
186
182
|
const sha256 = await digestOf(file);
|
|
187
183
|
objectId = await sendFile(project.id, file, size, sha256, mediaTypeOf(name), (text) => console.log(text));
|
|
188
184
|
}
|
|
189
185
|
catch (error) {
|
|
190
|
-
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
186
|
+
console.error((0, colour_js_1.red)(error instanceof Error ? error.message : String(error)));
|
|
191
187
|
return 1;
|
|
192
188
|
}
|
|
193
189
|
try {
|
|
@@ -195,23 +191,23 @@ async function commandAttach(parsed) {
|
|
|
195
191
|
}
|
|
196
192
|
catch (error) {
|
|
197
193
|
const message = error instanceof Error ? error.message : String(error);
|
|
198
|
-
console.error(red(message));
|
|
194
|
+
console.error((0, colour_js_1.red)(message));
|
|
199
195
|
if (/already has something called/i.test(message)) {
|
|
200
|
-
console.error(dim(" Use --name to give this one a different name on the version."));
|
|
196
|
+
console.error((0, colour_js_1.dim)(" Use --name to give this one a different name on the version."));
|
|
201
197
|
}
|
|
202
198
|
return 1;
|
|
203
199
|
}
|
|
204
|
-
console.log(green(`Attached. It is a download on v${target.sequence}.`));
|
|
200
|
+
console.log((0, colour_js_1.green)(`Attached. It is a download on v${target.sequence}.`));
|
|
205
201
|
if (!target.name) {
|
|
206
202
|
/*
|
|
207
203
|
Said because it is the difference between attaching and publishing. A
|
|
208
204
|
commit is not offered to anybody, so the download exists and nobody
|
|
209
205
|
outside the project can reach it yet.
|
|
210
206
|
*/
|
|
211
|
-
console.log(dim(" v") +
|
|
212
|
-
dim(String(target.sequence)) +
|
|
213
|
-
dim(" is a commit, so nobody outside the project can take it yet."));
|
|
214
|
-
console.log(dim(` Make it a version: cbx mark ${target.sequence} --name v1.0`));
|
|
207
|
+
console.log((0, colour_js_1.dim)(" v") +
|
|
208
|
+
(0, colour_js_1.dim)(String(target.sequence)) +
|
|
209
|
+
(0, colour_js_1.dim)(" is a commit, so nobody outside the project can take it yet."));
|
|
210
|
+
console.log((0, colour_js_1.dim)(` Make it a version: cbx mark ${target.sequence} --name v1.0`));
|
|
215
211
|
}
|
|
216
212
|
return 0;
|
|
217
213
|
}
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -73,6 +73,10 @@ const bold = (text) => (colour ? `[1m${text}[0m` : text);
|
|
|
73
73
|
const accent = (text) => (colour ? `[33m${text}[0m` : text);
|
|
74
74
|
const red = (text) => (colour ? `[31m${text}[0m` : text);
|
|
75
75
|
const green = (text) => (colour ? `[32m${text}[0m` : text);
|
|
76
|
+
/** "1 file", "3 files". */
|
|
77
|
+
function fileCount(count) {
|
|
78
|
+
return `${count} ${count === 1 ? "file" : "files"}`;
|
|
79
|
+
}
|
|
76
80
|
function bytes(value) {
|
|
77
81
|
if (value >= 1024 ** 3)
|
|
78
82
|
return `${(value / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -288,7 +292,7 @@ async function reconcile(folder, options = {}) {
|
|
|
288
292
|
await (0, config_js_1.writeLink)(folder, known);
|
|
289
293
|
}
|
|
290
294
|
if (behind && !options.quiet) {
|
|
291
|
-
console.log(dim(`The
|
|
295
|
+
console.log(dim(`The project is on v${behind.sequence}; this folder is based on v${known.sequence}.`));
|
|
292
296
|
}
|
|
293
297
|
return {
|
|
294
298
|
link: known,
|
|
@@ -732,6 +736,23 @@ function noteKeptDeletions(folder, baseline, syncing) {
|
|
|
732
736
|
` (${gone.slice(0, 3).join(", ")}${gone.length > 3 ? ", …" : ""}). A save adds and updates;` +
|
|
733
737
|
` run cbx submit --sync to remove ${gone.length === 1 ? "it" : "them"}.`));
|
|
734
738
|
}
|
|
739
|
+
/*
|
|
740
|
+
The service names each clash by its kind ("text_region", "add_add", …).
|
|
741
|
+
Those are its words, not the reader's: say what actually happened.
|
|
742
|
+
*/
|
|
743
|
+
const CONFLICT_WORDS = {
|
|
744
|
+
text_region: "you both changed the same lines",
|
|
745
|
+
modify_delete: "changed on one side, removed on the other",
|
|
746
|
+
add_add: "added on both sides, differently",
|
|
747
|
+
rename: "renamed differently on each side",
|
|
748
|
+
path_collision: "two files now want the same name",
|
|
749
|
+
binary: "not text, so it can't be combined line by line",
|
|
750
|
+
symlink: "a link both sides changed",
|
|
751
|
+
structured: "both changed the same entry",
|
|
752
|
+
};
|
|
753
|
+
function conflictWords(kind) {
|
|
754
|
+
return CONFLICT_WORDS[kind] ?? kind.replace(/_/g, " ");
|
|
755
|
+
}
|
|
735
756
|
async function commandSubmit(parsed) {
|
|
736
757
|
const folder = folderFor(parsed);
|
|
737
758
|
const message = flagText(parsed, "m", "message") ?? "";
|
|
@@ -1114,7 +1135,14 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
1114
1135
|
if (ahead.collidingPaths.length > 5) {
|
|
1115
1136
|
console.log(dim(` and ${ahead.collidingPaths.length - 5} more`));
|
|
1116
1137
|
}
|
|
1117
|
-
|
|
1138
|
+
/*
|
|
1139
|
+
This check compares whole files, so it cannot know yet whether the
|
|
1140
|
+
two of you touched the same lines. The service combines text line
|
|
1141
|
+
by line when it can; saying "it will be put on a merge" here was a
|
|
1142
|
+
promise the next line ("Saved v3") then broke.
|
|
1143
|
+
*/
|
|
1144
|
+
console.log(dim(" If you changed different lines of a text file, the two combine on their own."));
|
|
1145
|
+
console.log(dim(" Otherwise your upload is kept whole on a merge for you to settle."));
|
|
1118
1146
|
console.log(dim(" Run cbx get first if you would rather build on theirs."));
|
|
1119
1147
|
console.log("");
|
|
1120
1148
|
}
|
|
@@ -1238,7 +1266,7 @@ The connection failed: ${text}`));
|
|
|
1238
1266
|
: `${result.mergeTrack.conflicts.length} files overlap`}.`);
|
|
1239
1267
|
console.log(`Your work is complete and kept as ${accent(result.mergeTrack.reference)}:`);
|
|
1240
1268
|
for (const conflict of result.mergeTrack.conflicts.slice(0, 20)) {
|
|
1241
|
-
console.log(` ${red(conflict.path)} ${dim(conflict.kind)}`);
|
|
1269
|
+
console.log(` ${red(conflict.path)} ${dim(conflictWords(conflict.kind))}`);
|
|
1242
1270
|
}
|
|
1243
1271
|
console.log(`\nRun ${accent(`cbx merge ${result.mergeTrack.reference}`)} to decide,` +
|
|
1244
1272
|
` or ${accent("cbx merges")} to see everything waiting.`);
|
|
@@ -1655,7 +1683,7 @@ trackName) {
|
|
|
1655
1683
|
});
|
|
1656
1684
|
// The record is written; the run has not reported success yet.
|
|
1657
1685
|
(0, faults_js_1.maybeFail)("get:after-record");
|
|
1658
|
-
console.log(`${accent(`v${latest.sequence}`)} · ${result.files}
|
|
1686
|
+
console.log(`${accent(`v${latest.sequence}`)} · ${fileCount(result.files)} · ${bytes(result.bytes)} into ${destination}`);
|
|
1659
1687
|
return 0;
|
|
1660
1688
|
}
|
|
1661
1689
|
/**
|
|
@@ -1789,7 +1817,7 @@ async function commandUnbundle(parsed) {
|
|
|
1789
1817
|
dim(progress.path.slice(-40)));
|
|
1790
1818
|
});
|
|
1791
1819
|
done(line);
|
|
1792
|
-
console.log(`${result.files}
|
|
1820
|
+
console.log(`${fileCount(result.files)} · ${bytes(result.bytes)} into ${destination}`);
|
|
1793
1821
|
return 0;
|
|
1794
1822
|
}
|
|
1795
1823
|
async function commandInspect(parsed) {
|
|
@@ -1801,7 +1829,7 @@ async function commandInspect(parsed) {
|
|
|
1801
1829
|
const manifest = await (0, cbx_js_1.readManifest)(node_path_1.default.resolve(source));
|
|
1802
1830
|
const total = manifest.files.reduce((sum, file) => sum + file.size, 0);
|
|
1803
1831
|
console.log(`${bold(manifest.source_name)} · format ${manifest.format} v${manifest.format_version}`);
|
|
1804
|
-
console.log(`${manifest.files.length}
|
|
1832
|
+
console.log(`${fileCount(manifest.files.length)} · ${bytes(total)} · ` +
|
|
1805
1833
|
`${Object.keys(manifest.chunks).length} chunks`);
|
|
1806
1834
|
if (hasFlag(parsed, "files")) {
|
|
1807
1835
|
for (const file of manifest.files) {
|
|
@@ -2022,13 +2050,17 @@ async function commandMerge(parsed) {
|
|
|
2022
2050
|
}
|
|
2023
2051
|
}
|
|
2024
2052
|
console.log(`
|
|
2025
|
-
${accent(now.mergeTrack.reference)} ·
|
|
2053
|
+
${accent(now.mergeTrack.reference)} · ` +
|
|
2054
|
+
/*
|
|
2055
|
+
The combined version is only counted once it can be built; until then
|
|
2056
|
+
its file count is 0, which read as "your upload was empty".
|
|
2057
|
+
*/
|
|
2026
2058
|
(now.provisional.ready
|
|
2027
|
-
?
|
|
2059
|
+
? `${fileCount(now.provisional.fileCount)} · ready to apply`
|
|
2028
2060
|
: `${now.provisional.unresolvedPaths.length} still to decide`));
|
|
2029
2061
|
for (const conflict of now.conflicts) {
|
|
2030
2062
|
console.log(` ${conflict.resolvedAt ? accent("decided") : red("waiting")} ${conflict.path}` +
|
|
2031
|
-
` ${dim(conflict.kind)}${conflict.resolution ? dim(` (${conflict.resolution})`) : ""}`);
|
|
2063
|
+
` ${dim(conflictWords(conflict.kind))}${conflict.resolution ? dim(` (${conflict.resolution})`) : ""}`);
|
|
2032
2064
|
}
|
|
2033
2065
|
if (hasFlag(parsed, "apply")) {
|
|
2034
2066
|
if (!now.provisional.ready) {
|
|
@@ -2048,7 +2080,7 @@ Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
|
2048
2080
|
if (!decision && stillOpen) {
|
|
2049
2081
|
console.log(dim(`
|
|
2050
2082
|
--mine keeps yours, --theirs keeps what was already saved,` +
|
|
2051
|
-
` --drop removes the file.
|
|
2083
|
+
` --both keeps theirs and saves yours beside it, --drop removes the file.
|
|
2052
2084
|
Add --path <file> for one file, then --apply when ready.`));
|
|
2053
2085
|
}
|
|
2054
2086
|
else if (now.provisional.ready) {
|
|
@@ -3098,9 +3130,13 @@ const SPECS = [
|
|
|
3098
3130
|
usage: "people [project]",
|
|
3099
3131
|
options: [
|
|
3100
3132
|
{ flags: "--invite <email>", description: "ask somebody to join" },
|
|
3101
|
-
{ flags: "--role <role>", description: "
|
|
3133
|
+
{ flags: "--role <role>", description: "write, read or admin (default write)" },
|
|
3134
|
+
{ flags: "--accept <link>", description: "join a project you were invited to" },
|
|
3135
|
+
],
|
|
3136
|
+
examples: [
|
|
3137
|
+
"cbx people my-project --invite sam@example.com",
|
|
3138
|
+
"cbx people --accept https://coderook.com/?invite=…",
|
|
3102
3139
|
],
|
|
3103
|
-
examples: ["cbx people my-project --invite sam@example.com"],
|
|
3104
3140
|
run: service_commands_js_1.commandCollaborators,
|
|
3105
3141
|
},
|
|
3106
3142
|
{
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.accent = exports.green = exports.red = exports.bold = exports.dim = void 0;
|
|
4
|
+
/*
|
|
5
|
+
Colour for the command modules, on the same terms as cli.ts: only at a
|
|
6
|
+
terminal, and never when NO_COLOR is set. Each module used to carry its own
|
|
7
|
+
always-on copy, so output piped to a file, or a script asking for plain
|
|
8
|
+
text, still came back full of escape codes.
|
|
9
|
+
*/
|
|
10
|
+
const colour = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
11
|
+
const paint = (code) => (text) => colour ? `[${code}m${text}[0m` : text;
|
|
12
|
+
exports.dim = paint("2");
|
|
13
|
+
exports.bold = paint("1");
|
|
14
|
+
exports.red = paint("31");
|
|
15
|
+
exports.green = paint("32");
|
|
16
|
+
exports.accent = paint("33");
|