@inspecto-dev/cli 0.2.0-alpha.4 → 0.2.0-alpha.6

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 (41) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-test.log +16 -21
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +58 -11
  5. package/bin/inspecto.js +5 -1
  6. package/dist/bin.d.ts +5 -1
  7. package/dist/bin.js +89 -50
  8. package/dist/{chunk-EUCQCD3Y.js → chunk-PDDFPQJS.js} +1954 -1053
  9. package/dist/index.d.ts +128 -2
  10. package/dist/index.js +15 -3
  11. package/package.json +2 -1
  12. package/src/bin.ts +139 -67
  13. package/src/commands/apply.ts +114 -0
  14. package/src/commands/detect.ts +59 -0
  15. package/src/commands/doctor.ts +225 -72
  16. package/src/commands/init.ts +106 -183
  17. package/src/commands/plan.ts +41 -0
  18. package/src/detect/build-tool.ts +107 -3
  19. package/src/index.ts +13 -2
  20. package/src/inject/ast-injector.ts +20 -9
  21. package/src/inject/extension.ts +3 -1
  22. package/src/inject/strategies/vite.ts +2 -1
  23. package/src/instructions.ts +60 -46
  24. package/src/onboarding/apply.ts +325 -0
  25. package/src/onboarding/context.ts +36 -0
  26. package/src/onboarding/planner.ts +278 -0
  27. package/src/prompts.ts +54 -11
  28. package/src/types.ts +95 -0
  29. package/src/utils/fs.ts +2 -1
  30. package/src/utils/logger.ts +9 -0
  31. package/src/utils/output.ts +40 -0
  32. package/tests/apply.test.ts +537 -0
  33. package/tests/ast-injector.test.ts +50 -0
  34. package/tests/build-tool.test.ts +3 -5
  35. package/tests/detect.test.ts +94 -0
  36. package/tests/doctor.test.ts +224 -0
  37. package/tests/init.test.ts +333 -0
  38. package/tests/instructions.test.ts +61 -0
  39. package/tests/logger.test.ts +100 -0
  40. package/tests/plan.test.ts +713 -0
  41. package/tests/workspace-build-tool.test.ts +75 -0
@@ -0,0 +1,278 @@
1
+ import { buildOnboardingContext } from './context.js'
2
+ import type {
3
+ CommandMessage,
4
+ CommandStatus,
5
+ DetectionResult,
6
+ OnboardingContext,
7
+ PlanResult,
8
+ } from '../types.js'
9
+
10
+ function message(code: string, message: string): CommandMessage {
11
+ return { code, message }
12
+ }
13
+
14
+ function uniqueMessages(messages: CommandMessage[]): CommandMessage[] {
15
+ const seen = new Set<string>()
16
+ return messages.filter(item => {
17
+ const key = `${item.code}:${item.message}`
18
+ if (seen.has(key)) return false
19
+ seen.add(key)
20
+ return true
21
+ })
22
+ }
23
+
24
+ function detectionStatus(warnings: CommandMessage[], blockers: CommandMessage[]): CommandStatus {
25
+ if (blockers.length > 0) return 'blocked'
26
+ if (warnings.length > 0) return 'warning'
27
+ return 'ok'
28
+ }
29
+
30
+ function planStatus(warnings: CommandMessage[], blockers: CommandMessage[]): CommandStatus {
31
+ if (blockers.length > 0) return 'blocked'
32
+ if (warnings.length > 0) return 'warning'
33
+ return 'ok'
34
+ }
35
+
36
+ function supportedIde(context: OnboardingContext): string | undefined {
37
+ return context.ides.find(ide => ide.supported)?.ide
38
+ }
39
+
40
+ function supportedProvider(context: OnboardingContext): string | undefined {
41
+ return context.providers.find(provider => provider.supported)?.id
42
+ }
43
+
44
+ function buildToolBlockers(context: OnboardingContext): CommandMessage[] {
45
+ if (context.buildTools.unsupported.length > 0) {
46
+ return [
47
+ message(
48
+ 'unsupported-build-tool',
49
+ `Detected unsupported build tool(s): ${context.buildTools.unsupported.join(', ')}`,
50
+ ),
51
+ ]
52
+ }
53
+
54
+ if (context.buildTools.supported.length > 0) {
55
+ if (context.buildTools.supported.length === 1) {
56
+ return []
57
+ }
58
+
59
+ const targets = context.buildTools.supported
60
+ .map(target => target.packagePath ?? target.configPath)
61
+ .join(', ')
62
+
63
+ return [
64
+ message(
65
+ 'multiple-supported-build-targets',
66
+ `Multiple supported build targets detected: ${targets}. Run inspecto apply from a single app/package root until explicit target selection is available.`,
67
+ ),
68
+ ]
69
+ }
70
+
71
+ return [message('missing-build-tool', 'No supported build tool detected')]
72
+ }
73
+
74
+ function frameworkBlockers(context: OnboardingContext): CommandMessage[] {
75
+ if (context.frameworks.supported.length > 0) {
76
+ return []
77
+ }
78
+
79
+ if (context.frameworks.unsupported.length > 0) {
80
+ return [
81
+ message(
82
+ 'unsupported-framework',
83
+ `Detected unsupported framework(s): ${context.frameworks.unsupported.join(', ')}`,
84
+ ),
85
+ ]
86
+ }
87
+
88
+ return [message('missing-framework', 'No supported frontend framework detected')]
89
+ }
90
+
91
+ function unsupportedEnvironmentWarnings(context: OnboardingContext): CommandMessage[] {
92
+ const warnings: CommandMessage[] = []
93
+
94
+ if (context.frameworks.unsupported.length > 0 && context.frameworks.supported.length > 0) {
95
+ warnings.push(
96
+ message(
97
+ 'unsupported-framework-present',
98
+ `Unsupported framework(s) also detected: ${context.frameworks.unsupported.join(', ')}`,
99
+ ),
100
+ )
101
+ }
102
+
103
+ const unsupportedIdes = context.ides.filter(ide => !ide.supported).map(ide => ide.ide)
104
+ if (unsupportedIdes.length > 0) {
105
+ warnings.push(
106
+ message('unsupported-ide', `Unsupported IDE(s) detected: ${unsupportedIdes.join(', ')}`),
107
+ )
108
+ }
109
+
110
+ const unsupportedProviders = context.providers
111
+ .filter(provider => !provider.supported)
112
+ .map(provider => provider.label)
113
+ if (unsupportedProviders.length > 0) {
114
+ warnings.push(
115
+ message(
116
+ 'unsupported-provider',
117
+ `Unsupported provider(s) detected: ${unsupportedProviders.join(', ')}`,
118
+ ),
119
+ )
120
+ }
121
+
122
+ return warnings
123
+ }
124
+
125
+ function manualBuildToolActions(context: OnboardingContext): PlanResult['actions'] {
126
+ if (context.buildTools.unsupported.length > 0) {
127
+ return [
128
+ {
129
+ type: 'manual_step',
130
+ target: context.buildTools.unsupported.join(', '),
131
+ description:
132
+ 'Inspecto cannot auto-configure this build stack yet. Follow the manual setup guide for the detected framework or build tool.',
133
+ },
134
+ ]
135
+ }
136
+
137
+ if (context.buildTools.supported.length > 1) {
138
+ const targets = context.buildTools.supported
139
+ .map(target => target.packagePath ?? target.configPath)
140
+ .join(', ')
141
+
142
+ return [
143
+ {
144
+ type: 'manual_step',
145
+ target: targets,
146
+ description:
147
+ 'Run inspecto apply from the target app/package root. Root-level apply is blocked when multiple supported targets are detected.',
148
+ },
149
+ ]
150
+ }
151
+
152
+ return [
153
+ {
154
+ type: 'manual_step',
155
+ target: context.root,
156
+ description:
157
+ 'No supported build tool was detected. Add a supported build config before trying Inspecto again.',
158
+ },
159
+ ]
160
+ }
161
+
162
+ function manualFrameworkActions(context: OnboardingContext): PlanResult['actions'] {
163
+ if (context.frameworks.unsupported.length > 0) {
164
+ return [
165
+ {
166
+ type: 'manual_step',
167
+ target: context.frameworks.unsupported.join(', '),
168
+ description:
169
+ 'Inspecto cannot auto-configure this framework yet. Follow the manual setup guide for the detected framework.',
170
+ },
171
+ ]
172
+ }
173
+
174
+ return [
175
+ {
176
+ type: 'manual_step',
177
+ target: context.root,
178
+ description:
179
+ 'No supported frontend framework was detected. Add a supported React or Vue app before trying Inspecto again.',
180
+ },
181
+ ]
182
+ }
183
+
184
+ export async function createDetectionResult(root: string): Promise<DetectionResult> {
185
+ const context = await buildOnboardingContext(root)
186
+ const warnings = uniqueMessages([...unsupportedEnvironmentWarnings(context)])
187
+
188
+ const buildToolResult = buildToolBlockers(context)
189
+ const frameworkResult = frameworkBlockers(context)
190
+ const blockers = uniqueMessages([...buildToolResult, ...frameworkResult])
191
+
192
+ return {
193
+ status: detectionStatus(warnings, blockers),
194
+ warnings,
195
+ blockers,
196
+ project: {
197
+ root: context.root,
198
+ packageManager: context.packageManager,
199
+ },
200
+ environment: {
201
+ frameworks: context.frameworks.supported,
202
+ unsupportedFrameworks: context.frameworks.unsupported,
203
+ buildTools: context.buildTools.supported,
204
+ unsupportedBuildTools: context.buildTools.unsupported,
205
+ ides: context.ides,
206
+ providers: context.providers,
207
+ },
208
+ }
209
+ }
210
+
211
+ export function createPlanResult(context: OnboardingContext): PlanResult {
212
+ const warnings = uniqueMessages(unsupportedEnvironmentWarnings(context))
213
+ const blockers = uniqueMessages([...buildToolBlockers(context), ...frameworkBlockers(context)])
214
+ const actions: PlanResult['actions'] = []
215
+
216
+ let strategy: PlanResult['strategy'] = 'supported'
217
+
218
+ if (blockers.length > 0) {
219
+ strategy = 'manual'
220
+ if (
221
+ context.buildTools.unsupported.length > 0 ||
222
+ context.buildTools.supported.length === 0 ||
223
+ context.buildTools.supported.length > 1
224
+ ) {
225
+ actions.push(...manualBuildToolActions(context))
226
+ }
227
+ if (frameworkBlockers(context).length > 0) {
228
+ actions.push(...manualFrameworkActions(context))
229
+ }
230
+ } else {
231
+ actions.push({
232
+ type: 'install_dependency',
233
+ target: '@inspecto-dev/plugin @inspecto-dev/core',
234
+ description: `Install the Inspecto runtime packages with ${context.packageManager}.`,
235
+ })
236
+
237
+ for (const buildTool of context.buildTools.supported) {
238
+ actions.push({
239
+ type: 'modify_file',
240
+ target: buildTool.configPath,
241
+ description: `Inject the Inspecto plugin into ${buildTool.label}.`,
242
+ })
243
+ }
244
+
245
+ const ide = supportedIde(context)
246
+ if (ide === 'vscode') {
247
+ actions.push({
248
+ type: 'install_extension',
249
+ target: 'vscode',
250
+ description: 'Install the Inspecto VS Code extension.',
251
+ })
252
+ }
253
+ }
254
+
255
+ const defaults: PlanResult['defaults'] = {
256
+ shared: false,
257
+ extension: supportedIde(context) === 'vscode',
258
+ }
259
+
260
+ const provider = supportedProvider(context)
261
+ if (provider) {
262
+ defaults.provider = provider
263
+ }
264
+
265
+ const ide = supportedIde(context)
266
+ if (ide) {
267
+ defaults.ide = ide
268
+ }
269
+
270
+ return {
271
+ status: planStatus(warnings, blockers),
272
+ warnings,
273
+ blockers,
274
+ strategy,
275
+ actions,
276
+ defaults,
277
+ }
278
+ }
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.map((d, i) => {
49
- const modeLabels = d.providerModes.map(mode =>
50
- mode === 'extension' ? 'VS Code Extension' : 'Terminal CLI',
51
- )
52
- const modeStr = modeLabels.join(' & ')
53
- return {
54
- title: `${d.label} ${d.supported ? `(supported ${modeStr})` : '(unsupported/limited)'}`,
55
- value: i,
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
  */
package/src/types.ts CHANGED
@@ -8,6 +8,22 @@ export type PackageManager = 'bun' | 'pnpm' | 'yarn' | 'npm'
8
8
  /** Supported build tools (v1) */
9
9
  export type BuildTool = 'vite' | 'webpack' | 'rspack' | 'rsbuild' | 'esbuild' | 'rollup'
10
10
 
11
+ /** Machine-readable status for onboarding commands */
12
+ export type CommandStatus = 'ok' | 'warning' | 'blocked' | 'error'
13
+
14
+ /** Structured message emitted by onboarding commands */
15
+ export interface CommandMessage {
16
+ code: string
17
+ message: string
18
+ }
19
+
20
+ export interface OnboardingProvider {
21
+ id: string
22
+ label: string
23
+ supported: boolean
24
+ preferredMode: 'cli' | 'extension'
25
+ }
26
+
11
27
  /** Detected build tool with its config path */
12
28
  export interface BuildToolDetection {
13
29
  tool: BuildTool
@@ -22,6 +38,85 @@ export interface BuildToolDetection {
22
38
  packagePath?: string
23
39
  }
24
40
 
41
+ /** Normalized onboarding context shared by detection/planning */
42
+ export interface OnboardingContext {
43
+ root: string
44
+ packageManager: PackageManager
45
+ buildTools: {
46
+ supported: BuildToolDetection[]
47
+ unsupported: string[]
48
+ }
49
+ frameworks: {
50
+ supported: string[]
51
+ unsupported: string[]
52
+ }
53
+ ides: Array<{ ide: string; supported: boolean }>
54
+ providers: OnboardingProvider[]
55
+ }
56
+
57
+ /** Machine-readable detection output for skill-first onboarding */
58
+ export interface DetectionResult {
59
+ status: CommandStatus
60
+ warnings: CommandMessage[]
61
+ blockers: CommandMessage[]
62
+ project: {
63
+ root: string
64
+ packageManager: PackageManager
65
+ }
66
+ environment: {
67
+ frameworks: string[]
68
+ unsupportedFrameworks: string[]
69
+ buildTools: BuildToolDetection[]
70
+ unsupportedBuildTools: string[]
71
+ ides: Array<{ ide: string; supported: boolean }>
72
+ providers: OnboardingProvider[]
73
+ }
74
+ }
75
+
76
+ /** Machine-readable onboarding plan output */
77
+ export interface PlanResult {
78
+ status: CommandStatus
79
+ warnings: CommandMessage[]
80
+ blockers: CommandMessage[]
81
+ strategy: 'supported' | 'manual' | 'unsupported'
82
+ actions: Array<{
83
+ type: 'install_dependency' | 'modify_file' | 'install_extension' | 'manual_step'
84
+ target: string
85
+ description: string
86
+ }>
87
+ defaults: {
88
+ provider?: string
89
+ ide?: string
90
+ shared: boolean
91
+ extension: boolean
92
+ }
93
+ }
94
+
95
+ /** A single doctor diagnostic check/result */
96
+ export interface DoctorDiagnostic {
97
+ code: string
98
+ status: 'ok' | 'warning' | 'error'
99
+ message: string
100
+ hints: string[]
101
+ details?: Record<string, unknown>
102
+ }
103
+
104
+ /** Machine-readable diagnostics output for `inspecto doctor` */
105
+ export interface DoctorResult {
106
+ status: CommandStatus
107
+ summary: {
108
+ errors: number
109
+ warnings: number
110
+ }
111
+ project: {
112
+ root: string
113
+ packageManager?: PackageManager
114
+ }
115
+ errors: DoctorDiagnostic[]
116
+ warnings: DoctorDiagnostic[]
117
+ checks: DoctorDiagnostic[]
118
+ }
119
+
25
120
  /** Options passed to `inspecto init` */
26
121
  export interface InitOptions {
27
122
  shared: boolean
package/src/utils/fs.ts CHANGED
@@ -55,7 +55,8 @@ export async function removeDir(dirPath: string): Promise<void> {
55
55
  /** Read and parse JSON file */
56
56
  export async function readJSON<T = unknown>(filePath: string): Promise<T | null> {
57
57
  const text = await readFile(filePath)
58
- if (!text) return null
58
+ if (text === null) return null
59
+ if (text.trim() === '') return {} as T
59
60
  try {
60
61
  return JSON.parse(text) as T
61
62
  } catch {
@@ -57,6 +57,15 @@ export const log = {
57
57
  console.log()
58
58
  },
59
59
 
60
+ /** Copy-friendly code block without box characters */
61
+ copyableCodeBlock(lines: string[]) {
62
+ console.log()
63
+ for (const line of lines) {
64
+ console.log(` ${line}`)
65
+ }
66
+ console.log()
67
+ },
68
+
60
69
  /** Dry-run prefix */
61
70
  dryRun(text: string) {
62
71
  console.log(` ${pc.blue('[dry-run]')} ${text}`)
@@ -0,0 +1,40 @@
1
+ import { log } from './logger.js'
2
+
3
+ interface ReportCommandErrorOptions {
4
+ debug?: boolean
5
+ json?: boolean
6
+ }
7
+
8
+ export function writeCommandOutput<T>(result: T, json: boolean, renderText: (value: T) => void): T {
9
+ if (json) {
10
+ console.log(JSON.stringify(result, null, 2))
11
+ return result
12
+ }
13
+
14
+ renderText(result)
15
+ return result
16
+ }
17
+
18
+ export function reportCommandError(error: unknown, options: ReportCommandErrorOptions = {}): void {
19
+ const message = error instanceof Error ? error.message : String(error)
20
+ const stack = error instanceof Error ? error.stack : undefined
21
+
22
+ if (options.json) {
23
+ const payload = {
24
+ status: 'error' as const,
25
+ error: {
26
+ message,
27
+ ...(options.debug && stack ? { stack } : {}),
28
+ },
29
+ }
30
+
31
+ console.error(JSON.stringify(payload, null, 2))
32
+ return
33
+ }
34
+
35
+ log.error(message)
36
+
37
+ if (options.debug && stack) {
38
+ console.error(stack)
39
+ }
40
+ }