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 +56 -9
- package/lib/api.js +2 -2
- package/lib/auth.js +1 -1
- package/lib/setup.js +110 -15
- package/package.json +1 -1
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: "
|
|
126
|
-
action: "
|
|
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: "
|
|
149
|
-
action: "
|
|
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
|
|
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
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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");
|