@mycadet/assistant 0.12.10 → 0.13.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 (180) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  3. package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  7. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  8. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  9. package/openapi.yaml +2 -708
  10. package/package.json +2 -3
  11. package/scripts/sync-web-search-catalog.ts +1 -2
  12. package/src/__tests__/cloud-profile-guard.test.ts +41 -5
  13. package/src/__tests__/config-schema.test.ts +6 -38
  14. package/src/__tests__/conversation-queue.test.ts +45 -6
  15. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  16. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  17. package/src/__tests__/persona-resolver.test.ts +0 -51
  18. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  19. package/src/__tests__/run-due-schedules.test.ts +0 -4
  20. package/src/__tests__/schedule-store.test.ts +0 -12
  21. package/src/__tests__/stt-stream-session.test.ts +4 -4
  22. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  23. package/src/__tests__/voice-config-update.test.ts +12 -167
  24. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  25. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  26. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  27. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  28. package/src/calls/resolve-call-tts-provider.ts +6 -25
  29. package/src/calls/telephony-credential-preflight.ts +0 -10
  30. package/src/calls/telephony-tts-capability.ts +1 -17
  31. package/src/cli/commands/tts.ts +1 -2
  32. package/src/cli/index.help.ts +0 -2
  33. package/src/cli/program.ts +0 -2
  34. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  35. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  36. package/src/config/schemas/llm.ts +3 -5
  37. package/src/config/schemas/services.ts +5 -6
  38. package/src/config/schemas/stt.ts +1 -3
  39. package/src/config/schemas/tts.ts +1 -30
  40. package/src/daemon/daemon-readiness.ts +4 -29
  41. package/src/daemon/lifecycle.ts +0 -31
  42. package/src/daemon/providers-setup.ts +0 -8
  43. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  44. package/src/heartbeat/heartbeat-service.ts +1 -66
  45. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  46. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  47. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  48. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  49. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  50. package/src/live-voice/live-voice-session.ts +10 -6
  51. package/src/live-voice/live-voice-tts.ts +1 -8
  52. package/src/plugins/defaults/memory/worker.ts +3 -3
  53. package/src/prompts/persona-resolver.ts +0 -35
  54. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  55. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  56. package/src/providers/search-provider-catalog.ts +7 -39
  57. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  58. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  59. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  60. package/src/providers/speech-to-text/resolve.ts +3 -120
  61. package/src/runtime/http-router-types.ts +1 -3
  62. package/src/runtime/http-server.ts +1 -1
  63. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +13 -11
  64. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  65. package/src/runtime/routes/conversation-query-routes.ts +0 -96
  66. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  67. package/src/runtime/routes/index.ts +0 -4
  68. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  69. package/src/runtime/routes/live-voice-routes.ts +8 -11
  70. package/src/runtime/routes/secret-routes.ts +0 -6
  71. package/src/runtime/routes/tts-routes.ts +0 -38
  72. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  73. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  74. package/src/schedule/schedule-store.ts +0 -7
  75. package/src/schedule/scheduler.ts +0 -5
  76. package/src/schedule/worker.ts +1 -1
  77. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  78. package/src/stt/daemon-batch-transcriber.ts +4 -38
  79. package/src/stt/types.ts +1 -9
  80. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  81. package/src/tools/network/web-search.ts +4 -165
  82. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  83. package/src/tts/provider-catalog.ts +0 -4
  84. package/src/tts/types.ts +0 -1
  85. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  86. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  87. package/src/__tests__/migration-export-http.test.ts +0 -557
  88. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  89. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  90. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  91. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  92. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  93. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  94. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  95. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  96. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  97. package/src/__tests__/migration-transport.test.ts +0 -1089
  98. package/src/__tests__/migration-validate-http.test.ts +0 -634
  99. package/src/__tests__/migration-wizard.test.ts +0 -1395
  100. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  101. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  102. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  103. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  104. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  105. package/src/background-wake/background-wake-routes.test.ts +0 -906
  106. package/src/background-wake/cloud-client.test.ts +0 -328
  107. package/src/background-wake/cloud-client.ts +0 -196
  108. package/src/background-wake/next-wake.test.ts +0 -335
  109. package/src/background-wake/next-wake.ts +0 -188
  110. package/src/background-wake/publisher.ts +0 -93
  111. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  112. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  113. package/src/cli/commands/backup.help.ts +0 -180
  114. package/src/cli/commands/backup.ts +0 -398
  115. package/src/cloud/cloud-speech.test.ts +0 -335
  116. package/src/cloud/cloud-speech.ts +0 -300
  117. package/src/config/cloud-speech-defaults.ts +0 -300
  118. package/src/config/sanitize-for-transfer.ts +0 -111
  119. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  120. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  121. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  122. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  123. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  124. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  125. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  126. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  127. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  128. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  129. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  130. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  131. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  132. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  133. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  134. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  135. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  136. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  137. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  138. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  139. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  140. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  141. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  142. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  143. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  144. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  146. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  147. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  148. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  149. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  150. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  151. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  152. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  153. package/src/runtime/migrations/job-registry.ts +0 -297
  154. package/src/runtime/migrations/migration-transport.ts +0 -612
  155. package/src/runtime/migrations/migration-wizard.ts +0 -747
  156. package/src/runtime/migrations/origin-mode.ts +0 -42
  157. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  158. package/src/runtime/migrations/staged-import-path.ts +0 -116
  159. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  160. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  161. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  162. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  163. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  164. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  165. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  166. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  167. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  168. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  169. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  170. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  171. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  172. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  173. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  174. package/src/runtime/routes/background-wake-routes.ts +0 -387
  175. package/src/runtime/routes/migration-routes.ts +0 -2584
  176. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  177. package/src/tools/network/cloud-search-proxy.ts +0 -211
  178. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  179. package/src/tts/providers/cadet-provider.ts +0 -244
  180. package/src/types/tar-stream.d.ts +0 -66
@@ -1,747 +0,0 @@
1
- /**
2
- * Migration wizard state machine — shared module for guided migration UX.
3
- *
4
- * Drives the multi-step wizard for managed <-> self-hosted assistant
5
- * migrations. Usable from both macOS and iOS web views via WKWebView.
6
- *
7
- * Steps:
8
- * select-direction → upload-bundle → validate → preflight-review →
9
- * transfer → rebind-secrets → complete
10
- *
11
- * Each step has a status (idle / loading / success / error) with error
12
- * details and retry capability. The entire wizard state is serializable
13
- * for persistence/resume across app restarts.
14
- */
15
-
16
- import { z } from "zod";
17
-
18
- import type {
19
- ExportManagedResult,
20
- ImportCommitResponse,
21
- ImportPreflightResponse,
22
- TransportConfig,
23
- ValidateResponse,
24
- } from "./migration-transport.js";
25
- import {
26
- importCommit,
27
- importPreflight,
28
- MigrationTransportError,
29
- validateBundle,
30
- } from "./migration-transport.js";
31
-
32
- // ---------------------------------------------------------------------------
33
- // Types
34
- // ---------------------------------------------------------------------------
35
-
36
- export type MigrationDirection =
37
- | "managed-to-self-hosted"
38
- | "self-hosted-to-managed";
39
-
40
- export const WizardStepSchema = z.enum([
41
- "select-direction",
42
- "upload-bundle",
43
- "validate",
44
- "preflight-review",
45
- "transfer",
46
- "rebind-secrets",
47
- "complete",
48
- ]);
49
- export type WizardStep = z.infer<typeof WizardStepSchema>;
50
-
51
- const StepStatusSchema = z.enum(["idle", "loading", "success", "error"]);
52
- type StepStatus = z.infer<typeof StepStatusSchema>;
53
-
54
- const StepErrorSchema = z.object({
55
- message: z.string(),
56
- code: z.string().optional(),
57
- retryable: z.boolean(),
58
- });
59
- export type StepError = z.infer<typeof StepErrorSchema>;
60
-
61
- const StepStateSchema = z.object({
62
- status: StepStatusSchema,
63
- error: StepErrorSchema.optional(),
64
- });
65
- export type StepState = z.infer<typeof StepStateSchema>;
66
-
67
- /**
68
- * Full wizard state — serializable to JSON for persistence.
69
- *
70
- * All fields are plain data (no functions, no class instances).
71
- * Transport results that contain ArrayBuffer (non-serializable) are
72
- * intentionally excluded; the wizard tracks only the structured
73
- * metadata from each step.
74
- */
75
- export interface MigrationWizardState {
76
- currentStep: WizardStep;
77
- direction?: MigrationDirection;
78
- steps: Record<WizardStep, StepState>;
79
-
80
- /** Bundle file data is not persisted — must be re-provided on resume. */
81
- hasBundleData: boolean;
82
-
83
- /** Validation result from the validate step. */
84
- validateResult?: ValidateResponse;
85
-
86
- /** Preflight result from the preflight-review step. */
87
- preflightResult?: ImportPreflightResponse;
88
-
89
- /** Export result metadata (no binary data). */
90
- exportResult?:
91
- | ExportManagedResult
92
- | {
93
- ok: true;
94
- filename: string;
95
- schemaVersion: number;
96
- checksum: string;
97
- };
98
-
99
- /** Import commit result. */
100
- importResult?: ImportCommitResponse;
101
-
102
- /** Credential import results from the transfer step (if credentials were in the bundle). */
103
- credentialsImported?: {
104
- total: number;
105
- succeeded: number;
106
- failed: number;
107
- failedAccounts: string[];
108
- skippedPlatform?: number;
109
- };
110
-
111
- /** Timestamp of last state change (ISO 8601). */
112
- updatedAt: string;
113
-
114
- /** Timestamp of wizard creation (ISO 8601). */
115
- createdAt: string;
116
- }
117
-
118
- /**
119
- * Envelope for the persisted wizard state. The step payloads
120
- * (validateResult, preflightResult, …) are opaque here — they pass through
121
- * untouched and are the transport module's contract, validated at its own
122
- * boundary.
123
- */
124
- const WizardStateSchema = z.looseObject({
125
- currentStep: WizardStepSchema,
126
- direction: z
127
- .enum(["managed-to-self-hosted", "self-hosted-to-managed"])
128
- .optional(),
129
- steps: z.record(z.string(), StepStateSchema),
130
- hasBundleData: z.boolean(),
131
- updatedAt: z.string(),
132
- createdAt: z.string(),
133
- });
134
-
135
- // ---------------------------------------------------------------------------
136
- // Step ordering and transitions
137
- // ---------------------------------------------------------------------------
138
-
139
- const STEP_ORDER: readonly WizardStep[] = WizardStepSchema.options;
140
-
141
- const STEP_INDEX = new Map<WizardStep, number>(
142
- STEP_ORDER.map((step, i) => [step, i]),
143
- );
144
-
145
- /**
146
- * Steps that are available for a given migration direction.
147
- * Both directions use the same steps, but the semantics differ:
148
- *
149
- * - managed-to-self-hosted: export from managed, import to local runtime
150
- * - self-hosted-to-managed: export from local runtime, import to managed
151
- */
152
- function stepsForDirection(
153
- _direction: MigrationDirection,
154
- ): readonly WizardStep[] {
155
- return STEP_ORDER;
156
- }
157
-
158
- // ---------------------------------------------------------------------------
159
- // Transition validation
160
- // ---------------------------------------------------------------------------
161
-
162
- export interface TransitionResult {
163
- valid: boolean;
164
- reason?: string;
165
- }
166
-
167
- /**
168
- * Check whether advancing from `current` to `next` is a valid transition.
169
- *
170
- * Rules:
171
- * - Can advance to the immediately next step if the current step is in 'success' status.
172
- * - Can go back to any earlier step (allows re-doing steps).
173
- * - Cannot skip steps forward.
174
- * - 'complete' is a terminal step — no further transitions.
175
- */
176
- export function validateWizardTransition(
177
- state: MigrationWizardState,
178
- next: WizardStep,
179
- ): TransitionResult {
180
- const current = state.currentStep;
181
- if (current === next) {
182
- return { valid: true };
183
- }
184
-
185
- if (current === "complete") {
186
- return {
187
- valid: false,
188
- reason: 'Cannot transition from terminal step "complete"',
189
- };
190
- }
191
-
192
- const currentIdx = STEP_INDEX.get(current)!;
193
- const nextIdx = STEP_INDEX.get(next)!;
194
-
195
- // Going backward is always allowed
196
- if (nextIdx < currentIdx) {
197
- return { valid: true };
198
- }
199
-
200
- // Going forward: can only advance one step at a time, and current must be 'success'
201
- if (nextIdx > currentIdx + 1) {
202
- return {
203
- valid: false,
204
- reason: `Cannot skip from "${current}" to "${next}"`,
205
- };
206
- }
207
-
208
- // nextIdx === currentIdx + 1
209
- if (state.steps[current].status !== "success") {
210
- return {
211
- valid: false,
212
- reason: `Cannot advance from "${current}" — step status is "${state.steps[current].status}", expected "success"`,
213
- };
214
- }
215
-
216
- return { valid: true };
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // State factory
221
- // ---------------------------------------------------------------------------
222
-
223
- function makeStepStates(): Record<WizardStep, StepState> {
224
- const steps = {} as Record<WizardStep, StepState>;
225
- for (const step of STEP_ORDER) {
226
- steps[step] = { status: "idle" };
227
- }
228
- return steps;
229
- }
230
-
231
- /**
232
- * Create a fresh wizard state.
233
- */
234
- export function createWizardState(): MigrationWizardState {
235
- const now = new Date().toISOString();
236
- return {
237
- currentStep: "select-direction",
238
- steps: makeStepStates(),
239
- hasBundleData: false,
240
- updatedAt: now,
241
- createdAt: now,
242
- };
243
- }
244
-
245
- // ---------------------------------------------------------------------------
246
- // State transitions — pure functions that return a new state
247
- // ---------------------------------------------------------------------------
248
-
249
- function touch(state: MigrationWizardState): MigrationWizardState {
250
- return { ...state, updatedAt: new Date().toISOString() };
251
- }
252
-
253
- function setStepStatus(
254
- state: MigrationWizardState,
255
- step: WizardStep,
256
- status: StepStatus,
257
- error?: StepError,
258
- ): MigrationWizardState {
259
- return touch({
260
- ...state,
261
- steps: {
262
- ...state.steps,
263
- [step]: { status, ...(error ? { error } : {}) },
264
- },
265
- });
266
- }
267
-
268
- /**
269
- * Set the migration direction and advance to the next step.
270
- */
271
- export function selectDirection(
272
- state: MigrationWizardState,
273
- direction: MigrationDirection,
274
- ): MigrationWizardState {
275
- if (state.currentStep !== "select-direction") {
276
- throw new Error(
277
- `selectDirection called in wrong step: "${state.currentStep}"`,
278
- );
279
- }
280
-
281
- const updated = setStepStatus(state, "select-direction", "success");
282
- return touch({
283
- ...updated,
284
- direction,
285
- currentStep: "upload-bundle",
286
- });
287
- }
288
-
289
- /**
290
- * Record that bundle data has been provided and advance to validate.
291
- */
292
- export function setBundleUploaded(
293
- state: MigrationWizardState,
294
- ): MigrationWizardState {
295
- if (state.currentStep !== "upload-bundle") {
296
- throw new Error(
297
- `setBundleUploaded called in wrong step: "${state.currentStep}"`,
298
- );
299
- }
300
-
301
- const updated = setStepStatus(state, "upload-bundle", "success");
302
- return touch({
303
- ...updated,
304
- hasBundleData: true,
305
- currentStep: "validate",
306
- });
307
- }
308
-
309
- /**
310
- * Go back to a previous step. Resets all steps after the target step
311
- * to 'idle' so they can be re-executed.
312
- */
313
- export function goBackTo(
314
- state: MigrationWizardState,
315
- targetStep: WizardStep,
316
- ): MigrationWizardState {
317
- const result = validateWizardTransition(state, targetStep);
318
- if (!result.valid) {
319
- throw new Error(`Invalid back-navigation: ${result.reason}`);
320
- }
321
-
322
- const targetIdx = STEP_INDEX.get(targetStep)!;
323
- const newSteps = { ...state.steps };
324
-
325
- // Reset the target step and all steps after it to idle
326
- for (const step of STEP_ORDER) {
327
- const idx = STEP_INDEX.get(step)!;
328
- if (idx >= targetIdx) {
329
- newSteps[step] = { status: "idle" };
330
- }
331
- }
332
-
333
- return touch({
334
- ...state,
335
- currentStep: targetStep,
336
- steps: newSteps,
337
- // Clear results for steps that were reset
338
- ...(targetIdx <= STEP_INDEX.get("validate")!
339
- ? { validateResult: undefined }
340
- : {}),
341
- ...(targetIdx <= STEP_INDEX.get("preflight-review")!
342
- ? { preflightResult: undefined }
343
- : {}),
344
- ...(targetIdx <= STEP_INDEX.get("transfer")!
345
- ? {
346
- exportResult: undefined,
347
- importResult: undefined,
348
- credentialsImported: undefined,
349
- }
350
- : {}),
351
- });
352
- }
353
-
354
- // ---------------------------------------------------------------------------
355
- // Async step executors — coordinate transport calls with state updates
356
- // ---------------------------------------------------------------------------
357
-
358
- /**
359
- * Options for executing a wizard step, including transport config
360
- * and an optional state update callback for UI reactivity.
361
- */
362
- export interface StepExecutorOptions {
363
- sourceConfig: TransportConfig;
364
- destConfig: TransportConfig;
365
- bundleData: ArrayBuffer;
366
- onStateChange?: (state: MigrationWizardState) => void;
367
- }
368
-
369
- /**
370
- * Build a StepError from a caught exception.
371
- */
372
- function toStepError(err: unknown): StepError {
373
- if (err instanceof MigrationTransportError) {
374
- const retryable =
375
- err.statusCode === 429 ||
376
- (err.statusCode >= 500 && err.statusCode <= 504);
377
- return {
378
- message: err.message,
379
- code: `HTTP_${err.statusCode}`,
380
- retryable,
381
- };
382
- }
383
- if (err instanceof Error) {
384
- return { message: err.message, retryable: false };
385
- }
386
- return { message: String(err), retryable: false };
387
- }
388
-
389
- /**
390
- * Execute the validate step: call validateBundle and update state.
391
- */
392
- export async function executeValidateStep(
393
- state: MigrationWizardState,
394
- options: StepExecutorOptions,
395
- ): Promise<MigrationWizardState> {
396
- if (state.currentStep !== "validate") {
397
- throw new Error(
398
- `executeValidateStep called in wrong step: "${state.currentStep}"`,
399
- );
400
- }
401
-
402
- let current = setStepStatus(state, "validate", "loading");
403
- options.onStateChange?.(current);
404
-
405
- try {
406
- const result = await validateBundle(options.destConfig, options.bundleData);
407
- if (result.is_valid) {
408
- current = setStepStatus(current, "validate", "success");
409
- current = {
410
- ...current,
411
- validateResult: result,
412
- currentStep: "preflight-review",
413
- };
414
- } else {
415
- current = setStepStatus(current, "validate", "error", {
416
- message:
417
- result.errors.map((e) => e.message).join("; ") ||
418
- "Bundle validation failed",
419
- code: result.errors[0]?.code,
420
- retryable: true,
421
- });
422
- current = { ...current, validateResult: result };
423
- }
424
- } catch (err) {
425
- current = setStepStatus(current, "validate", "error", toStepError(err));
426
- }
427
-
428
- current = touch(current);
429
- options.onStateChange?.(current);
430
- return current;
431
- }
432
-
433
- /**
434
- * Execute the preflight-review step: call importPreflight and update state.
435
- */
436
- export async function executePreflightStep(
437
- state: MigrationWizardState,
438
- options: StepExecutorOptions,
439
- ): Promise<MigrationWizardState> {
440
- if (state.currentStep !== "preflight-review") {
441
- throw new Error(
442
- `executePreflightStep called in wrong step: "${state.currentStep}"`,
443
- );
444
- }
445
-
446
- let current = setStepStatus(state, "preflight-review", "loading");
447
- options.onStateChange?.(current);
448
-
449
- try {
450
- const result = await importPreflight(
451
- options.destConfig,
452
- options.bundleData,
453
- );
454
- if (result.can_import) {
455
- current = setStepStatus(current, "preflight-review", "success");
456
- current = {
457
- ...current,
458
- preflightResult: result,
459
- currentStep: "transfer",
460
- };
461
- } else if ("validation" in result) {
462
- current = setStepStatus(current, "preflight-review", "error", {
463
- message:
464
- result.validation.errors.map((e) => e.message).join("; ") ||
465
- "Import preflight validation failed",
466
- code: result.validation.errors[0]?.code,
467
- retryable: true,
468
- });
469
- current = { ...current, preflightResult: result };
470
- } else {
471
- current = setStepStatus(current, "preflight-review", "error", {
472
- message:
473
- result.conflicts.map((c) => c.message).join("; ") ||
474
- "Import blocked by conflicts",
475
- code: result.conflicts[0]?.code,
476
- retryable: true,
477
- });
478
- current = { ...current, preflightResult: result };
479
- }
480
- } catch (err) {
481
- current = setStepStatus(
482
- current,
483
- "preflight-review",
484
- "error",
485
- toStepError(err),
486
- );
487
- }
488
-
489
- current = touch(current);
490
- options.onStateChange?.(current);
491
- return current;
492
- }
493
-
494
- /**
495
- * Execute the transfer step: import the validated bundle to the destination.
496
- */
497
- export async function executeTransferStep(
498
- state: MigrationWizardState,
499
- options: StepExecutorOptions,
500
- ): Promise<MigrationWizardState> {
501
- if (state.currentStep !== "transfer") {
502
- throw new Error(
503
- `executeTransferStep called in wrong step: "${state.currentStep}"`,
504
- );
505
- }
506
-
507
- let current = setStepStatus(state, "transfer", "loading");
508
- options.onStateChange?.(current);
509
-
510
- try {
511
- // Import the same bundle that was validated and preflighted
512
- const importResult = await importCommit(
513
- options.destConfig,
514
- options.bundleData,
515
- );
516
- current = { ...current, importResult };
517
-
518
- if (importResult.success) {
519
- current = setStepStatus(current, "transfer", "success");
520
- // Extract credential import results from the response (if present)
521
- current = {
522
- ...current,
523
- currentStep: "rebind-secrets",
524
- ...(importResult.credentialsImported
525
- ? { credentialsImported: importResult.credentialsImported }
526
- : {}),
527
- };
528
- } else {
529
- current = setStepStatus(current, "transfer", "error", {
530
- message:
531
- importResult.message || `Import failed: ${importResult.reason}`,
532
- code: importResult.reason,
533
- retryable: true,
534
- });
535
- }
536
- } catch (err) {
537
- current = setStepStatus(current, "transfer", "error", toStepError(err));
538
- }
539
-
540
- current = touch(current);
541
- options.onStateChange?.(current);
542
- return current;
543
- }
544
-
545
- /**
546
- * Mark the rebind-secrets step as complete.
547
- *
548
- * This step is UI-driven (the user confirms they've rebound secrets
549
- * and channels). The wizard just tracks its completion.
550
- */
551
- export function completeRebindSecrets(
552
- state: MigrationWizardState,
553
- ): MigrationWizardState {
554
- if (state.currentStep !== "rebind-secrets") {
555
- throw new Error(
556
- `completeRebindSecrets called in wrong step: "${state.currentStep}"`,
557
- );
558
- }
559
-
560
- let current = setStepStatus(state, "rebind-secrets", "success");
561
- current = { ...current, currentStep: "complete" };
562
- current = setStepStatus(current, "complete", "success");
563
- return touch(current);
564
- }
565
-
566
- // ---------------------------------------------------------------------------
567
- // Persistence helpers
568
- // ---------------------------------------------------------------------------
569
-
570
- /**
571
- * Serialize wizard state to a JSON string for persistence.
572
- *
573
- * The state is already plain data — this is a convenience wrapper
574
- * that validates the shape and strips any undefined values.
575
- */
576
- export function serializeWizardState(state: MigrationWizardState): string {
577
- return JSON.stringify(state);
578
- }
579
-
580
- /**
581
- * Deserialize a persisted wizard state from JSON.
582
- *
583
- * Returns undefined if the input is invalid or corrupted.
584
- * On successful deserialization, `hasBundleData` is set to false
585
- * because binary ArrayBuffer data cannot be persisted — the user
586
- * must re-provide the bundle file after resuming.
587
- */
588
- export function deserializeWizardState(
589
- json: string,
590
- ): MigrationWizardState | undefined {
591
- try {
592
- const parsed = WizardStateSchema.safeParse(JSON.parse(json));
593
- if (!parsed.success) {
594
- return undefined;
595
- }
596
- const state = parsed.data;
597
-
598
- // Every step must be present (the persisted record is exhaustive).
599
- for (const step of STEP_ORDER) {
600
- if (!state.steps[step]) {
601
- return undefined;
602
- }
603
- }
604
-
605
- // Bundle data is never persisted — must be re-provided
606
- return { ...state, hasBundleData: false };
607
- } catch {
608
- return undefined;
609
- }
610
- }
611
-
612
- /**
613
- * Check if a persisted wizard state can be meaningfully resumed.
614
- *
615
- * A wizard is resumable if:
616
- * - It has a direction selected
617
- * - It is not already complete
618
- * - It is not on the first step (nothing to resume)
619
- */
620
- export function isResumable(state: MigrationWizardState): boolean {
621
- if (!state.direction) {
622
- return false;
623
- }
624
- if (state.currentStep === "complete") {
625
- return false;
626
- }
627
- if (state.currentStep === "select-direction") {
628
- return false;
629
- }
630
- return true;
631
- }
632
-
633
- /**
634
- * Prepare a deserialized state for resumption.
635
- *
636
- * If the current step was in 'loading' status when the app closed,
637
- * resets it to 'idle' so the user can retry. Steps that require
638
- * bundle data (validate, preflight-review, transfer) will need the
639
- * bundle re-uploaded if it was not persisted.
640
- */
641
- export function prepareForResume(
642
- state: MigrationWizardState,
643
- ): MigrationWizardState {
644
- let current = { ...state };
645
-
646
- // Reset loading steps to idle (they were interrupted)
647
- const step = current.currentStep;
648
- if (current.steps[step].status === "loading") {
649
- current = setStepStatus(current, step, "idle");
650
- }
651
-
652
- // If we need bundle data but don't have it, rewind to upload-bundle
653
- // and reset all downstream steps + stale results (mirrors goBackTo logic)
654
- const needsBundleData =
655
- step === "validate" || step === "preflight-review" || step === "transfer";
656
- if (needsBundleData && !current.hasBundleData) {
657
- const uploadBundleIdx = STEP_INDEX.get("upload-bundle")!;
658
- const newSteps = { ...current.steps };
659
- for (const s of STEP_ORDER) {
660
- const idx = STEP_INDEX.get(s)!;
661
- if (idx >= uploadBundleIdx) {
662
- newSteps[s] = { status: "idle" };
663
- }
664
- }
665
- current = {
666
- ...current,
667
- currentStep: "upload-bundle",
668
- steps: newSteps,
669
- validateResult: undefined,
670
- preflightResult: undefined,
671
- exportResult: undefined,
672
- importResult: undefined,
673
- credentialsImported: undefined,
674
- };
675
- }
676
-
677
- return touch(current);
678
- }
679
-
680
- // ---------------------------------------------------------------------------
681
- // Query helpers
682
- // ---------------------------------------------------------------------------
683
-
684
- /**
685
- * Get the ordered list of steps for the current wizard configuration.
686
- */
687
- export function getStepOrder(
688
- state: MigrationWizardState,
689
- ): readonly WizardStep[] {
690
- return state.direction ? stepsForDirection(state.direction) : STEP_ORDER;
691
- }
692
-
693
- /**
694
- * Get the index of the current step (0-based).
695
- */
696
- export function getCurrentStepIndex(state: MigrationWizardState): number {
697
- return STEP_INDEX.get(state.currentStep)!;
698
- }
699
-
700
- /**
701
- * Get the total number of steps.
702
- */
703
- export function getTotalSteps(): number {
704
- return STEP_ORDER.length;
705
- }
706
-
707
- /**
708
- * Check if a step is accessible (can be navigated to) from the current state.
709
- */
710
- export function isStepAccessible(
711
- state: MigrationWizardState,
712
- step: WizardStep,
713
- ): boolean {
714
- return validateWizardTransition(state, step).valid;
715
- }
716
-
717
- /**
718
- * Check if the wizard is in a terminal (completed) state.
719
- */
720
- export function isWizardComplete(state: MigrationWizardState): boolean {
721
- return (
722
- state.currentStep === "complete" &&
723
- state.steps["complete"].status === "success"
724
- );
725
- }
726
-
727
- /**
728
- * Check if the current step has an error that can be retried.
729
- */
730
- export function canRetryCurrentStep(state: MigrationWizardState): boolean {
731
- const stepState = state.steps[state.currentStep];
732
- return stepState.status === "error" && (stepState.error?.retryable ?? false);
733
- }
734
-
735
- /**
736
- * Reset the current step's error state to idle for retry.
737
- */
738
- export function resetStepForRetry(
739
- state: MigrationWizardState,
740
- ): MigrationWizardState {
741
- if (!canRetryCurrentStep(state)) {
742
- throw new Error(
743
- `Cannot retry step "${state.currentStep}" — not in a retryable error state`,
744
- );
745
- }
746
- return setStepStatus(state, state.currentStep, "idle");
747
- }