create-egregore 0.3.15 → 0.5.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/bin/cli.js CHANGED
@@ -3,9 +3,11 @@
3
3
  /**
4
4
  * create-egregore — Set up Egregore in one command.
5
5
  *
6
- * Two modes:
7
- * npx create-egregore --token st_xxxx (from website — primary path)
8
- * npx create-egregore (terminal-only fallback)
6
+ * Four modes:
7
+ * npx create-egregore --token st_xxxx (from website — connected mode)
8
+ * npx create-egregore (interactive — API-based, existing)
9
+ * npx create-egregore --local (local mode — founder, no API)
10
+ * npx create-egregore join <org> (local mode — join existing)
9
11
  */
10
12
 
11
13
  const ui = require("../lib/ui");
@@ -17,6 +19,7 @@ const API_URL = process.env.EGREGORE_API_URL || "https://egregore-production-55f
17
19
 
18
20
  function parseArgs(argv) {
19
21
  const args = {};
22
+ const positional = [];
20
23
  for (let i = 2; i < argv.length; i++) {
21
24
  if (argv[i] === "--token" && argv[i + 1]) {
22
25
  args.token = argv[++i];
@@ -24,20 +27,31 @@ function parseArgs(argv) {
24
27
  args.token = argv[i].split("=")[1];
25
28
  } else if (argv[i] === "--api" && argv[i + 1]) {
26
29
  args.api = argv[++i];
30
+ } else if (argv[i] === "--local") {
31
+ args.local = true;
27
32
  } else if (argv[i] === "--help" || argv[i] === "-h") {
28
33
  args.help = true;
34
+ } else if (!argv[i].startsWith("-")) {
35
+ positional.push(argv[i]);
29
36
  }
30
37
  }
38
+ if (positional[0] === "join") {
39
+ args.subcommand = "join";
40
+ args.joinOrg = positional[1];
41
+ }
31
42
  return args;
32
43
  }
33
44
 
34
45
  function showHelp() {
35
46
  ui.banner();
36
47
  ui.info("Usage:");
37
- ui.info(" npx create-egregore --token <setup-token> Install from website");
38
- ui.info(" npx create-egregore Interactive setup");
48
+ ui.info(" npx create-egregore Interactive setup (API)");
49
+ ui.info(" npx create-egregore --local New project (local mode)");
50
+ ui.info(" npx create-egregore join <org>[/<repo>] Join existing project");
51
+ ui.info(" npx create-egregore --token <setup-token> Install from website");
39
52
  ui.info("");
40
53
  ui.info("Options:");
54
+ ui.info(" --local Local mode — no API server needed");
41
55
  ui.info(" --token <token> Setup token from egregore.xyz");
42
56
  ui.info(" --api <url> API URL override");
43
57
  ui.info(" -h, --help Show this help");
@@ -52,15 +66,27 @@ async function main() {
52
66
  process.exit(0);
53
67
  }
54
68
 
55
- const api = new EgregoreAPI(args.api || API_URL);
56
-
57
69
  ui.banner();
58
70
 
59
71
  if (args.token) {
60
- // ===== Website flow: token provided =====
72
+ // ===== Connected mode: token provided (unchanged) =====
73
+ const api = new EgregoreAPI(args.api || API_URL);
61
74
  await tokenFlow(api, args.token);
75
+ } else if (args.subcommand === "join") {
76
+ // ===== Local mode: join existing =====
77
+ if (!args.joinOrg) {
78
+ ui.error("Usage: npx create-egregore join <github-org>");
79
+ process.exit(1);
80
+ }
81
+ const { localJoinFlow } = require("../lib/local");
82
+ await localJoinFlow(args.joinOrg, ui);
83
+ } else if (args.local) {
84
+ // ===== Local mode: founder (explicit opt-in) =====
85
+ const { localFounderFlow } = require("../lib/local");
86
+ await localFounderFlow(ui);
62
87
  } else {
63
- // ===== Terminal fallback: interactive =====
88
+ // ===== Interactive API flow (default — existing behavior) =====
89
+ const api = new EgregoreAPI(args.api || API_URL);
64
90
  await interactiveFlow(api);
65
91
  }
66
92
  }
package/lib/local.js ADDED
@@ -0,0 +1,683 @@
1
+ /**
2
+ * Local mode — setup Egregore using only GitHub (no Egregore API).
3
+ * Zero dependencies — uses helpers from setup.js + node built-ins.
4
+ */
5
+
6
+ const fs = require("node:fs");
7
+ const path = require("node:path");
8
+ const { execFileSync } = require("node:child_process");
9
+ const { deviceFlow } = require("./auth");
10
+ const {
11
+ ghApi,
12
+ acceptPendingInvitations,
13
+ registerInstance,
14
+ installShellAlias,
15
+ embedToken,
16
+ configureGitCredentials,
17
+ initMemoryDirs,
18
+ run,
19
+ } = require("./setup");
20
+
21
+ // Template repo for new egregore instances
22
+ // TODO: Change to egregore-ai/egregore when OSS repo exists
23
+ const TEMPLATE_OWNER = "Curve-Labs";
24
+ const TEMPLATE_REPO = "egregore-core";
25
+
26
+ // ── GitHub API helpers ──────────────────────────────────────────────
27
+
28
+ async function getUser(token) {
29
+ const { status, data } = await ghApi("GET", "/user", token);
30
+ if (status !== 200) throw new Error("GitHub auth failed — could not fetch user");
31
+ return { login: data.login, name: data.name || data.login, email: data.email || null };
32
+ }
33
+
34
+ async function listOrgs(token) {
35
+ const { status, data } = await ghApi("GET", "/user/orgs?per_page=100", token);
36
+ if (status !== 200 || !Array.isArray(data)) return [];
37
+ return data.map((o) => ({ login: o.login }));
38
+ }
39
+
40
+ async function repoExists(token, owner, repo) {
41
+ const { status } = await ghApi("GET", `/repos/${owner}/${repo}`, token);
42
+ return status === 200;
43
+ }
44
+
45
+ async function createFromTemplate(token, owner, name, description) {
46
+ const { status, data } = await ghApi(
47
+ "POST",
48
+ `/repos/${TEMPLATE_OWNER}/${TEMPLATE_REPO}/generate`,
49
+ token,
50
+ { owner, name, private: true, description: description || "Egregore instance" },
51
+ );
52
+ if (status !== 201 && status !== 200) {
53
+ const msg = typeof data === "object" ? data.message || JSON.stringify(data) : data;
54
+ throw new Error(`Template generation failed (HTTP ${status}): ${msg}`);
55
+ }
56
+ return data;
57
+ }
58
+
59
+ async function createRepo(token, owner, name, isOrg, description) {
60
+ const apiPath = isOrg ? `/orgs/${owner}/repos` : "/user/repos";
61
+ const { status, data } = await ghApi("POST", apiPath, token, {
62
+ name,
63
+ private: true,
64
+ auto_init: true,
65
+ description: description || "Egregore shared memory",
66
+ });
67
+ if (status === 422) return { already_exists: true };
68
+ if (status !== 201 && status !== 200) {
69
+ const msg = typeof data === "object" ? data.message || JSON.stringify(data) : data;
70
+ throw new Error(`Create repo failed (HTTP ${status}): ${msg}`);
71
+ }
72
+ return data;
73
+ }
74
+
75
+ async function waitForRepo(token, owner, repo, timeoutMs = 30000) {
76
+ const start = Date.now();
77
+ while (Date.now() - start < timeoutMs) {
78
+ if (await repoExists(token, owner, repo)) return true;
79
+ await new Promise((r) => setTimeout(r, 2000));
80
+ }
81
+ return false;
82
+ }
83
+
84
+ async function getFileContent(token, owner, repo, filePath) {
85
+ const { status, data } = await ghApi("GET", `/repos/${owner}/${repo}/contents/${filePath}`, token);
86
+ if (status !== 200 || !data.content) return null;
87
+ return Buffer.from(data.content, "base64").toString("utf-8");
88
+ }
89
+
90
+ async function putFileContent(token, owner, repo, filePath, content, message) {
91
+ // Get SHA if file already exists
92
+ const { status: existStatus, data: existData } = await ghApi(
93
+ "GET",
94
+ `/repos/${owner}/${repo}/contents/${filePath}`,
95
+ token,
96
+ );
97
+ const body = {
98
+ message,
99
+ content: Buffer.from(content).toString("base64"),
100
+ };
101
+ if (existStatus === 200 && existData.sha) {
102
+ body.sha = existData.sha;
103
+ }
104
+ return ghApi("PUT", `/repos/${owner}/${repo}/contents/${filePath}`, token, body);
105
+ }
106
+
107
+ async function listOrgRepos(token, org) {
108
+ const { status, data } = await ghApi("GET", `/orgs/${org}/repos?per_page=100&sort=updated`, token);
109
+ if (status !== 200 || !Array.isArray(data)) return [];
110
+ return data
111
+ .filter((r) => !r.archived && r.name !== "egregore" && r.name !== "egregore-core" && !r.name.endsWith("-memory"))
112
+ .map((r) => ({ name: r.name, language: r.language || "", description: r.description || "" }));
113
+ }
114
+
115
+ async function addCollaborator(token, owner, repo, username) {
116
+ const { status } = await ghApi(
117
+ "PUT",
118
+ `/repos/${owner}/${repo}/collaborators/${username}`,
119
+ token,
120
+ { permission: "push" },
121
+ );
122
+ return status === 201 || status === 204;
123
+ }
124
+
125
+ // ── Clone + local file helpers ──────────────────────────────────────
126
+
127
+ function cloneRepo(url, dir, token, ui, label) {
128
+ if (fs.existsSync(dir)) {
129
+ ui.warn(`${path.basename(dir)}/ already exists — pulling latest`);
130
+ run("git pull", { cwd: dir });
131
+ } else {
132
+ execFileSync("git", ["clone", embedToken(url, token), dir], {
133
+ stdio: "pipe",
134
+ encoding: "utf-8",
135
+ timeout: 60000,
136
+ });
137
+ try {
138
+ run(`git remote set-url origin ${url}`, { cwd: dir });
139
+ } catch {}
140
+ }
141
+ try {
142
+ run("git config credential.helper store", { cwd: dir });
143
+ } catch {}
144
+ ui.success(`Cloned ${label}`);
145
+ }
146
+
147
+ function setGitIdentity(dir, user) {
148
+ try {
149
+ run(`git config user.name "${user.name}"`, { cwd: dir });
150
+ run(`git config user.email "${user.login}@users.noreply.github.com"`, { cwd: dir });
151
+ } catch {}
152
+ }
153
+
154
+ function createSymlink(egregoreDir, memoryDir, ui) {
155
+ const target = path.join(egregoreDir, "memory");
156
+ if (fs.existsSync(target)) {
157
+ ui.warn("memory/ symlink already exists");
158
+ } else if (process.platform === "win32") {
159
+ fs.symlinkSync(path.resolve(memoryDir), target, "junction");
160
+ } else {
161
+ fs.symlinkSync(path.relative(egregoreDir, memoryDir), target);
162
+ }
163
+ ui.success("Linked");
164
+ }
165
+
166
+ // ── Flow 1: Founder (local mode) ───────────────────────────────────
167
+
168
+ async function localFounderFlow(ui) {
169
+ // 1. GitHub auth
170
+ ui.info("Let's set up Egregore. First, sign in with GitHub.\n");
171
+ let githubToken;
172
+ try {
173
+ githubToken = await deviceFlow(ui);
174
+ ui.success("Authenticated with GitHub");
175
+ } catch (err) {
176
+ ui.error(`GitHub auth failed: ${err.message}`);
177
+ process.exit(1);
178
+ }
179
+
180
+ // 2. Get user info
181
+ const user = await getUser(githubToken);
182
+
183
+ // 3. "What do you want to do?"
184
+ const action = await ui.choose("What do you want to do?", [
185
+ { label: "Start a new project", description: "Create repos and shared memory" },
186
+ { label: "Connect existing repos", description: "Set up Egregore for an org you manage" },
187
+ ]);
188
+ const isNewProject = action.label.startsWith("Start");
189
+
190
+ // 4. Detect GitHub org
191
+ console.log("");
192
+ const s = ui.spinner("Checking your GitHub accounts...");
193
+ const orgs = await listOrgs(githubToken);
194
+ s.stop("");
195
+
196
+ let githubOrg, isOrg;
197
+ if (orgs.length === 0) {
198
+ // No orgs — use personal account
199
+ githubOrg = user.login;
200
+ isOrg = false;
201
+ ui.info(`Using personal account: ${ui.bold(githubOrg)}`);
202
+ } else if (orgs.length === 1) {
203
+ // One org — use it automatically
204
+ githubOrg = orgs[0].login;
205
+ isOrg = true;
206
+ ui.info(`Using organization: ${ui.bold(githubOrg)}`);
207
+ } else {
208
+ // Multiple orgs — picker
209
+ const orgChoices = orgs.map((o) => ({ label: o.login, description: "Organization" }));
210
+ orgChoices.push({ label: user.login, description: "Personal account" });
211
+ const orgChoice = await ui.choose("Which GitHub account?", orgChoices);
212
+ githubOrg = orgChoice.label;
213
+ isOrg = githubOrg !== user.login;
214
+ }
215
+
216
+ // 5. Collect project info
217
+ let orgName, description, selectedRepos = [];
218
+
219
+ if (isNewProject) {
220
+ const nameInput = await ui.prompt("What's your team called?");
221
+ orgName = nameInput || githubOrg;
222
+
223
+ const descInput = await ui.prompt("What are you building? (one line)");
224
+ description = descInput || "";
225
+ } else {
226
+ // "Connect existing repos"
227
+ const nameInput = await ui.prompt(`Display name [${githubOrg}]:`);
228
+ orgName = nameInput || githubOrg;
229
+ description = "";
230
+
231
+ // Multi-select repos to manage
232
+ try {
233
+ const repos = await listOrgRepos(githubToken, githubOrg);
234
+ if (repos.length > 0) {
235
+ selectedRepos = await ui.multiSelect("Which repos should Egregore manage?", repos);
236
+ }
237
+ } catch {}
238
+ }
239
+
240
+ // 6. Derive repo names from team name
241
+ const slug = orgName
242
+ .toLowerCase()
243
+ .replace(/[^a-z0-9-]/g, "-")
244
+ .replace(/-+/g, "-")
245
+ .replace(/^-|-$/g, "") || "egregore";
246
+ const repoName = slug;
247
+ const memoryRepoName = `${slug}-memory`;
248
+
249
+ // Guard: if egregore repo already exists with a config, don't overwrite
250
+ console.log("");
251
+ const s1 = ui.spinner("Checking for existing Egregore...");
252
+ const egreExists = await repoExists(githubToken, githubOrg, repoName);
253
+ if (egreExists) {
254
+ const existingConfig = await getFileContent(githubToken, githubOrg, repoName, "egregore.json");
255
+ if (existingConfig) {
256
+ s1.stop(`${githubOrg}/${repoName} already has Egregore set up`);
257
+ console.log("");
258
+ ui.info("To join it:");
259
+ ui.info(` ${ui.bold(`npx create-egregore join ${githubOrg}/${repoName}`)}`);
260
+ console.log("");
261
+ ui.info("To set up a different one, choose a different team name.");
262
+ process.exit(0);
263
+ }
264
+ s1.stop("Egregore repo exists but has no config — using it");
265
+ } else {
266
+ try {
267
+ await createFromTemplate(githubToken, githubOrg, repoName, description);
268
+ if (!(await waitForRepo(githubToken, githubOrg, repoName))) {
269
+ s1.fail("Repo creation timed out");
270
+ process.exit(1);
271
+ }
272
+ s1.stop("Created egregore repo from template");
273
+ } catch (err) {
274
+ s1.fail("Template not available — creating empty repo");
275
+ try {
276
+ await createRepo(githubToken, githubOrg, repoName, isOrg, description || "Egregore instance");
277
+ if (!(await waitForRepo(githubToken, githubOrg, repoName))) {
278
+ ui.error("Repo creation failed");
279
+ process.exit(1);
280
+ }
281
+ ui.success("Created egregore repo");
282
+ } catch (err2) {
283
+ ui.error(`Could not create repo: ${err2.message}`);
284
+ process.exit(1);
285
+ }
286
+ }
287
+ }
288
+
289
+ // 7. Create memory repo
290
+ const s2 = ui.spinner("Creating memory repo...");
291
+ const memExists = await repoExists(githubToken, githubOrg, memoryRepoName);
292
+ if (memExists) {
293
+ s2.stop("Memory repo already exists — using it");
294
+ } else {
295
+ try {
296
+ await createRepo(githubToken, githubOrg, memoryRepoName, isOrg, `${orgName} shared memory`);
297
+ s2.stop("Created memory repo");
298
+ } catch (err) {
299
+ s2.fail(`Memory repo creation failed: ${err.message}`);
300
+ process.exit(1);
301
+ }
302
+ }
303
+
304
+ // 8. Write egregore.json to remote repo (so joiners can read it)
305
+ const egreConfig = {
306
+ org_name: orgName,
307
+ github_org: githubOrg,
308
+ memory_repo: `https://github.com/${githubOrg}/${memoryRepoName}.git`,
309
+ repos: selectedRepos,
310
+ };
311
+ try {
312
+ await putFileContent(
313
+ githubToken,
314
+ githubOrg,
315
+ repoName,
316
+ "egregore.json",
317
+ JSON.stringify(egreConfig, null, 2) + "\n",
318
+ `Configure egregore for ${orgName}`,
319
+ );
320
+ } catch {}
321
+
322
+ // 9. Init memory dirs on remote (so joiners get the structure)
323
+ const memDirs = ["people", "handoffs", "knowledge/decisions", "knowledge/patterns", "knowledge/findings", "quests", "wraps"];
324
+ for (const d of memDirs) {
325
+ try {
326
+ await putFileContent(githubToken, githubOrg, memoryRepoName, `${d}/.gitkeep`, "", `Initialize ${d}`);
327
+ } catch {}
328
+ }
329
+
330
+ // 10. Clone locally
331
+ const base = process.cwd();
332
+ const egregoreDir = path.join(base, repoName);
333
+ const memoryDir = path.join(base, memoryRepoName);
334
+ const forkUrl = `https://github.com/${githubOrg}/${repoName}.git`;
335
+ const memoryUrl = `https://github.com/${githubOrg}/${memoryRepoName}.git`;
336
+
337
+ const totalSteps = 6 + selectedRepos.length;
338
+ configureGitCredentials(githubToken);
339
+
340
+ ui.step(1, totalSteps, "Cloning egregore...");
341
+ cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
342
+ setGitIdentity(egregoreDir, user);
343
+
344
+ ui.step(2, totalSteps, "Cloning shared memory...");
345
+ cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
346
+ setGitIdentity(memoryDir, user);
347
+
348
+ // Ensure memory dirs exist locally (fallback if remote init failed)
349
+ initMemoryDirs(memoryDir);
350
+
351
+ // 11. Symlink
352
+ ui.step(3, totalSteps, "Linking memory...");
353
+ createSymlink(egregoreDir, memoryDir, ui);
354
+
355
+ // 12. Write .env (GITHUB_TOKEN only — no API key in local mode)
356
+ ui.step(4, totalSteps, "Writing credentials...");
357
+ fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${githubToken}\n`, { mode: 0o600 });
358
+ ui.success("Credentials saved");
359
+
360
+ // 13. Write egregore.json locally
361
+ fs.writeFileSync(path.join(egregoreDir, "egregore.json"), JSON.stringify(egreConfig, null, 2) + "\n");
362
+
363
+ // 14. "What should we call you?" + write state
364
+ console.log("");
365
+ const displayName = await ui.prompt(`What should we call you? [${user.login}]:`);
366
+
367
+ const state = {
368
+ github_username: user.login,
369
+ github_name: user.name,
370
+ display_name: displayName || user.login,
371
+ email: user.email,
372
+ onboarding_complete: false,
373
+ usage_type: "founder_group",
374
+ org_setup: true,
375
+ github_configured: true,
376
+ workspace_ready: true,
377
+ };
378
+ fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
379
+
380
+ // 15. Create founder's person file in memory
381
+ const today = new Date().toISOString().split("T")[0];
382
+ const personContent = [
383
+ "---",
384
+ `name: ${displayName || user.login}`,
385
+ `github: ${user.login}`,
386
+ `role: founder`,
387
+ `joined: ${today}`,
388
+ "---",
389
+ "",
390
+ ].join("\n");
391
+ const personFile = path.join(memoryDir, "people", `${user.login}.md`);
392
+ if (!fs.existsSync(personFile)) {
393
+ fs.writeFileSync(personFile, personContent);
394
+ try {
395
+ run("git add -A", { cwd: memoryDir });
396
+ run(`git commit -m "Add founder ${user.login}"`, { cwd: memoryDir });
397
+ run("git push", { cwd: memoryDir });
398
+ } catch {}
399
+ }
400
+
401
+ // 16. Clone managed repos
402
+ for (let i = 0; i < selectedRepos.length; i++) {
403
+ const mr = selectedRepos[i];
404
+ ui.step(5 + i, totalSteps, `Cloning ${mr}...`);
405
+ const repoDir = path.join(base, mr);
406
+ try {
407
+ const repoUrl = `https://github.com/${githubOrg}/${mr}.git`;
408
+ cloneRepo(repoUrl, repoDir, githubToken, ui, mr);
409
+ setGitIdentity(repoDir, user);
410
+ } catch {
411
+ ui.warn(`Could not clone ${mr} — you may not have access.`);
412
+ }
413
+ }
414
+
415
+ // 17. Register instance + shell alias
416
+ ui.step(5 + selectedRepos.length, totalSteps, "Registering instance...");
417
+ registerInstance(repoName, orgName, egregoreDir);
418
+ const alias = await installShellAlias(egregoreDir, ui);
419
+
420
+ // 18. Telegram group setup (optional)
421
+ console.log("");
422
+ ui.info("Egregore can send notifications to a Telegram group.");
423
+ ui.info("Create a group, add @egregore_bot, and paste the invite link.");
424
+ console.log("");
425
+ const telegramLink = await ui.prompt("Telegram group invite link (Enter to skip):");
426
+ if (telegramLink) {
427
+ egreConfig.telegram_group_link = telegramLink;
428
+ // Update local egregore.json
429
+ fs.writeFileSync(path.join(egregoreDir, "egregore.json"), JSON.stringify(egreConfig, null, 2) + "\n");
430
+ // Push to remote so joiners see it
431
+ try {
432
+ await putFileContent(
433
+ githubToken,
434
+ githubOrg,
435
+ repoName,
436
+ "egregore.json",
437
+ JSON.stringify(egreConfig, null, 2) + "\n",
438
+ "Add Telegram group link",
439
+ );
440
+ } catch {}
441
+ ui.success("Telegram group link saved");
442
+ }
443
+
444
+ // 19. Optional invite
445
+ console.log("");
446
+ const inviteAnswer = await ui.prompt("Know someone who'd find this useful? [y/N]:");
447
+ if (inviteAnswer && inviteAnswer.toLowerCase() === "y") {
448
+ const invUsername = await ui.prompt("GitHub username:");
449
+ if (invUsername) {
450
+ const invSpin = ui.spinner(`Inviting ${invUsername}...`);
451
+ try {
452
+ // Add as collaborator to all repos
453
+ await addCollaborator(githubToken, githubOrg, repoName, invUsername);
454
+ await addCollaborator(githubToken, githubOrg, memoryRepoName, invUsername);
455
+ for (const mr of selectedRepos) {
456
+ await addCollaborator(githubToken, githubOrg, mr, invUsername).catch(() => {});
457
+ }
458
+
459
+ // Create person file
460
+ const invPersonContent = [
461
+ "---",
462
+ `name: ${invUsername}`,
463
+ `github: ${invUsername}`,
464
+ `invited_by: ${user.login}`,
465
+ `joined: ${today}`,
466
+ "---",
467
+ "",
468
+ ].join("\n");
469
+ const invPersonFile = path.join(memoryDir, "people", `${invUsername}.md`);
470
+ if (!fs.existsSync(invPersonFile)) {
471
+ fs.writeFileSync(invPersonFile, invPersonContent);
472
+ try {
473
+ run("git add -A", { cwd: memoryDir });
474
+ run(`git commit -m "Invite ${invUsername}"`, { cwd: memoryDir });
475
+ run("git push", { cwd: memoryDir });
476
+ } catch {}
477
+ }
478
+
479
+ invSpin.stop(`Invited ${invUsername}`);
480
+ console.log("");
481
+ ui.info(`Tell them to run:`);
482
+ ui.info(` ${ui.bold(`npx create-egregore join ${githubOrg}/${repoName}`)}`);
483
+ } catch (err) {
484
+ invSpin.fail(`Could not invite ${invUsername}: ${err.message}`);
485
+ }
486
+ }
487
+ }
488
+
489
+ // 19. Done
490
+ console.log("");
491
+ ui.success(`Egregore is ready for ${ui.bold(orgName)}`);
492
+ console.log("");
493
+ ui.info("Your workspace:");
494
+ ui.info(` ${ui.cyan(`./${repoName}/`)} — Your Egregore instance`);
495
+ ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
496
+ for (const mr of selectedRepos) {
497
+ ui.info(` ${ui.cyan(`./${mr}/`)} — Managed repo`);
498
+ }
499
+ console.log("");
500
+ ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
501
+ console.log("");
502
+ }
503
+
504
+ // ── Flow 2: Join (local mode) ──────────────────────────────────────
505
+
506
+ async function localJoinFlow(orgArg, ui) {
507
+ // Parse org/repo if provided (e.g., "acme-org/ops-team")
508
+ let orgLogin, explicitRepo;
509
+ if (orgArg.includes("/")) {
510
+ [orgLogin, explicitRepo] = orgArg.split("/", 2);
511
+ } else {
512
+ orgLogin = orgArg;
513
+ explicitRepo = null;
514
+ }
515
+
516
+ // 1. GitHub auth
517
+ ui.info(`Joining ${ui.bold(orgLogin)}. First, sign in with GitHub.\n`);
518
+ let githubToken;
519
+ try {
520
+ githubToken = await deviceFlow(ui);
521
+ ui.success("Authenticated with GitHub");
522
+ } catch (err) {
523
+ ui.error(`GitHub auth failed: ${err.message}`);
524
+ process.exit(1);
525
+ }
526
+
527
+ // 2. Get user info
528
+ const user = await getUser(githubToken);
529
+
530
+ // 3. Accept pending invitations
531
+ console.log("");
532
+ const s1 = ui.spinner("Checking for invitations...");
533
+ await acceptPendingInvitations(githubToken, orgLogin, ui);
534
+ s1.stop("Checked invitations");
535
+
536
+ // 4. Find egregore repo
537
+ const s2 = ui.spinner(`Looking for egregore in ${orgLogin}...`);
538
+ let repoName = null;
539
+ if (explicitRepo) {
540
+ // User specified org/repo — use it directly
541
+ if (await repoExists(githubToken, orgLogin, explicitRepo)) {
542
+ repoName = explicitRepo;
543
+ }
544
+ } else {
545
+ // Search for known repo names
546
+ for (const candidate of ["egregore", "egregore-core"]) {
547
+ if (await repoExists(githubToken, orgLogin, candidate)) {
548
+ repoName = candidate;
549
+ break;
550
+ }
551
+ }
552
+ }
553
+ if (!repoName) {
554
+ s2.fail(`No egregore repo found in ${orgLogin}${explicitRepo ? `/${explicitRepo}` : ""}`);
555
+ ui.error("Make sure the org admin has set up Egregore and invited you.");
556
+ ui.info("If the repo has a custom name, use: npx create-egregore join <org>/<repo>");
557
+ process.exit(1);
558
+ }
559
+
560
+ // 5. Read egregore.json from remote
561
+ const configRaw = await getFileContent(githubToken, orgLogin, repoName, "egregore.json");
562
+ if (!configRaw) {
563
+ s2.fail("Could not read egregore.json");
564
+ ui.error("The egregore repo exists but has no egregore.json. Ask the admin to set up first.");
565
+ process.exit(1);
566
+ }
567
+ let config;
568
+ try {
569
+ config = JSON.parse(configRaw);
570
+ } catch {
571
+ s2.fail("Invalid egregore.json");
572
+ process.exit(1);
573
+ }
574
+ s2.stop(`Found ${ui.bold(config.org_name || orgLogin)}`);
575
+
576
+ const orgName = config.org_name || orgLogin;
577
+ const memoryRepoName = config.memory_repo
578
+ ? config.memory_repo.split("/").pop().replace(/\.git$/, "")
579
+ : `${orgLogin}-memory`;
580
+ const managedRepos = config.repos || [];
581
+
582
+ // 6. Clone locally
583
+ const base = process.cwd();
584
+ const forkUrl = `https://github.com/${orgLogin}/${repoName}.git`;
585
+ const memoryUrl = config.memory_repo || `https://github.com/${orgLogin}/${memoryRepoName}.git`;
586
+ const egregoreDir = path.join(base, repoName);
587
+ const memoryDir = path.join(base, memoryRepoName);
588
+
589
+ const totalSteps = 5 + managedRepos.length;
590
+ configureGitCredentials(githubToken);
591
+
592
+ ui.step(1, totalSteps, "Cloning egregore...");
593
+ cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
594
+ setGitIdentity(egregoreDir, user);
595
+
596
+ ui.step(2, totalSteps, "Cloning shared memory...");
597
+ cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
598
+ setGitIdentity(memoryDir, user);
599
+
600
+ // 7. Symlink
601
+ ui.step(3, totalSteps, "Linking memory...");
602
+ createSymlink(egregoreDir, memoryDir, ui);
603
+
604
+ // 8. Write .env
605
+ ui.step(4, totalSteps, "Writing credentials...");
606
+ fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${githubToken}\n`, { mode: 0o600 });
607
+ ui.success("Credentials saved");
608
+
609
+ // 9. "What should we call you?" + write state
610
+ console.log("");
611
+ const displayName = await ui.prompt(`What should we call you? [${user.login}]:`);
612
+
613
+ const state = {
614
+ github_username: user.login,
615
+ github_name: user.name,
616
+ display_name: displayName || user.login,
617
+ email: user.email,
618
+ onboarding_complete: false,
619
+ usage_type: "joiner_group",
620
+ org_setup: true,
621
+ github_configured: true,
622
+ workspace_ready: true,
623
+ };
624
+ fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
625
+
626
+ // 10. Register + alias
627
+ ui.step(5, totalSteps, "Registering instance...");
628
+ registerInstance(repoName, orgName, egregoreDir);
629
+ const alias = await installShellAlias(egregoreDir, ui);
630
+
631
+ // 11. Clone managed repos
632
+ for (let i = 0; i < managedRepos.length; i++) {
633
+ const mr = managedRepos[i];
634
+ ui.step(6 + i, totalSteps, `Cloning ${mr}...`);
635
+ const repoDir = path.join(base, mr);
636
+ try {
637
+ const repoUrl = `https://github.com/${orgLogin}/${mr}.git`;
638
+ cloneRepo(repoUrl, repoDir, githubToken, ui, mr);
639
+ setGitIdentity(repoDir, user);
640
+ } catch {
641
+ ui.warn(`Could not clone ${mr} — you may not have access yet.`);
642
+ }
643
+ }
644
+
645
+ // 12. Show Telegram group link if available
646
+ if (config.telegram_group_link) {
647
+ console.log("");
648
+ ui.info("Join the team's Telegram group for notifications:");
649
+ ui.info(` ${ui.cyan(config.telegram_group_link)}`);
650
+ }
651
+
652
+ // 13. Check for welcome note
653
+ const welcomeFile = path.join(memoryDir, "people", `${user.login}.md`);
654
+ if (fs.existsSync(welcomeFile)) {
655
+ try {
656
+ const content = fs.readFileSync(welcomeFile, "utf-8");
657
+ const inviterMatch = content.match(/invited_by:\s*(\S+)/);
658
+ // Check for content after the frontmatter closing ---
659
+ const bodyMatch = content.match(/^---[\s\S]*?---\s*\n([\s\S]+)/m);
660
+ if (inviterMatch) {
661
+ console.log("");
662
+ const note = bodyMatch && bodyMatch[1].trim() ? ` — "${bodyMatch[1].trim()}"` : "";
663
+ ui.info(`${inviterMatch[1]} invited you${note}`);
664
+ }
665
+ } catch {}
666
+ }
667
+
668
+ // 14. Done
669
+ console.log("");
670
+ ui.success(`Joined ${ui.bold(orgName)}`);
671
+ console.log("");
672
+ ui.info("Your workspace:");
673
+ ui.info(` ${ui.cyan(`./${repoName}/`)} — Egregore instance`);
674
+ ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
675
+ for (const mr of managedRepos) {
676
+ ui.info(` ${ui.cyan(`./${mr}/`)} — Managed repo`);
677
+ }
678
+ console.log("");
679
+ ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
680
+ console.log("");
681
+ }
682
+
683
+ module.exports = { localFounderFlow, localJoinFlow, addCollaborator, getUser, repoExists };
package/lib/setup.js CHANGED
@@ -14,18 +14,24 @@ function run(cmd, opts = {}) {
14
14
  return execSync(cmd, { stdio: "pipe", encoding: "utf-8", timeout: 60000, ...opts }).trim();
15
15
  }
16
16
 
17
- function ghApi(method, path, token) {
17
+ function ghApi(method, apiPath, token, body) {
18
18
  return new Promise((resolve, reject) => {
19
+ const payload = body ? JSON.stringify(body) : null;
20
+ const headers = {
21
+ Authorization: `Bearer ${token}`,
22
+ Accept: "application/vnd.github+json",
23
+ "User-Agent": "create-egregore",
24
+ };
25
+ if (payload) {
26
+ headers["Content-Type"] = "application/json";
27
+ headers["Content-Length"] = Buffer.byteLength(payload);
28
+ }
19
29
  const req = https.request(
20
30
  {
21
31
  hostname: "api.github.com",
22
- path,
32
+ path: apiPath,
23
33
  method,
24
- headers: {
25
- Authorization: `Bearer ${token}`,
26
- Accept: "application/vnd.github+json",
27
- "User-Agent": "create-egregore",
28
- },
34
+ headers,
29
35
  },
30
36
  (res) => {
31
37
  let buf = "";
@@ -40,10 +46,33 @@ function ghApi(method, path, token) {
40
46
  }
41
47
  );
42
48
  req.on("error", reject);
49
+ if (payload) req.write(payload);
43
50
  req.end();
44
51
  });
45
52
  }
46
53
 
54
+ /**
55
+ * Initialize memory directory structure with .gitkeep files.
56
+ */
57
+ function initMemoryDirs(memoryDir) {
58
+ const dirs = [
59
+ "people",
60
+ "handoffs",
61
+ "knowledge/decisions",
62
+ "knowledge/patterns",
63
+ "knowledge/findings",
64
+ "quests",
65
+ "wraps",
66
+ ];
67
+ for (const d of dirs) {
68
+ const fullPath = path.join(memoryDir, d);
69
+ if (!fs.existsSync(fullPath)) {
70
+ fs.mkdirSync(fullPath, { recursive: true });
71
+ fs.writeFileSync(path.join(fullPath, ".gitkeep"), "");
72
+ }
73
+ }
74
+ }
75
+
47
76
  /**
48
77
  * Accept pending GitHub repo collaboration invitations for the given org.
49
78
  */
@@ -365,4 +394,14 @@ async function installShellAlias(egregoreDir, ui) {
365
394
  }
366
395
  }
367
396
 
368
- module.exports = { install };
397
+ module.exports = {
398
+ install,
399
+ ghApi,
400
+ acceptPendingInvitations,
401
+ registerInstance,
402
+ installShellAlias,
403
+ embedToken,
404
+ configureGitCredentials,
405
+ initMemoryDirs,
406
+ run,
407
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.3.15",
3
+ "version": "0.5.0",
4
4
  "description": "Set up Egregore for your team in one command",
5
5
  "license": "MIT",
6
6
  "bin": {