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