@narumitw/pi-sync 0.50.3 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +89 -101
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +197 -205
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  176. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  177. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  178. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  179. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  180. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  181. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  182. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  183. package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  186. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  187. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  188. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  189. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  190. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  191. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  192. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  193. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  194. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  195. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  196. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  197. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  198. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  199. package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
  200. package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
  206. package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -6,87 +6,87 @@ import { safeTerminalText } from "../terminal-text.js";
6
6
  import { promptTextInput } from "./text-input.js";
7
7
 
8
8
  export async function readSetupConnection(name: string, signal?: AbortSignal) {
9
- const connection = (await readLocalConfigObject())?.storageConnections[name];
10
- signal?.throwIfAborted();
11
- if (!connection) throw new Error("Storage connection changed; reopen setup.");
12
- return connection;
9
+ const connection = (await readLocalConfigObject())?.storageConnections[name];
10
+ signal?.throwIfAborted();
11
+ if (!connection) throw new Error("Storage connection changed; reopen setup.");
12
+ return connection;
13
13
  }
14
14
 
15
15
  export const AUTOMATIC_SYNC_DESCRIPTION =
16
- "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.";
16
+ "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.";
17
17
 
18
18
  export async function chooseSetupContent(ctx: ExtensionCommandContext, signal?: AbortSignal) {
19
- const choice = await ctx.ui.select(
20
- "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.",
21
- ["Recommended Pi settings", "Minimal settings", "Cancel"],
22
- { signal },
23
- );
24
- signal?.throwIfAborted();
25
- if (!choice || choice === "Cancel") return undefined;
26
- return choice === "Minimal settings" ? ["settings.json", "AGENTS.md"] : [...DEFAULT_SYNC_INCLUDE];
19
+ const choice = await ctx.ui.select(
20
+ "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.",
21
+ ["Recommended Pi settings", "Minimal settings", "Cancel"],
22
+ { signal },
23
+ );
24
+ signal?.throwIfAborted();
25
+ if (!choice || choice === "Cancel") return undefined;
26
+ return choice === "Minimal settings" ? ["settings.json", "AGENTS.md"] : [...DEFAULT_SYNC_INCLUDE];
27
27
  }
28
28
 
29
29
  export async function chooseAutomaticSync(ctx: ExtensionCommandContext, signal?: AbortSignal) {
30
- const choice = await ctx.ui.select(
31
- `Automatic sync for this setup\n\n${AUTOMATIC_SYNC_DESCRIPTION}`,
32
- ["Keep automatic sync off", "Enable automatic sync", "Cancel"],
33
- { signal },
34
- );
35
- signal?.throwIfAborted();
36
- return !choice || choice === "Cancel" ? undefined : choice === "Enable automatic sync";
30
+ const choice = await ctx.ui.select(
31
+ `Automatic sync for this setup\n\n${AUTOMATIC_SYNC_DESCRIPTION}`,
32
+ ["Keep automatic sync off", "Enable automatic sync", "Cancel"],
33
+ { signal },
34
+ );
35
+ signal?.throwIfAborted();
36
+ return !choice || choice === "Cancel" ? undefined : choice === "Enable automatic sync";
37
37
  }
38
38
 
39
39
  export async function chooseSessions(ctx: ExtensionCommandContext, signal?: AbortSignal) {
40
- const choice = await ctx.ui.select(
41
- "Session conversations\n\nSessions can contain prompts, tool output, paths, screenshots, and secrets.",
42
- ["Keep sessions off (recommended)", "Include session conversations", "Cancel"],
43
- { signal },
44
- );
45
- signal?.throwIfAborted();
46
- if (!choice || choice === "Cancel") return undefined;
47
- if (choice !== "Include session conversations") return false;
48
- const confirmed = await ctx.ui.confirm(
49
- "Include session conversations?",
50
- "I understand that session JSONL can contain prompts, tool output, paths, screenshots, and secrets.",
51
- { signal },
52
- );
53
- signal?.throwIfAborted();
54
- return confirmed ? true : undefined;
40
+ const choice = await ctx.ui.select(
41
+ "Session conversations\n\nSessions can contain prompts, tool output, paths, screenshots, and secrets.",
42
+ ["Keep sessions off (recommended)", "Include session conversations", "Cancel"],
43
+ { signal },
44
+ );
45
+ signal?.throwIfAborted();
46
+ if (!choice || choice === "Cancel") return undefined;
47
+ if (choice !== "Include session conversations") return false;
48
+ const confirmed = await ctx.ui.confirm(
49
+ "Include session conversations?",
50
+ "I understand that session JSONL can contain prompts, tool output, paths, screenshots, and secrets.",
51
+ { signal },
52
+ );
53
+ signal?.throwIfAborted();
54
+ return confirmed ? true : undefined;
55
55
  }
56
56
 
57
57
  export function includedContentLines(include: readonly string[], sessions = false) {
58
- const paths = [...include, ...(sessions && !include.includes("sessions") ? ["sessions"] : [])];
59
- return [
60
- `Included content: ${paths.length} paths`,
61
- ...paths.map((item) => ` ${safeTerminalText(item)}`),
62
- `Sessions: ${sessions ? "On — privacy warning acknowledged" : "Off"}`,
63
- ];
58
+ const paths = [...include, ...(sessions && !include.includes("sessions") ? ["sessions"] : [])];
59
+ return [
60
+ `Included content: ${paths.length} paths`,
61
+ ...paths.map((item) => ` ${safeTerminalText(item)}`),
62
+ `Sessions: ${sessions ? "On — privacy warning acknowledged" : "Off"}`,
63
+ ];
64
64
  }
65
65
 
66
66
  export async function promptResourceName(
67
- ctx: ExtensionCommandContext,
68
- kind: "sync setup" | "storage connection",
69
- defaultValue: string,
70
- signal?: AbortSignal,
71
- literalBrackets = false,
67
+ ctx: ExtensionCommandContext,
68
+ kind: "sync setup" | "storage connection",
69
+ defaultValue: string,
70
+ signal?: AbortSignal,
71
+ literalBrackets = false,
72
72
  ) {
73
- return promptTextInput(
74
- ctx,
75
- `Name this ${kind}\n\nA local label, such as home or work.`,
76
- {
77
- defaultValue,
78
- rejectPlaceholders: !literalBrackets,
79
- validate: async (name) => {
80
- validateConfigName(name, kind);
81
- const settings = await readLocalConfigObject();
82
- signal?.throwIfAborted();
83
- const names = kind === "sync setup" ? settings?.syncSetups : settings?.storageConnections;
84
- if (names && Object.hasOwn(names, name)) {
85
- throw new Error(`A ${kind} with this name already exists. Choose a different name.`);
86
- }
87
- return name;
88
- },
89
- },
90
- signal,
91
- );
73
+ return promptTextInput(
74
+ ctx,
75
+ `Name this ${kind}\n\nA local label, such as home or work.`,
76
+ {
77
+ defaultValue,
78
+ rejectPlaceholders: !literalBrackets,
79
+ validate: async (name) => {
80
+ validateConfigName(name, kind);
81
+ const settings = await readLocalConfigObject();
82
+ signal?.throwIfAborted();
83
+ const names = kind === "sync setup" ? settings?.syncSetups : settings?.storageConnections;
84
+ if (names && Object.hasOwn(names, name)) {
85
+ throw new Error(`A ${kind} with this name already exists. Choose a different name.`);
86
+ }
87
+ return name;
88
+ },
89
+ },
90
+ signal,
91
+ );
92
92
  }
@@ -4,67 +4,66 @@ import { syncErrorGuidance } from "../../sync/sync-error-guidance.js";
4
4
 
5
5
  /** A settings draft is committed only after review; retryable I/O failures retain the same draft. */
6
6
  export async function saveReviewedDraft(
7
- ctx: ExtensionCommandContext,
8
- title: string,
9
- lines: readonly string[],
10
- label: string,
11
- save: (signal: AbortSignal) => Promise<unknown>,
12
- signal?: AbortSignal,
7
+ ctx: ExtensionCommandContext,
8
+ title: string,
9
+ lines: readonly string[],
10
+ label: string,
11
+ save: (signal: AbortSignal) => Promise<unknown>,
12
+ signal?: AbortSignal,
13
13
  ) {
14
- let saved = false;
15
- let failure: unknown;
16
- let failureText: string | undefined;
17
- const menu = defineMenu<undefined, "review", "save", ExtensionCommandContext>({
18
- start: "review",
19
- screens: {
20
- review: () => ({
21
- kind: "review",
22
- title,
23
- content: [...(failureText ? [failureText, ""] : []), ...lines].join("\n"),
24
- lines: ["Review before saving. Back cancels this draft."],
25
- format: { kind: "text" },
26
- viewportSize: "adaptive",
27
- confirm: { id: "save", label, action: "save" },
28
- hint: "back",
29
- }),
30
- },
31
- actions: {
32
- save: async ({ signal: actionSignal }) => {
33
- try {
34
- actionSignal.throwIfAborted();
35
- await save(actionSignal);
36
- saved = true;
37
- return { kind: "close" };
38
- } catch (error) {
39
- if (actionSignal.aborted) return { kind: "close" };
40
- // Do not silently rebase a stale review or overwrite an invalid settings file.
41
- // The caller must reopen current settings for those failures.
42
- if (!retryableSettingsError(error)) {
43
- failure = error;
44
- return { kind: "close" };
45
- }
46
- failureText = `Not saved. ${syncErrorGuidance(error)}\nYour draft is retained. Fix the problem, then save again, or cancel.`;
47
- ctx.ui.notify(failureText, "error");
48
- return { kind: "stay" };
49
- }
50
- },
51
- },
52
- });
53
- const result = await runMenu(ctx, menu, {
54
- getState: () => undefined,
55
- signal,
56
- isCurrent: () => !signal?.aborted,
57
- });
58
- if (signal?.aborted) return false;
59
- if (failure) throw failure;
60
- if (result.kind === "error") throw result.error;
61
- return saved;
14
+ let saved = false;
15
+ let failure: unknown;
16
+ let failureText: string | undefined;
17
+ const menu = defineMenu<undefined, "review", "save", ExtensionCommandContext>({
18
+ start: "review",
19
+ screens: {
20
+ review: () => ({
21
+ kind: "review",
22
+ title,
23
+ content: [...(failureText ? [failureText, ""] : []), ...lines].join("\n"),
24
+ lines: ["Review before saving. Back cancels this draft."],
25
+ format: { kind: "text" },
26
+ viewportSize: "adaptive",
27
+ confirm: { id: "save", label, action: "save" },
28
+ hint: "back",
29
+ }),
30
+ },
31
+ actions: {
32
+ save: async ({ signal: actionSignal }) => {
33
+ try {
34
+ actionSignal.throwIfAborted();
35
+ await save(actionSignal);
36
+ saved = true;
37
+ return { kind: "close" };
38
+ } catch (error) {
39
+ if (actionSignal.aborted) return { kind: "close" };
40
+ // Do not silently rebase a stale review or overwrite an invalid settings file.
41
+ // The caller must reopen current settings for those failures.
42
+ if (!retryableSettingsError(error)) {
43
+ failure = error;
44
+ return { kind: "close" };
45
+ }
46
+ failureText = `Not saved. ${syncErrorGuidance(error)}\nYour draft is retained. Fix the problem, then save again, or cancel.`;
47
+ ctx.ui.notify(failureText, "error");
48
+ return { kind: "stay" };
49
+ }
50
+ },
51
+ },
52
+ });
53
+ const result = await runMenu(ctx, menu, {
54
+ getState: () => undefined,
55
+ signal,
56
+ isCurrent: () => !signal?.aborted,
57
+ });
58
+ if (signal?.aborted) return false;
59
+ if (failure) throw failure;
60
+ if (result.kind === "error") throw result.error;
61
+ return saved;
62
62
  }
63
63
 
64
64
  function retryableSettingsError(error: unknown) {
65
- const code = (error as NodeJS.ErrnoException | undefined)?.code;
66
- return (
67
- code !== undefined &&
68
- ["EACCES", "EPERM", "ENOSPC", "EBUSY", "EIO", "EMFILE", "ENFILE", "EROFS"].includes(code)
69
- );
65
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
66
+ return (
67
+ code !== undefined && ["EACCES", "EPERM", "ENOSPC", "EBUSY", "EIO", "EMFILE", "ENFILE", "EROFS"].includes(code)
68
+ );
70
69
  }
@@ -13,169 +13,165 @@ import { backendStorageDescription } from "../manager-state.js";
13
13
  import { safeTerminalText } from "../terminal-text.js";
14
14
 
15
15
  async function selectSetupForSwitch(
16
- ctx: ExtensionCommandContext,
17
- raw: Record<string, unknown>,
18
- targets: Record<string, unknown>,
19
- active: string | undefined,
20
- signal?: AbortSignal,
16
+ ctx: ExtensionCommandContext,
17
+ raw: Record<string, unknown>,
18
+ targets: Record<string, unknown>,
19
+ active: string | undefined,
20
+ signal?: AbortSignal,
21
21
  ) {
22
- let selectedName: string | undefined;
23
- const nameById = new Map<string, string>();
24
- const profiles = ownRecord(raw.storageConnections);
25
- const menu = defineMenu<undefined, "setups", "select", ExtensionCommandContext>({
26
- start: "setups",
27
- screens: {
28
- setups: () => ({
29
- kind: "actions",
30
- title: "Switch sync setup",
31
- lines: [`Current sync setup: ${safeTerminalText(active ?? "none")}`],
32
- items: Object.keys(targets)
33
- .sort((left, right) => left.localeCompare(right))
34
- .map((candidate, index) => {
35
- const target = ownRecord(targets[candidate]);
36
- const storage = ownRecord(target?.storage);
37
- const profileName =
38
- typeof storage?.connection === "string" ? storage.connection : undefined;
39
- const profile = profileName && profiles ? ownRecord(profiles[profileName]) : undefined;
40
- const location = profile
41
- ? profile.type === "git"
42
- ? `${String(storage?.branch ?? "missing branch")}:${String(storage?.path ?? "missing path")}`
43
- : profile.type === "s3"
44
- ? `${String(storage?.bucket ?? "missing bucket")}/${String(storage?.path ?? "missing path")}`
45
- : String(storage?.path ?? "missing path")
46
- : `invalid: missing connection ${profileName ?? "reference"}`;
47
- const id = `setup:${index}`;
48
- nameById.set(id, candidate);
49
- return {
50
- id,
51
- label: `${safeTerminalText(candidate)}${candidate === active ? " (current)" : ""}`,
52
- description: `${safeTerminalText(profileName ?? "unknown")} · ${safeTerminalText(location)}`,
53
- action: "select" as const,
54
- };
55
- }),
56
- hint: "close",
57
- }),
58
- },
59
- actions: {
60
- select: async ({ itemId }) => {
61
- selectedName = nameById.get(itemId);
62
- return { kind: "close" };
63
- },
64
- },
65
- });
66
- await runMenu(ctx, menu, {
67
- getState: () => undefined,
68
- signal,
69
- isCurrent: () => !signal?.aborted,
70
- });
71
- return selectedName;
22
+ let selectedName: string | undefined;
23
+ const nameById = new Map<string, string>();
24
+ const profiles = ownRecord(raw.storageConnections);
25
+ const menu = defineMenu<undefined, "setups", "select", ExtensionCommandContext>({
26
+ start: "setups",
27
+ screens: {
28
+ setups: () => ({
29
+ kind: "actions",
30
+ title: "Switch sync setup",
31
+ lines: [`Current sync setup: ${safeTerminalText(active ?? "none")}`],
32
+ items: Object.keys(targets)
33
+ .sort((left, right) => left.localeCompare(right))
34
+ .map((candidate, index) => {
35
+ const target = ownRecord(targets[candidate]);
36
+ const storage = ownRecord(target?.storage);
37
+ const profileName = typeof storage?.connection === "string" ? storage.connection : undefined;
38
+ const profile = profileName && profiles ? ownRecord(profiles[profileName]) : undefined;
39
+ const location = profile
40
+ ? profile.type === "git"
41
+ ? `${String(storage?.branch ?? "missing branch")}:${String(storage?.path ?? "missing path")}`
42
+ : profile.type === "s3"
43
+ ? `${String(storage?.bucket ?? "missing bucket")}/${String(storage?.path ?? "missing path")}`
44
+ : String(storage?.path ?? "missing path")
45
+ : `invalid: missing connection ${profileName ?? "reference"}`;
46
+ const id = `setup:${index}`;
47
+ nameById.set(id, candidate);
48
+ return {
49
+ id,
50
+ label: `${safeTerminalText(candidate)}${candidate === active ? " (current)" : ""}`,
51
+ description: `${safeTerminalText(profileName ?? "unknown")} · ${safeTerminalText(location)}`,
52
+ action: "select" as const,
53
+ };
54
+ }),
55
+ hint: "close",
56
+ }),
57
+ },
58
+ actions: {
59
+ select: async ({ itemId }) => {
60
+ selectedName = nameById.get(itemId);
61
+ return { kind: "close" };
62
+ },
63
+ },
64
+ });
65
+ await runMenu(ctx, menu, {
66
+ getState: () => undefined,
67
+ signal,
68
+ isCurrent: () => !signal?.aborted,
69
+ });
70
+ return selectedName;
72
71
  }
73
72
 
74
73
  export async function showSetupSwitcher(
75
- ctx: ExtensionCommandContext,
76
- runRoute: RunRoute,
77
- selectedName?: string,
78
- signal?: AbortSignal,
74
+ ctx: ExtensionCommandContext,
75
+ runRoute: RunRoute,
76
+ selectedName?: string,
77
+ signal?: AbortSignal,
79
78
  ) {
80
- const raw = await readLocalConfigObject();
81
- if (signal?.aborted) return false;
82
- if (raw?.version !== 3) {
83
- ctx.ui.notify("Add a second sync setup before switching setups.", "info");
84
- return false;
85
- }
86
- const targets = ownRecord(raw.syncSetups);
87
- if (!targets) {
88
- ctx.ui.notify("No sync setups are configured.", "warning");
89
- return false;
90
- }
91
- const active = typeof raw.activeSyncSetup === "string" ? raw.activeSyncSetup : undefined;
92
- let name = selectedName;
93
- if (!name) {
94
- name = await selectSetupForSwitch(ctx, raw, targets, active, signal);
95
- if (!name) return false;
96
- }
97
- if (!name || !Object.hasOwn(targets, name)) {
98
- ctx.ui.notify(
99
- `Sync setup “${safeTerminalText(name ?? "unknown")}” no longer exists.`,
100
- "warning",
101
- );
102
- return false;
103
- }
104
- if (name === active) {
105
- ctx.ui.notify(`Sync setup “${safeTerminalText(name)}” is already current.`, "info");
106
- return false;
107
- }
108
- let config: AnySyncConfig;
109
- try {
110
- config = await loadConfig(name);
111
- if (signal?.aborted) return false;
112
- } catch (error) {
113
- ctx.ui.notify(
114
- `Cannot use sync setup “${safeTerminalText(name)}”: ${safeTerminalText(errorMessage(error))}`,
115
- "error",
116
- );
117
- return false;
118
- }
119
- const onSwitch = await loadOnSwitch();
120
- if (signal?.aborted) return false;
121
- const switchEffect =
122
- onSwitch === "ask-before-pull"
123
- ? "After switching, pi-sync will ask whether to review a pull for this setup."
124
- : onSwitch === "pull-after-switch"
125
- ? "After switching, pi-sync will check this setup and show exact changes before applying them."
126
- : "After switching, pi-sync will not pull or modify synced files.";
127
- const confirmed = await ctx.ui.confirm(
128
- "Switch sync setup?",
129
- [
130
- `From: ${safeTerminalText(active ?? "none")}`,
131
- `To: ${safeTerminalText(name)}`,
132
- `Storage: ${backendStorageDescription(config)}`,
133
- `Included content: ${config.include.length} paths`,
134
- `Automatic sync: ${automaticSyncSummary(config.automatic)} · Sessions: ${config.include.includes("sessions") ? "On" : "Off"}`,
135
- "",
136
- switchEffect,
137
- ].join("\n"),
138
- { signal },
139
- );
140
- if (signal?.aborted || !confirmed) return false;
141
- try {
142
- let pullClosed = false;
143
- const result = await useSyncSetup(
144
- ctx,
145
- name,
146
- async (selectedTarget) => {
147
- const pullResult = await runCancellableOperation(
148
- ctx,
149
- `Pulling sync setup “${safeTerminalText(name)}”…`,
150
- "pull",
151
- runRoute,
152
- {
153
- commitAware: true,
154
- cancelledMessage: null,
155
- target: selectedTarget,
156
- signal,
157
- },
158
- );
159
- const disposition = await dispatchManagerResult(ctx, pullResult, "pull", runRoute, signal);
160
- if (pullResult.kind === "closed" || pullResult.kind.endsWith("required")) {
161
- pullClosed = disposition.kind === "close";
162
- }
163
- if (disposition.appliedRoute === "pull") return "applied";
164
- if (pullResult.kind === "completed") return pullResult.outcome;
165
- return pullResult.kind === "cancelled" ? "cancelled" : undefined;
166
- },
167
- onSwitch,
168
- signal,
169
- syncConfigReviewIdentity(config),
170
- );
171
- if (pullClosed) return "closed";
172
- return result.pullApplied ? "pull-attempted" : "switched";
173
- } catch (error) {
174
- if (signal?.aborted) return false;
175
- ctx.ui.notify(
176
- `Sync setup “${safeTerminalText(name)}” was not switched: ${safeTerminalText(errorMessage(error))}`,
177
- "error",
178
- );
179
- return false;
180
- }
79
+ const raw = await readLocalConfigObject();
80
+ if (signal?.aborted) return false;
81
+ if (raw?.version !== 3) {
82
+ ctx.ui.notify("Add a second sync setup before switching setups.", "info");
83
+ return false;
84
+ }
85
+ const targets = ownRecord(raw.syncSetups);
86
+ if (!targets) {
87
+ ctx.ui.notify("No sync setups are configured.", "warning");
88
+ return false;
89
+ }
90
+ const active = typeof raw.activeSyncSetup === "string" ? raw.activeSyncSetup : undefined;
91
+ let name = selectedName;
92
+ if (!name) {
93
+ name = await selectSetupForSwitch(ctx, raw, targets, active, signal);
94
+ if (!name) return false;
95
+ }
96
+ if (!name || !Object.hasOwn(targets, name)) {
97
+ ctx.ui.notify(`Sync setup “${safeTerminalText(name ?? "unknown")}” no longer exists.`, "warning");
98
+ return false;
99
+ }
100
+ if (name === active) {
101
+ ctx.ui.notify(`Sync setup “${safeTerminalText(name)}” is already current.`, "info");
102
+ return false;
103
+ }
104
+ let config: AnySyncConfig;
105
+ try {
106
+ config = await loadConfig(name);
107
+ if (signal?.aborted) return false;
108
+ } catch (error) {
109
+ ctx.ui.notify(
110
+ `Cannot use sync setup “${safeTerminalText(name)}”: ${safeTerminalText(errorMessage(error))}`,
111
+ "error",
112
+ );
113
+ return false;
114
+ }
115
+ const onSwitch = await loadOnSwitch();
116
+ if (signal?.aborted) return false;
117
+ const switchEffect =
118
+ onSwitch === "ask-before-pull"
119
+ ? "After switching, pi-sync will ask whether to review a pull for this setup."
120
+ : onSwitch === "pull-after-switch"
121
+ ? "After switching, pi-sync will check this setup and show exact changes before applying them."
122
+ : "After switching, pi-sync will not pull or modify synced files.";
123
+ const confirmed = await ctx.ui.confirm(
124
+ "Switch sync setup?",
125
+ [
126
+ `From: ${safeTerminalText(active ?? "none")}`,
127
+ `To: ${safeTerminalText(name)}`,
128
+ `Storage: ${backendStorageDescription(config)}`,
129
+ `Included content: ${config.include.length} paths`,
130
+ `Automatic sync: ${automaticSyncSummary(config.automatic)} · Sessions: ${config.include.includes("sessions") ? "On" : "Off"}`,
131
+ "",
132
+ switchEffect,
133
+ ].join("\n"),
134
+ { signal },
135
+ );
136
+ if (signal?.aborted || !confirmed) return false;
137
+ try {
138
+ let pullClosed = false;
139
+ const result = await useSyncSetup(
140
+ ctx,
141
+ name,
142
+ async (selectedTarget) => {
143
+ const pullResult = await runCancellableOperation(
144
+ ctx,
145
+ `Pulling sync setup “${safeTerminalText(name)}”…`,
146
+ "pull",
147
+ runRoute,
148
+ {
149
+ commitAware: true,
150
+ cancelledMessage: null,
151
+ target: selectedTarget,
152
+ signal,
153
+ },
154
+ );
155
+ const disposition = await dispatchManagerResult(ctx, pullResult, "pull", runRoute, signal);
156
+ if (pullResult.kind === "closed" || pullResult.kind.endsWith("required")) {
157
+ pullClosed = disposition.kind === "close";
158
+ }
159
+ if (disposition.appliedRoute === "pull") return "applied";
160
+ if (pullResult.kind === "completed") return pullResult.outcome;
161
+ return pullResult.kind === "cancelled" ? "cancelled" : undefined;
162
+ },
163
+ onSwitch,
164
+ signal,
165
+ syncConfigReviewIdentity(config),
166
+ );
167
+ if (pullClosed) return "closed";
168
+ return result.pullApplied ? "pull-attempted" : "switched";
169
+ } catch (error) {
170
+ if (signal?.aborted) return false;
171
+ ctx.ui.notify(
172
+ `Sync setup “${safeTerminalText(name)}” was not switched: ${safeTerminalText(errorMessage(error))}`,
173
+ "error",
174
+ );
175
+ return false;
176
+ }
181
177
  }