@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.
@@ -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(exports.STARTER_IGNORE).filter((line) => !present.has(line));
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 ?? exports.STARTER_IGNORE, local: local ?? "" };
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
- const rule = (0, rules_js_1.decide)(relative, false, layers);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.29.5",
3
+ "version": "0.30.1",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",