create-egregore 0.17.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/egregore-launcher.js +1965 -0
- package/assets/egregore-launcher.sh +385 -0
- package/bin/cli.js +472 -81
- package/lib/api.js +3 -3
- package/lib/auth.js +255 -20
- package/lib/codex-runtime.js +268 -0
- package/lib/commands.js +31 -0
- package/lib/context.js +1 -1
- package/lib/local.js +618 -193
- package/lib/setup.js +837 -85
- package/lib/ui.js +780 -84
- package/package.json +17 -4
- package/runtime/codex/.codex/config.toml +15 -0
- package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
- package/runtime/codex/.codex/hooks.json +17 -0
- package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
- package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
- package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
- package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/comment/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
- package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
- package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
- package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
- package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
- package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
- package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
- package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
- package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
- package/runtime/codex/.codex/spec-manifest.json +108 -0
- package/runtime/codex/AGENTS.md +328 -0
- package/runtime/codex/bin/activity-data.sh +180 -0
- package/runtime/codex/bin/agent.sh +669 -0
- package/runtime/codex/bin/codex-install-skills.sh +78 -0
- package/runtime/codex/bin/codex-render-spec.mjs +308 -0
- package/runtime/codex/bin/codex-session-start.sh +238 -0
- package/runtime/codex/bin/codex-skill-render.mjs +315 -0
- package/runtime/codex/bin/codex-sync-skills.sh +223 -0
- package/runtime/codex/bin/dashboard-data.sh +178 -0
- package/runtime/codex/bin/graph-batch.sh +60 -0
- package/runtime/codex/bin/graph-op.sh +397 -0
- package/runtime/codex/bin/graph-wal.sh +228 -0
- package/runtime/codex/bin/graph.sh +141 -0
- package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/codex/bin/handoff-run.sh +569 -0
- package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
- package/runtime/codex/bin/index-handoff.sh +306 -0
- package/runtime/codex/bin/lib/artifact-id.sh +46 -0
- package/runtime/codex/bin/lib/config.sh +71 -0
- package/runtime/codex/bin/lib/context.sh +511 -0
- package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
- package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
- package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
- package/runtime/codex/bin/lib/git-sync.sh +262 -0
- package/runtime/codex/bin/lib/greeting.sh +432 -0
- package/runtime/codex/bin/lib/hash.sh +13 -0
- package/runtime/codex/bin/lib/identity.sh +164 -0
- package/runtime/codex/bin/lib/permissions.sh +106 -0
- package/runtime/codex/bin/lib/time.sh +56 -0
- package/runtime/codex/bin/lib/worktree-links.sh +54 -0
- package/runtime/codex/bin/notify.sh +207 -0
- package/runtime/codex/bin/publish-artifact.sh +187 -0
- package/runtime/codex/bin/publish-references.sh +144 -0
- package/runtime/codex/bin/repo-state.sh +99 -0
- package/runtime/codex/bin/telemetry.sh +350 -0
- package/runtime/codex/bin/worktree-create.sh +115 -0
- package/runtime/codex/bin/worktree-remove.sh +50 -0
- package/runtime/codex/bin/worktree.sh +199 -0
- package/runtime/codex/manifest.json +162 -0
package/lib/api.js
CHANGED
|
@@ -88,17 +88,17 @@ class EgregoreAPI {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async joinOrg(githubToken, { github_org, repo_name = "egregore
|
|
91
|
+
async joinOrg(githubToken, { github_org, repo_name = "egregore" }) {
|
|
92
92
|
return request("POST", `${this.base}/api/org/join`, {
|
|
93
93
|
headers: { Authorization: `Bearer ${githubToken}` },
|
|
94
94
|
body: { github_org, repo_name },
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
async registerOrg(githubToken, { org_name, github_org, slug }) {
|
|
98
|
+
async registerOrg(githubToken, { org_name, github_org, slug, flow_variant }) {
|
|
99
99
|
return request("POST", `${this.base}/api/org/register`, {
|
|
100
100
|
headers: { Authorization: `Bearer ${githubToken}` },
|
|
101
|
-
body: { org_name, github_org, slug },
|
|
101
|
+
body: { org_name, github_org, slug, ...(flow_variant ? { flow_variant } : {}) },
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
|
package/lib/auth.js
CHANGED
|
@@ -4,9 +4,201 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const https = require("node:https");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Try to get a token from the user's `gh` CLI, if installed and authenticated.
|
|
11
|
+
* Returns { token, scopes, user, hasRepoScope } on success, null when gh is
|
|
12
|
+
* missing/not authenticated/no token.
|
|
13
|
+
*
|
|
14
|
+
* Skips the Egregore GitHub App install step entirely — the gh-issued token
|
|
15
|
+
* has the scopes we need for founder/join flows. Matches the pattern in
|
|
16
|
+
* bin/init-gh.sh.
|
|
17
|
+
*
|
|
18
|
+
* Respects EGREGORE_SKIP_GH=1 to force device flow even when gh is present
|
|
19
|
+
* (useful for testing the fallback path).
|
|
20
|
+
*/
|
|
21
|
+
function envGhAuth() {
|
|
22
|
+
const envToken = (process.env.EGREGORE_GITHUB_TOKEN || "").trim();
|
|
23
|
+
if (!envToken) return null;
|
|
24
|
+
return {
|
|
25
|
+
token: envToken,
|
|
26
|
+
scopes: [],
|
|
27
|
+
user: process.env.EGREGORE_GITHUB_USER || null,
|
|
28
|
+
hasRepoScope: null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ghCliExists() {
|
|
33
|
+
if (process.env.EGREGORE_SKIP_GH === "1") return false;
|
|
34
|
+
const which = spawnSync("which", ["gh"], { stdio: "pipe" });
|
|
35
|
+
return which.status === 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function canUseInteractiveBrowserFlow() {
|
|
39
|
+
return !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Canonical, cross-platform install page — always correct regardless of OS.
|
|
43
|
+
const GH_INSTALL_URL = "https://cli.github.com";
|
|
44
|
+
|
|
45
|
+
// The most reliable one-line install for the current OS. Linux package names
|
|
46
|
+
// and repos vary too much for a single command, so we point at the canonical
|
|
47
|
+
// page there (and always print GH_INSTALL_URL alongside on every platform).
|
|
48
|
+
function _has(b) {
|
|
49
|
+
try { return spawnSync("which", [b], { stdio: "pipe" }).status === 0; }
|
|
50
|
+
catch { return false; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// True on a machine with no way to open a browser (server / SSH). macOS and
|
|
54
|
+
// Windows always have one; on Linux we need a display AND a browser opener.
|
|
55
|
+
function isHeadless() {
|
|
56
|
+
if (process.platform === "darwin" || process.platform === "win32") return false;
|
|
57
|
+
const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
58
|
+
const hasOpener = ["xdg-open", "x-www-browser", "www-browser", "wslview"].some(_has);
|
|
59
|
+
return !hasDisplay && !hasOpener;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Human-readable install command for the current OS (for manual instructions).
|
|
63
|
+
function ghInstallCommand() {
|
|
64
|
+
if (process.platform === "darwin") return "brew install gh";
|
|
65
|
+
if (process.platform === "win32") return "winget install --id GitHub.cli";
|
|
66
|
+
if (_has("apt-get") || _has("apt")) return "sudo apt install gh";
|
|
67
|
+
if (_has("dnf")) return "sudo dnf install gh";
|
|
68
|
+
if (_has("pacman")) return "sudo pacman -S github-cli";
|
|
69
|
+
if (_has("zypper")) return "sudo zypper install gh";
|
|
70
|
+
if (_has("brew")) return "brew install gh";
|
|
71
|
+
return "install the 'gh' package";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Runnable, non-interactive install command for the current OS — or null when
|
|
75
|
+
// we can't safely auto-install (unknown package manager, or brew/winget absent).
|
|
76
|
+
function ghInstallShellCommand() {
|
|
77
|
+
if (process.platform === "darwin") return _has("brew") ? "brew install gh" : null;
|
|
78
|
+
if (process.platform === "win32") return _has("winget") ? "winget install --id GitHub.cli --silent" : null;
|
|
79
|
+
if (_has("apt-get")) return "sudo apt-get install -y gh";
|
|
80
|
+
if (_has("dnf")) return "sudo dnf install -y gh";
|
|
81
|
+
if (_has("pacman")) return "sudo pacman -S --noconfirm github-cli";
|
|
82
|
+
if (_has("zypper")) return "sudo zypper install -y gh";
|
|
83
|
+
if (_has("brew")) return "brew install gh";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Run the OS install command (the user sees it + any sudo prompt), then start
|
|
88
|
+
// gh's own first-party login. Returns the gh token on success, null otherwise.
|
|
89
|
+
// gh is GitHub's CLI, so login uses GitHub's own screen — never an Egregore one.
|
|
90
|
+
function runGhInstallAndLogin(ui, cmd) {
|
|
91
|
+
ui?.info?.("");
|
|
92
|
+
ui?.info?.(`Running ${ui.bold ? ui.bold(cmd) : cmd}`);
|
|
93
|
+
ui?.info?.("");
|
|
94
|
+
const res = spawnSync("bash", ["-c", cmd], { stdio: "inherit", env: process.env });
|
|
95
|
+
if (res.status !== 0 || !_has("gh")) {
|
|
96
|
+
ui?.warn?.("Couldn't install GitHub CLI automatically.");
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
ui?.success?.("GitHub CLI installed.");
|
|
100
|
+
const auth = runGhAuthLogin(ui);
|
|
101
|
+
return auth ? auth.token : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function explainNoGhHeadless(ui) {
|
|
105
|
+
const bold = (s) => (ui.bold ? ui.bold(s) : s);
|
|
106
|
+
ui?.error?.("GitHub CLI is not installed, and this environment is not interactive.");
|
|
107
|
+
ui?.info?.("Install GitHub CLI, authenticate once, then re-run this command:");
|
|
108
|
+
ui?.info?.(` ${bold(ghInstallCommand())}`);
|
|
109
|
+
ui?.info?.(` ${bold("gh auth login")}`);
|
|
110
|
+
ui?.info?.(` Install for any OS: ${GH_INSTALL_URL}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function tryGhAuth() {
|
|
114
|
+
if (process.env.EGREGORE_SKIP_GH === "1") return null;
|
|
115
|
+
|
|
116
|
+
const envAuth = envGhAuth();
|
|
117
|
+
if (envAuth) return envAuth;
|
|
118
|
+
|
|
119
|
+
if (!ghCliExists()) return null;
|
|
120
|
+
|
|
121
|
+
// gh auth status writes to stderr; capture both.
|
|
122
|
+
const status = spawnSync("gh", ["auth", "status"], { stdio: "pipe", encoding: "utf-8" });
|
|
123
|
+
if (status.status !== 0) return null;
|
|
124
|
+
const statusText = `${status.stdout || ""}${status.stderr || ""}`;
|
|
125
|
+
|
|
126
|
+
const tokenRun = spawnSync("gh", ["auth", "token"], { stdio: "pipe", encoding: "utf-8" });
|
|
127
|
+
if (tokenRun.status !== 0) return null;
|
|
128
|
+
const token = (tokenRun.stdout || "").trim();
|
|
129
|
+
if (!token) return null;
|
|
130
|
+
|
|
131
|
+
// Parse declared token scopes when gh exposes them. Some gh versions and
|
|
132
|
+
// fine-grained auth paths do not print a classic Token scopes line. Treat
|
|
133
|
+
// that as "unknown" and continue with the first-party gh token instead of
|
|
134
|
+
// bouncing a correctly authenticated user into the Egregore App install.
|
|
135
|
+
const scopeMatch = statusText.match(/Token scopes:\s*(.+)/);
|
|
136
|
+
const scopes = scopeMatch
|
|
137
|
+
? scopeMatch[1].split(",").map((s) => s.trim().replace(/['"]/g, ""))
|
|
138
|
+
: [];
|
|
139
|
+
const hasRepoScope = scopes.some((s) => s === "repo");
|
|
140
|
+
|
|
141
|
+
// Extract user from status line: "Logged in to github.com account <login>"
|
|
142
|
+
const userMatch = statusText.match(/account\s+(\S+)/);
|
|
143
|
+
const user = userMatch ? userMatch[1] : null;
|
|
144
|
+
|
|
145
|
+
return { token, scopes, user, hasRepoScope: scopeMatch ? hasRepoScope : null };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function runGhAuthLogin(ui, opts = {}) {
|
|
149
|
+
const envAuth = envGhAuth();
|
|
150
|
+
if (envAuth) return envAuth;
|
|
151
|
+
if (!ghCliExists()) return null;
|
|
152
|
+
|
|
153
|
+
const scopes = opts.scopes || "repo,read:org";
|
|
154
|
+
const args = ["auth", "login", "--hostname", "github.com", "--git-protocol", "https", "--scopes", scopes];
|
|
155
|
+
|
|
156
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
157
|
+
ui?.info?.("GitHub CLI is installed but not authenticated.");
|
|
158
|
+
ui?.info?.(`Run ${ui.bold ? ui.bold("gh auth login") : "gh auth login"} and then re-run this command.`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const cyan = (s) => (ui.cyan ? ui.cyan(s) : s);
|
|
163
|
+
const bold = (s) => (ui.bold ? ui.bold(s) : s);
|
|
164
|
+
ui?.info?.("GitHub CLI is installed but not authenticated.");
|
|
165
|
+
ui?.info?.(`Starting ${bold("gh auth login")} so Egregore can use your local GitHub session.`);
|
|
166
|
+
ui?.info?.("");
|
|
167
|
+
|
|
168
|
+
// On a headless box (server/SSH, no browser) gh's "open browser" step errors
|
|
169
|
+
// and the manual link only appears AFTER the failure. Force gh's browser
|
|
170
|
+
// opener to just PRINT the URL instead — so the device link shows right at the
|
|
171
|
+
// moment of action, no error. On a laptop we leave it so the real browser opens.
|
|
172
|
+
const env = { ...process.env };
|
|
173
|
+
if (isHeadless()) {
|
|
174
|
+
env.BROWSER = "echo";
|
|
175
|
+
ui?.info?.(`No browser on this machine — when gh prompts, open the printed link`);
|
|
176
|
+
ui?.info?.(`(${cyan("https://github.com/login/device")}) and enter the one-time code yourself.`);
|
|
177
|
+
} else {
|
|
178
|
+
ui?.info?.(`A browser will open to sign in. If it doesn't, open`);
|
|
179
|
+
ui?.info?.(`${cyan("https://github.com/login/device")} and enter the one-time code gh shows.`);
|
|
180
|
+
}
|
|
181
|
+
ui?.info?.("");
|
|
182
|
+
|
|
183
|
+
const result = spawnSync("gh", args, { stdio: "inherit", env });
|
|
184
|
+
if (result.status !== 0) return null;
|
|
185
|
+
|
|
186
|
+
return tryGhAuth();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function tryGhAuthOrLogin(ui, opts = {}) {
|
|
190
|
+
const existing = tryGhAuth();
|
|
191
|
+
if (existing) return existing;
|
|
192
|
+
if (!ghCliExists()) return null;
|
|
193
|
+
return runGhAuthLogin(ui, opts);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// OAuth App — used for ALL device-flow auth (founder + join). We deliberately
|
|
197
|
+
// do NOT use a GitHub App here: a GitHub App device flow surfaces the App
|
|
198
|
+
// install/authorize screen, which blocked installs in the field. The OAuth App
|
|
199
|
+
// device flow lets the user paste a code at github.com/login/device — no popup,
|
|
200
|
+
// no App authorization page.
|
|
201
|
+
const OAUTH_APP_CLIENT_ID = "Ov23liYZjJbbbNgnJ9vH";
|
|
10
202
|
const SUPPORT_URL = "https://github.com/egregore-labs/egregore";
|
|
11
203
|
|
|
12
204
|
function post(url, body) {
|
|
@@ -95,12 +287,27 @@ function copyToClipboard(text) {
|
|
|
95
287
|
* Run GitHub Device Flow.
|
|
96
288
|
* Returns the access token string, or throws on failure.
|
|
97
289
|
* @param {object} [opts] - Options
|
|
98
|
-
* @param {string} [opts.mode] - "join" uses OAuth App with repo
|
|
290
|
+
* @param {string} [opts.mode] - "join" uses OAuth App with repo access
|
|
99
291
|
*/
|
|
100
292
|
async function deviceFlow(ui, opts = {}) {
|
|
293
|
+
if (!canUseInteractiveBrowserFlow()) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
"GitHub browser/device authentication requires an interactive terminal. " +
|
|
296
|
+
"Install GitHub CLI and run: gh auth login"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
101
300
|
const isJoin = opts.mode === "join";
|
|
102
|
-
|
|
103
|
-
|
|
301
|
+
// Always the OAuth App — never the GitHub App (see OAUTH_APP_CLIENT_ID note).
|
|
302
|
+
// Default to ONLY `repo` — the minimum to create/clone private repos. We
|
|
303
|
+
// deliberately omit `read:org`: it adds an "Organizations and teams" line and
|
|
304
|
+
// org-spanning Grant/Request prompts to the consent screen (the exact
|
|
305
|
+
// third-party-app friction we're escaping). Org enumeration is a gh-only
|
|
306
|
+
// affordance; the local device-flow path stays in the personal account.
|
|
307
|
+
// Connected mode (hosted) enumerates orgs server-side and opts into read:org
|
|
308
|
+
// explicitly via opts.scope.
|
|
309
|
+
const clientId = OAUTH_APP_CLIENT_ID;
|
|
310
|
+
const scope = opts.scope || "repo";
|
|
104
311
|
|
|
105
312
|
// 1. Request device code (with retry — handles transient network hiccups
|
|
106
313
|
// and GitHub edge server blips that caused ECONNREFUSED in the field)
|
|
@@ -126,34 +333,44 @@ async function deviceFlow(ui, opts = {}) {
|
|
|
126
333
|
}
|
|
127
334
|
|
|
128
335
|
const { device_code, user_code, verification_uri, interval: pollInterval } = codeResp;
|
|
129
|
-
|
|
336
|
+
// Device-entry page (NOT verification_uri_complete — we want the user to type
|
|
337
|
+
// the code, not follow a pre-filled link, so no browser is auto-opened).
|
|
338
|
+
const verifyUrl = verification_uri || "https://github.com/login/device";
|
|
130
339
|
|
|
131
|
-
// 2. Show code
|
|
340
|
+
// 2. Show the code for MANUAL entry. We do NOT open a browser — auto-opening
|
|
341
|
+
// is what made this feel like the old GitHub App popup. The user opens the
|
|
342
|
+
// page themselves and pastes the code (already on their clipboard).
|
|
132
343
|
const copied = copyToClipboard(user_code);
|
|
133
344
|
ui.info("");
|
|
134
|
-
|
|
135
|
-
|
|
345
|
+
if (isJoin) {
|
|
346
|
+
ui.info("Sign in to your GitHub to accept the invite and clone the repos:");
|
|
347
|
+
} else {
|
|
348
|
+
ui.info("Sign in to your GitHub — your repos are created in your account and stay yours:");
|
|
349
|
+
}
|
|
136
350
|
ui.info("");
|
|
351
|
+
ui.info(` 1. Open: ${ui.bold(verifyUrl)}`);
|
|
137
352
|
if (copied) {
|
|
138
|
-
ui.info(`
|
|
353
|
+
ui.info(` 2. Enter this code (copied to clipboard): ${ui.bold(user_code)}`);
|
|
139
354
|
} else {
|
|
140
|
-
ui.info(`
|
|
355
|
+
ui.info(` 2. Enter this code: ${ui.bold(user_code)}`);
|
|
141
356
|
}
|
|
142
|
-
ui.info("
|
|
143
|
-
ui.info(`If it doesn't open, visit: ${verifyUrl}`);
|
|
357
|
+
ui.info(" 3. Click Authorize.");
|
|
144
358
|
ui.info("");
|
|
145
|
-
openBrowser(verifyUrl);
|
|
146
359
|
|
|
147
|
-
// 3. Poll for token
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
360
|
+
// 3. Poll for token, with a live heartbeat so the wait never looks hung.
|
|
361
|
+
// Transient network errors (one bad packet, WiFi blip, GitHub edge
|
|
362
|
+
// restart) must NOT kill the whole flow — the user might be mid-authorize
|
|
363
|
+
// on the browser side. Catch per-poll errors and continue; the outer
|
|
364
|
+
// 5-minute timeout handles genuinely broken networks.
|
|
151
365
|
let interval = (pollInterval || 5) * 1000;
|
|
152
366
|
const timeout = 300000; // 5 minutes
|
|
153
367
|
const start = Date.now();
|
|
154
368
|
let consecutiveNetworkErrors = 0;
|
|
155
369
|
let networkWarningShown = false;
|
|
156
370
|
|
|
371
|
+
const waitMsg = "Waiting for you to authorize — the page stays open until you do";
|
|
372
|
+
let waitSpin = ui.spinner(waitMsg);
|
|
373
|
+
|
|
157
374
|
while (Date.now() - start < timeout) {
|
|
158
375
|
await sleep(interval);
|
|
159
376
|
|
|
@@ -170,16 +387,19 @@ async function deviceFlow(ui, opts = {}) {
|
|
|
170
387
|
// After 3 consecutive failures, surface a note so the user knows
|
|
171
388
|
// something is off — but keep polling until the outer timeout.
|
|
172
389
|
if (consecutiveNetworkErrors >= 3 && !networkWarningShown) {
|
|
390
|
+
waitSpin.clear();
|
|
173
391
|
ui.warn(
|
|
174
392
|
`Network trouble reaching GitHub (${err.message}). Still trying.\n` +
|
|
175
393
|
`If this persists, report at ${SUPPORT_URL}/issues`
|
|
176
394
|
);
|
|
177
395
|
networkWarningShown = true;
|
|
396
|
+
waitSpin = ui.spinner(waitMsg); // resume the heartbeat
|
|
178
397
|
}
|
|
179
398
|
continue;
|
|
180
399
|
}
|
|
181
400
|
|
|
182
401
|
if (tokenResp.access_token) {
|
|
402
|
+
waitSpin.clear(); // caller prints its own "Authenticated" line
|
|
183
403
|
return tokenResp.access_token;
|
|
184
404
|
}
|
|
185
405
|
|
|
@@ -191,17 +411,32 @@ async function deviceFlow(ui, opts = {}) {
|
|
|
191
411
|
continue;
|
|
192
412
|
}
|
|
193
413
|
|
|
414
|
+
waitSpin.fail("Authorization failed");
|
|
194
415
|
throw new Error(
|
|
195
416
|
`${tokenResp.error_description || tokenResp.error || "Auth failed"}\n` +
|
|
196
417
|
`If you need help, see ${SUPPORT_URL}`
|
|
197
418
|
);
|
|
198
419
|
}
|
|
199
420
|
|
|
421
|
+
waitSpin.fail("Timed out");
|
|
200
422
|
throw new Error(
|
|
201
423
|
`Timed out waiting for authorization (5 minutes).\n` +
|
|
202
|
-
`
|
|
424
|
+
`Open https://github.com/login/device and enter the code shown above.\n` +
|
|
203
425
|
`If this keeps happening, report at ${SUPPORT_URL}/issues`
|
|
204
426
|
);
|
|
205
427
|
}
|
|
206
428
|
|
|
207
|
-
module.exports = {
|
|
429
|
+
module.exports = {
|
|
430
|
+
canUseInteractiveBrowserFlow,
|
|
431
|
+
deviceFlow,
|
|
432
|
+
explainNoGhHeadless,
|
|
433
|
+
GH_INSTALL_URL,
|
|
434
|
+
ghCliExists,
|
|
435
|
+
ghInstallCommand,
|
|
436
|
+
ghInstallShellCommand,
|
|
437
|
+
openBrowser,
|
|
438
|
+
runGhAuthLogin,
|
|
439
|
+
runGhInstallAndLogin,
|
|
440
|
+
tryGhAuth,
|
|
441
|
+
tryGhAuthOrLogin,
|
|
442
|
+
};
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
const PACKAGE = require("../package.json");
|
|
8
|
+
|
|
9
|
+
const BUNDLE_DIR = path.join(__dirname, "..", "runtime", "codex");
|
|
10
|
+
const BUNDLE_MANIFEST = path.join(BUNDLE_DIR, "manifest.json");
|
|
11
|
+
const LOCAL_MANIFEST = path.join(".egregore", "codex-runtime-manifest.json");
|
|
12
|
+
|
|
13
|
+
const REQUIRED_RUNTIME_PATHS = [
|
|
14
|
+
"AGENTS.md",
|
|
15
|
+
".codex/config.toml",
|
|
16
|
+
".codex/hooks.json",
|
|
17
|
+
".codex/hooks/branch-guard.js",
|
|
18
|
+
".codex/skills",
|
|
19
|
+
"bin/agent.sh",
|
|
20
|
+
"bin/codex-install-skills.sh",
|
|
21
|
+
"bin/codex-session-start.sh",
|
|
22
|
+
"bin/codex-render-spec.mjs",
|
|
23
|
+
"bin/codex-skill-render.mjs",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function hashBuffer(buffer) {
|
|
27
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(file, fallback) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function listFiles(dir, prefix = "") {
|
|
39
|
+
if (!fs.existsSync(dir)) return [];
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
42
|
+
const rel = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
43
|
+
const full = path.join(dir, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
result.push(...listFiles(full, rel));
|
|
46
|
+
} else if (entry.isFile()) {
|
|
47
|
+
result.push(rel);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result.sort();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bundleManifest() {
|
|
54
|
+
return readJson(BUNDLE_MANIFEST, { files: [], skills: [], skillCount: 0 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function bundleFiles() {
|
|
58
|
+
return listFiles(BUNDLE_DIR).filter((rel) => rel !== "manifest.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function installManifestPath(egregoreDir) {
|
|
62
|
+
const gitPath = path.join(egregoreDir, ".git");
|
|
63
|
+
try {
|
|
64
|
+
const stat = fs.lstatSync(gitPath);
|
|
65
|
+
if (stat.isDirectory()) {
|
|
66
|
+
return path.join(gitPath, "info", "egregore-codex-runtime.json");
|
|
67
|
+
}
|
|
68
|
+
if (stat.isFile()) {
|
|
69
|
+
const raw = fs.readFileSync(gitPath, "utf-8").trim();
|
|
70
|
+
const match = raw.match(/^gitdir:\s*(.+)$/);
|
|
71
|
+
if (match) {
|
|
72
|
+
const gitDir = path.isAbsolute(match[1])
|
|
73
|
+
? match[1]
|
|
74
|
+
: path.resolve(egregoreDir, match[1]);
|
|
75
|
+
return path.join(gitDir, "info", "egregore-codex-runtime.json");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {}
|
|
79
|
+
return path.join(egregoreDir, LOCAL_MANIFEST);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readInstallManifest(egregoreDir) {
|
|
83
|
+
return readJson(installManifestPath(egregoreDir), { files: {} });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeInstallManifest(egregoreDir, manifest) {
|
|
87
|
+
const manifestPath = installManifestPath(egregoreDir);
|
|
88
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
89
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applySourceMode(src, dest) {
|
|
93
|
+
try {
|
|
94
|
+
fs.chmodSync(dest, fs.statSync(src).mode & 0o777);
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function copyFile(src, dest) {
|
|
99
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
100
|
+
fs.copyFileSync(src, dest);
|
|
101
|
+
applySourceMode(src, dest);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function installAgentsSkillsLink(egregoreDir, summary) {
|
|
105
|
+
const agentsDir = path.join(egregoreDir, ".agents");
|
|
106
|
+
const linkPath = path.join(agentsDir, "skills");
|
|
107
|
+
const target = "../.codex/skills";
|
|
108
|
+
|
|
109
|
+
if (pathExistsOrSymlink(linkPath)) {
|
|
110
|
+
try {
|
|
111
|
+
const stat = fs.lstatSync(linkPath);
|
|
112
|
+
if (stat.isSymbolicLink() && fs.readlinkSync(linkPath) === target) {
|
|
113
|
+
summary.skipped += 1;
|
|
114
|
+
summary.symlink = "skipped";
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
summary.conflicted += 1;
|
|
118
|
+
summary.conflicts.push(".agents/skills");
|
|
119
|
+
summary.symlink = "conflicted";
|
|
120
|
+
return;
|
|
121
|
+
} catch {
|
|
122
|
+
summary.conflicted += 1;
|
|
123
|
+
summary.conflicts.push(".agents/skills");
|
|
124
|
+
summary.symlink = "conflicted";
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
131
|
+
fs.symlinkSync(target, linkPath);
|
|
132
|
+
summary.installed += 1;
|
|
133
|
+
summary.symlink = "installed";
|
|
134
|
+
} catch {
|
|
135
|
+
summary.conflicted += 1;
|
|
136
|
+
summary.conflicts.push(".agents/skills");
|
|
137
|
+
summary.symlink = "conflicted";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pathExistsOrSymlink(file) {
|
|
142
|
+
try {
|
|
143
|
+
fs.lstatSync(file);
|
|
144
|
+
return true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function installCodexRuntime(egregoreDir) {
|
|
151
|
+
const summary = {
|
|
152
|
+
installed: 0,
|
|
153
|
+
updated: 0,
|
|
154
|
+
skipped: 0,
|
|
155
|
+
conflicted: 0,
|
|
156
|
+
conflicts: [],
|
|
157
|
+
files: [],
|
|
158
|
+
bundleSkills: bundleManifest().skills || [],
|
|
159
|
+
bundleSkillCount: bundleManifest().skillCount || 0,
|
|
160
|
+
symlink: "skipped",
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(BUNDLE_DIR)) {
|
|
164
|
+
throw new Error(`Codex runtime bundle missing: ${BUNDLE_DIR}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const previous = readInstallManifest(egregoreDir);
|
|
168
|
+
const next = {
|
|
169
|
+
package: PACKAGE.name,
|
|
170
|
+
version: PACKAGE.version,
|
|
171
|
+
installedAt: new Date().toISOString(),
|
|
172
|
+
files: {},
|
|
173
|
+
};
|
|
174
|
+
const previousFiles = previous.files || {};
|
|
175
|
+
|
|
176
|
+
for (const rel of bundleFiles()) {
|
|
177
|
+
const src = path.join(BUNDLE_DIR, rel);
|
|
178
|
+
const dest = path.join(egregoreDir, rel);
|
|
179
|
+
const sourceBuffer = fs.readFileSync(src);
|
|
180
|
+
const sourceHash = hashBuffer(sourceBuffer);
|
|
181
|
+
const previousHash = previousFiles[rel] && previousFiles[rel].hash;
|
|
182
|
+
const recordSource = () => {
|
|
183
|
+
next.files[rel] = { hash: sourceHash };
|
|
184
|
+
summary.files.push(rel);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (!pathExistsOrSymlink(dest)) {
|
|
188
|
+
copyFile(src, dest);
|
|
189
|
+
summary.installed += 1;
|
|
190
|
+
recordSource();
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let stat;
|
|
195
|
+
try {
|
|
196
|
+
stat = fs.lstatSync(dest);
|
|
197
|
+
} catch {
|
|
198
|
+
summary.conflicted += 1;
|
|
199
|
+
summary.conflicts.push(rel);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!stat.isFile()) {
|
|
204
|
+
summary.conflicted += 1;
|
|
205
|
+
summary.conflicts.push(rel);
|
|
206
|
+
if (previousHash) next.files[rel] = { hash: previousHash };
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const currentHash = hashBuffer(fs.readFileSync(dest));
|
|
211
|
+
if (currentHash === sourceHash) {
|
|
212
|
+
applySourceMode(src, dest);
|
|
213
|
+
summary.skipped += 1;
|
|
214
|
+
recordSource();
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (previousHash && currentHash === previousHash) {
|
|
219
|
+
copyFile(src, dest);
|
|
220
|
+
summary.updated += 1;
|
|
221
|
+
recordSource();
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
summary.conflicted += 1;
|
|
226
|
+
summary.conflicts.push(rel);
|
|
227
|
+
if (previousHash) next.files[rel] = { hash: previousHash };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
installAgentsSkillsLink(egregoreDir, summary);
|
|
231
|
+
writeInstallManifest(egregoreDir, next);
|
|
232
|
+
return summary;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hasCodexRuntimeSupport(egregoreDir) {
|
|
236
|
+
return REQUIRED_RUNTIME_PATHS.every((rel) => fs.existsSync(path.join(egregoreDir, rel)));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function reportCodexRuntimeSummary(summary, ui, opts = {}) {
|
|
240
|
+
if (!summary || !ui) return;
|
|
241
|
+
const label = opts.label || "Codex runtime";
|
|
242
|
+
const includeConflicts = opts.includeConflicts !== false;
|
|
243
|
+
const changed = summary.installed + summary.updated;
|
|
244
|
+
if (changed > 0) {
|
|
245
|
+
ui.success(
|
|
246
|
+
`${label} ready (${summary.installed} installed, ${summary.updated} updated, ${summary.skipped} unchanged)`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (includeConflicts && summary.conflicted > 0) {
|
|
250
|
+
// These "conflicts" are shared framework files your Egregore already ships
|
|
251
|
+
// and keeps current (agent.sh, graph-op.sh, AGENTS.md, …) — we keep yours
|
|
252
|
+
// and only add the Codex-specific files. It's expected, not an error, so
|
|
253
|
+
// say so calmly instead of warning.
|
|
254
|
+
const n = summary.conflicted;
|
|
255
|
+
const line = `${label}: kept ${n} framework file${n === 1 ? "" : "s"} your Egregore already manages.`;
|
|
256
|
+
ui.info(ui.dim ? ui.dim(line) : line);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
BUNDLE_DIR,
|
|
262
|
+
bundleFiles,
|
|
263
|
+
bundleManifest,
|
|
264
|
+
hasCodexRuntimeSupport,
|
|
265
|
+
installCodexRuntime,
|
|
266
|
+
installManifestPath,
|
|
267
|
+
reportCodexRuntimeSummary,
|
|
268
|
+
};
|
package/lib/commands.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Central command surface. create-egregore is the public package — this is the
|
|
4
|
+
// single source of truth for the advertised npx command across the installer
|
|
5
|
+
// and launcher surfaces.
|
|
6
|
+
const PUBLIC_NPX_PACKAGE = "create-egregore";
|
|
7
|
+
|
|
8
|
+
function publicPackageSpec() {
|
|
9
|
+
return `${PUBLIC_NPX_PACKAGE}@latest`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function npxCommand(args = []) {
|
|
13
|
+
return ["npx", "-y", publicPackageSpec(), ...args].filter(Boolean).join(" ");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function joinCommand(owner, repo) {
|
|
17
|
+
const target = repo ? `${owner}/${repo}` : owner;
|
|
18
|
+
return npxCommand(["join", target]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tokenCommand(token) {
|
|
22
|
+
return npxCommand(["--token", token]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
PUBLIC_NPX_PACKAGE,
|
|
27
|
+
publicPackageSpec,
|
|
28
|
+
npxCommand,
|
|
29
|
+
joinCommand,
|
|
30
|
+
tokenCommand,
|
|
31
|
+
};
|
package/lib/context.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Detect whether cwd is inside a git work-tree.
|
|
3
3
|
*
|
|
4
4
|
* Egregore installs as a sibling shell alongside managed repos. If a user runs
|
|
5
|
-
* `npx create-egregore` from inside an existing git repo, we want to redirect
|
|
5
|
+
* `npx -y create-egregore@latest` from inside an existing git repo, we want to redirect
|
|
6
6
|
* them to the repo's parent directory and auto-adopt their repo as a managed
|
|
7
7
|
* repo. This module is the single source of truth for that detection.
|
|
8
8
|
*/
|