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.
Files changed (4) hide show
  1. package/package.json +1 -1
  2. package/sol-mcp.js +11447 -94
  3. package/sol-secret-mcp.js +177 -15
  4. 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
- if (entryAud && entryAud.length)
4472
- return entryAud;
4473
- return world.files[env]?.audience ?? [];
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
- const missing = [...expected].filter((a) => !actual.has(a));
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 self = ctx.loadSelfIdentity();
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 ${ctx.actor} — are you a recipient? (set SOL_RECOVERY_CODE)`);
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 startSecretMcp(opts = {}) {
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 mkdirSync11, mkdtempSync, readdirSync as readdirSync8, readFileSync as readFileSync17, rmSync as rmSync2, unlinkSync as unlinkSync7, watch, writeFileSync as writeFileSync15 } from "node:fs";
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 join18, resolve as resolve4, sep as sep2 } from "node:path";
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 over stdio
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
- register: \`claude mcp add sol -- sol mcp\` | \`claude mcp add sol-secrets -- sol mcp --secret\``,
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 = join18(procCwd, ".sol");
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
- mkdirSync11(here, { recursive: true });
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(join18(cwd, rf))) {
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 = join18(solDir, "MERGE_HEAD");
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(join18(cwd, rp))) {
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(join18(cwd, path));
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 = join18(solDir, "objects");
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(join18(objDir, name));
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(join18(solDir, "env", "seal"))) {
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 = join18(cwd, ".solignore");
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(join18(solDir, "ops.jsonl"), res.ops.map((o) => JSON.stringify(o)).join(`
9853
+ writeFileSync15(join17(solDir, "ops.jsonl"), res.ops.map((o) => JSON.stringify(o)).join(`
9748
9854
  `) + (res.ops.length ? `
9749
9855
  ` : ""));
9750
- writeFileSync15(join18(solDir, "HEAD"), JSON.stringify({ head: res.newHead, seq: res.newSeq, logTip: res.newLogTip }));
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 = join18(cwd, path);
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(join18(solDir, "MERGE_HEAD"), other.head);
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
- mkdirSync11(dirname4(CRED_PATH), { recursive: true });
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 handle2 = want.toLowerCase();
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: handle2 }) });
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 = join18(dest, ".sol");
10803
+ const ddir = join17(dest, ".sol");
10681
10804
  if (existsSync17(ddir))
10682
10805
  die("already a sol repo: " + dest);
10683
- mkdirSync11(ddir, { recursive: true });
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(join18(ddir, "refs.json"), JSON.stringify({ current: "main", branches: { main: { head: res.head, base: res.head, remote: res.head } }, tags: {} }, null, 2));
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 = join18(target, ".sol");
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
- mkdirSync11(fdir, { recursive: true });
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(join18(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
10721
- writeFileSync15(join18(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
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(join18(cwd, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
11100
+ writeFileSync15(join17(cwd, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
10978
11101
  }
10979
- writeFileSync15(join18(solDir, "MERGE_HEAD"), remoteHead2);
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 = join18(target, ".sol");
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
- mkdirSync11(fdir, { recursive: true });
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(join18(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
11037
- writeFileSync15(join18(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
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(join18(tmpdir(), "sol-run-"));
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(join18(cwd, f));
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 = join18(target, ".sol");
11480
+ const fdir = join17(target, ".sol");
11358
11481
  if (existsSync17(fdir))
11359
11482
  die("already a sol repo: " + target);
11360
- mkdirSync11(fdir, { recursive: true });
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(join18(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
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(join18(fdir, "objects"))) {
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(join18(fdir, "objects", name))));
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 = join18(dirname4(cwd), `${basename(cwd)}-${name}`);
11529
+ const defaultDir = join17(dirname4(cwd), `${basename(cwd)}-${name}`);
11407
11530
  const viewDir = rest[1] ? resolve4(procCwd, rest[1]) : defaultDir;
11408
- if (existsSync17(join18(viewDir, ".sol")))
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: startWorkspaceMcp2 } = await Promise.resolve().then(() => (init_sol_mcp(), exports_sol_mcp));
11504
- await startWorkspaceMcp2({ solDir });
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 = join18(homedir2(), ".sol", "credentials");
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/sol.ts
11695
- import { readFileSync as readFileSync18 } from "fs";
11696
- function cliVersion2() {
11697
- if (typeof __SOL_COMPILED_VERSION__ === "string" && __SOL_COMPILED_VERSION__)
11698
- return __SOL_COMPILED_VERSION__;
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
- return JSON.parse(readFileSync18(new URL("./package.json", import.meta.url), "utf8")).version || "dev";
11701
- } catch {
11702
- return "dev";
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
- async function main() {
11706
- const argv = process.argv.slice(2);
11707
- if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
11708
- console.log(`sol ${cliVersion2()}`);
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 { runCli: runCli2 } = await Promise.resolve().then(() => (init_dispatch(), exports_dispatch));
11712
- await runCli2(argv);
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();