create-egregore 0.18.6 → 0.18.7

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.21
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.21";
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";
@@ -691,8 +691,15 @@ async function launch(inst, runtimeOverride) {
691
691
  }
692
692
 
693
693
  function setupNewEgregore(runtimeOverride) {
694
- const args = ["--yes", `${PUBLIC_NPX_PACKAGE}@latest`, "--open"];
694
+ // The launcher (alt-screen) already served as the entry screen. Pass
695
+ // --no-welcome so the spawned installer skips its own full-screen welcome
696
+ // bookend: a second alt-screen enter right after the launcher's teardown
697
+ // demands a redundant Enter and churns the alt buffer (duplicated footer on
698
+ // some terminals). The ending summary bookend is unaffected.
699
+ const args = ["--yes", `${PUBLIC_NPX_PACKAGE}@latest`, "--open", "--no-welcome"];
695
700
  if (runtimeOverride) args.push("--runtime", runtimeOverride);
701
+ // Bridge the npx-resolve gap so the first Enter never looks like a no-op.
702
+ 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
703
  const child = spawn("npx", args, {
697
704
  stdio: "inherit",
698
705
  cwd: safeCwd(),
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.7",
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": {