create-egregore 0.16.3 → 0.17.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.
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/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.");
@@ -294,11 +296,21 @@ async function localFounderFlow(ui) {
294
296
  }
295
297
 
296
298
  // 4. What — new or existing?
297
- const action = await ui.choose("What are you working on?", [
298
- { label: "Starting something new", description: "Create a project from scratch" },
299
- { label: "Existing project", description: "Connect repos I already have" },
300
- ]);
301
- const isNewProject = action.label.startsWith("Starting");
299
+ // If the user came in via the in-repo detection and chose "install
300
+ // alongside", they already told us they have an existing project — don't
301
+ // re-ask. Go straight to the existing-project path.
302
+ let isNewProject;
303
+ if (ctx.prefilledRepo) {
304
+ isNewProject = false;
305
+ console.log("");
306
+ ui.info(`Existing project — we'll manage ${ui.cyan(ctx.prefilledRepo.localName)} plus any others you pick.`);
307
+ } else {
308
+ const action = await ui.choose("What are you working on?", [
309
+ { label: "Starting something new", description: "Create a project from scratch" },
310
+ { label: "Existing project", description: "Connect repos I already have" },
311
+ ]);
312
+ isNewProject = action.label.startsWith("Starting");
313
+ }
302
314
 
303
315
  // 5. Collect project info
304
316
  let orgName, description, selectedRepos = [], newProjectRepo = null;
@@ -325,19 +337,36 @@ async function localFounderFlow(ui) {
325
337
  orgName = nameInput || githubOrg;
326
338
  description = "";
327
339
 
340
+ // Auto-adopt the user's current repo if it's under the chosen GitHub owner.
341
+ const autoAdopt =
342
+ ctx.prefilledRepo &&
343
+ ctx.prefilledRepo.owner &&
344
+ ctx.prefilledRepo.owner.toLowerCase() === githubOrg.toLowerCase()
345
+ ? ctx.prefilledRepo.name
346
+ : null;
347
+
328
348
  // Multi-select repos to manage
329
349
  const s0 = ui.spinner("Loading repos...");
330
350
  try {
331
351
  const repos = await listOrgRepos(githubToken, githubOrg, isOrg);
332
352
  s0.stop("Loaded repos");
333
353
  if (repos.length > 0) {
334
- selectedRepos = await ui.multiSelect("Which repos should Egregore manage?", repos);
354
+ const filtered = autoAdopt ? repos.filter((r) => r.name !== autoAdopt) : repos;
355
+ if (autoAdopt) {
356
+ console.log("");
357
+ ui.info(`${ui.cyan(autoAdopt)} will be managed automatically (your current repo)`);
358
+ }
359
+ selectedRepos = await ui.multiSelect("Which other repos should Egregore manage?", filtered);
335
360
  } else {
336
361
  ui.info("No repos found yet.");
337
362
  }
338
363
  } catch {
339
364
  s0.stop("Could not load repos");
340
365
  }
366
+ if (autoAdopt && !selectedRepos.includes(autoAdopt)) {
367
+ selectedRepos.unshift(autoAdopt);
368
+ }
369
+ if (autoAdopt) skipRepos.add(autoAdopt);
341
370
 
342
371
  // Detect default branch for each selected repo and convert to {name, base_branch} format
343
372
  if (selectedRepos.length > 0) {
@@ -503,7 +532,7 @@ async function localFounderFlow(ui) {
503
532
  }
504
533
 
505
534
  // 10. Clone locally
506
- const base = process.cwd();
535
+ const base = ctx.targetDir || process.cwd();
507
536
  const egregoreDir = path.join(base, repoName);
508
537
  const memoryDir = path.join(base, memoryRepoName);
509
538
  const forkUrl = `https://github.com/${githubOrg}/${repoName}.git`;
@@ -574,11 +603,16 @@ async function localFounderFlow(ui) {
574
603
  } catch {}
575
604
  }
576
605
 
577
- // 16. Clone managed repos
606
+ // 16. Clone managed repos — skipRepos contains the user's current working
607
+ // repo if auto-adopted, so we don't pull/reconfigure their live workdir.
578
608
  for (let i = 0; i < selectedRepos.length; i++) {
579
609
  const entry = selectedRepos[i];
580
610
  const mrName = typeof entry === "string" ? entry : entry.name;
581
611
  ui.step(5 + i, totalSteps, `Cloning ${mrName}...`);
612
+ if (skipRepos.has(mrName)) {
613
+ ui.success(`Using existing ${mrName}/ (your current repo)`);
614
+ continue;
615
+ }
582
616
  const repoDir = path.join(base, mrName);
583
617
  try {
584
618
  const repoUrl = `https://github.com/${githubOrg}/${mrName}.git`;
@@ -659,7 +693,7 @@ async function localFounderFlow(ui) {
659
693
 
660
694
  // ── Flow 2: Join (local mode) ──────────────────────────────────────
661
695
 
662
- async function localJoinFlow(orgArg, ui) {
696
+ async function localJoinFlow(orgArg, ui, ctx = {}) {
663
697
  // Parse org/repo if provided (e.g., "acme-org/ops-team")
664
698
  let orgLogin, explicitRepo;
665
699
  if (orgArg.includes("/")) {
@@ -698,7 +732,7 @@ async function localJoinFlow(orgArg, ui) {
698
732
 
699
733
  // 5. Clone repos using git credentials (SSH keys / gh CLI / credential helper)
700
734
  // The OAuth token only has repo:invite scope — cloning uses the joiner's own git setup.
701
- const base = process.cwd();
735
+ const base = ctx.targetDir || process.cwd();
702
736
  const egregoreDir = path.join(base, repoName);
703
737
 
704
738
  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.3",
3
+ "version": "0.17.1",
4
4
  "description": "Set up Egregore for your team in one command",
5
5
  "license": "MIT",
6
6
  "bin": {