@coderook/cli 0.1.0 → 0.3.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.
@@ -4,6 +4,11 @@ exports.whoami = whoami;
4
4
  exports.projects = projects;
5
5
  exports.findProject = findProject;
6
6
  exports.health = health;
7
+ exports.mergeTracks = mergeTracks;
8
+ exports.mergeTrack = mergeTrack;
9
+ exports.resolveMergeConflict = resolveMergeConflict;
10
+ exports.applyMerge = applyMerge;
11
+ exports.cancelMerge = cancelMerge;
7
12
  /** The small part of the API the command-line tool needs directly. */
8
13
  const config_js_1 = require("./config.js");
9
14
  async function call(route, options = {}) {
@@ -65,3 +70,22 @@ async function health() {
65
70
  });
66
71
  return (await response.json());
67
72
  }
73
+ async function mergeTracks(repositoryId) {
74
+ const body = await call(`/v1/repositories/${repositoryId}/merge-tracks`);
75
+ return body.mergeTracks;
76
+ }
77
+ async function mergeTrack(id) {
78
+ return call(`/v1/merge-tracks/${id}`);
79
+ }
80
+ async function resolveMergeConflict(mergeTrackId, conflictId, resolution) {
81
+ await call(`/v1/merge-tracks/${mergeTrackId}/conflicts/${conflictId}`, {
82
+ method: "PUT",
83
+ body: { resolution },
84
+ });
85
+ }
86
+ async function applyMerge(mergeTrackId) {
87
+ return call(`/v1/merge-tracks/${mergeTrackId}/apply`, { method: "POST" });
88
+ }
89
+ async function cancelMerge(mergeTrackId) {
90
+ await call(`/v1/merge-tracks/${mergeTrackId}/cancel`, { method: "POST" });
91
+ }
@@ -18,13 +18,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  const promises_1 = require("node:readline/promises");
19
19
  const node_path_1 = __importDefault(require("node:path"));
20
20
  const node_process_1 = __importDefault(require("node:process"));
21
+ const api_js_1 = require("./api.js");
21
22
  const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
22
23
  const upload_js_1 = require("../../desktop-app/src/main/upload.js");
23
24
  const download_js_1 = require("../../desktop-app/src/main/download.js");
24
25
  const cbx_js_1 = require("../../desktop-app/src/main/cbx.js");
25
- const api_js_1 = require("./api.js");
26
+ const api_js_2 = require("./api.js");
26
27
  const config_js_1 = require("./config.js");
27
- const VERSION = "0.1.0";
28
+ /*
29
+ Read from the package rather than written twice. A hardcoded copy had
30
+ already drifted from the published version, which makes `coderook doctor`
31
+ worse than useless when working out what somebody is actually running.
32
+ */
33
+ const VERSION = (() => {
34
+ try {
35
+ return (require("../../../package.json").version ?? "0.0.0");
36
+ }
37
+ catch {
38
+ return "0.0.0";
39
+ }
40
+ })();
28
41
  // ── presentation ────────────────────────────────────────────────────────────
29
42
  const colour = node_process_1.default.stdout.isTTY && !node_process_1.default.env.NO_COLOR;
30
43
  const dim = (text) => (colour ? `${text}` : text);
@@ -94,20 +107,51 @@ const folderFor = (parsed, index = 0) => node_path_1.default.resolve(parsed.posi
94
107
  * what that repository already holds. The account is the authority, exactly
95
108
  * as it is in the desktop application.
96
109
  */
110
+ /**
111
+ * Work out what this folder is based on, and whether the account has moved.
112
+ *
113
+ * The base is the version this folder was last brought level with — not
114
+ * whatever happens to be newest. Silently re-basing on the newest is what
115
+ * makes a folder look up to date when somebody else has published: the
116
+ * changes are then measured against their work, and saving quietly lays
117
+ * this machine's copy over theirs.
118
+ *
119
+ * So a folder that has fallen behind keeps its own base and is told, which
120
+ * is what lets the service merge the two rather than pick a winner.
121
+ */
97
122
  async function reconcile(folder, options = {}) {
98
123
  const link = await (0, config_js_1.readLink)(folder);
99
124
  const reference = link?.slug ?? node_path_1.default.basename(folder);
100
125
  const project = link
101
- ? (await (0, api_js_1.projects)()).find((candidate) => candidate.id === link.repositoryId)
102
- : await (0, api_js_1.findProject)(reference);
126
+ ? (await (0, api_js_2.projects)()).find((candidate) => candidate.id === link.repositoryId)
127
+ : await (0, api_js_2.findProject)(reference);
103
128
  if (!project?.versionCount) {
104
129
  // Nothing saved on the account, so everything here is outstanding.
105
- return { link: link ?? null, baseline: null };
130
+ return { link: link ?? null, baseline: null, behind: null };
106
131
  }
107
132
  const downloader = new download_js_1.Downloader(config_js_1.credentials);
108
133
  const latest = (await downloader.versions(project.id))[0];
109
134
  if (!latest)
110
- return { link: link ?? null, baseline: null };
135
+ return { link: link ?? null, baseline: null, behind: null };
136
+ /*
137
+ A folder that already knows which version it stands on keeps it. Only a
138
+ folder that has never been reconciled — or was linked before versions
139
+ were recorded — adopts the newest as its starting point.
140
+ */
141
+ const known = link?.versionId && link.manifest ? link : null;
142
+ const behind = known && known.versionId !== latest.id
143
+ ? { sequence: latest.sequence, id: latest.id }
144
+ : null;
145
+ if (known) {
146
+ if (behind && !options.quiet) {
147
+ console.log(dim(`The account is on v${behind.sequence}; this folder is based on v${known.sequence}.`));
148
+ }
149
+ return {
150
+ link: known,
151
+ baseline: new Map(Object.entries(known.manifest)),
152
+ behind,
153
+ };
154
+ }
111
155
  const baseline = new Map();
112
156
  for (const file of await downloader.files(project.id, latest.id)) {
113
157
  baseline.set(file.path, file.sha256);
@@ -116,13 +160,14 @@ async function reconcile(folder, options = {}) {
116
160
  repositoryId: project.id,
117
161
  slug: project.slug,
118
162
  sequence: latest.sequence,
163
+ versionId: latest.id,
119
164
  manifest: Object.fromEntries(baseline),
120
165
  };
121
166
  await (0, config_js_1.writeLink)(folder, fresh);
122
167
  if (!options.quiet && !link) {
123
168
  console.log(dim(`Linked this folder to ${project.slug}.`));
124
169
  }
125
- return { link: fresh, baseline };
170
+ return { link: fresh, baseline, behind: null };
126
171
  }
127
172
  // ── commands ────────────────────────────────────────────────────────────────
128
173
  async function commandSignIn(parsed) {
@@ -140,7 +185,7 @@ async function commandSignIn(parsed) {
140
185
  }
141
186
  await (0, config_js_1.storeToken)(token);
142
187
  try {
143
- const account = await (0, api_js_1.whoami)();
188
+ const account = await (0, api_js_2.whoami)();
144
189
  console.log(`Signed in as ${bold(account.displayName)} ${dim(`<${account.email}>`)} · ${account.plan}`);
145
190
  console.log(dim(`Token stored in ${(0, config_js_1.configDirectory)()}`));
146
191
  return 0;
@@ -157,12 +202,12 @@ async function commandSignOut() {
157
202
  return 0;
158
203
  }
159
204
  async function commandWhoami() {
160
- const account = await (0, api_js_1.whoami)();
205
+ const account = await (0, api_js_2.whoami)();
161
206
  console.log(`${bold(account.displayName)} <${account.email}> · ${account.plan}`);
162
207
  return 0;
163
208
  }
164
209
  async function commandProjects() {
165
- const all = await (0, api_js_1.projects)();
210
+ const all = await (0, api_js_2.projects)();
166
211
  if (!all.length) {
167
212
  console.log("No projects on this account yet.");
168
213
  return 0;
@@ -206,6 +251,31 @@ async function commandSubmit(parsed) {
206
251
  console.log("Nothing to submit; this folder matches the saved version.");
207
252
  return 0;
208
253
  }
254
+ /*
255
+ The policy is that credentials are never quietly dropped and never
256
+ quietly sent — they are asked about. The desktop application has always
257
+ put this question in front of the first upload; the command line was
258
+ sending them without a word, which is the worse half of the two
259
+ behaviours the policy exists to prevent.
260
+ */
261
+ const secrets = await (0, worktree_js_1.detectSecrets)(folder);
262
+ const sending = new Set(files.map((file) => file.path));
263
+ const exposed = secrets.filter((secret) => sending.has(secret));
264
+ if (exposed.length) {
265
+ console.log(red(`\n${exposed.length} file${exposed.length === 1 ? " looks like a credential" : "s look like credentials"}:`));
266
+ for (const secret of exposed.slice(0, 20))
267
+ console.log(` ${secret}`);
268
+ if (exposed.length > 20) {
269
+ console.log(dim(` …and ${exposed.length - 20} more`));
270
+ }
271
+ if (!hasFlag(parsed, "allow-secrets")) {
272
+ console.error(`\nNothing was sent. Add them to ${accent(".gitignore")} to leave them` +
273
+ ` behind, or pass ${accent("--allow-secrets")} if they genuinely belong` +
274
+ ` in the project.`);
275
+ return 1;
276
+ }
277
+ console.log(dim("Sending them anyway, because --allow-secrets was given."));
278
+ }
209
279
  if (hasFlag(parsed, "dry-run", "n")) {
210
280
  console.log(`${files.length} file${files.length === 1 ? "" : "s"} would be sent:`);
211
281
  for (const file of files)
@@ -214,21 +284,66 @@ async function commandSubmit(parsed) {
214
284
  }
215
285
  const line = progressLine();
216
286
  const uploader = new upload_js_1.Uploader(config_js_1.credentials);
217
- const result = await uploader.run({
218
- localPath: folder,
219
- include: files.map((file) => file.path),
220
- message,
221
- projectName: link?.slug ?? node_path_1.default.basename(folder),
222
- repositoryId: link?.repositoryId ?? null,
223
- }, (progress) => {
224
- line(` ${String(progress.percent).padStart(3)}% ${progress.stage.padEnd(7)} ` +
225
- `${progress.files}/${progress.totalFiles} ${progress.path.slice(-48)}`);
226
- });
287
+ let result;
288
+ try {
289
+ result = await uploader.run({
290
+ localPath: folder,
291
+ include: files.map((file) => file.path),
292
+ message,
293
+ projectName: link?.slug ?? node_path_1.default.basename(folder),
294
+ repositoryId: link?.repositoryId ?? null,
295
+ /*
296
+ Only claimed when this folder has actually been reconciled with a
297
+ known version. A folder linked before versions were recorded says
298
+ nothing rather than guessing, and publishes as it always did.
299
+ */
300
+ ...(link?.versionId ? { expectedHeadVersionId: link.versionId } : {}),
301
+ }, (progress) => {
302
+ line(` ${String(progress.percent).padStart(3)}% ${progress.stage.padEnd(7)} ` +
303
+ `${progress.files}/${progress.totalFiles} ${progress.path.slice(-48)}`);
304
+ });
305
+ }
306
+ catch (error) {
307
+ done(line);
308
+ const code = error.code;
309
+ if (code === "merge_required" || code === "track_moved") {
310
+ /*
311
+ Somebody else saved to this project first. Everything that did not
312
+ overlap has already been combined by the service; what is left is a
313
+ genuine decision, so the answer is to bring their work down and look
314
+ at it rather than to try again harder.
315
+ */
316
+ console.error(red(error instanceof Error ? error.message : String(error)));
317
+ console.error(`\nRun ${accent("coderook get")} to bring the latest version down, then` +
318
+ ` submit again. Nothing was changed on your account.`);
319
+ return 1;
320
+ }
321
+ throw error;
322
+ }
227
323
  done(line);
324
+ if (result.mergeTrack) {
325
+ /*
326
+ The upload is complete and kept, but the project has not changed —
327
+ calling this "saved" would be false, and the base is deliberately left
328
+ alone so the folder still knows what it was working from.
329
+ */
330
+ console.log(`\nSomebody else saved first, and ${result.mergeTrack.conflicts.length === 1
331
+ ? "one file overlaps"
332
+ : `${result.mergeTrack.conflicts.length} files overlap`}.`);
333
+ console.log(`Your work is complete and kept as ${accent(result.mergeTrack.reference)}:`);
334
+ for (const conflict of result.mergeTrack.conflicts.slice(0, 20)) {
335
+ console.log(` ${red(conflict.path)} ${dim(conflict.kind)}`);
336
+ }
337
+ console.log(`\nRun ${accent(`coderook merge ${result.mergeTrack.reference}`)} to decide,` +
338
+ ` or ${accent("coderook merges")} to see everything waiting.`);
339
+ return 0;
340
+ }
228
341
  await (0, config_js_1.writeLink)(folder, {
229
342
  repositoryId: result.repositoryId,
230
343
  slug: link?.slug ?? node_path_1.default.basename(folder),
231
344
  sequence: result.sequence,
345
+ // What this folder is now working from, so the next submit can say so.
346
+ versionId: result.versionId,
232
347
  manifest: result.manifest,
233
348
  });
234
349
  console.log(`Saved ${accent(`v${result.sequence}`)} · sent ${bytes(result.sentBytes)} in ` +
@@ -252,7 +367,7 @@ async function commandClone(parsed) {
252
367
  console.error(red("Which project? Try: coderook clone <project>"));
253
368
  return 1;
254
369
  }
255
- const project = await (0, api_js_1.findProject)(reference);
370
+ const project = await (0, api_js_2.findProject)(reference);
256
371
  if (!project) {
257
372
  console.error(red(`No project named ${reference} on this account.`));
258
373
  return 1;
@@ -277,10 +392,12 @@ async function fetchInto(repositoryId, destination, slug) {
277
392
  ` ${progress.path.slice(-48)}`);
278
393
  });
279
394
  done(line);
395
+ // Fetching is how a folder catches up, so this is what moves its base.
280
396
  await (0, config_js_1.writeLink)(destination, {
281
397
  repositoryId,
282
398
  slug,
283
399
  sequence: latest.sequence,
400
+ versionId: latest.id,
284
401
  manifest: result.manifest,
285
402
  });
286
403
  console.log(`${accent(`v${latest.sequence}`)} · ${result.files} files · ${bytes(result.bytes)} into ${destination}`);
@@ -355,7 +472,7 @@ async function commandInspect(parsed) {
355
472
  return 0;
356
473
  }
357
474
  async function commandDoctor() {
358
- const service = await (0, api_js_1.health)().catch(() => null);
475
+ const service = await (0, api_js_2.health)().catch(() => null);
359
476
  console.log(`CodeRook CLI ${VERSION} · Node ${node_process_1.default.versions.node} · ${node_process_1.default.platform}`);
360
477
  console.log(`Config: ${(0, config_js_1.configDirectory)()}`);
361
478
  console.log(service
@@ -368,7 +485,7 @@ async function commandDoctor() {
368
485
  return 0;
369
486
  }
370
487
  try {
371
- const account = await (0, api_js_1.whoami)();
488
+ const account = await (0, api_js_2.whoami)();
372
489
  console.log(`Account: ${account.email} · ${account.plan}`);
373
490
  }
374
491
  catch (error) {
@@ -390,6 +507,10 @@ ${bold("Working with a folder")}
390
507
  coderook clone <project> [dir] Fetch a project into a new folder
391
508
  coderook rules [folder] Show the ignore rules (--init to start one)
392
509
 
510
+ ${bold("When somebody saved first")}
511
+ coderook merges Uploads of yours waiting on a decision
512
+ coderook merge <ref> Look at one (--mine --theirs --both --drop)
513
+
393
514
  ${bold("Bundles")}
394
515
  coderook bundle [folder] [out] Pack the project as a .cbx
395
516
  coderook unbundle <file> [dir] Extract a .cbx
@@ -402,10 +523,132 @@ ${bold("Other")}
402
523
  ${bold("Options")}
403
524
  -m, --message The version message for submit
404
525
  -n, --dry-run Show what submit would send, without sending
526
+ --allow-secrets Send files that look like credentials anyway
405
527
  --token Supply the token to sign-in instead of being asked
406
528
 
407
529
  ${dim("The environment variable CODEROOK_TOKEN is used when set, so automated")}
408
530
  ${dim("runs need nothing on disk. CODEROOK_API_URL points at another service.")}`;
531
+ /**
532
+ * Everything waiting on a decision, for one folder's project.
533
+ *
534
+ * A diverted upload is easy to forget about — it is not an error and the
535
+ * project looks untouched — so this is the way to find out that work of
536
+ * yours is sitting somewhere, still complete, waiting.
537
+ */
538
+ async function commandMerges(parsed) {
539
+ const folder = folderFor(parsed);
540
+ const link = await (0, config_js_1.readLink)(folder);
541
+ if (!link) {
542
+ console.error(red("This folder is not linked to a project on your account."));
543
+ return 1;
544
+ }
545
+ const waiting = (await (0, api_js_1.mergeTracks)(link.repositoryId)).filter((merge) => merge.state === "open");
546
+ if (!waiting.length) {
547
+ console.log("Nothing is waiting to be merged.");
548
+ return 0;
549
+ }
550
+ for (const merge of waiting) {
551
+ const counts = merge.conflicts;
552
+ console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
553
+ }
554
+ console.log(dim(`
555
+ Run coderook merge <reference> to look at one.`));
556
+ return 0;
557
+ }
558
+ /** Find a merge by the reference a person would type, such as M-2. */
559
+ async function findMerge(folder, reference) {
560
+ const link = await (0, config_js_1.readLink)(folder);
561
+ if (!link)
562
+ throw new Error("This folder is not linked to a project on your account.");
563
+ const all = await (0, api_js_1.mergeTracks)(link.repositoryId);
564
+ const found = all.find((merge) => merge.reference.toLowerCase() === reference.toLowerCase());
565
+ if (!found)
566
+ throw new Error(`No merge called ${reference} on this project.`);
567
+ return found;
568
+ }
569
+ /**
570
+ * Look at one merge, and optionally finish it.
571
+ *
572
+ * Deciding happens here rather than in a command of its own because the
573
+ * decision only means anything next to the thing it is about.
574
+ */
575
+ async function commandMerge(parsed) {
576
+ const reference = parsed.positional[0];
577
+ if (!reference) {
578
+ console.error(red("Which merge? Try: coderook merge M-1"));
579
+ return 1;
580
+ }
581
+ const folder = folderFor({ ...parsed, positional: parsed.positional.slice(1) });
582
+ const summary = await findMerge(folder, reference);
583
+ const detail = await (0, api_js_1.mergeTrack)(summary.id);
584
+ if (hasFlag(parsed, "cancel")) {
585
+ await (0, api_js_1.cancelMerge)(summary.id);
586
+ console.log(`${summary.reference} cancelled. Your upload is still stored and nothing published was touched.`);
587
+ return 0;
588
+ }
589
+ const outstanding = detail.conflicts.filter((conflict) => !conflict.resolvedAt);
590
+ const decision = hasFlag(parsed, "mine")
591
+ ? "take_candidate"
592
+ : hasFlag(parsed, "theirs")
593
+ ? "take_target"
594
+ : hasFlag(parsed, "drop")
595
+ ? "delete"
596
+ : hasFlag(parsed, "both", "keep-both")
597
+ ? "keep_both"
598
+ : null;
599
+ if (decision) {
600
+ const only = flagText(parsed, "path");
601
+ const chosen = only
602
+ ? outstanding.filter((conflict) => conflict.path === only)
603
+ : outstanding;
604
+ if (!chosen.length) {
605
+ console.error(red(only ? `${only} has no outstanding decision.` : "Nothing left to decide."));
606
+ return 1;
607
+ }
608
+ for (const conflict of chosen) {
609
+ await (0, api_js_1.resolveMergeConflict)(summary.id, conflict.id, decision);
610
+ console.log(` ${conflict.path} → ${decision === "take_candidate"
611
+ ? "yours"
612
+ : decision === "take_target"
613
+ ? "theirs"
614
+ : decision === "keep_both"
615
+ ? `both, yours saved beside it`
616
+ : "removed"}`);
617
+ }
618
+ }
619
+ const now = await (0, api_js_1.mergeTrack)(summary.id);
620
+ console.log(`
621
+ ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
622
+ (now.provisional.ready
623
+ ? "ready to apply"
624
+ : `${now.provisional.unresolvedPaths.length} still to decide`));
625
+ for (const conflict of now.conflicts) {
626
+ console.log(` ${conflict.resolvedAt ? accent("decided") : red("waiting")} ${conflict.path}` +
627
+ ` ${dim(conflict.kind)}${conflict.resolution ? dim(` (${conflict.resolution})`) : ""}`);
628
+ }
629
+ if (hasFlag(parsed, "apply")) {
630
+ if (!now.provisional.ready) {
631
+ console.error(red("\nStill undecided files; nothing was applied."));
632
+ return 1;
633
+ }
634
+ const applied = await (0, api_js_1.applyMerge)(summary.id);
635
+ console.log(`
636
+ Applied as ${accent(`v${applied.version.sequence}`)}.`);
637
+ console.log(dim("Run coderook get to bring it down to this folder."));
638
+ return 0;
639
+ }
640
+ if (!decision) {
641
+ console.log(dim(`
642
+ --mine keeps yours, --theirs keeps what was already saved,` +
643
+ ` --drop removes the file.
644
+ Add --path <file> for one file, then --apply when ready.`));
645
+ }
646
+ else if (now.provisional.ready) {
647
+ console.log(dim(`
648
+ Run coderook merge ${now.mergeTrack.reference} --apply to publish it.`));
649
+ }
650
+ return 0;
651
+ }
409
652
  const COMMANDS = {
410
653
  "sign-in": commandSignIn,
411
654
  login: commandSignIn,
@@ -422,6 +665,8 @@ const COMMANDS = {
422
665
  bundle: commandBundle,
423
666
  unbundle: commandUnbundle,
424
667
  inspect: commandInspect,
668
+ merges: commandMerges,
669
+ merge: commandMerge,
425
670
  doctor: () => commandDoctor(),
426
671
  };
427
672
  async function main(argv) {
@@ -95,7 +95,16 @@ class Uploader {
95
95
  const body = text ? JSON.parse(text) : {};
96
96
  if (!response.ok) {
97
97
  const message = body?.error?.message ?? `${route} failed (${response.status})`;
98
- throw new Error(message);
98
+ /*
99
+ The service says what kind of failure this is, and callers need that
100
+ to say anything useful — "someone else saved first" deserves a
101
+ different suggestion from "the disk is full". Carrying the code on
102
+ the error keeps them from having to match on wording.
103
+ */
104
+ throw Object.assign(new Error(message), {
105
+ code: typeof body?.error?.code === "string" ? body.error.code : "",
106
+ status: response.status,
107
+ });
99
108
  }
100
109
  return body;
101
110
  }
@@ -288,6 +297,9 @@ class Uploader {
288
297
  contentType: "application/json",
289
298
  body: JSON.stringify({
290
299
  message: request.message.trim() || "Saved from the CodeRook desktop app",
300
+ ...(request.expectedHeadVersionId === undefined
301
+ ? {}
302
+ : { expectedHeadVersionId: request.expectedHeadVersionId }),
291
303
  sourceSize: sourceBytes,
292
304
  storedSize: storedBytes,
293
305
  files: contents.map((item) => ({
@@ -314,6 +326,7 @@ class Uploader {
314
326
  repositoryId,
315
327
  versionId: completed.version.id,
316
328
  sequence: completed.version.sequence,
329
+ ...(completed.mergeTrack ? { mergeTrack: completed.mergeTrack } : {}),
317
330
  sourceBytes,
318
331
  storedBytes,
319
332
  sentBytes,
package/package.json CHANGED
@@ -1,36 +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
- }
1
+ {
2
+ "name": "@coderook/cli",
3
+ "version": "0.3.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
+ }