@inspecto-dev/cli 0.2.0-alpha.5 → 0.3.0-alpha.1

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 (49) hide show
  1. package/.turbo/turbo-build.log +19 -20
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +93 -11
  4. package/bin/inspecto.js +5 -1
  5. package/dist/bin.d.ts +5 -1
  6. package/dist/bin.js +530 -49
  7. package/dist/chunk-FZS2TLXQ.js +3140 -0
  8. package/dist/index.d.ts +233 -2
  9. package/dist/index.js +17 -3
  10. package/package.json +3 -2
  11. package/src/bin.ts +286 -66
  12. package/src/commands/apply.ts +118 -0
  13. package/src/commands/detect.ts +59 -0
  14. package/src/commands/doctor.ts +225 -72
  15. package/src/commands/init.ts +143 -183
  16. package/src/commands/integration-install.ts +452 -0
  17. package/src/commands/onboard.ts +50 -0
  18. package/src/commands/plan.ts +41 -0
  19. package/src/detect/build-tool.ts +107 -3
  20. package/src/index.ts +17 -2
  21. package/src/inject/ast-injector.ts +17 -6
  22. package/src/inject/extension.ts +40 -22
  23. package/src/inject/gitignore.ts +10 -3
  24. package/src/instructions.ts +60 -46
  25. package/src/onboarding/apply.ts +364 -0
  26. package/src/onboarding/context.ts +36 -0
  27. package/src/onboarding/planner.ts +284 -0
  28. package/src/onboarding/session.ts +434 -0
  29. package/src/onboarding/target-resolution.ts +116 -0
  30. package/src/prompts.ts +54 -11
  31. package/src/types.ts +184 -0
  32. package/src/utils/fs.ts +2 -1
  33. package/src/utils/logger.ts +9 -0
  34. package/src/utils/output.ts +40 -0
  35. package/tests/apply.test.ts +583 -0
  36. package/tests/ast-injector.test.ts +50 -0
  37. package/tests/build-tool.test.ts +3 -5
  38. package/tests/detect.test.ts +94 -0
  39. package/tests/doctor.test.ts +224 -0
  40. package/tests/init.test.ts +364 -0
  41. package/tests/install-wrapper.test.ts +76 -0
  42. package/tests/instructions.test.ts +61 -0
  43. package/tests/integration-install.test.ts +294 -0
  44. package/tests/logger.test.ts +100 -0
  45. package/tests/onboard.test.ts +258 -0
  46. package/tests/plan.test.ts +713 -0
  47. package/tests/workspace-build-tool.test.ts +75 -0
  48. package/.turbo/turbo-test.log +0 -16
  49. package/dist/chunk-MIHQGC3L.js +0 -1720
@@ -8,27 +8,27 @@
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'
19
+ import { resolveOnboardingTarget } from '../onboarding/target-resolution.js'
22
20
  import {
23
21
  promptIDEChoice,
24
22
  promptProviderChoice,
25
23
  promptConfigChoice,
24
+ promptMonorepoPackageChoice,
26
25
  promptUnsupportedFrameworkContinue,
27
26
  } from '../prompts.js'
28
27
  import { printNextJsManualInstructions, printNuxtManualInstructions } from '../instructions.js'
29
28
 
30
29
  export async function init(options: InitOptions): Promise<void> {
31
- const root = process.cwd()
30
+ const repoRoot = process.cwd()
31
+ let projectRoot = repoRoot
32
32
  const mutations: Mutation[] = []
33
33
  const normalizedPackages = normalizePackageList(options.packages)
34
34
 
@@ -40,7 +40,7 @@ export async function init(options: InitOptions): Promise<void> {
40
40
  continue
41
41
  }
42
42
 
43
- const absolutePath = path.join(root, pkg)
43
+ const absolutePath = path.join(repoRoot, pkg)
44
44
  if (await exists(absolutePath)) {
45
45
  verifiedPackages.push(pkg)
46
46
  } else {
@@ -57,19 +57,63 @@ export async function init(options: InitOptions): Promise<void> {
57
57
  log.header('Inspecto Setup')
58
58
 
59
59
  // ---- Step 1: Validate project ----
60
- if (!(await exists(path.join(root, 'package.json')))) {
60
+ if (!(await exists(path.join(repoRoot, 'package.json')))) {
61
61
  log.error('No package.json found in current directory')
62
62
  log.hint('Run this command from your project root')
63
63
  return
64
64
  }
65
65
 
66
66
  // ---- 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),
67
+ const pm = await detectPackageManager(repoRoot)
68
+ let buildResult = await detectBuildTools(
69
+ repoRoot,
70
+ verifiedPackages.length > 0 ? verifiedPackages : undefined,
71
+ )
72
+
73
+ if (verifiedPackages.length === 0) {
74
+ const monorepoCandidates = buildResult.supported.filter(detection => !!detection.packagePath)
75
+ if (monorepoCandidates.length > 0) {
76
+ const frameworkSupportByPackage = await detectFrameworkSupportByPackage(
77
+ repoRoot,
78
+ monorepoCandidates,
79
+ )
80
+ const targetResolution = resolveOnboardingTarget({
81
+ repoRoot,
82
+ buildTools: monorepoCandidates,
83
+ frameworkSupportByPackage,
84
+ })
85
+
86
+ if (targetResolution.status === 'needs_selection') {
87
+ log.warn('Monorepo root detected with multiple candidate apps.')
88
+ const selectedPackage = await promptMonorepoPackageChoice(
89
+ monorepoCandidates.filter(detection =>
90
+ targetResolution.candidates.some(
91
+ candidate => candidate.packagePath === (detection.packagePath ?? ''),
92
+ ),
93
+ ),
94
+ )
95
+ if (!selectedPackage) {
96
+ log.hint('Run `inspecto init` inside the target app, or pass --packages <app-path>.')
97
+ return
98
+ }
99
+
100
+ projectRoot = path.join(repoRoot, selectedPackage)
101
+ buildResult = await detectBuildTools(repoRoot, [selectedPackage])
102
+ log.info(`Continuing initialization in ${selectedPackage}`)
103
+ } else if (targetResolution.selected?.packagePath) {
104
+ const selectedPackage = targetResolution.selected.packagePath
105
+ projectRoot = path.join(repoRoot, selectedPackage)
106
+ buildResult = await detectBuildTools(repoRoot, [selectedPackage])
107
+ log.warn(`Monorepo root detected. Using the only candidate app: ${selectedPackage}`)
108
+ log.hint('Run `inspecto init` inside that app next time to skip this prompt.')
109
+ }
110
+ }
111
+ }
112
+
113
+ const [frameworkResult, ideProbe, providerProbe] = await Promise.all([
114
+ detectFrameworks(projectRoot),
115
+ detectIDE(projectRoot),
116
+ detectProviders(projectRoot),
73
117
  ])
74
118
 
75
119
  // Package manager
@@ -77,7 +121,15 @@ export async function init(options: InitOptions): Promise<void> {
77
121
 
78
122
  // Framework verification
79
123
  if (frameworkResult.supported.length > 0) {
80
- log.success(`Detected framework: ${frameworkResult.supported.join(', ')}`)
124
+ const frameworks = frameworkResult.supported.join(', ')
125
+ log.success(`Detected framework: ${frameworks}`)
126
+ if (frameworkResult.unsupported.length > 0) {
127
+ log.hint(
128
+ `Other frameworks detected (${frameworkResult.unsupported
129
+ .map(f => f.name)
130
+ .join(', ')}) will be skipped in this setup.`,
131
+ )
132
+ }
81
133
  }
82
134
 
83
135
  const isSupported = frameworkResult.supported.length > 0
@@ -125,6 +177,13 @@ export async function init(options: InitOptions): Promise<void> {
125
177
  manualConfigRequiredFor = buildResult.unsupported[0] || ''
126
178
  log.warn(`Detected ${names} — automatic plugin injection is not supported in current version`)
127
179
  log.hint('You can still manually configure it by modifying your configuration file')
180
+
181
+ if (buildResult.unsupported.includes('Next.js')) {
182
+ printNextJsManualInstructions()
183
+ }
184
+ if (buildResult.unsupported.includes('Nuxt')) {
185
+ printNuxtManualInstructions()
186
+ }
128
187
  }
129
188
  if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
130
189
  log.warn('No recognized build tool detected')
@@ -161,6 +220,9 @@ export async function init(options: InitOptions): Promise<void> {
161
220
 
162
221
  // AI Tool detection
163
222
  let selectedProvider: ProviderDetection | null = null
223
+ const explicitProvider = options.provider
224
+ ? (providerProbe.detected.find(provider => provider.id === options.provider) ?? null)
225
+ : null
164
226
 
165
227
  if (!options.provider) {
166
228
  if (providerProbe.detected.length === 0) {
@@ -172,40 +234,19 @@ export async function init(options: InitOptions): Promise<void> {
172
234
  log.success(`Detected AI tool: ${selectedProvider.label}`)
173
235
  }
174
236
  } else {
237
+ log.info('Multiple providers detected, waiting for your selection...')
175
238
  selectedProvider = await promptProviderChoice(providerProbe.detected)
176
239
  if (selectedProvider) {
177
240
  log.success(`Selected provider: ${selectedProvider.label}`)
241
+ } else {
242
+ log.warn('No provider selected. You can set provider.default later in .inspecto/settings.')
178
243
  }
179
244
  }
180
245
  }
181
246
 
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
247
+ // ---- Step 3: Resolve injection targets ----
248
+ let injectionSkippedRequiresManualConfig = false
249
+ const supportedBuildTargets: BuildToolDetection[] = []
209
250
  if (buildResult.supported.length > 0) {
210
251
  if (verifiedPackages.length > 0) {
211
252
  const targets = buildResult.supported.filter(detection =>
@@ -226,17 +267,10 @@ export async function init(options: InitOptions): Promise<void> {
226
267
  }
227
268
 
228
269
  if (targets.length === 0) {
229
- injectionFailed = true
270
+ injectionSkippedRequiresManualConfig = true
230
271
  }
231
272
 
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
- }
273
+ supportedBuildTargets.push(...targets)
240
274
  } else {
241
275
  let target = resolveInjectionTarget(buildResult.supported)
242
276
 
@@ -245,150 +279,57 @@ export async function init(options: InitOptions): Promise<void> {
245
279
  }
246
280
 
247
281
  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
- }
282
+ supportedBuildTargets.push(target)
254
283
  } else {
255
- injectionFailed = true
284
+ injectionSkippedRequiresManualConfig = true
256
285
  log.warn('Skipping plugin injection (manual configuration required)')
257
286
  }
258
287
  }
259
288
  }
260
289
 
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
- }
290
+ const providerDefault = options.provider
291
+ ? `${options.provider}.${explicitProvider?.preferredMode ?? (options.provider === 'coco' ? 'cli' : 'extension')}`
292
+ : selectedProvider
293
+ ? `${selectedProvider.id}.${selectedProvider.preferredMode === 'cli' ? 'cli' : 'extension'}`
294
+ : undefined
295
+
296
+ const applyResult = await applyOnboardingPlan({
297
+ repoRoot,
298
+ projectRoot,
299
+ packageManager: pm,
300
+ supportedBuildTargets,
301
+ options: {
302
+ shared: options.shared,
303
+ skipInstall: options.skipInstall,
304
+ dryRun: options.dryRun,
305
+ noExtension: options.noExtension,
306
+ },
307
+ selectedIDE,
308
+ providerDefault,
309
+ manualConfigRequiredFor,
310
+ injectionSkippedRequiresManualConfig,
311
+ allowManualPlanApply: true,
312
+ })
313
+ mutations.push(...applyResult.mutations)
368
314
 
369
315
  // ---- Done ----
370
316
  if (options.dryRun) {
371
317
  log.blank()
372
318
  log.warn('Dry run complete. No files were modified.')
373
- } else if (
374
- installFailed ||
375
- injectionFailed ||
376
- manualExtensionInstallNeeded ||
377
- manualConfigRequiredFor
378
- ) {
319
+ } else {
379
320
  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()
321
+ if (applyResult.postInstall.nextSteps.length > 0) {
322
+ log.warn('──────── Manual Steps Required ────────')
323
+ applyResult.postInstall.nextSteps.forEach(step => log.error(step))
324
+ log.hint('Complete the items above.')
325
+ log.blank()
386
326
  } else {
387
- log.hint('Please check the logs above and complete the manual steps.')
327
+ log.ready('Ready! Inspecto is set up.')
328
+ log.info('Next:')
329
+ log.hint('1. Start or restart your dev server.')
330
+ log.hint('2. Open your app in the browser.')
331
+ log.hint('3. Hold Alt + Click any element to inspect.')
388
332
  }
389
- log.blank()
390
- } else {
391
- log.ready('Ready! Hold Alt + Click any element to inspect.')
392
333
  }
393
334
  }
394
335
 
@@ -421,3 +362,22 @@ function matchesAnyPackage(detection: BuildToolDetection, packages: string[]): b
421
362
  if (packages.length === 0) return true
422
363
  return packages.some(pkg => matchesPackage(detection, pkg))
423
364
  }
365
+
366
+ async function detectFrameworkSupportByPackage(
367
+ repoRoot: string,
368
+ buildTools: BuildToolDetection[],
369
+ ): Promise<Record<string, string[]>> {
370
+ const packagePaths = Array.from(
371
+ new Set(buildTools.map(buildTool => buildTool.packagePath).filter((value): value is string => !!value)),
372
+ )
373
+ const supportByPackage: Record<string, string[]> = {}
374
+
375
+ await Promise.all(
376
+ packagePaths.map(async packagePath => {
377
+ const result = await detectFrameworks(path.join(repoRoot, packagePath))
378
+ supportByPackage[packagePath] = result.supported
379
+ }),
380
+ )
381
+
382
+ return supportByPackage
383
+ }