@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
|
@@ -5,6 +5,7 @@ import { localConfigTemplate, readLocalConfigObject } from "../settings/settings
|
|
|
5
5
|
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
6
6
|
import { unlock, withLock } from "../state/lock.js";
|
|
7
7
|
import { migrateLegacyStateDirectory, stateDirectoryMigrationNotice } from "../state/state-directory.js";
|
|
8
|
+
import { withAgentSyncLock } from "../sync/agent-sync.js";
|
|
8
9
|
import { throwIfAborted } from "../sync/signals.js";
|
|
9
10
|
import { syncErrorGuidance } from "../sync/sync-error-guidance.js";
|
|
10
11
|
import { isSyncDecisionRequiredError, SetupPullRequiresUiError } from "../sync/sync-errors.js";
|
|
@@ -13,6 +14,7 @@ import { RemoteSelectionMismatchError } from "../sync/sync-policy.js";
|
|
|
13
14
|
import { automaticSyncSummary } from "../ui/automatic-sync-summary.js";
|
|
14
15
|
import type { RunRouteResult } from "../ui/cancellable-operation.js";
|
|
15
16
|
import { trackOperationDialogs } from "../ui/operation-dialogs.js";
|
|
17
|
+
import { startupTransferSummary } from "../ui/sync-behavior.js";
|
|
16
18
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
17
19
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
18
20
|
import { parseOptions, resolveSyncCommand, splitArgs, usage, validateCommandOptions } from "./command.js";
|
|
@@ -48,7 +50,7 @@ export async function executeRecoveryCommand(
|
|
|
48
50
|
const outcome = await withLock("pull", () => operations.pull(ctx, options));
|
|
49
51
|
return { kind: "completed", ...(outcome ? { outcome } : {}) };
|
|
50
52
|
}
|
|
51
|
-
await
|
|
53
|
+
await withAgentSyncLock(ctx, options, (current) => operations.syncBoth(ctx, current));
|
|
52
54
|
return { kind: "completed" };
|
|
53
55
|
} catch (error) {
|
|
54
56
|
if (signal?.aborted) return { kind: "failed" };
|
|
@@ -155,7 +157,7 @@ export async function executeCommand(
|
|
|
155
157
|
case "sync": {
|
|
156
158
|
const operations = await loaders.operations();
|
|
157
159
|
throwIfAborted(options.signal);
|
|
158
|
-
await
|
|
160
|
+
await withAgentSyncLock(ctx, options, (current) => operations.syncBoth(ctx, current));
|
|
159
161
|
return { kind: "completed" };
|
|
160
162
|
}
|
|
161
163
|
case "history": {
|
|
@@ -256,7 +258,9 @@ async function showConfig(ctx: ExtensionCommandContext, options: CommandOptions)
|
|
|
256
258
|
`automatic sync: ${automaticSyncSummary(config.automatic)}`,
|
|
257
259
|
`settings field merge: ${config.mergeSettings ? "On (experimental)" : "Off"}`,
|
|
258
260
|
`machine-local settings.json fields: ${(config.localFields ?? []).map(safeTerminalText).join(", ") || "none"}`,
|
|
259
|
-
`
|
|
261
|
+
`startup transfer: ${startupTransferSummary(config)}`,
|
|
262
|
+
`agent mode: ${config.agentMode ? "On (experimental)" : "Off"}`,
|
|
263
|
+
`continuous sync: ${config.continuous ? (config.agentMode ? "On (experimental)" : "paused; Agent mode is Off") : "Off"}`,
|
|
260
264
|
`included content: ${config.include.join(", ") || "none"}`,
|
|
261
265
|
`sessions: ${config.include.includes("sessions") ? "included" : "not included"}`,
|
|
262
266
|
`settings file: ${localConfigPath()}`,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
syncAttentionMatchesConfig,
|
|
14
14
|
} from "../ui/sync-attention.js";
|
|
15
15
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
16
|
-
import { parseOptions, splitArgs } from "./command.js";
|
|
16
|
+
import { parseOptions, splitArgs, validateCommandOptions } from "./command.js";
|
|
17
17
|
import { executeCommand, executeRecoveryCommand } from "./command-execution.js";
|
|
18
18
|
|
|
19
19
|
export async function handleCommand(
|
|
@@ -22,6 +22,7 @@ export async function handleCommand(
|
|
|
22
22
|
sessionSignal: AbortSignal,
|
|
23
23
|
loaders: SyncLoaders,
|
|
24
24
|
attention: SyncAttentionController,
|
|
25
|
+
onExplicitSync?: (setup?: string) => void,
|
|
25
26
|
) {
|
|
26
27
|
await reconcileObservation(attention, sessionSignal);
|
|
27
28
|
if (sessionSignal.aborted) return;
|
|
@@ -31,8 +32,18 @@ export async function handleCommand(
|
|
|
31
32
|
onCommit?: () => void,
|
|
32
33
|
target?: string,
|
|
33
34
|
onDialog?: (active: boolean) => void,
|
|
34
|
-
) =>
|
|
35
|
-
|
|
35
|
+
) => {
|
|
36
|
+
const [command, ...args] = splitArgs(route);
|
|
37
|
+
if (command === "sync") {
|
|
38
|
+
try {
|
|
39
|
+
const options = parseOptions(args);
|
|
40
|
+
validateCommandOptions("sync", options);
|
|
41
|
+
onExplicitSync?.(target ?? options.setup);
|
|
42
|
+
} catch {
|
|
43
|
+
/* Invalid routes do not request an automatic retry. */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return executeCommand(
|
|
36
47
|
route,
|
|
37
48
|
ctx,
|
|
38
49
|
combineSignals(sessionSignal, signal),
|
|
@@ -41,6 +52,7 @@ export async function handleCommand(
|
|
|
41
52
|
target,
|
|
42
53
|
onDialog,
|
|
43
54
|
);
|
|
55
|
+
};
|
|
44
56
|
if (!rawArgs.trim()) {
|
|
45
57
|
try {
|
|
46
58
|
const { showSyncManager } = await import("../ui/manager-ui.js");
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
2
|
+
import type { AgentAcceptance } from "../sync/agent-request.js";
|
|
3
|
+
import type { StartupTimings } from "../sync/startup-timings.js";
|
|
4
|
+
|
|
1
5
|
export interface CommandOptions {
|
|
2
6
|
yes: boolean;
|
|
3
7
|
force: boolean;
|
|
@@ -8,6 +12,12 @@ export interface CommandOptions {
|
|
|
8
12
|
setup?: string;
|
|
9
13
|
signal?: AbortSignal;
|
|
10
14
|
onCommit?: () => void;
|
|
15
|
+
/** In-memory startup diagnostics only; never persisted or sent to a backend. */
|
|
16
|
+
startupTimings?: StartupTimings;
|
|
17
|
+
/** Internal validated resolver result; never parsed from command arguments. */
|
|
18
|
+
agentAcceptance?: AgentAcceptance;
|
|
19
|
+
/** Internal cycle authorization, ownership and durable request reservation; never command arguments. */
|
|
20
|
+
background?: { validate(config?: AnySyncConfig): void; reserve(key: string): Promise<void> };
|
|
11
21
|
args: string[];
|
|
12
22
|
}
|
|
13
23
|
|
package/src/commands/command.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { AUTOMATIC_SYNC_DESCRIPTION, STARTUP_TRANSFER_DESCRIPTION } from "../ui/sync-behavior.js";
|
|
2
3
|
import type { CommandArgumentCompletion, CommandOptions } from "./command-types.js";
|
|
3
4
|
|
|
4
5
|
const YES_FLAG_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
@@ -232,8 +233,10 @@ export function usage() {
|
|
|
232
233
|
return [
|
|
233
234
|
"Usage: /sync <command>",
|
|
234
235
|
`Commands: ${commands}`,
|
|
235
|
-
|
|
236
|
+
`Automatic sync: ${AUTOMATIC_SYNC_DESCRIPTION}`,
|
|
237
|
+
`Startup transfer: ${STARTUP_TRANSFER_DESCRIPTION}`,
|
|
236
238
|
"Automatic transfer at startup is a separate opt-in: one conflict-free established-baseline transfer at idle in TUI/RPC, with conditional/lease publication and no resource reload.",
|
|
239
|
+
"Experimental Continuous sync is a separate default-Off opt-in in Settings. It requires Agent mode, monitors while Pi is open, and can incur recurring network/model costs. Safety blocks and durable automatic request limits remain.",
|
|
237
240
|
"Settings: use /sync init or edit storage connections and sync setups in ~/.pi/agent/pi-sync.json (or the configured Pi agent directory). Version 1 and version 2 settings are unsupported and are never rewritten.",
|
|
238
241
|
].join("\n");
|
|
239
242
|
}
|
package/src/settings/config.ts
CHANGED
|
@@ -52,15 +52,18 @@ function configFromSettings(settings: PiSyncSettingsV3, setupName?: string): Any
|
|
|
52
52
|
`Invalid pi-sync settings: sync setup “${selectedName}” references missing storage connection “${connectionName}”.`,
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
return {
|
|
56
|
+
...resolveSyncConfig(
|
|
57
|
+
selectedName,
|
|
58
|
+
setup,
|
|
59
|
+
connectionName,
|
|
60
|
+
connection,
|
|
61
|
+
settings.onSwitch,
|
|
62
|
+
settings.skipSecretScan ?? false,
|
|
63
|
+
settings.showStatus ?? true,
|
|
64
|
+
),
|
|
65
|
+
debug: settings.debug ?? false,
|
|
66
|
+
};
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
export type SyncSetupStorageReview = Pick<
|
|
@@ -77,6 +80,8 @@ export async function loadPartialConfig(setupName?: string): Promise<PartialConf
|
|
|
77
80
|
include: [...config.include],
|
|
78
81
|
automatic: config.automatic,
|
|
79
82
|
automaticTransfer: config.automaticTransfer ?? false,
|
|
83
|
+
agentMode: config.agentMode ?? false,
|
|
84
|
+
continuous: config.continuous ?? false,
|
|
80
85
|
onSwitch: config.onSwitch,
|
|
81
86
|
showStatus: config.showStatus,
|
|
82
87
|
};
|
|
@@ -112,10 +117,13 @@ export function syncConfigReviewIdentity(config: AnySyncConfig) {
|
|
|
112
117
|
config.include,
|
|
113
118
|
config.automatic,
|
|
114
119
|
config.automaticTransfer ?? false,
|
|
120
|
+
config.agentMode ?? false,
|
|
115
121
|
config.localFields,
|
|
116
122
|
config.mergeSettings ?? false,
|
|
117
123
|
config.mergeContent ?? false,
|
|
118
124
|
config.partialSync ?? false,
|
|
125
|
+
// Preserve existing Off identities and cached candidates across the upgrade.
|
|
126
|
+
...(config.continuous ? ["continuous"] : []),
|
|
119
127
|
]);
|
|
120
128
|
}
|
|
121
129
|
|
|
@@ -126,7 +134,7 @@ export function syncConfigReviewFingerprint(config: AnySyncConfig) {
|
|
|
126
134
|
/** Internal freshness token, including credentials; never display or persist it. */
|
|
127
135
|
export function syncCheckConfigFingerprint(config: AnySyncConfig) {
|
|
128
136
|
return createHash("sha256")
|
|
129
|
-
.update(JSON.stringify({ ...config, showStatus: undefined }))
|
|
137
|
+
.update(JSON.stringify({ ...config, showStatus: undefined, debug: undefined }))
|
|
130
138
|
.digest("hex");
|
|
131
139
|
}
|
|
132
140
|
|
|
@@ -163,6 +171,8 @@ function resolveSyncConfig(
|
|
|
163
171
|
include,
|
|
164
172
|
automatic: setup.sync.automatic,
|
|
165
173
|
automaticTransfer: setup.sync.automaticTransfer ?? false,
|
|
174
|
+
agentMode: setup.sync.agentMode ?? false,
|
|
175
|
+
continuous: setup.sync.continuous ?? false,
|
|
166
176
|
localFields: setup.sync.localFields === undefined ? undefined : normalizeLocalFields(setup.sync.localFields),
|
|
167
177
|
mergeSettings: setup.sync.mergeSettings ?? false,
|
|
168
178
|
mergeContent: setup.sync.mergeContent ?? false,
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
1
|
import { localConfigPath, readMigratingLocalConfigDocument, updateLocalConfigDocument } from "./config-file.js";
|
|
5
2
|
import type { OnSwitchAction, PiSyncSettingsV3 } from "./settings-types.js";
|
|
6
3
|
import { DEFAULT_ON_SWITCH, validateSettingsDocument } from "./settings-validation.js";
|
|
@@ -30,6 +27,7 @@ export function localConfigTemplate(): PiSyncSettingsV3 {
|
|
|
30
27
|
onSwitch: DEFAULT_ON_SWITCH,
|
|
31
28
|
skipSecretScan: false,
|
|
32
29
|
showStatus: true,
|
|
30
|
+
debug: false,
|
|
33
31
|
storageConnections: {},
|
|
34
32
|
syncSetups: {},
|
|
35
33
|
};
|
|
@@ -62,34 +60,3 @@ export function updateLocalConfig(update: (current: PiSyncSettingsV3) => PiSyncS
|
|
|
62
60
|
async function performLocalConfigUpdate(update: (current: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
|
|
63
61
|
return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
|
|
64
62
|
}
|
|
65
|
-
|
|
66
|
-
export async function writeLocalConfigObject(value: PiSyncSettingsV3 | Record<string, unknown>) {
|
|
67
|
-
validateSettingsDocument(value as Record<string, unknown>);
|
|
68
|
-
const configPath = localConfigPath();
|
|
69
|
-
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
70
|
-
try {
|
|
71
|
-
const stat = await fs.lstat(configPath);
|
|
72
|
-
if (stat.isSymbolicLink()) throw new Error(`Refusing to overwrite symlinked pi-sync settings: ${configPath}`);
|
|
73
|
-
if (!stat.isFile()) throw new Error(`pi-sync settings are not a regular file: ${configPath}`);
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
76
|
-
}
|
|
77
|
-
const temporaryPath = path.join(
|
|
78
|
-
path.dirname(configPath),
|
|
79
|
-
`.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`,
|
|
80
|
-
);
|
|
81
|
-
let handle: fs.FileHandle | undefined;
|
|
82
|
-
try {
|
|
83
|
-
handle = await fs.open(temporaryPath, "wx", 0o600);
|
|
84
|
-
await handle.writeFile(`${JSON.stringify(value, null, "\t")}\n`, "utf8");
|
|
85
|
-
if (process.platform !== "win32") await handle.chmod(0o600);
|
|
86
|
-
await handle.sync();
|
|
87
|
-
await handle.close();
|
|
88
|
-
handle = undefined;
|
|
89
|
-
await fs.rename(temporaryPath, configPath);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
await handle?.close().catch(() => undefined);
|
|
92
|
-
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
@@ -81,6 +81,10 @@ export interface SyncPolicySettings {
|
|
|
81
81
|
automatic: boolean;
|
|
82
82
|
/** Explicit opt-in, separate from the legacy observe/shutdown policy. */
|
|
83
83
|
automaticTransfer?: boolean;
|
|
84
|
+
/** Experimental autonomous merged-sync authorization. */
|
|
85
|
+
agentMode?: boolean;
|
|
86
|
+
/** Separate explicit authorization for recurring background work. */
|
|
87
|
+
continuous?: boolean;
|
|
84
88
|
/** Version 4: settings.json root fields never uploaded. */
|
|
85
89
|
localFields?: string[];
|
|
86
90
|
mergeSettings?: boolean;
|
|
@@ -101,6 +105,7 @@ export interface PiSyncSettingsV3 {
|
|
|
101
105
|
onSwitch: OnSwitchAction;
|
|
102
106
|
skipSecretScan?: boolean;
|
|
103
107
|
showStatus?: boolean;
|
|
108
|
+
debug?: boolean;
|
|
104
109
|
storageConnections: Record<string, StorageConnectionSettings>;
|
|
105
110
|
syncSetups: Record<string, SyncSetupSettings>;
|
|
106
111
|
[key: string]: unknown;
|
|
@@ -115,6 +120,8 @@ export interface SyncConfig<Backend extends ResolvedSyncBackend = ResolvedS3Back
|
|
|
115
120
|
include: string[];
|
|
116
121
|
automatic: boolean;
|
|
117
122
|
automaticTransfer?: boolean;
|
|
123
|
+
agentMode?: boolean;
|
|
124
|
+
continuous?: boolean;
|
|
118
125
|
localFields?: string[];
|
|
119
126
|
mergeSettings?: boolean;
|
|
120
127
|
mergeContent?: boolean;
|
|
@@ -122,6 +129,7 @@ export interface SyncConfig<Backend extends ResolvedSyncBackend = ResolvedS3Back
|
|
|
122
129
|
onSwitch: OnSwitchAction;
|
|
123
130
|
skipSecretScan: boolean;
|
|
124
131
|
showStatus: boolean;
|
|
132
|
+
debug?: boolean;
|
|
125
133
|
backend: Backend;
|
|
126
134
|
}
|
|
127
135
|
|
|
@@ -137,6 +145,8 @@ export interface PartialConfig {
|
|
|
137
145
|
include: string[];
|
|
138
146
|
automatic: boolean;
|
|
139
147
|
automaticTransfer?: boolean;
|
|
148
|
+
agentMode?: boolean;
|
|
149
|
+
continuous?: boolean;
|
|
140
150
|
onSwitch: OnSwitchAction;
|
|
141
151
|
showStatus: boolean;
|
|
142
152
|
bucket?: string;
|
|
@@ -68,6 +68,9 @@ export function validateSettingsDocument(value: Record<string, unknown>): PiSync
|
|
|
68
68
|
if (value.showStatus !== undefined && typeof value.showStatus !== "boolean") {
|
|
69
69
|
throw new Error("Invalid pi-sync settings: showStatus must be boolean.");
|
|
70
70
|
}
|
|
71
|
+
if (value.debug !== undefined && typeof value.debug !== "boolean") {
|
|
72
|
+
throw new Error("Invalid pi-sync settings: debug must be boolean.");
|
|
73
|
+
}
|
|
71
74
|
const storageConnections = requireNamedObjectMap(
|
|
72
75
|
value.storageConnections,
|
|
73
76
|
"storageConnections",
|
|
@@ -216,7 +219,7 @@ function validateSyncSetup(name: string, value: Record<string, unknown>, connect
|
|
|
216
219
|
const localFields = normalizeLocalFields(sync.localFields);
|
|
217
220
|
if (localFields.length > 0 && !include.includes("settings.json"))
|
|
218
221
|
throw new Error("Nonempty localFields requires settings.json in sync.include.");
|
|
219
|
-
for (const key of ["mergeContent", "partialSync"])
|
|
222
|
+
for (const key of ["mergeContent", "partialSync", "agentMode", "continuous"])
|
|
220
223
|
if (sync[key] !== undefined && typeof sync[key] !== "boolean") throw new Error(`${key} must be boolean.`);
|
|
221
224
|
if (sync.mergeSettings !== undefined && typeof sync.mergeSettings !== "boolean")
|
|
222
225
|
throw new Error("mergeSettings must be boolean.");
|
|
@@ -2,6 +2,7 @@ import { Readable, Writable } from "node:stream";
|
|
|
2
2
|
import { pipeline } from "node:stream/promises";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { createGunzip, gzip } from "node:zlib";
|
|
5
|
+
import { throwIfAborted } from "../sync/signals.js";
|
|
5
6
|
import { snapshotSelectionInclude } from "../sync/sync-policy.js";
|
|
6
7
|
import type { Snapshot } from "./snapshot-types.js";
|
|
7
8
|
|
|
@@ -47,8 +48,3 @@ export async function decodeSnapshot(
|
|
|
47
48
|
snapshotSelectionInclude(parsed);
|
|
48
49
|
return parsed;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
function throwIfAborted(signal?: AbortSignal) {
|
|
52
|
-
if (!signal?.aborted) return;
|
|
53
|
-
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
54
|
-
}
|
package/src/state/lock.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import lockfile from "proper-lockfile";
|
|
5
5
|
import type { CommandOptions } from "../commands/command-types.js";
|
|
6
|
+
import { throwIfAborted } from "../sync/signals.js";
|
|
6
7
|
import { LOCK_GUARD_STALE_MS, LOCK_GUARD_UPDATE_MS } from "./lock-policy.js";
|
|
7
8
|
import { LOCKFILE_FS_ADAPTER } from "./lockfile-fs.js";
|
|
8
9
|
import type { LockFile } from "./state-types.js";
|
|
@@ -57,8 +58,11 @@ export async function withLock<T>(
|
|
|
57
58
|
inspection = { status: "missing" };
|
|
58
59
|
}
|
|
59
60
|
if (inspection.status === "valid") {
|
|
60
|
-
throw
|
|
61
|
-
|
|
61
|
+
throw Object.assign(
|
|
62
|
+
new Error(
|
|
63
|
+
`pi-sync is already running (${inspection.lock.command}, pid ${inspection.lock.pid}, started ${inspection.lock.startedAt}).`,
|
|
64
|
+
),
|
|
65
|
+
{ code: "ELOCKED" },
|
|
62
66
|
);
|
|
63
67
|
}
|
|
64
68
|
if (inspection.status === "unreadable") {
|
|
@@ -220,11 +224,6 @@ async function unlockGuarded(ctx: ExtensionCommandContext, options: CommandOptio
|
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
226
|
|
|
223
|
-
function throwIfAborted(signal?: AbortSignal) {
|
|
224
|
-
if (!signal?.aborted) return;
|
|
225
|
-
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
227
|
async function releaseGuard(guard: Guard) {
|
|
229
228
|
try {
|
|
230
229
|
await guard.release();
|
|
@@ -261,11 +260,15 @@ async function describeHeldLock() {
|
|
|
261
260
|
return new Error("pi-sync lock owner exited; retry shortly while the lock guard expires.");
|
|
262
261
|
}
|
|
263
262
|
if (current) {
|
|
264
|
-
return
|
|
265
|
-
`Pi-sync is currently running (${current.command}, pid ${current.pid}, started ${current.startedAt})
|
|
263
|
+
return Object.assign(
|
|
264
|
+
new Error(`Pi-sync is currently running (${current.command}, pid ${current.pid}, started ${current.startedAt}).`),
|
|
265
|
+
{ code: "ELOCKED" },
|
|
266
266
|
);
|
|
267
267
|
}
|
|
268
|
-
return
|
|
268
|
+
return Object.assign(
|
|
269
|
+
new Error("Pi-sync is currently running (lock metadata is unreadable or still being written)."),
|
|
270
|
+
{ code: "ELOCKED" },
|
|
271
|
+
);
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
function isLockHeldError(error: unknown) {
|
|
@@ -66,7 +66,7 @@ export async function stageMergeBaseline(
|
|
|
66
66
|
return (
|
|
67
67
|
!collisions.has(file.path) &&
|
|
68
68
|
accepted.lastFileHashes[file.path] === file.sha256 &&
|
|
69
|
-
(file.path === "settings.json" || config.mergeContent) &&
|
|
69
|
+
(file.path === "settings.json" || config.mergeContent || (config.agentMode && isMergeTextPath(file.path))) &&
|
|
70
70
|
eligible(file)
|
|
71
71
|
);
|
|
72
72
|
});
|
|
@@ -145,6 +145,24 @@ export async function readMergeAncestors(config: AnySyncConfig, state: SyncState
|
|
|
145
145
|
}
|
|
146
146
|
return record.files;
|
|
147
147
|
}
|
|
148
|
+
/** A no-op must not skip seeding or repairing ancestors required by later merges. */
|
|
149
|
+
export async function hasCurrentMergeBaseline(config: AnySyncConfig, snapshot: Snapshot, state: SyncState) {
|
|
150
|
+
try {
|
|
151
|
+
const ancestors = await readMergeAncestors(config, state);
|
|
152
|
+
if (!ancestors) return false;
|
|
153
|
+
const hashes = new Map(ancestors.map((file) => [file.path, file.sha256]));
|
|
154
|
+
return snapshot.files.every(
|
|
155
|
+
(file) =>
|
|
156
|
+
!(file.path === "settings.json" || config.mergeContent || (config.agentMode && isMergeTextPath(file.path))) ||
|
|
157
|
+
!eligible(file) ||
|
|
158
|
+
hashes.get(file.path) === file.sha256,
|
|
159
|
+
);
|
|
160
|
+
} catch {
|
|
161
|
+
// Preserve the existing full path's validation and repair behavior for unknown evidence.
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
148
166
|
export async function readMergeAncestor(config: AnySyncConfig, state: SyncState, filePath: string) {
|
|
149
167
|
const file = (await readMergeAncestors(config, state))?.find((file) => file.path === filePath);
|
|
150
168
|
return file ? Buffer.from(file.contentBase64, "base64") : undefined;
|
|
@@ -79,7 +79,14 @@ export async function migrateLegacyStateDirectory(): Promise<StateDirectoryPrepa
|
|
|
79
79
|
realpath: false,
|
|
80
80
|
stale: LOCK_GUARD_STALE_MS,
|
|
81
81
|
});
|
|
82
|
-
|
|
82
|
+
// Idle continuous owners do not retain the exclusive migration guard.
|
|
83
|
+
// A live renewable lease still prevents moving their private state root.
|
|
84
|
+
const schedulerHeld = await lockfile.check(path.join(legacyStateDir(), "continuous", "scheduler"), {
|
|
85
|
+
fs: LOCKFILE_FS_ADAPTER,
|
|
86
|
+
realpath: false,
|
|
87
|
+
stale: LOCK_GUARD_STALE_MS,
|
|
88
|
+
});
|
|
89
|
+
if (legacyGuardHeld || schedulerHeld || (await pathExists(legacyLock))) {
|
|
83
90
|
return {
|
|
84
91
|
status: "deferred",
|
|
85
92
|
message:
|