@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.
@@ -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
- log.success(`Plugin already configured in ${detection.configPath} (skipped)`)
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
- log.dryRun(`Would automatically configure plugin in ${detection.configPath}`)
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
- log.success(`Configured plugin in ${detection.configPath}`)
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,