create-egregore 0.14.2 → 0.15.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.
Files changed (3) hide show
  1. package/lib/auth.js +12 -5
  2. package/lib/local.js +58 -103
  3. package/package.json +1 -1
package/lib/auth.js CHANGED
@@ -5,7 +5,8 @@
5
5
 
6
6
  const https = require("node:https");
7
7
 
8
- const CLIENT_ID = "Iv23li2obNsAjakoK2RE"; // GitHub App — clean consent, permissions via installation
8
+ const GITHUB_APP_CLIENT_ID = "Iv23li2obNsAjakoK2RE"; // GitHub App — founder flow (clean install screen)
9
+ const OAUTH_APP_CLIENT_ID = "Ov23liYZjJbbbNgnJ9vH"; // OAuth App — join flow (repo:invite only)
9
10
 
10
11
  function post(url, body) {
11
12
  return new Promise((resolve, reject) => {
@@ -72,12 +73,18 @@ function copyToClipboard(text) {
72
73
  /**
73
74
  * Run GitHub Device Flow.
74
75
  * Returns the access token string, or throws on failure.
76
+ * @param {object} [opts] - Options
77
+ * @param {string} [opts.mode] - "join" uses OAuth App with repo:invite scope
75
78
  */
76
- async function deviceFlow(ui) {
79
+ async function deviceFlow(ui, opts = {}) {
80
+ const isJoin = opts.mode === "join";
81
+ const clientId = isJoin ? OAUTH_APP_CLIENT_ID : GITHUB_APP_CLIENT_ID;
82
+ const scope = isJoin ? "repo:invite" : "";
83
+
77
84
  // 1. Request device code
78
85
  const codeResp = await post("https://github.com/login/device/code", {
79
- client_id: CLIENT_ID,
80
- scope: "",
86
+ client_id: clientId,
87
+ scope,
81
88
  });
82
89
 
83
90
  if (!codeResp.device_code) {
@@ -111,7 +118,7 @@ async function deviceFlow(ui) {
111
118
  await sleep(interval);
112
119
 
113
120
  const tokenResp = await post("https://github.com/login/oauth/access_token", {
114
- client_id: CLIENT_ID,
121
+ client_id: clientId,
115
122
  device_code,
116
123
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
117
124
  });
package/lib/local.js CHANGED
@@ -568,18 +568,11 @@ async function localJoinFlow(orgArg, ui) {
568
568
  explicitRepo = null;
569
569
  }
570
570
 
571
- // 1. Install GitHub App — needed for the token to access repository resources
571
+ // 1. GitHub auth — OAuth with repo:invite scope (narrow: just invitation acceptance)
572
572
  ui.info(`Joining ${ui.bold(orgLogin)}. First, sign in with GitHub.\n`);
573
- ui.info("Install the Egregore app on your GitHub account.");
574
- ui.info("This lets Egregore access the repos you've been invited to.\n");
575
- openBrowser(APP_INSTALL_URL);
576
- await ui.prompt("Press Enter when done...");
577
-
578
- // 2. GitHub auth (auto-approved after App install)
579
- ui.info("");
580
573
  let githubToken;
581
574
  try {
582
- githubToken = await deviceFlow(ui);
575
+ githubToken = await deviceFlow(ui, { mode: "join" });
583
576
  ui.success("Authenticated with GitHub");
584
577
  } catch (err) {
585
578
  ui.error(`GitHub auth failed: ${err.message}`);
@@ -589,54 +582,51 @@ async function localJoinFlow(orgArg, ui) {
589
582
  // 2. Get user info
590
583
  const user = await getUser(githubToken);
591
584
 
592
- // 3. Accept pending invitations (may silently fail with GitHub App tokens)
585
+ // 3. Accept pending invitations
593
586
  console.log("");
594
- const s1 = ui.spinner("Checking for invitations...");
587
+ const s1 = ui.spinner("Accepting invitations...");
595
588
  await acceptPendingInvitations(githubToken, orgLogin, ui);
596
- s1.stop("Checked invitations");
589
+ s1.stop("Invitations accepted");
597
590
 
598
- // 4. Find egregore repo
599
- let repoName = await findEgregoreRepo(githubToken, orgLogin, explicitRepo);
591
+ // 4. Determine repo name
592
+ const repoName = explicitRepo || "egregore";
593
+ if (!repoName) {
594
+ ui.error("Usage: npx create-egregore join <org>/<repo>");
595
+ process.exit(1);
596
+ }
600
597
 
601
- // 4b. If repo not found, the user likely has a pending invitation that couldn't
602
- // be accepted automatically. Open the repo page so they can accept with one click.
603
- if (!repoName && explicitRepo) {
604
- console.log("");
605
- ui.info("Accept the repo invitation in your browser.");
606
- openBrowser(`https://github.com/${orgLogin}/${explicitRepo}`);
607
- await ui.prompt("Press Enter after accepting...");
608
-
609
- const s2 = ui.spinner("Retrying...");
610
- repoName = await findEgregoreRepo(githubToken, orgLogin, explicitRepo);
611
- if (!repoName) {
612
- s2.fail(`Still can't access ${orgLogin}/${explicitRepo}`);
613
- ui.error("Make sure the org admin has set up Egregore and invited you.");
614
- process.exit(1);
615
- }
616
- s2.stop("Access confirmed");
617
- } else if (!repoName) {
618
- ui.error(`No egregore repo found in ${orgLogin}`);
619
- ui.error("Make sure the org admin has set up Egregore and invited you.");
620
- ui.info("If the repo has a custom name, use: npx create-egregore join <org>/<repo>");
598
+ // 5. Clone repos using git credentials (SSH keys / gh CLI / credential helper)
599
+ // The OAuth token only has repo:invite scope — cloning uses the joiner's own git setup.
600
+ const base = process.cwd();
601
+ const egregoreDir = path.join(base, repoName);
602
+
603
+ console.log("");
604
+ const s2 = ui.spinner(`Cloning ${orgLogin}/${repoName}...`);
605
+ try {
606
+ execFileSync("git", ["clone", `https://github.com/${orgLogin}/${repoName}.git`, egregoreDir], { stdio: "pipe", encoding: "utf-8", timeout: 60000 });
607
+ s2.stop(`Cloned ${repoName}`);
608
+ } catch (err) {
609
+ s2.fail(`Could not clone ${orgLogin}/${repoName}`);
610
+ ui.error("Make sure you have git credentials set up (SSH keys or gh auth login).");
611
+ ui.error("Also verify the org admin has set up Egregore and invited you.");
621
612
  process.exit(1);
622
613
  }
614
+ setGitIdentity(egregoreDir, user);
623
615
 
624
- // 5. Read egregore.json from remote
625
- const s3 = ui.spinner(`Reading config from ${orgLogin}/${repoName}...`);
626
- const configRaw = await getFileContent(githubToken, orgLogin, repoName, "egregore.json");
627
- if (!configRaw) {
628
- s3.fail("Could not read egregore.json");
629
- ui.error("The egregore repo exists but has no egregore.json. Ask the admin to set up first.");
616
+ // 6. Read egregore.json from local clone
617
+ const configPath = path.join(egregoreDir, "egregore.json");
618
+ if (!fs.existsSync(configPath)) {
619
+ ui.error("The repo has no egregore.json. Ask the admin to set up first.");
630
620
  process.exit(1);
631
621
  }
632
622
  let config;
633
623
  try {
634
- config = JSON.parse(configRaw);
624
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
635
625
  } catch {
636
- s3.fail("Invalid egregore.json");
626
+ ui.error("Invalid egregore.json");
637
627
  process.exit(1);
638
628
  }
639
- s3.stop(`Found ${ui.bold(config.org_name || orgLogin)}`);
629
+ ui.success(`Found ${ui.bold(config.org_name || orgLogin)}`);
640
630
 
641
631
  const orgName = config.org_name || orgLogin;
642
632
  const memoryRepoName = config.memory_repo
@@ -644,61 +634,34 @@ async function localJoinFlow(orgArg, ui) {
644
634
  : `${orgLogin}-memory`;
645
635
  const managedRepos = config.repos || [];
646
636
 
647
- // 5b. Check access to ALL repos — open browser for any that need invitation acceptance
648
- const allRepos = [repoName, memoryRepoName, ...managedRepos];
649
- const inaccessible = [];
650
- for (const repo of allRepos) {
651
- if (!(await repoExists(githubToken, orgLogin, repo))) {
652
- inaccessible.push(repo);
653
- }
654
- }
655
- if (inaccessible.length > 0) {
656
- console.log("");
657
- ui.info(`Accept ${inaccessible.length === 1 ? "the invitation" : `${inaccessible.length} invitations`} in your browser.`);
658
- for (const repo of inaccessible) {
659
- openBrowser(`https://github.com/${orgLogin}/${repo}`);
660
- }
661
- await ui.prompt("Press Enter after accepting...");
662
-
663
- // Retry — only block on main + memory, managed repos are best-effort
664
- for (const repo of [repoName, memoryRepoName]) {
665
- if (inaccessible.includes(repo) && !(await repoExists(githubToken, orgLogin, repo))) {
666
- ui.error(`Still can't access ${orgLogin}/${repo}`);
667
- ui.error("Make sure the org admin has set up Egregore and invited you.");
668
- process.exit(1);
669
- }
670
- }
671
- ui.success("Access confirmed");
672
- }
673
-
674
- // 6. Clone locally
675
- const base = process.cwd();
676
- const forkUrl = `https://github.com/${orgLogin}/${repoName}.git`;
637
+ // 7. Clone memory + managed repos
677
638
  const memoryUrl = config.memory_repo || `https://github.com/${orgLogin}/${memoryRepoName}.git`;
678
- const egregoreDir = path.join(base, repoName);
679
639
  const memoryDir = path.join(base, memoryRepoName);
680
640
 
681
- const totalSteps = 5 + managedRepos.length;
682
- configureGitCredentials(githubToken);
683
-
684
- ui.step(1, totalSteps, "Cloning egregore...");
685
- cloneRepo(forkUrl, egregoreDir, githubToken, ui, "egregore");
686
- setGitIdentity(egregoreDir, user);
641
+ const totalSteps = 4 + managedRepos.length;
642
+ // Store the invite token for later use (invitation acceptance, not cloning)
643
+ const inviteToken = githubToken;
687
644
 
688
- ui.step(2, totalSteps, "Cloning shared memory...");
689
- cloneRepo(memoryUrl, memoryDir, githubToken, ui, "memory");
645
+ // Clone memory using git credentials (same as main repo — no token in URL)
646
+ ui.step(1, totalSteps, "Cloning shared memory...");
647
+ try {
648
+ execFileSync("git", ["clone", memoryUrl, memoryDir], { stdio: "pipe", encoding: "utf-8", timeout: 60000 });
649
+ ui.success("Cloned memory");
650
+ } catch {
651
+ ui.warn("Could not clone memory repo — you may not have access yet.");
652
+ }
690
653
  setGitIdentity(memoryDir, user);
691
654
 
692
- // 7. Symlink
693
- ui.step(3, totalSteps, "Linking memory...");
655
+ // Symlink
656
+ ui.step(2, totalSteps, "Linking memory...");
694
657
  createSymlink(egregoreDir, memoryDir, ui);
695
658
 
696
- // 8. Write .env
697
- ui.step(4, totalSteps, "Writing credentials...");
698
- fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${githubToken}\n`, { mode: 0o600 });
659
+ // Write .env — store invite token for session-start invitation checks
660
+ ui.step(3, totalSteps, "Writing credentials...");
661
+ fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${inviteToken}\n`, { mode: 0o600 });
699
662
  ui.success("Credentials saved");
700
663
 
701
- // 9. "What should we call you?" + write state
664
+ // "What should we call you?" + write state
702
665
  console.log("");
703
666
  const displayName = await ui.prompt(`What should we call you? [${user.login}]:`);
704
667
 
@@ -715,30 +678,22 @@ async function localJoinFlow(orgArg, ui) {
715
678
  };
716
679
  fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
717
680
 
718
- // 10. Register + alias
719
- ui.step(5, totalSteps, "Registering instance...");
681
+ // Register + alias
682
+ ui.step(4, totalSteps, "Registering instance...");
720
683
  registerInstance(repoName, orgName, egregoreDir);
721
684
  const alias = await installShellAlias(egregoreDir, ui);
722
685
 
723
- // 11. Clone managed repos (retry once — git servers may lag behind invitation acceptance)
686
+ // Clone managed repos using git credentials
724
687
  for (let i = 0; i < managedRepos.length; i++) {
725
688
  const mr = managedRepos[i];
726
- ui.step(6 + i, totalSteps, `Cloning ${mr}...`);
689
+ ui.step(5 + i, totalSteps, `Cloning ${mr}...`);
727
690
  const repoDir = path.join(base, mr);
728
691
  try {
729
- const repoUrl = `https://github.com/${orgLogin}/${mr}.git`;
730
- cloneRepo(repoUrl, repoDir, githubToken, ui, mr);
692
+ execFileSync("git", ["clone", `https://github.com/${orgLogin}/${mr}.git`, repoDir], { stdio: "pipe", encoding: "utf-8", timeout: 60000 });
731
693
  setGitIdentity(repoDir, user);
694
+ ui.success(`Cloned ${mr}`);
732
695
  } catch {
733
- // Retry after a short delay — invitation acceptance may not have propagated
734
- await new Promise((r) => setTimeout(r, 3000));
735
- try {
736
- const repoUrl = `https://github.com/${orgLogin}/${mr}.git`;
737
- cloneRepo(repoUrl, repoDir, githubToken, ui, mr);
738
- setGitIdentity(repoDir, user);
739
- } catch {
740
- ui.warn(`Could not clone ${mr} — you may not have access yet.`);
741
- }
696
+ ui.warn(`Could not clone ${mr} — you may not have access yet.`);
742
697
  }
743
698
  }
744
699
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "Set up Egregore for your team in one command",
5
5
  "license": "MIT",
6
6
  "bin": {