@narumitw/pi-sync 0.50.2 → 0.51.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 (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-4CHTZMEO.ts → chunk-7KHKWL3Q.ts} +37 -55
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-G4RNQQZS.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-KW7YQEZN.ts → chunk-UA7RXOIT.ts} +48 -100
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-5KLKVMRC.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-IUB6ESEU.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +92 -112
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +198 -214
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-4CHTZMEO.ts.map +0 -7
  176. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  177. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  178. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  179. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  180. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  181. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  182. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  183. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-G4RNQQZS.ts.map +0 -7
  186. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  187. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  188. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  189. package/dist/chunks/chunk-KW7YQEZN.ts.map +0 -7
  190. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  191. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  192. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  193. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  194. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  195. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  196. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  197. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  198. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  199. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  200. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-FKIBM3GO.ts +0 -22
  206. package/dist/chunks/manager-ui-5KLKVMRC.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-D7RBRJNL.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-IUB6ESEU.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-FKIBM3GO.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-D7RBRJNL.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -7,271 +7,251 @@ import { syncErrorGuidance } from "../sync/sync-error-guidance.js";
7
7
  import { syncIncludeSelection } from "../sync/sync-policy.js";
8
8
  import { safeTerminalText } from "./terminal-text.js";
9
9
 
10
- export async function countValidSyncSetups(
11
- setups: Record<string, unknown> | undefined,
12
- signal?: AbortSignal,
13
- ) {
14
- let count = 0;
15
- for (const name of Object.keys(setups ?? {})) {
16
- throwIfAborted(signal);
17
- let valid = false;
18
- try {
19
- await loadConfig(name);
20
- valid = true;
21
- } catch {
22
- // Invalid setups stay visible in management but are not switchable.
23
- }
24
- throwIfAborted(signal);
25
- if (valid) count += 1;
26
- }
27
- return count;
10
+ export async function countValidSyncSetups(setups: Record<string, unknown> | undefined, signal?: AbortSignal) {
11
+ let count = 0;
12
+ for (const name of Object.keys(setups ?? {})) {
13
+ throwIfAborted(signal);
14
+ let valid = false;
15
+ try {
16
+ await loadConfig(name);
17
+ valid = true;
18
+ } catch {
19
+ // Invalid setups stay visible in management but are not switchable.
20
+ }
21
+ throwIfAborted(signal);
22
+ if (valid) count += 1;
23
+ }
24
+ return count;
28
25
  }
29
26
 
30
27
  function throwIfAborted(signal?: AbortSignal) {
31
- if (!signal?.aborted) return;
32
- throw signal.reason instanceof Error
33
- ? signal.reason
34
- : new DOMException("The operation was aborted", "AbortError");
28
+ if (!signal?.aborted) return;
29
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
35
30
  }
36
31
 
37
32
  type SyncSetupActions = {
38
- add(signal?: AbortSignal): Promise<void>;
39
- edit(name: string, signal?: AbortSignal): Promise<void>;
40
- makeCurrent(name: string, signal?: AbortSignal): Promise<"exit" | undefined>;
41
- remove(name: string, signal?: AbortSignal): Promise<void>;
33
+ add(signal?: AbortSignal): Promise<void>;
34
+ edit(name: string, signal?: AbortSignal): Promise<void>;
35
+ makeCurrent(name: string, signal?: AbortSignal): Promise<"exit" | undefined>;
36
+ remove(name: string, signal?: AbortSignal): Promise<void>;
42
37
  };
43
38
 
44
39
  interface SetupMenuState {
45
- setups: Record<string, unknown>;
46
- active?: string;
47
- selected?: {
48
- name: string;
49
- detail: string[];
50
- valid: boolean;
51
- removeUnavailable: boolean;
52
- };
40
+ setups: Record<string, unknown>;
41
+ active?: string;
42
+ selected?: {
43
+ name: string;
44
+ detail: string[];
45
+ valid: boolean;
46
+ removeUnavailable: boolean;
47
+ };
53
48
  }
54
49
 
55
50
  export async function showSyncSetups(
56
- ctx: ExtensionCommandContext,
57
- actions: SyncSetupActions,
58
- signal?: AbortSignal,
51
+ ctx: ExtensionCommandContext,
52
+ actions: SyncSetupActions,
53
+ signal?: AbortSignal,
59
54
  ): Promise<"exit" | undefined> {
60
- let selectedName: string | undefined;
61
- let exit = false;
62
- const nameById = new Map<string, string>();
63
- type Screen = "list" | "detail";
64
- type Action = "add" | "select" | "make-current" | "edit" | "remove" | "back";
65
- const menu = defineMenu<SetupMenuState, Screen, Action, ExtensionCommandContext>({
66
- start: "list",
67
- screens: {
68
- list: ({ state }) => {
69
- nameById.clear();
70
- const names = Object.keys(state.setups).sort((left, right) => left.localeCompare(right));
71
- return {
72
- kind: "actions",
73
- title: "Sync setups",
74
- lines: ["What to sync, where to store it, and when to sync."],
75
- items: [
76
- { id: "add", label: "Add sync setup", action: "add" },
77
- ...names.map((name, index) => {
78
- const id = `setup:${index}`;
79
- nameById.set(id, name);
80
- return {
81
- id,
82
- label: `${safeTerminalText(name)}${name === state.active ? " (current)" : ""}`,
83
- description: setupSummary(state.setups[name]),
84
- action: "select" as const,
85
- };
86
- }),
87
- ],
88
- hint: "back",
89
- };
90
- },
91
- detail: ({ state }) => ({
92
- kind: "actions",
93
- title: state.selected
94
- ? `Sync setup “${safeTerminalText(state.selected.name)}”`
95
- : "Sync setup",
96
- lines: state.selected?.detail ?? ["This sync setup no longer exists."],
97
- items: state.selected
98
- ? [
99
- ...(!state.selected.name ||
100
- state.selected.name === state.active ||
101
- !state.selected.valid
102
- ? []
103
- : [
104
- {
105
- id: "make-current",
106
- label: "Make current…",
107
- action: "make-current" as const,
108
- },
109
- ]),
110
- { id: "edit", label: "Edit storage location…", action: "edit" },
111
- ...(state.selected.removeUnavailable
112
- ? []
113
- : [
114
- {
115
- id: "remove",
116
- label: "Remove sync setup…",
117
- action: "remove" as const,
118
- },
119
- ]),
120
- { id: "back", label: "Back", action: "back" },
121
- ]
122
- : [{ id: "back", label: "Back", action: "back" }],
123
- hint: "back",
124
- }),
125
- },
126
- actions: {
127
- add: async () => {
128
- try {
129
- await actions.add(signal);
130
- } catch (error) {
131
- if (!signal?.aborted) {
132
- ctx.ui.notify(`Sync setup was not added: ${menuErrorMessage(error)}`, "error");
133
- }
134
- }
135
- return { kind: "stay" };
136
- },
137
- select: async ({ itemId }) => {
138
- selectedName = nameById.get(itemId);
139
- return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
140
- },
141
- "make-current": async () => {
142
- if (!selectedName) return { kind: "rejected" };
143
- try {
144
- exit = (await actions.makeCurrent(selectedName, signal)) === "exit";
145
- return exit ? { kind: "close" } : { kind: "stay" };
146
- } catch (error) {
147
- notifySetupChangeError(ctx, selectedName, error, signal);
148
- return { kind: "stay" };
149
- }
150
- },
151
- edit: async () => {
152
- if (!selectedName) return { kind: "rejected" };
153
- try {
154
- await actions.edit(selectedName, signal);
155
- } catch (error) {
156
- notifySetupChangeError(ctx, selectedName, error, signal);
157
- }
158
- return { kind: "stay" };
159
- },
160
- remove: async () => {
161
- if (!selectedName) return { kind: "rejected" };
162
- const name = selectedName;
163
- try {
164
- await actions.remove(name, signal);
165
- selectedName = undefined;
166
- return { kind: "back" };
167
- } catch (error) {
168
- notifySetupChangeError(ctx, name, error, signal);
169
- return { kind: "stay" };
170
- }
171
- },
172
- back: async () => {
173
- selectedName = undefined;
174
- return { kind: "back" };
175
- },
176
- },
177
- });
178
- await runMenu(ctx, menu, {
179
- getState: async () => loadSetupMenuState(selectedName, signal),
180
- signal,
181
- isCurrent: () => !signal?.aborted,
182
- });
183
- return exit ? "exit" : undefined;
55
+ let selectedName: string | undefined;
56
+ let exit = false;
57
+ const nameById = new Map<string, string>();
58
+ type Screen = "list" | "detail";
59
+ type Action = "add" | "select" | "make-current" | "edit" | "remove" | "back";
60
+ const menu = defineMenu<SetupMenuState, Screen, Action, ExtensionCommandContext>({
61
+ start: "list",
62
+ screens: {
63
+ list: ({ state }) => {
64
+ nameById.clear();
65
+ const names = Object.keys(state.setups).sort((left, right) => left.localeCompare(right));
66
+ return {
67
+ kind: "actions",
68
+ title: "Sync setups",
69
+ lines: ["What to sync, where to store it, and when to sync."],
70
+ items: [
71
+ { id: "add", label: "Add sync setup", action: "add" },
72
+ ...names.map((name, index) => {
73
+ const id = `setup:${index}`;
74
+ nameById.set(id, name);
75
+ return {
76
+ id,
77
+ label: `${safeTerminalText(name)}${name === state.active ? " (current)" : ""}`,
78
+ description: setupSummary(state.setups[name]),
79
+ action: "select" as const,
80
+ };
81
+ }),
82
+ ],
83
+ hint: "back",
84
+ };
85
+ },
86
+ detail: ({ state }) => ({
87
+ kind: "actions",
88
+ title: state.selected ? `Sync setup “${safeTerminalText(state.selected.name)}”` : "Sync setup",
89
+ lines: state.selected?.detail ?? ["This sync setup no longer exists."],
90
+ items: state.selected
91
+ ? [
92
+ ...(!state.selected.name || state.selected.name === state.active || !state.selected.valid
93
+ ? []
94
+ : [
95
+ {
96
+ id: "make-current",
97
+ label: "Make current…",
98
+ action: "make-current" as const,
99
+ },
100
+ ]),
101
+ { id: "edit", label: "Edit storage location…", action: "edit" },
102
+ ...(state.selected.removeUnavailable
103
+ ? []
104
+ : [
105
+ {
106
+ id: "remove",
107
+ label: "Remove sync setup…",
108
+ action: "remove" as const,
109
+ },
110
+ ]),
111
+ { id: "back", label: "Back", action: "back" },
112
+ ]
113
+ : [{ id: "back", label: "Back", action: "back" }],
114
+ hint: "back",
115
+ }),
116
+ },
117
+ actions: {
118
+ add: async () => {
119
+ try {
120
+ await actions.add(signal);
121
+ } catch (error) {
122
+ if (!signal?.aborted) {
123
+ ctx.ui.notify(`Sync setup was not added: ${menuErrorMessage(error)}`, "error");
124
+ }
125
+ }
126
+ return { kind: "stay" };
127
+ },
128
+ select: async ({ itemId }) => {
129
+ selectedName = nameById.get(itemId);
130
+ return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
131
+ },
132
+ "make-current": async () => {
133
+ if (!selectedName) return { kind: "rejected" };
134
+ try {
135
+ exit = (await actions.makeCurrent(selectedName, signal)) === "exit";
136
+ return exit ? { kind: "close" } : { kind: "stay" };
137
+ } catch (error) {
138
+ notifySetupChangeError(ctx, selectedName, error, signal);
139
+ return { kind: "stay" };
140
+ }
141
+ },
142
+ edit: async () => {
143
+ if (!selectedName) return { kind: "rejected" };
144
+ try {
145
+ await actions.edit(selectedName, signal);
146
+ } catch (error) {
147
+ notifySetupChangeError(ctx, selectedName, error, signal);
148
+ }
149
+ return { kind: "stay" };
150
+ },
151
+ remove: async () => {
152
+ if (!selectedName) return { kind: "rejected" };
153
+ const name = selectedName;
154
+ try {
155
+ await actions.remove(name, signal);
156
+ selectedName = undefined;
157
+ return { kind: "back" };
158
+ } catch (error) {
159
+ notifySetupChangeError(ctx, name, error, signal);
160
+ return { kind: "stay" };
161
+ }
162
+ },
163
+ back: async () => {
164
+ selectedName = undefined;
165
+ return { kind: "back" };
166
+ },
167
+ },
168
+ });
169
+ await runMenu(ctx, menu, {
170
+ getState: async () => loadSetupMenuState(selectedName, signal),
171
+ signal,
172
+ isCurrent: () => !signal?.aborted,
173
+ });
174
+ return exit ? "exit" : undefined;
184
175
  }
185
176
 
186
- async function loadSetupMenuState(
187
- selectedName: string | undefined,
188
- signal?: AbortSignal,
189
- ): Promise<SetupMenuState> {
190
- const raw = await readLocalConfigObject();
191
- throwIfAborted(signal);
192
- const setups = ownRecord(raw?.syncSetups) ?? {};
193
- const active = typeof raw?.activeSyncSetup === "string" ? raw.activeSyncSetup : undefined;
194
- if (!selectedName || !ownRecord(setups[selectedName])) return { setups, active };
195
- const setupCount = Object.keys(setups).length;
196
- const isCurrent = selectedName === active;
197
- let detail: string[];
198
- let valid = true;
199
- try {
200
- const config = await loadConfig(selectedName);
201
- throwIfAborted(signal);
202
- const selection = syncIncludeSelection(config.include);
203
- detail = [
204
- `Status: ${isCurrent ? "Current" : "Not current"}`,
205
- `Storage connection: ${safeTerminalText(config.connectionName)}`,
206
- `Endpoint: ${storageEndpoint(config)}`,
207
- `Storage location: ${storageLocation(config)}`,
208
- `Included content: ${selection.builtIns.length} built-in groups · ${selection.custom.length} extra paths`,
209
- `Sessions: ${selection.sessions ? "On — privacy-sensitive" : "Off"}`,
210
- `Automatic sync: ${config.automatic ? "On" : "Off"}`,
211
- isCurrent
212
- ? "Edit included content and automatic sync in /sync → Settings."
213
- : "To edit included content or automatic sync, make this setup current, then open /sync → Settings.",
214
- ];
215
- } catch (error) {
216
- valid = false;
217
- detail = [
218
- `Status: Invalid${isCurrent ? " current setup" : ""}`,
219
- `Reason: ${menuErrorMessage(error)}`,
220
- "Make current and sync are unavailable until this setup is repaired.",
221
- ];
222
- }
223
- const removeUnavailable = isCurrent && setupCount > 1;
224
- if (removeUnavailable) detail.push("Remove unavailable: switch to another setup first.");
225
- return {
226
- setups,
227
- active,
228
- selected: { name: selectedName, detail, valid, removeUnavailable },
229
- };
177
+ async function loadSetupMenuState(selectedName: string | undefined, signal?: AbortSignal): Promise<SetupMenuState> {
178
+ const raw = await readLocalConfigObject();
179
+ throwIfAborted(signal);
180
+ const setups = ownRecord(raw?.syncSetups) ?? {};
181
+ const active = typeof raw?.activeSyncSetup === "string" ? raw.activeSyncSetup : undefined;
182
+ if (!selectedName || !ownRecord(setups[selectedName])) return { setups, active };
183
+ const setupCount = Object.keys(setups).length;
184
+ const isCurrent = selectedName === active;
185
+ let detail: string[];
186
+ let valid = true;
187
+ try {
188
+ const config = await loadConfig(selectedName);
189
+ throwIfAborted(signal);
190
+ const selection = syncIncludeSelection(config.include);
191
+ detail = [
192
+ `Status: ${isCurrent ? "Current" : "Not current"}`,
193
+ `Storage connection: ${safeTerminalText(config.connectionName)}`,
194
+ `Endpoint: ${storageEndpoint(config)}`,
195
+ `Storage location: ${storageLocation(config)}`,
196
+ `Included content: ${selection.builtIns.length} built-in groups · ${selection.custom.length} extra paths`,
197
+ `Sessions: ${selection.sessions ? "On — privacy-sensitive" : "Off"}`,
198
+ `Automatic sync: ${config.automatic ? "On" : "Off"}`,
199
+ isCurrent
200
+ ? "Edit included content and automatic sync in /sync → Settings."
201
+ : "To edit included content or automatic sync, make this setup current, then open /sync → Settings.",
202
+ ];
203
+ } catch (error) {
204
+ valid = false;
205
+ detail = [
206
+ `Status: Invalid${isCurrent ? " current setup" : ""}`,
207
+ `Reason: ${menuErrorMessage(error)}`,
208
+ "Make current and sync are unavailable until this setup is repaired.",
209
+ ];
210
+ }
211
+ const removeUnavailable = isCurrent && setupCount > 1;
212
+ if (removeUnavailable) detail.push("Remove unavailable: switch to another setup first.");
213
+ return {
214
+ setups,
215
+ active,
216
+ selected: { name: selectedName, detail, valid, removeUnavailable },
217
+ };
230
218
  }
231
219
 
232
- function notifySetupChangeError(
233
- ctx: ExtensionCommandContext,
234
- name: string,
235
- error: unknown,
236
- signal?: AbortSignal,
237
- ) {
238
- if (signal?.aborted) return;
239
- ctx.ui.notify(
240
- `Sync setup “${safeTerminalText(name)}” was not changed: ${menuErrorMessage(error)}`,
241
- "error",
242
- );
220
+ function notifySetupChangeError(ctx: ExtensionCommandContext, name: string, error: unknown, signal?: AbortSignal) {
221
+ if (signal?.aborted) return;
222
+ ctx.ui.notify(`Sync setup “${safeTerminalText(name)}” was not changed: ${menuErrorMessage(error)}`, "error");
243
223
  }
244
224
 
245
225
  function menuErrorMessage(error: unknown) {
246
- return syncErrorGuidance(error);
226
+ return syncErrorGuidance(error);
247
227
  }
248
228
 
249
229
  function setupSummary(value: unknown) {
250
- const storage = ownRecord(ownRecord(value)?.storage);
251
- if (!storage || typeof storage.connection !== "string") return "Invalid storage reference";
252
- return safeTerminalText(
253
- `${storage.connection} · ${storage.bucket ?? storage.branch ?? "WebDAV"} · ${storage.path ?? "missing path"}`,
254
- );
230
+ const storage = ownRecord(ownRecord(value)?.storage);
231
+ if (!storage || typeof storage.connection !== "string") return "Invalid storage reference";
232
+ return safeTerminalText(
233
+ `${storage.connection} · ${storage.bucket ?? storage.branch ?? "WebDAV"} · ${storage.path ?? "missing path"}`,
234
+ );
255
235
  }
256
236
 
257
237
  function storageEndpoint(config: Awaited<ReturnType<typeof loadConfig>>) {
258
- switch (config.backend.type) {
259
- case "s3":
260
- return safeTerminalText(config.backend.profile.endpoint);
261
- case "git":
262
- return safeTerminalText(config.backend.profile.remote);
263
- case "webdav":
264
- return safeTerminalText(config.backend.profile.url);
265
- }
238
+ switch (config.backend.type) {
239
+ case "s3":
240
+ return safeTerminalText(config.backend.profile.endpoint);
241
+ case "git":
242
+ return safeTerminalText(config.backend.profile.remote);
243
+ case "webdav":
244
+ return safeTerminalText(config.backend.profile.url);
245
+ }
266
246
  }
267
247
 
268
248
  function storageLocation(config: Awaited<ReturnType<typeof loadConfig>>) {
269
- switch (config.backend.type) {
270
- case "s3":
271
- return safeTerminalText(`${config.backend.destination.bucket}/${config.storagePath}`);
272
- case "git":
273
- return safeTerminalText(`Git · ${config.backend.destination.branch}:${config.storagePath}`);
274
- case "webdav":
275
- return safeTerminalText(`WebDAV · ${config.storagePath}`);
276
- }
249
+ switch (config.backend.type) {
250
+ case "s3":
251
+ return safeTerminalText(`${config.backend.destination.bucket}/${config.storagePath}`);
252
+ case "git":
253
+ return safeTerminalText(`Git · ${config.backend.destination.branch}:${config.storagePath}`);
254
+ case "webdav":
255
+ return safeTerminalText(`WebDAV · ${config.storagePath}`);
256
+ }
277
257
  }
@@ -0,0 +1,17 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ const STATUS_KEY = "sync";
4
+
5
+ type SyncStatusContext = Pick<ExtensionContext, "hasUI" | "sessionManager" | "ui">;
6
+
7
+ const enabledBySession = new WeakMap<ExtensionContext["sessionManager"], boolean>();
8
+
9
+ export function configureSyncStatus(ctx: SyncStatusContext, enabled: boolean) {
10
+ enabledBySession.set(ctx.sessionManager, enabled);
11
+ if (!enabled && ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, undefined);
12
+ }
13
+
14
+ export function setSyncStatus(ctx: SyncStatusContext, value: string | undefined) {
15
+ if (!ctx.hasUI || (value !== undefined && enabledBySession.get(ctx.sessionManager) === false)) return;
16
+ ctx.ui.setStatus(STATUS_KEY, value);
17
+ }
@@ -1,4 +1,4 @@
1
1
  export function safeTerminalText(value: string) {
2
- // biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
3
- return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
2
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
3
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
4
4
  }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/sync/sync-errors.ts"],
4
- "sourcesContent": ["import type { SyncDecision } from \"./sync-decision.js\";\n\nexport class SetupPullRequiresUiError extends Error {}\n\nexport class SyncDecisionRequiredError extends Error {\n\treadonly decision: SyncDecision;\n\n\tconstructor(decision: SyncDecision) {\n\t\tsuper(decision.directMessage);\n\t\tthis.name = \"SyncDecisionRequiredError\";\n\t\tthis.decision = decision;\n\t}\n}\n\nexport function isSyncDecisionRequiredError(error: unknown): error is SyncDecisionRequiredError {\n\treturn error instanceof SyncDecisionRequiredError;\n}\n\nexport function errorMessage(error: unknown) {\n\tconst message = error instanceof Error ? error.message : String(error);\n\t// biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.\n\treturn message.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/gu, \"?\");\n}\n"],
5
- "mappings": ";;;;AAEO,IAAM,2BAAN,cAAuC,MAAM;AAAC;AAE9C,IAAM,4BAAN,cAAwC,MAAM;AAAA,EAC3C;AAAA,EAET,YAAY,UAAwB;AACnC,UAAM,SAAS,aAAa;AAC5B,SAAK,OAAO;AACZ,SAAK,WAAW;AAAA,EACjB;AACD;AAEO,SAAS,4BAA4B,OAAoD;AAC/F,SAAO,iBAAiB;AACzB;AAEO,SAAS,aAAa,OAAgB;AAC5C,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAErE,SAAO,QAAQ,QAAQ,kCAAkC,GAAG;AAC7D;",
6
- "names": []
7
- }