@inspecto-dev/cli 0.3.1 → 0.3.3
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 +7 -7
- package/.turbo/turbo-test.log +5160 -92
- package/CHANGELOG.md +20 -0
- package/README.md +126 -6
- package/dist/bin.js +59 -361
- package/dist/{chunk-PSYZB5GI.js → chunk-LJOKPCPD.js} +1389 -91
- 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/integration-automation.ts +485 -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 -101
- package/src/commands/onboard.ts +19 -0
- package/src/index.ts +1 -0
- package/src/inject/extension.ts +144 -24
- package/src/integrations/capabilities.ts +131 -0
- package/src/utils/process.ts +3 -0
- package/tests/extension-installer.test.ts +205 -0
- package/tests/install-wrapper.test.ts +45 -4
- package/tests/integration-automation.test.ts +491 -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 +118 -0
- package/tests/shared-capabilities.test.ts +45 -0
package/src/commands/onboard.ts
CHANGED
|
@@ -17,6 +17,23 @@ export interface OnboardCommandOptions {
|
|
|
17
17
|
noExtension?: boolean
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function printManualExtensionGuidance(result: OnboardCommandResult): void {
|
|
21
|
+
if (!result.ideExtension?.required || !result.ideExtension.manualRequired) {
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
log.warn('Complete the IDE extension install before verification.')
|
|
26
|
+
if (result.ideExtension.installCommand) {
|
|
27
|
+
log.hint(result.ideExtension.installCommand)
|
|
28
|
+
}
|
|
29
|
+
if (result.ideExtension.marketplaceUrl) {
|
|
30
|
+
log.hint(result.ideExtension.marketplaceUrl)
|
|
31
|
+
}
|
|
32
|
+
if (result.ideExtension.openVsxUrl) {
|
|
33
|
+
log.hint(result.ideExtension.openVsxUrl)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
function printOnboardResult(result: OnboardCommandResult): void {
|
|
21
38
|
log.header('Inspecto Onboard')
|
|
22
39
|
log.info(`Status: ${result.status}`)
|
|
@@ -32,6 +49,8 @@ function printOnboardResult(result: OnboardCommandResult): void {
|
|
|
32
49
|
log.warn(result.confirmation.question)
|
|
33
50
|
}
|
|
34
51
|
|
|
52
|
+
printManualExtensionGuidance(result)
|
|
53
|
+
|
|
35
54
|
const extensionReady =
|
|
36
55
|
!result.ideExtension?.required ||
|
|
37
56
|
(result.ideExtension.installed && !result.ideExtension.manualRequired)
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { apply } from './commands/apply.js'
|
|
|
2
2
|
export { detect } from './commands/detect.js'
|
|
3
3
|
export { init } from './commands/init.js'
|
|
4
4
|
export { collectDoctorResult, doctor } from './commands/doctor.js'
|
|
5
|
+
export { integrationDoctor } from './commands/integration-doctor.js'
|
|
5
6
|
export { onboard } from './commands/onboard.js'
|
|
6
7
|
export { plan } from './commands/plan.js'
|
|
7
8
|
export { teardown } from './commands/teardown.js'
|
package/src/inject/extension.ts
CHANGED
|
@@ -11,38 +11,27 @@
|
|
|
11
11
|
|
|
12
12
|
import { which, run, shell } from '../utils/exec.js'
|
|
13
13
|
import { exists } from '../utils/fs.js'
|
|
14
|
+
import {
|
|
15
|
+
getHostIdeBinaryCandidates,
|
|
16
|
+
getHostIdeBinaryName,
|
|
17
|
+
getHostIdeLabel,
|
|
18
|
+
isSupportedHostIde,
|
|
19
|
+
type SupportedHostIde,
|
|
20
|
+
} from '../integrations/capabilities.js'
|
|
14
21
|
import { log } from '../utils/logger.js'
|
|
15
22
|
import type { Mutation } from '../types.js'
|
|
16
23
|
|
|
17
24
|
const EXTENSION_ID = 'inspecto.inspecto'
|
|
18
25
|
|
|
19
|
-
/** Known VS Code binary locations by platform */
|
|
20
|
-
const VSCODE_PATHS: Record<string, string[]> = {
|
|
21
|
-
darwin: [
|
|
22
|
-
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
|
|
23
|
-
'/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders',
|
|
24
|
-
`${process.env.HOME}/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`,
|
|
25
|
-
],
|
|
26
|
-
linux: ['/usr/bin/code', '/usr/share/code/bin/code', '/snap/bin/code', '/usr/bin/code-insiders'],
|
|
27
|
-
win32: [
|
|
28
|
-
`${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code\\bin\\code.cmd`,
|
|
29
|
-
`${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code Insiders\\bin\\code-insiders.cmd`,
|
|
30
|
-
`${process.env.PROGRAMFILES}\\Microsoft VS Code\\bin\\code.cmd`,
|
|
31
|
-
],
|
|
32
|
-
}
|
|
33
|
-
|
|
34
26
|
/** Try to find the VS Code binary at known filesystem paths */
|
|
35
27
|
async function findVSCodeBinary(): Promise<string | null> {
|
|
36
|
-
const
|
|
37
|
-
const candidates = VSCODE_PATHS[platform] || []
|
|
38
|
-
|
|
28
|
+
const candidates = getHostIdeBinaryCandidates('vscode')
|
|
39
29
|
for (const candidate of candidates) {
|
|
40
30
|
if (await exists(candidate)) {
|
|
41
31
|
return candidate
|
|
42
32
|
}
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
// Also check for code-insiders in PATH
|
|
46
35
|
if (await which('code-insiders')) {
|
|
47
36
|
return 'code-insiders'
|
|
48
37
|
}
|
|
@@ -50,8 +39,88 @@ async function findVSCodeBinary(): Promise<string | null> {
|
|
|
50
39
|
return null
|
|
51
40
|
}
|
|
52
41
|
|
|
42
|
+
async function findIdeBinary(ide: SupportedHostIde): Promise<string | null> {
|
|
43
|
+
const binaryName = getHostIdeBinaryName(ide)
|
|
44
|
+
if (binaryName && (await which(binaryName))) {
|
|
45
|
+
return binaryName
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (ide === 'vscode') {
|
|
49
|
+
return findVSCodeBinary()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const candidates = getHostIdeBinaryCandidates(ide)
|
|
53
|
+
for (const candidate of candidates) {
|
|
54
|
+
if (await exists(candidate)) {
|
|
55
|
+
return candidate
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function installAlternativeIdeExtension(
|
|
63
|
+
binaryPath: string,
|
|
64
|
+
ideLabel: string,
|
|
65
|
+
extensionRef: string,
|
|
66
|
+
quiet: boolean,
|
|
67
|
+
): Promise<Mutation | null> {
|
|
68
|
+
try {
|
|
69
|
+
const { stdout } = await run(binaryPath, ['--list-extensions'])
|
|
70
|
+
if (extensionRef === EXTENSION_ID && stdout.toLowerCase().includes(EXTENSION_ID)) {
|
|
71
|
+
if (!quiet) {
|
|
72
|
+
log.success(`${ideLabel} extension already installed`)
|
|
73
|
+
}
|
|
74
|
+
return { type: 'extension_installed', id: EXTENSION_ID, description: 'already_installed' }
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Continue to install attempt if listing fails.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await run(binaryPath, ['--install-extension', extensionRef, '--force'])
|
|
82
|
+
if (!quiet) {
|
|
83
|
+
log.success(`${ideLabel} extension installed via CLI`)
|
|
84
|
+
}
|
|
85
|
+
return { type: 'extension_installed', id: extensionRef, description: 'installed_via_cli' }
|
|
86
|
+
} catch {
|
|
87
|
+
try {
|
|
88
|
+
const { stdout } = await run(binaryPath, ['--list-extensions'])
|
|
89
|
+
if (extensionRef === EXTENSION_ID && stdout.toLowerCase().includes(EXTENSION_ID)) {
|
|
90
|
+
if (!quiet) {
|
|
91
|
+
log.success(`${ideLabel} extension already installed`)
|
|
92
|
+
}
|
|
93
|
+
return { type: 'extension_installed', id: EXTENSION_ID, description: 'already_installed' }
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Fall through to manual guidance.
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function resolveHostIdeBinary(ide: string): Promise<string | null> {
|
|
104
|
+
if (!isSupportedHostIde(ide)) return null
|
|
105
|
+
return findIdeBinary(ide)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function openIdeWorkspace(ide: string, cwd: string): Promise<boolean> {
|
|
109
|
+
const binaryPath = await resolveHostIdeBinary(ide)
|
|
110
|
+
if (!binaryPath) {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await run(binaryPath, ['--new-window', cwd])
|
|
116
|
+
return true
|
|
117
|
+
} catch {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
53
122
|
/** Try to open a URI using the system default handler */
|
|
54
|
-
async function
|
|
123
|
+
export async function openUri(uri: string): Promise<boolean> {
|
|
55
124
|
try {
|
|
56
125
|
const platform = process.platform
|
|
57
126
|
if (platform === 'win32') {
|
|
@@ -73,6 +142,7 @@ export async function installExtension(
|
|
|
73
142
|
dryRun: boolean,
|
|
74
143
|
ide?: string,
|
|
75
144
|
quiet = false,
|
|
145
|
+
extensionRef = EXTENSION_ID,
|
|
76
146
|
): Promise<Mutation | null> {
|
|
77
147
|
if (dryRun) {
|
|
78
148
|
if (!quiet) {
|
|
@@ -91,7 +161,7 @@ export async function installExtension(
|
|
|
91
161
|
if (!quiet) {
|
|
92
162
|
log.success('VS Code extension installed via CLI')
|
|
93
163
|
}
|
|
94
|
-
return { type: 'extension_installed', id: EXTENSION_ID }
|
|
164
|
+
return { type: 'extension_installed', id: EXTENSION_ID, description: 'installed_via_cli' }
|
|
95
165
|
} catch {
|
|
96
166
|
// Fall through to next level
|
|
97
167
|
}
|
|
@@ -108,7 +178,7 @@ export async function installExtension(
|
|
|
108
178
|
'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future',
|
|
109
179
|
)
|
|
110
180
|
}
|
|
111
|
-
return { type: 'extension_installed', id: EXTENSION_ID }
|
|
181
|
+
return { type: 'extension_installed', id: EXTENSION_ID, description: 'installed_via_cli' }
|
|
112
182
|
} catch {
|
|
113
183
|
// Fall through to next level
|
|
114
184
|
}
|
|
@@ -116,12 +186,17 @@ export async function installExtension(
|
|
|
116
186
|
|
|
117
187
|
// Level 3: URI scheme
|
|
118
188
|
const uri = `vscode:extension/${EXTENSION_ID}`
|
|
119
|
-
if (await
|
|
189
|
+
if (await openUri(uri)) {
|
|
120
190
|
if (!quiet) {
|
|
121
191
|
log.warn('Opened extension page in VS Code')
|
|
122
192
|
log.hint('Please click "Install" in the opened VS Code window to complete setup.')
|
|
123
193
|
}
|
|
124
|
-
return {
|
|
194
|
+
return {
|
|
195
|
+
type: 'extension_installed',
|
|
196
|
+
id: EXTENSION_ID,
|
|
197
|
+
description: 'opened_install_page',
|
|
198
|
+
manual_action_required: true,
|
|
199
|
+
}
|
|
125
200
|
}
|
|
126
201
|
|
|
127
202
|
// Level 4: Manual fallback
|
|
@@ -136,6 +211,51 @@ export async function installExtension(
|
|
|
136
211
|
return null
|
|
137
212
|
}
|
|
138
213
|
|
|
214
|
+
if (ide === 'cursor' && process.platform === 'darwin') {
|
|
215
|
+
const cursorPath = await findIdeBinary('cursor')
|
|
216
|
+
if (cursorPath) {
|
|
217
|
+
const result = await installAlternativeIdeExtension(
|
|
218
|
+
cursorPath,
|
|
219
|
+
getHostIdeLabel('cursor'),
|
|
220
|
+
extensionRef,
|
|
221
|
+
quiet,
|
|
222
|
+
)
|
|
223
|
+
if (result) {
|
|
224
|
+
return result
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (ide === 'trae' && process.platform === 'darwin') {
|
|
230
|
+
const traePath = await findIdeBinary('trae')
|
|
231
|
+
if (traePath) {
|
|
232
|
+
const result = await installAlternativeIdeExtension(
|
|
233
|
+
traePath,
|
|
234
|
+
getHostIdeLabel('trae'),
|
|
235
|
+
extensionRef,
|
|
236
|
+
quiet,
|
|
237
|
+
)
|
|
238
|
+
if (result) {
|
|
239
|
+
return result
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (ide === 'trae-cn' && process.platform === 'darwin') {
|
|
245
|
+
const traeCnPath = await findIdeBinary('trae-cn')
|
|
246
|
+
if (traeCnPath) {
|
|
247
|
+
const result = await installAlternativeIdeExtension(
|
|
248
|
+
traeCnPath,
|
|
249
|
+
getHostIdeLabel('trae-cn'),
|
|
250
|
+
extensionRef,
|
|
251
|
+
quiet,
|
|
252
|
+
)
|
|
253
|
+
if (result) {
|
|
254
|
+
return result
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
139
259
|
// Other IDEs: Prompt to install via VSIX
|
|
140
260
|
if (!quiet) {
|
|
141
261
|
log.warn(`Could not auto-install extension for ${ide}`)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
DUAL_MODE_PROVIDER_CAPABILITIES,
|
|
4
|
+
HOST_IDE_IDS,
|
|
5
|
+
getHostIdeLabel,
|
|
6
|
+
isSupportedHostIde,
|
|
7
|
+
type DualModeProvider,
|
|
8
|
+
type SupportedHostIde,
|
|
9
|
+
} from '@inspecto-dev/types'
|
|
10
|
+
|
|
11
|
+
type Platform = 'darwin' | 'linux' | 'win32'
|
|
12
|
+
|
|
13
|
+
interface HostIdeCapability {
|
|
14
|
+
label: string
|
|
15
|
+
artifactDir: string
|
|
16
|
+
extensionDir: string
|
|
17
|
+
binaryName?: string
|
|
18
|
+
binaryPaths?: Partial<Record<Platform, string[]>>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const HOST_IDE_CAPABILITIES: Record<SupportedHostIde, HostIdeCapability> = {
|
|
22
|
+
vscode: {
|
|
23
|
+
label: 'VS Code',
|
|
24
|
+
artifactDir: '.vscode',
|
|
25
|
+
extensionDir: '.vscode/extensions',
|
|
26
|
+
binaryName: 'code',
|
|
27
|
+
binaryPaths: {
|
|
28
|
+
darwin: [
|
|
29
|
+
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
|
|
30
|
+
'/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders',
|
|
31
|
+
`${process.env.HOME}/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`,
|
|
32
|
+
],
|
|
33
|
+
linux: [
|
|
34
|
+
'/usr/bin/code',
|
|
35
|
+
'/usr/share/code/bin/code',
|
|
36
|
+
'/snap/bin/code',
|
|
37
|
+
'/usr/bin/code-insiders',
|
|
38
|
+
],
|
|
39
|
+
win32: [
|
|
40
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code\\bin\\code.cmd`,
|
|
41
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code Insiders\\bin\\code-insiders.cmd`,
|
|
42
|
+
`${process.env.PROGRAMFILES}\\Microsoft VS Code\\bin\\code.cmd`,
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
cursor: {
|
|
47
|
+
label: 'Cursor',
|
|
48
|
+
artifactDir: '.cursor',
|
|
49
|
+
extensionDir: '.cursor/extensions',
|
|
50
|
+
binaryName: 'cursor',
|
|
51
|
+
binaryPaths: {
|
|
52
|
+
darwin: [
|
|
53
|
+
'/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
|
|
54
|
+
`${process.env.HOME}/Applications/Cursor.app/Contents/Resources/app/bin/cursor`,
|
|
55
|
+
],
|
|
56
|
+
linux: ['/usr/bin/cursor', '/opt/Cursor/resources/app/bin/cursor'],
|
|
57
|
+
win32: [
|
|
58
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Cursor\\resources\\app\\bin\\cursor.cmd`,
|
|
59
|
+
`${process.env.PROGRAMFILES}\\Cursor\\resources\\app\\bin\\cursor.cmd`,
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
trae: {
|
|
64
|
+
label: 'Trae',
|
|
65
|
+
artifactDir: '.trae',
|
|
66
|
+
extensionDir: '.trae/extensions',
|
|
67
|
+
binaryName: 'trae',
|
|
68
|
+
binaryPaths: {
|
|
69
|
+
darwin: [
|
|
70
|
+
'/Applications/Trae.app/Contents/Resources/app/bin/trae',
|
|
71
|
+
`${process.env.HOME}/Applications/Trae.app/Contents/Resources/app/bin/trae`,
|
|
72
|
+
],
|
|
73
|
+
linux: ['/usr/bin/trae', '/opt/Trae/resources/app/bin/trae'],
|
|
74
|
+
win32: [
|
|
75
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Trae\\resources\\app\\bin\\trae.cmd`,
|
|
76
|
+
`${process.env.PROGRAMFILES}\\Trae\\resources\\app\\bin\\trae.cmd`,
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
'trae-cn': {
|
|
81
|
+
label: 'Trae CN',
|
|
82
|
+
artifactDir: '.trae-cn',
|
|
83
|
+
extensionDir: '.trae-cn/extensions',
|
|
84
|
+
binaryName: 'trae-cn',
|
|
85
|
+
binaryPaths: {
|
|
86
|
+
darwin: [
|
|
87
|
+
'/Applications/Trae CN.app/Contents/Resources/app/bin/trae-cn',
|
|
88
|
+
`${process.env.HOME}/Applications/Trae CN.app/Contents/Resources/app/bin/trae-cn`,
|
|
89
|
+
],
|
|
90
|
+
linux: ['/usr/bin/trae-cn', '/opt/Trae CN/resources/app/bin/trae-cn'],
|
|
91
|
+
win32: [
|
|
92
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Trae CN\\resources\\app\\bin\\trae-cn.cmd`,
|
|
93
|
+
`${process.env.PROGRAMFILES}\\Trae CN\\resources\\app\\bin\\trae-cn.cmd`,
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { HOST_IDE_IDS, getHostIdeLabel, isSupportedHostIde }
|
|
100
|
+
export type { SupportedHostIde }
|
|
101
|
+
|
|
102
|
+
export type DualModeAssistant = DualModeProvider
|
|
103
|
+
|
|
104
|
+
export function getHostIdeResolutionSourceLabel(source: string): string {
|
|
105
|
+
if (source === 'explicit') return 'from --host-ide'
|
|
106
|
+
if (source === 'config') return 'from .inspecto settings'
|
|
107
|
+
if (source === 'env') return 'from IDE terminal environment'
|
|
108
|
+
if (source === 'artifact') return 'from project files'
|
|
109
|
+
return source
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getHostIdeArtifactPath(ide: SupportedHostIde, cwd: string): string {
|
|
113
|
+
return path.join(cwd, HOST_IDE_CAPABILITIES[ide].artifactDir)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getHostIdeExtensionDir(ide: SupportedHostIde, homeDir: string): string {
|
|
117
|
+
return path.join(homeDir, HOST_IDE_CAPABILITIES[ide].extensionDir)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getHostIdeBinaryName(ide: SupportedHostIde): string | null {
|
|
121
|
+
return HOST_IDE_CAPABILITIES[ide].binaryName ?? null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getHostIdeBinaryCandidates(ide: SupportedHostIde): string[] {
|
|
125
|
+
const platform = process.platform as Platform
|
|
126
|
+
return HOST_IDE_CAPABILITIES[ide].binaryPaths?.[platform] ?? []
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getDualModeAssistantCapability(assistant: string) {
|
|
130
|
+
return DUAL_MODE_PROVIDER_CAPABILITIES[assistant as DualModeProvider]
|
|
131
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const runMock = vi.fn()
|
|
4
|
+
const shellMock = vi.fn()
|
|
5
|
+
const whichMock = vi.fn()
|
|
6
|
+
const existsMock = vi.fn()
|
|
7
|
+
const logMock = {
|
|
8
|
+
dryRun: vi.fn(),
|
|
9
|
+
success: vi.fn(),
|
|
10
|
+
info: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
hint: vi.fn(),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
vi.mock('../src/utils/exec.js', () => ({
|
|
16
|
+
run: runMock,
|
|
17
|
+
shell: shellMock,
|
|
18
|
+
which: whichMock,
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
vi.mock('../src/utils/fs.js', () => ({
|
|
22
|
+
exists: existsMock,
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
vi.mock('../src/utils/logger.js', () => ({
|
|
26
|
+
log: logMock,
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
describe('installExtension', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.resetAllMocks()
|
|
32
|
+
shellMock.mockResolvedValue({ stdout: '', stderr: '' })
|
|
33
|
+
existsMock.mockResolvedValue(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('treats an already-installed Cursor extension as success instead of showing manual install hints', async () => {
|
|
37
|
+
vi.stubEnv('TERM_PROGRAM', '')
|
|
38
|
+
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
|
39
|
+
whichMock.mockImplementation(async bin => bin === 'cursor')
|
|
40
|
+
runMock.mockImplementation(async (_command, args: string[]) => {
|
|
41
|
+
if (args[0] === '--list-extensions') {
|
|
42
|
+
return { stdout: 'inspecto.inspecto\n', stderr: '' }
|
|
43
|
+
}
|
|
44
|
+
throw new Error('install should not run when already installed')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const { installExtension } = await import('../src/inject/extension.js')
|
|
48
|
+
|
|
49
|
+
await expect(installExtension(false, 'cursor')).resolves.toMatchObject({
|
|
50
|
+
type: 'extension_installed',
|
|
51
|
+
id: 'inspecto.inspecto',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
expect(logMock.success).toHaveBeenCalledWith('Cursor extension already installed')
|
|
55
|
+
expect(logMock.warn).not.toHaveBeenCalledWith('Could not auto-install extension for cursor')
|
|
56
|
+
expect(runMock).toHaveBeenCalledWith('cursor', ['--list-extensions'])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('installs the Trae CN extension via the app binary on macOS when available', async () => {
|
|
60
|
+
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
|
61
|
+
whichMock.mockResolvedValue(false)
|
|
62
|
+
existsMock.mockImplementation(async filePath => {
|
|
63
|
+
return filePath === '/Applications/Trae CN.app/Contents/Resources/app/bin/trae-cn'
|
|
64
|
+
})
|
|
65
|
+
runMock.mockImplementation(async (_command, args: string[]) => {
|
|
66
|
+
if (args[0] === '--list-extensions') {
|
|
67
|
+
return { stdout: '', stderr: '' }
|
|
68
|
+
}
|
|
69
|
+
if (args[0] === '--install-extension') {
|
|
70
|
+
return { stdout: '', stderr: '' }
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`unexpected args: ${args.join(' ')}`)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const { installExtension } = await import('../src/inject/extension.js')
|
|
76
|
+
|
|
77
|
+
await expect(installExtension(false, 'trae-cn')).resolves.toMatchObject({
|
|
78
|
+
type: 'extension_installed',
|
|
79
|
+
id: 'inspecto.inspecto',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(runMock).toHaveBeenCalledWith(
|
|
83
|
+
'/Applications/Trae CN.app/Contents/Resources/app/bin/trae-cn',
|
|
84
|
+
['--install-extension', 'inspecto.inspecto', '--force'],
|
|
85
|
+
)
|
|
86
|
+
expect(logMock.success).toHaveBeenCalledWith('Trae CN extension installed via CLI')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('installs the Trae extension via PATH when available', async () => {
|
|
90
|
+
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
|
91
|
+
whichMock.mockImplementation(async bin => bin === 'trae')
|
|
92
|
+
runMock.mockImplementation(async (_command, args: string[]) => {
|
|
93
|
+
if (args[0] === '--list-extensions') {
|
|
94
|
+
return { stdout: '', stderr: '' }
|
|
95
|
+
}
|
|
96
|
+
if (args[0] === '--install-extension') {
|
|
97
|
+
return { stdout: '', stderr: '' }
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`unexpected args: ${args.join(' ')}`)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const { installExtension } = await import('../src/inject/extension.js')
|
|
103
|
+
|
|
104
|
+
await expect(installExtension(false, 'trae')).resolves.toMatchObject({
|
|
105
|
+
type: 'extension_installed',
|
|
106
|
+
id: 'inspecto.inspecto',
|
|
107
|
+
description: 'installed_via_cli',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(runMock).toHaveBeenCalledWith('trae', [
|
|
111
|
+
'--install-extension',
|
|
112
|
+
'inspecto.inspecto',
|
|
113
|
+
'--force',
|
|
114
|
+
])
|
|
115
|
+
expect(logMock.success).toHaveBeenCalledWith('Trae extension installed via CLI')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('installs a VSIX path in Trae CN when provided', async () => {
|
|
119
|
+
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
|
120
|
+
whichMock.mockImplementation(async bin => bin === 'trae-cn')
|
|
121
|
+
runMock.mockImplementation(async (_command, args: string[]) => {
|
|
122
|
+
if (args[0] === '--list-extensions') {
|
|
123
|
+
return { stdout: '', stderr: '' }
|
|
124
|
+
}
|
|
125
|
+
if (args[0] === '--install-extension') {
|
|
126
|
+
return { stdout: '', stderr: '' }
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`unexpected args: ${args.join(' ')}`)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const { installExtension } = await import('../src/inject/extension.js')
|
|
132
|
+
|
|
133
|
+
await expect(
|
|
134
|
+
installExtension(false, 'trae-cn', true, '/tmp/inspecto.vsix'),
|
|
135
|
+
).resolves.toMatchObject({
|
|
136
|
+
type: 'extension_installed',
|
|
137
|
+
id: '/tmp/inspecto.vsix',
|
|
138
|
+
description: 'installed_via_cli',
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
expect(runMock).toHaveBeenCalledWith('trae-cn', [
|
|
142
|
+
'--install-extension',
|
|
143
|
+
'/tmp/inspecto.vsix',
|
|
144
|
+
'--force',
|
|
145
|
+
])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('treats Trae CN as already installed when install fails but a follow-up extension check succeeds', async () => {
|
|
149
|
+
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
|
150
|
+
whichMock.mockResolvedValue(false)
|
|
151
|
+
|
|
152
|
+
let listCalls = 0
|
|
153
|
+
existsMock.mockImplementation(async filePath => {
|
|
154
|
+
return filePath === '/Applications/Trae CN.app/Contents/Resources/app/bin/trae-cn'
|
|
155
|
+
})
|
|
156
|
+
runMock.mockImplementation(async (_command, args: string[]) => {
|
|
157
|
+
if (args[0] === '--list-extensions') {
|
|
158
|
+
listCalls += 1
|
|
159
|
+
return {
|
|
160
|
+
stdout: listCalls === 1 ? '' : 'inspecto.inspecto\n',
|
|
161
|
+
stderr: '',
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (args[0] === '--install-extension') {
|
|
165
|
+
throw new Error('network failed')
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`unexpected args: ${args.join(' ')}`)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const { installExtension } = await import('../src/inject/extension.js')
|
|
171
|
+
|
|
172
|
+
await expect(installExtension(false, 'trae-cn')).resolves.toMatchObject({
|
|
173
|
+
type: 'extension_installed',
|
|
174
|
+
id: 'inspecto.inspecto',
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(logMock.success).toHaveBeenCalledWith('Trae CN extension already installed')
|
|
178
|
+
expect(logMock.warn).not.toHaveBeenCalledWith('Could not auto-install extension for trae-cn')
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('openIdeWorkspace', () => {
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
vi.resetAllMocks()
|
|
185
|
+
existsMock.mockResolvedValue(false)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('opens Trae CN in a new window for the requested workspace', async () => {
|
|
189
|
+
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
|
190
|
+
whichMock.mockResolvedValue(false)
|
|
191
|
+
existsMock.mockImplementation(async filePath => {
|
|
192
|
+
return filePath === '/Applications/Trae CN.app/Contents/Resources/app/bin/trae-cn'
|
|
193
|
+
})
|
|
194
|
+
runMock.mockResolvedValue({ stdout: '', stderr: '' })
|
|
195
|
+
|
|
196
|
+
const { openIdeWorkspace } = await import('../src/inject/extension.js')
|
|
197
|
+
|
|
198
|
+
await expect(openIdeWorkspace('trae-cn', '/repo/app')).resolves.toBe(true)
|
|
199
|
+
|
|
200
|
+
expect(runMock).toHaveBeenCalledWith(
|
|
201
|
+
'/Applications/Trae CN.app/Contents/Resources/app/bin/trae-cn',
|
|
202
|
+
['--new-window', '/repo/app'],
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -21,9 +21,9 @@ describe('assistant integration bootstrap wrapper', () => {
|
|
|
21
21
|
|
|
22
22
|
const fakeBin = path.join(tempRoot, 'bin')
|
|
23
23
|
await fs.mkdir(fakeBin, { recursive: true })
|
|
24
|
-
await fs.mkdir(path.join(tempRoot, '.github'), { recursive: true })
|
|
24
|
+
await fs.mkdir(path.join(tempRoot, '.github/skills/inspecto-onboarding'), { recursive: true })
|
|
25
25
|
await fs.writeFile(
|
|
26
|
-
path.join(tempRoot, '.github/
|
|
26
|
+
path.join(tempRoot, '.github/skills/inspecto-onboarding/SKILL.md'),
|
|
27
27
|
'old content\n',
|
|
28
28
|
'utf8',
|
|
29
29
|
)
|
|
@@ -54,7 +54,7 @@ describe('assistant integration bootstrap wrapper', () => {
|
|
|
54
54
|
)
|
|
55
55
|
await fs.chmod(path.join(fakeBin, 'curl'), 0o755)
|
|
56
56
|
|
|
57
|
-
const scriptPath = path.resolve(__dirname, '../../../
|
|
57
|
+
const scriptPath = path.resolve(__dirname, '../../../scripts/install.sh')
|
|
58
58
|
|
|
59
59
|
await execFileAsync('bash', [scriptPath, 'copilot', '--force'], {
|
|
60
60
|
cwd: tempRoot,
|
|
@@ -65,9 +65,50 @@ describe('assistant integration bootstrap wrapper', () => {
|
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
const installed = await fs.readFile(
|
|
68
|
-
path.join(tempRoot, '.github/
|
|
68
|
+
path.join(tempRoot, '.github/skills/inspecto-onboarding/SKILL.md'),
|
|
69
69
|
'utf8',
|
|
70
70
|
)
|
|
71
71
|
expect(installed).toBe('downloaded from wrapper\n')
|
|
72
72
|
})
|
|
73
|
+
|
|
74
|
+
it('passes --host-ide through to the CLI install command', async () => {
|
|
75
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'inspecto-wrapper-'))
|
|
76
|
+
tempDirs.push(tempRoot)
|
|
77
|
+
|
|
78
|
+
const fakeBin = path.join(tempRoot, 'bin')
|
|
79
|
+
await fs.mkdir(fakeBin, { recursive: true })
|
|
80
|
+
|
|
81
|
+
const argsFile = path.join(tempRoot, 'npx-args.txt')
|
|
82
|
+
await fs.writeFile(
|
|
83
|
+
path.join(fakeBin, 'npx'),
|
|
84
|
+
['#!/usr/bin/env bash', 'set -euo pipefail', 'printf "%s\n" "$@" > "$NPX_ARGS_FILE"'].join(
|
|
85
|
+
'\n',
|
|
86
|
+
),
|
|
87
|
+
'utf8',
|
|
88
|
+
)
|
|
89
|
+
await fs.chmod(path.join(fakeBin, 'npx'), 0o755)
|
|
90
|
+
|
|
91
|
+
const scriptPath = path.resolve(__dirname, '../../../scripts/install.sh')
|
|
92
|
+
|
|
93
|
+
await execFileAsync('bash', [scriptPath, 'codex', 'project', '--host-ide', 'cursor'], {
|
|
94
|
+
cwd: tempRoot,
|
|
95
|
+
env: {
|
|
96
|
+
...process.env,
|
|
97
|
+
PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
|
|
98
|
+
NPX_ARGS_FILE: argsFile,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const forwardedArgs = await fs.readFile(argsFile, 'utf8')
|
|
103
|
+
expect(forwardedArgs.trim().split('\n')).toEqual([
|
|
104
|
+
'@inspecto-dev/cli',
|
|
105
|
+
'integrations',
|
|
106
|
+
'install',
|
|
107
|
+
'codex',
|
|
108
|
+
'--host-ide',
|
|
109
|
+
'cursor',
|
|
110
|
+
'--scope',
|
|
111
|
+
'project',
|
|
112
|
+
])
|
|
113
|
+
})
|
|
73
114
|
})
|