create-egregore 0.17.4 → 0.18.0
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 +1965 -0
- package/assets/egregore-launcher.sh +385 -0
- package/bin/cli.js +472 -81
- package/lib/api.js +2 -2
- package/lib/auth.js +255 -20
- package/lib/codex-runtime.js +268 -0
- package/lib/commands.js +31 -0
- package/lib/context.js +1 -1
- package/lib/local.js +616 -189
- package/lib/setup.js +822 -84
- package/lib/ui.js +780 -84
- package/package.json +11 -3
- package/runtime/codex/.codex/config.toml +15 -0
- package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
- package/runtime/codex/.codex/hooks.json +17 -0
- package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
- package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
- package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
- package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/comment/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
- package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
- package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
- package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
- package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
- package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
- package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
- package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
- package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
- package/runtime/codex/.codex/spec-manifest.json +108 -0
- package/runtime/codex/AGENTS.md +328 -0
- package/runtime/codex/bin/activity-data.sh +180 -0
- package/runtime/codex/bin/agent.sh +669 -0
- package/runtime/codex/bin/codex-install-skills.sh +78 -0
- package/runtime/codex/bin/codex-render-spec.mjs +308 -0
- package/runtime/codex/bin/codex-session-start.sh +238 -0
- package/runtime/codex/bin/codex-skill-render.mjs +315 -0
- package/runtime/codex/bin/codex-sync-skills.sh +223 -0
- package/runtime/codex/bin/dashboard-data.sh +178 -0
- package/runtime/codex/bin/graph-batch.sh +60 -0
- package/runtime/codex/bin/graph-op.sh +397 -0
- package/runtime/codex/bin/graph-wal.sh +228 -0
- package/runtime/codex/bin/graph.sh +141 -0
- package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/codex/bin/handoff-run.sh +569 -0
- package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
- package/runtime/codex/bin/index-handoff.sh +306 -0
- package/runtime/codex/bin/lib/artifact-id.sh +46 -0
- package/runtime/codex/bin/lib/config.sh +71 -0
- package/runtime/codex/bin/lib/context.sh +511 -0
- package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
- package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
- package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
- package/runtime/codex/bin/lib/git-sync.sh +262 -0
- package/runtime/codex/bin/lib/greeting.sh +432 -0
- package/runtime/codex/bin/lib/hash.sh +13 -0
- package/runtime/codex/bin/lib/identity.sh +164 -0
- package/runtime/codex/bin/lib/permissions.sh +106 -0
- package/runtime/codex/bin/lib/time.sh +56 -0
- package/runtime/codex/bin/lib/worktree-links.sh +54 -0
- package/runtime/codex/bin/notify.sh +207 -0
- package/runtime/codex/bin/publish-artifact.sh +187 -0
- package/runtime/codex/bin/publish-references.sh +144 -0
- package/runtime/codex/bin/repo-state.sh +99 -0
- package/runtime/codex/bin/telemetry.sh +350 -0
- package/runtime/codex/bin/worktree-create.sh +115 -0
- package/runtime/codex/bin/worktree-remove.sh +50 -0
- package/runtime/codex/bin/worktree.sh +199 -0
- package/runtime/codex/manifest.json +162 -0
package/lib/ui.js
CHANGED
|
@@ -1,158 +1,854 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal UI — colors, spinner, prompts, banner.
|
|
3
3
|
* Zero dependencies. Node 18+ built-ins only.
|
|
4
|
+
*
|
|
5
|
+
* Respects NO_COLOR (https://no-color.org) and non-TTY output.
|
|
6
|
+
* All public surfaces preserved so existing callers in cli.js /
|
|
7
|
+
* local.js / setup.js continue working.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
const readline = require("node:readline");
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const CYAN = "\x1b[36m";
|
|
13
|
-
const YELLOW = "\x1b[33m";
|
|
14
|
-
const RED = "\x1b[31m";
|
|
15
|
-
const WHITE = "\x1b[37m";
|
|
12
|
+
// ── colors ──────────────────────────────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// Detect once on module load; `process.stdout.isTTY` check keeps pipes
|
|
15
|
+
// and CI clean. Every escape sequence is an empty string when disabled.
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
██╔════╝██╔════╝ ██╔══██╗██╔════╝██╔════╝ ██╔═══██╗██╔══██╗██╔════╝
|
|
20
|
-
█████╗ ██║ ███╗██████╔╝█████╗ ██║ ███╗██║ ██║██████╔╝█████╗
|
|
21
|
-
██╔══╝ ██║ ██║██╔══██╗██╔══╝ ██║ ██║██║ ██║██╔══██╗██╔══╝
|
|
22
|
-
███████╗╚██████╔╝██║ ██║███████╗╚██████╔╝╚██████╔╝██║ ██║███████╗
|
|
23
|
-
╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝${RESET}
|
|
24
|
-
`;
|
|
17
|
+
const COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
18
|
+
const esc = (code) => (COLOR ? `\x1b[${code}m` : "");
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
const RESET = esc(0);
|
|
21
|
+
const BOLD = esc(1);
|
|
22
|
+
const DIM = esc(2);
|
|
23
|
+
const ITALIC = esc(3);
|
|
24
|
+
const UNDER = esc(4);
|
|
25
|
+
const INVERT = esc(7);
|
|
26
|
+
|
|
27
|
+
const BLACK = esc(30);
|
|
28
|
+
const RED = esc(31);
|
|
29
|
+
const GREEN = esc(32);
|
|
30
|
+
const YELLOW = esc(33);
|
|
31
|
+
const BLUE = esc(34);
|
|
32
|
+
const MAGENTA = esc(35);
|
|
33
|
+
const CYAN = esc(36);
|
|
34
|
+
const WHITE = esc(37);
|
|
35
|
+
const GRAY = esc(90);
|
|
36
|
+
|
|
37
|
+
const BG_CYAN = esc(46);
|
|
38
|
+
|
|
39
|
+
// ── layout helpers ──────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const termWidth = () => {
|
|
42
|
+
const w = process.stdout.columns || 80;
|
|
43
|
+
return Math.min(Math.max(w, 40), 100);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const pad = (s, width) => {
|
|
47
|
+
// Visible-length aware: strip escape sequences for width calc
|
|
48
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
49
|
+
return s + " ".repeat(Math.max(0, width - visible.length));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ── banners ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const BANNER_LINES = [
|
|
55
|
+
" ███████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
56
|
+
" ██╔════╝██╔════╝ ██╔══██╗██╔════╝██╔════╝ ██╔═══██╗██╔══██╗██╔════╝",
|
|
57
|
+
" █████╗ ██║ ███╗██████╔╝█████╗ ██║ ███╗██║ ██║██████╔╝█████╗",
|
|
58
|
+
" ██╔══╝ ██║ ██║██╔══██╗██╔══╝ ██║ ██║██║ ██║██╔══██╗██╔══╝",
|
|
59
|
+
" ███████╗╚██████╔╝██║ ██║███████╗╚██████╔╝╚██████╔╝██║ ██║███████╗",
|
|
60
|
+
" ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const SIGIL_LINES = [
|
|
64
|
+
" ◆ ",
|
|
65
|
+
" ╱ ╲ ",
|
|
66
|
+
" ◆───◆ ",
|
|
67
|
+
" ╲ ╱ ",
|
|
68
|
+
" ◆ ",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const COMMAND_GROUPS = [
|
|
72
|
+
{ label: "core", cmds: "/reflect /handoff /quest /todo" },
|
|
73
|
+
{ label: "share", cmds: "/save /push /branch /pr" },
|
|
74
|
+
{ label: "knowledge", cmds: "/add /note /meeting /ingest" },
|
|
75
|
+
{ label: "identity", cmds: "/me /invite /announce" },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function banner(subtitle) {
|
|
79
|
+
process.stdout.write("\n");
|
|
80
|
+
for (const line of BANNER_LINES) {
|
|
81
|
+
process.stdout.write(`${BOLD}${CYAN}${line}${RESET}\n`);
|
|
82
|
+
}
|
|
83
|
+
if (subtitle) {
|
|
84
|
+
process.stdout.write(`\n ${DIM}${subtitle}${RESET}\n`);
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write("\n");
|
|
28
87
|
}
|
|
29
88
|
|
|
89
|
+
function bannerCompact(label) {
|
|
90
|
+
// One-line brand tag for subsequent screens after the big banner.
|
|
91
|
+
process.stdout.write(`\n ${BOLD}${CYAN}egregore${RESET}${DIM} · ${label || "setup"}${RESET}\n\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── structural primitives ───────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function section(title, subtitle) {
|
|
97
|
+
// A divider line + bold title. Use between major phases of a flow.
|
|
98
|
+
const w = termWidth() - 4;
|
|
99
|
+
process.stdout.write(`\n ${DIM}${"─".repeat(w)}${RESET}\n`);
|
|
100
|
+
process.stdout.write(` ${BOLD}${title}${RESET}`);
|
|
101
|
+
if (subtitle) {
|
|
102
|
+
process.stdout.write(` ${DIM}${subtitle}${RESET}`);
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write("\n\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function note(lines) {
|
|
108
|
+
// A soft-highlighted multi-line note. Use for framing text, insights,
|
|
109
|
+
// explanations that deserve more weight than a plain info() but don't
|
|
110
|
+
// need a full box.
|
|
111
|
+
const arr = Array.isArray(lines) ? lines : [lines];
|
|
112
|
+
process.stdout.write("\n");
|
|
113
|
+
for (const line of arr) {
|
|
114
|
+
if (line === "") {
|
|
115
|
+
process.stdout.write(` ${CYAN}│${RESET}\n`);
|
|
116
|
+
} else {
|
|
117
|
+
process.stdout.write(` ${CYAN}│${RESET} ${line}\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function card(title, body) {
|
|
124
|
+
// A bordered card for standout content (e.g. an invite command to share).
|
|
125
|
+
const bodyLines = Array.isArray(body) ? body : [body];
|
|
126
|
+
const contentWidth = Math.max(
|
|
127
|
+
title.length,
|
|
128
|
+
...bodyLines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, "").length),
|
|
129
|
+
);
|
|
130
|
+
const boxW = contentWidth + 4;
|
|
131
|
+
|
|
132
|
+
process.stdout.write("\n");
|
|
133
|
+
process.stdout.write(` ${DIM}┌─ ${title} ${"─".repeat(Math.max(0, boxW - title.length - 4))}┐${RESET}\n`);
|
|
134
|
+
process.stdout.write(` ${DIM}│${" ".repeat(boxW)}│${RESET}\n`);
|
|
135
|
+
for (const line of bodyLines) {
|
|
136
|
+
const visLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
137
|
+
const padded = line + " ".repeat(Math.max(0, boxW - visLen - 2));
|
|
138
|
+
process.stdout.write(` ${DIM}│${RESET} ${BOLD}${padded}${RESET}${DIM}│${RESET}\n`);
|
|
139
|
+
}
|
|
140
|
+
process.stdout.write(` ${DIM}│${" ".repeat(boxW)}│${RESET}\n`);
|
|
141
|
+
process.stdout.write(` ${DIM}└${"─".repeat(boxW)}┘${RESET}\n\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── messages ────────────────────────────────────────────────────────
|
|
145
|
+
|
|
30
146
|
function info(msg) {
|
|
31
|
-
|
|
147
|
+
process.stdout.write(` ${msg}\n`);
|
|
32
148
|
}
|
|
33
149
|
|
|
34
150
|
function success(msg) {
|
|
35
|
-
|
|
151
|
+
process.stdout.write(` ${GREEN}✓${RESET} ${msg}\n`);
|
|
36
152
|
}
|
|
37
153
|
|
|
38
154
|
function warn(msg) {
|
|
39
|
-
|
|
155
|
+
process.stdout.write(` ${YELLOW}!${RESET} ${msg}\n`);
|
|
40
156
|
}
|
|
41
157
|
|
|
42
158
|
function error(msg) {
|
|
43
|
-
|
|
159
|
+
process.stderr.write(` ${RED}✗${RESET} ${msg}\n`);
|
|
44
160
|
}
|
|
45
161
|
|
|
46
162
|
function step(n, total, msg) {
|
|
47
|
-
|
|
163
|
+
process.stdout.write(`\n ${DIM}[${n}/${total}]${RESET} ${msg}\n`);
|
|
48
164
|
}
|
|
49
165
|
|
|
50
|
-
|
|
51
|
-
return `${DIM}${msg}${RESET}`;
|
|
52
|
-
}
|
|
166
|
+
// ── inline formatters ──────────────────────────────────────────────
|
|
53
167
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
168
|
+
const dim = (m) => `${DIM}${m}${RESET}`;
|
|
169
|
+
const bold = (m) => `${BOLD}${m}${RESET}`;
|
|
170
|
+
const cyan = (m) => `${CYAN}${m}${RESET}`;
|
|
171
|
+
const green = (m) => `${GREEN}${m}${RESET}`;
|
|
172
|
+
const yellow = (m) => `${YELLOW}${m}${RESET}`;
|
|
173
|
+
const red = (m) => `${RED}${m}${RESET}`;
|
|
57
174
|
|
|
58
|
-
|
|
59
|
-
return `${CYAN}${msg}${RESET}`;
|
|
60
|
-
}
|
|
175
|
+
// ── spinner ─────────────────────────────────────────────────────────
|
|
61
176
|
|
|
62
|
-
// Simple spinner
|
|
63
177
|
function spinner(msg) {
|
|
64
178
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
65
179
|
let i = 0;
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
180
|
+
const tty = process.stdout.isTTY;
|
|
181
|
+
let interval = null;
|
|
182
|
+
if (tty) {
|
|
183
|
+
interval = setInterval(() => {
|
|
184
|
+
process.stdout.write(`\r ${CYAN}${frames[i++ % frames.length]}${RESET} ${msg}`);
|
|
185
|
+
}, 80);
|
|
186
|
+
} else {
|
|
187
|
+
// Non-TTY: just print once, don't animate
|
|
188
|
+
process.stdout.write(` … ${msg}\n`);
|
|
189
|
+
}
|
|
69
190
|
return {
|
|
70
191
|
stop(result) {
|
|
71
|
-
clearInterval(interval);
|
|
72
|
-
process.stdout.write(`\r\x1b[2K
|
|
192
|
+
if (interval) clearInterval(interval);
|
|
193
|
+
if (tty) process.stdout.write(`\r\x1b[2K`);
|
|
194
|
+
process.stdout.write(` ${GREEN}✓${RESET} ${result || msg}\n`);
|
|
73
195
|
},
|
|
74
196
|
fail(result) {
|
|
75
|
-
clearInterval(interval);
|
|
76
|
-
process.stdout.write(`\r\x1b[2K
|
|
197
|
+
if (interval) clearInterval(interval);
|
|
198
|
+
if (tty) process.stdout.write(`\r\x1b[2K`);
|
|
199
|
+
process.stdout.write(` ${RED}✗${RESET} ${result || msg}\n`);
|
|
200
|
+
},
|
|
201
|
+
// Stop animating and erase the line without printing a result symbol —
|
|
202
|
+
// for when the caller prints its own success/failure line next.
|
|
203
|
+
clear() {
|
|
204
|
+
if (interval) clearInterval(interval);
|
|
205
|
+
if (tty) process.stdout.write(`\r\x1b[2K`);
|
|
77
206
|
},
|
|
78
207
|
};
|
|
79
208
|
}
|
|
80
209
|
|
|
81
|
-
//
|
|
82
|
-
|
|
210
|
+
// ── prompts ─────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
function prompt(question, { default: defaultValue } = {}) {
|
|
83
213
|
return new Promise((resolve) => {
|
|
84
214
|
const rl = readline.createInterface({
|
|
85
215
|
input: process.stdin,
|
|
86
216
|
output: process.stdout,
|
|
87
217
|
});
|
|
88
|
-
|
|
218
|
+
const label = defaultValue
|
|
219
|
+
? `${CYAN}?${RESET} ${question} ${DIM}[${defaultValue}]${RESET} `
|
|
220
|
+
: `${CYAN}?${RESET} ${question} `;
|
|
221
|
+
rl.question(` ${label}`, (answer) => {
|
|
89
222
|
rl.close();
|
|
90
|
-
|
|
223
|
+
const trimmed = answer.trim();
|
|
224
|
+
resolve(trimmed || (defaultValue ?? ""));
|
|
91
225
|
});
|
|
92
226
|
});
|
|
93
227
|
}
|
|
94
228
|
|
|
95
|
-
|
|
229
|
+
async function confirm(question, defaultYes = false) {
|
|
230
|
+
const hint = defaultYes ? `${DIM}[Y/n]${RESET}` : `${DIM}[y/N]${RESET}`;
|
|
231
|
+
const answer = await prompt(`${question} ${hint}`);
|
|
232
|
+
if (!answer) return defaultYes;
|
|
233
|
+
return /^y(es)?$/i.test(answer);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── interactive pickers ─────────────────────────────────────────────
|
|
237
|
+
//
|
|
238
|
+
// Arrow-key navigation + type-to-filter + Enter to select + Esc to cancel.
|
|
239
|
+
// Uses Node readline raw mode; no external dependencies.
|
|
240
|
+
//
|
|
241
|
+
// Falls back to numbered input when stdin/stdout aren't TTYs (CI, pipes,
|
|
242
|
+
// non-interactive shells).
|
|
243
|
+
|
|
244
|
+
function canInteract() {
|
|
245
|
+
return process.stdin.isTTY && process.stdout.isTTY && !process.env.EGREGORE_NO_INTERACTIVE;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Visible length (strips ANSI escape sequences) for padding math.
|
|
249
|
+
const visibleLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
250
|
+
|
|
251
|
+
function visualRows(line, width = process.stdout.columns || 80) {
|
|
252
|
+
return Math.max(1, Math.ceil(visibleLen(line) / Math.max(1, width)));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Render a picker frame and return { lines: number } so the next frame can
|
|
256
|
+
// rewind and clear. Shared by choose + multiSelect.
|
|
257
|
+
function renderPicker({ question, filterText, lines, help }) {
|
|
258
|
+
const out = [];
|
|
259
|
+
const filterLabel = filterText
|
|
260
|
+
? ` ${DIM}filter:${RESET} ${filterText}${CYAN}▌${RESET}`
|
|
261
|
+
: "";
|
|
262
|
+
out.push(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET}${filterLabel ? " " + filterLabel : ""}`);
|
|
263
|
+
out.push("");
|
|
264
|
+
for (const line of lines) out.push(line);
|
|
265
|
+
if (help) {
|
|
266
|
+
out.push("");
|
|
267
|
+
out.push(` ${DIM}${help}${RESET}`);
|
|
268
|
+
}
|
|
269
|
+
const text = out.join("\n") + "\n";
|
|
270
|
+
process.stdout.write(text);
|
|
271
|
+
const width = process.stdout.columns || 80;
|
|
272
|
+
return out.reduce((sum, line) => sum + visualRows(line, width), 0);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function clearLines(n) {
|
|
276
|
+
if (n <= 0) return;
|
|
277
|
+
// Move up n lines and clear from cursor to end of screen.
|
|
278
|
+
process.stdout.write(`\x1b[${n}A\x1b[0J`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function filterOptions(options, filterText, getText) {
|
|
282
|
+
if (!filterText) return options.map((o, i) => ({ opt: o, origIdx: i }));
|
|
283
|
+
const q = filterText.toLowerCase();
|
|
284
|
+
const result = [];
|
|
285
|
+
for (let i = 0; i < options.length; i++) {
|
|
286
|
+
const text = getText(options[i]).toLowerCase();
|
|
287
|
+
if (text.includes(q)) result.push({ opt: options[i], origIdx: i });
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── choose: interactive single-select picker ────────────────────────
|
|
293
|
+
//
|
|
294
|
+
// options: [{ label, description }]
|
|
295
|
+
// returns: the picked option (same object reference as in options[])
|
|
296
|
+
// keys:
|
|
297
|
+
// ↑ ↓ navigate
|
|
298
|
+
// enter select
|
|
299
|
+
// type filter (case-insensitive substring match on label + desc)
|
|
300
|
+
// backspace pop filter
|
|
301
|
+
// esc cancel (returns the first option, or throws if opts.abortOnEsc)
|
|
302
|
+
// ctrl-c abort process
|
|
303
|
+
|
|
96
304
|
async function choose(question, options, hint) {
|
|
97
|
-
|
|
305
|
+
if (!canInteract()) return legacyChoose(question, options, hint);
|
|
306
|
+
return interactiveChoose(question, options, hint);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function interactiveChoose(question, options, hint) {
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
let cursor = 0;
|
|
312
|
+
let filterText = "";
|
|
313
|
+
let visible = filterOptions(options, filterText, (o) => `${o.label} ${o.description || ""}`);
|
|
314
|
+
let lastLineCount = 0;
|
|
315
|
+
|
|
316
|
+
const labelWidth = Math.min(
|
|
317
|
+
40,
|
|
318
|
+
Math.max(...options.map((o) => o.label.length)),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const render = (first = false) => {
|
|
322
|
+
const lines = visible.map((entry, i) => {
|
|
323
|
+
const { opt } = entry;
|
|
324
|
+
const selected = i === cursor;
|
|
325
|
+
const marker = selected ? `${CYAN}❯${RESET}` : " ";
|
|
326
|
+
const labelPart = selected ? `${BOLD}${CYAN}${pad(opt.label, labelWidth)}${RESET}` : pad(opt.label, labelWidth);
|
|
327
|
+
const desc = opt.description ? ` ${DIM}${opt.description}${RESET}` : "";
|
|
328
|
+
return ` ${marker} ${labelPart}${desc}`;
|
|
329
|
+
});
|
|
330
|
+
if (visible.length === 0) {
|
|
331
|
+
lines.push(` ${DIM}(no matches)${RESET}`);
|
|
332
|
+
}
|
|
333
|
+
if (!first) clearLines(lastLineCount);
|
|
334
|
+
lastLineCount = renderPicker({
|
|
335
|
+
question,
|
|
336
|
+
filterText,
|
|
337
|
+
lines,
|
|
338
|
+
help: hint || `${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · type to filter · ${DIM}esc${RESET} cancel`,
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const cleanup = () => {
|
|
343
|
+
process.stdin.setRawMode(false);
|
|
344
|
+
process.stdin.pause();
|
|
345
|
+
process.stdin.removeListener("keypress", onKey);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const finish = (value) => {
|
|
349
|
+
cleanup();
|
|
350
|
+
clearLines(lastLineCount);
|
|
351
|
+
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET} ${DIM}→${RESET} ${BOLD}${value.label}${RESET}\n`);
|
|
352
|
+
resolve(value);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const onKey = (_str, key) => {
|
|
356
|
+
if (!key) return;
|
|
357
|
+
if ((key.ctrl && key.name === "c") || key.name === "escape") {
|
|
358
|
+
cleanup();
|
|
359
|
+
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
360
|
+
process.exit(130);
|
|
361
|
+
}
|
|
362
|
+
if (key.name === "return") {
|
|
363
|
+
if (visible.length > 0) finish(visible[cursor].opt);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (key.name === "up" || (key.ctrl && key.name === "p")) {
|
|
367
|
+
cursor = (cursor - 1 + visible.length) % Math.max(1, visible.length);
|
|
368
|
+
render();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (key.name === "down" || (key.ctrl && key.name === "n")) {
|
|
372
|
+
cursor = (cursor + 1) % Math.max(1, visible.length);
|
|
373
|
+
render();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (key.name === "backspace") {
|
|
377
|
+
filterText = filterText.slice(0, -1);
|
|
378
|
+
visible = filterOptions(options, filterText, (o) => `${o.label} ${o.description || ""}`);
|
|
379
|
+
cursor = 0;
|
|
380
|
+
render();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Printable character → append to filter
|
|
384
|
+
if (_str && _str.length === 1 && !key.ctrl && !key.meta && _str.charCodeAt(0) >= 32) {
|
|
385
|
+
filterText += _str;
|
|
386
|
+
visible = filterOptions(options, filterText, (o) => `${o.label} ${o.description || ""}`);
|
|
387
|
+
cursor = 0;
|
|
388
|
+
render();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
readline.emitKeypressEvents(process.stdin);
|
|
393
|
+
process.stdin.setRawMode(true);
|
|
394
|
+
process.stdin.resume();
|
|
395
|
+
process.stdin.on("keypress", onKey);
|
|
396
|
+
render(true);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function legacyChoose(question, options, hint) {
|
|
401
|
+
process.stdout.write(`\n ${CYAN}◆${RESET} ${BOLD}${question}${RESET}\n\n`);
|
|
402
|
+
const labelWidth = Math.min(40, Math.max(...options.map((o) => o.label.length)));
|
|
98
403
|
for (let i = 0; i < options.length; i++) {
|
|
99
404
|
const { label, description } = options[i];
|
|
100
|
-
|
|
405
|
+
const num = String(i + 1).padStart(2, " ");
|
|
406
|
+
const paddedLabel = pad(label, labelWidth);
|
|
407
|
+
const desc = description ? ` ${DIM}— ${description}${RESET}` : "";
|
|
408
|
+
process.stdout.write(` ${DIM}${num})${RESET} ${BOLD}${paddedLabel}${RESET}${desc}\n`);
|
|
101
409
|
}
|
|
102
|
-
if (hint)
|
|
103
|
-
console.log();
|
|
104
|
-
|
|
410
|
+
if (hint) process.stdout.write(`\n ${DIM}${hint}${RESET}\n`);
|
|
105
411
|
while (true) {
|
|
106
|
-
const answer = await prompt(`
|
|
412
|
+
const answer = await prompt(`pick ${DIM}[1-${options.length}]${RESET}:`);
|
|
413
|
+
if (!answer) { warn(`Enter a number between 1 and ${options.length}.`); continue; }
|
|
107
414
|
const n = parseInt(answer, 10);
|
|
108
|
-
if (n >= 1 && n <= options.length)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
warn(`
|
|
415
|
+
if (n >= 1 && n <= options.length) return options[n - 1];
|
|
416
|
+
const labelMatch = options.find((o) => o.label.toLowerCase() === answer.toLowerCase());
|
|
417
|
+
if (labelMatch) return labelMatch;
|
|
418
|
+
warn(`Not a valid option. Enter 1-${options.length} or a label.`);
|
|
112
419
|
}
|
|
113
420
|
}
|
|
114
421
|
|
|
115
|
-
//
|
|
422
|
+
// ── multiSelect: interactive multi-select picker ────────────────────
|
|
423
|
+
//
|
|
424
|
+
// options: [{ name, language?, description? }]
|
|
425
|
+
// returns: array of picked names
|
|
426
|
+
// keys:
|
|
427
|
+
// ↑ ↓ navigate
|
|
428
|
+
// space toggle current
|
|
429
|
+
// a toggle all (respecting filter)
|
|
430
|
+
// enter confirm
|
|
431
|
+
// type filter
|
|
432
|
+
// esc confirm with current selection
|
|
433
|
+
|
|
116
434
|
async function multiSelect(question, options) {
|
|
117
435
|
if (options.length === 0) return [];
|
|
118
|
-
|
|
436
|
+
if (!canInteract()) return legacyMultiSelect(question, options);
|
|
437
|
+
return interactiveMultiSelect(question, options);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function interactiveMultiSelect(question, options) {
|
|
441
|
+
return new Promise((resolve) => {
|
|
442
|
+
let cursor = 0;
|
|
443
|
+
let filterText = "";
|
|
444
|
+
const picked = new Set();
|
|
445
|
+
let visible = filterOptions(options, filterText, (o) => `${o.name} ${o.description || ""} ${o.language || ""}`);
|
|
446
|
+
let lastLineCount = 0;
|
|
447
|
+
|
|
448
|
+
const nameWidth = Math.min(36, Math.max(...options.map((o) => (o.name || "").length)));
|
|
449
|
+
|
|
450
|
+
const render = (first = false) => {
|
|
451
|
+
const lines = visible.map((entry, i) => {
|
|
452
|
+
const { opt } = entry;
|
|
453
|
+
const isCursor = i === cursor;
|
|
454
|
+
const isPicked = picked.has(opt.name);
|
|
455
|
+
const cursorMark = isCursor ? `${CYAN}❯${RESET}` : " ";
|
|
456
|
+
const checkbox = isPicked ? `${GREEN}[✓]${RESET}` : `${DIM}[ ]${RESET}`;
|
|
457
|
+
const name = isCursor ? `${BOLD}${CYAN}${pad(opt.name, nameWidth)}${RESET}` : pad(opt.name, nameWidth);
|
|
458
|
+
const lang = opt.language ? ` ${DIM}(${opt.language})${RESET}` : "";
|
|
459
|
+
const desc = opt.description ? ` ${DIM}${opt.description}${RESET}` : "";
|
|
460
|
+
return ` ${cursorMark} ${checkbox} ${name}${lang}${desc}`;
|
|
461
|
+
});
|
|
462
|
+
if (visible.length === 0) {
|
|
463
|
+
lines.push(` ${DIM}(no matches)${RESET}`);
|
|
464
|
+
}
|
|
465
|
+
lines.push("");
|
|
466
|
+
lines.push(` ${DIM}${picked.size} selected${RESET}`);
|
|
467
|
+
if (!first) clearLines(lastLineCount);
|
|
468
|
+
lastLineCount = renderPicker({
|
|
469
|
+
question,
|
|
470
|
+
filterText,
|
|
471
|
+
lines,
|
|
472
|
+
help: `${DIM}↑↓${RESET} nav · ${DIM}space${RESET} toggle · ${DIM}a${RESET} all · ${DIM}enter${RESET} confirm · type to filter`,
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const cleanup = () => {
|
|
477
|
+
process.stdin.setRawMode(false);
|
|
478
|
+
process.stdin.pause();
|
|
479
|
+
process.stdin.removeListener("keypress", onKey);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const finish = () => {
|
|
483
|
+
cleanup();
|
|
484
|
+
clearLines(lastLineCount);
|
|
485
|
+
const summary = picked.size === 0 ? `${DIM}none${RESET}` : `${BOLD}${picked.size}${RESET} ${DIM}selected${RESET}`;
|
|
486
|
+
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET} ${DIM}→${RESET} ${summary}\n`);
|
|
487
|
+
resolve([...picked]);
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const toggleCurrent = () => {
|
|
491
|
+
if (visible.length === 0) return;
|
|
492
|
+
const name = visible[cursor].opt.name;
|
|
493
|
+
if (picked.has(name)) picked.delete(name);
|
|
494
|
+
else picked.add(name);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const toggleAll = () => {
|
|
498
|
+
// If every visible option is already picked, unselect all visible; else pick all visible.
|
|
499
|
+
const allPicked = visible.every((v) => picked.has(v.opt.name));
|
|
500
|
+
for (const v of visible) {
|
|
501
|
+
if (allPicked) picked.delete(v.opt.name);
|
|
502
|
+
else picked.add(v.opt.name);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const onKey = (_str, key) => {
|
|
507
|
+
if (!key) return;
|
|
508
|
+
if ((key.ctrl && key.name === "c") || key.name === "escape") {
|
|
509
|
+
cleanup();
|
|
510
|
+
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
511
|
+
process.exit(130);
|
|
512
|
+
}
|
|
513
|
+
if (key.name === "return") return finish();
|
|
514
|
+
if (key.name === "up" || (key.ctrl && key.name === "p")) {
|
|
515
|
+
cursor = (cursor - 1 + visible.length) % Math.max(1, visible.length);
|
|
516
|
+
render();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (key.name === "down" || (key.ctrl && key.name === "n")) {
|
|
520
|
+
cursor = (cursor + 1) % Math.max(1, visible.length);
|
|
521
|
+
render();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (key.name === "space") {
|
|
525
|
+
toggleCurrent();
|
|
526
|
+
render();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (_str === "a" && !filterText) {
|
|
530
|
+
toggleAll();
|
|
531
|
+
render();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (key.name === "backspace") {
|
|
535
|
+
filterText = filterText.slice(0, -1);
|
|
536
|
+
visible = filterOptions(options, filterText, (o) => `${o.name} ${o.description || ""} ${o.language || ""}`);
|
|
537
|
+
cursor = 0;
|
|
538
|
+
render();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (_str && _str.length === 1 && !key.ctrl && !key.meta && _str.charCodeAt(0) >= 32) {
|
|
542
|
+
filterText += _str;
|
|
543
|
+
visible = filterOptions(options, filterText, (o) => `${o.name} ${o.description || ""} ${o.language || ""}`);
|
|
544
|
+
cursor = 0;
|
|
545
|
+
render();
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
readline.emitKeypressEvents(process.stdin);
|
|
550
|
+
process.stdin.setRawMode(true);
|
|
551
|
+
process.stdin.resume();
|
|
552
|
+
process.stdin.on("keypress", onKey);
|
|
553
|
+
render(true);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function legacyMultiSelect(question, options) {
|
|
558
|
+
process.stdout.write(`\n ${CYAN}◆${RESET} ${BOLD}${question}${RESET}\n\n`);
|
|
559
|
+
const nameWidth = Math.min(36, Math.max(...options.map((o) => (o.name || "").length)));
|
|
119
560
|
for (let i = 0; i < options.length; i++) {
|
|
120
561
|
const { name, language, description } = options[i];
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
562
|
+
const num = String(i + 1).padStart(2, " ");
|
|
563
|
+
const paddedName = pad(name || "", nameWidth);
|
|
564
|
+
const lang = language ? ` ${DIM}(${language})${RESET}` : "";
|
|
565
|
+
const desc = description ? ` ${DIM}— ${description}${RESET}` : "";
|
|
566
|
+
process.stdout.write(` ${DIM}${num})${RESET} ${paddedName}${lang}${desc}\n`);
|
|
567
|
+
}
|
|
568
|
+
process.stdout.write(`\n ${DIM}numbers (e.g. 1,3-5), 'all', or Enter to skip${RESET}\n`);
|
|
569
|
+
const answer = await prompt("select:");
|
|
570
|
+
if (!answer || /^none$/i.test(answer)) return [];
|
|
571
|
+
if (/^all$/i.test(answer)) return options.map((o) => o.name);
|
|
572
|
+
const picked = new Set();
|
|
573
|
+
for (const part of answer.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
574
|
+
const range = part.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
575
|
+
if (range) {
|
|
576
|
+
const [, a, b] = range;
|
|
577
|
+
const lo = Math.min(+a, +b);
|
|
578
|
+
const hi = Math.max(+a, +b);
|
|
579
|
+
for (let n = lo; n <= hi; n++) {
|
|
580
|
+
if (n >= 1 && n <= options.length) picked.add(options[n - 1].name);
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
const n = parseInt(part, 10);
|
|
584
|
+
if (n >= 1 && n <= options.length) picked.add(options[n - 1].name);
|
|
585
|
+
}
|
|
124
586
|
}
|
|
125
|
-
|
|
587
|
+
return [...picked];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── commandBox ──────────────────────────────────────────────────────
|
|
591
|
+
//
|
|
592
|
+
// Preserved for back-compat; card() is the newer more-flexible version.
|
|
126
593
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
594
|
+
function commandBox(label, cmd) {
|
|
595
|
+
card(label, cmd);
|
|
596
|
+
}
|
|
130
597
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
598
|
+
// ── bookend screens (alt-screen) ───────────────────────────────────
|
|
599
|
+
//
|
|
600
|
+
// Full-screen welcome + summary moments that bracket the linear install
|
|
601
|
+
// flow. Enter alt-screen, render, wait for ENTER, exit alt-screen. This
|
|
602
|
+
// preserves both the user's prior shell scrollback AND the install
|
|
603
|
+
// transcript — the full-screen moment is the only thing that disappears.
|
|
604
|
+
//
|
|
605
|
+
// Fallback: callers should check canShowScreens() and fall back to inline
|
|
606
|
+
// renderers for non-TTY / CI / --no-screens.
|
|
607
|
+
|
|
608
|
+
const ALT_SCREEN_IN = "\x1b[?1049h";
|
|
609
|
+
const ALT_SCREEN_OUT = "\x1b[?1049l";
|
|
610
|
+
const CURSOR_HIDE = "\x1b[?25l";
|
|
611
|
+
const CURSOR_SHOW = "\x1b[?25h";
|
|
612
|
+
const CLEAR_HOME = "\x1b[2J\x1b[H";
|
|
613
|
+
|
|
614
|
+
function canShowScreens() {
|
|
615
|
+
return !!(process.stdin.isTTY && process.stdout.isTTY && !process.env.EGREGORE_NO_SCREENS);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function termSize() {
|
|
619
|
+
return {
|
|
620
|
+
w: process.stdout.columns || 80,
|
|
621
|
+
h: process.stdout.rows || 24,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Visible length (strips ANSI) for centering math.
|
|
626
|
+
function _visLen(s) {
|
|
627
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Center a string horizontally by prepending spaces.
|
|
631
|
+
function _center(line, w) {
|
|
632
|
+
const pad = Math.max(0, Math.floor((w - _visLen(line)) / 2));
|
|
633
|
+
return " ".repeat(pad) + line;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Pad vertical space to push content toward center-ish.
|
|
637
|
+
function _vpad(n) {
|
|
638
|
+
return Array.from({ length: Math.max(0, n) }, () => "").join("\n");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function _waitForEnter() {
|
|
642
|
+
return new Promise((resolve) => {
|
|
643
|
+
let done = false;
|
|
644
|
+
const onKey = (chunk, key) => {
|
|
645
|
+
if (done) return;
|
|
646
|
+
if (key && key.ctrl && key.name === "c") {
|
|
647
|
+
// Clean exit on ctrl-c — teardown alt-screen first
|
|
648
|
+
process.stdout.write(CURSOR_SHOW + ALT_SCREEN_OUT);
|
|
649
|
+
process.stdin.setRawMode(false);
|
|
650
|
+
process.stdin.pause();
|
|
651
|
+
process.stdin.removeListener("keypress", onKey);
|
|
652
|
+
process.exit(130);
|
|
653
|
+
}
|
|
654
|
+
if (key && (key.name === "return" || key.name === "escape" || key.name === "space")) {
|
|
655
|
+
done = true;
|
|
656
|
+
process.stdin.setRawMode(false);
|
|
657
|
+
process.stdin.pause();
|
|
658
|
+
process.stdin.removeListener("keypress", onKey);
|
|
659
|
+
resolve(key.name);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
readline.emitKeypressEvents(process.stdin);
|
|
663
|
+
process.stdin.setRawMode(true);
|
|
664
|
+
process.stdin.resume();
|
|
665
|
+
process.stdin.on("keypress", onKey);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function _readPkgVersion() {
|
|
670
|
+
try {
|
|
671
|
+
const fs = require("node:fs");
|
|
672
|
+
const path = require("node:path");
|
|
673
|
+
const raw = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8");
|
|
674
|
+
return JSON.parse(raw).version;
|
|
675
|
+
} catch {
|
|
676
|
+
return "?";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function _renderWelcomeBody({ w, h, version }) {
|
|
681
|
+
const panelInner = Math.min(76, Math.max(62, w - 6));
|
|
682
|
+
const panelOuter = panelInner + 2;
|
|
683
|
+
const showBanner = h >= 28 && w >= 74;
|
|
684
|
+
|
|
685
|
+
const lines = [];
|
|
686
|
+
|
|
687
|
+
if (showBanner) {
|
|
688
|
+
for (const l of BANNER_LINES) {
|
|
689
|
+
lines.push(`${BOLD}${CYAN}${l}${RESET}`);
|
|
136
690
|
}
|
|
691
|
+
} else {
|
|
692
|
+
lines.push(`${BOLD}${CYAN}EGREGORE${RESET}`);
|
|
137
693
|
}
|
|
138
|
-
|
|
694
|
+
lines.push("");
|
|
695
|
+
|
|
696
|
+
const leftWidth = 14;
|
|
697
|
+
const gutter = 2;
|
|
698
|
+
const rightWidth = panelInner - leftWidth - gutter - 2;
|
|
699
|
+
|
|
700
|
+
const leftLines = [
|
|
701
|
+
"",
|
|
702
|
+
...SIGIL_LINES.map((l) => `${CYAN}${l}${RESET}`),
|
|
703
|
+
"",
|
|
704
|
+
"",
|
|
705
|
+
`${BOLD}create-egregore${RESET}`,
|
|
706
|
+
`${DIM}v${version}${RESET}`,
|
|
707
|
+
];
|
|
708
|
+
|
|
709
|
+
const rightLines = [];
|
|
710
|
+
rightLines.push(`${BOLD}What you're installing${RESET}`);
|
|
711
|
+
rightLines.push("");
|
|
712
|
+
for (const g of COMMAND_GROUPS) {
|
|
713
|
+
rightLines.push(`${DIM}${pad(g.label, 10)}${RESET} ${g.cmds}`);
|
|
714
|
+
}
|
|
715
|
+
rightLines.push("");
|
|
716
|
+
rightLines.push(`${BOLD}Mode${RESET}`);
|
|
717
|
+
rightLines.push(`${DIM}local-first · memory lives in ./memory/${RESET}`);
|
|
718
|
+
rightLines.push(`${DIM}files are yours · git is the source of truth${RESET}`);
|
|
719
|
+
rightLines.push("");
|
|
720
|
+
rightLines.push(`${BOLD}Next${RESET}`);
|
|
721
|
+
rightLines.push(`${DIM}guided setup → choose an assistant${RESET}`);
|
|
722
|
+
|
|
723
|
+
const panelHeight = Math.max(leftLines.length, rightLines.length);
|
|
724
|
+
while (leftLines.length < panelHeight) leftLines.push("");
|
|
725
|
+
while (rightLines.length < panelHeight) rightLines.push("");
|
|
726
|
+
|
|
727
|
+
const label = " create-egregore ";
|
|
728
|
+
const dashesLeft = 2;
|
|
729
|
+
const dashesRight = Math.max(0, panelInner - label.length - dashesLeft);
|
|
730
|
+
lines.push(`${DIM}┌${"─".repeat(dashesLeft)}${RESET}${DIM}${label}${RESET}${DIM}${"─".repeat(dashesRight)}┐${RESET}`);
|
|
731
|
+
lines.push(`${DIM}│${RESET}${" ".repeat(panelInner)}${DIM}│${RESET}`);
|
|
732
|
+
|
|
733
|
+
for (let i = 0; i < panelHeight; i++) {
|
|
734
|
+
const left = leftLines[i];
|
|
735
|
+
const leftPad = " ".repeat(Math.max(0, leftWidth - visibleLen(left)));
|
|
736
|
+
const right = rightLines[i];
|
|
737
|
+
const rightPad = " ".repeat(Math.max(0, rightWidth - visibleLen(right)));
|
|
738
|
+
const inner = ` ${left}${leftPad}${" ".repeat(gutter)}${right}${rightPad} `;
|
|
739
|
+
const innerPad = " ".repeat(Math.max(0, panelInner - visibleLen(inner)));
|
|
740
|
+
lines.push(`${DIM}│${RESET}${inner}${innerPad}${DIM}│${RESET}`);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
lines.push(`${DIM}│${RESET}${" ".repeat(panelInner)}${DIM}│${RESET}`);
|
|
744
|
+
lines.push(`${DIM}└${"─".repeat(panelInner)}┘${RESET}`);
|
|
745
|
+
|
|
746
|
+
lines.push("");
|
|
747
|
+
lines.push(`${DIM}a shared memory layer for teams using Claude Code or Codex${RESET}`);
|
|
748
|
+
lines.push("");
|
|
749
|
+
lines.push(`${DIM}press ${RESET}${BOLD}↵${RESET}${DIM} to begin · ctrl-c to exit${RESET}`);
|
|
750
|
+
|
|
751
|
+
const margin = Math.max(0, Math.floor((w - panelOuter) / 2));
|
|
752
|
+
const rendered = lines.map((l) => " ".repeat(margin) + l);
|
|
753
|
+
const topPad = Math.max(0, Math.floor((h - lines.length - 1) / 4));
|
|
754
|
+
const bottomPad = Math.max(0, h - lines.length - topPad - 1);
|
|
755
|
+
|
|
756
|
+
return { body: rendered.join("\n"), topPad, bottomPad };
|
|
139
757
|
}
|
|
140
758
|
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
759
|
+
async function welcomeScreen(_opts = {}) {
|
|
760
|
+
const { w, h } = termSize();
|
|
761
|
+
const version = _readPkgVersion();
|
|
762
|
+
const { body, topPad, bottomPad } = _renderWelcomeBody({ w, h, version });
|
|
763
|
+
|
|
764
|
+
process.stdout.write(ALT_SCREEN_IN + CURSOR_HIDE + CLEAR_HOME);
|
|
765
|
+
process.stdout.write(_vpad(topPad));
|
|
766
|
+
process.stdout.write("\n");
|
|
767
|
+
process.stdout.write(body);
|
|
768
|
+
process.stdout.write(_vpad(bottomPad));
|
|
769
|
+
|
|
770
|
+
const result = await _waitForEnter();
|
|
771
|
+
process.stdout.write(CURSOR_SHOW + ALT_SCREEN_OUT);
|
|
772
|
+
if (result === "escape") process.exit(0);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Summary / ready screen — bookend at the end of the install. Shows the
|
|
776
|
+
// workspace that was created, the framing Insight (so the first Claude
|
|
777
|
+
// session can skip re-rendering it), and a "press enter to start your
|
|
778
|
+
// first session" prompt. After enter, alt-screen leaves and the caller
|
|
779
|
+
// execs into `claude start`.
|
|
780
|
+
async function summaryScreen({ orgName, workspaces = [], next = "start your first session", setup = null } = {}) {
|
|
781
|
+
const { w, h } = termSize();
|
|
782
|
+
const lines = [];
|
|
783
|
+
|
|
784
|
+
lines.push(`${BOLD}${GREEN}✓ You're done${RESET}`);
|
|
785
|
+
lines.push("");
|
|
786
|
+
lines.push(`${DIM}Egregore is ready for${RESET} ${BOLD}${orgName}${RESET}`);
|
|
787
|
+
lines.push("");
|
|
788
|
+
|
|
789
|
+
if (setup) {
|
|
790
|
+
const rows = [
|
|
791
|
+
["workspace", setup.workspace || "created"],
|
|
792
|
+
["invite", setup.invite || "skipped"],
|
|
793
|
+
["Telegram", setup.telegram || "skipped"],
|
|
794
|
+
["assistant", setup.assistant || ""],
|
|
795
|
+
["next", setup.command || "egregore"],
|
|
796
|
+
].filter(([, value]) => value);
|
|
797
|
+
lines.push(`${DIM}setup${RESET}`);
|
|
798
|
+
for (const [label, value] of rows) {
|
|
799
|
+
lines.push(` ${GREEN}●${RESET} ${DIM}${pad(label, 10)}${RESET} ${value}`);
|
|
800
|
+
}
|
|
801
|
+
lines.push("");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (workspaces.length) {
|
|
805
|
+
lines.push(`${DIM}workspace${RESET}`);
|
|
806
|
+
for (const ws of workspaces) {
|
|
807
|
+
const label = ws.label || "";
|
|
808
|
+
const desc = ws.description || "";
|
|
809
|
+
lines.push(` ${GREEN}●${RESET} ${CYAN}${label}${RESET} ${DIM}${desc}${RESET}`);
|
|
810
|
+
}
|
|
811
|
+
lines.push("");
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Framing Insight — this is the copy that /onboarding used to render.
|
|
815
|
+
// Showing it here means the first Claude session can skip it entirely.
|
|
816
|
+
lines.push("");
|
|
817
|
+
lines.push(`${DIM}${"─".repeat(Math.min(48, w - 20))}${RESET}`);
|
|
818
|
+
lines.push("");
|
|
819
|
+
lines.push("Every session leaves traces — decisions, patterns, context.");
|
|
820
|
+
lines.push(`${DIM}Egregore makes those traces persistent and shared.${RESET}`);
|
|
821
|
+
lines.push("");
|
|
822
|
+
lines.push("You do your work. When you're done, you hand off what you");
|
|
823
|
+
lines.push(`learned. The next session — yours or someone else's —`);
|
|
824
|
+
lines.push(`${DIM}starts smarter.${RESET}`);
|
|
825
|
+
lines.push("");
|
|
826
|
+
lines.push(`${DIM}${"─".repeat(Math.min(48, w - 20))}${RESET}`);
|
|
827
|
+
lines.push("");
|
|
828
|
+
lines.push("");
|
|
829
|
+
lines.push(`${DIM}press ${RESET}${BOLD}↵${RESET}${DIM} to ${next} · esc to skip launch${RESET}`);
|
|
830
|
+
|
|
831
|
+
const rendered = lines.map((l) => _center(l, w));
|
|
832
|
+
const topPad = Math.max(1, Math.floor((h - lines.length - 2) / 4));
|
|
833
|
+
const bottomPad = Math.max(0, h - lines.length - topPad - 1);
|
|
834
|
+
|
|
835
|
+
process.stdout.write(ALT_SCREEN_IN + CURSOR_HIDE + CLEAR_HOME);
|
|
836
|
+
process.stdout.write(_vpad(topPad));
|
|
837
|
+
process.stdout.write("\n");
|
|
838
|
+
process.stdout.write(rendered.join("\n"));
|
|
839
|
+
process.stdout.write(_vpad(bottomPad));
|
|
840
|
+
|
|
841
|
+
const result = await _waitForEnter();
|
|
842
|
+
process.stdout.write(CURSOR_SHOW + ALT_SCREEN_OUT);
|
|
843
|
+
return result; // "return" (launch) | "escape" (skip launch) | "space"
|
|
153
844
|
}
|
|
154
845
|
|
|
155
846
|
module.exports = {
|
|
156
|
-
banner,
|
|
157
|
-
|
|
847
|
+
banner, bannerCompact, section, note, card,
|
|
848
|
+
info, success, warn, error, step,
|
|
849
|
+
dim, bold, cyan, green, yellow, red,
|
|
850
|
+
spinner, prompt, choose, multiSelect, confirm,
|
|
851
|
+
commandBox,
|
|
852
|
+
canShowScreens, welcomeScreen, summaryScreen,
|
|
853
|
+
_renderWelcomeBody, _readPkgVersion,
|
|
158
854
|
};
|