@coderook/cli 0.31.13 → 0.32.1

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 (58) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/LICENSE +202 -0
  3. package/NOTICE +8 -0
  4. package/README.md +53 -1
  5. package/dist/cbx/LICENSE +202 -0
  6. package/dist/cbx/NOTICE +8 -0
  7. package/dist/{desktop-app/src/main → cbx/src/core}/cbx.js +31 -0
  8. package/dist/{desktop-app/src/main → cbx/src/core}/download.js +62 -1
  9. package/dist/{desktop-app/src/main → cbx/src/core}/rules.js +2 -0
  10. package/dist/cbx/src/local/errors.js +23 -0
  11. package/dist/cbx/src/local/history.js +522 -0
  12. package/dist/cbx/src/local/merge.js +277 -0
  13. package/dist/cbx/src/local/merge_text.js +148 -0
  14. package/dist/cbx/src/local/remote.js +437 -0
  15. package/dist/cbx/src/local/repository.js +324 -0
  16. package/dist/cbx/src/local/snapshot.js +338 -0
  17. package/dist/cbx/src/local/store.js +258 -0
  18. package/dist/{desktop-app → cbx}/src/shared/safe_path.js +12 -0
  19. package/dist/cli/src/api.js +1 -1
  20. package/dist/cli/src/cli.js +166 -24
  21. package/dist/cli/src/diff_command.js +2 -2
  22. package/dist/cli/src/git_remote.js +6 -6
  23. package/dist/cli/src/licence_commands.js +3 -3
  24. package/dist/cli/src/local_commands.js +506 -0
  25. package/dist/cli/src/mcp.js +2 -2
  26. package/dist/cli/src/pages_command.js +2 -2
  27. package/dist/cli/src/registry.js +1 -0
  28. package/dist/cli/src/runner.js +2 -2
  29. package/dist/cli/src/track_commands.js +1 -1
  30. package/package.json +11 -5
  31. package/skills/coderook/SKILL.md +24 -0
  32. package/LICENSE.txt +0 -46
  33. /package/dist/{desktop-app/src/main → cbx/src/core}/compress.js +0 -0
  34. /package/dist/{desktop-app/src/main → cbx/src/core}/credentials.js +0 -0
  35. /package/dist/{desktop-app/src/main → cbx/src/core}/detect.js +0 -0
  36. /package/dist/{desktop-app/src/main → cbx/src/core}/faults.js +0 -0
  37. /package/dist/{desktop-app/src/main → cbx/src/core}/identify.js +0 -0
  38. /package/dist/{desktop-app/src/main → cbx/src/core}/ignore_templates.js +0 -0
  39. /package/dist/{desktop-app/src/main → cbx/src/core}/licence_texts.js +0 -0
  40. /package/dist/{desktop-app/src/main → cbx/src/core}/licences.js +0 -0
  41. /package/dist/{desktop-app/src/main → cbx/src/core}/profile.js +0 -0
  42. /package/dist/{desktop-app/src/main → cbx/src/core}/publish_name.js +0 -0
  43. /package/dist/{desktop-app/src/main → cbx/src/core}/retry.js +0 -0
  44. /package/dist/{desktop-app/src/main → cbx/src/core}/secret_patterns.js +0 -0
  45. /package/dist/{desktop-app/src/main → cbx/src/core}/solid.js +0 -0
  46. /package/dist/{desktop-app/src/main → cbx/src/core}/staging.js +0 -0
  47. /package/dist/{desktop-app/src/main → cbx/src/core}/tracks.js +0 -0
  48. /package/dist/{desktop-app/src/main → cbx/src/core}/transfer_progress.js +0 -0
  49. /package/dist/{desktop-app/src/main → cbx/src/core}/upload.js +0 -0
  50. /package/dist/{desktop-app/src/main → cbx/src/core}/worktree.js +0 -0
  51. /package/dist/{desktop-app → cbx}/src/shared/chunking.js +0 -0
  52. /package/dist/{desktop-app → cbx}/src/shared/compression_policy.js +0 -0
  53. /package/dist/{desktop-app → cbx}/src/shared/delta.js +0 -0
  54. /package/dist/{desktop-app → cbx}/src/shared/format.js +0 -0
  55. /package/dist/{desktop-app → cbx}/src/shared/merge_diff.js +0 -0
  56. /package/dist/{desktop-app → cbx}/src/shared/publication.js +0 -0
  57. /package/dist/{desktop-app → cbx}/src/shared/telemetry.js +0 -0
  58. /package/dist/{desktop-app → cbx}/src/shared/types.js +0 -0
@@ -0,0 +1,506 @@
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.localHistory = localHistory;
7
+ exports.localFirst = localFirst;
8
+ exports.commandInit = commandInit;
9
+ exports.commandSave = commandSave;
10
+ exports.localStatus = localStatus;
11
+ exports.localLog = localLog;
12
+ exports.localDiff = localDiff;
13
+ exports.commandRestore = commandRestore;
14
+ exports.localSwitch = localSwitch;
15
+ exports.localPush = localPush;
16
+ exports.localPull = localPull;
17
+ exports.localMerge = localMerge;
18
+ /**
19
+ * The local history, from a terminal: `cbx init`, `save`, `restore`, and the
20
+ * local side of `status`, `diff`, `log` and `switch`.
21
+ *
22
+ * None of these talk to CodeRook. A folder with a `.cbx/` history answers
23
+ * `status`, `diff`, `log` and `switch` from it; a folder without one gets the
24
+ * account-backed commands those names have always meant, so nothing anybody
25
+ * already relies on changes until they run `cbx init`.
26
+ */
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const node_process_1 = __importDefault(require("node:process"));
29
+ const history_js_1 = require("../../cbx/src/local/history.js");
30
+ const merge_js_1 = require("../../cbx/src/local/merge.js");
31
+ const repository_js_1 = require("../../cbx/src/local/repository.js");
32
+ const remote_js_1 = require("../../cbx/src/local/remote.js");
33
+ const api_js_1 = require("./api.js");
34
+ const config_js_1 = require("./config.js");
35
+ const project_commands_js_1 = require("./project_commands.js");
36
+ const publish_js_1 = require("./publish.js");
37
+ const ESC = String.fromCharCode(27);
38
+ const paint = (code) => (value) => `${ESC}[${code}m${value}${ESC}[0m`;
39
+ const dim = paint("2");
40
+ const bold = paint("1");
41
+ const red = paint("31");
42
+ const green = paint("32");
43
+ const accent = paint("33");
44
+ const short = (id) => id.slice(0, 10);
45
+ const plural = (count, one, many = `${one}s`) => `${count.toLocaleString()} ${count === 1 ? one : many}`;
46
+ function text(parsed, ...names) {
47
+ for (const name of names) {
48
+ const value = parsed.flags.get(name);
49
+ if (typeof value === "string")
50
+ return value;
51
+ }
52
+ return undefined;
53
+ }
54
+ const has = (parsed, ...names) => names.some((name) => parsed.flags.has(name));
55
+ function bytes(value) {
56
+ if (value >= 1024 ** 3)
57
+ return `${(value / 1024 ** 3).toFixed(2)} GB`;
58
+ if (value >= 1024 ** 2)
59
+ return `${(value / 1024 ** 2).toFixed(1)} MB`;
60
+ if (value >= 1024)
61
+ return `${(value / 1024).toFixed(0)} KB`;
62
+ return `${value} B`;
63
+ }
64
+ /** The history the working directory is in, if it is in one. */
65
+ async function localHistory(from = node_process_1.default.cwd()) {
66
+ return repository_js_1.Repository.find(from);
67
+ }
68
+ /**
69
+ * A command that means the local history in a folder that has one, and what
70
+ * it has always meant everywhere else.
71
+ */
72
+ function localFirst(local, otherwise, from = () => node_process_1.default.cwd()) {
73
+ return async (parsed) => {
74
+ const repository = await localHistory(from(parsed));
75
+ return repository ? local(parsed, repository) : otherwise(parsed);
76
+ };
77
+ }
78
+ function printChanges(changes, limit = 50) {
79
+ const rows = [
80
+ ...changes.added.map((file) => [green("added "), file]),
81
+ ...changes.modified.map((file) => [accent("changed "), file]),
82
+ ...changes.deleted.map((file) => [red("deleted "), file]),
83
+ ].sort((left, right) => (left[1] < right[1] ? -1 : left[1] > right[1] ? 1 : 0));
84
+ for (const [mark, file] of rows.slice(0, limit))
85
+ console.log(` ${mark}${file}`);
86
+ if (rows.length > limit)
87
+ console.log(dim(` … and ${rows.length - limit} more`));
88
+ }
89
+ function describeSave(one) {
90
+ const when = new Date(one.created);
91
+ const date = Number.isNaN(when.getTime())
92
+ ? one.created
93
+ : when.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
94
+ return `${accent(short(one.id))} ${one.message.split("\n")[0]} ${dim(`${one.author} · ${date}`)}`;
95
+ }
96
+ /** Refusals are printed as sentences, not stack traces. */
97
+ async function reporting(work) {
98
+ try {
99
+ return await work();
100
+ }
101
+ catch (error) {
102
+ if (error instanceof remote_js_1.RemoteAhead || error instanceof remote_js_1.RemoteDiffers || error instanceof remote_js_1.NoRemote) {
103
+ console.error(red(error.message));
104
+ return 1;
105
+ }
106
+ const failure = (0, publish_js_1.classifyPublishFailure)(error);
107
+ if (failure?.kind === "credentials") {
108
+ console.error(red(failure.message));
109
+ console.error(dim("Take the key out of that save, or push it deliberately with --allow-secrets."));
110
+ return 1;
111
+ }
112
+ if (failure?.kind === "interrupted") {
113
+ console.error(red(`The connection failed part way: ${failure.message}`));
114
+ console.error(dim("Push again. Saves already on CodeRook are not sent twice."));
115
+ return 1;
116
+ }
117
+ if (error instanceof history_js_1.UnsavedChanges || error instanceof history_js_1.UnresolvedConflicts) {
118
+ console.error(red(error.message));
119
+ for (const file of error.files.slice(0, 20))
120
+ console.error(` ${file}`);
121
+ if (error.files.length > 20)
122
+ console.error(dim(` … and ${error.files.length - 20} more`));
123
+ return 1;
124
+ }
125
+ console.error(red(error instanceof Error ? error.message : String(error)));
126
+ return 1;
127
+ }
128
+ }
129
+ /* ---- init ------------------------------------------------------------ */
130
+ async function commandInit(parsed) {
131
+ return reporting(async () => {
132
+ const folder = node_path_1.default.resolve(parsed.positional[0] ?? node_process_1.default.cwd());
133
+ const inside = await localHistory(folder);
134
+ if (inside && inside.root !== folder) {
135
+ console.error(red(`${folder} is already inside the history at ${inside.root}.`));
136
+ console.error(dim("One history per project. Run cbx commands from anywhere inside it."));
137
+ return 1;
138
+ }
139
+ const repository = await (0, history_js_1.init)(folder, text(parsed, "line") ?? undefined);
140
+ console.log(`Started a history in ${bold(repository.root)} on ${accent(await repository.currentLine())}.`);
141
+ console.log(dim("It lives in .cbx/ and never leaves this machine by itself. Run " +
142
+ `cbx save -m "…" to record the folder as it is.`));
143
+ return 0;
144
+ });
145
+ }
146
+ /* ---- save ------------------------------------------------------------ */
147
+ async function commandSave(parsed) {
148
+ return reporting(async () => {
149
+ const repository = await repository_js_1.Repository.open(node_process_1.default.cwd());
150
+ const message = text(parsed, "m", "message") ?? "";
151
+ const merging = await (0, merge_js_1.readMergeState)(repository);
152
+ if (!message.trim() && !merging) {
153
+ console.error(red('A save needs a message: cbx save -m "what changed"'));
154
+ return 1;
155
+ }
156
+ const interactive = node_process_1.default.stdout.isTTY;
157
+ let shown = 0;
158
+ const result = await (0, history_js_1.save)(repository, {
159
+ message,
160
+ force: has(parsed, "force", "f"),
161
+ author: text(parsed, "author"),
162
+ progress: interactive
163
+ ? (done, total) => {
164
+ const now = Date.now();
165
+ if (now - shown < 100 && done < total)
166
+ return;
167
+ shown = now;
168
+ node_process_1.default.stdout.write(`\r${dim(`Reading ${done.toLocaleString()} of ${total.toLocaleString()} files`)} `);
169
+ }
170
+ : undefined,
171
+ });
172
+ if (interactive)
173
+ node_process_1.default.stdout.write(`\r${" ".repeat(60)}\r`);
174
+ if (!result.saved) {
175
+ console.log("Nothing has changed since the last save, so there is nothing to record.");
176
+ return 0;
177
+ }
178
+ const count = (0, history_js_1.changeCount)(result.changes);
179
+ console.log(`Saved ${accent(short(result.save.id))} on ${accent(result.save.line)}: ` +
180
+ `${result.save.message.split("\n")[0]}`);
181
+ console.log(dim(`${plural(count, "file")} changed · ${plural(result.files, "file")}, ` +
182
+ `${bytes(result.bytes)} in the save`));
183
+ printChanges(result.changes, 20);
184
+ return 0;
185
+ });
186
+ }
187
+ /* ---- status ---------------------------------------------------------- */
188
+ async function localStatus(_parsed, repository) {
189
+ return reporting(async () => {
190
+ const now = await (0, history_js_1.status)(repository);
191
+ console.log(`${bold(node_path_1.default.basename(repository.root))} on ${accent(now.line)}` +
192
+ (now.head ? dim(` last saved ${short(now.head.id)}: ${now.head.message.split("\n")[0]}`) : dim(" nothing saved yet")));
193
+ if (now.merging) {
194
+ const left = now.merging.unresolved.length;
195
+ console.log(`Merging ${accent(short(now.merging.theirs))}: ` +
196
+ (left ? red(`${plural(left, "file")} still ${left === 1 ? "has" : "have"} conflict markers`) : green("no conflict markers left")));
197
+ for (const conflict of now.merging.conflicts) {
198
+ const how = conflict.kind === "text"
199
+ ? now.merging.unresolved.includes(conflict.path) ? "markers in it" : "edited"
200
+ : conflict.kind === "binary"
201
+ ? "both changed; this line's copy is in the folder"
202
+ : conflict.kind === "deleted-here"
203
+ ? "deleted here, changed there; their copy is in the folder"
204
+ : "changed here, deleted there; this copy is in the folder";
205
+ console.log(` ${red("conflict")} ${conflict.path} ${dim(how)}`);
206
+ }
207
+ console.log(dim(`Finish with cbx save, take a side with cbx merge --mine or --theirs, or cbx merge --cancel.`));
208
+ }
209
+ const count = (0, history_js_1.changeCount)(now.changes);
210
+ if (!count) {
211
+ console.log("Nothing has changed since the last save.");
212
+ return 0;
213
+ }
214
+ console.log(`${plural(count, "file")} changed since the last save:`);
215
+ printChanges(now.changes);
216
+ console.log(dim(`cbx diff shows the lines; cbx save -m "…" records them.`));
217
+ return 0;
218
+ });
219
+ }
220
+ /* ---- log ------------------------------------------------------------- */
221
+ async function localLog(parsed, repository) {
222
+ return reporting(async () => {
223
+ const limit = Number(text(parsed, "limit") ?? 20);
224
+ const saves = await (0, history_js_1.log)(repository, {
225
+ from: parsed.positional[0],
226
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 20,
227
+ });
228
+ if (!saves.length) {
229
+ console.log(`Nothing saved on ${accent(await repository.currentLine())} yet.`);
230
+ return 0;
231
+ }
232
+ for (const one of saves)
233
+ console.log(describeSave(one));
234
+ return 0;
235
+ });
236
+ }
237
+ /* ---- diff ------------------------------------------------------------ */
238
+ async function localDiff(parsed, repository) {
239
+ return reporting(async () => {
240
+ const [from, to] = parsed.positional;
241
+ const contains = text(parsed, "path");
242
+ const context = Number(text(parsed, "context") ?? 3);
243
+ const changes = await (0, history_js_1.diff)(repository, {
244
+ from,
245
+ to,
246
+ only: contains ? (file) => file.includes(contains) : undefined,
247
+ context: Number.isFinite(context) && context >= 0 ? context : 3,
248
+ });
249
+ if (!changes.length) {
250
+ console.log(to ? "Those two saves hold the same files." : "Nothing has changed since the last save.");
251
+ return 0;
252
+ }
253
+ const mark = { added: green("+"), modified: accent("~"), deleted: red("-") };
254
+ for (const change of changes) {
255
+ console.log(`${mark[change.kind]} ${bold(change.path)}`);
256
+ if (has(parsed, "name-only"))
257
+ continue;
258
+ if (change.executableChanged)
259
+ console.log(dim(" execute permission changed"));
260
+ if (change.summary) {
261
+ console.log(dim(` ${change.summary}`));
262
+ continue;
263
+ }
264
+ for (const chunk of change.chunks ?? []) {
265
+ if (chunk.kind === "gap") {
266
+ console.log(dim(` ... ${chunk.skipped} unchanged`));
267
+ continue;
268
+ }
269
+ for (const line of chunk.lines) {
270
+ const at = String(line.candidateLine ?? line.targetLine ?? "").padStart(5);
271
+ if (line.side === "same")
272
+ console.log(dim(`${at} ${line.text}`));
273
+ else if (line.side === "candidate")
274
+ console.log(`${dim(at)} ${green("+")} ${green(line.text)}`);
275
+ else
276
+ console.log(`${dim(at)} ${red("-")} ${red(line.text)}`);
277
+ }
278
+ }
279
+ }
280
+ return 0;
281
+ });
282
+ }
283
+ /* ---- restore --------------------------------------------------------- */
284
+ async function commandRestore(parsed) {
285
+ return reporting(async () => {
286
+ const repository = await repository_js_1.Repository.open(node_process_1.default.cwd());
287
+ const from = text(parsed, "from");
288
+ const result = await (0, history_js_1.restore)(repository, {
289
+ from,
290
+ only: (0, history_js_1.pathFilter)(repository, parsed.positional),
291
+ force: has(parsed, "force", "f"),
292
+ });
293
+ const touched = result.written.length + result.removed.length;
294
+ if (!touched) {
295
+ console.log(`Already matches ${accent(short(result.save.id))}; nothing to put back.`);
296
+ return 0;
297
+ }
298
+ console.log(`Put back ${plural(touched, "file")} from ${accent(short(result.save.id))}: ` +
299
+ `${result.save.message.split("\n")[0]}`);
300
+ for (const file of result.written.slice(0, 20))
301
+ console.log(` ${green("restored")} ${file}`);
302
+ for (const file of result.removed.slice(0, 20))
303
+ console.log(` ${red("removed ")} ${file}`);
304
+ if (touched > 40)
305
+ console.log(dim(` … and more`));
306
+ console.log(dim("Your line has not moved. Save to record the folder as it is now."));
307
+ return 0;
308
+ });
309
+ }
310
+ /* ---- switch ---------------------------------------------------------- */
311
+ async function localSwitch(parsed, repository) {
312
+ return reporting(async () => {
313
+ const line = parsed.positional[0];
314
+ if (!line) {
315
+ const current = await repository.currentLine();
316
+ for (const one of await repository.lines()) {
317
+ console.log(`${one.name === current ? accent("*") : " "} ${one.name} ${dim(short(one.tip))}`);
318
+ }
319
+ if (!(await repository.lineExists(current))) {
320
+ console.log(`${accent("*")} ${current} ${dim("nothing saved yet")}`);
321
+ }
322
+ return 0;
323
+ }
324
+ const result = await (0, history_js_1.switchLine)(repository, line, {
325
+ create: has(parsed, "c", "create", "new", "n"),
326
+ force: has(parsed, "force", "f"),
327
+ });
328
+ if (result.created) {
329
+ console.log(`Started ${accent(result.line)}` +
330
+ (result.save ? ` from ${accent(short(result.save.id))}.` : ".") +
331
+ dim(" No files changed."));
332
+ return 0;
333
+ }
334
+ const touched = result.written.length + result.removed.length;
335
+ console.log(`On ${accent(result.line)} now` +
336
+ (touched ? `, ${plural(touched, "file")} changed.` : ", and no files needed changing."));
337
+ return 0;
338
+ });
339
+ }
340
+ /* ---- push and pull ---------------------------------------------------- */
341
+ /**
342
+ * Keep the folder's CodeRook link in step with what a push or pull made, so
343
+ * `cbx submit` and `cbx get` in the same folder start from the same place.
344
+ */
345
+ async function recordLink(repository, line, repositoryId, head, manifest) {
346
+ const link = await (0, config_js_1.readLink)(repository.root);
347
+ const slug = (await (0, api_js_1.projectById)(repositoryId).catch(() => null))?.slug ??
348
+ link?.slug ??
349
+ node_path_1.default.basename(repository.root);
350
+ await (0, config_js_1.writeLink)(repository.root, {
351
+ ...(link ?? {}),
352
+ repositoryId,
353
+ slug,
354
+ sequence: head.sequence,
355
+ track: line,
356
+ versionId: head.versionId,
357
+ baseVersionId: head.versionId,
358
+ observedHeadVersionId: head.versionId,
359
+ manifest,
360
+ local: manifest,
361
+ });
362
+ }
363
+ async function localPush(parsed, repository) {
364
+ return reporting(async () => {
365
+ const link = await (0, config_js_1.readLink)(repository.root);
366
+ const interactive = node_process_1.default.stdout.isTTY;
367
+ const result = await (0, remote_js_1.push)(repository, {
368
+ credentials: config_js_1.credentials,
369
+ projectName: link?.slug ?? node_path_1.default.basename(repository.root),
370
+ repositoryId: link?.repositoryId ?? null,
371
+ allowSecrets: has(parsed, "allow-secrets"),
372
+ acknowledged: has(parsed, "yes", "y"),
373
+ report: (event) => {
374
+ if (event.kind === "save" && interactive) {
375
+ node_process_1.default.stdout.write(`\r${dim(`Sending ${event.index + 1} of ${event.total}: ${event.save.message.split("\n")[0].slice(0, 50)}`)} `);
376
+ }
377
+ if (event.kind === "pushed" || event.kind === "unchanged") {
378
+ if (interactive)
379
+ node_process_1.default.stdout.write(`\r${" ".repeat(80)}\r`);
380
+ console.log(` ${accent(short(event.save.id))} ` +
381
+ (event.kind === "pushed" ? green(`v${event.sequence}`) : dim("already there")) +
382
+ ` ${event.save.message.split("\n")[0]}`);
383
+ }
384
+ },
385
+ });
386
+ if (!result.pushed.length) {
387
+ console.log(`Everything on ${accent(result.line)} is already on CodeRook.`);
388
+ return 0;
389
+ }
390
+ const last = result.pushed[result.pushed.length - 1];
391
+ console.log(`Pushed ${plural(result.pushed.length, "save")} to CodeRook on ${accent(result.line)}; ` +
392
+ `it is at ${accent(`v${last.sequence}`)} now.`);
393
+ if (result.repositoryId && result.head) {
394
+ await recordLink(repository, result.line, result.repositoryId, result.head, result.head.manifest);
395
+ }
396
+ return 0;
397
+ });
398
+ }
399
+ async function localPull(parsed, repository) {
400
+ return reporting(async () => {
401
+ let repositoryId = (await (0, remote_js_1.readRemote)(repository)).repositoryId;
402
+ const named = parsed.positional[0];
403
+ if (named) {
404
+ const project = await (0, project_commands_js_1.resolveProject)(named);
405
+ if (!project)
406
+ return 1;
407
+ repositoryId = project.id;
408
+ }
409
+ repositoryId ??= (await (0, config_js_1.readLink)(repository.root))?.repositoryId ?? null;
410
+ const interactive = node_process_1.default.stdout.isTTY;
411
+ const result = await (0, remote_js_1.pull)(repository, {
412
+ credentials: config_js_1.credentials,
413
+ repositoryId,
414
+ force: has(parsed, "force", "f"),
415
+ report: (event) => {
416
+ if (!interactive)
417
+ return;
418
+ if (event.kind === "version") {
419
+ node_process_1.default.stdout.write(`\r${dim(`Fetching v${event.version.sequence} (${event.index + 1} of ${event.total})`)}${" ".repeat(20)}`);
420
+ }
421
+ },
422
+ });
423
+ if (interactive)
424
+ node_process_1.default.stdout.write(`\r${" ".repeat(80)}\r`);
425
+ for (const save of result.imported)
426
+ console.log(` ${describeSave(save)}`);
427
+ if (result.merged?.conflicts) {
428
+ console.log(`CodeRook's ${accent(result.line)} and this history had both moved on, and merging them left ` +
429
+ red(plural(result.merged.conflicts.length, "conflict")) + ":");
430
+ for (const conflict of result.merged.conflicts)
431
+ console.log(` ${red("conflict")} ${conflict.path}`);
432
+ console.log(dim("Fix them, then cbx save and cbx push. cbx status lists them; cbx merge --cancel calls it off."));
433
+ return 1;
434
+ }
435
+ if (result.merged?.save) {
436
+ const touched = result.written.length + result.removed.length;
437
+ console.log(`Merged ${plural(result.imported.length, "save")} from CodeRook's ${accent(result.line)} with this history's own` +
438
+ (touched ? `; ${plural(touched, "file")} changed.` : ".") +
439
+ ` Push to send the merge.`);
440
+ return 0;
441
+ }
442
+ if (!result.moved) {
443
+ console.log(result.imported.length
444
+ ? `Fetched ${plural(result.imported.length, "save")}.`
445
+ : `Already up to date with CodeRook's ${accent(result.line)}.`);
446
+ return 0;
447
+ }
448
+ const touched = result.written.length + result.removed.length;
449
+ console.log(`Pulled ${plural(result.imported.length, "save")} on ${accent(result.line)}` +
450
+ (touched ? `; ${plural(touched, "file")} changed.` : "."));
451
+ const pulledFrom = (await (0, remote_js_1.readRemote)(repository)).repositoryId;
452
+ const tip = await repository.head();
453
+ if (pulledFrom && result.head && tip) {
454
+ const tree = await repository.readTree((await repository.readSave(tip)).tree);
455
+ const manifest = Object.fromEntries(tree.files.map((file) => [file.path, file.sha256]));
456
+ await recordLink(repository, result.line, pulledFrom, result.head, manifest);
457
+ }
458
+ return 0;
459
+ });
460
+ }
461
+ /* ---- merge ------------------------------------------------------------ */
462
+ async function localMerge(parsed, repository) {
463
+ return reporting(async () => {
464
+ if (has(parsed, "cancel", "abort")) {
465
+ const put = await (0, history_js_1.cancelMerge)(repository);
466
+ console.log(`Merge called off; ${plural(put.length, "file")} put back as the last save had them.`);
467
+ return 0;
468
+ }
469
+ const pathText = text(parsed, "path");
470
+ const only = pathText ? (0, history_js_1.pathFilter)(repository, [pathText]) : undefined;
471
+ for (const side of ["mine", "theirs"]) {
472
+ if (!has(parsed, side))
473
+ continue;
474
+ const settled = await (0, history_js_1.resolveConflicts)(repository, side, only);
475
+ console.log(`Took ${side === "mine" ? "this line's" : "the other"} side of ${plural(settled.length, "file")}:`);
476
+ for (const file of settled)
477
+ console.log(` ${file}`);
478
+ console.log(dim("Save when the rest are settled."));
479
+ return 0;
480
+ }
481
+ const other = parsed.positional[0];
482
+ if (!other) {
483
+ console.error(red("Merge what? Name a line or a save: cbx merge feature"));
484
+ return 1;
485
+ }
486
+ const outcome = await (0, history_js_1.merge)(repository, other, { message: text(parsed, "m", "message") });
487
+ const touched = (outcome.kind === "up-to-date" ? 0 : outcome.written.length + outcome.removed.length);
488
+ if (outcome.kind === "up-to-date") {
489
+ console.log(`${other} is already part of this line.`);
490
+ }
491
+ else if (outcome.kind === "fast-forward") {
492
+ console.log(`Moved to ${accent(short(outcome.save.id))}; ${plural(touched, "file")} changed. Nothing needed merging.`);
493
+ }
494
+ else if (outcome.kind === "merged") {
495
+ console.log(`Merged ${accent(other)} as ${accent(short(outcome.save.id))}; ${plural(touched, "file")} changed.`);
496
+ }
497
+ else {
498
+ console.log(`Merged what could be merged; ${red(plural(outcome.state.conflicts.length, "conflict"))} left:`);
499
+ for (const conflict of outcome.state.conflicts)
500
+ console.log(` ${red("conflict")} ${conflict.path}`);
501
+ console.log(dim("Fix them, then cbx save. Or take a side: cbx merge --theirs [--path file]. Or cbx merge --cancel."));
502
+ return 1;
503
+ }
504
+ return 0;
505
+ });
506
+ }
@@ -27,8 +27,8 @@ const promises_1 = require("node:fs/promises");
27
27
  const node_os_1 = __importDefault(require("node:os"));
28
28
  const node_path_1 = __importDefault(require("node:path"));
29
29
  const node_process_1 = __importDefault(require("node:process"));
30
- const download_js_1 = require("../../desktop-app/src/main/download.js");
31
- const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
30
+ const download_js_1 = require("../../cbx/src/core/download.js");
31
+ const worktree_js_1 = require("../../cbx/src/core/worktree.js");
32
32
  const api_js_1 = require("./api.js");
33
33
  const config_js_1 = require("./config.js");
34
34
  /** What the protocol calls itself. Both clients accept this revision. */
@@ -29,8 +29,8 @@ const node_process_1 = __importDefault(require("node:process"));
29
29
  const api_js_1 = require("./api.js");
30
30
  const config_js_1 = require("./config.js");
31
31
  const project_commands_js_1 = require("./project_commands.js");
32
- const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
33
- const rules_js_1 = require("../../desktop-app/src/main/rules.js");
32
+ const worktree_js_1 = require("../../cbx/src/core/worktree.js");
33
+ const rules_js_1 = require("../../cbx/src/core/rules.js");
34
34
  const colour = node_process_1.default.stdout.isTTY && !node_process_1.default.env.NO_COLOR;
35
35
  const dim = (text) => (colour ? `[2m${text}[0m` : text);
36
36
  const bold = (text) => (colour ? `[1m${text}[0m` : text);
@@ -19,6 +19,7 @@ exports.nearestCommand = nearestCommand;
19
19
  exports.GROUP_ORDER = [
20
20
  "Getting started",
21
21
  "Working with a folder",
22
+ "History on this machine",
22
23
  "Your projects",
23
24
  "People",
24
25
  "When somebody saved first",
@@ -34,8 +34,8 @@ const promises_1 = require("node:fs/promises");
34
34
  const node_os_1 = require("node:os");
35
35
  const node_os_2 = __importDefault(require("node:os"));
36
36
  const node_path_1 = __importDefault(require("node:path"));
37
- const download_js_1 = require("../../desktop-app/src/main/download.js");
38
- const identify_js_1 = require("../../desktop-app/src/main/identify.js");
37
+ const download_js_1 = require("../../cbx/src/core/download.js");
38
+ const identify_js_1 = require("../../cbx/src/core/identify.js");
39
39
  const config_js_1 = require("./config.js");
40
40
  const progress_js_1 = require("./progress.js");
41
41
  /** How often to tell the service the machine is still working. */
@@ -24,7 +24,7 @@ exports.trackFor = trackFor;
24
24
  exports.commandTracks = commandTracks;
25
25
  exports.commandTrack = commandTrack;
26
26
  const node_process_1 = __importDefault(require("node:process"));
27
- const tracks_js_1 = require("../../desktop-app/src/main/tracks.js");
27
+ const tracks_js_1 = require("../../cbx/src/core/tracks.js");
28
28
  const config_js_1 = require("./config.js");
29
29
  const project_commands_js_1 = require("./project_commands.js");
30
30
  const dim = (value) => `${value}`;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.31.13",
3
+ "version": "0.32.1",
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
- "license": "SEE LICENSE IN LICENSE.txt",
5
+ "license": "Apache-2.0",
6
6
  "homepage": "https://coderook.com",
7
7
  "bugs": {
8
- "url": "https://coderook.com/contact"
8
+ "url": "https://github.com/AlexC1991/cbx/issues"
9
9
  },
10
10
  "keywords": [
11
11
  "coderook",
@@ -30,10 +30,11 @@
30
30
  "files": [
31
31
  ".claude-plugin",
32
32
  "dist",
33
- "skills"
33
+ "skills",
34
+ "NOTICE"
34
35
  ],
35
36
  "scripts": {
36
- "build": "tsc -p tsconfig.json",
37
+ "build": "tsc -p tsconfig.json && node scripts/copy-cbx-licence.mjs",
37
38
  "check": "tsc -p tsconfig.json --noEmit",
38
39
  "test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
39
40
  "start": "node dist/cli/src/cli.js",
@@ -56,5 +57,10 @@
56
57
  "devDependencies": {
57
58
  "@types/node": "24.10.1",
58
59
  "typescript": "5.9.3"
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/AlexC1991/cbx.git",
64
+ "directory": "cli"
59
65
  }
60
66
  }
@@ -81,6 +81,30 @@ cbx submit --track spike -m "…"
81
81
  Switching says where the next save goes and nothing else — no files move.
82
82
  Run `cbx get` afterwards to bring that line's files in.
83
83
 
84
+ ## A history on this machine
85
+
86
+ A folder with a `.cbx/` folder in it keeps its own history, and there
87
+ `status`, `diff`, `log`, `switch`, `merge`, `push` and `pull` work on that
88
+ history instead of the account. Check with `cbx status`: it names the line and
89
+ the last local save.
90
+
91
+ ```bash
92
+ cbx init # start one (only when asked to)
93
+ cbx save -m "…" # record the folder as it is
94
+ cbx log # saves on this line
95
+ cbx switch -c spike # start a line here; no files change
96
+ cbx merge spike # bring that line's work in
97
+ cbx restore path --from main~1 # put a file back from an earlier save
98
+ cbx push # send new saves to CodeRook
99
+ cbx pull # bring CodeRook's saves in, merging if both moved
100
+ ```
101
+
102
+ A merge that stops on conflicts leaves markers in the files and waits. Show
103
+ the person the conflicts (`cbx status`) and let them decide; `cbx save`
104
+ finishes the merge once the markers are gone. Do not pass `--force` to
105
+ `restore`, `switch`, `save` or `pull` unless the person has said to throw away
106
+ the changes it names, and do not run `cbx merge --cancel` unless they asked.
107
+
84
108
  ## Versions, and going back
85
109
 
86
110
  Every save is a commit. A commit becomes a *version* — the thing the public