@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.
@@ -13,6 +13,7 @@ const logMock = {
13
13
  error: vi.fn(),
14
14
  blank: vi.fn(),
15
15
  }
16
+ const runIntegrationAutomationMock = vi.fn()
16
17
 
17
18
  vi.mock('../src/utils/fs.js', () => ({
18
19
  writeFile: writeFileMock,
@@ -33,6 +34,10 @@ vi.mock('../src/utils/logger.js', () => ({
33
34
  log: logMock,
34
35
  }))
35
36
 
37
+ vi.mock('../src/commands/integration-automation.js', () => ({
38
+ runIntegrationAutomation: runIntegrationAutomationMock,
39
+ }))
40
+
36
41
  describe('integration install', () => {
37
42
  beforeEach(() => {
38
43
  vi.resetModules()
@@ -47,6 +52,10 @@ describe('integration install', () => {
47
52
  statusText: 'OK',
48
53
  }),
49
54
  )
55
+ runIntegrationAutomationMock.mockResolvedValue({
56
+ status: 'launched',
57
+ message: 'Onboarding launched in Cursor for Codex.',
58
+ })
50
59
  })
51
60
 
52
61
  it('installs the Claude Code skill into the user skill directory', async () => {
@@ -70,38 +79,209 @@ describe('integration install', () => {
70
79
  '/Users/tester/.claude/skills/inspecto-onboarding-claude-code/scripts/run-inspecto.sh',
71
80
  0o755,
72
81
  )
82
+ expect(runIntegrationAutomationMock).not.toHaveBeenCalled()
73
83
  })
74
84
 
75
- it('installs the Codex skill into the Codex skill directory', async () => {
85
+ it('installs the Codex skill into the user skill directory when scoped to user', async () => {
76
86
  const { installIntegration } = await import('../src/commands/integration-install.js')
77
87
 
78
- await installIntegration('codex')
88
+ await installIntegration('codex', { scope: 'user' })
79
89
 
80
90
  expect(writeFileMock).toHaveBeenCalledWith(
81
- '/Users/tester/.codex/skills/inspecto-onboarding-codex/SKILL.md',
91
+ '/Users/tester/.agents/skills/inspecto-onboarding-codex/SKILL.md',
82
92
  '# mock asset',
83
93
  )
84
94
  expect(writeFileMock).toHaveBeenCalledWith(
85
- '/Users/tester/.codex/skills/inspecto-onboarding-codex/agents/openai.yaml',
95
+ '/Users/tester/.agents/skills/inspecto-onboarding-codex/agents/openai.yaml',
86
96
  '# mock asset',
87
97
  )
88
98
  expect(writeFileMock).toHaveBeenCalledWith(
89
- '/Users/tester/.codex/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
99
+ '/Users/tester/.agents/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
90
100
  '# mock asset',
91
101
  )
92
102
  expect(chmodMock).toHaveBeenCalledWith(
93
- '/Users/tester/.codex/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
103
+ '/Users/tester/.agents/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
94
104
  0o755,
95
105
  )
106
+ expect(logMock.success).toHaveBeenCalledWith('Step 1/6: Installed Codex integration assets')
107
+ expect(logMock.hint).toHaveBeenCalledWith(
108
+ '/Users/tester/.agents/skills/inspecto-onboarding-codex/SKILL.md',
109
+ )
110
+ expect(runIntegrationAutomationMock).not.toHaveBeenCalled()
111
+ expect(logMock.ready).toHaveBeenCalledWith(
112
+ 'Installed Codex integration assets. User-level installs only write integration assets and do not launch onboarding automatically.',
113
+ )
114
+ expect(logMock.hint).toHaveBeenCalledWith(
115
+ 'Run the install command again from your target project root with --host-ide <vscode|cursor|trae|trae-cn> when you want to launch onboarding automatically.',
116
+ )
117
+ })
118
+
119
+ it('clarifies that --host-ide does not launch onboarding for user-level installs', async () => {
120
+ const { installIntegration } = await import('../src/commands/integration-install.js')
121
+
122
+ await installIntegration('codex', { scope: 'user', ide: 'cursor' })
123
+
124
+ expect(runIntegrationAutomationMock).not.toHaveBeenCalled()
125
+ expect(logMock.ready).toHaveBeenCalledWith(
126
+ 'Installed Codex integration assets. User-level installs only write integration assets and do not launch onboarding automatically.',
127
+ )
128
+ expect(logMock.hint).toHaveBeenCalledWith(
129
+ 'The provided --host-ide value is saved only as a rerun hint for later; this command does not open Cursor for user-level installs.',
130
+ )
131
+ expect(logMock.hint).toHaveBeenCalledWith(
132
+ 'Run the install command again from your target project root with --host-ide cursor when you want to launch onboarding automatically.',
133
+ )
96
134
  })
97
135
 
98
- it('installs the Cursor AGENTS fallback when agents mode is selected', async () => {
136
+ it('installs the Cursor agents fallback gracefully acting as skills mode', async () => {
99
137
  const { installIntegration } = await import('../src/commands/integration-install.js')
100
138
 
101
139
  await installIntegration('cursor', { mode: 'agents' })
102
140
 
103
141
  expect(writeFileMock).toHaveBeenCalledTimes(1)
104
- expect(writeFileMock).toHaveBeenCalledWith('AGENTS.md', '# mock asset')
142
+ expect(writeFileMock).toHaveBeenCalledWith(
143
+ '.cursor/skills/inspecto-onboarding/SKILL.md',
144
+ '# mock asset',
145
+ )
146
+ })
147
+
148
+ it('surfaces a blocked automation outcome as a warning with next-step guidance', async () => {
149
+ runIntegrationAutomationMock.mockResolvedValue({
150
+ status: 'blocked',
151
+ message:
152
+ 'Automatic setup stopped: Inspecto could not find a runnable Gemini target in Trae CN.',
153
+ nextStep:
154
+ 'Install the Gemini plugin in Trae CN or install the `gemini` CLI, then rerun the command.',
155
+ })
156
+
157
+ const { installIntegration } = await import('../src/commands/integration-install.js')
158
+
159
+ await installIntegration('gemini', { ide: 'trae-cn' })
160
+
161
+ expect(logMock.warn).toHaveBeenCalledWith(
162
+ 'Automatic setup stopped: Inspecto could not find a runnable Gemini target in Trae CN.',
163
+ )
164
+ expect(logMock.hint).toHaveBeenCalledWith(
165
+ 'Install the Gemini plugin in Trae CN or install the `gemini` CLI, then rerun the command.',
166
+ )
167
+ })
168
+
169
+ it('surfaces a partial automation outcome as a warning with follow-up guidance', async () => {
170
+ runIntegrationAutomationMock.mockResolvedValue({
171
+ status: 'partial',
172
+ message:
173
+ 'Onboarding opened in Trae CN for Gemini, but the Inspecto extension may still need manual setup.',
174
+ nextStep: 'Install the Inspecto extension in Trae CN if IDE-side features are missing.',
175
+ })
176
+
177
+ const { installIntegration } = await import('../src/commands/integration-install.js')
178
+
179
+ await installIntegration('gemini', { ide: 'trae-cn' })
180
+
181
+ expect(logMock.warn).toHaveBeenCalledWith(
182
+ 'Onboarding opened in Trae CN for Gemini, but the Inspecto extension may still need manual setup.',
183
+ )
184
+ expect(logMock.hint).toHaveBeenCalledWith(
185
+ 'Install the Inspecto extension in Trae CN if IDE-side features are missing.',
186
+ )
187
+ })
188
+
189
+ it('does not write files in preview mode and surfaces the preview summary', async () => {
190
+ const fetchMock = vi.fn().mockResolvedValue({
191
+ ok: true,
192
+ text: async () => '# mock asset',
193
+ status: 200,
194
+ statusText: 'OK',
195
+ })
196
+ vi.stubGlobal('fetch', fetchMock)
197
+ runIntegrationAutomationMock.mockResolvedValue({
198
+ status: 'preview',
199
+ message:
200
+ 'Preview complete. Inspecto did not write files or open IDE windows. Review the resolved setup below, then rerun without --preview to apply it.',
201
+ nextStep:
202
+ 'Run the same command again without --preview to apply the integration and launch onboarding.',
203
+ })
204
+
205
+ const { installIntegration } = await import('../src/commands/integration-install.js')
206
+
207
+ await installIntegration('codex', { preview: true, ide: 'cursor' })
208
+
209
+ expect(writeFileMock).not.toHaveBeenCalled()
210
+ expect(chmodMock).not.toHaveBeenCalled()
211
+ expect(fetchMock).not.toHaveBeenCalled()
212
+ expect(logMock.info).toHaveBeenCalledWith('Step 1/6: Previewing Codex integration assets')
213
+ expect(logMock.ready).toHaveBeenCalledWith(
214
+ 'Preview complete. Inspecto did not write files or open IDE windows. Review the resolved setup below, then rerun without --preview to apply it.',
215
+ )
216
+ expect(logMock.hint).toHaveBeenCalledWith(
217
+ 'Run the same command again without --preview to apply the integration and launch onboarding.',
218
+ )
219
+ })
220
+
221
+ it('prints structured JSON output for preview mode when requested', async () => {
222
+ const fetchMock = vi.fn()
223
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
224
+ vi.stubGlobal('fetch', fetchMock)
225
+ runIntegrationAutomationMock.mockResolvedValue({
226
+ status: 'preview',
227
+ message:
228
+ 'Preview complete. Inspecto did not write files or open IDE windows. Review the resolved setup below, then rerun without --preview to apply it.',
229
+ nextStep:
230
+ 'Run the same command again without --preview to apply the integration and launch onboarding.',
231
+ details: {
232
+ hostIde: {
233
+ id: 'cursor',
234
+ label: 'Cursor',
235
+ source: 'explicit --host-ide',
236
+ confidence: 'high',
237
+ candidates: ['cursor'],
238
+ },
239
+ },
240
+ })
241
+
242
+ const { installIntegration } = await import('../src/commands/integration-install.js')
243
+
244
+ await installIntegration('codex', { preview: true, ide: 'cursor', json: true })
245
+
246
+ expect(writeFileMock).not.toHaveBeenCalled()
247
+ expect(fetchMock).not.toHaveBeenCalled()
248
+ expect(logMock.header).not.toHaveBeenCalled()
249
+ expect(logMock.info).not.toHaveBeenCalled()
250
+ expect(logMock.ready).not.toHaveBeenCalled()
251
+ const payload = JSON.parse(String(consoleLogSpy.mock.calls.at(-1)?.[0] ?? '{}'))
252
+ expect(payload).toMatchObject({
253
+ status: 'preview',
254
+ assistant: 'codex',
255
+ preview: true,
256
+ message:
257
+ 'Preview complete. Inspecto did not write files or open IDE windows. Review the resolved setup below, then rerun without --preview to apply it.',
258
+ })
259
+ expect(payload.assets).toEqual(
260
+ expect.arrayContaining(['.agents/skills/inspecto-onboarding-codex/SKILL.md']),
261
+ )
262
+
263
+ consoleLogSpy.mockRestore()
264
+ })
265
+
266
+ it('surfaces a blocked preview outcome as a warning with follow-up guidance', async () => {
267
+ runIntegrationAutomationMock.mockResolvedValue({
268
+ status: 'preview_blocked',
269
+ message:
270
+ 'Preview blocked. Inspecto did not write files or open IDE windows because setup cannot continue until the blocking issue below is resolved.',
271
+ nextStep:
272
+ 'Install the Trae CN CLI binary or rerun the command from a shell where it is available, then rerun the command.',
273
+ })
274
+
275
+ const { installIntegration } = await import('../src/commands/integration-install.js')
276
+
277
+ await installIntegration('gemini', { preview: true, ide: 'trae-cn' })
278
+
279
+ expect(logMock.warn).toHaveBeenCalledWith(
280
+ 'Preview blocked. Inspecto did not write files or open IDE windows because setup cannot continue until the blocking issue below is resolved.',
281
+ )
282
+ expect(logMock.hint).toHaveBeenCalledWith(
283
+ 'Install the Trae CN CLI binary or rerun the command from a shell where it is available, then rerun the command.',
284
+ )
105
285
  })
106
286
 
107
287
  it('refuses a multi-file Claude install before writing anything when one target already exists', async () => {
@@ -171,7 +351,7 @@ describe('integration install', () => {
171
351
  expect.objectContaining({
172
352
  assistant: 'codex',
173
353
  type: 'native-skill',
174
- installTarget: '~/.codex/skills/',
354
+ installTarget: '.agents/skills/',
175
355
  cliSupported: true,
176
356
  }),
177
357
  expect.objectContaining({
@@ -181,8 +361,8 @@ describe('integration install', () => {
181
361
  }),
182
362
  expect.objectContaining({
183
363
  assistant: 'copilot',
184
- type: 'instruction-template',
185
- installTarget: '.github/copilot-instructions.md or AGENTS.md',
364
+ type: 'native-skill',
365
+ installTarget: '.github/skills/inspecto-onboarding/',
186
366
  }),
187
367
  ]),
188
368
  )
@@ -198,21 +378,23 @@ describe('integration install', () => {
198
378
  '/Users/tester/.claude/skills/inspecto-onboarding-claude-code/agents/openai.yaml',
199
379
  '/Users/tester/.claude/skills/inspecto-onboarding-claude-code/scripts/run-inspecto.sh',
200
380
  ],
201
- preferredInstall: 'npx @inspecto-dev/cli integrations install claude-code --scope project',
381
+ preferredInstall:
382
+ 'npx @inspecto-dev/cli integrations install claude-code --scope project --host-ide <vscode|cursor|trae|trae-cn>',
202
383
  })
203
384
  })
204
385
 
205
386
  it('describes codex using the same CLI-managed install target model', async () => {
206
387
  const { describeIntegration } = await import('../src/commands/integration-install.js')
207
388
 
208
- expect(describeIntegration('codex')).toMatchObject({
389
+ expect(describeIntegration('codex', { scope: 'user' })).toMatchObject({
209
390
  assistant: 'codex',
210
391
  targets: [
211
- '/Users/tester/.codex/skills/inspecto-onboarding-codex/SKILL.md',
212
- '/Users/tester/.codex/skills/inspecto-onboarding-codex/agents/openai.yaml',
213
- '/Users/tester/.codex/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
392
+ '/Users/tester/.agents/skills/inspecto-onboarding-codex/SKILL.md',
393
+ '/Users/tester/.agents/skills/inspecto-onboarding-codex/agents/openai.yaml',
394
+ '/Users/tester/.agents/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
214
395
  ],
215
- preferredInstall: 'npx @inspecto-dev/cli integrations install codex',
396
+ preferredInstall:
397
+ 'npx @inspecto-dev/cli integrations install codex --host-ide <vscode|cursor|trae|trae-cn>',
216
398
  })
217
399
  })
218
400
 
@@ -225,13 +407,14 @@ describe('integration install', () => {
225
407
  '/Users/tester/.claude/skills/inspecto-onboarding-claude-code/SKILL.md',
226
408
  )
227
409
  expect(logMock.hint).toHaveBeenCalledWith(
228
- 'Preferred install: npx @inspecto-dev/cli integrations install claude-code --scope project',
410
+ 'Preferred install: npx @inspecto-dev/cli integrations install claude-code --scope project --host-ide <vscode|cursor|trae|trae-cn>',
229
411
  )
230
412
  expect(logMock.hint).not.toHaveBeenCalledWith('Restart Claude Code to load the new skill.')
231
413
  })
232
414
 
233
415
  it('rejects unsupported extra args and options for list/path', async () => {
234
416
  const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never)
417
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
235
418
 
236
419
  vi.doMock('../src/commands/init.js', () => ({ init: vi.fn() }))
237
420
  vi.doMock('../src/commands/doctor.js', () => ({ doctor: vi.fn() }))
@@ -244,18 +427,44 @@ describe('integration install', () => {
244
427
 
245
428
  await runCli(['node', 'inspecto', 'integrations', 'list', 'extra'])
246
429
  await runCli(['node', 'inspecto', 'integrations', 'path', 'claude-code', '--force'])
430
+ await runCli(['node', 'inspecto', 'integrations', 'list', '--host-ide', 'cursor'])
431
+ await runCli([
432
+ 'node',
433
+ 'inspecto',
434
+ 'integrations',
435
+ 'path',
436
+ 'claude-code',
437
+ '--inspecto-vsix',
438
+ '/tmp/inspecto.vsix',
439
+ ])
440
+ await runCli(['node', 'inspecto', 'integrations', 'path', 'claude-code', '--preview'])
441
+ await runCli(['node', 'inspecto', 'integrations', 'path', 'claude-code', '--json'])
247
442
 
248
443
  expect(logMock.error).toHaveBeenCalledWith(
249
- 'The `list` subcommand does not accept assistant names, --scope, --mode, or --force.',
444
+ 'The `list` subcommand does not accept assistant names, --scope, --mode, --host-ide, --inspecto-vsix, --compact, --json, --preview, or --force.',
250
445
  )
251
446
  expect(logMock.error).toHaveBeenCalledWith('The `path` subcommand does not support `--force`.')
447
+ expect(logMock.error).toHaveBeenCalledWith(
448
+ 'The `path` subcommand does not support `--inspecto-vsix`.',
449
+ )
450
+ expect(logMock.error).toHaveBeenCalledWith(
451
+ 'The `path` subcommand does not support `--preview`.',
452
+ )
453
+ expect(JSON.parse(String(consoleErrorSpy.mock.calls.at(-1)?.[0] ?? '{}'))).toMatchObject({
454
+ status: 'error',
455
+ error: {
456
+ message: 'The `path` subcommand does not support `--json`.',
457
+ },
458
+ })
252
459
  expect(exitSpy).toHaveBeenCalledWith(1)
253
460
 
461
+ consoleErrorSpy.mockRestore()
254
462
  exitSpy.mockRestore()
255
463
  })
256
464
 
257
465
  it('wires the nested CLI command to the integration installer', async () => {
258
466
  const installCommand = vi.fn().mockResolvedValue(undefined)
467
+ const doctorCommand = vi.fn().mockResolvedValue({ status: 'ok' })
259
468
  const listCommand = vi.fn().mockResolvedValue(undefined)
260
469
  const pathCommand = vi.fn().mockResolvedValue(undefined)
261
470
 
@@ -270,6 +479,9 @@ describe('integration install', () => {
270
479
  printIntegrationList: listCommand,
271
480
  printIntegrationPath: pathCommand,
272
481
  }))
482
+ vi.doMock('../src/commands/integration-doctor.js', () => ({
483
+ integrationDoctor: doctorCommand,
484
+ }))
273
485
 
274
486
  const { runCli } = await import('../src/bin.js')
275
487
 
@@ -279,6 +491,12 @@ describe('integration install', () => {
279
491
  'integrations',
280
492
  'install',
281
493
  'copilot',
494
+ '--host-ide',
495
+ 'cursor',
496
+ '--inspecto-vsix',
497
+ '/tmp/inspecto.vsix',
498
+ '--preview',
499
+ '--json',
282
500
  '--mode',
283
501
  'agents',
284
502
  '--force',
@@ -286,9 +504,53 @@ describe('integration install', () => {
286
504
 
287
505
  await runCli(['node', 'inspecto', 'integrations', 'list'])
288
506
  await runCli(['node', 'inspecto', 'integrations', 'path', 'claude-code', '--scope', 'user'])
507
+ await runCli([
508
+ 'node',
509
+ 'inspecto',
510
+ 'integrations',
511
+ 'doctor',
512
+ 'codex',
513
+ '--host-ide',
514
+ 'cursor',
515
+ '--compact',
516
+ '--json',
517
+ ])
289
518
 
290
- expect(installCommand).toHaveBeenCalledWith('copilot', { mode: 'agents', force: true })
519
+ expect(installCommand).toHaveBeenCalledWith('copilot', {
520
+ ide: 'cursor',
521
+ inspectoVsix: '/tmp/inspecto.vsix',
522
+ preview: true,
523
+ json: true,
524
+ mode: 'agents',
525
+ force: true,
526
+ })
291
527
  expect(listCommand).toHaveBeenCalledWith()
292
528
  expect(pathCommand).toHaveBeenCalledWith('claude-code', { scope: 'user' })
529
+ expect(doctorCommand).toHaveBeenCalledWith('codex', {
530
+ ide: 'cursor',
531
+ compact: true,
532
+ failOnBlocked: true,
533
+ json: true,
534
+ })
535
+ })
536
+
537
+ it('sets a non-zero exit code when integrations doctor reports a blocked result', async () => {
538
+ const doctorCommand = vi.fn().mockResolvedValue({ status: 'blocked' })
539
+
540
+ vi.doMock('../src/commands/init.js', () => ({ init: vi.fn() }))
541
+ vi.doMock('../src/commands/doctor.js', () => ({ doctor: vi.fn() }))
542
+ vi.doMock('../src/commands/teardown.js', () => ({ teardown: vi.fn() }))
543
+ vi.doMock('../src/commands/detect.js', () => ({ detect: vi.fn() }))
544
+ vi.doMock('../src/commands/plan.js', () => ({ plan: vi.fn() }))
545
+ vi.doMock('../src/commands/apply.js', () => ({ apply: vi.fn() }))
546
+ vi.doMock('../src/commands/integration-doctor.js', () => ({
547
+ integrationDoctor: doctorCommand,
548
+ }))
549
+
550
+ const { runCli } = await import('../src/bin.js')
551
+
552
+ await runCli(['node', 'inspecto', 'integrations', 'doctor', 'codex', '--json'])
553
+
554
+ expect(doctorCommand).toHaveBeenCalledWith('codex', { json: true, failOnBlocked: true })
293
555
  })
294
556
  })
@@ -352,5 +352,123 @@ describe('onboard command', () => {
352
352
  String(call[0]).includes('Start the local dev server with `pnpm dev`'),
353
353
  ),
354
354
  ).toBe(true)
355
+ expect(
356
+ consoleSpy.mock.calls.some(call =>
357
+ String(call[0]).includes('code --install-extension inspecto.inspecto'),
358
+ ),
359
+ ).toBe(false)
360
+ })
361
+
362
+ it('prints manual extension guidance only when the IDE extension still needs manual completion', async () => {
363
+ const session: ResolvedOnboardingSession = {
364
+ status: 'success',
365
+ target: {
366
+ status: 'resolved',
367
+ selected: {
368
+ packagePath: '',
369
+ configPath: 'vite.config.ts',
370
+ buildTool: 'vite',
371
+ frameworks: ['react'],
372
+ automaticInjection: true,
373
+ },
374
+ candidates: [
375
+ {
376
+ packagePath: '',
377
+ configPath: 'vite.config.ts',
378
+ buildTool: 'vite',
379
+ frameworks: ['react'],
380
+ automaticInjection: true,
381
+ },
382
+ ],
383
+ reason: 'Only one supported target was detected.',
384
+ },
385
+ summary: {
386
+ headline: 'Inspecto is ready to onboard /repo.',
387
+ changes: ['Install dependencies.'],
388
+ risks: [],
389
+ manualFollowUp: [],
390
+ },
391
+ confirmation: { required: false },
392
+ verification: {
393
+ available: true,
394
+ devCommand: 'pnpm dev',
395
+ message: 'Start the local dev server with `pnpm dev` to verify Inspecto in the browser.',
396
+ },
397
+ context: {
398
+ root: '/repo',
399
+ packageManager: 'pnpm',
400
+ buildTools: { supported: [], unsupported: [] },
401
+ frameworks: { supported: ['react'], unsupported: [] },
402
+ ides: [{ ide: 'vscode', supported: true }],
403
+ providers: [{ id: 'codex', label: 'Codex CLI', supported: true, preferredMode: 'cli' }],
404
+ },
405
+ plan: {
406
+ status: 'ok',
407
+ warnings: [],
408
+ blockers: [],
409
+ strategy: 'supported',
410
+ actions: [],
411
+ defaults: {
412
+ provider: 'codex',
413
+ ide: 'vscode',
414
+ shared: false,
415
+ extension: true,
416
+ },
417
+ },
418
+ projectRoot: '/repo',
419
+ selectedIDE: { ide: 'vscode', supported: true },
420
+ providerDefault: 'codex.cli',
421
+ }
422
+
423
+ const applied: OnboardCommandResult = {
424
+ status: 'partial_success',
425
+ target: session.target,
426
+ summary: session.summary,
427
+ confirmation: session.confirmation,
428
+ ideExtension: {
429
+ required: true,
430
+ installed: false,
431
+ manualRequired: true,
432
+ installCommand: 'code --install-extension inspecto.inspecto',
433
+ marketplaceUrl: 'https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto',
434
+ openVsxUrl: 'https://open-vsx.org/extension/inspecto/inspecto',
435
+ },
436
+ verification: session.verification,
437
+ result: {
438
+ changedFiles: ['vite.config.ts'],
439
+ installedDependencies: ['@inspecto-dev/plugin', '@inspecto-dev/core'],
440
+ selectedProviderDefault: 'codex.cli',
441
+ selectedIDE: 'vscode',
442
+ mutations: [],
443
+ },
444
+ diagnostics: {
445
+ warnings: ['IDE extension installation still needs manual completion.'],
446
+ errors: [],
447
+ nextSteps: [],
448
+ },
449
+ }
450
+
451
+ vi.mocked(resolveOnboardingSession).mockResolvedValue(session)
452
+ vi.mocked(applyResolvedOnboardingSession).mockResolvedValue(applied)
453
+
454
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
455
+ const result = await onboard({ json: false })
456
+
457
+ expect(result.status).toBe('partial_success')
458
+ expect(
459
+ consoleSpy.mock.calls.some(call =>
460
+ String(call[0]).includes('Complete the IDE extension install before verification'),
461
+ ),
462
+ ).toBe(true)
463
+ expect(
464
+ consoleSpy.mock.calls.some(call =>
465
+ String(call[0]).includes('code --install-extension inspecto.inspecto'),
466
+ ),
467
+ ).toBe(true)
468
+ expect(
469
+ consoleSpy.mock.calls.some(call =>
470
+ String(call[0]).includes('Start the local dev server with `pnpm dev`'),
471
+ ),
472
+ ).toBe(false)
355
473
  })
356
474
  })
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ getDualModeProviderCapability,
4
+ getHostIdeLabel,
5
+ isSupportedHostIde,
6
+ } from '@inspecto-dev/types'
7
+
8
+ describe('shared capabilities', () => {
9
+ it('exposes shared host IDE labels', () => {
10
+ expect(getHostIdeLabel('vscode')).toBe('VS Code')
11
+ expect(getHostIdeLabel('cursor')).toBe('Cursor')
12
+ expect(getHostIdeLabel('trae')).toBe('Trae')
13
+ expect(getHostIdeLabel('trae-cn')).toBe('Trae CN')
14
+ })
15
+
16
+ it('exposes supported host IDE guards', () => {
17
+ expect(isSupportedHostIde('vscode')).toBe(true)
18
+ expect(isSupportedHostIde('cursor')).toBe(true)
19
+ expect(isSupportedHostIde('trae')).toBe(true)
20
+ expect(isSupportedHostIde('trae-cn')).toBe(true)
21
+ expect(isSupportedHostIde('unknown')).toBe(false)
22
+ })
23
+
24
+ it('exposes dual-mode provider metadata', () => {
25
+ expect(getDualModeProviderCapability('codex')).toEqual({
26
+ label: 'Codex',
27
+ extensionId: 'openai.chatgpt',
28
+ cliBin: 'codex',
29
+ })
30
+
31
+ expect(getDualModeProviderCapability('claude-code')).toEqual({
32
+ label: 'Claude Code',
33
+ extensionId: 'anthropic.claude-code',
34
+ cliBin: 'claude',
35
+ })
36
+
37
+ expect(getDualModeProviderCapability('gemini')).toEqual({
38
+ label: 'Gemini',
39
+ extensionId: 'google.geminicodeassist',
40
+ cliBin: 'gemini',
41
+ })
42
+
43
+ expect(getDualModeProviderCapability('copilot')).toBeUndefined()
44
+ })
45
+ })