@coderook/cli 0.44.0 → 0.46.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.
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ENGINE_SCAN_MIN_FILES = exports.ENGINE_MIN_BYTES = exports.ENGINE_MIN_FILES = void 0;
7
+ exports.enginePath = enginePath;
8
+ exports.engineProblem = engineProblem;
9
+ exports.worthTheEngine = worthTheEngine;
10
+ exports.engineStore = engineStore;
11
+ exports.engineMeasure = engineMeasure;
12
+ exports.engineScan = engineScan;
13
+ exports.resetEngineForTests = resetEngineForTests;
14
+ /**
15
+ * The native byte engine (cbx/engine, Rust), when there is one.
16
+ *
17
+ * The engine does the same byte work as the TypeScript here — hashing, cutting
18
+ * big files, framing and packing objects — with the same results: the same
19
+ * SHA-256 names, the same chunk cuts, the same "cbx-local 1" store. It is only
20
+ * faster, because it reads, hashes and compresses on every core at once. The
21
+ * cross-tests in cbx/engine/compat compare the two on real folders.
22
+ *
23
+ * It is optional. Every caller keeps its own TypeScript path and uses it
24
+ * whenever this returns null: no engine for this platform, CBX_ENGINE=off, a
25
+ * failed run, or an answer that does not look right. A run that fails once is
26
+ * not tried again in the same process, so a broken binary costs one attempt.
27
+ *
28
+ * Where it is looked for:
29
+ * CBX_ENGINE=<path> that binary ("off" turns the engine off)
30
+ * engine-bin/<platform>-<arch>/cbx-engine[.exe] in this package or a folder above it
31
+ */
32
+ const node_child_process_1 = require("node:child_process");
33
+ const node_fs_1 = require("node:fs");
34
+ const node_path_1 = __importDefault(require("node:path"));
35
+ /** The engine's command protocol this code speaks; `cbx-engine version` reports its own. */
36
+ const PROTOCOL = 1;
37
+ /** Below this much work, starting a process costs more than it saves. */
38
+ exports.ENGINE_MIN_FILES = 24;
39
+ exports.ENGINE_MIN_BYTES = 32 * 1024 * 1024;
40
+ /** Below this many files a folder is walked faster here than by starting the engine. */
41
+ exports.ENGINE_SCAN_MIN_FILES = 400;
42
+ let located;
43
+ let broken = null;
44
+ function binaryName() {
45
+ return process.platform === "win32" ? "cbx-engine.exe" : "cbx-engine";
46
+ }
47
+ /*
48
+ Whether the binary may be run, making it so when it can be.
49
+
50
+ A package packed on Windows carries no execute bit, so on macOS and Linux the
51
+ engine arrives as a plain file. Setting the bit is all it needs; where the
52
+ install folder is not ours to change, the TypeScript path is used instead.
53
+ */
54
+ function runnable(candidate) {
55
+ if (process.platform === "win32")
56
+ return true;
57
+ try {
58
+ (0, node_fs_1.accessSync)(candidate, node_fs_1.constants.X_OK);
59
+ return true;
60
+ }
61
+ catch {
62
+ try {
63
+ (0, node_fs_1.chmodSync)(candidate, 0o755);
64
+ (0, node_fs_1.accessSync)(candidate, node_fs_1.constants.X_OK);
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ }
72
+ /** Where the engine is, or null when there is none to use. */
73
+ function enginePath() {
74
+ if (located !== undefined)
75
+ return located;
76
+ const chosen = process.env.CBX_ENGINE?.trim();
77
+ if (chosen === "off" || chosen === "0" || chosen === "false")
78
+ return (located = null);
79
+ if (chosen)
80
+ return (located = (0, node_fs_1.existsSync)(chosen) ? chosen : null);
81
+ /* CommonJS here; in an ES module build __dirname is not defined and only CBX_ENGINE applies. */
82
+ let directory = typeof __dirname === "string" ? __dirname : null;
83
+ const wanted = node_path_1.default.join("engine-bin", `${process.platform}-${process.arch}`, binaryName());
84
+ for (let depth = 0; directory && depth < 8; depth += 1) {
85
+ const candidate = node_path_1.default.join(directory, wanted);
86
+ if ((0, node_fs_1.existsSync)(candidate))
87
+ return (located = runnable(candidate) ? candidate : null);
88
+ const parent = node_path_1.default.dirname(directory);
89
+ directory = parent === directory ? null : parent;
90
+ }
91
+ return (located = null);
92
+ }
93
+ /** Why the engine is not being used, for diagnostics; null when it is available. */
94
+ function engineProblem() {
95
+ if (broken)
96
+ return broken;
97
+ return enginePath() ? null : "no engine for this platform";
98
+ }
99
+ /** Whether a batch of this size is worth handing to the engine. */
100
+ function worthTheEngine(files, bytes) {
101
+ if (enginePath() === null || broken || files === 0)
102
+ return false;
103
+ /* CBX_ENGINE_ALWAYS=1 sends every batch, however small, so tests can run everything through it. */
104
+ if (process.env.CBX_ENGINE_ALWAYS === "1")
105
+ return true;
106
+ return files >= exports.ENGINE_MIN_FILES || bytes >= exports.ENGINE_MIN_BYTES;
107
+ }
108
+ function run(args, input, onLine, signal) {
109
+ const binary = enginePath();
110
+ return new Promise((resolve) => {
111
+ let child;
112
+ try {
113
+ child = (0, node_child_process_1.spawn)(binary, args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
114
+ }
115
+ catch (error) {
116
+ resolve({ stdout: "", stderr: String(error), code: -1 });
117
+ return;
118
+ }
119
+ /* Cancelling the operation stops the engine with it. */
120
+ const stop = () => child.kill();
121
+ if (signal?.aborted)
122
+ stop();
123
+ signal?.addEventListener("abort", stop, { once: true });
124
+ child.on("close", () => signal?.removeEventListener("abort", stop));
125
+ const out = [];
126
+ let err = "";
127
+ let partial = "";
128
+ child.stdout.on("data", (data) => out.push(data));
129
+ child.stderr.on("data", (data) => {
130
+ const text = partial + data.toString("utf8");
131
+ const lines = text.split("\n");
132
+ partial = lines.pop() ?? "";
133
+ for (const line of lines) {
134
+ if (onLine && line.startsWith("{"))
135
+ onLine(line);
136
+ else
137
+ err += `${line}\n`;
138
+ }
139
+ });
140
+ child.on("error", (error) => resolve({ stdout: "", stderr: String(error), code: -1 }));
141
+ child.on("close", (code) => resolve({ stdout: Buffer.concat(out).toString("utf8"), stderr: err + partial, code }));
142
+ child.stdin.on("error", () => {
143
+ /* The engine exited before reading; its exit code says why. */
144
+ });
145
+ child.stdin.end(input ?? "");
146
+ });
147
+ }
148
+ /*
149
+ Every answer carries the protocol it speaks, so an engine from another
150
+ release is caught on its first answer without a separate process to ask.
151
+ */
152
+ function usable() {
153
+ return Boolean(enginePath()) && !broken;
154
+ }
155
+ async function ask(args, input, onLine, signal) {
156
+ if (!usable())
157
+ return null;
158
+ const answer = await run(args, JSON.stringify(input), onLine, signal);
159
+ /* Stopped on purpose is not a broken engine; the caller sees its own cancellation. */
160
+ if (signal?.aborted)
161
+ return null;
162
+ if (answer.code !== 0) {
163
+ broken = `cbx-engine ${args[0]} failed: ${answer.stderr.trim().slice(0, 300)}`;
164
+ return null;
165
+ }
166
+ let parsed = null;
167
+ try {
168
+ parsed = JSON.parse(answer.stdout);
169
+ }
170
+ catch {
171
+ broken = `cbx-engine ${args[0]} gave an answer that is not JSON`;
172
+ return null;
173
+ }
174
+ if (parsed?.protocol !== PROTOCOL) {
175
+ broken = `engine at ${enginePath()} does not speak protocol ${PROTOCOL}`;
176
+ return null;
177
+ }
178
+ return parsed;
179
+ }
180
+ const progressFrom = (report) => report
181
+ ? (line) => {
182
+ try {
183
+ const parsed = JSON.parse(line);
184
+ report(parsed.done, parsed.total, parsed.path);
185
+ }
186
+ catch {
187
+ /* A line that is not progress; ignored. */
188
+ }
189
+ }
190
+ : undefined;
191
+ /**
192
+ * Store files into a "cbx-local 1" object store, as `storeFile` does for each.
193
+ *
194
+ * Null when the engine is not available or the run failed; a file the engine
195
+ * could not read comes back with `error`, for the caller's own path to retry
196
+ * and report. Objects are on disk (packs flushed) when this returns.
197
+ */
198
+ async function engineStore(objectsDirectory, root, files, progress, signal) {
199
+ const answer = await ask(["store", objectsDirectory, root, ...(progress ? ["--progress"] : [])], files, progressFrom(progress), signal);
200
+ if (!answer || !Array.isArray(answer.files) || answer.files.length !== files.length) {
201
+ if (answer)
202
+ broken = "cbx-engine store answered for the wrong number of files";
203
+ return null;
204
+ }
205
+ const byPath = new Map();
206
+ for (const one of answer.files)
207
+ byPath.set(one.path, one);
208
+ return byPath;
209
+ }
210
+ /**
211
+ * Digests of many files at once; with `count`, also lines and whether the file is binary,
212
+ * worked out as `measure` in core/worktree.ts does.
213
+ */
214
+ async function engineMeasure(root, files, progress, signal) {
215
+ const answer = await ask(["measure", root, ...(progress ? ["--progress"] : [])], files, progressFrom(progress), signal);
216
+ if (!answer || !Array.isArray(answer.files) || answer.files.length !== files.length) {
217
+ if (answer)
218
+ broken = "cbx-engine measure answered for the wrong number of files";
219
+ return null;
220
+ }
221
+ const byPath = new Map();
222
+ for (const one of answer.files)
223
+ byPath.set(one.path, one);
224
+ return byPath;
225
+ }
226
+ /**
227
+ * The files the ignore rules let into a save, found by the engine's own walk.
228
+ *
229
+ * Decided exactly as core/rules.ts and the walks in core/worktree.ts decide (the cross-test
230
+ * engine/compat/rules-cross.mjs compares them under many rule sets), returned in the order
231
+ * changedFiles meets them: breadth first, each folder in disk order. `rules` is what readRules
232
+ * gave; `tracked` what gitTracked gave. Each file's size, mtimeMs and mode come from one stat.
233
+ * Null when there is no engine or the walk failed, including on a rule JavaScript could not
234
+ * compile, so the TypeScript walk runs and fails in its own words.
235
+ */
236
+ async function engineScan(root, rules, tracked,
237
+ /** How many files the folder held last time, when known; a small folder is walked faster without a process. */
238
+ expectedFiles,
239
+ /**
240
+ * Stat every file (the default). False lets the engine use the size and time the folder
241
+ * listing already carries, which costs nothing per file on Windows; a file still open for
242
+ * writing can show its previous size and time there, so only read-only reports ask for it.
243
+ */
244
+ exact = true) {
245
+ if (!enginePath() || broken)
246
+ return null;
247
+ if (expectedFiles !== undefined && expectedFiles < exports.ENGINE_SCAN_MIN_FILES && process.env.CBX_ENGINE_ALWAYS !== "1")
248
+ return null;
249
+ if (process.env.CBX_ENGINE_SCAN === "off")
250
+ return null;
251
+ const answer = await ask(["scan"], {
252
+ root,
253
+ shared: rules.shared,
254
+ local: rules.local,
255
+ trackedFiles: tracked ? [...tracked.files] : null,
256
+ trackedFolders: tracked ? [...tracked.folders] : null,
257
+ exact,
258
+ });
259
+ if (!answer || answer.invalidRule || !Array.isArray(answer.files))
260
+ return null;
261
+ return answer.files;
262
+ }
263
+ /** For tests: forget what was found, so a changed CBX_ENGINE is read again. */
264
+ function resetEngineForTests() {
265
+ located = undefined;
266
+ broken = null;
267
+ }
@@ -427,6 +427,8 @@ async function aiAccess(repositoryId) {
427
427
  const allowed = (key) => ai[key] !== false;
428
428
  return {
429
429
  aiRead: allowed("read"),
430
+ /* The one answer that starts as no: a service that does not send it has not been asked. */
431
+ aiTrain: ai.train === true,
430
432
  aiDownload: allowed("download"),
431
433
  aiContribute: allowed("contribute"),
432
434
  aiRequest: allowed("request"),
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ /**
38
+ * The cbx command.
39
+ *
40
+ * `cbx status` in a folder with a local history is the command people run
41
+ * most, and it needs only that history: no sign-in, no network, none of the
42
+ * upload, project or workspace code. Answered here it loads a handful of
43
+ * modules instead of all of the CLI's, which was most of its time. It prints
44
+ * through the same function the full command table uses (local_status.ts).
45
+ *
46
+ * Everything else goes to cli.ts unchanged, and so does status whenever
47
+ * anything about it is out of the ordinary: other arguments, a Node that is
48
+ * too old, no local history here, or the unread-messages reminder being due.
49
+ */
50
+ const reminder_gate_js_1 = require("./reminder_gate.js");
51
+ async function quickStatus() {
52
+ const argv = process.argv.slice(2);
53
+ if (argv.length !== 1 || argv[0] !== "status")
54
+ return null;
55
+ const { nodeTooOld } = await Promise.resolve().then(() => __importStar(require("./node_check.js")));
56
+ if (nodeTooOld())
57
+ return null;
58
+ if (await (0, reminder_gate_js_1.reminderDue)("status"))
59
+ return null;
60
+ const { Repository } = await Promise.resolve().then(() => __importStar(require("../../cbx/src/local/repository.js")));
61
+ const repository = await Repository.find(process.cwd());
62
+ if (!repository)
63
+ return null;
64
+ const { printLocalStatus, red } = await Promise.resolve().then(() => __importStar(require("./local_status.js")));
65
+ try {
66
+ return await printLocalStatus(repository);
67
+ }
68
+ catch (error) {
69
+ /* What localStatus's wrapper prints for anything status itself can raise. */
70
+ console.error(red(error instanceof Error ? error.message : String(error)));
71
+ return 1;
72
+ }
73
+ }
74
+ quickStatus().then((code) => {
75
+ if (code === null) {
76
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
77
+ require("./cli.js").start();
78
+ }
79
+ else {
80
+ process.exitCode = code;
81
+ }
82
+ }, () => {
83
+ /* Anything unexpected here: the full command line answers instead. */
84
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
85
+ require("./cli.js").start();
86
+ });
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.REGISTRY = void 0;
8
+ exports.start = start;
8
9
  /**
9
10
  * cbx — the CBX engine from the command line, talking to CodeRook.
10
11
  *
@@ -33,6 +34,7 @@ const upload_progress_js_1 = require("./upload_progress.js");
33
34
  const publish_js_1 = require("./publish.js");
34
35
  const attach_command_js_1 = require("./attach_command.js");
35
36
  const build_command_js_1 = require("./build_command.js");
37
+ const update_command_js_1 = require("./update_command.js");
36
38
  const node_check_js_1 = require("./node_check.js");
37
39
  const pages_command_js_1 = require("./pages_command.js");
38
40
  const mods_command_js_1 = require("./mods_command.js");
@@ -501,7 +503,23 @@ async function commandStatus(parsed) {
501
503
  send, with the flag that would send them spelled out. The safe default
502
504
  does not change: a save adds and updates.
503
505
  */
504
- const found = await (0, worktree_js_1.changedFiles)(folder, rules, baseline, "synchronize");
506
+ /*
507
+ The digests submit keeps, so an unchanged file is not read again just to
508
+ be reported unchanged. Without them status read and hashed every file in
509
+ the folder each time it ran. What it learns is kept for the next run.
510
+ */
511
+ const knownDigests = link?.repositoryId
512
+ ? await (0, config_js_1.readDigests)(link.repositoryId)
513
+ : new Map();
514
+ let learned = 0;
515
+ const remember = knownDigests.set.bind(knownDigests);
516
+ knownDigests.set = (key, value) => {
517
+ learned += 1;
518
+ return remember(key, value);
519
+ };
520
+ const found = await (0, worktree_js_1.changedFiles)(folder, rules, baseline, "synchronize", knownDigests, undefined, false);
521
+ if (learned && link?.repositoryId)
522
+ await (0, config_js_1.writeDigests)(link.repositoryId, knownDigests);
505
523
  const gone = found.filter((file) => file.deleted);
506
524
  const files = found.filter((file) => !file.deleted);
507
525
  // Tells the project's Actions page what this machine's folder holds; never delays or fails status.
@@ -4050,6 +4068,23 @@ const SPECS = [
4050
4068
  examples: ["claude mcp add coderook -- cbx mcp"],
4051
4069
  run: parsed => parsed.flags.has("workspaces") ? (0, workspace_command_js_1.commandWorkspaceMcp)() : (0, mcp_js_1.commandMcp)(version_js_1.VERSION),
4052
4070
  },
4071
+ {
4072
+ name: "update",
4073
+ aliases: ["upgrade"],
4074
+ group: "Getting started",
4075
+ summary: "install the newest cbx",
4076
+ usage: "update",
4077
+ options: [
4078
+ { flags: "--check", description: "only say whether a newer cbx is out" },
4079
+ ],
4080
+ detail: "Updates cbx itself, not a folder: `cbx get` brings a project's files\n" +
4081
+ "up to date. Asks the npm registry for the newest @coderook/cli and\n" +
4082
+ "installs it the way this copy was installed: globally with npm, or in\n" +
4083
+ "the project whose packages hold it. Through npx there is nothing to\n" +
4084
+ "install. Your folders, sign-in and settings are left as they are.",
4085
+ examples: ["cbx update", "cbx update --check"],
4086
+ run: parsed => (0, update_command_js_1.commandUpdate)(parsed.flags),
4087
+ },
4053
4088
  {
4054
4089
  name: "doctor",
4055
4090
  group: "Getting started",
@@ -4199,7 +4234,8 @@ function leave(code) {
4199
4234
  command table — that every command is listed once, and fits — and must not
4200
4235
  find itself running a command with the test runner's own arguments.
4201
4236
  */
4202
- if (require.main === module) {
4237
+ /** Run the command line with this process's arguments. bin.ts calls this for everything it does not answer itself. */
4238
+ function start() {
4203
4239
  (0, network_js_1.tuneNetwork)();
4204
4240
  main(node_process_1.default.argv.slice(2))
4205
4241
  .then(leave)
@@ -4208,3 +4244,5 @@ if (require.main === module) {
4208
4244
  leave(1);
4209
4245
  });
4210
4246
  }
4247
+ if (require.main === module)
4248
+ start();