@coderook/cli 0.43.0 → 0.44.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.
@@ -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, with cbx or plain git, from Claude Code.",
5
- "version": "0.43.0",
5
+ "version": "0.44.0",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -739,7 +739,7 @@ async function commandImport(parsed) {
739
739
  flags: submitFlags,
740
740
  });
741
741
  if (code === 0 && plan.temporary) {
742
- console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
742
+ console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
743
743
  ` +
744
744
  `Run ${accent("cbx get " + plan.name)} anywhere to fetch it fresh.`));
745
745
  }
@@ -1025,17 +1025,17 @@ async function commandSubmit(parsed) {
1025
1025
  */
1026
1026
  const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
1027
1027
  if (shielded.length && !hasFlag(parsed, "allow-private")) {
1028
- console.log(red(`
1028
+ console.log(red(`
1029
1029
  ${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
1030
1030
  for (const finding of shielded) {
1031
1031
  console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
1032
1032
  }
1033
- console.log(`
1033
+ console.log(`
1034
1034
  Nothing was sent. To leave them behind:`);
1035
1035
  for (const finding of shielded) {
1036
1036
  console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
1037
1037
  }
1038
- console.error(`
1038
+ console.error(`
1039
1039
  Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
1040
1040
  return 1;
1041
1041
  }
@@ -1084,7 +1084,7 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
1084
1084
  `rest were not opened.`));
1085
1085
  }
1086
1086
  if (stillPasted.length) {
1087
- console.log(red(`
1087
+ console.log(red(`
1088
1088
  ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
1089
1089
  for (const finding of stillPasted.slice(0, 20)) {
1090
1090
  console.log(` ${finding.path}`);
@@ -1099,11 +1099,11 @@ ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s
1099
1099
  console.log(dim(` …and ${stillPasted.length - 20} more`));
1100
1100
  }
1101
1101
  if (!hasFlag(parsed, "allow-secrets")) {
1102
- console.error(`
1102
+ console.error(`
1103
1103
  Nothing was sent. Move the key into an environment variable, and if` +
1104
1104
  ` it has ever been published, replace it at the service that issued` +
1105
1105
  ` it — a key that has leaked stays leaked.` +
1106
- `
1106
+ `
1107
1107
  Pass ${accent("--allow-secrets")} if these are not real keys.`);
1108
1108
  return 1;
1109
1109
  }
@@ -1309,7 +1309,7 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
1309
1309
  continue;
1310
1310
  }
1311
1311
  if (failure?.kind === "interrupted") {
1312
- console.error(red(`
1312
+ console.error(red(`
1313
1313
  The connection failed: ${text}`));
1314
1314
  console.error(`Your work may already have been saved. Run the same command again —` +
1315
1315
  ` it will not create a second version.`);
@@ -1566,7 +1566,7 @@ async function commandGet(parsed) {
1566
1566
  `changed while the interrupted fetch was stopped:`));
1567
1567
  for (const file of changedInTheGap.slice(0, 20))
1568
1568
  console.error(` ${file.path}`);
1569
- console.error(`
1569
+ console.error(`
1570
1570
  Save them with ${accent("cbx submit")}, or finish the fetch and ` +
1571
1571
  `discard them with ${accent("cbx get --replace")}.`);
1572
1572
  return 1;
@@ -1849,8 +1849,8 @@ async function suggestRules(folder, apply) {
1849
1849
  const addition = (0, detect_js_1.rulesFromSuggestions)(recommended);
1850
1850
  const shared = rules.shared.trimEnd();
1851
1851
  await (0, worktree_js_1.writeRules)(folder, {
1852
- shared: shared ? `${shared}
1853
-
1852
+ shared: shared ? `${shared}
1853
+
1854
1854
  ${addition}` : addition,
1855
1855
  local: rules.local,
1856
1856
  });
@@ -2055,7 +2055,7 @@ async function commandMerges(parsed) {
2055
2055
  const counts = merge.conflicts;
2056
2056
  console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
2057
2057
  }
2058
- console.log(dim(`
2058
+ console.log(dim(`
2059
2059
  Run cbx merge <reference> to look at one.`));
2060
2060
  return 0;
2061
2061
  }
@@ -2158,7 +2158,7 @@ async function commandMerge(parsed) {
2158
2158
  console.log(dim(` waiting on ${waiting.map((person) => person.displayName).join(", ")}`));
2159
2159
  }
2160
2160
  }
2161
- console.log(`
2161
+ console.log(`
2162
2162
  ${accent(now.mergeTrack.reference)} · ` +
2163
2163
  /*
2164
2164
  The combined version is only counted once it can be built; until then
@@ -2177,7 +2177,7 @@ ${accent(now.mergeTrack.reference)} · ` +
2177
2177
  return 1;
2178
2178
  }
2179
2179
  const applied = await (0, api_js_1.applyMerge)(summary.id);
2180
- console.log(`
2180
+ console.log(`
2181
2181
  Applied as ${accent(`v${applied.version.sequence}`)}.`);
2182
2182
  if (applied.closedIssues?.length) {
2183
2183
  console.log(`Closed ${applied.closedIssues.map((number) => `#${number}`).join(", ")}.`);
@@ -2187,13 +2187,13 @@ Applied as ${accent(`v${applied.version.sequence}`)}.`);
2187
2187
  }
2188
2188
  const stillOpen = now.conflicts.some((conflict) => !conflict.resolvedAt);
2189
2189
  if (!decision && stillOpen) {
2190
- console.log(dim(`
2190
+ console.log(dim(`
2191
2191
  --mine keeps yours, --theirs keeps what was already saved,` +
2192
- ` --both keeps theirs and saves yours beside it, --drop removes the file.
2192
+ ` --both keeps theirs and saves yours beside it, --drop removes the file.
2193
2193
  Add --path <file> for one file, then --apply when ready.`));
2194
2194
  }
2195
2195
  else if (now.provisional.ready) {
2196
- console.log(dim(`
2196
+ console.log(dim(`
2197
2197
  Run cbx merge ${now.mergeTrack.reference} --apply to publish it.`));
2198
2198
  }
2199
2199
  return 0;
@@ -2786,9 +2786,9 @@ const SPECS = [
2786
2786
  name: "workspace",
2787
2787
  group: "Workspaces",
2788
2788
  summary: "edit, save and run a private project remotely without a checkout",
2789
- usage: "workspace | workspace <usage|open|list|show|read|files|search|diff|patch|put|starter|save|run|logs|cancel|deploy|preview|handoff|close|recipe|assign|assignments|unassign|checkout|say|thread|mute|unmute> <project> [workspace] [job] | workspace put <project> <workspace> <path…> | workspace push [folder] | workspace watch [project] [workspace]",
2789
+ usage: "workspace | workspace <usage|open|list|show|read|files|search|diff|patch|put|starter|review|reviewers|save|run|logs|cancel|deploy|preview|handoff|close|recipe|assign|assignments|unassign|checkout|say|thread|mute|unmute> <project> [workspace] [job] | workspace put <project> <workspace> <path…> | workspace push [folder] | workspace watch [project] [workspace]",
2790
2790
  brief: "workspace <action> <project>",
2791
- detail: "Works on CodeRook's durable remote draft, never the current local folder. Use --data-file for structured edits or recipe configuration. Mutations need the reviewed --revision and a unique --key; reuse the same key and request when retrying. Recipes reference existing project workflows. Usage reads the shared workspace credits and separate PR allowance without a draft ID. put copies files from this folder into a draft: text as text, images, fonts and other binary files as bytes (up to 1 MiB each), a file that is gone here is removed there, and each file must still be the version the draft holds, so a teammate's edit is never overwritten. starter reads this folder (package.json scripts, Cargo.toml, go.mod, Python) and suggests one check recipe per package so drafts have something to run; --apply adds them, never replacing a recipe that exists. Saving and deploying are separate operations. Run on its own at a terminal, it lists your open workspaces with their unread messages: choose one to see its changes and what is being discussed, and reply, save, run checks, hand it over or close it with one key. say and thread post to and read a workspace's discussion as JSON; a machine signed in to your account can name itself with --as (or CBX_NAME) so its messages and unread count are its own. watch prints each new message, each message addressed to you (@your name, or the name given with --as) and each new saved version as it happens, until stopped; --json gives one JSON object per line for a program.",
2791
+ detail: "Works on CodeRook's durable remote draft, never the current local folder. Use --data-file for structured edits or recipe configuration. Mutations need the reviewed --revision and a unique --key; reuse the same key and request when retrying. Recipes reference existing project workflows. Usage reads the shared workspace credits and separate PR allowance without a draft ID. put copies files from this folder into a draft: text as text, images, fonts and other binary files as bytes (up to 1 MiB each), a file that is gone here is removed there, and each file must still be the version the draft holds, so a teammate's edit is never overwritten. starter reads this folder (package.json scripts, Cargo.toml, go.mod, Python) and suggests one check recipe per package so drafts have something to run; --apply adds them, never replacing a recipe that exists. review shows who must OK a draft before it is saved or deployed; --ok or --changes -m gives your verdict on the current revision, and counts only from the reviewer's own sign-in or token, never a name given with --as. reviewers lists the project's rules, and an administrator sets them with --data-file. Saving and deploying are separate operations. Run on its own at a terminal, it lists your open workspaces with their unread messages: choose one to see its changes and what is being discussed, and reply, save, run checks, hand it over or close it with one key. say and thread post to and read a workspace's discussion as JSON; a machine signed in to your account can name itself with --as (or CBX_NAME) so its messages and unread count are its own. watch prints each new message, each message addressed to you (@your name, or the name given with --as) and each new saved version as it happens, until stopped; --json gives one JSON object per line for a program.",
2792
2792
  options: [
2793
2793
  { flags: "--data-file <path>", description: "read an operation's JSON from this local file" },
2794
2794
  { flags: "--name <name>", description: "name a new workspace" },
@@ -2815,10 +2815,12 @@ const SPECS = [
2815
2815
  { flags: "--after <time>", description: "thread: continue from the next page's time" },
2816
2816
  { flags: "--root <folder>", description: "put/starter: the project folder (default: here)" },
2817
2817
  { flags: "--apply", description: "starter: create the suggested workflows and recipes" },
2818
+ { flags: "--ok", description: "review: approve the draft's current revision" },
2819
+ { flags: "--changes", description: "review: ask for changes (say what with -m)" },
2818
2820
  { flags: "--json", description: "watch: one JSON object per line; starter: the plan as JSON" },
2819
2821
  { flags: "--interval <seconds>", description: "watch: how often to look (default 20, at least 5)" },
2820
2822
  ],
2821
- examples: ["cbx workspace open my-project --name \"Laptop work\" --key laptop-open-001", "cbx workspace list my-project", "cbx workspace assign my-project <draft> --name Laptop", "cbx workspace checkout my-project <draft>", "cbx workspace push --save -m \"Fix the export dialog\"", "cbx workspace", "cbx workspace say my-project <draft> -m \"Tests updated\" --as GrokBot", "cbx workspace thread my-project <draft> --unread", "cbx workspace watch my-project --as GrokBot --json", "cbx workspace put my-project <draft> website/app/page.tsx website/public/og.png", "cbx workspace starter my-project --apply"],
2823
+ examples: ["cbx workspace open my-project --name \"Laptop work\" --key laptop-open-001", "cbx workspace list my-project", "cbx workspace assign my-project <draft> --name Laptop", "cbx workspace checkout my-project <draft>", "cbx workspace push --save -m \"Fix the export dialog\"", "cbx workspace", "cbx workspace say my-project <draft> -m \"Tests updated\" --as GrokBot", "cbx workspace thread my-project <draft> --unread", "cbx workspace watch my-project --as GrokBot --json", "cbx workspace put my-project <draft> website/app/page.tsx website/public/og.png", "cbx workspace starter my-project --apply", "cbx workspace review my-project <draft> --ok -m \"Matches the concept\""],
2822
2824
  run: workspace_command_js_1.commandWorkspace,
2823
2825
  },
2824
2826
  {
@@ -10,6 +10,7 @@ const config_js_1 = require("./config.js");
10
10
  const workspace_machine_js_1 = require("./workspace_machine.js");
11
11
  const workspace_threads_js_1 = require("./workspace_threads.js");
12
12
  const workspace_put_js_1 = require("./workspace_put.js");
13
+ const workspace_review_js_1 = require("./workspace_review.js");
13
14
  const workspace_starter_js_1 = require("./workspace_starter.js");
14
15
  const workspace_watch_js_1 = require("./workspace_watch.js");
15
16
  async function commandWorkspace(parsed) {
@@ -30,6 +31,13 @@ async function commandWorkspace(parsed) {
30
31
  const root = `/v1/repositories/${encodeURIComponent(project.id)}`;
31
32
  if (operation === "starter")
32
33
  return (0, workspace_starter_js_1.commandWorkspaceStarter)(parsed, root);
34
+ if (operation === "reviewers")
35
+ return (0, workspace_review_js_1.commandWorkspaceReviewers)(parsed, root);
36
+ if (operation === "review") {
37
+ if (!workspace)
38
+ throw new Error("Supply the workspace ID after the project");
39
+ return (0, workspace_review_js_1.commandWorkspaceReview)(parsed, root, workspace);
40
+ }
33
41
  if (operation === "put") {
34
42
  if (!workspace)
35
43
  throw new Error("Supply the workspace ID after the project");
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeReviewer = describeReviewer;
4
+ exports.commandWorkspaceReview = commandWorkspaceReview;
5
+ exports.commandWorkspaceReviewers = commandWorkspaceReviewers;
6
+ /**
7
+ * cbx workspace review <project> <workspace> --ok [-m "…"] | --changes -m "…"
8
+ * cbx workspace reviewers <project> [--data-file rules.json]
9
+ *
10
+ * The CLI side of the review gate (backend workspace_reviews.ts). A review
11
+ * counts only from the reviewer's own sign-in or token, so --as is refused
12
+ * here before the service refuses it, with the reason.
13
+ */
14
+ const promises_1 = require("node:fs/promises");
15
+ const api_js_1 = require("./api.js");
16
+ const colour_js_1 = require("./colour.js");
17
+ const flag = (parsed, ...names) => {
18
+ for (const name of names) {
19
+ const value = parsed.flags.get(name);
20
+ if (typeof value === "string")
21
+ return value;
22
+ }
23
+ return undefined;
24
+ };
25
+ /** One line per reviewer: a word for where it stands, never only a colour. */
26
+ function describeReviewer(s, revision) {
27
+ const when = [s.beforeSave && "save", s.beforeDeploy && "deploy"].filter(Boolean).join(" + ");
28
+ if (!s.applies)
29
+ return `${(0, colour_js_1.dim)("not needed")} ${s.label} ${(0, colour_js_1.dim)(`(no changes in its paths)`)}`;
30
+ const word = s.state === "ok"
31
+ ? (0, colour_js_1.green)("OK ")
32
+ : s.state === "changes"
33
+ ? (0, colour_js_1.red)("changes ")
34
+ : s.state === "outdated"
35
+ ? "outdated "
36
+ : (0, colour_js_1.dim)("waiting ");
37
+ const detail = s.state === "outdated"
38
+ ? `OK was for revision ${s.revision}, draft is at ${revision}`
39
+ : s.state === "changes" || (s.state === "ok" && s.note)
40
+ ? s.note
41
+ : "";
42
+ return `${word}${(0, colour_js_1.bold)(s.label)} ${(0, colour_js_1.dim)(`before ${when}`)}${detail ? ` ${detail}` : ""}`;
43
+ }
44
+ async function commandWorkspaceReview(parsed, root, workspace) {
45
+ if (parsed.flags.has("as") || process.env.CBX_NAME)
46
+ throw new Error("A review counts only from the reviewer's own sign-in or token; run it without --as or CBX_NAME.");
47
+ const ok = parsed.flags.has("ok");
48
+ const changes = parsed.flags.has("changes");
49
+ const draft = `${root}/workspaces/${encodeURIComponent(workspace)}`;
50
+ if (ok === changes) {
51
+ /* Neither: show where the draft stands. */
52
+ const shown = await (0, api_js_1.callApi)(draft);
53
+ const review = shown.review ?? [];
54
+ if (parsed.flags.has("json"))
55
+ console.log(JSON.stringify({ revision: shown.workspace.revision, review }, null, 2));
56
+ else if (!review.length)
57
+ console.log("This project names no reviewers. An administrator sets them with cbx workspace reviewers.");
58
+ else
59
+ for (const s of review)
60
+ console.log(describeReviewer(s, shown.workspace.revision));
61
+ return 0;
62
+ }
63
+ const note = flag(parsed, "m", "message") ?? "";
64
+ if (changes && !note.trim())
65
+ throw new Error('Say what needs to change: --changes -m "…"');
66
+ const revision = parsed.flags.has("revision")
67
+ ? Number(parsed.flags.get("revision"))
68
+ : (await (0, api_js_1.callApi)(draft)).workspace.revision;
69
+ const out = await (0, api_js_1.callApi)(`${draft}/reviews`, {
70
+ method: "POST",
71
+ body: { verdict: ok ? "ok" : "changes", note, expectedRevision: revision },
72
+ });
73
+ if (parsed.flags.has("json"))
74
+ console.log(JSON.stringify(out, null, 2));
75
+ else {
76
+ console.log(`${ok ? (0, colour_js_1.green)("OK") : (0, colour_js_1.red)("Changes asked")} on revision ${revision}.`);
77
+ for (const s of out.review)
78
+ console.log(` ${describeReviewer(s, revision)}`);
79
+ }
80
+ return 0;
81
+ }
82
+ async function commandWorkspaceReviewers(parsed, root) {
83
+ const source = flag(parsed, "data-file");
84
+ const rules = source
85
+ ? (await (0, api_js_1.callApi)(`${root}/workspace-review-rules`, {
86
+ method: "PUT",
87
+ body: { rules: JSON.parse(await (0, promises_1.readFile)(source, "utf8")) },
88
+ })).rules
89
+ : (await (0, api_js_1.callApi)(`${root}/workspace-review-rules`)).rules;
90
+ if (parsed.flags.has("json") || source)
91
+ console.log(JSON.stringify({ rules }, null, 2));
92
+ else if (!rules.length)
93
+ console.log(`No reviewers. Set them from a JSON list with ${(0, colour_js_1.accent)("cbx workspace reviewers <project> --data-file rules.json")}:\n` +
94
+ (0, colour_js_1.dim)(' [{"reviewer":"token:<id>","label":"UX Bot","paths":["website/"],"beforeSave":true,"beforeDeploy":false,"ownerMayOverride":false}]'));
95
+ else
96
+ for (const r of rules)
97
+ console.log(`${(0, colour_js_1.bold)(r.label)} ${(0, colour_js_1.dim)(r.reviewer)} ${r.paths.length ? r.paths.join(", ") : "every path"} ${(0, colour_js_1.dim)(`before ${[r.beforeSave && "save", r.beforeDeploy && "deploy"].filter(Boolean).join(" + ")}${r.ownerMayOverride ? "; owner may go ahead without" : ""}`)}`);
98
+ return 0;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://coderook.com",