@coderook/cli 0.29.5 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/cli.js +93 -7
- package/dist/desktop-app/src/main/detect.js +78 -0
- package/dist/desktop-app/src/main/rules.js +21 -3
- package/dist/desktop-app/src/main/transfer_progress.js +109 -0
- package/dist/desktop-app/src/main/upload.js +617 -559
- package/dist/desktop-app/src/main/worktree.js +158 -5
- 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, from Claude Code.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.30.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -35,6 +35,7 @@ const skill_command_js_1 = require("./skill_command.js");
|
|
|
35
35
|
const licence_commands_js_1 = require("./licence_commands.js");
|
|
36
36
|
const service_commands_js_1 = require("./service_commands.js");
|
|
37
37
|
const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
38
|
+
const rules_js_1 = require("../../desktop-app/src/main/rules.js");
|
|
38
39
|
const upload_js_1 = require("../../desktop-app/src/main/upload.js");
|
|
39
40
|
const download_js_1 = require("../../desktop-app/src/main/download.js");
|
|
40
41
|
const faults_js_1 = require("../../desktop-app/src/main/faults.js");
|
|
@@ -349,7 +350,22 @@ async function commandStatus(parsed) {
|
|
|
349
350
|
console.log(dim("Offline — comparing with the last Version cached for this folder."));
|
|
350
351
|
}
|
|
351
352
|
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
352
|
-
|
|
353
|
+
/*
|
|
354
|
+
Scanned the way `cbx diff` scans, and reported the way `cbx submit` acts.
|
|
355
|
+
|
|
356
|
+
Status used to ask only for additions and updates, which is exactly what a
|
|
357
|
+
plain save sends — so it was not wrong. But a deleted file then appeared
|
|
358
|
+
nowhere: `cbx diff` showed it as removed, `cbx status` said nothing, and
|
|
359
|
+
the two commands disagreed about the same folder. Somebody who had deleted
|
|
360
|
+
a file read that as CodeRook not having noticed it.
|
|
361
|
+
|
|
362
|
+
So deletions are found and named here, kept apart from what a save will
|
|
363
|
+
send, with the flag that would send them spelled out. The safe default
|
|
364
|
+
does not change: a save adds and updates.
|
|
365
|
+
*/
|
|
366
|
+
const found = await (0, worktree_js_1.changedFiles)(folder, rules, baseline, "synchronize");
|
|
367
|
+
const gone = found.filter((file) => file.deleted);
|
|
368
|
+
const files = found.filter((file) => !file.deleted);
|
|
353
369
|
console.log(bold(node_path_1.default.basename(folder)) + dim(` ${folder}`));
|
|
354
370
|
console.log(link
|
|
355
371
|
? `Account holds ${accent(`v${link.sequence}`)} · ${Object.keys(link.manifest).length} files ${dim(`(${link.slug})`)}`
|
|
@@ -370,18 +386,85 @@ async function commandStatus(parsed) {
|
|
|
370
386
|
for (const [path] of stale.slice(0, 10))
|
|
371
387
|
console.log(dim(` behind ${path}`));
|
|
372
388
|
}
|
|
389
|
+
/** The deletions, and what a plain save will do about them. */
|
|
390
|
+
/*
|
|
391
|
+
What this folder is, and what its own rules never heard about.
|
|
392
|
+
|
|
393
|
+
A real .gitignore always wins — that is not negotiable — but it also meant
|
|
394
|
+
that a Unity project whose file was written before the engine rules
|
|
395
|
+
existed went on sending its entire import cache, and nothing anywhere
|
|
396
|
+
said so. One real upload of forty-seven thousand files was forty-five
|
|
397
|
+
thousand of rebuildable cache, and it broke the project on the machine it
|
|
398
|
+
was downloaded to.
|
|
399
|
+
*/
|
|
400
|
+
const engine = async () => {
|
|
401
|
+
const { kind, missing } = await (0, worktree_js_1.missingEngineRules)(folder);
|
|
402
|
+
if (!kind.length || !missing.length)
|
|
403
|
+
return;
|
|
404
|
+
/*
|
|
405
|
+
Only the rules that would actually leave something out.
|
|
406
|
+
|
|
407
|
+
Naming every line the template has and this folder does not produced
|
|
408
|
+
sixty-odd of them, most matching nothing at all — a Visual Studio
|
|
409
|
+
template mentions `[Dd]ebug/x64/` whether or not you have one. What a
|
|
410
|
+
person needs is the two that are costing them forty thousand files, so
|
|
411
|
+
each candidate is measured against what this folder is currently
|
|
412
|
+
sending and the ones worth nothing are not mentioned.
|
|
413
|
+
*/
|
|
414
|
+
const sending = found.map((one) => one.path);
|
|
415
|
+
const worth = missing
|
|
416
|
+
.map((rule) => {
|
|
417
|
+
const layers = [{ base: "", rules: (0, rules_js_1.parseRules)(rule) }];
|
|
418
|
+
return {
|
|
419
|
+
rule,
|
|
420
|
+
files: sending.filter((one) => (0, rules_js_1.excludes)(one, false, layers)).length,
|
|
421
|
+
};
|
|
422
|
+
})
|
|
423
|
+
.filter((one) => one.files > 0)
|
|
424
|
+
.sort((left, right) => right.files - left.files);
|
|
425
|
+
if (!worth.length)
|
|
426
|
+
return;
|
|
427
|
+
const total = worth.reduce((sum, one) => sum + one.files, 0);
|
|
428
|
+
console.log(dim(`\nThis looks like a ${kind.join(" + ")} project, and its own rules are ` +
|
|
429
|
+
`missing ${worth.length} that would leave out ` +
|
|
430
|
+
`${total.toLocaleString()} file${total === 1 ? "" : "s"} you are sending — ` +
|
|
431
|
+
`${accent("cbx ignore --template " + kind[0])} adds them.`));
|
|
432
|
+
for (const one of worth.slice(0, 6)) {
|
|
433
|
+
console.log(dim(` ${String(one.files).padStart(7)} ${one.rule}`));
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const deletions = () => {
|
|
437
|
+
if (!gone.length)
|
|
438
|
+
return;
|
|
439
|
+
const many = gone.length === 1 ? "" : "s";
|
|
440
|
+
console.log(dim(`\n${gone.length} file${many} deleted here ${gone.length === 1 ? "is" : "are"} ` +
|
|
441
|
+
`still in the saved version. A save adds and updates, so ` +
|
|
442
|
+
`${gone.length === 1 ? "it stays" : "they stay"} until you pass ` +
|
|
443
|
+
`${accent("--sync")}.`));
|
|
444
|
+
for (const file of gone.slice(0, 10)) {
|
|
445
|
+
console.log(` ${red("deleted")} ${file.path}`);
|
|
446
|
+
}
|
|
447
|
+
if (gone.length > 10)
|
|
448
|
+
console.log(dim(` …and ${gone.length - 10} more`));
|
|
449
|
+
};
|
|
373
450
|
if (!files.length) {
|
|
374
451
|
console.log(stale.length
|
|
375
452
|
? "Nothing to submit; your own work is all saved."
|
|
376
|
-
:
|
|
453
|
+
: gone.length
|
|
454
|
+
? "Nothing here to add or update."
|
|
455
|
+
: "Nothing to submit; this folder matches the saved version.");
|
|
456
|
+
deletions();
|
|
457
|
+
await engine();
|
|
377
458
|
return 0;
|
|
378
459
|
}
|
|
379
460
|
console.log(`\n${files.length} file${files.length === 1 ? "" : "s"} to submit:`);
|
|
380
461
|
for (const file of files.slice(0, 50)) {
|
|
381
|
-
console.log(` ${
|
|
462
|
+
console.log(` ${accent("changed")} ${file.path}`);
|
|
382
463
|
}
|
|
383
464
|
if (files.length > 50)
|
|
384
465
|
console.log(dim(` …and ${files.length - 50} more`));
|
|
466
|
+
deletions();
|
|
467
|
+
await engine();
|
|
385
468
|
return 0;
|
|
386
469
|
}
|
|
387
470
|
/**
|
|
@@ -1311,11 +1394,14 @@ async function commandRules(parsed) {
|
|
|
1311
1394
|
return suggestRules(folder, hasFlag(parsed, "apply"));
|
|
1312
1395
|
}
|
|
1313
1396
|
if (hasFlag(parsed, "init")) {
|
|
1397
|
+
/*
|
|
1398
|
+
Whatever `readRules` decided, which is now the template that matches the
|
|
1399
|
+
folder rather than one generic list. Writing STARTER_IGNORE here would
|
|
1400
|
+
have handed a Unity project the JavaScript rules, which is how an upload
|
|
1401
|
+
came back missing things inside Assets.
|
|
1402
|
+
*/
|
|
1314
1403
|
const existing = await (0, worktree_js_1.readRules)(folder);
|
|
1315
|
-
await (0, worktree_js_1.writeRules)(folder,
|
|
1316
|
-
shared: existing.shared.trim() ? existing.shared : worktree_js_1.STARTER_IGNORE,
|
|
1317
|
-
local: existing.local,
|
|
1318
|
-
});
|
|
1404
|
+
await (0, worktree_js_1.writeRules)(folder, existing);
|
|
1319
1405
|
console.log(`Wrote ${node_path_1.default.join(folder, ".gitignore")}`);
|
|
1320
1406
|
return 0;
|
|
1321
1407
|
}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.STARTER_TOPUP_HEADING = exports.SUGGESTED_HEADING = void 0;
|
|
7
|
+
exports.templatesFor = templatesFor;
|
|
7
8
|
exports.suggestExclusions = suggestExclusions;
|
|
8
9
|
exports.suggestBulkDirectories = suggestBulkDirectories;
|
|
9
10
|
exports.rulesFromSuggestions = rulesFromSuggestions;
|
|
@@ -122,6 +123,83 @@ const norm = (value) => value.split("\\").join("/");
|
|
|
122
123
|
* their upload will take and the biggest item answers it. Anything matching
|
|
123
124
|
* nothing is simply absent — silence is the right output for a tidy folder.
|
|
124
125
|
*/
|
|
126
|
+
/**
|
|
127
|
+
* Which of github/gitignore's templates this folder is asking for.
|
|
128
|
+
*
|
|
129
|
+
* The templates have been vendored all along — three hundred of them, taken
|
|
130
|
+
* from github/gitignore — but nothing ever chose one. A folder with no rules
|
|
131
|
+
* of its own got the generic starter instead, and for a game project that is
|
|
132
|
+
* the wrong list: it carries `build/`, `dist/`, `out/` and `models/**` from
|
|
133
|
+
* the JavaScript and Python worlds, none of which are anchored, all of which
|
|
134
|
+
* can reach into somewhere like `Assets/Art/models`.
|
|
135
|
+
*
|
|
136
|
+
* Recognised from the files a project must have to be that kind of project,
|
|
137
|
+
* rather than from anything a person configures. Order matters: an engine is
|
|
138
|
+
* decided first, because a Unity project also has a package.json somewhere
|
|
139
|
+
* and the engine's own template is the one that knows about `Assets`.
|
|
140
|
+
*/
|
|
141
|
+
function templatesFor(entries) {
|
|
142
|
+
const here = new Set(entries.map((name) => name.toLowerCase()));
|
|
143
|
+
const any = (test) => entries.some(test);
|
|
144
|
+
const picked = [];
|
|
145
|
+
const take = (key) => {
|
|
146
|
+
if (!picked.includes(key))
|
|
147
|
+
picked.push(key);
|
|
148
|
+
};
|
|
149
|
+
/* An engine first, and at most one: their templates overlap and disagree. */
|
|
150
|
+
if (here.has("assets") && here.has("projectsettings"))
|
|
151
|
+
take("unity");
|
|
152
|
+
else if (any((name) => name.toLowerCase().endsWith(".uproject"))) {
|
|
153
|
+
take("unrealengine");
|
|
154
|
+
}
|
|
155
|
+
else if (here.has("project.godot"))
|
|
156
|
+
take("godot");
|
|
157
|
+
/*
|
|
158
|
+
Cocos before Node, and this order is the whole reason it is written down.
|
|
159
|
+
|
|
160
|
+
A Cocos project carries a package.json, so recognising Node first called
|
|
161
|
+
it a Node project and handed it Node's rules — which know nothing about
|
|
162
|
+
`library` or `temp`, the caches Cocos rebuilds. Measured on the soak
|
|
163
|
+
shape: eight and a half thousand cache files went from left out to sent.
|
|
164
|
+
*/
|
|
165
|
+
else if (here.has("project.json") && here.has("assets"))
|
|
166
|
+
take("cocos");
|
|
167
|
+
/* Then the languages, of which a folder may honestly be several. */
|
|
168
|
+
if (here.has("package.json"))
|
|
169
|
+
take("node");
|
|
170
|
+
if (here.has("pyproject.toml") ||
|
|
171
|
+
here.has("requirements.txt") ||
|
|
172
|
+
here.has("setup.py")) {
|
|
173
|
+
take("python");
|
|
174
|
+
}
|
|
175
|
+
if (here.has("cargo.toml"))
|
|
176
|
+
take("rust");
|
|
177
|
+
if (here.has("go.mod"))
|
|
178
|
+
take("go");
|
|
179
|
+
if (here.has("build.gradle") || here.has("build.gradle.kts"))
|
|
180
|
+
take("gradle");
|
|
181
|
+
/*
|
|
182
|
+
An engine's own project files are not a second kind of project.
|
|
183
|
+
|
|
184
|
+
Unity writes the .sln and the .csproj itself, and every engine here does
|
|
185
|
+
something similar. Reading that as "also a Visual Studio project" and
|
|
186
|
+
stacking GitHub's template on top is not a harmless extra: that template
|
|
187
|
+
ignores `*.meta`, which in the .NET world is build output and in Unity is
|
|
188
|
+
the sidecar carrying every asset's GUID. On a real project it would have
|
|
189
|
+
dropped 13,689 of them, and a Unity project without its metas is exactly
|
|
190
|
+
the broken-materials mess this was all started by.
|
|
191
|
+
*/
|
|
192
|
+
const engine = picked.length > 0;
|
|
193
|
+
if (!engine && any((name) => name.toLowerCase().endsWith(".sln"))) {
|
|
194
|
+
take("visualstudio");
|
|
195
|
+
}
|
|
196
|
+
if (!engine && any((name) => name.toLowerCase().endsWith(".xcodeproj"))) {
|
|
197
|
+
take("objective-c");
|
|
198
|
+
}
|
|
199
|
+
if (!engine && here.has("cmakelists.txt"))
|
|
200
|
+
take("cmake");
|
|
201
|
+
return picked;
|
|
202
|
+
}
|
|
125
203
|
function suggestExclusions(files) {
|
|
126
204
|
const totals = new Map();
|
|
127
205
|
for (const entry of files) {
|
|
@@ -7,6 +7,7 @@ exports.parseRules = parseRules;
|
|
|
7
7
|
exports.ruleMatches = ruleMatches;
|
|
8
8
|
exports.lastMatchingRule = lastMatchingRule;
|
|
9
9
|
exports.decide = decide;
|
|
10
|
+
exports.decidingRule = decidingRule;
|
|
10
11
|
exports.excludes = excludes;
|
|
11
12
|
exports.negationReachesInto = negationReachesInto;
|
|
12
13
|
exports.isIgnored = isIgnored;
|
|
@@ -198,7 +199,21 @@ function decide(relativePath, isDirectory, layers) {
|
|
|
198
199
|
* is the dangerous direction to differ in: it uploads what somebody thought
|
|
199
200
|
* they had excluded.
|
|
200
201
|
*/
|
|
201
|
-
|
|
202
|
+
/**
|
|
203
|
+
* The rule that settles this path, counting the directories above it.
|
|
204
|
+
*
|
|
205
|
+
* A directory rule is written about the directory — `/[Ll]ibrary/` matches
|
|
206
|
+
* `Library`, not `Library/Artifacts/f1.dat` — so anything asking only about
|
|
207
|
+
* the file finds no rule and calls it included. The upload has always walked
|
|
208
|
+
* the ancestors; the filter screen asked about the file alone, and so showed
|
|
209
|
+
* a Unity project's whole import cache as going up when the save would have
|
|
210
|
+
* left every byte of it behind.
|
|
211
|
+
*
|
|
212
|
+
* Returned as the rule rather than a yes or no, because the screen names the
|
|
213
|
+
* line responsible — "Library/ — 400 files" is the useful form, and a boolean
|
|
214
|
+
* cannot say it.
|
|
215
|
+
*/
|
|
216
|
+
function decidingRule(relativePath, isDirectory, layers) {
|
|
202
217
|
/*
|
|
203
218
|
Ancestors first, outermost in. The moment one is excluded the answer is
|
|
204
219
|
settled and nothing below it can appeal.
|
|
@@ -208,9 +223,12 @@ function excludes(relativePath, isDirectory, layers) {
|
|
|
208
223
|
const ancestor = parts.slice(0, depth).join("/");
|
|
209
224
|
const above = decide(ancestor, true, layers);
|
|
210
225
|
if (above && !above.negated)
|
|
211
|
-
return
|
|
226
|
+
return above;
|
|
212
227
|
}
|
|
213
|
-
|
|
228
|
+
return decide(relativePath, isDirectory, layers);
|
|
229
|
+
}
|
|
230
|
+
function excludes(relativePath, isDirectory, layers) {
|
|
231
|
+
const rule = decidingRule(relativePath, isDirectory, layers);
|
|
214
232
|
return Boolean(rule && !rule.negated);
|
|
215
233
|
}
|
|
216
234
|
/**
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TransferProgress = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* How much of an upload has happened, and how fast it is going.
|
|
6
|
+
*
|
|
7
|
+
* Lifted out of the upload pass because nearly every number a person watches
|
|
8
|
+
* during a save comes from here, and three separate faults have come out of it
|
|
9
|
+
* being a handful of loose counters in the middle of a very long function:
|
|
10
|
+
*
|
|
11
|
+
* - Six lanes each added their own offset to one shared total, so the figure
|
|
12
|
+
* jumped between six different answers and the bar ran backwards.
|
|
13
|
+
* - The rate was `transferred` over the whole elapsed time while the bar
|
|
14
|
+
* beside it showed something else, so one real upload sat at
|
|
15
|
+
* "152 MB of 1.1 GB · 10 B/s" — not a slow upload, a number describing a
|
|
16
|
+
* different thing.
|
|
17
|
+
* - A lifetime average meant a slow first minute went on dragging the figure
|
|
18
|
+
* down long after the link had recovered, which is the opposite of what
|
|
19
|
+
* somebody watching it wants to know.
|
|
20
|
+
*
|
|
21
|
+
* So the counters and the arithmetic over them live together, with names, and
|
|
22
|
+
* can be tested without starting an upload.
|
|
23
|
+
*/
|
|
24
|
+
class TransferProgress {
|
|
25
|
+
windowMs;
|
|
26
|
+
now;
|
|
27
|
+
/** Bytes of files that have finished, whether they travelled or not. */
|
|
28
|
+
settled = 0;
|
|
29
|
+
/** Bytes that actually crossed the wire. */
|
|
30
|
+
moved = 0;
|
|
31
|
+
/** Files the service already held, so nothing crossed the wire for them. */
|
|
32
|
+
reused = 0;
|
|
33
|
+
/*
|
|
34
|
+
How far each lane is through the file it currently holds, kept apart so the
|
|
35
|
+
total is the sum of real work rather than six lanes' guesses about one
|
|
36
|
+
shared number.
|
|
37
|
+
*/
|
|
38
|
+
inFlight = new Map();
|
|
39
|
+
samples = [];
|
|
40
|
+
constructor(
|
|
41
|
+
/** How long a window the rate is measured over. */
|
|
42
|
+
windowMs = 5000,
|
|
43
|
+
/** Injected so a test can move time without waiting for it. */
|
|
44
|
+
now = Date.now) {
|
|
45
|
+
this.windowMs = windowMs;
|
|
46
|
+
this.now = now;
|
|
47
|
+
}
|
|
48
|
+
/** A file finished: its own size, and what it cost to send. */
|
|
49
|
+
finished(sourceBytes, wireBytes) {
|
|
50
|
+
this.settled += sourceBytes;
|
|
51
|
+
this.moved += wireBytes;
|
|
52
|
+
}
|
|
53
|
+
/** A file the service already had. Counted apart so "sent" stays honest. */
|
|
54
|
+
alreadyHeld() {
|
|
55
|
+
this.reused += 1;
|
|
56
|
+
}
|
|
57
|
+
/** Where one lane has got to inside the file it is sending. */
|
|
58
|
+
lane(index, offset) {
|
|
59
|
+
this.inFlight.set(index, offset);
|
|
60
|
+
}
|
|
61
|
+
/** That lane has finished, so its offset is now part of the settled total. */
|
|
62
|
+
laneDone(index) {
|
|
63
|
+
this.inFlight.delete(index);
|
|
64
|
+
}
|
|
65
|
+
/** Everything settled, plus how far the open lanes have got. */
|
|
66
|
+
bytes() {
|
|
67
|
+
let total = this.settled;
|
|
68
|
+
for (const offset of this.inFlight.values())
|
|
69
|
+
total += offset;
|
|
70
|
+
return total;
|
|
71
|
+
}
|
|
72
|
+
get sentBytes() {
|
|
73
|
+
return this.settled;
|
|
74
|
+
}
|
|
75
|
+
get transferred() {
|
|
76
|
+
return this.moved;
|
|
77
|
+
}
|
|
78
|
+
get alreadyOnAccount() {
|
|
79
|
+
return this.reused;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* How fast the thing on screen is moving, over a short trailing window.
|
|
83
|
+
*
|
|
84
|
+
* The same counter the bar uses, so the two agree. Sampled rather than
|
|
85
|
+
* recorded per call: this is asked once per report, and reports arrive per
|
|
86
|
+
* file, which for small files is thousands a second.
|
|
87
|
+
*/
|
|
88
|
+
rate() {
|
|
89
|
+
const now = this.now();
|
|
90
|
+
const bytes = this.bytes();
|
|
91
|
+
const last = this.samples[this.samples.length - 1];
|
|
92
|
+
if (!last || now - last.at >= 250)
|
|
93
|
+
this.samples.push({ at: now, bytes });
|
|
94
|
+
while (this.samples.length > 2 && now - this.samples[0].at > this.windowMs) {
|
|
95
|
+
this.samples.shift();
|
|
96
|
+
}
|
|
97
|
+
const oldest = this.samples[0];
|
|
98
|
+
const seconds = (now - oldest.at) / 1000;
|
|
99
|
+
/*
|
|
100
|
+
Nothing until there is something to divide. Answering zero keeps the
|
|
101
|
+
window quiet rather than showing a figure computed from a fraction of a
|
|
102
|
+
second, which reads as a stall on an upload that has barely begun.
|
|
103
|
+
*/
|
|
104
|
+
if (seconds < 0.5)
|
|
105
|
+
return 0;
|
|
106
|
+
return Math.max(0, Math.round((bytes - oldest.bytes) / seconds));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.TransferProgress = TransferProgress;
|