@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
@@ -0,0 +1,257 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { OnboardingPatchPlan } from '../types.js'
4
+
5
+ type RouterMode = 'app' | 'pages' | 'mixed' | 'unknown'
6
+
7
+ export interface NextJsGuidance {
8
+ framework: 'react'
9
+ metaFramework: 'Next.js'
10
+ routerMode: RouterMode
11
+ autoApplied: string[]
12
+ pendingSteps: string[]
13
+ assistantPrompt: string
14
+ patches: OnboardingPatchPlan[]
15
+ }
16
+
17
+ const NEXT_CONFIG_CANDIDATES = ['next.config.ts', 'next.config.mjs', 'next.config.js'] as const
18
+ const APP_ROUTER_LAYOUTS = [
19
+ 'app/layout.tsx',
20
+ 'app/layout.jsx',
21
+ 'src/app/layout.tsx',
22
+ 'src/app/layout.jsx',
23
+ ] as const
24
+ const PAGES_ROUTER_APPS = [
25
+ 'pages/_app.tsx',
26
+ 'pages/_app.jsx',
27
+ 'src/pages/_app.tsx',
28
+ 'src/pages/_app.jsx',
29
+ ] as const
30
+ const PACKAGE_JSON_PATH = 'package.json'
31
+
32
+ function findFirstExisting(root: string, candidates: readonly string[]): string | undefined {
33
+ for (const candidate of candidates) {
34
+ if (fs.existsSync(path.join(root, candidate))) {
35
+ return candidate
36
+ }
37
+ }
38
+
39
+ return undefined
40
+ }
41
+
42
+ function detectRouterMode(root: string): RouterMode {
43
+ const hasAppRouter = Boolean(findFirstExisting(root, APP_ROUTER_LAYOUTS))
44
+ const hasPagesRouter = Boolean(findFirstExisting(root, PAGES_ROUTER_APPS))
45
+
46
+ if (hasAppRouter && hasPagesRouter) return 'mixed'
47
+ if (hasAppRouter) return 'app'
48
+ if (hasPagesRouter) return 'pages'
49
+ return 'unknown'
50
+ }
51
+
52
+ function detectNextConfigPath(root: string): string | undefined {
53
+ return findFirstExisting(root, NEXT_CONFIG_CANDIDATES)
54
+ }
55
+
56
+ function readConfig(root: string, relativePath?: string): string {
57
+ if (!relativePath) return ''
58
+ const filePath = path.join(root, relativePath)
59
+ if (!fs.existsSync(filePath)) return ''
60
+ return fs.readFileSync(filePath, 'utf8')
61
+ }
62
+
63
+ function detectPatchShape(source: string): {
64
+ status: OnboardingPatchPlan['status']
65
+ reason: OnboardingPatchPlan['reason']
66
+ confidence: OnboardingPatchPlan['confidence']
67
+ } {
68
+ if (
69
+ /export\s+default\s*\{[\s\S]*\}/m.test(source) ||
70
+ /module\.exports\s*=\s*\{[\s\S]*\}/m.test(source) ||
71
+ /const\s+[A-Za-z0-9_$]+\s*(?::[^=]+)?=\s*\{[\s\S]*\}\s*;?\s*export\s+default\s+[A-Za-z0-9_$]+\s*;?/m.test(
72
+ source,
73
+ ) ||
74
+ /\/\*\*[\s\S]*?@type\s*\{import\('next'\)\.NextConfig\}[\s\S]*?\*\/[\s\S]*?(export\s+default|module\.exports)\s*=?\s*\{[\s\S]*\}/m.test(
75
+ source,
76
+ )
77
+ ) {
78
+ return {
79
+ status: 'planned',
80
+ reason: 'next_config_object_export',
81
+ confidence: 'high',
82
+ }
83
+ }
84
+
85
+ if (
86
+ /module\.exports\s*=\s*[A-Za-z0-9_$]+\s*\(/m.test(source) ||
87
+ /export\s+default\s+[A-Za-z0-9_$]+\s*\(/m.test(source)
88
+ ) {
89
+ return {
90
+ status: 'manual_patch_required',
91
+ reason: 'next_config_wrapped_export',
92
+ confidence: 'medium',
93
+ }
94
+ }
95
+
96
+ if (source.trim().length === 0) {
97
+ return {
98
+ status: 'manual_patch_required',
99
+ reason: 'next_config_missing',
100
+ confidence: 'low',
101
+ }
102
+ }
103
+
104
+ return {
105
+ status: 'manual_patch_required',
106
+ reason: 'next_config_complex_shape',
107
+ confidence: 'medium',
108
+ }
109
+ }
110
+
111
+ function buildPatchSnippet(): string {
112
+ return [
113
+ "import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'",
114
+ '',
115
+ 'webpack(config, { dev, isServer }) {',
116
+ ' if (dev) {',
117
+ ' config.plugins.push(inspecto())',
118
+ ' }',
119
+ ' return config',
120
+ '}',
121
+ ].join('\n')
122
+ }
123
+
124
+ function buildAppRouterMountSnippet(): string {
125
+ return [
126
+ "import { useEffect } from 'react'",
127
+ '',
128
+ 'function InspectoProvider() {',
129
+ ' useEffect(() => {',
130
+ " if (process.env.NODE_ENV !== 'production') {",
131
+ " import('@inspecto-dev/core').then(({ mountInspector }) => {",
132
+ ' mountInspector()',
133
+ ' })',
134
+ ' }',
135
+ ' }, [])',
136
+ '',
137
+ ' return null',
138
+ '}',
139
+ '',
140
+ 'Render <InspectoProvider /> from app/layout.tsx inside the <body> tree.',
141
+ ].join('\n')
142
+ }
143
+
144
+ function buildPagesRouterMountSnippet(): string {
145
+ return [
146
+ "import { useEffect } from 'react'",
147
+ '',
148
+ 'useEffect(() => {',
149
+ " if (process.env.NODE_ENV !== 'production') {",
150
+ " import('@inspecto-dev/core').then(({ mountInspector }) => {",
151
+ ' mountInspector()',
152
+ ' })',
153
+ ' }',
154
+ '}, [])',
155
+ '',
156
+ 'Add this effect to pages/_app.tsx without changing the existing app wrapper behavior.',
157
+ ].join('\n')
158
+ }
159
+
160
+ function detectWebpackDevScriptPatch(root: string): OnboardingPatchPlan | undefined {
161
+ const packageJsonSource = readConfig(root, PACKAGE_JSON_PATH)
162
+ if (!packageJsonSource) {
163
+ return undefined
164
+ }
165
+
166
+ try {
167
+ const packageJson = JSON.parse(packageJsonSource) as { scripts?: Record<string, string> }
168
+ const devScript = packageJson.scripts?.dev
169
+ if (!devScript || !/\bnext\s+dev\b/.test(devScript) || /--webpack\b/.test(devScript)) {
170
+ return undefined
171
+ }
172
+
173
+ return {
174
+ path: PACKAGE_JSON_PATH,
175
+ status: 'manual_patch_required',
176
+ reason: 'next_dev_script_requires_webpack',
177
+ confidence: 'medium',
178
+ snippet: '"dev": "next dev --webpack"',
179
+ }
180
+ } catch {
181
+ return undefined
182
+ }
183
+ }
184
+
185
+ function buildPendingSteps(
186
+ routerMode: RouterMode,
187
+ configPath: string,
188
+ needsWebpackDevScript: boolean,
189
+ ): string[] {
190
+ const routerHint =
191
+ routerMode === 'app'
192
+ ? 'Complete the remaining client-side mount step for your App Router entry.'
193
+ : routerMode === 'pages'
194
+ ? 'Complete the remaining client-side mount step for your Pages Router entry.'
195
+ : routerMode === 'mixed'
196
+ ? 'Complete the remaining client-side mount step for the router entry your team actually uses in development.'
197
+ : 'Complete the remaining client-side mount step in the appropriate Next.js entry file.'
198
+
199
+ return [
200
+ `Review the generated Next.js patch plan for ${configPath}.`,
201
+ ...(needsWebpackDevScript
202
+ ? ['Update the Next.js dev script to use webpack mode for Inspecto validation.']
203
+ : []),
204
+ 'Keep the Inspecto webpack plugin enabled for both server and client development compilers in App Router projects.',
205
+ routerHint,
206
+ ]
207
+ }
208
+
209
+ export function createNextJsGuidance(root: string): NextJsGuidance {
210
+ const configPath = detectNextConfigPath(root) ?? 'next.config.js'
211
+ const configSource = readConfig(root, detectNextConfigPath(root))
212
+ const patchShape = detectPatchShape(configSource)
213
+ const routerMode = detectRouterMode(root)
214
+ const webpackDevScriptPatch = detectWebpackDevScriptPatch(root)
215
+
216
+ return {
217
+ framework: 'react',
218
+ metaFramework: 'Next.js',
219
+ routerMode,
220
+ autoApplied: ['dependencies', 'inspecto_settings'],
221
+ pendingSteps: buildPendingSteps(routerMode, configPath, Boolean(webpackDevScriptPatch)),
222
+ assistantPrompt:
223
+ 'Complete the remaining Inspecto onboarding for this Next.js project. Use the generated patches directly, keep existing app behavior unchanged, avoid unrelated documentation searches unless a patch is insufficient, and finish the client-side mount step safely.',
224
+ patches: [
225
+ {
226
+ path: configPath,
227
+ status: patchShape.status,
228
+ reason: patchShape.reason,
229
+ confidence: patchShape.confidence,
230
+ snippet: buildPatchSnippet(),
231
+ },
232
+ ...(routerMode === 'app' || routerMode === 'mixed'
233
+ ? [
234
+ {
235
+ path: findFirstExisting(root, APP_ROUTER_LAYOUTS) ?? 'app/layout.tsx',
236
+ status: 'manual_patch_required' as const,
237
+ reason: 'next_app_router_mount',
238
+ confidence: 'medium' as const,
239
+ snippet: buildAppRouterMountSnippet(),
240
+ },
241
+ ]
242
+ : []),
243
+ ...(routerMode === 'pages' || routerMode === 'mixed'
244
+ ? [
245
+ {
246
+ path: findFirstExisting(root, PAGES_ROUTER_APPS) ?? 'pages/_app.tsx',
247
+ status: 'manual_patch_required' as const,
248
+ reason: 'next_pages_router_mount',
249
+ confidence: 'medium' as const,
250
+ snippet: buildPagesRouterMountSnippet(),
251
+ },
252
+ ]
253
+ : []),
254
+ ...(webpackDevScriptPatch ? [webpackDevScriptPatch] : []),
255
+ ],
256
+ }
257
+ }
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { OnboardingPatchPlan } from '../types.js'
4
+
5
+ export interface NuxtGuidance {
6
+ framework: 'vue'
7
+ metaFramework: 'Nuxt'
8
+ autoApplied: string[]
9
+ pendingSteps: string[]
10
+ assistantPrompt: string
11
+ patches: OnboardingPatchPlan[]
12
+ }
13
+
14
+ const NUXT_CONFIG_CANDIDATES = ['nuxt.config.ts', 'nuxt.config.js'] as const
15
+
16
+ function findFirstExisting(root: string, candidates: readonly string[]): string | undefined {
17
+ for (const candidate of candidates) {
18
+ if (fs.existsSync(path.join(root, candidate))) {
19
+ return candidate
20
+ }
21
+ }
22
+
23
+ return undefined
24
+ }
25
+
26
+ function readConfig(root: string, relativePath?: string): string {
27
+ if (!relativePath) return ''
28
+ const filePath = path.join(root, relativePath)
29
+ if (!fs.existsSync(filePath)) return ''
30
+ return fs.readFileSync(filePath, 'utf8')
31
+ }
32
+
33
+ function detectPatchShape(source: string): {
34
+ status: OnboardingPatchPlan['status']
35
+ reason: OnboardingPatchPlan['reason']
36
+ confidence: OnboardingPatchPlan['confidence']
37
+ } {
38
+ if (/with[A-Za-z0-9_$]*\s*\(\s*defineNuxtConfig/m.test(source)) {
39
+ return {
40
+ status: 'manual_patch_required',
41
+ reason: 'nuxt_config_wrapped_export',
42
+ confidence: 'medium',
43
+ }
44
+ }
45
+
46
+ if (/defineNuxtConfig\s*\(\s*\{[\s\S]*\}\s*\)/m.test(source)) {
47
+ return {
48
+ status: 'planned',
49
+ reason: 'nuxt_config_object_export',
50
+ confidence: 'high',
51
+ }
52
+ }
53
+
54
+ if (source.trim().length === 0) {
55
+ return {
56
+ status: 'manual_patch_required',
57
+ reason: 'nuxt_config_missing',
58
+ confidence: 'low',
59
+ }
60
+ }
61
+
62
+ return {
63
+ status: 'manual_patch_required',
64
+ reason: 'nuxt_config_complex_shape',
65
+ confidence: 'medium',
66
+ }
67
+ }
68
+
69
+ function buildNuxtConfigSnippet(): string {
70
+ return [
71
+ "import { vitePlugin as inspecto } from '@inspecto-dev/plugin'",
72
+ '',
73
+ 'export default defineNuxtConfig({',
74
+ ' vite: {',
75
+ ' plugins: [inspecto()],',
76
+ ' },',
77
+ '})',
78
+ ].join('\n')
79
+ }
80
+
81
+ function buildNuxtPluginSnippet(): string {
82
+ return [
83
+ 'export default defineNuxtPlugin(() => {',
84
+ ' if (import.meta.dev) {',
85
+ " import('@inspecto-dev/core').then(({ mountInspector }) => {",
86
+ ' mountInspector()',
87
+ ' })',
88
+ ' }',
89
+ '})',
90
+ ].join('\n')
91
+ }
92
+
93
+ export function createNuxtGuidance(root: string): NuxtGuidance {
94
+ const configPath = findFirstExisting(root, NUXT_CONFIG_CANDIDATES) ?? 'nuxt.config.ts'
95
+ const configSource = readConfig(root, findFirstExisting(root, NUXT_CONFIG_CANDIDATES))
96
+ const patchShape = detectPatchShape(configSource)
97
+
98
+ const hasSrcDir =
99
+ fs.existsSync(path.join(root, 'src')) && fs.statSync(path.join(root, 'src')).isDirectory()
100
+ const pluginPath = hasSrcDir ? 'src/plugins/inspecto.client.ts' : 'plugins/inspecto.client.ts'
101
+
102
+ return {
103
+ framework: 'vue',
104
+ metaFramework: 'Nuxt',
105
+ autoApplied: ['dependencies', 'inspecto_settings'],
106
+ pendingSteps: [
107
+ `Review the generated Nuxt patch plan for ${configPath}.`,
108
+ `Complete the remaining Nuxt client plugin mount step in ${pluginPath}.`,
109
+ ],
110
+ assistantPrompt:
111
+ 'Complete the remaining Inspecto onboarding for this Nuxt project. Review the generated patch plan, keep existing app behavior unchanged, and finish the client plugin mount step safely.',
112
+ patches: [
113
+ {
114
+ path: configPath,
115
+ status: patchShape.status,
116
+ reason: patchShape.reason,
117
+ confidence: patchShape.confidence,
118
+ snippet: buildNuxtConfigSnippet(),
119
+ },
120
+ {
121
+ path: pluginPath,
122
+ status: 'manual_patch_required',
123
+ reason: 'nuxt_client_plugin_mount',
124
+ confidence: 'medium',
125
+ snippet: buildNuxtPluginSnippet(),
126
+ },
127
+ ],
128
+ }
129
+ }