@coderook/cli 0.45.0 → 0.46.1

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,289 @@
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
+ /*
100
+ Whether the engine should open and read files itself (store, measure), as
101
+ opposed to only walking folders (scan).
102
+
103
+ Not on Windows, for now. With real-time protection on, which is nearly
104
+ everyone, Windows Security scans each new file when the unsigned engine
105
+ opens it and barely slows Node, a signed and widely known program. On a
106
+ fresh copy of 8,525 files the engine took 13–16 s to store what Node read
107
+ and stored in 3 s; with protection off the engine took 0.5 s. Until the
108
+ engine is signed, Windows reads files here and the engine keeps the folder
109
+ walk, which only lists and stats and is not scanned. CBX_ENGINE_READS=on
110
+ turns engine reads back on.
111
+ */
112
+ function engineMayRead() {
113
+ if (process.env.CBX_ENGINE_READS === "on")
114
+ return true;
115
+ if (process.env.CBX_ENGINE_READS === "off")
116
+ return false;
117
+ return process.platform !== "win32";
118
+ }
119
+ /** Whether a batch of this size is worth handing to the engine to read. */
120
+ function worthTheEngine(files, bytes) {
121
+ if (enginePath() === null || broken || files === 0)
122
+ return false;
123
+ /* CBX_ENGINE_ALWAYS=1 sends every batch, however small, so tests can run everything through it. */
124
+ if (process.env.CBX_ENGINE_ALWAYS === "1")
125
+ return true;
126
+ if (!engineMayRead())
127
+ return false;
128
+ return files >= exports.ENGINE_MIN_FILES || bytes >= exports.ENGINE_MIN_BYTES;
129
+ }
130
+ function run(args, input, onLine, signal) {
131
+ const binary = enginePath();
132
+ return new Promise((resolve) => {
133
+ let child;
134
+ try {
135
+ child = (0, node_child_process_1.spawn)(binary, args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
136
+ }
137
+ catch (error) {
138
+ resolve({ stdout: "", stderr: String(error), code: -1 });
139
+ return;
140
+ }
141
+ /* Cancelling the operation stops the engine with it. */
142
+ const stop = () => child.kill();
143
+ if (signal?.aborted)
144
+ stop();
145
+ signal?.addEventListener("abort", stop, { once: true });
146
+ child.on("close", () => signal?.removeEventListener("abort", stop));
147
+ const out = [];
148
+ let err = "";
149
+ let partial = "";
150
+ child.stdout.on("data", (data) => out.push(data));
151
+ child.stderr.on("data", (data) => {
152
+ const text = partial + data.toString("utf8");
153
+ const lines = text.split("\n");
154
+ partial = lines.pop() ?? "";
155
+ for (const line of lines) {
156
+ if (onLine && line.startsWith("{"))
157
+ onLine(line);
158
+ else
159
+ err += `${line}\n`;
160
+ }
161
+ });
162
+ child.on("error", (error) => resolve({ stdout: "", stderr: String(error), code: -1 }));
163
+ child.on("close", (code) => resolve({ stdout: Buffer.concat(out).toString("utf8"), stderr: err + partial, code }));
164
+ child.stdin.on("error", () => {
165
+ /* The engine exited before reading; its exit code says why. */
166
+ });
167
+ child.stdin.end(input ?? "");
168
+ });
169
+ }
170
+ /*
171
+ Every answer carries the protocol it speaks, so an engine from another
172
+ release is caught on its first answer without a separate process to ask.
173
+ */
174
+ function usable() {
175
+ return Boolean(enginePath()) && !broken;
176
+ }
177
+ async function ask(args, input, onLine, signal) {
178
+ if (!usable())
179
+ return null;
180
+ const answer = await run(args, JSON.stringify(input), onLine, signal);
181
+ /* Stopped on purpose is not a broken engine; the caller sees its own cancellation. */
182
+ if (signal?.aborted)
183
+ return null;
184
+ if (answer.code !== 0) {
185
+ broken = `cbx-engine ${args[0]} failed: ${answer.stderr.trim().slice(0, 300)}`;
186
+ return null;
187
+ }
188
+ let parsed = null;
189
+ try {
190
+ parsed = JSON.parse(answer.stdout);
191
+ }
192
+ catch {
193
+ broken = `cbx-engine ${args[0]} gave an answer that is not JSON`;
194
+ return null;
195
+ }
196
+ if (parsed?.protocol !== PROTOCOL) {
197
+ broken = `engine at ${enginePath()} does not speak protocol ${PROTOCOL}`;
198
+ return null;
199
+ }
200
+ return parsed;
201
+ }
202
+ const progressFrom = (report) => report
203
+ ? (line) => {
204
+ try {
205
+ const parsed = JSON.parse(line);
206
+ report(parsed.done, parsed.total, parsed.path);
207
+ }
208
+ catch {
209
+ /* A line that is not progress; ignored. */
210
+ }
211
+ }
212
+ : undefined;
213
+ /**
214
+ * Store files into a "cbx-local 1" object store, as `storeFile` does for each.
215
+ *
216
+ * Null when the engine is not available or the run failed; a file the engine
217
+ * could not read comes back with `error`, for the caller's own path to retry
218
+ * and report. Objects are on disk (packs flushed) when this returns.
219
+ */
220
+ async function engineStore(objectsDirectory, root, files, progress, signal) {
221
+ const answer = await ask(["store", objectsDirectory, root, ...(progress ? ["--progress"] : [])], files, progressFrom(progress), signal);
222
+ if (!answer || !Array.isArray(answer.files) || answer.files.length !== files.length) {
223
+ if (answer)
224
+ broken = "cbx-engine store answered for the wrong number of files";
225
+ return null;
226
+ }
227
+ const byPath = new Map();
228
+ for (const one of answer.files)
229
+ byPath.set(one.path, one);
230
+ return byPath;
231
+ }
232
+ /**
233
+ * Digests of many files at once; with `count`, also lines and whether the file is binary,
234
+ * worked out as `measure` in core/worktree.ts does.
235
+ */
236
+ async function engineMeasure(root, files, progress, signal) {
237
+ const answer = await ask(["measure", root, ...(progress ? ["--progress"] : [])], files, progressFrom(progress), signal);
238
+ if (!answer || !Array.isArray(answer.files) || answer.files.length !== files.length) {
239
+ if (answer)
240
+ broken = "cbx-engine measure answered for the wrong number of files";
241
+ return null;
242
+ }
243
+ const byPath = new Map();
244
+ for (const one of answer.files)
245
+ byPath.set(one.path, one);
246
+ return byPath;
247
+ }
248
+ /**
249
+ * The files the ignore rules let into a save, found by the engine's own walk.
250
+ *
251
+ * Decided exactly as core/rules.ts and the walks in core/worktree.ts decide (the cross-test
252
+ * engine/compat/rules-cross.mjs compares them under many rule sets), returned in the order
253
+ * changedFiles meets them: breadth first, each folder in disk order. `rules` is what readRules
254
+ * gave; `tracked` what gitTracked gave. Each file's size, mtimeMs and mode come from one stat.
255
+ * Null when there is no engine or the walk failed, including on a rule JavaScript could not
256
+ * compile, so the TypeScript walk runs and fails in its own words.
257
+ */
258
+ async function engineScan(root, rules, tracked,
259
+ /** How many files the folder held last time, when known; a small folder is walked faster without a process. */
260
+ expectedFiles,
261
+ /**
262
+ * Stat every file (the default). False lets the engine use the size and time the folder
263
+ * listing already carries, which costs nothing per file on Windows; a file still open for
264
+ * writing can show its previous size and time there, so only read-only reports ask for it.
265
+ */
266
+ exact = true) {
267
+ if (!enginePath() || broken)
268
+ return null;
269
+ if (expectedFiles !== undefined && expectedFiles < exports.ENGINE_SCAN_MIN_FILES && process.env.CBX_ENGINE_ALWAYS !== "1")
270
+ return null;
271
+ if (process.env.CBX_ENGINE_SCAN === "off")
272
+ return null;
273
+ const answer = await ask(["scan"], {
274
+ root,
275
+ shared: rules.shared,
276
+ local: rules.local,
277
+ trackedFiles: tracked ? [...tracked.files] : null,
278
+ trackedFolders: tracked ? [...tracked.folders] : null,
279
+ exact,
280
+ });
281
+ if (!answer || answer.invalidRule || !Array.isArray(answer.files))
282
+ return null;
283
+ return answer.files;
284
+ }
285
+ /** For tests: forget what was found, so a changed CBX_ENGINE is read again. */
286
+ function resetEngineForTests() {
287
+ located = undefined;
288
+ broken = null;
289
+ }
@@ -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
  *
@@ -502,7 +503,23 @@ async function commandStatus(parsed) {
502
503
  send, with the flag that would send them spelled out. The safe default
503
504
  does not change: a save adds and updates.
504
505
  */
505
- 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);
506
523
  const gone = found.filter((file) => file.deleted);
507
524
  const files = found.filter((file) => !file.deleted);
508
525
  // Tells the project's Actions page what this machine's folder holds; never delays or fails status.
@@ -4217,7 +4234,8 @@ function leave(code) {
4217
4234
  command table — that every command is listed once, and fits — and must not
4218
4235
  find itself running a command with the test runner's own arguments.
4219
4236
  */
4220
- 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() {
4221
4239
  (0, network_js_1.tuneNetwork)();
4222
4240
  main(node_process_1.default.argv.slice(2))
4223
4241
  .then(leave)
@@ -4226,3 +4244,5 @@ if (require.main === module) {
4226
4244
  leave(1);
4227
4245
  });
4228
4246
  }
4247
+ if (require.main === module)
4248
+ start();