@narumitw/pi-sync 0.49.15 → 0.50.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 +49 -16
- package/dist/chunks/chunk-2B2ELUF5.ts +29 -0
- package/dist/chunks/chunk-2B2ELUF5.ts.map +7 -0
- package/dist/chunks/chunk-6LP3MCR2.ts +115 -0
- package/dist/chunks/chunk-6LP3MCR2.ts.map +7 -0
- package/dist/chunks/chunk-6UHKGALE.ts +134 -0
- package/dist/chunks/chunk-6UHKGALE.ts.map +7 -0
- package/dist/chunks/{chunk-JF6MZCT4.ts → chunk-AUSDLD6F.ts} +4 -4
- package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
- package/dist/chunks/{chunk-X7SQ2EQM.ts → chunk-AXXS2BPA.ts} +9 -9
- package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
- package/dist/chunks/{chunk-5T442OP3.ts → chunk-C7XY5NF5.ts} +210 -373
- package/dist/chunks/chunk-C7XY5NF5.ts.map +7 -0
- package/dist/chunks/{chunk-4U6AFLJB.ts → chunk-CHH26D4F.ts} +3 -2
- package/dist/chunks/chunk-CHH26D4F.ts.map +7 -0
- package/dist/chunks/chunk-CPEHGXJV.ts +50 -0
- package/dist/chunks/chunk-CPEHGXJV.ts.map +7 -0
- package/dist/chunks/{chunk-5MQ76BK5.ts → chunk-D5X27LCA.ts} +2 -2
- package/dist/chunks/chunk-D5X27LCA.ts.map +7 -0
- package/dist/chunks/{chunk-WSOBYHIZ.ts → chunk-GWMHAQG4.ts} +4 -4
- package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
- package/dist/chunks/{chunk-5LZALPCD.ts → chunk-HUDQATCL.ts} +25 -25
- package/dist/chunks/chunk-HUDQATCL.ts.map +7 -0
- package/dist/chunks/{chunk-YQ6UW7IF.ts → chunk-JAAKLL6M.ts} +2 -2
- package/dist/chunks/chunk-JAAKLL6M.ts.map +7 -0
- package/dist/chunks/chunk-JSYMGJQQ.ts +51 -0
- package/dist/chunks/chunk-JSYMGJQQ.ts.map +7 -0
- package/dist/chunks/chunk-L7TBO32M.ts +25 -0
- package/dist/chunks/chunk-L7TBO32M.ts.map +7 -0
- package/dist/chunks/{chunk-DFXBKHGN.ts → chunk-LCRI3ABW.ts} +21 -8
- package/dist/chunks/chunk-LCRI3ABW.ts.map +7 -0
- package/dist/chunks/chunk-LTBWGSBV.ts +12 -0
- package/dist/chunks/chunk-LTBWGSBV.ts.map +7 -0
- package/dist/chunks/chunk-NJFUIVYL.ts +17 -0
- package/dist/chunks/chunk-NJFUIVYL.ts.map +7 -0
- package/dist/chunks/{chunk-HVUSHMVL.ts → chunk-OEX4ABY6.ts} +39 -37
- package/dist/chunks/chunk-OEX4ABY6.ts.map +7 -0
- package/dist/chunks/chunk-OUHGBFYF.ts +1255 -0
- package/dist/chunks/chunk-OUHGBFYF.ts.map +7 -0
- package/dist/chunks/{chunk-HGV24P4T.ts → chunk-Q2TUM5X6.ts} +3 -3
- package/dist/chunks/chunk-Q2TUM5X6.ts.map +7 -0
- package/dist/chunks/{chunk-GLQLN5GV.ts → chunk-SL64RLXR.ts} +8 -6
- package/dist/chunks/chunk-SL64RLXR.ts.map +7 -0
- package/dist/chunks/chunk-T3HH3UEJ.ts +96 -0
- package/dist/chunks/chunk-T3HH3UEJ.ts.map +7 -0
- package/dist/chunks/chunk-TEN2ZBJV.ts +101 -0
- package/dist/chunks/chunk-TEN2ZBJV.ts.map +7 -0
- package/dist/chunks/chunk-THMF4BVL.ts +18 -0
- package/dist/chunks/chunk-THMF4BVL.ts.map +7 -0
- package/dist/chunks/{chunk-44LPGQTI.ts → chunk-X5OM6GXT.ts} +9 -7
- package/dist/chunks/chunk-X5OM6GXT.ts.map +7 -0
- package/dist/chunks/chunk-XBX4HBLG.ts +57 -0
- package/dist/chunks/chunk-XBX4HBLG.ts.map +7 -0
- package/dist/chunks/{chunk-ARN2672F.ts → chunk-YKJHPTGO.ts} +23 -55
- package/dist/chunks/chunk-YKJHPTGO.ts.map +7 -0
- package/dist/chunks/chunk-Z4DQF6L3.ts +12 -0
- package/dist/chunks/chunk-Z4DQF6L3.ts.map +7 -0
- package/dist/chunks/{chunk-F5QL2GPY.ts → chunk-ZI5MXMEX.ts} +5 -99
- package/dist/chunks/chunk-ZI5MXMEX.ts.map +7 -0
- package/dist/chunks/{file-selection-RKEHALNQ.ts → file-selection-FTTKAOYX.ts} +10 -7
- package/dist/chunks/file-selection-FTTKAOYX.ts.map +7 -0
- package/dist/chunks/{git-backend-E3UOUUOM.ts → git-backend-OQW46G2X.ts} +42 -17
- package/dist/chunks/git-backend-OQW46G2X.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-U23ODD32.ts +22 -0
- package/dist/chunks/manager-ui-ABATHC36.ts +1680 -0
- package/dist/chunks/manager-ui-ABATHC36.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-ZE3TY67S.ts +21 -0
- package/dist/chunks/{s3-backend-552LXHE4.ts → s3-backend-3P33LQHU.ts} +96 -18
- package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
- package/dist/chunks/{setup-switch-5YDG5YGJ.ts → setup-switch-IS2PEZ3J.ts} +9 -9
- package/dist/chunks/setup-wizard-VB3VDOV5.ts +89 -0
- package/dist/chunks/setup-wizard-VB3VDOV5.ts.map +7 -0
- package/dist/chunks/{snapshot-2RYBUDRA.ts → snapshot-HKEWCVK6.ts} +7 -6
- package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
- package/dist/chunks/{sync-operations-3LLIHWZO.ts → sync-operations-ONJ27QRL.ts} +374 -294
- package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
- package/dist/chunks/{sync-state-SRN6YRBK.ts → sync-state-Q2FTXTW6.ts} +6 -6
- package/dist/chunks/sync-state-Q2FTXTW6.ts.map +7 -0
- package/dist/chunks/{webdav-backend-ZD5RHRIU.ts → webdav-backend-ECIKD7SO.ts} +26 -13
- package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +7 -0
- package/dist/index.ts +477 -342
- package/dist/index.ts.map +4 -4
- package/docs/settings.md +30 -6
- package/package.json +1 -1
- package/src/{backend-factory.ts → backends/backend-factory.ts} +4 -4
- package/src/backends/backend-identity.ts +40 -0
- package/src/backends/backend-types.ts +81 -0
- package/src/{git-backend.ts → backends/git/git-backend.ts} +43 -19
- package/src/{git-config.ts → backends/git/git-config.ts} +1 -0
- package/src/{git-storage.ts → backends/git/git-storage.ts} +2 -2
- package/src/{s3-backend.ts → backends/s3/s3-backend.ts} +45 -9
- package/src/{s3-client.ts → backends/s3/s3-client.ts} +56 -8
- package/src/{sync-backend.ts → backends/sync-backend.ts} +1 -1
- package/src/{webdav-backend.ts → backends/webdav/webdav-backend.ts} +22 -10
- package/src/{webdav-client.ts → backends/webdav/webdav-client.ts} +1 -1
- package/src/{webdav-config.ts → backends/webdav/webdav-config.ts} +1 -0
- package/src/commands/command-execution.ts +330 -0
- package/src/commands/command-handler.ts +201 -0
- package/src/commands/command-types.ts +18 -0
- package/src/{command.ts → commands/command.ts} +2 -1
- package/src/commands/setup-completions.ts +6 -0
- package/src/settings/config.ts +238 -0
- package/src/{settings-management.ts → settings/settings-management.ts} +39 -14
- package/src/settings/settings-store.ts +109 -0
- package/src/settings/settings-types.ts +128 -0
- package/src/settings/settings-validation.ts +443 -0
- package/src/snapshot/session-paths.ts +84 -0
- package/src/{snapshot-apply.ts → snapshot/snapshot-apply.ts} +3 -3
- package/src/{snapshot-codec.ts → snapshot/snapshot-codec.ts} +2 -2
- package/src/{snapshot-transaction.ts → snapshot/snapshot-transaction.ts} +5 -4
- package/src/snapshot/snapshot-types.ts +38 -0
- package/src/{snapshot.ts → snapshot/snapshot.ts} +39 -20
- package/src/state/json-file.ts +20 -0
- package/src/{lock.ts → state/lock.ts} +3 -2
- package/src/{operation-availability.ts → state/operation-availability.ts} +2 -2
- package/src/state/state-types.ts +20 -0
- package/src/state/sync-state-store.ts +62 -0
- package/src/sync/automatic-sync.ts +103 -0
- package/src/{remote-snapshot.ts → sync/remote-snapshot.ts} +27 -4
- package/src/{setup-switch.ts → sync/setup-switch.ts} +6 -7
- package/src/sync/signals.ts +10 -0
- package/src/sync/startup-check.ts +128 -0
- package/src/{sync-decision.ts → sync/sync-decision.ts} +6 -3
- package/src/sync/sync-error-guidance.ts +53 -0
- package/src/sync/sync-inspection.ts +51 -0
- package/src/sync/sync-loaders.ts +54 -0
- package/src/{sync-operations.ts → sync/sync-mutations.ts} +34 -325
- package/src/sync/sync-operations.ts +12 -0
- package/src/{sync-policy.ts → sync/sync-policy.ts} +2 -2
- package/src/sync/sync-queries.ts +235 -0
- package/src/{sync-state.ts → sync/sync-state.ts} +5 -4
- package/src/sync-extension.ts +64 -708
- package/src/sync.ts +8 -8
- package/src/types.ts +47 -286
- package/src/ui/automatic-sync-summary.ts +6 -0
- package/src/{cancellable-operation.ts → ui/cancellable-operation.ts} +4 -4
- package/src/{file-selection.ts → ui/file-selection.ts} +5 -3
- package/src/{manager-attention.ts → ui/manager-attention.ts} +5 -1
- package/src/{manager-recovery.ts → ui/manager-recovery.ts} +6 -2
- package/src/{manager-state.ts → ui/manager-state.ts} +35 -14
- package/src/ui/manager-ui.ts +386 -0
- package/src/{remote-selection-ui.ts → ui/remote-selection-ui.ts} +21 -13
- package/src/{secret-input.ts → ui/secret-input.ts} +22 -4
- package/src/{settings-ui.ts → ui/settings-ui.ts} +17 -13
- package/src/ui/setup/git-ui.ts +293 -0
- package/src/{s3-credentials-ui.ts → ui/setup/s3-credentials-ui.ts} +13 -21
- package/src/ui/setup/s3-ui.ts +217 -0
- package/src/ui/setup/setup-actions.ts +127 -0
- package/src/ui/setup/setup-location-ui.ts +167 -0
- package/src/ui/setup/setup-name-ui.ts +34 -0
- package/src/ui/setup/setup-prompts.ts +92 -0
- package/src/ui/setup/setup-review.ts +70 -0
- package/src/ui/setup/setup-switcher.ts +181 -0
- package/src/ui/setup/setup-wizard.ts +39 -0
- package/src/ui/setup/text-input.ts +78 -0
- package/src/ui/setup/webdav-ui.ts +330 -0
- package/src/{storage-connections-ui.ts → ui/storage-connections-ui.ts} +99 -69
- package/src/{sync-attention.ts → ui/sync-attention.ts} +75 -6
- package/src/{sync-format.ts → ui/sync-format.ts} +7 -13
- package/src/{sync-resolution-ui.ts → ui/sync-resolution-ui.ts} +4 -3
- package/src/{sync-setups-ui.ts → ui/sync-setups-ui.ts} +23 -10
- package/src/ui/terminal-text.ts +4 -0
- package/dist/chunks/chunk-44LPGQTI.ts.map +0 -7
- package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
- package/dist/chunks/chunk-5LZALPCD.ts.map +0 -7
- package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
- package/dist/chunks/chunk-5T442OP3.ts.map +0 -7
- package/dist/chunks/chunk-ARN2672F.ts.map +0 -7
- package/dist/chunks/chunk-C7LXZ3UH.ts +0 -167
- package/dist/chunks/chunk-C7LXZ3UH.ts.map +0 -7
- package/dist/chunks/chunk-DFXBKHGN.ts.map +0 -7
- package/dist/chunks/chunk-E4COSV32.ts +0 -82
- package/dist/chunks/chunk-E4COSV32.ts.map +0 -7
- package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
- package/dist/chunks/chunk-GLQLN5GV.ts.map +0 -7
- package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
- package/dist/chunks/chunk-HVUSHMVL.ts.map +0 -7
- package/dist/chunks/chunk-JF6MZCT4.ts.map +0 -7
- package/dist/chunks/chunk-WSOBYHIZ.ts.map +0 -7
- package/dist/chunks/chunk-X7SQ2EQM.ts.map +0 -7
- package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
- package/dist/chunks/file-selection-RKEHALNQ.ts.map +0 -7
- package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-LWPYJAD5.ts +0 -18
- package/dist/chunks/manager-ui-NF4RUHBY.ts +0 -2785
- package/dist/chunks/manager-ui-NF4RUHBY.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-NUF5P4ZJ.ts +0 -17
- package/dist/chunks/s3-backend-552LXHE4.ts.map +0 -7
- package/dist/chunks/sync-operations-3LLIHWZO.ts.map +0 -7
- package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
- package/src/config.ts +0 -923
- package/src/git-ui.ts +0 -307
- package/src/manager-helpers.ts +0 -64
- package/src/manager-ui.ts +0 -997
- package/src/webdav-ui.ts +0 -421
- /package/dist/chunks/{manager-result-dispatcher-LWPYJAD5.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-NUF5P4ZJ.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-5YDG5YGJ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
- /package/dist/chunks/{snapshot-2RYBUDRA.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
- /package/dist/chunks/{sync-state-SRN6YRBK.ts.map → sync-inspection-FEP4L5TR.ts.map} +0 -0
- /package/src/{git-runner.ts → backends/git/git-runner.ts} +0 -0
- /package/src/{config-errors.ts → settings/config-errors.ts} +0 -0
- /package/src/{config-file.ts → settings/config-file.ts} +0 -0
- /package/src/{snapshot-paths.ts → snapshot/snapshot-paths.ts} +0 -0
- /package/src/{lock-policy.ts → state/lock-policy.ts} +0 -0
- /package/src/{lockfile-fs.ts → state/lockfile-fs.ts} +0 -0
- /package/src/{state-directory.ts → state/state-directory.ts} +0 -0
- /package/src/{sync-errors.ts → sync/sync-errors.ts} +0 -0
- /package/src/{manager-result-dispatcher.ts → ui/manager-result-dispatcher.ts} +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ResolvedS3Backend, ResolvedSyncBackend } from "../backends/backend-types.js";
|
|
2
|
+
export type StorageConnectionType = "s3" | "git" | "webdav";
|
|
3
|
+
export type OnSwitchAction = "ask-before-pull" | "pull-after-switch" | "switch-only";
|
|
4
|
+
|
|
5
|
+
export interface S3CredentialsSettings {
|
|
6
|
+
accessKeyId: string;
|
|
7
|
+
secretAccessKey: string;
|
|
8
|
+
sessionToken?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WebDavCredentialsSettings {
|
|
13
|
+
username: string;
|
|
14
|
+
password: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface S3StorageConnectionSettings {
|
|
19
|
+
type: "s3";
|
|
20
|
+
endpoint: string;
|
|
21
|
+
region: string;
|
|
22
|
+
credentials: S3CredentialsSettings;
|
|
23
|
+
remote?: never;
|
|
24
|
+
url?: never;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GitStorageConnectionSettings {
|
|
29
|
+
type: "git";
|
|
30
|
+
remote: string;
|
|
31
|
+
endpoint?: never;
|
|
32
|
+
region?: never;
|
|
33
|
+
credentials?: never;
|
|
34
|
+
url?: never;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WebDavStorageConnectionSettings {
|
|
39
|
+
type: "webdav";
|
|
40
|
+
url: string;
|
|
41
|
+
credentials: WebDavCredentialsSettings;
|
|
42
|
+
endpoint?: never;
|
|
43
|
+
region?: never;
|
|
44
|
+
remote?: never;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type StorageConnectionSettings =
|
|
49
|
+
| S3StorageConnectionSettings
|
|
50
|
+
| GitStorageConnectionSettings
|
|
51
|
+
| WebDavStorageConnectionSettings;
|
|
52
|
+
|
|
53
|
+
export interface CommonSyncSetupStorageSettings {
|
|
54
|
+
connection: string;
|
|
55
|
+
path: string;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface S3SyncSetupStorageSettings extends CommonSyncSetupStorageSettings {
|
|
60
|
+
bucket: string;
|
|
61
|
+
branch?: never;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GitSyncSetupStorageSettings extends CommonSyncSetupStorageSettings {
|
|
65
|
+
branch: string;
|
|
66
|
+
bucket?: never;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface WebDavSyncSetupStorageSettings extends CommonSyncSetupStorageSettings {
|
|
70
|
+
bucket?: never;
|
|
71
|
+
branch?: never;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type SyncSetupStorageSettings =
|
|
75
|
+
| S3SyncSetupStorageSettings
|
|
76
|
+
| GitSyncSetupStorageSettings
|
|
77
|
+
| WebDavSyncSetupStorageSettings;
|
|
78
|
+
|
|
79
|
+
export interface SyncPolicySettings {
|
|
80
|
+
include: string[];
|
|
81
|
+
automatic: boolean;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SyncSetupSettings {
|
|
86
|
+
storage: SyncSetupStorageSettings;
|
|
87
|
+
sync: SyncPolicySettings;
|
|
88
|
+
[key: string]: unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface PiSyncSettingsV3 {
|
|
92
|
+
version: 3;
|
|
93
|
+
activeSyncSetup?: string;
|
|
94
|
+
onSwitch: OnSwitchAction;
|
|
95
|
+
skipSecretScan?: boolean;
|
|
96
|
+
storageConnections: Record<string, StorageConnectionSettings>;
|
|
97
|
+
syncSetups: Record<string, SyncSetupSettings>;
|
|
98
|
+
[key: string]: unknown;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SyncConfig<Backend extends ResolvedSyncBackend = ResolvedS3Backend> {
|
|
102
|
+
setupName: string;
|
|
103
|
+
connectionName: string;
|
|
104
|
+
storagePath: string;
|
|
105
|
+
/** Snapshot/wire identity retained behind the settings normalization boundary. */
|
|
106
|
+
snapshotIdentity: string;
|
|
107
|
+
include: string[];
|
|
108
|
+
automatic: boolean;
|
|
109
|
+
onSwitch: OnSwitchAction;
|
|
110
|
+
skipSecretScan: boolean;
|
|
111
|
+
backend: Backend;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type AnySyncConfig = SyncConfig<ResolvedSyncBackend>;
|
|
115
|
+
export type CommonSyncConfig = Omit<AnySyncConfig, "backend">;
|
|
116
|
+
|
|
117
|
+
/** UI projection over a fully validated v3 setup; it is never persisted directly. */
|
|
118
|
+
export interface PartialConfig {
|
|
119
|
+
setupName: string;
|
|
120
|
+
connectionName: string;
|
|
121
|
+
storageKind: StorageConnectionType;
|
|
122
|
+
storagePath: string;
|
|
123
|
+
include: string[];
|
|
124
|
+
automatic: boolean;
|
|
125
|
+
onSwitch: OnSwitchAction;
|
|
126
|
+
bucket?: string;
|
|
127
|
+
branch?: string;
|
|
128
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { normalizeEndpointIdentity } from "../backends/backend-identity.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeGitBranch,
|
|
5
|
+
normalizeGitDirectory,
|
|
6
|
+
normalizeGitRemote,
|
|
7
|
+
normalizeGitRemoteIdentity,
|
|
8
|
+
} from "../backends/git/git-config.js";
|
|
9
|
+
import {
|
|
10
|
+
normalizeWebDavIdentityUrl,
|
|
11
|
+
normalizeWebDavPath,
|
|
12
|
+
normalizeWebDavUrl,
|
|
13
|
+
validateWebDavCredentials,
|
|
14
|
+
} from "../backends/webdav/webdav-config.js";
|
|
15
|
+
import { normalizeSyncInclude } from "../sync/sync-policy.js";
|
|
16
|
+
import { localConfigPath } from "./config-file.js";
|
|
17
|
+
import type {
|
|
18
|
+
OnSwitchAction,
|
|
19
|
+
PiSyncSettingsV3,
|
|
20
|
+
StorageConnectionSettings,
|
|
21
|
+
SyncSetupSettings,
|
|
22
|
+
} from "./settings-types.js";
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_ON_SWITCH: OnSwitchAction = "ask-before-pull";
|
|
25
|
+
|
|
26
|
+
export function normalizeOnSwitch(value: unknown): OnSwitchAction {
|
|
27
|
+
if (value === "ask-before-pull" || value === "pull-after-switch" || value === "switch-only") {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Invalid pi-sync settings: onSwitch must be "ask-before-pull", "pull-after-switch", or "switch-only".',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateSettingsDocument(value: Record<string, unknown>): PiSyncSettingsV3 {
|
|
36
|
+
if (value.version !== 3) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Unsupported pi-sync settings: version 3 is required. Keep the existing file for recovery, then create a new version 3 ${path.basename(localConfigPath())}; pi-sync will not migrate or overwrite old settings.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
rejectLegacyFields(
|
|
42
|
+
value,
|
|
43
|
+
[
|
|
44
|
+
"profiles",
|
|
45
|
+
"targets",
|
|
46
|
+
"activeTarget",
|
|
47
|
+
"targetSwitchAction",
|
|
48
|
+
"endpoint",
|
|
49
|
+
"bucket",
|
|
50
|
+
"region",
|
|
51
|
+
"accessKeyId",
|
|
52
|
+
"secretAccessKey",
|
|
53
|
+
"sessionToken",
|
|
54
|
+
"profile",
|
|
55
|
+
"prefix",
|
|
56
|
+
"autoSync",
|
|
57
|
+
"syncFiles",
|
|
58
|
+
"syncSessions",
|
|
59
|
+
"extraFiles",
|
|
60
|
+
],
|
|
61
|
+
"top level",
|
|
62
|
+
);
|
|
63
|
+
normalizeOnSwitch(value.onSwitch);
|
|
64
|
+
if (value.skipSecretScan !== undefined && typeof value.skipSecretScan !== "boolean") {
|
|
65
|
+
throw new Error("Invalid pi-sync settings: skipSecretScan must be boolean.");
|
|
66
|
+
}
|
|
67
|
+
const storageConnections = requireNamedObjectMap(
|
|
68
|
+
value.storageConnections,
|
|
69
|
+
"storageConnections",
|
|
70
|
+
"storage connection",
|
|
71
|
+
);
|
|
72
|
+
const syncSetups = requireNamedObjectMap(value.syncSetups, "syncSetups", "sync setup");
|
|
73
|
+
for (const name of Object.keys(storageConnections)) {
|
|
74
|
+
validateStorageConnection(
|
|
75
|
+
name,
|
|
76
|
+
requireOwnObject(storageConnections, name, "storage connection") as Record<string, unknown>,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
for (const name of Object.keys(syncSetups)) {
|
|
80
|
+
validateSyncSetup(
|
|
81
|
+
name,
|
|
82
|
+
requireOwnObject(syncSetups, name, "sync setup") as Record<string, unknown>,
|
|
83
|
+
storageConnections,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const names = Object.keys(syncSetups);
|
|
87
|
+
const activeSyncSetup = optionalCanonicalReference(value.activeSyncSetup, "activeSyncSetup");
|
|
88
|
+
if (names.length === 0) {
|
|
89
|
+
if (activeSyncSetup !== undefined) {
|
|
90
|
+
throw new Error("Invalid pi-sync settings: empty syncSetups cannot have activeSyncSetup.");
|
|
91
|
+
}
|
|
92
|
+
} else if (!activeSyncSetup || !Object.hasOwn(syncSetups, activeSyncSetup)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Invalid pi-sync settings: activeSyncSetup must reference an existing own-property sync setup.",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
validateUniqueRemoteSyncSetups(syncSetups, storageConnections);
|
|
98
|
+
return value as PiSyncSettingsV3;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateStorageConnection(name: string, value: Record<string, unknown>) {
|
|
102
|
+
rejectLegacyFields(
|
|
103
|
+
value,
|
|
104
|
+
["kind", "accessKeyId", "secretAccessKey", "sessionToken", "username", "password"],
|
|
105
|
+
`storage connection “${name}”`,
|
|
106
|
+
);
|
|
107
|
+
const type = requiredString(value.type, `storage connection “${name}” type`);
|
|
108
|
+
if (type !== "s3" && type !== "git" && type !== "webdav") {
|
|
109
|
+
throw new Error(`Invalid pi-sync settings: storage connection “${name}” has unsupported type.`);
|
|
110
|
+
}
|
|
111
|
+
const known = ["endpoint", "region", "remote", "url", "credentials"];
|
|
112
|
+
const allowed =
|
|
113
|
+
type === "s3"
|
|
114
|
+
? new Set(["endpoint", "region", "credentials"])
|
|
115
|
+
: type === "git"
|
|
116
|
+
? new Set(["remote"])
|
|
117
|
+
: new Set(["url", "credentials"]);
|
|
118
|
+
if (known.some((field) => Object.hasOwn(value, field) && !allowed.has(field))) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Invalid pi-sync settings: ${type.toUpperCase()} storage connection “${name}” mixes backend fields.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (type === "git") {
|
|
124
|
+
if (!normalizeGitRemote(requiredString(value.remote, `Git remote for “${name}”`))) {
|
|
125
|
+
throw new Error(`Invalid pi-sync settings: Git remote for “${name}” is required.`);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const credentials = requireRecord(
|
|
130
|
+
value.credentials,
|
|
131
|
+
`credentials for storage connection “${name}”`,
|
|
132
|
+
);
|
|
133
|
+
if (type === "webdav") {
|
|
134
|
+
normalizeWebDavUrl(requiredString(value.url, `WebDAV URL for “${name}”`));
|
|
135
|
+
const username = requiredString(credentials.username, `WebDAV username for “${name}”`);
|
|
136
|
+
const password = requiredSecret(credentials.password, `WebDAV password for “${name}”`);
|
|
137
|
+
if (
|
|
138
|
+
["accessKeyId", "secretAccessKey", "sessionToken"].some((field) =>
|
|
139
|
+
Object.hasOwn(credentials, field),
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
142
|
+
throw new Error(`Invalid pi-sync settings: WebDAV credentials for “${name}” mix fields.`);
|
|
143
|
+
}
|
|
144
|
+
validateWebDavCredentials(username, password);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
normalizeS3Endpoint(requiredString(value.endpoint, `S3 endpoint for “${name}”`));
|
|
148
|
+
requiredString(value.region, `S3 region for “${name}”`);
|
|
149
|
+
requiredString(credentials.accessKeyId, `S3 access key id for “${name}”`);
|
|
150
|
+
requiredSecret(credentials.secretAccessKey, `S3 secret access key for “${name}”`);
|
|
151
|
+
optionalString(credentials.sessionToken, `S3 session token for “${name}”`);
|
|
152
|
+
if (["username", "password"].some((field) => Object.hasOwn(credentials, field))) {
|
|
153
|
+
throw new Error(`Invalid pi-sync settings: S3 credentials for “${name}” mix fields.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validateSyncSetup(
|
|
158
|
+
name: string,
|
|
159
|
+
value: Record<string, unknown>,
|
|
160
|
+
connections: Record<string, unknown>,
|
|
161
|
+
) {
|
|
162
|
+
rejectLegacyFields(
|
|
163
|
+
value,
|
|
164
|
+
[
|
|
165
|
+
"profile",
|
|
166
|
+
"bucket",
|
|
167
|
+
"branch",
|
|
168
|
+
"path",
|
|
169
|
+
"prefix",
|
|
170
|
+
"directory",
|
|
171
|
+
"namespace",
|
|
172
|
+
"autoSync",
|
|
173
|
+
"syncFiles",
|
|
174
|
+
"syncSessions",
|
|
175
|
+
"extraFiles",
|
|
176
|
+
],
|
|
177
|
+
`sync setup “${name}”`,
|
|
178
|
+
);
|
|
179
|
+
const storage = requireRecord(value.storage, `storage for sync setup “${name}”`);
|
|
180
|
+
const sync = requireRecord(value.sync, `sync policy for sync setup “${name}”`);
|
|
181
|
+
rejectLegacyFields(
|
|
182
|
+
storage,
|
|
183
|
+
["profile", "prefix", "directory", "namespace"],
|
|
184
|
+
`storage for sync setup “${name}”`,
|
|
185
|
+
);
|
|
186
|
+
rejectLegacyFields(
|
|
187
|
+
sync,
|
|
188
|
+
["autoSync", "syncFiles", "syncSessions", "extraFiles"],
|
|
189
|
+
`sync policy for sync setup “${name}”`,
|
|
190
|
+
);
|
|
191
|
+
const connectionName = requiredCanonicalReference(
|
|
192
|
+
storage.connection,
|
|
193
|
+
`storage connection reference for sync setup “${name}”`,
|
|
194
|
+
);
|
|
195
|
+
validateConfigName(connectionName, "storage connection reference");
|
|
196
|
+
const connection = ownObject<Record<string, unknown>>(connections, connectionName);
|
|
197
|
+
if (!connection) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Invalid pi-sync settings: sync setup “${name}” references missing storage connection “${connectionName}”.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const type = connection.type;
|
|
203
|
+
normalizeStoragePath(requiredString(storage.path, `storage path for sync setup “${name}”`));
|
|
204
|
+
if (type === "s3") {
|
|
205
|
+
normalizeS3Bucket(requiredString(storage.bucket, `S3 bucket for sync setup “${name}”`));
|
|
206
|
+
if (Object.hasOwn(storage, "branch")) mixedSetupError("S3", name);
|
|
207
|
+
} else if (type === "git") {
|
|
208
|
+
normalizeGitBranch(requiredString(storage.branch, `Git branch for sync setup “${name}”`));
|
|
209
|
+
if (Object.hasOwn(storage, "bucket")) mixedSetupError("Git", name);
|
|
210
|
+
} else if (type === "webdav") {
|
|
211
|
+
if (Object.hasOwn(storage, "bucket") || Object.hasOwn(storage, "branch")) {
|
|
212
|
+
mixedSetupError("WebDAV", name);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!Object.hasOwn(sync, "include")) {
|
|
216
|
+
throw new Error(`Invalid pi-sync settings: sync setup “${name}” is missing sync.include.`);
|
|
217
|
+
}
|
|
218
|
+
normalizeSyncInclude(sync.include);
|
|
219
|
+
if (typeof sync.automatic !== "boolean") {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Invalid pi-sync settings: sync setup “${name}” sync.automatic must be boolean.`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function mixedSetupError(type: string, name: string): never {
|
|
227
|
+
throw new Error(`Invalid pi-sync settings: ${type} sync setup “${name}” mixes backend fields.`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function validateUniqueRemoteSyncSetups(
|
|
231
|
+
setups: Record<string, unknown>,
|
|
232
|
+
connections: Record<string, unknown>,
|
|
233
|
+
) {
|
|
234
|
+
const identities = new Map<string, string>();
|
|
235
|
+
for (const name of Object.keys(setups)) {
|
|
236
|
+
const setup = requireOwnObject(setups, name, "sync setup") as unknown as SyncSetupSettings;
|
|
237
|
+
const connection = requireOwnObject(
|
|
238
|
+
connections,
|
|
239
|
+
setup.storage.connection,
|
|
240
|
+
"storage connection",
|
|
241
|
+
) as unknown as StorageConnectionSettings;
|
|
242
|
+
const identity = effectiveSyncSetupRemoteIdentity(setup, connection);
|
|
243
|
+
const existing = identities.get(identity);
|
|
244
|
+
if (existing) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Invalid pi-sync settings: sync setups “${existing}” and “${name}” use the same normalized remote location.`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
identities.set(identity, name);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function effectiveSyncSetupRemoteIdentity(
|
|
254
|
+
setup: SyncSetupSettings,
|
|
255
|
+
connection: StorageConnectionSettings,
|
|
256
|
+
) {
|
|
257
|
+
const storagePath = normalizeStoragePath(setup.storage.path);
|
|
258
|
+
if (connection.type === "git") {
|
|
259
|
+
return JSON.stringify([
|
|
260
|
+
"git",
|
|
261
|
+
normalizeGitRemoteIdentity(connection.remote),
|
|
262
|
+
normalizeGitBranch(setup.storage.branch),
|
|
263
|
+
normalizeGitDirectory(storagePath),
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
if (connection.type === "webdav") {
|
|
267
|
+
return JSON.stringify([
|
|
268
|
+
"webdav",
|
|
269
|
+
normalizeWebDavIdentityUrl(connection.url),
|
|
270
|
+
connection.credentials.username.trim(),
|
|
271
|
+
normalizeWebDavPath(storagePath),
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
return JSON.stringify([
|
|
275
|
+
"s3",
|
|
276
|
+
normalizeEndpointIdentity(connection.endpoint),
|
|
277
|
+
normalizeS3Bucket(setup.storage.bucket),
|
|
278
|
+
storagePath,
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function rejectLegacyFields(
|
|
283
|
+
value: Record<string, unknown>,
|
|
284
|
+
fields: readonly string[],
|
|
285
|
+
context: string,
|
|
286
|
+
) {
|
|
287
|
+
const field = fields.find((candidate) => Object.hasOwn(value, candidate));
|
|
288
|
+
if (field) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`Invalid pi-sync settings: ${context} contains unsupported version 1/2 field “${field}”.`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function requireNamedObjectMap(value: unknown, field: string, itemLabel: string) {
|
|
296
|
+
const result = requireRecord(value, field);
|
|
297
|
+
for (const name of Object.keys(result)) validateConfigName(name, itemLabel);
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function requireOwnObject(value: Record<string, unknown>, key: string, label: string) {
|
|
302
|
+
const item = ownObject(value, key);
|
|
303
|
+
if (!item) throw new Error(`Invalid pi-sync settings: ${label} “${key}” must be an object.`);
|
|
304
|
+
return item;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function ownObject<T extends object>(
|
|
308
|
+
value: Record<string, unknown>,
|
|
309
|
+
key: string,
|
|
310
|
+
): T | undefined {
|
|
311
|
+
if (!Object.hasOwn(value, key)) return undefined;
|
|
312
|
+
const item = value[key];
|
|
313
|
+
return item && typeof item === "object" && !Array.isArray(item) ? (item as T) : undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function requireRecord(value: unknown, field: string): Record<string, unknown> {
|
|
317
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
318
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be an object.`);
|
|
319
|
+
}
|
|
320
|
+
return value as Record<string, unknown>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function validateConfigName(value: string, field: string) {
|
|
324
|
+
if (
|
|
325
|
+
!value.trim() ||
|
|
326
|
+
value !== value.trim() ||
|
|
327
|
+
value.length > 100 ||
|
|
328
|
+
value === "__proto__" ||
|
|
329
|
+
value === "prototype" ||
|
|
330
|
+
value === "constructor" ||
|
|
331
|
+
hasControlCharacter(value)
|
|
332
|
+
) {
|
|
333
|
+
throw new Error(`Invalid pi-sync settings: invalid ${field} name.`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function requiredString(value: unknown, field: string) {
|
|
338
|
+
if (typeof value !== "string" || !value.trim() || hasControlCharacter(value)) {
|
|
339
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be a non-empty string.`);
|
|
340
|
+
}
|
|
341
|
+
return value.trim();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function requiredCanonicalReference(value: unknown, field: string) {
|
|
345
|
+
const normalized = requiredString(value, field);
|
|
346
|
+
if (value !== normalized) {
|
|
347
|
+
throw new Error(`Invalid pi-sync settings: ${field} must not have surrounding whitespace.`);
|
|
348
|
+
}
|
|
349
|
+
return normalized;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function optionalCanonicalReference(value: unknown, field: string) {
|
|
353
|
+
const normalized = optionalString(value, field);
|
|
354
|
+
if (normalized !== undefined && value !== normalized) {
|
|
355
|
+
throw new Error(`Invalid pi-sync settings: ${field} must not have surrounding whitespace.`);
|
|
356
|
+
}
|
|
357
|
+
return normalized;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function requiredSecret(value: unknown, field: string) {
|
|
361
|
+
if (typeof value !== "string" || !value || hasControlCharacter(value)) {
|
|
362
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be configured.`);
|
|
363
|
+
}
|
|
364
|
+
return value;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function optionalString(value: unknown, field: string) {
|
|
368
|
+
if (value === undefined) return undefined;
|
|
369
|
+
if (typeof value !== "string" || hasControlCharacter(value)) {
|
|
370
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be a string.`);
|
|
371
|
+
}
|
|
372
|
+
return value.trim() || undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function normalizeStoragePath(value: string) {
|
|
376
|
+
if (value.trim() === "." || value.trim() === "./") return "./";
|
|
377
|
+
const normalized = value.trim().replace(/^\/+|\/+$/gu, "");
|
|
378
|
+
if (
|
|
379
|
+
!normalized ||
|
|
380
|
+
normalized.length > 1024 ||
|
|
381
|
+
normalized.startsWith("-") ||
|
|
382
|
+
normalized.includes("\\") ||
|
|
383
|
+
hasControlCharacter(normalized) ||
|
|
384
|
+
normalized.split("/").some((segment) => !segment || segment === "." || segment === "..")
|
|
385
|
+
) {
|
|
386
|
+
throw new Error("Invalid pi-sync settings: storage.path must be a safe relative path.");
|
|
387
|
+
}
|
|
388
|
+
return normalized;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function normalizeS3Endpoint(value: string) {
|
|
392
|
+
let url: URL;
|
|
393
|
+
try {
|
|
394
|
+
url = new URL(value.trim());
|
|
395
|
+
} catch {
|
|
396
|
+
throw new Error("Invalid pi-sync S3 endpoint.");
|
|
397
|
+
}
|
|
398
|
+
const loopback =
|
|
399
|
+
url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
|
|
400
|
+
if (
|
|
401
|
+
(url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
|
|
402
|
+
url.username ||
|
|
403
|
+
url.password ||
|
|
404
|
+
url.search ||
|
|
405
|
+
url.hash
|
|
406
|
+
) {
|
|
407
|
+
throw new Error("Invalid pi-sync S3 endpoint: HTTPS is required except for loopback.");
|
|
408
|
+
}
|
|
409
|
+
url.pathname = url.pathname.replace(/\/+$/gu, "");
|
|
410
|
+
return url.toString().replace(/\/$/u, "");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function normalizeS3Bucket(value: string | undefined) {
|
|
414
|
+
const bucket = requiredString(value, "S3 bucket");
|
|
415
|
+
if (bucket.includes("/") || bucket.includes("\\") || bucket.startsWith("-")) {
|
|
416
|
+
throw new Error("Invalid pi-sync S3 bucket.");
|
|
417
|
+
}
|
|
418
|
+
return bucket;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function hasControlCharacter(value: string) {
|
|
422
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Stored settings cannot contain controls.
|
|
423
|
+
return /[\u0000-\u001f\u007f-\u009f]/u.test(value);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function isCloudflareR2Endpoint(endpoint: string | undefined) {
|
|
427
|
+
const value = endpoint?.trim();
|
|
428
|
+
if (!value) return false;
|
|
429
|
+
try {
|
|
430
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
431
|
+
return (
|
|
432
|
+
hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com")
|
|
433
|
+
);
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function ownRecord(value: unknown): Record<string, unknown> | undefined {
|
|
440
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
441
|
+
? (value as Record<string, unknown>)
|
|
442
|
+
: undefined;
|
|
443
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type ExtensionCommandContext,
|
|
5
|
+
type ExtensionContext,
|
|
6
|
+
getAgentDir,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
9
|
+
import { readJsonIfExists } from "../state/json-file.js";
|
|
10
|
+
import type { Snapshot, SnapshotOptions } from "./snapshot-types.js";
|
|
11
|
+
|
|
12
|
+
export function sessionDirFromContext(ctx: ExtensionCommandContext | ExtensionContext) {
|
|
13
|
+
const manager = ctx.sessionManager as typeof ctx.sessionManager & {
|
|
14
|
+
usesDefaultSessionDir?: () => boolean;
|
|
15
|
+
};
|
|
16
|
+
const usesDefaultSessionDir = manager.usesDefaultSessionDir;
|
|
17
|
+
if (typeof usesDefaultSessionDir === "function" && usesDefaultSessionDir.call(manager)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const getSessionDir = manager.getSessionDir;
|
|
21
|
+
return typeof getSessionDir === "function"
|
|
22
|
+
? (getSessionDir.call(manager) as string | undefined)
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function configuredSessionDir() {
|
|
27
|
+
const settings = await readJsonIfExists<{ sessionDir?: string }>(
|
|
28
|
+
path.join(agentDir(), "settings.json"),
|
|
29
|
+
);
|
|
30
|
+
return settings?.sessionDir ? expandHome(settings.sessionDir) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function sessionDirForApply(
|
|
34
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
35
|
+
snapshot: Snapshot,
|
|
36
|
+
) {
|
|
37
|
+
const contextSessionDir = sessionDirFromContext(ctx);
|
|
38
|
+
const localSessionDir = await configuredSessionDir();
|
|
39
|
+
if (
|
|
40
|
+
contextSessionDir &&
|
|
41
|
+
path.resolve(contextSessionDir) !== path.resolve(localSessionDir ?? "")
|
|
42
|
+
) {
|
|
43
|
+
return contextSessionDir;
|
|
44
|
+
}
|
|
45
|
+
return sessionDirFromSnapshot(snapshot) ?? contextSessionDir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sessionDirFromSnapshot(snapshot: Snapshot) {
|
|
49
|
+
const settingsFile = snapshot.files.find((file) => file.path === "settings.json");
|
|
50
|
+
if (!settingsFile) return undefined;
|
|
51
|
+
try {
|
|
52
|
+
const settings = JSON.parse(
|
|
53
|
+
decodeBase64Strict(settingsFile.contentBase64, settingsFile.path).toString("utf8"),
|
|
54
|
+
) as { sessionDir?: string };
|
|
55
|
+
return settings.sessionDir ? expandHome(settings.sessionDir) : undefined;
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decodeBase64Strict(value: string, filePath: string) {
|
|
62
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
63
|
+
throw new Error(`Invalid base64 content in snapshot file: ${filePath}`);
|
|
64
|
+
}
|
|
65
|
+
return Buffer.from(value, "base64");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function agentDir() {
|
|
69
|
+
return getAgentDir();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function expandHome(value: string) {
|
|
73
|
+
return value === "~" || value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function snapshotOptionsForContext(
|
|
77
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
78
|
+
config: AnySyncConfig,
|
|
79
|
+
): SnapshotOptions {
|
|
80
|
+
return {
|
|
81
|
+
include: config.include,
|
|
82
|
+
sessionDir: sessionDirFromContext(ctx),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -2,7 +2,6 @@ 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 { agentDir } from "./config.js";
|
|
6
5
|
import {
|
|
7
6
|
assertWithinRoot,
|
|
8
7
|
isDeniedPath,
|
|
@@ -10,7 +9,8 @@ import {
|
|
|
10
9
|
parentPaths,
|
|
11
10
|
safeJoin,
|
|
12
11
|
toPosix,
|
|
13
|
-
} from "
|
|
12
|
+
} from "../paths.js";
|
|
13
|
+
import { agentDir } from "./session-paths.js";
|
|
14
14
|
import {
|
|
15
15
|
createSnapshot,
|
|
16
16
|
isSessionFilePath,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
applySnapshotTransaction,
|
|
24
24
|
recoverPendingSnapshotTransactions,
|
|
25
25
|
} from "./snapshot-transaction.js";
|
|
26
|
-
import type { Snapshot, SnapshotApplyPlan, SnapshotOptions } from "./types.js";
|
|
26
|
+
import type { Snapshot, SnapshotApplyPlan, SnapshotOptions } from "./snapshot-types.js";
|
|
27
27
|
|
|
28
28
|
function sha256(value: Buffer) {
|
|
29
29
|
return createHash("sha256").update(value).digest("hex");
|
|
@@ -2,8 +2,8 @@ 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 { snapshotSelectionInclude } from "
|
|
6
|
-
import type { Snapshot } from "./types.js";
|
|
5
|
+
import { snapshotSelectionInclude } from "../sync/sync-policy.js";
|
|
6
|
+
import type { Snapshot } from "./snapshot-types.js";
|
|
7
7
|
|
|
8
8
|
const VERSION = 1;
|
|
9
9
|
const MAX_DECOMPRESSED_SNAPSHOT_BYTES = 512 * 1024 * 1024;
|