create-egregore 0.19.6 → 0.19.8

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.7.1
2
+ // egregore-launcher-version: 0.7.3
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -31,7 +31,7 @@ const https = require("node:https");
31
31
  const { spawnSync, spawn } = require("node:child_process");
32
32
  const readline = require("node:readline");
33
33
 
34
- const VERSION = "0.7.1";
34
+ const VERSION = "0.7.3";
35
35
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
36
36
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
37
37
  const PUBLIC_NPX_PACKAGE = "create-egregore";
@@ -264,6 +264,10 @@ function readRegistry() {
264
264
  for (const e of entries) {
265
265
  if (!e || typeof e.path !== "string" || !e.path) continue;
266
266
  const key = path.resolve(e.path);
267
+ // Worktrees live under a parent instance's .claude/worktrees/ — they are
268
+ // NOT their own egregore. A session that ran inside one may have registered
269
+ // its cwd; keep those out of the instance list (browser, ls, default).
270
+ if (key.includes("/.claude/worktrees/")) continue;
267
271
  if (seen.has(key)) continue;
268
272
  seen.add(key);
269
273
  result.push(e);
@@ -1317,292 +1321,6 @@ function settingsDump(inst) {
1317
1321
  try { return JSON.parse(r.stdout); } catch { return null; }
1318
1322
  }
1319
1323
 
1320
- // Nested raw-mode list editor for a domain (repo | admin | people): hover an
1321
- // item and press `d` to remove it, `a` to open an inline add field, esc to go
1322
- // back. Mirrors the main browser's affordances. Its own raw-mode loop; resolves
1323
- // when the user backs out. Each mutation is ONE deterministic settings.sh verb;
1324
- // validation, the last-admin guard, and GitHub-access notes surface as notices.
1325
- function listEditor(inst, domain, label) {
1326
- const fieldFor = (d) => {
1327
- if (domain === "repo") return (d.repos || []).map((r) => r.name || r);
1328
- if (domain === "admin") return d.admins || [];
1329
- return d.people || [];
1330
- };
1331
- const noun = domain === "repo" ? "repo name" : "github handle";
1332
-
1333
- return new Promise((resolve) => {
1334
- let items = fieldFor(settingsDump(inst) || {});
1335
- let cursor = 0;
1336
- let mode = "browse"; // browse | add | confirm
1337
- let input = "";
1338
- let notice = "";
1339
- let rendered = 0;
1340
- let done = false;
1341
- const previousRaw = !!process.stdin.isRaw;
1342
- const isMouseEvent = makeMouseFilter();
1343
-
1344
- const refresh = () => {
1345
- items = fieldFor(settingsDump(inst) || {});
1346
- if (cursor >= items.length) cursor = Math.max(0, items.length - 1);
1347
- };
1348
-
1349
- const erase = () => {
1350
- if (!rendered) return;
1351
- process.stdout.write(`\x1b[${rendered}A` + ANSI.clearDown);
1352
- rendered = 0;
1353
- };
1354
-
1355
- const buildLines = () => {
1356
- const out = [];
1357
- out.push(``);
1358
- out.push(` ${BOLD}${label}${RESET}`);
1359
- out.push(` ${DIM}────────────────────────────────────────${RESET}`);
1360
- if (items.length === 0) {
1361
- out.push(` ${DIM}(none)${RESET}`);
1362
- } else {
1363
- items.forEach((it, i) => {
1364
- const sel = i === cursor;
1365
- out.push(` ${sel ? CYAN + "❯" + RESET : " "} ${sel ? BOLD + it + RESET : it}`);
1366
- });
1367
- }
1368
- out.push(``);
1369
- if (notice) out.push(` ${notice}`);
1370
- if (mode === "add") {
1371
- out.push(` ${CYAN}add ${noun}:${RESET} ${input}${CYAN}▌${RESET} ${DIM}enter save · esc cancel${RESET}`);
1372
- } else if (mode === "confirm") {
1373
- out.push(` ${YELLOW}remove ${items[cursor]}?${RESET} ${DIM}y / N${RESET}`);
1374
- } else {
1375
- out.push(` ${DIM}↑↓ nav · ${RESET}${BOLD}a${RESET}${DIM} add · ${RESET}${BOLD}d${RESET}${DIM} delete · esc back${RESET}`);
1376
- }
1377
- return out;
1378
- };
1379
-
1380
- const render = () => {
1381
- erase();
1382
- const lines = buildLines();
1383
- rendered = lines.length;
1384
- process.stdout.write(lines.map((l) => l + ANSI.clearLine).join("\n") + "\n");
1385
- };
1386
-
1387
- const cleanup = () => {
1388
- if (done) return;
1389
- done = true;
1390
- process.stdin.removeListener("keypress", onKey);
1391
- process.stdout.write(ANSI.mouseOff);
1392
- if (process.stdin.setRawMode) process.stdin.setRawMode(previousRaw);
1393
- if (!previousRaw) process.stdin.pause();
1394
- erase();
1395
- };
1396
-
1397
- // Run a verb (spawnSync blocks briefly — people ops hit GitHub). Surface the
1398
- // last stdout/stderr line as a notice; refresh the list from disk after.
1399
- const runVerb = (args) => {
1400
- const r = runSettings(inst, args);
1401
- const msg = ((r.stdout || "") + " " + (r.stderr || "")).trim().split("\n").map((s) => s.trim()).filter(Boolean).pop() || "";
1402
- notice = r && r.status === 0 ? msg : `${YELLOW}${msg}${RESET}`;
1403
- refresh();
1404
- };
1405
-
1406
- function onKey(chunk, key = {}) {
1407
- if (isMouseEvent(chunk, key)) return;
1408
- if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
1409
-
1410
- if (mode === "add") {
1411
- if (key.name === "escape") { mode = "browse"; input = ""; render(); return; }
1412
- if (key.name === "return") {
1413
- const v = input.trim().split(/\s+/)[0];
1414
- mode = "browse"; input = "";
1415
- if (v) runVerb([domain, "add", v]);
1416
- render();
1417
- return;
1418
- }
1419
- if (key.name === "backspace") { input = input.slice(0, -1); render(); return; }
1420
- if (chunk && chunk.length === 1 && chunk >= " " && !key.ctrl && !key.meta) { input += chunk; render(); return; }
1421
- return;
1422
- }
1423
-
1424
- if (mode === "confirm") {
1425
- if (chunk === "y" || chunk === "Y") {
1426
- const victim = items[cursor];
1427
- mode = "browse";
1428
- if (victim) runVerb([domain, "remove", victim]);
1429
- } else { mode = "browse"; }
1430
- render();
1431
- return;
1432
- }
1433
-
1434
- // browse
1435
- if (key.name === "escape" || key.name === "left") { cleanup(); resolve(); return; }
1436
- if (key.name === "up" || key.name === "k") { if (items.length) cursor = (cursor - 1 + items.length) % items.length; render(); return; }
1437
- if (key.name === "down" || key.name === "j") { if (items.length) cursor = (cursor + 1) % items.length; render(); return; }
1438
- if (chunk === "a") { mode = "add"; input = ""; notice = ""; render(); return; }
1439
- if (chunk === "d") { if (items.length) { mode = "confirm"; render(); } return; }
1440
- }
1441
-
1442
- readline.emitKeypressEvents(process.stdin);
1443
- if (process.stdin.setRawMode) process.stdin.setRawMode(true);
1444
- process.stdin.resume();
1445
- process.stdout.write(ANSI.mouseOff);
1446
- process.stdin.on("keypress", onKey);
1447
- render();
1448
- });
1449
- }
1450
-
1451
- // The `s`-key settings screen: a raw-mode menu (hosting toggle + repo/admin
1452
- // editors) over bin/settings.sh. Modeled on pickRuntimeInteractive.
1453
- function settingsScreen(inst) {
1454
- const title = inst.slug || inst.name || path.basename(inst.path);
1455
-
1456
- if (!fs.existsSync(settingsScriptPath(inst))) {
1457
- console.log(`\n ${YELLOW}Settings unavailable${RESET} — ${BOLD}${title}${RESET} has no ${DIM}bin/settings.sh${RESET}.`);
1458
- console.log(` Update it: open the egregore and run ${CYAN}/pull${RESET}.\n`);
1459
- return Promise.resolve();
1460
- }
1461
-
1462
- let data = settingsDump(inst) || { hosting: true, repos: [], admins: [], people: [] };
1463
-
1464
- const rows = [
1465
- {
1466
- label: () => `Hosted Artifacts`,
1467
- value: () => (data.hosting ? `${GREEN}ON${RESET}` : `${YELLOW}OFF${RESET}`),
1468
- detailLines: () => [data.hosting
1469
- ? ` ${DIM}Enter toggles. When OFF, artifacts never upload to egregore.xyz.${RESET}`
1470
- : ` ${DIM}Artifacts stay local. /view still renders them on localhost.${RESET}`],
1471
- },
1472
- {
1473
- label: () => `Managed repos`,
1474
- value: () => `${DIM}${data.repos.length}${RESET}`,
1475
- detailLines: () => data.repos.length
1476
- ? data.repos.map((r) => ` ${DIM}·${RESET} ${r.name || r}`)
1477
- : [` ${DIM}(none)${RESET}`],
1478
- },
1479
- {
1480
- label: () => `Admins`,
1481
- value: () => `${DIM}${data.admins.length}${RESET}`,
1482
- detailLines: () => data.admins.length
1483
- ? [` ${DIM}${data.admins.join(", ")}${RESET}`]
1484
- : [` ${DIM}(none)${RESET}`],
1485
- },
1486
- {
1487
- label: () => `People`,
1488
- value: () => `${DIM}${(data.people || []).length}${RESET}`,
1489
- detailLines: () => (data.people && data.people.length)
1490
- ? [` ${DIM}${data.people.join(", ")}${RESET}`]
1491
- : [` ${DIM}(none)${RESET}`],
1492
- },
1493
- ];
1494
-
1495
- return new Promise((resolve) => {
1496
- let cursor = 0;
1497
- let rendered = 0;
1498
- let done = false;
1499
- let notice = "";
1500
- const previousRaw = !!process.stdin.isRaw;
1501
- const isMouseEvent = makeMouseFilter();
1502
-
1503
- const erase = () => {
1504
- if (!rendered) return;
1505
- // Move to the TOP of the last-rendered block, then wipe everything below
1506
- // in one shot. Clearing line-by-line leaves the cursor at the BOTTOM, so
1507
- // the next redraw starts a row lower each time — that's the drift that
1508
- // pushes blank "margins" on top. Up-N + clearDown returns to the top and
1509
- // clears cleanly, so every render repaints in place.
1510
- process.stdout.write(`\x1b[${rendered}A` + ANSI.clearDown);
1511
- rendered = 0;
1512
- };
1513
-
1514
- const buildLines = () => {
1515
- const out = [];
1516
- out.push(``);
1517
- out.push(` ${BOLD}Settings${RESET} ${DIM}·${RESET} ${BOLD}${title}${RESET}`);
1518
- out.push(` ${DIM}────────────────────────────────────────${RESET}`);
1519
- rows.forEach((row, i) => {
1520
- const sel = i === cursor;
1521
- const marker = sel ? `${CYAN}❯${RESET}` : ` `;
1522
- const name = sel ? `${BOLD}${row.label()}${RESET}` : row.label();
1523
- out.push(` ${marker} ${name} ${row.value()}`);
1524
- if (sel) for (const dl of row.detailLines()) out.push(dl);
1525
- });
1526
- out.push(``);
1527
- if (notice) out.push(` ${notice}`);
1528
- out.push(` ${DIM}↑↓ nav · enter ${cursor === 0 ? "toggle" : "edit"} · esc back · q quit${RESET}`);
1529
- return out;
1530
- };
1531
-
1532
- const render = () => {
1533
- erase();
1534
- const lines = buildLines();
1535
- rendered = lines.length;
1536
- process.stdout.write(lines.map((l) => l + ANSI.clearLine).join("\n") + "\n");
1537
- };
1538
-
1539
- const teardownRaw = () => {
1540
- process.stdin.removeListener("keypress", onKey);
1541
- process.stdout.write(ANSI.mouseOff);
1542
- if (process.stdin.setRawMode) process.stdin.setRawMode(previousRaw);
1543
- if (!previousRaw) process.stdin.pause();
1544
- };
1545
-
1546
- const setupRaw = () => {
1547
- readline.emitKeypressEvents(process.stdin);
1548
- if (process.stdin.setRawMode) process.stdin.setRawMode(true);
1549
- process.stdin.resume();
1550
- process.stdout.write(ANSI.mouseOff);
1551
- process.stdin.on("keypress", onKey);
1552
- };
1553
-
1554
- const cleanup = () => {
1555
- if (done) return;
1556
- done = true;
1557
- teardownRaw();
1558
- erase();
1559
- };
1560
-
1561
- const toggleHosting = () => {
1562
- const r = runSettings(inst, ["hosting", data.hosting ? "off" : "on"]);
1563
- if (!r || r.status !== 0) {
1564
- notice = `${YELLOW}${((r && r.stderr) || "could not change hosting").trim().split("\n")[0]}${RESET}`;
1565
- } else {
1566
- data = settingsDump(inst) || data;
1567
- notice = `hosting ${data.hosting ? `${GREEN}ON${RESET}` : `${YELLOW}OFF${RESET}`}`;
1568
- }
1569
- render();
1570
- };
1571
-
1572
- // Hand off to the nested raw-mode list editor, then repaint this screen with
1573
- // refreshed counts when the user backs out.
1574
- const openList = (domain, label) => {
1575
- erase();
1576
- teardownRaw();
1577
- listEditor(inst, domain, label).then(() => {
1578
- data = settingsDump(inst) || data;
1579
- notice = "";
1580
- setupRaw();
1581
- render();
1582
- });
1583
- };
1584
-
1585
- function onKey(chunk, key = {}) {
1586
- if (isMouseEvent(chunk, key)) return;
1587
- if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
1588
- if (key.name === "q") { cleanup(); resolve(); return; }
1589
- // Esc / ← = back (esc never quits).
1590
- if (key.name === "escape" || key.name === "left") { cleanup(); resolve(); return; }
1591
- if (key.name === "up" || key.name === "k") { cursor = (cursor - 1 + rows.length) % rows.length; render(); return; }
1592
- if (key.name === "down" || key.name === "j") { cursor = (cursor + 1) % rows.length; render(); return; }
1593
- if (key.name === "return" || chunk === " ") {
1594
- if (cursor === 0) toggleHosting();
1595
- else if (cursor === 1) openList("repo", "Managed repos");
1596
- else if (cursor === 2) openList("admin", "Admins");
1597
- else if (cursor === 3) openList("people", "People");
1598
- }
1599
- }
1600
-
1601
- setupRaw();
1602
- render();
1603
- });
1604
- }
1605
-
1606
1324
  async function cmdSettings(args) {
1607
1325
  const registry = readRegistry();
1608
1326
  if (registry.length === 0) die("no egregores installed yet.\n run: npx -y create-egregore@latest");
@@ -1614,11 +1332,9 @@ async function cmdSettings(args) {
1614
1332
  if (!inst) {
1615
1333
  die(`multiple egregores installed; choose one:\n egregore settings <slug> [hosting|repo|admin]\n run: egregore ls`);
1616
1334
  }
1617
- // No verb + a TTY → interactive screen. Otherwise forward straight to the verb.
1618
- if (rest.length === 0) {
1619
- if (isTTY && canPrompt()) return settingsScreen(inst);
1620
- rest = ["dump"];
1621
- }
1335
+ // Interactive editing lives in the launcher (press `s`). With no verb, print
1336
+ // the snapshot; otherwise forward straight to the deterministic verb.
1337
+ if (rest.length === 0) rest = ["dump"];
1622
1338
  const r = runSettings(inst, rest);
1623
1339
  if (r.stdout) process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : r.stdout + "\n");
1624
1340
  if (r.stderr) process.stderr.write(r.stderr);
@@ -1845,6 +1561,78 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
1845
1561
  return lines;
1846
1562
  }
1847
1563
 
1564
+ // ── settings panel (rendered in-place inside the browser) ───────────
1565
+
1566
+ function settingsItemsFor(data, domain) {
1567
+ if (!data) return [];
1568
+ if (domain === "repo") return (data.repos || []).map((r) => r.name || r);
1569
+ if (domain === "admin") return data.admins || [];
1570
+ return data.people || [];
1571
+ }
1572
+
1573
+ const SETTINGS_DOMAIN_LABEL = { repo: "Managed repos", admin: "Admins", people: "People" };
1574
+ const SETTINGS_DOMAIN_NOUN = { repo: "repo name", admin: "github handle", people: "github handle" };
1575
+
1576
+ // Same box geometry as renderRuntimePanel so pressing `s` keeps the frame put.
1577
+ function renderSettingsPanel({ inst, data, cursor, view, domain, items, listCursor, mode, input, notice, width }) {
1578
+ const title = inst.slug || inst.name || path.basename(inst.path);
1579
+ const outerWidth = Math.min(Math.max(50, width - 2), 126);
1580
+ const innerWidth = Math.max(48, outerWidth - 2);
1581
+ const margin = " ".repeat(Math.max(0, Math.floor((width - outerWidth) / 2)));
1582
+ const label = " settings ";
1583
+ const d = data || {};
1584
+ const lines = [
1585
+ `${margin}${DIM}┌${"─".repeat(2)}${label}${"─".repeat(Math.max(0, innerWidth - label.length - 2))}┐${RESET}`,
1586
+ emptyBoxLine(innerWidth, margin),
1587
+ boxLine(`${BOLD}${CYAN}${title}${RESET}`, innerWidth, margin),
1588
+ boxLine(`${DIM}${inst.path}${RESET}`, innerWidth, margin),
1589
+ emptyBoxLine(innerWidth, margin),
1590
+ ];
1591
+
1592
+ if (view === "list") {
1593
+ lines.push(boxLine(`${BOLD}${SETTINGS_DOMAIN_LABEL[domain] || domain}${RESET}`, innerWidth, margin));
1594
+ lines.push(emptyBoxLine(innerWidth, margin));
1595
+ if (!items || !items.length) {
1596
+ lines.push(boxLine(`${DIM}(none)${RESET}`, innerWidth, margin));
1597
+ } else {
1598
+ items.forEach((it, i) => {
1599
+ const sel = i === listCursor;
1600
+ const marker = sel ? `${CYAN}❯${RESET}` : " ";
1601
+ lines.push(boxLine(`${marker} ${sel ? `${BOLD}${it}${RESET}` : it}`, innerWidth, margin));
1602
+ });
1603
+ }
1604
+ lines.push(emptyBoxLine(innerWidth, margin));
1605
+ if (notice) lines.push(boxLine(notice, innerWidth, margin));
1606
+ if (mode === "add") {
1607
+ lines.push(boxLine(`${CYAN}add ${SETTINGS_DOMAIN_NOUN[domain]}:${RESET} ${input}${CYAN}▌${RESET} ${DIM}enter save · esc cancel${RESET}`, innerWidth, margin));
1608
+ } else if (mode === "confirm") {
1609
+ lines.push(boxLine(`${YELLOW}remove ${items[listCursor]}?${RESET} ${DIM}y / N${RESET}`, innerWidth, margin));
1610
+ } else {
1611
+ lines.push(boxLine(`${DIM}↑↓ nav · ${RESET}${BOLD}a${RESET}${DIM} add · ${RESET}${BOLD}d${RESET}${DIM} delete · ← back · q quit${RESET}`, innerWidth, margin));
1612
+ }
1613
+ } else {
1614
+ const rows = [
1615
+ { name: "Hosted Artifacts", value: (d.hosting !== false) ? `${GREEN}ON${RESET}` : `${YELLOW}OFF${RESET}` },
1616
+ { name: "Managed repos", value: `${DIM}${(d.repos || []).length}${RESET}` },
1617
+ { name: "Admins", value: `${DIM}${(d.admins || []).length}${RESET}` },
1618
+ { name: "People", value: `${DIM}${(d.people || []).length}${RESET}` },
1619
+ ];
1620
+ rows.forEach((row, i) => {
1621
+ const sel = i === cursor;
1622
+ const marker = sel ? `${CYAN}❯${RESET}` : " ";
1623
+ const name = sel ? `${BOLD}${row.name}${RESET}` : row.name;
1624
+ lines.push(boxLine(`${marker} ${pad(name, 20)} ${row.value}`, innerWidth, margin));
1625
+ });
1626
+ lines.push(emptyBoxLine(innerWidth, margin));
1627
+ if (notice) lines.push(boxLine(notice, innerWidth, margin));
1628
+ lines.push(boxLine(`${DIM}↑↓ nav · ${RESET}${BOLD}enter${RESET}${DIM} ${cursor === 0 ? "toggle" : "open"} · ← back · q quit${RESET}`, innerWidth, margin));
1629
+ }
1630
+
1631
+ lines.push(emptyBoxLine(innerWidth, margin));
1632
+ lines.push(`${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`);
1633
+ return lines;
1634
+ }
1635
+
1848
1636
  // ── browser (full-screen TUI) ───────────────────────────────────────
1849
1637
 
1850
1638
  async function browser(args = {}) {
@@ -1877,6 +1665,18 @@ async function browser(args = {}) {
1877
1665
  inviteInst: null,
1878
1666
  runtimeInst: null,
1879
1667
  runtimeCursor: 0,
1668
+ // Settings panel — rendered in-place like the harness picker (renderSettingsPanel),
1669
+ // never a separate screen. Active while settingsInst is set.
1670
+ settingsInst: null,
1671
+ settingsData: null,
1672
+ settingsCursor: 0,
1673
+ settingsView: "menu", // "menu" | "list"
1674
+ settingsDomain: null, // "repo" | "admin" | "people"
1675
+ settingsItems: [],
1676
+ settingsListCursor: 0,
1677
+ settingsMode: "browse", // "browse" | "add" | "confirm"
1678
+ settingsInput: "",
1679
+ settingsNotice: "",
1880
1680
  cursor: initialCursorForCwd(registry),
1881
1681
  showHelp: false,
1882
1682
  modal: null, // { type: "remove" | "sweep", onYes, onNo }
@@ -1949,6 +1749,27 @@ async function browser(args = {}) {
1949
1749
  return;
1950
1750
  }
1951
1751
 
1752
+ if (state.settingsInst) {
1753
+ const settingsLines = renderSettingsPanel({
1754
+ inst: state.settingsInst,
1755
+ data: state.settingsData,
1756
+ cursor: state.settingsCursor,
1757
+ view: state.settingsView,
1758
+ domain: state.settingsDomain,
1759
+ items: state.settingsItems,
1760
+ listCursor: state.settingsListCursor,
1761
+ mode: state.settingsMode,
1762
+ input: state.settingsInput,
1763
+ notice: state.settingsNotice,
1764
+ width: w,
1765
+ });
1766
+ for (const line of settingsLines) out.push(line + ANSI.clearLine + "\n");
1767
+ const remainingRows = Math.max(0, h - settingsLines.length - 1);
1768
+ for (let i = 0; i < remainingRows; i++) out.push(ANSI.clearLine + "\n");
1769
+ process.stdout.write(out.join(""));
1770
+ return;
1771
+ }
1772
+
1952
1773
  // Header / hero panel
1953
1774
  const def = readDefault();
1954
1775
  const v = visible();
@@ -2202,10 +2023,78 @@ async function browser(args = {}) {
2202
2023
  };
2203
2024
 
2204
2025
  return new Promise((resolve) => {
2026
+ // Run a deterministic settings verb, refresh the panel's data + list in place.
2027
+ const settingsRunVerb = (verbArgs) => {
2028
+ const r = runSettings(state.settingsInst, verbArgs);
2029
+ const msg = ((r.stdout || "") + " " + (r.stderr || "")).trim().split("\n").map((s) => s.trim()).filter(Boolean).pop() || "";
2030
+ state.settingsNotice = r && r.status === 0 ? msg : `${YELLOW}${msg}${RESET}`;
2031
+ state.settingsData = settingsDump(state.settingsInst) || state.settingsData;
2032
+ if (state.settingsView === "list" && state.settingsDomain) {
2033
+ state.settingsItems = settingsItemsFor(state.settingsData, state.settingsDomain);
2034
+ if (state.settingsListCursor >= state.settingsItems.length) state.settingsListCursor = Math.max(0, state.settingsItems.length - 1);
2035
+ }
2036
+ };
2037
+
2205
2038
  process.stdin.on("keypress", (chunk, key) => {
2206
2039
  if (!key) return;
2207
2040
  if (isMouseEvent(chunk, key)) return; // drop scroll-wheel / mouse escape sequences
2208
2041
 
2042
+ if (state.settingsInst) {
2043
+ const S = state;
2044
+ if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
2045
+ if (key.name === "q") { cleanup(); resolve(); return; }
2046
+
2047
+ if (S.settingsView === "list") {
2048
+ if (S.settingsMode === "add") {
2049
+ if (key.name === "escape") { S.settingsMode = "browse"; S.settingsInput = ""; render(); return; }
2050
+ if (key.name === "return") {
2051
+ const v = S.settingsInput.trim().split(/\s+/)[0];
2052
+ S.settingsMode = "browse"; S.settingsInput = "";
2053
+ if (v) settingsRunVerb([S.settingsDomain, "add", v]);
2054
+ render(); return;
2055
+ }
2056
+ if (key.name === "backspace") { S.settingsInput = S.settingsInput.slice(0, -1); render(); return; }
2057
+ if (chunk && chunk.length === 1 && chunk >= " " && !key.ctrl && !key.meta) { S.settingsInput += chunk; render(); return; }
2058
+ return;
2059
+ }
2060
+ if (S.settingsMode === "confirm") {
2061
+ if (chunk === "y" || chunk === "Y") {
2062
+ const victim = S.settingsItems[S.settingsListCursor];
2063
+ S.settingsMode = "browse";
2064
+ if (victim) settingsRunVerb([S.settingsDomain, "remove", victim]);
2065
+ } else { S.settingsMode = "browse"; }
2066
+ render(); return;
2067
+ }
2068
+ // list browse
2069
+ if (key.name === "escape" || key.name === "left") { S.settingsView = "menu"; S.settingsNotice = ""; render(); return; }
2070
+ if ((key.name === "up" || key.name === "k") && S.settingsItems.length) { S.settingsListCursor = (S.settingsListCursor - 1 + S.settingsItems.length) % S.settingsItems.length; render(); return; }
2071
+ if ((key.name === "down" || key.name === "j") && S.settingsItems.length) { S.settingsListCursor = (S.settingsListCursor + 1) % S.settingsItems.length; render(); return; }
2072
+ if (chunk === "a") { S.settingsMode = "add"; S.settingsInput = ""; S.settingsNotice = ""; render(); return; }
2073
+ if (chunk === "d" && S.settingsItems.length) { S.settingsMode = "confirm"; render(); return; }
2074
+ return;
2075
+ }
2076
+
2077
+ // menu view
2078
+ if (key.name === "escape" || key.name === "left") { S.settingsInst = null; S.settingsNotice = ""; render(); return; }
2079
+ if (key.name === "up" || key.name === "k") { S.settingsCursor = (S.settingsCursor - 1 + 4) % 4; render(); return; }
2080
+ if (key.name === "down" || key.name === "j") { S.settingsCursor = (S.settingsCursor + 1) % 4; render(); return; }
2081
+ if (key.name === "return" || chunk === " ") {
2082
+ if (S.settingsCursor === 0) {
2083
+ const on = (S.settingsData && S.settingsData.hosting) !== false;
2084
+ settingsRunVerb(["hosting", on ? "off" : "on"]);
2085
+ render();
2086
+ } else {
2087
+ const domain = S.settingsCursor === 1 ? "repo" : S.settingsCursor === 2 ? "admin" : "people";
2088
+ S.settingsView = "list"; S.settingsDomain = domain;
2089
+ S.settingsItems = settingsItemsFor(S.settingsData, domain);
2090
+ S.settingsListCursor = 0; S.settingsMode = "browse"; S.settingsNotice = "";
2091
+ render();
2092
+ }
2093
+ return;
2094
+ }
2095
+ return;
2096
+ }
2097
+
2209
2098
  if (state.runtimeInst) {
2210
2099
  const runtimes = runtimeChoicesFor(state.runtimeInst, args.runtimeOverride);
2211
2100
  if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
@@ -2433,8 +2322,16 @@ async function browser(args = {}) {
2433
2322
  if (chunk === "s") {
2434
2323
  const pick = visible()[state.cursor]?.e;
2435
2324
  if (!pick) return;
2436
- cleanup();
2437
- resolve(settingsScreen(pick));
2325
+ if (!fs.existsSync(settingsScriptPath(pick))) {
2326
+ flashMessage(`${YELLOW}settings need bin/settings.sh — run /pull in this egregore${RESET}`, "warn");
2327
+ return;
2328
+ }
2329
+ state.settingsInst = pick;
2330
+ state.settingsData = settingsDump(pick) || { hosting: true, repos: [], admins: [], people: [] };
2331
+ state.settingsCursor = 0;
2332
+ state.settingsView = "menu";
2333
+ state.settingsNotice = "";
2334
+ render();
2438
2335
  return;
2439
2336
  }
2440
2337
  if (chunk === "u") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.19.6",
3
+ "version": "0.19.8",
4
4
  "description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
5
5
  "license": "MIT",
6
6
  "bin": {