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.
- package/index.js +177 -0
- package/package.json +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1594
|
+
const lockFile = join6(solDir2, "lock");
|
|
1543
1595
|
const deadline = Date.now() + 15000;
|
|
1544
1596
|
for (;; ) {
|
|
1545
1597
|
try {
|
|
1546
|
-
|
|
1598
|
+
writeFileSync6(lockFile, String(process.pid), { flag: "wx" });
|
|
1547
1599
|
break;
|
|
1548
1600
|
} catch {
|
|
1549
1601
|
try {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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) &&
|
|
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 =
|
|
2252
|
-
if (
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
2307
|
-
const parent2 =
|
|
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 (
|
|
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
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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 =
|
|
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()}
|
|
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 =
|
|
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
|
-
|
|
2431
|
-
while (
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
2641
|
-
const lead =
|
|
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 =
|
|
2660
|
-
if (!
|
|
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)});
|
|
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(
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
2935
|
-
const fdir =
|
|
2936
|
-
if (
|
|
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(
|
|
2953
|
-
writeFileSync9(
|
|
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
|
|
2971
|
-
|
|
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 =
|
|
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
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
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
|
-
|
|
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 =
|
|
3012
|
-
refs.branches[branch].remote =
|
|
3491
|
+
refs.branches[branch].head = convergedHead;
|
|
3492
|
+
refs.branches[branch].remote = convergedHead;
|
|
3013
3493
|
}
|
|
3014
3494
|
saveRefs(refs);
|
|
3015
3495
|
}
|
|
3016
|
-
setOpLogHead(
|
|
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} @ ${(
|
|
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} @ ${(
|
|
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 ||
|
|
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 =
|
|
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 &&
|
|
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 (!
|
|
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 &&
|
|
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(
|
|
3658
|
+
writeFileSync9(join10(cwd2, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
|
|
3131
3659
|
}
|
|
3132
|
-
writeFileSync9(
|
|
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 (!
|
|
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 ||
|
|
3148
|
-
const cur =
|
|
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 ||
|
|
3159
|
-
const target =
|
|
3160
|
-
const fdir =
|
|
3161
|
-
if (
|
|
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(
|
|
3187
|
-
writeFileSync9(
|
|
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 ||
|
|
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 ||
|
|
3781
|
+
const token = process.env.SOL_TOKEN || authExpired();
|
|
3254
3782
|
const sub = args[0];
|
|
3255
|
-
const localRefs = () =>
|
|
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(
|
|
3960
|
+
const dir = mkdtempSync(join10(tmpdir(), "sol-run-"));
|
|
3433
3961
|
try {
|
|
3434
|
-
const hn =
|
|
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 (
|
|
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(
|
|
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 =
|
|
3485
|
-
const target =
|
|
3486
|
-
const fdir =
|
|
3487
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
4031
|
+
store.put(decodeObject(readFileSync9(join10(fdir, "objects", name))));
|
|
3504
4032
|
} catch {}
|
|
3505
4033
|
}
|
|
3506
|
-
const onDisk =
|
|
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 =
|
|
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
|
|
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) =>
|
|
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
|
+
});
|