@inspecto-dev/cli 0.3.3 → 0.3.4

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.
@@ -1,6 +1,8 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  const writeFileMock = vi.fn()
4
+ const writeJSONMock = vi.fn()
5
+ const readJSONMock = vi.fn()
4
6
  const existsMock = vi.fn()
5
7
  const chmodMock = vi.fn()
6
8
  const logMock = {
@@ -14,9 +16,13 @@ const logMock = {
14
16
  blank: vi.fn(),
15
17
  }
16
18
  const runIntegrationAutomationMock = vi.fn()
19
+ const resolveIntegrationHostIdeMock = vi.fn()
20
+ const resolveIntegrationDispatchModeMock = vi.fn()
17
21
 
18
22
  vi.mock('../src/utils/fs.js', () => ({
19
23
  writeFile: writeFileMock,
24
+ writeJSON: writeJSONMock,
25
+ readJSON: readJSONMock,
20
26
  exists: existsMock,
21
27
  }))
22
28
 
@@ -38,11 +44,21 @@ vi.mock('../src/commands/integration-automation.js', () => ({
38
44
  runIntegrationAutomation: runIntegrationAutomationMock,
39
45
  }))
40
46
 
47
+ vi.mock('../src/commands/integration-host-ide.js', () => ({
48
+ resolveIntegrationHostIde: resolveIntegrationHostIdeMock,
49
+ }))
50
+
51
+ vi.mock('../src/commands/integration-dispatch-mode.js', () => ({
52
+ resolveIntegrationDispatchMode: resolveIntegrationDispatchModeMock,
53
+ }))
54
+
41
55
  describe('integration install', () => {
42
56
  beforeEach(() => {
43
57
  vi.resetModules()
44
58
  vi.clearAllMocks()
59
+ vi.spyOn(process, 'cwd').mockReturnValue('/repo')
45
60
  existsMock.mockResolvedValue(false)
61
+ readJSONMock.mockResolvedValue(null)
46
62
  vi.stubGlobal(
47
63
  'fetch',
48
64
  vi.fn().mockResolvedValue({
@@ -56,6 +72,17 @@ describe('integration install', () => {
56
72
  status: 'launched',
57
73
  message: 'Onboarding launched in Cursor for Codex.',
58
74
  })
75
+ resolveIntegrationHostIdeMock.mockResolvedValue({
76
+ ide: 'cursor',
77
+ source: 'explicit',
78
+ confidence: 'high',
79
+ candidates: ['cursor'],
80
+ })
81
+ resolveIntegrationDispatchModeMock.mockResolvedValue({
82
+ mode: 'extension',
83
+ ready: true,
84
+ reason: 'cursor_codex_extension',
85
+ })
59
86
  })
60
87
 
61
88
  it('installs the Claude Code skill into the user skill directory', async () => {
@@ -145,6 +172,92 @@ describe('integration install', () => {
145
172
  )
146
173
  })
147
174
 
175
+ it('installs the Trae skill with a launcher script', async () => {
176
+ const { installIntegration } = await import('../src/commands/integration-install.js')
177
+
178
+ await installIntegration('trae')
179
+
180
+ expect(writeFileMock).toHaveBeenCalledWith(
181
+ '.trae/skills/inspecto-onboarding/SKILL.md',
182
+ '# mock asset',
183
+ )
184
+ expect(writeFileMock).toHaveBeenCalledWith(
185
+ '.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
186
+ '# mock asset',
187
+ )
188
+ expect(chmodMock).toHaveBeenCalledWith(
189
+ '.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
190
+ 0o755,
191
+ )
192
+ })
193
+
194
+ it('persists an explicit project host ide into .inspecto/settings.local.json before onboarding launch', async () => {
195
+ const { installIntegration } = await import('../src/commands/integration-install.js')
196
+
197
+ await installIntegration('gemini', { ide: 'trae-cn' })
198
+
199
+ expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
200
+ ide: 'trae-cn',
201
+ 'provider.default': 'gemini.extension',
202
+ })
203
+ })
204
+
205
+ it('updates the project host ide and provider.default to the selected assistant without dropping other keys', async () => {
206
+ readJSONMock.mockResolvedValue({
207
+ ide: 'trae',
208
+ 'provider.default': 'coco.cli',
209
+ 'prompt.autoSend': true,
210
+ })
211
+
212
+ const { installIntegration } = await import('../src/commands/integration-install.js')
213
+
214
+ await installIntegration('gemini', { ide: 'trae-cn' })
215
+
216
+ expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
217
+ ide: 'trae-cn',
218
+ 'provider.default': 'gemini.extension',
219
+ 'prompt.autoSend': true,
220
+ })
221
+ })
222
+
223
+ it('persists the selected assistant as provider.default so onboarding does not fall back to another detected tool', async () => {
224
+ const { installIntegration } = await import('../src/commands/integration-install.js')
225
+
226
+ await installIntegration('codex', { ide: 'cursor' })
227
+
228
+ expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
229
+ ide: 'cursor',
230
+ 'provider.default': 'codex.extension',
231
+ })
232
+ })
233
+
234
+ it('does not persist host ide settings for user-level installs', async () => {
235
+ const { installIntegration } = await import('../src/commands/integration-install.js')
236
+
237
+ await installIntegration('codex', { scope: 'user', ide: 'cursor' })
238
+
239
+ expect(writeJSONMock).not.toHaveBeenCalled()
240
+ })
241
+
242
+ it('installs the Coco skill with a launcher script', async () => {
243
+ const { installIntegration } = await import('../src/commands/integration-install.js')
244
+
245
+ await installIntegration('coco')
246
+
247
+ expect(writeFileMock).toHaveBeenCalledWith(
248
+ '.trae/skills/inspecto-onboarding/SKILL.md',
249
+ '# mock asset',
250
+ )
251
+ expect(writeFileMock).toHaveBeenCalledWith(
252
+ '.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
253
+ '# mock asset',
254
+ )
255
+ expect(chmodMock).toHaveBeenCalledWith(
256
+ '.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
257
+ 0o755,
258
+ )
259
+ })
260
+
148
261
  it('surfaces a blocked automation outcome as a warning with next-step guidance', async () => {
149
262
  runIntegrationAutomationMock.mockResolvedValue({
150
263
  status: 'blocked',
@@ -207,6 +320,7 @@ describe('integration install', () => {
207
320
  await installIntegration('codex', { preview: true, ide: 'cursor' })
208
321
 
209
322
  expect(writeFileMock).not.toHaveBeenCalled()
323
+ expect(writeJSONMock).not.toHaveBeenCalled()
210
324
  expect(chmodMock).not.toHaveBeenCalled()
211
325
  expect(fetchMock).not.toHaveBeenCalled()
212
326
  expect(logMock.info).toHaveBeenCalledWith('Step 1/6: Previewing Codex integration assets')
@@ -398,6 +512,20 @@ describe('integration install', () => {
398
512
  })
399
513
  })
400
514
 
515
+ it('describes coco using the trae skill install target', async () => {
516
+ const { describeIntegration } = await import('../src/commands/integration-install.js')
517
+
518
+ expect(describeIntegration('coco')).toMatchObject({
519
+ assistant: 'coco',
520
+ targets: [
521
+ '.trae/skills/inspecto-onboarding/SKILL.md',
522
+ '.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
523
+ ],
524
+ preferredInstall:
525
+ 'npx @inspecto-dev/cli integrations install coco --host-ide <vscode|cursor|trae|trae-cn>',
526
+ })
527
+ })
528
+
401
529
  it('prints integration paths without using post-install hints', async () => {
402
530
  const { printIntegrationPath } = await import('../src/commands/integration-install.js')
403
531
 
@@ -68,6 +68,101 @@ describe('target resolution', () => {
68
68
  expect(resolution.status).toBe('resolved')
69
69
  expect(resolution.selected?.packagePath).toBe('apps/web')
70
70
  })
71
+
72
+ it('requires explicit selection when one package has multiple supported build configs', () => {
73
+ const resolution = resolveOnboardingTarget({
74
+ repoRoot: '/repo',
75
+ buildTools: [
76
+ {
77
+ tool: 'vite',
78
+ configPath: 'finder/vite.config.mts',
79
+ label: 'Vite (finder/vite.config.mts)',
80
+ packagePath: 'finder',
81
+ },
82
+ {
83
+ tool: 'rspack',
84
+ configPath: 'finder/rspack-config/rspack.config.dev.ts',
85
+ label: 'Rspack (finder/rspack-config/rspack.config.dev.ts)',
86
+ packagePath: 'finder',
87
+ isLegacyRspack: true,
88
+ },
89
+ ],
90
+ frameworkSupportByPackage: {
91
+ finder: ['react'],
92
+ },
93
+ })
94
+
95
+ expect(resolution.status).toBe('needs_selection')
96
+ expect(resolution.candidates).toHaveLength(2)
97
+ expect(new Set(resolution.candidates.map(item => item.id)).size).toBe(2)
98
+ expect(new Set(resolution.candidates.map(item => item.candidateId)).size).toBe(2)
99
+ expect(resolution.candidates.map(item => item.configPath)).toEqual([
100
+ 'finder/vite.config.mts',
101
+ 'finder/rspack-config/rspack.config.dev.ts',
102
+ ])
103
+ expect(resolution.selectionPurpose).toContain('build target')
104
+ expect(resolution.selectionInstructions).toContain('--target <candidateId>')
105
+ })
106
+
107
+ it('resolves an explicitly selected build target by candidate id', () => {
108
+ const resolution = resolveOnboardingTarget({
109
+ repoRoot: '/repo',
110
+ buildTools: [
111
+ {
112
+ tool: 'vite',
113
+ configPath: 'finder/vite.config.mts',
114
+ label: 'Vite (finder/vite.config.mts)',
115
+ packagePath: 'finder',
116
+ },
117
+ {
118
+ tool: 'rspack',
119
+ configPath: 'finder/rspack-config/rspack.config.dev.ts',
120
+ label: 'Rspack (finder/rspack-config/rspack.config.dev.ts)',
121
+ packagePath: 'finder',
122
+ isLegacyRspack: true,
123
+ },
124
+ ],
125
+ frameworkSupportByPackage: {
126
+ finder: ['react'],
127
+ },
128
+ selectedPackagePath: 'finder:rspack:finder/rspack-config/rspack.config.dev.ts',
129
+ })
130
+
131
+ expect(resolution.status).toBe('resolved')
132
+ expect(resolution.selected?.buildTool).toBe('rspack')
133
+ expect(resolution.selected?.configPath).toBe('finder/rspack-config/rspack.config.dev.ts')
134
+ expect(resolution.selected?.isLegacyRspack).toBe(true)
135
+ })
136
+
137
+ it('resolves an explicitly selected build target by config path for root-level candidates', () => {
138
+ const resolution = resolveOnboardingTarget({
139
+ repoRoot: '/repo',
140
+ buildTools: [
141
+ {
142
+ tool: 'webpack',
143
+ configPath: 'webpack.config.common.js',
144
+ label: 'Webpack (webpack.config.common.js)',
145
+ packagePath: '',
146
+ isLegacyWebpack: true,
147
+ },
148
+ {
149
+ tool: 'webpack',
150
+ configPath: 'webpack.dll.config.js',
151
+ label: 'Webpack (webpack.dll.config.js)',
152
+ packagePath: '',
153
+ isLegacyWebpack: true,
154
+ },
155
+ ],
156
+ frameworkSupportByPackage: {
157
+ '': ['react'],
158
+ },
159
+ selectedPackagePath: 'webpack.config.common.js',
160
+ })
161
+
162
+ expect(resolution.status).toBe('resolved')
163
+ expect(resolution.selected?.configPath).toBe('webpack.config.common.js')
164
+ expect(resolution.selected?.buildTool).toBe('webpack')
165
+ })
71
166
  })
72
167
 
73
168
  describe('onboard command', () => {
@@ -269,6 +269,108 @@ describe('planner orchestration', () => {
269
269
  ])
270
270
  })
271
271
 
272
+ it('returns a legacy rspack partial-manual strategy when the selected build target is legacy rspack', () => {
273
+ const context: OnboardingContext = {
274
+ root: '/repo/finder',
275
+ packageManager: 'pnpm',
276
+ buildTools: {
277
+ supported: [
278
+ {
279
+ tool: 'rspack',
280
+ configPath: 'finder/rspack-config/rspack.config.dev.ts',
281
+ label: 'Rspack (finder/rspack-config/rspack.config.dev.ts) [Legacy]',
282
+ packagePath: 'finder',
283
+ isLegacyRspack: true,
284
+ },
285
+ ],
286
+ unsupported: [],
287
+ },
288
+ frameworks: {
289
+ supported: ['react'],
290
+ unsupported: [],
291
+ },
292
+ ides: [{ ide: 'trae', supported: true }],
293
+ providers: [{ id: 'coco', label: 'Coco CLI', supported: true, preferredMode: 'cli' }],
294
+ }
295
+
296
+ const result = planner.createPlanResult(context)
297
+
298
+ expect(result.status).toBe('warning')
299
+ expect(result.strategy).toBe('manual')
300
+ expect(result.blockers).toEqual([])
301
+ expect(result.warnings).toEqual([
302
+ {
303
+ code: 'legacy-rspack-requires-manual-config',
304
+ message:
305
+ 'Legacy Rspack detected at finder/rspack-config/rspack.config.dev.ts. Inspecto must use the legacy Rspack plugin entry and manual config steps.',
306
+ },
307
+ ])
308
+ expect(result.actions).toEqual([
309
+ {
310
+ type: 'install_dependency',
311
+ target: '@inspecto-dev/plugin @inspecto-dev/core',
312
+ description: 'Install the Inspecto runtime packages with pnpm.',
313
+ },
314
+ {
315
+ type: 'manual_step',
316
+ target: 'finder/rspack-config/rspack.config.dev.ts',
317
+ description:
318
+ 'Update finder/rspack-config/rspack.config.dev.ts to import `rspackPlugin` from `@inspecto-dev/plugin/legacy/rspack` and add it to the Rspack plugins array.',
319
+ },
320
+ ])
321
+ })
322
+
323
+ it('returns a webpack 4 partial-manual strategy when the selected build target is legacy webpack', () => {
324
+ const context: OnboardingContext = {
325
+ root: '/repo/app',
326
+ packageManager: 'pnpm',
327
+ buildTools: {
328
+ supported: [
329
+ {
330
+ tool: 'webpack',
331
+ configPath: 'app/webpack.config.js',
332
+ label: 'Webpack (app/webpack.config.js) [Webpack 4]',
333
+ packagePath: 'app',
334
+ isLegacyWebpack: true,
335
+ },
336
+ ],
337
+ unsupported: [],
338
+ },
339
+ frameworks: {
340
+ supported: ['react'],
341
+ unsupported: [],
342
+ },
343
+ ides: [{ ide: 'cursor', supported: true }],
344
+ providers: [{ id: 'copilot', label: 'Copilot', supported: true, preferredMode: 'extension' }],
345
+ }
346
+
347
+ const result = planner.createPlanResult(context)
348
+
349
+ expect(result.status).toBe('warning')
350
+ expect(result.strategy).toBe('manual')
351
+ expect(result.blockers).toEqual([])
352
+ expect(result.warnings).toEqual([
353
+ {
354
+ code: 'legacy-webpack4-requires-manual-config',
355
+ message:
356
+ 'Webpack 4 detected at app/webpack.config.js. Inspecto must use the legacy Webpack 4 plugin entry and manual config steps.',
357
+ },
358
+ ])
359
+ expect(result.actions).toEqual([
360
+ {
361
+ type: 'install_dependency',
362
+ target: '@inspecto-dev/plugin @inspecto-dev/core',
363
+ description: 'Install the Inspecto runtime packages with pnpm.',
364
+ },
365
+ {
366
+ type: 'manual_step',
367
+ target: 'app/webpack.config.js',
368
+ description:
369
+ 'Update app/webpack.config.js to import `webpackPlugin` from `@inspecto-dev/plugin/legacy/webpack4` and add it to the Webpack plugins array.',
370
+ },
371
+ ])
372
+ })
373
+
272
374
  it('blocks root-level apply planning when multiple supported build targets are detected', () => {
273
375
  const context: OnboardingContext = {
274
376
  root: '/repo',