shadow-git 1.0.3 → 1.1.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 (79) hide show
  1. package/CLAUDE.md +5 -1
  2. package/README.md +13 -0
  3. package/dist/cli.js +498 -75
  4. package/package.json +1 -1
  5. package/src/cli.ts +69 -5
  6. package/src/commands/forward.ts +3 -3
  7. package/src/commands/track/add.ts +6 -4
  8. package/src/commands/track/clone.ts +16 -1
  9. package/src/commands/track/destroy.ts +5 -0
  10. package/src/commands/track/ignore.ts +2 -2
  11. package/src/commands/track/index.ts +12 -0
  12. package/src/commands/track/init.ts +5 -1
  13. package/src/commands/track/remove.ts +3 -2
  14. package/src/commands/track/swap.ts +61 -0
  15. package/src/commands/track/sync.ts +9 -6
  16. package/src/commands/track/unswap.ts +49 -0
  17. package/src/commands/track/verify.ts +11 -9
  18. package/src/core/errors.ts +31 -1
  19. package/src/core/exclude.ts +2 -2
  20. package/src/core/gitignore.ts +18 -3
  21. package/src/core/repository.ts +8 -0
  22. package/src/core/swap-mode.ts +83 -0
  23. package/src/core/update-check.ts +214 -0
  24. package/src/utils/logger.ts +20 -0
  25. package/tests/commands/forward.test.ts +6 -6
  26. package/tests/commands/track/add.test.ts +6 -6
  27. package/tests/commands/track/swap.test.ts +266 -0
  28. package/tests/commands/track/verify.test.ts +16 -16
  29. package/tests/core/exclude.test.ts +10 -10
  30. package/tests/core/gitignore.test.ts +30 -30
  31. package/tests/core/update-check.test.ts +494 -0
  32. package/tests/helpers/guard-child.mjs +2 -2
  33. package/.claude/settings.local.json +0 -43
  34. package/.temp/.sgit/COMMIT_EDITMSG +0 -1
  35. package/.temp/.sgit/HEAD +0 -1
  36. package/.temp/.sgit/config +0 -8
  37. package/.temp/.sgit/description +0 -1
  38. package/.temp/.sgit/hooks/applypatch-msg.sample +0 -15
  39. package/.temp/.sgit/hooks/commit-msg.sample +0 -24
  40. package/.temp/.sgit/hooks/fsmonitor-watchman.sample +0 -174
  41. package/.temp/.sgit/hooks/post-update.sample +0 -8
  42. package/.temp/.sgit/hooks/pre-applypatch.sample +0 -14
  43. package/.temp/.sgit/hooks/pre-commit +0 -6
  44. package/.temp/.sgit/hooks/pre-commit.sample +0 -49
  45. package/.temp/.sgit/hooks/pre-merge-commit.sample +0 -13
  46. package/.temp/.sgit/hooks/pre-push.sample +0 -53
  47. package/.temp/.sgit/hooks/pre-rebase.sample +0 -169
  48. package/.temp/.sgit/hooks/pre-receive.sample +0 -24
  49. package/.temp/.sgit/hooks/prepare-commit-msg.sample +0 -42
  50. package/.temp/.sgit/hooks/push-to-checkout.sample +0 -78
  51. package/.temp/.sgit/hooks/sendemail-validate.sample +0 -77
  52. package/.temp/.sgit/hooks/update.sample +0 -128
  53. package/.temp/.sgit/index +0 -0
  54. package/.temp/.sgit/info/exclude +0 -20
  55. package/.temp/.sgit/logs/HEAD +0 -1
  56. package/.temp/.sgit/logs/refs/heads/master +0 -1
  57. package/.temp/.sgit/objects/03/7f4e37a02573e40ab78b35dd4b442cc6110bae +0 -0
  58. package/.temp/.sgit/objects/2f/f0fd85dec20039eef6ff63ead905778a88843f +0 -0
  59. package/.temp/.sgit/objects/34/b3190a8a836593fb568a56c073a416a56fbf6d +0 -0
  60. package/.temp/.sgit/objects/70/ad6bbbfefd330d72a43dc2a810e6ef42269911 +0 -0
  61. package/.temp/.sgit/objects/90/4e09c7aadaa32d2426ab5464a975d5151ef195 +0 -0
  62. package/.temp/.sgit/objects/a4/6358feef49515d5f85d1d3a64e929182da7149 +0 -3
  63. package/.temp/.sgit/objects/c3/2432f724b666f647fe21a1ce16535cd7693f74 +0 -0
  64. package/.temp/.sgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
  65. package/.temp/.sgit/objects/e8/0f4253888cdadda1e3d810f6a3cb3c68c20b50 +0 -0
  66. package/.temp/.sgit/objects/ee/9dd32cca3b134623d184eb4de5805704c493e3 +0 -0
  67. package/.temp/.sgit/refs/heads/master +0 -1
  68. package/.temp/.sgit.json +0 -10
  69. package/.temp/README.md +0 -0
  70. package/.temp/docs/guide.md +0 -10
  71. package/.temp/lib/__init__.py +0 -1
  72. package/.temp/lib/utils.py +0 -5
  73. package/.temp/rec1/rec2/test +0 -0
  74. package/.temp/src/config.py +0 -3
  75. package/.temp/src/main.py +0 -5
  76. package/.temp/temp/shouldnt-track +0 -0
  77. package/.temp/temp.md +0 -3
  78. package/.temp/test/1 +0 -0
  79. package/.temp/tests/test_utils.py +0 -7
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
5
+ import { fileURLToPath } from "url";
4
6
  import { Command as Command2 } from "commander";
5
7
 
6
8
  // src/core/repository.ts
@@ -70,6 +72,44 @@ var PathNotTrackedError = class extends SgitError {
70
72
  super(`${path} is not tracked by sgit`);
71
73
  }
72
74
  };
75
+ var StaleGitignoreError = class extends SgitError {
76
+ constructor(files) {
77
+ super(
78
+ [
79
+ "found stale .gitignore.sgit-parked files (a previous sgit command did not complete cleanly):",
80
+ ...files.map((f) => ` ${f}`),
81
+ "",
82
+ "rename them back to .gitignore manually, then retry."
83
+ ].join("\n")
84
+ );
85
+ }
86
+ };
87
+ var AlreadySwappedError = class extends SgitError {
88
+ constructor() {
89
+ super("shadow-git is already in swap mode; run 'sgit track unswap' to exit.");
90
+ }
91
+ };
92
+ var NotSwappedError = class extends SgitError {
93
+ constructor() {
94
+ super("shadow-git is not in swap mode.");
95
+ }
96
+ };
97
+ var SwapModeActiveError = class extends SgitError {
98
+ constructor(op) {
99
+ super(`cannot ${op} while in swap mode; run 'sgit track unswap' first.`);
100
+ }
101
+ };
102
+ var SwapModeCorruptError = class extends SgitError {
103
+ constructor(details) {
104
+ super(`shadow-git state is inconsistent:
105
+ ${details}`);
106
+ }
107
+ };
108
+ var InProgressGitOpError = class extends SgitError {
109
+ constructor(gitDir, marker) {
110
+ super(`cannot proceed: ${gitDir} has an in-progress operation (${marker}). Finish or abort it first.`);
111
+ }
112
+ };
73
113
 
74
114
  // src/core/repository.ts
75
115
  async function findRoot() {
@@ -96,6 +136,12 @@ async function validateGitIdentity(root) {
96
136
  async function validateShadowRepo(root) {
97
137
  try {
98
138
  await access(join(root, ".sgit"), constants.F_OK);
139
+ return;
140
+ } catch {
141
+ }
142
+ try {
143
+ await access(join(root, ".git.sgit-parked"), constants.F_OK);
144
+ return;
99
145
  } catch {
100
146
  throw new NotInitializedError();
101
147
  }
@@ -119,9 +165,74 @@ async function validateNotSubmodule(root) {
119
165
 
120
166
  // src/commands/forward.ts
121
167
  import { spawnSync } from "child_process";
168
+
169
+ // src/core/swap-mode.ts
170
+ import { access as access2 } from "fs/promises";
171
+ import { constants as constants2, existsSync } from "fs";
122
172
  import { join as join2 } from "path";
123
- function forwardToGit(root, args) {
124
- const sgitDir = join2(root, ".sgit");
173
+ var SWAP_MARKER = ".git.sgit-parked";
174
+ async function pathExists(path) {
175
+ try {
176
+ await access2(path, constants2.F_OK);
177
+ return true;
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+ async function isSwapMode(root) {
183
+ return pathExists(join2(root, SWAP_MARKER));
184
+ }
185
+ function isSwapModeSync(root) {
186
+ return existsSync(join2(root, SWAP_MARKER));
187
+ }
188
+ async function resolveShadowDir(root) {
189
+ return await isSwapMode(root) ? join2(root, ".git") : join2(root, ".sgit");
190
+ }
191
+ async function resolveMainDir(root) {
192
+ return await isSwapMode(root) ? join2(root, SWAP_MARKER) : join2(root, ".git");
193
+ }
194
+ var IN_PROGRESS_MARKERS = [
195
+ "index.lock",
196
+ "MERGE_HEAD",
197
+ "CHERRY_PICK_HEAD",
198
+ "REVERT_HEAD",
199
+ "rebase-apply",
200
+ "rebase-merge",
201
+ "BISECT_LOG"
202
+ ];
203
+ async function assertNoInProgressOps(gitDir) {
204
+ for (const marker of IN_PROGRESS_MARKERS) {
205
+ if (await pathExists(join2(gitDir, marker))) {
206
+ throw new InProgressGitOpError(gitDir, marker);
207
+ }
208
+ }
209
+ }
210
+ async function assertConsistentState(root) {
211
+ const hasDisabled = await pathExists(join2(root, SWAP_MARKER));
212
+ const hasGit = await pathExists(join2(root, ".git"));
213
+ const hasSgit = await pathExists(join2(root, ".sgit"));
214
+ if (hasDisabled && !hasGit) {
215
+ throw new SwapModeCorruptError(
216
+ [
217
+ `found ${SWAP_MARKER}/ but no .git/ \u2014 a swap-mode transition was interrupted.`,
218
+ hasSgit ? `to recover: rename ${SWAP_MARKER} back to .git (.sgit is still intact).` : `to recover: rename ${SWAP_MARKER} back to .git, or rename whichever of .sgit/${SWAP_MARKER} you want active to .git.`
219
+ ].join("\n")
220
+ );
221
+ }
222
+ if (hasDisabled && hasSgit) {
223
+ throw new SwapModeCorruptError(
224
+ [
225
+ `found both ${SWAP_MARKER}/ and .sgit/ \u2014 unexpected state.`,
226
+ "normally only one exists at a time.",
227
+ `investigate and remove the stale one before proceeding.`
228
+ ].join("\n")
229
+ );
230
+ }
231
+ }
232
+
233
+ // src/commands/forward.ts
234
+ async function forwardToGit(root, args) {
235
+ const sgitDir = await resolveShadowDir(root);
125
236
  const result = spawnSync(
126
237
  "git",
127
238
  [`--git-dir=${sgitDir}`, `--work-tree=${root}`, ...args],
@@ -152,11 +263,32 @@ function error(msg) {
152
263
  process.stderr.write(`${RED}${msg}${RESET}
153
264
  `);
154
265
  }
266
+ function swapModeBanner() {
267
+ const msg = [
268
+ "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
269
+ "shadow-git is in SWAP MODE",
270
+ "main git is parked at .git.sgit-parked; .sgit is active at .git",
271
+ "this mode is for IDE/tool compatibility \u2014 read-only recommended",
272
+ "run 'sgit track unswap' before making main-repo changes",
273
+ "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
274
+ ].join("\n");
275
+ process.stderr.write(`${YELLOW}${msg}${RESET}
276
+ `);
277
+ }
278
+ function updateNotice(current, latest) {
279
+ const msg = [
280
+ `Update available: shadow-git ${current} \u2192 ${latest}`,
281
+ "Please run 'npm install -g shadow-git' to update."
282
+ ].join("\n");
283
+ process.stderr.write(`
284
+ ${CYAN}${msg}${RESET}
285
+ `);
286
+ }
155
287
 
156
288
  // src/commands/track/init.ts
157
289
  import { join as join5 } from "path";
158
- import { mkdir as mkdir2, writeFile as writeFile3, readFile as readFile3, access as access3 } from "fs/promises";
159
- import { constants as constants3 } from "fs";
290
+ import { mkdir as mkdir2, writeFile as writeFile3, readFile as readFile3, access as access4 } from "fs/promises";
291
+ import { constants as constants4 } from "fs";
160
292
 
161
293
  // src/core/manifest.ts
162
294
  import { readFile, writeFile } from "fs/promises";
@@ -246,7 +378,7 @@ function generateMainExclude(existingContent, manifestPaths) {
246
378
  while (preserved.length > 0 && preserved[preserved.length - 1].trim() === "") {
247
379
  preserved.pop();
248
380
  }
249
- const sgitEntries = [".sgit", ".sgit.json", ".gitignore-sgit.bat", ...manifestPaths];
381
+ const sgitEntries = [".sgit", ".sgit.json", ".git.sgit-parked", ".gitignore.sgit-parked", ...manifestPaths];
250
382
  return [
251
383
  ...preserved,
252
384
  "",
@@ -291,7 +423,7 @@ function generateShadowExclude(manifestPaths, ignorePaths = []) {
291
423
  "!.sgit.json",
292
424
  ...entryLines,
293
425
  ...ignorePaths,
294
- ".gitignore-sgit.bat",
426
+ ".gitignore.sgit-parked",
295
427
  SGIT_BLOCK_END
296
428
  ].join("\n") + "\n";
297
429
  }
@@ -312,7 +444,7 @@ import {
312
444
  mkdir,
313
445
  writeFile as writeFile2,
314
446
  rename,
315
- access as access2,
447
+ access as access3,
316
448
  lstat as lstat2,
317
449
  unlink,
318
450
  copyFile,
@@ -320,7 +452,7 @@ import {
320
452
  rm,
321
453
  readdir
322
454
  } from "fs/promises";
323
- import { constants as constants2 } from "fs";
455
+ import { constants as constants3 } from "fs";
324
456
  var RUNNER_SCRIPT = `#!/bin/sh
325
457
  # sgit hook runner \u2014 executes all hooks in pre-commit.d/
326
458
  dir="$(dirname "$0")/pre-commit.d"
@@ -434,7 +566,7 @@ async function uninstallMainHook(root) {
434
566
  }
435
567
  async function exists(path) {
436
568
  try {
437
- await access2(path, constants2.F_OK);
569
+ await access3(path, constants3.F_OK);
438
570
  return true;
439
571
  } catch {
440
572
  return false;
@@ -444,12 +576,15 @@ async function exists(path) {
444
576
  // src/commands/track/init.ts
445
577
  async function trackInit() {
446
578
  const root = await findRoot();
579
+ if (await isSwapMode(root)) {
580
+ throw new SwapModeActiveError("init");
581
+ }
447
582
  await validateNotWorktree(root);
448
583
  await validateNotSubmodule(root);
449
584
  await validateGitIdentity(root);
450
585
  const sgitDir = join5(root, ".sgit");
451
586
  try {
452
- await access3(sgitDir, constants3.F_OK);
587
+ await access4(sgitDir, constants4.F_OK);
453
588
  throw new AlreadyInitializedError();
454
589
  } catch (err) {
455
590
  if (err instanceof AlreadyInitializedError) throw err;
@@ -501,28 +636,30 @@ async function trackInit() {
501
636
 
502
637
  // src/commands/track/sync.ts
503
638
  import { join as join6 } from "path";
504
- import { writeFile as writeFile4, readFile as readFile4, mkdir as mkdir3, access as access4 } from "fs/promises";
505
- import { constants as constants4 } from "fs";
639
+ import { writeFile as writeFile4, readFile as readFile4, mkdir as mkdir3, access as access5 } from "fs/promises";
640
+ import { constants as constants5 } from "fs";
506
641
  async function syncExcludes(root) {
507
642
  const manifest = await readManifest(root);
508
- const mainExcludePath = join6(root, ".git", "info", "exclude");
643
+ const mainDir = await resolveMainDir(root);
644
+ const shadowDir = await resolveShadowDir(root);
645
+ const mainExcludePath = join6(mainDir, "info", "exclude");
509
646
  let existingExclude = "";
510
647
  try {
511
648
  existingExclude = await readFile4(mainExcludePath, "utf-8");
512
649
  } catch {
513
650
  }
514
- await mkdir3(join6(root, ".git", "info"), { recursive: true });
651
+ await mkdir3(join6(mainDir, "info"), { recursive: true });
515
652
  await writeFile4(
516
653
  mainExcludePath,
517
654
  generateMainExclude(existingExclude, manifest.paths)
518
655
  );
519
- const sgitExcludePath = join6(root, ".sgit", "info", "exclude");
520
- await mkdir3(join6(root, ".sgit", "info"), { recursive: true });
656
+ const sgitExcludePath = join6(shadowDir, "info", "exclude");
657
+ await mkdir3(join6(shadowDir, "info"), { recursive: true });
521
658
  await writeFile4(sgitExcludePath, generateShadowExclude(manifest.paths, manifest.ignore));
522
659
  for (const p of manifest.paths) {
523
660
  const checkPath = p.endsWith("/") ? p.slice(0, -1) : p;
524
661
  try {
525
- await access4(join6(root, checkPath), constants4.F_OK);
662
+ await access5(join6(root, checkPath), constants5.F_OK);
526
663
  } catch {
527
664
  warn(`path does not exist: ${p}`);
528
665
  }
@@ -537,8 +674,8 @@ async function trackSync() {
537
674
 
538
675
  // src/commands/track/add.ts
539
676
  import { join as join7 } from "path";
540
- import { access as access5 } from "fs/promises";
541
- import { constants as constants5 } from "fs";
677
+ import { access as access6 } from "fs/promises";
678
+ import { constants as constants6 } from "fs";
542
679
 
543
680
  // src/core/paths.ts
544
681
  import { resolve as resolve2, relative, sep } from "path";
@@ -590,11 +727,12 @@ function isPathCoveredByManifest(filePath, manifestPaths, ignorePaths = []) {
590
727
  }
591
728
 
592
729
  // src/commands/track/add.ts
593
- var BLOCKED_BASENAMES = /* @__PURE__ */ new Set([".gitignore", ".gitignore-sgit.bat"]);
730
+ var BLOCKED_BASENAMES = /* @__PURE__ */ new Set([".gitignore", ".gitignore.sgit-parked"]);
594
731
  async function trackAdd(paths, opts = {}) {
595
732
  const root = await findRoot();
596
733
  await validateShadowRepo(root);
597
- const sgitDir = join7(root, ".sgit");
734
+ const sgitDir = await resolveShadowDir(root);
735
+ const mainDir = await resolveMainDir(root);
598
736
  const added = [];
599
737
  for (const inputPath of paths) {
600
738
  const normalized = await normalizePath(inputPath, root);
@@ -606,7 +744,7 @@ async function trackAdd(paths, opts = {}) {
606
744
  }
607
745
  const checkPath = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
608
746
  try {
609
- await access5(join7(root, checkPath), constants5.F_OK);
747
+ await access6(join7(root, checkPath), constants6.F_OK);
610
748
  } catch {
611
749
  throw new PathNotFoundError(normalized);
612
750
  }
@@ -615,7 +753,7 @@ async function trackAdd(paths, opts = {}) {
615
753
  let trackedByMain = false;
616
754
  try {
617
755
  await execGit(["ls-files", "--error-unmatch", checkPath], {
618
- gitDir: join7(root, ".git"),
756
+ gitDir: mainDir,
619
757
  workTree: root
620
758
  });
621
759
  trackedByMain = true;
@@ -624,7 +762,7 @@ async function trackAdd(paths, opts = {}) {
624
762
  if (trackedByMain) {
625
763
  if (opts.adopt && !alreadyTracked) {
626
764
  const rmArgs = normalized.endsWith("/") ? ["rm", "--cached", "-r", checkPath] : ["rm", "--cached", checkPath];
627
- await execGit(rmArgs, { gitDir: join7(root, ".git"), workTree: root });
765
+ await execGit(rmArgs, { gitDir: mainDir, workTree: root });
628
766
  } else if (!opts.adopt) {
629
767
  throw new PathTrackedByMainError(normalized);
630
768
  }
@@ -653,11 +791,11 @@ async function trackAdd(paths, opts = {}) {
653
791
  }
654
792
 
655
793
  // src/commands/track/remove.ts
656
- import { join as join8, resolve as resolve3, relative as relative2, sep as sep2 } from "path";
794
+ import { resolve as resolve3, relative as relative2, sep as sep2 } from "path";
657
795
  async function trackRemove(paths) {
658
796
  const root = await findRoot();
659
797
  await validateShadowRepo(root);
660
- const sgitDir = join8(root, ".sgit");
798
+ const sgitDir = await resolveShadowDir(root);
661
799
  const removed = [];
662
800
  for (const inputPath of paths) {
663
801
  const absolute = resolve3(process.cwd(), inputPath);
@@ -701,26 +839,46 @@ async function trackList() {
701
839
  return manifest.paths;
702
840
  }
703
841
 
704
- // src/commands/track/verify.ts
705
- import { join as join10 } from "path";
706
-
707
842
  // src/core/gitignore.ts
708
843
  import {
709
844
  readdirSync,
710
845
  renameSync,
711
- existsSync,
846
+ existsSync as existsSync2,
712
847
  readFileSync,
713
848
  writeFileSync,
714
849
  unlinkSync
715
850
  } from "fs";
716
- import { join as join9, relative as relative3, sep as sep3 } from "path";
717
- var DISABLED_NAME = ".gitignore-sgit.bat";
851
+ import { join as join8, relative as relative3, sep as sep3 } from "path";
852
+ var DISABLED_NAME = ".gitignore.sgit-parked";
718
853
  var LOCK_FILE = ".gitignore-guard";
854
+ function refuseIfSwapped(root, op) {
855
+ if (isSwapModeSync(root)) {
856
+ throw new SwapModeActiveError(op);
857
+ }
858
+ }
859
+ function disableGitignores(root, manifestPaths) {
860
+ refuseIfSwapped(root, "disable gitignores");
861
+ const gitignores = findInTrackedDirs(root, manifestPaths, ".gitignore");
862
+ if (gitignores.length === 0) return [];
863
+ const stale = findInTrackedDirs(root, manifestPaths, DISABLED_NAME);
864
+ if (stale.length > 0) {
865
+ throw new StaleGitignoreError(stale);
866
+ }
867
+ const disabled = [];
868
+ for (const relPath of gitignores) {
869
+ const absPath = join8(root, relPath);
870
+ const dir = absPath.substring(0, absPath.length - ".gitignore".length);
871
+ renameSync(absPath, dir + DISABLED_NAME);
872
+ disabled.push(relPath);
873
+ }
874
+ return disabled;
875
+ }
719
876
  function enableGitignores(root, manifestPaths) {
877
+ refuseIfSwapped(root, "enable gitignores");
720
878
  const disabled = findInTrackedDirs(root, manifestPaths, DISABLED_NAME);
721
879
  const enabled = [];
722
880
  for (const relPath of disabled) {
723
- const absPath = join9(root, relPath);
881
+ const absPath = join8(root, relPath);
724
882
  const restoredPath = absPath.substring(0, absPath.length - DISABLED_NAME.length) + ".gitignore";
725
883
  renameSync(absPath, restoredPath);
726
884
  enabled.push(relPath);
@@ -731,7 +889,7 @@ function findStaleGitignores(root, manifestPaths) {
731
889
  return findInTrackedDirs(root, manifestPaths, DISABLED_NAME);
732
890
  }
733
891
  function isGuardActive(root) {
734
- const lockPath = join9(root, ".sgit", LOCK_FILE);
892
+ const lockPath = join8(root, ".sgit", LOCK_FILE);
735
893
  try {
736
894
  const pid = parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
737
895
  if (isNaN(pid)) return false;
@@ -742,7 +900,8 @@ function isGuardActive(root) {
742
900
  }
743
901
  }
744
902
  function setupGitignoreGuard(root, manifestPaths) {
745
- const lockPath = join9(root, ".sgit", LOCK_FILE);
903
+ refuseIfSwapped(root, "set up gitignore guard");
904
+ const lockPath = join8(root, ".sgit", LOCK_FILE);
746
905
  if (isGuardActive(root)) {
747
906
  return () => {
748
907
  };
@@ -750,7 +909,7 @@ function setupGitignoreGuard(root, manifestPaths) {
750
909
  const stale = findInTrackedDirs(root, manifestPaths, DISABLED_NAME);
751
910
  if (stale.length > 0) {
752
911
  warn(
753
- "found stale .gitignore-sgit.bat files from a previous crash; restoring"
912
+ "found stale .gitignore.sgit-parked files from a previous crash; restoring"
754
913
  );
755
914
  enableGitignores(root, manifestPaths);
756
915
  }
@@ -761,7 +920,7 @@ function setupGitignoreGuard(root, manifestPaths) {
761
920
  };
762
921
  }
763
922
  for (const relPath of gitignores) {
764
- const absPath = join9(root, relPath);
923
+ const absPath = join8(root, relPath);
765
924
  const dir = absPath.substring(0, absPath.length - ".gitignore".length);
766
925
  renameSync(absPath, dir + DISABLED_NAME);
767
926
  }
@@ -804,15 +963,15 @@ function findInTrackedDirs(root, manifestPaths, targetName) {
804
963
  const found = /* @__PURE__ */ new Set();
805
964
  for (const mPath of manifestPaths) {
806
965
  for (const ancestor of getAncestorDirs(mPath)) {
807
- const dirAbs = ancestor === "" ? root : join9(root, ancestor);
808
- const targetPath = join9(dirAbs, targetName);
809
- if (existsSync(targetPath)) {
966
+ const dirAbs = ancestor === "" ? root : join8(root, ancestor);
967
+ const targetPath = join8(dirAbs, targetName);
968
+ if (existsSync2(targetPath)) {
810
969
  found.add(targetPath);
811
970
  }
812
971
  }
813
972
  if (mPath.endsWith("/")) {
814
- const dirAbs = join9(root, mPath);
815
- if (existsSync(dirAbs)) {
973
+ const dirAbs = join8(root, mPath);
974
+ if (existsSync2(dirAbs)) {
816
975
  walkSync(dirAbs, targetName, found);
817
976
  }
818
977
  }
@@ -840,7 +999,7 @@ function walkSync(dir, targetName, results) {
840
999
  if (entry.isDirectory() && (entry.name === ".git" || entry.name === ".sgit")) {
841
1000
  continue;
842
1001
  }
843
- const fullPath = join9(dir, entry.name);
1002
+ const fullPath = join8(dir, entry.name);
844
1003
  if (entry.isDirectory()) {
845
1004
  walkSync(fullPath, targetName, results);
846
1005
  } else if (entry.name === targetName) {
@@ -859,17 +1018,17 @@ function cleanStaleLock(lockPath) {
859
1018
  async function verifyMain(root) {
860
1019
  const manifest = await readManifest(root);
861
1020
  const violations = [];
862
- if (!isGuardActive(root)) {
1021
+ if (!isGuardActive(root) && !await isSwapMode(root)) {
863
1022
  const stale = findStaleGitignores(root, manifest.paths);
864
1023
  violations.push(
865
1024
  ...stale.map(
866
- (f) => `${f} (stale .gitignore-sgit.bat \u2014 rename back to .gitignore)`
1025
+ (f) => `${f} (stale .gitignore.sgit-parked \u2014 rename back to .gitignore)`
867
1026
  )
868
1027
  );
869
1028
  }
870
1029
  try {
871
1030
  const staged = await execGit(["diff", "--cached", "--name-only", "--diff-filter=ACMR"], {
872
- gitDir: join10(root, ".git"),
1031
+ gitDir: await resolveMainDir(root),
873
1032
  workTree: root
874
1033
  });
875
1034
  if (staged.trim()) {
@@ -892,13 +1051,13 @@ async function verifyShadow(root) {
892
1051
  violations: [".sgit.json (missing or invalid on disk)"]
893
1052
  };
894
1053
  }
895
- const sgitDir = join10(root, ".sgit");
1054
+ const sgitDir = await resolveShadowDir(root);
896
1055
  const violations = [];
897
- if (!isGuardActive(root)) {
1056
+ if (!isGuardActive(root) && !await isSwapMode(root)) {
898
1057
  const stale = findStaleGitignores(root, manifest.paths);
899
1058
  violations.push(
900
1059
  ...stale.map(
901
- (f) => `${f} (stale .gitignore-sgit.bat \u2014 rename back to .gitignore)`
1060
+ (f) => `${f} (stale .gitignore.sgit-parked \u2014 rename back to .gitignore)`
902
1061
  )
903
1062
  );
904
1063
  }
@@ -987,19 +1146,22 @@ async function trackVerify(opts) {
987
1146
  }
988
1147
 
989
1148
  // src/commands/track/clone.ts
990
- import { join as join11, extname } from "path";
991
- import { access as access6, readFile as readFile5, writeFile as writeFile5, rename as rename2, mkdir as mkdir4, rm as rm2 } from "fs/promises";
992
- import { constants as constants6 } from "fs";
1149
+ import { join as join9, extname } from "path";
1150
+ import { access as access7, readFile as readFile5, writeFile as writeFile5, rename as rename2, mkdir as mkdir4, rm as rm2 } from "fs/promises";
1151
+ import { constants as constants7 } from "fs";
993
1152
  import { createInterface } from "readline";
994
1153
  import { randomBytes } from "crypto";
995
1154
  async function trackClone(url, opts = {}) {
996
1155
  const root = await findRoot();
1156
+ if (await isSwapMode(root)) {
1157
+ throw new SwapModeActiveError("clone");
1158
+ }
997
1159
  await validateNotWorktree(root);
998
1160
  await validateNotSubmodule(root);
999
1161
  await validateGitIdentity(root);
1000
- const sgitDir = join11(root, ".sgit");
1162
+ const sgitDir = join9(root, ".sgit");
1001
1163
  try {
1002
- await access6(sgitDir, constants6.F_OK);
1164
+ await access7(sgitDir, constants7.F_OK);
1003
1165
  throw new AlreadyInitializedError();
1004
1166
  } catch (err) {
1005
1167
  if (err instanceof AlreadyInitializedError) throw err;
@@ -1015,6 +1177,11 @@ async function trackClone(url, opts = {}) {
1015
1177
  }
1016
1178
  await execGit(["config", "core.bare", "false"], { gitDir: sgitDir });
1017
1179
  await execGit(["config", "core.worktree", root], { gitDir: sgitDir });
1180
+ await execGit(
1181
+ ["config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"],
1182
+ { gitDir: sgitDir }
1183
+ );
1184
+ await execGit(["fetch", "origin"], { gitDir: sgitDir });
1018
1185
  try {
1019
1186
  const name = await execGit(["config", "user.name"], { cwd: root });
1020
1187
  if (name) {
@@ -1047,7 +1214,7 @@ async function trackClone(url, opts = {}) {
1047
1214
  const conflicts = [];
1048
1215
  for (const file of trackedFiles) {
1049
1216
  try {
1050
- await access6(join11(root, file), constants6.F_OK);
1217
+ await access7(join9(root, file), constants7.F_OK);
1051
1218
  conflicts.push(file);
1052
1219
  } catch {
1053
1220
  }
@@ -1066,19 +1233,19 @@ async function trackClone(url, opts = {}) {
1066
1233
  if (choice === 2) {
1067
1234
  const savedContent = /* @__PURE__ */ new Map();
1068
1235
  for (const file of conflicts) {
1069
- savedContent.set(file, await readFile5(join11(root, file)));
1236
+ savedContent.set(file, await readFile5(join9(root, file)));
1070
1237
  }
1071
1238
  if (trackedFiles.length > 0) {
1072
1239
  await execGit(["checkout", "HEAD", "--", "."], { gitDir: sgitDir, workTree: root });
1073
1240
  }
1074
1241
  for (const [file, content] of savedContent) {
1075
- await mkdir4(join11(root, file, ".."), { recursive: true });
1076
- await writeFile5(join11(root, file), content);
1242
+ await mkdir4(join9(root, file, ".."), { recursive: true });
1243
+ await writeFile5(join9(root, file), content);
1077
1244
  }
1078
1245
  } else if (choice === 3) {
1079
1246
  for (const file of conflicts) {
1080
1247
  const renamed = generateOriginalName(file);
1081
- await rename2(join11(root, file), join11(root, renamed));
1248
+ await rename2(join9(root, file), join9(root, renamed));
1082
1249
  }
1083
1250
  if (trackedFiles.length > 0) {
1084
1251
  await execGit(["checkout", "HEAD", "--", "."], { gitDir: sgitDir, workTree: root });
@@ -1146,13 +1313,16 @@ function generateOriginalName(filePath) {
1146
1313
  }
1147
1314
 
1148
1315
  // src/commands/track/destroy.ts
1149
- import { join as join12, dirname as dirname2 } from "path";
1316
+ import { join as join10, dirname as dirname2 } from "path";
1150
1317
  import { readFile as readFile6, writeFile as writeFile6, rm as rm3, rmdir, unlink as unlink2 } from "fs/promises";
1151
1318
  import { createInterface as createInterface2 } from "readline";
1152
1319
  async function trackDestroy(opts) {
1153
1320
  const root = await findRoot();
1321
+ if (await isSwapMode(root)) {
1322
+ throw new SwapModeActiveError("destroy");
1323
+ }
1154
1324
  await validateShadowRepo(root);
1155
- const sgitDir = join12(root, ".sgit");
1325
+ const sgitDir = join10(root, ".sgit");
1156
1326
  let trackedFiles = [];
1157
1327
  try {
1158
1328
  const output = await execGit(["ls-tree", "-r", "--name-only", "HEAD"], {
@@ -1193,7 +1363,7 @@ async function trackDestroy(opts) {
1193
1363
  return;
1194
1364
  }
1195
1365
  }
1196
- const excludePath = join12(root, ".git", "info", "exclude");
1366
+ const excludePath = join10(root, ".git", "info", "exclude");
1197
1367
  try {
1198
1368
  const content = await readFile6(excludePath, "utf-8");
1199
1369
  const lines = content.split("\n");
@@ -1229,7 +1399,7 @@ async function trackDestroy(opts) {
1229
1399
  if (!opts.keepFiles && trackedFiles.length > 0) {
1230
1400
  for (const file of trackedFiles) {
1231
1401
  try {
1232
- await unlink2(join12(root, file));
1402
+ await unlink2(join10(root, file));
1233
1403
  } catch {
1234
1404
  }
1235
1405
  }
@@ -1244,13 +1414,13 @@ async function trackDestroy(opts) {
1244
1414
  const sortedDirs = [...dirs].sort((a, b) => b.length - a.length);
1245
1415
  for (const dir of sortedDirs) {
1246
1416
  try {
1247
- await rmdir(join12(root, dir));
1417
+ await rmdir(join10(root, dir));
1248
1418
  } catch {
1249
1419
  }
1250
1420
  }
1251
1421
  }
1252
- await rm3(join12(root, ".sgit"), { recursive: true, force: true });
1253
- await rm3(join12(root, ".sgit.json"), { force: true });
1422
+ await rm3(join10(root, ".sgit"), { recursive: true, force: true });
1423
+ await rm3(join10(root, ".sgit.json"), { force: true });
1254
1424
  if (opts.keepFiles) {
1255
1425
  success("Shadow repository removed. Tracked files kept on disk.");
1256
1426
  } else {
@@ -1271,11 +1441,10 @@ async function confirm(message) {
1271
1441
  }
1272
1442
 
1273
1443
  // src/commands/track/ignore.ts
1274
- import { join as join13 } from "path";
1275
1444
  async function trackIgnore(paths) {
1276
1445
  const root = await findRoot();
1277
1446
  await validateShadowRepo(root);
1278
- const sgitDir = join13(root, ".sgit");
1447
+ const sgitDir = await resolveShadowDir(root);
1279
1448
  const added = [];
1280
1449
  for (const inputPath of paths) {
1281
1450
  const normalized = await normalizePath(inputPath, root);
@@ -1337,7 +1506,7 @@ async function trackUnignore(paths) {
1337
1506
  }
1338
1507
 
1339
1508
  // src/commands/track/completion.ts
1340
- import { join as join14, dirname as dirname3 } from "path";
1509
+ import { join as join11, dirname as dirname3 } from "path";
1341
1510
  import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
1342
1511
  import { homedir } from "os";
1343
1512
  function bashCompletion() {
@@ -1566,11 +1735,11 @@ function getInstallPath(shell) {
1566
1735
  const home = homedir();
1567
1736
  switch (shell) {
1568
1737
  case "zsh":
1569
- return join14(home, ".zsh", "completions", "_sgit");
1738
+ return join11(home, ".zsh", "completions", "_sgit");
1570
1739
  case "fish":
1571
- return join14(home, ".config", "fish", "completions", "sgit.fish");
1740
+ return join11(home, ".config", "fish", "completions", "sgit.fish");
1572
1741
  default:
1573
- return join14(home, ".local", "share", "bash-completion", "completions", "sgit");
1742
+ return join11(home, ".local", "share", "bash-completion", "completions", "sgit");
1574
1743
  }
1575
1744
  }
1576
1745
  async function trackCompletion(action, opts) {
@@ -1596,6 +1765,76 @@ async function trackCompletion(action, opts) {
1596
1765
  process.exit(1);
1597
1766
  }
1598
1767
 
1768
+ // src/commands/track/swap.ts
1769
+ import { join as join12 } from "path";
1770
+ import { rename as rename3 } from "fs/promises";
1771
+ async function trackSwap() {
1772
+ const root = await findRoot();
1773
+ await validateShadowRepo(root);
1774
+ if (await isSwapMode(root)) {
1775
+ throw new AlreadySwappedError();
1776
+ }
1777
+ await assertNoInProgressOps(join12(root, ".git"));
1778
+ const manifest = await readManifest(root);
1779
+ const gitPath = join12(root, ".git");
1780
+ const sgitPath = join12(root, ".sgit");
1781
+ const disabledPath = join12(root, SWAP_MARKER);
1782
+ disableGitignores(root, manifest.paths);
1783
+ let gitRenamed = false;
1784
+ try {
1785
+ await rename3(gitPath, disabledPath);
1786
+ gitRenamed = true;
1787
+ await rename3(sgitPath, gitPath);
1788
+ } catch (err) {
1789
+ if (gitRenamed) {
1790
+ try {
1791
+ await rename3(disabledPath, gitPath);
1792
+ } catch {
1793
+ }
1794
+ }
1795
+ try {
1796
+ enableGitignores(root, manifest.paths);
1797
+ } catch {
1798
+ }
1799
+ throw err;
1800
+ }
1801
+ success(
1802
+ "Swap mode enabled. .sgit is now .git; main git is parked at .git.sgit-parked."
1803
+ );
1804
+ info("Run 'sgit track unswap' to exit.");
1805
+ }
1806
+
1807
+ // src/commands/track/unswap.ts
1808
+ import { join as join13 } from "path";
1809
+ import { rename as rename4 } from "fs/promises";
1810
+ async function trackUnswap() {
1811
+ const root = await findRoot();
1812
+ if (!await isSwapMode(root)) {
1813
+ throw new NotSwappedError();
1814
+ }
1815
+ const gitPath = join13(root, ".git");
1816
+ const sgitPath = join13(root, ".sgit");
1817
+ const disabledPath = join13(root, SWAP_MARKER);
1818
+ await assertNoInProgressOps(gitPath);
1819
+ let shadowRenamed = false;
1820
+ try {
1821
+ await rename4(gitPath, sgitPath);
1822
+ shadowRenamed = true;
1823
+ await rename4(disabledPath, gitPath);
1824
+ } catch (err) {
1825
+ if (shadowRenamed) {
1826
+ try {
1827
+ await rename4(sgitPath, gitPath);
1828
+ } catch {
1829
+ }
1830
+ }
1831
+ throw err;
1832
+ }
1833
+ const manifest = await readManifest(root);
1834
+ enableGitignores(root, manifest.paths);
1835
+ success("Swap mode disabled. Main git restored.");
1836
+ }
1837
+
1599
1838
  // src/commands/track/index.ts
1600
1839
  function registerTrackCommands(program) {
1601
1840
  const track = program.command("track").description("Manage shadow-tracked paths");
@@ -1622,6 +1861,8 @@ function registerTrackCommands(program) {
1622
1861
  async (action, opts) => trackCompletion(action, opts)
1623
1862
  )
1624
1863
  );
1864
+ track.command("swap").description("Swap .git with .sgit so IDE tools see the shadow repo as the active git").action(wrapAction(trackSwap));
1865
+ track.command("unswap").description("Exit swap mode and restore the main repository").action(wrapAction(trackUnswap));
1625
1866
  }
1626
1867
  function wrapAction(fn) {
1627
1868
  return async (...args) => {
@@ -1682,11 +1923,191 @@ function wrapAction2(fn) {
1682
1923
  };
1683
1924
  }
1684
1925
 
1926
+ // src/core/update-check.ts
1927
+ import { spawn } from "child_process";
1928
+ import {
1929
+ mkdirSync,
1930
+ readFileSync as readFileSync2,
1931
+ statSync,
1932
+ unlinkSync as unlinkSync2,
1933
+ writeFileSync as writeFileSync2
1934
+ } from "fs";
1935
+ import { homedir as homedir2 } from "os";
1936
+ import { isAbsolute, join as join14 } from "path";
1937
+ var PACKAGE_NAME = "shadow-git";
1938
+ var DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
1939
+ var DAY_MS = 24 * 60 * 60 * 1e3;
1940
+ var FETCH_TIMEOUT_MS = 1e4;
1941
+ var LOCK_STALE_MS = 6e4;
1942
+ var STATE_FILE = "update-check.json";
1943
+ var LOCK_FILE2 = "update-check.lock";
1944
+ var VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
1945
+ var UPDATE_CHECK_ARG = "__update-check";
1946
+ var HANDLED_ENV = "SGIT_UPDATE_NOTICE_HANDLED";
1947
+ function setupUpdateNotifier(currentVersion, entry, args) {
1948
+ const nested = Boolean(process.env[HANDLED_ENV]);
1949
+ process.env[HANDLED_ENV] = "1";
1950
+ if (nested || !isEnabled(args)) return;
1951
+ try {
1952
+ const dir = getCacheDir();
1953
+ if (isCheckDue(readState(dir)) && acquireLock(dir)) {
1954
+ spawnWorker(dir, entry);
1955
+ }
1956
+ process.on("exit", () => showNotice(dir, currentVersion));
1957
+ } catch {
1958
+ }
1959
+ }
1960
+ async function runUpdateCheckWorker() {
1961
+ try {
1962
+ const dir = getCacheDir();
1963
+ try {
1964
+ const latest = await fetchLatestVersion();
1965
+ if (latest !== void 0) {
1966
+ writeState(dir, { checkedAt: Date.now(), latest });
1967
+ }
1968
+ } finally {
1969
+ releaseLock(dir);
1970
+ }
1971
+ } catch {
1972
+ }
1973
+ }
1974
+ function isNewerVersion(latest, current) {
1975
+ const a = VERSION_RE.exec(latest);
1976
+ const b = VERSION_RE.exec(current);
1977
+ if (!a || !b) return false;
1978
+ for (let i = 1; i <= 3; i++) {
1979
+ const diff = Number(a[i]) - Number(b[i]);
1980
+ if (diff !== 0) return diff > 0;
1981
+ }
1982
+ return a[4] === void 0 && b[4] !== void 0;
1983
+ }
1984
+ function getCacheDir() {
1985
+ const xdg = process.env.XDG_CACHE_HOME;
1986
+ const base = xdg && isAbsolute(xdg) ? xdg : join14(homedir2(), ".cache");
1987
+ return join14(base, PACKAGE_NAME);
1988
+ }
1989
+ function isEnabled(args) {
1990
+ if (!process.stdout.isTTY || !process.stderr.isTTY) return false;
1991
+ if (process.env.NO_UPDATE_NOTIFIER) return false;
1992
+ if (args[0] === "track" && args[1] === "verify") return false;
1993
+ return true;
1994
+ }
1995
+ function isCheckDue(state) {
1996
+ if (!state) return true;
1997
+ const age = Date.now() - state.checkedAt;
1998
+ return age < 0 || age >= DAY_MS;
1999
+ }
2000
+ function showNotice(dir, currentVersion) {
2001
+ try {
2002
+ const latest = readState(dir)?.latest;
2003
+ if (latest !== void 0 && isNewerVersion(latest, currentVersion)) {
2004
+ updateNotice(currentVersion, latest);
2005
+ }
2006
+ } catch {
2007
+ }
2008
+ }
2009
+ function spawnWorker(dir, entry) {
2010
+ try {
2011
+ const child = spawn(process.execPath, [entry, UPDATE_CHECK_ARG], {
2012
+ cwd: dir,
2013
+ // not the user's repo — a lingering cwd can block deleting it
2014
+ detached: true,
2015
+ stdio: "ignore",
2016
+ windowsHide: true
2017
+ });
2018
+ child.on("error", () => releaseLock(dir));
2019
+ child.unref();
2020
+ } catch {
2021
+ releaseLock(dir);
2022
+ }
2023
+ }
2024
+ async function fetchLatestVersion() {
2025
+ const res = await fetch(DIST_TAGS_URL, {
2026
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2027
+ });
2028
+ if (!res.ok) return void 0;
2029
+ const tags = await res.json();
2030
+ return typeof tags?.latest === "string" ? tags.latest : void 0;
2031
+ }
2032
+ function readState(dir) {
2033
+ try {
2034
+ const data = JSON.parse(
2035
+ readFileSync2(join14(dir, STATE_FILE), "utf-8")
2036
+ );
2037
+ if (typeof data?.checkedAt !== "number" || typeof data.latest !== "string") {
2038
+ return void 0;
2039
+ }
2040
+ return { checkedAt: data.checkedAt, latest: data.latest };
2041
+ } catch {
2042
+ return void 0;
2043
+ }
2044
+ }
2045
+ function writeState(dir, state) {
2046
+ writeFileSync2(join14(dir, STATE_FILE), JSON.stringify(state, null, 2) + "\n");
2047
+ }
2048
+ function acquireLock(dir) {
2049
+ const lockPath = join14(dir, LOCK_FILE2);
2050
+ try {
2051
+ mkdirSync(dir, { recursive: true });
2052
+ writeFileSync2(lockPath, String(process.pid), { flag: "wx" });
2053
+ return true;
2054
+ } catch {
2055
+ }
2056
+ try {
2057
+ const age = Math.abs(Date.now() - statSync(lockPath).mtimeMs);
2058
+ if (age < LOCK_STALE_MS) return false;
2059
+ writeFileSync2(lockPath, String(process.pid));
2060
+ return true;
2061
+ } catch {
2062
+ return false;
2063
+ }
2064
+ }
2065
+ function releaseLock(dir) {
2066
+ try {
2067
+ unlinkSync2(join14(dir, LOCK_FILE2));
2068
+ } catch {
2069
+ }
2070
+ }
2071
+
1685
2072
  // src/cli.ts
1686
2073
  var COMMANDER_COMMANDS = /* @__PURE__ */ new Set(["track", "debug"]);
2074
+ function readVersion() {
2075
+ try {
2076
+ const pkg = JSON.parse(
2077
+ readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
2078
+ );
2079
+ return typeof pkg.version === "string" ? pkg.version : void 0;
2080
+ } catch {
2081
+ return void 0;
2082
+ }
2083
+ }
1687
2084
  async function run() {
1688
2085
  const args = process.argv.slice(2);
1689
- const skipGuard = args[0] === "track" && (args[1] === "init" || args[1] === "clone") || args[0] === "debug";
2086
+ if (args[0] === UPDATE_CHECK_ARG) {
2087
+ await runUpdateCheckWorker();
2088
+ process.exit(0);
2089
+ }
2090
+ const version = readVersion();
2091
+ if (version) {
2092
+ setupUpdateNotifier(version, fileURLToPath(import.meta.url), args);
2093
+ }
2094
+ let swapActive = false;
2095
+ try {
2096
+ const root = await findRoot();
2097
+ await assertConsistentState(root);
2098
+ swapActive = await isSwapMode(root);
2099
+ } catch (err) {
2100
+ if (err instanceof SwapModeCorruptError) {
2101
+ error(err.message);
2102
+ process.exit(1);
2103
+ }
2104
+ }
2105
+ const suppressBanner = args[0] === "track" && (args[1] === "swap" || args[1] === "unswap");
2106
+ if (swapActive && !suppressBanner) {
2107
+ swapModeBanner();
2108
+ }
2109
+ const selfManagedGuard = args[0] === "track" && (args[1] === "init" || args[1] === "clone" || args[1] === "swap" || args[1] === "unswap");
2110
+ const skipGuard = selfManagedGuard || args[0] === "debug" || swapActive;
1690
2111
  let guardCleanup;
1691
2112
  if (!skipGuard) {
1692
2113
  try {
@@ -1698,7 +2119,7 @@ async function run() {
1698
2119
  }
1699
2120
  try {
1700
2121
  if (args.length === 0 || COMMANDER_COMMANDS.has(args[0]) || args[0].startsWith("-")) {
1701
- const program = new Command2("sgit").description("Shadow Git \u2014 a secondary git repository for your project").version("0.1.0");
2122
+ const program = new Command2("sgit").description("Shadow Git \u2014 a secondary git repository for your project").version(version ?? "unknown");
1702
2123
  registerTrackCommands(program);
1703
2124
  registerDebugCommands(program);
1704
2125
  program.addHelpText(
@@ -1714,6 +2135,8 @@ Track commands:
1714
2135
  sgit track unignore Stop ignoring paths
1715
2136
  sgit track sync Regenerate exclude files
1716
2137
  sgit track verify Validate staged files
2138
+ sgit track swap Swap .git with .sgit for IDE compatibility
2139
+ sgit track unswap Exit swap mode
1717
2140
  sgit track destroy Remove shadow repository
1718
2141
  sgit track completion Install shell completions
1719
2142
 
@@ -1724,7 +2147,7 @@ All other commands are forwarded to git in the shadow repository:
1724
2147
  } else {
1725
2148
  const root = await findRoot();
1726
2149
  await validateShadowRepo(root);
1727
- const exitCode = forwardToGit(root, args);
2150
+ const exitCode = await forwardToGit(root, args);
1728
2151
  guardCleanup?.();
1729
2152
  guardCleanup = void 0;
1730
2153
  process.exit(exitCode);