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.
- package/assets/egregore-launcher.js +42 -5
- package/bin/cli.js +7 -1
- package/lib/local.js +65 -36
- package/lib/ui.js +30 -12
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.5.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
335
|
-
|
|
336
|
-
|
|
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