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.
- package/lib/auth.js +12 -5
- package/lib/local.js +58 -103
- 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
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
585
|
+
// 3. Accept pending invitations
|
|
593
586
|
console.log("");
|
|
594
|
-
const s1 = ui.spinner("
|
|
587
|
+
const s1 = ui.spinner("Accepting invitations...");
|
|
595
588
|
await acceptPendingInvitations(githubToken, orgLogin, ui);
|
|
596
|
-
s1.stop("
|
|
589
|
+
s1.stop("Invitations accepted");
|
|
597
590
|
|
|
598
|
-
// 4.
|
|
599
|
-
|
|
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
|
-
//
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
//
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
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(
|
|
624
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
635
625
|
} catch {
|
|
636
|
-
|
|
626
|
+
ui.error("Invalid egregore.json");
|
|
637
627
|
process.exit(1);
|
|
638
628
|
}
|
|
639
|
-
|
|
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
|
-
//
|
|
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 =
|
|
682
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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
|
-
//
|
|
693
|
-
ui.step(
|
|
655
|
+
// Symlink
|
|
656
|
+
ui.step(2, totalSteps, "Linking memory...");
|
|
694
657
|
createSymlink(egregoreDir, memoryDir, ui);
|
|
695
658
|
|
|
696
|
-
//
|
|
697
|
-
ui.step(
|
|
698
|
-
fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${
|
|
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
|
-
//
|
|
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
|
-
//
|
|
719
|
-
ui.step(
|
|
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
|
-
//
|
|
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(
|
|
689
|
+
ui.step(5 + i, totalSteps, `Cloning ${mr}...`);
|
|
727
690
|
const repoDir = path.join(base, mr);
|
|
728
691
|
try {
|
|
729
|
-
|
|
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
|
-
|
|
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
|
|