midsummer-sol 0.1.2 → 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 +617 -119
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() {
|
|
@@ -1890,6 +1933,27 @@ function printWorkingDiff(store, base, only) {
|
|
|
1890
1933
|
if (!added.length && !removed.length && !modified.length)
|
|
1891
1934
|
console.log(only ? `no working changes in ${only}` : "no working changes");
|
|
1892
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
|
+
}
|
|
1893
1957
|
function isWorkingPath(store, head, arg) {
|
|
1894
1958
|
const rel = repoRel(arg);
|
|
1895
1959
|
if (lexists(join6(cwd2, rel)))
|
|
@@ -2054,10 +2118,275 @@ async function writeBundle(solDir3, bundle, from = 0) {
|
|
|
2054
2118
|
return fresh.length;
|
|
2055
2119
|
}
|
|
2056
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
|
+
|
|
2057
2386
|
// src/bin/runtime.ts
|
|
2058
|
-
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";
|
|
2059
2388
|
import { platform } from "node:os";
|
|
2060
|
-
import { dirname as dirname4, join as
|
|
2389
|
+
import { dirname as dirname4, join as join9, relative as relative4 } from "node:path";
|
|
2061
2390
|
var SYMLINK_MODE3 = 40960;
|
|
2062
2391
|
var EXEC_MODE3 = 493;
|
|
2063
2392
|
function isolateCommand(command, scratch) {
|
|
@@ -2073,7 +2402,7 @@ function isolateCommand(command, scratch) {
|
|
|
2073
2402
|
}
|
|
2074
2403
|
throw new Error(`--isolate not yet wired for platform ${platform()} (macOS sandbox-exec only); omit --isolate to run unconfined`);
|
|
2075
2404
|
}
|
|
2076
|
-
function
|
|
2405
|
+
function hydrate3(store, head, dir) {
|
|
2077
2406
|
if (!head)
|
|
2078
2407
|
return 0;
|
|
2079
2408
|
let n = 0;
|
|
@@ -2081,7 +2410,7 @@ function hydrate2(store, head, dir) {
|
|
|
2081
2410
|
const blob = fileAt(store, head, p);
|
|
2082
2411
|
if (!blob)
|
|
2083
2412
|
continue;
|
|
2084
|
-
const abs =
|
|
2413
|
+
const abs = join9(dir, p);
|
|
2085
2414
|
mkdirSync6(dirname4(abs), { recursive: true });
|
|
2086
2415
|
const mode = modeAt(store, head, p);
|
|
2087
2416
|
if (mode === SYMLINK_MODE3) {
|
|
@@ -2103,7 +2432,7 @@ function hydrate2(store, head, dir) {
|
|
|
2103
2432
|
}
|
|
2104
2433
|
function walkDir(dir, base, pats, out = []) {
|
|
2105
2434
|
for (const name of readdirSync6(dir)) {
|
|
2106
|
-
const p =
|
|
2435
|
+
const p = join9(dir, name);
|
|
2107
2436
|
const rel = relative4(base, p);
|
|
2108
2437
|
if (isIgnored(rel, pats))
|
|
2109
2438
|
continue;
|
|
@@ -2121,11 +2450,11 @@ async function capture(repo, dir, keep = []) {
|
|
|
2121
2450
|
const pats = ignorePatterns();
|
|
2122
2451
|
const files = new Set(walkDir(dir, dir, pats));
|
|
2123
2452
|
for (const k of keep)
|
|
2124
|
-
if (
|
|
2453
|
+
if (existsSync9(join9(dir, k)))
|
|
2125
2454
|
files.add(k);
|
|
2126
2455
|
const written = [];
|
|
2127
2456
|
for (const f of files) {
|
|
2128
|
-
const abs =
|
|
2457
|
+
const abs = join9(dir, f);
|
|
2129
2458
|
const st = lstatSync4(abs);
|
|
2130
2459
|
if (st.isSymbolicLink()) {
|
|
2131
2460
|
const target = readlinkSync4(abs);
|
|
@@ -2153,7 +2482,7 @@ async function capture(repo, dir, keep = []) {
|
|
|
2153
2482
|
}
|
|
2154
2483
|
const deleted = [];
|
|
2155
2484
|
for (const t of await repo.list()) {
|
|
2156
|
-
if (!
|
|
2485
|
+
if (!existsSync9(join9(dir, t))) {
|
|
2157
2486
|
await repo.deleteFile(t);
|
|
2158
2487
|
deleted.push(t);
|
|
2159
2488
|
}
|
|
@@ -2162,7 +2491,7 @@ async function capture(repo, dir, keep = []) {
|
|
|
2162
2491
|
}
|
|
2163
2492
|
|
|
2164
2493
|
// src/bin/sol.ts
|
|
2165
|
-
var CRED_PATH =
|
|
2494
|
+
var CRED_PATH = join10(homedir(), ".sol", "credentials");
|
|
2166
2495
|
function tokenClaims(token) {
|
|
2167
2496
|
try {
|
|
2168
2497
|
return JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString());
|
|
@@ -2171,7 +2500,7 @@ function tokenClaims(token) {
|
|
|
2171
2500
|
}
|
|
2172
2501
|
}
|
|
2173
2502
|
async function loadStoredToken() {
|
|
2174
|
-
if (!
|
|
2503
|
+
if (!existsSync10(CRED_PATH))
|
|
2175
2504
|
return;
|
|
2176
2505
|
let creds;
|
|
2177
2506
|
try {
|
|
@@ -2198,6 +2527,15 @@ async function loadStoredToken() {
|
|
|
2198
2527
|
}
|
|
2199
2528
|
return creds.accessToken;
|
|
2200
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
|
+
}
|
|
2201
2539
|
function authHost() {
|
|
2202
2540
|
if (process.env.SOL_AUTH)
|
|
2203
2541
|
return process.env.SOL_AUTH.replace(/\/+$/, "");
|
|
@@ -2216,15 +2554,81 @@ function resolveRemote(solDir3) {
|
|
|
2216
2554
|
return;
|
|
2217
2555
|
return cfg.url ? cfg : { ...cfg, url: DEFAULT_REMOTE_URL };
|
|
2218
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
|
+
}
|
|
2219
2564
|
async function main() {
|
|
2220
2565
|
const argv = process.argv.slice(2);
|
|
2221
2566
|
if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
|
|
2222
|
-
console.log(
|
|
2567
|
+
console.log(`sol ${cliVersion()}`);
|
|
2223
2568
|
return;
|
|
2224
2569
|
}
|
|
2225
2570
|
const args = argv.slice(1);
|
|
2226
2571
|
const wantsHelp = args.includes("--help") || args.includes("-h");
|
|
2227
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\``,
|
|
2228
2632
|
mr: `sol mr open [--from <branch>] [--to <branch>] [--upstream <repo>] -t "title" [-m body]
|
|
2229
2633
|
sol mr list | show <id> | review <id> --approve|--request-changes|--comment [-m msg]
|
|
2230
2634
|
sol mr comment <id> -m msg [--path f --line N] | check <id> --run -- <cmd> | merge <id> [--force] | close <id>`,
|
|
@@ -2244,12 +2648,12 @@ async function main() {
|
|
|
2244
2648
|
if (t)
|
|
2245
2649
|
process.env.SOL_TOKEN = t;
|
|
2246
2650
|
}
|
|
2247
|
-
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;
|
|
2248
2652
|
try {
|
|
2249
2653
|
switch (cmd) {
|
|
2250
2654
|
case "init": {
|
|
2251
|
-
const here =
|
|
2252
|
-
if (
|
|
2655
|
+
const here = join10(procCwd2, ".sol");
|
|
2656
|
+
if (existsSync10(here))
|
|
2253
2657
|
die("already a sol repo: " + procCwd2);
|
|
2254
2658
|
if (repoRoot2 && repoRoot2 !== procCwd2 && !args.includes("--force")) {
|
|
2255
2659
|
die(`already inside a Sol repo at ${repoRoot2}
|
|
@@ -2263,7 +2667,7 @@ async function main() {
|
|
|
2263
2667
|
}
|
|
2264
2668
|
case "track":
|
|
2265
2669
|
case "add": {
|
|
2266
|
-
if (!
|
|
2670
|
+
if (!existsSync10(solDir2))
|
|
2267
2671
|
die("not a sol repo \u2014 run `sol init` first");
|
|
2268
2672
|
const files = args.filter((a) => a !== "." && !a.startsWith("-"));
|
|
2269
2673
|
if (!files.length) {
|
|
@@ -2274,7 +2678,7 @@ async function main() {
|
|
|
2274
2678
|
let n = 0;
|
|
2275
2679
|
for (const f of files) {
|
|
2276
2680
|
const rf = repoRel(f);
|
|
2277
|
-
if (!
|
|
2681
|
+
if (!existsSync10(join10(cwd2, rf))) {
|
|
2278
2682
|
console.error("skip (not on disk): " + f);
|
|
2279
2683
|
continue;
|
|
2280
2684
|
}
|
|
@@ -2303,14 +2707,14 @@ async function main() {
|
|
|
2303
2707
|
if (!message)
|
|
2304
2708
|
die('commit needs a message: sol commit "what you did" (scoped: sol commit -m "msg" file1 file2)');
|
|
2305
2709
|
const parentHead = await repo.head();
|
|
2306
|
-
const mergeHeadPath =
|
|
2307
|
-
const parent2 =
|
|
2710
|
+
const mergeHeadPath = join10(solDir2, "MERGE_HEAD");
|
|
2711
|
+
const parent2 = existsSync10(mergeHeadPath) ? readFileSync9(mergeHeadPath, "utf8").trim() || undefined : undefined;
|
|
2308
2712
|
let changed = 0;
|
|
2309
2713
|
let commitRoot = parentHead;
|
|
2310
2714
|
if (paths.length) {
|
|
2311
2715
|
for (const p of paths) {
|
|
2312
2716
|
const rp = repoRel(p);
|
|
2313
|
-
if (
|
|
2717
|
+
if (existsSync10(join10(cwd2, rp))) {
|
|
2314
2718
|
if (await snapshotFile(repo, rp))
|
|
2315
2719
|
changed++;
|
|
2316
2720
|
} else if ((await repo.list()).includes(rp)) {
|
|
@@ -2323,7 +2727,8 @@ async function main() {
|
|
|
2323
2727
|
commitRoot = await repo.head();
|
|
2324
2728
|
} else {
|
|
2325
2729
|
const optedIn = wholeTreeOptIn || process.env.SOL_ALLOW_WHOLE_TREE === "1";
|
|
2326
|
-
|
|
2730
|
+
const otherActorPresent = (await log.history()).some((o) => o.by && o.by !== actor2);
|
|
2731
|
+
if (process.env.SOL_ACTOR && !optedIn && otherActorPresent) {
|
|
2327
2732
|
die(`refusing a whole-tree commit as "${actor2}" \u2014 it would attribute ALL pending files to you.
|
|
2328
2733
|
concurrent agents: sol commit -m "${message}" <your files> (or a per-agent SolWorkspace/MCP)
|
|
2329
2734
|
if you truly own the whole tree: add --whole-tree or set SOL_ALLOW_WHOLE_TREE=1`);
|
|
@@ -2360,7 +2765,7 @@ async function main() {
|
|
|
2360
2765
|
}
|
|
2361
2766
|
case "status": {
|
|
2362
2767
|
const { repo, log } = open();
|
|
2363
|
-
const refs =
|
|
2768
|
+
const refs = existsSync10(refsPath()) ? await loadRefs(log) : undefined;
|
|
2364
2769
|
const head = await repo.head();
|
|
2365
2770
|
console.log(`repo ${cwd2}`);
|
|
2366
2771
|
console.log(`on ${refs ? refs.current : "main"} head ${head.slice(0, 14)} seq ${await log.seq()} actor ${actor2}`);
|
|
@@ -2416,7 +2821,7 @@ async function main() {
|
|
|
2416
2821
|
byRoot.set(o.rootAfter, o);
|
|
2417
2822
|
const live = await log.head() ?? "";
|
|
2418
2823
|
const refArg = args.find((a) => !a.startsWith("-"));
|
|
2419
|
-
const lrefs =
|
|
2824
|
+
const lrefs = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : null;
|
|
2420
2825
|
let tip = live;
|
|
2421
2826
|
if (refArg) {
|
|
2422
2827
|
tip = lrefs?.branches[refArg]?.head ?? refArg;
|
|
@@ -2427,19 +2832,27 @@ async function main() {
|
|
|
2427
2832
|
}
|
|
2428
2833
|
const chain = [];
|
|
2429
2834
|
const seen = new Set;
|
|
2430
|
-
|
|
2431
|
-
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;
|
|
2432
2840
|
seen.add(cur);
|
|
2433
2841
|
const c = byRoot.get(cur);
|
|
2434
2842
|
chain.push(c);
|
|
2435
|
-
|
|
2843
|
+
if (c.parent)
|
|
2844
|
+
stack.push(c.parent);
|
|
2845
|
+
if (c.parent2)
|
|
2846
|
+
stack.push(c.parent2);
|
|
2436
2847
|
}
|
|
2848
|
+
chain.sort((a, b) => b.seq - a.seq);
|
|
2437
2849
|
if (!chain.length) {
|
|
2438
2850
|
console.log(ops.length ? '(no commits on this branch yet \u2014 run `sol commit "msg"`)' : "(no history yet)");
|
|
2439
2851
|
}
|
|
2440
2852
|
for (const c of chain) {
|
|
2441
2853
|
const n = countChanges(store, c.parent ?? "", c.rootAfter);
|
|
2442
|
-
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(", ")}` : ""}]` : "";
|
|
2443
2856
|
console.log(`${c.rootAfter.slice(0, 14)} ${(c.by ?? "?").padEnd(14)} ${c.message ?? ""} (${n} change${n === 1 ? "" : "s"})${tag}`);
|
|
2444
2857
|
}
|
|
2445
2858
|
const wc = workingChanges(store, live);
|
|
@@ -2452,7 +2865,7 @@ async function main() {
|
|
|
2452
2865
|
const path = args[0] || die("rm needs a path");
|
|
2453
2866
|
let onDisk = false;
|
|
2454
2867
|
try {
|
|
2455
|
-
unlinkSync6(
|
|
2868
|
+
unlinkSync6(join10(cwd2, path));
|
|
2456
2869
|
onDisk = true;
|
|
2457
2870
|
} catch {}
|
|
2458
2871
|
if (onDisk) {
|
|
@@ -2619,11 +3032,11 @@ async function main() {
|
|
|
2619
3032
|
};
|
|
2620
3033
|
for (const op of ops)
|
|
2621
3034
|
walk(op.rootAfter);
|
|
2622
|
-
const objDir =
|
|
3035
|
+
const objDir = join10(solDir2, "objects");
|
|
2623
3036
|
let removed = 0;
|
|
2624
3037
|
for (const name of readdirSync7(objDir)) {
|
|
2625
3038
|
if (name.endsWith(".tmp") || !reachable.has(name)) {
|
|
2626
|
-
unlinkSync6(
|
|
3039
|
+
unlinkSync6(join10(objDir, name));
|
|
2627
3040
|
removed++;
|
|
2628
3041
|
}
|
|
2629
3042
|
}
|
|
@@ -2637,8 +3050,8 @@ async function main() {
|
|
|
2637
3050
|
console.log(p);
|
|
2638
3051
|
break;
|
|
2639
3052
|
}
|
|
2640
|
-
const f =
|
|
2641
|
-
const lead =
|
|
3053
|
+
const f = join10(cwd2, ".solignore");
|
|
3054
|
+
const lead = existsSync10(f) && !readFileSync9(f, "utf8").endsWith(`
|
|
2642
3055
|
`) ? `
|
|
2643
3056
|
` : "";
|
|
2644
3057
|
appendFileSync4(f, lead + pat + `
|
|
@@ -2656,8 +3069,8 @@ async function main() {
|
|
|
2656
3069
|
if (content === SEALED2)
|
|
2657
3070
|
die("already sealed: " + path);
|
|
2658
3071
|
if (content === undefined) {
|
|
2659
|
-
const abs =
|
|
2660
|
-
if (!
|
|
3072
|
+
const abs = join10(cwd2, path);
|
|
3073
|
+
if (!existsSync10(abs))
|
|
2661
3074
|
die("no such file: " + path);
|
|
2662
3075
|
content = readFileSync9(abs, "utf8");
|
|
2663
3076
|
}
|
|
@@ -2723,7 +3136,7 @@ async function main() {
|
|
|
2723
3136
|
refs.current = name;
|
|
2724
3137
|
saveRefs(refs);
|
|
2725
3138
|
const n = materializeDiff(loadStore(), fromHead, target.head);
|
|
2726
|
-
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`);
|
|
2727
3140
|
break;
|
|
2728
3141
|
}
|
|
2729
3142
|
case "merge": {
|
|
@@ -2744,7 +3157,7 @@ async function main() {
|
|
|
2744
3157
|
const result = merge({ store }, other.base, ours, other.head);
|
|
2745
3158
|
if (result.conflicts.length) {
|
|
2746
3159
|
materializeTree(store, result.head);
|
|
2747
|
-
writeFileSync9(
|
|
3160
|
+
writeFileSync9(join10(solDir2, "MERGE_HEAD"), other.head);
|
|
2748
3161
|
console.log(`merge ${name} -> ${result.conflicts.length} conflict(s), left in your working tree (uncommitted):`);
|
|
2749
3162
|
for (const c of result.conflicts)
|
|
2750
3163
|
console.log(" " + c.path);
|
|
@@ -2763,7 +3176,7 @@ async function main() {
|
|
|
2763
3176
|
break;
|
|
2764
3177
|
}
|
|
2765
3178
|
case "remote": {
|
|
2766
|
-
if (!
|
|
3179
|
+
if (!existsSync10(solDir2))
|
|
2767
3180
|
die("not a sol repo");
|
|
2768
3181
|
if (args[0]) {
|
|
2769
3182
|
const repoName = args[1] || die("usage: sol remote <url> <repo>");
|
|
@@ -2817,7 +3230,7 @@ async function main() {
|
|
|
2817
3230
|
if (!c.handle)
|
|
2818
3231
|
console.log(" (no handle yet \u2014 set one in the web app to get your <handle>/<repo> namespace)");
|
|
2819
3232
|
} else if (sub === "logout") {
|
|
2820
|
-
if (
|
|
3233
|
+
if (existsSync10(CRED_PATH))
|
|
2821
3234
|
unlinkSync6(CRED_PATH);
|
|
2822
3235
|
console.log("logged out");
|
|
2823
3236
|
} else if (sub === "status" || !sub) {
|
|
@@ -2826,7 +3239,7 @@ async function main() {
|
|
|
2826
3239
|
console.log(`authenticated via SOL_TOKEN (env)${c2.handle ? ` as @${c2.handle}` : ""}`);
|
|
2827
3240
|
break;
|
|
2828
3241
|
}
|
|
2829
|
-
if (!
|
|
3242
|
+
if (!existsSync10(CRED_PATH)) {
|
|
2830
3243
|
console.log("not logged in \u2014 run `sol auth login` (or set SOL_TOKEN)");
|
|
2831
3244
|
break;
|
|
2832
3245
|
}
|
|
@@ -2837,20 +3250,26 @@ async function main() {
|
|
|
2837
3250
|
} else if (sub === "whoami") {
|
|
2838
3251
|
const token = process.env.SOL_TOKEN || await loadStoredToken();
|
|
2839
3252
|
if (!token)
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
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>\`)`);
|
|
2848
3267
|
} else if (sub === "set-handle") {
|
|
2849
3268
|
const want = args[1] || die("usage: sol auth set-handle <name>");
|
|
2850
3269
|
const handle = want.toLowerCase();
|
|
2851
3270
|
const token = process.env.SOL_TOKEN || await loadStoredToken();
|
|
2852
3271
|
if (!token)
|
|
2853
|
-
|
|
3272
|
+
authExpired();
|
|
2854
3273
|
let hadHandle = false;
|
|
2855
3274
|
try {
|
|
2856
3275
|
const meRes = await fetch(`${authHost()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
|
|
@@ -2859,7 +3278,7 @@ async function main() {
|
|
|
2859
3278
|
} catch {}
|
|
2860
3279
|
const res = await fetch(`${authHost()}/api/auth/handle`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ handle }) });
|
|
2861
3280
|
if (res.status === 401)
|
|
2862
|
-
|
|
3281
|
+
authExpired();
|
|
2863
3282
|
if (!res.ok) {
|
|
2864
3283
|
let msg = `could not set handle (${res.status})`;
|
|
2865
3284
|
try {
|
|
@@ -2874,7 +3293,7 @@ async function main() {
|
|
|
2874
3293
|
console.log("heads-up: changing your handle re-namespaces your repos under the new <handle>/<repo>.");
|
|
2875
3294
|
console.log(`handle set to @${out.handle}`);
|
|
2876
3295
|
} else if (sub === "pat") {
|
|
2877
|
-
if (!
|
|
3296
|
+
if (!existsSync10(CRED_PATH))
|
|
2878
3297
|
die("run `sol auth login` first");
|
|
2879
3298
|
const creds = JSON.parse(readFileSync9(CRED_PATH, "utf8"));
|
|
2880
3299
|
const token = await loadStoredToken();
|
|
@@ -2928,12 +3347,33 @@ async function main() {
|
|
|
2928
3347
|
break;
|
|
2929
3348
|
}
|
|
2930
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
|
+
}
|
|
2931
3371
|
const [url, rest] = remoteUrlArg(args);
|
|
2932
3372
|
const repoName = rest[0] || die("usage: sol clone [<url>] <owner>/<repo> [dir]");
|
|
2933
3373
|
const token = process.env.SOL_TOKEN || die("set SOL_TOKEN to the backend bearer token");
|
|
2934
|
-
const target =
|
|
2935
|
-
const fdir =
|
|
2936
|
-
if (
|
|
3374
|
+
const target = resolve4(cwd2, rest[1] || repoName.split("/").pop() || repoName);
|
|
3375
|
+
const fdir = join10(target, ".sol");
|
|
3376
|
+
if (existsSync10(fdir))
|
|
2937
3377
|
die("already a sol repo: " + target);
|
|
2938
3378
|
const cfg = { url, repo: repoName };
|
|
2939
3379
|
const bundle = await remoteExport(cfg, token);
|
|
@@ -2949,8 +3389,8 @@ async function main() {
|
|
|
2949
3389
|
cloneBranches[name] = { head: h, base: h, remote: h };
|
|
2950
3390
|
if (!cloneBranches[onBranch])
|
|
2951
3391
|
cloneBranches[onBranch] = { head: checkoutHead, base: checkoutHead, remote: checkoutHead };
|
|
2952
|
-
writeFileSync9(
|
|
2953
|
-
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 }));
|
|
2954
3394
|
const store = new Store2;
|
|
2955
3395
|
for (const node of bundle.nodes)
|
|
2956
3396
|
store.put(node);
|
|
@@ -2967,13 +3407,21 @@ async function main() {
|
|
|
2967
3407
|
}
|
|
2968
3408
|
case "push": {
|
|
2969
3409
|
const { log } = open();
|
|
2970
|
-
|
|
2971
|
-
|
|
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();
|
|
2972
3420
|
const rh = await remoteHead(cfg, token);
|
|
2973
3421
|
const ops = await log.history();
|
|
2974
3422
|
const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
|
|
2975
3423
|
const localTip = await log.logTip();
|
|
2976
|
-
const localRefs =
|
|
3424
|
+
const localRefs = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : undefined;
|
|
2977
3425
|
const branch = localRefs?.current ?? "main";
|
|
2978
3426
|
const branchHead = await log.head() ?? "";
|
|
2979
3427
|
const remoteWasEmpty = rh.seq === 0 && !rh.tip;
|
|
@@ -2981,26 +3429,22 @@ async function main() {
|
|
|
2981
3429
|
console.log(remoteWasEmpty ? "nothing to push \u2014 local repo is empty (commit something first)" : "everything up to date");
|
|
2982
3430
|
break;
|
|
2983
3431
|
}
|
|
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
|
-
}
|
|
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
|
+
});
|
|
3001
3443
|
const canon = await remoteExport(cfg, token);
|
|
3444
|
+
const prevHead = await log.head() ?? "";
|
|
3002
3445
|
await writeBundle(solDir2, canon, 0);
|
|
3003
|
-
|
|
3446
|
+
const convergedHead = res.head ?? canon.refs?.branches?.[branch] ?? branchHead;
|
|
3447
|
+
if (existsSync10(refsPath())) {
|
|
3004
3448
|
const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
|
|
3005
3449
|
if (canon.refs) {
|
|
3006
3450
|
for (const [name, h] of Object.entries(canon.refs.branches)) {
|
|
@@ -3008,32 +3452,76 @@ async function main() {
|
|
|
3008
3452
|
}
|
|
3009
3453
|
}
|
|
3010
3454
|
if (refs.branches[branch]) {
|
|
3011
|
-
refs.branches[branch].head =
|
|
3012
|
-
refs.branches[branch].remote =
|
|
3455
|
+
refs.branches[branch].head = convergedHead;
|
|
3456
|
+
refs.branches[branch].remote = convergedHead;
|
|
3013
3457
|
}
|
|
3014
3458
|
saveRefs(refs);
|
|
3015
3459
|
}
|
|
3016
|
-
setOpLogHead(
|
|
3460
|
+
setOpLogHead(convergedHead);
|
|
3461
|
+
if (convergedHead && convergedHead !== prevHead)
|
|
3462
|
+
materializeDiff(loadStore(), prevHead, convergedHead);
|
|
3017
3463
|
const prod = res.refs?.production;
|
|
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(", ")}` : "";
|
|
3018
3466
|
if (remoteWasEmpty) {
|
|
3019
|
-
console.log(`created remote repo ${cfg.repo} \u2014 pushed ${res.applied} op(s) (branch ${branch} @ ${(
|
|
3467
|
+
console.log(`created remote repo ${cfg.repo} \u2014 pushed ${res.applied} op(s) (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${prod ? `; production=${prod}` : ""}`);
|
|
3020
3468
|
} else {
|
|
3021
|
-
console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(
|
|
3469
|
+
console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${mergedNote}${conflictNote}${prod ? `; production=${prod}` : ""}`);
|
|
3022
3470
|
}
|
|
3023
3471
|
break;
|
|
3024
3472
|
}
|
|
3025
3473
|
case "pull": {
|
|
3026
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
|
+
}
|
|
3027
3515
|
const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
|
|
3028
|
-
const token = process.env.SOL_TOKEN ||
|
|
3516
|
+
const token = process.env.SOL_TOKEN || authExpired();
|
|
3029
3517
|
const bundle = await remoteExport(cfg, token);
|
|
3030
3518
|
const ops = await log.history();
|
|
3031
3519
|
const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
|
|
3032
3520
|
const localTip = await log.logTip();
|
|
3033
|
-
const curBranch =
|
|
3521
|
+
const curBranch = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : bundle.refs?.production || "main";
|
|
3034
3522
|
const remoteCurHead = bundle.refs?.branches?.[curBranch] ?? bundle.head ?? "";
|
|
3035
3523
|
if (bundle.seq === localSeq && bundle.tip === localTip) {
|
|
3036
|
-
if (bundle.refs &&
|
|
3524
|
+
if (bundle.refs && existsSync10(refsPath())) {
|
|
3037
3525
|
const lr = JSON.parse(readFileSync9(refsPath(), "utf8"));
|
|
3038
3526
|
const before = lr.branches[lr.current]?.head;
|
|
3039
3527
|
for (const [name, h] of Object.entries(bundle.refs.branches))
|
|
@@ -3058,7 +3546,7 @@ async function main() {
|
|
|
3058
3546
|
break;
|
|
3059
3547
|
}
|
|
3060
3548
|
const syncRefHead = (h) => {
|
|
3061
|
-
if (!
|
|
3549
|
+
if (!existsSync10(refsPath()))
|
|
3062
3550
|
return;
|
|
3063
3551
|
const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
|
|
3064
3552
|
if (refs.branches[refs.current])
|
|
@@ -3078,7 +3566,7 @@ async function main() {
|
|
|
3078
3566
|
syncRefHead(remoteCurHead);
|
|
3079
3567
|
setOpLogHead(remoteCurHead);
|
|
3080
3568
|
materializeTree(loadStore(), remoteCurHead);
|
|
3081
|
-
if (bundle.refs &&
|
|
3569
|
+
if (bundle.refs && existsSync10(refsPath())) {
|
|
3082
3570
|
const lr = JSON.parse(readFileSync9(refsPath(), "utf8"));
|
|
3083
3571
|
for (const [name, h] of Object.entries(bundle.refs.branches))
|
|
3084
3572
|
lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
|
|
@@ -3127,9 +3615,9 @@ async function main() {
|
|
|
3127
3615
|
for (const c of result.conflicts) {
|
|
3128
3616
|
const blob = fileAt3(store, result.head, c.path);
|
|
3129
3617
|
if (blob)
|
|
3130
|
-
writeFileSync9(
|
|
3618
|
+
writeFileSync9(join10(cwd2, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
|
|
3131
3619
|
}
|
|
3132
|
-
writeFileSync9(
|
|
3620
|
+
writeFileSync9(join10(solDir2, "MERGE_HEAD"), remoteHead2);
|
|
3133
3621
|
console.log(`pulled + merged WITH ${result.conflicts.length} conflict(s), left uncommitted in your working tree:`);
|
|
3134
3622
|
for (const c of result.conflicts)
|
|
3135
3623
|
console.log(" " + c.path);
|
|
@@ -3141,11 +3629,11 @@ async function main() {
|
|
|
3141
3629
|
break;
|
|
3142
3630
|
}
|
|
3143
3631
|
case "promote": {
|
|
3144
|
-
if (!
|
|
3632
|
+
if (!existsSync10(solDir2))
|
|
3145
3633
|
die("not a sol repo");
|
|
3146
3634
|
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 =
|
|
3635
|
+
const token = process.env.SOL_TOKEN || authExpired();
|
|
3636
|
+
const cur = existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")).current : "main";
|
|
3149
3637
|
const branch = args[0] || cur;
|
|
3150
3638
|
const refs = await remotePromote(cfg, token, branch);
|
|
3151
3639
|
console.log(`promoted '${branch}' -> production '${refs.production}' now at ${(refs.branches[refs.production] ?? "").slice(0, 12)}`);
|
|
@@ -3155,10 +3643,10 @@ async function main() {
|
|
|
3155
3643
|
const [url, frest] = remoteUrlArg(args);
|
|
3156
3644
|
const parent = frest[0] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
|
|
3157
3645
|
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 (
|
|
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))
|
|
3162
3650
|
die("already a sol repo: " + target);
|
|
3163
3651
|
const parentCfg = { url, repo: parent };
|
|
3164
3652
|
const newCfg = { url, repo: newRepo, forkParent: parent };
|
|
@@ -3183,8 +3671,8 @@ async function main() {
|
|
|
3183
3671
|
const cloneBranches = {};
|
|
3184
3672
|
for (const [name, h] of Object.entries(srvRefs.branches))
|
|
3185
3673
|
cloneBranches[name] = { head: h, base: h, remote: h };
|
|
3186
|
-
writeFileSync9(
|
|
3187
|
-
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 }));
|
|
3188
3676
|
saveRemote(fdir, newCfg);
|
|
3189
3677
|
const store = new Store2;
|
|
3190
3678
|
for (const node of canon.nodes)
|
|
@@ -3201,8 +3689,8 @@ async function main() {
|
|
|
3201
3689
|
break;
|
|
3202
3690
|
}
|
|
3203
3691
|
case "access": {
|
|
3692
|
+
const token = process.env.SOL_TOKEN || authExpired();
|
|
3204
3693
|
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
3694
|
const sub = args[0];
|
|
3207
3695
|
if (!sub || sub === "show") {
|
|
3208
3696
|
const a = await accessGet(cfg, token);
|
|
@@ -3237,7 +3725,7 @@ async function main() {
|
|
|
3237
3725
|
}
|
|
3238
3726
|
case "forks": {
|
|
3239
3727
|
const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
|
|
3240
|
-
const token = process.env.SOL_TOKEN ||
|
|
3728
|
+
const token = process.env.SOL_TOKEN || authExpired();
|
|
3241
3729
|
const { forks } = await forksList(cfg, token);
|
|
3242
3730
|
if (!forks.length)
|
|
3243
3731
|
console.log(`(no forks of ${cfg.repo})`);
|
|
@@ -3250,9 +3738,9 @@ async function main() {
|
|
|
3250
3738
|
}
|
|
3251
3739
|
case "mr": {
|
|
3252
3740
|
const cfg = resolveRemote(solDir2) || die("no remote \u2014 set one with `sol remote <url> <repo>`");
|
|
3253
|
-
const token = process.env.SOL_TOKEN ||
|
|
3741
|
+
const token = process.env.SOL_TOKEN || authExpired();
|
|
3254
3742
|
const sub = args[0];
|
|
3255
|
-
const localRefs = () =>
|
|
3743
|
+
const localRefs = () => existsSync10(refsPath()) ? JSON.parse(readFileSync9(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
|
|
3256
3744
|
const flag = (name) => {
|
|
3257
3745
|
const i = args.indexOf(name);
|
|
3258
3746
|
return i >= 0 ? args[i + 1] : undefined;
|
|
@@ -3429,9 +3917,9 @@ ${mrSummary(pr)}`);
|
|
|
3429
3917
|
if (!command.length)
|
|
3430
3918
|
die("usage: sol run [--keep <path>] [--isolate] <command...>");
|
|
3431
3919
|
const head = await repo.head();
|
|
3432
|
-
const dir = mkdtempSync(
|
|
3920
|
+
const dir = mkdtempSync(join10(tmpdir(), "sol-run-"));
|
|
3433
3921
|
try {
|
|
3434
|
-
const hn =
|
|
3922
|
+
const hn = hydrate3(loadStore(), head, dir);
|
|
3435
3923
|
console.log(`hydrated ${hn} file(s) -> sandbox${isolate ? " (isolated: no network, writes confined to the sandbox)" : ""}`);
|
|
3436
3924
|
const argv2 = isolate ? isolateCommand(command, dir) : command;
|
|
3437
3925
|
console.log(`$ ${command.join(" ")}`);
|
|
@@ -3449,7 +3937,7 @@ ${mrSummary(pr)}`);
|
|
|
3449
3937
|
const { written, deleted } = await capture(repo, dir, keep);
|
|
3450
3938
|
if (written.length || deleted.length) {
|
|
3451
3939
|
await appendCommit(log, await repo.head(), `run: ${command.join(" ")}`, head);
|
|
3452
|
-
if (
|
|
3940
|
+
if (existsSync10(refsPath())) {
|
|
3453
3941
|
const refs = JSON.parse(readFileSync9(refsPath(), "utf8"));
|
|
3454
3942
|
if (refs.branches[refs.current]) {
|
|
3455
3943
|
refs.branches[refs.current].head = await repo.head();
|
|
@@ -3462,7 +3950,7 @@ ${mrSummary(pr)}`);
|
|
|
3462
3950
|
materialize(synced, nh, f);
|
|
3463
3951
|
for (const f of deleted) {
|
|
3464
3952
|
try {
|
|
3465
|
-
unlinkSync6(
|
|
3953
|
+
unlinkSync6(join10(cwd2, f));
|
|
3466
3954
|
} catch {}
|
|
3467
3955
|
}
|
|
3468
3956
|
console.log(`captured ${written.length} written, ${deleted.length} deleted file(s):`);
|
|
@@ -3481,10 +3969,10 @@ ${mrSummary(pr)}`);
|
|
|
3481
3969
|
case "git": {
|
|
3482
3970
|
const sub = args[0];
|
|
3483
3971
|
if (sub === "import") {
|
|
3484
|
-
const gitPath =
|
|
3485
|
-
const target =
|
|
3486
|
-
const fdir =
|
|
3487
|
-
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))
|
|
3488
3976
|
die("already a sol repo: " + target);
|
|
3489
3977
|
mkdirSync7(fdir, { recursive: true });
|
|
3490
3978
|
const { commits, branches, head, current } = await importGitRepo(gitPath, fdir);
|
|
@@ -3494,20 +3982,20 @@ ${mrSummary(pr)}`);
|
|
|
3494
3982
|
if (!refsBranches[current])
|
|
3495
3983
|
refsBranches[current] = { head, base: head };
|
|
3496
3984
|
refsBranches[current].head = head;
|
|
3497
|
-
writeFileSync9(
|
|
3985
|
+
writeFileSync9(join10(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
|
|
3498
3986
|
const store = new Store2;
|
|
3499
|
-
for (const name of readdirSync7(
|
|
3987
|
+
for (const name of readdirSync7(join10(fdir, "objects"))) {
|
|
3500
3988
|
if (name.endsWith(".tmp"))
|
|
3501
3989
|
continue;
|
|
3502
3990
|
try {
|
|
3503
|
-
store.put(decodeObject(readFileSync9(
|
|
3991
|
+
store.put(decodeObject(readFileSync9(join10(fdir, "objects", name))));
|
|
3504
3992
|
} catch {}
|
|
3505
3993
|
}
|
|
3506
|
-
const onDisk =
|
|
3994
|
+
const onDisk = hydrate3(store, head, target);
|
|
3507
3995
|
console.log(`imported ${commits} commit(s), ${branches.length} branch(es) from git -> ${args[2] || basename(gitPath)} (${onDisk} files; on branch ${current})`);
|
|
3508
3996
|
} else if (sub === "export") {
|
|
3509
3997
|
const { log } = open();
|
|
3510
|
-
const gitPath =
|
|
3998
|
+
const gitPath = resolve4(cwd2, args[1] || die('usage: sol git export <git-repo> [-m "msg"]'));
|
|
3511
3999
|
const mi = args.indexOf("-m");
|
|
3512
4000
|
const store = loadStore();
|
|
3513
4001
|
const head = await log.head() ?? "";
|
|
@@ -3547,6 +4035,10 @@ everyday (examples are copy-safe \u2014 use real filenames):
|
|
|
3547
4035
|
sol ignore "*.tmp" add an ignore pattern (no arg lists the active patterns)
|
|
3548
4036
|
sol fsck / sol gc verify integrity / drop unreachable objects
|
|
3549
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
|
+
|
|
3550
4042
|
branches & tags:
|
|
3551
4043
|
sol branch list branches (sol branch feature creates one at HEAD)
|
|
3552
4044
|
sol switch feature switch to a branch (captures current work first, never loses it)
|
|
@@ -3563,6 +4055,7 @@ auth (sign in once; remote commands then use the cached token, no SOL_TOKEN need
|
|
|
3563
4055
|
remotes (self-hostable backend; token in SOL_TOKEN or via sol auth login):
|
|
3564
4056
|
sol clone [<url>] <owner>/<repo> [dir] clone a remote repo (checks out PRODUCTION); default dir = <repo>
|
|
3565
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
|
|
3566
4059
|
sol promote [branch] point the remote's production branch at <branch> (default: current)
|
|
3567
4060
|
sol remote <url> <repo> set the remote (no arg: show it; url defaults to the hosted Sol)
|
|
3568
4061
|
sol fork [<url>] <parent> <new> [dir] make your own copy of a repo (all branches + history + a parent link)
|
|
@@ -3592,4 +4085,9 @@ addressed; history is a tamper-evident hash-chained op-log. attribute changes wi
|
|
|
3592
4085
|
release?.();
|
|
3593
4086
|
}
|
|
3594
4087
|
}
|
|
3595
|
-
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
|
+
});
|