create-egregore 0.15.1 → 0.16.1
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 +90 -23
- package/lib/setup.js +89 -14
- 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) {
|
|
@@ -317,16 +338,42 @@ async function localFounderFlow(ui) {
|
|
|
317
338
|
} catch {
|
|
318
339
|
s0.stop("Could not load repos");
|
|
319
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
|
+
}
|
|
320
364
|
}
|
|
321
365
|
|
|
322
366
|
// 6. Derive repo names from team name
|
|
323
|
-
const
|
|
367
|
+
const nameSlug = orgName
|
|
324
368
|
.toLowerCase()
|
|
325
369
|
.replace(/[^a-z0-9-]/g, "-")
|
|
326
370
|
.replace(/-+/g, "-")
|
|
327
371
|
.replace(/^-|-$/g, "") || "egregore";
|
|
328
|
-
|
|
329
|
-
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`;
|
|
330
377
|
|
|
331
378
|
// Guard: if egregore repo already exists with a config, don't overwrite
|
|
332
379
|
console.log("");
|
|
@@ -397,13 +444,13 @@ async function localFounderFlow(ui) {
|
|
|
397
444
|
const s3 = ui.spinner(`Creating ${newProjectRepo}...`);
|
|
398
445
|
const projExists = await repoExists(githubToken, githubOrg, newProjectRepo);
|
|
399
446
|
if (projExists) {
|
|
400
|
-
selectedRepos.push(newProjectRepo);
|
|
447
|
+
selectedRepos.push({ name: newProjectRepo });
|
|
401
448
|
s3.stop(`${newProjectRepo} already exists — added to managed repos`);
|
|
402
449
|
} else {
|
|
403
450
|
try {
|
|
404
451
|
await createRepo(githubToken, githubOrg, newProjectRepo, isOrg, description || "");
|
|
405
452
|
if (await waitForRepo(githubToken, githubOrg, newProjectRepo)) {
|
|
406
|
-
selectedRepos.push(newProjectRepo);
|
|
453
|
+
selectedRepos.push({ name: newProjectRepo });
|
|
407
454
|
s3.stop(`Created ${newProjectRepo}`);
|
|
408
455
|
} else {
|
|
409
456
|
s3.fail(`${newProjectRepo} creation timed out`);
|
|
@@ -438,6 +485,15 @@ async function localFounderFlow(ui) {
|
|
|
438
485
|
);
|
|
439
486
|
} catch {}
|
|
440
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
|
+
|
|
441
497
|
// 9. Init memory dirs on remote (so joiners get the structure)
|
|
442
498
|
const memDirs = ["people", "handoffs", "knowledge/decisions", "knowledge/patterns", "knowledge/findings", "quests", "wraps"];
|
|
443
499
|
for (const d of memDirs) {
|
|
@@ -459,6 +515,7 @@ async function localFounderFlow(ui) {
|
|
|
459
515
|
ui.step(1, totalSteps, "Cloning egregore...");
|
|
460
516
|
cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
|
|
461
517
|
setGitIdentity(egregoreDir, user);
|
|
518
|
+
ensureDevelop(egregoreDir);
|
|
462
519
|
|
|
463
520
|
ui.step(2, totalSteps, "Cloning shared memory...");
|
|
464
521
|
cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
|
|
@@ -519,15 +576,20 @@ async function localFounderFlow(ui) {
|
|
|
519
576
|
|
|
520
577
|
// 16. Clone managed repos
|
|
521
578
|
for (let i = 0; i < selectedRepos.length; i++) {
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
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);
|
|
525
583
|
try {
|
|
526
|
-
const repoUrl = `https://github.com/${githubOrg}/${
|
|
527
|
-
cloneRepo(repoUrl, repoDir, githubToken, ui,
|
|
584
|
+
const repoUrl = `https://github.com/${githubOrg}/${mrName}.git`;
|
|
585
|
+
cloneRepo(repoUrl, repoDir, githubToken, ui, mrName);
|
|
528
586
|
setGitIdentity(repoDir, user);
|
|
587
|
+
// New repos get develop branch (our workflow). Existing repos keep their default.
|
|
588
|
+
if (isNewProject) {
|
|
589
|
+
ensureDevelop(repoDir);
|
|
590
|
+
}
|
|
529
591
|
} catch {
|
|
530
|
-
ui.warn(`Could not clone ${
|
|
592
|
+
ui.warn(`Could not clone ${mrName} — you may not have access.`);
|
|
531
593
|
}
|
|
532
594
|
}
|
|
533
595
|
|
|
@@ -545,8 +607,9 @@ async function localFounderFlow(ui) {
|
|
|
545
607
|
// Add as collaborator to all repos
|
|
546
608
|
await addCollaborator(githubToken, githubOrg, repoName, invUsername);
|
|
547
609
|
await addCollaborator(githubToken, githubOrg, memoryRepoName, invUsername);
|
|
548
|
-
for (const
|
|
549
|
-
|
|
610
|
+
for (const entry of selectedRepos) {
|
|
611
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
612
|
+
await addCollaborator(githubToken, githubOrg, mrName, invUsername).catch(() => {});
|
|
550
613
|
}
|
|
551
614
|
|
|
552
615
|
// Create person file
|
|
@@ -585,8 +648,9 @@ async function localFounderFlow(ui) {
|
|
|
585
648
|
ui.info("Your workspace:");
|
|
586
649
|
ui.info(` ${ui.cyan(`./${repoName}/`)} — Your Egregore instance`);
|
|
587
650
|
ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
|
|
588
|
-
for (const
|
|
589
|
-
|
|
651
|
+
for (const entry of selectedRepos) {
|
|
652
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
653
|
+
ui.info(` ${ui.cyan(`./${mrName}/`)} — Managed repo`);
|
|
590
654
|
}
|
|
591
655
|
console.log("");
|
|
592
656
|
ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
|
|
@@ -648,6 +712,7 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
648
712
|
process.exit(1);
|
|
649
713
|
}
|
|
650
714
|
setGitIdentity(egregoreDir, user);
|
|
715
|
+
ensureDevelop(egregoreDir);
|
|
651
716
|
|
|
652
717
|
// 6. Read egregore.json from local clone
|
|
653
718
|
const configPath = path.join(egregoreDir, "egregore.json");
|
|
@@ -720,14 +785,15 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
720
785
|
|
|
721
786
|
// Clone managed repos using SSH-first fallback
|
|
722
787
|
for (let i = 0; i < managedRepos.length; i++) {
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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)) {
|
|
727
793
|
setGitIdentity(repoDir, user);
|
|
728
|
-
ui.success(`Cloned ${
|
|
794
|
+
ui.success(`Cloned ${mrName}`);
|
|
729
795
|
} else {
|
|
730
|
-
ui.warn(`Could not clone ${
|
|
796
|
+
ui.warn(`Could not clone ${mrName} — you may not have access yet.`);
|
|
731
797
|
}
|
|
732
798
|
}
|
|
733
799
|
|
|
@@ -761,8 +827,9 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
761
827
|
ui.info("Your workspace:");
|
|
762
828
|
ui.info(` ${ui.cyan(`./${repoName}/`)} — Egregore instance`);
|
|
763
829
|
ui.info(` ${ui.cyan(`./${memoryRepoName}/`)} — Shared knowledge`);
|
|
764
|
-
for (const
|
|
765
|
-
|
|
830
|
+
for (const entry of managedRepos) {
|
|
831
|
+
const mrName = typeof entry === "string" ? entry : entry.name;
|
|
832
|
+
ui.info(` ${ui.cyan(`./${mrName}/`)} — Managed repo`);
|
|
766
833
|
}
|
|
767
834
|
console.log("");
|
|
768
835
|
ui.info(`Next: open a ${ui.bold("new terminal")} and type ${ui.bold(alias.aliasName)} to start.`);
|
package/lib/setup.js
CHANGED
|
@@ -118,26 +118,21 @@ async function install(data, ui, targetDir) {
|
|
|
118
118
|
|
|
119
119
|
const totalSteps = 5 + repos.length;
|
|
120
120
|
|
|
121
|
-
// Configure git credential helper for HTTPS cloning
|
|
122
|
-
configureGitCredentials(github_token);
|
|
123
|
-
|
|
124
121
|
// Accept pending repo collaboration invitations (joiners get invited as collaborators)
|
|
125
122
|
await acceptPendingInvitations(github_token, github_org, ui);
|
|
126
123
|
|
|
127
|
-
//
|
|
128
|
-
const authedForkUrl = embedToken(fork_url, github_token);
|
|
129
|
-
const authedMemoryUrl = embedToken(memory_url, github_token);
|
|
130
|
-
|
|
131
|
-
// 1. Clone fork
|
|
124
|
+
// 1. Clone fork (SSH first, HTTPS fallback — no token embedding)
|
|
132
125
|
ui.step(1, totalSteps, "Cloning egregore...");
|
|
133
126
|
if (fs.existsSync(egregoreDir)) {
|
|
134
127
|
ui.warn("egregore/ already exists — pulling latest");
|
|
135
128
|
run("git pull", { cwd: egregoreDir });
|
|
136
129
|
} else {
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
if (!cloneRepo(fork_url, egregoreDir)) {
|
|
131
|
+
throw new Error(`Command failed: git clone ${fork_url} ${egregoreDir}\nCloning into '${egregoreDir}'...\nMake sure you have SSH keys set up or run 'gh auth login' first.`);
|
|
132
|
+
}
|
|
139
133
|
}
|
|
140
134
|
try { run("git config credential.helper store", { cwd: egregoreDir }); } catch {}
|
|
135
|
+
ensureDevelop(egregoreDir);
|
|
141
136
|
ui.success("Cloned egregore");
|
|
142
137
|
|
|
143
138
|
// Set repo-local git identity from GitHub user (not machine-global config)
|
|
@@ -156,8 +151,9 @@ async function install(data, ui, targetDir) {
|
|
|
156
151
|
ui.warn(`${memoryDirName}/ already exists — pulling latest`);
|
|
157
152
|
run("git pull", { cwd: memoryDir });
|
|
158
153
|
} else {
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
if (!cloneRepo(memory_url, memoryDir)) {
|
|
155
|
+
ui.warn(`Could not clone memory repo — you may need SSH keys or 'gh auth login'`);
|
|
156
|
+
}
|
|
161
157
|
}
|
|
162
158
|
try { run("git config credential.helper store", { cwd: memoryDir }); } catch {}
|
|
163
159
|
if (github_username) {
|
|
@@ -242,10 +238,12 @@ async function install(data, ui, targetDir) {
|
|
|
242
238
|
run("git pull", { cwd: repoDir });
|
|
243
239
|
} else {
|
|
244
240
|
const repoUrl = `https://github.com/${github_org}/${repoName}.git`;
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
if (!cloneRepo(repoUrl, repoDir)) {
|
|
242
|
+
throw new Error(`Clone failed for ${repoName}`);
|
|
243
|
+
}
|
|
247
244
|
}
|
|
248
245
|
try { run("git config credential.helper store", { cwd: repoDir }); } catch {}
|
|
246
|
+
ensureDevelop(repoDir);
|
|
249
247
|
clonedRepos.push(repoName);
|
|
250
248
|
ui.success(`Cloned ${repoName}`);
|
|
251
249
|
} catch {
|
|
@@ -274,6 +272,51 @@ async function install(data, ui, targetDir) {
|
|
|
274
272
|
console.log("");
|
|
275
273
|
}
|
|
276
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Ensure a cloned repo has a develop branch. Creates from default branch and pushes if missing.
|
|
277
|
+
* Idempotent — safe to call on repos that already have develop.
|
|
278
|
+
*/
|
|
279
|
+
function ensureDevelop(repoDir) {
|
|
280
|
+
try {
|
|
281
|
+
// Check if develop already exists on remote
|
|
282
|
+
const remoteRefs = execFileSync("git", ["-C", repoDir, "ls-remote", "--heads", "origin", "develop"], {
|
|
283
|
+
stdio: "pipe", encoding: "utf-8", timeout: 15000,
|
|
284
|
+
}).trim();
|
|
285
|
+
if (remoteRefs) return; // develop already exists on remote
|
|
286
|
+
|
|
287
|
+
// Detect default branch (main, master, or whatever origin/HEAD points to)
|
|
288
|
+
let defaultBranch = "";
|
|
289
|
+
try {
|
|
290
|
+
const head = execFileSync("git", ["-C", repoDir, "symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
291
|
+
stdio: "pipe", encoding: "utf-8", timeout: 5000,
|
|
292
|
+
}).trim();
|
|
293
|
+
defaultBranch = head.replace("refs/remotes/origin/", "");
|
|
294
|
+
} catch {}
|
|
295
|
+
if (!defaultBranch) {
|
|
296
|
+
for (const candidate of ["main", "master"]) {
|
|
297
|
+
try {
|
|
298
|
+
execFileSync("git", ["-C", repoDir, "show-ref", "--verify", "--quiet", `refs/remotes/origin/${candidate}`], {
|
|
299
|
+
stdio: "pipe", timeout: 5000,
|
|
300
|
+
});
|
|
301
|
+
defaultBranch = candidate;
|
|
302
|
+
break;
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!defaultBranch) return; // no default branch found — skip
|
|
307
|
+
|
|
308
|
+
// Create develop from default branch and push
|
|
309
|
+
execFileSync("git", ["-C", repoDir, "branch", "develop", `origin/${defaultBranch}`], {
|
|
310
|
+
stdio: "pipe", encoding: "utf-8", timeout: 5000,
|
|
311
|
+
});
|
|
312
|
+
execFileSync("git", ["-C", repoDir, "push", "-u", "origin", "develop"], {
|
|
313
|
+
stdio: "pipe", encoding: "utf-8", timeout: 30000,
|
|
314
|
+
});
|
|
315
|
+
} catch {
|
|
316
|
+
// Non-fatal — session-start.sh will retry on first launch
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
277
320
|
function embedToken(url, token) {
|
|
278
321
|
try {
|
|
279
322
|
return url.replace("https://github.com/", `https://x-access-token:${token}@github.com/`);
|
|
@@ -282,6 +325,37 @@ function embedToken(url, token) {
|
|
|
282
325
|
}
|
|
283
326
|
}
|
|
284
327
|
|
|
328
|
+
function toSSH(url) {
|
|
329
|
+
const m = url.match(/https:\/\/github\.com\/(.+?)(?:\.git)?$/);
|
|
330
|
+
return m ? `git@github.com:${m[1]}.git` : null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Clone a repo, trying SSH first then HTTPS (no token embedded).
|
|
335
|
+
* The repo:invite-scoped OAuth token can't clone — we rely on the
|
|
336
|
+
* user's own git credentials (SSH keys / gh CLI / credential helper).
|
|
337
|
+
*/
|
|
338
|
+
function cloneRepo(httpsUrl, dir, timeout = 60000) {
|
|
339
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
340
|
+
const sshUrl = toSSH(httpsUrl);
|
|
341
|
+
|
|
342
|
+
if (sshUrl) {
|
|
343
|
+
try {
|
|
344
|
+
execFileSync("git", ["clone", sshUrl, dir], { stdio: "pipe", encoding: "utf-8", timeout, env });
|
|
345
|
+
return true;
|
|
346
|
+
} catch {
|
|
347
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
execFileSync("git", ["clone", httpsUrl, dir], { stdio: "pipe", encoding: "utf-8", timeout, env });
|
|
353
|
+
return true;
|
|
354
|
+
} catch {}
|
|
355
|
+
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
285
359
|
function configureGitCredentials(token) {
|
|
286
360
|
try {
|
|
287
361
|
// Use --global since this runs before any repos are cloned (no local .git exists)
|
|
@@ -415,5 +489,6 @@ module.exports = {
|
|
|
415
489
|
embedToken,
|
|
416
490
|
configureGitCredentials,
|
|
417
491
|
initMemoryDirs,
|
|
492
|
+
ensureDevelop,
|
|
418
493
|
run,
|
|
419
494
|
};
|