@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.
package/dist/index.d.ts CHANGED
@@ -49,9 +49,14 @@ interface OnboardingContext {
49
49
  providers: OnboardingProvider[];
50
50
  }
51
51
  interface OnboardingTargetCandidate {
52
+ id?: string;
53
+ candidateId?: string;
52
54
  packagePath: string;
53
55
  configPath: string;
56
+ label?: string;
54
57
  buildTool: BuildTool;
58
+ isLegacyRspack?: boolean;
59
+ isLegacyWebpack?: boolean;
55
60
  frameworks: string[];
56
61
  automaticInjection: boolean;
57
62
  }
@@ -60,6 +65,8 @@ interface OnboardingTargetResolution {
60
65
  selected?: OnboardingTargetCandidate;
61
66
  candidates: OnboardingTargetCandidate[];
62
67
  reason: string;
68
+ selectionPurpose?: string;
69
+ selectionInstructions?: string;
63
70
  }
64
71
  interface OnboardingSummary {
65
72
  headline: string;
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  reportCommandError,
11
11
  teardown,
12
12
  writeCommandOutput
13
- } from "./chunk-LJOKPCPD.js";
13
+ } from "./chunk-2MOEVONN.js";
14
14
  export {
15
15
  apply,
16
16
  collectDoctorResult,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inspecto-dev/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "CLI tools for Inspecto onboarding and lifecycle management",
5
5
  "keywords": [
6
6
  "inspecto",
@@ -20,10 +20,10 @@
20
20
  "ora": "^9.3.0",
21
21
  "picocolors": "^1.0.0",
22
22
  "prompts": "^2.4.2",
23
- "@inspecto-dev/types": "0.3.3"
23
+ "@inspecto-dev/types": "0.3.4"
24
24
  },
25
25
  "devDependencies": {
26
- "@types/node": "^20.0.0",
26
+ "@types/node": "^20.19.39",
27
27
  "@types/prompts": "^2.4.9",
28
28
  "tsup": "^8.0.2",
29
29
  "typescript": "^5.4.5",
package/src/bin.ts CHANGED
@@ -128,7 +128,10 @@ export function createCli(_argv: readonly string[] = process.argv): CAC {
128
128
  cli
129
129
  .command('onboard', 'Run assistant-oriented Inspecto onboarding in one structured flow')
130
130
  .option('--json', 'Print machine-readable JSON output', { default: false })
131
- .option('--target <packagePath>', 'Select a monorepo target package explicitly')
131
+ .option(
132
+ '--target <candidateIdOrPath>',
133
+ 'Select the build target to onboard using a returned candidateId or compatible config path',
134
+ )
132
135
  .option('--yes', 'Accept a lightweight confirmation gate automatically', { default: false })
133
136
  .option('--shared', 'Write shared Inspecto settings instead of local-only settings')
134
137
  .option('--skip-install', 'Skip npm dependency installation')
@@ -282,6 +282,33 @@ export async function collectDoctorResult(root = process.cwd()): Promise<DoctorR
282
282
  const settings = await readJSON(targetPath)
283
283
  if (settings) {
284
284
  checks.push(createDiagnostic('settings-valid', 'ok', `.inspecto/${fileName} valid`))
285
+
286
+ const configuredIde =
287
+ typeof (settings as Record<string, unknown>).ide === 'string'
288
+ ? ((settings as Record<string, unknown>).ide as string)
289
+ : undefined
290
+ const detectedIdeCandidates = ideProbe.detected.map(item => item.ide)
291
+ if (
292
+ configuredIde &&
293
+ detectedIdeCandidates.length > 0 &&
294
+ !detectedIdeCandidates.includes(configuredIde)
295
+ ) {
296
+ checks.push(
297
+ createDiagnostic(
298
+ 'settings-ide-mismatch',
299
+ 'warning',
300
+ `.inspecto/${fileName} sets ide=${configuredIde}, but the current environment looks like ${detectedIdeCandidates.join(', ')}. Inspecto will use the configured IDE from ${fileName}.`,
301
+ [
302
+ `Update .inspecto/${fileName} if you want Inspecto to target the currently detected IDE instead.`,
303
+ ],
304
+ {
305
+ configuredIde,
306
+ detectedIdeCandidates,
307
+ precedence: `configured ide from ${fileName}`,
308
+ },
309
+ ),
310
+ )
311
+ }
285
312
  } else {
286
313
  checks.push(
287
314
  createDiagnostic(
@@ -2,10 +2,19 @@ import fs from 'node:fs/promises'
2
2
  import { homedir } from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
- import { exists, writeFile } from '../utils/fs.js'
5
+ import { exists, readJSON, writeFile, writeJSON } from '../utils/fs.js'
6
6
  import { log } from '../utils/logger.js'
7
7
  import { writeCommandOutput } from '../utils/output.js'
8
8
  import { runIntegrationAutomation } from './integration-automation.js'
9
+ import { resolveIntegrationDispatchMode } from './integration-dispatch-mode.js'
10
+ import { resolveIntegrationHostIde } from './integration-host-ide.js'
11
+ import { isSupportedHostIde, type SupportedHostIde } from '../integrations/capabilities.js'
12
+ import {
13
+ DEFAULT_PROVIDER_MODE,
14
+ VALID_MODES,
15
+ type Provider,
16
+ type ProviderMode,
17
+ } from '@inspecto-dev/types'
9
18
 
10
19
  const REPO_RAW_BASE = 'https://raw.githubusercontent.com/inspecto-dev/inspecto/main'
11
20
  const TOTAL_STEPS = 6
@@ -55,6 +64,12 @@ interface InstallPlan {
55
64
  nextStep: string
56
65
  }
57
66
 
67
+ interface InspectoSettingsShape {
68
+ ide?: string
69
+ 'provider.default'?: string
70
+ [key: string]: unknown
71
+ }
72
+
58
73
  export interface IntegrationInstallResult {
59
74
  status: 'launched' | 'partial' | 'blocked' | 'preview' | 'preview_blocked'
60
75
  assistant: string
@@ -117,7 +132,7 @@ const INTEGRATION_MANIFESTS: IntegrationManifest[] = [
117
132
  {
118
133
  assistant: 'coco',
119
134
  type: 'native-skill',
120
- installTarget: '.traecli/skills/inspecto-onboarding/',
135
+ installTarget: '.trae/skills/inspecto-onboarding/',
121
136
  preferredInstall:
122
137
  'npx @inspecto-dev/cli integrations install coco --host-ide <vscode|cursor|trae|trae-cn>',
123
138
  cliSupported: true,
@@ -194,6 +209,10 @@ export async function installIntegration(
194
209
  }
195
210
  }
196
211
 
212
+ if (shouldPersistProjectOnboardingDefaults(options)) {
213
+ await persistProjectOnboardingDefaults(assistant as AssistantId, options)
214
+ }
215
+
197
216
  const stepOneMessage = options.preview
198
217
  ? formatIntegrationStep(1, `Previewing ${getAssistantLabel(assistant)} integration assets`)
199
218
  : formatIntegrationStep(1, `Installed ${getAssistantLabel(assistant)} integration assets`)
@@ -292,6 +311,70 @@ export async function installIntegration(
292
311
  return result
293
312
  }
294
313
 
314
+ function shouldPersistProjectOnboardingDefaults(
315
+ options: InstallIntegrationOptions,
316
+ ): options is InstallIntegrationOptions & { ide: SupportedHostIde } {
317
+ return (
318
+ !options.preview && !shouldSkipAutomationForInstall(options) && isSupportedHostIde(options.ide)
319
+ )
320
+ }
321
+
322
+ function isProviderAssistant(value: string): value is Provider {
323
+ return Object.prototype.hasOwnProperty.call(VALID_MODES, value)
324
+ }
325
+
326
+ async function resolveProviderDefaultForAssistant(
327
+ assistant: AssistantId,
328
+ ide: SupportedHostIde,
329
+ ): Promise<string | undefined> {
330
+ if (!isProviderAssistant(assistant)) {
331
+ return undefined
332
+ }
333
+
334
+ let mode: ProviderMode | undefined
335
+ if (assistant === 'codex' || assistant === 'claude-code' || assistant === 'gemini') {
336
+ const dispatchMode = await resolveIntegrationDispatchMode({ assistant, hostIde: ide })
337
+ mode = dispatchMode.mode ?? DEFAULT_PROVIDER_MODE[assistant]
338
+ } else {
339
+ mode = DEFAULT_PROVIDER_MODE[assistant]
340
+ }
341
+
342
+ if (!mode || !VALID_MODES[assistant].includes(mode)) {
343
+ return undefined
344
+ }
345
+
346
+ return `${assistant}.${mode}`
347
+ }
348
+
349
+ async function persistProjectOnboardingDefaults(
350
+ assistant: AssistantId,
351
+ options: InstallIntegrationOptions & { ide: SupportedHostIde },
352
+ ): Promise<void> {
353
+ const settingsPath = path.join(process.cwd(), '.inspecto', 'settings.local.json')
354
+ const existingSettings = await readJSON<InspectoSettingsShape>(settingsPath)
355
+ const resolvedHostIde = await resolveIntegrationHostIde({
356
+ explicitIde: options.ide,
357
+ cwd: process.cwd(),
358
+ })
359
+ const providerDefault =
360
+ resolvedHostIde.ide && resolvedHostIde.confidence !== 'low'
361
+ ? await resolveProviderDefaultForAssistant(assistant, resolvedHostIde.ide)
362
+ : undefined
363
+ const mergedSettings =
364
+ existingSettings && typeof existingSettings === 'object'
365
+ ? {
366
+ ...existingSettings,
367
+ ide: options.ide,
368
+ ...(providerDefault ? { 'provider.default': providerDefault } : {}),
369
+ }
370
+ : {
371
+ ide: options.ide,
372
+ ...(providerDefault ? { 'provider.default': providerDefault } : {}),
373
+ }
374
+
375
+ await writeJSON(settingsPath, mergedSettings)
376
+ }
377
+
295
378
  function shouldSkipAutomationForInstall(options: InstallIntegrationOptions): boolean {
296
379
  return options.scope === 'user' && !options.preview
297
380
  }
@@ -375,6 +458,12 @@ function resolveInstallPlan(assistant: string, options: InstallIntegrationOption
375
458
  target: '.trae/skills/inspecto-onboarding/SKILL.md',
376
459
  localSource: 'skills/inspecto-onboarding-trae/SKILL.md',
377
460
  },
461
+ {
462
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-trae/scripts/run-inspecto.sh`,
463
+ target: '.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
464
+ localSource: 'skills/inspecto-onboarding-trae/scripts/run-inspecto.sh',
465
+ executable: true,
466
+ },
378
467
  ],
379
468
  successMessage: 'Installed Trae skill to .trae/skills/inspecto-onboarding/SKILL.md',
380
469
  nextStep: 'Open a new Trae chat and verify the inspecto-onboarding skill is available.',
@@ -384,11 +473,17 @@ function resolveInstallPlan(assistant: string, options: InstallIntegrationOption
384
473
  assets: [
385
474
  {
386
475
  source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-trae/SKILL.md`,
387
- target: '.traecli/skills/inspecto-onboarding/SKILL.md',
476
+ target: '.trae/skills/inspecto-onboarding/SKILL.md',
388
477
  localSource: 'skills/inspecto-onboarding-trae/SKILL.md',
389
478
  },
479
+ {
480
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-trae/scripts/run-inspecto.sh`,
481
+ target: '.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
482
+ localSource: 'skills/inspecto-onboarding-trae/scripts/run-inspecto.sh',
483
+ executable: true,
484
+ },
390
485
  ],
391
- successMessage: 'Installed Coco skill to .traecli/skills/inspecto-onboarding/SKILL.md',
486
+ successMessage: 'Installed Coco skill to .trae/skills/inspecto-onboarding/SKILL.md',
392
487
  nextStep: 'Start a new Coco session.',
393
488
  }
394
489
  default:
@@ -39,6 +39,20 @@ function printOnboardResult(result: OnboardCommandResult): void {
39
39
  log.info(`Status: ${result.status}`)
40
40
  log.info(result.summary.headline)
41
41
 
42
+ if (result.status === 'needs_target_selection') {
43
+ if (result.target.selectionPurpose) {
44
+ log.warn(result.target.selectionPurpose)
45
+ }
46
+ for (const candidate of result.target.candidates) {
47
+ const identifier = candidate.candidateId ?? candidate.id ?? candidate.configPath
48
+ const label = candidate.label ?? candidate.configPath
49
+ log.hint(`${identifier}: ${label}`)
50
+ }
51
+ if (result.target.selectionInstructions) {
52
+ log.hint(result.target.selectionInstructions)
53
+ }
54
+ }
55
+
42
56
  for (const change of result.summary.changes) {
43
57
  log.hint(change)
44
58
  }
@@ -50,6 +50,37 @@ async function getResolvedPackageVersion(pkgName: string, root: string): Promise
50
50
  }
51
51
  }
52
52
 
53
+ function parseFirstSemver(
54
+ version: string | null,
55
+ ): { major: number; minor: number; patch: number } | null {
56
+ if (!version) return null
57
+
58
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/)
59
+ if (!match) {
60
+ return null
61
+ }
62
+
63
+ return {
64
+ major: Number(match[1]),
65
+ minor: Number(match[2]),
66
+ patch: Number(match[3]),
67
+ }
68
+ }
69
+
70
+ function isLegacyRspackVersion(version: string | null): boolean {
71
+ const parsed = parseFirstSemver(version)
72
+ if (!parsed) return false
73
+
74
+ return parsed.major === 0 && parsed.minor < 4
75
+ }
76
+
77
+ function isLegacyWebpackVersion(version: string | null): boolean {
78
+ const parsed = parseFirstSemver(version)
79
+ if (!parsed) return false
80
+
81
+ return parsed.major === 4
82
+ }
83
+
53
84
  /** Supported build tools in v1 */
54
85
  const SUPPORTED_PATTERNS: { tool: BuildTool; files: string[]; label: string }[] = [
55
86
  {
@@ -76,7 +107,22 @@ const SUPPORTED_PATTERNS: { tool: BuildTool; files: string[]; label: string }[]
76
107
  },
77
108
  {
78
109
  tool: 'webpack',
79
- files: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs', 'webpack.config.cjs'],
110
+ files: [
111
+ 'webpack.config.js',
112
+ 'webpack.config.ts',
113
+ 'webpack.config.mjs',
114
+ 'webpack.config.cjs',
115
+ 'webpack.config.common.js',
116
+ 'webpack.config.common.ts',
117
+ 'webpack.config.dev.js',
118
+ 'webpack.config.dev.ts',
119
+ 'webpack.config.prod.js',
120
+ 'webpack.config.prod.ts',
121
+ 'webpack.config.esbuild.js',
122
+ 'webpack.config.esbuild.ts',
123
+ 'webpack.config.build-pre.js',
124
+ 'webpack.config.build-pre.ts',
125
+ ],
80
126
  label: 'Webpack',
81
127
  },
82
128
  {
@@ -280,6 +326,119 @@ interface PatternContext {
280
326
  scripts: Record<string, string>
281
327
  }
282
328
 
329
+ function rankScriptCommand(name: string, command: string): number {
330
+ const haystack = `${name} ${command}`.toLowerCase()
331
+ let score = 0
332
+
333
+ if (/(^|[\s:_-])(start|dev|serve|watch)([\s:_-]|$)/.test(haystack)) score += 8
334
+ if (/(^|[\s:_-])(prod|build|release|stats)([\s:_-]|$)/.test(haystack)) score -= 3
335
+ if (/(^|[\s:_-])(dll|vendor)([\s:_-]|$)/.test(haystack)) score -= 6
336
+ if (haystack.includes('webpack-dev-server')) score += 3
337
+ if (haystack.includes('webpack')) score += 1
338
+ if (haystack.includes('rspack')) score += 1
339
+
340
+ return score
341
+ }
342
+
343
+ function extractConfigArgs(scriptContent: string): string[] {
344
+ return Array.from(scriptContent.matchAll(/(?:-c|--config)\s+([^\s'"`;]+)/g))
345
+ .map(match => match[1])
346
+ .filter((value): value is string => Boolean(value))
347
+ }
348
+
349
+ async function resolveScriptRelativeCandidate(
350
+ targetRoot: string,
351
+ scriptPath: string,
352
+ candidate: string,
353
+ ): Promise<string | null> {
354
+ const normalizedCandidate = candidate.replace(/^['"`]|['"`]$/g, '')
355
+ const normalizedRelativeCandidate = path.normalize(normalizedCandidate)
356
+ const possiblePaths: string[] = []
357
+
358
+ if (normalizedRelativeCandidate.startsWith('..')) {
359
+ possiblePaths.push(
360
+ path.normalize(path.join(path.dirname(scriptPath), normalizedRelativeCandidate)),
361
+ )
362
+ } else {
363
+ possiblePaths.push(normalizedRelativeCandidate)
364
+ possiblePaths.push(
365
+ path.normalize(path.join(path.dirname(scriptPath), normalizedRelativeCandidate)),
366
+ )
367
+ }
368
+
369
+ for (const possiblePath of possiblePaths) {
370
+ if (await exists(path.join(targetRoot, possiblePath))) {
371
+ return possiblePath.split(path.sep).join('/')
372
+ }
373
+ }
374
+
375
+ return null
376
+ }
377
+
378
+ async function resolveRspackConfigFromScript(
379
+ targetRoot: string,
380
+ scriptPath: string,
381
+ ): Promise<string | null> {
382
+ const scriptContent = await readFile(path.join(targetRoot, scriptPath))
383
+ if (!scriptContent) {
384
+ return null
385
+ }
386
+
387
+ for (const candidate of extractConfigArgs(scriptContent)) {
388
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, scriptPath, candidate)
389
+ if (resolved) {
390
+ return resolved
391
+ }
392
+ }
393
+
394
+ const matches = scriptContent.matchAll(
395
+ /['"`]([^'"`\n]*rspack[^'"`\n]*config[^'"`\n]*\.(?:js|ts|mjs|cjs))['"`]/g,
396
+ )
397
+
398
+ for (const match of matches) {
399
+ const candidate = match[1]
400
+ if (!candidate) continue
401
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, scriptPath, candidate)
402
+ if (resolved) {
403
+ return resolved
404
+ }
405
+ }
406
+
407
+ return null
408
+ }
409
+
410
+ async function resolveWebpackBaseConfigFromFile(
411
+ targetRoot: string,
412
+ configPath: string,
413
+ ): Promise<string | null> {
414
+ const configContent = await readFile(path.join(targetRoot, configPath))
415
+ if (!configContent) {
416
+ return null
417
+ }
418
+
419
+ for (const candidate of extractConfigArgs(configContent)) {
420
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, configPath, candidate)
421
+ if (resolved) {
422
+ return resolved
423
+ }
424
+ }
425
+
426
+ const matches = configContent.matchAll(
427
+ /(?:configPath\s*=\s*|require\()\s*['"`]([^'"`\n]*webpack[^'"`\n]*config[^'"`\n]*\.(?:js|ts|mjs|cjs))['"`]/g,
428
+ )
429
+
430
+ for (const match of matches) {
431
+ const candidate = match[1]
432
+ if (!candidate) continue
433
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, configPath, candidate)
434
+ if (resolved) {
435
+ return resolved
436
+ }
437
+ }
438
+
439
+ return null
440
+ }
441
+
283
442
  async function detectPattern({
284
443
  pattern,
285
444
  workspaceRoot,
@@ -339,13 +498,47 @@ async function detectPattern({
339
498
  pattern.tool === 'rspack' ||
340
499
  pattern.tool === 'rsbuild')
341
500
  ) {
342
- for (const cmd of Object.values(scripts)) {
501
+ const rankedScripts = Object.entries(scripts).sort(
502
+ ([leftName, leftCommand], [rightName, rightCommand]) =>
503
+ rankScriptCommand(rightName, rightCommand) - rankScriptCommand(leftName, leftCommand),
504
+ )
505
+
506
+ for (const [, cmd] of rankedScripts) {
507
+ if (pattern.tool === 'webpack' || pattern.tool === 'rspack') {
508
+ for (const configArg of extractConfigArgs(cmd)) {
509
+ const resolvedConfig = await resolveScriptRelativeCandidate(targetRoot, '', configArg)
510
+ if (resolvedConfig && (cmd.includes(pattern.tool) || cmd.includes(`${pattern.tool}-`))) {
511
+ if (pattern.tool === 'webpack') {
512
+ detectedFile =
513
+ (await resolveWebpackBaseConfigFromFile(targetRoot, resolvedConfig)) ??
514
+ resolvedConfig
515
+ } else {
516
+ detectedFile =
517
+ (await resolveRspackConfigFromScript(targetRoot, resolvedConfig)) ?? resolvedConfig
518
+ }
519
+ break
520
+ }
521
+ }
522
+
523
+ if (detectedFile) {
524
+ break
525
+ }
526
+ }
527
+
343
528
  if (cmd.includes('node ')) {
344
529
  const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/)
345
530
  if (match && match[1]) {
346
531
  if (await exists(path.join(targetRoot, match[1]))) {
347
532
  if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
348
- detectedFile = match[1]
533
+ if (pattern.tool === 'rspack') {
534
+ detectedFile =
535
+ (await resolveRspackConfigFromScript(targetRoot, match[1])) ?? match[1]
536
+ } else if (pattern.tool === 'webpack') {
537
+ detectedFile =
538
+ (await resolveWebpackBaseConfigFromFile(targetRoot, match[1])) ?? match[1]
539
+ } else {
540
+ detectedFile = match[1]
541
+ }
349
542
  break
350
543
  }
351
544
  }
@@ -393,16 +586,11 @@ async function detectPattern({
393
586
  let isLegacyWebpack = false
394
587
 
395
588
  if (pattern.tool === 'rspack') {
396
- const version = resolvedVersion
397
- if (
398
- version &&
399
- (version.includes('0.3.') || version.includes('0.2.') || version.includes('0.1.'))
400
- ) {
589
+ if (isLegacyRspackVersion(resolvedVersion)) {
401
590
  isLegacyRspack = true
402
591
  }
403
592
  } else if (pattern.tool === 'webpack') {
404
- const version = resolvedVersion
405
- if ((version && version.includes('^4')) || version?.startsWith('4.')) {
593
+ if (isLegacyWebpackVersion(resolvedVersion)) {
406
594
  isLegacyWebpack = true
407
595
  }
408
596
  }