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
|
@@ -0,0 +1,1965 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// egregore-launcher-version: 0.5.17
|
|
3
|
+
//
|
|
4
|
+
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
|
+
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
6
|
+
// Zero dependencies: Node built-ins only.
|
|
7
|
+
//
|
|
8
|
+
// Interactive mode (bare `egregore` with a TTY):
|
|
9
|
+
// full-screen browser with filter, preview, and inline actions.
|
|
10
|
+
//
|
|
11
|
+
// Subcommands:
|
|
12
|
+
// egregore pick an instance, then pick an assistant
|
|
13
|
+
// egregore <slug|name> pick an assistant for the matching instance
|
|
14
|
+
// egregore ls | list list instances (non-interactive table)
|
|
15
|
+
// egregore use <slug> set the default for bare `egregore`
|
|
16
|
+
// egregore /invite <user> invite a GitHub user to an instance
|
|
17
|
+
// egregore prune [--dry-run] remove entries whose path is gone
|
|
18
|
+
// egregore rm <slug|path> remove a specific entry
|
|
19
|
+
// egregore browse force the full-screen browser
|
|
20
|
+
// egregore help | --help
|
|
21
|
+
// egregore --version
|
|
22
|
+
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
const fs = require("node:fs");
|
|
26
|
+
const path = require("node:path");
|
|
27
|
+
const os = require("node:os");
|
|
28
|
+
const http = require("node:http");
|
|
29
|
+
const https = require("node:https");
|
|
30
|
+
const { spawnSync, spawn } = require("node:child_process");
|
|
31
|
+
const readline = require("node:readline");
|
|
32
|
+
|
|
33
|
+
const VERSION = "0.5.17";
|
|
34
|
+
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
35
|
+
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
36
|
+
const PUBLIC_NPX_PACKAGE = "create-egregore";
|
|
37
|
+
|
|
38
|
+
const NATIVE_CODEX_SKILLS = [
|
|
39
|
+
"activity",
|
|
40
|
+
"handoff",
|
|
41
|
+
"wrap",
|
|
42
|
+
"announce",
|
|
43
|
+
"harvest",
|
|
44
|
+
"the-spiral",
|
|
45
|
+
"dashboard",
|
|
46
|
+
"deep-reflect",
|
|
47
|
+
"quest",
|
|
48
|
+
"invite",
|
|
49
|
+
"ask",
|
|
50
|
+
"save",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const PRIMARY_CODEX_SKILLS = [
|
|
54
|
+
"activity",
|
|
55
|
+
"dashboard",
|
|
56
|
+
"save",
|
|
57
|
+
"wrap",
|
|
58
|
+
"handoff",
|
|
59
|
+
"ask",
|
|
60
|
+
"invite",
|
|
61
|
+
"quest",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const EGREGORE_ART = [
|
|
65
|
+
"███████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
66
|
+
"██╔════╝██╔════╝ ██╔══██╗██╔════╝██╔════╝ ██╔═══██╗██╔══██╗██╔════╝",
|
|
67
|
+
"█████╗ ██║ ███╗██████╔╝█████╗ ██║ ███╗██║ ██║██████╔╝█████╗",
|
|
68
|
+
"██╔══╝ ██║ ██║██╔══██╗██╔══╝ ██║ ██║██║ ██║██╔══██╗██╔══╝",
|
|
69
|
+
"███████╗╚██████╔╝██║ ██║███████╗╚██████╔╝╚██████╔╝██║ ██║███████╗",
|
|
70
|
+
"╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const EGREGORE_SIGIL = [
|
|
74
|
+
" ◆ ",
|
|
75
|
+
" ╱ ╲ ",
|
|
76
|
+
" ◆───◆ ",
|
|
77
|
+
" ╲ ╱ ",
|
|
78
|
+
" ◆ ",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// ── colors / ansi ───────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const isTTY = process.stdout.isTTY;
|
|
84
|
+
const colorOn = isTTY && !process.env.NO_COLOR;
|
|
85
|
+
const E = (code) => (colorOn ? `\x1b[${code}m` : "");
|
|
86
|
+
const RESET = E(0);
|
|
87
|
+
const BOLD = E(1);
|
|
88
|
+
const DIM = E(2);
|
|
89
|
+
const INV = E(7);
|
|
90
|
+
const BLACK = E(30), RED = E(31), GREEN = E(32), YELLOW = E(33);
|
|
91
|
+
const BLUE = E(34), MAGENTA = E(35), CYAN = E(36), WHITE = E(37);
|
|
92
|
+
const GRAY = E(90);
|
|
93
|
+
|
|
94
|
+
const ANSI = {
|
|
95
|
+
altScreenEnter: "\x1b[?1049h",
|
|
96
|
+
altScreenLeave: "\x1b[?1049l",
|
|
97
|
+
cursorHide: "\x1b[?25l",
|
|
98
|
+
cursorShow: "\x1b[?25h",
|
|
99
|
+
clear: "\x1b[2J",
|
|
100
|
+
home: "\x1b[H",
|
|
101
|
+
clearLine: "\x1b[K",
|
|
102
|
+
moveTo: (row, col) => `\x1b[${row};${col}H`,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
106
|
+
const visLen = (s) => stripAnsi(s).length;
|
|
107
|
+
const pad = (s, n) => s + " ".repeat(Math.max(0, n - visLen(s)));
|
|
108
|
+
const truncate = (s, n) => {
|
|
109
|
+
if (visLen(s) <= n) return s;
|
|
110
|
+
// Naive truncate — assumes no escapes within the char range we're clipping.
|
|
111
|
+
const stripped = stripAnsi(s);
|
|
112
|
+
return stripped.slice(0, n - 1) + "…";
|
|
113
|
+
};
|
|
114
|
+
const canPrompt = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
115
|
+
const skillToken = (name) => `/${name}`;
|
|
116
|
+
const npxCommand = (args = []) => ["npx", "-y", `${PUBLIC_NPX_PACKAGE}@latest`, ...args].filter(Boolean).join(" ");
|
|
117
|
+
const joinCommand = (owner, repo) => npxCommand(["join", repo ? `${owner}/${repo}` : owner]);
|
|
118
|
+
|
|
119
|
+
function joinShort(values, maxWidth) {
|
|
120
|
+
const clean = values.filter(Boolean);
|
|
121
|
+
let out = "";
|
|
122
|
+
let hidden = 0;
|
|
123
|
+
for (let i = 0; i < clean.length; i++) {
|
|
124
|
+
const next = out ? `${out} ${clean[i]}` : clean[i];
|
|
125
|
+
if (visLen(next) <= maxWidth) {
|
|
126
|
+
out = next;
|
|
127
|
+
} else {
|
|
128
|
+
hidden = clean.length - i;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (hidden > 0) {
|
|
133
|
+
const suffix = ` +${hidden}`;
|
|
134
|
+
if (visLen(out + suffix) <= maxWidth) return out + suffix;
|
|
135
|
+
}
|
|
136
|
+
return out || clean[0] || "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function boxLine(content, innerWidth, margin = "") {
|
|
140
|
+
const bodyWidth = Math.max(8, innerWidth - 2);
|
|
141
|
+
return `${margin}${DIM}│${RESET} ${pad(truncate(content, bodyWidth), bodyWidth)} ${DIM}│${RESET}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function emptyBoxLine(innerWidth, margin = "") {
|
|
145
|
+
return boxLine("", innerWidth, margin);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── registry ────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function die(msg) {
|
|
151
|
+
process.stderr.write(`${RED}egregore:${RESET} ${msg}\n`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readRegistry() {
|
|
156
|
+
if (!fs.existsSync(REGISTRY)) return [];
|
|
157
|
+
try {
|
|
158
|
+
const data = JSON.parse(fs.readFileSync(REGISTRY, "utf-8"));
|
|
159
|
+
let entries = [];
|
|
160
|
+
if (Array.isArray(data)) entries = data;
|
|
161
|
+
// Tolerate legacy/alt wrapped shape: { instances: [...] }
|
|
162
|
+
else if (data && Array.isArray(data.instances)) entries = data.instances;
|
|
163
|
+
// Drop entries that don't have a `path` — older Hearth-era schemas
|
|
164
|
+
// (slug/profile/memory) coexist in some users' files and would crash
|
|
165
|
+
// `launch()` with `path: undefined`. Keep entries whose path exists OR
|
|
166
|
+
// doesn't yet exist — `egregore prune` is the canonical way to drop
|
|
167
|
+
// those, so we don't filter by existence here.
|
|
168
|
+
// Dedup by resolved path: some registries accumulated dozens of duplicate
|
|
169
|
+
// rows pointing at the same dir from older installers that didn't dedup
|
|
170
|
+
// on write. Keep the first occurrence of each path.
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const result = [];
|
|
173
|
+
for (const e of entries) {
|
|
174
|
+
if (!e || typeof e.path !== "string" || !e.path) continue;
|
|
175
|
+
const key = path.resolve(e.path);
|
|
176
|
+
if (seen.has(key)) continue;
|
|
177
|
+
seen.add(key);
|
|
178
|
+
result.push(e);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function writeRegistry(entries) {
|
|
187
|
+
const dir = path.dirname(REGISTRY);
|
|
188
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
189
|
+
fs.writeFileSync(REGISTRY, JSON.stringify(entries, null, 2) + "\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function backupRegistry() {
|
|
193
|
+
if (!fs.existsSync(REGISTRY)) return null;
|
|
194
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
195
|
+
const bak = `${REGISTRY}.bak.${ts}`;
|
|
196
|
+
fs.copyFileSync(REGISTRY, bak);
|
|
197
|
+
return bak;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function readDefault() {
|
|
201
|
+
if (!fs.existsSync(DEFAULT_FILE)) return null;
|
|
202
|
+
return fs.readFileSync(DEFAULT_FILE, "utf-8").trim() || null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function writeDefault(slug) {
|
|
206
|
+
const dir = path.dirname(DEFAULT_FILE);
|
|
207
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
208
|
+
fs.writeFileSync(DEFAULT_FILE, `${slug}\n`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resolveBySlug(registry, query) {
|
|
212
|
+
// Exact slug → exact name → case-insensitive substring (slug ∪ name)
|
|
213
|
+
let hit = registry.find((e) => e.slug === query);
|
|
214
|
+
if (hit) return hit;
|
|
215
|
+
hit = registry.find((e) => e.name === query);
|
|
216
|
+
if (hit) return hit;
|
|
217
|
+
const q = query.toLowerCase();
|
|
218
|
+
hit = registry.find(
|
|
219
|
+
(e) => e.slug.toLowerCase().includes(q) || (e.name || "").toLowerCase().includes(q),
|
|
220
|
+
);
|
|
221
|
+
return hit || null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pathContains(parent, child) {
|
|
225
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
226
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function initialCursorForCwd(registry, cwd = process.cwd()) {
|
|
230
|
+
let best = -1;
|
|
231
|
+
let bestLen = -1;
|
|
232
|
+
for (let i = 0; i < registry.length; i++) {
|
|
233
|
+
const entryPath = registry[i]?.path;
|
|
234
|
+
if (!entryPath || !pathContains(entryPath, cwd)) continue;
|
|
235
|
+
const len = path.resolve(entryPath).length;
|
|
236
|
+
if (len > bestLen) {
|
|
237
|
+
best = i;
|
|
238
|
+
bestLen = len;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (best >= 0) return best;
|
|
242
|
+
|
|
243
|
+
const def = readDefault();
|
|
244
|
+
if (def) {
|
|
245
|
+
const idx = registry.findIndex((entry) => entry.slug === def);
|
|
246
|
+
if (idx >= 0) return idx;
|
|
247
|
+
}
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── launch ──────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
function normalizeRuntime(raw) {
|
|
254
|
+
const value = String(raw || "").trim().toLowerCase();
|
|
255
|
+
if (!value) return null;
|
|
256
|
+
if (value === "claude" || value === "claude-code" || value === "claude_code") return "claude-code";
|
|
257
|
+
if (value === "codex" || value === "openai-codex" || value === "openai_codex") return "codex";
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseRuntimeFlags(argv) {
|
|
262
|
+
const rest = [];
|
|
263
|
+
let runtimeOverride = null;
|
|
264
|
+
for (let i = 0; i < argv.length; i++) {
|
|
265
|
+
const arg = argv[i];
|
|
266
|
+
if (arg === "--codex") {
|
|
267
|
+
runtimeOverride = "codex";
|
|
268
|
+
} else if (arg === "--claude" || arg === "--claude-code") {
|
|
269
|
+
runtimeOverride = "claude-code";
|
|
270
|
+
} else if (arg === "--runtime") {
|
|
271
|
+
const next = argv[++i];
|
|
272
|
+
if (!next) die("missing value for --runtime");
|
|
273
|
+
runtimeOverride = next;
|
|
274
|
+
} else if (arg.startsWith("--runtime=")) {
|
|
275
|
+
runtimeOverride = arg.slice("--runtime=".length);
|
|
276
|
+
} else {
|
|
277
|
+
rest.push(arg);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const normalized = runtimeOverride ? normalizeRuntime(runtimeOverride) : null;
|
|
281
|
+
if (runtimeOverride && !normalized) die(`unknown runtime: ${runtimeOverride}\n expected claude-code or codex`);
|
|
282
|
+
return { args: rest, runtimeOverride: normalized };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function storedRuntimeFor(inst) {
|
|
286
|
+
return (
|
|
287
|
+
normalizeRuntime(inst && inst.runtime) ||
|
|
288
|
+
normalizeRuntime(inst && inst.preferred_runtime) ||
|
|
289
|
+
"claude-code"
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function runtimeFor(inst, runtimeOverride) {
|
|
294
|
+
return normalizeRuntime(runtimeOverride) || storedRuntimeFor(inst);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function runtimeLabel(runtime) {
|
|
298
|
+
return runtime === "codex" ? "Codex" : "Claude Code";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function runtimeShort(runtime) {
|
|
302
|
+
return runtime === "codex" ? "codex" : "claude-code";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function runtimeBinary(runtime) {
|
|
306
|
+
return runtime === "codex" ? "codex" : "claude";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function assistantInstallCommand(runtime) {
|
|
310
|
+
return runtime === "codex"
|
|
311
|
+
? "npm install -g @openai/codex"
|
|
312
|
+
: "npm install -g @anthropic-ai/claude-code";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function commandExists(bin) {
|
|
316
|
+
return spawnSync("which", [bin], { stdio: "pipe" }).status === 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function runtimeChoiceDescription(runtime, inst, stored) {
|
|
320
|
+
const installed = commandExists(runtimeBinary(runtime));
|
|
321
|
+
const bits = [];
|
|
322
|
+
if (!installed) {
|
|
323
|
+
return runtime === "codex"
|
|
324
|
+
? "not installed · install Codex, then run egregore"
|
|
325
|
+
: "not installed · install Claude Code, then run egregore";
|
|
326
|
+
}
|
|
327
|
+
bits.push("installed");
|
|
328
|
+
if (runtime === stored) bits.push("saved default");
|
|
329
|
+
if (runtime === "codex") bits.push("portable agent protocol");
|
|
330
|
+
else bits.push("Claude Code integration");
|
|
331
|
+
return bits.join(" · ");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function runtimePickerLines(inst, runtimes, stored, cursor) {
|
|
335
|
+
const title = inst.slug || inst.name || path.basename(inst.path);
|
|
336
|
+
const labelWidth = Math.max(...runtimes.map((r) => runtimeLabel(r).length));
|
|
337
|
+
const lines = [
|
|
338
|
+
"",
|
|
339
|
+
` ${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · ${DIM}1-${runtimes.length}${RESET} quick select · ${DIM}esc${RESET} cancel`,
|
|
340
|
+
` ${CYAN}◆${RESET} ${BOLD}Launch ${title} with which assistant?${RESET}`,
|
|
341
|
+
"",
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
for (let i = 0; i < runtimes.length; i++) {
|
|
345
|
+
const runtime = runtimes[i];
|
|
346
|
+
const marker = i === cursor ? `${CYAN}❯${RESET}` : " ";
|
|
347
|
+
const label = pad(runtimeLabel(runtime), labelWidth);
|
|
348
|
+
const desc = runtimeChoiceDescription(runtime, inst, stored);
|
|
349
|
+
lines.push(` ${marker} ${BOLD}${label}${RESET} ${DIM}${desc}${RESET}`);
|
|
350
|
+
}
|
|
351
|
+
lines.push("");
|
|
352
|
+
return lines;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function pickRuntimeInteractive(inst, runtimes, stored) {
|
|
356
|
+
return new Promise((resolve) => {
|
|
357
|
+
let cursor = 0;
|
|
358
|
+
let rendered = 0;
|
|
359
|
+
let done = false;
|
|
360
|
+
const title = inst.slug || inst.name || path.basename(inst.path);
|
|
361
|
+
const previousRaw = !!process.stdin.isRaw;
|
|
362
|
+
|
|
363
|
+
const erase = () => {
|
|
364
|
+
if (!rendered) return;
|
|
365
|
+
process.stdout.write(`\x1b[${rendered}A`);
|
|
366
|
+
for (let i = 0; i < rendered; i++) {
|
|
367
|
+
process.stdout.write(ANSI.clearLine + (i === rendered - 1 ? "" : "\n"));
|
|
368
|
+
}
|
|
369
|
+
process.stdout.write("\r");
|
|
370
|
+
rendered = 0;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const cleanup = () => {
|
|
374
|
+
if (done) return;
|
|
375
|
+
done = true;
|
|
376
|
+
process.stdin.removeListener("keypress", onKey);
|
|
377
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(previousRaw);
|
|
378
|
+
if (!previousRaw) process.stdin.pause();
|
|
379
|
+
erase();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const finish = (runtime) => {
|
|
383
|
+
cleanup();
|
|
384
|
+
if (runtime) {
|
|
385
|
+
console.log(` ${CYAN}◆${RESET} Launching ${title} with ${BOLD}${runtimeLabel(runtime)}${RESET}`);
|
|
386
|
+
}
|
|
387
|
+
resolve(runtime);
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const render = () => {
|
|
391
|
+
erase();
|
|
392
|
+
const lines = runtimePickerLines(inst, runtimes, stored, cursor);
|
|
393
|
+
rendered = lines.length;
|
|
394
|
+
process.stdout.write(lines.map((line) => line + ANSI.clearLine).join("\n") + "\n");
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
function onKey(chunk, key = {}) {
|
|
398
|
+
if (key.ctrl && key.name === "c") {
|
|
399
|
+
cleanup();
|
|
400
|
+
process.exit(130);
|
|
401
|
+
}
|
|
402
|
+
if (key.name === "escape" || key.name === "q") return finish(null);
|
|
403
|
+
if (key.name === "up" || (key.ctrl && key.name === "p") || key.name === "k") {
|
|
404
|
+
cursor = (cursor - 1 + runtimes.length) % runtimes.length;
|
|
405
|
+
render();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (key.name === "down" || (key.ctrl && key.name === "n") || key.name === "j") {
|
|
409
|
+
cursor = (cursor + 1) % runtimes.length;
|
|
410
|
+
render();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (key.name === "return") return finish(runtimes[cursor]);
|
|
414
|
+
if (chunk && /^[1-9]$/.test(chunk)) {
|
|
415
|
+
const idx = Number(chunk) - 1;
|
|
416
|
+
if (idx >= 0 && idx < runtimes.length) return finish(runtimes[idx]);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
readline.emitKeypressEvents(process.stdin);
|
|
421
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
422
|
+
process.stdin.resume();
|
|
423
|
+
process.stdin.on("keypress", onKey);
|
|
424
|
+
render();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function runtimeChoicesFor(inst, runtimeOverride) {
|
|
429
|
+
const override = normalizeRuntime(runtimeOverride);
|
|
430
|
+
if (override) return [override];
|
|
431
|
+
const stored = storedRuntimeFor(inst);
|
|
432
|
+
const choices = stored === "codex"
|
|
433
|
+
? ["codex", "claude-code"]
|
|
434
|
+
: ["claude-code", "codex"];
|
|
435
|
+
return choices.sort((a, b) => Number(commandExists(runtimeBinary(b))) - Number(commandExists(runtimeBinary(a))));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function startSpinner(label) {
|
|
439
|
+
if (!isTTY) {
|
|
440
|
+
console.log(` ${label}...`);
|
|
441
|
+
return () => {};
|
|
442
|
+
}
|
|
443
|
+
const frames = ["|", "/", "-", "\\"];
|
|
444
|
+
let i = 0;
|
|
445
|
+
process.stdout.write(` ${CYAN}${frames[i]}${RESET} ${label}`);
|
|
446
|
+
const timer = setInterval(() => {
|
|
447
|
+
i = (i + 1) % frames.length;
|
|
448
|
+
process.stdout.write(`\r ${CYAN}${frames[i]}${RESET} ${label}${ANSI.clearLine}`);
|
|
449
|
+
}, 90);
|
|
450
|
+
return () => {
|
|
451
|
+
clearInterval(timer);
|
|
452
|
+
process.stdout.write(`\r${ANSI.clearLine}`);
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function runCapturedWithSpinner(cmd, args, options, label) {
|
|
457
|
+
return new Promise((resolve) => {
|
|
458
|
+
const stop = startSpinner(label);
|
|
459
|
+
let stdout = "";
|
|
460
|
+
let stderr = "";
|
|
461
|
+
let settled = false;
|
|
462
|
+
const child = spawn(cmd, args, {
|
|
463
|
+
cwd: options.cwd,
|
|
464
|
+
env: options.env || process.env,
|
|
465
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
469
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
470
|
+
child.on("error", (err) => {
|
|
471
|
+
if (settled) return;
|
|
472
|
+
settled = true;
|
|
473
|
+
stop();
|
|
474
|
+
resolve({ status: 1, stdout, stderr: stderr || String(err) });
|
|
475
|
+
});
|
|
476
|
+
child.on("close", (status) => {
|
|
477
|
+
if (settled) return;
|
|
478
|
+
settled = true;
|
|
479
|
+
stop();
|
|
480
|
+
resolve({ status, stdout, stderr });
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function chooseRuntimeForLaunch(inst, runtimeOverride) {
|
|
486
|
+
const override = normalizeRuntime(runtimeOverride);
|
|
487
|
+
if (override) return override;
|
|
488
|
+
|
|
489
|
+
const stored = storedRuntimeFor(inst);
|
|
490
|
+
if (!canPrompt()) return stored;
|
|
491
|
+
|
|
492
|
+
const runtimes = runtimeChoicesFor(inst);
|
|
493
|
+
return pickRuntimeInteractive(inst, runtimes, stored);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function renderCodexStartupCard(egregoreDir) {
|
|
497
|
+
const script = path.join(egregoreDir, "bin", "codex-session-start.sh");
|
|
498
|
+
if (fs.existsSync(script)) {
|
|
499
|
+
try {
|
|
500
|
+
const result = await runCapturedWithSpinner("bash", [script], {
|
|
501
|
+
cwd: egregoreDir,
|
|
502
|
+
}, "Preparing Codex startup");
|
|
503
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
504
|
+
if (result.status !== 0 && result.stderr) process.stderr.write(result.stderr);
|
|
505
|
+
if (result.status === 0) return true;
|
|
506
|
+
} catch {}
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function codexSupportMessage(egregoreDir) {
|
|
512
|
+
return [
|
|
513
|
+
"Codex assistant support is missing from this Egregore checkout.",
|
|
514
|
+
` path: ${egregoreDir}`,
|
|
515
|
+
"",
|
|
516
|
+
"This checkout needs a newer Egregore core with:",
|
|
517
|
+
" bin/codex-session-start.sh",
|
|
518
|
+
" bin/codex-install-skills.sh",
|
|
519
|
+
" .codex/config.toml",
|
|
520
|
+
" .codex/skills/",
|
|
521
|
+
" .codex/hooks.json",
|
|
522
|
+
" .codex/hooks/",
|
|
523
|
+
"",
|
|
524
|
+
"Run npx -y create-egregore@latest to install the packaged Codex runtime, then rerun egregore.",
|
|
525
|
+
"Egregore workflows appear as slash commands. In Codex, invoke them with the matching $skill token or ask in natural language.",
|
|
526
|
+
].join("\n");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function hasCodexCoreSupport(egregoreDir) {
|
|
530
|
+
return (
|
|
531
|
+
fs.existsSync(path.join(egregoreDir, "bin", "codex-session-start.sh")) &&
|
|
532
|
+
fs.existsSync(path.join(egregoreDir, "bin", "codex-install-skills.sh")) &&
|
|
533
|
+
fs.existsSync(path.join(egregoreDir, ".codex", "config.toml")) &&
|
|
534
|
+
fs.existsSync(path.join(egregoreDir, ".codex", "skills")) &&
|
|
535
|
+
fs.existsSync(path.join(egregoreDir, ".codex", "hooks.json")) &&
|
|
536
|
+
fs.existsSync(path.join(egregoreDir, ".codex", "hooks"))
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function installCodexSkills(egregoreDir) {
|
|
541
|
+
const script = path.join(egregoreDir, "bin", "codex-install-skills.sh");
|
|
542
|
+
if (!fs.existsSync(script)) return false;
|
|
543
|
+
try {
|
|
544
|
+
const result = spawnSync("bash", [script], {
|
|
545
|
+
cwd: egregoreDir,
|
|
546
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
547
|
+
encoding: "utf-8",
|
|
548
|
+
maxBuffer: 1024 * 1024,
|
|
549
|
+
});
|
|
550
|
+
return result.status === 0;
|
|
551
|
+
} catch {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function runtimeLaunchArgs(runtime, egregoreDir) {
|
|
557
|
+
if (runtime === "codex") {
|
|
558
|
+
return [
|
|
559
|
+
"--enable", "hooks",
|
|
560
|
+
"--sandbox", "danger-full-access",
|
|
561
|
+
"--ask-for-approval", "never",
|
|
562
|
+
"--cd", egregoreDir,
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
return ["start"];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function launch(inst, runtimeOverride) {
|
|
569
|
+
if (!inst) die("no instance to launch");
|
|
570
|
+
if (!fs.existsSync(inst.path)) die(`instance path does not exist: ${inst.path}`);
|
|
571
|
+
const runtime = await chooseRuntimeForLaunch(inst, runtimeOverride);
|
|
572
|
+
if (!runtime) return;
|
|
573
|
+
const binary = runtimeBinary(runtime);
|
|
574
|
+
if (!commandExists(binary)) {
|
|
575
|
+
die(`${runtimeLabel(runtime)} is not installed.\n Install it with:\n ${assistantInstallCommand(runtime)}\n Then run: egregore`);
|
|
576
|
+
}
|
|
577
|
+
if (runtime === "codex") {
|
|
578
|
+
if (!hasCodexCoreSupport(inst.path)) {
|
|
579
|
+
die(codexSupportMessage(inst.path));
|
|
580
|
+
}
|
|
581
|
+
const skillsInstalled = installCodexSkills(inst.path);
|
|
582
|
+
const cardRendered = await renderCodexStartupCard(inst.path);
|
|
583
|
+
if (!skillsInstalled || !cardRendered) {
|
|
584
|
+
die(codexSupportMessage(inst.path));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Replace the current process with the selected assistant in the instance dir.
|
|
588
|
+
// spawn + detached + exit-on-exit replicates exec() in Node.
|
|
589
|
+
const child = spawn(binary, runtimeLaunchArgs(runtime, inst.path), {
|
|
590
|
+
cwd: inst.path,
|
|
591
|
+
stdio: "inherit",
|
|
592
|
+
env: {
|
|
593
|
+
...process.env,
|
|
594
|
+
EGREGORE_RUNTIME: runtime,
|
|
595
|
+
EGREGORE_CODEX_TRUSTED: runtime === "codex" ? "1" : process.env.EGREGORE_CODEX_TRUSTED,
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
child.on("exit", (code, signal) => {
|
|
599
|
+
process.exit(signal ? 1 : (code ?? 0));
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function setupNewEgregore(runtimeOverride) {
|
|
604
|
+
const args = ["--yes", `${PUBLIC_NPX_PACKAGE}@latest`, "--open"];
|
|
605
|
+
if (runtimeOverride) args.push("--runtime", runtimeOverride);
|
|
606
|
+
const child = spawn("npx", args, {
|
|
607
|
+
stdio: "inherit",
|
|
608
|
+
cwd: process.cwd(),
|
|
609
|
+
env: { ...process.env },
|
|
610
|
+
});
|
|
611
|
+
child.on("exit", (code, signal) => {
|
|
612
|
+
process.exit(signal ? 1 : (code ?? 0));
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ── non-interactive commands ────────────────────────────────────────
|
|
617
|
+
|
|
618
|
+
function cmdLs() {
|
|
619
|
+
const registry = readRegistry();
|
|
620
|
+
const def = readDefault();
|
|
621
|
+
if (registry.length === 0) {
|
|
622
|
+
console.log(` ${DIM}no egregores installed yet.${RESET} run ${CYAN}npx -y create-egregore@latest${RESET}`);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const slugW = Math.min(24, Math.max(...registry.map((r) => (r.slug || "").length)));
|
|
626
|
+
const nameW = Math.min(24, Math.max(...registry.map((r) => (r.name || "").length)));
|
|
627
|
+
const runtimeW = Math.max(9, ...registry.map((r) => runtimeShort(runtimeFor(r)).length));
|
|
628
|
+
console.log(`\n ${BOLD}${registry.length}${RESET} egregore${registry.length === 1 ? "" : "s"} registered\n`);
|
|
629
|
+
console.log(` ${DIM}${pad("SLUG", slugW)} ${pad("NAME", nameW)} ${pad("ASSISTANT", runtimeW)} PATH${RESET}`);
|
|
630
|
+
for (const inst of registry) {
|
|
631
|
+
const marker = inst.slug === def ? `${GREEN}●${RESET}` : " ";
|
|
632
|
+
const slug = inst.slug === def ? `${BOLD}${pad(inst.slug, slugW)}${RESET}` : pad(inst.slug, slugW);
|
|
633
|
+
const runtime = pad(runtimeShort(runtimeFor(inst)), runtimeW);
|
|
634
|
+
console.log(` ${marker} ${slug} ${pad(inst.name || "", nameW)} ${runtime} ${DIM}${inst.path}${RESET}`);
|
|
635
|
+
}
|
|
636
|
+
if (def) console.log(`\n ${GREEN}●${RESET} = default marker (used by non-interactive ${BOLD}egregore${RESET})\n`);
|
|
637
|
+
else console.log("");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function cmdUse(slug) {
|
|
641
|
+
if (!slug) die("usage: egregore use <slug>");
|
|
642
|
+
const registry = readRegistry();
|
|
643
|
+
const hit = resolveBySlug(registry, slug);
|
|
644
|
+
if (!hit) die(`no instance matching '${slug}'. run: egregore ls`);
|
|
645
|
+
writeDefault(hit.slug);
|
|
646
|
+
console.log(` ${GREEN}✓${RESET} default set to ${BOLD}${hit.slug}${RESET}`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function cmdRmNonInteractive(target) {
|
|
650
|
+
if (!target) die("usage: egregore rm <slug|path>");
|
|
651
|
+
const registry = readRegistry();
|
|
652
|
+
const matches = registry.filter(
|
|
653
|
+
(e) => e.slug === target || e.path === target ||
|
|
654
|
+
e.slug.includes(target) || e.path.includes(target),
|
|
655
|
+
);
|
|
656
|
+
if (matches.length === 0) die(`no instance matching '${target}'. run: egregore ls`);
|
|
657
|
+
if (matches.length > 1) {
|
|
658
|
+
process.stderr.write(`\n ${YELLOW}!${RESET} ${matches.length} entries match ${BOLD}"${target}"${RESET}:\n\n`);
|
|
659
|
+
for (const m of matches) {
|
|
660
|
+
process.stderr.write(` ${pad(m.slug, 22)} ${pad(m.name, 24)} ${DIM}${m.path}${RESET}\n`);
|
|
661
|
+
}
|
|
662
|
+
process.stderr.write(`\n Use an exact path to pick one: ${CYAN}egregore rm <full-path>${RESET}\n\n`);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
const hit = matches[0];
|
|
666
|
+
process.stdout.write(`\n about to remove:\n ${pad(hit.slug, 22)} ${pad(hit.name, 24)} ${DIM}${hit.path}${RESET}\n\n`);
|
|
667
|
+
const ans = syncConfirm(" confirm? [y/N]: ");
|
|
668
|
+
if (!ans) { console.log(` ${DIM}cancelled.${RESET}`); return; }
|
|
669
|
+
|
|
670
|
+
const bak = backupRegistry();
|
|
671
|
+
writeRegistry(registry.filter((e) => e.path !== hit.path));
|
|
672
|
+
console.log(` ${GREEN}✓${RESET} removed.\n ${DIM}backup:${RESET} ${bak}\n`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function cmdPruneNonInteractive(dry) {
|
|
676
|
+
const registry = readRegistry();
|
|
677
|
+
const stale = registry.filter((e) => !fs.existsSync(e.path));
|
|
678
|
+
if (stale.length === 0) {
|
|
679
|
+
console.log(` ${GREEN}✓${RESET} registry is clean — ${registry.length} entries, all paths exist.`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
console.log(`\n ${YELLOW}!${RESET} ${stale.length} of ${registry.length} entries have missing paths:\n`);
|
|
683
|
+
for (const e of stale) console.log(` ${RED}✗${RESET} ${pad(e.slug, 22)} ${DIM}${e.path}${RESET}`);
|
|
684
|
+
console.log("");
|
|
685
|
+
if (dry) {
|
|
686
|
+
console.log(` ${DIM}(dry run — nothing removed. re-run without --dry-run to apply.)${RESET}\n`);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const ans = syncConfirm(" remove them? [y/N]: ");
|
|
690
|
+
if (!ans) { console.log(` ${DIM}cancelled.${RESET}`); return; }
|
|
691
|
+
const bak = backupRegistry();
|
|
692
|
+
writeRegistry(registry.filter((e) => fs.existsSync(e.path)));
|
|
693
|
+
console.log(` ${GREEN}✓${RESET} pruned ${stale.length} — ${registry.length - stale.length} remain.`);
|
|
694
|
+
console.log(` ${DIM}backup:${RESET} ${bak}\n`);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function cmdHelp() {
|
|
698
|
+
const lines = [
|
|
699
|
+
``,
|
|
700
|
+
` ${BOLD}${CYAN}egregore${RESET} ${DIM}launch and browse Egregore instances${RESET}`,
|
|
701
|
+
``,
|
|
702
|
+
` ${BOLD}USAGE${RESET}`,
|
|
703
|
+
` ${CYAN}egregore${RESET} pick an instance, then pick an assistant`,
|
|
704
|
+
` ${CYAN}egregore${RESET} ${DIM}<slug|name>${RESET} pick an assistant for the matching instance`,
|
|
705
|
+
` ${CYAN}egregore --runtime${RESET} ${DIM}<name> <slug>${RESET} advanced assistant override`,
|
|
706
|
+
` ${CYAN}egregore browse${RESET} force the full-screen browser`,
|
|
707
|
+
` ${CYAN}egregore ls${RESET} ${DIM}| list${RESET} list installed instances`,
|
|
708
|
+
` ${CYAN}egregore use${RESET} ${DIM}<slug>${RESET} set the default for bare ${BOLD}egregore${RESET}`,
|
|
709
|
+
` ${CYAN}egregore /invite${RESET} ${DIM}<user> [slug]${RESET} invite a GitHub user`,
|
|
710
|
+
` ${CYAN}egregore prune${RESET} ${DIM}[--dry-run]${RESET} remove entries with missing paths`,
|
|
711
|
+
` ${CYAN}egregore rm${RESET} ${DIM}<slug|path>${RESET} remove a specific entry`,
|
|
712
|
+
``,
|
|
713
|
+
` ${BOLD}BROWSER KEYS${RESET}`,
|
|
714
|
+
` ${DIM}↑ ↓${RESET} navigate`,
|
|
715
|
+
` ${DIM}enter${RESET} choose selected instance`,
|
|
716
|
+
` ${DIM}/${RESET} filter (type to narrow; esc to clear)`,
|
|
717
|
+
` ${DIM}n / +${RESET} create a new Egregore`,
|
|
718
|
+
` ${DIM}u${RESET} set selected as default`,
|
|
719
|
+
` ${DIM}i${RESET} invite a GitHub user to selected instance`,
|
|
720
|
+
` ${DIM}r${RESET} remove selected (with confirm)`,
|
|
721
|
+
` ${DIM}p${RESET} prune entries with missing paths (with confirm)`,
|
|
722
|
+
` ${DIM}?${RESET} toggle help overlay`,
|
|
723
|
+
` ${DIM}q / esc${RESET} quit browser`,
|
|
724
|
+
``,
|
|
725
|
+
` ${BOLD}REGISTRY${RESET}`,
|
|
726
|
+
` ${DIM}instances:${RESET} ~/.egregore/instances.json`,
|
|
727
|
+
` ${DIM}default:${RESET} ~/.egregore/default`,
|
|
728
|
+
` ${DIM}mutations back up to instances.json.bak.<timestamp>${RESET}`,
|
|
729
|
+
``,
|
|
730
|
+
];
|
|
731
|
+
console.log(lines.join("\n"));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ── invite flow ────────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
function readJsonFile(file) {
|
|
737
|
+
try {
|
|
738
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
739
|
+
} catch {
|
|
740
|
+
return {};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function readEnvValues(file) {
|
|
745
|
+
const values = {};
|
|
746
|
+
if (!fs.existsSync(file)) return values;
|
|
747
|
+
for (const raw of fs.readFileSync(file, "utf-8").split(/\r?\n/)) {
|
|
748
|
+
const line = raw.trim();
|
|
749
|
+
if (!line || line.startsWith("#")) continue;
|
|
750
|
+
const idx = line.indexOf("=");
|
|
751
|
+
if (idx < 0) continue;
|
|
752
|
+
values[line.slice(0, idx)] = line.slice(idx + 1);
|
|
753
|
+
}
|
|
754
|
+
return values;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function validGithubUsername(username) {
|
|
758
|
+
return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(username);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function repoNameFromRemote(remote) {
|
|
762
|
+
const clean = String(remote || "").trim().replace(/\.git$/, "");
|
|
763
|
+
if (!clean) return "";
|
|
764
|
+
const parts = clean.split(/[/:]/).filter(Boolean);
|
|
765
|
+
return parts[parts.length - 1] || "";
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function configForInstance(inst) {
|
|
769
|
+
const cfg = readJsonFile(path.join(inst.path, "egregore.json"));
|
|
770
|
+
const state = readJsonFile(path.join(inst.path, ".egregore-state.json"));
|
|
771
|
+
const env = { ...readEnvValues(path.join(inst.path, ".env")), ...process.env };
|
|
772
|
+
return { cfg, state, env };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function inviteRepos(cfg) {
|
|
776
|
+
const repos = [];
|
|
777
|
+
if (cfg.repo_name) repos.push(cfg.repo_name);
|
|
778
|
+
const memoryName = repoNameFromRemote(cfg.memory_repo);
|
|
779
|
+
if (memoryName) repos.push(memoryName);
|
|
780
|
+
if (Array.isArray(cfg.repos)) {
|
|
781
|
+
for (const entry of cfg.repos) {
|
|
782
|
+
const name = typeof entry === "string" ? entry : entry && entry.name;
|
|
783
|
+
if (name) repos.push(name);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return [...new Set(repos)];
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function requestJson(options, body) {
|
|
790
|
+
return new Promise((resolve) => {
|
|
791
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
792
|
+
const headers = {
|
|
793
|
+
Accept: "application/vnd.github+json",
|
|
794
|
+
"User-Agent": "egregore-launcher",
|
|
795
|
+
...(options.headers || {}),
|
|
796
|
+
};
|
|
797
|
+
if (payload) {
|
|
798
|
+
headers["Content-Type"] = "application/json";
|
|
799
|
+
headers["Content-Length"] = Buffer.byteLength(payload);
|
|
800
|
+
}
|
|
801
|
+
const transport = options.protocol === "http:" ? http : https;
|
|
802
|
+
const req = transport.request({ ...options, headers }, (res) => {
|
|
803
|
+
let buf = "";
|
|
804
|
+
res.on("data", (chunk) => { buf += chunk; });
|
|
805
|
+
res.on("end", () => {
|
|
806
|
+
let data = buf;
|
|
807
|
+
try { data = buf ? JSON.parse(buf) : {}; } catch {}
|
|
808
|
+
resolve({ status: res.statusCode || 0, data });
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
req.on("error", (err) => resolve({ status: 0, data: { message: err.message } }));
|
|
812
|
+
if (payload) req.write(payload);
|
|
813
|
+
req.end();
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function githubRequest(token, method, apiPath, body) {
|
|
818
|
+
return requestJson({
|
|
819
|
+
hostname: "api.github.com",
|
|
820
|
+
path: apiPath,
|
|
821
|
+
method,
|
|
822
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
823
|
+
}, body);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function apiRequest(apiUrl, apiKey, apiPath, body) {
|
|
827
|
+
const url = new URL(apiPath, apiUrl.replace(/\/+$/, "") + "/");
|
|
828
|
+
return requestJson({
|
|
829
|
+
hostname: url.hostname,
|
|
830
|
+
port: url.port || undefined,
|
|
831
|
+
path: `${url.pathname}${url.search}`,
|
|
832
|
+
method: "POST",
|
|
833
|
+
protocol: url.protocol,
|
|
834
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
835
|
+
}, body);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function accessLabel(status) {
|
|
839
|
+
if (status === 201) return `${GREEN}sent${RESET}`;
|
|
840
|
+
if (status === 204) return `${GREEN}already added${RESET}`;
|
|
841
|
+
if (status === 422) return `${YELLOW}already invited or added${RESET}`;
|
|
842
|
+
if (status === 403) return `${RED}permission denied${RESET}`;
|
|
843
|
+
if (status === 404) return `${RED}not found${RESET}`;
|
|
844
|
+
if (status === 0) return `${RED}network error${RESET}`;
|
|
845
|
+
return `${RED}failed ${status}${RESET}`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function runGit(cwd, args) {
|
|
849
|
+
try {
|
|
850
|
+
return spawnSync("git", ["-C", cwd, ...args], {
|
|
851
|
+
stdio: "pipe",
|
|
852
|
+
encoding: "utf-8",
|
|
853
|
+
timeout: 15000,
|
|
854
|
+
});
|
|
855
|
+
} catch (err) {
|
|
856
|
+
return { status: 1, stderr: String(err), stdout: "" };
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function ensurePersonFile(inst, username, inviter) {
|
|
861
|
+
const memoryDir = path.join(inst.path, "memory");
|
|
862
|
+
if (!fs.existsSync(memoryDir)) return { status: "missing-memory", path: null, committed: false, pushed: false };
|
|
863
|
+
const peopleDir = path.join(memoryDir, "people");
|
|
864
|
+
fs.mkdirSync(peopleDir, { recursive: true });
|
|
865
|
+
const personPath = path.join(peopleDir, `${username}.md`);
|
|
866
|
+
let created = false;
|
|
867
|
+
if (!fs.existsSync(personPath)) {
|
|
868
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
869
|
+
fs.writeFileSync(personPath, [
|
|
870
|
+
"---",
|
|
871
|
+
`name: ${username}`,
|
|
872
|
+
`github: ${username}`,
|
|
873
|
+
`invited_by: ${inviter}`,
|
|
874
|
+
`joined: ${today}`,
|
|
875
|
+
"---",
|
|
876
|
+
"",
|
|
877
|
+
].join("\n"));
|
|
878
|
+
created = true;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return {
|
|
882
|
+
status: created ? "created" : "exists",
|
|
883
|
+
path: personPath,
|
|
884
|
+
committed: false,
|
|
885
|
+
pushed: false,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function managedRepoNames(cfg) {
|
|
890
|
+
if (!Array.isArray(cfg.repos)) return [];
|
|
891
|
+
return cfg.repos
|
|
892
|
+
.map((entry) => (typeof entry === "string" ? entry : entry && entry.name))
|
|
893
|
+
.filter(Boolean);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function yamlList(values) {
|
|
897
|
+
const clean = values.filter(Boolean);
|
|
898
|
+
if (clean.length === 0) return " []";
|
|
899
|
+
return clean.map((value) => `\n - ${String(value).replace(/\n/g, " ")}`).join("");
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function safeInviteFilename(username) {
|
|
903
|
+
const safe = String(username || "invitee")
|
|
904
|
+
.toLowerCase()
|
|
905
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
906
|
+
.replace(/^-|-$/g, "") || "invitee";
|
|
907
|
+
return `${new Date().toISOString().slice(0, 10)}-${safe}.md`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function memoryRepoForInvite(cfg, githubOrg) {
|
|
911
|
+
if (cfg.memory_repo && /^https?:\/\//i.test(cfg.memory_repo)) return cfg.memory_repo;
|
|
912
|
+
if (cfg.memory_repo && /^git@/i.test(cfg.memory_repo)) return cfg.memory_repo;
|
|
913
|
+
if (cfg.memory_repo) return `https://github.com/${githubOrg}/${cfg.memory_repo}.git`;
|
|
914
|
+
return `https://github.com/${githubOrg}/egregore-memory.git`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function writeInviteArtifact(inst, username, inviter, cfg, opts = {}) {
|
|
918
|
+
const memoryDir = path.join(inst.path, "memory");
|
|
919
|
+
if (!fs.existsSync(memoryDir)) return { status: "missing-memory", path: null };
|
|
920
|
+
const invitesDir = path.join(memoryDir, "invites");
|
|
921
|
+
fs.mkdirSync(invitesDir, { recursive: true });
|
|
922
|
+
const githubOrg = cfg.github_org || "";
|
|
923
|
+
const repoName = cfg.repo_name || "egregore";
|
|
924
|
+
const fallbackCommand = opts.fallbackCommand || joinCommand(githubOrg, repoName);
|
|
925
|
+
const invitePath = path.join(invitesDir, safeInviteFilename(username));
|
|
926
|
+
const lines = [
|
|
927
|
+
"---",
|
|
928
|
+
"type: egregore_invite",
|
|
929
|
+
`created_at: ${new Date().toISOString()}`,
|
|
930
|
+
`inviter: ${inviter}`,
|
|
931
|
+
`invitee_github: ${username}`,
|
|
932
|
+
`org: ${githubOrg}`,
|
|
933
|
+
`repo: ${githubOrg}/${repoName}`,
|
|
934
|
+
`memory_repo: ${memoryRepoForInvite(cfg, githubOrg)}`,
|
|
935
|
+
`managed_repos:${yamlList(managedRepoNames(cfg))}`,
|
|
936
|
+
`invite_url: ${opts.inviteUrl || ""}`,
|
|
937
|
+
`fallback_command: ${fallbackCommand}`,
|
|
938
|
+
`telegram_group_link: ${cfg.telegram_group_link || ""}`,
|
|
939
|
+
`delivery: ${opts.delivery || "copyable_share_block"}`,
|
|
940
|
+
"---",
|
|
941
|
+
"",
|
|
942
|
+
`# Invite: ${username}`,
|
|
943
|
+
"",
|
|
944
|
+
`Fallback command: \`${fallbackCommand}\``,
|
|
945
|
+
"",
|
|
946
|
+
];
|
|
947
|
+
if (opts.inviteUrl) lines.push(`Invite URL: ${opts.inviteUrl}`, "");
|
|
948
|
+
if (cfg.telegram_group_link) lines.push(`Telegram: ${cfg.telegram_group_link}`, "");
|
|
949
|
+
fs.writeFileSync(invitePath, lines.join("\n"));
|
|
950
|
+
return { status: "written", path: invitePath };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function ensureInviteAudit(inst, username, inviter, cfg, opts = {}) {
|
|
954
|
+
const memoryDir = path.join(inst.path, "memory");
|
|
955
|
+
const person = ensurePersonFile(inst, username, inviter);
|
|
956
|
+
const artifact = writeInviteArtifact(inst, username, inviter, cfg, opts);
|
|
957
|
+
if (!fs.existsSync(memoryDir) || !fs.existsSync(path.join(memoryDir, ".git"))) {
|
|
958
|
+
return { person, artifact, committed: false, pushed: false };
|
|
959
|
+
}
|
|
960
|
+
const paths = [person.path, artifact.path].filter(Boolean);
|
|
961
|
+
for (const p of paths) runGit(memoryDir, ["add", path.relative(memoryDir, p)]);
|
|
962
|
+
const commit = runGit(memoryDir, ["commit", "-m", `Invite ${username}`]);
|
|
963
|
+
const committed = commit.status === 0;
|
|
964
|
+
const push = committed ? runGit(memoryDir, ["push"]) : { status: 1 };
|
|
965
|
+
return { person, artifact, committed, pushed: push.status === 0 };
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function tryNotifyInvite(inst, username, message, cfg, env) {
|
|
969
|
+
const script = path.join(inst.path, "bin", "notify.sh");
|
|
970
|
+
if (!fs.existsSync(script)) return null;
|
|
971
|
+
const hasConnectedChannel = (cfg.api_url && env.EGREGORE_API_KEY) || cfg.telegram_group_link;
|
|
972
|
+
if (!hasConnectedChannel) return null;
|
|
973
|
+
try {
|
|
974
|
+
const result = spawnSync("bash", [script, "send", username, message], {
|
|
975
|
+
cwd: inst.path,
|
|
976
|
+
stdio: "pipe",
|
|
977
|
+
encoding: "utf-8",
|
|
978
|
+
timeout: 15000,
|
|
979
|
+
env: { ...process.env, ...env },
|
|
980
|
+
});
|
|
981
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
982
|
+
if (result.status === 0 && /Sent|sent/i.test(output)) return { status: "sent", output };
|
|
983
|
+
return { status: "failed", output };
|
|
984
|
+
} catch (err) {
|
|
985
|
+
return { status: "failed", output: String(err) };
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function connectedInvite(inst, username, cfg, env) {
|
|
990
|
+
const apiUrl = cfg.api_url || "";
|
|
991
|
+
const apiKey = env.EGREGORE_API_KEY || "";
|
|
992
|
+
const ghToken = env.GITHUB_TOKEN || "";
|
|
993
|
+
if (!apiUrl || !apiKey || !ghToken) return null;
|
|
994
|
+
return apiRequest(apiUrl, apiKey, "/api/org/invite", {
|
|
995
|
+
github_org: cfg.github_org,
|
|
996
|
+
github_username: username,
|
|
997
|
+
repo_name: cfg.repo_name || "egregore",
|
|
998
|
+
slug: cfg.slug || inst.slug,
|
|
999
|
+
github_token: ghToken,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function inviteToInstance(inst, username) {
|
|
1004
|
+
if (!validGithubUsername(username)) die(`invalid GitHub username: ${username}`);
|
|
1005
|
+
if (!fs.existsSync(inst.path)) die(`instance path does not exist: ${inst.path}`);
|
|
1006
|
+
|
|
1007
|
+
const { cfg, state, env } = configForInstance(inst);
|
|
1008
|
+
const ghToken = env.GITHUB_TOKEN || "";
|
|
1009
|
+
const githubOrg = cfg.github_org || "";
|
|
1010
|
+
const repoName = cfg.repo_name || "egregore";
|
|
1011
|
+
const orgName = cfg.org_name || inst.name || githubOrg || "this Egregore";
|
|
1012
|
+
const inviter = state.github_username || state.display_name || os.userInfo().username;
|
|
1013
|
+
if (!githubOrg) die(`missing github_org in ${path.join(inst.path, "egregore.json")}`);
|
|
1014
|
+
if (!ghToken) die(`missing GITHUB_TOKEN in ${path.join(inst.path, ".env")}\n run: bash bin/github-auth.sh`);
|
|
1015
|
+
|
|
1016
|
+
console.log("");
|
|
1017
|
+
console.log(` ${BOLD}${CYAN}/invite${RESET} ${BOLD}${username}${RESET} ${DIM}→ ${orgName}${RESET}`);
|
|
1018
|
+
console.log("");
|
|
1019
|
+
|
|
1020
|
+
const connected = await connectedInvite(inst, username, cfg, env);
|
|
1021
|
+
if (connected && connected.status >= 200 && connected.status < 300 && connected.data && connected.data.invite_url) {
|
|
1022
|
+
const fallbackCommand = connected.data.fallback_command || joinCommand(githubOrg, repoName);
|
|
1023
|
+
const telegramGroupLink = connected.data.telegram_group_link || cfg.telegram_group_link || "";
|
|
1024
|
+
const inviteCfg = telegramGroupLink ? { ...cfg, telegram_group_link: telegramGroupLink } : cfg;
|
|
1025
|
+
const audit = ensureInviteAudit(inst, username, inviter, inviteCfg, {
|
|
1026
|
+
inviteUrl: connected.data.invite_url,
|
|
1027
|
+
fallbackCommand,
|
|
1028
|
+
delivery: "invite_url",
|
|
1029
|
+
});
|
|
1030
|
+
console.log(` ${GREEN}✓${RESET} invite link created`);
|
|
1031
|
+
if (connected.data.github_invite?.status) {
|
|
1032
|
+
console.log(` ${DIM}github${RESET} ${connected.data.github_invite.status}`);
|
|
1033
|
+
}
|
|
1034
|
+
if (connected.data.memory_access) {
|
|
1035
|
+
console.log(` ${DIM}memory${RESET} ${connected.data.memory_access.status || connected.data.memory_access}`);
|
|
1036
|
+
}
|
|
1037
|
+
const auditStatus = audit.artifact.status === "missing-memory"
|
|
1038
|
+
? `${YELLOW}memory/ missing${RESET}`
|
|
1039
|
+
: `${GREEN}recorded${RESET}${audit.pushed ? DIM + " · pushed" + RESET : ""}`;
|
|
1040
|
+
console.log(` ${DIM}memory/invites${RESET} ${auditStatus}`);
|
|
1041
|
+
const notified = tryNotifyInvite(inst, username, connected.data.invite_url, cfg, env);
|
|
1042
|
+
if (notified && notified.status === "sent") {
|
|
1043
|
+
console.log(` ${DIM}notify${RESET} ${GREEN}sent${RESET}`);
|
|
1044
|
+
}
|
|
1045
|
+
console.log("");
|
|
1046
|
+
console.log(` ${BOLD}Share this link:${RESET}`);
|
|
1047
|
+
console.log(` ${CYAN}${connected.data.invite_url}${RESET}`);
|
|
1048
|
+
console.log(` ${DIM}fallback:${RESET} ${fallbackCommand}`);
|
|
1049
|
+
if (telegramGroupLink) console.log(` ${DIM}telegram:${RESET} ${telegramGroupLink}`);
|
|
1050
|
+
console.log("");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (connected && connected.status !== 0) {
|
|
1054
|
+
const detail = connected.data && (connected.data.detail || connected.data.message || connected.data.error);
|
|
1055
|
+
console.log(` ${YELLOW}!${RESET} connected invite unavailable${detail ? `: ${detail}` : ""}`);
|
|
1056
|
+
console.log(` ${DIM}falling back to GitHub collaborator access${RESET}`);
|
|
1057
|
+
console.log("");
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const repos = inviteRepos(cfg);
|
|
1061
|
+
if (repos.length === 0) die("no repos configured for this Egregore");
|
|
1062
|
+
const results = [];
|
|
1063
|
+
for (const repo of repos) {
|
|
1064
|
+
const result = await githubRequest(
|
|
1065
|
+
ghToken,
|
|
1066
|
+
"PUT",
|
|
1067
|
+
`/repos/${encodeURIComponent(githubOrg)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
|
|
1068
|
+
{ permission: "push" },
|
|
1069
|
+
);
|
|
1070
|
+
results.push({ repo, status: result.status, data: result.data });
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const fallbackCommand = joinCommand(githubOrg, repoName);
|
|
1074
|
+
const audit = ensureInviteAudit(inst, username, inviter, cfg, {
|
|
1075
|
+
fallbackCommand,
|
|
1076
|
+
delivery: "copyable_share_block",
|
|
1077
|
+
});
|
|
1078
|
+
for (const result of results) {
|
|
1079
|
+
console.log(` ${DIM}${pad(result.repo, 24)}${RESET} ${accessLabel(result.status)}`);
|
|
1080
|
+
}
|
|
1081
|
+
const personState = audit.person.status === "missing-memory"
|
|
1082
|
+
? `${YELLOW}memory/ missing${RESET}`
|
|
1083
|
+
: audit.person.status === "created"
|
|
1084
|
+
? `${GREEN}created${RESET}`
|
|
1085
|
+
: `${DIM}already exists${RESET}`;
|
|
1086
|
+
const artifactState = audit.artifact.status === "missing-memory"
|
|
1087
|
+
? `${YELLOW}memory/ missing${RESET}`
|
|
1088
|
+
: `${GREEN}recorded${RESET}${audit.pushed ? DIM + " · pushed" + RESET : ""}`;
|
|
1089
|
+
console.log(` ${DIM}${pad("memory/people", 24)}${RESET} ${personState}`);
|
|
1090
|
+
console.log(` ${DIM}${pad("memory/invites", 24)}${RESET} ${artifactState}`);
|
|
1091
|
+
console.log("");
|
|
1092
|
+
console.log(` ${BOLD}Send them this join command:${RESET}`);
|
|
1093
|
+
console.log(` ${CYAN}${fallbackCommand}${RESET}`);
|
|
1094
|
+
if (cfg.telegram_group_link) console.log(` ${DIM}telegram:${RESET} ${cfg.telegram_group_link}`);
|
|
1095
|
+
|
|
1096
|
+
const failed = results.filter((r) => ![201, 204, 422].includes(r.status));
|
|
1097
|
+
if (failed.length > 0) {
|
|
1098
|
+
console.log("");
|
|
1099
|
+
console.log(` ${YELLOW}!${RESET} Some repo invites failed. Manual access page:`);
|
|
1100
|
+
console.log(` ${CYAN}https://github.com/${githubOrg}/${repoName}/settings/access${RESET}`);
|
|
1101
|
+
}
|
|
1102
|
+
console.log("");
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function resolveInviteInstance(registry, query) {
|
|
1106
|
+
if (query) return resolveBySlug(registry, query);
|
|
1107
|
+
const def = readDefault();
|
|
1108
|
+
if (def) {
|
|
1109
|
+
const hit = registry.find((e) => e.slug === def);
|
|
1110
|
+
if (hit) return hit;
|
|
1111
|
+
}
|
|
1112
|
+
return registry.length === 1 ? registry[0] : null;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function syncPrompt(question) {
|
|
1116
|
+
process.stdout.write(question);
|
|
1117
|
+
const buf = Buffer.alloc(256);
|
|
1118
|
+
const n = fs.readSync(0, buf, 0, 256, null);
|
|
1119
|
+
return buf.slice(0, n).toString().trim();
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function cmdInvite(args) {
|
|
1123
|
+
const registry = readRegistry();
|
|
1124
|
+
if (registry.length === 0) die("no egregores installed yet.\n run: npx -y create-egregore@latest");
|
|
1125
|
+
let username = args[0] || "";
|
|
1126
|
+
const instanceQuery = args[1] || "";
|
|
1127
|
+
if (!username && canPrompt()) username = syncPrompt(" GitHub username to invite: ");
|
|
1128
|
+
if (!username) die("usage: egregore /invite <github-username> [slug]");
|
|
1129
|
+
const inst = resolveInviteInstance(registry, instanceQuery);
|
|
1130
|
+
if (!inst) {
|
|
1131
|
+
die(`multiple egregores installed; choose one:\n egregore /invite ${username} <slug>\n run: egregore ls`);
|
|
1132
|
+
}
|
|
1133
|
+
return inviteToInstance(inst, username);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// ── tiny sync confirm (non-TUI flows) ───────────────────────────────
|
|
1137
|
+
|
|
1138
|
+
function syncConfirm(question) {
|
|
1139
|
+
process.stdout.write(question);
|
|
1140
|
+
// Synchronous read from stdin — blocks until the user hits Enter.
|
|
1141
|
+
const buf = Buffer.alloc(256);
|
|
1142
|
+
const n = fs.readSync(0, buf, 0, 256, null);
|
|
1143
|
+
const input = buf.slice(0, n).toString().trim();
|
|
1144
|
+
return /^y(es)?$/i.test(input);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// ── git info for preview ────────────────────────────────────────────
|
|
1148
|
+
|
|
1149
|
+
const previewCache = new Map(); // path → { branch, lastCommit, repos }
|
|
1150
|
+
|
|
1151
|
+
function loadSkillSummary(egregoreDir) {
|
|
1152
|
+
const skillsDir = path.join(egregoreDir, ".codex", "skills");
|
|
1153
|
+
const claudeSkillsDir = path.join(egregoreDir, ".claude", "skills");
|
|
1154
|
+
const summary = {
|
|
1155
|
+
names: [],
|
|
1156
|
+
native: [],
|
|
1157
|
+
adapterCount: 0,
|
|
1158
|
+
claudeNames: [],
|
|
1159
|
+
claudeCount: 0,
|
|
1160
|
+
};
|
|
1161
|
+
const loadNames = (dir) => {
|
|
1162
|
+
if (!fs.existsSync(dir)) return [];
|
|
1163
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
1164
|
+
.filter((entry) => entry.isDirectory())
|
|
1165
|
+
.map((entry) => entry.name)
|
|
1166
|
+
.filter((name) => fs.existsSync(path.join(dir, name, "SKILL.md")))
|
|
1167
|
+
.sort();
|
|
1168
|
+
};
|
|
1169
|
+
try {
|
|
1170
|
+
summary.names = loadNames(skillsDir);
|
|
1171
|
+
const adapters = [];
|
|
1172
|
+
for (const name of summary.names) {
|
|
1173
|
+
const skillFile = path.join(skillsDir, name, "SKILL.md");
|
|
1174
|
+
const body = fs.readFileSync(skillFile, "utf-8");
|
|
1175
|
+
if (body.includes("Fallback Codex adapter")) adapters.push(name);
|
|
1176
|
+
else summary.native.push(name);
|
|
1177
|
+
}
|
|
1178
|
+
summary.adapterCount = adapters.length;
|
|
1179
|
+
} catch {}
|
|
1180
|
+
try {
|
|
1181
|
+
summary.claudeNames = loadNames(claudeSkillsDir);
|
|
1182
|
+
summary.claudeCount = summary.claudeNames.length;
|
|
1183
|
+
} catch {}
|
|
1184
|
+
return summary;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function loadPreview(inst) {
|
|
1188
|
+
if (previewCache.has(inst.path)) return previewCache.get(inst.path);
|
|
1189
|
+
const data = { branch: null, lastCommit: null, repos: [], telegram: false, skills: loadSkillSummary(inst.path) };
|
|
1190
|
+
try {
|
|
1191
|
+
const r = spawnSync("git", ["-C", inst.path, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1192
|
+
stdio: "pipe", encoding: "utf-8", timeout: 2000,
|
|
1193
|
+
});
|
|
1194
|
+
if (r.status === 0) data.branch = (r.stdout || "").trim();
|
|
1195
|
+
} catch {}
|
|
1196
|
+
try {
|
|
1197
|
+
const r = spawnSync("git", ["-C", inst.path, "log", "-1", "--format=%cr · %s"], {
|
|
1198
|
+
stdio: "pipe", encoding: "utf-8", timeout: 2000,
|
|
1199
|
+
});
|
|
1200
|
+
if (r.status === 0) data.lastCommit = (r.stdout || "").trim();
|
|
1201
|
+
} catch {}
|
|
1202
|
+
try {
|
|
1203
|
+
const cfgPath = path.join(inst.path, "egregore.json");
|
|
1204
|
+
if (fs.existsSync(cfgPath)) {
|
|
1205
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
1206
|
+
data.repos = Array.isArray(cfg.repos)
|
|
1207
|
+
? cfg.repos.map((r) => (typeof r === "string" ? r : r && r.name)).filter(Boolean)
|
|
1208
|
+
: [];
|
|
1209
|
+
data.telegram = !!(cfg.telegram_chat_id || cfg.telegram_group_link);
|
|
1210
|
+
data.org = cfg.org_name;
|
|
1211
|
+
data.githubOrg = cfg.github_org;
|
|
1212
|
+
data.mode = cfg.mode;
|
|
1213
|
+
data.flowVariant = cfg.flow_variant;
|
|
1214
|
+
}
|
|
1215
|
+
} catch {}
|
|
1216
|
+
previewCache.set(inst.path, data);
|
|
1217
|
+
return data;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function selectedWorkflowLine(pv, width) {
|
|
1221
|
+
if (pv?.skills && pv.skills.native.length === 0) {
|
|
1222
|
+
return truncate("no Codex skills in this checkout", Math.max(12, width));
|
|
1223
|
+
}
|
|
1224
|
+
const available = pv?.skills?.native?.length
|
|
1225
|
+
? PRIMARY_CODEX_SKILLS.filter((name) => pv.skills.native.includes(name))
|
|
1226
|
+
: PRIMARY_CODEX_SKILLS;
|
|
1227
|
+
return joinShort(available.map(skillToken), Math.max(12, width));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, filterMode }) {
|
|
1231
|
+
const outerWidth = Math.min(Math.max(50, width - 2), 126);
|
|
1232
|
+
const innerWidth = Math.max(48, outerWidth - 2);
|
|
1233
|
+
const bodyWidth = Math.max(8, innerWidth - 2);
|
|
1234
|
+
const margin = " ".repeat(Math.max(0, Math.floor((width - outerWidth) / 2)));
|
|
1235
|
+
const label = " egregore ";
|
|
1236
|
+
const top = `${margin}${DIM}┌${"─".repeat(2)}${label}${"─".repeat(Math.max(0, innerWidth - label.length - 2))}┐${RESET}`;
|
|
1237
|
+
const bottom = `${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`;
|
|
1238
|
+
const mid = `${margin}${DIM}├${"─".repeat(innerWidth)}┤${RESET}`;
|
|
1239
|
+
const lines = [top];
|
|
1240
|
+
|
|
1241
|
+
const title = selected ? `${selected.slug || selected.name || "egregore"}` : "egregore";
|
|
1242
|
+
const org = pv?.org || selected?.name || "Egregore";
|
|
1243
|
+
const runtime = selected ? runtimeShort(runtimeFor(selected)) : "launcher";
|
|
1244
|
+
const skillsCount = pv?.skills?.names?.length || 0;
|
|
1245
|
+
const nativeCount = pv?.skills?.native?.length || 0;
|
|
1246
|
+
const adapterCount = pv?.skills?.adapterCount || 0;
|
|
1247
|
+
const claudeCount = pv?.skills?.claudeCount || 0;
|
|
1248
|
+
const workflows = selectedWorkflowLine(pv, innerWidth - 16);
|
|
1249
|
+
const filterText = filterMode
|
|
1250
|
+
? `filter /${filter}▌`
|
|
1251
|
+
: filter
|
|
1252
|
+
? `filter /${filter}`
|
|
1253
|
+
: "press ? for help";
|
|
1254
|
+
const canShowBigLogo = bodyWidth >= Math.max(...EGREGORE_ART.map((line) => line.length)) && height >= 24;
|
|
1255
|
+
const skillStatus = `Codex ${nativeCount} native · ${adapterCount} adapter · ${skillsCount} total · Claude ${claudeCount}`;
|
|
1256
|
+
const skillGroups = [
|
|
1257
|
+
`${DIM}core${RESET}: ${workflows}`,
|
|
1258
|
+
`${DIM}team${RESET}: ${joinShort(["/handoff", "/ask", "/invite", "/quest"], Math.max(16, bodyWidth - 8))}`,
|
|
1259
|
+
`${DIM}deeper${RESET}: ${joinShort(["/announce", "/harvest", "/the-spiral", "/deep-reflect"], Math.max(16, bodyWidth - 10))}`,
|
|
1260
|
+
`${DIM}fallback${RESET}: ${adapterCount} adapter skills`,
|
|
1261
|
+
];
|
|
1262
|
+
|
|
1263
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
1264
|
+
if (canShowBigLogo) {
|
|
1265
|
+
for (const artLine of EGREGORE_ART) {
|
|
1266
|
+
const centered = " ".repeat(Math.max(0, Math.floor((bodyWidth - artLine.length) / 2))) + `${BOLD}${CYAN}${artLine}${RESET}`;
|
|
1267
|
+
lines.push(boxLine(centered, innerWidth, margin));
|
|
1268
|
+
}
|
|
1269
|
+
} else {
|
|
1270
|
+
lines.push(boxLine(`${BOLD}${CYAN}EGREGORE${RESET} ${DIM}v${VERSION} · ${countSuffix} · ${filterText}${RESET}`, innerWidth, margin));
|
|
1271
|
+
}
|
|
1272
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
1273
|
+
lines.push(mid);
|
|
1274
|
+
lines.push(boxLine(`${BOLD}${title}${RESET} ${DIM}· ${org} · ${runtime} · ${countSuffix} · ${filterText}${RESET}`, innerWidth, margin));
|
|
1275
|
+
lines.push(boxLine(`${DIM}skills${RESET} ${skillStatus}`, innerWidth, margin));
|
|
1276
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
1277
|
+
|
|
1278
|
+
const sigilWidth = Math.max(...EGREGORE_SIGIL.map((line) => line.length));
|
|
1279
|
+
const twoColumn = bodyWidth >= 68;
|
|
1280
|
+
if (twoColumn) {
|
|
1281
|
+
const gap = 3;
|
|
1282
|
+
const rightWidth = Math.max(28, bodyWidth - sigilWidth - gap);
|
|
1283
|
+
const right = [
|
|
1284
|
+
`${BOLD}Available Skills${RESET}`,
|
|
1285
|
+
...skillGroups,
|
|
1286
|
+
"",
|
|
1287
|
+
`${BOLD}Assistants${RESET}`,
|
|
1288
|
+
`${DIM}claude-code${RESET}: ${commandExists("claude") ? "installed" : "not installed"}`,
|
|
1289
|
+
`${DIM}codex${RESET}: ${commandExists("codex") ? "installed" : "not installed"}`,
|
|
1290
|
+
];
|
|
1291
|
+
const rows = Math.max(EGREGORE_SIGIL.length + 2, right.length);
|
|
1292
|
+
for (let i = 0; i < rows; i++) {
|
|
1293
|
+
const sigil = i < EGREGORE_SIGIL.length ? `${CYAN}${pad(EGREGORE_SIGIL[i], sigilWidth)}${RESET}` : " ".repeat(sigilWidth);
|
|
1294
|
+
const info = right[i] || "";
|
|
1295
|
+
lines.push(boxLine(`${sigil}${" ".repeat(gap)}${truncate(info, rightWidth)}`, innerWidth, margin));
|
|
1296
|
+
}
|
|
1297
|
+
} else {
|
|
1298
|
+
lines.push(boxLine(`${BOLD}Available Skills${RESET}`, innerWidth, margin));
|
|
1299
|
+
for (const group of skillGroups) lines.push(boxLine(group, innerWidth, margin));
|
|
1300
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
1301
|
+
lines.push(boxLine(`${BOLD}Assistants${RESET} ${DIM}claude-code${RESET} ${commandExists("claude") ? "installed" : "missing"} · ${DIM}codex${RESET} ${commandExists("codex") ? "installed" : "missing"}`, innerWidth, margin));
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
1305
|
+
lines.push(boxLine(`${DIM}enter selects an instance, then choose Claude Code or Codex without leaving this screen${RESET}`, innerWidth, margin));
|
|
1306
|
+
lines.push(bottom);
|
|
1307
|
+
return lines;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function renderRuntimePanel({ inst, cursor, width, height }) {
|
|
1311
|
+
void height;
|
|
1312
|
+
const runtimes = runtimeChoicesFor(inst);
|
|
1313
|
+
const stored = storedRuntimeFor(inst);
|
|
1314
|
+
const title = inst.slug || inst.name || path.basename(inst.path);
|
|
1315
|
+
const outerWidth = Math.min(Math.max(50, width - 2), 112);
|
|
1316
|
+
const innerWidth = Math.max(48, outerWidth - 2);
|
|
1317
|
+
const margin = " ".repeat(Math.max(0, Math.floor((width - outerWidth) / 2)));
|
|
1318
|
+
const label = " assistant ";
|
|
1319
|
+
const lines = [
|
|
1320
|
+
`${margin}${DIM}┌${"─".repeat(2)}${label}${"─".repeat(Math.max(0, innerWidth - label.length - 2))}┐${RESET}`,
|
|
1321
|
+
emptyBoxLine(innerWidth, margin),
|
|
1322
|
+
boxLine(`${BOLD}${CYAN}Launch ${title}${RESET}`, innerWidth, margin),
|
|
1323
|
+
boxLine(`${DIM}${inst.path}${RESET}`, innerWidth, margin),
|
|
1324
|
+
emptyBoxLine(innerWidth, margin),
|
|
1325
|
+
];
|
|
1326
|
+
|
|
1327
|
+
for (let i = 0; i < runtimes.length; i++) {
|
|
1328
|
+
const runtime = runtimes[i];
|
|
1329
|
+
const selected = i === cursor;
|
|
1330
|
+
const marker = selected ? `${CYAN}❯${RESET}` : " ";
|
|
1331
|
+
const labelText = selected ? `${BOLD}${runtimeLabel(runtime)}${RESET}` : runtimeLabel(runtime);
|
|
1332
|
+
const desc = runtimeChoiceDescription(runtime, inst, stored);
|
|
1333
|
+
lines.push(boxLine(`${marker} ${pad(labelText, 16)} ${DIM}${desc}${RESET}`, innerWidth, margin));
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
1337
|
+
lines.push(boxLine(`${DIM}↑↓ navigate · enter select · 1-${runtimes.length} quick select · esc back · q quit${RESET}`, innerWidth, margin));
|
|
1338
|
+
lines.push(`${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`);
|
|
1339
|
+
return lines;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// ── browser (full-screen TUI) ───────────────────────────────────────
|
|
1343
|
+
|
|
1344
|
+
async function browser(args = {}) {
|
|
1345
|
+
let registry = readRegistry();
|
|
1346
|
+
if (registry.length === 0) {
|
|
1347
|
+
if (canPrompt()) return setupNewEgregore(args.runtimeOverride);
|
|
1348
|
+
console.log(` ${DIM}no egregores installed yet.${RESET}\n run ${CYAN}npx -y create-egregore@latest${RESET}`);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (!isTTY) {
|
|
1352
|
+
// Non-TTY fallback: numbered picker.
|
|
1353
|
+
return numberedPicker(registry, args.runtimeOverride);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// State
|
|
1357
|
+
const state = {
|
|
1358
|
+
filter: "",
|
|
1359
|
+
filterMode: false,
|
|
1360
|
+
inviteMode: false,
|
|
1361
|
+
inviteUsername: "",
|
|
1362
|
+
inviteInst: null,
|
|
1363
|
+
runtimeInst: null,
|
|
1364
|
+
runtimeCursor: 0,
|
|
1365
|
+
cursor: initialCursorForCwd(registry),
|
|
1366
|
+
showHelp: false,
|
|
1367
|
+
modal: null, // { type: "remove" | "prune", onYes, onNo }
|
|
1368
|
+
tick: 0,
|
|
1369
|
+
message: null, // ephemeral info line (e.g. "default set to X")
|
|
1370
|
+
messageKind: "info", // "info" | "success" | "warn"
|
|
1371
|
+
messageUntil: 0,
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
const newAction = {
|
|
1375
|
+
action: "new",
|
|
1376
|
+
label: "Set up a new Egregore",
|
|
1377
|
+
description: "guided install, Telegram, invite, assistant setup",
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
const visible = () => {
|
|
1381
|
+
const instanceItems = registry.map((e, i) => ({ type: "instance", e, origIdx: i }));
|
|
1382
|
+
if (!state.filter) return [...instanceItems, newAction];
|
|
1383
|
+
const q = state.filter.toLowerCase();
|
|
1384
|
+
const filtered = instanceItems
|
|
1385
|
+
.filter(({ e }) =>
|
|
1386
|
+
e.slug.toLowerCase().includes(q) ||
|
|
1387
|
+
(e.name || "").toLowerCase().includes(q) ||
|
|
1388
|
+
e.path.toLowerCase().includes(q),
|
|
1389
|
+
);
|
|
1390
|
+
if ("new setup create install another egregore".includes(q)) filtered.push(newAction);
|
|
1391
|
+
return filtered;
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
const clampCursor = () => {
|
|
1395
|
+
const v = visible();
|
|
1396
|
+
if (v.length === 0) { state.cursor = 0; return; }
|
|
1397
|
+
if (state.cursor >= v.length) state.cursor = v.length - 1;
|
|
1398
|
+
if (state.cursor < 0) state.cursor = 0;
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// ── render ────────────────────────────
|
|
1402
|
+
const render = () => {
|
|
1403
|
+
const w = process.stdout.columns || 80;
|
|
1404
|
+
const h = process.stdout.rows || 24;
|
|
1405
|
+
|
|
1406
|
+
const out = [];
|
|
1407
|
+
out.push(ANSI.home);
|
|
1408
|
+
|
|
1409
|
+
if (state.runtimeInst) {
|
|
1410
|
+
const runtimeLines = renderRuntimePanel({
|
|
1411
|
+
inst: state.runtimeInst,
|
|
1412
|
+
cursor: state.runtimeCursor,
|
|
1413
|
+
width: w,
|
|
1414
|
+
height: h,
|
|
1415
|
+
});
|
|
1416
|
+
for (const line of runtimeLines) out.push(line + ANSI.clearLine + "\n");
|
|
1417
|
+
const remainingRows = Math.max(0, h - runtimeLines.length - 1);
|
|
1418
|
+
for (let i = 0; i < remainingRows; i++) out.push(ANSI.clearLine + "\n");
|
|
1419
|
+
process.stdout.write(out.join(""));
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Header / hero panel
|
|
1424
|
+
const def = readDefault();
|
|
1425
|
+
const v = visible();
|
|
1426
|
+
const selectedItem = v[state.cursor];
|
|
1427
|
+
const selected = selectedItem?.e || null;
|
|
1428
|
+
const pv = selected ? loadPreview(selected) : null;
|
|
1429
|
+
const countSuffix = state.filter
|
|
1430
|
+
? `${v.length} of ${registry.length}`
|
|
1431
|
+
: `${registry.length} instance${registry.length === 1 ? "" : "s"}`;
|
|
1432
|
+
const heroLines = renderHeroPanel({
|
|
1433
|
+
selected,
|
|
1434
|
+
pv,
|
|
1435
|
+
width: w,
|
|
1436
|
+
height: h,
|
|
1437
|
+
countSuffix,
|
|
1438
|
+
filter: state.filter,
|
|
1439
|
+
filterMode: state.filterMode,
|
|
1440
|
+
});
|
|
1441
|
+
for (const line of heroLines) out.push(line + ANSI.clearLine + "\n");
|
|
1442
|
+
out.push(ANSI.clearLine + "\n");
|
|
1443
|
+
|
|
1444
|
+
// Layout: list takes top 60%, preview takes rest, footer is last 2 lines
|
|
1445
|
+
const footerRows = 2;
|
|
1446
|
+
const usedRows = heroLines.length + 1;
|
|
1447
|
+
const availableRows = Math.max(8, h - usedRows - footerRows - 2);
|
|
1448
|
+
const listRows = Math.max(4, Math.floor(availableRows * 0.48));
|
|
1449
|
+
const previewRows = Math.max(3, availableRows - listRows - 2); // -2 for dividers
|
|
1450
|
+
|
|
1451
|
+
// List
|
|
1452
|
+
const listPad = 4;
|
|
1453
|
+
const listAvail = w - listPad * 2;
|
|
1454
|
+
const slugW = Math.min(22, Math.max(6, ...registry.map((r) => (r.slug || "").length)));
|
|
1455
|
+
const nameW = Math.min(24, Math.max(6, ...registry.map((r) => (r.name || "").length)));
|
|
1456
|
+
|
|
1457
|
+
// Scroll window
|
|
1458
|
+
let scrollOff = 0;
|
|
1459
|
+
if (v.length > listRows) {
|
|
1460
|
+
// Keep cursor in view; center when possible.
|
|
1461
|
+
const half = Math.floor(listRows / 2);
|
|
1462
|
+
scrollOff = Math.max(0, Math.min(v.length - listRows, state.cursor - half));
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
for (let i = 0; i < listRows; i++) {
|
|
1466
|
+
const idx = scrollOff + i;
|
|
1467
|
+
if (idx >= v.length) {
|
|
1468
|
+
if (v.length === 0 && i === Math.floor(listRows / 2)) {
|
|
1469
|
+
const msg = state.filter ? "(no matches)" : "(empty)";
|
|
1470
|
+
out.push(" ".repeat(listPad) + DIM + msg + RESET + ANSI.clearLine + "\n");
|
|
1471
|
+
} else {
|
|
1472
|
+
out.push(ANSI.clearLine + "\n");
|
|
1473
|
+
}
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
const item = v[idx];
|
|
1477
|
+
if (item.action === "new") {
|
|
1478
|
+
const isCursor = idx === state.cursor;
|
|
1479
|
+
const cursorMark = isCursor ? `${CYAN}❯${RESET}` : " ";
|
|
1480
|
+
const plus = isCursor ? `${BOLD}${CYAN}+ new${RESET}` : `${CYAN}+ new${RESET}`;
|
|
1481
|
+
const label = isCursor ? `${BOLD}${item.label}${RESET}` : item.label;
|
|
1482
|
+
const desc = `${DIM}${item.description}${RESET}`;
|
|
1483
|
+
const line = ` ${cursorMark} ${pad(plus, slugW)} ${pad(label, nameW)} ${truncate(desc, Math.max(0, listAvail - slugW - nameW - 10))}`;
|
|
1484
|
+
out.push(" ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
const { e, origIdx } = item;
|
|
1488
|
+
void origIdx;
|
|
1489
|
+
const isCursor = idx === state.cursor;
|
|
1490
|
+
const isDefault = e.slug === def;
|
|
1491
|
+
const cursorMark = isCursor ? `${CYAN}❯${RESET}` : " ";
|
|
1492
|
+
const defMark = isDefault ? `${GREEN}●${RESET}` : " ";
|
|
1493
|
+
const slug = isCursor
|
|
1494
|
+
? `${BOLD}${CYAN}${pad(e.slug, slugW)}${RESET}`
|
|
1495
|
+
: (isDefault ? `${BOLD}${pad(e.slug, slugW)}${RESET}` : pad(e.slug, slugW));
|
|
1496
|
+
const name = pad(e.name || "", nameW);
|
|
1497
|
+
const pathDim = `${DIM}${truncate(e.path, Math.max(0, listAvail - slugW - nameW - 10))}${RESET}`;
|
|
1498
|
+
const line = ` ${cursorMark} ${defMark} ${slug} ${name} ${pathDim}`;
|
|
1499
|
+
out.push(" ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Divider
|
|
1503
|
+
out.push(DIM + " " + "─".repeat(w - 4) + RESET + ANSI.clearLine + "\n");
|
|
1504
|
+
|
|
1505
|
+
// Preview
|
|
1506
|
+
const previewLines = [];
|
|
1507
|
+
if (selectedItem?.action === "new") {
|
|
1508
|
+
const label = (k) => `${DIM}${pad(k, 10)}${RESET}`;
|
|
1509
|
+
previewLines.push(` ${label("action")} ${BOLD}Set up a new Egregore${RESET}`);
|
|
1510
|
+
previewLines.push(` ${label("flow")} guided install, Telegram, invite, ready/start`);
|
|
1511
|
+
previewLines.push(` ${label("assistant")} choose Claude Code or Codex during setup`);
|
|
1512
|
+
previewLines.push(` ${label("start")} press enter to begin`);
|
|
1513
|
+
} else if (!selected) {
|
|
1514
|
+
previewLines.push(`${DIM} (no selection)${RESET}`);
|
|
1515
|
+
} else {
|
|
1516
|
+
const label = (k) => `${DIM}${pad(k, 10)}${RESET}`;
|
|
1517
|
+
previewLines.push(` ${label("path")} ${selected.path}`);
|
|
1518
|
+
if (pv.org) previewLines.push(` ${label("org")} ${pv.org} ${DIM}(${pv.githubOrg || "?"})${RESET}`);
|
|
1519
|
+
if (pv.branch) previewLines.push(` ${label("branch")} ${BOLD}${pv.branch}${RESET}`);
|
|
1520
|
+
if (pv.lastCommit) previewLines.push(` ${label("last")} ${DIM}${truncate(pv.lastCommit, w - 18)}${RESET}`);
|
|
1521
|
+
if (pv.repos && pv.repos.length) {
|
|
1522
|
+
previewLines.push(` ${label("repos")} ${pv.repos.slice(0, 4).join(", ")}${pv.repos.length > 4 ? DIM + ` (+${pv.repos.length - 4} more)` + RESET : ""}`);
|
|
1523
|
+
}
|
|
1524
|
+
if (pv.skills) {
|
|
1525
|
+
const skillLine = selectedWorkflowLine(pv, Math.max(16, w - 20));
|
|
1526
|
+
previewLines.push(` ${label("invoke")} ${skillLine}`);
|
|
1527
|
+
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}`);
|
|
1528
|
+
}
|
|
1529
|
+
const modeBits = [];
|
|
1530
|
+
modeBits.push(runtimeShort(runtimeFor(selected, args.runtimeOverride)));
|
|
1531
|
+
if (pv.mode) modeBits.push(pv.mode);
|
|
1532
|
+
if (pv.flowVariant && pv.flowVariant !== "legacy") modeBits.push(pv.flowVariant);
|
|
1533
|
+
if (pv.telegram) modeBits.push("telegram");
|
|
1534
|
+
if (selected.slug === def) modeBits.push("default");
|
|
1535
|
+
if (modeBits.length) previewLines.push(` ${label("tags")} ${modeBits.map((m) => `${DIM}[${RESET}${m}${DIM}]${RESET}`).join(" ")}`);
|
|
1536
|
+
}
|
|
1537
|
+
for (let i = 0; i < previewRows; i++) {
|
|
1538
|
+
const line = previewLines[i] || "";
|
|
1539
|
+
out.push(line + ANSI.clearLine + "\n");
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// Divider
|
|
1543
|
+
out.push(DIM + " " + "─".repeat(w - 4) + RESET + ANSI.clearLine + "\n");
|
|
1544
|
+
|
|
1545
|
+
// Footer
|
|
1546
|
+
let footer;
|
|
1547
|
+
if (state.modal) {
|
|
1548
|
+
const m = state.modal;
|
|
1549
|
+
const prompt = m.type === "remove"
|
|
1550
|
+
? `remove ${BOLD}${m.inst.slug}${RESET}? ${DIM}[y/N]${RESET}`
|
|
1551
|
+
: `prune ${BOLD}${m.count}${RESET} stale entries? ${DIM}[y/N]${RESET}`;
|
|
1552
|
+
footer = ` ${YELLOW}?${RESET} ${prompt}${ANSI.clearLine}`;
|
|
1553
|
+
} else if (state.filterMode) {
|
|
1554
|
+
footer = ` ${DIM}typing filter · enter/esc to exit filter mode${RESET}${ANSI.clearLine}`;
|
|
1555
|
+
} else if (state.inviteMode) {
|
|
1556
|
+
const target = state.inviteInst?.slug || "selected";
|
|
1557
|
+
footer = ` ${CYAN}/invite${RESET} ${DIM}${target}:${RESET} ${state.inviteUsername}${CYAN}▌${RESET} ${DIM}enter send · esc cancel${RESET}${ANSI.clearLine}`;
|
|
1558
|
+
} else if (state.showHelp) {
|
|
1559
|
+
footer = ` ${DIM}? toggles this hint · q quits${RESET}${ANSI.clearLine}`;
|
|
1560
|
+
} else if (state.message && Date.now() < state.messageUntil) {
|
|
1561
|
+
const kindColor = state.messageKind === "success" ? GREEN : state.messageKind === "warn" ? YELLOW : CYAN;
|
|
1562
|
+
footer = ` ${kindColor}●${RESET} ${state.message}${ANSI.clearLine}`;
|
|
1563
|
+
} else {
|
|
1564
|
+
footer = w < 96
|
|
1565
|
+
? ` ${DIM}↑↓${RESET} nav ${DIM}enter${RESET} select ${DIM}n/+${RESET} new ${DIM}i${RESET} invite ${DIM}?${RESET} help ${DIM}q${RESET} quit${ANSI.clearLine}`
|
|
1566
|
+
: ` ${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}`;
|
|
1567
|
+
}
|
|
1568
|
+
out.push(footer + "\n");
|
|
1569
|
+
if (state.showHelp) {
|
|
1570
|
+
out.push(` ${DIM}● default · ❯ cursor · n/+ starts guided setup · i opens /invite${RESET}${ANSI.clearLine}`);
|
|
1571
|
+
} else {
|
|
1572
|
+
out.push(ANSI.clearLine);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
process.stdout.write(out.join(""));
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
// ── setup / teardown ───────────────────
|
|
1579
|
+
process.stdout.write(ANSI.altScreenEnter + ANSI.cursorHide + ANSI.clear + ANSI.home);
|
|
1580
|
+
let done = false;
|
|
1581
|
+
const cleanup = () => {
|
|
1582
|
+
if (done) return;
|
|
1583
|
+
done = true;
|
|
1584
|
+
process.stdin.setRawMode(false);
|
|
1585
|
+
process.stdin.pause();
|
|
1586
|
+
process.stdin.removeAllListeners("keypress");
|
|
1587
|
+
process.stdout.write(ANSI.cursorShow + ANSI.altScreenLeave);
|
|
1588
|
+
};
|
|
1589
|
+
process.on("exit", cleanup);
|
|
1590
|
+
process.on("SIGINT", () => { cleanup(); process.exit(130); });
|
|
1591
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(143); });
|
|
1592
|
+
process.stdout.on("resize", () => render());
|
|
1593
|
+
|
|
1594
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1595
|
+
process.stdin.setRawMode(true);
|
|
1596
|
+
process.stdin.resume();
|
|
1597
|
+
|
|
1598
|
+
const flashMessage = (msg, kind = "info", ms = 2200) => {
|
|
1599
|
+
state.message = msg;
|
|
1600
|
+
state.messageKind = kind;
|
|
1601
|
+
state.messageUntil = Date.now() + ms;
|
|
1602
|
+
render();
|
|
1603
|
+
setTimeout(() => {
|
|
1604
|
+
if (Date.now() >= state.messageUntil) render();
|
|
1605
|
+
}, ms + 50);
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
return new Promise((resolve) => {
|
|
1609
|
+
process.stdin.on("keypress", (chunk, key) => {
|
|
1610
|
+
if (!key) return;
|
|
1611
|
+
|
|
1612
|
+
if (state.runtimeInst) {
|
|
1613
|
+
const runtimes = runtimeChoicesFor(state.runtimeInst, args.runtimeOverride);
|
|
1614
|
+
if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
|
|
1615
|
+
if (key.name === "escape") {
|
|
1616
|
+
state.runtimeInst = null;
|
|
1617
|
+
state.runtimeCursor = 0;
|
|
1618
|
+
render();
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (key.name === "q") {
|
|
1622
|
+
cleanup();
|
|
1623
|
+
resolve();
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (key.name === "up" || (key.ctrl && key.name === "p") || key.name === "k") {
|
|
1627
|
+
state.runtimeCursor = (state.runtimeCursor - 1 + runtimes.length) % runtimes.length;
|
|
1628
|
+
render();
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if (key.name === "down" || (key.ctrl && key.name === "n") || key.name === "j") {
|
|
1632
|
+
state.runtimeCursor = (state.runtimeCursor + 1) % runtimes.length;
|
|
1633
|
+
render();
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (chunk && /^[1-9]$/.test(chunk)) {
|
|
1637
|
+
const idx = Number(chunk) - 1;
|
|
1638
|
+
if (idx >= 0 && idx < runtimes.length) {
|
|
1639
|
+
const inst = state.runtimeInst;
|
|
1640
|
+
cleanup();
|
|
1641
|
+
resolve(launch(inst, runtimes[idx]));
|
|
1642
|
+
}
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
if (key.name === "return") {
|
|
1646
|
+
const runtime = runtimes[state.runtimeCursor];
|
|
1647
|
+
const inst = state.runtimeInst;
|
|
1648
|
+
cleanup();
|
|
1649
|
+
resolve(launch(inst, runtime));
|
|
1650
|
+
}
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Modal handling takes priority
|
|
1655
|
+
if (state.modal) {
|
|
1656
|
+
if ((chunk && /^[yY]/.test(chunk))) {
|
|
1657
|
+
state.modal.onYes();
|
|
1658
|
+
state.modal = null;
|
|
1659
|
+
render();
|
|
1660
|
+
} else if (key.name === "escape" || (chunk && /^[nN]/.test(chunk)) || key.name === "return") {
|
|
1661
|
+
state.modal.onNo?.();
|
|
1662
|
+
state.modal = null;
|
|
1663
|
+
render();
|
|
1664
|
+
}
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Invite input mode
|
|
1669
|
+
if (state.inviteMode) {
|
|
1670
|
+
if (key.name === "escape") {
|
|
1671
|
+
state.inviteMode = false;
|
|
1672
|
+
state.inviteUsername = "";
|
|
1673
|
+
state.inviteInst = null;
|
|
1674
|
+
render();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (key.name === "return") {
|
|
1678
|
+
const username = state.inviteUsername.trim();
|
|
1679
|
+
const inst = state.inviteInst;
|
|
1680
|
+
if (!username || !inst) {
|
|
1681
|
+
state.inviteMode = false;
|
|
1682
|
+
state.inviteUsername = "";
|
|
1683
|
+
state.inviteInst = null;
|
|
1684
|
+
render();
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
cleanup();
|
|
1688
|
+
resolve(inviteToInstance(inst, username));
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (key.name === "backspace") {
|
|
1692
|
+
state.inviteUsername = state.inviteUsername.slice(0, -1);
|
|
1693
|
+
render();
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
if (chunk && chunk.length === 1 && chunk.charCodeAt(0) >= 32 && !key.ctrl && !key.meta) {
|
|
1697
|
+
state.inviteUsername += chunk;
|
|
1698
|
+
render();
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// Filter mode
|
|
1705
|
+
if (state.filterMode) {
|
|
1706
|
+
if (key.name === "escape") {
|
|
1707
|
+
state.filter = "";
|
|
1708
|
+
state.filterMode = false;
|
|
1709
|
+
clampCursor();
|
|
1710
|
+
render();
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
if (key.name === "return") {
|
|
1714
|
+
state.filterMode = false;
|
|
1715
|
+
render();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (key.name === "backspace") {
|
|
1719
|
+
state.filter = state.filter.slice(0, -1);
|
|
1720
|
+
clampCursor();
|
|
1721
|
+
render();
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (chunk && chunk.length === 1 && chunk.charCodeAt(0) >= 32 && !key.ctrl && !key.meta) {
|
|
1725
|
+
state.filter += chunk;
|
|
1726
|
+
state.cursor = 0;
|
|
1727
|
+
render();
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Normal mode
|
|
1734
|
+
if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
|
|
1735
|
+
if (key.name === "q" || key.name === "escape") {
|
|
1736
|
+
if (state.filter) {
|
|
1737
|
+
state.filter = "";
|
|
1738
|
+
clampCursor();
|
|
1739
|
+
render();
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
cleanup();
|
|
1743
|
+
resolve();
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
if (key.name === "up" || (key.ctrl && key.name === "p") || key.name === "k") {
|
|
1747
|
+
const v = visible();
|
|
1748
|
+
if (v.length > 0) state.cursor = (state.cursor - 1 + v.length) % v.length;
|
|
1749
|
+
render();
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (key.name === "down" || (key.ctrl && key.name === "n") || key.name === "j") {
|
|
1753
|
+
const v = visible();
|
|
1754
|
+
if (v.length > 0) state.cursor = (state.cursor + 1) % v.length;
|
|
1755
|
+
render();
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (key.name === "pageup") {
|
|
1759
|
+
const v = visible();
|
|
1760
|
+
if (v.length > 0) state.cursor = Math.max(0, state.cursor - 10);
|
|
1761
|
+
render();
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (key.name === "pagedown") {
|
|
1765
|
+
const v = visible();
|
|
1766
|
+
if (v.length > 0) state.cursor = Math.min(v.length - 1, state.cursor + 10);
|
|
1767
|
+
render();
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
if (key.name === "home" || (key.shift && key.name === "g")) {
|
|
1771
|
+
state.cursor = 0;
|
|
1772
|
+
render();
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (key.name === "end" || chunk === "G") {
|
|
1776
|
+
const v = visible();
|
|
1777
|
+
if (v.length > 0) state.cursor = v.length - 1;
|
|
1778
|
+
render();
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
if (key.name === "return") {
|
|
1782
|
+
const v = visible();
|
|
1783
|
+
const picked = v[state.cursor];
|
|
1784
|
+
if (picked?.action === "new") {
|
|
1785
|
+
cleanup();
|
|
1786
|
+
resolve(setupNewEgregore(args.runtimeOverride));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
const pick = picked?.e;
|
|
1790
|
+
if (!pick) return;
|
|
1791
|
+
if (args.runtimeOverride) {
|
|
1792
|
+
cleanup();
|
|
1793
|
+
resolve(launch(pick, args.runtimeOverride)); // replaces process
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
state.runtimeInst = pick;
|
|
1797
|
+
state.runtimeCursor = 0;
|
|
1798
|
+
render();
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (chunk === "/") {
|
|
1802
|
+
state.filterMode = true;
|
|
1803
|
+
render();
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (chunk === "n" || chunk === "+") {
|
|
1807
|
+
cleanup();
|
|
1808
|
+
resolve(setupNewEgregore(args.runtimeOverride));
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (chunk === "i") {
|
|
1812
|
+
const pick = visible()[state.cursor]?.e;
|
|
1813
|
+
if (!pick) return;
|
|
1814
|
+
state.inviteMode = true;
|
|
1815
|
+
state.inviteUsername = "";
|
|
1816
|
+
state.inviteInst = pick;
|
|
1817
|
+
render();
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (chunk === "u") {
|
|
1821
|
+
const pick = visible()[state.cursor]?.e;
|
|
1822
|
+
if (!pick) return;
|
|
1823
|
+
writeDefault(pick.slug);
|
|
1824
|
+
flashMessage(`default set to ${BOLD}${pick.slug}${RESET}`, "success");
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (chunk === "r") {
|
|
1828
|
+
const pick = visible()[state.cursor]?.e;
|
|
1829
|
+
if (!pick) return;
|
|
1830
|
+
state.modal = {
|
|
1831
|
+
type: "remove",
|
|
1832
|
+
inst: pick,
|
|
1833
|
+
onYes: () => {
|
|
1834
|
+
const bak = backupRegistry();
|
|
1835
|
+
registry = registry.filter((e) => e.path !== pick.path);
|
|
1836
|
+
writeRegistry(registry);
|
|
1837
|
+
previewCache.delete(pick.path);
|
|
1838
|
+
clampCursor();
|
|
1839
|
+
flashMessage(`removed ${BOLD}${pick.slug}${RESET} · backup: ${path.basename(bak)}`, "success");
|
|
1840
|
+
},
|
|
1841
|
+
};
|
|
1842
|
+
render();
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (chunk === "p") {
|
|
1846
|
+
const stale = registry.filter((e) => !fs.existsSync(e.path));
|
|
1847
|
+
if (stale.length === 0) {
|
|
1848
|
+
flashMessage(`registry is clean — all ${registry.length} paths exist.`, "success");
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
state.modal = {
|
|
1852
|
+
type: "prune",
|
|
1853
|
+
count: stale.length,
|
|
1854
|
+
onYes: () => {
|
|
1855
|
+
const bak = backupRegistry();
|
|
1856
|
+
registry = registry.filter((e) => fs.existsSync(e.path));
|
|
1857
|
+
writeRegistry(registry);
|
|
1858
|
+
clampCursor();
|
|
1859
|
+
flashMessage(`pruned ${stale.length} · ${registry.length} remain · backup: ${path.basename(bak)}`, "success");
|
|
1860
|
+
},
|
|
1861
|
+
};
|
|
1862
|
+
render();
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
if (chunk === "?") {
|
|
1866
|
+
state.showHelp = !state.showHelp;
|
|
1867
|
+
render();
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
render();
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// ── non-TTY fallback picker ─────────────────────────────────────────
|
|
1877
|
+
|
|
1878
|
+
async function numberedPicker(registry, runtimeOverride) {
|
|
1879
|
+
const def = readDefault();
|
|
1880
|
+
console.log("");
|
|
1881
|
+
console.log(` Multiple egregores installed. Pick one:\n`);
|
|
1882
|
+
for (let i = 0; i < registry.length; i++) {
|
|
1883
|
+
const inst = registry[i];
|
|
1884
|
+
const marker = inst.slug === def ? `${GREEN}●${RESET}` : " ";
|
|
1885
|
+
console.log(` ${marker} ${String(i + 1).padStart(2, " ")}) ${BOLD}${pad(inst.slug, 22)}${RESET} ${pad(inst.name || "", 24)}`);
|
|
1886
|
+
}
|
|
1887
|
+
console.log("");
|
|
1888
|
+
process.stdout.write(" Enter number or slug: ");
|
|
1889
|
+
const buf = Buffer.alloc(256);
|
|
1890
|
+
const n = fs.readSync(0, buf, 0, 256, null);
|
|
1891
|
+
const ans = buf.slice(0, n).toString().trim();
|
|
1892
|
+
if (!ans) die("no selection");
|
|
1893
|
+
const idx = parseInt(ans, 10);
|
|
1894
|
+
if (!isNaN(idx) && idx >= 1 && idx <= registry.length) return launch(registry[idx - 1], runtimeOverride);
|
|
1895
|
+
const hit = resolveBySlug(registry, ans);
|
|
1896
|
+
if (hit) return launch(hit, runtimeOverride);
|
|
1897
|
+
die(`no instance matching '${ans}'`);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// ── main ────────────────────────────────────────────────────────────
|
|
1901
|
+
|
|
1902
|
+
function main() {
|
|
1903
|
+
const parsed = parseRuntimeFlags(process.argv.slice(2));
|
|
1904
|
+
const args = parsed.args;
|
|
1905
|
+
const runtimeOverride = parsed.runtimeOverride;
|
|
1906
|
+
|
|
1907
|
+
const cmd = args[0] || "";
|
|
1908
|
+
|
|
1909
|
+
// Flags
|
|
1910
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") return cmdHelp();
|
|
1911
|
+
if (cmd === "--version" || cmd === "-V" || cmd === "version") {
|
|
1912
|
+
console.log(`egregore ${VERSION}`);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// Non-interactive subcommands
|
|
1917
|
+
if (cmd === "ls" || cmd === "list") return cmdLs();
|
|
1918
|
+
if (cmd === "use") return cmdUse(args[1]);
|
|
1919
|
+
if (cmd === "/invite" || cmd === "invite") return cmdInvite(args.slice(1));
|
|
1920
|
+
if (cmd === "prune") return cmdPruneNonInteractive(args[1] === "--dry-run" || args[1] === "-n");
|
|
1921
|
+
if (cmd === "rm" || cmd === "remove") return cmdRmNonInteractive(args[1]);
|
|
1922
|
+
if (cmd === "browse") return browser({ force: true, runtimeOverride });
|
|
1923
|
+
|
|
1924
|
+
// Anything starting with `-` that we didn't handle is unknown
|
|
1925
|
+
if (cmd.startsWith("-")) die(`unknown flag: ${cmd}\n run: egregore help`);
|
|
1926
|
+
|
|
1927
|
+
const registry = readRegistry();
|
|
1928
|
+
if (registry.length === 0) {
|
|
1929
|
+
if (canPrompt()) return setupNewEgregore(runtimeOverride);
|
|
1930
|
+
die("no egregores installed yet.\n run: npx -y create-egregore@latest");
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// Direct slug/name match fast path
|
|
1934
|
+
if (cmd) {
|
|
1935
|
+
const hit = resolveBySlug(registry, cmd);
|
|
1936
|
+
if (!hit) die(`no instance matching '${cmd}'. run: egregore ls`);
|
|
1937
|
+
return launch(hit, runtimeOverride);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// No args — in an interactive terminal, always keep the styled command
|
|
1941
|
+
// surface visible, even when only one instance is registered.
|
|
1942
|
+
if (canPrompt()) {
|
|
1943
|
+
return browser({ runtimeOverride });
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// Non-interactive fallback keeps old default/single-instance semantics.
|
|
1947
|
+
const def = readDefault();
|
|
1948
|
+
if (def) {
|
|
1949
|
+
const hit = registry.find((e) => e.slug === def);
|
|
1950
|
+
if (hit) return launch(hit, runtimeOverride);
|
|
1951
|
+
}
|
|
1952
|
+
if (registry.length === 1) return launch(registry[0], runtimeOverride);
|
|
1953
|
+
|
|
1954
|
+
// Multi-instance, no default — open the browser (TTY) or numbered picker (non-TTY).
|
|
1955
|
+
return browser({ runtimeOverride });
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
try {
|
|
1959
|
+
const r = main();
|
|
1960
|
+
if (r && typeof r.then === "function") {
|
|
1961
|
+
r.catch((err) => die(err.message || String(err)));
|
|
1962
|
+
}
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
die(err.message || String(err));
|
|
1965
|
+
}
|