@inspecto-dev/cli 0.2.0-alpha.6 → 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 +16 -0
- package/README.md +44 -9
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +448 -6
- package/dist/{chunk-PDDFPQJS.js → chunk-FZS2TLXQ.js} +620 -85
- package/dist/index.d.ts +106 -1
- package/dist/index.js +3 -1
- package/package.json +2 -2
- package/src/bin.ts +148 -0
- package/src/commands/apply.ts +5 -1
- package/src/commands/init.ts +60 -23
- package/src/commands/integration-install.ts +452 -0
- package/src/commands/onboard.ts +50 -0
- package/src/index.ts +4 -0
- package/src/inject/ast-injector.ts +15 -4
- package/src/inject/extension.ts +40 -24
- package/src/inject/gitignore.ts +10 -3
- package/src/onboarding/apply.ts +48 -9
- package/src/onboarding/planner.ts +6 -0
- package/src/onboarding/session.ts +434 -0
- package/src/onboarding/target-resolution.ts +116 -0
- package/src/types.ts +89 -0
- package/tests/apply.test.ts +47 -1
- package/tests/init.test.ts +31 -0
- package/tests/install-wrapper.test.ts +76 -0
- package/tests/integration-install.test.ts +294 -0
- package/tests/onboard.test.ts +258 -0
- package/.turbo/turbo-test.log +0 -16
package/src/inject/gitignore.ts
CHANGED
|
@@ -21,6 +21,7 @@ export async function updateGitignore(
|
|
|
21
21
|
root: string,
|
|
22
22
|
shared: boolean,
|
|
23
23
|
dryRun: boolean,
|
|
24
|
+
quiet = false,
|
|
24
25
|
): Promise<void> {
|
|
25
26
|
const gitignorePath = path.join(root, '.gitignore')
|
|
26
27
|
let content = (await readFile(gitignorePath)) ?? ''
|
|
@@ -34,7 +35,9 @@ export async function updateGitignore(
|
|
|
34
35
|
if (!dryRun) {
|
|
35
36
|
await writeFile(gitignorePath, content)
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
if (!quiet) {
|
|
39
|
+
log.success('Updated .gitignore: .inspecto/ is no longer fully ignored')
|
|
40
|
+
}
|
|
38
41
|
return
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -49,10 +52,14 @@ export async function updateGitignore(
|
|
|
49
52
|
content = content.trimEnd() + '\n' + section
|
|
50
53
|
|
|
51
54
|
if (dryRun) {
|
|
52
|
-
|
|
55
|
+
if (!quiet) {
|
|
56
|
+
log.dryRun(`Would update .gitignore with: ${missingRules.join(', ')}`)
|
|
57
|
+
}
|
|
53
58
|
} else {
|
|
54
59
|
await writeFile(gitignorePath, content)
|
|
55
|
-
|
|
60
|
+
if (!quiet) {
|
|
61
|
+
log.success('Updated .gitignore')
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
|
package/src/onboarding/apply.ts
CHANGED
|
@@ -61,6 +61,32 @@ export interface ApplyOnboardingResult {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function shellQuote(value: string): string {
|
|
65
|
+
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveRuntimePackages(): {
|
|
69
|
+
installSpec: string
|
|
70
|
+
installedDependencyNames: string[]
|
|
71
|
+
} {
|
|
72
|
+
const devRepo = process.env.INSPECTO_DEV_REPO
|
|
73
|
+
if (!devRepo) {
|
|
74
|
+
return {
|
|
75
|
+
installSpec: '@inspecto-dev/plugin @inspecto-dev/core',
|
|
76
|
+
installedDependencyNames: ['@inspecto-dev/plugin', '@inspecto-dev/core'],
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const normalizedRepo = path.resolve(devRepo)
|
|
81
|
+
return {
|
|
82
|
+
installSpec: [
|
|
83
|
+
shellQuote(path.join(normalizedRepo, 'packages/plugin')),
|
|
84
|
+
shellQuote(path.join(normalizedRepo, 'packages/core')),
|
|
85
|
+
].join(' '),
|
|
86
|
+
installedDependencyNames: ['@inspecto-dev/plugin', '@inspecto-dev/core'],
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
function resultStatus(nextSteps: string[]): CommandStatus {
|
|
65
91
|
return nextSteps.length > 0 ? 'warning' : 'ok'
|
|
66
92
|
}
|
|
@@ -157,10 +183,8 @@ async function applyOnboardingPlanInternal(
|
|
|
157
183
|
const promptsFileName = input.options.shared ? 'prompts.json' : 'prompts.local.json'
|
|
158
184
|
const settingsPath = path.join(settingsDir, settingsFileName)
|
|
159
185
|
const promptsPath = path.join(settingsDir, promptsFileName)
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
'@inspecto-dev/plugin @inspecto-dev/core',
|
|
163
|
-
)
|
|
186
|
+
const runtimePackages = resolveRuntimePackages()
|
|
187
|
+
const installCmd = getInstallCommand(input.packageManager, runtimePackages.installSpec)
|
|
164
188
|
const nextSteps: string[] = []
|
|
165
189
|
|
|
166
190
|
let installFailed = false
|
|
@@ -178,8 +202,9 @@ async function applyOnboardingPlanInternal(
|
|
|
178
202
|
await shell(installCmd, input.projectRoot)
|
|
179
203
|
spinner.succeed('Dependencies installed successfully')
|
|
180
204
|
reporter.success('Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies')
|
|
181
|
-
|
|
182
|
-
|
|
205
|
+
for (const name of runtimePackages.installedDependencyNames) {
|
|
206
|
+
mutations.push({ type: 'dependency_added', name, dev: true })
|
|
207
|
+
}
|
|
183
208
|
} catch (error: any) {
|
|
184
209
|
spinner.fail('Dependency installation failed')
|
|
185
210
|
installFailed = true
|
|
@@ -193,7 +218,12 @@ async function applyOnboardingPlanInternal(
|
|
|
193
218
|
|
|
194
219
|
let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig)
|
|
195
220
|
for (const target of input.supportedBuildTargets) {
|
|
196
|
-
const result = await injectPlugin(
|
|
221
|
+
const result = await injectPlugin(
|
|
222
|
+
input.repoRoot,
|
|
223
|
+
target,
|
|
224
|
+
input.options.dryRun,
|
|
225
|
+
input.options.quiet ?? false,
|
|
226
|
+
)
|
|
197
227
|
if (result.success) {
|
|
198
228
|
mutations.push(...result.mutations)
|
|
199
229
|
} else {
|
|
@@ -244,7 +274,12 @@ async function applyOnboardingPlanInternal(
|
|
|
244
274
|
}
|
|
245
275
|
|
|
246
276
|
if (!input.options.dryRun) {
|
|
247
|
-
await updateGitignore(
|
|
277
|
+
await updateGitignore(
|
|
278
|
+
input.projectRoot,
|
|
279
|
+
input.options.shared,
|
|
280
|
+
input.options.dryRun,
|
|
281
|
+
input.options.quiet ?? false,
|
|
282
|
+
)
|
|
248
283
|
mutations.push({
|
|
249
284
|
type: 'file_modified',
|
|
250
285
|
path: '.gitignore',
|
|
@@ -270,7 +305,11 @@ async function applyOnboardingPlanInternal(
|
|
|
270
305
|
if (input.options.noExtension) {
|
|
271
306
|
reporter.warn('Skipping IDE extension (--no-extension)')
|
|
272
307
|
} else if (shouldInstallExt) {
|
|
273
|
-
const extMutation = await installExtension(
|
|
308
|
+
const extMutation = await installExtension(
|
|
309
|
+
input.options.dryRun,
|
|
310
|
+
input.selectedIDE?.ide,
|
|
311
|
+
input.options.quiet ?? false,
|
|
312
|
+
)
|
|
274
313
|
if (extMutation && !input.options.dryRun) {
|
|
275
314
|
mutations.push(extMutation)
|
|
276
315
|
|
|
@@ -276,3 +276,9 @@ export function createPlanResult(context: OnboardingContext): PlanResult {
|
|
|
276
276
|
defaults,
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
|
+
|
|
280
|
+
export function planManualFollowUp(result: PlanResult): string[] {
|
|
281
|
+
return result.actions
|
|
282
|
+
.filter(action => action.type === 'manual_step')
|
|
283
|
+
.map(action => action.description)
|
|
284
|
+
}
|
|
@@ -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
|
+
}
|