@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.
Files changed (209) hide show
  1. package/README.md +134 -241
  2. package/dist/chunks/chunk-2B2ELUF5.ts +29 -0
  3. package/dist/chunks/chunk-2B2ELUF5.ts.map +7 -0
  4. package/dist/chunks/chunk-6LP3MCR2.ts +115 -0
  5. package/dist/chunks/chunk-6LP3MCR2.ts.map +7 -0
  6. package/dist/chunks/chunk-6UHKGALE.ts +134 -0
  7. package/dist/chunks/chunk-6UHKGALE.ts.map +7 -0
  8. package/dist/chunks/{chunk-RGJ6SOYR.ts → chunk-AUSDLD6F.ts} +4 -4
  9. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  10. package/dist/chunks/{chunk-3NDD5QPM.ts → chunk-AXXS2BPA.ts} +9 -9
  11. package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
  12. package/dist/chunks/{chunk-APZVBSM6.ts → chunk-C7XY5NF5.ts} +214 -365
  13. package/dist/chunks/chunk-C7XY5NF5.ts.map +7 -0
  14. package/dist/chunks/{chunk-4U6AFLJB.ts → chunk-CHH26D4F.ts} +3 -2
  15. package/dist/chunks/chunk-CHH26D4F.ts.map +7 -0
  16. package/dist/chunks/chunk-CPEHGXJV.ts +50 -0
  17. package/dist/chunks/chunk-CPEHGXJV.ts.map +7 -0
  18. package/dist/chunks/{chunk-5MQ76BK5.ts → chunk-D5X27LCA.ts} +2 -2
  19. package/dist/chunks/chunk-D5X27LCA.ts.map +7 -0
  20. package/dist/chunks/{chunk-LJDSHNS3.ts → chunk-GWMHAQG4.ts} +4 -4
  21. package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
  22. package/dist/chunks/{chunk-FR6CVXAL.ts → chunk-HUDQATCL.ts} +25 -25
  23. package/dist/chunks/chunk-HUDQATCL.ts.map +7 -0
  24. package/dist/chunks/{chunk-YQ6UW7IF.ts → chunk-JAAKLL6M.ts} +2 -2
  25. package/dist/chunks/chunk-JAAKLL6M.ts.map +7 -0
  26. package/dist/chunks/chunk-JSYMGJQQ.ts +51 -0
  27. package/dist/chunks/chunk-JSYMGJQQ.ts.map +7 -0
  28. package/dist/chunks/chunk-L7TBO32M.ts +25 -0
  29. package/dist/chunks/chunk-L7TBO32M.ts.map +7 -0
  30. package/dist/chunks/{chunk-4F3W4CSC.ts → chunk-LCRI3ABW.ts} +22 -8
  31. package/dist/chunks/chunk-LCRI3ABW.ts.map +7 -0
  32. package/dist/chunks/chunk-LTBWGSBV.ts +12 -0
  33. package/dist/chunks/chunk-LTBWGSBV.ts.map +7 -0
  34. package/dist/chunks/chunk-NJFUIVYL.ts +17 -0
  35. package/dist/chunks/chunk-NJFUIVYL.ts.map +7 -0
  36. package/dist/chunks/{chunk-EWX3TPLQ.ts → chunk-OEX4ABY6.ts} +39 -37
  37. package/dist/chunks/chunk-OEX4ABY6.ts.map +7 -0
  38. package/dist/chunks/chunk-OUHGBFYF.ts +1255 -0
  39. package/dist/chunks/chunk-OUHGBFYF.ts.map +7 -0
  40. package/dist/chunks/{chunk-HGV24P4T.ts → chunk-Q2TUM5X6.ts} +3 -3
  41. package/dist/chunks/chunk-Q2TUM5X6.ts.map +7 -0
  42. package/dist/chunks/{chunk-BHJUISB4.ts → chunk-SL64RLXR.ts} +8 -6
  43. package/dist/chunks/chunk-SL64RLXR.ts.map +7 -0
  44. package/dist/chunks/chunk-T3HH3UEJ.ts +96 -0
  45. package/dist/chunks/chunk-T3HH3UEJ.ts.map +7 -0
  46. package/dist/chunks/chunk-TEN2ZBJV.ts +101 -0
  47. package/dist/chunks/chunk-TEN2ZBJV.ts.map +7 -0
  48. package/dist/chunks/chunk-THMF4BVL.ts +18 -0
  49. package/dist/chunks/chunk-THMF4BVL.ts.map +7 -0
  50. package/dist/chunks/{chunk-LONPFFNP.ts → chunk-X5OM6GXT.ts} +9 -7
  51. package/dist/chunks/chunk-X5OM6GXT.ts.map +7 -0
  52. package/dist/chunks/chunk-XBX4HBLG.ts +57 -0
  53. package/dist/chunks/chunk-XBX4HBLG.ts.map +7 -0
  54. package/dist/chunks/{chunk-5WJNWFTG.ts → chunk-YKJHPTGO.ts} +23 -55
  55. package/dist/chunks/chunk-YKJHPTGO.ts.map +7 -0
  56. package/dist/chunks/chunk-Z4DQF6L3.ts +12 -0
  57. package/dist/chunks/chunk-Z4DQF6L3.ts.map +7 -0
  58. package/dist/chunks/{chunk-F5QL2GPY.ts → chunk-ZI5MXMEX.ts} +5 -99
  59. package/dist/chunks/chunk-ZI5MXMEX.ts.map +7 -0
  60. package/dist/chunks/{file-selection-4UABOP44.ts → file-selection-FTTKAOYX.ts} +10 -7
  61. package/dist/chunks/file-selection-FTTKAOYX.ts.map +7 -0
  62. package/dist/chunks/{git-backend-E3UOUUOM.ts → git-backend-OQW46G2X.ts} +42 -17
  63. package/dist/chunks/git-backend-OQW46G2X.ts.map +7 -0
  64. package/dist/chunks/manager-result-dispatcher-U23ODD32.ts +22 -0
  65. package/dist/chunks/manager-ui-ABATHC36.ts +1680 -0
  66. package/dist/chunks/manager-ui-ABATHC36.ts.map +7 -0
  67. package/dist/chunks/remote-selection-ui-ZE3TY67S.ts +21 -0
  68. package/dist/chunks/{s3-backend-3QZGO2EF.ts → s3-backend-3P33LQHU.ts} +96 -18
  69. package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
  70. package/dist/chunks/{setup-switch-X3W3FALZ.ts → setup-switch-IS2PEZ3J.ts} +9 -9
  71. package/dist/chunks/setup-wizard-VB3VDOV5.ts +89 -0
  72. package/dist/chunks/setup-wizard-VB3VDOV5.ts.map +7 -0
  73. package/dist/chunks/{snapshot-WBSV4MQG.ts → snapshot-HKEWCVK6.ts} +7 -6
  74. package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
  75. package/dist/chunks/{sync-operations-MPAFVVHA.ts → sync-operations-ONJ27QRL.ts} +381 -299
  76. package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
  77. package/dist/chunks/{sync-state-36H7ULG2.ts → sync-state-Q2FTXTW6.ts} +6 -6
  78. package/dist/chunks/sync-state-Q2FTXTW6.ts.map +7 -0
  79. package/dist/chunks/{webdav-backend-ZD5RHRIU.ts → webdav-backend-ECIKD7SO.ts} +26 -13
  80. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +7 -0
  81. package/dist/index.ts +477 -342
  82. package/dist/index.ts.map +4 -4
  83. package/docs/settings.md +211 -0
  84. package/package.json +6 -8
  85. package/src/{backend-factory.ts → backends/backend-factory.ts} +4 -4
  86. package/src/backends/backend-identity.ts +40 -0
  87. package/src/backends/backend-types.ts +81 -0
  88. package/src/{git-backend.ts → backends/git/git-backend.ts} +43 -19
  89. package/src/{git-config.ts → backends/git/git-config.ts} +1 -0
  90. package/src/{git-storage.ts → backends/git/git-storage.ts} +2 -2
  91. package/src/{s3-backend.ts → backends/s3/s3-backend.ts} +45 -9
  92. package/src/{s3-client.ts → backends/s3/s3-client.ts} +56 -8
  93. package/src/{sync-backend.ts → backends/sync-backend.ts} +1 -1
  94. package/src/{webdav-backend.ts → backends/webdav/webdav-backend.ts} +22 -10
  95. package/src/{webdav-client.ts → backends/webdav/webdav-client.ts} +1 -1
  96. package/src/{webdav-config.ts → backends/webdav/webdav-config.ts} +1 -0
  97. package/src/commands/command-execution.ts +330 -0
  98. package/src/commands/command-handler.ts +201 -0
  99. package/src/commands/command-types.ts +18 -0
  100. package/src/{command.ts → commands/command.ts} +2 -1
  101. package/src/commands/setup-completions.ts +6 -0
  102. package/src/settings/config.ts +238 -0
  103. package/src/{settings-management.ts → settings/settings-management.ts} +40 -14
  104. package/src/settings/settings-store.ts +109 -0
  105. package/src/settings/settings-types.ts +128 -0
  106. package/src/settings/settings-validation.ts +443 -0
  107. package/src/snapshot/session-paths.ts +84 -0
  108. package/src/{snapshot-apply.ts → snapshot/snapshot-apply.ts} +3 -3
  109. package/src/{snapshot-codec.ts → snapshot/snapshot-codec.ts} +2 -2
  110. package/src/{snapshot-transaction.ts → snapshot/snapshot-transaction.ts} +5 -4
  111. package/src/snapshot/snapshot-types.ts +38 -0
  112. package/src/{snapshot.ts → snapshot/snapshot.ts} +39 -20
  113. package/src/state/json-file.ts +20 -0
  114. package/src/{lock.ts → state/lock.ts} +3 -2
  115. package/src/{operation-availability.ts → state/operation-availability.ts} +2 -2
  116. package/src/state/state-types.ts +20 -0
  117. package/src/state/sync-state-store.ts +62 -0
  118. package/src/sync/automatic-sync.ts +103 -0
  119. package/src/{remote-snapshot.ts → sync/remote-snapshot.ts} +27 -4
  120. package/src/{setup-switch.ts → sync/setup-switch.ts} +6 -7
  121. package/src/sync/signals.ts +10 -0
  122. package/src/sync/startup-check.ts +128 -0
  123. package/src/{sync-decision.ts → sync/sync-decision.ts} +6 -3
  124. package/src/sync/sync-error-guidance.ts +53 -0
  125. package/src/sync/sync-inspection.ts +51 -0
  126. package/src/sync/sync-loaders.ts +54 -0
  127. package/src/{sync-operations.ts → sync/sync-mutations.ts} +41 -330
  128. package/src/sync/sync-operations.ts +12 -0
  129. package/src/{sync-policy.ts → sync/sync-policy.ts} +2 -2
  130. package/src/sync/sync-queries.ts +235 -0
  131. package/src/{sync-state.ts → sync/sync-state.ts} +5 -4
  132. package/src/sync-extension.ts +64 -708
  133. package/src/sync.ts +8 -8
  134. package/src/types.ts +47 -284
  135. package/src/ui/automatic-sync-summary.ts +6 -0
  136. package/src/{cancellable-operation.ts → ui/cancellable-operation.ts} +4 -4
  137. package/src/{file-selection.ts → ui/file-selection.ts} +5 -3
  138. package/src/{manager-attention.ts → ui/manager-attention.ts} +5 -1
  139. package/src/{manager-recovery.ts → ui/manager-recovery.ts} +6 -2
  140. package/src/{manager-state.ts → ui/manager-state.ts} +35 -14
  141. package/src/ui/manager-ui.ts +386 -0
  142. package/src/{remote-selection-ui.ts → ui/remote-selection-ui.ts} +21 -13
  143. package/src/{secret-input.ts → ui/secret-input.ts} +22 -4
  144. package/src/{settings-ui.ts → ui/settings-ui.ts} +42 -11
  145. package/src/ui/setup/git-ui.ts +293 -0
  146. package/src/{s3-credentials-ui.ts → ui/setup/s3-credentials-ui.ts} +13 -21
  147. package/src/ui/setup/s3-ui.ts +217 -0
  148. package/src/ui/setup/setup-actions.ts +127 -0
  149. package/src/ui/setup/setup-location-ui.ts +167 -0
  150. package/src/ui/setup/setup-name-ui.ts +34 -0
  151. package/src/ui/setup/setup-prompts.ts +92 -0
  152. package/src/ui/setup/setup-review.ts +70 -0
  153. package/src/ui/setup/setup-switcher.ts +181 -0
  154. package/src/ui/setup/setup-wizard.ts +39 -0
  155. package/src/ui/setup/text-input.ts +78 -0
  156. package/src/ui/setup/webdav-ui.ts +330 -0
  157. package/src/{storage-connections-ui.ts → ui/storage-connections-ui.ts} +99 -69
  158. package/src/{sync-attention.ts → ui/sync-attention.ts} +75 -6
  159. package/src/{sync-format.ts → ui/sync-format.ts} +7 -13
  160. package/src/{sync-resolution-ui.ts → ui/sync-resolution-ui.ts} +4 -3
  161. package/src/{sync-setups-ui.ts → ui/sync-setups-ui.ts} +23 -10
  162. package/src/ui/terminal-text.ts +4 -0
  163. package/dist/chunks/chunk-3NDD5QPM.ts.map +0 -7
  164. package/dist/chunks/chunk-4F3W4CSC.ts.map +0 -7
  165. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  166. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  167. package/dist/chunks/chunk-5WJNWFTG.ts.map +0 -7
  168. package/dist/chunks/chunk-APZVBSM6.ts.map +0 -7
  169. package/dist/chunks/chunk-BHJUISB4.ts.map +0 -7
  170. package/dist/chunks/chunk-EWX3TPLQ.ts.map +0 -7
  171. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  172. package/dist/chunks/chunk-FR6CVXAL.ts.map +0 -7
  173. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  174. package/dist/chunks/chunk-JNOOSPEW.ts +0 -82
  175. package/dist/chunks/chunk-JNOOSPEW.ts.map +0 -7
  176. package/dist/chunks/chunk-LJDSHNS3.ts.map +0 -7
  177. package/dist/chunks/chunk-LONPFFNP.ts.map +0 -7
  178. package/dist/chunks/chunk-RGJ6SOYR.ts.map +0 -7
  179. package/dist/chunks/chunk-W64NEHXT.ts +0 -167
  180. package/dist/chunks/chunk-W64NEHXT.ts.map +0 -7
  181. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  182. package/dist/chunks/file-selection-4UABOP44.ts.map +0 -7
  183. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  184. package/dist/chunks/manager-result-dispatcher-RNRQA3PW.ts +0 -18
  185. package/dist/chunks/manager-ui-QO2P4V3Z.ts +0 -2757
  186. package/dist/chunks/manager-ui-QO2P4V3Z.ts.map +0 -7
  187. package/dist/chunks/remote-selection-ui-LHSAGKGA.ts +0 -17
  188. package/dist/chunks/s3-backend-3QZGO2EF.ts.map +0 -7
  189. package/dist/chunks/sync-operations-MPAFVVHA.ts.map +0 -7
  190. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  191. package/src/config.ts +0 -910
  192. package/src/git-ui.ts +0 -307
  193. package/src/manager-helpers.ts +0 -64
  194. package/src/manager-ui.ts +0 -997
  195. package/src/webdav-ui.ts +0 -421
  196. /package/dist/chunks/{manager-result-dispatcher-RNRQA3PW.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
  197. /package/dist/chunks/{remote-selection-ui-LHSAGKGA.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
  198. /package/dist/chunks/{setup-switch-X3W3FALZ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  199. /package/dist/chunks/{snapshot-WBSV4MQG.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  200. /package/dist/chunks/{sync-state-36H7ULG2.ts.map → sync-inspection-FEP4L5TR.ts.map} +0 -0
  201. /package/src/{git-runner.ts → backends/git/git-runner.ts} +0 -0
  202. /package/src/{config-errors.ts → settings/config-errors.ts} +0 -0
  203. /package/src/{config-file.ts → settings/config-file.ts} +0 -0
  204. /package/src/{snapshot-paths.ts → snapshot/snapshot-paths.ts} +0 -0
  205. /package/src/{lock-policy.ts → state/lock-policy.ts} +0 -0
  206. /package/src/{lockfile-fs.ts → state/lockfile-fs.ts} +0 -0
  207. /package/src/{state-directory.ts → state/state-directory.ts} +0 -0
  208. /package/src/{sync-errors.ts → sync/sync-errors.ts} +0 -0
  209. /package/src/{manager-result-dispatcher.ts → ui/manager-result-dispatcher.ts} +0 -0
@@ -0,0 +1,386 @@
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { type ActionMenuItem, defineMenu, runMenu } from "@narumitw/pi-tui-kit";
3
+ import { loadConfig } from "../settings/config.js";
4
+ import { operationBlocksChanges, operationCanRecover } from "../state/operation-availability.js";
5
+ import { type RunRoute, runCancellableOperation } from "./cancellable-operation.js";
6
+ import {
7
+ attentionMainMenuItems,
8
+ blockedSyncMenuItem,
9
+ type SyncManagerAttentionOptions,
10
+ showManagerAttention,
11
+ } from "./manager-attention.js";
12
+ import { recoverSyncAccess } from "./manager-recovery.js";
13
+ import { dispatchManagerResult } from "./manager-result-dispatcher.js";
14
+ import { describeManagerState } from "./manager-state.js";
15
+ import { showSyncSettings } from "./settings-ui.js";
16
+ import { showSyncSetupManager } from "./setup/setup-actions.js";
17
+ import { showSetupSwitcher } from "./setup/setup-switcher.js";
18
+ import { showStorageConnections } from "./storage-connections-ui.js";
19
+ import { observationMatchesConfig } from "./sync-attention.js";
20
+
21
+ export async function showSyncManager(
22
+ ctx: ExtensionCommandContext,
23
+ runRoute: RunRoute,
24
+ sessionSignal?: AbortSignal,
25
+ options: SyncManagerAttentionOptions = {},
26
+ ): Promise<void> {
27
+ if (!ctx.hasUI) {
28
+ await runRoute("help");
29
+ return;
30
+ }
31
+ type Screen = "main" | "more" | "recovery";
32
+ type Action =
33
+ | "review-attention"
34
+ | "sync"
35
+ | "switch"
36
+ | "diff"
37
+ | "settings"
38
+ | "pull"
39
+ | "push"
40
+ | "setups"
41
+ | "connections"
42
+ | "history"
43
+ | "doctor"
44
+ | "unlock"
45
+ | "recover"
46
+ | "refresh"
47
+ | "help"
48
+ | "init"
49
+ | "back";
50
+ interface State {
51
+ manager: Awaited<ReturnType<typeof describeManagerState>>;
52
+ }
53
+ const menu = defineMenu<State, Screen, Action, ExtensionCommandContext>({
54
+ start: "main",
55
+ screens: {
56
+ main: ({ state }) => {
57
+ const attentionItems = attentionMainMenuItems(state.manager);
58
+ const managerItems = state.manager.actions.map(
59
+ (label) => blockedSyncMenuItem(label, state.manager) ?? syncMainMenuItem(label),
60
+ );
61
+ const operationFirst =
62
+ state.manager.operation !== undefined && state.manager.operation.kind !== "free";
63
+ return {
64
+ kind: "actions",
65
+ title: "Manage sync",
66
+ lines: state.manager.title.split("\n").slice(1),
67
+ items: operationFirst
68
+ ? [...managerItems, ...attentionItems]
69
+ : [...attentionItems, ...managerItems],
70
+ hint: "close",
71
+ };
72
+ },
73
+ more: () => ({
74
+ kind: "actions",
75
+ title: "More options",
76
+ items: [
77
+ { id: "pull", label: "Pull from remote…", action: "pull" },
78
+ { id: "push", label: "Push to remote…", action: "push" },
79
+ { id: "setups", label: "Sync setups…", action: "setups" },
80
+ {
81
+ id: "connections",
82
+ label: "Storage connections…",
83
+ action: "connections",
84
+ },
85
+ {
86
+ id: "doctor",
87
+ label: "Check setup",
88
+ description:
89
+ "Check current setup access. WebDAV also probes writes and repairs history.",
90
+ action: "doctor",
91
+ },
92
+ { id: "recovery", label: "History & recovery…", to: "recovery" },
93
+ { id: "help", label: "Help", action: "help" },
94
+ { id: "back", label: "Back", action: "back" },
95
+ ],
96
+ hint: "back",
97
+ }),
98
+ recovery: ({ state }) => ({
99
+ kind: "actions",
100
+ title: "History & recovery",
101
+ items: [
102
+ { id: "history", label: "Browse history", action: "history" },
103
+ {
104
+ id: "doctor",
105
+ label: "Check setup",
106
+ description: "WebDAV also probes writes and repairs history.",
107
+ action: "doctor",
108
+ },
109
+ ...(state.manager.operation && operationCanRecover(state.manager.operation)
110
+ ? [{ id: "unlock", label: "Recover stale operation", action: "unlock" as const }]
111
+ : []),
112
+ { id: "back", label: "Back", action: "back" },
113
+ ],
114
+ hint: "back",
115
+ }),
116
+ },
117
+ actions: {
118
+ "review-attention": async () => {
119
+ const attention = options.getAttention?.();
120
+ if (!attention) {
121
+ const observation = options.getObservation?.();
122
+ if (!observation) return { kind: "stay" };
123
+ // A head-only observation is not an authoritative decision. Load a fresh
124
+ // snapshot only after the user activates the existing review flow.
125
+ const { showRemoteSelectionReview } = await import("./remote-selection-ui.js");
126
+ if (sessionSignal?.aborted) return { kind: "close" };
127
+ const config = await loadConfig();
128
+ if (sessionSignal?.aborted) return { kind: "close" };
129
+ if (
130
+ options.getObservation?.() !== observation ||
131
+ !observationMatchesConfig(observation, config)
132
+ ) {
133
+ options.onObservationInvalidated?.();
134
+ return { kind: "stay" };
135
+ }
136
+ // Keep the check-time hint on cancellation or failure. Route commits and
137
+ // the manager's config/baseline reconciliation own invalidation.
138
+ const result = await showRemoteSelectionReview(
139
+ ctx,
140
+ observation.setupName,
141
+ sessionSignal,
142
+ undefined,
143
+ {
144
+ origin: "sync",
145
+ runRoute,
146
+ onSelectionResolved: () => {
147
+ if (!sessionSignal?.aborted && options.getObservation?.() === observation)
148
+ options.onObservationInvalidated?.();
149
+ },
150
+ },
151
+ );
152
+ if (sessionSignal?.aborted) return { kind: "close" };
153
+ if (result.kind === "route-result") {
154
+ return dispatchManagerResult(ctx, result.result, result.route, runRoute, sessionSignal);
155
+ }
156
+ return { kind: result.kind === "closed" || result.kind === "stale" ? "close" : "stay" };
157
+ }
158
+ const disposition = await showManagerAttention(
159
+ ctx,
160
+ attention,
161
+ runRoute,
162
+ sessionSignal,
163
+ () => options.onSelectionResolved?.(attention),
164
+ );
165
+ return { kind: disposition };
166
+ },
167
+ sync: async () => {
168
+ const pendingAttention = options.getAttention?.();
169
+ if (pendingAttention) {
170
+ const activeConfig = await loadConfig();
171
+ if (sessionSignal?.aborted) return { kind: "close" };
172
+ if (pendingAttention.decision.setupName === activeConfig.setupName) {
173
+ ctx.ui.notify("Review synced content before starting Sync now.", "warning");
174
+ return { kind: "stay" };
175
+ }
176
+ }
177
+ const result = await runCancellableOperation(
178
+ ctx,
179
+ "Checking current sync setup…",
180
+ "sync",
181
+ runRoute,
182
+ {
183
+ commitAware: true,
184
+ signal: sessionSignal,
185
+ },
186
+ );
187
+ const disposition = await dispatchManagerResult(
188
+ ctx,
189
+ result,
190
+ "sync",
191
+ runRoute,
192
+ sessionSignal,
193
+ );
194
+ return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
195
+ },
196
+ switch: async () => {
197
+ const result = await showSetupSwitcher(ctx, runRoute, undefined, sessionSignal);
198
+ return result === "pull-attempted" || result === "closed"
199
+ ? { kind: "close" }
200
+ : { kind: "stay" };
201
+ },
202
+ diff: async () => {
203
+ const result = await runCancellableOperation(
204
+ ctx,
205
+ "Checking current sync setup…",
206
+ "diff",
207
+ runRoute,
208
+ { signal: sessionSignal },
209
+ );
210
+ return result.kind === "closed" ? { kind: "close" } : { kind: "stay" };
211
+ },
212
+ settings: async () => {
213
+ await showSyncSettings(ctx, runRoute, sessionSignal);
214
+ return { kind: "stay" };
215
+ },
216
+ pull: async () => {
217
+ const result = await runCancellableOperation(
218
+ ctx,
219
+ "Checking remote changes…",
220
+ "pull",
221
+ runRoute,
222
+ {
223
+ commitAware: true,
224
+ cancelledMessage: "Pull check cancelled; no local files were changed.",
225
+ signal: sessionSignal,
226
+ },
227
+ );
228
+ const disposition = await dispatchManagerResult(
229
+ ctx,
230
+ result,
231
+ "pull",
232
+ runRoute,
233
+ sessionSignal,
234
+ );
235
+ return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
236
+ },
237
+ push: async () => {
238
+ const result = await runCancellableOperation(
239
+ ctx,
240
+ "Preparing push preview…",
241
+ "push",
242
+ runRoute,
243
+ {
244
+ commitAware: true,
245
+ cancelledMessage: "Push preparation cancelled; no remote files were changed.",
246
+ signal: sessionSignal,
247
+ },
248
+ );
249
+ const disposition = await dispatchManagerResult(
250
+ ctx,
251
+ result,
252
+ "push",
253
+ runRoute,
254
+ sessionSignal,
255
+ );
256
+ return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
257
+ },
258
+ setups: async () => {
259
+ const result = await showSyncSetupManager(ctx, runRoute, sessionSignal);
260
+ return result === "exit" ? { kind: "close" } : { kind: "stay" };
261
+ },
262
+ connections: async () => {
263
+ await showStorageConnections(ctx, sessionSignal);
264
+ return { kind: "stay" };
265
+ },
266
+ history: async () => {
267
+ await runRoute("history");
268
+ return { kind: "stay" };
269
+ },
270
+ doctor: async () => {
271
+ const result = await runCancellableOperation(
272
+ ctx,
273
+ "Checking setup access…",
274
+ "doctor",
275
+ runRoute,
276
+ { signal: sessionSignal },
277
+ );
278
+ return { kind: result.kind === "closed" ? "close" : "stay" };
279
+ },
280
+ unlock: async ({ state, signal: actionSignal }) => {
281
+ const result = await recoverSyncAccess(
282
+ ctx,
283
+ state.manager,
284
+ runRoute,
285
+ sessionSignal,
286
+ actionSignal,
287
+ );
288
+ if (result === "close") return { kind: "close" };
289
+ return result === "restored" ? { kind: "to", screen: "main" } : { kind: "stay" };
290
+ },
291
+ recover: async ({ state, signal: actionSignal }) => {
292
+ const result = await recoverSyncAccess(
293
+ ctx,
294
+ state.manager,
295
+ runRoute,
296
+ sessionSignal,
297
+ actionSignal,
298
+ );
299
+ return { kind: result === "close" ? "close" : "stay" };
300
+ },
301
+ refresh: async () => ({ kind: "stay" }),
302
+ help: async () => {
303
+ await runRoute("help");
304
+ return { kind: "close" };
305
+ },
306
+ init: async () => {
307
+ await runRoute("init");
308
+ return { kind: "stay" };
309
+ },
310
+ back: async () => ({ kind: "back" }),
311
+ },
312
+ });
313
+ await runMenu(ctx, menu, {
314
+ getState: async () => {
315
+ const pendingAttention = options.getAttention?.();
316
+ const observation = options.getObservation?.();
317
+ const manager = await describeManagerState(
318
+ sessionSignal,
319
+ pendingAttention,
320
+ undefined,
321
+ observation,
322
+ );
323
+ if (sessionSignal?.aborted) return { manager };
324
+ // A busy operation suppresses baseline reads, not the stored check-time hint.
325
+ if (
326
+ observation &&
327
+ options.getObservation?.() === observation &&
328
+ !manager.observation &&
329
+ !(manager.operation && operationBlocksChanges(manager.operation))
330
+ ) {
331
+ options.onObservationInvalidated?.();
332
+ }
333
+ if (pendingAttention && options.getAttention?.() === pendingAttention && !manager.attention) {
334
+ options.onSelectionResolved?.(pendingAttention);
335
+ }
336
+ return { manager };
337
+ },
338
+ signal: sessionSignal,
339
+ isCurrent: () => !sessionSignal?.aborted,
340
+ });
341
+ }
342
+
343
+ function syncMainMenuItem(
344
+ label: string,
345
+ ): ActionMenuItem<
346
+ "main" | "more" | "recovery",
347
+ | "sync"
348
+ | "switch"
349
+ | "diff"
350
+ | "settings"
351
+ | "setups"
352
+ | "connections"
353
+ | "recover"
354
+ | "refresh"
355
+ | "help"
356
+ | "init"
357
+ > {
358
+ if (label === "More…") return { id: "more", label, to: "more" };
359
+ if (label === "History & recovery…") return { id: "recovery", label, to: "recovery" };
360
+ const actions = new Map<
361
+ string,
362
+ | "sync"
363
+ | "switch"
364
+ | "diff"
365
+ | "settings"
366
+ | "setups"
367
+ | "connections"
368
+ | "recover"
369
+ | "refresh"
370
+ | "help"
371
+ | "init"
372
+ >([
373
+ ["Sync now (recommended)", "sync"],
374
+ ["Switch sync setup", "switch"],
375
+ ["Status & changes", "diff"],
376
+ ["Settings", "settings"],
377
+ ["Restore sync access… (recommended)", "recover"],
378
+ ["Refresh operation status", "refresh"],
379
+ ["Sync setups…", "setups"],
380
+ ["Storage connections…", "connections"],
381
+ ["Help", "help"],
382
+ ["Set up sync", "init"],
383
+ ["Use existing settings", "init"],
384
+ ]);
385
+ return { id: actions.get(label) ?? "help", label, action: actions.get(label) ?? "help" };
386
+ }
@@ -1,29 +1,30 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
3
- import { createSyncBackend, type SyncBackendFactory } from "./backend-factory.js";
4
- import {
5
- type RunRoute,
6
- type RunRouteResult,
7
- runCancellableOperation,
8
- } from "./cancellable-operation.js";
3
+ import { createSyncBackend, type SyncBackendFactory } from "../backends/backend-factory.js";
4
+ import type { RemoteHead, SyncBackend } from "../backends/sync-backend.js";
9
5
  import {
10
6
  loadConfig,
11
7
  loadPartialConfig,
12
8
  type SyncSetupStorageReview,
13
9
  syncConfigReviewFingerprint,
14
- } from "./config.js";
15
- import { readSnapshotForHead } from "./remote-snapshot.js";
16
- import { SyncSetupReviewChangedError, updateSyncSetup } from "./settings-management.js";
17
- import type { RemoteHead, SyncBackend } from "./sync-backend.js";
18
- import { errorMessage, safeTerminalText } from "./sync-format.js";
10
+ } from "../settings/config.js";
11
+ import { SyncSetupReviewChangedError, updateSyncSetup } from "../settings/settings-management.js";
12
+ import type { AnySyncConfig, PartialConfig } from "../settings/settings-types.js";
13
+ import { readSnapshotForHead } from "../sync/remote-snapshot.js";
14
+ import { errorMessage } from "../sync/sync-errors.js";
19
15
  import {
20
16
  compareSyncInclude,
21
17
  inspectRemoteSelection,
22
18
  type RemoteSelectionDecision,
23
19
  type RemoteSelectionState,
24
20
  sameSyncInclude,
25
- } from "./sync-policy.js";
26
- import type { AnySyncConfig, PartialConfig } from "./types.js";
21
+ } from "../sync/sync-policy.js";
22
+ import {
23
+ type RunRoute,
24
+ type RunRouteResult,
25
+ runCancellableOperation,
26
+ } from "./cancellable-operation.js";
27
+ import { safeTerminalText } from "./terminal-text.js";
27
28
 
28
29
  const STATUS_KEY = "sync";
29
30
 
@@ -34,6 +35,7 @@ export interface RemoteSelectionReviewOptions {
34
35
  origin?: RemoteSelectionOrigin;
35
36
  runRoute?: RunRoute;
36
37
  cancelLabel?: string;
38
+ /** The explicit mismatch is resolved or superseded; this does not imply file equality. */
37
39
  onSelectionResolved?: () => void;
38
40
  withStateAccess?: <T>(task: () => Promise<T>) => Promise<T>;
39
41
  }
@@ -57,6 +59,11 @@ export async function showRemoteSelectionReview(
57
59
  if (!decision) {
58
60
  const inspected = await inspectConfiguredRemoteSelection(ctx, setupName, signal, factory);
59
61
  if (!inspected || signal?.aborted) return { kind: "stale" };
62
+ // A successful fresh read can supersede an old explicit mismatch without
63
+ // changing local settings. This is not user cancellation or proof of file equality.
64
+ if (inspected.kind === "empty" || inspected.state.kind !== "different") {
65
+ options.onSelectionResolved?.();
66
+ }
60
67
  if (inspected.kind === "empty") {
61
68
  ctx.ui.notify("Remote storage has no snapshot or synced-content list yet.", "info");
62
69
  return { kind: "back" };
@@ -99,6 +106,7 @@ export async function showRemoteSelectionReview(
99
106
  );
100
107
  if (!refreshed || signal?.aborted) return { kind: "stale" };
101
108
  if (refreshed.kind === "empty") {
109
+ options.onSelectionResolved?.();
102
110
  ctx.ui.notify("Remote storage no longer has a snapshot or synced-content list.", "warning");
103
111
  return { kind: "back" };
104
112
  }
@@ -29,7 +29,9 @@ export async function promptSecret(
29
29
  const cancelKey = keybindingText(keybindings, "tui.select.cancel", "esc", ["ctrl+c"]);
30
30
  const applyTheme = () => {
31
31
  heading.setText(theme.fg("accent", theme.bold(title)));
32
- hint.setText(theme.fg("dim", `${submitKey} save • ${cancelKey} cancel • Input is hidden`));
32
+ hint.setText(
33
+ theme.fg("dim", `${submitKey} continue • ${cancelKey} cancel • Input is hidden`),
34
+ );
33
35
  };
34
36
  applyTheme();
35
37
  const input = new MaskedInput(keybindings);
@@ -60,10 +62,22 @@ export async function promptSecret(
60
62
  hint.invalidate();
61
63
  },
62
64
  handleInput(data: string) {
63
- if (matchesKey(data, Key.ctrl("c")) || keybindings.matches(data, "tui.select.cancel")) {
65
+ if (matchesKey(data, Key.ctrl("c"))) {
64
66
  complete(undefined);
65
- } else if (keybindings.matches(data, "tui.input.submit")) complete(input.getValue());
66
- else input.handleInput(data);
67
+ } else if (input.isPasting || data.includes("\u001b[200~")) {
68
+ input.handleInput(data);
69
+ } else if (keybindings.matches(data, "tui.select.cancel")) {
70
+ complete(undefined);
71
+ } else if (keybindings.matches(data, "tui.input.submit")) {
72
+ if (options.required !== false && input.getValue().length === 0) {
73
+ ctx.ui.notify(`${title} is required. Enter a value, or cancel.`, "warning");
74
+ } else if (hasControlCharacter(input.getValue())) {
75
+ ctx.ui.notify(
76
+ `${title} contains control characters. Remove them or re-enter the value, then continue.`,
77
+ "warning",
78
+ );
79
+ } else complete(input.getValue());
80
+ } else input.handleInput(data);
67
81
  tui.requestRender();
68
82
  },
69
83
  dispose() {
@@ -92,6 +106,10 @@ class MaskedInput implements Focusable {
92
106
 
93
107
  constructor(private readonly keybindings: KeybindingsManager) {}
94
108
 
109
+ get isPasting() {
110
+ return this.pasting;
111
+ }
112
+
95
113
  getValue() {
96
114
  return this.value.join("");
97
115
  }
@@ -1,16 +1,19 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
3
- import type { RunRoute } from "./cancellable-operation.js";
4
- import { loadConfig, localConfigPath } from "./config.js";
5
- import { dispatchManagerResult } from "./manager-result-dispatcher.js";
6
- import { updateSyncSetup } from "./settings-management.js";
3
+ import { loadConfig } from "../settings/config.js";
4
+ import { localConfigPath } from "../settings/config-file.js";
5
+ import { updateSyncSetup } from "../settings/settings-management.js";
6
+ import { updateLocalConfig } from "../settings/settings-store.js";
7
7
  import {
8
8
  SETUP_SWITCH_ACTION_OPTIONS,
9
9
  saveOnSwitch,
10
10
  setupSwitchActionFromLabel,
11
11
  setupSwitchActionLabel,
12
- } from "./setup-switch.js";
13
- import { safeTerminalText } from "./sync-format.js";
12
+ } from "../sync/setup-switch.js";
13
+ import type { RunRoute } from "./cancellable-operation.js";
14
+ import { dispatchManagerResult } from "./manager-result-dispatcher.js";
15
+ import { AUTOMATIC_SYNC_DESCRIPTION } from "./setup/setup-prompts.js";
16
+ import { safeTerminalText } from "./terminal-text.js";
14
17
 
15
18
  export type SyncSettingsRoute = RunRoute;
16
19
 
@@ -26,7 +29,7 @@ export async function showSyncSettings(
26
29
  const initial = await loadConfig();
27
30
  if (signal?.aborted) return;
28
31
  const setupName = initial.setupName;
29
- type Action = "automatic" | "on-switch" | "include" | "remote-include";
32
+ type Action = "automatic" | "skip-secret-scan" | "on-switch" | "include" | "remote-include";
30
33
  const menu = defineMenu<
31
34
  Awaited<ReturnType<typeof loadConfig>>,
32
35
  "settings",
@@ -45,16 +48,25 @@ export async function showSyncSettings(
45
48
  {
46
49
  id: "automatic",
47
50
  label: "Automatic sync",
48
- description: "Run conservative synchronization at session startup and shutdown.",
51
+ description: AUTOMATIC_SYNC_DESCRIPTION,
49
52
  currentValue: state.automatic ? "On" : "Off",
50
53
  values: ["On", "Off"],
51
54
  action: "automatic",
52
55
  },
56
+ {
57
+ id: "skipSecretScan",
58
+ label: "Skip secret scan (all setups)",
59
+ description:
60
+ "All setups: allow pushes without checking managed local files for possible secrets.",
61
+ currentValue: state.skipSecretScan ? "On" : "Off",
62
+ values: ["On", "Off"],
63
+ action: "skip-secret-scan",
64
+ },
53
65
  {
54
66
  id: "onSwitch",
55
- label: "After switching setup",
67
+ label: "After switching setup (all setups)",
56
68
  description:
57
- "Ask before a reviewed pull, start a reviewed pull, or switch without checking remote files.",
69
+ "All setups: ask before a pull review, open it automatically, or switch without pulling.",
58
70
  currentValue: setupSwitchActionLabel(state.onSwitch),
59
71
  values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
60
72
  action: "on-switch",
@@ -62,7 +74,7 @@ export async function showSyncSettings(
62
74
  {
63
75
  id: "include",
64
76
  label: "Included content",
65
- description: `${state.include.length} selected path${state.include.length === 1 ? "" : "s"}. Opens the reviewed content-selection draft.`,
77
+ description: `${state.include.length} selected path${state.include.length === 1 ? "" : "s"}. Choose which paths this setup syncs.`,
66
78
  currentValue: "Open editor",
67
79
  action: "include",
68
80
  },
@@ -100,6 +112,25 @@ export async function showSyncSettings(
100
112
  return { kind: "rejected" };
101
113
  }
102
114
  },
115
+ "skip-secret-scan": async ({ value, signal: actionSignal }) => {
116
+ const skipSecretScan = value === "On";
117
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
118
+ try {
119
+ const latest = await loadConfig(setupName);
120
+ if (mutationSignal.aborted) return { kind: "rejected" };
121
+ if (latest.skipSecretScan === skipSecretScan) return { kind: "stay" };
122
+ await updateLocalConfig((settings) => ({ ...settings, skipSecretScan }), mutationSignal);
123
+ if (mutationSignal.aborted) return { kind: "rejected" };
124
+ ctx.ui.notify(
125
+ `Secret scan ${skipSecretScan ? "disabled" : "enabled"} for pushes in all setups.`,
126
+ "info",
127
+ );
128
+ return { kind: "stay" };
129
+ } catch (error) {
130
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
131
+ return { kind: "rejected" };
132
+ }
133
+ },
103
134
  "on-switch": async ({ value, signal: actionSignal }) => {
104
135
  const action = value ? setupSwitchActionFromLabel(value) : undefined;
105
136
  if (!action) return { kind: "rejected" };