@inspecto-dev/cli 0.3.0 → 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.
- package/.turbo/turbo-build.log +20 -19
- package/.turbo/turbo-test.log +2653 -0
- package/CHANGELOG.md +20 -0
- package/README.md +126 -6
- package/dist/bin.js +59 -359
- package/dist/{chunk-IBYH7QZM.js → chunk-LJOKPCPD.js} +1397 -94
- package/dist/index.d.ts +74 -1
- package/dist/index.js +3 -1
- package/package.json +2 -2
- package/src/bin.ts +85 -6
- package/src/commands/init.ts +5 -1
- package/src/commands/integration-automation.ts +484 -0
- package/src/commands/integration-dispatch-mode.ts +92 -0
- package/src/commands/integration-doctor.ts +117 -0
- package/src/commands/integration-host-ide.ts +156 -0
- package/src/commands/integration-install.ts +262 -99
- package/src/commands/onboard.ts +24 -1
- package/src/index.ts +1 -0
- package/src/inject/extension.ts +144 -24
- package/src/integrations/capabilities.ts +131 -0
- package/src/onboarding/session.ts +13 -4
- package/src/utils/process.ts +3 -0
- package/tests/apply.test.ts +6 -1
- package/tests/extension-installer.test.ts +205 -0
- package/tests/install-wrapper.test.ts +45 -7
- package/tests/integration-automation.test.ts +435 -0
- package/tests/integration-dispatch-mode.test.ts +203 -0
- package/tests/integration-doctor.test.ts +165 -0
- package/tests/integration-host-ide.test.ts +172 -0
- package/tests/integration-install.test.ts +282 -20
- package/tests/onboard.test.ts +123 -1
- package/tests/runner-script.test.ts +72 -0
- package/tests/shared-capabilities.test.ts +45 -0
|
@@ -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
|
+
}
|