@coderook/cli 0.29.2 → 0.29.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/cli/src/cli.js +59 -1
- package/dist/cli/src/config.js +19 -0
- package/dist/desktop-app/src/main/detect.js +94 -4
- package/dist/desktop-app/src/main/download.js +59 -27
- package/dist/desktop-app/src/main/upload.js +15 -1
- package/dist/desktop-app/src/main/worktree.js +77 -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.29.
|
|
5
|
+
"version": "0.29.4",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -194,6 +194,23 @@ async function reconcile(folder, options = {}) {
|
|
|
194
194
|
const project = link
|
|
195
195
|
? (await (0, api_js_2.projects)()).find((candidate) => candidate.id === link.repositoryId)
|
|
196
196
|
: await (0, api_js_2.findProject)(reference);
|
|
197
|
+
/*
|
|
198
|
+
A link that has outlived the project it points at.
|
|
199
|
+
|
|
200
|
+
The list above is everything this account can reach — owned, shared with
|
|
201
|
+
them, and through their organisation — so a linked project missing from it
|
|
202
|
+
is gone or no longer theirs. Left alone, the next call that used the
|
|
203
|
+
remembered id failed with "Repository not found", which names neither the
|
|
204
|
+
folder nor anything to do about it; the soak suite hit exactly this and it
|
|
205
|
+
took a debugger to work out which folder was complaining.
|
|
206
|
+
*/
|
|
207
|
+
if (link && !project) {
|
|
208
|
+
throw new Error(`This folder is linked to a project that is no longer on your account.\n` +
|
|
209
|
+
` ${folder}\n` +
|
|
210
|
+
`It may have been deleted, or your access to it withdrawn.\n` +
|
|
211
|
+
`Detach the folder with ${accent("cbx unlink")} — the files here are ` +
|
|
212
|
+
`untouched, and submitting afterwards starts a new project from them.`);
|
|
213
|
+
}
|
|
197
214
|
if (!project?.versionCount) {
|
|
198
215
|
// Nothing saved on the account, so everything here is outstanding.
|
|
199
216
|
return { link: link ?? null, baseline: null, behind: null };
|
|
@@ -303,6 +320,19 @@ async function commandProjects() {
|
|
|
303
320
|
}
|
|
304
321
|
return 0;
|
|
305
322
|
}
|
|
323
|
+
async function commandUnlink(parsed) {
|
|
324
|
+
const folder = folderFor(parsed);
|
|
325
|
+
const link = await (0, config_js_1.readLink)(folder);
|
|
326
|
+
if (!(await (0, config_js_1.forgetLink)(folder))) {
|
|
327
|
+
console.log("This folder was not linked to a project.");
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
console.log(`Detached ${bold(node_path_1.default.basename(folder))}` +
|
|
331
|
+
(link?.slug ? dim(` from ${link.slug}`) : "") +
|
|
332
|
+
".");
|
|
333
|
+
console.log(dim("Nothing was deleted. Submitting from here now starts a new project."));
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
306
336
|
async function commandStatus(parsed) {
|
|
307
337
|
const folder = folderFor(parsed);
|
|
308
338
|
let link;
|
|
@@ -615,7 +645,21 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
|
615
645
|
file should be sent and the fix is to take the key out of it.
|
|
616
646
|
*/
|
|
617
647
|
const pasted = await (0, worktree_js_1.detectPastedCredentials)(folder, files.map((file) => file.path));
|
|
618
|
-
const stillPasted = pasted.filter((finding) => !exposed.includes(finding.path));
|
|
648
|
+
const stillPasted = pasted.found.filter((finding) => !exposed.includes(finding.path));
|
|
649
|
+
/*
|
|
650
|
+
Said whether anything was found or not.
|
|
651
|
+
|
|
652
|
+
The scan stops at twenty thousand files, and it used to stop in silence —
|
|
653
|
+
so a project of forty-seven thousand had twenty-seven thousand of them
|
|
654
|
+
never opened behind a clean run, and the terminal said nothing at all. A
|
|
655
|
+
check that quietly covers half of what it appears to cover is worse than
|
|
656
|
+
one that admits its limit, because the first is believed.
|
|
657
|
+
*/
|
|
658
|
+
if (!pasted.reach.complete) {
|
|
659
|
+
console.log(dim(`Checked the first ${pasted.reach.scanned.toLocaleString()} of ` +
|
|
660
|
+
`${pasted.reach.of.toLocaleString()} files for pasted keys; the ` +
|
|
661
|
+
`rest were not opened.`));
|
|
662
|
+
}
|
|
619
663
|
if (stillPasted.length) {
|
|
620
664
|
console.log(red(`
|
|
621
665
|
${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
|
|
@@ -1678,6 +1722,20 @@ const SPECS = [
|
|
|
1678
1722
|
],
|
|
1679
1723
|
run: commandSubmit,
|
|
1680
1724
|
},
|
|
1725
|
+
{
|
|
1726
|
+
name: "unlink",
|
|
1727
|
+
group: "Working with a folder",
|
|
1728
|
+
summary: "detach this folder from its project",
|
|
1729
|
+
usage: "unlink [folder]",
|
|
1730
|
+
detail: "Forgets which project this folder belongs to. Nothing is deleted:\n" +
|
|
1731
|
+
"the files here are untouched and the project stays on your account.\n\n" +
|
|
1732
|
+
"Mostly useful when a folder points at a project that is gone — one\n" +
|
|
1733
|
+
"that was deleted, or shared with you and since withdrawn — because\n" +
|
|
1734
|
+
"until the folder forgets it, every command asks about a project that\n" +
|
|
1735
|
+
"is no longer there.",
|
|
1736
|
+
examples: ["cbx unlink", "cbx unlink ./old-copy"],
|
|
1737
|
+
run: commandUnlink,
|
|
1738
|
+
},
|
|
1681
1739
|
{
|
|
1682
1740
|
name: "import",
|
|
1683
1741
|
group: "Getting started",
|
package/dist/cli/src/config.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.loadToken = loadToken;
|
|
|
11
11
|
exports.clearToken = clearToken;
|
|
12
12
|
exports.keyFor = keyFor;
|
|
13
13
|
exports.readLink = readLink;
|
|
14
|
+
exports.forgetLink = forgetLink;
|
|
14
15
|
exports.writeLink = writeLink;
|
|
15
16
|
/**
|
|
16
17
|
* Where the command-line tool keeps its credential and its project links.
|
|
@@ -151,6 +152,24 @@ async function readLink(localPath) {
|
|
|
151
152
|
baseVersionId: link.baseVersionId ?? link.versionId,
|
|
152
153
|
};
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Forget which project a folder belongs to.
|
|
157
|
+
*
|
|
158
|
+
* Both keys, because a folder linked by an older build is recorded under the
|
|
159
|
+
* old one and leaving that behind would mean the folder was still linked
|
|
160
|
+
* afterwards — which is the one thing this is for.
|
|
161
|
+
*/
|
|
162
|
+
async function forgetLink(localPath) {
|
|
163
|
+
const links = await readLinks();
|
|
164
|
+
const had = links[keyFor(localPath)] !== undefined ||
|
|
165
|
+
links[legacyKeyFor(localPath)] !== undefined;
|
|
166
|
+
if (!had)
|
|
167
|
+
return false;
|
|
168
|
+
delete links[keyFor(localPath)];
|
|
169
|
+
delete links[legacyKeyFor(localPath)];
|
|
170
|
+
await writePrivate(linksFile(), JSON.stringify(links, null, 2));
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
154
173
|
async function writeLink(localPath, link) {
|
|
155
174
|
const links = await readLinks();
|
|
156
175
|
// A folder recorded under the old key moves to the new one rather than
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.suggestExclusions = suggestExclusions;
|
|
7
|
+
exports.suggestBulkDirectories = suggestBulkDirectories;
|
|
7
8
|
exports.rulesFromSuggestions = rulesFromSuggestions;
|
|
8
9
|
/**
|
|
9
10
|
* Looking at a folder and saying what probably should not be uploaded.
|
|
@@ -39,6 +40,14 @@ const RULES = [
|
|
|
39
40
|
{ directory: ".next", reason: "Next.js build output", recommended: true },
|
|
40
41
|
{ directory: ".gradle", reason: "Gradle build state", recommended: true },
|
|
41
42
|
{ directory: ".terraform", reason: "Downloaded Terraform providers", recommended: true },
|
|
43
|
+
/*
|
|
44
|
+
Offered rather than recommended, and deliberately not in the starter
|
|
45
|
+
template: a Go project that vendors its dependencies often commits them on
|
|
46
|
+
purpose, so leaving them out by default would drop files the project is
|
|
47
|
+
meant to carry. Worth asking about, because it is usually most of the
|
|
48
|
+
files — 6,000 of 6,440 in the shape that found this.
|
|
49
|
+
*/
|
|
50
|
+
{ directory: "vendor", reason: "Vendored dependencies — often committed on purpose", recommended: false },
|
|
42
51
|
/*
|
|
43
52
|
Game engines, which is where the size actually is.
|
|
44
53
|
|
|
@@ -160,15 +169,96 @@ function suggestExclusions(files) {
|
|
|
160
169
|
break;
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
|
-
|
|
164
|
-
.map(([pattern, held]) => ({
|
|
172
|
+
const named = [...totals.entries()].map(([pattern, held]) => ({
|
|
165
173
|
pattern,
|
|
166
174
|
reason: held.rule.reason,
|
|
167
175
|
bytes: held.bytes,
|
|
168
176
|
files: held.files,
|
|
169
177
|
recommended: held.rule.recommended,
|
|
170
|
-
}))
|
|
171
|
-
|
|
178
|
+
}));
|
|
179
|
+
/*
|
|
180
|
+
Merged in here rather than offered through a second channel, so everything
|
|
181
|
+
that shows a suggestion keeps working unchanged. A folder already named by
|
|
182
|
+
a rule above is dropped: it has a better reason than "no source in it".
|
|
183
|
+
*/
|
|
184
|
+
const already = new Set(named.map((one) => one.pattern));
|
|
185
|
+
const bulk = suggestBulkDirectories(files).filter((one) => !already.has(one.pattern));
|
|
186
|
+
return [...named, ...bulk].sort((left, right) => right.bytes - left.bytes);
|
|
187
|
+
}
|
|
188
|
+
/*
|
|
189
|
+
Extensions that mean somebody wrote this by hand. Deliberately short: the
|
|
190
|
+
question below is only "is there any source in here at all", and a longer
|
|
191
|
+
list would answer yes for a folder holding one stray script.
|
|
192
|
+
*/
|
|
193
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
194
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rb", ".go", ".rs",
|
|
195
|
+
".java", ".kt", ".swift", ".c", ".h", ".cc", ".cpp", ".hpp", ".cs", ".php",
|
|
196
|
+
".lua", ".sh", ".ps1", ".sql", ".css", ".scss", ".html", ".vue", ".svelte",
|
|
197
|
+
".md", ".toml", ".yaml", ".yml", ".gradle", ".cmake",
|
|
198
|
+
]);
|
|
199
|
+
/** How many files a folder must hold before it is worth asking about. */
|
|
200
|
+
const BULK_FILES = 2_000;
|
|
201
|
+
/**
|
|
202
|
+
* And how much of the upload it must be.
|
|
203
|
+
*
|
|
204
|
+
* Set high on purpose. At half, a game's 4,000-file `assets/` folder in a
|
|
205
|
+
* 6,500-file project was flagged as "no source in it" — which is true and
|
|
206
|
+
* beside the point, because the art is the work. The folders this is for are
|
|
207
|
+
* not borderline: `work/` here is 98% of the upload and a Unity `Library/`
|
|
208
|
+
* is 96%.
|
|
209
|
+
*/
|
|
210
|
+
const BULK_SHARE = 0.7;
|
|
211
|
+
/** Above this share of hand-written files it is source, not output. */
|
|
212
|
+
const BULK_SOURCE_SHARE = 0.1;
|
|
213
|
+
/**
|
|
214
|
+
* One enormous folder that no list of names could have known about.
|
|
215
|
+
*
|
|
216
|
+
* Every rule above recognises a folder by its name, which works for the dozen
|
|
217
|
+
* things every project has and not at all for the one a person invented. This
|
|
218
|
+
* repository's own `work/` is 63,210 of the 64,554 files it would otherwise
|
|
219
|
+
* send — scratch output from a test harness, named nothing in particular, and
|
|
220
|
+
* invisible to every rule in this file.
|
|
221
|
+
*
|
|
222
|
+
* So this asks about shape instead of name: one folder that is most of the
|
|
223
|
+
* upload and holds no source in it. Never recommended, because the same shape
|
|
224
|
+
* is also what a genuine dataset looks like, and a project whose work really
|
|
225
|
+
* is fifty thousand data files should be asked once and then believed.
|
|
226
|
+
*/
|
|
227
|
+
function suggestBulkDirectories(files) {
|
|
228
|
+
if (files.length < BULK_FILES)
|
|
229
|
+
return [];
|
|
230
|
+
const tally = new Map();
|
|
231
|
+
for (const entry of files) {
|
|
232
|
+
const normalised = entry.path.split("\\").join("/");
|
|
233
|
+
const top = normalised.includes("/") ? normalised.slice(0, normalised.indexOf("/")) : "";
|
|
234
|
+
/* A file at the root belongs to no folder, so it cannot be one of these. */
|
|
235
|
+
if (!top)
|
|
236
|
+
continue;
|
|
237
|
+
const held = tally.get(top) ?? { bytes: 0, files: 0, source: 0 };
|
|
238
|
+
held.bytes += entry.size;
|
|
239
|
+
held.files += 1;
|
|
240
|
+
if (SOURCE_EXTENSIONS.has(node_path_1.default.extname(normalised).toLowerCase()))
|
|
241
|
+
held.source += 1;
|
|
242
|
+
tally.set(top, held);
|
|
243
|
+
}
|
|
244
|
+
const found = [];
|
|
245
|
+
for (const [top, held] of tally) {
|
|
246
|
+
if (held.files < BULK_FILES)
|
|
247
|
+
continue;
|
|
248
|
+
if (held.files / files.length < BULK_SHARE)
|
|
249
|
+
continue;
|
|
250
|
+
if (held.source / held.files >= BULK_SOURCE_SHARE)
|
|
251
|
+
continue;
|
|
252
|
+
found.push({
|
|
253
|
+
pattern: `${top}/`,
|
|
254
|
+
reason: `${held.files.toLocaleString()} of ${files.length.toLocaleString()} files, ` +
|
|
255
|
+
`and no source in it — generated?`,
|
|
256
|
+
bytes: held.bytes,
|
|
257
|
+
files: held.files,
|
|
258
|
+
recommended: false,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return found.sort((left, right) => right.bytes - left.bytes);
|
|
172
262
|
}
|
|
173
263
|
/**
|
|
174
264
|
* The suggestions a person accepted, as lines for the rules file.
|
|
@@ -585,43 +585,75 @@ class Downloader {
|
|
|
585
585
|
sayProgress(file.path);
|
|
586
586
|
};
|
|
587
587
|
/*
|
|
588
|
-
|
|
588
|
+
Bounded by bytes in flight, not by how many files are open.
|
|
589
589
|
|
|
590
590
|
Six lanes turned an hour-long restore of many small files into a
|
|
591
|
-
minute, and made a restore of
|
|
592
|
-
slower
|
|
593
|
-
|
|
594
|
-
|
|
591
|
+
minute, and made a restore of three very large ones eight times
|
|
592
|
+
slower — 93 seconds became 802. Splitting the queue by file size
|
|
593
|
+
fixed that shape and cost another: a project of forty-megabyte video
|
|
594
|
+
files had its six big ones running two at a time while the small ones
|
|
595
|
+
finished and left the line idle, and its fetch went from 54 seconds
|
|
596
|
+
to 151.
|
|
595
597
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
598
|
+
Neither is really about file count. A file is fetched a piece at a
|
|
599
|
+
time, so each job holds exactly one request open whatever its size —
|
|
600
|
+
what six large files at once actually costs is six streams' worth of
|
|
601
|
+
bytes being buffered, hashed and written against one line. So that is
|
|
602
|
+
what is bounded: lanes take work while the bytes already in flight
|
|
603
|
+
leave room for it. Forty-megabyte files then run five or six at a
|
|
604
|
+
time and hundred-megabyte ones run two, without a threshold having to
|
|
605
|
+
guess which project this is.
|
|
602
606
|
*/
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
607
|
+
const LANES = 6;
|
|
608
|
+
const BYTES_IN_FLIGHT = 200 * 1024 * 1024;
|
|
609
|
+
const sizeOf = (job) => job.kind === "pack" ? 0 : job.file.sourceSize;
|
|
610
|
+
let next = 0;
|
|
611
|
+
let flying = 0;
|
|
612
|
+
/*
|
|
613
|
+
Every waiting lane, not the most recent one. A single slot would let a
|
|
614
|
+
second waiter overwrite the first, and the first would then never be
|
|
615
|
+
woken — a restore that stops with lanes idle and work left.
|
|
616
|
+
*/
|
|
617
|
+
const waiting = [];
|
|
618
|
+
const wakeAll = () => {
|
|
619
|
+
while (waiting.length)
|
|
620
|
+
waiting.pop()();
|
|
621
|
+
};
|
|
622
|
+
const roomFor = async (size) => {
|
|
623
|
+
/*
|
|
624
|
+
One job always proceeds, however large. A file bigger than the whole
|
|
625
|
+
budget would otherwise wait for room that can never appear.
|
|
626
|
+
*/
|
|
627
|
+
while (flying > 0 && flying + size > BYTES_IN_FLIGHT) {
|
|
628
|
+
await new Promise((resolve) => {
|
|
629
|
+
waiting.push(resolve);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
const lane = async () => {
|
|
634
|
+
for (;;) {
|
|
635
|
+
const at = next;
|
|
636
|
+
next += 1;
|
|
637
|
+
if (at >= jobs.length)
|
|
638
|
+
return;
|
|
639
|
+
this.check();
|
|
640
|
+
const job = jobs[at];
|
|
641
|
+
const size = sizeOf(job);
|
|
642
|
+
await roomFor(size);
|
|
643
|
+
flying += size;
|
|
644
|
+
try {
|
|
616
645
|
if (job.kind === "pack")
|
|
617
646
|
await restorePack(job.id);
|
|
618
647
|
else
|
|
619
648
|
await restoreFile(job.file);
|
|
620
649
|
}
|
|
621
|
-
|
|
622
|
-
|
|
650
|
+
finally {
|
|
651
|
+
flying -= size;
|
|
652
|
+
wakeAll();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
623
655
|
};
|
|
624
|
-
await Promise.all(
|
|
656
|
+
await Promise.all(Array.from({ length: Math.min(LANES, jobs.length) }, () => lane()));
|
|
625
657
|
/*
|
|
626
658
|
Everything has arrived and verified, so it can go into place. Each
|
|
627
659
|
file is renamed over its destination individually: a rename within
|
|
@@ -132,6 +132,8 @@ class Uploader {
|
|
|
132
132
|
while somebody reads the plan is hashed again rather than published under
|
|
133
133
|
the digest it used to have.
|
|
134
134
|
*/
|
|
135
|
+
/** Whether this save has already said that packing was refused. */
|
|
136
|
+
packRefusalSaid = false;
|
|
135
137
|
plannedDigests = new Map();
|
|
136
138
|
planningCreatedRepositoryId = null;
|
|
137
139
|
/** Attached to every object write after the server admits the exact plan. */
|
|
@@ -272,6 +274,7 @@ class Uploader {
|
|
|
272
274
|
*/
|
|
273
275
|
async plan(request, report) {
|
|
274
276
|
this.planning = true;
|
|
277
|
+
this.packRefusalSaid = false;
|
|
275
278
|
this.activeQuoteId = null;
|
|
276
279
|
this.plannedQuote = null;
|
|
277
280
|
this.planningCreatedRepositoryId = null;
|
|
@@ -968,12 +971,23 @@ class Uploader {
|
|
|
968
971
|
batched.set(digest, one);
|
|
969
972
|
}
|
|
970
973
|
}
|
|
971
|
-
catch {
|
|
974
|
+
catch (reason) {
|
|
972
975
|
/*
|
|
973
976
|
A refusal here is never fatal: everything in the pack is simply
|
|
974
977
|
left for the ordinary path below, which sends it one at a time.
|
|
975
978
|
An older deployment without this route lands here every time.
|
|
979
|
+
|
|
980
|
+
Said once per save, not once per pack. It used to be swallowed
|
|
981
|
+
outright, and when the service began refusing packs for a reason
|
|
982
|
+
that also broke the fallback, the only thing anybody saw was the
|
|
983
|
+
fallback's complaint about an arbitrary file — which described
|
|
984
|
+
neither what went wrong nor where. One line naming the first
|
|
985
|
+
refusal is what makes the next one an hour shorter to find.
|
|
976
986
|
*/
|
|
987
|
+
if (!this.packRefusalSaid) {
|
|
988
|
+
this.packRefusalSaid = true;
|
|
989
|
+
console.warn(`Packing was refused; sending these files individually. ${String(reason).slice(0, 300)}`);
|
|
990
|
+
}
|
|
977
991
|
}
|
|
978
992
|
/*
|
|
979
993
|
Only what actually landed. Counting the whole pack meant a batch the
|
|
@@ -3,7 +3,9 @@ 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.LOCAL_EXCLUDE_FILE = exports.IGNORE_FILE = exports.STARTER_IGNORE = exports.EVALUATION_FILE_LIMIT = void 0;
|
|
6
|
+
exports.LOCAL_EXCLUDE_FILE = exports.IGNORE_FILE = exports.STARTER_IGNORE = exports.STARTER_MARKER = exports.EVALUATION_FILE_LIMIT = void 0;
|
|
7
|
+
exports.starterAuthored = starterAuthored;
|
|
8
|
+
exports.missingStarterRules = missingStarterRules;
|
|
7
9
|
exports.isRepository = isRepository;
|
|
8
10
|
exports.migrateRetiredRules = migrateRetiredRules;
|
|
9
11
|
exports.readRules = readRules;
|
|
@@ -54,7 +56,10 @@ async function git(root, ...args) {
|
|
|
54
56
|
* file would produce an incomplete project that looked backed up, so likely
|
|
55
57
|
* secrets are warned about instead (docs/UPLOAD_POLICY.md).
|
|
56
58
|
*/
|
|
57
|
-
exports.
|
|
59
|
+
exports.STARTER_MARKER = "# CodeRook starter rules — edit freely below";
|
|
60
|
+
exports.STARTER_IGNORE = `${exports.STARTER_MARKER}
|
|
61
|
+
|
|
62
|
+
# Dependencies and generated output
|
|
58
63
|
node_modules/
|
|
59
64
|
dist/
|
|
60
65
|
build/
|
|
@@ -96,7 +101,6 @@ models/**
|
|
|
96
101
|
*.pth
|
|
97
102
|
|
|
98
103
|
# Caches and local state
|
|
99
|
-
.venv/
|
|
100
104
|
.pytest_cache/
|
|
101
105
|
.mypy_cache/
|
|
102
106
|
.ruff_cache/
|
|
@@ -120,6 +124,63 @@ Preferences
|
|
|
120
124
|
# Archives of the project, inside the project
|
|
121
125
|
*.cbx
|
|
122
126
|
`;
|
|
127
|
+
/**
|
|
128
|
+
* Lines only this application writes, used to recognise its own handiwork.
|
|
129
|
+
*
|
|
130
|
+
* `*.cbx` is the strongest of them: a CodeRook bundle inside the project is a
|
|
131
|
+
* CodeRook idea and nothing else puts that line in a .gitignore. The browser
|
|
132
|
+
* profile names are the next best, because they are an unusual thing for a
|
|
133
|
+
* person to think of and they arrive together.
|
|
134
|
+
*/
|
|
135
|
+
const STARTER_FINGERPRINT = [
|
|
136
|
+
"IndexedDB/",
|
|
137
|
+
"Local Storage/",
|
|
138
|
+
"GPUCache/",
|
|
139
|
+
"Code Cache/",
|
|
140
|
+
"blob_storage/",
|
|
141
|
+
"Local State",
|
|
142
|
+
"Preferences",
|
|
143
|
+
];
|
|
144
|
+
/** Every rule in a block of text, without comments or blank lines. */
|
|
145
|
+
function ruleLines(text) {
|
|
146
|
+
return text
|
|
147
|
+
.split(/\r?\n/)
|
|
148
|
+
.map((line) => line.trim())
|
|
149
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether this .gitignore was written by CodeRook rather than by a person.
|
|
153
|
+
*
|
|
154
|
+
* It matters because the starter template grows: engine caches were added to
|
|
155
|
+
* it long after the first folders were set up, and those folders can never
|
|
156
|
+
* benefit — a project with any .gitignore uses that file and nothing else. A
|
|
157
|
+
* Unity project set up before the Unity rules existed therefore went on
|
|
158
|
+
* sending its whole import cache for good, 45,342 files of it, with the
|
|
159
|
+
* improved template sitting unused in this same file.
|
|
160
|
+
*
|
|
161
|
+
* Newly written starters carry STARTER_MARKER, so this is exact for anything
|
|
162
|
+
* written from now on. The fingerprint is for the ones already on disk.
|
|
163
|
+
*/
|
|
164
|
+
function starterAuthored(text) {
|
|
165
|
+
if (text.includes(exports.STARTER_MARKER))
|
|
166
|
+
return true;
|
|
167
|
+
const lines = new Set(ruleLines(text));
|
|
168
|
+
if (!lines.has("*.cbx"))
|
|
169
|
+
return false;
|
|
170
|
+
const seen = STARTER_FINGERPRINT.filter((one) => lines.has(one)).length;
|
|
171
|
+
return seen >= 3;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The starter rules this file is missing, in template order.
|
|
175
|
+
*
|
|
176
|
+
* Only ever offered, never applied on its own: a person may have deleted a
|
|
177
|
+
* line on purpose, and re-adding it silently would be this application
|
|
178
|
+
* overruling them in their own file.
|
|
179
|
+
*/
|
|
180
|
+
function missingStarterRules(text) {
|
|
181
|
+
const present = new Set(ruleLines(text));
|
|
182
|
+
return ruleLines(exports.STARTER_IGNORE).filter((line) => !present.has(line));
|
|
183
|
+
}
|
|
123
184
|
/** The shared rules file, committed with the project. */
|
|
124
185
|
exports.IGNORE_FILE = ".gitignore";
|
|
125
186
|
/** Personal exclusions, which must never reach a collaborator. */
|
|
@@ -1103,7 +1164,17 @@ onScanned) {
|
|
|
1103
1164
|
}
|
|
1104
1165
|
};
|
|
1105
1166
|
await Promise.all(Array.from({ length: Math.min(LANES, wanted.length) }, () => lane()));
|
|
1106
|
-
|
|
1167
|
+
/*
|
|
1168
|
+
`stop` is also set by the findings ceiling, which is a different statement
|
|
1169
|
+
from running out of budget — forty findings is plenty to act on, and the
|
|
1170
|
+
files after it are very likely more of the same. Only a bound on the
|
|
1171
|
+
reading makes the scan incomplete in a way worth saying out loud.
|
|
1172
|
+
*/
|
|
1173
|
+
const spent = read >= MOST_SCANNED_FILES || bytes >= MOST_SCANNED_BYTES;
|
|
1174
|
+
return {
|
|
1175
|
+
found,
|
|
1176
|
+
reach: { scanned: read, of: wanted.length, complete: !spent },
|
|
1177
|
+
};
|
|
1107
1178
|
}
|
|
1108
1179
|
async function uploadConcerns(root, include,
|
|
1109
1180
|
/** Passed through so the one slow part of this can say it is working. */
|
|
@@ -1124,7 +1195,8 @@ onScanned) {
|
|
|
1124
1195
|
is two warnings for one problem, and the first one is the actionable
|
|
1125
1196
|
one.
|
|
1126
1197
|
*/
|
|
1127
|
-
pasted: pasted.filter((finding) => !secrets.includes(finding.path)),
|
|
1198
|
+
pasted: pasted.found.filter((finding) => !secrets.includes(finding.path)),
|
|
1199
|
+
reach: pasted.reach,
|
|
1128
1200
|
};
|
|
1129
1201
|
}
|
|
1130
1202
|
/** Files the flow must ask about before the first upload. */
|