@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.
@@ -166,6 +166,173 @@ describe('apply onboarding flow', () => {
166
166
  })
167
167
  })
168
168
 
169
+ it('only injects the explicitly selected build target', async () => {
170
+ const rspackBuild: BuildToolDetection = {
171
+ tool: 'rspack',
172
+ configPath: 'finder/rspack.config.ts',
173
+ label: 'Rspack (finder/rspack.config.ts)',
174
+ packagePath: 'finder',
175
+ }
176
+
177
+ const result = await applyOnboardingPlan({
178
+ repoRoot: '/repo',
179
+ projectRoot: '/repo/finder',
180
+ packageManager: 'pnpm',
181
+ supportedBuildTargets: [rspackBuild],
182
+ options: {
183
+ shared: false,
184
+ skipInstall: false,
185
+ dryRun: false,
186
+ noExtension: false,
187
+ },
188
+ selectedIDE: { ide: 'vscode', supported: true },
189
+ providerDefault: 'codex.extension',
190
+ plan: {
191
+ status: 'ok',
192
+ warnings: [],
193
+ blockers: [],
194
+ strategy: 'supported',
195
+ actions: [],
196
+ defaults: {
197
+ shared: false,
198
+ extension: true,
199
+ provider: 'codex',
200
+ ide: 'vscode',
201
+ },
202
+ },
203
+ })
204
+
205
+ expect(astInjectorUtils.injectPlugin).toHaveBeenCalledTimes(1)
206
+ expect(astInjectorUtils.injectPlugin).toHaveBeenCalledWith('/repo', rspackBuild, false, false)
207
+ expect(result.postInstall.injectionFailed).toBe(false)
208
+ })
209
+
210
+ it('supports legacy rspack partial onboarding without attempting automatic injection', async () => {
211
+ const legacyRspackBuild: BuildToolDetection = {
212
+ tool: 'rspack',
213
+ configPath: 'finder/rspack-config/rspack.config.dev.ts',
214
+ label: 'Rspack (finder/rspack-config/rspack.config.dev.ts) [Legacy]',
215
+ packagePath: 'finder',
216
+ isLegacyRspack: true,
217
+ }
218
+
219
+ const result = await applyOnboardingPlan({
220
+ repoRoot: '/repo',
221
+ projectRoot: '/repo/finder',
222
+ packageManager: 'pnpm',
223
+ supportedBuildTargets: [legacyRspackBuild],
224
+ options: {
225
+ shared: false,
226
+ skipInstall: false,
227
+ dryRun: false,
228
+ noExtension: false,
229
+ },
230
+ selectedIDE: { ide: 'trae', supported: true },
231
+ providerDefault: 'coco.cli',
232
+ plan: {
233
+ status: 'warning',
234
+ warnings: [
235
+ {
236
+ code: 'legacy-rspack-requires-manual-config',
237
+ message:
238
+ 'Legacy Rspack detected at finder/rspack-config/rspack.config.dev.ts. Inspecto must use the legacy Rspack plugin entry and manual config steps.',
239
+ },
240
+ ],
241
+ blockers: [],
242
+ strategy: 'manual',
243
+ actions: [
244
+ {
245
+ type: 'install_dependency',
246
+ target: '@inspecto-dev/plugin @inspecto-dev/core',
247
+ description: 'Install the Inspecto runtime packages with pnpm.',
248
+ },
249
+ {
250
+ type: 'manual_step',
251
+ target: 'finder/rspack-config/rspack.config.dev.ts',
252
+ description:
253
+ '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.',
254
+ },
255
+ ],
256
+ defaults: {
257
+ shared: false,
258
+ extension: true,
259
+ provider: 'coco',
260
+ ide: 'trae',
261
+ },
262
+ },
263
+ allowManualPlanApply: true,
264
+ })
265
+
266
+ expect(astInjectorUtils.injectPlugin).not.toHaveBeenCalled()
267
+ expect(result.status).toBe('warning')
268
+ expect(result.postInstall.nextSteps).toContain(
269
+ '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.',
270
+ )
271
+ })
272
+
273
+ it('supports webpack 4 partial onboarding without attempting automatic injection', async () => {
274
+ const legacyWebpackBuild: BuildToolDetection = {
275
+ tool: 'webpack',
276
+ configPath: 'app/webpack.config.js',
277
+ label: 'Webpack (app/webpack.config.js) [Webpack 4]',
278
+ packagePath: 'app',
279
+ isLegacyWebpack: true,
280
+ }
281
+
282
+ const result = await applyOnboardingPlan({
283
+ repoRoot: '/repo',
284
+ projectRoot: '/repo/app',
285
+ packageManager: 'pnpm',
286
+ supportedBuildTargets: [legacyWebpackBuild],
287
+ options: {
288
+ shared: false,
289
+ skipInstall: false,
290
+ dryRun: false,
291
+ noExtension: false,
292
+ },
293
+ selectedIDE: { ide: 'cursor', supported: true },
294
+ providerDefault: 'copilot.extension',
295
+ plan: {
296
+ status: 'warning',
297
+ warnings: [
298
+ {
299
+ code: 'legacy-webpack4-requires-manual-config',
300
+ message:
301
+ 'Webpack 4 detected at app/webpack.config.js. Inspecto must use the legacy Webpack 4 plugin entry and manual config steps.',
302
+ },
303
+ ],
304
+ blockers: [],
305
+ strategy: 'manual',
306
+ actions: [
307
+ {
308
+ type: 'install_dependency',
309
+ target: '@inspecto-dev/plugin @inspecto-dev/core',
310
+ description: 'Install the Inspecto runtime packages with pnpm.',
311
+ },
312
+ {
313
+ type: 'manual_step',
314
+ target: 'app/webpack.config.js',
315
+ description:
316
+ 'Update app/webpack.config.js to import `webpackPlugin` from `@inspecto-dev/plugin/legacy/webpack4` and add it to the Webpack plugins array.',
317
+ },
318
+ ],
319
+ defaults: {
320
+ shared: false,
321
+ extension: true,
322
+ provider: 'copilot',
323
+ ide: 'cursor',
324
+ },
325
+ },
326
+ allowManualPlanApply: true,
327
+ })
328
+
329
+ expect(astInjectorUtils.injectPlugin).not.toHaveBeenCalled()
330
+ expect(result.status).toBe('warning')
331
+ expect(result.postInstall.nextSteps).toContain(
332
+ 'Update app/webpack.config.js to import `webpackPlugin` from `@inspecto-dev/plugin/legacy/webpack4` and add it to the Webpack plugins array.',
333
+ )
334
+ })
335
+
169
336
  it('uses onboarding context and planner output and prints JSON from the apply command', async () => {
170
337
  const context: OnboardingContext = {
171
338
  root: '/repo',
@@ -540,6 +707,73 @@ describe('apply onboarding flow', () => {
540
707
  expect(fsUtils.writeJSON).toHaveBeenCalledWith('/repo/.inspecto/prompts.local.json', [])
541
708
  })
542
709
 
710
+ it('merges missing defaults into an existing valid local settings file', async () => {
711
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
712
+ return filePath === '/repo/.inspecto/settings.local.json'
713
+ })
714
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
715
+ if (filePath === '/repo/.inspecto/settings.local.json') {
716
+ return { ide: 'trae-cn' }
717
+ }
718
+ return null
719
+ })
720
+
721
+ await applyOnboardingPlan({
722
+ repoRoot: '/repo',
723
+ projectRoot: '/repo',
724
+ packageManager: 'pnpm',
725
+ supportedBuildTargets: [],
726
+ options: {
727
+ shared: false,
728
+ skipInstall: true,
729
+ dryRun: false,
730
+ noExtension: true,
731
+ },
732
+ selectedIDE: { ide: 'trae-cn', supported: true },
733
+ providerDefault: 'coco.cli',
734
+ })
735
+
736
+ expect(fsUtils.writeJSON).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
737
+ ide: 'trae-cn',
738
+ 'provider.default': 'coco.cli',
739
+ })
740
+ })
741
+
742
+ it('inherits root inspecto defaults when writing package-local settings for a selected subproject', async () => {
743
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
744
+ return filePath === '/repo/.inspecto/settings.local.json'
745
+ })
746
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
747
+ if (filePath === '/repo/.inspecto/settings.local.json') {
748
+ return {
749
+ ide: 'vscode',
750
+ 'provider.default': 'codex.extension',
751
+ }
752
+ }
753
+ return null
754
+ })
755
+
756
+ await applyOnboardingPlan({
757
+ repoRoot: '/repo',
758
+ projectRoot: '/repo/finder',
759
+ packageManager: 'pnpm',
760
+ supportedBuildTargets: [],
761
+ options: {
762
+ shared: false,
763
+ skipInstall: true,
764
+ dryRun: false,
765
+ noExtension: true,
766
+ },
767
+ selectedIDE: { ide: 'cursor', supported: true },
768
+ providerDefault: 'codex.extension',
769
+ })
770
+
771
+ expect(fsUtils.writeJSON).toHaveBeenCalledWith('/repo/finder/.inspecto/settings.local.json', {
772
+ ide: 'vscode',
773
+ 'provider.default': 'codex.extension',
774
+ })
775
+ })
776
+
543
777
  it('prints the same short 3-step success guide when apply finishes cleanly', async () => {
544
778
  const context: OnboardingContext = {
545
779
  root: '/repo',
@@ -41,4 +41,203 @@ describe('detectBuildTools', () => {
41
41
  const result = await detectBuildTools('/repo')
42
42
  expect(result.supported.some(det => det.tool === 'vite')).toBe(true)
43
43
  })
44
+
45
+ it('marks rspack versions below 0.4.0 as legacy', async () => {
46
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
47
+ if (filePath.includes('package.json')) {
48
+ return { devDependencies: { '@rspack/core': '^0.3.14' } }
49
+ }
50
+ return null
51
+ })
52
+
53
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath =>
54
+ filePath.endsWith('rspack.config.ts'),
55
+ )
56
+
57
+ const result = await detectBuildTools('/repo')
58
+ expect(result.supported).toContainEqual(
59
+ expect.objectContaining({
60
+ tool: 'rspack',
61
+ isLegacyRspack: true,
62
+ }),
63
+ )
64
+ })
65
+
66
+ it('treats ranged rspack versions below 0.4.0 as legacy', async () => {
67
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
68
+ if (filePath.includes('package.json')) {
69
+ return { devDependencies: { '@rspack/core': '>=0.3.0 <0.4.0' } }
70
+ }
71
+ return null
72
+ })
73
+
74
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath =>
75
+ filePath.endsWith('rspack.config.ts'),
76
+ )
77
+
78
+ const result = await detectBuildTools('/repo')
79
+ expect(result.supported).toContainEqual(
80
+ expect.objectContaining({
81
+ tool: 'rspack',
82
+ isLegacyRspack: true,
83
+ }),
84
+ )
85
+ })
86
+
87
+ it('treats webpack 4 tilde ranges as legacy', async () => {
88
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
89
+ if (filePath.includes('package.json')) {
90
+ return { devDependencies: { webpack: '~4.46.0' } }
91
+ }
92
+ return null
93
+ })
94
+
95
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath =>
96
+ filePath.endsWith('webpack.config.js'),
97
+ )
98
+
99
+ const result = await detectBuildTools('/repo')
100
+ expect(result.supported).toContainEqual(
101
+ expect.objectContaining({
102
+ tool: 'webpack',
103
+ isLegacyWebpack: true,
104
+ }),
105
+ )
106
+ })
107
+
108
+ it('resolves the real rspack config path from a script entrypoint', async () => {
109
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
110
+ if (filePath.includes('package.json')) {
111
+ return {
112
+ devDependencies: { '@rspack/core': '^0.3.14' },
113
+ scripts: { dev: 'node ./rspack-scripts/dev/start.js dev' },
114
+ }
115
+ }
116
+ return null
117
+ })
118
+
119
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
120
+ return (
121
+ filePath.endsWith('rspack-scripts/dev/start.js') ||
122
+ filePath.endsWith('rspack-config/rspack.config.dev.ts')
123
+ )
124
+ })
125
+
126
+ vi.mocked(fsUtils.readFile).mockImplementation(async filePath => {
127
+ if (filePath.endsWith('rspack-scripts/dev/start.js')) {
128
+ return "const configPath = path.resolve(__dirname, '../../rspack-config/rspack.config.dev.ts')"
129
+ }
130
+ return null
131
+ })
132
+
133
+ const result = await detectBuildTools('/repo', ['finder'])
134
+ expect(result.supported).toContainEqual(
135
+ expect.objectContaining({
136
+ tool: 'rspack',
137
+ configPath: 'finder/rspack-config/rspack.config.dev.ts',
138
+ isLegacyRspack: true,
139
+ }),
140
+ )
141
+ })
142
+
143
+ it('resolves the real rspack config path from rspack serve -c inside a script wrapper', async () => {
144
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
145
+ if (filePath.includes('package.json')) {
146
+ return {
147
+ devDependencies: { '@rspack/core': '^0.3.14' },
148
+ scripts: { dev: 'node ./rspack-scripts/dev/start.js dev' },
149
+ }
150
+ }
151
+ return null
152
+ })
153
+
154
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
155
+ return (
156
+ filePath.endsWith('rspack-scripts/dev/start.js') ||
157
+ filePath.endsWith('rspack-config/rspack.config.dev.ts')
158
+ )
159
+ })
160
+
161
+ vi.mocked(fsUtils.readFile).mockImplementation(async filePath => {
162
+ if (filePath.endsWith('rspack-scripts/dev/start.js')) {
163
+ return 'const cli = `NODE_ENV=development rspack serve -c ./rspack-config/rspack.config.dev.ts`'
164
+ }
165
+ return null
166
+ })
167
+
168
+ const result = await detectBuildTools('/repo', ['finder'])
169
+ expect(result.supported).toContainEqual(
170
+ expect.objectContaining({
171
+ tool: 'rspack',
172
+ configPath: 'finder/rspack-config/rspack.config.dev.ts',
173
+ isLegacyRspack: true,
174
+ }),
175
+ )
176
+ })
177
+
178
+ it('prefers the dev webpack script and resolves a shared webpack base config', async () => {
179
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
180
+ if (filePath.includes('package.json')) {
181
+ return {
182
+ devDependencies: { webpack: '^4.46.0', 'webpack-cli': '^3.0.8' },
183
+ scripts: {
184
+ dll_dev: 'webpack --config webpack.dll.config.js',
185
+ start:
186
+ 'node ./node_modules/.bin/webpack-dev-server --hot --inline --progress --config webpack.config.esbuild.js',
187
+ prod: 'node ./node_modules/.bin/webpack --config webpack.config.prod.js',
188
+ },
189
+ }
190
+ }
191
+ return null
192
+ })
193
+
194
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
195
+ return (
196
+ filePath.endsWith('webpack.config.esbuild.js') ||
197
+ filePath.endsWith('webpack.config.common.js') ||
198
+ filePath.endsWith('webpack.dll.config.js')
199
+ )
200
+ })
201
+
202
+ vi.mocked(fsUtils.readFile).mockImplementation(async filePath => {
203
+ if (filePath.endsWith('webpack.config.esbuild.js')) {
204
+ return "const configPath = './webpack.config.common.js'; const devConfig = require(configPath);"
205
+ }
206
+ return null
207
+ })
208
+
209
+ const result = await detectBuildTools('/repo')
210
+ expect(result.supported).toContainEqual(
211
+ expect.objectContaining({
212
+ tool: 'webpack',
213
+ configPath: 'webpack.config.common.js',
214
+ isLegacyWebpack: true,
215
+ }),
216
+ )
217
+ })
218
+
219
+ it('detects webpack.config.common.js without relying on package scripts', async () => {
220
+ vi.mocked(fsUtils.readJSON).mockImplementation(async filePath => {
221
+ if (filePath.includes('package.json')) {
222
+ return {
223
+ devDependencies: { webpack: '^4.46.0', 'webpack-cli': '^3.0.8' },
224
+ scripts: {},
225
+ }
226
+ }
227
+ return null
228
+ })
229
+
230
+ vi.mocked(fsUtils.exists).mockImplementation(async filePath => {
231
+ return filePath.endsWith('webpack.config.common.js')
232
+ })
233
+
234
+ const result = await detectBuildTools('/repo')
235
+ expect(result.supported).toContainEqual(
236
+ expect.objectContaining({
237
+ tool: 'webpack',
238
+ configPath: 'webpack.config.common.js',
239
+ isLegacyWebpack: true,
240
+ }),
241
+ )
242
+ })
44
243
  })
@@ -200,6 +200,47 @@ describe('doctor command', () => {
200
200
  )
201
201
  })
202
202
 
203
+ it('warns when configured ide and detected ide differ, and explains that config wins', async () => {
204
+ mockFailingInstall()
205
+ vi.mocked(fsUtils.exists).mockImplementation(async (filePath: string) => {
206
+ const existingPaths = new Set([
207
+ '/repo/package.json',
208
+ '/repo/vite.config.ts',
209
+ '/repo/node_modules/@inspecto-dev/plugin',
210
+ '/repo/.inspecto/settings.local.json',
211
+ '/repo/.gitignore',
212
+ ])
213
+ return existingPaths.has(filePath)
214
+ })
215
+ vi.mocked(fsUtils.readJSON).mockImplementation(async (filePath: string) => {
216
+ if (filePath === '/repo/node_modules/@inspecto-dev/plugin/package.json') {
217
+ return { version: '1.2.3' }
218
+ }
219
+ if (filePath === '/repo/.inspecto/settings.local.json') {
220
+ return { ide: 'trae-cn', 'provider.default': 'coco.cli' }
221
+ }
222
+ return null
223
+ })
224
+ vi.mocked(ide.detectIDE).mockResolvedValue({
225
+ detected: [{ ide: 'trae', supported: true }],
226
+ })
227
+
228
+ const result = await collectDoctorResult('/repo')
229
+
230
+ expect(result.warnings).toEqual(
231
+ expect.arrayContaining([
232
+ expect.objectContaining({
233
+ code: 'settings-ide-mismatch',
234
+ message:
235
+ '.inspecto/settings.local.json sets ide=trae-cn, but the current environment looks like trae. Inspecto will use the configured IDE from settings.local.json.',
236
+ hints: [
237
+ 'Update .inspecto/settings.local.json if you want Inspecto to target the currently detected IDE instead.',
238
+ ],
239
+ }),
240
+ ]),
241
+ )
242
+ })
243
+
203
244
  it('preserves the human-readable doctor output in text mode', async () => {
204
245
  mockFailingInstall()
205
246
 
@@ -111,4 +111,60 @@ describe('assistant integration bootstrap wrapper', () => {
111
111
  'project',
112
112
  ])
113
113
  })
114
+
115
+ it('installs coco raw assets into the trae skill directory', async () => {
116
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'inspecto-wrapper-'))
117
+ tempDirs.push(tempRoot)
118
+
119
+ const fakeBin = path.join(tempRoot, 'bin')
120
+ await fs.mkdir(fakeBin, { recursive: true })
121
+
122
+ await fs.writeFile(path.join(fakeBin, 'npx'), '#!/usr/bin/env bash\nexit 127\n', 'utf8')
123
+ await fs.chmod(path.join(fakeBin, 'npx'), 0o755)
124
+
125
+ await fs.writeFile(
126
+ path.join(fakeBin, 'curl'),
127
+ [
128
+ '#!/usr/bin/env bash',
129
+ 'set -euo pipefail',
130
+ 'out=""',
131
+ 'while [[ $# -gt 0 ]]; do',
132
+ ' case "$1" in',
133
+ ' -o)',
134
+ ' out="$2"',
135
+ ' shift 2',
136
+ ' ;;',
137
+ ' *)',
138
+ ' shift',
139
+ ' ;;',
140
+ ' esac',
141
+ 'done',
142
+ 'printf "downloaded from wrapper\n" > "$out"',
143
+ ].join('\n'),
144
+ 'utf8',
145
+ )
146
+ await fs.chmod(path.join(fakeBin, 'curl'), 0o755)
147
+
148
+ const scriptPath = path.resolve(__dirname, '../../../scripts/install.sh')
149
+
150
+ await execFileAsync('bash', [scriptPath, 'coco'], {
151
+ cwd: tempRoot,
152
+ env: {
153
+ ...process.env,
154
+ PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
155
+ },
156
+ })
157
+
158
+ const installed = await fs.readFile(
159
+ path.join(tempRoot, '.trae/skills/inspecto-onboarding/SKILL.md'),
160
+ 'utf8',
161
+ )
162
+ expect(installed).toBe('downloaded from wrapper\n')
163
+
164
+ const launcher = await fs.readFile(
165
+ path.join(tempRoot, '.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh'),
166
+ 'utf8',
167
+ )
168
+ expect(launcher).toBe('downloaded from wrapper\n')
169
+ })
114
170
  })