@narumitw/pi-sync 0.49.14 → 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 +134 -241
- 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-RGJ6SOYR.ts → chunk-AUSDLD6F.ts} +4 -4
- package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
- package/dist/chunks/{chunk-3NDD5QPM.ts → chunk-AXXS2BPA.ts} +9 -9
- package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
- package/dist/chunks/{chunk-APZVBSM6.ts → chunk-C7XY5NF5.ts} +214 -365
- 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-LJDSHNS3.ts → chunk-GWMHAQG4.ts} +4 -4
- package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
- package/dist/chunks/{chunk-FR6CVXAL.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-4F3W4CSC.ts → chunk-LCRI3ABW.ts} +22 -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-EWX3TPLQ.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-BHJUISB4.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-LONPFFNP.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-5WJNWFTG.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-4UABOP44.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-3QZGO2EF.ts → s3-backend-3P33LQHU.ts} +96 -18
- package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
- package/dist/chunks/{setup-switch-X3W3FALZ.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-WBSV4MQG.ts → snapshot-HKEWCVK6.ts} +7 -6
- package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
- package/dist/chunks/{sync-operations-MPAFVVHA.ts → sync-operations-ONJ27QRL.ts} +381 -299
- package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
- package/dist/chunks/{sync-state-36H7ULG2.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 +211 -0
- package/package.json +6 -8
- 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} +40 -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} +41 -330
- 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 -284
- 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} +42 -11
- 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-3NDD5QPM.ts.map +0 -7
- package/dist/chunks/chunk-4F3W4CSC.ts.map +0 -7
- package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
- package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
- package/dist/chunks/chunk-5WJNWFTG.ts.map +0 -7
- package/dist/chunks/chunk-APZVBSM6.ts.map +0 -7
- package/dist/chunks/chunk-BHJUISB4.ts.map +0 -7
- package/dist/chunks/chunk-EWX3TPLQ.ts.map +0 -7
- package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
- package/dist/chunks/chunk-FR6CVXAL.ts.map +0 -7
- package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
- package/dist/chunks/chunk-JNOOSPEW.ts +0 -82
- package/dist/chunks/chunk-JNOOSPEW.ts.map +0 -7
- package/dist/chunks/chunk-LJDSHNS3.ts.map +0 -7
- package/dist/chunks/chunk-LONPFFNP.ts.map +0 -7
- package/dist/chunks/chunk-RGJ6SOYR.ts.map +0 -7
- package/dist/chunks/chunk-W64NEHXT.ts +0 -167
- package/dist/chunks/chunk-W64NEHXT.ts.map +0 -7
- package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
- package/dist/chunks/file-selection-4UABOP44.ts.map +0 -7
- package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-RNRQA3PW.ts +0 -18
- package/dist/chunks/manager-ui-QO2P4V3Z.ts +0 -2757
- package/dist/chunks/manager-ui-QO2P4V3Z.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-LHSAGKGA.ts +0 -17
- package/dist/chunks/s3-backend-3QZGO2EF.ts.map +0 -7
- package/dist/chunks/sync-operations-MPAFVVHA.ts.map +0 -7
- package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
- package/src/config.ts +0 -910
- 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-RNRQA3PW.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-LHSAGKGA.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-X3W3FALZ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
- /package/dist/chunks/{snapshot-WBSV4MQG.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
- /package/dist/chunks/{sync-state-36H7ULG2.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,238 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { backendIdentityCoordinates } from "../backends/backend-identity.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeGitBranch,
|
|
5
|
+
normalizeGitDirectory,
|
|
6
|
+
normalizeGitRemote,
|
|
7
|
+
} from "../backends/git/git-config.js";
|
|
8
|
+
import { normalizeWebDavPath, normalizeWebDavUrl } from "../backends/webdav/webdav-config.js";
|
|
9
|
+
import { normalizeSyncInclude } from "../sync/sync-policy.js";
|
|
10
|
+
import { localConfigPath, readActiveLocalConfigDocumentForRepair } from "./config-file.js";
|
|
11
|
+
import { requireSettings } from "./settings-store.js";
|
|
12
|
+
import type {
|
|
13
|
+
AnySyncConfig,
|
|
14
|
+
OnSwitchAction,
|
|
15
|
+
PartialConfig,
|
|
16
|
+
PiSyncSettingsV3,
|
|
17
|
+
StorageConnectionSettings,
|
|
18
|
+
SyncSetupSettings,
|
|
19
|
+
} from "./settings-types.js";
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_ON_SWITCH,
|
|
22
|
+
isCloudflareR2Endpoint,
|
|
23
|
+
normalizeS3Bucket,
|
|
24
|
+
normalizeS3Endpoint,
|
|
25
|
+
normalizeStoragePath,
|
|
26
|
+
optionalString,
|
|
27
|
+
ownObject,
|
|
28
|
+
requiredString,
|
|
29
|
+
validateConfigName,
|
|
30
|
+
validateSettingsDocument,
|
|
31
|
+
} from "./settings-validation.js";
|
|
32
|
+
|
|
33
|
+
export async function loadConfig(setupName?: string): Promise<AnySyncConfig> {
|
|
34
|
+
return configFromSettings(await requireSettings(), setupName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A background freshness read must never trigger legacy-file publication. */
|
|
38
|
+
export async function loadConfigForCheck(): Promise<AnySyncConfig> {
|
|
39
|
+
const document = await readActiveLocalConfigDocumentForRepair();
|
|
40
|
+
if (!document) throw new Error(`Missing pi-sync settings: ${localConfigPath()}`);
|
|
41
|
+
validateSettingsDocument(document.parsed);
|
|
42
|
+
return configFromSettings(document.parsed as PiSyncSettingsV3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function configFromSettings(settings: PiSyncSettingsV3, setupName?: string): AnySyncConfig {
|
|
46
|
+
const selectedName = setupName ?? settings.activeSyncSetup;
|
|
47
|
+
if (!selectedName) throw new Error("No sync setups are configured.");
|
|
48
|
+
validateConfigName(selectedName, "sync setup");
|
|
49
|
+
const setup = ownObject<SyncSetupSettings>(settings.syncSetups, selectedName);
|
|
50
|
+
if (!setup)
|
|
51
|
+
throw new Error(`Invalid pi-sync settings: sync setup “${selectedName}” was not found.`);
|
|
52
|
+
const connectionName = setup.storage.connection;
|
|
53
|
+
const connection = ownObject<StorageConnectionSettings>(
|
|
54
|
+
settings.storageConnections,
|
|
55
|
+
connectionName,
|
|
56
|
+
);
|
|
57
|
+
if (!connection) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Invalid pi-sync settings: sync setup “${selectedName}” references missing storage connection “${connectionName}”.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return resolveSyncConfig(
|
|
63
|
+
selectedName,
|
|
64
|
+
setup,
|
|
65
|
+
connectionName,
|
|
66
|
+
connection,
|
|
67
|
+
settings.onSwitch,
|
|
68
|
+
settings.skipSecretScan ?? false,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type SyncSetupStorageReview = Pick<
|
|
73
|
+
PartialConfig,
|
|
74
|
+
"connectionName" | "storageKind" | "storagePath" | "bucket" | "branch"
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
/** A validated setup-facing projection used by manager and settings UI. */
|
|
78
|
+
export async function loadPartialConfig(setupName?: string): Promise<PartialConfig> {
|
|
79
|
+
const config = await loadConfig(setupName);
|
|
80
|
+
return {
|
|
81
|
+
setupName: config.setupName,
|
|
82
|
+
...storageReviewFromConfig(config),
|
|
83
|
+
include: [...config.include],
|
|
84
|
+
automatic: config.automatic,
|
|
85
|
+
onSwitch: config.onSwitch,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function syncSetupStorageReview(
|
|
90
|
+
setupName: string,
|
|
91
|
+
setup: SyncSetupSettings,
|
|
92
|
+
connectionName: string,
|
|
93
|
+
connection: StorageConnectionSettings,
|
|
94
|
+
): SyncSetupStorageReview {
|
|
95
|
+
return storageReviewFromConfig(
|
|
96
|
+
resolveSyncConfig(setupName, setup, connectionName, connection, DEFAULT_ON_SWITCH, false),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function syncSetupReviewIdentity(
|
|
101
|
+
setupName: string,
|
|
102
|
+
setup: SyncSetupSettings,
|
|
103
|
+
connectionName: string,
|
|
104
|
+
connection: StorageConnectionSettings,
|
|
105
|
+
) {
|
|
106
|
+
return syncConfigReviewIdentity(
|
|
107
|
+
resolveSyncConfig(setupName, setup, connectionName, connection, DEFAULT_ON_SWITCH, false),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function syncConfigReviewIdentity(config: AnySyncConfig) {
|
|
112
|
+
return JSON.stringify([
|
|
113
|
+
config.setupName,
|
|
114
|
+
config.connectionName,
|
|
115
|
+
backendIdentityCoordinates(config),
|
|
116
|
+
config.include,
|
|
117
|
+
config.automatic,
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function syncConfigReviewFingerprint(config: AnySyncConfig) {
|
|
122
|
+
return createHash("sha256").update(syncConfigReviewIdentity(config)).digest("hex");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Internal freshness token, including credentials; never display or persist it. */
|
|
126
|
+
export function syncCheckConfigFingerprint(config: AnySyncConfig) {
|
|
127
|
+
return createHash("sha256").update(JSON.stringify(config)).digest("hex");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function storageReviewFromConfig(config: AnySyncConfig): SyncSetupStorageReview {
|
|
131
|
+
return {
|
|
132
|
+
connectionName: config.connectionName,
|
|
133
|
+
storageKind: config.backend.type,
|
|
134
|
+
storagePath: config.storagePath,
|
|
135
|
+
...(config.backend.type === "s3"
|
|
136
|
+
? { bucket: config.backend.destination.bucket }
|
|
137
|
+
: config.backend.type === "git"
|
|
138
|
+
? { branch: config.backend.destination.branch }
|
|
139
|
+
: {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveSyncConfig(
|
|
144
|
+
setupName: string,
|
|
145
|
+
setup: SyncSetupSettings,
|
|
146
|
+
connectionName: string,
|
|
147
|
+
connection: StorageConnectionSettings,
|
|
148
|
+
onSwitch: OnSwitchAction,
|
|
149
|
+
skipSecretScan: boolean,
|
|
150
|
+
): AnySyncConfig {
|
|
151
|
+
const storagePath = normalizeStoragePath(setup.storage.path);
|
|
152
|
+
const namespace =
|
|
153
|
+
storagePath === "./" ? "root" : storagePath.slice(storagePath.lastIndexOf("/") + 1);
|
|
154
|
+
const include = normalizeSyncInclude(setup.sync.include);
|
|
155
|
+
const common = {
|
|
156
|
+
setupName,
|
|
157
|
+
connectionName,
|
|
158
|
+
storagePath,
|
|
159
|
+
snapshotIdentity: namespace,
|
|
160
|
+
include,
|
|
161
|
+
automatic: setup.sync.automatic,
|
|
162
|
+
onSwitch,
|
|
163
|
+
skipSecretScan,
|
|
164
|
+
};
|
|
165
|
+
if (connection.type === "git") {
|
|
166
|
+
return {
|
|
167
|
+
...common,
|
|
168
|
+
backend: {
|
|
169
|
+
type: "git",
|
|
170
|
+
profile: { kind: "git", remote: normalizeGitRemote(connection.remote) as string },
|
|
171
|
+
destination: {
|
|
172
|
+
branch: normalizeGitBranch(setup.storage.branch),
|
|
173
|
+
directory: normalizeGitDirectory(storagePath),
|
|
174
|
+
namespace,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (connection.type === "webdav") {
|
|
180
|
+
return {
|
|
181
|
+
...common,
|
|
182
|
+
backend: {
|
|
183
|
+
type: "webdav",
|
|
184
|
+
profile: {
|
|
185
|
+
kind: "webdav",
|
|
186
|
+
url: normalizeWebDavUrl(connection.url) as string,
|
|
187
|
+
username: connection.credentials.username,
|
|
188
|
+
password: connection.credentials.password,
|
|
189
|
+
},
|
|
190
|
+
destination: { path: normalizeWebDavPath(storagePath), namespace },
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
...common,
|
|
196
|
+
backend: {
|
|
197
|
+
type: "s3",
|
|
198
|
+
profile: {
|
|
199
|
+
kind: isCloudflareR2Endpoint(connection.endpoint) ? "r2" : "s3-compatible",
|
|
200
|
+
endpoint: normalizeS3Endpoint(connection.endpoint),
|
|
201
|
+
region: requiredString(connection.region, "S3 region"),
|
|
202
|
+
accessKeyId: connection.credentials.accessKeyId,
|
|
203
|
+
secretAccessKey: connection.credentials.secretAccessKey,
|
|
204
|
+
sessionToken: optionalString(connection.credentials.sessionToken, "S3 session token"),
|
|
205
|
+
},
|
|
206
|
+
destination: {
|
|
207
|
+
bucket: normalizeS3Bucket(setup.storage.bucket),
|
|
208
|
+
prefix: storagePath,
|
|
209
|
+
namespace,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function sessionTokenWarnings(config: { endpoint?: string; sessionToken?: string }) {
|
|
216
|
+
if (!isCloudflareR2Endpoint(config.endpoint) || !config.sessionToken) return [];
|
|
217
|
+
return [
|
|
218
|
+
"session token: configured for Cloudflare R2; if R2 rejects X-Amz-Security-Token, pi-sync retries once without it. R2 static access keys usually do not need a session token.",
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function syncSessionsWarnings(config: { include: readonly string[] }) {
|
|
223
|
+
if (!config.include.includes("sessions")) return [];
|
|
224
|
+
return [
|
|
225
|
+
"sessions: included; Pi session JSONL can contain prompts, tool output, file paths, images, and secrets. Sync sessions only to storage you trust.",
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function isEnabled(value: boolean | string | undefined, defaultValue: boolean) {
|
|
230
|
+
if (value === undefined) return defaultValue;
|
|
231
|
+
if (typeof value === "boolean") return value;
|
|
232
|
+
return !["0", "false", "no", "off"].includes(value.trim().toLowerCase());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function isExplicitlyEnabled(value: boolean | string | undefined) {
|
|
236
|
+
if (typeof value === "boolean") return value;
|
|
237
|
+
return ["1", "true", "yes", "on"].includes(value?.trim().toLowerCase() ?? "");
|
|
238
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { normalizeSyncInclude } from "../sync/sync-policy.js";
|
|
3
|
+
import { type SyncSetupStorageReview, syncSetupStorageReview } from "./config.js";
|
|
4
|
+
import { localConfigPath } from "./config-file.js";
|
|
5
|
+
import { updateLocalConfig } from "./settings-store.js";
|
|
6
|
+
import type {
|
|
7
|
+
PiSyncSettingsV3,
|
|
8
|
+
StorageConnectionSettings,
|
|
9
|
+
SyncSetupSettings,
|
|
10
|
+
} from "./settings-types.js";
|
|
11
|
+
import { effectiveSyncSetupRemoteIdentity, validateConfigName } from "./settings-validation.js";
|
|
11
12
|
|
|
12
13
|
export class SyncSetupReviewChangedError extends Error {
|
|
13
14
|
constructor(message: string) {
|
|
@@ -31,6 +32,7 @@ export async function saveNewV3Settings(
|
|
|
31
32
|
version: 3,
|
|
32
33
|
activeSyncSetup: input.setupName,
|
|
33
34
|
onSwitch: "ask-before-pull",
|
|
35
|
+
skipSecretScan: false,
|
|
34
36
|
storageConnections: { [input.connectionName]: structuredClone(input.connection) },
|
|
35
37
|
syncSetups: {
|
|
36
38
|
[input.setupName]: {
|
|
@@ -39,13 +41,12 @@ export async function saveNewV3Settings(
|
|
|
39
41
|
},
|
|
40
42
|
},
|
|
41
43
|
};
|
|
42
|
-
|
|
44
|
+
return updateLocalConfig((current) => {
|
|
43
45
|
if (Object.keys(current.storageConnections).length || Object.keys(current.syncSetups).length) {
|
|
44
46
|
throw new Error(`Settings already exist: ${localConfigPath()}`);
|
|
45
47
|
}
|
|
46
|
-
return settings;
|
|
48
|
+
return { ...current, ...settings };
|
|
47
49
|
}, signal);
|
|
48
|
-
return settings;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export async function addStorageConnection(
|
|
@@ -91,7 +92,12 @@ export async function updateStorageConnection(
|
|
|
91
92
|
}, signal);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
export async function addSyncSetup(
|
|
95
|
+
export async function addSyncSetup(
|
|
96
|
+
name: string,
|
|
97
|
+
setup: SyncSetupSettings,
|
|
98
|
+
signal?: AbortSignal,
|
|
99
|
+
expectedConnection?: StorageConnectionSettings,
|
|
100
|
+
) {
|
|
95
101
|
validateConfigName(name, "sync setup");
|
|
96
102
|
await updateSettings((settings) => {
|
|
97
103
|
if (Object.hasOwn(settings.syncSetups, name))
|
|
@@ -99,6 +105,14 @@ export async function addSyncSetup(name: string, setup: SyncSetupSettings, signa
|
|
|
99
105
|
if (!Object.hasOwn(settings.storageConnections, setup.storage.connection)) {
|
|
100
106
|
throw new Error(`Storage connection not found: ${setup.storage.connection}`);
|
|
101
107
|
}
|
|
108
|
+
if (
|
|
109
|
+
expectedConnection &&
|
|
110
|
+
!isDeepStrictEqual(settings.storageConnections[setup.storage.connection], expectedConnection)
|
|
111
|
+
) {
|
|
112
|
+
throw new SyncSetupReviewChangedError(
|
|
113
|
+
"Storage connection changed while the setup review was open; reopen it.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
102
116
|
const nextSetups = { ...settings.syncSetups, [name]: structuredClone(setup) };
|
|
103
117
|
assertUniqueLocations(nextSetups, settings.storageConnections);
|
|
104
118
|
return {
|
|
@@ -115,6 +129,7 @@ export async function updateSyncSetup(
|
|
|
115
129
|
options: {
|
|
116
130
|
expectedStorage?: SyncSetupStorageReview;
|
|
117
131
|
expectedInclude?: readonly string[];
|
|
132
|
+
expectedConnection?: StorageConnectionSettings;
|
|
118
133
|
signal?: AbortSignal;
|
|
119
134
|
} = {},
|
|
120
135
|
) {
|
|
@@ -145,6 +160,17 @@ export async function updateSyncSetup(
|
|
|
145
160
|
);
|
|
146
161
|
}
|
|
147
162
|
}
|
|
163
|
+
if (
|
|
164
|
+
options.expectedConnection &&
|
|
165
|
+
!isDeepStrictEqual(
|
|
166
|
+
settings.storageConnections[setup.storage.connection],
|
|
167
|
+
options.expectedConnection,
|
|
168
|
+
)
|
|
169
|
+
) {
|
|
170
|
+
throw new SyncSetupReviewChangedError(
|
|
171
|
+
"Storage connection changed while the setup review was open; reopen it.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
148
174
|
const nextSetup = update(structuredClone(setup));
|
|
149
175
|
if (!Object.hasOwn(settings.storageConnections, nextSetup.storage.connection)) {
|
|
150
176
|
throw new Error(`Storage connection not found: ${nextSetup.storage.connection}`);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
localConfigPath,
|
|
6
|
+
readMigratingLocalConfigDocument,
|
|
7
|
+
updateLocalConfigDocument,
|
|
8
|
+
} from "./config-file.js";
|
|
9
|
+
import type { OnSwitchAction, PiSyncSettingsV3 } from "./settings-types.js";
|
|
10
|
+
import { DEFAULT_ON_SWITCH, validateSettingsDocument } from "./settings-validation.js";
|
|
11
|
+
|
|
12
|
+
let configUpdateQueue: Promise<void> = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
export async function requireSettings() {
|
|
15
|
+
const settings = await readLocalConfigObject();
|
|
16
|
+
if (!settings) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Missing pi-sync settings. Open /sync and choose Set up sync, or create ${localConfigPath()}.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return settings;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function configuredSyncSetupNames() {
|
|
25
|
+
const settings = await readLocalConfigObject();
|
|
26
|
+
return settings
|
|
27
|
+
? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right))
|
|
28
|
+
: [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadOnSwitch(): Promise<OnSwitchAction> {
|
|
32
|
+
return (await requireSettings()).onSwitch;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function localConfigTemplate(): PiSyncSettingsV3 {
|
|
36
|
+
return {
|
|
37
|
+
version: 3,
|
|
38
|
+
onSwitch: DEFAULT_ON_SWITCH,
|
|
39
|
+
skipSecretScan: false,
|
|
40
|
+
storageConnections: {},
|
|
41
|
+
syncSetups: {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function readLocalConfigDocument() {
|
|
46
|
+
const document = await readMigratingLocalConfigDocument((settings) => {
|
|
47
|
+
validateSettingsDocument(settings);
|
|
48
|
+
});
|
|
49
|
+
if (document) validateSettingsDocument(document.parsed);
|
|
50
|
+
return document;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function readLocalConfigObject(): Promise<PiSyncSettingsV3 | undefined> {
|
|
54
|
+
return (await readLocalConfigDocument())?.parsed as PiSyncSettingsV3 | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function updateLocalConfig(
|
|
58
|
+
update: (current: PiSyncSettingsV3) => PiSyncSettingsV3,
|
|
59
|
+
signal?: AbortSignal,
|
|
60
|
+
) {
|
|
61
|
+
const operation = configUpdateQueue.then(() => {
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
return performLocalConfigUpdate(update, signal);
|
|
64
|
+
});
|
|
65
|
+
configUpdateQueue = operation.then(
|
|
66
|
+
() => undefined,
|
|
67
|
+
() => undefined,
|
|
68
|
+
);
|
|
69
|
+
return operation;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function performLocalConfigUpdate(
|
|
73
|
+
update: (current: PiSyncSettingsV3) => PiSyncSettingsV3,
|
|
74
|
+
signal?: AbortSignal,
|
|
75
|
+
) {
|
|
76
|
+
return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function writeLocalConfigObject(value: PiSyncSettingsV3 | Record<string, unknown>) {
|
|
80
|
+
validateSettingsDocument(value as Record<string, unknown>);
|
|
81
|
+
const configPath = localConfigPath();
|
|
82
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
83
|
+
try {
|
|
84
|
+
const stat = await fs.lstat(configPath);
|
|
85
|
+
if (stat.isSymbolicLink())
|
|
86
|
+
throw new Error(`Refusing to overwrite symlinked pi-sync settings: ${configPath}`);
|
|
87
|
+
if (!stat.isFile()) throw new Error(`pi-sync settings are not a regular file: ${configPath}`);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
90
|
+
}
|
|
91
|
+
const temporaryPath = path.join(
|
|
92
|
+
path.dirname(configPath),
|
|
93
|
+
`.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`,
|
|
94
|
+
);
|
|
95
|
+
let handle: fs.FileHandle | undefined;
|
|
96
|
+
try {
|
|
97
|
+
handle = await fs.open(temporaryPath, "wx", 0o600);
|
|
98
|
+
await handle.writeFile(`${JSON.stringify(value, null, "\t")}\n`, "utf8");
|
|
99
|
+
if (process.platform !== "win32") await handle.chmod(0o600);
|
|
100
|
+
await handle.sync();
|
|
101
|
+
await handle.close();
|
|
102
|
+
handle = undefined;
|
|
103
|
+
await fs.rename(temporaryPath, configPath);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
await handle?.close().catch(() => undefined);
|
|
106
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -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
|
+
}
|