@narumitw/pi-sync 0.49.15 → 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 +49 -16
  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-JF6MZCT4.ts → chunk-AUSDLD6F.ts} +4 -4
  9. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  10. package/dist/chunks/{chunk-X7SQ2EQM.ts → chunk-AXXS2BPA.ts} +9 -9
  11. package/dist/chunks/chunk-AXXS2BPA.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-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-WSOBYHIZ.ts → chunk-GWMHAQG4.ts} +4 -4
  21. package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
  22. package/dist/chunks/{chunk-5LZALPCD.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-DFXBKHGN.ts → chunk-LCRI3ABW.ts} +21 -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-HVUSHMVL.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-GLQLN5GV.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-44LPGQTI.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-ARN2672F.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-RKEHALNQ.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-552LXHE4.ts → s3-backend-3P33LQHU.ts} +96 -18
  69. package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
  70. package/dist/chunks/{setup-switch-5YDG5YGJ.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-2RYBUDRA.ts → snapshot-HKEWCVK6.ts} +7 -6
  74. package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
  75. package/dist/chunks/{sync-operations-3LLIHWZO.ts → sync-operations-ONJ27QRL.ts} +374 -294
  76. package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
  77. package/dist/chunks/{sync-state-SRN6YRBK.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 +30 -6
  84. package/package.json +1 -1
  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} +39 -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} +34 -325
  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 -286
  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} +17 -13
  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-44LPGQTI.ts.map +0 -7
  164. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  165. package/dist/chunks/chunk-5LZALPCD.ts.map +0 -7
  166. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  167. package/dist/chunks/chunk-5T442OP3.ts.map +0 -7
  168. package/dist/chunks/chunk-ARN2672F.ts.map +0 -7
  169. package/dist/chunks/chunk-C7LXZ3UH.ts +0 -167
  170. package/dist/chunks/chunk-C7LXZ3UH.ts.map +0 -7
  171. package/dist/chunks/chunk-DFXBKHGN.ts.map +0 -7
  172. package/dist/chunks/chunk-E4COSV32.ts +0 -82
  173. package/dist/chunks/chunk-E4COSV32.ts.map +0 -7
  174. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  175. package/dist/chunks/chunk-GLQLN5GV.ts.map +0 -7
  176. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  177. package/dist/chunks/chunk-HVUSHMVL.ts.map +0 -7
  178. package/dist/chunks/chunk-JF6MZCT4.ts.map +0 -7
  179. package/dist/chunks/chunk-WSOBYHIZ.ts.map +0 -7
  180. package/dist/chunks/chunk-X7SQ2EQM.ts.map +0 -7
  181. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  182. package/dist/chunks/file-selection-RKEHALNQ.ts.map +0 -7
  183. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  184. package/dist/chunks/manager-result-dispatcher-LWPYJAD5.ts +0 -18
  185. package/dist/chunks/manager-ui-NF4RUHBY.ts +0 -2785
  186. package/dist/chunks/manager-ui-NF4RUHBY.ts.map +0 -7
  187. package/dist/chunks/remote-selection-ui-NUF5P4ZJ.ts +0 -17
  188. package/dist/chunks/s3-backend-552LXHE4.ts.map +0 -7
  189. package/dist/chunks/sync-operations-3LLIHWZO.ts.map +0 -7
  190. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  191. package/src/config.ts +0 -923
  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-LWPYJAD5.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
  197. /package/dist/chunks/{remote-selection-ui-NUF5P4ZJ.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
  198. /package/dist/chunks/{setup-switch-5YDG5YGJ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  199. /package/dist/chunks/{snapshot-2RYBUDRA.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  200. /package/dist/chunks/{sync-state-SRN6YRBK.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,1255 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ automaticSyncSummary
5
+ } from "./chunk-Z4DQF6L3.ts";
6
+ import {
7
+ setSyncSetupCompletions
8
+ } from "./chunk-CHH26D4F.ts";
9
+ import {
10
+ syncErrorGuidance
11
+ } from "./chunk-CPEHGXJV.ts";
12
+ import {
13
+ safeTerminalText
14
+ } from "./chunk-LTBWGSBV.ts";
15
+ import {
16
+ addStorageConnection,
17
+ addSyncSetup,
18
+ saveNewV3Settings,
19
+ updateStorageConnection,
20
+ updateSyncSetup
21
+ } from "./chunk-LCRI3ABW.ts";
22
+ import {
23
+ configuredSyncSetupNames,
24
+ effectiveSyncSetupRemoteIdentity,
25
+ normalizeS3Bucket,
26
+ normalizeS3Endpoint,
27
+ normalizeStoragePath,
28
+ ownRecord,
29
+ readLocalConfigObject,
30
+ requiredString,
31
+ validateConfigName
32
+ } from "./chunk-C7XY5NF5.ts";
33
+ import {
34
+ normalizeWebDavPath,
35
+ normalizeWebDavUrl,
36
+ validateWebDavCredentials
37
+ } from "./chunk-6LP3MCR2.ts";
38
+ import {
39
+ normalizeGitBranch,
40
+ normalizeGitDirectory,
41
+ normalizeGitRemote
42
+ } from "./chunk-TEN2ZBJV.ts";
43
+ import {
44
+ errorMessage
45
+ } from "./chunk-2B2ELUF5.ts";
46
+ import {
47
+ DEFAULT_SYNC_INCLUDE
48
+ } from "./chunk-JAAKLL6M.ts";
49
+
50
+ // src/commands/setup-completions.ts
51
+ async function refreshTargetCompletions() {
52
+ setSyncSetupCompletions(await configuredSyncSetupNames());
53
+ }
54
+
55
+ // src/ui/setup/text-input.ts
56
+ async function requiredExistingBucket(ctx, example, signal) {
57
+ return requiredValueInput(
58
+ ctx,
59
+ "Existing bucket\n\nThe bucket must already exist; pi-sync will not create it.",
60
+ example,
61
+ signal,
62
+ normalizeS3Bucket
63
+ );
64
+ }
65
+ async function requiredInput(ctx, title, defaultValue, signal, validate) {
66
+ return promptTextInput(ctx, title, { defaultValue, validate, rejectPlaceholders: true }, signal);
67
+ }
68
+ async function requiredValueInput(ctx, title, example, signal, validate) {
69
+ return promptTextInput(ctx, title, { example, validate, rejectPlaceholders: true }, signal);
70
+ }
71
+ async function promptTextInput(ctx, title, options, signal) {
72
+ const hint = options.defaultValue !== void 0 ? `Default: ${safeTerminalText(options.defaultValue)} (leave blank to keep)` : `Example: ${safeTerminalText(options.example ?? "")}
73
+ Enter your own value; this example is not a default.`;
74
+ while (true) {
75
+ signal?.throwIfAborted();
76
+ const value = await ctx.ui.input(`${title}
77
+
78
+ ${hint}`, void 0, { signal });
79
+ signal?.throwIfAborted();
80
+ if (value === void 0) return void 0;
81
+ try {
82
+ const normalized = value.trim() || options.defaultValue;
83
+ if (!normalized) throw new Error(`${title.split("\n")[0]} is required.`);
84
+ if (options.rejectPlaceholders && /[<>]/u.test(normalized)) {
85
+ throw new Error("Replace example placeholders such as <account-id> with your own value.");
86
+ }
87
+ const checked = requiredString(normalized, title.split("\n")[0] ?? "value");
88
+ const result = options.validate ? await options.validate(checked) : checked;
89
+ signal?.throwIfAborted();
90
+ return result;
91
+ } catch (error) {
92
+ signal?.throwIfAborted();
93
+ ctx.ui.notify(`${errorMessage(error)} Enter a corrected value, or cancel.`, "warning");
94
+ }
95
+ }
96
+ }
97
+
98
+ // src/ui/setup/setup-location-ui.ts
99
+ async function chooseInitialRemoteLocation(ctx, _preset, setupName, signal) {
100
+ const choice = await ctx.ui.select(
101
+ "Choose storage location\n\nUse an existing bucket. ./ stores snapshots at the bucket root; a custom path keeps them in a separate folder.",
102
+ ["Use an existing bucket at ./", "Customize remote location", "Cancel"],
103
+ { signal }
104
+ );
105
+ if (signal?.aborted || !choice || choice === "Cancel") return void 0;
106
+ if (choice === "Customize remote location")
107
+ return chooseCustomRemoteLocation(ctx, setupName, signal);
108
+ const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
109
+ return bucket ? { connectionName: setupName, bucket, path: "./" } : void 0;
110
+ }
111
+ async function chooseAdditionalRemoteLocation(ctx, settings, connectionName, setupName, signal) {
112
+ const setups = ownRecord(settings.syncSetups) ?? {};
113
+ const currentSetup = typeof settings.activeSyncSetup === "string" ? settings.activeSyncSetup : void 0;
114
+ const candidates = Object.entries(setups).map(([name, value]) => ({ name, storage: ownRecord(ownRecord(value)?.storage) })).filter(
115
+ (item) => item.storage?.connection === connectionName && typeof item.storage.bucket === "string"
116
+ );
117
+ const source = candidates.find((item) => item.name === currentSetup) ?? candidates.sort((left, right) => left.name.localeCompare(right.name))[0];
118
+ if (source) {
119
+ const suggestedPath = "./";
120
+ const sameBucketLabel = `Same bucket as \u201C${safeTerminalText(source.name)}\u201D`;
121
+ const choice = await ctx.ui.select(
122
+ [
123
+ `Storage location for \u201C${safeTerminalText(setupName)}\u201D`,
124
+ "",
125
+ `Existing bucket: ${safeTerminalText(String(source.storage.bucket))}`,
126
+ `Remote path: ${safeTerminalText(suggestedPath)}`,
127
+ "./ uses the bucket root. Use a different path or bucket for independent setups."
128
+ ].join("\n"),
129
+ [sameBucketLabel, "Use a different bucket", "Customize remote location", "Cancel"],
130
+ { signal }
131
+ );
132
+ if (signal?.aborted || !choice || choice === "Cancel") return void 0;
133
+ if (choice === sameBucketLabel) {
134
+ return { bucket: String(source.storage.bucket), path: suggestedPath };
135
+ }
136
+ if (choice === "Use a different bucket") {
137
+ const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
138
+ return bucket ? { bucket, path: "./" } : void 0;
139
+ }
140
+ const custom = await chooseCustomRemoteLocation(ctx, connectionName, signal);
141
+ return custom ? { bucket: custom.bucket, path: custom.path } : void 0;
142
+ }
143
+ const location = await chooseInitialRemoteLocation(ctx, "S3", setupName, signal);
144
+ return location ? { bucket: location.bucket, path: location.path } : void 0;
145
+ }
146
+ async function chooseCustomRemoteLocation(ctx, connectionName, signal) {
147
+ const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
148
+ if (!bucket) return void 0;
149
+ const storagePath = await requiredInput(
150
+ ctx,
151
+ "Storage path\n\nPath inside the bucket, not your local filesystem.\n./ uses the bucket root. Use different paths for independent setups.",
152
+ "./",
153
+ signal,
154
+ normalizeStoragePath
155
+ );
156
+ if (!storagePath) return void 0;
157
+ return { connectionName, bucket, path: normalizeStoragePath(storagePath) };
158
+ }
159
+ async function promptAvailableSetupStorage(ctx, initial, signal) {
160
+ let storage = initial;
161
+ while (!signal?.aborted) {
162
+ const settings = await readLocalConfigObject();
163
+ if (signal?.aborted) return void 0;
164
+ const connection = settings?.storageConnections[storage.connection];
165
+ if (!settings || !connection) throw new Error("Storage connection changed; reopen setup.");
166
+ const identity = setupPublicationIdentity(storage, connection);
167
+ const occupied = Object.entries(settings.syncSetups).find(
168
+ ([, setup]) => setupPublicationIdentity(
169
+ setup.storage,
170
+ settings.storageConnections[setup.storage.connection]
171
+ ) === identity
172
+ );
173
+ if (!occupied) return storage;
174
+ const git = connection.type === "git";
175
+ const message = `${git ? "Git branch" : "Storage location"} is already used by \u201C${safeTerminalText(occupied[0])}\u201D. ${git ? "Use a different branch; pi-sync owns the entire branch." : "Enter a different storage path, or cancel to choose another connection or bucket."}`;
176
+ ctx.ui.notify(message, "warning");
177
+ const title = `${git ? "Git branch for the new setup" : "Storage path for the new setup"}
178
+
179
+ ${message}`;
180
+ const example = git ? "pi-sync/work" : "backups/work";
181
+ const value = connection.type === "webdav" ? await promptTextInput(ctx, title, { example }, signal) : await requiredValueInput(ctx, title, example, signal);
182
+ if (signal?.aborted || value === void 0) return void 0;
183
+ try {
184
+ storage = git ? { ...storage, branch: normalizeGitBranch(value) } : { ...storage, path: normalizeStoragePath(value) };
185
+ } catch (error) {
186
+ ctx.ui.notify(safeTerminalText(errorMessage(error)), "warning");
187
+ }
188
+ }
189
+ return void 0;
190
+ }
191
+ function setupPublicationIdentity(storage, connection) {
192
+ return effectiveSyncSetupRemoteIdentity(
193
+ {
194
+ storage: connection.type === "git" ? { ...storage, path: "./" } : storage,
195
+ sync: { include: [], automatic: false }
196
+ },
197
+ connection
198
+ );
199
+ }
200
+
201
+ // src/ui/setup/setup-prompts.ts
202
+ async function readSetupConnection(name, signal) {
203
+ const connection = (await readLocalConfigObject())?.storageConnections[name];
204
+ signal?.throwIfAborted();
205
+ if (!connection) throw new Error("Storage connection changed; reopen setup.");
206
+ return connection;
207
+ }
208
+ var AUTOMATIC_SYNC_DESCRIPTION = "Check the current setup in the background at startup; review changes in /sync before transferring. At shutdown, automatically push selected content only when sessions are included. No startup dialogs.";
209
+ async function chooseSetupContent(ctx, signal) {
210
+ const choice = await ctx.ui.select(
211
+ "Choose included content\n\nRecommended: Pi settings, instructions, skills, prompts, themes, and extensions.\nMinimal: settings.json and AGENTS.md.\nReview the exact paths before saving.",
212
+ ["Recommended Pi settings", "Minimal settings", "Cancel"],
213
+ { signal }
214
+ );
215
+ signal?.throwIfAborted();
216
+ if (!choice || choice === "Cancel") return void 0;
217
+ return choice === "Minimal settings" ? ["settings.json", "AGENTS.md"] : [...DEFAULT_SYNC_INCLUDE];
218
+ }
219
+ async function chooseAutomaticSync(ctx, signal) {
220
+ const choice = await ctx.ui.select(
221
+ `Automatic sync for this setup
222
+
223
+ ${AUTOMATIC_SYNC_DESCRIPTION}`,
224
+ ["Keep automatic sync off", "Enable automatic sync", "Cancel"],
225
+ { signal }
226
+ );
227
+ signal?.throwIfAborted();
228
+ return !choice || choice === "Cancel" ? void 0 : choice === "Enable automatic sync";
229
+ }
230
+ async function chooseSessions(ctx, signal) {
231
+ const choice = await ctx.ui.select(
232
+ "Session conversations\n\nSessions can contain prompts, tool output, paths, screenshots, and secrets.",
233
+ ["Keep sessions off (recommended)", "Include session conversations", "Cancel"],
234
+ { signal }
235
+ );
236
+ signal?.throwIfAborted();
237
+ if (!choice || choice === "Cancel") return void 0;
238
+ if (choice !== "Include session conversations") return false;
239
+ const confirmed = await ctx.ui.confirm(
240
+ "Include session conversations?",
241
+ "I understand that session JSONL can contain prompts, tool output, paths, screenshots, and secrets.",
242
+ { signal }
243
+ );
244
+ signal?.throwIfAborted();
245
+ return confirmed ? true : void 0;
246
+ }
247
+ function includedContentLines(include, sessions = false) {
248
+ const paths = [...include, ...sessions && !include.includes("sessions") ? ["sessions"] : []];
249
+ return [
250
+ `Included content: ${paths.length} paths`,
251
+ ...paths.map((item) => ` ${safeTerminalText(item)}`),
252
+ `Sessions: ${sessions ? "On \u2014 privacy warning acknowledged" : "Off"}`
253
+ ];
254
+ }
255
+ async function promptResourceName(ctx, kind, defaultValue, signal, literalBrackets = false) {
256
+ return promptTextInput(
257
+ ctx,
258
+ `Name this ${kind}
259
+
260
+ A local label, such as home or work.`,
261
+ {
262
+ defaultValue,
263
+ rejectPlaceholders: !literalBrackets,
264
+ validate: async (name) => {
265
+ validateConfigName(name, kind);
266
+ const settings = await readLocalConfigObject();
267
+ signal?.throwIfAborted();
268
+ const names = kind === "sync setup" ? settings?.syncSetups : settings?.storageConnections;
269
+ if (names && Object.hasOwn(names, name)) {
270
+ throw new Error(`A ${kind} with this name already exists. Choose a different name.`);
271
+ }
272
+ return name;
273
+ }
274
+ },
275
+ signal
276
+ );
277
+ }
278
+
279
+ // src/ui/setup/setup-review.ts
280
+ import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
281
+ async function saveReviewedDraft(ctx, title, lines, label, save, signal) {
282
+ let saved = false;
283
+ let failure;
284
+ let failureText;
285
+ const menu = defineMenu({
286
+ start: "review",
287
+ screens: {
288
+ review: () => ({
289
+ kind: "review",
290
+ title,
291
+ content: [...failureText ? [failureText, ""] : [], ...lines].join("\n"),
292
+ lines: ["Review before saving. Back cancels this draft."],
293
+ format: { kind: "text" },
294
+ viewportSize: "adaptive",
295
+ confirm: { id: "save", label, action: "save" },
296
+ hint: "back"
297
+ })
298
+ },
299
+ actions: {
300
+ save: async ({ signal: actionSignal }) => {
301
+ try {
302
+ actionSignal.throwIfAborted();
303
+ await save(actionSignal);
304
+ saved = true;
305
+ return { kind: "close" };
306
+ } catch (error) {
307
+ if (actionSignal.aborted) return { kind: "close" };
308
+ if (!retryableSettingsError(error)) {
309
+ failure = error;
310
+ return { kind: "close" };
311
+ }
312
+ failureText = `Not saved. ${syncErrorGuidance(error)}
313
+ Your draft is retained. Fix the problem, then save again, or cancel.`;
314
+ ctx.ui.notify(failureText, "error");
315
+ return { kind: "stay" };
316
+ }
317
+ }
318
+ }
319
+ });
320
+ const result = await runMenu(ctx, menu, {
321
+ getState: () => void 0,
322
+ signal,
323
+ isCurrent: () => !signal?.aborted
324
+ });
325
+ if (signal?.aborted) return false;
326
+ if (failure) throw failure;
327
+ if (result.kind === "error") throw result.error;
328
+ return saved;
329
+ }
330
+ function retryableSettingsError(error) {
331
+ const code = error?.code;
332
+ return code !== void 0 && ["EACCES", "EPERM", "ENOSPC", "EBUSY", "EIO", "EMFILE", "ENFILE", "EROFS"].includes(code);
333
+ }
334
+
335
+ // src/ui/setup/git-ui.ts
336
+ async function showGitSetup(ctx, targetName, signal) {
337
+ const remote = await promptGitRemote(ctx, signal);
338
+ if (!remote) return false;
339
+ const destination = await promptGitDestination(ctx, signal);
340
+ if (!destination) return false;
341
+ const include = await chooseSetupContent(ctx, signal);
342
+ if (!include) return false;
343
+ const automatic = await chooseAutomaticSync(ctx, signal);
344
+ if (automatic === void 0) return false;
345
+ const saved = await saveReviewedDraft(
346
+ ctx,
347
+ "Review Git sync setup",
348
+ [
349
+ `Sync setup: ${safeTerminalText(targetName)}`,
350
+ `Storage connection: ${safeTerminalText(targetName)} (Git)`,
351
+ `Remote: ${safeTerminalText(remote)}`,
352
+ `Sync branch: ${safeTerminalText(destination.branch)}`,
353
+ `Storage location: ${safeTerminalText(destination.directory)}`,
354
+ ...includedContentLines(include),
355
+ `Automatic sync: ${automaticSyncSummary(automatic)}`,
356
+ "Authentication: existing Git credential helper or SSH configuration (not stored by pi-sync).",
357
+ "pi-sync manages the entire branch. The repository must already exist; a new branch is created on first push.",
358
+ "Saving does not contact remote storage or start syncing."
359
+ ],
360
+ "Save setup",
361
+ (saveSignal) => saveNewV3Settings(
362
+ {
363
+ setupName: targetName,
364
+ connectionName: targetName,
365
+ connection: { type: "git", remote },
366
+ setup: {
367
+ storage: {
368
+ connection: targetName,
369
+ branch: destination.branch,
370
+ path: destination.directory
371
+ },
372
+ sync: { include, automatic }
373
+ }
374
+ },
375
+ saveSignal
376
+ ),
377
+ signal
378
+ );
379
+ if (saved && !signal?.aborted)
380
+ ctx.ui.notify(
381
+ `Saved Git sync setup \u201C${safeTerminalText(targetName)}\u201D. Choose Sync now to start.`,
382
+ "info"
383
+ );
384
+ return saved;
385
+ }
386
+ async function showAddGitStorageProfile(ctx, signal) {
387
+ const name = await promptResourceName(ctx, "storage connection", "git", signal);
388
+ if (!name) return false;
389
+ const remote = await promptGitRemote(ctx, signal);
390
+ if (!remote) return false;
391
+ const saved = await saveReviewedDraft(
392
+ ctx,
393
+ "Review storage connection",
394
+ [
395
+ `Name: ${safeTerminalText(name)}`,
396
+ "Type: Git",
397
+ `Remote: ${safeTerminalText(remote)}`,
398
+ "Credentials: existing Git/SSH authentication (not stored)",
399
+ "Adding a connection does not contact the remote or start syncing."
400
+ ],
401
+ "Add storage connection",
402
+ (saveSignal) => addStorageConnection(name, { type: "git", remote }, saveSignal),
403
+ signal
404
+ );
405
+ if (saved && !signal?.aborted)
406
+ ctx.ui.notify(`Added storage connection \u201C${safeTerminalText(name)}\u201D.`, "info");
407
+ return saved;
408
+ }
409
+ async function showEditGitStorageProfile(ctx, name, profile, signal, affectedSetups) {
410
+ const remote = await promptGitRemote(
411
+ ctx,
412
+ signal,
413
+ typeof profile.remote === "string" ? profile.remote : void 0
414
+ );
415
+ if (!remote) return false;
416
+ const saved = await saveReviewedDraft(
417
+ ctx,
418
+ "Review storage connection",
419
+ [
420
+ `Storage connection: ${safeTerminalText(name)}`,
421
+ `Remote: ${safeTerminalText(String(profile.remote ?? "missing"))} \u2192 ${safeTerminalText(remote)}`,
422
+ `Affected sync setups: ${affectedSetups?.length ? affectedSetups.map(safeTerminalText).join(", ") : "None"}`,
423
+ "Saving changes future storage access for every affected setup; it does not move or delete remote history."
424
+ ],
425
+ "Save storage connection",
426
+ (saveSignal) => updateStorageConnection(
427
+ name,
428
+ (current) => {
429
+ if (current.type !== "git" || current.remote !== profile.remote)
430
+ throw new Error("Storage connection changed while it was open; reopen it.");
431
+ return { ...current, remote };
432
+ },
433
+ affectedSetups,
434
+ saveSignal
435
+ ),
436
+ signal
437
+ );
438
+ if (saved && !signal?.aborted)
439
+ ctx.ui.notify(`Saved storage connection \u201C${safeTerminalText(name)}\u201D.`, "info");
440
+ return saved;
441
+ }
442
+ async function showAddGitTarget(ctx, name, profile, signal) {
443
+ const selected = await promptGitDestination(ctx, signal);
444
+ if (!selected) return false;
445
+ const storage = await promptAvailableSetupStorage(
446
+ ctx,
447
+ { connection: profile, branch: selected.branch, path: selected.directory },
448
+ signal
449
+ );
450
+ if (!storage) return false;
451
+ const include = await chooseSetupContent(ctx, signal);
452
+ if (!include) return false;
453
+ const automatic = await chooseAutomaticSync(ctx, signal);
454
+ if (automatic === void 0) return false;
455
+ const connection = await readSetupConnection(profile, signal);
456
+ if (connection?.type !== "git") throw new Error("Storage connection changed; reopen setup.");
457
+ const saved = await saveReviewedDraft(
458
+ ctx,
459
+ "Review Git sync setup",
460
+ [
461
+ `Sync setup: ${safeTerminalText(name)}`,
462
+ `Storage connection: ${safeTerminalText(profile)}`,
463
+ `Remote: ${safeTerminalText(connection.remote)}`,
464
+ `Sync branch: ${safeTerminalText(storage.branch)}`,
465
+ `Storage location: ${safeTerminalText(storage.path)}`,
466
+ ...includedContentLines(include),
467
+ `Automatic sync: ${automaticSyncSummary(automatic)}`,
468
+ "pi-sync manages the entire branch. Adding this setup does not sync or modify remote data."
469
+ ],
470
+ "Add sync setup",
471
+ (saveSignal) => addSyncSetup(name, { storage, sync: { include, automatic } }, saveSignal, connection),
472
+ signal
473
+ );
474
+ if (saved && !signal?.aborted)
475
+ ctx.ui.notify(`Added sync setup \u201C${safeTerminalText(name)}\u201D.`, "info");
476
+ return saved;
477
+ }
478
+ async function showEditGitTarget(ctx, partial, signal) {
479
+ let destination = await promptGitDestination(ctx, signal, partial);
480
+ if (!destination) return false;
481
+ while (destination.directory !== partial.storagePath && destination.branch === partial.branch) {
482
+ ctx.ui.notify(
483
+ "Changing a Git storage path requires a new Git branch so the existing branch remains readable.",
484
+ "warning"
485
+ );
486
+ const branch = await requiredValueInput(
487
+ ctx,
488
+ "New Git branch\n\nKeep the previous branch unchanged; enter a new branch for this path.",
489
+ "pi-sync/archive",
490
+ signal,
491
+ normalizeGitBranch
492
+ );
493
+ if (!branch) return false;
494
+ destination = { ...destination, branch };
495
+ }
496
+ const chosen = destination;
497
+ const connection = await readSetupConnection(partial.connectionName, signal);
498
+ if (connection.type !== "git") throw new Error("Storage connection changed; reopen setup.");
499
+ const saved = await saveReviewedDraft(
500
+ ctx,
501
+ "Review sync setup",
502
+ [
503
+ `Sync setup: ${safeTerminalText(partial.setupName)}`,
504
+ `Storage connection: ${safeTerminalText(partial.connectionName)}`,
505
+ `Remote: ${safeTerminalText(connection.remote)}`,
506
+ `Branch: ${safeTerminalText(partial.branch ?? "missing")} \u2192 ${safeTerminalText(chosen.branch)}`,
507
+ `Storage path: ${safeTerminalText(partial.storagePath)} \u2192 ${safeTerminalText(chosen.directory)}`,
508
+ "Saving changes the future storage location only; it does not move or delete remote history."
509
+ ],
510
+ "Save sync setup",
511
+ (saveSignal) => updateSyncSetup(
512
+ partial.setupName,
513
+ (setup) => {
514
+ if (typeof setup.storage.branch !== "string")
515
+ throw new Error("Sync setup storage type changed; reopen it.");
516
+ return {
517
+ ...setup,
518
+ storage: { ...setup.storage, branch: chosen.branch, path: chosen.directory }
519
+ };
520
+ },
521
+ { expectedStorage: partial, expectedConnection: connection, signal: saveSignal }
522
+ ),
523
+ signal
524
+ );
525
+ if (saved && !signal?.aborted)
526
+ ctx.ui.notify(`Saved sync setup \u201C${safeTerminalText(partial.setupName)}\u201D.`, "info");
527
+ return saved;
528
+ }
529
+ async function promptGitRemote(ctx, signal, current) {
530
+ const title = "Git remote URL (SSH or HTTPS)\n\nUse an existing private repository with Git/SSH authentication already configured.";
531
+ const validate = (value) => {
532
+ const remote = normalizeGitRemote(value);
533
+ if (!remote) throw new Error("Git remote URL is required.");
534
+ return remote;
535
+ };
536
+ return current ? requiredInput(ctx, title, current, signal, validate) : requiredValueInput(
537
+ ctx,
538
+ title,
539
+ "git@github.com:owner/private-pi-sync.git (SSH) or https://github.com/owner/private-pi-sync.git (HTTPS)",
540
+ signal,
541
+ validate
542
+ );
543
+ }
544
+ async function promptGitDestination(ctx, signal, current = {}) {
545
+ const branch = await requiredInput(
546
+ ctx,
547
+ "Git branch for sync snapshots\n\npi-sync manages this entire branch, not your local working branch.\nUse a new branch or one already used by pi-sync; unrelated content is rejected.",
548
+ current.branch ?? "main",
549
+ signal,
550
+ normalizeGitBranch
551
+ );
552
+ if (!branch) return void 0;
553
+ const directory = await requiredInput(
554
+ ctx,
555
+ "Git storage path\n\nRelative to the repository root, not your local filesystem.\n./ stores manifest.json and files/ at the repository root.",
556
+ current.storagePath ?? "./",
557
+ signal,
558
+ normalizeGitDirectory
559
+ );
560
+ return directory ? { branch, directory } : void 0;
561
+ }
562
+
563
+ // src/ui/secret-input.ts
564
+ import {
565
+ CURSOR_MARKER,
566
+ decodeKittyPrintable,
567
+ Key,
568
+ matchesKey,
569
+ Text,
570
+ truncateToWidth
571
+ } from "@earendil-works/pi-tui";
572
+ import { runCustomInteraction } from "@narumitw/pi-tui-kit";
573
+ var MASK = "\u2022";
574
+ async function promptSecret(ctx, title, options = { required: true }) {
575
+ if (ctx.mode !== "tui" || options.signal?.aborted) return void 0;
576
+ const result = await runCustomInteraction(ctx, {
577
+ signal: options.signal,
578
+ isCurrent: () => !options.signal?.aborted,
579
+ create: ({ tui, theme, keybindings, complete }) => {
580
+ const heading = new Text("", 0, 0);
581
+ const hint = new Text("", 0, 0);
582
+ const submitKey = keybindingText(keybindings, "tui.input.submit", "enter");
583
+ const cancelKey = keybindingText(keybindings, "tui.select.cancel", "esc", ["ctrl+c"]);
584
+ const applyTheme = () => {
585
+ heading.setText(theme.fg("accent", theme.bold(title)));
586
+ hint.setText(
587
+ theme.fg("dim", `${submitKey} continue \u2022 ${cancelKey} cancel \u2022 Input is hidden`)
588
+ );
589
+ };
590
+ applyTheme();
591
+ const input = new MaskedInput(keybindings);
592
+ const component = {
593
+ get focused() {
594
+ return input.focused;
595
+ },
596
+ set focused(focused) {
597
+ input.focused = focused;
598
+ },
599
+ render(width) {
600
+ const safeWidth = Math.max(1, width);
601
+ return [
602
+ ...heading.render(safeWidth),
603
+ ...input.render(safeWidth),
604
+ ...hint.render(safeWidth)
605
+ ].map((line) => truncateToWidth(line, safeWidth));
606
+ },
607
+ invalidate() {
608
+ applyTheme();
609
+ heading.invalidate();
610
+ input.invalidate();
611
+ hint.invalidate();
612
+ },
613
+ handleInput(data) {
614
+ if (matchesKey(data, Key.ctrl("c"))) {
615
+ complete(void 0);
616
+ } else if (input.isPasting || data.includes("\x1B[200~")) {
617
+ input.handleInput(data);
618
+ } else if (keybindings.matches(data, "tui.select.cancel")) {
619
+ complete(void 0);
620
+ } else if (keybindings.matches(data, "tui.input.submit")) {
621
+ if (options.required !== false && input.getValue().length === 0) {
622
+ ctx.ui.notify(`${title} is required. Enter a value, or cancel.`, "warning");
623
+ } else if (hasControlCharacter(input.getValue())) {
624
+ ctx.ui.notify(
625
+ `${title} contains control characters. Remove them or re-enter the value, then continue.`,
626
+ "warning"
627
+ );
628
+ } else complete(input.getValue());
629
+ } else input.handleInput(data);
630
+ tui.requestRender();
631
+ },
632
+ dispose() {
633
+ input.clear();
634
+ }
635
+ };
636
+ return component;
637
+ }
638
+ });
639
+ if (result.kind === "error") throw result.error;
640
+ if (result.kind !== "completed" || result.value === void 0) return void 0;
641
+ const value = result.value;
642
+ if (options.required !== false && value.length === 0) {
643
+ ctx.ui.notify(`${title} is required.`, "warning");
644
+ return void 0;
645
+ }
646
+ return value;
647
+ }
648
+ var MaskedInput = class {
649
+ constructor(keybindings) {
650
+ this.keybindings = keybindings;
651
+ }
652
+ keybindings;
653
+ focused = false;
654
+ value = [];
655
+ cursor = 0;
656
+ paste = "";
657
+ pasting = false;
658
+ get isPasting() {
659
+ return this.pasting;
660
+ }
661
+ getValue() {
662
+ return this.value.join("");
663
+ }
664
+ handleInput(data) {
665
+ if (data.includes("\x1B[200~")) {
666
+ this.pasting = true;
667
+ this.paste = "";
668
+ data = data.replace("\x1B[200~", "");
669
+ }
670
+ if (this.pasting) {
671
+ this.paste += data;
672
+ const end = this.paste.indexOf("\x1B[201~");
673
+ if (end < 0) return;
674
+ const pasted = this.paste.slice(0, end).replace(/[\r\n]/gu, "").replace(/\t/gu, " ");
675
+ this.insert(pasted);
676
+ const remaining = this.paste.slice(end + 6);
677
+ this.paste = "";
678
+ this.pasting = false;
679
+ if (remaining) this.handleInput(remaining);
680
+ return;
681
+ }
682
+ if (this.keybindings.matches(data, "tui.editor.deleteCharBackward")) {
683
+ if (this.cursor > 0) this.value.splice(--this.cursor, 1);
684
+ return;
685
+ }
686
+ if (this.keybindings.matches(data, "tui.editor.deleteCharForward")) {
687
+ if (this.cursor < this.value.length) this.value.splice(this.cursor, 1);
688
+ return;
689
+ }
690
+ if (this.keybindings.matches(data, "tui.editor.cursorLeft")) {
691
+ this.cursor = Math.max(0, this.cursor - 1);
692
+ return;
693
+ }
694
+ if (this.keybindings.matches(data, "tui.editor.cursorRight")) {
695
+ this.cursor = Math.min(this.value.length, this.cursor + 1);
696
+ return;
697
+ }
698
+ if (this.keybindings.matches(data, "tui.editor.cursorLineStart")) {
699
+ this.cursor = 0;
700
+ return;
701
+ }
702
+ if (this.keybindings.matches(data, "tui.editor.cursorLineEnd")) {
703
+ this.cursor = this.value.length;
704
+ return;
705
+ }
706
+ if (this.keybindings.matches(data, "tui.editor.deleteToLineStart")) {
707
+ this.value.splice(0, this.cursor);
708
+ this.cursor = 0;
709
+ return;
710
+ }
711
+ if (this.keybindings.matches(data, "tui.editor.deleteToLineEnd")) {
712
+ this.value.splice(this.cursor);
713
+ return;
714
+ }
715
+ const printable = decodeKittyPrintable(data) ?? data;
716
+ if (!hasControlCharacter(printable)) this.insert(printable);
717
+ }
718
+ render(width) {
719
+ const prompt = "> ";
720
+ const available = width - prompt.length;
721
+ if (available <= 0) return [truncateToWidth(prompt, Math.max(1, width))];
722
+ const contentWidth = Math.max(0, available - 1);
723
+ let start = 0;
724
+ if (this.value.length > contentWidth) {
725
+ start = Math.max(
726
+ 0,
727
+ Math.min(this.cursor - Math.floor(contentWidth / 2), this.value.length - contentWidth)
728
+ );
729
+ }
730
+ const end = Math.min(this.value.length, start + contentWidth);
731
+ const visibleCursor = Math.max(0, Math.min(this.cursor - start, end - start));
732
+ const masks = Array.from({ length: end - start }, () => MASK);
733
+ const before = masks.slice(0, visibleCursor).join("");
734
+ const atCursor = visibleCursor < masks.length ? MASK : " ";
735
+ const after = masks.slice(visibleCursor + (visibleCursor < masks.length ? 1 : 0)).join("");
736
+ const marker = this.focused ? CURSOR_MARKER : "";
737
+ const line = `${prompt}${before}${marker}\x1B[7m${atCursor}\x1B[27m${after}`;
738
+ return [truncateToWidth(line, width, "")];
739
+ }
740
+ invalidate() {
741
+ }
742
+ clear() {
743
+ this.value.fill("");
744
+ this.value = [];
745
+ this.paste = "";
746
+ this.cursor = 0;
747
+ this.pasting = false;
748
+ }
749
+ insert(value) {
750
+ const characters = Array.from(value);
751
+ this.value.splice(this.cursor, 0, ...characters);
752
+ this.cursor += characters.length;
753
+ }
754
+ };
755
+ function keybindingText(keybindings, binding, fallback, additionalKeys = []) {
756
+ const keys = [.../* @__PURE__ */ new Set([...keybindings.getKeys(binding), ...additionalKeys])].map(String).map((key) => {
757
+ if (key === "return") return "enter";
758
+ if (key === "escape") return "esc";
759
+ return hasControlCharacter(key) ? "" : key;
760
+ }).filter(Boolean);
761
+ return keys.join("/") || fallback;
762
+ }
763
+ function hasControlCharacter(value) {
764
+ return [...value].some((character) => {
765
+ const code = character.codePointAt(0) ?? 0;
766
+ return code < 32 || code >= 127 && code <= 159;
767
+ });
768
+ }
769
+
770
+ // src/ui/setup/s3-credentials-ui.ts
771
+ async function chooseS3CredentialUpdate(ctx, profile, signal) {
772
+ const hasStored = typeof profile.accessKeyId === "string" && profile.accessKeyId.length > 0 && typeof profile.secretAccessKey === "string" && profile.secretAccessKey.length > 0;
773
+ if (hasStored) {
774
+ const action = await ctx.ui.select(
775
+ "Credentials",
776
+ ["Keep current credentials", "Replace credentials", "Cancel"],
777
+ { signal }
778
+ );
779
+ throwIfAborted(signal);
780
+ if (!action || action === "Cancel") return void 0;
781
+ if (action === "Keep current credentials") {
782
+ return { profileFields: {}, summary: "Unchanged (values hidden)", ready: true };
783
+ }
784
+ }
785
+ const selected = await chooseS3Credentials(ctx, signal);
786
+ return selected ? { ...selected, replace: true } : void 0;
787
+ }
788
+ function applyS3CredentialUpdate(profile, credentials) {
789
+ const next = { ...profile };
790
+ if (credentials.replace) {
791
+ delete next.accessKeyId;
792
+ delete next.secretAccessKey;
793
+ delete next.sessionToken;
794
+ }
795
+ return { ...next, ...credentials.profileFields };
796
+ }
797
+ async function chooseS3Credentials(ctx, signal) {
798
+ const choice = await ctx.ui.select(
799
+ "Credentials\n\nCredentials are stored in the private pi-sync settings file. Secret values are masked during input and never shown afterward.",
800
+ ["Store credentials privately", "Cancel"],
801
+ { signal }
802
+ );
803
+ throwIfAborted(signal);
804
+ if (choice !== "Store credentials privately") return void 0;
805
+ const accessKeyId = await requiredValueInput(
806
+ ctx,
807
+ "Access key ID\n\nUse the S3 API access key ID issued by your provider, not the secret access key.",
808
+ "access-key-id",
809
+ signal
810
+ );
811
+ if (!accessKeyId) return void 0;
812
+ const secretAccessKey = await promptSecret(ctx, "Secret access key", { signal });
813
+ throwIfAborted(signal);
814
+ if (secretAccessKey === void 0) return void 0;
815
+ return {
816
+ profileFields: { accessKeyId, secretAccessKey },
817
+ summary: "Stored privately (values hidden)",
818
+ ready: true
819
+ };
820
+ }
821
+ function throwIfAborted(signal) {
822
+ if (!signal?.aborted) return;
823
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
824
+ }
825
+
826
+ // src/ui/setup/s3-ui.ts
827
+ async function showS3Setup(ctx, preset, targetName, signal) {
828
+ const endpoint = await requiredValueInput(
829
+ ctx,
830
+ preset === "Cloudflare R2" ? "Cloudflare R2 endpoint\n\nCopy the S3 API endpoint from your R2 account; replace <account-id>." : "S3-compatible endpoint\n\nUse your provider's S3 API URL, not its web console.",
831
+ preset === "Cloudflare R2" ? "https://<account-id>.r2.cloudflarestorage.com" : "https://s3.example.com",
832
+ signal,
833
+ normalizeS3Endpoint
834
+ );
835
+ if (!endpoint) return false;
836
+ const region = preset === "Cloudflare R2" ? "auto" : await requiredInput(
837
+ ctx,
838
+ "Storage region\n\nUse the region assigned to your bucket by the provider.",
839
+ "us-east-1",
840
+ signal
841
+ );
842
+ if (!region) return false;
843
+ const location = await chooseInitialRemoteLocation(ctx, preset, targetName, signal);
844
+ if (!location) return false;
845
+ const credentials = await chooseS3Credentials(ctx, signal);
846
+ if (!credentials) return false;
847
+ const include = await chooseSetupContent(ctx, signal);
848
+ if (!include) return false;
849
+ const automatic = await chooseAutomaticSync(ctx, signal);
850
+ if (automatic === void 0) return false;
851
+ const sessions = await chooseSessions(ctx, signal);
852
+ if (sessions === void 0) return false;
853
+ const saved = await saveReviewedDraft(
854
+ ctx,
855
+ "Review sync setup",
856
+ [
857
+ `Sync setup: ${safeTerminalText(targetName)}`,
858
+ `Storage connection: ${safeTerminalText(location.connectionName)} (${preset})`,
859
+ `Endpoint: ${safeTerminalText(endpoint)}`,
860
+ `Region: ${safeTerminalText(region)}`,
861
+ `Bucket: ${safeTerminalText(location.bucket)}`,
862
+ `Storage location: ${safeTerminalText(location.path)}`,
863
+ ...includedContentLines(include, sessions),
864
+ `Automatic sync: ${automaticSyncSummary(automatic)}`,
865
+ `Credentials: ${credentials.summary}`,
866
+ "The bucket must already exist. Saving does not contact remote storage or start syncing."
867
+ ],
868
+ "Save sync setup",
869
+ (saveSignal) => saveNewV3Settings(
870
+ {
871
+ setupName: targetName,
872
+ connectionName: location.connectionName,
873
+ connection: {
874
+ type: "s3",
875
+ endpoint,
876
+ region,
877
+ credentials: {
878
+ accessKeyId: credentials.profileFields.accessKeyId ?? "",
879
+ secretAccessKey: credentials.profileFields.secretAccessKey ?? ""
880
+ }
881
+ },
882
+ setup: {
883
+ storage: {
884
+ connection: location.connectionName,
885
+ bucket: location.bucket,
886
+ path: location.path
887
+ },
888
+ sync: { include: [...include, ...sessions ? ["sessions"] : []], automatic }
889
+ }
890
+ },
891
+ saveSignal
892
+ ),
893
+ signal
894
+ );
895
+ if (!saved || signal?.aborted) return false;
896
+ await refreshTargetCompletions();
897
+ if (!signal?.aborted)
898
+ ctx.ui.notify(
899
+ `Sync setup \u201C${safeTerminalText(targetName)}\u201D saved. Choose Sync now to start.`,
900
+ "info"
901
+ );
902
+ return true;
903
+ }
904
+ async function showAddS3Target(ctx, raw, profile, name, signal) {
905
+ const location = await chooseAdditionalRemoteLocation(ctx, raw, profile, name, signal);
906
+ if (!location) return;
907
+ const storage = await promptAvailableSetupStorage(
908
+ ctx,
909
+ { connection: profile, ...location },
910
+ signal
911
+ );
912
+ if (!storage) return;
913
+ const include = await chooseSetupContent(ctx, signal);
914
+ if (!include) return;
915
+ const automatic = await chooseAutomaticSync(ctx, signal);
916
+ if (automatic === void 0) return;
917
+ const connection = await readSetupConnection(profile, signal);
918
+ if (connection.type !== "s3") throw new Error("Storage connection changed; reopen setup.");
919
+ const saved = await saveReviewedDraft(
920
+ ctx,
921
+ "Review new sync setup",
922
+ [
923
+ `Sync setup: ${safeTerminalText(name)}`,
924
+ `Storage connection: ${safeTerminalText(profile)}`,
925
+ `Endpoint: ${safeTerminalText(connection.endpoint)}`,
926
+ `Bucket: ${safeTerminalText(storage.bucket)}`,
927
+ `Storage location: ${safeTerminalText(storage.path)}`,
928
+ ...includedContentLines(include),
929
+ `Automatic sync: ${automaticSyncSummary(automatic)}`,
930
+ "Only the current setup is checked at startup. Shutdown can push selected content when sessions are included.",
931
+ "The bucket must already exist. Adding this setup does not sync or modify remote data."
932
+ ],
933
+ "Add sync setup",
934
+ (saveSignal) => addSyncSetup(name, { storage, sync: { include, automatic } }, saveSignal, connection),
935
+ signal
936
+ );
937
+ if (!saved || signal?.aborted) return;
938
+ await refreshTargetCompletions();
939
+ if (!signal?.aborted) ctx.ui.notify(`Added sync setup \u201C${safeTerminalText(name)}\u201D.`, "info");
940
+ }
941
+ async function showEditS3Target(ctx, partial, signal) {
942
+ const bucket = await requiredInput(
943
+ ctx,
944
+ "Bucket\n\nUse an existing bucket; pi-sync will not create it.",
945
+ partial.bucket ?? "pi-sync",
946
+ signal,
947
+ normalizeS3Bucket
948
+ );
949
+ if (!bucket) return;
950
+ const storagePath = await requiredInput(
951
+ ctx,
952
+ "Storage path\n\nPath inside the bucket, not your local filesystem. ./ uses the bucket root.",
953
+ partial.storagePath,
954
+ signal,
955
+ normalizeStoragePath
956
+ );
957
+ if (!storagePath) return;
958
+ const connection = await readSetupConnection(partial.connectionName, signal);
959
+ if (connection.type !== "s3") throw new Error("Storage connection changed; reopen setup.");
960
+ const saved = await saveReviewedDraft(
961
+ ctx,
962
+ "Review sync setup",
963
+ [
964
+ `Sync setup: ${safeTerminalText(partial.setupName)}`,
965
+ `Storage connection: ${safeTerminalText(partial.connectionName)}`,
966
+ `Endpoint: ${safeTerminalText(connection.endpoint)}`,
967
+ `Bucket: ${safeTerminalText(partial.bucket ?? "missing")} \u2192 ${safeTerminalText(bucket)}`,
968
+ `Storage path: ${safeTerminalText(partial.storagePath)} \u2192 ${safeTerminalText(storagePath)}`,
969
+ "Saving changes the future storage location only; it does not move or delete remote data."
970
+ ],
971
+ "Save sync setup",
972
+ (saveSignal) => updateSyncSetup(
973
+ partial.setupName,
974
+ (setup) => {
975
+ if (typeof setup.storage.bucket !== "string")
976
+ throw new Error("Sync setup storage type changed; reopen it.");
977
+ return { ...setup, storage: { ...setup.storage, bucket, path: storagePath } };
978
+ },
979
+ { expectedStorage: partial, expectedConnection: connection, signal: saveSignal }
980
+ ),
981
+ signal
982
+ );
983
+ if (saved && !signal?.aborted)
984
+ ctx.ui.notify(`Saved sync setup \u201C${safeTerminalText(partial.setupName)}\u201D.`, "info");
985
+ }
986
+
987
+ // src/ui/setup/webdav-ui.ts
988
+ async function showWebDavSetup(ctx, targetName, signal) {
989
+ const url = await promptWebDavUrl(ctx, signal);
990
+ if (!url) return false;
991
+ const username = await promptUsername(ctx, signal);
992
+ if (!username) return false;
993
+ const password = await promptSecret(ctx, "WebDAV password", { signal });
994
+ signal?.throwIfAborted();
995
+ if (password === void 0) return false;
996
+ const remotePath = await chooseDestination(ctx, signal);
997
+ if (!remotePath) return false;
998
+ const include = await chooseSetupContent(ctx, signal);
999
+ if (!include) return false;
1000
+ const automatic = await chooseAutomaticSync(ctx, signal);
1001
+ if (automatic === void 0) return false;
1002
+ const sessions = await chooseSessions(ctx, signal);
1003
+ if (sessions === void 0) return false;
1004
+ const saved = await saveReviewedDraft(
1005
+ ctx,
1006
+ "Review WebDAV setup",
1007
+ [
1008
+ `Sync setup: ${safeTerminalText(targetName)}`,
1009
+ `Storage connection: ${safeTerminalText(targetName)} (WebDAV)`,
1010
+ `URL: ${safeTerminalText(url)}`,
1011
+ `Storage location: ${safeTerminalText(remotePath)}`,
1012
+ "Username and password: stored privately (values hidden)",
1013
+ ...includedContentLines(include, sessions),
1014
+ `Automatic sync: ${automaticSyncSummary(automatic)}`,
1015
+ "Check setup tests whether this server can safely save sync updates using a temporary probe.",
1016
+ "Saving does not contact remote storage or start syncing."
1017
+ ],
1018
+ "Save setup",
1019
+ (saveSignal) => saveNewV3Settings(
1020
+ {
1021
+ setupName: targetName,
1022
+ connectionName: targetName,
1023
+ connection: { type: "webdav", url, credentials: { username, password } },
1024
+ setup: {
1025
+ storage: { connection: targetName, path: remotePath },
1026
+ sync: { include: [...include, ...sessions ? ["sessions"] : []], automatic }
1027
+ }
1028
+ },
1029
+ saveSignal
1030
+ ),
1031
+ signal
1032
+ );
1033
+ if (saved && !signal?.aborted)
1034
+ ctx.ui.notify(`Sync setup \u201C${safeTerminalText(targetName)}\u201D saved. Choose Sync now to start.`, "info");
1035
+ return saved;
1036
+ }
1037
+ async function showAddWebDavTarget(ctx, name, profile, signal) {
1038
+ const remotePath = await chooseDestination(ctx, signal);
1039
+ if (!remotePath) return false;
1040
+ const storage = await promptAvailableSetupStorage(
1041
+ ctx,
1042
+ { connection: profile, path: remotePath },
1043
+ signal
1044
+ );
1045
+ if (!storage) return false;
1046
+ const include = await chooseSetupContent(ctx, signal);
1047
+ if (!include) return false;
1048
+ const automatic = await chooseAutomaticSync(ctx, signal);
1049
+ if (automatic === void 0) return false;
1050
+ const connection = await readSetupConnection(profile, signal);
1051
+ if (connection.type !== "webdav") throw new Error("Storage connection changed; reopen setup.");
1052
+ const saved = await saveReviewedDraft(
1053
+ ctx,
1054
+ "Review WebDAV sync setup",
1055
+ [
1056
+ `Sync setup: ${safeTerminalText(name)}`,
1057
+ `Storage connection: ${safeTerminalText(profile)}`,
1058
+ `URL: ${safeTerminalText(connection.url)}`,
1059
+ `Storage location: ${safeTerminalText(storage.path)}`,
1060
+ ...includedContentLines(include),
1061
+ `Automatic sync: ${automaticSyncSummary(automatic)}`,
1062
+ "Adding this setup does not sync or modify remote data."
1063
+ ],
1064
+ "Add sync setup",
1065
+ (saveSignal) => addSyncSetup(name, { storage, sync: { include, automatic } }, saveSignal, connection),
1066
+ signal
1067
+ );
1068
+ if (saved && !signal?.aborted) ctx.ui.notify(`Added sync setup \u201C${safeTerminalText(name)}\u201D.`, "info");
1069
+ return saved;
1070
+ }
1071
+ async function showEditWebDavTarget(ctx, partial, signal) {
1072
+ const remotePath = await chooseDestination(ctx, signal, partial.storagePath);
1073
+ if (!remotePath) return false;
1074
+ const connection = await readSetupConnection(partial.connectionName, signal);
1075
+ if (connection.type !== "webdav") throw new Error("Storage connection changed; reopen setup.");
1076
+ const saved = await saveReviewedDraft(
1077
+ ctx,
1078
+ "Review sync setup",
1079
+ [
1080
+ `Sync setup: ${safeTerminalText(partial.setupName)}`,
1081
+ `Storage connection: ${safeTerminalText(partial.connectionName)}`,
1082
+ `URL: ${safeTerminalText(connection.url)}`,
1083
+ `Storage path: ${safeTerminalText(partial.storagePath)} \u2192 ${safeTerminalText(remotePath)}`,
1084
+ "Saving changes the future storage location only; it does not move or delete remote data."
1085
+ ],
1086
+ "Save sync setup",
1087
+ (saveSignal) => updateSyncSetup(
1088
+ partial.setupName,
1089
+ (setup) => ({
1090
+ ...setup,
1091
+ storage: { ...setup.storage, path: remotePath }
1092
+ }),
1093
+ { expectedStorage: partial, expectedConnection: connection, signal: saveSignal }
1094
+ ),
1095
+ signal
1096
+ );
1097
+ if (saved && !signal?.aborted)
1098
+ ctx.ui.notify(`Saved sync setup \u201C${safeTerminalText(partial.setupName)}\u201D.`, "info");
1099
+ return saved;
1100
+ }
1101
+ async function showAddWebDavStorageProfile(ctx, signal) {
1102
+ const name = await promptResourceName(ctx, "storage connection", "webdav", signal, true);
1103
+ if (!name) return false;
1104
+ const url = await promptWebDavUrl(ctx, signal);
1105
+ if (!url) return false;
1106
+ const username = await promptUsername(ctx, signal);
1107
+ if (!username) return false;
1108
+ const password = await promptSecret(ctx, "WebDAV password", { signal });
1109
+ signal?.throwIfAborted();
1110
+ if (password === void 0) return false;
1111
+ const saved = await saveReviewedDraft(
1112
+ ctx,
1113
+ "Review storage connection",
1114
+ [
1115
+ `Name: ${safeTerminalText(name)}`,
1116
+ "Type: WebDAV",
1117
+ `URL: ${safeTerminalText(url)}`,
1118
+ "Username and password: stored privately (values hidden)",
1119
+ "Adding a connection does not contact the server or start syncing."
1120
+ ],
1121
+ "Add storage connection",
1122
+ (saveSignal) => addStorageConnection(
1123
+ name,
1124
+ { type: "webdav", url, credentials: { username, password } },
1125
+ saveSignal
1126
+ ),
1127
+ signal
1128
+ );
1129
+ if (saved && !signal?.aborted) ctx.ui.notify(`Added storage connection \u201C${safeTerminalText(name)}\u201D.`, "info");
1130
+ return saved;
1131
+ }
1132
+ async function showEditWebDavStorageProfile(ctx, name, profile, signal, affectedSetups) {
1133
+ const url = await promptWebDavUrl(
1134
+ ctx,
1135
+ signal,
1136
+ typeof profile.url === "string" ? profile.url : void 0
1137
+ );
1138
+ if (!url) return false;
1139
+ const username = await promptUsername(ctx, signal);
1140
+ if (!username) return false;
1141
+ const hasPassword = typeof profile.password === "string" && profile.password.length > 0;
1142
+ const passwordAction = hasPassword ? await select(
1143
+ ctx,
1144
+ "WebDAV password",
1145
+ ["Keep current password", "Replace password", "Cancel"],
1146
+ signal
1147
+ ) : "Replace password";
1148
+ if (!passwordAction || passwordAction === "Cancel") return false;
1149
+ const replacePassword = passwordAction === "Replace password";
1150
+ const password = replacePassword ? await promptSecret(ctx, "New WebDAV password", { signal }) : void 0;
1151
+ signal?.throwIfAborted();
1152
+ if (replacePassword && password === void 0) return false;
1153
+ const saved = await saveReviewedDraft(
1154
+ ctx,
1155
+ "Review storage connection",
1156
+ [
1157
+ `Storage connection: ${safeTerminalText(name)}`,
1158
+ `URL: ${safeTerminalText(String(profile.url))} \u2192 ${safeTerminalText(url)}`,
1159
+ "Username: stored privately (value hidden)",
1160
+ `Password: ${replacePassword ? "will be replaced" : "unchanged"} (value hidden)`,
1161
+ `Affected sync setups: ${affectedSetups?.length ? affectedSetups.map(safeTerminalText).join(", ") : "None"}`,
1162
+ "Saving changes future storage access for every affected setup; it does not move remote data."
1163
+ ],
1164
+ "Save storage connection",
1165
+ (saveSignal) => updateStorageConnection(
1166
+ name,
1167
+ (current) => {
1168
+ if (current.type !== "webdav" || current.url !== profile.url || current.credentials.username !== profile.username || current.credentials.password !== profile.password)
1169
+ throw new Error("Storage connection changed while it was open; reopen it.");
1170
+ return {
1171
+ ...current,
1172
+ url,
1173
+ credentials: {
1174
+ ...current.credentials,
1175
+ username,
1176
+ password: password ?? current.credentials.password
1177
+ }
1178
+ };
1179
+ },
1180
+ affectedSetups,
1181
+ saveSignal
1182
+ ),
1183
+ signal
1184
+ );
1185
+ if (saved && !signal?.aborted) ctx.ui.notify(`Saved storage connection \u201C${safeTerminalText(name)}\u201D.`, "info");
1186
+ return saved;
1187
+ }
1188
+ async function chooseDestination(ctx, signal, current = "./") {
1189
+ return promptTextInput(
1190
+ ctx,
1191
+ "WebDAV storage path\n\nFolder relative to the collection URL, not your local filesystem.\n./ uses the collection root. Use different folders for independent setups.",
1192
+ { defaultValue: current, validate: normalizeWebDavPath },
1193
+ signal
1194
+ );
1195
+ }
1196
+ async function promptUsername(ctx, signal) {
1197
+ return promptTextInput(
1198
+ ctx,
1199
+ "WebDAV username\n\nEnter the account username; stored values remain hidden.",
1200
+ {
1201
+ example: "user",
1202
+ validate: (value) => {
1203
+ validateWebDavCredentials(value);
1204
+ return value;
1205
+ }
1206
+ },
1207
+ signal
1208
+ );
1209
+ }
1210
+ async function promptWebDavUrl(ctx, signal, current) {
1211
+ return promptTextInput(
1212
+ ctx,
1213
+ "WebDAV collection URL\n\nUse the HTTPS WebDAV URL from your provider, not its web login page.",
1214
+ {
1215
+ ...current ? { defaultValue: current } : { example: "https://cloud.example.com/remote.php/dav/files/user" },
1216
+ validate: (value) => {
1217
+ const url = normalizeWebDavUrl(value);
1218
+ if (!url) throw new Error("WebDAV URL is required.");
1219
+ return url;
1220
+ }
1221
+ },
1222
+ signal
1223
+ );
1224
+ }
1225
+ async function select(ctx, title, options, signal) {
1226
+ const value = await ctx.ui.select(title, options, { signal });
1227
+ signal?.throwIfAborted();
1228
+ return value;
1229
+ }
1230
+
1231
+ export {
1232
+ refreshTargetCompletions,
1233
+ requiredInput,
1234
+ requiredValueInput,
1235
+ AUTOMATIC_SYNC_DESCRIPTION,
1236
+ promptResourceName,
1237
+ saveReviewedDraft,
1238
+ showGitSetup,
1239
+ showAddGitStorageProfile,
1240
+ showEditGitStorageProfile,
1241
+ showAddGitTarget,
1242
+ showEditGitTarget,
1243
+ chooseS3CredentialUpdate,
1244
+ applyS3CredentialUpdate,
1245
+ chooseS3Credentials,
1246
+ showS3Setup,
1247
+ showAddS3Target,
1248
+ showEditS3Target,
1249
+ showWebDavSetup,
1250
+ showAddWebDavTarget,
1251
+ showEditWebDavTarget,
1252
+ showAddWebDavStorageProfile,
1253
+ showEditWebDavStorageProfile
1254
+ };
1255
+ //# sourceMappingURL=chunk-OUHGBFYF.ts.map