create-egregore 0.14.2 → 0.15.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/auth.js +12 -5
- package/lib/local.js +94 -105
- 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
|
@@ -168,6 +168,43 @@ function cloneRepo(url, dir, token, ui, label) {
|
|
|
168
168
|
ui.success(`Cloned ${label}`);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Convert HTTPS GitHub URL to SSH format.
|
|
173
|
+
* https://github.com/org/repo.git → git@github.com:org/repo.git
|
|
174
|
+
*/
|
|
175
|
+
function toSSH(url) {
|
|
176
|
+
const m = url.match(/https:\/\/github\.com\/(.+?)(?:\.git)?$/);
|
|
177
|
+
return m ? `git@github.com:${m[1]}.git` : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Clone a repo, trying SSH first then HTTPS.
|
|
182
|
+
* GIT_TERMINAL_PROMPT=0 prevents the hanging username/password prompt.
|
|
183
|
+
*/
|
|
184
|
+
function cloneWithFallback(httpsUrl, dir, timeout = 60000) {
|
|
185
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
186
|
+
const sshUrl = toSSH(httpsUrl);
|
|
187
|
+
|
|
188
|
+
// Try SSH first (most devs have SSH keys; GitHub no longer supports HTTPS password auth)
|
|
189
|
+
if (sshUrl) {
|
|
190
|
+
try {
|
|
191
|
+
execFileSync("git", ["clone", sshUrl, dir], { stdio: "pipe", encoding: "utf-8", timeout, env });
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
// Clean up partial clone so HTTPS fallback can use the same dir
|
|
195
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Fall back to HTTPS (works if user has gh CLI or a credential helper configured)
|
|
200
|
+
try {
|
|
201
|
+
execFileSync("git", ["clone", httpsUrl, dir], { stdio: "pipe", encoding: "utf-8", timeout, env });
|
|
202
|
+
return true;
|
|
203
|
+
} catch {}
|
|
204
|
+
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
171
208
|
function setGitIdentity(dir, user) {
|
|
172
209
|
try {
|
|
173
210
|
run(`git config user.name "${user.name}"`, { cwd: dir });
|
|
@@ -568,18 +605,11 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
568
605
|
explicitRepo = null;
|
|
569
606
|
}
|
|
570
607
|
|
|
571
|
-
// 1.
|
|
608
|
+
// 1. GitHub auth — OAuth with repo:invite scope (narrow: just invitation acceptance)
|
|
572
609
|
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
610
|
let githubToken;
|
|
581
611
|
try {
|
|
582
|
-
githubToken = await deviceFlow(ui);
|
|
612
|
+
githubToken = await deviceFlow(ui, { mode: "join" });
|
|
583
613
|
ui.success("Authenticated with GitHub");
|
|
584
614
|
} catch (err) {
|
|
585
615
|
ui.error(`GitHub auth failed: ${err.message}`);
|
|
@@ -589,54 +619,50 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
589
619
|
// 2. Get user info
|
|
590
620
|
const user = await getUser(githubToken);
|
|
591
621
|
|
|
592
|
-
// 3. Accept pending invitations
|
|
622
|
+
// 3. Accept pending invitations
|
|
593
623
|
console.log("");
|
|
594
|
-
const s1 = ui.spinner("
|
|
624
|
+
const s1 = ui.spinner("Accepting invitations...");
|
|
595
625
|
await acceptPendingInvitations(githubToken, orgLogin, ui);
|
|
596
|
-
s1.stop("
|
|
626
|
+
s1.stop("Invitations accepted");
|
|
627
|
+
|
|
628
|
+
// 4. Determine repo name
|
|
629
|
+
const repoName = explicitRepo || "egregore";
|
|
630
|
+
if (!repoName) {
|
|
631
|
+
ui.error("Usage: npx create-egregore join <org>/<repo>");
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
597
634
|
|
|
598
|
-
//
|
|
599
|
-
|
|
635
|
+
// 5. Clone repos using git credentials (SSH keys / gh CLI / credential helper)
|
|
636
|
+
// The OAuth token only has repo:invite scope — cloning uses the joiner's own git setup.
|
|
637
|
+
const base = process.cwd();
|
|
638
|
+
const egregoreDir = path.join(base, repoName);
|
|
600
639
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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>");
|
|
640
|
+
console.log("");
|
|
641
|
+
const s2 = ui.spinner(`Cloning ${orgLogin}/${repoName}...`);
|
|
642
|
+
if (cloneWithFallback(`https://github.com/${orgLogin}/${repoName}.git`, egregoreDir)) {
|
|
643
|
+
s2.stop(`Cloned ${repoName}`);
|
|
644
|
+
} else {
|
|
645
|
+
s2.fail(`Could not clone ${orgLogin}/${repoName}`);
|
|
646
|
+
ui.error("Make sure you have git credentials set up (SSH keys or gh auth login).");
|
|
647
|
+
ui.error("Also verify the org admin has set up Egregore and invited you.");
|
|
621
648
|
process.exit(1);
|
|
622
649
|
}
|
|
650
|
+
setGitIdentity(egregoreDir, user);
|
|
623
651
|
|
|
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.");
|
|
652
|
+
// 6. Read egregore.json from local clone
|
|
653
|
+
const configPath = path.join(egregoreDir, "egregore.json");
|
|
654
|
+
if (!fs.existsSync(configPath)) {
|
|
655
|
+
ui.error("The repo has no egregore.json. Ask the admin to set up first.");
|
|
630
656
|
process.exit(1);
|
|
631
657
|
}
|
|
632
658
|
let config;
|
|
633
659
|
try {
|
|
634
|
-
config = JSON.parse(
|
|
660
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
635
661
|
} catch {
|
|
636
|
-
|
|
662
|
+
ui.error("Invalid egregore.json");
|
|
637
663
|
process.exit(1);
|
|
638
664
|
}
|
|
639
|
-
|
|
665
|
+
ui.success(`Found ${ui.bold(config.org_name || orgLogin)}`);
|
|
640
666
|
|
|
641
667
|
const orgName = config.org_name || orgLogin;
|
|
642
668
|
const memoryRepoName = config.memory_repo
|
|
@@ -644,61 +670,33 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
644
670
|
: `${orgLogin}-memory`;
|
|
645
671
|
const managedRepos = config.repos || [];
|
|
646
672
|
|
|
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`;
|
|
673
|
+
// 7. Clone memory + managed repos
|
|
677
674
|
const memoryUrl = config.memory_repo || `https://github.com/${orgLogin}/${memoryRepoName}.git`;
|
|
678
|
-
const egregoreDir = path.join(base, repoName);
|
|
679
675
|
const memoryDir = path.join(base, memoryRepoName);
|
|
680
676
|
|
|
681
|
-
const totalSteps =
|
|
682
|
-
|
|
677
|
+
const totalSteps = 4 + managedRepos.length;
|
|
678
|
+
// Store the invite token for later use (invitation acceptance, not cloning)
|
|
679
|
+
const inviteToken = githubToken;
|
|
683
680
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
681
|
+
// Clone memory using SSH-first fallback (same as main repo)
|
|
682
|
+
ui.step(1, totalSteps, "Cloning shared memory...");
|
|
683
|
+
if (cloneWithFallback(memoryUrl, memoryDir)) {
|
|
684
|
+
ui.success("Cloned memory");
|
|
685
|
+
} else {
|
|
686
|
+
ui.warn("Could not clone memory repo — you may not have access yet.");
|
|
687
|
+
}
|
|
690
688
|
setGitIdentity(memoryDir, user);
|
|
691
689
|
|
|
692
|
-
//
|
|
693
|
-
ui.step(
|
|
690
|
+
// Symlink
|
|
691
|
+
ui.step(2, totalSteps, "Linking memory...");
|
|
694
692
|
createSymlink(egregoreDir, memoryDir, ui);
|
|
695
693
|
|
|
696
|
-
//
|
|
697
|
-
ui.step(
|
|
698
|
-
fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${
|
|
694
|
+
// Write .env — store invite token for session-start invitation checks
|
|
695
|
+
ui.step(3, totalSteps, "Writing credentials...");
|
|
696
|
+
fs.writeFileSync(path.join(egregoreDir, ".env"), `GITHUB_TOKEN=${inviteToken}\n`, { mode: 0o600 });
|
|
699
697
|
ui.success("Credentials saved");
|
|
700
698
|
|
|
701
|
-
//
|
|
699
|
+
// "What should we call you?" + write state
|
|
702
700
|
console.log("");
|
|
703
701
|
const displayName = await ui.prompt(`What should we call you? [${user.login}]:`);
|
|
704
702
|
|
|
@@ -715,30 +713,21 @@ async function localJoinFlow(orgArg, ui) {
|
|
|
715
713
|
};
|
|
716
714
|
fs.writeFileSync(path.join(egregoreDir, ".egregore-state.json"), JSON.stringify(state, null, 2) + "\n");
|
|
717
715
|
|
|
718
|
-
//
|
|
719
|
-
ui.step(
|
|
716
|
+
// Register + alias
|
|
717
|
+
ui.step(4, totalSteps, "Registering instance...");
|
|
720
718
|
registerInstance(repoName, orgName, egregoreDir);
|
|
721
719
|
const alias = await installShellAlias(egregoreDir, ui);
|
|
722
720
|
|
|
723
|
-
//
|
|
721
|
+
// Clone managed repos using SSH-first fallback
|
|
724
722
|
for (let i = 0; i < managedRepos.length; i++) {
|
|
725
723
|
const mr = managedRepos[i];
|
|
726
|
-
ui.step(
|
|
724
|
+
ui.step(5 + i, totalSteps, `Cloning ${mr}...`);
|
|
727
725
|
const repoDir = path.join(base, mr);
|
|
728
|
-
|
|
729
|
-
const repoUrl = `https://github.com/${orgLogin}/${mr}.git`;
|
|
730
|
-
cloneRepo(repoUrl, repoDir, githubToken, ui, mr);
|
|
726
|
+
if (cloneWithFallback(`https://github.com/${orgLogin}/${mr}.git`, repoDir)) {
|
|
731
727
|
setGitIdentity(repoDir, user);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
}
|
|
728
|
+
ui.success(`Cloned ${mr}`);
|
|
729
|
+
} else {
|
|
730
|
+
ui.warn(`Could not clone ${mr} — you may not have access yet.`);
|
|
742
731
|
}
|
|
743
732
|
}
|
|
744
733
|
|