@mycadet/assistant 0.12.11 → 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 (176) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.yaml +2 -708
  9. package/package.json +2 -3
  10. package/scripts/sync-web-search-catalog.ts +1 -2
  11. package/src/__tests__/cloud-profile-guard.test.ts +2 -2
  12. package/src/__tests__/config-schema.test.ts +6 -38
  13. package/src/__tests__/conversation-queue.test.ts +45 -6
  14. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  15. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  16. package/src/__tests__/persona-resolver.test.ts +0 -51
  17. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  18. package/src/__tests__/run-due-schedules.test.ts +0 -4
  19. package/src/__tests__/schedule-store.test.ts +0 -12
  20. package/src/__tests__/stt-stream-session.test.ts +4 -4
  21. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  22. package/src/__tests__/voice-config-update.test.ts +12 -167
  23. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  24. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  25. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  26. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  27. package/src/calls/resolve-call-tts-provider.ts +6 -25
  28. package/src/calls/telephony-credential-preflight.ts +0 -10
  29. package/src/calls/telephony-tts-capability.ts +1 -17
  30. package/src/cli/commands/tts.ts +1 -2
  31. package/src/cli/index.help.ts +0 -2
  32. package/src/cli/program.ts +0 -2
  33. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  34. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  35. package/src/config/schemas/services.ts +5 -6
  36. package/src/config/schemas/stt.ts +1 -3
  37. package/src/config/schemas/tts.ts +1 -30
  38. package/src/daemon/daemon-readiness.ts +4 -29
  39. package/src/daemon/lifecycle.ts +0 -31
  40. package/src/daemon/providers-setup.ts +0 -8
  41. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  42. package/src/heartbeat/heartbeat-service.ts +1 -66
  43. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  44. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  45. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  46. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  47. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  48. package/src/live-voice/live-voice-session.ts +10 -6
  49. package/src/live-voice/live-voice-tts.ts +1 -8
  50. package/src/plugins/defaults/memory/worker.ts +3 -3
  51. package/src/prompts/persona-resolver.ts +0 -35
  52. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  53. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  54. package/src/providers/search-provider-catalog.ts +7 -39
  55. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  56. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  57. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  58. package/src/providers/speech-to-text/resolve.ts +3 -120
  59. package/src/runtime/http-router-types.ts +1 -3
  60. package/src/runtime/http-server.ts +1 -1
  61. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  62. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  63. package/src/runtime/routes/index.ts +0 -4
  64. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  65. package/src/runtime/routes/live-voice-routes.ts +8 -11
  66. package/src/runtime/routes/secret-routes.ts +0 -6
  67. package/src/runtime/routes/tts-routes.ts +0 -38
  68. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  69. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  70. package/src/schedule/schedule-store.ts +0 -7
  71. package/src/schedule/scheduler.ts +0 -5
  72. package/src/schedule/worker.ts +1 -1
  73. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  74. package/src/stt/daemon-batch-transcriber.ts +4 -38
  75. package/src/stt/types.ts +1 -9
  76. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  77. package/src/tools/network/web-search.ts +4 -165
  78. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  79. package/src/tts/provider-catalog.ts +0 -4
  80. package/src/tts/types.ts +0 -1
  81. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  82. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  83. package/src/__tests__/migration-export-http.test.ts +0 -557
  84. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  85. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  86. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  87. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  88. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  89. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  90. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  91. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  92. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  93. package/src/__tests__/migration-transport.test.ts +0 -1089
  94. package/src/__tests__/migration-validate-http.test.ts +0 -634
  95. package/src/__tests__/migration-wizard.test.ts +0 -1395
  96. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  97. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  98. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  99. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  100. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  101. package/src/background-wake/background-wake-routes.test.ts +0 -906
  102. package/src/background-wake/cloud-client.test.ts +0 -328
  103. package/src/background-wake/cloud-client.ts +0 -196
  104. package/src/background-wake/next-wake.test.ts +0 -335
  105. package/src/background-wake/next-wake.ts +0 -188
  106. package/src/background-wake/publisher.ts +0 -93
  107. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  108. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  109. package/src/cli/commands/backup.help.ts +0 -180
  110. package/src/cli/commands/backup.ts +0 -398
  111. package/src/cloud/cloud-speech.test.ts +0 -335
  112. package/src/cloud/cloud-speech.ts +0 -300
  113. package/src/config/cloud-speech-defaults.ts +0 -300
  114. package/src/config/sanitize-for-transfer.ts +0 -111
  115. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  116. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  117. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  118. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  119. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  120. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  121. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  122. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  123. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  124. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  125. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  126. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  127. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  128. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  129. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  130. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  131. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  132. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  133. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  134. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  135. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  136. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  137. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  138. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  139. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  140. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  141. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  142. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  143. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  144. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  146. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  147. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  148. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  149. package/src/runtime/migrations/job-registry.ts +0 -297
  150. package/src/runtime/migrations/migration-transport.ts +0 -612
  151. package/src/runtime/migrations/migration-wizard.ts +0 -747
  152. package/src/runtime/migrations/origin-mode.ts +0 -42
  153. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  154. package/src/runtime/migrations/staged-import-path.ts +0 -116
  155. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  156. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  157. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  158. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  159. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  160. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  161. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  162. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  163. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  164. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  165. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  166. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  167. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  168. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  169. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  170. package/src/runtime/routes/background-wake-routes.ts +0 -387
  171. package/src/runtime/routes/migration-routes.ts +0 -2584
  172. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  173. package/src/tools/network/cloud-search-proxy.ts +0 -211
  174. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  175. package/src/tts/providers/cadet-provider.ts +0 -244
  176. 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
- }