midsummer-sol 0.1.1 → 0.1.3

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 (3) hide show
  1. package/index.js +177 -0
  2. package/package.json +1 -1
  3. package/sol.js +723 -148
package/sol.js CHANGED
@@ -3,9 +3,9 @@
3
3
 
4
4
  // src/bin/sol.ts
5
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";
6
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readdirSync as readdirSync7, readFileSync as readFileSync9, rmSync, unlinkSync as unlinkSync6, watch, writeFileSync as writeFileSync9 } from "fs";
7
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";
8
+ import { basename, dirname as dirname5, join as join10, resolve as resolve4, sep as sep3 } from "path";
9
9
 
10
10
  // src/attest.ts
11
11
  function canonicalOp(op) {
@@ -615,6 +615,49 @@ class FileStore {
615
615
  }
616
616
  }
617
617
 
618
+ class FileOpLog {
619
+ opsFile;
620
+ headFile;
621
+ constructor(solDir) {
622
+ this.opsFile = join(solDir, "ops.jsonl");
623
+ this.headFile = join(solDir, "HEAD");
624
+ }
625
+ readHead() {
626
+ return existsSync(this.headFile) ? JSON.parse(readFileSync(this.headFile, "utf8")) : { seq: 0 };
627
+ }
628
+ async head() {
629
+ return this.readHead().head;
630
+ }
631
+ async seq() {
632
+ return this.readHead().seq ?? 0;
633
+ }
634
+ async logTip() {
635
+ return this.readHead().logTip;
636
+ }
637
+ async append(entry) {
638
+ const chained = chainOp(this.readHead().logTip, entry);
639
+ appendFileSync(this.opsFile, JSON.stringify(chained) + `
640
+ `);
641
+ writeFileSync(this.headFile, JSON.stringify({ head: chained.rootAfter, seq: chained.seq, logTip: chained.entryHash }));
642
+ }
643
+ async history(opts) {
644
+ if (!existsSync(this.opsFile))
645
+ return [];
646
+ const all = [];
647
+ for (const l of readFileSync(this.opsFile, "utf8").split(`
648
+ `)) {
649
+ if (!l)
650
+ continue;
651
+ try {
652
+ all.push(JSON.parse(l));
653
+ } catch {
654
+ break;
655
+ }
656
+ }
657
+ return pageOps(all, opts);
658
+ }
659
+ }
660
+
618
661
  // src/crypto.ts
619
662
  import { createCipheriv, createDecipheriv, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
620
663
  var UNREADABLE = "<<unreadable>>";
@@ -785,7 +828,7 @@ class FileStore2 {
785
828
  }
786
829
  }
787
830
 
788
- class FileOpLog {
831
+ class FileOpLog2 {
789
832
  opsFile;
790
833
  headFile;
791
834
  constructor(solDir) {
@@ -1003,7 +1046,7 @@ function setHead(fdir, head) {
1003
1046
  }
1004
1047
  async function importGitRepo(gitPath, fdir) {
1005
1048
  const store = new FileStore2(fdir);
1006
- const log = new FileOpLog(fdir);
1049
+ const log = new FileOpLog2(fdir);
1007
1050
  const empty = await emptyRoot2(store);
1008
1051
  const commitList = git(gitPath, "rev-list", "--reverse", "--topo-order", "--all").toString().trim().split(`
1009
1052
  `).filter(Boolean);
@@ -1394,7 +1437,7 @@ function die(msg) {
1394
1437
  function open() {
1395
1438
  if (!existsSync6(solDir2))
1396
1439
  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) };
1440
+ return { repo: new AsyncRepo(new FileStore2(solDir2), new FileOpLog2(solDir2), actor2), log: new FileOpLog2(solDir2) };
1398
1441
  }
1399
1442
  var DEFAULT_IGNORE2 = [".sol", ".git", "node_modules", "dist", "build", ".next", ".cache", ".DS_Store", "__pycache__", "*.pyc", ".venv", "venv", "target", ".gradle", "*.log"];
1400
1443
  function ignorePatterns2() {
@@ -1630,6 +1673,28 @@ async function resolveRef(log, ref) {
1630
1673
  const op = ops.find((o) => o.rootAfter.startsWith(bare) || (o.entryHash ?? "").startsWith(bare)) || die("unknown ref: " + ref);
1631
1674
  return { head: op.rootAfter, op };
1632
1675
  }
1676
+ async function resolveRefSoft(log, ref) {
1677
+ const ops = await log.history();
1678
+ if (!ref || ref === "head" || ref === "HEAD")
1679
+ return { head: await log.head() ?? "", op: ops[ops.length - 1] };
1680
+ if (existsSync6(refsPath())) {
1681
+ const refs = JSON.parse(readFileSync6(refsPath(), "utf8"));
1682
+ if (ref === refs.current) {
1683
+ const h = await log.head() ?? "";
1684
+ return { head: h, op: [...ops].reverse().find((o) => o.rootAfter === h) ?? ops[ops.length - 1] };
1685
+ }
1686
+ const named = refs.branches[ref]?.head ?? refs.tags[ref];
1687
+ if (named !== undefined)
1688
+ return { head: named, op: [...ops].reverse().find((o) => o.rootAfter === named) };
1689
+ }
1690
+ if (/^\d+$/.test(ref)) {
1691
+ const op2 = ops.find((o) => o.seq === Number(ref));
1692
+ return op2 ? { head: op2.rootAfter, op: op2 } : undefined;
1693
+ }
1694
+ const bare = ref.startsWith("h_") ? ref : "h_" + ref;
1695
+ const op = ops.find((o) => o.rootAfter.startsWith(bare) || (o.entryHash ?? "").startsWith(bare));
1696
+ return op ? { head: op.rootAfter, op } : undefined;
1697
+ }
1633
1698
  function materialize(store, head, path) {
1634
1699
  const blob = fileAt(store, head, path);
1635
1700
  if (!blob)
@@ -1656,6 +1721,40 @@ function materialize(store, head, path) {
1656
1721
  }
1657
1722
  return true;
1658
1723
  }
1724
+ function materializeInto(store, head, target, path) {
1725
+ const blob = fileAt(store, head, path);
1726
+ if (!blob)
1727
+ return false;
1728
+ const abs = resolve2(target, path);
1729
+ if (abs !== target && !abs.startsWith(target + sep2))
1730
+ return false;
1731
+ const mode = modeAt(store, head, path);
1732
+ mkdirSync5(dirname3(abs), { recursive: true });
1733
+ if (mode === SYMLINK_MODE2) {
1734
+ try {
1735
+ unlinkSync4(abs);
1736
+ } catch {}
1737
+ symlinkSync3(blob.content, abs);
1738
+ return true;
1739
+ }
1740
+ writeFileSync6(abs, blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
1741
+ if (mode === EXEC_MODE2) {
1742
+ try {
1743
+ chmodSync3(abs, EXEC_MODE2);
1744
+ } catch {}
1745
+ }
1746
+ return true;
1747
+ }
1748
+ function writeWorkingIndexAt(targetSolDir, target, files) {
1749
+ const idx = {};
1750
+ for (const f of files) {
1751
+ try {
1752
+ const st = lstatSync3(join6(target, f));
1753
+ idx[f] = [st.mtimeMs, st.size, st.mode & 73 ? 1 : 0];
1754
+ } catch {}
1755
+ }
1756
+ writeFileSync6(join6(targetSolDir, "index.json"), JSON.stringify(idx));
1757
+ }
1659
1758
  function materializeTree(store, head) {
1660
1759
  const want = new Set(head ? listAll(store, head) : []);
1661
1760
  let n = 0;
@@ -1809,13 +1908,17 @@ function workingChanges(store, head, index = loadWorkingIndex()) {
1809
1908
  }
1810
1909
  return { added: added.sort(), modified: modified.sort(), removed: removed.sort() };
1811
1910
  }
1812
- function printWorkingDiff(store, base) {
1911
+ function printWorkingDiff(store, base, only) {
1813
1912
  const ch = workingChanges(store, base);
1814
- for (const f of ch.added)
1913
+ const keep = (f) => only === undefined || f === only;
1914
+ const added = ch.added.filter(keep);
1915
+ const removed = ch.removed.filter(keep);
1916
+ const modified = ch.modified.filter(keep);
1917
+ for (const f of added)
1815
1918
  console.log(`+ added ${f}`);
1816
- for (const f of ch.removed)
1919
+ for (const f of removed)
1817
1920
  console.log(`- removed ${f}`);
1818
- for (const f of ch.modified) {
1921
+ for (const f of modified) {
1819
1922
  console.log(`~ modified ${f}`);
1820
1923
  const abs = join6(cwd2, f);
1821
1924
  if (lstatSync3(abs).isSymbolicLink())
@@ -1827,8 +1930,37 @@ function printWorkingDiff(store, base) {
1827
1930
  if (typeof stored === "string" && stored !== buf.toString("utf8"))
1828
1931
  console.log(indent(lineHunks2(stored, buf.toString("utf8"))));
1829
1932
  }
1830
- if (!ch.added.length && !ch.removed.length && !ch.modified.length)
1831
- console.log("no working changes");
1933
+ if (!added.length && !removed.length && !modified.length)
1934
+ console.log(only ? `no working changes in ${only}` : "no working changes");
1935
+ }
1936
+ function unresolvedConflictPaths() {
1937
+ const out = [];
1938
+ for (const f of walkFiles()) {
1939
+ const abs = join6(cwd2, f);
1940
+ let buf;
1941
+ try {
1942
+ const st = lstatSync3(abs);
1943
+ if (st.isSymbolicLink() || st.isDirectory())
1944
+ continue;
1945
+ buf = readFileSync6(abs);
1946
+ } catch {
1947
+ continue;
1948
+ }
1949
+ if (buf.includes(0))
1950
+ continue;
1951
+ const text = buf.toString("utf8");
1952
+ if (/^<{7}( |$)/m.test(text) && /^>{7}( |$)/m.test(text))
1953
+ out.push(f);
1954
+ }
1955
+ return out;
1956
+ }
1957
+ function isWorkingPath(store, head, arg) {
1958
+ const rel = repoRel(arg);
1959
+ if (lexists(join6(cwd2, rel)))
1960
+ return rel;
1961
+ if (head && listAll(store, head).includes(rel))
1962
+ return rel;
1963
+ return;
1832
1964
  }
1833
1965
  function blameFile(ops, store, path) {
1834
1966
  let tagged = [];
@@ -1986,10 +2118,275 @@ async function writeBundle(solDir3, bundle, from = 0) {
1986
2118
  return fresh.length;
1987
2119
  }
1988
2120
 
2121
+ // src/bin/local-peer.ts
2122
+ import { existsSync as existsSync8 } from "node:fs";
2123
+ import { join as join8, resolve as resolve3 } from "node:path";
2124
+ function localPeerSolDir(arg, base) {
2125
+ if (/^https?:\/\//.test(arg))
2126
+ return;
2127
+ const p = resolve3(base, arg);
2128
+ if (existsSync8(join8(p, ".sol")))
2129
+ return join8(p, ".sol");
2130
+ if (existsSync8(join8(p, "objects")) && existsSync8(join8(p, "HEAD")))
2131
+ return p;
2132
+ return;
2133
+ }
2134
+ function openPeer(peerSolDir) {
2135
+ return { store: new FileStore2(peerSolDir), log: new FileOpLog2(peerSolDir) };
2136
+ }
2137
+ async function peerNodes(rep, head) {
2138
+ if (!head)
2139
+ return [];
2140
+ const out = [];
2141
+ const seen = new Set;
2142
+ const stack = [head];
2143
+ while (stack.length) {
2144
+ const h = stack.pop();
2145
+ if (seen.has(h))
2146
+ continue;
2147
+ seen.add(h);
2148
+ const node = await rep.store.get(h);
2149
+ if (!node)
2150
+ continue;
2151
+ out.push(node);
2152
+ if (node.kind === "tree")
2153
+ for (const e of Object.values(node.entries))
2154
+ stack.push(e.hash);
2155
+ }
2156
+ return out;
2157
+ }
2158
+
2159
+ // src/merge.ts
2160
+ function resolvePath2(base, ours, theirs) {
2161
+ const oursChanged = ours !== base;
2162
+ const theirsChanged = theirs !== base;
2163
+ if (!oursChanged && !theirsChanged) {
2164
+ return { content: base };
2165
+ }
2166
+ if (oursChanged && !theirsChanged) {
2167
+ return { content: ours };
2168
+ }
2169
+ if (!oursChanged && theirsChanged) {
2170
+ return { content: theirs };
2171
+ }
2172
+ if (ours === theirs) {
2173
+ return { content: ours };
2174
+ }
2175
+ if (base !== undefined && ours !== undefined && theirs !== undefined) {
2176
+ const m = merge3(base, ours, theirs);
2177
+ if (m.clean)
2178
+ return { content: m.text };
2179
+ return { content: m.text, conflict: { path: "", ours, theirs } };
2180
+ }
2181
+ return {
2182
+ content: ours,
2183
+ conflict: { path: "", ours, theirs }
2184
+ };
2185
+ }
2186
+ function merge2(repo, baseHead, oursHead, theirsHead) {
2187
+ const store = repo.store;
2188
+ const paths = new Set([
2189
+ ...listAll(store, baseHead),
2190
+ ...listAll(store, oursHead),
2191
+ ...listAll(store, theirsHead)
2192
+ ]);
2193
+ const conflicts = [];
2194
+ let root = emptyRoot(store);
2195
+ for (const path of [...paths].sort()) {
2196
+ const base = readFile(store, baseHead, path);
2197
+ const ours = readFile(store, oursHead, path);
2198
+ const theirs = readFile(store, theirsHead, path);
2199
+ const { content, conflict } = resolvePath2(base, ours, theirs);
2200
+ if (conflict)
2201
+ conflicts.push({ ...conflict, path });
2202
+ if (content !== undefined) {
2203
+ root = writeFile(store, root, path, content);
2204
+ }
2205
+ }
2206
+ return { head: root, conflicts };
2207
+ }
2208
+
2209
+ // src/converge.ts
2210
+ var EMPTY_TREE3 = { kind: "tree", entries: {} };
2211
+ function buildAncestry(ops) {
2212
+ const parents = new Map;
2213
+ const add = (child, parent) => {
2214
+ if (!child || !parent || parent === child)
2215
+ return;
2216
+ let s = parents.get(child);
2217
+ if (!s)
2218
+ parents.set(child, s = new Set);
2219
+ s.add(parent);
2220
+ };
2221
+ let prevRoot;
2222
+ for (const op of ops) {
2223
+ if (op.type === "checkpoint") {
2224
+ add(op.rootAfter, op.parent);
2225
+ add(op.rootAfter, op.parent2);
2226
+ } else {
2227
+ add(op.rootAfter, op.parent ?? prevRoot);
2228
+ }
2229
+ prevRoot = op.rootAfter;
2230
+ }
2231
+ return parents;
2232
+ }
2233
+ function ancestorsOf(head, parents) {
2234
+ const seen = new Set;
2235
+ const stack = [head];
2236
+ while (stack.length) {
2237
+ const h = stack.pop();
2238
+ if (seen.has(h))
2239
+ continue;
2240
+ seen.add(h);
2241
+ for (const p of parents.get(h) ?? [])
2242
+ stack.push(p);
2243
+ }
2244
+ return seen;
2245
+ }
2246
+ function mergeBase(a, b, parents) {
2247
+ if (a === b)
2248
+ return a;
2249
+ const bAnc = ancestorsOf(b, parents);
2250
+ const seen = new Set;
2251
+ let frontier = [a];
2252
+ while (frontier.length) {
2253
+ const next = [];
2254
+ for (const h of frontier) {
2255
+ if (seen.has(h))
2256
+ continue;
2257
+ seen.add(h);
2258
+ if (bAnc.has(h))
2259
+ return h;
2260
+ for (const p of parents.get(h) ?? [])
2261
+ next.push(p);
2262
+ }
2263
+ frontier = next;
2264
+ }
2265
+ return;
2266
+ }
2267
+ async function hydrate2(src, dst, root) {
2268
+ if (!root)
2269
+ return dst.put(EMPTY_TREE3);
2270
+ const stack = [root];
2271
+ const seen = new Set;
2272
+ while (stack.length) {
2273
+ const h = stack.pop();
2274
+ if (seen.has(h))
2275
+ continue;
2276
+ seen.add(h);
2277
+ const node = await src.get(h);
2278
+ if (!node)
2279
+ continue;
2280
+ dst.put(node);
2281
+ if (node.kind === "tree")
2282
+ for (const e of Object.values(node.entries))
2283
+ stack.push(e.hash);
2284
+ }
2285
+ return root;
2286
+ }
2287
+ async function persist(sync, dst, root) {
2288
+ const stack = [root];
2289
+ const seen = new Set;
2290
+ while (stack.length) {
2291
+ const h = stack.pop();
2292
+ if (seen.has(h))
2293
+ continue;
2294
+ seen.add(h);
2295
+ const node = sync.get(h);
2296
+ if (!node)
2297
+ continue;
2298
+ if (!await dst.has(h))
2299
+ await dst.put(node);
2300
+ if (node.kind === "tree")
2301
+ for (const e of Object.values(node.entries))
2302
+ stack.push(e.hash);
2303
+ }
2304
+ }
2305
+ async function converge(local, input) {
2306
+ const at = input.at ?? Date.now();
2307
+ const actor3 = input.actor ?? "system";
2308
+ for (const node of input.nodes)
2309
+ await local.store.put(node);
2310
+ const priorHead = await local.log.head();
2311
+ const incomingHead = input.incomingHead ?? input.ops[input.ops.length - 1]?.rootAfter;
2312
+ const sinceSeq = await local.log.seq();
2313
+ const existing = await local.log.history();
2314
+ const priorAncestry = buildAncestry(existing);
2315
+ const knownByEntry = new Map(existing.filter((o) => o.entryHash).map((o) => [o.entryHash, o]));
2316
+ const knownByTuple = new Map(existing.map((o) => [`${o.type}\x00${o.path}\x00${o.rootAfter}`, o]));
2317
+ const incoming = [...input.ops].sort((a, b) => a.seq - b.seq);
2318
+ let forkBase = input.base ?? priorHead;
2319
+ if (input.base === undefined) {
2320
+ for (const op of incoming) {
2321
+ const k = op.entryHash ? knownByEntry.get(op.entryHash) : knownByTuple.get(`${op.type}\x00${op.path}\x00${op.rootAfter}`);
2322
+ if (k)
2323
+ forkBase = k.rootAfter;
2324
+ }
2325
+ }
2326
+ let applied = 0;
2327
+ for (const op of incoming) {
2328
+ if (op.entryHash && knownByEntry.has(op.entryHash))
2329
+ continue;
2330
+ if (!op.entryHash && knownByTuple.has(`${op.type}\x00${op.path}\x00${op.rootAfter}`))
2331
+ continue;
2332
+ const stamped = applied === 0 && op.type !== "checkpoint" && op.parent === undefined ? { ...op, parent: forkBase } : { ...op };
2333
+ await local.log.append({ ...stamped, seq: await local.log.seq() + 1 });
2334
+ applied++;
2335
+ }
2336
+ let head = await local.log.head() ?? priorHead ?? await local.store.put(EMPTY_TREE3);
2337
+ let merged = false;
2338
+ let conflicts = [];
2339
+ if (priorHead && incomingHead && priorHead !== incomingHead) {
2340
+ const parents = buildAncestry(await local.log.history());
2341
+ for (const [c, ps] of priorAncestry) {
2342
+ let s = parents.get(c);
2343
+ if (!s)
2344
+ parents.set(c, s = new Set);
2345
+ for (const p of ps)
2346
+ s.add(p);
2347
+ }
2348
+ const priorIsAncestor = ancestorsOf(incomingHead, parents).has(priorHead);
2349
+ const incomingIsAncestor = ancestorsOf(priorHead, parents).has(incomingHead);
2350
+ if (incomingIsAncestor) {
2351
+ if (await local.log.head() !== priorHead)
2352
+ await appendHeadMove(local, priorHead, priorHead, incomingHead, actor3, at, "converge: keep ahead-of-incoming head");
2353
+ head = priorHead;
2354
+ } else if (!priorIsAncestor) {
2355
+ const base = input.base ?? mergeBase(priorHead, incomingHead, parents);
2356
+ const sync = new Store;
2357
+ const baseRoot = await hydrate2(local.store, sync, base);
2358
+ const oursRoot = await hydrate2(local.store, sync, priorHead);
2359
+ const theirsRoot = await hydrate2(local.store, sync, incomingHead);
2360
+ const result = merge2({ store: sync }, baseRoot, oursRoot, theirsRoot);
2361
+ conflicts = result.conflicts;
2362
+ await persist(sync, local.store, result.head);
2363
+ await local.log.append({
2364
+ seq: await local.log.seq() + 1,
2365
+ type: "checkpoint",
2366
+ path: "",
2367
+ rootAfter: result.head,
2368
+ at,
2369
+ by: actor3,
2370
+ message: conflicts.length ? `converge merge (${conflicts.length} conflict(s) kept with markers)` : "converge merge",
2371
+ parent: priorHead,
2372
+ parent2: incomingHead
2373
+ });
2374
+ head = result.head;
2375
+ merged = true;
2376
+ }
2377
+ }
2378
+ const finalOps = await local.log.history();
2379
+ const missingOps = finalOps.filter((o) => o.seq > sinceSeq);
2380
+ return { head, applied, merged, conflicts, missingOps };
2381
+ }
2382
+ async function appendHeadMove(local, head, parent, parent2, by, at, message) {
2383
+ await local.log.append({ seq: await local.log.seq() + 1, type: "checkpoint", path: "", rootAfter: head, at, by, message, parent, parent2 });
2384
+ }
2385
+
1989
2386
  // 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";
2387
+ import { chmodSync as chmodSync4, existsSync as existsSync9, 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
2388
  import { platform } from "node:os";
1992
- import { dirname as dirname4, join as join8, relative as relative4 } from "node:path";
2389
+ import { dirname as dirname4, join as join9, relative as relative4 } from "node:path";
1993
2390
  var SYMLINK_MODE3 = 40960;
1994
2391
  var EXEC_MODE3 = 493;
1995
2392
  function isolateCommand(command, scratch) {
@@ -2005,7 +2402,7 @@ function isolateCommand(command, scratch) {
2005
2402
  }
2006
2403
  throw new Error(`--isolate not yet wired for platform ${platform()} (macOS sandbox-exec only); omit --isolate to run unconfined`);
2007
2404
  }
2008
- function hydrate2(store, head, dir) {
2405
+ function hydrate3(store, head, dir) {
2009
2406
  if (!head)
2010
2407
  return 0;
2011
2408
  let n = 0;
@@ -2013,7 +2410,7 @@ function hydrate2(store, head, dir) {
2013
2410
  const blob = fileAt(store, head, p);
2014
2411
  if (!blob)
2015
2412
  continue;
2016
- const abs = join8(dir, p);
2413
+ const abs = join9(dir, p);
2017
2414
  mkdirSync6(dirname4(abs), { recursive: true });
2018
2415
  const mode = modeAt(store, head, p);
2019
2416
  if (mode === SYMLINK_MODE3) {
@@ -2035,7 +2432,7 @@ function hydrate2(store, head, dir) {
2035
2432
  }
2036
2433
  function walkDir(dir, base, pats, out = []) {
2037
2434
  for (const name of readdirSync6(dir)) {
2038
- const p = join8(dir, name);
2435
+ const p = join9(dir, name);
2039
2436
  const rel = relative4(base, p);
2040
2437
  if (isIgnored(rel, pats))
2041
2438
  continue;
@@ -2053,11 +2450,11 @@ async function capture(repo, dir, keep = []) {
2053
2450
  const pats = ignorePatterns();
2054
2451
  const files = new Set(walkDir(dir, dir, pats));
2055
2452
  for (const k of keep)
2056
- if (existsSync8(join8(dir, k)))
2453
+ if (existsSync9(join9(dir, k)))
2057
2454
  files.add(k);
2058
2455
  const written = [];
2059
2456
  for (const f of files) {
2060
- const abs = join8(dir, f);
2457
+ const abs = join9(dir, f);
2061
2458
  const st = lstatSync4(abs);
2062
2459
  if (st.isSymbolicLink()) {
2063
2460
  const target = readlinkSync4(abs);
@@ -2085,7 +2482,7 @@ async function capture(repo, dir, keep = []) {
2085
2482
  }
2086
2483
  const deleted = [];
2087
2484
  for (const t of await repo.list()) {
2088
- if (!existsSync8(join8(dir, t))) {
2485
+ if (!existsSync9(join9(dir, t))) {
2089
2486
  await repo.deleteFile(t);
2090
2487
  deleted.push(t);
2091
2488
  }
@@ -2094,7 +2491,7 @@ async function capture(repo, dir, keep = []) {
2094
2491
  }
2095
2492
 
2096
2493
  // src/bin/sol.ts
2097
- var CRED_PATH = join9(homedir(), ".sol", "credentials");
2494
+ var CRED_PATH = join10(homedir(), ".sol", "credentials");
2098
2495
  function tokenClaims(token) {
2099
2496
  try {
2100
2497
  return JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString());
@@ -2103,7 +2500,7 @@ function tokenClaims(token) {
2103
2500
  }
2104
2501
  }
2105
2502
  async function loadStoredToken() {
2106
- if (!existsSync9(CRED_PATH))
2503
+ if (!existsSync10(CRED_PATH))
2107
2504
  return;
2108
2505
  let creds;
2109
2506
  try {
@@ -2130,6 +2527,15 @@ async function loadStoredToken() {
2130
2527
  }
2131
2528
  return creds.accessToken;
2132
2529
  }
2530
+ function authExpired() {
2531
+ return die("session expired \u2014 run `sol auth login` (or set SOL_TOKEN)");
2532
+ }
2533
+ function identityFromToken(token) {
2534
+ const c = tokenClaims(token);
2535
+ if (!c.handle && !c.email && !c.userId && !c.sub)
2536
+ return;
2537
+ return { handle: c.handle, email: c.email, userId: c.userId ?? c.sub };
2538
+ }
2133
2539
  function authHost() {
2134
2540
  if (process.env.SOL_AUTH)
2135
2541
  return process.env.SOL_AUTH.replace(/\/+$/, "");
@@ -2148,15 +2554,81 @@ function resolveRemote(solDir3) {
2148
2554
  return;
2149
2555
  return cfg.url ? cfg : { ...cfg, url: DEFAULT_REMOTE_URL };
2150
2556
  }
2557
+ function cliVersion() {
2558
+ try {
2559
+ return JSON.parse(readFileSync9(new URL("./package.json", import.meta.url), "utf8")).version || "dev";
2560
+ } catch {
2561
+ return "dev";
2562
+ }
2563
+ }
2151
2564
  async function main() {
2152
2565
  const argv = process.argv.slice(2);
2153
2566
  if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
2154
- console.log("sol 0.1.0");
2567
+ console.log(`sol ${cliVersion()}`);
2155
2568
  return;
2156
2569
  }
2157
2570
  const args = argv.slice(1);
2158
2571
  const wantsHelp = args.includes("--help") || args.includes("-h");
2159
2572
  const SUBHELP = {
2573
+ commit: `sol commit "<message>" commit the whole working tree (single author)
2574
+ sol commit -m "<message>" <file>... commit ONLY those paths (correct attribution for concurrent agents)
2575
+
2576
+ flags:
2577
+ -m <message> message when scoping to files
2578
+ --whole-tree allow a whole-tree commit even with SOL_ACTOR set (else refused)
2579
+ --force commit even with unresolved <<<<<<< conflict markers
2580
+
2581
+ examples:
2582
+ sol commit "add login route"
2583
+ sol commit -m "fix parser" src/parse.ts src/lex.ts`,
2584
+ push: `sol push push the current branch to the configured remote (converging \u2014 never FF-rejected)
2585
+ sol push <repo> if no remote is set, use the hosted Sol (${DEFAULT_REMOTE_URL}) + this repo name, then push
2586
+ sol push --create <repo> same, explicit form (creates the repo on first push)
2587
+
2588
+ notes:
2589
+ a remote already configured? the <repo>/--create arg is ignored \u2014 \`sol push\` just syncs.
2590
+ set SOL_TOKEN (or \`sol auth login\`) first; the push registers the current branch's head on the remote.
2591
+
2592
+ examples:
2593
+ sol push # remote already configured
2594
+ sol push alice/app # one step: configure hosted remote + push`,
2595
+ pull: `sol pull fetch + converge the current branch from the configured remote
2596
+ sol pull <dir> OFFLINE: converge another local repo's .sol on disk into this one (multi-agent, no network)
2597
+
2598
+ notes:
2599
+ refuses to run over a dirty tree \u2014 commit or discard first. conflicts land in the working tree with markers.
2600
+ refuses to run while UNRESOLVED <<<<<<< markers remain \u2014 resolve them and \`sol commit\`, then pull again.`,
2601
+ seal: `sol seal <path> [recipient...] encrypt a file so the HOST only ever sees ciphertext (per-path privacy)
2602
+
2603
+ the content becomes host-blind ciphertext committed into history; the keys stay LOCAL (~/.sol keystore).
2604
+ you are always a recipient of your own seals. add other actors to share read access. \`sol cat\` decrypts
2605
+ for a recipient; everyone else sees <<sealed>>.
2606
+
2607
+ example:
2608
+ sol seal secrets/.env alice bob # alice, bob (and you) can read it; the server cannot`,
2609
+ remote: `sol remote show the configured remote
2610
+ sol remote <url> <repo> set the remote (url + repo name)
2611
+
2612
+ tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
2613
+ branch: `sol branch list branches (* = current)
2614
+ sol branch <name> [<ref>] create a branch at <ref> (default: HEAD)`,
2615
+ switch: `sol switch <branch> switch branches (commits/keeps current work first; never loses it)`,
2616
+ merge: `sol merge <branch> 3-way merge <branch> into the current branch
2617
+ conflicts land in the working tree with <<<<<<< markers \u2014 resolve, then \`sol commit\`.`,
2618
+ log: `sol log commit history for the current branch
2619
+ sol log <branch|commit> history scoped to a ref
2620
+ sol log --all every op in the log (not just commits)`,
2621
+ diff: `sol diff working tree vs HEAD
2622
+ sol diff <ref> working tree vs a branch/commit
2623
+ sol diff <ref> <ref> tree vs tree
2624
+ sol diff <path> one file, working tree vs HEAD`,
2625
+ restore: `sol restore [--from <ref>] [<path>...] restore a file (or the whole tree) from a ref (default HEAD)`,
2626
+ run: `sol run [--keep <path>]... [--isolate] <command...>
2627
+ hydrate the current tree to a sandbox, run the command, capture produced files back into a commit.
2628
+ --isolate confines the run (no network, writes stay in the sandbox).`,
2629
+ promote: "sol promote [branch] point the remote's production branch at <branch> (default: current branch)",
2630
+ git: `sol git import <repo> [dir] import a git repo's HEAD into a new Sol repo
2631
+ sol git export <repo> [-m "msg"] write Sol's tree back as a git commit, then \`git push\``,
2160
2632
  mr: `sol mr open [--from <branch>] [--to <branch>] [--upstream <repo>] -t "title" [-m body]
2161
2633
  sol mr list | show <id> | review <id> --approve|--request-changes|--comment [-m msg]
2162
2634
  sol mr comment <id> -m msg [--path f --line N] | check <id> --run -- <cmd> | merge <id> [--force] | close <id>`,
@@ -2176,12 +2648,12 @@ async function main() {
2176
2648
  if (t)
2177
2649
  process.env.SOL_TOKEN = t;
2178
2650
  }
2179
- const release = new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "pull", "run", "seal"]).has(cmd) && existsSync9(solDir2) ? acquireLock() : undefined;
2651
+ const release = new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "pull", "run", "seal"]).has(cmd) && existsSync10(solDir2) ? acquireLock() : undefined;
2180
2652
  try {
2181
2653
  switch (cmd) {
2182
2654
  case "init": {
2183
- const here = join9(procCwd2, ".sol");
2184
- if (existsSync9(here))
2655
+ const here = join10(procCwd2, ".sol");
2656
+ if (existsSync10(here))
2185
2657
  die("already a sol repo: " + procCwd2);
2186
2658
  if (repoRoot2 && repoRoot2 !== procCwd2 && !args.includes("--force")) {
2187
2659
  die(`already inside a Sol repo at ${repoRoot2}
@@ -2195,7 +2667,7 @@ async function main() {
2195
2667
  }
2196
2668
  case "track":
2197
2669
  case "add": {
2198
- if (!existsSync9(solDir2))
2670
+ if (!existsSync10(solDir2))
2199
2671
  die("not a sol repo \u2014 run `sol init` first");
2200
2672
  const files = args.filter((a) => a !== "." && !a.startsWith("-"));
2201
2673
  if (!files.length) {
@@ -2206,7 +2678,7 @@ async function main() {
2206
2678
  let n = 0;
2207
2679
  for (const f of files) {
2208
2680
  const rf = repoRel(f);
2209
- if (!existsSync9(join9(cwd2, rf))) {
2681
+ if (!existsSync10(join10(cwd2, rf))) {
2210
2682
  console.error("skip (not on disk): " + f);
2211
2683
  continue;
2212
2684
  }
@@ -2235,14 +2707,14 @@ async function main() {
2235
2707
  if (!message)
2236
2708
  die('commit needs a message: sol commit "what you did" (scoped: sol commit -m "msg" file1 file2)');
2237
2709
  const parentHead = await repo.head();
2238
- const mergeHeadPath = join9(solDir2, "MERGE_HEAD");
2239
- const parent2 = existsSync9(mergeHeadPath) ? readFileSync9(mergeHeadPath, "utf8").trim() || undefined : undefined;
2710
+ const mergeHeadPath = join10(solDir2, "MERGE_HEAD");
2711
+ const parent2 = existsSync10(mergeHeadPath) ? readFileSync9(mergeHeadPath, "utf8").trim() || undefined : undefined;
2240
2712
  let changed = 0;
2241
2713
  let commitRoot = parentHead;
2242
2714
  if (paths.length) {
2243
2715
  for (const p of paths) {
2244
2716
  const rp = repoRel(p);
2245
- if (existsSync9(join9(cwd2, rp))) {
2717
+ if (existsSync10(join10(cwd2, rp))) {
2246
2718
  if (await snapshotFile(repo, rp))
2247
2719
  changed++;
2248
2720
  } else if ((await repo.list()).includes(rp)) {
@@ -2255,7 +2727,8 @@ async function main() {
2255
2727
  commitRoot = await repo.head();
2256
2728
  } else {
2257
2729
  const optedIn = wholeTreeOptIn || process.env.SOL_ALLOW_WHOLE_TREE === "1";
2258
- if (process.env.SOL_ACTOR && !optedIn) {
2730
+ const otherActorPresent = (await log.history()).some((o) => o.by && o.by !== actor2);
2731
+ if (process.env.SOL_ACTOR && !optedIn && otherActorPresent) {
2259
2732
  die(`refusing a whole-tree commit as "${actor2}" \u2014 it would attribute ALL pending files to you.
2260
2733
  concurrent agents: sol commit -m "${message}" <your files> (or a per-agent SolWorkspace/MCP)
2261
2734
  if you truly own the whole tree: add --whole-tree or set SOL_ALLOW_WHOLE_TREE=1`);
@@ -2292,7 +2765,7 @@ async function main() {
2292
2765
  }
2293
2766
  case "status": {
2294
2767
  const { repo, log } = open();
2295
- const refs = existsSync9(refsPath()) ? await loadRefs(log) : undefined;
2768
+ const refs = existsSync10(refsPath()) ? await loadRefs(log) : undefined;
2296
2769
  const head = await repo.head();
2297
2770
  console.log(`repo ${cwd2}`);
2298
2771
  console.log(`on ${refs ? refs.current : "main"} head ${head.slice(0, 14)} seq ${await log.seq()} actor ${actor2}`);
@@ -2348,7 +2821,7 @@ async function main() {
2348
2821
  byRoot.set(o.rootAfter, o);
2349
2822
  const live = await log.head() ?? "";
2350
2823
  const refArg = args.find((a) => !a.startsWith("-"));
2351
- const lrefs = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : null;
2824
+ const lrefs = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : null;
2352
2825
  let tip = live;
2353
2826
  if (refArg) {
2354
2827
  tip = lrefs?.branches[refArg]?.head ?? refArg;
@@ -2359,19 +2832,27 @@ async function main() {
2359
2832
  }
2360
2833
  const chain = [];
2361
2834
  const seen = new Set;
2362
- let cur = tip;
2363
- while (cur && byRoot.has(cur) && !seen.has(cur)) {
2835
+ const stack = [tip];
2836
+ while (stack.length) {
2837
+ const cur = stack.pop();
2838
+ if (!cur || seen.has(cur) || !byRoot.has(cur))
2839
+ continue;
2364
2840
  seen.add(cur);
2365
2841
  const c = byRoot.get(cur);
2366
2842
  chain.push(c);
2367
- cur = c.parent;
2843
+ if (c.parent)
2844
+ stack.push(c.parent);
2845
+ if (c.parent2)
2846
+ stack.push(c.parent2);
2368
2847
  }
2848
+ chain.sort((a, b) => b.seq - a.seq);
2369
2849
  if (!chain.length) {
2370
2850
  console.log(ops.length ? '(no commits on this branch yet \u2014 run `sol commit "msg"`)' : "(no history yet)");
2371
2851
  }
2372
2852
  for (const c of chain) {
2373
2853
  const n = countChanges(store, c.parent ?? "", c.rootAfter);
2374
- const tag = c.parent2 ? ` [merge ${c.parent2.slice(0, 8)}]` : "";
2854
+ const mergedIn = [c.parent2 && byRoot.get(c.parent2)?.by].filter(Boolean);
2855
+ const tag = c.parent2 ? ` [merge ${c.parent2.slice(0, 8)}${mergedIn.length ? ` <- ${mergedIn.join(", ")}` : ""}]` : "";
2375
2856
  console.log(`${c.rootAfter.slice(0, 14)} ${(c.by ?? "?").padEnd(14)} ${c.message ?? ""} (${n} change${n === 1 ? "" : "s"})${tag}`);
2376
2857
  }
2377
2858
  const wc = workingChanges(store, live);
@@ -2384,7 +2865,7 @@ async function main() {
2384
2865
  const path = args[0] || die("rm needs a path");
2385
2866
  let onDisk = false;
2386
2867
  try {
2387
- unlinkSync6(join9(cwd2, path));
2868
+ unlinkSync6(join10(cwd2, path));
2388
2869
  onDisk = true;
2389
2870
  } catch {}
2390
2871
  if (onDisk) {
@@ -2401,10 +2882,26 @@ async function main() {
2401
2882
  case "diff": {
2402
2883
  const { log } = open();
2403
2884
  const store = loadStore();
2885
+ const head = await log.head() ?? "";
2404
2886
  if (args.length >= 2) {
2405
2887
  printTreeDiff(diffTrees(store, (await resolveRef(log, args[0])).head, (await resolveRef(log, args[1])).head));
2888
+ } else if (args.length === 1) {
2889
+ const resolved = await resolveRefSoft(log, args[0]);
2890
+ if (resolved) {
2891
+ printWorkingDiff(store, resolved.head);
2892
+ } else {
2893
+ const path = isWorkingPath(store, head, args[0]);
2894
+ if (path)
2895
+ printWorkingDiff(store, head, path);
2896
+ else
2897
+ die(`'${args[0]}' is neither a ref nor a working-tree path.
2898
+ usage: sol diff (working tree vs HEAD)
2899
+ sol diff <ref> (working tree vs a branch/commit)
2900
+ sol diff <ref> <ref> (tree vs tree)
2901
+ sol diff <path> (one file, working tree vs HEAD)`);
2902
+ }
2406
2903
  } else {
2407
- printWorkingDiff(store, (await resolveRef(log, args[0])).head);
2904
+ printWorkingDiff(store, head);
2408
2905
  }
2409
2906
  break;
2410
2907
  }
@@ -2535,11 +3032,11 @@ async function main() {
2535
3032
  };
2536
3033
  for (const op of ops)
2537
3034
  walk(op.rootAfter);
2538
- const objDir = join9(solDir2, "objects");
3035
+ const objDir = join10(solDir2, "objects");
2539
3036
  let removed = 0;
2540
3037
  for (const name of readdirSync7(objDir)) {
2541
3038
  if (name.endsWith(".tmp") || !reachable.has(name)) {
2542
- unlinkSync6(join9(objDir, name));
3039
+ unlinkSync6(join10(objDir, name));
2543
3040
  removed++;
2544
3041
  }
2545
3042
  }
@@ -2553,8 +3050,8 @@ async function main() {
2553
3050
  console.log(p);
2554
3051
  break;
2555
3052
  }
2556
- const f = join9(cwd2, ".solignore");
2557
- const lead = existsSync9(f) && !readFileSync9(f, "utf8").endsWith(`
3053
+ const f = join10(cwd2, ".solignore");
3054
+ const lead = existsSync10(f) && !readFileSync9(f, "utf8").endsWith(`
2558
3055
  `) ? `
2559
3056
  ` : "";
2560
3057
  appendFileSync4(f, lead + pat + `
@@ -2572,8 +3069,8 @@ async function main() {
2572
3069
  if (content === SEALED2)
2573
3070
  die("already sealed: " + path);
2574
3071
  if (content === undefined) {
2575
- const abs = join9(cwd2, path);
2576
- if (!existsSync9(abs))
3072
+ const abs = join10(cwd2, path);
3073
+ if (!existsSync10(abs))
2577
3074
  die("no such file: " + path);
2578
3075
  content = readFileSync9(abs, "utf8");
2579
3076
  }
@@ -2639,7 +3136,7 @@ async function main() {
2639
3136
  refs.current = name;
2640
3137
  saveRefs(refs);
2641
3138
  const n = materializeDiff(loadStore(), fromHead, target.head);
2642
- console.log(`switched to ${name} (${(target.head || "empty").slice(0, 12)}); working tree = ${n} file(s)`);
3139
+ console.log(`switched to ${name} (${(target.head || "empty").slice(0, 12)}); ${n} file(s) changed`);
2643
3140
  break;
2644
3141
  }
2645
3142
  case "merge": {
@@ -2660,7 +3157,7 @@ async function main() {
2660
3157
  const result = merge({ store }, other.base, ours, other.head);
2661
3158
  if (result.conflicts.length) {
2662
3159
  materializeTree(store, result.head);
2663
- writeFileSync9(join9(solDir2, "MERGE_HEAD"), other.head);
3160
+ writeFileSync9(join10(solDir2, "MERGE_HEAD"), other.head);
2664
3161
  console.log(`merge ${name} -> ${result.conflicts.length} conflict(s), left in your working tree (uncommitted):`);
2665
3162
  for (const c of result.conflicts)
2666
3163
  console.log(" " + c.path);
@@ -2679,7 +3176,7 @@ async function main() {
2679
3176
  break;
2680
3177
  }
2681
3178
  case "remote": {
2682
- if (!existsSync9(solDir2))
3179
+ if (!existsSync10(solDir2))
2683
3180
  die("not a sol repo");
2684
3181
  if (args[0]) {
2685
3182
  const repoName = args[1] || die("usage: sol remote <url> <repo>");
@@ -2733,7 +3230,7 @@ async function main() {
2733
3230
  if (!c.handle)
2734
3231
  console.log(" (no handle yet \u2014 set one in the web app to get your <handle>/<repo> namespace)");
2735
3232
  } else if (sub === "logout") {
2736
- if (existsSync9(CRED_PATH))
3233
+ if (existsSync10(CRED_PATH))
2737
3234
  unlinkSync6(CRED_PATH);
2738
3235
  console.log("logged out");
2739
3236
  } else if (sub === "status" || !sub) {
@@ -2742,7 +3239,7 @@ async function main() {
2742
3239
  console.log(`authenticated via SOL_TOKEN (env)${c2.handle ? ` as @${c2.handle}` : ""}`);
2743
3240
  break;
2744
3241
  }
2745
- if (!existsSync9(CRED_PATH)) {
3242
+ if (!existsSync10(CRED_PATH)) {
2746
3243
  console.log("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
2747
3244
  break;
2748
3245
  }
@@ -2753,20 +3250,26 @@ async function main() {
2753
3250
  } else if (sub === "whoami") {
2754
3251
  const token = process.env.SOL_TOKEN || await loadStoredToken();
2755
3252
  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>\`)`);
3253
+ authExpired();
3254
+ let id = identityFromToken(token);
3255
+ try {
3256
+ const res = await fetch(`${authHost()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
3257
+ if (res.status === 401 && !id)
3258
+ authExpired();
3259
+ if (res.ok) {
3260
+ const me = await res.json();
3261
+ id = { handle: me.handle ?? id?.handle, email: me.email ?? id?.email, userId: id?.userId };
3262
+ }
3263
+ } catch {}
3264
+ if (!id)
3265
+ authExpired();
3266
+ console.log(id.handle ? `signed in as @${id.handle} ${id.email ?? ""}`.trimEnd() : `signed in${id.email ? ` as ${id.email}` : ""} \u2014 no handle yet (\`sol auth set-handle <name>\`)`);
2764
3267
  } else if (sub === "set-handle") {
2765
3268
  const want = args[1] || die("usage: sol auth set-handle <name>");
2766
3269
  const handle = want.toLowerCase();
2767
3270
  const token = process.env.SOL_TOKEN || await loadStoredToken();
2768
3271
  if (!token)
2769
- die("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
3272
+ authExpired();
2770
3273
  let hadHandle = false;
2771
3274
  try {
2772
3275
  const meRes = await fetch(`${authHost()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
@@ -2775,7 +3278,7 @@ async function main() {
2775
3278
  } catch {}
2776
3279
  const res = await fetch(`${authHost()}/api/auth/handle`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ handle }) });
2777
3280
  if (res.status === 401)
2778
- die("not logged in \u2014 your session expired; run `sol auth login` again");
3281
+ authExpired();
2779
3282
  if (!res.ok) {
2780
3283
  let msg = `could not set handle (${res.status})`;
2781
3284
  try {
@@ -2790,7 +3293,7 @@ async function main() {
2790
3293
  console.log("heads-up: changing your handle re-namespaces your repos under the new <handle>/<repo>.");
2791
3294
  console.log(`handle set to @${out.handle}`);
2792
3295
  } else if (sub === "pat") {
2793
- if (!existsSync9(CRED_PATH))
3296
+ if (!existsSync10(CRED_PATH))
2794
3297
  die("run `sol auth login` first");
2795
3298
  const creds = JSON.parse(readFileSync9(CRED_PATH, "utf8"));
2796
3299
  const token = await loadStoredToken();
@@ -2844,12 +3347,33 @@ async function main() {
2844
3347
  break;
2845
3348
  }
2846
3349
  case "clone": {
3350
+ const localSrc = args[0] ? localPeerSolDir(args[0], procCwd2) : undefined;
3351
+ if (localSrc) {
3352
+ const peer = openPeer(localSrc);
3353
+ const peerHead = await peer.log.head();
3354
+ const dest = resolve4(procCwd2, args[1] || (args[0].replace(/\/+$/, "").split("/").pop() || "clone") + "-clone");
3355
+ const ddir = join10(dest, ".sol");
3356
+ if (existsSync10(ddir))
3357
+ die("already a sol repo: " + dest);
3358
+ mkdirSync7(ddir, { recursive: true });
3359
+ const res = await converge({ store: new FileStore(ddir), log: new FileOpLog(ddir) }, { nodes: await peerNodes(peer, peerHead), ops: await peer.log.history(), incomingHead: peerHead, actor: actor2 });
3360
+ const dstore = new Store2;
3361
+ for (const n2 of await peerNodes({ store: new FileStore(ddir), log: new FileOpLog(ddir) }, res.head))
3362
+ dstore.put(n2);
3363
+ const files = res.head ? listAll3(dstore, res.head) : [];
3364
+ for (const f of files)
3365
+ materializeInto(dstore, res.head, dest, f);
3366
+ writeFileSync9(join10(ddir, "refs.json"), JSON.stringify({ current: "main", branches: { main: { head: res.head, base: res.head, remote: res.head } }, tags: {} }, null, 2));
3367
+ writeWorkingIndexAt(ddir, dest, files);
3368
+ console.log(`cloned local peer ${args[0]} -> ${dest} (${(await peer.log.history()).length} ops, ${files.length} files)`);
3369
+ break;
3370
+ }
2847
3371
  const [url, rest] = remoteUrlArg(args);
2848
3372
  const repoName = rest[0] || die("usage: sol clone [<url>] <owner>/<repo> [dir]");
2849
3373
  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))
3374
+ const target = resolve4(cwd2, rest[1] || repoName.split("/").pop() || repoName);
3375
+ const fdir = join10(target, ".sol");
3376
+ if (existsSync10(fdir))
2853
3377
  die("already a sol repo: " + target);
2854
3378
  const cfg = { url, repo: repoName };
2855
3379
  const bundle = await remoteExport(cfg, token);
@@ -2865,63 +3389,62 @@ async function main() {
2865
3389
  cloneBranches[name] = { head: h, base: h, remote: h };
2866
3390
  if (!cloneBranches[onBranch])
2867
3391
  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 }));
3392
+ writeFileSync9(join10(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
3393
+ writeFileSync9(join10(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
2870
3394
  const store = new Store2;
2871
3395
  for (const node of bundle.nodes)
2872
3396
  store.put(node);
2873
3397
  let n = 0;
2874
3398
  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
- }
3399
+ const files = listAll3(store, checkoutHead);
3400
+ for (const f of files)
3401
+ if (materializeInto(store, checkoutHead, target, f))
3402
+ n++;
3403
+ writeWorkingIndexAt(fdir, target, files);
2886
3404
  }
2887
3405
  console.log(`cloned ${repoName} -> ${rest[1] || repoName} (${bundle.ops.length} ops, ${Object.keys(cloneBranches).length} branch(es), ${n} files, on ${onBranch})`);
2888
3406
  break;
2889
3407
  }
2890
3408
  case "push": {
2891
3409
  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");
3410
+ if (!loadRemote(solDir2)) {
3411
+ const ci = args.indexOf("--create");
3412
+ const repoName = ci >= 0 ? args[ci + 1] : args.find((a) => !a.startsWith("-"));
3413
+ if (repoName) {
3414
+ saveRemote(solDir2, { url: DEFAULT_REMOTE_URL, repo: repoName });
3415
+ console.log(`remote set: ${DEFAULT_REMOTE_URL} (repo ${repoName})`);
3416
+ }
3417
+ }
3418
+ const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`, or `sol push <repo>` to use the hosted Sol");
3419
+ const token = process.env.SOL_TOKEN || authExpired();
2894
3420
  const rh = await remoteHead(cfg, token);
2895
3421
  const ops = await log.history();
2896
3422
  const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
2897
3423
  const localTip = await log.logTip();
2898
- const localRefs = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : undefined;
3424
+ const localRefs = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : undefined;
2899
3425
  const branch = localRefs?.current ?? "main";
2900
3426
  const branchHead = await log.head() ?? "";
3427
+ const remoteWasEmpty = rh.seq === 0 && !rh.tip;
2901
3428
  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");
3429
+ console.log(remoteWasEmpty ? "nothing to push \u2014 local repo is empty (commit something first)" : "everything up to date");
2912
3430
  break;
2913
3431
  }
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
- }
3432
+ if (localSeq <= rh.seq) {}
3433
+ const forkBase = localRefs?.branches[branch]?.remote;
3434
+ const baseOp = forkBase ? ops.find((o) => o.rootAfter === forkBase) : undefined;
3435
+ const fromSeq = baseOp ? baseOp.seq : rh.seq;
3436
+ const res = await remotePush(cfg, token, {
3437
+ nodes: allLocalNodes(),
3438
+ ops: ops.filter((o) => o.seq > fromSeq),
3439
+ branch,
3440
+ head: branchHead,
3441
+ expectedHead: forkBase
3442
+ });
2922
3443
  const canon = await remoteExport(cfg, token);
3444
+ const prevHead = await log.head() ?? "";
2923
3445
  await writeBundle(solDir2, canon, 0);
2924
- if (existsSync9(refsPath())) {
3446
+ const convergedHead = res.head ?? canon.refs?.branches?.[branch] ?? branchHead;
3447
+ if (existsSync10(refsPath())) {
2925
3448
  const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
2926
3449
  if (canon.refs) {
2927
3450
  for (const [name, h] of Object.entries(canon.refs.branches)) {
@@ -2929,28 +3452,76 @@ async function main() {
2929
3452
  }
2930
3453
  }
2931
3454
  if (refs.branches[branch]) {
2932
- refs.branches[branch].head = res.head ?? branchHead;
2933
- refs.branches[branch].remote = res.head ?? branchHead;
3455
+ refs.branches[branch].head = convergedHead;
3456
+ refs.branches[branch].remote = convergedHead;
2934
3457
  }
2935
3458
  saveRefs(refs);
2936
3459
  }
2937
- setOpLogHead(res.head ?? branchHead);
3460
+ setOpLogHead(convergedHead);
3461
+ if (convergedHead && convergedHead !== prevHead)
3462
+ materializeDiff(loadStore(), prevHead, convergedHead);
2938
3463
  const prod = res.refs?.production;
2939
- console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(res.head ?? "").slice(0, 12)})${prod ? `; production=${prod}` : ""}`);
3464
+ const mergedNote = res.merged ? " (converged with a concurrent commit)" : "";
3465
+ const conflictNote = res.conflicts && res.conflicts.length ? ` \u2014 ${res.conflicts.length} conflict(s) kept with markers in: ${res.conflicts.map((c) => c.path).join(", ")}` : "";
3466
+ if (remoteWasEmpty) {
3467
+ console.log(`created remote repo ${cfg.repo} \u2014 pushed ${res.applied} op(s) (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${prod ? `; production=${prod}` : ""}`);
3468
+ } else {
3469
+ console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${mergedNote}${conflictNote}${prod ? `; production=${prod}` : ""}`);
3470
+ }
2940
3471
  break;
2941
3472
  }
2942
3473
  case "pull": {
2943
3474
  const { log } = open();
3475
+ {
3476
+ const marked = unresolvedConflictPaths();
3477
+ if (marked.length)
3478
+ die(`unresolved conflict markers in ${marked.join(", ")} \u2014 resolve them and \`sol commit\`, then pull again.`);
3479
+ }
3480
+ const peerArg = args[0];
3481
+ const peerSolDir = peerArg ? localPeerSolDir(peerArg, cwd2) : undefined;
3482
+ if (peerSolDir) {
3483
+ const wcLocal = workingChanges(loadStore(), await log.head() ?? "");
3484
+ if (wcLocal.added.length + wcLocal.modified.length + wcLocal.removed.length > 0) {
3485
+ die("you have uncommitted changes \u2014 commit (`sol commit`) or discard them before pulling.");
3486
+ }
3487
+ const peer = openPeer(peerSolDir);
3488
+ const peerHead = await peer.log.head();
3489
+ if (!peerHead) {
3490
+ console.log("peer repo is empty \u2014 nothing to pull");
3491
+ break;
3492
+ }
3493
+ const prevHead = await log.head() ?? "";
3494
+ const res = await converge({ store: new FileStore(solDir2), log }, { nodes: await peerNodes(peer, peerHead), ops: await peer.log.history(), incomingHead: peerHead, actor: actor2 });
3495
+ if (existsSync10(refsPath())) {
3496
+ const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
3497
+ if (refs.branches[refs.current])
3498
+ refs.branches[refs.current].head = res.head;
3499
+ saveRefs(refs);
3500
+ }
3501
+ setOpLogHead(res.head);
3502
+ if (res.head !== prevHead)
3503
+ materializeDiff(loadStore(), prevHead, res.head);
3504
+ if (res.conflicts.length) {
3505
+ console.log(`pulled + merged from ${peerArg} WITH ${res.conflicts.length} conflict(s) \u2014 both sides kept with markers in:`);
3506
+ for (const c of res.conflicts)
3507
+ console.log(" " + c.path);
3508
+ console.log("resolve the <<<<<<< markers, then `sol commit`");
3509
+ process.exitCode = 1;
3510
+ } else {
3511
+ console.log(`pulled from ${peerArg} -> ${res.applied} new op(s)${res.merged ? " (converged by merge)" : ""}, now at ${(res.head || "").slice(0, 12)}`);
3512
+ }
3513
+ break;
3514
+ }
2944
3515
  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");
3516
+ const token = process.env.SOL_TOKEN || authExpired();
2946
3517
  const bundle = await remoteExport(cfg, token);
2947
3518
  const ops = await log.history();
2948
3519
  const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
2949
3520
  const localTip = await log.logTip();
2950
- const curBranch = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : bundle.refs?.production || "main";
3521
+ const curBranch = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : bundle.refs?.production || "main";
2951
3522
  const remoteCurHead = bundle.refs?.branches?.[curBranch] ?? bundle.head ?? "";
2952
3523
  if (bundle.seq === localSeq && bundle.tip === localTip) {
2953
- if (bundle.refs && existsSync9(refsPath())) {
3524
+ if (bundle.refs && existsSync10(refsPath())) {
2954
3525
  const lr = JSON.parse(readFileSync9(refsPath(), "utf8"));
2955
3526
  const before = lr.branches[lr.current]?.head;
2956
3527
  for (const [name, h] of Object.entries(bundle.refs.branches))
@@ -2975,7 +3546,7 @@ async function main() {
2975
3546
  break;
2976
3547
  }
2977
3548
  const syncRefHead = (h) => {
2978
- if (!existsSync9(refsPath()))
3549
+ if (!existsSync10(refsPath()))
2979
3550
  return;
2980
3551
  const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
2981
3552
  if (refs.branches[refs.current])
@@ -2995,7 +3566,7 @@ async function main() {
2995
3566
  syncRefHead(remoteCurHead);
2996
3567
  setOpLogHead(remoteCurHead);
2997
3568
  materializeTree(loadStore(), remoteCurHead);
2998
- if (bundle.refs && existsSync9(refsPath())) {
3569
+ if (bundle.refs && existsSync10(refsPath())) {
2999
3570
  const lr = JSON.parse(readFileSync9(refsPath(), "utf8"));
3000
3571
  for (const [name, h] of Object.entries(bundle.refs.branches))
3001
3572
  lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
@@ -3044,9 +3615,9 @@ async function main() {
3044
3615
  for (const c of result.conflicts) {
3045
3616
  const blob = fileAt3(store, result.head, c.path);
3046
3617
  if (blob)
3047
- writeFileSync9(join9(cwd2, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
3618
+ writeFileSync9(join10(cwd2, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
3048
3619
  }
3049
- writeFileSync9(join9(solDir2, "MERGE_HEAD"), remoteHead2);
3620
+ writeFileSync9(join10(solDir2, "MERGE_HEAD"), remoteHead2);
3050
3621
  console.log(`pulled + merged WITH ${result.conflicts.length} conflict(s), left uncommitted in your working tree:`);
3051
3622
  for (const c of result.conflicts)
3052
3623
  console.log(" " + c.path);
@@ -3058,11 +3629,11 @@ async function main() {
3058
3629
  break;
3059
3630
  }
3060
3631
  case "promote": {
3061
- if (!existsSync9(solDir2))
3632
+ if (!existsSync10(solDir2))
3062
3633
  die("not a sol repo");
3063
3634
  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";
3635
+ const token = process.env.SOL_TOKEN || authExpired();
3636
+ const cur = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : "main";
3066
3637
  const branch = args[0] || cur;
3067
3638
  const refs = await remotePromote(cfg, token, branch);
3068
3639
  console.log(`promoted '${branch}' -> production '${refs.production}' now at ${(refs.branches[refs.production] ?? "").slice(0, 12)}`);
@@ -3072,10 +3643,10 @@ async function main() {
3072
3643
  const [url, frest] = remoteUrlArg(args);
3073
3644
  const parent = frest[0] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
3074
3645
  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))
3646
+ const token = process.env.SOL_TOKEN || authExpired();
3647
+ const target = resolve4(cwd2, frest[2] || newRepo);
3648
+ const fdir = join10(target, ".sol");
3649
+ if (existsSync10(fdir))
3079
3650
  die("already a sol repo: " + target);
3080
3651
  const parentCfg = { url, repo: parent };
3081
3652
  const newCfg = { url, repo: newRepo, forkParent: parent };
@@ -3100,32 +3671,26 @@ async function main() {
3100
3671
  const cloneBranches = {};
3101
3672
  for (const [name, h] of Object.entries(srvRefs.branches))
3102
3673
  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 }));
3674
+ writeFileSync9(join10(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
3675
+ writeFileSync9(join10(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
3105
3676
  saveRemote(fdir, newCfg);
3106
3677
  const store = new Store2;
3107
3678
  for (const node of canon.nodes)
3108
3679
  store.put(node);
3109
3680
  let n = 0;
3110
3681
  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
- }
3682
+ const files = listAll3(store, checkoutHead);
3683
+ for (const f of files)
3684
+ if (materializeInto(store, checkoutHead, target, f))
3685
+ n++;
3686
+ writeWorkingIndexAt(fdir, target, files);
3122
3687
  }
3123
3688
  console.log(`forked ${parent} -> ${newRepo} (your copy at ${args[3] || newRepo}: ${Object.keys(cloneBranches).length} branch(es), ${n} files; parent: ${parent})`);
3124
3689
  break;
3125
3690
  }
3126
3691
  case "access": {
3692
+ const token = process.env.SOL_TOKEN || authExpired();
3127
3693
  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
3694
  const sub = args[0];
3130
3695
  if (!sub || sub === "show") {
3131
3696
  const a = await accessGet(cfg, token);
@@ -3160,7 +3725,7 @@ async function main() {
3160
3725
  }
3161
3726
  case "forks": {
3162
3727
  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");
3728
+ const token = process.env.SOL_TOKEN || authExpired();
3164
3729
  const { forks } = await forksList(cfg, token);
3165
3730
  if (!forks.length)
3166
3731
  console.log(`(no forks of ${cfg.repo})`);
@@ -3173,9 +3738,9 @@ async function main() {
3173
3738
  }
3174
3739
  case "mr": {
3175
3740
  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");
3741
+ const token = process.env.SOL_TOKEN || authExpired();
3177
3742
  const sub = args[0];
3178
- const localRefs = () => existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
3743
+ const localRefs = () => existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
3179
3744
  const flag = (name) => {
3180
3745
  const i = args.indexOf(name);
3181
3746
  return i >= 0 ? args[i + 1] : undefined;
@@ -3352,9 +3917,9 @@ ${mrSummary(pr)}`);
3352
3917
  if (!command.length)
3353
3918
  die("usage: sol run [--keep <path>] [--isolate] <command...>");
3354
3919
  const head = await repo.head();
3355
- const dir = mkdtempSync(join9(tmpdir(), "sol-run-"));
3920
+ const dir = mkdtempSync(join10(tmpdir(), "sol-run-"));
3356
3921
  try {
3357
- const hn = hydrate2(loadStore(), head, dir);
3922
+ const hn = hydrate3(loadStore(), head, dir);
3358
3923
  console.log(`hydrated ${hn} file(s) -> sandbox${isolate ? " (isolated: no network, writes confined to the sandbox)" : ""}`);
3359
3924
  const argv2 = isolate ? isolateCommand(command, dir) : command;
3360
3925
  console.log(`$ ${command.join(" ")}`);
@@ -3372,7 +3937,7 @@ ${mrSummary(pr)}`);
3372
3937
  const { written, deleted } = await capture(repo, dir, keep);
3373
3938
  if (written.length || deleted.length) {
3374
3939
  await appendCommit(log, await repo.head(), `run: ${command.join(" ")}`, head);
3375
- if (existsSync9(refsPath())) {
3940
+ if (existsSync10(refsPath())) {
3376
3941
  const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
3377
3942
  if (refs.branches[refs.current]) {
3378
3943
  refs.branches[refs.current].head = await repo.head();
@@ -3385,7 +3950,7 @@ ${mrSummary(pr)}`);
3385
3950
  materialize(synced, nh, f);
3386
3951
  for (const f of deleted) {
3387
3952
  try {
3388
- unlinkSync6(join9(cwd2, f));
3953
+ unlinkSync6(join10(cwd2, f));
3389
3954
  } catch {}
3390
3955
  }
3391
3956
  console.log(`captured ${written.length} written, ${deleted.length} deleted file(s):`);
@@ -3404,10 +3969,10 @@ ${mrSummary(pr)}`);
3404
3969
  case "git": {
3405
3970
  const sub = args[0];
3406
3971
  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))
3972
+ const gitPath = resolve4(cwd2, args[1] || die("usage: sol git import <git-repo> [dir]"));
3973
+ const target = resolve4(cwd2, args[2] || basename(gitPath));
3974
+ const fdir = join10(target, ".sol");
3975
+ if (existsSync10(fdir))
3411
3976
  die("already a sol repo: " + target);
3412
3977
  mkdirSync7(fdir, { recursive: true });
3413
3978
  const { commits, branches, head, current } = await importGitRepo(gitPath, fdir);
@@ -3417,20 +3982,20 @@ ${mrSummary(pr)}`);
3417
3982
  if (!refsBranches[current])
3418
3983
  refsBranches[current] = { head, base: head };
3419
3984
  refsBranches[current].head = head;
3420
- writeFileSync9(join9(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
3985
+ writeFileSync9(join10(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
3421
3986
  const store = new Store2;
3422
- for (const name of readdirSync7(join9(fdir, "objects"))) {
3987
+ for (const name of readdirSync7(join10(fdir, "objects"))) {
3423
3988
  if (name.endsWith(".tmp"))
3424
3989
  continue;
3425
3990
  try {
3426
- store.put(decodeObject(readFileSync9(join9(fdir, "objects", name))));
3991
+ store.put(decodeObject(readFileSync9(join10(fdir, "objects", name))));
3427
3992
  } catch {}
3428
3993
  }
3429
- const onDisk = hydrate2(store, head, target);
3994
+ const onDisk = hydrate3(store, head, target);
3430
3995
  console.log(`imported ${commits} commit(s), ${branches.length} branch(es) from git -> ${args[2] || basename(gitPath)} (${onDisk} files; on branch ${current})`);
3431
3996
  } else if (sub === "export") {
3432
3997
  const { log } = open();
3433
- const gitPath = resolve3(cwd2, args[1] || die('usage: sol git export <git-repo> [-m "msg"]'));
3998
+ const gitPath = resolve4(cwd2, args[1] || die('usage: sol git export <git-repo> [-m "msg"]'));
3434
3999
  const mi = args.indexOf("-m");
3435
4000
  const store = loadStore();
3436
4001
  const head = await log.head() ?? "";
@@ -3470,6 +4035,10 @@ everyday (examples are copy-safe \u2014 use real filenames):
3470
4035
  sol ignore "*.tmp" add an ignore pattern (no arg lists the active patterns)
3471
4036
  sol fsck / sol gc verify integrity / drop unreachable objects
3472
4037
 
4038
+ privacy (native per-path encryption \u2014 the host only ever stores ciphertext):
4039
+ sol seal secrets/.env alice encrypt a file to recipients (you + alice); keys stay LOCAL, history is host-blind
4040
+ \`sol cat\` decrypts for a recipient; everyone else sees <<sealed>>. (sol seal --help)
4041
+
3473
4042
  branches & tags:
3474
4043
  sol branch list branches (sol branch feature creates one at HEAD)
3475
4044
  sol switch feature switch to a branch (captures current work first, never loses it)
@@ -3486,6 +4055,7 @@ auth (sign in once; remote commands then use the cached token, no SOL_TOKEN need
3486
4055
  remotes (self-hostable backend; token in SOL_TOKEN or via sol auth login):
3487
4056
  sol clone [<url>] <owner>/<repo> [dir] clone a remote repo (checks out PRODUCTION); default dir = <repo>
3488
4057
  sol push / sol pull sync your commits with the remote (push registers your branch's head)
4058
+ sol push <repo> one-step share: no remote set? use the hosted Sol + <repo>, then push
3489
4059
  sol promote [branch] point the remote's production branch at <branch> (default: current)
3490
4060
  sol remote <url> <repo> set the remote (no arg: show it; url defaults to the hosted Sol)
3491
4061
  sol fork [<url>] <parent> <new> [dir] make your own copy of a repo (all branches + history + a parent link)
@@ -3515,4 +4085,9 @@ addressed; history is a tamper-evident hash-chained op-log. attribute changes wi
3515
4085
  release?.();
3516
4086
  }
3517
4087
  }
3518
- main().catch((e) => die(e?.message || String(e)));
4088
+ main().catch((e) => {
4089
+ const msg = e?.message || String(e);
4090
+ if (/ -> 401\b/.test(msg) || /\b401 unauthorized\b/i.test(msg))
4091
+ authExpired();
4092
+ die(msg);
4093
+ });