@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.
- package/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-test.log +16 -21
- package/CHANGELOG.md +12 -0
- package/README.md +58 -11
- package/bin/inspecto.js +5 -1
- package/dist/bin.d.ts +5 -1
- package/dist/bin.js +89 -50
- package/dist/{chunk-EUCQCD3Y.js → chunk-PDDFPQJS.js} +1954 -1053
- package/dist/index.d.ts +128 -2
- package/dist/index.js +15 -3
- package/package.json +2 -1
- package/src/bin.ts +139 -67
- package/src/commands/apply.ts +114 -0
- package/src/commands/detect.ts +59 -0
- package/src/commands/doctor.ts +225 -72
- package/src/commands/init.ts +106 -183
- package/src/commands/plan.ts +41 -0
- package/src/detect/build-tool.ts +107 -3
- package/src/index.ts +13 -2
- package/src/inject/ast-injector.ts +20 -9
- package/src/inject/extension.ts +3 -1
- package/src/inject/strategies/vite.ts +2 -1
- package/src/instructions.ts +60 -46
- package/src/onboarding/apply.ts +325 -0
- package/src/onboarding/context.ts +36 -0
- package/src/onboarding/planner.ts +278 -0
- package/src/prompts.ts +54 -11
- package/src/types.ts +95 -0
- package/src/utils/fs.ts +2 -1
- package/src/utils/logger.ts +9 -0
- package/src/utils/output.ts +40 -0
- package/tests/apply.test.ts +537 -0
- package/tests/ast-injector.test.ts +50 -0
- package/tests/build-tool.test.ts +3 -5
- package/tests/detect.test.ts +94 -0
- package/tests/doctor.test.ts +224 -0
- package/tests/init.test.ts +333 -0
- package/tests/instructions.test.ts +61 -0
- package/tests/logger.test.ts +100 -0
- package/tests/plan.test.ts +713 -0
- package/tests/workspace-build-tool.test.ts +75 -0
package/src/commands/init.ts
CHANGED
|
@@ -8,27 +8,26 @@
|
|
|
8
8
|
// ============================================================
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import { log } from '../utils/logger.js'
|
|
11
|
-
import { exists
|
|
12
|
-
import {
|
|
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 {
|
|
19
|
-
import {
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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:
|
|
183
|
-
let
|
|
184
|
-
|
|
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
|
-
|
|
256
|
+
injectionSkippedRequiresManualConfig = true
|
|
230
257
|
}
|
|
231
258
|
|
|
232
|
-
|
|
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
|
-
|
|
249
|
-
if (result.success) {
|
|
250
|
-
mutations.push(...result.mutations)
|
|
251
|
-
} else {
|
|
252
|
-
injectionFailed = true
|
|
253
|
-
}
|
|
268
|
+
supportedBuildTargets.push(target)
|
|
254
269
|
} else {
|
|
255
|
-
|
|
270
|
+
injectionSkippedRequiresManualConfig = true
|
|
256
271
|
log.warn('Skipping plugin injection (manual configuration required)')
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
275
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
374
|
-
installFailed ||
|
|
375
|
-
injectionFailed ||
|
|
376
|
-
manualExtensionInstallNeeded ||
|
|
377
|
-
manualConfigRequiredFor
|
|
378
|
-
) {
|
|
305
|
+
} else {
|
|
379
306
|
log.blank()
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/detect/build-tool.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 ----
|
package/src/inject/extension.ts
CHANGED
|
@@ -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(
|
|
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..."')
|