@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
|
@@ -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,62 @@
|
|
|
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
|
+
const extensionReady =
|
|
32
|
+
!result.ideExtension?.required ||
|
|
33
|
+
(result.ideExtension.installed && !result.ideExtension.manualRequired)
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
extensionReady &&
|
|
37
|
+
(result.status === 'success' || result.status === 'partial_success') &&
|
|
38
|
+
result.verification?.message
|
|
39
|
+
) {
|
|
40
|
+
log.info(result.verification.message)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function onboard(options: OnboardCommandOptions = {}): Promise<OnboardCommandResult> {
|
|
45
|
+
const root = process.cwd()
|
|
46
|
+
const session = await resolveOnboardingSession(root, options)
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
session.status === 'error' ||
|
|
50
|
+
session.status === 'needs_target_selection' ||
|
|
51
|
+
session.status === 'needs_confirmation'
|
|
52
|
+
) {
|
|
53
|
+
return writeCommandOutput(
|
|
54
|
+
buildDeferredOnboardResult(session),
|
|
55
|
+
options.json ?? false,
|
|
56
|
+
printOnboardResult,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = await applyResolvedOnboardingSession(session, options)
|
|
61
|
+
return writeCommandOutput(result, options.json ?? false, printOnboardResult)
|
|
62
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { apply } from './commands/apply.js'
|
|
|
2
2
|
export { detect } from './commands/detect.js'
|
|
3
3
|
export { init } from './commands/init.js'
|
|
4
4
|
export { collectDoctorResult, doctor } from './commands/doctor.js'
|
|
5
|
+
export { onboard } from './commands/onboard.js'
|
|
5
6
|
export { plan } from './commands/plan.js'
|
|
6
7
|
export { teardown } from './commands/teardown.js'
|
|
7
8
|
export { writeCommandOutput, reportCommandError } from './utils/output.js'
|
|
@@ -12,5 +13,8 @@ export type {
|
|
|
12
13
|
InstallLock,
|
|
13
14
|
DoctorDiagnostic,
|
|
14
15
|
DoctorResult,
|
|
16
|
+
OnboardStatus,
|
|
17
|
+
OnboardCommandResult,
|
|
18
|
+
ResolvedOnboardingSession,
|
|
15
19
|
} from './types.js'
|
|
16
20
|
export type { Framework } from './detect/framework.js'
|
|
@@ -25,7 +25,9 @@ function printManualInstructions(
|
|
|
25
25
|
strategy: InjectStrategy | undefined,
|
|
26
26
|
detection: BuildToolDetection,
|
|
27
27
|
reason: string,
|
|
28
|
+
quiet = false,
|
|
28
29
|
) {
|
|
30
|
+
if (quiet) return
|
|
29
31
|
log.warn(`Could not automatically configure ${detection.configPath}`)
|
|
30
32
|
log.hint(`(reason: ${reason})`)
|
|
31
33
|
log.blank()
|
|
@@ -73,6 +75,7 @@ export async function injectPlugin(
|
|
|
73
75
|
root: string,
|
|
74
76
|
detection: BuildToolDetection,
|
|
75
77
|
dryRun: boolean,
|
|
78
|
+
quiet = false,
|
|
76
79
|
): Promise<InjectionResult> {
|
|
77
80
|
const configPath = path.join(root, detection.configPath)
|
|
78
81
|
const mutations: Mutation[] = []
|
|
@@ -82,13 +85,15 @@ export async function injectPlugin(
|
|
|
82
85
|
// Step 1: Read config file to check existence and idempotency
|
|
83
86
|
const content = await readFile(configPath)
|
|
84
87
|
if (!content) {
|
|
85
|
-
printManualInstructions(strategy, detection, 'config file not readable')
|
|
88
|
+
printManualInstructions(strategy, detection, 'config file not readable', quiet)
|
|
86
89
|
return { success: false, mutations, failureReason: 'config file not readable' }
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
// Step 2: Idempotency check
|
|
90
93
|
if (isAlreadyInjected(content)) {
|
|
91
|
-
|
|
94
|
+
if (!quiet) {
|
|
95
|
+
log.success(`Plugin already configured in ${detection.configPath} (skipped)`)
|
|
96
|
+
}
|
|
92
97
|
|
|
93
98
|
mutations.push({
|
|
94
99
|
type: 'file_modified',
|
|
@@ -104,13 +109,16 @@ export async function injectPlugin(
|
|
|
104
109
|
strategy,
|
|
105
110
|
detection,
|
|
106
111
|
`No injection strategy found for ${detection.tool}`,
|
|
112
|
+
quiet,
|
|
107
113
|
)
|
|
108
114
|
return { success: false, mutations, failureReason: 'No strategy found' }
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
// Step 3: Automatic configuration
|
|
112
118
|
if (dryRun) {
|
|
113
|
-
|
|
119
|
+
if (!quiet) {
|
|
120
|
+
log.dryRun(`Would automatically configure plugin in ${detection.configPath}`)
|
|
121
|
+
}
|
|
114
122
|
return { success: true, mutations: [] }
|
|
115
123
|
}
|
|
116
124
|
|
|
@@ -132,7 +140,9 @@ export async function injectPlugin(
|
|
|
132
140
|
description: 'Automatically configured inspecto() plugin',
|
|
133
141
|
})
|
|
134
142
|
|
|
135
|
-
|
|
143
|
+
if (!quiet) {
|
|
144
|
+
log.success(`Configured plugin in ${detection.configPath}`)
|
|
145
|
+
}
|
|
136
146
|
return { success: true, mutations }
|
|
137
147
|
} catch (err) {
|
|
138
148
|
// Graceback degradation
|
|
@@ -140,6 +150,7 @@ export async function injectPlugin(
|
|
|
140
150
|
strategy,
|
|
141
151
|
detection,
|
|
142
152
|
`Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
153
|
+
quiet,
|
|
143
154
|
)
|
|
144
155
|
return {
|
|
145
156
|
success: false,
|