create-egregore 0.16.2 → 0.17.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/bin/cli.js CHANGED
@@ -10,10 +10,13 @@
10
10
  * npx create-egregore join <org> (local mode — join existing)
11
11
  */
12
12
 
13
+ const fs = require("node:fs");
14
+ const path = require("node:path");
13
15
  const ui = require("../lib/ui");
14
16
  const { EgregoreAPI } = require("../lib/api");
15
17
  const { deviceFlow } = require("../lib/auth");
16
18
  const { install, ghApi } = require("../lib/setup");
19
+ const { detectContext, isWritable, isSystemDir } = require("../lib/context");
17
20
 
18
21
  const API_URL = process.env.EGREGORE_API_URL || "https://egregore-production-55f2.up.railway.app";
19
22
 
@@ -29,6 +32,8 @@ function parseArgs(argv) {
29
32
  args.api = argv[++i];
30
33
  } else if (argv[i] === "--open") {
31
34
  args.open = true;
35
+ } else if (argv[i] === "--force") {
36
+ args.force = true;
32
37
  } else if (argv[i] === "--help" || argv[i] === "-h") {
33
38
  args.help = true;
34
39
  } else if (!argv[i].startsWith("-")) {
@@ -42,6 +47,104 @@ function parseArgs(argv) {
42
47
  return args;
43
48
  }
44
49
 
50
+ /**
51
+ * If cwd is inside a git repo, show the "sibling shell" notice and offer a
52
+ * redirect. Returns { targetDir, prefilledRepo } — prefilledRepo is non-null
53
+ * only when we detected a repo. Caller passes both through to downstream flows.
54
+ *
55
+ * Bypassed entirely by --force.
56
+ */
57
+ async function maybeRedirect(force) {
58
+ if (force) return { targetDir: process.cwd(), prefilledRepo: null };
59
+
60
+ const ctx = detectContext();
61
+ if (!ctx) return { targetDir: process.cwd(), prefilledRepo: null };
62
+
63
+ const { repoName, parentDir, originOwner, originName } = ctx;
64
+
65
+ // Non-TTY guard: we can't prompt. Fail loudly so CI/Docker/piped scripts
66
+ // don't hang forever on readline.
67
+ if (!process.stdin.isTTY) {
68
+ 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.");
71
+ process.exit(1);
72
+ }
73
+
74
+ const parentWritable = isWritable(parentDir);
75
+ const parentIsSystem = isSystemDir(parentDir);
76
+
77
+ 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.");
82
+
83
+ if (parentIsSystem) {
84
+ console.log("");
85
+ ui.warn(`The parent directory (${parentDir}) looks like a system or home root — not a good place for an Egregore workspace.`);
86
+ }
87
+
88
+ const choices = [];
89
+ // Offer the parent redirect only if it's writable AND not a system dir.
90
+ if (parentWritable && !parentIsSystem) {
91
+ choices.push({
92
+ label: `Install in ${parentDir}`,
93
+ description: `Set up alongside ${repoName} and add it to managed repos (Recommended)`,
94
+ action: "parent",
95
+ });
96
+ }
97
+ choices.push({
98
+ label: "Pick a different directory",
99
+ description: "Choose where to install Egregore",
100
+ action: "pick",
101
+ });
102
+ choices.push({
103
+ label: "Cancel",
104
+ description: "Exit without changes",
105
+ action: "cancel",
106
+ });
107
+
108
+ const choice = await ui.choose("How would you like to proceed?", choices);
109
+
110
+ if (choice.action === "cancel") {
111
+ console.log("");
112
+ ui.info("Cancelled. Run from a clean directory, or pass --force to override.");
113
+ process.exit(0);
114
+ }
115
+
116
+ let targetDir = parentDir;
117
+ if (choice.action === "pick") {
118
+ const answer = await ui.prompt("Install directory (absolute path):");
119
+ if (!answer) {
120
+ ui.error("No directory provided");
121
+ process.exit(1);
122
+ }
123
+ targetDir = path.resolve(answer);
124
+ if (isSystemDir(targetDir)) {
125
+ ui.error(`${targetDir} looks like a system directory — pick something under your home or projects folder.`);
126
+ process.exit(1);
127
+ }
128
+ try {
129
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
130
+ fs.accessSync(targetDir, fs.constants.W_OK);
131
+ } catch (err) {
132
+ ui.error(`Cannot write to ${targetDir}: ${err.message}`);
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ return {
138
+ targetDir,
139
+ prefilledRepo: {
140
+ name: originName || repoName,
141
+ owner: originOwner,
142
+ localName: repoName,
143
+ toplevel: ctx.toplevel,
144
+ },
145
+ };
146
+ }
147
+
45
148
  function showHelp() {
46
149
  ui.banner();
47
150
  ui.info("Usage:");
@@ -51,9 +154,10 @@ function showHelp() {
51
154
  ui.info(" npx create-egregore --token <setup-token> Install from website");
52
155
  ui.info("");
53
156
  ui.info("Options:");
54
- ui.info(" --open Local mode — no API server needed");
157
+ ui.info(" --open Local mode — no API server needed");
55
158
  ui.info(" --token <token> Setup token from egregore.xyz");
56
159
  ui.info(" --api <url> API URL override");
160
+ ui.info(" --force Skip the in-repo detection check");
57
161
  ui.info(" -h, --help Show this help");
58
162
  ui.info("");
59
163
  }
@@ -68,10 +172,12 @@ async function main() {
68
172
 
69
173
  ui.banner();
70
174
 
175
+ const ctx = await maybeRedirect(args.force);
176
+
71
177
  if (args.token) {
72
- // ===== Connected mode: token provided (unchanged) =====
178
+ // ===== Connected mode: token provided =====
73
179
  const api = new EgregoreAPI(args.api || API_URL);
74
- await tokenFlow(api, args.token);
180
+ await tokenFlow(api, args.token, ctx);
75
181
  } else if (args.subcommand === "join") {
76
182
  // ===== Local mode: join existing =====
77
183
  if (!args.joinOrg) {
@@ -79,19 +185,19 @@ async function main() {
79
185
  process.exit(1);
80
186
  }
81
187
  const { localJoinFlow } = require("../lib/local");
82
- await localJoinFlow(args.joinOrg, ui);
188
+ await localJoinFlow(args.joinOrg, ui, ctx);
83
189
  } else if (args.open) {
84
190
  // ===== Local mode: founder (explicit opt-in) =====
85
191
  const { localFounderFlow } = require("../lib/local");
86
- await localFounderFlow(ui);
192
+ await localFounderFlow(ui, ctx);
87
193
  } else {
88
194
  // ===== Interactive API flow (default — existing behavior) =====
89
195
  const api = new EgregoreAPI(args.api || API_URL);
90
- await interactiveFlow(api);
196
+ await interactiveFlow(api, ctx);
91
197
  }
92
198
  }
93
199
 
94
- async function tokenFlow(api, token) {
200
+ async function tokenFlow(api, token, ctx) {
95
201
  const s = ui.spinner("Claiming setup token...");
96
202
  let data;
97
203
  try {
@@ -119,15 +225,30 @@ async function tokenFlow(api, token) {
119
225
  }
120
226
  }
121
227
 
228
+ // Token-mode setups already have their repo list baked in server-side. If
229
+ // the user's current working repo happens to match a managed-repo name, skip
230
+ // the clone/pull/configure step for it so we don't clobber uncommitted work.
231
+ const skipRepos = new Set();
232
+ if (ctx.prefilledRepo && Array.isArray(data.repos)) {
233
+ for (const r of data.repos) {
234
+ const n = typeof r === "string" ? r : r && r.name;
235
+ if (n && n === ctx.prefilledRepo.name) skipRepos.add(n);
236
+ }
237
+ }
122
238
  try {
123
- await install(data, ui);
239
+ await install(data, ui, ctx.targetDir, { skipRepos });
240
+ if (ctx.prefilledRepo && skipRepos.size === 0) {
241
+ console.log("");
242
+ ui.info(`Tip: add your current repo to managed_repos with:`);
243
+ ui.info(` /add ${ctx.prefilledRepo.toplevel || `../${ctx.prefilledRepo.localName}`}`);
244
+ }
124
245
  } catch (err) {
125
246
  ui.error(`Install failed: ${err.message}`);
126
247
  process.exit(1);
127
248
  }
128
249
  }
129
250
 
130
- async function interactiveFlow(api) {
251
+ async function interactiveFlow(api, ctx) {
131
252
  // 1. GitHub auth
132
253
  ui.info("Let's set up Egregore. First, sign in with GitHub.\n");
133
254
  let githubToken;
@@ -227,13 +348,13 @@ async function interactiveFlow(api) {
227
348
  // 5. Execute
228
349
  console.log("");
229
350
  if (choice.action === "join") {
230
- await joinFlow(api, githubToken, choice);
351
+ await joinFlow(api, githubToken, choice, ctx);
231
352
  } else {
232
- await setupFlow(api, githubToken, choice);
353
+ await setupFlow(api, githubToken, choice, ctx);
233
354
  }
234
355
  }
235
356
 
236
- async function setupFlow(api, githubToken, choice) {
357
+ async function setupFlow(api, githubToken, choice, ctx) {
237
358
  const orgName = await ui.prompt(`Organization display name [${choice.login}]:`);
238
359
  const name = orgName || choice.login;
239
360
 
@@ -241,19 +362,37 @@ async function setupFlow(api, githubToken, choice) {
241
362
  const instanceInput = await ui.prompt(`Instance name (e.g. "ops", "research") [leave blank for default]:`);
242
363
  const instanceName = instanceInput || undefined;
243
364
 
365
+ // If user was inside a repo owned by the chosen org, auto-adopt it.
366
+ const autoAdopt =
367
+ ctx.prefilledRepo &&
368
+ ctx.prefilledRepo.owner &&
369
+ ctx.prefilledRepo.owner.toLowerCase() === choice.login.toLowerCase()
370
+ ? ctx.prefilledRepo.name
371
+ : null;
372
+
244
373
  // Repo picker — show org repos for selection
245
374
  let selectedRepos = [];
246
375
  try {
247
376
  const repoData = await api.getOrgRepos(githubToken, choice.login);
248
377
  if (repoData.repos && repoData.repos.length > 0) {
378
+ const filtered = autoAdopt
379
+ ? repoData.repos.filter((r) => r.name !== autoAdopt)
380
+ : repoData.repos;
381
+ if (autoAdopt) {
382
+ console.log("");
383
+ ui.info(`${ui.cyan(autoAdopt)} will be managed automatically (your current repo)`);
384
+ }
249
385
  selectedRepos = await ui.multiSelect(
250
- "Which repos should Egregore manage?",
251
- repoData.repos,
386
+ "Which other repos should Egregore manage?",
387
+ filtered,
252
388
  );
253
389
  }
254
390
  } catch {
255
391
  // Non-fatal — continue without repos
256
392
  }
393
+ if (autoAdopt && !selectedRepos.includes(autoAdopt)) {
394
+ selectedRepos.unshift(autoAdopt);
395
+ }
257
396
 
258
397
  // Offer to create a new project repo
259
398
  console.log("");
@@ -306,7 +445,8 @@ async function setupFlow(api, githubToken, choice) {
306
445
 
307
446
  // Claim the token immediately
308
447
  const data = await api.claimToken(result.setup_token);
309
- await install(data, ui);
448
+ const skipRepos = autoAdopt ? new Set([autoAdopt]) : new Set();
449
+ await install(data, ui, ctx.targetDir, { skipRepos });
310
450
 
311
451
  if (result.telegram_invite_link) {
312
452
  console.log("");
@@ -319,7 +459,7 @@ async function setupFlow(api, githubToken, choice) {
319
459
  }
320
460
  }
321
461
 
322
- async function joinFlow(api, githubToken, choice) {
462
+ async function joinFlow(api, githubToken, choice, ctx) {
323
463
  const displayName = choice.repo_name ? `${choice.login} (${choice.repo_name})` : choice.login;
324
464
  const s = ui.spinner(`Joining ${ui.bold(displayName)}...`);
325
465
  try {
@@ -330,7 +470,14 @@ async function joinFlow(api, githubToken, choice) {
330
470
  s.stop("Joined");
331
471
 
332
472
  const data = await api.claimToken(result.setup_token);
333
- await install(data, ui);
473
+ const skipRepos = new Set();
474
+ if (ctx.prefilledRepo && Array.isArray(data.repos)) {
475
+ for (const r of data.repos) {
476
+ const n = typeof r === "string" ? r : r && r.name;
477
+ if (n && n === ctx.prefilledRepo.name) skipRepos.add(n);
478
+ }
479
+ }
480
+ await install(data, ui, ctx.targetDir, { skipRepos });
334
481
  } catch (err) {
335
482
  s.fail("Join failed");
336
483
  ui.error(err.message);
package/lib/auth.js CHANGED
@@ -7,6 +7,7 @@ const https = require("node:https");
7
7
 
8
8
  const GITHUB_APP_CLIENT_ID = "Iv23li2obNsAjakoK2RE"; // GitHub App — founder flow (clean install screen)
9
9
  const OAUTH_APP_CLIENT_ID = "Ov23liYZjJbbbNgnJ9vH"; // OAuth App — join flow (repo:invite only)
10
+ const SUPPORT_URL = "https://github.com/egregore-labs/egregore";
10
11
 
11
12
  function post(url, body) {
12
13
  return new Promise((resolve, reject) => {
@@ -45,6 +46,26 @@ function sleep(ms) {
45
46
  return new Promise((r) => setTimeout(r, ms));
46
47
  }
47
48
 
49
+ /**
50
+ * POST with retry + exponential backoff. Handles transient network errors
51
+ * (ECONNREFUSED, ECONNRESET, ETIMEDOUT) by retrying up to `retries` times
52
+ * with a 1s / 2s / 4s backoff. Throws the last error if all retries fail.
53
+ */
54
+ async function postWithRetry(url, body, retries = 3) {
55
+ let lastErr;
56
+ for (let attempt = 0; attempt < retries; attempt++) {
57
+ try {
58
+ return await post(url, body);
59
+ } catch (err) {
60
+ lastErr = err;
61
+ if (attempt < retries - 1) {
62
+ await sleep(1000 * Math.pow(2, attempt));
63
+ }
64
+ }
65
+ }
66
+ throw lastErr;
67
+ }
68
+
48
69
  function openBrowser(url) {
49
70
  const { execFile } = require("node:child_process");
50
71
  if (process.platform === "darwin") {
@@ -81,14 +102,27 @@ async function deviceFlow(ui, opts = {}) {
81
102
  const clientId = isJoin ? OAUTH_APP_CLIENT_ID : GITHUB_APP_CLIENT_ID;
82
103
  const scope = isJoin ? "repo:invite" : "";
83
104
 
84
- // 1. Request device code
85
- const codeResp = await post("https://github.com/login/device/code", {
86
- client_id: clientId,
87
- scope,
88
- });
105
+ // 1. Request device code (with retry — handles transient network hiccups
106
+ // and GitHub edge server blips that caused ECONNREFUSED in the field)
107
+ let codeResp;
108
+ try {
109
+ codeResp = await postWithRetry("https://github.com/login/device/code", {
110
+ client_id: clientId,
111
+ scope,
112
+ });
113
+ } catch (err) {
114
+ throw new Error(
115
+ `Could not reach GitHub after 3 attempts: ${err.message}\n` +
116
+ `Check your internet connection and try again.\n` +
117
+ `If this keeps happening, report it at ${SUPPORT_URL}/issues`
118
+ );
119
+ }
89
120
 
90
121
  if (!codeResp.device_code) {
91
- throw new Error("Failed to start GitHub device flow");
122
+ throw new Error(
123
+ `GitHub device flow did not return a code.\n` +
124
+ `This is unusual — please report at ${SUPPORT_URL}/issues`
125
+ );
92
126
  }
93
127
 
94
128
  const { device_code, user_code, verification_uri, interval: pollInterval } = codeResp;
@@ -109,19 +143,40 @@ async function deviceFlow(ui, opts = {}) {
109
143
  ui.info("");
110
144
  openBrowser(verifyUrl);
111
145
 
112
- // 3. Poll for token
146
+ // 3. Poll for token. Transient network errors (one bad packet, WiFi blip,
147
+ // GitHub edge restart) must NOT kill the whole flow — the user might
148
+ // be mid-authorize on the browser side. Catch per-poll errors and
149
+ // continue; the outer 5-minute timeout handles genuinely broken networks.
113
150
  let interval = (pollInterval || 5) * 1000;
114
151
  const timeout = 300000; // 5 minutes
115
152
  const start = Date.now();
153
+ let consecutiveNetworkErrors = 0;
154
+ let networkWarningShown = false;
116
155
 
117
156
  while (Date.now() - start < timeout) {
118
157
  await sleep(interval);
119
158
 
120
- const tokenResp = await post("https://github.com/login/oauth/access_token", {
121
- client_id: clientId,
122
- device_code,
123
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
124
- });
159
+ let tokenResp;
160
+ try {
161
+ tokenResp = await post("https://github.com/login/oauth/access_token", {
162
+ client_id: clientId,
163
+ device_code,
164
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
165
+ });
166
+ consecutiveNetworkErrors = 0;
167
+ } catch (err) {
168
+ consecutiveNetworkErrors++;
169
+ // After 3 consecutive failures, surface a note so the user knows
170
+ // something is off — but keep polling until the outer timeout.
171
+ if (consecutiveNetworkErrors >= 3 && !networkWarningShown) {
172
+ ui.warn(
173
+ `Network trouble reaching GitHub (${err.message}). Still trying.\n` +
174
+ `If this persists, report at ${SUPPORT_URL}/issues`
175
+ );
176
+ networkWarningShown = true;
177
+ }
178
+ continue;
179
+ }
125
180
 
126
181
  if (tokenResp.access_token) {
127
182
  return tokenResp.access_token;
@@ -135,10 +190,17 @@ async function deviceFlow(ui, opts = {}) {
135
190
  continue;
136
191
  }
137
192
 
138
- throw new Error(tokenResp.error_description || tokenResp.error || "Auth failed");
193
+ throw new Error(
194
+ `${tokenResp.error_description || tokenResp.error || "Auth failed"}\n` +
195
+ `If you need help, see ${SUPPORT_URL}`
196
+ );
139
197
  }
140
198
 
141
- throw new Error("Timed out waiting for authorization (5 minutes)");
199
+ throw new Error(
200
+ `Timed out waiting for authorization (5 minutes).\n` +
201
+ `If the browser tab didn't open, visit the verification URL manually and enter your code.\n` +
202
+ `If this keeps happening, report at ${SUPPORT_URL}/issues`
203
+ );
142
204
  }
143
205
 
144
206
  module.exports = { deviceFlow, openBrowser };
package/lib/context.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Detect whether cwd is inside a git work-tree.
3
+ *
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
6
+ * them to the repo's parent directory and auto-adopt their repo as a managed
7
+ * repo. This module is the single source of truth for that detection.
8
+ */
9
+
10
+ const { execFileSync } = require("node:child_process");
11
+ const path = require("node:path");
12
+ const fs = require("node:fs");
13
+
14
+ function gitOut(args, cwd) {
15
+ try {
16
+ return execFileSync("git", args, {
17
+ cwd,
18
+ stdio: ["ignore", "pipe", "ignore"],
19
+ encoding: "utf-8",
20
+ timeout: 5000,
21
+ }).trim();
22
+ } catch {
23
+ return "";
24
+ }
25
+ }
26
+
27
+ /**
28
+ * @param {string} [cwd=process.cwd()]
29
+ * @returns {null | { toplevel: string, repoName: string, parentDir: string, originOwner: string|null, originName: string|null }}
30
+ */
31
+ function detectContext(cwd = process.cwd()) {
32
+ let toplevel = gitOut(["rev-parse", "--show-toplevel"], cwd);
33
+ if (!toplevel) return null;
34
+
35
+ // Submodule → resolve to superproject
36
+ const sup = gitOut(["rev-parse", "--show-superproject-working-tree"], cwd);
37
+ if (sup) toplevel = sup;
38
+
39
+ // Worktree → resolve to main work-tree so parentDir isn't under .git/worktrees
40
+ const commonDir = gitOut(["rev-parse", "--git-common-dir"], toplevel);
41
+ if (commonDir) {
42
+ const absCommon = path.isAbsolute(commonDir) ? commonDir : path.resolve(toplevel, commonDir);
43
+ if (path.basename(absCommon) === ".git") {
44
+ const mainWorktree = path.dirname(absCommon);
45
+ if (mainWorktree && mainWorktree !== toplevel && fs.existsSync(mainWorktree)) {
46
+ toplevel = mainWorktree;
47
+ }
48
+ }
49
+ }
50
+
51
+ const repoName = path.basename(toplevel);
52
+ const parentDir = path.dirname(toplevel);
53
+
54
+ let originOwner = null;
55
+ let originName = null;
56
+ const origin = gitOut(["-C", toplevel, "remote", "get-url", "origin"]);
57
+ if (origin) {
58
+ const m = origin.match(/(?:github\.com[:/])([^/]+)\/([^/.]+?)(?:\.git)?$/);
59
+ if (m) {
60
+ originOwner = m[1];
61
+ originName = m[2];
62
+ }
63
+ }
64
+
65
+ return { toplevel, repoName, parentDir, originOwner, originName };
66
+ }
67
+
68
+ function isWritable(dir) {
69
+ try {
70
+ fs.accessSync(dir, fs.constants.W_OK);
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Return true if `dir` looks like a system or shared location where planting
79
+ * an egregore instance + memory repo + managed-repo clones would be disruptive
80
+ * or confusing. Detected: filesystem root, OS user roots, tmp/opt/usr/var,
81
+ * and the user's $HOME itself (which happens when someone runs `git init ~`
82
+ * for dotfile management).
83
+ */
84
+ function isSystemDir(dir) {
85
+ if (!dir) return true;
86
+ const resolved = path.resolve(dir);
87
+ const blocklist = new Set([
88
+ "/",
89
+ "/Users",
90
+ "/home",
91
+ "/root",
92
+ "/tmp",
93
+ "/private/tmp",
94
+ "/opt",
95
+ "/usr",
96
+ "/usr/local",
97
+ "/var",
98
+ "/etc",
99
+ "/private/var",
100
+ ]);
101
+ if (blocklist.has(resolved)) return true;
102
+ const home = require("node:os").homedir();
103
+ if (home && path.resolve(home) === resolved) return true;
104
+ return false;
105
+ }
106
+
107
+ module.exports = { detectContext, isWritable, isSystemDir };
package/lib/local.js CHANGED
@@ -250,7 +250,9 @@ const APP_INSTALL_URL = "https://github.com/apps/egregore-labs/installations/new
250
250
 
251
251
  // ── Flow 1: Founder (local mode) ───────────────────────────────────
252
252
 
253
- async function localFounderFlow(ui) {
253
+ async function localFounderFlow(ui, ctx = {}) {
254
+ // Names of repos already on disk as the user's own workdir — skip clone/pull.
255
+ const skipRepos = ctx.skipRepos instanceof Set ? new Set(ctx.skipRepos) : new Set();
254
256
  // 1. Install GitHub App — user picks which repos/accounts to grant access to
255
257
  ui.info("Let's set up Egregore.\n");
256
258
  ui.info("First, install the Egregore app on your GitHub account.");
@@ -325,19 +327,36 @@ async function localFounderFlow(ui) {
325
327
  orgName = nameInput || githubOrg;
326
328
  description = "";
327
329
 
330
+ // Auto-adopt the user's current repo if it's under the chosen GitHub owner.
331
+ const autoAdopt =
332
+ ctx.prefilledRepo &&
333
+ ctx.prefilledRepo.owner &&
334
+ ctx.prefilledRepo.owner.toLowerCase() === githubOrg.toLowerCase()
335
+ ? ctx.prefilledRepo.name
336
+ : null;
337
+
328
338
  // Multi-select repos to manage
329
339
  const s0 = ui.spinner("Loading repos...");
330
340
  try {
331
341
  const repos = await listOrgRepos(githubToken, githubOrg, isOrg);
332
342
  s0.stop("Loaded repos");
333
343
  if (repos.length > 0) {
334
- selectedRepos = await ui.multiSelect("Which repos should Egregore manage?", repos);
344
+ const filtered = autoAdopt ? repos.filter((r) => r.name !== autoAdopt) : repos;
345
+ if (autoAdopt) {
346
+ console.log("");
347
+ ui.info(`${ui.cyan(autoAdopt)} will be managed automatically (your current repo)`);
348
+ }
349
+ selectedRepos = await ui.multiSelect("Which other repos should Egregore manage?", filtered);
335
350
  } else {
336
351
  ui.info("No repos found yet.");
337
352
  }
338
353
  } catch {
339
354
  s0.stop("Could not load repos");
340
355
  }
356
+ if (autoAdopt && !selectedRepos.includes(autoAdopt)) {
357
+ selectedRepos.unshift(autoAdopt);
358
+ }
359
+ if (autoAdopt) skipRepos.add(autoAdopt);
341
360
 
342
361
  // Detect default branch for each selected repo and convert to {name, base_branch} format
343
362
  if (selectedRepos.length > 0) {
@@ -503,7 +522,7 @@ async function localFounderFlow(ui) {
503
522
  }
504
523
 
505
524
  // 10. Clone locally
506
- const base = process.cwd();
525
+ const base = ctx.targetDir || process.cwd();
507
526
  const egregoreDir = path.join(base, repoName);
508
527
  const memoryDir = path.join(base, memoryRepoName);
509
528
  const forkUrl = `https://github.com/${githubOrg}/${repoName}.git`;
@@ -574,11 +593,16 @@ async function localFounderFlow(ui) {
574
593
  } catch {}
575
594
  }
576
595
 
577
- // 16. Clone managed repos
596
+ // 16. Clone managed repos — skipRepos contains the user's current working
597
+ // repo if auto-adopted, so we don't pull/reconfigure their live workdir.
578
598
  for (let i = 0; i < selectedRepos.length; i++) {
579
599
  const entry = selectedRepos[i];
580
600
  const mrName = typeof entry === "string" ? entry : entry.name;
581
601
  ui.step(5 + i, totalSteps, `Cloning ${mrName}...`);
602
+ if (skipRepos.has(mrName)) {
603
+ ui.success(`Using existing ${mrName}/ (your current repo)`);
604
+ continue;
605
+ }
582
606
  const repoDir = path.join(base, mrName);
583
607
  try {
584
608
  const repoUrl = `https://github.com/${githubOrg}/${mrName}.git`;
@@ -659,7 +683,7 @@ async function localFounderFlow(ui) {
659
683
 
660
684
  // ── Flow 2: Join (local mode) ──────────────────────────────────────
661
685
 
662
- async function localJoinFlow(orgArg, ui) {
686
+ async function localJoinFlow(orgArg, ui, ctx = {}) {
663
687
  // Parse org/repo if provided (e.g., "acme-org/ops-team")
664
688
  let orgLogin, explicitRepo;
665
689
  if (orgArg.includes("/")) {
@@ -698,7 +722,7 @@ async function localJoinFlow(orgArg, ui) {
698
722
 
699
723
  // 5. Clone repos using git credentials (SSH keys / gh CLI / credential helper)
700
724
  // The OAuth token only has repo:invite scope — cloning uses the joiner's own git setup.
701
- const base = process.cwd();
725
+ const base = ctx.targetDir || process.cwd();
702
726
  const egregoreDir = path.join(base, repoName);
703
727
 
704
728
  console.log("");
package/lib/setup.js CHANGED
@@ -101,11 +101,16 @@ async function acceptPendingInvitations(githubToken, githubOrg, ui) {
101
101
  *
102
102
  * @param {object} data - from /api/org/claim/{token}
103
103
  * @param {object} ui - UI module for output
104
- * @param {string} targetDir - where to install (default: cwd)
104
+ * @param {string} [targetDir] - where to install (default: cwd)
105
+ * @param {object} [opts] - optional { skipRepos: Set<string> } — names of managed
106
+ * repos that already exist on disk as the user's own working repo. Skipped
107
+ * entirely (no clone, no pull, no config writes, no develop-branch push) to
108
+ * avoid clobbering uncommitted work or pushing branches to their origin.
105
109
  */
106
- async function install(data, ui, targetDir) {
110
+ async function install(data, ui, targetDir, opts = {}) {
107
111
  const { fork_url, memory_url, github_token, org_name, github_org, slug, api_key, repos = [], telegram_group_link, github_username, github_name, transcript_sharing } = data;
108
112
  const base = targetDir || process.cwd();
113
+ const skipRepos = opts.skipRepos instanceof Set ? opts.skipRepos : new Set();
109
114
 
110
115
  // Directory name from fork URL (what git clone would use), e.g. "egregore-core"
111
116
  const forkDirName = fork_url.split("/").pop().replace(/\.git$/, "");
@@ -233,6 +238,12 @@ async function install(data, ui, targetDir) {
233
238
  const repoName = typeof repoEntry === 'string' ? repoEntry : repoEntry.name;
234
239
  ui.step(6 + i, totalSteps, `Cloning ${repoName}...`);
235
240
  const repoDir = path.join(base, repoName);
241
+ // Auto-adopted from user's current working repo — already on disk, don't touch it.
242
+ if (skipRepos.has(repoName)) {
243
+ ui.success(`Using existing ${repoName}/ (your current repo)`);
244
+ clonedRepos.push(repoName);
245
+ continue;
246
+ }
236
247
  try {
237
248
  if (fs.existsSync(repoDir)) {
238
249
  ui.warn(`${repoName}/ already exists — pulling latest`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "description": "Set up Egregore for your team in one command",
5
5
  "license": "MIT",
6
6
  "bin": {