midsummer-sol 0.1.2 → 0.1.4

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 +673 -133
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) {
@@ -842,7 +885,6 @@ import {
842
885
  readdirSync as readdirSync3,
843
886
  readFileSync as readFileSync3,
844
887
  readlinkSync,
845
- rmdirSync,
846
888
  symlinkSync,
847
889
  unlinkSync,
848
890
  writeFileSync as writeFileSync3
@@ -1003,7 +1045,7 @@ function setHead(fdir, head) {
1003
1045
  }
1004
1046
  async function importGitRepo(gitPath, fdir) {
1005
1047
  const store = new FileStore2(fdir);
1006
- const log = new FileOpLog(fdir);
1048
+ const log = new FileOpLog2(fdir);
1007
1049
  const empty = await emptyRoot2(store);
1008
1050
  const commitList = git(gitPath, "rev-list", "--reverse", "--topo-order", "--all").toString().trim().split(`
1009
1051
  `).filter(Boolean);
@@ -1360,7 +1402,6 @@ import {
1360
1402
  readdirSync as readdirSync5,
1361
1403
  readFileSync as readFileSync6,
1362
1404
  readlinkSync as readlinkSync3,
1363
- rmdirSync as rmdirSync2,
1364
1405
  symlinkSync as symlinkSync3,
1365
1406
  unlinkSync as unlinkSync4,
1366
1407
  writeFileSync as writeFileSync6
@@ -1394,7 +1435,7 @@ function die(msg) {
1394
1435
  function open() {
1395
1436
  if (!existsSync6(solDir2))
1396
1437
  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) };
1438
+ return { repo: new AsyncRepo(new FileStore2(solDir2), new FileOpLog2(solDir2), actor2), log: new FileOpLog2(solDir2) };
1398
1439
  }
1399
1440
  var DEFAULT_IGNORE2 = [".sol", ".git", "node_modules", "dist", "build", ".next", ".cache", ".DS_Store", "__pycache__", "*.pyc", ".venv", "venv", "target", ".gradle", "*.log"];
1400
1441
  function ignorePatterns2() {
@@ -1538,20 +1579,42 @@ async function snapshotTree(repo) {
1538
1579
  function sleepSync(ms) {
1539
1580
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1540
1581
  }
1582
+ var LOCK_STALE_MS = 5000;
1583
+ function pidAlive(pid) {
1584
+ if (!pid || pid < 1)
1585
+ return false;
1586
+ try {
1587
+ process.kill(pid, 0);
1588
+ return true;
1589
+ } catch (e) {
1590
+ return e?.code === "EPERM";
1591
+ }
1592
+ }
1541
1593
  function acquireLock() {
1542
- const lockDir = join6(solDir2, "lock");
1594
+ const lockFile = join6(solDir2, "lock");
1543
1595
  const deadline = Date.now() + 15000;
1544
1596
  for (;; ) {
1545
1597
  try {
1546
- mkdirSync5(lockDir);
1598
+ writeFileSync6(lockFile, String(process.pid), { flag: "wx" });
1547
1599
  break;
1548
1600
  } catch {
1549
1601
  try {
1550
- if (Date.now() - lstatSync3(lockDir).mtimeMs > 30000) {
1551
- rmdirSync2(lockDir);
1552
- continue;
1602
+ const st = lstatSync3(lockFile);
1603
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
1604
+ let ownerPid = 0;
1605
+ try {
1606
+ ownerPid = parseInt(readFileSync6(lockFile, "utf8").trim(), 10) || 0;
1607
+ } catch {}
1608
+ if (!pidAlive(ownerPid)) {
1609
+ try {
1610
+ unlinkSync4(lockFile);
1611
+ } catch {}
1612
+ continue;
1613
+ }
1553
1614
  }
1554
- } catch {}
1615
+ } catch {
1616
+ continue;
1617
+ }
1555
1618
  if (Date.now() > deadline)
1556
1619
  die("repo is locked by another sol process (timed out)");
1557
1620
  sleepSync(25);
@@ -1563,7 +1626,7 @@ function acquireLock() {
1563
1626
  return;
1564
1627
  released = true;
1565
1628
  try {
1566
- rmdirSync2(lockDir);
1629
+ unlinkSync4(lockFile);
1567
1630
  } catch {}
1568
1631
  };
1569
1632
  process.on("exit", release);
@@ -1890,6 +1953,27 @@ function printWorkingDiff(store, base, only) {
1890
1953
  if (!added.length && !removed.length && !modified.length)
1891
1954
  console.log(only ? `no working changes in ${only}` : "no working changes");
1892
1955
  }
1956
+ function unresolvedConflictPaths() {
1957
+ const out = [];
1958
+ for (const f of walkFiles()) {
1959
+ const abs = join6(cwd2, f);
1960
+ let buf;
1961
+ try {
1962
+ const st = lstatSync3(abs);
1963
+ if (st.isSymbolicLink() || st.isDirectory())
1964
+ continue;
1965
+ buf = readFileSync6(abs);
1966
+ } catch {
1967
+ continue;
1968
+ }
1969
+ if (buf.includes(0))
1970
+ continue;
1971
+ const text = buf.toString("utf8");
1972
+ if (/^<{7}( |$)/m.test(text) && /^>{7}( |$)/m.test(text))
1973
+ out.push(f);
1974
+ }
1975
+ return out;
1976
+ }
1893
1977
  function isWorkingPath(store, head, arg) {
1894
1978
  const rel = repoRel(arg);
1895
1979
  if (lexists(join6(cwd2, rel)))
@@ -2054,10 +2138,275 @@ async function writeBundle(solDir3, bundle, from = 0) {
2054
2138
  return fresh.length;
2055
2139
  }
2056
2140
 
2141
+ // src/bin/local-peer.ts
2142
+ import { existsSync as existsSync8 } from "node:fs";
2143
+ import { join as join8, resolve as resolve3 } from "node:path";
2144
+ function localPeerSolDir(arg, base) {
2145
+ if (/^https?:\/\//.test(arg))
2146
+ return;
2147
+ const p = resolve3(base, arg);
2148
+ if (existsSync8(join8(p, ".sol")))
2149
+ return join8(p, ".sol");
2150
+ if (existsSync8(join8(p, "objects")) && existsSync8(join8(p, "HEAD")))
2151
+ return p;
2152
+ return;
2153
+ }
2154
+ function openPeer(peerSolDir) {
2155
+ return { store: new FileStore2(peerSolDir), log: new FileOpLog2(peerSolDir) };
2156
+ }
2157
+ async function peerNodes(rep, head) {
2158
+ if (!head)
2159
+ return [];
2160
+ const out = [];
2161
+ const seen = new Set;
2162
+ const stack = [head];
2163
+ while (stack.length) {
2164
+ const h = stack.pop();
2165
+ if (seen.has(h))
2166
+ continue;
2167
+ seen.add(h);
2168
+ const node = await rep.store.get(h);
2169
+ if (!node)
2170
+ continue;
2171
+ out.push(node);
2172
+ if (node.kind === "tree")
2173
+ for (const e of Object.values(node.entries))
2174
+ stack.push(e.hash);
2175
+ }
2176
+ return out;
2177
+ }
2178
+
2179
+ // src/merge.ts
2180
+ function resolvePath2(base, ours, theirs) {
2181
+ const oursChanged = ours !== base;
2182
+ const theirsChanged = theirs !== base;
2183
+ if (!oursChanged && !theirsChanged) {
2184
+ return { content: base };
2185
+ }
2186
+ if (oursChanged && !theirsChanged) {
2187
+ return { content: ours };
2188
+ }
2189
+ if (!oursChanged && theirsChanged) {
2190
+ return { content: theirs };
2191
+ }
2192
+ if (ours === theirs) {
2193
+ return { content: ours };
2194
+ }
2195
+ if (base !== undefined && ours !== undefined && theirs !== undefined) {
2196
+ const m = merge3(base, ours, theirs);
2197
+ if (m.clean)
2198
+ return { content: m.text };
2199
+ return { content: m.text, conflict: { path: "", ours, theirs } };
2200
+ }
2201
+ return {
2202
+ content: ours,
2203
+ conflict: { path: "", ours, theirs }
2204
+ };
2205
+ }
2206
+ function merge2(repo, baseHead, oursHead, theirsHead) {
2207
+ const store = repo.store;
2208
+ const paths = new Set([
2209
+ ...listAll(store, baseHead),
2210
+ ...listAll(store, oursHead),
2211
+ ...listAll(store, theirsHead)
2212
+ ]);
2213
+ const conflicts = [];
2214
+ let root = emptyRoot(store);
2215
+ for (const path of [...paths].sort()) {
2216
+ const base = readFile(store, baseHead, path);
2217
+ const ours = readFile(store, oursHead, path);
2218
+ const theirs = readFile(store, theirsHead, path);
2219
+ const { content, conflict } = resolvePath2(base, ours, theirs);
2220
+ if (conflict)
2221
+ conflicts.push({ ...conflict, path });
2222
+ if (content !== undefined) {
2223
+ root = writeFile(store, root, path, content);
2224
+ }
2225
+ }
2226
+ return { head: root, conflicts };
2227
+ }
2228
+
2229
+ // src/converge.ts
2230
+ var EMPTY_TREE3 = { kind: "tree", entries: {} };
2231
+ function buildAncestry(ops) {
2232
+ const parents = new Map;
2233
+ const add = (child, parent) => {
2234
+ if (!child || !parent || parent === child)
2235
+ return;
2236
+ let s = parents.get(child);
2237
+ if (!s)
2238
+ parents.set(child, s = new Set);
2239
+ s.add(parent);
2240
+ };
2241
+ let prevRoot;
2242
+ for (const op of ops) {
2243
+ if (op.type === "checkpoint") {
2244
+ add(op.rootAfter, op.parent);
2245
+ add(op.rootAfter, op.parent2);
2246
+ } else {
2247
+ add(op.rootAfter, op.parent ?? prevRoot);
2248
+ }
2249
+ prevRoot = op.rootAfter;
2250
+ }
2251
+ return parents;
2252
+ }
2253
+ function ancestorsOf(head, parents) {
2254
+ const seen = new Set;
2255
+ const stack = [head];
2256
+ while (stack.length) {
2257
+ const h = stack.pop();
2258
+ if (seen.has(h))
2259
+ continue;
2260
+ seen.add(h);
2261
+ for (const p of parents.get(h) ?? [])
2262
+ stack.push(p);
2263
+ }
2264
+ return seen;
2265
+ }
2266
+ function mergeBase(a, b, parents) {
2267
+ if (a === b)
2268
+ return a;
2269
+ const bAnc = ancestorsOf(b, parents);
2270
+ const seen = new Set;
2271
+ let frontier = [a];
2272
+ while (frontier.length) {
2273
+ const next = [];
2274
+ for (const h of frontier) {
2275
+ if (seen.has(h))
2276
+ continue;
2277
+ seen.add(h);
2278
+ if (bAnc.has(h))
2279
+ return h;
2280
+ for (const p of parents.get(h) ?? [])
2281
+ next.push(p);
2282
+ }
2283
+ frontier = next;
2284
+ }
2285
+ return;
2286
+ }
2287
+ async function hydrate2(src, dst, root) {
2288
+ if (!root)
2289
+ return dst.put(EMPTY_TREE3);
2290
+ const stack = [root];
2291
+ const seen = new Set;
2292
+ while (stack.length) {
2293
+ const h = stack.pop();
2294
+ if (seen.has(h))
2295
+ continue;
2296
+ seen.add(h);
2297
+ const node = await src.get(h);
2298
+ if (!node)
2299
+ continue;
2300
+ dst.put(node);
2301
+ if (node.kind === "tree")
2302
+ for (const e of Object.values(node.entries))
2303
+ stack.push(e.hash);
2304
+ }
2305
+ return root;
2306
+ }
2307
+ async function persist(sync, dst, root) {
2308
+ const stack = [root];
2309
+ const seen = new Set;
2310
+ while (stack.length) {
2311
+ const h = stack.pop();
2312
+ if (seen.has(h))
2313
+ continue;
2314
+ seen.add(h);
2315
+ const node = sync.get(h);
2316
+ if (!node)
2317
+ continue;
2318
+ if (!await dst.has(h))
2319
+ await dst.put(node);
2320
+ if (node.kind === "tree")
2321
+ for (const e of Object.values(node.entries))
2322
+ stack.push(e.hash);
2323
+ }
2324
+ }
2325
+ async function converge(local, input) {
2326
+ const at = input.at ?? Date.now();
2327
+ const actor3 = input.actor ?? "system";
2328
+ for (const node of input.nodes)
2329
+ await local.store.put(node);
2330
+ const priorHead = await local.log.head();
2331
+ const incomingHead = input.incomingHead ?? input.ops[input.ops.length - 1]?.rootAfter;
2332
+ const sinceSeq = await local.log.seq();
2333
+ const existing = await local.log.history();
2334
+ const priorAncestry = buildAncestry(existing);
2335
+ const knownByEntry = new Map(existing.filter((o) => o.entryHash).map((o) => [o.entryHash, o]));
2336
+ const knownByTuple = new Map(existing.map((o) => [`${o.type}\x00${o.path}\x00${o.rootAfter}`, o]));
2337
+ const incoming = [...input.ops].sort((a, b) => a.seq - b.seq);
2338
+ let forkBase = input.base ?? priorHead;
2339
+ if (input.base === undefined) {
2340
+ for (const op of incoming) {
2341
+ const k = op.entryHash ? knownByEntry.get(op.entryHash) : knownByTuple.get(`${op.type}\x00${op.path}\x00${op.rootAfter}`);
2342
+ if (k)
2343
+ forkBase = k.rootAfter;
2344
+ }
2345
+ }
2346
+ let applied = 0;
2347
+ for (const op of incoming) {
2348
+ if (op.entryHash && knownByEntry.has(op.entryHash))
2349
+ continue;
2350
+ if (!op.entryHash && knownByTuple.has(`${op.type}\x00${op.path}\x00${op.rootAfter}`))
2351
+ continue;
2352
+ const stamped = applied === 0 && op.type !== "checkpoint" && op.parent === undefined ? { ...op, parent: forkBase } : { ...op };
2353
+ await local.log.append({ ...stamped, seq: await local.log.seq() + 1 });
2354
+ applied++;
2355
+ }
2356
+ let head = await local.log.head() ?? priorHead ?? await local.store.put(EMPTY_TREE3);
2357
+ let merged = false;
2358
+ let conflicts = [];
2359
+ if (priorHead && incomingHead && priorHead !== incomingHead) {
2360
+ const parents = buildAncestry(await local.log.history());
2361
+ for (const [c, ps] of priorAncestry) {
2362
+ let s = parents.get(c);
2363
+ if (!s)
2364
+ parents.set(c, s = new Set);
2365
+ for (const p of ps)
2366
+ s.add(p);
2367
+ }
2368
+ const priorIsAncestor = ancestorsOf(incomingHead, parents).has(priorHead);
2369
+ const incomingIsAncestor = ancestorsOf(priorHead, parents).has(incomingHead);
2370
+ if (incomingIsAncestor) {
2371
+ if (await local.log.head() !== priorHead)
2372
+ await appendHeadMove(local, priorHead, priorHead, incomingHead, actor3, at, "converge: keep ahead-of-incoming head");
2373
+ head = priorHead;
2374
+ } else if (!priorIsAncestor) {
2375
+ const base = input.base ?? mergeBase(priorHead, incomingHead, parents);
2376
+ const sync = new Store;
2377
+ const baseRoot = await hydrate2(local.store, sync, base);
2378
+ const oursRoot = await hydrate2(local.store, sync, priorHead);
2379
+ const theirsRoot = await hydrate2(local.store, sync, incomingHead);
2380
+ const result = merge2({ store: sync }, baseRoot, oursRoot, theirsRoot);
2381
+ conflicts = result.conflicts;
2382
+ await persist(sync, local.store, result.head);
2383
+ await local.log.append({
2384
+ seq: await local.log.seq() + 1,
2385
+ type: "checkpoint",
2386
+ path: "",
2387
+ rootAfter: result.head,
2388
+ at,
2389
+ by: actor3,
2390
+ message: conflicts.length ? `converge merge (${conflicts.length} conflict(s) kept with markers)` : "converge merge",
2391
+ parent: priorHead,
2392
+ parent2: incomingHead
2393
+ });
2394
+ head = result.head;
2395
+ merged = true;
2396
+ }
2397
+ }
2398
+ const finalOps = await local.log.history();
2399
+ const missingOps = finalOps.filter((o) => o.seq > sinceSeq);
2400
+ return { head, applied, merged, conflicts, missingOps };
2401
+ }
2402
+ async function appendHeadMove(local, head, parent, parent2, by, at, message) {
2403
+ await local.log.append({ seq: await local.log.seq() + 1, type: "checkpoint", path: "", rootAfter: head, at, by, message, parent, parent2 });
2404
+ }
2405
+
2057
2406
  // src/bin/runtime.ts
2058
- 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";
2407
+ 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";
2059
2408
  import { platform } from "node:os";
2060
- import { dirname as dirname4, join as join8, relative as relative4 } from "node:path";
2409
+ import { dirname as dirname4, join as join9, relative as relative4 } from "node:path";
2061
2410
  var SYMLINK_MODE3 = 40960;
2062
2411
  var EXEC_MODE3 = 493;
2063
2412
  function isolateCommand(command, scratch) {
@@ -2073,7 +2422,7 @@ function isolateCommand(command, scratch) {
2073
2422
  }
2074
2423
  throw new Error(`--isolate not yet wired for platform ${platform()} (macOS sandbox-exec only); omit --isolate to run unconfined`);
2075
2424
  }
2076
- function hydrate2(store, head, dir) {
2425
+ function hydrate3(store, head, dir) {
2077
2426
  if (!head)
2078
2427
  return 0;
2079
2428
  let n = 0;
@@ -2081,7 +2430,7 @@ function hydrate2(store, head, dir) {
2081
2430
  const blob = fileAt(store, head, p);
2082
2431
  if (!blob)
2083
2432
  continue;
2084
- const abs = join8(dir, p);
2433
+ const abs = join9(dir, p);
2085
2434
  mkdirSync6(dirname4(abs), { recursive: true });
2086
2435
  const mode = modeAt(store, head, p);
2087
2436
  if (mode === SYMLINK_MODE3) {
@@ -2103,7 +2452,7 @@ function hydrate2(store, head, dir) {
2103
2452
  }
2104
2453
  function walkDir(dir, base, pats, out = []) {
2105
2454
  for (const name of readdirSync6(dir)) {
2106
- const p = join8(dir, name);
2455
+ const p = join9(dir, name);
2107
2456
  const rel = relative4(base, p);
2108
2457
  if (isIgnored(rel, pats))
2109
2458
  continue;
@@ -2121,11 +2470,11 @@ async function capture(repo, dir, keep = []) {
2121
2470
  const pats = ignorePatterns();
2122
2471
  const files = new Set(walkDir(dir, dir, pats));
2123
2472
  for (const k of keep)
2124
- if (existsSync8(join8(dir, k)))
2473
+ if (existsSync9(join9(dir, k)))
2125
2474
  files.add(k);
2126
2475
  const written = [];
2127
2476
  for (const f of files) {
2128
- const abs = join8(dir, f);
2477
+ const abs = join9(dir, f);
2129
2478
  const st = lstatSync4(abs);
2130
2479
  if (st.isSymbolicLink()) {
2131
2480
  const target = readlinkSync4(abs);
@@ -2153,7 +2502,7 @@ async function capture(repo, dir, keep = []) {
2153
2502
  }
2154
2503
  const deleted = [];
2155
2504
  for (const t of await repo.list()) {
2156
- if (!existsSync8(join8(dir, t))) {
2505
+ if (!existsSync9(join9(dir, t))) {
2157
2506
  await repo.deleteFile(t);
2158
2507
  deleted.push(t);
2159
2508
  }
@@ -2162,7 +2511,7 @@ async function capture(repo, dir, keep = []) {
2162
2511
  }
2163
2512
 
2164
2513
  // src/bin/sol.ts
2165
- var CRED_PATH = join9(homedir(), ".sol", "credentials");
2514
+ var CRED_PATH = join10(homedir(), ".sol", "credentials");
2166
2515
  function tokenClaims(token) {
2167
2516
  try {
2168
2517
  return JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString());
@@ -2171,7 +2520,7 @@ function tokenClaims(token) {
2171
2520
  }
2172
2521
  }
2173
2522
  async function loadStoredToken() {
2174
- if (!existsSync9(CRED_PATH))
2523
+ if (!existsSync10(CRED_PATH))
2175
2524
  return;
2176
2525
  let creds;
2177
2526
  try {
@@ -2198,6 +2547,15 @@ async function loadStoredToken() {
2198
2547
  }
2199
2548
  return creds.accessToken;
2200
2549
  }
2550
+ function authExpired() {
2551
+ return die("session expired \u2014 run `sol auth login` (or set SOL_TOKEN)");
2552
+ }
2553
+ function identityFromToken(token) {
2554
+ const c = tokenClaims(token);
2555
+ if (!c.handle && !c.email && !c.userId && !c.sub)
2556
+ return;
2557
+ return { handle: c.handle, email: c.email, userId: c.userId ?? c.sub };
2558
+ }
2201
2559
  function authHost() {
2202
2560
  if (process.env.SOL_AUTH)
2203
2561
  return process.env.SOL_AUTH.replace(/\/+$/, "");
@@ -2216,21 +2574,98 @@ function resolveRemote(solDir3) {
2216
2574
  return;
2217
2575
  return cfg.url ? cfg : { ...cfg, url: DEFAULT_REMOTE_URL };
2218
2576
  }
2577
+ function cliVersion() {
2578
+ try {
2579
+ return JSON.parse(readFileSync9(new URL("./package.json", import.meta.url), "utf8")).version || "dev";
2580
+ } catch {
2581
+ return "dev";
2582
+ }
2583
+ }
2219
2584
  async function main() {
2220
2585
  const argv = process.argv.slice(2);
2221
2586
  if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
2222
- console.log("sol 0.1.0");
2587
+ console.log(`sol ${cliVersion()}`);
2223
2588
  return;
2224
2589
  }
2225
2590
  const args = argv.slice(1);
2226
2591
  const wantsHelp = args.includes("--help") || args.includes("-h");
2227
2592
  const SUBHELP = {
2593
+ commit: `sol commit "<msg>" commits the whole working tree; works as-is for a solo author.
2594
+ sol commit -m "<msg>" <file>... commit ONLY those paths (correct attribution for concurrent agents)
2595
+
2596
+ (once OTHER authors have committed to this repo, a whole-tree commit is refused to avoid mis-attribution
2597
+ \u2014 scope to files \`sol commit -m "<msg>" <files>\` or force with --whole-tree).
2598
+
2599
+ flags:
2600
+ -m <message> message when scoping to files
2601
+ --whole-tree force a whole-tree commit even after OTHER authors are present (the guard above)
2602
+ --force commit even with unresolved <<<<<<< conflict markers
2603
+
2604
+ examples:
2605
+ sol commit "add login route"
2606
+ sol commit -m "fix parser" src/parse.ts src/lex.ts`,
2607
+ push: `sol push push the current branch to the configured remote (converging \u2014 never FF-rejected)
2608
+ sol push <repo> if no remote is set, use the hosted Sol (${DEFAULT_REMOTE_URL}) + this repo name, then push
2609
+ sol push --create <repo> same, explicit form (creates the repo on first push)
2610
+ sol push --public <repo> create + push + make it public in ONE step (else new repos are private)
2611
+
2612
+ flags:
2613
+ --create explicit "creates the repo on first push" (the bare \`<repo>\` form does this too)
2614
+ --public after a successful create/push, set public access (same op as \`sol access public\`)
2615
+
2616
+ notes:
2617
+ a remote already configured? the <repo>/--create arg is ignored \u2014 \`sol push\` just syncs.
2618
+ new repos are PRIVATE by default; \`--public\` is the one-step opt-in to share (or \`sol access public\` later).
2619
+ set SOL_TOKEN (or \`sol auth login\`) first; the push registers the current branch's head on the remote.
2620
+
2621
+ examples:
2622
+ sol push # remote already configured
2623
+ sol push alice/app # one step: configure hosted remote + push (private)
2624
+ sol push --create --public alice/app # create + push + share, all at once`,
2625
+ pull: `sol pull fetch + converge the current branch from the configured remote
2626
+ sol pull <dir> OFFLINE: converge another local repo's .sol on disk into this one (multi-agent, no network)
2627
+
2628
+ notes:
2629
+ refuses to run over a dirty tree \u2014 commit or discard first. conflicts land in the working tree with markers.
2630
+ refuses to run while UNRESOLVED <<<<<<< markers remain \u2014 resolve them and \`sol commit\`, then pull again.`,
2631
+ seal: `sol seal <path> [recipient...] encrypt a file so the HOST only ever sees ciphertext (per-path privacy)
2632
+
2633
+ the content becomes host-blind ciphertext committed into history; the keys stay LOCAL (~/.sol keystore).
2634
+ you are always a recipient of your own seals. add other actors to share read access. \`sol cat\` decrypts
2635
+ for a recipient; everyone else sees <<sealed>>.
2636
+
2637
+ example:
2638
+ sol seal secrets/.env alice bob # alice, bob (and you) can read it; the server cannot`,
2639
+ remote: `sol remote show the configured remote
2640
+ sol remote <url> <repo> set the remote (url + repo name)
2641
+
2642
+ tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
2643
+ branch: `sol branch list branches (* = current)
2644
+ sol branch <name> [<ref>] create a branch at <ref> (default: HEAD)`,
2645
+ switch: `sol switch <branch> switch branches (commits/keeps current work first; never loses it)`,
2646
+ merge: `sol merge <branch> 3-way merge <branch> into the current branch
2647
+ conflicts land in the working tree with <<<<<<< markers \u2014 resolve, then \`sol commit\`.`,
2648
+ log: `sol log commit history for the current branch
2649
+ sol log <branch|commit> history scoped to a ref
2650
+ sol log --all every op in the log (not just commits)`,
2651
+ diff: `sol diff working tree vs HEAD
2652
+ sol diff <ref> working tree vs a branch/commit
2653
+ sol diff <ref> <ref> tree vs tree
2654
+ sol diff <path> one file, working tree vs HEAD`,
2655
+ restore: `sol restore [--from <ref>] [<path>...] restore a file (or the whole tree) from a ref (default HEAD)`,
2656
+ run: `sol run [--keep <path>]... [--isolate] <command...>
2657
+ hydrate the current tree to a sandbox, run the command, capture produced files back into a commit.
2658
+ --isolate confines the run (no network, writes stay in the sandbox).`,
2659
+ promote: "sol promote [branch] point the remote's production branch at <branch> (default: current branch)",
2660
+ git: `sol git import <repo> [dir] import a git repo's HEAD into a new Sol repo
2661
+ sol git export <repo> [-m "msg"] write Sol's tree back as a git commit, then \`git push\``,
2228
2662
  mr: `sol mr open [--from <branch>] [--to <branch>] [--upstream <repo>] -t "title" [-m body]
2229
2663
  sol mr list | show <id> | review <id> --approve|--request-changes|--comment [-m msg]
2230
2664
  sol mr comment <id> -m msg [--path f --line N] | check <id> --run -- <cmd> | merge <id> [--force] | close <id>`,
2231
2665
  fork: "sol fork [<url>] <parent-repo> <new-repo> [dir] \u2014 your own copy (all branches + history + a parent link)",
2232
2666
  forks: "sol forks \u2014 list the forks of the current repo",
2233
- access: "sol access [show | public | private | add <userId> <read|write|admin> | remove <userId>]",
2667
+ access: "sol access [show | public | private | add <userId> <read|write|admin> | remove <userId>]\n tip: to share a NEW repo in one step, `sol push --public <repo>` (create + push + public) instead of pushing then `sol access public`.",
2668
+ share: "sol share \u2014 there is no separate share command: a new repo goes public in one step with `sol push --public <repo>`,\n or flip an existing one with `sol access public`. (new repos are private by default.)",
2234
2669
  auth: "sol auth [login [<web-url>] | logout | status | whoami | set-handle <name> | pat [days]]",
2235
2670
  clone: "sol clone [<url>] <owner>/<repo> [dir] \u2014 default dir = <repo> (url defaults to the hosted Sol)"
2236
2671
  };
@@ -2244,12 +2679,12 @@ async function main() {
2244
2679
  if (t)
2245
2680
  process.env.SOL_TOKEN = t;
2246
2681
  }
2247
- const release = new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "pull", "run", "seal"]).has(cmd) && existsSync9(solDir2) ? acquireLock() : undefined;
2682
+ const release = new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "pull", "push", "restore", "checkout", "run", "seal"]).has(cmd) && existsSync10(solDir2) ? acquireLock() : undefined;
2248
2683
  try {
2249
2684
  switch (cmd) {
2250
2685
  case "init": {
2251
- const here = join9(procCwd2, ".sol");
2252
- if (existsSync9(here))
2686
+ const here = join10(procCwd2, ".sol");
2687
+ if (existsSync10(here))
2253
2688
  die("already a sol repo: " + procCwd2);
2254
2689
  if (repoRoot2 && repoRoot2 !== procCwd2 && !args.includes("--force")) {
2255
2690
  die(`already inside a Sol repo at ${repoRoot2}
@@ -2263,7 +2698,7 @@ async function main() {
2263
2698
  }
2264
2699
  case "track":
2265
2700
  case "add": {
2266
- if (!existsSync9(solDir2))
2701
+ if (!existsSync10(solDir2))
2267
2702
  die("not a sol repo \u2014 run `sol init` first");
2268
2703
  const files = args.filter((a) => a !== "." && !a.startsWith("-"));
2269
2704
  if (!files.length) {
@@ -2274,7 +2709,7 @@ async function main() {
2274
2709
  let n = 0;
2275
2710
  for (const f of files) {
2276
2711
  const rf = repoRel(f);
2277
- if (!existsSync9(join9(cwd2, rf))) {
2712
+ if (!existsSync10(join10(cwd2, rf))) {
2278
2713
  console.error("skip (not on disk): " + f);
2279
2714
  continue;
2280
2715
  }
@@ -2303,14 +2738,14 @@ async function main() {
2303
2738
  if (!message)
2304
2739
  die('commit needs a message: sol commit "what you did" (scoped: sol commit -m "msg" file1 file2)');
2305
2740
  const parentHead = await repo.head();
2306
- const mergeHeadPath = join9(solDir2, "MERGE_HEAD");
2307
- const parent2 = existsSync9(mergeHeadPath) ? readFileSync9(mergeHeadPath, "utf8").trim() || undefined : undefined;
2741
+ const mergeHeadPath = join10(solDir2, "MERGE_HEAD");
2742
+ const parent2 = existsSync10(mergeHeadPath) ? readFileSync9(mergeHeadPath, "utf8").trim() || undefined : undefined;
2308
2743
  let changed = 0;
2309
2744
  let commitRoot = parentHead;
2310
2745
  if (paths.length) {
2311
2746
  for (const p of paths) {
2312
2747
  const rp = repoRel(p);
2313
- if (existsSync9(join9(cwd2, rp))) {
2748
+ if (existsSync10(join10(cwd2, rp))) {
2314
2749
  if (await snapshotFile(repo, rp))
2315
2750
  changed++;
2316
2751
  } else if ((await repo.list()).includes(rp)) {
@@ -2323,9 +2758,12 @@ async function main() {
2323
2758
  commitRoot = await repo.head();
2324
2759
  } else {
2325
2760
  const optedIn = wholeTreeOptIn || process.env.SOL_ALLOW_WHOLE_TREE === "1";
2326
- if (process.env.SOL_ACTOR && !optedIn) {
2327
- die(`refusing a whole-tree commit as "${actor2}" \u2014 it would attribute ALL pending files to you.
2328
- concurrent agents: sol commit -m "${message}" <your files> (or a per-agent SolWorkspace/MCP)
2761
+ const otherActors = [...new Set((await log.history()).map((o) => o.by).filter((b) => !!b && b !== actor2))];
2762
+ const otherActorPresent = otherActors.length > 0;
2763
+ if (process.env.SOL_ACTOR && !optedIn && otherActorPresent) {
2764
+ const others = otherActors.join(", ");
2765
+ die(`refusing a whole-tree commit as "${actor2}" \u2014 other author(s) have committed here (${others}), so sweeping the whole tree would mis-attribute their pending files to you. (this guard only fires because they're present; a solo author commits the whole tree freely.)
2766
+ scope to your files: sol commit -m "${message}" <your files> (or a per-agent SolWorkspace/MCP)
2329
2767
  if you truly own the whole tree: add --whole-tree or set SOL_ALLOW_WHOLE_TREE=1`);
2330
2768
  }
2331
2769
  const snap = await snapshotTree(repo);
@@ -2360,10 +2798,13 @@ async function main() {
2360
2798
  }
2361
2799
  case "status": {
2362
2800
  const { repo, log } = open();
2363
- const refs = existsSync9(refsPath()) ? await loadRefs(log) : undefined;
2801
+ const refs = existsSync10(refsPath()) ? await loadRefs(log) : undefined;
2364
2802
  const head = await repo.head();
2803
+ const headOp = head ? [...await log.history()].reverse().find((o) => o.rootAfter === head) : undefined;
2804
+ const headBy = headOp?.by ?? "?";
2365
2805
  console.log(`repo ${cwd2}`);
2366
- console.log(`on ${refs ? refs.current : "main"} head ${head.slice(0, 14)} seq ${await log.seq()} actor ${actor2}`);
2806
+ console.log(`on ${refs ? refs.current : "main"} head ${head ? `${head.slice(0, 14)} by ${headBy}` : "(empty)"} seq ${await log.seq()}`);
2807
+ console.log(`you: ${actor2}`);
2367
2808
  const ch = workingChanges(loadStore(), head);
2368
2809
  const dirty = ch.added.length + ch.modified.length + ch.removed.length;
2369
2810
  if (!dirty) {
@@ -2416,7 +2857,7 @@ async function main() {
2416
2857
  byRoot.set(o.rootAfter, o);
2417
2858
  const live = await log.head() ?? "";
2418
2859
  const refArg = args.find((a) => !a.startsWith("-"));
2419
- const lrefs = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : null;
2860
+ const lrefs = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : null;
2420
2861
  let tip = live;
2421
2862
  if (refArg) {
2422
2863
  tip = lrefs?.branches[refArg]?.head ?? refArg;
@@ -2427,19 +2868,27 @@ async function main() {
2427
2868
  }
2428
2869
  const chain = [];
2429
2870
  const seen = new Set;
2430
- let cur = tip;
2431
- while (cur && byRoot.has(cur) && !seen.has(cur)) {
2871
+ const stack = [tip];
2872
+ while (stack.length) {
2873
+ const cur = stack.pop();
2874
+ if (!cur || seen.has(cur) || !byRoot.has(cur))
2875
+ continue;
2432
2876
  seen.add(cur);
2433
2877
  const c = byRoot.get(cur);
2434
2878
  chain.push(c);
2435
- cur = c.parent;
2879
+ if (c.parent)
2880
+ stack.push(c.parent);
2881
+ if (c.parent2)
2882
+ stack.push(c.parent2);
2436
2883
  }
2884
+ chain.sort((a, b) => b.seq - a.seq);
2437
2885
  if (!chain.length) {
2438
2886
  console.log(ops.length ? '(no commits on this branch yet \u2014 run `sol commit "msg"`)' : "(no history yet)");
2439
2887
  }
2440
2888
  for (const c of chain) {
2441
2889
  const n = countChanges(store, c.parent ?? "", c.rootAfter);
2442
- const tag = c.parent2 ? ` [merge ${c.parent2.slice(0, 8)}]` : "";
2890
+ const mergedIn = [c.parent2 && byRoot.get(c.parent2)?.by].filter(Boolean);
2891
+ const tag = c.parent2 ? ` [merge ${c.parent2.slice(0, 8)}${mergedIn.length ? ` <- ${mergedIn.join(", ")}` : ""}]` : "";
2443
2892
  console.log(`${c.rootAfter.slice(0, 14)} ${(c.by ?? "?").padEnd(14)} ${c.message ?? ""} (${n} change${n === 1 ? "" : "s"})${tag}`);
2444
2893
  }
2445
2894
  const wc = workingChanges(store, live);
@@ -2452,7 +2901,7 @@ async function main() {
2452
2901
  const path = args[0] || die("rm needs a path");
2453
2902
  let onDisk = false;
2454
2903
  try {
2455
- unlinkSync6(join9(cwd2, path));
2904
+ unlinkSync6(join10(cwd2, path));
2456
2905
  onDisk = true;
2457
2906
  } catch {}
2458
2907
  if (onDisk) {
@@ -2619,11 +3068,11 @@ async function main() {
2619
3068
  };
2620
3069
  for (const op of ops)
2621
3070
  walk(op.rootAfter);
2622
- const objDir = join9(solDir2, "objects");
3071
+ const objDir = join10(solDir2, "objects");
2623
3072
  let removed = 0;
2624
3073
  for (const name of readdirSync7(objDir)) {
2625
3074
  if (name.endsWith(".tmp") || !reachable.has(name)) {
2626
- unlinkSync6(join9(objDir, name));
3075
+ unlinkSync6(join10(objDir, name));
2627
3076
  removed++;
2628
3077
  }
2629
3078
  }
@@ -2637,8 +3086,8 @@ async function main() {
2637
3086
  console.log(p);
2638
3087
  break;
2639
3088
  }
2640
- const f = join9(cwd2, ".solignore");
2641
- const lead = existsSync9(f) && !readFileSync9(f, "utf8").endsWith(`
3089
+ const f = join10(cwd2, ".solignore");
3090
+ const lead = existsSync10(f) && !readFileSync9(f, "utf8").endsWith(`
2642
3091
  `) ? `
2643
3092
  ` : "";
2644
3093
  appendFileSync4(f, lead + pat + `
@@ -2656,8 +3105,8 @@ async function main() {
2656
3105
  if (content === SEALED2)
2657
3106
  die("already sealed: " + path);
2658
3107
  if (content === undefined) {
2659
- const abs = join9(cwd2, path);
2660
- if (!existsSync9(abs))
3108
+ const abs = join10(cwd2, path);
3109
+ if (!existsSync10(abs))
2661
3110
  die("no such file: " + path);
2662
3111
  content = readFileSync9(abs, "utf8");
2663
3112
  }
@@ -2723,7 +3172,7 @@ async function main() {
2723
3172
  refs.current = name;
2724
3173
  saveRefs(refs);
2725
3174
  const n = materializeDiff(loadStore(), fromHead, target.head);
2726
- console.log(`switched to ${name} (${(target.head || "empty").slice(0, 12)}); working tree = ${n} file(s)`);
3175
+ console.log(`switched to ${name} (${(target.head || "empty").slice(0, 12)}); ${n} file(s) changed`);
2727
3176
  break;
2728
3177
  }
2729
3178
  case "merge": {
@@ -2744,7 +3193,7 @@ async function main() {
2744
3193
  const result = merge({ store }, other.base, ours, other.head);
2745
3194
  if (result.conflicts.length) {
2746
3195
  materializeTree(store, result.head);
2747
- writeFileSync9(join9(solDir2, "MERGE_HEAD"), other.head);
3196
+ writeFileSync9(join10(solDir2, "MERGE_HEAD"), other.head);
2748
3197
  console.log(`merge ${name} -> ${result.conflicts.length} conflict(s), left in your working tree (uncommitted):`);
2749
3198
  for (const c of result.conflicts)
2750
3199
  console.log(" " + c.path);
@@ -2763,7 +3212,7 @@ async function main() {
2763
3212
  break;
2764
3213
  }
2765
3214
  case "remote": {
2766
- if (!existsSync9(solDir2))
3215
+ if (!existsSync10(solDir2))
2767
3216
  die("not a sol repo");
2768
3217
  if (args[0]) {
2769
3218
  const repoName = args[1] || die("usage: sol remote <url> <repo>");
@@ -2817,7 +3266,7 @@ async function main() {
2817
3266
  if (!c.handle)
2818
3267
  console.log(" (no handle yet \u2014 set one in the web app to get your <handle>/<repo> namespace)");
2819
3268
  } else if (sub === "logout") {
2820
- if (existsSync9(CRED_PATH))
3269
+ if (existsSync10(CRED_PATH))
2821
3270
  unlinkSync6(CRED_PATH);
2822
3271
  console.log("logged out");
2823
3272
  } else if (sub === "status" || !sub) {
@@ -2826,7 +3275,7 @@ async function main() {
2826
3275
  console.log(`authenticated via SOL_TOKEN (env)${c2.handle ? ` as @${c2.handle}` : ""}`);
2827
3276
  break;
2828
3277
  }
2829
- if (!existsSync9(CRED_PATH)) {
3278
+ if (!existsSync10(CRED_PATH)) {
2830
3279
  console.log("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
2831
3280
  break;
2832
3281
  }
@@ -2837,20 +3286,26 @@ async function main() {
2837
3286
  } else if (sub === "whoami") {
2838
3287
  const token = process.env.SOL_TOKEN || await loadStoredToken();
2839
3288
  if (!token)
2840
- die("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
2841
- const res = await fetch(`${authHost()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
2842
- if (res.status === 401)
2843
- die("not logged in \u2014 your session expired; run `sol auth login` again");
2844
- if (!res.ok)
2845
- die(`could not reach ${authHost()} (me -> ${res.status})`);
2846
- const me = await res.json();
2847
- 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>\`)`);
3289
+ authExpired();
3290
+ let id = identityFromToken(token);
3291
+ try {
3292
+ const res = await fetch(`${authHost()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
3293
+ if (res.status === 401 && !id)
3294
+ authExpired();
3295
+ if (res.ok) {
3296
+ const me = await res.json();
3297
+ id = { handle: me.handle ?? id?.handle, email: me.email ?? id?.email, userId: id?.userId };
3298
+ }
3299
+ } catch {}
3300
+ if (!id)
3301
+ authExpired();
3302
+ 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>\`)`);
2848
3303
  } else if (sub === "set-handle") {
2849
3304
  const want = args[1] || die("usage: sol auth set-handle <name>");
2850
3305
  const handle = want.toLowerCase();
2851
3306
  const token = process.env.SOL_TOKEN || await loadStoredToken();
2852
3307
  if (!token)
2853
- die("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
3308
+ authExpired();
2854
3309
  let hadHandle = false;
2855
3310
  try {
2856
3311
  const meRes = await fetch(`${authHost()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
@@ -2859,7 +3314,7 @@ async function main() {
2859
3314
  } catch {}
2860
3315
  const res = await fetch(`${authHost()}/api/auth/handle`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ handle }) });
2861
3316
  if (res.status === 401)
2862
- die("not logged in \u2014 your session expired; run `sol auth login` again");
3317
+ authExpired();
2863
3318
  if (!res.ok) {
2864
3319
  let msg = `could not set handle (${res.status})`;
2865
3320
  try {
@@ -2874,7 +3329,7 @@ async function main() {
2874
3329
  console.log("heads-up: changing your handle re-namespaces your repos under the new <handle>/<repo>.");
2875
3330
  console.log(`handle set to @${out.handle}`);
2876
3331
  } else if (sub === "pat") {
2877
- if (!existsSync9(CRED_PATH))
3332
+ if (!existsSync10(CRED_PATH))
2878
3333
  die("run `sol auth login` first");
2879
3334
  const creds = JSON.parse(readFileSync9(CRED_PATH, "utf8"));
2880
3335
  const token = await loadStoredToken();
@@ -2928,12 +3383,33 @@ async function main() {
2928
3383
  break;
2929
3384
  }
2930
3385
  case "clone": {
3386
+ const localSrc = args[0] ? localPeerSolDir(args[0], procCwd2) : undefined;
3387
+ if (localSrc) {
3388
+ const peer = openPeer(localSrc);
3389
+ const peerHead = await peer.log.head();
3390
+ const dest = resolve4(procCwd2, args[1] || (args[0].replace(/\/+$/, "").split("/").pop() || "clone") + "-clone");
3391
+ const ddir = join10(dest, ".sol");
3392
+ if (existsSync10(ddir))
3393
+ die("already a sol repo: " + dest);
3394
+ mkdirSync7(ddir, { recursive: true });
3395
+ 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 });
3396
+ const dstore = new Store2;
3397
+ for (const n2 of await peerNodes({ store: new FileStore(ddir), log: new FileOpLog(ddir) }, res.head))
3398
+ dstore.put(n2);
3399
+ const files = res.head ? listAll3(dstore, res.head) : [];
3400
+ for (const f of files)
3401
+ materializeInto(dstore, res.head, dest, f);
3402
+ writeFileSync9(join10(ddir, "refs.json"), JSON.stringify({ current: "main", branches: { main: { head: res.head, base: res.head, remote: res.head } }, tags: {} }, null, 2));
3403
+ writeWorkingIndexAt(ddir, dest, files);
3404
+ console.log(`cloned local peer ${args[0]} -> ${dest} (${(await peer.log.history()).length} ops, ${files.length} files)`);
3405
+ break;
3406
+ }
2931
3407
  const [url, rest] = remoteUrlArg(args);
2932
3408
  const repoName = rest[0] || die("usage: sol clone [<url>] <owner>/<repo> [dir]");
2933
3409
  const token = process.env.SOL_TOKEN || die("set SOL_TOKEN to the backend bearer token");
2934
- const target = resolve3(cwd2, rest[1] || repoName.split("/").pop() || repoName);
2935
- const fdir = join9(target, ".sol");
2936
- if (existsSync9(fdir))
3410
+ const target = resolve4(cwd2, rest[1] || repoName.split("/").pop() || repoName);
3411
+ const fdir = join10(target, ".sol");
3412
+ if (existsSync10(fdir))
2937
3413
  die("already a sol repo: " + target);
2938
3414
  const cfg = { url, repo: repoName };
2939
3415
  const bundle = await remoteExport(cfg, token);
@@ -2949,8 +3425,8 @@ async function main() {
2949
3425
  cloneBranches[name] = { head: h, base: h, remote: h };
2950
3426
  if (!cloneBranches[onBranch])
2951
3427
  cloneBranches[onBranch] = { head: checkoutHead, base: checkoutHead, remote: checkoutHead };
2952
- writeFileSync9(join9(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
2953
- writeFileSync9(join9(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
3428
+ writeFileSync9(join10(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
3429
+ writeFileSync9(join10(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
2954
3430
  const store = new Store2;
2955
3431
  for (const node of bundle.nodes)
2956
3432
  store.put(node);
@@ -2967,13 +3443,21 @@ async function main() {
2967
3443
  }
2968
3444
  case "push": {
2969
3445
  const { log } = open();
2970
- const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
2971
- const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3446
+ const wantPublic = args.includes("--public");
3447
+ if (!loadRemote(solDir2)) {
3448
+ const repoName = args.find((a) => !a.startsWith("-"));
3449
+ if (repoName) {
3450
+ saveRemote(solDir2, { url: DEFAULT_REMOTE_URL, repo: repoName });
3451
+ console.log(`remote set: ${DEFAULT_REMOTE_URL} (repo ${repoName})`);
3452
+ }
3453
+ }
3454
+ const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`, or `sol push <repo>` to use the hosted Sol");
3455
+ const token = process.env.SOL_TOKEN || authExpired();
2972
3456
  const rh = await remoteHead(cfg, token);
2973
3457
  const ops = await log.history();
2974
3458
  const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
2975
3459
  const localTip = await log.logTip();
2976
- const localRefs = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : undefined;
3460
+ const localRefs = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : undefined;
2977
3461
  const branch = localRefs?.current ?? "main";
2978
3462
  const branchHead = await log.head() ?? "";
2979
3463
  const remoteWasEmpty = rh.seq === 0 && !rh.tip;
@@ -2981,26 +3465,22 @@ async function main() {
2981
3465
  console.log(remoteWasEmpty ? "nothing to push \u2014 local repo is empty (commit something first)" : "everything up to date");
2982
3466
  break;
2983
3467
  }
2984
- if (rh.seq > 0) {
2985
- const atRemote = ops.find((o) => o.seq === rh.seq);
2986
- if (!atRemote || atRemote.entryHash !== rh.tip)
2987
- die("push rejected \u2014 the remote has changes you don't have (run `sol pull` first)");
2988
- }
2989
- if (localSeq <= rh.seq) {
2990
- console.log("nothing to push");
2991
- break;
2992
- }
2993
- let res;
2994
- try {
2995
- res = await remotePush(cfg, token, { nodes: allLocalNodes(), ops: ops.filter((o) => o.seq > rh.seq), branch, head: branchHead, expectedHead: localRefs?.branches[branch]?.remote });
2996
- } catch (e) {
2997
- if (String(e?.message ?? "").includes("409"))
2998
- die(`push rejected \u2014 branch '${branch}' moved on the remote. run \`sol pull\`, then push again.`);
2999
- throw e;
3000
- }
3468
+ if (localSeq <= rh.seq) {}
3469
+ const forkBase = localRefs?.branches[branch]?.remote;
3470
+ const baseOp = forkBase ? ops.find((o) => o.rootAfter === forkBase) : undefined;
3471
+ const fromSeq = baseOp ? baseOp.seq : rh.seq;
3472
+ const res = await remotePush(cfg, token, {
3473
+ nodes: allLocalNodes(),
3474
+ ops: ops.filter((o) => o.seq > fromSeq),
3475
+ branch,
3476
+ head: branchHead,
3477
+ expectedHead: forkBase
3478
+ });
3001
3479
  const canon = await remoteExport(cfg, token);
3480
+ const prevHead = await log.head() ?? "";
3002
3481
  await writeBundle(solDir2, canon, 0);
3003
- if (existsSync9(refsPath())) {
3482
+ const convergedHead = res.head ?? canon.refs?.branches?.[branch] ?? branchHead;
3483
+ if (existsSync10(refsPath())) {
3004
3484
  const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
3005
3485
  if (canon.refs) {
3006
3486
  for (const [name, h] of Object.entries(canon.refs.branches)) {
@@ -3008,32 +3488,80 @@ async function main() {
3008
3488
  }
3009
3489
  }
3010
3490
  if (refs.branches[branch]) {
3011
- refs.branches[branch].head = res.head ?? branchHead;
3012
- refs.branches[branch].remote = res.head ?? branchHead;
3491
+ refs.branches[branch].head = convergedHead;
3492
+ refs.branches[branch].remote = convergedHead;
3013
3493
  }
3014
3494
  saveRefs(refs);
3015
3495
  }
3016
- setOpLogHead(res.head ?? branchHead);
3496
+ setOpLogHead(convergedHead);
3497
+ if (convergedHead && convergedHead !== prevHead)
3498
+ materializeDiff(loadStore(), prevHead, convergedHead);
3017
3499
  const prod = res.refs?.production;
3500
+ const mergedNote = res.merged ? " (converged with a concurrent commit)" : "";
3501
+ const conflictNote = res.conflicts && res.conflicts.length ? ` \u2014 ${res.conflicts.length} conflict(s) kept with markers in: ${res.conflicts.map((c) => c.path).join(", ")}` : "";
3018
3502
  if (remoteWasEmpty) {
3019
- console.log(`created remote repo ${cfg.repo} \u2014 pushed ${res.applied} op(s) (branch ${branch} @ ${(res.head ?? "").slice(0, 12)})${prod ? `; production=${prod}` : ""}`);
3503
+ console.log(`created remote repo ${cfg.repo} \u2014 pushed ${res.applied} op(s) (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${prod ? `; production=${prod}` : ""}`);
3020
3504
  } else {
3021
- console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(res.head ?? "").slice(0, 12)})${prod ? `; production=${prod}` : ""}`);
3505
+ console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${mergedNote}${conflictNote}${prod ? `; production=${prod}` : ""}`);
3506
+ }
3507
+ if (wantPublic) {
3508
+ const a = await accessSet(cfg, token, { visibility: "public" });
3509
+ console.log(`${cfg.repo} is now ${a.visibility}`);
3022
3510
  }
3023
3511
  break;
3024
3512
  }
3025
3513
  case "pull": {
3026
3514
  const { log } = open();
3515
+ {
3516
+ const marked = unresolvedConflictPaths();
3517
+ if (marked.length)
3518
+ die(`unresolved conflict markers in ${marked.join(", ")} \u2014 resolve them and \`sol commit\`, then pull again.`);
3519
+ }
3520
+ const peerArg = args[0];
3521
+ const peerSolDir = peerArg ? localPeerSolDir(peerArg, cwd2) : undefined;
3522
+ if (peerSolDir) {
3523
+ const wcLocal = workingChanges(loadStore(), await log.head() ?? "");
3524
+ if (wcLocal.added.length + wcLocal.modified.length + wcLocal.removed.length > 0) {
3525
+ die("you have uncommitted changes \u2014 commit (`sol commit`) or discard them before pulling.");
3526
+ }
3527
+ const peer = openPeer(peerSolDir);
3528
+ const peerHead = await peer.log.head();
3529
+ if (!peerHead) {
3530
+ console.log("peer repo is empty \u2014 nothing to pull");
3531
+ break;
3532
+ }
3533
+ const prevHead = await log.head() ?? "";
3534
+ const res = await converge({ store: new FileStore(solDir2), log }, { nodes: await peerNodes(peer, peerHead), ops: await peer.log.history(), incomingHead: peerHead, actor: actor2 });
3535
+ if (existsSync10(refsPath())) {
3536
+ const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
3537
+ if (refs.branches[refs.current])
3538
+ refs.branches[refs.current].head = res.head;
3539
+ saveRefs(refs);
3540
+ }
3541
+ setOpLogHead(res.head);
3542
+ if (res.head !== prevHead)
3543
+ materializeDiff(loadStore(), prevHead, res.head);
3544
+ if (res.conflicts.length) {
3545
+ console.log(`pulled + merged from ${peerArg} WITH ${res.conflicts.length} conflict(s) \u2014 both sides kept with markers in:`);
3546
+ for (const c of res.conflicts)
3547
+ console.log(" " + c.path);
3548
+ console.log("resolve the <<<<<<< markers, then `sol commit`");
3549
+ process.exitCode = 1;
3550
+ } else {
3551
+ console.log(`pulled from ${peerArg} -> ${res.applied} new op(s)${res.merged ? " (converged by merge)" : ""}, now at ${(res.head || "").slice(0, 12)}`);
3552
+ }
3553
+ break;
3554
+ }
3027
3555
  const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3028
- const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3556
+ const token = process.env.SOL_TOKEN || authExpired();
3029
3557
  const bundle = await remoteExport(cfg, token);
3030
3558
  const ops = await log.history();
3031
3559
  const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
3032
3560
  const localTip = await log.logTip();
3033
- const curBranch = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : bundle.refs?.production || "main";
3561
+ const curBranch = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : bundle.refs?.production || "main";
3034
3562
  const remoteCurHead = bundle.refs?.branches?.[curBranch] ?? bundle.head ?? "";
3035
3563
  if (bundle.seq === localSeq && bundle.tip === localTip) {
3036
- if (bundle.refs && existsSync9(refsPath())) {
3564
+ if (bundle.refs && existsSync10(refsPath())) {
3037
3565
  const lr = JSON.parse(readFileSync9(refsPath(), "utf8"));
3038
3566
  const before = lr.branches[lr.current]?.head;
3039
3567
  for (const [name, h] of Object.entries(bundle.refs.branches))
@@ -3058,7 +3586,7 @@ async function main() {
3058
3586
  break;
3059
3587
  }
3060
3588
  const syncRefHead = (h) => {
3061
- if (!existsSync9(refsPath()))
3589
+ if (!existsSync10(refsPath()))
3062
3590
  return;
3063
3591
  const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
3064
3592
  if (refs.branches[refs.current])
@@ -3078,7 +3606,7 @@ async function main() {
3078
3606
  syncRefHead(remoteCurHead);
3079
3607
  setOpLogHead(remoteCurHead);
3080
3608
  materializeTree(loadStore(), remoteCurHead);
3081
- if (bundle.refs && existsSync9(refsPath())) {
3609
+ if (bundle.refs && existsSync10(refsPath())) {
3082
3610
  const lr = JSON.parse(readFileSync9(refsPath(), "utf8"));
3083
3611
  for (const [name, h] of Object.entries(bundle.refs.branches))
3084
3612
  lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
@@ -3127,9 +3655,9 @@ async function main() {
3127
3655
  for (const c of result.conflicts) {
3128
3656
  const blob = fileAt3(store, result.head, c.path);
3129
3657
  if (blob)
3130
- writeFileSync9(join9(cwd2, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
3658
+ writeFileSync9(join10(cwd2, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
3131
3659
  }
3132
- writeFileSync9(join9(solDir2, "MERGE_HEAD"), remoteHead2);
3660
+ writeFileSync9(join10(solDir2, "MERGE_HEAD"), remoteHead2);
3133
3661
  console.log(`pulled + merged WITH ${result.conflicts.length} conflict(s), left uncommitted in your working tree:`);
3134
3662
  for (const c of result.conflicts)
3135
3663
  console.log(" " + c.path);
@@ -3141,11 +3669,11 @@ async function main() {
3141
3669
  break;
3142
3670
  }
3143
3671
  case "promote": {
3144
- if (!existsSync9(solDir2))
3672
+ if (!existsSync10(solDir2))
3145
3673
  die("not a sol repo");
3146
3674
  const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3147
- const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3148
- const cur = existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : "main";
3675
+ const token = process.env.SOL_TOKEN || authExpired();
3676
+ const cur = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : "main";
3149
3677
  const branch = args[0] || cur;
3150
3678
  const refs = await remotePromote(cfg, token, branch);
3151
3679
  console.log(`promoted '${branch}' -> production '${refs.production}' now at ${(refs.branches[refs.production] ?? "").slice(0, 12)}`);
@@ -3155,10 +3683,10 @@ async function main() {
3155
3683
  const [url, frest] = remoteUrlArg(args);
3156
3684
  const parent = frest[0] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
3157
3685
  const newRepo = frest[1] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
3158
- const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3159
- const target = resolve3(cwd2, frest[2] || newRepo);
3160
- const fdir = join9(target, ".sol");
3161
- if (existsSync9(fdir))
3686
+ const token = process.env.SOL_TOKEN || authExpired();
3687
+ const target = resolve4(cwd2, frest[2] || newRepo);
3688
+ const fdir = join10(target, ".sol");
3689
+ if (existsSync10(fdir))
3162
3690
  die("already a sol repo: " + target);
3163
3691
  const parentCfg = { url, repo: parent };
3164
3692
  const newCfg = { url, repo: newRepo, forkParent: parent };
@@ -3183,8 +3711,8 @@ async function main() {
3183
3711
  const cloneBranches = {};
3184
3712
  for (const [name, h] of Object.entries(srvRefs.branches))
3185
3713
  cloneBranches[name] = { head: h, base: h, remote: h };
3186
- writeFileSync9(join9(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
3187
- writeFileSync9(join9(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
3714
+ writeFileSync9(join10(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
3715
+ writeFileSync9(join10(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
3188
3716
  saveRemote(fdir, newCfg);
3189
3717
  const store = new Store2;
3190
3718
  for (const node of canon.nodes)
@@ -3201,8 +3729,8 @@ async function main() {
3201
3729
  break;
3202
3730
  }
3203
3731
  case "access": {
3732
+ const token = process.env.SOL_TOKEN || authExpired();
3204
3733
  const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3205
- const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3206
3734
  const sub = args[0];
3207
3735
  if (!sub || sub === "show") {
3208
3736
  const a = await accessGet(cfg, token);
@@ -3237,7 +3765,7 @@ async function main() {
3237
3765
  }
3238
3766
  case "forks": {
3239
3767
  const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3240
- const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3768
+ const token = process.env.SOL_TOKEN || authExpired();
3241
3769
  const { forks } = await forksList(cfg, token);
3242
3770
  if (!forks.length)
3243
3771
  console.log(`(no forks of ${cfg.repo})`);
@@ -3250,9 +3778,9 @@ async function main() {
3250
3778
  }
3251
3779
  case "mr": {
3252
3780
  const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
3253
- const token = process.env.SOL_TOKEN || die("set SOL_TOKEN");
3781
+ const token = process.env.SOL_TOKEN || authExpired();
3254
3782
  const sub = args[0];
3255
- const localRefs = () => existsSync9(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
3783
+ const localRefs = () => existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
3256
3784
  const flag = (name) => {
3257
3785
  const i = args.indexOf(name);
3258
3786
  return i >= 0 ? args[i + 1] : undefined;
@@ -3429,9 +3957,9 @@ ${mrSummary(pr)}`);
3429
3957
  if (!command.length)
3430
3958
  die("usage: sol run [--keep <path>] [--isolate] <command...>");
3431
3959
  const head = await repo.head();
3432
- const dir = mkdtempSync(join9(tmpdir(), "sol-run-"));
3960
+ const dir = mkdtempSync(join10(tmpdir(), "sol-run-"));
3433
3961
  try {
3434
- const hn = hydrate2(loadStore(), head, dir);
3962
+ const hn = hydrate3(loadStore(), head, dir);
3435
3963
  console.log(`hydrated ${hn} file(s) -> sandbox${isolate ? " (isolated: no network, writes confined to the sandbox)" : ""}`);
3436
3964
  const argv2 = isolate ? isolateCommand(command, dir) : command;
3437
3965
  console.log(`$ ${command.join(" ")}`);
@@ -3449,7 +3977,7 @@ ${mrSummary(pr)}`);
3449
3977
  const { written, deleted } = await capture(repo, dir, keep);
3450
3978
  if (written.length || deleted.length) {
3451
3979
  await appendCommit(log, await repo.head(), `run: ${command.join(" ")}`, head);
3452
- if (existsSync9(refsPath())) {
3980
+ if (existsSync10(refsPath())) {
3453
3981
  const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
3454
3982
  if (refs.branches[refs.current]) {
3455
3983
  refs.branches[refs.current].head = await repo.head();
@@ -3462,7 +3990,7 @@ ${mrSummary(pr)}`);
3462
3990
  materialize(synced, nh, f);
3463
3991
  for (const f of deleted) {
3464
3992
  try {
3465
- unlinkSync6(join9(cwd2, f));
3993
+ unlinkSync6(join10(cwd2, f));
3466
3994
  } catch {}
3467
3995
  }
3468
3996
  console.log(`captured ${written.length} written, ${deleted.length} deleted file(s):`);
@@ -3481,10 +4009,10 @@ ${mrSummary(pr)}`);
3481
4009
  case "git": {
3482
4010
  const sub = args[0];
3483
4011
  if (sub === "import") {
3484
- const gitPath = resolve3(cwd2, args[1] || die("usage: sol git import <git-repo> [dir]"));
3485
- const target = resolve3(cwd2, args[2] || basename(gitPath));
3486
- const fdir = join9(target, ".sol");
3487
- if (existsSync9(fdir))
4012
+ const gitPath = resolve4(cwd2, args[1] || die("usage: sol git import <git-repo> [dir]"));
4013
+ const target = resolve4(cwd2, args[2] || basename(gitPath));
4014
+ const fdir = join10(target, ".sol");
4015
+ if (existsSync10(fdir))
3488
4016
  die("already a sol repo: " + target);
3489
4017
  mkdirSync7(fdir, { recursive: true });
3490
4018
  const { commits, branches, head, current } = await importGitRepo(gitPath, fdir);
@@ -3494,20 +4022,20 @@ ${mrSummary(pr)}`);
3494
4022
  if (!refsBranches[current])
3495
4023
  refsBranches[current] = { head, base: head };
3496
4024
  refsBranches[current].head = head;
3497
- writeFileSync9(join9(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
4025
+ writeFileSync9(join10(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
3498
4026
  const store = new Store2;
3499
- for (const name of readdirSync7(join9(fdir, "objects"))) {
4027
+ for (const name of readdirSync7(join10(fdir, "objects"))) {
3500
4028
  if (name.endsWith(".tmp"))
3501
4029
  continue;
3502
4030
  try {
3503
- store.put(decodeObject(readFileSync9(join9(fdir, "objects", name))));
4031
+ store.put(decodeObject(readFileSync9(join10(fdir, "objects", name))));
3504
4032
  } catch {}
3505
4033
  }
3506
- const onDisk = hydrate2(store, head, target);
4034
+ const onDisk = hydrate3(store, head, target);
3507
4035
  console.log(`imported ${commits} commit(s), ${branches.length} branch(es) from git -> ${args[2] || basename(gitPath)} (${onDisk} files; on branch ${current})`);
3508
4036
  } else if (sub === "export") {
3509
4037
  const { log } = open();
3510
- const gitPath = resolve3(cwd2, args[1] || die('usage: sol git export <git-repo> [-m "msg"]'));
4038
+ const gitPath = resolve4(cwd2, args[1] || die('usage: sol git export <git-repo> [-m "msg"]'));
3511
4039
  const mi = args.indexOf("-m");
3512
4040
  const store = loadStore();
3513
4041
  const head = await log.head() ?? "";
@@ -3547,6 +4075,10 @@ everyday (examples are copy-safe \u2014 use real filenames):
3547
4075
  sol ignore "*.tmp" add an ignore pattern (no arg lists the active patterns)
3548
4076
  sol fsck / sol gc verify integrity / drop unreachable objects
3549
4077
 
4078
+ privacy (native per-path encryption \u2014 the host only ever stores ciphertext):
4079
+ sol seal secrets/.env alice encrypt a file to recipients (you + alice); keys stay LOCAL, history is host-blind
4080
+ \`sol cat\` decrypts for a recipient; everyone else sees <<sealed>>. (sol seal --help)
4081
+
3550
4082
  branches & tags:
3551
4083
  sol branch list branches (sol branch feature creates one at HEAD)
3552
4084
  sol switch feature switch to a branch (captures current work first, never loses it)
@@ -3563,6 +4095,8 @@ auth (sign in once; remote commands then use the cached token, no SOL_TOKEN need
3563
4095
  remotes (self-hostable backend; token in SOL_TOKEN or via sol auth login):
3564
4096
  sol clone [<url>] <owner>/<repo> [dir] clone a remote repo (checks out PRODUCTION); default dir = <repo>
3565
4097
  sol push / sol pull sync your commits with the remote (push registers your branch's head)
4098
+ sol push <repo> one-step share: no remote set? use the hosted Sol + <repo>, then push
4099
+ sol push --public <repo> create + push + make public in one step (new repos are private by default)
3566
4100
  sol promote [branch] point the remote's production branch at <branch> (default: current)
3567
4101
  sol remote <url> <repo> set the remote (no arg: show it; url defaults to the hosted Sol)
3568
4102
  sol fork [<url>] <parent> <new> [dir] make your own copy of a repo (all branches + history + a parent link)
@@ -3583,7 +4117,8 @@ git interop (adopt Sol without leaving GitHub):
3583
4117
  sol git export <repo> write Sol's tree back as a git commit (+ deletes), then \`git push\`
3584
4118
 
3585
4119
  for concurrent agents, use scoped commits (sol commit -m "msg" file) or a per-agent workspace.
3586
- a whole-tree commit is REFUSED when SOL_ACTOR is set (--whole-tree / SOL_ALLOW_WHOLE_TREE=1 to override).
4120
+ a solo author commits the whole tree freely; once OTHER authors are present the whole-tree commit is refused
4121
+ (--whole-tree / SOL_ALLOW_WHOLE_TREE=1 to override) so one agent can't sweep everyone's pending files.
3587
4122
 
3588
4123
  a ref is a branch/tag name, a commit hash-prefix, an op seq number, or HEAD. content is SHA-256
3589
4124
  addressed; history is a tamper-evident hash-chained op-log. attribute changes with SOL_ACTOR=you.`);
@@ -3592,4 +4127,9 @@ addressed; history is a tamper-evident hash-chained op-log. attribute changes wi
3592
4127
  release?.();
3593
4128
  }
3594
4129
  }
3595
- main().catch((e) => die(e?.message || String(e)));
4130
+ main().catch((e) => {
4131
+ const msg = e?.message || String(e);
4132
+ if (/ -> 401\b/.test(msg) || /\b401 unauthorized\b/i.test(msg))
4133
+ authExpired();
4134
+ die(msg);
4135
+ });