@coderook/cli 0.22.2 → 0.24.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.
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  /**
8
- * CodeRook from the command line.
8
+ * cbx — the CodeBox engine from the command line, talking to CodeRook.
9
9
  *
10
10
  * The same engines the desktop application uses — the same scanner, the same
11
11
  * ignore rules, the same upload and download — with a terminal in front of
@@ -17,12 +17,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  */
18
18
  const promises_1 = require("node:readline/promises");
19
19
  const node_os_1 = __importDefault(require("node:os"));
20
+ const promises_2 = require("node:fs/promises");
20
21
  const node_path_1 = __importDefault(require("node:path"));
21
22
  const node_process_1 = __importDefault(require("node:process"));
22
23
  const api_js_1 = require("./api.js");
23
24
  const registry_js_1 = require("./registry.js");
24
25
  const help_js_1 = require("./help.js");
25
26
  const progress_js_1 = require("./progress.js");
27
+ const publish_js_1 = require("./publish.js");
26
28
  const project_commands_js_1 = require("./project_commands.js");
27
29
  const track_commands_js_1 = require("./track_commands.js");
28
30
  const mcp_js_1 = require("./mcp.js");
@@ -37,11 +39,12 @@ const identify_js_1 = require("../../desktop-app/src/main/identify.js");
37
39
  const detect_js_1 = require("../../desktop-app/src/main/detect.js");
38
40
  const cbx_js_1 = require("../../desktop-app/src/main/cbx.js");
39
41
  const api_js_2 = require("./api.js");
42
+ const import_command_js_1 = require("./import_command.js");
40
43
  const runner_js_1 = require("./runner.js");
41
44
  const config_js_1 = require("./config.js");
42
45
  /*
43
46
  Read from the package rather than written twice. A hardcoded copy had
44
- already drifted from the published version, which makes `coderook doctor`
47
+ already drifted from the published version, which makes `cbx doctor`
45
48
  worse than useless when working out what somebody is actually running.
46
49
  */
47
50
  const VERSION = (() => {
@@ -295,7 +298,7 @@ async function commandStatus(parsed) {
295
298
  if (stale.length) {
296
299
  console.log(dim(`${stale.length} file${stale.length === 1 ? "" : "s"} in the saved version ` +
297
300
  `${stale.length === 1 ? "is" : "are"} newer than the cop${stale.length === 1 ? "y" : "ies"} here ` +
298
- `— run ${accent("coderook get")} to bring ${stale.length === 1 ? "it" : "them"} down.`));
301
+ `— run ${accent("cbx get")} to bring ${stale.length === 1 ? "it" : "them"} down.`));
299
302
  for (const [path] of stale.slice(0, 10))
300
303
  console.log(dim(` behind ${path}`));
301
304
  }
@@ -313,6 +316,103 @@ async function commandStatus(parsed) {
313
316
  console.log(dim(` …and ${files.length - 50} more`));
314
317
  return 0;
315
318
  }
319
+ /**
320
+ * Bring an existing repository in from another host.
321
+ *
322
+ * Snapshot only, and it says so. The files arrive, the history does not —
323
+ * see `import_command.ts` for why that is a limit rather than an omission.
324
+ * Everything after the fetch is the ordinary save path, so an import
325
+ * produces exactly the Version that saving the same folder would.
326
+ */
327
+ async function commandImport(parsed) {
328
+ const url = parsed.positional[0];
329
+ if (!url) {
330
+ console.error(red("Nothing to import from."));
331
+ console.error("Give the address of a repository, for example:");
332
+ console.error(` ${accent("cbx import https://github.com/owner/project")}`);
333
+ return 1;
334
+ }
335
+ if (!(0, import_command_js_1.looksLikeRepositoryUrl)(url)) {
336
+ console.error(red(`${url} does not look like a repository address.`));
337
+ console.error("Expected something like https://github.com/owner/project or\n" +
338
+ "git@github.com:owner/project.git");
339
+ return 1;
340
+ }
341
+ if (!(await (0, import_command_js_1.gitAvailable)())) {
342
+ console.error(red("Import needs git on this machine, and it was not found."));
343
+ console.error("Git is used to fetch the files once; nothing about your project\n" +
344
+ "afterwards depends on it.");
345
+ return 1;
346
+ }
347
+ const destination = parsed.positional[1] ?? null;
348
+ let plan;
349
+ try {
350
+ plan = await (0, import_command_js_1.fetchSnapshot)(url, destination, (line) => console.log(dim(line)));
351
+ }
352
+ catch (error) {
353
+ console.error(red("Could not fetch that repository."));
354
+ const detail = error instanceof Error ? error.message : String(error);
355
+ /*
356
+ git puts the useful sentence last and a stack of its own noise first.
357
+ A private repository with no credentials is by far the most common
358
+ failure, so it is named rather than left to be inferred.
359
+ */
360
+ console.error(detail.split("\n").filter(Boolean).slice(-3).join("\n"));
361
+ if (/authentication|denied|not found|could not read/i.test(detail)) {
362
+ console.error("\nIf it is private, sign in to it with git first — CodeRook uses\n" +
363
+ "the credentials git already has and never asks for a token.");
364
+ }
365
+ return 1;
366
+ }
367
+ const { files, bytes } = await (0, import_command_js_1.measure)(plan.folder);
368
+ if (!files) {
369
+ console.error(red("That repository has no files in it."));
370
+ if (plan.temporary)
371
+ await (0, promises_2.rm)(node_path_1.default.dirname(plan.folder), { recursive: true, force: true });
372
+ return 1;
373
+ }
374
+ console.log(`Fetched ${files} file${files === 1 ? "" : "s"}, ${(0, import_command_js_1.humanBytes)(bytes)}, ` +
375
+ `into ${plan.folder}`);
376
+ /*
377
+ Said before the upload rather than after it fails. The window is a share
378
+ of the plan allowance per week, so a large import is a thing somebody
379
+ should know about while they can still choose a smaller repository.
380
+ */
381
+ if (bytes > 1024 ** 3) {
382
+ console.log(dim("This is a large import. Uploads are limited to a share of your\n" +
383
+ "allowance each week, so a project this size may need more than one\n" +
384
+ "sitting; the save resumes rather than starting over."));
385
+ }
386
+ console.log(dim("History is not imported — this becomes the first version."));
387
+ /*
388
+ Hand the fetched folder to the ordinary save path. --allow-ignored is set
389
+ because git tracks files committed before the rule that excludes them,
390
+ and dropping those would make the import quietly lossy.
391
+ */
392
+ const submitFlags = new Map(parsed.flags);
393
+ submitFlags.set("allow-ignored", true);
394
+ /*
395
+ Name the project after the repository, not after wherever the files were
396
+ put. Without this an import into a temporary folder produces a project
397
+ called something like `tmp-4f21`, and the name is the thing somebody
398
+ types afterwards to fetch it.
399
+ */
400
+ if (!submitFlags.has("name"))
401
+ submitFlags.set("name", plan.name);
402
+ if (!submitFlags.has("m") && !submitFlags.has("message")) {
403
+ submitFlags.set("message", `Imported from ${url}`);
404
+ }
405
+ const code = await commandSubmit({
406
+ positional: [plan.folder],
407
+ flags: submitFlags,
408
+ });
409
+ if (code === 0 && plan.temporary) {
410
+ console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
411
+ ` +
412
+ `Run ${accent("cbx get " + plan.name)} anywhere to fetch it fresh.`));
413
+ }
414
+ return code;
415
+ }
316
416
  async function commandSubmit(parsed) {
317
417
  const folder = folderFor(parsed);
318
418
  const message = flagText(parsed, "m", "message") ?? "";
@@ -340,8 +440,39 @@ async function commandSubmit(parsed) {
340
440
  await (0, api_js_2.whoami)()
341
441
  .then((account) => account.displayName || account.username || "")
342
442
  .catch(() => ""), hasFlag(parsed, "no-licence"));
343
- const rules = await (0, worktree_js_1.readRules)(folder);
344
- const files = await (0, worktree_js_1.changedFiles)(folder, rules, baseline);
443
+ /*
444
+ Import sends what it fetched, filtering nothing.
445
+
446
+ The ignore rules exist to keep build output and local mess out of a
447
+ working folder, and applying them to a fresh clone gets the wrong answer
448
+ twice over: a clone holds exactly the files the source repository tracked
449
+ and nothing else, so there is no mess to exclude — while a repository
450
+ that tracks something its own `.gitignore` now names (which git does,
451
+ for anything committed before the rule) would have those files dropped.
452
+ Dropping them makes an import quietly lossy, which is worse than
453
+ refusing to import at all.
454
+ */
455
+ const importing = hasFlag(parsed, "allow-ignored");
456
+ const rules = importing
457
+ ? { shared: "", local: "" }
458
+ : await (0, worktree_js_1.readRules)(folder);
459
+ /*
460
+ Whether a file that is no longer here means "delete it".
461
+
462
+ The default says no, and that is the right default: a missing path is
463
+ usually an unmounted drive, a folder moved while something was open, or a
464
+ scan that ran mid-copy — and the cost of guessing wrong is somebody's work
465
+ removed from the one place it was safe. So the saved copy is left alone
466
+ (docs/UPLOAD_POLICY.md).
467
+
468
+ It also meant nothing on this side could ever remove a file. Publishing
469
+ from git can, because there the question does not arise: `git rm` is a
470
+ recorded act, so the deletion is known rather than inferred. That left the
471
+ remote helper as the only route to something the tool itself could not do,
472
+ which is the wrong way round — so this is that capability, asked for
473
+ plainly.
474
+ */
475
+ const files = await (0, worktree_js_1.changedFiles)(folder, rules, baseline, hasFlag(parsed, "sync") ? "synchronize" : "add-and-update");
345
476
  /*
346
477
  An upgraded folder with no materialisation record and something that
347
478
  differs from its recorded version is ambiguous in the one way that
@@ -364,8 +495,8 @@ async function commandSubmit(parsed) {
364
495
  console.error(`${files.length} file${files.length === 1 ? " differs" : "s differ"} here:`);
365
496
  for (const file of files.slice(0, 20))
366
497
  console.error(` ${file.path}`);
367
- console.error(`\nIf that is your work, send it with ${accent("coderook submit --force")}.` +
368
- `\nIf it is an old copy, take theirs with ${accent("coderook get --replace")}.`);
498
+ console.error(`\nIf that is your work, send it with ${accent("cbx submit --force")}.` +
499
+ `\nIf it is an old copy, take theirs with ${accent("cbx get --replace")}.`);
369
500
  return 1;
370
501
  }
371
502
  }
@@ -379,7 +510,7 @@ async function commandSubmit(parsed) {
379
510
  console.log(behindOn
380
511
  ? `Nothing to submit; your own work is all saved. ${behindOn} file` +
381
512
  `${behindOn === 1 ? "" : "s"} here ${behindOn === 1 ? "is" : "are"} ` +
382
- `behind the saved version — run ${accent("coderook get")}.`
513
+ `behind the saved version — run ${accent("cbx get")}.`
383
514
  : "Nothing to submit; this folder matches the saved version.");
384
515
  return 0;
385
516
  }
@@ -404,17 +535,17 @@ async function commandSubmit(parsed) {
404
535
  */
405
536
  const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
406
537
  if (shielded.length && !hasFlag(parsed, "allow-private")) {
407
- console.log(red(`
538
+ console.log(red(`
408
539
  ${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
409
540
  for (const finding of shielded) {
410
541
  console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
411
542
  }
412
- console.log(`
543
+ console.log(`
413
544
  Nothing was sent. To leave them behind:`);
414
545
  for (const finding of shielded) {
415
546
  console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
416
547
  }
417
- console.error(`
548
+ console.error(`
418
549
  Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
419
550
  return 1;
420
551
  }
@@ -449,7 +580,7 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
449
580
  const pasted = await (0, worktree_js_1.detectPastedCredentials)(folder, files.map((file) => file.path));
450
581
  const stillPasted = pasted.filter((finding) => !exposed.includes(finding.path));
451
582
  if (stillPasted.length) {
452
- console.log(red(`
583
+ console.log(red(`
453
584
  ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
454
585
  for (const finding of stillPasted.slice(0, 20)) {
455
586
  console.log(` ${finding.path}`);
@@ -464,11 +595,11 @@ ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s
464
595
  console.log(dim(` …and ${stillPasted.length - 20} more`));
465
596
  }
466
597
  if (!hasFlag(parsed, "allow-secrets")) {
467
- console.error(`
598
+ console.error(`
468
599
  Nothing was sent. Move the key into an environment variable, and if` +
469
600
  ` it has ever been published, replace it at the service that issued` +
470
601
  ` it — a key that has leaked stays leaked.` +
471
- `
602
+ `
472
603
  Pass ${accent("--allow-secrets")} if these are not real keys.`);
473
604
  return 1;
474
605
  }
@@ -477,31 +608,47 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
477
608
  if (added) {
478
609
  console.log(`${accent("Added an MIT licence")}, so other people may use this.`);
479
610
  console.log(dim(` Change it with `) +
480
- accent("coderook licence <name>") +
611
+ accent("cbx licence <name>") +
481
612
  dim(`, or remove it with `) +
482
- accent("coderook licence none") +
613
+ accent("cbx licence none") +
483
614
  dim("."));
484
615
  console.log("");
485
616
  }
486
617
  const line = progressLine();
487
618
  const uploader = new upload_js_1.Uploader(config_js_1.credentials);
488
- const uploadRequest = {
619
+ /*
620
+ Built by the shared description, so this and the git remote helper cannot
621
+ drift. They already had: the helper listed deletions in `deletions` alone
622
+ and published versions that still held the deleted file.
623
+ */
624
+ const uploadRequest = (0, publish_js_1.uploadRequestFor)({
489
625
  localPath: folder,
490
- include: files.map((file) => file.path),
491
- deletions: files.filter((file) => file.deleted).map((file) => file.path),
626
+ changed: files.filter((file) => !file.deleted).map((file) => file.path),
627
+ deleted: files.filter((file) => file.deleted).map((file) => file.path),
492
628
  message,
493
- projectName: link?.slug ?? node_path_1.default.basename(folder),
629
+ /*
630
+ A folder's name is the right default and the wrong answer for import,
631
+ where the folder is somewhere temporary and the project should carry
632
+ the name it had at the place it came from. An existing link always
633
+ wins: renaming somebody's project because they passed a flag would be
634
+ a surprise, and the flag exists for projects that do not exist yet.
635
+ */
636
+ projectName: link?.slug ?? flagText(parsed, "name") ?? node_path_1.default.basename(folder),
494
637
  repositoryId: link?.repositoryId ?? null,
495
638
  // Every current CLI publish states its ancestry. A brand-new project is
496
639
  // explicitly based on an empty Track; a linked folder names the immutable
497
640
  // Version it was last reconciled with.
498
641
  baseVersionId: link?.baseVersionId ?? null,
499
642
  track: flagText(parsed, "track") ?? (await (0, track_commands_js_1.trackFor)(folder)),
500
- ...(link?.baseVersionId
501
- ? { expectedHeadVersionId: link.baseVersionId }
502
- : {}),
643
+ /*
644
+ Only import sets this. Git keeps tracking files committed before the
645
+ rule that excludes them, so a faithful import carries paths the
646
+ project's own ignore rules now refuse; without the override the server
647
+ would reject the publication and the import would be lossy.
648
+ */
649
+ ...(hasFlag(parsed, "allow-ignored") ? { allowIgnored: true } : {}),
503
650
  ...(link ? { known: link.local ?? link.manifest ?? {} } : {}),
504
- };
651
+ });
505
652
  let result;
506
653
  try {
507
654
  const plan = await uploader.plan(uploadRequest, (progress) => {
@@ -532,23 +679,22 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
532
679
  }
533
680
  catch (error) {
534
681
  done(line);
535
- const code = error.code;
536
- const text = error instanceof Error ? error.message : String(error);
537
682
  /*
538
- A connection that fails part way through says nothing about whether
539
- the work was done. It may well have beenthe service records the
540
- attempt, so running the same command again is answered with the
541
- version it already made rather than a second one. Saying so is the
542
- difference between a person retrying and a person wondering.
683
+ Classified by the shared description rather than by a private regular
684
+ expression, so a push and a submit disagree about nothing including
685
+ what counts as "the connection failed" and what that means for whether
686
+ the work was saved.
543
687
  */
544
- if (!code && /fetch failed|ECONNRESET|socket hang up|network|ETIMEDOUT/i.test(text)) {
545
- console.error(red(`
688
+ const failure = (0, publish_js_1.classifyPublishFailure)(error);
689
+ const text = error instanceof Error ? error.message : String(error);
690
+ if (failure?.kind === "interrupted") {
691
+ console.error(red(`
546
692
  The connection failed: ${text}`));
547
693
  console.error(`Your work may already have been saved. Run the same command again —` +
548
694
  ` it will not create a second version.`);
549
695
  return 1;
550
696
  }
551
- if (code === "merge_required" || code === "track_moved") {
697
+ if (failure?.kind === "conflict") {
552
698
  /*
553
699
  Somebody else saved to this project first. Everything that did not
554
700
  overlap has already been combined by the service; what is left is a
@@ -556,7 +702,7 @@ The connection failed: ${text}`));
556
702
  at it rather than to try again harder.
557
703
  */
558
704
  console.error(red(error instanceof Error ? error.message : String(error)));
559
- console.error(`\nRun ${accent("coderook get")} to bring the latest version down, then` +
705
+ console.error(`\nRun ${accent("cbx get")} to bring the latest version down, then` +
560
706
  ` submit again. Nothing was changed on your account.`);
561
707
  return 1;
562
708
  }
@@ -576,8 +722,8 @@ The connection failed: ${text}`));
576
722
  for (const conflict of result.mergeTrack.conflicts.slice(0, 20)) {
577
723
  console.log(` ${red(conflict.path)} ${dim(conflict.kind)}`);
578
724
  }
579
- console.log(`\nRun ${accent(`coderook merge ${result.mergeTrack.reference}`)} to decide,` +
580
- ` or ${accent("coderook merges")} to see everything waiting.`);
725
+ console.log(`\nRun ${accent(`cbx merge ${result.mergeTrack.reference}`)} to decide,` +
726
+ ` or ${accent("cbx merges")} to see everything waiting.`);
581
727
  return 0;
582
728
  }
583
729
  /*
@@ -586,9 +732,29 @@ The connection failed: ${text}`));
586
732
  attempt name exists to make survivable.
587
733
  */
588
734
  (0, faults_js_1.maybeFail)("submit:after-commit");
735
+ /*
736
+ What the project is actually called, not what this folder is called.
737
+
738
+ A first publish recorded the folder's own name, which is right only when
739
+ nothing renamed the project — and two things routinely do: `--name`, and
740
+ the service turning a display name into a slug. When they differ, every
741
+ command that finds the project through this folder looks up a name that
742
+ does not exist: `tracks`, `versions`, `issues`, `releases`, `people`,
743
+ `watch`, `delete` and the rest all answered "No project matching …" in a
744
+ folder that had just published to it successfully.
745
+
746
+ Asked of the service rather than guessed, and only on a first publish,
747
+ because that is the only time this is not already known.
748
+ */
749
+ let recordedSlug = link?.slug;
750
+ if (!recordedSlug) {
751
+ recordedSlug = await (0, api_js_2.projects)()
752
+ .then((all) => all.find((candidate) => candidate.id === result.repositoryId)?.slug)
753
+ .catch(() => undefined);
754
+ }
589
755
  await (0, config_js_1.writeLink)(folder, {
590
756
  repositoryId: result.repositoryId,
591
- slug: link?.slug ?? node_path_1.default.basename(folder),
757
+ slug: recordedSlug ?? uploadRequest.projectName ?? node_path_1.default.basename(folder),
592
758
  sequence: result.sequence,
593
759
  // What this folder is now working from, so the next submit can say so.
594
760
  versionId: result.versionId,
@@ -687,8 +853,8 @@ async function commandGet(parsed) {
687
853
  console.error(red("This folder was connected by an older version of CodeRook,"));
688
854
  console.error("which did not record what it had received, so changed files here");
689
855
  console.error("cannot be told apart from copies left behind by a merge.\n");
690
- console.error(`Save them with ${accent("coderook submit")} if they are your work, or ` +
691
- `take the\nsaved version exactly with ${accent("coderook get --replace")}.`);
856
+ console.error(`Save them with ${accent("cbx submit")} if they are your work, or ` +
857
+ `take the\nsaved version exactly with ${accent("cbx get --replace")}.`);
692
858
  return 1;
693
859
  }
694
860
  }
@@ -724,9 +890,9 @@ async function commandGet(parsed) {
724
890
  `changed while the interrupted fetch was stopped:`));
725
891
  for (const file of changedInTheGap.slice(0, 20))
726
892
  console.error(` ${file.path}`);
727
- console.error(`
728
- Save them with ${accent("coderook submit")}, or finish the fetch and ` +
729
- `discard them with ${accent("coderook get --replace")}.`);
893
+ console.error(`
894
+ Save them with ${accent("cbx submit")}, or finish the fetch and ` +
895
+ `discard them with ${accent("cbx get --replace")}.`);
730
896
  return 1;
731
897
  }
732
898
  /*
@@ -746,8 +912,8 @@ Save them with ${accent("coderook submit")}, or finish the fetch and ` +
746
912
  console.error(` ${file.path}`);
747
913
  if (atRisk.length > 20)
748
914
  console.error(dim(` …and ${atRisk.length - 20} more`));
749
- console.error(`\nSave them first with ${accent("coderook submit")}, or discard them ` +
750
- `with ${accent("coderook get --replace")}.`);
915
+ console.error(`\nSave them first with ${accent("cbx submit")}, or discard them ` +
916
+ `with ${accent("cbx get --replace")}.`);
751
917
  return 1;
752
918
  }
753
919
  }
@@ -762,7 +928,7 @@ Save them with ${accent("coderook submit")}, or finish the fetch and ` +
762
928
  async function commandClone(parsed) {
763
929
  const reference = parsed.positional[0];
764
930
  if (!reference) {
765
- console.error(red("Which project? Try: coderook clone <project>"));
931
+ console.error(red("Which project? Try: cbx clone <project>"));
766
932
  return 1;
767
933
  }
768
934
  const project = await (0, api_js_2.findProject)(reference);
@@ -866,7 +1032,7 @@ async function suggestRules(folder, apply) {
866
1032
  const recommended = suggestions.filter((one) => one.recommended);
867
1033
  if (!apply) {
868
1034
  console.log("");
869
- console.log(dim("Add the + ones with ") + accent("coderook ignore --suggest --apply"));
1035
+ console.log(dim("Add the + ones with ") + accent("cbx ignore --suggest --apply"));
870
1036
  return 0;
871
1037
  }
872
1038
  if (!recommended.length) {
@@ -877,8 +1043,8 @@ async function suggestRules(folder, apply) {
877
1043
  const addition = (0, detect_js_1.rulesFromSuggestions)(recommended);
878
1044
  const shared = rules.shared.trimEnd();
879
1045
  await (0, worktree_js_1.writeRules)(folder, {
880
- shared: shared ? `${shared}
881
-
1046
+ shared: shared ? `${shared}
1047
+
882
1048
  ${addition}` : addition,
883
1049
  local: rules.local,
884
1050
  });
@@ -928,7 +1094,7 @@ async function commandBundle(parsed) {
928
1094
  async function commandUnbundle(parsed) {
929
1095
  const source = parsed.positional[0];
930
1096
  if (!source) {
931
- console.error(red("Which bundle? Try: coderook unbundle <file.cbx> [folder]"));
1097
+ console.error(red("Which bundle? Try: cbx unbundle <file.cbx> [folder]"));
932
1098
  return 1;
933
1099
  }
934
1100
  const manifest = await (0, cbx_js_1.readManifest)(node_path_1.default.resolve(source));
@@ -945,7 +1111,7 @@ async function commandUnbundle(parsed) {
945
1111
  async function commandInspect(parsed) {
946
1112
  const source = parsed.positional[0];
947
1113
  if (!source) {
948
- console.error(red("Which bundle? Try: coderook inspect <file.cbx>"));
1114
+ console.error(red("Which bundle? Try: cbx inspect <file.cbx>"));
949
1115
  return 1;
950
1116
  }
951
1117
  const manifest = await (0, cbx_js_1.readManifest)(node_path_1.default.resolve(source));
@@ -962,7 +1128,7 @@ async function commandInspect(parsed) {
962
1128
  }
963
1129
  async function commandDoctor() {
964
1130
  const service = await (0, api_js_2.health)().catch(() => null);
965
- console.log(`CodeRook CLI ${VERSION} · Node ${node_process_1.default.versions.node} · ${node_process_1.default.platform}`);
1131
+ console.log(`cbx ${VERSION} · Node ${node_process_1.default.versions.node} · ${node_process_1.default.platform}`);
966
1132
  console.log(`Config: ${(0, config_js_1.configDirectory)()}`);
967
1133
  console.log(service
968
1134
  ? `Service: ${service.status} · schema ${service.schemaVersion}` +
@@ -1005,8 +1171,8 @@ async function commandMerges(parsed) {
1005
1171
  const counts = merge.conflicts;
1006
1172
  console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
1007
1173
  }
1008
- console.log(dim(`
1009
- Run coderook merge <reference> to look at one.`));
1174
+ console.log(dim(`
1175
+ Run cbx merge <reference> to look at one.`));
1010
1176
  return 0;
1011
1177
  }
1012
1178
  /** Find a merge by the reference a person would type, such as M-2. */
@@ -1029,7 +1195,7 @@ async function findMerge(folder, reference) {
1029
1195
  async function commandMerge(parsed) {
1030
1196
  const reference = parsed.positional[0];
1031
1197
  if (!reference) {
1032
- console.error(red("Which merge? Try: coderook merge M-1"));
1198
+ console.error(red("Which merge? Try: cbx merge M-1"));
1033
1199
  return 1;
1034
1200
  }
1035
1201
  const folder = folderFor({ ...parsed, positional: parsed.positional.slice(1) });
@@ -1071,7 +1237,7 @@ async function commandMerge(parsed) {
1071
1237
  }
1072
1238
  }
1073
1239
  const now = await (0, api_js_1.mergeTrack)(summary.id);
1074
- console.log(`
1240
+ console.log(`
1075
1241
  ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
1076
1242
  (now.provisional.ready
1077
1243
  ? "ready to apply"
@@ -1086,20 +1252,20 @@ ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
1086
1252
  return 1;
1087
1253
  }
1088
1254
  const applied = await (0, api_js_1.applyMerge)(summary.id);
1089
- console.log(`
1255
+ console.log(`
1090
1256
  Applied as ${accent(`v${applied.version.sequence}`)}.`);
1091
- console.log(dim("Run coderook get to bring it down to this folder."));
1257
+ console.log(dim("Run cbx get to bring it down to this folder."));
1092
1258
  return 0;
1093
1259
  }
1094
1260
  if (!decision) {
1095
- console.log(dim(`
1261
+ console.log(dim(`
1096
1262
  --mine keeps yours, --theirs keeps what was already saved,` +
1097
- ` --drop removes the file.
1263
+ ` --drop removes the file.
1098
1264
  Add --path <file> for one file, then --apply when ready.`));
1099
1265
  }
1100
1266
  else if (now.provisional.ready) {
1101
- console.log(dim(`
1102
- Run coderook merge ${now.mergeTrack.reference} --apply to publish it.`));
1267
+ console.log(dim(`
1268
+ Run cbx merge ${now.mergeTrack.reference} --apply to publish it.`));
1103
1269
  }
1104
1270
  return 0;
1105
1271
  }
@@ -1119,7 +1285,7 @@ Run coderook merge ${now.mergeTrack.reference} --apply to publish it.`));
1119
1285
  async function commandRunner(parsed) {
1120
1286
  const reference = parsed.positional[0] ?? flagText(parsed, "project", "p");
1121
1287
  if (!reference) {
1122
- console.error(red("Which project? coderook runner <project>"));
1288
+ console.error(red("Which project? cbx runner <project>"));
1123
1289
  return 1;
1124
1290
  }
1125
1291
  const project = await (0, api_js_2.findProject)(reference);
@@ -1265,7 +1431,7 @@ const SPECS = [
1265
1431
  },
1266
1432
  {
1267
1433
  name: "submit",
1268
- aliases: ["publish"],
1434
+ aliases: ["publish", "push"],
1269
1435
  group: "Working with a folder",
1270
1436
  summary: "send the changes as a new version",
1271
1437
  usage: 'submit [folder] -m "…"',
@@ -1275,10 +1441,18 @@ const SPECS = [
1275
1441
  options: [
1276
1442
  { flags: "-m, --message <text>", description: "what changed, in a sentence" },
1277
1443
  { flags: "-n, --dry-run", description: "show what would be sent, send nothing" },
1444
+ {
1445
+ flags: "--name <name>",
1446
+ description: "name a new project this, instead of after the folder",
1447
+ },
1278
1448
  {
1279
1449
  flags: "--allow-secrets",
1280
1450
  description: "send files that look like credentials, and files with keys inside",
1281
1451
  },
1452
+ {
1453
+ flags: "--sync",
1454
+ description: "treat a file that is no longer here as deleted, rather than keeping the saved copy",
1455
+ },
1282
1456
  {
1283
1457
  flags: "--track <name>",
1284
1458
  description: "save onto this line, just this once",
@@ -1289,11 +1463,43 @@ const SPECS = [
1289
1463
  },
1290
1464
  ],
1291
1465
  examples: [
1292
- 'coderook submit -m "Fix the export dialog"',
1293
- 'coderook submit --track spike -m "Try the other encoder"',
1466
+ 'cbx submit -m "Fix the export dialog"',
1467
+ 'cbx submit --track spike -m "Try the other encoder"',
1294
1468
  ],
1295
1469
  run: commandSubmit,
1296
1470
  },
1471
+ {
1472
+ name: "import",
1473
+ group: "Getting started",
1474
+ summary: "bring a project in from another host",
1475
+ usage: "import <address> [folder]",
1476
+ detail: "Fetches a repository from GitHub, GitLab or anywhere else git can\n" +
1477
+ "reach, and saves it as the first version of a CodeRook project.\n\n" +
1478
+ "The files come across; the history does not. Every past commit would\n" +
1479
+ "have to be published as its own version, which on a large project\n" +
1480
+ "takes days — so this takes an honest snapshot rather than leaving a\n" +
1481
+ "half-finished import behind. What arrives is the current state of the\n" +
1482
+ "default branch.\n\n" +
1483
+ "A public repository needs nothing. A private one uses the credentials\n" +
1484
+ "git already has on this machine; CodeRook never asks for, stores or\n" +
1485
+ "forwards a token.\n\n" +
1486
+ "Files that the project's own ignore rules exclude are sent anyway,\n" +
1487
+ "because git keeps tracking anything committed before the rule that\n" +
1488
+ "excludes it, and leaving them out would lose files the source has.",
1489
+ options: [
1490
+ { flags: "-m, --message <text>", description: "the first version's message" },
1491
+ { flags: "--track <name>", description: "save onto this line" },
1492
+ {
1493
+ flags: "--no-licence",
1494
+ description: "do not add a licence to the new project",
1495
+ },
1496
+ ],
1497
+ examples: [
1498
+ "cbx import https://github.com/owner/project",
1499
+ "cbx import git@github.com:owner/project.git ./project",
1500
+ ],
1501
+ run: commandImport,
1502
+ },
1297
1503
  {
1298
1504
  /*
1299
1505
  Where the next save goes, which is a property of this folder rather
@@ -1301,14 +1507,14 @@ const SPECS = [
1301
1507
  different lines, which is most of the point of having them.
1302
1508
  */
1303
1509
  name: "track",
1304
- aliases: ["switch"],
1510
+ aliases: ["switch", "checkout"],
1305
1511
  group: "Working with a folder",
1306
1512
  summary: "show or change the line this folder saves to",
1307
1513
  usage: "track [name]",
1308
1514
  detail: "With no name, prints the line this folder saves to. With one, switches\n" +
1309
1515
  "to it. Switching says where the next save goes and nothing else: no\n" +
1310
1516
  "files move and nothing is fetched, so it is instant and safe to change\n" +
1311
- "your mind. Run `coderook get` afterwards to bring that line's files\n" +
1517
+ "your mind. Run `cbx get` afterwards to bring that line's files\n" +
1312
1518
  "into the folder.\n\n" +
1313
1519
  "A name that does not exist is refused rather than created, because a\n" +
1314
1520
  "typo in a branch name is an ordinary thing to do and a line called\n" +
@@ -1317,23 +1523,25 @@ const SPECS = [
1317
1523
  options: [
1318
1524
  { flags: "-n, --new", description: "start this line from where the project is now" },
1319
1525
  ],
1320
- examples: ["coderook track", "coderook track spike --new", "coderook track main"],
1526
+ examples: ["cbx track", "cbx track spike --new", "cbx track main"],
1321
1527
  run: track_commands_js_1.commandTrack,
1322
1528
  },
1323
1529
  {
1324
1530
  name: "tracks",
1531
+ aliases: ["branch", "branches"],
1325
1532
  group: "Your projects",
1326
1533
  summary: "the lines a project has, and any waiting on a decision",
1327
1534
  usage: "tracks [project]",
1328
1535
  detail: "Lists every line on the project, marking the one this folder saves to.\n" +
1329
1536
  "Merges waiting on a decision are listed beside them rather than hidden,\n" +
1330
1537
  "because somebody looking for where they can save needs to see the one\n" +
1331
- "they cannot. Finish one with `coderook merge`.",
1332
- examples: ["coderook tracks", "coderook tracks my-project"],
1538
+ "they cannot. Finish one with `cbx merge`.",
1539
+ examples: ["cbx tracks", "cbx tracks my-project"],
1333
1540
  run: track_commands_js_1.commandTracks,
1334
1541
  },
1335
1542
  {
1336
1543
  name: "get",
1544
+ aliases: ["pull"],
1337
1545
  group: "Working with a folder",
1338
1546
  summary: "bring this folder up to date",
1339
1547
  usage: "get [folder]",
@@ -1376,7 +1584,7 @@ const SPECS = [
1376
1584
  description: "replace a licence that is already there",
1377
1585
  },
1378
1586
  ],
1379
- examples: ["coderook licence", "coderook licence MIT", "coderook licence Apache-2.0"],
1587
+ examples: ["cbx licence", "cbx licence MIT", "cbx licence Apache-2.0"],
1380
1588
  run: licence_commands_js_1.commandLicence,
1381
1589
  },
1382
1590
  {
@@ -1423,6 +1631,7 @@ const SPECS = [
1423
1631
  },
1424
1632
  {
1425
1633
  name: "versions",
1634
+ aliases: ["log"],
1426
1635
  group: "Your projects",
1427
1636
  summary: "what has been saved to a project",
1428
1637
  usage: "versions [project]",
@@ -1448,9 +1657,9 @@ const SPECS = [
1448
1657
  { flags: "--request [off]", description: "automated calls to its endpoints" },
1449
1658
  ],
1450
1659
  examples: [
1451
- "coderook ai my-project",
1452
- "coderook ai my-project --read off",
1453
- "coderook ai my-project --download off --request off",
1660
+ "cbx ai my-project",
1661
+ "cbx ai my-project --read off",
1662
+ "cbx ai my-project --download off --request off",
1454
1663
  ],
1455
1664
  run: project_commands_js_1.commandAi,
1456
1665
  },
@@ -1463,16 +1672,43 @@ const SPECS = [
1463
1672
  { flags: '--new "<title>"', description: "open a new issue" },
1464
1673
  { flags: "--body <text>", description: "the description for a new one" },
1465
1674
  ],
1466
- examples: ['coderook issues my-project --new "Crash on export"'],
1675
+ examples: ['cbx issues my-project --new "Crash on export"'],
1467
1676
  run: service_commands_js_1.commandIssues,
1468
1677
  },
1469
1678
  {
1470
1679
  name: "releases",
1680
+ aliases: ["tags"],
1471
1681
  group: "Your projects",
1472
1682
  summary: "what has been released, and its files",
1473
1683
  usage: "releases [project]",
1474
1684
  run: service_commands_js_1.commandReleases,
1475
1685
  },
1686
+ {
1687
+ /*
1688
+ A release is a version with a name on it, so this names one rather than
1689
+ creating anything. Pushing a git tag does the same thing by the same
1690
+ route — the tag is the name, the tagged commit picks the version.
1691
+ */
1692
+ name: "release",
1693
+ aliases: ["tag"],
1694
+ group: "Your projects",
1695
+ summary: "give a version a name, so people know which one to fetch",
1696
+ usage: "release <name> [project]",
1697
+ detail: "Names the newest version unless you name another with --version.\n" +
1698
+ "Nothing is uploaded: a release is a version with a name on it, so the\n" +
1699
+ "version has to exist already.\n\n" +
1700
+ "The same thing happens when a git tag is pushed to a CodeRook remote —\n" +
1701
+ "`git push cbx v1.0` and `cbx release v1.0` are one action.",
1702
+ options: [
1703
+ { flags: "--version <n>", description: "name this version instead of the newest" },
1704
+ { flags: "--notes <text>", description: "what changed, for the release page" },
1705
+ ],
1706
+ examples: [
1707
+ "cbx release v1.0",
1708
+ 'cbx release v1.2 --version 41 --notes "Fixes the export dialog"',
1709
+ ],
1710
+ run: service_commands_js_1.commandRelease,
1711
+ },
1476
1712
  {
1477
1713
  name: "actions",
1478
1714
  aliases: ["workflows"],
@@ -1480,7 +1716,7 @@ const SPECS = [
1480
1716
  summary: "the automations a project has",
1481
1717
  usage: "actions [project]",
1482
1718
  detail: "Shows what each action runs, what it runs on, and how many of its\n" +
1483
- "runs have passed. Use `coderook runner` to execute them on this machine.",
1719
+ "runs have passed. Use `cbx runner` to execute them on this machine.",
1484
1720
  run: service_commands_js_1.commandWorkflows,
1485
1721
  },
1486
1722
  {
@@ -1497,7 +1733,7 @@ const SPECS = [
1497
1733
  description: "queued, running, passed, failed or cancelled",
1498
1734
  },
1499
1735
  ],
1500
- examples: ["coderook runs --status failed"],
1736
+ examples: ["cbx runs --status failed"],
1501
1737
  run: service_commands_js_1.commandRuns,
1502
1738
  },
1503
1739
  {
@@ -1508,7 +1744,7 @@ const SPECS = [
1508
1744
  detail: "The output the runner sent up, in order. Anything the command wrote to\n" +
1509
1745
  "stderr is shown in red. Exits 1 when the run failed, so this can be\n" +
1510
1746
  "the last line of a script.",
1511
- examples: ["coderook logs 12", "coderook logs #12 my-project"],
1747
+ examples: ["cbx logs 12", "cbx logs #12 my-project"],
1512
1748
  run: service_commands_js_1.commandLogs,
1513
1749
  },
1514
1750
  {
@@ -1532,7 +1768,7 @@ const SPECS = [
1532
1768
  { flags: "--invite <email>", description: "ask somebody to join" },
1533
1769
  { flags: "--role <role>", description: "what they may do (default member)" },
1534
1770
  ],
1535
- examples: ["coderook people my-project --invite sam@example.com"],
1771
+ examples: ["cbx people my-project --invite sam@example.com"],
1536
1772
  run: service_commands_js_1.commandCollaborators,
1537
1773
  },
1538
1774
  {
@@ -1617,7 +1853,7 @@ const SPECS = [
1617
1853
  { flags: "--labels <a,b>", description: "what kinds of run it answers to" },
1618
1854
  { flags: "--poll <seconds>", description: "how long between asks" },
1619
1855
  ],
1620
- examples: ["coderook runner my-game --labels windows,signing"],
1856
+ examples: ["cbx runner my-game --labels windows,signing"],
1621
1857
  run: commandRunner,
1622
1858
  },
1623
1859
  {
@@ -1641,7 +1877,7 @@ const SPECS = [
1641
1877
  "repository for everybody who clones it.\n" +
1642
1878
  "\n" +
1643
1879
  "It teaches the command line rather than the MCP server, because that\n" +
1644
- "needs no configuration at all. `coderook mcp` is still there when a\n" +
1880
+ "needs no configuration at all. `cbx mcp` is still there when a\n" +
1645
1881
  "structured connection is wanted.\n",
1646
1882
  options: [
1647
1883
  {
@@ -1649,7 +1885,7 @@ const SPECS = [
1649
1885
  description: "install into this project rather than for you",
1650
1886
  },
1651
1887
  ],
1652
- examples: ["coderook skill", "coderook skill --project"],
1888
+ examples: ["cbx skill", "cbx skill --project"],
1653
1889
  run: skill_command_js_1.commandSkill,
1654
1890
  },
1655
1891
  {
@@ -1668,11 +1904,11 @@ const SPECS = [
1668
1904
  "assistant at it and it starts and stops the process itself.\n" +
1669
1905
  "\n" +
1670
1906
  "Claude Code:\n" +
1671
- " claude mcp add coderook -- coderook mcp\n" +
1907
+ " claude mcp add coderook -- cbx mcp\n" +
1672
1908
  "\n" +
1673
1909
  "Codex, in ~/.codex/config.toml:\n" +
1674
1910
  " [mcp_servers.coderook]\n" +
1675
- " command = 'coderook'\n" +
1911
+ " command = 'cbx'\n" +
1676
1912
  " args = ['mcp']\n" +
1677
1913
  "\n" +
1678
1914
  "Everything it offers reads. It lists projects, versions and files, shows\n" +
@@ -1682,7 +1918,7 @@ const SPECS = [
1682
1918
  "\n" +
1683
1919
  "A project whose owner has turned off machine reading is refused, in\n" +
1684
1920
  "words rather than as a status code.\n",
1685
- examples: ["claude mcp add coderook -- coderook mcp"],
1921
+ examples: ["claude mcp add coderook -- cbx mcp"],
1686
1922
  run: () => (0, mcp_js_1.commandMcp)(VERSION),
1687
1923
  },
1688
1924
  {
@@ -1705,7 +1941,7 @@ async function main(argv) {
1705
1941
  return 0;
1706
1942
  }
1707
1943
  /*
1708
- `coderook help submit` and `coderook submit --help` reach the same page.
1944
+ `cbx help submit` and `cbx submit --help` reach the same page.
1709
1945
  People reach for both, and one of them silently doing something else is
1710
1946
  the kind of small betrayal that makes a tool feel unreliable.
1711
1947
  */