create-egregore 0.3.10 → 0.3.12

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
@@ -81,6 +81,18 @@ async function tokenFlow(api, token) {
81
81
  process.exit(1);
82
82
  }
83
83
 
84
+ // Joiner tokens don't include a github_token — run device flow to get one
85
+ if (!data.github_token || data.needs_cli_auth) {
86
+ ui.info("\nSign in with GitHub to complete setup.\n");
87
+ try {
88
+ data.github_token = await deviceFlow(ui);
89
+ ui.success("Authenticated with GitHub");
90
+ } catch (err) {
91
+ ui.error(`GitHub auth failed: ${err.message}`);
92
+ process.exit(1);
93
+ }
94
+ }
95
+
84
96
  try {
85
97
  await install(data, ui);
86
98
  } catch (err) {
@@ -117,13 +129,22 @@ async function interactiveFlow(api) {
117
129
  // 3. Build choices
118
130
  const choices = [];
119
131
 
120
- // Orgs with egregore → join
132
+ // Orgs with egregore → show each instance to join + option to set up another
121
133
  for (const org of orgsData.orgs) {
122
134
  if (org.has_egregore) {
135
+ for (const inst of org.instances || []) {
136
+ choices.push({
137
+ label: `${org.name || org.login} — ${inst.org_name || inst.repo_name}`,
138
+ description: "Join existing",
139
+ action: "join",
140
+ login: org.login,
141
+ repo_name: inst.repo_name,
142
+ });
143
+ }
123
144
  choices.push({
124
- label: org.name || org.login,
125
- description: "Join existing",
126
- action: "join",
145
+ label: `${org.name || org.login} — new instance`,
146
+ description: "Set up another",
147
+ action: "setup",
127
148
  login: org.login,
128
149
  });
129
150
  }
@@ -141,13 +162,23 @@ async function interactiveFlow(api) {
141
162
  }
142
163
  }
143
164
 
144
- // Personal account
165
+ // Personal account — show join for each existing instance + setup for new
145
166
  if (orgsData.personal.has_egregore) {
167
+ for (const inst of orgsData.personal.instances || []) {
168
+ choices.push({
169
+ label: `${orgsData.user.login} (personal) — ${inst.org_name || inst.repo_name}`,
170
+ description: "Join existing",
171
+ action: "join",
172
+ login: orgsData.user.login,
173
+ repo_name: inst.repo_name,
174
+ });
175
+ }
146
176
  choices.push({
147
- label: `${orgsData.user.login} (personal)`,
148
- description: "Join existing",
149
- action: "join",
177
+ label: `${orgsData.user.login} (personal) — new instance`,
178
+ description: "Set up another",
179
+ action: "setup",
150
180
  login: orgsData.user.login,
181
+ is_personal: true,
151
182
  });
152
183
  } else {
153
184
  choices.push({
@@ -180,6 +211,10 @@ async function setupFlow(api, githubToken, choice) {
180
211
  const orgName = await ui.prompt(`Organization display name [${choice.login}]:`);
181
212
  const name = orgName || choice.login;
182
213
 
214
+ // Instance name — determines repo names (egregore-{instance}, {instance}-memory)
215
+ const instanceInput = await ui.prompt(`Instance name (e.g. "ops", "research") [leave blank for default]:`);
216
+ const instanceName = instanceInput || undefined;
217
+
183
218
  // Repo picker — show org repos for selection
184
219
  let selectedRepos = [];
185
220
  try {
@@ -194,6 +229,14 @@ async function setupFlow(api, githubToken, choice) {
194
229
  // Non-fatal — continue without repos
195
230
  }
196
231
 
232
+ // Transcript sharing consent
233
+ console.log("");
234
+ console.log(" Egregore can collect session transcripts to build");
235
+ console.log(" organizational memory (decisions, patterns, handoffs).");
236
+ console.log(" Transcripts are private to your org. Members can opt out individually.");
237
+ const consentAnswer = await ui.prompt("Enable transcript collection? [Y/n]:");
238
+ const transcriptSharing = !consentAnswer || consentAnswer.toLowerCase() !== "n";
239
+
197
240
  const s = ui.spinner(`Setting up Egregore for ${ui.bold(name)}...`);
198
241
  try {
199
242
  const result = await api.setupOrg(githubToken, {
@@ -201,6 +244,8 @@ async function setupFlow(api, githubToken, choice) {
201
244
  org_name: name,
202
245
  is_personal: choice.is_personal || false,
203
246
  repos: selectedRepos,
247
+ instance_name: instanceName,
248
+ transcript_sharing: transcriptSharing,
204
249
  });
205
250
  s.stop("Setup complete on GitHub");
206
251
 
@@ -220,10 +265,12 @@ async function setupFlow(api, githubToken, choice) {
220
265
  }
221
266
 
222
267
  async function joinFlow(api, githubToken, choice) {
223
- const s = ui.spinner(`Joining ${ui.bold(choice.login)}...`);
268
+ const displayName = choice.repo_name ? `${choice.login} (${choice.repo_name})` : choice.login;
269
+ const s = ui.spinner(`Joining ${ui.bold(displayName)}...`);
224
270
  try {
225
271
  const result = await api.joinOrg(githubToken, {
226
272
  github_org: choice.login,
273
+ repo_name: choice.repo_name,
227
274
  });
228
275
  s.stop("Joined");
229
276
 
package/lib/api.js CHANGED
@@ -79,8 +79,8 @@ class EgregoreAPI {
79
79
  });
80
80
  }
81
81
 
82
- async setupOrg(githubToken, { github_org, org_name, is_personal = false, repos = [], instance_name }) {
83
- const body = { github_org, org_name, is_personal, repos };
82
+ async setupOrg(githubToken, { github_org, org_name, is_personal = false, repos = [], instance_name, transcript_sharing = false }) {
83
+ const body = { github_org, org_name, is_personal, repos, transcript_sharing };
84
84
  if (instance_name) body.instance_name = instance_name;
85
85
  return request("POST", `${this.base}/api/org/setup`, {
86
86
  headers: { Authorization: `Bearer ${githubToken}` },
package/lib/auth.js CHANGED
@@ -6,7 +6,7 @@
6
6
  const https = require("node:https");
7
7
 
8
8
  const CLIENT_ID = "Ov23lizB4nYEeIRsHTdb";
9
- const SCOPE = "repo,admin:org";
9
+ const SCOPE = "repo,read:org";
10
10
 
11
11
  function post(url, body) {
12
12
  return new Promise((resolve, reject) => {
package/lib/setup.js CHANGED
@@ -8,10 +8,65 @@ const fs = require("node:fs");
8
8
  const path = require("node:path");
9
9
  const os = require("node:os");
10
10
 
11
+ const https = require("node:https");
12
+
11
13
  function run(cmd, opts = {}) {
12
14
  return execSync(cmd, { stdio: "pipe", encoding: "utf-8", timeout: 60000, ...opts }).trim();
13
15
  }
14
16
 
17
+ function ghApi(method, path, token) {
18
+ return new Promise((resolve, reject) => {
19
+ const req = https.request(
20
+ {
21
+ hostname: "api.github.com",
22
+ path,
23
+ method,
24
+ headers: {
25
+ Authorization: `Bearer ${token}`,
26
+ Accept: "application/vnd.github+json",
27
+ "User-Agent": "create-egregore",
28
+ },
29
+ },
30
+ (res) => {
31
+ let buf = "";
32
+ res.on("data", (c) => (buf += c));
33
+ res.on("end", () => {
34
+ try {
35
+ resolve({ status: res.statusCode, data: JSON.parse(buf) });
36
+ } catch {
37
+ resolve({ status: res.statusCode, data: buf });
38
+ }
39
+ });
40
+ }
41
+ );
42
+ req.on("error", reject);
43
+ req.end();
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Accept pending GitHub repo collaboration invitations for the given org.
49
+ */
50
+ async function acceptPendingInvitations(githubToken, githubOrg, ui) {
51
+ try {
52
+ const { status, data } = await ghApi("GET", "/user/repository_invitations", githubToken);
53
+ if (status !== 200 || !Array.isArray(data)) return;
54
+
55
+ const matching = data.filter(
56
+ (inv) => inv.repository?.owner?.login?.toLowerCase() === githubOrg.toLowerCase()
57
+ );
58
+
59
+ for (const inv of matching) {
60
+ const patchResult = await ghApi("PATCH", `/user/repository_invitations/${inv.id}`, githubToken);
61
+ if (patchResult.status < 300) {
62
+ ui.success(`Accepted invite to ${inv.repository.full_name}`);
63
+ }
64
+ }
65
+ } catch {
66
+ // Non-fatal — user may need to accept manually
67
+ }
68
+ }
69
+
15
70
  /**
16
71
  * Full local setup from claimed token data.
17
72
  *
@@ -20,7 +75,7 @@ function run(cmd, opts = {}) {
20
75
  * @param {string} targetDir - where to install (default: cwd)
21
76
  */
22
77
  async function install(data, ui, targetDir) {
23
- const { fork_url, memory_url, github_token, org_name, github_org, slug, api_key, repos = [], telegram_group_link } = data;
78
+ const { fork_url, memory_url, github_token, org_name, github_org, slug, api_key, repos = [], telegram_group_link, github_username, github_name, transcript_sharing } = data;
24
79
  const base = targetDir || process.cwd();
25
80
 
26
81
  // Directory name from fork URL (what git clone would use), e.g. "egregore-core"
@@ -37,6 +92,9 @@ async function install(data, ui, targetDir) {
37
92
  // Configure git credential helper for HTTPS cloning
38
93
  configureGitCredentials(github_token);
39
94
 
95
+ // Accept pending repo collaboration invitations (joiners get invited as collaborators)
96
+ await acceptPendingInvitations(github_token, github_org, ui);
97
+
40
98
  // Embed token in URLs as fallback for private repos (credential helper may not work)
41
99
  const authedForkUrl = embedToken(fork_url, github_token);
42
100
  const authedMemoryUrl = embedToken(memory_url, github_token);
@@ -52,6 +110,16 @@ async function install(data, ui, targetDir) {
52
110
  }
53
111
  ui.success("Cloned egregore");
54
112
 
113
+ // Set repo-local git identity from GitHub user (not machine-global config)
114
+ if (github_username) {
115
+ try {
116
+ run(`git config user.name "${github_name || github_username}"`, { cwd: egregoreDir });
117
+ run(`git config user.email "${github_username}@users.noreply.github.com"`, { cwd: egregoreDir });
118
+ } catch {
119
+ // Non-fatal
120
+ }
121
+ }
122
+
55
123
  // 2. Clone memory
56
124
  ui.step(2, totalSteps, "Cloning shared memory...");
57
125
  if (fs.existsSync(memoryDir)) {
@@ -87,6 +155,22 @@ async function install(data, ui, targetDir) {
87
155
  fs.writeFileSync(envPath, envLines.join("\n") + "\n", { mode: 0o600 });
88
156
  ui.success("Credentials saved");
89
157
 
158
+ // 4b. Save identity to .egregore-state.json (so session-start.sh knows who this is)
159
+ const statePath = path.join(egregoreDir, ".egregore-state.json");
160
+ let state = {};
161
+ if (fs.existsSync(statePath)) {
162
+ try { state = JSON.parse(fs.readFileSync(statePath, "utf-8")); } catch {}
163
+ }
164
+ if (github_username) {
165
+ state.github_username = github_username;
166
+ state.github_name = github_name || github_username;
167
+ }
168
+ state.onboarding_complete = true;
169
+ if (transcript_sharing !== undefined) {
170
+ state.transcript_sharing = transcript_sharing;
171
+ }
172
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
173
+
90
174
  // 5. Register instance + shell alias
91
175
  ui.step(5, totalSteps, "Registering instance...");
92
176
  registerInstance(forkDirName, org_name, egregoreDir);
@@ -94,20 +178,26 @@ async function install(data, ui, targetDir) {
94
178
 
95
179
  // 6+. Clone managed repos (if any)
96
180
  const clonedRepos = [];
181
+ const failedRepos = [];
97
182
  for (let i = 0; i < repos.length; i++) {
98
183
  const repoName = repos[i];
99
184
  ui.step(6 + i, totalSteps, `Cloning ${repoName}...`);
100
185
  const repoDir = path.join(base, repoName);
101
- if (fs.existsSync(repoDir)) {
102
- ui.warn(`${repoName}/ already exists — pulling latest`);
103
- run("git pull", { cwd: repoDir });
104
- } else {
105
- const repoUrl = `https://github.com/${github_org}/${repoName}.git`;
106
- execFileSync("git", ["clone", embedToken(repoUrl, github_token), repoDir], { stdio: "pipe", encoding: "utf-8", timeout: 60000 });
107
- try { run(`git remote set-url origin ${repoUrl}`, { cwd: repoDir }); } catch {}
186
+ try {
187
+ if (fs.existsSync(repoDir)) {
188
+ ui.warn(`${repoName}/ already exists pulling latest`);
189
+ run("git pull", { cwd: repoDir });
190
+ } else {
191
+ const repoUrl = `https://github.com/${github_org}/${repoName}.git`;
192
+ execFileSync("git", ["clone", embedToken(repoUrl, github_token), repoDir], { stdio: "pipe", encoding: "utf-8", timeout: 60000 });
193
+ try { run(`git remote set-url origin ${repoUrl}`, { cwd: repoDir }); } catch {}
194
+ }
195
+ clonedRepos.push(repoName);
196
+ ui.success(`Cloned ${repoName}`);
197
+ } catch {
198
+ failedRepos.push(repoName);
199
+ ui.warn(`Could not clone ${repoName} — you may not have access yet. Ask the org admin to grant access.`);
108
200
  }
109
- clonedRepos.push(repoName);
110
- ui.success(`Cloned ${repoName}`);
111
201
  }
112
202
 
113
203
  // Done
@@ -227,11 +317,16 @@ async function installShellAlias(egregoreDir, ui) {
227
317
  defaultName = slug ? `egregore-${slug}` : "egregore-2";
228
318
  }
229
319
 
230
- // Ask user
231
- console.log("");
232
- ui.info(`This instance will be launched with a shell command.`);
233
- const answer = await ui.prompt(`Command name (Enter for ${ui.bold(defaultName)}):`);
234
- const aliasName = answer || defaultName;
320
+ // Ask user (skip prompt in non-interactive mode)
321
+ let aliasName;
322
+ if (process.stdin.isTTY) {
323
+ console.log("");
324
+ ui.info(`This instance will be launched with a shell command.`);
325
+ const answer = await ui.prompt(`Command name (Enter for ${ui.bold(defaultName)}):`);
326
+ aliasName = answer || defaultName;
327
+ } else {
328
+ aliasName = defaultName;
329
+ }
235
330
 
236
331
  // Remove old alias for this directory
237
332
  let lines = profileContent.split("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Set up Egregore for your team in one command",
5
5
  "license": "MIT",
6
6
  "bin": {