create-egregore 0.15.0 → 0.16.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/lib/api.js +7 -0
- package/lib/local.js +134 -33
- package/lib/setup.js +48 -0
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -95,6 +95,13 @@ class EgregoreAPI {
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
async registerOrg(githubToken, { org_name, github_org, slug }) {
|
|
99
|
+
return request("POST", `${this.base}/api/org/register`, {
|
|
100
|
+
headers: { Authorization: `Bearer ${githubToken}` },
|
|
101
|
+
body: { org_name, github_org, slug },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
async exchangeCode(code) {
|
|
99
106
|
return request("POST", `${this.base}/api/auth/github/callback`, {
|
|
100
107
|
body: { code },
|
package/lib/local.js
CHANGED
|
@@ -15,8 +15,10 @@ const {
|
|
|
15
15
|
embedToken,
|
|
16
16
|
configureGitCredentials,
|
|
17
17
|
initMemoryDirs,
|
|
18
|
+
ensureDevelop,
|
|
18
19
|
run,
|
|
19
20
|
} = require("./setup");
|
|
21
|
+
const { EgregoreAPI } = require("./api");
|
|
20
22
|
|
|
21
23
|
// Template repo for new egregore instances (OSS)
|
|
22
24
|
const TEMPLATE_OWNER = "egregore-labs";
|
|
@@ -146,6 +148,25 @@ async function addCollaborator(token, owner, repo, username) {
|
|
|
146
148
|
return status === 201 || status === 204;
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
// ── Branch detection for existing repos ────────────────────────────
|
|
152
|
+
|
|
153
|
+
async function detectRepoBranches(token, owner, repoName) {
|
|
154
|
+
const { status, data } = await ghApi("GET", `/repos/${owner}/${repoName}/branches?per_page=100`, token);
|
|
155
|
+
if (status !== 200 || !Array.isArray(data)) return null;
|
|
156
|
+
const branches = data.map((b) => b.name);
|
|
157
|
+
// Detect default branch via repo API
|
|
158
|
+
const { status: repoStatus, data: repoData } = await ghApi("GET", `/repos/${owner}/${repoName}`, token);
|
|
159
|
+
const defaultBranch = repoStatus === 200 ? repoData.default_branch || "main" : "main";
|
|
160
|
+
return {
|
|
161
|
+
name: repoName,
|
|
162
|
+
defaultBranch,
|
|
163
|
+
hasDevelop: branches.includes("develop"),
|
|
164
|
+
branches,
|
|
165
|
+
branchCount: branches.length,
|
|
166
|
+
contributors: branches.filter((b) => b.startsWith("dev/")).map((b) => b.split("/")[1]).filter((v, i, a) => a.indexOf(v) === i),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
149
170
|
// ── Clone + local file helpers ──────────────────────────────────────
|
|
150
171
|
|
|
151
172
|
function cloneRepo(url, dir, token, ui, label) {
|
|
@@ -168,6 +189,43 @@ function cloneRepo(url, dir, token, ui, label) {
|
|
|
168
189
|
ui.success(`Cloned ${label}`);
|
|
169
190
|
}
|
|
170
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Convert HTTPS GitHub URL to SSH format.
|
|
194
|
+
* https://github.com/org/repo.git → git@github.com:org/repo.git
|
|
195
|
+
*/
|
|
196
|
+
function toSSH(url) {
|
|
197
|
+
const m = url.match(/https:\/\/github\.com\/(.+?)(?:\.git)?$/);
|
|
198
|
+
return m ? `git@github.com:${m[1]}.git` : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Clone a repo, trying SSH first then HTTPS.
|
|
203
|
+
* GIT_TERMINAL_PROMPT=0 prevents the hanging username/password prompt.
|
|
204
|
+
*/
|
|
205
|
+
function cloneWithFallback(httpsUrl, dir, timeout = 60000) {
|
|
206
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
207
|
+
const sshUrl = toSSH(httpsUrl);
|
|
208
|
+
|
|
209
|
+
// Try SSH first (most devs have SSH keys; GitHub no longer supports HTTPS password auth)
|
|
210
|
+
if (sshUrl) {
|
|
211
|
+
try {
|
|
212
|
+
execFileSync("git", ["clone", sshUrl, dir], { stdio: "pipe", encoding: "utf-8", timeout, env });
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
// Clean up partial clone so HTTPS fallback can use the same dir
|
|
216
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Fall back to HTTPS (works if user has gh CLI or a credential helper configured)
|
|
221
|
+
try {
|
|
222
|
+
execFileSync("git", ["clone", httpsUrl, dir], { stdio: "pipe", encoding: "utf-8", timeout, env });
|
|
223
|
+
return true;
|
|
224
|
+
} catch {}
|
|
225
|
+
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
171
229
|
function setGitIdentity(dir, user) {
|
|
172
230
|
try {
|
|
173
231
|
run(`git config user.name "${user.name}"`, { cwd: dir });
|
|
@@ -280,16 +338,42 @@ async function localFounderFlow(ui) {
|
|
|
280
338
|
} catch {
|
|
281
339
|
s0.stop("Could not load repos");
|
|
282
340
|
}
|
|
341
|
+
|
|
342
|
+
// Detect default branch for each selected repo and convert to {name, base_branch} format
|
|
343
|
+
if (selectedRepos.length > 0) {
|
|
344
|
+
const s0b = ui.spinner("Checking branch setup...");
|
|
345
|
+
const reposWithBranch = [];
|
|
346
|
+
for (const repoName of selectedRepos) {
|
|
347
|
+
try {
|
|
348
|
+
const info = await detectRepoBranches(githubToken, githubOrg, repoName);
|
|
349
|
+
reposWithBranch.push({ name: repoName, base_branch: info ? info.defaultBranch : "main" });
|
|
350
|
+
} catch {
|
|
351
|
+
reposWithBranch.push({ name: repoName, base_branch: "main" });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
s0b.stop("Checked branches");
|
|
355
|
+
// Replace flat list with enriched objects
|
|
356
|
+
selectedRepos = reposWithBranch;
|
|
357
|
+
console.log("");
|
|
358
|
+
ui.info("Detected branch setup:");
|
|
359
|
+
for (const repo of reposWithBranch) {
|
|
360
|
+
ui.info(` ${ui.cyan(repo.name)} — PRs will target ${ui.bold(repo.base_branch)}`);
|
|
361
|
+
}
|
|
362
|
+
console.log("");
|
|
363
|
+
}
|
|
283
364
|
}
|
|
284
365
|
|
|
285
366
|
// 6. Derive repo names from team name
|
|
286
|
-
const
|
|
367
|
+
const nameSlug = orgName
|
|
287
368
|
.toLowerCase()
|
|
288
369
|
.replace(/[^a-z0-9-]/g, "-")
|
|
289
370
|
.replace(/-+/g, "-")
|
|
290
371
|
.replace(/^-|-$/g, "") || "egregore";
|
|
291
|
-
|
|
292
|
-
const
|
|
372
|
+
// Org slug prefixed with GitHub owner — globally unique across all egregores
|
|
373
|
+
const ghPrefix = githubOrg.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
374
|
+
const slug = ghPrefix === nameSlug ? nameSlug : `${ghPrefix}-${nameSlug}`;
|
|
375
|
+
const repoName = nameSlug;
|
|
376
|
+
const memoryRepoName = `${nameSlug}-memory`;
|
|
293
377
|
|
|
294
378
|
// Guard: if egregore repo already exists with a config, don't overwrite
|
|
295
379
|
console.log("");
|
|
@@ -360,13 +444,13 @@ async function localFounderFlow(ui) {
|
|
|
360
444
|
const s3 = ui.spinner(`Creating ${newProjectRepo}...`);
|
|
361
445
|
const projExists = await repoExists(githubToken, githubOrg, newProjectRepo);
|
|
362
446
|
if (projExists) {
|
|
363
|
-
selectedRepos.push(newProjectRepo);
|
|
447
|
+
selectedRepos.push({ name: newProjectRepo });
|
|
364
448
|
s3.stop(`${newProjectRepo} already exists — added to managed repos`);
|
|
365
449
|
} else {
|
|
366
450
|
try {
|
|
367
451
|
await createRepo(githubToken, githubOrg, newProjectRepo, isOrg, description || "");
|
|
368
452
|
if (await waitForRepo(githubToken, githubOrg, newProjectRepo)) {
|
|
369
|
-
selectedRepos.push(newProjectRepo);
|
|
453
|
+
selectedRepos.push({ name: newProjectRepo });
|
|
370
454
|
s3.stop(`Created ${newProjectRepo}`);
|
|
371
455
|
} else {
|
|
372
456
|
s3.fail(`${newProjectRepo} creation timed out`);
|
|
@@ -401,6 +485,15 @@ async function localFounderFlow(ui) {
|
|
|
401
485
|
);
|
|
402
486
|
} catch {}
|
|
403
487
|
|
|
488
|
+
// 8b. Register org with Egregore API (non-blocking — local mode works without it)
|
|
489
|
+
// Respects DO_NOT_TRACK (https://consult.gov.uk/digital-communications/online-advertising/supporting_documents/DNT%20standard%20v10.pdf)
|
|
490
|
+
if (!process.env.DO_NOT_TRACK && !process.env.EGREGORE_NO_TELEMETRY) {
|
|
491
|
+
try {
|
|
492
|
+
const api = new EgregoreAPI();
|
|
493
|
+
await api.registerOrg(githubToken, { org_name: orgName, github_org: githubOrg, slug });
|
|
494
|
+
} catch {}
|
|
495
|
+
}
|
|
496
|
+
|
|
404
497
|
// 9. Init memory dirs on remote (so joiners get the structure)
|
|
405
498
|
const memDirs = ["people", "handoffs", "knowledge/decisions", "knowledge/patterns", "knowledge/findings", "quests", "wraps"];
|
|
406
499
|
for (const d of memDirs) {
|
|
@@ -422,6 +515,7 @@ async function localFounderFlow(ui) {
|
|
|
422
515
|
ui.step(1, totalSteps, "Cloning egregore...");
|
|
423
516
|
cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
|
|
424
517
|
setGitIdentity(egregoreDir, user);
|
|
518
|
+
ensureDevelop(egregoreDir);
|
|
425
519
|
|
|
426
520
|
ui.step(2, totalSteps, "Cloning shared memory...");
|
|
427
521
|
cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
|
|
@@ -482,15 +576,20 @@ async function localFounderFlow(ui) {
|
|
|
482
576
|
|
|
483
577
|
// 16. Clone managed repos
|
|
484
578
|
for (let i = 0; i < selectedRepos.length; i++) {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
579
|
+
const entry = selectedRepos[i];
|
|
580
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
581
|
+
ui.step(5 + i, totalSteps, `Cloning ${mrName}...`);
|
|
582
|
+
const repoDir = path.join(base, mrName);
|
|
488
583
|
try {
|
|
489
|
-
const repoUrl = `https://github.com/${githubOrg}/${
|
|
490
|
-
cloneRepo(repoUrl, repoDir, githubToken, ui,
|
|
584
|
+
const repoUrl = `https://github.com/${githubOrg}/${mrName}.git`;
|
|
585
|
+
cloneRepo(repoUrl, repoDir, githubToken, ui, mrName);
|
|
491
586
|
setGitIdentity(repoDir, user);
|
|
587
|
+
// New repos get develop branch (our workflow). Existing repos keep their default.
|
|
588
|
+
if (isNewProject) {
|
|
589
|
+
ensureDevelop(repoDir);
|
|
590
|
+
}
|
|
492
591
|
} catch {
|
|
493
|
-
ui.warn(`Could not clone ${
|
|
592
|
+
ui.warn(`Could not clone ${mrName} — you may not have access.`);
|
|
494
593
|
}
|
|
495
594
|
}
|
|
496
595
|
|
|
@@ -508,8 +607,9 @@ async function localFounderFlow(ui) {
|
|
|
508
607
|
// Add as collaborator to all repos
|
|
509
608
|
await addCollaborator(githubToken, githubOrg, repoName, invUsername);
|
|
510
609
|
await addCollaborator(githubToken, githubOrg, memoryRepoName, invUsername);
|
|
511
|
-
for (const
|
|
512
|
-
|
|
610
|
+
for (const entry of selectedRepos) {
|
|
611
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
612
|
+
await addCollaborator(githubToken, githubOrg, mrName, invUsername).catch(() => {});
|
|
513
613
|
}
|
|
514
614
|
|
|
515
615
|
// Create person file
|
|
@@ -548,8 +648,9 @@ async function localFounderFlow(ui) {
|
|
|
548
648
|
ui.info("Your workspace:");
|
|
549
649
|
ui.info(` ${ui.cyan(`./${repoName}/`)} — Your Egregore instance`);
|
|
550
650
|
ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
|
|
551
|
-
for (const
|
|
552
|
-
|
|
651
|
+
for (const entry of selectedRepos) {
|
|
652
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
653
|
+
ui.info(` ${ui.cyan(`./${mrName}/`)} — Managed repo`);
|
|
553
654
|
}
|
|
554
655
|
console.log("");
|
|
555
656
|
ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
|
|
@@ -602,16 +703,16 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
602
703
|
|
|
603
704
|
console.log("");
|
|
604
705
|
const s2 = ui.spinner(`Cloning ${orgLogin}/${repoName}...`);
|
|
605
|
-
|
|
606
|
-
execFileSync("git", ["clone", `https://github.com/${orgLogin}/${repoName}.git`, egregoreDir], { stdio: "pipe", encoding: "utf-8", timeout: 60000 });
|
|
706
|
+
if (cloneWithFallback(`https://github.com/${orgLogin}/${repoName}.git`, egregoreDir)) {
|
|
607
707
|
s2.stop(`Cloned ${repoName}`);
|
|
608
|
-
}
|
|
708
|
+
} else {
|
|
609
709
|
s2.fail(`Could not clone ${orgLogin}/${repoName}`);
|
|
610
710
|
ui.error("Make sure you have git credentials set up (SSH keys or gh auth login).");
|
|
611
711
|
ui.error("Also verify the org admin has set up Egregore and invited you.");
|
|
612
712
|
process.exit(1);
|
|
613
713
|
}
|
|
614
714
|
setGitIdentity(egregoreDir, user);
|
|
715
|
+
ensureDevelop(egregoreDir);
|
|
615
716
|
|
|
616
717
|
// 6. Read egregore.json from local clone
|
|
617
718
|
const configPath = path.join(egregoreDir, "egregore.json");
|
|
@@ -642,12 +743,11 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
642
743
|
// Store the invite token for later use (invitation acceptance, not cloning)
|
|
643
744
|
const inviteToken = githubToken;
|
|
644
745
|
|
|
645
|
-
// Clone memory using
|
|
746
|
+
// Clone memory using SSH-first fallback (same as main repo)
|
|
646
747
|
ui.step(1, totalSteps, "Cloning shared memory...");
|
|
647
|
-
|
|
648
|
-
execFileSync("git", ["clone", memoryUrl, memoryDir], { stdio: "pipe", encoding: "utf-8", timeout: 60000 });
|
|
748
|
+
if (cloneWithFallback(memoryUrl, memoryDir)) {
|
|
649
749
|
ui.success("Cloned memory");
|
|
650
|
-
}
|
|
750
|
+
} else {
|
|
651
751
|
ui.warn("Could not clone memory repo — you may not have access yet.");
|
|
652
752
|
}
|
|
653
753
|
setGitIdentity(memoryDir, user);
|
|
@@ -683,17 +783,17 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
683
783
|
registerInstance(repoName, orgName, egregoreDir);
|
|
684
784
|
const alias = await installShellAlias(egregoreDir, ui);
|
|
685
785
|
|
|
686
|
-
// Clone managed repos using
|
|
786
|
+
// Clone managed repos using SSH-first fallback
|
|
687
787
|
for (let i = 0; i < managedRepos.length; i++) {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
788
|
+
const entry = managedRepos[i];
|
|
789
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
790
|
+
ui.step(5 + i, totalSteps, `Cloning ${mrName}...`);
|
|
791
|
+
const repoDir = path.join(base, mrName);
|
|
792
|
+
if (cloneWithFallback(`https://github.com/${orgLogin}/${mrName}.git`, repoDir)) {
|
|
693
793
|
setGitIdentity(repoDir, user);
|
|
694
|
-
ui.success(`Cloned ${
|
|
695
|
-
}
|
|
696
|
-
ui.warn(`Could not clone ${
|
|
794
|
+
ui.success(`Cloned ${mrName}`);
|
|
795
|
+
} else {
|
|
796
|
+
ui.warn(`Could not clone ${mrName} — you may not have access yet.`);
|
|
697
797
|
}
|
|
698
798
|
}
|
|
699
799
|
|
|
@@ -727,8 +827,9 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
727
827
|
ui.info("Your workspace:");
|
|
728
828
|
ui.info(` ${ui.cyan(`./${repoName}/`)} — Egregore instance`);
|
|
729
829
|
ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
|
|
730
|
-
for (const
|
|
731
|
-
|
|
830
|
+
for (const entry of managedRepos) {
|
|
831
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
832
|
+
ui.info(` ${ui.cyan(`./${mrName}/`)} — Managed repo`);
|
|
732
833
|
}
|
|
733
834
|
console.log("");
|
|
734
835
|
ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
|
package/lib/setup.js
CHANGED
|
@@ -138,6 +138,7 @@ async function install(data, ui, targetDir) {
|
|
|
138
138
|
try { run(`git remote set-url origin ${fork_url}`, { cwd: egregoreDir }); } catch {}
|
|
139
139
|
}
|
|
140
140
|
try { run("git config credential.helper store", { cwd: egregoreDir }); } catch {}
|
|
141
|
+
ensureDevelop(egregoreDir);
|
|
141
142
|
ui.success("Cloned egregore");
|
|
142
143
|
|
|
143
144
|
// Set repo-local git identity from GitHub user (not machine-global config)
|
|
@@ -246,6 +247,7 @@ async function install(data, ui, targetDir) {
|
|
|
246
247
|
try { run(`git remote set-url origin ${repoUrl}`, { cwd: repoDir }); } catch {}
|
|
247
248
|
}
|
|
248
249
|
try { run("git config credential.helper store", { cwd: repoDir }); } catch {}
|
|
250
|
+
ensureDevelop(repoDir);
|
|
249
251
|
clonedRepos.push(repoName);
|
|
250
252
|
ui.success(`Cloned ${repoName}`);
|
|
251
253
|
} catch {
|
|
@@ -274,6 +276,51 @@ async function install(data, ui, targetDir) {
|
|
|
274
276
|
console.log("");
|
|
275
277
|
}
|
|
276
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Ensure a cloned repo has a develop branch. Creates from default branch and pushes if missing.
|
|
281
|
+
* Idempotent — safe to call on repos that already have develop.
|
|
282
|
+
*/
|
|
283
|
+
function ensureDevelop(repoDir) {
|
|
284
|
+
try {
|
|
285
|
+
// Check if develop already exists on remote
|
|
286
|
+
const remoteRefs = execFileSync("git", ["-C", repoDir, "ls-remote", "--heads", "origin", "develop"], {
|
|
287
|
+
stdio: "pipe", encoding: "utf-8", timeout: 15000,
|
|
288
|
+
}).trim();
|
|
289
|
+
if (remoteRefs) return; // develop already exists on remote
|
|
290
|
+
|
|
291
|
+
// Detect default branch (main, master, or whatever origin/HEAD points to)
|
|
292
|
+
let defaultBranch = "";
|
|
293
|
+
try {
|
|
294
|
+
const head = execFileSync("git", ["-C", repoDir, "symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
295
|
+
stdio: "pipe", encoding: "utf-8", timeout: 5000,
|
|
296
|
+
}).trim();
|
|
297
|
+
defaultBranch = head.replace("refs/remotes/origin/", "");
|
|
298
|
+
} catch {}
|
|
299
|
+
if (!defaultBranch) {
|
|
300
|
+
for (const candidate of ["main", "master"]) {
|
|
301
|
+
try {
|
|
302
|
+
execFileSync("git", ["-C", repoDir, "show-ref", "--verify", "--quiet", `refs/remotes/origin/${candidate}`], {
|
|
303
|
+
stdio: "pipe", timeout: 5000,
|
|
304
|
+
});
|
|
305
|
+
defaultBranch = candidate;
|
|
306
|
+
break;
|
|
307
|
+
} catch {}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (!defaultBranch) return; // no default branch found — skip
|
|
311
|
+
|
|
312
|
+
// Create develop from default branch and push
|
|
313
|
+
execFileSync("git", ["-C", repoDir, "branch", "develop", `origin/${defaultBranch}`], {
|
|
314
|
+
stdio: "pipe", encoding: "utf-8", timeout: 5000,
|
|
315
|
+
});
|
|
316
|
+
execFileSync("git", ["-C", repoDir, "push", "-u", "origin", "develop"], {
|
|
317
|
+
stdio: "pipe", encoding: "utf-8", timeout: 30000,
|
|
318
|
+
});
|
|
319
|
+
} catch {
|
|
320
|
+
// Non-fatal — session-start.sh will retry on first launch
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
277
324
|
function embedToken(url, token) {
|
|
278
325
|
try {
|
|
279
326
|
return url.replace("https://github.com/", `https://x-access-token:${token}@github.com/`);
|
|
@@ -415,5 +462,6 @@ module.exports = {
|
|
|
415
462
|
embedToken,
|
|
416
463
|
configureGitCredentials,
|
|
417
464
|
initMemoryDirs,
|
|
465
|
+
ensureDevelop,
|
|
418
466
|
run,
|
|
419
467
|
};
|