create-egregore 0.17.3 → 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 +3 -3
- 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 +618 -193
- package/lib/setup.js +837 -85
- package/lib/ui.js +780 -84
- package/package.json +17 -4
- package/runtime/codex/.codex/config.toml +15 -0
- package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
- package/runtime/codex/.codex/hooks.json +17 -0
- package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
- package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
- package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
- package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/comment/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
- package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
- package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
- package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
- package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
- package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
- package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
- package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
- package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
- package/runtime/codex/.codex/spec-manifest.json +108 -0
- package/runtime/codex/AGENTS.md +328 -0
- package/runtime/codex/bin/activity-data.sh +180 -0
- package/runtime/codex/bin/agent.sh +669 -0
- package/runtime/codex/bin/codex-install-skills.sh +78 -0
- package/runtime/codex/bin/codex-render-spec.mjs +308 -0
- package/runtime/codex/bin/codex-session-start.sh +238 -0
- package/runtime/codex/bin/codex-skill-render.mjs +315 -0
- package/runtime/codex/bin/codex-sync-skills.sh +223 -0
- package/runtime/codex/bin/dashboard-data.sh +178 -0
- package/runtime/codex/bin/graph-batch.sh +60 -0
- package/runtime/codex/bin/graph-op.sh +397 -0
- package/runtime/codex/bin/graph-wal.sh +228 -0
- package/runtime/codex/bin/graph.sh +141 -0
- package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/codex/bin/handoff-run.sh +569 -0
- package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
- package/runtime/codex/bin/index-handoff.sh +306 -0
- package/runtime/codex/bin/lib/artifact-id.sh +46 -0
- package/runtime/codex/bin/lib/config.sh +71 -0
- package/runtime/codex/bin/lib/context.sh +511 -0
- package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
- package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
- package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
- package/runtime/codex/bin/lib/git-sync.sh +262 -0
- package/runtime/codex/bin/lib/greeting.sh +432 -0
- package/runtime/codex/bin/lib/hash.sh +13 -0
- package/runtime/codex/bin/lib/identity.sh +164 -0
- package/runtime/codex/bin/lib/permissions.sh +106 -0
- package/runtime/codex/bin/lib/time.sh +56 -0
- package/runtime/codex/bin/lib/worktree-links.sh +54 -0
- package/runtime/codex/bin/notify.sh +207 -0
- package/runtime/codex/bin/publish-artifact.sh +187 -0
- package/runtime/codex/bin/publish-references.sh +144 -0
- package/runtime/codex/bin/repo-state.sh +99 -0
- package/runtime/codex/bin/telemetry.sh +350 -0
- package/runtime/codex/bin/worktree-create.sh +115 -0
- package/runtime/codex/bin/worktree-remove.sh +50 -0
- package/runtime/codex/bin/worktree.sh +199 -0
- package/runtime/codex/manifest.json +162 -0
package/lib/setup.js
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
* Zero dependencies — uses node:child_process, node:fs, node:path, node:os.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { execSync, execFileSync } = require("node:child_process");
|
|
6
|
+
const { execSync, execFileSync, spawnSync, spawn } = require("node:child_process");
|
|
7
7
|
const fs = require("node:fs");
|
|
8
8
|
const path = require("node:path");
|
|
9
9
|
const os = require("node:os");
|
|
10
|
+
const {
|
|
11
|
+
hasCodexRuntimeSupport,
|
|
12
|
+
installCodexRuntime,
|
|
13
|
+
reportCodexRuntimeSummary,
|
|
14
|
+
} = require("./codex-runtime");
|
|
10
15
|
|
|
11
16
|
const https = require("node:https");
|
|
12
17
|
|
|
@@ -57,7 +62,9 @@ function ghApi(method, apiPath, token, body) {
|
|
|
57
62
|
function initMemoryDirs(memoryDir) {
|
|
58
63
|
const dirs = [
|
|
59
64
|
"people",
|
|
65
|
+
"invites",
|
|
60
66
|
"handoffs",
|
|
67
|
+
"knowledge/questions",
|
|
61
68
|
"knowledge/decisions",
|
|
62
69
|
"knowledge/patterns",
|
|
63
70
|
"knowledge/findings",
|
|
@@ -96,6 +103,202 @@ async function acceptPendingInvitations(githubToken, githubOrg, ui) {
|
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
// Normalize a git URL for equality comparison: strip protocol, .git suffix,
|
|
107
|
+
// SSH-vs-HTTPS distinction. github.com:org/repo / https://github.com/org/repo
|
|
108
|
+
// / git@github.com:org/repo all reduce to the same canonical form.
|
|
109
|
+
function normalizeGitUrl(url) {
|
|
110
|
+
if (!url) return "";
|
|
111
|
+
let u = url.trim().replace(/\.git$/i, "");
|
|
112
|
+
u = u.replace(/^git@([^:]+):/, "$1/");
|
|
113
|
+
u = u.replace(/^https?:\/\//i, "");
|
|
114
|
+
return u.toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function gitRemoteMatches(repoDir, expectedUrl) {
|
|
118
|
+
try {
|
|
119
|
+
const url = execFileSync("git", ["-C", repoDir, "remote", "get-url", "origin"], {
|
|
120
|
+
stdio: "pipe", encoding: "utf-8", timeout: 5000,
|
|
121
|
+
}).trim();
|
|
122
|
+
return normalizeGitUrl(url) === normalizeGitUrl(expectedUrl);
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// True only when every artifact the installer would produce is already in
|
|
129
|
+
// place AND pointing at the right remotes. Used to short-circuit a re-run
|
|
130
|
+
// of the clone/symlink steps when the user is just rejoining what they
|
|
131
|
+
// already have on disk.
|
|
132
|
+
function isFullySetUp(egregoreDir, memoryDir, forkUrl, memoryUrl) {
|
|
133
|
+
if (!fs.existsSync(path.join(egregoreDir, ".git"))) return false;
|
|
134
|
+
if (!gitRemoteMatches(egregoreDir, forkUrl)) return false;
|
|
135
|
+
if (!fs.existsSync(path.join(memoryDir, ".git"))) return false;
|
|
136
|
+
if (!gitRemoteMatches(memoryDir, memoryUrl)) return false;
|
|
137
|
+
if (!fs.existsSync(path.join(egregoreDir, "memory"))) return false;
|
|
138
|
+
if (!fs.existsSync(path.join(egregoreDir, ".env"))) return false;
|
|
139
|
+
if (!fs.existsSync(path.join(egregoreDir, "egregore.json"))) return false;
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function slugifyName(name) {
|
|
144
|
+
return String(name || "")
|
|
145
|
+
.trim()
|
|
146
|
+
.toLowerCase()
|
|
147
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
148
|
+
.replace(/-+/g, "-")
|
|
149
|
+
.replace(/^-|-$/g, "");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeUsageType(data = {}) {
|
|
153
|
+
if (data.usage_type === "founder_group" || data.usage_type === "joiner_group") {
|
|
154
|
+
return data.usage_type;
|
|
155
|
+
}
|
|
156
|
+
if (data.onboarding_role === "founder" || data.role === "founder") return "founder_group";
|
|
157
|
+
return "joiner_group";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function mergeProfileFields(existing, additions = []) {
|
|
161
|
+
const fields = Array.isArray(existing) ? existing.filter(Boolean) : [];
|
|
162
|
+
if (!fields.includes("name")) fields.push("name");
|
|
163
|
+
for (const field of additions) {
|
|
164
|
+
if (field && !fields.includes(field)) fields.push(field);
|
|
165
|
+
}
|
|
166
|
+
return fields;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function personProfileContent(state = {}, opts = {}) {
|
|
170
|
+
const now = opts.onboardedAt || new Date().toISOString();
|
|
171
|
+
const username = state.github_username || opts.githubUsername || "unknown";
|
|
172
|
+
const displayName = state.display_name || state.github_name || username;
|
|
173
|
+
const role = state.member_role || state.onboarding?.member_role || opts.memberRole || "Member";
|
|
174
|
+
return [
|
|
175
|
+
`# ${displayName}`,
|
|
176
|
+
"",
|
|
177
|
+
`GitHub: ${username}`,
|
|
178
|
+
`Role: ${role}`,
|
|
179
|
+
`Joined: ${now.split("T")[0]}`,
|
|
180
|
+
`Onboarded: ${now}`,
|
|
181
|
+
"",
|
|
182
|
+
].join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function ensurePersonOnboardingWitness(memoryDir, state = {}, opts = {}) {
|
|
186
|
+
const username = state.github_username || opts.githubUsername;
|
|
187
|
+
if (!memoryDir || !username) return null;
|
|
188
|
+
const peopleDir = path.join(memoryDir, "people");
|
|
189
|
+
fs.mkdirSync(peopleDir, { recursive: true });
|
|
190
|
+
const personFile = path.join(peopleDir, `${username}.md`);
|
|
191
|
+
const witness = personProfileContent(state, opts);
|
|
192
|
+
let next = witness;
|
|
193
|
+
|
|
194
|
+
if (fs.existsSync(personFile)) {
|
|
195
|
+
const current = fs.readFileSync(personFile, "utf-8");
|
|
196
|
+
if (/^Onboarded:/m.test(current)) return personFile;
|
|
197
|
+
if (/^---\s*$/m.test(current) && current.trimStart().startsWith("---")) {
|
|
198
|
+
const body = current.match(/^---[\s\S]*?---\s*\n([\s\S]*)$/m)?.[1]?.trim();
|
|
199
|
+
next = body ? `${witness}\n${body}\n` : witness;
|
|
200
|
+
} else if (current.trimStart().startsWith("# ")) {
|
|
201
|
+
next = `${current.replace(/\s*$/, "")}\nOnboarded: ${opts.onboardedAt || new Date().toISOString()}\n`;
|
|
202
|
+
} else if (current.trim()) {
|
|
203
|
+
next = `${witness}\n${current.trim()}\n`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fs.writeFileSync(personFile, next);
|
|
208
|
+
try {
|
|
209
|
+
const rel = path.relative(memoryDir, personFile);
|
|
210
|
+
run(`git add ${JSON.stringify(rel)}`, { cwd: memoryDir });
|
|
211
|
+
run(`git commit -m ${JSON.stringify(`Onboard ${username}`)}`, { cwd: memoryDir });
|
|
212
|
+
run("git push", { cwd: memoryDir });
|
|
213
|
+
} catch {}
|
|
214
|
+
return personFile;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function ensureInstallerMemberRole(data = {}, existingState = {}, ui) {
|
|
218
|
+
const existing = data.member_role || existingState.member_role || existingState.onboarding?.member_role;
|
|
219
|
+
if (existing) return existing;
|
|
220
|
+
const usageType = normalizeUsageType(data);
|
|
221
|
+
const fallback = usageType === "founder_group" ? "Founder" : "Member";
|
|
222
|
+
if (!process.stdin.isTTY) return fallback;
|
|
223
|
+
console.log("");
|
|
224
|
+
const answer = await ui.prompt("What's your role here?", { default: fallback });
|
|
225
|
+
return answer || fallback;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildOnboardingState(existing = {}, data = {}, opts = {}) {
|
|
229
|
+
const state = { ...existing };
|
|
230
|
+
const preferredRuntime = normalizeRuntime(opts.runtime);
|
|
231
|
+
const githubUsername = data.github_username || state.github_username || "";
|
|
232
|
+
const githubName = data.github_name || state.github_name || githubUsername;
|
|
233
|
+
const displayName = data.display_name || state.display_name || githubName || githubUsername;
|
|
234
|
+
const usageType = normalizeUsageType(data);
|
|
235
|
+
const memberRole = data.member_role || state.member_role || state.onboarding?.member_role || "";
|
|
236
|
+
const requestedComplete =
|
|
237
|
+
opts.onboardingComplete === true ||
|
|
238
|
+
data.onboarding_complete === true ||
|
|
239
|
+
data.onboarding_complete === "true";
|
|
240
|
+
const alreadyComplete =
|
|
241
|
+
state.onboarding_complete === true ||
|
|
242
|
+
state.onboarding_complete === "true";
|
|
243
|
+
const shouldComplete = alreadyComplete || (requestedComplete && !!memberRole);
|
|
244
|
+
|
|
245
|
+
if (githubUsername) state.github_username = githubUsername;
|
|
246
|
+
if (githubName) state.github_name = githubName;
|
|
247
|
+
if (displayName) {
|
|
248
|
+
state.display_name = displayName;
|
|
249
|
+
state.name = state.name || slugifyName(displayName) || githubUsername;
|
|
250
|
+
}
|
|
251
|
+
if (memberRole) state.member_role = memberRole;
|
|
252
|
+
if (data.github_email || data.email) state.email = data.github_email || data.email;
|
|
253
|
+
|
|
254
|
+
state.onboarding_complete = shouldComplete;
|
|
255
|
+
state.usage_type = usageType;
|
|
256
|
+
state.org_setup = true;
|
|
257
|
+
state.github_configured = true;
|
|
258
|
+
state.workspace_ready = true;
|
|
259
|
+
if (data.transcript_sharing !== undefined) {
|
|
260
|
+
state.transcript_sharing = data.transcript_sharing;
|
|
261
|
+
}
|
|
262
|
+
if (opts.flowVariant) {
|
|
263
|
+
state.flow_variant = opts.flowVariant;
|
|
264
|
+
} else if (data.flow_variant) {
|
|
265
|
+
state.flow_variant = data.flow_variant;
|
|
266
|
+
}
|
|
267
|
+
state.preferred_runtime = preferredRuntime;
|
|
268
|
+
state.profile_fields_collected = mergeProfileFields(state.profile_fields_collected, memberRole ? ["role"] : []);
|
|
269
|
+
state.onboarding = state.onboarding || {};
|
|
270
|
+
if (memberRole) state.onboarding.member_role = memberRole;
|
|
271
|
+
if (state.onboarding_complete) {
|
|
272
|
+
state.onboarding.phase = "complete";
|
|
273
|
+
state.onboarding.completed_at = state.onboarding.completed_at || new Date().toISOString();
|
|
274
|
+
} else {
|
|
275
|
+
state.onboarding.phase = state.onboarding.phase || "orient";
|
|
276
|
+
state.onboarding.started_at = state.onboarding.started_at || new Date().toISOString();
|
|
277
|
+
}
|
|
278
|
+
state.onboarding.installer_captured = true;
|
|
279
|
+
state.onboarding.invite_handled_by =
|
|
280
|
+
data.invite_handled_by || state.onboarding.invite_handled_by || "installer";
|
|
281
|
+
if (data.invite_skipped !== undefined) {
|
|
282
|
+
state.onboarding.invite_skipped = !!data.invite_skipped;
|
|
283
|
+
} else if (usageType === "founder_group" && state.onboarding.invite_skipped === undefined) {
|
|
284
|
+
state.onboarding.invite_skipped = true;
|
|
285
|
+
}
|
|
286
|
+
if (data.onboarding_role || data.role) {
|
|
287
|
+
state.onboarding.role = data.onboarding_role || data.role;
|
|
288
|
+
}
|
|
289
|
+
return state;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function ensureInstallerDisplayName(data, existingState, ui) {
|
|
293
|
+
if (data.display_name || existingState.display_name) return data.display_name || existingState.display_name;
|
|
294
|
+
const fallback = data.github_name || data.github_username;
|
|
295
|
+
if (!fallback) return "";
|
|
296
|
+
if (!process.stdin.isTTY) return fallback;
|
|
297
|
+
console.log("");
|
|
298
|
+
const answer = await ui.prompt(`What should we call you? [${fallback}]:`);
|
|
299
|
+
return answer || fallback;
|
|
300
|
+
}
|
|
301
|
+
|
|
99
302
|
/**
|
|
100
303
|
* Full local setup from claimed token data.
|
|
101
304
|
*
|
|
@@ -108,11 +311,12 @@ async function acceptPendingInvitations(githubToken, githubOrg, ui) {
|
|
|
108
311
|
* avoid clobbering uncommitted work or pushing branches to their origin.
|
|
109
312
|
*/
|
|
110
313
|
async function install(data, ui, targetDir, opts = {}) {
|
|
111
|
-
const { fork_url, memory_url, github_token, org_name, github_org, slug, api_key, repos = [], telegram_group_link, github_username, github_name
|
|
314
|
+
const { fork_url, memory_url, github_token, org_name, github_org, slug, api_key, repos = [], telegram_group_link, github_username, github_name } = data;
|
|
112
315
|
const base = targetDir || process.cwd();
|
|
113
316
|
const skipRepos = opts.skipRepos instanceof Set ? opts.skipRepos : new Set();
|
|
317
|
+
const preferredRuntime = normalizeRuntime(opts.runtime);
|
|
114
318
|
|
|
115
|
-
// Directory name from fork URL (what git clone would use), e.g. "egregore
|
|
319
|
+
// Directory name from fork URL (what git clone would use), e.g. "egregore"
|
|
116
320
|
const forkDirName = fork_url.split("/").pop().replace(/\.git$/, "");
|
|
117
321
|
const egregoreDir = path.join(base, forkDirName);
|
|
118
322
|
const memoryDirName = memory_url
|
|
@@ -123,67 +327,80 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
123
327
|
|
|
124
328
|
const totalSteps = 5 + repos.length;
|
|
125
329
|
|
|
330
|
+
// Detect a complete prior install at this target so "Join existing" doesn't
|
|
331
|
+
// re-walk the 7-step setup for a user who already has everything wired up.
|
|
332
|
+
// We still rewrite credentials / state / register (steps 4–5) so tokens
|
|
333
|
+
// refresh and the registry entry is current — only the noisy clone+symlink
|
|
334
|
+
// steps are skipped. Falls through to the normal flow if anything is off.
|
|
335
|
+
const alreadyInstalled = isFullySetUp(egregoreDir, memoryDir, fork_url, memory_url);
|
|
336
|
+
|
|
126
337
|
// Accept pending repo collaboration invitations (joiners get invited as collaborators)
|
|
338
|
+
if (github_token) configureGitCredentials(github_token);
|
|
127
339
|
await acceptPendingInvitations(github_token, github_org, ui);
|
|
128
340
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
ui.warn("egregore/ already exists — pulling latest");
|
|
133
|
-
run("git pull", { cwd: egregoreDir });
|
|
341
|
+
if (alreadyInstalled) {
|
|
342
|
+
ui.success(`Already installed at ${ui.bold(egregoreDir)} — refreshing setup`);
|
|
343
|
+
console.log("");
|
|
134
344
|
} else {
|
|
135
|
-
|
|
136
|
-
|
|
345
|
+
// 1. Clone fork (SSH first, HTTPS fallback — no token embedding)
|
|
346
|
+
ui.step(1, totalSteps, "Cloning egregore...");
|
|
347
|
+
if (fs.existsSync(egregoreDir)) {
|
|
348
|
+
ui.warn("egregore/ already exists — pulling latest");
|
|
349
|
+
run("git pull", { cwd: egregoreDir });
|
|
350
|
+
} else {
|
|
351
|
+
if (!cloneRepo(fork_url, egregoreDir)) {
|
|
352
|
+
throw new Error(`Command failed: git clone ${fork_url} ${egregoreDir}\nCloning into '${egregoreDir}'...\nMake sure you have SSH keys set up or run 'gh auth login' first.`);
|
|
353
|
+
}
|
|
137
354
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
ui.success("Cloned egregore");
|
|
355
|
+
try { run("git config credential.helper store", { cwd: egregoreDir }); } catch {}
|
|
356
|
+
ensureDevelop(egregoreDir);
|
|
357
|
+
ui.success("Cloned egregore");
|
|
142
358
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
359
|
+
// Set repo-local git identity from GitHub user (not machine-global config)
|
|
360
|
+
if (github_username) {
|
|
361
|
+
try {
|
|
362
|
+
run(`git config user.name "${github_name || github_username}"`, { cwd: egregoreDir });
|
|
363
|
+
run(`git config user.email "${github_username}@users.noreply.github.com"`, { cwd: egregoreDir });
|
|
364
|
+
} catch {
|
|
365
|
+
// Non-fatal
|
|
366
|
+
}
|
|
150
367
|
}
|
|
151
|
-
}
|
|
152
368
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
369
|
+
// 2. Clone memory
|
|
370
|
+
ui.step(2, totalSteps, "Cloning shared memory...");
|
|
371
|
+
if (fs.existsSync(memoryDir)) {
|
|
372
|
+
ui.warn(`${memoryDirName}/ already exists — pulling latest`);
|
|
373
|
+
run("git pull", { cwd: memoryDir });
|
|
374
|
+
} else {
|
|
375
|
+
if (!cloneRepo(memory_url, memoryDir)) {
|
|
376
|
+
ui.warn(`Could not clone memory repo — you may need SSH keys or 'gh auth login'`);
|
|
377
|
+
}
|
|
161
378
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
ui.success("Cloned memory");
|
|
379
|
+
try { run("git config credential.helper store", { cwd: memoryDir }); } catch {}
|
|
380
|
+
if (github_username) {
|
|
381
|
+
try {
|
|
382
|
+
run(`git config user.name "${github_name || github_username}"`, { cwd: memoryDir });
|
|
383
|
+
run(`git config user.email "${github_username}@users.noreply.github.com"`, { cwd: memoryDir });
|
|
384
|
+
} catch {}
|
|
385
|
+
}
|
|
386
|
+
ui.success("Cloned memory");
|
|
171
387
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} else {
|
|
178
|
-
if (process.platform === "win32") {
|
|
179
|
-
// Junctions require absolute paths on Windows
|
|
180
|
-
fs.symlinkSync(path.resolve(memoryDir), symlinkTarget, "junction");
|
|
388
|
+
// 3. Symlink (use junction on Windows — no admin required)
|
|
389
|
+
ui.step(3, totalSteps, "Linking memory...");
|
|
390
|
+
const symlinkTarget = path.join(egregoreDir, "memory");
|
|
391
|
+
if (fs.existsSync(symlinkTarget)) {
|
|
392
|
+
ui.warn("memory/ symlink already exists");
|
|
181
393
|
} else {
|
|
182
|
-
|
|
183
|
-
|
|
394
|
+
if (process.platform === "win32") {
|
|
395
|
+
// Junctions require absolute paths on Windows
|
|
396
|
+
fs.symlinkSync(path.resolve(memoryDir), symlinkTarget, "junction");
|
|
397
|
+
} else {
|
|
398
|
+
const relPath = path.relative(egregoreDir, memoryDir);
|
|
399
|
+
fs.symlinkSync(relPath, symlinkTarget);
|
|
400
|
+
}
|
|
184
401
|
}
|
|
402
|
+
ui.success("Linked");
|
|
185
403
|
}
|
|
186
|
-
ui.success("Linked");
|
|
187
404
|
|
|
188
405
|
// 4. Write .env (secrets only — never committed to git)
|
|
189
406
|
ui.step(4, totalSteps, "Writing credentials...");
|
|
@@ -199,19 +416,15 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
199
416
|
if (fs.existsSync(statePath)) {
|
|
200
417
|
try { state = JSON.parse(fs.readFileSync(statePath, "utf-8")); } catch {}
|
|
201
418
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
state.github_configured = true;
|
|
210
|
-
state.workspace_ready = true;
|
|
211
|
-
if (transcript_sharing !== undefined) {
|
|
212
|
-
state.transcript_sharing = transcript_sharing;
|
|
213
|
-
}
|
|
419
|
+
const displayName = await ensureInstallerDisplayName(data, state, ui);
|
|
420
|
+
const memberRole = await ensureInstallerMemberRole(data, state, ui);
|
|
421
|
+
state = buildOnboardingState(state, { ...data, display_name: displayName, member_role: memberRole }, {
|
|
422
|
+
runtime: preferredRuntime,
|
|
423
|
+
flowVariant: opts.flowVariant,
|
|
424
|
+
onboardingComplete: opts.onboardingComplete !== false,
|
|
425
|
+
});
|
|
214
426
|
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
427
|
+
try { ensurePersonOnboardingWitness(memoryDir, state); } catch {}
|
|
215
428
|
|
|
216
429
|
// 4c. Ensure mode is set in egregore.json (connected mode for API-based setup)
|
|
217
430
|
const configPath = path.join(egregoreDir, "egregore.json");
|
|
@@ -225,10 +438,15 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
225
438
|
} catch {}
|
|
226
439
|
}
|
|
227
440
|
|
|
228
|
-
//
|
|
441
|
+
// 4d. Overlay packaged Codex support before registering or launching. This
|
|
442
|
+
// makes cloned public templates usable even before the public repo syncs.
|
|
443
|
+
const codexRuntime = installCodexRuntime(egregoreDir);
|
|
444
|
+
reportCodexRuntimeSummary(codexRuntime, ui);
|
|
445
|
+
|
|
446
|
+
// 5. Register instance + install launcher binary
|
|
229
447
|
ui.step(5, totalSteps, "Registering instance...");
|
|
230
|
-
registerInstance(forkDirName, org_name, egregoreDir);
|
|
231
|
-
const alias = await
|
|
448
|
+
registerInstance(forkDirName, org_name, egregoreDir, preferredRuntime);
|
|
449
|
+
const alias = await installLauncherBinary(egregoreDir, ui);
|
|
232
450
|
|
|
233
451
|
// 6+. Clone managed repos (if any)
|
|
234
452
|
const clonedRepos = [];
|
|
@@ -264,24 +482,59 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
264
482
|
}
|
|
265
483
|
}
|
|
266
484
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
485
|
+
// Warm the npx cache for egregore-artifacts so the first /view or
|
|
486
|
+
// publish-artifact call doesn't pay a cold npm download (~5-10s).
|
|
487
|
+
// Background + detached — we don't care about completion, and any
|
|
488
|
+
// failure is silent. The first real use benefits if this finishes in
|
|
489
|
+
// time, otherwise it costs nothing.
|
|
490
|
+
try {
|
|
491
|
+
const { spawn } = require("child_process");
|
|
492
|
+
const warm = spawn("npx", ["-y", "egregore-artifacts@latest", "--help"], {
|
|
493
|
+
detached: true,
|
|
494
|
+
stdio: "ignore",
|
|
495
|
+
});
|
|
496
|
+
warm.unref();
|
|
497
|
+
} catch { /* ignore — cosmetic warmup */ }
|
|
498
|
+
|
|
499
|
+
// Done — full-screen summary bookend if we can; inline workspace list
|
|
500
|
+
// otherwise. Same contract as localFounderFlow.
|
|
501
|
+
const workspaces = [
|
|
502
|
+
{ label: `./${forkDirName}/`, description: "your Egregore instance" },
|
|
503
|
+
{ label: `./${memoryDirName}/`, description: "shared knowledge" },
|
|
504
|
+
...clonedRepos.map((rn) => ({ label: `./${rn}/`, description: "managed repo" })),
|
|
505
|
+
];
|
|
277
506
|
if (telegram_group_link) {
|
|
507
|
+
workspaces.push({ label: telegram_group_link, description: "telegram group" });
|
|
508
|
+
}
|
|
509
|
+
const usageType = normalizeUsageType(data);
|
|
510
|
+
const setupSummary = {
|
|
511
|
+
workspace: "created",
|
|
512
|
+
invite: usageType === "joiner_group" ? "accepted" : (data.invite_skipped ? "skipped" : "handled"),
|
|
513
|
+
telegram: telegram_group_link ? "connected" : "skipped",
|
|
514
|
+
assistant: runtimeDisplay(preferredRuntime),
|
|
515
|
+
command: "run egregore anytime",
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const ui_ = require("./ui");
|
|
519
|
+
if (!opts.noScreens && ui_.canShowScreens()) {
|
|
520
|
+
const result = await ui_.summaryScreen({ orgName: org_name, workspaces, setup: setupSummary });
|
|
521
|
+
if (result === "escape") opts.noAutolaunch = true;
|
|
522
|
+
} else {
|
|
523
|
+
ui.section("You're done", `Egregore is ready for ${ui.bold(org_name)}`);
|
|
524
|
+
ui.info(`${ui.green("●")} workspace ${setupSummary.workspace}`);
|
|
525
|
+
ui.info(`${ui.green("●")} invite ${setupSummary.invite}`);
|
|
526
|
+
ui.info(`${ui.green("●")} Telegram ${setupSummary.telegram}`);
|
|
527
|
+
ui.info(`${ui.green("●")} assistant ${setupSummary.assistant}`);
|
|
528
|
+
ui.info(`${ui.green("●")} next ${setupSummary.command}`);
|
|
529
|
+
console.log("");
|
|
530
|
+
ui.info(`Your workspace:`);
|
|
531
|
+
for (const ws of workspaces) {
|
|
532
|
+
ui.info(` ${ui.cyan(ws.label)} ${ui.dim(ws.description || "")}`);
|
|
533
|
+
}
|
|
278
534
|
console.log("");
|
|
279
|
-
ui.info(`Join the Telegram group for notifications:`);
|
|
280
|
-
ui.info(` ${ui.cyan(telegram_group_link)}`);
|
|
281
535
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
console.log("");
|
|
536
|
+
|
|
537
|
+
await launchOrInstruct(path.join(base, forkDirName), alias, ui, opts);
|
|
285
538
|
}
|
|
286
539
|
|
|
287
540
|
/**
|
|
@@ -342,13 +595,28 @@ function toSSH(url) {
|
|
|
342
595
|
return m ? `git@github.com:${m[1]}.git` : null;
|
|
343
596
|
}
|
|
344
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Environment for git clone/fetch during install. GIT_TERMINAL_PROMPT=0
|
|
600
|
+
* stops the hanging username/password prompt; BatchMode makes the SSH-first
|
|
601
|
+
* attempt fail fast on machines without keys instead of blocking the install
|
|
602
|
+
* on an interactive "Are you sure you want to continue connecting?" host-key
|
|
603
|
+
* prompt mid-spinner (fresh machines have no github.com in known_hosts).
|
|
604
|
+
*/
|
|
605
|
+
function gitCloneEnv(base = process.env) {
|
|
606
|
+
return {
|
|
607
|
+
...base,
|
|
608
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
609
|
+
GIT_SSH_COMMAND: base.GIT_SSH_COMMAND || "ssh -oBatchMode=yes",
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
345
613
|
/**
|
|
346
614
|
* Clone a repo, trying SSH first then HTTPS (no token embedded).
|
|
347
615
|
* The repo:invite-scoped OAuth token can't clone — we rely on the
|
|
348
616
|
* user's own git credentials (SSH keys / gh CLI / credential helper).
|
|
349
617
|
*/
|
|
350
618
|
function cloneRepo(httpsUrl, dir, timeout = 60000) {
|
|
351
|
-
const env =
|
|
619
|
+
const env = gitCloneEnv();
|
|
352
620
|
const sshUrl = toSSH(httpsUrl);
|
|
353
621
|
|
|
354
622
|
if (sshUrl) {
|
|
@@ -383,7 +651,145 @@ function configureGitCredentials(token) {
|
|
|
383
651
|
}
|
|
384
652
|
}
|
|
385
653
|
|
|
386
|
-
function
|
|
654
|
+
function normalizeRuntime(runtime) {
|
|
655
|
+
const raw = String(runtime || "claude-code").trim().toLowerCase();
|
|
656
|
+
if (raw === "claude" || raw === "claude-code" || raw === "claude_code") return "claude-code";
|
|
657
|
+
if (raw === "codex" || raw === "openai-codex" || raw === "openai_codex") return "codex";
|
|
658
|
+
throw new Error(`Unsupported Egregore runtime: ${runtime}`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function runtimeDisplay(runtime) {
|
|
662
|
+
return normalizeRuntime(runtime) === "codex" ? "Codex" : "Claude Code";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function runtimeBinary(runtime) {
|
|
666
|
+
return normalizeRuntime(runtime) === "codex" ? "codex" : "claude";
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// One-line install command for the chosen assistant — shown when the
|
|
670
|
+
// install finishes on a machine that doesn't have the assistant yet.
|
|
671
|
+
function assistantInstallCommand(runtime) {
|
|
672
|
+
return normalizeRuntime(runtime) === "codex"
|
|
673
|
+
? "npm install -g @openai/codex"
|
|
674
|
+
: "npm install -g @anthropic-ai/claude-code";
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function runtimeLaunchArgs(runtime, egregoreDir) {
|
|
678
|
+
if (normalizeRuntime(runtime) === "codex") {
|
|
679
|
+
return [
|
|
680
|
+
"--enable", "hooks",
|
|
681
|
+
"--sandbox", "danger-full-access",
|
|
682
|
+
"--ask-for-approval", "never",
|
|
683
|
+
"--cd", egregoreDir,
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
return ["start"];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function startSpinner(label) {
|
|
690
|
+
if (!process.stdout.isTTY) {
|
|
691
|
+
console.log(` ${label}...`);
|
|
692
|
+
return () => {};
|
|
693
|
+
}
|
|
694
|
+
const frames = ["|", "/", "-", "\\"];
|
|
695
|
+
let i = 0;
|
|
696
|
+
process.stdout.write(` ${frames[i]} ${label}`);
|
|
697
|
+
const timer = setInterval(() => {
|
|
698
|
+
i = (i + 1) % frames.length;
|
|
699
|
+
process.stdout.write(`\r ${frames[i]} ${label}\x1b[K`);
|
|
700
|
+
}, 90);
|
|
701
|
+
return () => {
|
|
702
|
+
clearInterval(timer);
|
|
703
|
+
process.stdout.write("\r\x1b[K");
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function runCapturedWithSpinner(cmd, args, options, label) {
|
|
708
|
+
return new Promise((resolve) => {
|
|
709
|
+
const stop = startSpinner(label);
|
|
710
|
+
let stdout = "";
|
|
711
|
+
let stderr = "";
|
|
712
|
+
let settled = false;
|
|
713
|
+
const child = spawn(cmd, args, {
|
|
714
|
+
cwd: options.cwd,
|
|
715
|
+
env: options.env || process.env,
|
|
716
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
720
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
721
|
+
child.on("error", (err) => {
|
|
722
|
+
if (settled) return;
|
|
723
|
+
settled = true;
|
|
724
|
+
stop();
|
|
725
|
+
resolve({ status: 1, stdout, stderr: stderr || String(err) });
|
|
726
|
+
});
|
|
727
|
+
child.on("close", (status) => {
|
|
728
|
+
if (settled) return;
|
|
729
|
+
settled = true;
|
|
730
|
+
stop();
|
|
731
|
+
resolve({ status, stdout, stderr });
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function renderCodexStartupCard(egregoreDir) {
|
|
737
|
+
const script = path.join(egregoreDir, "bin", "codex-session-start.sh");
|
|
738
|
+
if (fs.existsSync(script)) {
|
|
739
|
+
try {
|
|
740
|
+
const result = await runCapturedWithSpinner("bash", [script], {
|
|
741
|
+
cwd: egregoreDir,
|
|
742
|
+
}, "Preparing Codex startup");
|
|
743
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
744
|
+
if (result.status !== 0 && result.stderr) process.stderr.write(result.stderr);
|
|
745
|
+
if (result.status === 0) return true;
|
|
746
|
+
} catch {}
|
|
747
|
+
}
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function codexSupportMessage(egregoreDir) {
|
|
752
|
+
return [
|
|
753
|
+
"Codex assistant support is missing from this Egregore checkout.",
|
|
754
|
+
` path: ${egregoreDir}`,
|
|
755
|
+
"",
|
|
756
|
+
"This checkout needs a newer Egregore core with:",
|
|
757
|
+
" bin/codex-session-start.sh",
|
|
758
|
+
" bin/codex-install-skills.sh",
|
|
759
|
+
" .codex/config.toml",
|
|
760
|
+
" .codex/skills/",
|
|
761
|
+
" .codex/hooks.json",
|
|
762
|
+
" .codex/hooks/",
|
|
763
|
+
"",
|
|
764
|
+
"Run npx -y create-egregore@latest to install the packaged Codex runtime, then rerun egregore.",
|
|
765
|
+
"Egregore workflows appear as slash commands. In Codex, invoke them with the matching $skill token or ask in natural language.",
|
|
766
|
+
].join("\n");
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function hasCodexCoreSupport(egregoreDir) {
|
|
770
|
+
return hasCodexRuntimeSupport(egregoreDir);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function installCodexSkills(egregoreDir) {
|
|
774
|
+
const script = path.join(egregoreDir, "bin", "codex-install-skills.sh");
|
|
775
|
+
if (!fs.existsSync(script)) return false;
|
|
776
|
+
try {
|
|
777
|
+
const result = spawnSync("bash", [script], {
|
|
778
|
+
cwd: egregoreDir,
|
|
779
|
+
encoding: "utf-8",
|
|
780
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
781
|
+
maxBuffer: 1024 * 1024,
|
|
782
|
+
});
|
|
783
|
+
return result.status === 0;
|
|
784
|
+
} catch {}
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function commandExists(bin) {
|
|
789
|
+
return spawnSync("which", [bin], { stdio: "pipe" }).status === 0;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function registerInstance(slug, name, egregoreDir, runtime) {
|
|
387
793
|
const registryDir = path.join(os.homedir(), ".egregore");
|
|
388
794
|
const registryFile = path.join(registryDir, "instances.json");
|
|
389
795
|
|
|
@@ -394,15 +800,28 @@ function registerInstance(slug, name, egregoreDir) {
|
|
|
394
800
|
let instances = [];
|
|
395
801
|
if (fs.existsSync(registryFile)) {
|
|
396
802
|
try {
|
|
397
|
-
|
|
803
|
+
const parsed = JSON.parse(fs.readFileSync(registryFile, "utf-8"));
|
|
804
|
+
// Accept either the canonical [..] or the legacy/alt {instances: [..]}
|
|
805
|
+
// shape. Subsequent writes normalize back to a plain array.
|
|
806
|
+
if (Array.isArray(parsed)) {
|
|
807
|
+
instances = parsed;
|
|
808
|
+
} else if (parsed && Array.isArray(parsed.instances)) {
|
|
809
|
+
instances = parsed.instances;
|
|
810
|
+
}
|
|
398
811
|
} catch {
|
|
399
812
|
instances = [];
|
|
400
813
|
}
|
|
401
814
|
}
|
|
402
815
|
|
|
403
|
-
const entry = { slug, name, path: egregoreDir };
|
|
404
816
|
// Dedup by path (not slug) so multiple instances from the same org can coexist
|
|
405
817
|
const idx = instances.findIndex((i) => i.path === egregoreDir);
|
|
818
|
+
const previous = idx >= 0 ? instances[idx] : {};
|
|
819
|
+
const entry = { ...previous, slug, name, path: egregoreDir };
|
|
820
|
+
if (runtime) {
|
|
821
|
+
entry.runtime = normalizeRuntime(runtime);
|
|
822
|
+
} else if (!entry.runtime && entry.preferred_runtime) {
|
|
823
|
+
entry.runtime = normalizeRuntime(entry.preferred_runtime);
|
|
824
|
+
}
|
|
406
825
|
if (idx >= 0) {
|
|
407
826
|
instances[idx] = entry;
|
|
408
827
|
} else {
|
|
@@ -412,6 +831,331 @@ function registerInstance(slug, name, egregoreDir) {
|
|
|
412
831
|
fs.writeFileSync(registryFile, JSON.stringify(instances, null, 2) + "\n");
|
|
413
832
|
}
|
|
414
833
|
|
|
834
|
+
// ── Auto-launch helper ─────────────────────────────────────────────
|
|
835
|
+
//
|
|
836
|
+
// Replaces the legacy "open a new terminal and type <alias>" closing with an
|
|
837
|
+
// actual agent-runtime invocation in the egregore directory. The installer
|
|
838
|
+
// process waits for the runtime to exit, then propagates the exit code.
|
|
839
|
+
//
|
|
840
|
+
// ctx.noAutolaunch=true disables this (for CI / scripted installs) and falls
|
|
841
|
+
// back to printing the command the user should type.
|
|
842
|
+
// A child process can't change the parent shell's PATH, so right after install
|
|
843
|
+
// the launcher works in any NEW shell but not the current one (its rc hasn't
|
|
844
|
+
// re-loaded). Detect that and give the exact fix — avoids a confusing
|
|
845
|
+
// "egregore: command not found" when the user types it again in this shell.
|
|
846
|
+
function launcherNextStep(ui, aliasName) {
|
|
847
|
+
const binDir = path.join(os.homedir(), ".local", "bin");
|
|
848
|
+
const onPath = (process.env.PATH || "").split(path.delimiter).some((p) => {
|
|
849
|
+
try { return p && path.resolve(p) === binDir; } catch { return false; }
|
|
850
|
+
});
|
|
851
|
+
if (onPath) return `type ${ui.bold(aliasName)} in any terminal.`;
|
|
852
|
+
const shell = (process.env.SHELL || "").split("/").pop();
|
|
853
|
+
const rc = shell === "zsh" ? "~/.zshrc" : "~/.bashrc";
|
|
854
|
+
return `type ${ui.bold(aliasName)} in a new terminal — or run ${ui.bold("source " + rc)} in this one.`;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function launchOrInstruct(egregoreDir, alias, ui, ctx = {}) {
|
|
858
|
+
const aliasName = (alias && alias.aliasName) || "egregore";
|
|
859
|
+
const runtime = normalizeRuntime(ctx.runtime);
|
|
860
|
+
const display = runtimeDisplay(runtime);
|
|
861
|
+
const binary = runtimeBinary(runtime);
|
|
862
|
+
|
|
863
|
+
if (ctx.noAutolaunch) {
|
|
864
|
+
ui.info(`Next: ${launcherNextStep(ui, aliasName)}`);
|
|
865
|
+
console.log("");
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (!commandExists(binary)) {
|
|
870
|
+
const hasClaude = commandExists("claude");
|
|
871
|
+
const hasCodex = commandExists("codex");
|
|
872
|
+
// The user already chose `runtime` at the picker — lead with installing
|
|
873
|
+
// THAT one ("install Codex first"), then offer the other as an alternative.
|
|
874
|
+
const other = runtime === "codex" ? "claude-code" : "codex";
|
|
875
|
+
if (!hasClaude && !hasCodex) {
|
|
876
|
+
ui.warn(`Almost there — install ${display} to start sessions.`);
|
|
877
|
+
ui.info(` Install ${display} first:`);
|
|
878
|
+
ui.info(` ${ui.bold(assistantInstallCommand(runtime))}`);
|
|
879
|
+
ui.info(` ${ui.dim(`(or use ${runtimeDisplay(other)}: ${assistantInstallCommand(other)})`)}`);
|
|
880
|
+
ui.info(` Then ${launcherNextStep(ui, aliasName)}`);
|
|
881
|
+
console.log("");
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
ui.warn(`Almost there — Egregore needs ${display} to run your sessions.`);
|
|
885
|
+
ui.info(` Install ${display} first:`);
|
|
886
|
+
ui.info(` ${ui.bold(assistantInstallCommand(runtime))}`);
|
|
887
|
+
ui.info(` Then ${launcherNextStep(ui, aliasName)}`);
|
|
888
|
+
if (runtime === "codex") ui.info(" Choose Codex when prompted.");
|
|
889
|
+
console.log("");
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
ui.info(`Starting Egregore in ${ui.bold(display)} now — press ${ui.bold("Ctrl+D")} (or /exit) to return to your shell.`);
|
|
894
|
+
ui.info(`Next time, ${launcherNextStep(ui, aliasName)}`);
|
|
895
|
+
console.log("");
|
|
896
|
+
|
|
897
|
+
if (runtime === "codex") {
|
|
898
|
+
const codexRuntime = installCodexRuntime(egregoreDir);
|
|
899
|
+
reportCodexRuntimeSummary(codexRuntime, ui);
|
|
900
|
+
if (!hasCodexCoreSupport(egregoreDir)) {
|
|
901
|
+
ui.warn(codexSupportMessage(egregoreDir));
|
|
902
|
+
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
903
|
+
console.log("");
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const skillsInstalled = installCodexSkills(egregoreDir);
|
|
907
|
+
const cardRendered = await renderCodexStartupCard(egregoreDir);
|
|
908
|
+
if (!skillsInstalled || !cardRendered) {
|
|
909
|
+
ui.warn(codexSupportMessage(egregoreDir));
|
|
910
|
+
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
911
|
+
console.log("");
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const result = spawnSync(binary, runtimeLaunchArgs(runtime, egregoreDir), {
|
|
917
|
+
cwd: egregoreDir,
|
|
918
|
+
stdio: "inherit",
|
|
919
|
+
env: { ...process.env, EGREGORE_RUNTIME: runtime },
|
|
920
|
+
});
|
|
921
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Launcher binary template path. We ship a Node-based TUI launcher as the
|
|
925
|
+
// primary; the shell script remains in the repo for back-compat but the
|
|
926
|
+
// installer prefers the .js variant because it provides the full-screen
|
|
927
|
+
// browser, better rendering, and richer keybinds.
|
|
928
|
+
const LAUNCHER_TEMPLATE_JS = path.join(__dirname, "..", "assets", "egregore-launcher.js");
|
|
929
|
+
const LAUNCHER_TEMPLATE_SH = path.join(__dirname, "..", "assets", "egregore-launcher.sh");
|
|
930
|
+
|
|
931
|
+
function readLauncherTemplate() {
|
|
932
|
+
// Prefer the Node launcher; fall back to bash only if the .js is missing.
|
|
933
|
+
if (fs.existsSync(LAUNCHER_TEMPLATE_JS)) return fs.readFileSync(LAUNCHER_TEMPLATE_JS, "utf-8");
|
|
934
|
+
return fs.readFileSync(LAUNCHER_TEMPLATE_SH, "utf-8");
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function currentTemplatePath() {
|
|
938
|
+
return fs.existsSync(LAUNCHER_TEMPLATE_JS) ? LAUNCHER_TEMPLATE_JS : LAUNCHER_TEMPLATE_SH;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Parse a semver from a version-stamp comment. Accepts both styles so we can
|
|
942
|
+
// upgrade a bash launcher to the Node launcher across a major line:
|
|
943
|
+
// # egregore-launcher-version: X.Y.Z (bash)
|
|
944
|
+
// // egregore-launcher-version: X.Y.Z (node)
|
|
945
|
+
function launcherVersion(contents) {
|
|
946
|
+
const m = contents.match(/egregore-launcher-version:\s*(\d+\.\d+\.\d+)/);
|
|
947
|
+
return m ? m[1] : null;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Return 1 if a > b, -1 if a < b, 0 if equal. Both are semver strings "x.y.z".
|
|
951
|
+
function compareVersions(a, b) {
|
|
952
|
+
const pa = a.split(".").map(Number);
|
|
953
|
+
const pb = b.split(".").map(Number);
|
|
954
|
+
for (let i = 0; i < 3; i++) {
|
|
955
|
+
if (pa[i] !== pb[i]) return pa[i] > pb[i] ? 1 : -1;
|
|
956
|
+
}
|
|
957
|
+
return 0;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function isDirWritable(dir) {
|
|
961
|
+
try {
|
|
962
|
+
if (!fs.existsSync(dir)) return false;
|
|
963
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
964
|
+
return true;
|
|
965
|
+
} catch {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function dirOnPath(dir) {
|
|
971
|
+
const pathEntries = (process.env.PATH || "").split(path.delimiter);
|
|
972
|
+
return pathEntries.some((p) => {
|
|
973
|
+
try {
|
|
974
|
+
return p && fs.existsSync(p) && fs.realpathSync(p) === fs.realpathSync(dir);
|
|
975
|
+
} catch {
|
|
976
|
+
return p === dir;
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function appendLocalBinToRc(ui) {
|
|
982
|
+
// Ensure ~/.local/bin is on PATH for future shells by appending to rc files.
|
|
983
|
+
// Idempotent — only appends if the export line isn't already present.
|
|
984
|
+
const rcFiles = [".zshrc", ".bashrc", ".bash_profile"].map((f) => path.join(os.homedir(), f));
|
|
985
|
+
const exportLine = `export PATH="$HOME/.local/bin:$PATH"`;
|
|
986
|
+
let touched = [];
|
|
987
|
+
for (const rc of rcFiles) {
|
|
988
|
+
if (!fs.existsSync(rc)) continue;
|
|
989
|
+
try {
|
|
990
|
+
const content = fs.readFileSync(rc, "utf-8");
|
|
991
|
+
if (content.includes(`$HOME/.local/bin`) || content.includes(`${os.homedir()}/.local/bin`)) continue;
|
|
992
|
+
fs.appendFileSync(rc, `\n# Added by create-egregore\n${exportLine}\n`);
|
|
993
|
+
touched.push(path.basename(rc));
|
|
994
|
+
} catch {}
|
|
995
|
+
}
|
|
996
|
+
if (touched.length > 0) {
|
|
997
|
+
ui.info(`Added ~/.local/bin to PATH in: ${touched.join(", ")} (takes effect in new shells).`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Install the `egregore` PATH binary. Writes a launcher script that reads
|
|
1003
|
+
* ~/.egregore/instances.json and launches the chosen instance.
|
|
1004
|
+
*
|
|
1005
|
+
* Placement preference:
|
|
1006
|
+
* 1. /usr/local/bin/egregore (writable without sudo)
|
|
1007
|
+
* 2. /opt/homebrew/bin/egregore (Apple Silicon Homebrew, writable without sudo)
|
|
1008
|
+
* 3. ~/.local/bin/egregore + PATH append (no sudo; works in new shells)
|
|
1009
|
+
*
|
|
1010
|
+
* If an `egregore` binary already exists at the chosen path:
|
|
1011
|
+
* - Same content (by byte match) → no-op (silent)
|
|
1012
|
+
* - Different content → keep existing, warn (don't clobber user customizations)
|
|
1013
|
+
*
|
|
1014
|
+
* Returns { aliasName: "egregore", binaryPath, location, replaced }
|
|
1015
|
+
*/
|
|
1016
|
+
async function installLauncherBinary(egregoreDir, ui) {
|
|
1017
|
+
const template = readLauncherTemplate();
|
|
1018
|
+
const candidates = [
|
|
1019
|
+
{ dir: "/usr/local/bin", location: "system" },
|
|
1020
|
+
{ dir: "/opt/homebrew/bin", location: "system" },
|
|
1021
|
+
{ dir: path.join(os.homedir(), ".local", "bin"), location: "user" },
|
|
1022
|
+
];
|
|
1023
|
+
|
|
1024
|
+
let chosen = null;
|
|
1025
|
+
for (const c of candidates) {
|
|
1026
|
+
if (c.location === "user" && !fs.existsSync(c.dir)) {
|
|
1027
|
+
try {
|
|
1028
|
+
fs.mkdirSync(c.dir, { recursive: true });
|
|
1029
|
+
} catch {
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (isDirWritable(c.dir)) {
|
|
1034
|
+
chosen = c;
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (!chosen) {
|
|
1040
|
+
ui.warn("Could not find a writable bin directory. Install the launcher manually:");
|
|
1041
|
+
ui.info(` cp ${currentTemplatePath()} ~/.local/bin/egregore && chmod +x ~/.local/bin/egregore`);
|
|
1042
|
+
return { aliasName: "egregore", binaryPath: null, location: null, replaced: false };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const binaryPath = path.join(chosen.dir, "egregore");
|
|
1046
|
+
let replaced = false;
|
|
1047
|
+
|
|
1048
|
+
if (fs.existsSync(binaryPath)) {
|
|
1049
|
+
try {
|
|
1050
|
+
const existing = fs.readFileSync(binaryPath, "utf-8");
|
|
1051
|
+
if (existing === template) {
|
|
1052
|
+
// Same content — silent no-op, still chmod for safety
|
|
1053
|
+
try { fs.chmodSync(binaryPath, 0o755); } catch {}
|
|
1054
|
+
if (chosen.location === "user" && !dirOnPath(chosen.dir)) appendLocalBinToRc(ui);
|
|
1055
|
+
removeShadowingEgregoreAliases(ui);
|
|
1056
|
+
return { aliasName: "egregore", binaryPath, location: chosen.location, replaced: false };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Different content — decide upgrade vs preserve via version stamps.
|
|
1060
|
+
const existingVer = launcherVersion(existing);
|
|
1061
|
+
const templateVer = launcherVersion(template);
|
|
1062
|
+
|
|
1063
|
+
if (existingVer && templateVer) {
|
|
1064
|
+
if (compareVersions(templateVer, existingVer) > 0) {
|
|
1065
|
+
// Template is newer — safe upgrade.
|
|
1066
|
+
fs.writeFileSync(binaryPath, template, { mode: 0o755 });
|
|
1067
|
+
ui.success(`Upgraded ${ui.dim("egregore")} at ${binaryPath} ${ui.dim(`(${existingVer} → ${templateVer})`)}`);
|
|
1068
|
+
if (chosen.location === "user" && !dirOnPath(chosen.dir)) appendLocalBinToRc(ui);
|
|
1069
|
+
removeShadowingEgregoreAliases(ui);
|
|
1070
|
+
return { aliasName: "egregore", binaryPath, location: chosen.location, replaced: true };
|
|
1071
|
+
}
|
|
1072
|
+
if (compareVersions(templateVer, existingVer) < 0) {
|
|
1073
|
+
// Template is older — don't downgrade. Warn lightly.
|
|
1074
|
+
ui.warn(`Existing ${binaryPath} (v${existingVer}) is newer than this installer's template (v${templateVer}) — leaving it alone.`);
|
|
1075
|
+
removeShadowingEgregoreAliases(ui);
|
|
1076
|
+
return { aliasName: "egregore", binaryPath, location: chosen.location, replaced: false };
|
|
1077
|
+
}
|
|
1078
|
+
// Equal stamps but different content → user edited the file. Don't clobber.
|
|
1079
|
+
ui.warn(`Existing ${binaryPath} has been edited locally — not overwriting.`);
|
|
1080
|
+
ui.info(` To reset: cp ${currentTemplatePath()} ${binaryPath}`);
|
|
1081
|
+
return { aliasName: "egregore", binaryPath, location: chosen.location, replaced: false };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// No version stamp on existing. If it looks like one of OUR old
|
|
1085
|
+
// launchers (pre-0.4.1, before stamps existed), upgrade it. Otherwise
|
|
1086
|
+
// assume the user customized it and preserve.
|
|
1087
|
+
// Match both the bash (REGISTRY=…) and Node (path.join(os.homedir()…))
|
|
1088
|
+
// forms, plus a generic substring that's stable across versions.
|
|
1089
|
+
const looksLikeOurs =
|
|
1090
|
+
existing.includes(`.egregore/instances.json`) ||
|
|
1091
|
+
existing.includes(`egregore-launcher-version`);
|
|
1092
|
+
if (looksLikeOurs) {
|
|
1093
|
+
fs.writeFileSync(binaryPath, template, { mode: 0o755 });
|
|
1094
|
+
ui.success(`Upgraded ${ui.dim("egregore")} at ${binaryPath} ${ui.dim(`(pre-0.4.1 → ${templateVer})`)}`);
|
|
1095
|
+
if (chosen.location === "user" && !dirOnPath(chosen.dir)) appendLocalBinToRc(ui);
|
|
1096
|
+
removeShadowingEgregoreAliases(ui);
|
|
1097
|
+
return { aliasName: "egregore", binaryPath, location: chosen.location, replaced: true };
|
|
1098
|
+
}
|
|
1099
|
+
ui.warn(`Existing ${binaryPath} doesn't look like an egregore launcher — not overwriting.`);
|
|
1100
|
+
ui.info(` If you meant to install: rm ${binaryPath} and re-run create-egregore.`);
|
|
1101
|
+
return { aliasName: "egregore", binaryPath, location: chosen.location, replaced: false };
|
|
1102
|
+
} catch {
|
|
1103
|
+
// Fall through to write
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
fs.writeFileSync(binaryPath, template, { mode: 0o755 });
|
|
1109
|
+
replaced = false;
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
ui.warn(`Could not write launcher to ${binaryPath}: ${err.message}`);
|
|
1112
|
+
return { aliasName: "egregore", binaryPath: null, location: null, replaced: false };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (chosen.location === "user" && !dirOnPath(chosen.dir)) {
|
|
1116
|
+
appendLocalBinToRc(ui);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
removeShadowingEgregoreAliases(ui);
|
|
1120
|
+
ui.success(`Installed ${ui.dim("egregore")} at ${binaryPath}`);
|
|
1121
|
+
return { aliasName: "egregore", binaryPath, location: chosen.location, replaced };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Strip legacy `alias egregore=...` lines from common shell rc files. These
|
|
1125
|
+
// come from older `create-egregore` installs that shipped a shell alias
|
|
1126
|
+
// instead of a PATH binary. In zsh, aliases beat PATH lookups, so the alias
|
|
1127
|
+
// shadows the new launcher and `egregore` keeps running the alias's
|
|
1128
|
+
// `cd <dir> && claude start` even after the launcher is installed.
|
|
1129
|
+
// Idempotent: if no alias exists, nothing happens. Non-fatal on errors.
|
|
1130
|
+
function removeShadowingEgregoreAliases(ui) {
|
|
1131
|
+
const home = os.homedir();
|
|
1132
|
+
const targets = [
|
|
1133
|
+
{ rcPath: path.join(home, ".zshrc"), prefix: "alias egregore=" },
|
|
1134
|
+
{ rcPath: path.join(home, ".bashrc"), prefix: "alias egregore=" },
|
|
1135
|
+
{ rcPath: path.join(home, ".bash_profile"), prefix: "alias egregore=" },
|
|
1136
|
+
{ rcPath: path.join(home, ".profile"), prefix: "alias egregore=" },
|
|
1137
|
+
{ rcPath: path.join(home, ".config", "fish", "config.fish"), prefix: "alias egregore " },
|
|
1138
|
+
];
|
|
1139
|
+
const touched = [];
|
|
1140
|
+
for (const { rcPath, prefix } of targets) {
|
|
1141
|
+
if (!fs.existsSync(rcPath)) continue;
|
|
1142
|
+
try {
|
|
1143
|
+
const content = fs.readFileSync(rcPath, "utf-8");
|
|
1144
|
+
const lines = content.split("\n");
|
|
1145
|
+
const kept = lines.filter((l) => !l.startsWith(prefix));
|
|
1146
|
+
if (kept.length === lines.length) continue;
|
|
1147
|
+
fs.writeFileSync(rcPath, kept.join("\n"));
|
|
1148
|
+
touched.push(path.basename(rcPath));
|
|
1149
|
+
} catch {
|
|
1150
|
+
// Non-fatal — best effort. Worst case: the alias stays and the user
|
|
1151
|
+
// can remove it by hand.
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (touched.length > 0 && ui) {
|
|
1155
|
+
ui.info(`Removed legacy ${ui.bold("egregore")} alias from ${touched.join(", ")} — start a new shell to use the launcher.`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
415
1159
|
async function installShellAlias(egregoreDir, ui) {
|
|
416
1160
|
try {
|
|
417
1161
|
// Detect shell profile
|
|
@@ -497,9 +1241,17 @@ module.exports = {
|
|
|
497
1241
|
ghApi,
|
|
498
1242
|
acceptPendingInvitations,
|
|
499
1243
|
registerInstance,
|
|
1244
|
+
normalizeRuntime,
|
|
500
1245
|
installShellAlias,
|
|
1246
|
+
installLauncherBinary,
|
|
1247
|
+
launchOrInstruct,
|
|
1248
|
+
buildOnboardingState,
|
|
1249
|
+
ensurePersonOnboardingWitness,
|
|
1250
|
+
ensureInstallerMemberRole,
|
|
501
1251
|
embedToken,
|
|
502
1252
|
configureGitCredentials,
|
|
1253
|
+
gitCloneEnv,
|
|
1254
|
+
assistantInstallCommand,
|
|
503
1255
|
initMemoryDirs,
|
|
504
1256
|
ensureDevelop,
|
|
505
1257
|
run,
|