@inspecto-dev/cli 0.2.0-alpha.6 → 0.3.0
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-IBYH7QZM.js} +624 -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 +62 -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 +352 -0
- package/.turbo/turbo-test.log +0 -16
package/src/inject/extension.ts
CHANGED
|
@@ -69,9 +69,15 @@ async function tryOpenURI(uri: string): Promise<boolean> {
|
|
|
69
69
|
/**
|
|
70
70
|
* Attempt to install the VS Code extension using waterfall degradation.
|
|
71
71
|
*/
|
|
72
|
-
export async function installExtension(
|
|
72
|
+
export async function installExtension(
|
|
73
|
+
dryRun: boolean,
|
|
74
|
+
ide?: string,
|
|
75
|
+
quiet = false,
|
|
76
|
+
): Promise<Mutation | null> {
|
|
73
77
|
if (dryRun) {
|
|
74
|
-
|
|
78
|
+
if (!quiet) {
|
|
79
|
+
log.dryRun('Would attempt to install VS Code extension')
|
|
80
|
+
}
|
|
75
81
|
return null
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -82,7 +88,9 @@ export async function installExtension(dryRun: boolean, ide?: string): Promise<M
|
|
|
82
88
|
if (await which('code')) {
|
|
83
89
|
try {
|
|
84
90
|
await run('code', ['--install-extension', EXTENSION_ID])
|
|
85
|
-
|
|
91
|
+
if (!quiet) {
|
|
92
|
+
log.success('VS Code extension installed via CLI')
|
|
93
|
+
}
|
|
86
94
|
return { type: 'extension_installed', id: EXTENSION_ID }
|
|
87
95
|
} catch {
|
|
88
96
|
// Fall through to next level
|
|
@@ -94,10 +102,12 @@ export async function installExtension(dryRun: boolean, ide?: string): Promise<M
|
|
|
94
102
|
if (codePath) {
|
|
95
103
|
try {
|
|
96
104
|
await run(codePath, ['--install-extension', EXTENSION_ID])
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
if (!quiet) {
|
|
106
|
+
log.success('VS Code extension installed via binary path')
|
|
107
|
+
log.info(
|
|
108
|
+
'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future',
|
|
109
|
+
)
|
|
110
|
+
}
|
|
101
111
|
return { type: 'extension_installed', id: EXTENSION_ID }
|
|
102
112
|
} catch {
|
|
103
113
|
// Fall through to next level
|
|
@@ -107,31 +117,37 @@ export async function installExtension(dryRun: boolean, ide?: string): Promise<M
|
|
|
107
117
|
// Level 3: URI scheme
|
|
108
118
|
const uri = `vscode:extension/${EXTENSION_ID}`
|
|
109
119
|
if (await tryOpenURI(uri)) {
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
if (!quiet) {
|
|
121
|
+
log.warn('Opened extension page in VS Code')
|
|
122
|
+
log.hint('Please click "Install" in the opened VS Code window to complete setup.')
|
|
123
|
+
}
|
|
112
124
|
return { type: 'extension_installed', id: EXTENSION_ID, manual_action_required: true }
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
// Level 4: Manual fallback
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
if (!quiet) {
|
|
129
|
+
log.warn('Could not auto-install VS Code extension')
|
|
130
|
+
log.hint('Please install it manually to enable Inspector features:')
|
|
131
|
+
log.hint(' 1. Open VS Code')
|
|
132
|
+
log.hint(' 2. Press Ctrl+Shift+X (or Cmd+Shift+X)')
|
|
133
|
+
log.hint(' 3. Search for "Inspecto"')
|
|
134
|
+
log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`)
|
|
135
|
+
}
|
|
122
136
|
return null
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
// Other IDEs: Prompt to install via VSIX
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
if (!quiet) {
|
|
141
|
+
log.warn(`Could not auto-install extension for ${ide}`)
|
|
142
|
+
log.hint('Please install it manually to enable Inspector features:')
|
|
143
|
+
log.hint(
|
|
144
|
+
' 1. Download the latest .vsix file (Open VSX: https://open-vsx.org/extension/inspecto/inspecto)',
|
|
145
|
+
)
|
|
146
|
+
log.hint(` 2. Open ${ide}`)
|
|
147
|
+
log.hint(' 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)')
|
|
148
|
+
log.hint(' 4. Type and select "Extensions: Install from VSIX..."')
|
|
149
|
+
log.hint(' 5. Select the downloaded .vsix file')
|
|
150
|
+
}
|
|
135
151
|
return null
|
|
136
152
|
}
|
|
137
153
|
|
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
|
+
}
|