@inspecto-dev/cli 0.3.1 → 0.3.2

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.
@@ -0,0 +1,484 @@
1
+ import {
2
+ installExtension,
3
+ openIdeWorkspace,
4
+ openUri,
5
+ resolveHostIdeBinary,
6
+ } from '../inject/extension.js'
7
+ import {
8
+ getDualModeAssistantCapability,
9
+ getHostIdeLabel,
10
+ getHostIdeResolutionSourceLabel,
11
+ type SupportedHostIde,
12
+ } from '../integrations/capabilities.js'
13
+ import { exists } from '../utils/fs.js'
14
+ import { log } from '../utils/logger.js'
15
+ import { resolveIntegrationHostIde } from './integration-host-ide.js'
16
+ import { resolveIntegrationDispatchMode } from './integration-dispatch-mode.js'
17
+
18
+ const ONBOARDING_PROMPT = 'Set up Inspecto in this project'
19
+ const TOTAL_STEPS = 6
20
+ const EXTENSION_ID = 'inspecto.inspecto'
21
+
22
+ interface IntegrationAutomationOptions {
23
+ ide?: string
24
+ inspectoVsix?: string
25
+ preview?: boolean
26
+ silent?: boolean
27
+ }
28
+
29
+ interface IntegrationAutomationDetails {
30
+ hostIde?: {
31
+ id: string | null
32
+ label?: string
33
+ source?: string
34
+ confidence: string
35
+ candidates: string[]
36
+ }
37
+ inspectoExtension?: {
38
+ source: 'marketplace' | 'local_vsix'
39
+ reference: string
40
+ binaryAvailable?: boolean
41
+ binaryPath?: string | null
42
+ status?: string
43
+ }
44
+ runtime?: {
45
+ assistant: string
46
+ ready: boolean
47
+ mode: string | null
48
+ }
49
+ workspace?: {
50
+ path?: string
51
+ attempted: boolean
52
+ opened?: boolean
53
+ }
54
+ onboarding?: {
55
+ uri: string
56
+ autoSend: boolean
57
+ }
58
+ }
59
+
60
+ export interface IntegrationAutomationResult {
61
+ status: 'launched' | 'partial' | 'blocked' | 'preview' | 'preview_blocked'
62
+ message: string
63
+ nextStep?: string
64
+ details?: IntegrationAutomationDetails
65
+ }
66
+
67
+ function getPreviewReadyMessage(): string {
68
+ return 'Preview complete. Inspecto did not write files or open IDE windows. Review the resolved setup below, then rerun without --preview to apply it.'
69
+ }
70
+
71
+ function getPreviewBlockedMessage(): string {
72
+ return 'Preview blocked. Inspecto did not write files or open IDE windows because setup cannot continue until the blocking issue below is resolved.'
73
+ }
74
+
75
+ function getHostIdeBlockedMessage(): string {
76
+ return 'Automatic setup stopped: Inspecto could not determine which IDE should receive onboarding.'
77
+ }
78
+
79
+ function getRuntimeBlockedMessage(assistant: string, ide: SupportedHostIde): string {
80
+ return `Automatic setup stopped: Inspecto could not find a runnable ${getAssistantLabel(assistant)} target in ${getHostIdeLabel(ide)}.`
81
+ }
82
+
83
+ function getLaunchBlockedMessage(ide: SupportedHostIde): string {
84
+ return `Automatic setup stopped: Inspecto could not open onboarding in ${getHostIdeLabel(ide)}.`
85
+ }
86
+
87
+ export async function runIntegrationAutomation(
88
+ assistant: string,
89
+ options: IntegrationAutomationOptions = {},
90
+ cwd?: string,
91
+ ): Promise<IntegrationAutomationResult> {
92
+ const silent = options.silent ?? false
93
+ const resolvedHostIde = await resolveIntegrationHostIde({
94
+ ...(options.ide ? { explicitIde: options.ide } : {}),
95
+ ...(cwd ? { cwd } : {}),
96
+ })
97
+
98
+ const details: IntegrationAutomationDetails = {
99
+ hostIde: {
100
+ id: resolvedHostIde.ide,
101
+ confidence: resolvedHostIde.confidence,
102
+ candidates: resolvedHostIde.candidates,
103
+ ...(resolvedHostIde.ide
104
+ ? {
105
+ label: getHostIdeLabel(resolvedHostIde.ide),
106
+ source: getHostIdeResolutionSourceLabel(resolvedHostIde.source),
107
+ }
108
+ : {}),
109
+ },
110
+ }
111
+
112
+ if (!resolvedHostIde.ide || resolvedHostIde.confidence === 'low') {
113
+ if (!silent) {
114
+ log.warn(formatIntegrationStep(2, 'Could not confidently resolve the host IDE'))
115
+ }
116
+ if (resolvedHostIde.candidates.length > 0) {
117
+ if (!silent) {
118
+ log.hint(`Candidates: ${resolvedHostIde.candidates.join(', ')}`)
119
+ }
120
+ }
121
+ if (!silent) {
122
+ log.hint(
123
+ 'Re-run with --host-ide <vscode|cursor|trae|trae-cn> or run the command from the target IDE terminal to continue automatic setup.',
124
+ )
125
+ }
126
+ return {
127
+ status: 'blocked',
128
+ message: getHostIdeBlockedMessage(),
129
+ nextStep:
130
+ 'Re-run with --host-ide <vscode|cursor|trae|trae-cn> or run the command from the target IDE terminal to continue automatic setup.',
131
+ details,
132
+ }
133
+ }
134
+
135
+ const previewParams = new URLSearchParams()
136
+ previewParams.set('target', assistant)
137
+ previewParams.set('prompt', ONBOARDING_PROMPT)
138
+ previewParams.set('autoSend', String(shouldAutoSend(assistant, resolvedHostIde.ide)))
139
+ if (cwd) {
140
+ previewParams.set('workspace', cwd)
141
+ }
142
+
143
+ const dispatchMode = await resolveIntegrationDispatchMode({
144
+ assistant,
145
+ hostIde: resolvedHostIde.ide,
146
+ })
147
+
148
+ if (dispatchMode.mode) {
149
+ previewParams.set('overrides', JSON.stringify({ type: dispatchMode.mode }))
150
+ }
151
+
152
+ const launchUri = `${resolvedHostIde.ide}://inspecto.inspecto/send?${previewParams.toString()}`
153
+ details.inspectoExtension = {
154
+ source: options.inspectoVsix ? 'local_vsix' : 'marketplace',
155
+ reference: options.inspectoVsix ?? EXTENSION_ID,
156
+ }
157
+ details.runtime = {
158
+ assistant: getAssistantLabel(assistant),
159
+ ready: dispatchMode.ready,
160
+ mode: dispatchMode.mode,
161
+ }
162
+ details.workspace = {
163
+ ...(cwd ? { path: cwd } : {}),
164
+ attempted: Boolean(cwd),
165
+ }
166
+ details.onboarding = {
167
+ uri: launchUri,
168
+ autoSend: shouldAutoSend(assistant, resolvedHostIde.ide),
169
+ }
170
+
171
+ if (options.preview) {
172
+ if (!silent) {
173
+ log.info(formatIntegrationStep(2, 'Previewed host IDE resolution'))
174
+ log.hint(
175
+ `${getHostIdeLabel(resolvedHostIde.ide)} (${getHostIdeResolutionSourceLabel(resolvedHostIde.source)})`,
176
+ )
177
+ }
178
+
179
+ const hostIdeBinary = await resolveHostIdeBinary(resolvedHostIde.ide)
180
+ details.inspectoExtension.binaryAvailable = Boolean(hostIdeBinary)
181
+ details.inspectoExtension.binaryPath = hostIdeBinary
182
+ if (!hostIdeBinary) {
183
+ const nextStep = `Install the ${getHostIdeLabel(resolvedHostIde.ide)} CLI binary or rerun the command from a shell where it is available, then rerun the command.`
184
+ details.inspectoExtension.status = 'missing_host_ide_binary'
185
+ if (!silent) {
186
+ log.warn(
187
+ formatIntegrationStep(
188
+ 3,
189
+ `Could not verify Inspecto extension installation in ${getHostIdeLabel(resolvedHostIde.ide)}`,
190
+ ),
191
+ )
192
+ log.hint(
193
+ `No ${getHostIdeLabel(resolvedHostIde.ide)} CLI binary was found. Automatic extension install and workspace opening may not work.`,
194
+ )
195
+ }
196
+ return {
197
+ status: 'preview_blocked',
198
+ message: getPreviewBlockedMessage(),
199
+ nextStep,
200
+ details,
201
+ }
202
+ }
203
+
204
+ if (options.inspectoVsix && !(await exists(options.inspectoVsix))) {
205
+ const nextStep = `Provide a valid local VSIX path for ${getHostIdeLabel(resolvedHostIde.ide)} or remove --inspecto-vsix, then rerun the command.`
206
+ details.inspectoExtension.status = 'missing_local_vsix'
207
+ if (!silent) {
208
+ log.warn(
209
+ formatIntegrationStep(
210
+ 3,
211
+ `Could not verify the local Inspecto VSIX for ${getHostIdeLabel(resolvedHostIde.ide)}`,
212
+ ),
213
+ )
214
+ log.hint(`The local VSIX path does not exist: ${options.inspectoVsix}`)
215
+ }
216
+ return {
217
+ status: 'preview_blocked',
218
+ message: getPreviewBlockedMessage(),
219
+ nextStep,
220
+ details,
221
+ }
222
+ }
223
+
224
+ details.inspectoExtension.status = 'preview_ready'
225
+ if (!silent) {
226
+ log.info(formatIntegrationStep(3, 'Previewed Inspecto extension installation'))
227
+ log.hint(
228
+ options.inspectoVsix
229
+ ? `Local VSIX (${options.inspectoVsix})`
230
+ : `Marketplace install in ${getHostIdeLabel(resolvedHostIde.ide)}`,
231
+ )
232
+ }
233
+
234
+ if (!dispatchMode.ready) {
235
+ const nextStep = `Install the ${getAssistantLabel(assistant)} plugin in ${getHostIdeLabel(resolvedHostIde.ide)} or install the \`${getAssistantCliName(assistant)}\` CLI, then rerun the command.`
236
+ if (!silent) {
237
+ log.warn(
238
+ formatIntegrationStep(
239
+ 4,
240
+ `Could not resolve a runnable ${getAssistantLabel(assistant)} runtime`,
241
+ ),
242
+ )
243
+ log.hint(nextStep)
244
+ }
245
+ return {
246
+ status: 'preview_blocked',
247
+ message: getPreviewBlockedMessage(),
248
+ nextStep,
249
+ details,
250
+ }
251
+ }
252
+
253
+ if (!silent) {
254
+ log.info(formatIntegrationStep(4, `Previewed ${getAssistantLabel(assistant)} runtime`))
255
+ log.hint(getDispatchModeLabel(assistant, resolvedHostIde.ide, dispatchMode.mode))
256
+ }
257
+
258
+ if (cwd) {
259
+ if (!silent) {
260
+ log.info(
261
+ formatIntegrationStep(
262
+ 5,
263
+ `Previewed workspace routing in ${getHostIdeLabel(resolvedHostIde.ide)}`,
264
+ ),
265
+ )
266
+ log.hint(cwd)
267
+ }
268
+ }
269
+
270
+ if (!silent) {
271
+ log.info(formatIntegrationStep(6, 'Previewed onboarding launch'))
272
+ log.hint(launchUri)
273
+ }
274
+ return {
275
+ status: 'preview',
276
+ message: getPreviewReadyMessage(),
277
+ nextStep:
278
+ 'Run the same command again without --preview to apply the integration and launch onboarding.',
279
+ details,
280
+ }
281
+ }
282
+
283
+ if (!silent) {
284
+ log.success(formatIntegrationStep(2, 'Resolved host IDE'))
285
+ log.hint(
286
+ `${getHostIdeLabel(resolvedHostIde.ide)} (${getHostIdeResolutionSourceLabel(resolvedHostIde.source)})`,
287
+ )
288
+ }
289
+
290
+ const installResult = await installExtension(
291
+ false,
292
+ resolvedHostIde.ide,
293
+ true,
294
+ options.inspectoVsix,
295
+ )
296
+ details.inspectoExtension.status = installResult?.description ?? 'not_prepared'
297
+ if (!silent) {
298
+ logInstallStep(resolvedHostIde.ide, installResult)
299
+ }
300
+ if (installResult?.type === 'extension_installed') {
301
+ if (!silent) {
302
+ if (installResult.manual_action_required) {
303
+ log.hint('Complete the IDE install prompt before retrying if onboarding does not appear.')
304
+ } else {
305
+ log.hint('Waiting briefly for the IDE extension to finish activating...')
306
+ }
307
+ }
308
+ await new Promise(resolve => setTimeout(resolve, 1500))
309
+ } else {
310
+ if (!silent) {
311
+ log.warn(
312
+ formatIntegrationStep(
313
+ 3,
314
+ `Could not prepare the Inspecto extension for ${getHostIdeLabel(resolvedHostIde.ide)}`,
315
+ ),
316
+ )
317
+ log.hint('Automatic onboarding may fail until the Inspecto extension is installed.')
318
+ }
319
+ }
320
+
321
+ if (cwd) {
322
+ const openedWorkspace = await openIdeWorkspace(resolvedHostIde.ide, cwd)
323
+ details.workspace.opened = openedWorkspace
324
+ if (openedWorkspace) {
325
+ if (!silent) {
326
+ log.success(
327
+ formatIntegrationStep(5, `Opened workspace in ${getHostIdeLabel(resolvedHostIde.ide)}`),
328
+ )
329
+ log.hint(cwd)
330
+ }
331
+ await new Promise(resolve => setTimeout(resolve, 1000))
332
+ } else {
333
+ if (!silent) {
334
+ log.warn(
335
+ formatIntegrationStep(
336
+ 5,
337
+ `Could not open the workspace in ${getHostIdeLabel(resolvedHostIde.ide)}`,
338
+ ),
339
+ )
340
+ log.hint(cwd)
341
+ }
342
+ }
343
+ }
344
+
345
+ if (!dispatchMode.ready) {
346
+ const nextStep = `Install the ${getAssistantLabel(assistant)} plugin in ${getHostIdeLabel(resolvedHostIde.ide)} or install the \`${getAssistantCliName(assistant)}\` CLI, then rerun the command.`
347
+ if (!silent) {
348
+ log.warn(
349
+ formatIntegrationStep(
350
+ 4,
351
+ `Could not resolve a runnable ${getAssistantLabel(assistant)} runtime`,
352
+ ),
353
+ )
354
+ log.hint(nextStep)
355
+ }
356
+ return {
357
+ status: 'blocked',
358
+ message: getRuntimeBlockedMessage(assistant, resolvedHostIde.ide),
359
+ nextStep,
360
+ details,
361
+ }
362
+ }
363
+
364
+ if (!silent) {
365
+ log.success(formatIntegrationStep(4, `Resolved ${getAssistantLabel(assistant)} runtime`))
366
+ log.hint(getDispatchModeLabel(assistant, resolvedHostIde.ide, dispatchMode.mode))
367
+ }
368
+
369
+ const workspaceOpenFailed = Boolean(cwd) && details.workspace?.opened === false
370
+
371
+ const launched = await openUri(launchUri)
372
+ if (launched) {
373
+ if (!silent) {
374
+ log.success(
375
+ formatIntegrationStep(6, `Launched onboarding in ${getHostIdeLabel(resolvedHostIde.ide)}`),
376
+ )
377
+ log.hint(`${getAssistantLabel(assistant)} via ${dispatchMode.mode ?? 'default'} mode`)
378
+ }
379
+ if (workspaceOpenFailed) {
380
+ const nextStep = `If the wrong IDE window received onboarding, open ${cwd} in ${getHostIdeLabel(resolvedHostIde.ide)} and rerun the command from that project.`
381
+ return {
382
+ status: 'partial',
383
+ message: `Onboarding opened in ${getHostIdeLabel(resolvedHostIde.ide)} for ${getAssistantLabel(assistant)}, but Inspecto could not open the target workspace first.`,
384
+ nextStep,
385
+ details,
386
+ }
387
+ }
388
+ return {
389
+ status: installResult ? 'launched' : 'partial',
390
+ message: installResult
391
+ ? `Onboarding opened in ${getHostIdeLabel(resolvedHostIde.ide)} for ${getAssistantLabel(assistant)}.`
392
+ : `Onboarding opened in ${getHostIdeLabel(resolvedHostIde.ide)} for ${getAssistantLabel(assistant)}, but the Inspecto extension may still need manual setup.`,
393
+ ...(installResult
394
+ ? {}
395
+ : {
396
+ nextStep: `Install the Inspecto extension in ${getHostIdeLabel(resolvedHostIde.ide)} if IDE-side features are missing.`,
397
+ }),
398
+ details,
399
+ }
400
+ } else {
401
+ if (!silent) {
402
+ log.warn(
403
+ formatIntegrationStep(
404
+ 6,
405
+ `Could not launch onboarding in ${getHostIdeLabel(resolvedHostIde.ide)}`,
406
+ ),
407
+ )
408
+ log.hint(launchUri)
409
+ }
410
+ return {
411
+ status: 'blocked',
412
+ message: getLaunchBlockedMessage(resolvedHostIde.ide),
413
+ nextStep: launchUri,
414
+ details,
415
+ }
416
+ }
417
+ }
418
+
419
+ function shouldAutoSend(assistant: string, ide: SupportedHostIde): boolean {
420
+ if (assistant === 'copilot') return true
421
+ if (assistant === 'codex') return true
422
+ return false
423
+ }
424
+
425
+ function getAssistantLabel(assistant: string): string {
426
+ return getDualModeAssistantCapability(assistant)?.label ?? assistant
427
+ }
428
+
429
+ function getAssistantCliName(assistant: string): string {
430
+ return getDualModeAssistantCapability(assistant)?.cliBin ?? assistant
431
+ }
432
+
433
+ function formatIntegrationStep(step: number, text: string): string {
434
+ return `Step ${step}/${TOTAL_STEPS}: ${text}`
435
+ }
436
+
437
+ function logInstallStep(
438
+ ide: SupportedHostIde,
439
+ installResult: Awaited<ReturnType<typeof installExtension>>,
440
+ ): void {
441
+ if (!installResult) {
442
+ return
443
+ }
444
+
445
+ if (installResult.description === 'already_installed') {
446
+ log.success(
447
+ formatIntegrationStep(3, `Inspecto extension already installed in ${getHostIdeLabel(ide)}`),
448
+ )
449
+ log.hint(installResult.id ?? 'inspecto.inspecto')
450
+ return
451
+ }
452
+
453
+ if (installResult.description === 'opened_install_page') {
454
+ log.warn(
455
+ formatIntegrationStep(
456
+ 3,
457
+ `Opened the Inspecto extension install page in ${getHostIdeLabel(ide)}`,
458
+ ),
459
+ )
460
+ log.hint('Finish the extension install in the IDE window, then rerun the command if needed.')
461
+ return
462
+ }
463
+
464
+ log.success(
465
+ formatIntegrationStep(3, `Installed the Inspecto extension in ${getHostIdeLabel(ide)}`),
466
+ )
467
+ log.hint(installResult.id ?? 'inspecto.inspecto')
468
+ }
469
+
470
+ function getDispatchModeLabel(
471
+ assistant: string,
472
+ ide: SupportedHostIde,
473
+ mode: string | null,
474
+ ): string {
475
+ if (mode === 'cli') {
476
+ return `CLI fallback (\`${getAssistantCliName(assistant)}\`)`
477
+ }
478
+
479
+ if (mode === 'extension') {
480
+ return `${getAssistantLabel(assistant)} plugin in ${getHostIdeLabel(ide)}`
481
+ }
482
+
483
+ return 'Default runtime'
484
+ }
@@ -0,0 +1,92 @@
1
+ import { homedir } from 'node:os'
2
+ import path from 'node:path'
3
+ import type { ProviderMode, SupportedHostIde } from '@inspecto-dev/types'
4
+ import { exists, readJSON } from '../utils/fs.js'
5
+ import { which } from '../utils/exec.js'
6
+ import {
7
+ getDualModeAssistantCapability,
8
+ getHostIdeExtensionDir,
9
+ } from '../integrations/capabilities.js'
10
+
11
+ interface ResolveIntegrationDispatchModeOptions {
12
+ assistant: string
13
+ hostIde: SupportedHostIde
14
+ homeDir?: string
15
+ }
16
+
17
+ interface ResolveIntegrationDispatchModeResult {
18
+ mode: ProviderMode | null
19
+ ready: boolean
20
+ reason: string
21
+ }
22
+
23
+ export async function resolveIntegrationDispatchMode(
24
+ options: ResolveIntegrationDispatchModeOptions,
25
+ ): Promise<ResolveIntegrationDispatchModeResult> {
26
+ const assistantRule = getDualModeAssistantCapability(options.assistant)
27
+ const home = options.homeDir ?? homedir()
28
+ const extensionDir = getHostIdeExtensionDir(options.hostIde, home)
29
+
30
+ if (assistantRule && extensionDir) {
31
+ if (await isIdeExtensionInstalled(assistantRule.extensionId, extensionDir)) {
32
+ return {
33
+ mode: 'extension',
34
+ ready: true,
35
+ reason: `${options.hostIde}_${options.assistant}_extension`,
36
+ }
37
+ }
38
+
39
+ if (await which(assistantRule.cliBin)) {
40
+ return {
41
+ mode: 'cli',
42
+ ready: true,
43
+ reason: `${options.assistant}_cli`,
44
+ }
45
+ }
46
+
47
+ return {
48
+ mode: null,
49
+ ready: false,
50
+ reason: `missing_${options.assistant}_runtime`,
51
+ }
52
+ }
53
+
54
+ return {
55
+ mode: null,
56
+ ready: true,
57
+ reason: 'default',
58
+ }
59
+ }
60
+
61
+ async function isIdeExtensionInstalled(
62
+ extensionId: string,
63
+ extensionsDir: string,
64
+ ): Promise<boolean> {
65
+ if (!(await exists(extensionsDir))) {
66
+ return false
67
+ }
68
+
69
+ let extensionFolders: string[]
70
+ try {
71
+ const { readdir } = await import('node:fs/promises')
72
+ extensionFolders = await readdir(extensionsDir)
73
+ } catch {
74
+ return false
75
+ }
76
+
77
+ const obsoletePath = path.join(extensionsDir, '.obsolete')
78
+ let obsoleteFolders = new Set<string>()
79
+ if (await exists(obsoletePath)) {
80
+ const obsolete = await readJSON<Record<string, boolean>>(obsoletePath)
81
+ if (obsolete) {
82
+ obsoleteFolders = new Set(Object.keys(obsolete))
83
+ }
84
+ }
85
+
86
+ return extensionFolders.some(folder => {
87
+ if (obsoleteFolders.has(folder)) return false
88
+ const lower = folder.toLowerCase()
89
+ const normalized = extensionId.toLowerCase()
90
+ return lower === normalized || lower.startsWith(`${normalized}-`)
91
+ })
92
+ }
@@ -0,0 +1,117 @@
1
+ import { describeIntegration, type InstallIntegrationOptions } from './integration-install.js'
2
+ import { runIntegrationAutomation } from './integration-automation.js'
3
+ import { log } from '../utils/logger.js'
4
+ import { writeCommandOutput } from '../utils/output.js'
5
+ import { exitProcess } from '../utils/process.js'
6
+
7
+ const INTEGRATION_DOCTOR_SCHEMA_VERSION = '1'
8
+
9
+ export interface IntegrationDoctorOptions extends Pick<
10
+ InstallIntegrationOptions,
11
+ 'scope' | 'mode' | 'ide' | 'inspectoVsix' | 'json'
12
+ > {
13
+ compact?: boolean
14
+ failOnBlocked?: boolean
15
+ }
16
+
17
+ export interface IntegrationDoctorResult {
18
+ schemaVersion: string
19
+ status: 'ok' | 'blocked'
20
+ assistant: string
21
+ assets: string[]
22
+ message: string
23
+ nextStep?: string
24
+ automation: Awaited<ReturnType<typeof runIntegrationAutomation>>
25
+ }
26
+
27
+ function printIntegrationDoctorResult(
28
+ result: IntegrationDoctorResult,
29
+ options: Pick<IntegrationDoctorOptions, 'compact'> = {},
30
+ ): void {
31
+ log.header('Inspecto Integration Doctor')
32
+ log.info(`Assistant: ${result.assistant}`)
33
+ const compact = options.compact ?? false
34
+
35
+ if (!compact && result.assets.length > 0) {
36
+ log.info('Asset targets:')
37
+ for (const asset of result.assets) {
38
+ log.hint(asset)
39
+ }
40
+ }
41
+
42
+ const details = result.automation.details
43
+ if (details?.hostIde?.id && details.hostIde.label) {
44
+ const hostIdeDetail = details.hostIde.source
45
+ ? `${details.hostIde.label} (${details.hostIde.source})`
46
+ : details.hostIde.label
47
+ log.info(`Host IDE: ${hostIdeDetail}`)
48
+ }
49
+
50
+ if (!compact && details?.inspectoExtension) {
51
+ log.info(
52
+ `Inspecto extension: ${details.inspectoExtension.source} (${details.inspectoExtension.reference})`,
53
+ )
54
+ }
55
+
56
+ if (details?.runtime) {
57
+ const mode = details.runtime.mode ?? 'default'
58
+ const readiness = details.runtime.ready ? ` via ${mode}` : ' unavailable'
59
+ log.info(`Runtime: ${details.runtime.assistant}${readiness}`)
60
+ }
61
+
62
+ if (details?.workspace?.path) {
63
+ log.info(`Workspace: ${details.workspace.path}`)
64
+ }
65
+
66
+ if (!compact && details?.onboarding?.uri) {
67
+ log.info(`Onboarding URI: ${details.onboarding.uri}`)
68
+ }
69
+
70
+ if (result.status === 'ok') {
71
+ log.ready(result.message)
72
+ } else {
73
+ log.warn(result.message)
74
+ if (result.nextStep) {
75
+ log.hint(result.nextStep)
76
+ }
77
+ }
78
+ }
79
+
80
+ export async function integrationDoctor(
81
+ assistant: string,
82
+ options: IntegrationDoctorOptions = {},
83
+ ): Promise<IntegrationDoctorResult> {
84
+ const description = describeIntegration(assistant, options)
85
+ const automation = await runIntegrationAutomation(
86
+ assistant,
87
+ {
88
+ ...(options.ide ? { ide: options.ide } : {}),
89
+ ...(options.inspectoVsix ? { inspectoVsix: options.inspectoVsix } : {}),
90
+ preview: true,
91
+ silent: true,
92
+ },
93
+ process.cwd(),
94
+ )
95
+
96
+ const result: IntegrationDoctorResult = {
97
+ schemaVersion: INTEGRATION_DOCTOR_SCHEMA_VERSION,
98
+ status: automation.status === 'preview' ? 'ok' : 'blocked',
99
+ assistant,
100
+ assets: description.targets,
101
+ message: automation.message,
102
+ ...(automation.nextStep ? { nextStep: automation.nextStep } : {}),
103
+ automation,
104
+ }
105
+
106
+ const written = writeCommandOutput(result, options.json ?? false, value =>
107
+ printIntegrationDoctorResult(value, {
108
+ ...(options.compact !== undefined ? { compact: options.compact } : {}),
109
+ }),
110
+ )
111
+
112
+ if (result.status === 'blocked' && options.failOnBlocked) {
113
+ exitProcess(1)
114
+ }
115
+
116
+ return written
117
+ }