@coderook/cli 0.1.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,626 @@
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.LOCAL_EXCLUDE_FILE = exports.IGNORE_FILE = exports.STARTER_IGNORE = exports.EVALUATION_FILE_LIMIT = void 0;
7
+ exports.isRepository = isRepository;
8
+ exports.migrateRetiredRules = migrateRetiredRules;
9
+ exports.readRules = readRules;
10
+ exports.writeRules = writeRules;
11
+ exports.collectLayers = collectLayers;
12
+ exports.assertReadable = assertReadable;
13
+ exports.branchName = branchName;
14
+ exports.changedFiles = changedFiles;
15
+ exports.totalSize = totalSize;
16
+ exports.fileDiff = fileDiff;
17
+ exports.projectTree = projectTree;
18
+ exports.evaluateRules = evaluateRules;
19
+ exports.detectSecrets = detectSecrets;
20
+ /** Reading a project folder: changed files, diffs, and rule measurement. */
21
+ const node_child_process_1 = require("node:child_process");
22
+ const node_crypto_1 = require("node:crypto");
23
+ const promises_1 = require("node:fs/promises");
24
+ const node_path_1 = __importDefault(require("node:path"));
25
+ const node_util_1 = require("node:util");
26
+ const rules_js_1 = require("./rules.js");
27
+ const run = (0, node_util_1.promisify)(node_child_process_1.execFile);
28
+ /** Past this the evaluation reports truncated rather than walking forever. */
29
+ exports.EVALUATION_FILE_LIMIT = 300_000;
30
+ async function git(root, ...args) {
31
+ try {
32
+ const { stdout } = await run("git", ["-C", root, ...args], {
33
+ maxBuffer: 64 * 1024 * 1024,
34
+ windowsHide: true,
35
+ });
36
+ return stdout;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * A starter template, offered rather than imposed: these lines are written
44
+ * into the project's own .gitignore, so nothing is hidden in the client.
45
+ *
46
+ * Credentials are deliberately absent. Silently omitting a configuration
47
+ * file would produce an incomplete project that looked backed up, so likely
48
+ * secrets are warned about instead (docs/UPLOAD_POLICY.md).
49
+ */
50
+ exports.STARTER_IGNORE = `# Dependencies and generated output
51
+ node_modules/
52
+ dist/
53
+ build/
54
+ out/
55
+ .next/
56
+ target/
57
+ __pycache__/
58
+ .venv/
59
+ venv/
60
+ *.log
61
+
62
+ # Large model weights
63
+ models/**
64
+ *.safetensors
65
+ *.ckpt
66
+ *.pt
67
+ *.pth
68
+ `;
69
+ /** The shared rules file, committed with the project. */
70
+ exports.IGNORE_FILE = ".gitignore";
71
+ /** Personal exclusions, which must never reach a collaborator. */
72
+ exports.LOCAL_EXCLUDE_FILE = node_path_1.default.join(".git", "info", "exclude");
73
+ /** The two files an earlier build wrote, kept only so they can be migrated. */
74
+ const RETIRED_IGNORE = ".coderookignore";
75
+ const RETIRED_KEEP = ".coderookkeep";
76
+ async function readIfPresent(full) {
77
+ try {
78
+ const contents = await (0, promises_1.readFile)(full, "utf8");
79
+ return contents.trim() ? contents : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ /** Whether the folder is a git repository, worktree or otherwise. */
86
+ async function isRepository(root) {
87
+ try {
88
+ await (0, promises_1.stat)(node_path_1.default.join(root, ".git"));
89
+ return true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ /**
96
+ * Convert the rules an earlier build wrote into one .gitignore.
97
+ *
98
+ * `.coderookkeep` held "always include" lines, which gitignore expresses as
99
+ * `!` negations, so those are translated rather than dropped. Nothing is
100
+ * deleted here; the retired files are removed only once the new rules have
101
+ * been written successfully.
102
+ */
103
+ function migrateRetiredRules(ignore, keep) {
104
+ if (!ignore && !keep)
105
+ return null;
106
+ const lines = [];
107
+ if (ignore)
108
+ lines.push(ignore.replace(/\s*$/, ""));
109
+ const negations = (keep ?? "")
110
+ .split(/\r?\n/)
111
+ .map((line) => line.trim())
112
+ .filter((line) => line && !line.startsWith("#"))
113
+ .map((line) => (line.startsWith("!") ? line : `!${line}`));
114
+ if (negations.length) {
115
+ lines.push("", "# Always include (migrated from .coderookkeep)", ...negations);
116
+ }
117
+ return `${lines.join("\n")}\n`;
118
+ }
119
+ async function readRules(root) {
120
+ const shared = await readIfPresent(node_path_1.default.join(root, exports.IGNORE_FILE));
121
+ const local = await readIfPresent(node_path_1.default.join(root, exports.LOCAL_EXCLUDE_FILE));
122
+ if (shared !== null)
123
+ return { shared, local: local ?? "" };
124
+ // No .gitignore yet. Anything an earlier build wrote is carried over, so a
125
+ // project that was already set up does not silently lose its rules.
126
+ const migrated = migrateRetiredRules(await readIfPresent(node_path_1.default.join(root, RETIRED_IGNORE)), await readIfPresent(node_path_1.default.join(root, RETIRED_KEEP)));
127
+ return { shared: migrated ?? exports.STARTER_IGNORE, local: local ?? "" };
128
+ }
129
+ /** Write the rules where the project, the CLI and git all expect them. */
130
+ async function writeRules(root, rules) {
131
+ await (0, promises_1.writeFile)(node_path_1.default.join(root, exports.IGNORE_FILE), rules.shared, "utf8");
132
+ if (rules.local.trim() && (await isRepository(root))) {
133
+ const target = node_path_1.default.join(root, exports.LOCAL_EXCLUDE_FILE);
134
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(target), { recursive: true });
135
+ await (0, promises_1.writeFile)(target, rules.local, "utf8");
136
+ }
137
+ // Only now that the real file exists are the retired ones removed.
138
+ for (const retired of [RETIRED_IGNORE, RETIRED_KEEP]) {
139
+ await (0, promises_1.rm)(node_path_1.default.join(root, retired), { force: true });
140
+ }
141
+ }
142
+ /**
143
+ * Every .gitignore that governs this tree, root first. Nested files apply to
144
+ * their own directory downwards and, being deeper, have the final say.
145
+ */
146
+ async function collectLayers(root, rules) {
147
+ const layers = [
148
+ // Personal exclusions sit at the root but must never be shared.
149
+ ...(rules.local.trim() ? [{ base: "", rules: (0, rules_js_1.parseRules)(rules.local) }] : []),
150
+ { base: "", rules: (0, rules_js_1.parseRules)(rules.shared) },
151
+ ];
152
+ const walk = async (directory, base) => {
153
+ let entries;
154
+ try {
155
+ entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
156
+ }
157
+ catch {
158
+ return;
159
+ }
160
+ for (const entry of entries) {
161
+ if ((0, rules_js_1.isUncounted)(entry.name) || entry.isSymbolicLink())
162
+ continue;
163
+ const relative = base ? `${base}/${entry.name}` : entry.name;
164
+ if (entry.isDirectory()) {
165
+ // A directory already excluded cannot contribute rules of its own.
166
+ if ((0, rules_js_1.excludes)(relative, true, layers))
167
+ continue;
168
+ await walk(node_path_1.default.join(directory, entry.name), relative);
169
+ }
170
+ else if (entry.name === exports.IGNORE_FILE && base) {
171
+ const contents = await readIfPresent(node_path_1.default.join(directory, entry.name));
172
+ if (contents)
173
+ layers.push({ base, rules: (0, rules_js_1.parseRules)(contents) });
174
+ }
175
+ }
176
+ };
177
+ await walk(root, "");
178
+ return layers;
179
+ }
180
+ /** Fail loudly on a folder that is gone, rather than reporting it as empty. */
181
+ async function assertReadable(root) {
182
+ try {
183
+ const info = await (0, promises_1.stat)(root);
184
+ if (!info.isDirectory())
185
+ throw new Error("not a folder");
186
+ }
187
+ catch {
188
+ throw new Error(`${root} is not a folder this machine can read`);
189
+ }
190
+ }
191
+ async function branchName(root) {
192
+ const output = await git(root, "branch", "--show-current");
193
+ return output?.trim() || "main";
194
+ }
195
+ /** Files bigger than this are listed but not read to count their lines. */
196
+ const LINE_COUNT_LIMIT = 4 * 1024 * 1024;
197
+ /** Enough rows for any real project; past it the rail would be unusable. */
198
+ const LISTED_FILE_LIMIT = 20_000;
199
+ /** How a file reads: line count, or binary, or too big to bother. */
200
+ async function measure(full, size) {
201
+ if (size > LINE_COUNT_LIMIT)
202
+ return { lines: 0, binary: true, hash: "" };
203
+ try {
204
+ const contents = await (0, promises_1.readFile)(full);
205
+ const binary = contents.subarray(0, 8192).includes(0);
206
+ return {
207
+ lines: binary ? 0 : contents.toString("utf8").split(/\r?\n/).length,
208
+ binary,
209
+ hash: (0, node_crypto_1.createHash)("sha256").update(contents).digest("hex"),
210
+ };
211
+ }
212
+ catch {
213
+ return { lines: 0, binary: true, hash: "" };
214
+ }
215
+ }
216
+ /**
217
+ * The files that would go into the next version.
218
+ *
219
+ * This deliberately does not ask git. A CodeRook version is not a commit:
220
+ * a folder that is not a repository at all still has everything to upload,
221
+ * and a repository with a spotlessly clean worktree still has everything to
222
+ * upload when no version has ever been saved. The filter rules decide what
223
+ * is a candidate; the baseline decides what is new.
224
+ */
225
+ async function changedFiles(root, rules, baseline = null, mode = "add-and-update") {
226
+ const layers = await collectLayers(root, rules);
227
+ const files = [];
228
+ const present = new Set();
229
+ const pending = [root];
230
+ while (pending.length && files.length < LISTED_FILE_LIMIT) {
231
+ const directory = pending.pop();
232
+ let entries;
233
+ try {
234
+ entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
235
+ }
236
+ catch {
237
+ continue;
238
+ }
239
+ for (const entry of entries) {
240
+ const full = node_path_1.default.join(directory, entry.name);
241
+ if (entry.isSymbolicLink())
242
+ continue;
243
+ if ((0, rules_js_1.isUncounted)(entry.name))
244
+ continue;
245
+ const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
246
+ if (entry.isDirectory()) {
247
+ // Skipping an excluded directory outright is what keeps this fast,
248
+ // but a `!` rule exists to rescue what is inside one, so the subtree
249
+ // is only skipped when no negation could reach into it.
250
+ if ((0, rules_js_1.excludes)(relative, true, layers) && !(0, rules_js_1.negationReachesInto)(relative, layers)) {
251
+ continue;
252
+ }
253
+ pending.push(full);
254
+ continue;
255
+ }
256
+ if (!entry.isFile())
257
+ continue;
258
+ if ((0, rules_js_1.excludes)(relative, false, layers))
259
+ continue;
260
+ let size;
261
+ try {
262
+ size = (await (0, promises_1.stat)(full)).size;
263
+ }
264
+ catch {
265
+ continue;
266
+ }
267
+ present.add(relative);
268
+ const measured = await measure(full, size);
269
+ const saved = baseline?.get(relative);
270
+ if (saved && measured.hash && saved === measured.hash)
271
+ continue;
272
+ files.push({
273
+ path: relative,
274
+ // Against a saved version the true line delta needs the old copy;
275
+ // until a version exists to fetch, a changed file counts as rewritten.
276
+ added: measured.lines,
277
+ removed: 0,
278
+ included: true,
279
+ binary: measured.binary,
280
+ });
281
+ }
282
+ }
283
+ // A file that was in the last version and is gone now is only a change in
284
+ // synchronize mode. The safe default adds and updates, and leaves the
285
+ // saved copy of a missing file alone (docs/UPLOAD_POLICY.md).
286
+ if (mode === "synchronize") {
287
+ for (const [saved] of baseline ?? []) {
288
+ if (present.has(saved))
289
+ continue;
290
+ files.push({
291
+ path: saved,
292
+ added: 0,
293
+ removed: 0,
294
+ included: true,
295
+ binary: false,
296
+ deleted: true,
297
+ });
298
+ }
299
+ }
300
+ return files.sort((left, right) => left.path.localeCompare(right.path));
301
+ }
302
+ /** The size on disk of everything the rules would upload. */
303
+ async function totalSize(root, files) {
304
+ let total = 0;
305
+ for (const file of files) {
306
+ try {
307
+ total += (await (0, promises_1.stat)(node_path_1.default.join(root, file.path))).size;
308
+ }
309
+ catch {
310
+ /* deleted since the scan; it contributes nothing */
311
+ }
312
+ }
313
+ return total;
314
+ }
315
+ /** The unified diff for one file, including files git does not track yet. */
316
+ async function fileDiff(root, file, ignoreWhitespace = false) {
317
+ const output = await git(root, "diff", "--no-ext-diff", "--unified=3", ...(ignoreWhitespace ? ["--ignore-all-space"] : []), "--", file);
318
+ const hunks = [];
319
+ let current = null;
320
+ let oldLine = 0;
321
+ let newLine = 0;
322
+ for (const raw of (output ?? "").split(/\r?\n/)) {
323
+ if (raw.startsWith("@@")) {
324
+ current = { header: raw, lines: [] };
325
+ hunks.push(current);
326
+ const match = /@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
327
+ if (match) {
328
+ oldLine = Number(match[1]);
329
+ newLine = Number(match[2]);
330
+ }
331
+ continue;
332
+ }
333
+ if (!current)
334
+ continue;
335
+ if (/^(diff |index |--- |\+\+\+ )/.test(raw))
336
+ continue;
337
+ if (raw.startsWith("+")) {
338
+ current.lines.push({
339
+ kind: "add",
340
+ number: newLine++,
341
+ oldNumber: null,
342
+ text: raw.slice(1),
343
+ });
344
+ }
345
+ else if (raw.startsWith("-")) {
346
+ current.lines.push({
347
+ kind: "del",
348
+ number: oldLine,
349
+ oldNumber: oldLine++,
350
+ text: raw.slice(1),
351
+ });
352
+ }
353
+ else if (raw.startsWith(" ")) {
354
+ current.lines.push({
355
+ kind: "ctx",
356
+ number: newLine,
357
+ oldNumber: oldLine,
358
+ text: raw.slice(1),
359
+ });
360
+ oldLine += 1;
361
+ newLine += 1;
362
+ }
363
+ }
364
+ if (hunks.length)
365
+ return hunks;
366
+ // Untracked files have no diff; show them as wholly added.
367
+ try {
368
+ const contents = await (0, promises_1.readFile)(node_path_1.default.join(root, file));
369
+ if (contents.subarray(0, 8192).includes(0)) {
370
+ return [{ header: `Binary file added: ${file}`, lines: [] }];
371
+ }
372
+ const lines = contents.toString("utf8").split(/\r?\n/);
373
+ return [
374
+ {
375
+ header: `@@ -0,0 +1,${lines.length} @@ ${file}`,
376
+ lines: lines.slice(0, 10_000).map((text, index) => ({
377
+ kind: "add",
378
+ number: index + 1,
379
+ oldNumber: null,
380
+ text,
381
+ })),
382
+ },
383
+ ];
384
+ }
385
+ catch {
386
+ return [{ header: `No textual diff is available for ${file}`, lines: [] }];
387
+ }
388
+ }
389
+ /**
390
+ * The folder as a tree, so filtering can be done by ticking things rather
391
+ * than by writing glob patterns. Sizes are rolled up, because the only way
392
+ * to decide whether to keep a folder is to know what it costs.
393
+ */
394
+ async function projectTree(root, limit = exports.EVALUATION_FILE_LIMIT) {
395
+ let seen = 0;
396
+ const walk = async (directory, relative) => {
397
+ const node = {
398
+ path: relative,
399
+ name: relative ? relative.slice(relative.lastIndexOf("/") + 1) : ".",
400
+ directory: true,
401
+ size: 0,
402
+ files: 0,
403
+ children: [],
404
+ };
405
+ let entries;
406
+ try {
407
+ entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
408
+ }
409
+ catch {
410
+ return node;
411
+ }
412
+ for (const entry of entries) {
413
+ if (entry.isSymbolicLink())
414
+ continue;
415
+ if ((0, rules_js_1.isUncounted)(entry.name))
416
+ continue;
417
+ const full = node_path_1.default.join(directory, entry.name);
418
+ const childPath = relative ? `${relative}/${entry.name}` : entry.name;
419
+ if (entry.isDirectory()) {
420
+ const child = await walk(full, childPath);
421
+ node.children.push(child);
422
+ node.size += child.size;
423
+ node.files += child.files;
424
+ continue;
425
+ }
426
+ if (!entry.isFile())
427
+ continue;
428
+ seen += 1;
429
+ if (seen > limit) {
430
+ node.truncated = true;
431
+ break;
432
+ }
433
+ let size = 0;
434
+ try {
435
+ size = (await (0, promises_1.stat)(full)).size;
436
+ }
437
+ catch {
438
+ continue;
439
+ }
440
+ node.children.push({
441
+ path: childPath,
442
+ name: entry.name,
443
+ directory: false,
444
+ size,
445
+ files: 1,
446
+ });
447
+ node.size += size;
448
+ node.files += 1;
449
+ }
450
+ // Folders first, then files, each alphabetically — the familiar order.
451
+ node.children.sort((left, right) => left.directory === right.directory
452
+ ? left.name.localeCompare(right.name)
453
+ : left.directory
454
+ ? -1
455
+ : 1);
456
+ return node;
457
+ };
458
+ return walk(root, "");
459
+ }
460
+ /**
461
+ * Measure what the supplied rules would upload, skip and force-keep.
462
+ *
463
+ * This deliberately walks into ignored directories, because the filtering
464
+ * screen has to be able to say what each rule costs before it is accepted.
465
+ */
466
+ async function evaluateRules(root, rules, options = {}) {
467
+ const layers = await collectLayers(root, rules);
468
+ const limit = options.limit ?? exports.EVALUATION_FILE_LIMIT;
469
+ const impactBytes = new Map();
470
+ const impactFiles = new Map();
471
+ const excluded = [];
472
+ let uploadFiles = 0;
473
+ let uploadBytes = 0;
474
+ let skippedFiles = 0;
475
+ let skippedBytes = 0;
476
+ let keptFiles = 0;
477
+ let keptBytes = 0;
478
+ let seen = 0;
479
+ let truncated = false;
480
+ const pending = [root];
481
+ while (pending.length) {
482
+ if (options.shouldStop?.())
483
+ return emptyEvaluation(true);
484
+ const directory = pending.pop();
485
+ let entries;
486
+ try {
487
+ entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
488
+ }
489
+ catch {
490
+ continue;
491
+ }
492
+ for (const entry of entries) {
493
+ if (options.shouldStop?.())
494
+ return emptyEvaluation(true);
495
+ const full = node_path_1.default.join(directory, entry.name);
496
+ if (entry.isSymbolicLink())
497
+ continue;
498
+ if ((0, rules_js_1.isUncounted)(entry.name))
499
+ continue;
500
+ if (entry.isDirectory()) {
501
+ pending.push(full);
502
+ continue;
503
+ }
504
+ if (!entry.isFile())
505
+ continue;
506
+ seen += 1;
507
+ if (seen > limit) {
508
+ truncated = true;
509
+ pending.length = 0;
510
+ break;
511
+ }
512
+ let size;
513
+ try {
514
+ size = (await (0, promises_1.stat)(full)).size;
515
+ }
516
+ catch {
517
+ continue;
518
+ }
519
+ const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
520
+ // The last rule to speak wins, across every .gitignore that governs
521
+ // this path. A `!` rule brings the file back and is worth reporting,
522
+ // because it is how "always include" is expressed.
523
+ const rule = (0, rules_js_1.decide)(relative, false, layers);
524
+ if (rule?.negated) {
525
+ keptFiles += 1;
526
+ keptBytes += size;
527
+ uploadFiles += 1;
528
+ uploadBytes += size;
529
+ excluded.push({
530
+ path: relative,
531
+ bytes: size,
532
+ rule: `!${rule.pattern}`,
533
+ forceKept: true,
534
+ });
535
+ continue;
536
+ }
537
+ if (rule) {
538
+ skippedFiles += 1;
539
+ skippedBytes += size;
540
+ impactBytes.set(rule.pattern, (impactBytes.get(rule.pattern) ?? 0) + size);
541
+ impactFiles.set(rule.pattern, (impactFiles.get(rule.pattern) ?? 0) + 1);
542
+ excluded.push({
543
+ path: relative,
544
+ bytes: size,
545
+ rule: rule.pattern,
546
+ forceKept: false,
547
+ });
548
+ continue;
549
+ }
550
+ uploadFiles += 1;
551
+ uploadBytes += size;
552
+ }
553
+ }
554
+ excluded.sort((left, right) => right.bytes - left.bytes);
555
+ const impacts = [...impactBytes.entries()]
556
+ .map(([pattern, bytes]) => ({
557
+ pattern,
558
+ bytes,
559
+ files: impactFiles.get(pattern) ?? 0,
560
+ }))
561
+ .sort((left, right) => right.bytes - left.bytes);
562
+ return {
563
+ uploadFiles,
564
+ uploadBytes,
565
+ skippedFiles,
566
+ skippedBytes,
567
+ keptFiles,
568
+ keptBytes,
569
+ impacts,
570
+ excluded: excluded.slice(0, 400),
571
+ truncated,
572
+ };
573
+ }
574
+ function emptyEvaluation(truncated) {
575
+ return {
576
+ uploadFiles: 0,
577
+ uploadBytes: 0,
578
+ skippedFiles: 0,
579
+ skippedBytes: 0,
580
+ keptFiles: 0,
581
+ keptBytes: 0,
582
+ impacts: [],
583
+ excluded: [],
584
+ truncated,
585
+ };
586
+ }
587
+ const SECRET_NAMES = new Set([".env", ".env.local", ".env.production"]);
588
+ const SECRET_SUFFIXES = [".pem", ".key", ".p12", ".pfx"];
589
+ /** Files the flow must ask about before the first upload. */
590
+ async function detectSecrets(root) {
591
+ const found = [];
592
+ const pending = [root];
593
+ let seen = 0;
594
+ while (pending.length && found.length < 50) {
595
+ const directory = pending.pop();
596
+ let entries;
597
+ try {
598
+ entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
599
+ }
600
+ catch {
601
+ continue;
602
+ }
603
+ for (const entry of entries) {
604
+ if (seen > 40_000)
605
+ return found;
606
+ const full = node_path_1.default.join(directory, entry.name);
607
+ if ((0, rules_js_1.isUncounted)(entry.name))
608
+ continue;
609
+ if (entry.isDirectory()) {
610
+ if (entry.name !== "node_modules")
611
+ pending.push(full);
612
+ continue;
613
+ }
614
+ seen += 1;
615
+ const name = entry.name.toLowerCase();
616
+ const isSecret = SECRET_NAMES.has(name) ||
617
+ name.startsWith(".env.") ||
618
+ SECRET_SUFFIXES.some((suffix) => name.endsWith(suffix)) ||
619
+ node_path_1.default.relative(root, directory).split(node_path_1.default.sep).includes("secrets");
620
+ if (isSecret) {
621
+ found.push(node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/"));
622
+ }
623
+ }
624
+ }
625
+ return found;
626
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ /** Types shared by the Electron main process and the renderer. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@coderook/cli",
3
+ "version": "0.1.0",
4
+ "description": "CodeRook from the command line, on any operating system",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "homepage": "https://coderook.com",
7
+ "bugs": {
8
+ "url": "https://coderook.com/contact"
9
+ },
10
+ "keywords": [
11
+ "coderook",
12
+ "versioning",
13
+ "backup",
14
+ "cbx"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20.11.0"
18
+ },
19
+ "bin": {
20
+ "coderook": "dist/cli/src/cli.js"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "check": "tsc -p tsconfig.json --noEmit",
28
+ "test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
29
+ "start": "node dist/cli/src/cli.js",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "24.10.1",
34
+ "typescript": "5.9.3"
35
+ }
36
+ }