create-egregore 0.17.4 → 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.
Files changed (110) hide show
  1. package/assets/egregore-launcher.js +1965 -0
  2. package/assets/egregore-launcher.sh +385 -0
  3. package/bin/cli.js +472 -81
  4. package/lib/api.js +2 -2
  5. package/lib/auth.js +255 -20
  6. package/lib/codex-runtime.js +268 -0
  7. package/lib/commands.js +31 -0
  8. package/lib/context.js +1 -1
  9. package/lib/local.js +616 -189
  10. package/lib/setup.js +822 -84
  11. package/lib/ui.js +780 -84
  12. package/package.json +11 -3
  13. package/runtime/codex/.codex/config.toml +15 -0
  14. package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
  15. package/runtime/codex/.codex/hooks.json +17 -0
  16. package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
  17. package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
  18. package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
  19. package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
  20. package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
  21. package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
  22. package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
  23. package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
  24. package/runtime/codex/.codex/skills/comment/SKILL.md +25 -0
  25. package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
  26. package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
  27. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
  28. package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
  29. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
  30. package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
  31. package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
  32. package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
  33. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
  34. package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
  35. package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
  36. package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
  37. package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
  38. package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
  39. package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
  40. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
  41. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  42. package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
  43. package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
  44. package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
  45. package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
  46. package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
  47. package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
  48. package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
  49. package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
  50. package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
  51. package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
  52. package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
  53. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
  54. package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
  55. package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
  56. package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
  57. package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
  58. package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
  59. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
  60. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
  61. package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
  62. package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
  63. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
  64. package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
  65. package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
  66. package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
  67. package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
  68. package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
  69. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
  70. package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
  71. package/runtime/codex/.codex/spec-manifest.json +108 -0
  72. package/runtime/codex/AGENTS.md +328 -0
  73. package/runtime/codex/bin/activity-data.sh +180 -0
  74. package/runtime/codex/bin/agent.sh +669 -0
  75. package/runtime/codex/bin/codex-install-skills.sh +78 -0
  76. package/runtime/codex/bin/codex-render-spec.mjs +308 -0
  77. package/runtime/codex/bin/codex-session-start.sh +238 -0
  78. package/runtime/codex/bin/codex-skill-render.mjs +315 -0
  79. package/runtime/codex/bin/codex-sync-skills.sh +223 -0
  80. package/runtime/codex/bin/dashboard-data.sh +178 -0
  81. package/runtime/codex/bin/graph-batch.sh +60 -0
  82. package/runtime/codex/bin/graph-op.sh +397 -0
  83. package/runtime/codex/bin/graph-wal.sh +228 -0
  84. package/runtime/codex/bin/graph.sh +141 -0
  85. package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
  86. package/runtime/codex/bin/handoff-run.sh +569 -0
  87. package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
  88. package/runtime/codex/bin/index-handoff.sh +306 -0
  89. package/runtime/codex/bin/lib/artifact-id.sh +46 -0
  90. package/runtime/codex/bin/lib/config.sh +71 -0
  91. package/runtime/codex/bin/lib/context.sh +511 -0
  92. package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
  93. package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
  94. package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
  95. package/runtime/codex/bin/lib/git-sync.sh +262 -0
  96. package/runtime/codex/bin/lib/greeting.sh +432 -0
  97. package/runtime/codex/bin/lib/hash.sh +13 -0
  98. package/runtime/codex/bin/lib/identity.sh +164 -0
  99. package/runtime/codex/bin/lib/permissions.sh +106 -0
  100. package/runtime/codex/bin/lib/time.sh +56 -0
  101. package/runtime/codex/bin/lib/worktree-links.sh +54 -0
  102. package/runtime/codex/bin/notify.sh +207 -0
  103. package/runtime/codex/bin/publish-artifact.sh +187 -0
  104. package/runtime/codex/bin/publish-references.sh +144 -0
  105. package/runtime/codex/bin/repo-state.sh +99 -0
  106. package/runtime/codex/bin/telemetry.sh +350 -0
  107. package/runtime/codex/bin/worktree-create.sh +115 -0
  108. package/runtime/codex/bin/worktree-remove.sh +50 -0
  109. package/runtime/codex/bin/worktree.sh +199 -0
  110. package/runtime/codex/manifest.json +162 -0
package/lib/api.js CHANGED
@@ -95,10 +95,10 @@ class EgregoreAPI {
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
- const GITHUB_APP_CLIENT_ID = "Iv23li2obNsAjakoK2RE"; // GitHub App — founder flow (clean install screen)
9
- const OAUTH_APP_CLIENT_ID = "Ov23liYZjJbbbNgnJ9vH"; // OAuth App — join flow (repo:invite only)
9
+ /**
10
+ * 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:invite scope
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
- const clientId = isJoin ? OAUTH_APP_CLIENT_ID : GITHUB_APP_CLIENT_ID;
103
- const scope = isJoin ? "repo:invite" : "";
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
- const verifyUrl = codeResp.verification_uri_complete || verification_uri;
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 + open browser
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
- ui.info("A GitHub page will open. Just click Authorize — you'll choose");
135
- ui.info("where to set up Egregore in the next step.");
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(`Code copied to clipboard: ${ui.bold(user_code)}`);
353
+ ui.info(` 2. Enter this code (copied to clipboard): ${ui.bold(user_code)}`);
139
354
  } else {
140
- ui.info(`Your code: ${ui.bold(user_code)}`);
355
+ ui.info(` 2. Enter this code: ${ui.bold(user_code)}`);
141
356
  }
142
- ui.info("Opening browser...");
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. Transient network errors (one bad packet, WiFi blip,
148
- // GitHub edge restart) must NOT kill the whole flow — the user might
149
- // be mid-authorize on the browser side. Catch per-poll errors and
150
- // continue; the outer 5-minute timeout handles genuinely broken networks.
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
- `If the browser tab didn't open, visit the verification URL manually and enter your code.\n` +
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 = { deviceFlow, openBrowser };
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
+ };
@@ -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
  */