@inspecto-dev/cli 0.3.3 → 0.3.5

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 (46) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-test.log +10594 -4044
  3. package/CHANGELOG.md +28 -0
  4. package/dist/bin.js +36 -2
  5. package/dist/{chunk-LJOKPCPD.js → chunk-7ABJRH3F.js} +1701 -182
  6. package/dist/index.d.ts +69 -4
  7. package/dist/index.js +7 -1
  8. package/package.json +3 -3
  9. package/src/bin.ts +49 -1
  10. package/src/commands/dev-config.ts +109 -0
  11. package/src/commands/doctor.ts +189 -9
  12. package/src/commands/init.ts +10 -3
  13. package/src/commands/integration-automation.ts +2 -0
  14. package/src/commands/integration-host-ide.ts +18 -15
  15. package/src/commands/integration-install.ts +100 -5
  16. package/src/commands/onboard.ts +80 -15
  17. package/src/detect/build-tool.ts +212 -15
  18. package/src/detect/framework.ts +3 -0
  19. package/src/detect/package-manager.ts +1 -1
  20. package/src/index.ts +1 -0
  21. package/src/inject/gitignore.ts +13 -2
  22. package/src/instructions.ts +33 -7
  23. package/src/onboarding/apply.ts +255 -28
  24. package/src/onboarding/nextjs-guidance.ts +257 -0
  25. package/src/onboarding/nuxt-guidance.ts +129 -0
  26. package/src/onboarding/planner.ts +337 -10
  27. package/src/onboarding/session.ts +127 -31
  28. package/src/onboarding/target-resolution.ts +79 -3
  29. package/src/onboarding/umi-guidance.ts +139 -0
  30. package/src/types.ts +58 -3
  31. package/tests/apply.test.ts +553 -0
  32. package/tests/build-tool.test.ts +199 -0
  33. package/tests/dev-config.test.ts +73 -0
  34. package/tests/doctor.test.ts +130 -0
  35. package/tests/init.test.ts +17 -0
  36. package/tests/install-wrapper.test.ts +56 -0
  37. package/tests/instructions.test.ts +10 -6
  38. package/tests/integration-host-ide.test.ts +20 -0
  39. package/tests/integration-install.test.ts +193 -0
  40. package/tests/nextjs-guidance.test.ts +128 -0
  41. package/tests/nuxt-guidance.test.ts +67 -0
  42. package/tests/onboard.test.ts +511 -0
  43. package/tests/plan.test.ts +283 -21
  44. package/tests/runner-script.test.ts +120 -1
  45. package/tests/session-resolve.test.ts +116 -0
  46. package/tests/session.test.ts +120 -0
@@ -6,10 +6,20 @@ import { readFile, writeFile } from '../utils/fs.js'
6
6
  import { log } from '../utils/logger.js'
7
7
 
8
8
  /** Rules for default mode: fine-grained rules only */
9
- const DEFAULT_RULES = ['.inspecto/install.lock', '.inspecto/cache.json', '.inspecto/*.local.json']
9
+ const DEFAULT_RULES = [
10
+ '.inspecto/install.lock',
11
+ '.inspecto/cache.json',
12
+ '.inspecto/*.local.json',
13
+ '.inspecto/dev.json',
14
+ ]
10
15
 
11
16
  /** Rules for shared mode: same as default in current design to allow settings.json */
12
- const SHARED_RULES = ['.inspecto/install.lock', '.inspecto/cache.json', '.inspecto/*.local.json']
17
+ const SHARED_RULES = [
18
+ '.inspecto/install.lock',
19
+ '.inspecto/cache.json',
20
+ '.inspecto/*.local.json',
21
+ '.inspecto/dev.json',
22
+ ]
13
23
 
14
24
  /**
15
25
  * Update .gitignore based on the init mode.
@@ -77,6 +87,7 @@ export async function cleanGitignore(root: string): Promise<void> {
77
87
  .replace(/^\.inspecto\/install\.lock\s*$/gm, '')
78
88
  .replace(/^\.inspecto\/cache\.json\s*$/gm, '')
79
89
  .replace(/^\.inspecto\/\*\.local\.json\s*$/gm, '')
90
+ .replace(/^\.inspecto\/dev\.json\s*$/gm, '')
80
91
  .replace(/\n{3,}/g, '\n\n') // Collapse excess blank lines
81
92
 
82
93
  await writeFile(gitignorePath, cleaned)
@@ -2,7 +2,9 @@ import { log } from './utils/logger.js'
2
2
 
3
3
  export function printNuxtManualInstructions() {
4
4
  log.blank()
5
- log.hint('Nuxt requires manual setup in the current version.')
5
+ log.hint(
6
+ 'Nuxt supports guided setup in the current version. Inspecto can prepare the config patch, but the client plugin mount step still needs review.',
7
+ )
6
8
  log.hint('1. Update `nuxt.config.ts` to register the Inspecto Vite plugin:')
7
9
  log.copyableCodeBlock([
8
10
  "import { vitePlugin as inspecto } from '@inspecto-dev/plugin'",
@@ -13,7 +15,7 @@ export function printNuxtManualInstructions() {
13
15
  ' },',
14
16
  '})',
15
17
  ])
16
- log.hint('2. Create `plugins/inspecto.client.ts` to mount `@inspecto-dev/core` in development:')
18
+ log.hint('2. Complete the remaining client plugin mount step in `plugins/inspecto.client.ts`:')
17
19
  log.copyableCodeBlock([
18
20
  'export default defineNuxtPlugin(() => {',
19
21
  ' if (import.meta.dev) {',
@@ -23,12 +25,33 @@ export function printNuxtManualInstructions() {
23
25
  ' }',
24
26
  '})',
25
27
  ])
26
- log.hint('3. Restart your Nuxt dev server after updating the config.')
28
+ log.hint('3. Restart your Nuxt dev server after applying the guided patches.')
29
+ }
30
+
31
+ export function printUmiManualInstructions() {
32
+ log.blank()
33
+ log.hint('Umi supports guided setup in the current version.')
34
+ log.hint('1. Update `config/config.ts` or `.umirc.ts` to register the Inspecto webpack plugin:')
35
+ log.copyableCodeBlock([
36
+ "import { defineConfig } from 'umi'",
37
+ "import { webpack4Plugin } from '@inspecto-dev/plugin/legacy/webpack4'",
38
+ '',
39
+ 'export default defineConfig({',
40
+ ' chainWebpack(memo) {',
41
+ " if (process.env.NODE_ENV === 'development') {",
42
+ " memo.plugin('inspecto').use(webpack4Plugin())",
43
+ ' }',
44
+ ' },',
45
+ '})',
46
+ ])
47
+ log.hint('2. Restart your Umi dev server after applying the guided patches.')
27
48
  }
28
49
 
29
50
  export function printNextJsManualInstructions() {
30
51
  log.blank()
31
- log.hint('Next.js requires manual setup in the current version.')
52
+ log.hint(
53
+ 'Next.js supports guided setup in the current version. Inspecto can prepare the config patch, but the client-side mount step still needs review.',
54
+ )
32
55
  log.hint('1. Update `next.config.mjs` to register the Inspecto webpack plugin:')
33
56
  log.copyableCodeBlock([
34
57
  "import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'",
@@ -36,7 +59,7 @@ export function printNextJsManualInstructions() {
36
59
  "/** @type {import('next').NextConfig} */",
37
60
  'const nextConfig = {',
38
61
  ' webpack: (config, { dev, isServer }) => {',
39
- ' if (dev && !isServer) {',
62
+ ' if (dev) {',
40
63
  ' config.plugins.push(inspecto())',
41
64
  ' }',
42
65
  ' return config',
@@ -46,7 +69,10 @@ export function printNextJsManualInstructions() {
46
69
  'export default nextConfig',
47
70
  ])
48
71
  log.hint(
49
- '2. Initialize `@inspecto-dev/core` from a client component such as `app/layout.tsx` or `pages/_app.tsx`:',
72
+ 'Keep the plugin enabled for both server and client development compilers so App Router server components also receive Inspecto transforms.',
73
+ )
74
+ log.hint(
75
+ '2. Complete the remaining client-side mount step in `app/layout.tsx` or `pages/_app.tsx`:',
50
76
  )
51
77
  log.copyableCodeBlock([
52
78
  "'use client'",
@@ -65,5 +91,5 @@ export function printNextJsManualInstructions() {
65
91
  ' return <html><body>{children}</body></html>',
66
92
  '}',
67
93
  ])
68
- log.hint('3. Restart your Next.js dev server after updating the config.')
94
+ log.hint('3. Restart your Next.js dev server after applying the guided patches.')
69
95
  }
@@ -5,7 +5,7 @@ import { injectPlugin } from '../inject/ast-injector.js'
5
5
  import { installExtension } from '../inject/extension.js'
6
6
  import { updateGitignore } from '../inject/gitignore.js'
7
7
  import { shell } from '../utils/exec.js'
8
- import { exists, readJSON, writeJSON } from '../utils/fs.js'
8
+ import { exists, readFile, readJSON, writeFile, writeJSON } from '../utils/fs.js'
9
9
  import { log } from '../utils/logger.js'
10
10
  import type {
11
11
  BuildToolDetection,
@@ -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,141 @@ 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 =>
137
+ ['manual_step', 'generate_patch_plan', 'generate_file', 'manual_confirmation'].includes(
138
+ action.type,
139
+ ),
140
+ )
141
+ .map(action => action.description)
142
+
143
+ if (!includeBlockers) {
144
+ return steps
145
+ }
146
+
147
+ return [...plan.blockers.map(blocker => blocker.message), ...steps]
148
+ }
149
+
150
+ function applyGuidedPatchContent(source: string, snippet: string): string {
151
+ if (source.includes("import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'")) {
152
+ return source
153
+ }
154
+ if (source.includes("import { vitePlugin as inspecto } from '@inspecto-dev/plugin'")) {
155
+ return source
156
+ }
157
+
158
+ const trimmedSource = source.trimEnd()
159
+ if (/export\s+default\s*\{/m.test(source)) {
160
+ const nextSource = trimmedSource.replace(
161
+ /export\s+default\s*\{/m,
162
+ "import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'\n\nexport default {\n webpack(config, { dev, isServer }) {\n if (dev) {\n config.plugins.push(inspecto())\n }\n return config\n },",
163
+ )
164
+ return `${nextSource}\n`
165
+ }
166
+
167
+ if (/module\.exports\s*=\s*\{/m.test(source)) {
168
+ const nextSource = trimmedSource.replace(
169
+ /module\.exports\s*=\s*\{/m,
170
+ "const { webpackPlugin: inspecto } = require('@inspecto-dev/plugin')\n\nmodule.exports = {\n webpack(config, { dev, isServer }) {\n if (dev) {\n config.plugins.push(inspecto())\n }\n return config\n },",
171
+ )
172
+ return `${nextSource}\n`
173
+ }
174
+
175
+ const objectExportVariableMatch = source.match(
176
+ /(const\s+([A-Za-z0-9_$]+)\s*(?::[^=]+)?=\s*\{[\s\S]*?\}\s*;?\s*)export\s+default\s+\2\s*;?/m,
177
+ )
178
+ const variableDeclaration = objectExportVariableMatch?.[1]
179
+ const variableName = objectExportVariableMatch?.[2]
180
+ if (variableDeclaration && variableName) {
181
+ const nextDeclaration = variableDeclaration.replace(
182
+ /=\s*\{/m,
183
+ '= {\n webpack(config, { dev, isServer }) {\n if (dev) {\n config.plugins.push(inspecto())\n }\n return config\n },',
184
+ )
185
+ const nextSource = source.replace(
186
+ objectExportVariableMatch[0],
187
+ `import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'\n\n${nextDeclaration}export default ${variableName}`,
188
+ )
189
+ return `${nextSource.trimEnd()}\n`
190
+ }
191
+
192
+ if (/defineNuxtConfig\s*\(\s*\{/m.test(source)) {
193
+ const trimmedSource = source.trimEnd()
194
+ const nextSource = trimmedSource.replace(
195
+ /defineNuxtConfig\s*\(\s*\{/m,
196
+ 'defineNuxtConfig({\n vite: {\n plugins: [inspecto()],\n },',
197
+ )
198
+ return `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'\n\n${nextSource}\n`
199
+ }
200
+
201
+ const jsdocMatch = source.match(
202
+ /\/\*\*[\s\S]*?@type\s*\{import\('next'\)\.NextConfig\}[\s\S]*?\*\/[\s\S]*?(export\s+default|module\.exports)\s*=?\s*\{/m,
203
+ )
204
+ if (jsdocMatch) {
205
+ const isEsm = jsdocMatch[1] === 'export default'
206
+ const importStatement = isEsm
207
+ ? "import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'\n\n"
208
+ : "const { webpackPlugin: inspecto } = require('@inspecto-dev/plugin')\n\n"
209
+
210
+ const replacementPattern = isEsm ? /export\s+default\s*\{/m : /module\.exports\s*=\s*\{/m
211
+
212
+ const injectConfig = isEsm
213
+ ? 'export default {\n webpack(config, { dev, isServer }) {\n if (dev) {\n config.plugins.push(inspecto())\n }\n return config\n },'
214
+ : 'module.exports = {\n webpack(config, { dev, isServer }) {\n if (dev) {\n config.plugins.push(inspecto())\n }\n return config\n },'
215
+
216
+ const nextSource = source.replace(replacementPattern, injectConfig)
217
+ return `${importStatement}${nextSource.trimEnd()}\n`
218
+ }
219
+
220
+ return `${trimmedSource}\n\n${snippet}\n`
221
+ }
222
+
223
+ async function applyGuidedPlanPatches(
224
+ input: ApplyOnboardingInput,
225
+ mutations: Mutation[],
226
+ reporter: ApplyReporter,
227
+ ): Promise<string[]> {
228
+ const nextSteps: string[] = []
229
+
230
+ if (!input.plan || input.plan.strategy !== 'guided' || !input.plan.patches?.length) {
231
+ return nextSteps
232
+ }
233
+
234
+ for (const patch of input.plan.patches) {
235
+ if (
236
+ patch.status !== 'planned' ||
237
+ (!patch.reason.startsWith('next_config_') && !patch.reason.startsWith('nuxt_config_'))
238
+ ) {
239
+ continue
240
+ }
241
+
242
+ const patchPath = path.join(input.projectRoot, patch.path)
243
+ const existingContent = await readFile(patchPath)
244
+ if (existingContent === null) {
245
+ nextSteps.push(`Could not auto-apply the guided patch for ${patch.path}.`)
246
+ continue
247
+ }
248
+
249
+ const nextContent = applyGuidedPatchContent(existingContent, patch.snippet)
250
+ if (nextContent === existingContent) {
251
+ continue
252
+ }
253
+
254
+ if (input.options.dryRun) {
255
+ reporter.dryRun(`Would apply guided patch to ${patch.path}`)
256
+ continue
257
+ }
258
+
259
+ await writeFile(patchPath, nextContent)
260
+ mutations.push({
261
+ type: 'file_modified',
262
+ path: patch.path,
263
+ description: 'Automatically configured Inspecto guided Next.js patch',
264
+ })
265
+ reporter.success(`Applied guided patch to ${patch.path}`)
266
+ }
267
+
268
+ return nextSteps
101
269
  }
102
270
 
103
271
  export async function applyOnboardingPlan(
@@ -163,6 +331,10 @@ async function applyOnboardingPlanInternal(
163
331
  input: ApplyOnboardingInput,
164
332
  ): Promise<ApplyOnboardingResult> {
165
333
  const reporter = createReporter(input.options.quiet)
334
+ const additiveManualPlan =
335
+ (input.plan?.strategy === 'manual' || input.plan?.strategy === 'guided') &&
336
+ input.allowManualPlanApply &&
337
+ input.plan.blockers.length === 0
166
338
 
167
339
  if (input.plan && input.plan.strategy !== 'supported' && !input.allowManualPlanApply) {
168
340
  return {
@@ -172,7 +344,7 @@ async function applyOnboardingPlanInternal(
172
344
  installFailed: false,
173
345
  injectionFailed: false,
174
346
  manualExtensionInstallNeeded: false,
175
- nextSteps: manualPlanSteps(input.plan),
347
+ nextSteps: manualPlanSteps(input.plan, true),
176
348
  },
177
349
  }
178
350
  }
@@ -183,6 +355,11 @@ async function applyOnboardingPlanInternal(
183
355
  const promptsFileName = input.options.shared ? 'prompts.json' : 'prompts.local.json'
184
356
  const settingsPath = path.join(settingsDir, settingsFileName)
185
357
  const promptsPath = path.join(settingsDir, promptsFileName)
358
+ const inheritedDefaults = await readInheritedSettingsDefaults(
359
+ input.repoRoot,
360
+ input.projectRoot,
361
+ settingsFileName,
362
+ )
186
363
  const runtimePackages = resolveRuntimePackages()
187
364
  const installCmd = getInstallCommand(input.packageManager, runtimePackages.installSpec)
188
365
  const nextSteps: string[] = []
@@ -209,6 +386,11 @@ async function applyOnboardingPlanInternal(
209
386
  spinner.fail('Dependency installation failed')
210
387
  installFailed = true
211
388
  reporter.error(`Failed to install dependency: ${error?.message || 'Unknown error'}`)
389
+ if (error?.stderr) {
390
+ reporter.error(`Details: ${error.stderr}`)
391
+ } else if (error?.stdout) {
392
+ reporter.error(`Details: ${error.stdout}`)
393
+ }
212
394
  reporter.hint(`Run manually in ${input.projectRoot}: ${installCmd}`)
213
395
  reporter.hint(
214
396
  'Setup will continue without dependencies, but Inspecto may not run until installation succeeds.',
@@ -217,20 +399,26 @@ async function applyOnboardingPlanInternal(
217
399
  }
218
400
 
219
401
  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
402
+ if (!additiveManualPlan) {
403
+ for (const target of input.supportedBuildTargets) {
404
+ const result = await injectPlugin(
405
+ input.repoRoot,
406
+ target,
407
+ input.options.dryRun,
408
+ input.options.quiet ?? false,
409
+ )
410
+ if (result.success) {
411
+ mutations.push(...result.mutations)
412
+ } else {
413
+ injectionFailed = true
414
+ }
231
415
  }
232
416
  }
233
417
 
418
+ if (additiveManualPlan) {
419
+ nextSteps.push(...(await applyGuidedPlanPatches(input, mutations, reporter)))
420
+ }
421
+
234
422
  if (await exists(settingsPath)) {
235
423
  const existingSettings = await readJSON(settingsPath)
236
424
  if (existingSettings === null) {
@@ -238,20 +426,56 @@ async function applyOnboardingPlanInternal(
238
426
  reporter.hint('Please fix the syntax errors manually, or delete it and re-run init')
239
427
  nextSteps.push(`Fix .inspecto/${settingsFileName} or delete it and rerun Inspecto setup.`)
240
428
  } else {
241
- reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`)
429
+ const mergedSettings =
430
+ existingSettings && typeof existingSettings === 'object'
431
+ ? { ...(existingSettings as Record<string, unknown>) }
432
+ : {}
433
+ let settingsChanged = false
434
+
435
+ const desiredIde =
436
+ inheritedDefaults.ide ??
437
+ (input.selectedIDE?.supported ? normalizeSupportedIde(input.selectedIDE.ide) : undefined)
438
+
439
+ if (desiredIde && !mergedSettings.ide) {
440
+ mergedSettings.ide = desiredIde
441
+ settingsChanged = true
442
+ }
443
+
444
+ const desiredProviderDefault = inheritedDefaults.providerDefault ?? input.providerDefault
445
+ if (desiredProviderDefault && !mergedSettings['provider.default']) {
446
+ mergedSettings['provider.default'] = desiredProviderDefault
447
+ settingsChanged = true
448
+ }
449
+
450
+ if (settingsChanged) {
451
+ if (input.options.dryRun) {
452
+ reporter.dryRun(`Would update .inspecto/${settingsFileName}`)
453
+ } else {
454
+ await writeJSON(settingsPath, mergedSettings)
455
+ reporter.success(`Updated .inspecto/${settingsFileName} with missing defaults`)
456
+ mutations.push({
457
+ type: 'file_modified',
458
+ path: `.inspecto/${settingsFileName}`,
459
+ description: 'Merged missing Inspecto defaults into existing settings',
460
+ })
461
+ }
462
+ } else {
463
+ reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`)
464
+ }
242
465
  }
243
466
  } else {
244
467
  const defaultSettings: Record<string, unknown> = {}
468
+ const desiredIde =
469
+ inheritedDefaults.ide ??
470
+ (input.selectedIDE?.supported ? normalizeSupportedIde(input.selectedIDE.ide) : undefined)
471
+ const desiredProviderDefault = inheritedDefaults.providerDefault ?? input.providerDefault
245
472
 
246
- if (input.selectedIDE?.supported) {
247
- defaultSettings.ide =
248
- input.selectedIDE.ide.toLowerCase() === 'vscode'
249
- ? 'vscode'
250
- : input.selectedIDE.ide.toLowerCase()
473
+ if (desiredIde) {
474
+ defaultSettings.ide = desiredIde
251
475
  }
252
476
 
253
- if (input.providerDefault) {
254
- defaultSettings['provider.default'] = input.providerDefault
477
+ if (desiredProviderDefault) {
478
+ defaultSettings['provider.default'] = desiredProviderDefault
255
479
  }
256
480
 
257
481
  if (input.options.dryRun) {
@@ -340,6 +564,9 @@ async function applyOnboardingPlanInternal(
340
564
  if (manualExtensionInstallNeeded) {
341
565
  nextSteps.push('Install the Inspecto IDE extension manually')
342
566
  }
567
+ if (additiveManualPlan && input.plan) {
568
+ nextSteps.push(...manualPlanSteps(input.plan, false))
569
+ }
343
570
  if (input.manualConfigRequiredFor === 'Nuxt') {
344
571
  nextSteps.push(
345
572
  'Nuxt detected—please follow the Nuxt instructions printed above to finish setup.',