create-egregore 0.20.2 → 0.20.4
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 +13 -41
- package/bin/cli.js +14 -129
- package/lib/api.js +6 -2
- package/lib/auth.js +1 -1
- package/lib/install-access.js +87 -0
- package/lib/setup.js +93 -21
- package/lib/token-install.js +65 -0
- package/package.json +2 -2
- package/runtime/codex/bin/attendant.sh +14 -301
- package/runtime/codex/bin/autosave.sh +1 -27
- package/runtime/codex/bin/codex-session-start.sh +3 -3
- package/runtime/pi/bin/attendant.sh +14 -301
- package/runtime/pi/bin/autosave.sh +1 -27
- package/runtime/pi/bin/codex-session-start.sh +3 -3
- package/runtime/prime/bin/attendant.sh +14 -301
- package/runtime/prime/bin/autosave.sh +1 -27
- package/runtime/prime/bin/codex-session-start.sh +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.20.
|
|
2
|
+
// egregore-launcher-version: 0.20.4
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -33,7 +33,7 @@ const crypto = require("node:crypto");
|
|
|
33
33
|
const { spawnSync, spawn } = require("node:child_process");
|
|
34
34
|
const readline = require("node:readline");
|
|
35
35
|
|
|
36
|
-
const VERSION = "0.20.
|
|
36
|
+
const VERSION = "0.20.4";
|
|
37
37
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
38
38
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
39
39
|
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
|
@@ -702,9 +702,8 @@ function codexSupportMessage(egregoreDir) {
|
|
|
702
702
|
" .codex/hooks.json",
|
|
703
703
|
" .codex/hooks/",
|
|
704
704
|
"",
|
|
705
|
-
"
|
|
706
|
-
"
|
|
707
|
-
"Then rerun egregore.",
|
|
705
|
+
"Restore this instance using its installed runtime version, then rerun egregore.",
|
|
706
|
+
"A global CLI refresh does not repair instance files.",
|
|
708
707
|
"Egregore workflows appear as slash commands. In Codex, invoke them with the matching $skill token or ask in natural language.",
|
|
709
708
|
].join("\n");
|
|
710
709
|
}
|
|
@@ -735,7 +734,7 @@ function piSupportMessage(egregoreDir) {
|
|
|
735
734
|
"Pi meta-harness support is missing from this Egregore checkout.",
|
|
736
735
|
` path: ${egregoreDir}`,
|
|
737
736
|
"",
|
|
738
|
-
"
|
|
737
|
+
"Restore this instance using its installed runtime version, then rerun egregore.",
|
|
739
738
|
].join("\n");
|
|
740
739
|
}
|
|
741
740
|
|
|
@@ -754,31 +753,10 @@ function primeSupportMessage(egregoreDir) {
|
|
|
754
753
|
"Prime Agent runtime support is missing from this Egregore checkout.",
|
|
755
754
|
` path: ${egregoreDir}`,
|
|
756
755
|
"",
|
|
757
|
-
"
|
|
756
|
+
"Restore this instance using its installed runtime version, then rerun egregore.",
|
|
758
757
|
].join("\n");
|
|
759
758
|
}
|
|
760
759
|
|
|
761
|
-
function repairPortableRuntime(egregoreDir, runtime) {
|
|
762
|
-
const label = runtimeLabel(runtime);
|
|
763
|
-
process.stdout.write(`\n ${CYAN}↻${RESET} ${BOLD}Repairing ${label} support${RESET}${DIM} — updating this checkout${RESET}\n`);
|
|
764
|
-
const result = spawnSync(
|
|
765
|
-
"npx",
|
|
766
|
-
["-y", `${PUBLIC_NPX_PACKAGE}@latest`, "--refresh-launcher"],
|
|
767
|
-
{
|
|
768
|
-
cwd: egregoreDir,
|
|
769
|
-
stdio: "inherit",
|
|
770
|
-
env: { ...process.env, EGREGORE_RUNTIME_REPAIR: runtime },
|
|
771
|
-
timeout: 3 * 60 * 1000,
|
|
772
|
-
},
|
|
773
|
-
);
|
|
774
|
-
if (result.error || result.status !== 0) {
|
|
775
|
-
process.stderr.write(` ${YELLOW}!${RESET} Automatic ${label} repair failed; the checkout was left in place.\n`);
|
|
776
|
-
return false;
|
|
777
|
-
}
|
|
778
|
-
process.stdout.write(` ${GREEN}✓${RESET} ${label} support repaired\n\n`);
|
|
779
|
-
return true;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
760
|
// Minimal y/N line prompt for pre-launch decisions (the TUI select machinery
|
|
783
761
|
// is torn down by the time launch() runs).
|
|
784
762
|
function confirmLine(question) {
|
|
@@ -908,19 +886,15 @@ async function launch(inst, runtimeOverride) {
|
|
|
908
886
|
}
|
|
909
887
|
}
|
|
910
888
|
if (runtime === "codex") {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
die(codexSupportMessage(inst.path));
|
|
914
|
-
}
|
|
915
|
-
}
|
|
889
|
+
// Launching never changes an instance's runtime or release channel.
|
|
890
|
+
if (!hasCodexCoreSupport(inst.path)) die(codexSupportMessage(inst.path));
|
|
916
891
|
const skillsInstalled = installCodexSkills(inst.path);
|
|
917
892
|
const cardRendered = await renderCodexStartupCard(inst.path);
|
|
918
893
|
if (!skillsInstalled || !cardRendered) {
|
|
919
894
|
die(codexSupportMessage(inst.path));
|
|
920
895
|
}
|
|
921
896
|
} else if (runtime === "pi") {
|
|
922
|
-
if (!hasPiCoreSupport(inst.path)
|
|
923
|
-
(!repairPortableRuntime(inst.path, runtime) || !hasPiCoreSupport(inst.path))) {
|
|
897
|
+
if (!hasPiCoreSupport(inst.path)) {
|
|
924
898
|
die(piSupportMessage(inst.path));
|
|
925
899
|
}
|
|
926
900
|
}
|
|
@@ -931,10 +905,7 @@ async function launch(inst, runtimeOverride) {
|
|
|
931
905
|
// check + the trust-flag probe, which shells out to the binary), then a
|
|
932
906
|
// single handoff line explains the remaining in-harness startup.
|
|
933
907
|
const stopLoader = startSpinner("Starting Prime Agent");
|
|
934
|
-
|
|
935
|
-
if (!coreOk && repairPortableRuntime(inst.path, runtime)) {
|
|
936
|
-
coreOk = hasPrimeCoreSupport(inst.path);
|
|
937
|
-
}
|
|
908
|
+
const coreOk = hasPrimeCoreSupport(inst.path);
|
|
938
909
|
launchArgs = coreOk ? runtimeLaunchArgs(runtime, inst.path) : null;
|
|
939
910
|
stopLoader();
|
|
940
911
|
if (!coreOk) die(primeSupportMessage(inst.path));
|
|
@@ -1797,9 +1768,10 @@ async function cmdConnect(args) {
|
|
|
1797
1768
|
console.log(` ${DIM}${line}${RESET}`);
|
|
1798
1769
|
}
|
|
1799
1770
|
}
|
|
1771
|
+
// Connect is an instance-scoped overlay; never refresh other runtimes here.
|
|
1800
1772
|
const runtimeRefresh = spawnSync(
|
|
1801
1773
|
"npx",
|
|
1802
|
-
["-y", `${PUBLIC_NPX_PACKAGE}
|
|
1774
|
+
["-y", `${PUBLIC_NPX_PACKAGE}@${VERSION}`, "--refresh-connect"],
|
|
1803
1775
|
{
|
|
1804
1776
|
cwd: inst.path,
|
|
1805
1777
|
encoding: "utf-8",
|
|
@@ -1811,7 +1783,7 @@ async function cmdConnect(args) {
|
|
|
1811
1783
|
console.log(` ${GREEN}✓${RESET} Connect features installed`);
|
|
1812
1784
|
} else {
|
|
1813
1785
|
console.log(` ${YELLOW}!${RESET} Connect features will retry on the next update`);
|
|
1814
|
-
console.log(` ${DIM}npx -y ${PUBLIC_NPX_PACKAGE}
|
|
1786
|
+
console.log(` ${DIM}npx -y ${PUBLIC_NPX_PACKAGE}@${VERSION} --refresh-connect${RESET}`);
|
|
1815
1787
|
}
|
|
1816
1788
|
console.log("");
|
|
1817
1789
|
console.log(` ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is connected. Local files remain the source of truth.`);
|
package/bin/cli.js
CHANGED
|
@@ -25,27 +25,8 @@ const {
|
|
|
25
25
|
ghCliExists,
|
|
26
26
|
tryGhAuthOrLogin,
|
|
27
27
|
} = require("../lib/auth");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
installCodexRuntime,
|
|
31
|
-
reportCodexRuntimeSummary,
|
|
32
|
-
} = require("../lib/codex-runtime");
|
|
33
|
-
const {
|
|
34
|
-
hasPiRuntimeSupport,
|
|
35
|
-
installPiRuntime,
|
|
36
|
-
reportPiRuntimeSummary,
|
|
37
|
-
} = require("../lib/pi-runtime");
|
|
38
|
-
const {
|
|
39
|
-
hasPrimeRuntimeSupport,
|
|
40
|
-
installPrimeRuntime,
|
|
41
|
-
reportPrimeRuntimeSummary,
|
|
42
|
-
} = require("../lib/prime-runtime");
|
|
43
|
-
const {
|
|
44
|
-
connectEligibility,
|
|
45
|
-
fetchConnectRuntimePack,
|
|
46
|
-
installConnectRuntime,
|
|
47
|
-
reportConnectRuntimeSummary,
|
|
48
|
-
} = require("../lib/connect-runtime");
|
|
28
|
+
// Global CLI maintenance never installs or reconciles instance runtimes.
|
|
29
|
+
// Fresh installation and explicit instance updates own those writes.
|
|
49
30
|
const {
|
|
50
31
|
install,
|
|
51
32
|
ghApi,
|
|
@@ -120,45 +101,6 @@ function assistantLabel(runtime) {
|
|
|
120
101
|
return "Claude Code";
|
|
121
102
|
}
|
|
122
103
|
|
|
123
|
-
async function upgradePortableRuntimesForExisting(instances) {
|
|
124
|
-
for (const inst of instances) {
|
|
125
|
-
if (!inst || !inst.path || !fs.existsSync(inst.path)) continue;
|
|
126
|
-
try {
|
|
127
|
-
// Fetch and validate the private overlay before pruning the old public
|
|
128
|
-
// bundle. During the one-time ownership migration, an API outage must
|
|
129
|
-
// leave an existing Connect installation untouched.
|
|
130
|
-
const eligibility = connectEligibility(inst.path);
|
|
131
|
-
const connectPack = eligibility.eligible
|
|
132
|
-
? await fetchConnectRuntimePack(inst.path)
|
|
133
|
-
: null;
|
|
134
|
-
const summary = installCodexRuntime(inst.path);
|
|
135
|
-
const ready = hasCodexRuntimeSupport(inst.path);
|
|
136
|
-
reportCodexRuntimeSummary(summary, ui, {
|
|
137
|
-
label: `Codex runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
138
|
-
includeConflicts: !ready,
|
|
139
|
-
});
|
|
140
|
-
const piSummary = installPiRuntime(inst.path);
|
|
141
|
-
const piReady = hasPiRuntimeSupport(inst.path);
|
|
142
|
-
reportPiRuntimeSummary(piSummary, ui, {
|
|
143
|
-
label: `Pi meta-harness for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
144
|
-
includeConflicts: !piReady,
|
|
145
|
-
});
|
|
146
|
-
const primeSummary = installPrimeRuntime(inst.path);
|
|
147
|
-
const primeReady = hasPrimeRuntimeSupport(inst.path);
|
|
148
|
-
reportPrimeRuntimeSummary(primeSummary, ui, {
|
|
149
|
-
label: `Prime Agent runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
150
|
-
includeConflicts: !primeReady,
|
|
151
|
-
});
|
|
152
|
-
const connectSummary = await installConnectRuntime(inst.path, { validatedPack: connectPack });
|
|
153
|
-
reportConnectRuntimeSummary(connectSummary, ui, {
|
|
154
|
-
label: `Connect features for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
155
|
-
});
|
|
156
|
-
} catch (err) {
|
|
157
|
-
ui.warn(`Could not refresh shell-runtime support for ${inst.path}: ${err.message}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
104
|
async function chooseRuntime(args) {
|
|
163
105
|
if (args.runtime) {
|
|
164
106
|
try {
|
|
@@ -199,7 +141,6 @@ async function chooseRuntime(args) {
|
|
|
199
141
|
}
|
|
200
142
|
|
|
201
143
|
async function upgradeLauncherFromExisting(instances) {
|
|
202
|
-
await upgradePortableRuntimesForExisting(instances);
|
|
203
144
|
const inst = instances.find((entry) => entry && entry.path && fs.existsSync(entry.path));
|
|
204
145
|
if (!inst) return null;
|
|
205
146
|
try {
|
|
@@ -433,7 +374,7 @@ function showHelp() {
|
|
|
433
374
|
ui.info(" --force Skip the in-repo detection check");
|
|
434
375
|
ui.info(" --no-autolaunch Don't launch the harness at the end (for CI)");
|
|
435
376
|
ui.info(" --no-screens Skip the full-screen welcome/ready bookends");
|
|
436
|
-
ui.info(" --refresh-launcher Refresh the Egregore CLI
|
|
377
|
+
ui.info(" --refresh-launcher Refresh only the global Egregore CLI, then exit");
|
|
437
378
|
ui.info(" -h, --help Show this help");
|
|
438
379
|
ui.info("");
|
|
439
380
|
}
|
|
@@ -513,7 +454,6 @@ async function main() {
|
|
|
513
454
|
ui.error("No installed Egregore instance was found. Run create-egregore without --refresh-launcher.");
|
|
514
455
|
process.exit(1);
|
|
515
456
|
}
|
|
516
|
-
await upgradePortableRuntimesForExisting(instances);
|
|
517
457
|
const alias = await installLauncherBinary(instances[0].path, ui);
|
|
518
458
|
if (!alias || !alias.binaryPath) {
|
|
519
459
|
ui.error("Could not refresh the Egregore CLI.");
|
|
@@ -603,75 +543,20 @@ async function main() {
|
|
|
603
543
|
}
|
|
604
544
|
|
|
605
545
|
async function tokenFlow(api, token, ctx) {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
ui.error(err.message);
|
|
614
|
-
ui.info("");
|
|
615
|
-
ui.info("The token may have expired or already been used.");
|
|
616
|
-
ui.info("Visit egregore.xyz to get a new one, or run without --token:");
|
|
617
|
-
ui.info(" npx -y create-egregore@latest");
|
|
618
|
-
process.exit(1);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Joiner tokens don't include a github_token — try gh CLI first. If gh is
|
|
622
|
-
// installed but logged out, run gh auth login instead of device flow.
|
|
623
|
-
if (!data.github_token || data.needs_cli_auth) {
|
|
624
|
-
const ghAuth = tryGhAuthOrLogin(ui, data.needs_cli_auth ? { mode: "join" } : {});
|
|
625
|
-
if (ghAuth) {
|
|
626
|
-
ui.success(`Using your ${ui.bold("gh")} CLI auth${ghAuth.user ? ` (${ghAuth.user})` : ""}`);
|
|
627
|
-
data.github_token = ghAuth.token;
|
|
628
|
-
} else if (ghCliExists()) {
|
|
629
|
-
ui.error("GitHub CLI auth did not complete.");
|
|
630
|
-
ui.info(`Run ${ui.bold("gh auth login")} and then re-run this command.`);
|
|
631
|
-
process.exit(1);
|
|
632
|
-
} else {
|
|
546
|
+
const { installFromToken } = require("../lib/token-install");
|
|
547
|
+
await installFromToken(api, token, ctx, {
|
|
548
|
+
ui, install, request: ghApi,
|
|
549
|
+
authenticate: async (forceLogin) => {
|
|
550
|
+
const auth = tryGhAuthOrLogin(ui, { mode: "join", requireRepoScope: true, forceLogin });
|
|
551
|
+
if (auth) return auth;
|
|
552
|
+
if (ghCliExists()) return null;
|
|
633
553
|
if (!canUseInteractiveBrowserFlow()) {
|
|
634
554
|
explainNoGhHeadless(ui);
|
|
635
|
-
|
|
555
|
+
return null;
|
|
636
556
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
ui.success("Authenticated with GitHub");
|
|
641
|
-
} catch (err) {
|
|
642
|
-
ui.error(`GitHub auth failed: ${err.message}`);
|
|
643
|
-
process.exit(1);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Token-mode setups already have their repo list baked in server-side. If
|
|
649
|
-
// the user's current working repo happens to match a managed-repo name, skip
|
|
650
|
-
// the clone/pull/configure step for it so we don't clobber uncommitted work.
|
|
651
|
-
const skipRepos = new Set();
|
|
652
|
-
if (ctx.prefilledRepo && Array.isArray(data.repos)) {
|
|
653
|
-
for (const r of data.repos) {
|
|
654
|
-
const n = typeof r === "string" ? r : r && r.name;
|
|
655
|
-
if (n && n === ctx.prefilledRepo.name) skipRepos.add(n);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
try {
|
|
659
|
-
await install(data, ui, ctx.targetDir, {
|
|
660
|
-
skipRepos,
|
|
661
|
-
noAutolaunch: ctx.noAutolaunch,
|
|
662
|
-
noScreens: ctx.noScreens,
|
|
663
|
-
flowVariant: ctx.flowVariant,
|
|
664
|
-
runtime: ctx.runtime,
|
|
665
|
-
});
|
|
666
|
-
if (ctx.prefilledRepo && skipRepos.size === 0) {
|
|
667
|
-
console.log("");
|
|
668
|
-
ui.info(`Tip: add your current repo to managed_repos with:`);
|
|
669
|
-
ui.info(` /add ${ctx.prefilledRepo.toplevel || `../${ctx.prefilledRepo.localName}`}`);
|
|
670
|
-
}
|
|
671
|
-
} catch (err) {
|
|
672
|
-
ui.error(`Install failed: ${err.message}`);
|
|
673
|
-
process.exit(1);
|
|
674
|
-
}
|
|
557
|
+
return { token: await deviceFlow(ui, { mode: "join", scope: "repo" }) };
|
|
558
|
+
},
|
|
559
|
+
});
|
|
675
560
|
}
|
|
676
561
|
|
|
677
562
|
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
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
const { commandExists } = require("./path-lookup");
|
|
30
30
|
|
|
31
31
|
const https = require("node:https");
|
|
32
|
+
const { RepositoryAccessError, ensureRepositoryAccess } = require("./install-access");
|
|
32
33
|
|
|
33
34
|
function run(cmd, opts = {}) {
|
|
34
35
|
return execSync(cmd, { stdio: "pipe", encoding: "utf-8", timeout: 60000, ...opts }).trim();
|
|
@@ -52,6 +53,7 @@ function ghApi(method, apiPath, token, body) {
|
|
|
52
53
|
path: apiPath,
|
|
53
54
|
method,
|
|
54
55
|
headers,
|
|
56
|
+
timeout: 15000,
|
|
55
57
|
},
|
|
56
58
|
(res) => {
|
|
57
59
|
let buf = "";
|
|
@@ -66,6 +68,7 @@ function ghApi(method, apiPath, token, body) {
|
|
|
66
68
|
}
|
|
67
69
|
);
|
|
68
70
|
req.on("error", reject);
|
|
71
|
+
req.on("timeout", () => req.destroy(new Error("GitHub request timed out. Retry the same install command.")));
|
|
69
72
|
if (payload) req.write(payload);
|
|
70
73
|
req.end();
|
|
71
74
|
});
|
|
@@ -416,9 +419,15 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
416
419
|
// steps are skipped. Falls through to the normal flow if anything is off.
|
|
417
420
|
const alreadyInstalled = isFullySetUp(egregoreDir, memoryDir, fork_url, memory_url);
|
|
418
421
|
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
+
// Verify before credential writes or cloning. The same verified credential
|
|
423
|
+
// handles Git operations, so an unrelated SSH account cannot take over.
|
|
424
|
+
if (opts.verifyAccess) {
|
|
425
|
+
const required = await ensureRepositoryAccess(data, ui, { request: ghApi });
|
|
426
|
+
for (const name of required) verifyGitAccess(`https://github.com/${name}.git`, github_token);
|
|
427
|
+
} else {
|
|
428
|
+
if (github_token) configureGitCredentials(github_token);
|
|
429
|
+
await acceptPendingInvitations(github_token, github_org, ui);
|
|
430
|
+
}
|
|
422
431
|
|
|
423
432
|
if (alreadyInstalled) {
|
|
424
433
|
ui.success(`Already installed at ${ui.bold(egregoreDir)} — refreshing setup`);
|
|
@@ -426,7 +435,9 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
426
435
|
} else {
|
|
427
436
|
// 1. Clone fork (SSH first, HTTPS fallback — no token embedding)
|
|
428
437
|
ui.step(1, totalSteps, "Cloning egregore...");
|
|
429
|
-
if (
|
|
438
|
+
if (opts.verifyAccess) {
|
|
439
|
+
syncAuthenticatedRepository(fork_url, egregoreDir, github_token);
|
|
440
|
+
} else if (fs.existsSync(egregoreDir)) {
|
|
430
441
|
ui.warn("egregore/ already exists — pulling latest");
|
|
431
442
|
run("git pull", { cwd: egregoreDir });
|
|
432
443
|
} else {
|
|
@@ -449,7 +460,9 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
449
460
|
|
|
450
461
|
// 2. Clone memory
|
|
451
462
|
ui.step(2, totalSteps, "Cloning shared memory...");
|
|
452
|
-
if (
|
|
463
|
+
if (opts.verifyAccess) {
|
|
464
|
+
syncAuthenticatedRepository(memory_url, memoryDir, github_token);
|
|
465
|
+
} else if (fs.existsSync(memoryDir)) {
|
|
453
466
|
ui.warn(`${memoryDirName}/ already exists — pulling latest`);
|
|
454
467
|
run("git pull", { cwd: memoryDir });
|
|
455
468
|
} else {
|
|
@@ -576,7 +589,9 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
576
589
|
continue;
|
|
577
590
|
}
|
|
578
591
|
try {
|
|
579
|
-
if (
|
|
592
|
+
if (opts.verifyAccess) {
|
|
593
|
+
syncAuthenticatedRepository(`https://github.com/${github_org}/${repoName}.git`, repoDir, github_token);
|
|
594
|
+
} else if (fs.existsSync(repoDir)) {
|
|
580
595
|
ui.warn(`${repoName}/ already exists — pulling latest`);
|
|
581
596
|
run("git pull", { cwd: repoDir });
|
|
582
597
|
} else {
|
|
@@ -588,7 +603,8 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
588
603
|
try { run("git config credential.helper store", { cwd: repoDir }); } catch {}
|
|
589
604
|
clonedRepos.push(repoName);
|
|
590
605
|
ui.success(`Cloned ${repoName}`);
|
|
591
|
-
} catch {
|
|
606
|
+
} catch (err) {
|
|
607
|
+
if (opts.verifyAccess) throw err;
|
|
592
608
|
failedRepos.push(repoName);
|
|
593
609
|
ui.warn(`Could not clone ${repoName} — you may not have access yet. Ask the org admin to grant access.`);
|
|
594
610
|
}
|
|
@@ -823,6 +839,61 @@ function gitCloneEnv(base = process.env) {
|
|
|
823
839
|
};
|
|
824
840
|
}
|
|
825
841
|
|
|
842
|
+
function authenticatedGitEnv(token, base = process.env) {
|
|
843
|
+
if (!token || /[\r\n]/.test(token)) throw new Error("A valid GitHub credential is required.");
|
|
844
|
+
const env = gitCloneEnv(base);
|
|
845
|
+
const count = Number(env.GIT_CONFIG_COUNT || 0);
|
|
846
|
+
if (!Number.isSafeInteger(count) || count < 0) throw new Error("Invalid Git configuration environment.");
|
|
847
|
+
const settings = [
|
|
848
|
+
["credential.helper", ""],
|
|
849
|
+
["http.https://github.com/.extraheader", ""],
|
|
850
|
+
["http.https://github.com/.extraheader", `Authorization: Basic ${Buffer.from("x-access-token:" + token).toString("base64")}`],
|
|
851
|
+
];
|
|
852
|
+
settings.forEach(([key, value], index) => {
|
|
853
|
+
env[`GIT_CONFIG_KEY_${count + index}`] = key;
|
|
854
|
+
env[`GIT_CONFIG_VALUE_${count + index}`] = value;
|
|
855
|
+
});
|
|
856
|
+
env.GIT_CONFIG_COUNT = String(count + settings.length);
|
|
857
|
+
return env;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function authenticatedGit(args, token, cwd) {
|
|
861
|
+
try {
|
|
862
|
+
return execFileSync("git", args, {
|
|
863
|
+
cwd, env: authenticatedGitEnv(token), timeout: 60000,
|
|
864
|
+
stdio: "pipe", encoding: "utf-8",
|
|
865
|
+
}).trim();
|
|
866
|
+
} catch (err) {
|
|
867
|
+
const detail = redactToken(err.stderr || err.message, token)
|
|
868
|
+
.split(Buffer.from("x-access-token:" + token).toString("base64")).join("***");
|
|
869
|
+
throw new Error(`Git ${args[0]} failed: ${detail.trim()}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function verifyGitAccess(url, token) {
|
|
874
|
+
try {
|
|
875
|
+
authenticatedGit(["ls-remote", url], token);
|
|
876
|
+
} catch (err) {
|
|
877
|
+
throw new RepositoryAccessError(
|
|
878
|
+
`Cannot read ${url} with the verified GitHub account. ${err.message}`,
|
|
879
|
+
/authentication|could not read Username|not found|403|access denied/i.test(err.message),
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function syncAuthenticatedRepository(url, dir, token) {
|
|
885
|
+
if (fs.existsSync(dir)) {
|
|
886
|
+
const origin = authenticatedGit(["remote", "get-url", "origin"], token, dir);
|
|
887
|
+
if (normalizeGitUrl(origin) !== normalizeGitUrl(url)) {
|
|
888
|
+
throw new Error(`Refusing to update ${dir}: its Git origin differs from this setup.`);
|
|
889
|
+
}
|
|
890
|
+
// A retry reuses completed clones. Do not merge a remote default branch
|
|
891
|
+
// into a user's working branch or disturb changes made after the failure.
|
|
892
|
+
} else {
|
|
893
|
+
authenticatedGit(["clone", url, dir], token);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
826
897
|
/**
|
|
827
898
|
* Clone a repo, trying SSH first then HTTPS (no token embedded).
|
|
828
899
|
* The repo:invite-scoped OAuth token can't clone — we rely on the
|
|
@@ -1021,7 +1092,7 @@ function codexSupportMessage(egregoreDir) {
|
|
|
1021
1092
|
" .codex/hooks.json",
|
|
1022
1093
|
" .codex/hooks/",
|
|
1023
1094
|
"",
|
|
1024
|
-
"
|
|
1095
|
+
"Restore this instance using its installed runtime version, then rerun egregore.",
|
|
1025
1096
|
"Egregore workflows appear as slash commands. In Codex, invoke them with the matching $skill token or ask in natural language.",
|
|
1026
1097
|
].join("\n");
|
|
1027
1098
|
}
|
|
@@ -1035,7 +1106,7 @@ function piSupportMessage(egregoreDir) {
|
|
|
1035
1106
|
"Pi meta-harness support is missing from this Egregore checkout.",
|
|
1036
1107
|
` path: ${egregoreDir}`,
|
|
1037
1108
|
"",
|
|
1038
|
-
"
|
|
1109
|
+
"Restore this instance using its installed runtime version, then rerun egregore.",
|
|
1039
1110
|
"Pi loads Egregore through project-local .pi resources and the shared portable skill corpus.",
|
|
1040
1111
|
].join("\n");
|
|
1041
1112
|
}
|
|
@@ -1045,7 +1116,7 @@ function primeSupportMessage(egregoreDir) {
|
|
|
1045
1116
|
"Prime Agent runtime support is missing from this Egregore checkout.",
|
|
1046
1117
|
` path: ${egregoreDir}`,
|
|
1047
1118
|
"",
|
|
1048
|
-
"
|
|
1119
|
+
"Restore this instance using its installed runtime version, then rerun egregore.",
|
|
1049
1120
|
"Prime Agent loads Egregore through project-local .prime/agent resources and the shared portable skill corpus.",
|
|
1050
1121
|
].join("\n");
|
|
1051
1122
|
}
|
|
@@ -1171,8 +1242,6 @@ async function launchOrInstruct(egregoreDir, alias, ui, ctx = {}) {
|
|
|
1171
1242
|
console.log("");
|
|
1172
1243
|
|
|
1173
1244
|
if (runtime === "codex") {
|
|
1174
|
-
const codexRuntime = installCodexRuntime(egregoreDir);
|
|
1175
|
-
reportCodexRuntimeSummary(codexRuntime, ui);
|
|
1176
1245
|
if (!hasCodexCoreSupport(egregoreDir)) {
|
|
1177
1246
|
ui.warn(codexSupportMessage(egregoreDir));
|
|
1178
1247
|
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
@@ -1188,8 +1257,6 @@ async function launchOrInstruct(egregoreDir, alias, ui, ctx = {}) {
|
|
|
1188
1257
|
return;
|
|
1189
1258
|
}
|
|
1190
1259
|
} else if (runtime === "pi") {
|
|
1191
|
-
const piRuntime = installPiRuntime(egregoreDir);
|
|
1192
|
-
reportPiRuntimeSummary(piRuntime, ui);
|
|
1193
1260
|
if (!hasPiRuntimeSupport(egregoreDir)) {
|
|
1194
1261
|
ui.warn(piSupportMessage(egregoreDir));
|
|
1195
1262
|
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
@@ -1197,8 +1264,6 @@ async function launchOrInstruct(egregoreDir, alias, ui, ctx = {}) {
|
|
|
1197
1264
|
return;
|
|
1198
1265
|
}
|
|
1199
1266
|
} else if (runtime === "prime") {
|
|
1200
|
-
const primeRuntime = installPrimeRuntime(egregoreDir);
|
|
1201
|
-
reportPrimeRuntimeSummary(primeRuntime, ui);
|
|
1202
1267
|
if (!hasPrimeRuntimeSupport(egregoreDir)) {
|
|
1203
1268
|
ui.warn(primeSupportMessage(egregoreDir));
|
|
1204
1269
|
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
@@ -1309,11 +1374,15 @@ function appendLocalBinToRc(ui) {
|
|
|
1309
1374
|
*/
|
|
1310
1375
|
async function installLauncherBinary(egregoreDir, ui) {
|
|
1311
1376
|
const template = readLauncherTemplate();
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
{ dir: path.
|
|
1316
|
-
|
|
1377
|
+
// An explicit destination must never fall through to machine-wide paths.
|
|
1378
|
+
const configuredBinDir = process.env.EGREGORE_LAUNCHER_BIN_DIR;
|
|
1379
|
+
const candidates = configuredBinDir
|
|
1380
|
+
? [{ dir: path.resolve(configuredBinDir), location: "user" }]
|
|
1381
|
+
: [
|
|
1382
|
+
{ dir: "/usr/local/bin", location: "system" },
|
|
1383
|
+
{ dir: "/opt/homebrew/bin", location: "system" },
|
|
1384
|
+
{ dir: path.join(os.homedir(), ".local", "bin"), location: "user" },
|
|
1385
|
+
];
|
|
1317
1386
|
|
|
1318
1387
|
let chosen = null;
|
|
1319
1388
|
for (const c of candidates) {
|
|
@@ -1546,6 +1615,9 @@ module.exports = {
|
|
|
1546
1615
|
redactToken,
|
|
1547
1616
|
configureGitCredentials,
|
|
1548
1617
|
gitCloneEnv,
|
|
1618
|
+
authenticatedGitEnv,
|
|
1619
|
+
verifyGitAccess,
|
|
1620
|
+
syncAuthenticatedRepository,
|
|
1549
1621
|
assistantInstallCommand,
|
|
1550
1622
|
initMemoryDirs,
|
|
1551
1623
|
ensureDevelop,
|