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/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;
|
|
@@ -86,9 +114,7 @@ async function findEgregoreRepo(token, orgLogin, explicitRepo) {
|
|
|
86
114
|
if (explicitRepo) {
|
|
87
115
|
if (await repoExists(token, orgLogin, explicitRepo)) return explicitRepo;
|
|
88
116
|
} else {
|
|
89
|
-
|
|
90
|
-
if (await repoExists(token, orgLogin, candidate)) return candidate;
|
|
91
|
-
}
|
|
117
|
+
if (await repoExists(token, orgLogin, "egregore")) return "egregore";
|
|
92
118
|
}
|
|
93
119
|
return null;
|
|
94
120
|
}
|
|
@@ -118,9 +144,19 @@ async function putFileContent(token, owner, repo, filePath, content, message) {
|
|
|
118
144
|
|
|
119
145
|
async function listOrgRepos(token, owner, isOrg) {
|
|
120
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.
|
|
121
157
|
const baseUrl = isOrg
|
|
122
158
|
? `/orgs/${owner}/repos?per_page=100&sort=updated&type=all`
|
|
123
|
-
: `/
|
|
159
|
+
: `/user/repos?per_page=100&sort=updated&affiliation=owner`;
|
|
124
160
|
|
|
125
161
|
let allRepos = [];
|
|
126
162
|
let page = 1;
|
|
@@ -133,8 +169,16 @@ async function listOrgRepos(token, owner, isOrg) {
|
|
|
133
169
|
page++;
|
|
134
170
|
}
|
|
135
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
|
+
|
|
136
180
|
return allRepos
|
|
137
|
-
.filter((r) => !r.archived && r.name !== "egregore" &&
|
|
181
|
+
.filter((r) => !r.archived && r.name !== "egregore" && !r.name.endsWith("-memory"))
|
|
138
182
|
.map((r) => ({ name: r.name, language: r.language || "", description: r.description || "" }));
|
|
139
183
|
}
|
|
140
184
|
|
|
@@ -178,6 +222,7 @@ function cloneRepo(url, dir, token, ui, label) {
|
|
|
178
222
|
stdio: "pipe",
|
|
179
223
|
encoding: "utf-8",
|
|
180
224
|
timeout: 60000,
|
|
225
|
+
env: gitCloneEnv(),
|
|
181
226
|
});
|
|
182
227
|
try {
|
|
183
228
|
run(`git remote set-url origin ${url}`, { cwd: dir });
|
|
@@ -203,7 +248,7 @@ function toSSH(url) {
|
|
|
203
248
|
* GIT_TERMINAL_PROMPT=0 prevents the hanging username/password prompt.
|
|
204
249
|
*/
|
|
205
250
|
function cloneWithFallback(httpsUrl, dir, timeout = 60000) {
|
|
206
|
-
const env =
|
|
251
|
+
const env = gitCloneEnv();
|
|
207
252
|
const sshUrl = toSSH(httpsUrl);
|
|
208
253
|
|
|
209
254
|
// Try SSH first (most devs have SSH keys; GitHub no longer supports HTTPS password auth)
|
|
@@ -245,52 +290,263 @@ function createSymlink(egregoreDir, memoryDir, ui) {
|
|
|
245
290
|
ui.success("Linked");
|
|
246
291
|
}
|
|
247
292
|
|
|
248
|
-
//
|
|
249
|
-
|
|
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
|
+
}
|
|
250
481
|
|
|
251
482
|
// ── Flow 1: Founder (local mode) ───────────────────────────────────
|
|
252
483
|
|
|
253
484
|
async function localFounderFlow(ui, ctx = {}) {
|
|
485
|
+
const preferredRuntime = normalizeRuntime(ctx.runtime);
|
|
254
486
|
// Names of repos already on disk as the user's own workdir — skip clone/pull.
|
|
255
487
|
const skipRepos = ctx.skipRepos instanceof Set ? new Set(ctx.skipRepos) : new Set();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
|
|
265
498
|
let githubToken;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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.`);
|
|
271
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
|
+
}
|
|
272
523
|
}
|
|
273
524
|
|
|
274
525
|
// 2. Get user info
|
|
275
526
|
const user = await getUser(githubToken);
|
|
276
527
|
|
|
277
|
-
// 3. Where — always
|
|
278
|
-
|
|
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?");
|
|
279
531
|
const s = ui.spinner("Checking your GitHub accounts...");
|
|
280
532
|
const orgs = await listOrgs(githubToken);
|
|
281
|
-
s.stop("");
|
|
533
|
+
s.stop("Accounts loaded");
|
|
282
534
|
|
|
283
535
|
let githubOrg, isOrg;
|
|
284
536
|
if (orgs.length === 0) {
|
|
285
537
|
githubOrg = user.login;
|
|
286
538
|
isOrg = false;
|
|
287
|
-
ui.info(`Using personal account: ${ui.bold(githubOrg)}`);
|
|
539
|
+
ui.info(`Using your personal account: ${ui.bold(githubOrg)}`);
|
|
288
540
|
} else {
|
|
289
541
|
const orgChoices = [{ label: user.login, description: "Personal account" }];
|
|
290
542
|
for (const o of orgs) {
|
|
291
543
|
orgChoices.push({ label: o.login, description: "Organization" });
|
|
292
544
|
}
|
|
293
|
-
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
|
+
);
|
|
294
550
|
githubOrg = orgChoice.label;
|
|
295
551
|
isOrg = githubOrg !== user.login;
|
|
296
552
|
}
|
|
@@ -302,8 +558,7 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
302
558
|
let isNewProject;
|
|
303
559
|
if (ctx.prefilledRepo) {
|
|
304
560
|
isNewProject = false;
|
|
305
|
-
|
|
306
|
-
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.`);
|
|
307
562
|
} else {
|
|
308
563
|
const action = await ui.choose("What are you working on?", [
|
|
309
564
|
{ label: "Starting something new", description: "Create a project from scratch" },
|
|
@@ -313,28 +568,55 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
313
568
|
}
|
|
314
569
|
|
|
315
570
|
// 5. Collect project info
|
|
571
|
+
ui.section("Name it", "what's this Egregore called?");
|
|
316
572
|
let orgName, description, selectedRepos = [], newProjectRepo = null;
|
|
317
573
|
|
|
318
574
|
if (isNewProject) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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`;
|
|
324
587
|
|
|
325
588
|
// Ask about project repo
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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;
|
|
333
616
|
}
|
|
334
617
|
} else {
|
|
335
618
|
// "Existing project"
|
|
336
|
-
|
|
337
|
-
orgName = nameInput || githubOrg;
|
|
619
|
+
orgName = await ui.prompt("what should we call this Egregore?", { default: githubOrg });
|
|
338
620
|
description = "";
|
|
339
621
|
|
|
340
622
|
// Auto-adopt the user's current repo if it's under the chosen GitHub owner.
|
|
@@ -414,7 +696,7 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
414
696
|
s1.stop(`${githubOrg}/${repoName} already has Egregore set up`);
|
|
415
697
|
console.log("");
|
|
416
698
|
ui.info("To join it:");
|
|
417
|
-
ui.info(` ${ui.bold(
|
|
699
|
+
ui.info(` ${ui.bold(joinCommand(githubOrg, repoName))}`);
|
|
418
700
|
console.log("");
|
|
419
701
|
ui.info("To set up a different one, choose a different team name.");
|
|
420
702
|
process.exit(0);
|
|
@@ -503,8 +785,11 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
503
785
|
report_url: "https://xgksfrirtdumacvmfkzj.supabase.co",
|
|
504
786
|
report_key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inhna3NmcmlydGR1bWFjdm1ma3pqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA5OTA1ODksImV4cCI6MjA4NjU2NjU4OX0.1UWNgER6nQazrayly1KDrswClURUkGKxeirZg5L8uk4",
|
|
505
787
|
};
|
|
506
|
-
|
|
507
|
-
|
|
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(
|
|
508
793
|
githubToken,
|
|
509
794
|
githubOrg,
|
|
510
795
|
repoName,
|
|
@@ -512,19 +797,28 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
512
797
|
JSON.stringify(egreConfig, null, 2) + "\n",
|
|
513
798
|
`Configure egregore for ${orgName}`,
|
|
514
799
|
);
|
|
515
|
-
|
|
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
|
+
}
|
|
516
806
|
|
|
517
807
|
// 8b. Register org with Egregore API (non-blocking — local mode works without it)
|
|
518
808
|
// Respects DO_NOT_TRACK (https://consult.gov.uk/digital-communications/online-advertising/supporting_documents/DNT%20standard%20v10.pdf)
|
|
519
809
|
if (!process.env.DO_NOT_TRACK && !process.env.EGREGORE_NO_TELEMETRY) {
|
|
520
810
|
try {
|
|
521
811
|
const api = new EgregoreAPI();
|
|
522
|
-
await api.registerOrg(githubToken, {
|
|
812
|
+
await api.registerOrg(githubToken, {
|
|
813
|
+
org_name: orgName,
|
|
814
|
+
github_org: githubOrg,
|
|
815
|
+
slug,
|
|
816
|
+
});
|
|
523
817
|
} catch {}
|
|
524
818
|
}
|
|
525
819
|
|
|
526
820
|
// 9. Init memory dirs on remote (so joiners get the structure)
|
|
527
|
-
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"];
|
|
528
822
|
for (const d of memDirs) {
|
|
529
823
|
try {
|
|
530
824
|
await putFileContent(githubToken, githubOrg, memoryRepoName, `${d}/.gitkeep`, "", `Initialize ${d}`);
|
|
@@ -541,10 +835,23 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
541
835
|
const totalSteps = 6 + selectedRepos.length;
|
|
542
836
|
configureGitCredentials(githubToken);
|
|
543
837
|
|
|
838
|
+
ui.section("Install", "cloning repos, linking memory, wiring credentials");
|
|
839
|
+
|
|
544
840
|
ui.step(1, totalSteps, "Cloning egregore...");
|
|
545
841
|
cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
|
|
546
842
|
setGitIdentity(egregoreDir, user);
|
|
547
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
|
+
}
|
|
548
855
|
|
|
549
856
|
ui.step(2, totalSteps, "Cloning shared memory...");
|
|
550
857
|
cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
|
|
@@ -566,19 +873,43 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
566
873
|
fs.writeFileSync(path.join(egregoreDir, "egregore.json"), JSON.stringify(egreConfig, null, 2) + "\n");
|
|
567
874
|
|
|
568
875
|
// 14. "What should we call you?" + write state
|
|
569
|
-
|
|
570
|
-
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
|
+
);
|
|
571
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, "-");
|
|
572
889
|
const state = {
|
|
573
890
|
github_username: user.login,
|
|
574
891
|
github_name: user.name,
|
|
575
892
|
display_name: displayName || user.login,
|
|
893
|
+
name: slugifiedName,
|
|
894
|
+
member_role: memberRole,
|
|
576
895
|
email: user.email,
|
|
577
896
|
onboarding_complete: false,
|
|
578
897
|
usage_type: "founder_group",
|
|
579
898
|
org_setup: true,
|
|
580
899
|
github_configured: true,
|
|
581
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"],
|
|
582
913
|
};
|
|
583
914
|
fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
|
|
584
915
|
|
|
@@ -627,14 +958,73 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
627
958
|
}
|
|
628
959
|
}
|
|
629
960
|
|
|
630
|
-
|
|
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
|
|
631
980
|
ui.step(5 + selectedRepos.length, totalSteps, "Registering instance...");
|
|
632
|
-
registerInstance(repoName, orgName, egregoreDir);
|
|
633
|
-
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
|
+
}
|
|
634
1027
|
|
|
635
|
-
// 18. Optional invite
|
|
636
|
-
console.log("");
|
|
637
|
-
const invUsername = await ui.prompt("Invite a teammate? Enter their GitHub username (or press Enter to skip):");
|
|
638
1028
|
if (invUsername) {
|
|
639
1029
|
const invSpin = ui.spinner(`Inviting ${invUsername}...`);
|
|
640
1030
|
try {
|
|
@@ -659,36 +1049,106 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
659
1049
|
const invPersonFile = path.join(memoryDir, "people", `${invUsername}.md`);
|
|
660
1050
|
if (!fs.existsSync(invPersonFile)) {
|
|
661
1051
|
fs.writeFileSync(invPersonFile, invPersonContent);
|
|
662
|
-
try {
|
|
663
|
-
run("git add -A", { cwd: memoryDir });
|
|
664
|
-
run(`git commit -m "Invite ${invUsername}"`, { cwd: memoryDir });
|
|
665
|
-
run("git push", { cwd: memoryDir });
|
|
666
|
-
} catch {}
|
|
667
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);
|
|
668
1066
|
|
|
669
1067
|
invSpin.stop(`Invited ${invUsername}`);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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);
|
|
673
1074
|
} catch (err) {
|
|
674
1075
|
invSpin.fail(`Could not invite ${invUsername}: ${err.message}`);
|
|
675
1076
|
}
|
|
676
1077
|
}
|
|
677
1078
|
|
|
678
|
-
//
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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 {}
|
|
688
1098
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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);
|
|
692
1152
|
}
|
|
693
1153
|
|
|
694
1154
|
// ── Flow 2: Join (local mode) ──────────────────────────────────────
|
|
@@ -716,22 +1176,43 @@ function parseJoinTarget(arg) {
|
|
|
716
1176
|
return { orgLogin: parts[0], explicitRepo: parts[1] || null };
|
|
717
1177
|
}
|
|
718
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
|
+
|
|
719
1186
|
async function localJoinFlow(orgArg, ui, ctx = {}) {
|
|
720
1187
|
const { orgLogin, explicitRepo } = parseJoinTarget(orgArg);
|
|
721
1188
|
if (!orgLogin) {
|
|
722
|
-
ui.error("Usage: npx create-egregore join <org>[/<repo>]");
|
|
1189
|
+
ui.error("Usage: npx -y create-egregore@latest join <org>[/<repo>]");
|
|
723
1190
|
process.exit(1);
|
|
724
1191
|
}
|
|
725
1192
|
|
|
726
|
-
// 1. GitHub auth —
|
|
727
|
-
|
|
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)}.`);
|
|
728
1196
|
let githubToken;
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
ui.success("
|
|
732
|
-
|
|
733
|
-
|
|
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.`);
|
|
734
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
|
+
}
|
|
735
1216
|
}
|
|
736
1217
|
|
|
737
1218
|
// 2. Get user info
|
|
@@ -743,111 +1224,70 @@ async function localJoinFlow(orgArg, ui, ctx = {}) {
|
|
|
743
1224
|
await acceptPendingInvitations(githubToken, orgLogin, ui);
|
|
744
1225
|
s1.stop("Invitations accepted");
|
|
745
1226
|
|
|
746
|
-
// 4.
|
|
1227
|
+
// 4. Resolve egregore.json before cloning. The shared install() helper owns
|
|
1228
|
+
// all local filesystem setup after this point.
|
|
747
1229
|
const repoName = explicitRepo || "egregore";
|
|
748
|
-
|
|
749
|
-
// 5. Clone repos using git credentials (SSH keys / gh CLI / credential helper)
|
|
750
|
-
// The OAuth token only has repo:invite scope — cloning uses the joiner's own git setup.
|
|
751
|
-
const base = ctx.targetDir || process.cwd();
|
|
752
|
-
const egregoreDir = path.join(base, repoName);
|
|
753
|
-
|
|
754
|
-
console.log("");
|
|
755
|
-
const s2 = ui.spinner(`Cloning ${orgLogin}/${repoName}...`);
|
|
756
|
-
if (cloneWithFallback(`https://github.com/${orgLogin}/${repoName}.git`, egregoreDir)) {
|
|
757
|
-
s2.stop(`Cloned ${repoName}`);
|
|
758
|
-
} else {
|
|
759
|
-
s2.fail(`Could not clone ${orgLogin}/${repoName}`);
|
|
760
|
-
ui.error("Make sure you have git credentials set up (SSH keys or gh auth login).");
|
|
761
|
-
ui.error("Also verify the org admin has set up Egregore and invited you.");
|
|
762
|
-
process.exit(1);
|
|
763
|
-
}
|
|
764
|
-
setGitIdentity(egregoreDir, user);
|
|
765
|
-
ensureDevelop(egregoreDir);
|
|
766
|
-
|
|
767
|
-
// 6. Read egregore.json from local clone
|
|
768
|
-
const configPath = path.join(egregoreDir, "egregore.json");
|
|
769
|
-
if (!fs.existsSync(configPath)) {
|
|
770
|
-
ui.error("The repo has no egregore.json. Ask the admin to set up first.");
|
|
771
|
-
process.exit(1);
|
|
772
|
-
}
|
|
1230
|
+
const s2 = ui.spinner(`Reading ${orgLogin}/${repoName}/egregore.json...`);
|
|
773
1231
|
let config;
|
|
774
1232
|
try {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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)}`);
|
|
778
1241
|
process.exit(1);
|
|
779
1242
|
}
|
|
780
1243
|
ui.success(`Found ${ui.bold(config.org_name || orgLogin)}`);
|
|
781
1244
|
|
|
782
1245
|
const orgName = config.org_name || orgLogin;
|
|
783
|
-
const memoryRepoName = config.memory_repo
|
|
784
|
-
? config.memory_repo.split("/").pop().replace(/\.git$/, "")
|
|
785
|
-
: `${orgLogin}-memory`;
|
|
786
1246
|
const managedRepos = config.repos || [];
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
// Store the invite token for later use (invitation acceptance, not cloning)
|
|
794
|
-
const inviteToken = githubToken;
|
|
795
|
-
|
|
796
|
-
// Clone memory using SSH-first fallback (same as main repo)
|
|
797
|
-
ui.step(1, totalSteps, "Cloning shared memory...");
|
|
798
|
-
if (cloneWithFallback(memoryUrl, memoryDir)) {
|
|
799
|
-
ui.success("Cloned memory");
|
|
800
|
-
} else {
|
|
801
|
-
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
|
+
}
|
|
802
1253
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
ui.step(3, totalSteps, "Writing credentials...");
|
|
811
|
-
fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${inviteToken}\n`, { mode: 0o600 });
|
|
812
|
-
ui.success("Credentials saved");
|
|
813
|
-
|
|
814
|
-
// "What should we call you?" + write state
|
|
815
|
-
console.log("");
|
|
816
|
-
const displayName = await ui.prompt(`What should we call you? [${user.login}]:`);
|
|
817
|
-
|
|
818
|
-
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,
|
|
819
1261
|
github_username: user.login,
|
|
820
1262
|
github_name: user.name,
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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,
|
|
824
1268
|
usage_type: "joiner_group",
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
workspace_ready: true,
|
|
1269
|
+
onboarding_role: "joiner",
|
|
1270
|
+
invite_handled_by: "installer",
|
|
828
1271
|
};
|
|
829
|
-
fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
|
|
830
|
-
|
|
831
|
-
// Register + alias
|
|
832
|
-
ui.step(4, totalSteps, "Registering instance...");
|
|
833
|
-
registerInstance(repoName, orgName, egregoreDir);
|
|
834
|
-
const alias = await installShellAlias(egregoreDir, ui);
|
|
835
1272
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
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);
|
|
848
1284
|
}
|
|
849
1285
|
|
|
850
|
-
|
|
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.
|
|
851
1291
|
const welcomeFile = path.join(memoryDir, "people", `${user.login}.md`);
|
|
852
1292
|
if (fs.existsSync(welcomeFile)) {
|
|
853
1293
|
try {
|
|
@@ -863,27 +1303,12 @@ async function localJoinFlow(orgArg, ui, ctx = {}) {
|
|
|
863
1303
|
} catch {}
|
|
864
1304
|
}
|
|
865
1305
|
|
|
866
|
-
//
|
|
1306
|
+
// 6. Show Telegram group link if configured
|
|
867
1307
|
if (config.telegram_group_link) {
|
|
868
1308
|
console.log("");
|
|
869
1309
|
ui.info("Join the team's Telegram group:");
|
|
870
1310
|
ui.info(` ${ui.cyan(config.telegram_group_link)}`);
|
|
871
1311
|
}
|
|
872
|
-
|
|
873
|
-
// 14. Done
|
|
874
|
-
console.log("");
|
|
875
|
-
ui.success(`Joined ${ui.bold(orgName)}`);
|
|
876
|
-
console.log("");
|
|
877
|
-
ui.info("Your workspace:");
|
|
878
|
-
ui.info(` ${ui.cyan(`./${repoName}/`)} — Egregore instance`);
|
|
879
|
-
ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
|
|
880
|
-
for (const entry of managedRepos) {
|
|
881
|
-
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
882
|
-
ui.info(` ${ui.cyan(`./${mrName}/`)} — Managed repo`);
|
|
883
|
-
}
|
|
884
|
-
console.log("");
|
|
885
|
-
ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
|
|
886
|
-
console.log("");
|
|
887
1312
|
}
|
|
888
1313
|
|
|
889
|
-
module.exports = { localFounderFlow, localJoinFlow, parseJoinTarget, addCollaborator, getUser, repoExists, createRepo, waitForRepo };
|
|
1314
|
+
module.exports = { localFounderFlow, localJoinFlow, parseJoinTarget, addCollaborator, getUser, repoExists, createRepo, waitForRepo, retryAsync, ghFallbackNote };
|