create-egregore 0.18.7 → 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.21
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.21";
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";
@@ -99,6 +99,7 @@ const ANSI = {
99
99
  clear: "\x1b[2J",
100
100
  home: "\x1b[H",
101
101
  clearLine: "\x1b[K",
102
+ clearDown: "\x1b[J",
102
103
  // Disable every terminal mouse-reporting mode. Some terminals (tmux,
103
104
  // iTerm2) or a TUI run earlier in the session leave mouse reporting ON,
104
105
  // which turns the scroll wheel into escape sequences that corrupt our
@@ -126,6 +127,23 @@ function makeMouseFilter() {
126
127
  let x11 = 0; // raw bytes left to swallow after ESC [ M
127
128
  return function isMouseEvent(chunk, key) {
128
129
  const seq = (key && key.sequence) || (typeof chunk === "string" ? chunk : "") || "";
130
+ // A real, unshifted navigation/action key is never part of a mouse report
131
+ // — those decompose into "[<"/"[M" plus digit keypresses, never named
132
+ // arrows/enter/etc. If a quirky terminal (e.g. Warp) emitted a malformed or
133
+ // partial mouse sequence, the stuck sgr/x11 state below would otherwise
134
+ // swallow these keys and freeze navigation ("arrows don't work"). Reset and
135
+ // let them through. Digits are deliberately NOT included — they stay
136
+ // filtered mid-sequence so scroll-wheel protection still holds.
137
+ if (key && !key.shift && !key.ctrl && !key.meta) {
138
+ const nm = key.name;
139
+ if (nm === "up" || nm === "down" || nm === "left" || nm === "right" ||
140
+ nm === "return" || nm === "escape" || nm === "home" || nm === "end" ||
141
+ nm === "pageup" || nm === "pagedown") {
142
+ sgr = false;
143
+ x11 = 0;
144
+ return false;
145
+ }
146
+ }
129
147
  if (sgr) {
130
148
  if (/[Mm]/.test(seq)) sgr = false; // SGR terminates on M (press) / m (release)
131
149
  return true;
@@ -1411,7 +1429,9 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
1411
1429
  const runtimes = runtimeChoicesFor(inst);
1412
1430
  const stored = storedRuntimeFor(inst);
1413
1431
  const title = inst.slug || inst.name || path.basename(inst.path);
1414
- 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);
1415
1435
  const innerWidth = Math.max(48, outerWidth - 2);
1416
1436
  const margin = " ".repeat(Math.max(0, Math.floor((width - outerWidth) / 2)));
1417
1437
  const label = " assistant ";
@@ -1503,11 +1523,25 @@ async function browser(args = {}) {
1503
1523
 
1504
1524
  // ── render ────────────────────────────
1505
1525
  const render = () => {
1526
+ if (done) return; // the parent stays alive under the spawned assistant — never redraw after teardown
1506
1527
  const w = process.stdout.columns || 80;
1507
1528
  const h = process.stdout.rows || 24;
1508
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
+
1509
1537
  const out = [];
1510
- out.push(ANSI.home);
1538
+ // Home AND clear-to-end-of-screen each frame. The per-line ANSI.clearLine
1539
+ // calls below are belt-and-suspenders; some terminals (notably Warp) don't
1540
+ // apply incremental \x1b[K reliably across an alt-screen redraw, leaving
1541
+ // stale lines — most visibly a duplicated footer. A single clear-down after
1542
+ // home wipes the slate before redraw. It's part of the same atomic write,
1543
+ // so there's no extra flicker.
1544
+ out.push(ANSI.home + ANSI.clearDown);
1511
1545
 
1512
1546
  if (state.runtimeInst) {
1513
1547
  const runtimeLines = renderRuntimePanel({
@@ -1551,9 +1585,9 @@ async function browser(args = {}) {
1551
1585
  const listRows = Math.max(4, Math.floor(availableRows * 0.48));
1552
1586
  const previewRows = Math.max(3, availableRows - listRows - 2); // -2 for dividers
1553
1587
 
1554
- // List
1588
+ // List — kept within the centered block so long paths don't spill past it.
1555
1589
  const listPad = 4;
1556
- const listAvail = w - listPad * 2;
1590
+ const listAvail = blockWidth - 4;
1557
1591
  const slugW = Math.min(22, Math.max(6, ...registry.map((r) => (r.slug || "").length)));
1558
1592
  const nameW = Math.min(24, Math.max(6, ...registry.map((r) => (r.name || "").length)));
1559
1593
 
@@ -1570,7 +1604,7 @@ async function browser(args = {}) {
1570
1604
  if (idx >= v.length) {
1571
1605
  if (v.length === 0 && i === Math.floor(listRows / 2)) {
1572
1606
  const msg = state.filter ? "(no matches)" : "(empty)";
1573
- out.push(" ".repeat(listPad) + DIM + msg + RESET + ANSI.clearLine + "\n");
1607
+ out.push(M + " ".repeat(listPad) + DIM + msg + RESET + ANSI.clearLine + "\n");
1574
1608
  } else {
1575
1609
  out.push(ANSI.clearLine + "\n");
1576
1610
  }
@@ -1584,7 +1618,7 @@ async function browser(args = {}) {
1584
1618
  const label = isCursor ? `${BOLD}${item.label}${RESET}` : item.label;
1585
1619
  const desc = `${DIM}${item.description}${RESET}`;
1586
1620
  const line = ` ${cursorMark} ${pad(plus, slugW)} ${pad(label, nameW)} ${truncate(desc, Math.max(0, listAvail - slugW - nameW - 10))}`;
1587
- out.push(" ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
1621
+ out.push(M + " ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
1588
1622
  continue;
1589
1623
  }
1590
1624
  const { e, origIdx } = item;
@@ -1599,11 +1633,11 @@ async function browser(args = {}) {
1599
1633
  const name = pad(e.name || "", nameW);
1600
1634
  const pathDim = `${DIM}${truncate(e.path, Math.max(0, listAvail - slugW - nameW - 10))}${RESET}`;
1601
1635
  const line = ` ${cursorMark} ${defMark} ${slug} ${name} ${pathDim}`;
1602
- out.push(" ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
1636
+ out.push(M + " ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
1603
1637
  }
1604
1638
 
1605
1639
  // Divider
1606
- out.push(DIM + " " + "─".repeat(w - 4) + RESET + ANSI.clearLine + "\n");
1640
+ out.push(DIM + M + "─".repeat(blockWidth) + RESET + ANSI.clearLine + "\n");
1607
1641
 
1608
1642
  // Preview
1609
1643
  const previewLines = [];
@@ -1617,15 +1651,20 @@ async function browser(args = {}) {
1617
1651
  previewLines.push(`${DIM} (no selection)${RESET}`);
1618
1652
  } else {
1619
1653
  const label = (k) => `${DIM}${pad(k, 10)}${RESET}`;
1620
- previewLines.push(` ${label("path")} ${selected.path}`);
1621
- 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)}`);
1622
1660
  if (pv.branch) previewLines.push(` ${label("branch")} ${BOLD}${pv.branch}${RESET}`);
1623
- 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}`);
1624
1662
  if (pv.repos && pv.repos.length) {
1625
- 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)}`);
1626
1665
  }
1627
1666
  if (pv.skills) {
1628
- const skillLine = selectedWorkflowLine(pv, Math.max(16, w - 20));
1667
+ const skillLine = selectedWorkflowLine(pv, Math.max(16, blockWidth - 20));
1629
1668
  previewLines.push(` ${label("invoke")} ${skillLine}`);
1630
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}`);
1631
1670
  }
@@ -1639,11 +1678,11 @@ async function browser(args = {}) {
1639
1678
  }
1640
1679
  for (let i = 0; i < previewRows; i++) {
1641
1680
  const line = previewLines[i] || "";
1642
- out.push(line + ANSI.clearLine + "\n");
1681
+ out.push((line ? M + line : "") + ANSI.clearLine + "\n");
1643
1682
  }
1644
1683
 
1645
1684
  // Divider
1646
- out.push(DIM + " " + "─".repeat(w - 4) + RESET + ANSI.clearLine + "\n");
1685
+ out.push(DIM + M + "─".repeat(blockWidth) + RESET + ANSI.clearLine + "\n");
1647
1686
 
1648
1687
  // Footer
1649
1688
  let footer;
@@ -1664,13 +1703,13 @@ async function browser(args = {}) {
1664
1703
  const kindColor = state.messageKind === "success" ? GREEN : state.messageKind === "warn" ? YELLOW : CYAN;
1665
1704
  footer = ` ${kindColor}●${RESET} ${state.message}${ANSI.clearLine}`;
1666
1705
  } else {
1667
- footer = w < 96
1706
+ footer = blockWidth < 96
1668
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}`
1669
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}`;
1670
1709
  }
1671
- out.push(footer + "\n");
1710
+ out.push(M + footer + "\n");
1672
1711
  if (state.showHelp) {
1673
- 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}`);
1674
1713
  } else {
1675
1714
  out.push(ANSI.clearLine);
1676
1715
  }
@@ -1682,18 +1721,44 @@ async function browser(args = {}) {
1682
1721
  process.stdout.write(ANSI.altScreenEnter + ANSI.mouseOff + ANSI.cursorHide + ANSI.clear + ANSI.home);
1683
1722
  const isMouseEvent = makeMouseFilter();
1684
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;
1685
1738
  const cleanup = () => {
1686
1739
  if (done) return;
1687
1740
  done = true;
1688
1741
  process.stdin.setRawMode(false);
1689
1742
  process.stdin.pause();
1690
1743
  process.stdin.removeAllListeners("keypress");
1691
- process.stdout.write(ANSI.mouseOff + ANSI.cursorShow + ANSI.altScreenLeave);
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);
1748
+ // Clear the alt buffer BEFORE leaving it. A well-behaved terminal discards
1749
+ // the alt buffer and restores the primary screen on \x1b[?1049l, so this is
1750
+ // a harmless no-op there. Warp, however, dumps the alt buffer's contents
1751
+ // into the primary scrollback on exit — clearing first means it dumps a
1752
+ // blank screen instead of the whole launcher frame, so the next flow (the
1753
+ // guided "+ new" setup) starts on a clean screen.
1754
+ process.stdout.write(ANSI.mouseOff + ANSI.cursorShow + ANSI.clear + ANSI.home + ANSI.altScreenLeave);
1692
1755
  };
1756
+ onSigint = () => { cleanup(); process.exit(130); };
1757
+ onSigterm = () => { cleanup(); process.exit(143); };
1693
1758
  process.on("exit", cleanup);
1694
- process.on("SIGINT", () => { cleanup(); process.exit(130); });
1695
- process.on("SIGTERM", () => { cleanup(); process.exit(143); });
1696
- process.stdout.on("resize", () => render());
1759
+ process.on("SIGINT", onSigint);
1760
+ process.on("SIGTERM", onSigterm);
1761
+ process.stdout.on("resize", onResize);
1697
1762
 
1698
1763
  readline.emitKeypressEvents(process.stdin);
1699
1764
  process.stdin.setRawMode(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.18.7",
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"