create-egregore 0.18.8 → 0.18.9

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.5.23
2
+ // egregore-launcher-version: 0.5.24
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -30,7 +30,7 @@ const https = require("node:https");
30
30
  const { spawnSync, spawn } = require("node:child_process");
31
31
  const readline = require("node:readline");
32
32
 
33
- const VERSION = "0.5.23";
33
+ const VERSION = "0.5.24";
34
34
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
35
35
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
36
36
  const PUBLIC_NPX_PACKAGE = "create-egregore";
@@ -1429,7 +1429,9 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
1429
1429
  const runtimes = runtimeChoicesFor(inst);
1430
1430
  const stored = storedRuntimeFor(inst);
1431
1431
  const title = inst.slug || inst.name || path.basename(inst.path);
1432
- const outerWidth = Math.min(Math.max(50, width - 2), 112);
1432
+ // Same cap/margin as renderHeroPanel so the centered box keeps its position
1433
+ // when you move from the instance list to the assistant picker.
1434
+ const outerWidth = Math.min(Math.max(50, width - 2), 126);
1433
1435
  const innerWidth = Math.max(48, outerWidth - 2);
1434
1436
  const margin = " ".repeat(Math.max(0, Math.floor((width - outerWidth) / 2)));
1435
1437
  const label = " assistant ";
@@ -1521,9 +1523,17 @@ async function browser(args = {}) {
1521
1523
 
1522
1524
  // ── render ────────────────────────────
1523
1525
  const render = () => {
1526
+ if (done) return; // the parent stays alive under the spawned assistant — never redraw after teardown
1524
1527
  const w = process.stdout.columns || 80;
1525
1528
  const h = process.stdout.rows || 24;
1526
1529
 
1530
+ // One centered block: the hero/runtime box, the list, dividers, preview and
1531
+ // footer all share this gutter and width so the whole UI centers together
1532
+ // (matches renderHeroPanel's outerWidth/margin math).
1533
+ const blockWidth = Math.min(Math.max(50, w - 2), 126);
1534
+ const gutter = Math.max(0, Math.floor((w - blockWidth) / 2));
1535
+ const M = " ".repeat(gutter);
1536
+
1527
1537
  const out = [];
1528
1538
  // Home AND clear-to-end-of-screen each frame. The per-line ANSI.clearLine
1529
1539
  // calls below are belt-and-suspenders; some terminals (notably Warp) don't
@@ -1575,9 +1585,9 @@ async function browser(args = {}) {
1575
1585
  const listRows = Math.max(4, Math.floor(availableRows * 0.48));
1576
1586
  const previewRows = Math.max(3, availableRows - listRows - 2); // -2 for dividers
1577
1587
 
1578
- // List
1588
+ // List — kept within the centered block so long paths don't spill past it.
1579
1589
  const listPad = 4;
1580
- const listAvail = w - listPad * 2;
1590
+ const listAvail = blockWidth - 4;
1581
1591
  const slugW = Math.min(22, Math.max(6, ...registry.map((r) => (r.slug || "").length)));
1582
1592
  const nameW = Math.min(24, Math.max(6, ...registry.map((r) => (r.name || "").length)));
1583
1593
 
@@ -1594,7 +1604,7 @@ async function browser(args = {}) {
1594
1604
  if (idx >= v.length) {
1595
1605
  if (v.length === 0 && i === Math.floor(listRows / 2)) {
1596
1606
  const msg = state.filter ? "(no matches)" : "(empty)";
1597
- out.push(" ".repeat(listPad) + DIM + msg + RESET + ANSI.clearLine + "\n");
1607
+ out.push(M + " ".repeat(listPad) + DIM + msg + RESET + ANSI.clearLine + "\n");
1598
1608
  } else {
1599
1609
  out.push(ANSI.clearLine + "\n");
1600
1610
  }
@@ -1608,7 +1618,7 @@ async function browser(args = {}) {
1608
1618
  const label = isCursor ? `${BOLD}${item.label}${RESET}` : item.label;
1609
1619
  const desc = `${DIM}${item.description}${RESET}`;
1610
1620
  const line = ` ${cursorMark} ${pad(plus, slugW)} ${pad(label, nameW)} ${truncate(desc, Math.max(0, listAvail - slugW - nameW - 10))}`;
1611
- out.push(" ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
1621
+ out.push(M + " ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
1612
1622
  continue;
1613
1623
  }
1614
1624
  const { e, origIdx } = item;
@@ -1623,11 +1633,11 @@ async function browser(args = {}) {
1623
1633
  const name = pad(e.name || "", nameW);
1624
1634
  const pathDim = `${DIM}${truncate(e.path, Math.max(0, listAvail - slugW - nameW - 10))}${RESET}`;
1625
1635
  const line = ` ${cursorMark} ${defMark} ${slug} ${name} ${pathDim}`;
1626
- out.push(" ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
1636
+ out.push(M + " ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
1627
1637
  }
1628
1638
 
1629
1639
  // Divider
1630
- out.push(DIM + " " + "─".repeat(w - 4) + RESET + ANSI.clearLine + "\n");
1640
+ out.push(DIM + M + "─".repeat(blockWidth) + RESET + ANSI.clearLine + "\n");
1631
1641
 
1632
1642
  // Preview
1633
1643
  const previewLines = [];
@@ -1641,15 +1651,20 @@ async function browser(args = {}) {
1641
1651
  previewLines.push(`${DIM} (no selection)${RESET}`);
1642
1652
  } else {
1643
1653
  const label = (k) => `${DIM}${pad(k, 10)}${RESET}`;
1644
- previewLines.push(` ${label("path")} ${selected.path}`);
1645
- if (pv.org) previewLines.push(` ${label("org")} ${pv.org} ${DIM}(${pv.githubOrg || "?"})${RESET}`);
1654
+ // A preview line is " " + label(10) + " " + value = 13 + value; bound the
1655
+ // value to blockWidth-15 so every line's right edge lands two cols inside
1656
+ // the box border, level with the box inner content (gutter+blockWidth-2).
1657
+ const valW = Math.max(16, blockWidth - 15);
1658
+ previewLines.push(` ${label("path")} ${truncate(selected.path, valW)}`);
1659
+ if (pv.org) previewLines.push(` ${label("org")} ${truncate(`${pv.org} ${DIM}(${pv.githubOrg || "?"})${RESET}`, valW)}`);
1646
1660
  if (pv.branch) previewLines.push(` ${label("branch")} ${BOLD}${pv.branch}${RESET}`);
1647
- if (pv.lastCommit) previewLines.push(` ${label("last")} ${DIM}${truncate(pv.lastCommit, w - 18)}${RESET}`);
1661
+ if (pv.lastCommit) previewLines.push(` ${label("last")} ${DIM}${truncate(pv.lastCommit, valW)}${RESET}`);
1648
1662
  if (pv.repos && pv.repos.length) {
1649
- previewLines.push(` ${label("repos")} ${pv.repos.slice(0, 4).join(", ")}${pv.repos.length > 4 ? DIM + ` (+${pv.repos.length - 4} more)` + RESET : ""}`);
1663
+ const reposText = `${pv.repos.slice(0, 4).join(", ")}${pv.repos.length > 4 ? ` (+${pv.repos.length - 4} more)` : ""}`;
1664
+ previewLines.push(` ${label("repos")} ${truncate(reposText, valW)}`);
1650
1665
  }
1651
1666
  if (pv.skills) {
1652
- const skillLine = selectedWorkflowLine(pv, Math.max(16, w - 20));
1667
+ const skillLine = selectedWorkflowLine(pv, Math.max(16, blockWidth - 20));
1653
1668
  previewLines.push(` ${label("invoke")} ${skillLine}`);
1654
1669
  previewLines.push(` ${label("skills")} Codex ${BOLD}${pv.skills.native.length}${RESET} native, ${BOLD}${pv.skills.adapterCount}${RESET} adapters; Claude ${BOLD}${pv.skills.claudeCount || 0}${RESET}`);
1655
1670
  }
@@ -1663,11 +1678,11 @@ async function browser(args = {}) {
1663
1678
  }
1664
1679
  for (let i = 0; i < previewRows; i++) {
1665
1680
  const line = previewLines[i] || "";
1666
- out.push(line + ANSI.clearLine + "\n");
1681
+ out.push((line ? M + line : "") + ANSI.clearLine + "\n");
1667
1682
  }
1668
1683
 
1669
1684
  // Divider
1670
- out.push(DIM + " " + "─".repeat(w - 4) + RESET + ANSI.clearLine + "\n");
1685
+ out.push(DIM + M + "─".repeat(blockWidth) + RESET + ANSI.clearLine + "\n");
1671
1686
 
1672
1687
  // Footer
1673
1688
  let footer;
@@ -1688,13 +1703,13 @@ async function browser(args = {}) {
1688
1703
  const kindColor = state.messageKind === "success" ? GREEN : state.messageKind === "warn" ? YELLOW : CYAN;
1689
1704
  footer = ` ${kindColor}●${RESET} ${state.message}${ANSI.clearLine}`;
1690
1705
  } else {
1691
- footer = w < 96
1706
+ footer = blockWidth < 96
1692
1707
  ? ` ${DIM}↑↓${RESET} nav ${DIM}enter${RESET} select ${DIM}n/+${RESET} new ${DIM}i${RESET} invite ${DIM}?${RESET} help ${DIM}q${RESET} quit${ANSI.clearLine}`
1693
1708
  : ` ${DIM}↑↓${RESET} nav ${DIM}enter${RESET} select ${DIM}n/+${RESET} new ${DIM}/${RESET} filter ${DIM}i${RESET} invite ${DIM}u${RESET} default ${DIM}r${RESET} rm ${DIM}p${RESET} prune ${DIM}?${RESET} help ${DIM}q${RESET} quit${ANSI.clearLine}`;
1694
1709
  }
1695
- out.push(footer + "\n");
1710
+ out.push(M + footer + "\n");
1696
1711
  if (state.showHelp) {
1697
- out.push(` ${DIM}● default · ❯ cursor · n/+ starts guided setup · i opens /invite${RESET}${ANSI.clearLine}`);
1712
+ out.push(`${M} ${DIM}● default · ❯ cursor · n/+ starts guided setup · i opens /invite${RESET}${ANSI.clearLine}`);
1698
1713
  } else {
1699
1714
  out.push(ANSI.clearLine);
1700
1715
  }
@@ -1706,12 +1721,30 @@ async function browser(args = {}) {
1706
1721
  process.stdout.write(ANSI.altScreenEnter + ANSI.mouseOff + ANSI.cursorHide + ANSI.clear + ANSI.home);
1707
1722
  const isMouseEvent = makeMouseFilter();
1708
1723
  let done = false;
1724
+ // Named so cleanup() can detach them. The launcher is NOT a true exec — it
1725
+ // stays alive as the parent of the spawned assistant (claude/codex) for the
1726
+ // whole session. The load-bearing detach is the RESIZE listener: Ghostty (and
1727
+ // other terminals) re-send SIGWINCH when a tab regains focus, and a stale
1728
+ // "resize"→render() repaints the launcher frame on top of the running
1729
+ // assistant — exactly the "I switched tabs and the launcher screen came back"
1730
+ // bug. The SIGINT/SIGTERM/exit handlers are dropped too because they're now
1731
+ // pointless: during the live session the child holds the tty in raw mode, so
1732
+ // Ctrl-C reaches it as a 0x03 byte (the parent never sees a SIGINT); and an
1733
+ // out-of-band kill -INT/-TERM exits the parent 130/143 by Node's default
1734
+ // disposition anyway — identical to the explicit handlers we remove.
1735
+ const onResize = () => render(); // render() itself no-ops once done is set
1736
+ let onSigint;
1737
+ let onSigterm;
1709
1738
  const cleanup = () => {
1710
1739
  if (done) return;
1711
1740
  done = true;
1712
1741
  process.stdin.setRawMode(false);
1713
1742
  process.stdin.pause();
1714
1743
  process.stdin.removeAllListeners("keypress");
1744
+ process.stdout.removeListener("resize", onResize);
1745
+ if (onSigint) process.removeListener("SIGINT", onSigint);
1746
+ if (onSigterm) process.removeListener("SIGTERM", onSigterm);
1747
+ process.removeListener("exit", cleanup);
1715
1748
  // Clear the alt buffer BEFORE leaving it. A well-behaved terminal discards
1716
1749
  // the alt buffer and restores the primary screen on \x1b[?1049l, so this is
1717
1750
  // a harmless no-op there. Warp, however, dumps the alt buffer's contents
@@ -1720,10 +1753,12 @@ async function browser(args = {}) {
1720
1753
  // guided "+ new" setup) starts on a clean screen.
1721
1754
  process.stdout.write(ANSI.mouseOff + ANSI.cursorShow + ANSI.clear + ANSI.home + ANSI.altScreenLeave);
1722
1755
  };
1756
+ onSigint = () => { cleanup(); process.exit(130); };
1757
+ onSigterm = () => { cleanup(); process.exit(143); };
1723
1758
  process.on("exit", cleanup);
1724
- process.on("SIGINT", () => { cleanup(); process.exit(130); });
1725
- process.on("SIGTERM", () => { cleanup(); process.exit(143); });
1726
- process.stdout.on("resize", () => render());
1759
+ process.on("SIGINT", onSigint);
1760
+ process.on("SIGTERM", onSigterm);
1761
+ process.stdout.on("resize", onResize);
1727
1762
 
1728
1763
  readline.emitKeypressEvents(process.stdin);
1729
1764
  process.stdin.setRawMode(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.18.8",
3
+ "version": "0.18.9",
4
4
  "description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code or Codex.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "build:runtime": "node scripts/build-runtime-bundle.mjs",
14
- "test": "node scripts/build-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js",
14
+ "test": "node scripts/build-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js",
15
15
  "test:e2e": "node --test test/e2e-local.test.js",
16
16
  "test:all": "node scripts/build-runtime-bundle.mjs --check && node --test test/"
17
17
  },
@@ -36,13 +36,16 @@ EMPTY='{"fields":[],"values":[]}'
36
36
  if [ -z "$API_URL" ] || [ -z "$API_KEY" ]; then
37
37
  GRAPH_REASON="missing_config"
38
38
  else
39
- # Store HTTP status code alongside response body
39
+ # Store HTTP status code alongside response body. curl already emits 000
40
+ # on transport failure; keep the exit status separate so it cannot become
41
+ # 000000 and fall through as an invalid JSON response.
42
+ CURL_STATUS=0
40
43
  HTTP_CODE=$(curl -s -o "$TMPDIR/activity_raw.json" -w "%{http_code}" \
41
44
  "${API_URL}/api/activity/dashboard?github_username=$(printf '%s' "$GH_USER" | jq -sRr @uri)" \
42
45
  -H "Authorization: Bearer $API_KEY" \
43
- --connect-timeout 5 --max-time 15 2>/dev/null || echo "000")
46
+ --connect-timeout 5 --max-time 15 2>/dev/null) || CURL_STATUS=$?
44
47
 
45
- if [ "$HTTP_CODE" = "000" ]; then
48
+ if [ "$CURL_STATUS" -ne 0 ] || [ "$HTTP_CODE" = "000" ] || ! [[ "$HTTP_CODE" =~ ^[0-9][0-9][0-9]$ ]]; then
46
49
  GRAPH_REASON="unreachable"
47
50
  elif [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
48
51
  GRAPH_REASON="auth_error"
@@ -44,12 +44,16 @@ SESSION_ID=$(cat "$HOME/.egregore/session-${PROJ_HASH}.id" 2>/dev/null || echo "
44
44
  if [ -z "$API_URL" ] || [ -z "$API_KEY" ]; then
45
45
  GRAPH_REASON="missing_config"
46
46
  else
47
+ # Keep curl's transport status separate from %{http_code}; otherwise a
48
+ # failed request can concatenate 000 + fallback text and look like an
49
+ # unexpected API response.
50
+ CURL_STATUS=0
47
51
  HTTP_CODE=$(curl -s -o "$TMPDIR/dashboard_raw.json" -w "%{http_code}" \
48
52
  "${API_URL}/api/personal/dashboard?github_username=$(printf '%s' "$GH_USER" | jq -sRr @uri)&time_range=$(printf '%s' "$TIME_RANGE" | jq -sRr @uri)&session_id=$(printf '%s' "$SESSION_ID" | jq -sRr @uri)" \
49
53
  -H "Authorization: Bearer $API_KEY" \
50
- --connect-timeout 5 --max-time 15 2>/dev/null || echo "000")
54
+ --connect-timeout 5 --max-time 15 2>/dev/null) || CURL_STATUS=$?
51
55
 
52
- if [ "$HTTP_CODE" = "000" ]; then
56
+ if [ "$CURL_STATUS" -ne 0 ] || [ "$HTTP_CODE" = "000" ] || ! [[ "$HTTP_CODE" =~ ^[0-9][0-9][0-9]$ ]]; then
53
57
  GRAPH_REASON="unreachable"
54
58
  elif [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
55
59
  GRAPH_REASON="auth_error"