@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
@@ -2,6 +2,11 @@ import path from 'node:path'
2
2
  import { detectFrameworks } from '../detect/framework.js'
3
3
  import { detectIDE } from '../detect/ide.js'
4
4
  import { detectProviders } from '../detect/provider.js'
5
+ import {
6
+ getHostIdeBinaryName,
7
+ isSupportedHostIde,
8
+ type SupportedHostIde,
9
+ } from '../integrations/capabilities.js'
5
10
  import { applyOnboardingPlan, type ApplyOnboardingResult } from './apply.js'
6
11
  import { buildOnboardingContext } from './context.js'
7
12
  import { createPlanResult, planManualFollowUp } from './planner.js'
@@ -61,9 +66,10 @@ async function buildVerification(
61
66
  projectRoot: string,
62
67
  packageManager: OnboardingContext['packageManager'],
63
68
  ): Promise<OnboardingVerification> {
64
- const packageJson = await readJSON<{ scripts?: Record<string, string> }>(
65
- path.join(projectRoot, 'package.json'),
66
- )
69
+ const packageJson = await readJSON<{
70
+ scripts?: Record<string, string>
71
+ dependencies?: Record<string, string>
72
+ }>(path.join(projectRoot, 'package.json'))
67
73
 
68
74
  if (packageJson?.scripts?.dev) {
69
75
  const devCommand = getVerificationCommand(packageManager)
@@ -74,13 +80,34 @@ async function buildVerification(
74
80
  }
75
81
  }
76
82
 
83
+ if (packageJson?.scripts?.start && packageJson?.dependencies?.next) {
84
+ const devCommand = packageManager === 'bun' ? 'bunx next dev' : 'npx next dev'
85
+ return {
86
+ available: true,
87
+ devCommand,
88
+ message: `Start the local dev server with \`${devCommand}\` to verify Inspecto in the browser.`,
89
+ }
90
+ }
91
+
77
92
  return {
78
93
  available: false,
79
94
  message: 'Start your normal local dev server command to verify Inspecto in the browser.',
80
95
  }
81
96
  }
82
97
 
98
+ function buildExtensionInstallCommand(ide?: string): string {
99
+ if (ide && isSupportedHostIde(ide)) {
100
+ const binaryName = getHostIdeBinaryName(ide as SupportedHostIde)
101
+ if (binaryName) {
102
+ return `${binaryName} --install-extension inspecto.inspecto`
103
+ }
104
+ }
105
+
106
+ return 'code --install-extension inspecto.inspecto'
107
+ }
108
+
83
109
  function buildIdeExtensionStatus(input: {
110
+ ide?: string
84
111
  required: boolean
85
112
  installed: boolean
86
113
  manualRequired: boolean
@@ -97,8 +124,12 @@ function buildIdeExtensionStatus(input: {
97
124
  required: true,
98
125
  installed: input.installed,
99
126
  manualRequired: input.manualRequired,
100
- installCommand: 'code --install-extension inspecto.inspecto',
101
- marketplaceUrl: 'https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto',
127
+ installCommand: buildExtensionInstallCommand(input.ide),
128
+ ...(input.ide === 'vscode'
129
+ ? {
130
+ marketplaceUrl: 'https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto',
131
+ }
132
+ : {}),
102
133
  openVsxUrl: 'https://open-vsx.org/extension/inspecto/inspecto',
103
134
  }
104
135
  }
@@ -126,8 +157,9 @@ async function detectFrameworkSupportByPackage(
126
157
 
127
158
  async function buildTargetedContext(
128
159
  rootContext: OnboardingContext,
129
- packagePath: string,
160
+ target: { id?: string; packagePath: string },
130
161
  ): Promise<OnboardingContext> {
162
+ const packagePath = normalizePackagePath(target.packagePath)
131
163
  const projectRoot = packagePath ? path.join(rootContext.root, packagePath) : rootContext.root
132
164
  const [frameworks, ides, providers] = await Promise.all([
133
165
  detectFrameworks(projectRoot),
@@ -140,7 +172,11 @@ async function buildTargetedContext(
140
172
  packageManager: rootContext.packageManager,
141
173
  buildTools: {
142
174
  supported: rootContext.buildTools.supported.filter(item => {
143
- return normalizePackagePath(item.packagePath) === packagePath
175
+ const itemPackagePath = normalizePackagePath(item.packagePath)
176
+ if (target.id) {
177
+ return `${itemPackagePath || '.'}:${item.tool}:${item.configPath}` === target.id
178
+ }
179
+ return itemPackagePath === packagePath
144
180
  }),
145
181
  unsupported: [],
146
182
  },
@@ -160,7 +196,12 @@ async function buildTargetedContext(
160
196
 
161
197
  function buildOnboardingSummary(plan: PlanResult, projectRoot: string): OnboardingSummary {
162
198
  const changes = plan.actions
163
- .filter(action => action.type !== 'manual_step')
199
+ .filter(
200
+ action =>
201
+ !['manual_step', 'generate_patch_plan', 'generate_file', 'manual_confirmation'].includes(
202
+ action.type,
203
+ ),
204
+ )
164
205
  .map(action => action.description)
165
206
  const risks = [...plan.warnings.map(item => item.message)]
166
207
  const manualFollowUp = planManualFollowUp(plan)
@@ -230,18 +271,28 @@ function buildPreApplyResult(
230
271
  }
231
272
  : undefined
232
273
 
274
+ const ideExtension = buildIdeExtensionStatus({
275
+ ...(session.selectedIDE?.ide ? { ide: session.selectedIDE.ide } : {}),
276
+ required: session.plan.defaults.extension,
277
+ installed: false,
278
+ manualRequired: session.plan.defaults.extension,
279
+ })
280
+
233
281
  return {
234
282
  status,
235
283
  target: session.target,
236
284
  summary: session.summary,
237
285
  confirmation: session.confirmation,
238
- ideExtension: buildIdeExtensionStatus({
239
- required: session.plan.defaults.extension,
240
- installed: false,
241
- manualRequired: session.plan.defaults.extension,
242
- }),
286
+ ideExtension,
243
287
  verification: session.verification,
244
- diagnostics,
288
+ ...(session.framework ? { framework: session.framework } : {}),
289
+ ...(session.metaFramework ? { metaFramework: session.metaFramework } : {}),
290
+ ...(session.routerMode ? { routerMode: session.routerMode } : {}),
291
+ ...(session.autoApplied ? { autoApplied: session.autoApplied } : {}),
292
+ ...(session.pendingSteps ? { pendingSteps: session.pendingSteps } : {}),
293
+ ...(session.assistantPrompt ? { assistantPrompt: session.assistantPrompt } : {}),
294
+ ...(session.patches ? { patches: session.patches } : {}),
295
+ ...(diagnostics ? { diagnostics } : {}),
245
296
  }
246
297
  }
247
298
 
@@ -258,8 +309,8 @@ function buildExecutionResult(
258
309
  installedDependencies: applyResult.mutations
259
310
  .map(item => item.name)
260
311
  .filter((value): value is string => !!value),
261
- selectedProviderDefault: session.providerDefault,
262
- selectedIDE: session.selectedIDE?.ide,
312
+ ...(session.providerDefault ? { selectedProviderDefault: session.providerDefault } : {}),
313
+ ...(session.selectedIDE?.ide ? { selectedIDE: session.selectedIDE.ide } : {}),
263
314
  mutations: applyResult.mutations,
264
315
  }
265
316
  }
@@ -307,27 +358,48 @@ export async function resolveOnboardingSession(
307
358
  repoRoot: root,
308
359
  buildTools: rootContext.buildTools.supported,
309
360
  frameworkSupportByPackage,
310
- selectedPackagePath: options.target,
361
+ ...(options.target ? { selectedPackagePath: options.target } : {}),
311
362
  })
312
363
 
313
- if (target.candidates.length === 0) {
364
+ const isGuided = rootContext.buildTools.unsupported.some(t =>
365
+ ['Next.js', 'Nuxt', 'Umi'].includes(t),
366
+ )
367
+
368
+ if (target.candidates.length === 0 || isGuided) {
314
369
  const plan = createPlanResult(rootContext)
370
+ const guidedStatus = plan.strategy === 'guided' ? 'partial_success' : 'error'
371
+ const resolvedTarget =
372
+ plan.strategy === 'guided'
373
+ ? {
374
+ status: 'guided' as const,
375
+ candidates: [],
376
+ reason: `Guided onboarding is available for ${plan.metaFramework ?? 'this project'}.`,
377
+ }
378
+ : target
315
379
  return {
316
- status: 'error',
317
- target,
380
+ status: guidedStatus,
381
+ target: resolvedTarget,
318
382
  summary: buildOnboardingSummary(plan, root),
319
383
  confirmation: { required: false },
320
384
  verification: rootVerification,
321
385
  context: rootContext,
322
386
  plan,
323
387
  projectRoot: root,
388
+ ...(plan.framework ? { framework: plan.framework } : {}),
389
+ ...(plan.metaFramework ? { metaFramework: plan.metaFramework } : {}),
390
+ ...(plan.routerMode ? { routerMode: plan.routerMode } : {}),
391
+ ...(plan.autoApplied ? { autoApplied: plan.autoApplied } : {}),
392
+ ...(plan.pendingSteps ? { pendingSteps: plan.pendingSteps } : {}),
393
+ ...(plan.assistantPrompt ? { assistantPrompt: plan.assistantPrompt } : {}),
394
+ ...(plan.patches ? { patches: plan.patches } : {}),
324
395
  }
325
396
  }
326
397
 
327
398
  if (target.status === 'needs_selection') {
328
399
  const plan = createPlanResult(rootContext)
329
400
  const summary: OnboardingSummary = {
330
- headline: 'Inspecto found multiple plausible app targets and needs one selection.',
401
+ headline:
402
+ 'Inspecto needs one build target selection before setup so it knows which local dev build should receive the plugin and settings.',
331
403
  changes: [],
332
404
  risks: [],
333
405
  manualFollowUp: [],
@@ -344,8 +416,7 @@ export async function resolveOnboardingSession(
344
416
  }
345
417
  }
346
418
 
347
- const packagePath = normalizePackagePath(target.selected?.packagePath)
348
- const context = await buildTargetedContext(rootContext, packagePath)
419
+ const context = await buildTargetedContext(rootContext, target.selected!)
349
420
  const verification = await buildVerification(context.root, context.packageManager)
350
421
  const plan = createPlanResult(context)
351
422
  const summary = buildOnboardingSummary(plan, context.root)
@@ -375,6 +446,11 @@ export async function resolveOnboardingSession(
375
446
  status = 'partial_success'
376
447
  }
377
448
 
449
+ const providerDefault = getProviderDefault(
450
+ plan.defaults.provider,
451
+ selectedProvider?.preferredMode,
452
+ )
453
+
378
454
  return {
379
455
  status,
380
456
  target,
@@ -385,7 +461,14 @@ export async function resolveOnboardingSession(
385
461
  plan,
386
462
  projectRoot: context.root,
387
463
  selectedIDE,
388
- providerDefault: getProviderDefault(plan.defaults.provider, selectedProvider?.preferredMode),
464
+ ...(providerDefault ? { providerDefault } : {}),
465
+ ...(plan.framework ? { framework: plan.framework } : {}),
466
+ ...(plan.metaFramework ? { metaFramework: plan.metaFramework } : {}),
467
+ ...(plan.routerMode ? { routerMode: plan.routerMode } : {}),
468
+ ...(plan.autoApplied ? { autoApplied: plan.autoApplied } : {}),
469
+ ...(plan.pendingSteps ? { pendingSteps: plan.pendingSteps } : {}),
470
+ ...(plan.assistantPrompt ? { assistantPrompt: plan.assistantPrompt } : {}),
471
+ ...(plan.patches ? { patches: plan.patches } : {}),
389
472
  }
390
473
  }
391
474
 
@@ -409,6 +492,9 @@ export async function applyResolvedOnboardingSession(
409
492
  selectedIDE: session.selectedIDE,
410
493
  providerDefault: session.providerDefault,
411
494
  plan: session.plan,
495
+ allowManualPlanApply:
496
+ (session.plan.strategy === 'manual' || session.plan.strategy === 'guided') &&
497
+ session.plan.blockers.length === 0,
412
498
  })
413
499
 
414
500
  const diagnostics = buildExecutionDiagnostics(session, applyResult)
@@ -419,20 +505,30 @@ export async function applyResolvedOnboardingSession(
419
505
  ? 'partial_success'
420
506
  : 'success'
421
507
 
508
+ const ideExtension = buildIdeExtensionStatus({
509
+ ...(session.selectedIDE?.ide ? { ide: session.selectedIDE.ide } : {}),
510
+ required: session.plan.defaults.extension,
511
+ installed:
512
+ session.plan.defaults.extension && !applyResult.postInstall.manualExtensionInstallNeeded,
513
+ manualRequired: applyResult.postInstall.manualExtensionInstallNeeded,
514
+ })
515
+
422
516
  return {
423
517
  status,
424
518
  target: session.target,
425
519
  summary: session.summary,
426
520
  confirmation: session.confirmation,
427
- ideExtension: buildIdeExtensionStatus({
428
- required: session.plan.defaults.extension,
429
- installed:
430
- session.plan.defaults.extension && !applyResult.postInstall.manualExtensionInstallNeeded,
431
- manualRequired: applyResult.postInstall.manualExtensionInstallNeeded,
432
- }),
521
+ ideExtension,
433
522
  verification,
434
523
  result: buildExecutionResult(session, applyResult),
435
- diagnostics,
524
+ ...(session.framework ? { framework: session.framework } : {}),
525
+ ...(session.metaFramework ? { metaFramework: session.metaFramework } : {}),
526
+ ...(session.routerMode ? { routerMode: session.routerMode } : {}),
527
+ ...(session.autoApplied ? { autoApplied: session.autoApplied } : {}),
528
+ ...(session.pendingSteps ? { pendingSteps: session.pendingSteps } : {}),
529
+ ...(session.assistantPrompt ? { assistantPrompt: session.assistantPrompt } : {}),
530
+ ...(session.patches ? { patches: session.patches } : {}),
531
+ ...(diagnostics ? { diagnostics } : {}),
436
532
  }
437
533
  }
438
534
 
@@ -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
+ ...(buildTool.isLegacyRspack ? { isLegacyRspack: true } : {}),
67
+ ...(buildTool.isLegacyWebpack ? { isLegacyWebpack: true } : {}),
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
  }
@@ -92,9 +162,11 @@ export function resolveOnboardingTarget(
92
162
  if (candidates.length === 1) {
93
163
  return {
94
164
  status: 'resolved',
95
- selected: candidates[0],
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
  }
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { OnboardingPatchPlan } from '../types.js'
4
+
5
+ export interface UmiGuidance {
6
+ framework: 'react'
7
+ metaFramework: 'Umi'
8
+ autoApplied: string[]
9
+ pendingSteps: string[]
10
+ assistantPrompt: string
11
+ patches: OnboardingPatchPlan[]
12
+ }
13
+
14
+ const UMI_CONFIG_CANDIDATES = [
15
+ '.umirc.ts',
16
+ '.umirc.js',
17
+ 'config/config.ts',
18
+ 'config/config.js',
19
+ ] as const
20
+
21
+ function findFirstExisting(root: string, candidates: readonly string[]): string | undefined {
22
+ for (const candidate of candidates) {
23
+ if (fs.existsSync(path.join(root, candidate))) {
24
+ return candidate
25
+ }
26
+ }
27
+
28
+ return undefined
29
+ }
30
+
31
+ function readConfig(root: string, relativePath?: string): string {
32
+ if (!relativePath) return ''
33
+ const filePath = path.join(root, relativePath)
34
+ if (!fs.existsSync(filePath)) return ''
35
+ return fs.readFileSync(filePath, 'utf8')
36
+ }
37
+
38
+ function detectPatchShape(source: string): {
39
+ status: OnboardingPatchPlan['status']
40
+ reason: OnboardingPatchPlan['reason']
41
+ confidence: OnboardingPatchPlan['confidence']
42
+ } {
43
+ if (/defineConfig\s*\(\s*\{[\s\S]*\}\s*\)/m.test(source)) {
44
+ return {
45
+ status: 'planned',
46
+ reason: 'umi_config_object_export',
47
+ confidence: 'high',
48
+ }
49
+ }
50
+
51
+ if (source.trim().length === 0) {
52
+ return {
53
+ status: 'manual_patch_required',
54
+ reason: 'umi_config_missing',
55
+ confidence: 'low',
56
+ }
57
+ }
58
+
59
+ return {
60
+ status: 'manual_patch_required',
61
+ reason: 'umi_config_complex_shape',
62
+ confidence: 'medium',
63
+ }
64
+ }
65
+
66
+ function buildUmiConfigSnippet(): string {
67
+ return [
68
+ "import { defineConfig } from 'umi'",
69
+ "import { webpack4Plugin } from '@inspecto-dev/plugin/legacy/webpack4'",
70
+ '',
71
+ 'export default defineConfig({',
72
+ ' chainWebpack(memo) {',
73
+ " if (process.env.NODE_ENV === 'development') {",
74
+ " memo.plugin('inspecto').use(webpack4Plugin())",
75
+ ' }',
76
+ ' },',
77
+ '})',
78
+ ].join('\n')
79
+ }
80
+
81
+ function buildUmiMountSnippet(): string {
82
+ return [
83
+ "import { useEffect } from 'react'",
84
+ '',
85
+ 'export function rootContainer(container: React.ReactNode) {',
86
+ ' return (',
87
+ ' <InspectoWrapper>{container}</InspectoWrapper>',
88
+ ' )',
89
+ '}',
90
+ '',
91
+ 'function InspectoWrapper({ children }: { children: React.ReactNode }) {',
92
+ ' useEffect(() => {',
93
+ " if (process.env.NODE_ENV !== 'production') {",
94
+ " import('@inspecto-dev/core').then(({ mountInspector }) => {",
95
+ ' mountInspector({',
96
+ " serverUrl: 'http://127.0.0.1:' + ((window as any).__AI_INSPECTOR_PORT__ || 5678),",
97
+ ' })',
98
+ ' })',
99
+ ' }',
100
+ ' }, [])',
101
+ '',
102
+ ' return <>{children}</>',
103
+ '}',
104
+ ].join('\n')
105
+ }
106
+
107
+ export function createUmiGuidance(root: string): UmiGuidance {
108
+ const configPath = findFirstExisting(root, UMI_CONFIG_CANDIDATES) ?? '.umirc.ts'
109
+ const configSource = readConfig(root, findFirstExisting(root, UMI_CONFIG_CANDIDATES))
110
+ const patchShape = detectPatchShape(configSource)
111
+
112
+ return {
113
+ framework: 'react',
114
+ metaFramework: 'Umi',
115
+ autoApplied: ['dependencies', 'inspecto_settings'],
116
+ pendingSteps: [
117
+ `Review the generated Umi patch plan for ${configPath}.`,
118
+ 'Complete the remaining client-side mount step in src/app.tsx.',
119
+ ],
120
+ assistantPrompt:
121
+ 'Complete the remaining Inspecto onboarding for this Umi project. Review the generated patch plan, keep existing app behavior unchanged, and finish the client-side mount step safely.',
122
+ patches: [
123
+ {
124
+ path: configPath,
125
+ status: patchShape.status,
126
+ reason: patchShape.reason,
127
+ confidence: patchShape.confidence,
128
+ snippet: buildUmiConfigSnippet(),
129
+ },
130
+ {
131
+ path: 'src/app.tsx',
132
+ status: 'manual_patch_required',
133
+ reason: 'umi_app_mount',
134
+ confidence: 'medium',
135
+ snippet: buildUmiMountSnippet(),
136
+ },
137
+ ],
138
+ }
139
+ }
package/src/types.ts CHANGED
@@ -63,18 +63,25 @@ 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
  }
72
77
 
73
78
  export interface OnboardingTargetResolution {
74
- status: 'resolved' | 'needs_selection'
79
+ status: 'resolved' | 'needs_selection' | 'guided'
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 {
@@ -84,6 +91,14 @@ export interface OnboardingSummary {
84
91
  manualFollowUp: string[]
85
92
  }
86
93
 
94
+ export interface OnboardingPatchPlan {
95
+ path: string
96
+ status: 'planned' | 'manual_patch_required'
97
+ reason: string
98
+ snippet: string
99
+ confidence: 'high' | 'medium' | 'low'
100
+ }
101
+
87
102
  export interface OnboardingConfirmation {
88
103
  required: boolean
89
104
  reason?: string
@@ -119,6 +134,16 @@ export interface OnboardingVerification {
119
134
  message: string
120
135
  }
121
136
 
137
+ export interface OnboardingAssistantHandoff {
138
+ framework?: string
139
+ metaFramework?: string
140
+ routerMode?: 'app' | 'pages' | 'mixed' | 'unknown'
141
+ autoApplied?: string[]
142
+ pendingSteps?: string[]
143
+ assistantPrompt?: string
144
+ patches?: OnboardingPatchPlan[]
145
+ }
146
+
122
147
  export interface ResolvedOnboardingSession {
123
148
  status: OnboardStatus
124
149
  target: OnboardingTargetResolution
@@ -130,6 +155,14 @@ export interface ResolvedOnboardingSession {
130
155
  projectRoot: string
131
156
  selectedIDE?: { ide: string; supported: boolean } | null
132
157
  providerDefault?: string
158
+ framework?: string
159
+ metaFramework?: string
160
+ routerMode?: 'app' | 'pages' | 'mixed' | 'unknown'
161
+ autoApplied?: string[]
162
+ pendingSteps?: string[]
163
+ assistantPrompt?: string
164
+ patches?: OnboardingPatchPlan[]
165
+ handoff?: OnboardingAssistantHandoff
133
166
  }
134
167
 
135
168
  export interface OnboardCommandResult {
@@ -141,6 +174,14 @@ export interface OnboardCommandResult {
141
174
  verification?: OnboardingVerification
142
175
  result?: OnboardingExecutionResult
143
176
  diagnostics?: OnboardingDiagnostics
177
+ framework?: string
178
+ metaFramework?: string
179
+ routerMode?: 'app' | 'pages' | 'mixed' | 'unknown'
180
+ autoApplied?: string[]
181
+ pendingSteps?: string[]
182
+ assistantPrompt?: string
183
+ patches?: OnboardingPatchPlan[]
184
+ handoff?: OnboardingAssistantHandoff
144
185
  }
145
186
 
146
187
  /** Machine-readable detection output for skill-first onboarding */
@@ -167,9 +208,16 @@ export interface PlanResult {
167
208
  status: CommandStatus
168
209
  warnings: CommandMessage[]
169
210
  blockers: CommandMessage[]
170
- strategy: 'supported' | 'manual' | 'unsupported'
211
+ strategy: 'supported' | 'guided' | 'manual' | 'unsupported'
171
212
  actions: Array<{
172
- type: 'install_dependency' | 'modify_file' | 'install_extension' | 'manual_step'
213
+ type:
214
+ | 'install_dependency'
215
+ | 'modify_file'
216
+ | 'install_extension'
217
+ | 'manual_step'
218
+ | 'generate_patch_plan'
219
+ | 'generate_file'
220
+ | 'manual_confirmation'
173
221
  target: string
174
222
  description: string
175
223
  }>
@@ -179,6 +227,13 @@ export interface PlanResult {
179
227
  shared: boolean
180
228
  extension: boolean
181
229
  }
230
+ framework?: string
231
+ metaFramework?: string
232
+ routerMode?: 'app' | 'pages' | 'mixed' | 'unknown'
233
+ autoApplied?: string[]
234
+ pendingSteps?: string[]
235
+ assistantPrompt?: string
236
+ patches?: OnboardingPatchPlan[]
182
237
  }
183
238
 
184
239
  /** A single doctor diagnostic check/result */