@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
@@ -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
+ }