@coderook/cli 0.31.13 → 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 (55) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +43 -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 +1 -1
  18. package/dist/cli/src/cli.js +164 -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 +2 -2
  25. package/dist/cli/src/registry.js +1 -0
  26. package/dist/cli/src/runner.js +2 -2
  27. package/dist/cli/src/track_commands.js +1 -1
  28. package/package.json +2 -2
  29. package/skills/coderook/SKILL.md +24 -0
  30. /package/dist/{desktop-app/src/main → cbx/src/core}/compress.js +0 -0
  31. /package/dist/{desktop-app/src/main → cbx/src/core}/credentials.js +0 -0
  32. /package/dist/{desktop-app/src/main → cbx/src/core}/detect.js +0 -0
  33. /package/dist/{desktop-app/src/main → cbx/src/core}/faults.js +0 -0
  34. /package/dist/{desktop-app/src/main → cbx/src/core}/identify.js +0 -0
  35. /package/dist/{desktop-app/src/main → cbx/src/core}/ignore_templates.js +0 -0
  36. /package/dist/{desktop-app/src/main → cbx/src/core}/licence_texts.js +0 -0
  37. /package/dist/{desktop-app/src/main → cbx/src/core}/licences.js +0 -0
  38. /package/dist/{desktop-app/src/main → cbx/src/core}/profile.js +0 -0
  39. /package/dist/{desktop-app/src/main → cbx/src/core}/publish_name.js +0 -0
  40. /package/dist/{desktop-app/src/main → cbx/src/core}/retry.js +0 -0
  41. /package/dist/{desktop-app/src/main → cbx/src/core}/secret_patterns.js +0 -0
  42. /package/dist/{desktop-app/src/main → cbx/src/core}/solid.js +0 -0
  43. /package/dist/{desktop-app/src/main → cbx/src/core}/staging.js +0 -0
  44. /package/dist/{desktop-app/src/main → cbx/src/core}/tracks.js +0 -0
  45. /package/dist/{desktop-app/src/main → cbx/src/core}/transfer_progress.js +0 -0
  46. /package/dist/{desktop-app/src/main → cbx/src/core}/upload.js +0 -0
  47. /package/dist/{desktop-app/src/main → cbx/src/core}/worktree.js +0 -0
  48. /package/dist/{desktop-app → cbx}/src/shared/chunking.js +0 -0
  49. /package/dist/{desktop-app → cbx}/src/shared/compression_policy.js +0 -0
  50. /package/dist/{desktop-app → cbx}/src/shared/delta.js +0 -0
  51. /package/dist/{desktop-app → cbx}/src/shared/format.js +0 -0
  52. /package/dist/{desktop-app → cbx}/src/shared/merge_diff.js +0 -0
  53. /package/dist/{desktop-app → cbx}/src/shared/publication.js +0 -0
  54. /package/dist/{desktop-app → cbx}/src/shared/telemetry.js +0 -0
  55. /package/dist/{desktop-app → cbx}/src/shared/types.js +0 -0
@@ -0,0 +1,277 @@
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.MergeInProgress = void 0;
7
+ exports.readMergeState = readMergeState;
8
+ exports.clearMergeState = clearMergeState;
9
+ exports.mergeBase = mergeBase;
10
+ exports.isAncestor = isAncestor;
11
+ exports.mergeInto = mergeInto;
12
+ exports.unresolved = unresolved;
13
+ exports.takeSide = takeSide;
14
+ /**
15
+ * Merging two lines of a local history.
16
+ *
17
+ * Every path is decided by comparing both sides with the save they last had
18
+ * in common. A file only one side changed is taken from that side, a file
19
+ * both changed the same way is taken once, and a text file both changed
20
+ * differently is merged line by line. Only what is left after that is a
21
+ * conflict: marked in the file for text, and for anything else left as this
22
+ * side had it and named, so a person decides.
23
+ *
24
+ * A merge with no conflicts is saved at once with both saves as parents. One
25
+ * with conflicts writes what it could into the folder and waits in
26
+ * `.cbx/merge.json` until the person saves (which makes the merge save) or
27
+ * cancels (which puts the folder back).
28
+ */
29
+ const promises_1 = require("node:fs/promises");
30
+ const node_path_1 = __importDefault(require("node:path"));
31
+ const merge_diff_js_1 = require("../shared/merge_diff.js");
32
+ const snapshot_js_1 = require("./snapshot.js");
33
+ const merge_text_js_1 = require("./merge_text.js");
34
+ const errors_js_1 = require("./errors.js");
35
+ /** Past this a file is not merged line by line. */
36
+ const TEXT_LIMIT = 4 * 1024 * 1024;
37
+ const MERGE_FILE = "merge.json";
38
+ class MergeInProgress extends Error {
39
+ }
40
+ exports.MergeInProgress = MergeInProgress;
41
+ async function readMergeState(repository) {
42
+ try {
43
+ return JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(repository.directory, MERGE_FILE), "utf8"));
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ async function writeMergeState(repository, state) {
50
+ const target = node_path_1.default.join(repository.directory, MERGE_FILE);
51
+ if (!state) {
52
+ await (0, promises_1.rm)(target, { force: true });
53
+ return;
54
+ }
55
+ await (0, promises_1.writeFile)(`${target}.partial`, JSON.stringify(state, null, 2));
56
+ await (0, promises_1.rename)(`${target}.partial`, target);
57
+ }
58
+ async function clearMergeState(repository) {
59
+ await writeMergeState(repository, null);
60
+ }
61
+ /** Every ancestor of `from`, itself included, through all parents. */
62
+ async function ancestors(repository, from) {
63
+ const seen = new Set();
64
+ const queue = [from];
65
+ while (queue.length) {
66
+ const id = queue.shift();
67
+ if (seen.has(id))
68
+ continue;
69
+ seen.add(id);
70
+ queue.push(...(await repository.readSave(id)).parents);
71
+ }
72
+ return seen;
73
+ }
74
+ /** The nearest save both descend from, or null for unrelated histories. */
75
+ async function mergeBase(repository, left, right) {
76
+ const mine = await ancestors(repository, left);
77
+ const seen = new Set();
78
+ const queue = [right];
79
+ while (queue.length) {
80
+ const id = queue.shift();
81
+ if (mine.has(id))
82
+ return id;
83
+ if (seen.has(id))
84
+ continue;
85
+ seen.add(id);
86
+ queue.push(...(await repository.readSave(id)).parents);
87
+ }
88
+ return null;
89
+ }
90
+ async function isAncestor(repository, ancestor, of) {
91
+ return (await ancestors(repository, of)).has(ancestor);
92
+ }
93
+ const byPath = (tree) => new Map(tree.files.map((file) => [file.path, file]));
94
+ function looksBinary(bytes) {
95
+ return bytes.subarray(0, 8192).includes(0);
96
+ }
97
+ /**
98
+ * Merge the save `theirs` into the folder's line. Assumes the lock is held;
99
+ * `merge` in history.ts is the public way in.
100
+ */
101
+ async function mergeInto(repository, theirs, options) {
102
+ if (await readMergeState(repository)) {
103
+ throw new MergeInProgress("A merge is already waiting on conflicts. Resolve them and save, or cancel it with cbx merge --cancel.");
104
+ }
105
+ const line = await repository.currentLine();
106
+ const ours = await repository.head();
107
+ const theirSave = await repository.readSave(theirs);
108
+ const theirTree = await repository.readTree(theirSave.tree);
109
+ const oursTree = ours
110
+ ? await repository.readTree((await repository.readSave(ours)).tree)
111
+ : { format: "cbx-tree", version: 1, files: [] };
112
+ const mine = byPath(oursTree);
113
+ const other = byPath(theirTree);
114
+ if (ours && (ours === theirs || (await isAncestor(repository, theirs, ours)))) {
115
+ return { kind: "up-to-date" };
116
+ }
117
+ const reading = await (0, snapshot_js_1.readFolder)(repository, { store: false, previous: oursTree });
118
+ /* Unsaved work is never overwritten; only the files a merge changes are checked. */
119
+ const guard = (touched) => {
120
+ const blocked = [];
121
+ for (const file of touched) {
122
+ const now = reading.files.get(file);
123
+ if (!now) {
124
+ if (mine.has(file))
125
+ blocked.push(file);
126
+ continue;
127
+ }
128
+ if (!(0, snapshot_js_1.sameFile)(now, mine.get(file)))
129
+ blocked.push(file);
130
+ }
131
+ if (blocked.length) {
132
+ throw new errors_js_1.UnsavedChanges(blocked.sort(), "Merging", "Save them or put them back first, then merge again.");
133
+ }
134
+ };
135
+ if (!ours || (await isAncestor(repository, ours, theirs))) {
136
+ const touched = new Set([...mine.keys(), ...other.keys()].filter((file) => !(0, snapshot_js_1.sameFile)(mine.get(file), other.get(file))));
137
+ guard(touched);
138
+ const done = await (0, snapshot_js_1.checkout)(repository, reading.files, theirTree, (file) => touched.has(file));
139
+ await repository.setLineTip(line, theirs);
140
+ return { kind: "fast-forward", save: theirSave, ...done };
141
+ }
142
+ const baseId = await mergeBase(repository, ours, theirs);
143
+ const base = baseId
144
+ ? byPath(await repository.readTree((await repository.readSave(baseId)).tree))
145
+ : new Map();
146
+ const result = new Map();
147
+ const conflicts = [];
148
+ const labels = { ours: `${line} (this history)`, theirs: options.label };
149
+ const paths = new Set([...base.keys(), ...mine.keys(), ...other.keys()]);
150
+ for (const file of [...paths].sort()) {
151
+ const b = base.get(file);
152
+ const o = mine.get(file);
153
+ const t = other.get(file);
154
+ const settled = (entry) => {
155
+ if (entry)
156
+ result.set(file, entry);
157
+ };
158
+ if ((0, snapshot_js_1.sameFile)(o, t) || (!o && !t)) {
159
+ settled(o);
160
+ continue;
161
+ }
162
+ if ((0, snapshot_js_1.sameFile)(b, o) || (!b && !o)) {
163
+ settled(t);
164
+ continue;
165
+ }
166
+ if ((0, snapshot_js_1.sameFile)(b, t) || (!b && !t)) {
167
+ settled(o);
168
+ continue;
169
+ }
170
+ /* Only the execute bit differs on one side: take the content once, and that bit. */
171
+ if (o && t && o.sha256 === t.sha256) {
172
+ settled({ ...o, executable: b && b.executable !== o.executable ? o.executable : t.executable });
173
+ continue;
174
+ }
175
+ if (!o || !t) {
176
+ /* Changed on one side, deleted on the other: keep the changed file and ask. */
177
+ settled(o ?? t);
178
+ conflicts.push({ path: file, kind: o ? "deleted-there" : "deleted-here" });
179
+ continue;
180
+ }
181
+ const tooBig = Math.max(o.size, t.size, b?.size ?? 0) > TEXT_LIMIT;
182
+ const ourBytes = tooBig ? null : await (0, snapshot_js_1.contentsOf)(repository, o);
183
+ const theirBytes = tooBig ? null : await (0, snapshot_js_1.contentsOf)(repository, t);
184
+ const baseBytes = tooBig || !b ? Buffer.alloc(0) : await (0, snapshot_js_1.contentsOf)(repository, b);
185
+ if (!ourBytes || !theirBytes || looksBinary(ourBytes) || looksBinary(theirBytes) || looksBinary(baseBytes)) {
186
+ settled(o);
187
+ conflicts.push({ path: file, kind: "binary" });
188
+ continue;
189
+ }
190
+ const merged = (0, merge_text_js_1.mergeText)(baseBytes.toString("utf8"), ourBytes.toString("utf8"), theirBytes.toString("utf8"), labels);
191
+ const text = merged?.text ?? (`<<<<<<< ${labels.ours}\n${ourBytes.toString("utf8")}\n=======\n${theirBytes.toString("utf8")}\n>>>>>>> ${labels.theirs}\n`);
192
+ const bytes = Buffer.from(text, "utf8");
193
+ const digest = await repository.objects.put(bytes);
194
+ settled({
195
+ path: file,
196
+ size: bytes.length,
197
+ sha256: digest,
198
+ executable: o.executable,
199
+ chunks: bytes.length ? [digest] : [],
200
+ });
201
+ if (!merged || merged.conflicts)
202
+ conflicts.push({ path: file, kind: "text" });
203
+ }
204
+ await repository.objects.flush();
205
+ const mergedTree = { format: "cbx-tree", version: 1, files: [...result.values()] };
206
+ const touched = new Set([...mine.keys(), ...result.keys()].filter((file) => !(0, snapshot_js_1.sameFile)(mine.get(file), result.get(file))));
207
+ guard(touched);
208
+ const done = await (0, snapshot_js_1.checkout)(repository, reading.files, mergedTree, (file) => touched.has(file));
209
+ if (conflicts.length) {
210
+ const state = {
211
+ format: "cbx-merge",
212
+ version: 1,
213
+ theirs,
214
+ base: baseId,
215
+ message: options.message,
216
+ touched: [...touched].sort(),
217
+ conflicts,
218
+ };
219
+ await writeMergeState(repository, state);
220
+ return { kind: "conflicts", state, ...done };
221
+ }
222
+ const tree = await repository.writeTree(mergedTree.files);
223
+ const save = await repository.writeSave({
224
+ format: "cbx-save",
225
+ version: 1,
226
+ tree,
227
+ parents: [ours, theirs],
228
+ line,
229
+ message: options.message,
230
+ author: options.author,
231
+ created: (options.now ?? new Date()).toISOString(),
232
+ });
233
+ await repository.setLineTip(line, save.id);
234
+ /* The files just written are racy; the next status reads them again, once. */
235
+ return { kind: "merged", save, ...done };
236
+ }
237
+ /** Conflicted text files that still carry markers in the folder. */
238
+ async function unresolved(repository, state) {
239
+ const left = [];
240
+ for (const conflict of state.conflicts) {
241
+ if (conflict.kind !== "text")
242
+ continue;
243
+ try {
244
+ const text = await (0, promises_1.readFile)(node_path_1.default.join(repository.root, conflict.path), "utf8");
245
+ if ((0, merge_diff_js_1.hasConflictMarkers)(text))
246
+ left.push(conflict.path);
247
+ }
248
+ catch {
249
+ /* Deleted is a decision too. */
250
+ }
251
+ }
252
+ return left;
253
+ }
254
+ /** Settle conflicted files by taking one side whole. */
255
+ async function takeSide(repository, side, only) {
256
+ const state = await readMergeState(repository);
257
+ if (!state)
258
+ throw new Error("No merge is waiting on conflicts.");
259
+ const source = side === "mine" ? await repository.head() : state.theirs;
260
+ const tree = source
261
+ ? await repository.readTree((await repository.readSave(source)).tree)
262
+ : { format: "cbx-tree", version: 1, files: [] };
263
+ const wanted = new Set(state.conflicts.map((one) => one.path).filter((file) => !only || only(file)));
264
+ if (!wanted.size)
265
+ throw new Error("None of the conflicts is at that path.");
266
+ const reading = await (0, snapshot_js_1.readFolder)(repository, { store: false, previous: tree });
267
+ await (0, snapshot_js_1.checkout)(repository, reading.files, tree, (file) => wanted.has(file));
268
+ /* A side that does not hold the file settles it by removing it. */
269
+ for (const file of wanted) {
270
+ if (!tree.files.some((entry) => entry.path === file)) {
271
+ await (0, promises_1.rm)(node_path_1.default.join(repository.root, file), { force: true });
272
+ }
273
+ }
274
+ state.conflicts = state.conflicts.filter((one) => !wanted.has(one.path));
275
+ await writeMergeState(repository, state);
276
+ return [...wanted].sort();
277
+ }
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ /**
3
+ * A three-way merge of text: what two people did to the same starting point.
4
+ *
5
+ * The shared diff in `shared/merge_diff` compares two sides, which is all a
6
+ * conflict on CodeRook needs because the service already knows the base. A
7
+ * local merge has the base too, and using it is the whole difference between
8
+ * a merge and a comparison: a line only one side touched is taken without
9
+ * asking, and only a line both sides changed differently is a conflict.
10
+ *
11
+ * Lines keep their own endings, so a file with CRLF endings comes out with
12
+ * CRLF endings, and nothing is normalised that nobody asked to change.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.splitLines = splitLines;
16
+ exports.mergeText = mergeText;
17
+ /*
18
+ The most comparison cells one line match may fill. The match is an exact
19
+ longest-common-subsequence, quadratic in the lines that differ once the
20
+ shared start and end are set aside; past this the file is not merged line
21
+ by line and is reported as a conflict instead.
22
+ */
23
+ const CELL_LIMIT = 16_000_000;
24
+ /** Lines with their endings, so joining them gives back the exact text. */
25
+ function splitLines(text) {
26
+ if (!text)
27
+ return [];
28
+ return text.split(/(?<=\n)/);
29
+ }
30
+ /**
31
+ * For each line of `from`, the line of `to` it is matched with, or -1.
32
+ * Null when the files differ too widely to match affordably.
33
+ */
34
+ function match(from, to) {
35
+ const matched = new Int32Array(from.length).fill(-1);
36
+ let start = 0;
37
+ while (start < from.length && start < to.length && from[start] === to[start]) {
38
+ matched[start] = start;
39
+ start += 1;
40
+ }
41
+ let end = 0;
42
+ while (end < from.length - start &&
43
+ end < to.length - start &&
44
+ from[from.length - 1 - end] === to[to.length - 1 - end]) {
45
+ matched[from.length - 1 - end] = to.length - 1 - end;
46
+ end += 1;
47
+ }
48
+ const rows = from.length - start - end;
49
+ const columns = to.length - start - end;
50
+ if (!rows || !columns)
51
+ return matched;
52
+ if ((rows + 1) * (columns + 1) > CELL_LIMIT)
53
+ return null;
54
+ /* Suffix table: table[a][b] is the LCS length of from[a..] and to[b..]. */
55
+ const width = columns + 1;
56
+ const table = new Uint32Array((rows + 1) * width);
57
+ for (let a = rows - 1; a >= 0; a -= 1) {
58
+ for (let b = columns - 1; b >= 0; b -= 1) {
59
+ table[a * width + b] =
60
+ from[start + a] === to[start + b]
61
+ ? table[(a + 1) * width + b + 1] + 1
62
+ : Math.max(table[(a + 1) * width + b], table[a * width + b + 1]);
63
+ }
64
+ }
65
+ let a = 0;
66
+ let b = 0;
67
+ while (a < rows && b < columns) {
68
+ if (from[start + a] === to[start + b]) {
69
+ matched[start + a] = start + b;
70
+ a += 1;
71
+ b += 1;
72
+ }
73
+ else if (table[(a + 1) * width + b] >= table[a * width + b + 1]) {
74
+ a += 1;
75
+ }
76
+ else {
77
+ b += 1;
78
+ }
79
+ }
80
+ return matched;
81
+ }
82
+ const same = (left, right) => left.length === right.length && left.every((line, at) => line === right[at]);
83
+ /**
84
+ * Merge `ours` and `theirs`, both descended from `base`.
85
+ *
86
+ * Null when the files are too different to match line by line; the caller
87
+ * treats the whole file as one conflict.
88
+ */
89
+ function mergeText(base, ours, theirs, labels = { ours: "ours", theirs: "theirs" }) {
90
+ const o = splitLines(base);
91
+ const a = splitLines(ours);
92
+ const b = splitLines(theirs);
93
+ const toA = match(o, a);
94
+ const toB = match(o, b);
95
+ if (!toA || !toB)
96
+ return null;
97
+ const eol = ours.includes("\r\n") ? "\r\n" : "\n";
98
+ const out = [];
99
+ let conflicts = 0;
100
+ let i = 0;
101
+ let j = 0;
102
+ let k = 0;
103
+ const settle = (baseSlice, oursSlice, theirsSlice) => {
104
+ if (same(oursSlice, baseSlice))
105
+ out.push(...theirsSlice);
106
+ else if (same(theirsSlice, baseSlice))
107
+ out.push(...oursSlice);
108
+ else if (same(oursSlice, theirsSlice))
109
+ out.push(...oursSlice);
110
+ else {
111
+ conflicts += 1;
112
+ /* A side that ends without a newline would run into the marker. */
113
+ const ended = (lines) => {
114
+ const copy = [...lines];
115
+ const last = copy[copy.length - 1];
116
+ if (last !== undefined && !last.endsWith("\n"))
117
+ copy[copy.length - 1] = last + eol;
118
+ return copy;
119
+ };
120
+ out.push(`<<<<<<< ${labels.ours}${eol}`, ...ended(oursSlice), `=======${eol}`);
121
+ out.push(...ended(theirsSlice), `>>>>>>> ${labels.theirs}${eol}`);
122
+ }
123
+ };
124
+ for (;;) {
125
+ /* The next base line both sides kept, at or after where we are. */
126
+ let stable = i;
127
+ while (stable < o.length && (toA[stable] < j || toB[stable] < k))
128
+ stable += 1;
129
+ if (stable === o.length) {
130
+ settle(o.slice(i), a.slice(j), b.slice(k));
131
+ break;
132
+ }
133
+ const nextA = toA[stable];
134
+ const nextB = toB[stable];
135
+ if (stable === i && nextA === j && nextB === k) {
136
+ out.push(o[i]);
137
+ i += 1;
138
+ j += 1;
139
+ k += 1;
140
+ continue;
141
+ }
142
+ settle(o.slice(i, stable), a.slice(j, nextA), b.slice(k, nextB));
143
+ i = stable;
144
+ j = nextA;
145
+ k = nextB;
146
+ }
147
+ return { text: out.join(""), conflicts };
148
+ }