@inspecto-dev/cli 0.2.0-alpha.1 → 0.2.0-alpha.3

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.
@@ -14,149 +14,18 @@ import { detectPackageManager, getInstallCommand } from '../detect/package-manag
14
14
  import { detectBuildTools, resolveInjectionTarget } from '../detect/build-tool.js'
15
15
  import { detectFrameworks } from '../detect/framework.js'
16
16
  import { detectIDE } from '../detect/ide.js'
17
- import { detectAITools, type AIToolDetection } from '../detect/ai-tool.js'
17
+ import { detectProviders, type ProviderDetection } from '../detect/provider.js'
18
18
  import { injectPlugin } from '../inject/ast-injector.js'
19
19
  import { updateGitignore } from '../inject/gitignore.js'
20
20
  import { installExtension } from '../inject/extension.js'
21
- import type { InitOptions, InstallLock, Mutation, BuildToolDetection } from '../types.js'
22
-
23
- /**
24
- * Interactive prompt for IDE choice.
25
- */
26
- async function promptIDEChoice(
27
- detections: { ide: string; supported: boolean }[],
28
- ): Promise<{ ide: string; supported: boolean } | null> {
29
- if (!process.stdin.isTTY) {
30
- log.warn('Multiple IDEs detected but stdin is not interactive')
31
- log.hint(`Using: ${detections[0]!.ide} (first match)`)
32
- return detections[0]!
33
- }
34
-
35
- console.log()
36
- console.log(' ? Detected multiple IDEs:')
37
- detections.forEach((d, i) => {
38
- const status = d.supported ? ' (supported)' : ' (unsupported/limited)'
39
- console.log(` ${i + 1}. ${d.ide}${status}`)
40
- })
41
- console.log()
42
-
43
- return new Promise(resolve => {
44
- process.stdout.write(' > Your choice: ')
45
-
46
- // We must resume stdin in case it was paused by a previous prompt
47
- process.stdin.resume()
48
- process.stdin.setEncoding('utf-8')
49
-
50
- const onData = (data: Buffer) => {
51
- const choice = parseInt(String(data).trim(), 10)
52
-
53
- // Cleanup the listener to avoid memory leaks or multiple fires
54
- process.stdin.off('data', onData)
55
- process.stdin.pause() // Pause so the CLI can exit when done
56
-
57
- if (choice >= 1 && choice <= detections.length) {
58
- resolve(detections[choice - 1]!)
59
- } else {
60
- resolve(null)
61
- }
62
- }
63
-
64
- process.stdin.on('data', onData)
65
- })
66
- }
67
- /**
68
- * Interactive prompt for AI tool choice.
69
- */
70
- async function promptAIToolChoice(detections: AIToolDetection[]): Promise<AIToolDetection | null> {
71
- if (!process.stdin.isTTY) {
72
- log.warn('Multiple AI tools detected but stdin is not interactive')
73
- log.hint(`Using: ${detections[0]!.label} (first match)`)
74
- return detections[0]!
75
- }
76
-
77
- console.log()
78
- console.log(' ? Detected multiple AI tools:')
79
- detections.forEach((d, i) => {
80
- // Map toolModes array to human-readable labels
81
- const modeLabels = d.toolModes.map(mode =>
82
- mode === 'plugin' ? 'VS Code Extension' : 'Terminal CLI',
83
- )
84
- const modeStr = modeLabels.join(' & ')
85
-
86
- const status = d.supported ? ` (supported ${modeStr})` : ` (unsupported/limited)`
87
- console.log(` ${i + 1}. ${d.label}${status}`)
88
- })
89
- console.log()
90
-
91
- return new Promise(resolve => {
92
- process.stdout.write(' > Your choice: ')
93
-
94
- // We must resume stdin in case it was paused by a previous prompt
95
- process.stdin.resume()
96
- process.stdin.setEncoding('utf-8')
97
-
98
- const onData = (data: Buffer) => {
99
- const choice = parseInt(String(data).trim(), 10)
100
-
101
- // Cleanup the listener to avoid memory leaks or multiple fires
102
- process.stdin.off('data', onData)
103
- process.stdin.pause() // Pause so the CLI can exit when done
104
-
105
- if (choice >= 1 && choice <= detections.length) {
106
- resolve(detections[choice - 1]!)
107
- } else {
108
- resolve(null)
109
- }
110
- }
111
-
112
- process.stdin.on('data', onData)
113
- })
114
- }
115
-
116
- async function promptConfigChoice(
117
- detections: BuildToolDetection[],
118
- ): Promise<BuildToolDetection | null> {
119
- if (!process.stdin.isTTY) {
120
- log.warn('Multiple config files detected but stdin is not interactive')
121
- log.hint(`Using: ${detections[0]!.label} (first match)`)
122
- return detections[0]!
123
- }
124
-
125
- console.log()
126
- console.log(' ? Detected multiple build tool configs:')
127
- detections.forEach((d, i) => {
128
- // Determine recommendation based on whether it seems like the "main" one
129
- // We can infer Rsbuild > Rspack > Vite > Webpack as a priority if needed,
130
- // but without package.json scripts analysis, we just highlight them plainly.
131
- console.log(` ${i + 1}. ${d.label}`)
132
- })
133
- console.log(` ${detections.length + 1}. Skip (I'll configure manually)`)
134
- console.log()
135
-
136
- return new Promise(resolve => {
137
- process.stdout.write(' > Your choice: ')
138
-
139
- // We must resume stdin in case it was paused by a previous prompt
140
- process.stdin.resume()
141
- process.stdin.setEncoding('utf-8')
142
-
143
- const onData = (data: Buffer) => {
144
- const choice = parseInt(String(data).trim(), 10)
145
-
146
- // Cleanup the listener to avoid memory leaks or multiple fires
147
- process.stdin.off('data', onData)
148
- process.stdin.pause() // Pause so the CLI can exit when done
149
-
150
- if (choice >= 1 && choice <= detections.length) {
151
- resolve(detections[choice - 1]!)
152
- } else {
153
- resolve(null)
154
- }
155
- }
156
-
157
- process.stdin.on('data', onData)
158
- })
159
- }
21
+ import type { InitOptions, InstallLock, Mutation } from '../types.js'
22
+ import {
23
+ promptIDEChoice,
24
+ promptProviderChoice,
25
+ promptConfigChoice,
26
+ promptUnsupportedFrameworkContinue,
27
+ } from '../prompts.js'
28
+ import { printNextJsManualInstructions, printNuxtManualInstructions } from '../instructions.js'
160
29
 
161
30
  export async function init(options: InitOptions): Promise<void> {
162
31
  const root = process.cwd()
@@ -172,56 +41,79 @@ export async function init(options: InitOptions): Promise<void> {
172
41
  }
173
42
 
174
43
  // ---- Step 2: Detect environment ----
44
+ const [pm, frameworkResult, buildResult, ideProbe, providerProbe] = await Promise.all([
45
+ detectPackageManager(root),
46
+ detectFrameworks(root),
47
+ detectBuildTools(root),
48
+ detectIDE(root),
49
+ detectProviders(root),
50
+ ])
175
51
 
176
52
  // Package manager
177
- const pm = await detectPackageManager(root)
178
53
  log.success(`Detected package manager: ${pm}`)
179
54
 
180
- // Framework detection (supported + unsupported)
181
- const frameworkResult = await detectFrameworks(root)
55
+ // Framework verification
182
56
  if (frameworkResult.supported.length > 0) {
183
57
  log.success(`Detected framework: ${frameworkResult.supported.join(', ')}`)
184
58
  }
185
- if (frameworkResult.unsupported.length > 0) {
59
+
60
+ const isSupported = frameworkResult.supported.length > 0
61
+ const hasUnsupported = frameworkResult.unsupported.length > 0
62
+
63
+ if (!isSupported) {
64
+ if (hasUnsupported) {
65
+ const names = frameworkResult.unsupported.map(f => f.name).join(', ')
66
+ log.warn(`Detected ${names} — not supported in v1 (React / Vue only)`)
67
+ } else {
68
+ log.warn('No frontend framework detected')
69
+ log.hint('Inspecto current version supports React and Vue projects')
70
+ }
71
+
72
+ if (!options.force) {
73
+ const shouldContinue = await promptUnsupportedFrameworkContinue()
74
+ if (!shouldContinue) {
75
+ log.warn('Initialization aborted.')
76
+ return
77
+ }
78
+ } else {
79
+ log.warn('Continuing anyway (--force)')
80
+ }
81
+ } else if (hasUnsupported) {
186
82
  const names = frameworkResult.unsupported.map(f => f.name).join(', ')
187
- log.warn(`Detected ${names} — not supported in v1 (React / Vue only)`)
188
- log.hint('Inspecto may still work but is not tested for this framework')
189
- }
190
- if (frameworkResult.supported.length === 0 && frameworkResult.unsupported.length === 0) {
191
- log.warn('No frontend framework detected')
192
- log.hint('Inspecto v1 supports React and Vue projects')
83
+ log.hint(
84
+ `Note: Inspecto will be configured for ${frameworkResult.supported.join(', ')}. Other detected frameworks (${names}) will be ignored.`,
85
+ )
193
86
  }
194
87
 
195
- // Build tool detection (supported + unsupported)
196
- const buildResult = await detectBuildTools(root)
88
+ // Build tool detection
89
+ let manualConfigRequiredFor = ''
197
90
  if (buildResult.supported.length > 0) {
198
91
  buildResult.supported.forEach(bt => log.success(`Detected: ${bt.label}`))
199
92
  }
200
93
  if (buildResult.unsupported.length > 0) {
201
94
  const names = buildResult.unsupported.join(', ')
202
- log.warn(`Detected ${names} not supported in v1`)
203
- log.hint('v1 supports: Vite, Webpack, Rspack, esbuild, Rollup')
204
- log.hint('Meta-framework support (Next.js, Nuxt, etc.) is planned for v2')
95
+ manualConfigRequiredFor = buildResult.unsupported[0] || ''
96
+ log.warn(`Detected ${names} automatic plugin injection is not supported in current version`)
97
+ log.hint('You can still manually configure it by modifying your configuration file')
205
98
  }
206
99
  if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
207
100
  log.warn('No recognized build tool detected')
208
- log.hint('v1 supports: Vite, Webpack, Rspack, esbuild, Rollup')
101
+ log.hint('current version supports: Vite, Webpack, Rspack, esbuild, Rollup')
209
102
  log.hint('Dependency will be installed but plugin injection will be skipped')
103
+ log.hint(
104
+ 'Please refer to the manual setup guide: https://inspecto-dev.github.io/inspecto/guide/manual-installation',
105
+ )
210
106
  }
211
107
 
212
108
  // IDE detection
213
- const ideProbe = await detectIDE(root)
214
109
  let selectedIDE: { ide: string; supported: boolean } | null = null
215
110
 
216
111
  if (ideProbe.detected.length === 0) {
217
112
  log.error('No IDE detected in current project')
218
113
  log.hint('Please open this project in a supported IDE (like VS Code)')
219
- // We could potentially block here, but we will allow the rest of the CLI to run
220
- // for settings generation and build config injection.
221
114
  } else if (ideProbe.detected.length === 1) {
222
115
  selectedIDE = ideProbe.detected[0]!
223
116
  } else {
224
- // Has multiple
225
117
  selectedIDE = await promptIDEChoice(ideProbe.detected)
226
118
  }
227
119
 
@@ -238,24 +130,21 @@ export async function init(options: InitOptions): Promise<void> {
238
130
  }
239
131
 
240
132
  // AI Tool detection
241
- const aiToolProbe = await detectAITools(root)
242
- let selectedAITool: AIToolDetection | null = null
133
+ let selectedProvider: ProviderDetection | null = null
243
134
 
244
- // If user passed --prefer, we trust it over probing, but if they didn't:
245
- if (!options.prefer) {
246
- if (aiToolProbe.detected.length === 0) {
135
+ if (!options.provider) {
136
+ if (providerProbe.detected.length === 0) {
247
137
  log.warn('No supported AI tools detected')
248
138
  log.hint('Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot')
249
- } else if (aiToolProbe.detected.length === 1) {
250
- selectedAITool = aiToolProbe.detected[0]!
251
- if (selectedAITool.supported) {
252
- log.success(`Detected AI tool: ${selectedAITool.label}`)
139
+ } else if (providerProbe.detected.length === 1) {
140
+ selectedProvider = providerProbe.detected[0]!
141
+ if (selectedProvider.supported) {
142
+ log.success(`Detected AI tool: ${selectedProvider.label}`)
253
143
  }
254
144
  } else {
255
- // Has multiple AI Tools
256
- selectedAITool = await promptAIToolChoice(aiToolProbe.detected)
257
- if (selectedAITool) {
258
- log.success(`Selected AI tool: ${selectedAITool.label}`)
145
+ selectedProvider = await promptProviderChoice(providerProbe.detected)
146
+ if (selectedProvider) {
147
+ log.success(`Selected provider: ${selectedProvider.label}`)
259
148
  }
260
149
  }
261
150
  }
@@ -275,22 +164,12 @@ export async function init(options: InitOptions): Promise<void> {
275
164
  throw new Error(result.stderr)
276
165
  }
277
166
  log.success('Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies')
278
- mutations.push({
279
- type: 'dependency_added',
280
- name: '@inspecto-dev/plugin',
281
- dev: true,
282
- })
283
- mutations.push({
284
- type: 'dependency_added',
285
- name: '@inspecto-dev/core',
286
- dev: true,
287
- })
167
+ mutations.push({ type: 'dependency_added', name: '@inspecto-dev/plugin', dev: true })
168
+ mutations.push({ type: 'dependency_added', name: '@inspecto-dev/core', dev: true })
288
169
  } catch (err: any) {
289
170
  installFailed = true
290
171
  log.error(`Failed to install dependency: ${err?.message || 'Unknown error'}`)
291
172
  log.hint(`Run manually: ${installCmd}`)
292
- // We do not return here to allow the rest of the setup to continue,
293
- // but we will show a warning at the end instead of the success message.
294
173
  }
295
174
  }
296
175
  }
@@ -319,68 +198,58 @@ export async function init(options: InitOptions): Promise<void> {
319
198
 
320
199
  // ---- Step 5: Generate default settings ----
321
200
  const settingsDir = path.join(root, '.inspecto')
322
- const settingsPath = path.join(settingsDir, 'settings.json')
323
- const promptsPath = path.join(settingsDir, 'prompts.json')
201
+ const settingsFileName = options.shared ? 'settings.json' : 'settings.local.json'
202
+ const promptsFileName = options.shared ? 'prompts.json' : 'prompts.local.json'
203
+
204
+ const settingsPath = path.join(settingsDir, settingsFileName)
205
+ const promptsPath = path.join(settingsDir, promptsFileName)
324
206
 
325
207
  if (await exists(settingsPath)) {
326
- // Attempt to read the existing settings file to see if it's valid JSON
327
208
  const existingSettings = await readJSON(settingsPath)
328
209
  if (existingSettings === null) {
329
- log.warn('.inspecto/settings.json exists but contains invalid JSON')
210
+ log.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`)
330
211
  log.hint('Please fix the syntax errors manually, or delete it and re-run init')
331
212
  } else {
332
- log.success('.inspecto/settings.json already exists (skipped)')
213
+ log.success(`.inspecto/${settingsFileName} already exists (skipped)`)
333
214
  }
334
215
  } else {
335
- // Only omit properties that can be auto-inferred
336
- // The schema allows empty objects or just specifying the IDE/prefer
337
216
  const defaultSettings: Record<string, unknown> = {}
338
217
 
339
218
  if (selectedIDE && selectedIDE.supported) {
340
- defaultSettings.ide = selectedIDE.ide === 'vscode' ? 'vscode' : selectedIDE.ide // Fallback string handling
219
+ defaultSettings.ide =
220
+ selectedIDE.ide.toLowerCase() === 'vscode' ? 'vscode' : selectedIDE.ide.toLowerCase()
341
221
  }
342
222
 
343
- if (options.prefer) {
344
- defaultSettings.prefer = options.prefer
345
- } else if (selectedAITool) {
346
- defaultSettings.prefer = selectedAITool.id
347
-
348
- // If the selected tool has a specific mode (like cli vs plugin), we can pre-configure the providers block
349
- // to ensure it uses the intended mode, providing an optimal out-of-the-box experience.
350
- if (selectedAITool.preferredMode) {
351
- defaultSettings.providers = {
352
- [selectedAITool.id]: {
353
- type: selectedAITool.preferredMode,
354
- },
355
- }
356
- }
223
+ if (options.provider) {
224
+ const tool = options.provider
225
+ const mode = tool === 'coco' ? 'cli' : 'extension'
226
+ defaultSettings['provider.default'] = `${tool}.${mode}`
227
+ } else if (selectedProvider) {
228
+ const toolId = selectedProvider.id as string
229
+ const mode = selectedProvider.preferredMode === 'cli' ? 'cli' : 'extension'
230
+ defaultSettings['provider.default'] = `${toolId}.${mode}`
357
231
  }
358
232
 
359
233
  if (options.dryRun) {
360
- log.dryRun('Would create .inspecto/settings.json')
234
+ log.dryRun(`Would create .inspecto/${settingsFileName}`)
361
235
  } else {
362
236
  await writeJSON(settingsPath, defaultSettings)
363
- log.success('Created .inspecto/settings.json')
364
- mutations.push({ type: 'file_created', path: '.inspecto/settings.json' })
237
+ log.success(`Created .inspecto/${settingsFileName}`)
238
+ mutations.push({ type: 'file_created', path: `.inspecto/${settingsFileName}` })
365
239
  }
366
240
  }
367
241
 
368
- // Generate prompts.json to disable low-frequency intents by default
369
242
  if (await exists(promptsPath)) {
370
- log.success('.inspecto/prompts.json already exists (skipped)')
243
+ log.success(`.inspecto/${promptsFileName} already exists (skipped)`)
371
244
  } else {
372
- const defaultPrompts = [
373
- { id: 'code-review', enabled: false },
374
- { id: 'generate-test', enabled: false },
375
- { id: 'performance', enabled: false },
376
- ]
245
+ const defaultPrompts: unknown[] = []
377
246
 
378
247
  if (options.dryRun) {
379
- log.dryRun('Would create .inspecto/prompts.json')
248
+ log.dryRun(`Would create .inspecto/${promptsFileName}`)
380
249
  } else {
381
250
  await writeJSON(promptsPath, defaultPrompts)
382
- log.success('Created .inspecto/prompts.json (disabling low-frequency intents)')
383
- mutations.push({ type: 'file_created', path: '.inspecto/prompts.json' })
251
+ log.success(`Created .inspecto/${promptsFileName}`)
252
+ mutations.push({ type: 'file_created', path: `.inspecto/${promptsFileName}` })
384
253
  }
385
254
  }
386
255
 
@@ -406,19 +275,17 @@ export async function init(options: InitOptions): Promise<void> {
406
275
  await writeJSON(path.join(settingsDir, 'install.lock'), lock)
407
276
  }
408
277
 
409
- // ---- Step 8: Install VS Code extension ----
410
- // Only attempt if IDE is VS Code or not detected (might be VS Code)
278
+ // ---- Step 8: Install IDE extension ----
411
279
  const shouldInstallExt =
412
280
  !options.noExtension && (!selectedIDE || (selectedIDE && selectedIDE.supported))
413
-
414
281
  let manualExtensionInstallNeeded = false
415
282
 
416
283
  if (options.noExtension) {
417
- log.warn('Skipping VS Code extension (--no-extension)')
284
+ log.warn('Skipping IDE extension (--no-extension)')
418
285
  } else if (!shouldInstallExt) {
419
- // Unsupported IDE detected — skip extension, already warned above
286
+ // Unsupported IDE detected — skip extension
420
287
  } else {
421
- const extMutation = await installExtension(options.dryRun)
288
+ const extMutation = await installExtension(options.dryRun, selectedIDE?.ide)
422
289
  if (extMutation && !options.dryRun) {
423
290
  mutations.push(extMutation)
424
291
 
@@ -441,10 +308,22 @@ export async function init(options: InitOptions): Promise<void> {
441
308
  if (options.dryRun) {
442
309
  log.blank()
443
310
  log.warn('Dry run complete. No files were modified.')
444
- } else if (installFailed || injectionFailed || manualExtensionInstallNeeded) {
311
+ } else if (
312
+ installFailed ||
313
+ injectionFailed ||
314
+ manualExtensionInstallNeeded ||
315
+ manualConfigRequiredFor
316
+ ) {
445
317
  log.blank()
446
318
  log.warn('Setup completed with some manual steps required.')
447
- log.hint('Please check the logs above and complete the manual steps.')
319
+
320
+ if (manualConfigRequiredFor === 'Nuxt') {
321
+ printNuxtManualInstructions()
322
+ } else if (manualConfigRequiredFor === 'Next.js') {
323
+ printNextJsManualInstructions()
324
+ } else {
325
+ log.hint('Please check the logs above and complete the manual steps.')
326
+ }
448
327
  log.blank()
449
328
  } else {
450
329
  log.ready('Ready! Hold Alt + Click any element to inspect.')
@@ -3,7 +3,7 @@
3
3
  // ============================================================
4
4
  import path from 'node:path'
5
5
  import { log } from '../utils/logger.js'
6
- import { exists, readJSON, copyFile, removeDir, removeFile } from '../utils/fs.js'
6
+ import { exists, readJSON, removeDir } from '../utils/fs.js'
7
7
  import { shell } from '../utils/exec.js'
8
8
  import { detectPackageManager, getUninstallCommand } from '../detect/package-manager.js'
9
9
  import { cleanGitignore } from '../inject/gitignore.js'
@@ -43,7 +43,7 @@ export async function teardown(): Promise<void> {
43
43
  log.success('Cleaned .gitignore entries')
44
44
 
45
45
  // Warn about config file
46
- log.warn('Cannot restore build config (no backup reference)')
46
+ log.warn('Cannot restore build config auto-magically')
47
47
  log.hint('Please manually remove the inspecto() plugin from your build config')
48
48
 
49
49
  log.blank()
@@ -57,27 +57,17 @@ export async function teardown(): Promise<void> {
57
57
  for (const mutation of lock.mutations) {
58
58
  switch (mutation.type) {
59
59
  case 'file_modified': {
60
- if (mutation.backup && mutation.path) {
61
- const backupPath = path.join(root, mutation.backup)
62
- const targetPath = path.join(root, mutation.path)
63
-
64
- if (mutation.path === '.gitignore') {
65
- // For .gitignore, use the clean function instead of backup restore
66
- await cleanGitignore(root)
67
- log.success('Cleaned .gitignore entries')
68
- // Remove the .gitignore backup if it exists
69
- await removeFile(backupPath)
70
- } else if (await exists(backupPath)) {
71
- await copyFile(backupPath, targetPath)
72
- await removeFile(backupPath)
73
- log.success(`Restored ${mutation.path} from backup`)
74
- } else {
75
- log.warn(`Backup not found: ${mutation.backup}`)
76
- log.hint(`Please manually remove inspecto from ${mutation.path}`)
77
- }
78
- } else if (mutation.path && mutation.path !== '.gitignore') {
79
- log.warn(`Cannot auto-restore ${mutation.path} (no backup recorded)`)
80
- log.hint(`Please manually remove the inspecto() plugin from ${mutation.path}`)
60
+ if (!mutation.path) break
61
+
62
+ if (mutation.path === '.gitignore') {
63
+ // gitignore is cleaned up at the end of teardown, so we can just log here
64
+ log.success('Cleaned .gitignore entries')
65
+ } else if (mutation.path) {
66
+ // We no longer create .bak files in the new AST approach.
67
+ // In a future update, we can use magicast to auto-remove the plugin from AST.
68
+ // For now, we print a manual warning since we did not backup the file.
69
+ log.warn(`Cannot auto-restore ${mutation.path}`)
70
+ log.hint(`Please manually remove the inspecto plugin from ${mutation.path}`)
81
71
  }
82
72
  break
83
73
  }