create-egregore 0.21.0-runtime-mvp.26 → 0.21.0-runtime-mvp.27
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/assets/egregore-launcher.js +2 -2
- package/bin/cli.js +11 -66
- package/lib/api.js +6 -2
- package/lib/auth.js +1 -1
- package/lib/install-access.js +87 -0
- package/lib/setup.js +81 -7
- package/lib/token-install.js +65 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.21.0-runtime-mvp.
|
|
2
|
+
// egregore-launcher-version: 0.21.0-runtime-mvp.27
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -35,7 +35,7 @@ const crypto = require("node:crypto");
|
|
|
35
35
|
const { spawnSync, spawn } = require("node:child_process");
|
|
36
36
|
const readline = require("node:readline");
|
|
37
37
|
|
|
38
|
-
const VERSION = "0.21.0-runtime-mvp.
|
|
38
|
+
const VERSION = "0.21.0-runtime-mvp.27";
|
|
39
39
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
40
40
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
41
41
|
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
package/bin/cli.js
CHANGED
|
@@ -559,75 +559,20 @@ async function main() {
|
|
|
559
559
|
}
|
|
560
560
|
|
|
561
561
|
async function tokenFlow(api, token, ctx) {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
ui.error(err.message);
|
|
570
|
-
ui.info("");
|
|
571
|
-
ui.info("The token may have expired or already been used.");
|
|
572
|
-
ui.info("Visit egregore.xyz to get a new one, or run without --token:");
|
|
573
|
-
ui.info(" npx -y create-egregore@latest");
|
|
574
|
-
process.exit(1);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Joiner tokens don't include a github_token — try gh CLI first. If gh is
|
|
578
|
-
// installed but logged out, run gh auth login instead of device flow.
|
|
579
|
-
if (!data.github_token || data.needs_cli_auth) {
|
|
580
|
-
const ghAuth = tryGhAuthOrLogin(ui, data.needs_cli_auth ? { mode: "join" } : {});
|
|
581
|
-
if (ghAuth) {
|
|
582
|
-
ui.success(`Using your ${ui.bold("gh")} CLI auth${ghAuth.user ? ` (${ghAuth.user})` : ""}`);
|
|
583
|
-
data.github_token = ghAuth.token;
|
|
584
|
-
} else if (ghCliExists()) {
|
|
585
|
-
ui.error("GitHub CLI auth did not complete.");
|
|
586
|
-
ui.info(`Run ${ui.bold("gh auth login")} and then re-run this command.`);
|
|
587
|
-
process.exit(1);
|
|
588
|
-
} else {
|
|
562
|
+
const { installFromToken } = require("../lib/token-install");
|
|
563
|
+
await installFromToken(api, token, ctx, {
|
|
564
|
+
ui, install, request: ghApi,
|
|
565
|
+
authenticate: async (forceLogin) => {
|
|
566
|
+
const auth = tryGhAuthOrLogin(ui, { mode: "join", requireRepoScope: true, forceLogin });
|
|
567
|
+
if (auth) return auth;
|
|
568
|
+
if (ghCliExists()) return null;
|
|
589
569
|
if (!canUseInteractiveBrowserFlow()) {
|
|
590
570
|
explainNoGhHeadless(ui);
|
|
591
|
-
|
|
571
|
+
return null;
|
|
592
572
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
ui.success("Authenticated with GitHub");
|
|
597
|
-
} catch (err) {
|
|
598
|
-
ui.error(`GitHub auth failed: ${err.message}`);
|
|
599
|
-
process.exit(1);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Token-mode setups already have their repo list baked in server-side. If
|
|
605
|
-
// the user's current working repo happens to match a managed-repo name, skip
|
|
606
|
-
// the clone/pull/configure step for it so we don't clobber uncommitted work.
|
|
607
|
-
const skipRepos = new Set();
|
|
608
|
-
if (ctx.prefilledRepo && Array.isArray(data.repos)) {
|
|
609
|
-
for (const r of data.repos) {
|
|
610
|
-
const n = typeof r === "string" ? r : r && r.name;
|
|
611
|
-
if (n && n === ctx.prefilledRepo.name) skipRepos.add(n);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
try {
|
|
615
|
-
await install(data, ui, ctx.targetDir, {
|
|
616
|
-
skipRepos,
|
|
617
|
-
noAutolaunch: ctx.noAutolaunch,
|
|
618
|
-
noScreens: ctx.noScreens,
|
|
619
|
-
flowVariant: ctx.flowVariant,
|
|
620
|
-
runtime: ctx.runtime,
|
|
621
|
-
});
|
|
622
|
-
if (ctx.prefilledRepo && skipRepos.size === 0) {
|
|
623
|
-
console.log("");
|
|
624
|
-
ui.info(`Tip: add your current repo to managed_repos with:`);
|
|
625
|
-
ui.info(` /add ${ctx.prefilledRepo.toplevel || `../${ctx.prefilledRepo.localName}`}`);
|
|
626
|
-
}
|
|
627
|
-
} catch (err) {
|
|
628
|
-
ui.error(`Install failed: ${err.message}`);
|
|
629
|
-
process.exit(1);
|
|
630
|
-
}
|
|
573
|
+
return { token: await deviceFlow(ui, { mode: "join", scope: "repo" }) };
|
|
574
|
+
},
|
|
575
|
+
});
|
|
631
576
|
}
|
|
632
577
|
|
|
633
578
|
async function interactiveFlow(api, ctx) {
|
package/lib/api.js
CHANGED
|
@@ -63,8 +63,12 @@ class EgregoreAPI {
|
|
|
63
63
|
this.base = (baseUrl || DEFAULT_API).replace(/\/$/, "");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
async claimToken(token) {
|
|
67
|
-
|
|
66
|
+
async claimToken(token, githubToken) {
|
|
67
|
+
// Old APIs reject POST without consuming a one-time setup token. Never
|
|
68
|
+
// silently downgrade an authenticated retry to the legacy GET protocol.
|
|
69
|
+
return request(githubToken ? "POST" : "GET", `${this.base}/api/org/claim/${token}`, {
|
|
70
|
+
headers: githubToken ? { Authorization: `Bearer ${githubToken}` } : {},
|
|
71
|
+
});
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
async getOrgs(githubToken) {
|
package/lib/auth.js
CHANGED
|
@@ -212,7 +212,7 @@ function runGhAuthLogin(ui, opts = {}) {
|
|
|
212
212
|
|
|
213
213
|
function tryGhAuthOrLogin(ui, opts = {}) {
|
|
214
214
|
const existing = tryGhAuth();
|
|
215
|
-
if (existing) return existing;
|
|
215
|
+
if (existing && !opts.forceLogin && !(opts.requireRepoScope && existing.hasRepoScope === false)) return existing;
|
|
216
216
|
if (!ghCliExists()) return null;
|
|
217
217
|
return runGhAuthLogin(ui, opts);
|
|
218
218
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Repository access for a connected install. Never accept unrelated invitations. */
|
|
2
|
+
class RepositoryAccessError extends Error {
|
|
3
|
+
constructor(message, canReauthenticate = false) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "RepositoryAccessError";
|
|
6
|
+
this.canReauthenticate = canReauthenticate;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function repositoryFromUrl(value) {
|
|
11
|
+
const url = new URL(value);
|
|
12
|
+
const name = url.pathname.replace(/^\//, "").replace(/\.git$/, "");
|
|
13
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password ||
|
|
14
|
+
url.search || url.hash || !/^[\w.-]+\/[\w.-]+$/.test(name)) {
|
|
15
|
+
throw new Error("Setup contains an invalid GitHub repository URL.");
|
|
16
|
+
}
|
|
17
|
+
return name;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function requiredRepositories(data) {
|
|
21
|
+
const names = [repositoryFromUrl(data.fork_url), repositoryFromUrl(data.memory_url)];
|
|
22
|
+
for (const entry of data.repos || []) {
|
|
23
|
+
const name = typeof entry === "string" ? entry : entry?.name;
|
|
24
|
+
if (!/^[\w.-]+$/.test(name || "") || name === "." || name === "..") {
|
|
25
|
+
throw new Error("Setup contains an invalid managed repository name.");
|
|
26
|
+
}
|
|
27
|
+
names.push(repositoryFromUrl(`https://github.com/${data.github_org}/${name}`));
|
|
28
|
+
}
|
|
29
|
+
return [...new Map(names.map(name => [name.toLowerCase(), name])).values()];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matchesIdentity(data, user) {
|
|
33
|
+
// IDs survive account renames. Older payloads can only be checked by login.
|
|
34
|
+
if (data.github_id != null) return String(data.github_id) === String(user?.id);
|
|
35
|
+
return !!data.github_username && data.github_username.toLowerCase() === user?.login?.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ensureRepositoryAccess(data, ui, { request, sleep = ms => new Promise(r => setTimeout(r, ms)) }) {
|
|
39
|
+
const token = data.github_token;
|
|
40
|
+
if (!token) throw new RepositoryAccessError("GitHub authentication is required for this installation.", true);
|
|
41
|
+
const who = await request("GET", "/user", token);
|
|
42
|
+
if (who.status !== 200) throw new RepositoryAccessError(`GitHub identity check failed (HTTP ${who.status}).`, who.status === 401 || who.status === 403);
|
|
43
|
+
if (!matchesIdentity(data, who.data)) throw new RepositoryAccessError("Your active GitHub account does not match the account that accepted this invitation. Sign in to that account and retry.");
|
|
44
|
+
|
|
45
|
+
const required = requiredRepositories(data);
|
|
46
|
+
async function missingRepos() {
|
|
47
|
+
const missing = [];
|
|
48
|
+
for (const name of required) {
|
|
49
|
+
const result = await request("GET", `/repos/${name}`, token);
|
|
50
|
+
if (result.status === 200 && result.data?.permissions?.pull !== false) continue;
|
|
51
|
+
if (![401, 403, 404, 200].includes(result.status)) {
|
|
52
|
+
throw new RepositoryAccessError(`GitHub access check for ${name} failed (HTTP ${result.status}). Retry the same install command.`);
|
|
53
|
+
}
|
|
54
|
+
missing.push(name);
|
|
55
|
+
}
|
|
56
|
+
return missing;
|
|
57
|
+
}
|
|
58
|
+
let missing = await missingRepos();
|
|
59
|
+
if (!missing.length) return required;
|
|
60
|
+
const wanted = new Set(missing.map(name => name.toLowerCase()));
|
|
61
|
+
// Collect before accepting: removing invitations while paginating would skip entries.
|
|
62
|
+
const invitations = [];
|
|
63
|
+
for (let page = 1; ; page++) {
|
|
64
|
+
const result = await request("GET", `/user/repository_invitations?per_page=100&page=${page}`, token);
|
|
65
|
+
if (result.status !== 200 || !Array.isArray(result.data)) {
|
|
66
|
+
throw new RepositoryAccessError(`Could not list GitHub repository invitations (HTTP ${result.status}); access is missing for ${missing.join(", ")}.`, [401, 403].includes(result.status));
|
|
67
|
+
}
|
|
68
|
+
invitations.push(...result.data.filter(inv => wanted.has(inv.repository?.full_name?.toLowerCase())));
|
|
69
|
+
if (result.data.length < 100) break;
|
|
70
|
+
if (page === 100) throw new RepositoryAccessError("GitHub returned too many invitation pages. Retry after reviewing pending invitations.");
|
|
71
|
+
}
|
|
72
|
+
let acceptanceError;
|
|
73
|
+
for (const inv of invitations) {
|
|
74
|
+
const result = await request("PATCH", `/user/repository_invitations/${inv.id}`, token);
|
|
75
|
+
if (result.status >= 200 && result.status < 300) ui.success(`Accepted invite to ${inv.repository.full_name}`);
|
|
76
|
+
else acceptanceError = new RepositoryAccessError(`Could not accept the GitHub invitation for ${inv.repository.full_name} (HTTP ${result.status}).`, [401, 403].includes(result.status));
|
|
77
|
+
}
|
|
78
|
+
// A concurrent acceptance can reject our PATCH yet leave effective access ready.
|
|
79
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
80
|
+
missing = await missingRepos();
|
|
81
|
+
if (!missing.length) return required;
|
|
82
|
+
if (attempt < 2) await sleep(250 * (attempt + 1));
|
|
83
|
+
}
|
|
84
|
+
throw acceptanceError || new RepositoryAccessError(`GitHub access is still missing for ${missing.join(", ")}. Confirm this account has been invited to these repositories, then retry the same install command.`, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { RepositoryAccessError, requiredRepositories, matchesIdentity, ensureRepositoryAccess };
|
package/lib/setup.js
CHANGED
|
@@ -40,6 +40,7 @@ const {
|
|
|
40
40
|
} = require("./runtime-identity");
|
|
41
41
|
|
|
42
42
|
const https = require("node:https");
|
|
43
|
+
const { RepositoryAccessError, ensureRepositoryAccess } = require("./install-access");
|
|
43
44
|
|
|
44
45
|
function run(cmd, opts = {}) {
|
|
45
46
|
return execSync(cmd, { stdio: "pipe", encoding: "utf-8", timeout: 60000, ...opts }).trim();
|
|
@@ -98,6 +99,7 @@ function ghApi(method, apiPath, token, body) {
|
|
|
98
99
|
path: apiPath,
|
|
99
100
|
method,
|
|
100
101
|
headers,
|
|
102
|
+
timeout: 15000,
|
|
101
103
|
},
|
|
102
104
|
(res) => {
|
|
103
105
|
let buf = "";
|
|
@@ -112,6 +114,7 @@ function ghApi(method, apiPath, token, body) {
|
|
|
112
114
|
}
|
|
113
115
|
);
|
|
114
116
|
req.on("error", reject);
|
|
117
|
+
req.on("timeout", () => req.destroy(new Error("GitHub request timed out. Retry the same install command.")));
|
|
115
118
|
if (payload) req.write(payload);
|
|
116
119
|
req.end();
|
|
117
120
|
});
|
|
@@ -490,9 +493,15 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
490
493
|
// steps are skipped. Falls through to the normal flow if anything is off.
|
|
491
494
|
const alreadyInstalled = isFullySetUp(egregoreDir, memoryDir, fork_url, memory_url);
|
|
492
495
|
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
+
// Verify before credential writes or cloning. The same verified credential
|
|
497
|
+
// handles Git operations, so an unrelated SSH account cannot take over.
|
|
498
|
+
if (opts.verifyAccess) {
|
|
499
|
+
const required = await ensureRepositoryAccess(data, ui, { request: ghApi });
|
|
500
|
+
for (const name of required) verifyGitAccess(`https://github.com/${name}.git`, github_token);
|
|
501
|
+
} else {
|
|
502
|
+
if (github_token) configureGitCredentials(github_token);
|
|
503
|
+
await acceptPendingInvitations(github_token, github_org, ui);
|
|
504
|
+
}
|
|
496
505
|
|
|
497
506
|
if (alreadyInstalled) {
|
|
498
507
|
ui.success(`Already installed at ${ui.bold(egregoreDir)} — refreshing setup`);
|
|
@@ -500,7 +509,9 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
500
509
|
} else {
|
|
501
510
|
// 1. Clone fork (SSH first, HTTPS fallback — no token embedding)
|
|
502
511
|
ui.step(1, totalSteps, "Cloning egregore...");
|
|
503
|
-
if (
|
|
512
|
+
if (opts.verifyAccess) {
|
|
513
|
+
syncAuthenticatedRepository(fork_url, egregoreDir, github_token);
|
|
514
|
+
} else if (fs.existsSync(egregoreDir)) {
|
|
504
515
|
ui.warn("egregore/ already exists — pulling latest");
|
|
505
516
|
run("git pull", { cwd: egregoreDir });
|
|
506
517
|
} else {
|
|
@@ -523,7 +534,9 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
523
534
|
|
|
524
535
|
// 2. Clone memory
|
|
525
536
|
ui.step(2, totalSteps, "Cloning shared memory...");
|
|
526
|
-
if (
|
|
537
|
+
if (opts.verifyAccess) {
|
|
538
|
+
syncAuthenticatedRepository(memory_url, memoryDir, github_token);
|
|
539
|
+
} else if (fs.existsSync(memoryDir)) {
|
|
527
540
|
ui.warn(`${memoryDirName}/ already exists — pulling latest`);
|
|
528
541
|
run("git pull", { cwd: memoryDir });
|
|
529
542
|
} else {
|
|
@@ -671,7 +684,9 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
671
684
|
continue;
|
|
672
685
|
}
|
|
673
686
|
try {
|
|
674
|
-
if (
|
|
687
|
+
if (opts.verifyAccess) {
|
|
688
|
+
syncAuthenticatedRepository(`https://github.com/${github_org}/${repoName}.git`, repoDir, github_token);
|
|
689
|
+
} else if (fs.existsSync(repoDir)) {
|
|
675
690
|
ui.warn(`${repoName}/ already exists — pulling latest`);
|
|
676
691
|
run("git pull", { cwd: repoDir });
|
|
677
692
|
} else {
|
|
@@ -683,7 +698,8 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
683
698
|
try { run("git config credential.helper store", { cwd: repoDir }); } catch {}
|
|
684
699
|
clonedRepos.push(repoName);
|
|
685
700
|
ui.success(`Cloned ${repoName}`);
|
|
686
|
-
} catch {
|
|
701
|
+
} catch (err) {
|
|
702
|
+
if (opts.verifyAccess) throw err;
|
|
687
703
|
failedRepos.push(repoName);
|
|
688
704
|
ui.warn(`Could not clone ${repoName} — you may not have access yet. Ask the org admin to grant access.`);
|
|
689
705
|
}
|
|
@@ -932,6 +948,61 @@ function gitCloneEnv(base = process.env) {
|
|
|
932
948
|
};
|
|
933
949
|
}
|
|
934
950
|
|
|
951
|
+
function authenticatedGitEnv(token, base = process.env) {
|
|
952
|
+
if (!token || /[\r\n]/.test(token)) throw new Error("A valid GitHub credential is required.");
|
|
953
|
+
const env = gitCloneEnv(base);
|
|
954
|
+
const count = Number(env.GIT_CONFIG_COUNT || 0);
|
|
955
|
+
if (!Number.isSafeInteger(count) || count < 0) throw new Error("Invalid Git configuration environment.");
|
|
956
|
+
const settings = [
|
|
957
|
+
["credential.helper", ""],
|
|
958
|
+
["http.https://github.com/.extraheader", ""],
|
|
959
|
+
["http.https://github.com/.extraheader", `Authorization: Basic ${Buffer.from("x-access-token:" + token).toString("base64")}`],
|
|
960
|
+
];
|
|
961
|
+
settings.forEach(([key, value], index) => {
|
|
962
|
+
env[`GIT_CONFIG_KEY_${count + index}`] = key;
|
|
963
|
+
env[`GIT_CONFIG_VALUE_${count + index}`] = value;
|
|
964
|
+
});
|
|
965
|
+
env.GIT_CONFIG_COUNT = String(count + settings.length);
|
|
966
|
+
return env;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function authenticatedGit(args, token, cwd) {
|
|
970
|
+
try {
|
|
971
|
+
return execFileSync("git", args, {
|
|
972
|
+
cwd, env: authenticatedGitEnv(token), timeout: 60000,
|
|
973
|
+
stdio: "pipe", encoding: "utf-8",
|
|
974
|
+
}).trim();
|
|
975
|
+
} catch (err) {
|
|
976
|
+
const detail = redactToken(err.stderr || err.message, token)
|
|
977
|
+
.split(Buffer.from("x-access-token:" + token).toString("base64")).join("***");
|
|
978
|
+
throw new Error(`Git ${args[0]} failed: ${detail.trim()}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function verifyGitAccess(url, token) {
|
|
983
|
+
try {
|
|
984
|
+
authenticatedGit(["ls-remote", url], token);
|
|
985
|
+
} catch (err) {
|
|
986
|
+
throw new RepositoryAccessError(
|
|
987
|
+
`Cannot read ${url} with the verified GitHub account. ${err.message}`,
|
|
988
|
+
/authentication|could not read Username|not found|403|access denied/i.test(err.message),
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function syncAuthenticatedRepository(url, dir, token) {
|
|
994
|
+
if (fs.existsSync(dir)) {
|
|
995
|
+
const origin = authenticatedGit(["remote", "get-url", "origin"], token, dir);
|
|
996
|
+
if (normalizeGitUrl(origin) !== normalizeGitUrl(url)) {
|
|
997
|
+
throw new Error(`Refusing to update ${dir}: its Git origin differs from this setup.`);
|
|
998
|
+
}
|
|
999
|
+
// A retry reuses completed clones. Do not merge a remote default branch
|
|
1000
|
+
// into a user's working branch or disturb changes made after the failure.
|
|
1001
|
+
} else {
|
|
1002
|
+
authenticatedGit(["clone", url, dir], token);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
935
1006
|
/**
|
|
936
1007
|
* Clone a repo, trying SSH first then HTTPS (no token embedded).
|
|
937
1008
|
* The repo:invite-scoped OAuth token can't clone — we rely on the
|
|
@@ -1731,6 +1802,9 @@ module.exports = {
|
|
|
1731
1802
|
redactToken,
|
|
1732
1803
|
configureGitCredentials,
|
|
1733
1804
|
gitCloneEnv,
|
|
1805
|
+
authenticatedGitEnv,
|
|
1806
|
+
verifyGitAccess,
|
|
1807
|
+
syncAuthenticatedRepository,
|
|
1734
1808
|
assistantInstallCommand,
|
|
1735
1809
|
initMemoryDirs,
|
|
1736
1810
|
ensureDevelop,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Authenticate before redeeming, and keep retries on the original connected setup. */
|
|
2
|
+
const { RepositoryAccessError, matchesIdentity } = require("./install-access");
|
|
3
|
+
|
|
4
|
+
async function installFromToken(api, token, ctx, deps) {
|
|
5
|
+
const { ui, authenticate, request, install } = deps;
|
|
6
|
+
let auth = await authenticate(false);
|
|
7
|
+
if (!auth?.token) throw new Error("GitHub sign-in did not complete. Retry this install command to continue joining the same Egregore.");
|
|
8
|
+
let who = await request("GET", "/user", auth.token);
|
|
9
|
+
if ([401, 403].includes(who.status)) {
|
|
10
|
+
ui.info("Your GitHub session needs authentication. Signing in again before opening this setup.");
|
|
11
|
+
auth = await authenticate(true);
|
|
12
|
+
if (!auth?.token) throw new Error("GitHub sign-in did not complete. Retry this install command.");
|
|
13
|
+
who = await request("GET", "/user", auth.token);
|
|
14
|
+
}
|
|
15
|
+
if (who.status !== 200 || !who.data?.id) throw new Error(`GitHub identity check failed (HTTP ${who.status}). Retry after signing in.`);
|
|
16
|
+
const spinner = ui.spinner("Verifying setup...");
|
|
17
|
+
let data;
|
|
18
|
+
try {
|
|
19
|
+
data = await api.claimToken(token, auth.token);
|
|
20
|
+
spinner.stop(`Setting up ${ui.bold(data.org_name)}`);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
spinner.fail("Could not open this setup");
|
|
23
|
+
if (/Method Not Allowed|HTTP 405/i.test(err.message)) {
|
|
24
|
+
throw new Error("The setup service needs the authenticated retry update before this installer can continue. The setup token has not been consumed.");
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`${err.message}. Retry with the GitHub account that accepted the invitation. If the setup has expired, reopen the invitation page to continue joining the same Egregore.`);
|
|
27
|
+
}
|
|
28
|
+
if (!matchesIdentity(data, who.data)) throw new Error("This setup belongs to a different GitHub account. Sign in to the account that accepted the invitation and retry.");
|
|
29
|
+
|
|
30
|
+
const skipRepos = new Set();
|
|
31
|
+
if (ctx.prefilledRepo && Array.isArray(data.repos)) {
|
|
32
|
+
for (const repo of data.repos) {
|
|
33
|
+
const name = typeof repo === "string" ? repo : repo?.name;
|
|
34
|
+
if (name === ctx.prefilledRepo.name) skipRepos.add(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const options = {
|
|
38
|
+
skipRepos,
|
|
39
|
+
noAutolaunch: ctx.noAutolaunch,
|
|
40
|
+
noScreens: ctx.noScreens,
|
|
41
|
+
flowVariant: ctx.flowVariant,
|
|
42
|
+
runtime: ctx.runtime,
|
|
43
|
+
verifyAccess: true,
|
|
44
|
+
};
|
|
45
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
await install({ ...data, github_token: auth.token }, ui, ctx.targetDir, options);
|
|
48
|
+
return;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (attempt === 0 && err instanceof RepositoryAccessError && err.canReauthenticate) {
|
|
51
|
+
ui.warn(err.message);
|
|
52
|
+
ui.info("Signing in again with permission to accept invitations and read the required repositories.");
|
|
53
|
+
const next = await authenticate(true);
|
|
54
|
+
if (next?.token) {
|
|
55
|
+
auth = next;
|
|
56
|
+
// install() verifies identity again before invitations or any writes.
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`${err.message}\nRetry the same install command to continue this setup before its token expires.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { installFromToken };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.21.0-runtime-mvp.
|
|
3
|
+
"version": "0.21.0-runtime-mvp.27",
|
|
4
4
|
"description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"build:claude-runtime": "node scripts/build-claude-runtime-bundle.mjs",
|
|
15
15
|
"build:pi-runtime": "node scripts/build-pi-runtime-bundle.mjs",
|
|
16
16
|
"build:prime-runtime": "node scripts/build-prime-runtime-bundle.mjs",
|
|
17
|
-
"test": "node ../../bin/capability-distribution.mjs validate --root ../.. && node scripts/build-runtime-bundle.mjs --check && node scripts/build-claude-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node scripts/build-prime-runtime-bundle.mjs --check && node ../../bin/prime-render-spec.mjs --check && node ../../bin/pi-render-spec.mjs --check && node ../../api/scripts/build-connect-runtime-pack.mjs --check && node --test test/capability-distribution.test.js test/connect-runtime.test.js test/context.test.js test/path-lookup.test.js test/nixos-portability.test.js test/parse-join-target.test.js test/skip-repos.test.js test/install-origin-guard.test.js test/onboarding-shortcircuit.test.js test/runtime-identity.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/claude-runtime.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/prime-runtime.test.js test/prime-branch-policy.test.js test/prime-bridge-skill.test.js test/pi-observe-context.test.js test/pi-session-smoke.test.js test/prime-session-smoke.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/runtime-readiness.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js test/launcher-connect.test.js test/installer-connect-choice.test.js test/launcher-connected-status.test.js test/launcher-telemetry.test.js test/launcher-runtime.test.js test/launcher-settings.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/connected-config.test.js test/workflow-settings.test.js",
|
|
17
|
+
"test": "node ../../bin/capability-distribution.mjs validate --root ../.. && node scripts/build-runtime-bundle.mjs --check && node scripts/build-claude-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node scripts/build-prime-runtime-bundle.mjs --check && node ../../bin/prime-render-spec.mjs --check && node ../../bin/pi-render-spec.mjs --check && node ../../api/scripts/build-connect-runtime-pack.mjs --check && node --test test/invite-install.test.js test/capability-distribution.test.js test/connect-runtime.test.js test/context.test.js test/path-lookup.test.js test/nixos-portability.test.js test/parse-join-target.test.js test/skip-repos.test.js test/install-origin-guard.test.js test/onboarding-shortcircuit.test.js test/runtime-identity.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/claude-runtime.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/prime-runtime.test.js test/prime-branch-policy.test.js test/prime-bridge-skill.test.js test/pi-observe-context.test.js test/pi-session-smoke.test.js test/prime-session-smoke.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/runtime-readiness.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js test/launcher-connect.test.js test/installer-connect-choice.test.js test/launcher-connected-status.test.js test/launcher-telemetry.test.js test/launcher-runtime.test.js test/launcher-settings.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/connected-config.test.js test/workflow-settings.test.js",
|
|
18
18
|
"test:packed-runtime": "node scripts/packed-runtime-canary.mjs",
|
|
19
19
|
"test:e2e": "node --test test/e2e-local.test.js",
|
|
20
20
|
"test:all": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-claude-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/"
|