@inspecto-dev/cli 0.2.0-alpha.4 → 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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-test.log +16 -21
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +58 -11
  5. package/bin/inspecto.js +5 -1
  6. package/dist/bin.d.ts +5 -1
  7. package/dist/bin.js +89 -50
  8. package/dist/{chunk-EUCQCD3Y.js → chunk-PDDFPQJS.js} +1954 -1053
  9. package/dist/index.d.ts +128 -2
  10. package/dist/index.js +15 -3
  11. package/package.json +2 -1
  12. package/src/bin.ts +139 -67
  13. package/src/commands/apply.ts +114 -0
  14. package/src/commands/detect.ts +59 -0
  15. package/src/commands/doctor.ts +225 -72
  16. package/src/commands/init.ts +106 -183
  17. package/src/commands/plan.ts +41 -0
  18. package/src/detect/build-tool.ts +107 -3
  19. package/src/index.ts +13 -2
  20. package/src/inject/ast-injector.ts +20 -9
  21. package/src/inject/extension.ts +3 -1
  22. package/src/inject/strategies/vite.ts +2 -1
  23. package/src/instructions.ts +60 -46
  24. package/src/onboarding/apply.ts +325 -0
  25. package/src/onboarding/context.ts +36 -0
  26. package/src/onboarding/planner.ts +278 -0
  27. package/src/prompts.ts +54 -11
  28. package/src/types.ts +95 -0
  29. package/src/utils/fs.ts +2 -1
  30. package/src/utils/logger.ts +9 -0
  31. package/src/utils/output.ts +40 -0
  32. package/tests/apply.test.ts +537 -0
  33. package/tests/ast-injector.test.ts +50 -0
  34. package/tests/build-tool.test.ts +3 -5
  35. package/tests/detect.test.ts +94 -0
  36. package/tests/doctor.test.ts +224 -0
  37. package/tests/init.test.ts +333 -0
  38. package/tests/instructions.test.ts +61 -0
  39. package/tests/logger.test.ts +100 -0
  40. package/tests/plan.test.ts +713 -0
  41. package/tests/workspace-build-tool.test.ts +75 -0
@@ -8,27 +8,26 @@
8
8
  // ============================================================
9
9
  import path from 'node:path'
10
10
  import { log } from '../utils/logger.js'
11
- import { exists, writeJSON, readJSON } from '../utils/fs.js'
12
- import { shell } from '../utils/exec.js'
13
- import { detectPackageManager, getInstallCommand } from '../detect/package-manager.js'
11
+ import { exists } from '../utils/fs.js'
12
+ import { detectPackageManager } from '../detect/package-manager.js'
14
13
  import { detectBuildTools, resolveInjectionTarget } from '../detect/build-tool.js'
15
14
  import { detectFrameworks } from '../detect/framework.js'
16
15
  import { detectIDE } from '../detect/ide.js'
17
16
  import { detectProviders, type ProviderDetection } from '../detect/provider.js'
18
- import { injectPlugin } from '../inject/ast-injector.js'
19
- import { updateGitignore } from '../inject/gitignore.js'
20
- import { installExtension } from '../inject/extension.js'
21
- import type { InitOptions, InstallLock, Mutation, BuildToolDetection } from '../types.js'
17
+ import { applyOnboardingPlan } from '../onboarding/apply.js'
18
+ import type { InitOptions, Mutation, BuildToolDetection } from '../types.js'
22
19
  import {
23
20
  promptIDEChoice,
24
21
  promptProviderChoice,
25
22
  promptConfigChoice,
23
+ promptMonorepoPackageChoice,
26
24
  promptUnsupportedFrameworkContinue,
27
25
  } from '../prompts.js'
28
26
  import { printNextJsManualInstructions, printNuxtManualInstructions } from '../instructions.js'
29
27
 
30
28
  export async function init(options: InitOptions): Promise<void> {
31
- const root = process.cwd()
29
+ const repoRoot = process.cwd()
30
+ let projectRoot = repoRoot
32
31
  const mutations: Mutation[] = []
33
32
  const normalizedPackages = normalizePackageList(options.packages)
34
33
 
@@ -40,7 +39,7 @@ export async function init(options: InitOptions): Promise<void> {
40
39
  continue
41
40
  }
42
41
 
43
- const absolutePath = path.join(root, pkg)
42
+ const absolutePath = path.join(repoRoot, pkg)
44
43
  if (await exists(absolutePath)) {
45
44
  verifiedPackages.push(pkg)
46
45
  } else {
@@ -57,19 +56,50 @@ export async function init(options: InitOptions): Promise<void> {
57
56
  log.header('Inspecto Setup')
58
57
 
59
58
  // ---- Step 1: Validate project ----
60
- if (!(await exists(path.join(root, 'package.json')))) {
59
+ if (!(await exists(path.join(repoRoot, 'package.json')))) {
61
60
  log.error('No package.json found in current directory')
62
61
  log.hint('Run this command from your project root')
63
62
  return
64
63
  }
65
64
 
66
65
  // ---- Step 2: Detect environment ----
67
- const [pm, frameworkResult, buildResult, ideProbe, providerProbe] = await Promise.all([
68
- detectPackageManager(root),
69
- detectFrameworks(root),
70
- detectBuildTools(root, verifiedPackages.length > 0 ? verifiedPackages : undefined),
71
- detectIDE(root),
72
- detectProviders(root),
66
+ const pm = await detectPackageManager(repoRoot)
67
+ let buildResult = await detectBuildTools(
68
+ repoRoot,
69
+ verifiedPackages.length > 0 ? verifiedPackages : undefined,
70
+ )
71
+
72
+ if (verifiedPackages.length === 0) {
73
+ const monorepoTargets = Array.from(
74
+ new Set(
75
+ buildResult.supported.map(d => d.packagePath).filter((value): value is string => !!value),
76
+ ),
77
+ )
78
+
79
+ if (monorepoTargets.length > 1) {
80
+ log.warn('Monorepo root detected with multiple candidate apps.')
81
+ const selectedPackage = await promptMonorepoPackageChoice(buildResult.supported)
82
+ if (!selectedPackage) {
83
+ log.hint('Run `inspecto init` inside the target app, or pass --packages <app-path>.')
84
+ return
85
+ }
86
+
87
+ projectRoot = path.join(repoRoot, selectedPackage)
88
+ buildResult = await detectBuildTools(repoRoot, [selectedPackage])
89
+ log.info(`Continuing initialization in ${selectedPackage}`)
90
+ } else if (monorepoTargets.length === 1) {
91
+ const [selectedPackage] = monorepoTargets
92
+ projectRoot = path.join(repoRoot, selectedPackage!)
93
+ buildResult = await detectBuildTools(repoRoot, [selectedPackage!])
94
+ log.warn(`Monorepo root detected. Using the only candidate app: ${selectedPackage}`)
95
+ log.hint('Run `inspecto init` inside that app next time to skip this prompt.')
96
+ }
97
+ }
98
+
99
+ const [frameworkResult, ideProbe, providerProbe] = await Promise.all([
100
+ detectFrameworks(projectRoot),
101
+ detectIDE(projectRoot),
102
+ detectProviders(projectRoot),
73
103
  ])
74
104
 
75
105
  // Package manager
@@ -77,7 +107,15 @@ export async function init(options: InitOptions): Promise<void> {
77
107
 
78
108
  // Framework verification
79
109
  if (frameworkResult.supported.length > 0) {
80
- log.success(`Detected framework: ${frameworkResult.supported.join(', ')}`)
110
+ const frameworks = frameworkResult.supported.join(', ')
111
+ log.success(`Detected framework: ${frameworks}`)
112
+ if (frameworkResult.unsupported.length > 0) {
113
+ log.hint(
114
+ `Other frameworks detected (${frameworkResult.unsupported
115
+ .map(f => f.name)
116
+ .join(', ')}) will be skipped in this setup.`,
117
+ )
118
+ }
81
119
  }
82
120
 
83
121
  const isSupported = frameworkResult.supported.length > 0
@@ -125,6 +163,13 @@ export async function init(options: InitOptions): Promise<void> {
125
163
  manualConfigRequiredFor = buildResult.unsupported[0] || ''
126
164
  log.warn(`Detected ${names} — automatic plugin injection is not supported in current version`)
127
165
  log.hint('You can still manually configure it by modifying your configuration file')
166
+
167
+ if (buildResult.unsupported.includes('Next.js')) {
168
+ printNextJsManualInstructions()
169
+ }
170
+ if (buildResult.unsupported.includes('Nuxt')) {
171
+ printNuxtManualInstructions()
172
+ }
128
173
  }
129
174
  if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
130
175
  log.warn('No recognized build tool detected')
@@ -161,6 +206,9 @@ export async function init(options: InitOptions): Promise<void> {
161
206
 
162
207
  // AI Tool detection
163
208
  let selectedProvider: ProviderDetection | null = null
209
+ const explicitProvider = options.provider
210
+ ? (providerProbe.detected.find(provider => provider.id === options.provider) ?? null)
211
+ : null
164
212
 
165
213
  if (!options.provider) {
166
214
  if (providerProbe.detected.length === 0) {
@@ -172,40 +220,19 @@ export async function init(options: InitOptions): Promise<void> {
172
220
  log.success(`Detected AI tool: ${selectedProvider.label}`)
173
221
  }
174
222
  } else {
223
+ log.info('Multiple providers detected, waiting for your selection...')
175
224
  selectedProvider = await promptProviderChoice(providerProbe.detected)
176
225
  if (selectedProvider) {
177
226
  log.success(`Selected provider: ${selectedProvider.label}`)
227
+ } else {
228
+ log.warn('No provider selected. You can set provider.default later in .inspecto/settings.')
178
229
  }
179
230
  }
180
231
  }
181
232
 
182
- // ---- Step 3: Install dependency ----
183
- let installFailed = false
184
- if (options.skipInstall) {
185
- log.warn('Skipping dependency installation (--skip-install)')
186
- } else {
187
- const installCmd = getInstallCommand(pm, '@inspecto-dev/plugin @inspecto-dev/core')
188
- if (options.dryRun) {
189
- log.dryRun(`Would run: ${installCmd}`)
190
- } else {
191
- try {
192
- const result = await shell(installCmd, root)
193
- if (result.stderr && result.stderr.toLowerCase().includes('error')) {
194
- throw new Error(result.stderr)
195
- }
196
- log.success('Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies')
197
- mutations.push({ type: 'dependency_added', name: '@inspecto-dev/plugin', dev: true })
198
- mutations.push({ type: 'dependency_added', name: '@inspecto-dev/core', dev: true })
199
- } catch (err: any) {
200
- installFailed = true
201
- log.error(`Failed to install dependency: ${err?.message || 'Unknown error'}`)
202
- log.hint(`Run manually: ${installCmd}`)
203
- }
204
- }
205
- }
206
-
207
- // ---- Step 4: Inject plugin into build config ----
208
- let injectionFailed = false
233
+ // ---- Step 3: Resolve injection targets ----
234
+ let injectionSkippedRequiresManualConfig = false
235
+ const supportedBuildTargets: BuildToolDetection[] = []
209
236
  if (buildResult.supported.length > 0) {
210
237
  if (verifiedPackages.length > 0) {
211
238
  const targets = buildResult.supported.filter(detection =>
@@ -226,17 +253,10 @@ export async function init(options: InitOptions): Promise<void> {
226
253
  }
227
254
 
228
255
  if (targets.length === 0) {
229
- injectionFailed = true
256
+ injectionSkippedRequiresManualConfig = true
230
257
  }
231
258
 
232
- for (const target of targets) {
233
- const result = await injectPlugin(root, target, options.dryRun)
234
- if (result.success) {
235
- mutations.push(...result.mutations)
236
- } else {
237
- injectionFailed = true
238
- }
239
- }
259
+ supportedBuildTargets.push(...targets)
240
260
  } else {
241
261
  let target = resolveInjectionTarget(buildResult.supported)
242
262
 
@@ -245,150 +265,53 @@ export async function init(options: InitOptions): Promise<void> {
245
265
  }
246
266
 
247
267
  if (target) {
248
- const result = await injectPlugin(root, target, options.dryRun)
249
- if (result.success) {
250
- mutations.push(...result.mutations)
251
- } else {
252
- injectionFailed = true
253
- }
268
+ supportedBuildTargets.push(target)
254
269
  } else {
255
- injectionFailed = true
270
+ injectionSkippedRequiresManualConfig = true
256
271
  log.warn('Skipping plugin injection (manual configuration required)')
257
272
  }
258
273
  }
259
274
  }
260
275
 
261
- // ---- Step 5: Generate default settings ----
262
- const settingsDir = path.join(root, '.inspecto')
263
- const settingsFileName = options.shared ? 'settings.json' : 'settings.local.json'
264
- const promptsFileName = options.shared ? 'prompts.json' : 'prompts.local.json'
265
-
266
- const settingsPath = path.join(settingsDir, settingsFileName)
267
- const promptsPath = path.join(settingsDir, promptsFileName)
268
-
269
- if (await exists(settingsPath)) {
270
- const existingSettings = await readJSON(settingsPath)
271
- if (existingSettings === null) {
272
- log.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`)
273
- log.hint('Please fix the syntax errors manually, or delete it and re-run init')
274
- } else {
275
- log.success(`.inspecto/${settingsFileName} already exists (skipped)`)
276
- }
277
- } else {
278
- const defaultSettings: Record<string, unknown> = {}
279
-
280
- if (selectedIDE && selectedIDE.supported) {
281
- defaultSettings.ide =
282
- selectedIDE.ide.toLowerCase() === 'vscode' ? 'vscode' : selectedIDE.ide.toLowerCase()
283
- }
284
-
285
- if (options.provider) {
286
- const tool = options.provider
287
- const mode = tool === 'coco' ? 'cli' : 'extension'
288
- defaultSettings['provider.default'] = `${tool}.${mode}`
289
- } else if (selectedProvider) {
290
- const toolId = selectedProvider.id as string
291
- const mode = selectedProvider.preferredMode === 'cli' ? 'cli' : 'extension'
292
- defaultSettings['provider.default'] = `${toolId}.${mode}`
293
- }
294
-
295
- if (options.dryRun) {
296
- log.dryRun(`Would create .inspecto/${settingsFileName}`)
297
- } else {
298
- await writeJSON(settingsPath, defaultSettings)
299
- log.success(`Created .inspecto/${settingsFileName}`)
300
- mutations.push({ type: 'file_created', path: `.inspecto/${settingsFileName}` })
301
- }
302
- }
303
-
304
- if (await exists(promptsPath)) {
305
- log.success(`.inspecto/${promptsFileName} already exists (skipped)`)
306
- } else {
307
- const defaultPrompts: unknown[] = []
308
-
309
- if (options.dryRun) {
310
- log.dryRun(`Would create .inspecto/${promptsFileName}`)
311
- } else {
312
- await writeJSON(promptsPath, defaultPrompts)
313
- log.success(`Created .inspecto/${promptsFileName}`)
314
- mutations.push({ type: 'file_created', path: `.inspecto/${promptsFileName}` })
315
- }
316
- }
317
-
318
- // ---- Step 6: Update .gitignore ----
319
- if (!options.dryRun) {
320
- await updateGitignore(root, options.shared, options.dryRun)
321
- mutations.push({
322
- type: 'file_modified',
323
- path: '.gitignore',
324
- description: 'Appended .inspecto/ ignore rules',
325
- })
326
- } else {
327
- log.dryRun('Would update .gitignore')
328
- }
329
-
330
- // ---- Step 7: Write install.lock ----
331
- if (!options.dryRun && mutations.length > 0) {
332
- const lock: InstallLock = {
333
- version: '1.0.0',
334
- created_at: new Date().toISOString(),
335
- mutations,
336
- }
337
- await writeJSON(path.join(settingsDir, 'install.lock'), lock)
338
- }
339
-
340
- // ---- Step 8: Install IDE extension ----
341
- const shouldInstallExt =
342
- !options.noExtension && (!selectedIDE || (selectedIDE && selectedIDE.supported))
343
- let manualExtensionInstallNeeded = false
344
-
345
- if (options.noExtension) {
346
- log.warn('Skipping IDE extension (--no-extension)')
347
- } else if (!shouldInstallExt) {
348
- // Unsupported IDE detected — skip extension
349
- } else {
350
- const extMutation = await installExtension(options.dryRun, selectedIDE?.ide)
351
- if (extMutation && !options.dryRun) {
352
- mutations.push(extMutation)
353
-
354
- if (extMutation.manual_action_required) {
355
- manualExtensionInstallNeeded = true
356
- }
357
-
358
- const lockPath = path.join(settingsDir, 'install.lock')
359
- const lock = await readJSON<InstallLock>(lockPath)
360
- if (lock) {
361
- lock.mutations = mutations
362
- await writeJSON(lockPath, lock)
363
- }
364
- } else if (extMutation === null && !options.dryRun) {
365
- manualExtensionInstallNeeded = true
366
- }
367
- }
276
+ const providerDefault = options.provider
277
+ ? `${options.provider}.${explicitProvider?.preferredMode ?? (options.provider === 'coco' ? 'cli' : 'extension')}`
278
+ : selectedProvider
279
+ ? `${selectedProvider.id}.${selectedProvider.preferredMode === 'cli' ? 'cli' : 'extension'}`
280
+ : undefined
281
+
282
+ const applyResult = await applyOnboardingPlan({
283
+ repoRoot,
284
+ projectRoot,
285
+ packageManager: pm,
286
+ supportedBuildTargets,
287
+ options: {
288
+ shared: options.shared,
289
+ skipInstall: options.skipInstall,
290
+ dryRun: options.dryRun,
291
+ noExtension: options.noExtension,
292
+ },
293
+ selectedIDE,
294
+ providerDefault,
295
+ manualConfigRequiredFor,
296
+ injectionSkippedRequiresManualConfig,
297
+ allowManualPlanApply: true,
298
+ })
299
+ mutations.push(...applyResult.mutations)
368
300
 
369
301
  // ---- Done ----
370
302
  if (options.dryRun) {
371
303
  log.blank()
372
304
  log.warn('Dry run complete. No files were modified.')
373
- } else if (
374
- installFailed ||
375
- injectionFailed ||
376
- manualExtensionInstallNeeded ||
377
- manualConfigRequiredFor
378
- ) {
305
+ } else {
379
306
  log.blank()
380
- log.warn('Setup completed with some manual steps required.')
381
-
382
- if (manualConfigRequiredFor === 'Nuxt') {
383
- printNuxtManualInstructions()
384
- } else if (manualConfigRequiredFor === 'Next.js') {
385
- printNextJsManualInstructions()
307
+ if (applyResult.postInstall.nextSteps.length > 0) {
308
+ log.warn('──────── Manual Steps Required ────────')
309
+ applyResult.postInstall.nextSteps.forEach(step => log.error(step))
310
+ log.hint('Complete the items above.')
311
+ log.blank()
386
312
  } else {
387
- log.hint('Please check the logs above and complete the manual steps.')
313
+ log.ready('Ready! Hold Alt + Click any element to inspect.')
388
314
  }
389
- log.blank()
390
- } else {
391
- log.ready('Ready! Hold Alt + Click any element to inspect.')
392
315
  }
393
316
  }
394
317
 
@@ -0,0 +1,41 @@
1
+ import { log } from '../utils/logger.js'
2
+ import { writeCommandOutput } from '../utils/output.js'
3
+ import { buildOnboardingContext } from '../onboarding/context.js'
4
+ import { createPlanResult } from '../onboarding/planner.js'
5
+ import type { PlanResult } from '../types.js'
6
+
7
+ function printPlanResult(result: PlanResult): void {
8
+ log.header('Inspecto Plan')
9
+ log.info(`Status: ${result.status}`)
10
+ log.info(`Strategy: ${result.strategy}`)
11
+
12
+ if (result.defaults.provider) {
13
+ log.info(`Default provider: ${result.defaults.provider}`)
14
+ }
15
+ if (result.defaults.ide) {
16
+ log.info(`Default IDE: ${result.defaults.ide}`)
17
+ }
18
+ log.info(`Shared mode: ${result.defaults.shared ? 'enabled' : 'disabled'}`)
19
+ log.info(`Extension mode: ${result.defaults.extension ? 'enabled' : 'disabled'}`)
20
+
21
+ if (result.actions.length > 0) {
22
+ log.blank()
23
+ log.info('Actions:')
24
+ for (const action of result.actions) {
25
+ log.hint(`${action.type}: ${action.target} — ${action.description}`)
26
+ }
27
+ }
28
+
29
+ for (const blocker of result.blockers) {
30
+ log.error(blocker.message)
31
+ }
32
+ for (const warning of result.warnings) {
33
+ log.warn(warning.message)
34
+ }
35
+ }
36
+
37
+ export async function plan(json = false): Promise<PlanResult> {
38
+ const context = await buildOnboardingContext(process.cwd())
39
+ const result = createPlanResult(context)
40
+ return writeCommandOutput(result, json, printPlanResult)
41
+ }
@@ -5,8 +5,9 @@
5
5
  // Recognized but unsupported: Next.js / Nuxt / Remix / Astro / SvelteKit
6
6
  // ============================================================
7
7
  import path from 'node:path'
8
+ import fs from 'node:fs/promises'
8
9
  import { createRequire } from 'node:module'
9
- import { exists, readJSON } from '../utils/fs.js'
10
+ import { exists, readFile, readJSON } from '../utils/fs.js'
10
11
  import type { BuildTool, BuildToolDetection } from '../types.js'
11
12
 
12
13
  interface PackageJSON {
@@ -128,6 +129,89 @@ function createTargets(root: string, packagePaths?: string[]): DetectionTarget[]
128
129
  }))
129
130
  }
130
131
 
132
+ async function getWorkspacePackagePatterns(root: string): Promise<string[]> {
133
+ const pkg = await readJSON<{ workspaces?: string[] | { packages?: string[] } }>(
134
+ path.join(root, 'package.json'),
135
+ )
136
+
137
+ const workspaces = pkg?.workspaces
138
+ if (Array.isArray(workspaces)) {
139
+ return workspaces
140
+ }
141
+
142
+ if (workspaces && Array.isArray(workspaces.packages)) {
143
+ return workspaces.packages
144
+ }
145
+
146
+ const pnpmWorkspace = await readFile(path.join(root, 'pnpm-workspace.yaml'))
147
+ if (!pnpmWorkspace) {
148
+ return []
149
+ }
150
+
151
+ const patterns: string[] = []
152
+ for (const line of pnpmWorkspace.split('\n')) {
153
+ const match = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/)
154
+ if (match?.[1]) {
155
+ patterns.push(match[1])
156
+ }
157
+ }
158
+
159
+ return patterns
160
+ }
161
+
162
+ async function expandWorkspacePattern(root: string, pattern: string): Promise<string[]> {
163
+ const normalized = pattern.replace(/\\/g, '/').replace(/\/$/, '')
164
+ if (!normalized || normalized.startsWith('!')) {
165
+ return []
166
+ }
167
+
168
+ if (!normalized.includes('*')) {
169
+ return (await exists(path.join(root, normalized))) ? [normalized] : []
170
+ }
171
+
172
+ const starIndex = normalized.indexOf('*')
173
+ const baseDir = normalized.slice(0, starIndex).replace(/\/$/, '')
174
+ const suffix = normalized.slice(starIndex + 1)
175
+
176
+ if (suffix && suffix !== '') {
177
+ return []
178
+ }
179
+
180
+ const absoluteBaseDir = path.join(root, baseDir)
181
+ if (!(await exists(absoluteBaseDir))) {
182
+ return []
183
+ }
184
+
185
+ try {
186
+ const entries = await fs.readdir(absoluteBaseDir, { withFileTypes: true })
187
+ return entries
188
+ .filter(entry => entry.isDirectory())
189
+ .map(entry => path.posix.join(baseDir, entry.name))
190
+ } catch {
191
+ return []
192
+ }
193
+ }
194
+
195
+ async function detectWorkspaceTargets(root: string): Promise<DetectionTarget[]> {
196
+ const patterns = await getWorkspacePackagePatterns(root)
197
+ if (patterns.length === 0) {
198
+ return []
199
+ }
200
+
201
+ const packagePaths = new Set<string>()
202
+ for (const pattern of patterns) {
203
+ const expanded = await expandWorkspacePattern(root, pattern)
204
+ for (const packagePath of expanded) {
205
+ packagePaths.add(packagePath)
206
+ }
207
+ }
208
+
209
+ return Array.from(packagePaths).map(packagePath => ({
210
+ packagePath,
211
+ absolutePath: path.join(root, packagePath),
212
+ }))
213
+ }
214
+
131
215
  /**
132
216
  * Detect all build tools / meta-frameworks.
133
217
  * Returns supported tools and recognized-but-unsupported meta-frameworks.
@@ -138,7 +222,10 @@ export async function detectBuildTools(
138
222
  ): Promise<BuildToolResult> {
139
223
  const supported: BuildToolDetection[] = []
140
224
  const unsupported = new Set<string>()
141
- const targets = createTargets(root, packagePaths)
225
+ const explicitTargets = createTargets(root, packagePaths)
226
+ const workspaceTargets =
227
+ !packagePaths || packagePaths.length === 0 ? await detectWorkspaceTargets(root) : []
228
+ const targets = workspaceTargets.length > 0 ? workspaceTargets : explicitTargets
142
229
 
143
230
  for (const target of targets) {
144
231
  const pkg = await readJSON<PackageJSON>(path.join(target.absolutePath, 'package.json'))
@@ -230,6 +317,7 @@ async function detectPattern({
230
317
  }
231
318
 
232
319
  let detectedFile = ''
320
+ let inferredFromScripts = false
233
321
 
234
322
  if (pattern.tool === 'esbuild' && !hasDep) {
235
323
  return null
@@ -274,6 +362,7 @@ async function detectPattern({
274
362
  }
275
363
 
276
364
  if (!detectedFile) {
365
+ inferredFromScripts = true
277
366
  detectedFile = 'package.json (scripts)'
278
367
  break
279
368
  }
@@ -282,6 +371,21 @@ async function detectPattern({
282
371
  }
283
372
 
284
373
  if (!detectedFile) {
374
+ if (
375
+ hasDep &&
376
+ (pattern.tool === 'rollup' ||
377
+ pattern.tool === 'webpack' ||
378
+ pattern.tool === 'rspack' ||
379
+ pattern.tool === 'esbuild')
380
+ ) {
381
+ // dependency present but no config/scripting evidence; provide low-confidence detection
382
+ return {
383
+ tool: pattern.tool,
384
+ configPath: 'package.json (dependency)',
385
+ label: `${pattern.label} (detected via dependency)`,
386
+ packagePath: packagePath || undefined,
387
+ }
388
+ }
285
389
  return null
286
390
  }
287
391
 
@@ -311,7 +415,7 @@ async function detectPattern({
311
415
  configPath: relativeConfig,
312
416
  label: `${pattern.label} (${relativeConfig})${isLegacyRspack ? ' [Legacy]' : ''}${
313
417
  isLegacyWebpack ? ' [Webpack 4]' : ''
314
- }`,
418
+ }${inferredFromScripts ? ' [Scripts Detected]' : ''}`,
315
419
  isLegacyRspack,
316
420
  isLegacyWebpack,
317
421
  packagePath: packagePath || undefined,
package/src/index.ts CHANGED
@@ -1,5 +1,16 @@
1
+ export { apply } from './commands/apply.js'
2
+ export { detect } from './commands/detect.js'
1
3
  export { init } from './commands/init.js'
2
- export { doctor } from './commands/doctor.js'
4
+ export { collectDoctorResult, doctor } from './commands/doctor.js'
5
+ export { plan } from './commands/plan.js'
3
6
  export { teardown } from './commands/teardown.js'
4
- export type { InitOptions, BuildTool, PackageManager, InstallLock } from './types.js'
7
+ export { writeCommandOutput, reportCommandError } from './utils/output.js'
8
+ export type {
9
+ InitOptions,
10
+ BuildTool,
11
+ PackageManager,
12
+ InstallLock,
13
+ DoctorDiagnostic,
14
+ DoctorResult,
15
+ } from './types.js'
5
16
  export type { Framework } from './detect/framework.js'
@@ -15,7 +15,7 @@
15
15
 
16
16
  import path from 'node:path'
17
17
  import { loadFile, writeFile as writeAstFile } from 'magicast'
18
- import { exists, readFile } from '../utils/fs.js'
18
+ import { readFile } from '../utils/fs.js'
19
19
  import { log } from '../utils/logger.js'
20
20
  import type { BuildToolDetection, Mutation } from '../types.js'
21
21
  import { STRATEGIES } from './strategies/index.js'
@@ -33,7 +33,7 @@ function printManualInstructions(
33
33
 
34
34
  if (strategy) {
35
35
  const instructions = strategy.getManualInstructions(detection, reason)
36
- log.codeBlock(instructions)
36
+ log.copyableCodeBlock(instructions)
37
37
  } else {
38
38
  log.error(`Unsupported build tool: ${detection.tool}`)
39
39
  }
@@ -41,13 +41,24 @@ function printManualInstructions(
41
41
 
42
42
  /** Check if inspecto is already injected (idempotency). */
43
43
  function isAlreadyInjected(content: string): boolean {
44
- // Use regex to avoid false positives in comments or variables
45
- return (
46
- /import\s+.*@inspecto-dev\/plugin/.test(content) ||
47
- /require\(['"]@inspecto-dev\/plugin['"]\)/.test(content) ||
48
- /import\s+.*ai-dev-inspector/.test(content) || // Legacy support
49
- /require\(['"]ai-dev-inspector['"]\)/.test(content)
50
- )
44
+ const normalized = content.replace(/\s+/g, ' ')
45
+ const importPlugin = /import\s+(.+?)\s+from\s+['"]@inspecto-dev\/plugin['"]/g
46
+ const requirePlugin = /require\(['"]@inspecto-dev\/plugin['"]\)/
47
+ const legacyImport = /import\s+.*ai-dev-inspector/.test(normalized)
48
+ const legacyRequire = /require\(['"]ai-dev-inspector['"]\)/.test(normalized)
49
+
50
+ if (legacyImport || legacyRequire || requirePlugin.test(normalized)) return true
51
+
52
+ let match: RegExpExecArray | null
53
+ importPlugin.lastIndex = 0
54
+ while ((match = importPlugin.exec(normalized))) {
55
+ const importClause = match[1] || ''
56
+ if (/inspecto/.test(importClause) || /vitePlugin/.test(importClause)) {
57
+ return true
58
+ }
59
+ }
60
+
61
+ return false
51
62
  }
52
63
 
53
64
  // ---- Main injection orchestrator ----
@@ -125,7 +125,9 @@ export async function installExtension(dryRun: boolean, ide?: string): Promise<M
125
125
  // Other IDEs: Prompt to install via VSIX
126
126
  log.warn(`Could not auto-install extension for ${ide}`)
127
127
  log.hint('Please install it manually to enable Inspector features:')
128
- log.hint(' 1. Download the latest .vsix file from Inspecto releases')
128
+ log.hint(
129
+ ' 1. Download the latest .vsix file (Open VSX: https://open-vsx.org/extension/inspecto/inspecto)',
130
+ )
129
131
  log.hint(` 2. Open ${ide}`)
130
132
  log.hint(' 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)')
131
133
  log.hint(' 4. Type and select "Extensions: Install from VSIX..."')