@coderook/cli 0.30.0 → 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.
@@ -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.30.0",
5
+ "version": "0.30.1",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -60,24 +60,24 @@ const RULES = [
60
60
  project where the question "what should I leave out?" has the largest
61
61
  possible answer was the one the detector had nothing to say about.
62
62
  */
63
- { directory: "Library", reason: "Unity's import cache — rebuilt when the project next opens", recommended: true },
64
- { directory: "Temp", reason: "Unity's scratch folder for the running editor", recommended: true },
65
- { directory: "MemoryCaptures", reason: "Unity memory snapshots — large, and not the project", recommended: true },
66
- { directory: "Recordings", reason: "Unity recorder output", recommended: true },
67
- { directory: "UserSettings", reason: "Your own editor layout — not part of the project", recommended: true },
68
- { directory: "DerivedDataCache", reason: "Unreal's derived data cache — rebuilt on demand", recommended: true },
69
- { directory: "Intermediate", reason: "Unreal build intermediates — rebuilt when you build", recommended: true },
70
- { directory: "Binaries", reason: "Unreal compiled output — rebuilt when you build", recommended: true },
63
+ { directory: "Library", atRoot: true, reason: "Unity's import cache — rebuilt when the project next opens", recommended: true },
64
+ { directory: "Temp", atRoot: true, reason: "Unity's scratch folder for the running editor", recommended: true },
65
+ { directory: "MemoryCaptures", atRoot: true, reason: "Unity memory snapshots — large, and not the project", recommended: true },
66
+ { directory: "Recordings", atRoot: true, reason: "Unity recorder output", recommended: true },
67
+ { directory: "UserSettings", atRoot: true, reason: "Your own editor layout — not part of the project", recommended: true },
68
+ { directory: "DerivedDataCache", atRoot: true, reason: "Unreal's derived data cache — rebuilt on demand", recommended: true },
69
+ { directory: "Intermediate", atRoot: true, reason: "Unreal build intermediates — rebuilt when you build", recommended: true },
70
+ { directory: "Binaries", atRoot: true, reason: "Unreal compiled output — rebuilt when you build", recommended: true },
71
71
  /*
72
72
  Named like the work, and it is not: Unreal keeps logs, crash reports and
73
73
  autosaves here. Offered rather than ticked, because a folder called
74
74
  "Saved" is the one nobody should have excluded on our say-so.
75
75
  */
76
- { directory: "Saved", reason: "Unreal logs and autosaves — check before excluding", recommended: false },
77
- { directory: ".godot", reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
78
- { directory: ".import", reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
79
- { directory: "Logs", reason: "Editor logs", recommended: true },
80
- { directory: "obj", reason: "Compiler intermediates — rebuilt when you build", recommended: true },
76
+ { directory: "Saved", atRoot: true, reason: "Unreal logs and autosaves — check before excluding", recommended: false },
77
+ { directory: ".godot", atRoot: true, reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
78
+ { directory: ".import", atRoot: true, reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
79
+ { directory: "Logs", atRoot: true, reason: "Editor logs", recommended: true },
80
+ { directory: "obj", atRoot: true, reason: "Compiler intermediates — rebuilt when you build", recommended: true },
81
81
  { directory: ".vs", reason: "Visual Studio's local cache", recommended: true },
82
82
  /*
83
83
  Offered, not ticked. JetBrains keeps run configurations in here that some
@@ -104,7 +104,7 @@ const RULES = [
104
104
  { directory: "dist", reason: "Usually build output — check before excluding", recommended: false },
105
105
  { directory: "build", reason: "Usually build output — check before excluding", recommended: false },
106
106
  { directory: "out", reason: "Usually build output — check before excluding", recommended: false },
107
- { directory: "bin", reason: "Usually build output — check before excluding", recommended: false },
107
+ { directory: "bin", atRoot: true, reason: "Usually build output — check before excluding", recommended: false },
108
108
  { extension: ".safetensors", reason: "Model weights — large, and usually downloadable", recommended: false },
109
109
  { extension: ".ckpt", reason: "Model weights — large, and usually downloadable", recommended: false },
110
110
  { extension: ".pt", reason: "Model weights — large, and usually downloadable", recommended: false },
@@ -222,11 +222,17 @@ function suggestExclusions(files) {
222
222
  gets written matching the folder that is there.
223
223
  */
224
224
  const wanted = rule.directory.toLowerCase();
225
- const found = segments
226
- .slice(0, -1)
227
- .find((segment) => segment.toLowerCase() === wanted);
225
+ /*
226
+ At the root, where the rule is about the project's own output, and
227
+ written anchored so it stays there. Unanchored, this offered to
228
+ leave out `Assets/Props/obj` — a folder of 3D models — because a
229
+ C# build also writes to a folder called obj. The file it proposed
230
+ removing was the one the rules had just been fixed to protect.
231
+ */
232
+ const within = rule.atRoot ? segments.slice(0, 1) : segments.slice(0, -1);
233
+ const found = within.find((segment) => segment.toLowerCase() === wanted);
228
234
  if (found)
229
- pattern = `${found}/`;
235
+ pattern = rule.atRoot ? `/${found}/` : `${found}/`;
230
236
  }
231
237
  if (!pattern && rule.file && name === rule.file) {
232
238
  pattern = rule.file;
@@ -132,17 +132,36 @@ function matcher(pattern) {
132
132
  }
133
133
  return expression;
134
134
  }
135
+ /** A directory pattern with or without its trailing slash is the same rule. */
136
+ function trailingSlashOff(pattern) {
137
+ let end = pattern.length;
138
+ while (end > 0 && pattern[end - 1] === "/")
139
+ end -= 1;
140
+ return pattern.slice(0, end);
141
+ }
135
142
  function ruleMatches(relativePath, isDirectory, rule) {
136
143
  const expression = matcher(rule.pattern);
137
144
  if (rule.directoryOnly) {
138
- if (rule.anchored || rule.pattern.includes("/")) {
139
- return (expression.test(relativePath) ||
140
- relativePath.startsWith(`${rule.pattern.replace(/\/+$/, "")}/`));
141
- }
142
- const segments = relativePath.split("/");
143
- // A file inside an ignored directory is matched by its parent segments.
144
- const scope = isDirectory ? segments : segments.slice(0, -1);
145
- return scope.some((segment) => expression.test(segment));
145
+ /*
146
+ A rule written `dir/` speaks about directories and about nothing else.
147
+ Everything under an excluded directory goes too, but that is git
148
+ declining to look inside it rather than the pattern reaching down, and
149
+ the difference shows the moment a later rule re-includes the directory:
150
+ `build/` then `!build/` leaves `build/keep.txt` travelling, which is the
151
+ escape hatch people are told to use. It shows again in reverse, which is
152
+ how this was found — asking `!Library/PackageCache/a.package/` about a
153
+ file let the whole cache out of a folder only meant to be opened.
154
+
155
+ So the pattern answers only for directories, and a file's ancestors are
156
+ asked separately, outermost in, by `decidingRule`.
157
+ */
158
+ if (!isDirectory)
159
+ return false;
160
+ const bare = trailingSlashOff(rule.pattern);
161
+ if (rule.anchored || bare.includes("/"))
162
+ return expression.test(relativePath);
163
+ const own = relativePath.split("/").pop() ?? relativePath;
164
+ return expression.test(own);
146
165
  }
147
166
  if (rule.anchored || rule.pattern.includes("/")) {
148
167
  return expression.test(relativePath);
@@ -3,13 +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.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;
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
9
  exports.missingEngineRules = missingEngineRules;
10
10
  exports.isRepository = isRepository;
11
11
  exports.migrateRetiredRules = migrateRetiredRules;
12
12
  exports.starterFor = starterFor;
13
+ exports.moveRule = moveRule;
13
14
  exports.readRules = readRules;
14
15
  exports.writeRules = writeRules;
15
16
  exports.collectLayers = collectLayers;
@@ -38,6 +39,8 @@ const ignore_templates_js_1 = require("./ignore_templates.js");
38
39
  const rules_js_1 = require("./rules.js");
39
40
  const secret_patterns_js_1 = require("./secret_patterns.js");
40
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);
41
44
  /** Past this the evaluation reports truncated rather than walking forever. */
42
45
  exports.EVALUATION_FILE_LIMIT = 300_000;
43
46
  async function git(root, ...args) {
@@ -359,9 +362,14 @@ const EXTRA_TEMPLATES = {
359
362
  /.creator/`,
360
363
  },
361
364
  };
362
- async function starterFor(root) {
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
+ */
363
371
  const entries = await (0, promises_1.readdir)(root).catch(() => []);
364
- const keys = (0, detect_js_1.templatesFor)(entries);
372
+ const keys = chosen ? (chosen === "none" ? [] : [chosen]) : (0, detect_js_1.templatesFor)(entries);
365
373
  if (!keys.length)
366
374
  return exports.STARTER_IGNORE;
367
375
  const parts = [exports.STARTER_MARKER];
@@ -377,6 +385,106 @@ async function starterFor(root) {
377
385
  parts.push("", exports.CODEROOK_TAIL_HEADING, exports.CODEROOK_TAIL);
378
386
  return `${parts.join("\n")}\n`;
379
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";
380
488
  async function readRules(root) {
381
489
  const shared = await readIfPresent(node_path_1.default.join(root, exports.IGNORE_FILE));
382
490
  const local = await readIfPresent(node_path_1.default.join(root, exports.LOCAL_EXCLUDE_FILE));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.30.0",
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",