@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.
@@ -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'
@@ -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 platform = process.platform
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 tryOpenURI(uri: string): Promise<boolean> {
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 tryOpenURI(uri)) {
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 { type: 'extension_installed', id: EXTENSION_ID, manual_action_required: true }
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,3 @@
1
+ export function exitProcess(code: number): never {
2
+ process.exit(code)
3
+ }
@@ -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/copilot-instructions.md'),
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, '../../../assistant-integrations/scripts/install.sh')
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/copilot-instructions.md'),
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
  })