sidecarsync 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +281 -53
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -407,7 +407,8 @@ function jjDefaultWorkspace(root) {
407
407
  try {
408
408
  if (!fs3.statSync(pointer).isFile())
409
409
  return;
410
- const workspace = path3.dirname(path3.dirname(fs3.readFileSync(pointer, "utf8").trim()));
410
+ const repo = path3.resolve(path3.dirname(pointer), fs3.readFileSync(pointer, "utf8").trim());
411
+ const workspace = path3.dirname(path3.dirname(repo));
411
412
  return fs3.existsSync(path3.join(workspace, ".jj")) ? workspace : undefined;
412
413
  } catch {
413
414
  return;
@@ -2628,7 +2629,26 @@ function repairLinkedCheckout(root, config, sidecarPath) {
2628
2629
  }
2629
2630
  throw new SidecarError(`sidecar checkout at ${sidecarPath} is not a usable Git checkout; if this repo moved, repair it there first (\`git worktree repair\`), or delete the checkout and run \`sidecar clone\``);
2630
2631
  }
2631
- function cloneOrUpdate(root, config, bootstrapMain) {
2632
+ function checkoutIsUnlinkedFromFamily(root, config, sidecarPath) {
2633
+ if (isStandalone(config))
2634
+ return false;
2635
+ try {
2636
+ if (!fs7.statSync(path7.join(sidecarPath, ".git")).isDirectory())
2637
+ return false;
2638
+ } catch {
2639
+ return false;
2640
+ }
2641
+ const primary = familyPrimaryRoot(root);
2642
+ if (!primary)
2643
+ return false;
2644
+ try {
2645
+ const primaryConfig = readConfig(path7.join(primary, ".sidecar"));
2646
+ return primaryConfig.remote === config.remote;
2647
+ } catch {
2648
+ return false;
2649
+ }
2650
+ }
2651
+ function cloneOrUpdate(root, config, bootstrapMain, options) {
2632
2652
  const sidecarPath = resolveSidecarPath(root, config);
2633
2653
  if (fs7.existsSync(sidecarPath) && !hasGitMetadata(sidecarPath)) {
2634
2654
  if (fs7.readdirSync(sidecarPath).length) {
@@ -2656,6 +2676,13 @@ function cloneOrUpdate(root, config, bootstrapMain) {
2656
2676
  } else {
2657
2677
  throw new SidecarError(`${sidecarPath} is not usable as a sidecar checkout`);
2658
2678
  }
2679
+ if (options?.checkoutId) {
2680
+ fs7.writeFileSync(path7.join(gitDir(sidecarPath), "sidecar-id"), `${options.checkoutId}
2681
+ `, {
2682
+ encoding: "utf8",
2683
+ mode: 384
2684
+ });
2685
+ }
2659
2686
  ensureCommitIdentity(sidecarPath);
2660
2687
  ensureRedactionFilter(sidecarPath, config.redaction);
2661
2688
  if (bootstrapMain)
@@ -3680,8 +3707,187 @@ var init_cmd_init = __esm(() => {
3680
3707
  init_redaction();
3681
3708
  });
3682
3709
 
3683
- // src/cmd-status.ts
3710
+ // src/cmd-refresh.ts
3684
3711
  import fs10 from "node:fs";
3712
+ import path9 from "node:path";
3713
+ function worktreeHoldingBranch(repo, branch) {
3714
+ const result = git(repo, ["worktree", "list", "--porcelain"], { check: false });
3715
+ if (result.status !== 0)
3716
+ return;
3717
+ let current;
3718
+ for (const line of result.stdout.split(/\r?\n/)) {
3719
+ if (line.startsWith("worktree "))
3720
+ current = line.slice("worktree ".length).trim();
3721
+ else if (line === `branch refs/heads/${branch}`)
3722
+ return current;
3723
+ }
3724
+ return;
3725
+ }
3726
+ function checkoutIsOwnRepo(sidecarPath) {
3727
+ const top = git(sidecarPath, ["rev-parse", "--show-toplevel"], { check: false });
3728
+ if (top.status !== 0)
3729
+ return false;
3730
+ return realpathOr(top.stdout.trim()) === realpathOr(sidecarPath);
3731
+ }
3732
+ function unpushedCommits(sidecarPath) {
3733
+ if (!hasAnyCommit(sidecarPath))
3734
+ return 0;
3735
+ const counted = git(sidecarPath, ["rev-list", "--count", "HEAD", "--not", "--remotes=origin"], {
3736
+ check: false
3737
+ });
3738
+ return counted.status === 0 ? Number(counted.stdout.trim()) || 0 : 0;
3739
+ }
3740
+ function dependentWorktrees(sidecarPath) {
3741
+ try {
3742
+ if (!fs10.statSync(path9.join(sidecarPath, ".git")).isDirectory())
3743
+ return [];
3744
+ } catch {
3745
+ return [];
3746
+ }
3747
+ const result = git(sidecarPath, ["worktree", "list", "--porcelain"], { check: false });
3748
+ if (result.status !== 0)
3749
+ return [];
3750
+ const self = realpathOr(sidecarPath);
3751
+ return result.stdout.split(/\r?\n/).filter((line) => line.startsWith("worktree ")).map((line) => line.slice("worktree ".length).trim()).filter((entry) => entry && realpathOr(entry) !== self);
3752
+ }
3753
+ function existingCheckoutId(sidecarPath) {
3754
+ const candidates = [];
3755
+ const reported = git(sidecarPath, ["rev-parse", "--git-dir"], { check: false });
3756
+ if (reported.status === 0)
3757
+ candidates.push(path9.resolve(sidecarPath, reported.stdout.trim()));
3758
+ candidates.push(path9.join(sidecarPath, ".git"));
3759
+ for (const candidate of candidates) {
3760
+ try {
3761
+ const id = slug(fs10.readFileSync(path9.join(candidate, "sidecar-id"), "utf8"));
3762
+ if (id)
3763
+ return id;
3764
+ } catch {}
3765
+ }
3766
+ return;
3767
+ }
3768
+ function refreshCheckout(root, config) {
3769
+ const sidecarPath = resolveSidecarPath(root, config);
3770
+ if (isStandalone(config)) {
3771
+ throw new SidecarError("refusing to delete a standalone sidecar, which is the repo itself");
3772
+ }
3773
+ const relative = path9.relative(root, sidecarPath);
3774
+ if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
3775
+ throw new SidecarError(`refusing to delete ${sidecarPath}, which is not inside ${root}`);
3776
+ }
3777
+ const checkoutId = existingCheckoutId(sidecarPath);
3778
+ fs10.rmSync(sidecarPath, { recursive: true, force: true });
3779
+ const family = familySidecarCheckout(root, config);
3780
+ if (family) {
3781
+ git(family, ["worktree", "prune", "--expire", "now"], { check: false });
3782
+ fetch(family, true, false);
3783
+ }
3784
+ cloneOrUpdate(root, config, true, { checkoutId });
3785
+ logSidecarEvent("checkout-refresh", { root, sidecarPath, checkoutId: checkoutId ?? null });
3786
+ }
3787
+ function refreshStandaloneCheckout(root, config, resetInbox) {
3788
+ ensureCommitIdentity(root);
3789
+ ensureRedactionFilter(root, config.redaction);
3790
+ fetch(root, true, false);
3791
+ if (config.redaction !== "none") {
3792
+ logSidecarEvent("checkout-refresh", { root, standalone: true, settled: false });
3793
+ return `left ${config.branch} and the inbox branch untouched: settling them means switching branches, which under redaction would replace local files with their redacted pushed contents`;
3794
+ }
3795
+ ensureMainBranch(root, config);
3796
+ const inbox = expandInbox(config, root);
3797
+ if (resetInbox && branchExists(root, inbox) && branchExists(root, config.branch)) {
3798
+ const tip = git(root, ["rev-parse", "--short", inbox]).stdout.trim();
3799
+ const discarded = `refs/sidecar-discarded/${inbox}/${utcTimestamp()}-${tip}`;
3800
+ git(root, ["update-ref", discarded, inbox], { check: false });
3801
+ git(root, ["branch", "-f", inbox, config.branch]);
3802
+ console.log(`reset ${paint("brand", inbox)} to ${config.branch}; old tip kept at ${paint("brand", discarded)}`);
3803
+ }
3804
+ ensureInboxBranch(root, config, inbox);
3805
+ logSidecarEvent("checkout-refresh", { root, standalone: true, settled: true, resetInbox });
3806
+ return;
3807
+ }
3808
+ function cmdRefresh(args) {
3809
+ const parsed = parseOptions(args, {
3810
+ boolean: new Set(["--force", "--yes", "-y"]),
3811
+ value: new Set
3812
+ });
3813
+ if (parsed.positional.length)
3814
+ throw new SidecarError("usage: sidecar refresh [--force] [--yes]");
3815
+ const [root, config] = loadProject();
3816
+ const force = parsed.flags.has("--force");
3817
+ const standalone = isStandalone(config);
3818
+ const sidecarPath = requireSidecarCheckout(root, config);
3819
+ const readable = checkoutIsOwnRepo(sidecarPath);
3820
+ if (!readable && standalone) {
3821
+ throw new SidecarError(`${sidecarPath} is not a readable Git repository, and in standalone mode that repo is your own — sidecar will not rebuild it`);
3822
+ }
3823
+ if (!readable && !force) {
3824
+ throw new SidecarError(`${sidecarPath} is not a readable Git repository, so what it still holds cannot be checked; \`sidecar refresh --force\` replaces it anyway`);
3825
+ }
3826
+ let inbox;
3827
+ if (readable) {
3828
+ inbox = expandInbox(config, sidecarPath);
3829
+ fetch(sidecarPath, true, false);
3830
+ const unpushed = unpushedCommits(sidecarPath);
3831
+ const dirtyFiles = git(sidecarPath, ["status", "--porcelain"], { check: false }).stdout.split(`
3832
+ `).filter(Boolean).length;
3833
+ if ((unpushed || dirtyFiles) && !force) {
3834
+ const held = [
3835
+ unpushed ? `${unpushed} commit(s) the remote has not seen` : "",
3836
+ dirtyFiles ? `${dirtyFiles} uncommitted file(s)` : ""
3837
+ ].filter(Boolean);
3838
+ throw new SidecarError(`this checkout still holds ${held.join(" and ")}; run \`sidecar sync\` to push them, then refresh — or \`sidecar refresh --force\` to discard them`);
3839
+ }
3840
+ }
3841
+ if (standalone) {
3842
+ console.log(`${paint("repo", root)} is its own sidecar, so refresh does not rebuild it.`);
3843
+ console.log(config.redaction === "none" ? `it rewires the redaction filter and settles ${config.branch} onto ${paint("brand", `origin/${config.branch}`)}${force ? `, then resets the inbox branch to ${config.branch}` : ""}.` : `it rewires the redaction filter and, because redaction is on, leaves your branches where they are.`);
3844
+ } else {
3845
+ const dependents = dependentWorktrees(sidecarPath);
3846
+ if (dependents.length && !force) {
3847
+ throw new SidecarError(`${dependents.length} other checkout(s) share this one's Git store (${dependents.join(", ")}); refresh those working copies instead, or \`sidecar refresh --force\` to replace this one and leave them to be refreshed too`);
3848
+ }
3849
+ const family = familySidecarCheckout(root, config);
3850
+ const holder = inbox && family ? worktreeHoldingBranch(family, inbox) : undefined;
3851
+ if (holder && realpathOr(holder) !== realpathOr(sidecarPath)) {
3852
+ throw new SidecarError(`${inbox} is already checked out at ${holder}; give this working copy its own inbox (a {random} in the .sidecar inbox template) before refreshing`);
3853
+ }
3854
+ console.log(`refresh deletes ${paint("brand", sidecarPath)} and clones it again from ${paint("brand", config.remote)}, ${paint("attn", "discarding anything not pushed")}.`);
3855
+ if (family)
3856
+ console.log(`the rebuilt checkout will share this repo family's Git store.`);
3857
+ }
3858
+ const confirmed = parsed.flags.has("--yes") || parsed.flags.has("-y") || promptYesNoDefaultNo("continue?");
3859
+ if (!confirmed) {
3860
+ console.log("nothing changed");
3861
+ return 0;
3862
+ }
3863
+ let declined;
3864
+ withSyncLock(root, "throw", () => {
3865
+ if (readable && !force && isDirty(sidecarPath)) {
3866
+ throw new SidecarError("the sidecar checkout changed while waiting for confirmation; rerun refresh");
3867
+ }
3868
+ if (standalone)
3869
+ declined = refreshStandaloneCheckout(root, config, force);
3870
+ else
3871
+ refreshCheckout(root, config);
3872
+ });
3873
+ registerCurrentInstance(root, config, { event: "refresh" });
3874
+ console.log(`refreshed sidecar at ${paint("brand", sidecarPath)}`);
3875
+ if (declined)
3876
+ console.error(`sidecar: ${declined}`);
3877
+ return 0;
3878
+ }
3879
+ var init_cmd_refresh = __esm(() => {
3880
+ init_color();
3881
+ init_util();
3882
+ init_git();
3883
+ init_config();
3884
+ init_state();
3885
+ init_sync();
3886
+ init_ui();
3887
+ });
3888
+
3889
+ // src/cmd-status.ts
3890
+ import fs11 from "node:fs";
3685
3891
  function statusLine(label, value, role) {
3686
3892
  labelLine(STATUS_LABEL_WIDTH, label, value, role);
3687
3893
  }
@@ -3720,6 +3926,9 @@ function cmdStatus(args) {
3720
3926
  else
3721
3927
  statusLine("branch", `${branch} — not the inbox branch; sync will switch back`, "attn");
3722
3928
  statusLine("dirty", dirty ? "yes" : "no", dirty ? "attn" : "quiet");
3929
+ if (checkoutIsUnlinkedFromFamily(root, config, sidecarPath)) {
3930
+ statusLine("family", "independent clone — syncs via the remote; `sidecar refresh` links it", "attn");
3931
+ }
3723
3932
  printDaemonLine();
3724
3933
  printLastSyncLine(root);
3725
3934
  const pending = pendingStatusInboxBranches(sidecarPath, config);
@@ -3749,6 +3958,7 @@ function cmdStatusJson() {
3749
3958
  globalInstall: shouldUseGlobalRegistry() || Boolean(findGlobalSidecarExecutable()),
3750
3959
  currentBranch: branch || undefined,
3751
3960
  dirty: checkoutPresent ? Boolean(git(sidecarPath, ["status", "--porcelain"]).stdout.trim()) : undefined,
3961
+ familyLinked: checkoutPresent ? !checkoutIsUnlinkedFromFamily(root, config, sidecarPath) : undefined,
3752
3962
  daemon: daemonHealth().text,
3753
3963
  lastSyncAt: readInstances().find((instance) => instance.root === root)?.lastSyncAt,
3754
3964
  pendingInbox: checkoutPresent ? pendingStatusInboxBranches(sidecarPath, config) : undefined
@@ -3898,16 +4108,16 @@ function cmdTail(args) {
3898
4108
  throw new SidecarError("--lines requires a positive integer");
3899
4109
  }
3900
4110
  const filePath = sidecarLogPath();
3901
- if (!fs10.existsSync(filePath)) {
4111
+ if (!fs11.existsSync(filePath)) {
3902
4112
  if (parsed.flags.has("-f") || parsed.flags.has("--follow")) {
3903
4113
  followLog(filePath, 0);
3904
4114
  return 0;
3905
4115
  }
3906
4116
  return 0;
3907
4117
  }
3908
- const stat = fs10.statSync(filePath);
4118
+ const stat = fs11.statSync(filePath);
3909
4119
  if (stat.size > 0) {
3910
- process.stdout.write(lastLines(fs10.readFileSync(filePath, "utf8"), lines));
4120
+ process.stdout.write(lastLines(fs11.readFileSync(filePath, "utf8"), lines));
3911
4121
  }
3912
4122
  if (parsed.flags.has("-f") || parsed.flags.has("--follow")) {
3913
4123
  followLog(filePath, stat.size);
@@ -3930,7 +4140,7 @@ function followLog(filePath, startOffset) {
3930
4140
  sleep(1000);
3931
4141
  let stat;
3932
4142
  try {
3933
- stat = fs10.statSync(filePath);
4143
+ stat = fs11.statSync(filePath);
3934
4144
  } catch {
3935
4145
  offset = 0;
3936
4146
  continue;
@@ -3939,17 +4149,17 @@ function followLog(filePath, startOffset) {
3939
4149
  offset = 0;
3940
4150
  if (stat.size <= offset)
3941
4151
  continue;
3942
- const fd = fs10.openSync(filePath, "r");
4152
+ const fd = fs11.openSync(filePath, "r");
3943
4153
  try {
3944
4154
  const length = stat.size - offset;
3945
4155
  const buffer = Buffer.alloc(length);
3946
- const bytesRead = fs10.readSync(fd, buffer, 0, length, offset);
4156
+ const bytesRead = fs11.readSync(fd, buffer, 0, length, offset);
3947
4157
  if (bytesRead > 0) {
3948
4158
  process.stdout.write(buffer.subarray(0, bytesRead).toString("utf8"));
3949
4159
  offset += bytesRead;
3950
4160
  }
3951
4161
  } finally {
3952
- fs10.closeSync(fd);
4162
+ fs11.closeSync(fd);
3953
4163
  }
3954
4164
  }
3955
4165
  }
@@ -3980,8 +4190,8 @@ __export(exports_daemon, {
3980
4190
  checkAndInstallUpdate: () => checkAndInstallUpdate,
3981
4191
  WATCH_LIMIT: () => WATCH_LIMIT
3982
4192
  });
3983
- import fs11 from "node:fs";
3984
- import path9 from "node:path";
4193
+ import fs12 from "node:fs";
4194
+ import path10 from "node:path";
3985
4195
  import { spawn as spawn2 } from "node:child_process";
3986
4196
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3987
4197
  async function runDaemonLoop(options) {
@@ -4073,7 +4283,7 @@ async function runCycle(state) {
4073
4283
  let failed = 0;
4074
4284
  let skipped = 0;
4075
4285
  for (const instance of readInstances()) {
4076
- if (!fs11.existsSync(instance.configPath)) {
4286
+ if (!fs12.existsSync(instance.configPath)) {
4077
4287
  const misses = (state.misses.get(instance.root) ?? 0) + 1;
4078
4288
  state.misses.set(instance.root, misses);
4079
4289
  if (misses >= PRUNE_AFTER_MISSES) {
@@ -4178,7 +4388,7 @@ async function syncIfDirty(state, root, trigger) {
4178
4388
  }
4179
4389
  async function checkoutIsDirty(root) {
4180
4390
  const sidecarPath = readInstances().find((instance) => instance.root === root)?.sidecarPath;
4181
- if (!sidecarPath || !fs11.existsSync(sidecarPath))
4391
+ if (!sidecarPath || !fs12.existsSync(sidecarPath))
4182
4392
  return false;
4183
4393
  const result = await runChild("git", ["-C", sidecarPath, "status", "--porcelain"], { timeoutMs: 30000 });
4184
4394
  return result.status === 0 && Boolean(result.stdout.trim());
@@ -4186,7 +4396,7 @@ async function checkoutIsDirty(root) {
4186
4396
  function localSidecarCliPath(root) {
4187
4397
  if (!projectDependsOnSidecar(root))
4188
4398
  return;
4189
- const candidate = path9.join(root, "node_modules", PACKAGE_NAME, "dist", "cli.js");
4399
+ const candidate = path10.join(root, "node_modules", PACKAGE_NAME, "dist", "cli.js");
4190
4400
  if (!isFile(candidate))
4191
4401
  return;
4192
4402
  const localVersion = installedPackageVersion(root);
@@ -4198,7 +4408,7 @@ function currentCliPath() {
4198
4408
  return process.argv[1] || fileURLToPath3(import.meta.url);
4199
4409
  }
4200
4410
  function selectWatchTargets(instances, limit = WATCH_LIMIT) {
4201
- return [...instances].filter((instance) => fs11.existsSync(instance.configPath) && fs11.existsSync(instance.sidecarPath)).sort((left, right) => instanceRecency(right) - instanceRecency(left)).slice(0, limit);
4411
+ return [...instances].filter((instance) => fs12.existsSync(instance.configPath) && fs12.existsSync(instance.sidecarPath)).sort((left, right) => instanceRecency(right) - instanceRecency(left)).slice(0, limit);
4202
4412
  }
4203
4413
  function instanceRecency(instance) {
4204
4414
  const time = Date.parse(instance.lastSyncAt ?? instance.updatedAt ?? instance.registeredAt);
@@ -4328,7 +4538,7 @@ async function watchRegistry(state) {
4328
4538
  const watcher = chokidar.watch(sidecarStateDir(), { ignoreInitial: true, depth: 0 });
4329
4539
  watcher.on("all", (...args) => {
4330
4540
  const filePath = typeof args[1] === "string" ? args[1] : "";
4331
- if (path9.basename(filePath) !== "instances.json")
4541
+ if (path10.basename(filePath) !== "instances.json")
4332
4542
  return;
4333
4543
  if (state.registryTimer)
4334
4544
  return;
@@ -4366,18 +4576,18 @@ function compileGitignoreMatcher(lines) {
4366
4576
  function watchIgnoreMatcher(sidecarPath) {
4367
4577
  let gitignore;
4368
4578
  try {
4369
- const ignoreFile = path9.join(sidecarPath, ".gitignore");
4370
- if (fs11.existsSync(ignoreFile)) {
4371
- gitignore = compileGitignoreMatcher(fs11.readFileSync(ignoreFile, "utf8").split(`
4579
+ const ignoreFile = path10.join(sidecarPath, ".gitignore");
4580
+ if (fs12.existsSync(ignoreFile)) {
4581
+ gitignore = compileGitignoreMatcher(fs12.readFileSync(ignoreFile, "utf8").split(`
4372
4582
  `));
4373
4583
  }
4374
4584
  } catch {}
4375
- const root = path9.resolve(sidecarPath);
4585
+ const root = path10.resolve(sidecarPath);
4376
4586
  return (candidate) => {
4377
- const relative = path9.relative(root, candidate);
4587
+ const relative = path10.relative(root, candidate);
4378
4588
  if (!relative)
4379
4589
  return false;
4380
- const normalized = relative.split(path9.sep).join("/");
4590
+ const normalized = relative.split(path10.sep).join("/");
4381
4591
  if (normalized.startsWith(".."))
4382
4592
  return true;
4383
4593
  if (normalized === ".git" || normalized.startsWith(".git/"))
@@ -4453,10 +4663,10 @@ function restartAfterUpdate() {
4453
4663
  }
4454
4664
  async function acquireDaemonPid() {
4455
4665
  const pidPath = daemonPidPath();
4456
- fs11.mkdirSync(path9.dirname(pidPath), { recursive: true });
4666
+ fs12.mkdirSync(path10.dirname(pidPath), { recursive: true });
4457
4667
  while (true) {
4458
4668
  try {
4459
- fs11.writeFileSync(pidPath, `${process.pid}
4669
+ fs12.writeFileSync(pidPath, `${process.pid}
4460
4670
  `, { encoding: "utf8", flag: "wx" });
4461
4671
  return;
4462
4672
  } catch (error) {
@@ -4472,7 +4682,7 @@ async function acquireDaemonPid() {
4472
4682
  continue;
4473
4683
  }
4474
4684
  logSidecarEvent("daemon-pid-heal", { holder: holder ?? null });
4475
- fs11.rmSync(pidPath, { force: true });
4685
+ fs12.rmSync(pidPath, { force: true });
4476
4686
  }
4477
4687
  }
4478
4688
  function installShutdownHandlers() {
@@ -4487,12 +4697,12 @@ function installShutdownHandlers() {
4487
4697
  function removeOwnPidFile() {
4488
4698
  try {
4489
4699
  if (readPid(daemonPidPath()) === process.pid)
4490
- fs11.rmSync(daemonPidPath(), { force: true });
4700
+ fs12.rmSync(daemonPidPath(), { force: true });
4491
4701
  } catch {}
4492
4702
  }
4493
4703
  function readPid(pidPath) {
4494
4704
  try {
4495
- const pid = Number(fs11.readFileSync(pidPath, "utf8").trim());
4705
+ const pid = Number(fs12.readFileSync(pidPath, "utf8").trim());
4496
4706
  return Number.isInteger(pid) && pid > 0 ? pid : undefined;
4497
4707
  } catch {
4498
4708
  return;
@@ -4533,21 +4743,21 @@ function runChild(command, args, options) {
4533
4743
  }
4534
4744
  function isFile(filePath) {
4535
4745
  try {
4536
- return fs11.statSync(filePath).isFile();
4746
+ return fs12.statSync(filePath).isFile();
4537
4747
  } catch {
4538
4748
  return false;
4539
4749
  }
4540
4750
  }
4541
4751
  function realpathOr2(filePath) {
4542
4752
  try {
4543
- return fs11.realpathSync(filePath);
4753
+ return fs12.realpathSync(filePath);
4544
4754
  } catch {
4545
- return path9.resolve(filePath);
4755
+ return path10.resolve(filePath);
4546
4756
  }
4547
4757
  }
4548
4758
  function isInsidePath2(child, parent) {
4549
- const relative = path9.relative(parent, child);
4550
- return Boolean(relative) && !relative.startsWith("..") && !path9.isAbsolute(relative);
4759
+ const relative = path10.relative(parent, child);
4760
+ return Boolean(relative) && !relative.startsWith("..") && !path10.isAbsolute(relative);
4551
4761
  }
4552
4762
  function escapeRegex(value) {
4553
4763
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4750,9 +4960,9 @@ var init_cmd_daemon = __esm(() => {
4750
4960
  });
4751
4961
 
4752
4962
  // src/cmd-sync.ts
4753
- import fs12 from "node:fs";
4963
+ import fs13 from "node:fs";
4754
4964
  import os6 from "node:os";
4755
- import path10 from "node:path";
4965
+ import path11 from "node:path";
4756
4966
  function cmdSnapshot(args) {
4757
4967
  const parsed = parseOptions(args, {
4758
4968
  boolean: new Set(["--push"]),
@@ -4808,6 +5018,12 @@ function cmdSync(args) {
4808
5018
  if (synced) {
4809
5019
  registerCurrentInstance(root, config, { event: "sync", lastSyncAt: nowIso() });
4810
5020
  reportSyncHealth(root, config, { status: "ok" });
5021
+ if (!soft) {
5022
+ const sidecarPath = resolveSidecarPath(root, config);
5023
+ if (checkoutIsUnlinkedFromFamily(root, config, sidecarPath)) {
5024
+ console.log("sidecar: this checkout is an independent clone, so it settles with its siblings through the remote; `sidecar refresh` links it to the one this repo family shares");
5025
+ }
5026
+ }
4811
5027
  }
4812
5028
  return 0;
4813
5029
  }
@@ -4851,7 +5067,7 @@ function cmdRedactions(args) {
4851
5067
  let shown = 0;
4852
5068
  let items = 0;
4853
5069
  for (const relPath of files) {
4854
- const delta = fileRedactionDelta(path10.join(sidecarPath, relPath), config.redaction);
5070
+ const delta = fileRedactionDelta(path11.join(sidecarPath, relPath), config.redaction);
4855
5071
  if (!delta)
4856
5072
  continue;
4857
5073
  if (shown)
@@ -4871,12 +5087,12 @@ ${items} redaction(s) in ${shown} file(s) will be pushed this way (mode: ${confi
4871
5087
  return 0;
4872
5088
  }
4873
5089
  function printRedactionDiff(original, redacted) {
4874
- const scratch = fs12.mkdtempSync(path10.join(os6.tmpdir(), "sidecar-redactions-"));
5090
+ const scratch = fs13.mkdtempSync(path11.join(os6.tmpdir(), "sidecar-redactions-"));
4875
5091
  try {
4876
- const localPath = path10.join(scratch, "local");
4877
- const pushedPath = path10.join(scratch, "pushed");
4878
- fs12.writeFileSync(localPath, original, "utf8");
4879
- fs12.writeFileSync(pushedPath, redacted, "utf8");
5092
+ const localPath = path11.join(scratch, "local");
5093
+ const pushedPath = path11.join(scratch, "pushed");
5094
+ fs13.writeFileSync(localPath, original, "utf8");
5095
+ fs13.writeFileSync(pushedPath, redacted, "utf8");
4880
5096
  const color = colorLevel() > 0 ? ["--color"] : [];
4881
5097
  const diff = gitRaw(["diff", "--no-index", ...color, "--", localPath, pushedPath], { check: false });
4882
5098
  const lines = diff.stdout.split(`
@@ -4887,16 +5103,16 @@ function printRedactionDiff(original, redacted) {
4887
5103
  if (body)
4888
5104
  console.log(body);
4889
5105
  } finally {
4890
- fs12.rmSync(scratch, { recursive: true, force: true });
5106
+ fs13.rmSync(scratch, { recursive: true, force: true });
4891
5107
  }
4892
5108
  }
4893
5109
  function cmdRedact(args) {
4894
5110
  const parsed = parseOptions(args, { boolean: new Set, value: new Set(["--mode"]) });
4895
5111
  const mode = redactionModeConfigValue(getValue(parsed, "--mode", DEFAULT_REDACTION_MODE), "--mode");
4896
- const output = redactBuffer(fs12.readFileSync(0), mode);
5112
+ const output = redactBuffer(fs13.readFileSync(0), mode);
4897
5113
  let offset = 0;
4898
5114
  while (offset < output.length) {
4899
- offset += fs12.writeSync(1, output, offset, output.length - offset);
5115
+ offset += fs13.writeSync(1, output, offset, output.length - offset);
4900
5116
  }
4901
5117
  return 0;
4902
5118
  }
@@ -4979,6 +5195,7 @@ var init_commands = __esm(() => {
4979
5195
  init_util();
4980
5196
  init_install();
4981
5197
  init_cmd_init();
5198
+ init_cmd_refresh();
4982
5199
  init_cmd_status();
4983
5200
  init_cmd_daemon();
4984
5201
  init_cmd_sync();
@@ -5025,6 +5242,16 @@ var init_commands = __esm(() => {
5025
5242
  { name: "tail", run: cmdTail, section: "sync", usage: "tail [-f|--follow] [-n|--lines count]" },
5026
5243
  { name: "update", run: cmdUpdate, section: "sync", usage: "update" },
5027
5244
  { name: "clone", run: cmdClone, section: "advanced", usage: "clone [--if-missing]" },
5245
+ {
5246
+ name: "refresh",
5247
+ run: cmdRefresh,
5248
+ section: "advanced",
5249
+ usage: "refresh [--force] [--yes]",
5250
+ notes: [
5251
+ "delete the sidecar checkout and clone it again",
5252
+ "discards anything unpushed; refuses until `sidecar sync` has run"
5253
+ ]
5254
+ },
5028
5255
  { name: "deinit", run: cmdDeinit, section: "advanced", usage: "deinit" },
5029
5256
  { name: "snapshot", run: cmdSnapshot, section: "advanced", usage: "snapshot [--push] [-m message]" },
5030
5257
  { name: "merge", run: cmdMerge, section: "advanced", usage: "merge [--fork-files] [--no-push]" },
@@ -5095,6 +5322,7 @@ var init_cli = __esm(() => {
5095
5322
  init_sync();
5096
5323
  init_commands();
5097
5324
  init_cmd_init();
5325
+ init_cmd_refresh();
5098
5326
  init_cmd_status();
5099
5327
  init_cmd_daemon();
5100
5328
  init_cmd_sync();
@@ -5102,8 +5330,8 @@ var init_cli = __esm(() => {
5102
5330
 
5103
5331
  // src/bin.ts
5104
5332
  init_cli();
5105
- import fs13 from "node:fs";
5106
- import path11 from "node:path";
5333
+ import fs14 from "node:fs";
5334
+ import path12 from "node:path";
5107
5335
  import { spawnSync as spawnSync5 } from "node:child_process";
5108
5336
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5109
5337
  var SKIP_LOCAL_EXEC_ENV3 = "SIDECAR_SKIP_LOCAL_EXEC";
@@ -5132,15 +5360,15 @@ if (!process.env[SKIP_LOCAL_EXEC_ENV3]) {
5132
5360
  }
5133
5361
  process.exit(await main());
5134
5362
  function findLocalInstall(start, self) {
5135
- let current = path11.resolve(start);
5363
+ let current = path12.resolve(start);
5136
5364
  while (true) {
5137
5365
  if (projectDependsOnSidecar2(current)) {
5138
- const candidate = path11.join(current, "node_modules", PACKAGE_NAME2, "dist", "cli.js");
5366
+ const candidate = path12.join(current, "node_modules", PACKAGE_NAME2, "dist", "cli.js");
5139
5367
  if (isFile2(candidate) && !sameFile(candidate, self)) {
5140
5368
  return { executable: candidate, newer: localIsNewer(current) };
5141
5369
  }
5142
5370
  }
5143
- const parent = path11.dirname(current);
5371
+ const parent = path12.dirname(current);
5144
5372
  if (parent === current)
5145
5373
  return;
5146
5374
  current = parent;
@@ -5151,11 +5379,11 @@ function localIsNewer(projectRoot) {
5151
5379
  return localVersion !== undefined && compareVersions(localVersion, packageVersion()) > 0;
5152
5380
  }
5153
5381
  function projectDependsOnSidecar2(projectRoot) {
5154
- const manifestPath = path11.join(projectRoot, "package.json");
5382
+ const manifestPath = path12.join(projectRoot, "package.json");
5155
5383
  if (!isFile2(manifestPath))
5156
5384
  return false;
5157
5385
  try {
5158
- const manifest = JSON.parse(fs13.readFileSync(manifestPath, "utf8"));
5386
+ const manifest = JSON.parse(fs14.readFileSync(manifestPath, "utf8"));
5159
5387
  return Boolean(manifest.dependencies?.[PACKAGE_NAME2] || manifest.devDependencies?.[PACKAGE_NAME2] || manifest.optionalDependencies?.[PACKAGE_NAME2] || manifest.peerDependencies?.[PACKAGE_NAME2]);
5160
5388
  } catch {
5161
5389
  return false;
@@ -5163,14 +5391,14 @@ function projectDependsOnSidecar2(projectRoot) {
5163
5391
  }
5164
5392
  function isFile2(filePath) {
5165
5393
  try {
5166
- return fs13.statSync(filePath).isFile();
5394
+ return fs14.statSync(filePath).isFile();
5167
5395
  } catch {
5168
5396
  return false;
5169
5397
  }
5170
5398
  }
5171
5399
  function sameFile(first, second) {
5172
5400
  try {
5173
- return fs13.realpathSync(first) === fs13.realpathSync(second);
5401
+ return fs14.realpathSync(first) === fs14.realpathSync(second);
5174
5402
  } catch {
5175
5403
  return false;
5176
5404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidecarsync",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "A Git-backed sidecar inbox for repo-local development metadata.",
5
5
  "license": "MIT",
6
6
  "repository": {