create-egregore 0.18.6 → 0.18.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.5.20
2
+ // egregore-launcher-version: 0.5.23
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.20";
33
+ const VERSION = "0.5.23";
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;
@@ -691,8 +709,15 @@ async function launch(inst, runtimeOverride) {
691
709
  }
692
710
 
693
711
  function setupNewEgregore(runtimeOverride) {
694
- const args = ["--yes", `${PUBLIC_NPX_PACKAGE}@latest`, "--open"];
712
+ // The launcher (alt-screen) already served as the entry screen. Pass
713
+ // --no-welcome so the spawned installer skips its own full-screen welcome
714
+ // bookend: a second alt-screen enter right after the launcher's teardown
715
+ // demands a redundant Enter and churns the alt buffer (duplicated footer on
716
+ // some terminals). The ending summary bookend is unaffected.
717
+ const args = ["--yes", `${PUBLIC_NPX_PACKAGE}@latest`, "--open", "--no-welcome"];
695
718
  if (runtimeOverride) args.push("--runtime", runtimeOverride);
719
+ // Bridge the npx-resolve gap so the first Enter never looks like a no-op.
720
+ process.stdout.write(`\n ${CYAN}❯${RESET} ${BOLD}Setting up a new Egregore…${RESET}\n ${DIM}fetching the latest installer — this can take a few seconds${RESET}\n\n`);
696
721
  const child = spawn("npx", args, {
697
722
  stdio: "inherit",
698
723
  cwd: safeCwd(),
@@ -1500,7 +1525,13 @@ async function browser(args = {}) {
1500
1525
  const h = process.stdout.rows || 24;
1501
1526
 
1502
1527
  const out = [];
1503
- out.push(ANSI.home);
1528
+ // Home AND clear-to-end-of-screen each frame. The per-line ANSI.clearLine
1529
+ // calls below are belt-and-suspenders; some terminals (notably Warp) don't
1530
+ // apply incremental \x1b[K reliably across an alt-screen redraw, leaving
1531
+ // stale lines — most visibly a duplicated footer. A single clear-down after
1532
+ // home wipes the slate before redraw. It's part of the same atomic write,
1533
+ // so there's no extra flicker.
1534
+ out.push(ANSI.home + ANSI.clearDown);
1504
1535
 
1505
1536
  if (state.runtimeInst) {
1506
1537
  const runtimeLines = renderRuntimePanel({
@@ -1681,7 +1712,13 @@ async function browser(args = {}) {
1681
1712
  process.stdin.setRawMode(false);
1682
1713
  process.stdin.pause();
1683
1714
  process.stdin.removeAllListeners("keypress");
1684
- process.stdout.write(ANSI.mouseOff + ANSI.cursorShow + ANSI.altScreenLeave);
1715
+ // Clear the alt buffer BEFORE leaving it. A well-behaved terminal discards
1716
+ // the alt buffer and restores the primary screen on \x1b[?1049l, so this is
1717
+ // a harmless no-op there. Warp, however, dumps the alt buffer's contents
1718
+ // into the primary scrollback on exit — clearing first means it dumps a
1719
+ // blank screen instead of the whole launcher frame, so the next flow (the
1720
+ // guided "+ new" setup) starts on a clean screen.
1721
+ process.stdout.write(ANSI.mouseOff + ANSI.cursorShow + ANSI.clear + ANSI.home + ANSI.altScreenLeave);
1685
1722
  };
1686
1723
  process.on("exit", cleanup);
1687
1724
  process.on("SIGINT", () => { cleanup(); process.exit(130); });
package/bin/cli.js CHANGED
@@ -205,6 +205,12 @@ function parseArgs(argv) {
205
205
  args.noAutolaunch = true;
206
206
  } else if (argv[i] === "--no-screens") {
207
207
  args.noScreens = true;
208
+ } else if (argv[i] === "--no-welcome") {
209
+ // Skip only the full-screen entry welcome bookend (keep the ending
210
+ // summary). Used when re-entered from the launcher's "+ new", which
211
+ // already served as the entry screen — a second alt-screen welcome
212
+ // would otherwise demand a redundant Enter and churn the alt buffer.
213
+ args.noWelcome = true;
208
214
  } else if (argv[i] === "--codex") {
209
215
  args.runtime = "codex";
210
216
  } else if (argv[i] === "--claude" || argv[i] === "--claude-code") {
@@ -459,7 +465,7 @@ async function main() {
459
465
  // or the other — don't double up. `--no-screens` and non-TTY fall back
460
466
  // to the inline banner.
461
467
  const useScreens = !args.noScreens && ui.canShowScreens();
462
- if (useScreens) {
468
+ if (useScreens && !args.noWelcome) {
463
469
  const mode = args.subcommand === "join" ? "join"
464
470
  : args.token ? "token"
465
471
  : args.open ? "founder"
package/lib/local.js CHANGED
@@ -990,42 +990,14 @@ async function localFounderFlow(ui, ctx = {}) {
990
990
  statePath: path.join(egregoreDir, ".egregore-state.json"),
991
991
  });
992
992
 
993
- // 18. Optional invite — picker for org members, text fallback otherwise.
993
+ // 18. Optional invite — looped so the founder can bring in several
994
+ // teammates and then explicitly finish, instead of being dropped into
995
+ // the assistant the instant the first invite lands.
994
996
  ui.section("Bring someone in", "Egregore shines when it's shared");
995
997
 
996
- let invUsername = "";
997
- if (isOrg) {
998
- const memberSpin = ui.spinner("Loading org members...");
999
- const members = await listOrgMembers(githubToken, githubOrg);
1000
- memberSpin.stop(members.length > 0 ? `Loaded ${members.length} members` : "No visible members");
1001
-
1002
- // Filter out the current user — they can't invite themselves.
1003
- const pickable = members
1004
- .filter((m) => m.login !== user.login)
1005
- .slice(0, 20);
1006
-
1007
- if (pickable.length > 0) {
1008
- const choices = [
1009
- { label: "Skip — work solo for now", description: "I can always /invite later" },
1010
- ...pickable.map((m) => ({ label: m.login, description: `@${githubOrg} member` })),
1011
- { label: "Type a username", description: "invite someone not in the list" },
1012
- ];
1013
- const picked = await ui.choose("who should we invite?", choices);
1014
- if (picked.label === "Skip — work solo for now") {
1015
- invUsername = "";
1016
- } else if (picked.label === "Type a username") {
1017
- invUsername = await ui.prompt("GitHub username");
1018
- } else {
1019
- invUsername = picked.label;
1020
- }
1021
- } else {
1022
- invUsername = await ui.prompt("teammate's GitHub username (or Enter to skip)");
1023
- }
1024
- } else {
1025
- invUsername = await ui.prompt("teammate's GitHub username (or Enter to skip)");
1026
- }
1027
-
1028
- if (invUsername) {
998
+ // Perform a single invite. Returns true on success so the caller can track
999
+ // who's been added (and keep them out of the picker on the next round).
1000
+ const inviteOne = async (invUsername) => {
1029
1001
  const invSpin = ui.spinner(`Inviting ${invUsername}...`);
1030
1002
  try {
1031
1003
  // Add as collaborator to all repos
@@ -1071,9 +1043,64 @@ async function localFounderFlow(ui, ctx = {}) {
1071
1043
  shareLines.push(`Telegram: ${egreConfig.telegram_group_link}`);
1072
1044
  }
1073
1045
  ui.card("share", shareLines);
1046
+ return true;
1074
1047
  } catch (err) {
1075
1048
  invSpin.fail(`Could not invite ${invUsername}: ${err.message}`);
1049
+ return false;
1050
+ }
1051
+ };
1052
+
1053
+ // Load org members once (the picker source); text fallback otherwise.
1054
+ let orgMembers = [];
1055
+ if (isOrg) {
1056
+ const memberSpin = ui.spinner("Loading org members...");
1057
+ orgMembers = await listOrgMembers(githubToken, githubOrg);
1058
+ memberSpin.stop(orgMembers.length > 0 ? `Loaded ${orgMembers.length} members` : "No visible members");
1059
+ }
1060
+
1061
+ const invitedUsers = [];
1062
+ // Loop until the founder skips/finishes or runs out of people to invite.
1063
+ for (;;) {
1064
+ const first = invitedUsers.length === 0;
1065
+ let invUsername = "";
1066
+
1067
+ // Members not yet invited and not the founder themselves. No cap — the
1068
+ // picker scrolls and filters, so large orgs stay navigable.
1069
+ const pickable = orgMembers
1070
+ .filter((m) => m.login !== user.login && !invitedUsers.includes(m.login));
1071
+
1072
+ if (isOrg && pickable.length > 0) {
1073
+ const stopChoice = first
1074
+ ? { label: "Skip — work solo for now", description: "I can always /invite later" }
1075
+ : { label: "Done — continue setup", description: `${invitedUsers.length} invited so far` };
1076
+ const choices = [
1077
+ stopChoice,
1078
+ ...pickable.map((m) => ({ label: m.login, description: `@${githubOrg} member` })),
1079
+ { label: "Type a username", description: "invite someone not in the list" },
1080
+ ];
1081
+ const picked = await ui.choose(first ? "who should we invite?" : "invite someone else?", choices);
1082
+ if (picked.label === stopChoice.label) break;
1083
+ if (picked.label === "Type a username") {
1084
+ invUsername = await ui.prompt("GitHub username");
1085
+ } else {
1086
+ invUsername = picked.label;
1087
+ }
1088
+ } else {
1089
+ const prompt = first
1090
+ ? "teammate's GitHub username (or Enter to skip)"
1091
+ : "another teammate's GitHub username (or Enter to finish)";
1092
+ invUsername = await ui.prompt(prompt);
1076
1093
  }
1094
+
1095
+ invUsername = (invUsername || "").trim();
1096
+ if (!invUsername) break;
1097
+ if (invitedUsers.includes(invUsername)) {
1098
+ ui.info(`Already invited ${invUsername}.`);
1099
+ continue;
1100
+ }
1101
+
1102
+ const ok = await inviteOne(invUsername);
1103
+ if (ok) invitedUsers.push(invUsername);
1077
1104
  }
1078
1105
 
1079
1106
  // 18b. Record invite outcome in state so the /onboarding skill knows to
@@ -1083,7 +1110,7 @@ async function localFounderFlow(ui, ctx = {}) {
1083
1110
  try {
1084
1111
  const cur = JSON.parse(fs.readFileSync(statePath, "utf-8"));
1085
1112
  cur.onboarding = cur.onboarding || {};
1086
- cur.onboarding.invite_skipped = !invUsername;
1113
+ cur.onboarding.invite_skipped = invitedUsers.length === 0;
1087
1114
  cur.onboarding.invite_handled_by = "installer";
1088
1115
  cur.onboarding.phase = "complete";
1089
1116
  cur.onboarding.completed_at = cur.onboarding.completed_at || new Date().toISOString();
@@ -1111,7 +1138,9 @@ async function localFounderFlow(ui, ctx = {}) {
1111
1138
  const assistantName = preferredRuntime === "codex" ? "Codex" : "Claude Code";
1112
1139
  const setupSummary = {
1113
1140
  workspace: "created",
1114
- invite: invUsername ? `sent to ${invUsername}` : "skipped",
1141
+ invite: invitedUsers.length
1142
+ ? `sent to ${invitedUsers.join(", ")}`
1143
+ : "skipped",
1115
1144
  telegram: telegramResult.groupLink
1116
1145
  ? "connected"
1117
1146
  : telegramResult.setup
package/lib/ui.js CHANGED
@@ -330,20 +330,38 @@ async function interactiveChoose(question, options, hint) {
330
330
  Math.max(...options.map((o) => o.label.length)),
331
331
  );
332
332
 
333
+ // Scrolling viewport: only ever draw a bounded window of options so a big
334
+ // list (hundreds of org members/repos) doesn't flood the terminal. The
335
+ // window follows the cursor; "↑/↓ N more" mark the hidden remainder, and
336
+ // type-to-filter narrows the full set. Sized to the terminal, clamped.
337
+ const VIEWPORT = Math.max(6, Math.min(14, (process.stdout.rows || 24) - 8));
338
+
339
+ const renderRow = (entry, i) => {
340
+ const { opt } = entry;
341
+ const desc = opt.description ? ` ${DIM}${opt.description}${RESET}` : "";
342
+ if (opt.disabled) {
343
+ return ` ${DIM}· ${pad(opt.label, labelWidth)}${RESET}${desc}`;
344
+ }
345
+ const selected = i === cursor;
346
+ const marker = selected ? `${CYAN}❯${RESET}` : " ";
347
+ const labelPart = selected ? `${BOLD}${CYAN}${pad(opt.label, labelWidth)}${RESET}` : pad(opt.label, labelWidth);
348
+ return ` ${marker} ${labelPart}${desc}`;
349
+ };
350
+
333
351
  const render = (first = false) => {
334
- const lines = visible.map((entry, i) => {
335
- const { opt } = entry;
336
- const desc = opt.description ? ` ${DIM}${opt.description}${RESET}` : "";
337
- if (opt.disabled) {
338
- return ` ${DIM}· ${pad(opt.label, labelWidth)}${RESET}${desc}`;
339
- }
340
- const selected = i === cursor;
341
- const marker = selected ? `${CYAN}❯${RESET}` : " ";
342
- const labelPart = selected ? `${BOLD}${CYAN}${pad(opt.label, labelWidth)}${RESET}` : pad(opt.label, labelWidth);
343
- return ` ${marker} ${labelPart}${desc}`;
344
- });
345
- if (visible.length === 0) {
352
+ const lines = [];
353
+ const total = visible.length;
354
+ if (total === 0) {
346
355
  lines.push(` ${DIM}(no matches)${RESET}`);
356
+ } else if (total <= VIEWPORT) {
357
+ for (let i = 0; i < total; i++) lines.push(renderRow(visible[i], i));
358
+ } else {
359
+ const half = Math.floor(VIEWPORT / 2);
360
+ const start = Math.max(0, Math.min(total - VIEWPORT, cursor - half));
361
+ const end = Math.min(total, start + VIEWPORT);
362
+ if (start > 0) lines.push(` ${DIM}↑ ${start} more${RESET}`);
363
+ for (let i = start; i < end; i++) lines.push(renderRow(visible[i], i));
364
+ if (end < total) lines.push(` ${DIM}↓ ${total - end} more · type to filter${RESET}`);
347
365
  }
348
366
  if (!first) clearLines(lastLineCount);
349
367
  lastLineCount = renderPicker({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.18.6",
3
+ "version": "0.18.8",
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": {