@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +40 -0
- package/dist/cbx/src/core/download.js +370 -96
- package/dist/cbx/src/core/rules.js +65 -4
- package/dist/cbx/src/core/worktree.js +37 -4
- package/dist/cli/src/cli.js +94 -8
- package/dist/cli/src/mods_command.js +518 -0
- package/dist/cli/src/network.js +11 -0
- package/dist/cli/src/zip_writer.js +188 -0
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +26 -1
|
@@ -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 (
|
|
177
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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",
|