@inspecto-dev/cli 0.2.0-alpha.6 → 0.3.0

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.
@@ -69,9 +69,15 @@ async function tryOpenURI(uri: string): Promise<boolean> {
69
69
  /**
70
70
  * Attempt to install the VS Code extension using waterfall degradation.
71
71
  */
72
- export async function installExtension(dryRun: boolean, ide?: string): Promise<Mutation | null> {
72
+ export async function installExtension(
73
+ dryRun: boolean,
74
+ ide?: string,
75
+ quiet = false,
76
+ ): Promise<Mutation | null> {
73
77
  if (dryRun) {
74
- log.dryRun('Would attempt to install VS Code extension')
78
+ if (!quiet) {
79
+ log.dryRun('Would attempt to install VS Code extension')
80
+ }
75
81
  return null
76
82
  }
77
83
 
@@ -82,7 +88,9 @@ export async function installExtension(dryRun: boolean, ide?: string): Promise<M
82
88
  if (await which('code')) {
83
89
  try {
84
90
  await run('code', ['--install-extension', EXTENSION_ID])
85
- log.success('VS Code extension installed via CLI')
91
+ if (!quiet) {
92
+ log.success('VS Code extension installed via CLI')
93
+ }
86
94
  return { type: 'extension_installed', id: EXTENSION_ID }
87
95
  } catch {
88
96
  // Fall through to next level
@@ -94,10 +102,12 @@ export async function installExtension(dryRun: boolean, ide?: string): Promise<M
94
102
  if (codePath) {
95
103
  try {
96
104
  await run(codePath, ['--install-extension', EXTENSION_ID])
97
- log.success('VS Code extension installed via binary path')
98
- log.info(
99
- 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future',
100
- )
105
+ if (!quiet) {
106
+ log.success('VS Code extension installed via binary path')
107
+ log.info(
108
+ 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future',
109
+ )
110
+ }
101
111
  return { type: 'extension_installed', id: EXTENSION_ID }
102
112
  } catch {
103
113
  // Fall through to next level
@@ -107,31 +117,37 @@ export async function installExtension(dryRun: boolean, ide?: string): Promise<M
107
117
  // Level 3: URI scheme
108
118
  const uri = `vscode:extension/${EXTENSION_ID}`
109
119
  if (await tryOpenURI(uri)) {
110
- log.warn('Opened extension page in VS Code')
111
- log.hint('Please click "Install" in the opened VS Code window to complete setup.')
120
+ if (!quiet) {
121
+ log.warn('Opened extension page in VS Code')
122
+ log.hint('Please click "Install" in the opened VS Code window to complete setup.')
123
+ }
112
124
  return { type: 'extension_installed', id: EXTENSION_ID, manual_action_required: true }
113
125
  }
114
126
 
115
127
  // Level 4: Manual fallback
116
- log.warn('Could not auto-install VS Code extension')
117
- log.hint('Please install it manually to enable Inspector features:')
118
- log.hint(' 1. Open VS Code')
119
- log.hint(' 2. Press Ctrl+Shift+X (or Cmd+Shift+X)')
120
- log.hint(' 3. Search for "Inspecto"')
121
- log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`)
128
+ if (!quiet) {
129
+ log.warn('Could not auto-install VS Code extension')
130
+ log.hint('Please install it manually to enable Inspector features:')
131
+ log.hint(' 1. Open VS Code')
132
+ log.hint(' 2. Press Ctrl+Shift+X (or Cmd+Shift+X)')
133
+ log.hint(' 3. Search for "Inspecto"')
134
+ log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`)
135
+ }
122
136
  return null
123
137
  }
124
138
 
125
139
  // Other IDEs: Prompt to install via VSIX
126
- log.warn(`Could not auto-install extension for ${ide}`)
127
- log.hint('Please install it manually to enable Inspector features:')
128
- log.hint(
129
- ' 1. Download the latest .vsix file (Open VSX: https://open-vsx.org/extension/inspecto/inspecto)',
130
- )
131
- log.hint(` 2. Open ${ide}`)
132
- log.hint(' 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)')
133
- log.hint(' 4. Type and select "Extensions: Install from VSIX..."')
134
- log.hint(' 5. Select the downloaded .vsix file')
140
+ if (!quiet) {
141
+ log.warn(`Could not auto-install extension for ${ide}`)
142
+ log.hint('Please install it manually to enable Inspector features:')
143
+ log.hint(
144
+ ' 1. Download the latest .vsix file (Open VSX: https://open-vsx.org/extension/inspecto/inspecto)',
145
+ )
146
+ log.hint(` 2. Open ${ide}`)
147
+ log.hint(' 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)')
148
+ log.hint(' 4. Type and select "Extensions: Install from VSIX..."')
149
+ log.hint(' 5. Select the downloaded .vsix file')
150
+ }
135
151
  return null
136
152
  }
137
153
 
@@ -21,6 +21,7 @@ export async function updateGitignore(
21
21
  root: string,
22
22
  shared: boolean,
23
23
  dryRun: boolean,
24
+ quiet = false,
24
25
  ): Promise<void> {
25
26
  const gitignorePath = path.join(root, '.gitignore')
26
27
  let content = (await readFile(gitignorePath)) ?? ''
@@ -34,7 +35,9 @@ export async function updateGitignore(
34
35
  if (!dryRun) {
35
36
  await writeFile(gitignorePath, content)
36
37
  }
37
- log.success('Updated .gitignore: .inspecto/ is no longer fully ignored')
38
+ if (!quiet) {
39
+ log.success('Updated .gitignore: .inspecto/ is no longer fully ignored')
40
+ }
38
41
  return
39
42
  }
40
43
 
@@ -49,10 +52,14 @@ export async function updateGitignore(
49
52
  content = content.trimEnd() + '\n' + section
50
53
 
51
54
  if (dryRun) {
52
- log.dryRun(`Would update .gitignore with: ${missingRules.join(', ')}`)
55
+ if (!quiet) {
56
+ log.dryRun(`Would update .gitignore with: ${missingRules.join(', ')}`)
57
+ }
53
58
  } else {
54
59
  await writeFile(gitignorePath, content)
55
- log.success('Updated .gitignore')
60
+ if (!quiet) {
61
+ log.success('Updated .gitignore')
62
+ }
56
63
  }
57
64
  }
58
65
 
@@ -61,6 +61,32 @@ export interface ApplyOnboardingResult {
61
61
  }
62
62
  }
63
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
+
64
90
  function resultStatus(nextSteps: string[]): CommandStatus {
65
91
  return nextSteps.length > 0 ? 'warning' : 'ok'
66
92
  }
@@ -157,10 +183,8 @@ async function applyOnboardingPlanInternal(
157
183
  const promptsFileName = input.options.shared ? 'prompts.json' : 'prompts.local.json'
158
184
  const settingsPath = path.join(settingsDir, settingsFileName)
159
185
  const promptsPath = path.join(settingsDir, promptsFileName)
160
- const installCmd = getInstallCommand(
161
- input.packageManager,
162
- '@inspecto-dev/plugin @inspecto-dev/core',
163
- )
186
+ const runtimePackages = resolveRuntimePackages()
187
+ const installCmd = getInstallCommand(input.packageManager, runtimePackages.installSpec)
164
188
  const nextSteps: string[] = []
165
189
 
166
190
  let installFailed = false
@@ -178,8 +202,9 @@ async function applyOnboardingPlanInternal(
178
202
  await shell(installCmd, input.projectRoot)
179
203
  spinner.succeed('Dependencies installed successfully')
180
204
  reporter.success('Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies')
181
- mutations.push({ type: 'dependency_added', name: '@inspecto-dev/plugin', dev: true })
182
- mutations.push({ type: 'dependency_added', name: '@inspecto-dev/core', dev: true })
205
+ for (const name of runtimePackages.installedDependencyNames) {
206
+ mutations.push({ type: 'dependency_added', name, dev: true })
207
+ }
183
208
  } catch (error: any) {
184
209
  spinner.fail('Dependency installation failed')
185
210
  installFailed = true
@@ -193,7 +218,12 @@ async function applyOnboardingPlanInternal(
193
218
 
194
219
  let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig)
195
220
  for (const target of input.supportedBuildTargets) {
196
- const result = await injectPlugin(input.repoRoot, target, input.options.dryRun)
221
+ const result = await injectPlugin(
222
+ input.repoRoot,
223
+ target,
224
+ input.options.dryRun,
225
+ input.options.quiet ?? false,
226
+ )
197
227
  if (result.success) {
198
228
  mutations.push(...result.mutations)
199
229
  } else {
@@ -244,7 +274,12 @@ async function applyOnboardingPlanInternal(
244
274
  }
245
275
 
246
276
  if (!input.options.dryRun) {
247
- await updateGitignore(input.projectRoot, input.options.shared, input.options.dryRun)
277
+ await updateGitignore(
278
+ input.projectRoot,
279
+ input.options.shared,
280
+ input.options.dryRun,
281
+ input.options.quiet ?? false,
282
+ )
248
283
  mutations.push({
249
284
  type: 'file_modified',
250
285
  path: '.gitignore',
@@ -270,7 +305,11 @@ async function applyOnboardingPlanInternal(
270
305
  if (input.options.noExtension) {
271
306
  reporter.warn('Skipping IDE extension (--no-extension)')
272
307
  } else if (shouldInstallExt) {
273
- const extMutation = await installExtension(input.options.dryRun, input.selectedIDE?.ide)
308
+ const extMutation = await installExtension(
309
+ input.options.dryRun,
310
+ input.selectedIDE?.ide,
311
+ input.options.quiet ?? false,
312
+ )
274
313
  if (extMutation && !input.options.dryRun) {
275
314
  mutations.push(extMutation)
276
315
 
@@ -276,3 +276,9 @@ export function createPlanResult(context: OnboardingContext): PlanResult {
276
276
  defaults,
277
277
  }
278
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
+ }
@@ -0,0 +1,434 @@
1
+ import path from 'node:path'
2
+ import { detectFrameworks } from '../detect/framework.js'
3
+ import { detectIDE } from '../detect/ide.js'
4
+ import { detectProviders } from '../detect/provider.js'
5
+ import { applyOnboardingPlan, type ApplyOnboardingResult } from './apply.js'
6
+ import { buildOnboardingContext } from './context.js'
7
+ import { createPlanResult, planManualFollowUp } from './planner.js'
8
+ import { resolveOnboardingTarget } from './target-resolution.js'
9
+ import { readJSON } from '../utils/fs.js'
10
+ import type {
11
+ OnboardCommandResult,
12
+ OnboardingContext,
13
+ OnboardingDiagnostics,
14
+ OnboardingExecutionResult,
15
+ OnboardingIdeExtensionStatus,
16
+ OnboardingSummary,
17
+ OnboardingVerification,
18
+ PlanResult,
19
+ ResolvedOnboardingSession,
20
+ } from '../types.js'
21
+
22
+ export interface ResolveOnboardingSessionOptions {
23
+ json?: boolean
24
+ target?: string
25
+ yes?: boolean
26
+ shared?: boolean
27
+ skipInstall?: boolean
28
+ dryRun?: boolean
29
+ noExtension?: boolean
30
+ }
31
+
32
+ function normalizePackagePath(packagePath?: string): string {
33
+ if (!packagePath || packagePath === '.') return ''
34
+ return packagePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '')
35
+ }
36
+
37
+ function getProviderDefault(
38
+ providerId?: string,
39
+ preferredMode?: 'cli' | 'extension',
40
+ ): string | undefined {
41
+ if (!providerId) return undefined
42
+ const mode = preferredMode ?? (providerId === 'coco' ? 'cli' : 'extension')
43
+ return `${providerId}.${mode}`
44
+ }
45
+
46
+ function getVerificationCommand(packageManager: OnboardingContext['packageManager']): string {
47
+ switch (packageManager) {
48
+ case 'pnpm':
49
+ return 'pnpm dev'
50
+ case 'yarn':
51
+ return 'yarn dev'
52
+ case 'bun':
53
+ return 'bun run dev'
54
+ case 'npm':
55
+ default:
56
+ return 'npm run dev'
57
+ }
58
+ }
59
+
60
+ async function buildVerification(
61
+ projectRoot: string,
62
+ packageManager: OnboardingContext['packageManager'],
63
+ ): Promise<OnboardingVerification> {
64
+ const packageJson = await readJSON<{ scripts?: Record<string, string> }>(
65
+ path.join(projectRoot, 'package.json'),
66
+ )
67
+
68
+ if (packageJson?.scripts?.dev) {
69
+ const devCommand = getVerificationCommand(packageManager)
70
+ return {
71
+ available: true,
72
+ devCommand,
73
+ message: `Start the local dev server with \`${devCommand}\` to verify Inspecto in the browser.`,
74
+ }
75
+ }
76
+
77
+ return {
78
+ available: false,
79
+ message: 'Start your normal local dev server command to verify Inspecto in the browser.',
80
+ }
81
+ }
82
+
83
+ function buildIdeExtensionStatus(input: {
84
+ required: boolean
85
+ installed: boolean
86
+ manualRequired: boolean
87
+ }): OnboardingIdeExtensionStatus {
88
+ if (!input.required) {
89
+ return {
90
+ required: false,
91
+ installed: false,
92
+ manualRequired: false,
93
+ }
94
+ }
95
+
96
+ return {
97
+ required: true,
98
+ installed: input.installed,
99
+ manualRequired: input.manualRequired,
100
+ installCommand: 'code --install-extension inspecto.inspecto',
101
+ marketplaceUrl: 'https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto',
102
+ openVsxUrl: 'https://open-vsx.org/extension/inspecto/inspecto',
103
+ }
104
+ }
105
+
106
+ async function detectFrameworkSupportByPackage(
107
+ repoRoot: string,
108
+ context: OnboardingContext,
109
+ ): Promise<Record<string, string[]>> {
110
+ const packagePaths = new Set(
111
+ context.buildTools.supported.map(item => normalizePackagePath(item.packagePath)),
112
+ )
113
+ const supportByPackage: Record<string, string[]> = {}
114
+
115
+ await Promise.all(
116
+ Array.from(packagePaths).map(async packagePath => {
117
+ const frameworkResult = await detectFrameworks(
118
+ packagePath ? path.join(repoRoot, packagePath) : repoRoot,
119
+ )
120
+ supportByPackage[packagePath] = frameworkResult.supported
121
+ }),
122
+ )
123
+
124
+ return supportByPackage
125
+ }
126
+
127
+ async function buildTargetedContext(
128
+ rootContext: OnboardingContext,
129
+ packagePath: string,
130
+ ): Promise<OnboardingContext> {
131
+ const projectRoot = packagePath ? path.join(rootContext.root, packagePath) : rootContext.root
132
+ const [frameworks, ides, providers] = await Promise.all([
133
+ detectFrameworks(projectRoot),
134
+ detectIDE(projectRoot),
135
+ detectProviders(projectRoot),
136
+ ])
137
+
138
+ return {
139
+ root: projectRoot,
140
+ packageManager: rootContext.packageManager,
141
+ buildTools: {
142
+ supported: rootContext.buildTools.supported.filter(item => {
143
+ return normalizePackagePath(item.packagePath) === packagePath
144
+ }),
145
+ unsupported: [],
146
+ },
147
+ frameworks: {
148
+ supported: frameworks.supported,
149
+ unsupported: frameworks.unsupported.map(item => item.name),
150
+ },
151
+ ides: ides.detected.map(({ ide, supported }) => ({ ide, supported })),
152
+ providers: providers.detected.map(({ id, label, supported, preferredMode }) => ({
153
+ id,
154
+ label,
155
+ supported,
156
+ preferredMode,
157
+ })),
158
+ }
159
+ }
160
+
161
+ function buildOnboardingSummary(plan: PlanResult, projectRoot: string): OnboardingSummary {
162
+ const changes = plan.actions
163
+ .filter(action => action.type !== 'manual_step')
164
+ .map(action => action.description)
165
+ const risks = [...plan.warnings.map(item => item.message)]
166
+ const manualFollowUp = planManualFollowUp(plan)
167
+
168
+ let headline = `Inspecto is ready to onboard ${projectRoot}.`
169
+ if (manualFollowUp.length > 0) {
170
+ headline = `Inspecto can partially onboard ${projectRoot}, but manual follow-up remains.`
171
+ } else if (plan.status === 'blocked') {
172
+ headline = `Inspecto could not build an automatic onboarding path for ${projectRoot}.`
173
+ }
174
+
175
+ return {
176
+ headline,
177
+ changes,
178
+ risks,
179
+ manualFollowUp,
180
+ }
181
+ }
182
+
183
+ function buildConfirmation(
184
+ plan: PlanResult,
185
+ summary: OnboardingSummary,
186
+ session: {
187
+ targetWasHeuristic: boolean
188
+ supportedIdeCount: number
189
+ supportedProviderCount: number
190
+ },
191
+ options: ResolveOnboardingSessionOptions,
192
+ ): { required: boolean; reason?: string; question?: string } {
193
+ if (options.yes) {
194
+ return { required: false }
195
+ }
196
+
197
+ const reasons: string[] = []
198
+ if (session.targetWasHeuristic) reasons.push('a monorepo target was preselected')
199
+ if (summary.manualFollowUp.length > 0) reasons.push('manual follow-up will remain after setup')
200
+ if (options.noExtension || options.skipInstall) reasons.push('non-core setup steps are being skipped')
201
+ if (session.supportedIdeCount > 1 || session.supportedProviderCount > 1) {
202
+ reasons.push('multiple IDE or provider choices are still relevant')
203
+ }
204
+ if (plan.warnings.length > 0) reasons.push('the CLI detected non-blocking risk')
205
+
206
+ if (reasons.length === 0) {
207
+ return { required: false }
208
+ }
209
+
210
+ return {
211
+ required: true,
212
+ reason: reasons.join('; '),
213
+ question: 'Proceed with Inspecto onboarding using the proposed default target and settings?',
214
+ }
215
+ }
216
+
217
+ function buildPreApplyResult(
218
+ status: ResolvedOnboardingSession['status'],
219
+ session: ResolvedOnboardingSession,
220
+ ): OnboardCommandResult {
221
+ const diagnostics: OnboardingDiagnostics | undefined =
222
+ session.summary.risks.length > 0 || session.summary.manualFollowUp.length > 0 || session.plan.blockers.length > 0
223
+ ? {
224
+ warnings: session.summary.risks,
225
+ errors: session.plan.blockers.map(item => item.message),
226
+ nextSteps: session.summary.manualFollowUp,
227
+ }
228
+ : undefined
229
+
230
+ return {
231
+ status,
232
+ target: session.target,
233
+ summary: session.summary,
234
+ confirmation: session.confirmation,
235
+ ideExtension: buildIdeExtensionStatus({
236
+ required: session.plan.defaults.extension,
237
+ installed: false,
238
+ manualRequired: session.plan.defaults.extension,
239
+ }),
240
+ verification: session.verification,
241
+ diagnostics,
242
+ }
243
+ }
244
+
245
+ function buildExecutionResult(
246
+ session: ResolvedOnboardingSession,
247
+ applyResult: ApplyOnboardingResult,
248
+ ): OnboardingExecutionResult {
249
+ return {
250
+ changedFiles: Array.from(
251
+ new Set(applyResult.mutations.map(item => item.path).filter((value): value is string => !!value)),
252
+ ),
253
+ installedDependencies: applyResult.mutations
254
+ .map(item => item.name)
255
+ .filter((value): value is string => !!value),
256
+ selectedProviderDefault: session.providerDefault,
257
+ selectedIDE: session.selectedIDE?.ide,
258
+ mutations: applyResult.mutations,
259
+ }
260
+ }
261
+
262
+ function buildExecutionDiagnostics(
263
+ session: ResolvedOnboardingSession,
264
+ applyResult: ApplyOnboardingResult,
265
+ ): OnboardingDiagnostics | undefined {
266
+ const warnings = [...session.plan.warnings.map(item => item.message)]
267
+ const errors: string[] = []
268
+
269
+ if (applyResult.postInstall.installFailed) {
270
+ errors.push('Dependency installation failed during onboarding.')
271
+ }
272
+ if (applyResult.postInstall.injectionFailed) {
273
+ warnings.push('Automatic plugin injection did not finish cleanly.')
274
+ }
275
+ if (applyResult.postInstall.manualExtensionInstallNeeded) {
276
+ warnings.push('IDE extension installation still needs manual completion.')
277
+ }
278
+
279
+ if (warnings.length === 0 && errors.length === 0 && applyResult.postInstall.nextSteps.length === 0) {
280
+ return undefined
281
+ }
282
+
283
+ return {
284
+ warnings,
285
+ errors,
286
+ nextSteps: applyResult.postInstall.nextSteps,
287
+ }
288
+ }
289
+
290
+ export async function resolveOnboardingSession(
291
+ root: string,
292
+ options: ResolveOnboardingSessionOptions = {},
293
+ ): Promise<ResolvedOnboardingSession> {
294
+ const rootContext = await buildOnboardingContext(root)
295
+ const rootVerification = await buildVerification(root, rootContext.packageManager)
296
+ const frameworkSupportByPackage = await detectFrameworkSupportByPackage(root, rootContext)
297
+ const target = resolveOnboardingTarget({
298
+ repoRoot: root,
299
+ buildTools: rootContext.buildTools.supported,
300
+ frameworkSupportByPackage,
301
+ selectedPackagePath: options.target,
302
+ })
303
+
304
+ if (target.candidates.length === 0) {
305
+ const plan = createPlanResult(rootContext)
306
+ return {
307
+ status: 'error',
308
+ target,
309
+ summary: buildOnboardingSummary(plan, root),
310
+ confirmation: { required: false },
311
+ verification: rootVerification,
312
+ context: rootContext,
313
+ plan,
314
+ projectRoot: root,
315
+ }
316
+ }
317
+
318
+ if (target.status === 'needs_selection') {
319
+ const plan = createPlanResult(rootContext)
320
+ const summary: OnboardingSummary = {
321
+ headline: 'Inspecto found multiple plausible app targets and needs one selection.',
322
+ changes: [],
323
+ risks: [],
324
+ manualFollowUp: [],
325
+ }
326
+ return {
327
+ status: 'needs_target_selection',
328
+ target,
329
+ summary,
330
+ confirmation: { required: false },
331
+ verification: rootVerification,
332
+ context: rootContext,
333
+ plan,
334
+ projectRoot: root,
335
+ }
336
+ }
337
+
338
+ const packagePath = normalizePackagePath(target.selected?.packagePath)
339
+ const context = await buildTargetedContext(rootContext, packagePath)
340
+ const verification = await buildVerification(context.root, context.packageManager)
341
+ const plan = createPlanResult(context)
342
+ const summary = buildOnboardingSummary(plan, context.root)
343
+ const confirmation = buildConfirmation(
344
+ plan,
345
+ summary,
346
+ {
347
+ targetWasHeuristic: target.candidates.length > 1 && options.target === undefined,
348
+ supportedIdeCount: context.ides.filter(item => item.supported).length,
349
+ supportedProviderCount: context.providers.filter(item => item.supported).length,
350
+ },
351
+ options,
352
+ )
353
+ const selectedProvider =
354
+ context.providers.find(provider => provider.id === plan.defaults.provider) ?? null
355
+ const selectedIDE =
356
+ context.ides.find(ide => ide.ide === plan.defaults.ide) ??
357
+ context.ides.find(ide => ide.supported) ??
358
+ null
359
+
360
+ let status: ResolvedOnboardingSession['status'] = 'success'
361
+ if (plan.status === 'blocked') {
362
+ status = 'error'
363
+ } else if (confirmation.required) {
364
+ status = 'needs_confirmation'
365
+ } else if (summary.manualFollowUp.length > 0 || plan.warnings.length > 0) {
366
+ status = 'partial_success'
367
+ }
368
+
369
+ return {
370
+ status,
371
+ target,
372
+ summary,
373
+ confirmation,
374
+ verification,
375
+ context,
376
+ plan,
377
+ projectRoot: context.root,
378
+ selectedIDE,
379
+ providerDefault: getProviderDefault(plan.defaults.provider, selectedProvider?.preferredMode),
380
+ }
381
+ }
382
+
383
+ export async function applyResolvedOnboardingSession(
384
+ session: ResolvedOnboardingSession,
385
+ options: ResolveOnboardingSessionOptions = {},
386
+ ): Promise<OnboardCommandResult> {
387
+ const verification = await buildVerification(session.projectRoot, session.context.packageManager)
388
+ const applyResult = await applyOnboardingPlan({
389
+ repoRoot: process.cwd(),
390
+ projectRoot: session.projectRoot,
391
+ packageManager: session.context.packageManager,
392
+ supportedBuildTargets: session.context.buildTools.supported,
393
+ options: {
394
+ shared: options.shared ?? session.plan.defaults.shared,
395
+ skipInstall: options.skipInstall ?? false,
396
+ dryRun: options.dryRun ?? false,
397
+ noExtension: options.noExtension ?? !session.plan.defaults.extension,
398
+ quiet: options.json ?? false,
399
+ },
400
+ selectedIDE: session.selectedIDE,
401
+ providerDefault: session.providerDefault,
402
+ plan: session.plan,
403
+ })
404
+
405
+ const diagnostics = buildExecutionDiagnostics(session, applyResult)
406
+ const status =
407
+ applyResult.postInstall.installFailed && session.context.buildTools.supported.length === 0
408
+ ? 'error'
409
+ : diagnostics?.nextSteps.length || diagnostics?.errors.length || diagnostics?.warnings.length
410
+ ? 'partial_success'
411
+ : 'success'
412
+
413
+ return {
414
+ status,
415
+ target: session.target,
416
+ summary: session.summary,
417
+ confirmation: session.confirmation,
418
+ ideExtension: buildIdeExtensionStatus({
419
+ required: session.plan.defaults.extension,
420
+ installed:
421
+ session.plan.defaults.extension && !applyResult.postInstall.manualExtensionInstallNeeded,
422
+ manualRequired: applyResult.postInstall.manualExtensionInstallNeeded,
423
+ }),
424
+ verification,
425
+ result: buildExecutionResult(session, applyResult),
426
+ diagnostics,
427
+ }
428
+ }
429
+
430
+ export function buildDeferredOnboardResult(
431
+ session: ResolvedOnboardingSession,
432
+ ): OnboardCommandResult {
433
+ return buildPreApplyResult(session.status, session)
434
+ }