create-egregore 0.17.4 → 0.18.1
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 +12 -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/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/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 +40 -0
- package/runtime/codex/bin/activity-data.sh +177 -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 +174 -0
- package/runtime/codex/bin/graph-batch.sh +60 -0
- package/runtime/codex/bin/graph-op.sh +380 -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 +431 -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 +463 -0
- package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -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 +414 -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 +173 -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 +118 -0
- package/runtime/codex/bin/worktree-remove.sh +50 -0
- package/runtime/codex/bin/worktree.sh +198 -0
- package/runtime/codex/manifest.json +155 -0
package/lib/local.js
CHANGED
|
@@ -6,19 +6,35 @@
|
|
|
6
6
|
const fs = require("node:fs");
|
|
7
7
|
const path = require("node:path");
|
|
8
8
|
const { execFileSync } = require("node:child_process");
|
|
9
|
-
const { deviceFlow, openBrowser } = require("./auth");
|
|
10
9
|
const {
|
|
10
|
+
GH_INSTALL_URL,
|
|
11
|
+
ghCliExists,
|
|
12
|
+
ghInstallCommand,
|
|
13
|
+
ghInstallShellCommand,
|
|
14
|
+
openBrowser,
|
|
15
|
+
runGhInstallAndLogin,
|
|
16
|
+
tryGhAuthOrLogin,
|
|
17
|
+
} = require("./auth");
|
|
18
|
+
const { installCodexRuntime, reportCodexRuntimeSummary } = require("./codex-runtime");
|
|
19
|
+
const {
|
|
20
|
+
install,
|
|
11
21
|
ghApi,
|
|
12
22
|
acceptPendingInvitations,
|
|
13
23
|
registerInstance,
|
|
14
|
-
|
|
24
|
+
normalizeRuntime,
|
|
25
|
+
installLauncherBinary,
|
|
26
|
+
launchOrInstruct,
|
|
15
27
|
embedToken,
|
|
16
28
|
configureGitCredentials,
|
|
29
|
+
gitCloneEnv,
|
|
17
30
|
initMemoryDirs,
|
|
18
31
|
ensureDevelop,
|
|
32
|
+
ensurePersonOnboardingWitness,
|
|
33
|
+
ensureInstallerMemberRole,
|
|
19
34
|
run,
|
|
20
35
|
} = require("./setup");
|
|
21
36
|
const { EgregoreAPI } = require("./api");
|
|
37
|
+
const { joinCommand } = require("./commands");
|
|
22
38
|
|
|
23
39
|
// Template repo for new egregore instances (OSS)
|
|
24
40
|
const TEMPLATE_OWNER = "egregore-labs";
|
|
@@ -38,6 +54,18 @@ async function listOrgs(token) {
|
|
|
38
54
|
return data.map((o) => ({ login: o.login }));
|
|
39
55
|
}
|
|
40
56
|
|
|
57
|
+
async function listOrgMembers(token, org) {
|
|
58
|
+
// Returns [{login}]. Only visible members (public or visible to the token).
|
|
59
|
+
// Non-fatal on error — caller falls back to a text prompt.
|
|
60
|
+
try {
|
|
61
|
+
const { status, data } = await ghApi("GET", `/orgs/${org}/members?per_page=100`, token);
|
|
62
|
+
if (status !== 200 || !Array.isArray(data)) return [];
|
|
63
|
+
return data.map((m) => ({ login: m.login }));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
async function repoExists(token, owner, repo) {
|
|
42
70
|
const { status } = await ghApi("GET", `/repos/${owner}/${repo}`, token);
|
|
43
71
|
return status === 200;
|
|
@@ -116,9 +144,19 @@ async function putFileContent(token, owner, repo, filePath, content, message) {
|
|
|
116
144
|
|
|
117
145
|
async function listOrgRepos(token, owner, isOrg) {
|
|
118
146
|
// Fetch all repos (public + private). Paginate to get beyond the first 100.
|
|
147
|
+
//
|
|
148
|
+
// For ORGS: /orgs/{owner}/repos?type=all returns everything the token can
|
|
149
|
+
// see in that org (public + private + member-of) given `repo` scope.
|
|
150
|
+
//
|
|
151
|
+
// For PERSONAL: /users/{owner}/repos returns ONLY public repos — the
|
|
152
|
+
// endpoint doesn't honor the token's `repo` scope for private listings.
|
|
153
|
+
// The right endpoint for "this authenticated user's own repos" is
|
|
154
|
+
// /user/repos, which returns public + private + collaborator with `repo`
|
|
155
|
+
// scope. We filter affiliation=owner so we don't surface random collab
|
|
156
|
+
// repos in the founder's picker.
|
|
119
157
|
const baseUrl = isOrg
|
|
120
158
|
? `/orgs/${owner}/repos?per_page=100&sort=updated&type=all`
|
|
121
|
-
: `/
|
|
159
|
+
: `/user/repos?per_page=100&sort=updated&affiliation=owner`;
|
|
122
160
|
|
|
123
161
|
let allRepos = [];
|
|
124
162
|
let page = 1;
|
|
@@ -131,6 +169,14 @@ async function listOrgRepos(token, owner, isOrg) {
|
|
|
131
169
|
page++;
|
|
132
170
|
}
|
|
133
171
|
|
|
172
|
+
// Personal /user/repos returns repos across all of the user's accounts
|
|
173
|
+
// (their personal namespace + any orgs they're a member of). When the
|
|
174
|
+
// caller asked for the personal scope, narrow to repos owned by `owner`
|
|
175
|
+
// so org repos don't pollute the personal picker.
|
|
176
|
+
if (!isOrg) {
|
|
177
|
+
allRepos = allRepos.filter((r) => r.owner && r.owner.login === owner);
|
|
178
|
+
}
|
|
179
|
+
|
|
134
180
|
return allRepos
|
|
135
181
|
.filter((r) => !r.archived && r.name !== "egregore" && !r.name.endsWith("-memory"))
|
|
136
182
|
.map((r) => ({ name: r.name, language: r.language || "", description: r.description || "" }));
|
|
@@ -176,6 +222,7 @@ function cloneRepo(url, dir, token, ui, label) {
|
|
|
176
222
|
stdio: "pipe",
|
|
177
223
|
encoding: "utf-8",
|
|
178
224
|
timeout: 60000,
|
|
225
|
+
env: gitCloneEnv(),
|
|
179
226
|
});
|
|
180
227
|
try {
|
|
181
228
|
run(`git remote set-url origin ${url}`, { cwd: dir });
|
|
@@ -201,7 +248,7 @@ function toSSH(url) {
|
|
|
201
248
|
* GIT_TERMINAL_PROMPT=0 prevents the hanging username/password prompt.
|
|
202
249
|
*/
|
|
203
250
|
function cloneWithFallback(httpsUrl, dir, timeout = 60000) {
|
|
204
|
-
const env =
|
|
251
|
+
const env = gitCloneEnv();
|
|
205
252
|
const sshUrl = toSSH(httpsUrl);
|
|
206
253
|
|
|
207
254
|
// Try SSH first (most devs have SSH keys; GitHub no longer supports HTTPS password auth)
|
|
@@ -243,52 +290,263 @@ function createSymlink(egregoreDir, memoryDir, ui) {
|
|
|
243
290
|
ui.success("Linked");
|
|
244
291
|
}
|
|
245
292
|
|
|
246
|
-
//
|
|
247
|
-
|
|
293
|
+
// Retry an async fn until it returns truthy, up to `attempts` tries with a
|
|
294
|
+
// fixed delay between them. Returns the last result (truthy on success).
|
|
295
|
+
async function retryAsync(fn, attempts = 2, delayMs = 1500) {
|
|
296
|
+
let result;
|
|
297
|
+
for (let i = 0; i < attempts; i++) {
|
|
298
|
+
try {
|
|
299
|
+
result = await fn();
|
|
300
|
+
} catch {
|
|
301
|
+
result = false;
|
|
302
|
+
}
|
|
303
|
+
if (result) return result;
|
|
304
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, delayMs));
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Shown when gh isn't installed. gh is GitHub's own CLI — it signs the user in
|
|
310
|
+
// locally so the installer can work with THEIR repos. No Egregore OAuth app, no
|
|
311
|
+
// third-party "requesting access" screen. Language stays ownership-first: this
|
|
312
|
+
// is their machine and their repos; Egregore isn't managing or holding anything.
|
|
313
|
+
function ghFallbackNote(ui) {
|
|
314
|
+
const lines = [
|
|
315
|
+
`Egregore sets up in your own GitHub, on your own machine — you own all of it.`,
|
|
316
|
+
`It signs in with GitHub CLI (${ui.cyan("gh")}), GitHub's own tool, so setup can see`,
|
|
317
|
+
`your repos and create your Egregore. No third-party access screen, nothing hosted by us.`,
|
|
318
|
+
``,
|
|
319
|
+
`Install gh, sign in, then re-run — that's the whole setup:`,
|
|
320
|
+
` 1. ${ui.bold(ghInstallCommand())}`,
|
|
321
|
+
` 2. ${ui.bold("gh auth login")}`,
|
|
322
|
+
` 3. ${ui.bold("npx -y create-egregore@latest")}`,
|
|
323
|
+
``,
|
|
324
|
+
`${ui.dim("Install for any OS: " + GH_INSTALL_URL)}`,
|
|
325
|
+
];
|
|
326
|
+
return lines;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function repoEntryName(entry) {
|
|
330
|
+
return typeof entry === "string" ? entry : entry && entry.name;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function yamlList(values) {
|
|
334
|
+
const clean = values.filter(Boolean);
|
|
335
|
+
if (clean.length === 0) return " []";
|
|
336
|
+
return clean.map((v) => `\n - ${String(v).replace(/\n/g, " ")}`).join("");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function inviteFilename(username) {
|
|
340
|
+
const safe = String(username || "invitee")
|
|
341
|
+
.toLowerCase()
|
|
342
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
343
|
+
.replace(/^-|-$/g, "") || "invitee";
|
|
344
|
+
return `${new Date().toISOString().slice(0, 10)}-${safe}.md`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function writeInviteArtifact(memoryDir, invite) {
|
|
348
|
+
const invitesDir = path.join(memoryDir, "invites");
|
|
349
|
+
fs.mkdirSync(invitesDir, { recursive: true });
|
|
350
|
+
const file = path.join(invitesDir, inviteFilename(invite.invitee));
|
|
351
|
+
const managedRepos = (invite.managedRepos || []).map(repoEntryName).filter(Boolean);
|
|
352
|
+
const lines = [
|
|
353
|
+
"---",
|
|
354
|
+
"type: egregore_invite",
|
|
355
|
+
`created_at: ${new Date().toISOString()}`,
|
|
356
|
+
`inviter: ${invite.inviter}`,
|
|
357
|
+
`invitee_github: ${invite.invitee}`,
|
|
358
|
+
`org: ${invite.githubOrg}`,
|
|
359
|
+
`repo: ${invite.githubOrg}/${invite.repoName}`,
|
|
360
|
+
`memory_repo: ${invite.memoryRepo}`,
|
|
361
|
+
`managed_repos:${yamlList(managedRepos)}`,
|
|
362
|
+
`invite_url: ${invite.inviteUrl || ""}`,
|
|
363
|
+
`fallback_command: ${invite.fallbackCommand}`,
|
|
364
|
+
`telegram_group_link: ${invite.telegramGroupLink || ""}`,
|
|
365
|
+
`delivery: ${invite.delivery || "copyable_share_block"}`,
|
|
366
|
+
"---",
|
|
367
|
+
"",
|
|
368
|
+
`# Invite: ${invite.invitee}`,
|
|
369
|
+
"",
|
|
370
|
+
`Fallback command: \`${invite.fallbackCommand}\``,
|
|
371
|
+
"",
|
|
372
|
+
];
|
|
373
|
+
if (invite.inviteUrl) lines.push(`Invite URL: ${invite.inviteUrl}`, "");
|
|
374
|
+
if (invite.telegramGroupLink) lines.push(`Telegram: ${invite.telegramGroupLink}`, "");
|
|
375
|
+
fs.writeFileSync(file, lines.join("\n"));
|
|
376
|
+
return file;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function commitMemoryInvite(memoryDir, paths, username) {
|
|
380
|
+
if (!fs.existsSync(path.join(memoryDir, ".git"))) {
|
|
381
|
+
return { committed: false, pushed: false };
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
for (const p of paths.filter(Boolean)) {
|
|
385
|
+
run(`git add ${JSON.stringify(path.relative(memoryDir, p))}`, { cwd: memoryDir });
|
|
386
|
+
}
|
|
387
|
+
run(`git commit -m "Invite ${username}"`, { cwd: memoryDir });
|
|
388
|
+
try {
|
|
389
|
+
run("git push", { cwd: memoryDir });
|
|
390
|
+
return { committed: true, pushed: true };
|
|
391
|
+
} catch {
|
|
392
|
+
return { committed: true, pushed: false };
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
return { committed: false, pushed: false };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function telegramBotInviteLink(slug) {
|
|
400
|
+
return `https://t.me/Egregore_clbot?startgroup=org_${encodeURIComponent(slug || "egregore")}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function looksLikeUrl(value) {
|
|
404
|
+
return /^https?:\/\//i.test(String(value || "").trim());
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function maybeSetupTelegramGroup(ui, {
|
|
408
|
+
egregoreDir,
|
|
409
|
+
githubToken,
|
|
410
|
+
githubOrg,
|
|
411
|
+
repoName,
|
|
412
|
+
egreConfig,
|
|
413
|
+
statePath,
|
|
414
|
+
}) {
|
|
415
|
+
ui.section("Telegram group", "optional shared notifications before you invite teammates");
|
|
416
|
+
const shouldSetup = await ui.confirm("Set up a Telegram group now?", false);
|
|
417
|
+
const botLink = telegramBotInviteLink(egreConfig.slug);
|
|
418
|
+
|
|
419
|
+
let state = {};
|
|
420
|
+
try { state = JSON.parse(fs.readFileSync(statePath, "utf-8")); } catch {}
|
|
421
|
+
state.telegram_setup = state.telegram_setup || {};
|
|
422
|
+
state.telegram_setup.handled_by = "installer";
|
|
423
|
+
state.telegram_setup.bot_invite_link = botLink;
|
|
424
|
+
state.telegram_setup.skipped = !shouldSetup;
|
|
425
|
+
|
|
426
|
+
if (!shouldSetup) {
|
|
427
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
428
|
+
ui.info(`You can run ${ui.bold("/telegram-connect")} later. Invites will include the CLI join command only.`);
|
|
429
|
+
return { setup: false, groupLink: "" };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
ui.note([
|
|
433
|
+
"Step 1: In Telegram, create a new group or open an existing team group.",
|
|
434
|
+
"Step 2: Add the Egregore bot to that group.",
|
|
435
|
+
"Step 3: Come back here and press Enter.",
|
|
436
|
+
]);
|
|
437
|
+
ui.info(`Bot add link: ${ui.cyan(botLink)}`);
|
|
438
|
+
openBrowser(botLink);
|
|
439
|
+
|
|
440
|
+
await ui.prompt("Press Enter here after the bot is in the group");
|
|
441
|
+
ui.note([
|
|
442
|
+
"Step 4: In Telegram, copy a group invite link.",
|
|
443
|
+
"Paste it below so teammate invites include the Telegram group too.",
|
|
444
|
+
"If you skip this, Egregore will still work, but invites will not include Telegram.",
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
const groupLink = await ui.prompt("Telegram group join link (paste here, or Enter to skip)");
|
|
448
|
+
const cleaned = groupLink.trim();
|
|
449
|
+
let storedGroupLink = "";
|
|
450
|
+
if (cleaned && !looksLikeUrl(cleaned)) {
|
|
451
|
+
ui.warn("That does not look like a URL, so I will not store it.");
|
|
452
|
+
} else if (cleaned) {
|
|
453
|
+
storedGroupLink = cleaned;
|
|
454
|
+
egreConfig.telegram_group_link = cleaned;
|
|
455
|
+
state.telegram_group_link = cleaned;
|
|
456
|
+
state.telegram_setup.group_link = cleaned;
|
|
457
|
+
|
|
458
|
+
const configPath = path.join(egregoreDir, "egregore.json");
|
|
459
|
+
fs.writeFileSync(configPath, JSON.stringify(egreConfig, null, 2) + "\n");
|
|
460
|
+
try {
|
|
461
|
+
await putFileContent(
|
|
462
|
+
githubToken,
|
|
463
|
+
githubOrg,
|
|
464
|
+
repoName,
|
|
465
|
+
"egregore.json",
|
|
466
|
+
JSON.stringify(egreConfig, null, 2) + "\n",
|
|
467
|
+
"Configure Telegram group",
|
|
468
|
+
);
|
|
469
|
+
} catch {}
|
|
470
|
+
try {
|
|
471
|
+
run("git add egregore.json", { cwd: egregoreDir });
|
|
472
|
+
run('git commit -m "Configure Telegram group"', { cwd: egregoreDir });
|
|
473
|
+
run("git push", { cwd: egregoreDir });
|
|
474
|
+
} catch {}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
478
|
+
ui.success(storedGroupLink ? "Telegram group recorded" : "Telegram bot added; no group link saved");
|
|
479
|
+
return { setup: true, groupLink: storedGroupLink };
|
|
480
|
+
}
|
|
248
481
|
|
|
249
482
|
// ── Flow 1: Founder (local mode) ───────────────────────────────────
|
|
250
483
|
|
|
251
484
|
async function localFounderFlow(ui, ctx = {}) {
|
|
485
|
+
const preferredRuntime = normalizeRuntime(ctx.runtime);
|
|
252
486
|
// Names of repos already on disk as the user's own workdir — skip clone/pull.
|
|
253
487
|
const skipRepos = ctx.skipRepos instanceof Set ? new Set(ctx.skipRepos) : new Set();
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
488
|
+
|
|
489
|
+
// 1. GitHub auth — prefer the gh CLI. If gh is installed but logged out,
|
|
490
|
+
// run gh auth login instead of opening the Egregore App page. The App
|
|
491
|
+
// fallback is only for machines without gh.
|
|
492
|
+
ui.section("Connect your GitHub", "sign in to your own account — everything created is yours");
|
|
493
|
+
ui.note([
|
|
494
|
+
"Egregore builds in your GitHub and on your machine — you own all of it.",
|
|
495
|
+
"This CLI just helps you set it up. Nothing is hosted by us.",
|
|
496
|
+
]);
|
|
497
|
+
|
|
263
498
|
let githubToken;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
499
|
+
// gh is the only local auth path: GitHub's own CLI, so no Egregore OAuth
|
|
500
|
+
// consent screen ever. We do NOT fall back to a third-party device flow —
|
|
501
|
+
// when gh is missing we guide the user to install it and re-run.
|
|
502
|
+
const ghAuth = tryGhAuthOrLogin(ui);
|
|
503
|
+
if (ghAuth) {
|
|
504
|
+
ui.success(`Using your ${ui.bold("gh")} CLI auth${ghAuth.user ? ` (${ghAuth.user})` : ""}`);
|
|
505
|
+
githubToken = ghAuth.token;
|
|
506
|
+
} else if (ghCliExists()) {
|
|
507
|
+
ui.error("GitHub CLI auth did not complete.");
|
|
508
|
+
ui.info(`Run ${ui.bold("gh auth login")} and then re-run this installer.`);
|
|
269
509
|
process.exit(1);
|
|
510
|
+
} else {
|
|
511
|
+
// gh isn't installed. Offer to install it + sign in right here — Enter
|
|
512
|
+
// accepts. gh is GitHub's own CLI, so there's no Egregore consent screen.
|
|
513
|
+
ui.note(ghFallbackNote(ui));
|
|
514
|
+
const cmd = ghInstallShellCommand();
|
|
515
|
+
if (cmd) {
|
|
516
|
+
const ok = await ui.confirm(`Install GitHub CLI now and sign in? (${ui.bold(cmd)})`, true);
|
|
517
|
+
if (ok) githubToken = runGhInstallAndLogin(ui, cmd);
|
|
518
|
+
}
|
|
519
|
+
if (!githubToken) {
|
|
520
|
+
ui.info("No problem — run the steps above when ready, then re-run this.");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
270
523
|
}
|
|
271
524
|
|
|
272
525
|
// 2. Get user info
|
|
273
526
|
const user = await getUser(githubToken);
|
|
274
527
|
|
|
275
|
-
// 3. Where — always
|
|
276
|
-
|
|
528
|
+
// 3. Where — we always have a gh token (the only local auth path), which
|
|
529
|
+
// carries org read, so we can list the user's accounts and offer a picker.
|
|
530
|
+
ui.section("Pick a home", "where should this Egregore live?");
|
|
277
531
|
const s = ui.spinner("Checking your GitHub accounts...");
|
|
278
532
|
const orgs = await listOrgs(githubToken);
|
|
279
|
-
s.stop("");
|
|
533
|
+
s.stop("Accounts loaded");
|
|
280
534
|
|
|
281
535
|
let githubOrg, isOrg;
|
|
282
536
|
if (orgs.length === 0) {
|
|
283
537
|
githubOrg = user.login;
|
|
284
538
|
isOrg = false;
|
|
285
|
-
ui.info(`Using personal account: ${ui.bold(githubOrg)}`);
|
|
539
|
+
ui.info(`Using your personal account: ${ui.bold(githubOrg)}`);
|
|
286
540
|
} else {
|
|
287
541
|
const orgChoices = [{ label: user.login, description: "Personal account" }];
|
|
288
542
|
for (const o of orgs) {
|
|
289
543
|
orgChoices.push({ label: o.login, description: "Organization" });
|
|
290
544
|
}
|
|
291
|
-
const orgChoice = await ui.choose(
|
|
545
|
+
const orgChoice = await ui.choose(
|
|
546
|
+
"Where should Egregore live?",
|
|
547
|
+
orgChoices,
|
|
548
|
+
"Don't see your org? Authorize it for your account on GitHub, then re-run.",
|
|
549
|
+
);
|
|
292
550
|
githubOrg = orgChoice.label;
|
|
293
551
|
isOrg = githubOrg !== user.login;
|
|
294
552
|
}
|
|
@@ -300,8 +558,7 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
300
558
|
let isNewProject;
|
|
301
559
|
if (ctx.prefilledRepo) {
|
|
302
560
|
isNewProject = false;
|
|
303
|
-
|
|
304
|
-
ui.info(`Existing project — we'll manage ${ui.cyan(ctx.prefilledRepo.localName)} plus any others you pick.`);
|
|
561
|
+
ui.info(`${ui.green("→")} existing project — we'll manage ${ui.cyan(ctx.prefilledRepo.localName)} plus any others you pick.`);
|
|
305
562
|
} else {
|
|
306
563
|
const action = await ui.choose("What are you working on?", [
|
|
307
564
|
{ label: "Starting something new", description: "Create a project from scratch" },
|
|
@@ -311,28 +568,55 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
311
568
|
}
|
|
312
569
|
|
|
313
570
|
// 5. Collect project info
|
|
571
|
+
ui.section("Name it", "what's this Egregore called?");
|
|
314
572
|
let orgName, description, selectedRepos = [], newProjectRepo = null;
|
|
315
573
|
|
|
316
574
|
if (isNewProject) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
575
|
+
orgName = await ui.prompt("name your Egregore (team or group name)", { default: githubOrg });
|
|
576
|
+
description = await ui.prompt("what are you building? (one line)");
|
|
577
|
+
|
|
578
|
+
// Derive the egregore + memory repo names now so we can validate the
|
|
579
|
+
// optional project repo name against them before the user commits to it.
|
|
580
|
+
// Matches the final derivation at line ~395 below.
|
|
581
|
+
const slugifyRepo = (s) => s.toLowerCase()
|
|
582
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
583
|
+
.replace(/-+/g, "-")
|
|
584
|
+
.replace(/^-|-$/g, "") || "egregore";
|
|
585
|
+
const predictedRepoName = slugifyRepo(orgName);
|
|
586
|
+
const predictedMemoryRepoName = `${predictedRepoName}-memory`;
|
|
322
587
|
|
|
323
588
|
// Ask about project repo
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
589
|
+
ui.note([
|
|
590
|
+
"Your Egregore gets its own repo (memory + config) in your account.",
|
|
591
|
+
"Have a code project too? Egregore can add a repo for it in your account.",
|
|
592
|
+
]);
|
|
593
|
+
|
|
594
|
+
while (true) {
|
|
595
|
+
const projectName = await ui.prompt("code project repo name (Enter to skip)");
|
|
596
|
+
if (!projectName) {
|
|
597
|
+
newProjectRepo = null;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
const slug = slugifyRepo(projectName);
|
|
601
|
+
if (!slug) {
|
|
602
|
+
ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes.");
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (slug === predictedRepoName) {
|
|
606
|
+
ui.warn(`"${slug}" is already the name of your Egregore repo. Pick a different name.`);
|
|
607
|
+
ui.info(` ${ui.dim("suggestion:")} ${ui.cyan(`${slug}-app`)}`);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (slug === predictedMemoryRepoName) {
|
|
611
|
+
ui.warn(`"${slug}" collides with your memory repo. Pick a different name.`);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
newProjectRepo = slug;
|
|
615
|
+
break;
|
|
331
616
|
}
|
|
332
617
|
} else {
|
|
333
618
|
// "Existing project"
|
|
334
|
-
|
|
335
|
-
orgName = nameInput || githubOrg;
|
|
619
|
+
orgName = await ui.prompt("what should we call this Egregore?", { default: githubOrg });
|
|
336
620
|
description = "";
|
|
337
621
|
|
|
338
622
|
// Auto-adopt the user's current repo if it's under the chosen GitHub owner.
|
|
@@ -412,7 +696,7 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
412
696
|
s1.stop(`${githubOrg}/${repoName} already has Egregore set up`);
|
|
413
697
|
console.log("");
|
|
414
698
|
ui.info("To join it:");
|
|
415
|
-
ui.info(` ${ui.bold(
|
|
699
|
+
ui.info(` ${ui.bold(joinCommand(githubOrg, repoName))}`);
|
|
416
700
|
console.log("");
|
|
417
701
|
ui.info("To set up a different one, choose a different team name.");
|
|
418
702
|
process.exit(0);
|
|
@@ -501,8 +785,11 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
501
785
|
report_url: "https://xgksfrirtdumacvmfkzj.supabase.co",
|
|
502
786
|
report_key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inhna3NmcmlydGR1bWFjdm1ma3pqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA5OTA1ODksImV4cCI6MjA4NjU2NjU4OX0.1UWNgER6nQazrayly1KDrswClURUkGKxeirZg5L8uk4",
|
|
503
787
|
};
|
|
504
|
-
|
|
505
|
-
|
|
788
|
+
// Joiners resolve the org by reading egregore.json from the remote — if
|
|
789
|
+
// this push silently fails (observed: transient failure right after
|
|
790
|
+
// template generation), the org is unjoinable and the founder never knows.
|
|
791
|
+
const configPushed = await retryAsync(async () => {
|
|
792
|
+
const { status } = await putFileContent(
|
|
506
793
|
githubToken,
|
|
507
794
|
githubOrg,
|
|
508
795
|
repoName,
|
|
@@ -510,19 +797,28 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
510
797
|
JSON.stringify(egreConfig, null, 2) + "\n",
|
|
511
798
|
`Configure egregore for ${orgName}`,
|
|
512
799
|
);
|
|
513
|
-
|
|
800
|
+
return status < 300;
|
|
801
|
+
});
|
|
802
|
+
if (!configPushed) {
|
|
803
|
+
ui.warn(`Could not publish egregore.json to ${githubOrg}/${repoName} — teammates can't join until it's there.`);
|
|
804
|
+
ui.info(` Re-run this installer to retry, or push egregore.json from the cloned repo.`);
|
|
805
|
+
}
|
|
514
806
|
|
|
515
807
|
// 8b. Register org with Egregore API (non-blocking — local mode works without it)
|
|
516
808
|
// Respects DO_NOT_TRACK (https://consult.gov.uk/digital-communications/online-advertising/supporting_documents/DNT%20standard%20v10.pdf)
|
|
517
809
|
if (!process.env.DO_NOT_TRACK && !process.env.EGREGORE_NO_TELEMETRY) {
|
|
518
810
|
try {
|
|
519
811
|
const api = new EgregoreAPI();
|
|
520
|
-
await api.registerOrg(githubToken, {
|
|
812
|
+
await api.registerOrg(githubToken, {
|
|
813
|
+
org_name: orgName,
|
|
814
|
+
github_org: githubOrg,
|
|
815
|
+
slug,
|
|
816
|
+
});
|
|
521
817
|
} catch {}
|
|
522
818
|
}
|
|
523
819
|
|
|
524
820
|
// 9. Init memory dirs on remote (so joiners get the structure)
|
|
525
|
-
const memDirs = ["people", "handoffs", "knowledge/decisions", "knowledge/patterns", "knowledge/findings", "quests", "wraps"];
|
|
821
|
+
const memDirs = ["people", "invites", "handoffs", "knowledge/questions", "knowledge/decisions", "knowledge/patterns", "knowledge/findings", "quests", "wraps"];
|
|
526
822
|
for (const d of memDirs) {
|
|
527
823
|
try {
|
|
528
824
|
await putFileContent(githubToken, githubOrg, memoryRepoName, `${d}/.gitkeep`, "", `Initialize ${d}`);
|
|
@@ -539,10 +835,23 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
539
835
|
const totalSteps = 6 + selectedRepos.length;
|
|
540
836
|
configureGitCredentials(githubToken);
|
|
541
837
|
|
|
838
|
+
ui.section("Install", "cloning repos, linking memory, wiring credentials");
|
|
839
|
+
|
|
542
840
|
ui.step(1, totalSteps, "Cloning egregore...");
|
|
543
841
|
cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
|
|
544
842
|
setGitIdentity(egregoreDir, user);
|
|
545
843
|
ensureDevelop(egregoreDir);
|
|
844
|
+
// ensureDevelop only creates develop on the REMOTE (git branch, no checkout),
|
|
845
|
+
// so the clone is still on main. The egregore operates on develop, so switch
|
|
846
|
+
// to it now — otherwise local overlays (the codex runtime) get committed to
|
|
847
|
+
// main and are orphaned when session-start later checks out develop.
|
|
848
|
+
try {
|
|
849
|
+
execFileSync("git", ["-C", egregoreDir, "checkout", "develop"], { stdio: "pipe" });
|
|
850
|
+
} catch {
|
|
851
|
+
try {
|
|
852
|
+
execFileSync("git", ["-C", egregoreDir, "checkout", "-b", "develop", "origin/develop"], { stdio: "pipe" });
|
|
853
|
+
} catch {}
|
|
854
|
+
}
|
|
546
855
|
|
|
547
856
|
ui.step(2, totalSteps, "Cloning shared memory...");
|
|
548
857
|
cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
|
|
@@ -564,19 +873,43 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
564
873
|
fs.writeFileSync(path.join(egregoreDir, "egregore.json"), JSON.stringify(egreConfig, null, 2) + "\n");
|
|
565
874
|
|
|
566
875
|
// 14. "What should we call you?" + write state
|
|
567
|
-
|
|
568
|
-
const displayName = await ui.prompt(
|
|
876
|
+
ui.section("Your name", "this is how teammates will see you in handoffs");
|
|
877
|
+
const displayName = await ui.prompt("what should we call you?", { default: user.login });
|
|
878
|
+
const memberRole = await ensureInstallerMemberRole(
|
|
879
|
+
{ usage_type: "founder_group", member_role: ctx.memberRole },
|
|
880
|
+
{},
|
|
881
|
+
ui
|
|
882
|
+
);
|
|
569
883
|
|
|
884
|
+
// 14b. Write onboarding-shortcut state so the /onboarding skill can skip
|
|
885
|
+
// ORIENT + INVITE-offer (the installer already collected display_name
|
|
886
|
+
// and ran the invite flow). See .claude/skills/onboarding/SKILL.md
|
|
887
|
+
// "Pre-captured check" blocks.
|
|
888
|
+
const slugifiedName = (displayName || user.login).toLowerCase().replace(/\s+/g, "-");
|
|
570
889
|
const state = {
|
|
571
890
|
github_username: user.login,
|
|
572
891
|
github_name: user.name,
|
|
573
892
|
display_name: displayName || user.login,
|
|
893
|
+
name: slugifiedName,
|
|
894
|
+
member_role: memberRole,
|
|
574
895
|
email: user.email,
|
|
575
896
|
onboarding_complete: false,
|
|
576
897
|
usage_type: "founder_group",
|
|
577
898
|
org_setup: true,
|
|
578
899
|
github_configured: true,
|
|
579
900
|
workspace_ready: true,
|
|
901
|
+
preferred_runtime: preferredRuntime,
|
|
902
|
+
// Pre-captured onboarding signals. Invite flag is set later in step 18
|
|
903
|
+
// after the user's answer; default to undefined here so the skill knows
|
|
904
|
+
// to wait, then the installer mutates this block on invite completion.
|
|
905
|
+
onboarding: {
|
|
906
|
+
phase: "orient",
|
|
907
|
+
started_at: new Date().toISOString(),
|
|
908
|
+
installer_captured: true,
|
|
909
|
+
role: "founder",
|
|
910
|
+
member_role: memberRole,
|
|
911
|
+
},
|
|
912
|
+
profile_fields_collected: ["name", "role"],
|
|
580
913
|
};
|
|
581
914
|
fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
|
|
582
915
|
|
|
@@ -625,14 +958,73 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
625
958
|
}
|
|
626
959
|
}
|
|
627
960
|
|
|
628
|
-
|
|
961
|
+
const codexRuntime = installCodexRuntime(egregoreDir);
|
|
962
|
+
reportCodexRuntimeSummary(codexRuntime, ui);
|
|
963
|
+
|
|
964
|
+
// 16b. Commit the Codex runtime into the repo so it's TRACKED — symmetric
|
|
965
|
+
// with .claude/ (which ships in the template). An untracked overlay can
|
|
966
|
+
// be lost to later git operations, and joiners would never get it. This
|
|
967
|
+
// way every Egregore supports BOTH assistants, persistently and for the
|
|
968
|
+
// whole team: pick Claude Code or Codex, now or later, and it just works.
|
|
969
|
+
try {
|
|
970
|
+
const toCommit = Array.isArray(codexRuntime.files) ? [...codexRuntime.files] : [];
|
|
971
|
+
if (fs.existsSync(path.join(egregoreDir, ".agents", "skills"))) toCommit.push(".agents/skills");
|
|
972
|
+
if (toCommit.length) {
|
|
973
|
+
execFileSync("git", ["add", "--", ...toCommit], { cwd: egregoreDir, stdio: "pipe" });
|
|
974
|
+
run('git commit -m "Add Codex runtime so this Egregore supports both assistants"', { cwd: egregoreDir });
|
|
975
|
+
run("git push", { cwd: egregoreDir });
|
|
976
|
+
}
|
|
977
|
+
} catch {}
|
|
978
|
+
|
|
979
|
+
// 17. Register instance + install launcher binary
|
|
629
980
|
ui.step(5 + selectedRepos.length, totalSteps, "Registering instance...");
|
|
630
|
-
registerInstance(repoName, orgName, egregoreDir);
|
|
631
|
-
const alias = await
|
|
981
|
+
registerInstance(repoName, orgName, egregoreDir, preferredRuntime);
|
|
982
|
+
const alias = await installLauncherBinary(egregoreDir, ui);
|
|
983
|
+
|
|
984
|
+
const telegramResult = await maybeSetupTelegramGroup(ui, {
|
|
985
|
+
egregoreDir,
|
|
986
|
+
githubToken,
|
|
987
|
+
githubOrg,
|
|
988
|
+
repoName,
|
|
989
|
+
egreConfig,
|
|
990
|
+
statePath: path.join(egregoreDir, ".egregore-state.json"),
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// 18. Optional invite — picker for org members, text fallback otherwise.
|
|
994
|
+
ui.section("Bring someone in", "Egregore shines when it's shared");
|
|
995
|
+
|
|
996
|
+
let invUsername = "";
|
|
997
|
+
if (isOrg) {
|
|
998
|
+
const memberSpin = ui.spinner("Loading org members...");
|
|
999
|
+
const members = await listOrgMembers(githubToken, githubOrg);
|
|
1000
|
+
memberSpin.stop(members.length > 0 ? `Loaded ${members.length} members` : "No visible members");
|
|
1001
|
+
|
|
1002
|
+
// Filter out the current user — they can't invite themselves.
|
|
1003
|
+
const pickable = members
|
|
1004
|
+
.filter((m) => m.login !== user.login)
|
|
1005
|
+
.slice(0, 20);
|
|
1006
|
+
|
|
1007
|
+
if (pickable.length > 0) {
|
|
1008
|
+
const choices = [
|
|
1009
|
+
{ label: "Skip — work solo for now", description: "I can always /invite later" },
|
|
1010
|
+
...pickable.map((m) => ({ label: m.login, description: `@${githubOrg} member` })),
|
|
1011
|
+
{ label: "Type a username", description: "invite someone not in the list" },
|
|
1012
|
+
];
|
|
1013
|
+
const picked = await ui.choose("who should we invite?", choices);
|
|
1014
|
+
if (picked.label === "Skip — work solo for now") {
|
|
1015
|
+
invUsername = "";
|
|
1016
|
+
} else if (picked.label === "Type a username") {
|
|
1017
|
+
invUsername = await ui.prompt("GitHub username");
|
|
1018
|
+
} else {
|
|
1019
|
+
invUsername = picked.label;
|
|
1020
|
+
}
|
|
1021
|
+
} else {
|
|
1022
|
+
invUsername = await ui.prompt("teammate's GitHub username (or Enter to skip)");
|
|
1023
|
+
}
|
|
1024
|
+
} else {
|
|
1025
|
+
invUsername = await ui.prompt("teammate's GitHub username (or Enter to skip)");
|
|
1026
|
+
}
|
|
632
1027
|
|
|
633
|
-
// 18. Optional invite
|
|
634
|
-
console.log("");
|
|
635
|
-
const invUsername = await ui.prompt("Invite a teammate? Enter their GitHub username (or press Enter to skip):");
|
|
636
1028
|
if (invUsername) {
|
|
637
1029
|
const invSpin = ui.spinner(`Inviting ${invUsername}...`);
|
|
638
1030
|
try {
|
|
@@ -657,36 +1049,106 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
657
1049
|
const invPersonFile = path.join(memoryDir, "people", `${invUsername}.md`);
|
|
658
1050
|
if (!fs.existsSync(invPersonFile)) {
|
|
659
1051
|
fs.writeFileSync(invPersonFile, invPersonContent);
|
|
660
|
-
try {
|
|
661
|
-
run("git add -A", { cwd: memoryDir });
|
|
662
|
-
run(`git commit -m "Invite ${invUsername}"`, { cwd: memoryDir });
|
|
663
|
-
run("git push", { cwd: memoryDir });
|
|
664
|
-
} catch {}
|
|
665
1052
|
}
|
|
1053
|
+
const fallbackCommand = joinCommand(githubOrg, repoName);
|
|
1054
|
+
const inviteArtifact = writeInviteArtifact(memoryDir, {
|
|
1055
|
+
inviter: user.login,
|
|
1056
|
+
invitee: invUsername,
|
|
1057
|
+
githubOrg,
|
|
1058
|
+
repoName,
|
|
1059
|
+
memoryRepo: `https://github.com/${githubOrg}/${memoryRepoName}.git`,
|
|
1060
|
+
managedRepos: selectedRepos,
|
|
1061
|
+
fallbackCommand,
|
|
1062
|
+
telegramGroupLink: egreConfig.telegram_group_link || "",
|
|
1063
|
+
delivery: "copyable_share_block",
|
|
1064
|
+
});
|
|
1065
|
+
commitMemoryInvite(memoryDir, [invPersonFile, inviteArtifact], invUsername);
|
|
666
1066
|
|
|
667
1067
|
invSpin.stop(`Invited ${invUsername}`);
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1068
|
+
ui.info("send this to them:");
|
|
1069
|
+
const shareLines = [fallbackCommand];
|
|
1070
|
+
if (egreConfig.telegram_group_link) {
|
|
1071
|
+
shareLines.push(`Telegram: ${egreConfig.telegram_group_link}`);
|
|
1072
|
+
}
|
|
1073
|
+
ui.card("share", shareLines);
|
|
671
1074
|
} catch (err) {
|
|
672
1075
|
invSpin.fail(`Could not invite ${invUsername}: ${err.message}`);
|
|
673
1076
|
}
|
|
674
1077
|
}
|
|
675
1078
|
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1079
|
+
// 18b. Record invite outcome in state so the /onboarding skill knows to
|
|
1080
|
+
// skip the INVITE-offer step. See SKILL.md "Pre-captured check".
|
|
1081
|
+
{
|
|
1082
|
+
const statePath = path.join(egregoreDir, ".egregore-state.json");
|
|
1083
|
+
try {
|
|
1084
|
+
const cur = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
1085
|
+
cur.onboarding = cur.onboarding || {};
|
|
1086
|
+
cur.onboarding.invite_skipped = !invUsername;
|
|
1087
|
+
cur.onboarding.invite_handled_by = "installer";
|
|
1088
|
+
cur.onboarding.phase = "complete";
|
|
1089
|
+
cur.onboarding.completed_at = cur.onboarding.completed_at || new Date().toISOString();
|
|
1090
|
+
cur.onboarding.role = cur.onboarding.role || "founder";
|
|
1091
|
+
cur.onboarding_complete = true;
|
|
1092
|
+
cur.profile_fields_collected = Array.isArray(cur.profile_fields_collected)
|
|
1093
|
+
? Array.from(new Set([...cur.profile_fields_collected, "name"]))
|
|
1094
|
+
: ["name"];
|
|
1095
|
+
fs.writeFileSync(statePath, JSON.stringify(cur, null, 2) + "\n");
|
|
1096
|
+
ensurePersonOnboardingWitness(memoryDir, cur);
|
|
1097
|
+
} catch {}
|
|
686
1098
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1099
|
+
|
|
1100
|
+
// 19. Done — summary screen (alt-screen) if we can, inline otherwise.
|
|
1101
|
+
// Either way it doubles as the Insight-rendering moment that lets
|
|
1102
|
+
// /onboarding skip ORIENT on first Claude session.
|
|
1103
|
+
const workspaces = [
|
|
1104
|
+
{ label: `./${repoName}/`, description: "your Egregore instance" },
|
|
1105
|
+
{ label: `./${memoryRepoName}/`, description: "shared knowledge" },
|
|
1106
|
+
...selectedRepos.map((entry) => {
|
|
1107
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
1108
|
+
return { label: `./${mrName}/`, description: "managed repo" };
|
|
1109
|
+
}),
|
|
1110
|
+
];
|
|
1111
|
+
const assistantName = preferredRuntime === "codex" ? "Codex" : "Claude Code";
|
|
1112
|
+
const setupSummary = {
|
|
1113
|
+
workspace: "created",
|
|
1114
|
+
invite: invUsername ? `sent to ${invUsername}` : "skipped",
|
|
1115
|
+
telegram: telegramResult.groupLink
|
|
1116
|
+
? "connected"
|
|
1117
|
+
: telegramResult.setup
|
|
1118
|
+
? "bot added; no join link saved"
|
|
1119
|
+
: "skipped",
|
|
1120
|
+
assistant: assistantName,
|
|
1121
|
+
command: "run egregore anytime",
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
if (!ctx.noScreens && ui.canShowScreens()) {
|
|
1125
|
+
const result = await ui.summaryScreen({ orgName, workspaces, setup: setupSummary });
|
|
1126
|
+
if (result === "escape") {
|
|
1127
|
+
// User pressed esc: skip auto-launch, just return to shell.
|
|
1128
|
+
ctx.noAutolaunch = true;
|
|
1129
|
+
}
|
|
1130
|
+
} else {
|
|
1131
|
+
ui.section("You're done", `Egregore is ready for ${ui.bold(orgName)}`);
|
|
1132
|
+
ui.info(`${ui.green("●")} workspace ${setupSummary.workspace}`);
|
|
1133
|
+
ui.info(`${ui.green("●")} invite ${setupSummary.invite}`);
|
|
1134
|
+
ui.info(`${ui.green("●")} Telegram ${setupSummary.telegram}`);
|
|
1135
|
+
ui.info(`${ui.green("●")} assistant ${setupSummary.assistant}`);
|
|
1136
|
+
ui.info(`${ui.green("●")} next ${setupSummary.command}`);
|
|
1137
|
+
console.log("");
|
|
1138
|
+
for (const ws of workspaces) {
|
|
1139
|
+
ui.info(`${ui.green("●")} ${ui.cyan(ws.label)} ${ui.dim(ws.description)}`);
|
|
1140
|
+
}
|
|
1141
|
+
ui.note([
|
|
1142
|
+
"Every session leaves traces — decisions, patterns, context.",
|
|
1143
|
+
"Egregore makes those traces persistent and shared.",
|
|
1144
|
+
"",
|
|
1145
|
+
"You do your work. When you're done, you hand off what you",
|
|
1146
|
+
"learned. The next session — yours or someone else's — starts",
|
|
1147
|
+
"smarter.",
|
|
1148
|
+
]);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
await launchOrInstruct(egregoreDir, alias, ui, ctx);
|
|
690
1152
|
}
|
|
691
1153
|
|
|
692
1154
|
// ── Flow 2: Join (local mode) ──────────────────────────────────────
|
|
@@ -714,22 +1176,43 @@ function parseJoinTarget(arg) {
|
|
|
714
1176
|
return { orgLogin: parts[0], explicitRepo: parts[1] || null };
|
|
715
1177
|
}
|
|
716
1178
|
|
|
1179
|
+
function normalizeMemoryUrl(memoryRepo, owner, fallbackName) {
|
|
1180
|
+
if (memoryRepo && /^https?:\/\//i.test(memoryRepo)) return memoryRepo;
|
|
1181
|
+
if (memoryRepo && /^git@/i.test(memoryRepo)) return memoryRepo;
|
|
1182
|
+
const repoName = memoryRepo || fallbackName;
|
|
1183
|
+
return `https://github.com/${owner}/${repoName}.git`;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
717
1186
|
async function localJoinFlow(orgArg, ui, ctx = {}) {
|
|
718
1187
|
const { orgLogin, explicitRepo } = parseJoinTarget(orgArg);
|
|
719
1188
|
if (!orgLogin) {
|
|
720
|
-
ui.error("Usage: npx create-egregore join <org>[/<repo>]");
|
|
1189
|
+
ui.error("Usage: npx -y create-egregore@latest join <org>[/<repo>]");
|
|
721
1190
|
process.exit(1);
|
|
722
1191
|
}
|
|
723
1192
|
|
|
724
|
-
// 1. GitHub auth —
|
|
725
|
-
|
|
1193
|
+
// 1. GitHub auth — prefer gh CLI. If gh is installed but logged out, run
|
|
1194
|
+
// gh auth login. Only no-gh machines use the device fallback.
|
|
1195
|
+
ui.info(`Joining ${ui.bold(orgLogin)}.`);
|
|
726
1196
|
let githubToken;
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
ui.success("
|
|
730
|
-
|
|
731
|
-
|
|
1197
|
+
const ghAuth = tryGhAuthOrLogin(ui, { mode: "join" });
|
|
1198
|
+
if (ghAuth) {
|
|
1199
|
+
ui.success(`Using your ${ui.bold("gh")} CLI auth${ghAuth.user ? ` (${ghAuth.user})` : ""}`);
|
|
1200
|
+
githubToken = ghAuth.token;
|
|
1201
|
+
} else if (ghCliExists()) {
|
|
1202
|
+
ui.error("GitHub CLI auth did not complete.");
|
|
1203
|
+
ui.info(`Run ${ui.bold("gh auth login")} and then re-run this join command.`);
|
|
732
1204
|
process.exit(1);
|
|
1205
|
+
} else {
|
|
1206
|
+
ui.note(ghFallbackNote(ui));
|
|
1207
|
+
const cmd = ghInstallShellCommand();
|
|
1208
|
+
if (cmd) {
|
|
1209
|
+
const ok = await ui.confirm(`Install GitHub CLI now and sign in? (${ui.bold(cmd)})`, true);
|
|
1210
|
+
if (ok) githubToken = runGhInstallAndLogin(ui, cmd);
|
|
1211
|
+
}
|
|
1212
|
+
if (!githubToken) {
|
|
1213
|
+
ui.info("No problem — run the steps above when ready, then re-run this.");
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
733
1216
|
}
|
|
734
1217
|
|
|
735
1218
|
// 2. Get user info
|
|
@@ -741,111 +1224,70 @@ async function localJoinFlow(orgArg, ui, ctx = {}) {
|
|
|
741
1224
|
await acceptPendingInvitations(githubToken, orgLogin, ui);
|
|
742
1225
|
s1.stop("Invitations accepted");
|
|
743
1226
|
|
|
744
|
-
// 4.
|
|
1227
|
+
// 4. Resolve egregore.json before cloning. The shared install() helper owns
|
|
1228
|
+
// all local filesystem setup after this point.
|
|
745
1229
|
const repoName = explicitRepo || "egregore";
|
|
746
|
-
|
|
747
|
-
// 5. Clone repos using git credentials (SSH keys / gh CLI / credential helper)
|
|
748
|
-
// The OAuth token only has repo:invite scope — cloning uses the joiner's own git setup.
|
|
749
|
-
const base = ctx.targetDir || process.cwd();
|
|
750
|
-
const egregoreDir = path.join(base, repoName);
|
|
751
|
-
|
|
752
|
-
console.log("");
|
|
753
|
-
const s2 = ui.spinner(`Cloning ${orgLogin}/${repoName}...`);
|
|
754
|
-
if (cloneWithFallback(`https://github.com/${orgLogin}/${repoName}.git`, egregoreDir)) {
|
|
755
|
-
s2.stop(`Cloned ${repoName}`);
|
|
756
|
-
} else {
|
|
757
|
-
s2.fail(`Could not clone ${orgLogin}/${repoName}`);
|
|
758
|
-
ui.error("Make sure you have git credentials set up (SSH keys or gh auth login).");
|
|
759
|
-
ui.error("Also verify the org admin has set up Egregore and invited you.");
|
|
760
|
-
process.exit(1);
|
|
761
|
-
}
|
|
762
|
-
setGitIdentity(egregoreDir, user);
|
|
763
|
-
ensureDevelop(egregoreDir);
|
|
764
|
-
|
|
765
|
-
// 6. Read egregore.json from local clone
|
|
766
|
-
const configPath = path.join(egregoreDir, "egregore.json");
|
|
767
|
-
if (!fs.existsSync(configPath)) {
|
|
768
|
-
ui.error("The repo has no egregore.json. Ask the admin to set up first.");
|
|
769
|
-
process.exit(1);
|
|
770
|
-
}
|
|
1230
|
+
const s2 = ui.spinner(`Reading ${orgLogin}/${repoName}/egregore.json...`);
|
|
771
1231
|
let config;
|
|
772
1232
|
try {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1233
|
+
const raw = await getFileContent(githubToken, orgLogin, repoName, "egregore.json");
|
|
1234
|
+
if (!raw) throw new Error("missing");
|
|
1235
|
+
config = JSON.parse(raw);
|
|
1236
|
+
s2.stop("Resolved Egregore config");
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
s2.fail("Could not read egregore.json");
|
|
1239
|
+
ui.error(`Make sure ${orgLogin}/${repoName} exists and you accepted the GitHub invitation.`);
|
|
1240
|
+
ui.info(`Ask the admin to resend: ${joinCommand(orgLogin, repoName)}`);
|
|
776
1241
|
process.exit(1);
|
|
777
1242
|
}
|
|
778
1243
|
ui.success(`Found ${ui.bold(config.org_name || orgLogin)}`);
|
|
779
1244
|
|
|
780
1245
|
const orgName = config.org_name || orgLogin;
|
|
781
|
-
const memoryRepoName = config.memory_repo
|
|
782
|
-
? config.memory_repo.split("/").pop().replace(/\.git$/, "")
|
|
783
|
-
: `${orgLogin}-memory`;
|
|
784
1246
|
const managedRepos = config.repos || [];
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
// Store the invite token for later use (invitation acceptance, not cloning)
|
|
792
|
-
const inviteToken = githubToken;
|
|
793
|
-
|
|
794
|
-
// Clone memory using SSH-first fallback (same as main repo)
|
|
795
|
-
ui.step(1, totalSteps, "Cloning shared memory...");
|
|
796
|
-
if (cloneWithFallback(memoryUrl, memoryDir)) {
|
|
797
|
-
ui.success("Cloned memory");
|
|
798
|
-
} else {
|
|
799
|
-
ui.warn("Could not clone memory repo — you may not have access yet.");
|
|
1247
|
+
const skipRepos = new Set();
|
|
1248
|
+
if (ctx.prefilledRepo && Array.isArray(managedRepos)) {
|
|
1249
|
+
for (const entry of managedRepos) {
|
|
1250
|
+
const name = repoEntryName(entry);
|
|
1251
|
+
if (name && name === ctx.prefilledRepo.name) skipRepos.add(name);
|
|
1252
|
+
}
|
|
800
1253
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
ui.step(3, totalSteps, "Writing credentials...");
|
|
809
|
-
fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${inviteToken}\n`, { mode: 0o600 });
|
|
810
|
-
ui.success("Credentials saved");
|
|
811
|
-
|
|
812
|
-
// "What should we call you?" + write state
|
|
813
|
-
console.log("");
|
|
814
|
-
const displayName = await ui.prompt(`What should we call you? [${user.login}]:`);
|
|
815
|
-
|
|
816
|
-
const state = {
|
|
1254
|
+
const memoryUrl = normalizeMemoryUrl(config.memory_repo, orgLogin, `${repoName}-memory`);
|
|
1255
|
+
const installData = {
|
|
1256
|
+
fork_url: `https://github.com/${orgLogin}/${repoName}.git`,
|
|
1257
|
+
memory_url: memoryUrl,
|
|
1258
|
+
github_token: githubToken,
|
|
1259
|
+
org_name: orgName,
|
|
1260
|
+
github_org: orgLogin,
|
|
817
1261
|
github_username: user.login,
|
|
818
1262
|
github_name: user.name,
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1263
|
+
github_email: user.email,
|
|
1264
|
+
slug: config.slug || orgLogin,
|
|
1265
|
+
repos: managedRepos,
|
|
1266
|
+
repo_name: repoName,
|
|
1267
|
+
telegram_group_link: config.telegram_group_link,
|
|
822
1268
|
usage_type: "joiner_group",
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
workspace_ready: true,
|
|
1269
|
+
onboarding_role: "joiner",
|
|
1270
|
+
invite_handled_by: "installer",
|
|
826
1271
|
};
|
|
827
|
-
fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
|
|
828
|
-
|
|
829
|
-
// Register + alias
|
|
830
|
-
ui.step(4, totalSteps, "Registering instance...");
|
|
831
|
-
registerInstance(repoName, orgName, egregoreDir);
|
|
832
|
-
const alias = await installShellAlias(egregoreDir, ui);
|
|
833
1272
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
1273
|
+
try {
|
|
1274
|
+
await install(installData, ui, ctx.targetDir, {
|
|
1275
|
+
noAutolaunch: ctx.noAutolaunch,
|
|
1276
|
+
noScreens: ctx.noScreens,
|
|
1277
|
+
skipRepos,
|
|
1278
|
+
flowVariant: ctx.flowVariant,
|
|
1279
|
+
runtime: ctx.runtime,
|
|
1280
|
+
});
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
ui.error(`Install failed: ${err.message}`);
|
|
1283
|
+
process.exit(1);
|
|
846
1284
|
}
|
|
847
1285
|
|
|
848
|
-
|
|
1286
|
+
const base = ctx.targetDir || process.cwd();
|
|
1287
|
+
const egregoreDir = path.join(base, repoName);
|
|
1288
|
+
const memoryDir = path.join(base, memoryUrl.split("/").pop().replace(/\.git$/, ""));
|
|
1289
|
+
|
|
1290
|
+
// 5. Check for welcome note after install() has cloned and linked memory.
|
|
849
1291
|
const welcomeFile = path.join(memoryDir, "people", `${user.login}.md`);
|
|
850
1292
|
if (fs.existsSync(welcomeFile)) {
|
|
851
1293
|
try {
|
|
@@ -861,27 +1303,12 @@ async function localJoinFlow(orgArg, ui, ctx = {}) {
|
|
|
861
1303
|
} catch {}
|
|
862
1304
|
}
|
|
863
1305
|
|
|
864
|
-
//
|
|
1306
|
+
// 6. Show Telegram group link if configured
|
|
865
1307
|
if (config.telegram_group_link) {
|
|
866
1308
|
console.log("");
|
|
867
1309
|
ui.info("Join the team's Telegram group:");
|
|
868
1310
|
ui.info(` ${ui.cyan(config.telegram_group_link)}`);
|
|
869
1311
|
}
|
|
870
|
-
|
|
871
|
-
// 14. Done
|
|
872
|
-
console.log("");
|
|
873
|
-
ui.success(`Joined ${ui.bold(orgName)}`);
|
|
874
|
-
console.log("");
|
|
875
|
-
ui.info("Your workspace:");
|
|
876
|
-
ui.info(` ${ui.cyan(`./${repoName}/`)} — Egregore instance`);
|
|
877
|
-
ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
|
|
878
|
-
for (const entry of managedRepos) {
|
|
879
|
-
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
880
|
-
ui.info(` ${ui.cyan(`./${mrName}/`)} — Managed repo`);
|
|
881
|
-
}
|
|
882
|
-
console.log("");
|
|
883
|
-
ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
|
|
884
|
-
console.log("");
|
|
885
1312
|
}
|
|
886
1313
|
|
|
887
|
-
module.exports = { localFounderFlow, localJoinFlow, parseJoinTarget, addCollaborator, getUser, repoExists, createRepo, waitForRepo };
|
|
1314
|
+
module.exports = { localFounderFlow, localJoinFlow, parseJoinTarget, addCollaborator, getUser, repoExists, createRepo, waitForRepo, retryAsync, ghFallbackNote };
|