@coderook/cli 0.29.3 → 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.
@@ -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.3",
5
+ "version": "0.29.4",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -645,7 +645,21 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
645
645
  file should be sent and the fix is to take the key out of it.
646
646
  */
647
647
  const pasted = await (0, worktree_js_1.detectPastedCredentials)(folder, files.map((file) => file.path));
648
- 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
+ }
649
663
  if (stillPasted.length) {
650
664
  console.log(red(`
651
665
  ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
@@ -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
- return [...totals.entries()]
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
- .sort((left, right) => right.bytes - left.bytes);
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.
@@ -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.STARTER_IGNORE = `# Dependencies and generated output
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
- return found;
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.29.3",
3
+ "version": "0.29.4",
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",