@coderook/cli 0.31.12 → 0.32.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.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +73 -1
  3. package/dist/cbx/LICENSE +202 -0
  4. package/dist/cbx/NOTICE +8 -0
  5. package/dist/{desktop-app/src/main → cbx/src/core}/cbx.js +31 -0
  6. package/dist/{desktop-app/src/main → cbx/src/core}/download.js +62 -1
  7. package/dist/{desktop-app/src/main → cbx/src/core}/rules.js +2 -0
  8. package/dist/cbx/src/local/errors.js +23 -0
  9. package/dist/cbx/src/local/history.js +522 -0
  10. package/dist/cbx/src/local/merge.js +277 -0
  11. package/dist/cbx/src/local/merge_text.js +148 -0
  12. package/dist/cbx/src/local/remote.js +437 -0
  13. package/dist/cbx/src/local/repository.js +324 -0
  14. package/dist/cbx/src/local/snapshot.js +338 -0
  15. package/dist/cbx/src/local/store.js +258 -0
  16. package/dist/{desktop-app → cbx}/src/shared/safe_path.js +12 -0
  17. package/dist/cli/src/api.js +114 -2
  18. package/dist/cli/src/cli.js +198 -22
  19. package/dist/cli/src/diff_command.js +2 -2
  20. package/dist/cli/src/git_remote.js +6 -6
  21. package/dist/cli/src/licence_commands.js +3 -3
  22. package/dist/cli/src/local_commands.js +506 -0
  23. package/dist/cli/src/mcp.js +2 -2
  24. package/dist/cli/src/pages_command.js +625 -0
  25. package/dist/cli/src/project_commands.js +1 -0
  26. package/dist/cli/src/registry.js +1 -0
  27. package/dist/cli/src/runner.js +2 -2
  28. package/dist/cli/src/track_commands.js +1 -1
  29. package/package.json +2 -2
  30. package/skills/coderook/SKILL.md +41 -0
  31. /package/dist/{desktop-app/src/main → cbx/src/core}/compress.js +0 -0
  32. /package/dist/{desktop-app/src/main → cbx/src/core}/credentials.js +0 -0
  33. /package/dist/{desktop-app/src/main → cbx/src/core}/detect.js +0 -0
  34. /package/dist/{desktop-app/src/main → cbx/src/core}/faults.js +0 -0
  35. /package/dist/{desktop-app/src/main → cbx/src/core}/identify.js +0 -0
  36. /package/dist/{desktop-app/src/main → cbx/src/core}/ignore_templates.js +0 -0
  37. /package/dist/{desktop-app/src/main → cbx/src/core}/licence_texts.js +0 -0
  38. /package/dist/{desktop-app/src/main → cbx/src/core}/licences.js +0 -0
  39. /package/dist/{desktop-app/src/main → cbx/src/core}/profile.js +0 -0
  40. /package/dist/{desktop-app/src/main → cbx/src/core}/publish_name.js +0 -0
  41. /package/dist/{desktop-app/src/main → cbx/src/core}/retry.js +0 -0
  42. /package/dist/{desktop-app/src/main → cbx/src/core}/secret_patterns.js +0 -0
  43. /package/dist/{desktop-app/src/main → cbx/src/core}/solid.js +0 -0
  44. /package/dist/{desktop-app/src/main → cbx/src/core}/staging.js +0 -0
  45. /package/dist/{desktop-app/src/main → cbx/src/core}/tracks.js +0 -0
  46. /package/dist/{desktop-app/src/main → cbx/src/core}/transfer_progress.js +0 -0
  47. /package/dist/{desktop-app/src/main → cbx/src/core}/upload.js +0 -0
  48. /package/dist/{desktop-app/src/main → cbx/src/core}/worktree.js +0 -0
  49. /package/dist/{desktop-app → cbx}/src/shared/chunking.js +0 -0
  50. /package/dist/{desktop-app → cbx}/src/shared/compression_policy.js +0 -0
  51. /package/dist/{desktop-app → cbx}/src/shared/delta.js +0 -0
  52. /package/dist/{desktop-app → cbx}/src/shared/format.js +0 -0
  53. /package/dist/{desktop-app → cbx}/src/shared/merge_diff.js +0 -0
  54. /package/dist/{desktop-app → cbx}/src/shared/publication.js +0 -0
  55. /package/dist/{desktop-app → cbx}/src/shared/telemetry.js +0 -0
  56. /package/dist/{desktop-app → cbx}/src/shared/types.js +0 -0
@@ -0,0 +1,625 @@
1
+ "use strict";
2
+ /**
3
+ * `cbx pages`: a public project served as a website.
4
+ *
5
+ * Kept in its own file beside the other project commands. It is one command
6
+ * with four answers — what the site is set to, find the build for me, on, off
7
+ * — and the "find it for me" answer reaches onto the disk and into the ignore
8
+ * rules, which none of the account-side commands do.
9
+ *
10
+ * Turning a site on is the step that asks. What it serves is the project's
11
+ * own code, running in a stranger's browser under the project's name, and
12
+ * that deserves a yes from somebody who can give one. Turning it off asks
13
+ * nothing: it takes nothing from anybody who should have it.
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.BUILD_FOLDERS = exports.whereWords = exports.kindWords = void 0;
20
+ exports.splitPages = splitPages;
21
+ exports.normaliseFolder = normaliseFolder;
22
+ exports.explainRefusal = explainRefusal;
23
+ exports.commandPages = commandPages;
24
+ exports.findWebBuilds = findWebBuilds;
25
+ exports.reinclusionFor = reinclusionFor;
26
+ const promises_1 = require("node:fs/promises");
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const node_process_1 = __importDefault(require("node:process"));
29
+ const api_js_1 = require("./api.js");
30
+ const config_js_1 = require("./config.js");
31
+ const project_commands_js_1 = require("./project_commands.js");
32
+ const worktree_js_1 = require("../../cbx/src/core/worktree.js");
33
+ const rules_js_1 = require("../../cbx/src/core/rules.js");
34
+ const colour = node_process_1.default.stdout.isTTY && !node_process_1.default.env.NO_COLOR;
35
+ const dim = (text) => (colour ? `[2m${text}[0m` : text);
36
+ const bold = (text) => (colour ? `[1m${text}[0m` : text);
37
+ const accent = (text) => (colour ? `[33m${text}[0m` : text);
38
+ const red = (text) => (colour ? `[31m${text}[0m` : text);
39
+ const green = (text) => (colour ? `[32m${text}[0m` : text);
40
+ const SUBCOMMANDS = new Set(["status", "auto", "on", "off"]);
41
+ /** `[action] [project]`, where the action is recognised by being one. */
42
+ function splitPages(positional) {
43
+ const [first, second] = positional;
44
+ if (first && SUBCOMMANDS.has(first.toLowerCase())) {
45
+ return {
46
+ action: first.toLowerCase(),
47
+ project: second,
48
+ };
49
+ }
50
+ return { action: "status", project: first };
51
+ }
52
+ /** What a kind of build is called when it is said out loud. */
53
+ const KIND_WORDS = {
54
+ unity: "a Unity WebGL build",
55
+ godot: "a Godot web export",
56
+ vite: "a Vite build",
57
+ next: "a Next.js static export",
58
+ react: "a React build",
59
+ static: "a website",
60
+ };
61
+ const kindWords = (kind) => KIND_WORDS[kind] ?? "a website";
62
+ exports.kindWords = kindWords;
63
+ /** "in dist/", or "at the top of the project" for the empty folder. */
64
+ const whereWords = (folder) => folder ? `in ${folder.replace(/\/+$/, "")}/` : "at the top of the project";
65
+ exports.whereWords = whereWords;
66
+ /**
67
+ * A folder as the service wants it: forward slashes, no leading `./`, no
68
+ * trailing slash, and "" for the top. Typed on Windows it arrives with
69
+ * backslashes, and `.` is how people say "right here".
70
+ */
71
+ function normaliseFolder(typed) {
72
+ const cleaned = typed
73
+ .trim()
74
+ .replace(/\\/g, "/")
75
+ .replace(/^(\.\/)+/, "")
76
+ .replace(/^\/+/, "")
77
+ .replace(/\/+$/, "");
78
+ return cleaned === "." ? "" : cleaned;
79
+ }
80
+ /** A switch given as `--spa`, `--spa=off` or `--no-spa`. */
81
+ function switchValue(parsed, ...names) {
82
+ for (const name of names) {
83
+ const value = parsed.flags.get(name);
84
+ if (value !== undefined) {
85
+ if (value === true)
86
+ return true;
87
+ return !["off", "false", "no", "n", "0"].includes(value.trim().toLowerCase());
88
+ }
89
+ if (parsed.flags.get(`no-${name}`) !== undefined)
90
+ return false;
91
+ }
92
+ return undefined;
93
+ }
94
+ const sure = (parsed) => parsed.flags.get("yes") === true || parsed.flags.get("y") === true;
95
+ /**
96
+ * Say a refusal in a way somebody can act on.
97
+ *
98
+ * The service's own sentence is passed on as it is — it knows why — with a
99
+ * line after it where there is an obvious next command. Two are recognised
100
+ * by status rather than wording: "this service has no Pages yet", which is
101
+ * not the person's fault and not something they can fix, and "only the owner
102
+ * can", which the service says in its own words.
103
+ */
104
+ function explainRefusal(error, projectRef) {
105
+ if (error instanceof api_js_1.ServiceError) {
106
+ if (error.status === 503 ||
107
+ error.code === "pages_not_ready" ||
108
+ (error.status === 404 && error.code === "not_found")) {
109
+ return ["Pages is not available on this service yet."];
110
+ }
111
+ if (error.status === 403) {
112
+ return [
113
+ error.message ||
114
+ "Access denied: only the project's owner or an admin can change its site.",
115
+ ];
116
+ }
117
+ const lines = [error.message];
118
+ if (error.code === "pages_needs_public") {
119
+ lines.push(dim(` Make the project public first: cbx visibility public${projectRef}`));
120
+ }
121
+ else if (error.code === "version_not_public") {
122
+ lines.push(dim(" Only a published version can be served. cbx promote publishes a save."));
123
+ }
124
+ return lines;
125
+ }
126
+ return [error instanceof Error ? error.message : String(error)];
127
+ }
128
+ function refuse(error, projectRef) {
129
+ for (const line of explainRefusal(error, projectRef))
130
+ console.error(red(line));
131
+ return 1;
132
+ }
133
+ /** The settings, laid out the way somebody checks them. */
134
+ function printSettings(project, settings) {
135
+ const label = (text) => ` ${text.padEnd(9)} `;
136
+ console.log(`${bold(project.name)} — Pages is ${settings.enabled ? green("on") : dim("off")}`);
137
+ console.log(label("Address") +
138
+ (settings.address
139
+ ? settings.address
140
+ : dim("goes live when the Pages domain is set up")));
141
+ if (settings.live) {
142
+ const name = settings.live.name ? ` ${bold(settings.live.name)}` : "";
143
+ const which = settings.versionId ? "pinned" : "newest published";
144
+ /* While off it is what would be served, so it is not called "Serving". */
145
+ console.log(label(settings.enabled ? "Serving" : "Would serve") +
146
+ `${accent(`v${settings.live.sequence}`)}${name} ${dim(which)}`);
147
+ }
148
+ else {
149
+ console.log(label("Serving") + dim("nothing published yet"));
150
+ }
151
+ console.log(label("Folder") +
152
+ (settings.folder ? `${settings.folder}/` : "top of the project") +
153
+ (settings.entry ? dim(` (${settings.entry.path})`) : ""));
154
+ console.log(label("Options") +
155
+ `single-page app ${settings.spa ? "yes" : "no"}` +
156
+ dim(" · ") +
157
+ `cross-origin isolation ${settings.isolation ? "yes" : "no"}`);
158
+ if (settings.entry && !settings.entry.found) {
159
+ console.log(accent(` Warning: ${settings.entry.path} is not in ` +
160
+ (settings.live ? `v${settings.live.sequence}` : "the published files") +
161
+ ", so the site has no front page."));
162
+ }
163
+ if (settings.blocked)
164
+ console.log(label("Blocked") + red(settings.blocked));
165
+ }
166
+ /** Pages settings for a project, or null after saying why not. */
167
+ async function current(project, projectRef) {
168
+ try {
169
+ return await (0, api_js_1.pages)(project.id);
170
+ }
171
+ catch (error) {
172
+ refuse(error, projectRef);
173
+ return null;
174
+ }
175
+ }
176
+ /**
177
+ * Show, find, switch on or switch off a project's site.
178
+ */
179
+ async function commandPages(parsed) {
180
+ const { action, project: reference } = splitPages(parsed.positional);
181
+ const project = await (0, project_commands_js_1.resolveProject)(reference);
182
+ if (!project)
183
+ return 1;
184
+ const projectRef = reference ? ` ${reference}` : "";
185
+ if (action === "status") {
186
+ const settings = await current(project, projectRef);
187
+ if (!settings)
188
+ return 1;
189
+ printSettings(project, settings);
190
+ if (!settings.enabled) {
191
+ console.log("");
192
+ console.log(dim(`Turn it on: cbx pages auto${projectRef} finds the build for you`));
193
+ console.log(dim(` or: cbx pages on${projectRef} --folder <path>`));
194
+ }
195
+ return 0;
196
+ }
197
+ if (action === "off") {
198
+ try {
199
+ const settings = await (0, api_js_1.setPages)(project.id, { enabled: false });
200
+ console.log(green(`Pages is off for ${project.name}.`));
201
+ if (settings.address)
202
+ console.log(dim(` ${settings.address} no longer serves anything.`));
203
+ return 0;
204
+ }
205
+ catch (error) {
206
+ return refuse(error, projectRef);
207
+ }
208
+ }
209
+ if (action === "on")
210
+ return turnOn(parsed, project, projectRef);
211
+ return auto(parsed, project, projectRef);
212
+ }
213
+ /** Refused rather than asked when nobody can answer; see commandVisibility. */
214
+ function nobodyToAsk(what) {
215
+ console.error(red(`${what} needs a yes, and nobody is here to give one.`));
216
+ console.error(dim(" Run it in a terminal to be asked, or add --yes."));
217
+ return 1;
218
+ }
219
+ /** Said before the question, so the yes is to something specific. */
220
+ function sayWhatGoesPublic(project, address) {
221
+ console.log(`Pages serves ${bold(project.name)}'s published files as a website` +
222
+ (address ? ` at ${address}` : "") +
223
+ ".");
224
+ console.log(" Anybody can open it, and the code in it runs in their browser under the project's name.");
225
+ if (!address)
226
+ console.log(dim(" It goes live when the Pages domain is set up."));
227
+ }
228
+ function notPublic(project, projectRef) {
229
+ if (project.visibility === "public")
230
+ return null;
231
+ console.error(red(`${project.name} is not public, and only a public project can have a site.`));
232
+ console.error(dim(` Make it public first: cbx visibility public${projectRef}`));
233
+ return 1;
234
+ }
235
+ /** `--version 12`, `--version <id>` or `--version newest`. */
236
+ async function versionFor(project, typed) {
237
+ const text = typed.trim().replace(/^v(?=\d+$)/i, "");
238
+ if (["newest", "latest"].includes(text.toLowerCase()))
239
+ return { versionId: null };
240
+ if (/^\d+$/.test(text)) {
241
+ const saved = await (0, api_js_1.versions)(project.id);
242
+ const found = saved.find((one) => one.sequence === Number(text));
243
+ if (!found)
244
+ return { error: `${project.name} has no v${text}.` };
245
+ return { versionId: found.id };
246
+ }
247
+ return { versionId: text };
248
+ }
249
+ async function turnOn(parsed, project, projectRef) {
250
+ const change = { enabled: true };
251
+ const folder = parsed.flags.get("folder");
252
+ if (folder === true) {
253
+ console.error(red("--folder needs a path, such as --folder dist. Use --folder . for the top."));
254
+ return 1;
255
+ }
256
+ if (typeof folder === "string")
257
+ change.folder = normaliseFolder(folder);
258
+ const spa = switchValue(parsed, "spa");
259
+ if (spa !== undefined)
260
+ change.spa = spa;
261
+ const isolation = switchValue(parsed, "isolate", "isolation");
262
+ if (isolation !== undefined)
263
+ change.isolation = isolation;
264
+ const version = parsed.flags.get("version");
265
+ if (version === true) {
266
+ console.error(red("--version needs a number, an id, or newest."));
267
+ return 1;
268
+ }
269
+ if (typeof version === "string") {
270
+ const found = await versionFor(project, version);
271
+ if ("error" in found) {
272
+ console.error(red(found.error));
273
+ return 1;
274
+ }
275
+ change.versionId = found.versionId;
276
+ }
277
+ const refused = notPublic(project, projectRef);
278
+ if (refused !== null)
279
+ return refused;
280
+ const before = await current(project, projectRef);
281
+ if (!before)
282
+ return 1;
283
+ /*
284
+ Asked only when the site is off. Moving a site that is already public to
285
+ another folder or version changes what it shows, not whether it exists,
286
+ and that decision has already been made by somebody who could make it.
287
+ */
288
+ if (!before.enabled && !sure(parsed)) {
289
+ if (!node_process_1.default.stdin.isTTY)
290
+ return nobodyToAsk(`Turning on Pages for ${project.name}`);
291
+ sayWhatGoesPublic(project, before.address);
292
+ if (!(await (0, project_commands_js_1.agreed)(`Turn on Pages for ${project.name}? [y/N] `))) {
293
+ console.log(dim(`Nothing changed. Pages is still off for ${project.name}.`));
294
+ return 1;
295
+ }
296
+ }
297
+ let settings;
298
+ try {
299
+ settings = await (0, api_js_1.setPages)(project.id, change);
300
+ }
301
+ catch (error) {
302
+ return refuse(error, projectRef);
303
+ }
304
+ console.log(green(before.enabled ? `Pages settings changed for ${project.name}.` : `Pages is on for ${project.name}.`));
305
+ console.log("");
306
+ printSettings(project, settings);
307
+ return 0;
308
+ }
309
+ async function auto(parsed, project, projectRef) {
310
+ let found;
311
+ try {
312
+ found = await (0, api_js_1.detectPages)(project.id);
313
+ }
314
+ catch (error) {
315
+ return refuse(error, projectRef);
316
+ }
317
+ if (found.suggestion) {
318
+ const suggestion = found.suggestion;
319
+ const where = (0, exports.whereWords)(suggestion.folder);
320
+ const inVersion = found.version ? ` of v${found.version.sequence}` : "";
321
+ console.log(`Found ${(0, exports.kindWords)(suggestion.kind)} ${where}${inVersion}.`);
322
+ if (suggestion.why)
323
+ console.log(dim(` ${suggestion.why}`));
324
+ const extras = [
325
+ suggestion.spa ? "as a single-page app" : "",
326
+ suggestion.isolation ? "with cross-origin isolation" : "",
327
+ ].filter(Boolean);
328
+ if (extras.length)
329
+ console.log(dim(` It would be served ${extras.join(", ")}.`));
330
+ const refused = notPublic(project, projectRef);
331
+ if (refused !== null)
332
+ return refused;
333
+ if (!sure(parsed)) {
334
+ if (!node_process_1.default.stdin.isTTY)
335
+ return nobodyToAsk(`Turning on Pages for ${project.name}`);
336
+ const before = await (0, api_js_1.pages)(project.id).catch(() => null);
337
+ sayWhatGoesPublic(project, before?.address ?? null);
338
+ if (!(await (0, project_commands_js_1.agreed)(`Turn on Pages for ${project.name} with this? [y/N] `))) {
339
+ console.log(dim("Nothing changed."));
340
+ return 1;
341
+ }
342
+ }
343
+ let settings;
344
+ try {
345
+ settings = await (0, api_js_1.autoPages)(project.id);
346
+ }
347
+ catch (error) {
348
+ return refuse(error, projectRef);
349
+ }
350
+ console.log(green(`Pages is on for ${project.name}.`));
351
+ console.log("");
352
+ printSettings(project, settings);
353
+ return 0;
354
+ }
355
+ /*
356
+ Nothing to serve in what was published. If this is the project's own
357
+ folder, the build may be sitting right here and simply never have gone
358
+ up — most often because the ignore rules leave build output out, which
359
+ is exactly right for a repository and exactly wrong for a site.
360
+ */
361
+ const root = node_process_1.default.cwd();
362
+ const link = await (0, config_js_1.readLink)(root).catch(() => null);
363
+ if (link?.repositoryId === project.id) {
364
+ const builds = await findWebBuilds(root);
365
+ if (builds.length)
366
+ return offerLocalBuild(parsed, root, builds, projectRef);
367
+ }
368
+ console.log(found.problem ?? "Nothing in the published files looks like a website.");
369
+ return 1;
370
+ }
371
+ // ── the build on the disk ───────────────────────────────────────────────────
372
+ /** Where build tools put a site, in roughly the order they are met. */
373
+ exports.BUILD_FOLDERS = [
374
+ "dist",
375
+ "build",
376
+ "out",
377
+ "public",
378
+ "docs",
379
+ "Build",
380
+ "WebGL",
381
+ "web",
382
+ "html5",
383
+ "export",
384
+ "site",
385
+ "_site",
386
+ "www",
387
+ ];
388
+ async function isFile(target) {
389
+ try {
390
+ return (await (0, promises_1.stat)(target)).isFile();
391
+ }
392
+ catch {
393
+ return false;
394
+ }
395
+ }
396
+ async function namesIn(directory) {
397
+ try {
398
+ return await (0, promises_1.readdir)(directory, { withFileTypes: true });
399
+ }
400
+ catch {
401
+ return [];
402
+ }
403
+ }
404
+ /** Unity's loader beside a Build folder, or Godot's pack beside the page. */
405
+ async function kindOf(directory) {
406
+ for (const entry of await namesIn(node_path_1.default.join(directory, "Build"))) {
407
+ if (entry.isFile() && entry.name.endsWith(".loader.js"))
408
+ return "unity";
409
+ }
410
+ for (const entry of await namesIn(directory)) {
411
+ if (entry.isFile() && entry.name.endsWith(".pck"))
412
+ return "godot";
413
+ }
414
+ return "static";
415
+ }
416
+ /**
417
+ * Folders here holding an index.html, engine builds first.
418
+ *
419
+ * The usual build folders, one level inside them (`Build/WebGL`,
420
+ * `export/web`), and any other top-level folder only when an engine's marker
421
+ * says it is a build — a stray index.html in `templates/` is not a site.
422
+ */
423
+ async function findWebBuilds(root) {
424
+ const found = [];
425
+ const seen = new Set();
426
+ const consider = async (folder, needMarker) => {
427
+ const key = folder.toLowerCase();
428
+ if (seen.has(key))
429
+ return;
430
+ const directory = node_path_1.default.join(root, ...folder.split("/"));
431
+ if (!(await isFile(node_path_1.default.join(directory, "index.html"))))
432
+ return;
433
+ const kind = await kindOf(directory);
434
+ if (needMarker && kind === "static")
435
+ return;
436
+ seen.add(key);
437
+ found.push({ folder, kind });
438
+ };
439
+ const top = (await namesIn(root)).filter((entry) => entry.isDirectory() &&
440
+ !entry.name.startsWith(".") &&
441
+ !(0, rules_js_1.isUncounted)(entry.name) &&
442
+ entry.name !== "node_modules");
443
+ const usual = new Set(exports.BUILD_FOLDERS.map((name) => name.toLowerCase()));
444
+ const ordered = [...top].sort((left, right) => rank(left.name) - rank(right.name) || left.name.localeCompare(right.name));
445
+ function rank(name) {
446
+ const at = exports.BUILD_FOLDERS.findIndex((one) => one.toLowerCase() === name.toLowerCase());
447
+ return at === -1 ? exports.BUILD_FOLDERS.length : at;
448
+ }
449
+ for (const entry of ordered) {
450
+ if (!usual.has(entry.name.toLowerCase()))
451
+ continue;
452
+ await consider(entry.name, false);
453
+ for (const inner of await namesIn(node_path_1.default.join(root, entry.name))) {
454
+ if (inner.isDirectory() && !inner.name.startsWith(".")) {
455
+ await consider(`${entry.name}/${inner.name}`, false);
456
+ }
457
+ }
458
+ }
459
+ for (const entry of ordered) {
460
+ if (usual.has(entry.name.toLowerCase()))
461
+ continue;
462
+ await consider(entry.name, true);
463
+ }
464
+ /* An engine's build is a surer answer than a folder that happens to be
465
+ called dist, so it goes first; otherwise the usual order stands. */
466
+ return found.sort((left, right) => Number(left.kind === "static") - Number(right.kind === "static"));
467
+ }
468
+ /** Characters a gitignore pattern would read as syntax, written as themselves. */
469
+ const literal = (name) => name.replace(/[\\[\]*?!#]/g, "\\$&");
470
+ /** Which file a rule came from, for saying so. */
471
+ function sourceOf(rule, layers, rules) {
472
+ const layer = layers.find((one) => one.rules.includes(rule));
473
+ if (!layer)
474
+ return ".gitignore";
475
+ if (layer.base)
476
+ return `${layer.base}/.gitignore`;
477
+ /* The personal layer, when there is one, is always first. */
478
+ if (rules.local.trim() && layers[0] === layer)
479
+ return ".git/info/exclude";
480
+ return ".gitignore";
481
+ }
482
+ const written = (rule) => `${rule.negated ? "!" : ""}${rule.anchored ? "/" : ""}${rule.pattern}${rule.directoryOnly ? "/" : ""}`;
483
+ /**
484
+ * Whether the ignore rules leave a build's front page out, and what would
485
+ * bring it back.
486
+ *
487
+ * Asked of the same rules, the same layers and the same matcher a save
488
+ * uses, so the answer is the one `cbx submit` will act on. The fix is
489
+ * checked the same way before it is offered: git cannot re-include a file
490
+ * whose folder is excluded, so each excluded folder on the way down gets
491
+ * its own negation, and a rule in a deeper .gitignore — which outranks the
492
+ * project's own — is named rather than argued with.
493
+ */
494
+ async function reinclusionFor(root, folder, rules) {
495
+ const entry = folder ? `${folder}/index.html` : "index.html";
496
+ const tracked = await (0, worktree_js_1.gitTracked)(root);
497
+ if (tracked?.files.has(entry))
498
+ return { excluded: false };
499
+ const layers = await (0, worktree_js_1.collectLayers)(root, rules);
500
+ const deciding = (0, rules_js_1.decidingRule)(entry, false, layers);
501
+ if (!deciding || deciding.negated)
502
+ return { excluded: false };
503
+ const first = { rule: written(deciding), source: sourceOf(deciding, layers, rules) };
504
+ const lines = [];
505
+ const parts = folder ? folder.split("/") : [];
506
+ for (let attempt = 0; attempt <= parts.length + 1; attempt += 1) {
507
+ const shared = withLines(rules.shared, lines);
508
+ const trial = await (0, worktree_js_1.collectLayers)(root, { ...rules, shared });
509
+ const rule = (0, rules_js_1.decidingRule)(entry, false, trial);
510
+ if (!rule || rule.negated)
511
+ return { excluded: true, ...first, lines };
512
+ const source = sourceOf(rule, trial, { ...rules, shared });
513
+ if (source !== ".gitignore" && source !== ".git/info/exclude") {
514
+ return {
515
+ excluded: true,
516
+ ...first,
517
+ lines: null,
518
+ obstacle: `${source} has "${written(rule)}", and a deeper .gitignore outranks the project's own. Change or remove that line there.`,
519
+ };
520
+ }
521
+ /* The outermost folder still left out, or the page itself. */
522
+ let next = null;
523
+ for (let depth = 1; depth <= parts.length; depth += 1) {
524
+ const ancestor = parts.slice(0, depth).join("/");
525
+ if ((0, rules_js_1.excludes)(ancestor, true, trial)) {
526
+ next = `!/${parts.slice(0, depth).map(literal).join("/")}/`;
527
+ break;
528
+ }
529
+ }
530
+ next ??= `!/${[...parts, "index.html"].map(literal).join("/")}`;
531
+ if (lines.includes(next))
532
+ break;
533
+ lines.push(next);
534
+ }
535
+ return {
536
+ excluded: true,
537
+ ...first,
538
+ lines: null,
539
+ obstacle: `A negation in .gitignore does not bring ${entry} back. Change the "${first.rule}" line in ${first.source} instead.`,
540
+ };
541
+ }
542
+ const PAGES_HEADING = "# Kept for CodeRook Pages: the site is served from this build.";
543
+ function withLines(shared, lines) {
544
+ if (!lines.length)
545
+ return shared;
546
+ const body = shared.replace(/\s*$/, "");
547
+ return `${body}${body ? "\n\n" : ""}${PAGES_HEADING}\n${lines.join("\n")}\n`;
548
+ }
549
+ /** Files under a folder the rules would still leave out, a few named. */
550
+ async function stillLeftOut(root, folder, rules) {
551
+ const layers = await (0, worktree_js_1.collectLayers)(root, rules);
552
+ const result = { count: 0, examples: [] };
553
+ const pending = [folder];
554
+ let looked = 0;
555
+ while (pending.length && looked < 20_000) {
556
+ const relative = pending.shift();
557
+ for (const entry of await namesIn(node_path_1.default.join(root, ...relative.split("/")))) {
558
+ const child = relative ? `${relative}/${entry.name}` : entry.name;
559
+ if (entry.isDirectory()) {
560
+ if (!(0, rules_js_1.isUncounted)(entry.name))
561
+ pending.push(child);
562
+ }
563
+ else if (entry.isFile()) {
564
+ looked += 1;
565
+ if ((0, rules_js_1.excludes)(child, false, layers)) {
566
+ result.count += 1;
567
+ if (result.examples.length < 3)
568
+ result.examples.push(child);
569
+ }
570
+ }
571
+ }
572
+ }
573
+ return result;
574
+ }
575
+ function nextSteps(projectRef) {
576
+ console.log("");
577
+ console.log("Then, to serve it:");
578
+ console.log(` 1. cbx submit ${dim("save the folder, build included")}`);
579
+ console.log(` 2. cbx promote ${dim("publish that save (or make it public from History)")}`);
580
+ console.log(` 3. cbx pages auto${projectRef} ${dim("turn Pages on with what it finds")}`);
581
+ }
582
+ async function offerLocalBuild(parsed, root, builds, projectRef) {
583
+ const build = builds[0];
584
+ const label = build.folder ? `${build.folder}/` : "the top of the folder";
585
+ console.log(`Nothing published can be served yet, but ${(0, exports.kindWords)(build.kind)} is here in ${label}.`);
586
+ if (builds.length > 1) {
587
+ console.log(dim(` Also found: ${builds.slice(1).map((one) => `${one.folder}/`).join(", ")}`));
588
+ }
589
+ const rules = await (0, worktree_js_1.readRules)(root);
590
+ const verdict = await reinclusionFor(root, build.folder, rules);
591
+ if (!verdict.excluded) {
592
+ console.log("It is not in the published version: it has not been saved and published yet.");
593
+ nextSteps(projectRef);
594
+ return 0;
595
+ }
596
+ console.log(`Your ignore rules leave ${label} out of every save — "${verdict.rule}" in ${verdict.source}.`);
597
+ console.log(dim(" That is right for a repository, where a build can be rebuilt, and wrong for a site."));
598
+ if (!verdict.lines) {
599
+ console.log(accent(` ${verdict.obstacle}`));
600
+ nextSteps(projectRef);
601
+ return 1;
602
+ }
603
+ console.log("Adding this to .gitignore keeps it in:");
604
+ for (const line of verdict.lines)
605
+ console.log(` ${accent(line)}`);
606
+ if (!sure(parsed)) {
607
+ if (!node_process_1.default.stdin.isTTY)
608
+ return nobodyToAsk("Changing .gitignore");
609
+ if (!(await (0, project_commands_js_1.agreed)("Add it to .gitignore? [y/N] "))) {
610
+ console.log(dim("Nothing changed."));
611
+ return 1;
612
+ }
613
+ }
614
+ const changed = { ...rules, shared: withLines(rules.shared, verdict.lines) };
615
+ await (0, worktree_js_1.writeRules)(root, changed);
616
+ console.log(green(`Added to .gitignore. ${label} now goes up with the next save.`));
617
+ const left = await stillLeftOut(root, build.folder, changed);
618
+ if (left.count) {
619
+ console.log(accent(` ${left.count} file${left.count === 1 ? "" : "s"} in ${label} ` +
620
+ `${left.count === 1 ? "is" : "are"} still left out by other rules, such as ${left.examples.join(", ")}.`));
621
+ console.log(dim(" cbx ignore shows the rules in force."));
622
+ }
623
+ nextSteps(projectRef);
624
+ return 0;
625
+ }
@@ -17,6 +17,7 @@ exports.commandAi = commandAi;
17
17
  exports.readLevel = readLevel;
18
18
  exports.splitVisibility = splitVisibility;
19
19
  exports.offeredPublicly = offeredPublicly;
20
+ exports.agreed = agreed;
20
21
  exports.commandVisibility = commandVisibility;
21
22
  exports.commandVersions = commandVersions;
22
23
  const node_process_1 = __importDefault(require("node:process"));
@@ -19,6 +19,7 @@ exports.nearestCommand = nearestCommand;
19
19
  exports.GROUP_ORDER = [
20
20
  "Getting started",
21
21
  "Working with a folder",
22
+ "History on this machine",
22
23
  "Your projects",
23
24
  "People",
24
25
  "When somebody saved first",
@@ -34,8 +34,8 @@ const promises_1 = require("node:fs/promises");
34
34
  const node_os_1 = require("node:os");
35
35
  const node_os_2 = __importDefault(require("node:os"));
36
36
  const node_path_1 = __importDefault(require("node:path"));
37
- const download_js_1 = require("../../desktop-app/src/main/download.js");
38
- const identify_js_1 = require("../../desktop-app/src/main/identify.js");
37
+ const download_js_1 = require("../../cbx/src/core/download.js");
38
+ const identify_js_1 = require("../../cbx/src/core/identify.js");
39
39
  const config_js_1 = require("./config.js");
40
40
  const progress_js_1 = require("./progress.js");
41
41
  /** How often to tell the service the machine is still working. */
@@ -24,7 +24,7 @@ exports.trackFor = trackFor;
24
24
  exports.commandTracks = commandTracks;
25
25
  exports.commandTrack = commandTrack;
26
26
  const node_process_1 = __importDefault(require("node:process"));
27
- const tracks_js_1 = require("../../desktop-app/src/main/tracks.js");
27
+ const tracks_js_1 = require("../../cbx/src/core/tracks.js");
28
28
  const config_js_1 = require("./config.js");
29
29
  const project_commands_js_1 = require("./project_commands.js");
30
30
  const dim = (value) => `${value}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.31.12",
3
+ "version": "0.32.0",
4
4
  "description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",
@@ -33,7 +33,7 @@
33
33
  "skills"
34
34
  ],
35
35
  "scripts": {
36
- "build": "tsc -p tsconfig.json",
36
+ "build": "tsc -p tsconfig.json && node scripts/copy-cbx-licence.mjs",
37
37
  "check": "tsc -p tsconfig.json --noEmit",
38
38
  "test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
39
39
  "start": "node dist/cli/src/cli.js",