@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.
- package/.turbo/turbo-build.log +19 -20
- package/CHANGELOG.md +22 -0
- package/README.md +93 -11
- package/bin/inspecto.js +5 -1
- package/dist/bin.d.ts +5 -1
- package/dist/bin.js +530 -49
- package/dist/chunk-FZS2TLXQ.js +3140 -0
- package/dist/index.d.ts +233 -2
- package/dist/index.js +17 -3
- package/package.json +3 -2
- package/src/bin.ts +286 -66
- package/src/commands/apply.ts +118 -0
- package/src/commands/detect.ts +59 -0
- package/src/commands/doctor.ts +225 -72
- package/src/commands/init.ts +143 -183
- package/src/commands/integration-install.ts +452 -0
- package/src/commands/onboard.ts +50 -0
- package/src/commands/plan.ts +41 -0
- package/src/detect/build-tool.ts +107 -3
- package/src/index.ts +17 -2
- package/src/inject/ast-injector.ts +17 -6
- package/src/inject/extension.ts +40 -22
- package/src/inject/gitignore.ts +10 -3
- package/src/instructions.ts +60 -46
- package/src/onboarding/apply.ts +364 -0
- package/src/onboarding/context.ts +36 -0
- package/src/onboarding/planner.ts +284 -0
- package/src/onboarding/session.ts +434 -0
- package/src/onboarding/target-resolution.ts +116 -0
- package/src/prompts.ts +54 -11
- package/src/types.ts +184 -0
- package/src/utils/fs.ts +2 -1
- package/src/utils/logger.ts +9 -0
- package/src/utils/output.ts +40 -0
- package/tests/apply.test.ts +583 -0
- package/tests/ast-injector.test.ts +50 -0
- package/tests/build-tool.test.ts +3 -5
- package/tests/detect.test.ts +94 -0
- package/tests/doctor.test.ts +224 -0
- package/tests/init.test.ts +364 -0
- package/tests/install-wrapper.test.ts +76 -0
- package/tests/instructions.test.ts +61 -0
- package/tests/integration-install.test.ts +294 -0
- package/tests/logger.test.ts +100 -0
- package/tests/onboard.test.ts +258 -0
- package/tests/plan.test.ts +713 -0
- package/tests/workspace-build-tool.test.ts +75 -0
- package/.turbo/turbo-test.log +0 -16
- package/dist/chunk-MIHQGC3L.js +0 -1720
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { detectFrameworks } from '../detect/framework.js'
|
|
3
|
+
import { detectIDE } from '../detect/ide.js'
|
|
4
|
+
import { detectProviders } from '../detect/provider.js'
|
|
5
|
+
import { applyOnboardingPlan, type ApplyOnboardingResult } from './apply.js'
|
|
6
|
+
import { buildOnboardingContext } from './context.js'
|
|
7
|
+
import { createPlanResult, planManualFollowUp } from './planner.js'
|
|
8
|
+
import { resolveOnboardingTarget } from './target-resolution.js'
|
|
9
|
+
import { readJSON } from '../utils/fs.js'
|
|
10
|
+
import type {
|
|
11
|
+
OnboardCommandResult,
|
|
12
|
+
OnboardingContext,
|
|
13
|
+
OnboardingDiagnostics,
|
|
14
|
+
OnboardingExecutionResult,
|
|
15
|
+
OnboardingIdeExtensionStatus,
|
|
16
|
+
OnboardingSummary,
|
|
17
|
+
OnboardingVerification,
|
|
18
|
+
PlanResult,
|
|
19
|
+
ResolvedOnboardingSession,
|
|
20
|
+
} from '../types.js'
|
|
21
|
+
|
|
22
|
+
export interface ResolveOnboardingSessionOptions {
|
|
23
|
+
json?: boolean
|
|
24
|
+
target?: string
|
|
25
|
+
yes?: boolean
|
|
26
|
+
shared?: boolean
|
|
27
|
+
skipInstall?: boolean
|
|
28
|
+
dryRun?: boolean
|
|
29
|
+
noExtension?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePackagePath(packagePath?: string): string {
|
|
33
|
+
if (!packagePath || packagePath === '.') return ''
|
|
34
|
+
return packagePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getProviderDefault(
|
|
38
|
+
providerId?: string,
|
|
39
|
+
preferredMode?: 'cli' | 'extension',
|
|
40
|
+
): string | undefined {
|
|
41
|
+
if (!providerId) return undefined
|
|
42
|
+
const mode = preferredMode ?? (providerId === 'coco' ? 'cli' : 'extension')
|
|
43
|
+
return `${providerId}.${mode}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getVerificationCommand(packageManager: OnboardingContext['packageManager']): string {
|
|
47
|
+
switch (packageManager) {
|
|
48
|
+
case 'pnpm':
|
|
49
|
+
return 'pnpm dev'
|
|
50
|
+
case 'yarn':
|
|
51
|
+
return 'yarn dev'
|
|
52
|
+
case 'bun':
|
|
53
|
+
return 'bun run dev'
|
|
54
|
+
case 'npm':
|
|
55
|
+
default:
|
|
56
|
+
return 'npm run dev'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function buildVerification(
|
|
61
|
+
projectRoot: string,
|
|
62
|
+
packageManager: OnboardingContext['packageManager'],
|
|
63
|
+
): Promise<OnboardingVerification> {
|
|
64
|
+
const packageJson = await readJSON<{ scripts?: Record<string, string> }>(
|
|
65
|
+
path.join(projectRoot, 'package.json'),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if (packageJson?.scripts?.dev) {
|
|
69
|
+
const devCommand = getVerificationCommand(packageManager)
|
|
70
|
+
return {
|
|
71
|
+
available: true,
|
|
72
|
+
devCommand,
|
|
73
|
+
message: `Start the local dev server with \`${devCommand}\` to verify Inspecto in the browser.`,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
available: false,
|
|
79
|
+
message: 'Start your normal local dev server command to verify Inspecto in the browser.',
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildIdeExtensionStatus(input: {
|
|
84
|
+
required: boolean
|
|
85
|
+
installed: boolean
|
|
86
|
+
manualRequired: boolean
|
|
87
|
+
}): OnboardingIdeExtensionStatus {
|
|
88
|
+
if (!input.required) {
|
|
89
|
+
return {
|
|
90
|
+
required: false,
|
|
91
|
+
installed: false,
|
|
92
|
+
manualRequired: false,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
required: true,
|
|
98
|
+
installed: input.installed,
|
|
99
|
+
manualRequired: input.manualRequired,
|
|
100
|
+
installCommand: 'code --install-extension inspecto.inspecto',
|
|
101
|
+
marketplaceUrl: 'https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto',
|
|
102
|
+
openVsxUrl: 'https://open-vsx.org/extension/inspecto/inspecto',
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function detectFrameworkSupportByPackage(
|
|
107
|
+
repoRoot: string,
|
|
108
|
+
context: OnboardingContext,
|
|
109
|
+
): Promise<Record<string, string[]>> {
|
|
110
|
+
const packagePaths = new Set(
|
|
111
|
+
context.buildTools.supported.map(item => normalizePackagePath(item.packagePath)),
|
|
112
|
+
)
|
|
113
|
+
const supportByPackage: Record<string, string[]> = {}
|
|
114
|
+
|
|
115
|
+
await Promise.all(
|
|
116
|
+
Array.from(packagePaths).map(async packagePath => {
|
|
117
|
+
const frameworkResult = await detectFrameworks(
|
|
118
|
+
packagePath ? path.join(repoRoot, packagePath) : repoRoot,
|
|
119
|
+
)
|
|
120
|
+
supportByPackage[packagePath] = frameworkResult.supported
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return supportByPackage
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function buildTargetedContext(
|
|
128
|
+
rootContext: OnboardingContext,
|
|
129
|
+
packagePath: string,
|
|
130
|
+
): Promise<OnboardingContext> {
|
|
131
|
+
const projectRoot = packagePath ? path.join(rootContext.root, packagePath) : rootContext.root
|
|
132
|
+
const [frameworks, ides, providers] = await Promise.all([
|
|
133
|
+
detectFrameworks(projectRoot),
|
|
134
|
+
detectIDE(projectRoot),
|
|
135
|
+
detectProviders(projectRoot),
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
root: projectRoot,
|
|
140
|
+
packageManager: rootContext.packageManager,
|
|
141
|
+
buildTools: {
|
|
142
|
+
supported: rootContext.buildTools.supported.filter(item => {
|
|
143
|
+
return normalizePackagePath(item.packagePath) === packagePath
|
|
144
|
+
}),
|
|
145
|
+
unsupported: [],
|
|
146
|
+
},
|
|
147
|
+
frameworks: {
|
|
148
|
+
supported: frameworks.supported,
|
|
149
|
+
unsupported: frameworks.unsupported.map(item => item.name),
|
|
150
|
+
},
|
|
151
|
+
ides: ides.detected.map(({ ide, supported }) => ({ ide, supported })),
|
|
152
|
+
providers: providers.detected.map(({ id, label, supported, preferredMode }) => ({
|
|
153
|
+
id,
|
|
154
|
+
label,
|
|
155
|
+
supported,
|
|
156
|
+
preferredMode,
|
|
157
|
+
})),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildOnboardingSummary(plan: PlanResult, projectRoot: string): OnboardingSummary {
|
|
162
|
+
const changes = plan.actions
|
|
163
|
+
.filter(action => action.type !== 'manual_step')
|
|
164
|
+
.map(action => action.description)
|
|
165
|
+
const risks = [...plan.warnings.map(item => item.message)]
|
|
166
|
+
const manualFollowUp = planManualFollowUp(plan)
|
|
167
|
+
|
|
168
|
+
let headline = `Inspecto is ready to onboard ${projectRoot}.`
|
|
169
|
+
if (manualFollowUp.length > 0) {
|
|
170
|
+
headline = `Inspecto can partially onboard ${projectRoot}, but manual follow-up remains.`
|
|
171
|
+
} else if (plan.status === 'blocked') {
|
|
172
|
+
headline = `Inspecto could not build an automatic onboarding path for ${projectRoot}.`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
headline,
|
|
177
|
+
changes,
|
|
178
|
+
risks,
|
|
179
|
+
manualFollowUp,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildConfirmation(
|
|
184
|
+
plan: PlanResult,
|
|
185
|
+
summary: OnboardingSummary,
|
|
186
|
+
session: {
|
|
187
|
+
targetWasHeuristic: boolean
|
|
188
|
+
supportedIdeCount: number
|
|
189
|
+
supportedProviderCount: number
|
|
190
|
+
},
|
|
191
|
+
options: ResolveOnboardingSessionOptions,
|
|
192
|
+
): { required: boolean; reason?: string; question?: string } {
|
|
193
|
+
if (options.yes) {
|
|
194
|
+
return { required: false }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const reasons: string[] = []
|
|
198
|
+
if (session.targetWasHeuristic) reasons.push('a monorepo target was preselected')
|
|
199
|
+
if (summary.manualFollowUp.length > 0) reasons.push('manual follow-up will remain after setup')
|
|
200
|
+
if (options.noExtension || options.skipInstall) reasons.push('non-core setup steps are being skipped')
|
|
201
|
+
if (session.supportedIdeCount > 1 || session.supportedProviderCount > 1) {
|
|
202
|
+
reasons.push('multiple IDE or provider choices are still relevant')
|
|
203
|
+
}
|
|
204
|
+
if (plan.warnings.length > 0) reasons.push('the CLI detected non-blocking risk')
|
|
205
|
+
|
|
206
|
+
if (reasons.length === 0) {
|
|
207
|
+
return { required: false }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
required: true,
|
|
212
|
+
reason: reasons.join('; '),
|
|
213
|
+
question: 'Proceed with Inspecto onboarding using the proposed default target and settings?',
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildPreApplyResult(
|
|
218
|
+
status: ResolvedOnboardingSession['status'],
|
|
219
|
+
session: ResolvedOnboardingSession,
|
|
220
|
+
): OnboardCommandResult {
|
|
221
|
+
const diagnostics: OnboardingDiagnostics | undefined =
|
|
222
|
+
session.summary.risks.length > 0 || session.summary.manualFollowUp.length > 0 || session.plan.blockers.length > 0
|
|
223
|
+
? {
|
|
224
|
+
warnings: session.summary.risks,
|
|
225
|
+
errors: session.plan.blockers.map(item => item.message),
|
|
226
|
+
nextSteps: session.summary.manualFollowUp,
|
|
227
|
+
}
|
|
228
|
+
: undefined
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
status,
|
|
232
|
+
target: session.target,
|
|
233
|
+
summary: session.summary,
|
|
234
|
+
confirmation: session.confirmation,
|
|
235
|
+
ideExtension: buildIdeExtensionStatus({
|
|
236
|
+
required: session.plan.defaults.extension,
|
|
237
|
+
installed: false,
|
|
238
|
+
manualRequired: session.plan.defaults.extension,
|
|
239
|
+
}),
|
|
240
|
+
verification: session.verification,
|
|
241
|
+
diagnostics,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildExecutionResult(
|
|
246
|
+
session: ResolvedOnboardingSession,
|
|
247
|
+
applyResult: ApplyOnboardingResult,
|
|
248
|
+
): OnboardingExecutionResult {
|
|
249
|
+
return {
|
|
250
|
+
changedFiles: Array.from(
|
|
251
|
+
new Set(applyResult.mutations.map(item => item.path).filter((value): value is string => !!value)),
|
|
252
|
+
),
|
|
253
|
+
installedDependencies: applyResult.mutations
|
|
254
|
+
.map(item => item.name)
|
|
255
|
+
.filter((value): value is string => !!value),
|
|
256
|
+
selectedProviderDefault: session.providerDefault,
|
|
257
|
+
selectedIDE: session.selectedIDE?.ide,
|
|
258
|
+
mutations: applyResult.mutations,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildExecutionDiagnostics(
|
|
263
|
+
session: ResolvedOnboardingSession,
|
|
264
|
+
applyResult: ApplyOnboardingResult,
|
|
265
|
+
): OnboardingDiagnostics | undefined {
|
|
266
|
+
const warnings = [...session.plan.warnings.map(item => item.message)]
|
|
267
|
+
const errors: string[] = []
|
|
268
|
+
|
|
269
|
+
if (applyResult.postInstall.installFailed) {
|
|
270
|
+
errors.push('Dependency installation failed during onboarding.')
|
|
271
|
+
}
|
|
272
|
+
if (applyResult.postInstall.injectionFailed) {
|
|
273
|
+
warnings.push('Automatic plugin injection did not finish cleanly.')
|
|
274
|
+
}
|
|
275
|
+
if (applyResult.postInstall.manualExtensionInstallNeeded) {
|
|
276
|
+
warnings.push('IDE extension installation still needs manual completion.')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (warnings.length === 0 && errors.length === 0 && applyResult.postInstall.nextSteps.length === 0) {
|
|
280
|
+
return undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
warnings,
|
|
285
|
+
errors,
|
|
286
|
+
nextSteps: applyResult.postInstall.nextSteps,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function resolveOnboardingSession(
|
|
291
|
+
root: string,
|
|
292
|
+
options: ResolveOnboardingSessionOptions = {},
|
|
293
|
+
): Promise<ResolvedOnboardingSession> {
|
|
294
|
+
const rootContext = await buildOnboardingContext(root)
|
|
295
|
+
const rootVerification = await buildVerification(root, rootContext.packageManager)
|
|
296
|
+
const frameworkSupportByPackage = await detectFrameworkSupportByPackage(root, rootContext)
|
|
297
|
+
const target = resolveOnboardingTarget({
|
|
298
|
+
repoRoot: root,
|
|
299
|
+
buildTools: rootContext.buildTools.supported,
|
|
300
|
+
frameworkSupportByPackage,
|
|
301
|
+
selectedPackagePath: options.target,
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
if (target.candidates.length === 0) {
|
|
305
|
+
const plan = createPlanResult(rootContext)
|
|
306
|
+
return {
|
|
307
|
+
status: 'error',
|
|
308
|
+
target,
|
|
309
|
+
summary: buildOnboardingSummary(plan, root),
|
|
310
|
+
confirmation: { required: false },
|
|
311
|
+
verification: rootVerification,
|
|
312
|
+
context: rootContext,
|
|
313
|
+
plan,
|
|
314
|
+
projectRoot: root,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (target.status === 'needs_selection') {
|
|
319
|
+
const plan = createPlanResult(rootContext)
|
|
320
|
+
const summary: OnboardingSummary = {
|
|
321
|
+
headline: 'Inspecto found multiple plausible app targets and needs one selection.',
|
|
322
|
+
changes: [],
|
|
323
|
+
risks: [],
|
|
324
|
+
manualFollowUp: [],
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
status: 'needs_target_selection',
|
|
328
|
+
target,
|
|
329
|
+
summary,
|
|
330
|
+
confirmation: { required: false },
|
|
331
|
+
verification: rootVerification,
|
|
332
|
+
context: rootContext,
|
|
333
|
+
plan,
|
|
334
|
+
projectRoot: root,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const packagePath = normalizePackagePath(target.selected?.packagePath)
|
|
339
|
+
const context = await buildTargetedContext(rootContext, packagePath)
|
|
340
|
+
const verification = await buildVerification(context.root, context.packageManager)
|
|
341
|
+
const plan = createPlanResult(context)
|
|
342
|
+
const summary = buildOnboardingSummary(plan, context.root)
|
|
343
|
+
const confirmation = buildConfirmation(
|
|
344
|
+
plan,
|
|
345
|
+
summary,
|
|
346
|
+
{
|
|
347
|
+
targetWasHeuristic: target.candidates.length > 1 && options.target === undefined,
|
|
348
|
+
supportedIdeCount: context.ides.filter(item => item.supported).length,
|
|
349
|
+
supportedProviderCount: context.providers.filter(item => item.supported).length,
|
|
350
|
+
},
|
|
351
|
+
options,
|
|
352
|
+
)
|
|
353
|
+
const selectedProvider =
|
|
354
|
+
context.providers.find(provider => provider.id === plan.defaults.provider) ?? null
|
|
355
|
+
const selectedIDE =
|
|
356
|
+
context.ides.find(ide => ide.ide === plan.defaults.ide) ??
|
|
357
|
+
context.ides.find(ide => ide.supported) ??
|
|
358
|
+
null
|
|
359
|
+
|
|
360
|
+
let status: ResolvedOnboardingSession['status'] = 'success'
|
|
361
|
+
if (plan.status === 'blocked') {
|
|
362
|
+
status = 'error'
|
|
363
|
+
} else if (confirmation.required) {
|
|
364
|
+
status = 'needs_confirmation'
|
|
365
|
+
} else if (summary.manualFollowUp.length > 0 || plan.warnings.length > 0) {
|
|
366
|
+
status = 'partial_success'
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
status,
|
|
371
|
+
target,
|
|
372
|
+
summary,
|
|
373
|
+
confirmation,
|
|
374
|
+
verification,
|
|
375
|
+
context,
|
|
376
|
+
plan,
|
|
377
|
+
projectRoot: context.root,
|
|
378
|
+
selectedIDE,
|
|
379
|
+
providerDefault: getProviderDefault(plan.defaults.provider, selectedProvider?.preferredMode),
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function applyResolvedOnboardingSession(
|
|
384
|
+
session: ResolvedOnboardingSession,
|
|
385
|
+
options: ResolveOnboardingSessionOptions = {},
|
|
386
|
+
): Promise<OnboardCommandResult> {
|
|
387
|
+
const verification = await buildVerification(session.projectRoot, session.context.packageManager)
|
|
388
|
+
const applyResult = await applyOnboardingPlan({
|
|
389
|
+
repoRoot: process.cwd(),
|
|
390
|
+
projectRoot: session.projectRoot,
|
|
391
|
+
packageManager: session.context.packageManager,
|
|
392
|
+
supportedBuildTargets: session.context.buildTools.supported,
|
|
393
|
+
options: {
|
|
394
|
+
shared: options.shared ?? session.plan.defaults.shared,
|
|
395
|
+
skipInstall: options.skipInstall ?? false,
|
|
396
|
+
dryRun: options.dryRun ?? false,
|
|
397
|
+
noExtension: options.noExtension ?? !session.plan.defaults.extension,
|
|
398
|
+
quiet: options.json ?? false,
|
|
399
|
+
},
|
|
400
|
+
selectedIDE: session.selectedIDE,
|
|
401
|
+
providerDefault: session.providerDefault,
|
|
402
|
+
plan: session.plan,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const diagnostics = buildExecutionDiagnostics(session, applyResult)
|
|
406
|
+
const status =
|
|
407
|
+
applyResult.postInstall.installFailed && session.context.buildTools.supported.length === 0
|
|
408
|
+
? 'error'
|
|
409
|
+
: diagnostics?.nextSteps.length || diagnostics?.errors.length || diagnostics?.warnings.length
|
|
410
|
+
? 'partial_success'
|
|
411
|
+
: 'success'
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
status,
|
|
415
|
+
target: session.target,
|
|
416
|
+
summary: session.summary,
|
|
417
|
+
confirmation: session.confirmation,
|
|
418
|
+
ideExtension: buildIdeExtensionStatus({
|
|
419
|
+
required: session.plan.defaults.extension,
|
|
420
|
+
installed:
|
|
421
|
+
session.plan.defaults.extension && !applyResult.postInstall.manualExtensionInstallNeeded,
|
|
422
|
+
manualRequired: applyResult.postInstall.manualExtensionInstallNeeded,
|
|
423
|
+
}),
|
|
424
|
+
verification,
|
|
425
|
+
result: buildExecutionResult(session, applyResult),
|
|
426
|
+
diagnostics,
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function buildDeferredOnboardResult(
|
|
431
|
+
session: ResolvedOnboardingSession,
|
|
432
|
+
): OnboardCommandResult {
|
|
433
|
+
return buildPreApplyResult(session.status, session)
|
|
434
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuildToolDetection,
|
|
3
|
+
OnboardingTargetCandidate,
|
|
4
|
+
OnboardingTargetResolution,
|
|
5
|
+
} from '../types.js'
|
|
6
|
+
|
|
7
|
+
export interface ResolveOnboardingTargetInput {
|
|
8
|
+
repoRoot: string
|
|
9
|
+
buildTools: BuildToolDetection[]
|
|
10
|
+
frameworkSupportByPackage: Record<string, string[]>
|
|
11
|
+
selectedPackagePath?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RankedCandidate {
|
|
15
|
+
candidate: OnboardingTargetCandidate
|
|
16
|
+
score: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizePackagePath(packagePath?: string): string {
|
|
20
|
+
if (!packagePath || packagePath === '.') return ''
|
|
21
|
+
return packagePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function looksLikeAppPackage(packagePath: string): boolean {
|
|
25
|
+
if (!packagePath) return true
|
|
26
|
+
return /(^|\/)(app|apps|web|client|frontend|site)(\/|$)/i.test(packagePath)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function looksLikeAuxiliaryPackage(packagePath: string): boolean {
|
|
30
|
+
return /(^|\/)(docs?|example|examples|playground|storybook|demo)(\/|$)/i.test(packagePath)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildCandidates(input: ResolveOnboardingTargetInput): OnboardingTargetCandidate[] {
|
|
34
|
+
return input.buildTools.map(buildTool => {
|
|
35
|
+
const packagePath = normalizePackagePath(buildTool.packagePath)
|
|
36
|
+
return {
|
|
37
|
+
packagePath,
|
|
38
|
+
configPath: buildTool.configPath,
|
|
39
|
+
buildTool: buildTool.tool,
|
|
40
|
+
frameworks: input.frameworkSupportByPackage[packagePath] ?? [],
|
|
41
|
+
automaticInjection: true,
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function rankCandidate(candidate: OnboardingTargetCandidate): number {
|
|
47
|
+
let score = 0
|
|
48
|
+
|
|
49
|
+
if (candidate.frameworks.length > 0) score += 4
|
|
50
|
+
if (candidate.automaticInjection) score += 2
|
|
51
|
+
if (looksLikeAppPackage(candidate.packagePath)) score += 1
|
|
52
|
+
if (looksLikeAuxiliaryPackage(candidate.packagePath)) score -= 2
|
|
53
|
+
|
|
54
|
+
return score
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rankCandidates(candidates: OnboardingTargetCandidate[]): RankedCandidate[] {
|
|
58
|
+
return candidates
|
|
59
|
+
.map(candidate => ({
|
|
60
|
+
candidate,
|
|
61
|
+
score: rankCandidate(candidate),
|
|
62
|
+
}))
|
|
63
|
+
.sort((left, right) => right.score - left.score)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveOnboardingTarget(
|
|
67
|
+
input: ResolveOnboardingTargetInput,
|
|
68
|
+
): OnboardingTargetResolution {
|
|
69
|
+
const candidates = buildCandidates(input)
|
|
70
|
+
|
|
71
|
+
if (candidates.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
status: 'needs_selection',
|
|
74
|
+
candidates,
|
|
75
|
+
reason: 'No supported targets were detected.',
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const explicitlySelected = normalizePackagePath(input.selectedPackagePath)
|
|
80
|
+
if (input.selectedPackagePath !== undefined) {
|
|
81
|
+
const selected = candidates.find(candidate => candidate.packagePath === explicitlySelected)
|
|
82
|
+
if (selected) {
|
|
83
|
+
return {
|
|
84
|
+
status: 'resolved',
|
|
85
|
+
selected,
|
|
86
|
+
candidates,
|
|
87
|
+
reason: `Using the explicitly selected target: ${selected.packagePath || '.'}.`,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (candidates.length === 1) {
|
|
93
|
+
return {
|
|
94
|
+
status: 'resolved',
|
|
95
|
+
selected: candidates[0],
|
|
96
|
+
candidates,
|
|
97
|
+
reason: 'Only one supported target was detected.',
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ranked = rankCandidates(candidates)
|
|
102
|
+
if (ranked.length > 1 && ranked[0]!.score === ranked[1]!.score) {
|
|
103
|
+
return {
|
|
104
|
+
status: 'needs_selection',
|
|
105
|
+
candidates,
|
|
106
|
+
reason: 'Multiple supported targets look equally plausible.',
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
status: 'resolved',
|
|
112
|
+
selected: ranked[0]!.candidate,
|
|
113
|
+
candidates,
|
|
114
|
+
reason: `Preselected ${ranked[0]!.candidate.packagePath || '.'} because it has the strongest supported app signal.`,
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/prompts.ts
CHANGED
|
@@ -45,19 +45,21 @@ export async function promptProviderChoice(
|
|
|
45
45
|
type: 'select',
|
|
46
46
|
name: 'choice',
|
|
47
47
|
message: 'Detected multiple providers, please choose one:',
|
|
48
|
-
choices: detections
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
choices: detections
|
|
49
|
+
.map((d, i) => {
|
|
50
|
+
const modeLabels = d.providerModes.map(mode =>
|
|
51
|
+
mode === 'extension' ? 'VS Code Extension' : 'Terminal CLI',
|
|
52
|
+
)
|
|
53
|
+
const modeStr = modeLabels.join(' & ')
|
|
54
|
+
return {
|
|
55
|
+
title: `${d.label} ${d.supported ? `(supported ${modeStr})` : '(unsupported/limited)'}`,
|
|
56
|
+
value: i,
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.concat({ title: 'Skip (configure later)', value: -1 }),
|
|
58
60
|
})
|
|
59
61
|
|
|
60
|
-
if (choice === undefined) return null
|
|
62
|
+
if (choice === undefined || choice === -1) return null
|
|
61
63
|
return detections[choice]!
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -94,6 +96,47 @@ export async function promptConfigChoice(
|
|
|
94
96
|
return detections[choice]!
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Interactive prompt for choosing the target app when init is run from a monorepo root.
|
|
101
|
+
*/
|
|
102
|
+
export async function promptMonorepoPackageChoice(
|
|
103
|
+
detections: BuildToolDetection[],
|
|
104
|
+
): Promise<string | null> {
|
|
105
|
+
const uniquePackages = Array.from(
|
|
106
|
+
new Map(
|
|
107
|
+
detections
|
|
108
|
+
.filter((d): d is BuildToolDetection & { packagePath: string } => !!d.packagePath)
|
|
109
|
+
.map(d => [d.packagePath, d]),
|
|
110
|
+
).values(),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if (uniquePackages.length === 0) {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!process.stdin.isTTY) {
|
|
118
|
+
log.error('Monorepo root detected, but stdin is not interactive.')
|
|
119
|
+
log.hint(
|
|
120
|
+
'Re-run `inspecto init` inside a specific app directory, or pass --packages <app-path>.',
|
|
121
|
+
)
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { choice } = await prompts({
|
|
126
|
+
type: 'select',
|
|
127
|
+
name: 'choice',
|
|
128
|
+
message: 'Monorepo root detected. Choose the app to initialize:',
|
|
129
|
+
choices: uniquePackages.map((d, i) => ({
|
|
130
|
+
title: `${d.packagePath} (${d.tool})`,
|
|
131
|
+
description: d.configPath,
|
|
132
|
+
value: i,
|
|
133
|
+
})),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (choice === undefined) return null
|
|
137
|
+
return uniquePackages[choice]!.packagePath
|
|
138
|
+
}
|
|
139
|
+
|
|
97
140
|
/**
|
|
98
141
|
* Interactive prompt for continuing with unsupported frameworks.
|
|
99
142
|
*/
|