@narumitw/pi-sync 0.57.10 → 0.57.11
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/CHANGELOG.md +6 -0
- package/README.md +5 -1
- package/dist/chunks/{agent-candidate-EYB35AGL.ts → agent-candidate-OCDPRI5G.ts} +11 -11
- package/dist/chunks/{agent-plan-KO43GTOR.ts → agent-plan-P5B5RGZ7.ts} +14 -14
- package/dist/chunks/{agent-refusal-retry-C2HQROTP.ts → agent-refusal-retry-476YGNU5.ts} +10 -10
- package/dist/chunks/{agent-store-UT52DS7B.ts → agent-store-RJSMNOYP.ts} +7 -7
- package/dist/chunks/{agent-sync-VM3TXWA3.ts → agent-sync-EUK2CPSS.ts} +6 -6
- package/dist/chunks/{chunk-UCD7TQDT.ts → chunk-3IYAPMST.ts} +4 -4
- package/dist/chunks/{chunk-JXWTEJS5.ts → chunk-45COPAK7.ts} +10 -3
- package/dist/chunks/chunk-45COPAK7.ts.map +7 -0
- package/dist/chunks/{chunk-DOJRPBQU.ts → chunk-5TSCBBPP.ts} +4 -4
- package/dist/chunks/{chunk-IMMMZUFC.ts → chunk-67INJPKR.ts} +4 -4
- package/dist/chunks/{chunk-CDQGSZL3.ts → chunk-6LD5KX55.ts} +6 -6
- package/dist/chunks/{chunk-MDUORMCK.ts → chunk-6O3EDBRL.ts} +2 -2
- package/dist/chunks/{chunk-AAPAHANE.ts → chunk-7A35ZAVO.ts} +320 -219
- package/dist/chunks/chunk-7A35ZAVO.ts.map +7 -0
- package/dist/chunks/{chunk-2VSN3MFX.ts → chunk-7OLLPPLU.ts} +29 -23
- package/dist/chunks/chunk-7OLLPPLU.ts.map +7 -0
- package/dist/chunks/{chunk-STVQQ5KG.ts → chunk-7ZIYU5QS.ts} +6 -6
- package/dist/chunks/{chunk-AO6EDUA6.ts → chunk-AKMV47YC.ts} +3 -3
- package/dist/chunks/{chunk-66GAUVDL.ts → chunk-BEST45H2.ts} +6 -6
- package/dist/chunks/{chunk-DO55XUNU.ts → chunk-CBLTWXAU.ts} +15 -5
- package/dist/chunks/chunk-CBLTWXAU.ts.map +7 -0
- package/dist/chunks/{chunk-PWA3YN5Z.ts → chunk-CPT2BN6N.ts} +2 -2
- package/dist/chunks/{chunk-73MDIVYJ.ts → chunk-DN4IA7MR.ts} +6 -6
- package/dist/chunks/{chunk-4RLATUP4.ts → chunk-DRTC4PKM.ts} +6 -6
- package/dist/chunks/{chunk-PHANBTVM.ts → chunk-ESKOPQVB.ts} +4 -4
- package/dist/chunks/{chunk-ODF2M4RF.ts → chunk-F7E3VFXU.ts} +5 -5
- package/dist/chunks/{chunk-YZ5BQNRZ.ts → chunk-FHE2DCA2.ts} +2 -2
- package/dist/chunks/{chunk-YZ5BQNRZ.ts.map → chunk-FHE2DCA2.ts.map} +2 -2
- package/dist/chunks/{chunk-MRIPZGPR.ts → chunk-FLHCOJCD.ts} +3 -3
- package/dist/chunks/{chunk-5WJQ6XOW.ts → chunk-GNYGNHFF.ts} +2 -2
- package/dist/chunks/{chunk-DFJGTVPX.ts → chunk-KYTT7KV4.ts} +2 -2
- package/dist/chunks/{chunk-UGSSZWQ6.ts → chunk-MKSMJPVA.ts} +2 -2
- package/dist/chunks/{chunk-RAMHGOX2.ts → chunk-OJARL2TG.ts} +4 -4
- package/dist/chunks/{chunk-BJXSCGQK.ts → chunk-OKPPZA4D.ts} +148 -89
- package/dist/chunks/chunk-OKPPZA4D.ts.map +7 -0
- package/dist/chunks/{chunk-RX3WGWVL.ts → chunk-OVBAW7KV.ts} +177 -130
- package/dist/chunks/chunk-OVBAW7KV.ts.map +7 -0
- package/dist/chunks/{chunk-U77DWNRH.ts → chunk-Q23JJW7E.ts} +3 -3
- package/dist/chunks/{chunk-QWV3XKNJ.ts → chunk-TIMWN2RH.ts} +5 -5
- package/dist/chunks/{chunk-Z5O2TLNF.ts → chunk-TZC7GDJE.ts} +7 -7
- package/dist/chunks/{chunk-TO2GMX3K.ts → chunk-XJHQPB3E.ts} +2 -2
- package/dist/chunks/{chunk-JY2RVOUL.ts → chunk-Y4UVXOLQ.ts} +5 -5
- package/dist/chunks/{chunk-M5IV3KL2.ts → chunk-ZDDBZH72.ts} +3 -3
- package/dist/chunks/{command-handler-FO6F2APH.ts → command-handler-F2E7A5T5.ts} +22 -17
- package/dist/chunks/command-handler-F2E7A5T5.ts.map +7 -0
- package/dist/chunks/{config-B37W7FXW.ts → config-HYG7G5EM.ts} +6 -4
- package/dist/chunks/{conflict-artifacts-6XGSSU5S.ts → conflict-artifacts-J6WSDCWP.ts} +12 -12
- package/dist/chunks/{conflict-review-BQSHSJB3.ts → conflict-review-2QMO2K6Q.ts} +23 -23
- package/dist/chunks/{continuous-record-4DQ2EAZS.ts → continuous-record-AH7JGYKM.ts} +6 -6
- package/dist/chunks/{continuous-sync-ZT3UEQLK.ts → continuous-sync-VRMXCA5X.ts} +18 -18
- package/dist/chunks/{file-selection-FBLKM25V.ts → file-selection-IELTXSGZ.ts} +6 -7
- package/dist/chunks/{file-selection-FBLKM25V.ts.map → file-selection-IELTXSGZ.ts.map} +1 -1
- package/dist/chunks/{git-backend-7PCWECRN.ts → git-backend-KHD36W27.ts} +6 -7
- package/dist/chunks/{git-backend-7PCWECRN.ts.map → git-backend-KHD36W27.ts.map} +1 -1
- package/dist/chunks/{local-fields-B6JWLPAE.ts → local-fields-JEKAPZKD.ts} +6 -7
- package/dist/chunks/manager-result-dispatcher-6W52NM7F.ts +32 -0
- package/dist/chunks/{manager-ui-DGRTG76I.ts → manager-ui-LM7DSJ2Y.ts} +103 -52
- package/dist/chunks/manager-ui-LM7DSJ2Y.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-HL3VLMAM.ts → merge-baseline-store-65GKUMXI.ts} +6 -6
- package/dist/chunks/{remote-selection-ui-OHMIQ32X.ts → remote-selection-ui-TD4C4W3J.ts} +16 -12
- package/dist/chunks/{s3-backend-CJXBEWWU.ts → s3-backend-L24UZ6IK.ts} +12 -10
- package/dist/chunks/s3-backend-L24UZ6IK.ts.map +7 -0
- package/dist/chunks/{setup-switch-66GWMBNH.ts → setup-switch-PIPXYPCW.ts} +5 -5
- package/dist/chunks/{setup-wizard-OHZXJVQZ.ts → setup-wizard-RR5U3SUB.ts} +6 -6
- package/dist/chunks/{snapshot-VRHNAUUH.ts → snapshot-RGNDYT6A.ts} +5 -6
- package/dist/chunks/{snapshot-transaction-EUGA4VX6.ts → snapshot-transaction-26BQO36S.ts} +8 -8
- package/dist/chunks/{sync-inspection-4WFQEJOE.ts → sync-inspection-MOMLGUTD.ts} +11 -11
- package/dist/chunks/{sync-operations-X4AOXMBT.ts → sync-operations-CV3TULLD.ts} +25 -25
- package/dist/chunks/{sync-state-SWLSMK4G.ts → sync-state-7L4NT7W2.ts} +7 -8
- package/dist/chunks/{webdav-backend-2Q5ZEBST.ts → webdav-backend-3ZCACILP.ts} +7 -8
- package/dist/chunks/{webdav-backend-2Q5ZEBST.ts.map → webdav-backend-3ZCACILP.ts.map} +1 -1
- package/dist/index.ts +19 -19
- package/package.json +1 -1
- package/skills/configure-pi-sync/references/configuration.md +8 -1
- package/src/backends/s3/s3-client.ts +8 -1
- package/src/commands/command-execution.ts +8 -3
- package/src/paths.ts +6 -1
- package/src/settings/config-file.ts +205 -118
- package/src/settings/config.ts +1 -1
- package/src/settings/settings-management.ts +15 -1
- package/src/settings/settings-store.ts +0 -14
- package/src/snapshot/session-paths.ts +15 -2
- package/src/snapshot/snapshot-mutation-queues.ts +142 -0
- package/src/snapshot/snapshot-transaction.ts +122 -125
- package/src/sync/merge-apply.ts +6 -0
- package/src/ui/cancellable-operation.ts +36 -13
- package/src/ui/manager-state.ts +3 -1
- package/src/ui/manager-ui.ts +76 -24
- package/src/ui/remote-selection-ui.ts +131 -82
- package/src/ui/settings-ui.ts +2 -1
- package/dist/chunks/chunk-2VSN3MFX.ts.map +0 -7
- package/dist/chunks/chunk-AAPAHANE.ts.map +0 -7
- package/dist/chunks/chunk-BJXSCGQK.ts.map +0 -7
- package/dist/chunks/chunk-DO55XUNU.ts.map +0 -7
- package/dist/chunks/chunk-JXWTEJS5.ts.map +0 -7
- package/dist/chunks/chunk-RX3WGWVL.ts.map +0 -7
- package/dist/chunks/command-handler-FO6F2APH.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-NODX3KKH.ts +0 -28
- package/dist/chunks/manager-ui-DGRTG76I.ts.map +0 -7
- package/dist/chunks/s3-backend-CJXBEWWU.ts.map +0 -7
- /package/dist/chunks/{agent-candidate-EYB35AGL.ts.map → agent-candidate-OCDPRI5G.ts.map} +0 -0
- /package/dist/chunks/{agent-plan-KO43GTOR.ts.map → agent-plan-P5B5RGZ7.ts.map} +0 -0
- /package/dist/chunks/{agent-refusal-retry-C2HQROTP.ts.map → agent-refusal-retry-476YGNU5.ts.map} +0 -0
- /package/dist/chunks/{agent-store-UT52DS7B.ts.map → agent-store-RJSMNOYP.ts.map} +0 -0
- /package/dist/chunks/{agent-sync-VM3TXWA3.ts.map → agent-sync-EUK2CPSS.ts.map} +0 -0
- /package/dist/chunks/{chunk-UCD7TQDT.ts.map → chunk-3IYAPMST.ts.map} +0 -0
- /package/dist/chunks/{chunk-DOJRPBQU.ts.map → chunk-5TSCBBPP.ts.map} +0 -0
- /package/dist/chunks/{chunk-IMMMZUFC.ts.map → chunk-67INJPKR.ts.map} +0 -0
- /package/dist/chunks/{chunk-CDQGSZL3.ts.map → chunk-6LD5KX55.ts.map} +0 -0
- /package/dist/chunks/{chunk-MDUORMCK.ts.map → chunk-6O3EDBRL.ts.map} +0 -0
- /package/dist/chunks/{chunk-STVQQ5KG.ts.map → chunk-7ZIYU5QS.ts.map} +0 -0
- /package/dist/chunks/{chunk-AO6EDUA6.ts.map → chunk-AKMV47YC.ts.map} +0 -0
- /package/dist/chunks/{chunk-66GAUVDL.ts.map → chunk-BEST45H2.ts.map} +0 -0
- /package/dist/chunks/{chunk-PWA3YN5Z.ts.map → chunk-CPT2BN6N.ts.map} +0 -0
- /package/dist/chunks/{chunk-73MDIVYJ.ts.map → chunk-DN4IA7MR.ts.map} +0 -0
- /package/dist/chunks/{chunk-4RLATUP4.ts.map → chunk-DRTC4PKM.ts.map} +0 -0
- /package/dist/chunks/{chunk-PHANBTVM.ts.map → chunk-ESKOPQVB.ts.map} +0 -0
- /package/dist/chunks/{chunk-ODF2M4RF.ts.map → chunk-F7E3VFXU.ts.map} +0 -0
- /package/dist/chunks/{chunk-MRIPZGPR.ts.map → chunk-FLHCOJCD.ts.map} +0 -0
- /package/dist/chunks/{chunk-5WJQ6XOW.ts.map → chunk-GNYGNHFF.ts.map} +0 -0
- /package/dist/chunks/{chunk-DFJGTVPX.ts.map → chunk-KYTT7KV4.ts.map} +0 -0
- /package/dist/chunks/{chunk-UGSSZWQ6.ts.map → chunk-MKSMJPVA.ts.map} +0 -0
- /package/dist/chunks/{chunk-RAMHGOX2.ts.map → chunk-OJARL2TG.ts.map} +0 -0
- /package/dist/chunks/{chunk-U77DWNRH.ts.map → chunk-Q23JJW7E.ts.map} +0 -0
- /package/dist/chunks/{chunk-QWV3XKNJ.ts.map → chunk-TIMWN2RH.ts.map} +0 -0
- /package/dist/chunks/{chunk-Z5O2TLNF.ts.map → chunk-TZC7GDJE.ts.map} +0 -0
- /package/dist/chunks/{chunk-TO2GMX3K.ts.map → chunk-XJHQPB3E.ts.map} +0 -0
- /package/dist/chunks/{chunk-JY2RVOUL.ts.map → chunk-Y4UVXOLQ.ts.map} +0 -0
- /package/dist/chunks/{chunk-M5IV3KL2.ts.map → chunk-ZDDBZH72.ts.map} +0 -0
- /package/dist/chunks/{config-B37W7FXW.ts.map → config-HYG7G5EM.ts.map} +0 -0
- /package/dist/chunks/{conflict-artifacts-6XGSSU5S.ts.map → conflict-artifacts-J6WSDCWP.ts.map} +0 -0
- /package/dist/chunks/{conflict-review-BQSHSJB3.ts.map → conflict-review-2QMO2K6Q.ts.map} +0 -0
- /package/dist/chunks/{continuous-record-4DQ2EAZS.ts.map → continuous-record-AH7JGYKM.ts.map} +0 -0
- /package/dist/chunks/{continuous-sync-ZT3UEQLK.ts.map → continuous-sync-VRMXCA5X.ts.map} +0 -0
- /package/dist/chunks/{local-fields-B6JWLPAE.ts.map → local-fields-JEKAPZKD.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-NODX3KKH.ts.map → manager-result-dispatcher-6W52NM7F.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-HL3VLMAM.ts.map → merge-baseline-store-65GKUMXI.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-OHMIQ32X.ts.map → remote-selection-ui-TD4C4W3J.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-66GWMBNH.ts.map → setup-switch-PIPXYPCW.ts.map} +0 -0
- /package/dist/chunks/{setup-wizard-OHZXJVQZ.ts.map → setup-wizard-RR5U3SUB.ts.map} +0 -0
- /package/dist/chunks/{snapshot-VRHNAUUH.ts.map → snapshot-RGNDYT6A.ts.map} +0 -0
- /package/dist/chunks/{snapshot-transaction-EUGA4VX6.ts.map → snapshot-transaction-26BQO36S.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-4WFQEJOE.ts.map → sync-inspection-MOMLGUTD.ts.map} +0 -0
- /package/dist/chunks/{sync-operations-X4AOXMBT.ts.map → sync-operations-CV3TULLD.ts.map} +0 -0
- /package/dist/chunks/{sync-state-SWLSMK4G.ts.map → sync-state-7L4NT7W2.ts.map} +0 -0
|
@@ -35,8 +35,14 @@ const LOCKFILE_FS_ADAPTER = {
|
|
|
35
35
|
utimesSync,
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
type PublishFile = (
|
|
39
|
-
|
|
38
|
+
type PublishFile = (
|
|
39
|
+
source: string,
|
|
40
|
+
destination: string,
|
|
41
|
+
expected?: LocalConfigDocument,
|
|
42
|
+
signal?: AbortSignal,
|
|
43
|
+
) => Promise<void>;
|
|
44
|
+
let configUpdateQueue: Promise<void> = Promise.resolve();
|
|
45
|
+
let publishConfigFile: PublishFile = publishFileWithGuard;
|
|
40
46
|
let beforeConfigPublicationHook: () => Promise<void> = async () => undefined;
|
|
41
47
|
let afterMissingConfigReadProbeHook: () => Promise<void> = async () => undefined;
|
|
42
48
|
let afterReplacementInstalledHook: () => Promise<void> = async () => undefined;
|
|
@@ -125,10 +131,12 @@ export function legacyLocalConfigPath() {
|
|
|
125
131
|
export async function activeLocalConfigPath() {
|
|
126
132
|
const canonicalPath = localConfigPath();
|
|
127
133
|
const legacyPath = legacyLocalConfigPath();
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
return queueConfigOperation(() =>
|
|
135
|
+
withLocalConfigReadLockIfNeeded(async () => {
|
|
136
|
+
if (await pathExists(canonicalPath)) return canonicalPath;
|
|
137
|
+
return (await pathExists(legacyPath)) ? legacyPath : canonicalPath;
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
export function consumeLocalConfigMigrationNotice() {
|
|
@@ -141,21 +149,25 @@ export function consumeLocalConfigMigrationNotice() {
|
|
|
141
149
|
export async function readActiveLocalConfigDocumentForRepair(): Promise<LocalConfigDocument | undefined> {
|
|
142
150
|
const canonicalPath = localConfigPath();
|
|
143
151
|
const legacyPath = legacyLocalConfigPath();
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
return queueConfigOperation(() =>
|
|
153
|
+
withLocalConfigReadLockIfNeeded(async () => {
|
|
154
|
+
const filePath = (await pathExists(canonicalPath)) ? canonicalPath : legacyPath;
|
|
155
|
+
const snapshot = await readConfigSnapshotIfExists(filePath);
|
|
156
|
+
return snapshot ? { path: filePath, ...snapshot } : undefined;
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
export async function readMigratingLocalConfigDocument(
|
|
152
162
|
validateForMigration: (settings: Record<string, unknown>) => void,
|
|
153
163
|
): Promise<LocalConfigDocument | undefined> {
|
|
154
|
-
return
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
return queueConfigOperation(() =>
|
|
165
|
+
withLocalConfigReadLockIfNeeded(async () => {
|
|
166
|
+
const configPath = await prepareLocalConfigPath(validateForMigration);
|
|
167
|
+
const snapshot = await readConfigSnapshotIfExists(configPath);
|
|
168
|
+
return snapshot ? { path: configPath, ...snapshot } : undefined;
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
async function withLocalConfigReadLockIfNeeded<T>(read: () => Promise<T>): Promise<T> {
|
|
@@ -164,57 +176,81 @@ async function withLocalConfigReadLockIfNeeded<T>(read: () => Promise<T>): Promi
|
|
|
164
176
|
}
|
|
165
177
|
await afterMissingConfigReadProbeHook();
|
|
166
178
|
if (await pathExists(configMutationLockPath())) return withLocalConfigFileLock(read);
|
|
179
|
+
await assertNoInterruptedConfigReplacement();
|
|
167
180
|
return read();
|
|
168
181
|
}
|
|
169
182
|
|
|
183
|
+
function queueConfigOperation<T>(run: () => Promise<T>): Promise<T> {
|
|
184
|
+
// Mutation callbacks use the unlocked helpers below, never a queued public read.
|
|
185
|
+
const operation = configUpdateQueue.then(run);
|
|
186
|
+
configUpdateQueue = operation.then(
|
|
187
|
+
() => undefined,
|
|
188
|
+
() => undefined,
|
|
189
|
+
);
|
|
190
|
+
return operation;
|
|
191
|
+
}
|
|
192
|
+
|
|
170
193
|
export function updateLocalConfigDocument<T extends Record<string, unknown>>(
|
|
171
194
|
defaultValue: T,
|
|
172
195
|
update: (current: T) => T,
|
|
173
196
|
validate: (value: Record<string, unknown>) => void,
|
|
174
197
|
signal?: AbortSignal,
|
|
175
198
|
): Promise<T> {
|
|
176
|
-
return
|
|
177
|
-
signal?.throwIfAborted();
|
|
178
|
-
const configPath = await prepareLocalConfigPath(validate);
|
|
179
|
-
const snapshot = await readConfigSnapshotIfExists(configPath);
|
|
180
|
-
const document = snapshot ? { path: configPath, ...snapshot } : undefined;
|
|
181
|
-
const current = document ? (structuredClone(document.parsed) as T) : structuredClone(defaultValue);
|
|
182
|
-
const next = update(current);
|
|
183
|
-
validate(next);
|
|
199
|
+
return queueConfigOperation(() => {
|
|
184
200
|
signal?.throwIfAborted();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
201
|
+
return withLocalConfigFileLock(async () => {
|
|
202
|
+
signal?.throwIfAborted();
|
|
203
|
+
const configPath = await prepareLocalConfigPath(validate);
|
|
204
|
+
signal?.throwIfAborted();
|
|
205
|
+
const snapshot = await readConfigSnapshotIfExists(configPath);
|
|
206
|
+
signal?.throwIfAborted();
|
|
207
|
+
const document = snapshot ? { path: configPath, ...snapshot } : undefined;
|
|
208
|
+
const current = document ? (structuredClone(document.parsed) as T) : structuredClone(defaultValue);
|
|
209
|
+
const next = update(current);
|
|
210
|
+
validate(next);
|
|
211
|
+
signal?.throwIfAborted();
|
|
212
|
+
if (document && JSON.stringify(document.parsed) === JSON.stringify(next)) return next;
|
|
213
|
+
if (document) await replaceLocalConfigDocumentUnlocked(document, next, signal);
|
|
214
|
+
else await installPrivateConfigExclusively(localConfigPath(), serializedConfig(next), signal);
|
|
215
|
+
return next;
|
|
216
|
+
});
|
|
189
217
|
});
|
|
190
218
|
}
|
|
191
219
|
|
|
192
|
-
export function createLocalConfigDocument(value: Record<string, unknown
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
220
|
+
export function createLocalConfigDocument(value: Record<string, unknown>, signal?: AbortSignal) {
|
|
221
|
+
return queueConfigOperation(() => {
|
|
222
|
+
signal?.throwIfAborted();
|
|
223
|
+
return withLocalConfigFileLock(async () => {
|
|
224
|
+
signal?.throwIfAborted();
|
|
225
|
+
const bytes = serializedConfig(value);
|
|
226
|
+
try {
|
|
227
|
+
await installPrivateConfigExclusively(localConfigPath(), bytes, signal);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
230
|
+
throw new Error("Pi-sync settings were created concurrently; reopen settings and retry.");
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
200
233
|
}
|
|
201
|
-
|
|
202
|
-
}
|
|
234
|
+
});
|
|
203
235
|
});
|
|
204
236
|
}
|
|
205
237
|
|
|
206
238
|
export function replaceLocalConfigDocument(document: LocalConfigDocument, value: Record<string, unknown>) {
|
|
207
|
-
return withLocalConfigFileLock(() => replaceLocalConfigDocumentUnlocked(document, value));
|
|
239
|
+
return queueConfigOperation(() => withLocalConfigFileLock(() => replaceLocalConfigDocumentUnlocked(document, value)));
|
|
208
240
|
}
|
|
209
241
|
|
|
210
|
-
async function replaceLocalConfigDocumentUnlocked(
|
|
242
|
+
async function replaceLocalConfigDocumentUnlocked(
|
|
243
|
+
document: LocalConfigDocument,
|
|
244
|
+
value: Record<string, unknown>,
|
|
245
|
+
signal?: AbortSignal,
|
|
246
|
+
) {
|
|
211
247
|
const nextBytes = serializedConfig(value);
|
|
212
248
|
const canonicalPath = localConfigPath();
|
|
213
249
|
if (document.path !== canonicalPath) {
|
|
214
250
|
if (!(await configDocumentStillMatches(document))) throw settingsChangedError();
|
|
215
251
|
let installed: FileIdentity;
|
|
216
252
|
try {
|
|
217
|
-
installed = await installPrivateConfigExclusively(canonicalPath, nextBytes);
|
|
253
|
+
installed = await installPrivateConfigExclusively(canonicalPath, nextBytes, signal);
|
|
218
254
|
} catch (error) {
|
|
219
255
|
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
220
256
|
throw new Error("Canonical settings were created concurrently; no settings were replaced.");
|
|
@@ -228,30 +264,62 @@ async function replaceLocalConfigDocumentUnlocked(document: LocalConfigDocument,
|
|
|
228
264
|
return;
|
|
229
265
|
}
|
|
230
266
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
await restoreQuarantinedConfig(canonicalPath, quarantinePath);
|
|
237
|
-
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
238
|
-
throw new Error("Canonical settings changed concurrently; no settings were replaced.");
|
|
239
|
-
}
|
|
240
|
-
throw error;
|
|
241
|
-
}
|
|
267
|
+
const recoveryPath = path.join(
|
|
268
|
+
path.dirname(canonicalPath),
|
|
269
|
+
`.${path.basename(canonicalPath)}.${randomUUID()}.schema-migration-source`,
|
|
270
|
+
);
|
|
271
|
+
const previousHandle = await fs.open(canonicalPath, "r");
|
|
242
272
|
try {
|
|
243
|
-
await
|
|
244
|
-
if (
|
|
273
|
+
const previous = await previousHandle.stat();
|
|
274
|
+
if (previous.dev !== document.identity.dev || previous.ino !== document.identity.ino) {
|
|
245
275
|
throw settingsChangedError();
|
|
246
276
|
}
|
|
247
|
-
|
|
277
|
+
await writePrivateConfigBytes(recoveryPath, document.bytes, "wx");
|
|
278
|
+
await syncParentDirectory(canonicalPath);
|
|
279
|
+
await withPrivateConfigTemporary(canonicalPath, nextBytes, async (temporaryPath, installed) => {
|
|
280
|
+
let published = false;
|
|
281
|
+
let previousBytesRead: Promise<Buffer> | undefined;
|
|
282
|
+
const readPreviousBytes = () => (previousBytesRead ??= previousHandle.readFile());
|
|
283
|
+
try {
|
|
284
|
+
signal?.throwIfAborted();
|
|
285
|
+
await publishConfigFile(temporaryPath, canonicalPath, document, signal);
|
|
286
|
+
published = true;
|
|
287
|
+
await syncParentDirectory(canonicalPath);
|
|
288
|
+
await afterReplacementInstalledHook();
|
|
289
|
+
if (!(await readPreviousBytes()).equals(document.bytes)) throw settingsChangedError();
|
|
290
|
+
if (!(await fileIdentityAndContentsMatch(canonicalPath, installed, nextBytes))) {
|
|
291
|
+
throw settingsChangedError();
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (published || (await fileIdentityAndContentsMatch(canonicalPath, installed, nextBytes))) {
|
|
295
|
+
const previousBytes = await readPreviousBytes().catch(() => document.bytes);
|
|
296
|
+
if (!previousBytes.equals(document.bytes)) {
|
|
297
|
+
await writePrivateConfigBytes(recoveryPath, previousBytes, "w");
|
|
298
|
+
}
|
|
299
|
+
// Rollback also replaces atomically; never remove the active settings first.
|
|
300
|
+
if (await fileIdentityAndContentsMatch(canonicalPath, installed, nextBytes)) {
|
|
301
|
+
await fs.rename(recoveryPath, canonicalPath);
|
|
302
|
+
await syncParentDirectory(canonicalPath);
|
|
303
|
+
}
|
|
304
|
+
} else if (await configDocumentStillMatches(document)) {
|
|
305
|
+
await fs.rm(recoveryPath);
|
|
306
|
+
}
|
|
307
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
308
|
+
throw new Error("Canonical settings changed concurrently; no settings were replaced.");
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
await fs.rm(recoveryPath).catch(() => undefined);
|
|
314
|
+
await syncParentDirectory(canonicalPath).catch(() => undefined);
|
|
248
315
|
} catch (error) {
|
|
249
|
-
await
|
|
250
|
-
|
|
316
|
+
if (await configDocumentStillMatches(document)) {
|
|
317
|
+
await fs.rm(recoveryPath, { force: true }).catch(() => undefined);
|
|
318
|
+
}
|
|
251
319
|
throw error;
|
|
320
|
+
} finally {
|
|
321
|
+
await previousHandle.close().catch(() => undefined);
|
|
252
322
|
}
|
|
253
|
-
await fs.rm(quarantinePath).catch(() => undefined);
|
|
254
|
-
await syncParentDirectory(canonicalPath).catch(() => undefined);
|
|
255
323
|
}
|
|
256
324
|
|
|
257
325
|
async function prepareLocalConfigPath(validateForMigration: (settings: Record<string, unknown>) => void) {
|
|
@@ -390,49 +458,51 @@ function parseConfigObject(bytes: Buffer, filePath: string) {
|
|
|
390
458
|
return parsed as Record<string, unknown>;
|
|
391
459
|
}
|
|
392
460
|
|
|
393
|
-
async function installPrivateConfigExclusively(filePath: string, bytes: Buffer) {
|
|
461
|
+
async function installPrivateConfigExclusively(filePath: string, bytes: Buffer, signal?: AbortSignal) {
|
|
462
|
+
return withPrivateConfigTemporary(filePath, bytes, async (temporaryPath, installed) => {
|
|
463
|
+
signal?.throwIfAborted();
|
|
464
|
+
await publishConfigFile(temporaryPath, filePath, undefined, signal);
|
|
465
|
+
try {
|
|
466
|
+
if (!(await fileIdentityAndContentsMatch(filePath, installed, bytes))) {
|
|
467
|
+
throw new Error(`Published pi-sync settings changed concurrently: ${filePath}`);
|
|
468
|
+
}
|
|
469
|
+
await syncParentDirectory(filePath);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
await quarantineAndRemoveConfigIfMatchesUnlocked(filePath, installed, bytes);
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
return installed;
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function withPrivateConfigTemporary<T>(
|
|
479
|
+
filePath: string,
|
|
480
|
+
bytes: Buffer,
|
|
481
|
+
publish: (temporaryPath: string, identity: FileIdentity) => Promise<T>,
|
|
482
|
+
): Promise<T> {
|
|
394
483
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
395
484
|
const temporaryPath = path.join(
|
|
396
485
|
path.dirname(filePath),
|
|
397
486
|
`.${path.basename(filePath)}.${process.pid}.${randomUUID()}.migrate`,
|
|
398
487
|
);
|
|
399
|
-
let handle: fs.FileHandle | undefined;
|
|
400
488
|
try {
|
|
401
|
-
|
|
489
|
+
const identity = await writePrivateConfigBytes(temporaryPath, bytes, "wx");
|
|
490
|
+
return await publish(temporaryPath, identity);
|
|
491
|
+
} finally {
|
|
492
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function writePrivateConfigBytes(filePath: string, bytes: Buffer, flags: "w" | "wx") {
|
|
497
|
+
const handle = await fs.open(filePath, flags, 0o600);
|
|
498
|
+
try {
|
|
402
499
|
await handle.writeFile(bytes);
|
|
403
500
|
if (process.platform !== "win32") await handle.chmod(0o600);
|
|
404
501
|
await handle.sync();
|
|
405
|
-
await handle.
|
|
406
|
-
handle = undefined;
|
|
407
|
-
await publishConfigFile(temporaryPath, filePath);
|
|
408
|
-
const installed = await fs.lstat(filePath);
|
|
409
|
-
let publishedHandle: fs.FileHandle | undefined;
|
|
410
|
-
let publicationError: unknown;
|
|
411
|
-
try {
|
|
412
|
-
if (installed.isSymbolicLink() || !installed.isFile()) {
|
|
413
|
-
throw new Error(`Published pi-sync settings are not a regular file: ${filePath}`);
|
|
414
|
-
}
|
|
415
|
-
publishedHandle = await fs.open(filePath, "r+");
|
|
416
|
-
const published = await publishedHandle.stat();
|
|
417
|
-
if (published.dev !== installed.dev || published.ino !== installed.ino) {
|
|
418
|
-
throw new Error(`Published pi-sync settings changed while opening: ${filePath}`);
|
|
419
|
-
}
|
|
420
|
-
if (process.platform !== "win32") await publishedHandle.chmod(0o600);
|
|
421
|
-
await publishedHandle.sync();
|
|
422
|
-
await syncParentDirectory(filePath);
|
|
423
|
-
} catch (error) {
|
|
424
|
-
publicationError = error;
|
|
425
|
-
} finally {
|
|
426
|
-
await publishedHandle?.close().catch(() => undefined);
|
|
427
|
-
}
|
|
428
|
-
if (publicationError) {
|
|
429
|
-
await quarantineAndRemoveConfigIfMatchesUnlocked(filePath, { dev: installed.dev, ino: installed.ino }, bytes);
|
|
430
|
-
throw publicationError;
|
|
431
|
-
}
|
|
502
|
+
const installed = await handle.stat();
|
|
432
503
|
return { dev: installed.dev, ino: installed.ino };
|
|
433
504
|
} finally {
|
|
434
|
-
await handle
|
|
435
|
-
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
505
|
+
await handle.close();
|
|
436
506
|
}
|
|
437
507
|
}
|
|
438
508
|
|
|
@@ -440,29 +510,6 @@ async function configDocumentStillMatches(document: LocalConfigDocument) {
|
|
|
440
510
|
return fileIdentityAndContentsMatch(document.path, document.identity, document.bytes);
|
|
441
511
|
}
|
|
442
512
|
|
|
443
|
-
async function claimCanonicalConfigDocument(document: LocalConfigDocument) {
|
|
444
|
-
const quarantinePath = path.join(
|
|
445
|
-
path.dirname(document.path),
|
|
446
|
-
`.${path.basename(document.path)}.${randomUUID()}.schema-migration-source`,
|
|
447
|
-
);
|
|
448
|
-
try {
|
|
449
|
-
await fs.rename(document.path, quarantinePath);
|
|
450
|
-
await syncParentDirectory(document.path);
|
|
451
|
-
} catch (error) {
|
|
452
|
-
await restoreQuarantinedConfig(document.path, quarantinePath);
|
|
453
|
-
throw error;
|
|
454
|
-
}
|
|
455
|
-
if (!(await fileIdentityAndContentsMatch(quarantinePath, document.identity, document.bytes))) {
|
|
456
|
-
await restoreQuarantinedConfig(document.path, quarantinePath);
|
|
457
|
-
throw settingsChangedError();
|
|
458
|
-
}
|
|
459
|
-
if (await pathExists(document.path)) {
|
|
460
|
-
await restoreQuarantinedConfig(document.path, quarantinePath);
|
|
461
|
-
throw new Error("Canonical settings changed concurrently; no settings were replaced.");
|
|
462
|
-
}
|
|
463
|
-
return quarantinePath;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
513
|
function settingsChangedError() {
|
|
467
514
|
return new Error("pi-sync settings changed during migration; no settings were replaced.");
|
|
468
515
|
}
|
|
@@ -472,7 +519,9 @@ async function configSnapshotStillMatches(filePath: string, snapshot: ConfigSnap
|
|
|
472
519
|
}
|
|
473
520
|
|
|
474
521
|
export function quarantineAndRemoveConfigIfMatches(filePath: string, identity: FileIdentity, expectedBytes: Buffer) {
|
|
475
|
-
return
|
|
522
|
+
return queueConfigOperation(() =>
|
|
523
|
+
withLocalConfigFileLock(() => quarantineAndRemoveConfigIfMatchesUnlocked(filePath, identity, expectedBytes)),
|
|
524
|
+
);
|
|
476
525
|
}
|
|
477
526
|
|
|
478
527
|
async function quarantineAndRemoveConfigIfMatchesUnlocked(
|
|
@@ -560,6 +609,7 @@ export async function withLocalConfigFileLock<T>(run: () => Promise<T>): Promise
|
|
|
560
609
|
},
|
|
561
610
|
});
|
|
562
611
|
try {
|
|
612
|
+
await assertNoInterruptedConfigReplacement();
|
|
563
613
|
const result = await run();
|
|
564
614
|
if (compromisedError) throw compromisedError;
|
|
565
615
|
return result;
|
|
@@ -579,18 +629,55 @@ async function syncParentDirectory(filePath: string) {
|
|
|
579
629
|
}
|
|
580
630
|
}
|
|
581
631
|
|
|
582
|
-
async function
|
|
632
|
+
async function publishFileWithGuard(
|
|
633
|
+
source: string,
|
|
634
|
+
destination: string,
|
|
635
|
+
expected?: LocalConfigDocument,
|
|
636
|
+
signal?: AbortSignal,
|
|
637
|
+
) {
|
|
583
638
|
await beforeConfigPublicationHook();
|
|
584
|
-
|
|
639
|
+
signal?.throwIfAborted();
|
|
640
|
+
if (expected) {
|
|
641
|
+
// The shared lock orders Pi Sync writers; rename has no CAS for noncooperating writers.
|
|
642
|
+
if (!(await configDocumentStillMatches(expected))) throw settingsChangedError();
|
|
643
|
+
signal?.throwIfAborted();
|
|
644
|
+
await fs.rename(source, destination);
|
|
645
|
+
} else {
|
|
646
|
+
await renameFileWithoutReplacement(source, destination, signal);
|
|
647
|
+
}
|
|
585
648
|
}
|
|
586
649
|
|
|
587
|
-
async function renameFileWithoutReplacement(source: string, destination: string) {
|
|
650
|
+
async function renameFileWithoutReplacement(source: string, destination: string, signal?: AbortSignal) {
|
|
588
651
|
if (await pathExists(destination)) {
|
|
589
652
|
throw Object.assign(new Error(`Settings already exist: ${destination}`), { code: "EEXIST" });
|
|
590
653
|
}
|
|
654
|
+
signal?.throwIfAborted();
|
|
591
655
|
await fs.rename(source, destination);
|
|
592
656
|
}
|
|
593
657
|
|
|
658
|
+
async function assertNoInterruptedConfigReplacement() {
|
|
659
|
+
const configPath = localConfigPath();
|
|
660
|
+
if (await pathExists(configPath)) return;
|
|
661
|
+
let names: string[];
|
|
662
|
+
try {
|
|
663
|
+
names = await fs.readdir(path.dirname(configPath));
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
const prefix = `.${path.basename(configPath)}.`;
|
|
669
|
+
if (
|
|
670
|
+
names.some(
|
|
671
|
+
(name) =>
|
|
672
|
+
name.startsWith(prefix) && (name.endsWith(".schema-migration-source") || name.endsWith(".migration-retired")),
|
|
673
|
+
)
|
|
674
|
+
) {
|
|
675
|
+
throw new Error(
|
|
676
|
+
"Interrupted pi-sync settings replacement: restore the retained recovery file to pi-sync.json before retrying; legacy settings were not loaded.",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
594
681
|
async function pathExists(filePath: string) {
|
|
595
682
|
try {
|
|
596
683
|
await fs.lstat(filePath);
|
package/src/settings/config.ts
CHANGED
|
@@ -39,7 +39,7 @@ export async function loadConfigForCheck(): Promise<AnySyncConfig> {
|
|
|
39
39
|
return configFromSettings(document.parsed as PiSyncSettingsV3);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function configFromSettings(settings: PiSyncSettingsV3, setupName?: string): AnySyncConfig {
|
|
42
|
+
export function configFromSettings(settings: PiSyncSettingsV3, setupName?: string): AnySyncConfig {
|
|
43
43
|
const selectedName = setupName ?? settings.activeSyncSetup;
|
|
44
44
|
if (!selectedName) throw new Error("No sync setups are configured.");
|
|
45
45
|
validateConfigName(selectedName, "sync setup");
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { normalizeSyncInclude } from "../sync/sync-policy.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
configFromSettings,
|
|
5
|
+
type SyncSetupStorageReview,
|
|
6
|
+
syncCheckConfigFingerprint,
|
|
7
|
+
syncSetupStorageReview,
|
|
8
|
+
} from "./config.js";
|
|
4
9
|
import { localConfigPath } from "./config-file.js";
|
|
5
10
|
import { updateLocalConfig } from "./settings-store.js";
|
|
6
11
|
import type { PiSyncSettingsV3, StorageConnectionSettings, SyncSetupSettings } from "./settings-types.js";
|
|
@@ -119,6 +124,7 @@ export async function updateSyncSetup(
|
|
|
119
124
|
expectedStorage?: SyncSetupStorageReview;
|
|
120
125
|
expectedInclude?: readonly string[];
|
|
121
126
|
expectedConnection?: StorageConnectionSettings;
|
|
127
|
+
expectedConfigFingerprint?: string;
|
|
122
128
|
signal?: AbortSignal;
|
|
123
129
|
} = {},
|
|
124
130
|
) {
|
|
@@ -126,6 +132,14 @@ export async function updateSyncSetup(
|
|
|
126
132
|
await updateSettings((settings) => {
|
|
127
133
|
const setup = settings.syncSetups[name];
|
|
128
134
|
if (!setup) throw new Error(`Sync setup not found: ${name}`);
|
|
135
|
+
if (
|
|
136
|
+
options.expectedConfigFingerprint !== undefined &&
|
|
137
|
+
syncCheckConfigFingerprint(configFromSettings(settings, name)) !== options.expectedConfigFingerprint
|
|
138
|
+
) {
|
|
139
|
+
throw new SyncSetupReviewChangedError(
|
|
140
|
+
`Sync setup “${name}” changed while it was open; reopen it and review the current configuration.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
129
143
|
if (options.expectedInclude && !sameNames(normalizeSyncInclude(setup.sync.include), options.expectedInclude)) {
|
|
130
144
|
throw new SyncSetupReviewChangedError(
|
|
131
145
|
`Sync setup “${name}” included content changed while it was open; reopen it and review the current selection.`,
|
|
@@ -2,8 +2,6 @@ import { localConfigPath, readMigratingLocalConfigDocument, updateLocalConfigDoc
|
|
|
2
2
|
import type { OnSwitchAction, PiSyncSettingsV3 } from "./settings-types.js";
|
|
3
3
|
import { DEFAULT_ON_SWITCH, validateSettingsDocument } from "./settings-validation.js";
|
|
4
4
|
|
|
5
|
-
let configUpdateQueue: Promise<void> = Promise.resolve();
|
|
6
|
-
|
|
7
5
|
export async function requireSettings() {
|
|
8
6
|
const settings = await readLocalConfigObject();
|
|
9
7
|
if (!settings) {
|
|
@@ -46,17 +44,5 @@ export async function readLocalConfigObject(): Promise<PiSyncSettingsV3 | undefi
|
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
export function updateLocalConfig(update: (current: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
|
|
49
|
-
const operation = configUpdateQueue.then(() => {
|
|
50
|
-
signal?.throwIfAborted();
|
|
51
|
-
return performLocalConfigUpdate(update, signal);
|
|
52
|
-
});
|
|
53
|
-
configUpdateQueue = operation.then(
|
|
54
|
-
() => undefined,
|
|
55
|
-
() => undefined,
|
|
56
|
-
);
|
|
57
|
-
return operation;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function performLocalConfigUpdate(update: (current: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
|
|
61
47
|
return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
|
|
62
48
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { type ExtensionCommandContext, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
5
|
-
import { readJsonIfExists } from "../state/json-file.js";
|
|
6
6
|
import { sessionStorageRoot } from "./snapshot-paths.js";
|
|
7
7
|
import type { Snapshot, SnapshotOptions } from "./snapshot-types.js";
|
|
8
8
|
|
|
@@ -19,7 +19,20 @@ export function sessionDirFromContext(ctx: ExtensionCommandContext | ExtensionCo
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export async function configuredSessionDir() {
|
|
22
|
-
|
|
22
|
+
let content: string;
|
|
23
|
+
try {
|
|
24
|
+
content = await fs.readFile(path.join(agentDir(), "settings.json"), "utf8");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
let settings: { sessionDir?: string } | null;
|
|
30
|
+
try {
|
|
31
|
+
// Pi accepts a leading UTF-8 BOM in its settings file; snapshot bytes stay unchanged.
|
|
32
|
+
settings = JSON.parse(content.replace(/^\uFEFF/u, "")) as typeof settings;
|
|
33
|
+
} catch {
|
|
34
|
+
throw new SyntaxError("Pi settings.json cannot be parsed; repair the file before syncing.");
|
|
35
|
+
}
|
|
23
36
|
return settings?.sessionDir ? expandSessionDir(settings.sessionDir) : undefined;
|
|
24
37
|
}
|
|
25
38
|
|