@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.
Files changed (100) hide show
  1. package/README.md +19 -11
  2. package/dist/chunks/{chunk-ORQRHEEO.ts → chunk-26PMXY7E.ts} +3 -3
  3. package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-2FETCGGT.ts} +2 -2
  4. package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-35GM6XKM.ts} +275 -151
  5. package/dist/chunks/chunk-35GM6XKM.ts.map +7 -0
  6. package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-A3TFXY4O.ts} +2 -2
  7. package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-AAZDBIBJ.ts} +2 -2
  8. package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-CAGJOKB7.ts} +5 -5
  9. package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-E6ONFG57.ts} +15 -2
  10. package/dist/chunks/{chunk-QE7NZZWT.ts.map → chunk-E6ONFG57.ts.map} +2 -2
  11. package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-GC3QOL6J.ts} +2 -2
  12. package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-HMXUKU43.ts} +3 -3
  13. package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-I4JYVFUS.ts} +5 -6
  14. package/dist/chunks/chunk-I4JYVFUS.ts.map +7 -0
  15. package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-ITFR6NNQ.ts} +14 -2
  16. package/dist/chunks/chunk-ITFR6NNQ.ts.map +7 -0
  17. package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-KATKRR3D.ts} +18 -11
  18. package/dist/chunks/chunk-KATKRR3D.ts.map +7 -0
  19. package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-KGE47GCZ.ts} +2 -2
  20. package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-R35N4CL3.ts} +2 -2
  21. package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
  22. package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
  23. package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-VCL7TZYW.ts} +2 -2
  24. package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-XU2VXGQV.ts} +14 -8
  25. package/dist/chunks/chunk-XU2VXGQV.ts.map +7 -0
  26. package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-YTHQEW35.ts} +3 -3
  27. package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-4CZFB7WI.ts} +3 -3
  28. package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-ZOTOHL2E.ts} +11 -10
  29. package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-ZOTOHL2E.ts.map} +1 -1
  30. package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-UNHBVVON.ts} +3 -3
  31. package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-HVRTPEVK.ts} +7 -7
  32. package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-ZIN4BUSF.ts} +297 -136
  33. package/dist/chunks/manager-ui-ZIN4BUSF.ts.map +7 -0
  34. package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-ICXVTCML.ts} +5 -3
  35. package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-H6AEVA3Q.ts} +6 -6
  36. package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y4P3O5B5.ts} +2 -2
  37. package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-ZMW5C4M7.ts} +4 -4
  38. package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-VW7F3JN4.ts} +5 -5
  39. package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-3EMS2J5B.ts} +5 -4
  40. package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-2CWST333.ts} +140 -24
  41. package/dist/chunks/sync-operations-2CWST333.ts.map +7 -0
  42. package/dist/index.ts +103 -68
  43. package/dist/index.ts.map +2 -2
  44. package/docs/settings.md +47 -10
  45. package/docs/tui-contract.md +61 -0
  46. package/package.json +1 -1
  47. package/src/commands/command-execution.ts +2 -1
  48. package/src/commands/command-types.ts +4 -0
  49. package/src/commands/command.ts +3 -1
  50. package/src/settings/config.ts +13 -10
  51. package/src/settings/settings-store.ts +1 -0
  52. package/src/settings/settings-types.ts +2 -0
  53. package/src/settings/settings-validation.ts +3 -0
  54. package/src/state/merge-baseline-store.ts +18 -0
  55. package/src/sync/apply-review.ts +63 -0
  56. package/src/sync/automatic-sync.ts +45 -33
  57. package/src/sync/merged-sync.ts +202 -104
  58. package/src/sync/startup-check.ts +39 -21
  59. package/src/sync/startup-timings.ts +90 -0
  60. package/src/sync/sync-inspection.ts +8 -4
  61. package/src/sync/sync-mutations.ts +49 -11
  62. package/src/sync/sync-queries.ts +14 -3
  63. package/src/ui/manager-state.ts +10 -6
  64. package/src/ui/manager-ui.ts +57 -7
  65. package/src/ui/merge-review.ts +3 -3
  66. package/src/ui/merged-sync-summary.ts +2 -18
  67. package/src/ui/settings-frame.ts +55 -0
  68. package/src/ui/settings-ui.ts +141 -43
  69. package/src/ui/setup/setup-prompts.ts +4 -3
  70. package/src/ui/sync-behavior.ts +15 -0
  71. package/src/ui/sync-format.ts +69 -38
  72. package/src/ui/transfer-changes.ts +33 -0
  73. package/src/ui/transfer-review.ts +27 -0
  74. package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
  75. package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
  76. package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
  77. package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
  78. package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
  79. package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
  80. package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
  81. /package/dist/chunks/{chunk-ORQRHEEO.ts.map → chunk-26PMXY7E.ts.map} +0 -0
  82. /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-2FETCGGT.ts.map} +0 -0
  83. /package/dist/chunks/{chunk-V6VZ5TLG.ts.map → chunk-A3TFXY4O.ts.map} +0 -0
  84. /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-AAZDBIBJ.ts.map} +0 -0
  85. /package/dist/chunks/{chunk-5ZWQ33XL.ts.map → chunk-CAGJOKB7.ts.map} +0 -0
  86. /package/dist/chunks/{chunk-IJ6VV6FN.ts.map → chunk-GC3QOL6J.ts.map} +0 -0
  87. /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-HMXUKU43.ts.map} +0 -0
  88. /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-KGE47GCZ.ts.map} +0 -0
  89. /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-R35N4CL3.ts.map} +0 -0
  90. /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-VCL7TZYW.ts.map} +0 -0
  91. /package/dist/chunks/{chunk-UQ3ALBIB.ts.map → chunk-YTHQEW35.ts.map} +0 -0
  92. /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → conflict-artifacts-4CZFB7WI.ts.map} +0 -0
  93. /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-UNHBVVON.ts.map} +0 -0
  94. /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → manager-result-dispatcher-HVRTPEVK.ts.map} +0 -0
  95. /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → merge-baseline-store-ICXVTCML.ts.map} +0 -0
  96. /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → remote-selection-ui-H6AEVA3Q.ts.map} +0 -0
  97. /package/dist/chunks/{s3-backend-AQGV6LFT.ts.map → s3-backend-Y4P3O5B5.ts.map} +0 -0
  98. /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → setup-switch-ZMW5C4M7.ts.map} +0 -0
  99. /package/dist/chunks/{setup-wizard-GSDPDFC4.ts.map → setup-wizard-VW7F3JN4.ts.map} +0 -0
  100. /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → sync-inspection-3EMS2J5B.ts.map} +0 -0
@@ -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 resolveSyncConfig(
56
- selectedName,
57
- setup,
58
- connectionName,
59
- connection,
60
- settings.onSwitch,
61
- settings.skipSecretScan ?? false,
62
- settings.showStatus ?? true,
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
 
@@ -30,6 +30,7 @@ export function localConfigTemplate(): PiSyncSettingsV3 {
30
30
  onSwitch: DEFAULT_ON_SWITCH,
31
31
  skipSecretScan: false,
32
32
  showStatus: true,
33
+ debug: false,
33
34
  storageConnections: {},
34
35
  syncSetups: {},
35
36
  };
@@ -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
- throwIfAborted(signal);
32
- const stateNotice = stateDirectoryMigrationNotice();
33
- if (stateNotice && ctx.hasUI) ctx.ui.notify(stateNotice, "warning");
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 names = settings ? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right)) : [];
49
- setSyncSetupCompletions(names);
50
- configureSyncStatus(ctx, settings?.showStatus ?? true);
51
- } catch {
52
- if (signal.aborted) return;
53
- setSyncSetupCompletions([]);
54
- }
55
- const migrationNotice = consumeLocalConfigMigrationNotice();
56
- if (migrationNotice && ctx.hasUI) ctx.ui.notify(migrationNotice, "warning");
57
- throwIfAborted(signal);
58
- if (!ctx.hasUI) return;
59
- try {
60
- const config = await loadConfig();
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
- configureSyncStatus(ctx, config.showStatus);
63
- return config.automatic || config.automaticTransfer ? config : undefined;
64
- } catch (error) {
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 (!isMissingConfigError(error)) {
67
- ctx.ui.notify(`pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}`, "warning");
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