@coderook/cli 0.42.3 → 0.42.5

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.42.3",
5
+ "version": "0.42.5",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.commandWorkspace = commandWorkspace;
4
+ exports.deployLanded = deployLanded;
4
5
  exports.commandWorkspaceMcp = commandWorkspaceMcp;
5
6
  const promises_1 = require("node:fs/promises");
6
7
  const node_readline_1 = require("node:readline");
@@ -154,13 +155,80 @@ async function commandWorkspace(parsed) {
154
155
  else if (operation !== "show")
155
156
  throw new Error(`Unknown workspace operation ${operation}`);
156
157
  }
157
- const result = await (0, api_js_1.callApi)(path, {
158
- method,
159
- ...(body === undefined ? {} : { body }),
160
- });
158
+ let result;
159
+ try {
160
+ result = await (0, api_js_1.callApi)(path, {
161
+ method,
162
+ ...(body === undefined ? {} : { body }),
163
+ });
164
+ }
165
+ catch (error) {
166
+ /*
167
+ Saves land one at a time, each on top of whatever landed before, so the
168
+ version a save made can differ from the draft that was checked: other
169
+ people's work landed in between. That version is the one to check and
170
+ ship, so check it where it is and deploy it, rather than refusing.
171
+ */
172
+ if (operation === "deploy" && error instanceof api_js_1.ServiceError && error.code === "workspace_saved_content_changed") {
173
+ result = await deployLanded(root, body);
174
+ }
175
+ else
176
+ throw error;
177
+ }
161
178
  console.log(JSON.stringify(result, null, 2));
162
179
  return 0;
163
180
  }
181
+ const say = (line) => process.stderr.write(line + "\n");
182
+ const FINISHED = new Set(["passed", "failed", "cancelled", "canceled", "timed_out", "error", "skipped"]);
183
+ /** Check a saved version where it landed, in an unedited draft on it, then deploy it. */
184
+ async function deployLanded(root, body, options = {}) {
185
+ const api = options.api ?? api_js_1.callApi;
186
+ const wait = options.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
187
+ const saved = String(body.savedVersionId ?? "");
188
+ const recipe = String(body.recipe ?? "");
189
+ say(`That version merged onto the line after this draft was checked. Checking the version itself, then deploying it.`);
190
+ const opened = await api(`${root}/workspaces`, {
191
+ method: "POST",
192
+ body: { name: `Deploy ${saved.slice(0, 8)} (${recipe})`, operationKey: `deploy-landed-${saved}-${recipe}`, baseVersionId: saved },
193
+ });
194
+ const ws = `${root}/workspaces/${opened.workspace.id}`;
195
+ if (opened.workspace.revision !== 0)
196
+ throw new Error(`Draft ${opened.workspace.id} was edited; open a fresh one on the saved version.`);
197
+ const shown = await api(ws);
198
+ const required = shown.recipes?.find((r) => r.name === recipe)?.required_recipes ?? [];
199
+ const deadline = Date.now() + (options.deadlineMs ?? 45 * 60_000);
200
+ for (const name of required) {
201
+ const started = await api(`${ws}/runs`, {
202
+ method: "POST",
203
+ body: { expectedRevision: 0, operationKey: `check-${saved}-${name}`, recipe: name },
204
+ });
205
+ say(` ${name}: run #${started.number ?? started.runId} on the saved version…`);
206
+ for (;;) {
207
+ /* The job endpoint answers { run: { status, number, … }, logs, artifacts }. */
208
+ const answer = await api(`${ws}/runs/${started.runId}`);
209
+ const run = answer.run ?? { status: "unknown" };
210
+ if (run.status === "passed") {
211
+ say(` ${name}: passed`);
212
+ break;
213
+ }
214
+ if (FINISHED.has(run.status) || run.status === "unknown")
215
+ throw new Error(`${name} ${run.status} on the saved version (run #${run.number ?? started.runId}); not deploying.`);
216
+ if (Date.now() > deadline)
217
+ throw new Error(`${name} has not finished; deploy again later with the same command.`);
218
+ await wait(15_000);
219
+ }
220
+ }
221
+ return api(`${ws}/runs`, {
222
+ method: "POST",
223
+ body: {
224
+ expectedRevision: 0,
225
+ operationKey: `${String(body.operationKey ?? "deploy")}-landed`,
226
+ recipe,
227
+ target: body.target,
228
+ savedVersionId: saved,
229
+ },
230
+ });
231
+ }
164
232
  /** Explicit remote/write-capable alternative; the original cbx mcp stays read-only. */
165
233
  async function commandWorkspaceMcp() {
166
234
  const lines = (0, node_readline_1.createInterface)({ input: process.stdin, crlfDelay: Infinity });
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MACHINE_OPERATIONS = void 0;
7
+ exports.readWhole = readWhole;
7
8
  exports.commandWorkspaceMachine = commandWorkspaceMachine;
8
9
  exports.commandMachines = commandMachines;
9
10
  /**
@@ -23,7 +24,7 @@ exports.commandMachines = commandMachines;
23
24
  * it, and its record lives in .coderook/workspace.json. Edits go in as the
24
25
  * draft's own changes, each checked against the copy this folder last had,
25
26
  * so somebody else's edit to the same file in the meantime is refused rather
26
- * than overwritten. Text only, 256 KiB a file, as the draft itself allows.
27
+ * than overwritten. Text only, 1 MiB a file, as the draft itself allows.
27
28
  */
28
29
  const node_crypto_1 = require("node:crypto");
29
30
  const node_fs_1 = require("node:fs");
@@ -36,8 +37,29 @@ const config_js_1 = require("./config.js");
36
37
  const machine_js_1 = require("./machine.js");
37
38
  const colour_js_1 = require("./colour.js");
38
39
  const RECORD = node_path_1.default.join(".coderook", "workspace.json");
39
- const MAX_FILE = 256 * 1024;
40
- const MAX_BATCH = 900 * 1024;
40
+ /* The draft's own limit per text file (backend remote_workspaces.ts MAX_EDIT_BYTES). */
41
+ const MAX_FILE = 1024 * 1024;
42
+ /*
43
+ One request to the draft may be 2 MiB. Batches are measured as the JSON that
44
+ is sent, since escaping makes content larger than its bytes on disk, and kept
45
+ well under the cap.
46
+ */
47
+ const MAX_REQUEST = 2 * 1024 * 1024;
48
+ const MAX_BATCH = 1536 * 1024;
49
+ const encodedSize = (change) => Buffer.byteLength(JSON.stringify(change));
50
+ /** One draft text file in full, from the line-paged read (2,000 lines a request). */
51
+ async function readWhole(draft, filePath, read = (route) => (0, api_js_1.callApi)(route)) {
52
+ const lines = [];
53
+ for (let start = 1;;) {
54
+ const page = await read(`${draft}/files?path=${encodeURIComponent(filePath)}&startLine=${start}&lines=2000`);
55
+ const got = page.content.split("\n");
56
+ lines.push(...got);
57
+ if (!page.truncated)
58
+ break;
59
+ start += got.length;
60
+ }
61
+ return lines.join("\n");
62
+ }
41
63
  function flag(parsed, ...names) {
42
64
  for (const name of names) {
43
65
  const value = parsed.flags.get(name);
@@ -149,6 +171,14 @@ async function checkout(found, draft, workspaceId, into) {
149
171
  delete manifest[change.path];
150
172
  }
151
173
  else {
174
+ /*
175
+ A diff page carries at most 4 MiB of content; a text file past that
176
+ budget comes with only its digest, and is read on its own, a page of
177
+ lines at a time.
178
+ */
179
+ if (change.after.content === null && change.after.reason?.startsWith("page byte limit")) {
180
+ change.after.content = await readWhole(draft, change.path);
181
+ }
152
182
  if (change.after.content === null) {
153
183
  throw new Error(`The draft's copy of ${change.path} could not be read as text (${change.after.reason ?? "unreadable"}).`);
154
184
  }
@@ -199,10 +229,16 @@ async function push(parsed, where) {
199
229
  refused.push(file.path);
200
230
  continue;
201
231
  }
202
- changes.push({ path: file.path, content: bytes.toString("utf8"), expectedSha256: record.files[file.path] ?? null });
232
+ const change = { path: file.path, content: bytes.toString("utf8"), expectedSha256: record.files[file.path] ?? null };
233
+ /* Under 1 MiB but so full of escapes that it cannot travel in one request. */
234
+ if (encodedSize(change) > MAX_REQUEST - 64 * 1024) {
235
+ refused.push(file.path);
236
+ continue;
237
+ }
238
+ changes.push(change);
203
239
  }
204
240
  if (refused.length) {
205
- console.error((0, colour_js_1.red)(`${refused.length} file${refused.length === 1 ? " is" : "s are"} binary or over 256 KiB, which a draft cannot take:`));
241
+ console.error((0, colour_js_1.red)(`${refused.length} file${refused.length === 1 ? " is" : "s are"} binary or over 1 MiB, which a draft cannot take:`));
206
242
  for (const one of refused.slice(0, 20))
207
243
  console.error(` ${one}`);
208
244
  console.error((0, colour_js_1.dim)("Nothing was sent. Leave them out, or save them with a normal upload from a linked folder."));
@@ -225,7 +261,7 @@ async function push(parsed, where) {
225
261
  let bytes = 0;
226
262
  while (start < changes.length) {
227
263
  const next = changes[start];
228
- const size = next.content ? Buffer.byteLength(next.content) : 0;
264
+ const size = encodedSize(next);
229
265
  if (batch.length && bytes + size > MAX_BATCH)
230
266
  break;
231
267
  batch.push(next);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.42.3",
3
+ "version": "0.42.5",
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",