@coderook/cli 0.34.10 → 0.35.0

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.
@@ -170,11 +170,70 @@ function ruleMatches(relativePath, isDirectory, rule) {
170
170
  }
171
171
  return relativePath.split("/").some((segment) => expression.test(segment));
172
172
  }
173
+ const NONE = [];
174
+ const indexes = new WeakMap();
175
+ function indexFor(rules) {
176
+ let index = indexes.get(rules);
177
+ if (index)
178
+ return index;
179
+ const kinds = rules.map((rule) => {
180
+ if (rule.directoryOnly) {
181
+ const bare = trailingSlashOff(rule.pattern);
182
+ return rule.anchored || bare.includes("/") ? "wholePath" : "ownName";
183
+ }
184
+ return rule.anchored || rule.pattern.includes("/") ? "wholePath" : "segment";
185
+ });
186
+ const named = kinds.flatMap((kind, at) => (kind === "wholePath" ? [] : [at]));
187
+ index = { kinds, named, answers: new Map() };
188
+ indexes.set(rules, index);
189
+ return index;
190
+ }
191
+ function namedMatches(index, rules, name) {
192
+ let found = index.answers.get(name);
193
+ if (found)
194
+ return found;
195
+ found = NONE;
196
+ for (const at of index.named) {
197
+ if (matcher(rules[at].pattern).test(name)) {
198
+ if (found === NONE)
199
+ found = [];
200
+ found.push(at);
201
+ }
202
+ }
203
+ index.answers.set(name, found);
204
+ return found;
205
+ }
173
206
  /** The last matching rule wins, as it does in gitignore. */
174
207
  function lastMatchingRule(relativePath, isDirectory, rules) {
208
+ if (!rules.length)
209
+ return null;
210
+ const index = indexFor(rules);
211
+ const segments = relativePath.split("/");
212
+ /* Which named rules some segment satisfies, and which the last one does. */
213
+ const anySegment = new Uint8Array(rules.length);
214
+ const ownName = new Uint8Array(rules.length);
215
+ for (let at = 0; at < segments.length; at += 1) {
216
+ const hits = namedMatches(index, rules, segments[at]);
217
+ for (const hit of hits)
218
+ anySegment[hit] = 1;
219
+ if (at === segments.length - 1)
220
+ for (const hit of hits)
221
+ ownName[hit] = 1;
222
+ }
175
223
  let found = null;
176
- for (const rule of rules) {
177
- if (ruleMatches(relativePath, isDirectory, rule))
224
+ for (let at = 0; at < rules.length; at += 1) {
225
+ const rule = rules[at];
226
+ const kind = index.kinds[at];
227
+ let matched;
228
+ if (kind === "segment")
229
+ matched = anySegment[at] === 1;
230
+ else if (kind === "ownName")
231
+ matched = isDirectory && ownName[at] === 1;
232
+ else if (rule.directoryOnly && !isDirectory)
233
+ matched = false;
234
+ else
235
+ matched = matcher(rule.pattern).test(relativePath);
236
+ if (matched)
178
237
  found = rule;
179
238
  }
180
239
  return found;
@@ -223,8 +282,10 @@ function decidingRule(relativePath, isDirectory, layers, remembered) {
223
282
  }
224
283
  return decide(relativePath, isDirectory, layers);
225
284
  }
226
- function excludes(relativePath, isDirectory, layers) {
227
- const rule = decidingRule(relativePath, isDirectory, layers);
285
+ function excludes(relativePath, isDirectory, layers,
286
+ /* Folder answers to reuse; see FolderAnswers. Only for one fixed set of rules. */
287
+ remembered) {
288
+ const rule = decidingRule(relativePath, isDirectory, layers, remembered);
228
289
  return Boolean(rule && !rule.negated);
229
290
  }
230
291
  /**
@@ -1110,6 +1110,12 @@ onFound) {
1110
1110
  const layers = await collectLayers(root, rules);
1111
1111
  const tracked = await gitTracked(root);
1112
1112
  const found = [];
1113
+ /*
1114
+ Every entry asks about each folder above it; those answers do not change
1115
+ during one walk, so each folder is decided once rather than once per file
1116
+ beneath it.
1117
+ */
1118
+ const folderAnswers = new Map();
1113
1119
  /*
1114
1120
  A level at a time, eight in flight.
1115
1121
 
@@ -1142,7 +1148,7 @@ onFound) {
1142
1148
  continue;
1143
1149
  const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
1144
1150
  if (entry.isDirectory()) {
1145
- if ((0, rules_js_1.excludes)(relative, true, layers) &&
1151
+ if ((0, rules_js_1.excludes)(relative, true, layers, folderAnswers) &&
1146
1152
  !(0, rules_js_1.negationReachesInto)(relative, layers) &&
1147
1153
  !tracked?.folders.has(relative)) {
1148
1154
  continue;
@@ -1152,7 +1158,7 @@ onFound) {
1152
1158
  }
1153
1159
  if (!entry.isFile())
1154
1160
  continue;
1155
- if ((0, rules_js_1.excludes)(relative, false, layers) && !tracked?.files.has(relative))
1161
+ if ((0, rules_js_1.excludes)(relative, false, layers, folderAnswers) && !tracked?.files.has(relative))
1156
1162
  continue;
1157
1163
  here.push({ full, relative });
1158
1164
  }
@@ -1608,11 +1614,30 @@ const SECRET_SUFFIXES = [".pem", ".key", ".p12", ".pfx", ".keystore", ".jks"];
1608
1614
  the answer be a rule — one line in .gitignore covers the lot, where a list
1609
1615
  of the files inside it would go stale the moment the program ran again.
1610
1616
  */
1617
+ /** Chromium's disk cache: an index beside data_N blocks, Cache_Data, or the js/wasm code cache. */
1618
+ async function looksLikeBrowserCache(directory) {
1619
+ const names = new Set((await (0, promises_1.readdir)(directory).catch(() => [])).map((name) => name.toLowerCase()));
1620
+ if (names.has("cache_data") || names.has("index-dir"))
1621
+ return true;
1622
+ if (names.has("index") && names.has("data_0"))
1623
+ return true;
1624
+ return names.has("js") && names.has("wasm");
1625
+ }
1611
1626
  const PRIVATE_DIRECTORIES = [
1612
1627
  {
1613
- match: /^(Local Storage|Session Storage|IndexedDB|Service Worker|Cache|Code Cache|GPUCache)$/i,
1628
+ match: /^(Local Storage|Session Storage|IndexedDB|Service Worker)$/i,
1614
1629
  because: "a browser profile, which holds the sites you are signed in to",
1615
1630
  },
1631
+ /*
1632
+ "Cache" is also an ordinary name for source: FreeType keeps ftcache.c in
1633
+ `src/cache`, and a Godot checkout was refused as "a browser profile" for
1634
+ it. So these three only count when what is inside is a browser's cache.
1635
+ */
1636
+ {
1637
+ match: /^(Cache|Code Cache|GPUCache)$/i,
1638
+ because: "a browser profile, which holds the sites you are signed in to",
1639
+ inside: looksLikeBrowserCache,
1640
+ },
1616
1641
  { match: /^\.wrangler$/i, because: "local service state, including emulated databases" },
1617
1642
  { match: /^\.aws$/i, because: "cloud credentials" },
1618
1643
  { match: /^\.ssh$/i, because: "SSH keys" },
@@ -1661,7 +1686,15 @@ async function detectPrivateDirectories(root) {
1661
1686
  continue;
1662
1687
  const full = node_path_1.default.join(directory, entry.name);
1663
1688
  const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
1664
- const rule = PRIVATE_DIRECTORIES.find((candidate) => candidate.match.test(entry.name));
1689
+ let rule;
1690
+ for (const candidate of PRIVATE_DIRECTORIES) {
1691
+ if (!candidate.match.test(entry.name))
1692
+ continue;
1693
+ if (candidate.inside && !(await candidate.inside(full)))
1694
+ continue;
1695
+ rule = candidate;
1696
+ break;
1697
+ }
1665
1698
  if (rule) {
1666
1699
  found.push({
1667
1700
  path: relative,
@@ -31,6 +31,7 @@ const attach_command_js_1 = require("./attach_command.js");
31
31
  const build_command_js_1 = require("./build_command.js");
32
32
  const node_check_js_1 = require("./node_check.js");
33
33
  const pages_command_js_1 = require("./pages_command.js");
34
+ const mods_command_js_1 = require("./mods_command.js");
34
35
  const project_commands_js_1 = require("./project_commands.js");
35
36
  const track_commands_js_1 = require("./track_commands.js");
36
37
  const tracks_js_1 = require("../../cbx/src/core/tracks.js");
@@ -1656,14 +1657,33 @@ trackName) {
1656
1657
  });
1657
1658
  }
1658
1659
  const line = progressLine();
1659
- const result = await downloader.run(repositoryId, latest.id, destination, (progress) => {
1660
- line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
1661
- `${progress.files}/${progress.totalFiles} ` +
1662
- dim(progress.path.slice(-40)));
1663
- },
1664
- // What this folder received last time, so a file the project has since
1665
- // dropped is removed while everything untracked is left alone.
1666
- held, mode);
1660
+ let result;
1661
+ try {
1662
+ result = await downloader.run(repositoryId, latest.id, destination, (progress) => {
1663
+ line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
1664
+ `${progress.files}/${progress.totalFiles} ` +
1665
+ dim(progress.path.slice(-40)));
1666
+ },
1667
+ // What this folder received last time, so a file the project has since
1668
+ // dropped is removed while everything untracked is left alone.
1669
+ held, mode);
1670
+ }
1671
+ catch (error) {
1672
+ done(line);
1673
+ if (!(error instanceof download_js_1.DownloadFailed))
1674
+ throw error;
1675
+ /*
1676
+ Named, and with what it did not do. A transfer fails before anything
1677
+ is put in place, so the folder is as it was and the staging copy is
1678
+ gone — which is what someone deciding whether to run it again needs.
1679
+ */
1680
+ console.error(red(error.message));
1681
+ console.error(dim(((0, node_fs_1.existsSync)(destination)
1682
+ ? `Nothing in ${destination} was changed.`
1683
+ : `No folder was made at ${destination}.`) +
1684
+ " Run the same command again to retry."));
1685
+ return 1;
1686
+ }
1667
1687
  done(line);
1668
1688
  /*
1669
1689
  Said, not hidden. The files are right, but the service sent them still
@@ -2773,6 +2793,72 @@ const SPECS = [
2773
2793
  ],
2774
2794
  run: pages_command_js_1.commandPages,
2775
2795
  },
2796
+ {
2797
+ name: "mods",
2798
+ group: "Your projects",
2799
+ summary: "mod repositories for games: set one up, publish to one",
2800
+ usage: "mods <create|repos|info|settings|builds|list|publish|remove|block|reports|delete> …",
2801
+ detail: "A mod repository is where a game's players get mods from. Its owner\n" +
2802
+ "sets the rules — which file types a mod may hold, archives opened and\n" +
2803
+ "checked too, the manifest each upload carries, size limits, and whether\n" +
2804
+ "anybody or only the owner may upload. Modders publish with their own\n" +
2805
+ "CodeRook accounts; nothing is listed until it passes the rules and a\n" +
2806
+ "malware scan, and anything that runs (.exe, .dll, scripts) is refused\n" +
2807
+ "in every repository. The game's client reads a catalogue signed with\n" +
2808
+ "the repository's own key: see coderook.com/docs#mod-repositories.\n\n" +
2809
+ " create <repo> --name \"Game\" set one up; prints its address and public key\n" +
2810
+ " repos the repositories you run\n" +
2811
+ " info <repo> a repository's rules, for anybody\n" +
2812
+ " settings <repo> [rules…] change the rules (owner)\n" +
2813
+ " builds <repo> game builds allowed to read it (owner)\n" +
2814
+ " publish <folder|zip> --repo <repo> upload a mod; waits for its checks\n" +
2815
+ " list <repo> [--all] your mods and how their checks went\n" +
2816
+ " remove <mod> --repo <repo> take a mod down\n" +
2817
+ " block <username> --repo <repo> stop an uploader (owner)\n" +
2818
+ " reports <repo> what players reported (owner)\n" +
2819
+ " delete <repo> delete a repository and everything in it",
2820
+ options: [
2821
+ { flags: "--name <text>", description: "create/settings: the game's name" },
2822
+ { flags: "--allow <exts>", description: "create/settings: file types a mod may hold, e.g. .png,.json" },
2823
+ { flags: "--nested <exts>", description: "create/settings: archives inside a mod that are opened and checked too" },
2824
+ { flags: "--manifest <file|none>", description: "create/settings: the manifest every part carries (default mod.json)" },
2825
+ { flags: "--fields <list>", description: "create/settings: manifest fields that must match: id,name,folder,version" },
2826
+ { flags: "--uploads <anyone|owner>", description: "create/settings: who may upload" },
2827
+ { flags: "--max-part <size>", description: "create/settings: largest part, e.g. 2GB" },
2828
+ { flags: "--register <file>", description: "builds: register the build secret in this file (never printed)" },
2829
+ { flags: "--new", description: "builds: have the service make a build secret (shown once)" },
2830
+ { flags: "--off <id>", description: "builds: turn a build off" },
2831
+ { flags: "--repo <repo>", description: "publish/list/remove/block: which repository" },
2832
+ { flags: "--part <id>", description: "publish: which part of the mod this is (default main)" },
2833
+ { flags: "--id <mod>", description: "publish: the mod's id (default: manifest, then folder name)" },
2834
+ { flags: "--version <v>", description: "publish: the mod's version" },
2835
+ { flags: "--folder <name>", description: "publish: the folder the game installs it into" },
2836
+ { flags: "--part-name <text>", description: "publish: a readable name for the part" },
2837
+ { flags: "--about <text>", description: "create/settings/publish: a description" },
2838
+ { flags: "--notice <text>", description: "publish: a notice shown with the mod (credits, licences)" },
2839
+ { flags: "--block <exts>", description: "create/settings: more file types to refuse" },
2840
+ { flags: "--max-files <n>", description: "create/settings: most files in a part" },
2841
+ { flags: "--max-ratio <n>", description: "create/settings: how many times its size a part may unpack to" },
2842
+ { flags: "--max-parts <n>", description: "create/settings: most parts in a mod" },
2843
+ { flags: "--expires <date>", description: "builds: when a registered build stops working" },
2844
+ { flags: "--all", description: "list: every mod in the repository (owner)" },
2845
+ { flags: "--as-owner", description: "remove: as the repository's owner, any mod" },
2846
+ { flags: "--disable", description: "settings: stop serving and taking uploads" },
2847
+ { flags: "--enable", description: "settings: start again" },
2848
+ { flags: "--undo", description: "block: let the uploader back in" },
2849
+ { flags: "-y, --yes", description: "remove/delete: without asking" },
2850
+ { flags: "--no-wait", description: "publish: do not wait for the checks" },
2851
+ { flags: "--take-down", description: "block: also hide the uploader's mods" },
2852
+ ],
2853
+ examples: [
2854
+ 'cbx mods create my-game --name "My Game" --allow .png,.ogg,.json --nested .pak',
2855
+ "cbx mods builds my-game --register release/build-secret.bin",
2856
+ "cbx mods publish ./mods/BetterTrees --repo my-game",
2857
+ "cbx mods list my-game",
2858
+ "cbx mods publish ./HD-Art --repo zt1 --part animals",
2859
+ ],
2860
+ run: mods_command_js_1.commandMods,
2861
+ },
2776
2862
  {
2777
2863
  name: "issues",
2778
2864
  group: "Your projects",