@inspecto-dev/cli 0.3.4 → 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 (44) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-test.log +10036 -5601
  3. package/CHANGELOG.md +8 -0
  4. package/dist/bin.js +32 -1
  5. package/dist/{chunk-2MOEVONN.js → chunk-7ABJRH3F.js} +1232 -145
  6. package/dist/index.d.ts +62 -4
  7. package/dist/index.js +7 -1
  8. package/package.json +2 -2
  9. package/src/bin.ts +45 -0
  10. package/src/commands/dev-config.ts +109 -0
  11. package/src/commands/doctor.ts +162 -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 +1 -1
  16. package/src/commands/onboard.ts +72 -21
  17. package/src/detect/build-tool.ts +14 -5
  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 +137 -3
  24. package/src/onboarding/nextjs-guidance.ts +257 -0
  25. package/src/onboarding/nuxt-guidance.ts +129 -0
  26. package/src/onboarding/planner.ts +257 -6
  27. package/src/onboarding/session.ts +117 -27
  28. package/src/onboarding/target-resolution.ts +3 -3
  29. package/src/onboarding/umi-guidance.ts +139 -0
  30. package/src/types.ts +51 -3
  31. package/tests/apply.test.ts +319 -0
  32. package/tests/dev-config.test.ts +73 -0
  33. package/tests/doctor.test.ts +89 -0
  34. package/tests/init.test.ts +17 -0
  35. package/tests/instructions.test.ts +10 -6
  36. package/tests/integration-host-ide.test.ts +20 -0
  37. package/tests/integration-install.test.ts +65 -0
  38. package/tests/nextjs-guidance.test.ts +128 -0
  39. package/tests/nuxt-guidance.test.ts +67 -0
  40. package/tests/onboard.test.ts +416 -0
  41. package/tests/plan.test.ts +181 -21
  42. package/tests/runner-script.test.ts +120 -1
  43. package/tests/session-resolve.test.ts +116 -0
  44. package/tests/session.test.ts +120 -0
@@ -5,7 +5,7 @@ import {
5
5
  } from '../onboarding/session.js'
6
6
  import { log } from '../utils/logger.js'
7
7
  import { writeCommandOutput } from '../utils/output.js'
8
- import type { OnboardCommandResult } from '../types.js'
8
+ import type { OnboardCommandResult, OnboardingAssistantHandoff } from '../types.js'
9
9
 
10
10
  export interface OnboardCommandOptions {
11
11
  json?: boolean
@@ -34,47 +34,98 @@ function printManualExtensionGuidance(result: OnboardCommandResult): void {
34
34
  }
35
35
  }
36
36
 
37
+ function buildAssistantHandoff(
38
+ result: OnboardCommandResult,
39
+ ): OnboardingAssistantHandoff | undefined {
40
+ if (
41
+ !result.framework &&
42
+ !result.metaFramework &&
43
+ !result.routerMode &&
44
+ !result.autoApplied &&
45
+ !result.pendingSteps &&
46
+ !result.assistantPrompt &&
47
+ !result.patches
48
+ ) {
49
+ return result.handoff
50
+ }
51
+
52
+ return {
53
+ ...(result.framework ? { framework: result.framework } : {}),
54
+ ...(result.metaFramework ? { metaFramework: result.metaFramework } : {}),
55
+ ...(result.routerMode ? { routerMode: result.routerMode } : {}),
56
+ ...(result.autoApplied ? { autoApplied: result.autoApplied } : {}),
57
+ ...(result.pendingSteps ? { pendingSteps: result.pendingSteps } : {}),
58
+ ...(result.assistantPrompt ? { assistantPrompt: result.assistantPrompt } : {}),
59
+ ...(result.patches ? { patches: result.patches } : {}),
60
+ }
61
+ }
62
+
63
+ function normalizeOnboardResult(result: OnboardCommandResult): OnboardCommandResult {
64
+ const handoff = buildAssistantHandoff(result)
65
+ if (!handoff) {
66
+ return result
67
+ }
68
+
69
+ return {
70
+ ...result,
71
+ handoff,
72
+ }
73
+ }
74
+
75
+ function collectDisplayNextSteps(result: OnboardCommandResult): string[] {
76
+ return Array.from(
77
+ new Set([...(result.diagnostics?.nextSteps ?? []), ...(result.handoff?.pendingSteps ?? [])]),
78
+ )
79
+ }
80
+
37
81
  function printOnboardResult(result: OnboardCommandResult): void {
82
+ const normalized = normalizeOnboardResult(result)
38
83
  log.header('Inspecto Onboard')
39
- log.info(`Status: ${result.status}`)
40
- log.info(result.summary.headline)
84
+ log.info(`Status: ${normalized.status}`)
85
+ log.info(normalized.summary.headline)
41
86
 
42
- if (result.status === 'needs_target_selection') {
43
- if (result.target.selectionPurpose) {
44
- log.warn(result.target.selectionPurpose)
87
+ if (normalized.status === 'needs_target_selection') {
88
+ if (normalized.target.selectionPurpose) {
89
+ log.warn(normalized.target.selectionPurpose)
45
90
  }
46
- for (const candidate of result.target.candidates) {
91
+ for (const candidate of normalized.target.candidates) {
47
92
  const identifier = candidate.candidateId ?? candidate.id ?? candidate.configPath
48
93
  const label = candidate.label ?? candidate.configPath
49
94
  log.hint(`${identifier}: ${label}`)
50
95
  }
51
- if (result.target.selectionInstructions) {
52
- log.hint(result.target.selectionInstructions)
96
+ if (normalized.target.selectionInstructions) {
97
+ log.hint(normalized.target.selectionInstructions)
53
98
  }
54
99
  }
55
100
 
56
- for (const change of result.summary.changes) {
101
+ for (const change of normalized.summary.changes) {
57
102
  log.hint(change)
58
103
  }
59
- for (const step of result.diagnostics?.nextSteps ?? []) {
104
+ for (const step of collectDisplayNextSteps(normalized)) {
60
105
  log.warn(step)
61
106
  }
62
- if (result.confirmation.required && result.confirmation.question) {
63
- log.warn(result.confirmation.question)
107
+ for (const patch of normalized.handoff?.patches ?? []) {
108
+ log.hint(`Patch target: ${patch.path} (${patch.reason})`)
109
+ }
110
+ if (normalized.handoff?.assistantPrompt) {
111
+ log.hint(normalized.handoff.assistantPrompt)
112
+ }
113
+ if (normalized.confirmation.required && normalized.confirmation.question) {
114
+ log.warn(normalized.confirmation.question)
64
115
  }
65
116
 
66
- printManualExtensionGuidance(result)
117
+ printManualExtensionGuidance(normalized)
67
118
 
68
119
  const extensionReady =
69
- !result.ideExtension?.required ||
70
- (result.ideExtension.installed && !result.ideExtension.manualRequired)
120
+ !normalized.ideExtension?.required ||
121
+ (normalized.ideExtension.installed && !normalized.ideExtension.manualRequired)
71
122
 
72
123
  if (
73
124
  extensionReady &&
74
- (result.status === 'success' || result.status === 'partial_success') &&
75
- result.verification?.message
125
+ (normalized.status === 'success' || normalized.status === 'partial_success') &&
126
+ normalized.verification?.message
76
127
  ) {
77
- log.info(result.verification.message)
128
+ log.info(normalized.verification.message)
78
129
  }
79
130
  }
80
131
 
@@ -88,12 +139,12 @@ export async function onboard(options: OnboardCommandOptions = {}): Promise<Onbo
88
139
  session.status === 'needs_confirmation'
89
140
  ) {
90
141
  return writeCommandOutput(
91
- buildDeferredOnboardResult(session),
142
+ normalizeOnboardResult(buildDeferredOnboardResult(session)),
92
143
  options.json ?? false,
93
144
  printOnboardResult,
94
145
  )
95
146
  }
96
147
 
97
- const result = await applyResolvedOnboardingSession(session, options)
148
+ const result = normalizeOnboardResult(await applyResolvedOnboardingSession(session, options))
98
149
  return writeCommandOutput(result, options.json ?? false, printOnboardResult)
99
150
  }
@@ -139,6 +139,11 @@ const SUPPORTED_PATTERNS: { tool: BuildTool; files: string[]; label: string }[]
139
139
 
140
140
  /** Recognized but unsupported meta-frameworks — detect via dep + config file */
141
141
  const UNSUPPORTED_META: { name: string; dep: string; files: string[] }[] = [
142
+ {
143
+ name: 'Umi',
144
+ dep: 'umi',
145
+ files: ['.umirc.ts', '.umirc.js', 'config/config.ts', 'config/config.js'],
146
+ },
142
147
  { name: 'Next.js', dep: 'next', files: ['next.config.mjs', 'next.config.js', 'next.config.ts'] },
143
148
  { name: 'Nuxt', dep: 'nuxt', files: ['nuxt.config.ts', 'nuxt.config.js'] },
144
149
  { name: 'Remix', dep: '@remix-run/dev', files: ['remix.config.js', 'remix.config.ts'] },
@@ -297,7 +302,9 @@ export async function detectBuildTools(
297
302
  }
298
303
 
299
304
  const unsupportedChecks = UNSUPPORTED_META.map(async meta => {
300
- if (!(meta.dep in allDeps)) return null
305
+ // Check if any dependency matches (supports exact match or scoped packages if necessary, but here we do exact match)
306
+ const hasDep = meta.dep in allDeps || Object.keys(allDeps).some(dep => dep.includes(meta.dep))
307
+ if (!hasDep) return null
301
308
  for (const file of meta.files) {
302
309
  if (await exists(path.join(target.absolutePath, file))) {
303
310
  return meta.name
@@ -471,6 +478,8 @@ async function detectPattern({
471
478
  }
472
479
  } else if (pattern.tool === 'rsbuild') {
473
480
  hasDep = !!allDeps['@rsbuild/core'] || isPackageResolvable('@rsbuild/core', targetRoot)
481
+ } else if (pattern.tool === 'vite') {
482
+ hasDep = !!allDeps['vite'] || isPackageResolvable('vite', targetRoot)
474
483
  } else {
475
484
  hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, targetRoot)
476
485
  }
@@ -576,7 +585,7 @@ async function detectPattern({
576
585
  tool: pattern.tool,
577
586
  configPath: 'package.json (dependency)',
578
587
  label: `${pattern.label} (detected via dependency)`,
579
- packagePath: packagePath || undefined,
588
+ ...(packagePath ? { packagePath } : {}),
580
589
  }
581
590
  }
582
591
  return null
@@ -604,9 +613,9 @@ async function detectPattern({
604
613
  label: `${pattern.label} (${relativeConfig})${isLegacyRspack ? ' [Legacy]' : ''}${
605
614
  isLegacyWebpack ? ' [Webpack 4]' : ''
606
615
  }${inferredFromScripts ? ' [Scripts Detected]' : ''}`,
607
- isLegacyRspack,
608
- isLegacyWebpack,
609
- packagePath: packagePath || undefined,
616
+ ...(isLegacyRspack ? { isLegacyRspack: true } : {}),
617
+ ...(isLegacyWebpack ? { isLegacyWebpack: true } : {}),
618
+ ...(packagePath ? { packagePath } : {}),
610
619
  }
611
620
  }
612
621
 
@@ -44,9 +44,12 @@ const SUPPORTED_FRAMEWORKS: { framework: Framework; deps: string[] }[] = [
44
44
  const UNSUPPORTED_FRAMEWORKS: { name: string; dep: string }[] = [
45
45
  { name: 'Solid', dep: 'solid-js' },
46
46
  { name: 'Svelte', dep: 'svelte' },
47
+ { name: 'SvelteKit', dep: '@sveltejs/kit' },
47
48
  { name: 'Angular', dep: '@angular/core' },
48
49
  { name: 'Preact', dep: 'preact' },
49
50
  { name: 'Lit', dep: 'lit' },
51
+ { name: 'Qwik', dep: 'qwik' },
52
+ { name: 'Alpine', dep: 'lit-html' },
50
53
  ]
51
54
 
52
55
  /**
@@ -14,8 +14,8 @@ export async function detectPackageManager(root: string): Promise<PackageManager
14
14
  ['bun.lockb', 'bun'],
15
15
  ['bun.lock', 'bun'],
16
16
  ['pnpm-lock.yaml', 'pnpm'],
17
- ['yarn.lock', 'yarn'],
18
17
  ['package-lock.json', 'npm'],
18
+ ['yarn.lock', 'yarn'],
19
19
  ]
20
20
 
21
21
  const results = await Promise.all(
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { apply } from './commands/apply.js'
2
2
  export { detect } from './commands/detect.js'
3
+ export { devLink, devStatus, devUnlink } from './commands/dev-config.js'
3
4
  export { init } from './commands/init.js'
4
5
  export { collectDoctorResult, doctor } from './commands/doctor.js'
5
6
  export { integrationDoctor } from './commands/integration-doctor.js'
@@ -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,
@@ -133,7 +133,11 @@ function resultStatus(nextSteps: string[]): CommandStatus {
133
133
 
134
134
  function manualPlanSteps(plan: PlanResult, includeBlockers = true): string[] {
135
135
  const steps = plan.actions
136
- .filter(action => action.type === 'manual_step')
136
+ .filter(action =>
137
+ ['manual_step', 'generate_patch_plan', 'generate_file', 'manual_confirmation'].includes(
138
+ action.type,
139
+ ),
140
+ )
137
141
  .map(action => action.description)
138
142
 
139
143
  if (!includeBlockers) {
@@ -143,6 +147,127 @@ function manualPlanSteps(plan: PlanResult, includeBlockers = true): string[] {
143
147
  return [...plan.blockers.map(blocker => blocker.message), ...steps]
144
148
  }
145
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
269
+ }
270
+
146
271
  export async function applyOnboardingPlan(
147
272
  input: ApplyOnboardingInput,
148
273
  ): Promise<ApplyOnboardingResult> {
@@ -207,7 +332,7 @@ async function applyOnboardingPlanInternal(
207
332
  ): Promise<ApplyOnboardingResult> {
208
333
  const reporter = createReporter(input.options.quiet)
209
334
  const additiveManualPlan =
210
- input.plan?.strategy === 'manual' &&
335
+ (input.plan?.strategy === 'manual' || input.plan?.strategy === 'guided') &&
211
336
  input.allowManualPlanApply &&
212
337
  input.plan.blockers.length === 0
213
338
 
@@ -261,6 +386,11 @@ async function applyOnboardingPlanInternal(
261
386
  spinner.fail('Dependency installation failed')
262
387
  installFailed = true
263
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
+ }
264
394
  reporter.hint(`Run manually in ${input.projectRoot}: ${installCmd}`)
265
395
  reporter.hint(
266
396
  'Setup will continue without dependencies, but Inspecto may not run until installation succeeds.',
@@ -285,6 +415,10 @@ async function applyOnboardingPlanInternal(
285
415
  }
286
416
  }
287
417
 
418
+ if (additiveManualPlan) {
419
+ nextSteps.push(...(await applyGuidedPlanPatches(input, mutations, reporter)))
420
+ }
421
+
288
422
  if (await exists(settingsPath)) {
289
423
  const existingSettings = await readJSON(settingsPath)
290
424
  if (existingSettings === null) {