@narumitw/pi-sync 0.54.2 → 0.56.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/README.md +40 -15
- package/dist/chunks/agent-candidate-3JN6DJKT.ts +26 -0
- package/dist/chunks/agent-plan-QCUJWBP4.ts +99 -0
- package/dist/chunks/agent-plan-QCUJWBP4.ts.map +7 -0
- package/dist/chunks/agent-runner-E2JU2VZ6.ts +182 -0
- package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +7 -0
- package/dist/chunks/agent-store-A3F6IYWZ.ts +152 -0
- package/dist/chunks/agent-store-A3F6IYWZ.ts.map +7 -0
- package/dist/chunks/chunk-5Q5SSKNG.ts +128 -0
- package/dist/chunks/chunk-5Q5SSKNG.ts.map +7 -0
- package/dist/chunks/chunk-5XZOC7FD.ts +215 -0
- package/dist/chunks/chunk-5XZOC7FD.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-AAEEHR7H.ts} +31 -87
- package/dist/chunks/chunk-AAEEHR7H.ts.map +7 -0
- package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-B4QZBDBT.ts} +2 -2
- package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-CRKTAPSL.ts} +8 -8
- package/dist/chunks/chunk-DT7M4VQS.ts +69 -0
- package/dist/chunks/chunk-DT7M4VQS.ts.map +7 -0
- package/dist/chunks/chunk-ED3TYIVW.ts +229 -0
- package/dist/chunks/chunk-ED3TYIVW.ts.map +7 -0
- package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-EUMINX7L.ts} +6 -6
- package/dist/chunks/chunk-FQOSHGJ7.ts +215 -0
- package/dist/chunks/chunk-FQOSHGJ7.ts.map +7 -0
- package/dist/chunks/chunk-GNH73BJ5.ts +153 -0
- package/dist/chunks/chunk-GNH73BJ5.ts.map +7 -0
- package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-GP5DU62N.ts} +8 -9
- package/dist/chunks/chunk-GP5DU62N.ts.map +7 -0
- package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-GSNPASKB.ts} +2 -2
- package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-IWZH5KYS.ts} +23 -7
- package/dist/chunks/chunk-IWZH5KYS.ts.map +7 -0
- package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-JJTARUSO.ts} +15 -14
- package/dist/chunks/chunk-JJTARUSO.ts.map +7 -0
- package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-NLWS55B6.ts} +15 -2
- package/dist/chunks/chunk-NLWS55B6.ts.map +7 -0
- package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-OEWQVWLC.ts} +4 -4
- package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-OTZRCKYP.ts} +380 -510
- package/dist/chunks/chunk-OTZRCKYP.ts.map +7 -0
- package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-OWBAZA6J.ts} +24 -12
- package/dist/chunks/chunk-OWBAZA6J.ts.map +7 -0
- package/dist/chunks/chunk-PRUEIIK7.ts +24 -0
- package/dist/chunks/chunk-PRUEIIK7.ts.map +7 -0
- package/dist/chunks/chunk-QGK445K7.ts +21 -0
- package/dist/chunks/chunk-QGK445K7.ts.map +7 -0
- package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-QO333NM4.ts} +49 -21
- package/dist/chunks/chunk-QO333NM4.ts.map +7 -0
- package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-QSHMOL3I.ts} +22 -24
- package/dist/chunks/chunk-QSHMOL3I.ts.map +7 -0
- package/dist/chunks/chunk-S2FMH5FT.ts +97 -0
- package/dist/chunks/chunk-S2FMH5FT.ts.map +7 -0
- package/dist/chunks/{chunk-BVVP33U5.ts → chunk-SWRVXVFD.ts} +12 -9
- package/dist/chunks/chunk-SWRVXVFD.ts.map +7 -0
- package/dist/chunks/{chunk-ZX2Q6THB.ts → chunk-UBV24CG4.ts} +4 -5
- package/dist/chunks/chunk-UBV24CG4.ts.map +7 -0
- package/dist/chunks/chunk-UOHR7LRO.ts +70 -0
- package/dist/chunks/chunk-UOHR7LRO.ts.map +7 -0
- package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-UPNVIUBN.ts} +3 -3
- package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
- package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
- package/dist/chunks/chunk-VJN7KU4J.ts +350 -0
- package/dist/chunks/chunk-VJN7KU4J.ts.map +7 -0
- package/dist/chunks/chunk-YHT4YTYG.ts +31 -0
- package/dist/chunks/chunk-YHT4YTYG.ts.map +7 -0
- package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-YK52VDXG.ts} +6 -5
- package/dist/chunks/chunk-YK52VDXG.ts.map +7 -0
- package/dist/chunks/config-KGIYTTXQ.ts +37 -0
- package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-FXFIIBCB.ts} +8 -7
- package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-6WIEQLXW.ts} +31 -24
- package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-6WIEQLXW.ts.map} +1 -1
- package/dist/chunks/continuous-record-LFRVXQBD.ts +30 -0
- package/dist/chunks/continuous-sync-AWAAOXK2.ts +545 -0
- package/dist/chunks/continuous-sync-AWAAOXK2.ts.map +7 -0
- package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-3JJYOVNR.ts} +5 -5
- package/dist/chunks/{git-backend-FD7AVSK6.ts → git-backend-KYDZ673X.ts} +28 -32
- package/dist/chunks/git-backend-KYDZ673X.ts.map +7 -0
- package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-AGBBVDDS.ts} +10 -9
- package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-KRAW3NE2.ts} +464 -164
- package/dist/chunks/manager-ui-KRAW3NE2.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-RFCAH7KL.ts} +7 -5
- package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-5725JDNC.ts} +9 -8
- package/dist/chunks/remote-selection-ui-5725JDNC.ts.map +7 -0
- package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y7WSCLJI.ts} +38 -38
- package/dist/chunks/s3-backend-Y7WSCLJI.ts.map +7 -0
- package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-E7PHKI6J.ts} +7 -6
- package/dist/chunks/setup-switch-E7PHKI6J.ts.map +7 -0
- package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-65DROCIJ.ts} +14 -13
- package/dist/chunks/setup-wizard-65DROCIJ.ts.map +7 -0
- package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-XFJOAMJD.ts} +9 -8
- package/dist/chunks/sync-inspection-XFJOAMJD.ts.map +7 -0
- package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-HDUDT5PD.ts} +188 -73
- package/dist/chunks/sync-operations-HDUDT5PD.ts.map +7 -0
- package/dist/chunks/{webdav-backend-TBILVEQK.ts → webdav-backend-6K26F6LT.ts} +39 -48
- package/dist/chunks/webdav-backend-6K26F6LT.ts.map +7 -0
- package/dist/index.ts +246 -131
- package/dist/index.ts.map +3 -3
- package/docs/agent-mode-verification.md +99 -0
- package/docs/continuous-sync-verification.md +87 -0
- package/docs/settings.md +131 -11
- package/docs/tui-contract.md +61 -0
- package/package.json +3 -2
- package/src/backends/git/git-backend.ts +1 -4
- package/src/backends/git/git-runner.ts +1 -5
- package/src/backends/s3/s3-backend.ts +3 -25
- package/src/backends/s3/s3-client.ts +14 -2
- package/src/backends/snapshot-pointer.ts +27 -0
- package/src/backends/webdav/webdav-backend.ts +3 -29
- package/src/backends/webdav/webdav-client.ts +11 -7
- package/src/commands/command-execution.ts +7 -3
- package/src/commands/command-handler.ts +15 -3
- package/src/commands/command-types.ts +10 -0
- package/src/commands/command.ts +4 -1
- package/src/settings/config.ts +20 -10
- package/src/settings/settings-store.ts +1 -34
- package/src/settings/settings-types.ts +10 -0
- package/src/settings/settings-validation.ts +4 -1
- package/src/snapshot/snapshot-codec.ts +1 -5
- package/src/state/lock.ts +13 -10
- package/src/state/merge-baseline-store.ts +19 -1
- package/src/state/state-directory.ts +8 -1
- package/src/sync/agent-candidate.ts +251 -0
- package/src/sync/agent-plan.ts +87 -0
- package/src/sync/agent-policy.ts +13 -0
- package/src/sync/agent-request.ts +17 -0
- package/src/sync/agent-runner.ts +188 -0
- package/src/sync/agent-store.ts +151 -0
- package/src/sync/agent-sync.ts +91 -0
- package/src/sync/apply-review.ts +63 -0
- package/src/sync/automatic-sync.ts +46 -33
- package/src/sync/continuous-errors.ts +85 -0
- package/src/sync/continuous-lease.ts +221 -0
- package/src/sync/continuous-record.ts +179 -0
- package/src/sync/continuous-storage.ts +55 -0
- package/src/sync/continuous-sync.ts +516 -0
- package/src/sync/continuous-watch.ts +59 -0
- package/src/sync/merged-sync.ts +268 -112
- package/src/sync/remote-snapshot.ts +2 -3
- package/src/sync/settings-merge.ts +19 -3
- package/src/sync/setup-switch.ts +1 -5
- package/src/sync/startup-check.ts +51 -29
- package/src/sync/startup-timings.ts +90 -0
- package/src/sync/sync-inspection.ts +16 -5
- package/src/sync/sync-mutations.ts +55 -28
- package/src/sync/sync-queries.ts +19 -3
- package/src/sync/text-merge.ts +3 -3
- package/src/sync-extension.ts +75 -19
- package/src/ui/agent-mode-setting.ts +91 -0
- package/src/ui/manager-state.ts +12 -6
- package/src/ui/manager-ui.ts +57 -7
- package/src/ui/merge-review.ts +3 -3
- package/src/ui/merged-sync-summary.ts +2 -18
- package/src/ui/remote-selection-ui.ts +5 -5
- package/src/ui/settings-frame.ts +55 -0
- package/src/ui/settings-ui.ts +223 -43
- package/src/ui/setup/s3-credentials-ui.ts +1 -5
- package/src/ui/setup/setup-name-ui.ts +2 -5
- package/src/ui/setup/setup-prompts.ts +4 -3
- package/src/ui/setup/setup-switcher.ts +2 -0
- package/src/ui/sync-behavior.ts +15 -0
- package/src/ui/sync-format.ts +69 -38
- package/src/ui/sync-setups-ui.ts +3 -5
- package/src/ui/transfer-changes.ts +33 -0
- package/src/ui/transfer-review.ts +27 -0
- package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
- package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
- package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
- package/dist/chunks/chunk-5ZWQ33XL.ts.map +0 -7
- package/dist/chunks/chunk-BVVP33U5.ts.map +0 -7
- package/dist/chunks/chunk-IJ6VV6FN.ts.map +0 -7
- package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
- package/dist/chunks/chunk-ORQRHEEO.ts +0 -362
- package/dist/chunks/chunk-ORQRHEEO.ts.map +0 -7
- package/dist/chunks/chunk-QE7NZZWT.ts.map +0 -7
- package/dist/chunks/chunk-UQ3ALBIB.ts.map +0 -7
- package/dist/chunks/chunk-V6VZ5TLG.ts.map +0 -7
- package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
- package/dist/chunks/chunk-ZX2Q6THB.ts.map +0 -7
- package/dist/chunks/git-backend-FD7AVSK6.ts.map +0 -7
- package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
- package/dist/chunks/s3-backend-AQGV6LFT.ts.map +0 -7
- package/dist/chunks/setup-wizard-GSDPDFC4.ts.map +0 -7
- package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
- package/dist/chunks/webdav-backend-TBILVEQK.ts.map +0 -7
- /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → agent-candidate-3JN6DJKT.ts.map} +0 -0
- /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-B4QZBDBT.ts.map} +0 -0
- /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-CRKTAPSL.ts.map} +0 -0
- /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-EUMINX7L.ts.map} +0 -0
- /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-GSNPASKB.ts.map} +0 -0
- /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-OEWQVWLC.ts.map} +0 -0
- /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-UPNVIUBN.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → config-KGIYTTXQ.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → conflict-artifacts-FXFIIBCB.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → continuous-record-LFRVXQBD.ts.map} +0 -0
- /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-3JJYOVNR.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → manager-result-dispatcher-AGBBVDDS.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → merge-baseline-store-RFCAH7KL.ts.map} +0 -0
|
@@ -10,7 +10,7 @@ export type SettingsMergeResult =
|
|
|
10
10
|
| { kind: "merged"; content: Buffer }
|
|
11
11
|
| { kind: "review"; reason: "unsupported-format" | "field-conflict"; fields: string[] };
|
|
12
12
|
|
|
13
|
-
const COUPLED_FIELDS = [
|
|
13
|
+
export const COUPLED_FIELDS = [
|
|
14
14
|
["defaultProvider", "defaultModel"],
|
|
15
15
|
["queueMode", "steeringMode"],
|
|
16
16
|
["websockets", "transport"],
|
|
@@ -19,7 +19,12 @@ const COUPLED_FIELDS = [
|
|
|
19
19
|
] as const;
|
|
20
20
|
|
|
21
21
|
/** Only global settings.json; objects/arrays are atomic units until their invariants are proven. */
|
|
22
|
-
export function mergeSettingsJson(
|
|
22
|
+
export function mergeSettingsJson(
|
|
23
|
+
ancestor: Buffer,
|
|
24
|
+
local: Buffer,
|
|
25
|
+
remote: Buffer,
|
|
26
|
+
selections: ReadonlyMap<string, "local" | "remote"> = new Map(),
|
|
27
|
+
): SettingsMergeResult {
|
|
23
28
|
let documents: JsonDocument[];
|
|
24
29
|
try {
|
|
25
30
|
documents = [ancestor, local, remote].map(parseSettingsDocument);
|
|
@@ -38,7 +43,18 @@ export function mergeSettingsJson(ancestor: Buffer, local: Buffer, remote: Buffe
|
|
|
38
43
|
unit.every((field) =>
|
|
39
44
|
jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value),
|
|
40
45
|
);
|
|
41
|
-
const
|
|
46
|
+
const choice = selections.get(unit[0] ?? "");
|
|
47
|
+
const source = equal(left, right)
|
|
48
|
+
? left
|
|
49
|
+
: equal(left, base)
|
|
50
|
+
? right
|
|
51
|
+
: equal(right, base)
|
|
52
|
+
? left
|
|
53
|
+
: choice === "local"
|
|
54
|
+
? left
|
|
55
|
+
: choice === "remote"
|
|
56
|
+
? right
|
|
57
|
+
: undefined;
|
|
42
58
|
if (!source) {
|
|
43
59
|
conflicts.push(...unit.filter((field) => fields.has(field)));
|
|
44
60
|
continue;
|
package/src/sync/setup-switch.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { configuredSyncSetupNames, updateLocalConfig } from "../settings/setting
|
|
|
5
5
|
import type { OnSwitchAction } from "../settings/settings-types.js";
|
|
6
6
|
import { normalizeOnSwitch } from "../settings/settings-validation.js";
|
|
7
7
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
8
|
+
import { throwIfAborted } from "./signals.js";
|
|
8
9
|
import { SetupPullRequiresUiError } from "./sync-errors.js";
|
|
9
10
|
|
|
10
11
|
export { SetupPullRequiresUiError } from "./sync-errors.js";
|
|
@@ -128,8 +129,3 @@ export async function useSyncSetup(
|
|
|
128
129
|
}
|
|
129
130
|
return { pullApplied: pullOutcome === "applied" };
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
-
function throwIfAborted(signal?: AbortSignal) {
|
|
133
|
-
if (!signal?.aborted) return;
|
|
134
|
-
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
135
|
-
}
|
|
@@ -9,7 +9,10 @@ import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-st
|
|
|
9
9
|
import type { SyncAttentionController } from "../ui/sync-attention.js";
|
|
10
10
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
11
11
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
12
|
+
import { startupTransferEnabled } from "./agent-policy.js";
|
|
13
|
+
import { withAgentSyncLock } from "./agent-sync.js";
|
|
12
14
|
import { combineSignals, throwIfAborted } from "./signals.js";
|
|
15
|
+
import { createStartupTimings } from "./startup-timings.js";
|
|
13
16
|
import { errorMessage } from "./sync-errors.js";
|
|
14
17
|
import type { SyncLoaders } from "./sync-loaders.js";
|
|
15
18
|
|
|
@@ -41,18 +44,19 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
41
44
|
},
|
|
42
45
|
start(ctx: ExtensionContext, sessionSignal: AbortSignal, initialConfig?: AnySyncConfig) {
|
|
43
46
|
if (!ctx.hasUI || sessionSignal.aborted) return;
|
|
44
|
-
if (initialConfig
|
|
47
|
+
if (initialConfig && startupTransferEnabled(initialConfig) && !ctx.isIdle()) {
|
|
45
48
|
pending = { ctx, signal: sessionSignal, config: initialConfig };
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
if (active) throw new Error("Previous startup check has not been drained.");
|
|
52
|
+
const timings = createStartupTimings();
|
|
49
53
|
const controller = new AbortController();
|
|
50
54
|
const signal = combineSignals(sessionSignal, controller.signal);
|
|
51
55
|
const task = {
|
|
52
56
|
controller,
|
|
53
57
|
owner: ctx.sessionManager,
|
|
54
58
|
promise: Promise.resolve(),
|
|
55
|
-
transfer: Boolean(initialConfig
|
|
59
|
+
transfer: Boolean(initialConfig && startupTransferEnabled(initialConfig)),
|
|
56
60
|
};
|
|
57
61
|
active = task;
|
|
58
62
|
const isCurrent = () => !sessionSignal.aborted && active === task && task.owner === ctx.sessionManager;
|
|
@@ -62,33 +66,36 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
62
66
|
);
|
|
63
67
|
let config: AnySyncConfig | undefined;
|
|
64
68
|
let checking = false;
|
|
69
|
+
let stopped = false;
|
|
65
70
|
// Handle every detached rejection here; Pi's awaited event runner cannot catch it.
|
|
66
71
|
task.promise = Promise.resolve()
|
|
67
72
|
.then(async () => {
|
|
68
73
|
try {
|
|
69
74
|
throwIfAborted(signal);
|
|
70
75
|
config = initialConfig ?? (await loadConfigForCheck());
|
|
71
|
-
task.transfer =
|
|
76
|
+
task.transfer = startupTransferEnabled(config);
|
|
72
77
|
throwIfAborted(signal);
|
|
73
|
-
if (!config.automatic && !config
|
|
78
|
+
if (!config.automatic && !startupTransferEnabled(config)) return;
|
|
74
79
|
const captured = config;
|
|
75
80
|
const identity = syncCheckConfigFingerprint(captured);
|
|
76
81
|
checking = true;
|
|
77
82
|
setSyncStatus(ctx, "sync ...");
|
|
78
|
-
const { inspectSync } = await loaders.inspection();
|
|
83
|
+
const { inspectSync } = await timings.measure("inspection import", () => loaders.inspection());
|
|
79
84
|
throwIfAborted(signal);
|
|
80
85
|
const currentConfig = await loadConfigForCheck();
|
|
81
86
|
throwIfAborted(signal);
|
|
82
87
|
if (syncCheckConfigFingerprint(currentConfig) !== identity) return;
|
|
83
88
|
// Lock order: state migration guard → sync operation → backend cache.
|
|
84
89
|
// Read heads may write only private cache data. Never call doctor/syncBoth.
|
|
85
|
-
const inspection = await
|
|
86
|
-
|
|
87
|
-
return withLock("startup-check", () => {
|
|
90
|
+
const inspection = await timings.measure("inspection and guards", () =>
|
|
91
|
+
withStateDirectoryAccess(() => {
|
|
88
92
|
throwIfAborted(signal);
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
return withLock("startup-check", () => {
|
|
94
|
+
throwIfAborted(signal);
|
|
95
|
+
return inspectSync(captured, snapshotOptionsForContext(ctx, captured), signal, undefined, timings);
|
|
96
|
+
});
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
92
99
|
throwIfAborted(signal);
|
|
93
100
|
const latest = await loadConfigForCheck();
|
|
94
101
|
throwIfAborted(signal);
|
|
@@ -101,34 +108,39 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
101
108
|
configIdentity: identity,
|
|
102
109
|
checkedAt: new Date().toISOString(),
|
|
103
110
|
inspection,
|
|
104
|
-
automaticTransfer: captured
|
|
111
|
+
automaticTransfer: startupTransferEnabled(captured),
|
|
105
112
|
});
|
|
106
113
|
if (
|
|
107
|
-
captured
|
|
114
|
+
startupTransferEnabled(captured) &&
|
|
108
115
|
!inspection.firstSync &&
|
|
109
116
|
inspection.head &&
|
|
110
117
|
inspection.selectionState?.kind === "same"
|
|
111
118
|
) {
|
|
112
|
-
const operations = await loaders.operations();
|
|
119
|
+
const operations = await timings.measure("operations import", () => loaders.operations());
|
|
113
120
|
throwIfAborted(signal);
|
|
114
121
|
if (!isCurrent()) return;
|
|
115
122
|
if (!ctx.isIdle()) {
|
|
116
123
|
pending = { ctx, signal: sessionSignal, config: captured };
|
|
117
124
|
return;
|
|
118
125
|
}
|
|
119
|
-
await
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
await timings.measure("automatic transfer", () =>
|
|
127
|
+
withStateDirectoryAccess(() =>
|
|
128
|
+
withAgentSyncLock(
|
|
129
|
+
ctx,
|
|
130
|
+
{
|
|
131
|
+
args: [],
|
|
132
|
+
yes: true,
|
|
133
|
+
force: false,
|
|
134
|
+
stale: false,
|
|
135
|
+
silent: true,
|
|
136
|
+
reload: false,
|
|
137
|
+
auto: true,
|
|
138
|
+
signal,
|
|
139
|
+
startupTimings: timings,
|
|
140
|
+
onCommit: () => attention.clearObservation(),
|
|
141
|
+
},
|
|
142
|
+
(options) => operations.syncBoth(ctx, options),
|
|
143
|
+
),
|
|
132
144
|
),
|
|
133
145
|
);
|
|
134
146
|
throwIfAborted(signal);
|
|
@@ -141,10 +153,11 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
141
153
|
configIdentity: identity,
|
|
142
154
|
checkedAt: new Date().toISOString(),
|
|
143
155
|
inspection,
|
|
144
|
-
automaticTransfer: captured
|
|
156
|
+
automaticTransfer: startupTransferEnabled(captured),
|
|
145
157
|
});
|
|
146
158
|
if (ctx.mode === "rpc") attention.notifyObservation(ctx);
|
|
147
159
|
} catch (error) {
|
|
160
|
+
stopped = true;
|
|
148
161
|
if (!isCurrent() || sessionSignal.aborted || isMissingConfigError(error)) return;
|
|
149
162
|
if (signal.aborted && controller.signal.reason?.name !== "TimeoutError") return;
|
|
150
163
|
// A settings edit invalidates failures as well as successful observations.
|
|
@@ -154,7 +167,7 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
154
167
|
if (!latest || syncCheckConfigFingerprint(latest) !== syncCheckConfigFingerprint(config)) return;
|
|
155
168
|
}
|
|
156
169
|
ctx.ui.notify(
|
|
157
|
-
config
|
|
170
|
+
config && startupTransferEnabled(config)
|
|
158
171
|
? `pi-sync automatic transfer stopped: ${safeTerminalText(errorMessage(error))}. Run /sync to review or reconcile retained recovery evidence. No resources were reloaded.`
|
|
159
172
|
: `pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}. Run /sync status to retry. No startup transfer was performed.`,
|
|
160
173
|
"warning",
|
|
@@ -177,6 +190,15 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
177
190
|
}
|
|
178
191
|
} else setSyncStatus(ctx, undefined);
|
|
179
192
|
}
|
|
193
|
+
if (isCurrent()) {
|
|
194
|
+
timings.publish(
|
|
195
|
+
ctx,
|
|
196
|
+
config?.debug ?? false,
|
|
197
|
+
sessionSignal,
|
|
198
|
+
"background startup",
|
|
199
|
+
signal.aborted || stopped ? "stopped" : "completed",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
180
202
|
if (active === task) active = undefined;
|
|
181
203
|
}
|
|
182
204
|
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
type StartupStage =
|
|
5
|
+
| "transaction recovery"
|
|
6
|
+
| "settings read"
|
|
7
|
+
| "config resolution"
|
|
8
|
+
| "inspection import"
|
|
9
|
+
| "inspection and guards"
|
|
10
|
+
| "backend initialization"
|
|
11
|
+
| "local snapshot"
|
|
12
|
+
| "sync state read"
|
|
13
|
+
| "remote head"
|
|
14
|
+
| "operations import"
|
|
15
|
+
| "automatic transfer"
|
|
16
|
+
| "transfer authorization"
|
|
17
|
+
| "transfer backend initialization"
|
|
18
|
+
| "transfer journal read"
|
|
19
|
+
| "transfer journal recovery"
|
|
20
|
+
| "transfer state read"
|
|
21
|
+
| "transfer local snapshot"
|
|
22
|
+
| "transfer remote head"
|
|
23
|
+
| "transfer snapshot download"
|
|
24
|
+
| "transfer unchanged verification"
|
|
25
|
+
| "transfer merge planning"
|
|
26
|
+
| "transfer settings merge"
|
|
27
|
+
| "transfer content merge"
|
|
28
|
+
| "transfer preflight"
|
|
29
|
+
| "transfer backup"
|
|
30
|
+
| "transfer journal write"
|
|
31
|
+
| "transfer remote publication"
|
|
32
|
+
| "transfer local apply"
|
|
33
|
+
| "transfer baseline write"
|
|
34
|
+
| "transfer cleanup";
|
|
35
|
+
|
|
36
|
+
/** Fixed labels and durations only: never retain errors, paths, settings, or payloads. */
|
|
37
|
+
export function createStartupTimings() {
|
|
38
|
+
const started = performance.now();
|
|
39
|
+
const stages = new Map<StartupStage, { ms: number; calls: number; interrupted: boolean }>();
|
|
40
|
+
let unchanged = false;
|
|
41
|
+
return {
|
|
42
|
+
markUnchanged() {
|
|
43
|
+
unchanged = true;
|
|
44
|
+
},
|
|
45
|
+
async measure<T>(stage: StartupStage, run: () => T | Promise<T>): Promise<T> {
|
|
46
|
+
const start = performance.now();
|
|
47
|
+
let completed = false;
|
|
48
|
+
try {
|
|
49
|
+
const result = await run();
|
|
50
|
+
completed = true;
|
|
51
|
+
return result;
|
|
52
|
+
} finally {
|
|
53
|
+
const previous = stages.get(stage);
|
|
54
|
+
stages.set(stage, {
|
|
55
|
+
ms: (previous?.ms ?? 0) + performance.now() - start,
|
|
56
|
+
calls: (previous?.calls ?? 0) + 1,
|
|
57
|
+
interrupted: Boolean(previous?.interrupted || !completed),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
publish(
|
|
62
|
+
ctx: ExtensionContext,
|
|
63
|
+
enabled: boolean,
|
|
64
|
+
sessionSignal: AbortSignal,
|
|
65
|
+
scope: "local initialization" | "background startup",
|
|
66
|
+
outcome: "completed" | "stopped" = "completed",
|
|
67
|
+
) {
|
|
68
|
+
if (!enabled || !ctx.hasUI || sessionSignal.aborted) return;
|
|
69
|
+
try {
|
|
70
|
+
ctx.ui.notify(
|
|
71
|
+
[
|
|
72
|
+
`pi-sync debug: ${scope} (${outcome})`,
|
|
73
|
+
...Array.from(
|
|
74
|
+
stages,
|
|
75
|
+
([stage, value]) =>
|
|
76
|
+
`${stage}: ${value.ms.toFixed(1)} ms${value.calls > 1 ? ` (${value.calls} calls)` : ""}${value.interrupted ? " (interrupted)" : ""}`,
|
|
77
|
+
),
|
|
78
|
+
...(unchanged ? ["transfer result: unchanged (fast path)"] : []),
|
|
79
|
+
`total: ${(performance.now() - started).toFixed(1)} ms`,
|
|
80
|
+
].join("\n"),
|
|
81
|
+
"info",
|
|
82
|
+
);
|
|
83
|
+
} catch {
|
|
84
|
+
// Diagnostics must not change sync behavior if the UI is unavailable during teardown.
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type StartupTimings = ReturnType<typeof createStartupTimings>;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { createSyncBackend, type SyncBackendFactory } from "../backends/backend-factory.js";
|
|
2
3
|
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
3
4
|
import { createSnapshot } from "../snapshot/snapshot.js";
|
|
4
5
|
import type { SnapshotOptions } from "../snapshot/snapshot-types.js";
|
|
5
6
|
import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-store.js";
|
|
6
7
|
import { throwIfAborted } from "./signals.js";
|
|
8
|
+
import { createStartupTimings, type StartupTimings } from "./startup-timings.js";
|
|
7
9
|
import { inspectRemoteSelection } from "./sync-policy.js";
|
|
8
|
-
import { hasLocalChanges, remoteChangedSinceState } from "./sync-state.js";
|
|
10
|
+
import { fileHashMap, hasLocalChanges, remoteChangedSinceState } from "./sync-state.js";
|
|
9
11
|
|
|
10
12
|
/** Advisory baseline-relative observation, never an authorization to transfer files. */
|
|
11
13
|
export async function inspectSync(
|
|
@@ -13,17 +15,26 @@ export async function inspectSync(
|
|
|
13
15
|
options: SnapshotOptions,
|
|
14
16
|
signal?: AbortSignal,
|
|
15
17
|
factory: SyncBackendFactory = createSyncBackend,
|
|
18
|
+
timings: StartupTimings = createStartupTimings(),
|
|
16
19
|
) {
|
|
17
20
|
throwIfAborted(signal);
|
|
18
|
-
const backend = await factory(config);
|
|
21
|
+
const backend = await timings.measure("backend initialization", () => factory(config));
|
|
19
22
|
throwIfAborted(signal);
|
|
20
|
-
const local = await
|
|
23
|
+
const local = await timings.measure("local snapshot", () =>
|
|
24
|
+
createSnapshot(config.snapshotIdentity, { ...options, signal }),
|
|
25
|
+
);
|
|
21
26
|
throwIfAborted(signal);
|
|
22
|
-
const state = await readStateForConfig(config);
|
|
27
|
+
const state = await timings.measure("sync state read", () => readStateForConfig(config));
|
|
23
28
|
throwIfAborted(signal);
|
|
24
|
-
const head = await backend.readHead(signal);
|
|
29
|
+
const head = await timings.measure("remote head", () => backend.readHead(signal));
|
|
25
30
|
throwIfAborted(signal);
|
|
31
|
+
const freshness: { localIdentity?: string } = {
|
|
32
|
+
localIdentity: createHash("sha256")
|
|
33
|
+
.update(JSON.stringify(fileHashMap(local)))
|
|
34
|
+
.digest("hex"),
|
|
35
|
+
};
|
|
26
36
|
return {
|
|
37
|
+
...freshness,
|
|
27
38
|
head,
|
|
28
39
|
selectionState: head ? inspectRemoteSelection(config.include, { selection: head.selection, files: [] }) : undefined,
|
|
29
40
|
localFiles: local.files.length,
|
|
@@ -23,7 +23,7 @@ import { applySnapshot } from "../snapshot/snapshot-apply.js";
|
|
|
23
23
|
import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
24
24
|
import { pruneMergeBaselines } from "../state/merge-baseline-store.js";
|
|
25
25
|
import type { SyncState } from "../state/state-types.js";
|
|
26
|
-
import { readStateForConfig, writeStateForConfig } from "../state/sync-state-store.js";
|
|
26
|
+
import { readStateForConfig, syncStateFingerprint, writeStateForConfig } from "../state/sync-state-store.js";
|
|
27
27
|
import {
|
|
28
28
|
countPreservedRemoteFiles,
|
|
29
29
|
formatPullSummary,
|
|
@@ -31,6 +31,9 @@ import {
|
|
|
31
31
|
formatRollbackSummary,
|
|
32
32
|
} from "../ui/sync-format.js";
|
|
33
33
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
34
|
+
import { confirmTransferReview } from "../ui/transfer-review.js";
|
|
35
|
+
import { startupTransferEnabled } from "./agent-policy.js";
|
|
36
|
+
import { inspectCaseCleanup, revalidateCaseCleanup } from "./apply-review.js";
|
|
34
37
|
import { confirmFieldMigration } from "./field-migration.js";
|
|
35
38
|
import { overlayLocalFields, portableSnapshot, sameLocalFields, validatePortableSnapshot } from "./local-fields.js";
|
|
36
39
|
import { readMergeJournal, requireNoMergeJournal, retireMergeJournal } from "./merge-journal.js";
|
|
@@ -155,7 +158,7 @@ export async function push(
|
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
if (!remoteForUpload && head && config.localFields !== undefined)
|
|
161
|
+
if (!remoteForUpload && head && (config.localFields !== undefined || !options.yes))
|
|
159
162
|
remoteForUpload = await readSnapshotForHead(backend, head, options.signal);
|
|
160
163
|
if (!(await confirmFieldMigration(ctx, config, state, remoteForUpload, options.force, options.signal))) {
|
|
161
164
|
setSyncStatus(ctx, undefined);
|
|
@@ -208,12 +211,15 @@ export async function push(
|
|
|
208
211
|
if (configIdentity !== syncCheckConfigFingerprint(await loadConfig(options.setup)))
|
|
209
212
|
throw new Error("Settings changed during push review.");
|
|
210
213
|
validateMutation();
|
|
211
|
-
if (config.localFields !== undefined || (await readMergeJournal(config))) {
|
|
214
|
+
if (!options.yes || config.localFields !== undefined || (await readMergeJournal(config))) {
|
|
212
215
|
const current = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
213
216
|
if (!sameHashes(fileHashMap(current), fileHashMap(localRaw)))
|
|
214
|
-
throw new Error("Local content changed during
|
|
217
|
+
throw new Error("Local content changed during push review; retry from current bytes.");
|
|
215
218
|
}
|
|
216
|
-
|
|
219
|
+
const currentState = await readStateForConfig(config);
|
|
220
|
+
if (!options.yes && syncStateFingerprint(currentState) !== syncStateFingerprint(state))
|
|
221
|
+
throw new Error("Baseline changed during push review.");
|
|
222
|
+
requireResolvedDirectionalProgress(currentState, options);
|
|
217
223
|
validateMutation();
|
|
218
224
|
const result = await backend.publishSnapshot(upload, expectedRemoteHead(head), {
|
|
219
225
|
signal: options.signal,
|
|
@@ -316,11 +322,23 @@ export async function pull(
|
|
|
316
322
|
});
|
|
317
323
|
}
|
|
318
324
|
|
|
325
|
+
const caseCleanup = options.yes ? [] : await inspectCaseCleanup(physical, options.signal);
|
|
326
|
+
validateMutation();
|
|
319
327
|
if (
|
|
320
328
|
!options.yes &&
|
|
321
|
-
!(await
|
|
329
|
+
!(await confirmTransferReview(
|
|
330
|
+
ctx,
|
|
322
331
|
snapshotIncludesSessions(remote) ? "Pull pi settings and sessions?" : "Pull pi settings?",
|
|
323
|
-
formatPullSummary(
|
|
332
|
+
formatPullSummary(
|
|
333
|
+
config,
|
|
334
|
+
backend.destination,
|
|
335
|
+
local,
|
|
336
|
+
physical,
|
|
337
|
+
protectedSessionPaths(ctx),
|
|
338
|
+
caseCleanup.map((item) => item.path),
|
|
339
|
+
),
|
|
340
|
+
options.signal,
|
|
341
|
+
validateMutation,
|
|
324
342
|
))
|
|
325
343
|
) {
|
|
326
344
|
setSyncStatus(ctx, undefined);
|
|
@@ -353,7 +371,11 @@ export async function pull(
|
|
|
353
371
|
const backup = await backupLocal(config.snapshotIdentity, snapshotOptionsForContext(ctx, config), options.signal);
|
|
354
372
|
const applySessionDir = await sessionDirForApply(ctx, physical);
|
|
355
373
|
if (recoveringMerge) await requireFreshRecovery();
|
|
356
|
-
|
|
374
|
+
const currentState = await readStateForConfig(config);
|
|
375
|
+
if (!options.yes && syncStateFingerprint(currentState) !== syncStateFingerprint(state))
|
|
376
|
+
throw new Error("Baseline changed during pull review.");
|
|
377
|
+
requireResolvedDirectionalProgress(currentState, options);
|
|
378
|
+
if (!options.yes) await revalidateCaseCleanup(caseCleanup, physical, options.signal);
|
|
357
379
|
validateMutation();
|
|
358
380
|
options.onCommit?.();
|
|
359
381
|
const lastFileHashes = await applySnapshot(physical, protectedSessionPaths(ctx), {
|
|
@@ -408,7 +430,9 @@ export async function syncBoth(
|
|
|
408
430
|
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
409
431
|
const config = await loadConfig(options.setup);
|
|
410
432
|
throwIfAborted(options.signal);
|
|
411
|
-
|
|
433
|
+
options.background?.validate(config);
|
|
434
|
+
if (config.include.length > 0 && options.auto && startupTransferEnabled(config))
|
|
435
|
+
return mergeSync(ctx, options, factory);
|
|
412
436
|
if (!options.auto && config.include.length > 0 && (await readMergeJournal(config)))
|
|
413
437
|
return mergeSync(ctx, options, factory);
|
|
414
438
|
const state = await readStateForConfig(config);
|
|
@@ -430,15 +454,13 @@ export async function syncBoth(
|
|
|
430
454
|
throwIfAborted(options.signal);
|
|
431
455
|
const localChanged = hasLocalChanges(local, state, config);
|
|
432
456
|
const remoteChanged = remote ? hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx)) : false;
|
|
433
|
-
const firstSync = !state.lastAppliedSnapshot;
|
|
434
457
|
if (options.auto) throw new Error("Automatic transfer is not authorized by the current settings.");
|
|
435
458
|
|
|
459
|
+
// Established baselines return through mergeSync above; only initial-source selection remains.
|
|
436
460
|
const portableLocal = portableSnapshot(local, config.localFields);
|
|
437
461
|
const acceptMatchingState = async (snapshot: Snapshot) => {
|
|
438
462
|
requireCompatibleRemoteSelection(config, snapshot);
|
|
439
463
|
const accepted = portableSnapshot(snapshot, config.localFields);
|
|
440
|
-
if (state.lastAppliedSnapshot && !sameLocalFields(state.localFields, config.localFields))
|
|
441
|
-
throw new Error("Local-field rules changed; review and confirm a directional migration first.");
|
|
442
464
|
await writeStateForConfig(
|
|
443
465
|
config,
|
|
444
466
|
{
|
|
@@ -456,7 +478,7 @@ export async function syncBoth(
|
|
|
456
478
|
await pruneMergeBaselines(config, await readStateForConfig(config), validateMutation);
|
|
457
479
|
validateMutation();
|
|
458
480
|
};
|
|
459
|
-
if (
|
|
481
|
+
if (remote && remote.files.length > 0 && local.files.length > 0) {
|
|
460
482
|
requireCompatibleRemoteSelection(config, remote);
|
|
461
483
|
const portableRemote = portableSnapshot(remote, config.localFields);
|
|
462
484
|
if (!canPullRemoteSettingsOnFirstSync(portableLocal, portableRemote)) {
|
|
@@ -504,18 +526,6 @@ export async function syncBoth(
|
|
|
504
526
|
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
505
527
|
return;
|
|
506
528
|
}
|
|
507
|
-
if (localChanged && remoteChanged && state.lastAppliedSnapshot) {
|
|
508
|
-
throw createSyncDecision({
|
|
509
|
-
kind: "both-changed",
|
|
510
|
-
config,
|
|
511
|
-
state,
|
|
512
|
-
local,
|
|
513
|
-
remote,
|
|
514
|
-
localChanged,
|
|
515
|
-
remoteChanged,
|
|
516
|
-
directMessage: "Both local and remote changed. Run /sync diff and resolve with push --force or pull --force.",
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
529
|
if (remoteChanged) {
|
|
520
530
|
await pull(ctx, options, factory);
|
|
521
531
|
return;
|
|
@@ -573,11 +583,21 @@ export async function rollback(
|
|
|
573
583
|
if (!(await confirmFieldMigration(ctx, config, state, currentRemote, true, options.signal))) return;
|
|
574
584
|
validateMutation();
|
|
575
585
|
const physical = overlayLocalFields(remote, local, config.localFields);
|
|
586
|
+
const caseCleanup = options.yes ? [] : await inspectCaseCleanup(physical, options.signal);
|
|
587
|
+
validateMutation();
|
|
576
588
|
|
|
577
589
|
if (!options.yes) {
|
|
578
590
|
const confirmed = await ctx.ui.confirm(
|
|
579
591
|
snapshotIncludesSessions(remote) ? "Rollback pi settings and sessions?" : "Rollback pi settings?",
|
|
580
|
-
formatRollbackSummary(
|
|
592
|
+
formatRollbackSummary(
|
|
593
|
+
config,
|
|
594
|
+
backend.destination,
|
|
595
|
+
local,
|
|
596
|
+
physical,
|
|
597
|
+
target,
|
|
598
|
+
protectedSessionPaths(ctx),
|
|
599
|
+
caseCleanup.map((item) => item.path),
|
|
600
|
+
),
|
|
581
601
|
{ signal: options.signal },
|
|
582
602
|
);
|
|
583
603
|
validateMutation();
|
|
@@ -599,7 +619,11 @@ export async function rollback(
|
|
|
599
619
|
throwIfAborted(options.signal);
|
|
600
620
|
const backup = await backupLocal(config.snapshotIdentity, snapshotOptionsForContext(ctx, config), options.signal);
|
|
601
621
|
const applySessionDir = await sessionDirForApply(ctx, remote);
|
|
602
|
-
|
|
622
|
+
const currentState = await readStateForConfig(config);
|
|
623
|
+
if (!options.yes && syncStateFingerprint(currentState) !== syncStateFingerprint(state))
|
|
624
|
+
throw new Error("Baseline changed during rollback review.");
|
|
625
|
+
requireResolvedDirectionalProgress(currentState, options, false);
|
|
626
|
+
if (!options.yes) await revalidateCaseCleanup(caseCleanup, physical, options.signal);
|
|
603
627
|
validateMutation();
|
|
604
628
|
options.onCommit?.();
|
|
605
629
|
const lastFileHashes = await applySnapshot(physical, protectedSessionPaths(ctx), {
|
|
@@ -724,9 +748,12 @@ async function confirmPush(
|
|
|
724
748
|
) {
|
|
725
749
|
throwIfAborted(options.signal);
|
|
726
750
|
if (options.yes) return true;
|
|
727
|
-
const confirmed = await
|
|
751
|
+
const confirmed = await confirmTransferReview(
|
|
752
|
+
ctx,
|
|
728
753
|
title,
|
|
729
754
|
formatPushSummary(config, backend.destination, upload, head, countPreservedRemoteFiles(local, upload), remote),
|
|
755
|
+
options.signal,
|
|
756
|
+
captureMutationOwner(ctx, options.signal),
|
|
730
757
|
);
|
|
731
758
|
throwIfAborted(options.signal);
|
|
732
759
|
if (confirmed) return true;
|
package/src/sync/sync-queries.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { snapshotOptionsForContext } from "../snapshot/session-paths.js";
|
|
|
7
7
|
import { createSnapshot, scanSnapshot } from "../snapshot/snapshot.js";
|
|
8
8
|
import type { SnapshotOptions } from "../snapshot/snapshot-types.js";
|
|
9
9
|
import { inspectLock, isLockGuardHeld, isStaleLock, withLock } from "../state/lock.js";
|
|
10
|
+
import { readStateForConfig } from "../state/sync-state-store.js";
|
|
10
11
|
import { formatDiff, formatSnapshotOnlyDiff, publicationCapabilityDescription } from "../ui/sync-format.js";
|
|
11
12
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
12
13
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
@@ -35,6 +36,10 @@ export async function status(
|
|
|
35
36
|
);
|
|
36
37
|
throwIfAborted(options.signal);
|
|
37
38
|
|
|
39
|
+
const continuous = config.continuous
|
|
40
|
+
? await (await import("./continuous-record.js")).continuousStatusLines(config)
|
|
41
|
+
: [];
|
|
42
|
+
throwIfAborted(options.signal);
|
|
38
43
|
const remoteText = head ? `remote: ${head.snapshotId} from ${head.machine} at ${head.createdAt}` : "remote: empty";
|
|
39
44
|
const warnings = syncSessionsWarnings(config);
|
|
40
45
|
setSyncStatus(ctx, undefined);
|
|
@@ -46,6 +51,7 @@ export async function status(
|
|
|
46
51
|
`publication safety: ${publicationCapabilityDescription(capability)}`,
|
|
47
52
|
`included content: ${config.include.join(", ") || "none"}`,
|
|
48
53
|
`sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
|
|
54
|
+
...continuous,
|
|
49
55
|
remoteText,
|
|
50
56
|
formatRemoteSelectionStatus(selectionState),
|
|
51
57
|
`local files: ${localFiles}`,
|
|
@@ -185,8 +191,15 @@ export async function history(
|
|
|
185
191
|
const backend = await factory(config);
|
|
186
192
|
const snapshots = (await backend.listHistory(options.signal)).slice(-20).reverse();
|
|
187
193
|
throwIfAborted(options.signal);
|
|
194
|
+
const lastApplied = await readStateForConfig(config)
|
|
195
|
+
.then((state) => state.lastAppliedSnapshot)
|
|
196
|
+
.catch(() => undefined);
|
|
197
|
+
throwIfAborted(options.signal);
|
|
198
|
+
const appliedDetails = lastApplied
|
|
199
|
+
? `Last applied snapshot ID: ${safeTerminalText(lastApplied)}\nApplication time: unavailable. Snapshot dates are creation times.`
|
|
200
|
+
: "Last applied snapshot ID: unavailable.";
|
|
188
201
|
if (snapshots.length === 0) {
|
|
189
|
-
ctx.ui.notify(
|
|
202
|
+
ctx.ui.notify(`No remote pi-sync history found.\n${appliedDetails}`, "info");
|
|
190
203
|
return;
|
|
191
204
|
}
|
|
192
205
|
|
|
@@ -197,7 +210,7 @@ export async function history(
|
|
|
197
210
|
`${index + 1}. ${item.createdAt} · ${safeTerminalText(item.machine)} · ${item.snapshotId}${item.snapshotId === currentSnapshot ? " (current)" : ""}${item.syncSessions ? " · sessions" : ""}`,
|
|
198
211
|
);
|
|
199
212
|
const selected = await ctx.ui.select(
|
|
200
|
-
`History for sync setup “${safeTerminalText(config.setupName)}”\n\nChoose a snapshot to preview rollback.`,
|
|
213
|
+
`History for sync setup “${safeTerminalText(config.setupName)}”\n\n${appliedDetails}\n\nChoose a snapshot to preview rollback.`,
|
|
201
214
|
[...labels, "Back"],
|
|
202
215
|
{ signal: options.signal },
|
|
203
216
|
);
|
|
@@ -215,7 +228,10 @@ export async function history(
|
|
|
215
228
|
return;
|
|
216
229
|
}
|
|
217
230
|
ctx.ui.notify(
|
|
218
|
-
|
|
231
|
+
[
|
|
232
|
+
appliedDetails,
|
|
233
|
+
...snapshots.map((item) => `${item.snapshotRef} ${item.createdAt} ${safeTerminalText(item.machine)}`),
|
|
234
|
+
].join("\n"),
|
|
219
235
|
"info",
|
|
220
236
|
);
|
|
221
237
|
}
|
package/src/sync/text-merge.ts
CHANGED
|
@@ -30,11 +30,11 @@ export function text(bytes: Buffer) {
|
|
|
30
30
|
throw new Error("Unsupported text encoding.");
|
|
31
31
|
return decoded;
|
|
32
32
|
}
|
|
33
|
-
type Hunk = { start: number; end: number; lines: string[] };
|
|
34
|
-
function lines(value: string) {
|
|
33
|
+
export type Hunk = { start: number; end: number; lines: string[] };
|
|
34
|
+
export function lines(value: string) {
|
|
35
35
|
return value.match(/[^\n]*\n|[^\n]+$/gu) ?? [];
|
|
36
36
|
}
|
|
37
|
-
function edits(base: string[], next: string[], signal?: AbortSignal): Hunk[] {
|
|
37
|
+
export function edits(base: string[], next: string[], signal?: AbortSignal): Hunk[] {
|
|
38
38
|
const width = next.length + 1;
|
|
39
39
|
if ((base.length + 1) * width > 4_000_000) throw new Error("Text merge complexity bound exceeded.");
|
|
40
40
|
// Intern once so each charged DP cell compares fixed-size IDs, not long line strings.
|