@inspecto-dev/cli 0.3.3 → 0.3.5

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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-test.log +10594 -4044
  3. package/CHANGELOG.md +28 -0
  4. package/dist/bin.js +36 -2
  5. package/dist/{chunk-LJOKPCPD.js → chunk-7ABJRH3F.js} +1701 -182
  6. package/dist/index.d.ts +69 -4
  7. package/dist/index.js +7 -1
  8. package/package.json +3 -3
  9. package/src/bin.ts +49 -1
  10. package/src/commands/dev-config.ts +109 -0
  11. package/src/commands/doctor.ts +189 -9
  12. package/src/commands/init.ts +10 -3
  13. package/src/commands/integration-automation.ts +2 -0
  14. package/src/commands/integration-host-ide.ts +18 -15
  15. package/src/commands/integration-install.ts +100 -5
  16. package/src/commands/onboard.ts +80 -15
  17. package/src/detect/build-tool.ts +212 -15
  18. package/src/detect/framework.ts +3 -0
  19. package/src/detect/package-manager.ts +1 -1
  20. package/src/index.ts +1 -0
  21. package/src/inject/gitignore.ts +13 -2
  22. package/src/instructions.ts +33 -7
  23. package/src/onboarding/apply.ts +255 -28
  24. package/src/onboarding/nextjs-guidance.ts +257 -0
  25. package/src/onboarding/nuxt-guidance.ts +129 -0
  26. package/src/onboarding/planner.ts +337 -10
  27. package/src/onboarding/session.ts +127 -31
  28. package/src/onboarding/target-resolution.ts +79 -3
  29. package/src/onboarding/umi-guidance.ts +139 -0
  30. package/src/types.ts +58 -3
  31. package/tests/apply.test.ts +553 -0
  32. package/tests/build-tool.test.ts +199 -0
  33. package/tests/dev-config.test.ts +73 -0
  34. package/tests/doctor.test.ts +130 -0
  35. package/tests/init.test.ts +17 -0
  36. package/tests/install-wrapper.test.ts +56 -0
  37. package/tests/instructions.test.ts +10 -6
  38. package/tests/integration-host-ide.test.ts +20 -0
  39. package/tests/integration-install.test.ts +193 -0
  40. package/tests/nextjs-guidance.test.ts +128 -0
  41. package/tests/nuxt-guidance.test.ts +67 -0
  42. package/tests/onboard.test.ts +511 -0
  43. package/tests/plan.test.ts +283 -21
  44. package/tests/runner-script.test.ts +120 -1
  45. package/tests/session-resolve.test.ts +116 -0
  46. package/tests/session.test.ts +120 -0
@@ -12,6 +12,7 @@ export type HostIdeSource = 'explicit' | 'config' | 'env' | 'artifact' | 'ambigu
12
12
  export interface ResolveIntegrationHostIdeOptions {
13
13
  explicitIde?: string
14
14
  cwd?: string
15
+ ignoreProjectArtifacts?: boolean
15
16
  }
16
17
 
17
18
  export interface ResolvedIntegrationHostIde {
@@ -51,7 +52,7 @@ export async function resolveIntegrationHostIde(
51
52
  const envCandidates = detectEnvHostIdes()
52
53
  if (envCandidates.length === 1) {
53
54
  return {
54
- ide: envCandidates[0],
55
+ ide: envCandidates[0]!,
55
56
  confidence: 'high',
56
57
  source: 'env',
57
58
  candidates: envCandidates,
@@ -67,22 +68,24 @@ export async function resolveIntegrationHostIde(
67
68
  }
68
69
  }
69
70
 
70
- const artifactCandidates = await detectArtifactHostIdes(cwd)
71
- if (artifactCandidates.length === 1) {
72
- return {
73
- ide: artifactCandidates[0],
74
- confidence: 'medium',
75
- source: 'artifact',
76
- candidates: artifactCandidates,
71
+ if (!options.ignoreProjectArtifacts) {
72
+ const artifactCandidates = await detectArtifactHostIdes(cwd)
73
+ if (artifactCandidates.length === 1) {
74
+ return {
75
+ ide: artifactCandidates[0]!,
76
+ confidence: 'medium',
77
+ source: 'artifact',
78
+ candidates: artifactCandidates,
79
+ }
77
80
  }
78
- }
79
81
 
80
- if (artifactCandidates.length > 1) {
81
- return {
82
- ide: null,
83
- confidence: 'low',
84
- source: 'ambiguous',
85
- candidates: artifactCandidates,
82
+ if (artifactCandidates.length > 1) {
83
+ return {
84
+ ide: null,
85
+ confidence: 'low',
86
+ source: 'ambiguous',
87
+ candidates: artifactCandidates,
88
+ }
86
89
  }
87
90
  }
88
91
 
@@ -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`)
@@ -247,7 +266,7 @@ export async function installIntegration(
247
266
 
248
267
  const automationResult = await runIntegrationAutomation(
249
268
  assistant,
250
- { ...options, silent },
269
+ { ...options, silent, ignoreProjectArtifacts: true },
251
270
  process.cwd(),
252
271
  )
253
272
  const result: IntegrationInstallResult = {
@@ -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:
@@ -5,7 +5,7 @@ import {
5
5
  } from '../onboarding/session.js'
6
6
  import { log } from '../utils/logger.js'
7
7
  import { writeCommandOutput } from '../utils/output.js'
8
- import type { OnboardCommandResult } from '../types.js'
8
+ import type { OnboardCommandResult, OnboardingAssistantHandoff } from '../types.js'
9
9
 
10
10
  export interface OnboardCommandOptions {
11
11
  json?: boolean
@@ -34,33 +34,98 @@ function printManualExtensionGuidance(result: OnboardCommandResult): void {
34
34
  }
35
35
  }
36
36
 
37
+ function buildAssistantHandoff(
38
+ result: OnboardCommandResult,
39
+ ): OnboardingAssistantHandoff | undefined {
40
+ if (
41
+ !result.framework &&
42
+ !result.metaFramework &&
43
+ !result.routerMode &&
44
+ !result.autoApplied &&
45
+ !result.pendingSteps &&
46
+ !result.assistantPrompt &&
47
+ !result.patches
48
+ ) {
49
+ return result.handoff
50
+ }
51
+
52
+ return {
53
+ ...(result.framework ? { framework: result.framework } : {}),
54
+ ...(result.metaFramework ? { metaFramework: result.metaFramework } : {}),
55
+ ...(result.routerMode ? { routerMode: result.routerMode } : {}),
56
+ ...(result.autoApplied ? { autoApplied: result.autoApplied } : {}),
57
+ ...(result.pendingSteps ? { pendingSteps: result.pendingSteps } : {}),
58
+ ...(result.assistantPrompt ? { assistantPrompt: result.assistantPrompt } : {}),
59
+ ...(result.patches ? { patches: result.patches } : {}),
60
+ }
61
+ }
62
+
63
+ function normalizeOnboardResult(result: OnboardCommandResult): OnboardCommandResult {
64
+ const handoff = buildAssistantHandoff(result)
65
+ if (!handoff) {
66
+ return result
67
+ }
68
+
69
+ return {
70
+ ...result,
71
+ handoff,
72
+ }
73
+ }
74
+
75
+ function collectDisplayNextSteps(result: OnboardCommandResult): string[] {
76
+ return Array.from(
77
+ new Set([...(result.diagnostics?.nextSteps ?? []), ...(result.handoff?.pendingSteps ?? [])]),
78
+ )
79
+ }
80
+
37
81
  function printOnboardResult(result: OnboardCommandResult): void {
82
+ const normalized = normalizeOnboardResult(result)
38
83
  log.header('Inspecto Onboard')
39
- log.info(`Status: ${result.status}`)
40
- log.info(result.summary.headline)
84
+ log.info(`Status: ${normalized.status}`)
85
+ log.info(normalized.summary.headline)
41
86
 
42
- for (const change of result.summary.changes) {
87
+ if (normalized.status === 'needs_target_selection') {
88
+ if (normalized.target.selectionPurpose) {
89
+ log.warn(normalized.target.selectionPurpose)
90
+ }
91
+ for (const candidate of normalized.target.candidates) {
92
+ const identifier = candidate.candidateId ?? candidate.id ?? candidate.configPath
93
+ const label = candidate.label ?? candidate.configPath
94
+ log.hint(`${identifier}: ${label}`)
95
+ }
96
+ if (normalized.target.selectionInstructions) {
97
+ log.hint(normalized.target.selectionInstructions)
98
+ }
99
+ }
100
+
101
+ for (const change of normalized.summary.changes) {
43
102
  log.hint(change)
44
103
  }
45
- for (const step of result.diagnostics?.nextSteps ?? []) {
104
+ for (const step of collectDisplayNextSteps(normalized)) {
46
105
  log.warn(step)
47
106
  }
48
- if (result.confirmation.required && result.confirmation.question) {
49
- log.warn(result.confirmation.question)
107
+ for (const patch of normalized.handoff?.patches ?? []) {
108
+ log.hint(`Patch target: ${patch.path} (${patch.reason})`)
109
+ }
110
+ if (normalized.handoff?.assistantPrompt) {
111
+ log.hint(normalized.handoff.assistantPrompt)
112
+ }
113
+ if (normalized.confirmation.required && normalized.confirmation.question) {
114
+ log.warn(normalized.confirmation.question)
50
115
  }
51
116
 
52
- printManualExtensionGuidance(result)
117
+ printManualExtensionGuidance(normalized)
53
118
 
54
119
  const extensionReady =
55
- !result.ideExtension?.required ||
56
- (result.ideExtension.installed && !result.ideExtension.manualRequired)
120
+ !normalized.ideExtension?.required ||
121
+ (normalized.ideExtension.installed && !normalized.ideExtension.manualRequired)
57
122
 
58
123
  if (
59
124
  extensionReady &&
60
- (result.status === 'success' || result.status === 'partial_success') &&
61
- result.verification?.message
125
+ (normalized.status === 'success' || normalized.status === 'partial_success') &&
126
+ normalized.verification?.message
62
127
  ) {
63
- log.info(result.verification.message)
128
+ log.info(normalized.verification.message)
64
129
  }
65
130
  }
66
131
 
@@ -74,12 +139,12 @@ export async function onboard(options: OnboardCommandOptions = {}): Promise<Onbo
74
139
  session.status === 'needs_confirmation'
75
140
  ) {
76
141
  return writeCommandOutput(
77
- buildDeferredOnboardResult(session),
142
+ normalizeOnboardResult(buildDeferredOnboardResult(session)),
78
143
  options.json ?? false,
79
144
  printOnboardResult,
80
145
  )
81
146
  }
82
147
 
83
- const result = await applyResolvedOnboardingSession(session, options)
148
+ const result = normalizeOnboardResult(await applyResolvedOnboardingSession(session, options))
84
149
  return writeCommandOutput(result, options.json ?? false, printOnboardResult)
85
150
  }
@@ -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
  {
@@ -93,6 +139,11 @@ const SUPPORTED_PATTERNS: { tool: BuildTool; files: string[]; label: string }[]
93
139
 
94
140
  /** Recognized but unsupported meta-frameworks — detect via dep + config file */
95
141
  const UNSUPPORTED_META: { name: string; dep: string; files: string[] }[] = [
142
+ {
143
+ name: 'Umi',
144
+ dep: 'umi',
145
+ files: ['.umirc.ts', '.umirc.js', 'config/config.ts', 'config/config.js'],
146
+ },
96
147
  { name: 'Next.js', dep: 'next', files: ['next.config.mjs', 'next.config.js', 'next.config.ts'] },
97
148
  { name: 'Nuxt', dep: 'nuxt', files: ['nuxt.config.ts', 'nuxt.config.js'] },
98
149
  { name: 'Remix', dep: '@remix-run/dev', files: ['remix.config.js', 'remix.config.ts'] },
@@ -251,7 +302,9 @@ export async function detectBuildTools(
251
302
  }
252
303
 
253
304
  const unsupportedChecks = UNSUPPORTED_META.map(async meta => {
254
- if (!(meta.dep in allDeps)) return null
305
+ // Check if any dependency matches (supports exact match or scoped packages if necessary, but here we do exact match)
306
+ const hasDep = meta.dep in allDeps || Object.keys(allDeps).some(dep => dep.includes(meta.dep))
307
+ if (!hasDep) return null
255
308
  for (const file of meta.files) {
256
309
  if (await exists(path.join(target.absolutePath, file))) {
257
310
  return meta.name
@@ -280,6 +333,119 @@ interface PatternContext {
280
333
  scripts: Record<string, string>
281
334
  }
282
335
 
336
+ function rankScriptCommand(name: string, command: string): number {
337
+ const haystack = `${name} ${command}`.toLowerCase()
338
+ let score = 0
339
+
340
+ if (/(^|[\s:_-])(start|dev|serve|watch)([\s:_-]|$)/.test(haystack)) score += 8
341
+ if (/(^|[\s:_-])(prod|build|release|stats)([\s:_-]|$)/.test(haystack)) score -= 3
342
+ if (/(^|[\s:_-])(dll|vendor)([\s:_-]|$)/.test(haystack)) score -= 6
343
+ if (haystack.includes('webpack-dev-server')) score += 3
344
+ if (haystack.includes('webpack')) score += 1
345
+ if (haystack.includes('rspack')) score += 1
346
+
347
+ return score
348
+ }
349
+
350
+ function extractConfigArgs(scriptContent: string): string[] {
351
+ return Array.from(scriptContent.matchAll(/(?:-c|--config)\s+([^\s'"`;]+)/g))
352
+ .map(match => match[1])
353
+ .filter((value): value is string => Boolean(value))
354
+ }
355
+
356
+ async function resolveScriptRelativeCandidate(
357
+ targetRoot: string,
358
+ scriptPath: string,
359
+ candidate: string,
360
+ ): Promise<string | null> {
361
+ const normalizedCandidate = candidate.replace(/^['"`]|['"`]$/g, '')
362
+ const normalizedRelativeCandidate = path.normalize(normalizedCandidate)
363
+ const possiblePaths: string[] = []
364
+
365
+ if (normalizedRelativeCandidate.startsWith('..')) {
366
+ possiblePaths.push(
367
+ path.normalize(path.join(path.dirname(scriptPath), normalizedRelativeCandidate)),
368
+ )
369
+ } else {
370
+ possiblePaths.push(normalizedRelativeCandidate)
371
+ possiblePaths.push(
372
+ path.normalize(path.join(path.dirname(scriptPath), normalizedRelativeCandidate)),
373
+ )
374
+ }
375
+
376
+ for (const possiblePath of possiblePaths) {
377
+ if (await exists(path.join(targetRoot, possiblePath))) {
378
+ return possiblePath.split(path.sep).join('/')
379
+ }
380
+ }
381
+
382
+ return null
383
+ }
384
+
385
+ async function resolveRspackConfigFromScript(
386
+ targetRoot: string,
387
+ scriptPath: string,
388
+ ): Promise<string | null> {
389
+ const scriptContent = await readFile(path.join(targetRoot, scriptPath))
390
+ if (!scriptContent) {
391
+ return null
392
+ }
393
+
394
+ for (const candidate of extractConfigArgs(scriptContent)) {
395
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, scriptPath, candidate)
396
+ if (resolved) {
397
+ return resolved
398
+ }
399
+ }
400
+
401
+ const matches = scriptContent.matchAll(
402
+ /['"`]([^'"`\n]*rspack[^'"`\n]*config[^'"`\n]*\.(?:js|ts|mjs|cjs))['"`]/g,
403
+ )
404
+
405
+ for (const match of matches) {
406
+ const candidate = match[1]
407
+ if (!candidate) continue
408
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, scriptPath, candidate)
409
+ if (resolved) {
410
+ return resolved
411
+ }
412
+ }
413
+
414
+ return null
415
+ }
416
+
417
+ async function resolveWebpackBaseConfigFromFile(
418
+ targetRoot: string,
419
+ configPath: string,
420
+ ): Promise<string | null> {
421
+ const configContent = await readFile(path.join(targetRoot, configPath))
422
+ if (!configContent) {
423
+ return null
424
+ }
425
+
426
+ for (const candidate of extractConfigArgs(configContent)) {
427
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, configPath, candidate)
428
+ if (resolved) {
429
+ return resolved
430
+ }
431
+ }
432
+
433
+ const matches = configContent.matchAll(
434
+ /(?:configPath\s*=\s*|require\()\s*['"`]([^'"`\n]*webpack[^'"`\n]*config[^'"`\n]*\.(?:js|ts|mjs|cjs))['"`]/g,
435
+ )
436
+
437
+ for (const match of matches) {
438
+ const candidate = match[1]
439
+ if (!candidate) continue
440
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, configPath, candidate)
441
+ if (resolved) {
442
+ return resolved
443
+ }
444
+ }
445
+
446
+ return null
447
+ }
448
+
283
449
  async function detectPattern({
284
450
  pattern,
285
451
  workspaceRoot,
@@ -312,6 +478,8 @@ async function detectPattern({
312
478
  }
313
479
  } else if (pattern.tool === 'rsbuild') {
314
480
  hasDep = !!allDeps['@rsbuild/core'] || isPackageResolvable('@rsbuild/core', targetRoot)
481
+ } else if (pattern.tool === 'vite') {
482
+ hasDep = !!allDeps['vite'] || isPackageResolvable('vite', targetRoot)
315
483
  } else {
316
484
  hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, targetRoot)
317
485
  }
@@ -339,13 +507,47 @@ async function detectPattern({
339
507
  pattern.tool === 'rspack' ||
340
508
  pattern.tool === 'rsbuild')
341
509
  ) {
342
- for (const cmd of Object.values(scripts)) {
510
+ const rankedScripts = Object.entries(scripts).sort(
511
+ ([leftName, leftCommand], [rightName, rightCommand]) =>
512
+ rankScriptCommand(rightName, rightCommand) - rankScriptCommand(leftName, leftCommand),
513
+ )
514
+
515
+ for (const [, cmd] of rankedScripts) {
516
+ if (pattern.tool === 'webpack' || pattern.tool === 'rspack') {
517
+ for (const configArg of extractConfigArgs(cmd)) {
518
+ const resolvedConfig = await resolveScriptRelativeCandidate(targetRoot, '', configArg)
519
+ if (resolvedConfig && (cmd.includes(pattern.tool) || cmd.includes(`${pattern.tool}-`))) {
520
+ if (pattern.tool === 'webpack') {
521
+ detectedFile =
522
+ (await resolveWebpackBaseConfigFromFile(targetRoot, resolvedConfig)) ??
523
+ resolvedConfig
524
+ } else {
525
+ detectedFile =
526
+ (await resolveRspackConfigFromScript(targetRoot, resolvedConfig)) ?? resolvedConfig
527
+ }
528
+ break
529
+ }
530
+ }
531
+
532
+ if (detectedFile) {
533
+ break
534
+ }
535
+ }
536
+
343
537
  if (cmd.includes('node ')) {
344
538
  const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/)
345
539
  if (match && match[1]) {
346
540
  if (await exists(path.join(targetRoot, match[1]))) {
347
541
  if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
348
- detectedFile = match[1]
542
+ if (pattern.tool === 'rspack') {
543
+ detectedFile =
544
+ (await resolveRspackConfigFromScript(targetRoot, match[1])) ?? match[1]
545
+ } else if (pattern.tool === 'webpack') {
546
+ detectedFile =
547
+ (await resolveWebpackBaseConfigFromFile(targetRoot, match[1])) ?? match[1]
548
+ } else {
549
+ detectedFile = match[1]
550
+ }
349
551
  break
350
552
  }
351
553
  }
@@ -383,7 +585,7 @@ async function detectPattern({
383
585
  tool: pattern.tool,
384
586
  configPath: 'package.json (dependency)',
385
587
  label: `${pattern.label} (detected via dependency)`,
386
- packagePath: packagePath || undefined,
588
+ ...(packagePath ? { packagePath } : {}),
387
589
  }
388
590
  }
389
591
  return null
@@ -393,16 +595,11 @@ async function detectPattern({
393
595
  let isLegacyWebpack = false
394
596
 
395
597
  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
- ) {
598
+ if (isLegacyRspackVersion(resolvedVersion)) {
401
599
  isLegacyRspack = true
402
600
  }
403
601
  } else if (pattern.tool === 'webpack') {
404
- const version = resolvedVersion
405
- if ((version && version.includes('^4')) || version?.startsWith('4.')) {
602
+ if (isLegacyWebpackVersion(resolvedVersion)) {
406
603
  isLegacyWebpack = true
407
604
  }
408
605
  }
@@ -416,9 +613,9 @@ async function detectPattern({
416
613
  label: `${pattern.label} (${relativeConfig})${isLegacyRspack ? ' [Legacy]' : ''}${
417
614
  isLegacyWebpack ? ' [Webpack 4]' : ''
418
615
  }${inferredFromScripts ? ' [Scripts Detected]' : ''}`,
419
- isLegacyRspack,
420
- isLegacyWebpack,
421
- packagePath: packagePath || undefined,
616
+ ...(isLegacyRspack ? { isLegacyRspack: true } : {}),
617
+ ...(isLegacyWebpack ? { isLegacyWebpack: true } : {}),
618
+ ...(packagePath ? { packagePath } : {}),
422
619
  }
423
620
  }
424
621
 
@@ -44,9 +44,12 @@ const SUPPORTED_FRAMEWORKS: { framework: Framework; deps: string[] }[] = [
44
44
  const UNSUPPORTED_FRAMEWORKS: { name: string; dep: string }[] = [
45
45
  { name: 'Solid', dep: 'solid-js' },
46
46
  { name: 'Svelte', dep: 'svelte' },
47
+ { name: 'SvelteKit', dep: '@sveltejs/kit' },
47
48
  { name: 'Angular', dep: '@angular/core' },
48
49
  { name: 'Preact', dep: 'preact' },
49
50
  { name: 'Lit', dep: 'lit' },
51
+ { name: 'Qwik', dep: 'qwik' },
52
+ { name: 'Alpine', dep: 'lit-html' },
50
53
  ]
51
54
 
52
55
  /**
@@ -14,8 +14,8 @@ export async function detectPackageManager(root: string): Promise<PackageManager
14
14
  ['bun.lockb', 'bun'],
15
15
  ['bun.lock', 'bun'],
16
16
  ['pnpm-lock.yaml', 'pnpm'],
17
- ['yarn.lock', 'yarn'],
18
17
  ['package-lock.json', 'npm'],
18
+ ['yarn.lock', 'yarn'],
19
19
  ]
20
20
 
21
21
  const results = await Promise.all(
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { apply } from './commands/apply.js'
2
2
  export { detect } from './commands/detect.js'
3
+ export { devLink, devStatus, devUnlink } from './commands/dev-config.js'
3
4
  export { init } from './commands/init.js'
4
5
  export { collectDoctorResult, doctor } from './commands/doctor.js'
5
6
  export { integrationDoctor } from './commands/integration-doctor.js'