midsummer-sol 0.1.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 (5) hide show
  1. package/README.md +99 -0
  2. package/index.js +2035 -0
  3. package/package.json +39 -0
  4. package/sol-mcp.js +638 -0
  5. package/sol.js +3518 -0
package/sol.js ADDED
@@ -0,0 +1,3518 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // src/bin/sol.ts
5
+ import { execFileSync as execFileSync2 } from "child_process";
6
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readdirSync as readdirSync7, readFileSync as readFileSync9, rmSync, unlinkSync as unlinkSync6, watch, writeFileSync as writeFileSync9 } from "fs";
7
+ import { homedir, hostname, platform as platform2, tmpdir } from "os";
8
+ import { basename, dirname as dirname5, join as join9, resolve as resolve3, sep as sep3 } from "path";
9
+
10
+ // src/attest.ts
11
+ function canonicalOp(op) {
12
+ return JSON.stringify({
13
+ seq: op.seq,
14
+ type: op.type,
15
+ path: op.path,
16
+ rootAfter: op.rootAfter,
17
+ by: op.by ?? null,
18
+ message: op.message ?? null
19
+ });
20
+ }
21
+
22
+ // src/errors.ts
23
+ class CorruptRepoError extends Error {
24
+ constructor(message) {
25
+ super(`corrupt repo: ${message}`);
26
+ this.name = "CorruptRepoError";
27
+ }
28
+ }
29
+
30
+ class CorruptObjectError extends Error {
31
+ hash;
32
+ constructor(hash, message = "stored bytes do not hash to their content address") {
33
+ super(`corrupt object ${hash}: ${message}`);
34
+ this.hash = hash;
35
+ this.name = "CorruptObjectError";
36
+ }
37
+ }
38
+
39
+ // src/store.ts
40
+ import { createHash } from "node:crypto";
41
+ function hashString(s) {
42
+ return "h_" + createHash("sha256").update(s, "utf8").digest("hex");
43
+ }
44
+ function hashNode(node) {
45
+ let canon;
46
+ if (node.kind === "blob") {
47
+ canon = node.encoding ? `blob\x00${node.encoding}\x00${node.content}` : `blob\x00${node.content}`;
48
+ } else if (node.kind === "sealed")
49
+ canon = `sealed\x00${node.box}`;
50
+ else
51
+ canon = "tree\x00" + Object.keys(node.entries).sort().map((k) => {
52
+ const e = node.entries[k];
53
+ return e.mode === undefined ? `${k}\x00${e.kind}\x00${e.hash}` : `${k}\x00${e.kind}\x00${e.hash}\x00${e.mode}`;
54
+ }).join(`
55
+ `);
56
+ return hashString(canon);
57
+ }
58
+
59
+ class Store {
60
+ objects = new Map;
61
+ put(node) {
62
+ const h = hashNode(node);
63
+ if (!this.objects.has(h))
64
+ this.objects.set(h, node);
65
+ return h;
66
+ }
67
+ get(h) {
68
+ return this.objects.get(h);
69
+ }
70
+ getTree(h) {
71
+ const n = this.get(h);
72
+ if (!n || n.kind !== "tree")
73
+ throw new Error(`not a tree: ${h}`);
74
+ return n;
75
+ }
76
+ has(h) {
77
+ return this.get(h) !== undefined;
78
+ }
79
+ size() {
80
+ return this.objects.size;
81
+ }
82
+ }
83
+
84
+ // src/chain.ts
85
+ function hashEntry(prevHash, op) {
86
+ return hashString(canonicalOp(op) + "\x00" + (prevHash ?? ""));
87
+ }
88
+ function verifyChain(ops) {
89
+ let prev;
90
+ for (const op of ops) {
91
+ if (op.prevHash !== prev)
92
+ throw new CorruptRepoError(`op-log prevHash mismatch at seq ${op.seq}`);
93
+ if (op.entryHash !== hashEntry(prev, op))
94
+ throw new CorruptRepoError(`op-log chain broken at seq ${op.seq}`);
95
+ prev = op.entryHash;
96
+ }
97
+ return prev;
98
+ }
99
+
100
+ // src/tree.ts
101
+ var EMPTY_TREE = { kind: "tree", entries: {} };
102
+ function emptyRoot(store) {
103
+ return store.put(EMPTY_TREE);
104
+ }
105
+ function segments(path) {
106
+ return path.split("/").filter(Boolean);
107
+ }
108
+ function writeFile(store, root, path, content, opts = {}) {
109
+ const blob = opts.encoding ? { kind: "blob", content, encoding: opts.encoding } : { kind: "blob", content };
110
+ return writeInto(store, root, segments(path), store.put(blob), opts.mode);
111
+ }
112
+ function writeInto(store, treeHash, segs, blobHash, mode) {
113
+ const tree = store.getTree(treeHash);
114
+ const entries = { ...tree.entries };
115
+ const [head, ...rest] = segs;
116
+ if (rest.length === 0) {
117
+ entries[head] = mode === undefined ? { kind: "blob", hash: blobHash } : { kind: "blob", hash: blobHash, mode };
118
+ } else {
119
+ const child = entries[head];
120
+ const childHash = child?.kind === "tree" ? child.hash : emptyRoot(store);
121
+ entries[head] = { kind: "tree", hash: writeInto(store, childHash, rest, blobHash, mode) };
122
+ }
123
+ return store.put({ kind: "tree", entries });
124
+ }
125
+ function fileAt(store, root, path) {
126
+ const segs = segments(path);
127
+ let cur = root;
128
+ for (let i = 0;i < segs.length; i++) {
129
+ const entry = store.getTree(cur).entries[segs[i]];
130
+ if (!entry)
131
+ return;
132
+ if (i === segs.length - 1)
133
+ return entry.kind === "blob" ? store.get(entry.hash) : undefined;
134
+ if (entry.kind !== "tree")
135
+ return;
136
+ cur = entry.hash;
137
+ }
138
+ return;
139
+ }
140
+ function modeAt(store, root, path) {
141
+ const segs = segments(path);
142
+ let cur = root;
143
+ for (let i = 0;i < segs.length; i++) {
144
+ const entry = store.getTree(cur).entries[segs[i]];
145
+ if (!entry)
146
+ return;
147
+ if (i === segs.length - 1)
148
+ return entry.mode;
149
+ if (entry.kind !== "tree")
150
+ return;
151
+ cur = entry.hash;
152
+ }
153
+ return;
154
+ }
155
+ function readFile(store, root, path) {
156
+ const segs = segments(path);
157
+ let cur = root;
158
+ for (let i = 0;i < segs.length; i++) {
159
+ const entry = store.getTree(cur).entries[segs[i]];
160
+ if (!entry)
161
+ return;
162
+ if (i === segs.length - 1) {
163
+ return entry.kind === "blob" ? store.get(entry.hash).content : undefined;
164
+ }
165
+ if (entry.kind !== "tree")
166
+ return;
167
+ cur = entry.hash;
168
+ }
169
+ return;
170
+ }
171
+ function listAll(store, root, prefix = "") {
172
+ const tree = store.getTree(root);
173
+ const out = [];
174
+ for (const [name, entry] of Object.entries(tree.entries)) {
175
+ const p = prefix ? `${prefix}/${name}` : name;
176
+ if (entry.kind === "tree")
177
+ out.push(...listAll(store, entry.hash, p));
178
+ else
179
+ out.push(p);
180
+ }
181
+ return out.sort();
182
+ }
183
+ function entryKindAt(store, root, path) {
184
+ const segs = segments(path);
185
+ let cur = root;
186
+ for (let i = 0;i < segs.length; i++) {
187
+ const entry = store.getTree(cur).entries[segs[i]];
188
+ if (!entry)
189
+ return;
190
+ if (i === segs.length - 1)
191
+ return entry.kind;
192
+ if (entry.kind !== "tree")
193
+ return;
194
+ cur = entry.hash;
195
+ }
196
+ return;
197
+ }
198
+
199
+ // src/diff.ts
200
+ function diffTrees(store, aHead, bHead) {
201
+ const aPaths = new Set(listAll(store, aHead));
202
+ const bPaths = new Set(listAll(store, bHead));
203
+ const added = [];
204
+ const removed = [];
205
+ const modified = [];
206
+ for (const p of bPaths)
207
+ if (!aPaths.has(p))
208
+ added.push(p);
209
+ for (const p of aPaths)
210
+ if (!bPaths.has(p))
211
+ removed.push(p);
212
+ for (const p of aPaths) {
213
+ if (!bPaths.has(p))
214
+ continue;
215
+ const a = fileAt(store, aHead, p);
216
+ const b = fileAt(store, bHead, p);
217
+ if (!a || !b)
218
+ continue;
219
+ if (a.content === b.content && a.encoding === b.encoding)
220
+ continue;
221
+ const binary = a.encoding === "base64" || b.encoding === "base64";
222
+ const hunks = binary ? "" : lineHunks(a.content, b.content);
223
+ modified.push({ path: p, hunks });
224
+ }
225
+ added.sort();
226
+ removed.sort();
227
+ modified.sort((x, y) => x.path < y.path ? -1 : x.path > y.path ? 1 : 0);
228
+ return { added, removed, modified };
229
+ }
230
+ function splitLines(s) {
231
+ return s.split(`
232
+ `);
233
+ }
234
+ function lcsSteps(a, b) {
235
+ const n = a.length;
236
+ const m = b.length;
237
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
238
+ for (let i2 = n - 1;i2 >= 0; i2--) {
239
+ for (let j2 = m - 1;j2 >= 0; j2--) {
240
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
241
+ }
242
+ }
243
+ const steps = [];
244
+ let i = 0;
245
+ let j = 0;
246
+ while (i < n && j < m) {
247
+ if (a[i] === b[j]) {
248
+ steps.push({ tag: " ", line: a[i] });
249
+ i++;
250
+ j++;
251
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
252
+ steps.push({ tag: "-", line: a[i] });
253
+ i++;
254
+ } else {
255
+ steps.push({ tag: "+", line: b[j] });
256
+ j++;
257
+ }
258
+ }
259
+ while (i < n)
260
+ steps.push({ tag: "-", line: a[i++] });
261
+ while (j < m)
262
+ steps.push({ tag: "+", line: b[j++] });
263
+ return steps;
264
+ }
265
+ function lineHunks(aText, bText) {
266
+ const steps = lcsSteps(splitLines(aText), splitLines(bText));
267
+ const body = steps.map((s) => `${s.tag}${s.line}`).join(`
268
+ `);
269
+ return `@@ -1 +1 @@
270
+ ${body}`;
271
+ }
272
+
273
+ // src/file-store.ts
274
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
275
+ import { join } from "node:path";
276
+ import { gunzipSync, gzipSync } from "node:zlib";
277
+
278
+ // src/chain.ts
279
+ function hashEntry2(prevHash, op) {
280
+ return hashString(canonicalOp(op) + "\x00" + (prevHash ?? ""));
281
+ }
282
+ function chainOp(prevHash, op) {
283
+ return { ...op, prevHash, entryHash: hashEntry2(prevHash, op) };
284
+ }
285
+
286
+ // src/async-store.ts
287
+ async function getTree(store, hash) {
288
+ const node = await store.get(hash);
289
+ if (!node || node.kind !== "tree")
290
+ throw new Error(`not a tree: ${hash}`);
291
+ return node;
292
+ }
293
+
294
+ class MemoryAsyncStore {
295
+ nodes = new Map;
296
+ async put(node) {
297
+ const h = hashNode(node);
298
+ if (!this.nodes.has(h))
299
+ this.nodes.set(h, node);
300
+ return h;
301
+ }
302
+ async get(hash) {
303
+ return this.nodes.get(hash);
304
+ }
305
+ async has(hash) {
306
+ return this.nodes.has(hash);
307
+ }
308
+ get size() {
309
+ return this.nodes.size;
310
+ }
311
+ }
312
+
313
+ // src/async-tree.ts
314
+ function segments2(path) {
315
+ return path.split("/").filter(Boolean);
316
+ }
317
+ async function emptyRoot2(store) {
318
+ return store.put({ kind: "tree", entries: {} });
319
+ }
320
+ async function writeFile2(store, root, path, content, opts = {}) {
321
+ const blob = opts.encoding ? { kind: "blob", content, encoding: opts.encoding } : { kind: "blob", content };
322
+ const hash = await store.put(blob);
323
+ return writeInto2(store, root, segments2(path), hash, "blob", opts.mode);
324
+ }
325
+ async function writeSealed(store, root, path, box) {
326
+ const hash = await store.put({ kind: "sealed", box });
327
+ return writeInto2(store, root, segments2(path), hash, "sealed");
328
+ }
329
+ async function writeInto2(store, treeHash, segs, hash, leafKind, mode) {
330
+ const tree = await getTree(store, treeHash);
331
+ const entries = { ...tree.entries };
332
+ const [head, ...rest] = segs;
333
+ if (rest.length === 0) {
334
+ entries[head] = mode === undefined ? { kind: leafKind, hash } : { kind: leafKind, hash, mode };
335
+ } else {
336
+ const child = entries[head];
337
+ const childHash = child?.kind === "tree" ? child.hash : await emptyRoot2(store);
338
+ entries[head] = { kind: "tree", hash: await writeInto2(store, childHash, rest, hash, leafKind, mode) };
339
+ }
340
+ return store.put({ kind: "tree", entries });
341
+ }
342
+ async function applyBatch(store, root, changes) {
343
+ if (changes.size === 0)
344
+ return root;
345
+ const tree = await getTree(store, root);
346
+ const entries = { ...tree.entries };
347
+ const subdirs = new Map;
348
+ for (const [path, leaf] of changes) {
349
+ const i = path.indexOf("/");
350
+ if (i === -1) {
351
+ if (leaf === null)
352
+ delete entries[path];
353
+ else
354
+ entries[path] = leaf.mode === undefined ? { kind: leaf.kind, hash: leaf.hash } : { kind: leaf.kind, hash: leaf.hash, mode: leaf.mode };
355
+ } else {
356
+ const head = path.slice(0, i);
357
+ let m = subdirs.get(head);
358
+ if (!m) {
359
+ m = new Map;
360
+ subdirs.set(head, m);
361
+ }
362
+ m.set(path.slice(i + 1), leaf);
363
+ }
364
+ }
365
+ for (const [name, sub] of subdirs) {
366
+ const child = entries[name];
367
+ const childRoot = child && child.kind === "tree" ? child.hash : await emptyRoot2(store);
368
+ const built = await applyBatch(store, childRoot, sub);
369
+ const builtTree = await getTree(store, built);
370
+ if (Object.keys(builtTree.entries).length === 0)
371
+ delete entries[name];
372
+ else
373
+ entries[name] = { kind: "tree", hash: built };
374
+ }
375
+ return store.put({ kind: "tree", entries });
376
+ }
377
+ async function deleteFile(store, root, path) {
378
+ return deleteInto(store, root, segments2(path));
379
+ }
380
+ async function deleteInto(store, treeHash, segs) {
381
+ const tree = await getTree(store, treeHash);
382
+ const entries = { ...tree.entries };
383
+ const [head, ...rest] = segs;
384
+ if (!entries[head])
385
+ return treeHash;
386
+ if (rest.length === 0) {
387
+ delete entries[head];
388
+ } else if (entries[head].kind === "tree") {
389
+ entries[head] = { kind: "tree", hash: await deleteInto(store, entries[head].hash, rest) };
390
+ }
391
+ return store.put({ kind: "tree", entries });
392
+ }
393
+ async function nodeAt(store, root, path) {
394
+ const segs = segments2(path);
395
+ let cur = root;
396
+ for (let i = 0;i < segs.length; i++) {
397
+ const entry = (await getTree(store, cur)).entries[segs[i]];
398
+ if (!entry)
399
+ return;
400
+ if (i === segs.length - 1) {
401
+ if (entry.kind === "tree")
402
+ return;
403
+ const node = await store.get(entry.hash);
404
+ if (!node)
405
+ return;
406
+ if (node.kind === "blob")
407
+ return { kind: "blob", content: node.content };
408
+ if (node.kind === "sealed")
409
+ return { kind: "sealed", box: node.box };
410
+ return;
411
+ }
412
+ if (entry.kind !== "tree")
413
+ return;
414
+ cur = entry.hash;
415
+ }
416
+ return;
417
+ }
418
+ async function fileAt2(store, root, path) {
419
+ const segs = segments2(path);
420
+ let cur = root;
421
+ for (let i = 0;i < segs.length; i++) {
422
+ const entry = (await getTree(store, cur)).entries[segs[i]];
423
+ if (!entry)
424
+ return;
425
+ if (i === segs.length - 1) {
426
+ if (entry.kind !== "blob")
427
+ return;
428
+ const node = await store.get(entry.hash);
429
+ return node && node.kind === "blob" ? node : undefined;
430
+ }
431
+ if (entry.kind !== "tree")
432
+ return;
433
+ cur = entry.hash;
434
+ }
435
+ return;
436
+ }
437
+ async function modeAt2(store, root, path) {
438
+ const segs = segments2(path);
439
+ let cur = root;
440
+ for (let i = 0;i < segs.length; i++) {
441
+ const entry = (await getTree(store, cur)).entries[segs[i]];
442
+ if (!entry)
443
+ return;
444
+ if (i === segs.length - 1)
445
+ return entry.mode;
446
+ if (entry.kind !== "tree")
447
+ return;
448
+ cur = entry.hash;
449
+ }
450
+ return;
451
+ }
452
+ async function listAll2(store, root, prefix = "") {
453
+ const tree = await getTree(store, root);
454
+ const out = [];
455
+ for (const [name, entry] of Object.entries(tree.entries)) {
456
+ const p = prefix ? `${prefix}/${name}` : name;
457
+ if (entry.kind === "tree")
458
+ out.push(...await listAll2(store, entry.hash, p));
459
+ else
460
+ out.push(p);
461
+ }
462
+ return out.sort();
463
+ }
464
+
465
+ // src/types.ts
466
+ var SEALED = "<<sealed>>";
467
+
468
+ // src/async-repo.ts
469
+ function pageOps(ops, opts = {}) {
470
+ let out = opts.from !== undefined ? ops.filter((o) => o.seq >= opts.from) : ops.slice();
471
+ if (opts.limit !== undefined)
472
+ out = out.slice(0, opts.limit);
473
+ return out;
474
+ }
475
+ class AsyncRepo {
476
+ store;
477
+ log;
478
+ actor;
479
+ constructor(store, log, actor = "anon") {
480
+ this.store = store;
481
+ this.log = log;
482
+ this.actor = actor;
483
+ }
484
+ async currentRoot() {
485
+ const head = await this.log.head();
486
+ if (head === undefined) {
487
+ const seq = await this.log.seq();
488
+ if (seq > 0)
489
+ throw new CorruptRepoError(`op-log has ${seq} op(s) but no head pointer — head is lost`);
490
+ return emptyRoot2(this.store);
491
+ }
492
+ if (!await this.store.has(head)) {
493
+ throw new CorruptRepoError(`head ${head} references a node missing from the store — dangling head`);
494
+ }
495
+ return head;
496
+ }
497
+ async writeFile(path, content, at = Date.now(), by = this.actor) {
498
+ const rootAfter = await writeFile2(this.store, await this.currentRoot(), path, content);
499
+ await this.log.append({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by });
500
+ return rootAfter;
501
+ }
502
+ async applyBatch(changes) {
503
+ const root = await this.currentRoot();
504
+ const leaves = new Map;
505
+ for (const c of changes) {
506
+ if ("delete" in c) {
507
+ leaves.set(c.path, null);
508
+ continue;
509
+ }
510
+ const blob = c.encoding ? { kind: "blob", content: c.content, encoding: c.encoding } : { kind: "blob", content: c.content };
511
+ const hash = await this.store.put(blob);
512
+ leaves.set(c.path, { kind: "blob", hash, mode: c.mode });
513
+ }
514
+ return { root: await applyBatch(this.store, root, leaves), changed: changes.length };
515
+ }
516
+ async writeBytes(path, data, at = Date.now(), by = this.actor) {
517
+ const rootAfter = await writeFile2(this.store, await this.currentRoot(), path, Buffer.from(data).toString("base64"), { encoding: "base64" });
518
+ await this.log.append({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by });
519
+ return rootAfter;
520
+ }
521
+ async readBytes(path) {
522
+ const leaf = await this.read(path);
523
+ if (!leaf)
524
+ return;
525
+ if (leaf.kind === "sealed")
526
+ return SEALED;
527
+ const f = await fileAt2(this.store, await this.currentRoot(), path);
528
+ if (!f)
529
+ return;
530
+ return new Uint8Array(Buffer.from(f.content, f.encoding === "base64" ? "base64" : "utf8"));
531
+ }
532
+ async chmod(path, mode, at = Date.now()) {
533
+ const f = await fileAt2(this.store, await this.currentRoot(), path);
534
+ if (!f)
535
+ return this.currentRoot();
536
+ const rootAfter = await writeFile2(this.store, await this.currentRoot(), path, f.content, { encoding: f.encoding, mode });
537
+ await this.log.append({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by: this.actor });
538
+ return rootAfter;
539
+ }
540
+ async mode(path) {
541
+ return modeAt2(this.store, await this.currentRoot(), path);
542
+ }
543
+ async writeSealed(path, box, at = Date.now()) {
544
+ const rootAfter = await writeSealed(this.store, await this.currentRoot(), path, box);
545
+ await this.log.append({ seq: await this.log.seq() + 1, type: "seal", path, rootAfter, at, by: this.actor });
546
+ return rootAfter;
547
+ }
548
+ async read(path) {
549
+ return nodeAt(this.store, await this.currentRoot(), path);
550
+ }
551
+ async deleteFile(path, at = Date.now(), by = this.actor) {
552
+ const rootAfter = await deleteFile(this.store, await this.currentRoot(), path);
553
+ await this.log.append({ seq: await this.log.seq() + 1, type: "delete", path, rootAfter, at, by });
554
+ return rootAfter;
555
+ }
556
+ async readFile(path) {
557
+ const leaf = await this.read(path);
558
+ if (!leaf)
559
+ return;
560
+ return leaf.kind === "sealed" ? SEALED : leaf.content;
561
+ }
562
+ async list() {
563
+ return listAll2(this.store, await this.currentRoot());
564
+ }
565
+ async head() {
566
+ return this.currentRoot();
567
+ }
568
+ async history(opts) {
569
+ return this.log.history(opts);
570
+ }
571
+ }
572
+
573
+ // src/file-store.ts
574
+ function encodeObject(node) {
575
+ return gzipSync(JSON.stringify(node), { level: 1 });
576
+ }
577
+ function decodeObject(raw) {
578
+ const json = raw.length >= 2 && raw[0] === 31 && raw[1] === 139 ? gunzipSync(raw).toString("utf8") : raw.toString("utf8");
579
+ return JSON.parse(json);
580
+ }
581
+
582
+ class FileStore {
583
+ objects;
584
+ constructor(solDir) {
585
+ this.objects = join(solDir, "objects");
586
+ mkdirSync(this.objects, { recursive: true });
587
+ }
588
+ path(hash) {
589
+ return join(this.objects, hash);
590
+ }
591
+ async put(node) {
592
+ const h = hashNode(node);
593
+ const p = this.path(h);
594
+ if (!existsSync(p)) {
595
+ const tmp = `${p}.tmp`;
596
+ writeFileSync(tmp, encodeObject(node));
597
+ renameSync(tmp, p);
598
+ }
599
+ return h;
600
+ }
601
+ async get(hash) {
602
+ const p = this.path(hash);
603
+ if (!existsSync(p))
604
+ return;
605
+ const node = decodeObject(readFileSync(p));
606
+ if (hashNode(node) !== hash)
607
+ throw new CorruptObjectError(hash);
608
+ return node;
609
+ }
610
+ async has(hash) {
611
+ return existsSync(this.path(hash));
612
+ }
613
+ count() {
614
+ return existsSync(this.objects) ? readdirSync(this.objects).length : 0;
615
+ }
616
+ }
617
+
618
+ // src/crypto.ts
619
+ import { createCipheriv, createDecipheriv, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
620
+ var UNREADABLE = "<<unreadable>>";
621
+
622
+ class KeyRing {
623
+ keys = new Map;
624
+ ensure(actor) {
625
+ let k = this.keys.get(actor);
626
+ if (!k) {
627
+ k = randomBytes(32);
628
+ this.keys.set(actor, k);
629
+ }
630
+ return k;
631
+ }
632
+ key(actor) {
633
+ return this.keys.get(actor);
634
+ }
635
+ serialize() {
636
+ const out = {};
637
+ for (const [a, k] of this.keys)
638
+ out[a] = k.toString("base64");
639
+ return out;
640
+ }
641
+ load(data) {
642
+ for (const [a, k] of Object.entries(data))
643
+ this.keys.set(a, Buffer.from(k, "base64"));
644
+ }
645
+ }
646
+ var RECOVERY_INFO = Buffer.from("forge-vcs/recovery/v1\x00");
647
+
648
+ // src/crypto.ts
649
+ import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createPublicKey as createPublicKey2, diffieHellman as diffieHellman2, generateKeyPairSync as generateKeyPairSync2, hkdfSync as hkdfSync2, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
650
+ var UNREADABLE2 = "<<unreadable>>";
651
+
652
+ class KeyRing2 {
653
+ keys = new Map;
654
+ ensure(actor) {
655
+ let k = this.keys.get(actor);
656
+ if (!k) {
657
+ k = randomBytes2(32);
658
+ this.keys.set(actor, k);
659
+ }
660
+ return k;
661
+ }
662
+ key(actor) {
663
+ return this.keys.get(actor);
664
+ }
665
+ serialize() {
666
+ const out = {};
667
+ for (const [a, k] of this.keys)
668
+ out[a] = k.toString("base64");
669
+ return out;
670
+ }
671
+ load(data) {
672
+ for (const [a, k] of Object.entries(data))
673
+ this.keys.set(a, Buffer.from(k, "base64"));
674
+ }
675
+ }
676
+ function aeadSeal(key, plain) {
677
+ const iv = randomBytes2(12);
678
+ const c = createCipheriv2("aes-256-gcm", key, iv);
679
+ const ct = Buffer.concat([c.update(plain), c.final()]);
680
+ return { iv: iv.toString("base64"), ct: ct.toString("base64"), tag: c.getAuthTag().toString("base64") };
681
+ }
682
+ function aeadOpen(key, box) {
683
+ const d = createDecipheriv2("aes-256-gcm", key, Buffer.from(box.iv, "base64"));
684
+ d.setAuthTag(Buffer.from(box.tag, "base64"));
685
+ return Buffer.concat([d.update(Buffer.from(box.ct, "base64")), d.final()]);
686
+ }
687
+ function sealContent(ring, content, recipients, epoch = 1) {
688
+ const cek = randomBytes2(32);
689
+ const body = aeadSeal(cek, Buffer.from(content, "utf8"));
690
+ const lockboxes = {};
691
+ for (const r of recipients)
692
+ lockboxes[r] = aeadSeal(ring.ensure(r), cek);
693
+ return { body, lockboxes, epoch };
694
+ }
695
+ function openContent(ring, box, actor) {
696
+ const lb = box.lockboxes[actor];
697
+ const key = ring.key(actor);
698
+ if (!lb || !key)
699
+ return UNREADABLE2;
700
+ try {
701
+ const cek = aeadOpen(key, lb);
702
+ return aeadOpen(cek, box.body).toString("utf8");
703
+ } catch {
704
+ return UNREADABLE2;
705
+ }
706
+ }
707
+ var RECOVERY_INFO2 = Buffer.from("forge-vcs/recovery/v1\x00");
708
+
709
+ // src/sealed-client.ts
710
+ class SealedClient {
711
+ repo;
712
+ ring;
713
+ constructor(repo, ring) {
714
+ this.repo = repo;
715
+ this.ring = ring;
716
+ }
717
+ async write(path, content) {
718
+ return this.repo.writeFile(path, content);
719
+ }
720
+ async seal(path, content, recipients) {
721
+ const box = sealContent(this.ring, content, recipients);
722
+ return this.repo.writeSealed(path, JSON.stringify(box));
723
+ }
724
+ async open(path, actor) {
725
+ const leaf = await this.repo.read(path);
726
+ if (!leaf)
727
+ return;
728
+ if (leaf.kind === "blob")
729
+ return leaf.content;
730
+ const box = JSON.parse(leaf.box);
731
+ return openContent(this.ring, box, actor);
732
+ }
733
+ }
734
+
735
+ // src/bin/git-adapter.ts
736
+ import { execFileSync } from "node:child_process";
737
+ import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
738
+ import { join as join5 } from "node:path";
739
+
740
+ // src/file-store.ts
741
+ import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
742
+ import { join as join2 } from "node:path";
743
+ import { gunzipSync as gunzipSync2, gzipSync as gzipSync2 } from "node:zlib";
744
+ function encodeObject2(node) {
745
+ return gzipSync2(JSON.stringify(node), { level: 1 });
746
+ }
747
+ function decodeObject2(raw) {
748
+ const json = raw.length >= 2 && raw[0] === 31 && raw[1] === 139 ? gunzipSync2(raw).toString("utf8") : raw.toString("utf8");
749
+ return JSON.parse(json);
750
+ }
751
+
752
+ class FileStore2 {
753
+ objects;
754
+ constructor(solDir) {
755
+ this.objects = join2(solDir, "objects");
756
+ mkdirSync2(this.objects, { recursive: true });
757
+ }
758
+ path(hash) {
759
+ return join2(this.objects, hash);
760
+ }
761
+ async put(node) {
762
+ const h = hashNode(node);
763
+ const p = this.path(h);
764
+ if (!existsSync2(p)) {
765
+ const tmp = `${p}.tmp`;
766
+ writeFileSync2(tmp, encodeObject2(node));
767
+ renameSync2(tmp, p);
768
+ }
769
+ return h;
770
+ }
771
+ async get(hash) {
772
+ const p = this.path(hash);
773
+ if (!existsSync2(p))
774
+ return;
775
+ const node = decodeObject2(readFileSync2(p));
776
+ if (hashNode(node) !== hash)
777
+ throw new CorruptObjectError(hash);
778
+ return node;
779
+ }
780
+ async has(hash) {
781
+ return existsSync2(this.path(hash));
782
+ }
783
+ count() {
784
+ return existsSync2(this.objects) ? readdirSync2(this.objects).length : 0;
785
+ }
786
+ }
787
+
788
+ class FileOpLog {
789
+ opsFile;
790
+ headFile;
791
+ constructor(solDir) {
792
+ this.opsFile = join2(solDir, "ops.jsonl");
793
+ this.headFile = join2(solDir, "HEAD");
794
+ }
795
+ readHead() {
796
+ return existsSync2(this.headFile) ? JSON.parse(readFileSync2(this.headFile, "utf8")) : { seq: 0 };
797
+ }
798
+ async head() {
799
+ return this.readHead().head;
800
+ }
801
+ async seq() {
802
+ return this.readHead().seq ?? 0;
803
+ }
804
+ async logTip() {
805
+ return this.readHead().logTip;
806
+ }
807
+ async append(entry) {
808
+ const chained = chainOp(this.readHead().logTip, entry);
809
+ appendFileSync2(this.opsFile, JSON.stringify(chained) + `
810
+ `);
811
+ writeFileSync2(this.headFile, JSON.stringify({ head: chained.rootAfter, seq: chained.seq, logTip: chained.entryHash }));
812
+ }
813
+ async history(opts) {
814
+ if (!existsSync2(this.opsFile))
815
+ return [];
816
+ const all = [];
817
+ for (const l of readFileSync2(this.opsFile, "utf8").split(`
818
+ `)) {
819
+ if (!l)
820
+ continue;
821
+ try {
822
+ all.push(JSON.parse(l));
823
+ } catch {
824
+ break;
825
+ }
826
+ }
827
+ return pageOps(all, opts);
828
+ }
829
+ }
830
+
831
+ // src/bin/runtime.ts
832
+ import { chmodSync as chmodSync2, existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync4, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
833
+ import { dirname as dirname2, join as join4, relative as relative2 } from "node:path";
834
+
835
+ // src/bin/lib.ts
836
+ import {
837
+ appendFileSync as appendFileSync3,
838
+ chmodSync,
839
+ existsSync as existsSync3,
840
+ lstatSync,
841
+ mkdirSync as mkdirSync3,
842
+ readdirSync as readdirSync3,
843
+ readFileSync as readFileSync3,
844
+ readlinkSync,
845
+ rmdirSync,
846
+ symlinkSync,
847
+ unlinkSync,
848
+ writeFileSync as writeFileSync3
849
+ } from "node:fs";
850
+ import { dirname, join as join3, relative, resolve, sep } from "node:path";
851
+
852
+ // src/diff.ts
853
+ function splitLines2(s) {
854
+ return s.split(`
855
+ `);
856
+ }
857
+ function lcsSteps2(a, b) {
858
+ const n = a.length;
859
+ const m = b.length;
860
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
861
+ for (let i2 = n - 1;i2 >= 0; i2--) {
862
+ for (let j2 = m - 1;j2 >= 0; j2--) {
863
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
864
+ }
865
+ }
866
+ const steps = [];
867
+ let i = 0;
868
+ let j = 0;
869
+ while (i < n && j < m) {
870
+ if (a[i] === b[j]) {
871
+ steps.push({ tag: " ", line: a[i] });
872
+ i++;
873
+ j++;
874
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
875
+ steps.push({ tag: "-", line: a[i] });
876
+ i++;
877
+ } else {
878
+ steps.push({ tag: "+", line: b[j] });
879
+ j++;
880
+ }
881
+ }
882
+ while (i < n)
883
+ steps.push({ tag: "-", line: a[i++] });
884
+ while (j < m)
885
+ steps.push({ tag: "+", line: b[j++] });
886
+ return steps;
887
+ }
888
+ function lineHunks2(aText, bText) {
889
+ const steps = lcsSteps2(splitLines2(aText), splitLines2(bText));
890
+ const body = steps.map((s) => `${s.tag}${s.line}`).join(`
891
+ `);
892
+ return `@@ -1 +1 @@
893
+ ${body}`;
894
+ }
895
+
896
+ // src/bin/lib.ts
897
+ var procCwd = process.cwd();
898
+ function findRepoRoot(start) {
899
+ let d = start;
900
+ for (;; ) {
901
+ if (existsSync3(join3(d, ".sol")))
902
+ return d;
903
+ const parent = dirname(d);
904
+ if (parent === d)
905
+ return;
906
+ d = parent;
907
+ }
908
+ }
909
+ var repoRoot = findRepoRoot(procCwd);
910
+ var cwd = repoRoot ?? procCwd;
911
+ var solDir = join3(cwd, ".sol");
912
+ var actor = process.env.SOL_ACTOR || process.env.USER || "you";
913
+ var DEFAULT_IGNORE = [".sol", ".git", "node_modules", "dist", "build", ".next", ".cache", ".DS_Store", "__pycache__", "*.pyc", ".venv", "venv", "target", ".gradle", "*.log"];
914
+ function ignorePatterns() {
915
+ const f = join3(cwd, ".solignore");
916
+ const extra = existsSync3(f) ? readFileSync3(f, "utf8").split(`
917
+ `).map((s) => s.trim()).filter((s) => s && !s.startsWith("#")) : [];
918
+ return [...DEFAULT_IGNORE, ...extra];
919
+ }
920
+ function isIgnored(rel, pats) {
921
+ const segs = rel.split("/");
922
+ return pats.some((p0) => {
923
+ const p = p0.endsWith("/") ? p0.slice(0, -1) : p0;
924
+ if (p.startsWith("*."))
925
+ return rel.endsWith(p.slice(1));
926
+ return rel === p || rel.startsWith(p + "/") || segs.includes(p);
927
+ });
928
+ }
929
+
930
+ // src/bin/runtime.ts
931
+ var SYMLINK_MODE = 40960;
932
+ var EXEC_MODE = 493;
933
+ function hydrate(store, head, dir) {
934
+ if (!head)
935
+ return 0;
936
+ let n = 0;
937
+ for (const p of listAll(store, head)) {
938
+ const blob = fileAt(store, head, p);
939
+ if (!blob)
940
+ continue;
941
+ const abs = join4(dir, p);
942
+ mkdirSync4(dirname2(abs), { recursive: true });
943
+ const mode = modeAt(store, head, p);
944
+ if (mode === SYMLINK_MODE) {
945
+ try {
946
+ unlinkSync2(abs);
947
+ } catch {}
948
+ symlinkSync2(blob.content, abs);
949
+ } else {
950
+ writeFileSync4(abs, blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
951
+ if (mode === EXEC_MODE) {
952
+ try {
953
+ chmodSync2(abs, EXEC_MODE);
954
+ } catch {}
955
+ }
956
+ }
957
+ n++;
958
+ }
959
+ return n;
960
+ }
961
+
962
+ // src/bin/git-adapter.ts
963
+ var git = (cwd2, ...a) => execFileSync("git", ["-C", cwd2, ...a], { maxBuffer: 1 << 30 });
964
+ function parseLsTree(out) {
965
+ const m = new Map;
966
+ for (const entry of out.split("\x00")) {
967
+ if (!entry)
968
+ continue;
969
+ const tab = entry.indexOf("\t");
970
+ const meta = entry.slice(0, tab).split(" ");
971
+ m.set(entry.slice(tab + 1), { mode: meta[0], hash: meta[2] });
972
+ }
973
+ return m;
974
+ }
975
+ async function applyGitFile(repo, path, mode, content) {
976
+ if (mode === "120000") {
977
+ const target = content.toString("utf8");
978
+ if (await repo.readFile(path) !== target || await repo.mode(path) !== 40960) {
979
+ await repo.writeFile(path, target);
980
+ await repo.chmod(path, 40960);
981
+ }
982
+ return;
983
+ }
984
+ const exec = mode === "100755";
985
+ const curExec = await repo.mode(path) === 493;
986
+ if (content.includes(0)) {
987
+ const cur = await repo.readBytes(path);
988
+ const same = cur !== undefined && cur !== SEALED && Buffer.from(cur).equals(content);
989
+ if (!same)
990
+ await repo.writeBytes(path, new Uint8Array(content));
991
+ } else {
992
+ const disk = content.toString("utf8");
993
+ if (await repo.readFile(path) !== disk)
994
+ await repo.writeFile(path, disk);
995
+ }
996
+ if (exec !== curExec)
997
+ await repo.chmod(path, exec ? 493 : 420);
998
+ }
999
+ function setHead(fdir, head) {
1000
+ const hf = join5(fdir, "HEAD");
1001
+ const cur = existsSync5(hf) ? JSON.parse(readFileSync5(hf, "utf8")) : { seq: 0 };
1002
+ writeFileSync5(hf, JSON.stringify({ ...cur, head }));
1003
+ }
1004
+ async function importGitRepo(gitPath, fdir) {
1005
+ const store = new FileStore2(fdir);
1006
+ const log = new FileOpLog(fdir);
1007
+ const empty = await emptyRoot2(store);
1008
+ const commitList = git(gitPath, "rev-list", "--reverse", "--topo-order", "--all").toString().trim().split(`
1009
+ `).filter(Boolean);
1010
+ const gitToSol = new Map;
1011
+ const gitFiles = new Map;
1012
+ let count = 0;
1013
+ for (const gc of commitList) {
1014
+ const author = git(gitPath, "log", "-1", "--format=%an", gc).toString().trim() || "git";
1015
+ const message = git(gitPath, "log", "-1", "--format=%B", gc).toString().trim();
1016
+ const parents = git(gitPath, "log", "-1", "--format=%P", gc).toString().trim().split(/\s+/).filter(Boolean);
1017
+ const p1Sol = parents[0] ? gitToSol.get(parents[0]) ?? empty : empty;
1018
+ setHead(fdir, p1Sol);
1019
+ const repo = new AsyncRepo(store, log, author);
1020
+ const tree = parseLsTree(git(gitPath, "ls-tree", "-r", "-z", gc).toString());
1021
+ for (const [path, { mode, hash }] of tree) {
1022
+ if (mode === "160000")
1023
+ continue;
1024
+ await applyGitFile(repo, path, mode, git(gitPath, "cat-file", "blob", hash));
1025
+ }
1026
+ for (const p of parents[0] ? gitFiles.get(parents[0]) ?? new Set : new Set)
1027
+ if (!tree.has(p))
1028
+ await repo.deleteFile(p);
1029
+ const head = await repo.head();
1030
+ const p2Sol = parents[1] ? gitToSol.get(parents[1]) : undefined;
1031
+ await log.append({ seq: await log.seq() + 1, type: "checkpoint", path: "", rootAfter: head, at: Date.now(), by: author, message, parent: p1Sol, ...p2Sol ? { parent2: p2Sol } : {} });
1032
+ gitToSol.set(gc, head);
1033
+ gitFiles.set(gc, new Set(tree.keys()));
1034
+ count++;
1035
+ }
1036
+ const branches = [];
1037
+ for (const line of git(gitPath, "for-each-ref", "--format=%(refname:short) %(objectname)", "refs/heads").toString().trim().split(`
1038
+ `).filter(Boolean)) {
1039
+ const [name, tip] = line.split(" ");
1040
+ const solHead = gitToSol.get(tip);
1041
+ if (solHead)
1042
+ branches.push({ name, head: solHead });
1043
+ }
1044
+ const current = git(gitPath, "rev-parse", "--abbrev-ref", "HEAD").toString().trim() || "main";
1045
+ const currentSol = gitToSol.get(git(gitPath, "rev-parse", "HEAD").toString().trim()) ?? await log.head() ?? "";
1046
+ setHead(fdir, currentSol);
1047
+ return { commits: count, branches, head: currentSol, current };
1048
+ }
1049
+ function exportToGit(store, head, gitPath, message) {
1050
+ const want = new Set(head ? listAll(store, head) : []);
1051
+ const tracked = git(gitPath, "ls-files", "-z").toString().split("\x00").filter(Boolean);
1052
+ let deleted = 0;
1053
+ for (const f of tracked) {
1054
+ if (!want.has(f)) {
1055
+ try {
1056
+ unlinkSync3(join5(gitPath, f));
1057
+ } catch {}
1058
+ deleted++;
1059
+ }
1060
+ }
1061
+ const written = hydrate(store, head, gitPath);
1062
+ git(gitPath, "add", "-A");
1063
+ git(gitPath, "commit", "-m", message || "sol export", "--allow-empty");
1064
+ return { written, deleted };
1065
+ }
1066
+
1067
+ // src/log.ts
1068
+ function groupIntoUnits(ops) {
1069
+ const units = [];
1070
+ let cur = [];
1071
+ for (const op of ops) {
1072
+ if (op.type === "checkpoint") {
1073
+ units.push({ label: op.message, by: op.by, at: op.at, head: op.rootAfter, ops: cur, open: false });
1074
+ cur = [];
1075
+ } else {
1076
+ cur.push(op);
1077
+ }
1078
+ }
1079
+ if (cur.length > 0) {
1080
+ const last = cur[cur.length - 1];
1081
+ units.push({ by: last.by, at: last.at, head: last.rootAfter, ops: cur, open: true });
1082
+ }
1083
+ return units;
1084
+ }
1085
+ function formatLog(ops) {
1086
+ return ops.map((op) => {
1087
+ const by = op.by ?? "?";
1088
+ const head = `${op.seq} ${by} ${op.type} ${op.path}`;
1089
+ return op.message ? `${head}: ${op.message}` : head;
1090
+ });
1091
+ }
1092
+
1093
+ // src/text-merge.ts
1094
+ function lines(s) {
1095
+ return s.split(`
1096
+ `);
1097
+ }
1098
+ function eq(a, b) {
1099
+ return a.length === b.length && a.every((x, i) => x === b[i]);
1100
+ }
1101
+ function lcsPairs(a, b) {
1102
+ const n = a.length;
1103
+ const m = b.length;
1104
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
1105
+ for (let i2 = n - 1;i2 >= 0; i2--) {
1106
+ for (let j2 = m - 1;j2 >= 0; j2--) {
1107
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
1108
+ }
1109
+ }
1110
+ const pairs = [];
1111
+ let i = 0;
1112
+ let j = 0;
1113
+ while (i < n && j < m) {
1114
+ if (a[i] === b[j]) {
1115
+ pairs.push([i, j]);
1116
+ i++;
1117
+ j++;
1118
+ } else if (dp[i + 1][j] >= dp[i][j + 1])
1119
+ i++;
1120
+ else
1121
+ j++;
1122
+ }
1123
+ return pairs;
1124
+ }
1125
+ function hunks(base, other) {
1126
+ const pairs = lcsPairs(base, other);
1127
+ const out = [];
1128
+ let pb = 0;
1129
+ let po = 0;
1130
+ const flush = (bEnd, oEnd) => {
1131
+ if (bEnd > pb || oEnd > po)
1132
+ out.push({ bs: pb, be: bEnd, lines: other.slice(po, oEnd) });
1133
+ };
1134
+ for (const [bi, oi] of pairs) {
1135
+ flush(bi, oi);
1136
+ pb = bi + 1;
1137
+ po = oi + 1;
1138
+ }
1139
+ flush(base.length, other.length);
1140
+ return out;
1141
+ }
1142
+ function sideContent(side, rs, re, base) {
1143
+ const res = [];
1144
+ let pos = rs;
1145
+ for (const h of side) {
1146
+ if (h.bs < rs || h.be > re)
1147
+ continue;
1148
+ for (let k = pos;k < h.bs; k++)
1149
+ res.push(base[k]);
1150
+ res.push(...h.lines);
1151
+ pos = Math.max(pos, h.be);
1152
+ }
1153
+ for (let k = pos;k < re; k++)
1154
+ res.push(base[k]);
1155
+ return res;
1156
+ }
1157
+ function merge3(baseS, oursS, theirsS) {
1158
+ const base = lines(baseS);
1159
+ const ours = hunks(base, lines(oursS));
1160
+ const theirs = hunks(base, lines(theirsS));
1161
+ const all = [
1162
+ ...ours.map((h) => ({ ...h, side: "o" })),
1163
+ ...theirs.map((h) => ({ ...h, side: "t" }))
1164
+ ].sort((x, y) => x.bs - y.bs || x.be - y.be);
1165
+ const regions = [];
1166
+ for (const h of all) {
1167
+ const last = regions[regions.length - 1];
1168
+ if (last && h.bs < last.re) {
1169
+ last.re = Math.max(last.re, h.be);
1170
+ if (h.side === "o")
1171
+ last.o = true;
1172
+ else
1173
+ last.t = true;
1174
+ } else {
1175
+ regions.push({ rs: h.bs, re: h.be, o: h.side === "o", t: h.side === "t" });
1176
+ }
1177
+ }
1178
+ const out = [];
1179
+ let conflicts = 0;
1180
+ let pos = 0;
1181
+ for (const reg of regions) {
1182
+ for (let k = pos;k < reg.rs; k++)
1183
+ out.push(base[k]);
1184
+ const o = sideContent(ours, reg.rs, reg.re, base);
1185
+ const t = sideContent(theirs, reg.rs, reg.re, base);
1186
+ if (reg.o && reg.t) {
1187
+ if (eq(o, t))
1188
+ out.push(...o);
1189
+ else {
1190
+ conflicts++;
1191
+ out.push("<<<<<<< ours", ...o, "=======", ...t, ">>>>>>> theirs");
1192
+ }
1193
+ } else if (reg.o)
1194
+ out.push(...o);
1195
+ else
1196
+ out.push(...t);
1197
+ pos = reg.re;
1198
+ }
1199
+ for (let k = pos;k < base.length; k++)
1200
+ out.push(base[k]);
1201
+ return { clean: conflicts === 0, text: out.join(`
1202
+ `), conflicts };
1203
+ }
1204
+
1205
+ // src/merge.ts
1206
+ function resolvePath(base, ours, theirs) {
1207
+ const oursChanged = ours !== base;
1208
+ const theirsChanged = theirs !== base;
1209
+ if (!oursChanged && !theirsChanged) {
1210
+ return { content: base };
1211
+ }
1212
+ if (oursChanged && !theirsChanged) {
1213
+ return { content: ours };
1214
+ }
1215
+ if (!oursChanged && theirsChanged) {
1216
+ return { content: theirs };
1217
+ }
1218
+ if (ours === theirs) {
1219
+ return { content: ours };
1220
+ }
1221
+ if (base !== undefined && ours !== undefined && theirs !== undefined) {
1222
+ const m = merge3(base, ours, theirs);
1223
+ if (m.clean)
1224
+ return { content: m.text };
1225
+ return { content: m.text, conflict: { path: "", ours, theirs } };
1226
+ }
1227
+ return {
1228
+ content: ours,
1229
+ conflict: { path: "", ours, theirs }
1230
+ };
1231
+ }
1232
+ function merge(repo, baseHead, oursHead, theirsHead) {
1233
+ const store = repo.store;
1234
+ const paths = new Set([
1235
+ ...listAll(store, baseHead),
1236
+ ...listAll(store, oursHead),
1237
+ ...listAll(store, theirsHead)
1238
+ ]);
1239
+ const conflicts = [];
1240
+ let root = emptyRoot(store);
1241
+ for (const path of [...paths].sort()) {
1242
+ const base = readFile(store, baseHead, path);
1243
+ const ours = readFile(store, oursHead, path);
1244
+ const theirs = readFile(store, theirsHead, path);
1245
+ const { content, conflict } = resolvePath(base, ours, theirs);
1246
+ if (conflict)
1247
+ conflicts.push({ ...conflict, path });
1248
+ if (content !== undefined) {
1249
+ root = writeFile(store, root, path, content);
1250
+ }
1251
+ }
1252
+ return { head: root, conflicts };
1253
+ }
1254
+
1255
+ // src/mr.ts
1256
+ function latestVerdicts(mr) {
1257
+ const m = new Map;
1258
+ for (const r of mr.reviews)
1259
+ m.set(r.actor, r.verdict);
1260
+ return m;
1261
+ }
1262
+ function mrSummary(mr) {
1263
+ const approvals = [...latestVerdicts(mr).values()].filter((v) => v === "approve").length;
1264
+ const checks = mr.checks.length ? `${mr.checks.filter((c) => c.status === "pass").length}/${mr.checks.length} checks pass` : "no checks";
1265
+ const fails = mr.checks.filter((c) => c.status === "fail").length;
1266
+ return `${approvals} approval${approvals === 1 ? "" : "s"}, ${checks}${fails ? `, ${fails} failing` : ""}`;
1267
+ }
1268
+
1269
+ // src/store.ts
1270
+ import { createHash as createHash2 } from "node:crypto";
1271
+ function hashString2(s) {
1272
+ return "h_" + createHash2("sha256").update(s, "utf8").digest("hex");
1273
+ }
1274
+ function hashNode2(node) {
1275
+ let canon;
1276
+ if (node.kind === "blob") {
1277
+ canon = node.encoding ? `blob\x00${node.encoding}\x00${node.content}` : `blob\x00${node.content}`;
1278
+ } else if (node.kind === "sealed")
1279
+ canon = `sealed\x00${node.box}`;
1280
+ else
1281
+ canon = "tree\x00" + Object.keys(node.entries).sort().map((k) => {
1282
+ const e = node.entries[k];
1283
+ return e.mode === undefined ? `${k}\x00${e.kind}\x00${e.hash}` : `${k}\x00${e.kind}\x00${e.hash}\x00${e.mode}`;
1284
+ }).join(`
1285
+ `);
1286
+ return hashString2(canon);
1287
+ }
1288
+
1289
+ class Store2 {
1290
+ objects = new Map;
1291
+ put(node) {
1292
+ const h = hashNode2(node);
1293
+ if (!this.objects.has(h))
1294
+ this.objects.set(h, node);
1295
+ return h;
1296
+ }
1297
+ get(h) {
1298
+ return this.objects.get(h);
1299
+ }
1300
+ getTree(h) {
1301
+ const n = this.get(h);
1302
+ if (!n || n.kind !== "tree")
1303
+ throw new Error(`not a tree: ${h}`);
1304
+ return n;
1305
+ }
1306
+ has(h) {
1307
+ return this.get(h) !== undefined;
1308
+ }
1309
+ size() {
1310
+ return this.objects.size;
1311
+ }
1312
+ }
1313
+
1314
+ // src/tree.ts
1315
+ var EMPTY_TREE2 = { kind: "tree", entries: {} };
1316
+ function emptyRoot3(store) {
1317
+ return store.put(EMPTY_TREE2);
1318
+ }
1319
+ function segments3(path) {
1320
+ return path.split("/").filter(Boolean);
1321
+ }
1322
+ function fileAt3(store, root, path) {
1323
+ const segs = segments3(path);
1324
+ let cur = root;
1325
+ for (let i = 0;i < segs.length; i++) {
1326
+ const entry = store.getTree(cur).entries[segs[i]];
1327
+ if (!entry)
1328
+ return;
1329
+ if (i === segs.length - 1)
1330
+ return entry.kind === "blob" ? store.get(entry.hash) : undefined;
1331
+ if (entry.kind !== "tree")
1332
+ return;
1333
+ cur = entry.hash;
1334
+ }
1335
+ return;
1336
+ }
1337
+ function listAll3(store, root, prefix = "") {
1338
+ const tree = store.getTree(root);
1339
+ const out = [];
1340
+ for (const [name, entry] of Object.entries(tree.entries)) {
1341
+ const p = prefix ? `${prefix}/${name}` : name;
1342
+ if (entry.kind === "tree")
1343
+ out.push(...listAll3(store, entry.hash, p));
1344
+ else
1345
+ out.push(p);
1346
+ }
1347
+ return out.sort();
1348
+ }
1349
+
1350
+ // src/types.ts
1351
+ var SEALED2 = "<<sealed>>";
1352
+
1353
+ // src/bin/lib.ts
1354
+ import {
1355
+ appendFileSync as appendFileSync4,
1356
+ chmodSync as chmodSync3,
1357
+ existsSync as existsSync6,
1358
+ lstatSync as lstatSync3,
1359
+ mkdirSync as mkdirSync5,
1360
+ readdirSync as readdirSync5,
1361
+ readFileSync as readFileSync6,
1362
+ readlinkSync as readlinkSync3,
1363
+ rmdirSync as rmdirSync2,
1364
+ symlinkSync as symlinkSync3,
1365
+ unlinkSync as unlinkSync4,
1366
+ writeFileSync as writeFileSync6
1367
+ } from "node:fs";
1368
+ import { dirname as dirname3, join as join6, relative as relative3, resolve as resolve2, sep as sep2 } from "node:path";
1369
+ var procCwd2 = process.cwd();
1370
+ function findRepoRoot2(start) {
1371
+ let d = start;
1372
+ for (;; ) {
1373
+ if (existsSync6(join6(d, ".sol")))
1374
+ return d;
1375
+ const parent = dirname3(d);
1376
+ if (parent === d)
1377
+ return;
1378
+ d = parent;
1379
+ }
1380
+ }
1381
+ var repoRoot2 = findRepoRoot2(procCwd2);
1382
+ var cwd2 = repoRoot2 ?? procCwd2;
1383
+ var solDir2 = join6(cwd2, ".sol");
1384
+ var actor2 = process.env.SOL_ACTOR || process.env.USER || "you";
1385
+ function repoRel(p) {
1386
+ return relative3(cwd2, resolve2(procCwd2, p));
1387
+ }
1388
+ var SYMLINK_MODE2 = 40960;
1389
+ var EXEC_MODE2 = 493;
1390
+ function die(msg) {
1391
+ console.error("sol: " + msg);
1392
+ process.exit(1);
1393
+ }
1394
+ function open() {
1395
+ if (!existsSync6(solDir2))
1396
+ die("not a sol repo — run `sol init` first");
1397
+ return { repo: new AsyncRepo(new FileStore2(solDir2), new FileOpLog(solDir2), actor2), log: new FileOpLog(solDir2) };
1398
+ }
1399
+ var DEFAULT_IGNORE2 = [".sol", ".git", "node_modules", "dist", "build", ".next", ".cache", ".DS_Store", "__pycache__", "*.pyc", ".venv", "venv", "target", ".gradle", "*.log"];
1400
+ function ignorePatterns2() {
1401
+ const f = join6(cwd2, ".solignore");
1402
+ const extra = existsSync6(f) ? readFileSync6(f, "utf8").split(`
1403
+ `).map((s) => s.trim()).filter((s) => s && !s.startsWith("#")) : [];
1404
+ return [...DEFAULT_IGNORE2, ...extra];
1405
+ }
1406
+ function isIgnored2(rel, pats) {
1407
+ const segs = rel.split("/");
1408
+ return pats.some((p0) => {
1409
+ const p = p0.endsWith("/") ? p0.slice(0, -1) : p0;
1410
+ if (p.startsWith("*."))
1411
+ return rel.endsWith(p.slice(1));
1412
+ return rel === p || rel.startsWith(p + "/") || segs.includes(p);
1413
+ });
1414
+ }
1415
+ function walkEntries(dir = cwd2, base = cwd2, pats = ignorePatterns2(), out = []) {
1416
+ for (const name of readdirSync5(dir)) {
1417
+ const p = join6(dir, name);
1418
+ const rel = relative3(base, p);
1419
+ if (isIgnored2(rel, pats))
1420
+ continue;
1421
+ const st = lstatSync3(p);
1422
+ if (st.isDirectory() && !st.isSymbolicLink())
1423
+ walkEntries(p, base, pats, out);
1424
+ else
1425
+ out.push({ rel, st });
1426
+ }
1427
+ return out;
1428
+ }
1429
+ function walkFiles(dir = cwd2, base = cwd2, pats = ignorePatterns2()) {
1430
+ return walkEntries(dir, base, pats).map((e) => e.rel);
1431
+ }
1432
+ async function snapshotFile(repo, f) {
1433
+ const abs = join6(cwd2, f);
1434
+ const st = lstatSync3(abs);
1435
+ if (st.isSymbolicLink()) {
1436
+ const target = readlinkSync3(abs);
1437
+ if (await repo.readFile(f) === target && await repo.mode(f) === SYMLINK_MODE2)
1438
+ return false;
1439
+ await repo.writeFile(f, target);
1440
+ await repo.chmod(f, SYMLINK_MODE2);
1441
+ return true;
1442
+ }
1443
+ const wantExec = (st.mode & 73) !== 0;
1444
+ const curMode = await repo.mode(f);
1445
+ const isExec = curMode === EXEC_MODE2;
1446
+ const buf = readFileSync6(abs);
1447
+ if (buf.includes(0)) {
1448
+ const cur = await repo.readBytes(f);
1449
+ const same2 = cur !== undefined && cur !== SEALED && Buffer.from(cur).equals(buf);
1450
+ if (same2 && wantExec === isExec)
1451
+ return false;
1452
+ if (!same2)
1453
+ await repo.writeBytes(f, new Uint8Array(buf));
1454
+ if (wantExec !== isExec)
1455
+ await repo.chmod(f, wantExec ? EXEC_MODE2 : 420);
1456
+ return true;
1457
+ }
1458
+ const disk = buf.toString("utf8");
1459
+ const stored = await repo.readFile(f);
1460
+ if (stored === SEALED)
1461
+ return false;
1462
+ const same = stored === disk;
1463
+ if (same && wantExec === isExec)
1464
+ return false;
1465
+ if (!same)
1466
+ await repo.writeFile(f, disk);
1467
+ if (wantExec !== isExec)
1468
+ await repo.chmod(f, wantExec ? EXEC_MODE2 : 420);
1469
+ return true;
1470
+ }
1471
+ async function fileChange(repo, f) {
1472
+ const abs = join6(cwd2, f);
1473
+ const st = lstatSync3(abs);
1474
+ if (st.isSymbolicLink()) {
1475
+ const target = readlinkSync3(abs);
1476
+ if (await repo.readFile(f) === target && await repo.mode(f) === SYMLINK_MODE2)
1477
+ return null;
1478
+ return { path: f, content: target, mode: SYMLINK_MODE2 };
1479
+ }
1480
+ const wantMode = (st.mode & 73) !== 0 ? EXEC_MODE2 : undefined;
1481
+ const curMode = await repo.mode(f);
1482
+ const buf = readFileSync6(abs);
1483
+ if (buf.includes(0)) {
1484
+ const cur = await repo.readBytes(f);
1485
+ if (cur === SEALED)
1486
+ return null;
1487
+ const same = cur !== undefined && Buffer.from(cur).equals(buf);
1488
+ if (same && curMode === wantMode)
1489
+ return null;
1490
+ return { path: f, content: buf.toString("base64"), encoding: "base64", mode: wantMode };
1491
+ }
1492
+ const disk = buf.toString("utf8");
1493
+ const stored = await repo.readFile(f);
1494
+ if (stored === SEALED)
1495
+ return null;
1496
+ if (stored === disk && curMode === wantMode)
1497
+ return null;
1498
+ return { path: f, content: disk, mode: wantMode };
1499
+ }
1500
+ var includePath = () => join6(solDir2, "include");
1501
+ function forceIncluded() {
1502
+ return existsSync6(includePath()) ? readFileSync6(includePath(), "utf8").split(`
1503
+ `).map((s) => s.trim()).filter(Boolean) : [];
1504
+ }
1505
+ function addInclude(path) {
1506
+ if (!forceIncluded().includes(path))
1507
+ appendFileSync4(includePath(), path + `
1508
+ `);
1509
+ }
1510
+ function lexists(abs) {
1511
+ try {
1512
+ lstatSync3(abs);
1513
+ return true;
1514
+ } catch {
1515
+ return false;
1516
+ }
1517
+ }
1518
+ async function snapshotTree(repo) {
1519
+ const onDisk = new Set(walkFiles());
1520
+ for (const f of forceIncluded())
1521
+ if (lexists(join6(cwd2, f)))
1522
+ onDisk.add(f);
1523
+ const tracked = await repo.list();
1524
+ for (const t of tracked)
1525
+ if (!onDisk.has(t) && lexists(join6(cwd2, t)))
1526
+ onDisk.add(t);
1527
+ const changes = [];
1528
+ for (const f of onDisk) {
1529
+ const c = await fileChange(repo, f);
1530
+ if (c)
1531
+ changes.push(c);
1532
+ }
1533
+ for (const t of tracked)
1534
+ if (!lexists(join6(cwd2, t)))
1535
+ changes.push({ path: t, delete: true });
1536
+ return repo.applyBatch(changes);
1537
+ }
1538
+ function sleepSync(ms) {
1539
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1540
+ }
1541
+ function acquireLock() {
1542
+ const lockDir = join6(solDir2, "lock");
1543
+ const deadline = Date.now() + 15000;
1544
+ for (;; ) {
1545
+ try {
1546
+ mkdirSync5(lockDir);
1547
+ break;
1548
+ } catch {
1549
+ try {
1550
+ if (Date.now() - lstatSync3(lockDir).mtimeMs > 30000) {
1551
+ rmdirSync2(lockDir);
1552
+ continue;
1553
+ }
1554
+ } catch {}
1555
+ if (Date.now() > deadline)
1556
+ die("repo is locked by another sol process (timed out)");
1557
+ sleepSync(25);
1558
+ }
1559
+ }
1560
+ let released = false;
1561
+ const release = () => {
1562
+ if (released)
1563
+ return;
1564
+ released = true;
1565
+ try {
1566
+ rmdirSync2(lockDir);
1567
+ } catch {}
1568
+ };
1569
+ process.on("exit", release);
1570
+ return release;
1571
+ }
1572
+
1573
+ class LazyStore extends Store {
1574
+ objDir;
1575
+ constructor(objDir) {
1576
+ super();
1577
+ this.objDir = objDir;
1578
+ }
1579
+ get(h) {
1580
+ const cached = this.objects.get(h);
1581
+ if (cached)
1582
+ return cached;
1583
+ const p = join6(this.objDir, h);
1584
+ if (!existsSync6(p))
1585
+ return;
1586
+ try {
1587
+ const node = decodeObject2(readFileSync6(p));
1588
+ this.objects.set(h, node);
1589
+ return node;
1590
+ } catch {
1591
+ return;
1592
+ }
1593
+ }
1594
+ }
1595
+ function loadStore() {
1596
+ return new LazyStore(join6(solDir2, "objects"));
1597
+ }
1598
+ function allLocalNodes() {
1599
+ const objDir = join6(solDir2, "objects");
1600
+ const out = [];
1601
+ if (existsSync6(objDir))
1602
+ for (const name of readdirSync5(objDir)) {
1603
+ if (name.endsWith(".tmp"))
1604
+ continue;
1605
+ try {
1606
+ out.push(decodeObject2(readFileSync6(join6(objDir, name))));
1607
+ } catch {}
1608
+ }
1609
+ return out;
1610
+ }
1611
+ async function resolveRef(log, ref) {
1612
+ const ops = await log.history();
1613
+ if (!ref || ref === "head" || ref === "HEAD")
1614
+ return { head: await log.head() ?? "", op: ops[ops.length - 1] };
1615
+ if (existsSync6(refsPath())) {
1616
+ const refs = JSON.parse(readFileSync6(refsPath(), "utf8"));
1617
+ if (ref === refs.current) {
1618
+ const h = await log.head() ?? "";
1619
+ return { head: h, op: [...ops].reverse().find((o) => o.rootAfter === h) ?? ops[ops.length - 1] };
1620
+ }
1621
+ const named = refs.branches[ref]?.head ?? refs.tags[ref];
1622
+ if (named !== undefined)
1623
+ return { head: named, op: [...ops].reverse().find((o) => o.rootAfter === named) };
1624
+ }
1625
+ if (/^\d+$/.test(ref)) {
1626
+ const op2 = ops.find((o) => o.seq === Number(ref)) || die("no op at seq " + ref);
1627
+ return { head: op2.rootAfter, op: op2 };
1628
+ }
1629
+ const bare = ref.startsWith("h_") ? ref : "h_" + ref;
1630
+ const op = ops.find((o) => o.rootAfter.startsWith(bare) || (o.entryHash ?? "").startsWith(bare)) || die("unknown ref: " + ref);
1631
+ return { head: op.rootAfter, op };
1632
+ }
1633
+ function materialize(store, head, path) {
1634
+ const blob = fileAt(store, head, path);
1635
+ if (!blob)
1636
+ return false;
1637
+ const abs = resolve2(cwd2, path);
1638
+ if (abs !== cwd2 && !abs.startsWith(cwd2 + sep2)) {
1639
+ console.error("sol: refusing to write outside the repo: " + path);
1640
+ return false;
1641
+ }
1642
+ const mode = modeAt(store, head, path);
1643
+ mkdirSync5(dirname3(abs), { recursive: true });
1644
+ if (mode === SYMLINK_MODE2) {
1645
+ try {
1646
+ unlinkSync4(abs);
1647
+ } catch {}
1648
+ symlinkSync3(blob.content, abs);
1649
+ return true;
1650
+ }
1651
+ writeFileSync6(abs, blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
1652
+ if (mode === EXEC_MODE2) {
1653
+ try {
1654
+ chmodSync3(abs, EXEC_MODE2);
1655
+ } catch {}
1656
+ }
1657
+ return true;
1658
+ }
1659
+ function materializeTree(store, head) {
1660
+ const want = new Set(head ? listAll(store, head) : []);
1661
+ let n = 0;
1662
+ for (const f of walkFiles())
1663
+ if (!want.has(f)) {
1664
+ unlinkSync4(join6(cwd2, f));
1665
+ n++;
1666
+ }
1667
+ for (const f of want)
1668
+ if (materialize(store, head, f))
1669
+ n++;
1670
+ writeWorkingIndex([...want]);
1671
+ return n;
1672
+ }
1673
+ function fileHashes(store, root, prefix = "", out = new Map) {
1674
+ const tree = store.getTree(root);
1675
+ for (const [name, e] of Object.entries(tree.entries)) {
1676
+ const p = prefix ? `${prefix}/${name}` : name;
1677
+ if (e.kind === "tree")
1678
+ fileHashes(store, e.hash, p, out);
1679
+ else
1680
+ out.set(p, e.hash);
1681
+ }
1682
+ return out;
1683
+ }
1684
+ function materializeDiff(store, fromHead, toHead) {
1685
+ const from = fromHead ? fileHashes(store, fromHead) : new Map;
1686
+ const to = toHead ? fileHashes(store, toHead) : new Map;
1687
+ let n = 0;
1688
+ for (const p of from.keys())
1689
+ if (!to.has(p)) {
1690
+ try {
1691
+ unlinkSync4(join6(cwd2, p));
1692
+ n++;
1693
+ } catch {}
1694
+ }
1695
+ for (const [p, h] of to)
1696
+ if (from.get(p) !== h && materialize(store, toHead, p))
1697
+ n++;
1698
+ writeWorkingIndex([...to.keys()]);
1699
+ return n;
1700
+ }
1701
+ function countChanges(store, fromHead, toHead) {
1702
+ const hashes = (head) => {
1703
+ if (!head)
1704
+ return new Map;
1705
+ try {
1706
+ return fileHashes(store, head);
1707
+ } catch {
1708
+ return new Map;
1709
+ }
1710
+ };
1711
+ const from = hashes(fromHead);
1712
+ const to = hashes(toHead);
1713
+ let n = 0;
1714
+ for (const p of from.keys())
1715
+ if (!to.has(p))
1716
+ n++;
1717
+ for (const [p, h] of to)
1718
+ if (from.get(p) !== h)
1719
+ n++;
1720
+ return n;
1721
+ }
1722
+ var indent = (s) => s.split(`
1723
+ `).map((l) => " " + l).join(`
1724
+ `);
1725
+ function printTreeDiff(d) {
1726
+ for (const p of d.added)
1727
+ console.log(`+ added ${p}`);
1728
+ for (const p of d.removed)
1729
+ console.log(`- removed ${p}`);
1730
+ for (const m of d.modified) {
1731
+ console.log(`~ modified ${m.path}`);
1732
+ if (m.hunks)
1733
+ console.log(indent(m.hunks));
1734
+ }
1735
+ if (!d.added.length && !d.removed.length && !d.modified.length)
1736
+ console.log("no changes");
1737
+ }
1738
+ var indexPath = () => join6(solDir2, "index.json");
1739
+ function loadWorkingIndex() {
1740
+ if (!existsSync6(indexPath()))
1741
+ return new Map;
1742
+ try {
1743
+ return new Map(Object.entries(JSON.parse(readFileSync6(indexPath(), "utf8"))));
1744
+ } catch {
1745
+ return new Map;
1746
+ }
1747
+ }
1748
+ function writeWorkingIndex(files) {
1749
+ if (!existsSync6(solDir2))
1750
+ return;
1751
+ const idx = {};
1752
+ for (const f of files) {
1753
+ try {
1754
+ const st = lstatSync3(join6(cwd2, f));
1755
+ idx[f] = [st.mtimeMs, st.size, st.mode & 73 ? 1 : 0];
1756
+ } catch {}
1757
+ }
1758
+ writeFileSync6(indexPath(), JSON.stringify(idx));
1759
+ }
1760
+ function workingChanges(store, head, index = loadWorkingIndex()) {
1761
+ const inHead = new Set(head ? listAll(store, head) : []);
1762
+ const onDisk = new Map;
1763
+ for (const e of walkEntries())
1764
+ onDisk.set(e.rel, e.st);
1765
+ for (const f of forceIncluded())
1766
+ if (!onDisk.has(f)) {
1767
+ try {
1768
+ onDisk.set(f, lstatSync3(join6(cwd2, f)));
1769
+ } catch {}
1770
+ }
1771
+ for (const t of inHead)
1772
+ if (!onDisk.has(t)) {
1773
+ try {
1774
+ onDisk.set(t, lstatSync3(join6(cwd2, t)));
1775
+ } catch {}
1776
+ }
1777
+ const added = [];
1778
+ const modified = [];
1779
+ const removed = [];
1780
+ for (const f of onDisk.keys())
1781
+ if (!inHead.has(f))
1782
+ added.push(f);
1783
+ for (const f of inHead)
1784
+ if (!onDisk.has(f) && index.has(f))
1785
+ removed.push(f);
1786
+ for (const [f, st] of onDisk) {
1787
+ if (!inHead.has(f))
1788
+ continue;
1789
+ const ix = index.get(f);
1790
+ if (ix && !st.isSymbolicLink() && ix[0] === st.mtimeMs && ix[1] === st.size && ix[2] === (st.mode & 73 ? 1 : 0))
1791
+ continue;
1792
+ if (entryKindAt(store, head, f) === "sealed")
1793
+ continue;
1794
+ const stored = readFile(store, head, f);
1795
+ if (stored === SEALED)
1796
+ continue;
1797
+ const abs = join6(cwd2, f);
1798
+ if (st.isSymbolicLink()) {
1799
+ if (stored !== readlinkSync3(abs))
1800
+ modified.push(f);
1801
+ continue;
1802
+ }
1803
+ const buf = readFileSync6(abs);
1804
+ const diskContent = buf.includes(0) ? buf.toString("base64") : buf.toString("utf8");
1805
+ const wantExec = (st.mode & 73) !== 0;
1806
+ const isExec = modeAt(store, head, f) === EXEC_MODE2;
1807
+ if (stored !== diskContent || wantExec !== isExec)
1808
+ modified.push(f);
1809
+ }
1810
+ return { added: added.sort(), modified: modified.sort(), removed: removed.sort() };
1811
+ }
1812
+ function printWorkingDiff(store, base) {
1813
+ const ch = workingChanges(store, base);
1814
+ for (const f of ch.added)
1815
+ console.log(`+ added ${f}`);
1816
+ for (const f of ch.removed)
1817
+ console.log(`- removed ${f}`);
1818
+ for (const f of ch.modified) {
1819
+ console.log(`~ modified ${f}`);
1820
+ const abs = join6(cwd2, f);
1821
+ if (lstatSync3(abs).isSymbolicLink())
1822
+ continue;
1823
+ const buf = readFileSync6(abs);
1824
+ if (buf.includes(0))
1825
+ continue;
1826
+ const stored = readFile(store, base, f);
1827
+ if (typeof stored === "string" && stored !== buf.toString("utf8"))
1828
+ console.log(indent(lineHunks2(stored, buf.toString("utf8"))));
1829
+ }
1830
+ if (!ch.added.length && !ch.removed.length && !ch.modified.length)
1831
+ console.log("no working changes");
1832
+ }
1833
+ function blameFile(ops, store, path) {
1834
+ let tagged = [];
1835
+ const currentLines = () => tagged.map((t) => t.line);
1836
+ const linesOf = (content) => content === "" ? [] : content.replace(/\n$/, "").split(`
1837
+ `);
1838
+ const sameLines = (a, b) => a.length === b.length && a.every((line, i) => line === b[i]);
1839
+ for (const op of ops) {
1840
+ let content;
1841
+ if (op.type === "write" && op.path === path) {
1842
+ content = readFile(store, op.rootAfter, path);
1843
+ } else if (op.type === "delete" && op.path === path) {
1844
+ tagged = [];
1845
+ continue;
1846
+ } else if (op.type === "checkpoint") {
1847
+ content = readFile(store, op.rootAfter, path);
1848
+ if (content === undefined || content === SEALED) {
1849
+ if (tagged.length)
1850
+ tagged = [];
1851
+ continue;
1852
+ }
1853
+ if (sameLines(currentLines(), linesOf(content)))
1854
+ continue;
1855
+ } else {
1856
+ continue;
1857
+ }
1858
+ if (content === undefined || content === SEALED) {
1859
+ tagged = [];
1860
+ continue;
1861
+ }
1862
+ const next = linesOf(content);
1863
+ const keep = lcsMatch(tagged.map((t) => t.line), next);
1864
+ const out = [];
1865
+ for (let j = 0;j < next.length; j++)
1866
+ out.push(keep[j] >= 0 ? { line: next[j], by: tagged[keep[j]].by, seq: tagged[keep[j]].seq } : { line: next[j], by: op.by, seq: op.seq });
1867
+ tagged = out;
1868
+ }
1869
+ return tagged;
1870
+ }
1871
+ function lcsMatch(a, b) {
1872
+ const n = a.length;
1873
+ const m = b.length;
1874
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
1875
+ for (let i2 = n - 1;i2 >= 0; i2--)
1876
+ for (let j2 = m - 1;j2 >= 0; j2--)
1877
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
1878
+ const match = new Array(m).fill(-1);
1879
+ let i = 0;
1880
+ let j = 0;
1881
+ while (i < n && j < m) {
1882
+ if (a[i] === b[j]) {
1883
+ match[j] = i;
1884
+ i++;
1885
+ j++;
1886
+ } else if (dp[i + 1][j] >= dp[i][j + 1])
1887
+ i++;
1888
+ else
1889
+ j++;
1890
+ }
1891
+ return match;
1892
+ }
1893
+ var refsPath = () => join6(solDir2, "refs.json");
1894
+ var saveRefs = (refs) => writeFileSync6(refsPath(), JSON.stringify(refs, null, 2));
1895
+ async function loadRefs(log) {
1896
+ const head = await log.head() ?? "";
1897
+ if (!existsSync6(refsPath())) {
1898
+ const fresh = { current: "main", branches: { main: { head, base: head } }, tags: {} };
1899
+ saveRefs(fresh);
1900
+ return fresh;
1901
+ }
1902
+ const refs = JSON.parse(readFileSync6(refsPath(), "utf8"));
1903
+ if (refs.branches[refs.current])
1904
+ refs.branches[refs.current].head = head;
1905
+ return refs;
1906
+ }
1907
+ function setOpLogHead(head) {
1908
+ const hf = join6(solDir2, "HEAD");
1909
+ const cur = existsSync6(hf) ? JSON.parse(readFileSync6(hf, "utf8")) : { seq: 0 };
1910
+ writeFileSync6(hf, JSON.stringify({ ...cur, head }));
1911
+ }
1912
+ async function persistTree(src, dst, head) {
1913
+ const node = src.get(head);
1914
+ if (!node)
1915
+ return;
1916
+ await dst.put(node);
1917
+ if (node.kind === "tree")
1918
+ for (const e of Object.values(node.entries))
1919
+ await persistTree(src, dst, e.hash);
1920
+ }
1921
+ async function appendCommit(log, head, message, parent, parent2) {
1922
+ await log.append({ seq: await log.seq() + 1, type: "checkpoint", path: "", rootAfter: head, at: Date.now(), by: actor2, message, parent, ...parent2 ? { parent2 } : {} });
1923
+ }
1924
+ async function appendCapture(log, root) {
1925
+ await log.append({ seq: await log.seq() + 1, type: "write", path: "", rootAfter: root, at: Date.now(), by: actor2 });
1926
+ }
1927
+ var keysPath = () => join6(solDir2, "keys.json");
1928
+ function loadKeyRing() {
1929
+ const ring = new KeyRing2;
1930
+ if (existsSync6(keysPath()))
1931
+ ring.load(JSON.parse(readFileSync6(keysPath(), "utf8")));
1932
+ return ring;
1933
+ }
1934
+ function saveKeyRing(ring) {
1935
+ writeFileSync6(keysPath(), JSON.stringify(ring.serialize(), null, 2), { mode: 384 });
1936
+ try {
1937
+ chmodSync3(keysPath(), 384);
1938
+ } catch {}
1939
+ }
1940
+
1941
+ // src/bin/remote.ts
1942
+ import { appendFileSync as appendFileSync5, existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
1943
+ import { join as join7 } from "node:path";
1944
+ var endpoint = (cfg, path) => `${cfg.url.replace(/\/+$/, "")}${path}${path.includes("?") ? "&" : "?"}repo=${encodeURIComponent(cfg.repo)}`;
1945
+ async function call(cfg, token, path, init) {
1946
+ const res = await fetch(endpoint(cfg, path), {
1947
+ ...init,
1948
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
1949
+ });
1950
+ if (!res.ok)
1951
+ throw new Error(`remote ${path} -> ${res.status}: ${(await res.text().catch(() => "")).slice(0, 200)}`);
1952
+ return res.json();
1953
+ }
1954
+ var remoteHead = (cfg, token) => call(cfg, token, "/head");
1955
+ var remoteExport = (cfg, token) => call(cfg, token, "/export");
1956
+ var remotePush = (cfg, token, body) => call(cfg, token, "/push", { method: "POST", body: JSON.stringify(body) });
1957
+ var remotePromote = (cfg, token, branch) => call(cfg, token, "/promote", { method: "POST", body: JSON.stringify({ branch }) });
1958
+ var mrOpen = (cfg, token, body) => call(cfg, token, "/mr", { method: "POST", body: JSON.stringify(body) });
1959
+ var mrList = (cfg, token) => call(cfg, token, "/mrs");
1960
+ var mrDiff = (cfg, token, id) => call(cfg, token, `/mr/diff?id=${id}`);
1961
+ var mrGet = (cfg, token, id) => call(cfg, token, `/mr?id=${id}`);
1962
+ var mrAction = (cfg, token, action, body) => call(cfg, token, `/mr/${action}`, { method: "POST", body: JSON.stringify(body) });
1963
+ var accessGet = (cfg, token) => call(cfg, token, "/access");
1964
+ var accessSet = (cfg, token, body) => call(cfg, token, "/access", { method: "POST", body: JSON.stringify(body) });
1965
+ var forkMeta = (cfg, token, parent) => call(cfg, token, "/fork-meta", { method: "POST", body: JSON.stringify({ parent }) });
1966
+ var forksList = (cfg, token) => call(cfg, token, "/forks");
1967
+ function loadRemote(solDir3) {
1968
+ const p = join7(solDir3, "remote.json");
1969
+ return existsSync7(p) ? JSON.parse(readFileSync7(p, "utf8")) : undefined;
1970
+ }
1971
+ var saveRemote = (solDir3, cfg) => writeFileSync7(join7(solDir3, "remote.json"), JSON.stringify(cfg, null, 2));
1972
+ async function writeBundle(solDir3, bundle, from = 0) {
1973
+ const store = new FileStore2(solDir3);
1974
+ for (const node of bundle.nodes)
1975
+ await store.put(node);
1976
+ const fresh = bundle.ops.filter((o) => o.seq > from);
1977
+ const lines2 = fresh.map((o) => JSON.stringify(o)).join(`
1978
+ `) + (fresh.length ? `
1979
+ ` : "");
1980
+ const opsFile = join7(solDir3, "ops.jsonl");
1981
+ if (from === 0)
1982
+ writeFileSync7(opsFile, lines2);
1983
+ else if (fresh.length)
1984
+ appendFileSync5(opsFile, lines2);
1985
+ writeFileSync7(join7(solDir3, "HEAD"), JSON.stringify({ head: bundle.head, seq: bundle.seq, logTip: bundle.tip }));
1986
+ return fresh.length;
1987
+ }
1988
+
1989
+ // src/bin/runtime.ts
1990
+ import { chmodSync as chmodSync4, existsSync as existsSync8, lstatSync as lstatSync4, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync8, readlinkSync as readlinkSync4, symlinkSync as symlinkSync4, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
1991
+ import { platform } from "node:os";
1992
+ import { dirname as dirname4, join as join8, relative as relative4 } from "node:path";
1993
+ var SYMLINK_MODE3 = 40960;
1994
+ var EXEC_MODE3 = 493;
1995
+ function isolateCommand(command, scratch) {
1996
+ if (platform() === "darwin") {
1997
+ const profile = [
1998
+ "(version 1)",
1999
+ "(allow default)",
2000
+ "(deny network*)",
2001
+ "(deny file-write*)",
2002
+ `(allow file-write* (subpath ${JSON.stringify(scratch)}) (subpath "/private/var/folders") (subpath "/private/tmp") (subpath "/dev") (literal "/dev/null"))`
2003
+ ].join(" ");
2004
+ return ["sandbox-exec", "-p", profile, ...command];
2005
+ }
2006
+ throw new Error(`--isolate not yet wired for platform ${platform()} (macOS sandbox-exec only); omit --isolate to run unconfined`);
2007
+ }
2008
+ function hydrate2(store, head, dir) {
2009
+ if (!head)
2010
+ return 0;
2011
+ let n = 0;
2012
+ for (const p of listAll(store, head)) {
2013
+ const blob = fileAt(store, head, p);
2014
+ if (!blob)
2015
+ continue;
2016
+ const abs = join8(dir, p);
2017
+ mkdirSync6(dirname4(abs), { recursive: true });
2018
+ const mode = modeAt(store, head, p);
2019
+ if (mode === SYMLINK_MODE3) {
2020
+ try {
2021
+ unlinkSync5(abs);
2022
+ } catch {}
2023
+ symlinkSync4(blob.content, abs);
2024
+ } else {
2025
+ writeFileSync8(abs, blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
2026
+ if (mode === EXEC_MODE3) {
2027
+ try {
2028
+ chmodSync4(abs, EXEC_MODE3);
2029
+ } catch {}
2030
+ }
2031
+ }
2032
+ n++;
2033
+ }
2034
+ return n;
2035
+ }
2036
+ function walkDir(dir, base, pats, out = []) {
2037
+ for (const name of readdirSync6(dir)) {
2038
+ const p = join8(dir, name);
2039
+ const rel = relative4(base, p);
2040
+ if (isIgnored(rel, pats))
2041
+ continue;
2042
+ const st = lstatSync4(p);
2043
+ if (st.isSymbolicLink())
2044
+ out.push(rel);
2045
+ else if (st.isDirectory())
2046
+ walkDir(p, base, pats, out);
2047
+ else
2048
+ out.push(rel);
2049
+ }
2050
+ return out;
2051
+ }
2052
+ async function capture(repo, dir, keep = []) {
2053
+ const pats = ignorePatterns();
2054
+ const files = new Set(walkDir(dir, dir, pats));
2055
+ for (const k of keep)
2056
+ if (existsSync8(join8(dir, k)))
2057
+ files.add(k);
2058
+ const written = [];
2059
+ for (const f of files) {
2060
+ const abs = join8(dir, f);
2061
+ const st = lstatSync4(abs);
2062
+ if (st.isSymbolicLink()) {
2063
+ const target = readlinkSync4(abs);
2064
+ if (await repo.readFile(f) !== target) {
2065
+ await repo.writeFile(f, target);
2066
+ await repo.chmod(f, SYMLINK_MODE3);
2067
+ written.push(f);
2068
+ }
2069
+ continue;
2070
+ }
2071
+ const buf = readFileSync8(abs);
2072
+ if (buf.includes(0)) {
2073
+ const cur = await repo.readBytes(f);
2074
+ if (cur === undefined || cur === SEALED || !Buffer.from(cur).equals(buf)) {
2075
+ await repo.writeBytes(f, new Uint8Array(buf));
2076
+ written.push(f);
2077
+ }
2078
+ } else {
2079
+ const disk = buf.toString("utf8");
2080
+ if (await repo.readFile(f) !== disk) {
2081
+ await repo.writeFile(f, disk);
2082
+ written.push(f);
2083
+ }
2084
+ }
2085
+ }
2086
+ const deleted = [];
2087
+ for (const t of await repo.list()) {
2088
+ if (!existsSync8(join8(dir, t))) {
2089
+ await repo.deleteFile(t);
2090
+ deleted.push(t);
2091
+ }
2092
+ }
2093
+ return { written, deleted };
2094
+ }
2095
+
2096
+ // src/bin/sol.ts
2097
+ var CRED_PATH = join9(homedir(), ".sol", "credentials");
2098
+ function tokenClaims(token) {
2099
+ try {
2100
+ return JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString());
2101
+ } catch {
2102
+ return {};
2103
+ }
2104
+ }
2105
+ async function loadStoredToken() {
2106
+ if (!existsSync9(CRED_PATH))
2107
+ return;
2108
+ let creds;
2109
+ try {
2110
+ creds = JSON.parse(readFileSync9(CRED_PATH, "utf8"));
2111
+ } catch {
2112
+ return;
2113
+ }
2114
+ if (!creds.accessToken)
2115
+ return;
2116
+ const exp = tokenClaims(creds.accessToken).exp;
2117
+ if (typeof exp === "number" && exp * 1000 > Date.now() + 30000)
2118
+ return creds.accessToken;
2119
+ if (creds.refreshToken && creds.webUrl) {
2120
+ try {
2121
+ const res = await fetch(`${creds.webUrl}/api/auth/refresh`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ refreshToken: creds.refreshToken }) });
2122
+ if (res.ok) {
2123
+ const r = await res.json();
2124
+ if (r.accessToken) {
2125
+ writeFileSync9(CRED_PATH, JSON.stringify({ ...creds, accessToken: r.accessToken, refreshToken: r.refreshToken ?? creds.refreshToken }, null, 2), { mode: 384 });
2126
+ return r.accessToken;
2127
+ }
2128
+ }
2129
+ } catch {}
2130
+ }
2131
+ return creds.accessToken;
2132
+ }
2133
+ function authHost() {
2134
+ if (process.env.SOL_AUTH)
2135
+ return process.env.SOL_AUTH.replace(/\/+$/, "");
2136
+ try {
2137
+ const w = JSON.parse(readFileSync9(CRED_PATH, "utf8")).webUrl;
2138
+ if (w)
2139
+ return w.replace(/\/+$/, "");
2140
+ } catch {}
2141
+ return "https://auth.midsummer.new";
2142
+ }
2143
+ var DEFAULT_REMOTE_URL = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
2144
+ var remoteUrlArg = (a) => /^https?:\/\//.test(a[0] ?? "") ? [a[0].replace(/\/+$/, ""), a.slice(1)] : [DEFAULT_REMOTE_URL, a];
2145
+ function resolveRemote(solDir3) {
2146
+ const cfg = loadRemote(solDir3);
2147
+ if (!cfg)
2148
+ return;
2149
+ return cfg.url ? cfg : { ...cfg, url: DEFAULT_REMOTE_URL };
2150
+ }
2151
+ async function main() {
2152
+ const argv = process.argv.slice(2);
2153
+ if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
2154
+ console.log("sol 0.1.0");
2155
+ return;
2156
+ }
2157
+ const args = argv.slice(1);
2158
+ const wantsHelp = args.includes("--help") || args.includes("-h");
2159
+ const SUBHELP = {
2160
+ mr: `sol mr open [--from <branch>] [--to <branch>] [--upstream <repo>] -t "title" [-m body]
2161
+ sol mr list | show <id> | review <id> --approve|--request-changes|--comment [-m msg]
2162
+ sol mr comment <id> -m msg [--path f --line N] | check <id> --run -- <cmd> | merge <id> [--force] | close <id>`,
2163
+ fork: "sol fork [<url>] <parent-repo> <new-repo> [dir] \u2014 your own copy (all branches + history + a parent link)",
2164
+ forks: "sol forks \u2014 list the forks of the current repo",
2165
+ access: "sol access [show | public | private | add <userId> <read|write|admin> | remove <userId>]",
2166
+ auth: "sol auth [login [<web-url>] | logout | status | whoami | set-handle <name> | pat [days]]",
2167
+ clone: "sol clone [<url>] <owner>/<repo> [dir] \u2014 default dir = <repo> (url defaults to the hosted Sol)"
2168
+ };
2169
+ if (wantsHelp && argv[0] && SUBHELP[argv[0]]) {
2170
+ console.log(SUBHELP[argv[0]]);
2171
+ return;
2172
+ }
2173
+ const cmd = argv[0] && wantsHelp ? "help" : argv[0];
2174
+ if (!process.env.SOL_TOKEN && new Set(["clone", "push", "pull", "promote", "fork", "forks", "mr", "access"]).has(cmd ?? "")) {
2175
+ const t = await loadStoredToken();
2176
+ if (t)
2177
+ process.env.SOL_TOKEN = t;
2178
+ }
2179
+ const release = new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "pull", "run", "seal"]).has(cmd) && existsSync9(solDir2) ? acquireLock() : undefined;
2180
+ try {
2181
+ switch (cmd) {
2182
+ case "init": {
2183
+ const here = join9(procCwd2, ".sol");
2184
+ if (existsSync9(here))
2185
+ die("already a sol repo: " + procCwd2);
2186
+ if (repoRoot2 && repoRoot2 !== procCwd2 && !args.includes("--force")) {
2187
+ die(`already inside a Sol repo at ${repoRoot2}
2188
+ -> just commit into it: \`sol commit ...\` works from here (sol walks up to find the repo)
2189
+ -> to nest a NEW repo here anyway: \`sol init --force\``);
2190
+ }
2191
+ mkdirSync7(here, { recursive: true });
2192
+ new FileStore(here);
2193
+ console.log(`initialized empty sol repo in ${here}`);
2194
+ break;
2195
+ }
2196
+ case "track":
2197
+ case "add": {
2198
+ if (!existsSync9(solDir2))
2199
+ die("not a sol repo \u2014 run `sol init` first");
2200
+ const files = args.filter((a) => a !== "." && !a.startsWith("-"));
2201
+ if (!files.length) {
2202
+ console.log('every change is auto-captured \u2014 just `sol commit`. Sol has no staging area, so there is nothing to "add".');
2203
+ console.log("use `sol track <ignored-file>` only to force-track a normally-ignored path.");
2204
+ break;
2205
+ }
2206
+ let n = 0;
2207
+ for (const f of files) {
2208
+ const rf = repoRel(f);
2209
+ if (!existsSync9(join9(cwd2, rf))) {
2210
+ console.error("skip (not on disk): " + f);
2211
+ continue;
2212
+ }
2213
+ addInclude(rf);
2214
+ n++;
2215
+ }
2216
+ console.log(`will track ${n} path(s) on the next commit`);
2217
+ break;
2218
+ }
2219
+ case "commit":
2220
+ case "checkpoint": {
2221
+ const { repo, log } = open();
2222
+ let message = "";
2223
+ const paths = [];
2224
+ const wholeTreeOptIn = args.includes("--whole-tree");
2225
+ const ca = args.filter((x) => x !== "--whole-tree" && x !== "--force");
2226
+ const mi = ca.indexOf("-m");
2227
+ if (mi >= 0) {
2228
+ message = ca[mi + 1] ?? "";
2229
+ for (let i = 0;i < ca.length; i++)
2230
+ if (i !== mi && i !== mi + 1)
2231
+ paths.push(ca[i]);
2232
+ } else {
2233
+ message = ca.join(" ");
2234
+ }
2235
+ if (!message)
2236
+ die('commit needs a message: sol commit "what you did" (scoped: sol commit -m "msg" file1 file2)');
2237
+ const parentHead = await repo.head();
2238
+ const mergeHeadPath = join9(solDir2, "MERGE_HEAD");
2239
+ const parent2 = existsSync9(mergeHeadPath) ? readFileSync9(mergeHeadPath, "utf8").trim() || undefined : undefined;
2240
+ let changed = 0;
2241
+ let commitRoot = parentHead;
2242
+ if (paths.length) {
2243
+ for (const p of paths) {
2244
+ const rp = repoRel(p);
2245
+ if (existsSync9(join9(cwd2, rp))) {
2246
+ if (await snapshotFile(repo, rp))
2247
+ changed++;
2248
+ } else if ((await repo.list()).includes(rp)) {
2249
+ await repo.deleteFile(rp);
2250
+ changed++;
2251
+ } else {
2252
+ console.error("skip (not on disk, not tracked): " + p);
2253
+ }
2254
+ }
2255
+ commitRoot = await repo.head();
2256
+ } else {
2257
+ const optedIn = wholeTreeOptIn || process.env.SOL_ALLOW_WHOLE_TREE === "1";
2258
+ if (process.env.SOL_ACTOR && !optedIn) {
2259
+ die(`refusing a whole-tree commit as "${actor2}" \u2014 it would attribute ALL pending files to you.
2260
+ concurrent agents: sol commit -m "${message}" <your files> (or a per-agent SolWorkspace/MCP)
2261
+ if you truly own the whole tree: add --whole-tree or set SOL_ALLOW_WHOLE_TREE=1`);
2262
+ }
2263
+ const snap = await snapshotTree(repo);
2264
+ changed = snap.changed;
2265
+ commitRoot = snap.root;
2266
+ if (!process.env.SOL_ACTOR && changed > 1) {
2267
+ console.error(`note: whole-tree commit attributes all ${changed} pending files to ${actor2}. for concurrent agents, prefer \`sol commit -m "msg" <files>\`.`);
2268
+ }
2269
+ }
2270
+ if (!args.includes("--force")) {
2271
+ const cstore = loadStore();
2272
+ const cd = diffTrees(cstore, parentHead, commitRoot);
2273
+ const changedPaths = paths.length ? paths.map((p) => repoRel(p)) : [...cd.added, ...cd.modified.map((m) => m.path)];
2274
+ for (const p of changedPaths) {
2275
+ const f = fileAt3(cstore, commitRoot, p);
2276
+ if (f && f.encoding !== "base64" && /^<{7}( |$)/m.test(f.content) && /^>{7}( |$)/m.test(f.content)) {
2277
+ die(`refusing to commit unresolved conflict markers in ${p} \u2014 resolve the <<<<<<< / >>>>>>> blocks first (or \`sol commit --force ...\` to override).`);
2278
+ }
2279
+ }
2280
+ }
2281
+ await appendCommit(log, commitRoot, message, parentHead, parent2);
2282
+ if (parent2) {
2283
+ try {
2284
+ unlinkSync6(mergeHeadPath);
2285
+ } catch {}
2286
+ }
2287
+ const refs = await loadRefs(log);
2288
+ saveRefs(refs);
2289
+ writeWorkingIndex(await repo.list());
2290
+ console.log(`commit ${commitRoot.slice(0, 14)} \u2014 ${message} (${changed} file change${changed === 1 ? "" : "s"})`);
2291
+ break;
2292
+ }
2293
+ case "status": {
2294
+ const { repo, log } = open();
2295
+ const refs = existsSync9(refsPath()) ? await loadRefs(log) : undefined;
2296
+ const head = await repo.head();
2297
+ console.log(`repo ${cwd2}`);
2298
+ console.log(`on ${refs ? refs.current : "main"} head ${head.slice(0, 14)} seq ${await log.seq()} actor ${actor2}`);
2299
+ const ch = workingChanges(loadStore(), head);
2300
+ const dirty = ch.added.length + ch.modified.length + ch.removed.length;
2301
+ if (!dirty) {
2302
+ console.log("working tree clean");
2303
+ break;
2304
+ }
2305
+ console.log(`${dirty} uncommitted change(s):`);
2306
+ for (const f of ch.added)
2307
+ console.log(" + " + f);
2308
+ for (const f of ch.modified)
2309
+ console.log(" ~ " + f);
2310
+ for (const f of ch.removed)
2311
+ console.log(" - " + f);
2312
+ break;
2313
+ }
2314
+ case "ls": {
2315
+ for (const f of await open().repo.list())
2316
+ console.log(f);
2317
+ break;
2318
+ }
2319
+ case "cat": {
2320
+ const path = args[0] || die("cat needs a path");
2321
+ const repo = open().repo;
2322
+ const c = await repo.readFile(path);
2323
+ if (c === undefined)
2324
+ die("no such tracked path: " + path);
2325
+ if (c === SEALED2) {
2326
+ const opened = await new SealedClient(repo, loadKeyRing()).open(path, actor2);
2327
+ process.stdout.write(opened === undefined || opened === UNREADABLE ? `<<sealed \u2014 you are not a recipient>>
2328
+ ` : opened);
2329
+ break;
2330
+ }
2331
+ process.stdout.write(c);
2332
+ break;
2333
+ }
2334
+ case "log": {
2335
+ const { log } = open();
2336
+ const ops = await log.history();
2337
+ if (args.includes("--all") || args.includes("--ops")) {
2338
+ if (!ops.length)
2339
+ console.log("(no history yet)");
2340
+ for (const line of formatLog(ops))
2341
+ console.log(line.replace(/\bcheckpoint\b/, "commit"));
2342
+ break;
2343
+ }
2344
+ const store = loadStore();
2345
+ const byRoot = new Map;
2346
+ for (const o of ops)
2347
+ if (o.type === "checkpoint")
2348
+ byRoot.set(o.rootAfter, o);
2349
+ const live = await log.head() ?? "";
2350
+ const refArg = args.find((a) => !a.startsWith("-"));
2351
+ const lrefs = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : null;
2352
+ let tip = live;
2353
+ if (refArg) {
2354
+ tip = lrefs?.branches[refArg]?.head ?? refArg;
2355
+ } else if (lrefs && !byRoot.has(tip)) {
2356
+ const stored = lrefs.branches[lrefs.current]?.head;
2357
+ if (stored && byRoot.has(stored))
2358
+ tip = stored;
2359
+ }
2360
+ const chain = [];
2361
+ const seen = new Set;
2362
+ let cur = tip;
2363
+ while (cur && byRoot.has(cur) && !seen.has(cur)) {
2364
+ seen.add(cur);
2365
+ const c = byRoot.get(cur);
2366
+ chain.push(c);
2367
+ cur = c.parent;
2368
+ }
2369
+ if (!chain.length) {
2370
+ console.log(ops.length ? '(no commits on this branch yet \u2014 run `sol commit "msg"`)' : "(no history yet)");
2371
+ }
2372
+ for (const c of chain) {
2373
+ const n = countChanges(store, c.parent ?? "", c.rootAfter);
2374
+ const tag = c.parent2 ? ` [merge ${c.parent2.slice(0, 8)}]` : "";
2375
+ console.log(`${c.rootAfter.slice(0, 14)} ${(c.by ?? "?").padEnd(14)} ${c.message ?? ""} (${n} change${n === 1 ? "" : "s"})${tag}`);
2376
+ }
2377
+ const wc = workingChanges(store, live);
2378
+ const dirty = wc.added.length + wc.modified.length + wc.removed.length;
2379
+ if (dirty)
2380
+ console.log(`(working: ${dirty} uncommitted change(s))`);
2381
+ break;
2382
+ }
2383
+ case "rm": {
2384
+ const path = args[0] || die("rm needs a path");
2385
+ let onDisk = false;
2386
+ try {
2387
+ unlinkSync6(join9(cwd2, path));
2388
+ onDisk = true;
2389
+ } catch {}
2390
+ if (onDisk) {
2391
+ console.log(`removed ${path} from disk (commit to record the deletion)`);
2392
+ break;
2393
+ }
2394
+ const { repo } = open();
2395
+ if (!(await repo.list()).includes(path))
2396
+ die("not tracked and not on disk: " + path);
2397
+ await repo.deleteFile(path);
2398
+ console.log("removed (tracked-only) " + path);
2399
+ break;
2400
+ }
2401
+ case "diff": {
2402
+ const { log } = open();
2403
+ const store = loadStore();
2404
+ if (args.length >= 2) {
2405
+ printTreeDiff(diffTrees(store, (await resolveRef(log, args[0])).head, (await resolveRef(log, args[1])).head));
2406
+ } else {
2407
+ printWorkingDiff(store, (await resolveRef(log, args[0])).head);
2408
+ }
2409
+ break;
2410
+ }
2411
+ case "restore":
2412
+ case "checkout": {
2413
+ const { log } = open();
2414
+ const store = loadStore();
2415
+ let from = "head";
2416
+ const rest = [];
2417
+ for (let i = 0;i < args.length; i++) {
2418
+ if (args[i] === "--from")
2419
+ from = args[++i];
2420
+ else if (args[i] !== "--all")
2421
+ rest.push(args[i]);
2422
+ }
2423
+ const { head } = await resolveRef(log, from);
2424
+ if (rest.length === 0) {
2425
+ const n = materializeTree(store, head);
2426
+ console.log(`restored the working tree to ${from === "head" ? "HEAD" : from} (${n} change(s))`);
2427
+ } else {
2428
+ let n = 0;
2429
+ for (const p of rest) {
2430
+ if (materialize(store, head, p))
2431
+ n++;
2432
+ else
2433
+ console.error(`not in ${from}: ${p}`);
2434
+ }
2435
+ console.log(`restored ${n} file(s) from ${from === "head" ? "HEAD" : from}`);
2436
+ }
2437
+ break;
2438
+ }
2439
+ case "show": {
2440
+ const { log } = open();
2441
+ const store = loadStore();
2442
+ const ops = await log.history();
2443
+ const { op } = await resolveRef(log, args[0]);
2444
+ if (!op)
2445
+ die("nothing to show (empty repo)");
2446
+ const idx = ops.findIndex((o) => o.seq === op.seq);
2447
+ let prev = emptyRoot3(store);
2448
+ if (op.parent !== undefined) {
2449
+ prev = op.parent;
2450
+ } else if (op.type === "checkpoint") {
2451
+ for (let i = idx - 1;i >= 0; i--)
2452
+ if (ops[i].type === "checkpoint") {
2453
+ prev = ops[i].rootAfter;
2454
+ break;
2455
+ }
2456
+ } else if (idx > 0) {
2457
+ prev = ops[idx - 1].rootAfter;
2458
+ }
2459
+ const kind = op.type === "checkpoint" ? "commit" : op.type;
2460
+ console.log(`${kind} ${op.rootAfter.slice(0, 14)} seq ${op.seq} by ${op.by ?? "?"}${op.message ? `
2461
+
2462
+ ` + op.message : ""}
2463
+ `);
2464
+ printTreeDiff(diffTrees(store, prev, op.rootAfter));
2465
+ break;
2466
+ }
2467
+ case "blame": {
2468
+ const path = args[0] || die("blame needs a path");
2469
+ const ops = await open().repo.history();
2470
+ const lines2 = blameFile(ops, loadStore(), path);
2471
+ if (!lines2.length)
2472
+ die("no history for " + path);
2473
+ for (const t of lines2)
2474
+ console.log(`${String(t.seq).padStart(4)} ${(t.by ?? "?").padEnd(16)} ${t.line}`);
2475
+ break;
2476
+ }
2477
+ case "fsck": {
2478
+ const { log } = open();
2479
+ const store = loadStore();
2480
+ const head = await log.head();
2481
+ const ops = await log.history();
2482
+ let chainOk = true;
2483
+ let chainErr = "";
2484
+ try {
2485
+ for (let i = 0;i < ops.length; i++)
2486
+ if (ops[i].seq !== i + 1)
2487
+ throw new Error("gap before seq " + (i + 1));
2488
+ verifyChain(ops);
2489
+ } catch (e) {
2490
+ chainOk = false;
2491
+ chainErr = String(e?.message ?? e);
2492
+ }
2493
+ const missing = [];
2494
+ if (head) {
2495
+ const seen = new Set;
2496
+ const walk = (h) => {
2497
+ if (seen.has(h))
2498
+ return;
2499
+ seen.add(h);
2500
+ const node = store.get(h);
2501
+ if (!node) {
2502
+ missing.push(h);
2503
+ return;
2504
+ }
2505
+ if (node.kind === "tree")
2506
+ for (const e of Object.values(node.entries))
2507
+ walk(e.hash);
2508
+ };
2509
+ walk(head);
2510
+ }
2511
+ const ok = chainOk && missing.length === 0;
2512
+ console.log(`fsck: ${ok ? "OK" : "PROBLEMS FOUND"}`);
2513
+ console.log(` op-log: ${ops.length} op(s), chain ${chainOk ? "valid + contiguous" : "BROKEN \u2014 " + chainErr}`);
2514
+ console.log(` head: ${head ? head.slice(0, 16) : "(empty)"}, ${missing.length} missing object(s)`);
2515
+ for (const mh of missing)
2516
+ console.log(" missing " + mh);
2517
+ process.exitCode = ok ? 0 : 1;
2518
+ break;
2519
+ }
2520
+ case "gc": {
2521
+ const { log } = open();
2522
+ const store = loadStore();
2523
+ const ops = await log.history();
2524
+ const reachable = new Set;
2525
+ const walk = (h) => {
2526
+ if (reachable.has(h))
2527
+ return;
2528
+ const node = store.get(h);
2529
+ if (!node)
2530
+ return;
2531
+ reachable.add(h);
2532
+ if (node.kind === "tree")
2533
+ for (const e of Object.values(node.entries))
2534
+ walk(e.hash);
2535
+ };
2536
+ for (const op of ops)
2537
+ walk(op.rootAfter);
2538
+ const objDir = join9(solDir2, "objects");
2539
+ let removed = 0;
2540
+ for (const name of readdirSync7(objDir)) {
2541
+ if (name.endsWith(".tmp") || !reachable.has(name)) {
2542
+ unlinkSync6(join9(objDir, name));
2543
+ removed++;
2544
+ }
2545
+ }
2546
+ console.log(`gc: kept ${reachable.size} object(s), removed ${removed} unreachable`);
2547
+ break;
2548
+ }
2549
+ case "ignore": {
2550
+ const pat = args.join(" ").trim();
2551
+ if (!pat) {
2552
+ for (const p of ignorePatterns2())
2553
+ console.log(p);
2554
+ break;
2555
+ }
2556
+ const f = join9(cwd2, ".solignore");
2557
+ const lead = existsSync9(f) && !readFileSync9(f, "utf8").endsWith(`
2558
+ `) ? `
2559
+ ` : "";
2560
+ appendFileSync4(f, lead + pat + `
2561
+ `);
2562
+ console.log("ignoring: " + pat);
2563
+ break;
2564
+ }
2565
+ case "seal": {
2566
+ const { repo } = open();
2567
+ const path = args[0] || die("usage: sol seal <path> [recipient...]");
2568
+ const recipients = args.slice(1);
2569
+ if (!recipients.includes(actor2))
2570
+ recipients.push(actor2);
2571
+ let content = await repo.readFile(path);
2572
+ if (content === SEALED2)
2573
+ die("already sealed: " + path);
2574
+ if (content === undefined) {
2575
+ const abs = join9(cwd2, path);
2576
+ if (!existsSync9(abs))
2577
+ die("no such file: " + path);
2578
+ content = readFileSync9(abs, "utf8");
2579
+ }
2580
+ const ring = loadKeyRing();
2581
+ await new SealedClient(repo, ring).seal(path, content, recipients);
2582
+ saveKeyRing(ring);
2583
+ console.log(`sealed ${path} to ${recipients.join(", ")} \u2014 content is now host-blind ciphertext (the keystore stays local)`);
2584
+ break;
2585
+ }
2586
+ case "branch":
2587
+ case "branches": {
2588
+ const { log } = open();
2589
+ const refs = await loadRefs(log);
2590
+ if (cmd === "branches" || !args[0]) {
2591
+ for (const [n, b] of Object.entries(refs.branches))
2592
+ console.log(`${n === refs.current ? "* " : " "}${n} ${(b.head || "empty").slice(0, 12)}`);
2593
+ break;
2594
+ }
2595
+ const name = args[0];
2596
+ if (refs.branches[name] || refs.tags[name])
2597
+ die("already exists: " + name);
2598
+ let head = await log.head() ?? "";
2599
+ if (args[1]) {
2600
+ const resolved = refs.branches[args[1]]?.head ?? refs.tags[args[1]] ?? (args[1].startsWith("h_") ? args[1] : "");
2601
+ if (!resolved)
2602
+ die(`cannot resolve ref '${args[1]}' \u2014 use a branch/tag name or a commit hash`);
2603
+ head = resolved;
2604
+ }
2605
+ refs.branches[name] = { head, base: head };
2606
+ saveRefs(refs);
2607
+ console.log(`branch ${name} at ${(head || "empty").slice(0, 12)}`);
2608
+ break;
2609
+ }
2610
+ case "tag": {
2611
+ const { log } = open();
2612
+ const refs = await loadRefs(log);
2613
+ if (!args[0]) {
2614
+ for (const [n, h] of Object.entries(refs.tags))
2615
+ console.log(`${n} ${h.slice(0, 12)}`);
2616
+ break;
2617
+ }
2618
+ const name = args[0];
2619
+ if (refs.branches[name] || refs.tags[name])
2620
+ die("already exists: " + name);
2621
+ refs.tags[name] = await log.head() ?? "";
2622
+ saveRefs(refs);
2623
+ console.log(`tag ${name} at ${(refs.tags[name] || "empty").slice(0, 12)}`);
2624
+ break;
2625
+ }
2626
+ case "switch": {
2627
+ const { repo, log } = open();
2628
+ const name = args[0] || die("switch needs a branch name");
2629
+ const refs = await loadRefs(log);
2630
+ const target = refs.branches[name];
2631
+ if (!target)
2632
+ die(`no such branch: ${name} (run \`sol branch ${name}\` to create it)`);
2633
+ const fromHead = await repo.head();
2634
+ const wc = workingChanges(loadStore(), fromHead);
2635
+ if (wc.added.length + wc.modified.length + wc.removed.length > 0) {
2636
+ die('you have uncommitted changes \u2014 commit them first (`sol commit "msg"`), then switch. your work is safe on disk.');
2637
+ }
2638
+ setOpLogHead(target.head);
2639
+ refs.current = name;
2640
+ saveRefs(refs);
2641
+ const n = materializeDiff(loadStore(), fromHead, target.head);
2642
+ console.log(`switched to ${name} (${(target.head || "empty").slice(0, 12)}); working tree = ${n} file(s)`);
2643
+ break;
2644
+ }
2645
+ case "merge": {
2646
+ const { repo, log } = open();
2647
+ const name = args[0] || die("merge needs a branch name");
2648
+ const refs = await loadRefs(log);
2649
+ const other = refs.branches[name];
2650
+ if (!other)
2651
+ die("no such branch: " + name);
2652
+ if (name === refs.current)
2653
+ die("cannot merge a branch into itself");
2654
+ const mc = workingChanges(loadStore(), await repo.head());
2655
+ if (mc.added.length + mc.modified.length + mc.removed.length > 0) {
2656
+ die("you have uncommitted changes \u2014 commit them first, then merge.");
2657
+ }
2658
+ const ours = await log.head() ?? "";
2659
+ const store = loadStore();
2660
+ const result = merge({ store }, other.base, ours, other.head);
2661
+ if (result.conflicts.length) {
2662
+ materializeTree(store, result.head);
2663
+ writeFileSync9(join9(solDir2, "MERGE_HEAD"), other.head);
2664
+ console.log(`merge ${name} -> ${result.conflicts.length} conflict(s), left in your working tree (uncommitted):`);
2665
+ for (const c of result.conflicts)
2666
+ console.log(" " + c.path);
2667
+ console.log("resolve the <<<<<<< markers, then `sol commit`");
2668
+ process.exitCode = 1;
2669
+ break;
2670
+ }
2671
+ await persistTree(store, new FileStore(solDir2), result.head);
2672
+ await appendCommit(log, result.head, `merge ${name} into ${refs.current}`, ours, other.head);
2673
+ refs.branches[refs.current].head = result.head;
2674
+ refs.branches[name].base = other.head;
2675
+ saveRefs(refs);
2676
+ setOpLogHead(result.head);
2677
+ materializeTree(store, result.head);
2678
+ console.log(`merged ${name} into ${refs.current} cleanly -> ${result.head.slice(0, 12)}`);
2679
+ break;
2680
+ }
2681
+ case "remote": {
2682
+ if (!existsSync9(solDir2))
2683
+ die("not a sol repo");
2684
+ if (args[0]) {
2685
+ const repoName = args[1] || die("usage: sol remote <url> <repo>");
2686
+ saveRemote(solDir2, { url: args[0], repo: repoName });
2687
+ console.log(`remote set: ${args[0]} (repo ${repoName})`);
2688
+ break;
2689
+ }
2690
+ const cfg = loadRemote(solDir2);
2691
+ console.log(cfg ? `${cfg.url} (repo ${cfg.repo}${cfg.forkParent ? `, fork of ${cfg.forkParent}` : ""})` : "(no remote \u2014 `sol remote <url> <repo>` or `sol clone`)");
2692
+ break;
2693
+ }
2694
+ case "auth": {
2695
+ const sub = args[0];
2696
+ if (sub === "login") {
2697
+ const webUrl = (args[1] || "https://auth.midsummer.new").replace(/\/+$/, "");
2698
+ const osLabel = { darwin: "macOS", win32: "Windows", linux: "Linux" }[platform2()] ?? platform2();
2699
+ const deviceLabel = `${osLabel} \xB7 ${hostname()}`;
2700
+ const codeRes = await fetch(`${webUrl}/api/auth/device/code`, {
2701
+ method: "POST",
2702
+ headers: { "content-type": "application/json" },
2703
+ body: JSON.stringify({ deviceLabel })
2704
+ });
2705
+ if (!codeRes.ok)
2706
+ die(`could not reach ${webUrl} (device/code -> ${codeRes.status})`);
2707
+ const { code, authorizeUrl, verification_uri_complete, interval } = await codeRes.json();
2708
+ const openUrl = verification_uri_complete || `${authorizeUrl || `${webUrl}/cli/authorize`}?code=${encodeURIComponent(code)}`;
2709
+ console.log(`
2710
+ To sign in, open: ${openUrl}`);
2711
+ console.log(` (code, for reference: ${code})
2712
+ `);
2713
+ console.log(" Waiting for approval...");
2714
+ const deadline = Date.now() + 5 * 60 * 1000;
2715
+ let tokens;
2716
+ while (Date.now() < deadline) {
2717
+ await new Promise((r) => setTimeout(r, (interval || 2) * 1000));
2718
+ const pr = await (await fetch(`${webUrl}/api/auth/device/poll`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ code }) })).json();
2719
+ if (pr.status === "approved" && pr.accessToken) {
2720
+ tokens = { accessToken: pr.accessToken, refreshToken: pr.refreshToken ?? "" };
2721
+ break;
2722
+ }
2723
+ if (pr.status === "expired")
2724
+ die("the code expired \u2014 run `sol auth login` again");
2725
+ }
2726
+ if (!tokens)
2727
+ die("timed out waiting for approval");
2728
+ mkdirSync7(dirname5(CRED_PATH), { recursive: true });
2729
+ writeFileSync9(CRED_PATH, JSON.stringify({ webUrl, ...tokens }, null, 2), { mode: 384 });
2730
+ const c = tokenClaims(tokens.accessToken);
2731
+ console.log(`
2732
+ Logged in as ${c.handle ? `@${c.handle}` : c.email || "user"}.`);
2733
+ if (!c.handle)
2734
+ console.log(" (no handle yet \u2014 set one in the web app to get your <handle>/<repo> namespace)");
2735
+ } else if (sub === "logout") {
2736
+ if (existsSync9(CRED_PATH))
2737
+ unlinkSync6(CRED_PATH);
2738
+ console.log("logged out");
2739
+ } else if (sub === "status" || !sub) {
2740
+ if (process.env.SOL_TOKEN) {
2741
+ const c2 = tokenClaims(process.env.SOL_TOKEN);
2742
+ console.log(`authenticated via SOL_TOKEN (env)${c2.handle ? ` as @${c2.handle}` : ""}`);
2743
+ break;
2744
+ }
2745
+ if (!existsSync9(CRED_PATH)) {
2746
+ console.log("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
2747
+ break;
2748
+ }
2749
+ const creds = JSON.parse(readFileSync9(CRED_PATH, "utf8"));
2750
+ const c = tokenClaims(creds.accessToken || "");
2751
+ const stale = typeof c.exp === "number" && c.exp * 1000 < Date.now();
2752
+ console.log(`logged in as ${c.handle ? `@${c.handle}` : c.email || "user"} via ${creds.webUrl}${stale ? " (token stale \u2014 refreshes on next use)" : ""}`);
2753
+ } else if (sub === "whoami") {
2754
+ const token = process.env.SOL_TOKEN || await loadStoredToken();
2755
+ if (!token)
2756
+ die("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
2757
+ const res = await fetch(`${authHost()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
2758
+ if (res.status === 401)
2759
+ die("not logged in \u2014 your session expired; run `sol auth login` again");
2760
+ if (!res.ok)
2761
+ die(`could not reach ${authHost()} (me -> ${res.status})`);
2762
+ const me = await res.json();
2763
+ console.log(me.handle ? `signed in as @${me.handle} ${me.email ?? ""}`.trimEnd() : `signed in${me.email ? ` as ${me.email}` : ""} \u2014 no handle yet (\`sol auth set-handle <name>\`)`);
2764
+ } else if (sub === "set-handle") {
2765
+ const want = args[1] || die("usage: sol auth set-handle <name>");
2766
+ const handle = want.toLowerCase();
2767
+ const token = process.env.SOL_TOKEN || await loadStoredToken();
2768
+ if (!token)
2769
+ die("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
2770
+ let hadHandle = false;
2771
+ try {
2772
+ const meRes = await fetch(`${authHost()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
2773
+ if (meRes.ok)
2774
+ hadHandle = Boolean((await meRes.json()).handle);
2775
+ } catch {}
2776
+ const res = await fetch(`${authHost()}/api/auth/handle`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ handle }) });
2777
+ if (res.status === 401)
2778
+ die("not logged in \u2014 your session expired; run `sol auth login` again");
2779
+ if (!res.ok) {
2780
+ let msg = `could not set handle (${res.status})`;
2781
+ try {
2782
+ const err = await res.json();
2783
+ if (err.error?.message)
2784
+ msg = err.error.message;
2785
+ } catch {}
2786
+ die(msg);
2787
+ }
2788
+ const out = await res.json();
2789
+ if (hadHandle)
2790
+ console.log("heads-up: changing your handle re-namespaces your repos under the new <handle>/<repo>.");
2791
+ console.log(`handle set to @${out.handle}`);
2792
+ } else if (sub === "pat") {
2793
+ if (!existsSync9(CRED_PATH))
2794
+ die("run `sol auth login` first");
2795
+ const creds = JSON.parse(readFileSync9(CRED_PATH, "utf8"));
2796
+ const token = await loadStoredToken();
2797
+ if (!token || !creds.webUrl)
2798
+ die("run `sol auth login` first");
2799
+ const patAction = args[1];
2800
+ if (patAction === "list") {
2801
+ const res = await fetch(`${creds.webUrl}/api/auth/pat`, { headers: { authorization: `Bearer ${token}` } });
2802
+ if (!res.ok)
2803
+ die(`could not list PATs (${res.status})`);
2804
+ const { tokens } = await res.json();
2805
+ if (!tokens.length) {
2806
+ console.log("(no personal access tokens \u2014 `sol auth pat [days] [name]` to mint one)");
2807
+ break;
2808
+ }
2809
+ for (const t of tokens) {
2810
+ const state = t.revokedAt ? "revoked" : t.expiresAt < Date.now() ? "expired" : "active";
2811
+ const used = t.lastUsedAt ? `, last used ${new Date(t.lastUsedAt).toISOString().slice(0, 10)}` : "";
2812
+ console.log(` ${t.id} ${t.name.padEnd(20)} [${state}] expires ${new Date(t.expiresAt).toISOString().slice(0, 10)}${used}`);
2813
+ }
2814
+ } else if (patAction === "revoke") {
2815
+ const id = args[2] || die("usage: sol auth pat revoke <id>");
2816
+ const res = await fetch(`${creds.webUrl}/api/auth/pat`, { method: "DELETE", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ id }) });
2817
+ if (res.status === 404)
2818
+ die(`no such PAT (or not yours): ${id}`);
2819
+ if (!res.ok)
2820
+ die(`could not revoke PAT (${res.status})`);
2821
+ const out = await res.json();
2822
+ console.log(`revoked PAT ${id}${out.edgePropagated === false ? " (DB only \u2014 edge KV not configured; valid at the edge until expiry)" : ""}`);
2823
+ } else {
2824
+ const days = Number(patAction) || 90;
2825
+ const name = (Number(patAction) ? args[2] : patAction) || undefined;
2826
+ const res = await fetch(`${creds.webUrl}/api/auth/pat`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ days, ...name ? { name } : {} }) });
2827
+ if (!res.ok)
2828
+ die(`could not mint a PAT (${res.status})`);
2829
+ const out = await res.json();
2830
+ console.log(`
2831
+ Personal Access Token "${out.name}" (id ${out.id}, expires in ${days} days) \u2014 store it now, it is not shown again:
2832
+ `);
2833
+ console.log(` ${out.token}
2834
+ `);
2835
+ console.log(" Use it for git or CI (no device flow needed):");
2836
+ console.log(" git clone https://x:<token>@<sol-backend>/git/<owner>/<repo>");
2837
+ console.log(" export SOL_TOKEN=<token>");
2838
+ console.log(`
2839
+ Manage: sol auth pat list | sol auth pat revoke ${out.id}`);
2840
+ }
2841
+ } else {
2842
+ die("usage: sol auth [login [<web-url>] | logout | status | whoami | set-handle <name> | pat [days] [name] | pat list | pat revoke <id>]");
2843
+ }
2844
+ break;
2845
+ }
2846
+ case "clone": {
2847
+ const [url, rest] = remoteUrlArg(args);
2848
+ const repoName = rest[0] || die("usage: sol clone [<url>] <owner>/<repo> [dir]");
2849
+ const token = process.env.SOL_TOKEN || die("set SOL_TOKEN to the backend bearer token");
2850
+ const target = resolve3(cwd2, rest[1] || repoName.split("/").pop() || repoName);
2851
+ const fdir = join9(target, ".sol");
2852
+ if (existsSync9(fdir))
2853
+ die("already a sol repo: " + target);
2854
+ const cfg = { url, repo: repoName };
2855
+ const bundle = await remoteExport(cfg, token);
2856
+ mkdirSync7(fdir, { recursive: true });
2857
+ await writeBundle(fdir, bundle, 0);
2858
+ saveRemote(fdir, cfg);
2859
+ const srvRefs = bundle.refs ?? { branches: { main: bundle.head ?? "" }, production: "main" };
2860
+ const checkout = bundle.checkout ?? { branch: srvRefs.production || "main", head: srvRefs.branches[srvRefs.production] ?? bundle.head };
2861
+ const onBranch = checkout.branch;
2862
+ const checkoutHead = checkout.head ?? bundle.head ?? "";
2863
+ const cloneBranches = {};
2864
+ for (const [name, h] of Object.entries(srvRefs.branches))
2865
+ cloneBranches[name] = { head: h, base: h, remote: h };
2866
+ if (!cloneBranches[onBranch])
2867
+ cloneBranches[onBranch] = { head: checkoutHead, base: checkoutHead, remote: checkoutHead };
2868
+ writeFileSync9(join9(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
2869
+ writeFileSync9(join9(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
2870
+ const store = new Store2;
2871
+ for (const node of bundle.nodes)
2872
+ store.put(node);
2873
+ let n = 0;
2874
+ if (checkoutHead) {
2875
+ for (const f of listAll3(store, checkoutHead)) {
2876
+ const abs = resolve3(target, f);
2877
+ if (abs !== target && !abs.startsWith(target + sep3))
2878
+ continue;
2879
+ const blob = fileAt3(store, checkoutHead, f);
2880
+ if (!blob)
2881
+ continue;
2882
+ mkdirSync7(dirname5(abs), { recursive: true });
2883
+ writeFileSync9(abs, blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
2884
+ n++;
2885
+ }
2886
+ }
2887
+ console.log(`cloned ${repoName} -> ${rest[1] || repoName} (${bundle.ops.length} ops, ${Object.keys(cloneBranches).length} branch(es), ${n} files, on ${onBranch})`);
2888
+ break;
2889
+ }
2890
+ case "push": {
2891
+ const { log } = open();
2892
+ const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
2893
+ const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
2894
+ const rh = await remoteHead(cfg, token);
2895
+ const ops = await log.history();
2896
+ const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
2897
+ const localTip = await log.logTip();
2898
+ const localRefs = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : undefined;
2899
+ const branch = localRefs?.current ?? "main";
2900
+ const branchHead = await log.head() ?? "";
2901
+ if (localSeq === rh.seq && localTip === rh.tip) {
2902
+ console.log("everything up to date");
2903
+ break;
2904
+ }
2905
+ if (rh.seq > 0) {
2906
+ const atRemote = ops.find((o) => o.seq === rh.seq);
2907
+ if (!atRemote || atRemote.entryHash !== rh.tip)
2908
+ die("push rejected \u2014 the remote has changes you don't have (run `sol pull` first)");
2909
+ }
2910
+ if (localSeq <= rh.seq) {
2911
+ console.log("nothing to push");
2912
+ break;
2913
+ }
2914
+ let res;
2915
+ try {
2916
+ res = await remotePush(cfg, token, { nodes: allLocalNodes(), ops: ops.filter((o) => o.seq > rh.seq), branch, head: branchHead, expectedHead: localRefs?.branches[branch]?.remote });
2917
+ } catch (e) {
2918
+ if (String(e?.message ?? "").includes("409"))
2919
+ die(`push rejected \u2014 branch '${branch}' moved on the remote. run \`sol pull\`, then push again.`);
2920
+ throw e;
2921
+ }
2922
+ const canon = await remoteExport(cfg, token);
2923
+ await writeBundle(solDir2, canon, 0);
2924
+ if (existsSync9(refsPath())) {
2925
+ const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
2926
+ if (canon.refs) {
2927
+ for (const [name, h] of Object.entries(canon.refs.branches)) {
2928
+ refs.branches[name] = { head: refs.branches[name]?.head ?? h, base: refs.branches[name]?.base ?? h, remote: h };
2929
+ }
2930
+ }
2931
+ if (refs.branches[branch]) {
2932
+ refs.branches[branch].head = res.head ?? branchHead;
2933
+ refs.branches[branch].remote = res.head ?? branchHead;
2934
+ }
2935
+ saveRefs(refs);
2936
+ }
2937
+ setOpLogHead(res.head ?? branchHead);
2938
+ const prod = res.refs?.production;
2939
+ console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(res.head ?? "").slice(0, 12)})${prod ? `; production=${prod}` : ""}`);
2940
+ break;
2941
+ }
2942
+ case "pull": {
2943
+ const { log } = open();
2944
+ const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
2945
+ const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
2946
+ const bundle = await remoteExport(cfg, token);
2947
+ const ops = await log.history();
2948
+ const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
2949
+ const localTip = await log.logTip();
2950
+ const curBranch = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : bundle.refs?.production || "main";
2951
+ const remoteCurHead = bundle.refs?.branches?.[curBranch] ?? bundle.head ?? "";
2952
+ if (bundle.seq === localSeq && bundle.tip === localTip) {
2953
+ if (bundle.refs && existsSync9(refsPath())) {
2954
+ const lr = JSON.parse(readFileSync9(refsPath(), "utf8"));
2955
+ const before = lr.branches[lr.current]?.head;
2956
+ for (const [name, h] of Object.entries(bundle.refs.branches))
2957
+ lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
2958
+ saveRefs(lr);
2959
+ const after = lr.branches[lr.current]?.head;
2960
+ if (after && after !== before) {
2961
+ setOpLogHead(after);
2962
+ materializeDiff(loadStore(), before ?? "", after);
2963
+ console.log(`updated ${lr.current} -> ${after.slice(0, 12)} (a ref moved on the remote \u2014 merge/promote)`);
2964
+ break;
2965
+ }
2966
+ }
2967
+ console.log("already up to date");
2968
+ break;
2969
+ }
2970
+ let n = 0;
2971
+ while (n < ops.length && n < bundle.ops.length && ops[n].entryHash === bundle.ops[n].entryHash)
2972
+ n++;
2973
+ if (n === bundle.ops.length) {
2974
+ console.log("up to date (local is ahead \u2014 run `sol push`)");
2975
+ break;
2976
+ }
2977
+ const syncRefHead = (h) => {
2978
+ if (!existsSync9(refsPath()))
2979
+ return;
2980
+ const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
2981
+ if (refs.branches[refs.current])
2982
+ refs.branches[refs.current].head = h;
2983
+ saveRefs(refs);
2984
+ };
2985
+ {
2986
+ const wc = workingChanges(loadStore(), await log.head() ?? "");
2987
+ const dirty = [...wc.added.map((f) => "+ " + f), ...wc.modified.map((f) => "~ " + f), ...wc.removed.map((f) => "- " + f)];
2988
+ if (dirty.length)
2989
+ die(`you have uncommitted changes \u2014 commit (\`sol commit\`) or discard them before pulling:
2990
+ ${dirty.join(`
2991
+ `)}`);
2992
+ }
2993
+ if (n === ops.length) {
2994
+ const added = await writeBundle(solDir2, bundle, localSeq);
2995
+ syncRefHead(remoteCurHead);
2996
+ setOpLogHead(remoteCurHead);
2997
+ materializeTree(loadStore(), remoteCurHead);
2998
+ if (bundle.refs && existsSync9(refsPath())) {
2999
+ const lr = JSON.parse(readFileSync9(refsPath(), "utf8"));
3000
+ for (const [name, h] of Object.entries(bundle.refs.branches))
3001
+ lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
3002
+ saveRefs(lr);
3003
+ }
3004
+ console.log(`pulled ${added} new op(s) -> ${curBranch} now at ${(remoteCurHead || "").slice(0, 12)}`);
3005
+ break;
3006
+ }
3007
+ const store = loadStore();
3008
+ for (const node of bundle.nodes)
3009
+ store.put(node);
3010
+ const base = n > 0 ? ops[n - 1].rootAfter : emptyRoot3(store);
3011
+ const localHead = await log.head() ?? "";
3012
+ const remoteHead2 = remoteCurHead;
3013
+ const pathAuthor = new Map;
3014
+ for (const op of ops.slice(n))
3015
+ if (op.by && op.path)
3016
+ pathAuthor.set(op.path, op.by);
3017
+ const result = merge({ store }, base, localHead, remoteHead2);
3018
+ const conflicted = new Set(result.conflicts.map((c) => c.path));
3019
+ await writeBundle(solDir2, bundle, 0);
3020
+ setOpLogHead(remoteHead2);
3021
+ const { repo: rebased } = open();
3022
+ const d = diffTrees(store, remoteHead2, result.head);
3023
+ for (const p of [...d.added, ...d.modified.map((m) => m.path)]) {
3024
+ if (conflicted.has(p))
3025
+ continue;
3026
+ const blob = fileAt3(store, result.head, p);
3027
+ if (!blob)
3028
+ continue;
3029
+ const author = pathAuthor.get(p);
3030
+ if (blob.encoding === "base64")
3031
+ await rebased.writeBytes(p, new Uint8Array(Buffer.from(blob.content, "base64")), undefined, author);
3032
+ else
3033
+ await rebased.writeFile(p, blob.content, undefined, author);
3034
+ }
3035
+ for (const p of d.removed)
3036
+ if (!conflicted.has(p))
3037
+ await rebased.deleteFile(p, undefined, pathAuthor.get(p));
3038
+ if (!result.conflicts.length)
3039
+ await appendCommit(log, await rebased.head(), "merge remote into local", remoteHead2);
3040
+ const mergedHead = await log.head() ?? "";
3041
+ syncRefHead(mergedHead);
3042
+ materializeTree(loadStore(), mergedHead);
3043
+ if (result.conflicts.length) {
3044
+ for (const c of result.conflicts) {
3045
+ const blob = fileAt3(store, result.head, c.path);
3046
+ if (blob)
3047
+ writeFileSync9(join9(cwd2, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
3048
+ }
3049
+ writeFileSync9(join9(solDir2, "MERGE_HEAD"), remoteHead2);
3050
+ console.log(`pulled + merged WITH ${result.conflicts.length} conflict(s), left uncommitted in your working tree:`);
3051
+ for (const c of result.conflicts)
3052
+ console.log(" " + c.path);
3053
+ console.log("resolve the <<<<<<< markers, then `sol commit` and `sol push`");
3054
+ process.exitCode = 1;
3055
+ } else {
3056
+ console.log(`pulled + merged remote into local -> ${mergedHead.slice(0, 12)} (now run \`sol push\`)`);
3057
+ }
3058
+ break;
3059
+ }
3060
+ case "promote": {
3061
+ if (!existsSync9(solDir2))
3062
+ die("not a sol repo");
3063
+ const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3064
+ const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3065
+ const cur = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : "main";
3066
+ const branch = args[0] || cur;
3067
+ const refs = await remotePromote(cfg, token, branch);
3068
+ console.log(`promoted '${branch}' -> production '${refs.production}' now at ${(refs.branches[refs.production] ?? "").slice(0, 12)}`);
3069
+ break;
3070
+ }
3071
+ case "fork": {
3072
+ const [url, frest] = remoteUrlArg(args);
3073
+ const parent = frest[0] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
3074
+ const newRepo = frest[1] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
3075
+ const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3076
+ const target = resolve3(cwd2, frest[2] || newRepo);
3077
+ const fdir = join9(target, ".sol");
3078
+ if (existsSync9(fdir))
3079
+ die("already a sol repo: " + target);
3080
+ const parentCfg = { url, repo: parent };
3081
+ const newCfg = { url, repo: newRepo, forkParent: parent };
3082
+ const bundle = await remoteExport(parentCfg, token);
3083
+ if (!bundle.ops.length)
3084
+ die(`parent repo '${parent}' is empty or unreachable`);
3085
+ const prodBranch = bundle.refs?.production || "main";
3086
+ const prodHead = bundle.checkout?.head || bundle.head;
3087
+ await remotePush(newCfg, token, { nodes: bundle.nodes, ops: bundle.ops, branch: prodBranch, head: prodHead });
3088
+ for (const [name, h] of Object.entries(bundle.refs?.branches ?? {})) {
3089
+ if (name !== prodBranch)
3090
+ await remotePush(newCfg, token, { nodes: [], ops: [], branch: name, head: h });
3091
+ }
3092
+ await forkMeta(newCfg, token, parent);
3093
+ const canon = await remoteExport(newCfg, token);
3094
+ mkdirSync7(fdir, { recursive: true });
3095
+ await writeBundle(fdir, canon, 0);
3096
+ const srvRefs = canon.refs ?? { branches: { main: canon.head ?? "" }, production: "main" };
3097
+ const checkout = canon.checkout ?? { branch: srvRefs.production || "main", head: srvRefs.branches[srvRefs.production] ?? canon.head };
3098
+ const onBranch = checkout.branch;
3099
+ const checkoutHead = checkout.head ?? canon.head ?? "";
3100
+ const cloneBranches = {};
3101
+ for (const [name, h] of Object.entries(srvRefs.branches))
3102
+ cloneBranches[name] = { head: h, base: h, remote: h };
3103
+ writeFileSync9(join9(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
3104
+ writeFileSync9(join9(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
3105
+ saveRemote(fdir, newCfg);
3106
+ const store = new Store2;
3107
+ for (const node of canon.nodes)
3108
+ store.put(node);
3109
+ let n = 0;
3110
+ if (checkoutHead) {
3111
+ for (const f of listAll3(store, checkoutHead)) {
3112
+ const abs = resolve3(target, f);
3113
+ if (abs !== target && !abs.startsWith(target + sep3))
3114
+ continue;
3115
+ const blob = fileAt3(store, checkoutHead, f);
3116
+ if (!blob)
3117
+ continue;
3118
+ mkdirSync7(dirname5(abs), { recursive: true });
3119
+ writeFileSync9(abs, blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
3120
+ n++;
3121
+ }
3122
+ }
3123
+ console.log(`forked ${parent} -> ${newRepo} (your copy at ${args[3] || newRepo}: ${Object.keys(cloneBranches).length} branch(es), ${n} files; parent: ${parent})`);
3124
+ break;
3125
+ }
3126
+ case "access": {
3127
+ const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3128
+ const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3129
+ const sub = args[0];
3130
+ if (!sub || sub === "show") {
3131
+ const a = await accessGet(cfg, token);
3132
+ console.log(`${cfg.repo}: ${a.visibility}`);
3133
+ const cols = Object.entries(a.collaborators);
3134
+ if (cols.length)
3135
+ for (const [u, r] of cols)
3136
+ console.log(` ${u}: ${r}`);
3137
+ else
3138
+ console.log(" (no collaborators)");
3139
+ } else if (sub === "public" || sub === "private") {
3140
+ const a = await accessSet(cfg, token, { visibility: sub });
3141
+ console.log(`${cfg.repo} is now ${a.visibility}`);
3142
+ } else if (sub === "add") {
3143
+ const userId = args[1] || die("usage: sol access add <userId> <read|write|admin>");
3144
+ const role = args[2] || "read";
3145
+ if (role !== "read" && role !== "write" && role !== "admin")
3146
+ die(`invalid role '${role}' \u2014 use read, write, or admin`);
3147
+ const a = await accessSet(cfg, token, { collaborator: { userId, role } });
3148
+ if (a.collaborators?.[userId] === role)
3149
+ console.log(`granted ${role} to ${userId} on ${cfg.repo}`);
3150
+ else
3151
+ die(`failed to grant ${role} to ${userId} (the server did not apply it)`);
3152
+ } else if (sub === "remove") {
3153
+ const userId = args[1] || die("usage: sol access remove <userId>");
3154
+ await accessSet(cfg, token, { collaborator: { userId, remove: true } });
3155
+ console.log(`removed ${userId} from ${cfg.repo}`);
3156
+ } else {
3157
+ die("usage: sol access [show | public | private | add <userId> <role> | remove <userId>]");
3158
+ }
3159
+ break;
3160
+ }
3161
+ case "forks": {
3162
+ const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3163
+ const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3164
+ const { forks } = await forksList(cfg, token);
3165
+ if (!forks.length)
3166
+ console.log(`(no forks of ${cfg.repo})`);
3167
+ else {
3168
+ console.log(`forks of ${cfg.repo}:`);
3169
+ for (const f of forks)
3170
+ console.log(` ${f.repo}`);
3171
+ }
3172
+ break;
3173
+ }
3174
+ case "mr": {
3175
+ const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3176
+ const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3177
+ const sub = args[0];
3178
+ const localRefs = () => existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
3179
+ const flag = (name) => {
3180
+ const i = args.indexOf(name);
3181
+ return i >= 0 ? args[i + 1] : undefined;
3182
+ };
3183
+ const has = (name) => args.includes(name);
3184
+ const idArg = () => +(args[1] || die("this `sol pr` command needs an <id>"));
3185
+ if (sub === "open") {
3186
+ const refs = localRefs();
3187
+ const fromBranch = flag("--from") || refs.current;
3188
+ const fromHead = refs.branches[fromBranch]?.head;
3189
+ const upstream = flag("--upstream") || (has("--upstream") ? cfg.forkParent : undefined);
3190
+ const target = upstream ? { url: cfg.url, repo: upstream } : cfg;
3191
+ const pr = await mrOpen(target, token, { fromRepo: upstream ? cfg.repo : undefined, fromBranch, fromHead, toBranch: flag("--to"), title: flag("-t") || flag("--title"), body: flag("-m") || flag("--body") });
3192
+ const where = upstream ? `${cfg.repo}/${pr.fromBranch} -> ${upstream}/${pr.toBranch}` : `${pr.fromBranch} -> ${pr.toBranch}`;
3193
+ console.log(`opened MR #${pr.id}${upstream ? ` on ${upstream}` : ""}: ${pr.title} (${where})`);
3194
+ } else if (sub === "list") {
3195
+ const { mrs } = await mrList(cfg, token);
3196
+ if (!mrs.length)
3197
+ console.log("(no merge requests)");
3198
+ for (const p of mrs)
3199
+ console.log(`#${p.id} [${p.status}] ${p.fromRepo ? `${p.fromRepo}/` : ""}${p.fromBranch} -> ${p.toBranch} "${p.title}" \u2014 ${mrSummary(p)}`);
3200
+ } else if (sub === "show") {
3201
+ const pr = await mrGet(cfg, token, idArg());
3202
+ console.log(`MR #${pr.id} [${pr.status}] by ${pr.author}`);
3203
+ console.log(`${pr.fromRepo ? `${pr.fromRepo}/` : ""}${pr.fromBranch} (${(pr.fromHead || "").slice(0, 12)}) -> ${pr.toBranch}${pr.fromRepo ? " [cross-fork]" : ""}`);
3204
+ console.log(pr.title);
3205
+ if (pr.body)
3206
+ console.log(`
3207
+ ${pr.body}`);
3208
+ console.log(`
3209
+ ${mrSummary(pr)}`);
3210
+ for (const c of pr.checks)
3211
+ console.log(` check ${c.name}: ${c.status}${c.detail ? ` (${c.detail.split(`
3212
+ `)[0]})` : ""}`);
3213
+ for (const r of pr.reviews)
3214
+ console.log(` review ${r.actor}: ${r.verdict}${r.body ? ` \u2014 ${r.body}` : ""}`);
3215
+ for (const c of pr.comments)
3216
+ console.log(` comment ${c.actor}${c.path ? ` @${c.path}:${c.line}` : ""}: ${c.body}`);
3217
+ console.log(`
3218
+ --- diff (what would merge) ---`);
3219
+ if (pr.fromRepo) {
3220
+ const d = await mrDiff(cfg, token, pr.id);
3221
+ for (const p of d.added ?? [])
3222
+ console.log(` + ${p}`);
3223
+ for (const m of d.modified ?? []) {
3224
+ const path = typeof m === "string" ? m : m.path;
3225
+ console.log(` ~ ${path}`);
3226
+ const hunks2 = typeof m === "string" ? "" : m.hunks;
3227
+ if (hunks2)
3228
+ console.log(hunks2.split(`
3229
+ `).map((l) => " " + l).join(`
3230
+ `));
3231
+ }
3232
+ for (const p of d.removed ?? [])
3233
+ console.log(` - ${p}`);
3234
+ if (!((d.added?.length ?? 0) + (d.modified?.length ?? 0) + (d.removed?.length ?? 0)))
3235
+ console.log(" (no changes)");
3236
+ } else {
3237
+ const toHead = localRefs().branches[pr.toBranch]?.head;
3238
+ if (toHead && pr.fromHead) {
3239
+ try {
3240
+ printTreeDiff(diffTrees(loadStore(), toHead, pr.fromHead));
3241
+ } catch {
3242
+ console.log("(run `sol pull` to compute the diff locally)");
3243
+ }
3244
+ }
3245
+ }
3246
+ } else if (sub === "review") {
3247
+ const verdict = has("--approve") ? "approve" : has("--request-changes") ? "request-changes" : "comment";
3248
+ const pr = await mrAction(cfg, token, "review", { id: idArg(), verdict, body: flag("-m") ?? "" });
3249
+ console.log(`reviewed MR #${pr.id}: ${verdict}`);
3250
+ } else if (sub === "comment") {
3251
+ const line = flag("--line");
3252
+ const pr = await mrAction(cfg, token, "comment", { id: idArg(), body: flag("-m") ?? "", path: flag("--path"), line: line ? +line : undefined });
3253
+ console.log(`commented on MR #${pr.id}`);
3254
+ } else if (sub === "check") {
3255
+ const id = idArg();
3256
+ if (has("--run")) {
3257
+ const dashes = args.indexOf("--");
3258
+ if (dashes < 0 || args.length <= dashes + 1)
3259
+ die("usage: sol mr check <id> --run -- <test command>");
3260
+ const cmd2 = args.slice(dashes + 1);
3261
+ const name = flag("--name") || cmd2.join(" ");
3262
+ let status = "pass";
3263
+ let detail = "";
3264
+ try {
3265
+ execFileSync2(cmd2[0], cmd2.slice(1), { cwd: cwd2, stdio: "pipe" });
3266
+ } catch (e) {
3267
+ status = "fail";
3268
+ const err = e;
3269
+ detail = (err.stdout?.toString() || err.message || "").slice(-200);
3270
+ }
3271
+ const pr = await mrAction(cfg, token, "check", { id, name, status, detail });
3272
+ console.log(`check '${name}' on MR #${pr.id}: ${status}`);
3273
+ } else {
3274
+ const name = flag("--name") || "check";
3275
+ const status = flag("--status") || "pending";
3276
+ await mrAction(cfg, token, "check", { id, name, status, detail: flag("--detail") });
3277
+ console.log(`reported check '${name}' = ${status} on MR #${id}`);
3278
+ }
3279
+ } else if (sub === "merge") {
3280
+ const id = idArg();
3281
+ try {
3282
+ const pr = await mrAction(cfg, token, "merge", { id, force: has("--force") });
3283
+ console.log(`merged MR #${pr.id} -> ${pr.toBranch} now at ${(pr.mergeHead || "").slice(0, 12)} (run \`sol pull\` to sync)`);
3284
+ } catch (e) {
3285
+ const msg = String(e.message).replace(/^remote \/mr\/merge -> \d+: /, "");
3286
+ let clean = msg;
3287
+ try {
3288
+ const j = JSON.parse(msg);
3289
+ clean = j.reasons?.length ? `${j.error}: ${j.reasons.join("; ")} \u2014 use \`--force\` to override` : j.error;
3290
+ } catch {}
3291
+ die(clean);
3292
+ }
3293
+ } else if (sub === "close") {
3294
+ const pr = await mrAction(cfg, token, "close", { id: idArg() });
3295
+ console.log(`closed MR #${pr.id}`);
3296
+ } else {
3297
+ die("usage: sol mr open|list|show <id>|review <id>|comment <id>|check <id>|merge <id>|close <id>");
3298
+ }
3299
+ break;
3300
+ }
3301
+ case "watch": {
3302
+ const { repo, log } = open();
3303
+ const tick = async (announce) => {
3304
+ const release2 = acquireLock();
3305
+ let changed = 0;
3306
+ try {
3307
+ const snap = await snapshotTree(repo);
3308
+ changed = snap.changed;
3309
+ if (changed) {
3310
+ await appendCapture(log, snap.root);
3311
+ writeWorkingIndex(await repo.list());
3312
+ }
3313
+ } finally {
3314
+ release2();
3315
+ }
3316
+ const t = new Date().toLocaleTimeString();
3317
+ if (changed)
3318
+ console.log(`[${t}] auto-captured ${changed} change(s) -> ${(await repo.head()).slice(0, 12)}`);
3319
+ else if (announce)
3320
+ console.log(`[${t}] working tree already up to date`);
3321
+ };
3322
+ await tick(true);
3323
+ console.log(`sol watching ${cwd2}
3324
+ every change is auto-captured into the op-log \u2014 nothing is lost, even if you never commit.
3325
+ run \`sol commit "msg"\` any time to mark a milestone. Ctrl-C to stop.`);
3326
+ let timer;
3327
+ watch(cwd2, { recursive: true }, (_e, filename) => {
3328
+ if (!filename)
3329
+ return;
3330
+ const top = String(filename).split(sep3)[0];
3331
+ if (top === ".sol" || DEFAULT_IGNORE2.includes(top))
3332
+ return;
3333
+ clearTimeout(timer);
3334
+ timer = setTimeout(() => tick(false).catch((e) => console.error("sol: " + (e?.message || e))), 400);
3335
+ });
3336
+ await new Promise(() => {});
3337
+ break;
3338
+ }
3339
+ case "run": {
3340
+ const { repo, log } = open();
3341
+ const keep = [];
3342
+ const command = [];
3343
+ let isolate = false;
3344
+ for (let i = 0;i < args.length; i++) {
3345
+ if (args[i] === "--keep")
3346
+ keep.push(args[++i]);
3347
+ else if (args[i] === "--isolate")
3348
+ isolate = true;
3349
+ else
3350
+ command.push(args[i]);
3351
+ }
3352
+ if (!command.length)
3353
+ die("usage: sol run [--keep <path>] [--isolate] <command...>");
3354
+ const head = await repo.head();
3355
+ const dir = mkdtempSync(join9(tmpdir(), "sol-run-"));
3356
+ try {
3357
+ const hn = hydrate2(loadStore(), head, dir);
3358
+ console.log(`hydrated ${hn} file(s) -> sandbox${isolate ? " (isolated: no network, writes confined to the sandbox)" : ""}`);
3359
+ const argv2 = isolate ? isolateCommand(command, dir) : command;
3360
+ console.log(`$ ${command.join(" ")}`);
3361
+ let failed = 0;
3362
+ try {
3363
+ execFileSync2(argv2[0], argv2.slice(1), { cwd: dir, stdio: "inherit" });
3364
+ } catch (e) {
3365
+ failed = typeof e?.status === "number" ? e.status : 1;
3366
+ }
3367
+ if (failed) {
3368
+ console.error(`(command failed with exit ${failed}; nothing captured)`);
3369
+ process.exitCode = failed;
3370
+ break;
3371
+ }
3372
+ const { written, deleted } = await capture(repo, dir, keep);
3373
+ if (written.length || deleted.length) {
3374
+ await appendCommit(log, await repo.head(), `run: ${command.join(" ")}`, head);
3375
+ if (existsSync9(refsPath())) {
3376
+ const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
3377
+ if (refs.branches[refs.current]) {
3378
+ refs.branches[refs.current].head = await repo.head();
3379
+ saveRefs(refs);
3380
+ }
3381
+ }
3382
+ const synced = loadStore();
3383
+ const nh = await repo.head();
3384
+ for (const f of written)
3385
+ materialize(synced, nh, f);
3386
+ for (const f of deleted) {
3387
+ try {
3388
+ unlinkSync6(join9(cwd2, f));
3389
+ } catch {}
3390
+ }
3391
+ console.log(`captured ${written.length} written, ${deleted.length} deleted file(s):`);
3392
+ for (const f of written)
3393
+ console.log(" + " + f);
3394
+ for (const f of deleted)
3395
+ console.log(" - " + f);
3396
+ } else {
3397
+ console.log("no files captured (the run produced no tracked changes)");
3398
+ }
3399
+ } finally {
3400
+ rmSync(dir, { recursive: true, force: true });
3401
+ }
3402
+ break;
3403
+ }
3404
+ case "git": {
3405
+ const sub = args[0];
3406
+ if (sub === "import") {
3407
+ const gitPath = resolve3(cwd2, args[1] || die("usage: sol git import <git-repo> [dir]"));
3408
+ const target = resolve3(cwd2, args[2] || basename(gitPath));
3409
+ const fdir = join9(target, ".sol");
3410
+ if (existsSync9(fdir))
3411
+ die("already a sol repo: " + target);
3412
+ mkdirSync7(fdir, { recursive: true });
3413
+ const { commits, branches, head, current } = await importGitRepo(gitPath, fdir);
3414
+ const refsBranches = {};
3415
+ for (const b of branches)
3416
+ refsBranches[b.name] = { head: b.head, base: b.head };
3417
+ if (!refsBranches[current])
3418
+ refsBranches[current] = { head, base: head };
3419
+ refsBranches[current].head = head;
3420
+ writeFileSync9(join9(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
3421
+ const store = new Store2;
3422
+ for (const name of readdirSync7(join9(fdir, "objects"))) {
3423
+ if (name.endsWith(".tmp"))
3424
+ continue;
3425
+ try {
3426
+ store.put(decodeObject(readFileSync9(join9(fdir, "objects", name))));
3427
+ } catch {}
3428
+ }
3429
+ const onDisk = hydrate2(store, head, target);
3430
+ console.log(`imported ${commits} commit(s), ${branches.length} branch(es) from git -> ${args[2] || basename(gitPath)} (${onDisk} files; on branch ${current})`);
3431
+ } else if (sub === "export") {
3432
+ const { log } = open();
3433
+ const gitPath = resolve3(cwd2, args[1] || die('usage: sol git export <git-repo> [-m "msg"]'));
3434
+ const mi = args.indexOf("-m");
3435
+ const store = loadStore();
3436
+ const head = await log.head() ?? "";
3437
+ const closed = groupIntoUnits(await log.history()).filter((u) => !u.open);
3438
+ const msg = mi >= 0 ? args[mi + 1] : closed.length ? closed[closed.length - 1].label ?? "sol export" : "sol export";
3439
+ const { written, deleted } = exportToGit(store, head, gitPath, msg ?? "sol export");
3440
+ console.log(`exported ${written} file(s), ${deleted} deleted -> git commit in ${args[1]} (now \`git push\`)`);
3441
+ } else {
3442
+ die('usage: sol git import <repo> [dir] | sol git export <repo> [-m "msg"]');
3443
+ }
3444
+ break;
3445
+ }
3446
+ default:
3447
+ if (cmd && cmd !== "help" && cmd !== "-h" && cmd !== "--help") {
3448
+ console.error("sol: unknown command: " + cmd + `
3449
+ `);
3450
+ process.exitCode = 1;
3451
+ }
3452
+ console.log(`sol \u2014 local content-addressed VCS (the new git)
3453
+
3454
+ the model: edit files freely, then commit \u2014 or run \`sol watch\` and every change is captured
3455
+ automatically. there is NO separate "git add".
3456
+
3457
+ everyday (examples are copy-safe \u2014 use real filenames):
3458
+ sol init create a repo in ./.sol
3459
+ sol watch AUTO-CAPTURE every change (run once; nothing is ever lost)
3460
+ sol run npm test hydrate to a sandbox, run, capture outputs (--isolate to confine)
3461
+ sol commit "what changed" commit the whole tree (SINGLE-AUTHOR; -m "msg" file.ts scopes to files)
3462
+ sol status current branch + uncommitted changes
3463
+ sol log commit history (sol log --all for every op)
3464
+ sol diff working-tree changes (sol diff main feature between refs)
3465
+ sol show 5 a commit's message + its diff
3466
+ sol restore --from 3 app.py put a file (or the whole tree) back from a ref
3467
+ sol blame todo/cli.py who/which commit last touched each line
3468
+ sol ls / sol cat README.md list tracked files / print one
3469
+ sol rm old.txt delete a file (from the repo and disk)
3470
+ sol ignore "*.tmp" add an ignore pattern (no arg lists the active patterns)
3471
+ sol fsck / sol gc verify integrity / drop unreachable objects
3472
+
3473
+ branches & tags:
3474
+ sol branch list branches (sol branch feature creates one at HEAD)
3475
+ sol switch feature switch to a branch (captures current work first, never loses it)
3476
+ sol merge feature 3-way merge a branch in (conflicts land in the working tree)
3477
+ sol tag v1 mark an immutable label at HEAD
3478
+
3479
+ auth (sign in once; remote commands then use the cached token, no SOL_TOKEN needed):
3480
+ sol auth login [<web-url>] device-flow sign-in via the Midsummer web app; caches the token ~/.sol/credentials
3481
+ sol auth whoami print the identity behind the cached token (signed in as @<handle>)
3482
+ sol auth set-handle <name> choose your @handle \u2014 the <owner> in <owner>/<repo> (changing it re-namespaces repos)
3483
+ sol auth pat [days] mint a long-lived Personal Access Token for git/CI (default 90 days)
3484
+ sol auth status / logout show who you're signed in as / clear the cached token
3485
+
3486
+ remotes (self-hostable backend; token in SOL_TOKEN or via sol auth login):
3487
+ sol clone [<url>] <owner>/<repo> [dir] clone a remote repo (checks out PRODUCTION); default dir = <repo>
3488
+ sol push / sol pull sync your commits with the remote (push registers your branch's head)
3489
+ sol promote [branch] point the remote's production branch at <branch> (default: current)
3490
+ sol remote <url> <repo> set the remote (no arg: show it; url defaults to the hosted Sol)
3491
+ sol fork [<url>] <parent> <new> [dir] make your own copy of a repo (all branches + history + a parent link)
3492
+ sol forks list the forks of the current repo
3493
+ sol access [show|public|private|add <userId> <role>|remove <userId>] manage repo visibility + collaborators
3494
+
3495
+ merge requests (propose a branch's commits to merge; reviews + CI checks):
3496
+ sol mr open [--from B] [--to B] -t "title" [-m body] open an MR (from=current branch, to=production)
3497
+ sol mr list list MRs (open/merged/closed)
3498
+ sol mr show <id> metadata, reviews/checks, + the diff it would merge
3499
+ sol mr review <id> --approve | --request-changes | --comment [-m ...]
3500
+ sol mr check <id> --run [--name N] -- <test cmd> run the tests, report a pass/fail check
3501
+ sol mr merge <id> [--force] fast-forward-merge (gated on checks + reviews; --force overrides)
3502
+ sol mr close <id>
3503
+
3504
+ git interop (adopt Sol without leaving GitHub):
3505
+ sol git import <repo> [dir] import a git repo's HEAD into a new Sol repo (modes/symlinks/binaries)
3506
+ sol git export <repo> write Sol's tree back as a git commit (+ deletes), then \`git push\`
3507
+
3508
+ for concurrent agents, use scoped commits (sol commit -m "msg" file) or a per-agent workspace.
3509
+ a whole-tree commit is REFUSED when SOL_ACTOR is set (--whole-tree / SOL_ALLOW_WHOLE_TREE=1 to override).
3510
+
3511
+ a ref is a branch/tag name, a commit hash-prefix, an op seq number, or HEAD. content is SHA-256
3512
+ addressed; history is a tamper-evident hash-chained op-log. attribute changes with SOL_ACTOR=you.`);
3513
+ }
3514
+ } finally {
3515
+ release?.();
3516
+ }
3517
+ }
3518
+ main().catch((e) => die(e?.message || String(e)));