@narumitw/pi-sync 0.54.2 → 0.54.3
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 +19 -11
- package/dist/chunks/{chunk-ORQRHEEO.ts → chunk-26PMXY7E.ts} +3 -3
- package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-2FETCGGT.ts} +2 -2
- package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-35GM6XKM.ts} +275 -151
- package/dist/chunks/chunk-35GM6XKM.ts.map +7 -0
- package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-A3TFXY4O.ts} +2 -2
- package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-AAZDBIBJ.ts} +2 -2
- package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-CAGJOKB7.ts} +5 -5
- package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-E6ONFG57.ts} +15 -2
- package/dist/chunks/{chunk-QE7NZZWT.ts.map → chunk-E6ONFG57.ts.map} +2 -2
- package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-GC3QOL6J.ts} +2 -2
- package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-HMXUKU43.ts} +3 -3
- package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-I4JYVFUS.ts} +5 -6
- package/dist/chunks/chunk-I4JYVFUS.ts.map +7 -0
- package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-ITFR6NNQ.ts} +14 -2
- package/dist/chunks/chunk-ITFR6NNQ.ts.map +7 -0
- package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-KATKRR3D.ts} +18 -11
- package/dist/chunks/chunk-KATKRR3D.ts.map +7 -0
- package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-KGE47GCZ.ts} +2 -2
- package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-R35N4CL3.ts} +2 -2
- package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
- package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
- package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-VCL7TZYW.ts} +2 -2
- package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-XU2VXGQV.ts} +14 -8
- package/dist/chunks/chunk-XU2VXGQV.ts.map +7 -0
- package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-YTHQEW35.ts} +3 -3
- package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-4CZFB7WI.ts} +3 -3
- package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-ZOTOHL2E.ts} +11 -10
- package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-ZOTOHL2E.ts.map} +1 -1
- package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-UNHBVVON.ts} +3 -3
- package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-HVRTPEVK.ts} +7 -7
- package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-ZIN4BUSF.ts} +297 -136
- package/dist/chunks/manager-ui-ZIN4BUSF.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-ICXVTCML.ts} +5 -3
- package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-H6AEVA3Q.ts} +6 -6
- package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y4P3O5B5.ts} +2 -2
- package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-ZMW5C4M7.ts} +4 -4
- package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-VW7F3JN4.ts} +5 -5
- package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-3EMS2J5B.ts} +5 -4
- package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-2CWST333.ts} +140 -24
- package/dist/chunks/sync-operations-2CWST333.ts.map +7 -0
- package/dist/index.ts +103 -68
- package/dist/index.ts.map +2 -2
- package/docs/settings.md +47 -10
- package/docs/tui-contract.md +61 -0
- package/package.json +1 -1
- package/src/commands/command-execution.ts +2 -1
- package/src/commands/command-types.ts +4 -0
- package/src/commands/command.ts +3 -1
- package/src/settings/config.ts +13 -10
- package/src/settings/settings-store.ts +1 -0
- package/src/settings/settings-types.ts +2 -0
- package/src/settings/settings-validation.ts +3 -0
- package/src/state/merge-baseline-store.ts +18 -0
- package/src/sync/apply-review.ts +63 -0
- package/src/sync/automatic-sync.ts +45 -33
- package/src/sync/merged-sync.ts +202 -104
- package/src/sync/startup-check.ts +39 -21
- package/src/sync/startup-timings.ts +90 -0
- package/src/sync/sync-inspection.ts +8 -4
- package/src/sync/sync-mutations.ts +49 -11
- package/src/sync/sync-queries.ts +14 -3
- package/src/ui/manager-state.ts +10 -6
- package/src/ui/manager-ui.ts +57 -7
- package/src/ui/merge-review.ts +3 -3
- package/src/ui/merged-sync-summary.ts +2 -18
- package/src/ui/settings-frame.ts +55 -0
- package/src/ui/settings-ui.ts +141 -43
- package/src/ui/setup/setup-prompts.ts +4 -3
- package/src/ui/sync-behavior.ts +15 -0
- package/src/ui/sync-format.ts +69 -38
- package/src/ui/transfer-changes.ts +33 -0
- package/src/ui/transfer-review.ts +27 -0
- package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
- package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
- package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
- package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
- package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
- package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
- package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
- /package/dist/chunks/{chunk-ORQRHEEO.ts.map → chunk-26PMXY7E.ts.map} +0 -0
- /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-2FETCGGT.ts.map} +0 -0
- /package/dist/chunks/{chunk-V6VZ5TLG.ts.map → chunk-A3TFXY4O.ts.map} +0 -0
- /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-AAZDBIBJ.ts.map} +0 -0
- /package/dist/chunks/{chunk-5ZWQ33XL.ts.map → chunk-CAGJOKB7.ts.map} +0 -0
- /package/dist/chunks/{chunk-IJ6VV6FN.ts.map → chunk-GC3QOL6J.ts.map} +0 -0
- /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-HMXUKU43.ts.map} +0 -0
- /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-KGE47GCZ.ts.map} +0 -0
- /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-R35N4CL3.ts.map} +0 -0
- /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-VCL7TZYW.ts.map} +0 -0
- /package/dist/chunks/{chunk-UQ3ALBIB.ts.map → chunk-YTHQEW35.ts.map} +0 -0
- /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → conflict-artifacts-4CZFB7WI.ts.map} +0 -0
- /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-UNHBVVON.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → manager-result-dispatcher-HVRTPEVK.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → merge-baseline-store-ICXVTCML.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → remote-selection-ui-H6AEVA3Q.ts.map} +0 -0
- /package/dist/chunks/{s3-backend-AQGV6LFT.ts.map → s3-backend-Y4P3O5B5.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → setup-switch-ZMW5C4M7.ts.map} +0 -0
- /package/dist/chunks/{setup-wizard-GSDPDFC4.ts.map → setup-wizard-VW7F3JN4.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → sync-inspection-3EMS2J5B.ts.map} +0 -0
package/src/settings/config.ts
CHANGED
|
@@ -52,15 +52,18 @@ function configFromSettings(settings: PiSyncSettingsV3, setupName?: string): Any
|
|
|
52
52
|
`Invalid pi-sync settings: sync setup “${selectedName}” references missing storage connection “${connectionName}”.`,
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
return {
|
|
56
|
+
...resolveSyncConfig(
|
|
57
|
+
selectedName,
|
|
58
|
+
setup,
|
|
59
|
+
connectionName,
|
|
60
|
+
connection,
|
|
61
|
+
settings.onSwitch,
|
|
62
|
+
settings.skipSecretScan ?? false,
|
|
63
|
+
settings.showStatus ?? true,
|
|
64
|
+
),
|
|
65
|
+
debug: settings.debug ?? false,
|
|
66
|
+
};
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
export type SyncSetupStorageReview = Pick<
|
|
@@ -126,7 +129,7 @@ export function syncConfigReviewFingerprint(config: AnySyncConfig) {
|
|
|
126
129
|
/** Internal freshness token, including credentials; never display or persist it. */
|
|
127
130
|
export function syncCheckConfigFingerprint(config: AnySyncConfig) {
|
|
128
131
|
return createHash("sha256")
|
|
129
|
-
.update(JSON.stringify({ ...config, showStatus: undefined }))
|
|
132
|
+
.update(JSON.stringify({ ...config, showStatus: undefined, debug: undefined }))
|
|
130
133
|
.digest("hex");
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -101,6 +101,7 @@ export interface PiSyncSettingsV3 {
|
|
|
101
101
|
onSwitch: OnSwitchAction;
|
|
102
102
|
skipSecretScan?: boolean;
|
|
103
103
|
showStatus?: boolean;
|
|
104
|
+
debug?: boolean;
|
|
104
105
|
storageConnections: Record<string, StorageConnectionSettings>;
|
|
105
106
|
syncSetups: Record<string, SyncSetupSettings>;
|
|
106
107
|
[key: string]: unknown;
|
|
@@ -122,6 +123,7 @@ export interface SyncConfig<Backend extends ResolvedSyncBackend = ResolvedS3Back
|
|
|
122
123
|
onSwitch: OnSwitchAction;
|
|
123
124
|
skipSecretScan: boolean;
|
|
124
125
|
showStatus: boolean;
|
|
126
|
+
debug?: boolean;
|
|
125
127
|
backend: Backend;
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -68,6 +68,9 @@ export function validateSettingsDocument(value: Record<string, unknown>): PiSync
|
|
|
68
68
|
if (value.showStatus !== undefined && typeof value.showStatus !== "boolean") {
|
|
69
69
|
throw new Error("Invalid pi-sync settings: showStatus must be boolean.");
|
|
70
70
|
}
|
|
71
|
+
if (value.debug !== undefined && typeof value.debug !== "boolean") {
|
|
72
|
+
throw new Error("Invalid pi-sync settings: debug must be boolean.");
|
|
73
|
+
}
|
|
71
74
|
const storageConnections = requireNamedObjectMap(
|
|
72
75
|
value.storageConnections,
|
|
73
76
|
"storageConnections",
|
|
@@ -145,6 +145,24 @@ export async function readMergeAncestors(config: AnySyncConfig, state: SyncState
|
|
|
145
145
|
}
|
|
146
146
|
return record.files;
|
|
147
147
|
}
|
|
148
|
+
/** A no-op must not skip seeding or repairing ancestors required by later merges. */
|
|
149
|
+
export async function hasCurrentMergeBaseline(config: AnySyncConfig, snapshot: Snapshot, state: SyncState) {
|
|
150
|
+
try {
|
|
151
|
+
const ancestors = await readMergeAncestors(config, state);
|
|
152
|
+
if (!ancestors) return false;
|
|
153
|
+
const hashes = new Map(ancestors.map((file) => [file.path, file.sha256]));
|
|
154
|
+
return snapshot.files.every(
|
|
155
|
+
(file) =>
|
|
156
|
+
!(file.path === "settings.json" || config.mergeContent) ||
|
|
157
|
+
!eligible(file) ||
|
|
158
|
+
hashes.get(file.path) === file.sha256,
|
|
159
|
+
);
|
|
160
|
+
} catch {
|
|
161
|
+
// Preserve the existing full path's validation and repair behavior for unknown evidence.
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
148
166
|
export async function readMergeAncestor(config: AnySyncConfig, state: SyncState, filePath: string) {
|
|
149
167
|
const file = (await readMergeAncestors(config, state))?.find((file) => file.path === filePath);
|
|
150
168
|
return file ? Buffer.from(file.contentBase64, "base64") : undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { agentDir } from "../snapshot/session-paths.js";
|
|
6
|
+
import { addTopLevelCaseVariantDeletes } from "../snapshot/snapshot-apply.js";
|
|
7
|
+
import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
8
|
+
import { throwIfAborted } from "./signals.js";
|
|
9
|
+
|
|
10
|
+
export interface CaseCleanupReview {
|
|
11
|
+
path: string;
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Inspect the apply engine's additional alias removals, which logical snapshots omit. */
|
|
16
|
+
export async function inspectCaseCleanup(snapshot: Snapshot, signal?: AbortSignal): Promise<CaseCleanupReview[]> {
|
|
17
|
+
throwIfAborted(signal);
|
|
18
|
+
const root = agentDir();
|
|
19
|
+
const plan = await addTopLevelCaseVariantDeletes(root, { writes: [], deletes: [] }, snapshot);
|
|
20
|
+
throwIfAborted(signal);
|
|
21
|
+
const review: CaseCleanupReview[] = [];
|
|
22
|
+
for (const target of plan.deletes.sort()) {
|
|
23
|
+
const stat = await fs.lstat(target);
|
|
24
|
+
throwIfAborted(signal);
|
|
25
|
+
const hash = createHash("sha256");
|
|
26
|
+
if (stat.isSymbolicLink()) hash.update(await fs.readlink(target));
|
|
27
|
+
else {
|
|
28
|
+
if (!stat.isFile()) throw new Error("Case-variant path is no longer a regular file or symlink.");
|
|
29
|
+
const handle = await fs.open(
|
|
30
|
+
target,
|
|
31
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
32
|
+
);
|
|
33
|
+
try {
|
|
34
|
+
throwIfAborted(signal);
|
|
35
|
+
const opened = await handle.stat();
|
|
36
|
+
if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino)
|
|
37
|
+
throw new Error("Case-variant file changed while preparing review.");
|
|
38
|
+
for await (const bytes of handle.createReadStream({ autoClose: false, signal })) {
|
|
39
|
+
throwIfAborted(signal);
|
|
40
|
+
hash.update(bytes);
|
|
41
|
+
}
|
|
42
|
+
} finally {
|
|
43
|
+
await handle.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throwIfAborted(signal);
|
|
47
|
+
review.push({
|
|
48
|
+
path: path.basename(target),
|
|
49
|
+
fingerprint: `${stat.dev}:${stat.ino}:${stat.mode}:${hash.digest("hex")}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return review;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function revalidateCaseCleanup(
|
|
56
|
+
expected: readonly CaseCleanupReview[],
|
|
57
|
+
snapshot: Snapshot,
|
|
58
|
+
signal?: AbortSignal,
|
|
59
|
+
) {
|
|
60
|
+
const current = await inspectCaseCleanup(snapshot, signal);
|
|
61
|
+
if (JSON.stringify(current) !== JSON.stringify(expected))
|
|
62
|
+
throw new Error("Case-variant paths changed during transfer review; reopen the preview.");
|
|
63
|
+
}
|
|
@@ -13,6 +13,7 @@ import { ensureStateDir, readStateForConfig } from "../state/sync-state-store.js
|
|
|
13
13
|
import { configureSyncStatus, setSyncStatus } from "../ui/sync-status.js";
|
|
14
14
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
15
15
|
import { throwIfAborted } from "./signals.js";
|
|
16
|
+
import { createStartupTimings } from "./startup-timings.js";
|
|
16
17
|
import { errorMessage } from "./sync-errors.js";
|
|
17
18
|
import type { SyncLoaders } from "./sync-loaders.js";
|
|
18
19
|
import { captureMutationOwner } from "./sync-local.js";
|
|
@@ -28,44 +29,55 @@ const AUTO_SYNC_OPTIONS: CommandOptions = {
|
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export async function startSession(ctx: ExtensionContext, signal: AbortSignal) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Recovery can restore managed files: finish it before Pi accepts user edits.
|
|
35
|
-
// Unlike remote inspection, it must never be detached behind startup.
|
|
36
|
-
const validateMutation = captureMutationOwner(ctx, signal);
|
|
37
|
-
const currentFile = ctx.sessionManager.getSessionFile?.();
|
|
38
|
-
await recoverSnapshotTransactionsOnStartup({
|
|
39
|
-
signal,
|
|
40
|
-
sessionDir: sessionDirFromContext(ctx),
|
|
41
|
-
protectedTargets: currentFile ? [currentFile] : [],
|
|
42
|
-
validateMutation,
|
|
43
|
-
});
|
|
44
|
-
throwIfAborted(signal);
|
|
32
|
+
const timings = createStartupTimings();
|
|
33
|
+
let debug = false;
|
|
34
|
+
let outcome: "completed" | "stopped" = "stopped";
|
|
45
35
|
try {
|
|
46
|
-
const settings = await readLocalConfigObject();
|
|
47
36
|
throwIfAborted(signal);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
37
|
+
const stateNotice = stateDirectoryMigrationNotice();
|
|
38
|
+
if (stateNotice && ctx.hasUI) ctx.ui.notify(stateNotice, "warning");
|
|
39
|
+
// Recovery can restore managed files: finish it before Pi accepts user edits.
|
|
40
|
+
// Unlike remote inspection, it must never be detached behind startup.
|
|
41
|
+
const validateMutation = captureMutationOwner(ctx, signal);
|
|
42
|
+
const currentFile = ctx.sessionManager.getSessionFile?.();
|
|
43
|
+
await timings.measure("transaction recovery", () =>
|
|
44
|
+
recoverSnapshotTransactionsOnStartup({
|
|
45
|
+
signal,
|
|
46
|
+
sessionDir: sessionDirFromContext(ctx),
|
|
47
|
+
protectedTargets: currentFile ? [currentFile] : [],
|
|
48
|
+
validateMutation,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
61
51
|
throwIfAborted(signal);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
try {
|
|
53
|
+
const settings = await timings.measure("settings read", () => readLocalConfigObject());
|
|
54
|
+
throwIfAborted(signal);
|
|
55
|
+
debug = settings?.debug ?? false;
|
|
56
|
+
const names = settings ? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right)) : [];
|
|
57
|
+
setSyncSetupCompletions(names);
|
|
58
|
+
configureSyncStatus(ctx, settings?.showStatus ?? true);
|
|
59
|
+
} catch {
|
|
60
|
+
if (signal.aborted) return;
|
|
61
|
+
setSyncSetupCompletions([]);
|
|
62
|
+
}
|
|
63
|
+
const migrationNotice = consumeLocalConfigMigrationNotice();
|
|
64
|
+
if (migrationNotice && ctx.hasUI) ctx.ui.notify(migrationNotice, "warning");
|
|
65
65
|
throwIfAborted(signal);
|
|
66
|
-
if (!
|
|
67
|
-
|
|
66
|
+
if (!ctx.hasUI) return;
|
|
67
|
+
try {
|
|
68
|
+
const config = await timings.measure("config resolution", () => loadConfig());
|
|
69
|
+
throwIfAborted(signal);
|
|
70
|
+
configureSyncStatus(ctx, config.showStatus);
|
|
71
|
+
outcome = "completed";
|
|
72
|
+
return config.automatic || config.automaticTransfer ? config : undefined;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throwIfAborted(signal);
|
|
75
|
+
if (!isMissingConfigError(error)) {
|
|
76
|
+
ctx.ui.notify(`pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}`, "warning");
|
|
77
|
+
}
|
|
68
78
|
}
|
|
79
|
+
} finally {
|
|
80
|
+
timings.publish(ctx, debug, signal, "local initialization", outcome);
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
|