@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.
- package/.turbo/turbo-build.log +19 -20
- package/CHANGELOG.md +22 -0
- package/README.md +93 -11
- package/bin/inspecto.js +5 -1
- package/dist/bin.d.ts +5 -1
- package/dist/bin.js +530 -49
- package/dist/chunk-FZS2TLXQ.js +3140 -0
- package/dist/index.d.ts +233 -2
- package/dist/index.js +17 -3
- package/package.json +3 -2
- package/src/bin.ts +286 -66
- package/src/commands/apply.ts +118 -0
- package/src/commands/detect.ts +59 -0
- package/src/commands/doctor.ts +225 -72
- package/src/commands/init.ts +143 -183
- package/src/commands/integration-install.ts +452 -0
- package/src/commands/onboard.ts +50 -0
- package/src/commands/plan.ts +41 -0
- package/src/detect/build-tool.ts +107 -3
- package/src/index.ts +17 -2
- package/src/inject/ast-injector.ts +17 -6
- package/src/inject/extension.ts +40 -22
- package/src/inject/gitignore.ts +10 -3
- package/src/instructions.ts +60 -46
- package/src/onboarding/apply.ts +364 -0
- package/src/onboarding/context.ts +36 -0
- package/src/onboarding/planner.ts +284 -0
- package/src/onboarding/session.ts +434 -0
- package/src/onboarding/target-resolution.ts +116 -0
- package/src/prompts.ts +54 -11
- package/src/types.ts +184 -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 +583 -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 +364 -0
- package/tests/install-wrapper.test.ts +76 -0
- package/tests/instructions.test.ts +61 -0
- package/tests/integration-install.test.ts +294 -0
- package/tests/logger.test.ts +100 -0
- package/tests/onboard.test.ts +258 -0
- package/tests/plan.test.ts +713 -0
- package/tests/workspace-build-tool.test.ts +75 -0
- package/.turbo/turbo-test.log +0 -16
- package/dist/chunk-MIHQGC3L.js +0 -1720
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import { getInstallCommand } from '../detect/package-manager.js'
|
|
4
|
+
import { injectPlugin } from '../inject/ast-injector.js'
|
|
5
|
+
import { installExtension } from '../inject/extension.js'
|
|
6
|
+
import { updateGitignore } from '../inject/gitignore.js'
|
|
7
|
+
import { shell } from '../utils/exec.js'
|
|
8
|
+
import { exists, readJSON, writeJSON } from '../utils/fs.js'
|
|
9
|
+
import { log } from '../utils/logger.js'
|
|
10
|
+
import type {
|
|
11
|
+
BuildToolDetection,
|
|
12
|
+
CommandStatus,
|
|
13
|
+
InstallLock,
|
|
14
|
+
Mutation,
|
|
15
|
+
PackageManager,
|
|
16
|
+
PlanResult,
|
|
17
|
+
} from '../types.js'
|
|
18
|
+
|
|
19
|
+
export interface ApplyOnboardingInput {
|
|
20
|
+
repoRoot: string
|
|
21
|
+
projectRoot: string
|
|
22
|
+
packageManager: PackageManager
|
|
23
|
+
supportedBuildTargets: BuildToolDetection[]
|
|
24
|
+
options: {
|
|
25
|
+
shared: boolean
|
|
26
|
+
skipInstall: boolean
|
|
27
|
+
dryRun: boolean
|
|
28
|
+
noExtension: boolean
|
|
29
|
+
quiet?: boolean | undefined
|
|
30
|
+
}
|
|
31
|
+
selectedIDE?: { ide: string; supported: boolean } | null | undefined
|
|
32
|
+
providerDefault?: string | undefined
|
|
33
|
+
manualConfigRequiredFor?: string | undefined
|
|
34
|
+
injectionSkippedRequiresManualConfig?: boolean | undefined
|
|
35
|
+
plan?: PlanResult | undefined
|
|
36
|
+
allowManualPlanApply?: boolean | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ApplyReporter {
|
|
40
|
+
warn(text: string): void
|
|
41
|
+
success(text: string): void
|
|
42
|
+
error(text: string): void
|
|
43
|
+
hint(text: string): void
|
|
44
|
+
dryRun(text: string): void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ApplySpinner {
|
|
48
|
+
start(): void
|
|
49
|
+
succeed(text: string): void
|
|
50
|
+
fail(text: string): void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ApplyOnboardingResult {
|
|
54
|
+
status: CommandStatus
|
|
55
|
+
mutations: Mutation[]
|
|
56
|
+
postInstall: {
|
|
57
|
+
installFailed: boolean
|
|
58
|
+
injectionFailed: boolean
|
|
59
|
+
manualExtensionInstallNeeded: boolean
|
|
60
|
+
nextSteps: string[]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shellQuote(value: string): string {
|
|
65
|
+
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveRuntimePackages(): {
|
|
69
|
+
installSpec: string
|
|
70
|
+
installedDependencyNames: string[]
|
|
71
|
+
} {
|
|
72
|
+
const devRepo = process.env.INSPECTO_DEV_REPO
|
|
73
|
+
if (!devRepo) {
|
|
74
|
+
return {
|
|
75
|
+
installSpec: '@inspecto-dev/plugin @inspecto-dev/core',
|
|
76
|
+
installedDependencyNames: ['@inspecto-dev/plugin', '@inspecto-dev/core'],
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const normalizedRepo = path.resolve(devRepo)
|
|
81
|
+
return {
|
|
82
|
+
installSpec: [
|
|
83
|
+
shellQuote(path.join(normalizedRepo, 'packages/plugin')),
|
|
84
|
+
shellQuote(path.join(normalizedRepo, 'packages/core')),
|
|
85
|
+
].join(' '),
|
|
86
|
+
installedDependencyNames: ['@inspecto-dev/plugin', '@inspecto-dev/core'],
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resultStatus(nextSteps: string[]): CommandStatus {
|
|
91
|
+
return nextSteps.length > 0 ? 'warning' : 'ok'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function manualPlanSteps(plan: PlanResult): string[] {
|
|
95
|
+
return [
|
|
96
|
+
...plan.blockers.map(blocker => blocker.message),
|
|
97
|
+
...plan.actions
|
|
98
|
+
.filter(action => action.type === 'manual_step')
|
|
99
|
+
.map(action => action.description),
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function applyOnboardingPlan(
|
|
104
|
+
input: ApplyOnboardingInput,
|
|
105
|
+
): Promise<ApplyOnboardingResult> {
|
|
106
|
+
return applyOnboardingPlanInternal(input)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createReporter(quiet = false): ApplyReporter {
|
|
110
|
+
if (quiet) {
|
|
111
|
+
return {
|
|
112
|
+
warn() {},
|
|
113
|
+
success() {},
|
|
114
|
+
error() {},
|
|
115
|
+
hint() {},
|
|
116
|
+
dryRun() {},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
warn(text: string) {
|
|
122
|
+
log.warn(text)
|
|
123
|
+
},
|
|
124
|
+
success(text: string) {
|
|
125
|
+
log.success(text)
|
|
126
|
+
},
|
|
127
|
+
error(text: string) {
|
|
128
|
+
log.error(text)
|
|
129
|
+
},
|
|
130
|
+
hint(text: string) {
|
|
131
|
+
log.hint(text)
|
|
132
|
+
},
|
|
133
|
+
dryRun(text: string) {
|
|
134
|
+
log.dryRun(text)
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createSpinner(text: string, quiet = false): ApplySpinner {
|
|
140
|
+
if (quiet) {
|
|
141
|
+
return {
|
|
142
|
+
start() {},
|
|
143
|
+
succeed() {},
|
|
144
|
+
fail() {},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const spinner = ora(text)
|
|
149
|
+
return {
|
|
150
|
+
start() {
|
|
151
|
+
spinner.start()
|
|
152
|
+
},
|
|
153
|
+
succeed(successText: string) {
|
|
154
|
+
spinner.succeed(successText)
|
|
155
|
+
},
|
|
156
|
+
fail(failureText: string) {
|
|
157
|
+
spinner.fail(failureText)
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function applyOnboardingPlanInternal(
|
|
163
|
+
input: ApplyOnboardingInput,
|
|
164
|
+
): Promise<ApplyOnboardingResult> {
|
|
165
|
+
const reporter = createReporter(input.options.quiet)
|
|
166
|
+
|
|
167
|
+
if (input.plan && input.plan.strategy !== 'supported' && !input.allowManualPlanApply) {
|
|
168
|
+
return {
|
|
169
|
+
status: input.plan.status,
|
|
170
|
+
mutations: [],
|
|
171
|
+
postInstall: {
|
|
172
|
+
installFailed: false,
|
|
173
|
+
injectionFailed: false,
|
|
174
|
+
manualExtensionInstallNeeded: false,
|
|
175
|
+
nextSteps: manualPlanSteps(input.plan),
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const mutations: Mutation[] = []
|
|
181
|
+
const settingsDir = path.join(input.projectRoot, '.inspecto')
|
|
182
|
+
const settingsFileName = input.options.shared ? 'settings.json' : 'settings.local.json'
|
|
183
|
+
const promptsFileName = input.options.shared ? 'prompts.json' : 'prompts.local.json'
|
|
184
|
+
const settingsPath = path.join(settingsDir, settingsFileName)
|
|
185
|
+
const promptsPath = path.join(settingsDir, promptsFileName)
|
|
186
|
+
const runtimePackages = resolveRuntimePackages()
|
|
187
|
+
const installCmd = getInstallCommand(input.packageManager, runtimePackages.installSpec)
|
|
188
|
+
const nextSteps: string[] = []
|
|
189
|
+
|
|
190
|
+
let installFailed = false
|
|
191
|
+
if (input.options.skipInstall) {
|
|
192
|
+
reporter.warn('Skipping dependency installation (--skip-install)')
|
|
193
|
+
} else if (input.options.dryRun) {
|
|
194
|
+
reporter.dryRun(`Would run: ${installCmd}`)
|
|
195
|
+
} else {
|
|
196
|
+
const spinner = createSpinner(
|
|
197
|
+
`Installing devDependencies via: ${installCmd}`,
|
|
198
|
+
input.options.quiet,
|
|
199
|
+
)
|
|
200
|
+
try {
|
|
201
|
+
spinner.start()
|
|
202
|
+
await shell(installCmd, input.projectRoot)
|
|
203
|
+
spinner.succeed('Dependencies installed successfully')
|
|
204
|
+
reporter.success('Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies')
|
|
205
|
+
for (const name of runtimePackages.installedDependencyNames) {
|
|
206
|
+
mutations.push({ type: 'dependency_added', name, dev: true })
|
|
207
|
+
}
|
|
208
|
+
} catch (error: any) {
|
|
209
|
+
spinner.fail('Dependency installation failed')
|
|
210
|
+
installFailed = true
|
|
211
|
+
reporter.error(`Failed to install dependency: ${error?.message || 'Unknown error'}`)
|
|
212
|
+
reporter.hint(`Run manually in ${input.projectRoot}: ${installCmd}`)
|
|
213
|
+
reporter.hint(
|
|
214
|
+
'Setup will continue without dependencies, but Inspecto may not run until installation succeeds.',
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig)
|
|
220
|
+
for (const target of input.supportedBuildTargets) {
|
|
221
|
+
const result = await injectPlugin(
|
|
222
|
+
input.repoRoot,
|
|
223
|
+
target,
|
|
224
|
+
input.options.dryRun,
|
|
225
|
+
input.options.quiet ?? false,
|
|
226
|
+
)
|
|
227
|
+
if (result.success) {
|
|
228
|
+
mutations.push(...result.mutations)
|
|
229
|
+
} else {
|
|
230
|
+
injectionFailed = true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (await exists(settingsPath)) {
|
|
235
|
+
const existingSettings = await readJSON(settingsPath)
|
|
236
|
+
if (existingSettings === null) {
|
|
237
|
+
reporter.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`)
|
|
238
|
+
reporter.hint('Please fix the syntax errors manually, or delete it and re-run init')
|
|
239
|
+
nextSteps.push(`Fix .inspecto/${settingsFileName} or delete it and rerun Inspecto setup.`)
|
|
240
|
+
} else {
|
|
241
|
+
reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`)
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
const defaultSettings: Record<string, unknown> = {}
|
|
245
|
+
|
|
246
|
+
if (input.selectedIDE?.supported) {
|
|
247
|
+
defaultSettings.ide =
|
|
248
|
+
input.selectedIDE.ide.toLowerCase() === 'vscode'
|
|
249
|
+
? 'vscode'
|
|
250
|
+
: input.selectedIDE.ide.toLowerCase()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (input.providerDefault) {
|
|
254
|
+
defaultSettings['provider.default'] = input.providerDefault
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (input.options.dryRun) {
|
|
258
|
+
reporter.dryRun(`Would create .inspecto/${settingsFileName}`)
|
|
259
|
+
} else {
|
|
260
|
+
await writeJSON(settingsPath, defaultSettings)
|
|
261
|
+
reporter.success(`Created .inspecto/${settingsFileName}`)
|
|
262
|
+
mutations.push({ type: 'file_created', path: `.inspecto/${settingsFileName}` })
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (await exists(promptsPath)) {
|
|
267
|
+
reporter.success(`.inspecto/${promptsFileName} already exists (skipped)`)
|
|
268
|
+
} else if (input.options.dryRun) {
|
|
269
|
+
reporter.dryRun(`Would create .inspecto/${promptsFileName}`)
|
|
270
|
+
} else {
|
|
271
|
+
await writeJSON(promptsPath, [])
|
|
272
|
+
reporter.success(`Created .inspecto/${promptsFileName}`)
|
|
273
|
+
mutations.push({ type: 'file_created', path: `.inspecto/${promptsFileName}` })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!input.options.dryRun) {
|
|
277
|
+
await updateGitignore(
|
|
278
|
+
input.projectRoot,
|
|
279
|
+
input.options.shared,
|
|
280
|
+
input.options.dryRun,
|
|
281
|
+
input.options.quiet ?? false,
|
|
282
|
+
)
|
|
283
|
+
mutations.push({
|
|
284
|
+
type: 'file_modified',
|
|
285
|
+
path: '.gitignore',
|
|
286
|
+
description: 'Appended .inspecto/ ignore rules',
|
|
287
|
+
})
|
|
288
|
+
} else {
|
|
289
|
+
reporter.dryRun('Would update .gitignore')
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!input.options.dryRun && mutations.length > 0) {
|
|
293
|
+
const lock: InstallLock = {
|
|
294
|
+
version: '1.0.0',
|
|
295
|
+
created_at: new Date().toISOString(),
|
|
296
|
+
mutations,
|
|
297
|
+
}
|
|
298
|
+
await writeJSON(path.join(settingsDir, 'install.lock'), lock)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const shouldInstallExt =
|
|
302
|
+
!input.options.noExtension && (!input.selectedIDE || input.selectedIDE.supported)
|
|
303
|
+
let manualExtensionInstallNeeded = false
|
|
304
|
+
|
|
305
|
+
if (input.options.noExtension) {
|
|
306
|
+
reporter.warn('Skipping IDE extension (--no-extension)')
|
|
307
|
+
} else if (shouldInstallExt) {
|
|
308
|
+
const extMutation = await installExtension(
|
|
309
|
+
input.options.dryRun,
|
|
310
|
+
input.selectedIDE?.ide,
|
|
311
|
+
input.options.quiet ?? false,
|
|
312
|
+
)
|
|
313
|
+
if (extMutation && !input.options.dryRun) {
|
|
314
|
+
mutations.push(extMutation)
|
|
315
|
+
|
|
316
|
+
if (extMutation.manual_action_required) {
|
|
317
|
+
manualExtensionInstallNeeded = true
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const lockPath = path.join(settingsDir, 'install.lock')
|
|
321
|
+
const lock = await readJSON<InstallLock>(lockPath)
|
|
322
|
+
if (lock) {
|
|
323
|
+
lock.mutations = mutations
|
|
324
|
+
await writeJSON(lockPath, lock)
|
|
325
|
+
}
|
|
326
|
+
} else if (extMutation === null && !input.options.dryRun) {
|
|
327
|
+
manualExtensionInstallNeeded = true
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!input.options.dryRun) {
|
|
332
|
+
if (installFailed) {
|
|
333
|
+
nextSteps.push(`Install dependencies manually in ${input.projectRoot}: ${installCmd}`)
|
|
334
|
+
}
|
|
335
|
+
if (injectionFailed) {
|
|
336
|
+
nextSteps.push(
|
|
337
|
+
'Plugin injection skipped. Follow manual instructions printed above to update your config.',
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
if (manualExtensionInstallNeeded) {
|
|
341
|
+
nextSteps.push('Install the Inspecto IDE extension manually')
|
|
342
|
+
}
|
|
343
|
+
if (input.manualConfigRequiredFor === 'Nuxt') {
|
|
344
|
+
nextSteps.push(
|
|
345
|
+
'Nuxt detected—please follow the Nuxt instructions printed above to finish setup.',
|
|
346
|
+
)
|
|
347
|
+
} else if (input.manualConfigRequiredFor === 'Next.js') {
|
|
348
|
+
nextSteps.push(
|
|
349
|
+
'Next.js detected—please follow the Next.js instructions printed above to finish setup.',
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
status: resultStatus(nextSteps),
|
|
356
|
+
mutations,
|
|
357
|
+
postInstall: {
|
|
358
|
+
installFailed,
|
|
359
|
+
injectionFailed,
|
|
360
|
+
manualExtensionInstallNeeded,
|
|
361
|
+
nextSteps,
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { detectBuildTools } from '../detect/build-tool.js'
|
|
2
|
+
import { detectFrameworks } from '../detect/framework.js'
|
|
3
|
+
import { detectIDE } from '../detect/ide.js'
|
|
4
|
+
import { detectPackageManager } from '../detect/package-manager.js'
|
|
5
|
+
import { detectProviders } from '../detect/provider.js'
|
|
6
|
+
import type { OnboardingContext } from '../types.js'
|
|
7
|
+
|
|
8
|
+
export async function buildOnboardingContext(root: string): Promise<OnboardingContext> {
|
|
9
|
+
const [packageManager, buildTools, frameworks, ides, providers] = await Promise.all([
|
|
10
|
+
detectPackageManager(root),
|
|
11
|
+
detectBuildTools(root),
|
|
12
|
+
detectFrameworks(root),
|
|
13
|
+
detectIDE(root),
|
|
14
|
+
detectProviders(root),
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
root,
|
|
19
|
+
packageManager,
|
|
20
|
+
buildTools: {
|
|
21
|
+
supported: buildTools.supported,
|
|
22
|
+
unsupported: buildTools.unsupported,
|
|
23
|
+
},
|
|
24
|
+
frameworks: {
|
|
25
|
+
supported: frameworks.supported,
|
|
26
|
+
unsupported: frameworks.unsupported.map(item => item.name),
|
|
27
|
+
},
|
|
28
|
+
ides: ides.detected.map(({ ide, supported }) => ({ ide, supported })),
|
|
29
|
+
providers: providers.detected.map(({ id, label, supported, preferredMode }) => ({
|
|
30
|
+
id,
|
|
31
|
+
label,
|
|
32
|
+
supported,
|
|
33
|
+
preferredMode,
|
|
34
|
+
})),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { buildOnboardingContext } from './context.js'
|
|
2
|
+
import type {
|
|
3
|
+
CommandMessage,
|
|
4
|
+
CommandStatus,
|
|
5
|
+
DetectionResult,
|
|
6
|
+
OnboardingContext,
|
|
7
|
+
PlanResult,
|
|
8
|
+
} from '../types.js'
|
|
9
|
+
|
|
10
|
+
function message(code: string, message: string): CommandMessage {
|
|
11
|
+
return { code, message }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function uniqueMessages(messages: CommandMessage[]): CommandMessage[] {
|
|
15
|
+
const seen = new Set<string>()
|
|
16
|
+
return messages.filter(item => {
|
|
17
|
+
const key = `${item.code}:${item.message}`
|
|
18
|
+
if (seen.has(key)) return false
|
|
19
|
+
seen.add(key)
|
|
20
|
+
return true
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function detectionStatus(warnings: CommandMessage[], blockers: CommandMessage[]): CommandStatus {
|
|
25
|
+
if (blockers.length > 0) return 'blocked'
|
|
26
|
+
if (warnings.length > 0) return 'warning'
|
|
27
|
+
return 'ok'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function planStatus(warnings: CommandMessage[], blockers: CommandMessage[]): CommandStatus {
|
|
31
|
+
if (blockers.length > 0) return 'blocked'
|
|
32
|
+
if (warnings.length > 0) return 'warning'
|
|
33
|
+
return 'ok'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function supportedIde(context: OnboardingContext): string | undefined {
|
|
37
|
+
return context.ides.find(ide => ide.supported)?.ide
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function supportedProvider(context: OnboardingContext): string | undefined {
|
|
41
|
+
return context.providers.find(provider => provider.supported)?.id
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildToolBlockers(context: OnboardingContext): CommandMessage[] {
|
|
45
|
+
if (context.buildTools.unsupported.length > 0) {
|
|
46
|
+
return [
|
|
47
|
+
message(
|
|
48
|
+
'unsupported-build-tool',
|
|
49
|
+
`Detected unsupported build tool(s): ${context.buildTools.unsupported.join(', ')}`,
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (context.buildTools.supported.length > 0) {
|
|
55
|
+
if (context.buildTools.supported.length === 1) {
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const targets = context.buildTools.supported
|
|
60
|
+
.map(target => target.packagePath ?? target.configPath)
|
|
61
|
+
.join(', ')
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
message(
|
|
65
|
+
'multiple-supported-build-targets',
|
|
66
|
+
`Multiple supported build targets detected: ${targets}. Run inspecto apply from a single app/package root until explicit target selection is available.`,
|
|
67
|
+
),
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [message('missing-build-tool', 'No supported build tool detected')]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function frameworkBlockers(context: OnboardingContext): CommandMessage[] {
|
|
75
|
+
if (context.frameworks.supported.length > 0) {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (context.frameworks.unsupported.length > 0) {
|
|
80
|
+
return [
|
|
81
|
+
message(
|
|
82
|
+
'unsupported-framework',
|
|
83
|
+
`Detected unsupported framework(s): ${context.frameworks.unsupported.join(', ')}`,
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return [message('missing-framework', 'No supported frontend framework detected')]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function unsupportedEnvironmentWarnings(context: OnboardingContext): CommandMessage[] {
|
|
92
|
+
const warnings: CommandMessage[] = []
|
|
93
|
+
|
|
94
|
+
if (context.frameworks.unsupported.length > 0 && context.frameworks.supported.length > 0) {
|
|
95
|
+
warnings.push(
|
|
96
|
+
message(
|
|
97
|
+
'unsupported-framework-present',
|
|
98
|
+
`Unsupported framework(s) also detected: ${context.frameworks.unsupported.join(', ')}`,
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const unsupportedIdes = context.ides.filter(ide => !ide.supported).map(ide => ide.ide)
|
|
104
|
+
if (unsupportedIdes.length > 0) {
|
|
105
|
+
warnings.push(
|
|
106
|
+
message('unsupported-ide', `Unsupported IDE(s) detected: ${unsupportedIdes.join(', ')}`),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const unsupportedProviders = context.providers
|
|
111
|
+
.filter(provider => !provider.supported)
|
|
112
|
+
.map(provider => provider.label)
|
|
113
|
+
if (unsupportedProviders.length > 0) {
|
|
114
|
+
warnings.push(
|
|
115
|
+
message(
|
|
116
|
+
'unsupported-provider',
|
|
117
|
+
`Unsupported provider(s) detected: ${unsupportedProviders.join(', ')}`,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return warnings
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function manualBuildToolActions(context: OnboardingContext): PlanResult['actions'] {
|
|
126
|
+
if (context.buildTools.unsupported.length > 0) {
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
type: 'manual_step',
|
|
130
|
+
target: context.buildTools.unsupported.join(', '),
|
|
131
|
+
description:
|
|
132
|
+
'Inspecto cannot auto-configure this build stack yet. Follow the manual setup guide for the detected framework or build tool.',
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (context.buildTools.supported.length > 1) {
|
|
138
|
+
const targets = context.buildTools.supported
|
|
139
|
+
.map(target => target.packagePath ?? target.configPath)
|
|
140
|
+
.join(', ')
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
{
|
|
144
|
+
type: 'manual_step',
|
|
145
|
+
target: targets,
|
|
146
|
+
description:
|
|
147
|
+
'Run inspecto apply from the target app/package root. Root-level apply is blocked when multiple supported targets are detected.',
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
type: 'manual_step',
|
|
155
|
+
target: context.root,
|
|
156
|
+
description:
|
|
157
|
+
'No supported build tool was detected. Add a supported build config before trying Inspecto again.',
|
|
158
|
+
},
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function manualFrameworkActions(context: OnboardingContext): PlanResult['actions'] {
|
|
163
|
+
if (context.frameworks.unsupported.length > 0) {
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
type: 'manual_step',
|
|
167
|
+
target: context.frameworks.unsupported.join(', '),
|
|
168
|
+
description:
|
|
169
|
+
'Inspecto cannot auto-configure this framework yet. Follow the manual setup guide for the detected framework.',
|
|
170
|
+
},
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
type: 'manual_step',
|
|
177
|
+
target: context.root,
|
|
178
|
+
description:
|
|
179
|
+
'No supported frontend framework was detected. Add a supported React or Vue app before trying Inspecto again.',
|
|
180
|
+
},
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function createDetectionResult(root: string): Promise<DetectionResult> {
|
|
185
|
+
const context = await buildOnboardingContext(root)
|
|
186
|
+
const warnings = uniqueMessages([...unsupportedEnvironmentWarnings(context)])
|
|
187
|
+
|
|
188
|
+
const buildToolResult = buildToolBlockers(context)
|
|
189
|
+
const frameworkResult = frameworkBlockers(context)
|
|
190
|
+
const blockers = uniqueMessages([...buildToolResult, ...frameworkResult])
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
status: detectionStatus(warnings, blockers),
|
|
194
|
+
warnings,
|
|
195
|
+
blockers,
|
|
196
|
+
project: {
|
|
197
|
+
root: context.root,
|
|
198
|
+
packageManager: context.packageManager,
|
|
199
|
+
},
|
|
200
|
+
environment: {
|
|
201
|
+
frameworks: context.frameworks.supported,
|
|
202
|
+
unsupportedFrameworks: context.frameworks.unsupported,
|
|
203
|
+
buildTools: context.buildTools.supported,
|
|
204
|
+
unsupportedBuildTools: context.buildTools.unsupported,
|
|
205
|
+
ides: context.ides,
|
|
206
|
+
providers: context.providers,
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createPlanResult(context: OnboardingContext): PlanResult {
|
|
212
|
+
const warnings = uniqueMessages(unsupportedEnvironmentWarnings(context))
|
|
213
|
+
const blockers = uniqueMessages([...buildToolBlockers(context), ...frameworkBlockers(context)])
|
|
214
|
+
const actions: PlanResult['actions'] = []
|
|
215
|
+
|
|
216
|
+
let strategy: PlanResult['strategy'] = 'supported'
|
|
217
|
+
|
|
218
|
+
if (blockers.length > 0) {
|
|
219
|
+
strategy = 'manual'
|
|
220
|
+
if (
|
|
221
|
+
context.buildTools.unsupported.length > 0 ||
|
|
222
|
+
context.buildTools.supported.length === 0 ||
|
|
223
|
+
context.buildTools.supported.length > 1
|
|
224
|
+
) {
|
|
225
|
+
actions.push(...manualBuildToolActions(context))
|
|
226
|
+
}
|
|
227
|
+
if (frameworkBlockers(context).length > 0) {
|
|
228
|
+
actions.push(...manualFrameworkActions(context))
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
actions.push({
|
|
232
|
+
type: 'install_dependency',
|
|
233
|
+
target: '@inspecto-dev/plugin @inspecto-dev/core',
|
|
234
|
+
description: `Install the Inspecto runtime packages with ${context.packageManager}.`,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
for (const buildTool of context.buildTools.supported) {
|
|
238
|
+
actions.push({
|
|
239
|
+
type: 'modify_file',
|
|
240
|
+
target: buildTool.configPath,
|
|
241
|
+
description: `Inject the Inspecto plugin into ${buildTool.label}.`,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const ide = supportedIde(context)
|
|
246
|
+
if (ide === 'vscode') {
|
|
247
|
+
actions.push({
|
|
248
|
+
type: 'install_extension',
|
|
249
|
+
target: 'vscode',
|
|
250
|
+
description: 'Install the Inspecto VS Code extension.',
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const defaults: PlanResult['defaults'] = {
|
|
256
|
+
shared: false,
|
|
257
|
+
extension: supportedIde(context) === 'vscode',
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const provider = supportedProvider(context)
|
|
261
|
+
if (provider) {
|
|
262
|
+
defaults.provider = provider
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const ide = supportedIde(context)
|
|
266
|
+
if (ide) {
|
|
267
|
+
defaults.ide = ide
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
status: planStatus(warnings, blockers),
|
|
272
|
+
warnings,
|
|
273
|
+
blockers,
|
|
274
|
+
strategy,
|
|
275
|
+
actions,
|
|
276
|
+
defaults,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function planManualFollowUp(result: PlanResult): string[] {
|
|
281
|
+
return result.actions
|
|
282
|
+
.filter(action => action.type === 'manual_step')
|
|
283
|
+
.map(action => action.description)
|
|
284
|
+
}
|