@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,522 @@
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.UnsavedChanges = exports.UnresolvedConflicts = exports.Repository = void 0;
7
+ exports.compareTrees = compareTrees;
8
+ exports.byPath = byPath;
9
+ exports.changeCount = changeCount;
10
+ exports.pathFilter = pathFilter;
11
+ exports.headTree = headTree;
12
+ exports.init = init;
13
+ exports.save = save;
14
+ exports.log = log;
15
+ exports.status = status;
16
+ exports.diff = diff;
17
+ exports.lineDiff = lineDiff;
18
+ exports.inTheWay = inTheWay;
19
+ exports.hiddenInTheWay = hiddenInTheWay;
20
+ exports.restore = restore;
21
+ exports.switchLine = switchLine;
22
+ exports.merge = merge;
23
+ exports.cancelMerge = cancelMerge;
24
+ exports.resolveConflicts = resolveConflicts;
25
+ /**
26
+ * What `cbx init`, `save`, `log`, `status`, `diff`, `restore` and `switch` do,
27
+ * without the printing. Everything here works with no network at all.
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 merge_diff_js_1 = require("../shared/merge_diff.js");
33
+ const repository_js_1 = require("./repository.js");
34
+ const snapshot_js_1 = require("./snapshot.js");
35
+ const errors_js_1 = require("./errors.js");
36
+ const merge_js_1 = require("./merge.js");
37
+ var repository_js_2 = require("./repository.js");
38
+ Object.defineProperty(exports, "Repository", { enumerable: true, get: function () { return repository_js_2.Repository; } });
39
+ var errors_js_2 = require("./errors.js");
40
+ Object.defineProperty(exports, "UnresolvedConflicts", { enumerable: true, get: function () { return errors_js_2.UnresolvedConflicts; } });
41
+ Object.defineProperty(exports, "UnsavedChanges", { enumerable: true, get: function () { return errors_js_2.UnsavedChanges; } });
42
+ const EMPTY_TREE = { format: "cbx-tree", version: 1, files: [] };
43
+ /** Past this a file is described, not diffed line by line. */
44
+ const DIFF_BYTE_LIMIT = 4 * 1024 * 1024;
45
+ function compareTrees(before, after) {
46
+ const changes = { added: [], modified: [], deleted: [] };
47
+ for (const [file, entry] of after) {
48
+ const was = before.get(file);
49
+ if (!was)
50
+ changes.added.push(file);
51
+ else if (!(0, snapshot_js_1.sameFile)(was, entry))
52
+ changes.modified.push(file);
53
+ }
54
+ for (const file of before.keys())
55
+ if (!after.has(file))
56
+ changes.deleted.push(file);
57
+ for (const list of Object.values(changes))
58
+ list.sort();
59
+ return changes;
60
+ }
61
+ function byPath(tree) {
62
+ return new Map(tree.files.map((file) => [file.path, file]));
63
+ }
64
+ function changeCount(changes) {
65
+ return changes.added.length + changes.modified.length + changes.deleted.length;
66
+ }
67
+ /**
68
+ * A filter for paths the user named, relative to the history's root.
69
+ *
70
+ * A name matches itself and everything under it, so naming a folder names
71
+ * its contents. `.` or the root itself names everything.
72
+ */
73
+ function pathFilter(repository, names, cwd = process.cwd()) {
74
+ if (!names.length)
75
+ return undefined;
76
+ const wanted = names.map((name) => {
77
+ const relative = node_path_1.default
78
+ .relative(repository.root, node_path_1.default.resolve(cwd, name))
79
+ .split(node_path_1.default.sep)
80
+ .join("/");
81
+ if (relative.startsWith("..") || node_path_1.default.isAbsolute(relative)) {
82
+ throw new Error(`${name} is outside this project.`);
83
+ }
84
+ return relative;
85
+ });
86
+ if (wanted.some((one) => one === ""))
87
+ return undefined;
88
+ return (file) => wanted.some((one) => file === one || file.startsWith(`${one}/`));
89
+ }
90
+ async function headTree(repository) {
91
+ const head = await repository.head();
92
+ if (!head)
93
+ return { save: null, tree: EMPTY_TREE };
94
+ const save = await repository.readSave(head);
95
+ return { save, tree: await repository.readTree(save.tree) };
96
+ }
97
+ /* ---- init ------------------------------------------------------------ */
98
+ async function init(root, line = repository_js_1.DEFAULT_LINE) {
99
+ return repository_js_1.Repository.init(root, line);
100
+ }
101
+ /**
102
+ * Record the folder as it is now, on the current line.
103
+ *
104
+ * A save identical to the last one is refused rather than made, because a
105
+ * history of saves that each say nothing happened is noise.
106
+ */
107
+ async function save(repository, options) {
108
+ return repository.locked(async () => {
109
+ /*
110
+ A merge waiting on conflicts is finished by saving: the save gets both
111
+ parents, may say nothing new (keeping one side whole is a decision), and
112
+ takes the merge's own message when given none.
113
+ */
114
+ const merging = await (0, merge_js_1.readMergeState)(repository);
115
+ const message = options.message.trim() || merging?.message || "";
116
+ if (!message)
117
+ throw new Error("A save needs a message saying what changed.");
118
+ if (merging && !options.force) {
119
+ const left = await (0, merge_js_1.unresolved)(repository, merging);
120
+ if (left.length)
121
+ throw new errors_js_1.UnresolvedConflicts(left);
122
+ }
123
+ const { save: parent, tree: before } = await headTree(repository);
124
+ const reading = await (0, snapshot_js_1.readFolder)(repository, {
125
+ store: true,
126
+ previous: before,
127
+ progress: options.progress,
128
+ });
129
+ const tree = await repository.writeTree([...reading.files.values()]);
130
+ await repository.writeIndex(reading.index);
131
+ if (!merging && parent && parent.tree === tree) {
132
+ return { saved: false, reason: "unchanged" };
133
+ }
134
+ const line = await repository.currentLine();
135
+ const parents = parent ? [parent.id] : [];
136
+ if (merging)
137
+ parents.push(merging.theirs);
138
+ const made = await repository.writeSave({
139
+ format: "cbx-save",
140
+ version: 1,
141
+ tree,
142
+ parents,
143
+ line,
144
+ message,
145
+ author: options.author ?? (await repository.author()),
146
+ created: (options.now ?? new Date()).toISOString(),
147
+ });
148
+ await repository.setLineTip(line, made.id);
149
+ if (merging)
150
+ await (0, merge_js_1.clearMergeState)(repository);
151
+ let bytes = 0;
152
+ for (const file of reading.files.values())
153
+ bytes += file.size;
154
+ return {
155
+ saved: true,
156
+ save: made,
157
+ changes: compareTrees(byPath(before), reading.files),
158
+ files: reading.files.size,
159
+ bytes,
160
+ };
161
+ });
162
+ }
163
+ /* ---- log ------------------------------------------------------------- */
164
+ /** Saves newest first, following each save's first parent. */
165
+ async function log(repository, options = {}) {
166
+ const limit = options.limit ?? Infinity;
167
+ const found = [];
168
+ let at;
169
+ if (options.from) {
170
+ at = await repository.resolve(options.from);
171
+ }
172
+ else {
173
+ const head = await repository.head();
174
+ at = head ? await repository.readSave(head) : null;
175
+ }
176
+ while (at && found.length < limit) {
177
+ found.push(at);
178
+ const parent = at.parents[0];
179
+ at = parent ? await repository.readSave(parent) : null;
180
+ }
181
+ return found;
182
+ }
183
+ async function status(repository) {
184
+ const { save: head, tree } = await headTree(repository);
185
+ const reading = await (0, snapshot_js_1.readFolder)(repository, { store: false, previous: tree });
186
+ /*
187
+ Kept, so the next status does not hash the same files again. The index
188
+ is only a cache: an entry this writes without objects makes a save read
189
+ that file once more, never trust something it has not stored.
190
+ */
191
+ await repository.writeIndex(reading.index);
192
+ const merging = await (0, merge_js_1.readMergeState)(repository);
193
+ return {
194
+ line: await repository.currentLine(),
195
+ head,
196
+ changes: compareTrees(byPath(tree), reading.files),
197
+ ...(merging
198
+ ? {
199
+ merging: {
200
+ theirs: merging.theirs,
201
+ conflicts: merging.conflicts,
202
+ unresolved: await (0, merge_js_1.unresolved)(repository, merging),
203
+ },
204
+ }
205
+ : {}),
206
+ };
207
+ }
208
+ function looksBinary(bytes) {
209
+ return bytes.subarray(0, 8192).includes(0);
210
+ }
211
+ /**
212
+ * What changed between two states of the project.
213
+ *
214
+ * `from` defaults to the last save and `to` to the folder as it is, so with
215
+ * neither it answers "what have I changed since I last saved".
216
+ */
217
+ async function diff(repository, options = {}) {
218
+ try {
219
+ return await diffBetween(repository, options);
220
+ }
221
+ finally {
222
+ await repository.objects.release();
223
+ }
224
+ }
225
+ async function diffBetween(repository, options) {
226
+ const beforeTree = options.from
227
+ ? await repository.readTree((await repository.resolve(options.from)).tree)
228
+ : (await headTree(repository)).tree;
229
+ const before = byPath(beforeTree);
230
+ let after;
231
+ let afterIsFolder = false;
232
+ if (options.to) {
233
+ after = byPath(await repository.readTree((await repository.resolve(options.to)).tree));
234
+ }
235
+ else {
236
+ after = (await (0, snapshot_js_1.readFolder)(repository, { store: false, previous: beforeTree })).files;
237
+ afterIsFolder = true;
238
+ }
239
+ const bytesOf = async (entry, fromFolder) => fromFolder
240
+ ? (0, promises_1.readFile)(node_path_1.default.join(repository.root, entry.path))
241
+ : (0, snapshot_js_1.contentsOf)(repository, entry);
242
+ const changes = compareTrees(before, after);
243
+ const results = [];
244
+ const kinds = [
245
+ ["added", changes.added],
246
+ ["modified", changes.modified],
247
+ ["deleted", changes.deleted],
248
+ ];
249
+ const ordered = kinds
250
+ .flatMap(([kind, files]) => files.map((file) => ({ kind, file })))
251
+ .filter(({ file }) => !options.only || options.only(file))
252
+ .sort((left, right) => (left.file < right.file ? -1 : left.file > right.file ? 1 : 0));
253
+ for (const { kind, file } of ordered) {
254
+ const was = before.get(file);
255
+ const now = after.get(file);
256
+ const result = { path: file, kind };
257
+ if (was && now && was.sha256 === now.sha256) {
258
+ result.executableChanged = true;
259
+ result.chunks = [];
260
+ results.push(result);
261
+ continue;
262
+ }
263
+ const size = Math.max(was?.size ?? 0, now?.size ?? 0);
264
+ if (size > DIFF_BYTE_LIMIT) {
265
+ result.summary = `${describeSize(was?.size)} → ${describeSize(now?.size)}, too large to show line by line`;
266
+ results.push(result);
267
+ continue;
268
+ }
269
+ const left = was ? await bytesOf(was, false) : Buffer.alloc(0);
270
+ const right = now ? await bytesOf(now, afterIsFolder) : Buffer.alloc(0);
271
+ if (looksBinary(left) || looksBinary(right)) {
272
+ result.summary = `binary, ${describeSize(was?.size)} → ${describeSize(now?.size)}`;
273
+ results.push(result);
274
+ continue;
275
+ }
276
+ if (was && now)
277
+ result.executableChanged = was.executable !== now.executable;
278
+ const lines = lineDiff(left.toString("utf8"), right.toString("utf8"));
279
+ if (!lines) {
280
+ result.summary = "too many lines changed to show line by line";
281
+ results.push(result);
282
+ continue;
283
+ }
284
+ result.chunks = (0, merge_diff_js_1.foldUnchanged)(lines, options.context ?? 3);
285
+ results.push(result);
286
+ }
287
+ return results;
288
+ }
289
+ /*
290
+ The most comparison cells the line diff may fill.
291
+
292
+ The shared diff is an exact longest-common-subsequence table, which is the
293
+ right call for a merge conflict and quadratic in memory. Lines both sides
294
+ share at the start and end are taken off first, so an ordinary edit costs
295
+ almost nothing; past this, the change is described instead.
296
+ */
297
+ const DIFF_CELL_LIMIT = 4_000_000;
298
+ /** Lines in order, or null when the files differ too widely to compare. */
299
+ function lineDiff(before, after) {
300
+ const left = (0, merge_diff_js_1.linesOf)(before);
301
+ const right = (0, merge_diff_js_1.linesOf)(after);
302
+ let start = 0;
303
+ while (start < left.length && start < right.length && left[start] === right[start])
304
+ start += 1;
305
+ let end = 0;
306
+ while (end < left.length - start &&
307
+ end < right.length - start &&
308
+ left[left.length - 1 - end] === right[right.length - 1 - end]) {
309
+ end += 1;
310
+ }
311
+ const middleLeft = left.slice(start, left.length - end);
312
+ const middleRight = right.slice(start, right.length - end);
313
+ if ((middleLeft.length + 1) * (middleRight.length + 1) > DIFF_CELL_LIMIT)
314
+ return null;
315
+ const same = (a, b) => ({
316
+ side: "same",
317
+ targetLine: a + 1,
318
+ candidateLine: b + 1,
319
+ text: left[a],
320
+ });
321
+ const out = [];
322
+ for (let at = 0; at < start; at += 1)
323
+ out.push(same(at, at));
324
+ /* Every line newline-ended, so an empty line survives the round trip. */
325
+ const joined = (lines) => lines.map((line) => `${line}\n`).join("");
326
+ for (const line of (0, merge_diff_js_1.diffLines)(joined(middleLeft), joined(middleRight))) {
327
+ out.push({
328
+ ...line,
329
+ targetLine: line.targetLine === null ? null : line.targetLine + start,
330
+ candidateLine: line.candidateLine === null ? null : line.candidateLine + start,
331
+ });
332
+ }
333
+ for (let at = 0; at < end; at += 1) {
334
+ out.push(same(left.length - end + at, right.length - end + at));
335
+ }
336
+ return out;
337
+ }
338
+ function describeSize(size) {
339
+ if (size === undefined)
340
+ return "nothing";
341
+ if (size < 1024)
342
+ return `${size} B`;
343
+ if (size < 1024 * 1024)
344
+ return `${(size / 1024).toFixed(1)} KB`;
345
+ return `${(size / 1024 / 1024).toFixed(1)} MB`;
346
+ }
347
+ /* ---- restore and switch ---------------------------------------------- */
348
+ /**
349
+ * Unsaved changes among the files a checkout will rewrite or delete.
350
+ *
351
+ * A file that is missing from the folder is never in the way: putting it
352
+ * back loses nothing. Nor is one that already holds what the checkout would
353
+ * write. Anything else that differs from the last save would be lost.
354
+ */
355
+ function inTheWay(current, head, target, touched) {
356
+ const blocked = [];
357
+ for (const file of touched) {
358
+ const now = current.get(file);
359
+ if (!now)
360
+ continue;
361
+ if ((0, snapshot_js_1.sameFile)(now, head.get(file)) || (0, snapshot_js_1.sameFile)(now, target.get(file)))
362
+ continue;
363
+ blocked.push(file);
364
+ }
365
+ return blocked.sort();
366
+ }
367
+ /**
368
+ * Files the checkout would write over that the folder reading never saw.
369
+ *
370
+ * The reading lists only what the ignore rules let through, so a file the
371
+ * rules now leave out but a save still holds would be written over with no
372
+ * check at all. Those are looked at on disk directly.
373
+ */
374
+ async function hiddenInTheWay(repository, current, target, touched) {
375
+ const blocked = [];
376
+ const unseen = [...touched].filter((file) => target.has(file) && !current.has(file));
377
+ await (0, snapshot_js_1.inLanes)(unseen, 16, async (file) => {
378
+ let bytes;
379
+ try {
380
+ bytes = await (0, promises_1.readFile)(node_path_1.default.join(repository.root, file));
381
+ }
382
+ catch {
383
+ return;
384
+ }
385
+ if ((0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex") !== target.get(file).sha256) {
386
+ blocked.push(file);
387
+ }
388
+ });
389
+ return blocked;
390
+ }
391
+ /**
392
+ * Put files back the way a save had them. The line does not move: this
393
+ * changes the folder, and the next save records whatever it now holds.
394
+ */
395
+ async function restore(repository, options = {}) {
396
+ return repository.locked(async () => {
397
+ const source = await repository.resolve(options.from ?? "HEAD");
398
+ const target = await repository.readTree(source.tree);
399
+ const { tree: headSaved } = await headTree(repository);
400
+ const reading = await (0, snapshot_js_1.readFolder)(repository, { store: false, previous: headSaved });
401
+ const saved = byPath(headSaved);
402
+ const wanted = byPath(target);
403
+ /*
404
+ Every file either save holds, within the paths named. Files neither
405
+ holds are none of the history's business and are never touched.
406
+ */
407
+ const touched = new Set([...saved.keys(), ...wanted.keys()].filter((file) => !options.only || options.only(file)));
408
+ if (!options.force) {
409
+ const blocked = [
410
+ ...inTheWay(reading.files, saved, wanted, touched),
411
+ ...(await hiddenInTheWay(repository, reading.files, wanted, touched)),
412
+ ].sort();
413
+ if (blocked.length)
414
+ throw new errors_js_1.UnsavedChanges(blocked, "Restoring");
415
+ }
416
+ /*
417
+ Compared with what is on disk, not what was saved, so a file with
418
+ unsaved changes is rewritten and one that already matches is not.
419
+ */
420
+ const done = await (0, snapshot_js_1.checkout)(repository, reading.files, target, (file) => touched.has(file));
421
+ return { save: source, ...done };
422
+ });
423
+ }
424
+ /**
425
+ * Move the folder to another line of work, or start one with `create`.
426
+ *
427
+ * A new line begins at the current save and changes no files. Moving to an
428
+ * existing line rewrites only the files that differ between the two, and
429
+ * unsaved changes to those files stop it unless `force` is given.
430
+ */
431
+ async function switchLine(repository, line, options = {}) {
432
+ const problem = (0, repository_js_1.lineNameProblem)(line);
433
+ if (problem)
434
+ throw new Error(`Cannot name a line "${line}": ${problem}.`);
435
+ return repository.locked(async () => {
436
+ if (await (0, merge_js_1.readMergeState)(repository)) {
437
+ throw new Error("A merge is waiting on conflicts. Finish it with cbx save, or cancel it with cbx merge --cancel.");
438
+ }
439
+ const current = await repository.currentLine();
440
+ if (options.create) {
441
+ if (await repository.lineExists(line)) {
442
+ throw new Error(`There is already a line called ${line}. Leave out -c to switch to it.`);
443
+ }
444
+ const head = await repository.head();
445
+ if (head)
446
+ await repository.setLineTip(line, head);
447
+ await repository.setCurrentLine(line);
448
+ return {
449
+ line,
450
+ created: true,
451
+ save: head ? await repository.readSave(head) : null,
452
+ written: [],
453
+ removed: [],
454
+ };
455
+ }
456
+ if (line === current) {
457
+ throw new Error(`Already on ${line}.`);
458
+ }
459
+ const tip = await repository.lineTip(line);
460
+ if (!tip)
461
+ throw new Error(`No line called ${line}. Add -c to start one here.`);
462
+ const targetSave = await repository.readSave(tip);
463
+ const target = await repository.readTree(targetSave.tree);
464
+ const { tree: headSaved } = await headTree(repository);
465
+ const reading = await (0, snapshot_js_1.readFolder)(repository, { store: false, previous: headSaved });
466
+ const saved = byPath(headSaved);
467
+ const wanted = byPath(target);
468
+ /*
469
+ Only files the two lines disagree about. An edit to a file both lines
470
+ hold alike comes along to the new line untouched, as it does in git.
471
+ */
472
+ const touched = new Set([...saved.keys(), ...wanted.keys()].filter((file) => !(0, snapshot_js_1.sameFile)(saved.get(file), wanted.get(file))));
473
+ if (!options.force) {
474
+ const blocked = [
475
+ ...inTheWay(reading.files, saved, wanted, touched),
476
+ ...(await hiddenInTheWay(repository, reading.files, wanted, touched)),
477
+ ].sort();
478
+ if (blocked.length)
479
+ throw new errors_js_1.UnsavedChanges(blocked, `Switching to ${line}`);
480
+ }
481
+ const done = await (0, snapshot_js_1.checkout)(repository, reading.files, target, (file) => touched.has(file));
482
+ await repository.setCurrentLine(line);
483
+ return { line, created: false, save: targetSave, ...done };
484
+ });
485
+ }
486
+ /* ---- merge ------------------------------------------------------------ */
487
+ /**
488
+ * Merge another line, or any save, into the folder's line.
489
+ *
490
+ * Moves the line straight to it when this line has nothing of its own, and
491
+ * does nothing when it is already included. Otherwise see merge.ts.
492
+ */
493
+ async function merge(repository, other, options = {}) {
494
+ return repository.locked(async () => {
495
+ const theirs = await repository.resolve(other);
496
+ const line = await repository.currentLine();
497
+ return (0, merge_js_1.mergeInto)(repository, theirs.id, {
498
+ message: options.message?.trim() || `Merge ${other} into ${line}`,
499
+ author: options.author ?? (await repository.author()),
500
+ label: other,
501
+ now: options.now,
502
+ });
503
+ });
504
+ }
505
+ /** Call a waiting merge off and put back every file it changed. */
506
+ async function cancelMerge(repository) {
507
+ return repository.locked(async () => {
508
+ const state = await (0, merge_js_1.readMergeState)(repository);
509
+ if (!state)
510
+ throw new Error("No merge is waiting.");
511
+ const { tree } = await headTree(repository);
512
+ const touched = new Set(state.touched);
513
+ const reading = await (0, snapshot_js_1.readFolder)(repository, { store: false, previous: tree });
514
+ const done = await (0, snapshot_js_1.checkout)(repository, reading.files, tree, (file) => touched.has(file));
515
+ await (0, merge_js_1.clearMergeState)(repository);
516
+ return [...done.written, ...done.removed].sort();
517
+ });
518
+ }
519
+ /** Settle conflicts by taking this line's side or the other's, whole. */
520
+ async function resolveConflicts(repository, side, only) {
521
+ return repository.locked(() => (0, merge_js_1.takeSide)(repository, side, only));
522
+ }