@inspecto-dev/cli 0.2.0-alpha.5 → 0.2.0-alpha.6

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.
@@ -2,54 +2,68 @@ import { log } from './utils/logger.js'
2
2
 
3
3
  export function printNuxtManualInstructions() {
4
4
  log.blank()
5
- log.hint('To enable Inspecto in Nuxt, update your nuxt.config.ts:')
6
- console.log(`\x1b[36m
7
- import { vitePlugin as inspecto } from '@inspecto-dev/plugin'
8
- export default defineNuxtConfig({
9
- vite: {
10
- plugins: [inspecto()]
11
- }
12
- })
13
- \x1b[0m`)
14
- log.hint('And create a Nuxt plugin at plugins/inspecto.client.ts:')
15
- console.log(`\x1b[36m
16
- export default defineNuxtPlugin(() => {
17
- if (import.meta.dev) {
18
- import('@inspecto-dev/core').then(({ mountInspector }) => {
19
- mountInspector()
20
- })
21
- }
22
- })
23
- \x1b[0m`)
5
+ log.hint('Nuxt requires manual setup in the current version.')
6
+ log.hint('1. Update `nuxt.config.ts` to register the Inspecto Vite plugin:')
7
+ log.copyableCodeBlock([
8
+ "import { vitePlugin as inspecto } from '@inspecto-dev/plugin'",
9
+ '',
10
+ 'export default defineNuxtConfig({',
11
+ ' vite: {',
12
+ ' plugins: [inspecto()],',
13
+ ' },',
14
+ '})',
15
+ ])
16
+ log.hint('2. Create `plugins/inspecto.client.ts` to mount `@inspecto-dev/core` in development:')
17
+ log.copyableCodeBlock([
18
+ 'export default defineNuxtPlugin(() => {',
19
+ ' if (import.meta.dev) {',
20
+ " import('@inspecto-dev/core').then(({ mountInspector }) => {",
21
+ ' mountInspector()',
22
+ ' })',
23
+ ' }',
24
+ '})',
25
+ ])
26
+ log.hint('3. Restart your Nuxt dev server after updating the config.')
24
27
  }
25
28
 
26
29
  export function printNextJsManualInstructions() {
27
30
  log.blank()
28
- log.hint('To enable Inspecto in Next.js, update your next.config.mjs:')
29
- console.log(`\x1b[36m
30
- import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'
31
- const nextConfig = {
32
- webpack: (config, { dev, isServer }) => {
33
- if (dev && !isServer) config.plugins.push(inspecto())
34
- return config
35
- }
36
- }
37
- export default nextConfig
38
- \x1b[0m`)
39
- log.hint('And initialize the client dynamically in your app/layout.tsx (or pages/_app.tsx):')
40
- console.log(`\x1b[36m
41
- 'use client'
42
- import { useEffect } from 'react'
43
-
44
- export default function RootLayout({ children }) {
45
- useEffect(() => {
46
- if (process.env.NODE_ENV !== 'production') {
47
- import('@inspecto-dev/core').then(({ mountInspector }) => {
48
- mountInspector({ serverUrl: 'http://127.0.0.1:5678' })
49
- })
50
- }
51
- }, [])
52
- return <html><body>{children}</body></html>
53
- }
54
- \x1b[0m`)
31
+ log.hint('Next.js requires manual setup in the current version.')
32
+ log.hint('1. Update `next.config.mjs` to register the Inspecto webpack plugin:')
33
+ log.copyableCodeBlock([
34
+ "import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'",
35
+ '',
36
+ "/** @type {import('next').NextConfig} */",
37
+ 'const nextConfig = {',
38
+ ' webpack: (config, { dev, isServer }) => {',
39
+ ' if (dev && !isServer) {',
40
+ ' config.plugins.push(inspecto())',
41
+ ' }',
42
+ ' return config',
43
+ ' },',
44
+ '}',
45
+ '',
46
+ 'export default nextConfig',
47
+ ])
48
+ log.hint(
49
+ '2. Initialize `@inspecto-dev/core` from a client component such as `app/layout.tsx` or `pages/_app.tsx`:',
50
+ )
51
+ log.copyableCodeBlock([
52
+ "'use client'",
53
+ '',
54
+ "import { useEffect } from 'react'",
55
+ '',
56
+ 'export default function RootLayout({ children }) {',
57
+ ' useEffect(() => {',
58
+ " if (process.env.NODE_ENV !== 'production') {",
59
+ " import('@inspecto-dev/core').then(({ mountInspector }) => {",
60
+ " mountInspector({ serverUrl: 'http://127.0.0.1:5678' })",
61
+ ' })',
62
+ ' }',
63
+ ' }, [])',
64
+ '',
65
+ ' return <html><body>{children}</body></html>',
66
+ '}',
67
+ ])
68
+ log.hint('3. Restart your Next.js dev server after updating the config.')
55
69
  }
@@ -0,0 +1,325 @@
1
+ import path from 'node:path'
2
+ import ora from 'ora'
3
+ import { getInstallCommand } from '../detect/package-manager.js'
4
+ import { injectPlugin } from '../inject/ast-injector.js'
5
+ import { installExtension } from '../inject/extension.js'
6
+ import { updateGitignore } from '../inject/gitignore.js'
7
+ import { shell } from '../utils/exec.js'
8
+ import { exists, readJSON, writeJSON } from '../utils/fs.js'
9
+ import { log } from '../utils/logger.js'
10
+ import type {
11
+ BuildToolDetection,
12
+ CommandStatus,
13
+ InstallLock,
14
+ Mutation,
15
+ PackageManager,
16
+ PlanResult,
17
+ } from '../types.js'
18
+
19
+ export interface ApplyOnboardingInput {
20
+ repoRoot: string
21
+ projectRoot: string
22
+ packageManager: PackageManager
23
+ supportedBuildTargets: BuildToolDetection[]
24
+ options: {
25
+ shared: boolean
26
+ skipInstall: boolean
27
+ dryRun: boolean
28
+ noExtension: boolean
29
+ quiet?: boolean | undefined
30
+ }
31
+ selectedIDE?: { ide: string; supported: boolean } | null | undefined
32
+ providerDefault?: string | undefined
33
+ manualConfigRequiredFor?: string | undefined
34
+ injectionSkippedRequiresManualConfig?: boolean | undefined
35
+ plan?: PlanResult | undefined
36
+ allowManualPlanApply?: boolean | undefined
37
+ }
38
+
39
+ interface ApplyReporter {
40
+ warn(text: string): void
41
+ success(text: string): void
42
+ error(text: string): void
43
+ hint(text: string): void
44
+ dryRun(text: string): void
45
+ }
46
+
47
+ interface ApplySpinner {
48
+ start(): void
49
+ succeed(text: string): void
50
+ fail(text: string): void
51
+ }
52
+
53
+ export interface ApplyOnboardingResult {
54
+ status: CommandStatus
55
+ mutations: Mutation[]
56
+ postInstall: {
57
+ installFailed: boolean
58
+ injectionFailed: boolean
59
+ manualExtensionInstallNeeded: boolean
60
+ nextSteps: string[]
61
+ }
62
+ }
63
+
64
+ function resultStatus(nextSteps: string[]): CommandStatus {
65
+ return nextSteps.length > 0 ? 'warning' : 'ok'
66
+ }
67
+
68
+ function manualPlanSteps(plan: PlanResult): string[] {
69
+ return [
70
+ ...plan.blockers.map(blocker => blocker.message),
71
+ ...plan.actions
72
+ .filter(action => action.type === 'manual_step')
73
+ .map(action => action.description),
74
+ ]
75
+ }
76
+
77
+ export async function applyOnboardingPlan(
78
+ input: ApplyOnboardingInput,
79
+ ): Promise<ApplyOnboardingResult> {
80
+ return applyOnboardingPlanInternal(input)
81
+ }
82
+
83
+ function createReporter(quiet = false): ApplyReporter {
84
+ if (quiet) {
85
+ return {
86
+ warn() {},
87
+ success() {},
88
+ error() {},
89
+ hint() {},
90
+ dryRun() {},
91
+ }
92
+ }
93
+
94
+ return {
95
+ warn(text: string) {
96
+ log.warn(text)
97
+ },
98
+ success(text: string) {
99
+ log.success(text)
100
+ },
101
+ error(text: string) {
102
+ log.error(text)
103
+ },
104
+ hint(text: string) {
105
+ log.hint(text)
106
+ },
107
+ dryRun(text: string) {
108
+ log.dryRun(text)
109
+ },
110
+ }
111
+ }
112
+
113
+ function createSpinner(text: string, quiet = false): ApplySpinner {
114
+ if (quiet) {
115
+ return {
116
+ start() {},
117
+ succeed() {},
118
+ fail() {},
119
+ }
120
+ }
121
+
122
+ const spinner = ora(text)
123
+ return {
124
+ start() {
125
+ spinner.start()
126
+ },
127
+ succeed(successText: string) {
128
+ spinner.succeed(successText)
129
+ },
130
+ fail(failureText: string) {
131
+ spinner.fail(failureText)
132
+ },
133
+ }
134
+ }
135
+
136
+ async function applyOnboardingPlanInternal(
137
+ input: ApplyOnboardingInput,
138
+ ): Promise<ApplyOnboardingResult> {
139
+ const reporter = createReporter(input.options.quiet)
140
+
141
+ if (input.plan && input.plan.strategy !== 'supported' && !input.allowManualPlanApply) {
142
+ return {
143
+ status: input.plan.status,
144
+ mutations: [],
145
+ postInstall: {
146
+ installFailed: false,
147
+ injectionFailed: false,
148
+ manualExtensionInstallNeeded: false,
149
+ nextSteps: manualPlanSteps(input.plan),
150
+ },
151
+ }
152
+ }
153
+
154
+ const mutations: Mutation[] = []
155
+ const settingsDir = path.join(input.projectRoot, '.inspecto')
156
+ const settingsFileName = input.options.shared ? 'settings.json' : 'settings.local.json'
157
+ const promptsFileName = input.options.shared ? 'prompts.json' : 'prompts.local.json'
158
+ const settingsPath = path.join(settingsDir, settingsFileName)
159
+ const promptsPath = path.join(settingsDir, promptsFileName)
160
+ const installCmd = getInstallCommand(
161
+ input.packageManager,
162
+ '@inspecto-dev/plugin @inspecto-dev/core',
163
+ )
164
+ const nextSteps: string[] = []
165
+
166
+ let installFailed = false
167
+ if (input.options.skipInstall) {
168
+ reporter.warn('Skipping dependency installation (--skip-install)')
169
+ } else if (input.options.dryRun) {
170
+ reporter.dryRun(`Would run: ${installCmd}`)
171
+ } else {
172
+ const spinner = createSpinner(
173
+ `Installing devDependencies via: ${installCmd}`,
174
+ input.options.quiet,
175
+ )
176
+ try {
177
+ spinner.start()
178
+ await shell(installCmd, input.projectRoot)
179
+ spinner.succeed('Dependencies installed successfully')
180
+ reporter.success('Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies')
181
+ mutations.push({ type: 'dependency_added', name: '@inspecto-dev/plugin', dev: true })
182
+ mutations.push({ type: 'dependency_added', name: '@inspecto-dev/core', dev: true })
183
+ } catch (error: any) {
184
+ spinner.fail('Dependency installation failed')
185
+ installFailed = true
186
+ reporter.error(`Failed to install dependency: ${error?.message || 'Unknown error'}`)
187
+ reporter.hint(`Run manually in ${input.projectRoot}: ${installCmd}`)
188
+ reporter.hint(
189
+ 'Setup will continue without dependencies, but Inspecto may not run until installation succeeds.',
190
+ )
191
+ }
192
+ }
193
+
194
+ let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig)
195
+ for (const target of input.supportedBuildTargets) {
196
+ const result = await injectPlugin(input.repoRoot, target, input.options.dryRun)
197
+ if (result.success) {
198
+ mutations.push(...result.mutations)
199
+ } else {
200
+ injectionFailed = true
201
+ }
202
+ }
203
+
204
+ if (await exists(settingsPath)) {
205
+ const existingSettings = await readJSON(settingsPath)
206
+ if (existingSettings === null) {
207
+ reporter.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`)
208
+ reporter.hint('Please fix the syntax errors manually, or delete it and re-run init')
209
+ nextSteps.push(`Fix .inspecto/${settingsFileName} or delete it and rerun Inspecto setup.`)
210
+ } else {
211
+ reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`)
212
+ }
213
+ } else {
214
+ const defaultSettings: Record<string, unknown> = {}
215
+
216
+ if (input.selectedIDE?.supported) {
217
+ defaultSettings.ide =
218
+ input.selectedIDE.ide.toLowerCase() === 'vscode'
219
+ ? 'vscode'
220
+ : input.selectedIDE.ide.toLowerCase()
221
+ }
222
+
223
+ if (input.providerDefault) {
224
+ defaultSettings['provider.default'] = input.providerDefault
225
+ }
226
+
227
+ if (input.options.dryRun) {
228
+ reporter.dryRun(`Would create .inspecto/${settingsFileName}`)
229
+ } else {
230
+ await writeJSON(settingsPath, defaultSettings)
231
+ reporter.success(`Created .inspecto/${settingsFileName}`)
232
+ mutations.push({ type: 'file_created', path: `.inspecto/${settingsFileName}` })
233
+ }
234
+ }
235
+
236
+ if (await exists(promptsPath)) {
237
+ reporter.success(`.inspecto/${promptsFileName} already exists (skipped)`)
238
+ } else if (input.options.dryRun) {
239
+ reporter.dryRun(`Would create .inspecto/${promptsFileName}`)
240
+ } else {
241
+ await writeJSON(promptsPath, [])
242
+ reporter.success(`Created .inspecto/${promptsFileName}`)
243
+ mutations.push({ type: 'file_created', path: `.inspecto/${promptsFileName}` })
244
+ }
245
+
246
+ if (!input.options.dryRun) {
247
+ await updateGitignore(input.projectRoot, input.options.shared, input.options.dryRun)
248
+ mutations.push({
249
+ type: 'file_modified',
250
+ path: '.gitignore',
251
+ description: 'Appended .inspecto/ ignore rules',
252
+ })
253
+ } else {
254
+ reporter.dryRun('Would update .gitignore')
255
+ }
256
+
257
+ if (!input.options.dryRun && mutations.length > 0) {
258
+ const lock: InstallLock = {
259
+ version: '1.0.0',
260
+ created_at: new Date().toISOString(),
261
+ mutations,
262
+ }
263
+ await writeJSON(path.join(settingsDir, 'install.lock'), lock)
264
+ }
265
+
266
+ const shouldInstallExt =
267
+ !input.options.noExtension && (!input.selectedIDE || input.selectedIDE.supported)
268
+ let manualExtensionInstallNeeded = false
269
+
270
+ if (input.options.noExtension) {
271
+ reporter.warn('Skipping IDE extension (--no-extension)')
272
+ } else if (shouldInstallExt) {
273
+ const extMutation = await installExtension(input.options.dryRun, input.selectedIDE?.ide)
274
+ if (extMutation && !input.options.dryRun) {
275
+ mutations.push(extMutation)
276
+
277
+ if (extMutation.manual_action_required) {
278
+ manualExtensionInstallNeeded = true
279
+ }
280
+
281
+ const lockPath = path.join(settingsDir, 'install.lock')
282
+ const lock = await readJSON<InstallLock>(lockPath)
283
+ if (lock) {
284
+ lock.mutations = mutations
285
+ await writeJSON(lockPath, lock)
286
+ }
287
+ } else if (extMutation === null && !input.options.dryRun) {
288
+ manualExtensionInstallNeeded = true
289
+ }
290
+ }
291
+
292
+ if (!input.options.dryRun) {
293
+ if (installFailed) {
294
+ nextSteps.push(`Install dependencies manually in ${input.projectRoot}: ${installCmd}`)
295
+ }
296
+ if (injectionFailed) {
297
+ nextSteps.push(
298
+ 'Plugin injection skipped. Follow manual instructions printed above to update your config.',
299
+ )
300
+ }
301
+ if (manualExtensionInstallNeeded) {
302
+ nextSteps.push('Install the Inspecto IDE extension manually')
303
+ }
304
+ if (input.manualConfigRequiredFor === 'Nuxt') {
305
+ nextSteps.push(
306
+ 'Nuxt detected—please follow the Nuxt instructions printed above to finish setup.',
307
+ )
308
+ } else if (input.manualConfigRequiredFor === 'Next.js') {
309
+ nextSteps.push(
310
+ 'Next.js detected—please follow the Next.js instructions printed above to finish setup.',
311
+ )
312
+ }
313
+ }
314
+
315
+ return {
316
+ status: resultStatus(nextSteps),
317
+ mutations,
318
+ postInstall: {
319
+ installFailed,
320
+ injectionFailed,
321
+ manualExtensionInstallNeeded,
322
+ nextSteps,
323
+ },
324
+ }
325
+ }
@@ -0,0 +1,36 @@
1
+ import { detectBuildTools } from '../detect/build-tool.js'
2
+ import { detectFrameworks } from '../detect/framework.js'
3
+ import { detectIDE } from '../detect/ide.js'
4
+ import { detectPackageManager } from '../detect/package-manager.js'
5
+ import { detectProviders } from '../detect/provider.js'
6
+ import type { OnboardingContext } from '../types.js'
7
+
8
+ export async function buildOnboardingContext(root: string): Promise<OnboardingContext> {
9
+ const [packageManager, buildTools, frameworks, ides, providers] = await Promise.all([
10
+ detectPackageManager(root),
11
+ detectBuildTools(root),
12
+ detectFrameworks(root),
13
+ detectIDE(root),
14
+ detectProviders(root),
15
+ ])
16
+
17
+ return {
18
+ root,
19
+ packageManager,
20
+ buildTools: {
21
+ supported: buildTools.supported,
22
+ unsupported: buildTools.unsupported,
23
+ },
24
+ frameworks: {
25
+ supported: frameworks.supported,
26
+ unsupported: frameworks.unsupported.map(item => item.name),
27
+ },
28
+ ides: ides.detected.map(({ ide, supported }) => ({ ide, supported })),
29
+ providers: providers.detected.map(({ id, label, supported, preferredMode }) => ({
30
+ id,
31
+ label,
32
+ supported,
33
+ preferredMode,
34
+ })),
35
+ }
36
+ }