@narumitw/pi-sync 0.49.15 → 0.50.1

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 (213) hide show
  1. package/README.md +49 -16
  2. package/dist/chunks/attention-widget-Z7G3SYYJ.ts +9 -0
  3. package/dist/chunks/attention-widget-Z7G3SYYJ.ts.map +7 -0
  4. package/dist/chunks/chunk-2B2ELUF5.ts +29 -0
  5. package/dist/chunks/chunk-2B2ELUF5.ts.map +7 -0
  6. package/dist/chunks/{chunk-ARN2672F.ts → chunk-4CHTZMEO.ts} +23 -55
  7. package/dist/chunks/chunk-4CHTZMEO.ts.map +7 -0
  8. package/dist/chunks/chunk-6LP3MCR2.ts +115 -0
  9. package/dist/chunks/chunk-6LP3MCR2.ts.map +7 -0
  10. package/dist/chunks/{chunk-JF6MZCT4.ts → chunk-AUSDLD6F.ts} +4 -4
  11. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  12. package/dist/chunks/{chunk-5T442OP3.ts → chunk-C7XY5NF5.ts} +210 -373
  13. package/dist/chunks/chunk-C7XY5NF5.ts.map +7 -0
  14. package/dist/chunks/chunk-CCSN3F47.ts +51 -0
  15. package/dist/chunks/chunk-CCSN3F47.ts.map +7 -0
  16. package/dist/chunks/{chunk-4U6AFLJB.ts → chunk-CHH26D4F.ts} +3 -2
  17. package/dist/chunks/chunk-CHH26D4F.ts.map +7 -0
  18. package/dist/chunks/chunk-CPEHGXJV.ts +50 -0
  19. package/dist/chunks/chunk-CPEHGXJV.ts.map +7 -0
  20. package/dist/chunks/{chunk-5MQ76BK5.ts → chunk-D5X27LCA.ts} +2 -2
  21. package/dist/chunks/chunk-D5X27LCA.ts.map +7 -0
  22. package/dist/chunks/{chunk-WSOBYHIZ.ts → chunk-ETTRJDNP.ts} +4 -4
  23. package/dist/chunks/chunk-ETTRJDNP.ts.map +7 -0
  24. package/dist/chunks/{chunk-44LPGQTI.ts → chunk-G4RNQQZS.ts} +9 -7
  25. package/dist/chunks/chunk-G4RNQQZS.ts.map +7 -0
  26. package/dist/chunks/{chunk-5LZALPCD.ts → chunk-HUDQATCL.ts} +25 -25
  27. package/dist/chunks/chunk-HUDQATCL.ts.map +7 -0
  28. package/dist/chunks/{chunk-X7SQ2EQM.ts → chunk-IJ6FJFA5.ts} +9 -9
  29. package/dist/chunks/chunk-IJ6FJFA5.ts.map +7 -0
  30. package/dist/chunks/{chunk-YQ6UW7IF.ts → chunk-JAAKLL6M.ts} +2 -2
  31. package/dist/chunks/chunk-JAAKLL6M.ts.map +7 -0
  32. package/dist/chunks/chunk-KW7YQEZN.ts +1255 -0
  33. package/dist/chunks/chunk-KW7YQEZN.ts.map +7 -0
  34. package/dist/chunks/{chunk-DFXBKHGN.ts → chunk-LCRI3ABW.ts} +21 -8
  35. package/dist/chunks/chunk-LCRI3ABW.ts.map +7 -0
  36. package/dist/chunks/chunk-LTBWGSBV.ts +12 -0
  37. package/dist/chunks/chunk-LTBWGSBV.ts.map +7 -0
  38. package/dist/chunks/chunk-NJFUIVYL.ts +17 -0
  39. package/dist/chunks/chunk-NJFUIVYL.ts.map +7 -0
  40. package/dist/chunks/{chunk-HVUSHMVL.ts → chunk-OEX4ABY6.ts} +39 -37
  41. package/dist/chunks/chunk-OEX4ABY6.ts.map +7 -0
  42. package/dist/chunks/{chunk-HGV24P4T.ts → chunk-Q2TUM5X6.ts} +3 -3
  43. package/dist/chunks/chunk-Q2TUM5X6.ts.map +7 -0
  44. package/dist/chunks/chunk-S7YBZR42.ts +146 -0
  45. package/dist/chunks/chunk-S7YBZR42.ts.map +7 -0
  46. package/dist/chunks/{chunk-GLQLN5GV.ts → chunk-SL64RLXR.ts} +8 -6
  47. package/dist/chunks/chunk-SL64RLXR.ts.map +7 -0
  48. package/dist/chunks/chunk-SZWBGPID.ts +57 -0
  49. package/dist/chunks/chunk-SZWBGPID.ts.map +7 -0
  50. package/dist/chunks/chunk-T2B5IU74.ts +25 -0
  51. package/dist/chunks/chunk-T2B5IU74.ts.map +7 -0
  52. package/dist/chunks/chunk-T3HH3UEJ.ts +96 -0
  53. package/dist/chunks/chunk-T3HH3UEJ.ts.map +7 -0
  54. package/dist/chunks/chunk-TEN2ZBJV.ts +101 -0
  55. package/dist/chunks/chunk-TEN2ZBJV.ts.map +7 -0
  56. package/dist/chunks/chunk-THMF4BVL.ts +18 -0
  57. package/dist/chunks/chunk-THMF4BVL.ts.map +7 -0
  58. package/dist/chunks/chunk-Z4DQF6L3.ts +12 -0
  59. package/dist/chunks/chunk-Z4DQF6L3.ts.map +7 -0
  60. package/dist/chunks/{chunk-F5QL2GPY.ts → chunk-ZI5MXMEX.ts} +5 -99
  61. package/dist/chunks/chunk-ZI5MXMEX.ts.map +7 -0
  62. package/dist/chunks/{file-selection-RKEHALNQ.ts → file-selection-FTTKAOYX.ts} +10 -7
  63. package/dist/chunks/file-selection-FTTKAOYX.ts.map +7 -0
  64. package/dist/chunks/{git-backend-E3UOUUOM.ts → git-backend-GMVS4PD4.ts} +39 -14
  65. package/dist/chunks/git-backend-GMVS4PD4.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-FKIBM3GO.ts +22 -0
  67. package/dist/chunks/manager-ui-R3XPUFFS.ts +1680 -0
  68. package/dist/chunks/manager-ui-R3XPUFFS.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-D7RBRJNL.ts +21 -0
  70. package/dist/chunks/{s3-backend-552LXHE4.ts → s3-backend-66O4FGBH.ts} +94 -16
  71. package/dist/chunks/s3-backend-66O4FGBH.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-5YDG5YGJ.ts → setup-switch-IS2PEZ3J.ts} +9 -9
  73. package/dist/chunks/setup-wizard-IUB6ESEU.ts +89 -0
  74. package/dist/chunks/setup-wizard-IUB6ESEU.ts.map +7 -0
  75. package/dist/chunks/{snapshot-2RYBUDRA.ts → snapshot-HKEWCVK6.ts} +7 -6
  76. package/dist/chunks/sync-inspection-CST2KL65.ts +20 -0
  77. package/dist/chunks/{sync-operations-3LLIHWZO.ts → sync-operations-LBX3GKCM.ts} +369 -289
  78. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +7 -0
  79. package/dist/chunks/{sync-state-SRN6YRBK.ts → sync-state-Q2FTXTW6.ts} +6 -6
  80. package/dist/chunks/sync-state-Q2FTXTW6.ts.map +7 -0
  81. package/dist/chunks/{webdav-backend-ZD5RHRIU.ts → webdav-backend-ECIKD7SO.ts} +26 -13
  82. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +7 -0
  83. package/dist/index.ts +476 -341
  84. package/dist/index.ts.map +4 -4
  85. package/docs/settings.md +30 -6
  86. package/package.json +1 -1
  87. package/src/{backend-factory.ts → backends/backend-factory.ts} +4 -4
  88. package/src/backends/backend-identity.ts +40 -0
  89. package/src/backends/backend-types.ts +81 -0
  90. package/src/{git-backend.ts → backends/git/git-backend.ts} +43 -19
  91. package/src/{git-config.ts → backends/git/git-config.ts} +1 -0
  92. package/src/{git-storage.ts → backends/git/git-storage.ts} +2 -2
  93. package/src/{s3-backend.ts → backends/s3/s3-backend.ts} +45 -9
  94. package/src/{s3-client.ts → backends/s3/s3-client.ts} +56 -8
  95. package/src/{sync-backend.ts → backends/sync-backend.ts} +1 -1
  96. package/src/{webdav-backend.ts → backends/webdav/webdav-backend.ts} +22 -10
  97. package/src/{webdav-client.ts → backends/webdav/webdav-client.ts} +1 -1
  98. package/src/{webdav-config.ts → backends/webdav/webdav-config.ts} +1 -0
  99. package/src/commands/command-execution.ts +330 -0
  100. package/src/commands/command-handler.ts +201 -0
  101. package/src/commands/command-types.ts +18 -0
  102. package/src/{command.ts → commands/command.ts} +2 -1
  103. package/src/commands/setup-completions.ts +6 -0
  104. package/src/settings/config.ts +238 -0
  105. package/src/{settings-management.ts → settings/settings-management.ts} +39 -14
  106. package/src/settings/settings-store.ts +109 -0
  107. package/src/settings/settings-types.ts +128 -0
  108. package/src/settings/settings-validation.ts +443 -0
  109. package/src/snapshot/session-paths.ts +84 -0
  110. package/src/{snapshot-apply.ts → snapshot/snapshot-apply.ts} +3 -3
  111. package/src/{snapshot-codec.ts → snapshot/snapshot-codec.ts} +2 -2
  112. package/src/{snapshot-transaction.ts → snapshot/snapshot-transaction.ts} +5 -4
  113. package/src/snapshot/snapshot-types.ts +38 -0
  114. package/src/{snapshot.ts → snapshot/snapshot.ts} +39 -20
  115. package/src/state/json-file.ts +20 -0
  116. package/src/{lock.ts → state/lock.ts} +3 -2
  117. package/src/{operation-availability.ts → state/operation-availability.ts} +2 -2
  118. package/src/state/state-types.ts +20 -0
  119. package/src/state/sync-state-store.ts +62 -0
  120. package/src/sync/automatic-sync.ts +103 -0
  121. package/src/{remote-snapshot.ts → sync/remote-snapshot.ts} +27 -4
  122. package/src/{setup-switch.ts → sync/setup-switch.ts} +6 -7
  123. package/src/sync/signals.ts +10 -0
  124. package/src/sync/startup-check.ts +128 -0
  125. package/src/{sync-decision.ts → sync/sync-decision.ts} +6 -3
  126. package/src/sync/sync-error-guidance.ts +53 -0
  127. package/src/sync/sync-inspection.ts +51 -0
  128. package/src/sync/sync-loaders.ts +54 -0
  129. package/src/{sync-operations.ts → sync/sync-mutations.ts} +34 -325
  130. package/src/sync/sync-operations.ts +12 -0
  131. package/src/{sync-policy.ts → sync/sync-policy.ts} +2 -2
  132. package/src/sync/sync-queries.ts +235 -0
  133. package/src/{sync-state.ts → sync/sync-state.ts} +5 -4
  134. package/src/sync-extension.ts +64 -708
  135. package/src/sync.ts +8 -8
  136. package/src/types.ts +47 -286
  137. package/src/ui/attention-widget.ts +1 -0
  138. package/src/ui/automatic-sync-summary.ts +6 -0
  139. package/src/{cancellable-operation.ts → ui/cancellable-operation.ts} +4 -4
  140. package/src/{file-selection.ts → ui/file-selection.ts} +5 -3
  141. package/src/{manager-attention.ts → ui/manager-attention.ts} +5 -1
  142. package/src/{manager-recovery.ts → ui/manager-recovery.ts} +6 -2
  143. package/src/{manager-state.ts → ui/manager-state.ts} +35 -14
  144. package/src/ui/manager-ui.ts +386 -0
  145. package/src/{remote-selection-ui.ts → ui/remote-selection-ui.ts} +21 -13
  146. package/src/{secret-input.ts → ui/secret-input.ts} +22 -4
  147. package/src/{settings-ui.ts → ui/settings-ui.ts} +17 -13
  148. package/src/ui/setup/git-ui.ts +293 -0
  149. package/src/{s3-credentials-ui.ts → ui/setup/s3-credentials-ui.ts} +13 -21
  150. package/src/ui/setup/s3-ui.ts +217 -0
  151. package/src/ui/setup/setup-actions.ts +127 -0
  152. package/src/ui/setup/setup-location-ui.ts +167 -0
  153. package/src/ui/setup/setup-name-ui.ts +34 -0
  154. package/src/ui/setup/setup-prompts.ts +92 -0
  155. package/src/ui/setup/setup-review.ts +70 -0
  156. package/src/ui/setup/setup-switcher.ts +181 -0
  157. package/src/ui/setup/setup-wizard.ts +39 -0
  158. package/src/ui/setup/text-input.ts +78 -0
  159. package/src/ui/setup/webdav-ui.ts +330 -0
  160. package/src/{storage-connections-ui.ts → ui/storage-connections-ui.ts} +99 -69
  161. package/src/ui/sync-attention.ts +187 -0
  162. package/src/{sync-format.ts → ui/sync-format.ts} +7 -13
  163. package/src/{sync-resolution-ui.ts → ui/sync-resolution-ui.ts} +4 -3
  164. package/src/{sync-setups-ui.ts → ui/sync-setups-ui.ts} +23 -10
  165. package/src/ui/terminal-text.ts +4 -0
  166. package/dist/chunks/chunk-44LPGQTI.ts.map +0 -7
  167. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  168. package/dist/chunks/chunk-5LZALPCD.ts.map +0 -7
  169. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  170. package/dist/chunks/chunk-5T442OP3.ts.map +0 -7
  171. package/dist/chunks/chunk-ARN2672F.ts.map +0 -7
  172. package/dist/chunks/chunk-C7LXZ3UH.ts +0 -167
  173. package/dist/chunks/chunk-C7LXZ3UH.ts.map +0 -7
  174. package/dist/chunks/chunk-DFXBKHGN.ts.map +0 -7
  175. package/dist/chunks/chunk-E4COSV32.ts +0 -82
  176. package/dist/chunks/chunk-E4COSV32.ts.map +0 -7
  177. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  178. package/dist/chunks/chunk-GLQLN5GV.ts.map +0 -7
  179. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  180. package/dist/chunks/chunk-HVUSHMVL.ts.map +0 -7
  181. package/dist/chunks/chunk-JF6MZCT4.ts.map +0 -7
  182. package/dist/chunks/chunk-WSOBYHIZ.ts.map +0 -7
  183. package/dist/chunks/chunk-X7SQ2EQM.ts.map +0 -7
  184. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  185. package/dist/chunks/file-selection-RKEHALNQ.ts.map +0 -7
  186. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  187. package/dist/chunks/manager-result-dispatcher-LWPYJAD5.ts +0 -18
  188. package/dist/chunks/manager-ui-NF4RUHBY.ts +0 -2785
  189. package/dist/chunks/manager-ui-NF4RUHBY.ts.map +0 -7
  190. package/dist/chunks/remote-selection-ui-NUF5P4ZJ.ts +0 -17
  191. package/dist/chunks/s3-backend-552LXHE4.ts.map +0 -7
  192. package/dist/chunks/sync-operations-3LLIHWZO.ts.map +0 -7
  193. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  194. package/src/config.ts +0 -923
  195. package/src/git-ui.ts +0 -307
  196. package/src/manager-helpers.ts +0 -64
  197. package/src/manager-ui.ts +0 -997
  198. package/src/sync-attention.ts +0 -100
  199. package/src/webdav-ui.ts +0 -421
  200. /package/dist/chunks/{manager-result-dispatcher-LWPYJAD5.ts.map → manager-result-dispatcher-FKIBM3GO.ts.map} +0 -0
  201. /package/dist/chunks/{remote-selection-ui-NUF5P4ZJ.ts.map → remote-selection-ui-D7RBRJNL.ts.map} +0 -0
  202. /package/dist/chunks/{setup-switch-5YDG5YGJ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  203. /package/dist/chunks/{snapshot-2RYBUDRA.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  204. /package/dist/chunks/{sync-state-SRN6YRBK.ts.map → sync-inspection-CST2KL65.ts.map} +0 -0
  205. /package/src/{git-runner.ts → backends/git/git-runner.ts} +0 -0
  206. /package/src/{config-errors.ts → settings/config-errors.ts} +0 -0
  207. /package/src/{config-file.ts → settings/config-file.ts} +0 -0
  208. /package/src/{snapshot-paths.ts → snapshot/snapshot-paths.ts} +0 -0
  209. /package/src/{lock-policy.ts → state/lock-policy.ts} +0 -0
  210. /package/src/{lockfile-fs.ts → state/lockfile-fs.ts} +0 -0
  211. /package/src/{state-directory.ts → state/state-directory.ts} +0 -0
  212. /package/src/{sync-errors.ts → sync/sync-errors.ts} +0 -0
  213. /package/src/{manager-result-dispatcher.ts → ui/manager-result-dispatcher.ts} +0 -0
@@ -1,100 +0,0 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { truncateToWidth } from "@earendil-works/pi-tui";
3
- import { syncConfigReviewFingerprint } from "./config.js";
4
- import { safeTerminalText } from "./sync-format.js";
5
- import {
6
- compareSyncInclude,
7
- type RemoteSelectionDecision,
8
- sameSyncInclude,
9
- } from "./sync-policy.js";
10
- import type { AnySyncConfig } from "./types.js";
11
-
12
- const STATUS_KEY = "sync";
13
- const WIDGET_KEY = "sync:attention";
14
-
15
- export type SyncAttentionOrigin = "sync" | "pull" | "push";
16
-
17
- export interface SyncAttentionState {
18
- decision: RemoteSelectionDecision;
19
- origin: SyncAttentionOrigin;
20
- offered: boolean;
21
- }
22
-
23
- export function syncAttentionMatchesConfig(attention: SyncAttentionState, config: AnySyncConfig) {
24
- return (
25
- attention.decision.setupName === config.setupName &&
26
- attention.decision.configIdentity === syncConfigReviewFingerprint(config) &&
27
- sameSyncInclude(attention.decision.localInclude, config.include)
28
- );
29
- }
30
-
31
- export interface SyncAttentionController {
32
- set(decision: RemoteSelectionDecision, origin: SyncAttentionOrigin): void;
33
- current(): SyncAttentionState | undefined;
34
- markOffered(): boolean;
35
- clear(ctx: ExtensionContext): void;
36
- reset(ctx: ExtensionContext): void;
37
- publish(ctx: ExtensionContext): void;
38
- }
39
-
40
- export function createSyncAttentionController(): SyncAttentionController {
41
- let state: SyncAttentionState | undefined;
42
-
43
- return {
44
- set(decision, origin) {
45
- state = { decision, origin, offered: false };
46
- },
47
- current() {
48
- return state;
49
- },
50
- markOffered() {
51
- if (!state || state.offered) return false;
52
- state = { ...state, offered: true };
53
- return true;
54
- },
55
- clear(ctx) {
56
- state = undefined;
57
- clearAttentionPresentation(ctx);
58
- },
59
- reset(ctx) {
60
- state = undefined;
61
- clearAttentionPresentation(ctx);
62
- },
63
- publish(ctx) {
64
- if (!state) {
65
- clearAttentionPresentation(ctx);
66
- return;
67
- }
68
- const presentation = attentionPresentation(state.decision);
69
- ctx.ui.setStatus(STATUS_KEY, presentation.status);
70
- if (ctx.mode !== "tui") return;
71
- ctx.ui.setWidget(WIDGET_KEY, (_tui, theme) => ({
72
- render(width: number) {
73
- const safeWidth = Math.max(1, width);
74
- return presentation.lines.map((line, index) =>
75
- truncateToWidth(theme.fg(index === 0 ? "warning" : "muted", line), safeWidth, "…"),
76
- );
77
- },
78
- invalidate() {},
79
- }));
80
- },
81
- };
82
- }
83
-
84
- function attentionPresentation(decision: RemoteSelectionDecision) {
85
- const setupName = safeTerminalText(decision.setupName);
86
- const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
87
- const difference =
88
- comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0
89
- ? "Only list order differs"
90
- : `Remote ${comparison.remoteOnly.length} · Device ${comparison.localOnly.length}`;
91
- return {
92
- status: "review needed",
93
- lines: [`Pi Sync needs review · ${setupName}`, difference, "No changes · Run /sync to review"],
94
- };
95
- }
96
-
97
- function clearAttentionPresentation(ctx: ExtensionContext) {
98
- ctx.ui.setStatus(STATUS_KEY, undefined);
99
- ctx.ui.setWidget(WIDGET_KEY, undefined);
100
- }
package/src/webdav-ui.ts DELETED
@@ -1,421 +0,0 @@
1
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { promptSecret } from "./secret-input.js";
3
- import {
4
- addStorageConnection,
5
- addSyncSetup,
6
- saveNewV3Settings,
7
- updateStorageConnection,
8
- updateSyncSetup,
9
- } from "./settings-management.js";
10
- import { DEFAULT_SYNC_INCLUDE } from "./sync-policy.js";
11
- import type { PartialConfig } from "./types.js";
12
- import {
13
- normalizeWebDavPath,
14
- normalizeWebDavUrl,
15
- validateWebDavCredentials,
16
- validateWebDavNamespace,
17
- } from "./webdav-config.js";
18
-
19
- export async function showWebDavSetup(
20
- ctx: ExtensionCommandContext,
21
- targetName: string,
22
- signal?: AbortSignal,
23
- ) {
24
- const url = await requiredInput(
25
- ctx,
26
- "WebDAV collection URL",
27
- "https://cloud.example.com/remote.php/dav/files/user",
28
- signal,
29
- );
30
- if (!url) return false;
31
- const username = await requiredInput(ctx, "WebDAV username", "user", signal);
32
- if (!username) return false;
33
- const password = await awaitActive(signal, promptSecret(ctx, "WebDAV password", { signal }));
34
- if (password === undefined) return false;
35
- const location = await chooseDestination(ctx, targetName, signal);
36
- if (!location) return false;
37
- const connection = validateConnection(ctx, url, username, password);
38
- const destination = validateDestination(ctx, location.path);
39
- if (!connection || !destination) return false;
40
- const content = await chooseContent(ctx, signal);
41
- if (!content) return false;
42
- const automatic = await select(
43
- ctx,
44
- "Automatic sync for this setup",
45
- ["Enable automatic sync", "Keep automatic sync off", "Cancel"],
46
- signal,
47
- );
48
- if (!automatic || automatic === "Cancel") return false;
49
- const sessions = await chooseSessions(ctx, signal);
50
- if (sessions === undefined) return false;
51
- const profileName = "webdav";
52
- const review = await select(
53
- ctx,
54
- [
55
- "Review WebDAV setup",
56
- "",
57
- `Sync setup: ${safe(targetName)}`,
58
- `Storage connection: ${profileName} (WebDAV)`,
59
- `URL: ${displayUrl(connection.url)}`,
60
- `Storage location: ${safe(destination.path)}`,
61
- "Username: stored in the private settings file (value hidden)",
62
- "Password: configured (value hidden)",
63
- `Conditional writes: /sync doctor verifies atomic If-Match and If-None-Match support before publication.`,
64
- `Included content: ${content.length} built-in groups · Sessions: ${sessions ? "On — privacy warning acknowledged" : "Off"}`,
65
- `Automatic sync: ${automatic === "Enable automatic sync" ? "On" : "Off"}`,
66
- ].join("\n"),
67
- ["Save setup", "Cancel"],
68
- signal,
69
- );
70
- if (review !== "Save setup") return false;
71
- throwIfAborted(signal);
72
- await awaitActive(
73
- signal,
74
- saveNewV3Settings(
75
- {
76
- setupName: targetName,
77
- connectionName: profileName,
78
- connection: {
79
- type: "webdav",
80
- url: connection.url,
81
- credentials: { username: connection.username, password: connection.password ?? "" },
82
- },
83
- setup: {
84
- storage: { connection: profileName, path: destination.path },
85
- sync: {
86
- include: [...content, ...(sessions ? ["sessions"] : [])],
87
- automatic: automatic === "Enable automatic sync",
88
- },
89
- },
90
- },
91
- signal,
92
- ),
93
- );
94
- ctx.ui.notify(`Sync setup “${safe(targetName)}” is ready. Use Sync now when ready.`, "info");
95
- return true;
96
- }
97
-
98
- export async function showAddWebDavTarget(
99
- ctx: ExtensionCommandContext,
100
- name: string,
101
- profile: string,
102
- signal?: AbortSignal,
103
- ) {
104
- const location = await chooseDestination(ctx, name, signal);
105
- if (!location) return false;
106
- const destination = validateDestination(ctx, location.path);
107
- if (!destination) return false;
108
- const content = await chooseContent(ctx, signal);
109
- if (!content) return false;
110
- const review = await select(
111
- ctx,
112
- `Review WebDAV sync setup\n\nSync setup: ${safe(name)}\nStorage connection: ${safe(profile)}\nStorage location: ${safe(destination.path)}\nIncluded content: ${content.length} built-in groups · Sessions: Off\nAdding this setup does not sync or modify remote data.`,
113
- ["Add sync setup", "Cancel"],
114
- signal,
115
- );
116
- if (review !== "Add sync setup") return false;
117
- throwIfAborted(signal);
118
- await awaitActive(
119
- signal,
120
- addSyncSetup(
121
- name,
122
- {
123
- storage: { connection: profile, path: destination.path },
124
- sync: { include: content, automatic: true },
125
- },
126
- signal,
127
- ),
128
- );
129
- ctx.ui.notify(`Added sync setup “${safe(name)}”.`, "info");
130
- return true;
131
- }
132
-
133
- export async function showEditWebDavTarget(
134
- ctx: ExtensionCommandContext,
135
- partial: PartialConfig,
136
- signal?: AbortSignal,
137
- ) {
138
- const remotePath = await requiredInput(ctx, "WebDAV storage path", partial.storagePath, signal);
139
- if (!remotePath) return false;
140
- const destination = validateDestination(ctx, remotePath);
141
- if (!destination) return false;
142
- const review = await select(
143
- ctx,
144
- `Review sync setup “${safe(partial.setupName)}”\n\nStorage path: ${safe(partial.storagePath)} → ${safe(destination.path)}\nSaving changes the future storage location only; it does not move or delete remote data.`,
145
- ["Save sync setup", "Cancel"],
146
- signal,
147
- );
148
- if (review !== "Save sync setup") return false;
149
- throwIfAborted(signal);
150
- await awaitActive(
151
- signal,
152
- updateSyncSetup(
153
- partial.setupName,
154
- (setup) => ({
155
- ...setup,
156
- storage: { ...setup.storage, path: destination.path },
157
- }),
158
- { expectedStorage: partial, signal },
159
- ),
160
- );
161
- ctx.ui.notify(`Saved sync setup “${safe(partial.setupName)}”.`, "info");
162
- return true;
163
- }
164
-
165
- export async function showAddWebDavStorageProfile(
166
- ctx: ExtensionCommandContext,
167
- signal?: AbortSignal,
168
- ) {
169
- const name = await requiredInput(ctx, "Name this storage connection", "webdav", signal);
170
- if (!name) return false;
171
- const url = await requiredInput(
172
- ctx,
173
- "WebDAV collection URL",
174
- "https://cloud.example.com/dav",
175
- signal,
176
- );
177
- if (!url) return false;
178
- const username = await requiredInput(ctx, "WebDAV username", "user", signal);
179
- if (!username) return false;
180
- const password = await awaitActive(signal, promptSecret(ctx, "WebDAV password", { signal }));
181
- if (password === undefined) return false;
182
- const connection = validateConnection(ctx, url, username, password);
183
- if (!connection) return false;
184
- const review = await select(
185
- ctx,
186
- `Review storage connection\n\nName: ${safe(name)}\nType: WebDAV\nURL: ${displayUrl(connection.url)}\nUsername: stored privately (value hidden)\nPassword: configured (value hidden)\nAdding a connection does not contact the server or start syncing.`,
187
- ["Add storage connection", "Cancel"],
188
- signal,
189
- );
190
- if (review !== "Add storage connection") return false;
191
- throwIfAborted(signal);
192
- await awaitActive(
193
- signal,
194
- addStorageConnection(
195
- name,
196
- {
197
- type: "webdav",
198
- url: connection.url,
199
- credentials: { username: connection.username, password: connection.password ?? "" },
200
- },
201
- signal,
202
- ),
203
- );
204
- ctx.ui.notify(`Added storage connection “${safe(name)}”.`, "info");
205
- return true;
206
- }
207
-
208
- export async function showEditWebDavStorageProfile(
209
- ctx: ExtensionCommandContext,
210
- name: string,
211
- profile: Record<string, unknown>,
212
- signal?: AbortSignal,
213
- affectedSetups?: string[],
214
- ) {
215
- const url = await requiredInput(
216
- ctx,
217
- "WebDAV collection URL",
218
- String(profile.url ?? "https://cloud.example.com/dav"),
219
- signal,
220
- );
221
- if (!url) return false;
222
- const username = await requiredInput(
223
- ctx,
224
- "WebDAV username",
225
- String(profile.username ?? "user"),
226
- signal,
227
- );
228
- if (!username) return false;
229
- let password: string | undefined;
230
- let replacePassword = false;
231
- if (typeof profile.password === "string" && profile.password.length > 0) {
232
- const passwordAction = await select(
233
- ctx,
234
- "WebDAV password",
235
- ["Keep current password", "Replace password", "Cancel"],
236
- signal,
237
- );
238
- if (!passwordAction || passwordAction === "Cancel") return false;
239
- replacePassword = passwordAction === "Replace password";
240
- } else {
241
- replacePassword = true;
242
- }
243
- if (replacePassword) {
244
- password = await awaitActive(signal, promptSecret(ctx, "New WebDAV password", { signal }));
245
- if (password === undefined) return false;
246
- }
247
- const connection = validateConnection(ctx, url, username, password);
248
- if (!connection) return false;
249
- const review = await select(
250
- ctx,
251
- `Review storage connection\n\nStorage connection: ${safe(name)}\nURL: ${displayUrl(String(profile.url ?? "https://invalid.invalid"))} → ${displayUrl(connection.url)}\nUsername: stored privately (value hidden)\nPassword: ${replacePassword ? "will be replaced" : "unchanged"} (value hidden)\nAffected sync setups: ${affectedSetups && affectedSetups.length > 0 ? affectedSetups.map(safe).join(", ") : "None"}\nSaving changes future storage access for every affected setup; it does not move remote data.`,
252
- ["Save storage connection", "Cancel"],
253
- signal,
254
- );
255
- if (review !== "Save storage connection") return false;
256
- throwIfAborted(signal);
257
- await awaitActive(
258
- signal,
259
- updateStorageConnection(
260
- name,
261
- (current) => {
262
- if (current.type !== "webdav") {
263
- throw new Error("Storage connection type changed; reopen it.");
264
- }
265
- return {
266
- ...current,
267
- url: connection.url,
268
- credentials: {
269
- ...current.credentials,
270
- username: connection.username,
271
- password: replacePassword
272
- ? (connection.password ?? current.credentials.password)
273
- : current.credentials.password,
274
- },
275
- };
276
- },
277
- affectedSetups,
278
- signal,
279
- ),
280
- );
281
- ctx.ui.notify(`Saved storage connection “${safe(name)}”.`, "info");
282
- return true;
283
- }
284
-
285
- async function chooseDestination(
286
- ctx: ExtensionCommandContext,
287
- targetName: string,
288
- signal?: AbortSignal,
289
- ) {
290
- const remotePath = await requiredInput(
291
- ctx,
292
- "WebDAV storage path",
293
- `pi-sync/${targetName}`,
294
- signal,
295
- );
296
- return remotePath ? validateDestination(ctx, remotePath) : undefined;
297
- }
298
-
299
- async function chooseContent(ctx: ExtensionCommandContext, signal?: AbortSignal) {
300
- const choice = await select(
301
- ctx,
302
- "Choose an initial sync preset",
303
- ["Recommended Pi settings", "Minimal settings", "Cancel"],
304
- signal,
305
- );
306
- if (!choice || choice === "Cancel") return undefined;
307
- return choice === "Minimal settings" ? ["settings.json", "AGENTS.md"] : [...DEFAULT_SYNC_INCLUDE];
308
- }
309
-
310
- async function chooseSessions(ctx: ExtensionCommandContext, signal?: AbortSignal) {
311
- const choice = await select(
312
- ctx,
313
- "Session conversations\n\nSessions can contain prompts, tool output, paths, screenshots, and secrets.",
314
- ["Keep sessions off (recommended)", "Include session conversations", "Cancel"],
315
- signal,
316
- );
317
- if (!choice || choice === "Cancel") return undefined;
318
- if (choice !== "Include session conversations") return false;
319
- return confirm(
320
- ctx,
321
- "Include session conversations?",
322
- "I understand that session JSONL can contain prompts, tool output, paths, screenshots, and secrets.",
323
- signal,
324
- );
325
- }
326
-
327
- async function requiredInput(
328
- ctx: ExtensionCommandContext,
329
- title: string,
330
- placeholder: string,
331
- signal?: AbortSignal,
332
- ) {
333
- const value = await awaitActive(signal, ctx.ui.input(title, placeholder, { signal }));
334
- if (value === undefined) return undefined;
335
- const trimmed = value.trim();
336
- if (!trimmed) {
337
- ctx.ui.notify(`${title} is required.`, "warning");
338
- return undefined;
339
- }
340
- return trimmed;
341
- }
342
-
343
- async function select(
344
- ctx: ExtensionCommandContext,
345
- title: string,
346
- options: string[],
347
- signal?: AbortSignal,
348
- ) {
349
- return awaitActive(signal, ctx.ui.select(title, options, { signal }));
350
- }
351
-
352
- async function confirm(
353
- ctx: ExtensionCommandContext,
354
- title: string,
355
- message: string,
356
- signal?: AbortSignal,
357
- ) {
358
- return awaitActive(signal, ctx.ui.confirm(title, message, { signal }));
359
- }
360
-
361
- async function awaitActive<T>(signal: AbortSignal | undefined, operation: Promise<T>) {
362
- const result = await operation;
363
- throwIfAborted(signal);
364
- return result;
365
- }
366
-
367
- function throwIfAborted(signal?: AbortSignal) {
368
- if (!signal?.aborted) return;
369
- throw signal.reason instanceof Error
370
- ? signal.reason
371
- : new DOMException("The operation was aborted", "AbortError");
372
- }
373
-
374
- function validateConnection(
375
- ctx: ExtensionCommandContext,
376
- url: string,
377
- username: string,
378
- password?: string,
379
- ) {
380
- try {
381
- const normalizedUrl = normalizeWebDavUrl(url);
382
- if (!normalizedUrl) throw new Error("WebDAV URL is required.");
383
- validateWebDavCredentials(username, password);
384
- return {
385
- url: normalizedUrl,
386
- username: username.trim(),
387
- ...(password === undefined ? {} : { password }),
388
- };
389
- } catch (error) {
390
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
391
- return undefined;
392
- }
393
- }
394
-
395
- function validateDestination(ctx: ExtensionCommandContext, path: string, namespace?: string) {
396
- try {
397
- const basePath = normalizeWebDavPath(path);
398
- const normalizedPath = namespace
399
- ? normalizeWebDavPath(`${basePath}/${namespace.trim()}`)
400
- : basePath;
401
- const resolvedNamespace = normalizedPath.slice(normalizedPath.lastIndexOf("/") + 1);
402
- validateWebDavNamespace(resolvedNamespace);
403
- return { path: normalizedPath, namespace: resolvedNamespace };
404
- } catch (error) {
405
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
406
- return undefined;
407
- }
408
- }
409
-
410
- function displayUrl(value: string) {
411
- try {
412
- return `${new URL(value).origin}/…`;
413
- } catch {
414
- return "invalid URL (value hidden)";
415
- }
416
- }
417
-
418
- function safe(value: string) {
419
- // biome-ignore lint/suspicious/noControlCharactersInRegex: Settings values are untrusted terminal input.
420
- return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "�");
421
- }
File without changes
File without changes
File without changes
File without changes