create-egregore 0.20.3 → 0.20.5
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 +120 -70
- package/bin/cli.js +80 -66
- package/lib/api.js +6 -2
- package/lib/auth.js +1 -1
- package/lib/harness-add.js +159 -0
- package/lib/install-access.js +87 -0
- package/lib/setup.js +84 -10
- 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.20.
|
|
2
|
+
// egregore-launcher-version: 0.20.5
|
|
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.5";
|
|
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");
|
|
@@ -689,72 +689,120 @@ async function renderCodexStartupCard(egregoreDir) {
|
|
|
689
689
|
return false;
|
|
690
690
|
}
|
|
691
691
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
"
|
|
695
|
-
|
|
696
|
-
"",
|
|
697
|
-
"
|
|
698
|
-
"
|
|
699
|
-
"
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
"
|
|
703
|
-
"
|
|
704
|
-
"",
|
|
705
|
-
"
|
|
706
|
-
"
|
|
707
|
-
|
|
708
|
-
|
|
692
|
+
const CORE_SUPPORT_PATHS = {
|
|
693
|
+
codex: [
|
|
694
|
+
"bin/codex-session-start.sh",
|
|
695
|
+
"bin/codex-install-skills.sh",
|
|
696
|
+
".codex/config.toml",
|
|
697
|
+
".codex/skills",
|
|
698
|
+
".codex/hooks.json",
|
|
699
|
+
".codex/hooks",
|
|
700
|
+
],
|
|
701
|
+
pi: [
|
|
702
|
+
"bin/pi-session-start.sh",
|
|
703
|
+
".pi/APPEND_SYSTEM.md",
|
|
704
|
+
".pi/settings.json",
|
|
705
|
+
".pi/extensions/egregore.ts",
|
|
706
|
+
".codex/skills",
|
|
707
|
+
],
|
|
708
|
+
prime: [
|
|
709
|
+
"bin/prime-session-start.sh",
|
|
710
|
+
".prime/agent/APPEND_SYSTEM.md",
|
|
711
|
+
".prime/agent/settings.json",
|
|
712
|
+
".prime/agent/extensions/egregore.ts",
|
|
713
|
+
".codex/skills",
|
|
714
|
+
],
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
function missingCorePaths(runtime, egregoreDir) {
|
|
718
|
+
return (CORE_SUPPORT_PATHS[runtime] || []).filter(
|
|
719
|
+
(rel) => !fs.existsSync(path.join(egregoreDir, ...rel.split("/"))),
|
|
720
|
+
);
|
|
709
721
|
}
|
|
710
722
|
|
|
711
723
|
function hasCodexCoreSupport(egregoreDir) {
|
|
712
|
-
return (
|
|
713
|
-
fs.existsSync(path.join(egregoreDir, "bin", "codex-session-start.sh")) &&
|
|
714
|
-
fs.existsSync(path.join(egregoreDir, "bin", "codex-install-skills.sh")) &&
|
|
715
|
-
fs.existsSync(path.join(egregoreDir, ".codex", "config.toml")) &&
|
|
716
|
-
fs.existsSync(path.join(egregoreDir, ".codex", "skills")) &&
|
|
717
|
-
fs.existsSync(path.join(egregoreDir, ".codex", "hooks.json")) &&
|
|
718
|
-
fs.existsSync(path.join(egregoreDir, ".codex", "hooks"))
|
|
719
|
-
);
|
|
724
|
+
return missingCorePaths("codex", egregoreDir).length === 0;
|
|
720
725
|
}
|
|
721
726
|
|
|
722
727
|
function hasPiCoreSupport(egregoreDir) {
|
|
723
|
-
return (
|
|
724
|
-
fs.existsSync(path.join(egregoreDir, "bin", "pi-session-start.sh")) &&
|
|
725
|
-
fs.existsSync(path.join(egregoreDir, ".pi", "APPEND_SYSTEM.md")) &&
|
|
726
|
-
fs.existsSync(path.join(egregoreDir, ".pi", "settings.json")) &&
|
|
727
|
-
fs.existsSync(path.join(egregoreDir, ".pi", "extensions", "egregore.ts")) &&
|
|
728
|
-
fs.existsSync(path.join(egregoreDir, ".codex", "skills"))
|
|
729
|
-
);
|
|
728
|
+
return missingCorePaths("pi", egregoreDir).length === 0;
|
|
730
729
|
}
|
|
731
730
|
|
|
732
|
-
function
|
|
733
|
-
return
|
|
734
|
-
|
|
731
|
+
function hasPrimeCoreSupport(egregoreDir) {
|
|
732
|
+
return missingCorePaths("prime", egregoreDir).length === 0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// The repair command is pinned to this launcher's own release. `@latest`
|
|
736
|
+
// once pulled another release channel's files into an instance; the
|
|
737
|
+
// launcher and the files it installs must always be the same version.
|
|
738
|
+
function harnessAddCommand(runtime) {
|
|
739
|
+
return `npx -y ${PUBLIC_NPX_PACKAGE}@${VERSION} --add-harness ${runtimeShort(runtime)}`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function harnessSupportMessage(runtime, egregoreDir) {
|
|
743
|
+
const label = runtimeLabel(runtime);
|
|
744
|
+
const missing = missingCorePaths(runtime, egregoreDir);
|
|
745
|
+
const lines = [
|
|
746
|
+
`${label} support is missing from this Egregore checkout.`,
|
|
735
747
|
` path: ${egregoreDir}`,
|
|
748
|
+
];
|
|
749
|
+
if (missing.length > 0) {
|
|
750
|
+
lines.push("", "Missing:");
|
|
751
|
+
for (const rel of missing) lines.push(` ${rel}`);
|
|
752
|
+
}
|
|
753
|
+
lines.push(
|
|
736
754
|
"",
|
|
737
|
-
|
|
738
|
-
|
|
755
|
+
`Add ${label} support to this instance (missing files only, nothing overwritten):`,
|
|
756
|
+
` cd ${egregoreDir}`,
|
|
757
|
+
` ${harnessAddCommand(runtime)}`,
|
|
758
|
+
"Then rerun egregore.",
|
|
759
|
+
);
|
|
760
|
+
if (runtime === "codex") {
|
|
761
|
+
lines.push("Egregore workflows appear as slash commands. In Codex, invoke them with the matching $skill token or ask in natural language.");
|
|
762
|
+
}
|
|
763
|
+
return lines.join("\n");
|
|
739
764
|
}
|
|
740
765
|
|
|
741
|
-
function
|
|
742
|
-
return (
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
fs.existsSync(path.join(egregoreDir, ".codex", "skills"))
|
|
748
|
-
);
|
|
766
|
+
function codexSupportMessage(egregoreDir) {
|
|
767
|
+
return harnessSupportMessage("codex", egregoreDir);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function piSupportMessage(egregoreDir) {
|
|
771
|
+
return harnessSupportMessage("pi", egregoreDir);
|
|
749
772
|
}
|
|
750
773
|
|
|
751
774
|
function primeSupportMessage(egregoreDir) {
|
|
752
|
-
return
|
|
753
|
-
|
|
754
|
-
|
|
775
|
+
return harnessSupportMessage("prime", egregoreDir);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// A launch that fails the support check offers to add exactly that harness
|
|
779
|
+
// to exactly this instance, using this launcher's own release. Without a
|
|
780
|
+
// terminal to ask, print the command and stop; never install unattended.
|
|
781
|
+
async function ensureHarnessSupport(inst, runtime) {
|
|
782
|
+
if (missingCorePaths(runtime, inst.path).length === 0) return;
|
|
783
|
+
const message = harnessSupportMessage(runtime, inst.path);
|
|
784
|
+
if (!canPrompt()) die(message);
|
|
785
|
+
const label = runtimeLabel(runtime);
|
|
786
|
+
const name = inst.name || inst.slug || path.basename(inst.path);
|
|
787
|
+
process.stdout.write([
|
|
755
788
|
"",
|
|
756
|
-
|
|
757
|
-
|
|
789
|
+
` ${BOLD}${label} support is missing from ${name}.${RESET}`,
|
|
790
|
+
` ${DIM}Missing: ${missingCorePaths(runtime, inst.path).join(", ")}${RESET}`,
|
|
791
|
+
` ${DIM}Egregore can add it now. Only missing files are written; nothing is overwritten:${RESET}`,
|
|
792
|
+
` ${harnessAddCommand(runtime)}`,
|
|
793
|
+
"",
|
|
794
|
+
].join("\n"));
|
|
795
|
+
const yes = await confirmLine(`Add ${label} support to ${name} now?`);
|
|
796
|
+
if (!yes) die(message);
|
|
797
|
+
const result = spawnSync(
|
|
798
|
+
"npx",
|
|
799
|
+
["-y", `${PUBLIC_NPX_PACKAGE}@${VERSION}`, "--add-harness", runtimeShort(runtime)],
|
|
800
|
+
{ cwd: inst.path, stdio: "inherit", env: process.env, timeout: 3 * 60 * 1000 },
|
|
801
|
+
);
|
|
802
|
+
if (result.error || result.status !== 0 || missingCorePaths(runtime, inst.path).length > 0) {
|
|
803
|
+
die(`${label} support could not be added automatically.\n\n${harnessSupportMessage(runtime, inst.path)}`);
|
|
804
|
+
}
|
|
805
|
+
process.stdout.write(` ${GREEN}✓${RESET} ${label} support added to ${name}\n\n`);
|
|
758
806
|
}
|
|
759
807
|
|
|
760
808
|
// Minimal y/N line prompt for pre-launch decisions (the TUI select machinery
|
|
@@ -809,7 +857,7 @@ function primeAuthHint() {
|
|
|
809
857
|
|
|
810
858
|
function installCodexSkills(egregoreDir) {
|
|
811
859
|
const script = path.join(egregoreDir, "bin", "codex-install-skills.sh");
|
|
812
|
-
if (!fs.existsSync(script)) return false;
|
|
860
|
+
if (!fs.existsSync(script)) return { ok: false, stderr: `missing ${script}` };
|
|
813
861
|
try {
|
|
814
862
|
const result = spawnSync("bash", [script], {
|
|
815
863
|
cwd: egregoreDir,
|
|
@@ -817,9 +865,9 @@ function installCodexSkills(egregoreDir) {
|
|
|
817
865
|
encoding: "utf-8",
|
|
818
866
|
maxBuffer: 1024 * 1024,
|
|
819
867
|
});
|
|
820
|
-
return result.status === 0;
|
|
821
|
-
} catch {
|
|
822
|
-
return false;
|
|
868
|
+
return { ok: result.status === 0, stderr: (result.stderr || "").trim() };
|
|
869
|
+
} catch (err) {
|
|
870
|
+
return { ok: false, stderr: String(err && err.message ? err.message : err) };
|
|
823
871
|
}
|
|
824
872
|
}
|
|
825
873
|
|
|
@@ -886,29 +934,31 @@ async function launch(inst, runtimeOverride) {
|
|
|
886
934
|
}
|
|
887
935
|
}
|
|
888
936
|
if (runtime === "codex") {
|
|
889
|
-
// Launching never changes an instance's runtime or release channel.
|
|
890
|
-
|
|
891
|
-
|
|
937
|
+
// Launching never changes an instance's runtime or release channel. A
|
|
938
|
+
// missing harness is added only with the user's consent, one harness
|
|
939
|
+
// into this one instance, at this launcher's own version.
|
|
940
|
+
await ensureHarnessSupport(inst, runtime);
|
|
941
|
+
const skills = installCodexSkills(inst.path);
|
|
942
|
+
if (!skills.ok) {
|
|
943
|
+
die(`Codex skill setup failed in ${inst.path} (bin/codex-install-skills.sh):\n${skills.stderr || " (no error output)"}`);
|
|
944
|
+
}
|
|
892
945
|
const cardRendered = await renderCodexStartupCard(inst.path);
|
|
893
|
-
if (!
|
|
894
|
-
die(
|
|
946
|
+
if (!cardRendered) {
|
|
947
|
+
die(`Codex startup card failed in ${inst.path} (bin/codex-session-start.sh exited non-zero; its output is above).`);
|
|
895
948
|
}
|
|
896
949
|
} else if (runtime === "pi") {
|
|
897
|
-
|
|
898
|
-
die(piSupportMessage(inst.path));
|
|
899
|
-
}
|
|
950
|
+
await ensureHarnessSupport(inst, runtime);
|
|
900
951
|
}
|
|
901
952
|
let launchArgs = null;
|
|
902
953
|
if (runtime === "prime") {
|
|
954
|
+
await ensureHarnessSupport(inst, runtime);
|
|
903
955
|
// Cover the gap between choosing the harness and Prime's first paint:
|
|
904
|
-
// the loader runs through the launcher-side pre-flight (
|
|
905
|
-
//
|
|
906
|
-
//
|
|
956
|
+
// the loader runs through the launcher-side pre-flight (the trust-flag
|
|
957
|
+
// probe, which shells out to the binary), then a single handoff line
|
|
958
|
+
// explains the remaining in-harness startup.
|
|
907
959
|
const stopLoader = startSpinner("Starting Prime Agent");
|
|
908
|
-
|
|
909
|
-
launchArgs = coreOk ? runtimeLaunchArgs(runtime, inst.path) : null;
|
|
960
|
+
launchArgs = runtimeLaunchArgs(runtime, inst.path);
|
|
910
961
|
stopLoader();
|
|
911
|
-
if (!coreOk) die(primeSupportMessage(inst.path));
|
|
912
962
|
primeAuthHint();
|
|
913
963
|
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}Prime Agent starting${RESET}${DIM} — the Egregore greeting loads inside in a few seconds${RESET}\n`);
|
|
914
964
|
}
|
package/bin/cli.js
CHANGED
|
@@ -89,6 +89,32 @@ function detectCwdEgregore(cwd) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Resolve which instance `--add-harness` writes into. Explicit `--instance`
|
|
93
|
+
// wins; otherwise the current directory when it is an instance; otherwise the
|
|
94
|
+
// only registered instance. Several registered instances and no explicit
|
|
95
|
+
// choice is an error — this command must never fan out.
|
|
96
|
+
function resolveAddHarnessTarget(explicitPath) {
|
|
97
|
+
const isInstance = (dir) => dir && fs.existsSync(path.join(dir, "egregore.json"));
|
|
98
|
+
if (explicitPath) {
|
|
99
|
+
const dir = path.resolve(explicitPath);
|
|
100
|
+
if (isInstance(dir)) return dir;
|
|
101
|
+
ui.error(`Not an Egregore instance: ${dir} (no egregore.json).`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
let cwd = null;
|
|
105
|
+
try { cwd = process.cwd(); } catch {}
|
|
106
|
+
if (isInstance(cwd)) return cwd;
|
|
107
|
+
const registered = readLocalInstances().filter((inst) => isInstance(inst.path));
|
|
108
|
+
if (registered.length === 1) return path.resolve(registered[0].path);
|
|
109
|
+
if (registered.length === 0) {
|
|
110
|
+
ui.error("No Egregore instance found. Run this inside the instance directory, or pass --instance <path>.");
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
ui.error("Several Egregore instances are registered. Pass --instance <path> to choose one:");
|
|
114
|
+
for (const inst of registered) ui.info(` ${inst.path}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
function binaryOnPath(bin) {
|
|
93
119
|
return commandExists(bin);
|
|
94
120
|
}
|
|
@@ -199,6 +225,14 @@ function parseArgs(argv) {
|
|
|
199
225
|
args.refreshLauncher = true;
|
|
200
226
|
} else if (argv[i] === "--refresh-connect") {
|
|
201
227
|
args.refreshConnect = true;
|
|
228
|
+
} else if (argv[i] === "--add-harness" && argv[i + 1]) {
|
|
229
|
+
args.addHarness = argv[++i];
|
|
230
|
+
} else if (argv[i].startsWith("--add-harness=")) {
|
|
231
|
+
args.addHarness = argv[i].split("=")[1];
|
|
232
|
+
} else if (argv[i] === "--instance" && argv[i + 1]) {
|
|
233
|
+
args.instance = argv[++i];
|
|
234
|
+
} else if (argv[i].startsWith("--instance=")) {
|
|
235
|
+
args.instance = argv[i].split("=")[1];
|
|
202
236
|
} else if (argv[i] === "--codex") {
|
|
203
237
|
args.runtime = "codex";
|
|
204
238
|
} else if (argv[i] === "--pi") {
|
|
@@ -375,6 +409,8 @@ function showHelp() {
|
|
|
375
409
|
ui.info(" --no-autolaunch Don't launch the harness at the end (for CI)");
|
|
376
410
|
ui.info(" --no-screens Skip the full-screen welcome/ready bookends");
|
|
377
411
|
ui.info(" --refresh-launcher Refresh only the global Egregore CLI, then exit");
|
|
412
|
+
ui.info(" --add-harness <h> Add codex, pi, or prime support to one existing instance (missing files only)");
|
|
413
|
+
ui.info(" --instance <path> Instance for --add-harness (default: cwd, or the only registered instance)");
|
|
378
414
|
ui.info(" -h, --help Show this help");
|
|
379
415
|
ui.info("");
|
|
380
416
|
}
|
|
@@ -421,6 +457,39 @@ async function main() {
|
|
|
421
457
|
}
|
|
422
458
|
}
|
|
423
459
|
|
|
460
|
+
if (args.addHarness) {
|
|
461
|
+
// Explicit, instance-scoped harness addition. This is the only path that
|
|
462
|
+
// writes harness files into an existing instance from the global CLI: one
|
|
463
|
+
// harness, one instance, missing files only. The launcher invokes it with
|
|
464
|
+
// its own pinned version when a launch fails the support check.
|
|
465
|
+
const { addHarnessRuntime, harnessLabel, normalizeHarness } = require("../lib/harness-add");
|
|
466
|
+
const harness = normalizeHarness(args.addHarness);
|
|
467
|
+
if (!harness) {
|
|
468
|
+
ui.error(`Unknown harness "${args.addHarness}". Use --add-harness codex, pi, or prime.`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
const targetDir = resolveAddHarnessTarget(args.instance);
|
|
472
|
+
if (!targetDir) process.exit(1);
|
|
473
|
+
try {
|
|
474
|
+
const summary = addHarnessRuntime(targetDir, harness);
|
|
475
|
+
const label = harnessLabel(harness);
|
|
476
|
+
if (summary.installed.length === 0) {
|
|
477
|
+
ui.success(`${label} support already present in ${targetDir}`);
|
|
478
|
+
} else {
|
|
479
|
+
ui.success(`Added ${label} support to ${targetDir} ${ui.dim(`(${summary.installed.length} files, ${summary.present} kept)`)}`);
|
|
480
|
+
}
|
|
481
|
+
if (!summary.ready) {
|
|
482
|
+
ui.warn(`${label} support is still incomplete in ${targetDir}; an existing file may be a directory or symlink where a file is expected.`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
ui.info(`Run: egregore --runtime ${harness}`);
|
|
486
|
+
return;
|
|
487
|
+
} catch (err) {
|
|
488
|
+
ui.error(String(err.message || err));
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
424
493
|
if (args.refreshConnect) {
|
|
425
494
|
// Session-start maintenance mode: refresh the Connect overlay for an
|
|
426
495
|
// existing instance. Local instances no-op instantly; network failure is
|
|
@@ -543,75 +612,20 @@ async function main() {
|
|
|
543
612
|
}
|
|
544
613
|
|
|
545
614
|
async function tokenFlow(api, token, ctx) {
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
ui.error(err.message);
|
|
554
|
-
ui.info("");
|
|
555
|
-
ui.info("The token may have expired or already been used.");
|
|
556
|
-
ui.info("Visit egregore.xyz to get a new one, or run without --token:");
|
|
557
|
-
ui.info(" npx -y create-egregore@latest");
|
|
558
|
-
process.exit(1);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Joiner tokens don't include a github_token — try gh CLI first. If gh is
|
|
562
|
-
// installed but logged out, run gh auth login instead of device flow.
|
|
563
|
-
if (!data.github_token || data.needs_cli_auth) {
|
|
564
|
-
const ghAuth = tryGhAuthOrLogin(ui, data.needs_cli_auth ? { mode: "join" } : {});
|
|
565
|
-
if (ghAuth) {
|
|
566
|
-
ui.success(`Using your ${ui.bold("gh")} CLI auth${ghAuth.user ? ` (${ghAuth.user})` : ""}`);
|
|
567
|
-
data.github_token = ghAuth.token;
|
|
568
|
-
} else if (ghCliExists()) {
|
|
569
|
-
ui.error("GitHub CLI auth did not complete.");
|
|
570
|
-
ui.info(`Run ${ui.bold("gh auth login")} and then re-run this command.`);
|
|
571
|
-
process.exit(1);
|
|
572
|
-
} else {
|
|
615
|
+
const { installFromToken } = require("../lib/token-install");
|
|
616
|
+
await installFromToken(api, token, ctx, {
|
|
617
|
+
ui, install, request: ghApi,
|
|
618
|
+
authenticate: async (forceLogin) => {
|
|
619
|
+
const auth = tryGhAuthOrLogin(ui, { mode: "join", requireRepoScope: true, forceLogin });
|
|
620
|
+
if (auth) return auth;
|
|
621
|
+
if (ghCliExists()) return null;
|
|
573
622
|
if (!canUseInteractiveBrowserFlow()) {
|
|
574
623
|
explainNoGhHeadless(ui);
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
ui.info("\nSign in to your GitHub to complete setup.\n");
|
|
578
|
-
try {
|
|
579
|
-
data.github_token = await deviceFlow(ui, data.needs_cli_auth ? { mode: "join" } : {});
|
|
580
|
-
ui.success("Authenticated with GitHub");
|
|
581
|
-
} catch (err) {
|
|
582
|
-
ui.error(`GitHub auth failed: ${err.message}`);
|
|
583
|
-
process.exit(1);
|
|
624
|
+
return null;
|
|
584
625
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
// Token-mode setups already have their repo list baked in server-side. If
|
|
589
|
-
// the user's current working repo happens to match a managed-repo name, skip
|
|
590
|
-
// the clone/pull/configure step for it so we don't clobber uncommitted work.
|
|
591
|
-
const skipRepos = new Set();
|
|
592
|
-
if (ctx.prefilledRepo && Array.isArray(data.repos)) {
|
|
593
|
-
for (const r of data.repos) {
|
|
594
|
-
const n = typeof r === "string" ? r : r && r.name;
|
|
595
|
-
if (n && n === ctx.prefilledRepo.name) skipRepos.add(n);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
try {
|
|
599
|
-
await install(data, ui, ctx.targetDir, {
|
|
600
|
-
skipRepos,
|
|
601
|
-
noAutolaunch: ctx.noAutolaunch,
|
|
602
|
-
noScreens: ctx.noScreens,
|
|
603
|
-
flowVariant: ctx.flowVariant,
|
|
604
|
-
runtime: ctx.runtime,
|
|
605
|
-
});
|
|
606
|
-
if (ctx.prefilledRepo && skipRepos.size === 0) {
|
|
607
|
-
console.log("");
|
|
608
|
-
ui.info(`Tip: add your current repo to managed_repos with:`);
|
|
609
|
-
ui.info(` /add ${ctx.prefilledRepo.toplevel || `../${ctx.prefilledRepo.localName}`}`);
|
|
610
|
-
}
|
|
611
|
-
} catch (err) {
|
|
612
|
-
ui.error(`Install failed: ${err.message}`);
|
|
613
|
-
process.exit(1);
|
|
614
|
-
}
|
|
626
|
+
return { token: await deviceFlow(ui, { mode: "join", scope: "repo" }) };
|
|
627
|
+
},
|
|
628
|
+
});
|
|
615
629
|
}
|
|
616
630
|
|
|
617
631
|
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,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Add one harness (Codex, Pi, Prime Agent) to an existing instance.
|
|
4
|
+
//
|
|
5
|
+
// Setup overlays only the harness chosen at install time, so a Claude-only
|
|
6
|
+
// instance has no Codex files. The September 2026 launcher auto-repair fixed
|
|
7
|
+
// that by running the *latest* installer against *every* registered instance,
|
|
8
|
+
// which overwrote and deleted files in instances on another release channel.
|
|
9
|
+
// This module is the scoped replacement:
|
|
10
|
+
//
|
|
11
|
+
// - one harness, one instance, chosen explicitly;
|
|
12
|
+
// - copies only files that are missing from that harness's packaged bundle;
|
|
13
|
+
// - never overwrites, never deletes, never touches another harness;
|
|
14
|
+
// - records what it wrote in its own manifest so nothing else claims
|
|
15
|
+
// ownership of pre-existing files.
|
|
16
|
+
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
|
|
20
|
+
const PACKAGE = require("../package.json");
|
|
21
|
+
|
|
22
|
+
const HARNESSES = {
|
|
23
|
+
codex: { label: "Codex", module: "./codex-runtime", bundle: "codex" },
|
|
24
|
+
pi: { label: "Pi", module: "./pi-runtime", bundle: "pi" },
|
|
25
|
+
prime: { label: "Prime Agent", module: "./prime-runtime", bundle: "prime" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function normalizeHarness(name) {
|
|
29
|
+
const value = String(name || "").trim().toLowerCase();
|
|
30
|
+
if (value === "codex") return "codex";
|
|
31
|
+
if (value === "pi") return "pi";
|
|
32
|
+
if (value === "prime" || value === "prime-agent") return "prime";
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function harnessLabel(harness) {
|
|
37
|
+
return (HARNESSES[harness] && HARNESSES[harness].label) || harness;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bundleDirFor(harness) {
|
|
41
|
+
return path.join(__dirname, "..", "runtime", HARNESSES[harness].bundle);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function listFiles(dir, prefix = "") {
|
|
45
|
+
if (!fs.existsSync(dir)) return [];
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
48
|
+
const rel = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
49
|
+
const full = path.join(dir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
result.push(...listFiles(full, rel));
|
|
52
|
+
} else if (entry.isFile()) {
|
|
53
|
+
result.push(rel);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pathExistsOrSymlink(file) {
|
|
60
|
+
try {
|
|
61
|
+
fs.lstatSync(file);
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function copyMissingFile(src, dest) {
|
|
69
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
70
|
+
fs.copyFileSync(src, dest);
|
|
71
|
+
try {
|
|
72
|
+
const mode = fs.statSync(src).mode & 0o777;
|
|
73
|
+
fs.chmodSync(dest, mode);
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Required paths come from the harness module so the CLI and the launcher's
|
|
78
|
+
// pre-launch check agree on what "support present" means.
|
|
79
|
+
function requiredPathsFor(harness) {
|
|
80
|
+
const mod = require(HARNESSES[harness].module);
|
|
81
|
+
const checker = mod.hasCodexRuntimeSupport || mod.hasPiRuntimeSupport || mod.hasPrimeRuntimeSupport;
|
|
82
|
+
return { hasSupport: (dir) => Boolean(checker && checker(dir)) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function manifestPath(egregoreDir, harness) {
|
|
86
|
+
return path.join(egregoreDir, ".egregore", `${harness}-harness-add.json`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Current Codex discovers project skills via .agents/skills -> ../.codex/skills.
|
|
90
|
+
// Create the link only when nothing is there; never replace a real directory.
|
|
91
|
+
function ensureAgentsSkillsLink(egregoreDir, summary) {
|
|
92
|
+
const agentsDir = path.join(egregoreDir, ".agents");
|
|
93
|
+
const link = path.join(agentsDir, "skills");
|
|
94
|
+
if (pathExistsOrSymlink(link)) return;
|
|
95
|
+
try {
|
|
96
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
97
|
+
fs.symlinkSync(path.join("..", ".codex", "skills"), link);
|
|
98
|
+
summary.installed.push(".agents/skills");
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Copy every file of `harness`'s packaged bundle that is missing from
|
|
104
|
+
* `egregoreDir`. Existing files are left untouched regardless of content.
|
|
105
|
+
*
|
|
106
|
+
* Returns { harness, installed: [rel...], present: n, ready: bool }.
|
|
107
|
+
*/
|
|
108
|
+
function addHarnessRuntime(egregoreDir, harnessName) {
|
|
109
|
+
const harness = normalizeHarness(harnessName);
|
|
110
|
+
if (!harness) {
|
|
111
|
+
throw new Error(`Unknown harness "${harnessName}". Expected codex, pi, or prime.`);
|
|
112
|
+
}
|
|
113
|
+
const bundleDir = bundleDirFor(harness);
|
|
114
|
+
if (!fs.existsSync(bundleDir)) {
|
|
115
|
+
throw new Error(`This create-egregore release ships no ${harnessLabel(harness)} bundle (${bundleDir}).`);
|
|
116
|
+
}
|
|
117
|
+
if (!fs.existsSync(path.join(egregoreDir, "egregore.json"))) {
|
|
118
|
+
throw new Error(`Not an Egregore instance: ${egregoreDir} (no egregore.json).`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const summary = { harness, installed: [], present: 0, ready: false };
|
|
122
|
+
for (const rel of listFiles(bundleDir)) {
|
|
123
|
+
if (rel === "manifest.json") continue;
|
|
124
|
+
const dest = path.join(egregoreDir, rel);
|
|
125
|
+
if (pathExistsOrSymlink(dest)) {
|
|
126
|
+
summary.present += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
copyMissingFile(path.join(bundleDir, rel), dest);
|
|
130
|
+
summary.installed.push(rel);
|
|
131
|
+
}
|
|
132
|
+
if (harness === "codex") ensureAgentsSkillsLink(egregoreDir, summary);
|
|
133
|
+
|
|
134
|
+
summary.ready = requiredPathsFor(harness).hasSupport(egregoreDir);
|
|
135
|
+
|
|
136
|
+
if (summary.installed.length > 0) {
|
|
137
|
+
const file = manifestPath(egregoreDir, harness);
|
|
138
|
+
const previous = (() => {
|
|
139
|
+
try { return JSON.parse(fs.readFileSync(file, "utf-8")); } catch { return { files: [] }; }
|
|
140
|
+
})();
|
|
141
|
+
const files = Array.from(new Set([...(previous.files || []), ...summary.installed])).sort();
|
|
142
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
143
|
+
fs.writeFileSync(file, JSON.stringify({
|
|
144
|
+
package: PACKAGE.name,
|
|
145
|
+
version: PACKAGE.version,
|
|
146
|
+
harness,
|
|
147
|
+
addedAt: new Date().toISOString(),
|
|
148
|
+
files,
|
|
149
|
+
}, null, 2) + "\n");
|
|
150
|
+
}
|
|
151
|
+
return summary;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
HARNESSES,
|
|
156
|
+
addHarnessRuntime,
|
|
157
|
+
harnessLabel,
|
|
158
|
+
normalizeHarness,
|
|
159
|
+
};
|
|
@@ -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
|
+
`Add it with: npx -y create-egregore@${require("../package.json").version} --add-harness codex (run inside the instance; missing files only), 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
|
+
`Add it with: npx -y create-egregore@${require("../package.json").version} --add-harness pi (run inside the instance; missing files only), 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
|
+
`Add it with: npx -y create-egregore@${require("../package.json").version} --add-harness prime (run inside the instance; missing files only), 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
|
}
|
|
@@ -1544,6 +1615,9 @@ module.exports = {
|
|
|
1544
1615
|
redactToken,
|
|
1545
1616
|
configureGitCredentials,
|
|
1546
1617
|
gitCloneEnv,
|
|
1618
|
+
authenticatedGitEnv,
|
|
1619
|
+
verifyGitAccess,
|
|
1620
|
+
syncAuthenticatedRepository,
|
|
1547
1621
|
assistantInstallCommand,
|
|
1548
1622
|
initMemoryDirs,
|
|
1549
1623
|
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.20.
|
|
3
|
+
"version": "0.20.5",
|
|
4
4
|
"description": "Set up Egregore for your team in one command \u2014 installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build:runtime": "node ../../bin/capability-distribution.mjs generate-skill-view --root ../.. && node scripts/build-runtime-bundle.mjs && node scripts/build-pi-runtime-bundle.mjs && node scripts/build-prime-runtime-bundle.mjs && node ../../api/scripts/build-connect-runtime-pack.mjs",
|
|
14
14
|
"build:pi-runtime": "node scripts/build-pi-runtime-bundle.mjs",
|
|
15
15
|
"build:prime-runtime": "node scripts/build-prime-runtime-bundle.mjs",
|
|
16
|
-
"test": "node ../../bin/capability-distribution.mjs validate --root ../.. && node scripts/build-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/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/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/prime-session-smoke.test.js test/pi-product-workflows.test.js test/installer-runtime-source.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-permissions.test.js test/launcher-update.test.js test/connected-config.test.js test/workflow-settings.test.js",
|
|
16
|
+
"test": "node ../../bin/capability-distribution.mjs validate --root ../.. && node scripts/build-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/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/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/prime-session-smoke.test.js test/pi-product-workflows.test.js test/installer-runtime-source.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-permissions.test.js test/launcher-update.test.js test/harness-add.test.js test/connected-config.test.js test/workflow-settings.test.js",
|
|
17
17
|
"test:e2e": "node --test test/e2e-local.test.js",
|
|
18
18
|
"test:all": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/"
|
|
19
19
|
},
|