@narumitw/pi-sync 0.50.3 → 0.51.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/README.md +4 -1
- package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
- package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
- package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
- package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
- package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
- package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
- package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
- package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
- package/dist/chunks/chunk-6546TS54.ts.map +7 -0
- package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
- package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
- package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
- package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
- package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
- package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
- package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
- package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
- package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
- package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
- package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
- package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
- package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
- package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
- package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
- package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
- package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
- package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
- package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
- package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
- package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
- package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
- package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
- package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
- package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
- package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
- package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
- package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
- package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
- package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
- package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
- package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
- package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
- package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
- package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
- package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
- package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
- package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
- package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
- package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
- package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
- package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
- package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
- package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
- package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
- package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
- package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
- package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
- package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
- package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
- package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
- package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
- package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
- package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
- package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
- package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
- package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
- package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
- package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
- package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
- package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
- package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
- package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
- package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
- package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
- package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
- package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
- package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
- package/dist/index.ts +49 -74
- package/dist/index.ts.map +3 -3
- package/docs/settings.md +9 -0
- package/package.json +61 -62
- package/src/backends/backend-factory.ts +15 -15
- package/src/backends/backend-identity.ts +31 -31
- package/src/backends/backend-types.ts +42 -42
- package/src/backends/git/git-backend.ts +714 -788
- package/src/backends/git/git-config.ts +104 -111
- package/src/backends/git/git-runner.ts +230 -247
- package/src/backends/git/git-storage.ts +228 -245
- package/src/backends/s3/s3-backend.ts +408 -441
- package/src/backends/s3/s3-client.ts +288 -333
- package/src/backends/sync-backend.ts +65 -65
- package/src/backends/webdav/webdav-backend.ts +542 -589
- package/src/backends/webdav/webdav-client.ts +292 -316
- package/src/backends/webdav/webdav-config.ts +57 -68
- package/src/commands/command-execution.ts +270 -289
- package/src/commands/command-handler.ts +160 -163
- package/src/commands/command-types.ts +13 -13
- package/src/commands/command.ts +190 -196
- package/src/commands/setup-completions.ts +1 -1
- package/src/paths.ts +47 -47
- package/src/settings/config-errors.ts +4 -5
- package/src/settings/config-file.ts +473 -506
- package/src/settings/config.ts +171 -174
- package/src/settings/settings-management.ts +191 -216
- package/src/settings/settings-store.ts +63 -77
- package/src/settings/settings-types.ts +78 -75
- package/src/settings/settings-validation.ts +328 -364
- package/src/snapshot/session-paths.ts +41 -55
- package/src/snapshot/snapshot-apply.ts +197 -229
- package/src/snapshot/snapshot-codec.ts +29 -31
- package/src/snapshot/snapshot-paths.ts +4 -6
- package/src/snapshot/snapshot-transaction.ts +161 -170
- package/src/snapshot/snapshot-types.ts +24 -24
- package/src/snapshot/snapshot.ts +310 -343
- package/src/state/json-file.ts +11 -11
- package/src/state/lock.ts +226 -242
- package/src/state/lockfile-fs.ts +20 -20
- package/src/state/operation-availability.ts +36 -41
- package/src/state/state-directory.ts +171 -173
- package/src/state/state-types.ts +15 -15
- package/src/state/sync-state-store.ts +23 -23
- package/src/sync/automatic-sync.ts +75 -80
- package/src/sync/remote-snapshot.ts +52 -60
- package/src/sync/setup-switch.ts +99 -114
- package/src/sync/signals.ts +3 -5
- package/src/sync/startup-check.ts +97 -111
- package/src/sync/sync-decision.ts +76 -80
- package/src/sync/sync-error-guidance.ts +41 -48
- package/src/sync/sync-errors.ts +10 -10
- package/src/sync/sync-inspection.ts +29 -33
- package/src/sync/sync-loaders.ts +28 -36
- package/src/sync/sync-mutations.ts +510 -622
- package/src/sync/sync-operations.ts +7 -7
- package/src/sync/sync-policy.ts +296 -326
- package/src/sync/sync-queries.ts +175 -194
- package/src/sync/sync-state.ts +74 -100
- package/src/sync-extension.ts +89 -101
- package/src/sync.ts +26 -26
- package/src/types.ts +37 -37
- package/src/ui/automatic-sync-summary.ts +1 -3
- package/src/ui/cancellable-operation.ts +89 -101
- package/src/ui/file-selection.ts +250 -274
- package/src/ui/manager-attention.ts +51 -63
- package/src/ui/manager-recovery.ts +48 -57
- package/src/ui/manager-result-dispatcher.ts +62 -76
- package/src/ui/manager-state.ts +200 -232
- package/src/ui/manager-ui.ts +264 -362
- package/src/ui/remote-selection-ui.ts +547 -588
- package/src/ui/secret-input.ts +197 -205
- package/src/ui/settings-ui.ts +175 -172
- package/src/ui/setup/git-ui.ts +232 -261
- package/src/ui/setup/s3-credentials-ui.ts +58 -65
- package/src/ui/setup/s3-ui.ts +171 -195
- package/src/ui/setup/setup-actions.ts +91 -101
- package/src/ui/setup/setup-location-ui.ts +121 -137
- package/src/ui/setup/setup-name-ui.ts +27 -27
- package/src/ui/setup/setup-prompts.ts +65 -65
- package/src/ui/setup/setup-review.ts +58 -59
- package/src/ui/setup/setup-switcher.ts +156 -160
- package/src/ui/setup/setup-wizard.ts +28 -28
- package/src/ui/setup/text-input.ts +54 -58
- package/src/ui/setup/webdav-ui.ts +246 -293
- package/src/ui/storage-connections-ui.ts +317 -369
- package/src/ui/sync-attention.ts +148 -164
- package/src/ui/sync-format.ts +110 -121
- package/src/ui/sync-resolution-ui.ts +147 -153
- package/src/ui/sync-setups-ui.ts +218 -238
- package/src/ui/sync-status.ts +17 -0
- package/src/ui/terminal-text.ts +2 -2
- package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
- package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
- package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
- package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
- package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
- package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
- package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
- package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
- package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
- package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
- package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
- package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
- package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
- package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
- package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
- package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
- package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
- package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
- package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
- package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
- package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
- package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
- package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
- package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
- package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
- package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
- package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
- package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
- package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
- package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
- package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
- package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
- package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
- package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
- package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
- package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
- package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
- /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
- /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
- /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
|
@@ -1,84 +1,70 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
type ExtensionCommandContext,
|
|
5
|
-
type ExtensionContext,
|
|
6
|
-
getAgentDir,
|
|
7
|
-
} from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type ExtensionCommandContext, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
4
|
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
9
5
|
import { readJsonIfExists } from "../state/json-file.js";
|
|
10
6
|
import type { Snapshot, SnapshotOptions } from "./snapshot-types.js";
|
|
11
7
|
|
|
12
8
|
export function sessionDirFromContext(ctx: ExtensionCommandContext | ExtensionContext) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
? (getSessionDir.call(manager) as string | undefined)
|
|
23
|
-
: undefined;
|
|
9
|
+
const manager = ctx.sessionManager as typeof ctx.sessionManager & {
|
|
10
|
+
usesDefaultSessionDir?: () => boolean;
|
|
11
|
+
};
|
|
12
|
+
const usesDefaultSessionDir = manager.usesDefaultSessionDir;
|
|
13
|
+
if (typeof usesDefaultSessionDir === "function" && usesDefaultSessionDir.call(manager)) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const getSessionDir = manager.getSessionDir;
|
|
17
|
+
return typeof getSessionDir === "function" ? (getSessionDir.call(manager) as string | undefined) : undefined;
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
export async function configuredSessionDir() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
);
|
|
30
|
-
return settings?.sessionDir ? expandHome(settings.sessionDir) : undefined;
|
|
21
|
+
const settings = await readJsonIfExists<{ sessionDir?: string }>(path.join(agentDir(), "settings.json"));
|
|
22
|
+
return settings?.sessionDir ? expandHome(settings.sessionDir) : undefined;
|
|
31
23
|
}
|
|
32
24
|
|
|
33
|
-
export async function sessionDirForApply(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
contextSessionDir &&
|
|
41
|
-
path.resolve(contextSessionDir) !== path.resolve(localSessionDir ?? "")
|
|
42
|
-
) {
|
|
43
|
-
return contextSessionDir;
|
|
44
|
-
}
|
|
45
|
-
return sessionDirFromSnapshot(snapshot) ?? contextSessionDir;
|
|
25
|
+
export async function sessionDirForApply(ctx: ExtensionCommandContext | ExtensionContext, snapshot: Snapshot) {
|
|
26
|
+
const contextSessionDir = sessionDirFromContext(ctx);
|
|
27
|
+
const localSessionDir = await configuredSessionDir();
|
|
28
|
+
if (contextSessionDir && path.resolve(contextSessionDir) !== path.resolve(localSessionDir ?? "")) {
|
|
29
|
+
return contextSessionDir;
|
|
30
|
+
}
|
|
31
|
+
return sessionDirFromSnapshot(snapshot) ?? contextSessionDir;
|
|
46
32
|
}
|
|
47
33
|
|
|
48
34
|
function sessionDirFromSnapshot(snapshot: Snapshot) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
const settingsFile = snapshot.files.find((file) => file.path === "settings.json");
|
|
36
|
+
if (!settingsFile) return undefined;
|
|
37
|
+
try {
|
|
38
|
+
const settings = JSON.parse(decodeBase64Strict(settingsFile.contentBase64, settingsFile.path).toString("utf8")) as {
|
|
39
|
+
sessionDir?: string;
|
|
40
|
+
};
|
|
41
|
+
return settings.sessionDir ? expandHome(settings.sessionDir) : undefined;
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
59
45
|
}
|
|
60
46
|
|
|
61
47
|
function decodeBase64Strict(value: string, filePath: string) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
49
|
+
throw new Error(`Invalid base64 content in snapshot file: ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
return Buffer.from(value, "base64");
|
|
66
52
|
}
|
|
67
53
|
|
|
68
54
|
export function agentDir() {
|
|
69
|
-
|
|
55
|
+
return getAgentDir();
|
|
70
56
|
}
|
|
71
57
|
|
|
72
58
|
function expandHome(value: string) {
|
|
73
|
-
|
|
59
|
+
return value === "~" || value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
74
60
|
}
|
|
75
61
|
|
|
76
62
|
export function snapshotOptionsForContext(
|
|
77
|
-
|
|
78
|
-
|
|
63
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
64
|
+
config: AnySyncConfig,
|
|
79
65
|
): SnapshotOptions {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
66
|
+
return {
|
|
67
|
+
include: config.include,
|
|
68
|
+
sessionDir: sessionDirFromContext(ctx),
|
|
69
|
+
};
|
|
84
70
|
}
|
|
@@ -2,288 +2,256 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { Dirent } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
assertWithinRoot,
|
|
7
|
-
isDeniedPath,
|
|
8
|
-
isPathInside,
|
|
9
|
-
parentPaths,
|
|
10
|
-
safeJoin,
|
|
11
|
-
toPosix,
|
|
12
|
-
} from "../paths.js";
|
|
5
|
+
import { assertWithinRoot, isDeniedPath, isPathInside, parentPaths, safeJoin, toPosix } from "../paths.js";
|
|
13
6
|
import { agentDir } from "./session-paths.js";
|
|
14
7
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
createSnapshot,
|
|
9
|
+
isSessionFilePath,
|
|
10
|
+
isSessionPath,
|
|
11
|
+
sessionStorageRoot,
|
|
12
|
+
snapshotIncludesSessions,
|
|
13
|
+
snapshotTarget,
|
|
21
14
|
} from "./snapshot.js";
|
|
22
|
-
import {
|
|
23
|
-
applySnapshotTransaction,
|
|
24
|
-
recoverPendingSnapshotTransactions,
|
|
25
|
-
} from "./snapshot-transaction.js";
|
|
15
|
+
import { applySnapshotTransaction, recoverPendingSnapshotTransactions } from "./snapshot-transaction.js";
|
|
26
16
|
import type { Snapshot, SnapshotApplyPlan, SnapshotOptions } from "./snapshot-types.js";
|
|
27
17
|
|
|
28
18
|
function sha256(value: Buffer) {
|
|
29
|
-
|
|
19
|
+
return createHash("sha256").update(value).digest("hex");
|
|
30
20
|
}
|
|
31
21
|
|
|
32
22
|
function fileHashMap(snapshot: Snapshot) {
|
|
33
|
-
|
|
23
|
+
return Object.fromEntries(snapshot.files.map((file) => [file.path, file.sha256]));
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
export async function applySnapshot(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
SnapshotOptions,
|
|
41
|
-
"include" | "sessionDir" | "syncFiles" | "syncSessions" | "extraFiles"
|
|
42
|
-
> = {},
|
|
27
|
+
snapshot: Snapshot,
|
|
28
|
+
protectedRelativePaths = new Set<string>(),
|
|
29
|
+
options: Pick<SnapshotOptions, "include" | "sessionDir" | "syncFiles" | "syncSessions" | "extraFiles"> = {},
|
|
43
30
|
) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
31
|
+
const root = agentDir();
|
|
32
|
+
const { sessionDir } = options;
|
|
33
|
+
await recoverPendingSnapshotTransactions();
|
|
34
|
+
const current = await createSnapshot(snapshot.profile, {
|
|
35
|
+
...options,
|
|
36
|
+
...(options.include === undefined ? { syncSessions: snapshotIncludesSessions(snapshot) } : {}),
|
|
37
|
+
sessionDir,
|
|
38
|
+
});
|
|
39
|
+
const plan = await addTopLevelCaseVariantDeletes(
|
|
40
|
+
root,
|
|
41
|
+
protectSnapshotApplyPlan(
|
|
42
|
+
root,
|
|
43
|
+
preflightSnapshotApply(root, snapshot, current, { sessionDir }),
|
|
44
|
+
protectedRelativePaths,
|
|
45
|
+
sessionDir,
|
|
46
|
+
),
|
|
47
|
+
snapshot,
|
|
48
|
+
);
|
|
49
|
+
await preflightSnapshotMutations(root, plan, sessionDir);
|
|
50
|
+
await applySnapshotTransaction(plan, { sessionDir });
|
|
51
|
+
return appliedFileHashMap(snapshot, current, protectedRelativePaths);
|
|
65
52
|
}
|
|
66
53
|
|
|
67
54
|
export function preflightSnapshotApply(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
root: string,
|
|
56
|
+
snapshot: Snapshot,
|
|
57
|
+
current: Snapshot,
|
|
58
|
+
options: { sessionDir?: string } = {},
|
|
72
59
|
): SnapshotApplyPlan {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
const seenPaths = new Set<string>();
|
|
61
|
+
const remotePaths = new Set<string>();
|
|
62
|
+
const writes: Array<{ target: string; content: Buffer }> = [];
|
|
63
|
+
const deletes: string[] = [];
|
|
77
64
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
for (const file of snapshot.files) {
|
|
66
|
+
const normalized = toPosix(file.path);
|
|
67
|
+
if (!isSafeSnapshotPath(file.path)) {
|
|
68
|
+
throw new Error(`Unsafe path in snapshot: ${file.path}`);
|
|
69
|
+
}
|
|
70
|
+
if (isSessionPath(normalized) && !isSessionFilePath(normalized)) {
|
|
71
|
+
throw new Error(`Unsafe session path in snapshot: ${file.path}`);
|
|
72
|
+
}
|
|
73
|
+
if (seenPaths.has(normalized)) throw new Error(`Duplicate path in snapshot: ${normalized}`);
|
|
74
|
+
seenPaths.add(normalized);
|
|
75
|
+
remotePaths.add(normalized);
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
77
|
+
const target = snapshotTarget(root, normalized, options.sessionDir);
|
|
78
|
+
const content = decodeBase64Strict(file.contentBase64, normalized);
|
|
79
|
+
if (sha256(content) !== file.sha256) throw new Error(`Checksum mismatch in snapshot file: ${normalized}`);
|
|
80
|
+
writes.push({ target, content });
|
|
81
|
+
}
|
|
96
82
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
83
|
+
const deletePaths = new Set<string>();
|
|
84
|
+
for (const file of current.files) {
|
|
85
|
+
const normalized = toPosix(file.path);
|
|
86
|
+
if (!remotePaths.has(normalized)) {
|
|
87
|
+
deletePaths.add(snapshotTarget(root, normalized, options.sessionDir));
|
|
88
|
+
}
|
|
89
|
+
for (const remotePath of parentPaths(normalized)) {
|
|
90
|
+
if (remotePaths.has(remotePath)) {
|
|
91
|
+
deletePaths.add(snapshotTarget(root, remotePath, options.sessionDir));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
deletes.push(...deletePaths);
|
|
110
96
|
|
|
111
|
-
|
|
97
|
+
return { writes, deletes };
|
|
112
98
|
}
|
|
113
99
|
|
|
114
100
|
export function protectSnapshotApplyPlan(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
101
|
+
root: string,
|
|
102
|
+
plan: SnapshotApplyPlan,
|
|
103
|
+
protectedRelativePaths: Set<string>,
|
|
104
|
+
sessionDir?: string,
|
|
119
105
|
): SnapshotApplyPlan {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
deletes: plan.deletes.filter((target) => !protectedTargets.has(target)),
|
|
129
|
-
};
|
|
106
|
+
if (protectedRelativePaths.size === 0) return plan;
|
|
107
|
+
const protectedTargets = new Set(
|
|
108
|
+
[...protectedRelativePaths].map((relativePath) => snapshotTarget(root, relativePath, sessionDir)),
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
writes: plan.writes.filter((item) => !protectedTargets.has(item.target)),
|
|
112
|
+
deletes: plan.deletes.filter((target) => !protectedTargets.has(target)),
|
|
113
|
+
};
|
|
130
114
|
}
|
|
131
115
|
|
|
132
116
|
export async function addTopLevelCaseVariantDeletes(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
117
|
+
root: string,
|
|
118
|
+
plan: SnapshotApplyPlan,
|
|
119
|
+
snapshot: Pick<Snapshot, "files">,
|
|
136
120
|
): Promise<SnapshotApplyPlan> {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
121
|
+
const topLevelPaths = new Map<string, string>();
|
|
122
|
+
for (const file of snapshot.files) {
|
|
123
|
+
const normalized = toPosix(file.path);
|
|
124
|
+
if (!normalized.includes("/") && isSafeSnapshotPath(file.path)) {
|
|
125
|
+
topLevelPaths.set(normalized.toLowerCase(), normalized);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (topLevelPaths.size === 0) return plan;
|
|
145
129
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
130
|
+
let entries: Dirent[];
|
|
131
|
+
try {
|
|
132
|
+
entries = await fs.readdir(root, { withFileTypes: true });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return plan;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
153
137
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
deletes.add(safeJoin(root, entry.name));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return { ...plan, deletes: [...deletes] };
|
|
138
|
+
const deletes = new Set(plan.deletes);
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const canonicalPath = topLevelPaths.get(entry.name.toLowerCase());
|
|
141
|
+
if (canonicalPath && entry.name !== canonicalPath && (entry.isFile() || entry.isSymbolicLink())) {
|
|
142
|
+
deletes.add(safeJoin(root, entry.name));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { ...plan, deletes: [...deletes] };
|
|
166
146
|
}
|
|
167
147
|
|
|
168
|
-
export function appliedFileHashMap(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
delete hashes[normalized];
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return hashes;
|
|
148
|
+
export function appliedFileHashMap(snapshot: Snapshot, current: Snapshot, protectedRelativePaths: Set<string>) {
|
|
149
|
+
const hashes = fileHashMap(snapshot);
|
|
150
|
+
if (protectedRelativePaths.size === 0) return hashes;
|
|
151
|
+
const currentHashes = fileHashMap(current);
|
|
152
|
+
for (const relativePath of protectedRelativePaths) {
|
|
153
|
+
const normalized = toPosix(relativePath);
|
|
154
|
+
if (currentHashes[normalized]) {
|
|
155
|
+
hashes[normalized] = currentHashes[normalized];
|
|
156
|
+
} else {
|
|
157
|
+
delete hashes[normalized];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return hashes;
|
|
185
161
|
}
|
|
186
162
|
|
|
187
163
|
function isSafeSnapshotPath(relativePath: string) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
164
|
+
if (relativePath.includes("\\")) return false;
|
|
165
|
+
const normalized = toPosix(relativePath);
|
|
166
|
+
return (
|
|
167
|
+
Boolean(normalized) &&
|
|
168
|
+
normalized !== "." &&
|
|
169
|
+
normalized !== ".." &&
|
|
170
|
+
!normalized.startsWith("../") &&
|
|
171
|
+
!path.posix.isAbsolute(normalized) &&
|
|
172
|
+
path.posix.normalize(normalized) === normalized &&
|
|
173
|
+
!isDeniedPath(normalized)
|
|
174
|
+
);
|
|
199
175
|
}
|
|
200
176
|
|
|
201
177
|
function decodeBase64Strict(value: string, filePath: string) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
178
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
179
|
+
throw new Error(`Invalid base64 content in snapshot file: ${filePath}`);
|
|
180
|
+
}
|
|
181
|
+
return Buffer.from(value, "base64");
|
|
206
182
|
}
|
|
207
183
|
|
|
208
184
|
async function preflightSnapshotMutations(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
185
|
+
root: string,
|
|
186
|
+
plan: { deletes: string[]; writes: Array<{ target: string; content: Buffer }> },
|
|
187
|
+
sessionDir?: string,
|
|
212
188
|
) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
item.target,
|
|
221
|
-
deletePaths,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
189
|
+
const deletePaths = new Set(plan.deletes);
|
|
190
|
+
for (const target of plan.deletes) {
|
|
191
|
+
await assertNoSymlinkParents(rootForTarget(root, target, sessionDir), target);
|
|
192
|
+
}
|
|
193
|
+
for (const item of plan.writes) {
|
|
194
|
+
await prepareSnapshotWrite(rootForTarget(root, item.target, sessionDir), item.target, deletePaths);
|
|
195
|
+
}
|
|
224
196
|
}
|
|
225
197
|
|
|
226
198
|
function rootForTarget(root: string, target: string, sessionDir?: string) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
199
|
+
const sessionRoot = sessionDir ? sessionStorageRoot(root, sessionDir) : undefined;
|
|
200
|
+
if (sessionRoot && isPathInside(sessionRoot, target)) return sessionRoot;
|
|
201
|
+
return root;
|
|
230
202
|
}
|
|
231
203
|
|
|
232
204
|
async function prepareSnapshotWrite(root: string, target: string, deletePaths: Set<string>) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
205
|
+
const parentWillBeReplaced = await ensureSafeDirectory(root, path.dirname(target), deletePaths);
|
|
206
|
+
if (parentWillBeReplaced) return;
|
|
207
|
+
try {
|
|
208
|
+
const stat = await fs.lstat(target);
|
|
209
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing to overwrite symlink during snapshot apply: ${target}`);
|
|
210
|
+
if (stat.isDirectory() && !deletePaths.has(target)) {
|
|
211
|
+
throw new Error(`Refusing to overwrite directory during snapshot apply: ${target}`);
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
215
|
+
}
|
|
245
216
|
}
|
|
246
217
|
|
|
247
218
|
async function ensureSafeDirectory(root: string, directory: string, deletePaths: Set<string>) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return false;
|
|
219
|
+
assertWithinRoot(root, directory);
|
|
220
|
+
const rootPath = path.resolve(root);
|
|
221
|
+
const relative = path.relative(rootPath, path.resolve(directory));
|
|
222
|
+
let current = rootPath;
|
|
223
|
+
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
224
|
+
current = path.join(current, part);
|
|
225
|
+
try {
|
|
226
|
+
const stat = await fs.lstat(current);
|
|
227
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing to follow symlink during snapshot apply: ${current}`);
|
|
228
|
+
if (!stat.isDirectory()) {
|
|
229
|
+
if (deletePaths.has(current)) return true;
|
|
230
|
+
throw new Error(`Snapshot path parent is not a directory: ${current}`);
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
234
|
+
await fs.mkdir(current);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
268
238
|
}
|
|
269
239
|
|
|
270
240
|
async function assertNoSymlinkParents(root: string, target: string) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
}
|
|
241
|
+
assertWithinRoot(root, target);
|
|
242
|
+
const rootPath = path.resolve(root);
|
|
243
|
+
const relative = path.relative(rootPath, path.resolve(target));
|
|
244
|
+
let current = rootPath;
|
|
245
|
+
const parts = relative.split(path.sep).filter(Boolean);
|
|
246
|
+
for (const part of parts.slice(0, -1)) {
|
|
247
|
+
current = path.join(current, part);
|
|
248
|
+
try {
|
|
249
|
+
const stat = await fs.lstat(current);
|
|
250
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing to follow symlink during snapshot apply: ${current}`);
|
|
251
|
+
if (!stat.isDirectory()) throw new Error(`Snapshot path parent is not a directory: ${current}`);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
289
257
|
}
|