@narumitw/pi-sync 0.49.14 → 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 +134 -241
  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-RGJ6SOYR.ts → chunk-AUSDLD6F.ts} +4 -4
  9. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  10. package/dist/chunks/{chunk-3NDD5QPM.ts → chunk-AXXS2BPA.ts} +9 -9
  11. package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
  12. package/dist/chunks/{chunk-APZVBSM6.ts → chunk-C7XY5NF5.ts} +214 -365
  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-LJDSHNS3.ts → chunk-GWMHAQG4.ts} +4 -4
  21. package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
  22. package/dist/chunks/{chunk-FR6CVXAL.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-4F3W4CSC.ts → chunk-LCRI3ABW.ts} +22 -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-EWX3TPLQ.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-BHJUISB4.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-LONPFFNP.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-5WJNWFTG.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-4UABOP44.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-3QZGO2EF.ts → s3-backend-3P33LQHU.ts} +96 -18
  69. package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
  70. package/dist/chunks/{setup-switch-X3W3FALZ.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-WBSV4MQG.ts → snapshot-HKEWCVK6.ts} +7 -6
  74. package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
  75. package/dist/chunks/{sync-operations-MPAFVVHA.ts → sync-operations-ONJ27QRL.ts} +381 -299
  76. package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
  77. package/dist/chunks/{sync-state-36H7ULG2.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 +211 -0
  84. package/package.json +6 -8
  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} +40 -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} +41 -330
  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 -284
  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} +42 -11
  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-3NDD5QPM.ts.map +0 -7
  164. package/dist/chunks/chunk-4F3W4CSC.ts.map +0 -7
  165. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  166. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  167. package/dist/chunks/chunk-5WJNWFTG.ts.map +0 -7
  168. package/dist/chunks/chunk-APZVBSM6.ts.map +0 -7
  169. package/dist/chunks/chunk-BHJUISB4.ts.map +0 -7
  170. package/dist/chunks/chunk-EWX3TPLQ.ts.map +0 -7
  171. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  172. package/dist/chunks/chunk-FR6CVXAL.ts.map +0 -7
  173. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  174. package/dist/chunks/chunk-JNOOSPEW.ts +0 -82
  175. package/dist/chunks/chunk-JNOOSPEW.ts.map +0 -7
  176. package/dist/chunks/chunk-LJDSHNS3.ts.map +0 -7
  177. package/dist/chunks/chunk-LONPFFNP.ts.map +0 -7
  178. package/dist/chunks/chunk-RGJ6SOYR.ts.map +0 -7
  179. package/dist/chunks/chunk-W64NEHXT.ts +0 -167
  180. package/dist/chunks/chunk-W64NEHXT.ts.map +0 -7
  181. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  182. package/dist/chunks/file-selection-4UABOP44.ts.map +0 -7
  183. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  184. package/dist/chunks/manager-result-dispatcher-RNRQA3PW.ts +0 -18
  185. package/dist/chunks/manager-ui-QO2P4V3Z.ts +0 -2757
  186. package/dist/chunks/manager-ui-QO2P4V3Z.ts.map +0 -7
  187. package/dist/chunks/remote-selection-ui-LHSAGKGA.ts +0 -17
  188. package/dist/chunks/s3-backend-3QZGO2EF.ts.map +0 -7
  189. package/dist/chunks/sync-operations-MPAFVVHA.ts.map +0 -7
  190. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  191. package/src/config.ts +0 -910
  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-RNRQA3PW.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
  197. /package/dist/chunks/{remote-selection-ui-LHSAGKGA.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
  198. /package/dist/chunks/{setup-switch-X3W3FALZ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  199. /package/dist/chunks/{snapshot-WBSV4MQG.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  200. /package/dist/chunks/{sync-state-36H7ULG2.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,330 @@
1
+ import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { loadConfig, sessionTokenWarnings, syncSessionsWarnings } from "../settings/config.js";
3
+ import {
4
+ activeLocalConfigPath,
5
+ createLocalConfigDocument,
6
+ localConfigPath,
7
+ } from "../settings/config-file.js";
8
+ import { localConfigTemplate, readLocalConfigObject } from "../settings/settings-store.js";
9
+ import type { AnySyncConfig } from "../settings/settings-types.js";
10
+ import { unlock, withLock } from "../state/lock.js";
11
+ import {
12
+ migrateLegacyStateDirectory,
13
+ stateDirectoryMigrationNotice,
14
+ } from "../state/state-directory.js";
15
+ import { throwIfAborted } from "../sync/signals.js";
16
+ import { syncErrorGuidance } from "../sync/sync-error-guidance.js";
17
+ import { isSyncDecisionRequiredError, SetupPullRequiresUiError } from "../sync/sync-errors.js";
18
+ import type { SyncLoaders } from "../sync/sync-loaders.js";
19
+ import { RemoteSelectionMismatchError } from "../sync/sync-policy.js";
20
+ import { automaticSyncSummary } from "../ui/automatic-sync-summary.js";
21
+ import type { RunRouteResult } from "../ui/cancellable-operation.js";
22
+ import {
23
+ parseOptions,
24
+ resolveSyncCommand,
25
+ splitArgs,
26
+ usage,
27
+ validateCommandOptions,
28
+ } from "./command.js";
29
+ import type { CommandOptions } from "./command-types.js";
30
+
31
+ const STATUS_KEY = "sync";
32
+
33
+ export async function executeRecoveryCommand(
34
+ rawArgs: string,
35
+ ctx: ExtensionContext,
36
+ signal: AbortSignal | undefined,
37
+ loaders: SyncLoaders,
38
+ onCommit?: () => void,
39
+ setup?: string,
40
+ ): 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
+ }
77
+ }
78
+
79
+ export async function executeCommand(
80
+ rawArgs: string,
81
+ ctx: ExtensionCommandContext,
82
+ signal: AbortSignal | undefined,
83
+ loaders: SyncLoaders,
84
+ onCommit?: () => void,
85
+ setup?: string,
86
+ ): 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);
96
+
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
+ }
202
+ }
203
+
204
+ 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");
229
+ }
230
+
231
+ 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
+ }
237
+
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
+ );
249
+ }
250
+
251
+ 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
+ );
273
+ }
274
+
275
+ 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
+ }
302
+ }
303
+
304
+ 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
+ }
316
+ }
317
+
318
+ 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
+ }
330
+ }
@@ -0,0 +1,201 @@
1
+ import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { loadConfig } from "../settings/config.js";
3
+ import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-store.js";
4
+ import { combineSignals } from "../sync/signals.js";
5
+ import { errorMessage } from "../sync/sync-errors.js";
6
+ import type { SyncLoaders } from "../sync/sync-loaders.js";
7
+ import { formatRemoteSelectionMismatch } from "../sync/sync-policy.js";
8
+ import type { RunRouteResult } from "../ui/cancellable-operation.js";
9
+ import {
10
+ observationMatchesConfig,
11
+ type SyncAttentionController,
12
+ type SyncAttentionOrigin,
13
+ syncAttentionMatchesConfig,
14
+ } from "../ui/sync-attention.js";
15
+ import { parseOptions, splitArgs } from "./command.js";
16
+ import { executeCommand, executeRecoveryCommand } from "./command-execution.js";
17
+
18
+ const STATUS_KEY = "sync";
19
+
20
+ export async function handleCommand(
21
+ rawArgs: string,
22
+ ctx: ExtensionCommandContext,
23
+ sessionSignal: AbortSignal,
24
+ loaders: SyncLoaders,
25
+ attention: SyncAttentionController,
26
+ ) {
27
+ await reconcileObservation(attention, sessionSignal);
28
+ if (sessionSignal.aborted) return;
29
+ const run = (route: string, signal?: AbortSignal, onCommit?: () => void, target?: string) =>
30
+ executeCommand(
31
+ route,
32
+ ctx,
33
+ combineSignals(sessionSignal, signal),
34
+ loaders,
35
+ observationCommitCallback(attention, sessionSignal, onCommit),
36
+ target,
37
+ );
38
+ if (!rawArgs.trim()) {
39
+ try {
40
+ const { showSyncManager } = await import("../ui/manager-ui.js");
41
+ if (sessionSignal.aborted) return;
42
+ await showSyncManager(ctx, run, sessionSignal, {
43
+ getAttention: () => attention.current(),
44
+ getObservation: () => attention.observation(),
45
+ onObservationInvalidated: () => attention.clearObservation(),
46
+ onSelectionResolved: (expected) => {
47
+ if (attention.current() === expected) attention.clear(ctx);
48
+ },
49
+ });
50
+ } catch (error) {
51
+ if (sessionSignal.aborted) return;
52
+ ctx.ui.setStatus(STATUS_KEY, undefined);
53
+ ctx.ui.notify(errorMessage(error), "error");
54
+ }
55
+ await reconcileObservation(attention, sessionSignal);
56
+ if (!sessionSignal.aborted) attention.publish(ctx);
57
+ return;
58
+ }
59
+ const result = await run(rawArgs);
60
+ if (result.kind === "decision-required") {
61
+ ctx.ui.notify(result.decision.directMessage, "error");
62
+ } else if (result.kind === "remote-selection-required") {
63
+ const origin = directSelectionOrigin(rawArgs);
64
+ if (origin) attention.set(result.decision, origin);
65
+ const deterministic = splitArgs(rawArgs).some((arg) => arg === "--yes" || arg === "-y");
66
+ if (origin && ctx.mode === "tui" && !deterministic) {
67
+ await resolveSelectionAttention(ctx, attention, sessionSignal, loaders);
68
+ } else {
69
+ ctx.ui.notify(
70
+ formatRemoteSelectionMismatch(
71
+ result.decision.setupName,
72
+ result.decision.localInclude,
73
+ result.decision.remoteInclude,
74
+ ),
75
+ "error",
76
+ );
77
+ }
78
+ }
79
+ await clearAttentionAfterCompletedOperation(rawArgs, result, ctx, attention, sessionSignal);
80
+ await reconcileSelectionAttention(ctx, attention, sessionSignal);
81
+ await reconcileObservation(attention, sessionSignal);
82
+ if (!sessionSignal.aborted) attention.publish(ctx);
83
+ }
84
+
85
+ function observationCommitCallback(
86
+ attention: SyncAttentionController,
87
+ sessionSignal: AbortSignal,
88
+ onCommit?: () => void,
89
+ ) {
90
+ const observed = attention.observation();
91
+ return () => {
92
+ // Opening or cancelling a review changes nothing. A commit may change data
93
+ // even if later publication/baseline cleanup fails; never restore old hints.
94
+ if (!sessionSignal.aborted && attention.observation() === observed)
95
+ attention.clearObservation();
96
+ onCommit?.();
97
+ };
98
+ }
99
+
100
+ async function reconcileObservation(attention: SyncAttentionController, signal: AbortSignal) {
101
+ const observed = attention.observation();
102
+ if (!observed || signal.aborted) return;
103
+ try {
104
+ const config = await loadConfig();
105
+ if (signal.aborted || attention.observation() !== observed) return;
106
+ if (!observationMatchesConfig(observed, config)) {
107
+ attention.clearObservation();
108
+ return;
109
+ }
110
+ const state = await readStateForConfig(config);
111
+ if (signal.aborted || attention.observation() !== observed) return;
112
+ if (syncStateFingerprint(state) !== observed.inspection.stateIdentity)
113
+ attention.clearObservation();
114
+ } catch {
115
+ if (!signal.aborted && attention.observation() === observed) attention.clearObservation();
116
+ }
117
+ }
118
+
119
+ async function clearAttentionAfterCompletedOperation(
120
+ rawArgs: string,
121
+ result: RunRouteResult,
122
+ ctx: ExtensionContext,
123
+ attention: SyncAttentionController,
124
+ signal: AbortSignal,
125
+ ) {
126
+ if (result.kind !== "completed" || result.outcome === "cancelled" || signal.aborted) return;
127
+ const [command, ...rest] = splitArgs(rawArgs);
128
+ if (command !== "sync" && command !== "pull" && command !== "push") return;
129
+ const current = attention.current();
130
+ if (!current) return;
131
+ try {
132
+ const options = parseOptions(rest);
133
+ const setupName = options.setup ?? (await loadConfig()).setupName;
134
+ if (signal.aborted || attention.current() !== current) return;
135
+ if (current.decision.setupName === setupName) attention.clear(ctx);
136
+ } catch {
137
+ // Attention reconciliation below owns malformed or concurrently changed settings.
138
+ }
139
+ }
140
+
141
+ async function reconcileSelectionAttention(
142
+ ctx: ExtensionContext,
143
+ attention: SyncAttentionController,
144
+ signal: AbortSignal,
145
+ ) {
146
+ const current = attention.current();
147
+ if (!current || signal.aborted) return;
148
+ try {
149
+ const config = await loadConfig(current.decision.setupName);
150
+ if (signal.aborted || attention.current() !== current) return;
151
+ if (!syncAttentionMatchesConfig(current, config)) attention.clear(ctx);
152
+ } catch {
153
+ if (!signal.aborted && attention.current() === current) attention.clear(ctx);
154
+ }
155
+ }
156
+
157
+ function directSelectionOrigin(rawArgs: string): SyncAttentionOrigin | undefined {
158
+ const command = splitArgs(rawArgs)[0];
159
+ return command === "sync" || command === "pull" || command === "push" ? command : undefined;
160
+ }
161
+
162
+ export async function resolveSelectionAttention(
163
+ ctx: ExtensionContext,
164
+ attention: SyncAttentionController,
165
+ signal: AbortSignal,
166
+ loaders: SyncLoaders,
167
+ options: {
168
+ cancelLabel?: string;
169
+ withStateAccess?: <T>(task: () => Promise<T>) => Promise<T>;
170
+ } = {},
171
+ ) {
172
+ const current = attention.current();
173
+ if (!current || signal.aborted) return;
174
+ const { dispatchManagerResult } = await import("../ui/manager-result-dispatcher.js");
175
+ if (signal.aborted || attention.current() !== current) return;
176
+ await dispatchManagerResult(
177
+ ctx,
178
+ { kind: "remote-selection-required", decision: current.decision },
179
+ current.origin,
180
+ (route, actionSignal, onCommit, target) => {
181
+ const execute = () =>
182
+ executeRecoveryCommand(
183
+ route,
184
+ ctx,
185
+ combineSignals(signal, actionSignal),
186
+ loaders,
187
+ observationCommitCallback(attention, signal, onCommit),
188
+ target,
189
+ );
190
+ return options.withStateAccess ? options.withStateAccess(execute) : execute();
191
+ },
192
+ signal,
193
+ {
194
+ cancelLabel: options.cancelLabel,
195
+ withStateAccess: options.withStateAccess,
196
+ onSelectionResolved: () => {
197
+ if (attention.current() === current) attention.clear(ctx);
198
+ },
199
+ },
200
+ );
201
+ }
@@ -0,0 +1,18 @@
1
+ export interface CommandOptions {
2
+ yes: boolean;
3
+ force: boolean;
4
+ stale: boolean;
5
+ silent: boolean;
6
+ reload: boolean;
7
+ auto: boolean;
8
+ setup?: string;
9
+ signal?: AbortSignal;
10
+ onCommit?: () => void;
11
+ args: string[];
12
+ }
13
+
14
+ export interface CommandArgumentCompletion {
15
+ value: string;
16
+ label: string;
17
+ description?: string;
18
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import type { CommandArgumentCompletion, CommandOptions } from "./types.js";
2
+ import type { CommandArgumentCompletion, CommandOptions } from "./command-types.js";
3
3
 
4
4
  const YES_FLAG_COMPLETIONS: readonly CommandArgumentCompletion[] = [
5
5
  { value: "--yes", label: "--yes", description: "Skip confirmation prompts" },
@@ -235,6 +235,7 @@ export function usage() {
235
235
  return [
236
236
  "Usage: /sync <command>",
237
237
  `Commands: ${commands}`,
238
+ "Automatic sync: background startup checks only; use /sync to review and transfer. At shutdown, selected content is pushed automatically only when sessions are included. Startup checks run in TUI/RPC, not print/JSON.",
238
239
  "Settings: use /sync init or edit storage connections and sync setups in ~/.pi/agent/pi-sync.json (or the configured Pi agent directory). Version 1 and version 2 settings are unsupported and are never rewritten.",
239
240
  ].join("\n");
240
241
  }
@@ -0,0 +1,6 @@
1
+ import { configuredSyncSetupNames } from "../settings/settings-store.js";
2
+ import { setSyncSetupCompletions } from "./command.js";
3
+
4
+ export async function refreshTargetCompletions() {
5
+ setSyncSetupCompletions(await configuredSyncSetupNames());
6
+ }