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