@narumitw/pi-sync 0.50.3 → 0.51.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 +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
package/src/snapshot/snapshot.ts
CHANGED
|
@@ -11,430 +11,397 @@ export { isDeniedPath } from "../paths.js";
|
|
|
11
11
|
export { sessionStorageRoot } from "./snapshot-paths.js";
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
canonicalSnapshotPathForConfig,
|
|
15
|
+
customIncludePathsByLower,
|
|
16
|
+
DEFAULT_SYNC_INCLUDE,
|
|
17
|
+
includeFromSelectionConfig,
|
|
18
|
+
isConfiguredSnapshotPath,
|
|
19
|
+
isPreservableUnmanagedSnapshotPath,
|
|
20
|
+
normalizeExtraFiles,
|
|
21
|
+
normalizeSyncFiles,
|
|
22
|
+
type SyncSelectionConfig,
|
|
23
|
+
selectionForSnapshot,
|
|
24
|
+
snapshotSelectionInclude,
|
|
25
|
+
syncIncludeSelection,
|
|
26
26
|
} from "../sync/sync-policy.js";
|
|
27
27
|
import type { Snapshot, SnapshotFile, SnapshotOptions } from "./snapshot-types.js";
|
|
28
28
|
|
|
29
29
|
export { canonicalSnapshotPathForConfig, isConfiguredSnapshotPath } from "../sync/sync-policy.js";
|
|
30
30
|
|
|
31
31
|
const VERSION = 1;
|
|
32
|
-
const TOP_LEVEL_FILES: readonly string[] = DEFAULT_SYNC_INCLUDE.filter((name) =>
|
|
33
|
-
name.includes("."),
|
|
34
|
-
);
|
|
32
|
+
const TOP_LEVEL_FILES: readonly string[] = DEFAULT_SYNC_INCLUDE.filter((name) => name.includes("."));
|
|
35
33
|
const TOP_LEVEL_DIRS = new Set<string>(DEFAULT_SYNC_INCLUDE.filter((name) => !name.includes(".")));
|
|
36
34
|
const SECRET_PATTERNS = [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
/AWS_SECRET_ACCESS_KEY\s*[=:]\s*['"]?[A-Za-z0-9/+]{35,}/i,
|
|
36
|
+
/(ANTHROPIC|OPENAI|GEMINI|GOOGLE|FIRECRAWL|GITHUB|CLOUDFLARE|R2|S3)_[A-Z0-9_]*(KEY|TOKEN|SECRET)\s*[=:]\s*['"]?[^\s'"]{12,}/i,
|
|
37
|
+
/sk-ant-[A-Za-z0-9_-]{20,}/,
|
|
38
|
+
/sk-[A-Za-z0-9]{20,}/,
|
|
39
|
+
/gh[pousr]_[A-Za-z0-9_]{20,}/,
|
|
42
40
|
];
|
|
43
41
|
|
|
44
42
|
function selectTopLevelFileEntry(entries: Dirent[], fileName: string) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
const exact = entries.find((entry) => entry.isFile() && entry.name === fileName);
|
|
44
|
+
if (exact) return exact;
|
|
45
|
+
const lower = fileName.toLowerCase();
|
|
46
|
+
return entries
|
|
47
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase() === lower)
|
|
48
|
+
.sort((left, right) => left.name.localeCompare(right.name))[0];
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
function sha256(value: Buffer) {
|
|
54
|
-
|
|
52
|
+
return createHash("sha256").update(value).digest("hex");
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
function isSafeSnapshotPath(relativePath: string) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
if (relativePath.includes("\\")) return false;
|
|
57
|
+
const normalized = toPosix(relativePath);
|
|
58
|
+
return (
|
|
59
|
+
Boolean(normalized) &&
|
|
60
|
+
normalized !== "." &&
|
|
61
|
+
normalized !== ".." &&
|
|
62
|
+
!normalized.startsWith("../") &&
|
|
63
|
+
!path.posix.isAbsolute(normalized) &&
|
|
64
|
+
path.posix.normalize(normalized) === normalized &&
|
|
65
|
+
!isDeniedPath(normalized)
|
|
66
|
+
);
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
function snapshotsMatch(left: Snapshot, right: Snapshot) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
const leftHashes = new Map(left.files.map((file) => [file.path, file.sha256]));
|
|
71
|
+
const rightHashes = new Map(right.files.map((file) => [file.path, file.sha256]));
|
|
72
|
+
return (
|
|
73
|
+
left.syncSessions === right.syncSessions &&
|
|
74
|
+
sameOptionalInclude(snapshotSelectionInclude(left), snapshotSelectionInclude(right)) &&
|
|
75
|
+
leftHashes.size === rightHashes.size &&
|
|
76
|
+
[...leftHashes].every(([filePath, hash]) => rightHashes.get(filePath) === hash)
|
|
77
|
+
);
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
function snapshotId() {
|
|
83
|
-
|
|
81
|
+
return `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
export function regenerateSnapshotIdentity(snapshot: Snapshot): Snapshot {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
return {
|
|
86
|
+
...snapshot,
|
|
87
|
+
id: snapshotId(),
|
|
88
|
+
createdAt: new Date().toISOString(),
|
|
89
|
+
machine: os.hostname(),
|
|
90
|
+
};
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
export async function createSnapshot(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
selection: selectionForSnapshot(include),
|
|
114
|
-
files,
|
|
115
|
-
};
|
|
93
|
+
export async function createSnapshot(profile: string, options: SnapshotOptions = {}): Promise<Snapshot> {
|
|
94
|
+
options.signal?.throwIfAborted();
|
|
95
|
+
const include = effectiveInclude(options);
|
|
96
|
+
const syncSessions = include.includes("sessions");
|
|
97
|
+
const sessionDir = options.sessionDir ?? (await configuredSessionDir());
|
|
98
|
+
options.signal?.throwIfAborted();
|
|
99
|
+
const files = await collectFiles(agentDir(), { include, sessionDir, signal: options.signal });
|
|
100
|
+
options.signal?.throwIfAborted();
|
|
101
|
+
return {
|
|
102
|
+
version: VERSION,
|
|
103
|
+
id: snapshotId(),
|
|
104
|
+
createdAt: new Date().toISOString(),
|
|
105
|
+
machine: os.hostname(),
|
|
106
|
+
profile,
|
|
107
|
+
syncSessions,
|
|
108
|
+
selection: selectionForSnapshot(include),
|
|
109
|
+
files,
|
|
110
|
+
};
|
|
116
111
|
}
|
|
117
112
|
|
|
118
113
|
function effectiveInclude(options: SnapshotOptions) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
114
|
+
if (options.include) return options.include;
|
|
115
|
+
return [
|
|
116
|
+
...normalizeSyncFiles(options.syncFiles),
|
|
117
|
+
...normalizeExtraFiles(options.extraFiles),
|
|
118
|
+
...(options.syncSessions ? ["sessions"] : []),
|
|
119
|
+
];
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
export async function collectFiles(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
signal?.throwIfAborted();
|
|
163
|
-
return results.sort((left, right) => left.path.localeCompare(right.path));
|
|
122
|
+
export async function collectFiles(root: string, options: SnapshotOptions = {}): Promise<SnapshotFile[]> {
|
|
123
|
+
const signal = options.signal;
|
|
124
|
+
signal?.throwIfAborted();
|
|
125
|
+
const results: SnapshotFile[] = [];
|
|
126
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
127
|
+
signal?.throwIfAborted();
|
|
128
|
+
const selection = syncIncludeSelection(effectiveInclude(options));
|
|
129
|
+
const selectedFiles = new Set<string>(selection.builtIns);
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (entry.isDirectory() && TOP_LEVEL_DIRS.has(entry.name) && selectedFiles.has(entry.name)) {
|
|
132
|
+
await collectDirectory(results, root, entry.name, { signal });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const fileName of TOP_LEVEL_FILES) {
|
|
136
|
+
if (!selectedFiles.has(fileName)) continue;
|
|
137
|
+
const entry = selectTopLevelFileEntry(entries, fileName);
|
|
138
|
+
if (entry) await addFile(results, root, entry.name, fileName, signal);
|
|
139
|
+
}
|
|
140
|
+
for (const relativePath of selection.custom) {
|
|
141
|
+
await collectIncludedPath(results, root, relativePath, signal);
|
|
142
|
+
}
|
|
143
|
+
if (selection.sessions) {
|
|
144
|
+
try {
|
|
145
|
+
await collectDirectory(results, sessionStorageRoot(root, options.sessionDir), "", {
|
|
146
|
+
sessionsOnly: true,
|
|
147
|
+
virtualPrefix: "sessions",
|
|
148
|
+
signal,
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
return results.sort((left, right) => left.path.localeCompare(right.path));
|
|
164
156
|
}
|
|
165
157
|
|
|
166
|
-
async function collectIncludedPath(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
signal?.throwIfAborted();
|
|
184
|
-
const entry = selectTopLevelFileEntry(entries, relativePath);
|
|
185
|
-
if (entry) await addFile(results, root, entry.name, relativePath, signal);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
158
|
+
async function collectIncludedPath(results: SnapshotFile[], root: string, relativePath: string, signal?: AbortSignal) {
|
|
159
|
+
signal?.throwIfAborted();
|
|
160
|
+
const absolutePath = safeJoin(root, relativePath);
|
|
161
|
+
try {
|
|
162
|
+
const stat = await fs.lstat(absolutePath);
|
|
163
|
+
signal?.throwIfAborted();
|
|
164
|
+
if (stat.isFile()) await addFile(results, root, relativePath, relativePath, signal);
|
|
165
|
+
else if (stat.isDirectory()) await collectDirectory(results, root, relativePath, { signal });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
168
|
+
if (!relativePath.includes("/")) {
|
|
169
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
170
|
+
signal?.throwIfAborted();
|
|
171
|
+
const entry = selectTopLevelFileEntry(entries, relativePath);
|
|
172
|
+
if (entry) await addFile(results, root, entry.name, relativePath, signal);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
188
175
|
}
|
|
189
176
|
|
|
190
177
|
async function collectDirectory(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
178
|
+
results: SnapshotFile[],
|
|
179
|
+
root: string,
|
|
180
|
+
relativeDirectory: string,
|
|
181
|
+
options: { sessionsOnly?: boolean; virtualPrefix?: string; signal?: AbortSignal } = {},
|
|
195
182
|
) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
}
|
|
183
|
+
options.signal?.throwIfAborted();
|
|
184
|
+
const absoluteDirectory = path.join(root, relativeDirectory);
|
|
185
|
+
for (const entry of await fs.readdir(absoluteDirectory, { withFileTypes: true })) {
|
|
186
|
+
options.signal?.throwIfAborted();
|
|
187
|
+
const relativePath = relativeDirectory ? posixJoin(relativeDirectory, entry.name) : entry.name;
|
|
188
|
+
const snapshotPath = options.virtualPrefix ? posixJoin(options.virtualPrefix, relativePath) : relativePath;
|
|
189
|
+
if (isDeniedPath(snapshotPath)) continue;
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
await collectDirectory(results, root, relativePath, options);
|
|
192
|
+
} else if (entry.isFile() && (!options.sessionsOnly || isSessionFilePath(snapshotPath))) {
|
|
193
|
+
await addFile(results, root, relativePath, snapshotPath, options.signal);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
211
196
|
}
|
|
212
197
|
|
|
213
198
|
async function addFile(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
199
|
+
results: SnapshotFile[],
|
|
200
|
+
root: string,
|
|
201
|
+
relativePath: string,
|
|
202
|
+
snapshotPath = relativePath,
|
|
203
|
+
signal?: AbortSignal,
|
|
219
204
|
) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
205
|
+
signal?.throwIfAborted();
|
|
206
|
+
if (!isSafeSnapshotPath(snapshotPath)) return;
|
|
207
|
+
const absolutePath = safeJoin(root, relativePath);
|
|
208
|
+
const content = await fs.readFile(absolutePath, { signal });
|
|
209
|
+
signal?.throwIfAborted();
|
|
210
|
+
results.push({
|
|
211
|
+
path: snapshotPath,
|
|
212
|
+
contentBase64: content.toString("base64"),
|
|
213
|
+
sha256: sha256(content),
|
|
214
|
+
});
|
|
230
215
|
}
|
|
231
216
|
|
|
232
217
|
export function isSessionPath(relativePath: string) {
|
|
233
|
-
|
|
218
|
+
return toPosix(relativePath).startsWith("sessions/");
|
|
234
219
|
}
|
|
235
220
|
|
|
236
221
|
export function isSessionFilePath(relativePath: string) {
|
|
237
|
-
|
|
238
|
-
|
|
222
|
+
const normalized = toPosix(relativePath);
|
|
223
|
+
return isSessionPath(normalized) && normalized.endsWith(".jsonl");
|
|
239
224
|
}
|
|
240
225
|
|
|
241
|
-
export function sessionSnapshotPathFromAbsolute(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!relativePath || relativePath.startsWith("../") || path.posix.isAbsolute(relativePath)) {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
251
|
-
const snapshotPath = posixJoin("sessions", relativePath);
|
|
252
|
-
return isSessionFilePath(snapshotPath) ? snapshotPath : undefined;
|
|
226
|
+
export function sessionSnapshotPathFromAbsolute(sessionFile: string, configuredSessionDir?: string) {
|
|
227
|
+
const relativePath = toPosix(path.relative(sessionStorageRoot(agentDir(), configuredSessionDir), sessionFile));
|
|
228
|
+
if (!relativePath || relativePath.startsWith("../") || path.posix.isAbsolute(relativePath)) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const snapshotPath = posixJoin("sessions", relativePath);
|
|
232
|
+
return isSessionFilePath(snapshotPath) ? snapshotPath : undefined;
|
|
253
233
|
}
|
|
254
234
|
|
|
255
235
|
export function snapshotTarget(root: string, relativePath: string, configuredSessionDir?: string) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
return safeJoin(root, relativePath);
|
|
236
|
+
if (isSessionPath(relativePath)) {
|
|
237
|
+
return safeJoin(sessionStorageRoot(root, configuredSessionDir), relativePath.slice("sessions/".length));
|
|
238
|
+
}
|
|
239
|
+
return safeJoin(root, relativePath);
|
|
263
240
|
}
|
|
264
241
|
|
|
265
242
|
export function snapshotIncludesSessions(snapshot: Snapshot) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
243
|
+
return (
|
|
244
|
+
snapshot.syncSessions === true ||
|
|
245
|
+
snapshotSelectionInclude(snapshot)?.includes("sessions") === true ||
|
|
246
|
+
snapshot.files.some((file) => isSessionPath(file.path))
|
|
247
|
+
);
|
|
271
248
|
}
|
|
272
249
|
|
|
273
250
|
export function filterSnapshotForConfigPolicy(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
251
|
+
snapshot: Snapshot,
|
|
252
|
+
config: SyncSelectionConfig,
|
|
253
|
+
options: { regenerateId?: boolean } = {},
|
|
277
254
|
) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
255
|
+
const include = includeFromSelectionConfig(config);
|
|
256
|
+
const includePaths = customIncludePathsByLower(include);
|
|
257
|
+
const filtered = {
|
|
258
|
+
...snapshot,
|
|
259
|
+
syncSessions: include.includes("sessions") ? snapshot.syncSessions : false,
|
|
260
|
+
selection: selectionForSnapshot(include),
|
|
261
|
+
files: canonicalizeSnapshotFilesForConfig(snapshot.files, config, includePaths),
|
|
262
|
+
};
|
|
263
|
+
if (!options.regenerateId || snapshotsMatch(snapshot, filtered)) return filtered;
|
|
264
|
+
return {
|
|
265
|
+
...filtered,
|
|
266
|
+
id: snapshotId(),
|
|
267
|
+
createdAt: new Date().toISOString(),
|
|
268
|
+
machine: os.hostname(),
|
|
269
|
+
};
|
|
293
270
|
}
|
|
294
271
|
|
|
295
272
|
function canonicalizeSnapshotFilesForConfig(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
273
|
+
files: SnapshotFile[],
|
|
274
|
+
config: SyncSelectionConfig,
|
|
275
|
+
includePaths: Map<string, string>,
|
|
299
276
|
) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return [
|
|
326
|
-
...configuredFiles,
|
|
327
|
-
...[...extraCandidates.values()].map((candidate) => candidate.file),
|
|
328
|
-
].sort((left, right) => left.path.localeCompare(right.path));
|
|
277
|
+
const configuredFiles: SnapshotFile[] = [];
|
|
278
|
+
const extraCandidates = new Map<string, { exact: boolean; file: SnapshotFile; originalPath: string }>();
|
|
279
|
+
for (const file of files) {
|
|
280
|
+
const normalized = toPosix(file.path);
|
|
281
|
+
if (!isSafeSnapshotPath(file.path) || !isConfiguredSnapshotPath(normalized, config)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (normalized.includes("/")) {
|
|
285
|
+
configuredFiles.push(normalized === file.path ? file : { ...file, path: normalized });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const topLevelPath = canonicalSnapshotPathForConfig(normalized, includePaths);
|
|
289
|
+
const candidate = {
|
|
290
|
+
exact: normalized === topLevelPath,
|
|
291
|
+
file: { ...file, path: topLevelPath },
|
|
292
|
+
originalPath: normalized,
|
|
293
|
+
};
|
|
294
|
+
const current = extraCandidates.get(topLevelPath.toLowerCase());
|
|
295
|
+
if (!current || isPreferredExtraCandidate(candidate, current)) {
|
|
296
|
+
extraCandidates.set(topLevelPath.toLowerCase(), candidate);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return [...configuredFiles, ...[...extraCandidates.values()].map((candidate) => candidate.file)].sort((left, right) =>
|
|
300
|
+
left.path.localeCompare(right.path),
|
|
301
|
+
);
|
|
329
302
|
}
|
|
330
303
|
|
|
331
304
|
function isPreferredExtraCandidate(
|
|
332
|
-
|
|
333
|
-
|
|
305
|
+
left: { exact: boolean; originalPath: string },
|
|
306
|
+
right: { exact: boolean; originalPath: string },
|
|
334
307
|
) {
|
|
335
|
-
|
|
336
|
-
|
|
308
|
+
if (left.exact !== right.exact) return left.exact;
|
|
309
|
+
return left.originalPath.localeCompare(right.originalPath) < 0;
|
|
337
310
|
}
|
|
338
311
|
|
|
339
312
|
export function snapshotWithoutSessions(snapshot: Snapshot) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
313
|
+
const files = snapshot.files.filter((file) => !isSessionPath(file.path));
|
|
314
|
+
const include = snapshotSelectionInclude(snapshot)?.filter((item) => item !== "sessions");
|
|
315
|
+
if (
|
|
316
|
+
files.length === snapshot.files.length &&
|
|
317
|
+
snapshot.syncSessions !== true &&
|
|
318
|
+
include?.length === snapshot.selection?.include.length
|
|
319
|
+
) {
|
|
320
|
+
return snapshot;
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
...snapshot,
|
|
324
|
+
id: snapshotId(),
|
|
325
|
+
createdAt: new Date().toISOString(),
|
|
326
|
+
machine: os.hostname(),
|
|
327
|
+
syncSessions: false,
|
|
328
|
+
...(include ? { selection: selectionForSnapshot(include) } : {}),
|
|
329
|
+
files,
|
|
330
|
+
};
|
|
358
331
|
}
|
|
359
332
|
|
|
360
333
|
export function scanSnapshot(snapshot: Snapshot) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
334
|
+
const findings: string[] = [];
|
|
335
|
+
for (const file of snapshot.files) {
|
|
336
|
+
const content = Buffer.from(file.contentBase64, "base64");
|
|
337
|
+
if (content.includes(0)) continue;
|
|
338
|
+
const text = content.toString("utf8");
|
|
339
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
340
|
+
if (pattern.test(text)) {
|
|
341
|
+
findings.push(file.path);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return findings;
|
|
374
347
|
}
|
|
375
348
|
|
|
376
|
-
export function mergeRemotePreservedFiles(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
machine: os.hostname(),
|
|
404
|
-
syncSessions: snapshotIncludesSessions(local) || snapshotIncludesSessions(remote),
|
|
405
|
-
files: [...local.files, ...preserved].sort((left, right) =>
|
|
406
|
-
left.path.localeCompare(right.path),
|
|
407
|
-
),
|
|
408
|
-
};
|
|
349
|
+
export function mergeRemotePreservedFiles(local: Snapshot, remote: Snapshot, config: SyncSelectionConfig) {
|
|
350
|
+
const localPathNames = new Set(local.files.map((file) => file.path.toLowerCase()));
|
|
351
|
+
const preservedPathNames = new Set<string>();
|
|
352
|
+
const preserved = remote.files.filter((file) => {
|
|
353
|
+
const normalized = toPosix(file.path);
|
|
354
|
+
const lower = normalized.toLowerCase();
|
|
355
|
+
if (
|
|
356
|
+
localPathNames.has(lower) ||
|
|
357
|
+
preservedPathNames.has(lower) ||
|
|
358
|
+
!isSafeSnapshotPath(file.path) ||
|
|
359
|
+
isConfiguredSnapshotPath(normalized, config) ||
|
|
360
|
+
!isPreservableUnmanagedSnapshotPath(normalized)
|
|
361
|
+
) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
preservedPathNames.add(lower);
|
|
365
|
+
return true;
|
|
366
|
+
});
|
|
367
|
+
if (preserved.length === 0) return local;
|
|
368
|
+
return {
|
|
369
|
+
...local,
|
|
370
|
+
id: snapshotId(),
|
|
371
|
+
createdAt: new Date().toISOString(),
|
|
372
|
+
machine: os.hostname(),
|
|
373
|
+
syncSessions: snapshotIncludesSessions(local) || snapshotIncludesSessions(remote),
|
|
374
|
+
files: [...local.files, ...preserved].sort((left, right) => left.path.localeCompare(right.path)),
|
|
375
|
+
};
|
|
409
376
|
}
|
|
410
377
|
|
|
411
378
|
export function mergeRemoteSessionFiles(local: Snapshot, remote: Snapshot) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
379
|
+
const remoteSessions = remote.files.filter((file) => {
|
|
380
|
+
const normalized = toPosix(file.path);
|
|
381
|
+
return isSessionFilePath(normalized) && isSafeSnapshotPath(file.path);
|
|
382
|
+
});
|
|
383
|
+
if (remoteSessions.length === 0 && !snapshotIncludesSessions(remote)) return local;
|
|
384
|
+
const localInclude = snapshotSelectionInclude(local);
|
|
385
|
+
return {
|
|
386
|
+
...local,
|
|
387
|
+
id: snapshotId(),
|
|
388
|
+
createdAt: new Date().toISOString(),
|
|
389
|
+
machine: os.hostname(),
|
|
390
|
+
syncSessions: true,
|
|
391
|
+
...(localInclude
|
|
392
|
+
? {
|
|
393
|
+
selection: selectionForSnapshot(
|
|
394
|
+
localInclude.includes("sessions") ? localInclude : [...localInclude, "sessions"],
|
|
395
|
+
),
|
|
396
|
+
}
|
|
397
|
+
: {}),
|
|
398
|
+
files: [...local.files.filter((file) => !isSessionPath(file.path)), ...remoteSessions].sort((left, right) =>
|
|
399
|
+
left.path.localeCompare(right.path),
|
|
400
|
+
),
|
|
401
|
+
};
|
|
435
402
|
}
|
|
436
403
|
|
|
437
404
|
function sameOptionalInclude(left: string[] | undefined, right: string[] | undefined) {
|
|
438
|
-
|
|
439
|
-
|
|
405
|
+
if (!left || !right) return left === right;
|
|
406
|
+
return left.length === right.length && left.every((item, index) => item === right[index]);
|
|
440
407
|
}
|