@coderook/cli 0.2.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,11 +18,12 @@ 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
28
  /*
28
29
  Read from the package rather than written twice. A hardcoded copy had
@@ -122,8 +123,8 @@ async function reconcile(folder, options = {}) {
122
123
  const link = await (0, config_js_1.readLink)(folder);
123
124
  const reference = link?.slug ?? node_path_1.default.basename(folder);
124
125
  const project = link
125
- ? (await (0, api_js_1.projects)()).find((candidate) => candidate.id === link.repositoryId)
126
- : 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);
127
128
  if (!project?.versionCount) {
128
129
  // Nothing saved on the account, so everything here is outstanding.
129
130
  return { link: link ?? null, baseline: null, behind: null };
@@ -184,7 +185,7 @@ async function commandSignIn(parsed) {
184
185
  }
185
186
  await (0, config_js_1.storeToken)(token);
186
187
  try {
187
- const account = await (0, api_js_1.whoami)();
188
+ const account = await (0, api_js_2.whoami)();
188
189
  console.log(`Signed in as ${bold(account.displayName)} ${dim(`<${account.email}>`)} · ${account.plan}`);
189
190
  console.log(dim(`Token stored in ${(0, config_js_1.configDirectory)()}`));
190
191
  return 0;
@@ -201,12 +202,12 @@ async function commandSignOut() {
201
202
  return 0;
202
203
  }
203
204
  async function commandWhoami() {
204
- const account = await (0, api_js_1.whoami)();
205
+ const account = await (0, api_js_2.whoami)();
205
206
  console.log(`${bold(account.displayName)} <${account.email}> · ${account.plan}`);
206
207
  return 0;
207
208
  }
208
209
  async function commandProjects() {
209
- const all = await (0, api_js_1.projects)();
210
+ const all = await (0, api_js_2.projects)();
210
211
  if (!all.length) {
211
212
  console.log("No projects on this account yet.");
212
213
  return 0;
@@ -320,6 +321,23 @@ async function commandSubmit(parsed) {
320
321
  throw error;
321
322
  }
322
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
+ }
323
341
  await (0, config_js_1.writeLink)(folder, {
324
342
  repositoryId: result.repositoryId,
325
343
  slug: link?.slug ?? node_path_1.default.basename(folder),
@@ -349,7 +367,7 @@ async function commandClone(parsed) {
349
367
  console.error(red("Which project? Try: coderook clone <project>"));
350
368
  return 1;
351
369
  }
352
- const project = await (0, api_js_1.findProject)(reference);
370
+ const project = await (0, api_js_2.findProject)(reference);
353
371
  if (!project) {
354
372
  console.error(red(`No project named ${reference} on this account.`));
355
373
  return 1;
@@ -454,7 +472,7 @@ async function commandInspect(parsed) {
454
472
  return 0;
455
473
  }
456
474
  async function commandDoctor() {
457
- const service = await (0, api_js_1.health)().catch(() => null);
475
+ const service = await (0, api_js_2.health)().catch(() => null);
458
476
  console.log(`CodeRook CLI ${VERSION} · Node ${node_process_1.default.versions.node} · ${node_process_1.default.platform}`);
459
477
  console.log(`Config: ${(0, config_js_1.configDirectory)()}`);
460
478
  console.log(service
@@ -467,7 +485,7 @@ async function commandDoctor() {
467
485
  return 0;
468
486
  }
469
487
  try {
470
- const account = await (0, api_js_1.whoami)();
488
+ const account = await (0, api_js_2.whoami)();
471
489
  console.log(`Account: ${account.email} · ${account.plan}`);
472
490
  }
473
491
  catch (error) {
@@ -489,6 +507,10 @@ ${bold("Working with a folder")}
489
507
  coderook clone <project> [dir] Fetch a project into a new folder
490
508
  coderook rules [folder] Show the ignore rules (--init to start one)
491
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
+
492
514
  ${bold("Bundles")}
493
515
  coderook bundle [folder] [out] Pack the project as a .cbx
494
516
  coderook unbundle <file> [dir] Extract a .cbx
@@ -506,6 +528,127 @@ ${bold("Options")}
506
528
 
507
529
  ${dim("The environment variable CODEROOK_TOKEN is used when set, so automated")}
508
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
+ }
509
652
  const COMMANDS = {
510
653
  "sign-in": commandSignIn,
511
654
  login: commandSignIn,
@@ -522,6 +665,8 @@ const COMMANDS = {
522
665
  bundle: commandBundle,
523
666
  unbundle: commandUnbundle,
524
667
  inspect: commandInspect,
668
+ merges: commandMerges,
669
+ merge: commandMerge,
525
670
  doctor: () => commandDoctor(),
526
671
  };
527
672
  async function main(argv) {
@@ -326,6 +326,7 @@ class Uploader {
326
326
  repositoryId,
327
327
  versionId: completed.version.id,
328
328
  sequence: completed.version.sequence,
329
+ ...(completed.mergeTrack ? { mergeTrack: completed.mergeTrack } : {}),
329
330
  sourceBytes,
330
331
  storedBytes,
331
332
  sentBytes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",