@coderook/cli 0.31.5 → 0.31.7

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.
@@ -2,7 +2,7 @@
2
2
  "name": "coderook",
3
3
  "displayName": "CodeRook",
4
4
  "description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, from Claude Code.",
5
- "version": "0.31.5",
5
+ "version": "0.31.7",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -10,6 +10,7 @@ exports.projects = projects;
10
10
  exports.findPublicProject = findPublicProject;
11
11
  exports.findProject = findProject;
12
12
  exports.health = health;
13
+ exports.requestReviews = requestReviews;
13
14
  exports.openProposal = openProposal;
14
15
  exports.proposals = proposals;
15
16
  exports.mergeTracks = mergeTracks;
@@ -267,6 +268,11 @@ async function health() {
267
268
  });
268
269
  return (await response.json());
269
270
  }
271
+ /** Ask people, by username, to review a merge. */
272
+ async function requestReviews(mergeTrackId, usernames) {
273
+ const body = await call(`/v1/merge-tracks/${mergeTrackId}/review-requests`, { method: "POST", body: { reviewers: usernames } });
274
+ return body.asked;
275
+ }
270
276
  /** Offer a line of work to another, usually main. */
271
277
  async function openProposal(repositoryId, input) {
272
278
  return call(`/v1/repositories/${repositoryId}/proposals`, {
@@ -1667,6 +1667,17 @@ async function commandMerge(parsed) {
1667
1667
  const folder = folderFor({ ...parsed, positional: parsed.positional.slice(1) });
1668
1668
  const summary = await findMerge(folder, reference);
1669
1669
  const detail = await (0, api_js_1.mergeTrack)(summary.id);
1670
+ /*
1671
+ Asking people to look. Named the way people name each other, and said
1672
+ back by name, so a typo in a username is visible rather than silent.
1673
+ */
1674
+ const ask = flagText(parsed, "ask");
1675
+ if (ask) {
1676
+ const asked = await (0, api_js_1.requestReviews)(summary.id, ask.split(",").map((name) => name.trim()).filter(Boolean));
1677
+ console.log(asked.length
1678
+ ? `Asked ${asked.map((person) => person.displayName).join(", ")} to review ${summary.reference}.`
1679
+ : "Nobody new was asked.");
1680
+ }
1670
1681
  if (hasFlag(parsed, "cancel")) {
1671
1682
  await (0, api_js_1.cancelMerge)(summary.id);
1672
1683
  console.log(`${summary.reference} cancelled. Your upload is still stored and nothing published was touched.`);
@@ -1714,6 +1725,20 @@ async function commandMerge(parsed) {
1714
1725
  for (const blocked of now.provisional.blockedByChecks ?? []) {
1715
1726
  console.log(` ${red("check")} ${blocked}`);
1716
1727
  }
1728
+ for (const one of now.changeRequest?.reviews ?? []) {
1729
+ if (one.verdict === "comment" && !one.body)
1730
+ continue;
1731
+ const said = one.verdict === "approve"
1732
+ ? accent("approved")
1733
+ : one.verdict === "request_changes"
1734
+ ? red("asked for changes")
1735
+ : dim("commented");
1736
+ console.log(` ${one.reviewerName ?? "A reviewer"} ${said}${one.body ? dim(`: ${one.body}`) : ""}`);
1737
+ }
1738
+ const waiting = (now.changeRequest?.requested ?? []).filter((person) => !person.reviewed);
1739
+ if (waiting.length) {
1740
+ console.log(dim(` waiting on ${waiting.map((person) => person.displayName).join(", ")}`));
1741
+ }
1717
1742
  }
1718
1743
  console.log(`
1719
1744
  ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
@@ -2556,12 +2581,16 @@ const SPECS = [
2556
2581
  options: [
2557
2582
  { flags: "--ship <action>", description: "its builds become downloads on the version" },
2558
2583
  { flags: "--no-ship <action>", description: "keep its files without attaching them" },
2584
+ { flags: "--on-save <action>", description: "run it on every save" },
2585
+ { flags: "--on-proposal <action>", description: "run it on proposals; its result becomes a check" },
2586
+ { flags: "--by-hand <action>", description: "run it only when somebody starts it" },
2559
2587
  { flags: "--project <name>", description: "which project" },
2560
2588
  ],
2561
2589
  examples: [
2562
2590
  "cbx actions",
2563
2591
  'cbx actions --ship "Windows installer"',
2564
2592
  'cbx actions --no-ship "Windows installer"',
2593
+ 'cbx actions --on-proposal Tests',
2565
2594
  ],
2566
2595
  run: service_commands_js_1.commandWorkflows,
2567
2596
  },
@@ -2686,6 +2715,7 @@ const SPECS = [
2686
2715
  { flags: "--theirs", description: "keep theirs" },
2687
2716
  { flags: "--both", description: "keep both" },
2688
2717
  { flags: "--drop", description: "abandon the upload" },
2718
+ { flags: "--ask <usernames>", description: "ask people to review it, comma separated" },
2689
2719
  ],
2690
2720
  run: commandMerge,
2691
2721
  },
@@ -314,6 +314,46 @@ async function commandWorkflows(parsed) {
314
314
  }
315
315
  console.log("");
316
316
  }
317
+ /*
318
+ When an action runs. A trigger used to be a label nothing acted on; now a
319
+ save or a proposal really does start the actions that asked for it, so
320
+ choosing is worth a flag.
321
+ */
322
+ const WHEN = [
323
+ ["on-save", "version_saved", "runs on every save"],
324
+ ["on-proposal", "merge_opened", "runs when a line is proposed, and again on each save to it"],
325
+ ["by-hand", "manual", "runs when somebody starts it"],
326
+ ];
327
+ for (const [flag, trigger, meaning] of WHEN) {
328
+ const named = parsed.flags.get(flag);
329
+ if (named === undefined)
330
+ continue;
331
+ if (typeof named !== "string" || !named.trim()) {
332
+ console.error(red("Which action?") + dim(` cbx actions --${flag} "Tests"`));
333
+ return 1;
334
+ }
335
+ const target = found.find((workflow) => workflow.name.toLowerCase() === named.trim().toLowerCase());
336
+ if (!target) {
337
+ console.error(red(`${project.name} has no action called ${named.trim()}.`));
338
+ if (found.length) {
339
+ console.error(dim(` It has: ${found.map((one) => one.name).join(", ")}`));
340
+ }
341
+ return 1;
342
+ }
343
+ try {
344
+ await (0, api_js_1.changeWorkflow)(project.id, target.id, { trigger });
345
+ }
346
+ catch (error) {
347
+ console.error(red(error instanceof Error ? error.message : String(error)));
348
+ return 1;
349
+ }
350
+ target.trigger = trigger;
351
+ console.log(green(`${target.name} ${meaning}.`));
352
+ if (trigger === "merge_opened") {
353
+ console.log(dim(" Its result becomes a required check on the proposal."));
354
+ }
355
+ console.log("");
356
+ }
317
357
  if (!found.length) {
318
358
  console.log(dim("No actions on this project."));
319
359
  return 0;
@@ -256,8 +256,36 @@ class Tracks {
256
256
  ready: body.provisional?.ready ?? false,
257
257
  unresolvedPaths: body.provisional?.unresolvedPaths ?? [],
258
258
  blockedByChecks: body.provisional?.blockedByChecks ?? [],
259
+ proposal: body.proposal ?? null,
260
+ request: body.changeRequest
261
+ ? { ...body.changeRequest, requested: body.changeRequest.requested ?? [] }
262
+ : null,
259
263
  };
260
264
  }
265
+ /**
266
+ * Offer a line to another, usually main.
267
+ *
268
+ * The service reads both lines, combines what it can, and answers with the
269
+ * merge it opened — which the window then shows like any other.
270
+ */
271
+ async propose(repositoryId, input) {
272
+ const body = await this.call(`/v1/repositories/${repositoryId}/proposals`, {
273
+ method: "POST",
274
+ body: JSON.stringify(input.body ? input : { from: input.from, into: input.into, title: input.title }),
275
+ });
276
+ return {
277
+ mergeTrackId: body.mergeTrack.id,
278
+ reference: body.mergeTrack.reference,
279
+ unresolved: body.conflicts.unresolved,
280
+ };
281
+ }
282
+ /** Say where you stand on a proposal. */
283
+ async review(mergeTrackId, verdict, note) {
284
+ await this.call(`/v1/merge-tracks/${mergeTrackId}/review`, {
285
+ method: "PUT",
286
+ body: JSON.stringify(note ? { verdict, body: note } : { verdict }),
287
+ });
288
+ }
261
289
  /**
262
290
  * One side of a conflict, as text.
263
291
  *
@@ -1623,6 +1623,8 @@ class Uploader {
1623
1623
  sourceBytes: totalBytes,
1624
1624
  excludedBytes: 0,
1625
1625
  objects: [...this.plannedObjects.values()],
1626
+ /* So a protected line is refused before anything is sent. */
1627
+ track: request.track ?? "main",
1626
1628
  }),
1627
1629
  }));
1628
1630
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.31.5",
3
+ "version": "0.31.7",
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",