midsummer-sol 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sol.js +64 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "midsummer-sol",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Sol — agent-native version control (a new git). CLI, MCP server, and no-filesystem SDK.",
5
5
  "bin": {
6
6
  "sol": "./sol.js",
package/sol.js CHANGED
@@ -885,7 +885,6 @@ import {
885
885
  readdirSync as readdirSync3,
886
886
  readFileSync as readFileSync3,
887
887
  readlinkSync,
888
- rmdirSync,
889
888
  symlinkSync,
890
889
  unlinkSync,
891
890
  writeFileSync as writeFileSync3
@@ -1403,7 +1402,6 @@ import {
1403
1402
  readdirSync as readdirSync5,
1404
1403
  readFileSync as readFileSync6,
1405
1404
  readlinkSync as readlinkSync3,
1406
- rmdirSync as rmdirSync2,
1407
1405
  symlinkSync as symlinkSync3,
1408
1406
  unlinkSync as unlinkSync4,
1409
1407
  writeFileSync as writeFileSync6
@@ -1581,20 +1579,42 @@ async function snapshotTree(repo) {
1581
1579
  function sleepSync(ms) {
1582
1580
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1583
1581
  }
1582
+ var LOCK_STALE_MS = 5000;
1583
+ function pidAlive(pid) {
1584
+ if (!pid || pid < 1)
1585
+ return false;
1586
+ try {
1587
+ process.kill(pid, 0);
1588
+ return true;
1589
+ } catch (e) {
1590
+ return e?.code === "EPERM";
1591
+ }
1592
+ }
1584
1593
  function acquireLock() {
1585
- const lockDir = join6(solDir2, "lock");
1594
+ const lockFile = join6(solDir2, "lock");
1586
1595
  const deadline = Date.now() + 15000;
1587
1596
  for (;; ) {
1588
1597
  try {
1589
- mkdirSync5(lockDir);
1598
+ writeFileSync6(lockFile, String(process.pid), { flag: "wx" });
1590
1599
  break;
1591
1600
  } catch {
1592
1601
  try {
1593
- if (Date.now() - lstatSync3(lockDir).mtimeMs > 30000) {
1594
- rmdirSync2(lockDir);
1595
- continue;
1602
+ const st = lstatSync3(lockFile);
1603
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
1604
+ let ownerPid = 0;
1605
+ try {
1606
+ ownerPid = parseInt(readFileSync6(lockFile, "utf8").trim(), 10) || 0;
1607
+ } catch {}
1608
+ if (!pidAlive(ownerPid)) {
1609
+ try {
1610
+ unlinkSync4(lockFile);
1611
+ } catch {}
1612
+ continue;
1613
+ }
1596
1614
  }
1597
- } catch {}
1615
+ } catch {
1616
+ continue;
1617
+ }
1598
1618
  if (Date.now() > deadline)
1599
1619
  die("repo is locked by another sol process (timed out)");
1600
1620
  sleepSync(25);
@@ -1606,7 +1626,7 @@ function acquireLock() {
1606
1626
  return;
1607
1627
  released = true;
1608
1628
  try {
1609
- rmdirSync2(lockDir);
1629
+ unlinkSync4(lockFile);
1610
1630
  } catch {}
1611
1631
  };
1612
1632
  process.on("exit", release);
@@ -2570,12 +2590,15 @@ async function main() {
2570
2590
  const args = argv.slice(1);
2571
2591
  const wantsHelp = args.includes("--help") || args.includes("-h");
2572
2592
  const SUBHELP = {
2573
- commit: `sol commit "<message>" commit the whole working tree (single author)
2574
- sol commit -m "<message>" <file>... commit ONLY those paths (correct attribution for concurrent agents)
2593
+ commit: `sol commit "<msg>" commits the whole working tree; works as-is for a solo author.
2594
+ sol commit -m "<msg>" <file>... commit ONLY those paths (correct attribution for concurrent agents)
2595
+
2596
+ (once OTHER authors have committed to this repo, a whole-tree commit is refused to avoid mis-attribution
2597
+ \u2014 scope to files \`sol commit -m "<msg>" <files>\` or force with --whole-tree).
2575
2598
 
2576
2599
  flags:
2577
2600
  -m <message> message when scoping to files
2578
- --whole-tree allow a whole-tree commit even with SOL_ACTOR set (else refused)
2601
+ --whole-tree force a whole-tree commit even after OTHER authors are present (the guard above)
2579
2602
  --force commit even with unresolved <<<<<<< conflict markers
2580
2603
 
2581
2604
  examples:
@@ -2584,14 +2607,21 @@ examples:
2584
2607
  push: `sol push push the current branch to the configured remote (converging \u2014 never FF-rejected)
2585
2608
  sol push <repo> if no remote is set, use the hosted Sol (${DEFAULT_REMOTE_URL}) + this repo name, then push
2586
2609
  sol push --create <repo> same, explicit form (creates the repo on first push)
2610
+ sol push --public <repo> create + push + make it public in ONE step (else new repos are private)
2611
+
2612
+ flags:
2613
+ --create explicit "creates the repo on first push" (the bare \`<repo>\` form does this too)
2614
+ --public after a successful create/push, set public access (same op as \`sol access public\`)
2587
2615
 
2588
2616
  notes:
2589
2617
  a remote already configured? the <repo>/--create arg is ignored \u2014 \`sol push\` just syncs.
2618
+ new repos are PRIVATE by default; \`--public\` is the one-step opt-in to share (or \`sol access public\` later).
2590
2619
  set SOL_TOKEN (or \`sol auth login\`) first; the push registers the current branch's head on the remote.
2591
2620
 
2592
2621
  examples:
2593
2622
  sol push # remote already configured
2594
- sol push alice/app # one step: configure hosted remote + push`,
2623
+ sol push alice/app # one step: configure hosted remote + push (private)
2624
+ sol push --create --public alice/app # create + push + share, all at once`,
2595
2625
  pull: `sol pull fetch + converge the current branch from the configured remote
2596
2626
  sol pull <dir> OFFLINE: converge another local repo's .sol on disk into this one (multi-agent, no network)
2597
2627
 
@@ -2634,7 +2664,8 @@ tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
2634
2664
  sol mr comment <id> -m msg [--path f --line N] | check <id> --run -- <cmd> | merge <id> [--force] | close <id>`,
2635
2665
  fork: "sol fork [<url>] <parent-repo> <new-repo> [dir] \u2014 your own copy (all branches + history + a parent link)",
2636
2666
  forks: "sol forks \u2014 list the forks of the current repo",
2637
- access: "sol access [show | public | private | add <userId> <read|write|admin> | remove <userId>]",
2667
+ access: "sol access [show | public | private | add <userId> <read|write|admin> | remove <userId>]\n tip: to share a NEW repo in one step, `sol push --public <repo>` (create + push + public) instead of pushing then `sol access public`.",
2668
+ share: "sol share \u2014 there is no separate share command: a new repo goes public in one step with `sol push --public <repo>`,\n or flip an existing one with `sol access public`. (new repos are private by default.)",
2638
2669
  auth: "sol auth [login [<web-url>] | logout | status | whoami | set-handle <name> | pat [days]]",
2639
2670
  clone: "sol clone [<url>] <owner>/<repo> [dir] \u2014 default dir = <repo> (url defaults to the hosted Sol)"
2640
2671
  };
@@ -2648,7 +2679,7 @@ tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
2648
2679
  if (t)
2649
2680
  process.env.SOL_TOKEN = t;
2650
2681
  }
2651
- const release = new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "pull", "run", "seal"]).has(cmd) && existsSync10(solDir2) ? acquireLock() : undefined;
2682
+ const release = new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "pull", "push", "restore", "checkout", "run", "seal"]).has(cmd) && existsSync10(solDir2) ? acquireLock() : undefined;
2652
2683
  try {
2653
2684
  switch (cmd) {
2654
2685
  case "init": {
@@ -2727,10 +2758,12 @@ tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
2727
2758
  commitRoot = await repo.head();
2728
2759
  } else {
2729
2760
  const optedIn = wholeTreeOptIn || process.env.SOL_ALLOW_WHOLE_TREE === "1";
2730
- const otherActorPresent = (await log.history()).some((o) => o.by && o.by !== actor2);
2761
+ const otherActors = [...new Set((await log.history()).map((o) => o.by).filter((b) => !!b && b !== actor2))];
2762
+ const otherActorPresent = otherActors.length > 0;
2731
2763
  if (process.env.SOL_ACTOR && !optedIn && otherActorPresent) {
2732
- die(`refusing a whole-tree commit as "${actor2}" \u2014 it would attribute ALL pending files to you.
2733
- concurrent agents: sol commit -m "${message}" <your files> (or a per-agent SolWorkspace/MCP)
2764
+ const others = otherActors.join(", ");
2765
+ die(`refusing a whole-tree commit as "${actor2}" \u2014 other author(s) have committed here (${others}), so sweeping the whole tree would mis-attribute their pending files to you. (this guard only fires because they're present; a solo author commits the whole tree freely.)
2766
+ scope to your files: sol commit -m "${message}" <your files> (or a per-agent SolWorkspace/MCP)
2734
2767
  if you truly own the whole tree: add --whole-tree or set SOL_ALLOW_WHOLE_TREE=1`);
2735
2768
  }
2736
2769
  const snap = await snapshotTree(repo);
@@ -2767,8 +2800,11 @@ tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
2767
2800
  const { repo, log } = open();
2768
2801
  const refs = existsSync10(refsPath()) ? await loadRefs(log) : undefined;
2769
2802
  const head = await repo.head();
2803
+ const headOp = head ? [...await log.history()].reverse().find((o) => o.rootAfter === head) : undefined;
2804
+ const headBy = headOp?.by ?? "?";
2770
2805
  console.log(`repo ${cwd2}`);
2771
- console.log(`on ${refs ? refs.current : "main"} head ${head.slice(0, 14)} seq ${await log.seq()} actor ${actor2}`);
2806
+ console.log(`on ${refs ? refs.current : "main"} head ${head ? `${head.slice(0, 14)} by ${headBy}` : "(empty)"} seq ${await log.seq()}`);
2807
+ console.log(`you: ${actor2}`);
2772
2808
  const ch = workingChanges(loadStore(), head);
2773
2809
  const dirty = ch.added.length + ch.modified.length + ch.removed.length;
2774
2810
  if (!dirty) {
@@ -3407,9 +3443,9 @@ tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
3407
3443
  }
3408
3444
  case "push": {
3409
3445
  const { log } = open();
3446
+ const wantPublic = args.includes("--public");
3410
3447
  if (!loadRemote(solDir2)) {
3411
- const ci = args.indexOf("--create");
3412
- const repoName = ci >= 0 ? args[ci + 1] : args.find((a) => !a.startsWith("-"));
3448
+ const repoName = args.find((a) => !a.startsWith("-"));
3413
3449
  if (repoName) {
3414
3450
  saveRemote(solDir2, { url: DEFAULT_REMOTE_URL, repo: repoName });
3415
3451
  console.log(`remote set: ${DEFAULT_REMOTE_URL} (repo ${repoName})`);
@@ -3468,6 +3504,10 @@ tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
3468
3504
  } else {
3469
3505
  console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${mergedNote}${conflictNote}${prod ? `; production=${prod}` : ""}`);
3470
3506
  }
3507
+ if (wantPublic) {
3508
+ const a = await accessSet(cfg, token, { visibility: "public" });
3509
+ console.log(`${cfg.repo} is now ${a.visibility}`);
3510
+ }
3471
3511
  break;
3472
3512
  }
3473
3513
  case "pull": {
@@ -4056,6 +4096,7 @@ remotes (self-hostable backend; token in SOL_TOKEN or via sol auth login):
4056
4096
  sol clone [<url>] <owner>/<repo> [dir] clone a remote repo (checks out PRODUCTION); default dir = <repo>
4057
4097
  sol push / sol pull sync your commits with the remote (push registers your branch's head)
4058
4098
  sol push <repo> one-step share: no remote set? use the hosted Sol + <repo>, then push
4099
+ sol push --public <repo> create + push + make public in one step (new repos are private by default)
4059
4100
  sol promote [branch] point the remote's production branch at <branch> (default: current)
4060
4101
  sol remote <url> <repo> set the remote (no arg: show it; url defaults to the hosted Sol)
4061
4102
  sol fork [<url>] <parent> <new> [dir] make your own copy of a repo (all branches + history + a parent link)
@@ -4076,7 +4117,8 @@ git interop (adopt Sol without leaving GitHub):
4076
4117
  sol git export <repo> write Sol's tree back as a git commit (+ deletes), then \`git push\`
4077
4118
 
4078
4119
  for concurrent agents, use scoped commits (sol commit -m "msg" file) or a per-agent workspace.
4079
- a whole-tree commit is REFUSED when SOL_ACTOR is set (--whole-tree / SOL_ALLOW_WHOLE_TREE=1 to override).
4120
+ a solo author commits the whole tree freely; once OTHER authors are present the whole-tree commit is refused
4121
+ (--whole-tree / SOL_ALLOW_WHOLE_TREE=1 to override) so one agent can't sweep everyone's pending files.
4080
4122
 
4081
4123
  a ref is a branch/tag name, a commit hash-prefix, an op seq number, or HEAD. content is SHA-256
4082
4124
  addressed; history is a tamper-evident hash-chained op-log. attribute changes with SOL_ACTOR=you.`);