@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.
@@ -0,0 +1,203 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const existsMock = vi.fn()
4
+ const readJSONMock = vi.fn()
5
+ const whichMock = vi.fn()
6
+
7
+ vi.mock('../src/utils/fs.js', () => ({
8
+ exists: existsMock,
9
+ readJSON: readJSONMock,
10
+ }))
11
+
12
+ vi.mock('../src/utils/exec.js', () => ({
13
+ which: whichMock,
14
+ }))
15
+
16
+ describe('resolveIntegrationDispatchMode', () => {
17
+ beforeEach(() => {
18
+ vi.resetAllMocks()
19
+ existsMock.mockResolvedValue(false)
20
+ readJSONMock.mockResolvedValue(null)
21
+ whichMock.mockResolvedValue(false)
22
+ })
23
+
24
+ it('prefers the Cursor Codex extension when it is installed', async () => {
25
+ existsMock.mockImplementation(async filePath => {
26
+ return (
27
+ filePath === '/Users/tester/.cursor/extensions' ||
28
+ filePath === '/Users/tester/.cursor/extensions/.obsolete'
29
+ )
30
+ })
31
+ readJSONMock.mockResolvedValue({})
32
+
33
+ vi.doMock('node:fs/promises', () => ({
34
+ readdir: vi.fn().mockResolvedValue(['openai.chatgpt-1.2.3']),
35
+ }))
36
+
37
+ const { resolveIntegrationDispatchMode } =
38
+ await import('../src/commands/integration-dispatch-mode.js')
39
+
40
+ await expect(
41
+ resolveIntegrationDispatchMode({
42
+ assistant: 'codex',
43
+ hostIde: 'cursor',
44
+ homeDir: '/Users/tester',
45
+ }),
46
+ ).resolves.toMatchObject({
47
+ mode: 'extension',
48
+ ready: true,
49
+ reason: 'cursor_codex_extension',
50
+ })
51
+ })
52
+
53
+ it('falls back to codex CLI when the Cursor extension is not installed but the CLI is available', async () => {
54
+ whichMock.mockImplementation(async bin => bin === 'codex')
55
+
56
+ const { resolveIntegrationDispatchMode } =
57
+ await import('../src/commands/integration-dispatch-mode.js')
58
+
59
+ await expect(
60
+ resolveIntegrationDispatchMode({
61
+ assistant: 'codex',
62
+ hostIde: 'cursor',
63
+ homeDir: '/Users/tester',
64
+ }),
65
+ ).resolves.toMatchObject({
66
+ mode: 'cli',
67
+ ready: true,
68
+ reason: 'codex_cli',
69
+ })
70
+ })
71
+
72
+ it('returns guidance when neither the Cursor Codex extension nor the codex CLI is available', async () => {
73
+ const { resolveIntegrationDispatchMode } =
74
+ await import('../src/commands/integration-dispatch-mode.js')
75
+
76
+ await expect(
77
+ resolveIntegrationDispatchMode({
78
+ assistant: 'codex',
79
+ hostIde: 'cursor',
80
+ homeDir: '/Users/tester',
81
+ }),
82
+ ).resolves.toMatchObject({
83
+ mode: null,
84
+ ready: false,
85
+ reason: 'missing_codex_runtime',
86
+ })
87
+ })
88
+
89
+ it('prefers the Claude Code extension in Cursor when available', async () => {
90
+ existsMock.mockImplementation(async filePath => {
91
+ return (
92
+ filePath === '/Users/tester/.cursor/extensions' ||
93
+ filePath === '/Users/tester/.cursor/extensions/.obsolete'
94
+ )
95
+ })
96
+ readJSONMock.mockResolvedValue({})
97
+
98
+ vi.doMock('node:fs/promises', () => ({
99
+ readdir: vi.fn().mockResolvedValue(['anthropic.claude-code-0.9.0']),
100
+ }))
101
+
102
+ const { resolveIntegrationDispatchMode } =
103
+ await import('../src/commands/integration-dispatch-mode.js')
104
+
105
+ await expect(
106
+ resolveIntegrationDispatchMode({
107
+ assistant: 'claude-code',
108
+ hostIde: 'cursor',
109
+ homeDir: '/Users/tester',
110
+ }),
111
+ ).resolves.toMatchObject({
112
+ mode: 'extension',
113
+ ready: true,
114
+ reason: 'cursor_claude-code_extension',
115
+ })
116
+ })
117
+
118
+ it('falls back to claude CLI in Cursor when the extension is unavailable', async () => {
119
+ whichMock.mockImplementation(async bin => bin === 'claude')
120
+
121
+ const { resolveIntegrationDispatchMode } =
122
+ await import('../src/commands/integration-dispatch-mode.js')
123
+
124
+ await expect(
125
+ resolveIntegrationDispatchMode({
126
+ assistant: 'claude-code',
127
+ hostIde: 'cursor',
128
+ homeDir: '/Users/tester',
129
+ }),
130
+ ).resolves.toMatchObject({
131
+ mode: 'cli',
132
+ ready: true,
133
+ reason: 'claude-code_cli',
134
+ })
135
+ })
136
+
137
+ it('prefers the Gemini extension in VS Code when available', async () => {
138
+ existsMock.mockImplementation(async filePath => {
139
+ return (
140
+ filePath === '/Users/tester/.vscode/extensions' ||
141
+ filePath === '/Users/tester/.vscode/extensions/.obsolete'
142
+ )
143
+ })
144
+ readJSONMock.mockResolvedValue({})
145
+
146
+ vi.doMock('node:fs/promises', () => ({
147
+ readdir: vi.fn().mockResolvedValue(['google.geminicodeassist-2.1.0']),
148
+ }))
149
+
150
+ const { resolveIntegrationDispatchMode } =
151
+ await import('../src/commands/integration-dispatch-mode.js')
152
+
153
+ await expect(
154
+ resolveIntegrationDispatchMode({
155
+ assistant: 'gemini',
156
+ hostIde: 'vscode',
157
+ homeDir: '/Users/tester',
158
+ }),
159
+ ).resolves.toMatchObject({
160
+ mode: 'extension',
161
+ ready: true,
162
+ reason: 'vscode_gemini_extension',
163
+ })
164
+ })
165
+
166
+ it('falls back to gemini CLI when the VS Code extension is unavailable', async () => {
167
+ whichMock.mockImplementation(async bin => bin === 'gemini')
168
+
169
+ const { resolveIntegrationDispatchMode } =
170
+ await import('../src/commands/integration-dispatch-mode.js')
171
+
172
+ await expect(
173
+ resolveIntegrationDispatchMode({
174
+ assistant: 'gemini',
175
+ hostIde: 'vscode',
176
+ homeDir: '/Users/tester',
177
+ }),
178
+ ).resolves.toMatchObject({
179
+ mode: 'cli',
180
+ ready: true,
181
+ reason: 'gemini_cli',
182
+ })
183
+ })
184
+
185
+ it('falls back to gemini CLI in Trae CN when the Gemini extension is unavailable', async () => {
186
+ whichMock.mockImplementation(async bin => bin === 'gemini')
187
+
188
+ const { resolveIntegrationDispatchMode } =
189
+ await import('../src/commands/integration-dispatch-mode.js')
190
+
191
+ await expect(
192
+ resolveIntegrationDispatchMode({
193
+ assistant: 'gemini',
194
+ hostIde: 'trae-cn',
195
+ homeDir: '/Users/tester',
196
+ }),
197
+ ).resolves.toMatchObject({
198
+ mode: 'cli',
199
+ ready: true,
200
+ reason: 'gemini_cli',
201
+ })
202
+ })
203
+ })
@@ -0,0 +1,165 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const describeIntegrationMock = vi.fn()
4
+ const runIntegrationAutomationMock = vi.fn()
5
+ const exitProcessMock = vi.fn()
6
+ const logMock = {
7
+ header: vi.fn(),
8
+ info: vi.fn(),
9
+ success: vi.fn(),
10
+ warn: vi.fn(),
11
+ hint: vi.fn(),
12
+ ready: vi.fn(),
13
+ error: vi.fn(),
14
+ blank: vi.fn(),
15
+ }
16
+
17
+ vi.mock('../src/commands/integration-install.js', () => ({
18
+ describeIntegration: describeIntegrationMock,
19
+ }))
20
+
21
+ vi.mock('../src/commands/integration-automation.js', () => ({
22
+ runIntegrationAutomation: runIntegrationAutomationMock,
23
+ }))
24
+
25
+ vi.mock('../src/utils/logger.js', () => ({
26
+ log: logMock,
27
+ }))
28
+
29
+ vi.mock('../src/utils/process.js', () => ({
30
+ exitProcess: exitProcessMock,
31
+ }))
32
+
33
+ describe('integration doctor', () => {
34
+ beforeEach(() => {
35
+ vi.resetAllMocks()
36
+ vi.spyOn(process, 'cwd').mockReturnValue('/repo')
37
+ describeIntegrationMock.mockReturnValue({
38
+ assistant: 'codex',
39
+ type: 'native-skill',
40
+ targets: ['.agents/skills/inspecto-onboarding-codex/SKILL.md'],
41
+ preferredInstall: 'npx @inspecto-dev/cli integrations install codex',
42
+ cliSupported: true,
43
+ })
44
+ runIntegrationAutomationMock.mockResolvedValue({
45
+ status: 'preview',
46
+ message:
47
+ 'Preview complete. Inspecto did not write files or open IDE windows. Review the resolved setup below, then rerun without --preview to apply it.',
48
+ nextStep:
49
+ 'Run the same command again without --preview to apply the integration and launch onboarding.',
50
+ details: {
51
+ hostIde: {
52
+ id: 'cursor',
53
+ label: 'Cursor',
54
+ source: 'from --host-ide',
55
+ confidence: 'high',
56
+ candidates: ['cursor'],
57
+ },
58
+ inspectoExtension: {
59
+ source: 'marketplace',
60
+ reference: 'inspecto.inspecto',
61
+ binaryAvailable: true,
62
+ status: 'preview_ready',
63
+ },
64
+ runtime: {
65
+ assistant: 'Codex',
66
+ ready: true,
67
+ mode: 'cli',
68
+ },
69
+ workspace: {
70
+ path: '/repo',
71
+ attempted: true,
72
+ },
73
+ onboarding: {
74
+ uri: 'cursor://inspecto.inspecto/send?...',
75
+ autoSend: true,
76
+ },
77
+ },
78
+ })
79
+ })
80
+
81
+ it('returns structured JSON diagnostics for integration preflight checks', async () => {
82
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
83
+ const { integrationDoctor } = await import('../src/commands/integration-doctor.js')
84
+
85
+ const result = await integrationDoctor('codex', { ide: 'cursor', json: true })
86
+
87
+ expect(runIntegrationAutomationMock).toHaveBeenCalledWith(
88
+ 'codex',
89
+ { ide: 'cursor', preview: true, silent: true },
90
+ '/repo',
91
+ )
92
+ expect(result).toMatchObject({
93
+ schemaVersion: '1',
94
+ status: 'ok',
95
+ assistant: 'codex',
96
+ assets: ['.agents/skills/inspecto-onboarding-codex/SKILL.md'],
97
+ automation: {
98
+ status: 'preview',
99
+ details: {
100
+ hostIde: {
101
+ id: 'cursor',
102
+ },
103
+ },
104
+ },
105
+ })
106
+ expect(logMock.header).not.toHaveBeenCalled()
107
+ expect(consoleSpy).toHaveBeenCalledWith(JSON.stringify(result, null, 2))
108
+ })
109
+
110
+ it('prints a human-readable preflight summary in text mode', async () => {
111
+ const { integrationDoctor } = await import('../src/commands/integration-doctor.js')
112
+
113
+ const result = await integrationDoctor('codex', { ide: 'cursor' })
114
+
115
+ expect(result.status).toBe('ok')
116
+ expect(logMock.header).toHaveBeenCalledWith('Inspecto Integration Doctor')
117
+ expect(logMock.info).toHaveBeenCalledWith('Assistant: codex')
118
+ expect(logMock.info).toHaveBeenCalledWith('Host IDE: Cursor (from --host-ide)')
119
+ expect(logMock.info).toHaveBeenCalledWith('Inspecto extension: marketplace (inspecto.inspecto)')
120
+ expect(logMock.info).toHaveBeenCalledWith('Runtime: Codex via cli')
121
+ expect(logMock.info).toHaveBeenCalledWith('Workspace: /repo')
122
+ expect(logMock.info).toHaveBeenCalledWith('Onboarding URI: cursor://inspecto.inspecto/send?...')
123
+ })
124
+
125
+ it('prints a compact summary when compact mode is enabled', async () => {
126
+ const { integrationDoctor } = await import('../src/commands/integration-doctor.js')
127
+
128
+ const result = await integrationDoctor('codex', { ide: 'cursor', compact: true })
129
+
130
+ expect(result.status).toBe('ok')
131
+ expect(logMock.header).toHaveBeenCalledWith('Inspecto Integration Doctor')
132
+ expect(logMock.info).toHaveBeenCalledWith('Assistant: codex')
133
+ expect(logMock.info).toHaveBeenCalledWith('Host IDE: Cursor (from --host-ide)')
134
+ expect(logMock.info).toHaveBeenCalledWith('Runtime: Codex via cli')
135
+ expect(logMock.info).not.toHaveBeenCalledWith('Asset targets:')
136
+ expect(logMock.info).not.toHaveBeenCalledWith(
137
+ 'Inspecto extension: marketplace (inspecto.inspecto)',
138
+ )
139
+ expect(logMock.info).not.toHaveBeenCalledWith(
140
+ 'Onboarding URI: cursor://inspecto.inspecto/send?...',
141
+ )
142
+ })
143
+
144
+ it('exits with code 1 when failOnBlocked is enabled and the preflight is blocked', async () => {
145
+ runIntegrationAutomationMock.mockResolvedValue({
146
+ status: 'preview_blocked',
147
+ message:
148
+ 'Preview blocked. Inspecto did not write files or open IDE windows because setup cannot continue until the blocking issue below is resolved.',
149
+ nextStep: 'Install Codex in Cursor and rerun the command.',
150
+ details: {
151
+ runtime: {
152
+ assistant: 'Codex',
153
+ ready: false,
154
+ mode: null,
155
+ },
156
+ },
157
+ })
158
+
159
+ const { integrationDoctor } = await import('../src/commands/integration-doctor.js')
160
+
161
+ await integrationDoctor('codex', { ide: 'cursor', failOnBlocked: true })
162
+
163
+ expect(exitProcessMock).toHaveBeenCalledWith(1)
164
+ })
165
+ })
@@ -0,0 +1,172 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import * as fsUtils from '../src/utils/fs.js'
3
+
4
+ vi.mock('../src/utils/fs.js', () => ({
5
+ exists: vi.fn(),
6
+ readJSON: vi.fn(),
7
+ }))
8
+
9
+ describe('resolveIntegrationHostIde', () => {
10
+ const originalEnv = process.env
11
+
12
+ beforeEach(() => {
13
+ vi.resetAllMocks()
14
+ process.env = { ...originalEnv }
15
+ delete process.env.TERM_PROGRAM
16
+ delete process.env.CURSOR_TRACE_DIR
17
+ delete process.env.CURSOR_CHANNEL
18
+ delete process.env.TRAE_APP_DIR
19
+ delete process.env.__CFBundleIdentifier
20
+ delete process.env.COCO_IDE_PLUGIN_TYPE
21
+ delete process.env.npm_config_user_agent
22
+ vi.mocked(fsUtils.exists).mockResolvedValue(false)
23
+ vi.mocked(fsUtils.readJSON).mockResolvedValue(null)
24
+ })
25
+
26
+ afterEach(() => {
27
+ process.env = originalEnv
28
+ })
29
+
30
+ it('prefers an explicit ide argument', async () => {
31
+ const { resolveIntegrationHostIde } = await import('../src/commands/integration-host-ide.js')
32
+
33
+ await expect(
34
+ resolveIntegrationHostIde({
35
+ explicitIde: 'cursor',
36
+ cwd: '/repo',
37
+ }),
38
+ ).resolves.toMatchObject({
39
+ ide: 'cursor',
40
+ confidence: 'high',
41
+ source: 'explicit',
42
+ })
43
+ })
44
+
45
+ it('accepts trae-cn as an explicit ide argument', async () => {
46
+ const { resolveIntegrationHostIde } = await import('../src/commands/integration-host-ide.js')
47
+
48
+ await expect(
49
+ resolveIntegrationHostIde({
50
+ explicitIde: 'trae-cn',
51
+ cwd: '/repo',
52
+ }),
53
+ ).resolves.toMatchObject({
54
+ ide: 'trae-cn',
55
+ confidence: 'high',
56
+ source: 'explicit',
57
+ })
58
+ })
59
+
60
+ it('uses .inspecto settings ide when present', async () => {
61
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
62
+ if (filePath === '/repo/.inspecto/settings.local.json') {
63
+ return { ide: 'vscode' }
64
+ }
65
+ return null
66
+ })
67
+
68
+ const { resolveIntegrationHostIde } = await import('../src/commands/integration-host-ide.js')
69
+
70
+ await expect(
71
+ resolveIntegrationHostIde({
72
+ cwd: '/repo',
73
+ }),
74
+ ).resolves.toMatchObject({
75
+ ide: 'vscode',
76
+ confidence: 'high',
77
+ source: 'config',
78
+ })
79
+ })
80
+
81
+ it('uses trae-cn from .inspecto settings when present', async () => {
82
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
83
+ if (filePath === '/repo/.inspecto/settings.local.json') {
84
+ return { ide: 'trae-cn' }
85
+ }
86
+ return null
87
+ })
88
+
89
+ const { resolveIntegrationHostIde } = await import('../src/commands/integration-host-ide.js')
90
+
91
+ await expect(
92
+ resolveIntegrationHostIde({
93
+ cwd: '/repo',
94
+ }),
95
+ ).resolves.toMatchObject({
96
+ ide: 'trae-cn',
97
+ confidence: 'high',
98
+ source: 'config',
99
+ })
100
+ })
101
+
102
+ it('treats a single env-detected ide as high confidence', async () => {
103
+ process.env.CURSOR_CHANNEL = 'stable'
104
+
105
+ const { resolveIntegrationHostIde } = await import('../src/commands/integration-host-ide.js')
106
+
107
+ await expect(
108
+ resolveIntegrationHostIde({
109
+ cwd: '/repo',
110
+ }),
111
+ ).resolves.toMatchObject({
112
+ ide: 'cursor',
113
+ confidence: 'high',
114
+ source: 'env',
115
+ })
116
+ })
117
+
118
+ it('treats a single project artifact as medium confidence', async () => {
119
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
120
+ return filePath === '/repo/.cursor'
121
+ })
122
+
123
+ const { resolveIntegrationHostIde } = await import('../src/commands/integration-host-ide.js')
124
+
125
+ await expect(
126
+ resolveIntegrationHostIde({
127
+ cwd: '/repo',
128
+ }),
129
+ ).resolves.toMatchObject({
130
+ ide: 'cursor',
131
+ confidence: 'medium',
132
+ source: 'artifact',
133
+ })
134
+ })
135
+
136
+ it('treats a .trae-cn project artifact as medium confidence', async () => {
137
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
138
+ return filePath === '/repo/.trae-cn'
139
+ })
140
+
141
+ const { resolveIntegrationHostIde } = await import('../src/commands/integration-host-ide.js')
142
+
143
+ await expect(
144
+ resolveIntegrationHostIde({
145
+ cwd: '/repo',
146
+ }),
147
+ ).resolves.toMatchObject({
148
+ ide: 'trae-cn',
149
+ confidence: 'medium',
150
+ source: 'artifact',
151
+ })
152
+ })
153
+
154
+ it('refuses to resolve an ide when project artifacts are ambiguous', async () => {
155
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
156
+ return filePath === '/repo/.cursor' || filePath === '/repo/.vscode'
157
+ })
158
+
159
+ const { resolveIntegrationHostIde } = await import('../src/commands/integration-host-ide.js')
160
+
161
+ await expect(
162
+ resolveIntegrationHostIde({
163
+ cwd: '/repo',
164
+ }),
165
+ ).resolves.toMatchObject({
166
+ ide: null,
167
+ confidence: 'low',
168
+ source: 'ambiguous',
169
+ candidates: ['cursor', 'vscode'],
170
+ })
171
+ })
172
+ })