@narumitw/pi-sync 0.50.2 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-4CHTZMEO.ts → chunk-7KHKWL3Q.ts} +37 -55
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-G4RNQQZS.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-KW7YQEZN.ts → chunk-UA7RXOIT.ts} +48 -100
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-5KLKVMRC.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-IUB6ESEU.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +92 -112
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +198 -214
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-4CHTZMEO.ts.map +0 -7
  176. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  177. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  178. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  179. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  180. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  181. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  182. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  183. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-G4RNQQZS.ts.map +0 -7
  186. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  187. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  188. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  189. package/dist/chunks/chunk-KW7YQEZN.ts.map +0 -7
  190. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  191. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  192. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  193. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  194. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  195. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  196. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  197. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  198. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  199. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  200. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-FKIBM3GO.ts +0 -22
  206. package/dist/chunks/manager-ui-5KLKVMRC.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-D7RBRJNL.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-IUB6ESEU.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-FKIBM3GO.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-D7RBRJNL.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -1,17 +1,10 @@
1
1
  import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { loadConfig, sessionTokenWarnings, syncSessionsWarnings } from "../settings/config.js";
3
- import {
4
- activeLocalConfigPath,
5
- createLocalConfigDocument,
6
- localConfigPath,
7
- } from "../settings/config-file.js";
3
+ import { activeLocalConfigPath, createLocalConfigDocument, localConfigPath } from "../settings/config-file.js";
8
4
  import { localConfigTemplate, readLocalConfigObject } from "../settings/settings-store.js";
9
5
  import type { AnySyncConfig } from "../settings/settings-types.js";
10
6
  import { unlock, withLock } from "../state/lock.js";
11
- import {
12
- migrateLegacyStateDirectory,
13
- stateDirectoryMigrationNotice,
14
- } from "../state/state-directory.js";
7
+ import { migrateLegacyStateDirectory, stateDirectoryMigrationNotice } from "../state/state-directory.js";
15
8
  import { throwIfAborted } from "../sync/signals.js";
16
9
  import { syncErrorGuidance } from "../sync/sync-error-guidance.js";
17
10
  import { isSyncDecisionRequiredError, SetupPullRequiresUiError } from "../sync/sync-errors.js";
@@ -19,312 +12,300 @@ import type { SyncLoaders } from "../sync/sync-loaders.js";
19
12
  import { RemoteSelectionMismatchError } from "../sync/sync-policy.js";
20
13
  import { automaticSyncSummary } from "../ui/automatic-sync-summary.js";
21
14
  import type { RunRouteResult } from "../ui/cancellable-operation.js";
22
- import {
23
- parseOptions,
24
- resolveSyncCommand,
25
- splitArgs,
26
- usage,
27
- validateCommandOptions,
28
- } from "./command.js";
15
+ import { setSyncStatus } from "../ui/sync-status.js";
16
+ import { parseOptions, resolveSyncCommand, splitArgs, usage, validateCommandOptions } from "./command.js";
29
17
  import type { CommandOptions } from "./command-types.js";
30
18
 
31
- const STATUS_KEY = "sync";
32
-
33
19
  export async function executeRecoveryCommand(
34
- rawArgs: string,
35
- ctx: ExtensionContext,
36
- signal: AbortSignal | undefined,
37
- loaders: SyncLoaders,
38
- onCommit?: () => void,
39
- setup?: string,
20
+ rawArgs: string,
21
+ ctx: ExtensionContext,
22
+ signal: AbortSignal | undefined,
23
+ loaders: SyncLoaders,
24
+ onCommit?: () => void,
25
+ setup?: string,
40
26
  ): Promise<RunRouteResult> {
41
- try {
42
- const [subcommand, ...rest] = splitArgs(rawArgs);
43
- if (subcommand !== "sync" && subcommand !== "pull" && subcommand !== "push") {
44
- throw new Error(`Unsupported sync recovery route: ${subcommand ?? "missing"}`);
45
- }
46
- const options = parseOptions(rest);
47
- if (setup !== undefined) options.setup = setup;
48
- if (signal) options.signal = signal;
49
- if (onCommit) options.onCommit = onCommit;
50
- options.reload = false;
51
- options.auto = false;
52
- validateCommandOptions(subcommand, options);
53
- const operations = await loaders.operations();
54
- throwIfAborted(options.signal);
55
- if (subcommand === "push") {
56
- const outcome = await withLock("push", () => operations.push(ctx, options));
57
- return { kind: "completed", ...(outcome ? { outcome } : {}) };
58
- }
59
- if (subcommand === "pull") {
60
- const outcome = await withLock("pull", () => operations.pull(ctx, options));
61
- return { kind: "completed", ...(outcome ? { outcome } : {}) };
62
- }
63
- await withLock("sync", () => operations.syncBoth(ctx, options));
64
- return { kind: "completed" };
65
- } catch (error) {
66
- if (signal?.aborted) return { kind: "failed" };
67
- ctx.ui.setStatus(STATUS_KEY, undefined);
68
- if (error instanceof RemoteSelectionMismatchError) {
69
- return { kind: "remote-selection-required", decision: error.decision };
70
- }
71
- if (isSyncDecisionRequiredError(error)) {
72
- return { kind: "decision-required", decision: error.decision };
73
- }
74
- ctx.ui.notify(syncErrorGuidance(error), "error");
75
- return { kind: "failed" };
76
- }
27
+ try {
28
+ const [subcommand, ...rest] = splitArgs(rawArgs);
29
+ if (subcommand !== "sync" && subcommand !== "pull" && subcommand !== "push") {
30
+ throw new Error(`Unsupported sync recovery route: ${subcommand ?? "missing"}`);
31
+ }
32
+ const options = parseOptions(rest);
33
+ if (setup !== undefined) options.setup = setup;
34
+ if (signal) options.signal = signal;
35
+ if (onCommit) options.onCommit = onCommit;
36
+ options.reload = false;
37
+ options.auto = false;
38
+ validateCommandOptions(subcommand, options);
39
+ const operations = await loaders.operations();
40
+ throwIfAborted(options.signal);
41
+ if (subcommand === "push") {
42
+ const outcome = await withLock("push", () => operations.push(ctx, options));
43
+ return { kind: "completed", ...(outcome ? { outcome } : {}) };
44
+ }
45
+ if (subcommand === "pull") {
46
+ const outcome = await withLock("pull", () => operations.pull(ctx, options));
47
+ return { kind: "completed", ...(outcome ? { outcome } : {}) };
48
+ }
49
+ await withLock("sync", () => operations.syncBoth(ctx, options));
50
+ return { kind: "completed" };
51
+ } catch (error) {
52
+ if (signal?.aborted) return { kind: "failed" };
53
+ setSyncStatus(ctx, undefined);
54
+ if (error instanceof RemoteSelectionMismatchError) {
55
+ return { kind: "remote-selection-required", decision: error.decision };
56
+ }
57
+ if (isSyncDecisionRequiredError(error)) {
58
+ return { kind: "decision-required", decision: error.decision };
59
+ }
60
+ ctx.ui.notify(syncErrorGuidance(error), "error");
61
+ return { kind: "failed" };
62
+ }
77
63
  }
78
64
 
79
65
  export async function executeCommand(
80
- rawArgs: string,
81
- ctx: ExtensionCommandContext,
82
- signal: AbortSignal | undefined,
83
- loaders: SyncLoaders,
84
- onCommit?: () => void,
85
- setup?: string,
66
+ rawArgs: string,
67
+ ctx: ExtensionCommandContext,
68
+ signal: AbortSignal | undefined,
69
+ loaders: SyncLoaders,
70
+ onCommit?: () => void,
71
+ setup?: string,
86
72
  ): Promise<RunRouteResult> {
87
- try {
88
- const command = await resolveSyncCommand(rawArgs, ctx);
89
- if (signal?.aborted || !command) return { kind: "completed" };
90
- const { subcommand, rest } = command;
91
- const options = parseOptions(rest);
92
- if (setup !== undefined) options.setup = setup;
93
- if (signal) options.signal = signal;
94
- if (onCommit) options.onCommit = onCommit;
95
- validateCommandOptions(subcommand, options);
73
+ try {
74
+ const command = await resolveSyncCommand(rawArgs, ctx);
75
+ if (signal?.aborted || !command) return { kind: "completed" };
76
+ const { subcommand, rest } = command;
77
+ const options = parseOptions(rest);
78
+ if (setup !== undefined) options.setup = setup;
79
+ if (signal) options.signal = signal;
80
+ if (onCommit) options.onCommit = onCommit;
81
+ validateCommandOptions(subcommand, options);
96
82
 
97
- switch (subcommand) {
98
- case "help":
99
- ctx.ui.notify(usage(), "info");
100
- return { kind: "completed" };
101
- case "use": {
102
- const { useSyncSetup } = await loaders.setupSwitch();
103
- throwIfAborted(options.signal);
104
- await useSyncSetup(
105
- ctx,
106
- options.args[0] ?? "",
107
- async (selectedSetup) => {
108
- const operations = await loaders.operations();
109
- throwIfAborted(options.signal);
110
- return withLock("pull", () =>
111
- operations.pull(ctx, { ...options, setup: selectedSetup }),
112
- );
113
- },
114
- undefined,
115
- options.signal,
116
- );
117
- return { kind: "completed" };
118
- }
119
- case "init":
120
- await initConfig(ctx, signal);
121
- return { kind: "completed" };
122
- case "config":
123
- await showConfig(ctx, options);
124
- return { kind: "completed" };
125
- case "files": {
126
- const { showFileSelection } = await import("../ui/file-selection.js");
127
- throwIfAborted(options.signal);
128
- await showFileSelection(ctx, options.setup, options.signal);
129
- return { kind: "completed" };
130
- }
131
- case "status": {
132
- const operations = await loaders.operations();
133
- throwIfAborted(options.signal);
134
- await operations.status(ctx, options);
135
- return { kind: "completed" };
136
- }
137
- case "diff": {
138
- const operations = await loaders.operations();
139
- throwIfAborted(options.signal);
140
- await operations.diff(ctx, options);
141
- return { kind: "completed" };
142
- }
143
- case "doctor": {
144
- const operations = await loaders.operations();
145
- throwIfAborted(options.signal);
146
- await operations.doctor(ctx, options);
147
- return { kind: "completed" };
148
- }
149
- case "push": {
150
- const operations = await loaders.operations();
151
- throwIfAborted(options.signal);
152
- const outcome = await withLock("push", () => operations.push(ctx, options));
153
- return { kind: "completed", ...(outcome ? { outcome } : {}) };
154
- }
155
- case "pull": {
156
- const operations = await loaders.operations();
157
- throwIfAborted(options.signal);
158
- const outcome = await withLock("pull", () => operations.pull(ctx, options));
159
- return { kind: "completed", ...(outcome ? { outcome } : {}) };
160
- }
161
- case "sync": {
162
- const operations = await loaders.operations();
163
- throwIfAborted(options.signal);
164
- await withLock("sync", () => operations.syncBoth(ctx, options));
165
- return { kind: "completed" };
166
- }
167
- case "history": {
168
- const operations = await loaders.operations();
169
- throwIfAborted(options.signal);
170
- await operations.history(ctx, options);
171
- return { kind: "completed" };
172
- }
173
- case "rollback": {
174
- const operations = await loaders.operations();
175
- throwIfAborted(options.signal);
176
- await withLock("rollback", () => operations.rollback(ctx, options));
177
- return { kind: "completed" };
178
- }
179
- case "migrate-state":
180
- await migrateStateDirectory(ctx, options);
181
- return { kind: "completed" };
182
- case "unlock":
183
- await unlock(ctx, options);
184
- return { kind: "completed" };
185
- default:
186
- ctx.ui.notify(`Unknown /sync command: ${subcommand}\n\n${usage()}`, "warning");
187
- return { kind: "failed" };
188
- }
189
- } catch (error) {
190
- if (signal?.aborted) return { kind: "failed" };
191
- ctx.ui.setStatus(STATUS_KEY, undefined);
192
- if (error instanceof SetupPullRequiresUiError) throw error;
193
- if (error instanceof RemoteSelectionMismatchError) {
194
- return { kind: "remote-selection-required", decision: error.decision };
195
- }
196
- if (isSyncDecisionRequiredError(error)) {
197
- return { kind: "decision-required", decision: error.decision };
198
- }
199
- ctx.ui.notify(syncErrorGuidance(error), "error");
200
- return { kind: "failed" };
201
- }
83
+ switch (subcommand) {
84
+ case "help":
85
+ ctx.ui.notify(usage(), "info");
86
+ return { kind: "completed" };
87
+ case "use": {
88
+ const { useSyncSetup } = await loaders.setupSwitch();
89
+ throwIfAborted(options.signal);
90
+ await useSyncSetup(
91
+ ctx,
92
+ options.args[0] ?? "",
93
+ async (selectedSetup) => {
94
+ const operations = await loaders.operations();
95
+ throwIfAborted(options.signal);
96
+ return withLock("pull", () => operations.pull(ctx, { ...options, setup: selectedSetup }));
97
+ },
98
+ undefined,
99
+ options.signal,
100
+ );
101
+ return { kind: "completed" };
102
+ }
103
+ case "init":
104
+ await initConfig(ctx, signal);
105
+ return { kind: "completed" };
106
+ case "config":
107
+ await showConfig(ctx, options);
108
+ return { kind: "completed" };
109
+ case "files": {
110
+ const { showFileSelection } = await import("../ui/file-selection.js");
111
+ throwIfAborted(options.signal);
112
+ await showFileSelection(ctx, options.setup, options.signal);
113
+ return { kind: "completed" };
114
+ }
115
+ case "status": {
116
+ const operations = await loaders.operations();
117
+ throwIfAborted(options.signal);
118
+ await operations.status(ctx, options);
119
+ return { kind: "completed" };
120
+ }
121
+ case "diff": {
122
+ const operations = await loaders.operations();
123
+ throwIfAborted(options.signal);
124
+ await operations.diff(ctx, options);
125
+ return { kind: "completed" };
126
+ }
127
+ case "doctor": {
128
+ const operations = await loaders.operations();
129
+ throwIfAborted(options.signal);
130
+ await operations.doctor(ctx, options);
131
+ return { kind: "completed" };
132
+ }
133
+ case "push": {
134
+ const operations = await loaders.operations();
135
+ throwIfAborted(options.signal);
136
+ const outcome = await withLock("push", () => operations.push(ctx, options));
137
+ return { kind: "completed", ...(outcome ? { outcome } : {}) };
138
+ }
139
+ case "pull": {
140
+ const operations = await loaders.operations();
141
+ throwIfAborted(options.signal);
142
+ const outcome = await withLock("pull", () => operations.pull(ctx, options));
143
+ return { kind: "completed", ...(outcome ? { outcome } : {}) };
144
+ }
145
+ case "sync": {
146
+ const operations = await loaders.operations();
147
+ throwIfAborted(options.signal);
148
+ await withLock("sync", () => operations.syncBoth(ctx, options));
149
+ return { kind: "completed" };
150
+ }
151
+ case "history": {
152
+ const operations = await loaders.operations();
153
+ throwIfAborted(options.signal);
154
+ await operations.history(ctx, options);
155
+ return { kind: "completed" };
156
+ }
157
+ case "rollback": {
158
+ const operations = await loaders.operations();
159
+ throwIfAborted(options.signal);
160
+ await withLock("rollback", () => operations.rollback(ctx, options));
161
+ return { kind: "completed" };
162
+ }
163
+ case "migrate-state":
164
+ await migrateStateDirectory(ctx, options);
165
+ return { kind: "completed" };
166
+ case "unlock":
167
+ await unlock(ctx, options);
168
+ return { kind: "completed" };
169
+ default:
170
+ ctx.ui.notify(`Unknown /sync command: ${subcommand}\n\n${usage()}`, "warning");
171
+ return { kind: "failed" };
172
+ }
173
+ } catch (error) {
174
+ if (signal?.aborted) return { kind: "failed" };
175
+ setSyncStatus(ctx, undefined);
176
+ if (error instanceof SetupPullRequiresUiError) throw error;
177
+ if (error instanceof RemoteSelectionMismatchError) {
178
+ return { kind: "remote-selection-required", decision: error.decision };
179
+ }
180
+ if (isSyncDecisionRequiredError(error)) {
181
+ return { kind: "decision-required", decision: error.decision };
182
+ }
183
+ ctx.ui.notify(syncErrorGuidance(error), "error");
184
+ return { kind: "failed" };
185
+ }
202
186
  }
203
187
 
204
188
  async function migrateStateDirectory(ctx: ExtensionCommandContext, options: CommandOptions) {
205
- const notice = stateDirectoryMigrationNotice();
206
- if (!notice) {
207
- ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info");
208
- return;
209
- }
210
- if (
211
- !options.yes &&
212
- !(await ctx.ui.confirm(
213
- "Migrate pi-sync state directory",
214
- "Confirm that every other Pi process is closed. pi-sync will atomically rename .pisync/ to pi-sync/ without merging or deleting either root.",
215
- { signal: options.signal },
216
- ))
217
- ) {
218
- ctx.ui.notify("pi-sync state migration cancelled.", "info");
219
- return;
220
- }
221
- throwIfAborted(options.signal);
222
- const result = await migrateLegacyStateDirectory();
223
- throwIfAborted(options.signal);
224
- if (result.status === "ready") {
225
- ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info");
226
- return;
227
- }
228
- ctx.ui.notify(result.message, result.status === "migrated" ? "info" : "warning");
189
+ const notice = stateDirectoryMigrationNotice();
190
+ if (!notice) {
191
+ ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info");
192
+ return;
193
+ }
194
+ if (
195
+ !options.yes &&
196
+ !(await ctx.ui.confirm(
197
+ "Migrate pi-sync state directory",
198
+ "Confirm that every other Pi process is closed. pi-sync will atomically rename .pisync/ to pi-sync/ without merging or deleting either root.",
199
+ { signal: options.signal },
200
+ ))
201
+ ) {
202
+ ctx.ui.notify("pi-sync state migration cancelled.", "info");
203
+ return;
204
+ }
205
+ throwIfAborted(options.signal);
206
+ const result = await migrateLegacyStateDirectory();
207
+ throwIfAborted(options.signal);
208
+ if (result.status === "ready") {
209
+ ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info");
210
+ return;
211
+ }
212
+ ctx.ui.notify(result.message, result.status === "migrated" ? "info" : "warning");
229
213
  }
230
214
 
231
215
  async function initConfig(ctx: ExtensionCommandContext, signal?: AbortSignal) {
232
- const configPath = localConfigPath();
233
- if (await readLocalConfigObject()) {
234
- ctx.ui.notify(`Config already exists: ${await activeLocalConfigPath()}`, "info");
235
- return;
236
- }
216
+ const configPath = localConfigPath();
217
+ if (await readLocalConfigObject()) {
218
+ ctx.ui.notify(`Config already exists: ${await activeLocalConfigPath()}`, "info");
219
+ return;
220
+ }
237
221
 
238
- if (ctx.mode === "tui") {
239
- const { showSetupWizard } = await import("../ui/setup/setup-wizard.js");
240
- throwIfAborted(signal);
241
- await showSetupWizard(ctx, signal);
242
- return;
243
- }
244
- await createLocalConfigDocument(localConfigTemplate());
245
- ctx.ui.notify(
246
- `Created ${configPath}. Add a storage connection and sync setup before syncing.`,
247
- "info",
248
- );
222
+ if (ctx.mode === "tui") {
223
+ const { showSetupWizard } = await import("../ui/setup/setup-wizard.js");
224
+ throwIfAborted(signal);
225
+ await showSetupWizard(ctx, signal);
226
+ return;
227
+ }
228
+ await createLocalConfigDocument(localConfigTemplate());
229
+ ctx.ui.notify(`Created ${configPath}. Add a storage connection and sync setup before syncing.`, "info");
249
230
  }
250
231
 
251
232
  async function showConfig(ctx: ExtensionCommandContext, options: CommandOptions) {
252
- const config = await loadConfig(options.setup);
253
- const warnings = [
254
- ...(config.backend.type === "s3" ? sessionTokenWarnings(config.backend.profile) : []),
255
- ...syncSessionsWarnings(config),
256
- ];
257
- const storageLines = configStorageLines(config);
258
- ctx.ui.notify(
259
- [
260
- "pi-sync config:",
261
- `sync setup: ${config.setupName}`,
262
- `storage connection: ${config.connectionName}`,
263
- ...storageLines,
264
- `storage path: ${config.storagePath}`,
265
- `automatic sync: ${automaticSyncSummary(config.automatic)}`,
266
- `included content: ${config.include.join(", ") || "none"}`,
267
- `sessions: ${config.include.includes("sessions") ? "included" : "not included"}`,
268
- `settings file: ${localConfigPath()}`,
269
- ...warnings,
270
- ].join("\n"),
271
- warnings.length > 0 ? "warning" : "info",
272
- );
233
+ const config = await loadConfig(options.setup);
234
+ const warnings = [
235
+ ...(config.backend.type === "s3" ? sessionTokenWarnings(config.backend.profile) : []),
236
+ ...syncSessionsWarnings(config),
237
+ ];
238
+ const storageLines = configStorageLines(config);
239
+ ctx.ui.notify(
240
+ [
241
+ "pi-sync config:",
242
+ `sync setup: ${config.setupName}`,
243
+ `storage connection: ${config.connectionName}`,
244
+ ...storageLines,
245
+ `storage path: ${config.storagePath}`,
246
+ `automatic sync: ${automaticSyncSummary(config.automatic)}`,
247
+ `included content: ${config.include.join(", ") || "none"}`,
248
+ `sessions: ${config.include.includes("sessions") ? "included" : "not included"}`,
249
+ `settings file: ${localConfigPath()}`,
250
+ ...warnings,
251
+ ].join("\n"),
252
+ warnings.length > 0 ? "warning" : "info",
253
+ );
273
254
  }
274
255
 
275
256
  function configStorageLines(config: AnySyncConfig) {
276
- switch (config.backend.type) {
277
- case "git":
278
- return [
279
- "kind: git",
280
- `remote: ${displayGitRemote(config.backend.profile.remote)}`,
281
- "authentication: existing Git/SSH credentials (not stored)",
282
- `branch: ${config.backend.destination.branch}`,
283
- ];
284
- case "webdav":
285
- return [
286
- "kind: webdav",
287
- `url: ${displayWebDavUrl(config.backend.profile.url, config.backend.profile.username)}`,
288
- "username: configured (value hidden)",
289
- "password: configured",
290
- ];
291
- case "s3":
292
- return [
293
- "kind: s3",
294
- `endpoint: ${config.backend.profile.endpoint}`,
295
- `bucket: ${config.backend.destination.bucket}`,
296
- `region: ${config.backend.profile.region}`,
297
- "access key id: configured",
298
- "secret access key: configured",
299
- `session token: ${config.backend.profile.sessionToken ? "configured" : "not configured"}`,
300
- ];
301
- }
257
+ switch (config.backend.type) {
258
+ case "git":
259
+ return [
260
+ "kind: git",
261
+ `remote: ${displayGitRemote(config.backend.profile.remote)}`,
262
+ "authentication: existing Git/SSH credentials (not stored)",
263
+ `branch: ${config.backend.destination.branch}`,
264
+ ];
265
+ case "webdav":
266
+ return [
267
+ "kind: webdav",
268
+ `url: ${displayWebDavUrl(config.backend.profile.url, config.backend.profile.username)}`,
269
+ "username: configured (value hidden)",
270
+ "password: configured",
271
+ ];
272
+ case "s3":
273
+ return [
274
+ "kind: s3",
275
+ `endpoint: ${config.backend.profile.endpoint}`,
276
+ `bucket: ${config.backend.destination.bucket}`,
277
+ `region: ${config.backend.profile.region}`,
278
+ "access key id: configured",
279
+ "secret access key: configured",
280
+ `session token: ${config.backend.profile.sessionToken ? "configured" : "not configured"}`,
281
+ ];
282
+ }
302
283
  }
303
284
 
304
285
  function displayGitRemote(value: string | undefined) {
305
- if (!value) return "missing";
306
- try {
307
- const url = new URL(value);
308
- url.username = "";
309
- url.password = "";
310
- url.search = "";
311
- url.hash = "";
312
- return url.toString();
313
- } catch {
314
- return value.replace(/^(?:[^@\s]+@)?(?<host>[^:]+):.+$/u, "$<host>:…");
315
- }
286
+ if (!value) return "missing";
287
+ try {
288
+ const url = new URL(value);
289
+ url.username = "";
290
+ url.password = "";
291
+ url.search = "";
292
+ url.hash = "";
293
+ return url.toString();
294
+ } catch {
295
+ return value.replace(/^(?:[^@\s]+@)?(?<host>[^:]+):.+$/u, "$<host>:…");
296
+ }
316
297
  }
317
298
 
318
299
  function displayWebDavUrl(value: string | undefined, username: string | undefined) {
319
- if (!value) return "missing";
320
- try {
321
- const url = new URL(value);
322
- url.username = "";
323
- url.password = "";
324
- url.search = "";
325
- url.hash = "";
326
- return username ? `${url.origin}/…` : `${url.origin}${url.pathname}`;
327
- } catch {
328
- return "invalid (value hidden)";
329
- }
300
+ if (!value) return "missing";
301
+ try {
302
+ const url = new URL(value);
303
+ url.username = "";
304
+ url.password = "";
305
+ url.search = "";
306
+ url.hash = "";
307
+ return username ? `${url.origin}/…` : `${url.origin}${url.pathname}`;
308
+ } catch {
309
+ return "invalid (value hidden)";
310
+ }
330
311
  }