@coderook/cli 0.31.12 → 0.32.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.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +73 -1
  3. package/dist/cbx/LICENSE +202 -0
  4. package/dist/cbx/NOTICE +8 -0
  5. package/dist/{desktop-app/src/main → cbx/src/core}/cbx.js +31 -0
  6. package/dist/{desktop-app/src/main → cbx/src/core}/download.js +62 -1
  7. package/dist/{desktop-app/src/main → cbx/src/core}/rules.js +2 -0
  8. package/dist/cbx/src/local/errors.js +23 -0
  9. package/dist/cbx/src/local/history.js +522 -0
  10. package/dist/cbx/src/local/merge.js +277 -0
  11. package/dist/cbx/src/local/merge_text.js +148 -0
  12. package/dist/cbx/src/local/remote.js +437 -0
  13. package/dist/cbx/src/local/repository.js +324 -0
  14. package/dist/cbx/src/local/snapshot.js +338 -0
  15. package/dist/cbx/src/local/store.js +258 -0
  16. package/dist/{desktop-app → cbx}/src/shared/safe_path.js +12 -0
  17. package/dist/cli/src/api.js +114 -2
  18. package/dist/cli/src/cli.js +198 -22
  19. package/dist/cli/src/diff_command.js +2 -2
  20. package/dist/cli/src/git_remote.js +6 -6
  21. package/dist/cli/src/licence_commands.js +3 -3
  22. package/dist/cli/src/local_commands.js +506 -0
  23. package/dist/cli/src/mcp.js +2 -2
  24. package/dist/cli/src/pages_command.js +625 -0
  25. package/dist/cli/src/project_commands.js +1 -0
  26. package/dist/cli/src/registry.js +1 -0
  27. package/dist/cli/src/runner.js +2 -2
  28. package/dist/cli/src/track_commands.js +1 -1
  29. package/package.json +2 -2
  30. package/skills/coderook/SKILL.md +41 -0
  31. /package/dist/{desktop-app/src/main → cbx/src/core}/compress.js +0 -0
  32. /package/dist/{desktop-app/src/main → cbx/src/core}/credentials.js +0 -0
  33. /package/dist/{desktop-app/src/main → cbx/src/core}/detect.js +0 -0
  34. /package/dist/{desktop-app/src/main → cbx/src/core}/faults.js +0 -0
  35. /package/dist/{desktop-app/src/main → cbx/src/core}/identify.js +0 -0
  36. /package/dist/{desktop-app/src/main → cbx/src/core}/ignore_templates.js +0 -0
  37. /package/dist/{desktop-app/src/main → cbx/src/core}/licence_texts.js +0 -0
  38. /package/dist/{desktop-app/src/main → cbx/src/core}/licences.js +0 -0
  39. /package/dist/{desktop-app/src/main → cbx/src/core}/profile.js +0 -0
  40. /package/dist/{desktop-app/src/main → cbx/src/core}/publish_name.js +0 -0
  41. /package/dist/{desktop-app/src/main → cbx/src/core}/retry.js +0 -0
  42. /package/dist/{desktop-app/src/main → cbx/src/core}/secret_patterns.js +0 -0
  43. /package/dist/{desktop-app/src/main → cbx/src/core}/solid.js +0 -0
  44. /package/dist/{desktop-app/src/main → cbx/src/core}/staging.js +0 -0
  45. /package/dist/{desktop-app/src/main → cbx/src/core}/tracks.js +0 -0
  46. /package/dist/{desktop-app/src/main → cbx/src/core}/transfer_progress.js +0 -0
  47. /package/dist/{desktop-app/src/main → cbx/src/core}/upload.js +0 -0
  48. /package/dist/{desktop-app/src/main → cbx/src/core}/worktree.js +0 -0
  49. /package/dist/{desktop-app → cbx}/src/shared/chunking.js +0 -0
  50. /package/dist/{desktop-app → cbx}/src/shared/compression_policy.js +0 -0
  51. /package/dist/{desktop-app → cbx}/src/shared/delta.js +0 -0
  52. /package/dist/{desktop-app → cbx}/src/shared/format.js +0 -0
  53. /package/dist/{desktop-app → cbx}/src/shared/merge_diff.js +0 -0
  54. /package/dist/{desktop-app → cbx}/src/shared/publication.js +0 -0
  55. /package/dist/{desktop-app → cbx}/src/shared/telemetry.js +0 -0
  56. /package/dist/{desktop-app → cbx}/src/shared/types.js +0 -0
@@ -0,0 +1,258 @@
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.ObjectStore = exports.PACKED_OBJECT_LIMIT = exports.DIGEST = void 0;
7
+ exports.digestOf = digestOf;
8
+ /**
9
+ * The object store under `.cbx/objects`: everything a local history holds.
10
+ *
11
+ * Every object is named by the SHA-256 of its original bytes and kept as a
12
+ * chunk frame (see `core/cbx.ts`), so it is compressed when that saves at
13
+ * least two percent and verified whenever it is read. Pieces of files, the
14
+ * trees that list them and the saves that name a tree are all objects; an
15
+ * object never changes once written, which is what makes writing one safe to
16
+ * repeat and a store safe to copy while nothing is saving.
17
+ *
18
+ * .cbx/objects/ab/cdef… one object on its own ("loose")
19
+ * .cbx/objects/packs/<id>.pack many small objects, frame after frame
20
+ * .cbx/objects/packs/<id>.idx where each of them starts, as JSON
21
+ *
22
+ * Small objects go into packs because a file per object is what made the
23
+ * first save of twenty thousand small files take thirty-six seconds on
24
+ * Windows, nearly all of it creating files. A pack is written whole and
25
+ * renamed into place before its index is, so an index on disk always
26
+ * describes a finished pack; a pack with no index is an interrupted write
27
+ * and is never read.
28
+ */
29
+ const node_crypto_1 = require("node:crypto");
30
+ const promises_1 = require("node:fs/promises");
31
+ const node_path_1 = __importDefault(require("node:path"));
32
+ const cbx_js_1 = require("../core/cbx.js");
33
+ exports.DIGEST = /^[0-9a-f]{64}$/;
34
+ /** Objects at most this big are packed rather than written on their own. */
35
+ exports.PACKED_OBJECT_LIMIT = 256 * 1024;
36
+ /** A pack is written out once it holds this much. */
37
+ const PACK_BYTES = 32 * 1024 * 1024;
38
+ function digestOf(bytes) {
39
+ return (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
40
+ }
41
+ class ObjectStore {
42
+ directory;
43
+ /* Fan-out folders already known to exist, so each is made once, not per object. */
44
+ made = new Set();
45
+ /* Every packed object, once the indexes have been read. */
46
+ packed = null;
47
+ /* Small objects waiting to be written as one pack. */
48
+ pending = new Map();
49
+ /* The batch being written right now, still readable while it is. */
50
+ writing = new Map();
51
+ pendingBytes = 0;
52
+ flushing = null;
53
+ /*
54
+ Packs held open while an operation reads from them. Opening a pack for
55
+ each object made restoring twenty thousand small files take three
56
+ minutes. Closed by `release`, which every operation calls when it ends,
57
+ because Windows will not delete a folder holding an open file.
58
+ */
59
+ handles = new Map();
60
+ constructor(directory) {
61
+ this.directory = directory;
62
+ }
63
+ get packDirectory() {
64
+ return node_path_1.default.join(this.directory, "packs");
65
+ }
66
+ where(digest) {
67
+ if (!exports.DIGEST.test(digest))
68
+ throw new Error(`Not an object name: ${digest}`);
69
+ return node_path_1.default.join(this.directory, digest.slice(0, 2), digest.slice(2));
70
+ }
71
+ async packs() {
72
+ if (this.packed)
73
+ return this.packed;
74
+ const found = new Map();
75
+ let names = [];
76
+ try {
77
+ names = await (0, promises_1.readdir)(this.packDirectory);
78
+ }
79
+ catch {
80
+ /* No packs yet. */
81
+ }
82
+ for (const name of names.filter((one) => one.endsWith(".idx")).sort()) {
83
+ const pack = node_path_1.default.join(this.packDirectory, name.replace(/\.idx$/, ".pack"));
84
+ const index = JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(this.packDirectory, name), "utf8"));
85
+ for (const [digest, [offset, length]] of Object.entries(index)) {
86
+ found.set(digest, { pack, offset, length });
87
+ }
88
+ }
89
+ this.packed = found;
90
+ return found;
91
+ }
92
+ async loose(digest) {
93
+ try {
94
+ await (0, promises_1.stat)(this.where(digest));
95
+ return true;
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ async has(digest) {
102
+ if (this.pending.has(digest) || this.writing.has(digest))
103
+ return true;
104
+ if ((await this.packs()).has(digest))
105
+ return true;
106
+ return this.loose(digest);
107
+ }
108
+ /**
109
+ * Keep `raw`, returning its name.
110
+ *
111
+ * A small object is held until there is a pack's worth, then written with
112
+ * the others; call `flush` before anything else needs to read it from disk.
113
+ * A large one is written beside its final name and renamed into place, so
114
+ * an interrupted save never leaves half an object under a real name. An
115
+ * object already present is not written again: same name, same bytes.
116
+ */
117
+ async put(raw, options = {}) {
118
+ const digest = digestOf(raw);
119
+ if (await this.has(digest))
120
+ return digest;
121
+ const { frame } = (0, cbx_js_1.chunkFrame)(raw);
122
+ if (!options.loose && raw.length <= exports.PACKED_OBJECT_LIMIT) {
123
+ this.pending.set(digest, frame);
124
+ this.pendingBytes += frame.length;
125
+ if (this.pendingBytes >= PACK_BYTES)
126
+ await this.flush();
127
+ return digest;
128
+ }
129
+ const target = this.where(digest);
130
+ const folder = node_path_1.default.dirname(target);
131
+ if (!this.made.has(folder)) {
132
+ await (0, promises_1.mkdir)(folder, { recursive: true });
133
+ this.made.add(folder);
134
+ }
135
+ const partial = `${target}.${(0, node_crypto_1.randomBytes)(6).toString("hex")}.partial`;
136
+ await (0, promises_1.writeFile)(partial, frame);
137
+ try {
138
+ await (0, promises_1.rename)(partial, target);
139
+ }
140
+ catch (error) {
141
+ await (0, promises_1.rm)(partial, { force: true });
142
+ /* Another writer got there first with the same bytes. */
143
+ if (!(await this.loose(digest)))
144
+ throw error;
145
+ }
146
+ return digest;
147
+ }
148
+ /** Write whatever small objects are waiting as one pack. */
149
+ async flush() {
150
+ /* One pack at a time; a second caller waits for the first and goes again. */
151
+ while (this.flushing)
152
+ await this.flushing;
153
+ if (!this.pending.size)
154
+ return;
155
+ const taken = this.pending;
156
+ this.pending = new Map();
157
+ this.pendingBytes = 0;
158
+ this.writing = taken;
159
+ this.flushing = this.writePack(taken).finally(() => {
160
+ this.writing = new Map();
161
+ this.flushing = null;
162
+ });
163
+ await this.flushing;
164
+ }
165
+ async writePack(objects) {
166
+ await (0, promises_1.mkdir)(this.packDirectory, { recursive: true });
167
+ const index = {};
168
+ const frames = [];
169
+ let offset = 0;
170
+ for (const [digest, frame] of [...objects].sort(([left], [right]) => (left < right ? -1 : 1))) {
171
+ index[digest] = [offset, frame.length];
172
+ frames.push(frame);
173
+ offset += frame.length;
174
+ }
175
+ const body = Buffer.concat(frames);
176
+ const id = digestOf(body);
177
+ const pack = node_path_1.default.join(this.packDirectory, `${id}.pack`);
178
+ const idx = node_path_1.default.join(this.packDirectory, `${id}.idx`);
179
+ await (0, promises_1.writeFile)(`${pack}.partial`, body);
180
+ await (0, promises_1.rename)(`${pack}.partial`, pack);
181
+ await (0, promises_1.writeFile)(`${idx}.partial`, JSON.stringify(index));
182
+ await (0, promises_1.rename)(`${idx}.partial`, idx);
183
+ const known = await this.packs();
184
+ for (const [digest, [at, length]] of Object.entries(index)) {
185
+ known.set(digest, { pack, offset: at, length });
186
+ }
187
+ }
188
+ async get(digest) {
189
+ const waiting = this.pending.get(digest) ?? this.writing.get(digest);
190
+ if (waiting)
191
+ return (0, cbx_js_1.openChunkFrame)(waiting, digest);
192
+ const located = (await this.packs()).get(digest);
193
+ if (located) {
194
+ let opening = this.handles.get(located.pack);
195
+ if (!opening) {
196
+ opening = (0, promises_1.open)(located.pack, "r");
197
+ this.handles.set(located.pack, opening);
198
+ }
199
+ const handle = await opening;
200
+ const frame = Buffer.alloc(located.length);
201
+ const { bytesRead } = await handle.read(frame, 0, located.length, located.offset);
202
+ if (bytesRead !== located.length) {
203
+ throw new Error(`Object ${digest.slice(0, 12)} runs past the end of its pack`);
204
+ }
205
+ return (0, cbx_js_1.openChunkFrame)(frame, digest);
206
+ }
207
+ let frame;
208
+ try {
209
+ frame = await (0, promises_1.readFile)(this.where(digest));
210
+ }
211
+ catch {
212
+ throw new Error(`The history is missing object ${digest.slice(0, 12)}`);
213
+ }
214
+ return (0, cbx_js_1.openChunkFrame)(frame, digest);
215
+ }
216
+ /** Close any packs held open for reading. Safe to call at any time. */
217
+ async release() {
218
+ const open = [...this.handles.values()];
219
+ this.handles.clear();
220
+ for (const opening of open) {
221
+ try {
222
+ await (await opening).close();
223
+ }
224
+ catch {
225
+ /* Never opened, or already closed. */
226
+ }
227
+ }
228
+ }
229
+ /** Stored on its own, so a save or tree is one file a person can find. */
230
+ async putJson(value) {
231
+ return this.put(Buffer.from(JSON.stringify(value), "utf8"), { loose: true });
232
+ }
233
+ async getJson(digest) {
234
+ return JSON.parse((await this.get(digest)).toString("utf8"));
235
+ }
236
+ /** Every object whose name starts with `prefix` (at least four characters). */
237
+ async matching(prefix) {
238
+ if (!/^[0-9a-f]{4,64}$/.test(prefix))
239
+ return [];
240
+ const found = new Set();
241
+ try {
242
+ const rest = prefix.slice(2);
243
+ for (const name of await (0, promises_1.readdir)(node_path_1.default.join(this.directory, prefix.slice(0, 2)))) {
244
+ if (name.length === 62 && name.startsWith(rest))
245
+ found.add(prefix.slice(0, 2) + name);
246
+ }
247
+ }
248
+ catch {
249
+ /* No loose objects under that prefix. */
250
+ }
251
+ for (const digest of (await this.packs()).keys()) {
252
+ if (digest.startsWith(prefix))
253
+ found.add(digest);
254
+ }
255
+ return [...found];
256
+ }
257
+ }
258
+ exports.ObjectStore = ObjectStore;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isGitDirectoryName = isGitDirectoryName;
7
+ exports.isCbxDirectoryName = isCbxDirectoryName;
7
8
  exports.whyUnsafe = whyUnsafe;
8
9
  exports.safeParts = safeParts;
9
10
  exports.inside = inside;
@@ -38,6 +39,15 @@ function isGitDirectoryName(part) {
38
39
  const name = part.split(":")[0].replace(/[. ]+$/, "").toLowerCase();
39
40
  return name === ".git" || /^git~\d+$/.test(name);
40
41
  }
42
+ /**
43
+ * A name CBX keeps its local history under, compared the way git's is.
44
+ *
45
+ * A version holding `.cbx/lines/main` would otherwise move somebody's line to
46
+ * a save of the version's choosing the moment it was fetched.
47
+ */
48
+ function isCbxDirectoryName(part) {
49
+ return part.split(":")[0].replace(/[. ]+$/, "").toLowerCase() === ".cbx";
50
+ }
41
51
  /** Why this path must not be written, or null when it may. */
42
52
  function whyUnsafe(relative) {
43
53
  if (!relative)
@@ -52,6 +62,8 @@ function whyUnsafe(relative) {
52
62
  return "it climbs out of the folder";
53
63
  if (isGitDirectoryName(part))
54
64
  return "it writes into git's own directory";
65
+ if (isCbxDirectoryName(part))
66
+ return "it writes into CBX's own history";
55
67
  /* A stream or a drive-relative name on Windows; refused by the service too. */
56
68
  if (process.platform === "win32" && part.includes(":")) {
57
69
  return "it names a Windows data stream";
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ServiceError = void 0;
3
4
  exports.putObject = putObject;
4
5
  exports.beginUpload = beginUpload;
5
6
  exports.uploadPart = uploadPart;
@@ -22,6 +23,10 @@ exports.cancelMerge = cancelMerge;
22
23
  exports.aiAccess = aiAccess;
23
24
  exports.setAiAccess = setAiAccess;
24
25
  exports.updateProject = updateProject;
26
+ exports.pages = pages;
27
+ exports.setPages = setPages;
28
+ exports.detectPages = detectPages;
29
+ exports.autoPages = autoPages;
25
30
  exports.compareVersions = compareVersions;
26
31
  exports.fileTextAt = fileTextAt;
27
32
  exports.versions = versions;
@@ -53,7 +58,7 @@ exports.addWebhook = addWebhook;
53
58
  exports.removeWebhook = removeWebhook;
54
59
  exports.collisionCheck = collisionCheck;
55
60
  /** The small part of the API the command-line tool needs directly. */
56
- const identify_js_1 = require("../../desktop-app/src/main/identify.js");
61
+ const identify_js_1 = require("../../cbx/src/core/identify.js");
57
62
  const config_js_1 = require("./config.js");
58
63
  /**
59
64
  * How long one request may go unanswered before it is treated as lost.
@@ -70,6 +75,25 @@ const config_js_1 = require("./config.js");
70
75
  * than ending the run.
71
76
  */
72
77
  const REQUEST_DEADLINE_MS = 180_000;
78
+ /**
79
+ * A refusal from the service, with what it said and why.
80
+ *
81
+ * Still an Error carrying the service's own sentence, so every caller that
82
+ * prints `error.message` goes on printing the same words. The status and the
83
+ * code are there for the few commands that answer a particular refusal
84
+ * differently — "not available here yet" is not the same as "not allowed".
85
+ */
86
+ class ServiceError extends Error {
87
+ status;
88
+ code;
89
+ constructor(message, status, code) {
90
+ super(message);
91
+ this.status = status;
92
+ this.code = code;
93
+ this.name = "ServiceError";
94
+ }
95
+ }
96
+ exports.ServiceError = ServiceError;
73
97
  async function call(route, options = {}) {
74
98
  const token = await (0, config_js_1.loadToken)();
75
99
  if (!token) {
@@ -93,7 +117,7 @@ async function call(route, options = {}) {
93
117
  if (response.status === 401) {
94
118
  throw new Error("That token was not accepted. Run: cbx sign-in");
95
119
  }
96
- throw new Error(body?.error?.message ?? `${route} failed (${response.status})`);
120
+ throw new ServiceError(body?.error?.message ?? `${route} failed (${response.status})`, response.status, typeof body?.error?.code === "string" ? body.error.code : null);
97
121
  }
98
122
  return body;
99
123
  }
@@ -388,6 +412,94 @@ async function updateProject(repositoryId, change) {
388
412
  body: change,
389
413
  });
390
414
  }
415
+ function pagesSettings(row) {
416
+ const live = row.live;
417
+ const entry = row.entry;
418
+ return {
419
+ enabled: row.enabled === true,
420
+ folder: String(row.folder ?? ""),
421
+ versionId: row.versionId == null ? null : String(row.versionId),
422
+ spa: row.spa === true,
423
+ isolation: row.isolation === true,
424
+ address: row.address == null ? null : String(row.address),
425
+ domainReady: row.domainReady === true,
426
+ live: live
427
+ ? {
428
+ id: String(live.id ?? ""),
429
+ sequence: Number(live.sequence ?? 0),
430
+ name: live.name == null ? null : String(live.name),
431
+ }
432
+ : null,
433
+ entry: entry
434
+ ? { path: String(entry.path ?? ""), found: entry.found === true }
435
+ : null,
436
+ blocked: row.blocked == null ? null : String(row.blocked),
437
+ };
438
+ }
439
+ const pagesRoute = (repositoryId) => `/v1/repositories/${encodeURIComponent(repositoryId)}/pages`;
440
+ /** What a project's site is set to, and what it is serving. */
441
+ async function pages(repositoryId) {
442
+ return pagesSettings(await call(pagesRoute(repositoryId)));
443
+ }
444
+ /** Change a project's site. Only what is given is sent. */
445
+ async function setPages(repositoryId, change) {
446
+ return pagesSettings(await call(pagesRoute(repositoryId), {
447
+ method: "PUT",
448
+ body: change,
449
+ }));
450
+ }
451
+ /** What the newest published version looks like it could serve. */
452
+ async function detectPages(repositoryId) {
453
+ const body = await call(`${pagesRoute(repositoryId)}/detect`);
454
+ const version = body.version;
455
+ const suggestion = body.suggestion;
456
+ return {
457
+ version: version
458
+ ? {
459
+ id: String(version.id ?? ""),
460
+ sequence: Number(version.sequence ?? 0),
461
+ name: version.name == null ? null : String(version.name),
462
+ }
463
+ : null,
464
+ suggestion: suggestion
465
+ ? {
466
+ folder: String(suggestion.folder ?? ""),
467
+ kind: String(suggestion.kind ?? "static"),
468
+ spa: suggestion.spa === true,
469
+ isolation: suggestion.isolation === true,
470
+ why: String(suggestion.why ?? ""),
471
+ }
472
+ : null,
473
+ candidates: Array.isArray(body.candidates)
474
+ ? body.candidates.map((one) => ({
475
+ folder: String(one.folder ?? ""),
476
+ kind: String(one.kind ?? "static"),
477
+ why: String(one.why ?? ""),
478
+ }))
479
+ : [],
480
+ problem: body.problem == null ? null : String(body.problem),
481
+ };
482
+ }
483
+ /**
484
+ * Detect and apply in one request, so what is switched on is what the
485
+ * service found rather than what this client last saw.
486
+ */
487
+ async function autoPages(repositoryId) {
488
+ const body = await call(`${pagesRoute(repositoryId)}/auto`, { method: "POST" });
489
+ const applied = body.applied;
490
+ return {
491
+ ...pagesSettings(body),
492
+ applied: applied
493
+ ? {
494
+ folder: String(applied.folder ?? ""),
495
+ kind: String(applied.kind ?? "static"),
496
+ spa: applied.spa === true,
497
+ isolation: applied.isolation === true,
498
+ why: String(applied.why ?? ""),
499
+ }
500
+ : null,
501
+ };
502
+ }
391
503
  /**
392
504
  * What changed between two saved versions.
393
505
  *