sidecarsync 1.2.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.
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
  }
@@ -3705,7 +3911,7 @@ function cmdStatus(args) {
3705
3911
  statusLine("main branch", config.branch);
3706
3912
  statusLine("inbox branch", inbox);
3707
3913
  if (!checkoutPresent) {
3708
- statusLine("checkout", "missing", "bad");
3914
+ statusLine("checkout", "missing — run `sidecar init`", "bad");
3709
3915
  printDaemonLine();
3710
3916
  printLastSyncLine(root);
3711
3917
  return 0;
@@ -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);
@@ -3746,8 +3955,10 @@ function cmdStatusJson() {
3746
3955
  branch: config.branch,
3747
3956
  inbox,
3748
3957
  checkout: checkoutPresent ? "present" : "missing",
3958
+ globalInstall: shouldUseGlobalRegistry() || Boolean(findGlobalSidecarExecutable()),
3749
3959
  currentBranch: branch || undefined,
3750
3960
  dirty: checkoutPresent ? Boolean(git(sidecarPath, ["status", "--porcelain"]).stdout.trim()) : undefined,
3961
+ familyLinked: checkoutPresent ? !checkoutIsUnlinkedFromFamily(root, config, sidecarPath) : undefined,
3751
3962
  daemon: daemonHealth().text,
3752
3963
  lastSyncAt: readInstances().find((instance) => instance.root === root)?.lastSyncAt,
3753
3964
  pendingInbox: checkoutPresent ? pendingStatusInboxBranches(sidecarPath, config) : undefined
@@ -3761,8 +3972,12 @@ function pendingStatusInboxBranches(sidecarPath, config) {
3761
3972
  return pendingInboxBranches(sidecarPath, config).filter((remoteBranch) => !isAncestor(sidecarPath, remoteBranch, base));
3762
3973
  }
3763
3974
  function daemonHealth() {
3764
- if (!shouldUseGlobalRegistry())
3765
- return { text: "no global install", role: "quiet" };
3975
+ if (!shouldUseGlobalRegistry()) {
3976
+ if (!findGlobalSidecarExecutable()) {
3977
+ return { text: `no global install — nothing syncs; \`npm install -g ${PACKAGE_SPEC}\``, role: "bad" };
3978
+ }
3979
+ return { text: "owned by the global install", role: "quiet" };
3980
+ }
3766
3981
  const service = daemonServiceStatus();
3767
3982
  if (!service.available)
3768
3983
  return { text: service.message ?? "unavailable", role: "quiet" };
@@ -3893,16 +4108,16 @@ function cmdTail(args) {
3893
4108
  throw new SidecarError("--lines requires a positive integer");
3894
4109
  }
3895
4110
  const filePath = sidecarLogPath();
3896
- if (!fs10.existsSync(filePath)) {
4111
+ if (!fs11.existsSync(filePath)) {
3897
4112
  if (parsed.flags.has("-f") || parsed.flags.has("--follow")) {
3898
4113
  followLog(filePath, 0);
3899
4114
  return 0;
3900
4115
  }
3901
4116
  return 0;
3902
4117
  }
3903
- const stat = fs10.statSync(filePath);
4118
+ const stat = fs11.statSync(filePath);
3904
4119
  if (stat.size > 0) {
3905
- process.stdout.write(lastLines(fs10.readFileSync(filePath, "utf8"), lines));
4120
+ process.stdout.write(lastLines(fs11.readFileSync(filePath, "utf8"), lines));
3906
4121
  }
3907
4122
  if (parsed.flags.has("-f") || parsed.flags.has("--follow")) {
3908
4123
  followLog(filePath, stat.size);
@@ -3925,7 +4140,7 @@ function followLog(filePath, startOffset) {
3925
4140
  sleep(1000);
3926
4141
  let stat;
3927
4142
  try {
3928
- stat = fs10.statSync(filePath);
4143
+ stat = fs11.statSync(filePath);
3929
4144
  } catch {
3930
4145
  offset = 0;
3931
4146
  continue;
@@ -3934,17 +4149,17 @@ function followLog(filePath, startOffset) {
3934
4149
  offset = 0;
3935
4150
  if (stat.size <= offset)
3936
4151
  continue;
3937
- const fd = fs10.openSync(filePath, "r");
4152
+ const fd = fs11.openSync(filePath, "r");
3938
4153
  try {
3939
4154
  const length = stat.size - offset;
3940
4155
  const buffer = Buffer.alloc(length);
3941
- const bytesRead = fs10.readSync(fd, buffer, 0, length, offset);
4156
+ const bytesRead = fs11.readSync(fd, buffer, 0, length, offset);
3942
4157
  if (bytesRead > 0) {
3943
4158
  process.stdout.write(buffer.subarray(0, bytesRead).toString("utf8"));
3944
4159
  offset += bytesRead;
3945
4160
  }
3946
4161
  } finally {
3947
- fs10.closeSync(fd);
4162
+ fs11.closeSync(fd);
3948
4163
  }
3949
4164
  }
3950
4165
  }
@@ -3975,8 +4190,8 @@ __export(exports_daemon, {
3975
4190
  checkAndInstallUpdate: () => checkAndInstallUpdate,
3976
4191
  WATCH_LIMIT: () => WATCH_LIMIT
3977
4192
  });
3978
- import fs11 from "node:fs";
3979
- import path9 from "node:path";
4193
+ import fs12 from "node:fs";
4194
+ import path10 from "node:path";
3980
4195
  import { spawn as spawn2 } from "node:child_process";
3981
4196
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3982
4197
  async function runDaemonLoop(options) {
@@ -4068,7 +4283,7 @@ async function runCycle(state) {
4068
4283
  let failed = 0;
4069
4284
  let skipped = 0;
4070
4285
  for (const instance of readInstances()) {
4071
- if (!fs11.existsSync(instance.configPath)) {
4286
+ if (!fs12.existsSync(instance.configPath)) {
4072
4287
  const misses = (state.misses.get(instance.root) ?? 0) + 1;
4073
4288
  state.misses.set(instance.root, misses);
4074
4289
  if (misses >= PRUNE_AFTER_MISSES) {
@@ -4173,7 +4388,7 @@ async function syncIfDirty(state, root, trigger) {
4173
4388
  }
4174
4389
  async function checkoutIsDirty(root) {
4175
4390
  const sidecarPath = readInstances().find((instance) => instance.root === root)?.sidecarPath;
4176
- if (!sidecarPath || !fs11.existsSync(sidecarPath))
4391
+ if (!sidecarPath || !fs12.existsSync(sidecarPath))
4177
4392
  return false;
4178
4393
  const result = await runChild("git", ["-C", sidecarPath, "status", "--porcelain"], { timeoutMs: 30000 });
4179
4394
  return result.status === 0 && Boolean(result.stdout.trim());
@@ -4181,7 +4396,7 @@ async function checkoutIsDirty(root) {
4181
4396
  function localSidecarCliPath(root) {
4182
4397
  if (!projectDependsOnSidecar(root))
4183
4398
  return;
4184
- 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");
4185
4400
  if (!isFile(candidate))
4186
4401
  return;
4187
4402
  const localVersion = installedPackageVersion(root);
@@ -4193,7 +4408,7 @@ function currentCliPath() {
4193
4408
  return process.argv[1] || fileURLToPath3(import.meta.url);
4194
4409
  }
4195
4410
  function selectWatchTargets(instances, limit = WATCH_LIMIT) {
4196
- 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);
4197
4412
  }
4198
4413
  function instanceRecency(instance) {
4199
4414
  const time = Date.parse(instance.lastSyncAt ?? instance.updatedAt ?? instance.registeredAt);
@@ -4323,7 +4538,7 @@ async function watchRegistry(state) {
4323
4538
  const watcher = chokidar.watch(sidecarStateDir(), { ignoreInitial: true, depth: 0 });
4324
4539
  watcher.on("all", (...args) => {
4325
4540
  const filePath = typeof args[1] === "string" ? args[1] : "";
4326
- if (path9.basename(filePath) !== "instances.json")
4541
+ if (path10.basename(filePath) !== "instances.json")
4327
4542
  return;
4328
4543
  if (state.registryTimer)
4329
4544
  return;
@@ -4361,18 +4576,18 @@ function compileGitignoreMatcher(lines) {
4361
4576
  function watchIgnoreMatcher(sidecarPath) {
4362
4577
  let gitignore;
4363
4578
  try {
4364
- const ignoreFile = path9.join(sidecarPath, ".gitignore");
4365
- if (fs11.existsSync(ignoreFile)) {
4366
- 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(`
4367
4582
  `));
4368
4583
  }
4369
4584
  } catch {}
4370
- const root = path9.resolve(sidecarPath);
4585
+ const root = path10.resolve(sidecarPath);
4371
4586
  return (candidate) => {
4372
- const relative = path9.relative(root, candidate);
4587
+ const relative = path10.relative(root, candidate);
4373
4588
  if (!relative)
4374
4589
  return false;
4375
- const normalized = relative.split(path9.sep).join("/");
4590
+ const normalized = relative.split(path10.sep).join("/");
4376
4591
  if (normalized.startsWith(".."))
4377
4592
  return true;
4378
4593
  if (normalized === ".git" || normalized.startsWith(".git/"))
@@ -4448,10 +4663,10 @@ function restartAfterUpdate() {
4448
4663
  }
4449
4664
  async function acquireDaemonPid() {
4450
4665
  const pidPath = daemonPidPath();
4451
- fs11.mkdirSync(path9.dirname(pidPath), { recursive: true });
4666
+ fs12.mkdirSync(path10.dirname(pidPath), { recursive: true });
4452
4667
  while (true) {
4453
4668
  try {
4454
- fs11.writeFileSync(pidPath, `${process.pid}
4669
+ fs12.writeFileSync(pidPath, `${process.pid}
4455
4670
  `, { encoding: "utf8", flag: "wx" });
4456
4671
  return;
4457
4672
  } catch (error) {
@@ -4467,7 +4682,7 @@ async function acquireDaemonPid() {
4467
4682
  continue;
4468
4683
  }
4469
4684
  logSidecarEvent("daemon-pid-heal", { holder: holder ?? null });
4470
- fs11.rmSync(pidPath, { force: true });
4685
+ fs12.rmSync(pidPath, { force: true });
4471
4686
  }
4472
4687
  }
4473
4688
  function installShutdownHandlers() {
@@ -4482,12 +4697,12 @@ function installShutdownHandlers() {
4482
4697
  function removeOwnPidFile() {
4483
4698
  try {
4484
4699
  if (readPid(daemonPidPath()) === process.pid)
4485
- fs11.rmSync(daemonPidPath(), { force: true });
4700
+ fs12.rmSync(daemonPidPath(), { force: true });
4486
4701
  } catch {}
4487
4702
  }
4488
4703
  function readPid(pidPath) {
4489
4704
  try {
4490
- const pid = Number(fs11.readFileSync(pidPath, "utf8").trim());
4705
+ const pid = Number(fs12.readFileSync(pidPath, "utf8").trim());
4491
4706
  return Number.isInteger(pid) && pid > 0 ? pid : undefined;
4492
4707
  } catch {
4493
4708
  return;
@@ -4528,21 +4743,21 @@ function runChild(command, args, options) {
4528
4743
  }
4529
4744
  function isFile(filePath) {
4530
4745
  try {
4531
- return fs11.statSync(filePath).isFile();
4746
+ return fs12.statSync(filePath).isFile();
4532
4747
  } catch {
4533
4748
  return false;
4534
4749
  }
4535
4750
  }
4536
4751
  function realpathOr2(filePath) {
4537
4752
  try {
4538
- return fs11.realpathSync(filePath);
4753
+ return fs12.realpathSync(filePath);
4539
4754
  } catch {
4540
- return path9.resolve(filePath);
4755
+ return path10.resolve(filePath);
4541
4756
  }
4542
4757
  }
4543
4758
  function isInsidePath2(child, parent) {
4544
- const relative = path9.relative(parent, child);
4545
- return Boolean(relative) && !relative.startsWith("..") && !path9.isAbsolute(relative);
4759
+ const relative = path10.relative(parent, child);
4760
+ return Boolean(relative) && !relative.startsWith("..") && !path10.isAbsolute(relative);
4546
4761
  }
4547
4762
  function escapeRegex(value) {
4548
4763
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4745,9 +4960,9 @@ var init_cmd_daemon = __esm(() => {
4745
4960
  });
4746
4961
 
4747
4962
  // src/cmd-sync.ts
4748
- import fs12 from "node:fs";
4963
+ import fs13 from "node:fs";
4749
4964
  import os6 from "node:os";
4750
- import path10 from "node:path";
4965
+ import path11 from "node:path";
4751
4966
  function cmdSnapshot(args) {
4752
4967
  const parsed = parseOptions(args, {
4753
4968
  boolean: new Set(["--push"]),
@@ -4803,6 +5018,12 @@ function cmdSync(args) {
4803
5018
  if (synced) {
4804
5019
  registerCurrentInstance(root, config, { event: "sync", lastSyncAt: nowIso() });
4805
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
+ }
4806
5027
  }
4807
5028
  return 0;
4808
5029
  }
@@ -4846,7 +5067,7 @@ function cmdRedactions(args) {
4846
5067
  let shown = 0;
4847
5068
  let items = 0;
4848
5069
  for (const relPath of files) {
4849
- const delta = fileRedactionDelta(path10.join(sidecarPath, relPath), config.redaction);
5070
+ const delta = fileRedactionDelta(path11.join(sidecarPath, relPath), config.redaction);
4850
5071
  if (!delta)
4851
5072
  continue;
4852
5073
  if (shown)
@@ -4866,12 +5087,12 @@ ${items} redaction(s) in ${shown} file(s) will be pushed this way (mode: ${confi
4866
5087
  return 0;
4867
5088
  }
4868
5089
  function printRedactionDiff(original, redacted) {
4869
- const scratch = fs12.mkdtempSync(path10.join(os6.tmpdir(), "sidecar-redactions-"));
5090
+ const scratch = fs13.mkdtempSync(path11.join(os6.tmpdir(), "sidecar-redactions-"));
4870
5091
  try {
4871
- const localPath = path10.join(scratch, "local");
4872
- const pushedPath = path10.join(scratch, "pushed");
4873
- fs12.writeFileSync(localPath, original, "utf8");
4874
- 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");
4875
5096
  const color = colorLevel() > 0 ? ["--color"] : [];
4876
5097
  const diff = gitRaw(["diff", "--no-index", ...color, "--", localPath, pushedPath], { check: false });
4877
5098
  const lines = diff.stdout.split(`
@@ -4882,16 +5103,16 @@ function printRedactionDiff(original, redacted) {
4882
5103
  if (body)
4883
5104
  console.log(body);
4884
5105
  } finally {
4885
- fs12.rmSync(scratch, { recursive: true, force: true });
5106
+ fs13.rmSync(scratch, { recursive: true, force: true });
4886
5107
  }
4887
5108
  }
4888
5109
  function cmdRedact(args) {
4889
5110
  const parsed = parseOptions(args, { boolean: new Set, value: new Set(["--mode"]) });
4890
5111
  const mode = redactionModeConfigValue(getValue(parsed, "--mode", DEFAULT_REDACTION_MODE), "--mode");
4891
- const output = redactBuffer(fs12.readFileSync(0), mode);
5112
+ const output = redactBuffer(fs13.readFileSync(0), mode);
4892
5113
  let offset = 0;
4893
5114
  while (offset < output.length) {
4894
- offset += fs12.writeSync(1, output, offset, output.length - offset);
5115
+ offset += fs13.writeSync(1, output, offset, output.length - offset);
4895
5116
  }
4896
5117
  return 0;
4897
5118
  }
@@ -4974,6 +5195,7 @@ var init_commands = __esm(() => {
4974
5195
  init_util();
4975
5196
  init_install();
4976
5197
  init_cmd_init();
5198
+ init_cmd_refresh();
4977
5199
  init_cmd_status();
4978
5200
  init_cmd_daemon();
4979
5201
  init_cmd_sync();
@@ -5020,6 +5242,16 @@ var init_commands = __esm(() => {
5020
5242
  { name: "tail", run: cmdTail, section: "sync", usage: "tail [-f|--follow] [-n|--lines count]" },
5021
5243
  { name: "update", run: cmdUpdate, section: "sync", usage: "update" },
5022
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
+ },
5023
5255
  { name: "deinit", run: cmdDeinit, section: "advanced", usage: "deinit" },
5024
5256
  { name: "snapshot", run: cmdSnapshot, section: "advanced", usage: "snapshot [--push] [-m message]" },
5025
5257
  { name: "merge", run: cmdMerge, section: "advanced", usage: "merge [--fork-files] [--no-push]" },
@@ -5090,6 +5322,7 @@ var init_cli = __esm(() => {
5090
5322
  init_sync();
5091
5323
  init_commands();
5092
5324
  init_cmd_init();
5325
+ init_cmd_refresh();
5093
5326
  init_cmd_status();
5094
5327
  init_cmd_daemon();
5095
5328
  init_cmd_sync();
@@ -5097,8 +5330,8 @@ var init_cli = __esm(() => {
5097
5330
 
5098
5331
  // src/bin.ts
5099
5332
  init_cli();
5100
- import fs13 from "node:fs";
5101
- import path11 from "node:path";
5333
+ import fs14 from "node:fs";
5334
+ import path12 from "node:path";
5102
5335
  import { spawnSync as spawnSync5 } from "node:child_process";
5103
5336
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5104
5337
  var SKIP_LOCAL_EXEC_ENV3 = "SIDECAR_SKIP_LOCAL_EXEC";
@@ -5127,15 +5360,15 @@ if (!process.env[SKIP_LOCAL_EXEC_ENV3]) {
5127
5360
  }
5128
5361
  process.exit(await main());
5129
5362
  function findLocalInstall(start, self) {
5130
- let current = path11.resolve(start);
5363
+ let current = path12.resolve(start);
5131
5364
  while (true) {
5132
5365
  if (projectDependsOnSidecar2(current)) {
5133
- 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");
5134
5367
  if (isFile2(candidate) && !sameFile(candidate, self)) {
5135
5368
  return { executable: candidate, newer: localIsNewer(current) };
5136
5369
  }
5137
5370
  }
5138
- const parent = path11.dirname(current);
5371
+ const parent = path12.dirname(current);
5139
5372
  if (parent === current)
5140
5373
  return;
5141
5374
  current = parent;
@@ -5146,11 +5379,11 @@ function localIsNewer(projectRoot) {
5146
5379
  return localVersion !== undefined && compareVersions(localVersion, packageVersion()) > 0;
5147
5380
  }
5148
5381
  function projectDependsOnSidecar2(projectRoot) {
5149
- const manifestPath = path11.join(projectRoot, "package.json");
5382
+ const manifestPath = path12.join(projectRoot, "package.json");
5150
5383
  if (!isFile2(manifestPath))
5151
5384
  return false;
5152
5385
  try {
5153
- const manifest = JSON.parse(fs13.readFileSync(manifestPath, "utf8"));
5386
+ const manifest = JSON.parse(fs14.readFileSync(manifestPath, "utf8"));
5154
5387
  return Boolean(manifest.dependencies?.[PACKAGE_NAME2] || manifest.devDependencies?.[PACKAGE_NAME2] || manifest.optionalDependencies?.[PACKAGE_NAME2] || manifest.peerDependencies?.[PACKAGE_NAME2]);
5155
5388
  } catch {
5156
5389
  return false;
@@ -5158,14 +5391,14 @@ function projectDependsOnSidecar2(projectRoot) {
5158
5391
  }
5159
5392
  function isFile2(filePath) {
5160
5393
  try {
5161
- return fs13.statSync(filePath).isFile();
5394
+ return fs14.statSync(filePath).isFile();
5162
5395
  } catch {
5163
5396
  return false;
5164
5397
  }
5165
5398
  }
5166
5399
  function sameFile(first, second) {
5167
5400
  try {
5168
- return fs13.realpathSync(first) === fs13.realpathSync(second);
5401
+ return fs14.realpathSync(first) === fs14.realpathSync(second);
5169
5402
  } catch {
5170
5403
  return false;
5171
5404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidecarsync",
3
- "version": "1.2.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": {
@@ -14,8 +14,7 @@ try {
14
14
  enableDaemon(packageRoot);
15
15
  recordInstallSource(packageRoot);
16
16
  } else {
17
- cloneIfMissing(packageRoot);
18
- registerWithGlobalSidecar(packageRoot);
17
+ setUpLocalInstall(packageRoot);
19
18
  }
20
19
  } catch (error) {
21
20
  console.warn(`sidecar: postinstall failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -61,10 +60,36 @@ function recordInstallSource(packageRoot) {
61
60
  });
62
61
  }
63
62
 
64
- function cloneIfMissing(packageRoot) {
63
+ // The global install owns the daemon, so it is what makes a cloned checkout
64
+ // actually sync. Without it a clone here would leave the user with a sidecar
65
+ // directory that silently never updates — worse than no checkout at all.
66
+ function setUpLocalInstall(packageRoot) {
65
67
  const projectRoot = findInstallProjectRoot();
66
68
  if (!projectRoot || !fs.existsSync(path.join(projectRoot, ".sidecar"))) return;
67
69
 
70
+ const globalSidecar = findGlobalSidecar(packageRoot);
71
+ if (!globalSidecar) {
72
+ warnMissingGlobalSidecar();
73
+ return;
74
+ }
75
+
76
+ cloneIfMissing(packageRoot, projectRoot);
77
+ registerWithGlobalSidecar(globalSidecar, projectRoot);
78
+ }
79
+
80
+ function warnMissingGlobalSidecar() {
81
+ console.warn(
82
+ [
83
+ "sidecar: no global install found, so this repo's sidecar checkout was not cloned.",
84
+ "sidecar: without the global install there is no daemon, and nothing would sync.",
85
+ "sidecar: install it, then set this repo up:",
86
+ "sidecar: npm install -g sidecarsync",
87
+ "sidecar: sidecar init",
88
+ ].join("\n"),
89
+ );
90
+ }
91
+
92
+ function cloneIfMissing(packageRoot, projectRoot) {
68
93
  const cliPath = path.join(packageRoot, "dist", "cli.js");
69
94
  if (!fs.existsSync(cliPath)) return;
70
95
 
@@ -80,13 +105,7 @@ function cloneIfMissing(packageRoot) {
80
105
  if (output) console.warn(output);
81
106
  }
82
107
 
83
- function registerWithGlobalSidecar(packageRoot) {
84
- const projectRoot = findInstallProjectRoot();
85
- if (!projectRoot || !fs.existsSync(path.join(projectRoot, ".sidecar"))) return;
86
-
87
- const globalSidecar = findGlobalSidecar(packageRoot);
88
- if (!globalSidecar) return;
89
-
108
+ function registerWithGlobalSidecar(globalSidecar, projectRoot) {
90
109
  const result = spawnSync(globalSidecar, ["register-install"], {
91
110
  cwd: projectRoot,
92
111
  encoding: "utf8",