@coderook/cli 0.34.6 → 0.34.8
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/secret_patterns.js +7 -1
- package/dist/cbx/src/core/upload.js +147 -36
- package/dist/cli/src/cli.js +23 -6
- package/dist/cli/src/mcp.js +2 -2
- package/dist/cli/src/project_commands.js +1 -1
- package/dist/cli/src/service_commands.js +2 -1
- 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.8",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
|
@@ -90,7 +90,13 @@ exports.CREDENTIAL_PATTERNS = [
|
|
|
90
90
|
{
|
|
91
91
|
id: "slack-token",
|
|
92
92
|
name: "a Slack token",
|
|
93
|
-
|
|
93
|
+
/*
|
|
94
|
+
Slack's tokens open with a number group: xoxb-1234…-5678…-AbC…. The
|
|
95
|
+
loose form also took "xoxb-test-bot-token", which is how test suites
|
|
96
|
+
spell a fake one — an open-source project's integration tests were
|
|
97
|
+
refused six times over for them.
|
|
98
|
+
*/
|
|
99
|
+
match: /\bxox[baprs]-\d+-[0-9A-Za-z-]{10,}/g,
|
|
94
100
|
},
|
|
95
101
|
{
|
|
96
102
|
id: "stripe-live-key",
|
|
@@ -180,6 +180,8 @@ class Uploader {
|
|
|
180
180
|
return new Map(this.plannedDigests);
|
|
181
181
|
}
|
|
182
182
|
planningCreatedRepositoryId = null;
|
|
183
|
+
/** Set once the send has begun to move objects; see execute. */
|
|
184
|
+
sendingBegan = false;
|
|
183
185
|
/** Attached to every object write after the server admits the exact plan. */
|
|
184
186
|
activeQuoteId = null;
|
|
185
187
|
constructor(credentials) {
|
|
@@ -409,9 +411,22 @@ class Uploader {
|
|
|
409
411
|
/** Execute a still-active quote produced by {@link plan}. */
|
|
410
412
|
async execute(request, plan, report) {
|
|
411
413
|
this.activeQuoteId = plan.quoteId;
|
|
414
|
+
this.sendingBegan = false;
|
|
412
415
|
try {
|
|
413
416
|
return await this.runPass({ ...request, repositoryId: plan.repositoryId }, report);
|
|
414
417
|
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
/*
|
|
420
|
+
A first save that stops before a single object has moved leaves no
|
|
421
|
+
project behind. The plan created it, and it used to stay — empty, at
|
|
422
|
+
v0 — after a refusal that had just said nothing was changed. Once
|
|
423
|
+
objects have gone up it stays, because a retry reuses them.
|
|
424
|
+
*/
|
|
425
|
+
if (plan.repositoryCreated && !this.sendingBegan) {
|
|
426
|
+
await this.cancelPlan(plan, true).catch(() => undefined);
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
415
430
|
finally {
|
|
416
431
|
this.activeQuoteId = null;
|
|
417
432
|
}
|
|
@@ -609,6 +624,17 @@ class Uploader {
|
|
|
609
624
|
const HASH_LANES = 8;
|
|
610
625
|
const measured = new Array(sending.length).fill(null);
|
|
611
626
|
let hashed = 0;
|
|
627
|
+
/*
|
|
628
|
+
Files whose bytes are no longer the bytes the plan priced.
|
|
629
|
+
|
|
630
|
+
The service holds a save to what its plan named, so a file written to
|
|
631
|
+
between the plan and the send went up as an object nobody had asked
|
|
632
|
+
for and was refused as "a file that was not part of what it asked to
|
|
633
|
+
send", with advice to update the app. Found by saving a folder while a
|
|
634
|
+
program appended to a text file in it. Caught here instead, before
|
|
635
|
+
anything is sent, and named for what it is.
|
|
636
|
+
*/
|
|
637
|
+
const changedSincePlan = [];
|
|
612
638
|
let cursor = 0;
|
|
613
639
|
/** Throttled: twenty thousand sends is its own cost, and nobody reads them. */
|
|
614
640
|
let told = 0;
|
|
@@ -691,6 +717,7 @@ class Uploader {
|
|
|
691
717
|
and a match there means the digest already taken still describes the
|
|
692
718
|
bytes. Reading is what costs; deciding not to read is free.
|
|
693
719
|
*/
|
|
720
|
+
const fromPlan = this.planning ? undefined : this.plannedDigests.get(file.path);
|
|
694
721
|
const remembered = this.plannedDigests.get(file.path) ??
|
|
695
722
|
request.knownDigests?.get(file.path);
|
|
696
723
|
if (remembered &&
|
|
@@ -722,6 +749,8 @@ class Uploader {
|
|
|
722
749
|
hashed += 1;
|
|
723
750
|
continue;
|
|
724
751
|
}
|
|
752
|
+
if (fromPlan && fromPlan.sha256 !== digest)
|
|
753
|
+
changedSincePlan.push(file.path);
|
|
725
754
|
this.plannedDigests.set(file.path, { size, mtimeMs, sha256: digest });
|
|
726
755
|
measured[index] = {
|
|
727
756
|
path: file.path,
|
|
@@ -752,6 +781,16 @@ class Uploader {
|
|
|
752
781
|
if (!declarations.length && !deletions.size) {
|
|
753
782
|
throw new Error("Nothing is selected to upload");
|
|
754
783
|
}
|
|
784
|
+
if (changedSincePlan.length) {
|
|
785
|
+
const one = changedSincePlan.length === 1;
|
|
786
|
+
const shown = changedSincePlan.slice(0, 5).join(", ");
|
|
787
|
+
const rest = changedSincePlan.length > 5 ? ` and ${changedSincePlan.length - 5} more` : "";
|
|
788
|
+
throw new Error(`${changedSincePlan.length} file${one ? "" : "s"} changed while the save was running, ` +
|
|
789
|
+
`so nothing was sent: ${shown}${rest}. Something is still writing to ${one ? "it" : "them"}. ` +
|
|
790
|
+
`Save again once ${one ? "it has" : "they have"} stopped changing, or leave ${one ? "it" : "them"} ` +
|
|
791
|
+
`out — untick ${one ? "it" : "them"} in the app, or add ${one ? "it" : "them"} to .gitignore. ` +
|
|
792
|
+
`Nothing was changed on your account.`);
|
|
793
|
+
}
|
|
755
794
|
return { declarations, totalBytes };
|
|
756
795
|
}
|
|
757
796
|
/**
|
|
@@ -809,6 +848,66 @@ class Uploader {
|
|
|
809
848
|
* that did not arrive, while past here there is a version. Keeping the
|
|
810
849
|
* boundary a function call rather than a comment makes that legible.
|
|
811
850
|
*/
|
|
851
|
+
/**
|
|
852
|
+
* Refuse a save that would leave out files somebody chose, naming why.
|
|
853
|
+
*
|
|
854
|
+
* Called as soon as the reading is done as well as before the version is
|
|
855
|
+
* named. Only the second existed, after the plan had already created the
|
|
856
|
+
* project, so a first save refused for one locked file left an empty
|
|
857
|
+
* project behind while saying "Nothing was changed on your account".
|
|
858
|
+
*/
|
|
859
|
+
refuseUnreadable(dropped, readFailures) {
|
|
860
|
+
const shown = dropped.slice(0, 5).join(", ");
|
|
861
|
+
const rest = dropped.length > 5 ? ` and ${dropped.length - 5} more` : "";
|
|
862
|
+
const details = dropped
|
|
863
|
+
.map((name) => readFailures.get(name) ? `${name}: ${readFailures.get(name)}` : "")
|
|
864
|
+
.filter(Boolean)
|
|
865
|
+
.slice(0, 3)
|
|
866
|
+
.join("; ");
|
|
867
|
+
/*
|
|
868
|
+
Say what to do about it. Refusing is right — a version quietly missing
|
|
869
|
+
a file somebody chose is the one failure a backup tool must never have
|
|
870
|
+
— but the message stopped at the refusal, and the person is left with
|
|
871
|
+
a list of paths and no idea that the fix is either closing whatever
|
|
872
|
+
holds them or unticking them. Almost every case is a file another
|
|
873
|
+
program has open, so that is what it says.
|
|
874
|
+
*/
|
|
875
|
+
/*
|
|
876
|
+
Each cause named for what it is. Every failure used to be put down to
|
|
877
|
+
"another program may have a file open", which was wrong for a file
|
|
878
|
+
this account may not read and for one deleted mid-save, and "untick"
|
|
879
|
+
means nothing on the command line, where there is nothing to tick.
|
|
880
|
+
Found by saving a folder with one file held open and one with
|
|
881
|
+
reading denied.
|
|
882
|
+
*/
|
|
883
|
+
const reasonOf = (name) => readFailures.get(name) ?? "";
|
|
884
|
+
const causes = new Set(dropped.map((name) => {
|
|
885
|
+
const reason = reasonOf(name);
|
|
886
|
+
if (reason.includes("ignore rules"))
|
|
887
|
+
return "ignored";
|
|
888
|
+
if (/EACCES|EPERM|permission|access is denied/i.test(reason))
|
|
889
|
+
return "permission";
|
|
890
|
+
if (/ENOENT|no such file/i.test(reason))
|
|
891
|
+
return "gone";
|
|
892
|
+
return "busy";
|
|
893
|
+
}));
|
|
894
|
+
const said = [];
|
|
895
|
+
if (causes.has("busy"))
|
|
896
|
+
said.push("Another program has a file open: close it.");
|
|
897
|
+
if (causes.has("permission"))
|
|
898
|
+
said.push("This account is not allowed to read a file: change its permissions.");
|
|
899
|
+
if (causes.has("gone"))
|
|
900
|
+
said.push("A file was deleted or moved while the save was running.");
|
|
901
|
+
if (causes.has("ignored"))
|
|
902
|
+
said.push("A file is excluded by the project's ignore rules: change the rules.");
|
|
903
|
+
throw new Error(`${dropped.length} selected file${dropped.length === 1 ? "" : "s"} could not be included, ` +
|
|
904
|
+
`so nothing was saved: ${shown}${rest}. ` +
|
|
905
|
+
`${said.join(" ")} Or leave ${dropped.length === 1 ? "it" : "them"} out — untick ` +
|
|
906
|
+
`${dropped.length === 1 ? "it" : "them"} in the app, or add ${dropped.length === 1 ? "it" : "them"} ` +
|
|
907
|
+
`to .gitignore — and save again. ` +
|
|
908
|
+
`Nothing was changed on your account.` +
|
|
909
|
+
(details ? ` Details: ${details}` : ""));
|
|
910
|
+
}
|
|
812
911
|
async publishVersion(at) {
|
|
813
912
|
const { request, repositoryId, declarations, uploaded, reused, prior, everything, ticked, deletions, readFailures, vanished, totalBytes, progress, report, } = at;
|
|
814
913
|
// 4. Name the version, which is what makes the upload visible.
|
|
@@ -837,7 +936,7 @@ class Uploader {
|
|
|
837
936
|
const contents = [
|
|
838
937
|
...uploaded,
|
|
839
938
|
...reused.map((file) => {
|
|
840
|
-
const held = prior.get(file.path);
|
|
939
|
+
const held = file.storedAs ?? prior.get(file.path);
|
|
841
940
|
/*
|
|
842
941
|
Republished in the shape it was already stored in. A file kept in
|
|
843
942
|
pieces names the same pieces; flattening it to one object here would
|
|
@@ -872,7 +971,7 @@ class Uploader {
|
|
|
872
971
|
: { objectId: held.objectId }),
|
|
873
972
|
sourceSize: held.sourceSize,
|
|
874
973
|
storedSize: held.storedSize,
|
|
875
|
-
mediaType:
|
|
974
|
+
mediaType: file.mediaType,
|
|
876
975
|
executable: request.executable?.get(file.path) ?? held.executable,
|
|
877
976
|
};
|
|
878
977
|
}),
|
|
@@ -899,31 +998,8 @@ class Uploader {
|
|
|
899
998
|
...[...ticked].filter((chosen) => !named.has(chosen) && !deletions.has(chosen)),
|
|
900
999
|
]),
|
|
901
1000
|
];
|
|
902
|
-
if (dropped.length)
|
|
903
|
-
|
|
904
|
-
const rest = dropped.length > 5 ? ` and ${dropped.length - 5} more` : "";
|
|
905
|
-
const details = dropped
|
|
906
|
-
.map((name) => readFailures.get(name) ? `${name}: ${readFailures.get(name)}` : "")
|
|
907
|
-
.filter(Boolean)
|
|
908
|
-
.slice(0, 3)
|
|
909
|
-
.join("; ");
|
|
910
|
-
/*
|
|
911
|
-
Say what to do about it. Refusing is right — a version quietly missing
|
|
912
|
-
a file somebody chose is the one failure a backup tool must never have
|
|
913
|
-
— but the message stopped at the refusal, and the person is left with
|
|
914
|
-
a list of paths and no idea that the fix is either closing whatever
|
|
915
|
-
holds them or unticking them. Almost every case is a file another
|
|
916
|
-
program has open, so that is what it says.
|
|
917
|
-
*/
|
|
918
|
-
const excluded = dropped.some((name) => readFailures.get(name)?.includes("ignore rules"));
|
|
919
|
-
throw new Error(`${dropped.length} selected file${dropped.length === 1 ? "" : "s"} could not be included, ` +
|
|
920
|
-
`so nothing was saved: ${shown}${rest}. ` +
|
|
921
|
-
(excluded
|
|
922
|
-
? `At least one is excluded by the project's ignore rules. Change the rules or untick it and try again. `
|
|
923
|
-
: `Another program may have a file open. Close it or untick the file and try again. `) +
|
|
924
|
-
`Nothing was changed on your account.` +
|
|
925
|
-
(details ? ` Details: ${details}` : ""));
|
|
926
|
-
}
|
|
1001
|
+
if (dropped.length)
|
|
1002
|
+
this.refuseUnreadable(dropped, readFailures);
|
|
927
1003
|
const sourceBytes = contents.reduce((total, item) => total + item.sourceSize, 0);
|
|
928
1004
|
const storedBytes = contents.reduce((total, item) => total + item.storedSize, 0);
|
|
929
1005
|
/*
|
|
@@ -1016,7 +1092,7 @@ class Uploader {
|
|
|
1016
1092
|
const manifest = Object.keys(acceptedManifest).length
|
|
1017
1093
|
? acceptedManifest
|
|
1018
1094
|
: {
|
|
1019
|
-
...Object.fromEntries(reused.map((file) => [file.path, prior.get(file.path).sha256])),
|
|
1095
|
+
...Object.fromEntries(reused.map((file) => [file.path, (file.storedAs ?? prior.get(file.path)).sha256])),
|
|
1020
1096
|
...Object.fromEntries(declarations.map((declaration) => [declaration.path, declaration.sha256])),
|
|
1021
1097
|
};
|
|
1022
1098
|
/*
|
|
@@ -1031,6 +1107,16 @@ class Uploader {
|
|
|
1031
1107
|
the very end of a save.
|
|
1032
1108
|
*/
|
|
1033
1109
|
const sentDigest = new Map(declarations.map((declaration) => [declaration.path, declaration.sha256]));
|
|
1110
|
+
/*
|
|
1111
|
+
A file named from bytes already stored, at a path the last version did
|
|
1112
|
+
not have, holds those bytes here too. Without this a renamed folder was
|
|
1113
|
+
saved correctly and then reported as "behind" file by file, because this
|
|
1114
|
+
record looked its old digest up by its new path and found nothing.
|
|
1115
|
+
*/
|
|
1116
|
+
for (const file of reused) {
|
|
1117
|
+
if (file.storedAs)
|
|
1118
|
+
sentDigest.set(file.path, file.storedAs.sha256);
|
|
1119
|
+
}
|
|
1034
1120
|
const onDisk = new Set(everything.map((file) => file.path));
|
|
1035
1121
|
const local = Object.fromEntries(everything.flatMap((file) => {
|
|
1036
1122
|
const sent = sentDigest.get(file.path);
|
|
@@ -1104,6 +1190,8 @@ class Uploader {
|
|
|
1104
1190
|
* rather than before.
|
|
1105
1191
|
*/
|
|
1106
1192
|
async sendObjects(at) {
|
|
1193
|
+
if (!this.planning)
|
|
1194
|
+
this.sendingBegan = true;
|
|
1107
1195
|
const { request, repositoryId, prior,
|
|
1108
1196
|
/*
|
|
1109
1197
|
Bound under a distinct name on purpose. `runSection` declares its own
|
|
@@ -1708,6 +1796,12 @@ class Uploader {
|
|
|
1708
1796
|
than from the disk.
|
|
1709
1797
|
*/
|
|
1710
1798
|
const hashed = await this.hashSelection(request, sending, deletions, readFailures, vanished, report);
|
|
1799
|
+
/*
|
|
1800
|
+
Before the project is created or anything priced: see refuseUnreadable.
|
|
1801
|
+
`vanished` holds every chosen file that is gone or could not be read.
|
|
1802
|
+
*/
|
|
1803
|
+
if (vanished.length)
|
|
1804
|
+
this.refuseUnreadable([...new Set(vanished)], readFailures);
|
|
1711
1805
|
/*
|
|
1712
1806
|
A file that is exactly what the last version held is kept, not sent.
|
|
1713
1807
|
|
|
@@ -1723,31 +1817,48 @@ class Uploader {
|
|
|
1723
1817
|
same bytes, and the previous version's object for them — a pack slice,
|
|
1724
1818
|
its chunks, or a plain object — is named again instead.
|
|
1725
1819
|
*/
|
|
1726
|
-
const
|
|
1820
|
+
const stored = (was) => !!was && !!(was.pack?.objectId || was.chunks?.length || was.objectId);
|
|
1821
|
+
/*
|
|
1822
|
+
Matched by what a file is, not only by where it sits.
|
|
1823
|
+
|
|
1824
|
+
Same path and same bytes was the only reuse, so renaming a folder sent
|
|
1825
|
+
every file in it again: the service recognises a stored object by its
|
|
1826
|
+
digest but not a slice of a pack, and small files live in packs. Moving
|
|
1827
|
+
a 2,192-file folder in a copy of the Cheat Engine sources re-sent all of
|
|
1828
|
+
it, 5.5 MB, to record bytes the project already held. Any file in the
|
|
1829
|
+
previous version with the same digest is the same bytes, wherever it was.
|
|
1830
|
+
*/
|
|
1831
|
+
const priorByDigest = new Map();
|
|
1832
|
+
for (const was of prior.values()) {
|
|
1833
|
+
if (stored(was) && !priorByDigest.has(was.sha256))
|
|
1834
|
+
priorByDigest.set(was.sha256, was);
|
|
1835
|
+
}
|
|
1836
|
+
const storedAs = (declaration) => {
|
|
1727
1837
|
const was = prior.get(declaration.path);
|
|
1728
|
-
|
|
1729
|
-
was
|
|
1730
|
-
|
|
1731
|
-
!!(was.pack?.objectId || was.chunks?.length || was.objectId));
|
|
1838
|
+
if (stored(was) && was.sha256 === declaration.sha256)
|
|
1839
|
+
return was;
|
|
1840
|
+
return priorByDigest.get(declaration.sha256) ?? null;
|
|
1732
1841
|
};
|
|
1842
|
+
const keptAsItWas = (declaration) => storedAs(declaration) !== null;
|
|
1733
1843
|
const declarations = hashed.declarations.filter((one) => !keptAsItWas(one));
|
|
1734
1844
|
let totalBytes = hashed.totalBytes;
|
|
1735
1845
|
for (const one of hashed.declarations) {
|
|
1736
|
-
|
|
1846
|
+
const held = storedAs(one);
|
|
1847
|
+
if (!held)
|
|
1737
1848
|
continue;
|
|
1738
1849
|
totalBytes -= one.size;
|
|
1739
|
-
const held = prior.get(one.path);
|
|
1740
1850
|
reused.push({
|
|
1741
1851
|
path: one.path,
|
|
1742
1852
|
sourceSize: held.sourceSize,
|
|
1743
1853
|
storedSize: held.storedSize,
|
|
1744
|
-
mediaType:
|
|
1854
|
+
mediaType: one.mediaType,
|
|
1745
1855
|
added: 0,
|
|
1746
1856
|
removed: 0,
|
|
1747
1857
|
included: true,
|
|
1748
1858
|
deleted: false,
|
|
1749
1859
|
binary: false,
|
|
1750
1860
|
lines: 0,
|
|
1861
|
+
...(held === prior.get(one.path) ? {} : { storedAs: held }),
|
|
1751
1862
|
});
|
|
1752
1863
|
}
|
|
1753
1864
|
(0, profile_js_1.counted)("files kept as they were", hashed.declarations.length - declarations.length);
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -376,7 +376,7 @@ async function commandProjects() {
|
|
|
376
376
|
const width = Math.max(...all.map((project) => project.slug.length));
|
|
377
377
|
for (const project of all) {
|
|
378
378
|
console.log(`${accent(project.slug.padEnd(width))} v${String(project.versionCount).padEnd(4)}` +
|
|
379
|
-
`${String(project.fileCount).padStart(6)} files ${bytes(project.storedBytes).padStart(9)}` +
|
|
379
|
+
`${String(project.fileCount).padStart(6)} ${project.fileCount === 1 ? "file " : "files"} ${bytes(project.storedBytes).padStart(9)}` +
|
|
380
380
|
` ${dim(project.visibility)}`);
|
|
381
381
|
}
|
|
382
382
|
return 0;
|
|
@@ -454,7 +454,7 @@ async function commandStatus(parsed) {
|
|
|
454
454
|
const files = found.filter((file) => !file.deleted);
|
|
455
455
|
console.log(bold(node_path_1.default.basename(folder)) + dim(` ${folder}`));
|
|
456
456
|
console.log(link
|
|
457
|
-
? `Account holds ${accent(`v${link.sequence}`)} · ${Object.keys(link.manifest).length} files ${dim(`(${link.slug})`)}`
|
|
457
|
+
? `Account holds ${accent(`v${link.sequence}`)} · ${Object.keys(link.manifest).length} ${Object.keys(link.manifest).length === 1 ? "file" : "files"} ${dim(`(${link.slug})`)}`
|
|
458
458
|
: dim("This folder is not linked to a project."));
|
|
459
459
|
if (!link) {
|
|
460
460
|
const likes = (0, folder_match_js_1.similarProjects)(await (0, api_js_2.projects)().catch(() => []), node_path_1.default.basename(folder));
|
|
@@ -574,8 +574,10 @@ async function commandStatus(parsed) {
|
|
|
574
574
|
return 0;
|
|
575
575
|
}
|
|
576
576
|
console.log(`\n${files.length} file${files.length === 1 ? "" : "s"} to submit:`);
|
|
577
|
+
/* New is not changed: a file the saved version never had was listed as "changed". */
|
|
577
578
|
for (const file of files.slice(0, 50)) {
|
|
578
|
-
|
|
579
|
+
const fresh = !baseline?.has(file.path);
|
|
580
|
+
console.log(` ${fresh ? green("new ") : accent("changed")} ${file.path}`);
|
|
579
581
|
}
|
|
580
582
|
if (files.length > 50)
|
|
581
583
|
console.log(dim(` …and ${files.length - 50} more`));
|
|
@@ -728,13 +730,14 @@ async function digestOfFile(full) {
|
|
|
728
730
|
*/
|
|
729
731
|
function noteKeptDeletions(folder, baseline, syncing) {
|
|
730
732
|
if (syncing || !baseline?.size)
|
|
731
|
-
return;
|
|
733
|
+
return 0;
|
|
732
734
|
const gone = [...baseline.keys()].filter((file) => !(0, node_fs_1.existsSync)(node_path_1.default.join(folder, ...file.split("/"))));
|
|
733
735
|
if (!gone.length)
|
|
734
|
-
return;
|
|
736
|
+
return 0;
|
|
735
737
|
console.log(dim(`${gone.length} file${gone.length === 1 ? "" : "s"} deleted here ${gone.length === 1 ? "is" : "are"} still in the saved version` +
|
|
736
738
|
` (${gone.slice(0, 3).join(", ")}${gone.length > 3 ? ", …" : ""}). A save adds and updates;` +
|
|
737
739
|
` run cbx submit --sync to remove ${gone.length === 1 ? "it" : "them"}.`));
|
|
740
|
+
return gone.length;
|
|
738
741
|
}
|
|
739
742
|
/*
|
|
740
743
|
The service names each clash by its kind ("text_region", "add_add", …).
|
|
@@ -914,6 +917,20 @@ async function commandSubmit(parsed) {
|
|
|
914
917
|
}
|
|
915
918
|
}
|
|
916
919
|
if (!files.length) {
|
|
920
|
+
/*
|
|
921
|
+
Deleted files are not "nothing". A plain save never removes, so a
|
|
922
|
+
folder with 1,723 files deleted from it was told it "matches the saved
|
|
923
|
+
version" straight after status had listed every one of them — found by
|
|
924
|
+
deleting a folder from a copy of the Cheat Engine sources.
|
|
925
|
+
*/
|
|
926
|
+
if (!hasFlag(parsed, "sync")) {
|
|
927
|
+
const kept = [...(baseline?.keys() ?? [])].some((file) => !(0, node_fs_1.existsSync)(node_path_1.default.join(folder, ...file.split("/"))));
|
|
928
|
+
if (kept) {
|
|
929
|
+
console.log("Nothing new to submit.");
|
|
930
|
+
noteKeptDeletions(folder, baseline, false);
|
|
931
|
+
return 0;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
917
934
|
// Having nothing to send is not the same as holding the whole version:
|
|
918
935
|
// a merge can leave this folder with an older copy of somebody else's
|
|
919
936
|
// file. Saying so is what stops "nothing to do" from reading as "level".
|
|
@@ -1263,7 +1280,7 @@ The connection failed: ${text}`));
|
|
|
1263
1280
|
*/
|
|
1264
1281
|
console.log(`\nSomebody else saved first, and ${result.mergeTrack.conflicts.length === 1
|
|
1265
1282
|
? "one file overlaps"
|
|
1266
|
-
: `${result.mergeTrack.conflicts.length} files overlap`}.`);
|
|
1283
|
+
: `${result.mergeTrack.conflicts.length} ${result.mergeTrack.conflicts.length === 1 ? "file overlaps" : "files overlap"}`}.`);
|
|
1267
1284
|
console.log(`Your work is complete and kept as ${accent(result.mergeTrack.reference)}:`);
|
|
1268
1285
|
for (const conflict of result.mergeTrack.conflicts.slice(0, 20)) {
|
|
1269
1286
|
console.log(` ${red(conflict.path)} ${dim(conflictWords(conflict.kind))}`);
|
package/dist/cli/src/mcp.js
CHANGED
|
@@ -136,7 +136,7 @@ const TOOLS = [
|
|
|
136
136
|
if (!saved.length)
|
|
137
137
|
return "No versions saved yet.";
|
|
138
138
|
return saved
|
|
139
|
-
.map((one) => `v${one.sequence} ${one.createdAt.slice(0, 10)} ${one.fileCount} files ` +
|
|
139
|
+
.map((one) => `v${one.sequence} ${one.createdAt.slice(0, 10)} ${one.fileCount} ${one.fileCount === 1 ? "file" : "files"} ` +
|
|
140
140
|
`${bytes(one.storedSize)} ${one.message || "(no message)"}`)
|
|
141
141
|
.join("\n");
|
|
142
142
|
},
|
|
@@ -167,7 +167,7 @@ const TOOLS = [
|
|
|
167
167
|
if (files.length > shown.length) {
|
|
168
168
|
lines.push(`… and ${files.length - shown.length} more`);
|
|
169
169
|
}
|
|
170
|
-
return `v${version.sequence} · ${files.length} files\n${lines.join("\n")}`;
|
|
170
|
+
return `v${version.sequence} · ${files.length} ${files.length === 1 ? "file" : "files"}\n${lines.join("\n")}`;
|
|
171
171
|
},
|
|
172
172
|
},
|
|
173
173
|
{
|
|
@@ -385,7 +385,7 @@ async function commandVersions(parsed) {
|
|
|
385
385
|
...version.labels.map((label) => (0, colour_js_1.accent)(label.name)),
|
|
386
386
|
].filter(Boolean);
|
|
387
387
|
console.log(` ${(0, colour_js_1.accent)(`v${version.sequence}`).padEnd(16)} ${when.padEnd(22)}` +
|
|
388
|
-
`${String(version.fileCount).padStart(5)} files ${bytes(version.storedSize)}` +
|
|
388
|
+
`${String(version.fileCount).padStart(5)} ${version.fileCount === 1 ? "file " : "files"} ${bytes(version.storedSize)}` +
|
|
389
389
|
(marks.length ? ` ${marks.join(" ")}` : ""));
|
|
390
390
|
if (version.message)
|
|
391
391
|
console.log(` ${(0, colour_js_1.dim)(version.message)}`);
|
|
@@ -569,7 +569,8 @@ async function commandDelete(parsed) {
|
|
|
569
569
|
if (!project)
|
|
570
570
|
return 1;
|
|
571
571
|
if (parsed.flags.get("yes") !== true) {
|
|
572
|
-
console.log(`This deletes ${(0, colour_js_1.bold)(project.name)} — ${project.versionCount}
|
|
572
|
+
console.log(`This deletes ${(0, colour_js_1.bold)(project.name)} — ${project.versionCount} ` +
|
|
573
|
+
`${project.versionCount === 1 ? "version" : "versions"}, ` +
|
|
573
574
|
`${bytes(project.storedBytes)}.`);
|
|
574
575
|
console.log((0, colour_js_1.dim)("It is kept for 30 days before the storage is reclaimed."));
|
|
575
576
|
const prompt = (0, promises_1.createInterface)({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.8",
|
|
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",
|