@coderook/cli 0.41.2 → 0.42.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.41.2",
5
+ "version": "0.42.0",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -24,6 +24,7 @@ const node_fs_1 = require("node:fs");
24
24
  const node_path_1 = __importDefault(require("node:path"));
25
25
  const node_process_1 = __importDefault(require("node:process"));
26
26
  const workspace_command_js_1 = require("./workspace_command.js");
27
+ const workspace_threads_js_1 = require("./workspace_threads.js");
27
28
  const api_js_1 = require("./api.js");
28
29
  const registry_js_1 = require("./registry.js");
29
30
  const help_js_1 = require("./help.js");
@@ -2755,9 +2756,9 @@ const SPECS = [
2755
2756
  name: "workspace",
2756
2757
  group: "Workspaces",
2757
2758
  summary: "edit, save and run a private project remotely without a checkout",
2758
- usage: "workspace <usage|open|list|show|read|files|search|diff|patch|save|run|logs|cancel|deploy|preview|handoff|close|recipe|assign|assignments|unassign|checkout> <project> [workspace] [job] | workspace push [folder]",
2759
+ usage: "workspace | workspace <usage|open|list|show|read|files|search|diff|patch|save|run|logs|cancel|deploy|preview|handoff|close|recipe|assign|assignments|unassign|checkout|say|thread|mute|unmute> <project> [workspace] [job] | workspace push [folder]",
2759
2760
  brief: "workspace <action> <project>",
2760
- 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. Saving and deploying are separate operations.",
2761
+ 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. 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.",
2761
2762
  options: [
2762
2763
  { flags: "--data-file <path>", description: "read an operation's JSON from this local file" },
2763
2764
  { flags: "--name <name>", description: "name a new workspace" },
@@ -2778,8 +2779,12 @@ const SPECS = [
2778
2779
  { flags: "--days <number>", description: "assign: how long the handed-over token lasts (1-90, default 14)" },
2779
2780
  { flags: "--save", description: "push: save the draft after sending the changes" },
2780
2781
  { flags: "--dry-run", description: "push: list what would be sent, send nothing" },
2782
+ { flags: "--as <name>", description: "say/thread/list/save: post and read as this machine name (or set CBX_NAME)" },
2783
+ { flags: "--unread", description: "thread: only messages this reader has not read" },
2784
+ { flags: "--peek", description: "thread: read without marking anything read" },
2785
+ { flags: "--after <time>", description: "thread: continue from the next page's time" },
2781
2786
  ],
2782
- 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\""],
2787
+ 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"],
2783
2788
  run: workspace_command_js_1.commandWorkspace,
2784
2789
  },
2785
2790
  {
@@ -3831,6 +3836,7 @@ const SPECS = [
3831
3836
  description: "everything, versions, issues, releases, ignore",
3832
3837
  },
3833
3838
  { flags: "--email [off]", description: "include it in the daily email" },
3839
+ { flags: "--discussions <all|off>", description: "whether workspace messages reach your notifications" },
3834
3840
  ],
3835
3841
  run: service_commands_js_1.commandWatch,
3836
3842
  },
@@ -4135,7 +4141,11 @@ async function main(argv) {
4135
4141
  if (spec.deprecatedBy) {
4136
4142
  console.error(dim(`"${name}" is now "${spec.deprecatedBy}". The old name still works.`));
4137
4143
  }
4138
- return spec.run(parse(rest, spec));
4144
+ const parsed = parse(rest, spec);
4145
+ const code = await spec.run(parsed);
4146
+ /* One line when a workspace thread has unread messages: terminals only, at most every few minutes. */
4147
+ await (0, workspace_threads_js_1.unreadReminder)(name, parsed);
4148
+ return code;
4139
4149
  }
4140
4150
  /**
4141
4151
  * Set the status and let Node wind down on its own.
@@ -421,6 +421,23 @@ async function commandWatch(parsed) {
421
421
  if (parsed.flags.has("email")) {
422
422
  change.emailDigest = parsed.flags.get("email") !== "off";
423
423
  }
424
+ /* Workspace discussions are their own switch (workspace_threads.ts). */
425
+ const discussions = parsed.flags.get("discussions");
426
+ if (discussions !== undefined) {
427
+ if (discussions !== "all" && discussions !== "off") {
428
+ console.error((0, colour_js_1.red)("--discussions must be all or off"));
429
+ return 1;
430
+ }
431
+ await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(project.id)}/workspace-discussions/watch`, {
432
+ method: "PUT",
433
+ body: { level: discussions },
434
+ });
435
+ console.log((0, colour_js_1.green)(discussions === "all"
436
+ ? `Workspace discussions in ${project.name}: new messages reach your notifications.`
437
+ : `Workspace discussions in ${project.name}: off. Threads still show unread counts.`));
438
+ if (!Object.keys(change).length)
439
+ return 0;
440
+ }
424
441
  if (Object.keys(change).length) {
425
442
  await (0, api_js_1.setWatch)(project.id, change);
426
443
  console.log((0, colour_js_1.green)(`Updated what reaches you about ${project.name}.`));
@@ -7,16 +7,26 @@ const node_readline_1 = require("node:readline");
7
7
  const api_js_1 = require("./api.js");
8
8
  const config_js_1 = require("./config.js");
9
9
  const workspace_machine_js_1 = require("./workspace_machine.js");
10
+ const workspace_threads_js_1 = require("./workspace_threads.js");
10
11
  async function commandWorkspace(parsed) {
11
12
  const [operation, projectRef, workspace, runId] = parsed.positional;
12
13
  if (operation && workspace_machine_js_1.MACHINE_OPERATIONS.has(operation))
13
14
  return (0, workspace_machine_js_1.commandWorkspaceMachine)(parsed);
15
+ /* On its own at a terminal: choose a workspace and see what is being discussed. Scripts get the usage line as before. */
16
+ if (!operation && process.stdin.isTTY && process.stdout.isTTY)
17
+ return (0, workspace_threads_js_1.interactiveWorkspaces)(parsed);
14
18
  if (!operation || !projectRef)
15
19
  throw new Error("Use cbx workspace <operation> <project> [workspace]. See cbx help workspace.");
16
20
  const project = await (0, api_js_1.findProject)(projectRef);
17
21
  if (!project)
18
22
  throw new Error(`No accessible project ${projectRef}`);
19
23
  const root = `/v1/repositories/${encodeURIComponent(project.id)}`;
24
+ if (workspace_threads_js_1.THREAD_OPERATIONS.has(operation)) {
25
+ if (!workspace)
26
+ throw new Error("Supply the workspace ID after the project");
27
+ return (0, workspace_threads_js_1.commandWorkspaceThread)(operation, root, workspace, parsed);
28
+ }
29
+ const named = (0, workspace_threads_js_1.authorName)(parsed);
20
30
  const flag = (name) => {
21
31
  const value = parsed.flags.get(name);
22
32
  return typeof value === "string" ? value : undefined;
@@ -41,7 +51,11 @@ async function commandWorkspace(parsed) {
41
51
  const key = flag("key") ?? data.operationKey;
42
52
  const revision = integer("revision") ?? data.expectedRevision;
43
53
  let path = `${root}/workspaces`, method = "GET", body;
44
- if (operation === "usage") {
54
+ if (operation === "list") {
55
+ if (named)
56
+ path += `?as=${encodeURIComponent(named)}`;
57
+ }
58
+ else if (operation === "usage") {
45
59
  path = `${root}/workspace-usage`;
46
60
  }
47
61
  else if (operation === "open") {
@@ -95,6 +109,7 @@ async function commandWorkspace(parsed) {
95
109
  operationKey: key,
96
110
  expectedRevision: revision,
97
111
  message: flag("message") ?? flag("m") ?? data.message,
112
+ ...(named ? { as: named } : {}),
98
113
  };
99
114
  }
100
115
  else if (operation === "run" ||
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.THREAD_OPERATIONS = void 0;
7
+ exports.authorName = authorName;
8
+ exports.commandWorkspaceThread = commandWorkspaceThread;
9
+ exports.unreadReminder = unreadReminder;
10
+ exports.interactiveWorkspaces = interactiveWorkspaces;
11
+ /**
12
+ * Workspace discussions in the terminal (approved concept, 2026-11-05).
13
+ *
14
+ * cbx workspace at a terminal: choose a workspace, see what is
15
+ * being discussed, and act on it with one key
16
+ * cbx workspace say <p> <w> -m … post to a workspace's thread (JSON)
17
+ * cbx workspace thread <p> <w> read it (JSON); --unread, --peek
18
+ * cbx workspace mute|unmute <p> <w>
19
+ *
20
+ * A machine signed in to the account can name itself with --as or CBX_NAME, so
21
+ * its messages read as its own ("GrokBot · your account") and it keeps its own
22
+ * read point. A machine a draft was handed to is named by its handover already.
23
+ *
24
+ * Any other command ends with one line when something is unread (unreadReminder),
25
+ * at most every few minutes and only at a terminal, so scripts never see it.
26
+ */
27
+ const node_crypto_1 = require("node:crypto");
28
+ const promises_1 = require("node:fs/promises");
29
+ const node_path_1 = __importDefault(require("node:path"));
30
+ const node_readline_1 = __importDefault(require("node:readline"));
31
+ const api_js_1 = require("./api.js");
32
+ const colour_js_1 = require("./colour.js");
33
+ const config_js_1 = require("./config.js");
34
+ exports.THREAD_OPERATIONS = new Set(["say", "thread", "mute", "unmute"]);
35
+ /** The name this machine posts and reads as, if it gave one. */
36
+ function authorName(parsed) {
37
+ const flag = parsed.flags.get("as");
38
+ const name = typeof flag === "string" ? flag : process.env.CBX_NAME;
39
+ return name?.trim() || undefined;
40
+ }
41
+ const asQuery = (name) => (name ? `as=${encodeURIComponent(name)}` : "");
42
+ const join = (...parts) => {
43
+ const query = parts.filter(Boolean).join("&");
44
+ return query ? `?${query}` : "";
45
+ };
46
+ /** say, thread, mute and unmute: plain JSON, like every other workspace operation. */
47
+ async function commandWorkspaceThread(operation, root, workspaceId, parsed) {
48
+ const name = authorName(parsed);
49
+ const base = `${root}/workspaces/${encodeURIComponent(workspaceId)}/messages`;
50
+ let result;
51
+ if (operation === "say") {
52
+ const message = parsed.flags.get("message") ?? parsed.flags.get("m");
53
+ if (typeof message !== "string" || !message.trim())
54
+ throw new Error('Write the message with -m "…"');
55
+ result = await (0, api_js_1.callApi)(base, { method: "POST", body: { body: message, ...(name ? { as: name } : {}) } });
56
+ }
57
+ else if (operation === "thread") {
58
+ const after = parsed.flags.get("after");
59
+ result = await (0, api_js_1.callApi)(base +
60
+ join(asQuery(name), parsed.flags.has("unread") ? "unread=true" : "", parsed.flags.has("peek") ? "peek=true" : "", typeof after === "string" ? `after=${encodeURIComponent(after)}` : ""));
61
+ }
62
+ else {
63
+ result = await (0, api_js_1.callApi)(`${base}/mute`, {
64
+ method: "PUT",
65
+ body: { muted: operation === "mute", ...(name ? { as: name } : {}) },
66
+ });
67
+ }
68
+ console.log(JSON.stringify(result, null, 2));
69
+ return 0;
70
+ }
71
+ /* ----------------------------------------------------------- the reminder */
72
+ const REMIND_EVERY_MS = 5 * 60_000;
73
+ /**
74
+ * One line after a command when a workspace thread has unread messages. Only
75
+ * at a terminal, at most every five minutes, never slower than 1.5 s, and
76
+ * silent on any failure: it must not change what a command does or prints in
77
+ * a script.
78
+ */
79
+ async function unreadReminder(command, parsed) {
80
+ if (!process.stdout.isTTY || process.env.CODEROOK_NO_REMINDERS)
81
+ return;
82
+ if (["workspace", "sign-in", "sign-out", "help", "mcp"].includes(command))
83
+ return;
84
+ const stamp = node_path_1.default.join((0, config_js_1.configDirectory)(), "discussions-checked");
85
+ try {
86
+ const last = Number((await (0, promises_1.readFile)(stamp, "utf8")).trim());
87
+ if (Date.now() - last < REMIND_EVERY_MS)
88
+ return;
89
+ }
90
+ catch {
91
+ /* Never checked on this machine. */
92
+ }
93
+ try {
94
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(stamp), { recursive: true });
95
+ await (0, promises_1.writeFile)(stamp, String(Date.now()));
96
+ const found = await Promise.race([
97
+ (0, api_js_1.callApi)(`/v1/account/workspace-threads${join(asQuery(authorName(parsed)))}`),
98
+ new Promise((resolve) => setTimeout(() => resolve(null), 1500).unref()),
99
+ ]);
100
+ const waiting = found?.workspaces.filter((w) => w.unread > 0) ?? [];
101
+ if (!waiting.length)
102
+ return;
103
+ const total = waiting.reduce((n, w) => n + w.unread, 0);
104
+ const where = waiting.length === 1 ? `in ${waiting[0].name}` : `in ${waiting.length} workspaces`;
105
+ console.log(`\n${(0, colour_js_1.accent)("●")} ${(0, colour_js_1.bold)(`${total} new message${total === 1 ? "" : "s"}`)} ${(0, colour_js_1.dim)(where)} ${(0, colour_js_1.dim)("· cbx workspace")}`);
106
+ }
107
+ catch {
108
+ /* A reminder is never worth an error. */
109
+ }
110
+ }
111
+ /* ----------------------------------------------------------- the picker */
112
+ const when = (iso) => {
113
+ const date = new Date(iso);
114
+ const today = new Date();
115
+ if (date.toDateString() === today.toDateString())
116
+ return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
117
+ const yesterday = new Date(today.valueOf() - 86_400_000);
118
+ if (date.toDateString() === yesterday.toDateString())
119
+ return "yesterday";
120
+ return date.toLocaleDateString(undefined, { day: "numeric", month: "short" });
121
+ };
122
+ const pad = (text, width) => (text.length > width ? `${text.slice(0, width - 1)}…` : text.padEnd(width));
123
+ function who(message) {
124
+ const label = message.author.label;
125
+ if (message.author.kind === "person")
126
+ return (0, colour_js_1.accent)("You");
127
+ if (message.author.kind === "named")
128
+ return `${(0, colour_js_1.bold)(label)} ${(0, colour_js_1.dim)("· your account")}`;
129
+ return `${(0, colour_js_1.bold)(label)} ${(0, colour_js_1.dim)("· handed over by you")}`;
130
+ }
131
+ function renderMessage(message) {
132
+ const time = (0, colour_js_1.dim)(when(message.createdAt));
133
+ if (message.kind === "event")
134
+ return ` ${(0, colour_js_1.green)("✓")} ${(0, colour_js_1.dim)(message.body)} ${(0, colour_js_1.dim)("·")} ${time}`;
135
+ const body = message.deleted ? (0, colour_js_1.dim)("(deleted)") : message.body.split("\n").map((line) => ` ${line}`).join("\n");
136
+ return ` ${who(message)} ${(0, colour_js_1.dim)("·")} ${time}${message.editedAt ? (0, colour_js_1.dim)(" · edited") : ""}\n${body}`;
137
+ }
138
+ /** Read one key at a terminal. */
139
+ function key() {
140
+ return new Promise((resolve) => {
141
+ const handler = (sequence, info) => {
142
+ process.stdin.off("keypress", handler);
143
+ resolve({ name: info?.name ?? sequence, sequence, ctrl: Boolean(info?.ctrl) });
144
+ };
145
+ process.stdin.on("keypress", handler);
146
+ });
147
+ }
148
+ function raw(on) {
149
+ if (process.stdin.isTTY)
150
+ process.stdin.setRawMode(on);
151
+ if (on)
152
+ process.stdin.resume();
153
+ else
154
+ process.stdin.pause();
155
+ }
156
+ /** Ask for a line of text, out of raw mode. */
157
+ async function ask(prompt) {
158
+ raw(false);
159
+ const lines = node_readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
160
+ try {
161
+ return (await new Promise((resolve) => lines.question(prompt, resolve))).trim();
162
+ }
163
+ finally {
164
+ lines.close();
165
+ }
166
+ }
167
+ const clear = () => process.stdout.write("\x1b[2J\x1b[H");
168
+ /** `cbx workspace` on its own at a terminal. */
169
+ async function interactiveWorkspaces(parsed) {
170
+ const name = authorName(parsed);
171
+ node_readline_1.default.emitKeypressEvents(process.stdin);
172
+ let selected = 0;
173
+ let filter = "";
174
+ try {
175
+ for (;;) {
176
+ const listed = await (0, api_js_1.callApi)(`/v1/account/workspace-threads${join(asQuery(name))}`);
177
+ const shown = listed.workspaces.filter((w) => `${w.project} ${w.name}`.toLowerCase().includes(filter.toLowerCase()));
178
+ selected = Math.min(selected, Math.max(0, shown.length - 1));
179
+ clear();
180
+ console.log(`${(0, colour_js_1.dim)("Open workspaces")}${name ? (0, colour_js_1.dim)(` as ${name}`) : ""} ${(0, colour_js_1.dim)("· ↑↓ choose · Enter open · / filter · q quit")}\n`);
181
+ if (!shown.length)
182
+ console.log((0, colour_js_1.dim)(filter ? ` Nothing matches "${filter}".` : " No open workspaces. Start one with cbx workspace open <project> --name …"));
183
+ shown.forEach((w, index) => {
184
+ const count = w.unread ? (0, colour_js_1.accent)(pad(`${w.unread} new`, 7)) : (0, colour_js_1.dim)(pad("read", 7));
185
+ const line = ` ${index === selected ? (0, colour_js_1.accent)("›") : " "} ${pad(w.project, 14)} ${pad(w.name, 40)} ${count} ${(0, colour_js_1.dim)(when(w.activeAt))}`;
186
+ console.log(index === selected ? (0, colour_js_1.bold)(line) : line);
187
+ });
188
+ console.log((0, colour_js_1.dim)(`\n${shown.length} open${filter ? ` matching "${filter}"` : ""} · ${listed.archived} archived (cbx workspace list <project> shows them)`));
189
+ raw(true);
190
+ const pressed = await key();
191
+ if (pressed.ctrl && pressed.name === "c")
192
+ return 0;
193
+ if (pressed.name === "q" || pressed.name === "escape")
194
+ return 0;
195
+ if (pressed.name === "up")
196
+ selected = Math.max(0, selected - 1);
197
+ else if (pressed.name === "down")
198
+ selected = Math.min(shown.length - 1, selected + 1);
199
+ else if (pressed.sequence === "/") {
200
+ filter = await ask("filter › ");
201
+ selected = 0;
202
+ }
203
+ else if (pressed.name === "return" && shown[selected]) {
204
+ const done = await openWorkspace(shown[selected], name);
205
+ if (done)
206
+ return 0;
207
+ }
208
+ }
209
+ }
210
+ finally {
211
+ raw(false);
212
+ }
213
+ }
214
+ /** One workspace: what changed, what is being said, and what to do. Returns true to leave cbx. */
215
+ async function openWorkspace(listed, name) {
216
+ const root = `/v1/repositories/${encodeURIComponent(listed.projectId)}/workspaces/${encodeURIComponent(listed.id)}`;
217
+ let notice = "";
218
+ for (;;) {
219
+ const shown = await (0, api_js_1.callApi)(root);
220
+ const thread = await (0, api_js_1.callApi)(`${root}/messages${join(asQuery(name))}`);
221
+ const w = shown.workspace;
222
+ clear();
223
+ console.log(`${(0, colour_js_1.bold)(w.name)} ${(0, colour_js_1.dim)(`${listed.project} · ${w.track} · revision ${w.revision}${w.archived ? " · archived" : ""}`)}`);
224
+ const files = shown.changes;
225
+ console.log(files.length
226
+ ? `${(0, colour_js_1.dim)(`${files.length} file${files.length === 1 ? "" : "s"} changed`)} ${(0, colour_js_1.dim)(files.slice(0, 3).map((f) => f.path).join(" · "))}${files.length > 3 ? (0, colour_js_1.dim)(" …") : ""}`
227
+ : (0, colour_js_1.dim)("No changes yet"));
228
+ if (thread.pinned)
229
+ console.log(`\n${(0, colour_js_1.accent)("PINNED")} ${(0, colour_js_1.dim)("handoff note")}\n ${thread.pinned.split("\n").slice(0, 4).join("\n ")}`);
230
+ const recent = thread.messages.slice(-6);
231
+ console.log(`\n${(0, colour_js_1.dim)("Discussion")}${listed.unread ? ` ${(0, colour_js_1.accent)(`${listed.unread} new`)}` : ""}${thread.muted ? (0, colour_js_1.dim)(" · muted") : ""}`);
232
+ console.log(recent.length ? recent.map(renderMessage).join("\n") : (0, colour_js_1.dim)(" Nothing said yet. Press r to start."));
233
+ if (thread.messages.length > recent.length || thread.next)
234
+ console.log((0, colour_js_1.dim)(" … earlier messages: press t"));
235
+ listed.unread = 0;
236
+ console.log((0, colour_js_1.dim)("\n────────────────────────────────────────────────────────────"));
237
+ console.log(` ${(0, colour_js_1.accent)("r")} reply ${(0, colour_js_1.accent)("t")} whole thread ${(0, colour_js_1.accent)("f")} files ${(0, colour_js_1.accent)("u")} run checks\n` +
238
+ ` ${(0, colour_js_1.accent)("s")} save as a version ${(0, colour_js_1.accent)("h")} hand over ${(0, colour_js_1.accent)("m")} ${thread.muted ? "unmute" : "mute"} ${(0, colour_js_1.accent)("c")} close ${(0, colour_js_1.accent)("q")} back`);
239
+ if (notice)
240
+ console.log(`\n${notice}`);
241
+ notice = "";
242
+ raw(true);
243
+ const pressed = await key();
244
+ if (pressed.ctrl && pressed.name === "c")
245
+ return true;
246
+ if (pressed.name === "q" || pressed.name === "escape")
247
+ return false;
248
+ try {
249
+ if (pressed.name === "r") {
250
+ const text = await ask((0, colour_js_1.accent)("reply › "));
251
+ if (text) {
252
+ await (0, api_js_1.callApi)(`${root}/messages`, { method: "POST", body: { body: text, ...(name ? { as: name } : {}) } });
253
+ notice = (0, colour_js_1.green)("Sent.");
254
+ }
255
+ }
256
+ else if (pressed.name === "t") {
257
+ await wholeThread(root, name);
258
+ }
259
+ else if (pressed.name === "f") {
260
+ clear();
261
+ console.log((0, colour_js_1.bold)(`${w.name} · changes`));
262
+ console.log(files.length ? files.map((f) => ` ${f.removed ? (0, colour_js_1.red)("removed") : (0, colour_js_1.green)("changed")} ${f.path}`).join("\n") : (0, colour_js_1.dim)(" No changes yet"));
263
+ await pause();
264
+ }
265
+ else if (pressed.name === "m") {
266
+ await (0, api_js_1.callApi)(`${root}/messages/mute`, { method: "PUT", body: { muted: !thread.muted, ...(name ? { as: name } : {}) } });
267
+ notice = (0, colour_js_1.green)(thread.muted ? "Unmuted." : "Muted. New messages here no longer notify you.");
268
+ }
269
+ else if (pressed.name === "s") {
270
+ const message = await ask("describe the version › ");
271
+ if (message) {
272
+ const saved = await (0, api_js_1.callApi)(`${root}/save`, {
273
+ method: "POST",
274
+ body: { expectedRevision: w.revision, operationKey: `picker-${(0, node_crypto_1.randomUUID)()}`, message, ...(name ? { as: name } : {}) },
275
+ });
276
+ notice = (0, colour_js_1.green)(`Saved${saved.version?.sequence ? ` as v${saved.version.sequence}` : ""}${saved.review ? ` on ${saved.review.line}, for review` : ""}.`);
277
+ }
278
+ }
279
+ else if (pressed.name === "u") {
280
+ const checks = shown.recipes.filter((one) => one.kind === "check");
281
+ if (!checks.length)
282
+ notice = (0, colour_js_1.dim)("This project has no check recipes. cbx workspace recipe sets one up.");
283
+ else {
284
+ const pick = await ask(`recipe (${checks.map((one) => one.name).join(", ")}) › `);
285
+ const recipe = checks.find((one) => one.name === pick) ?? (checks.length === 1 && !pick ? checks[0] : undefined);
286
+ if (recipe) {
287
+ const run = await (0, api_js_1.callApi)(`${root}/runs`, {
288
+ method: "POST",
289
+ body: { expectedRevision: w.revision, operationKey: `picker-${(0, node_crypto_1.randomUUID)()}`, recipe: recipe.name },
290
+ });
291
+ notice = (0, colour_js_1.green)(`Started ${recipe.name}.`) + (0, colour_js_1.dim)(run.runId ? ` cbx workspace logs ${listed.project} ${listed.id} ${run.runId}` : "");
292
+ }
293
+ else
294
+ notice = (0, colour_js_1.dim)("No such recipe; nothing started.");
295
+ }
296
+ }
297
+ else if (pressed.name === "h") {
298
+ const machine = await ask("machine name › ");
299
+ if (machine) {
300
+ const made = await (0, api_js_1.callApi)(`${root}/assignments`, {
301
+ method: "POST",
302
+ body: { name: machine },
303
+ });
304
+ clear();
305
+ console.log(`${(0, colour_js_1.bold)(`Handed to ${machine}`)}\n\n ${made.token}\n\n${(0, colour_js_1.dim)("Shown once. On that machine: cbx workspace checkout " + listed.project + " " + listed.id + " (with CODEROOK_TOKEN set to it).")}`);
306
+ await pause();
307
+ }
308
+ }
309
+ else if (pressed.name === "c") {
310
+ const sure = await ask(`archive "${w.name}"? every machine it was handed to loses access (y/N) › `);
311
+ if (sure.toLowerCase() === "y") {
312
+ await (0, api_js_1.callApi)(root, { method: "PATCH", body: { expectedRevision: w.revision, archived: true } });
313
+ return false;
314
+ }
315
+ }
316
+ }
317
+ catch (error) {
318
+ notice = (0, colour_js_1.red)(error instanceof Error ? error.message : String(error));
319
+ }
320
+ }
321
+ }
322
+ async function wholeThread(root, name) {
323
+ clear();
324
+ let after = null;
325
+ do {
326
+ const page = await (0, api_js_1.callApi)(`${root}/messages${join(asQuery(name), after ? `after=${encodeURIComponent(after)}` : "")}`);
327
+ if (!after && page.pinned)
328
+ console.log(`${(0, colour_js_1.accent)("PINNED")} ${(0, colour_js_1.dim)("handoff note")}\n ${page.pinned.split("\n").join("\n ")}\n`);
329
+ for (const message of page.messages)
330
+ console.log(renderMessage(message));
331
+ after = page.next;
332
+ } while (after);
333
+ await pause();
334
+ }
335
+ async function pause() {
336
+ console.log((0, colour_js_1.dim)("\npress any key"));
337
+ raw(true);
338
+ await key();
339
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.41.2",
3
+ "version": "0.42.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",