@inspecto-dev/cli 0.3.3 → 0.3.4

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.
@@ -50,6 +50,46 @@ interface ApplySpinner {
50
50
  fail(text: string): void
51
51
  }
52
52
 
53
+ function normalizeSupportedIde(ide?: string): string | undefined {
54
+ if (!ide) return undefined
55
+ return ide.toLowerCase() === 'vscode' ? 'vscode' : ide.toLowerCase()
56
+ }
57
+
58
+ async function readInheritedSettingsDefaults(
59
+ repoRoot: string,
60
+ projectRoot: string,
61
+ settingsFileName: string,
62
+ ): Promise<{ ide?: string; providerDefault?: string }> {
63
+ if (path.resolve(repoRoot) === path.resolve(projectRoot)) {
64
+ return {}
65
+ }
66
+
67
+ const inheritedSettingsPath = path.join(repoRoot, '.inspecto', settingsFileName)
68
+ if (!(await exists(inheritedSettingsPath))) {
69
+ return {}
70
+ }
71
+
72
+ const inheritedSettings = await readJSON<Record<string, unknown>>(inheritedSettingsPath)
73
+ if (!inheritedSettings || typeof inheritedSettings !== 'object') {
74
+ return {}
75
+ }
76
+
77
+ const inheritedDefaults: { ide?: string; providerDefault?: string } = {}
78
+
79
+ if (typeof inheritedSettings.ide === 'string') {
80
+ const normalizedIde = normalizeSupportedIde(inheritedSettings.ide)
81
+ if (normalizedIde) {
82
+ inheritedDefaults.ide = normalizedIde
83
+ }
84
+ }
85
+
86
+ if (typeof inheritedSettings['provider.default'] === 'string') {
87
+ inheritedDefaults.providerDefault = inheritedSettings['provider.default']
88
+ }
89
+
90
+ return inheritedDefaults
91
+ }
92
+
53
93
  export interface ApplyOnboardingResult {
54
94
  status: CommandStatus
55
95
  mutations: Mutation[]
@@ -91,13 +131,16 @@ function resultStatus(nextSteps: string[]): CommandStatus {
91
131
  return nextSteps.length > 0 ? 'warning' : 'ok'
92
132
  }
93
133
 
94
- function manualPlanSteps(plan: PlanResult): string[] {
95
- return [
96
- ...plan.blockers.map(blocker => blocker.message),
97
- ...plan.actions
98
- .filter(action => action.type === 'manual_step')
99
- .map(action => action.description),
100
- ]
134
+ function manualPlanSteps(plan: PlanResult, includeBlockers = true): string[] {
135
+ const steps = plan.actions
136
+ .filter(action => action.type === 'manual_step')
137
+ .map(action => action.description)
138
+
139
+ if (!includeBlockers) {
140
+ return steps
141
+ }
142
+
143
+ return [...plan.blockers.map(blocker => blocker.message), ...steps]
101
144
  }
102
145
 
103
146
  export async function applyOnboardingPlan(
@@ -163,6 +206,10 @@ async function applyOnboardingPlanInternal(
163
206
  input: ApplyOnboardingInput,
164
207
  ): Promise<ApplyOnboardingResult> {
165
208
  const reporter = createReporter(input.options.quiet)
209
+ const additiveManualPlan =
210
+ input.plan?.strategy === 'manual' &&
211
+ input.allowManualPlanApply &&
212
+ input.plan.blockers.length === 0
166
213
 
167
214
  if (input.plan && input.plan.strategy !== 'supported' && !input.allowManualPlanApply) {
168
215
  return {
@@ -172,7 +219,7 @@ async function applyOnboardingPlanInternal(
172
219
  installFailed: false,
173
220
  injectionFailed: false,
174
221
  manualExtensionInstallNeeded: false,
175
- nextSteps: manualPlanSteps(input.plan),
222
+ nextSteps: manualPlanSteps(input.plan, true),
176
223
  },
177
224
  }
178
225
  }
@@ -183,6 +230,11 @@ async function applyOnboardingPlanInternal(
183
230
  const promptsFileName = input.options.shared ? 'prompts.json' : 'prompts.local.json'
184
231
  const settingsPath = path.join(settingsDir, settingsFileName)
185
232
  const promptsPath = path.join(settingsDir, promptsFileName)
233
+ const inheritedDefaults = await readInheritedSettingsDefaults(
234
+ input.repoRoot,
235
+ input.projectRoot,
236
+ settingsFileName,
237
+ )
186
238
  const runtimePackages = resolveRuntimePackages()
187
239
  const installCmd = getInstallCommand(input.packageManager, runtimePackages.installSpec)
188
240
  const nextSteps: string[] = []
@@ -217,17 +269,19 @@ async function applyOnboardingPlanInternal(
217
269
  }
218
270
 
219
271
  let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig)
220
- for (const target of input.supportedBuildTargets) {
221
- const result = await injectPlugin(
222
- input.repoRoot,
223
- target,
224
- input.options.dryRun,
225
- input.options.quiet ?? false,
226
- )
227
- if (result.success) {
228
- mutations.push(...result.mutations)
229
- } else {
230
- injectionFailed = true
272
+ if (!additiveManualPlan) {
273
+ for (const target of input.supportedBuildTargets) {
274
+ const result = await injectPlugin(
275
+ input.repoRoot,
276
+ target,
277
+ input.options.dryRun,
278
+ input.options.quiet ?? false,
279
+ )
280
+ if (result.success) {
281
+ mutations.push(...result.mutations)
282
+ } else {
283
+ injectionFailed = true
284
+ }
231
285
  }
232
286
  }
233
287
 
@@ -238,20 +292,56 @@ async function applyOnboardingPlanInternal(
238
292
  reporter.hint('Please fix the syntax errors manually, or delete it and re-run init')
239
293
  nextSteps.push(`Fix .inspecto/${settingsFileName} or delete it and rerun Inspecto setup.`)
240
294
  } else {
241
- reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`)
295
+ const mergedSettings =
296
+ existingSettings && typeof existingSettings === 'object'
297
+ ? { ...(existingSettings as Record<string, unknown>) }
298
+ : {}
299
+ let settingsChanged = false
300
+
301
+ const desiredIde =
302
+ inheritedDefaults.ide ??
303
+ (input.selectedIDE?.supported ? normalizeSupportedIde(input.selectedIDE.ide) : undefined)
304
+
305
+ if (desiredIde && !mergedSettings.ide) {
306
+ mergedSettings.ide = desiredIde
307
+ settingsChanged = true
308
+ }
309
+
310
+ const desiredProviderDefault = inheritedDefaults.providerDefault ?? input.providerDefault
311
+ if (desiredProviderDefault && !mergedSettings['provider.default']) {
312
+ mergedSettings['provider.default'] = desiredProviderDefault
313
+ settingsChanged = true
314
+ }
315
+
316
+ if (settingsChanged) {
317
+ if (input.options.dryRun) {
318
+ reporter.dryRun(`Would update .inspecto/${settingsFileName}`)
319
+ } else {
320
+ await writeJSON(settingsPath, mergedSettings)
321
+ reporter.success(`Updated .inspecto/${settingsFileName} with missing defaults`)
322
+ mutations.push({
323
+ type: 'file_modified',
324
+ path: `.inspecto/${settingsFileName}`,
325
+ description: 'Merged missing Inspecto defaults into existing settings',
326
+ })
327
+ }
328
+ } else {
329
+ reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`)
330
+ }
242
331
  }
243
332
  } else {
244
333
  const defaultSettings: Record<string, unknown> = {}
334
+ const desiredIde =
335
+ inheritedDefaults.ide ??
336
+ (input.selectedIDE?.supported ? normalizeSupportedIde(input.selectedIDE.ide) : undefined)
337
+ const desiredProviderDefault = inheritedDefaults.providerDefault ?? input.providerDefault
245
338
 
246
- if (input.selectedIDE?.supported) {
247
- defaultSettings.ide =
248
- input.selectedIDE.ide.toLowerCase() === 'vscode'
249
- ? 'vscode'
250
- : input.selectedIDE.ide.toLowerCase()
339
+ if (desiredIde) {
340
+ defaultSettings.ide = desiredIde
251
341
  }
252
342
 
253
- if (input.providerDefault) {
254
- defaultSettings['provider.default'] = input.providerDefault
343
+ if (desiredProviderDefault) {
344
+ defaultSettings['provider.default'] = desiredProviderDefault
255
345
  }
256
346
 
257
347
  if (input.options.dryRun) {
@@ -340,6 +430,9 @@ async function applyOnboardingPlanInternal(
340
430
  if (manualExtensionInstallNeeded) {
341
431
  nextSteps.push('Install the Inspecto IDE extension manually')
342
432
  }
433
+ if (additiveManualPlan && input.plan) {
434
+ nextSteps.push(...manualPlanSteps(input.plan, false))
435
+ }
343
436
  if (input.manualConfigRequiredFor === 'Nuxt') {
344
437
  nextSteps.push(
345
438
  'Nuxt detected—please follow the Nuxt instructions printed above to finish setup.',
@@ -71,6 +71,31 @@ function buildToolBlockers(context: OnboardingContext): CommandMessage[] {
71
71
  return [message('missing-build-tool', 'No supported build tool detected')]
72
72
  }
73
73
 
74
+ function buildToolWarnings(context: OnboardingContext): CommandMessage[] {
75
+ const warnings: CommandMessage[] = []
76
+
77
+ if (context.buildTools.supported.length === 1) {
78
+ const buildTool = context.buildTools.supported[0]!
79
+ if (buildTool.tool === 'rspack' && buildTool.isLegacyRspack) {
80
+ warnings.push(
81
+ message(
82
+ 'legacy-rspack-requires-manual-config',
83
+ `Legacy Rspack detected at ${buildTool.configPath}. Inspecto must use the legacy Rspack plugin entry and manual config steps.`,
84
+ ),
85
+ )
86
+ } else if (buildTool.tool === 'webpack' && buildTool.isLegacyWebpack) {
87
+ warnings.push(
88
+ message(
89
+ 'legacy-webpack4-requires-manual-config',
90
+ `Webpack 4 detected at ${buildTool.configPath}. Inspecto must use the legacy Webpack 4 plugin entry and manual config steps.`,
91
+ ),
92
+ )
93
+ }
94
+ }
95
+
96
+ return warnings
97
+ }
98
+
74
99
  function frameworkBlockers(context: OnboardingContext): CommandMessage[] {
75
100
  if (context.frameworks.supported.length > 0) {
76
101
  return []
@@ -149,6 +174,36 @@ function manualBuildToolActions(context: OnboardingContext): PlanResult['actions
149
174
  ]
150
175
  }
151
176
 
177
+ const buildTool = context.buildTools.supported[0]
178
+ if (buildTool?.tool === 'rspack' && buildTool.isLegacyRspack) {
179
+ return [
180
+ {
181
+ type: 'install_dependency',
182
+ target: '@inspecto-dev/plugin @inspecto-dev/core',
183
+ description: `Install the Inspecto runtime packages with ${context.packageManager}.`,
184
+ },
185
+ {
186
+ type: 'manual_step',
187
+ target: buildTool.configPath,
188
+ description: `Update ${buildTool.configPath} to import \`rspackPlugin\` from \`@inspecto-dev/plugin/legacy/rspack\` and add it to the Rspack plugins array.`,
189
+ },
190
+ ]
191
+ }
192
+ if (buildTool?.tool === 'webpack' && buildTool.isLegacyWebpack) {
193
+ return [
194
+ {
195
+ type: 'install_dependency',
196
+ target: '@inspecto-dev/plugin @inspecto-dev/core',
197
+ description: `Install the Inspecto runtime packages with ${context.packageManager}.`,
198
+ },
199
+ {
200
+ type: 'manual_step',
201
+ target: buildTool.configPath,
202
+ description: `Update ${buildTool.configPath} to import \`webpackPlugin\` from \`@inspecto-dev/plugin/legacy/webpack4\` and add it to the Webpack plugins array.`,
203
+ },
204
+ ]
205
+ }
206
+
152
207
  return [
153
208
  {
154
209
  type: 'manual_step',
@@ -183,7 +238,10 @@ function manualFrameworkActions(context: OnboardingContext): PlanResult['actions
183
238
 
184
239
  export async function createDetectionResult(root: string): Promise<DetectionResult> {
185
240
  const context = await buildOnboardingContext(root)
186
- const warnings = uniqueMessages([...unsupportedEnvironmentWarnings(context)])
241
+ const warnings = uniqueMessages([
242
+ ...unsupportedEnvironmentWarnings(context),
243
+ ...buildToolWarnings(context),
244
+ ])
187
245
 
188
246
  const buildToolResult = buildToolBlockers(context)
189
247
  const frameworkResult = frameworkBlockers(context)
@@ -209,18 +267,36 @@ export async function createDetectionResult(root: string): Promise<DetectionResu
209
267
  }
210
268
 
211
269
  export function createPlanResult(context: OnboardingContext): PlanResult {
212
- const warnings = uniqueMessages(unsupportedEnvironmentWarnings(context))
270
+ const warnings = uniqueMessages([
271
+ ...unsupportedEnvironmentWarnings(context),
272
+ ...buildToolWarnings(context),
273
+ ])
213
274
  const blockers = uniqueMessages([...buildToolBlockers(context), ...frameworkBlockers(context)])
214
275
  const actions: PlanResult['actions'] = []
215
276
 
216
277
  let strategy: PlanResult['strategy'] = 'supported'
217
278
 
218
- if (blockers.length > 0) {
279
+ const hasLegacyRspackManualPlan =
280
+ context.buildTools.supported.length === 1 &&
281
+ context.buildTools.supported[0]?.tool === 'rspack' &&
282
+ context.buildTools.supported[0]?.isLegacyRspack
283
+ const hasLegacyWebpackManualPlan =
284
+ context.buildTools.supported.length === 1 &&
285
+ context.buildTools.supported[0]?.tool === 'webpack' &&
286
+ context.buildTools.supported[0]?.isLegacyWebpack
287
+
288
+ if (blockers.length > 0 || hasLegacyRspackManualPlan || hasLegacyWebpackManualPlan) {
219
289
  strategy = 'manual'
220
290
  if (
221
291
  context.buildTools.unsupported.length > 0 ||
222
292
  context.buildTools.supported.length === 0 ||
223
- context.buildTools.supported.length > 1
293
+ context.buildTools.supported.length > 1 ||
294
+ context.buildTools.supported.some(
295
+ buildTool => buildTool.tool === 'rspack' && buildTool.isLegacyRspack,
296
+ ) ||
297
+ context.buildTools.supported.some(
298
+ buildTool => buildTool.tool === 'webpack' && buildTool.isLegacyWebpack,
299
+ )
224
300
  ) {
225
301
  actions.push(...manualBuildToolActions(context))
226
302
  }
@@ -126,8 +126,9 @@ async function detectFrameworkSupportByPackage(
126
126
 
127
127
  async function buildTargetedContext(
128
128
  rootContext: OnboardingContext,
129
- packagePath: string,
129
+ target: { id?: string; packagePath: string },
130
130
  ): Promise<OnboardingContext> {
131
+ const packagePath = normalizePackagePath(target.packagePath)
131
132
  const projectRoot = packagePath ? path.join(rootContext.root, packagePath) : rootContext.root
132
133
  const [frameworks, ides, providers] = await Promise.all([
133
134
  detectFrameworks(projectRoot),
@@ -140,7 +141,11 @@ async function buildTargetedContext(
140
141
  packageManager: rootContext.packageManager,
141
142
  buildTools: {
142
143
  supported: rootContext.buildTools.supported.filter(item => {
143
- return normalizePackagePath(item.packagePath) === packagePath
144
+ const itemPackagePath = normalizePackagePath(item.packagePath)
145
+ if (target.id) {
146
+ return `${itemPackagePath || '.'}:${item.tool}:${item.configPath}` === target.id
147
+ }
148
+ return itemPackagePath === packagePath
144
149
  }),
145
150
  unsupported: [],
146
151
  },
@@ -327,7 +332,8 @@ export async function resolveOnboardingSession(
327
332
  if (target.status === 'needs_selection') {
328
333
  const plan = createPlanResult(rootContext)
329
334
  const summary: OnboardingSummary = {
330
- headline: 'Inspecto found multiple plausible app targets and needs one selection.',
335
+ headline:
336
+ 'Inspecto needs one build target selection before setup so it knows which local dev build should receive the plugin and settings.',
331
337
  changes: [],
332
338
  risks: [],
333
339
  manualFollowUp: [],
@@ -344,8 +350,7 @@ export async function resolveOnboardingSession(
344
350
  }
345
351
  }
346
352
 
347
- const packagePath = normalizePackagePath(target.selected?.packagePath)
348
- const context = await buildTargetedContext(rootContext, packagePath)
353
+ const context = await buildTargetedContext(rootContext, target.selected!)
349
354
  const verification = await buildVerification(context.root, context.packageManager)
350
355
  const plan = createPlanResult(context)
351
356
  const summary = buildOnboardingSummary(plan, context.root)
@@ -409,6 +414,7 @@ export async function applyResolvedOnboardingSession(
409
414
  selectedIDE: session.selectedIDE,
410
415
  providerDefault: session.providerDefault,
411
416
  plan: session.plan,
417
+ allowManualPlanApply: session.plan.strategy === 'manual' && session.plan.blockers.length === 0,
412
418
  })
413
419
 
414
420
  const diagnostics = buildExecutionDiagnostics(session, applyResult)
@@ -16,11 +16,36 @@ interface RankedCandidate {
16
16
  score: number
17
17
  }
18
18
 
19
+ function buildCandidateId(candidate: {
20
+ packagePath: string
21
+ buildTool: string
22
+ configPath: string
23
+ }): string {
24
+ return [candidate.packagePath || '.', candidate.buildTool, candidate.configPath].join(':')
25
+ }
26
+
19
27
  function normalizePackagePath(packagePath?: string): string {
20
28
  if (!packagePath || packagePath === '.') return ''
21
29
  return packagePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '')
22
30
  }
23
31
 
32
+ function normalizeTargetValue(target?: string): string {
33
+ if (!target) return ''
34
+ return target.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '')
35
+ }
36
+
37
+ function buildSelectionPurpose(): string {
38
+ return 'Choose the build target that runs your local development build so Inspecto can attach the right plugin and settings.'
39
+ }
40
+
41
+ function buildSelectionInstructions(hasCandidates: boolean): string {
42
+ if (!hasCandidates) {
43
+ return 'If auto-detection missed your build entrypoint, rerun with --target <configPath> using the config file or wrapper script your dev command actually starts.'
44
+ }
45
+
46
+ return 'Rerun with --target <candidateId> using one returned candidateId. The CLI also accepts an exact configPath from the candidate list as a compatibility fallback.'
47
+ }
48
+
24
49
  function looksLikeAppPackage(packagePath: string): boolean {
25
50
  if (!packagePath) return true
26
51
  return /(^|\/)(app|apps|web|client|frontend|site)(\/|$)/i.test(packagePath)
@@ -33,13 +58,19 @@ function looksLikeAuxiliaryPackage(packagePath: string): boolean {
33
58
  function buildCandidates(input: ResolveOnboardingTargetInput): OnboardingTargetCandidate[] {
34
59
  return input.buildTools.map(buildTool => {
35
60
  const packagePath = normalizePackagePath(buildTool.packagePath)
36
- return {
61
+ const candidate: OnboardingTargetCandidate = {
37
62
  packagePath,
38
63
  configPath: buildTool.configPath,
64
+ label: buildTool.label,
39
65
  buildTool: buildTool.tool,
66
+ isLegacyRspack: buildTool.isLegacyRspack,
67
+ isLegacyWebpack: buildTool.isLegacyWebpack,
40
68
  frameworks: input.frameworkSupportByPackage[packagePath] ?? [],
41
69
  automaticInjection: true,
42
70
  }
71
+ candidate.id = buildCandidateId(candidate)
72
+ candidate.candidateId = candidate.id
73
+ return candidate
43
74
  })
44
75
  }
45
76
 
@@ -73,18 +104,57 @@ export function resolveOnboardingTarget(
73
104
  status: 'needs_selection',
74
105
  candidates,
75
106
  reason: 'No supported targets were detected.',
107
+ selectionPurpose: buildSelectionPurpose(),
108
+ selectionInstructions: buildSelectionInstructions(false),
76
109
  }
77
110
  }
78
111
 
79
112
  const explicitlySelected = normalizePackagePath(input.selectedPackagePath)
113
+ const explicitlySelectedValue = normalizeTargetValue(input.selectedPackagePath)
80
114
  if (input.selectedPackagePath !== undefined) {
81
- const selected = candidates.find(candidate => candidate.packagePath === explicitlySelected)
115
+ const selectedById = candidates.find(
116
+ candidate =>
117
+ candidate.id === input.selectedPackagePath ||
118
+ candidate.candidateId === input.selectedPackagePath,
119
+ )
120
+ if (selectedById) {
121
+ return {
122
+ status: 'resolved',
123
+ selected: selectedById,
124
+ candidates,
125
+ reason: `Using the explicitly selected target: ${selectedById.configPath}.`,
126
+ selectionPurpose: buildSelectionPurpose(),
127
+ selectionInstructions: buildSelectionInstructions(true),
128
+ }
129
+ }
130
+
131
+ const selectedByConfigPath = candidates.find(
132
+ candidate => normalizeTargetValue(candidate.configPath) === explicitlySelectedValue,
133
+ )
134
+ if (selectedByConfigPath) {
135
+ return {
136
+ status: 'resolved',
137
+ selected: selectedByConfigPath,
138
+ candidates,
139
+ reason: `Using the explicitly selected config path: ${selectedByConfigPath.configPath}.`,
140
+ selectionPurpose: buildSelectionPurpose(),
141
+ selectionInstructions: buildSelectionInstructions(true),
142
+ }
143
+ }
144
+
145
+ const matchingPackageCandidates = candidates.filter(
146
+ candidate => candidate.packagePath === explicitlySelected,
147
+ )
148
+ const selected =
149
+ matchingPackageCandidates.length === 1 ? matchingPackageCandidates[0] : undefined
82
150
  if (selected) {
83
151
  return {
84
152
  status: 'resolved',
85
153
  selected,
86
154
  candidates,
87
155
  reason: `Using the explicitly selected target: ${selected.packagePath || '.'}.`,
156
+ selectionPurpose: buildSelectionPurpose(),
157
+ selectionInstructions: buildSelectionInstructions(true),
88
158
  }
89
159
  }
90
160
  }
@@ -95,6 +165,8 @@ export function resolveOnboardingTarget(
95
165
  selected: candidates[0],
96
166
  candidates,
97
167
  reason: 'Only one supported target was detected.',
168
+ selectionPurpose: buildSelectionPurpose(),
169
+ selectionInstructions: buildSelectionInstructions(true),
98
170
  }
99
171
  }
100
172
 
@@ -104,6 +176,8 @@ export function resolveOnboardingTarget(
104
176
  status: 'needs_selection',
105
177
  candidates,
106
178
  reason: 'Multiple supported targets look equally plausible.',
179
+ selectionPurpose: buildSelectionPurpose(),
180
+ selectionInstructions: buildSelectionInstructions(true),
107
181
  }
108
182
  }
109
183
 
@@ -112,5 +186,7 @@ export function resolveOnboardingTarget(
112
186
  selected: ranked[0]!.candidate,
113
187
  candidates,
114
188
  reason: `Preselected ${ranked[0]!.candidate.packagePath || '.'} because it has the strongest supported app signal.`,
189
+ selectionPurpose: buildSelectionPurpose(),
190
+ selectionInstructions: buildSelectionInstructions(true),
115
191
  }
116
192
  }
package/src/types.ts CHANGED
@@ -63,9 +63,14 @@ export interface OnboardingContext {
63
63
  }
64
64
 
65
65
  export interface OnboardingTargetCandidate {
66
+ id?: string
67
+ candidateId?: string
66
68
  packagePath: string
67
69
  configPath: string
70
+ label?: string
68
71
  buildTool: BuildTool
72
+ isLegacyRspack?: boolean
73
+ isLegacyWebpack?: boolean
69
74
  frameworks: string[]
70
75
  automaticInjection: boolean
71
76
  }
@@ -75,6 +80,8 @@ export interface OnboardingTargetResolution {
75
80
  selected?: OnboardingTargetCandidate
76
81
  candidates: OnboardingTargetCandidate[]
77
82
  reason: string
83
+ selectionPurpose?: string
84
+ selectionInstructions?: string
78
85
  }
79
86
 
80
87
  export interface OnboardingSummary {