@coderook/cli 0.42.7 → 0.43.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.
@@ -1,19 +1,19 @@
1
- {
2
- "name": "coderook",
3
- "displayName": "CodeRook",
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.7",
6
- "author": {
7
- "name": "ACCA Gaming Productions",
8
- "url": "https://coderook.com"
9
- },
10
- "homepage": "https://coderook.com/docs#assistants",
11
- "license": "Apache-2.0",
12
- "keywords": [
13
- "coderook",
14
- "versioning",
15
- "snapshots",
16
- "backup",
17
- "git"
18
- ]
19
- }
1
+ {
2
+ "name": "coderook",
3
+ "displayName": "CodeRook",
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",
6
+ "author": {
7
+ "name": "ACCA Gaming Productions",
8
+ "url": "https://coderook.com"
9
+ },
10
+ "homepage": "https://coderook.com/docs#assistants",
11
+ "license": "Apache-2.0",
12
+ "keywords": [
13
+ "coderook",
14
+ "versioning",
15
+ "snapshots",
16
+ "backup",
17
+ "git"
18
+ ]
19
+ }
@@ -52,6 +52,7 @@ exports.tokens = tokens;
52
52
  exports.getPrivatePaths = getPrivatePaths;
53
53
  exports.putPrivatePaths = putPrivatePaths;
54
54
  exports.callApi = callApi;
55
+ exports.callApiBytes = callApiBytes;
55
56
  exports.revokeToken = revokeToken;
56
57
  exports.deleteProject = deleteProject;
57
58
  exports.projectLabels = projectLabels;
@@ -851,6 +852,35 @@ async function putPrivatePaths(repositoryId, patterns) {
851
852
  async function callApi(route, options = {}) {
852
853
  return call(route, options);
853
854
  }
855
+ /** A GET whose answer is bytes, not JSON (a draft's raw file). Errors still arrive as JSON. */
856
+ async function callApiBytes(route) {
857
+ const token = await (0, config_js_1.loadToken)();
858
+ if (!token)
859
+ throw new Error("Not signed in. Run: cbx sign-in");
860
+ const response = await fetch(`${(0, config_js_1.apiOrigin)()}${route}`, {
861
+ headers: {
862
+ accept: "application/octet-stream, application/json",
863
+ authorization: `Bearer ${token}`,
864
+ "user-agent": "CodeRook-CLI/0.1",
865
+ ...(0, identify_js_1.clientHeaders)(),
866
+ },
867
+ signal: AbortSignal.timeout(REQUEST_DEADLINE_MS),
868
+ });
869
+ if (!response.ok) {
870
+ const text = await response.text();
871
+ let body = {};
872
+ try {
873
+ body = text ? JSON.parse(text) : {};
874
+ }
875
+ catch {
876
+ /* not JSON */
877
+ }
878
+ if (response.status === 401)
879
+ throw new Error("That token was not accepted. Run: cbx sign-in");
880
+ throw new ServiceError(body.error?.message ?? `${route} failed (${response.status})`, response.status, typeof body.error?.code === "string" ? body.error.code : null);
881
+ }
882
+ return new Uint8Array(await response.arrayBuffer());
883
+ }
854
884
  async function revokeToken(tokenId) {
855
885
  await call(`/v1/account/tokens/${encodeURIComponent(tokenId)}`, {
856
886
  method: "DELETE",
@@ -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|save|run|logs|cancel|deploy|preview|handoff|close|recipe|assign|assignments|unassign|checkout|say|thread|mute|unmute> <project> [workspace] [job] | workspace push [folder]",
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]",
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. 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.",
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.",
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" },
@@ -2813,8 +2813,12 @@ const SPECS = [
2813
2813
  { flags: "--unread", description: "thread: only messages this reader has not read" },
2814
2814
  { flags: "--peek", description: "thread: read without marking anything read" },
2815
2815
  { flags: "--after <time>", description: "thread: continue from the next page's time" },
2816
+ { flags: "--root <folder>", description: "put/starter: the project folder (default: here)" },
2817
+ { flags: "--apply", description: "starter: create the suggested workflows and recipes" },
2818
+ { flags: "--json", description: "watch: one JSON object per line; starter: the plan as JSON" },
2819
+ { flags: "--interval <seconds>", description: "watch: how often to look (default 20, at least 5)" },
2816
2820
  ],
2817
- 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"],
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"],
2818
2822
  run: workspace_command_js_1.commandWorkspace,
2819
2823
  },
2820
2824
  {
@@ -9,6 +9,9 @@ const api_js_1 = require("./api.js");
9
9
  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
+ const workspace_put_js_1 = require("./workspace_put.js");
13
+ const workspace_starter_js_1 = require("./workspace_starter.js");
14
+ const workspace_watch_js_1 = require("./workspace_watch.js");
12
15
  async function commandWorkspace(parsed) {
13
16
  const [operation, projectRef, workspace, runId] = parsed.positional;
14
17
  if (operation && workspace_machine_js_1.MACHINE_OPERATIONS.has(operation))
@@ -16,12 +19,22 @@ async function commandWorkspace(parsed) {
16
19
  /* On its own at a terminal: choose a workspace and see what is being discussed. Scripts get the usage line as before. */
17
20
  if (!operation && process.stdin.isTTY && process.stdout.isTTY)
18
21
  return (0, workspace_threads_js_1.interactiveWorkspaces)(parsed);
22
+ /* Every draft you can read when no project is named. */
23
+ if (operation === "watch")
24
+ return (0, workspace_watch_js_1.commandWorkspaceWatch)(parsed);
19
25
  if (!operation || !projectRef)
20
26
  throw new Error("Use cbx workspace <operation> <project> [workspace]. See cbx help workspace.");
21
27
  const project = await (0, api_js_1.findProject)(projectRef);
22
28
  if (!project)
23
29
  throw new Error(`No accessible project ${projectRef}`);
24
30
  const root = `/v1/repositories/${encodeURIComponent(project.id)}`;
31
+ if (operation === "starter")
32
+ return (0, workspace_starter_js_1.commandWorkspaceStarter)(parsed, root);
33
+ if (operation === "put") {
34
+ if (!workspace)
35
+ throw new Error("Supply the workspace ID after the project");
36
+ return (0, workspace_put_js_1.commandWorkspacePut)(parsed, root, workspace);
37
+ }
25
38
  if (workspace_threads_js_1.THREAD_OPERATIONS.has(operation)) {
26
39
  if (!workspace)
27
40
  throw new Error("Supply the workspace ID after the project");
@@ -36,6 +36,7 @@ const api_js_1 = require("./api.js");
36
36
  const config_js_1 = require("./config.js");
37
37
  const machine_js_1 = require("./machine.js");
38
38
  const colour_js_1 = require("./colour.js");
39
+ const workspace_put_js_1 = require("./workspace_put.js");
39
40
  const RECORD = node_path_1.default.join(".coderook", "workspace.json");
40
41
  /* The draft's own limit per text file (backend remote_workspaces.ts MAX_EDIT_BYTES). */
41
42
  const MAX_FILE = 1024 * 1024;
@@ -45,7 +46,6 @@ const MAX_FILE = 1024 * 1024;
45
46
  well under the cap.
46
47
  */
47
48
  const MAX_REQUEST = 2 * 1024 * 1024;
48
- const MAX_BATCH = 1536 * 1024;
49
49
  const encodedSize = (change) => Buffer.byteLength(JSON.stringify(change));
50
50
  /** One draft text file in full, from the line-paged read (2,000 lines a request). */
51
51
  async function readWhole(draft, filePath, read = (route) => (0, api_js_1.callApi)(route)) {
@@ -179,14 +179,14 @@ async function checkout(found, draft, workspaceId, into) {
179
179
  if (change.after.content === null && change.after.reason?.startsWith("page byte limit")) {
180
180
  change.after.content = await readWhole(draft, change.path);
181
181
  }
182
- if (change.after.content === null) {
183
- throw new Error(`The draft's copy of ${change.path} could not be read as text (${change.after.reason ?? "unreadable"}).`);
184
- }
185
- if (sha(change.after.content) !== change.after.sha256) {
182
+ /* Not text (an image, a font, an archive): its exact bytes, from the draft's raw read. */
183
+ const body = change.after.content ??
184
+ (await (0, api_js_1.callApiBytes)(`${draft}/raw?path=${encodeURIComponent(change.path)}`));
185
+ if (sha(body) !== change.after.sha256) {
186
186
  throw new Error(`The draft's copy of ${change.path} did not arrive exactly; nothing was overwritten past it. Try again.`);
187
187
  }
188
188
  await (0, promises_1.mkdir)(node_path_1.default.dirname(target), { recursive: true });
189
- await (0, promises_1.writeFile)(target, change.after.content);
189
+ await (0, promises_1.writeFile)(target, body);
190
190
  manifest[change.path] = change.after.sha256;
191
191
  }
192
192
  edited += 1;
@@ -216,6 +216,8 @@ async function push(parsed, where) {
216
216
  const rules = await (0, worktree_js_1.readRules)(folder);
217
217
  const found = await (0, worktree_js_1.changedFiles)(folder, rules, new Map(Object.entries(record.files)), "synchronize");
218
218
  const changes = [];
219
+ const sent = new Map();
220
+ const sizes = new Map();
219
221
  const refused = [];
220
222
  for (const file of found) {
221
223
  if (file.path === RECORD.replaceAll("\\", "/") || file.path.startsWith(".coderook/"))
@@ -225,11 +227,14 @@ async function push(parsed, where) {
225
227
  continue;
226
228
  }
227
229
  const bytes = await (0, promises_1.readFile)(node_path_1.default.join(folder, ...file.path.split("/")));
228
- if (bytes.length > MAX_FILE || bytes.includes(0)) {
230
+ if (bytes.length > MAX_FILE) {
229
231
  refused.push(file.path);
230
232
  continue;
231
233
  }
232
- const change = { path: file.path, content: bytes.toString("utf8"), expectedSha256: record.files[file.path] ?? null };
234
+ /* Text as text; images, fonts and other binary files as base64. */
235
+ const change = { path: file.path, ...(0, workspace_put_js_1.encodeFile)(bytes), expectedSha256: record.files[file.path] ?? null };
236
+ sent.set(file.path, sha(bytes));
237
+ sizes.set(file.path, bytes.length);
233
238
  /* Under 1 MiB but so full of escapes that it cannot travel in one request. */
234
239
  if (encodedSize(change) > MAX_REQUEST - 64 * 1024) {
235
240
  refused.push(file.path);
@@ -238,7 +243,7 @@ async function push(parsed, where) {
238
243
  changes.push(change);
239
244
  }
240
245
  if (refused.length) {
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:`));
246
+ console.error((0, colour_js_1.red)(`${refused.length} file${refused.length === 1 ? " is" : "s are"} over 1 MiB, which a draft cannot take:`));
242
247
  for (const one of refused.slice(0, 20))
243
248
  console.error(` ${one}`);
244
249
  console.error((0, colour_js_1.dim)("Nothing was sent. Leave them out, or save them with a normal upload from a linked folder."));
@@ -256,18 +261,7 @@ async function push(parsed, where) {
256
261
  }
257
262
  // In batches the draft accepts, each against the revision the last one made.
258
263
  let revision = record.revision;
259
- for (let start = 0; start < changes.length;) {
260
- const batch = [];
261
- let bytes = 0;
262
- while (start < changes.length) {
263
- const next = changes[start];
264
- const size = encodedSize(next);
265
- if (batch.length && bytes + size > MAX_BATCH)
266
- break;
267
- batch.push(next);
268
- bytes += size;
269
- start += 1;
270
- }
264
+ for (const batch of (0, workspace_put_js_1.batches)(changes, sizes)) {
271
265
  const result = await (0, api_js_1.callApi)(`${draft}/changes`, {
272
266
  method: "POST",
273
267
  body: { expectedRevision: revision, operationKey: key("push"), changes: batch },
@@ -277,7 +271,7 @@ async function push(parsed, where) {
277
271
  if (one.remove)
278
272
  delete record.files[one.path];
279
273
  else
280
- record.files[one.path] = sha(one.content);
274
+ record.files[one.path] = sent.get(one.path);
281
275
  }
282
276
  record.revision = revision;
283
277
  await writeRecord(folder, record);
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeFile = encodeFile;
4
+ exports.projectPath = projectPath;
5
+ exports.batches = batches;
6
+ exports.editKey = editKey;
7
+ exports.commandWorkspacePut = commandWorkspacePut;
8
+ /**
9
+ * cbx workspace put <project> <workspace> <path…>
10
+ *
11
+ * Copies files from this computer into a draft: the step every teammate and
12
+ * assistant otherwise scripts by hand (read the file, look up the hash the
13
+ * draft holds, build the JSON, keep under the request limits).
14
+ *
15
+ * - Text goes as content; anything else (images, fonts, archives, anything
16
+ * with a NUL byte or invalid UTF-8) goes as contentBase64.
17
+ * - A path that no longer exists here is removed from the draft.
18
+ * - Each change carries the hash the draft holds for that path, so a file a
19
+ * teammate changed meanwhile is refused instead of overwritten.
20
+ * - Large sets are split into several edits (32 files or 1 MiB each), and
21
+ * each edit's key is derived from what it sends, so running the same
22
+ * command again after a dropped connection does not apply anything twice.
23
+ */
24
+ const node_crypto_1 = require("node:crypto");
25
+ const promises_1 = require("node:fs/promises");
26
+ const node_path_1 = require("node:path");
27
+ const api_js_1 = require("./api.js");
28
+ const MAX_FILES = 32;
29
+ const MAX_BYTES = 1024 * 1024;
30
+ /* A request may be 2 MiB; escaping and base64 make the JSON larger than the bytes, so keep well under. */
31
+ const MAX_JSON = 1536 * 1024;
32
+ /** UTF-8 text with no NUL byte travels as text; every other file as base64. */
33
+ function encodeFile(bytes) {
34
+ if (!bytes.includes(0)) {
35
+ try {
36
+ return { content: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes) };
37
+ }
38
+ catch {
39
+ /* not UTF-8: fall through to base64 */
40
+ }
41
+ }
42
+ return { contentBase64: Buffer.from(bytes).toString("base64") };
43
+ }
44
+ /** A project-relative path with forward slashes, refusing anything outside the root. */
45
+ function projectPath(root, given) {
46
+ const full = (0, node_path_1.isAbsolute)(given) ? given : (0, node_path_1.resolve)(root, given);
47
+ const rel = (0, node_path_1.relative)(root, full);
48
+ if (!rel || rel.startsWith("..") || (0, node_path_1.isAbsolute)(rel))
49
+ throw new Error(`${given} is outside ${root}; run from the project folder or pass --root`);
50
+ return rel.split(node_path_1.sep).join("/");
51
+ }
52
+ /**
53
+ * Groups changes into edits the service accepts: at most 32 files, 1 MiB of
54
+ * file content and 1.5 MiB of JSON each. `sizes` gives each path's bytes on disk.
55
+ */
56
+ function batches(changes, sizes) {
57
+ const out = [];
58
+ let current = [];
59
+ let bytes = 0;
60
+ let json = 0;
61
+ for (const change of changes) {
62
+ const size = sizes.get(change.path) ?? 0;
63
+ const encoded = Buffer.byteLength(JSON.stringify(change));
64
+ if (current.length && (current.length >= MAX_FILES || bytes + size > MAX_BYTES || json + encoded > MAX_JSON)) {
65
+ out.push(current);
66
+ current = [];
67
+ bytes = 0;
68
+ json = 0;
69
+ }
70
+ current.push(change);
71
+ bytes += size;
72
+ json += encoded;
73
+ }
74
+ if (current.length)
75
+ out.push(current);
76
+ return out;
77
+ }
78
+ /** A key that is the same whenever the same edit is sent to the same revision. */
79
+ function editKey(revision, changes) {
80
+ const hash = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify([revision, changes])).digest("hex");
81
+ return `put-${revision}-${hash.slice(0, 40)}`;
82
+ }
83
+ async function commandWorkspacePut(parsed, root, workspace) {
84
+ const paths = parsed.positional.slice(3);
85
+ if (!paths.length)
86
+ throw new Error("Name the files to put: cbx workspace put <project> <workspace> <path…>");
87
+ const folder = (0, node_path_1.resolve)(String(parsed.flags.get("root") ?? process.cwd()));
88
+ const draft = `${root}/workspaces/${encodeURIComponent(workspace)}`;
89
+ const wanted = [...new Set(paths.map((p) => projectPath(folder, p)))];
90
+ /* The hashes the draft holds now; the listing pages 500 at a time, so stop once every wanted path is found. */
91
+ const held = new Map();
92
+ for (let offset = 0; offset !== null && held.size < wanted.length;) {
93
+ const page = await (0, api_js_1.callApi)(`${draft}/files?offset=${offset}`);
94
+ for (const f of page.files)
95
+ if (wanted.includes(f.path))
96
+ held.set(f.path, f.sha256);
97
+ offset = page.nextOffset;
98
+ }
99
+ const changes = [];
100
+ const unchanged = [];
101
+ const sizes = new Map();
102
+ for (const path of wanted) {
103
+ const local = (0, node_path_1.join)(folder, ...path.split("/"));
104
+ const exists = await (0, promises_1.stat)(local).then((s) => s.isFile(), () => false);
105
+ if (!exists) {
106
+ const sha = held.get(path);
107
+ if (!sha)
108
+ throw new Error(`${path} is neither here nor in the draft`);
109
+ changes.push({ path, remove: true, expectedSha256: sha });
110
+ continue;
111
+ }
112
+ const bytes = await (0, promises_1.readFile)(local);
113
+ if (bytes.length > MAX_BYTES)
114
+ throw new Error(`${path} is ${bytes.length} bytes; a draft takes files up to 1 MiB. Save larger files with cbx submit.`);
115
+ /* Already what the draft holds: nothing to send, so running put twice changes nothing. */
116
+ if ((0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex") === held.get(path)) {
117
+ unchanged.push(path);
118
+ continue;
119
+ }
120
+ sizes.set(path, bytes.length);
121
+ changes.push({ path, ...encodeFile(bytes), expectedSha256: held.get(path) ?? null });
122
+ }
123
+ if (!changes.length) {
124
+ console.log(JSON.stringify({ workspace, written: [], removed: [], binary: [], unchanged }, null, 2));
125
+ return 0;
126
+ }
127
+ let revision = parsed.flags.has("revision")
128
+ ? Number(parsed.flags.get("revision"))
129
+ : (await (0, api_js_1.callApi)(draft)).workspace.revision;
130
+ const written = [];
131
+ const removed = [];
132
+ const binary = [];
133
+ for (const batch of batches(changes, sizes)) {
134
+ const out = await (0, api_js_1.callApi)(`${draft}/changes`, {
135
+ method: "POST",
136
+ body: { expectedRevision: revision, operationKey: editKey(revision, batch), changes: batch },
137
+ });
138
+ revision = out.workspace.revision;
139
+ for (const c of batch) {
140
+ if ("remove" in c)
141
+ removed.push(c.path);
142
+ else
143
+ written.push(c.path);
144
+ if ("contentBase64" in c)
145
+ binary.push(c.path);
146
+ }
147
+ }
148
+ console.log(JSON.stringify({ workspace, revision, written, removed, binary, unchanged }, null, 2));
149
+ return 0;
150
+ }
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectStarters = detectStarters;
4
+ exports.commandWorkspaceStarter = commandWorkspaceStarter;
5
+ /**
6
+ * cbx workspace starter <project> [--apply] [--root folder]
7
+ *
8
+ * A draft can run checks only through recipes, and a recipe needs a workflow
9
+ * with a command behind it. A new project has neither, so the first person or
10
+ * assistant to open a draft finds "An administrator must configure this named
11
+ * workspace recipe first" and nothing to run.
12
+ *
13
+ * This reads the project folder and proposes one check recipe per package it
14
+ * recognises (Node by its lockfile, Python, Rust, Go), shows the plan, and with
15
+ * --apply creates the workflows and recipes. It never replaces a recipe or a
16
+ * workflow that already exists, and it never makes deploy recipes: those carry
17
+ * credentials and targets, which only the project's owner can choose.
18
+ */
19
+ const node_fs_1 = require("node:fs");
20
+ const promises_1 = require("node:fs/promises");
21
+ const node_path_1 = require("node:path");
22
+ const api_js_1 = require("./api.js");
23
+ const colour_js_1 = require("./colour.js");
24
+ const SKIP = new Set(["node_modules", ".git", ".coderook", ".rook", "dist", "build", "target", ".venv", "venv", "vendor", ".next"]);
25
+ const slug = (directory) => (directory.split("/").pop() || "project").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "project";
26
+ function nodeStarter(directory, seen) {
27
+ const at = (name) => (directory ? `${directory}/${name}` : name);
28
+ let scripts = {};
29
+ try {
30
+ scripts = JSON.parse(seen.get(at("package.json")) ?? "{}").scripts ?? {};
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ const has = (name) => typeof scripts[name] === "string" && !/no test specified/.test(String(scripts[name]));
36
+ /* Fastest first, so a draft hears about a type error before waiting on the tests. */
37
+ const steps = ["check", "typecheck", "lint", "build", "test"].filter(has);
38
+ if (!steps.length)
39
+ return null;
40
+ const [install, run] = seen.has(at("pnpm-lock.yaml"))
41
+ ? ["corepack enable && pnpm install --frozen-lockfile", "pnpm run"]
42
+ : seen.has(at("yarn.lock"))
43
+ ? ["corepack enable && yarn install --immutable", "yarn run"]
44
+ : seen.has(at("package-lock.json"))
45
+ ? ["npm ci", "npm run"]
46
+ : ["npm install", "npm run"];
47
+ return {
48
+ recipe: `${slug(directory)}-check`,
49
+ workflow: `Draft check · ${directory || "project"}`,
50
+ directory,
51
+ command: [install, ...steps.map((s) => `${run} ${s}`)].join(" && "),
52
+ because: `package.json scripts: ${steps.join(", ")}`,
53
+ };
54
+ }
55
+ /** The starters a folder's files suggest, one per recognised package. */
56
+ function detectStarters(seen) {
57
+ const directories = new Set();
58
+ for (const path of seen.keys()) {
59
+ const cut = path.lastIndexOf("/");
60
+ directories.add(cut === -1 ? "" : path.slice(0, cut));
61
+ }
62
+ const out = [];
63
+ for (const directory of [...directories].sort()) {
64
+ const at = (name) => (directory ? `${directory}/${name}` : name);
65
+ const node = seen.has(at("package.json")) ? nodeStarter(directory, seen) : null;
66
+ if (node)
67
+ out.push(node);
68
+ else if (seen.has(at("Cargo.toml")))
69
+ out.push({ recipe: `${slug(directory)}-check`, workflow: `Draft check · ${directory || "project"}`, directory, command: "cargo test", because: "Cargo.toml" });
70
+ else if (seen.has(at("go.mod")))
71
+ out.push({ recipe: `${slug(directory)}-check`, workflow: `Draft check · ${directory || "project"}`, directory, command: "go vet ./... && go test ./...", because: "go.mod" });
72
+ else if (seen.has(at("pyproject.toml")) || seen.has(at("requirements.txt"))) {
73
+ const install = seen.has(at("requirements.txt")) ? "python -m pip install -r requirements.txt" : "python -m pip install -e .";
74
+ out.push({ recipe: `${slug(directory)}-check`, workflow: `Draft check · ${directory || "project"}`, directory, command: `${install} && python -m pytest`, because: seen.has(at("pyproject.toml")) ? "pyproject.toml" : "requirements.txt" });
75
+ }
76
+ }
77
+ /* Two folders with the same last name ("app/web", "site/web") keep apart by their full path. */
78
+ const names = new Map();
79
+ for (const s of out)
80
+ names.set(s.recipe, (names.get(s.recipe) ?? 0) + 1);
81
+ for (const s of out)
82
+ if ((names.get(s.recipe) ?? 0) > 1)
83
+ s.recipe = `${s.directory.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "project"}-check`;
84
+ return out;
85
+ }
86
+ const MARKERS = new Set(["package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "Cargo.toml", "go.mod", "pyproject.toml", "requirements.txt"]);
87
+ /** The marker files in a folder, two levels deep, skipping dependencies and build output. */
88
+ async function scan(root) {
89
+ const seen = new Map();
90
+ const walk = async (rel, depth) => {
91
+ const entries = await (0, promises_1.readdir)((0, node_path_1.join)(root, ...rel.split("/").filter(Boolean)), { withFileTypes: true }).catch(() => []);
92
+ for (const entry of entries) {
93
+ const path = rel ? `${rel}/${entry.name}` : entry.name;
94
+ if (entry.isDirectory()) {
95
+ if (depth < 2 && !SKIP.has(entry.name) && !entry.name.startsWith("."))
96
+ await walk(path, depth + 1);
97
+ }
98
+ else if (MARKERS.has(entry.name)) {
99
+ seen.set(path, entry.name === "package.json" ? await (0, promises_1.readFile)((0, node_path_1.join)(root, ...path.split("/")), "utf8") : "");
100
+ }
101
+ }
102
+ };
103
+ await walk("", 0);
104
+ return seen;
105
+ }
106
+ async function commandWorkspaceStarter(parsed, root) {
107
+ const folder = (0, node_path_1.resolve)(String(parsed.flags.get("root") ?? process.cwd()));
108
+ if (!(0, node_fs_1.existsSync)(folder))
109
+ throw new Error(`${folder} does not exist`);
110
+ const starters = detectStarters(await scan(folder));
111
+ const existing = await (0, api_js_1.callApi)(`${root}/workspace-recipes`);
112
+ const taken = new Set((existing.recipes ?? []).map((r) => r.name));
113
+ const fresh = starters.filter((s) => !taken.has(s.recipe));
114
+ const json = parsed.flags.has("json");
115
+ if (!starters.length) {
116
+ if (json)
117
+ console.log(JSON.stringify({ starters: [] }));
118
+ else
119
+ console.log("No package.json scripts, Cargo.toml, go.mod or Python project found here. Pass --root with the project folder.");
120
+ return 0;
121
+ }
122
+ if (!parsed.flags.has("apply")) {
123
+ if (json)
124
+ console.log(JSON.stringify({ starters, existing: [...taken] }, null, 2));
125
+ else {
126
+ for (const s of starters) {
127
+ const kept = taken.has(s.recipe);
128
+ console.log(`${kept ? (0, colour_js_1.dim)("exists ") : (0, colour_js_1.green)("new ")} ${(0, colour_js_1.bold)(s.recipe)} ${(0, colour_js_1.dim)(`in ${s.directory || "."}`)}`);
129
+ if (!kept)
130
+ console.log(` ${s.command}\n ${(0, colour_js_1.dim)(s.because)}`);
131
+ }
132
+ console.log(fresh.length
133
+ ? (0, colour_js_1.dim)(`\nNothing was created. Add ${fresh.length === 1 ? "it" : "them"} with `) + (0, colour_js_1.accent)("--apply") + (0, colour_js_1.dim)("; drafts then run them with cbx workspace run <project> <draft> --recipe <name>.")
134
+ : (0, colour_js_1.dim)("\nEvery suggested recipe already exists."));
135
+ }
136
+ return 0;
137
+ }
138
+ const project = root.split("/").pop();
139
+ const known = new Map((await (0, api_js_1.workflows)(decodeURIComponent(project))).map((w) => [w.name, w.id]));
140
+ const made = [];
141
+ for (const s of fresh) {
142
+ /* A workflow of the same name is reused as it is, never rewritten. */
143
+ let workflowId = known.get(s.workflow);
144
+ if (!workflowId) {
145
+ const created = await (0, api_js_1.callApi)(`${root}/workflows`, {
146
+ method: "POST",
147
+ body: { name: s.workflow, trigger: "manual", command: s.command, workingDirectory: s.directory || null, runsOn: "hosted" },
148
+ });
149
+ workflowId = created.id;
150
+ }
151
+ await (0, api_js_1.callApi)(`${root}/workspace-recipes/${encodeURIComponent(s.recipe)}`, {
152
+ method: "PUT",
153
+ body: { kind: "check", workflowId, includePaths: s.directory ? [`${s.directory}/`] : [] },
154
+ });
155
+ made.push(s.recipe);
156
+ }
157
+ if (json)
158
+ console.log(JSON.stringify({ created: made, existing: [...taken] }, null, 2));
159
+ else if (made.length)
160
+ console.log(`${(0, colour_js_1.green)("Added")} ${made.join(", ")}. Run one on a draft with ${(0, colour_js_1.accent)("cbx workspace run <project> <draft> --recipe <name>")}.`);
161
+ else
162
+ console.log("Every suggested recipe already exists; nothing was changed.");
163
+ return 0;
164
+ }
@@ -31,6 +31,7 @@ const node_readline_1 = __importDefault(require("node:readline"));
31
31
  const api_js_1 = require("./api.js");
32
32
  const colour_js_1 = require("./colour.js");
33
33
  const config_js_1 = require("./config.js");
34
+ const workspace_watch_js_1 = require("./workspace_watch.js");
34
35
  exports.THREAD_OPERATIONS = new Set(["say", "thread", "mute", "unmute"]);
35
36
  /** The name this machine posts and reads as, if it gave one. */
36
37
  function authorName(parsed) {
@@ -100,14 +101,68 @@ async function unreadReminder(command, parsed) {
100
101
  const waiting = found?.workspaces.filter((w) => w.unread > 0) ?? [];
101
102
  if (!waiting.length)
102
103
  return;
104
+ /*
105
+ Messages addressed to this reader come first, with who sent them and
106
+ how to answer: a teammate or an assistant asked by name should not have
107
+ to find that in a count. Each is shown once; the count follows.
108
+ */
109
+ const addressed = await Promise.race([
110
+ addressedTo(parsed, waiting.slice(0, 3)),
111
+ new Promise((resolve) => setTimeout(() => resolve([]), 1500).unref()),
112
+ ]);
113
+ for (const one of addressed.slice(0, 2)) {
114
+ console.log(`\n${(0, colour_js_1.accent)("●")} ${(0, colour_js_1.bold)("For you")} ${(0, colour_js_1.dim)("in")} ${one.workspace.name} ${(0, colour_js_1.dim)("from")} ${one.from}: ${one.snippet}` +
115
+ `\n ${(0, colour_js_1.dim)(`cbx workspace thread ${one.workspace.project} ${one.workspace.id} --unread`)}`);
116
+ }
103
117
  const total = waiting.reduce((n, w) => n + w.unread, 0);
104
118
  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")}`);
119
+ console.log(`${addressed.length ? "" : "\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
120
  }
107
121
  catch {
108
122
  /* A reminder is never worth an error. */
109
123
  }
110
124
  }
125
+ /**
126
+ * Unread messages that name this reader (@name), not yet shown on this
127
+ * machine. A machine answers to the name it runs as; a person to their
128
+ * username and display name, read once a day and kept beside the config.
129
+ */
130
+ async function addressedTo(parsed, waiting) {
131
+ const as = authorName(parsed);
132
+ const names = as ? [as] : await cachedReaderNames();
133
+ if (!names.length)
134
+ return [];
135
+ const shownFile = node_path_1.default.join((0, config_js_1.configDirectory)(), "mentions-shown.json");
136
+ const shown = new Set(await (0, promises_1.readFile)(shownFile, "utf8").then((t) => JSON.parse(t)).catch(() => []));
137
+ const out = [];
138
+ for (const workspace of waiting) {
139
+ const thread = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(workspace.projectId)}/workspaces/${encodeURIComponent(workspace.id)}/messages${join(asQuery(as), "unread=true", "peek=true")}`);
140
+ for (const message of thread.messages) {
141
+ if (message.kind !== "message" || message.author.mine || shown.has(message.id) || !(0, workspace_watch_js_1.mentions)(message.body, names))
142
+ continue;
143
+ shown.add(message.id);
144
+ const text = message.body.replace(/\s+/g, " ").trim();
145
+ out.push({ workspace, from: (0, workspace_watch_js_1.authorLabel)(message.author), snippet: text.length > 140 ? `${text.slice(0, 139)}…` : text });
146
+ }
147
+ }
148
+ if (out.length)
149
+ await (0, promises_1.writeFile)(shownFile, JSON.stringify([...shown].slice(-500))).catch(() => undefined);
150
+ return out;
151
+ }
152
+ async function cachedReaderNames() {
153
+ const file = node_path_1.default.join((0, config_js_1.configDirectory)(), "reader-names.json");
154
+ try {
155
+ const kept = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
156
+ if (Date.now() - kept.at < 86_400_000)
157
+ return kept.names;
158
+ }
159
+ catch {
160
+ /* Not read yet on this machine. */
161
+ }
162
+ const names = await (0, workspace_watch_js_1.readerNames)();
163
+ await (0, promises_1.writeFile)(file, JSON.stringify({ at: Date.now(), names })).catch(() => undefined);
164
+ return names;
165
+ }
111
166
  /* ----------------------------------------------------------- the picker */
112
167
  const when = (iso) => {
113
168
  const date = new Date(iso);
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mentions = mentions;
4
+ exports.freshEvents = freshEvents;
5
+ exports.freshSaves = freshSaves;
6
+ exports.authorLabel = authorLabel;
7
+ exports.describe = describe;
8
+ exports.readerNames = readerNames;
9
+ exports.commandWorkspaceWatch = commandWorkspaceWatch;
10
+ /**
11
+ * cbx workspace watch [project] [workspace]
12
+ *
13
+ * What a person or an assistant working on drafts needs to hear about, as it
14
+ * happens: new messages in the threads it can read, messages addressed to it
15
+ * (@mentions), and new saved versions. One line per event until stopped,
16
+ * plain text at a terminal or JSON lines with --json for a program to read.
17
+ *
18
+ * Built so neither a teammate nor an assistant has to write its own poller.
19
+ * It reads with peek, so watching never marks anything read: the reader still
20
+ * sees the thread as unread when it opens it.
21
+ */
22
+ const api_js_1 = require("./api.js");
23
+ const colour_js_1 = require("./colour.js");
24
+ const workspace_threads_js_1 = require("./workspace_threads.js");
25
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ /**
27
+ * Whether a message names this reader as @name, as a whole word (the same
28
+ * rule the bell uses). A reader can answer to several names: the one a
29
+ * machine runs as, or a person's username and display name.
30
+ */
31
+ function mentions(body, names) {
32
+ const list = (typeof names === "string" ? [names] : names ?? [])
33
+ .map((name) => name?.trim())
34
+ .filter((name) => Boolean(name));
35
+ return list.some((name) => new RegExp(`(^|[^\\p{L}\\p{N}_])@${escapeRegExp(name)}(?![\\p{L}\\p{N}_-])`, "iu").test(body));
36
+ }
37
+ /** The events in one look that have not been reported, oldest first; updates state. */
38
+ function freshEvents(state, draft, messages, names) {
39
+ const out = [];
40
+ for (const m of [...messages].sort((a, b) => a.createdAt.localeCompare(b.createdAt))) {
41
+ if (state.seen.has(m.id))
42
+ continue;
43
+ state.seen.add(m.id);
44
+ if (Date.parse(m.createdAt) < state.since || m.author.mine)
45
+ continue;
46
+ const where = { project: draft.project, projectId: draft.projectId, workspace: draft.id, workspaceName: draft.name, id: m.id, at: m.createdAt };
47
+ if (m.kind === "event")
48
+ out.push({ type: "event", ...where, body: m.body });
49
+ else
50
+ out.push({ type: mentions(m.body, names) ? "mention" : "message", ...where, author: m.author, body: m.body });
51
+ }
52
+ return out;
53
+ }
54
+ /** New saved versions since the last look; the first look only records where things stand. */
55
+ function freshSaves(state, project, versions) {
56
+ const numbers = versions
57
+ .map((v) => ({ sequence: Number(v.sequence), message: v.message ?? "" }))
58
+ .filter((v) => Number.isFinite(v.sequence));
59
+ const top = numbers.reduce((max, v) => Math.max(max, v.sequence), 0);
60
+ const before = state.newest.get(project.id);
61
+ state.newest.set(project.id, Math.max(before ?? 0, top));
62
+ if (before === undefined)
63
+ return [];
64
+ return numbers
65
+ .filter((v) => v.sequence > before)
66
+ .sort((a, b) => a.sequence - b.sequence)
67
+ .map((v) => ({ type: "save", project: project.name, projectId: project.id, sequence: v.sequence, message: v.message }));
68
+ }
69
+ const flat = (text, most) => {
70
+ const one = text.replace(/\s+/g, " ").trim();
71
+ return one.length > most ? `${one.slice(0, most - 1)}…` : one;
72
+ };
73
+ /** Who wrote it, and how sure the service is: a person on the site, a machine on its own token, or a typed name. */
74
+ function authorLabel(author) {
75
+ if (author.kind === "person")
76
+ return `${author.label} (site)`;
77
+ if (author.kind === "token")
78
+ return `${author.label} (own token)`;
79
+ return `${author.label} (named, not verified)`;
80
+ }
81
+ function describe(event) {
82
+ if (event.type === "save")
83
+ return `${(0, colour_js_1.accent)("save")} ${event.project} v${event.sequence} ${flat(event.message, 160)}`;
84
+ if (event.type === "event")
85
+ return `${(0, colour_js_1.dim)("event")} ${event.workspaceName} ${flat(event.body, 160)}`;
86
+ const label = event.type === "mention" ? (0, colour_js_1.bold)("for you ") : "message ";
87
+ return `${label} ${event.workspaceName} ${authorLabel(event.author)}: ${flat(event.body, 200)} ${(0, colour_js_1.dim)(event.workspace)}`;
88
+ }
89
+ /** The names a person signed in here answers to; empty when that cannot be read. */
90
+ async function readerNames() {
91
+ try {
92
+ const me = (await (0, api_js_1.whoami)());
93
+ return [me.username, me.displayName].filter((name) => Boolean(name?.trim()));
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ }
99
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
100
+ async function commandWorkspaceWatch(parsed) {
101
+ const [, projectRef, workspaceId] = parsed.positional;
102
+ const name = (0, workspace_threads_js_1.authorName)(parsed);
103
+ const json = parsed.flags.has("json");
104
+ const every = Math.max(5, Number(parsed.flags.get("interval") ?? 20) || 20) * 1000;
105
+ const project = projectRef ? await (0, api_js_1.findProject)(projectRef) : null;
106
+ if (projectRef && !project)
107
+ throw new Error(`No accessible project ${projectRef}`);
108
+ const as = name ? `as=${encodeURIComponent(name)}` : "";
109
+ /* A machine answers to the name it runs as; a person to their own username and display name. */
110
+ const names = name ? [name] : await readerNames();
111
+ const state = {
112
+ seen: new Set(),
113
+ newest: new Map(),
114
+ since: parsed.flags.has("unread") ? 0 : Date.now() - 60_000,
115
+ };
116
+ const emit = (event) => console.log(json ? JSON.stringify(event) : describe(event));
117
+ if (!json && process.stdout.isTTY)
118
+ console.error((0, colour_js_1.dim)(`Watching ${workspaceId ? "one draft" : project ? project.name : "every draft you can read"}` +
119
+ `${names.length ? `, messages for @${names[0]} first` : ""}. Stop with Ctrl+C.`));
120
+ let stopped = false;
121
+ process.once("SIGINT", () => {
122
+ stopped = true;
123
+ });
124
+ let failures = 0;
125
+ while (!stopped) {
126
+ try {
127
+ const listed = await (0, api_js_1.callApi)(`/v1/account/workspace-threads${as ? `?${as}` : ""}`);
128
+ const drafts = (listed.workspaces ?? []).filter((w) => (!project || w.projectId === project.id) && (!workspaceId || w.id === workspaceId || w.id.startsWith(workspaceId)));
129
+ for (const draft of drafts) {
130
+ if (!draft.unread)
131
+ continue;
132
+ const query = [as, "unread=true", "peek=true"].filter(Boolean).join("&");
133
+ const thread = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(draft.projectId)}/workspaces/${encodeURIComponent(draft.id)}/messages?${query}`);
134
+ const events = freshEvents(state, draft, thread.messages ?? [], names);
135
+ /* Messages addressed to this reader first: they are the ones it is being asked to act on. */
136
+ for (const event of [...events.filter((e) => e.type === "mention"), ...events.filter((e) => e.type !== "mention")])
137
+ emit(event);
138
+ }
139
+ const projects = project
140
+ ? [{ id: project.id, name: project.name }]
141
+ : [...new Map(drafts.map((d) => [d.projectId, { id: d.projectId, name: d.project }])).values()];
142
+ for (const one of projects) {
143
+ const versions = await (0, api_js_1.callApi)(`/v1/repositories/${encodeURIComponent(one.id)}/versions`);
144
+ for (const event of freshSaves(state, one, versions.versions ?? []))
145
+ emit(event);
146
+ }
147
+ failures = 0;
148
+ }
149
+ catch (error) {
150
+ failures += 1;
151
+ console.error(`watch: ${error instanceof Error ? error.message : String(error)}${failures > 1 ? ` (${failures} in a row)` : ""}`);
152
+ }
153
+ /* Backs off while the service is unreachable, up to five minutes, and returns to the interval on success. */
154
+ const pause = Math.min(300_000, every * 2 ** Math.min(failures, 4));
155
+ for (let left = pause; left > 0 && !stopped; left -= 500)
156
+ await wait(Math.min(500, left));
157
+ }
158
+ return 0;
159
+ }
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "@coderook/cli",
3
- "version": "0.42.7",
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
- "license": "Apache-2.0",
6
- "homepage": "https://coderook.com",
7
- "bugs": {
8
- "url": "https://github.com/AlexC1991/cbx/issues"
9
- },
10
- "keywords": [
11
- "coderook",
12
- "cbx",
13
- "version-control",
14
- "vcs",
15
- "versioning",
16
- "project-hosting",
17
- "backup",
18
- "snapshots",
19
- "large-files",
20
- "gamedev"
21
- ],
22
- "engines": {
23
- "node": ">=22.15.0"
24
- },
25
- "bin": {
26
- "cbx": "dist/cli/src/cli.js",
27
- "coderook": "dist/cli/src/cli.js",
28
- "git-remote-coderook": "dist/cli/src/git_remote_bin.js"
29
- },
30
- "files": [
31
- ".claude-plugin",
32
- "dist",
33
- "skills",
34
- "NOTICE"
35
- ],
36
- "scripts": {
37
- "build": "tsc -p tsconfig.json && node scripts/copy-cbx-licence.mjs",
38
- "check": "tsc -p tsconfig.json --noEmit",
39
- "test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
40
- "start": "node dist/cli/src/cli.js",
41
- "prepublishOnly": "npm run sync:plugin && npm run build",
42
- "test:e2e": "node test/e2e.mjs",
43
- "test:matrix": "node test/state-matrix.mjs",
44
- "test:attempt": "node test/attempt-identity.mjs",
45
- "test:get": "node test/get-safety.mjs",
46
- "test:live": "node test/e2e.mjs --allow-production && node test/state-matrix.mjs --allow-production && node test/get-safety.mjs --allow-production && node test/get-track.mjs --allow-production && node test/get-protects-edits.mjs --allow-production && node test/upgrade-migration.mjs --allow-production && node test/fault-get.mjs --allow-production && node test/version-floor.mjs --allow-production && node test/fault-submit.mjs --allow-production && node test/race-attempts.mjs --allow-production && node test/attempt-identity.mjs --allow-production",
47
- "test:getedits": "node test/get-protects-edits.mjs",
48
- "test:upgrade": "node test/upgrade-migration.mjs",
49
- "test:faultget": "node test/fault-get.mjs",
50
- "test:floor": "node test/version-floor.mjs",
51
- "test:faultsubmit": "node test/fault-submit.mjs",
52
- "test:race": "node test/race-attempts.mjs",
53
- "test:runner": "node test/runner-live.mjs",
54
- "sync:plugin": "node scripts/sync-plugin-version.mjs",
55
- "test:track": "node test/get-track.mjs",
56
- "package:remote": "node ../scripts/package-cli-release.mjs",
57
- "publish:remote": "node ../scripts/publish-cli-release.mjs"
58
- },
59
- "devDependencies": {
60
- "@types/node": "24.10.1",
61
- "typescript": "5.9.3"
62
- },
63
- "repository": {
64
- "type": "git",
65
- "url": "git+https://github.com/AlexC1991/cbx.git",
66
- "directory": "cli"
67
- }
68
- }
1
+ {
2
+ "name": "@coderook/cli",
3
+ "version": "0.43.0",
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
+ "license": "Apache-2.0",
6
+ "homepage": "https://coderook.com",
7
+ "bugs": {
8
+ "url": "https://github.com/AlexC1991/cbx/issues"
9
+ },
10
+ "keywords": [
11
+ "coderook",
12
+ "cbx",
13
+ "version-control",
14
+ "vcs",
15
+ "versioning",
16
+ "project-hosting",
17
+ "backup",
18
+ "snapshots",
19
+ "large-files",
20
+ "gamedev"
21
+ ],
22
+ "engines": {
23
+ "node": ">=22.15.0"
24
+ },
25
+ "bin": {
26
+ "cbx": "dist/cli/src/cli.js",
27
+ "coderook": "dist/cli/src/cli.js",
28
+ "git-remote-coderook": "dist/cli/src/git_remote_bin.js"
29
+ },
30
+ "files": [
31
+ ".claude-plugin",
32
+ "dist",
33
+ "skills",
34
+ "NOTICE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json && node scripts/copy-cbx-licence.mjs",
38
+ "check": "tsc -p tsconfig.json --noEmit",
39
+ "test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
40
+ "start": "node dist/cli/src/cli.js",
41
+ "prepublishOnly": "npm run sync:plugin && npm run build",
42
+ "test:e2e": "node test/e2e.mjs",
43
+ "test:matrix": "node test/state-matrix.mjs",
44
+ "test:attempt": "node test/attempt-identity.mjs",
45
+ "test:get": "node test/get-safety.mjs",
46
+ "test:live": "node test/e2e.mjs --allow-production && node test/state-matrix.mjs --allow-production && node test/get-safety.mjs --allow-production && node test/get-track.mjs --allow-production && node test/get-protects-edits.mjs --allow-production && node test/upgrade-migration.mjs --allow-production && node test/fault-get.mjs --allow-production && node test/version-floor.mjs --allow-production && node test/fault-submit.mjs --allow-production && node test/race-attempts.mjs --allow-production && node test/attempt-identity.mjs --allow-production",
47
+ "test:getedits": "node test/get-protects-edits.mjs",
48
+ "test:upgrade": "node test/upgrade-migration.mjs",
49
+ "test:faultget": "node test/fault-get.mjs",
50
+ "test:floor": "node test/version-floor.mjs",
51
+ "test:faultsubmit": "node test/fault-submit.mjs",
52
+ "test:race": "node test/race-attempts.mjs",
53
+ "test:runner": "node test/runner-live.mjs",
54
+ "sync:plugin": "node scripts/sync-plugin-version.mjs",
55
+ "test:track": "node test/get-track.mjs",
56
+ "package:remote": "node ../scripts/package-cli-release.mjs",
57
+ "publish:remote": "node ../scripts/publish-cli-release.mjs"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "24.10.1",
61
+ "typescript": "5.9.3"
62
+ },
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/AlexC1991/cbx.git",
66
+ "directory": "cli"
67
+ }
68
+ }