create-egregore 0.17.4 → 0.18.1

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 (106) 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 +12 -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/commit/SKILL.md +25 -0
  25. package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
  26. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
  27. package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
  28. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
  29. package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
  30. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
  31. package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
  32. package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
  33. package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
  34. package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
  35. package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
  36. package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
  37. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
  38. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  39. package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
  40. package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
  41. package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
  42. package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
  43. package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
  44. package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
  45. package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
  46. package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
  47. package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
  48. package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
  49. package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
  50. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
  51. package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
  52. package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
  53. package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
  54. package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
  55. package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
  56. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
  57. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
  58. package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
  59. package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
  60. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
  61. package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
  62. package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
  63. package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
  64. package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
  65. package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
  66. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
  67. package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
  68. package/runtime/codex/.codex/spec-manifest.json +108 -0
  69. package/runtime/codex/AGENTS.md +40 -0
  70. package/runtime/codex/bin/activity-data.sh +177 -0
  71. package/runtime/codex/bin/agent.sh +669 -0
  72. package/runtime/codex/bin/codex-install-skills.sh +78 -0
  73. package/runtime/codex/bin/codex-render-spec.mjs +308 -0
  74. package/runtime/codex/bin/codex-session-start.sh +238 -0
  75. package/runtime/codex/bin/codex-skill-render.mjs +315 -0
  76. package/runtime/codex/bin/codex-sync-skills.sh +223 -0
  77. package/runtime/codex/bin/dashboard-data.sh +174 -0
  78. package/runtime/codex/bin/graph-batch.sh +60 -0
  79. package/runtime/codex/bin/graph-op.sh +380 -0
  80. package/runtime/codex/bin/graph-wal.sh +228 -0
  81. package/runtime/codex/bin/graph.sh +141 -0
  82. package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
  83. package/runtime/codex/bin/handoff-run.sh +431 -0
  84. package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
  85. package/runtime/codex/bin/index-handoff.sh +306 -0
  86. package/runtime/codex/bin/lib/artifact-id.sh +46 -0
  87. package/runtime/codex/bin/lib/config.sh +71 -0
  88. package/runtime/codex/bin/lib/context.sh +463 -0
  89. package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
  90. package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
  91. package/runtime/codex/bin/lib/git-sync.sh +262 -0
  92. package/runtime/codex/bin/lib/greeting.sh +414 -0
  93. package/runtime/codex/bin/lib/hash.sh +13 -0
  94. package/runtime/codex/bin/lib/identity.sh +164 -0
  95. package/runtime/codex/bin/lib/permissions.sh +106 -0
  96. package/runtime/codex/bin/lib/time.sh +56 -0
  97. package/runtime/codex/bin/lib/worktree-links.sh +54 -0
  98. package/runtime/codex/bin/notify.sh +207 -0
  99. package/runtime/codex/bin/publish-artifact.sh +173 -0
  100. package/runtime/codex/bin/publish-references.sh +144 -0
  101. package/runtime/codex/bin/repo-state.sh +99 -0
  102. package/runtime/codex/bin/telemetry.sh +350 -0
  103. package/runtime/codex/bin/worktree-create.sh +118 -0
  104. package/runtime/codex/bin/worktree-remove.sh +50 -0
  105. package/runtime/codex/bin/worktree.sh +198 -0
  106. package/runtime/codex/manifest.json +155 -0
package/bin/cli.js CHANGED
@@ -1,24 +1,184 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * create-egregore — Set up Egregore in one command.
4
+ * create-egregore — set up Egregore for your team in one command.
5
+ *
6
+ * - Installs an `egregore` PATH binary instead of a shell alias
7
+ * - Auto-execs into the selected assistant (Claude Code or Codex) at the end
5
8
  *
6
9
  * Four modes:
7
- * npx create-egregore --token st_xxxx (from website — connected mode)
8
- * npx create-egregore (interactive — API-based, existing)
9
- * npx create-egregore --open (local mode — founder, no API)
10
- * npx create-egregore join <org> (local mode — join existing)
10
+ * npx -y create-egregore@latest --token st_xxxx (from website)
11
+ * npx -y create-egregore@latest (guided setup / launcher)
12
+ * npx -y create-egregore@latest join <org> (join existing)
11
13
  */
12
14
 
13
15
  const fs = require("node:fs");
16
+ const os = require("node:os");
14
17
  const path = require("node:path");
18
+ const { spawnSync } = require("node:child_process");
15
19
  const ui = require("../lib/ui");
16
20
  const { EgregoreAPI } = require("../lib/api");
17
- const { deviceFlow } = require("../lib/auth");
18
- const { install, ghApi } = require("../lib/setup");
21
+ const {
22
+ canUseInteractiveBrowserFlow,
23
+ deviceFlow,
24
+ explainNoGhHeadless,
25
+ ghCliExists,
26
+ tryGhAuthOrLogin,
27
+ } = require("../lib/auth");
28
+ const {
29
+ hasCodexRuntimeSupport,
30
+ installCodexRuntime,
31
+ reportCodexRuntimeSummary,
32
+ } = require("../lib/codex-runtime");
33
+ const {
34
+ install,
35
+ ghApi,
36
+ registerInstance,
37
+ installLauncherBinary,
38
+ normalizeRuntime,
39
+ } = require("../lib/setup");
19
40
  const { detectContext, isWritable, isSystemDir } = require("../lib/context");
20
41
 
21
42
  const API_URL = process.env.EGREGORE_API_URL || "https://egregore-production-55f2.up.railway.app";
43
+ const INSTANCES_FILE = path.join(os.homedir(), ".egregore", "instances.json");
44
+
45
+ function readLocalInstances() {
46
+ if (!fs.existsSync(INSTANCES_FILE)) return [];
47
+ try {
48
+ let data = JSON.parse(fs.readFileSync(INSTANCES_FILE, "utf-8"));
49
+ // Accept legacy/alt wrapped format: { instances: [...] }
50
+ if (data && !Array.isArray(data) && Array.isArray(data.instances)) {
51
+ data = data.instances;
52
+ }
53
+ if (!Array.isArray(data)) return [];
54
+ // Filter out invalid + dedup by resolved path. Some registries accumulated
55
+ // duplicate entries from old installers that didn't dedup on write.
56
+ const seen = new Set();
57
+ const result = [];
58
+ for (const entry of data) {
59
+ if (!entry || !entry.path || !fs.existsSync(entry.path)) continue;
60
+ const key = path.resolve(entry.path);
61
+ if (seen.has(key)) continue;
62
+ seen.add(key);
63
+ result.push(entry);
64
+ }
65
+ return result;
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+
71
+ // Detect whether `cwd` is itself an egregore install — even if cloned into a
72
+ // dir with a non-canonical name (e.g. `curve-labs-core` instead of `egregore`).
73
+ // Identified by structural artifacts (egregore.json + memory symlink), not by
74
+ // directory name. Returns null when cwd is not an egregore.
75
+ function detectCwdEgregore(cwd) {
76
+ const configPath = path.join(cwd, "egregore.json");
77
+ const memoryPath = path.join(cwd, "memory");
78
+ if (!fs.existsSync(configPath)) return null;
79
+ if (!fs.existsSync(memoryPath)) return null;
80
+ try {
81
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
82
+ return {
83
+ path: cwd,
84
+ slug: config.slug || path.basename(cwd),
85
+ name: config.org_name || path.basename(cwd),
86
+ adopt: true,
87
+ };
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function binaryOnPath(bin) {
94
+ return spawnSync("which", [bin], { stdio: "pipe" }).status === 0;
95
+ }
96
+
97
+ function assistantLabel(runtime) {
98
+ return normalizeRuntime(runtime) === "codex" ? "Codex" : "Claude Code";
99
+ }
100
+
101
+ function upgradeCodexRuntimeForExisting(instances) {
102
+ for (const inst of instances) {
103
+ if (!inst || !inst.path || !fs.existsSync(inst.path)) continue;
104
+ try {
105
+ const summary = installCodexRuntime(inst.path);
106
+ const ready = hasCodexRuntimeSupport(inst.path);
107
+ reportCodexRuntimeSummary(summary, ui, {
108
+ label: `Codex runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
109
+ includeConflicts: !ready,
110
+ });
111
+ } catch (err) {
112
+ ui.warn(`Could not install Codex runtime for ${inst.path}: ${err.message}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ async function chooseRuntime(args) {
118
+ if (args.runtime) {
119
+ try {
120
+ return normalizeRuntime(args.runtime);
121
+ } catch (err) {
122
+ ui.error(err.message);
123
+ process.exit(1);
124
+ }
125
+ }
126
+
127
+ if (!process.stdin.isTTY) return "claude-code";
128
+
129
+ const hasClaude = binaryOnPath("claude");
130
+ const hasCodex = binaryOnPath("codex");
131
+ const recommended = !hasClaude && hasCodex ? "codex" : "claude-code";
132
+ const choices = ["claude-code", "codex"]
133
+ .map((runtime) => {
134
+ const installed = runtime === "codex" ? hasCodex : hasClaude;
135
+ const label = assistantLabel(runtime);
136
+ const missingCopy = runtime === "codex"
137
+ ? "Install Codex later, then run egregore and choose Codex"
138
+ : "Install Claude Code later, then run egregore and choose Claude Code";
139
+ return {
140
+ label: `${label}${installed ? "" : " (not installed)"}${recommended === runtime ? " (recommended)" : ""}`,
141
+ description: installed
142
+ ? (runtime === "codex" ? "Installed · portable agent protocol" : "Installed · Claude Code integration")
143
+ : missingCopy,
144
+ runtime,
145
+ installed,
146
+ };
147
+ })
148
+ .sort((a, b) => Number(b.installed) - Number(a.installed));
149
+ const choice = await ui.choose("Which assistant do you want to use?", choices);
150
+ return choice.runtime;
151
+ }
152
+
153
+ async function upgradeLauncherFromExisting(instances) {
154
+ upgradeCodexRuntimeForExisting(instances);
155
+ const inst = instances.find((entry) => entry && entry.path && fs.existsSync(entry.path));
156
+ if (!inst) return null;
157
+ try {
158
+ return await installLauncherBinary(inst.path, ui);
159
+ } catch {
160
+ // Non-fatal: opening the launcher can still try PATH, and setup retries later.
161
+ return null;
162
+ }
163
+ }
164
+
165
+ function openLauncherBrowser(alias, runtimeOverride) {
166
+ const binary = (alias && alias.binaryPath) || "egregore";
167
+ const launcherArgs = [];
168
+ if (runtimeOverride) launcherArgs.push("--runtime", normalizeRuntime(runtimeOverride));
169
+ launcherArgs.push("browse");
170
+
171
+ const result = spawnSync(binary, launcherArgs, {
172
+ stdio: "inherit",
173
+ cwd: process.cwd(),
174
+ env: { ...process.env },
175
+ });
176
+ if (result.error) {
177
+ ui.warn(`Could not open the egregore launcher: ${result.error.message}`);
178
+ return false;
179
+ }
180
+ process.exit(result.signal ? 1 : (result.status ?? 0));
181
+ }
22
182
 
23
183
  function parseArgs(argv) {
24
184
  const args = {};
@@ -32,8 +192,22 @@ function parseArgs(argv) {
32
192
  args.api = argv[++i];
33
193
  } else if (argv[i] === "--open") {
34
194
  args.open = true;
195
+ } else if (argv[i] === "--connected" || argv[i] === "--hosted") {
196
+ args.connected = true;
35
197
  } else if (argv[i] === "--force") {
36
198
  args.force = true;
199
+ } else if (argv[i] === "--no-autolaunch") {
200
+ args.noAutolaunch = true;
201
+ } else if (argv[i] === "--no-screens") {
202
+ args.noScreens = true;
203
+ } else if (argv[i] === "--codex") {
204
+ args.runtime = "codex";
205
+ } else if (argv[i] === "--claude" || argv[i] === "--claude-code") {
206
+ args.runtime = "claude-code";
207
+ } else if (argv[i] === "--runtime" && argv[i + 1]) {
208
+ args.runtime = argv[++i];
209
+ } else if (argv[i].startsWith("--runtime=")) {
210
+ args.runtime = argv[i].split("=")[1];
37
211
  } else if (argv[i] === "--help" || argv[i] === "-h") {
38
212
  args.help = true;
39
213
  } else if (!argv[i].startsWith("-")) {
@@ -61,24 +235,40 @@ async function maybeRedirect(force) {
61
235
  if (!ctx) return { targetDir: process.cwd(), prefilledRepo: null };
62
236
 
63
237
  const { repoName, parentDir, originOwner, originName } = ctx;
238
+ const cwdEgregore = detectCwdEgregore(ctx.toplevel);
239
+ const isInsideEgregore = !!cwdEgregore;
64
240
 
65
241
  // Non-TTY guard: we can't prompt. Fail loudly so CI/Docker/piped scripts
66
242
  // don't hang forever on readline.
67
243
  if (!process.stdin.isTTY) {
68
244
  console.log("");
69
- ui.error(`Detected a git repo at ./${repoName} but stdin is not a TTY — cannot prompt.`);
70
- ui.info("Re-run from outside the repo, or pass --force to install here anyway.");
245
+ if (isInsideEgregore) {
246
+ ui.error(`Detected an Egregore checkout at ./${repoName} but stdin is not a TTY — cannot prompt.`);
247
+ ui.info(`Re-run from ${parentDir}, or pass --force to install here anyway.`);
248
+ } else {
249
+ ui.error(`Detected a git repo at ./${repoName} but stdin is not a TTY — cannot prompt.`);
250
+ ui.info("Re-run from outside the repo, or pass --force to install here anyway.");
251
+ }
71
252
  process.exit(1);
72
253
  }
73
254
 
74
255
  const parentWritable = isWritable(parentDir);
75
256
  const parentIsSystem = isSystemDir(parentDir);
257
+ const fallbackDir = path.join(os.homedir(), "egregores");
258
+ const recommendedDir = parentWritable && !parentIsSystem ? parentDir : fallbackDir;
76
259
 
77
260
  console.log("");
78
- ui.warn(`You're inside a git repo: ${ui.bold(`./${repoName}`)}`);
79
- console.log("");
80
- ui.info("Egregore lives as a sibling shell that manages your repos,");
81
- ui.info("not inside them. Your repo stays where it is.");
261
+ if (isInsideEgregore) {
262
+ ui.warn(`You're inside an Egregore checkout: ${ui.bold(`./${repoName}`)}`);
263
+ console.log("");
264
+ ui.info("To set up another Egregore, put it next to this checkout.");
265
+ ui.info(`Recommended local path: ${ui.cyan(recommendedDir)}`);
266
+ } else {
267
+ ui.warn(`You're inside a git repo: ${ui.bold(`./${repoName}`)}`);
268
+ console.log("");
269
+ ui.info("Egregore lives as a sibling shell that manages your repos,");
270
+ ui.info("not inside them. Your repo stays where it is.");
271
+ }
82
272
 
83
273
  if (parentIsSystem) {
84
274
  console.log("");
@@ -86,8 +276,13 @@ async function maybeRedirect(force) {
86
276
  }
87
277
 
88
278
  const choices = [];
89
- // Offer the parent redirect only if it's writable AND not a system dir.
90
- if (parentWritable && !parentIsSystem) {
279
+ if (isInsideEgregore) {
280
+ choices.push({
281
+ label: "Use recommended local path",
282
+ description: recommendedDir,
283
+ action: "recommended",
284
+ });
285
+ } else if (parentWritable && !parentIsSystem) {
91
286
  choices.push({
92
287
  label: `Install in ${parentDir}`,
93
288
  description: `Set up alongside ${repoName} and add it to managed repos (Recommended)`,
@@ -95,8 +290,8 @@ async function maybeRedirect(force) {
95
290
  });
96
291
  }
97
292
  choices.push({
98
- label: "Pick a different directory",
99
- description: "Choose where to install Egregore",
293
+ label: "Choose another folder",
294
+ description: "Paste or type a local folder path",
100
295
  action: "pick",
101
296
  });
102
297
  choices.push({
@@ -113,7 +308,7 @@ async function maybeRedirect(force) {
113
308
  process.exit(0);
114
309
  }
115
310
 
116
- let targetDir = parentDir;
311
+ let targetDir = choice.action === "recommended" ? recommendedDir : parentDir;
117
312
  if (choice.action === "pick") {
118
313
  const answer = await ui.prompt("Install directory (absolute path):");
119
314
  if (!answer) {
@@ -133,10 +328,19 @@ async function maybeRedirect(force) {
133
328
  process.exit(1);
134
329
  }
135
330
  }
331
+ if (choice.action === "recommended") {
332
+ try {
333
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
334
+ fs.accessSync(targetDir, fs.constants.W_OK);
335
+ } catch (err) {
336
+ ui.error(`Cannot write to ${targetDir}: ${err.message}`);
337
+ process.exit(1);
338
+ }
339
+ }
136
340
 
137
341
  return {
138
342
  targetDir,
139
- prefilledRepo: {
343
+ prefilledRepo: isInsideEgregore ? null : {
140
344
  name: originName || repoName,
141
345
  owner: originOwner,
142
346
  localName: repoName,
@@ -147,21 +351,56 @@ async function maybeRedirect(force) {
147
351
 
148
352
  function showHelp() {
149
353
  ui.banner();
150
- ui.info("Usage:");
151
- ui.info(" npx create-egregore Interactive setup (API)");
152
- ui.info(" npx create-egregore --open New project (local mode)");
153
- ui.info(" npx create-egregore join <org>[/<repo>] Join existing project");
154
- ui.info(" npx create-egregore --token <setup-token> Install from website");
354
+ ui.info("create-egregore — set up Egregore for your team in one command.");
155
355
  ui.info("");
156
- ui.info("Options:");
157
- ui.info(" --open Local mode — no API server needed");
356
+ ui.info("Common:");
357
+ ui.info(" npx -y create-egregore@latest Create your Egregore (or open the launcher)");
358
+ ui.info(" npx -y create-egregore@latest join <org>[/<repo>] Join existing project");
359
+ ui.info(" npx -y create-egregore@latest --token <setup-token> Install from website");
360
+ ui.info("");
361
+ ui.info("Advanced options:");
362
+ ui.info(" --open Local founder mode (now the default; flag kept for back-compat)");
363
+ ui.info(" --connected Use the hosted egregore.xyz setup flow instead of local");
158
364
  ui.info(" --token <token> Setup token from egregore.xyz");
159
- ui.info(" --api <url> API URL override");
365
+ ui.info(" --api <url> Use connected API setup with this URL");
366
+ ui.info(" --codex Prefer Codex for this local install");
367
+ ui.info(" --claude Prefer Claude Code for this local install");
368
+ ui.info(" --runtime <name> Assistant override: claude-code or codex");
160
369
  ui.info(" --force Skip the in-repo detection check");
370
+ ui.info(" --no-autolaunch Don't launch the assistant at the end (for CI)");
371
+ ui.info(" --no-screens Skip the full-screen welcome/ready bookends");
161
372
  ui.info(" -h, --help Show this help");
162
373
  ui.info("");
163
374
  }
164
375
 
376
+ async function preflight() {
377
+ // Verify prerequisites before starting. Fast fail with specific fix hints
378
+ // beats discovering missing tools mid-install. Claude Code is checked
379
+ // separately at auto-exec time since users can install it after.
380
+ const { spawnSync } = require("node:child_process");
381
+ const checks = [
382
+ { bin: "git", hint: "install Xcode command-line tools or https://git-scm.com" },
383
+ { bin: "jq", hint: "brew install jq (or https://stedolan.github.io/jq/)" },
384
+ ];
385
+ const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
386
+ const missing = [];
387
+ if (nodeMajor < 18) {
388
+ missing.push({ bin: "node >=18", hint: `you're on Node ${process.versions.node}; upgrade at https://nodejs.org` });
389
+ }
390
+ for (const c of checks) {
391
+ const r = spawnSync("which", [c.bin], { stdio: "pipe" });
392
+ if (r.status !== 0) missing.push(c);
393
+ }
394
+ if (missing.length === 0) return;
395
+
396
+ ui.warn("missing prerequisites:");
397
+ for (const m of missing) {
398
+ ui.info(` ${ui.red("✗")} ${ui.bold(m.bin)} ${ui.dim("— " + m.hint)}`);
399
+ }
400
+ ui.info("");
401
+ process.exit(1);
402
+ }
403
+
165
404
  async function main() {
166
405
  const args = parseArgs(process.argv);
167
406
 
@@ -169,10 +408,69 @@ async function main() {
169
408
  showHelp();
170
409
  process.exit(0);
171
410
  }
411
+ if (args.runtime) {
412
+ try {
413
+ args.runtime = normalizeRuntime(args.runtime);
414
+ } catch (err) {
415
+ ui.error(err.message);
416
+ process.exit(1);
417
+ }
418
+ }
172
419
 
173
- ui.banner();
420
+ // Local-first product surface: if the user already has egregores installed
421
+ // and didn't pass an explicit mode flag, refresh the launcher and open the
422
+ // same TUI as bare `egregore`. With no installed instances, make bare
423
+ // `npx -y create-egregore@latest` enter the guided local founder setup (`--open`).
424
+ //
425
+ // We also surface the cwd as a launchable when it looks like an egregore
426
+ // install (egregore.json + memory symlink present) but isn't in the registry
427
+ // — common when the egregore was cloned into a non-canonical directory name
428
+ // and so its path was never recorded.
429
+ const noInstallFlags = !args.token && !args.open && !args.connected && !args.subcommand && !args.force && !args.api;
430
+ if (noInstallFlags && process.stdin.isTTY) {
431
+ const registered = readLocalInstances();
432
+ const cwdInstall = detectCwdEgregore(process.cwd());
433
+ const alreadyRegistered =
434
+ cwdInstall && registered.some((i) => path.resolve(i.path) === path.resolve(cwdInstall.path));
435
+ const instances =
436
+ cwdInstall && !alreadyRegistered ? [cwdInstall, ...registered] : registered;
437
+ if (instances.length > 0) {
438
+ if (cwdInstall && !alreadyRegistered) {
439
+ try {
440
+ registerInstance(cwdInstall.slug, cwdInstall.name, cwdInstall.path, args.runtime || "claude-code");
441
+ } catch {}
442
+ }
443
+ const alias = await upgradeLauncherFromExisting(instances);
444
+ if (openLauncherBrowser(alias, args.runtime)) return;
445
+ ui.warn("Falling back to guided local setup.");
446
+ args.open = true;
447
+ }
448
+ if (instances.length === 0) {
449
+ args.open = true;
450
+ }
451
+ }
452
+
453
+ // Bookend: full-screen welcome (alt-screen) or inline banner. Only one
454
+ // or the other — don't double up. `--no-screens` and non-TTY fall back
455
+ // to the inline banner.
456
+ const useScreens = !args.noScreens && ui.canShowScreens();
457
+ if (useScreens) {
458
+ const mode = args.subcommand === "join" ? "join"
459
+ : args.token ? "token"
460
+ : args.open ? "founder"
461
+ : "founder";
462
+ await ui.welcomeScreen({ mode });
463
+ } else {
464
+ ui.banner();
465
+ }
466
+
467
+ await preflight();
174
468
 
175
469
  const ctx = await maybeRedirect(args.force);
470
+ // Thread install options through ctx so downstream flows can use them.
471
+ ctx.noAutolaunch = !!args.noAutolaunch;
472
+ ctx.noScreens = !useScreens;
473
+ ctx.runtime = await chooseRuntime(args);
176
474
 
177
475
  if (args.token) {
178
476
  // ===== Connected mode: token provided =====
@@ -181,19 +479,20 @@ async function main() {
181
479
  } else if (args.subcommand === "join") {
182
480
  // ===== Local mode: join existing =====
183
481
  if (!args.joinOrg) {
184
- ui.error("Usage: npx create-egregore join <github-org>");
482
+ ui.error("Usage: npx -y create-egregore@latest join <github-org>");
185
483
  process.exit(1);
186
484
  }
187
485
  const { localJoinFlow } = require("../lib/local");
188
486
  await localJoinFlow(args.joinOrg, ui, ctx);
189
- } else if (args.open) {
190
- // ===== Local mode: founder (explicit opt-in) =====
191
- const { localFounderFlow } = require("../lib/local");
192
- await localFounderFlow(ui, ctx);
193
- } else {
194
- // ===== Interactive API flow (default — existing behavior) =====
487
+ } else if (args.connected) {
488
+ // ===== Connected/hosted interactive flow (explicit opt-in) =====
195
489
  const api = new EgregoreAPI(args.api || API_URL);
196
490
  await interactiveFlow(api, ctx);
491
+ } else {
492
+ // ===== Local founder is the DEFAULT — bare `npx create-egregore` and
493
+ // --open both land here. Hosted is opt-in via --connected/--token.
494
+ const { localFounderFlow } = require("../lib/local");
495
+ await localFounderFlow(ui, ctx);
197
496
  }
198
497
  }
199
498
 
@@ -209,19 +508,34 @@ async function tokenFlow(api, token, ctx) {
209
508
  ui.info("");
210
509
  ui.info("The token may have expired or already been used.");
211
510
  ui.info("Visit egregore.xyz to get a new one, or run without --token:");
212
- ui.info(" npx create-egregore");
511
+ ui.info(" npx -y create-egregore@latest");
213
512
  process.exit(1);
214
513
  }
215
514
 
216
- // Joiner tokens don't include a github_token — run device flow to get one
515
+ // Joiner tokens don't include a github_token — try gh CLI first. If gh is
516
+ // installed but logged out, run gh auth login instead of device flow.
217
517
  if (!data.github_token || data.needs_cli_auth) {
218
- ui.info("\nSign in with GitHub to complete setup.\n");
219
- try {
220
- data.github_token = await deviceFlow(ui);
221
- ui.success("Authenticated with GitHub");
222
- } catch (err) {
223
- ui.error(`GitHub auth failed: ${err.message}`);
518
+ const ghAuth = tryGhAuthOrLogin(ui, data.needs_cli_auth ? { mode: "join" } : {});
519
+ if (ghAuth) {
520
+ ui.success(`Using your ${ui.bold("gh")} CLI auth${ghAuth.user ? ` (${ghAuth.user})` : ""}`);
521
+ data.github_token = ghAuth.token;
522
+ } else if (ghCliExists()) {
523
+ ui.error("GitHub CLI auth did not complete.");
524
+ ui.info(`Run ${ui.bold("gh auth login")} and then re-run this command.`);
224
525
  process.exit(1);
526
+ } else {
527
+ if (!canUseInteractiveBrowserFlow()) {
528
+ explainNoGhHeadless(ui);
529
+ process.exit(1);
530
+ }
531
+ ui.info("\nSign in to your GitHub to complete setup.\n");
532
+ try {
533
+ data.github_token = await deviceFlow(ui, data.needs_cli_auth ? { mode: "join" } : {});
534
+ ui.success("Authenticated with GitHub");
535
+ } catch (err) {
536
+ ui.error(`GitHub auth failed: ${err.message}`);
537
+ process.exit(1);
538
+ }
225
539
  }
226
540
  }
227
541
 
@@ -236,7 +550,13 @@ async function tokenFlow(api, token, ctx) {
236
550
  }
237
551
  }
238
552
  try {
239
- await install(data, ui, ctx.targetDir, { skipRepos });
553
+ await install(data, ui, ctx.targetDir, {
554
+ skipRepos,
555
+ noAutolaunch: ctx.noAutolaunch,
556
+ noScreens: ctx.noScreens,
557
+ flowVariant: ctx.flowVariant,
558
+ runtime: ctx.runtime,
559
+ });
240
560
  if (ctx.prefilledRepo && skipRepos.size === 0) {
241
561
  console.log("");
242
562
  ui.info(`Tip: add your current repo to managed_repos with:`);
@@ -249,15 +569,30 @@ async function tokenFlow(api, token, ctx) {
249
569
  }
250
570
 
251
571
  async function interactiveFlow(api, ctx) {
252
- // 1. GitHub auth
253
- ui.info("Let's set up Egregore. First, sign in with GitHub.\n");
572
+ // 1. GitHub auth — gh CLI if available, else device flow.
254
573
  let githubToken;
255
- try {
256
- githubToken = await deviceFlow(ui);
257
- ui.success("Authenticated with GitHub");
258
- } catch (err) {
259
- ui.error(`GitHub auth failed: ${err.message}`);
574
+ const ghAuth = tryGhAuthOrLogin(ui);
575
+ if (ghAuth) {
576
+ ui.success(`Using your ${ui.bold("gh")} CLI auth${ghAuth.user ? ` (${ghAuth.user})` : ""}`);
577
+ githubToken = ghAuth.token;
578
+ } else if (ghCliExists()) {
579
+ ui.error("GitHub CLI auth did not complete.");
580
+ ui.info(`Run ${ui.bold("gh auth login")} and then re-run this command.`);
260
581
  process.exit(1);
582
+ } else {
583
+ if (!canUseInteractiveBrowserFlow()) {
584
+ explainNoGhHeadless(ui);
585
+ process.exit(1);
586
+ }
587
+ ui.info("Let's set up Egregore in your own account. First, sign in to your GitHub.\n");
588
+ try {
589
+ // Connected mode enumerates orgs server-side, so it needs read:org here.
590
+ githubToken = await deviceFlow(ui, { scope: "repo read:org" });
591
+ ui.success("Authenticated with GitHub");
592
+ } catch (err) {
593
+ ui.error(`GitHub auth failed: ${err.message}`);
594
+ process.exit(1);
595
+ }
261
596
  }
262
597
 
263
598
  // 2. Detect orgs
@@ -280,9 +615,13 @@ async function interactiveFlow(api, ctx) {
280
615
  for (const org of orgsData.orgs) {
281
616
  if (org.has_egregore) {
282
617
  for (const inst of org.instances || []) {
618
+ const desc =
619
+ inst.org_name && inst.org_name !== inst.repo_name
620
+ ? `Join existing · ${inst.org_name}`
621
+ : "Join existing";
283
622
  choices.push({
284
- label: `${org.name || org.login} — ${inst.org_name || inst.repo_name}`,
285
- description: "Join existing",
623
+ label: `${org.name || org.login} — ${inst.repo_name}`,
624
+ description: desc,
286
625
  action: "join",
287
626
  login: org.login,
288
627
  repo_name: inst.repo_name,
@@ -312,9 +651,13 @@ async function interactiveFlow(api, ctx) {
312
651
  // Personal account — show join for each existing instance + setup for new
313
652
  if (orgsData.personal.has_egregore) {
314
653
  for (const inst of orgsData.personal.instances || []) {
654
+ const desc =
655
+ inst.org_name && inst.org_name !== inst.repo_name
656
+ ? `Join existing · ${inst.org_name}`
657
+ : "Join existing";
315
658
  choices.push({
316
- label: `${orgsData.user.login} (personal) — ${inst.org_name || inst.repo_name}`,
317
- description: "Join existing",
659
+ label: `${orgsData.user.login} (personal) — ${inst.repo_name}`,
660
+ description: desc,
318
661
  action: "join",
319
662
  login: orgsData.user.login,
320
663
  repo_name: inst.repo_name,
@@ -394,32 +737,68 @@ async function setupFlow(api, githubToken, choice, ctx) {
394
737
  selectedRepos.unshift(autoAdopt);
395
738
  }
396
739
 
397
- // Offer to create a new project repo
740
+ // Offer to create a new project repo. Pre-validate against the predicted
741
+ // egregore + memory repo names so the user can't accidentally create
742
+ // (or worse, "add an existing" via 422-fallback) a repo that collides
743
+ // with their Egregore instance.
744
+ const slugifyRepo = (s) => s.trim().toLowerCase()
745
+ .replace(/[^a-z0-9-]/g, "-")
746
+ .replace(/-+/g, "-")
747
+ .replace(/^-|-$/g, "");
748
+ const predictedEgregoreRepo = instanceName
749
+ ? `egregore-${slugifyRepo(instanceName)}`
750
+ : "egregore";
751
+ const predictedMemoryRepo = instanceName
752
+ ? `${slugifyRepo(instanceName)}-memory`
753
+ : `${slugifyRepo(choice.login)}-memory`;
754
+
398
755
  console.log("");
399
- const newRepoName = await ui.prompt("Create a new project repo? Enter name (or press Enter to skip):");
400
- if (newRepoName) {
401
- const repoSlug = newRepoName.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
402
- if (repoSlug) {
403
- const s2 = ui.spinner(`Creating ${repoSlug}...`);
404
- try {
405
- const apiPath = choice.is_personal ? "/user/repos" : `/orgs/${choice.login}/repos`;
406
- const { status } = await ghApi("POST", apiPath, githubToken, {
407
- name: repoSlug,
408
- private: true,
409
- auto_init: true,
410
- });
411
- if (status === 201 || status === 200) {
412
- selectedRepos.push(repoSlug);
413
- s2.stop(`Created ${repoSlug}`);
414
- } else if (status === 422) {
415
- selectedRepos.push(repoSlug);
416
- s2.stop(`${repoSlug} already exists — added to managed repos`);
417
- } else {
418
- s2.fail(`Could not create ${repoSlug}`);
419
- }
420
- } catch (err) {
421
- s2.fail(`Could not create ${repoSlug}: ${err.message}`);
756
+ let projectRepoSlug = null;
757
+ while (true) {
758
+ const newRepoName = await ui.prompt("Create a new project repo? Enter name (or press Enter to skip):");
759
+ if (!newRepoName) break;
760
+ const repoSlug = slugifyRepo(newRepoName);
761
+ if (!repoSlug) {
762
+ ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes.");
763
+ continue;
764
+ }
765
+ if (repoSlug === predictedEgregoreRepo) {
766
+ ui.warn(`"${repoSlug}" collides with your Egregore repo. Pick a different name.`);
767
+ ui.info(` ${ui.dim("suggestion:")} ${ui.cyan(`${repoSlug}-app`)}`);
768
+ continue;
769
+ }
770
+ if (repoSlug === predictedMemoryRepo) {
771
+ ui.warn(`"${repoSlug}" collides with your memory repo. Pick a different name.`);
772
+ continue;
773
+ }
774
+ projectRepoSlug = repoSlug;
775
+ break;
776
+ }
777
+ if (projectRepoSlug) {
778
+ const s2 = ui.spinner(`Creating ${projectRepoSlug}...`);
779
+ try {
780
+ const apiPath = choice.is_personal ? "/user/repos" : `/orgs/${choice.login}/repos`;
781
+ const { status } = await ghApi("POST", apiPath, githubToken, {
782
+ name: projectRepoSlug,
783
+ private: true,
784
+ auto_init: true,
785
+ });
786
+ if (status === 201 || status === 200) {
787
+ selectedRepos.push(projectRepoSlug);
788
+ s2.stop(`Created ${projectRepoSlug}`);
789
+ } else if (status === 422) {
790
+ // Repo already exists. We pre-validated against egregore + memory
791
+ // names, so this is a NEW conflict (some other repo of the user's
792
+ // already has this name). Surface and add as managed — same UX as
793
+ // before but with the accidental-collision-on-egregore-name path
794
+ // already filtered out.
795
+ selectedRepos.push(projectRepoSlug);
796
+ s2.stop(`${projectRepoSlug} already exists — added to managed repos`);
797
+ } else {
798
+ s2.fail(`Could not create ${projectRepoSlug}`);
422
799
  }
800
+ } catch (err) {
801
+ s2.fail(`Could not create ${projectRepoSlug}: ${err.message}`);
423
802
  }
424
803
  }
425
804
 
@@ -446,7 +825,13 @@ async function setupFlow(api, githubToken, choice, ctx) {
446
825
  // Claim the token immediately
447
826
  const data = await api.claimToken(result.setup_token);
448
827
  const skipRepos = autoAdopt ? new Set([autoAdopt]) : new Set();
449
- await install(data, ui, ctx.targetDir, { skipRepos });
828
+ await install(data, ui, ctx.targetDir, {
829
+ skipRepos,
830
+ noAutolaunch: ctx.noAutolaunch,
831
+ noScreens: ctx.noScreens,
832
+ flowVariant: ctx.flowVariant,
833
+ runtime: ctx.runtime,
834
+ });
450
835
 
451
836
  if (result.telegram_invite_link) {
452
837
  console.log("");
@@ -477,7 +862,13 @@ async function joinFlow(api, githubToken, choice, ctx) {
477
862
  if (n && n === ctx.prefilledRepo.name) skipRepos.add(n);
478
863
  }
479
864
  }
480
- await install(data, ui, ctx.targetDir, { skipRepos });
865
+ await install(data, ui, ctx.targetDir, {
866
+ skipRepos,
867
+ noAutolaunch: ctx.noAutolaunch,
868
+ noScreens: ctx.noScreens,
869
+ flowVariant: ctx.flowVariant,
870
+ runtime: ctx.runtime,
871
+ });
481
872
  } catch (err) {
482
873
  s.fail("Join failed");
483
874
  ui.error(err.message);