@inspecto-dev/cli 0.2.0-alpha.5 → 0.3.0-alpha.1

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 (49) hide show
  1. package/.turbo/turbo-build.log +19 -20
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +93 -11
  4. package/bin/inspecto.js +5 -1
  5. package/dist/bin.d.ts +5 -1
  6. package/dist/bin.js +530 -49
  7. package/dist/chunk-FZS2TLXQ.js +3140 -0
  8. package/dist/index.d.ts +233 -2
  9. package/dist/index.js +17 -3
  10. package/package.json +3 -2
  11. package/src/bin.ts +286 -66
  12. package/src/commands/apply.ts +118 -0
  13. package/src/commands/detect.ts +59 -0
  14. package/src/commands/doctor.ts +225 -72
  15. package/src/commands/init.ts +143 -183
  16. package/src/commands/integration-install.ts +452 -0
  17. package/src/commands/onboard.ts +50 -0
  18. package/src/commands/plan.ts +41 -0
  19. package/src/detect/build-tool.ts +107 -3
  20. package/src/index.ts +17 -2
  21. package/src/inject/ast-injector.ts +17 -6
  22. package/src/inject/extension.ts +40 -22
  23. package/src/inject/gitignore.ts +10 -3
  24. package/src/instructions.ts +60 -46
  25. package/src/onboarding/apply.ts +364 -0
  26. package/src/onboarding/context.ts +36 -0
  27. package/src/onboarding/planner.ts +284 -0
  28. package/src/onboarding/session.ts +434 -0
  29. package/src/onboarding/target-resolution.ts +116 -0
  30. package/src/prompts.ts +54 -11
  31. package/src/types.ts +184 -0
  32. package/src/utils/fs.ts +2 -1
  33. package/src/utils/logger.ts +9 -0
  34. package/src/utils/output.ts +40 -0
  35. package/tests/apply.test.ts +583 -0
  36. package/tests/ast-injector.test.ts +50 -0
  37. package/tests/build-tool.test.ts +3 -5
  38. package/tests/detect.test.ts +94 -0
  39. package/tests/doctor.test.ts +224 -0
  40. package/tests/init.test.ts +364 -0
  41. package/tests/install-wrapper.test.ts +76 -0
  42. package/tests/instructions.test.ts +61 -0
  43. package/tests/integration-install.test.ts +294 -0
  44. package/tests/logger.test.ts +100 -0
  45. package/tests/onboard.test.ts +258 -0
  46. package/tests/plan.test.ts +713 -0
  47. package/tests/workspace-build-tool.test.ts +75 -0
  48. package/.turbo/turbo-test.log +0 -16
  49. package/dist/chunk-MIHQGC3L.js +0 -1720
@@ -0,0 +1,452 @@
1
+ import fs from 'node:fs/promises'
2
+ import { homedir } from 'node:os'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { exists, writeFile } from '../utils/fs.js'
6
+ import { log } from '../utils/logger.js'
7
+
8
+ const REPO_RAW_BASE = 'https://raw.githubusercontent.com/inspecto-dev/inspecto/main'
9
+
10
+ type AssistantId = 'codex' | 'claude-code' | 'copilot' | 'cursor' | 'gemini' | 'trae' | 'coco'
11
+ type ClaudeScope = 'project' | 'user'
12
+ type CopilotMode = 'instructions' | 'agents'
13
+ type CursorMode = 'rules' | 'agents'
14
+
15
+ interface DownloadAsset {
16
+ source: string
17
+ target: string
18
+ executable?: boolean
19
+ localSource?: string
20
+ }
21
+
22
+ export interface IntegrationManifest {
23
+ assistant: string
24
+ type:
25
+ | 'native-skill'
26
+ | 'instruction-template'
27
+ | 'rule-template'
28
+ | 'context-template'
29
+ | 'compatibility-template'
30
+ installTarget: string
31
+ preferredInstall: string
32
+ cliSupported: boolean
33
+ }
34
+
35
+ export interface IntegrationDescription {
36
+ assistant: string
37
+ type: IntegrationManifest['type']
38
+ targets: string[]
39
+ preferredInstall: string
40
+ cliSupported: boolean
41
+ }
42
+
43
+ export interface InstallIntegrationOptions {
44
+ scope?: ClaudeScope
45
+ mode?: CopilotMode | CursorMode
46
+ force?: boolean
47
+ }
48
+
49
+ interface InstallPlan {
50
+ assets: DownloadAsset[]
51
+ successMessage: string
52
+ nextStep: string
53
+ }
54
+
55
+ const INTEGRATION_MANIFESTS: IntegrationManifest[] = [
56
+ {
57
+ assistant: 'codex',
58
+ type: 'native-skill',
59
+ installTarget: '~/.codex/skills/',
60
+ preferredInstall: 'npx @inspecto-dev/cli integrations install codex',
61
+ cliSupported: true,
62
+ },
63
+ {
64
+ assistant: 'claude-code',
65
+ type: 'native-skill',
66
+ installTarget: '.claude/skills/ or ~/.claude/skills/',
67
+ preferredInstall: 'npx @inspecto-dev/cli integrations install claude-code --scope project',
68
+ cliSupported: true,
69
+ },
70
+ {
71
+ assistant: 'copilot',
72
+ type: 'instruction-template',
73
+ installTarget: '.github/copilot-instructions.md or AGENTS.md',
74
+ preferredInstall: 'npx @inspecto-dev/cli integrations install copilot',
75
+ cliSupported: true,
76
+ },
77
+ {
78
+ assistant: 'cursor',
79
+ type: 'rule-template',
80
+ installTarget: '.cursor/rules/inspecto-onboarding.mdc or AGENTS.md',
81
+ preferredInstall: 'npx @inspecto-dev/cli integrations install cursor --mode rules',
82
+ cliSupported: true,
83
+ },
84
+ {
85
+ assistant: 'gemini',
86
+ type: 'context-template',
87
+ installTarget: 'GEMINI.md',
88
+ preferredInstall: 'npx @inspecto-dev/cli integrations install gemini',
89
+ cliSupported: true,
90
+ },
91
+ {
92
+ assistant: 'trae',
93
+ type: 'compatibility-template',
94
+ installTarget: 'AGENTS.md',
95
+ preferredInstall: 'npx @inspecto-dev/cli integrations install trae',
96
+ cliSupported: true,
97
+ },
98
+ {
99
+ assistant: 'coco',
100
+ type: 'compatibility-template',
101
+ installTarget: 'AGENTS.md',
102
+ preferredInstall: 'npx @inspecto-dev/cli integrations install coco',
103
+ cliSupported: true,
104
+ },
105
+ ]
106
+
107
+ export async function installIntegration(
108
+ assistant: string,
109
+ options: InstallIntegrationOptions = {},
110
+ ): Promise<void> {
111
+ const plan = resolveInstallPlan(assistant, options)
112
+
113
+ log.header('Inspecto Integration Install')
114
+
115
+ for (const asset of plan.assets) {
116
+ if ((await exists(asset.target)) && !options.force) {
117
+ throw new Error(
118
+ `Refusing to overwrite existing file: ${asset.target}. Re-run with --force if you want to replace it.`,
119
+ )
120
+ }
121
+ }
122
+
123
+ const downloadedAssets = [] as Array<{ asset: DownloadAsset; content: string }>
124
+
125
+ for (const asset of plan.assets) {
126
+ const content = await loadAsset(asset)
127
+ downloadedAssets.push({ asset, content })
128
+ }
129
+
130
+ for (const { asset, content } of downloadedAssets) {
131
+ await writeFile(asset.target, content)
132
+
133
+ if (asset.executable) {
134
+ await fs.chmod(asset.target, 0o755)
135
+ }
136
+ }
137
+
138
+ log.success(plan.successMessage)
139
+ log.hint(plan.nextStep)
140
+ }
141
+
142
+ export function listIntegrationManifests(): IntegrationManifest[] {
143
+ return INTEGRATION_MANIFESTS.map(manifest => ({ ...manifest }))
144
+ }
145
+
146
+ export function describeIntegration(
147
+ assistant: string,
148
+ options: InstallIntegrationOptions = {},
149
+ ): IntegrationDescription {
150
+ const manifest = getIntegrationManifest(assistant)
151
+
152
+ const targets = manifest.cliSupported
153
+ ? resolveInstallPlan(assistant, options).assets.map(asset => asset.target)
154
+ : [manifest.installTarget]
155
+
156
+ return {
157
+ assistant: manifest.assistant,
158
+ type: manifest.type,
159
+ targets,
160
+ preferredInstall: manifest.preferredInstall,
161
+ cliSupported: manifest.cliSupported,
162
+ }
163
+ }
164
+
165
+ export function printIntegrationList(): void {
166
+ log.header('Inspecto Integrations')
167
+ for (const manifest of INTEGRATION_MANIFESTS) {
168
+ const support = manifest.cliSupported ? 'CLI' : 'native installer'
169
+ log.info(`${manifest.assistant} — ${manifest.type} — ${manifest.installTarget} — ${support}`)
170
+ }
171
+ }
172
+
173
+ export function printIntegrationPath(
174
+ assistant: string,
175
+ options: InstallIntegrationOptions = {},
176
+ ): void {
177
+ const description = describeIntegration(assistant, options)
178
+
179
+ log.header(`Inspecto Integration Paths: ${description.assistant}`)
180
+ for (const target of description.targets) {
181
+ log.info(target)
182
+ }
183
+
184
+ if (description.cliSupported) {
185
+ log.hint(`Preferred install: ${description.preferredInstall}`)
186
+ } else {
187
+ log.hint(`Native install required: ${description.preferredInstall}`)
188
+ }
189
+ }
190
+
191
+ function resolveInstallPlan(assistant: string, options: InstallIntegrationOptions): InstallPlan {
192
+ switch (assistant as AssistantId) {
193
+ case 'codex':
194
+ return resolveCodexPlan(options)
195
+ case 'claude-code':
196
+ return resolveClaudeCodePlan(options)
197
+ case 'copilot':
198
+ return resolveCopilotPlan(options)
199
+ case 'cursor':
200
+ return resolveCursorPlan(options)
201
+ case 'gemini':
202
+ return {
203
+ assets: [
204
+ {
205
+ source: `${REPO_RAW_BASE}/assistant-integrations/gemini/GEMINI.md`,
206
+ target: 'GEMINI.md',
207
+ localSource: 'assistant-integrations/gemini/GEMINI.md',
208
+ },
209
+ ],
210
+ successMessage: 'Installed Gemini context to GEMINI.md',
211
+ nextStep: 'Start a new Gemini CLI session.',
212
+ }
213
+ case 'trae':
214
+ return {
215
+ assets: [
216
+ {
217
+ source: `${REPO_RAW_BASE}/assistant-integrations/trae/AGENTS.md`,
218
+ target: 'AGENTS.md',
219
+ localSource: 'assistant-integrations/trae/AGENTS.md',
220
+ },
221
+ ],
222
+ successMessage: 'Installed Trae compatibility instructions to AGENTS.md',
223
+ nextStep: 'Open a new Trae chat.',
224
+ }
225
+ case 'coco':
226
+ return {
227
+ assets: [
228
+ {
229
+ source: `${REPO_RAW_BASE}/assistant-integrations/coco/AGENTS.md`,
230
+ target: 'AGENTS.md',
231
+ localSource: 'assistant-integrations/coco/AGENTS.md',
232
+ },
233
+ ],
234
+ successMessage: 'Installed Coco compatibility instructions to AGENTS.md',
235
+ nextStep: 'Start a new Coco session.',
236
+ }
237
+ default:
238
+ throw new Error(`Unknown assistant: ${assistant}`)
239
+ }
240
+ }
241
+
242
+ function getIntegrationManifest(assistant: string): IntegrationManifest {
243
+ const manifest = INTEGRATION_MANIFESTS.find(item => item.assistant === assistant)
244
+
245
+ if (!manifest) {
246
+ throw new Error(
247
+ `Unknown assistant: ${assistant}. Run 'inspecto integrations list' to see available targets.`,
248
+ )
249
+ }
250
+
251
+ return manifest
252
+ }
253
+
254
+ function resolveCodexPlan(options: InstallIntegrationOptions): InstallPlan {
255
+ if (options.scope !== undefined) {
256
+ throw new Error('`--scope` is not supported for codex.')
257
+ }
258
+
259
+ if (options.mode !== undefined) {
260
+ throw new Error('`--mode` is not supported for codex.')
261
+ }
262
+
263
+ const baseDir = path.join(homedir(), '.codex/skills/inspecto-onboarding-codex')
264
+
265
+ return {
266
+ assets: [
267
+ {
268
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-codex/SKILL.md`,
269
+ target: path.join(baseDir, 'SKILL.md'),
270
+ localSource: 'skills/inspecto-onboarding-codex/SKILL.md',
271
+ },
272
+ {
273
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-codex/agents/openai.yaml`,
274
+ target: path.join(baseDir, 'agents/openai.yaml'),
275
+ localSource: 'skills/inspecto-onboarding-codex/agents/openai.yaml',
276
+ },
277
+ {
278
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh`,
279
+ target: path.join(baseDir, 'scripts/run-inspecto.sh'),
280
+ executable: true,
281
+ localSource: 'skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
282
+ },
283
+ ],
284
+ successMessage: `Installed Codex skill to ${baseDir}`,
285
+ nextStep: 'Restart Codex or start a new Codex session to load the skill.',
286
+ }
287
+ }
288
+
289
+ function resolveClaudeCodePlan(options: InstallIntegrationOptions): InstallPlan {
290
+ const scope = options.scope ?? 'project'
291
+ const unsupportedMode = options.mode !== undefined
292
+
293
+ if (unsupportedMode) {
294
+ throw new Error(
295
+ '`--mode` is not supported for claude-code. Use `--scope project|user` instead.',
296
+ )
297
+ }
298
+
299
+ if (scope !== 'project' && scope !== 'user') {
300
+ throw new Error(`Unknown Claude Code scope: ${scope}`)
301
+ }
302
+
303
+ const baseDir =
304
+ scope === 'user'
305
+ ? path.join(homedir(), '.claude/skills/inspecto-onboarding-claude-code')
306
+ : '.claude/skills/inspecto-onboarding-claude-code'
307
+
308
+ return {
309
+ assets: [
310
+ {
311
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-claude-code/SKILL.md`,
312
+ target: path.join(baseDir, 'SKILL.md'),
313
+ localSource: 'skills/inspecto-onboarding-claude-code/SKILL.md',
314
+ },
315
+ {
316
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-claude-code/agents/openai.yaml`,
317
+ target: path.join(baseDir, 'agents/openai.yaml'),
318
+ localSource: 'skills/inspecto-onboarding-claude-code/agents/openai.yaml',
319
+ },
320
+ {
321
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-claude-code/scripts/run-inspecto.sh`,
322
+ target: path.join(baseDir, 'scripts/run-inspecto.sh'),
323
+ executable: true,
324
+ localSource: 'skills/inspecto-onboarding-claude-code/scripts/run-inspecto.sh',
325
+ },
326
+ ],
327
+ successMessage: `Installed Claude Code skill to ${baseDir}`,
328
+ nextStep: 'Restart Claude Code to load the new skill.',
329
+ }
330
+ }
331
+
332
+ function resolveCopilotPlan(options: InstallIntegrationOptions): InstallPlan {
333
+ const mode = options.mode ?? 'instructions'
334
+
335
+ if (options.scope !== undefined) {
336
+ throw new Error(
337
+ '`--scope` is not supported for copilot. Use `--mode instructions|agents` instead.',
338
+ )
339
+ }
340
+
341
+ switch (mode) {
342
+ case 'instructions':
343
+ return {
344
+ assets: [
345
+ {
346
+ source: `${REPO_RAW_BASE}/assistant-integrations/copilot/.github/copilot-instructions.md`,
347
+ target: '.github/copilot-instructions.md',
348
+ localSource: 'assistant-integrations/copilot/.github/copilot-instructions.md',
349
+ },
350
+ ],
351
+ successMessage: 'Installed Copilot instructions to .github/copilot-instructions.md',
352
+ nextStep: 'Open a new Copilot chat or agent session.',
353
+ }
354
+ case 'agents':
355
+ return {
356
+ assets: [
357
+ {
358
+ source: `${REPO_RAW_BASE}/assistant-integrations/copilot/AGENTS.md`,
359
+ target: 'AGENTS.md',
360
+ localSource: 'assistant-integrations/copilot/AGENTS.md',
361
+ },
362
+ ],
363
+ successMessage: 'Installed Copilot compatibility instructions to AGENTS.md',
364
+ nextStep: 'Open a new Copilot chat or agent session.',
365
+ }
366
+ default:
367
+ throw new Error(`Unknown Copilot mode: ${mode}`)
368
+ }
369
+ }
370
+
371
+ function resolveCursorPlan(options: InstallIntegrationOptions): InstallPlan {
372
+ const mode = options.mode ?? 'rules'
373
+
374
+ if (options.scope !== undefined) {
375
+ throw new Error('`--scope` is not supported for cursor. Use `--mode rules|agents` instead.')
376
+ }
377
+
378
+ switch (mode) {
379
+ case 'rules':
380
+ return {
381
+ assets: [
382
+ {
383
+ source: `${REPO_RAW_BASE}/assistant-integrations/cursor/.cursor/rules/inspecto-onboarding.mdc`,
384
+ target: '.cursor/rules/inspecto-onboarding.mdc',
385
+ localSource: 'assistant-integrations/cursor/.cursor/rules/inspecto-onboarding.mdc',
386
+ },
387
+ ],
388
+ successMessage: 'Installed Cursor rule to .cursor/rules/inspecto-onboarding.mdc',
389
+ nextStep: 'Open a new Cursor chat.',
390
+ }
391
+ case 'agents':
392
+ return {
393
+ assets: [
394
+ {
395
+ source: `${REPO_RAW_BASE}/assistant-integrations/cursor/AGENTS.md`,
396
+ target: 'AGENTS.md',
397
+ localSource: 'assistant-integrations/cursor/AGENTS.md',
398
+ },
399
+ ],
400
+ successMessage: 'Installed Cursor compatibility instructions to AGENTS.md',
401
+ nextStep: 'Open a new Cursor chat.',
402
+ }
403
+ default:
404
+ throw new Error(`Unknown Cursor mode: ${mode}`)
405
+ }
406
+ }
407
+
408
+ async function loadAsset(asset: DownloadAsset): Promise<string> {
409
+ if (asset.localSource) {
410
+ const localPath = await resolveBundledAssetPath(asset.localSource)
411
+ if (localPath) {
412
+ return await fs.readFile(localPath, 'utf-8')
413
+ }
414
+ }
415
+
416
+ return await downloadAsset(asset.source)
417
+ }
418
+
419
+ async function resolveBundledAssetPath(relativePath: string): Promise<string | null> {
420
+ const startDir = path.dirname(fileURLToPath(import.meta.url))
421
+ let currentDir = startDir
422
+
423
+ for (let depth = 0; depth < 8; depth += 1) {
424
+ const candidate = path.join(currentDir, relativePath)
425
+ if (await exists(candidate)) {
426
+ return candidate
427
+ }
428
+
429
+ const parent = path.dirname(currentDir)
430
+ if (parent === currentDir) break
431
+ currentDir = parent
432
+ }
433
+
434
+ return null
435
+ }
436
+
437
+ async function downloadAsset(source: string): Promise<string> {
438
+ let response: Response
439
+
440
+ try {
441
+ response = await fetch(source)
442
+ } catch (error) {
443
+ const reason = error instanceof Error ? error.message : String(error)
444
+ throw new Error(`Failed to download ${source}: ${reason}`)
445
+ }
446
+
447
+ if (!response.ok) {
448
+ throw new Error(`Failed to download ${source}: ${response.status} ${response.statusText}`)
449
+ }
450
+
451
+ return await response.text()
452
+ }
@@ -0,0 +1,50 @@
1
+ import { resolveOnboardingSession, applyResolvedOnboardingSession, buildDeferredOnboardResult } from '../onboarding/session.js'
2
+ import { log } from '../utils/logger.js'
3
+ import { writeCommandOutput } from '../utils/output.js'
4
+ import type { OnboardCommandResult } from '../types.js'
5
+
6
+ export interface OnboardCommandOptions {
7
+ json?: boolean
8
+ target?: string
9
+ yes?: boolean
10
+ shared?: boolean
11
+ skipInstall?: boolean
12
+ dryRun?: boolean
13
+ noExtension?: boolean
14
+ }
15
+
16
+ function printOnboardResult(result: OnboardCommandResult): void {
17
+ log.header('Inspecto Onboard')
18
+ log.info(`Status: ${result.status}`)
19
+ log.info(result.summary.headline)
20
+
21
+ for (const change of result.summary.changes) {
22
+ log.hint(change)
23
+ }
24
+ for (const step of result.diagnostics?.nextSteps ?? []) {
25
+ log.warn(step)
26
+ }
27
+ if (result.confirmation.required && result.confirmation.question) {
28
+ log.warn(result.confirmation.question)
29
+ }
30
+ }
31
+
32
+ export async function onboard(options: OnboardCommandOptions = {}): Promise<OnboardCommandResult> {
33
+ const root = process.cwd()
34
+ const session = await resolveOnboardingSession(root, options)
35
+
36
+ if (
37
+ session.status === 'error' ||
38
+ session.status === 'needs_target_selection' ||
39
+ session.status === 'needs_confirmation'
40
+ ) {
41
+ return writeCommandOutput(
42
+ buildDeferredOnboardResult(session),
43
+ options.json ?? false,
44
+ printOnboardResult,
45
+ )
46
+ }
47
+
48
+ const result = await applyResolvedOnboardingSession(session, options)
49
+ return writeCommandOutput(result, options.json ?? false, printOnboardResult)
50
+ }
@@ -0,0 +1,41 @@
1
+ import { log } from '../utils/logger.js'
2
+ import { writeCommandOutput } from '../utils/output.js'
3
+ import { buildOnboardingContext } from '../onboarding/context.js'
4
+ import { createPlanResult } from '../onboarding/planner.js'
5
+ import type { PlanResult } from '../types.js'
6
+
7
+ function printPlanResult(result: PlanResult): void {
8
+ log.header('Inspecto Plan')
9
+ log.info(`Status: ${result.status}`)
10
+ log.info(`Strategy: ${result.strategy}`)
11
+
12
+ if (result.defaults.provider) {
13
+ log.info(`Default provider: ${result.defaults.provider}`)
14
+ }
15
+ if (result.defaults.ide) {
16
+ log.info(`Default IDE: ${result.defaults.ide}`)
17
+ }
18
+ log.info(`Shared mode: ${result.defaults.shared ? 'enabled' : 'disabled'}`)
19
+ log.info(`Extension mode: ${result.defaults.extension ? 'enabled' : 'disabled'}`)
20
+
21
+ if (result.actions.length > 0) {
22
+ log.blank()
23
+ log.info('Actions:')
24
+ for (const action of result.actions) {
25
+ log.hint(`${action.type}: ${action.target} — ${action.description}`)
26
+ }
27
+ }
28
+
29
+ for (const blocker of result.blockers) {
30
+ log.error(blocker.message)
31
+ }
32
+ for (const warning of result.warnings) {
33
+ log.warn(warning.message)
34
+ }
35
+ }
36
+
37
+ export async function plan(json = false): Promise<PlanResult> {
38
+ const context = await buildOnboardingContext(process.cwd())
39
+ const result = createPlanResult(context)
40
+ return writeCommandOutput(result, json, printPlanResult)
41
+ }
@@ -5,8 +5,9 @@
5
5
  // Recognized but unsupported: Next.js / Nuxt / Remix / Astro / SvelteKit
6
6
  // ============================================================
7
7
  import path from 'node:path'
8
+ import fs from 'node:fs/promises'
8
9
  import { createRequire } from 'node:module'
9
- import { exists, readJSON } from '../utils/fs.js'
10
+ import { exists, readFile, readJSON } from '../utils/fs.js'
10
11
  import type { BuildTool, BuildToolDetection } from '../types.js'
11
12
 
12
13
  interface PackageJSON {
@@ -128,6 +129,89 @@ function createTargets(root: string, packagePaths?: string[]): DetectionTarget[]
128
129
  }))
129
130
  }
130
131
 
132
+ async function getWorkspacePackagePatterns(root: string): Promise<string[]> {
133
+ const pkg = await readJSON<{ workspaces?: string[] | { packages?: string[] } }>(
134
+ path.join(root, 'package.json'),
135
+ )
136
+
137
+ const workspaces = pkg?.workspaces
138
+ if (Array.isArray(workspaces)) {
139
+ return workspaces
140
+ }
141
+
142
+ if (workspaces && Array.isArray(workspaces.packages)) {
143
+ return workspaces.packages
144
+ }
145
+
146
+ const pnpmWorkspace = await readFile(path.join(root, 'pnpm-workspace.yaml'))
147
+ if (!pnpmWorkspace) {
148
+ return []
149
+ }
150
+
151
+ const patterns: string[] = []
152
+ for (const line of pnpmWorkspace.split('\n')) {
153
+ const match = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/)
154
+ if (match?.[1]) {
155
+ patterns.push(match[1])
156
+ }
157
+ }
158
+
159
+ return patterns
160
+ }
161
+
162
+ async function expandWorkspacePattern(root: string, pattern: string): Promise<string[]> {
163
+ const normalized = pattern.replace(/\\/g, '/').replace(/\/$/, '')
164
+ if (!normalized || normalized.startsWith('!')) {
165
+ return []
166
+ }
167
+
168
+ if (!normalized.includes('*')) {
169
+ return (await exists(path.join(root, normalized))) ? [normalized] : []
170
+ }
171
+
172
+ const starIndex = normalized.indexOf('*')
173
+ const baseDir = normalized.slice(0, starIndex).replace(/\/$/, '')
174
+ const suffix = normalized.slice(starIndex + 1)
175
+
176
+ if (suffix && suffix !== '') {
177
+ return []
178
+ }
179
+
180
+ const absoluteBaseDir = path.join(root, baseDir)
181
+ if (!(await exists(absoluteBaseDir))) {
182
+ return []
183
+ }
184
+
185
+ try {
186
+ const entries = await fs.readdir(absoluteBaseDir, { withFileTypes: true })
187
+ return entries
188
+ .filter(entry => entry.isDirectory())
189
+ .map(entry => path.posix.join(baseDir, entry.name))
190
+ } catch {
191
+ return []
192
+ }
193
+ }
194
+
195
+ async function detectWorkspaceTargets(root: string): Promise<DetectionTarget[]> {
196
+ const patterns = await getWorkspacePackagePatterns(root)
197
+ if (patterns.length === 0) {
198
+ return []
199
+ }
200
+
201
+ const packagePaths = new Set<string>()
202
+ for (const pattern of patterns) {
203
+ const expanded = await expandWorkspacePattern(root, pattern)
204
+ for (const packagePath of expanded) {
205
+ packagePaths.add(packagePath)
206
+ }
207
+ }
208
+
209
+ return Array.from(packagePaths).map(packagePath => ({
210
+ packagePath,
211
+ absolutePath: path.join(root, packagePath),
212
+ }))
213
+ }
214
+
131
215
  /**
132
216
  * Detect all build tools / meta-frameworks.
133
217
  * Returns supported tools and recognized-but-unsupported meta-frameworks.
@@ -138,7 +222,10 @@ export async function detectBuildTools(
138
222
  ): Promise<BuildToolResult> {
139
223
  const supported: BuildToolDetection[] = []
140
224
  const unsupported = new Set<string>()
141
- const targets = createTargets(root, packagePaths)
225
+ const explicitTargets = createTargets(root, packagePaths)
226
+ const workspaceTargets =
227
+ !packagePaths || packagePaths.length === 0 ? await detectWorkspaceTargets(root) : []
228
+ const targets = workspaceTargets.length > 0 ? workspaceTargets : explicitTargets
142
229
 
143
230
  for (const target of targets) {
144
231
  const pkg = await readJSON<PackageJSON>(path.join(target.absolutePath, 'package.json'))
@@ -230,6 +317,7 @@ async function detectPattern({
230
317
  }
231
318
 
232
319
  let detectedFile = ''
320
+ let inferredFromScripts = false
233
321
 
234
322
  if (pattern.tool === 'esbuild' && !hasDep) {
235
323
  return null
@@ -274,6 +362,7 @@ async function detectPattern({
274
362
  }
275
363
 
276
364
  if (!detectedFile) {
365
+ inferredFromScripts = true
277
366
  detectedFile = 'package.json (scripts)'
278
367
  break
279
368
  }
@@ -282,6 +371,21 @@ async function detectPattern({
282
371
  }
283
372
 
284
373
  if (!detectedFile) {
374
+ if (
375
+ hasDep &&
376
+ (pattern.tool === 'rollup' ||
377
+ pattern.tool === 'webpack' ||
378
+ pattern.tool === 'rspack' ||
379
+ pattern.tool === 'esbuild')
380
+ ) {
381
+ // dependency present but no config/scripting evidence; provide low-confidence detection
382
+ return {
383
+ tool: pattern.tool,
384
+ configPath: 'package.json (dependency)',
385
+ label: `${pattern.label} (detected via dependency)`,
386
+ packagePath: packagePath || undefined,
387
+ }
388
+ }
285
389
  return null
286
390
  }
287
391
 
@@ -311,7 +415,7 @@ async function detectPattern({
311
415
  configPath: relativeConfig,
312
416
  label: `${pattern.label} (${relativeConfig})${isLegacyRspack ? ' [Legacy]' : ''}${
313
417
  isLegacyWebpack ? ' [Webpack 4]' : ''
314
- }`,
418
+ }${inferredFromScripts ? ' [Scripts Detected]' : ''}`,
315
419
  isLegacyRspack,
316
420
  isLegacyWebpack,
317
421
  packagePath: packagePath || undefined,