midsummer-sol 0.2.1 → 0.2.2
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/package.json +1 -1
- package/sol-mcp.js +11447 -94
- package/sol-secret-mcp.js +177 -15
- package/sol.js +4190 -155
package/sol.js
CHANGED
|
@@ -3312,6 +3312,141 @@ function clearMergeConflicts(solDir2) {
|
|
|
3312
3312
|
var configPath = (solDir2) => join6(solDir2, "sol.json"), semanticPath = (solDir2) => join6(solDir2, "SEMANTIC"), mergeConflictsPath = (solDir2) => join6(solDir2, "MERGE_CONFLICTS");
|
|
3313
3313
|
var init_test_gate = () => {};
|
|
3314
3314
|
|
|
3315
|
+
// src/bin/mcp-http.ts
|
|
3316
|
+
var exports_mcp_http = {};
|
|
3317
|
+
__export(exports_mcp_http, {
|
|
3318
|
+
serveMcpHttp: () => serveMcpHttp,
|
|
3319
|
+
resolveMcpToken: () => resolveMcpToken,
|
|
3320
|
+
parseStandaloneHttp: () => parseStandaloneHttp
|
|
3321
|
+
});
|
|
3322
|
+
import { randomUUID, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
3323
|
+
import { createServer } from "node:http";
|
|
3324
|
+
function tokenMatches(provided, expected) {
|
|
3325
|
+
const a = Buffer.from(provided);
|
|
3326
|
+
const b = Buffer.from(expected);
|
|
3327
|
+
if (a.length !== b.length)
|
|
3328
|
+
return false;
|
|
3329
|
+
return timingSafeEqual3(a, b);
|
|
3330
|
+
}
|
|
3331
|
+
function bearer(req) {
|
|
3332
|
+
const h = req.headers["authorization"];
|
|
3333
|
+
if (typeof h !== "string")
|
|
3334
|
+
return;
|
|
3335
|
+
const m = /^Bearer\s+(.+)$/i.exec(h.trim());
|
|
3336
|
+
return m ? m[1].trim() : undefined;
|
|
3337
|
+
}
|
|
3338
|
+
function writeJson(res, status, body) {
|
|
3339
|
+
const s = JSON.stringify(body);
|
|
3340
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
3341
|
+
res.end(s);
|
|
3342
|
+
}
|
|
3343
|
+
async function readBody(req) {
|
|
3344
|
+
const chunks = [];
|
|
3345
|
+
for await (const c of req)
|
|
3346
|
+
chunks.push(c);
|
|
3347
|
+
if (chunks.length === 0)
|
|
3348
|
+
return;
|
|
3349
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
3350
|
+
if (!raw.trim())
|
|
3351
|
+
return;
|
|
3352
|
+
return JSON.parse(raw);
|
|
3353
|
+
}
|
|
3354
|
+
async function serveMcpHttp(buildServer, opts) {
|
|
3355
|
+
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
3356
|
+
const { isInitializeRequest } = await import("@modelcontextprotocol/sdk/types.js");
|
|
3357
|
+
const port = opts.port ?? 8765;
|
|
3358
|
+
const host = opts.host ?? "127.0.0.1";
|
|
3359
|
+
const transports = new Map;
|
|
3360
|
+
const http = createServer(async (req, res) => {
|
|
3361
|
+
try {
|
|
3362
|
+
const tok = bearer(req);
|
|
3363
|
+
if (!tok || !tokenMatches(tok, opts.token)) {
|
|
3364
|
+
res.setHeader("WWW-Authenticate", "Bearer");
|
|
3365
|
+
writeJson(res, 401, { jsonrpc: "2.0", error: { code: -32001, message: "unauthorized: a valid Authorization: Bearer token is required" }, id: null });
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
const url = (req.url ?? "").split("?")[0];
|
|
3369
|
+
if (url !== MCP_PATH) {
|
|
3370
|
+
writeJson(res, 404, { jsonrpc: "2.0", error: { code: -32601, message: `not found — the MCP endpoint is ${MCP_PATH}` }, id: null });
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
3374
|
+
const sid = typeof sessionId === "string" ? sessionId : undefined;
|
|
3375
|
+
if (req.method === "POST") {
|
|
3376
|
+
const body = await readBody(req);
|
|
3377
|
+
let transport = sid ? transports.get(sid) : undefined;
|
|
3378
|
+
if (!transport) {
|
|
3379
|
+
if (sid || !isInitializeRequest(body)) {
|
|
3380
|
+
writeJson(res, 400, { jsonrpc: "2.0", error: { code: -32000, message: "no valid session — send an initialize request first" }, id: null });
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
transport = new StreamableHTTPServerTransport({
|
|
3384
|
+
sessionIdGenerator: () => randomUUID(),
|
|
3385
|
+
onsessioninitialized: (id) => void transports.set(id, transport)
|
|
3386
|
+
});
|
|
3387
|
+
transport.onclose = () => {
|
|
3388
|
+
if (transport.sessionId)
|
|
3389
|
+
transports.delete(transport.sessionId);
|
|
3390
|
+
};
|
|
3391
|
+
const server = await buildServer();
|
|
3392
|
+
await server.connect(transport);
|
|
3393
|
+
}
|
|
3394
|
+
await transport.handleRequest(req, res, body);
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
if (req.method === "GET" || req.method === "DELETE") {
|
|
3398
|
+
const transport = sid ? transports.get(sid) : undefined;
|
|
3399
|
+
if (!transport) {
|
|
3400
|
+
writeJson(res, 400, { jsonrpc: "2.0", error: { code: -32000, message: "unknown or missing Mcp-Session-Id" }, id: null });
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
await transport.handleRequest(req, res);
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
writeJson(res, 405, { jsonrpc: "2.0", error: { code: -32000, message: "method not allowed" }, id: null });
|
|
3407
|
+
} catch (e) {
|
|
3408
|
+
if (!res.headersSent) {
|
|
3409
|
+
writeJson(res, 500, { jsonrpc: "2.0", error: { code: -32603, message: "internal error: " + (e instanceof Error ? e.message : String(e)) }, id: null });
|
|
3410
|
+
} else {
|
|
3411
|
+
try {
|
|
3412
|
+
res.end();
|
|
3413
|
+
} catch {}
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
await new Promise((resolve2, reject) => {
|
|
3418
|
+
http.once("error", reject);
|
|
3419
|
+
http.listen(port, host, () => {
|
|
3420
|
+
http.off("error", reject);
|
|
3421
|
+
process.stderr.write(`${opts.label} MCP (HTTP) listening on http://${host}:${port}${MCP_PATH} — Authorization: Bearer required
|
|
3422
|
+
`);
|
|
3423
|
+
resolve2();
|
|
3424
|
+
});
|
|
3425
|
+
});
|
|
3426
|
+
await new Promise(() => {});
|
|
3427
|
+
}
|
|
3428
|
+
function resolveMcpToken(flagToken) {
|
|
3429
|
+
const t = (flagToken ?? process.env.SOL_MCP_TOKEN ?? "").trim();
|
|
3430
|
+
return t.length ? t : undefined;
|
|
3431
|
+
}
|
|
3432
|
+
function parseStandaloneHttp(argv, label = "sol") {
|
|
3433
|
+
if (!argv.includes("--http"))
|
|
3434
|
+
return;
|
|
3435
|
+
const val = (name) => {
|
|
3436
|
+
const i = argv.indexOf(name);
|
|
3437
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined;
|
|
3438
|
+
};
|
|
3439
|
+
const token = resolveMcpToken(val("--token"));
|
|
3440
|
+
if (!token) {
|
|
3441
|
+
process.stderr.write("refusing to start an OPEN HTTP MCP — set SOL_MCP_TOKEN (or --token <T>). every request must send `Authorization: Bearer <token>`.\n");
|
|
3442
|
+
process.exit(1);
|
|
3443
|
+
}
|
|
3444
|
+
const portRaw = val("--port");
|
|
3445
|
+
return { token, port: portRaw ? Number(portRaw) : undefined, host: val("--host"), label };
|
|
3446
|
+
}
|
|
3447
|
+
var MCP_PATH = "/mcp";
|
|
3448
|
+
var init_mcp_http = () => {};
|
|
3449
|
+
|
|
3315
3450
|
// src/secret/store.ts
|
|
3316
3451
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3317
3452
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -4468,9 +4603,11 @@ function resolveAll(world) {
|
|
|
4468
4603
|
return world.manifest.order.map((e) => resolveOne(world, e));
|
|
4469
4604
|
}
|
|
4470
4605
|
function audienceFor(world, env, entryAud) {
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4606
|
+
const base = entryAud && entryAud.length ? entryAud : world.files[env]?.audience ?? [];
|
|
4607
|
+
const runtime = world.manifest.runtime[env];
|
|
4608
|
+
if (runtime && !base.includes(runtime))
|
|
4609
|
+
return [...base, runtime];
|
|
4610
|
+
return base;
|
|
4474
4611
|
}
|
|
4475
4612
|
function ensureEnvDir(solDir2) {
|
|
4476
4613
|
mkdirSync6(envDir(solDir2), { recursive: true });
|
|
@@ -4561,7 +4698,14 @@ function validateWorld(solDir2, world, opts = {}) {
|
|
|
4561
4698
|
const actual = new Set(recipientsOf(box));
|
|
4562
4699
|
const expected = new Set(resolvedAud.accountIds);
|
|
4563
4700
|
const extra = [...actual].filter((a) => !expected.has(a));
|
|
4564
|
-
|
|
4701
|
+
let missing = [...expected].filter((a) => !actual.has(a));
|
|
4702
|
+
const runtimeHandle2 = world.manifest.runtime[r.env];
|
|
4703
|
+
const runtimeAccts = new Set((runtimeHandle2 ? world.gate.handles[runtimeHandle2] ?? [] : []).map((rec) => rec.accountId));
|
|
4704
|
+
const missingRuntime = missing.filter((a) => runtimeAccts.has(a));
|
|
4705
|
+
missing = missing.filter((a) => !runtimeAccts.has(a));
|
|
4706
|
+
if (missingRuntime.length) {
|
|
4707
|
+
issues.push({ severity: "warn", check: "audience-integrity", message: `${r.env}/${s.name}: the runtime recipient [${missingRuntime.join(", ")}] joined the env @audience after this value was sealed — it can't inject this secret yet. reseal it (\`sol secret set ${s.name} --env ${r.env}\`) to include the runtime.` });
|
|
4708
|
+
}
|
|
4565
4709
|
if (extra.length || missing.length) {
|
|
4566
4710
|
issues.push({ severity: "error", check: "audience-integrity", message: `${r.env}/${s.name}: sealed recipients drift from the audience (missing: [${missing.join(", ")}], extra: [${extra.join(", ")}]) — reseal with \`sol secret set\` or \`sol secret audience\`` });
|
|
4567
4711
|
}
|
|
@@ -7429,7 +7573,9 @@ function secretInject(ctx, args) {
|
|
|
7429
7573
|
ctx.die("usage: sol secret inject --env E -- <cmd> [args...]");
|
|
7430
7574
|
const cmd = args.slice(dashdash + 1);
|
|
7431
7575
|
const world = loadWorld(ctx.solDir);
|
|
7432
|
-
const
|
|
7576
|
+
const runtimeRoot = process.env.SOL_RUNTIME_RECOVERY_CODE;
|
|
7577
|
+
const self = runtimeRoot ? runtimeSelfFromRoot(env, runtimeRoot) : ctx.loadSelfIdentity();
|
|
7578
|
+
const recipientActor = self?.accountId ?? ctx.actor;
|
|
7433
7579
|
const injected = {};
|
|
7434
7580
|
let revealedCount = 0;
|
|
7435
7581
|
for (const s of resolveOne(world, env).secrets) {
|
|
@@ -7446,7 +7592,7 @@ function secretInject(ctx, args) {
|
|
|
7446
7592
|
revealedCount++;
|
|
7447
7593
|
}
|
|
7448
7594
|
if (!revealedCount)
|
|
7449
|
-
ctx.die(`no secrets in ${env} are readable by ${
|
|
7595
|
+
ctx.die(`no secrets in ${env} are readable by ${recipientActor} — are you a recipient? (set SOL_RECOVERY_CODE, or SOL_RUNTIME_RECOVERY_CODE when injecting from the runtime)`);
|
|
7450
7596
|
process.stderr.write(`sol: injecting ${revealedCount} secret(s) into the subprocess env: ${Object.keys(injected).join(", ")}
|
|
7451
7597
|
`);
|
|
7452
7598
|
const res = spawnSync2(cmd[0], cmd.slice(1), { stdio: "inherit", env: { ...process.env, ...injected } });
|
|
@@ -7563,6 +7709,11 @@ async function runtimeProvision(ctx, args) {
|
|
|
7563
7709
|
const { gate } = await audienceAdd(audCtx, world.gate, prov.handle, prov.accountId, prov.x25519Pub);
|
|
7564
7710
|
writeAudienceDoc(ctx.solDir, gate);
|
|
7565
7711
|
journal2(ctx, id, { op: "audience-add", handle: prov.handle, members: memberSnapshot(gate, prov.handle), recipientAtOp: true, prevAud, newAud: (gate.handles[prov.handle] ?? []).map((r) => r.accountId), at: Date.now() });
|
|
7712
|
+
const envFile = world.files[env];
|
|
7713
|
+
if (envFile && !(envFile.audience ?? []).includes(prov.handle)) {
|
|
7714
|
+
envFile.audience = [...envFile.audience ?? [], prov.handle];
|
|
7715
|
+
writeEnvFile(ctx.solDir, envFile);
|
|
7716
|
+
}
|
|
7566
7717
|
world.manifest.runtime[env] = prov.handle;
|
|
7567
7718
|
writeManifest(ctx.solDir, world.manifest);
|
|
7568
7719
|
regenerateSchema(ctx.solDir, loadWorld(ctx.solDir));
|
|
@@ -7866,16 +8017,9 @@ __export(exports_sol_secret_mcp, {
|
|
|
7866
8017
|
});
|
|
7867
8018
|
import { existsSync as existsSync16 } from "node:fs";
|
|
7868
8019
|
import { join as join16 } from "node:path";
|
|
7869
|
-
async function
|
|
8020
|
+
async function buildSecretServer(solDir2) {
|
|
7870
8021
|
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
7871
|
-
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7872
8022
|
const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
7873
|
-
const solDir2 = opts.solDir || process.env.SOL_DIR || join16(process.cwd(), ".sol");
|
|
7874
|
-
if (!existsSync16(solDir2)) {
|
|
7875
|
-
process.stderr.write(`sol-secret-mcp: no .sol at ${solDir2} — run \`sol init\` first (or set SOL_DIR)
|
|
7876
|
-
`);
|
|
7877
|
-
process.exit(1);
|
|
7878
|
-
}
|
|
7879
8023
|
const dirUrl = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
|
|
7880
8024
|
async function fetchKey2(account) {
|
|
7881
8025
|
const { fetchKey: f } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
@@ -7905,11 +8049,28 @@ async function startSecretMcp(opts = {}) {
|
|
|
7905
8049
|
const r = await callMcpTool(ctx, req.params.name, req.params.arguments ?? {});
|
|
7906
8050
|
return { content: [{ type: "text", text: r.text }], isError: r.isError };
|
|
7907
8051
|
});
|
|
8052
|
+
return server;
|
|
8053
|
+
}
|
|
8054
|
+
async function startSecretMcp(opts = {}) {
|
|
8055
|
+
const solDir2 = opts.solDir || process.env.SOL_DIR || join16(process.cwd(), ".sol");
|
|
8056
|
+
if (!existsSync16(solDir2)) {
|
|
8057
|
+
process.stderr.write(`sol-secret-mcp: no .sol at ${solDir2} — run \`sol init\` first (or set SOL_DIR)
|
|
8058
|
+
`);
|
|
8059
|
+
process.exit(1);
|
|
8060
|
+
}
|
|
8061
|
+
if (opts.http) {
|
|
8062
|
+
const { serveMcpHttp: serveMcpHttp2 } = await Promise.resolve().then(() => (init_mcp_http(), exports_mcp_http));
|
|
8063
|
+
await serveMcpHttp2(() => buildSecretServer(solDir2), opts.http);
|
|
8064
|
+
return;
|
|
8065
|
+
}
|
|
8066
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8067
|
+
const server = await buildSecretServer(solDir2);
|
|
7908
8068
|
await server.connect(new StdioServerTransport);
|
|
7909
8069
|
}
|
|
7910
8070
|
var init_sol_secret_mcp = __esm(() => {
|
|
7911
8071
|
init_mcp_tools();
|
|
7912
8072
|
init_identity_store();
|
|
8073
|
+
init_mcp_http();
|
|
7913
8074
|
if (false) {}
|
|
7914
8075
|
});
|
|
7915
8076
|
|
|
@@ -8020,85 +8181,6 @@ var init_workspace = __esm(() => {
|
|
|
8020
8181
|
init_async_repo();
|
|
8021
8182
|
});
|
|
8022
8183
|
|
|
8023
|
-
// src/bin/sol-mcp.ts
|
|
8024
|
-
var exports_sol_mcp = {};
|
|
8025
|
-
__export(exports_sol_mcp, {
|
|
8026
|
-
startWorkspaceMcp: () => startWorkspaceMcp
|
|
8027
|
-
});
|
|
8028
|
-
import { mkdirSync as mkdirSync10 } from "node:fs";
|
|
8029
|
-
import { join as join17 } from "node:path";
|
|
8030
|
-
async function handle(ws, name, a) {
|
|
8031
|
-
switch (name) {
|
|
8032
|
-
case "sol_write":
|
|
8033
|
-
await ws.write(a.path, a.content);
|
|
8034
|
-
return text(`wrote ${a.path} (${a.content.length} chars)`);
|
|
8035
|
-
case "sol_read": {
|
|
8036
|
-
const c = await ws.read(a.path);
|
|
8037
|
-
return c === undefined ? text(`(absent: ${a.path})`) : text(c);
|
|
8038
|
-
}
|
|
8039
|
-
case "sol_edit":
|
|
8040
|
-
await ws.edit(a.path, a.old_str, a.new_str, { all: a.all });
|
|
8041
|
-
return text(`edited ${a.path}`);
|
|
8042
|
-
case "sol_ls": {
|
|
8043
|
-
const f = await ws.ls(a.prefix);
|
|
8044
|
-
return text(f.length ? f.join(`
|
|
8045
|
-
`) : "(no files yet)");
|
|
8046
|
-
}
|
|
8047
|
-
case "sol_move":
|
|
8048
|
-
await ws.move(a.from, a.to);
|
|
8049
|
-
return text(`moved ${a.from} -> ${a.to}`);
|
|
8050
|
-
case "sol_rm":
|
|
8051
|
-
await ws.remove(a.path);
|
|
8052
|
-
return text(`removed ${a.path}`);
|
|
8053
|
-
case "sol_commit":
|
|
8054
|
-
case "sol_checkpoint": {
|
|
8055
|
-
const h = await ws.commit(a.message);
|
|
8056
|
-
return text(`commit ${h.slice(0, 14)} — ${a.message}`);
|
|
8057
|
-
}
|
|
8058
|
-
case "sol_history": {
|
|
8059
|
-
const ops = await ws.history();
|
|
8060
|
-
return text(ops.map((o) => `${o.seq} ${o.type} ${o.path || ""}${o.message ? " : " + o.message : ""}`).join(`
|
|
8061
|
-
`) || "(empty)");
|
|
8062
|
-
}
|
|
8063
|
-
default:
|
|
8064
|
-
throw new Error("unknown tool: " + name);
|
|
8065
|
-
}
|
|
8066
|
-
}
|
|
8067
|
-
async function startWorkspaceMcp(opts = {}) {
|
|
8068
|
-
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
8069
|
-
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8070
|
-
const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
8071
|
-
const solDir2 = opts.solDir || process.env.SOL_DIR || join17(process.cwd(), ".sol");
|
|
8072
|
-
mkdirSync10(solDir2, { recursive: true });
|
|
8073
|
-
const ws = new SolWorkspace(new FileStore(solDir2), new FileOpLog(solDir2), process.env.SOL_ACTOR || "agent");
|
|
8074
|
-
const server = new Server({ name: "sol", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
8075
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
8076
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
8077
|
-
try {
|
|
8078
|
-
return await handle(ws, req.params.name, req.params.arguments ?? {});
|
|
8079
|
-
} catch (e) {
|
|
8080
|
-
return { content: [{ type: "text", text: "sol error: " + (e?.message ?? e) }], isError: true };
|
|
8081
|
-
}
|
|
8082
|
-
});
|
|
8083
|
-
await server.connect(new StdioServerTransport);
|
|
8084
|
-
}
|
|
8085
|
-
var tools, text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
8086
|
-
var init_sol_mcp = __esm(() => {
|
|
8087
|
-
init_file_store();
|
|
8088
|
-
init_workspace();
|
|
8089
|
-
tools = [
|
|
8090
|
-
{ name: "sol_write", description: "Create or overwrite a text file in the sol workspace. Authoring goes here — not to a disk.", inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } },
|
|
8091
|
-
{ name: "sol_read", description: "Read a text file from the sol workspace.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
|
8092
|
-
{ name: "sol_edit", description: "Replace a unique snippet in a file (set all=true to replace every occurrence).", inputSchema: { type: "object", properties: { path: { type: "string" }, old_str: { type: "string" }, new_str: { type: "string" }, all: { type: "boolean" } }, required: ["path", "old_str", "new_str"] } },
|
|
8093
|
-
{ name: "sol_ls", description: "List tracked files, optionally scoped to a directory prefix.", inputSchema: { type: "object", properties: { prefix: { type: "string" } } } },
|
|
8094
|
-
{ name: "sol_move", description: "Move or rename a file.", inputSchema: { type: "object", properties: { from: { type: "string" }, to: { type: "string" } }, required: ["from", "to"] } },
|
|
8095
|
-
{ name: "sol_rm", description: "Delete (untrack) a file.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
|
8096
|
-
{ name: "sol_commit", description: "Record a commit (a shareable milestone) over the current state.", inputSchema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] } },
|
|
8097
|
-
{ name: "sol_history", description: "Show the authoring history (the op-log).", inputSchema: { type: "object", properties: {} } }
|
|
8098
|
-
];
|
|
8099
|
-
if (false) {}
|
|
8100
|
-
});
|
|
8101
|
-
|
|
8102
8184
|
// src/bin/dispatch.ts
|
|
8103
8185
|
var exports_dispatch = {};
|
|
8104
8186
|
__export(exports_dispatch, {
|
|
@@ -8106,9 +8188,9 @@ __export(exports_dispatch, {
|
|
|
8106
8188
|
dispatch: () => dispatch
|
|
8107
8189
|
});
|
|
8108
8190
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
8109
|
-
import { existsSync as existsSync17, mkdirSync as
|
|
8191
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync8, readFileSync as readFileSync17, rmSync as rmSync2, unlinkSync as unlinkSync7, watch, writeFileSync as writeFileSync15 } from "node:fs";
|
|
8110
8192
|
import { homedir as homedir2, hostname, platform as platform2, tmpdir } from "node:os";
|
|
8111
|
-
import { basename, dirname as dirname4, join as
|
|
8193
|
+
import { basename, dirname as dirname4, join as join17, resolve as resolve4, sep as sep2 } from "node:path";
|
|
8112
8194
|
function globCovers(pattern, path) {
|
|
8113
8195
|
let re = "";
|
|
8114
8196
|
for (let i = 0;i < pattern.length; i++) {
|
|
@@ -8182,6 +8264,24 @@ function authHost() {
|
|
|
8182
8264
|
} catch {}
|
|
8183
8265
|
return "https://auth.midsummer.new";
|
|
8184
8266
|
}
|
|
8267
|
+
async function resolveMcpHttp(a, label) {
|
|
8268
|
+
if (!a.includes("--http"))
|
|
8269
|
+
return;
|
|
8270
|
+
const flagVal = (name) => {
|
|
8271
|
+
const i = a.indexOf(name);
|
|
8272
|
+
return i >= 0 && i + 1 < a.length ? a[i + 1] : undefined;
|
|
8273
|
+
};
|
|
8274
|
+
const { resolveMcpToken: resolveMcpToken2 } = await Promise.resolve().then(() => (init_mcp_http(), exports_mcp_http));
|
|
8275
|
+
const token = resolveMcpToken2(flagVal("--token"));
|
|
8276
|
+
if (!token) {
|
|
8277
|
+
die("refusing to start an OPEN HTTP MCP — set a bearer token via SOL_MCP_TOKEN (or --token <T>). every request must send `Authorization: Bearer <token>`.");
|
|
8278
|
+
}
|
|
8279
|
+
const portRaw = flagVal("--port");
|
|
8280
|
+
const port = portRaw ? Number(portRaw) : undefined;
|
|
8281
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65535))
|
|
8282
|
+
die(`invalid --port: ${portRaw}`);
|
|
8283
|
+
return { token, port, host: flagVal("--host"), label: label ?? (a.includes("--secret") || a.includes("--secrets") ? "sol-secrets" : "sol") };
|
|
8284
|
+
}
|
|
8185
8285
|
function resolveRemote(solDir2) {
|
|
8186
8286
|
const cfg = loadRemote(solDir2);
|
|
8187
8287
|
if (!cfg)
|
|
@@ -8734,13 +8834,19 @@ SOL_RECOVERY_CODE (which only unlocks SIGNED gate edits, never a value op).
|
|
|
8734
8834
|
|
|
8735
8835
|
register:
|
|
8736
8836
|
claude mcp add sol-secrets -- sol secret mcp # the INSTALLED binary serves it (no separate bin on PATH)`,
|
|
8737
|
-
mcp: `sol mcp [--secret] serve a sol MCP server
|
|
8837
|
+
mcp: `sol mcp [--secret] [--http [--port N] [--host H] [--token T]] serve a sol MCP server
|
|
8738
8838
|
|
|
8739
8839
|
sol mcp the file/VCS AUTHORING server (sol_write/read/edit/ls/move/rm/commit/history) — an agent
|
|
8740
8840
|
authors directly into the op-log + content store, no working copy.
|
|
8741
8841
|
sol mcp --secret the AGENT-SAFE SECRET-MANAGER server (== \`sol secret mcp\`) — 14 value-free env/secret tools.
|
|
8742
8842
|
|
|
8743
|
-
|
|
8843
|
+
transport: stdio is the DEFAULT (local pipe). \`--http\` serves the SAME tool surface over the MCP Streamable HTTP
|
|
8844
|
+
transport — the enabler for hosting it on a remote. an HTTP MCP is a NETWORK endpoint, so it requires a bearer
|
|
8845
|
+
token: set SOL_MCP_TOKEN (or pass --token <T>) and every request must send \`Authorization: Bearer <token>\` (else
|
|
8846
|
+
401). it serves on http://127.0.0.1:8765/mcp by default; override with --host / --port.
|
|
8847
|
+
|
|
8848
|
+
register: \`claude mcp add sol -- sol mcp\` | \`claude mcp add sol-secrets -- sol mcp --secret\`
|
|
8849
|
+
http: \`SOL_MCP_TOKEN=… sol mcp --http --port 8765\` (then point an HTTP MCP client at http://HOST:PORT/mcp)`,
|
|
8744
8850
|
resolve: `sol resolve <sol://env/NAME[#field]> resolve a sol:// reference to its value (CLIENT-SIDE, recipient-gated)
|
|
8745
8851
|
|
|
8746
8852
|
sol resolve sol://production/STRIPE_KEY
|
|
@@ -8781,7 +8887,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
8781
8887
|
try {
|
|
8782
8888
|
switch (cmd) {
|
|
8783
8889
|
case "init": {
|
|
8784
|
-
const here =
|
|
8890
|
+
const here = join17(procCwd, ".sol");
|
|
8785
8891
|
if (existsSync17(here))
|
|
8786
8892
|
die("already a sol repo: " + procCwd);
|
|
8787
8893
|
if (repoRoot && repoRoot !== procCwd && !args.includes("--force")) {
|
|
@@ -8789,7 +8895,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
8789
8895
|
-> just commit into it: \`sol commit ...\` works from here (sol walks up to find the repo)
|
|
8790
8896
|
-> to nest a NEW repo here anyway: \`sol init --force\``);
|
|
8791
8897
|
}
|
|
8792
|
-
|
|
8898
|
+
mkdirSync10(here, { recursive: true });
|
|
8793
8899
|
new FileStore(here);
|
|
8794
8900
|
console.log(`initialized empty sol repo in ${here}`);
|
|
8795
8901
|
break;
|
|
@@ -8994,7 +9100,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
8994
9100
|
let n = 0;
|
|
8995
9101
|
for (const f of files) {
|
|
8996
9102
|
const rf = repoRel(f);
|
|
8997
|
-
if (!existsSync17(
|
|
9103
|
+
if (!existsSync17(join17(cwd, rf))) {
|
|
8998
9104
|
console.error("skip (not on disk): " + f);
|
|
8999
9105
|
continue;
|
|
9000
9106
|
}
|
|
@@ -9023,14 +9129,14 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
9023
9129
|
if (!message)
|
|
9024
9130
|
die('commit needs a message: sol commit "what you did" (scoped: sol commit -m "msg" file1 file2)');
|
|
9025
9131
|
const parentHead = await repo.head();
|
|
9026
|
-
const mergeHeadPath =
|
|
9132
|
+
const mergeHeadPath = join17(solDir, "MERGE_HEAD");
|
|
9027
9133
|
const parent2 = existsSync17(mergeHeadPath) ? readFileSync17(mergeHeadPath, "utf8").trim() || undefined : undefined;
|
|
9028
9134
|
let changed = 0;
|
|
9029
9135
|
let commitRoot = parentHead;
|
|
9030
9136
|
if (paths.length) {
|
|
9031
9137
|
for (const p of paths) {
|
|
9032
9138
|
const rp = repoRel(p);
|
|
9033
|
-
if (existsSync17(
|
|
9139
|
+
if (existsSync17(join17(cwd, rp))) {
|
|
9034
9140
|
if (await snapshotFile(repo, rp))
|
|
9035
9141
|
changed++;
|
|
9036
9142
|
} else if ((await repo.list()).includes(rp)) {
|
|
@@ -9375,7 +9481,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
9375
9481
|
const path = args[0] || die("rm needs a path");
|
|
9376
9482
|
let onDisk = false;
|
|
9377
9483
|
try {
|
|
9378
|
-
unlinkSync7(
|
|
9484
|
+
unlinkSync7(join17(cwd, path));
|
|
9379
9485
|
onDisk = true;
|
|
9380
9486
|
} catch {}
|
|
9381
9487
|
if (onDisk) {
|
|
@@ -9603,16 +9709,16 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
9603
9709
|
if (op.prov)
|
|
9604
9710
|
walk(op.prov);
|
|
9605
9711
|
}
|
|
9606
|
-
const objDir =
|
|
9712
|
+
const objDir = join17(solDir, "objects");
|
|
9607
9713
|
let removed = 0;
|
|
9608
9714
|
for (const name of readdirSync8(objDir)) {
|
|
9609
9715
|
if (name.endsWith(".tmp") || !reachable.has(name)) {
|
|
9610
|
-
unlinkSync7(
|
|
9716
|
+
unlinkSync7(join17(objDir, name));
|
|
9611
9717
|
removed++;
|
|
9612
9718
|
}
|
|
9613
9719
|
}
|
|
9614
9720
|
console.log(`gc: kept ${reachable.size} object(s), removed ${removed} unreachable`);
|
|
9615
|
-
if (existsSync17(
|
|
9721
|
+
if (existsSync17(join17(solDir, "env", "seal"))) {
|
|
9616
9722
|
const { gcStaleStanzas: gcStaleStanzas2 } = await Promise.resolve().then(() => (init_secret(), exports_secret));
|
|
9617
9723
|
const st = gcStaleStanzas2(solDir);
|
|
9618
9724
|
if (st.removed)
|
|
@@ -9627,7 +9733,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
9627
9733
|
console.log(p);
|
|
9628
9734
|
break;
|
|
9629
9735
|
}
|
|
9630
|
-
const f =
|
|
9736
|
+
const f = join17(cwd, ".solignore");
|
|
9631
9737
|
const lead = existsSync17(f) && !readFileSync17(f, "utf8").endsWith(`
|
|
9632
9738
|
`) ? `
|
|
9633
9739
|
` : "";
|
|
@@ -9744,10 +9850,10 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
9744
9850
|
let removed = 0;
|
|
9745
9851
|
{
|
|
9746
9852
|
const res = await scrubHistory2(solDir, ops, targets);
|
|
9747
|
-
writeFileSync15(
|
|
9853
|
+
writeFileSync15(join17(solDir, "ops.jsonl"), res.ops.map((o) => JSON.stringify(o)).join(`
|
|
9748
9854
|
`) + (res.ops.length ? `
|
|
9749
9855
|
` : ""));
|
|
9750
|
-
writeFileSync15(
|
|
9856
|
+
writeFileSync15(join17(solDir, "HEAD"), JSON.stringify({ head: res.newHead, seq: res.newSeq, logTip: res.newLogTip }));
|
|
9751
9857
|
if (existsSync17(refsPath())) {
|
|
9752
9858
|
const refs = JSON.parse(readFileSync17(refsPath(), "utf8"));
|
|
9753
9859
|
for (const b of Object.values(refs.branches)) {
|
|
@@ -9907,7 +10013,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
9907
10013
|
if (content === SEALED && !args.includes("--hide-names"))
|
|
9908
10014
|
die("already sealed: " + path);
|
|
9909
10015
|
if (content === undefined) {
|
|
9910
|
-
const abs =
|
|
10016
|
+
const abs = join17(cwd, path);
|
|
9911
10017
|
if (!existsSync17(abs))
|
|
9912
10018
|
die("no such file: " + path);
|
|
9913
10019
|
content = readFileSync17(abs, "utf8");
|
|
@@ -9998,6 +10104,21 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
|
|
|
9998
10104
|
audienceAccounts.push({ accountId: spec.accountId, fingerprint: resolved.fingerprint, keyEpoch: resolved.entry.keyEpoch });
|
|
9999
10105
|
crossAccount.push(spec.accountId);
|
|
10000
10106
|
}
|
|
10107
|
+
const bareSelfSeal = !rawRecipients.length && !policyApplied && !Object.keys(recipientPubKeys).length && !escrowSlots.length;
|
|
10108
|
+
if (bareSelfSeal) {
|
|
10109
|
+
const { loadIdentity: loadIdentity2 } = await Promise.resolve().then(() => (init_identity_store(), exports_identity_store));
|
|
10110
|
+
const stored = loadIdentity2();
|
|
10111
|
+
if (stored) {
|
|
10112
|
+
const { pubFingerprint: pubFingerprint2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
10113
|
+
recipientPubKeys[stored.accountId] = stored.x25519Pub;
|
|
10114
|
+
audienceAccounts.push({ accountId: stored.accountId, fingerprint: pubFingerprint2(stored.x25519Pub), keyEpoch: stored.keyEpoch });
|
|
10115
|
+
localRecipients.add(actor);
|
|
10116
|
+
} else if (!wantJson) {
|
|
10117
|
+
console.error(`WARNING: no identity (~/.sol/identity.json) — sealing "${path}" with a LOCAL-ONLY symmetric key.`);
|
|
10118
|
+
console.error(` this box will NOT decrypt on another machine even with your recovery code (the key lives only in this repo's .sol keyring).`);
|
|
10119
|
+
console.error(` run \`sol keys init\` first for a PORTABLE self-seal (decryptable with your recovery code on any of your devices).`);
|
|
10120
|
+
}
|
|
10121
|
+
}
|
|
10001
10122
|
const client = new SealedClient2(repo, ring);
|
|
10002
10123
|
const wantHideName = args.includes("--hide-names") || hideNamesFromPolicy;
|
|
10003
10124
|
const wantHideExistence = args.includes("--hide-existence") || hideExistenceFromPolicy;
|
|
@@ -10128,6 +10249,8 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10128
10249
|
break;
|
|
10129
10250
|
}
|
|
10130
10251
|
console.log(`sealed ${path} to ${recipients.join(", ")} — content is now host-blind ciphertext (the keystore stays local)`);
|
|
10252
|
+
if (bareSelfSeal && audienceAccounts.length)
|
|
10253
|
+
console.log(` PORTABLE self-seal: also wrapped to your own identity (@${audienceAccounts[0].accountId}) — it decrypts with your recovery code on any of your machines.`);
|
|
10131
10254
|
if (hiddenSlot)
|
|
10132
10255
|
console.log(` name hidden (L2): the host now sees an opaque slot \x00slot\x00${hiddenSlot}, never "${path.split("/").pop()}" — recipients rebuild the real name locally.`);
|
|
10133
10256
|
if (policyApplied)
|
|
@@ -10398,7 +10521,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10398
10521
|
const result = merge2({ store: store2 }, other.base, ours, other.head);
|
|
10399
10522
|
if (result.conflicts.length) {
|
|
10400
10523
|
materializeTree(store2, result.head);
|
|
10401
|
-
writeFileSync15(
|
|
10524
|
+
writeFileSync15(join17(solDir, "MERGE_HEAD"), other.head);
|
|
10402
10525
|
saveMergeConflicts(solDir, result.conflicts);
|
|
10403
10526
|
console.log(`merge ${name} -> ${result.conflicts.length} conflict(s), left in your working tree (uncommitted):`);
|
|
10404
10527
|
for (const c of result.conflicts)
|
|
@@ -10545,7 +10668,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10545
10668
|
}
|
|
10546
10669
|
if (!tokens)
|
|
10547
10670
|
die("timed out waiting for approval");
|
|
10548
|
-
|
|
10671
|
+
mkdirSync10(dirname4(CRED_PATH), { recursive: true });
|
|
10549
10672
|
writeFileSync15(CRED_PATH, JSON.stringify({ webUrl, ...tokens }, null, 2), { mode: 384 });
|
|
10550
10673
|
const c = tokenClaims(tokens.accessToken);
|
|
10551
10674
|
console.log(`
|
|
@@ -10589,7 +10712,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10589
10712
|
console.log(id.handle ? `signed in as @${id.handle} ${id.email ?? ""}`.trimEnd() : `signed in${id.email ? ` as ${id.email}` : ""} — no handle yet (\`sol auth set-handle <name>\`)`);
|
|
10590
10713
|
} else if (sub === "set-handle") {
|
|
10591
10714
|
const want = args[1] || die("usage: sol auth set-handle <name>");
|
|
10592
|
-
const
|
|
10715
|
+
const handle = want.toLowerCase();
|
|
10593
10716
|
const token = process.env.SOL_TOKEN || await loadStoredToken();
|
|
10594
10717
|
if (!token)
|
|
10595
10718
|
authExpired();
|
|
@@ -10599,7 +10722,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10599
10722
|
if (meRes.ok)
|
|
10600
10723
|
hadHandle = Boolean((await meRes.json()).handle);
|
|
10601
10724
|
} catch {}
|
|
10602
|
-
const res = await fetch(`${authHost()}/api/auth/handle`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ handle
|
|
10725
|
+
const res = await fetch(`${authHost()}/api/auth/handle`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ handle }) });
|
|
10603
10726
|
if (res.status === 401)
|
|
10604
10727
|
authExpired();
|
|
10605
10728
|
if (!res.ok) {
|
|
@@ -10677,10 +10800,10 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10677
10800
|
const peer = openPeer2(localSrc);
|
|
10678
10801
|
const peerHead = await peer.log.head();
|
|
10679
10802
|
const dest = resolve4(procCwd, args[1] || (args[0].replace(/\/+$/, "").split("/").pop() || "clone") + "-clone");
|
|
10680
|
-
const ddir =
|
|
10803
|
+
const ddir = join17(dest, ".sol");
|
|
10681
10804
|
if (existsSync17(ddir))
|
|
10682
10805
|
die("already a sol repo: " + dest);
|
|
10683
|
-
|
|
10806
|
+
mkdirSync10(ddir, { recursive: true });
|
|
10684
10807
|
const peerOps = await peer.log.history();
|
|
10685
10808
|
const res = await converge2({ store: new FileStore(ddir), log: new FileOpLog(ddir) }, { nodes: await peerNodes2(peer, peerHead, peerOps), ops: peerOps, incomingHead: peerHead, actor });
|
|
10686
10809
|
const dstore = new Store;
|
|
@@ -10690,7 +10813,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10690
10813
|
const files = (res.head ? listAll(dstore, res.head) : []).filter((f) => !f.split("/").some(isReservedKey2));
|
|
10691
10814
|
for (const f of files)
|
|
10692
10815
|
materializeInto(dstore, res.head, dest, f);
|
|
10693
|
-
writeFileSync15(
|
|
10816
|
+
writeFileSync15(join17(ddir, "refs.json"), JSON.stringify({ current: "main", branches: { main: { head: res.head, base: res.head, remote: res.head } }, tags: {} }, null, 2));
|
|
10694
10817
|
writeWorkingIndexAt(ddir, dest, files);
|
|
10695
10818
|
console.log(`cloned local peer ${args[0]} -> ${dest} (${(await peer.log.history()).length} ops, ${files.length} files)`);
|
|
10696
10819
|
break;
|
|
@@ -10699,12 +10822,12 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10699
10822
|
const repoName = rest[0] || die("usage: sol clone [<url>] <owner>/<repo> [dir]");
|
|
10700
10823
|
const token = process.env.SOL_TOKEN || die("set SOL_TOKEN to the backend bearer token");
|
|
10701
10824
|
const target = resolve4(cwd, rest[1] || repoName.split("/").pop() || repoName);
|
|
10702
|
-
const fdir =
|
|
10825
|
+
const fdir = join17(target, ".sol");
|
|
10703
10826
|
if (existsSync17(fdir))
|
|
10704
10827
|
die("already a sol repo: " + target);
|
|
10705
10828
|
const cfg = { url, repo: repoName };
|
|
10706
10829
|
const bundle = await remoteExport(cfg, token);
|
|
10707
|
-
|
|
10830
|
+
mkdirSync10(fdir, { recursive: true });
|
|
10708
10831
|
await writeBundle(fdir, bundle, 0);
|
|
10709
10832
|
saveRemote(fdir, cfg);
|
|
10710
10833
|
await pullEnvState(fdir, cfg, token);
|
|
@@ -10717,8 +10840,8 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10717
10840
|
cloneBranches[name] = { head: h, base: h, remote: h };
|
|
10718
10841
|
if (!cloneBranches[onBranch])
|
|
10719
10842
|
cloneBranches[onBranch] = { head: checkoutHead, base: checkoutHead, remote: checkoutHead };
|
|
10720
|
-
writeFileSync15(
|
|
10721
|
-
writeFileSync15(
|
|
10843
|
+
writeFileSync15(join17(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
|
|
10844
|
+
writeFileSync15(join17(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
|
|
10722
10845
|
const store2 = new Store;
|
|
10723
10846
|
for (const node of bundle.nodes)
|
|
10724
10847
|
store2.put(node);
|
|
@@ -10974,9 +11097,9 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
10974
11097
|
for (const c of result.conflicts) {
|
|
10975
11098
|
const blob = fileAt(store2, result.head, c.path);
|
|
10976
11099
|
if (blob)
|
|
10977
|
-
writeFileSync15(
|
|
11100
|
+
writeFileSync15(join17(cwd, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
|
|
10978
11101
|
}
|
|
10979
|
-
writeFileSync15(
|
|
11102
|
+
writeFileSync15(join17(solDir, "MERGE_HEAD"), remoteHead2);
|
|
10980
11103
|
saveMergeConflicts(solDir, result.conflicts);
|
|
10981
11104
|
console.log(`pulled + merged WITH ${result.conflicts.length} conflict(s), left uncommitted in your working tree:`);
|
|
10982
11105
|
for (const c of result.conflicts)
|
|
@@ -11007,7 +11130,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
11007
11130
|
const newRepo = frest[1] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
|
|
11008
11131
|
const token = process.env.SOL_TOKEN || authExpired();
|
|
11009
11132
|
const target = resolve4(cwd, frest[2] || newRepo);
|
|
11010
|
-
const fdir =
|
|
11133
|
+
const fdir = join17(target, ".sol");
|
|
11011
11134
|
if (existsSync17(fdir))
|
|
11012
11135
|
die("already a sol repo: " + target);
|
|
11013
11136
|
const parentCfg = { url, repo: parent };
|
|
@@ -11024,7 +11147,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
11024
11147
|
}
|
|
11025
11148
|
await forkMeta(newCfg, token, parent);
|
|
11026
11149
|
const canon = await remoteExport(newCfg, token);
|
|
11027
|
-
|
|
11150
|
+
mkdirSync10(fdir, { recursive: true });
|
|
11028
11151
|
await writeBundle(fdir, canon, 0);
|
|
11029
11152
|
const srvRefs = canon.refs ?? { branches: { main: canon.head ?? "" }, production: "main" };
|
|
11030
11153
|
const checkout = canon.checkout ?? { branch: srvRefs.production || "main", head: srvRefs.branches[srvRefs.production] ?? canon.head };
|
|
@@ -11033,8 +11156,8 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
|
|
|
11033
11156
|
const cloneBranches = {};
|
|
11034
11157
|
for (const [name, h] of Object.entries(srvRefs.branches))
|
|
11035
11158
|
cloneBranches[name] = { head: h, base: h, remote: h };
|
|
11036
|
-
writeFileSync15(
|
|
11037
|
-
writeFileSync15(
|
|
11159
|
+
writeFileSync15(join17(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
|
|
11160
|
+
writeFileSync15(join17(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
|
|
11038
11161
|
saveRemote(fdir, newCfg);
|
|
11039
11162
|
const store2 = new Store;
|
|
11040
11163
|
for (const node of canon.nodes)
|
|
@@ -11299,7 +11422,7 @@ ${mrSummary2(pr)}`);
|
|
|
11299
11422
|
die("usage: sol run [--keep <path>] [--isolate] <command...>");
|
|
11300
11423
|
const { capture: capture3, hydrate: hydrate3, isolateCommand: isolateCommand2 } = await Promise.resolve().then(() => (init_runtime(), exports_runtime));
|
|
11301
11424
|
const head = await repo.head();
|
|
11302
|
-
const dir = mkdtempSync(
|
|
11425
|
+
const dir = mkdtempSync(join17(tmpdir(), "sol-run-"));
|
|
11303
11426
|
try {
|
|
11304
11427
|
const hn = hydrate3(loadStore(), head, dir);
|
|
11305
11428
|
console.log(`hydrated ${hn} file(s) -> sandbox${isolate ? " (isolated: no network, writes confined to the sandbox)" : ""}`);
|
|
@@ -11332,7 +11455,7 @@ ${mrSummary2(pr)}`);
|
|
|
11332
11455
|
materialize(synced, nh, f);
|
|
11333
11456
|
for (const f of deleted) {
|
|
11334
11457
|
try {
|
|
11335
|
-
unlinkSync7(
|
|
11458
|
+
unlinkSync7(join17(cwd, f));
|
|
11336
11459
|
} catch {}
|
|
11337
11460
|
}
|
|
11338
11461
|
console.log(`captured ${written.length} written, ${deleted.length} deleted file(s):`);
|
|
@@ -11354,10 +11477,10 @@ ${mrSummary2(pr)}`);
|
|
|
11354
11477
|
if (sub === "import") {
|
|
11355
11478
|
const gitPath = resolve4(cwd, args[1] || die("usage: sol git import <git-repo> [dir]"));
|
|
11356
11479
|
const target = resolve4(cwd, args[2] || basename(gitPath));
|
|
11357
|
-
const fdir =
|
|
11480
|
+
const fdir = join17(target, ".sol");
|
|
11358
11481
|
if (existsSync17(fdir))
|
|
11359
11482
|
die("already a sol repo: " + target);
|
|
11360
|
-
|
|
11483
|
+
mkdirSync10(fdir, { recursive: true });
|
|
11361
11484
|
const { commits, branches, head, current } = await importGitRepo2(gitPath, fdir);
|
|
11362
11485
|
const refsBranches = {};
|
|
11363
11486
|
for (const b of branches)
|
|
@@ -11365,13 +11488,13 @@ ${mrSummary2(pr)}`);
|
|
|
11365
11488
|
if (!refsBranches[current])
|
|
11366
11489
|
refsBranches[current] = { head, base: head };
|
|
11367
11490
|
refsBranches[current].head = head;
|
|
11368
|
-
writeFileSync15(
|
|
11491
|
+
writeFileSync15(join17(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
|
|
11369
11492
|
const store2 = new Store;
|
|
11370
|
-
for (const name of readdirSync8(
|
|
11493
|
+
for (const name of readdirSync8(join17(fdir, "objects"))) {
|
|
11371
11494
|
if (name.endsWith(".tmp"))
|
|
11372
11495
|
continue;
|
|
11373
11496
|
try {
|
|
11374
|
-
store2.put(decodeObject(readFileSync17(
|
|
11497
|
+
store2.put(decodeObject(readFileSync17(join17(fdir, "objects", name))));
|
|
11375
11498
|
} catch {}
|
|
11376
11499
|
}
|
|
11377
11500
|
const onDisk = hydrate3(store2, head, target);
|
|
@@ -11403,9 +11526,9 @@ ${mrSummary2(pr)}`);
|
|
|
11403
11526
|
die("already inside a view — create views from the parent repo (its `.sol` owns the shared store + op-log).");
|
|
11404
11527
|
const name = args.find((a) => !a.startsWith("-")) || die("usage: sol view <name> [dir]");
|
|
11405
11528
|
const rest = args.filter((a) => !a.startsWith("-"));
|
|
11406
|
-
const defaultDir =
|
|
11529
|
+
const defaultDir = join17(dirname4(cwd), `${basename(cwd)}-${name}`);
|
|
11407
11530
|
const viewDir = rest[1] ? resolve4(procCwd, rest[1]) : defaultDir;
|
|
11408
|
-
if (existsSync17(
|
|
11531
|
+
if (existsSync17(join17(viewDir, ".sol")))
|
|
11409
11532
|
die("already a sol repo/view: " + viewDir);
|
|
11410
11533
|
const branch = `view/${name}`;
|
|
11411
11534
|
const startHead = await log.head() ?? emptyRoot(loadStore());
|
|
@@ -11496,12 +11619,13 @@ ${mrSummary2(pr)}`);
|
|
|
11496
11619
|
break;
|
|
11497
11620
|
}
|
|
11498
11621
|
case "mcp": {
|
|
11622
|
+
const http = await resolveMcpHttp(args);
|
|
11499
11623
|
if (args.includes("--secret") || args.includes("--secrets")) {
|
|
11500
11624
|
const { startSecretMcp: startSecretMcp2 } = await Promise.resolve().then(() => (init_sol_secret_mcp(), exports_sol_secret_mcp));
|
|
11501
|
-
await startSecretMcp2({ solDir });
|
|
11625
|
+
await startSecretMcp2({ solDir, http });
|
|
11502
11626
|
} else {
|
|
11503
|
-
const { startWorkspaceMcp
|
|
11504
|
-
await
|
|
11627
|
+
const { startWorkspaceMcp } = await Promise.resolve().then(() => (init_sol_mcp(), exports_sol_mcp));
|
|
11628
|
+
await startWorkspaceMcp({ solDir, http });
|
|
11505
11629
|
}
|
|
11506
11630
|
break;
|
|
11507
11631
|
}
|
|
@@ -11509,8 +11633,9 @@ ${mrSummary2(pr)}`);
|
|
|
11509
11633
|
case "secret":
|
|
11510
11634
|
case "resolve": {
|
|
11511
11635
|
if (cmd === "secret" && args[0] === "mcp") {
|
|
11636
|
+
const http = await resolveMcpHttp(args.slice(1), "sol-secrets");
|
|
11512
11637
|
const { startSecretMcp: startSecretMcp2 } = await Promise.resolve().then(() => (init_sol_secret_mcp(), exports_sol_secret_mcp));
|
|
11513
|
-
await startSecretMcp2({ solDir });
|
|
11638
|
+
await startSecretMcp2({ solDir, http });
|
|
11514
11639
|
break;
|
|
11515
11640
|
}
|
|
11516
11641
|
if (!existsSync17(solDir))
|
|
@@ -11686,29 +11811,3939 @@ var init_dispatch = __esm(() => {
|
|
|
11686
11811
|
init_remote();
|
|
11687
11812
|
init_lib();
|
|
11688
11813
|
init_test_gate();
|
|
11689
|
-
CRED_PATH =
|
|
11814
|
+
CRED_PATH = join17(homedir2(), ".sol", "credentials");
|
|
11690
11815
|
DEFAULT_REMOTE_URL = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
|
|
11691
11816
|
ATTEST_KEY = process.env.SOL_ATTEST_KEY || undefined;
|
|
11692
11817
|
});
|
|
11693
11818
|
|
|
11694
|
-
// src/bin/
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11819
|
+
// src/bin/mcp-vcs-tools.ts
|
|
11820
|
+
function sanitizeEgress2(text) {
|
|
11821
|
+
const saved = [];
|
|
11822
|
+
const protectedText = text.replace(SOL_ID_RE, (m) => {
|
|
11823
|
+
const token = `[[solid:${saved.length}]]`;
|
|
11824
|
+
saved.push(m);
|
|
11825
|
+
return token;
|
|
11826
|
+
});
|
|
11827
|
+
const cleaned = sanitizer2.sanitize(protectedText).sanitized;
|
|
11828
|
+
return cleaned.replace(/\[\[solid:(\d+)\]\]/g, (_, i) => saved[Number(i)] ?? "");
|
|
11829
|
+
}
|
|
11830
|
+
async function runSol(argv) {
|
|
11831
|
+
const { runCli: runCli2 } = await Promise.resolve().then(() => (init_dispatch(), exports_dispatch));
|
|
11832
|
+
const lines2 = [];
|
|
11833
|
+
const origLog = console.log;
|
|
11834
|
+
const origErr = console.error;
|
|
11835
|
+
const origExit = process.exit;
|
|
11836
|
+
process.exitCode = 0;
|
|
11837
|
+
console.log = (...a) => void lines2.push(a.map((x) => typeof x === "string" ? x : String(x)).join(" "));
|
|
11838
|
+
console.error = (...a) => void lines2.push(a.map((x) => typeof x === "string" ? x : String(x)).join(" "));
|
|
11839
|
+
process.exit = (code) => {
|
|
11840
|
+
throw new CliExit(code ?? 0);
|
|
11841
|
+
};
|
|
11699
11842
|
try {
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
return
|
|
11843
|
+
await runCli2(argv);
|
|
11844
|
+
const code = typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
11845
|
+
return { out: lines2.join(`
|
|
11846
|
+
`), exitCode: code };
|
|
11847
|
+
} catch (e) {
|
|
11848
|
+
if (e instanceof CliExit)
|
|
11849
|
+
return { out: lines2.join(`
|
|
11850
|
+
`), exitCode: e.code || 1 };
|
|
11851
|
+
throw e;
|
|
11852
|
+
} finally {
|
|
11853
|
+
console.log = origLog;
|
|
11854
|
+
console.error = origErr;
|
|
11855
|
+
process.exit = origExit;
|
|
11856
|
+
process.exitCode = 0;
|
|
11703
11857
|
}
|
|
11704
11858
|
}
|
|
11705
|
-
|
|
11706
|
-
const
|
|
11707
|
-
|
|
11708
|
-
|
|
11859
|
+
function buildArgv(name, a) {
|
|
11860
|
+
const str = (k) => typeof a[k] === "string" && a[k] !== "" ? a[k] : undefined;
|
|
11861
|
+
const flag2 = (k) => a[k] === true || a[k] === "true";
|
|
11862
|
+
switch (name) {
|
|
11863
|
+
case "sol_status":
|
|
11864
|
+
return ["status", "--json"];
|
|
11865
|
+
case "sol_log":
|
|
11866
|
+
return flag2("all") ? ["log", "--all", "--json"] : ["log", "--json"];
|
|
11867
|
+
case "sol_diff": {
|
|
11868
|
+
const v = ["diff"];
|
|
11869
|
+
if (str("a"))
|
|
11870
|
+
v.push(str("a"));
|
|
11871
|
+
if (str("b"))
|
|
11872
|
+
v.push(str("b"));
|
|
11873
|
+
v.push("--json");
|
|
11874
|
+
return v;
|
|
11875
|
+
}
|
|
11876
|
+
case "sol_blame":
|
|
11877
|
+
return ["blame", str("path"), "--json"];
|
|
11878
|
+
case "sol_branch": {
|
|
11879
|
+
if (!str("name"))
|
|
11880
|
+
return ["branches"];
|
|
11881
|
+
const v = ["branch", str("name")];
|
|
11882
|
+
if (str("at"))
|
|
11883
|
+
v.push(str("at"));
|
|
11884
|
+
return v;
|
|
11885
|
+
}
|
|
11886
|
+
case "sol_checkout":
|
|
11887
|
+
return ["switch", str("branch")];
|
|
11888
|
+
case "sol_merge":
|
|
11889
|
+
return ["merge", str("branch"), "--json"];
|
|
11890
|
+
case "sol_view": {
|
|
11891
|
+
const v = ["view", str("name")];
|
|
11892
|
+
if (str("dir"))
|
|
11893
|
+
v.push(str("dir"));
|
|
11894
|
+
return v;
|
|
11895
|
+
}
|
|
11896
|
+
case "sol_views":
|
|
11897
|
+
return ["views", "--json"];
|
|
11898
|
+
case "sol_pull":
|
|
11899
|
+
return str("from") ? ["pull", str("from")] : ["pull"];
|
|
11900
|
+
case "sol_push": {
|
|
11901
|
+
const v = ["push"];
|
|
11902
|
+
if (flag2("create"))
|
|
11903
|
+
v.push("--create");
|
|
11904
|
+
if (flag2("public"))
|
|
11905
|
+
v.push("--public");
|
|
11906
|
+
if (str("repo"))
|
|
11907
|
+
v.push(str("repo"));
|
|
11908
|
+
v.push("--json");
|
|
11909
|
+
return v;
|
|
11910
|
+
}
|
|
11911
|
+
case "sol_clone": {
|
|
11912
|
+
const v = ["clone", str("repo")];
|
|
11913
|
+
if (str("dir"))
|
|
11914
|
+
v.push(str("dir"));
|
|
11915
|
+
return v;
|
|
11916
|
+
}
|
|
11917
|
+
case "sol_hide": {
|
|
11918
|
+
const v = ["hide", str("pattern")];
|
|
11919
|
+
if (str("role"))
|
|
11920
|
+
v.push("--role", str("role"));
|
|
11921
|
+
if (str("team"))
|
|
11922
|
+
v.push("--team", str("team"));
|
|
11923
|
+
if (str("users"))
|
|
11924
|
+
v.push("--users", str("users"));
|
|
11925
|
+
if (flag2("hide_names"))
|
|
11926
|
+
v.push("--hide-names");
|
|
11927
|
+
if (flag2("hide_existence"))
|
|
11928
|
+
v.push("--hide-existence");
|
|
11929
|
+
v.push("--json");
|
|
11930
|
+
return v;
|
|
11931
|
+
}
|
|
11932
|
+
case "sol_seal": {
|
|
11933
|
+
const v = ["seal"];
|
|
11934
|
+
if (str("path"))
|
|
11935
|
+
v.push(str("path"));
|
|
11936
|
+
if (flag2("reapply"))
|
|
11937
|
+
v.push("--reapply");
|
|
11938
|
+
v.push("--json");
|
|
11939
|
+
return v;
|
|
11940
|
+
}
|
|
11941
|
+
case "sol_sealed":
|
|
11942
|
+
return ["sealed", "--json"];
|
|
11943
|
+
default:
|
|
11944
|
+
throw new Error("unknown VCS tool: " + name);
|
|
11945
|
+
}
|
|
11946
|
+
}
|
|
11947
|
+
async function callVcsTool(name, args = {}) {
|
|
11948
|
+
let argv;
|
|
11949
|
+
try {
|
|
11950
|
+
argv = buildArgv(name, args);
|
|
11951
|
+
} catch (e) {
|
|
11952
|
+
return { text: sanitizeEgress2("sol: " + (e instanceof Error ? e.message : String(e))), isError: true };
|
|
11953
|
+
}
|
|
11954
|
+
try {
|
|
11955
|
+
const { out, exitCode } = await runSol(argv);
|
|
11956
|
+
return { text: sanitizeEgress2(out || "(ok)"), isError: exitCode !== 0 };
|
|
11957
|
+
} catch (e) {
|
|
11958
|
+
return { text: sanitizeEgress2("sol: " + (e instanceof Error ? e.message : String(e))), isError: true };
|
|
11959
|
+
}
|
|
11960
|
+
}
|
|
11961
|
+
var VCS_TOOLS, sanitizer2, SOL_ID_RE, CliExit;
|
|
11962
|
+
var init_mcp_vcs_tools = __esm(() => {
|
|
11963
|
+
init_sanitizer();
|
|
11964
|
+
VCS_TOOLS = [
|
|
11965
|
+
{ name: "sol_status", description: "Working-tree status as JSON: branch, head, the explicit state enum (CLEAN|DIRTY|CONFLICTED|SEMANTIC|EMPTY), per-path changes, and conflicts. The agent keys on `state` directly — no text scraping.", inputSchema: { type: "object", properties: {} } },
|
|
11966
|
+
{ name: "sol_log", description: "Commit history. Branch-scoped by default; set all=true for the AUTHORITATIVE full op-log (every write/seal/checkpoint/undo). Structured JSON with seq, id, author, message, root, parents, provenance.", inputSchema: { type: "object", properties: { all: { type: "boolean", description: "true = the full op-log lineage, not just the branch DAG" } } } },
|
|
11967
|
+
{ name: "sol_diff", description: "Diff as JSON. No args = working tree vs HEAD. One ref = working tree vs that ref. Two refs = tree vs tree. One path = that file vs HEAD. Host-blind: a non-recipient gets sealed/hidden rows, never decrypted content.", inputSchema: { type: "object", properties: { a: { type: "string", description: "a ref (branch/commit) or a path" }, b: { type: "string", description: "a second ref for a tree-vs-tree diff" } } } },
|
|
11968
|
+
{ name: "sol_blame", description: "Per-line authorship for a file as JSON: who authored each line, at which commit, when. Provenance-bound (trusted/forged detection).", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
|
11969
|
+
{ name: "sol_branch", description: "List branches (no name) or create one. `name` creates a branch; optional `at` starts it at a branch/tag/commit (default: current head).", inputSchema: { type: "object", properties: { name: { type: "string", description: "omit to list; provide to create" }, at: { type: "string", description: "optional start ref (branch/tag/commit hash)" } }, required: [] } },
|
|
11970
|
+
{ name: "sol_checkout", description: "Switch the working tree to a branch (the `sol switch` verb). Refuses on a dirty tree — commit first. Use sol_branch to create a branch before checking it out.", inputSchema: { type: "object", properties: { branch: { type: "string" } }, required: ["branch"] } },
|
|
11971
|
+
{ name: "sol_merge", description: "Merge another branch into the current one (lossless converge). A clean merge advances + commits; line conflicts land in the working tree with markers (resolve, then sol_commit). Reports a SEMANTIC flag if the post-merge check fails.", inputSchema: { type: "object", properties: { branch: { type: "string" } }, required: ["branch"] } },
|
|
11972
|
+
{ name: "sol_view", description: "Create a CLONE-FREE agent view: a new working dir with its own branch + index that SHARES this repo's content store (zero object duplication). The agent edits + commits there, then converges back with sol_pull. `dir` is optional (defaults to a sibling).", inputSchema: { type: "object", properties: { name: { type: "string" }, dir: { type: "string", description: "optional target dir (default: a sibling <repo>-<name>)" } }, required: ["name"] } },
|
|
11973
|
+
{ name: "sol_views", description: "Enumerate every view of this repo as JSON: name, dir, branch, head, author, active/stale, and the shared-object count (one store on disk).", inputSchema: { type: "object", properties: {} } },
|
|
11974
|
+
{ name: "sol_pull", description: "Converge from the configured remote, OR from a local peer/view on disk when `from` is a directory path (offline multi-agent merge). Refuses over a dirty tree or unresolved conflicts. Conflicts land in the working tree with markers.", inputSchema: { type: "object", properties: { from: { type: "string", description: "optional: another repo's directory to converge from (offline). Omit to pull the configured remote." } } } },
|
|
11975
|
+
{ name: "sol_push", description: "Push the current branch to the configured remote (converging — never FF-rejected). With no remote set, `repo` (owner/name) configures the hosted Sol then pushes. `create`/`public` opt into repo creation + public access in one step.", inputSchema: { type: "object", properties: { repo: { type: "string", description: "owner/name — configures + creates the remote when none is set" }, create: { type: "boolean" }, public: { type: "boolean" } } } },
|
|
11976
|
+
{ name: "sol_clone", description: "Clone a repo into a new directory. `repo` = owner/name (hosted backend, needs SOL_TOKEN) OR a local directory path (offline local-peer clone, no network). `dir` is the optional destination.", inputSchema: { type: "object", properties: { repo: { type: "string", description: "owner/name (hosted) or a local .sol directory path (offline)" }, dir: { type: "string", description: "optional destination dir" } }, required: ["repo"] } },
|
|
11977
|
+
{ name: "sol_hide", description: "Add/update a VisibilityPolicy RULE binding a glob to an AUDIENCE (who may DECRYPT). Host-visible METADATA only — no keys, no plaintext. `list` shows rules + covered paths; `remove` drops a rule. The actual hiding is the lockboxes sol_seal mints.", inputSchema: { type: "object", properties: { pattern: { type: "string", description: 'a glob over tree paths, or "list" / "remove <pattern>"' }, role: { type: "string", description: "write|admin — hide from anyone below that role" }, team: { type: "string" }, users: { type: "string", description: "comma-separated account list" }, hide_names: { type: "boolean" }, hide_existence: { type: "boolean" } }, required: ["pattern"] } },
|
|
11978
|
+
{ name: "sol_seal", description: "Mint/refresh the per-file lockboxes that enforce the hide rules (the host-blind encryption step). Operates on audiences + lockboxes, NEVER plaintext — the agent never sees a sealed value. `reapply=true` re-wraps after an ACL change. Returns a value-free JSON summary.", inputSchema: { type: "object", properties: { path: { type: "string", description: "optional: seal one path (default: every pending path)" }, reapply: { type: "boolean", description: "re-resolve + re-wrap after an audience change" } } } },
|
|
11979
|
+
{ name: "sol_sealed", description: "Report the sealed/hidden state as JSON: which paths are sealed, to which audience, and whether THIS identity can decrypt them. Identifiers + readiness only — never a plaintext value.", inputSchema: { type: "object", properties: {} } }
|
|
11980
|
+
];
|
|
11981
|
+
sanitizer2 = new VaultSanitizer;
|
|
11982
|
+
SOL_ID_RE = /\bh_[0-9a-f]{64}\b|\b(?:[0-9a-f]{4}-){7}[0-9a-f]{4}\b/g;
|
|
11983
|
+
CliExit = class CliExit extends Error {
|
|
11984
|
+
code;
|
|
11985
|
+
constructor(code) {
|
|
11986
|
+
super(`sol exited ${code}`);
|
|
11987
|
+
this.code = code;
|
|
11988
|
+
}
|
|
11989
|
+
};
|
|
11990
|
+
});
|
|
11991
|
+
|
|
11992
|
+
// src/bin/sol-mcp.ts
|
|
11993
|
+
var exports_sol_mcp = {};
|
|
11994
|
+
__export(exports_sol_mcp, {
|
|
11995
|
+
startWorkspaceMcp: () => startWorkspaceMcp
|
|
11996
|
+
});
|
|
11997
|
+
import { mkdirSync as mkdirSync11 } from "node:fs";
|
|
11998
|
+
import { dirname as dirname5, join as join18 } from "node:path";
|
|
11999
|
+
async function handle(ws, name, a) {
|
|
12000
|
+
switch (name) {
|
|
12001
|
+
case "sol_write":
|
|
12002
|
+
await ws.write(a.path, a.content);
|
|
12003
|
+
return text(`wrote ${a.path} (${a.content.length} chars)`);
|
|
12004
|
+
case "sol_read": {
|
|
12005
|
+
const c = await ws.read(a.path);
|
|
12006
|
+
return c === undefined ? text(`(absent: ${a.path})`) : text(c);
|
|
12007
|
+
}
|
|
12008
|
+
case "sol_edit":
|
|
12009
|
+
await ws.edit(a.path, a.old_str, a.new_str, { all: a.all });
|
|
12010
|
+
return text(`edited ${a.path}`);
|
|
12011
|
+
case "sol_ls": {
|
|
12012
|
+
const f = await ws.ls(a.prefix);
|
|
12013
|
+
return text(f.length ? f.join(`
|
|
12014
|
+
`) : "(no files yet)");
|
|
12015
|
+
}
|
|
12016
|
+
case "sol_move":
|
|
12017
|
+
await ws.move(a.from, a.to);
|
|
12018
|
+
return text(`moved ${a.from} -> ${a.to}`);
|
|
12019
|
+
case "sol_rm":
|
|
12020
|
+
await ws.remove(a.path);
|
|
12021
|
+
return text(`removed ${a.path}`);
|
|
12022
|
+
case "sol_commit":
|
|
12023
|
+
case "sol_checkpoint": {
|
|
12024
|
+
const h = await ws.commit(a.message);
|
|
12025
|
+
return text(`commit ${h.slice(0, 14)} — ${a.message}`);
|
|
12026
|
+
}
|
|
12027
|
+
case "sol_history": {
|
|
12028
|
+
const ops = await ws.history();
|
|
12029
|
+
return text(ops.map((o) => `${o.seq} ${o.type} ${o.path || ""}${o.message ? " : " + o.message : ""}`).join(`
|
|
12030
|
+
`) || "(empty)");
|
|
12031
|
+
}
|
|
12032
|
+
default:
|
|
12033
|
+
throw new Error("unknown tool: " + name);
|
|
12034
|
+
}
|
|
12035
|
+
}
|
|
12036
|
+
async function buildWorkspaceServer(solDir2) {
|
|
12037
|
+
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
12038
|
+
const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
12039
|
+
mkdirSync11(solDir2, { recursive: true });
|
|
12040
|
+
const repoRoot2 = dirname5(solDir2);
|
|
12041
|
+
if (process.cwd() !== repoRoot2) {
|
|
12042
|
+
try {
|
|
12043
|
+
process.chdir(repoRoot2);
|
|
12044
|
+
} catch {}
|
|
12045
|
+
}
|
|
12046
|
+
const ws = new SolWorkspace(new FileStore(solDir2), new FileOpLog(solDir2), process.env.SOL_ACTOR || "agent");
|
|
12047
|
+
const server = new Server({ name: "sol", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
12048
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
12049
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
12050
|
+
if (VCS_TOOL_NAMES.has(req.params.name)) {
|
|
12051
|
+
const r = await callVcsTool(req.params.name, req.params.arguments ?? {});
|
|
12052
|
+
return { content: [{ type: "text", text: r.text }], isError: r.isError };
|
|
12053
|
+
}
|
|
12054
|
+
try {
|
|
12055
|
+
return await handle(ws, req.params.name, req.params.arguments ?? {});
|
|
12056
|
+
} catch (e) {
|
|
12057
|
+
return { content: [{ type: "text", text: "sol error: " + (e?.message ?? e) }], isError: true };
|
|
12058
|
+
}
|
|
12059
|
+
});
|
|
12060
|
+
return server;
|
|
12061
|
+
}
|
|
12062
|
+
async function startWorkspaceMcp(opts = {}) {
|
|
12063
|
+
const solDir2 = opts.solDir || process.env.SOL_DIR || join18(process.cwd(), ".sol");
|
|
12064
|
+
if (opts.http) {
|
|
12065
|
+
const { serveMcpHttp: serveMcpHttp2 } = await Promise.resolve().then(() => (init_mcp_http(), exports_mcp_http));
|
|
12066
|
+
await serveMcpHttp2(() => buildWorkspaceServer(solDir2), opts.http);
|
|
11709
12067
|
return;
|
|
11710
12068
|
}
|
|
11711
|
-
const {
|
|
11712
|
-
await
|
|
12069
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
12070
|
+
const server = await buildWorkspaceServer(solDir2);
|
|
12071
|
+
await server.connect(new StdioServerTransport);
|
|
12072
|
+
}
|
|
12073
|
+
var authoringTools, VCS_TOOL_NAMES, tools, text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
12074
|
+
var init_sol_mcp = __esm(() => {
|
|
12075
|
+
init_file_store();
|
|
12076
|
+
init_workspace();
|
|
12077
|
+
init_mcp_http();
|
|
12078
|
+
init_mcp_vcs_tools();
|
|
12079
|
+
authoringTools = [
|
|
12080
|
+
{ name: "sol_write", description: "Create or overwrite a text file in the sol workspace. Authoring goes here — not to a disk.", inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } },
|
|
12081
|
+
{ name: "sol_read", description: "Read a text file from the sol workspace.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
|
12082
|
+
{ name: "sol_edit", description: "Replace a unique snippet in a file (set all=true to replace every occurrence).", inputSchema: { type: "object", properties: { path: { type: "string" }, old_str: { type: "string" }, new_str: { type: "string" }, all: { type: "boolean" } }, required: ["path", "old_str", "new_str"] } },
|
|
12083
|
+
{ name: "sol_ls", description: "List tracked files, optionally scoped to a directory prefix.", inputSchema: { type: "object", properties: { prefix: { type: "string" } } } },
|
|
12084
|
+
{ name: "sol_move", description: "Move or rename a file.", inputSchema: { type: "object", properties: { from: { type: "string" }, to: { type: "string" } }, required: ["from", "to"] } },
|
|
12085
|
+
{ name: "sol_rm", description: "Delete (untrack) a file.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
|
12086
|
+
{ name: "sol_commit", description: "Record a commit (a shareable milestone) over the current state.", inputSchema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] } },
|
|
12087
|
+
{ name: "sol_history", description: "Show the authoring history (the op-log).", inputSchema: { type: "object", properties: {} } }
|
|
12088
|
+
];
|
|
12089
|
+
VCS_TOOL_NAMES = new Set(VCS_TOOLS.map((t) => t.name));
|
|
12090
|
+
tools = [...authoringTools, ...VCS_TOOLS];
|
|
12091
|
+
if (false) {}
|
|
12092
|
+
});
|
|
12093
|
+
|
|
12094
|
+
// src/bin/dispatch.ts
|
|
12095
|
+
var exports_dispatch2 = {};
|
|
12096
|
+
__export(exports_dispatch2, {
|
|
12097
|
+
runCli: () => runCli2,
|
|
12098
|
+
dispatch: () => dispatch2
|
|
12099
|
+
});
|
|
12100
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
12101
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, readdirSync as readdirSync9, readFileSync as readFileSync18, rmSync as rmSync3, unlinkSync as unlinkSync8, watch as watch2, writeFileSync as writeFileSync16 } from "node:fs";
|
|
12102
|
+
import { homedir as homedir3, hostname as hostname2, platform as platform3, tmpdir as tmpdir2 } from "node:os";
|
|
12103
|
+
import { basename as basename2, dirname as dirname6, join as join19, resolve as resolve5, sep as sep3 } from "node:path";
|
|
12104
|
+
function globCovers2(pattern, path) {
|
|
12105
|
+
let re = "";
|
|
12106
|
+
for (let i = 0;i < pattern.length; i++) {
|
|
12107
|
+
const c = pattern[i];
|
|
12108
|
+
if (c === "*") {
|
|
12109
|
+
if (pattern[i + 1] === "*") {
|
|
12110
|
+
i++;
|
|
12111
|
+
if (pattern[i + 1] === "/")
|
|
12112
|
+
i++;
|
|
12113
|
+
re += ".*";
|
|
12114
|
+
} else
|
|
12115
|
+
re += "[^/]*";
|
|
12116
|
+
} else if (c === "?")
|
|
12117
|
+
re += "[^/]";
|
|
12118
|
+
else
|
|
12119
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
12120
|
+
}
|
|
12121
|
+
return new RegExp(`^${re}$`).test(path);
|
|
12122
|
+
}
|
|
12123
|
+
function tokenClaims2(token) {
|
|
12124
|
+
try {
|
|
12125
|
+
return JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString());
|
|
12126
|
+
} catch {
|
|
12127
|
+
return {};
|
|
12128
|
+
}
|
|
12129
|
+
}
|
|
12130
|
+
async function loadStoredToken2() {
|
|
12131
|
+
if (!existsSync18(CRED_PATH2))
|
|
12132
|
+
return;
|
|
12133
|
+
let creds;
|
|
12134
|
+
try {
|
|
12135
|
+
creds = JSON.parse(readFileSync18(CRED_PATH2, "utf8"));
|
|
12136
|
+
} catch {
|
|
12137
|
+
return;
|
|
12138
|
+
}
|
|
12139
|
+
if (!creds.accessToken)
|
|
12140
|
+
return;
|
|
12141
|
+
const exp = tokenClaims2(creds.accessToken).exp;
|
|
12142
|
+
if (typeof exp === "number" && exp * 1000 > Date.now() + 30000)
|
|
12143
|
+
return creds.accessToken;
|
|
12144
|
+
if (creds.refreshToken && creds.webUrl) {
|
|
12145
|
+
try {
|
|
12146
|
+
const res = await fetch(`${creds.webUrl}/api/auth/refresh`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ refreshToken: creds.refreshToken }) });
|
|
12147
|
+
if (res.ok) {
|
|
12148
|
+
const r = await res.json();
|
|
12149
|
+
if (r.accessToken) {
|
|
12150
|
+
writeFileSync16(CRED_PATH2, JSON.stringify({ ...creds, accessToken: r.accessToken, refreshToken: r.refreshToken ?? creds.refreshToken }, null, 2), { mode: 384 });
|
|
12151
|
+
return r.accessToken;
|
|
12152
|
+
}
|
|
12153
|
+
}
|
|
12154
|
+
} catch {}
|
|
12155
|
+
}
|
|
12156
|
+
return creds.accessToken;
|
|
12157
|
+
}
|
|
12158
|
+
function authExpired2() {
|
|
12159
|
+
return die("session expired — run `sol auth login` (or set SOL_TOKEN)");
|
|
12160
|
+
}
|
|
12161
|
+
function identityFromToken2(token) {
|
|
12162
|
+
const c = tokenClaims2(token);
|
|
12163
|
+
if (!c.handle && !c.email && !c.userId && !c.sub)
|
|
12164
|
+
return;
|
|
12165
|
+
return { handle: c.handle, email: c.email, userId: c.userId ?? c.sub };
|
|
12166
|
+
}
|
|
12167
|
+
function authHost2() {
|
|
12168
|
+
if (process.env.SOL_AUTH)
|
|
12169
|
+
return process.env.SOL_AUTH.replace(/\/+$/, "");
|
|
12170
|
+
try {
|
|
12171
|
+
const w = JSON.parse(readFileSync18(CRED_PATH2, "utf8")).webUrl;
|
|
12172
|
+
if (w)
|
|
12173
|
+
return w.replace(/\/+$/, "");
|
|
12174
|
+
} catch {}
|
|
12175
|
+
return "https://auth.midsummer.new";
|
|
12176
|
+
}
|
|
12177
|
+
async function resolveMcpHttp2(a, label) {
|
|
12178
|
+
if (!a.includes("--http"))
|
|
12179
|
+
return;
|
|
12180
|
+
const flagVal = (name) => {
|
|
12181
|
+
const i = a.indexOf(name);
|
|
12182
|
+
return i >= 0 && i + 1 < a.length ? a[i + 1] : undefined;
|
|
12183
|
+
};
|
|
12184
|
+
const { resolveMcpToken: resolveMcpToken2 } = await Promise.resolve().then(() => (init_mcp_http(), exports_mcp_http));
|
|
12185
|
+
const token = resolveMcpToken2(flagVal("--token"));
|
|
12186
|
+
if (!token) {
|
|
12187
|
+
die("refusing to start an OPEN HTTP MCP — set a bearer token via SOL_MCP_TOKEN (or --token <T>). every request must send `Authorization: Bearer <token>`.");
|
|
12188
|
+
}
|
|
12189
|
+
const portRaw = flagVal("--port");
|
|
12190
|
+
const port = portRaw ? Number(portRaw) : undefined;
|
|
12191
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65535))
|
|
12192
|
+
die(`invalid --port: ${portRaw}`);
|
|
12193
|
+
return { token, port, host: flagVal("--host"), label: label ?? (a.includes("--secret") || a.includes("--secrets") ? "sol-secrets" : "sol") };
|
|
12194
|
+
}
|
|
12195
|
+
function resolveRemote2(solDir2) {
|
|
12196
|
+
const cfg = loadRemote(solDir2);
|
|
12197
|
+
if (!cfg)
|
|
12198
|
+
return;
|
|
12199
|
+
return cfg.url ? cfg : { ...cfg, url: DEFAULT_REMOTE_URL2 };
|
|
12200
|
+
}
|
|
12201
|
+
async function pushEnvState2(solDirPath, cfg, token) {
|
|
12202
|
+
const { readEnvStateBundle: readEnvStateBundle2 } = await Promise.resolve().then(() => (init_anchor(), exports_anchor));
|
|
12203
|
+
const bundle = readEnvStateBundle2(solDirPath);
|
|
12204
|
+
if (!bundle)
|
|
12205
|
+
return;
|
|
12206
|
+
try {
|
|
12207
|
+
const res = await remoteEnvPush(cfg, token, bundle);
|
|
12208
|
+
if (res.reason === "registered") {
|
|
12209
|
+
console.log(`env anchor: registered env owner ${res.record.owner.genesisFingerprint ?? res.record.owner.creatorFingerprint ?? "(none)"} on the remote (TOFU)`);
|
|
12210
|
+
}
|
|
12211
|
+
} catch (e) {
|
|
12212
|
+
console.error(`
|
|
12213
|
+
ENV ANCHOR REJECTED by the remote: ${e?.message ?? String(e)}
|
|
12214
|
+
the trust-bearing env-state did NOT sync. run \`sol env verify --remote\` to see the owner/head divergence.`);
|
|
12215
|
+
process.exitCode = 1;
|
|
12216
|
+
}
|
|
12217
|
+
}
|
|
12218
|
+
async function pullEnvState2(solDirPath, cfg, token) {
|
|
12219
|
+
try {
|
|
12220
|
+
const { bundle } = await remoteEnvPull(cfg, token);
|
|
12221
|
+
if (!bundle || !bundle.journal?.length && !bundle.creatorPin)
|
|
12222
|
+
return;
|
|
12223
|
+
const { writeEnvStateBundle: writeEnvStateBundle2, ensureEnvDir: ensureEnvDir2 } = await Promise.resolve().then(() => (init_secret(), exports_secret));
|
|
12224
|
+
ensureEnvDir2(solDirPath);
|
|
12225
|
+
writeEnvStateBundle2(solDirPath, bundle);
|
|
12226
|
+
} catch {}
|
|
12227
|
+
}
|
|
12228
|
+
async function surfaceEnvDivergence2(solDirPath, cfg, token) {
|
|
12229
|
+
try {
|
|
12230
|
+
const { remoteEnvAnchor: remoteEnvAnchor2 } = await Promise.resolve().then(() => (init_remote(), exports_remote));
|
|
12231
|
+
const { record } = await remoteEnvAnchor2(cfg, token);
|
|
12232
|
+
if (!record)
|
|
12233
|
+
return;
|
|
12234
|
+
const { readEnvStateBundle: readEnvStateBundle2 } = await Promise.resolve().then(() => (init_anchor(), exports_anchor));
|
|
12235
|
+
const { verifyLocalAgainstRemote: verifyLocalAgainstRemote2 } = await Promise.resolve().then(() => (init_env_state(), exports_env_state));
|
|
12236
|
+
const local = readEnvStateBundle2(solDirPath);
|
|
12237
|
+
if (!local)
|
|
12238
|
+
return;
|
|
12239
|
+
const verdict = verifyLocalAgainstRemote2(local, record);
|
|
12240
|
+
for (const i of verdict.issues)
|
|
12241
|
+
console.error(`${i.severity === "error" ? "ENV ANCHOR ERROR" : "env anchor warn"} [${i.check}] ${i.message}`);
|
|
12242
|
+
if (!verdict.ok)
|
|
12243
|
+
process.exitCode = 1;
|
|
12244
|
+
} catch {}
|
|
12245
|
+
}
|
|
12246
|
+
function cliVersion2() {
|
|
12247
|
+
if (typeof __SOL_COMPILED_VERSION__ === "string" && __SOL_COMPILED_VERSION__)
|
|
12248
|
+
return __SOL_COMPILED_VERSION__;
|
|
12249
|
+
try {
|
|
12250
|
+
return JSON.parse(readFileSync18(new URL("./package.json", import.meta.url), "utf8")).version || "dev";
|
|
12251
|
+
} catch {
|
|
12252
|
+
return "dev";
|
|
12253
|
+
}
|
|
12254
|
+
}
|
|
12255
|
+
function reportSemanticGate2(outcome, json = false) {
|
|
12256
|
+
if (!outcome.configured || outcome.ok)
|
|
12257
|
+
return;
|
|
12258
|
+
if (json) {
|
|
12259
|
+
console.log(JSON.stringify({ semantic: { ok: false, check: outcome.command, exitCode: outcome.exitCode, output: outcome.output } }));
|
|
12260
|
+
process.exitCode = 1;
|
|
12261
|
+
return;
|
|
12262
|
+
}
|
|
12263
|
+
console.log(`
|
|
12264
|
+
SEMANTIC conflict — the merge auto-converged with NO line conflicts, but the check FAILED (\`${outcome.command}\`, exit ${outcome.exitCode}).`);
|
|
12265
|
+
console.log(" the merge stays committed (convergence is lossless); review it and land a follow-up commit.");
|
|
12266
|
+
console.log(" inspect: `sol status --json` (state: SEMANTIC) | re-run: `sol check`");
|
|
12267
|
+
if (outcome.output)
|
|
12268
|
+
console.log(` --- check output (tail) ---
|
|
12269
|
+
` + outcome.output.split(`
|
|
12270
|
+
`).map((l) => " " + l).join(`
|
|
12271
|
+
`));
|
|
12272
|
+
process.exitCode = 1;
|
|
12273
|
+
}
|
|
12274
|
+
async function dispatch2(argv) {
|
|
12275
|
+
if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
|
|
12276
|
+
console.log(`sol ${cliVersion2()}`);
|
|
12277
|
+
return;
|
|
12278
|
+
}
|
|
12279
|
+
const args = argv.slice(1);
|
|
12280
|
+
const wantsHelp = args.includes("--help") || args.includes("-h");
|
|
12281
|
+
const SUBHELP = {
|
|
12282
|
+
commit: `sol commit "<msg>" commits the whole working tree; works as-is for a solo author.
|
|
12283
|
+
sol commit -m "<msg>" <file>... commit ONLY those paths (correct attribution for concurrent agents)
|
|
12284
|
+
|
|
12285
|
+
(once OTHER authors have committed to this repo, a whole-tree commit is refused to avoid mis-attribution
|
|
12286
|
+
— scope to files \`sol commit -m "<msg>" <files>\` or force with --whole-tree).
|
|
12287
|
+
|
|
12288
|
+
flags:
|
|
12289
|
+
-m <message> message when scoping to files
|
|
12290
|
+
--whole-tree force a whole-tree commit even after OTHER authors are present (the guard above)
|
|
12291
|
+
--force commit even with unresolved <<<<<<< conflict markers
|
|
12292
|
+
|
|
12293
|
+
examples:
|
|
12294
|
+
sol commit "add login route"
|
|
12295
|
+
sol commit -m "fix parser" src/parse.ts src/lex.ts`,
|
|
12296
|
+
push: `sol push push the current branch to the configured remote (converging — never FF-rejected)
|
|
12297
|
+
sol push <repo> if no remote is set, use the hosted Sol (${DEFAULT_REMOTE_URL2}) + this repo name, then push
|
|
12298
|
+
sol push --create <repo> same, explicit form (creates the repo on first push)
|
|
12299
|
+
sol push --public <repo> create + push + make it public in ONE step (else new repos are private)
|
|
12300
|
+
|
|
12301
|
+
flags:
|
|
12302
|
+
--create explicit "creates the repo on first push" (the bare \`<repo>\` form does this too)
|
|
12303
|
+
--public after a successful create/push, set public access (same op as \`sol access public\`)
|
|
12304
|
+
|
|
12305
|
+
notes:
|
|
12306
|
+
a remote already configured? the <repo>/--create arg is ignored — \`sol push\` just syncs.
|
|
12307
|
+
new repos are PRIVATE by default; \`--public\` is the one-step opt-in to share (or \`sol access public\` later).
|
|
12308
|
+
set SOL_TOKEN (or \`sol auth login\`) first; the push registers the current branch's head on the remote.
|
|
12309
|
+
|
|
12310
|
+
examples:
|
|
12311
|
+
sol push # remote already configured
|
|
12312
|
+
sol push alice/app # one step: configure hosted remote + push (private)
|
|
12313
|
+
sol push --create --public alice/app # create + push + share, all at once`,
|
|
12314
|
+
pull: `sol pull fetch + converge the current branch from the configured remote
|
|
12315
|
+
sol pull <dir> OFFLINE: converge another local repo's .sol on disk into this one (multi-agent, no network)
|
|
12316
|
+
|
|
12317
|
+
notes:
|
|
12318
|
+
refuses to run over a dirty tree — commit or discard first. conflicts land in the working tree with markers.
|
|
12319
|
+
refuses to run while UNRESOLVED <<<<<<< markers remain — resolve them and \`sol commit\`, then pull again.`,
|
|
12320
|
+
hide: `sol hide <pattern> [--role write|admin] [--team <id>] [--users a,b] [--escrow] [--no-list] [--hide-names] [--hide-existence] [--strict]
|
|
12321
|
+
add/update a VisibilityPolicy RULE — the HEADLINE verb. binds a glob over tree
|
|
12322
|
+
paths to an AUDIENCE (who may DECRYPT). the rule is host-visible METADATA (no
|
|
12323
|
+
keys, no plaintext); the actual hiding is the per-file lockboxes \`sol seal\` mints.
|
|
12324
|
+
sol hide list show the policy rules + which CURRENT paths each one covers
|
|
12325
|
+
sol hide remove <pattern> drop a rule (does NOT auto-unseal existing content — re-seal to recall)
|
|
12326
|
+
|
|
12327
|
+
audience (default --role write — "hide from anyone who is only \`read\`"):
|
|
12328
|
+
--role <write|admin> everyone whose repo role is >= that (resolved against the ACL grants, server-assisted)
|
|
12329
|
+
--team <id> a specific team
|
|
12330
|
+
--users a,b,c an explicit account list
|
|
12331
|
+
--escrow ALSO wrap to the org-escrow recovery slot (opt-in; loud — an admin can then recover)
|
|
12332
|
+
|
|
12333
|
+
THE HIDING LADDER (each rung hides STRICTLY more; pick the weakest that meets the threat model):
|
|
12334
|
+
(default) DECRYPT-ONLY: host + collaborators see the PATH, not the bytes. content is sealed; the
|
|
12335
|
+
filename + the fact the file exists stay fully host-visible. the baseline \`sol seal\`.
|
|
12336
|
+
--no-list NO-LIST: collaborators can't LIST/fetch it (a server-side path-ACL), but the HOST still
|
|
12337
|
+
reads the plaintext path string. hidden from collaborators, VISIBLE TO HOST.
|
|
12338
|
+
--hide-names HIDE-NAMES (L2): the NAME is hidden from host AND collaborators. \`sol seal\` replaces the
|
|
12339
|
+
cleartext filename with an opaque \`\\0slot\\0<id>\` + seals the real name into the box. the
|
|
12340
|
+
host sees a slot exists at a known path, never the name. recipients rebuild it locally.
|
|
12341
|
+
--hide-existence HIDE-EXISTENCE (L3, IMPLIES --hide-names): the PATH is ABSENT from host + collaborators.
|
|
12342
|
+
\`sol seal\` moves the entry into its dir's ONE \`\\0hidden\` sealed sidecar. a non-recipient
|
|
12343
|
+
sees only a per-dir marker ("this dir has >=1 hidden child"), NEVER the name, the bytes, or
|
|
12344
|
+
even that THIS path exists. the strongest hiding (the fix-CVE-auth-bypass.ts / stargate case).
|
|
12345
|
+
--strict (with --hide-existence) REFUSE to existence-hide a path whose name is already BURNED in op
|
|
12346
|
+
history. default: proceed + WARN (the old name stays recoverable from history; hiding
|
|
12347
|
+
protects names written AFTER the upgrade). use --strict for a true zero-day filename. (D-STRUCT-5)
|
|
12348
|
+
|
|
12349
|
+
after adding a rule, seal the covered paths to it: \`sol seal <path>\` (no recipients) consults the policy.
|
|
12350
|
+
host-blind: the server resolves WHO (accountIds + their PUBLIC keys) as metadata; the CLIENT mints every lockbox.
|
|
12351
|
+
|
|
12352
|
+
examples:
|
|
12353
|
+
sol hide "src/private/**" # hide from \`read\` collaborators (default --role write)
|
|
12354
|
+
sol hide "**/*.env" --role admin # only admins+ may decrypt any .env
|
|
12355
|
+
sol hide "secrets/**" --users alice,bob # an explicit audience
|
|
12356
|
+
sol hide "vault/**" --no-list # full opacity: read-collabs can't even list vault/
|
|
12357
|
+
sol hide "src/fix-CVE-*.ts" --hide-names # L2: the FILENAME is hidden from the host too
|
|
12358
|
+
sol hide "features/stargate/**" --hide-existence # L3: the path is ABSENT from host + collaborators
|
|
12359
|
+
sol hide list`,
|
|
12360
|
+
seal: `sol seal <path> [recipient...] encrypt a file so the HOST only ever sees ciphertext (per-path privacy)
|
|
12361
|
+
|
|
12362
|
+
sol seal <path> NO recipients -> consult the VisibilityPolicy for <path> + seal to its audience
|
|
12363
|
+
sol seal <dir> --seal-subtree hide a DIRECTORY's CONTENTS: collapse it into ONE sealed node (the host sees
|
|
12364
|
+
the dir NAME + one blob, never the child names/count). a recipient's pull
|
|
12365
|
+
decrypts it back into a real directory; a non-recipient gets nothing within.
|
|
12366
|
+
sol seal <path> --hide-names hide the FILENAME too (L2): after sealing the content, replace the cleartext
|
|
12367
|
+
entry with an opaque \`\\0slot\\0<id>\` slot + seal the real name into the box.
|
|
12368
|
+
the host never sees the name; recipients rebuild it in their local view.
|
|
12369
|
+
sol seal <path> --escrow ALSO wrap the CEK to the org-escrow recovery slot (OPT-IN, default OFF, LOUD):
|
|
12370
|
+
an org admin holding the escrow key CAN recover this content. surfaced loudly
|
|
12371
|
+
in the output + \`sol sealed\` so "hidden from admins" is never silently false.
|
|
12372
|
+
sol seal --reapply [<path>] re-resolve the audience + re-wrap to the CURRENT policy (bumps epoch)
|
|
12373
|
+
|
|
12374
|
+
a bare \`sol seal <path>\` with no recipients looks up the policy rule covering <path> (\`sol hide\`) and wraps
|
|
12375
|
+
the CEK to that audience's CURRENT members. an EXPLICIT recipient list still works (ad-hoc \`users\` audience).
|
|
12376
|
+
\`--reapply\` re-resolves every policy-covered sealed path (or just <path>) against the live ACL + re-wraps,
|
|
12377
|
+
bumping the box epoch. NO-RECALL caveat: a newly-added writer cannot read PAST sealed content until a reapply
|
|
12378
|
+
re-wraps the CEK to them; a removed recipient is not retroactively recalled — for a LIVE secret, rotate it at
|
|
12379
|
+
the source. (\`sol seal --reapply\` is printed loudly so the limit is never silent.)
|
|
12380
|
+
|
|
12381
|
+
the content becomes host-blind ciphertext committed into history; the keys stay LOCAL (~/.sol keystore).
|
|
12382
|
+
you are always a recipient of your own seals. add other actors to share read access. \`sol cat\` decrypts
|
|
12383
|
+
for a recipient; everyone else sees <<sealed>>.
|
|
12384
|
+
|
|
12385
|
+
a recipient may be a CROSS-ACCOUNT identity: \`alice\`, \`@alice\`, or \`alice@acct\`. for one with a PUBLISHED
|
|
12386
|
+
identity key (\`sol keys publish\`) the CEK is wrapped to their X25519 PUBLIC key (host-blind cross-account
|
|
12387
|
+
delivery); the client verifies their self-sig + TOFU-pins the fingerprint. an actor with NO published key
|
|
12388
|
+
stays a legacy SYMMETRIC recipient. \`sol sealed\` shows each sealed path's audience.
|
|
12389
|
+
|
|
12390
|
+
example:
|
|
12391
|
+
sol seal secrets/.env alice bob # alice, bob (and you) can read it; the server cannot
|
|
12392
|
+
sol seal secrets/.env @alice # cross-account: wrap to alice's published X25519 key (host stays blind)`,
|
|
12393
|
+
sealed: `sol sealed list every sealed path with its AUDIENCE (recipient accounts + fingerprints + epoch)
|
|
12394
|
+
sol sealed --json {count, sealed:[{path, epoch, accounts:[{accountId, fingerprint, keyEpoch}], local[], escrow[]}]}
|
|
12395
|
+
sol sealed --check DRIFT: per sealed path, does its CURRENT recipient set still match the policy
|
|
12396
|
+
audience? after an ACL change (a reader promoted/removed), lists which sealed
|
|
12397
|
+
paths are now OUT OF POLICY + suggests \`sol seal --reapply\`. compares RECIPIENT
|
|
12398
|
+
IDENTIFIERS only (host-visible) — never content (calls the backend /policy/check).
|
|
12399
|
+
|
|
12400
|
+
the audience is recoverable from the box bytes (who holds a lockbox is host-visible); cross-account
|
|
12401
|
+
fingerprints come from the local audience record written at seal time. an \`escrow\` slot is surfaced LOUDLY (an
|
|
12402
|
+
org-escrow key-holder CAN recover that path — it is NOT hidden from admins). content stays host-blind.`,
|
|
12403
|
+
open: `sol open <path> open a sealed path's plaintext (as a recipient — like \`sol cat\`)
|
|
12404
|
+
sol open <path> --recovery [slot] ORG-ESCROW RECOVERY (D1, §7): recover a sealed path WITHOUT being a named
|
|
12405
|
+
recipient, via the recovery SLOT the CEK was wrapped to (default org-escrow).
|
|
12406
|
+
sol open <path> --recovery org-escrow --as <escrowAccountId>
|
|
12407
|
+
|
|
12408
|
+
for an escrow key-holder: set SOL_RECOVERY_CODE to the org-escrow recovery code and run with --recovery. the
|
|
12409
|
+
escrow PRIVATE key is derived OFF-HOST from that code (the host never holds it); recovery reuses the existing
|
|
12410
|
+
recovery primitive (openWithRecovery — no new crypto). only paths sealed WITH \`--escrow\` carry a recovery slot.`,
|
|
12411
|
+
keys: `sol keys show your local identity (accountId, epoch, fingerprint)
|
|
12412
|
+
sol keys init mint your X25519 identity keypair; print the recovery code ONCE
|
|
12413
|
+
sol keys publish publish your PUBLIC key to the directory so others can seal to you
|
|
12414
|
+
sol keys rotate mint a new key epoch + publish it (past seals stay on the old epoch)
|
|
12415
|
+
sol keys verify <acct> <fpr> out-of-band fingerprint check of a peer's published key (anti-MITM, TOFU)
|
|
12416
|
+
sol keys export write an ENCRYPTED keystore backup bundle (to stdout)
|
|
12417
|
+
sol keys import <bundle.json> restore an identity from an encrypted backup bundle
|
|
12418
|
+
|
|
12419
|
+
host-blind by construction: the directory holds PUBLIC keys only. your PRIVATE key is NEVER stored or sent —
|
|
12420
|
+
it re-derives from the recovery code you hold off-host, so device loss is survivable and the server stays blind.
|
|
12421
|
+
|
|
12422
|
+
env:
|
|
12423
|
+
SOL_RECOVERY_CODE your recovery code — needed by publish/rotate/export (the private key derives from it)
|
|
12424
|
+
SOL_KEYSTORE_PASSPHRASE passphrase that encrypts an export/import bundle
|
|
12425
|
+
SOL_ACCOUNT override the accountId (self-host / tests); else taken from your login token
|
|
12426
|
+
|
|
12427
|
+
examples:
|
|
12428
|
+
sol keys init # mint + show the recovery code
|
|
12429
|
+
SOL_RECOVERY_CODE="..." sol keys publish
|
|
12430
|
+
sol keys verify alice 1a2b-3c4d-... # confirm alice's key out-of-band before sealing to her`,
|
|
12431
|
+
remote: `sol remote show the configured remote
|
|
12432
|
+
sol remote <url> <repo> set the remote (url + repo name)
|
|
12433
|
+
|
|
12434
|
+
tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
|
|
12435
|
+
branch: `sol branch list branches (* = current)
|
|
12436
|
+
sol branch <name> [<ref>] create a branch at <ref> (default: HEAD)`,
|
|
12437
|
+
switch: `sol switch <branch> switch branches (commits/keeps current work first; never loses it)`,
|
|
12438
|
+
merge: `sol merge <branch> 3-way merge <branch> into the current branch
|
|
12439
|
+
conflicts land in the working tree with <<<<<<< markers — resolve, then \`sol commit\`.`,
|
|
12440
|
+
undo: `sol undo undo the LAST commit on the current branch (roll back a bad agent turn)
|
|
12441
|
+
|
|
12442
|
+
moves the branch head back to its parent and updates the working tree to match — NON-DESTRUCTIVELY:
|
|
12443
|
+
the undone commit STAYS in the op-log (audit; \`sol log --all\` shows it), the hash chain stays
|
|
12444
|
+
append-only + valid (\`sol fsck\` OK), and the move is recorded as a new op so it converges with peers
|
|
12445
|
+
losslessly. refuses over a dirty tree — commit or discard first.
|
|
12446
|
+
|
|
12447
|
+
undo (head-move, last commit only) vs revert (an inverse commit of ANY commit, full history kept).
|
|
12448
|
+
|
|
12449
|
+
flags:
|
|
12450
|
+
--json {undid, head, parent, filesChanged}`,
|
|
12451
|
+
revert: `sol revert <ref> append a NEW commit that inverts the changes <ref> introduced (like git revert)
|
|
12452
|
+
|
|
12453
|
+
history is fully preserved — <ref> stays present; the inverse lands as a fresh commit on HEAD. computed
|
|
12454
|
+
by a 3-way merge, so later concurrent edits are carried and genuine conflicts land with <<<<<<< markers
|
|
12455
|
+
(resolve, then \`sol commit\`). refuses over a dirty tree.
|
|
12456
|
+
|
|
12457
|
+
<ref> is a branch/tag name, a commit hash-prefix, or an op seq number.
|
|
12458
|
+
|
|
12459
|
+
flags:
|
|
12460
|
+
--json {reverted, head, filesChanged, committed} (or {reverted, conflicts, committed:false})
|
|
12461
|
+
|
|
12462
|
+
example:
|
|
12463
|
+
sol revert 7 # undo what commit seq 7 did, as a new commit on top`,
|
|
12464
|
+
log: `sol log commit history for the current branch
|
|
12465
|
+
sol log <branch|commit> history scoped to a ref
|
|
12466
|
+
sol log --all every op in the log (not just commits)
|
|
12467
|
+
sol log --json array of {hash, by, message, seq, parents, prov} (agent-parseable)
|
|
12468
|
+
sol log --all --json the FULL op-log: {version, ops:[{seq, id, type, path, by, message, root, parents, prov}]}`,
|
|
12469
|
+
diff: `sol diff working tree vs HEAD
|
|
12470
|
+
sol diff <ref> working tree vs a branch/commit
|
|
12471
|
+
sol diff <ref> <ref> tree vs tree
|
|
12472
|
+
sol diff <path> one file, working tree vs HEAD
|
|
12473
|
+
sol diff --json array of {path, kind, conflicted} for the working diff`,
|
|
12474
|
+
status: `sol status branch, head, and the working-tree changes (conflicts called out distinctly)
|
|
12475
|
+
sol status --json structured state: {branch, head, headAuthor, seq, sessionActor, clean, changes, conflicts}`,
|
|
12476
|
+
conflicts: `sol conflicts list paths with unresolved <<<<<<< conflict markers (+ a per-file hunk summary)
|
|
12477
|
+
sol conflicts --json {count, conflicts:[{path, hunks:[...], base, ours, theirs}]}
|
|
12478
|
+
base/ours/theirs are the FULL 3-way sides (base = common ancestor) so an
|
|
12479
|
+
agent can resolve programmatically — the in-file markers are only 2-way.`,
|
|
12480
|
+
check: `sol check run the configured test-gate command against the working tree (on demand)
|
|
12481
|
+
sol check --set "<cmd>" set the gate command (e.g. \`sol check --set "bun test"\`), stored in .sol/sol.json
|
|
12482
|
+
sol check --show print the configured command
|
|
12483
|
+
sol check --clear remove it (the convergence gate goes inert)
|
|
12484
|
+
sol check --json {configured, ok, command, exitCode, output}
|
|
12485
|
+
|
|
12486
|
+
what it's for (TEST-GATED CONVERGENCE):
|
|
12487
|
+
a line-based merge can auto-converge a tree that's SEMANTICALLY broken (two agents each add a line at
|
|
12488
|
+
different positions -> both survive, ZERO line conflicts). after a converging merge (pull/merge/push) the
|
|
12489
|
+
gate runs automatically: if the check FAILS, the merge STAYS committed (convergence is lossless) and a
|
|
12490
|
+
distinct SEMANTIC conflict is flagged — \`sol status --json\` then reports state:"SEMANTIC". resolve with a
|
|
12491
|
+
follow-up commit (which clears the flag). with no check configured the gate is inert (behavior unchanged).`,
|
|
12492
|
+
restore: `sol restore [--from <ref>] [<path>...] restore a file (or the whole tree) from a ref (default HEAD)`,
|
|
12493
|
+
run: `sol run [--keep <path>]... [--isolate] <command...>
|
|
12494
|
+
hydrate the current tree to a sandbox, run the command, capture produced files back into a commit.
|
|
12495
|
+
--isolate confines the run (no network, writes stay in the sandbox).`,
|
|
12496
|
+
promote: "sol promote [branch] point the remote's production branch at <branch> (default: current branch)",
|
|
12497
|
+
git: `sol git import <repo> [dir] import a git repo's HEAD into a new Sol repo
|
|
12498
|
+
sol git export <repo> [-b branch] replay the Sol commit DAG as git history (+ provenance trailers), then \`git push\``,
|
|
12499
|
+
mr: `sol mr open [--from <branch>] [--to <branch>] [--upstream <repo>] -t "title" [-m body]
|
|
12500
|
+
sol mr list | show <id> | review <id> --approve|--request-changes|--comment [-m msg]
|
|
12501
|
+
sol mr comment <id> -m msg [--path f --line N] | check <id> --run -- <cmd> | merge <id> [--force] | close <id>`,
|
|
12502
|
+
fork: "sol fork [<url>] <parent-repo> <new-repo> [dir] — your own copy (all branches + history + a parent link)",
|
|
12503
|
+
forks: "sol forks — list the forks of the current repo",
|
|
12504
|
+
access: "sol access [show | public | private | add <userId> <read|write|admin> | remove <userId> | add-team <teamId> <read|write|admin> | remove-team <teamId>]\n tip: to share a NEW repo in one step, `sol push --public <repo>` (create + push + public) instead of pushing then `sol access public`.",
|
|
12505
|
+
share: "sol share — there is no separate share command: a new repo goes public in one step with `sol push --public <repo>`,\n or flip an existing one with `sol access public`. (new repos are private by default.)",
|
|
12506
|
+
auth: "sol auth [login [<web-url>] | logout | status | whoami | set-handle <name> | pat [days]]",
|
|
12507
|
+
clone: "sol clone [<url>] <owner>/<repo> [dir] — default dir = <repo> (url defaults to the hosted Sol)",
|
|
12508
|
+
view: `sol view <name> [dir] create a clone-free AGENT VIEW (default dir ./<name>)
|
|
12509
|
+
|
|
12510
|
+
a view is a new working directory with its OWN working tree + branch + index, that SHARES this repo's
|
|
12511
|
+
content store (.sol/objects) on disk — NO object/disk duplication (git's worktrees duplicate node_modules
|
|
12512
|
+
/build = gigabytes; a view duplicates nothing — only its own tiny branch op-log). it starts at the
|
|
12513
|
+
parent's current head on a new branch \`view/<name>\`. N agents each in their own view commit CONCURRENTLY
|
|
12514
|
+
with zero clobber (each view's commit is independent over the one shared store), then converge losslessly.
|
|
12515
|
+
|
|
12516
|
+
example:
|
|
12517
|
+
sol view agent-a # ./agent-a shares this repo's store; commit there, then \`sol pull <parent>\``,
|
|
12518
|
+
views: `sol views list every view of this repo (name, dir, branch, head, author, active/stale)
|
|
12519
|
+
sol views --json machine-readable {repo, sharedObjects, views[]}
|
|
12520
|
+
sol views --prune drop registry entries whose working dir is gone
|
|
12521
|
+
sol views --prune <name> [--delete] prune one view (--delete also removes its working dir)`,
|
|
12522
|
+
env: `sol env <subcommand> .sol/env/ legibility + gate management
|
|
12523
|
+
|
|
12524
|
+
TRUST BOUNDARY: env READS are AGENT-SAFE / keyless — ls/show/schema/diff/validate/audit touch the SCHEMA + the
|
|
12525
|
+
host-visible gate metadata, never a value, and need no key. the GATE-MANAGEMENT edits (audience add/rm, anchor)
|
|
12526
|
+
are OPERATOR-AUTHENTICATED: they mutate committed state and must be cryptographically SIGNED, so they require
|
|
12527
|
+
SOL_RECOVERY_CODE (a keyless agent is refused). reads legible to all; management writes proven.
|
|
12528
|
+
|
|
12529
|
+
read subcommands (agent-safe, keyless):
|
|
12530
|
+
sol env ls list envs (extends, config/secret counts, the runtime recipient handle)
|
|
12531
|
+
sol env show <env> the resolved (base ⊕ overlay) view — secrets render [sealed]/[unset], NEVER values
|
|
12532
|
+
sol env schema [--write] the de-valued cross-env schema (the agent's read surface); --write regenerates schema.lock
|
|
12533
|
+
sol env diff <a> <b> value-blind shape delta between two envs (name/type/audience/presence)
|
|
12534
|
+
sol env validate [--agent <id>] structural checks: fail-closed, drift, audience integrity, agent-blind, schema freshness
|
|
12535
|
+
sol env validate --remote ALSO consult the REMOTE anchor (FINDING A): flag a locally re-keyed/forked env HARD
|
|
12536
|
+
sol env verify alias for \`validate --remote\` — the external owner-anchor cross-check
|
|
12537
|
+
sol env audit <env> journal-authentic who-can-decrypt + file-vs-journal drift (identifiers only)
|
|
12538
|
+
sol env audience ls list the gate (audience.toml) — public material only
|
|
12539
|
+
|
|
12540
|
+
management subcommands (operator-authenticated — need SOL_RECOVERY_CODE; a keyless agent is refused):
|
|
12541
|
+
sol env init [--envs a,b,c] scaffold .sol/env/; an operator with SOL_RECOVERY_CODE also writes creator.pin (anchors genesis)
|
|
12542
|
+
sol env anchor pin the current operator as creator (creator.pin) for an already-UNANCHORED repo
|
|
12543
|
+
sol env audience add|rm BUILD/manage the gate — SIGNED gate ops; see \`sol env audience --help\`
|
|
12544
|
+
|
|
12545
|
+
every subcommand takes --json. (sol env <sub> --help for a focused page.)`,
|
|
12546
|
+
"env diff": `sol env diff <a> <b> [--json] value-blind shape delta between two envs
|
|
12547
|
+
|
|
12548
|
+
reports name/type/audience/presence deltas (+ added, - removed, ~ changed) — NEVER value-equality. a config key
|
|
12549
|
+
whose VALUE differs between envs is "unchanged" here (the diff is value-blind by design).
|
|
12550
|
+
|
|
12551
|
+
example:
|
|
12552
|
+
sol env diff staging production`,
|
|
12553
|
+
"env validate": `sol env validate [--agent <id>] [--remote] [--json] structural, value-blind checks over .sol/env/
|
|
12554
|
+
|
|
12555
|
+
checks: fail-closed grammar (no plaintext under a secret slot), cross-env drift, audience integrity (every aud
|
|
12556
|
+
resolves through audience.toml; sealed recipients == the audience), the agent is in NO audience, schema.lock fresh.
|
|
12557
|
+
--agent <id> assert <id> (or SOL_AGENT_ACCOUNT) appears in no secret audience (the locked CI invariant)
|
|
12558
|
+
--remote ALSO consult the REMOTE EXTERNAL ANCHOR — the remote-tracked env-owner identity + journal head.
|
|
12559
|
+
the LOCAL checks are self-referential (they verify .sol/env vs creator.pin + the signed journal,
|
|
12560
|
+
never the root-of-trust key against an externally-anchored owner), so a foreign key that rebuilt
|
|
12561
|
+
.sol/env (a RE-KEY, FINDING A) reports GREEN locally. --remote flags it HARD against the owner the
|
|
12562
|
+
remote pinned (TOFU on first push) — the root of trust a full-FS attacker cannot rewrite.`,
|
|
12563
|
+
"env verify": `sol env verify [--agent <id>] [--json] \`validate\` that ALWAYS consults the remote anchor (= \`validate --remote\`)
|
|
12564
|
+
|
|
12565
|
+
flags a locally re-keyed or forked env (FINDING A) HARD against the remote-pinned owner identity + journal head.
|
|
12566
|
+
needs a configured remote + SOL_TOKEN (\`sol remote\` / \`sol auth login\`); offline it warns the anchor was not consulted.`,
|
|
12567
|
+
"env show": `sol env show <env> [--json] the resolved (base ⊕ overlay) view of one env — secrets render [sealed]/[unset], NEVER values`,
|
|
12568
|
+
"env schema": `sol env schema [--write] [--json] the de-valued, cross-env-unioned schema (the agent's read surface)
|
|
12569
|
+
|
|
12570
|
+
--write regenerate + commit .sol/env/schema.lock from the current world
|
|
12571
|
+
config renders its plaintext value; a secret renders only its per-env STATE (sealed/unset) — value-free by construction.`,
|
|
12572
|
+
"env ls": `sol env ls [--json] list registered envs (extends, config/secret counts, the runtime recipient handle)
|
|
12573
|
+
|
|
12574
|
+
the \`runtime=\` column is the real manifest.runtime[env] binding — the machine recipient that decrypts the env at
|
|
12575
|
+
deploy/boot. blank until you \`sol secret runtime provision --env <env>\` (which mints + records it).`,
|
|
12576
|
+
"env audit": `sol env audit <env> [--json] the journal-authentic who-can-decrypt + drift (identifiers only, never a value)
|
|
12577
|
+
|
|
12578
|
+
== \`sol secret audit --env <env>\` — same backing as the \`secret_audit\` MCP tool.
|
|
12579
|
+
who-can-decrypt per secret, the recipients its audience expands to through the gate REPLAYED from the SIGNED
|
|
12580
|
+
gate ops (NOT the hand-editable audience.toml) — so a file-poisoned gate cannot inflate it.
|
|
12581
|
+
drift handles whose audience.toml membership disagrees with the journal-authored set (a hand-added
|
|
12582
|
+
intruder / a swapped pubkey), plus the journal trust verdict.
|
|
12583
|
+
agent-safe: identifiers + the trust verdict only; no value, no key.`,
|
|
12584
|
+
"env anchor": `sol env anchor [--json] pin the CURRENT operator as the env creator (creator.pin) for an UNANCHORED repo
|
|
12585
|
+
|
|
12586
|
+
\`sol env validate\` warns \`genesis-anchor: UNANCHORED\` when a repo has a journal-derived gate but NO creator pin
|
|
12587
|
+
(the pin was never written, or was lost) — a re-bootstrap by a foreign key then can't be told from the true
|
|
12588
|
+
creator. \`env anchor\` writes creator.pin as the current operator, CROSS-BOUND to the main op-log genesis (the
|
|
12589
|
+
external birth cert a full-FS attacker cannot rewrite). thereafter only this key may author the gate-bootstrap.
|
|
12590
|
+
|
|
12591
|
+
operator-authenticated: requires SOL_RECOVERY_CODE (a keyless caller cannot anchor). REFUSES to re-pin over an
|
|
12592
|
+
existing creator.pin (re-pinning IS the re-bootstrap attack). \`sol env init\` already anchors at genesis when the
|
|
12593
|
+
creator runs it with a recovery code; \`anchor\` is the after-the-fact fix for a repo that wasn't.`,
|
|
12594
|
+
"env init": `sol env init [--envs a,b,c] scaffold .sol/env/ (manifest + base + per-env files + an empty audience.toml)
|
|
12595
|
+
|
|
12596
|
+
default envs: development,staging,production. \`sol secret declare\` also creates the manifest on first use.
|
|
12597
|
+
when run by an OPERATOR (SOL_RECOVERY_CODE set), it ALSO writes creator.pin — pinning that operator as the genesis
|
|
12598
|
+
creator (anchoring the gate-bootstrap). a keyless init leaves the genesis UNANCHORED (fix later with \`sol env anchor\`).`,
|
|
12599
|
+
"env audience": `sol env audience add <handle> <account> [--pubkey <sol1pk_…>] add a recipient to THE GATE (audience.toml)
|
|
12600
|
+
sol env audience rm <handle> [<account>] remove an account (or the whole handle)
|
|
12601
|
+
sol env audience ls [<handle>] list the gate (or one handle's recipients)
|
|
12602
|
+
|
|
12603
|
+
a HANDLE is role:NAME | team:NAME | machine:NAME. the account's X25519 PUBLIC key is sourced, in order:
|
|
12604
|
+
--pubkey <sol1pk_…> an explicit key (offline / not-yet-published)
|
|
12605
|
+
your local keystore when <account> is YOUR OWN identity (no network)
|
|
12606
|
+
the key directory GET /keys/<account> (host-blind, public material only — they must \`sol keys publish\` first)
|
|
12607
|
+
|
|
12608
|
+
this is the KEYSTONE: declare -> set -> seal -> reveal works WITHOUT hand-editing audience.toml.
|
|
12609
|
+
adding/removing here changes WHO a future \`sol secret set\` seals to; reseal existing values with \`sol secret audience\`.
|
|
12610
|
+
|
|
12611
|
+
examples:
|
|
12612
|
+
sol env audience add machine:runtime-prod machine:runtime-prod # source my own published pubkey
|
|
12613
|
+
sol env audience add team:payments alice@acme # fetch alice's key from the directory
|
|
12614
|
+
sol env audience add role:deploy ci@acme --pubkey sol1pk_9af… # pin an explicit key offline
|
|
12615
|
+
sol env audience ls team:payments`,
|
|
12616
|
+
secret: `sol secret <subcommand> per-secret lifecycle. declare/ref/list/audit are AGENT-SAFE; set/reveal/inject/export touch values
|
|
12617
|
+
|
|
12618
|
+
schema-side (agent-safe — touch the schema/gate metadata, never a value):
|
|
12619
|
+
sol secret declare NAME --env E [--audience h1,h2] [--type T] [--hide-name] add an UNSET slot (no value)
|
|
12620
|
+
sol secret ref NAME --env E print the sol:// handle for code/config (never a value)
|
|
12621
|
+
sol secret list --env E names + audience + set? (NEVER values)
|
|
12622
|
+
sol secret config set K V --env E plaintext config (rejected if K is a declared secret)
|
|
12623
|
+
sol secret audit --env E journal-authentic who-can-decrypt + drift (== \`sol env audit E\`; identifiers only)
|
|
12624
|
+
|
|
12625
|
+
value-bearing (recipient-gated / human side-channel — the agent holds no key):
|
|
12626
|
+
sol secret set NAME --env E seal a value from a SECURE SIDE-CHANNEL (no-echo TTY / --stdin / --fd) — NEVER argv
|
|
12627
|
+
sol secret reveal NAME --env E recipient-gated read; a non-recipient / the host / the agent is refused
|
|
12628
|
+
sol secret inject --env E -- cmd decrypt MY readable secrets into a SUBPROCESS env only (masked from this process)
|
|
12629
|
+
sol secret export --env E DEPLOY/BOOT producer: the runtime decrypts EVERY secret it's a recipient of to a
|
|
12630
|
+
NAME=value map -> a SECURE SINK (--fd / non-TTY). inject (run a cmd) vs export
|
|
12631
|
+
(materialize the map). see \`sol secret export --help\`.
|
|
12632
|
+
|
|
12633
|
+
management (operator-authenticated — SIGNED gate ops; need SOL_RECOVERY_CODE):
|
|
12634
|
+
sol secret audience add|rm NAME --env E <handle> change a secret's audience + AUTO-RESEAL (recipient-only)
|
|
12635
|
+
sol secret runtime provision --env E [--fd N] mint + add the env's machine:runtime-<E> recipient; see \`--help\`
|
|
12636
|
+
sol secret runtime ls | show --env E the manifest runtime binding (agent-safe)
|
|
12637
|
+
|
|
12638
|
+
agent (no value ever):
|
|
12639
|
+
sol secret mcp serve the AGENT-SAFE secret MCP over stdio (== \`sol mcp --secret\`) — the 14
|
|
12640
|
+
value-free env/secret tools; an agent declares/refs/lists/audits + triggers
|
|
12641
|
+
deploys WITHOUT ever seeing a value. register: \`claude mcp add sol-secrets -- sol secret mcp\`
|
|
12642
|
+
|
|
12643
|
+
(sol secret <sub> --help for a focused page.)`,
|
|
12644
|
+
"secret set": `sol secret set NAME --env E seal a value to the env audience, CLIENT-SIDE (agent-blind, host-blind)
|
|
12645
|
+
|
|
12646
|
+
the value comes from a SECURE SIDE-CHANNEL, in priority order — NEVER from argv (which lands in ps/history/the
|
|
12647
|
+
agent's tool-call record):
|
|
12648
|
+
--fd <n> read from an inherited file descriptor (orchestrator path)
|
|
12649
|
+
--stdin read from piped stdin (CI path)
|
|
12650
|
+
(TTY) a no-echo prompt (human path)
|
|
12651
|
+
|
|
12652
|
+
write-authz: declaring a slot is open, but OVERWRITING an already-sealed value requires you to be a CURRENT
|
|
12653
|
+
RECIPIENT of that secret — a non-recipient (incl. the agent) cannot clobber a value it can't read. a \`set\` on an
|
|
12654
|
+
undeclared name auto-declares the slot first.
|
|
12655
|
+
|
|
12656
|
+
examples:
|
|
12657
|
+
sol secret set STRIPE_KEY --env production # no-echo TTY prompt
|
|
12658
|
+
printf %s "$VALUE" | sol secret set STRIPE_KEY --env production --stdin # CI/pipe`,
|
|
12659
|
+
"secret declare": `sol secret declare NAME --env E [--audience h1,h2] [--type T] [--hide-name] add an UNSET secret slot
|
|
12660
|
+
|
|
12661
|
+
AGENT-SAFE: touches the SCHEMA only, never a value. the slot is sealed-PENDING until \`sol secret set\` supplies a value.
|
|
12662
|
+
--audience h1,h2 the seal audience handles (else the env file's @audience). resolve through audience.toml.
|
|
12663
|
+
--type T an advisory shape hint ("string" | "url" | …)
|
|
12664
|
+
--hide-name name-hiding opt-up: the real name lives only in the sealed body
|
|
12665
|
+
|
|
12666
|
+
example:
|
|
12667
|
+
sol secret declare DATABASE_URL --env staging --audience machine:runtime-staging --type url`,
|
|
12668
|
+
"secret reveal": `sol secret reveal NAME --env E [--field F] print a secret's value — RECIPIENT-ONLY
|
|
12669
|
+
|
|
12670
|
+
refuses for a non-recipient, the host, and the agent (they hold no audience key — UNREADABLE by construction, not a
|
|
12671
|
+
policy flag). a corrupted/tampered leaf is reported as a DECRYPT failure, distinct from "not a recipient".
|
|
12672
|
+
--field F project a #field out of a multi-value (JSON) secret`,
|
|
12673
|
+
"secret inject": `sol secret inject --env E -- <cmd> [args...] run <cmd> with MY readable secrets in its env
|
|
12674
|
+
|
|
12675
|
+
decrypts only the secrets I am a recipient of into the SUBPROCESS environment — they never touch this process's
|
|
12676
|
+
stdout and are never echoed. a non-recipient sees an empty injection (refused). name-hidden secrets are skipped
|
|
12677
|
+
(no env-var name to inject under).
|
|
12678
|
+
|
|
12679
|
+
example:
|
|
12680
|
+
SOL_RECOVERY_CODE=… sol secret inject --env production -- wrangler deploy`,
|
|
12681
|
+
"secret audience": `sol secret audience add|rm NAME --env E <handle> change ONE secret's audience + AUTO-RESEAL
|
|
12682
|
+
|
|
12683
|
+
re-wraps the sealed value to the new audience. requires YOU to be a CURRENT RECIPIENT (you decrypt, then re-seal —
|
|
12684
|
+
the host holds no key); a non-recipient is refused. bumps the seal epoch.
|
|
12685
|
+
|
|
12686
|
+
example:
|
|
12687
|
+
sol secret audience add STRIPE_KEY --env production team:payments`,
|
|
12688
|
+
"secret config": `sol secret config set KEY VAL --env E set a PLAINTEXT config value
|
|
12689
|
+
|
|
12690
|
+
rejected if KEY is a declared secret (use \`sol secret set\` for those). config is agent- and host-visible by design.`,
|
|
12691
|
+
"secret list": `sol secret list --env E [--json] names + audience + set? for an env — AGENT-SAFE, never values`,
|
|
12692
|
+
"secret ref": `sol secret ref NAME --env E print the sol:// handle for code/config (e.g. sol://production/STRIPE_KEY) — never a value`,
|
|
12693
|
+
"secret export": `sol secret export --env E [--format dotenv|json] [--fd N] DEPLOY/BOOT-time value producer (recipient-gated)
|
|
12694
|
+
|
|
12695
|
+
re-derives the RUNTIME recipient's private key from its bootstrap root (SOL_RUNTIME_RECOVERY_CODE, the env-scoped
|
|
12696
|
+
seed seeded on the deploy box / read at boot) and decrypts EVERY secret in --env the runtime is a recipient of into
|
|
12697
|
+
a NAME=value map. this is the producer for the deploy secrets payload (Mode A) and the boot-time process env (Mode B).
|
|
12698
|
+
falls back to the CALLER's own identity when no runtime root is present (a human recipient running export locally).
|
|
12699
|
+
|
|
12700
|
+
inject vs export:
|
|
12701
|
+
inject RUNS a command with the values in its SUBPROCESS env (ephemeral; never materialized to a file)
|
|
12702
|
+
export MATERIALIZES the NAME=value map to a sink the deploy/boot consumes (a secrets file / an inherited fd)
|
|
12703
|
+
|
|
12704
|
+
SINK GATING (value-bearing — never a log/TTY):
|
|
12705
|
+
--fd <n> write the rendered map to an inherited file descriptor (the orchestrator/secrets-file path)
|
|
12706
|
+
(non-TTY) a piped/redirected stdout is allowed; an interactive TTY is REFUSED (so a value can't land onscreen)
|
|
12707
|
+
--format dotenv (NAME='value', default) | json (a compact object)
|
|
12708
|
+
|
|
12709
|
+
the agent (in no audience, holding no runtime root) cannot run this to read a value.
|
|
12710
|
+
|
|
12711
|
+
examples:
|
|
12712
|
+
sol secret export --env production --fd 3 3>secrets.out # deploy: materialize to a secrets file
|
|
12713
|
+
SOL_RUNTIME_RECOVERY_CODE=… sol secret export --env production --format json --fd 3 3>env.json`,
|
|
12714
|
+
"secret audit": `sol secret audit --env E [--json] journal-authentic who-can-decrypt + drift (identifiers only)
|
|
12715
|
+
|
|
12716
|
+
== \`sol env audit E\`. who-can-decrypt expands each secret's audience through the gate REPLAYED from the SIGNED gate
|
|
12717
|
+
ops (not the hand-editable audience.toml); drift names handles whose file membership disagrees with the journal-
|
|
12718
|
+
authored set, plus the journal trust verdict. agent-safe: identifiers + verdict only, never a value.`,
|
|
12719
|
+
"secret runtime": `sol secret runtime provision --env E [--fd N] [--json] PROVISION the env's machine recipient (machine:runtime-<E>)
|
|
12720
|
+
sol secret runtime ls [--json] the manifest runtime binding per env (agent-safe)
|
|
12721
|
+
sol secret runtime show --env E [--json] one env's runtime binding + whether it's in the authentic gate
|
|
12722
|
+
|
|
12723
|
+
PHASE B — the runtime is a RECIPIENT. \`provision\` mints the env's BOOTSTRAP ROOT (a recovery code — the runtime's
|
|
12724
|
+
standing decryptor), derives its X25519 identity, adds machine:runtime-<E> to the env audience as a SIGNED gate op,
|
|
12725
|
+
and records manifest.runtime[E]. it prints ONLY public material + the custody instruction; the ROOT goes to a
|
|
12726
|
+
SECURE SINK and is NEVER shown on a TTY or logged.
|
|
12727
|
+
|
|
12728
|
+
operator-authenticated: \`provision\` is a signed gate op — needs SOL_RECOVERY_CODE (a keyless agent is refused).
|
|
12729
|
+
the ROOT sink (value-bearing):
|
|
12730
|
+
--fd <n> write the bootstrap root to an inherited fd (custody it write-only in your runtime secret store)
|
|
12731
|
+
(non-TTY) a piped/redirected stdout is allowed; an interactive TTY is REFUSED
|
|
12732
|
+
custody the root WRITE-ONLY (e.g. the CF Secrets Store binding env.SOL_RUNTIME_ROOT); the runtime reads it at boot
|
|
12733
|
+
to \`sol secret export\`. the agent/host never holds it.
|
|
12734
|
+
|
|
12735
|
+
example:
|
|
12736
|
+
SOL_RECOVERY_CODE=… sol secret runtime provision --env production --fd 3 3>runtime-root.secret`,
|
|
12737
|
+
"secret mcp": `sol secret mcp serve the AGENT-SAFE secret-manager MCP over stdio (identical to \`sol mcp --secret\`)
|
|
12738
|
+
|
|
12739
|
+
exposes the 14 value-free env/secret tools (env_list/show/schema/diff/validate, secret_declare/ref/list/audit,
|
|
12740
|
+
secret_audience_ls/add/rm, deploy_trigger, secret_export_trigger) so an agent manages env + secrets across ALL
|
|
12741
|
+
environments WITHOUT ever seeing a value. set/reveal/inject/export are ABSENT by construction; every result passes
|
|
12742
|
+
the value-blind egress sanitizer. the server runs as the AGENT identity (keyless) unless an operator sets
|
|
12743
|
+
SOL_RECOVERY_CODE (which only unlocks SIGNED gate edits, never a value op).
|
|
12744
|
+
|
|
12745
|
+
register:
|
|
12746
|
+
claude mcp add sol-secrets -- sol secret mcp # the INSTALLED binary serves it (no separate bin on PATH)`,
|
|
12747
|
+
mcp: `sol mcp [--secret] [--http [--port N] [--host H] [--token T]] serve a sol MCP server
|
|
12748
|
+
|
|
12749
|
+
sol mcp the file/VCS AUTHORING server (sol_write/read/edit/ls/move/rm/commit/history) — an agent
|
|
12750
|
+
authors directly into the op-log + content store, no working copy.
|
|
12751
|
+
sol mcp --secret the AGENT-SAFE SECRET-MANAGER server (== \`sol secret mcp\`) — 14 value-free env/secret tools.
|
|
12752
|
+
|
|
12753
|
+
transport: stdio is the DEFAULT (local pipe). \`--http\` serves the SAME tool surface over the MCP Streamable HTTP
|
|
12754
|
+
transport — the enabler for hosting it on a remote. an HTTP MCP is a NETWORK endpoint, so it requires a bearer
|
|
12755
|
+
token: set SOL_MCP_TOKEN (or pass --token <T>) and every request must send \`Authorization: Bearer <token>\` (else
|
|
12756
|
+
401). it serves on http://127.0.0.1:8765/mcp by default; override with --host / --port.
|
|
12757
|
+
|
|
12758
|
+
register: \`claude mcp add sol -- sol mcp\` | \`claude mcp add sol-secrets -- sol mcp --secret\`
|
|
12759
|
+
http: \`SOL_MCP_TOKEN=… sol mcp --http --port 8765\` (then point an HTTP MCP client at http://HOST:PORT/mcp)`,
|
|
12760
|
+
resolve: `sol resolve <sol://env/NAME[#field]> resolve a sol:// reference to its value (CLIENT-SIDE, recipient-gated)
|
|
12761
|
+
|
|
12762
|
+
sol resolve sol://production/STRIPE_KEY
|
|
12763
|
+
sol resolve sol://production/DATABASE_URL#password # project a JSON sub-field
|
|
12764
|
+
sol resolve sol://$ENV/DB --env production # $ENV resolved from --env / SOL_ENV
|
|
12765
|
+
|
|
12766
|
+
refused for a non-recipient (host/agent-blind). a missing or malformed reference is a usage error, not a crash.`
|
|
12767
|
+
};
|
|
12768
|
+
if (wantsHelp && argv[0]) {
|
|
12769
|
+
const twoKey = argv[1] ? `${argv[0]} ${argv[1]}` : undefined;
|
|
12770
|
+
const page = twoKey && SUBHELP[twoKey] || SUBHELP[argv[0]];
|
|
12771
|
+
if (page) {
|
|
12772
|
+
console.log(page);
|
|
12773
|
+
return;
|
|
12774
|
+
}
|
|
12775
|
+
}
|
|
12776
|
+
const cmd = argv[0] && wantsHelp ? "help" : argv[0];
|
|
12777
|
+
if (!process.env.SOL_TOKEN && new Set(["clone", "push", "pull", "promote", "fork", "forks", "mr", "access", "keys", "hide", "seal"]).has(cmd ?? "")) {
|
|
12778
|
+
const t = await loadStoredToken2();
|
|
12779
|
+
if (t)
|
|
12780
|
+
process.env.SOL_TOKEN = t;
|
|
12781
|
+
}
|
|
12782
|
+
if (new Set(["clone", "pull", "switch", "merge", "restore", "checkout", "fork", "view", "diff", "show", "blame", "log"]).has(cmd ?? "")) {
|
|
12783
|
+
try {
|
|
12784
|
+
const { loadSelfIdentity: loadSelfIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
12785
|
+
const { openContent: openContent2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
12786
|
+
const ring = existsSync18(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
|
|
12787
|
+
const self = loadSelfIdentity2();
|
|
12788
|
+
setSealedDecryptor((boxStr) => {
|
|
12789
|
+
const box = JSON.parse(boxStr);
|
|
12790
|
+
const opened = openContent2(ring, box, actor, self);
|
|
12791
|
+
return opened === "<<unreadable>>" ? undefined : opened;
|
|
12792
|
+
});
|
|
12793
|
+
} catch {}
|
|
12794
|
+
}
|
|
12795
|
+
const servesMcp = cmd === "mcp" || cmd === "secret" && args[0] === "mcp";
|
|
12796
|
+
const release = !servesMcp && new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "undo", "revert", "pull", "push", "restore", "checkout", "run", "seal", "view", "env", "secret"]).has(cmd) && existsSync18(solDir) ? acquireLock() : undefined;
|
|
12797
|
+
try {
|
|
12798
|
+
switch (cmd) {
|
|
12799
|
+
case "init": {
|
|
12800
|
+
const here = join19(procCwd, ".sol");
|
|
12801
|
+
if (existsSync18(here))
|
|
12802
|
+
die("already a sol repo: " + procCwd);
|
|
12803
|
+
if (repoRoot && repoRoot !== procCwd && !args.includes("--force")) {
|
|
12804
|
+
die(`already inside a Sol repo at ${repoRoot}
|
|
12805
|
+
-> just commit into it: \`sol commit ...\` works from here (sol walks up to find the repo)
|
|
12806
|
+
-> to nest a NEW repo here anyway: \`sol init --force\``);
|
|
12807
|
+
}
|
|
12808
|
+
mkdirSync12(here, { recursive: true });
|
|
12809
|
+
new FileStore(here);
|
|
12810
|
+
console.log(`initialized empty sol repo in ${here}`);
|
|
12811
|
+
break;
|
|
12812
|
+
}
|
|
12813
|
+
case "keygen": {
|
|
12814
|
+
const { generateKeypair: generateKeypair2 } = await Promise.resolve().then(() => (init_sign(), exports_sign));
|
|
12815
|
+
const kp = generateKeypair2();
|
|
12816
|
+
if (args.includes("--json")) {
|
|
12817
|
+
console.log(JSON.stringify({ seed: kp.seed, pub: kp.pub, fingerprint: kp.fingerprint }));
|
|
12818
|
+
break;
|
|
12819
|
+
}
|
|
12820
|
+
console.log(`fingerprint: ${kp.fingerprint}`);
|
|
12821
|
+
console.log(`public key: ${kp.pub}`);
|
|
12822
|
+
console.log(`SOL_SIGNING_KEY=${kp.seed}`);
|
|
12823
|
+
console.log(`
|
|
12824
|
+
# provision the agent with the seed above, then trust it (use the single fingerprint printed above):
|
|
12825
|
+
# sol trust <agent-name> <fingerprint>`);
|
|
12826
|
+
break;
|
|
12827
|
+
}
|
|
12828
|
+
case "keys": {
|
|
12829
|
+
const { deriveIdentity: deriveIdentity2, generateRecoveryCode: generateRecoveryCode2, pubFingerprint: pubFingerprint2, signPubkey: signPubkey2, verifyPubkeySig: verifyPubkeySig2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
12830
|
+
const { exportBundle: exportBundle2, importBundle: importBundle2, loadIdentity: loadIdentity2, pinVerified: pinVerified2, saveIdentity: saveIdentity2 } = await Promise.resolve().then(() => (init_identity_store(), exports_identity_store));
|
|
12831
|
+
const accountOf = async () => {
|
|
12832
|
+
if (process.env.SOL_ACCOUNT)
|
|
12833
|
+
return process.env.SOL_ACCOUNT;
|
|
12834
|
+
const tok = process.env.SOL_TOKEN || await loadStoredToken2();
|
|
12835
|
+
const h = tok ? identityFromToken2(tok)?.handle : undefined;
|
|
12836
|
+
return h || die("no account identity — run `sol auth login` (or set SOL_ACCOUNT for self-host)");
|
|
12837
|
+
};
|
|
12838
|
+
const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL2).replace(/\/+$/, "");
|
|
12839
|
+
const sub = args[0];
|
|
12840
|
+
const wantJson = args.includes("--json");
|
|
12841
|
+
if (sub === "init") {
|
|
12842
|
+
if (loadIdentity2() && !args.includes("--force"))
|
|
12843
|
+
die("identity already exists — `sol keys rotate` to roll the epoch, or `sol keys init --force` to replace");
|
|
12844
|
+
const accountId = await accountOf();
|
|
12845
|
+
const recoveryCode = generateRecoveryCode2();
|
|
12846
|
+
const id = deriveIdentity2(accountId, recoveryCode, 1);
|
|
12847
|
+
const fingerprint = pubFingerprint2(id.x25519Pub);
|
|
12848
|
+
saveIdentity2({ accountId, x25519Pub: id.x25519Pub, edPub: id.edPub, keyEpoch: 1, fingerprint, createdAt: Date.now() });
|
|
12849
|
+
if (wantJson) {
|
|
12850
|
+
console.log(JSON.stringify({ accountId, keyEpoch: 1, fingerprint, recoveryCode }));
|
|
12851
|
+
break;
|
|
12852
|
+
}
|
|
12853
|
+
console.log(`identity minted for @${accountId} (X25519, epoch 1) — host-blind: the private key is NEVER stored or sent.`);
|
|
12854
|
+
console.log(`fingerprint: ${fingerprint}`);
|
|
12855
|
+
console.log(`
|
|
12856
|
+
RECOVERY CODE (shown ONCE — write it down, it is the ONLY way to recover this key on a new device):
|
|
12857
|
+
`);
|
|
12858
|
+
console.log(` ${recoveryCode}
|
|
12859
|
+
`);
|
|
12860
|
+
console.log(`next: \`sol keys publish\` to share your public key so others can seal to you.`);
|
|
12861
|
+
break;
|
|
12862
|
+
}
|
|
12863
|
+
if (sub === "publish" || sub === "rotate") {
|
|
12864
|
+
const stored2 = loadIdentity2() || die("no identity yet — run `sol keys init` first");
|
|
12865
|
+
const accountId = stored2.accountId;
|
|
12866
|
+
const recoveryCode = process.env.SOL_RECOVERY_CODE || die("set SOL_RECOVERY_CODE to your recovery code (the private key is re-derived from it; it is never stored)");
|
|
12867
|
+
const keyEpoch = sub === "rotate" ? stored2.keyEpoch + 1 : stored2.keyEpoch;
|
|
12868
|
+
const id = deriveIdentity2(accountId, recoveryCode, keyEpoch);
|
|
12869
|
+
if (sub === "publish" && id.x25519Pub !== stored2.x25519Pub)
|
|
12870
|
+
die("recovery code does not match this identity (derived a different key)");
|
|
12871
|
+
const sig = signPubkey2(id);
|
|
12872
|
+
const token = process.env.SOL_TOKEN || await loadStoredToken2() || authExpired2();
|
|
12873
|
+
const res = await fetch(`${dirUrl}/keys`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ accountId, x25519Pub: id.x25519Pub, edPub: id.edPub, keyEpoch, sig }) });
|
|
12874
|
+
if (!res.ok)
|
|
12875
|
+
die(`publish failed: ${res.status} ${(await res.text().catch(() => "")).slice(0, 200)}`);
|
|
12876
|
+
if (sub === "rotate")
|
|
12877
|
+
saveIdentity2({ ...stored2, x25519Pub: id.x25519Pub, edPub: id.edPub, keyEpoch, fingerprint: pubFingerprint2(id.x25519Pub) });
|
|
12878
|
+
if (wantJson) {
|
|
12879
|
+
console.log(JSON.stringify({ accountId, keyEpoch, fingerprint: pubFingerprint2(id.x25519Pub), published: true }));
|
|
12880
|
+
break;
|
|
12881
|
+
}
|
|
12882
|
+
console.log(`published @${accountId} pubkey (epoch ${keyEpoch}) to the directory — public key only; the host stays blind.`);
|
|
12883
|
+
if (sub === "rotate")
|
|
12884
|
+
console.log(" NOTE: past sealed content stays wrapped to the OLD epoch — `sol seal --reapply` re-wraps it (no-recall until then).");
|
|
12885
|
+
break;
|
|
12886
|
+
}
|
|
12887
|
+
if (sub === "verify") {
|
|
12888
|
+
const acct = args[1] || die("usage: sol keys verify <accountId> <fingerprint>");
|
|
12889
|
+
const claimed = args[2] || die("usage: sol keys verify <accountId> <fingerprint>");
|
|
12890
|
+
const res = await fetch(`${dirUrl}/keys/${encodeURIComponent(acct)}`);
|
|
12891
|
+
if (res.status === 404)
|
|
12892
|
+
die(`no published key for @${acct} (ask them to run \`sol keys publish\`)`);
|
|
12893
|
+
if (!res.ok)
|
|
12894
|
+
die(`directory lookup failed: ${res.status}`);
|
|
12895
|
+
const entry = await res.json();
|
|
12896
|
+
const fp = pubFingerprint2(entry.x25519Pub);
|
|
12897
|
+
const sigOk = verifyPubkeySig2(entry);
|
|
12898
|
+
const match = fp.replace(/-/g, "").toLowerCase() === claimed.replace(/[-:\s]/g, "").toLowerCase();
|
|
12899
|
+
if (match && sigOk)
|
|
12900
|
+
pinVerified2(acct, fp);
|
|
12901
|
+
if (wantJson) {
|
|
12902
|
+
console.log(JSON.stringify({ accountId: acct, fingerprint: fp, keyEpoch: entry.keyEpoch, selfSigned: sigOk, fingerprintMatch: match, verified: match && sigOk }));
|
|
12903
|
+
break;
|
|
12904
|
+
}
|
|
12905
|
+
console.log(`@${acct} (epoch ${entry.keyEpoch})`);
|
|
12906
|
+
console.log(` directory fingerprint: ${fp}`);
|
|
12907
|
+
console.log(` self-signature: ${sigOk ? "valid ✓" : "INVALID ✗ (possible MITM — do NOT trust)"}`);
|
|
12908
|
+
console.log(` your fingerprint: ${match ? "MATCH ✓ (pinned, trust-on-first-use)" : "MISMATCH ✗ — the directory key differs from what you were told"}`);
|
|
12909
|
+
if (!(match && sigOk))
|
|
12910
|
+
process.exitCode = 1;
|
|
12911
|
+
break;
|
|
12912
|
+
}
|
|
12913
|
+
if (sub === "export") {
|
|
12914
|
+
const stored2 = loadIdentity2() || die("no identity to export — `sol keys init` first");
|
|
12915
|
+
const recoveryCode = process.env.SOL_RECOVERY_CODE || die("set SOL_RECOVERY_CODE — the export wraps it under a passphrase (the private key is never stored in the clear)");
|
|
12916
|
+
const passphrase = process.env.SOL_KEYSTORE_PASSPHRASE || die("set SOL_KEYSTORE_PASSPHRASE to encrypt the backup bundle");
|
|
12917
|
+
const bundle = exportBundle2(stored2.accountId, stored2.keyEpoch, recoveryCode, passphrase);
|
|
12918
|
+
console.log(JSON.stringify(bundle, null, 2));
|
|
12919
|
+
break;
|
|
12920
|
+
}
|
|
12921
|
+
if (sub === "import") {
|
|
12922
|
+
const passphrase = process.env.SOL_KEYSTORE_PASSPHRASE || die("set SOL_KEYSTORE_PASSPHRASE to decrypt the bundle");
|
|
12923
|
+
const file = args[1] || die("usage: sol keys import <bundle.json> (set SOL_KEYSTORE_PASSPHRASE)");
|
|
12924
|
+
const bundle = JSON.parse(readFileSync18(file, "utf8"));
|
|
12925
|
+
let recovered;
|
|
12926
|
+
try {
|
|
12927
|
+
recovered = importBundle2(bundle, passphrase);
|
|
12928
|
+
} catch {
|
|
12929
|
+
die("import failed — wrong passphrase or a tampered bundle");
|
|
12930
|
+
}
|
|
12931
|
+
const id = deriveIdentity2(recovered.accountId, recovered.recoveryCode, recovered.keyEpoch);
|
|
12932
|
+
saveIdentity2({ accountId: recovered.accountId, x25519Pub: id.x25519Pub, edPub: id.edPub, keyEpoch: recovered.keyEpoch, fingerprint: pubFingerprint2(id.x25519Pub), createdAt: Date.now() });
|
|
12933
|
+
console.log(`imported identity for @${recovered.accountId} (epoch ${recovered.keyEpoch}) — re-derived locally; the recovery code stays yours.`);
|
|
12934
|
+
console.log(` RECOVERY CODE (keep it safe):
|
|
12935
|
+
|
|
12936
|
+
${recovered.recoveryCode}
|
|
12937
|
+
`);
|
|
12938
|
+
break;
|
|
12939
|
+
}
|
|
12940
|
+
const stored = loadIdentity2();
|
|
12941
|
+
if (!stored) {
|
|
12942
|
+
console.log("no identity yet — `sol keys init` mints your X25519 identity keypair (host-blind).");
|
|
12943
|
+
break;
|
|
12944
|
+
}
|
|
12945
|
+
if (wantJson) {
|
|
12946
|
+
console.log(JSON.stringify({ accountId: stored.accountId, keyEpoch: stored.keyEpoch, fingerprint: stored.fingerprint, x25519Pub: stored.x25519Pub }));
|
|
12947
|
+
break;
|
|
12948
|
+
}
|
|
12949
|
+
console.log(`identity: @${stored.accountId} (epoch ${stored.keyEpoch})`);
|
|
12950
|
+
console.log(` fingerprint: ${stored.fingerprint}`);
|
|
12951
|
+
console.log(` the private key is NOT stored — it re-derives from your recovery code (host stays blind).`);
|
|
12952
|
+
break;
|
|
12953
|
+
}
|
|
12954
|
+
case "trust": {
|
|
12955
|
+
if (!existsSync18(solDir))
|
|
12956
|
+
die("not a sol repo — run `sol init` first");
|
|
12957
|
+
const map = loadTrust();
|
|
12958
|
+
if (args[0] === "--remove" || args[0] === "-r") {
|
|
12959
|
+
const name2 = args[1] || die("trust --remove needs an actor name");
|
|
12960
|
+
delete map[name2];
|
|
12961
|
+
saveTrust(map);
|
|
12962
|
+
console.log(`untrusted ${name2}`);
|
|
12963
|
+
break;
|
|
12964
|
+
}
|
|
12965
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
12966
|
+
if (!positional.length) {
|
|
12967
|
+
if (args.includes("--json")) {
|
|
12968
|
+
const trusted = Object.entries(map).map(([actor2, fingerprint2]) => ({ actor: actor2, fingerprint: fingerprint2 }));
|
|
12969
|
+
console.log(JSON.stringify({ count: trusted.length, trusted, map }));
|
|
12970
|
+
} else {
|
|
12971
|
+
const names = Object.keys(map);
|
|
12972
|
+
if (!names.length)
|
|
12973
|
+
console.log("(no trusted actors yet — `sol trust <name> <fingerprint>`)");
|
|
12974
|
+
for (const n of names)
|
|
12975
|
+
console.log(`${n.padEnd(20)} ${map[n]}`);
|
|
12976
|
+
}
|
|
12977
|
+
break;
|
|
12978
|
+
}
|
|
12979
|
+
const name = positional[0];
|
|
12980
|
+
const fp = positional[1] || die("trust needs: sol trust <actor-name> <fingerprint>");
|
|
12981
|
+
const { fingerprintOf: fingerprintOf2, normalizeFingerprint: normalizeFingerprint2 } = await Promise.resolve().then(() => (init_sign(), exports_sign));
|
|
12982
|
+
let fingerprint;
|
|
12983
|
+
if (/sol1:/i.test(fp)) {
|
|
12984
|
+
const norm = normalizeFingerprint2(fp);
|
|
12985
|
+
if (!norm)
|
|
12986
|
+
die(`malformed fingerprint "${fp}" — expected sol1: followed by exactly 16 hex chars (e.g. sol1:6944abd09081aeae). check for a doubled/duplicated value or stray characters.`);
|
|
12987
|
+
fingerprint = norm;
|
|
12988
|
+
} else {
|
|
12989
|
+
try {
|
|
12990
|
+
fingerprint = fingerprintOf2(fp);
|
|
12991
|
+
} catch {
|
|
12992
|
+
die(`"${fp}" is neither a sol1: fingerprint nor a valid public key — pass the fingerprint from \`sol keygen\` (sol1:…) or the recipient's public key.`);
|
|
12993
|
+
}
|
|
12994
|
+
}
|
|
12995
|
+
map[name] = fingerprint;
|
|
12996
|
+
saveTrust(map);
|
|
12997
|
+
console.log(`trusted ${name} = ${fingerprint}`);
|
|
12998
|
+
break;
|
|
12999
|
+
}
|
|
13000
|
+
case "track":
|
|
13001
|
+
case "add": {
|
|
13002
|
+
if (!existsSync18(solDir))
|
|
13003
|
+
die("not a sol repo — run `sol init` first");
|
|
13004
|
+
const files = args.filter((a) => a !== "." && !a.startsWith("-"));
|
|
13005
|
+
if (!files.length) {
|
|
13006
|
+
console.log('every change is auto-captured — just `sol commit`. Sol has no staging area, so there is nothing to "add".');
|
|
13007
|
+
console.log("use `sol track <ignored-file>` only to force-track a normally-ignored path.");
|
|
13008
|
+
break;
|
|
13009
|
+
}
|
|
13010
|
+
let n = 0;
|
|
13011
|
+
for (const f of files) {
|
|
13012
|
+
const rf = repoRel(f);
|
|
13013
|
+
if (!existsSync18(join19(cwd, rf))) {
|
|
13014
|
+
console.error("skip (not on disk): " + f);
|
|
13015
|
+
continue;
|
|
13016
|
+
}
|
|
13017
|
+
addInclude(rf);
|
|
13018
|
+
n++;
|
|
13019
|
+
}
|
|
13020
|
+
console.log(`will track ${n} path(s) on the next commit`);
|
|
13021
|
+
break;
|
|
13022
|
+
}
|
|
13023
|
+
case "commit":
|
|
13024
|
+
case "checkpoint": {
|
|
13025
|
+
const { repo, log } = open();
|
|
13026
|
+
let message = "";
|
|
13027
|
+
const paths = [];
|
|
13028
|
+
const wholeTreeOptIn = args.includes("--whole-tree");
|
|
13029
|
+
const ca = args.filter((x) => x !== "--whole-tree" && x !== "--force");
|
|
13030
|
+
const mi = ca.indexOf("-m");
|
|
13031
|
+
if (mi >= 0) {
|
|
13032
|
+
message = ca[mi + 1] ?? "";
|
|
13033
|
+
for (let i = 0;i < ca.length; i++)
|
|
13034
|
+
if (i !== mi && i !== mi + 1)
|
|
13035
|
+
paths.push(ca[i]);
|
|
13036
|
+
} else {
|
|
13037
|
+
message = ca.join(" ");
|
|
13038
|
+
}
|
|
13039
|
+
if (!message)
|
|
13040
|
+
die('commit needs a message: sol commit "what you did" (scoped: sol commit -m "msg" file1 file2)');
|
|
13041
|
+
const parentHead = await repo.head();
|
|
13042
|
+
const mergeHeadPath = join19(solDir, "MERGE_HEAD");
|
|
13043
|
+
const parent2 = existsSync18(mergeHeadPath) ? readFileSync18(mergeHeadPath, "utf8").trim() || undefined : undefined;
|
|
13044
|
+
let changed = 0;
|
|
13045
|
+
let commitRoot = parentHead;
|
|
13046
|
+
if (paths.length) {
|
|
13047
|
+
for (const p of paths) {
|
|
13048
|
+
const rp = repoRel(p);
|
|
13049
|
+
if (existsSync18(join19(cwd, rp))) {
|
|
13050
|
+
if (await snapshotFile(repo, rp))
|
|
13051
|
+
changed++;
|
|
13052
|
+
} else if ((await repo.list()).includes(rp)) {
|
|
13053
|
+
await repo.deleteFile(rp);
|
|
13054
|
+
changed++;
|
|
13055
|
+
} else {
|
|
13056
|
+
console.error("skip (not on disk, not tracked): " + p);
|
|
13057
|
+
}
|
|
13058
|
+
}
|
|
13059
|
+
commitRoot = await repo.head();
|
|
13060
|
+
} else {
|
|
13061
|
+
const optedIn = wholeTreeOptIn || process.env.SOL_ALLOW_WHOLE_TREE === "1";
|
|
13062
|
+
const otherActors = [...new Set((await log.history()).map((o) => o.by).filter((b) => !!b && b !== actor))];
|
|
13063
|
+
const otherActorPresent = otherActors.length > 0;
|
|
13064
|
+
if (process.env.SOL_ACTOR && !optedIn && otherActorPresent) {
|
|
13065
|
+
const others = otherActors.join(", ");
|
|
13066
|
+
die(`refusing a whole-tree commit as "${actor}" — other author(s) have committed here (${others}), so sweeping the whole tree would mis-attribute their pending files to you. (this guard only fires because they're present; a solo author commits the whole tree freely.)
|
|
13067
|
+
scope to your files: sol commit -m "${message}" <your files> (or a per-agent SolWorkspace/MCP)
|
|
13068
|
+
if you truly own the whole tree: add --whole-tree or set SOL_ALLOW_WHOLE_TREE=1`);
|
|
13069
|
+
}
|
|
13070
|
+
const snap = await snapshotTree(repo);
|
|
13071
|
+
changed = snap.changed;
|
|
13072
|
+
commitRoot = snap.root;
|
|
13073
|
+
if (!process.env.SOL_ACTOR && changed > 1) {
|
|
13074
|
+
console.error(`note: whole-tree commit attributes all ${changed} pending files to ${actor}. for concurrent agents, prefer \`sol commit -m "msg" <files>\`.`);
|
|
13075
|
+
}
|
|
13076
|
+
}
|
|
13077
|
+
if (!args.includes("--force")) {
|
|
13078
|
+
const cstore = loadStore();
|
|
13079
|
+
const cd = diffTrees(cstore, parentHead, commitRoot);
|
|
13080
|
+
const changedPaths = paths.length ? paths.map((p) => repoRel(p)) : [...cd.added, ...cd.modified.map((m) => m.path)];
|
|
13081
|
+
for (const p of changedPaths) {
|
|
13082
|
+
const f = fileAt(cstore, commitRoot, p);
|
|
13083
|
+
if (f && f.encoding !== "base64" && /^<{7}( |$)/m.test(f.content) && /^>{7}( |$)/m.test(f.content)) {
|
|
13084
|
+
die(`refusing to commit unresolved conflict markers in ${p} — resolve the <<<<<<< / >>>>>>> blocks first (or \`sol commit --force ...\` to override).`);
|
|
13085
|
+
}
|
|
13086
|
+
}
|
|
13087
|
+
}
|
|
13088
|
+
await appendCommit(log, commitRoot, message, parentHead, parent2);
|
|
13089
|
+
if (parent2) {
|
|
13090
|
+
try {
|
|
13091
|
+
unlinkSync8(mergeHeadPath);
|
|
13092
|
+
} catch {}
|
|
13093
|
+
}
|
|
13094
|
+
const refs = await loadRefs(log);
|
|
13095
|
+
saveRefs(refs);
|
|
13096
|
+
writeWorkingIndex(await repo.list());
|
|
13097
|
+
clearMergeConflicts(solDir);
|
|
13098
|
+
clearSemanticFlag(solDir);
|
|
13099
|
+
console.log(`commit ${commitRoot.slice(0, 14)} — ${message} (${changed} file change${changed === 1 ? "" : "s"})`);
|
|
13100
|
+
break;
|
|
13101
|
+
}
|
|
13102
|
+
case "status": {
|
|
13103
|
+
const { repo, log } = open();
|
|
13104
|
+
const refs = existsSync18(refsPath()) ? await loadRefs(log) : undefined;
|
|
13105
|
+
const head = await repo.head();
|
|
13106
|
+
const headOp = head ? [...await log.history()].reverse().find((o) => o.rootAfter === head) : undefined;
|
|
13107
|
+
const headBy = headOp?.by ?? "?";
|
|
13108
|
+
const seq = await log.seq();
|
|
13109
|
+
const rows = workingChangeRows(loadStore(), head);
|
|
13110
|
+
const conflicts = unresolvedConflictPaths();
|
|
13111
|
+
const dirty = rows.length;
|
|
13112
|
+
const clean = dirty === 0 && conflicts.length === 0;
|
|
13113
|
+
const semantic = activeSemanticFlag(solDir, head || undefined);
|
|
13114
|
+
const state = conflicts.length ? "CONFLICTED" : dirty ? "DIRTY" : semantic ? "SEMANTIC" : !head ? "EMPTY" : "CLEAN";
|
|
13115
|
+
if (args.includes("--json")) {
|
|
13116
|
+
console.log(JSON.stringify({
|
|
13117
|
+
branch: refs ? refs.current : "main",
|
|
13118
|
+
head: head || null,
|
|
13119
|
+
headAuthor: head ? headBy : null,
|
|
13120
|
+
seq,
|
|
13121
|
+
sessionActor: actor,
|
|
13122
|
+
state,
|
|
13123
|
+
clean,
|
|
13124
|
+
changes: rows.map((r) => ({ path: r.path, kind: r.kind })),
|
|
13125
|
+
conflicts,
|
|
13126
|
+
semantic: semantic ? { check: semantic.check, exitCode: semantic.exitCode, head: semantic.head, at: semantic.at, output: semantic.output } : undefined
|
|
13127
|
+
}));
|
|
13128
|
+
break;
|
|
13129
|
+
}
|
|
13130
|
+
console.log(`repo ${cwd}`);
|
|
13131
|
+
console.log(`on ${refs ? refs.current : "main"} head ${head ? `${head.slice(0, 14)} by ${headBy}` : "(empty)"} seq ${seq}`);
|
|
13132
|
+
console.log(`you: ${actor}`);
|
|
13133
|
+
if (conflicts.length) {
|
|
13134
|
+
console.log(`CONFLICTED — ${conflicts.length} path(s) with unresolved conflicts:`);
|
|
13135
|
+
for (const f of conflicts)
|
|
13136
|
+
console.log(" ! " + f);
|
|
13137
|
+
console.log(" resolve the <<<<<<< / ======= / >>>>>>> markers, then `sol commit` (`sol conflicts` to inspect)");
|
|
13138
|
+
}
|
|
13139
|
+
if (semantic) {
|
|
13140
|
+
console.log(`SEMANTIC conflict — a merge auto-converged with no line conflicts, but the check FAILED (\`${semantic.check}\`, exit ${semantic.exitCode}).`);
|
|
13141
|
+
console.log(" review the merged result and land a follow-up commit (`sol check` to re-run; it clears on pass or a new commit).");
|
|
13142
|
+
}
|
|
13143
|
+
if (!dirty) {
|
|
13144
|
+
if (!conflicts.length && !semantic)
|
|
13145
|
+
console.log("working tree clean");
|
|
13146
|
+
break;
|
|
13147
|
+
}
|
|
13148
|
+
const conflictSet = new Set(conflicts);
|
|
13149
|
+
console.log(`${dirty} uncommitted change(s):`);
|
|
13150
|
+
for (const r of rows) {
|
|
13151
|
+
const sigil = r.kind === "added" ? "+" : r.kind === "removed" ? "-" : conflictSet.has(r.path) ? "!" : "~";
|
|
13152
|
+
console.log(` ${sigil} ${r.path}${sigil === "!" ? " (conflicted)" : ""}`);
|
|
13153
|
+
}
|
|
13154
|
+
break;
|
|
13155
|
+
}
|
|
13156
|
+
case "conflicts": {
|
|
13157
|
+
open();
|
|
13158
|
+
const paths = unresolvedConflictPaths();
|
|
13159
|
+
const recorded = new Map(loadMergeConflicts(solDir).map((c) => [c.path, c]));
|
|
13160
|
+
if (args.includes("--json")) {
|
|
13161
|
+
console.log(JSON.stringify({
|
|
13162
|
+
count: paths.length,
|
|
13163
|
+
conflicts: paths.map((p) => {
|
|
13164
|
+
const r = recorded.get(p);
|
|
13165
|
+
const hunks2 = conflictHunks(p).map((h) => ({ ...h, base: r?.base }));
|
|
13166
|
+
return { path: p, hunks: hunks2, base: r?.base, ours: r?.ours, theirs: r?.theirs };
|
|
13167
|
+
})
|
|
13168
|
+
}));
|
|
13169
|
+
break;
|
|
13170
|
+
}
|
|
13171
|
+
if (!paths.length) {
|
|
13172
|
+
console.log("no unresolved conflicts");
|
|
13173
|
+
break;
|
|
13174
|
+
}
|
|
13175
|
+
console.log(`${paths.length} path(s) with unresolved conflicts:`);
|
|
13176
|
+
for (const p of paths) {
|
|
13177
|
+
const hunks2 = conflictHunks(p);
|
|
13178
|
+
console.log(` ! ${p} (${hunks2.length} hunk${hunks2.length === 1 ? "" : "s"})`);
|
|
13179
|
+
for (const h of hunks2)
|
|
13180
|
+
console.log(` lines ${h.startLine}-${h.endLine}: ${h.ours.length} ours / ${h.theirs.length} theirs`);
|
|
13181
|
+
}
|
|
13182
|
+
console.log("resolve the markers, then `sol commit` (or `sol conflicts --json` for the full hunks)");
|
|
13183
|
+
break;
|
|
13184
|
+
}
|
|
13185
|
+
case "ls": {
|
|
13186
|
+
const wantJson = args.includes("--json");
|
|
13187
|
+
const { repo } = open();
|
|
13188
|
+
const head = await repo.head();
|
|
13189
|
+
const store2 = loadStore();
|
|
13190
|
+
const { revealView: revealView2, slotIdFromKey: slotIdFromKey2 } = await Promise.resolve().then(() => (init_struct(), exports_struct));
|
|
13191
|
+
const { listAll: listAll3 } = await Promise.resolve().then(() => (init_tree(), exports_tree));
|
|
13192
|
+
const { loadSelfIdentity: loadSelfIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
13193
|
+
const { openContent: openContent2, UNREADABLE: UNREADABLE2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
13194
|
+
const ring = existsSync18(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
|
|
13195
|
+
const self = loadSelfIdentity2();
|
|
13196
|
+
const decrypt = (boxStr) => {
|
|
13197
|
+
try {
|
|
13198
|
+
const opened = openContent2(ring, JSON.parse(boxStr), actor, self);
|
|
13199
|
+
return opened === UNREADABLE2 ? undefined : opened;
|
|
13200
|
+
} catch {
|
|
13201
|
+
return;
|
|
13202
|
+
}
|
|
13203
|
+
};
|
|
13204
|
+
const { HIDDEN_KEY: HIDDEN_KEY2 } = await Promise.resolve().then(() => (init_struct(), exports_struct));
|
|
13205
|
+
const view = head ? revealView2(store2, head, decrypt) : head;
|
|
13206
|
+
const rows = (head ? listAll3(store2, view) : []).map((p) => {
|
|
13207
|
+
const seg = p.split("/").pop() ?? p;
|
|
13208
|
+
if (seg === HIDDEN_KEY2)
|
|
13209
|
+
return { path: p, hidden: "existence", slotId: null };
|
|
13210
|
+
const slotId = slotIdFromKey2(seg);
|
|
13211
|
+
if (slotId !== undefined)
|
|
13212
|
+
return { path: p, hidden: "name", slotId };
|
|
13213
|
+
return { path: p, hidden: null, slotId: null };
|
|
13214
|
+
});
|
|
13215
|
+
if (wantJson) {
|
|
13216
|
+
console.log(JSON.stringify({ count: rows.length, files: rows }));
|
|
13217
|
+
break;
|
|
13218
|
+
}
|
|
13219
|
+
for (const r of rows) {
|
|
13220
|
+
if (r.hidden === "existence")
|
|
13221
|
+
console.log(`\uD83D\uDD12 ${r.path.split("/").slice(0, -1).join("/") || "."}/ [hidden children — names & existence sealed]`);
|
|
13222
|
+
else if (r.hidden === "name")
|
|
13223
|
+
console.log(`\uD83D\uDD12 ${r.path.split("/").slice(0, -1).concat(`[locked] ${r.slotId} (name hidden)`).join("/")}`);
|
|
13224
|
+
else
|
|
13225
|
+
console.log(r.path);
|
|
13226
|
+
}
|
|
13227
|
+
break;
|
|
13228
|
+
}
|
|
13229
|
+
case "cat": {
|
|
13230
|
+
const wantJson = args.includes("--json");
|
|
13231
|
+
const path = args.filter((a) => !a.startsWith("-"))[0] || die("cat needs a path");
|
|
13232
|
+
const repo = open().repo;
|
|
13233
|
+
let c = await repo.readFile(path);
|
|
13234
|
+
if (c === undefined) {
|
|
13235
|
+
const head = await repo.head();
|
|
13236
|
+
if (head) {
|
|
13237
|
+
const store2 = loadStore();
|
|
13238
|
+
const { revealView: revealView2 } = await Promise.resolve().then(() => (init_struct(), exports_struct));
|
|
13239
|
+
const { readFile: readTree, entryKindAt: kindAt } = await Promise.resolve().then(() => (init_tree(), exports_tree));
|
|
13240
|
+
const { loadSelfIdentity: loadSelfIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
13241
|
+
const { openContent: openContent2, UNREADABLE: UNREADABLE2, KeyRing: KeyRing3 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
13242
|
+
const ring = existsSync18(solDir) ? loadKeyRing() : new KeyRing3;
|
|
13243
|
+
const self = loadSelfIdentity2();
|
|
13244
|
+
const decrypt = (boxStr) => {
|
|
13245
|
+
try {
|
|
13246
|
+
const opened = openContent2(ring, JSON.parse(boxStr), actor, self);
|
|
13247
|
+
return opened === UNREADABLE2 ? undefined : opened;
|
|
13248
|
+
} catch {
|
|
13249
|
+
return;
|
|
13250
|
+
}
|
|
13251
|
+
};
|
|
13252
|
+
const view = revealView2(store2, head, decrypt);
|
|
13253
|
+
if (view !== head) {
|
|
13254
|
+
const revealed = readTree(store2, view, path);
|
|
13255
|
+
if (revealed !== undefined)
|
|
13256
|
+
c = revealed;
|
|
13257
|
+
else if (kindAt(store2, view, path) === "sealed")
|
|
13258
|
+
die(`name+content hidden: pull/checkout to materialize "${path}" as a recipient (cat can't address a sealed slot)`);
|
|
13259
|
+
}
|
|
13260
|
+
}
|
|
13261
|
+
}
|
|
13262
|
+
if (c === undefined)
|
|
13263
|
+
die("no such tracked path: " + path);
|
|
13264
|
+
if (c === SEALED) {
|
|
13265
|
+
const [{ SealedClient: SealedClient2 }, { UNREADABLE: UNREADABLE2 }, { loadSelfIdentity: loadSelfIdentity2 }] = await Promise.all([Promise.resolve().then(() => (init_sealed_client(), exports_sealed_client)), Promise.resolve().then(() => (init_crypto(), exports_crypto)), Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience))]);
|
|
13266
|
+
const self = loadSelfIdentity2();
|
|
13267
|
+
const opened = await new SealedClient2(repo, loadKeyRing()).open(path, actor, self);
|
|
13268
|
+
const readable = !(opened === undefined || opened === UNREADABLE2);
|
|
13269
|
+
if (wantJson) {
|
|
13270
|
+
console.log(JSON.stringify({ path, sealed: true, readable, actor, content: readable ? opened : null }));
|
|
13271
|
+
break;
|
|
13272
|
+
}
|
|
13273
|
+
process.stdout.write(readable ? opened : `<<sealed — you are not a recipient>>
|
|
13274
|
+
`);
|
|
13275
|
+
break;
|
|
13276
|
+
}
|
|
13277
|
+
if (wantJson) {
|
|
13278
|
+
console.log(JSON.stringify({ path, sealed: false, readable: true, content: c }));
|
|
13279
|
+
break;
|
|
13280
|
+
}
|
|
13281
|
+
process.stdout.write(c);
|
|
13282
|
+
break;
|
|
13283
|
+
}
|
|
13284
|
+
case "log": {
|
|
13285
|
+
const { log } = open();
|
|
13286
|
+
const ops = await log.history();
|
|
13287
|
+
if (args.includes("--all") || args.includes("--ops")) {
|
|
13288
|
+
if (args.includes("--json")) {
|
|
13289
|
+
const store3 = loadStore();
|
|
13290
|
+
const trust2 = loadTrust();
|
|
13291
|
+
console.log(JSON.stringify({
|
|
13292
|
+
version: 1,
|
|
13293
|
+
ops: ops.map((o) => ({
|
|
13294
|
+
seq: o.seq,
|
|
13295
|
+
id: o.entryHash ?? null,
|
|
13296
|
+
type: o.type,
|
|
13297
|
+
path: o.path,
|
|
13298
|
+
by: o.by ?? null,
|
|
13299
|
+
message: o.message ?? null,
|
|
13300
|
+
root: o.rootAfter,
|
|
13301
|
+
parents: [o.parent, o.parent2].filter((p) => !!p),
|
|
13302
|
+
prevHash: o.prevHash ?? null,
|
|
13303
|
+
at: o.at,
|
|
13304
|
+
prov: provJson(o, store3, ATTEST_KEY2, trust2) ?? null
|
|
13305
|
+
}))
|
|
13306
|
+
}));
|
|
13307
|
+
break;
|
|
13308
|
+
}
|
|
13309
|
+
if (!ops.length)
|
|
13310
|
+
console.log("(no history yet)");
|
|
13311
|
+
for (const line of formatLog(ops))
|
|
13312
|
+
console.log(line.replace(/\bcheckpoint\b/, "commit"));
|
|
13313
|
+
break;
|
|
13314
|
+
}
|
|
13315
|
+
const store2 = loadStore();
|
|
13316
|
+
const trust = loadTrust();
|
|
13317
|
+
const producersByRoot = new Map;
|
|
13318
|
+
for (const o of ops)
|
|
13319
|
+
if (o.type === "checkpoint") {
|
|
13320
|
+
let list = producersByRoot.get(o.rootAfter);
|
|
13321
|
+
if (!list)
|
|
13322
|
+
producersByRoot.set(o.rootAfter, list = []);
|
|
13323
|
+
list.push(o);
|
|
13324
|
+
}
|
|
13325
|
+
const latestAt = (root) => {
|
|
13326
|
+
const list = root ? producersByRoot.get(root) : undefined;
|
|
13327
|
+
return list ? list[list.length - 1] : undefined;
|
|
13328
|
+
};
|
|
13329
|
+
const parentAt = (root, before) => {
|
|
13330
|
+
const list = root ? producersByRoot.get(root) : undefined;
|
|
13331
|
+
if (!list)
|
|
13332
|
+
return;
|
|
13333
|
+
let best;
|
|
13334
|
+
for (const p of list)
|
|
13335
|
+
if (p.seq < before.seq && (!best || p.seq > best.seq))
|
|
13336
|
+
best = p;
|
|
13337
|
+
return best;
|
|
13338
|
+
};
|
|
13339
|
+
const live = await log.head() ?? "";
|
|
13340
|
+
const refArg = args.find((a) => !a.startsWith("-"));
|
|
13341
|
+
const lrefs = existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")) : null;
|
|
13342
|
+
let tipRoot = live;
|
|
13343
|
+
if (refArg) {
|
|
13344
|
+
tipRoot = lrefs?.branches[refArg]?.head ?? refArg;
|
|
13345
|
+
} else if (lrefs && !producersByRoot.has(tipRoot)) {
|
|
13346
|
+
const stored = lrefs.branches[lrefs.current]?.head;
|
|
13347
|
+
if (stored && producersByRoot.has(stored))
|
|
13348
|
+
tipRoot = stored;
|
|
13349
|
+
}
|
|
13350
|
+
const chain = [];
|
|
13351
|
+
const seen = new Set;
|
|
13352
|
+
const stack = [latestAt(tipRoot)];
|
|
13353
|
+
while (stack.length) {
|
|
13354
|
+
const c = stack.pop();
|
|
13355
|
+
if (!c || seen.has(c.seq))
|
|
13356
|
+
continue;
|
|
13357
|
+
seen.add(c.seq);
|
|
13358
|
+
chain.push(c);
|
|
13359
|
+
stack.push(parentAt(c.parent, c));
|
|
13360
|
+
if (c.parent2)
|
|
13361
|
+
stack.push(parentAt(c.parent2, c));
|
|
13362
|
+
}
|
|
13363
|
+
chain.sort((a, b) => b.seq - a.seq);
|
|
13364
|
+
if (args.includes("--json")) {
|
|
13365
|
+
console.log(JSON.stringify(chain.map((c) => ({
|
|
13366
|
+
hash: c.rootAfter,
|
|
13367
|
+
by: c.by ?? null,
|
|
13368
|
+
message: c.message ?? "",
|
|
13369
|
+
seq: c.seq,
|
|
13370
|
+
parents: [c.parent, c.parent2].filter((p) => !!p),
|
|
13371
|
+
prov: provJson(c, store2, ATTEST_KEY2, trust) ?? null
|
|
13372
|
+
}))));
|
|
13373
|
+
break;
|
|
13374
|
+
}
|
|
13375
|
+
if (!chain.length) {
|
|
13376
|
+
console.log(ops.length ? '(no commits on this branch yet — run `sol commit "msg"`)' : "(no history yet)");
|
|
13377
|
+
}
|
|
13378
|
+
for (const c of chain) {
|
|
13379
|
+
const n = countChanges(store2, c.parent ?? "", c.rootAfter);
|
|
13380
|
+
const mergedIn = [c.parent2 && parentAt(c.parent2, c)?.by].filter(Boolean);
|
|
13381
|
+
const tag = c.parent2 ? ` [merge ${c.parent2.slice(0, 8)}${mergedIn.length ? ` <- ${mergedIn.join(", ")}` : ""}]` : "";
|
|
13382
|
+
console.log(`${c.rootAfter.slice(0, 14)} ${authorLabel(c, store2).padEnd(14)} ${c.message ?? ""} (${n} change${n === 1 ? "" : "s"})${tag} ${attTag(c, ATTEST_KEY2)} ${signTag(c, trust)}`);
|
|
13383
|
+
}
|
|
13384
|
+
const wc = workingChanges(store2, live);
|
|
13385
|
+
const dirty = wc.added.length + wc.modified.length + wc.removed.length;
|
|
13386
|
+
if (dirty)
|
|
13387
|
+
console.log(`(working: ${dirty} uncommitted change(s))`);
|
|
13388
|
+
break;
|
|
13389
|
+
}
|
|
13390
|
+
case "rm": {
|
|
13391
|
+
const path = args[0] || die("rm needs a path");
|
|
13392
|
+
let onDisk = false;
|
|
13393
|
+
try {
|
|
13394
|
+
unlinkSync8(join19(cwd, path));
|
|
13395
|
+
onDisk = true;
|
|
13396
|
+
} catch {}
|
|
13397
|
+
if (onDisk) {
|
|
13398
|
+
console.log(`removed ${path} from disk (commit to record the deletion)`);
|
|
13399
|
+
break;
|
|
13400
|
+
}
|
|
13401
|
+
const { repo } = open();
|
|
13402
|
+
if (!(await repo.list()).includes(path))
|
|
13403
|
+
die("not tracked and not on disk: " + path);
|
|
13404
|
+
await repo.deleteFile(path);
|
|
13405
|
+
console.log("removed (tracked-only) " + path);
|
|
13406
|
+
break;
|
|
13407
|
+
}
|
|
13408
|
+
case "diff": {
|
|
13409
|
+
const { log } = open();
|
|
13410
|
+
const store2 = loadStore();
|
|
13411
|
+
const head = await log.head() ?? "";
|
|
13412
|
+
const wantJson = args.includes("--json");
|
|
13413
|
+
const da = args.filter((a) => a !== "--json");
|
|
13414
|
+
const emitJson = (base, only) => console.log(JSON.stringify(workingChangeRows(store2, base, only)));
|
|
13415
|
+
if (da.length >= 2) {
|
|
13416
|
+
const aR = revealedHeadForRender(store2, (await resolveRef(log, da[0])).head);
|
|
13417
|
+
const bR = revealedHeadForRender(store2, (await resolveRef(log, da[1])).head);
|
|
13418
|
+
const td = diffTrees(store2, aR, bR);
|
|
13419
|
+
wantJson ? console.log(JSON.stringify(td)) : printTreeDiff(td);
|
|
13420
|
+
} else if (da.length === 1) {
|
|
13421
|
+
const resolved = await resolveRefSoft(log, da[0]);
|
|
13422
|
+
if (resolved) {
|
|
13423
|
+
wantJson ? emitJson(resolved.head) : printWorkingDiff(store2, resolved.head);
|
|
13424
|
+
} else {
|
|
13425
|
+
const path = isWorkingPath(store2, head, da[0]);
|
|
13426
|
+
if (path)
|
|
13427
|
+
wantJson ? emitJson(head, path) : printWorkingDiff(store2, head, path);
|
|
13428
|
+
else
|
|
13429
|
+
die(`'${da[0]}' is neither a ref nor a working-tree path.
|
|
13430
|
+
usage: sol diff (working tree vs HEAD)
|
|
13431
|
+
sol diff <ref> (working tree vs a branch/commit)
|
|
13432
|
+
sol diff <ref> <ref> (tree vs tree)
|
|
13433
|
+
sol diff <path> (one file, working tree vs HEAD)`);
|
|
13434
|
+
}
|
|
13435
|
+
} else {
|
|
13436
|
+
wantJson ? emitJson(head) : printWorkingDiff(store2, head);
|
|
13437
|
+
}
|
|
13438
|
+
break;
|
|
13439
|
+
}
|
|
13440
|
+
case "restore":
|
|
13441
|
+
case "checkout": {
|
|
13442
|
+
const { log } = open();
|
|
13443
|
+
const store2 = loadStore();
|
|
13444
|
+
let from = "head";
|
|
13445
|
+
const rest = [];
|
|
13446
|
+
for (let i = 0;i < args.length; i++) {
|
|
13447
|
+
if (args[i] === "--from")
|
|
13448
|
+
from = args[++i];
|
|
13449
|
+
else if (args[i] !== "--all")
|
|
13450
|
+
rest.push(args[i]);
|
|
13451
|
+
}
|
|
13452
|
+
const { head } = await resolveRef(log, from);
|
|
13453
|
+
if (rest.length === 0) {
|
|
13454
|
+
const n = materializeTree(store2, head);
|
|
13455
|
+
console.log(`restored the working tree to ${from === "head" ? "HEAD" : from} (${n} change(s))`);
|
|
13456
|
+
} else {
|
|
13457
|
+
let n = 0;
|
|
13458
|
+
for (const p of rest) {
|
|
13459
|
+
if (materialize(store2, head, p))
|
|
13460
|
+
n++;
|
|
13461
|
+
else
|
|
13462
|
+
console.error(`not in ${from}: ${p}`);
|
|
13463
|
+
}
|
|
13464
|
+
console.log(`restored ${n} file(s) from ${from === "head" ? "HEAD" : from}`);
|
|
13465
|
+
}
|
|
13466
|
+
break;
|
|
13467
|
+
}
|
|
13468
|
+
case "show": {
|
|
13469
|
+
const { log } = open();
|
|
13470
|
+
const store2 = loadStore();
|
|
13471
|
+
const trust = loadTrust();
|
|
13472
|
+
const ops = await log.history();
|
|
13473
|
+
const refArg = args.find((a) => !a.startsWith("-"));
|
|
13474
|
+
const resolved = await resolveRef(log, refArg);
|
|
13475
|
+
let op = resolved.op;
|
|
13476
|
+
if (op && op.type !== "checkpoint") {
|
|
13477
|
+
const sealing = ops.find((o) => o.type === "checkpoint" && o.rootAfter === op.rootAfter);
|
|
13478
|
+
if (sealing)
|
|
13479
|
+
op = sealing;
|
|
13480
|
+
}
|
|
13481
|
+
if (!op)
|
|
13482
|
+
die("nothing to show (empty repo)");
|
|
13483
|
+
const idx = ops.findIndex((o) => o.seq === op.seq);
|
|
13484
|
+
let prev = emptyRoot(store2);
|
|
13485
|
+
if (op.parent !== undefined) {
|
|
13486
|
+
prev = op.parent;
|
|
13487
|
+
} else if (op.type === "checkpoint") {
|
|
13488
|
+
for (let i = idx - 1;i >= 0; i--)
|
|
13489
|
+
if (ops[i].type === "checkpoint") {
|
|
13490
|
+
prev = ops[i].rootAfter;
|
|
13491
|
+
break;
|
|
13492
|
+
}
|
|
13493
|
+
} else if (idx > 0) {
|
|
13494
|
+
prev = ops[idx - 1].rootAfter;
|
|
13495
|
+
}
|
|
13496
|
+
const kind = op.type === "checkpoint" ? "commit" : op.type;
|
|
13497
|
+
const prevR = revealedHeadForRender(store2, prev);
|
|
13498
|
+
const afterR = revealedHeadForRender(store2, op.rootAfter);
|
|
13499
|
+
if (args.includes("--json")) {
|
|
13500
|
+
console.log(JSON.stringify({ hash: op.rootAfter, seq: op.seq, by: op.by ?? null, message: op.message ?? "", prov: provJson(op, store2, ATTEST_KEY2, trust) ?? null, diff: diffTrees(store2, prevR, afterR) }));
|
|
13501
|
+
break;
|
|
13502
|
+
}
|
|
13503
|
+
const pnode = provOf(op, store2);
|
|
13504
|
+
console.log(`${kind} ${op.rootAfter.slice(0, 14)} seq ${op.seq} by ${authorLabel(op, store2)} ${attTag(op, ATTEST_KEY2)} ${signTag(op, trust)}${op.message ? `
|
|
13505
|
+
|
|
13506
|
+
` + op.message : ""}${pnode?.rationale ? `
|
|
13507
|
+
|
|
13508
|
+
rationale: ` + pnode.rationale : ""}
|
|
13509
|
+
`);
|
|
13510
|
+
printTreeDiff(diffTrees(store2, prevR, afterR));
|
|
13511
|
+
break;
|
|
13512
|
+
}
|
|
13513
|
+
case "blame": {
|
|
13514
|
+
const path = args.find((a) => !a.startsWith("-")) || die("blame needs a path");
|
|
13515
|
+
const store2 = loadStore();
|
|
13516
|
+
const trust = loadTrust();
|
|
13517
|
+
const repo = open().repo;
|
|
13518
|
+
const ops = await repo.history();
|
|
13519
|
+
const head = await repo.head() ?? "";
|
|
13520
|
+
const { entryKindAt: kindAt } = await Promise.resolve().then(() => (init_tree(), exports_tree));
|
|
13521
|
+
const readAt = (root, p) => {
|
|
13522
|
+
const revealed = revealedHeadForRender(store2, root);
|
|
13523
|
+
const c = readFile(store2, revealed, p);
|
|
13524
|
+
return c === undefined || c === SEALED ? undefined : c;
|
|
13525
|
+
};
|
|
13526
|
+
const lines2 = blameFile(ops, store2, path, readAt);
|
|
13527
|
+
if (!lines2.length) {
|
|
13528
|
+
const revealedHead2 = revealedHeadForRender(store2, head);
|
|
13529
|
+
const hostKind = head ? kindAt(store2, head, path) : undefined;
|
|
13530
|
+
const revealedKind = revealedHead2 ? kindAt(store2, revealedHead2, path) : undefined;
|
|
13531
|
+
if (hostKind === "sealed" && revealedKind !== "blob") {
|
|
13532
|
+
if (args.includes("--json")) {
|
|
13533
|
+
console.log(JSON.stringify({ path, sealed: true, lines: [] }));
|
|
13534
|
+
break;
|
|
13535
|
+
}
|
|
13536
|
+
console.log(`${path}: sealed — content hidden (no per-line blame; you are not a recipient). structural history is visible via \`sol log\`.`);
|
|
13537
|
+
break;
|
|
13538
|
+
}
|
|
13539
|
+
die("no history for " + path);
|
|
13540
|
+
}
|
|
13541
|
+
const bySeq = new Map(ops.map((o) => [o.seq, o]));
|
|
13542
|
+
if (args.includes("--json")) {
|
|
13543
|
+
console.log(JSON.stringify(lines2.map((t, i) => {
|
|
13544
|
+
const op = bySeq.get(t.seq);
|
|
13545
|
+
return { line: i + 1, content: t.line, seq: t.seq, by: t.by ?? null, kind: op?.kind ?? null, prov: op ? provJson(op, store2, ATTEST_KEY2, trust) ?? null : null };
|
|
13546
|
+
})));
|
|
13547
|
+
break;
|
|
13548
|
+
}
|
|
13549
|
+
for (const t of lines2) {
|
|
13550
|
+
const op = bySeq.get(t.seq);
|
|
13551
|
+
const who = op ? authorLabel(op, store2) : t.by ?? "?";
|
|
13552
|
+
const tag = op ? attTag(op, ATTEST_KEY2) : "[unattested/local]";
|
|
13553
|
+
const sigtag = op ? signTag(op, trust) : "unsigned";
|
|
13554
|
+
console.log(`${String(t.seq).padStart(4)} ${who.padEnd(16)} ${tag.padEnd(24)} ${sigtag.padEnd(26)} ${t.line}`);
|
|
13555
|
+
}
|
|
13556
|
+
break;
|
|
13557
|
+
}
|
|
13558
|
+
case "fsck": {
|
|
13559
|
+
const { log } = open();
|
|
13560
|
+
const store2 = loadStore();
|
|
13561
|
+
const head = await log.head();
|
|
13562
|
+
const ops = await log.history();
|
|
13563
|
+
let chainOk = true;
|
|
13564
|
+
let chainErr = "";
|
|
13565
|
+
try {
|
|
13566
|
+
for (let i = 0;i < ops.length; i++)
|
|
13567
|
+
if (ops[i].seq !== i + 1)
|
|
13568
|
+
throw new Error("gap before seq " + (i + 1));
|
|
13569
|
+
verifyChain(ops);
|
|
13570
|
+
} catch (e) {
|
|
13571
|
+
chainOk = false;
|
|
13572
|
+
chainErr = String(e?.message ?? e);
|
|
13573
|
+
}
|
|
13574
|
+
const missing = [];
|
|
13575
|
+
if (head) {
|
|
13576
|
+
const seen = new Set;
|
|
13577
|
+
const walk = (h) => {
|
|
13578
|
+
if (seen.has(h))
|
|
13579
|
+
return;
|
|
13580
|
+
seen.add(h);
|
|
13581
|
+
const node = store2.get(h);
|
|
13582
|
+
if (!node) {
|
|
13583
|
+
missing.push(h);
|
|
13584
|
+
return;
|
|
13585
|
+
}
|
|
13586
|
+
if (node.kind === "tree")
|
|
13587
|
+
for (const e of Object.values(node.entries))
|
|
13588
|
+
walk(e.hash);
|
|
13589
|
+
};
|
|
13590
|
+
walk(head);
|
|
13591
|
+
}
|
|
13592
|
+
const ok = chainOk && missing.length === 0;
|
|
13593
|
+
console.log(`fsck: ${ok ? "OK" : "PROBLEMS FOUND"}`);
|
|
13594
|
+
console.log(` op-log: ${ops.length} op(s), chain ${chainOk ? "valid + contiguous" : "BROKEN — " + chainErr}`);
|
|
13595
|
+
console.log(` head: ${head ? head.slice(0, 16) : "(empty)"}, ${missing.length} missing object(s)`);
|
|
13596
|
+
for (const mh of missing)
|
|
13597
|
+
console.log(" missing " + mh);
|
|
13598
|
+
process.exitCode = ok ? 0 : 1;
|
|
13599
|
+
break;
|
|
13600
|
+
}
|
|
13601
|
+
case "gc": {
|
|
13602
|
+
const { log } = open();
|
|
13603
|
+
const store2 = loadStore();
|
|
13604
|
+
const ops = await log.history();
|
|
13605
|
+
const reachable = new Set;
|
|
13606
|
+
const walk = (h) => {
|
|
13607
|
+
if (reachable.has(h))
|
|
13608
|
+
return;
|
|
13609
|
+
const node = store2.get(h);
|
|
13610
|
+
if (!node)
|
|
13611
|
+
return;
|
|
13612
|
+
reachable.add(h);
|
|
13613
|
+
if (node.kind === "tree")
|
|
13614
|
+
for (const e of Object.values(node.entries))
|
|
13615
|
+
walk(e.hash);
|
|
13616
|
+
};
|
|
13617
|
+
for (const op of ops) {
|
|
13618
|
+
walk(op.rootAfter);
|
|
13619
|
+
if (op.prov)
|
|
13620
|
+
walk(op.prov);
|
|
13621
|
+
}
|
|
13622
|
+
const objDir = join19(solDir, "objects");
|
|
13623
|
+
let removed = 0;
|
|
13624
|
+
for (const name of readdirSync9(objDir)) {
|
|
13625
|
+
if (name.endsWith(".tmp") || !reachable.has(name)) {
|
|
13626
|
+
unlinkSync8(join19(objDir, name));
|
|
13627
|
+
removed++;
|
|
13628
|
+
}
|
|
13629
|
+
}
|
|
13630
|
+
console.log(`gc: kept ${reachable.size} object(s), removed ${removed} unreachable`);
|
|
13631
|
+
if (existsSync18(join19(solDir, "env", "seal"))) {
|
|
13632
|
+
const { gcStaleStanzas: gcStaleStanzas2 } = await Promise.resolve().then(() => (init_secret(), exports_secret));
|
|
13633
|
+
const st = gcStaleStanzas2(solDir);
|
|
13634
|
+
if (st.removed)
|
|
13635
|
+
console.log(`gc: removed ${st.removed} orphaned sealed stanza(s) (${st.bytes} bytes) — stale post-reseal epochs, value-safe`);
|
|
13636
|
+
}
|
|
13637
|
+
break;
|
|
13638
|
+
}
|
|
13639
|
+
case "ignore": {
|
|
13640
|
+
const pat = args.join(" ").trim();
|
|
13641
|
+
if (!pat) {
|
|
13642
|
+
for (const p of ignorePatterns())
|
|
13643
|
+
console.log(p);
|
|
13644
|
+
break;
|
|
13645
|
+
}
|
|
13646
|
+
const f = join19(cwd, ".solignore");
|
|
13647
|
+
const lead = existsSync18(f) && !readFileSync18(f, "utf8").endsWith(`
|
|
13648
|
+
`) ? `
|
|
13649
|
+
` : "";
|
|
13650
|
+
appendFileSync2(f, lead + pat + `
|
|
13651
|
+
`);
|
|
13652
|
+
console.log("ignoring: " + pat);
|
|
13653
|
+
break;
|
|
13654
|
+
}
|
|
13655
|
+
case "hide": {
|
|
13656
|
+
const { repo } = open();
|
|
13657
|
+
const wantJson = args.includes("--json");
|
|
13658
|
+
const cfg = resolveRemote2(solDir) || die("no remote — `sol push <repo>` (or `sol remote <url> <repo>`) first; the policy lives on the repo");
|
|
13659
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
13660
|
+
const sub = args[0];
|
|
13661
|
+
const flag2 = (name) => {
|
|
13662
|
+
const i = args.indexOf(name);
|
|
13663
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
13664
|
+
};
|
|
13665
|
+
const has2 = (name) => args.includes(name);
|
|
13666
|
+
if (sub === "list") {
|
|
13667
|
+
const policy = await policyGet(cfg, token);
|
|
13668
|
+
const paths = await repo.list();
|
|
13669
|
+
const describe2 = (a) => a.kind === "role" ? `role>=${a.min}` : a.kind === "team" ? `team:${a.teamId}` : a.kind === "users" ? `users:${a.userIds.join(",")}` : "owner";
|
|
13670
|
+
const rows = policy.rules.map((r) => ({ ...r, covers: paths.filter((p) => globCovers2(r.pattern, p)) }));
|
|
13671
|
+
if (wantJson) {
|
|
13672
|
+
console.log(JSON.stringify({ count: rows.length, rules: rows }));
|
|
13673
|
+
break;
|
|
13674
|
+
}
|
|
13675
|
+
if (!rows.length) {
|
|
13676
|
+
console.log('no visibility-policy rules yet — `sol hide "<pattern>" --role write` to add one.');
|
|
13677
|
+
break;
|
|
13678
|
+
}
|
|
13679
|
+
for (const r of rows) {
|
|
13680
|
+
console.log(`${r.pattern} -> ${describe2(r.audience)}${r.recovery?.length ? ` +escrow(${r.recovery.join(",")})` : ""}${r.noList ? " [no-list: full opacity]" : ""}${r.hideExistence ? " [hide-existence: L3 path absent]" : r.hideNames ? " [hide-names: L2 filename sealed]" : ""}`);
|
|
13681
|
+
if (r.covers.length)
|
|
13682
|
+
for (const p of r.covers)
|
|
13683
|
+
console.log(` ${p}`);
|
|
13684
|
+
else
|
|
13685
|
+
console.log(" (no current paths match)");
|
|
13686
|
+
}
|
|
13687
|
+
break;
|
|
13688
|
+
}
|
|
13689
|
+
if (sub === "remove") {
|
|
13690
|
+
const pattern2 = args[1] || die("usage: sol hide remove <pattern>");
|
|
13691
|
+
const { removed } = await policyRemove(cfg, token, pattern2);
|
|
13692
|
+
if (wantJson) {
|
|
13693
|
+
console.log(JSON.stringify({ pattern: pattern2, removed }));
|
|
13694
|
+
break;
|
|
13695
|
+
}
|
|
13696
|
+
if (!removed)
|
|
13697
|
+
die(`no policy rule for pattern "${pattern2}"`);
|
|
13698
|
+
console.log(`removed policy rule "${pattern2}".`);
|
|
13699
|
+
console.log(" NOTE: this does NOT auto-unseal existing content — already-sealed paths keep their lockboxes.");
|
|
13700
|
+
console.log(" to recall, re-seal them: `sol seal --reapply` (re-wraps to the now-current audience, bumps epoch).");
|
|
13701
|
+
break;
|
|
13702
|
+
}
|
|
13703
|
+
const pattern = args.filter((a) => !a.startsWith("-"))[0] || die("usage: sol hide <pattern> [--role write|admin] [--team <id>] [--users a,b] [--escrow]");
|
|
13704
|
+
let audience2;
|
|
13705
|
+
const usersArg = flag2("--users");
|
|
13706
|
+
const teamArg = flag2("--team");
|
|
13707
|
+
const roleArg = flag2("--role");
|
|
13708
|
+
if (usersArg)
|
|
13709
|
+
audience2 = { kind: "users", userIds: usersArg.split(",").map((s) => s.trim()).filter(Boolean) };
|
|
13710
|
+
else if (teamArg)
|
|
13711
|
+
audience2 = { kind: "team", teamId: teamArg };
|
|
13712
|
+
else if (has2("--owner"))
|
|
13713
|
+
audience2 = { kind: "owner" };
|
|
13714
|
+
else {
|
|
13715
|
+
const min = roleArg ?? "write";
|
|
13716
|
+
if (min !== "read" && min !== "write" && min !== "admin")
|
|
13717
|
+
die(`invalid --role '${min}' — use write or admin`);
|
|
13718
|
+
audience2 = { kind: "role", min };
|
|
13719
|
+
}
|
|
13720
|
+
const noList = has2("--no-list");
|
|
13721
|
+
const hideExistence2 = has2("--hide-existence");
|
|
13722
|
+
const hideNames = has2("--hide-names") || hideExistence2;
|
|
13723
|
+
const rule = { pattern, audience: audience2, ...has2("--escrow") ? { recovery: ["org-escrow"] } : {}, ...noList ? { noList: true } : {}, ...hideNames ? { hideNames: true } : {}, ...hideExistence2 ? { hideExistence: true } : {} };
|
|
13724
|
+
const res = await policyUpsert(cfg, token, rule);
|
|
13725
|
+
if (res.error)
|
|
13726
|
+
die(`policy update rejected: ${res.error}`);
|
|
13727
|
+
const describe = (a) => a.kind === "role" ? `role>=${a.min}` : a.kind === "team" ? `team:${a.teamId}` : a.kind === "users" ? `users:${a.userIds.join(",")}` : "owner";
|
|
13728
|
+
if (wantJson) {
|
|
13729
|
+
console.log(JSON.stringify({ pattern, audience: audience2, recovery: rule.recovery ?? [], noList, hideNames, hideExistence: hideExistence2, rules: res.policy.rules.length }));
|
|
13730
|
+
break;
|
|
13731
|
+
}
|
|
13732
|
+
const covered = (await repo.list()).filter((p) => globCovers2(pattern, p));
|
|
13733
|
+
const flags = `${noList ? " [--no-list: full opacity — non-audience members can't list/fetch these paths]" : ""}${hideExistence2 ? " [--hide-existence: L3 — the PATH is ABSENT from host+collaborators; only a per-dir \\0hidden marker remains]" : hideNames ? " [--hide-names: L2 — the FILENAME is sealed too; non-recipients see only an opaque slot]" : ""}`;
|
|
13734
|
+
console.log(`hid "${pattern}" -> ${describe(audience2)}${flags} — a VisibilityPolicy rule (host-visible metadata; the host learns the audience, never a key).`);
|
|
13735
|
+
if (covered.length) {
|
|
13736
|
+
console.log(` ${covered.length} current path(s) match: ${covered.slice(0, 8).join(", ")}${covered.length > 8 ? ", …" : ""}`);
|
|
13737
|
+
console.log(" seal them to this audience now: `sol seal <path>` (no recipients consults the policy).");
|
|
13738
|
+
} else {
|
|
13739
|
+
console.log(" no current paths match — new files under this pattern seal to the audience on `sol seal <path>`.");
|
|
13740
|
+
}
|
|
13741
|
+
break;
|
|
13742
|
+
}
|
|
13743
|
+
case "seal": {
|
|
13744
|
+
const { repo, log } = open();
|
|
13745
|
+
const wantJson = args.includes("--json");
|
|
13746
|
+
const reapply = args.includes("--reapply");
|
|
13747
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
13748
|
+
if (args.includes("--scrub-history")) {
|
|
13749
|
+
const { pendingScrubPaths: pendingScrubPaths2, historyHasCleartext: historyHasCleartext2, scrubHistory: scrubHistory2, clearPendingScrub: clearPendingScrub2, gcAfterScrub: gcAfterScrub2 } = await Promise.resolve().then(() => (init_secret_scrub(), exports_secret_scrub));
|
|
13750
|
+
const ops = await log.history();
|
|
13751
|
+
const only = positional[0];
|
|
13752
|
+
const targets = (only ? [only] : pendingScrubPaths2(solDir)).filter((p) => historyHasCleartext2(solDir, ops, p));
|
|
13753
|
+
if (!targets.length) {
|
|
13754
|
+
if (only)
|
|
13755
|
+
die(`no pre-seal cleartext in history for "${only}" — nothing to scrub (it was never committed in the clear, or is already scrubbed).`);
|
|
13756
|
+
clearPendingScrub2(solDir);
|
|
13757
|
+
console.log(wantJson ? JSON.stringify({ scrubbed: [], rewrittenOps: 0 }) : "no paths with recoverable pre-seal cleartext — nothing to scrub.");
|
|
13758
|
+
break;
|
|
13759
|
+
}
|
|
13760
|
+
let removed = 0;
|
|
13761
|
+
{
|
|
13762
|
+
const res = await scrubHistory2(solDir, ops, targets);
|
|
13763
|
+
writeFileSync16(join19(solDir, "ops.jsonl"), res.ops.map((o) => JSON.stringify(o)).join(`
|
|
13764
|
+
`) + (res.ops.length ? `
|
|
13765
|
+
` : ""));
|
|
13766
|
+
writeFileSync16(join19(solDir, "HEAD"), JSON.stringify({ head: res.newHead, seq: res.newSeq, logTip: res.newLogTip }));
|
|
13767
|
+
if (existsSync18(refsPath())) {
|
|
13768
|
+
const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
|
|
13769
|
+
for (const b of Object.values(refs.branches)) {
|
|
13770
|
+
if (b.head && res.rootMap.has(b.head))
|
|
13771
|
+
b.head = res.rootMap.get(b.head);
|
|
13772
|
+
if (b.base && res.rootMap.has(b.base))
|
|
13773
|
+
b.base = res.rootMap.get(b.base);
|
|
13774
|
+
if (b.remote && res.rootMap.has(b.remote))
|
|
13775
|
+
b.remote = res.rootMap.get(b.remote);
|
|
13776
|
+
}
|
|
13777
|
+
saveRefs(refs);
|
|
13778
|
+
}
|
|
13779
|
+
removed = gcAfterScrub2(solDir, res.ops);
|
|
13780
|
+
clearPendingScrub2(solDir);
|
|
13781
|
+
}
|
|
13782
|
+
if (wantJson) {
|
|
13783
|
+
console.log(JSON.stringify({ scrubbed: targets, removedObjects: removed }));
|
|
13784
|
+
break;
|
|
13785
|
+
}
|
|
13786
|
+
console.log(`scrubbed pre-seal cleartext from history for: ${targets.join(", ")} — the op-log was rewritten and ${removed} orphaned cleartext object(s) dropped (\`sol restore\` can no longer recover them); push is unblocked.`);
|
|
13787
|
+
console.log(" NOTE: this is a DESTRUCTIVE local history rewrite. collaborators who already pulled the old history must re-clone.");
|
|
13788
|
+
break;
|
|
13789
|
+
}
|
|
13790
|
+
if (reapply) {
|
|
13791
|
+
const cfg = resolveRemote2(solDir) || die("no remote — the policy lives on the repo; `sol remote <url> <repo>` first");
|
|
13792
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
13793
|
+
const { SealedClient: SealedClient3 } = await Promise.resolve().then(() => (init_sealed_client(), exports_sealed_client));
|
|
13794
|
+
const { UNREADABLE: UNREADABLE2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
13795
|
+
const { loadSelfIdentity: loadSelfIdentity2, recordAudience: recordAudience3 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
13796
|
+
const ring2 = loadKeyRing();
|
|
13797
|
+
const self = loadSelfIdentity2();
|
|
13798
|
+
const client2 = new SealedClient3(repo, ring2);
|
|
13799
|
+
const only = positional[0];
|
|
13800
|
+
const targets = [];
|
|
13801
|
+
for (const p of await repo.list()) {
|
|
13802
|
+
if (only && p !== only)
|
|
13803
|
+
continue;
|
|
13804
|
+
const leaf = await repo.read(p);
|
|
13805
|
+
if (leaf?.kind === "sealed")
|
|
13806
|
+
targets.push(p);
|
|
13807
|
+
}
|
|
13808
|
+
if (only && !targets.length)
|
|
13809
|
+
die(`not a sealed path: ${only}`);
|
|
13810
|
+
let rewrapped = 0;
|
|
13811
|
+
const skipped = [];
|
|
13812
|
+
for (const path2 of targets) {
|
|
13813
|
+
const resolved = await recipientsForPath(cfg, token, path2);
|
|
13814
|
+
if (!resolved.covered) {
|
|
13815
|
+
skipped.push({ path: path2, reason: "no policy rule covers it (explicit/ad-hoc seal — leave as-is)" });
|
|
13816
|
+
continue;
|
|
13817
|
+
}
|
|
13818
|
+
const opened = await client2.open(path2, actor, self);
|
|
13819
|
+
if (opened === undefined || opened === UNREADABLE2) {
|
|
13820
|
+
skipped.push({ path: path2, reason: "you are not a current recipient — a recipient must run --reapply" });
|
|
13821
|
+
continue;
|
|
13822
|
+
}
|
|
13823
|
+
const leaf = await repo.read(path2);
|
|
13824
|
+
const oldEpoch = leaf?.kind === "sealed" ? JSON.parse(leaf.box).epoch ?? 1 : 1;
|
|
13825
|
+
const recipientPubKeys2 = {};
|
|
13826
|
+
const audienceAccounts2 = [];
|
|
13827
|
+
const { pubFingerprint: pubFingerprint2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
13828
|
+
for (const r of resolved.recipients) {
|
|
13829
|
+
recipientPubKeys2[r.accountId] = r.x25519Pub;
|
|
13830
|
+
audienceAccounts2.push({ accountId: r.accountId, fingerprint: pubFingerprint2(r.x25519Pub), keyEpoch: r.keyEpoch });
|
|
13831
|
+
}
|
|
13832
|
+
await client2.sealToAccounts(path2, opened, recipientPubKeys2, { localRecipients: [actor], epoch: oldEpoch + 1 });
|
|
13833
|
+
recordAudience3(solDir, { path: path2, epoch: oldEpoch + 1, at: Date.now(), accounts: audienceAccounts2, local: [actor] });
|
|
13834
|
+
rewrapped++;
|
|
13835
|
+
if (resolved.unresolved.length && !wantJson)
|
|
13836
|
+
console.error(`note: ${path2}: ${resolved.unresolved.length} audience account(s) have no published key yet — not wrapped: ${resolved.unresolved.join(", ")}`);
|
|
13837
|
+
}
|
|
13838
|
+
saveKeyRing(ring2);
|
|
13839
|
+
if (wantJson) {
|
|
13840
|
+
console.log(JSON.stringify({ reapplied: rewrapped, skipped }));
|
|
13841
|
+
break;
|
|
13842
|
+
}
|
|
13843
|
+
console.log(`reapplied policy to ${rewrapped} sealed path(s) — re-wrapped to the CURRENT audience, epoch bumped.`);
|
|
13844
|
+
for (const s of skipped)
|
|
13845
|
+
console.log(` skipped ${s.path}: ${s.reason}`);
|
|
13846
|
+
console.log(`
|
|
13847
|
+
NO-RECALL: a newly-added writer cannot read PAST sealed content until a reapply re-wraps the CEK to`);
|
|
13848
|
+
console.log(" them; a removed recipient is not retroactively recalled (they may already hold old plaintext). for a");
|
|
13849
|
+
console.log(" LIVE secret (an API key), rotate it AT THE SOURCE — re-sealing only protects content written from now.");
|
|
13850
|
+
break;
|
|
13851
|
+
}
|
|
13852
|
+
if (args.includes("--seal-subtree")) {
|
|
13853
|
+
const dir = positional[0] || die("usage: sol seal <dir> --seal-subtree [recipient...]");
|
|
13854
|
+
const ring2 = loadKeyRing();
|
|
13855
|
+
const { SealedClient: SealedClient3 } = await Promise.resolve().then(() => (init_sealed_client(), exports_sealed_client));
|
|
13856
|
+
const { parseRecipient: parseRecipient3, resolveRecipient: resolveRecipient3, recordAudience: recordAudience3 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
13857
|
+
const { pubFingerprint: pubFingerprint2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
13858
|
+
const dirUrl2 = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL2).replace(/\/+$/, "");
|
|
13859
|
+
const explicit = positional.slice(1);
|
|
13860
|
+
const recipientPubKeys2 = {};
|
|
13861
|
+
const audienceAccounts2 = [];
|
|
13862
|
+
const localRecipients2 = new Set([actor]);
|
|
13863
|
+
const crossAccount2 = [];
|
|
13864
|
+
let policyApplied2 = false;
|
|
13865
|
+
if (!explicit.length) {
|
|
13866
|
+
const cfg = resolveRemote2(solDir);
|
|
13867
|
+
const token = process.env.SOL_TOKEN;
|
|
13868
|
+
if (cfg && token) {
|
|
13869
|
+
const resolved = await recipientsForPath(cfg, token, dir).catch(() => {
|
|
13870
|
+
return;
|
|
13871
|
+
});
|
|
13872
|
+
if (resolved?.covered) {
|
|
13873
|
+
policyApplied2 = true;
|
|
13874
|
+
for (const r of resolved.recipients) {
|
|
13875
|
+
recipientPubKeys2[r.accountId] = r.x25519Pub;
|
|
13876
|
+
audienceAccounts2.push({ accountId: r.accountId, fingerprint: pubFingerprint2(r.x25519Pub), keyEpoch: r.keyEpoch });
|
|
13877
|
+
if (r.accountId !== actor)
|
|
13878
|
+
crossAccount2.push(r.accountId);
|
|
13879
|
+
}
|
|
13880
|
+
}
|
|
13881
|
+
}
|
|
13882
|
+
}
|
|
13883
|
+
for (const raw of explicit) {
|
|
13884
|
+
const spec = parseRecipient3(raw);
|
|
13885
|
+
if (spec.accountId === actor) {
|
|
13886
|
+
localRecipients2.add(actor);
|
|
13887
|
+
continue;
|
|
13888
|
+
}
|
|
13889
|
+
const resolved = await resolveRecipient3(dirUrl2, spec);
|
|
13890
|
+
if (!resolved) {
|
|
13891
|
+
localRecipients2.add(spec.accountId);
|
|
13892
|
+
continue;
|
|
13893
|
+
}
|
|
13894
|
+
if (!resolved.selfSigned)
|
|
13895
|
+
die(`@${spec.accountId}: directory key has an INVALID self-signature — possible MITM, refusing to seal.`);
|
|
13896
|
+
if (resolved.pinMismatch)
|
|
13897
|
+
die(`@${spec.accountId}: directory fingerprint differs from your pinned key — possible MITM/rotation, refusing to seal.`);
|
|
13898
|
+
recipientPubKeys2[spec.accountId] = resolved.entry.x25519Pub;
|
|
13899
|
+
audienceAccounts2.push({ accountId: spec.accountId, fingerprint: resolved.fingerprint, keyEpoch: resolved.entry.keyEpoch });
|
|
13900
|
+
crossAccount2.push(spec.accountId);
|
|
13901
|
+
}
|
|
13902
|
+
const client2 = new SealedClient3(repo, ring2);
|
|
13903
|
+
const head = await client2.sealSubtree(dir, recipientPubKeys2, { localRecipients: [...localRecipients2] });
|
|
13904
|
+
if (head === undefined)
|
|
13905
|
+
die(`not a directory in the current head: ${dir} (seal a tracked directory; commit its files first)`);
|
|
13906
|
+
saveKeyRing(ring2);
|
|
13907
|
+
recordAudience3(solDir, { path: dir, epoch: 1, at: Date.now(), accounts: audienceAccounts2, local: [...localRecipients2] });
|
|
13908
|
+
const recipients2 = [...crossAccount2.map((a) => `@${a}`), ...[...localRecipients2]];
|
|
13909
|
+
if (wantJson) {
|
|
13910
|
+
console.log(JSON.stringify({ path: dir, sealedSubtree: true, recipients: recipients2, audience: { accounts: audienceAccounts2, local: [...localRecipients2] } }));
|
|
13911
|
+
break;
|
|
13912
|
+
}
|
|
13913
|
+
console.log(`sealed subtree ${dir} -> ${recipients2.join(", ")} — the directory's CONTENTS are now host-blind: the host sees the dir name + one opaque blob, never the child names/count.`);
|
|
13914
|
+
if (policyApplied2)
|
|
13915
|
+
console.log(" audience resolved from the VisibilityPolicy (`sol hide`) for this dir.");
|
|
13916
|
+
if (crossAccount2.length)
|
|
13917
|
+
console.log(` cross-account: ${crossAccount2.map((a) => `@${a}`).join(", ")} — wrapped to their published X25519 keys; the host stays blind.`);
|
|
13918
|
+
break;
|
|
13919
|
+
}
|
|
13920
|
+
const path = positional[0] || die("usage: sol seal <path> [recipient...] | sol seal --reapply [<path>]");
|
|
13921
|
+
const rawRecipients = positional.slice(1);
|
|
13922
|
+
let content = await repo.readFile(path);
|
|
13923
|
+
if (content === SEALED && !args.includes("--hide-names"))
|
|
13924
|
+
die("already sealed: " + path);
|
|
13925
|
+
if (content === undefined) {
|
|
13926
|
+
const abs = join19(cwd, path);
|
|
13927
|
+
if (!existsSync18(abs))
|
|
13928
|
+
die("no such file: " + path);
|
|
13929
|
+
content = readFileSync18(abs, "utf8");
|
|
13930
|
+
}
|
|
13931
|
+
const ring = loadKeyRing();
|
|
13932
|
+
const { SealedClient: SealedClient2 } = await Promise.resolve().then(() => (init_sealed_client(), exports_sealed_client));
|
|
13933
|
+
const { parseRecipient: parseRecipient2, resolveRecipient: resolveRecipient2, recordAudience: recordAudience2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
13934
|
+
const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL2).replace(/\/+$/, "");
|
|
13935
|
+
const recipientPubKeys = {};
|
|
13936
|
+
const audienceAccounts = [];
|
|
13937
|
+
const localRecipients = new Set([actor]);
|
|
13938
|
+
const crossAccount = [];
|
|
13939
|
+
const recoveryPubKeys = {};
|
|
13940
|
+
const escrowSlots = [];
|
|
13941
|
+
const escrowUnresolved = [];
|
|
13942
|
+
const wantEscrow = args.includes("--escrow");
|
|
13943
|
+
let policyApplied = false;
|
|
13944
|
+
let hideNamesFromPolicy = false;
|
|
13945
|
+
let hideExistenceFromPolicy = false;
|
|
13946
|
+
if (!rawRecipients.length) {
|
|
13947
|
+
const cfg = resolveRemote2(solDir);
|
|
13948
|
+
const token = process.env.SOL_TOKEN;
|
|
13949
|
+
if (cfg && token) {
|
|
13950
|
+
const { pubFingerprint: pubFingerprint2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
13951
|
+
const resolved = await recipientsForPath(cfg, token, path).catch(() => {
|
|
13952
|
+
return;
|
|
13953
|
+
});
|
|
13954
|
+
if (resolved?.covered) {
|
|
13955
|
+
policyApplied = true;
|
|
13956
|
+
hideNamesFromPolicy = resolved.hideNames === true;
|
|
13957
|
+
hideExistenceFromPolicy = resolved.hideExistence === true;
|
|
13958
|
+
for (const r of resolved.recipients) {
|
|
13959
|
+
recipientPubKeys[r.accountId] = r.x25519Pub;
|
|
13960
|
+
audienceAccounts.push({ accountId: r.accountId, fingerprint: pubFingerprint2(r.x25519Pub), keyEpoch: r.keyEpoch });
|
|
13961
|
+
if (r.accountId !== actor)
|
|
13962
|
+
crossAccount.push(r.accountId);
|
|
13963
|
+
}
|
|
13964
|
+
if (resolved.unresolved.length && !wantJson)
|
|
13965
|
+
console.error(`note: ${resolved.unresolved.length} audience account(s) have no published key yet — not wrapped: ${resolved.unresolved.join(", ")} (they cannot decrypt until they \`sol keys publish\`).`);
|
|
13966
|
+
for (const [slot, pub] of Object.entries(resolved.recoveryKeys ?? {})) {
|
|
13967
|
+
recoveryPubKeys[slot] = pub;
|
|
13968
|
+
escrowSlots.push(slot);
|
|
13969
|
+
}
|
|
13970
|
+
for (const slot of resolved.unresolvedRecovery ?? [])
|
|
13971
|
+
escrowUnresolved.push(slot);
|
|
13972
|
+
}
|
|
13973
|
+
}
|
|
13974
|
+
}
|
|
13975
|
+
if (wantEscrow && !escrowSlots.length) {
|
|
13976
|
+
const cfg = resolveRemote2(solDir);
|
|
13977
|
+
const token = process.env.SOL_TOKEN;
|
|
13978
|
+
if (cfg && token) {
|
|
13979
|
+
const { recipientsForAudience: recipientsForAudience2 } = await Promise.resolve().then(() => (init_remote(), exports_remote));
|
|
13980
|
+
const r = await recipientsForAudience2(cfg, token, { kind: "owner" }, ["org-escrow"]).catch(() => {
|
|
13981
|
+
return;
|
|
13982
|
+
});
|
|
13983
|
+
for (const [slot, pub] of Object.entries(r?.recoveryKeys ?? {})) {
|
|
13984
|
+
recoveryPubKeys[slot] = pub;
|
|
13985
|
+
escrowSlots.push(slot);
|
|
13986
|
+
}
|
|
13987
|
+
for (const slot of r?.unresolvedRecovery ?? [])
|
|
13988
|
+
escrowUnresolved.push(slot);
|
|
13989
|
+
}
|
|
13990
|
+
if (!escrowSlots.length && !escrowUnresolved.length)
|
|
13991
|
+
escrowUnresolved.push("org-escrow");
|
|
13992
|
+
}
|
|
13993
|
+
for (const raw of rawRecipients) {
|
|
13994
|
+
const spec = parseRecipient2(raw);
|
|
13995
|
+
if (spec.accountId === actor) {
|
|
13996
|
+
localRecipients.add(actor);
|
|
13997
|
+
continue;
|
|
13998
|
+
}
|
|
13999
|
+
const resolved = await resolveRecipient2(dirUrl, spec);
|
|
14000
|
+
if (!resolved) {
|
|
14001
|
+
localRecipients.add(spec.accountId);
|
|
14002
|
+
continue;
|
|
14003
|
+
}
|
|
14004
|
+
if (!resolved.selfSigned) {
|
|
14005
|
+
die(`@${spec.accountId}: directory key has an INVALID self-signature — possible MITM, refusing to seal. verify out-of-band: \`sol keys verify ${spec.accountId} <fpr>\``);
|
|
14006
|
+
}
|
|
14007
|
+
if (resolved.pinMismatch) {
|
|
14008
|
+
die(`@${spec.accountId}: directory fingerprint ${resolved.fingerprint} DIFFERS from your pinned key — possible MITM/rotation, refusing to seal. re-verify: \`sol keys verify ${spec.accountId} <fpr>\``);
|
|
14009
|
+
}
|
|
14010
|
+
if (!resolved.pinned && !wantJson) {
|
|
14011
|
+
console.error(`note: @${spec.accountId} pinned on first use (fingerprint ${resolved.fingerprint}). confirm it out-of-band with \`sol keys verify ${spec.accountId} <fpr>\`.`);
|
|
14012
|
+
}
|
|
14013
|
+
recipientPubKeys[spec.accountId] = resolved.entry.x25519Pub;
|
|
14014
|
+
audienceAccounts.push({ accountId: spec.accountId, fingerprint: resolved.fingerprint, keyEpoch: resolved.entry.keyEpoch });
|
|
14015
|
+
crossAccount.push(spec.accountId);
|
|
14016
|
+
}
|
|
14017
|
+
const bareSelfSeal = !rawRecipients.length && !policyApplied && !Object.keys(recipientPubKeys).length && !escrowSlots.length;
|
|
14018
|
+
if (bareSelfSeal) {
|
|
14019
|
+
const { loadIdentity: loadIdentity2 } = await Promise.resolve().then(() => (init_identity_store(), exports_identity_store));
|
|
14020
|
+
const stored = loadIdentity2();
|
|
14021
|
+
if (stored) {
|
|
14022
|
+
const { pubFingerprint: pubFingerprint2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
14023
|
+
recipientPubKeys[stored.accountId] = stored.x25519Pub;
|
|
14024
|
+
audienceAccounts.push({ accountId: stored.accountId, fingerprint: pubFingerprint2(stored.x25519Pub), keyEpoch: stored.keyEpoch });
|
|
14025
|
+
localRecipients.add(actor);
|
|
14026
|
+
} else if (!wantJson) {
|
|
14027
|
+
console.error(`WARNING: no identity (~/.sol/identity.json) — sealing "${path}" with a LOCAL-ONLY symmetric key.`);
|
|
14028
|
+
console.error(` this box will NOT decrypt on another machine even with your recovery code (the key lives only in this repo's .sol keyring).`);
|
|
14029
|
+
console.error(` run \`sol keys init\` first for a PORTABLE self-seal (decryptable with your recovery code on any of your devices).`);
|
|
14030
|
+
}
|
|
14031
|
+
}
|
|
14032
|
+
const client = new SealedClient2(repo, ring);
|
|
14033
|
+
const wantHideName = args.includes("--hide-names") || hideNamesFromPolicy;
|
|
14034
|
+
const wantHideExistence = args.includes("--hide-existence") || hideExistenceFromPolicy;
|
|
14035
|
+
const onTree = await repo.readFile(path);
|
|
14036
|
+
const alreadySealed = onTree === SEALED;
|
|
14037
|
+
const { isHiddenPath: isHiddenPathFn } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
14038
|
+
const alreadyHidden = onTree === undefined && isHiddenPathFn(solDir, path);
|
|
14039
|
+
const untrackedFresh = onTree === undefined && !alreadyHidden && (wantHideName || wantHideExistence);
|
|
14040
|
+
let hiddenSlot;
|
|
14041
|
+
if (wantHideExistence && (onTree !== undefined || untrackedFresh)) {
|
|
14042
|
+
const strict = args.includes("--strict");
|
|
14043
|
+
const priorOps = await log.history();
|
|
14044
|
+
const burned = priorOps.some((o) => o.path === path || o.path.startsWith(path + "/"));
|
|
14045
|
+
if (burned) {
|
|
14046
|
+
if (strict)
|
|
14047
|
+
die(`--strict: "${path}" already appears in op history — its name is burned and cannot be truly existence-hidden. hide a fresh path, or drop --strict to proceed (the old name stays recoverable from history).`);
|
|
14048
|
+
if (!wantJson)
|
|
14049
|
+
console.error(`WARNING: "${path}" already appears in op history — existence-hiding protects the name going forward, but the OLD name is still recoverable from prior ops. use \`--strict\` to refuse, or hide a fresh path for a true zero-day filename.`);
|
|
14050
|
+
}
|
|
14051
|
+
const { loadSelfIdentity: loadSelfIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
14052
|
+
const { openContent: openContent2, UNREADABLE: UNREADABLE2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
14053
|
+
const self = loadSelfIdentity2();
|
|
14054
|
+
const openPrior = (boxStr) => {
|
|
14055
|
+
try {
|
|
14056
|
+
const opened = openContent2(ring, JSON.parse(boxStr), actor, self);
|
|
14057
|
+
return opened === UNREADABLE2 ? undefined : opened;
|
|
14058
|
+
} catch {
|
|
14059
|
+
return;
|
|
14060
|
+
}
|
|
14061
|
+
};
|
|
14062
|
+
const exHead = await client.hideExistence(path, recipientPubKeys, { localRecipients: [...localRecipients], openPrior, contentSeal: untrackedFresh || !alreadySealed, ...untrackedFresh ? { absentContent: content } : {}, ...escrowSlots.length ? { recoveryPubKeys } : {} });
|
|
14063
|
+
if (exHead === undefined)
|
|
14064
|
+
die(`nothing to existence-hide at "${path}" — the path is not in the tree and no on-disk bytes were found. (a fresh hide needs the file on disk; an already-hidden path edits in your revealed working tree.)`);
|
|
14065
|
+
saveKeyRing(ring);
|
|
14066
|
+
const { recordHiddenPath: recordHiddenPath2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
14067
|
+
recordHiddenPath2(solDir, path, "existence");
|
|
14068
|
+
if (Object.keys(recipientPubKeys).length)
|
|
14069
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: audienceAccounts, local: [...localRecipients] });
|
|
14070
|
+
else
|
|
14071
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: [], local: [...localRecipients] });
|
|
14072
|
+
const recipients2 = [...crossAccount.map((a) => `@${a}`), ...[...localRecipients]];
|
|
14073
|
+
const segs2 = path.split("/").filter(Boolean);
|
|
14074
|
+
const dir2 = segs2.slice(0, -1).join("/") || ".";
|
|
14075
|
+
if (wantJson) {
|
|
14076
|
+
console.log(JSON.stringify({ path, sealed: true, recipients: recipients2, audience: { accounts: audienceAccounts, local: [...localRecipients] }, existenceHidden: true, hiddenDir: dir2 }));
|
|
14077
|
+
break;
|
|
14078
|
+
}
|
|
14079
|
+
console.log(`existence-hidden ${path} (L3) — the host now sees only a per-dir \x00hidden marker in "${dir2}", never "${path.split("/").pop()}", its bytes, or that this path exists. recipients reveal it locally.`);
|
|
14080
|
+
if (policyApplied)
|
|
14081
|
+
console.log(" audience resolved from the VisibilityPolicy (`sol hide`) for this path.");
|
|
14082
|
+
if (crossAccount.length)
|
|
14083
|
+
console.log(` cross-account: ${crossAccount.map((a) => `@${a}`).join(", ")} — wrapped to their published X25519 keys; the host stays blind.`);
|
|
14084
|
+
break;
|
|
14085
|
+
}
|
|
14086
|
+
if (wantHideExistence && onTree === undefined) {
|
|
14087
|
+
die(`"${path}" is already existence-hidden (absent from the host tree). edit it in your revealed working tree, then re-run \`sol seal ${path} --hide-existence\` to re-seal the new bytes under the same \x00hidden sidecar.`);
|
|
14088
|
+
}
|
|
14089
|
+
if (wantHideName && onTree === undefined) {
|
|
14090
|
+
const { slotForPath: slotForPath2, recordNameSlot: recordNameSlot2, recordHiddenPath: recordHiddenPath2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
14091
|
+
const existingSlot = slotForPath2(solDir, path);
|
|
14092
|
+
if (existingSlot) {
|
|
14093
|
+
const segs = path.split("/").filter(Boolean);
|
|
14094
|
+
const realDir = segs.slice(0, -1).join("/");
|
|
14095
|
+
const name = segs[segs.length - 1];
|
|
14096
|
+
await client.reHideName(realDir, name, existingSlot, content, recipientPubKeys, { localRecipients: [...localRecipients], ...escrowSlots.length ? { recoveryPubKeys } : {} });
|
|
14097
|
+
recordNameSlot2(solDir, path, existingSlot);
|
|
14098
|
+
recordHiddenPath2(solDir, path, "name");
|
|
14099
|
+
if (Object.keys(recipientPubKeys).length)
|
|
14100
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: audienceAccounts, local: [...localRecipients] });
|
|
14101
|
+
else
|
|
14102
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: [], local: [...localRecipients] });
|
|
14103
|
+
saveKeyRing(ring);
|
|
14104
|
+
hiddenSlot = existingSlot;
|
|
14105
|
+
const recipients2 = [...crossAccount.map((a) => `@${a}`), ...[...localRecipients]];
|
|
14106
|
+
if (wantJson) {
|
|
14107
|
+
console.log(JSON.stringify({ path, sealed: true, recipients: recipients2, audience: { accounts: audienceAccounts, local: [...localRecipients] }, nameHidden: true, slotId: hiddenSlot, modified: true }));
|
|
14108
|
+
break;
|
|
14109
|
+
}
|
|
14110
|
+
console.log(`re-sealed ${path} (edit) under its stable slot \x00slot\x00${hiddenSlot} — converge sees a MODIFICATION, the name stays host-blind`);
|
|
14111
|
+
break;
|
|
14112
|
+
}
|
|
14113
|
+
}
|
|
14114
|
+
if (!(wantHideName && !alreadySealed)) {
|
|
14115
|
+
if (Object.keys(recipientPubKeys).length) {
|
|
14116
|
+
await client.sealToAccounts(path, content, recipientPubKeys, { localRecipients: [...localRecipients], ...escrowSlots.length ? { recoveryPubKeys } : {} });
|
|
14117
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: audienceAccounts, local: [...localRecipients] });
|
|
14118
|
+
} else if (escrowSlots.length) {
|
|
14119
|
+
await client.sealToAccounts(path, content, {}, { localRecipients: [...localRecipients], recoveryPubKeys });
|
|
14120
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: [], local: [...localRecipients] });
|
|
14121
|
+
} else {
|
|
14122
|
+
await client.seal(path, content, [...localRecipients]);
|
|
14123
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: [], local: [...localRecipients] });
|
|
14124
|
+
}
|
|
14125
|
+
saveKeyRing(ring);
|
|
14126
|
+
if (onTree !== undefined && onTree !== SEALED) {
|
|
14127
|
+
const { historyHasCleartext: historyHasCleartext2, addPendingScrub: addPendingScrub2 } = await Promise.resolve().then(() => (init_secret_scrub(), exports_secret_scrub));
|
|
14128
|
+
if (historyHasCleartext2(solDir, await log.history(), path)) {
|
|
14129
|
+
addPendingScrub2(solDir, path);
|
|
14130
|
+
if (!wantJson) {
|
|
14131
|
+
console.error(`
|
|
14132
|
+
WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal cleartext is STILL recoverable from history (\`sol restore\`) and would ship to every clone on push.`);
|
|
14133
|
+
console.error(` push is now BLOCKED for safety. run \`sol seal ${path} --scrub-history\` to rewrite it out of history, then push.
|
|
14134
|
+
`);
|
|
14135
|
+
}
|
|
14136
|
+
}
|
|
14137
|
+
}
|
|
14138
|
+
}
|
|
14139
|
+
if (wantHideName) {
|
|
14140
|
+
const { slotForPath: slotForPath2, recordNameSlot: recordNameSlot2, recordHiddenPath: recordHiddenPath2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
14141
|
+
const nameHidePubKeys = { ...recipientPubKeys };
|
|
14142
|
+
const existing = slotForPath2(solDir, path);
|
|
14143
|
+
hiddenSlot = await client.hideName(path, nameHidePubKeys, { localRecipients: [...localRecipients], slotId: existing, contentSeal: untrackedFresh || !alreadySealed, ...untrackedFresh ? { absentContent: content } : {}, ...escrowSlots.length ? { recoveryPubKeys } : {} });
|
|
14144
|
+
if (hiddenSlot) {
|
|
14145
|
+
recordNameSlot2(solDir, path, hiddenSlot);
|
|
14146
|
+
recordHiddenPath2(solDir, path, "name");
|
|
14147
|
+
if (Object.keys(recipientPubKeys).length)
|
|
14148
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: audienceAccounts, local: [...localRecipients] });
|
|
14149
|
+
else
|
|
14150
|
+
recordAudience2(solDir, { path, epoch: 1, at: Date.now(), accounts: [], local: [...localRecipients] });
|
|
14151
|
+
} else {
|
|
14152
|
+
die(`nothing to name-hide at "${path}" — the path is not in the tree and no on-disk bytes were found. (a fresh hide needs the file on disk.)`);
|
|
14153
|
+
}
|
|
14154
|
+
saveKeyRing(ring);
|
|
14155
|
+
}
|
|
14156
|
+
const recipients = [...crossAccount.map((a) => `@${a}`), ...[...localRecipients]];
|
|
14157
|
+
if (wantJson) {
|
|
14158
|
+
console.log(JSON.stringify({ path, sealed: true, recipients, audience: { accounts: audienceAccounts, local: [...localRecipients] }, ...hiddenSlot ? { nameHidden: true, slotId: hiddenSlot } : {}, ...escrowSlots.length ? { escrow: escrowSlots } : {}, ...escrowUnresolved.length ? { escrowUnresolved } : {} }));
|
|
14159
|
+
break;
|
|
14160
|
+
}
|
|
14161
|
+
console.log(`sealed ${path} to ${recipients.join(", ")} — content is now host-blind ciphertext (the keystore stays local)`);
|
|
14162
|
+
if (bareSelfSeal && audienceAccounts.length)
|
|
14163
|
+
console.log(` PORTABLE self-seal: also wrapped to your own identity (@${audienceAccounts[0].accountId}) — it decrypts with your recovery code on any of your machines.`);
|
|
14164
|
+
if (hiddenSlot)
|
|
14165
|
+
console.log(` name hidden (L2): the host now sees an opaque slot \x00slot\x00${hiddenSlot}, never "${path.split("/").pop()}" — recipients rebuild the real name locally.`);
|
|
14166
|
+
if (policyApplied)
|
|
14167
|
+
console.log(" audience resolved from the VisibilityPolicy (`sol hide`) for this path — server named the accounts, the client minted the lockboxes.");
|
|
14168
|
+
if (crossAccount.length)
|
|
14169
|
+
console.log(` cross-account: ${crossAccount.map((a) => `@${a}`).join(", ")} — wrapped to their published X25519 keys; the host stays blind.`);
|
|
14170
|
+
if (escrowSlots.length) {
|
|
14171
|
+
console.log(` ⚠ ESCROW ON: the CEK is ALSO wrapped to the org-escrow recovery slot [${escrowSlots.join(", ")}] — an org admin holding the escrow PRIVATE key CAN recover this content. it is NOT hidden from them.`);
|
|
14172
|
+
console.log(" (the host still never holds the escrow private key — recovery is the admin's, off-host.)");
|
|
14173
|
+
}
|
|
14174
|
+
if (escrowUnresolved.length)
|
|
14175
|
+
console.log(` ⚠ ESCROW REQUESTED BUT UNRESOLVED: no published key for [${escrowUnresolved.join(", ")}] — the content is NOT escrow-recoverable. publish the org-escrow key, then \`sol seal --reapply\`.`);
|
|
14176
|
+
break;
|
|
14177
|
+
}
|
|
14178
|
+
case "sealed": {
|
|
14179
|
+
const { repo } = open();
|
|
14180
|
+
const wantJson = args.includes("--json");
|
|
14181
|
+
if (args.includes("--check")) {
|
|
14182
|
+
const cfg = resolveRemote2(solDir) || die("no remote — drift is checked against the policy on the repo; `sol remote <url> <repo>` first");
|
|
14183
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
14184
|
+
const { policyCheck: policyCheck2 } = await Promise.resolve().then(() => (init_remote(), exports_remote));
|
|
14185
|
+
const states = [];
|
|
14186
|
+
for (const path of await repo.list()) {
|
|
14187
|
+
const leaf = await repo.read(path);
|
|
14188
|
+
if (!leaf || leaf.kind !== "sealed")
|
|
14189
|
+
continue;
|
|
14190
|
+
const box = JSON.parse(leaf.box);
|
|
14191
|
+
states.push({ path, recipients: Object.keys(box.asym ?? {}), recovery: Object.keys(box.recovery ?? {}) });
|
|
14192
|
+
}
|
|
14193
|
+
if (!states.length) {
|
|
14194
|
+
if (wantJson) {
|
|
14195
|
+
console.log(JSON.stringify({ count: 0, drifted: 0, reports: [] }));
|
|
14196
|
+
break;
|
|
14197
|
+
}
|
|
14198
|
+
console.log("no sealed paths in this repo.");
|
|
14199
|
+
break;
|
|
14200
|
+
}
|
|
14201
|
+
const res = await policyCheck2(cfg, token, states);
|
|
14202
|
+
if (wantJson) {
|
|
14203
|
+
console.log(JSON.stringify(res));
|
|
14204
|
+
break;
|
|
14205
|
+
}
|
|
14206
|
+
const describe = (a) => !a ? "(no rule)" : a.kind === "role" ? `role>=${a.min}` : a.kind === "team" ? `team:${a.teamId}` : a.kind === "users" ? `users:${a.userIds.join(",")}` : "owner";
|
|
14207
|
+
const drifted = res.reports.filter((r) => r.covered && !r.inPolicy);
|
|
14208
|
+
const inPolicy = res.reports.filter((r) => r.covered && r.inPolicy);
|
|
14209
|
+
const adhoc = res.reports.filter((r) => !r.covered);
|
|
14210
|
+
if (!drifted.length)
|
|
14211
|
+
console.log(`all ${inPolicy.length} policy-covered sealed path(s) are IN POLICY — recipient sets match the audience.`);
|
|
14212
|
+
for (const r of drifted) {
|
|
14213
|
+
console.log(`DRIFT ${r.path} -> ${describe(r.audience)} (recipients out of policy)`);
|
|
14214
|
+
if (r.missing.length)
|
|
14215
|
+
console.log(` missing (audience says recipient, box does NOT wrap to): ${r.missing.map((a) => `@${a}`).join(", ")}`);
|
|
14216
|
+
if (r.extra.length)
|
|
14217
|
+
console.log(` extra (box still wraps to, audience no longer includes): ${r.extra.map((a) => `@${a}`).join(", ")}`);
|
|
14218
|
+
if (r.recoveryDrift)
|
|
14219
|
+
console.log(` escrow (rule wants [${r.expectedRecovery.join(",") || "none"}], box has [${r.actualRecovery.join(",") || "none"}])`);
|
|
14220
|
+
}
|
|
14221
|
+
if (inPolicy.length && drifted.length)
|
|
14222
|
+
console.log(`(${inPolicy.length} path(s) in policy)`);
|
|
14223
|
+
if (adhoc.length)
|
|
14224
|
+
console.log(`(${adhoc.length} ad-hoc/explicit seal(s) not governed by any rule — left as-is)`);
|
|
14225
|
+
if (drifted.length) {
|
|
14226
|
+
console.log(`
|
|
14227
|
+
${drifted.length} sealed path(s) are OUT OF POLICY. re-wrap them to the current audience:`);
|
|
14228
|
+
console.log(" sol seal --reapply # all policy-covered sealed paths");
|
|
14229
|
+
console.log(" sol seal --reapply <path> # one path");
|
|
14230
|
+
console.log(" NO-RECALL: a removed recipient is not retroactively recalled (they may already hold old plaintext);");
|
|
14231
|
+
console.log(" a newly-added reader cannot read PAST content until --reapply re-wraps the CEK to them.");
|
|
14232
|
+
}
|
|
14233
|
+
break;
|
|
14234
|
+
}
|
|
14235
|
+
const { loadAudiences: loadAudiences2, loadSelfIdentity: loadSelfIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
14236
|
+
const { openContent: openContent2, UNREADABLE: UNREADABLE2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
14237
|
+
const { parseStruct: parseStruct2 } = await Promise.resolve().then(() => (init_struct(), exports_struct));
|
|
14238
|
+
const audiences = loadAudiences2(solDir);
|
|
14239
|
+
const ring = existsSync18(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
|
|
14240
|
+
const self = loadSelfIdentity2();
|
|
14241
|
+
const levelOf = (boxStr) => {
|
|
14242
|
+
try {
|
|
14243
|
+
const opened = openContent2(ring, JSON.parse(boxStr), actor, self);
|
|
14244
|
+
if (opened !== UNREADABLE2) {
|
|
14245
|
+
const st = parseStruct2(opened);
|
|
14246
|
+
if (st?.__solStruct === "subtree")
|
|
14247
|
+
return { level: "subtree" };
|
|
14248
|
+
if (st?.__solStruct === "name")
|
|
14249
|
+
return { level: "name", realName: st.name };
|
|
14250
|
+
if (st?.__solStruct === "entries")
|
|
14251
|
+
return { level: "existence", hiddenNames: Object.keys(st.slots) };
|
|
14252
|
+
}
|
|
14253
|
+
} catch {}
|
|
14254
|
+
return { level: "content" };
|
|
14255
|
+
};
|
|
14256
|
+
const { slotIdFromKey: slotIdFromKey2, HIDDEN_KEY: HIDDEN_KEY2 } = await Promise.resolve().then(() => (init_struct(), exports_struct));
|
|
14257
|
+
const sealedPaths = [];
|
|
14258
|
+
for (const path of await repo.list()) {
|
|
14259
|
+
const leaf = await repo.read(path);
|
|
14260
|
+
if (!leaf || leaf.kind !== "sealed")
|
|
14261
|
+
continue;
|
|
14262
|
+
const box = JSON.parse(leaf.box);
|
|
14263
|
+
const rec = audiences[path];
|
|
14264
|
+
const asymAccounts = Object.keys(box.asym ?? {});
|
|
14265
|
+
const accounts = asymAccounts.map((accountId) => {
|
|
14266
|
+
const r = rec?.accounts.find((a) => a.accountId === accountId);
|
|
14267
|
+
return { accountId, fingerprint: r?.fingerprint ?? "(unknown — sealed elsewhere)", keyEpoch: r?.keyEpoch ?? 0 };
|
|
14268
|
+
});
|
|
14269
|
+
const local = Object.keys(box.lockboxes ?? {});
|
|
14270
|
+
const escrow = Object.keys(box.recovery ?? {});
|
|
14271
|
+
const lv = levelOf(leaf.box);
|
|
14272
|
+
const seg = path.split("/").pop() ?? path;
|
|
14273
|
+
const slotId = slotIdFromKey2(seg);
|
|
14274
|
+
const isHiddenSidecar = seg === HIDDEN_KEY2;
|
|
14275
|
+
const dir = path.split("/").slice(0, -1).join("/") || ".";
|
|
14276
|
+
const display = isHiddenSidecar ? lv.hiddenNames ? `${dir}/ (existence-hidden: ${lv.hiddenNames.map((k) => slotIdFromKey2(k) ? `[slot ${slotIdFromKey2(k)}]` : k).join(", ")})` : `${dir}/ [per-dir \x00hidden marker — ${">="}1 hidden child, names sealed]` : slotId !== undefined ? lv.realName ? `${path.split("/").slice(0, -1).concat(lv.realName).join("/")} (slot ${slotId})` : `[locked] slot ${slotId} (name hidden)` : path;
|
|
14277
|
+
sealedPaths.push({ path, display, level: lv.level, ...slotId !== undefined ? { slotId } : {}, ...lv.realName ? { realName: lv.realName } : {}, ...lv.hiddenNames ? { hiddenNames: lv.hiddenNames } : {}, epoch: box.epoch ?? 1, accounts, local, escrow });
|
|
14278
|
+
}
|
|
14279
|
+
if (wantJson) {
|
|
14280
|
+
console.log(JSON.stringify({ count: sealedPaths.length, sealed: sealedPaths }));
|
|
14281
|
+
break;
|
|
14282
|
+
}
|
|
14283
|
+
if (!sealedPaths.length) {
|
|
14284
|
+
console.log("no sealed paths in this repo.");
|
|
14285
|
+
break;
|
|
14286
|
+
}
|
|
14287
|
+
for (const s of sealedPaths) {
|
|
14288
|
+
const kind = s.level === "existence" ? "existence " : s.level === "subtree" ? "sealed dir " : s.level === "name" ? "name-hidden" : "sealed file";
|
|
14289
|
+
console.log(`[${kind}] ${s.display} (epoch ${s.epoch})`);
|
|
14290
|
+
for (const a of s.accounts)
|
|
14291
|
+
console.log(` @${a.accountId} ${a.fingerprint}${a.keyEpoch ? ` (key epoch ${a.keyEpoch})` : ""}`);
|
|
14292
|
+
if (s.local.length)
|
|
14293
|
+
console.log(` local: ${s.local.join(", ")}`);
|
|
14294
|
+
if (s.escrow.length)
|
|
14295
|
+
console.log(` ⚠ escrow: [${s.escrow.join(", ")}] — an org-escrow key-holder CAN recover this content (it is NOT hidden from admins).`);
|
|
14296
|
+
}
|
|
14297
|
+
break;
|
|
14298
|
+
}
|
|
14299
|
+
case "open": {
|
|
14300
|
+
const recoveryFlagIdx = args.indexOf("--recovery");
|
|
14301
|
+
const path = args.filter((a) => !a.startsWith("-"))[0] || die("usage: sol open <path> [--recovery [slotId]] [--as <escrowAccountId>]");
|
|
14302
|
+
const wantJson = args.includes("--json");
|
|
14303
|
+
const repo = open().repo;
|
|
14304
|
+
const leaf = await repo.read(path);
|
|
14305
|
+
if (!leaf)
|
|
14306
|
+
die("no such tracked path: " + path);
|
|
14307
|
+
if (leaf.kind !== "sealed") {
|
|
14308
|
+
process.stdout.write(leaf.kind === "blob" ? leaf.content : "");
|
|
14309
|
+
break;
|
|
14310
|
+
}
|
|
14311
|
+
if (recoveryFlagIdx === -1) {
|
|
14312
|
+
const [{ SealedClient: SealedClient2 }, { UNREADABLE: UNREADABLE3 }, { loadSelfIdentity: loadSelfIdentity2 }] = await Promise.all([Promise.resolve().then(() => (init_sealed_client(), exports_sealed_client)), Promise.resolve().then(() => (init_crypto(), exports_crypto)), Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience))]);
|
|
14313
|
+
const opened2 = await new SealedClient2(repo, loadKeyRing()).open(path, actor, loadSelfIdentity2());
|
|
14314
|
+
const readable = !(opened2 === undefined || opened2 === UNREADABLE3);
|
|
14315
|
+
if (wantJson) {
|
|
14316
|
+
console.log(JSON.stringify({ path, recovered: false, readable, content: readable ? opened2 : null }));
|
|
14317
|
+
break;
|
|
14318
|
+
}
|
|
14319
|
+
process.stdout.write(readable ? opened2 : `<<sealed — you are not a recipient; use --recovery if you hold an escrow key>>
|
|
14320
|
+
`);
|
|
14321
|
+
break;
|
|
14322
|
+
}
|
|
14323
|
+
const box = JSON.parse(leaf.box);
|
|
14324
|
+
const slots = Object.keys(box.recovery ?? {});
|
|
14325
|
+
if (!slots.length)
|
|
14326
|
+
die(`"${path}" has no recovery slot — it was sealed WITHOUT --escrow. only a named recipient can open it.`);
|
|
14327
|
+
const slotArg = args[recoveryFlagIdx + 1] && !args[recoveryFlagIdx + 1].startsWith("-") && args[recoveryFlagIdx + 1] !== path ? args[recoveryFlagIdx + 1] : undefined;
|
|
14328
|
+
const slotId = slotArg ?? (slots.includes("org-escrow") ? "org-escrow" : slots[0]);
|
|
14329
|
+
if (!slots.includes(slotId))
|
|
14330
|
+
die(`"${path}" has no recovery slot "${slotId}". available: ${slots.join(", ")}`);
|
|
14331
|
+
const asIdx = args.indexOf("--as");
|
|
14332
|
+
const escrowAccount = asIdx >= 0 ? args[asIdx + 1] : slotId;
|
|
14333
|
+
const recoveryCode = process.env.SOL_RECOVERY_CODE || die("set SOL_RECOVERY_CODE to the org-escrow recovery code (the escrow private key derives from it off-host; the host never holds it)");
|
|
14334
|
+
const { deriveIdentity: deriveIdentity2, openWithRecovery: openWithRecovery2, UNREADABLE: UNREADABLE2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
|
|
14335
|
+
const escrowId = deriveIdentity2(escrowAccount, recoveryCode, 1);
|
|
14336
|
+
const opened = openWithRecovery2(escrowId.x25519Priv, JSON.parse(leaf.box), slotId);
|
|
14337
|
+
if (opened === UNREADABLE2)
|
|
14338
|
+
die(`recovery FAILED for slot "${slotId}" — wrong escrow code/account, or the box isn't wrapped to this escrow key. (the host holds no key; recovery is yours alone.)`);
|
|
14339
|
+
const { parseStruct: parseStruct2 } = await Promise.resolve().then(() => (init_struct(), exports_struct));
|
|
14340
|
+
const struct = parseStruct2(opened);
|
|
14341
|
+
if (struct) {
|
|
14342
|
+
if (wantJson) {
|
|
14343
|
+
console.log(JSON.stringify({ path, recovered: true, slot: slotId, struct: struct.__solStruct }));
|
|
14344
|
+
break;
|
|
14345
|
+
}
|
|
14346
|
+
console.error(`recovered ${path} via escrow slot "${slotId}" — this is a ${struct.__solStruct} STRUCTURE box, not file bytes. recover the underlying files via a recipient checkout.`);
|
|
14347
|
+
break;
|
|
14348
|
+
}
|
|
14349
|
+
if (wantJson) {
|
|
14350
|
+
console.log(JSON.stringify({ path, recovered: true, slot: slotId, content: opened }));
|
|
14351
|
+
break;
|
|
14352
|
+
}
|
|
14353
|
+
process.stdout.write(opened);
|
|
14354
|
+
break;
|
|
14355
|
+
}
|
|
14356
|
+
case "branch":
|
|
14357
|
+
case "branches": {
|
|
14358
|
+
const { log } = open();
|
|
14359
|
+
const refs = await loadRefs(log);
|
|
14360
|
+
if (cmd === "branches" || !args[0]) {
|
|
14361
|
+
for (const [n, b] of Object.entries(refs.branches))
|
|
14362
|
+
console.log(`${n === refs.current ? "* " : " "}${n} ${(b.head || "empty").slice(0, 12)}`);
|
|
14363
|
+
break;
|
|
14364
|
+
}
|
|
14365
|
+
const name = args[0];
|
|
14366
|
+
if (refs.branches[name] || refs.tags[name])
|
|
14367
|
+
die("already exists: " + name);
|
|
14368
|
+
let head = await log.head() ?? "";
|
|
14369
|
+
if (args[1]) {
|
|
14370
|
+
const resolved = refs.branches[args[1]]?.head ?? refs.tags[args[1]] ?? (args[1].startsWith("h_") ? args[1] : "");
|
|
14371
|
+
if (!resolved)
|
|
14372
|
+
die(`cannot resolve ref '${args[1]}' — use a branch/tag name or a commit hash`);
|
|
14373
|
+
head = resolved;
|
|
14374
|
+
}
|
|
14375
|
+
refs.branches[name] = { head, base: head };
|
|
14376
|
+
saveRefs(refs);
|
|
14377
|
+
console.log(`branch ${name} at ${(head || "empty").slice(0, 12)}`);
|
|
14378
|
+
break;
|
|
14379
|
+
}
|
|
14380
|
+
case "tag": {
|
|
14381
|
+
const { log } = open();
|
|
14382
|
+
const refs = await loadRefs(log);
|
|
14383
|
+
if (!args[0]) {
|
|
14384
|
+
for (const [n, h] of Object.entries(refs.tags))
|
|
14385
|
+
console.log(`${n} ${h.slice(0, 12)}`);
|
|
14386
|
+
break;
|
|
14387
|
+
}
|
|
14388
|
+
const name = args[0];
|
|
14389
|
+
if (refs.branches[name] || refs.tags[name])
|
|
14390
|
+
die("already exists: " + name);
|
|
14391
|
+
refs.tags[name] = await log.head() ?? "";
|
|
14392
|
+
saveRefs(refs);
|
|
14393
|
+
console.log(`tag ${name} at ${(refs.tags[name] || "empty").slice(0, 12)}`);
|
|
14394
|
+
break;
|
|
14395
|
+
}
|
|
14396
|
+
case "switch": {
|
|
14397
|
+
const { repo, log } = open();
|
|
14398
|
+
const name = args[0] || die("switch needs a branch name");
|
|
14399
|
+
const refs = await loadRefs(log);
|
|
14400
|
+
const target = refs.branches[name];
|
|
14401
|
+
if (!target)
|
|
14402
|
+
die(`no such branch: ${name} (run \`sol branch ${name}\` to create it)`);
|
|
14403
|
+
const fromHead = await repo.head();
|
|
14404
|
+
const wc = workingChanges(loadStore(), fromHead);
|
|
14405
|
+
if (wc.added.length + wc.modified.length + wc.removed.length > 0) {
|
|
14406
|
+
die('you have uncommitted changes — commit them first (`sol commit "msg"`), then switch. your work is safe on disk.');
|
|
14407
|
+
}
|
|
14408
|
+
setOpLogHead(target.head);
|
|
14409
|
+
refs.current = name;
|
|
14410
|
+
saveRefs(refs);
|
|
14411
|
+
const n = materializeDiff(loadStore(), fromHead, target.head);
|
|
14412
|
+
console.log(`switched to ${name} (${(target.head || "empty").slice(0, 12)}); ${n} file(s) changed`);
|
|
14413
|
+
break;
|
|
14414
|
+
}
|
|
14415
|
+
case "merge": {
|
|
14416
|
+
const { repo, log } = open();
|
|
14417
|
+
const name = args[0] || die("merge needs a branch name");
|
|
14418
|
+
const refs = await loadRefs(log);
|
|
14419
|
+
const other = refs.branches[name];
|
|
14420
|
+
if (!other)
|
|
14421
|
+
die("no such branch: " + name);
|
|
14422
|
+
if (name === refs.current)
|
|
14423
|
+
die("cannot merge a branch into itself");
|
|
14424
|
+
const mc = workingChanges(loadStore(), await repo.head());
|
|
14425
|
+
if (mc.added.length + mc.modified.length + mc.removed.length > 0) {
|
|
14426
|
+
die("you have uncommitted changes — commit them first, then merge.");
|
|
14427
|
+
}
|
|
14428
|
+
const ours = await log.head() ?? "";
|
|
14429
|
+
const store2 = loadStore();
|
|
14430
|
+
const { merge: merge2 } = await Promise.resolve().then(() => (init_merge(), exports_merge));
|
|
14431
|
+
const result = merge2({ store: store2 }, other.base, ours, other.head);
|
|
14432
|
+
if (result.conflicts.length) {
|
|
14433
|
+
materializeTree(store2, result.head);
|
|
14434
|
+
writeFileSync16(join19(solDir, "MERGE_HEAD"), other.head);
|
|
14435
|
+
saveMergeConflicts(solDir, result.conflicts);
|
|
14436
|
+
console.log(`merge ${name} -> ${result.conflicts.length} conflict(s), left in your working tree (uncommitted):`);
|
|
14437
|
+
for (const c of result.conflicts)
|
|
14438
|
+
console.log(" " + c.path);
|
|
14439
|
+
console.log("resolve the <<<<<<< markers, then `sol commit`");
|
|
14440
|
+
process.exitCode = 1;
|
|
14441
|
+
break;
|
|
14442
|
+
}
|
|
14443
|
+
await persistTree(store2, new FileStore(solDir, objectsDir()), result.head);
|
|
14444
|
+
await appendCommit(log, result.head, `merge ${name} into ${refs.current}`, ours, other.head);
|
|
14445
|
+
refs.branches[refs.current].head = result.head;
|
|
14446
|
+
refs.branches[name].base = other.head;
|
|
14447
|
+
saveRefs(refs);
|
|
14448
|
+
setOpLogHead(result.head);
|
|
14449
|
+
materializeTree(store2, result.head);
|
|
14450
|
+
clearMergeConflicts(solDir);
|
|
14451
|
+
console.log(`merged ${name} into ${refs.current} cleanly -> ${result.head.slice(0, 12)}`);
|
|
14452
|
+
reportSemanticGate2(gateMerge(solDir, cfgDir2(), cwd, result.head), args.includes("--json"));
|
|
14453
|
+
break;
|
|
14454
|
+
}
|
|
14455
|
+
case "undo": {
|
|
14456
|
+
const { repo, log } = open();
|
|
14457
|
+
const refs = await loadRefs(log);
|
|
14458
|
+
const store2 = loadStore();
|
|
14459
|
+
const wc = workingChanges(store2, await repo.head());
|
|
14460
|
+
if (wc.added.length + wc.modified.length + wc.removed.length > 0) {
|
|
14461
|
+
die('you have uncommitted changes — commit them first (`sol commit "msg"`), then undo. your work is safe on disk.');
|
|
14462
|
+
}
|
|
14463
|
+
const head = await log.head() ?? "";
|
|
14464
|
+
const ops = await log.history();
|
|
14465
|
+
const headOp = [...ops].reverse().find((o) => o.type === "checkpoint" && o.rootAfter === head);
|
|
14466
|
+
if (!head || !headOp)
|
|
14467
|
+
die("nothing to undo — no commit on this branch yet.");
|
|
14468
|
+
const parent = headOp.parent;
|
|
14469
|
+
if (parent === undefined)
|
|
14470
|
+
die("nothing to undo — this is the first commit on the branch (it has no parent).");
|
|
14471
|
+
const parentTree = parent || emptyRoot(store2);
|
|
14472
|
+
const message = `undo ${head.slice(0, 8)} — ${headOp.message ?? ""}`.trimEnd();
|
|
14473
|
+
await appendCommit(log, parentTree, message, head);
|
|
14474
|
+
refs.branches[refs.current].head = parentTree;
|
|
14475
|
+
saveRefs(refs);
|
|
14476
|
+
setOpLogHead(parentTree);
|
|
14477
|
+
const n = materializeDiff(store2, head, parentTree);
|
|
14478
|
+
const undone = await log.head() ?? parentTree;
|
|
14479
|
+
if (args.includes("--json")) {
|
|
14480
|
+
console.log(JSON.stringify({ undid: head, head: undone, parent: parentTree || null, filesChanged: n }));
|
|
14481
|
+
break;
|
|
14482
|
+
}
|
|
14483
|
+
console.log(`undid ${head.slice(0, 14)} — branch ${refs.current} now at ${(parentTree || "empty").slice(0, 12)} (${n} file change${n === 1 ? "" : "s"})`);
|
|
14484
|
+
console.log(" the undone commit stays in history (fsck OK) — `sol log --all` to see it; `sol revert` for a preserved-history inverse.");
|
|
14485
|
+
break;
|
|
14486
|
+
}
|
|
14487
|
+
case "revert": {
|
|
14488
|
+
const { repo, log } = open();
|
|
14489
|
+
const refs = await loadRefs(log);
|
|
14490
|
+
const store2 = loadStore();
|
|
14491
|
+
const wc = workingChanges(store2, await repo.head());
|
|
14492
|
+
if (wc.added.length + wc.modified.length + wc.removed.length > 0) {
|
|
14493
|
+
die("you have uncommitted changes — commit them first, then revert.");
|
|
14494
|
+
}
|
|
14495
|
+
const refArg = args.find((a) => !a.startsWith("-")) || die("revert needs a ref: sol revert <branch|commit|seq>");
|
|
14496
|
+
const ops = await log.history();
|
|
14497
|
+
const resolved = await resolveRef(log, refArg);
|
|
14498
|
+
const target = resolved.head;
|
|
14499
|
+
const targetOp = resolved.op?.type === "checkpoint" && resolved.op.rootAfter === target ? resolved.op : [...ops].reverse().find((o) => o.type === "checkpoint" && o.rootAfter === target);
|
|
14500
|
+
if (!targetOp)
|
|
14501
|
+
die(`'${refArg}' does not name a commit to revert.`);
|
|
14502
|
+
const before = targetOp.parent ?? "";
|
|
14503
|
+
const beforeTree = before || emptyRoot(store2);
|
|
14504
|
+
const head = await log.head() ?? "";
|
|
14505
|
+
const { merge: merge2 } = await Promise.resolve().then(() => (init_merge(), exports_merge));
|
|
14506
|
+
const result = merge2({ store: store2 }, target, head, beforeTree);
|
|
14507
|
+
if (result.conflicts.length) {
|
|
14508
|
+
materializeTree(store2, result.head);
|
|
14509
|
+
if (args.includes("--json")) {
|
|
14510
|
+
console.log(JSON.stringify({ reverted: target, conflicts: result.conflicts.map((c) => c.path), committed: false }));
|
|
14511
|
+
break;
|
|
14512
|
+
}
|
|
14513
|
+
console.log(`revert ${target.slice(0, 12)} -> ${result.conflicts.length} conflict(s), left in your working tree (uncommitted):`);
|
|
14514
|
+
for (const c of result.conflicts)
|
|
14515
|
+
console.log(" " + c.path);
|
|
14516
|
+
console.log("resolve the <<<<<<< markers, then `sol commit`");
|
|
14517
|
+
process.exitCode = 1;
|
|
14518
|
+
break;
|
|
14519
|
+
}
|
|
14520
|
+
await persistTree(store2, new FileStore(solDir, objectsDir()), result.head);
|
|
14521
|
+
const message = `revert ${target.slice(0, 8)} — ${targetOp.message ?? ""}`.trimEnd();
|
|
14522
|
+
await appendCommit(log, result.head, message, head);
|
|
14523
|
+
refs.branches[refs.current].head = result.head;
|
|
14524
|
+
saveRefs(refs);
|
|
14525
|
+
setOpLogHead(result.head);
|
|
14526
|
+
const n = materializeDiff(store2, head, result.head);
|
|
14527
|
+
if (args.includes("--json")) {
|
|
14528
|
+
console.log(JSON.stringify({ reverted: target, head: result.head, filesChanged: n, committed: true }));
|
|
14529
|
+
break;
|
|
14530
|
+
}
|
|
14531
|
+
console.log(`reverted ${target.slice(0, 14)} — new commit ${result.head.slice(0, 12)} (${n} file change${n === 1 ? "" : "s"}); ${refArg} stays in history.`);
|
|
14532
|
+
break;
|
|
14533
|
+
}
|
|
14534
|
+
case "remote": {
|
|
14535
|
+
if (!existsSync18(solDir))
|
|
14536
|
+
die("not a sol repo");
|
|
14537
|
+
if (args[0]) {
|
|
14538
|
+
const repoName = args[1] || die("usage: sol remote <url> <repo>");
|
|
14539
|
+
saveRemote(solDir, { url: args[0], repo: repoName });
|
|
14540
|
+
console.log(`remote set: ${args[0]} (repo ${repoName})`);
|
|
14541
|
+
break;
|
|
14542
|
+
}
|
|
14543
|
+
const cfg = loadRemote(solDir);
|
|
14544
|
+
console.log(cfg ? `${cfg.url} (repo ${cfg.repo}${cfg.forkParent ? `, fork of ${cfg.forkParent}` : ""})` : "(no remote — `sol remote <url> <repo>` or `sol clone`)");
|
|
14545
|
+
break;
|
|
14546
|
+
}
|
|
14547
|
+
case "auth": {
|
|
14548
|
+
const sub = args[0];
|
|
14549
|
+
if (sub === "login") {
|
|
14550
|
+
const webUrl = (args[1] || "https://auth.midsummer.new").replace(/\/+$/, "");
|
|
14551
|
+
const osLabel = { darwin: "macOS", win32: "Windows", linux: "Linux" }[platform3()] ?? platform3();
|
|
14552
|
+
const deviceLabel = `${osLabel} · ${hostname2()}`;
|
|
14553
|
+
const codeRes = await fetch(`${webUrl}/api/auth/device/code`, {
|
|
14554
|
+
method: "POST",
|
|
14555
|
+
headers: { "content-type": "application/json" },
|
|
14556
|
+
body: JSON.stringify({ deviceLabel })
|
|
14557
|
+
});
|
|
14558
|
+
if (!codeRes.ok)
|
|
14559
|
+
die(`could not reach ${webUrl} (device/code -> ${codeRes.status})`);
|
|
14560
|
+
const { code, authorizeUrl, verification_uri_complete, interval } = await codeRes.json();
|
|
14561
|
+
const openUrl = verification_uri_complete || `${authorizeUrl || `${webUrl}/cli/authorize`}?code=${encodeURIComponent(code)}`;
|
|
14562
|
+
console.log(`
|
|
14563
|
+
To sign in, open: ${openUrl}`);
|
|
14564
|
+
console.log(` (code, for reference: ${code})
|
|
14565
|
+
`);
|
|
14566
|
+
console.log(" Waiting for approval...");
|
|
14567
|
+
const deadline = Date.now() + 300000;
|
|
14568
|
+
let tokens;
|
|
14569
|
+
while (Date.now() < deadline) {
|
|
14570
|
+
await new Promise((r) => setTimeout(r, (interval || 2) * 1000));
|
|
14571
|
+
const pr = await (await fetch(`${webUrl}/api/auth/device/poll`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ code }) })).json();
|
|
14572
|
+
if (pr.status === "approved" && pr.accessToken) {
|
|
14573
|
+
tokens = { accessToken: pr.accessToken, refreshToken: pr.refreshToken ?? "" };
|
|
14574
|
+
break;
|
|
14575
|
+
}
|
|
14576
|
+
if (pr.status === "expired")
|
|
14577
|
+
die("the code expired — run `sol auth login` again");
|
|
14578
|
+
}
|
|
14579
|
+
if (!tokens)
|
|
14580
|
+
die("timed out waiting for approval");
|
|
14581
|
+
mkdirSync12(dirname6(CRED_PATH2), { recursive: true });
|
|
14582
|
+
writeFileSync16(CRED_PATH2, JSON.stringify({ webUrl, ...tokens }, null, 2), { mode: 384 });
|
|
14583
|
+
const c = tokenClaims2(tokens.accessToken);
|
|
14584
|
+
console.log(`
|
|
14585
|
+
Logged in as ${c.handle ? `@${c.handle}` : c.email || "user"}.`);
|
|
14586
|
+
if (!c.handle)
|
|
14587
|
+
console.log(" (no handle yet — set one in the web app to get your <handle>/<repo> namespace)");
|
|
14588
|
+
} else if (sub === "logout") {
|
|
14589
|
+
if (existsSync18(CRED_PATH2))
|
|
14590
|
+
unlinkSync8(CRED_PATH2);
|
|
14591
|
+
console.log("logged out");
|
|
14592
|
+
} else if (sub === "status" || !sub) {
|
|
14593
|
+
if (process.env.SOL_TOKEN) {
|
|
14594
|
+
const c2 = tokenClaims2(process.env.SOL_TOKEN);
|
|
14595
|
+
console.log(`authenticated via SOL_TOKEN (env)${c2.handle ? ` as @${c2.handle}` : ""}`);
|
|
14596
|
+
break;
|
|
14597
|
+
}
|
|
14598
|
+
if (!existsSync18(CRED_PATH2)) {
|
|
14599
|
+
console.log("not logged in — run `sol auth login` (or set SOL_TOKEN)");
|
|
14600
|
+
break;
|
|
14601
|
+
}
|
|
14602
|
+
const creds = JSON.parse(readFileSync18(CRED_PATH2, "utf8"));
|
|
14603
|
+
const c = tokenClaims2(creds.accessToken || "");
|
|
14604
|
+
const stale = typeof c.exp === "number" && c.exp * 1000 < Date.now();
|
|
14605
|
+
console.log(`logged in as ${c.handle ? `@${c.handle}` : c.email || "user"} via ${creds.webUrl}${stale ? " (token stale — refreshes on next use)" : ""}`);
|
|
14606
|
+
} else if (sub === "whoami") {
|
|
14607
|
+
const token = process.env.SOL_TOKEN || await loadStoredToken2();
|
|
14608
|
+
if (!token)
|
|
14609
|
+
authExpired2();
|
|
14610
|
+
let id = identityFromToken2(token);
|
|
14611
|
+
try {
|
|
14612
|
+
const res = await fetch(`${authHost2()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
|
|
14613
|
+
if (res.status === 401 && !id)
|
|
14614
|
+
authExpired2();
|
|
14615
|
+
if (res.ok) {
|
|
14616
|
+
const me = await res.json();
|
|
14617
|
+
id = { handle: me.handle ?? id?.handle, email: me.email ?? id?.email, userId: id?.userId };
|
|
14618
|
+
}
|
|
14619
|
+
} catch {}
|
|
14620
|
+
if (!id)
|
|
14621
|
+
authExpired2();
|
|
14622
|
+
console.log(id.handle ? `signed in as @${id.handle} ${id.email ?? ""}`.trimEnd() : `signed in${id.email ? ` as ${id.email}` : ""} — no handle yet (\`sol auth set-handle <name>\`)`);
|
|
14623
|
+
} else if (sub === "set-handle") {
|
|
14624
|
+
const want = args[1] || die("usage: sol auth set-handle <name>");
|
|
14625
|
+
const handle2 = want.toLowerCase();
|
|
14626
|
+
const token = process.env.SOL_TOKEN || await loadStoredToken2();
|
|
14627
|
+
if (!token)
|
|
14628
|
+
authExpired2();
|
|
14629
|
+
let hadHandle = false;
|
|
14630
|
+
try {
|
|
14631
|
+
const meRes = await fetch(`${authHost2()}/api/auth/me`, { headers: { authorization: `Bearer ${token}` } });
|
|
14632
|
+
if (meRes.ok)
|
|
14633
|
+
hadHandle = Boolean((await meRes.json()).handle);
|
|
14634
|
+
} catch {}
|
|
14635
|
+
const res = await fetch(`${authHost2()}/api/auth/handle`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ handle: handle2 }) });
|
|
14636
|
+
if (res.status === 401)
|
|
14637
|
+
authExpired2();
|
|
14638
|
+
if (!res.ok) {
|
|
14639
|
+
let msg = `could not set handle (${res.status})`;
|
|
14640
|
+
try {
|
|
14641
|
+
const err = await res.json();
|
|
14642
|
+
if (err.error?.message)
|
|
14643
|
+
msg = err.error.message;
|
|
14644
|
+
} catch {}
|
|
14645
|
+
die(msg);
|
|
14646
|
+
}
|
|
14647
|
+
const out = await res.json();
|
|
14648
|
+
if (hadHandle)
|
|
14649
|
+
console.log("heads-up: changing your handle re-namespaces your repos under the new <handle>/<repo>.");
|
|
14650
|
+
console.log(`handle set to @${out.handle}`);
|
|
14651
|
+
} else if (sub === "pat") {
|
|
14652
|
+
if (!existsSync18(CRED_PATH2))
|
|
14653
|
+
die("run `sol auth login` first");
|
|
14654
|
+
const creds = JSON.parse(readFileSync18(CRED_PATH2, "utf8"));
|
|
14655
|
+
const token = await loadStoredToken2();
|
|
14656
|
+
if (!token || !creds.webUrl)
|
|
14657
|
+
die("run `sol auth login` first");
|
|
14658
|
+
const patAction = args[1];
|
|
14659
|
+
if (patAction === "list") {
|
|
14660
|
+
const res = await fetch(`${creds.webUrl}/api/auth/pat`, { headers: { authorization: `Bearer ${token}` } });
|
|
14661
|
+
if (!res.ok)
|
|
14662
|
+
die(`could not list PATs (${res.status})`);
|
|
14663
|
+
const { tokens } = await res.json();
|
|
14664
|
+
if (!tokens.length) {
|
|
14665
|
+
console.log("(no personal access tokens — `sol auth pat [days] [name]` to mint one)");
|
|
14666
|
+
break;
|
|
14667
|
+
}
|
|
14668
|
+
for (const t of tokens) {
|
|
14669
|
+
const state = t.revokedAt ? "revoked" : t.expiresAt < Date.now() ? "expired" : "active";
|
|
14670
|
+
const used = t.lastUsedAt ? `, last used ${new Date(t.lastUsedAt).toISOString().slice(0, 10)}` : "";
|
|
14671
|
+
console.log(` ${t.id} ${t.name.padEnd(20)} [${state}] expires ${new Date(t.expiresAt).toISOString().slice(0, 10)}${used}`);
|
|
14672
|
+
}
|
|
14673
|
+
} else if (patAction === "revoke") {
|
|
14674
|
+
const id = args[2] || die("usage: sol auth pat revoke <id>");
|
|
14675
|
+
const res = await fetch(`${creds.webUrl}/api/auth/pat`, { method: "DELETE", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ id }) });
|
|
14676
|
+
if (res.status === 404)
|
|
14677
|
+
die(`no such PAT (or not yours): ${id}`);
|
|
14678
|
+
if (!res.ok)
|
|
14679
|
+
die(`could not revoke PAT (${res.status})`);
|
|
14680
|
+
const out = await res.json();
|
|
14681
|
+
console.log(`revoked PAT ${id}${out.edgePropagated === false ? " (DB only — edge KV not configured; valid at the edge until expiry)" : ""}`);
|
|
14682
|
+
} else {
|
|
14683
|
+
const days = Number(patAction) || 90;
|
|
14684
|
+
const name = (Number(patAction) ? args[2] : patAction) || undefined;
|
|
14685
|
+
const res = await fetch(`${creds.webUrl}/api/auth/pat`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ days, ...name ? { name } : {} }) });
|
|
14686
|
+
if (!res.ok)
|
|
14687
|
+
die(`could not mint a PAT (${res.status})`);
|
|
14688
|
+
const out = await res.json();
|
|
14689
|
+
console.log(`
|
|
14690
|
+
Personal Access Token "${out.name}" (id ${out.id}, expires in ${days} days) — store it now, it is not shown again:
|
|
14691
|
+
`);
|
|
14692
|
+
console.log(` ${out.token}
|
|
14693
|
+
`);
|
|
14694
|
+
console.log(" Use it for git or CI (no device flow needed):");
|
|
14695
|
+
console.log(" git clone https://x:<token>@<sol-backend>/git/<owner>/<repo>");
|
|
14696
|
+
console.log(" export SOL_TOKEN=<token>");
|
|
14697
|
+
console.log(`
|
|
14698
|
+
Manage: sol auth pat list | sol auth pat revoke ${out.id}`);
|
|
14699
|
+
}
|
|
14700
|
+
} else {
|
|
14701
|
+
die("usage: sol auth [login [<web-url>] | logout | status | whoami | set-handle <name> | pat [days] [name] | pat list | pat revoke <id>]");
|
|
14702
|
+
}
|
|
14703
|
+
break;
|
|
14704
|
+
}
|
|
14705
|
+
case "clone": {
|
|
14706
|
+
const { localPeerSolDir: localPeerSolDir2, openPeer: openPeer2, peerNodes: peerNodes2 } = await Promise.resolve().then(() => (init_local_peer(), exports_local_peer));
|
|
14707
|
+
const localSrc = args[0] ? localPeerSolDir2(args[0], procCwd) : undefined;
|
|
14708
|
+
if (localSrc) {
|
|
14709
|
+
const { converge: converge2 } = await Promise.resolve().then(() => (init_converge(), exports_converge));
|
|
14710
|
+
const peer = openPeer2(localSrc);
|
|
14711
|
+
const peerHead = await peer.log.head();
|
|
14712
|
+
const dest = resolve5(procCwd, args[1] || (args[0].replace(/\/+$/, "").split("/").pop() || "clone") + "-clone");
|
|
14713
|
+
const ddir = join19(dest, ".sol");
|
|
14714
|
+
if (existsSync18(ddir))
|
|
14715
|
+
die("already a sol repo: " + dest);
|
|
14716
|
+
mkdirSync12(ddir, { recursive: true });
|
|
14717
|
+
const peerOps = await peer.log.history();
|
|
14718
|
+
const res = await converge2({ store: new FileStore(ddir), log: new FileOpLog(ddir) }, { nodes: await peerNodes2(peer, peerHead, peerOps), ops: peerOps, incomingHead: peerHead, actor });
|
|
14719
|
+
const dstore = new Store;
|
|
14720
|
+
for (const n2 of await peerNodes2({ store: new FileStore(ddir), log: new FileOpLog(ddir) }, res.head))
|
|
14721
|
+
dstore.put(n2);
|
|
14722
|
+
const { isReservedKey: isReservedKey2 } = await Promise.resolve().then(() => (init_struct(), exports_struct));
|
|
14723
|
+
const files = (res.head ? listAll(dstore, res.head) : []).filter((f) => !f.split("/").some(isReservedKey2));
|
|
14724
|
+
for (const f of files)
|
|
14725
|
+
materializeInto(dstore, res.head, dest, f);
|
|
14726
|
+
writeFileSync16(join19(ddir, "refs.json"), JSON.stringify({ current: "main", branches: { main: { head: res.head, base: res.head, remote: res.head } }, tags: {} }, null, 2));
|
|
14727
|
+
writeWorkingIndexAt(ddir, dest, files);
|
|
14728
|
+
console.log(`cloned local peer ${args[0]} -> ${dest} (${(await peer.log.history()).length} ops, ${files.length} files)`);
|
|
14729
|
+
break;
|
|
14730
|
+
}
|
|
14731
|
+
const [url, rest] = remoteUrlArg2(args);
|
|
14732
|
+
const repoName = rest[0] || die("usage: sol clone [<url>] <owner>/<repo> [dir]");
|
|
14733
|
+
const token = process.env.SOL_TOKEN || die("set SOL_TOKEN to the backend bearer token");
|
|
14734
|
+
const target = resolve5(cwd, rest[1] || repoName.split("/").pop() || repoName);
|
|
14735
|
+
const fdir = join19(target, ".sol");
|
|
14736
|
+
if (existsSync18(fdir))
|
|
14737
|
+
die("already a sol repo: " + target);
|
|
14738
|
+
const cfg = { url, repo: repoName };
|
|
14739
|
+
const bundle = await remoteExport(cfg, token);
|
|
14740
|
+
mkdirSync12(fdir, { recursive: true });
|
|
14741
|
+
await writeBundle(fdir, bundle, 0);
|
|
14742
|
+
saveRemote(fdir, cfg);
|
|
14743
|
+
await pullEnvState2(fdir, cfg, token);
|
|
14744
|
+
const srvRefs = bundle.refs ?? { branches: { main: bundle.head ?? "" }, production: "main" };
|
|
14745
|
+
const checkout = bundle.checkout ?? { branch: srvRefs.production || "main", head: srvRefs.branches[srvRefs.production] ?? bundle.head };
|
|
14746
|
+
const onBranch = checkout.branch;
|
|
14747
|
+
const checkoutHead = checkout.head ?? bundle.head ?? "";
|
|
14748
|
+
const cloneBranches = {};
|
|
14749
|
+
for (const [name, h] of Object.entries(srvRefs.branches))
|
|
14750
|
+
cloneBranches[name] = { head: h, base: h, remote: h };
|
|
14751
|
+
if (!cloneBranches[onBranch])
|
|
14752
|
+
cloneBranches[onBranch] = { head: checkoutHead, base: checkoutHead, remote: checkoutHead };
|
|
14753
|
+
writeFileSync16(join19(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
|
|
14754
|
+
writeFileSync16(join19(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
|
|
14755
|
+
const store2 = new Store;
|
|
14756
|
+
for (const node of bundle.nodes)
|
|
14757
|
+
store2.put(node);
|
|
14758
|
+
let n = 0;
|
|
14759
|
+
if (checkoutHead) {
|
|
14760
|
+
const files = listAll(store2, checkoutHead);
|
|
14761
|
+
for (const f of files)
|
|
14762
|
+
if (materializeInto(store2, checkoutHead, target, f))
|
|
14763
|
+
n++;
|
|
14764
|
+
writeWorkingIndexAt(fdir, target, files);
|
|
14765
|
+
}
|
|
14766
|
+
console.log(`cloned ${repoName} -> ${rest[1] || repoName} (${bundle.ops.length} ops, ${Object.keys(cloneBranches).length} branch(es), ${n} files, on ${onBranch})`);
|
|
14767
|
+
break;
|
|
14768
|
+
}
|
|
14769
|
+
case "push": {
|
|
14770
|
+
const { log } = open();
|
|
14771
|
+
const wantPublic = args.includes("--public");
|
|
14772
|
+
if (!loadRemote(solDir)) {
|
|
14773
|
+
const repoName = args.find((a) => !a.startsWith("-"));
|
|
14774
|
+
if (repoName) {
|
|
14775
|
+
saveRemote(solDir, { url: DEFAULT_REMOTE_URL2, repo: repoName });
|
|
14776
|
+
console.log(`remote set: ${DEFAULT_REMOTE_URL2} (repo ${repoName})`);
|
|
14777
|
+
}
|
|
14778
|
+
}
|
|
14779
|
+
const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`, or `sol push <repo>` to use the hosted Sol");
|
|
14780
|
+
{
|
|
14781
|
+
const { pendingScrubPaths: pendingScrubPaths2, historyHasCleartext: historyHasCleartext2 } = await Promise.resolve().then(() => (init_secret_scrub(), exports_secret_scrub));
|
|
14782
|
+
const ops0 = await log.history();
|
|
14783
|
+
const stillLeaking = pendingScrubPaths2(solDir).filter((p) => historyHasCleartext2(solDir, ops0, p));
|
|
14784
|
+
if (stillLeaking.length) {
|
|
14785
|
+
die(`refusing to push — pre-seal CLEARTEXT is still in history for: ${stillLeaking.join(", ")}. the plaintext would ship to every clone (recoverable via \`sol restore\`). run \`sol seal ${stillLeaking[0]} --scrub-history\` (repeat per path, or \`sol seal --scrub-history\` for all) first.`);
|
|
14786
|
+
}
|
|
14787
|
+
}
|
|
14788
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
14789
|
+
const rh = await remoteHead(cfg, token);
|
|
14790
|
+
const ops = await log.history();
|
|
14791
|
+
const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
|
|
14792
|
+
const localTip = await log.logTip();
|
|
14793
|
+
const localRefs = existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")) : undefined;
|
|
14794
|
+
const branch = localRefs?.current ?? "main";
|
|
14795
|
+
const branchHead = await log.head() ?? "";
|
|
14796
|
+
const remoteWasEmpty = rh.seq === 0 && !rh.tip;
|
|
14797
|
+
if (localSeq === rh.seq && localTip === rh.tip) {
|
|
14798
|
+
console.log(remoteWasEmpty ? "nothing to push — local repo is empty (commit something first)" : "everything up to date");
|
|
14799
|
+
break;
|
|
14800
|
+
}
|
|
14801
|
+
if (localSeq <= rh.seq) {}
|
|
14802
|
+
const forkBase = localRefs?.branches[branch]?.remote;
|
|
14803
|
+
const baseOp = forkBase ? ops.find((o) => o.rootAfter === forkBase) : undefined;
|
|
14804
|
+
const fromSeq = baseOp ? baseOp.seq : rh.seq;
|
|
14805
|
+
const res = await remotePush(cfg, token, {
|
|
14806
|
+
nodes: allLocalNodes(),
|
|
14807
|
+
ops: ops.filter((o) => o.seq > fromSeq),
|
|
14808
|
+
branch,
|
|
14809
|
+
head: branchHead,
|
|
14810
|
+
expectedHead: forkBase
|
|
14811
|
+
});
|
|
14812
|
+
const canon = await remoteExport(cfg, token);
|
|
14813
|
+
const prevHead = await log.head() ?? "";
|
|
14814
|
+
await writeBundle(solDir, canon, 0);
|
|
14815
|
+
const convergedHead = res.head ?? canon.refs?.branches?.[branch] ?? branchHead;
|
|
14816
|
+
if (existsSync18(refsPath())) {
|
|
14817
|
+
const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
|
|
14818
|
+
if (canon.refs) {
|
|
14819
|
+
for (const [name, h] of Object.entries(canon.refs.branches)) {
|
|
14820
|
+
refs.branches[name] = { head: refs.branches[name]?.head ?? h, base: refs.branches[name]?.base ?? h, remote: h };
|
|
14821
|
+
}
|
|
14822
|
+
}
|
|
14823
|
+
if (refs.branches[branch]) {
|
|
14824
|
+
refs.branches[branch].head = convergedHead;
|
|
14825
|
+
refs.branches[branch].remote = convergedHead;
|
|
14826
|
+
}
|
|
14827
|
+
saveRefs(refs);
|
|
14828
|
+
}
|
|
14829
|
+
setOpLogHead(convergedHead);
|
|
14830
|
+
if (convergedHead && convergedHead !== prevHead)
|
|
14831
|
+
materializeDiff(loadStore(), prevHead, convergedHead);
|
|
14832
|
+
const prod = res.refs?.production;
|
|
14833
|
+
const mergedNote = res.merged ? " (converged with a concurrent commit)" : "";
|
|
14834
|
+
const conflictNote = res.conflicts && res.conflicts.length ? ` — ${res.conflicts.length} conflict(s) kept with markers in: ${res.conflicts.map((c) => c.path).join(", ")}` : "";
|
|
14835
|
+
if (remoteWasEmpty) {
|
|
14836
|
+
console.log(`created remote repo ${cfg.repo} — pushed ${res.applied} op(s) (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${prod ? `; production=${prod}` : ""}`);
|
|
14837
|
+
} else {
|
|
14838
|
+
console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${mergedNote}${conflictNote}${prod ? `; production=${prod}` : ""}`);
|
|
14839
|
+
}
|
|
14840
|
+
await pushEnvState2(solDir, cfg, token);
|
|
14841
|
+
if (wantPublic) {
|
|
14842
|
+
const a = await accessSet(cfg, token, { visibility: "public" });
|
|
14843
|
+
console.log(`${cfg.repo} is now ${a.visibility}`);
|
|
14844
|
+
}
|
|
14845
|
+
if (res.merged && !(res.conflicts && res.conflicts.length) && convergedHead) {
|
|
14846
|
+
reportSemanticGate2(gateMerge(solDir, cfgDir2(), cwd, convergedHead), args.includes("--json"));
|
|
14847
|
+
}
|
|
14848
|
+
break;
|
|
14849
|
+
}
|
|
14850
|
+
case "pull": {
|
|
14851
|
+
const { log } = open();
|
|
14852
|
+
const [{ localPeerSolDir: localPeerSolDir2, openPeer: openPeer2, peerNodes: peerNodes2 }, { converge: converge2 }, { merge: merge2 }] = await Promise.all([
|
|
14853
|
+
Promise.resolve().then(() => (init_local_peer(), exports_local_peer)),
|
|
14854
|
+
Promise.resolve().then(() => (init_converge(), exports_converge)),
|
|
14855
|
+
Promise.resolve().then(() => (init_merge(), exports_merge))
|
|
14856
|
+
]);
|
|
14857
|
+
{
|
|
14858
|
+
const marked = unresolvedConflictPaths();
|
|
14859
|
+
if (marked.length)
|
|
14860
|
+
die(`unresolved conflict markers in ${marked.join(", ")} — resolve them and \`sol commit\`, then pull again.`);
|
|
14861
|
+
}
|
|
14862
|
+
const peerArg = args[0];
|
|
14863
|
+
const peerSolDir = peerArg ? localPeerSolDir2(peerArg, cwd) : undefined;
|
|
14864
|
+
if (peerSolDir) {
|
|
14865
|
+
const wcLocal = workingChanges(loadStore(), await log.head() ?? "");
|
|
14866
|
+
if (wcLocal.added.length + wcLocal.modified.length + wcLocal.removed.length > 0) {
|
|
14867
|
+
die("you have uncommitted changes — commit (`sol commit`) or discard them before pulling.");
|
|
14868
|
+
}
|
|
14869
|
+
const peer = openPeer2(peerSolDir);
|
|
14870
|
+
const peerHead = await peer.log.head();
|
|
14871
|
+
if (!peerHead) {
|
|
14872
|
+
console.log("peer repo is empty — nothing to pull");
|
|
14873
|
+
break;
|
|
14874
|
+
}
|
|
14875
|
+
const prevHead = await log.head() ?? "";
|
|
14876
|
+
const peerOps = await peer.log.history();
|
|
14877
|
+
const peerMeta = readViewMeta(peerSolDir);
|
|
14878
|
+
const ownMeta = readViewMeta(solDir);
|
|
14879
|
+
const base2 = peerMeta?.startHead ?? ownMeta?.startHead;
|
|
14880
|
+
const res = await converge2({ store: new FileStore(solDir, objectsDir()), log }, { nodes: await peerNodes2(peer, peerHead, peerOps), ops: peerOps, incomingHead: peerHead, actor, ...base2 ? { base: base2 } : {} });
|
|
14881
|
+
if (existsSync18(refsPath())) {
|
|
14882
|
+
const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
|
|
14883
|
+
if (refs.branches[refs.current])
|
|
14884
|
+
refs.branches[refs.current].head = res.head;
|
|
14885
|
+
saveRefs(refs);
|
|
14886
|
+
}
|
|
14887
|
+
setOpLogHead(res.head);
|
|
14888
|
+
if (res.head !== prevHead)
|
|
14889
|
+
materializeDiff(loadStore(), prevHead, res.head);
|
|
14890
|
+
if (res.conflicts.length) {
|
|
14891
|
+
saveMergeConflicts(solDir, res.conflicts);
|
|
14892
|
+
console.log(`pulled + merged from ${peerArg} WITH ${res.conflicts.length} conflict(s) — both sides kept with markers in:`);
|
|
14893
|
+
for (const c of res.conflicts)
|
|
14894
|
+
console.log(" " + c.path);
|
|
14895
|
+
console.log("resolve the <<<<<<< markers, then `sol commit`");
|
|
14896
|
+
process.exitCode = 1;
|
|
14897
|
+
} else {
|
|
14898
|
+
clearMergeConflicts(solDir);
|
|
14899
|
+
console.log(`pulled from ${peerArg} -> ${res.applied} new op(s)${res.merged ? " (converged by merge)" : ""}, now at ${(res.head || "").slice(0, 12)}`);
|
|
14900
|
+
if (res.merged)
|
|
14901
|
+
reportSemanticGate2(gateMerge(solDir, cfgDir2(), cwd, res.head), args.includes("--json"));
|
|
14902
|
+
}
|
|
14903
|
+
break;
|
|
14904
|
+
}
|
|
14905
|
+
const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
|
|
14906
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
14907
|
+
await surfaceEnvDivergence2(solDir, cfg, token);
|
|
14908
|
+
await pullEnvState2(solDir, cfg, token);
|
|
14909
|
+
const bundle = await remoteExport(cfg, token);
|
|
14910
|
+
const ops = await log.history();
|
|
14911
|
+
const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
|
|
14912
|
+
const localTip = await log.logTip();
|
|
14913
|
+
const curBranch = existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")).current : bundle.refs?.production || "main";
|
|
14914
|
+
const remoteCurHead = bundle.refs?.branches?.[curBranch] ?? bundle.head ?? "";
|
|
14915
|
+
if (bundle.seq === localSeq && bundle.tip === localTip) {
|
|
14916
|
+
if (bundle.refs && existsSync18(refsPath())) {
|
|
14917
|
+
const lr = JSON.parse(readFileSync18(refsPath(), "utf8"));
|
|
14918
|
+
const before = lr.branches[lr.current]?.head;
|
|
14919
|
+
for (const [name, h] of Object.entries(bundle.refs.branches))
|
|
14920
|
+
lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
|
|
14921
|
+
saveRefs(lr);
|
|
14922
|
+
const after = lr.branches[lr.current]?.head;
|
|
14923
|
+
if (after && after !== before) {
|
|
14924
|
+
setOpLogHead(after);
|
|
14925
|
+
materializeDiff(loadStore(), before ?? "", after);
|
|
14926
|
+
console.log(`updated ${lr.current} -> ${after.slice(0, 12)} (a ref moved on the remote — merge/promote)`);
|
|
14927
|
+
break;
|
|
14928
|
+
}
|
|
14929
|
+
}
|
|
14930
|
+
console.log("already up to date");
|
|
14931
|
+
break;
|
|
14932
|
+
}
|
|
14933
|
+
let n = 0;
|
|
14934
|
+
while (n < ops.length && n < bundle.ops.length && ops[n].entryHash === bundle.ops[n].entryHash)
|
|
14935
|
+
n++;
|
|
14936
|
+
if (n === bundle.ops.length) {
|
|
14937
|
+
console.log("up to date (local is ahead — run `sol push`)");
|
|
14938
|
+
break;
|
|
14939
|
+
}
|
|
14940
|
+
const syncRefHead = (h) => {
|
|
14941
|
+
if (!existsSync18(refsPath()))
|
|
14942
|
+
return;
|
|
14943
|
+
const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
|
|
14944
|
+
if (refs.branches[refs.current])
|
|
14945
|
+
refs.branches[refs.current].head = h;
|
|
14946
|
+
saveRefs(refs);
|
|
14947
|
+
};
|
|
14948
|
+
{
|
|
14949
|
+
const wc = workingChanges(loadStore(), await log.head() ?? "");
|
|
14950
|
+
const dirty = [...wc.added.map((f) => "+ " + f), ...wc.modified.map((f) => "~ " + f), ...wc.removed.map((f) => "- " + f)];
|
|
14951
|
+
if (dirty.length)
|
|
14952
|
+
die(`you have uncommitted changes — commit (\`sol commit\`) or discard them before pulling:
|
|
14953
|
+
${dirty.join(`
|
|
14954
|
+
`)}`);
|
|
14955
|
+
}
|
|
14956
|
+
if (n === ops.length) {
|
|
14957
|
+
const added = await writeBundle(solDir, bundle, localSeq);
|
|
14958
|
+
syncRefHead(remoteCurHead);
|
|
14959
|
+
setOpLogHead(remoteCurHead);
|
|
14960
|
+
materializeTree(loadStore(), remoteCurHead);
|
|
14961
|
+
if (bundle.refs && existsSync18(refsPath())) {
|
|
14962
|
+
const lr = JSON.parse(readFileSync18(refsPath(), "utf8"));
|
|
14963
|
+
for (const [name, h] of Object.entries(bundle.refs.branches))
|
|
14964
|
+
lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
|
|
14965
|
+
saveRefs(lr);
|
|
14966
|
+
}
|
|
14967
|
+
console.log(`pulled ${added} new op(s) -> ${curBranch} now at ${(remoteCurHead || "").slice(0, 12)}`);
|
|
14968
|
+
break;
|
|
14969
|
+
}
|
|
14970
|
+
const store2 = loadStore();
|
|
14971
|
+
for (const node of bundle.nodes)
|
|
14972
|
+
store2.put(node);
|
|
14973
|
+
const base = n > 0 ? ops[n - 1].rootAfter : emptyRoot(store2);
|
|
14974
|
+
const localHead = await log.head() ?? "";
|
|
14975
|
+
const remoteHead2 = remoteCurHead;
|
|
14976
|
+
const pathAuthor = new Map;
|
|
14977
|
+
for (const op of ops.slice(n))
|
|
14978
|
+
if (op.by && op.path)
|
|
14979
|
+
pathAuthor.set(op.path, op.by);
|
|
14980
|
+
const result = merge2({ store: store2 }, base, localHead, remoteHead2);
|
|
14981
|
+
const conflicted = new Set(result.conflicts.map((c) => c.path));
|
|
14982
|
+
await writeBundle(solDir, bundle, 0);
|
|
14983
|
+
setOpLogHead(remoteHead2);
|
|
14984
|
+
const { repo: rebased } = open();
|
|
14985
|
+
const d = diffTrees(store2, remoteHead2, result.head);
|
|
14986
|
+
for (const p of [...d.added, ...d.modified.map((m) => m.path)]) {
|
|
14987
|
+
if (conflicted.has(p))
|
|
14988
|
+
continue;
|
|
14989
|
+
const blob = fileAt(store2, result.head, p);
|
|
14990
|
+
if (!blob)
|
|
14991
|
+
continue;
|
|
14992
|
+
const author = pathAuthor.get(p);
|
|
14993
|
+
if (blob.encoding === "base64")
|
|
14994
|
+
await rebased.writeBytes(p, new Uint8Array(Buffer.from(blob.content, "base64")), undefined, author);
|
|
14995
|
+
else
|
|
14996
|
+
await rebased.writeFile(p, blob.content, undefined, author);
|
|
14997
|
+
}
|
|
14998
|
+
for (const p of d.removed)
|
|
14999
|
+
if (!conflicted.has(p))
|
|
15000
|
+
await rebased.deleteFile(p, undefined, pathAuthor.get(p));
|
|
15001
|
+
if (!result.conflicts.length)
|
|
15002
|
+
await appendCommit(log, await rebased.head(), "merge remote into local", remoteHead2);
|
|
15003
|
+
const mergedHead = await log.head() ?? "";
|
|
15004
|
+
syncRefHead(mergedHead);
|
|
15005
|
+
materializeTree(loadStore(), mergedHead);
|
|
15006
|
+
if (result.conflicts.length) {
|
|
15007
|
+
for (const c of result.conflicts) {
|
|
15008
|
+
const blob = fileAt(store2, result.head, c.path);
|
|
15009
|
+
if (blob)
|
|
15010
|
+
writeFileSync16(join19(cwd, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
|
|
15011
|
+
}
|
|
15012
|
+
writeFileSync16(join19(solDir, "MERGE_HEAD"), remoteHead2);
|
|
15013
|
+
saveMergeConflicts(solDir, result.conflicts);
|
|
15014
|
+
console.log(`pulled + merged WITH ${result.conflicts.length} conflict(s), left uncommitted in your working tree:`);
|
|
15015
|
+
for (const c of result.conflicts)
|
|
15016
|
+
console.log(" " + c.path);
|
|
15017
|
+
console.log("resolve the <<<<<<< markers, then `sol commit` and `sol push`");
|
|
15018
|
+
process.exitCode = 1;
|
|
15019
|
+
} else {
|
|
15020
|
+
clearMergeConflicts(solDir);
|
|
15021
|
+
console.log(`pulled + merged remote into local -> ${mergedHead.slice(0, 12)} (now run \`sol push\`)`);
|
|
15022
|
+
reportSemanticGate2(gateMerge(solDir, cfgDir2(), cwd, mergedHead), args.includes("--json"));
|
|
15023
|
+
}
|
|
15024
|
+
break;
|
|
15025
|
+
}
|
|
15026
|
+
case "promote": {
|
|
15027
|
+
if (!existsSync18(solDir))
|
|
15028
|
+
die("not a sol repo");
|
|
15029
|
+
const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
|
|
15030
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
15031
|
+
const cur = existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")).current : "main";
|
|
15032
|
+
const branch = args[0] || cur;
|
|
15033
|
+
const refs = await remotePromote(cfg, token, branch);
|
|
15034
|
+
console.log(`promoted '${branch}' -> production '${refs.production}' now at ${(refs.branches[refs.production] ?? "").slice(0, 12)}`);
|
|
15035
|
+
break;
|
|
15036
|
+
}
|
|
15037
|
+
case "fork": {
|
|
15038
|
+
const [url, frest] = remoteUrlArg2(args);
|
|
15039
|
+
const parent = frest[0] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
|
|
15040
|
+
const newRepo = frest[1] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
|
|
15041
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
15042
|
+
const target = resolve5(cwd, frest[2] || newRepo);
|
|
15043
|
+
const fdir = join19(target, ".sol");
|
|
15044
|
+
if (existsSync18(fdir))
|
|
15045
|
+
die("already a sol repo: " + target);
|
|
15046
|
+
const parentCfg = { url, repo: parent };
|
|
15047
|
+
const newCfg = { url, repo: newRepo, forkParent: parent };
|
|
15048
|
+
const bundle = await remoteExport(parentCfg, token);
|
|
15049
|
+
if (!bundle.ops.length)
|
|
15050
|
+
die(`parent repo '${parent}' is empty or unreachable`);
|
|
15051
|
+
const prodBranch = bundle.refs?.production || "main";
|
|
15052
|
+
const prodHead = bundle.checkout?.head || bundle.head;
|
|
15053
|
+
await remotePush(newCfg, token, { nodes: bundle.nodes, ops: bundle.ops, branch: prodBranch, head: prodHead });
|
|
15054
|
+
for (const [name, h] of Object.entries(bundle.refs?.branches ?? {})) {
|
|
15055
|
+
if (name !== prodBranch)
|
|
15056
|
+
await remotePush(newCfg, token, { nodes: [], ops: [], branch: name, head: h });
|
|
15057
|
+
}
|
|
15058
|
+
await forkMeta(newCfg, token, parent);
|
|
15059
|
+
const canon = await remoteExport(newCfg, token);
|
|
15060
|
+
mkdirSync12(fdir, { recursive: true });
|
|
15061
|
+
await writeBundle(fdir, canon, 0);
|
|
15062
|
+
const srvRefs = canon.refs ?? { branches: { main: canon.head ?? "" }, production: "main" };
|
|
15063
|
+
const checkout = canon.checkout ?? { branch: srvRefs.production || "main", head: srvRefs.branches[srvRefs.production] ?? canon.head };
|
|
15064
|
+
const onBranch = checkout.branch;
|
|
15065
|
+
const checkoutHead = checkout.head ?? canon.head ?? "";
|
|
15066
|
+
const cloneBranches = {};
|
|
15067
|
+
for (const [name, h] of Object.entries(srvRefs.branches))
|
|
15068
|
+
cloneBranches[name] = { head: h, base: h, remote: h };
|
|
15069
|
+
writeFileSync16(join19(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
|
|
15070
|
+
writeFileSync16(join19(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
|
|
15071
|
+
saveRemote(fdir, newCfg);
|
|
15072
|
+
const store2 = new Store;
|
|
15073
|
+
for (const node of canon.nodes)
|
|
15074
|
+
store2.put(node);
|
|
15075
|
+
let n = 0;
|
|
15076
|
+
if (checkoutHead) {
|
|
15077
|
+
const files = listAll(store2, checkoutHead);
|
|
15078
|
+
for (const f of files)
|
|
15079
|
+
if (materializeInto(store2, checkoutHead, target, f))
|
|
15080
|
+
n++;
|
|
15081
|
+
writeWorkingIndexAt(fdir, target, files);
|
|
15082
|
+
}
|
|
15083
|
+
console.log(`forked ${parent} -> ${newRepo} (your copy at ${args[3] || newRepo}: ${Object.keys(cloneBranches).length} branch(es), ${n} files; parent: ${parent})`);
|
|
15084
|
+
break;
|
|
15085
|
+
}
|
|
15086
|
+
case "access": {
|
|
15087
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
15088
|
+
const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
|
|
15089
|
+
const sub = args[0];
|
|
15090
|
+
if (!sub || sub === "show") {
|
|
15091
|
+
const a = await accessGet(cfg, token);
|
|
15092
|
+
console.log(`${cfg.repo}: ${a.visibility}`);
|
|
15093
|
+
const cols = Object.entries(a.collaborators);
|
|
15094
|
+
if (cols.length)
|
|
15095
|
+
for (const [u, r] of cols)
|
|
15096
|
+
console.log(` ${u}: ${r}`);
|
|
15097
|
+
else
|
|
15098
|
+
console.log(" (no collaborators)");
|
|
15099
|
+
const teams = Object.entries(a.teams ?? {});
|
|
15100
|
+
if (teams.length)
|
|
15101
|
+
for (const [t, r] of teams)
|
|
15102
|
+
console.log(` team:${t}: ${r}`);
|
|
15103
|
+
} else if (sub === "public" || sub === "private") {
|
|
15104
|
+
const a = await accessSet(cfg, token, { visibility: sub });
|
|
15105
|
+
console.log(`${cfg.repo} is now ${a.visibility}`);
|
|
15106
|
+
} else if (sub === "add") {
|
|
15107
|
+
const userId = args[1] || die("usage: sol access add <userId> <read|write|admin>");
|
|
15108
|
+
const role = args[2] || "read";
|
|
15109
|
+
if (role !== "read" && role !== "write" && role !== "admin")
|
|
15110
|
+
die(`invalid role '${role}' — use read, write, or admin`);
|
|
15111
|
+
const a = await accessSet(cfg, token, { collaborator: { userId, role } });
|
|
15112
|
+
if (a.collaborators?.[userId] === role)
|
|
15113
|
+
console.log(`granted ${role} to ${userId} on ${cfg.repo}`);
|
|
15114
|
+
else
|
|
15115
|
+
die(`failed to grant ${role} to ${userId} (the server did not apply it)`);
|
|
15116
|
+
} else if (sub === "add-team") {
|
|
15117
|
+
const teamId = args[1] || die("usage: sol access add-team <teamId> <read|write|admin>");
|
|
15118
|
+
const role = args[2] || "read";
|
|
15119
|
+
if (role !== "read" && role !== "write" && role !== "admin")
|
|
15120
|
+
die(`invalid role '${role}' — use read, write, or admin`);
|
|
15121
|
+
const a = await accessSet(cfg, token, { team: { teamId, role } });
|
|
15122
|
+
if (a.teams?.[teamId] === role)
|
|
15123
|
+
console.log(`granted ${role} to team:${teamId} on ${cfg.repo}`);
|
|
15124
|
+
else
|
|
15125
|
+
die(`failed to grant ${role} to team:${teamId} (the server did not apply it)`);
|
|
15126
|
+
} else if (sub === "remove-team") {
|
|
15127
|
+
const teamId = args[1] || die("usage: sol access remove-team <teamId>");
|
|
15128
|
+
await accessSet(cfg, token, { team: { teamId, remove: true } });
|
|
15129
|
+
console.log(`removed team:${teamId} from ${cfg.repo}`);
|
|
15130
|
+
} else if (sub === "remove") {
|
|
15131
|
+
const userId = args[1] || die("usage: sol access remove <userId>");
|
|
15132
|
+
await accessSet(cfg, token, { collaborator: { userId, remove: true } });
|
|
15133
|
+
console.log(`removed ${userId} from ${cfg.repo}`);
|
|
15134
|
+
} else {
|
|
15135
|
+
die("usage: sol access [show | public | private | add <userId> <role> | remove <userId> | add-team <teamId> <role> | remove-team <teamId>]");
|
|
15136
|
+
}
|
|
15137
|
+
break;
|
|
15138
|
+
}
|
|
15139
|
+
case "forks": {
|
|
15140
|
+
const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
|
|
15141
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
15142
|
+
const { forks } = await forksList(cfg, token);
|
|
15143
|
+
if (!forks.length)
|
|
15144
|
+
console.log(`(no forks of ${cfg.repo})`);
|
|
15145
|
+
else {
|
|
15146
|
+
console.log(`forks of ${cfg.repo}:`);
|
|
15147
|
+
for (const f of forks)
|
|
15148
|
+
console.log(` ${f.repo}`);
|
|
15149
|
+
}
|
|
15150
|
+
break;
|
|
15151
|
+
}
|
|
15152
|
+
case "mr": {
|
|
15153
|
+
const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
|
|
15154
|
+
const token = process.env.SOL_TOKEN || authExpired2();
|
|
15155
|
+
const { mrSummary: mrSummary2 } = await Promise.resolve().then(() => exports_mr);
|
|
15156
|
+
const sub = args[0];
|
|
15157
|
+
const localRefs = () => existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
|
|
15158
|
+
const flag2 = (name) => {
|
|
15159
|
+
const i = args.indexOf(name);
|
|
15160
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
15161
|
+
};
|
|
15162
|
+
const has2 = (name) => args.includes(name);
|
|
15163
|
+
const idArg = () => +(args[1] || die("this `sol pr` command needs an <id>"));
|
|
15164
|
+
if (sub === "open") {
|
|
15165
|
+
const refs = localRefs();
|
|
15166
|
+
const fromBranch = flag2("--from") || refs.current;
|
|
15167
|
+
const fromHead = refs.branches[fromBranch]?.head;
|
|
15168
|
+
const upstream = flag2("--upstream") || (has2("--upstream") ? cfg.forkParent : undefined);
|
|
15169
|
+
const target = upstream ? { url: cfg.url, repo: upstream } : cfg;
|
|
15170
|
+
const pr = await mrOpen(target, token, { fromRepo: upstream ? cfg.repo : undefined, fromBranch, fromHead, toBranch: flag2("--to"), title: flag2("-t") || flag2("--title"), body: flag2("-m") || flag2("--body") });
|
|
15171
|
+
const where = upstream ? `${cfg.repo}/${pr.fromBranch} -> ${upstream}/${pr.toBranch}` : `${pr.fromBranch} -> ${pr.toBranch}`;
|
|
15172
|
+
console.log(`opened MR #${pr.id}${upstream ? ` on ${upstream}` : ""}: ${pr.title} (${where})`);
|
|
15173
|
+
} else if (sub === "list") {
|
|
15174
|
+
const { mrs } = await mrList(cfg, token);
|
|
15175
|
+
if (!mrs.length)
|
|
15176
|
+
console.log("(no merge requests)");
|
|
15177
|
+
for (const p of mrs)
|
|
15178
|
+
console.log(`#${p.id} [${p.status}] ${p.fromRepo ? `${p.fromRepo}/` : ""}${p.fromBranch} -> ${p.toBranch} "${p.title}" — ${mrSummary2(p)}`);
|
|
15179
|
+
} else if (sub === "show") {
|
|
15180
|
+
const pr = await mrGet(cfg, token, idArg());
|
|
15181
|
+
console.log(`MR #${pr.id} [${pr.status}] by ${pr.author}`);
|
|
15182
|
+
console.log(`${pr.fromRepo ? `${pr.fromRepo}/` : ""}${pr.fromBranch} (${(pr.fromHead || "").slice(0, 12)}) -> ${pr.toBranch}${pr.fromRepo ? " [cross-fork]" : ""}`);
|
|
15183
|
+
console.log(pr.title);
|
|
15184
|
+
if (pr.body)
|
|
15185
|
+
console.log(`
|
|
15186
|
+
${pr.body}`);
|
|
15187
|
+
console.log(`
|
|
15188
|
+
${mrSummary2(pr)}`);
|
|
15189
|
+
for (const c of pr.checks)
|
|
15190
|
+
console.log(` check ${c.name}: ${c.status}${c.detail ? ` (${c.detail.split(`
|
|
15191
|
+
`)[0]})` : ""}`);
|
|
15192
|
+
for (const r of pr.reviews)
|
|
15193
|
+
console.log(` review ${r.actor}: ${r.verdict}${r.body ? ` — ${r.body}` : ""}`);
|
|
15194
|
+
for (const c of pr.comments)
|
|
15195
|
+
console.log(` comment ${c.actor}${c.path ? ` @${c.path}:${c.line}` : ""}: ${c.body}`);
|
|
15196
|
+
console.log(`
|
|
15197
|
+
--- diff (what would merge) ---`);
|
|
15198
|
+
if (pr.fromRepo) {
|
|
15199
|
+
const d = await mrDiff(cfg, token, pr.id);
|
|
15200
|
+
for (const p of d.added ?? [])
|
|
15201
|
+
console.log(` + ${p}`);
|
|
15202
|
+
for (const m of d.modified ?? []) {
|
|
15203
|
+
const path = typeof m === "string" ? m : m.path;
|
|
15204
|
+
console.log(` ~ ${path}`);
|
|
15205
|
+
const hunks2 = typeof m === "string" ? "" : m.hunks;
|
|
15206
|
+
if (hunks2)
|
|
15207
|
+
console.log(hunks2.split(`
|
|
15208
|
+
`).map((l) => " " + l).join(`
|
|
15209
|
+
`));
|
|
15210
|
+
}
|
|
15211
|
+
for (const p of d.removed ?? [])
|
|
15212
|
+
console.log(` - ${p}`);
|
|
15213
|
+
if (!((d.added?.length ?? 0) + (d.modified?.length ?? 0) + (d.removed?.length ?? 0)))
|
|
15214
|
+
console.log(" (no changes)");
|
|
15215
|
+
} else {
|
|
15216
|
+
const toHead = localRefs().branches[pr.toBranch]?.head;
|
|
15217
|
+
if (toHead && pr.fromHead) {
|
|
15218
|
+
try {
|
|
15219
|
+
printTreeDiff(diffTrees(loadStore(), toHead, pr.fromHead));
|
|
15220
|
+
} catch {
|
|
15221
|
+
console.log("(run `sol pull` to compute the diff locally)");
|
|
15222
|
+
}
|
|
15223
|
+
}
|
|
15224
|
+
}
|
|
15225
|
+
} else if (sub === "review") {
|
|
15226
|
+
const verdict = has2("--approve") ? "approve" : has2("--request-changes") ? "request-changes" : "comment";
|
|
15227
|
+
const pr = await mrAction(cfg, token, "review", { id: idArg(), verdict, body: flag2("-m") ?? "" });
|
|
15228
|
+
console.log(`reviewed MR #${pr.id}: ${verdict}`);
|
|
15229
|
+
} else if (sub === "comment") {
|
|
15230
|
+
const line = flag2("--line");
|
|
15231
|
+
const pr = await mrAction(cfg, token, "comment", { id: idArg(), body: flag2("-m") ?? "", path: flag2("--path"), line: line ? +line : undefined });
|
|
15232
|
+
console.log(`commented on MR #${pr.id}`);
|
|
15233
|
+
} else if (sub === "check") {
|
|
15234
|
+
const id = idArg();
|
|
15235
|
+
if (has2("--run")) {
|
|
15236
|
+
const dashes = args.indexOf("--");
|
|
15237
|
+
if (dashes < 0 || args.length <= dashes + 1)
|
|
15238
|
+
die("usage: sol mr check <id> --run -- <test command>");
|
|
15239
|
+
const cmd2 = args.slice(dashes + 1);
|
|
15240
|
+
const name = flag2("--name") || cmd2.join(" ");
|
|
15241
|
+
let status = "pass";
|
|
15242
|
+
let detail = "";
|
|
15243
|
+
try {
|
|
15244
|
+
execFileSync3(cmd2[0], cmd2.slice(1), { cwd, stdio: "pipe" });
|
|
15245
|
+
} catch (e) {
|
|
15246
|
+
status = "fail";
|
|
15247
|
+
const err = e;
|
|
15248
|
+
detail = (err.stdout?.toString() || err.message || "").slice(-200);
|
|
15249
|
+
}
|
|
15250
|
+
const pr = await mrAction(cfg, token, "check", { id, name, status, detail });
|
|
15251
|
+
console.log(`check '${name}' on MR #${pr.id}: ${status}`);
|
|
15252
|
+
} else {
|
|
15253
|
+
const name = flag2("--name") || "check";
|
|
15254
|
+
const status = flag2("--status") || "pending";
|
|
15255
|
+
await mrAction(cfg, token, "check", { id, name, status, detail: flag2("--detail") });
|
|
15256
|
+
console.log(`reported check '${name}' = ${status} on MR #${id}`);
|
|
15257
|
+
}
|
|
15258
|
+
} else if (sub === "merge") {
|
|
15259
|
+
const id = idArg();
|
|
15260
|
+
try {
|
|
15261
|
+
const pr = await mrAction(cfg, token, "merge", { id, force: has2("--force") });
|
|
15262
|
+
console.log(`merged MR #${pr.id} -> ${pr.toBranch} now at ${(pr.mergeHead || "").slice(0, 12)} (run \`sol pull\` to sync)`);
|
|
15263
|
+
} catch (e) {
|
|
15264
|
+
const msg = String(e.message).replace(/^remote \/mr\/merge -> \d+: /, "");
|
|
15265
|
+
let clean = msg;
|
|
15266
|
+
try {
|
|
15267
|
+
const j = JSON.parse(msg);
|
|
15268
|
+
clean = j.reasons?.length ? `${j.error}: ${j.reasons.join("; ")} — use \`--force\` to override` : j.error;
|
|
15269
|
+
} catch {}
|
|
15270
|
+
die(clean);
|
|
15271
|
+
}
|
|
15272
|
+
} else if (sub === "close") {
|
|
15273
|
+
const pr = await mrAction(cfg, token, "close", { id: idArg() });
|
|
15274
|
+
console.log(`closed MR #${pr.id}`);
|
|
15275
|
+
} else {
|
|
15276
|
+
die("usage: sol mr open|list|show <id>|review <id>|comment <id>|check <id>|merge <id>|close <id>");
|
|
15277
|
+
}
|
|
15278
|
+
break;
|
|
15279
|
+
}
|
|
15280
|
+
case "watch": {
|
|
15281
|
+
const { repo, log } = open();
|
|
15282
|
+
const tick = async (announce) => {
|
|
15283
|
+
const release2 = acquireLock();
|
|
15284
|
+
let changed = 0;
|
|
15285
|
+
try {
|
|
15286
|
+
const snap = await snapshotTree(repo);
|
|
15287
|
+
changed = snap.changed;
|
|
15288
|
+
if (changed) {
|
|
15289
|
+
await appendCapture(log, snap.root);
|
|
15290
|
+
writeWorkingIndex(await repo.list());
|
|
15291
|
+
}
|
|
15292
|
+
} finally {
|
|
15293
|
+
release2();
|
|
15294
|
+
}
|
|
15295
|
+
const t = new Date().toLocaleTimeString();
|
|
15296
|
+
if (changed)
|
|
15297
|
+
console.log(`[${t}] auto-captured ${changed} change(s) -> ${(await repo.head()).slice(0, 12)}`);
|
|
15298
|
+
else if (announce)
|
|
15299
|
+
console.log(`[${t}] working tree already up to date`);
|
|
15300
|
+
};
|
|
15301
|
+
await tick(true);
|
|
15302
|
+
console.log(`sol watching ${cwd}
|
|
15303
|
+
every change is auto-captured into the op-log — nothing is lost, even if you never commit.
|
|
15304
|
+
run \`sol commit "msg"\` any time to mark a milestone. Ctrl-C to stop.`);
|
|
15305
|
+
let timer;
|
|
15306
|
+
watch2(cwd, { recursive: true }, (_e, filename) => {
|
|
15307
|
+
if (!filename)
|
|
15308
|
+
return;
|
|
15309
|
+
const top = String(filename).split(sep3)[0];
|
|
15310
|
+
if (top === ".sol" || DEFAULT_IGNORE.includes(top))
|
|
15311
|
+
return;
|
|
15312
|
+
clearTimeout(timer);
|
|
15313
|
+
timer = setTimeout(() => tick(false).catch((e) => console.error("sol: " + (e?.message || e))), 400);
|
|
15314
|
+
});
|
|
15315
|
+
await new Promise(() => {});
|
|
15316
|
+
break;
|
|
15317
|
+
}
|
|
15318
|
+
case "run": {
|
|
15319
|
+
const { repo, log } = open();
|
|
15320
|
+
const keep = [];
|
|
15321
|
+
const command = [];
|
|
15322
|
+
let isolate = false;
|
|
15323
|
+
for (let i = 0;i < args.length; i++) {
|
|
15324
|
+
if (args[i] === "--keep")
|
|
15325
|
+
keep.push(args[++i]);
|
|
15326
|
+
else if (args[i] === "--isolate")
|
|
15327
|
+
isolate = true;
|
|
15328
|
+
else
|
|
15329
|
+
command.push(args[i]);
|
|
15330
|
+
}
|
|
15331
|
+
if (!command.length)
|
|
15332
|
+
die("usage: sol run [--keep <path>] [--isolate] <command...>");
|
|
15333
|
+
const { capture: capture3, hydrate: hydrate3, isolateCommand: isolateCommand2 } = await Promise.resolve().then(() => (init_runtime(), exports_runtime));
|
|
15334
|
+
const head = await repo.head();
|
|
15335
|
+
const dir = mkdtempSync2(join19(tmpdir2(), "sol-run-"));
|
|
15336
|
+
try {
|
|
15337
|
+
const hn = hydrate3(loadStore(), head, dir);
|
|
15338
|
+
console.log(`hydrated ${hn} file(s) -> sandbox${isolate ? " (isolated: no network, writes confined to the sandbox)" : ""}`);
|
|
15339
|
+
const argv2 = isolate ? isolateCommand2(command, dir) : command;
|
|
15340
|
+
console.log(`$ ${command.join(" ")}`);
|
|
15341
|
+
let failed = 0;
|
|
15342
|
+
try {
|
|
15343
|
+
execFileSync3(argv2[0], argv2.slice(1), { cwd: dir, stdio: "inherit" });
|
|
15344
|
+
} catch (e) {
|
|
15345
|
+
failed = typeof e?.status === "number" ? e.status : 1;
|
|
15346
|
+
}
|
|
15347
|
+
if (failed) {
|
|
15348
|
+
console.error(`(command failed with exit ${failed}; nothing captured)`);
|
|
15349
|
+
process.exitCode = failed;
|
|
15350
|
+
break;
|
|
15351
|
+
}
|
|
15352
|
+
const { written, deleted } = await capture3(repo, dir, keep);
|
|
15353
|
+
if (written.length || deleted.length) {
|
|
15354
|
+
await appendCommit(log, await repo.head(), `run: ${command.join(" ")}`, head);
|
|
15355
|
+
if (existsSync18(refsPath())) {
|
|
15356
|
+
const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
|
|
15357
|
+
if (refs.branches[refs.current]) {
|
|
15358
|
+
refs.branches[refs.current].head = await repo.head();
|
|
15359
|
+
saveRefs(refs);
|
|
15360
|
+
}
|
|
15361
|
+
}
|
|
15362
|
+
const synced = loadStore();
|
|
15363
|
+
const nh = await repo.head();
|
|
15364
|
+
for (const f of written)
|
|
15365
|
+
materialize(synced, nh, f);
|
|
15366
|
+
for (const f of deleted) {
|
|
15367
|
+
try {
|
|
15368
|
+
unlinkSync8(join19(cwd, f));
|
|
15369
|
+
} catch {}
|
|
15370
|
+
}
|
|
15371
|
+
console.log(`captured ${written.length} written, ${deleted.length} deleted file(s):`);
|
|
15372
|
+
for (const f of written)
|
|
15373
|
+
console.log(" + " + f);
|
|
15374
|
+
for (const f of deleted)
|
|
15375
|
+
console.log(" - " + f);
|
|
15376
|
+
} else {
|
|
15377
|
+
console.log("no files captured (the run produced no tracked changes)");
|
|
15378
|
+
}
|
|
15379
|
+
} finally {
|
|
15380
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
15381
|
+
}
|
|
15382
|
+
break;
|
|
15383
|
+
}
|
|
15384
|
+
case "git": {
|
|
15385
|
+
const [{ exportHistoryToGit: exportHistoryToGit2, importGitRepo: importGitRepo2 }, { hydrate: hydrate3 }] = await Promise.all([Promise.resolve().then(() => (init_git_adapter(), exports_git_adapter)), Promise.resolve().then(() => (init_runtime(), exports_runtime))]);
|
|
15386
|
+
const sub = args[0];
|
|
15387
|
+
if (sub === "import") {
|
|
15388
|
+
const gitPath = resolve5(cwd, args[1] || die("usage: sol git import <git-repo> [dir]"));
|
|
15389
|
+
const target = resolve5(cwd, args[2] || basename2(gitPath));
|
|
15390
|
+
const fdir = join19(target, ".sol");
|
|
15391
|
+
if (existsSync18(fdir))
|
|
15392
|
+
die("already a sol repo: " + target);
|
|
15393
|
+
mkdirSync12(fdir, { recursive: true });
|
|
15394
|
+
const { commits, branches, head, current } = await importGitRepo2(gitPath, fdir);
|
|
15395
|
+
const refsBranches = {};
|
|
15396
|
+
for (const b of branches)
|
|
15397
|
+
refsBranches[b.name] = { head: b.head, base: b.head };
|
|
15398
|
+
if (!refsBranches[current])
|
|
15399
|
+
refsBranches[current] = { head, base: head };
|
|
15400
|
+
refsBranches[current].head = head;
|
|
15401
|
+
writeFileSync16(join19(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
|
|
15402
|
+
const store2 = new Store;
|
|
15403
|
+
for (const name of readdirSync9(join19(fdir, "objects"))) {
|
|
15404
|
+
if (name.endsWith(".tmp"))
|
|
15405
|
+
continue;
|
|
15406
|
+
try {
|
|
15407
|
+
store2.put(decodeObject(readFileSync18(join19(fdir, "objects", name))));
|
|
15408
|
+
} catch {}
|
|
15409
|
+
}
|
|
15410
|
+
const onDisk = hydrate3(store2, head, target);
|
|
15411
|
+
console.log(`imported ${commits} commit(s), ${branches.length} branch(es) from git -> ${args[2] || basename2(gitPath)} (${onDisk} files; on branch ${current})`);
|
|
15412
|
+
} else if (sub === "export") {
|
|
15413
|
+
const { log } = open();
|
|
15414
|
+
const gitPath = resolve5(cwd, args[1] || die("usage: sol git export <git-repo> [-b branch]"));
|
|
15415
|
+
const store2 = loadStore();
|
|
15416
|
+
const head = await log.head() ?? "";
|
|
15417
|
+
if (!head || !listAll(store2, head).length) {
|
|
15418
|
+
die(`nothing to export — this Sol repo has no committed files yet. \`sol commit\` your work first, then \`sol git export ${args[1]}\`.`);
|
|
15419
|
+
}
|
|
15420
|
+
const bi = args.indexOf("-b");
|
|
15421
|
+
const branch = bi >= 0 ? args[bi + 1] : (await loadRefs(log)).current || "main";
|
|
15422
|
+
try {
|
|
15423
|
+
const { commits, head: oid } = await exportHistoryToGit2(await log.history(), head, async (h) => store2.get(h), store2, gitPath, branch, loadTrust());
|
|
15424
|
+
console.log(`exported ${commits} commit(s) -> refs/heads/${branch} @ ${oid.slice(0, 12)} in ${args[1]} (now \`git push\`)`);
|
|
15425
|
+
} catch (e) {
|
|
15426
|
+
die(e.message);
|
|
15427
|
+
}
|
|
15428
|
+
} else {
|
|
15429
|
+
die("usage: sol git import <repo> [dir] | sol git export <repo> [-b branch]");
|
|
15430
|
+
}
|
|
15431
|
+
break;
|
|
15432
|
+
}
|
|
15433
|
+
case "view": {
|
|
15434
|
+
const { log } = open();
|
|
15435
|
+
if (readViewMeta(solDir))
|
|
15436
|
+
die("already inside a view — create views from the parent repo (its `.sol` owns the shared store + op-log).");
|
|
15437
|
+
const name = args.find((a) => !a.startsWith("-")) || die("usage: sol view <name> [dir]");
|
|
15438
|
+
const rest = args.filter((a) => !a.startsWith("-"));
|
|
15439
|
+
const defaultDir = join19(dirname6(cwd), `${basename2(cwd)}-${name}`);
|
|
15440
|
+
const viewDir = rest[1] ? resolve5(procCwd, rest[1]) : defaultDir;
|
|
15441
|
+
if (existsSync18(join19(viewDir, ".sol")))
|
|
15442
|
+
die("already a sol repo/view: " + viewDir);
|
|
15443
|
+
const branch = `view/${name}`;
|
|
15444
|
+
const startHead = await log.head() ?? emptyRoot(loadStore());
|
|
15445
|
+
const { createView: createView2, sharedObjectCount: sharedObjectCount2 } = await Promise.resolve().then(() => (init_views(), exports_views));
|
|
15446
|
+
const { files } = createView2({ parentSol: solDir, viewDir, name, branch, actor, startHead });
|
|
15447
|
+
const objN = sharedObjectCount2(solDir);
|
|
15448
|
+
console.log(`created view '${name}' -> ${viewDir}`);
|
|
15449
|
+
console.log(` branch ${branch} @ ${(startHead || "empty").slice(0, 12)} (you: ${actor})`);
|
|
15450
|
+
console.log(` ${files} file(s) materialized; SHARING ${objN} object(s) from ${solDir} (zero copy — one store on disk)`);
|
|
15451
|
+
console.log(` -> cd ${viewDir} && edit + \`sol commit -m "..." <files>\`; converge with \`sol pull ${cwd}\` (or from the parent: \`sol pull ${viewDir}\`)`);
|
|
15452
|
+
break;
|
|
15453
|
+
}
|
|
15454
|
+
case "views": {
|
|
15455
|
+
if (!existsSync18(solDir))
|
|
15456
|
+
die("not a sol repo");
|
|
15457
|
+
const parentSol = parentSolDir(solDir) ?? solDir;
|
|
15458
|
+
const { pruneViews: pruneViews2, viewStatuses: viewStatuses2, sharedObjectCount: sharedObjectCount2 } = await Promise.resolve().then(() => (init_views(), exports_views));
|
|
15459
|
+
if (args[0] === "--prune" || args[0] === "prune") {
|
|
15460
|
+
const named = args.slice(1).find((a) => !a.startsWith("-"));
|
|
15461
|
+
const removed = pruneViews2(parentSol, { name: named, deleteDir: args.includes("--delete") });
|
|
15462
|
+
console.log(removed.length ? `pruned ${removed.length} view(s): ${removed.join(", ")}` : "no views to prune");
|
|
15463
|
+
break;
|
|
15464
|
+
}
|
|
15465
|
+
const views = await viewStatuses2(parentSol);
|
|
15466
|
+
if (args.includes("--json")) {
|
|
15467
|
+
console.log(JSON.stringify({ repo: parentSol, sharedObjects: sharedObjectCount2(parentSol), views }));
|
|
15468
|
+
break;
|
|
15469
|
+
}
|
|
15470
|
+
if (!views.length) {
|
|
15471
|
+
console.log("no views — create one with `sol view <name>` (a clone-free agent working tree sharing this store)");
|
|
15472
|
+
break;
|
|
15473
|
+
}
|
|
15474
|
+
console.log(`views of ${parentSol} (${sharedObjectCount2(parentSol)} SHARED object(s), one store on disk):`);
|
|
15475
|
+
for (const v of views) {
|
|
15476
|
+
const state = v.stale ? "STALE" : "active";
|
|
15477
|
+
console.log(` ${v.name.padEnd(16)} ${v.branch.padEnd(18)} ${(v.head || "empty").slice(0, 12)} ${v.actor.padEnd(12)} [${state}]`);
|
|
15478
|
+
console.log(` ${v.dir}`);
|
|
15479
|
+
}
|
|
15480
|
+
console.log(" prune: `sol views --prune` (drop gone dirs) | `sol views --prune <name> [--delete]`");
|
|
15481
|
+
break;
|
|
15482
|
+
}
|
|
15483
|
+
case "check": {
|
|
15484
|
+
open();
|
|
15485
|
+
const cd = cfgDir2();
|
|
15486
|
+
const setIdx = args.indexOf("--set");
|
|
15487
|
+
if (setIdx >= 0) {
|
|
15488
|
+
const cmd2 = args[setIdx + 1] || die('usage: sol check --set "<command>" (e.g. sol check --set "bun test")');
|
|
15489
|
+
const cfg2 = loadSolConfig(cd);
|
|
15490
|
+
cfg2.check = cmd2;
|
|
15491
|
+
saveSolConfig(cd, cfg2);
|
|
15492
|
+
console.log(`check command set: ${cmd2}
|
|
15493
|
+
it runs automatically after a converging merge (pull/merge/push); \`sol check\` runs it now.`);
|
|
15494
|
+
break;
|
|
15495
|
+
}
|
|
15496
|
+
if (args.includes("--clear")) {
|
|
15497
|
+
const cfg2 = loadSolConfig(cd);
|
|
15498
|
+
delete cfg2.check;
|
|
15499
|
+
saveSolConfig(cd, cfg2);
|
|
15500
|
+
clearSemanticFlag(solDir);
|
|
15501
|
+
console.log("check command cleared (the convergence gate is now inert)");
|
|
15502
|
+
break;
|
|
15503
|
+
}
|
|
15504
|
+
if (args.includes("--show")) {
|
|
15505
|
+
const c = loadSolConfig(cd).check;
|
|
15506
|
+
console.log(c ? `check: ${c}` : 'no check configured — set one with `sol check --set "<cmd>"`');
|
|
15507
|
+
break;
|
|
15508
|
+
}
|
|
15509
|
+
const cfg = loadSolConfig(cd);
|
|
15510
|
+
if (!cfg.check) {
|
|
15511
|
+
if (args.includes("--json"))
|
|
15512
|
+
console.log(JSON.stringify({ configured: false, ok: true }));
|
|
15513
|
+
else
|
|
15514
|
+
console.log('no check configured — set one with `sol check --set "<cmd>"` (e.g. `sol check --set "bun test"`)');
|
|
15515
|
+
break;
|
|
15516
|
+
}
|
|
15517
|
+
const outcome = runCheck(cd, cwd);
|
|
15518
|
+
if (outcome.ok)
|
|
15519
|
+
clearSemanticFlag(solDir);
|
|
15520
|
+
if (args.includes("--json")) {
|
|
15521
|
+
console.log(JSON.stringify(outcome));
|
|
15522
|
+
} else {
|
|
15523
|
+
console.log(`check: ${cfg.check}`);
|
|
15524
|
+
console.log(outcome.ok ? "PASS" : `FAIL (exit ${outcome.exitCode})`);
|
|
15525
|
+
if (outcome.output)
|
|
15526
|
+
console.log(outcome.output);
|
|
15527
|
+
}
|
|
15528
|
+
process.exitCode = outcome.ok ? 0 : 1;
|
|
15529
|
+
break;
|
|
15530
|
+
}
|
|
15531
|
+
case "mcp": {
|
|
15532
|
+
const http = await resolveMcpHttp2(args);
|
|
15533
|
+
if (args.includes("--secret") || args.includes("--secrets")) {
|
|
15534
|
+
const { startSecretMcp: startSecretMcp2 } = await Promise.resolve().then(() => (init_sol_secret_mcp(), exports_sol_secret_mcp));
|
|
15535
|
+
await startSecretMcp2({ solDir, http });
|
|
15536
|
+
} else {
|
|
15537
|
+
const { startWorkspaceMcp: startWorkspaceMcp2 } = await Promise.resolve().then(() => (init_sol_mcp(), exports_sol_mcp));
|
|
15538
|
+
await startWorkspaceMcp2({ solDir, http });
|
|
15539
|
+
}
|
|
15540
|
+
break;
|
|
15541
|
+
}
|
|
15542
|
+
case "env":
|
|
15543
|
+
case "secret":
|
|
15544
|
+
case "resolve": {
|
|
15545
|
+
if (cmd === "secret" && args[0] === "mcp") {
|
|
15546
|
+
const http = await resolveMcpHttp2(args.slice(1), "sol-secrets");
|
|
15547
|
+
const { startSecretMcp: startSecretMcp2 } = await Promise.resolve().then(() => (init_sol_secret_mcp(), exports_sol_secret_mcp));
|
|
15548
|
+
await startSecretMcp2({ solDir, http });
|
|
15549
|
+
break;
|
|
15550
|
+
}
|
|
15551
|
+
if (!existsSync18(solDir))
|
|
15552
|
+
die("not a sol repo — run `sol init` first");
|
|
15553
|
+
const { runEnv: runEnv2, runSecret: runSecret2, resolveReference: resolveReference2 } = await Promise.resolve().then(() => (init_secret2(), exports_secret2));
|
|
15554
|
+
const { loadSelfIdentity: loadSelfIdentity2, loadManageIdentity: loadManageIdentity2, fetchKey: fetchKey2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
|
|
15555
|
+
const { loadIdentity: loadIdentity2 } = await Promise.resolve().then(() => (init_identity_store(), exports_identity_store));
|
|
15556
|
+
const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL2).replace(/\/+$/, "");
|
|
15557
|
+
const selfPub = (account) => {
|
|
15558
|
+
const id = loadIdentity2();
|
|
15559
|
+
return id && id.accountId === account ? id.x25519Pub : undefined;
|
|
15560
|
+
};
|
|
15561
|
+
const dirPub = async (account) => (await fetchKey2(dirUrl, account))?.x25519Pub;
|
|
15562
|
+
const selfEdPub = (account) => {
|
|
15563
|
+
const id = loadIdentity2();
|
|
15564
|
+
return id && id.accountId === account ? id.edPub : undefined;
|
|
15565
|
+
};
|
|
15566
|
+
const dirEdPub = async (account) => (await fetchKey2(dirUrl, account))?.edPub;
|
|
15567
|
+
const remoteAnchorVerify = async () => {
|
|
15568
|
+
const rcfg = resolveRemote2(solDir);
|
|
15569
|
+
const token = process.env.SOL_TOKEN || await loadStoredToken2();
|
|
15570
|
+
if (!rcfg || !token)
|
|
15571
|
+
return;
|
|
15572
|
+
const { remoteEnvAnchor: remoteEnvAnchor2 } = await Promise.resolve().then(() => (init_remote(), exports_remote));
|
|
15573
|
+
const { readEnvStateBundle: readEnvStateBundle2 } = await Promise.resolve().then(() => (init_anchor(), exports_anchor));
|
|
15574
|
+
const { verifyLocalAgainstRemote: verifyLocalAgainstRemote2 } = await Promise.resolve().then(() => (init_env_state(), exports_env_state));
|
|
15575
|
+
const { record } = await remoteEnvAnchor2(rcfg, token);
|
|
15576
|
+
const local = readEnvStateBundle2(solDir) ?? { v: 1, journal: [], journalLen: 0 };
|
|
15577
|
+
return verifyLocalAgainstRemote2(local, record ?? undefined);
|
|
15578
|
+
};
|
|
15579
|
+
const ctx = { solDir, actor, loadSelfIdentity: () => loadSelfIdentity2(), loadManageIdentity: () => loadManageIdentity2(), die, selfPub, dirPub, selfEdPub, dirEdPub, remoteAnchorVerify };
|
|
15580
|
+
if (cmd === "env")
|
|
15581
|
+
await runEnv2(ctx, args[0], args.slice(1));
|
|
15582
|
+
else if (cmd === "secret")
|
|
15583
|
+
await runSecret2(ctx, args[0], args.slice(1));
|
|
15584
|
+
else {
|
|
15585
|
+
const ei = args.indexOf("--env");
|
|
15586
|
+
resolveReference2(ctx, args.filter((a) => !a.startsWith("-"))[0], ei >= 0 ? args[ei + 1] : undefined);
|
|
15587
|
+
}
|
|
15588
|
+
break;
|
|
15589
|
+
}
|
|
15590
|
+
default:
|
|
15591
|
+
if (cmd && cmd !== "help" && cmd !== "-h" && cmd !== "--help") {
|
|
15592
|
+
console.error("sol: unknown command: " + cmd + `
|
|
15593
|
+
`);
|
|
15594
|
+
process.exitCode = 1;
|
|
15595
|
+
}
|
|
15596
|
+
console.log(`sol — local content-addressed VCS (the new git)
|
|
15597
|
+
|
|
15598
|
+
the model: edit files freely, then commit — or run \`sol watch\` and every change is captured
|
|
15599
|
+
automatically. there is NO separate "git add".
|
|
15600
|
+
|
|
15601
|
+
everyday (examples are copy-safe — use real filenames):
|
|
15602
|
+
sol init create a repo in ./.sol
|
|
15603
|
+
sol watch AUTO-CAPTURE every change (run once; nothing is ever lost)
|
|
15604
|
+
sol run npm test hydrate to a sandbox, run, capture outputs (--isolate to confine)
|
|
15605
|
+
sol commit "what changed" commit the whole tree (SINGLE-AUTHOR; -m "msg" file.ts scopes to files)
|
|
15606
|
+
sol status current branch + uncommitted changes
|
|
15607
|
+
sol log commit history (sol log --all for every op)
|
|
15608
|
+
sol diff working-tree changes (sol diff main feature between refs)
|
|
15609
|
+
sol show 5 a commit's message + its diff
|
|
15610
|
+
sol undo roll back the LAST commit (non-destructive — it stays in history, fsck OK)
|
|
15611
|
+
sol revert <ref> commit the inverse of any commit (like git revert; full history kept)
|
|
15612
|
+
sol restore --from 3 app.py put a file (or the whole tree) back from a ref
|
|
15613
|
+
sol blame todo/cli.py who/which commit last touched each line
|
|
15614
|
+
sol ls / sol cat README.md list tracked files / print one
|
|
15615
|
+
sol rm old.txt delete a file (from the repo and disk)
|
|
15616
|
+
sol ignore "*.tmp" add an ignore pattern (no arg lists the active patterns)
|
|
15617
|
+
sol fsck / sol gc verify integrity / drop unreachable objects
|
|
15618
|
+
sol check --set "bun test" gate convergence on a test command: a merge that line-converges but FAILS
|
|
15619
|
+
the check is flagged as a SEMANTIC conflict (status state:"SEMANTIC"), not
|
|
15620
|
+
silently accepted. \`sol check\` runs it on demand. (sol check --help)
|
|
15621
|
+
|
|
15622
|
+
per-agent authorship signing (CRYPTOGRAPHIC author identity — not the forgeable plaintext SOL_ACTOR):
|
|
15623
|
+
sol keygen mint an Ed25519 keypair; prints SOL_SIGNING_KEY=<seed> + its fingerprint
|
|
15624
|
+
the orchestrator gives each agent its OWN seed (SOL_SIGNING_KEY / a keyfile)
|
|
15625
|
+
so every commit is signed AT authorship — log/show/blame show "signed <fp> ✓"
|
|
15626
|
+
sol trust <name> <fingerprint> bind an actor name to a key; a commit claiming that name but signed by a
|
|
15627
|
+
DIFFERENT key is then flagged FORGED (sol trust = list, --remove <name> = drop)
|
|
15628
|
+
|
|
15629
|
+
privacy (native per-path encryption — the host only ever stores ciphertext):
|
|
15630
|
+
sol hide "src/private/**" ROLE-BASED hiding: a VisibilityPolicy rule "only \`write\`+ may decrypt this" —
|
|
15631
|
+
the headline verb. \`sol seal <path>\` then seals to the resolved audience. (sol hide --help)
|
|
15632
|
+
sol seal secrets/.env NO recipients -> consult the policy for this path + seal to its audience
|
|
15633
|
+
sol seal secrets/.env alice explicit recipients (you + alice); keys stay LOCAL, history is host-blind
|
|
15634
|
+
\`sol cat\` decrypts for a recipient; everyone else sees <<sealed>>. (sol seal --help)
|
|
15635
|
+
sol seal secrets/.env @alice CROSS-ACCOUNT: wrap to alice's PUBLISHED X25519 key — she opens it on her own
|
|
15636
|
+
account, the host never holds a key (\`sol keys publish\` first). (sol seal --help)
|
|
15637
|
+
sol seal --reapply re-resolve every policy-covered seal + re-wrap to the CURRENT audience (bumps epoch)
|
|
15638
|
+
sol sealed list every sealed path + its audience (recipient accounts + fingerprints + epoch)
|
|
15639
|
+
|
|
15640
|
+
cross-account identity keys (host-blind: the directory holds PUBLIC keys only, never a private key or plaintext):
|
|
15641
|
+
sol keys init mint your X25519 identity keypair; the recovery code is shown ONCE (back it up)
|
|
15642
|
+
sol keys publish publish your PUBLIC key so others can seal to you across accounts
|
|
15643
|
+
sol keys rotate roll to a new key epoch (past seals stay on the old epoch until reapplied)
|
|
15644
|
+
sol keys verify <acct> <fpr> confirm a peer's published key out-of-band before sealing to them (anti-MITM)
|
|
15645
|
+
sol keys export / import encrypted keystore backup. (sol keys --help)
|
|
15646
|
+
|
|
15647
|
+
clone-free agent views (the worktree killer — N agents, one shared store on disk, zero duplication):
|
|
15648
|
+
sol view <name> [dir] new working tree + branch + index that SHARES this repo's object store
|
|
15649
|
+
(git would need a full worktree per agent: node_modules/build duplicated, ~GBs,
|
|
15650
|
+
and concurrent agents deadlock on .git/index.lock). commit in the view, then
|
|
15651
|
+
\`sol pull <view>\` converges losslessly. (sol view --help)
|
|
15652
|
+
sol views list every view (name, dir, branch, head, author, active/stale) + --prune
|
|
15653
|
+
|
|
15654
|
+
branches & tags:
|
|
15655
|
+
sol branch list branches (sol branch feature creates one at HEAD)
|
|
15656
|
+
sol switch feature switch to a branch (captures current work first, never loses it)
|
|
15657
|
+
sol merge feature 3-way merge a branch in (conflicts land in the working tree)
|
|
15658
|
+
sol tag v1 mark an immutable label at HEAD
|
|
15659
|
+
|
|
15660
|
+
auth (sign in once; remote commands then use the cached token, no SOL_TOKEN needed):
|
|
15661
|
+
sol auth login [<web-url>] device-flow sign-in via the Midsummer web app; caches the token ~/.sol/credentials
|
|
15662
|
+
sol auth whoami print the identity behind the cached token (signed in as @<handle>)
|
|
15663
|
+
sol auth set-handle <name> choose your @handle — the <owner> in <owner>/<repo> (changing it re-namespaces repos)
|
|
15664
|
+
sol auth pat [days] mint a long-lived Personal Access Token for git/CI (default 90 days)
|
|
15665
|
+
sol auth status / logout show who you're signed in as / clear the cached token
|
|
15666
|
+
|
|
15667
|
+
remotes (self-hostable backend; token in SOL_TOKEN or via sol auth login):
|
|
15668
|
+
sol clone [<url>] <owner>/<repo> [dir] clone a remote repo (checks out PRODUCTION); default dir = <repo>
|
|
15669
|
+
sol push / sol pull sync your commits with the remote (push registers your branch's head)
|
|
15670
|
+
sol push <repo> one-step share: no remote set? use the hosted Sol + <repo>, then push
|
|
15671
|
+
sol push --public <repo> create + push + make public in one step (new repos are private by default)
|
|
15672
|
+
sol promote [branch] point the remote's production branch at <branch> (default: current)
|
|
15673
|
+
sol remote <url> <repo> set the remote (no arg: show it; url defaults to the hosted Sol)
|
|
15674
|
+
sol fork [<url>] <parent> <new> [dir] make your own copy of a repo (all branches + history + a parent link)
|
|
15675
|
+
sol forks list the forks of the current repo
|
|
15676
|
+
sol access [show|public|private|add <userId> <role>|remove <userId>] manage repo visibility + collaborators
|
|
15677
|
+
|
|
15678
|
+
merge requests (propose a branch's commits to merge; reviews + CI checks):
|
|
15679
|
+
sol mr open [--from B] [--to B] -t "title" [-m body] open an MR (from=current branch, to=production)
|
|
15680
|
+
sol mr list list MRs (open/merged/closed)
|
|
15681
|
+
sol mr show <id> metadata, reviews/checks, + the diff it would merge
|
|
15682
|
+
sol mr review <id> --approve | --request-changes | --comment [-m ...]
|
|
15683
|
+
sol mr check <id> --run [--name N] -- <test cmd> run the tests, report a pass/fail check
|
|
15684
|
+
sol mr merge <id> [--force] fast-forward-merge (gated on checks + reviews; --force overrides)
|
|
15685
|
+
sol mr close <id>
|
|
15686
|
+
|
|
15687
|
+
git interop (adopt Sol without leaving GitHub):
|
|
15688
|
+
sol git import <repo> [dir] import a git repo's HEAD into a new Sol repo (modes/symlinks/binaries)
|
|
15689
|
+
sol git export <repo> [-b br] replay the Sol commit DAG as git history (parents/merges/author/time + provenance trailers), then \`git push\`
|
|
15690
|
+
|
|
15691
|
+
for concurrent agents, use scoped commits (sol commit -m "msg" file) or a per-agent workspace.
|
|
15692
|
+
a solo author commits the whole tree freely; once OTHER authors are present the whole-tree commit is refused
|
|
15693
|
+
(--whole-tree / SOL_ALLOW_WHOLE_TREE=1 to override) so one agent can't sweep everyone's pending files.
|
|
15694
|
+
|
|
15695
|
+
a ref is a branch/tag name, a commit hash-prefix, an op seq number, or HEAD. content is SHA-256
|
|
15696
|
+
addressed; history is a tamper-evident hash-chained op-log. attribute changes with SOL_ACTOR=you.`);
|
|
15697
|
+
}
|
|
15698
|
+
} finally {
|
|
15699
|
+
release?.();
|
|
15700
|
+
}
|
|
15701
|
+
}
|
|
15702
|
+
async function runCli2(argv) {
|
|
15703
|
+
try {
|
|
15704
|
+
await dispatch2(argv);
|
|
15705
|
+
} catch (e) {
|
|
15706
|
+
const msg = e?.message || String(e);
|
|
15707
|
+
if (/ -> 401\b/.test(msg) || /\b401 unauthorized\b/i.test(msg))
|
|
15708
|
+
authExpired2();
|
|
15709
|
+
die(msg);
|
|
15710
|
+
}
|
|
15711
|
+
}
|
|
15712
|
+
var CRED_PATH2, DEFAULT_REMOTE_URL2, ATTEST_KEY2, remoteUrlArg2 = (a) => /^https?:\/\//.test(a[0] ?? "") ? [a[0].replace(/\/+$/, ""), a.slice(1)] : [DEFAULT_REMOTE_URL2, a], cfgDir2 = () => parentSolDir(solDir) ?? solDir;
|
|
15713
|
+
var init_dispatch2 = __esm(() => {
|
|
15714
|
+
init_chain();
|
|
15715
|
+
init_diff();
|
|
15716
|
+
init_file_store();
|
|
15717
|
+
init_prov();
|
|
15718
|
+
init_store();
|
|
15719
|
+
init_tree();
|
|
15720
|
+
init_lib();
|
|
15721
|
+
init_remote();
|
|
15722
|
+
init_lib();
|
|
15723
|
+
init_test_gate();
|
|
15724
|
+
CRED_PATH2 = join19(homedir3(), ".sol", "credentials");
|
|
15725
|
+
DEFAULT_REMOTE_URL2 = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
|
|
15726
|
+
ATTEST_KEY2 = process.env.SOL_ATTEST_KEY || undefined;
|
|
15727
|
+
});
|
|
15728
|
+
|
|
15729
|
+
// src/bin/sol.ts
|
|
15730
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
15731
|
+
function cliVersion3() {
|
|
15732
|
+
if (typeof __SOL_COMPILED_VERSION__ === "string" && __SOL_COMPILED_VERSION__)
|
|
15733
|
+
return __SOL_COMPILED_VERSION__;
|
|
15734
|
+
try {
|
|
15735
|
+
return JSON.parse(readFileSync19(new URL("./package.json", import.meta.url), "utf8")).version || "dev";
|
|
15736
|
+
} catch {
|
|
15737
|
+
return "dev";
|
|
15738
|
+
}
|
|
15739
|
+
}
|
|
15740
|
+
async function main() {
|
|
15741
|
+
const argv = process.argv.slice(2);
|
|
15742
|
+
if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
|
|
15743
|
+
console.log(`sol ${cliVersion3()}`);
|
|
15744
|
+
return;
|
|
15745
|
+
}
|
|
15746
|
+
const { runCli: runCli3 } = await Promise.resolve().then(() => (init_dispatch2(), exports_dispatch2));
|
|
15747
|
+
await runCli3(argv);
|
|
11713
15748
|
}
|
|
11714
15749
|
main();
|