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 +289 -56
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -10
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
4118
|
+
const stat = fs11.statSync(filePath);
|
|
3904
4119
|
if (stat.size > 0) {
|
|
3905
|
-
process.stdout.write(lastLines(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3979
|
-
import
|
|
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 (!
|
|
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 || !
|
|
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 =
|
|
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) =>
|
|
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 (
|
|
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 =
|
|
4365
|
-
if (
|
|
4366
|
-
gitignore = compileGitignoreMatcher(
|
|
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 =
|
|
4585
|
+
const root = path10.resolve(sidecarPath);
|
|
4371
4586
|
return (candidate) => {
|
|
4372
|
-
const relative =
|
|
4587
|
+
const relative = path10.relative(root, candidate);
|
|
4373
4588
|
if (!relative)
|
|
4374
4589
|
return false;
|
|
4375
|
-
const normalized = relative.split(
|
|
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
|
-
|
|
4666
|
+
fs12.mkdirSync(path10.dirname(pidPath), { recursive: true });
|
|
4452
4667
|
while (true) {
|
|
4453
4668
|
try {
|
|
4454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4700
|
+
fs12.rmSync(daemonPidPath(), { force: true });
|
|
4486
4701
|
} catch {}
|
|
4487
4702
|
}
|
|
4488
4703
|
function readPid(pidPath) {
|
|
4489
4704
|
try {
|
|
4490
|
-
const pid = Number(
|
|
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
|
|
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
|
|
4753
|
+
return fs12.realpathSync(filePath);
|
|
4539
4754
|
} catch {
|
|
4540
|
-
return
|
|
4755
|
+
return path10.resolve(filePath);
|
|
4541
4756
|
}
|
|
4542
4757
|
}
|
|
4543
4758
|
function isInsidePath2(child, parent) {
|
|
4544
|
-
const relative =
|
|
4545
|
-
return Boolean(relative) && !relative.startsWith("..") && !
|
|
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
|
|
4963
|
+
import fs13 from "node:fs";
|
|
4749
4964
|
import os6 from "node:os";
|
|
4750
|
-
import
|
|
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(
|
|
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 =
|
|
5090
|
+
const scratch = fs13.mkdtempSync(path11.join(os6.tmpdir(), "sidecar-redactions-"));
|
|
4870
5091
|
try {
|
|
4871
|
-
const localPath =
|
|
4872
|
-
const pushedPath =
|
|
4873
|
-
|
|
4874
|
-
|
|
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
|
-
|
|
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(
|
|
5112
|
+
const output = redactBuffer(fs13.readFileSync(0), mode);
|
|
4892
5113
|
let offset = 0;
|
|
4893
5114
|
while (offset < output.length) {
|
|
4894
|
-
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
|
|
5101
|
-
import
|
|
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 =
|
|
5363
|
+
let current = path12.resolve(start);
|
|
5131
5364
|
while (true) {
|
|
5132
5365
|
if (projectDependsOnSidecar2(current)) {
|
|
5133
|
-
const candidate =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
5401
|
+
return fs14.realpathSync(first) === fs14.realpathSync(second);
|
|
5169
5402
|
} catch {
|
|
5170
5403
|
return false;
|
|
5171
5404
|
}
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -14,8 +14,7 @@ try {
|
|
|
14
14
|
enableDaemon(packageRoot);
|
|
15
15
|
recordInstallSource(packageRoot);
|
|
16
16
|
} else {
|
|
17
|
-
|
|
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
|
-
|
|
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(
|
|
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",
|