create-egregore 0.3.15 → 0.6.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,20 +3,23 @@
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");
12
14
  const { EgregoreAPI } = require("../lib/api");
13
15
  const { deviceFlow } = require("../lib/auth");
14
- const { install } = require("../lib/setup");
16
+ const { install, ghApi } = require("../lib/setup");
15
17
 
16
18
  const API_URL = process.env.EGREGORE_API_URL || "https://egregore-production-55f2.up.railway.app";
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
  }
@@ -229,6 +255,35 @@ async function setupFlow(api, githubToken, choice) {
229
255
  // Non-fatal — continue without repos
230
256
  }
231
257
 
258
+ // Offer to create a new project repo
259
+ console.log("");
260
+ const newRepoName = await ui.prompt("Create a new project repo? Enter name (or press Enter to skip):");
261
+ if (newRepoName) {
262
+ const repoSlug = newRepoName.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
263
+ if (repoSlug) {
264
+ const s2 = ui.spinner(`Creating ${repoSlug}...`);
265
+ try {
266
+ const apiPath = choice.is_personal ? "/user/repos" : `/orgs/${choice.login}/repos`;
267
+ const { status } = await ghApi("POST", apiPath, githubToken, {
268
+ name: repoSlug,
269
+ private: true,
270
+ auto_init: true,
271
+ });
272
+ if (status === 201 || status === 200) {
273
+ selectedRepos.push(repoSlug);
274
+ s2.stop(`Created ${repoSlug}`);
275
+ } else if (status === 422) {
276
+ selectedRepos.push(repoSlug);
277
+ s2.stop(`${repoSlug} already exists — added to managed repos`);
278
+ } else {
279
+ s2.fail(`Could not create ${repoSlug}`);
280
+ }
281
+ } catch (err) {
282
+ s2.fail(`Could not create ${repoSlug}: ${err.message}`);
283
+ }
284
+ }
285
+ }
286
+
232
287
  // Transcript sharing consent
233
288
  console.log("");
234
289
  console.log(" Egregore can collect session transcripts to build");
package/lib/local.js ADDED
@@ -0,0 +1,741 @@
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 = [], newProjectRepo = null;
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
+
226
+ // Ask about project repo
227
+ console.log("");
228
+ const projectAction = await ui.choose("Do you have a project repo?", [
229
+ { label: "Create a new repo", description: "Start fresh — we'll create it on GitHub" },
230
+ { label: "Connect an existing repo", description: "I already have code" },
231
+ { label: "Skip for now", description: "I'll add projects later" },
232
+ ]);
233
+
234
+ if (projectAction.label.startsWith("Create")) {
235
+ const projectName = await ui.prompt("Repo name:");
236
+ if (projectName) {
237
+ newProjectRepo = projectName.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
238
+ }
239
+ } else if (projectAction.label.startsWith("Connect")) {
240
+ const s0 = ui.spinner("Loading repos...");
241
+ try {
242
+ const repos = await listOrgRepos(githubToken, githubOrg);
243
+ s0.stop("Loaded repos");
244
+ if (repos.length > 0) {
245
+ selectedRepos = await ui.multiSelect("Which repos should Egregore manage?", repos);
246
+ } else {
247
+ ui.info("No repos found in this org yet.");
248
+ }
249
+ } catch {
250
+ s0.stop("Could not load repos");
251
+ }
252
+ }
253
+ } else {
254
+ // "Connect existing repos"
255
+ const nameInput = await ui.prompt(`Display name [${githubOrg}]:`);
256
+ orgName = nameInput || githubOrg;
257
+ description = "";
258
+
259
+ // Multi-select repos to manage
260
+ try {
261
+ const repos = await listOrgRepos(githubToken, githubOrg);
262
+ if (repos.length > 0) {
263
+ selectedRepos = await ui.multiSelect("Which repos should Egregore manage?", repos);
264
+ }
265
+ } catch {}
266
+ }
267
+
268
+ // 6. Derive repo names from team name
269
+ const slug = orgName
270
+ .toLowerCase()
271
+ .replace(/[^a-z0-9-]/g, "-")
272
+ .replace(/-+/g, "-")
273
+ .replace(/^-|-$/g, "") || "egregore";
274
+ const repoName = slug;
275
+ const memoryRepoName = `${slug}-memory`;
276
+
277
+ // Guard: if egregore repo already exists with a config, don't overwrite
278
+ console.log("");
279
+ const s1 = ui.spinner("Checking for existing Egregore...");
280
+ const egreExists = await repoExists(githubToken, githubOrg, repoName);
281
+ if (egreExists) {
282
+ const existingConfig = await getFileContent(githubToken, githubOrg, repoName, "egregore.json");
283
+ if (existingConfig) {
284
+ s1.stop(`${githubOrg}/${repoName} already has Egregore set up`);
285
+ console.log("");
286
+ ui.info("To join it:");
287
+ ui.info(` ${ui.bold(`npx create-egregore join ${githubOrg}/${repoName}`)}`);
288
+ console.log("");
289
+ ui.info("To set up a different one, choose a different team name.");
290
+ process.exit(0);
291
+ }
292
+ s1.stop("Egregore repo exists but has no config — using it");
293
+ } else {
294
+ try {
295
+ await createFromTemplate(githubToken, githubOrg, repoName, description);
296
+ if (!(await waitForRepo(githubToken, githubOrg, repoName))) {
297
+ s1.fail("Repo creation timed out");
298
+ process.exit(1);
299
+ }
300
+ s1.stop("Created egregore repo from template");
301
+ } catch (err) {
302
+ s1.fail("Template not available — creating empty repo");
303
+ try {
304
+ await createRepo(githubToken, githubOrg, repoName, isOrg, description || "Egregore instance");
305
+ if (!(await waitForRepo(githubToken, githubOrg, repoName))) {
306
+ ui.error("Repo creation failed");
307
+ process.exit(1);
308
+ }
309
+ ui.success("Created egregore repo");
310
+ } catch (err2) {
311
+ ui.error(`Could not create repo: ${err2.message}`);
312
+ process.exit(1);
313
+ }
314
+ }
315
+ }
316
+
317
+ // 7. Create memory repo
318
+ const s2 = ui.spinner("Creating memory repo...");
319
+ const memExists = await repoExists(githubToken, githubOrg, memoryRepoName);
320
+ if (memExists) {
321
+ s2.stop("Memory repo already exists — using it");
322
+ } else {
323
+ try {
324
+ await createRepo(githubToken, githubOrg, memoryRepoName, isOrg, `${orgName} shared memory`);
325
+ s2.stop("Created memory repo");
326
+ } catch (err) {
327
+ s2.fail(`Memory repo creation failed: ${err.message}`);
328
+ process.exit(1);
329
+ }
330
+ }
331
+
332
+ // 7b. Create project repo (if requested)
333
+ if (newProjectRepo) {
334
+ if (newProjectRepo === repoName || newProjectRepo === memoryRepoName) {
335
+ ui.warn(`"${newProjectRepo}" conflicts with Egregore repo names — skipping`);
336
+ newProjectRepo = null;
337
+ } else {
338
+ const s3 = ui.spinner(`Creating ${newProjectRepo}...`);
339
+ const projExists = await repoExists(githubToken, githubOrg, newProjectRepo);
340
+ if (projExists) {
341
+ selectedRepos.push(newProjectRepo);
342
+ s3.stop(`${newProjectRepo} already exists — added to managed repos`);
343
+ } else {
344
+ try {
345
+ await createRepo(githubToken, githubOrg, newProjectRepo, isOrg, description || "");
346
+ if (await waitForRepo(githubToken, githubOrg, newProjectRepo)) {
347
+ selectedRepos.push(newProjectRepo);
348
+ s3.stop(`Created ${newProjectRepo}`);
349
+ } else {
350
+ s3.fail(`${newProjectRepo} creation timed out`);
351
+ }
352
+ } catch (err) {
353
+ s3.fail(`Could not create ${newProjectRepo}: ${err.message}`);
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ // 8. Write egregore.json to remote repo (so joiners can read it)
360
+ const egreConfig = {
361
+ org_name: orgName,
362
+ github_org: githubOrg,
363
+ memory_repo: `https://github.com/${githubOrg}/${memoryRepoName}.git`,
364
+ repos: selectedRepos,
365
+ };
366
+ try {
367
+ await putFileContent(
368
+ githubToken,
369
+ githubOrg,
370
+ repoName,
371
+ "egregore.json",
372
+ JSON.stringify(egreConfig, null, 2) + "\n",
373
+ `Configure egregore for ${orgName}`,
374
+ );
375
+ } catch {}
376
+
377
+ // 9. Init memory dirs on remote (so joiners get the structure)
378
+ const memDirs = ["people", "handoffs", "knowledge/decisions", "knowledge/patterns", "knowledge/findings", "quests", "wraps"];
379
+ for (const d of memDirs) {
380
+ try {
381
+ await putFileContent(githubToken, githubOrg, memoryRepoName, `${d}/.gitkeep`, "", `Initialize ${d}`);
382
+ } catch {}
383
+ }
384
+
385
+ // 10. Clone locally
386
+ const base = process.cwd();
387
+ const egregoreDir = path.join(base, repoName);
388
+ const memoryDir = path.join(base, memoryRepoName);
389
+ const forkUrl = `https://github.com/${githubOrg}/${repoName}.git`;
390
+ const memoryUrl = `https://github.com/${githubOrg}/${memoryRepoName}.git`;
391
+
392
+ const totalSteps = 6 + selectedRepos.length;
393
+ configureGitCredentials(githubToken);
394
+
395
+ ui.step(1, totalSteps, "Cloning egregore...");
396
+ cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
397
+ setGitIdentity(egregoreDir, user);
398
+
399
+ ui.step(2, totalSteps, "Cloning shared memory...");
400
+ cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
401
+ setGitIdentity(memoryDir, user);
402
+
403
+ // Ensure memory dirs exist locally (fallback if remote init failed)
404
+ initMemoryDirs(memoryDir);
405
+
406
+ // 11. Symlink
407
+ ui.step(3, totalSteps, "Linking memory...");
408
+ createSymlink(egregoreDir, memoryDir, ui);
409
+
410
+ // 12. Write .env (GITHUB_TOKEN only — no API key in local mode)
411
+ ui.step(4, totalSteps, "Writing credentials...");
412
+ fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${githubToken}\n`, { mode: 0o600 });
413
+ ui.success("Credentials saved");
414
+
415
+ // 13. Write egregore.json locally
416
+ fs.writeFileSync(path.join(egregoreDir, "egregore.json"), JSON.stringify(egreConfig, null, 2) + "\n");
417
+
418
+ // 14. "What should we call you?" + write state
419
+ console.log("");
420
+ const displayName = await ui.prompt(`What should we call you? [${user.login}]:`);
421
+
422
+ const state = {
423
+ github_username: user.login,
424
+ github_name: user.name,
425
+ display_name: displayName || user.login,
426
+ email: user.email,
427
+ onboarding_complete: false,
428
+ usage_type: "founder_group",
429
+ org_setup: true,
430
+ github_configured: true,
431
+ workspace_ready: true,
432
+ };
433
+ fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
434
+
435
+ // 15. Create founder's person file in memory
436
+ const today = new Date().toISOString().split("T")[0];
437
+ const personContent = [
438
+ "---",
439
+ `name: ${displayName || user.login}`,
440
+ `github: ${user.login}`,
441
+ `role: founder`,
442
+ `joined: ${today}`,
443
+ "---",
444
+ "",
445
+ ].join("\n");
446
+ const personFile = path.join(memoryDir, "people", `${user.login}.md`);
447
+ if (!fs.existsSync(personFile)) {
448
+ fs.writeFileSync(personFile, personContent);
449
+ try {
450
+ run("git add -A", { cwd: memoryDir });
451
+ run(`git commit -m "Add founder ${user.login}"`, { cwd: memoryDir });
452
+ run("git push", { cwd: memoryDir });
453
+ } catch {}
454
+ }
455
+
456
+ // 16. Clone managed repos
457
+ for (let i = 0; i < selectedRepos.length; i++) {
458
+ const mr = selectedRepos[i];
459
+ ui.step(5 + i, totalSteps, `Cloning ${mr}...`);
460
+ const repoDir = path.join(base, mr);
461
+ try {
462
+ const repoUrl = `https://github.com/${githubOrg}/${mr}.git`;
463
+ cloneRepo(repoUrl, repoDir, githubToken, ui, mr);
464
+ setGitIdentity(repoDir, user);
465
+ } catch {
466
+ ui.warn(`Could not clone ${mr} — you may not have access.`);
467
+ }
468
+ }
469
+
470
+ // 17. Register instance + shell alias
471
+ ui.step(5 + selectedRepos.length, totalSteps, "Registering instance...");
472
+ registerInstance(repoName, orgName, egregoreDir);
473
+ const alias = await installShellAlias(egregoreDir, ui);
474
+
475
+ // 18. Telegram group setup (optional)
476
+ console.log("");
477
+ ui.info("Want notifications? Set up a Telegram group:");
478
+ ui.info("");
479
+ ui.info(` 1. Create a Telegram group for your team`);
480
+ ui.info(` 2. Add the bot: ${ui.cyan("https://t.me/egregore_bot")}`);
481
+ ui.info(` 3. Paste the group invite link below`);
482
+ console.log("");
483
+ const telegramLink = await ui.prompt("Group invite link (Enter to skip):");
484
+ if (telegramLink) {
485
+ egreConfig.telegram_group_link = telegramLink;
486
+ // Update local egregore.json
487
+ fs.writeFileSync(path.join(egregoreDir, "egregore.json"), JSON.stringify(egreConfig, null, 2) + "\n");
488
+ // Push to remote so joiners see it
489
+ try {
490
+ await putFileContent(
491
+ githubToken,
492
+ githubOrg,
493
+ repoName,
494
+ "egregore.json",
495
+ JSON.stringify(egreConfig, null, 2) + "\n",
496
+ "Add Telegram group link",
497
+ );
498
+ } catch {}
499
+ ui.success("Telegram group link saved");
500
+ }
501
+
502
+ // 19. Optional invite
503
+ console.log("");
504
+ const inviteAnswer = await ui.prompt("Know someone who'd find this useful? [y/N]:");
505
+ if (inviteAnswer && inviteAnswer.toLowerCase() === "y") {
506
+ const invUsername = await ui.prompt("GitHub username:");
507
+ if (invUsername) {
508
+ const invSpin = ui.spinner(`Inviting ${invUsername}...`);
509
+ try {
510
+ // Add as collaborator to all repos
511
+ await addCollaborator(githubToken, githubOrg, repoName, invUsername);
512
+ await addCollaborator(githubToken, githubOrg, memoryRepoName, invUsername);
513
+ for (const mr of selectedRepos) {
514
+ await addCollaborator(githubToken, githubOrg, mr, invUsername).catch(() => {});
515
+ }
516
+
517
+ // Create person file
518
+ const invPersonContent = [
519
+ "---",
520
+ `name: ${invUsername}`,
521
+ `github: ${invUsername}`,
522
+ `invited_by: ${user.login}`,
523
+ `joined: ${today}`,
524
+ "---",
525
+ "",
526
+ ].join("\n");
527
+ const invPersonFile = path.join(memoryDir, "people", `${invUsername}.md`);
528
+ if (!fs.existsSync(invPersonFile)) {
529
+ fs.writeFileSync(invPersonFile, invPersonContent);
530
+ try {
531
+ run("git add -A", { cwd: memoryDir });
532
+ run(`git commit -m "Invite ${invUsername}"`, { cwd: memoryDir });
533
+ run("git push", { cwd: memoryDir });
534
+ } catch {}
535
+ }
536
+
537
+ invSpin.stop(`Invited ${invUsername}`);
538
+ console.log("");
539
+ ui.info(`Tell them to run:`);
540
+ ui.info(` ${ui.bold(`npx create-egregore join ${githubOrg}/${repoName}`)}`);
541
+ } catch (err) {
542
+ invSpin.fail(`Could not invite ${invUsername}: ${err.message}`);
543
+ }
544
+ }
545
+ }
546
+
547
+ // 19. Done
548
+ console.log("");
549
+ ui.success(`Egregore is ready for ${ui.bold(orgName)}`);
550
+ console.log("");
551
+ ui.info("Your workspace:");
552
+ ui.info(` ${ui.cyan(`./${repoName}/`)} — Your Egregore instance`);
553
+ ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
554
+ for (const mr of selectedRepos) {
555
+ ui.info(` ${ui.cyan(`./${mr}/`)} — Managed repo`);
556
+ }
557
+ console.log("");
558
+ ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
559
+ console.log("");
560
+ }
561
+
562
+ // ── Flow 2: Join (local mode) ──────────────────────────────────────
563
+
564
+ async function localJoinFlow(orgArg, ui) {
565
+ // Parse org/repo if provided (e.g., "acme-org/ops-team")
566
+ let orgLogin, explicitRepo;
567
+ if (orgArg.includes("/")) {
568
+ [orgLogin, explicitRepo] = orgArg.split("/", 2);
569
+ } else {
570
+ orgLogin = orgArg;
571
+ explicitRepo = null;
572
+ }
573
+
574
+ // 1. GitHub auth
575
+ ui.info(`Joining ${ui.bold(orgLogin)}. First, sign in with GitHub.\n`);
576
+ let githubToken;
577
+ try {
578
+ githubToken = await deviceFlow(ui);
579
+ ui.success("Authenticated with GitHub");
580
+ } catch (err) {
581
+ ui.error(`GitHub auth failed: ${err.message}`);
582
+ process.exit(1);
583
+ }
584
+
585
+ // 2. Get user info
586
+ const user = await getUser(githubToken);
587
+
588
+ // 3. Accept pending invitations
589
+ console.log("");
590
+ const s1 = ui.spinner("Checking for invitations...");
591
+ await acceptPendingInvitations(githubToken, orgLogin, ui);
592
+ s1.stop("Checked invitations");
593
+
594
+ // 4. Find egregore repo
595
+ const s2 = ui.spinner(`Looking for egregore in ${orgLogin}...`);
596
+ let repoName = null;
597
+ if (explicitRepo) {
598
+ // User specified org/repo — use it directly
599
+ if (await repoExists(githubToken, orgLogin, explicitRepo)) {
600
+ repoName = explicitRepo;
601
+ }
602
+ } else {
603
+ // Search for known repo names
604
+ for (const candidate of ["egregore", "egregore-core"]) {
605
+ if (await repoExists(githubToken, orgLogin, candidate)) {
606
+ repoName = candidate;
607
+ break;
608
+ }
609
+ }
610
+ }
611
+ if (!repoName) {
612
+ s2.fail(`No egregore repo found in ${orgLogin}${explicitRepo ? `/${explicitRepo}` : ""}`);
613
+ ui.error("Make sure the org admin has set up Egregore and invited you.");
614
+ ui.info("If the repo has a custom name, use: npx create-egregore join <org>/<repo>");
615
+ process.exit(1);
616
+ }
617
+
618
+ // 5. Read egregore.json from remote
619
+ const configRaw = await getFileContent(githubToken, orgLogin, repoName, "egregore.json");
620
+ if (!configRaw) {
621
+ s2.fail("Could not read egregore.json");
622
+ ui.error("The egregore repo exists but has no egregore.json. Ask the admin to set up first.");
623
+ process.exit(1);
624
+ }
625
+ let config;
626
+ try {
627
+ config = JSON.parse(configRaw);
628
+ } catch {
629
+ s2.fail("Invalid egregore.json");
630
+ process.exit(1);
631
+ }
632
+ s2.stop(`Found ${ui.bold(config.org_name || orgLogin)}`);
633
+
634
+ const orgName = config.org_name || orgLogin;
635
+ const memoryRepoName = config.memory_repo
636
+ ? config.memory_repo.split("/").pop().replace(/\.git$/, "")
637
+ : `${orgLogin}-memory`;
638
+ const managedRepos = config.repos || [];
639
+
640
+ // 6. Clone locally
641
+ const base = process.cwd();
642
+ const forkUrl = `https://github.com/${orgLogin}/${repoName}.git`;
643
+ const memoryUrl = config.memory_repo || `https://github.com/${orgLogin}/${memoryRepoName}.git`;
644
+ const egregoreDir = path.join(base, repoName);
645
+ const memoryDir = path.join(base, memoryRepoName);
646
+
647
+ const totalSteps = 5 + managedRepos.length;
648
+ configureGitCredentials(githubToken);
649
+
650
+ ui.step(1, totalSteps, "Cloning egregore...");
651
+ cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
652
+ setGitIdentity(egregoreDir, user);
653
+
654
+ ui.step(2, totalSteps, "Cloning shared memory...");
655
+ cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
656
+ setGitIdentity(memoryDir, user);
657
+
658
+ // 7. Symlink
659
+ ui.step(3, totalSteps, "Linking memory...");
660
+ createSymlink(egregoreDir, memoryDir, ui);
661
+
662
+ // 8. Write .env
663
+ ui.step(4, totalSteps, "Writing credentials...");
664
+ fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${githubToken}\n`, { mode: 0o600 });
665
+ ui.success("Credentials saved");
666
+
667
+ // 9. "What should we call you?" + write state
668
+ console.log("");
669
+ const displayName = await ui.prompt(`What should we call you? [${user.login}]:`);
670
+
671
+ const state = {
672
+ github_username: user.login,
673
+ github_name: user.name,
674
+ display_name: displayName || user.login,
675
+ email: user.email,
676
+ onboarding_complete: false,
677
+ usage_type: "joiner_group",
678
+ org_setup: true,
679
+ github_configured: true,
680
+ workspace_ready: true,
681
+ };
682
+ fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
683
+
684
+ // 10. Register + alias
685
+ ui.step(5, totalSteps, "Registering instance...");
686
+ registerInstance(repoName, orgName, egregoreDir);
687
+ const alias = await installShellAlias(egregoreDir, ui);
688
+
689
+ // 11. Clone managed repos
690
+ for (let i = 0; i < managedRepos.length; i++) {
691
+ const mr = managedRepos[i];
692
+ ui.step(6 + i, totalSteps, `Cloning ${mr}...`);
693
+ const repoDir = path.join(base, mr);
694
+ try {
695
+ const repoUrl = `https://github.com/${orgLogin}/${mr}.git`;
696
+ cloneRepo(repoUrl, repoDir, githubToken, ui, mr);
697
+ setGitIdentity(repoDir, user);
698
+ } catch {
699
+ ui.warn(`Could not clone ${mr} — you may not have access yet.`);
700
+ }
701
+ }
702
+
703
+ // 12. Show Telegram group link if available
704
+ if (config.telegram_group_link) {
705
+ console.log("");
706
+ ui.info("Join the team's Telegram group for notifications:");
707
+ ui.info(` ${ui.cyan(config.telegram_group_link)}`);
708
+ }
709
+
710
+ // 13. Check for welcome note
711
+ const welcomeFile = path.join(memoryDir, "people", `${user.login}.md`);
712
+ if (fs.existsSync(welcomeFile)) {
713
+ try {
714
+ const content = fs.readFileSync(welcomeFile, "utf-8");
715
+ const inviterMatch = content.match(/invited_by:\s*(\S+)/);
716
+ // Check for content after the frontmatter closing ---
717
+ const bodyMatch = content.match(/^---[\s\S]*?---\s*\n([\s\S]+)/m);
718
+ if (inviterMatch) {
719
+ console.log("");
720
+ const note = bodyMatch && bodyMatch[1].trim() ? ` — "${bodyMatch[1].trim()}"` : "";
721
+ ui.info(`${inviterMatch[1]} invited you${note}`);
722
+ }
723
+ } catch {}
724
+ }
725
+
726
+ // 14. Done
727
+ console.log("");
728
+ ui.success(`Joined ${ui.bold(orgName)}`);
729
+ console.log("");
730
+ ui.info("Your workspace:");
731
+ ui.info(` ${ui.cyan(`./${repoName}/`)} — Egregore instance`);
732
+ ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
733
+ for (const mr of managedRepos) {
734
+ ui.info(` ${ui.cyan(`./${mr}/`)} — Managed repo`);
735
+ }
736
+ console.log("");
737
+ ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
738
+ console.log("");
739
+ }
740
+
741
+ module.exports = { localFounderFlow, localJoinFlow, addCollaborator, getUser, repoExists, createRepo, waitForRepo };
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.6.0",
4
4
  "description": "Set up Egregore for your team in one command",
5
5
  "license": "MIT",
6
6
  "bin": {