sidecarsync 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +205 -39
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -216,12 +216,15 @@ function utcTimestamp() {
|
|
|
216
216
|
function nowIso() {
|
|
217
217
|
return new Date().toISOString();
|
|
218
218
|
}
|
|
219
|
-
var SidecarError;
|
|
219
|
+
var DIRTY_CHECKOUT_EXIT = 75, SidecarError;
|
|
220
220
|
var init_util = __esm(() => {
|
|
221
221
|
SidecarError = class SidecarError extends Error {
|
|
222
|
-
|
|
222
|
+
reason;
|
|
223
|
+
stage;
|
|
224
|
+
constructor(message, options = {}) {
|
|
223
225
|
super(message);
|
|
224
226
|
this.name = "SidecarError";
|
|
227
|
+
this.reason = options.reason;
|
|
225
228
|
}
|
|
226
229
|
};
|
|
227
230
|
});
|
|
@@ -1402,7 +1405,7 @@ function isDirty(repo) {
|
|
|
1402
1405
|
}
|
|
1403
1406
|
function ensureClean(repo) {
|
|
1404
1407
|
if (isDirty(repo))
|
|
1405
|
-
throw new SidecarError("sidecar checkout has uncommitted changes");
|
|
1408
|
+
throw new SidecarError("sidecar checkout has uncommitted changes", { reason: "dirty-checkout" });
|
|
1406
1409
|
}
|
|
1407
1410
|
function sidecarIdentity() {
|
|
1408
1411
|
return { name: currentUser(), email: `${slug(currentUser())}@${slug(currentHost())}.local` };
|
|
@@ -3812,7 +3815,7 @@ function inboxBranchPrefix(template) {
|
|
|
3812
3815
|
const slashIndex = staticPrefix.lastIndexOf("/");
|
|
3813
3816
|
return slashIndex === -1 ? staticPrefix : staticPrefix.slice(0, slashIndex + 1);
|
|
3814
3817
|
}
|
|
3815
|
-
var DEFAULT_PATH = "sidecar", DEFAULT_BRANCH = "main", DEFAULT_INBOX = "sidecar-inbox/{user}/{random}", branchValidity, DEFAULT_PEER = "default", PEER_ENV = "SIDECAR_PEER", SOFT_SYNC_ENV = "SIDECAR_SYNC_SOFT", LOCAL_SYNC_ENV = "SIDECAR_SYNC_LOCAL", PEER_NAME, RESERVED_PEER_SUFFIXES, RESOLVE_MODES, DEFAULT_RESOLVE = "fork", REVIEW_MODES, DEFAULT_REVIEW_TIMEOUT_SECONDS, CONFIG_VERSION = 1, CONFIG_KEYS, failuresArePrivate = false, PUBLIC_REFS = "refs/sidecar-public/";
|
|
3818
|
+
var DEFAULT_PATH = "sidecar", DEFAULT_BRANCH = "main", DEFAULT_INBOX = "sidecar-inbox/{user}/{random}", branchValidity, DEFAULT_PEER = "default", PEER_ENV = "SIDECAR_PEER", SOFT_SYNC_ENV = "SIDECAR_SYNC_SOFT", LOCAL_SYNC_ENV = "SIDECAR_SYNC_LOCAL", DEFAULT_DEBOUNCE_SECONDS = 60, PEER_NAME, RESERVED_PEER_SUFFIXES, RESOLVE_MODES, DEFAULT_RESOLVE = "fork", REVIEW_MODES, DEFAULT_REVIEW_TIMEOUT_SECONDS, CONFIG_VERSION = 1, CONFIG_KEYS, failuresArePrivate = false, PUBLIC_REFS = "refs/sidecar-public/";
|
|
3816
3819
|
var init_config = __esm(() => {
|
|
3817
3820
|
init_dist();
|
|
3818
3821
|
init_util();
|
|
@@ -4200,6 +4203,37 @@ function syncLockIsStale(lockDir) {
|
|
|
4200
4203
|
return error.code !== "EPERM";
|
|
4201
4204
|
}
|
|
4202
4205
|
}
|
|
4206
|
+
function reviewDueDir() {
|
|
4207
|
+
return path6.join(sidecarStateDir(), "review-due");
|
|
4208
|
+
}
|
|
4209
|
+
function reviewDuePath(configPath) {
|
|
4210
|
+
const key = `${realpathOr(path6.dirname(configPath))}\x00${path6.basename(configPath)}`;
|
|
4211
|
+
return path6.join(reviewDueDir(), `${crypto3.createHash("sha256").update(key).digest("hex").slice(0, 16)}.json`);
|
|
4212
|
+
}
|
|
4213
|
+
function writeReviewDue(configPath, dueAt) {
|
|
4214
|
+
const filePath = reviewDuePath(configPath);
|
|
4215
|
+
fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
4216
|
+
const temporary = `${filePath}.${crypto3.randomUUID()}.tmp`;
|
|
4217
|
+
try {
|
|
4218
|
+
fs6.writeFileSync(temporary, `${JSON.stringify({ configPath, dueAt: new Date(dueAt).toISOString() })}
|
|
4219
|
+
`, "utf8");
|
|
4220
|
+
fs6.renameSync(temporary, filePath);
|
|
4221
|
+
} finally {
|
|
4222
|
+
fs6.rmSync(temporary, { force: true });
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
function clearReviewDue(configPath) {
|
|
4226
|
+
fs6.rmSync(reviewDuePath(configPath), { force: true });
|
|
4227
|
+
}
|
|
4228
|
+
function readReviewDue(configPath) {
|
|
4229
|
+
try {
|
|
4230
|
+
const { dueAt } = JSON.parse(fs6.readFileSync(reviewDuePath(configPath), "utf8"));
|
|
4231
|
+
const time = typeof dueAt === "string" ? Date.parse(dueAt) : Number.NaN;
|
|
4232
|
+
return Number.isFinite(time) ? time : undefined;
|
|
4233
|
+
} catch {
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4203
4237
|
var STATE_DIR_ENV = "SIDECAR_STATE_DIR", DEFAULT_SETTINGS, LOG_ROTATE_BYTES, SYNC_LOCK_WAIT_MS;
|
|
4204
4238
|
var init_state = __esm(() => {
|
|
4205
4239
|
init_util();
|
|
@@ -4924,8 +4958,9 @@ function syncProject(root, config, options) {
|
|
|
4924
4958
|
}
|
|
4925
4959
|
if (!options.remote)
|
|
4926
4960
|
return {};
|
|
4927
|
-
if (reviewRequired(config))
|
|
4928
|
-
return publishReviewed(root, sidecarPath, config, inbox, siblings, stage);
|
|
4961
|
+
if (reviewRequired(config)) {
|
|
4962
|
+
return publishReviewed(root, sidecarPath, config, inbox, siblings, stage, options.reviewFloor ?? false);
|
|
4963
|
+
}
|
|
4929
4964
|
stage("push-inbox");
|
|
4930
4965
|
syncBranchBeforePush(sidecarPath, inbox, config);
|
|
4931
4966
|
pushBranch(sidecarPath, inbox);
|
|
@@ -5025,7 +5060,7 @@ function publicTip(repo, branch) {
|
|
|
5025
5060
|
function isPublic(repo, commit) {
|
|
5026
5061
|
return Boolean(git(repo, ["for-each-ref", "--contains", commit, "--format=%(refname)", PUBLIC_REFS]).stdout.trim());
|
|
5027
5062
|
}
|
|
5028
|
-
function publishReviewed(root, sidecarPath, config, inbox, siblings, stage) {
|
|
5063
|
+
function publishReviewed(root, sidecarPath, config, inbox, siblings, stage, reviewFloor) {
|
|
5029
5064
|
stage("fetch");
|
|
5030
5065
|
withPublicGit(sidecarPath, () => {
|
|
5031
5066
|
fetchPublic(sidecarPath);
|
|
@@ -5043,12 +5078,25 @@ function publishReviewed(root, sidecarPath, config, inbox, siblings, stage) {
|
|
|
5043
5078
|
stage("settle-remote");
|
|
5044
5079
|
settleCheckouts(sidecarPath, config, inbox, siblings);
|
|
5045
5080
|
stage("review");
|
|
5046
|
-
const gate = withPublicGit(sidecarPath, () => gateUnpublished(root, sidecarPath, config));
|
|
5047
|
-
if (gate
|
|
5081
|
+
const gate = withPublicGit(sidecarPath, () => gateUnpublished(root, sidecarPath, config, reviewFloor));
|
|
5082
|
+
if (!gate)
|
|
5083
|
+
return {};
|
|
5084
|
+
if ("due" in gate)
|
|
5085
|
+
return { reviewDue: gate.due };
|
|
5086
|
+
if (gate.launch)
|
|
5048
5087
|
launchReviewer(gate.review);
|
|
5049
|
-
return
|
|
5088
|
+
return { held: { review: gate.review, attention: reviewAttention(gate.review) } };
|
|
5050
5089
|
}
|
|
5051
|
-
function gateUnpublished(root, sidecarPath, config) {
|
|
5090
|
+
function gateUnpublished(root, sidecarPath, config, floor) {
|
|
5091
|
+
const configPath = peerConfigPath(root, config.peer);
|
|
5092
|
+
const result = gateReview(root, sidecarPath, config, floor);
|
|
5093
|
+
if (result && "due" in result)
|
|
5094
|
+
writeReviewDue(configPath, result.due);
|
|
5095
|
+
else
|
|
5096
|
+
clearReviewDue(configPath);
|
|
5097
|
+
return result;
|
|
5098
|
+
}
|
|
5099
|
+
function gateReview(root, sidecarPath, config, floor) {
|
|
5052
5100
|
const head = git(sidecarPath, ["rev-parse", `refs/heads/${config.branch}`]).stdout.trim();
|
|
5053
5101
|
const headTree = treeOf(sidecarPath, head);
|
|
5054
5102
|
const publicMain = publicTip(sidecarPath, config.branch);
|
|
@@ -5070,10 +5118,25 @@ function gateUnpublished(root, sidecarPath, config) {
|
|
|
5070
5118
|
return { review: open, launch: false };
|
|
5071
5119
|
markSuperseded(open);
|
|
5072
5120
|
}
|
|
5121
|
+
if (floor) {
|
|
5122
|
+
const due = reviewFloorEndsAt(sidecarPath, config);
|
|
5123
|
+
if (due > Date.now()) {
|
|
5124
|
+
console.log(`the next review opens in ${Math.ceil((due - Date.now()) / 1000)}s; reviews open at most once per debounce`);
|
|
5125
|
+
return { due };
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5073
5128
|
const review = createReview({ root, config, sidecarPath, base, head });
|
|
5074
5129
|
console.log(`opened review ${paint("brand", review.record.id)} for ${review.record.files.length} changed file(s)`);
|
|
5075
5130
|
return { review, launch: hasReviewer };
|
|
5076
5131
|
}
|
|
5132
|
+
function reviewFloorEndsAt(sidecarPath, config) {
|
|
5133
|
+
const previous = listReviews(sidecarPath)[0];
|
|
5134
|
+
if (!previous)
|
|
5135
|
+
return 0;
|
|
5136
|
+
const { createdAt, launchedAt } = previous.record;
|
|
5137
|
+
const started = Math.max(Date.parse(createdAt), launchedAt ? Date.parse(launchedAt) : 0);
|
|
5138
|
+
return started + (config.debounce ?? DEFAULT_DEBOUNCE_SECONDS) * 1000;
|
|
5139
|
+
}
|
|
5077
5140
|
function publishApprovedReview(sidecarPath, config, inbox, review) {
|
|
5078
5141
|
const { record } = review;
|
|
5079
5142
|
const target = record.publishing ?? { inbox, commit: cutPublishCommit(sidecarPath, config, inbox, record) };
|
|
@@ -5333,7 +5396,6 @@ function readFleetHealth(sidecarPath) {
|
|
|
5333
5396
|
return entries.sort((left, right) => rank[left.state] - rank[right.state] || left.record.machine.localeCompare(right.record.machine) || left.branch.localeCompare(right.branch));
|
|
5334
5397
|
}
|
|
5335
5398
|
function mergeInboxBranches(sidecarPath, config, options) {
|
|
5336
|
-
ensureClean(sidecarPath);
|
|
5337
5399
|
ensureCommitIdentity(sidecarPath);
|
|
5338
5400
|
if (options.remote)
|
|
5339
5401
|
fetch(sidecarPath, false);
|
|
@@ -5353,9 +5415,11 @@ function mergeInboxBranches(sidecarPath, config, options) {
|
|
|
5353
5415
|
}
|
|
5354
5416
|
}
|
|
5355
5417
|
if (!hasAnyCommit(sidecarPath)) {
|
|
5418
|
+
ensureClean(sidecarPath);
|
|
5356
5419
|
return mergeInboxBranchesAt(sidecarPath, config, options);
|
|
5357
5420
|
}
|
|
5358
5421
|
if (git(sidecarPath, ["branch", "--show-current"]).stdout.trim() === config.branch) {
|
|
5422
|
+
ensureClean(sidecarPath);
|
|
5359
5423
|
ensureInboxBranch(sidecarPath, config, expandInbox(config, sidecarPath));
|
|
5360
5424
|
}
|
|
5361
5425
|
return withScratchWorktree(sidecarPath, config, "HEAD", (worktree) => mergeInboxBranchesAt(worktree, config, options));
|
|
@@ -5915,7 +5979,7 @@ function syncBranchBeforePush(repo, branch, config) {
|
|
|
5915
5979
|
if (isAncestor(repo, remoteBranch, "HEAD"))
|
|
5916
5980
|
return;
|
|
5917
5981
|
if (isDirty(repo)) {
|
|
5918
|
-
throw new SidecarError(`${remoteBranch} has commits not in local ${branch}, and the sidecar checkout has uncommitted changes
|
|
5982
|
+
throw new SidecarError(`${remoteBranch} has commits not in local ${branch}, and the sidecar checkout has uncommitted changes`, { reason: "dirty-checkout" });
|
|
5919
5983
|
}
|
|
5920
5984
|
if (isAncestor(repo, "HEAD", remoteBranch)) {
|
|
5921
5985
|
git(repo, ["merge", "--ff-only", remoteBranch]);
|
|
@@ -5926,12 +5990,19 @@ function syncBranchBeforePush(repo, branch, config) {
|
|
|
5926
5990
|
function refreshInboxFromMain(repo, config, inbox) {
|
|
5927
5991
|
if (!branchExists(repo, inbox) || !branchExists(repo, config.branch))
|
|
5928
5992
|
return;
|
|
5929
|
-
|
|
5993
|
+
if (isDirty(repo)) {
|
|
5994
|
+
logSidecarEvent("settle-deferred", { sidecarPath: repo });
|
|
5995
|
+
return;
|
|
5996
|
+
}
|
|
5930
5997
|
git(repo, ["switch", inbox]);
|
|
5931
5998
|
const result = git(repo, ["merge", "--ff-only", config.branch], { check: false });
|
|
5932
|
-
if (result.status
|
|
5933
|
-
|
|
5999
|
+
if (result.status === 0)
|
|
6000
|
+
return;
|
|
6001
|
+
if (isDirty(repo)) {
|
|
6002
|
+
logSidecarEvent("settle-deferred", { sidecarPath: repo });
|
|
6003
|
+
return;
|
|
5934
6004
|
}
|
|
6005
|
+
throw new SidecarError(result.stderr.trim() || `could not fast-forward ${inbox} to ${config.branch}`);
|
|
5935
6006
|
}
|
|
5936
6007
|
function pushBranch(repo, branch) {
|
|
5937
6008
|
git(repo, ["push", "--no-follow-tags", "-u", "origin", `HEAD:refs/heads/${branch}`]);
|
|
@@ -8229,6 +8300,7 @@ __export(exports_daemon, {
|
|
|
8229
8300
|
selectWatchTargets: () => selectWatchTargets,
|
|
8230
8301
|
scheduleFor: () => scheduleFor,
|
|
8231
8302
|
runDaemonLoop: () => runDaemonLoop,
|
|
8303
|
+
reviewDueAt: () => reviewDueAt,
|
|
8232
8304
|
refreshWatchers: () => refreshWatchers,
|
|
8233
8305
|
compileGitignoreMatcher: () => compileGitignoreMatcher,
|
|
8234
8306
|
checkAndInstallUpdate: () => checkAndInstallUpdate,
|
|
@@ -8299,6 +8371,7 @@ async function runDaemonLoop(options) {
|
|
|
8299
8371
|
await acquireDaemonPid();
|
|
8300
8372
|
installShutdownHandlers();
|
|
8301
8373
|
await watchRegistry(state);
|
|
8374
|
+
await watchReviewDue(state);
|
|
8302
8375
|
const bootVersion = packageVersion();
|
|
8303
8376
|
while (true) {
|
|
8304
8377
|
maybeAdoptNewerInstall(state, bootVersion);
|
|
@@ -8411,10 +8484,11 @@ async function syncInstance(state, key, trigger, options = {}) {
|
|
|
8411
8484
|
state.syncingFamilies.add(family);
|
|
8412
8485
|
if (!options.localOnly) {
|
|
8413
8486
|
state.lastRemoteSyncAt.set(key, Date.now());
|
|
8414
|
-
clearTimeout(state.remoteTimers.get(key));
|
|
8487
|
+
clearTimeout(state.remoteTimers.get(key)?.timer);
|
|
8415
8488
|
state.remoteTimers.delete(key);
|
|
8416
8489
|
}
|
|
8417
8490
|
let succeeded = false;
|
|
8491
|
+
let dirtyCheckout = false;
|
|
8418
8492
|
try {
|
|
8419
8493
|
const localCli = localSidecarCliPath(root);
|
|
8420
8494
|
const cli = localCli ?? currentCliPath();
|
|
@@ -8431,19 +8505,27 @@ async function syncInstance(state, key, trigger, options = {}) {
|
|
|
8431
8505
|
},
|
|
8432
8506
|
timeoutMs: SYNC_TIMEOUT_MS
|
|
8433
8507
|
});
|
|
8508
|
+
dirtyCheckout = !result.timedOut && result.status === DIRTY_CHECKOUT_EXIT;
|
|
8434
8509
|
if (result.status === 0) {
|
|
8435
|
-
|
|
8436
|
-
|
|
8510
|
+
if (!options.localOnly) {
|
|
8511
|
+
state.failures.delete(key);
|
|
8512
|
+
state.skipUntilCycle.delete(key);
|
|
8513
|
+
}
|
|
8437
8514
|
logSidecarEvent("daemon-sync", { ...peerFields(key), trigger, local: Boolean(localCli) });
|
|
8438
8515
|
succeeded = true;
|
|
8439
8516
|
} else {
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8517
|
+
if (!dirtyCheckout) {
|
|
8518
|
+
const failures = (state.failures.get(key) ?? 0) + 1;
|
|
8519
|
+
state.failures.set(key, failures);
|
|
8520
|
+
state.skipUntilCycle.set(key, state.cycleCount + Math.min(2 ** (failures - 1), MAX_BACKOFF_CYCLES));
|
|
8521
|
+
}
|
|
8522
|
+
if (!options.localOnly)
|
|
8523
|
+
state.lastRemoteSyncAt.set(key, Date.now());
|
|
8443
8524
|
logSidecarEvent("failure", {
|
|
8444
8525
|
command: "daemon",
|
|
8445
8526
|
...peerFields(key),
|
|
8446
8527
|
trigger,
|
|
8528
|
+
reason: result.timedOut ? "timeout" : dirtyCheckout ? "dirty-checkout" : `exit-${result.status}`,
|
|
8447
8529
|
message: result.timedOut ? "sync timed out" : !outputIsPrivate(key) && result.output.trim().slice(-500) || `sync exited ${result.status}`
|
|
8448
8530
|
});
|
|
8449
8531
|
}
|
|
@@ -8453,12 +8535,24 @@ async function syncInstance(state, key, trigger, options = {}) {
|
|
|
8453
8535
|
if (options.localOnly)
|
|
8454
8536
|
armRemoteSync(state, key);
|
|
8455
8537
|
}
|
|
8456
|
-
if (succeeded)
|
|
8538
|
+
if (succeeded) {
|
|
8539
|
+
if (!options.localOnly)
|
|
8540
|
+
armReviewDue(state, key, "after-sync");
|
|
8457
8541
|
await followUpTrailingSync(state, key);
|
|
8458
|
-
else
|
|
8459
|
-
state.
|
|
8542
|
+
} else {
|
|
8543
|
+
followUpFailedSync(state, key, { dirtyCheckout, remote: !options.localOnly });
|
|
8544
|
+
}
|
|
8460
8545
|
return succeeded;
|
|
8461
8546
|
}
|
|
8547
|
+
function followUpFailedSync(state, key, failure) {
|
|
8548
|
+
if (failure.dirtyCheckout) {
|
|
8549
|
+
state.trailingPending.add(key);
|
|
8550
|
+
if (failure.remote)
|
|
8551
|
+
armRemoteSync(state, key);
|
|
8552
|
+
}
|
|
8553
|
+
if (state.trailingPending.has(key) && !state.pendingTimers.has(key))
|
|
8554
|
+
openTrailingWindow(state, key, SETTLE_WINDOW_MS);
|
|
8555
|
+
}
|
|
8462
8556
|
async function followUpTrailingSync(state, key) {
|
|
8463
8557
|
if (!state.trailingPending.delete(key))
|
|
8464
8558
|
return;
|
|
@@ -8474,7 +8568,9 @@ async function checkoutIsDirty(key) {
|
|
|
8474
8568
|
if (!sidecarPath || !fs14.existsSync(sidecarPath))
|
|
8475
8569
|
return false;
|
|
8476
8570
|
const result = await runChild("git", ["-C", sidecarPath, "status", "--porcelain"], { timeoutMs: 30000 });
|
|
8477
|
-
|
|
8571
|
+
if (result.status !== 0)
|
|
8572
|
+
return true;
|
|
8573
|
+
return Boolean(result.stdout.trim());
|
|
8478
8574
|
}
|
|
8479
8575
|
function localSidecarCliPath(root) {
|
|
8480
8576
|
if (!projectDependsOnSidecar(root))
|
|
@@ -8602,14 +8698,35 @@ async function beginWatchSync(state, key) {
|
|
|
8602
8698
|
state.trailingPending.add(key);
|
|
8603
8699
|
}
|
|
8604
8700
|
function armRemoteSync(state, key) {
|
|
8605
|
-
if (state.remoteTimers.has(key))
|
|
8606
|
-
return;
|
|
8607
8701
|
const elapsed = Date.now() - (state.lastRemoteSyncAt.get(key) ?? 0);
|
|
8702
|
+
armRemoteAt(state, key, Date.now() + Math.max(SETTLE_WINDOW_MS, scheduleFor(key, state.options).debounceSeconds * 1000 - elapsed), "remote-due");
|
|
8703
|
+
}
|
|
8704
|
+
function armRemoteAt(state, key, at, trigger) {
|
|
8705
|
+
const booked = state.remoteTimers.get(key);
|
|
8706
|
+
if (booked && booked.at <= at)
|
|
8707
|
+
return;
|
|
8708
|
+
clearTimeout(booked?.timer);
|
|
8608
8709
|
const timer = setTimeout(() => {
|
|
8609
8710
|
state.remoteTimers.delete(key);
|
|
8610
|
-
syncInstance(state, key,
|
|
8611
|
-
}, Math.max(
|
|
8612
|
-
state.remoteTimers.set(key, timer);
|
|
8711
|
+
syncInstance(state, key, trigger);
|
|
8712
|
+
}, Math.max(0, at - Date.now()));
|
|
8713
|
+
state.remoteTimers.set(key, { timer, at });
|
|
8714
|
+
}
|
|
8715
|
+
function armReviewDue(state, key, when) {
|
|
8716
|
+
const at = reviewDueAt(readReviewDue(key), when, Date.now(), scheduleFor(key, state.options).debounceSeconds * 1000);
|
|
8717
|
+
if (at !== undefined)
|
|
8718
|
+
armRemoteAt(state, key, at, "review-due");
|
|
8719
|
+
}
|
|
8720
|
+
function reviewDueAt(due, when, now, debounceMs) {
|
|
8721
|
+
if (due === undefined)
|
|
8722
|
+
return;
|
|
8723
|
+
if (due > now)
|
|
8724
|
+
return due;
|
|
8725
|
+
if (when === "start")
|
|
8726
|
+
return now;
|
|
8727
|
+
if (when === "after-sync")
|
|
8728
|
+
return now + Math.max(SETTLE_WINDOW_MS, debounceMs);
|
|
8729
|
+
return;
|
|
8613
8730
|
}
|
|
8614
8731
|
function remoteIsDue(state, key) {
|
|
8615
8732
|
const last = state.lastRemoteSyncAt.get(key) ?? 0;
|
|
@@ -8653,6 +8770,34 @@ async function watchRegistry(state) {
|
|
|
8653
8770
|
});
|
|
8654
8771
|
}
|
|
8655
8772
|
}
|
|
8773
|
+
async function watchReviewDue(state) {
|
|
8774
|
+
const scan = (when) => {
|
|
8775
|
+
for (const instance of readInstances())
|
|
8776
|
+
armReviewDue(state, instance.configPath, when);
|
|
8777
|
+
};
|
|
8778
|
+
scan("start");
|
|
8779
|
+
const chokidar = await loadChokidar();
|
|
8780
|
+
if (!chokidar)
|
|
8781
|
+
return;
|
|
8782
|
+
try {
|
|
8783
|
+
fs14.mkdirSync(reviewDueDir(), { recursive: true });
|
|
8784
|
+
const watcher = chokidar.watch(reviewDueDir(), { ignoreInitial: true, depth: 0 });
|
|
8785
|
+
let timer;
|
|
8786
|
+
watcher.on("all", () => {
|
|
8787
|
+
if (timer)
|
|
8788
|
+
return;
|
|
8789
|
+
timer = setTimeout(() => {
|
|
8790
|
+
timer = undefined;
|
|
8791
|
+
scan("change");
|
|
8792
|
+
}, 500);
|
|
8793
|
+
});
|
|
8794
|
+
} catch (error) {
|
|
8795
|
+
logSidecarEvent("failure", {
|
|
8796
|
+
command: "daemon",
|
|
8797
|
+
message: `could not watch review due times: ${error instanceof Error ? error.message : String(error)}`
|
|
8798
|
+
});
|
|
8799
|
+
}
|
|
8800
|
+
}
|
|
8656
8801
|
function compileGitignoreMatcher(lines) {
|
|
8657
8802
|
const rules = import_ignore.default({ ignorecase: false }).add(lines);
|
|
8658
8803
|
return (relativePath) => {
|
|
@@ -8978,7 +9123,7 @@ async function cmdDaemonRun(args) {
|
|
|
8978
9123
|
if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
|
|
8979
9124
|
throw new SidecarError("--interval must be > 0");
|
|
8980
9125
|
}
|
|
8981
|
-
const debounceSeconds = Number(getValue(parsed, "--debounce",
|
|
9126
|
+
const debounceSeconds = Number(getValue(parsed, "--debounce", String(DEFAULT_DEBOUNCE_SECONDS)));
|
|
8982
9127
|
if (!Number.isFinite(debounceSeconds) || debounceSeconds < 0) {
|
|
8983
9128
|
throw new SidecarError("--debounce must be >= 0");
|
|
8984
9129
|
}
|
|
@@ -9102,7 +9247,8 @@ function cmdSync(args) {
|
|
|
9102
9247
|
lock: soft ? "skip" : "throw",
|
|
9103
9248
|
remote: !parsed.flags.has("--local") && process.env[LOCAL_SYNC_ENV] !== "1",
|
|
9104
9249
|
snapshot: !parsed.flags.has("--no-snapshot"),
|
|
9105
|
-
message: getValue(parsed, "--message", getValue(parsed, "-m", "")) || undefined
|
|
9250
|
+
message: getValue(parsed, "--message", getValue(parsed, "-m", "")) || undefined,
|
|
9251
|
+
reviewFloor: soft
|
|
9106
9252
|
};
|
|
9107
9253
|
for (const peer of peers) {
|
|
9108
9254
|
announcePeer(peer, peers);
|
|
@@ -9113,12 +9259,13 @@ function cmdSync(args) {
|
|
|
9113
9259
|
} catch (error) {
|
|
9114
9260
|
if (peers.length === 1)
|
|
9115
9261
|
throw error;
|
|
9116
|
-
failed.push(peer.name);
|
|
9262
|
+
failed.push({ name: peer.name, error });
|
|
9117
9263
|
console.error(`sidecar: ${peer.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
9118
9264
|
}
|
|
9119
9265
|
}
|
|
9120
9266
|
if (failed.length) {
|
|
9121
|
-
|
|
9267
|
+
const dirty = failed.every(({ error }) => error instanceof SidecarError && error.reason === "dirty-checkout");
|
|
9268
|
+
throw new SidecarError(`${failed.length} of ${peers.length} peers failed to sync: ${failed.map(({ name }) => name).join(", ")}`, dirty ? { reason: "dirty-checkout" } : {});
|
|
9122
9269
|
}
|
|
9123
9270
|
return 0;
|
|
9124
9271
|
}
|
|
@@ -9135,6 +9282,7 @@ function syncPeer(peer, options) {
|
|
|
9135
9282
|
snapshot: options.snapshot,
|
|
9136
9283
|
remote,
|
|
9137
9284
|
message: options.message,
|
|
9285
|
+
reviewFloor: options.reviewFloor,
|
|
9138
9286
|
onStage: (name) => {
|
|
9139
9287
|
stage = name;
|
|
9140
9288
|
}
|
|
@@ -9142,6 +9290,8 @@ function syncPeer(peer, options) {
|
|
|
9142
9290
|
registerCurrentInstance(root, config, remote ? { event: "sync", lastSyncAt: nowIso() } : { event: "sync-local" });
|
|
9143
9291
|
});
|
|
9144
9292
|
} catch (error) {
|
|
9293
|
+
if (error instanceof SidecarError)
|
|
9294
|
+
error.stage ??= stage;
|
|
9145
9295
|
if (remote)
|
|
9146
9296
|
reportSyncHealth(root, config, {
|
|
9147
9297
|
status: "failed",
|
|
@@ -9156,8 +9306,9 @@ function syncPeer(peer, options) {
|
|
|
9156
9306
|
const { id } = held.review.record;
|
|
9157
9307
|
console.log(held.attention ? `sidecar: publishing is held — review ${id} needs attention: ${held.attention}; \`sidecar review show ${id}\`` : `sidecar: publishing is held for review ${id}; \`sidecar review show ${id}\``);
|
|
9158
9308
|
}
|
|
9159
|
-
if (remote)
|
|
9160
|
-
reportSyncHealth(root, config, held ? { status: "ok", review: held.attention ? "attention" : "pending" } : { status: "ok" });
|
|
9309
|
+
if (remote) {
|
|
9310
|
+
reportSyncHealth(root, config, held ? { status: "ok", review: held.attention ? "attention" : "pending" } : outcome.reviewDue ? { status: "ok", review: "pending" } : { status: "ok" });
|
|
9311
|
+
}
|
|
9161
9312
|
if (!soft) {
|
|
9162
9313
|
const sidecarPath = resolveSidecarPath(root, config);
|
|
9163
9314
|
if (checkoutIsUnlinkedFromFamily(root, config, sidecarPath)) {
|
|
@@ -9425,7 +9576,20 @@ function decideCommand(verdict, args) {
|
|
|
9425
9576
|
return 0;
|
|
9426
9577
|
}
|
|
9427
9578
|
const peer = loadPeer(selected.record.root, selected.record.peer);
|
|
9428
|
-
|
|
9579
|
+
try {
|
|
9580
|
+
syncPeer(peer, { lock: "wait", remote: true, snapshot: true, reviewFloor: true });
|
|
9581
|
+
} catch (error) {
|
|
9582
|
+
if (readReview(id)?.state !== "published") {
|
|
9583
|
+
console.error(`sidecar: review ${id} is approved, but this sync could not confirm it published; the next sync finishes publishing it`);
|
|
9584
|
+
throw error;
|
|
9585
|
+
}
|
|
9586
|
+
logSidecarEvent("review-followup-failure", {
|
|
9587
|
+
id,
|
|
9588
|
+
...error instanceof SidecarError && error.reason ? { reason: error.reason } : {},
|
|
9589
|
+
...error instanceof SidecarError && error.stage ? { stage: error.stage } : {}
|
|
9590
|
+
});
|
|
9591
|
+
console.error(`sidecar: published review ${id}; the sync after it stopped early and the next sync finishes it: ${error instanceof Error ? error.message : String(error)}`);
|
|
9592
|
+
}
|
|
9429
9593
|
return 0;
|
|
9430
9594
|
}
|
|
9431
9595
|
function retryCommand(args) {
|
|
@@ -9680,12 +9844,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
9680
9844
|
if (command !== "redact" && command !== "deinit" && shouldUseGlobalRegistry()) {
|
|
9681
9845
|
logSidecarEvent("failure", {
|
|
9682
9846
|
command,
|
|
9847
|
+
...error instanceof SidecarError && error.reason ? { reason: error.reason } : {},
|
|
9848
|
+
...error instanceof SidecarError && error.stage ? { stage: error.stage } : {},
|
|
9683
9849
|
...privateFailures() ? {} : { message: error instanceof Error ? error.message : String(error) }
|
|
9684
9850
|
});
|
|
9685
9851
|
}
|
|
9686
9852
|
if (error instanceof SidecarError) {
|
|
9687
9853
|
console.error(`${paint("bad", "sidecar:", colorLevel(process.stderr))} ${error.message}`);
|
|
9688
|
-
return 1;
|
|
9854
|
+
return error.reason === "dirty-checkout" ? DIRTY_CHECKOUT_EXIT : 1;
|
|
9689
9855
|
}
|
|
9690
9856
|
if (error instanceof Error && error.name === "AbortError") {
|
|
9691
9857
|
console.error("sidecar: stopped");
|