@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.
@@ -0,0 +1,30 @@
1
+ import { addVitePlugin } from 'magicast/helpers'
2
+ import type { InjectStrategy, InjectOptions } from './types.js'
3
+ import type { BuildTool, BuildToolDetection } from '../../types.js'
4
+
5
+ export class ViteStrategy implements InjectStrategy {
6
+ name = 'Vite'
7
+
8
+ supports(tool: BuildTool): boolean {
9
+ return tool === 'vite'
10
+ }
11
+
12
+ inject({ mod, detection }: InjectOptions): void {
13
+ addVitePlugin(mod, {
14
+ from: '@inspecto-dev/plugin',
15
+ constructor: 'vitePlugin',
16
+ })
17
+ }
18
+
19
+ getManualInstructions(detection: BuildToolDetection, reason: string): string[] {
20
+ return [
21
+ `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
22
+ '',
23
+ '// Add to your plugins array:',
24
+ `plugins: [`,
25
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
26
+ ` ...otherPlugins`,
27
+ `].filter(Boolean)`,
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,36 @@
1
+ import type { InjectStrategy, InjectOptions } from './types.js'
2
+ import type { BuildTool, BuildToolDetection } from '../../types.js'
3
+
4
+ export class WebpackStrategy implements InjectStrategy {
5
+ name = 'Webpack'
6
+
7
+ supports(tool: BuildTool): boolean {
8
+ return tool === 'webpack'
9
+ }
10
+
11
+ inject(options: InjectOptions): void {
12
+ // AST manipulation for Webpack configs (often CommonJS or complex objects) is brittle in v1
13
+ throw new Error('Webpack requires manual plugin configuration')
14
+ }
15
+
16
+ getManualInstructions(detection: BuildToolDetection, reason: string): string[] {
17
+ const importPkg = detection.isLegacyWebpack
18
+ ? '@inspecto-dev/plugin/legacy/webpack4'
19
+ : '@inspecto-dev/plugin'
20
+ const pluginName = detection.isLegacyWebpack ? 'webpack4Plugin' : 'webpackPlugin'
21
+
22
+ const pluginCall = detection.isLegacyWebpack
23
+ ? `process.env.NODE_ENV !== 'production' && inspecto({\n pathType: 'absolute',\n escapeTags: ['Transition', 'AnimatePresence'],\n })`
24
+ : `process.env.NODE_ENV !== 'production' && inspecto()`
25
+
26
+ return [
27
+ `import { ${pluginName} as inspecto } from '${importPkg}'`,
28
+ '',
29
+ '// Add to your plugins array:',
30
+ `plugins: [`,
31
+ ` ${pluginCall},`,
32
+ ` ...otherPlugins`,
33
+ `].filter(Boolean)`,
34
+ ]
35
+ }
36
+ }
@@ -0,0 +1,55 @@
1
+ import { log } from './utils/logger.js'
2
+
3
+ export function printNuxtManualInstructions() {
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`)
24
+ }
25
+
26
+ export function printNextJsManualInstructions() {
27
+ 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`)
55
+ }
package/src/prompts.ts ADDED
@@ -0,0 +1,115 @@
1
+ import prompts from 'prompts'
2
+ import { log } from './utils/logger.js'
3
+ import type { ProviderDetection } from './detect/provider.js'
4
+ import type { BuildToolDetection } from './types.js'
5
+
6
+ /**
7
+ * Interactive prompt for IDE choice.
8
+ */
9
+ export async function promptIDEChoice(
10
+ detections: { ide: string; supported: boolean }[],
11
+ ): Promise<{ ide: string; supported: boolean } | null> {
12
+ if (!process.stdin.isTTY) {
13
+ log.warn('Multiple IDEs detected but stdin is not interactive')
14
+ log.hint(`Using: ${detections[0]!.ide} (first match)`)
15
+ return detections[0]!
16
+ }
17
+
18
+ const { choice } = await prompts({
19
+ type: 'select',
20
+ name: 'choice',
21
+ message: 'Detected multiple IDEs, please choose one:',
22
+ choices: detections.map((d, i) => ({
23
+ title: `${d.ide} ${d.supported ? '(supported)' : '(unsupported/limited)'}`,
24
+ value: i,
25
+ })),
26
+ })
27
+
28
+ if (choice === undefined) return null
29
+ return detections[choice]!
30
+ }
31
+
32
+ /**
33
+ * Interactive prompt for AI tool choice.
34
+ */
35
+ export async function promptProviderChoice(
36
+ detections: ProviderDetection[],
37
+ ): Promise<ProviderDetection | null> {
38
+ if (!process.stdin.isTTY) {
39
+ log.warn('Multiple AI tools detected but stdin is not interactive')
40
+ log.hint(`Using: ${detections[0]!.label} (first match)`)
41
+ return detections[0]!
42
+ }
43
+
44
+ const { choice } = await prompts({
45
+ type: 'select',
46
+ name: 'choice',
47
+ message: 'Detected multiple providers, please choose one:',
48
+ choices: detections.map((d, i) => {
49
+ const modeLabels = d.providerModes.map(mode =>
50
+ mode === 'extension' ? 'VS Code Extension' : 'Terminal CLI',
51
+ )
52
+ const modeStr = modeLabels.join(' & ')
53
+ return {
54
+ title: `${d.label} ${d.supported ? `(supported ${modeStr})` : '(unsupported/limited)'}`,
55
+ value: i,
56
+ }
57
+ }),
58
+ })
59
+
60
+ if (choice === undefined) return null
61
+ return detections[choice]!
62
+ }
63
+
64
+ /**
65
+ * Interactive prompt for Build Tool Config choice.
66
+ */
67
+ export async function promptConfigChoice(
68
+ detections: BuildToolDetection[],
69
+ ): Promise<BuildToolDetection | null> {
70
+ if (!process.stdin.isTTY) {
71
+ log.warn('Multiple config files detected but stdin is not interactive')
72
+ log.hint(`Using: ${detections[0]!.label} (first match)`)
73
+ return detections[0]!
74
+ }
75
+
76
+ const choices = detections.map((d, i) => ({
77
+ title: d.label,
78
+ value: i,
79
+ }))
80
+
81
+ choices.push({
82
+ title: "Skip (I'll configure manually)",
83
+ value: -1,
84
+ })
85
+
86
+ const { choice } = await prompts({
87
+ type: 'select',
88
+ name: 'choice',
89
+ message: 'Detected multiple build tool configs, please choose one to inject:',
90
+ choices,
91
+ })
92
+
93
+ if (choice === undefined || choice === -1) return null
94
+ return detections[choice]!
95
+ }
96
+
97
+ /**
98
+ * Interactive prompt for continuing with unsupported frameworks.
99
+ */
100
+ export async function promptUnsupportedFrameworkContinue(): Promise<boolean> {
101
+ if (!process.stdin.isTTY) {
102
+ log.error('Unsupported framework detected in non-interactive environment.')
103
+ log.hint('Use --force to skip this check and continue anyway.')
104
+ return false
105
+ }
106
+
107
+ const { confirm } = await prompts({
108
+ type: 'confirm',
109
+ name: 'confirm',
110
+ message: 'Inspecto may not work properly. Do you want to continue anyway?',
111
+ initial: false,
112
+ })
113
+
114
+ return !!confirm
115
+ }
package/src/types.ts CHANGED
@@ -16,6 +16,8 @@ export interface BuildToolDetection {
16
16
  label: string
17
17
  /** Whether this is a legacy rspack version (< 0.4.0) */
18
18
  isLegacyRspack?: boolean
19
+ /** Whether this is Webpack 4.x */
20
+ isLegacyWebpack?: boolean
19
21
  }
20
22
 
21
23
  /** Options passed to `inspecto init` */
@@ -23,9 +25,10 @@ export interface InitOptions {
23
25
  shared: boolean
24
26
  skipInstall: boolean
25
27
  dryRun: boolean
26
- prefer?: string
28
+ provider?: string
27
29
  noExtension: boolean
28
30
  packages?: string[]
31
+ force: boolean
29
32
  }
30
33
 
31
34
  /** A single mutation recorded in install.lock */
package/tests/ide.test.ts CHANGED
@@ -23,7 +23,7 @@ describe('detectIDE', () => {
23
23
  vi.mocked(fsUtils.exists).mockResolvedValue(false)
24
24
 
25
25
  const result = await detectIDE('/mock/root')
26
- expect(result.detected).toEqual([{ ide: 'Trae', supported: false }])
26
+ expect(result.detected).toEqual([{ ide: 'trae', supported: true }])
27
27
  })
28
28
 
29
29
  it('detects Cursor from environment variables', async () => {
@@ -31,7 +31,7 @@ describe('detectIDE', () => {
31
31
  vi.mocked(fsUtils.exists).mockResolvedValue(false)
32
32
 
33
33
  const result = await detectIDE('/mock/root')
34
- expect(result.detected).toEqual([{ ide: 'Cursor', supported: false }])
34
+ expect(result.detected).toEqual([{ ide: 'cursor', supported: true }])
35
35
  })
36
36
 
37
37
  it('detects VS Code from environment variables without false positives', async () => {
@@ -89,6 +89,6 @@ describe('detectIDE', () => {
89
89
 
90
90
  const result = await detectIDE('/mock/root')
91
91
  // VS Code shouldn't be added since it shares TERM_PROGRAM with Trae
92
- expect(result.detected).toEqual([{ ide: 'Trae', supported: false }])
92
+ expect(result.detected).toEqual([{ ide: 'trae', supported: true }])
93
93
  })
94
94
  })
@@ -1,127 +0,0 @@
1
- // ============================================================
2
- // src/detect/ai-tool.ts — AI Tool detection (v1)
3
- //
4
- // Detects installed AI tools via PATH (CLI) or IDE extensions (Plugin).
5
- // ============================================================
6
- import path from 'node:path'
7
- import { exists, readJSON } from '../utils/fs.js'
8
- import { which } from '../utils/exec.js'
9
- import type { AiTool } from '@inspecto-dev/types'
10
-
11
- export interface AIToolDetection {
12
- id: AiTool
13
- label: string
14
- supported: boolean
15
- toolModes: Array<'cli' | 'plugin'>
16
- // The primary mode that will be written to settings if selected
17
- preferredMode: 'cli' | 'plugin'
18
- }
19
-
20
- const KNOWN_CLI_TOOLS: { id: AiTool; bin: string; label: string; supported: boolean }[] = [
21
- { id: 'claude-code', bin: 'claude', label: 'Claude Code', supported: true },
22
- { id: 'coco', bin: 'coco', label: 'Trae CLI (Coco)', supported: true },
23
- { id: 'codex', bin: 'codex', label: 'Codex CLI', supported: true },
24
- { id: 'gemini', bin: 'gemini', label: 'Gemini CLI', supported: true },
25
- ]
26
-
27
- const KNOWN_IDE_PLUGINS: { id: AiTool; extId: string; label: string; supported: boolean }[] = [
28
- { id: 'claude-code', extId: 'anthropic.claude-code', label: 'Claude Code', supported: true },
29
- { id: 'github-copilot', extId: 'github.copilot', label: 'GitHub Copilot', supported: true },
30
- { id: 'codex', extId: 'openai.chatgpt', label: 'Codex (ChatGPT)', supported: true },
31
- { id: 'gemini', extId: 'google.geminicodeassist', label: 'Gemini Code Assist', supported: true },
32
- ]
33
-
34
- export interface AIToolProbeResult {
35
- detected: AIToolDetection[]
36
- }
37
-
38
- /**
39
- * Detect all installed AI tools by checking PATH binaries and IDE extensions.
40
- */
41
- export async function detectAITools(root: string): Promise<AIToolProbeResult> {
42
- // Use a map to merge duplicate CLI/Plugin detections for the same AI tool
43
- const detectedMap = new Map<AiTool, AIToolDetection>()
44
-
45
- // 1. Detect CLI tools
46
- for (const tool of KNOWN_CLI_TOOLS) {
47
- if (await which(tool.bin)) {
48
- detectedMap.set(tool.id, {
49
- id: tool.id,
50
- label: tool.label,
51
- supported: tool.supported,
52
- toolModes: ['cli'],
53
- preferredMode: 'cli',
54
- })
55
- }
56
- }
57
-
58
- // 2. Detect IDE plugins (VS Code extensions)
59
- // Check the local workspace .vscode/extensions.json first (recommendations)
60
- const extensionsJsonPath = path.join(root, '.vscode', 'extensions.json')
61
- let recommendedExts: string[] = []
62
- if (await exists(extensionsJsonPath)) {
63
- try {
64
- const extData = await readJSON<{ recommendations?: string[] }>(extensionsJsonPath)
65
- if (extData && Array.isArray(extData.recommendations)) {
66
- recommendedExts = extData.recommendations.map(e => e.toLowerCase())
67
- }
68
- } catch {
69
- // ignore JSON parse errors here
70
- }
71
- }
72
-
73
- // Check user's global VS Code extensions folder
74
- const homeDir = process.env.HOME || process.env.USERPROFILE || ''
75
- const globalExtDir = path.join(homeDir, '.vscode', 'extensions')
76
- const globalExtExists = await exists(globalExtDir)
77
-
78
- // Wait for all plugin checks to resolve
79
- for (const plugin of KNOWN_IDE_PLUGINS) {
80
- let isInstalled = false
81
-
82
- // Check if it's explicitly recommended in the workspace
83
- if (recommendedExts.includes(plugin.extId.toLowerCase())) {
84
- isInstalled = true
85
- }
86
- // Otherwise try to find it in the global extensions folder by prefix
87
- else if (globalExtExists) {
88
- try {
89
- const { readdir } = await import('node:fs/promises')
90
- const folders = await readdir(globalExtDir)
91
- if (
92
- folders.some(f => {
93
- const lower = f.toLowerCase()
94
- return (
95
- lower === plugin.extId.toLowerCase() ||
96
- lower.startsWith(plugin.extId.toLowerCase() + '-')
97
- )
98
- })
99
- ) {
100
- isInstalled = true
101
- }
102
- } catch {
103
- // Fallback or ignore
104
- }
105
- }
106
-
107
- if (isInstalled) {
108
- // If we already detected the CLI version of this tool, we append 'plugin' to the modes
109
- // and set the preferredMode to 'plugin' since plugin integration is generally more seamless.
110
- const existing = detectedMap.get(plugin.id)
111
- if (existing) {
112
- existing.toolModes.push('plugin')
113
- existing.preferredMode = 'plugin'
114
- } else {
115
- detectedMap.set(plugin.id, {
116
- id: plugin.id,
117
- label: plugin.label,
118
- supported: plugin.supported,
119
- toolModes: ['plugin'],
120
- preferredMode: 'plugin',
121
- })
122
- }
123
- }
124
- }
125
-
126
- return { detected: Array.from(detectedMap.values()) }
127
- }