@coderook/cli 0.29.5 → 0.30.1
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 +102 -18
- package/dist/desktop-app/src/main/rules.js +48 -11
- 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 +266 -5
- package/package.json +1 -1
|
@@ -3,11 +3,14 @@ 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.STARTER_MARKER = exports.EVALUATION_FILE_LIMIT = void 0;
|
|
6
|
+
exports.IGNORE_HEADING = exports.KEEP_HEADING = exports.LOCAL_EXCLUDE_FILE = exports.IGNORE_FILE = exports.STARTER_IGNORE = exports.CODEROOK_TAIL = exports.CODEROOK_TAIL_HEADING = exports.KEPT_ANYWAY = exports.KEPT_ANYWAY_HEADING = exports.ALSO_LEFT_OUT = exports.ALSO_LEFT_OUT_HEADING = exports.STARTER_MARKER = exports.EVALUATION_FILE_LIMIT = void 0;
|
|
7
7
|
exports.starterAuthored = starterAuthored;
|
|
8
8
|
exports.missingStarterRules = missingStarterRules;
|
|
9
|
+
exports.missingEngineRules = missingEngineRules;
|
|
9
10
|
exports.isRepository = isRepository;
|
|
10
11
|
exports.migrateRetiredRules = migrateRetiredRules;
|
|
12
|
+
exports.starterFor = starterFor;
|
|
13
|
+
exports.moveRule = moveRule;
|
|
11
14
|
exports.readRules = readRules;
|
|
12
15
|
exports.writeRules = writeRules;
|
|
13
16
|
exports.collectLayers = collectLayers;
|
|
@@ -31,9 +34,13 @@ const node_fs_1 = require("node:fs");
|
|
|
31
34
|
const promises_1 = require("node:fs/promises");
|
|
32
35
|
const node_path_1 = __importDefault(require("node:path"));
|
|
33
36
|
const node_util_1 = require("node:util");
|
|
37
|
+
const detect_js_1 = require("./detect.js");
|
|
38
|
+
const ignore_templates_js_1 = require("./ignore_templates.js");
|
|
34
39
|
const rules_js_1 = require("./rules.js");
|
|
35
40
|
const secret_patterns_js_1 = require("./secret_patterns.js");
|
|
36
41
|
const run = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
42
|
+
/* A code point, so nothing between here and disk can eat the escape. */
|
|
43
|
+
const NEWLINE = String.fromCharCode(10);
|
|
37
44
|
/** Past this the evaluation reports truncated rather than walking forever. */
|
|
38
45
|
exports.EVALUATION_FILE_LIMIT = 300_000;
|
|
39
46
|
async function git(root, ...args) {
|
|
@@ -57,6 +64,73 @@ async function git(root, ...args) {
|
|
|
57
64
|
* secrets are warned about instead (docs/UPLOAD_POLICY.md).
|
|
58
65
|
*/
|
|
59
66
|
exports.STARTER_MARKER = "# CodeRook starter rules — edit freely below";
|
|
67
|
+
/**
|
|
68
|
+
* What github/gitignore has no template for, kept beside whichever it picks.
|
|
69
|
+
*
|
|
70
|
+
* A browser or Electron profile left in a project folder holds files another
|
|
71
|
+
* program keeps open — a LOCK that cannot be read while it runs will stop a
|
|
72
|
+
* save outright — and none of it is the work. No language template mentions
|
|
73
|
+
* them, because no language puts them there.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Where CodeRook and git want different things, and CodeRook wins.
|
|
77
|
+
*
|
|
78
|
+
* A .gitignore is written for a repository, where an archive is something you
|
|
79
|
+
* could rebuild from the source beside it, so github/gitignore leaves
|
|
80
|
+
* `*.unitypackage` out. CodeRook is not that. A version here is meant to be
|
|
81
|
+
* the whole of a folder, and an asset-store package sitting inside `Assets`
|
|
82
|
+
* is not rebuildable from anything in the project — it is a thing somebody
|
|
83
|
+
* bought, and on the other machine it would simply be gone.
|
|
84
|
+
*
|
|
85
|
+
* Measured on a real project: forty-two of them, one per render pipeline and
|
|
86
|
+
* Unity version, all inside `Assets`. Exactly the kind of "missing stuff"
|
|
87
|
+
* that a save is supposed to make impossible.
|
|
88
|
+
*
|
|
89
|
+
* The `.meta` line guards against this application's own layering, and
|
|
90
|
+
* nothing more. Unity's template keeps metas; `*.meta` is build output to
|
|
91
|
+
* Visual Studio and its template says so, and a Unity folder has a .sln in it
|
|
92
|
+
* because Unity writes one. Placed after every template so it is the last
|
|
93
|
+
* word among the rules we stack — it cannot, and should not, outrank a line
|
|
94
|
+
* somebody writes in their own file afterwards.
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* Caches and weights the chosen template has no reason to mention.
|
|
98
|
+
*
|
|
99
|
+
* A github/gitignore template is written for a repository on that platform,
|
|
100
|
+
* not for every way somebody lays a project out. Xcode is the plain case:
|
|
101
|
+
* DerivedData normally sits in your home folder, so GitHub never mentions it,
|
|
102
|
+
* and a project configured to keep it alongside the source sent seven
|
|
103
|
+
* thousand files of build output that the old generic list had caught.
|
|
104
|
+
*
|
|
105
|
+
* Anchored, every one of them. `models/` unanchored is what used to reach
|
|
106
|
+
* into a Unity project's `Assets/Art/models`, which is the whole reason any
|
|
107
|
+
* of this was looked at.
|
|
108
|
+
*/
|
|
109
|
+
exports.ALSO_LEFT_OUT_HEADING = "# Also left out by CodeRook: caches and weights no template mentions.";
|
|
110
|
+
exports.ALSO_LEFT_OUT = `/DerivedData/
|
|
111
|
+
/models/
|
|
112
|
+
*.safetensors
|
|
113
|
+
*.ckpt
|
|
114
|
+
*.pt
|
|
115
|
+
*.pth`;
|
|
116
|
+
exports.KEPT_ANYWAY_HEADING = "# Kept even though the template leaves them out: not rebuildable from this folder.";
|
|
117
|
+
exports.KEPT_ANYWAY = `!/[Aa]ssets/**/*.meta
|
|
118
|
+
!/[Aa]ssets/**/*.unitypackage
|
|
119
|
+
!/[Aa]ssets/**/*.unitypackage.meta`;
|
|
120
|
+
exports.CODEROOK_TAIL_HEADING = "# Left out by CodeRook, whatever the project is.";
|
|
121
|
+
exports.CODEROOK_TAIL = `IndexedDB/
|
|
122
|
+
Local Storage/
|
|
123
|
+
Session Storage/
|
|
124
|
+
Service Worker/
|
|
125
|
+
Network/
|
|
126
|
+
GPUCache/
|
|
127
|
+
Code Cache/
|
|
128
|
+
blob_storage/
|
|
129
|
+
Local State
|
|
130
|
+
Preferences
|
|
131
|
+
|
|
132
|
+
# Archives of the project, inside the project
|
|
133
|
+
*.cbx`;
|
|
60
134
|
exports.STARTER_IGNORE = `${exports.STARTER_MARKER}
|
|
61
135
|
|
|
62
136
|
# Dependencies and generated output
|
|
@@ -177,9 +251,32 @@ function starterAuthored(text) {
|
|
|
177
251
|
* line on purpose, and re-adding it silently would be this application
|
|
178
252
|
* overruling them in their own file.
|
|
179
253
|
*/
|
|
180
|
-
function missingStarterRules(text) {
|
|
254
|
+
function missingStarterRules(text, reference = exports.STARTER_IGNORE) {
|
|
181
255
|
const present = new Set(ruleLines(text));
|
|
182
|
-
return ruleLines(
|
|
256
|
+
return ruleLines(reference).filter((line) => !present.has(line));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The rules this folder's own file is missing for the kind of project it is.
|
|
260
|
+
*
|
|
261
|
+
* The gap nothing could reach. A folder with a `.gitignore` of its own keeps
|
|
262
|
+
* it — that is right, and a real one must always win — but it also meant that
|
|
263
|
+
* every improvement to the rules could never arrive anywhere that needed
|
|
264
|
+
* them. A Unity project whose file had been written before the engine rules
|
|
265
|
+
* existed went on sending its whole import cache, and nothing said so.
|
|
266
|
+
*
|
|
267
|
+
* Reported rather than applied. A person may have deleted a line on purpose,
|
|
268
|
+
* and putting it back without asking is this application overruling them in
|
|
269
|
+
* their own file.
|
|
270
|
+
*/
|
|
271
|
+
async function missingEngineRules(root) {
|
|
272
|
+
const entries = await (0, promises_1.readdir)(root).catch(() => []);
|
|
273
|
+
const kind = (0, detect_js_1.templatesFor)(entries);
|
|
274
|
+
if (!kind.length)
|
|
275
|
+
return { kind, missing: [] };
|
|
276
|
+
const shared = await readIfPresent(node_path_1.default.join(root, exports.IGNORE_FILE));
|
|
277
|
+
if (shared === null)
|
|
278
|
+
return { kind, missing: [] };
|
|
279
|
+
return { kind, missing: missingStarterRules(shared, await starterFor(root)) };
|
|
183
280
|
}
|
|
184
281
|
/** The shared rules file, committed with the project. */
|
|
185
282
|
exports.IGNORE_FILE = ".gitignore";
|
|
@@ -231,6 +328,163 @@ function migrateRetiredRules(ignore, keep) {
|
|
|
231
328
|
}
|
|
232
329
|
return `${lines.join("\n")}\n`;
|
|
233
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* The rules a folder gets when it has none of its own.
|
|
333
|
+
*
|
|
334
|
+
* github/gitignore's templates have been carried in this application all
|
|
335
|
+
* along, and until now nothing chose one: every folder got the same generic
|
|
336
|
+
* starter. For a game project that is the wrong list. It carries `build/`,
|
|
337
|
+
* `dist/`, `out/` and `models/**` from the JavaScript and Python worlds, none
|
|
338
|
+
* of them anchored, so a folder like `Assets/Art/models` or
|
|
339
|
+
* `Assets/Plugins/Binaries` is matched by a rule that was written about
|
|
340
|
+
* somewhere else entirely — and the files inside it are quietly not sent.
|
|
341
|
+
*
|
|
342
|
+
* So the folder is asked what it is, and the template that knows about its
|
|
343
|
+
* layout is used. GitHub's Unity rules anchor every one of theirs — `/[Oo]bj/`
|
|
344
|
+
* rather than `obj/` — precisely so they cannot reach into `Assets`.
|
|
345
|
+
*
|
|
346
|
+
* The starter still follows, for the things no language template covers: a
|
|
347
|
+
* browser profile left in the folder, and CodeRook's own archives.
|
|
348
|
+
*/
|
|
349
|
+
/**
|
|
350
|
+
* Engines github/gitignore has no template for.
|
|
351
|
+
*
|
|
352
|
+
* Kept deliberately small and anchored. An unanchored `library/` would reach
|
|
353
|
+
* a folder of that name anywhere in the project, which is the mistake the
|
|
354
|
+
* generic starter was already making.
|
|
355
|
+
*/
|
|
356
|
+
const EXTRA_TEMPLATES = {
|
|
357
|
+
cocos: {
|
|
358
|
+
name: "Cocos",
|
|
359
|
+
body: `/[Ll]ibrary/
|
|
360
|
+
/[Tt]emp/
|
|
361
|
+
/[Bb]uild/
|
|
362
|
+
/.creator/`,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
async function starterFor(root, chosen) {
|
|
366
|
+
/*
|
|
367
|
+
What somebody said it is beats what the folder looks like.
|
|
368
|
+
`none` is a real answer — "this is not any of those" — and gets the plain
|
|
369
|
+
starter rather than being read as "no answer given".
|
|
370
|
+
*/
|
|
371
|
+
const entries = await (0, promises_1.readdir)(root).catch(() => []);
|
|
372
|
+
const keys = chosen ? (chosen === "none" ? [] : [chosen]) : (0, detect_js_1.templatesFor)(entries);
|
|
373
|
+
if (!keys.length)
|
|
374
|
+
return exports.STARTER_IGNORE;
|
|
375
|
+
const parts = [exports.STARTER_MARKER];
|
|
376
|
+
for (const key of keys) {
|
|
377
|
+
const template = ignore_templates_js_1.IGNORE_TEMPLATES[key] ?? EXTRA_TEMPLATES[key];
|
|
378
|
+
if (!template)
|
|
379
|
+
continue;
|
|
380
|
+
const from = ignore_templates_js_1.IGNORE_TEMPLATES[key] ? ", from github/gitignore" : "";
|
|
381
|
+
parts.push("", `# ${template.name}${from}`, template.body.trimEnd());
|
|
382
|
+
}
|
|
383
|
+
parts.push("", exports.ALSO_LEFT_OUT_HEADING, exports.ALSO_LEFT_OUT);
|
|
384
|
+
parts.push("", exports.KEPT_ANYWAY_HEADING, exports.KEPT_ANYWAY);
|
|
385
|
+
parts.push("", exports.CODEROOK_TAIL_HEADING, exports.CODEROOK_TAIL);
|
|
386
|
+
return `${parts.join("\n")}\n`;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Move one path onto the ignore list, or onto the keep list.
|
|
390
|
+
*
|
|
391
|
+
* Both are the same act — writing a line into the project's own rules — which
|
|
392
|
+
* is why they are one function. Keeping means a `!` rule, and a `!` rule only
|
|
393
|
+
* means anything if it comes after whatever would have taken the file, so the
|
|
394
|
+
* line goes at the end under its own heading.
|
|
395
|
+
*
|
|
396
|
+
* Lifted out of the window's event handler so it can be tested. It could not
|
|
397
|
+
* be before, which is how a folder came to be written as a rule that also
|
|
398
|
+
* matched a file of the same name.
|
|
399
|
+
*/
|
|
400
|
+
async function moveRule(root, file, keep) {
|
|
401
|
+
/*
|
|
402
|
+
The rules travel with the project. A .gitignore that excluded itself would
|
|
403
|
+
never reach the website, the command line, or another machine.
|
|
404
|
+
*/
|
|
405
|
+
if (file === exports.IGNORE_FILE) {
|
|
406
|
+
throw new Error("The ignore rules have to travel with the project");
|
|
407
|
+
}
|
|
408
|
+
const rules = await readRules(root);
|
|
409
|
+
/*
|
|
410
|
+
A folder is written as a folder rule.
|
|
411
|
+
|
|
412
|
+
The path arrives without a trailing slash, and writing it as-is makes a
|
|
413
|
+
rule that also matches a *file* of that name — not what anybody meant by
|
|
414
|
+
excluding a directory, and a disagreement with what the command line
|
|
415
|
+
prints for the same folder. Any slash already there is taken off first, so
|
|
416
|
+
a caller passing a rule rather than a path cannot produce `name//`.
|
|
417
|
+
*/
|
|
418
|
+
const bare = file.replace(/\/+$/, "");
|
|
419
|
+
const directory = await (0, promises_1.stat)(node_path_1.default.join(root, bare))
|
|
420
|
+
.then((entry) => entry.isDirectory())
|
|
421
|
+
.catch(() => file.endsWith("/"));
|
|
422
|
+
const target = directory ? `${bare}/` : bare;
|
|
423
|
+
const line = keep ? `!${target}` : target;
|
|
424
|
+
/* Saying it twice changes nothing and makes the file harder to read. */
|
|
425
|
+
const already = ruleLines(rules.shared).some((one) => one === line);
|
|
426
|
+
if (already)
|
|
427
|
+
return rules;
|
|
428
|
+
const heading = keep ? exports.KEEP_HEADING : exports.IGNORE_HEADING;
|
|
429
|
+
let body = rules.shared.replace(/\s*$/, "");
|
|
430
|
+
/*
|
|
431
|
+
A keep inside an excluded folder needs the folder opened first.
|
|
432
|
+
|
|
433
|
+
Rules cannot re-include a file whose directory is excluded — nothing ever
|
|
434
|
+
looks inside a directory it has already decided to skip, so `!Library/x`
|
|
435
|
+
under `Library/` does precisely nothing. That is most of what this button
|
|
436
|
+
is for: rescuing one file out of a cache.
|
|
437
|
+
|
|
438
|
+
The way through is the documented one. `Library/` becomes `Library/**`,
|
|
439
|
+
which leaves the directory itself includable, and every directory on the
|
|
440
|
+
way down to the file is named back in. Written here rather than asked of
|
|
441
|
+
the person, because the alternative is a button that looks like it worked.
|
|
442
|
+
*/
|
|
443
|
+
if (keep) {
|
|
444
|
+
/*
|
|
445
|
+
Which rule is in the way is a question for the matcher, not for string
|
|
446
|
+
comparison. The line is `/[Ll]ibrary/` — a pattern — and no amount of
|
|
447
|
+
comparing it against `Library/PackageCache/...` will ever match, which
|
|
448
|
+
is exactly how the first version of this wrote a negation that did
|
|
449
|
+
nothing at all.
|
|
450
|
+
*/
|
|
451
|
+
const parts = target.split("/");
|
|
452
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
453
|
+
const ancestor = parts.slice(0, depth).join("/");
|
|
454
|
+
const layers = [{ base: "", rules: (0, rules_js_1.parseRules)(body) }];
|
|
455
|
+
const blocking = (0, rules_js_1.decide)(ancestor, true, layers);
|
|
456
|
+
if (!blocking || blocking.negated || !blocking.directoryOnly)
|
|
457
|
+
continue;
|
|
458
|
+
/*
|
|
459
|
+
Replaced line by line rather than by pattern. The thing being found
|
|
460
|
+
is itself a pattern — `/[Ll]ibrary/` — so building a regular
|
|
461
|
+
expression out of it means escaping a string that is mostly regular
|
|
462
|
+
expression already, for no gain over comparing the text.
|
|
463
|
+
*/
|
|
464
|
+
const was = (blocking.anchored ? "/" : "") + blocking.pattern + "/";
|
|
465
|
+
const opened = (blocking.anchored ? "/" : "") + blocking.pattern + "/**";
|
|
466
|
+
body = body
|
|
467
|
+
.split(NEWLINE)
|
|
468
|
+
.map((one) => (one.trim() === was ? opened : one))
|
|
469
|
+
.join(NEWLINE);
|
|
470
|
+
/* Then name every directory between it and the file back in. */
|
|
471
|
+
for (let at = depth; at < parts.length - 1; at += 1) {
|
|
472
|
+
const open = "!" + parts.slice(0, at + 1).join("/") + "/";
|
|
473
|
+
if (!ruleLines(body).includes(open))
|
|
474
|
+
body += NEWLINE + open;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const shared = body.includes(heading)
|
|
479
|
+
? body + NEWLINE + line + NEWLINE
|
|
480
|
+
: body + NEWLINE + NEWLINE + heading + NEWLINE + line + NEWLINE;
|
|
481
|
+
const next = { ...rules, shared };
|
|
482
|
+
await writeRules(root, next);
|
|
483
|
+
return next;
|
|
484
|
+
}
|
|
485
|
+
/** The two headings a move writes under, so a person can find their own edits. */
|
|
486
|
+
exports.KEEP_HEADING = "# Always include";
|
|
487
|
+
exports.IGNORE_HEADING = "# Left out from the filter screen";
|
|
234
488
|
async function readRules(root) {
|
|
235
489
|
const shared = await readIfPresent(node_path_1.default.join(root, exports.IGNORE_FILE));
|
|
236
490
|
const local = await readIfPresent(node_path_1.default.join(root, exports.LOCAL_EXCLUDE_FILE));
|
|
@@ -239,7 +493,7 @@ async function readRules(root) {
|
|
|
239
493
|
// No .gitignore yet. Anything an earlier build wrote is carried over, so a
|
|
240
494
|
// project that was already set up does not silently lose its rules.
|
|
241
495
|
const migrated = migrateRetiredRules(await readIfPresent(node_path_1.default.join(root, RETIRED_IGNORE)), await readIfPresent(node_path_1.default.join(root, RETIRED_KEEP)));
|
|
242
|
-
return { shared: migrated ??
|
|
496
|
+
return { shared: migrated ?? (await starterFor(root)), local: local ?? "" };
|
|
243
497
|
}
|
|
244
498
|
/** Write the rules where the project, the CLI and git all expect them. */
|
|
245
499
|
async function writeRules(root, rules) {
|
|
@@ -893,7 +1147,14 @@ async function evaluateRules(root, rules, options = {}) {
|
|
|
893
1147
|
// The last rule to speak wins, across every .gitignore that governs
|
|
894
1148
|
// this path. A `!` rule brings the file back and is worth reporting,
|
|
895
1149
|
// because it is how "always include" is expressed.
|
|
896
|
-
|
|
1150
|
+
/*
|
|
1151
|
+
The directories above it count. Asking `decide` about the file alone
|
|
1152
|
+
is what made this screen report a Unity project's whole Library as
|
|
1153
|
+
going up: `/[Ll]ibrary/` speaks about the directory, so no rule
|
|
1154
|
+
matched the file and every one of them looked included — while the
|
|
1155
|
+
save, which has always walked the ancestors, left them all behind.
|
|
1156
|
+
*/
|
|
1157
|
+
const rule = (0, rules_js_1.decidingRule)(relative, false, layers);
|
|
897
1158
|
if (rule?.negated) {
|
|
898
1159
|
keptFiles += 1;
|
|
899
1160
|
keptBytes += size;
|