@orchid-labs/pluxx 0.1.1 → 0.1.4

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 (103) hide show
  1. package/README.md +25 -8
  2. package/bin/pluxx.js +19 -28
  3. package/dist/agents.d.ts +16 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/cli/agent.d.ts +62 -0
  6. package/dist/cli/agent.d.ts.map +1 -1
  7. package/dist/cli/doctor.d.ts +2 -0
  8. package/dist/cli/doctor.d.ts.map +1 -1
  9. package/dist/cli/entry.d.ts +2 -0
  10. package/dist/cli/entry.d.ts.map +1 -0
  11. package/dist/cli/index.d.ts +7 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +21810 -0
  14. package/dist/cli/init-from-mcp.d.ts +17 -1
  15. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  16. package/dist/cli/install.d.ts +1 -0
  17. package/dist/cli/install.d.ts.map +1 -1
  18. package/dist/cli/lint.d.ts +3 -1
  19. package/dist/cli/lint.d.ts.map +1 -1
  20. package/dist/cli/mcp-proxy.d.ts.map +1 -1
  21. package/dist/cli/migrate.d.ts.map +1 -1
  22. package/dist/cli/primitive-summary.d.ts +14 -0
  23. package/dist/cli/primitive-summary.d.ts.map +1 -0
  24. package/dist/cli/prompt.d.ts +1 -1
  25. package/dist/cli/publish.d.ts +6 -1
  26. package/dist/cli/publish.d.ts.map +1 -1
  27. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  28. package/dist/cli/verify-install.d.ts +25 -0
  29. package/dist/cli/verify-install.d.ts.map +1 -0
  30. package/dist/commands.d.ts +10 -0
  31. package/dist/commands.d.ts.map +1 -0
  32. package/dist/compiler-intent.d.ts +165 -0
  33. package/dist/compiler-intent.d.ts.map +1 -0
  34. package/dist/config/load.d.ts.map +1 -1
  35. package/dist/delegation.d.ts +11 -0
  36. package/dist/delegation.d.ts.map +1 -0
  37. package/dist/generators/amp/index.d.ts.map +1 -1
  38. package/dist/generators/base.d.ts +5 -0
  39. package/dist/generators/base.d.ts.map +1 -1
  40. package/dist/generators/claude-code/index.d.ts.map +1 -1
  41. package/dist/generators/cline/index.d.ts.map +1 -1
  42. package/dist/generators/codex/index.d.ts +4 -0
  43. package/dist/generators/codex/index.d.ts.map +1 -1
  44. package/dist/generators/cursor/index.d.ts +1 -0
  45. package/dist/generators/cursor/index.d.ts.map +1 -1
  46. package/dist/generators/gemini-cli/index.d.ts.map +1 -1
  47. package/dist/generators/github-copilot/index.d.ts.map +1 -1
  48. package/dist/generators/opencode/index.d.ts +1 -0
  49. package/dist/generators/opencode/index.d.ts.map +1 -1
  50. package/dist/generators/openhands/index.d.ts.map +1 -1
  51. package/dist/generators/roo-code/index.d.ts.map +1 -1
  52. package/dist/generators/shared/claude-family.d.ts.map +1 -1
  53. package/dist/generators/warp/index.d.ts.map +1 -1
  54. package/dist/index.d.ts +4 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +5371 -553
  57. package/dist/schema.d.ts +91 -42
  58. package/dist/schema.d.ts.map +1 -1
  59. package/dist/text-files.d.ts +5 -0
  60. package/dist/text-files.d.ts.map +1 -0
  61. package/dist/validation/platform-rules.d.ts +15 -1
  62. package/dist/validation/platform-rules.d.ts.map +1 -1
  63. package/package.json +15 -13
  64. package/src/cli/agent.ts +0 -1455
  65. package/src/cli/dev.ts +0 -112
  66. package/src/cli/doctor.ts +0 -987
  67. package/src/cli/eval.ts +0 -470
  68. package/src/cli/index.ts +0 -2933
  69. package/src/cli/init-from-mcp.ts +0 -2115
  70. package/src/cli/install.ts +0 -860
  71. package/src/cli/lint.ts +0 -1249
  72. package/src/cli/mcp-proxy.ts +0 -322
  73. package/src/cli/migrate.ts +0 -867
  74. package/src/cli/prompt.ts +0 -82
  75. package/src/cli/publish.ts +0 -401
  76. package/src/cli/runtime.ts +0 -86
  77. package/src/cli/sync-from-mcp.ts +0 -586
  78. package/src/cli/test.ts +0 -142
  79. package/src/compatibility/matrix.ts +0 -149
  80. package/src/config/define.ts +0 -20
  81. package/src/config/load.ts +0 -74
  82. package/src/generators/amp/index.ts +0 -63
  83. package/src/generators/base.ts +0 -188
  84. package/src/generators/claude-code/index.ts +0 -172
  85. package/src/generators/cline/index.ts +0 -35
  86. package/src/generators/codex/index.ts +0 -143
  87. package/src/generators/cursor/index.ts +0 -158
  88. package/src/generators/gemini-cli/index.ts +0 -83
  89. package/src/generators/github-copilot/index.ts +0 -32
  90. package/src/generators/hooks-warning.ts +0 -51
  91. package/src/generators/index.ts +0 -71
  92. package/src/generators/opencode/index.ts +0 -526
  93. package/src/generators/openhands/index.ts +0 -32
  94. package/src/generators/roo-code/index.ts +0 -35
  95. package/src/generators/shared/claude-family.ts +0 -215
  96. package/src/generators/warp/index.ts +0 -32
  97. package/src/hook-events.ts +0 -33
  98. package/src/index.ts +0 -34
  99. package/src/mcp/introspect.ts +0 -1107
  100. package/src/permissions.ts +0 -260
  101. package/src/schema.ts +0 -312
  102. package/src/user-config.ts +0 -177
  103. package/src/validation/platform-rules.ts +0 -686
package/src/cli/index.ts DELETED
@@ -1,2933 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { loadConfig } from '../config/load'
4
- import { build } from '../generators'
5
- import {
6
- AGENT_PROMPT_KINDS,
7
- AGENT_RUNNERS,
8
- applyAgentPreparePlan,
9
- applyAgentPromptPlan,
10
- planAgentPrepare,
11
- planAgentPrompt,
12
- planAgentRun,
13
- runAgentPlan,
14
- type AgentPromptKind,
15
- type AgentRunner,
16
- type AgentRunnerModelSummary,
17
- } from './agent'
18
- import { doctorConsumer, doctorProject, printDoctorReport } from './doctor'
19
- import {
20
- ensureHookTrust,
21
- getInstallFollowupNotes,
22
- installPlugin,
23
- listHookCommands,
24
- planInstallPlugin,
25
- planInstallUserConfig,
26
- resolveInstallUserConfig,
27
- uninstallPlugin,
28
- } from './install'
29
- import { runDev } from './dev'
30
- import {
31
- analyzeMcpQuality,
32
- applyMcpScaffoldPlan,
33
- buildToolExampleRequest,
34
- deriveDisplayName,
35
- derivePluginName,
36
- MCP_HOOK_MODES,
37
- MCP_RUNTIME_AUTH_MODES,
38
- MCP_SKILL_GROUPINGS,
39
- type McpQualityReport,
40
- planMcpScaffold,
41
- type McpHookMode,
42
- type McpRuntimeAuthMode,
43
- parseMcpSourceInput,
44
- type McpSkillGrouping,
45
- writeMcpScaffold,
46
- } from './init-from-mcp'
47
- import { migrate } from './migrate'
48
- import { runMcpProxy } from './mcp-proxy'
49
- import { lintProject, printLintResult, runLint } from './lint'
50
- import {
51
- discoverMcpAuthFromError,
52
- introspectMcpServer,
53
- McpIntrospectionError,
54
- type IntrospectedMcpServer,
55
- } from '../mcp/introspect'
56
- import { promptText, promptYesNo, PromptCancelledError } from './prompt'
57
- import * as clack from '@clack/prompts'
58
- import type { McpAuth, McpServer, TargetPlatform } from '../schema'
59
- import { basename, resolve } from 'path'
60
- import { mkdir, mkdtemp, rm } from 'fs/promises'
61
- import { tmpdir } from 'os'
62
- import { spawn } from 'child_process'
63
- import { formatSyncSummary, planSyncFromMcp, syncFromMcp } from './sync-from-mcp'
64
- import { formatPublishPlan, planPublish, runPublish } from './publish'
65
- import { createCliRuntime, createSpinner, printJson, readFlag, readMultiValueOption, readOption } from './runtime'
66
- import { printTestResult, runTestSuite, type TestRunResult } from './test'
67
- import { printEvalReport, runEvalSuite } from './eval'
68
-
69
- const args = process.argv.slice(2)
70
- const command = args[0]
71
- const runtime = createCliRuntime(args)
72
- const DEFAULT_INIT_TARGETS = ['claude-code', 'cursor', 'codex', 'opencode'] as const satisfies readonly TargetPlatform[]
73
- const AUTOPILOT_MODES = ['quick', 'standard', 'thorough'] as const
74
- const ALL_TARGET_PLATFORMS = [
75
- 'claude-code',
76
- 'cursor',
77
- 'codex',
78
- 'opencode',
79
- 'github-copilot',
80
- 'openhands',
81
- 'warp',
82
- 'gemini-cli',
83
- 'roo-code',
84
- 'cline',
85
- 'amp',
86
- ] as const satisfies readonly TargetPlatform[]
87
-
88
- export interface InitFromMcpOptions {
89
- source?: string
90
- assumeDefaults: boolean
91
- name?: string
92
- author?: string
93
- displayName?: string
94
- targets?: string
95
- authEnv?: string
96
- authType?: string
97
- authHeader?: string
98
- authTemplate?: string
99
- runtimeAuth?: string
100
- oauthWrapper: boolean
101
- grouping?: string
102
- hooks?: string
103
- transport?: string
104
- jsonOutput: boolean
105
- }
106
-
107
- interface InitFromMcpSummary {
108
- pluginName: string
109
- displayName: string
110
- source: string
111
- toolCount: number
112
- targets: TargetPlatform[]
113
- grouping: McpSkillGrouping
114
- requestedHookMode: McpHookMode
115
- hookMode: McpHookMode
116
- hookEvents: string[]
117
- files: string[]
118
- createdFiles: string[]
119
- updatedFiles: string[]
120
- lint: {
121
- errors: number
122
- warnings: number
123
- }
124
- quality: McpQualityReport
125
- notes: string[]
126
- nextSteps: string[]
127
- dryRun?: boolean
128
- }
129
-
130
- interface AutopilotSummary {
131
- ok: boolean
132
- pluginName: string
133
- displayName: string
134
- source: string
135
- mode: AutopilotMode
136
- runner: AgentRunner
137
- model: AgentRunnerModelSummary
138
- targets: TargetPlatform[]
139
- toolCount: number
140
- grouping: McpSkillGrouping
141
- requestedHookMode: McpHookMode
142
- hookMode: McpHookMode
143
- hookEvents: string[]
144
- quality: McpQualityReport
145
- review: boolean
146
- verify: boolean
147
- steps: number
148
- init: {
149
- createdFiles: string[]
150
- updatedFiles: string[]
151
- files: string[]
152
- }
153
- agent: {
154
- taxonomy: {
155
- enabled: boolean
156
- reason: string
157
- command?: string[]
158
- commandDisplay?: string
159
- createdFiles: string[]
160
- updatedFiles: string[]
161
- runnerExitCode?: number
162
- durationMs?: number
163
- }
164
- instructions: {
165
- enabled: boolean
166
- reason: string
167
- command?: string[]
168
- commandDisplay?: string
169
- createdFiles: string[]
170
- updatedFiles: string[]
171
- runnerExitCode?: number
172
- durationMs?: number
173
- }
174
- review?: {
175
- enabled: boolean
176
- reason: string
177
- command?: string[]
178
- commandDisplay?: string
179
- createdFiles: string[]
180
- updatedFiles: string[]
181
- runnerExitCode?: number
182
- durationMs?: number
183
- }
184
- }
185
- verification?: TestRunResult
186
- verificationDurationMs?: number
187
- runnerLogsStreamed?: boolean
188
- failureStage?: 'auth' | 'introspection' | 'runner' | 'verification'
189
- failureMessage?: string
190
- dryRun?: boolean
191
- }
192
-
193
- type AutopilotMode = typeof AUTOPILOT_MODES[number]
194
-
195
- interface AutopilotPassDecision {
196
- enabled: boolean
197
- reason: string
198
- }
199
-
200
- export async function main() {
201
- switch (command) {
202
- case 'build':
203
- await runBuild()
204
- break
205
- case 'dev':
206
- await runDev(args.slice(1))
207
- break
208
- case 'validate':
209
- await runValidate()
210
- break
211
- case 'lint':
212
- await runLintCommand()
213
- break
214
- case 'doctor':
215
- await runDoctor()
216
- break
217
- case 'agent':
218
- await runAgent()
219
- break
220
- case 'mcp':
221
- await runMcp()
222
- break
223
- case 'autopilot':
224
- await runAutopilot()
225
- break
226
- case 'init':
227
- await runInit()
228
- break
229
- case 'install':
230
- await runInstall()
231
- break
232
- case 'publish':
233
- await runPublishCommand()
234
- break
235
- case 'uninstall':
236
- await runUninstall()
237
- break
238
- case 'sync':
239
- await runSync()
240
- break
241
- case 'migrate':
242
- await runMigrate()
243
- break
244
- case 'test':
245
- await runTestCommand()
246
- break
247
- case 'eval':
248
- await runEvalCommand()
249
- break
250
- case undefined:
251
- case 'help':
252
- case '--help':
253
- case '-h':
254
- printHelp()
255
- break
256
- default:
257
- console.error(`Unknown command: ${command}`)
258
- printHelp()
259
- process.exit(1)
260
- }
261
- }
262
-
263
- function hasAgentContextHints(input: {
264
- docsUrl?: string
265
- websiteUrl?: string
266
- contextPaths?: string[]
267
- }): boolean {
268
- return Boolean(input.docsUrl || input.websiteUrl || (input.contextPaths?.length ?? 0) > 0)
269
- }
270
-
271
- function planAutopilotPasses(input: {
272
- mode: AutopilotMode
273
- quality: McpQualityReport
274
- reviewRequested: boolean
275
- docsUrl?: string
276
- websiteUrl?: string
277
- contextPaths?: string[]
278
- }): Record<AgentPromptKind, AutopilotPassDecision> {
279
- const qualityHasWarnings = input.quality.warnings > 0
280
- const qualityHasSignals = qualityHasWarnings || input.quality.infos > 0
281
- const hasContext = hasAgentContextHints(input)
282
-
283
- if (input.mode === 'quick') {
284
- return {
285
- taxonomy: qualityHasWarnings
286
- ? { enabled: true, reason: 'quick mode runs taxonomy only when MCP metadata warnings are present' }
287
- : { enabled: false, reason: 'quick mode skips taxonomy when MCP metadata already looks usable' },
288
- instructions: { enabled: false, reason: 'quick mode skips the separate instructions rewrite pass' },
289
- review: { enabled: false, reason: 'quick mode never runs review; use standard or thorough for critique passes' },
290
- }
291
- }
292
-
293
- if (input.mode === 'standard') {
294
- return {
295
- taxonomy: qualityHasSignals || hasContext
296
- ? {
297
- enabled: true,
298
- reason: qualityHasSignals
299
- ? 'standard mode runs taxonomy when MCP metadata signals cleanup work'
300
- : 'standard mode runs taxonomy when docs, website, or extra context are provided',
301
- }
302
- : { enabled: false, reason: 'standard mode skips taxonomy when the deterministic scaffold already has strong metadata' },
303
- instructions: qualityHasWarnings || hasContext
304
- ? {
305
- enabled: true,
306
- reason: qualityHasWarnings
307
- ? 'standard mode rewrites instructions when MCP metadata warnings are present'
308
- : 'standard mode rewrites instructions when richer docs or website context are provided',
309
- }
310
- : { enabled: false, reason: 'standard mode skips the separate instructions pass to keep refinement lighter' },
311
- review: input.reviewRequested
312
- ? { enabled: true, reason: 'review requested explicitly via --review' }
313
- : { enabled: false, reason: 'review is opt-in in standard mode' },
314
- }
315
- }
316
-
317
- return {
318
- taxonomy: { enabled: true, reason: 'thorough mode always runs taxonomy refinement' },
319
- instructions: { enabled: true, reason: 'thorough mode always runs instructions refinement' },
320
- review: input.reviewRequested
321
- ? { enabled: true, reason: 'review requested explicitly via --review' }
322
- : { enabled: true, reason: 'thorough mode includes a review pass by default' },
323
- }
324
- }
325
-
326
- function countAutopilotSteps(input: {
327
- taxonomy: AutopilotPassDecision
328
- instructions: AutopilotPassDecision
329
- review: AutopilotPassDecision
330
- verify: boolean
331
- }): number {
332
- return 2
333
- + Number(input.taxonomy.enabled)
334
- + Number(input.instructions.enabled)
335
- + Number(input.review.enabled)
336
- + Number(input.verify)
337
- }
338
-
339
- function formatAutopilotPassLine(label: string, decision: AutopilotPassDecision): string {
340
- return `${label}: ${decision.enabled ? 'run' : 'skip'} (${decision.reason})`
341
- }
342
-
343
- function summarizeAutopilotWorkload(input: {
344
- taxonomy: AutopilotPassDecision
345
- instructions: AutopilotPassDecision
346
- review: AutopilotPassDecision
347
- verify: boolean
348
- }): string {
349
- const agentPassCount = Number(input.taxonomy.enabled) + Number(input.instructions.enabled) + Number(input.review.enabled)
350
- if (agentPassCount === 0 && !input.verify) {
351
- return 'deterministic scaffold only'
352
- }
353
- if (agentPassCount === 0) {
354
- return 'deterministic scaffold + verification'
355
- }
356
- return `${agentPassCount} agent pass${agentPassCount === 1 ? '' : 'es'}${input.verify ? ' + verification' : ''}`
357
- }
358
-
359
- function formatDuration(durationMs?: number): string | undefined {
360
- if (durationMs === undefined) {
361
- return undefined
362
- }
363
-
364
- if (durationMs < 1000) {
365
- return `${durationMs}ms`
366
- }
367
-
368
- return `${(durationMs / 1000).toFixed(durationMs >= 10_000 ? 0 : 1)}s`
369
- }
370
-
371
- interface InstallActionSummary {
372
- enabled: boolean
373
- platforms: TargetPlatform[]
374
- notes: string[]
375
- installTargets: Array<{
376
- platform: TargetPlatform
377
- pluginDir: string
378
- built: boolean
379
- existing: boolean
380
- }>
381
- }
382
-
383
- async function maybeInstallBuiltOutputs(
384
- config: Awaited<ReturnType<typeof loadConfig>>,
385
- platforms: TargetPlatform[],
386
- ): Promise<InstallActionSummary | undefined> {
387
- if (!args.includes('--install')) {
388
- return undefined
389
- }
390
-
391
- const distDir = `${process.cwd()}/${config.outDir}`
392
- const installPlan = planInstallPlugin(distDir, config.name, platforms)
393
-
394
- if (!runtime.dryRun) {
395
- await ensureHookTrust({
396
- pluginName: config.name,
397
- hooks: config.hooks,
398
- trust: args.includes('--trust'),
399
- isTTY: runtime.isInteractive,
400
- })
401
- const resolvedUserConfig = await resolveInstallUserConfig(config, platforms, {
402
- isTTY: runtime.isInteractive,
403
- })
404
- await installPlugin(distDir, config.name, platforms, {
405
- config,
406
- quiet: true,
407
- resolvedUserConfig,
408
- })
409
- }
410
-
411
- return {
412
- enabled: true,
413
- platforms,
414
- notes: getInstallFollowupNotes(platforms),
415
- installTargets: installPlan.map((target) => ({
416
- platform: target.platform,
417
- pluginDir: target.description,
418
- built: target.built,
419
- existing: target.existing,
420
- })),
421
- }
422
- }
423
-
424
- function logAutopilotRunnerWait(step: number, totalSteps: number, label: string, runner: AgentRunner): void {
425
- if (runtime.jsonOutput || runtime.quiet || !runtime.isInteractive) {
426
- return
427
- }
428
-
429
- clack.log.step(`Autopilot ${step}/${totalSteps} · ${label} via ${runner} headless runner. This can take a few minutes.`)
430
- }
431
-
432
- async function runTimedSpinnerTask<T>(input: {
433
- spinner: ReturnType<typeof clack.spinner> | undefined
434
- startLabel: string
435
- waitLabel: string
436
- successLabel: (result: T) => string
437
- task: () => Promise<T>
438
- }): Promise<T> {
439
- const startedAt = Date.now()
440
- let interval: ReturnType<typeof setInterval> | undefined
441
-
442
- input.spinner?.start(input.startLabel)
443
- if (input.spinner) {
444
- interval = setInterval(() => {
445
- input.spinner?.message(`${input.waitLabel} (${formatDuration(Date.now() - startedAt) ?? '0ms'} elapsed)`)
446
- }, 1000)
447
- }
448
-
449
- try {
450
- const result = await input.task()
451
- if (interval) {
452
- clearInterval(interval)
453
- }
454
- input.spinner?.stop(input.successLabel(result))
455
- return result
456
- } catch (error) {
457
- if (interval) {
458
- clearInterval(interval)
459
- }
460
- throw error
461
- }
462
- }
463
-
464
- async function runBuild() {
465
- const targets = parseTargetFlagValues(args)
466
- const config = await loadConfig()
467
- const platforms = targets ?? config.targets
468
- const cwd = process.cwd()
469
- const shouldInstall = args.includes('--install')
470
-
471
- if (runtime.dryRun) {
472
- const install = await maybeInstallBuiltOutputs(config, platforms)
473
- const summary = {
474
- dryRun: true,
475
- targets: platforms,
476
- outDir: config.outDir,
477
- outputPaths: platforms.map((platform) => `${config.outDir}/${platform}/`),
478
- install,
479
- }
480
- if (runtime.jsonOutput) {
481
- printJson(summary)
482
- } else if (!runtime.quiet) {
483
- console.log(`Dry run: would build ${platforms.join(', ')}`)
484
- summary.outputPaths.forEach((path) => console.log(` ${path}`))
485
- }
486
- return
487
- }
488
-
489
- if (!runtime.jsonOutput && !runtime.quiet) {
490
- console.log(`Building for: ${platforms.join(', ')}`)
491
- }
492
-
493
- const lintResult = await lintProject(cwd, { targets: platforms })
494
- if (lintResult.errors > 0) {
495
- if (runtime.jsonOutput) {
496
- printJson({
497
- ok: false,
498
- reason: 'lint-errors',
499
- lint: lintResult,
500
- })
501
- } else {
502
- printLintResult(lintResult, cwd)
503
- console.error('Build aborted due to lint errors.')
504
- }
505
- process.exit(1)
506
- }
507
-
508
- if (!runtime.jsonOutput && !runtime.quiet && lintResult.warnings > 0) {
509
- printLintResult(lintResult, cwd)
510
- }
511
-
512
- await build(config, cwd, { targets })
513
- const install = await maybeInstallBuiltOutputs(config, platforms)
514
-
515
- if (runtime.jsonOutput) {
516
- printJson({
517
- ok: true,
518
- targets: platforms,
519
- outDir: config.outDir,
520
- outputPaths: platforms.map((platform) => `${config.outDir}/${platform}/`),
521
- lint: lintResult,
522
- install,
523
- })
524
- return
525
- }
526
-
527
- if (!runtime.quiet) {
528
- console.log(`Done! Output in ${config.outDir}/`)
529
- for (const platform of platforms) {
530
- console.log(` ${config.outDir}/${platform}/`)
531
- }
532
- if (shouldInstall && install) {
533
- console.log('Installed for local testing:')
534
- for (const target of install.installTargets) {
535
- console.log(` ${target.platform} -> ${target.pluginDir}`)
536
- }
537
- for (const note of install.notes) {
538
- console.log(note)
539
- }
540
- }
541
- }
542
- }
543
-
544
- async function runValidate() {
545
- try {
546
- const config = await loadConfig()
547
- console.log(`Config valid: ${config.name}@${config.version}`)
548
- console.log(` Targets: ${config.targets.join(', ')}`)
549
- console.log(` Skills: ${config.skills}`)
550
- if (config.mcp) {
551
- console.log(` MCP servers: ${Object.keys(config.mcp).join(', ')}`)
552
- }
553
- if (config.hooks) {
554
- const events = Object.keys(config.hooks).filter(k => config.hooks![k as keyof typeof config.hooks])
555
- console.log(` Hook events: ${events.join(', ')}`)
556
- }
557
- } catch (err) {
558
- console.error('Validation failed:')
559
- console.error(err instanceof Error ? err.message : err)
560
- process.exit(1)
561
- }
562
- }
563
-
564
- async function runLintCommand() {
565
- if (runtime.jsonOutput) {
566
- const result = await lintProject(process.cwd())
567
- printJson(result)
568
- if (result.errors > 0) {
569
- process.exit(1)
570
- }
571
- return
572
- }
573
-
574
- if (runtime.quiet) {
575
- const result = await lintProject(process.cwd())
576
- if (result.errors > 0 || result.warnings > 0) {
577
- printLintResult(result, process.cwd())
578
- }
579
- if (result.errors > 0) {
580
- process.exit(1)
581
- }
582
- return
583
- }
584
-
585
- const exitCode = await runLint(process.cwd())
586
- if (exitCode !== 0) {
587
- process.exit(exitCode)
588
- }
589
- }
590
-
591
- async function resolveTextOption(options: {
592
- label: string
593
- defaultValue?: string
594
- providedValue?: string
595
- assumeDefaults?: boolean
596
- }): Promise<string> {
597
- if (options.providedValue !== undefined) {
598
- return options.providedValue
599
- }
600
-
601
- if (options.assumeDefaults) {
602
- return options.defaultValue ?? ''
603
- }
604
-
605
- return await promptText(options.label, options.defaultValue)
606
- }
607
-
608
- async function resolveChoiceOption<T extends string>(options: {
609
- label: string
610
- values: readonly T[]
611
- defaultValue: T
612
- providedValue?: string
613
- assumeDefaults?: boolean
614
- }): Promise<T> {
615
- const raw = await resolveTextOption({
616
- label: `${options.label} (${options.values.join('/')})`,
617
- defaultValue: options.defaultValue,
618
- providedValue: options.providedValue,
619
- assumeDefaults: options.assumeDefaults,
620
- })
621
- return parseChoiceOption(raw, options.values, options.label)
622
- }
623
-
624
- function parseChoiceOption<T extends string>(value: string, validValues: readonly T[], label: string): T {
625
- const normalized = value.trim().toLowerCase()
626
- const match = validValues.find((entry) => entry.toLowerCase() === normalized)
627
- if (!match) {
628
- throw new Error(`${label} must be one of: ${validValues.join(', ')}`)
629
- }
630
- return match
631
- }
632
-
633
- function parseTargetPlatforms(raw: string): TargetPlatform[] {
634
- const targets = raw
635
- .split(',')
636
- .map((target) => target.trim())
637
- .filter(Boolean)
638
-
639
- if (targets.length === 0) {
640
- throw new Error('Provide at least one target platform.')
641
- }
642
-
643
- const invalid = targets.filter((target) => !(ALL_TARGET_PLATFORMS as readonly string[]).includes(target))
644
- if (invalid.length > 0) {
645
- throw new Error(
646
- `Unknown target platform(s): ${invalid.join(', ')}. Supported: ${ALL_TARGET_PLATFORMS.join(', ')}`,
647
- )
648
- }
649
-
650
- return targets as TargetPlatform[]
651
- }
652
-
653
- function parseTargetFlagValues(rawArgs: string[]): TargetPlatform[] | undefined {
654
- const values = readMultiValueOption(rawArgs, '--target')
655
- if (!values) return undefined
656
- return parseTargetPlatforms(values.join(','))
657
- }
658
-
659
- function defaultHookMode(source: { auth?: { type: string; envVar?: string }; transport?: string; env?: Record<string, string> }): McpHookMode {
660
- if (source.auth?.type && source.auth.type !== 'none' && source.auth.envVar) {
661
- return 'safe'
662
- }
663
-
664
- if (source.transport === 'stdio' && source.env && Object.keys(source.env).length > 0) {
665
- return 'safe'
666
- }
667
-
668
- return 'none'
669
- }
670
-
671
- interface McpAuthProviderHint {
672
- id: 'linear' | 'generic-oauth'
673
- label: string
674
- envVar: string
675
- tokenLabel: string
676
- tokenAuthType: 'bearer' | 'header'
677
- authHeader?: string
678
- authTemplate?: string
679
- wrapperCommand?: string
680
- guidance?: string
681
- }
682
-
683
- function getSourceUrl(source: McpServer): string | undefined {
684
- return source.transport === 'stdio' ? undefined : source.url
685
- }
686
-
687
- function collectAuthCandidateUrls(
688
- source: McpServer,
689
- discoveredAuth?: ReturnType<typeof discoverMcpAuthFromError> | null,
690
- ): string[] {
691
- return [
692
- getSourceUrl(source),
693
- discoveredAuth?.authorizationUrl,
694
- discoveredAuth?.resourceMetadataUrl,
695
- ].filter((value): value is string => Boolean(value))
696
- }
697
-
698
- function matchesHost(urlValue: string, suffix: string): boolean {
699
- try {
700
- return new URL(urlValue).hostname.endsWith(suffix)
701
- } catch {
702
- return false
703
- }
704
- }
705
-
706
- export function buildOauthWrapperCommand(source: McpServer): string | undefined {
707
- if (source.transport !== 'http') return undefined
708
- return `npx -y mcp-remote ${source.url}`
709
- }
710
-
711
- export function buildOauthWrapperSource(source: McpServer): McpServer | undefined {
712
- if (source.transport !== 'http') return undefined
713
- return {
714
- transport: 'stdio',
715
- command: 'npx',
716
- args: ['-y', 'mcp-remote', source.url],
717
- }
718
- }
719
-
720
- export function inferMcpAuthProvider(
721
- source: McpServer,
722
- discoveredAuth?: ReturnType<typeof discoverMcpAuthFromError> | null,
723
- ): McpAuthProviderHint | null {
724
- const candidates = collectAuthCandidateUrls(source, discoveredAuth)
725
- if (candidates.some((value) => matchesHost(value, 'linear.app'))) {
726
- return {
727
- id: 'linear',
728
- label: 'Linear',
729
- envVar: 'LINEAR_API_KEY',
730
- tokenLabel: 'API key or OAuth token',
731
- tokenAuthType: 'bearer',
732
- authTemplate: 'Bearer ${value}',
733
- wrapperCommand: buildOauthWrapperCommand(source),
734
- guidance: 'Linear supports direct Authorization: Bearer tokens/API keys and the official mcp-remote wrapper for clients that do not support remote MCP.',
735
- }
736
- }
737
-
738
- if (discoveredAuth?.kind === 'platform') {
739
- return {
740
- id: 'generic-oauth',
741
- label: 'OAuth-first MCP',
742
- envVar: 'OAUTH_ACCESS_TOKEN',
743
- tokenLabel: 'access token or API key',
744
- tokenAuthType: 'bearer',
745
- authTemplate: 'Bearer ${value}',
746
- wrapperCommand: buildOauthWrapperCommand(source),
747
- }
748
- }
749
-
750
- return null
751
- }
752
-
753
- function defaultAuthEnvVar(
754
- provider: McpAuthProviderHint | null,
755
- discoveredAuth?: ReturnType<typeof discoverMcpAuthFromError> | null,
756
- ): string {
757
- if (provider?.envVar) return provider.envVar
758
- if (discoveredAuth?.kind === 'header') return 'API_KEY'
759
- if (discoveredAuth?.kind === 'platform') return 'OAUTH_ACCESS_TOKEN'
760
- return ''
761
- }
762
-
763
- function tryOpenBrowser(url: string): boolean {
764
- const launcher = process.platform === 'darwin'
765
- ? { command: 'open', args: [url] }
766
- : process.platform === 'win32'
767
- ? { command: 'cmd', args: ['/c', 'start', '', url] }
768
- : { command: 'xdg-open', args: [url] }
769
-
770
- try {
771
- const child = spawn(launcher.command, launcher.args, {
772
- detached: true,
773
- stdio: 'ignore',
774
- })
775
- child.unref()
776
- return true
777
- } catch {
778
- return false
779
- }
780
- }
781
-
782
- export function resolveRemoteAuthType(options: Pick<InitFromMcpOptions, 'authType' | 'authHeader'>): 'bearer' | 'header' | 'platform' {
783
- if (options.authType) {
784
- return parseChoiceOption(options.authType, ['bearer', 'header', 'platform'] as const, 'Auth type')
785
- }
786
-
787
- if (options.authHeader && options.authHeader.trim() && options.authHeader.trim().toLowerCase() !== 'authorization') {
788
- return 'header'
789
- }
790
-
791
- return 'bearer'
792
- }
793
-
794
- export function buildRemoteAuthConfig(options: Pick<InitFromMcpOptions, 'authEnv' | 'authType' | 'authHeader' | 'authTemplate'>): McpAuth | undefined {
795
- if (options.authType?.trim() === 'platform') {
796
- return {
797
- type: 'platform',
798
- mode: 'oauth',
799
- }
800
- }
801
-
802
- const envVar = options.authEnv?.trim()
803
- if (!envVar) return undefined
804
-
805
- const authType = resolveRemoteAuthType(options)
806
-
807
- if (authType === 'platform') {
808
- return {
809
- type: 'platform',
810
- mode: 'oauth',
811
- }
812
- }
813
-
814
- if (authType === 'header') {
815
- const headerName = options.authHeader?.trim()
816
- if (!headerName) {
817
- throw new Error('Header auth requires --auth-header HEADER_NAME.')
818
- }
819
-
820
- return {
821
- type: 'header',
822
- envVar,
823
- headerName,
824
- headerTemplate: options.authTemplate?.trim() || '${value}',
825
- }
826
- }
827
-
828
- return {
829
- type: 'bearer',
830
- envVar,
831
- headerName: options.authHeader?.trim() || 'Authorization',
832
- headerTemplate: options.authTemplate?.trim() || 'Bearer ${value}',
833
- }
834
- }
835
-
836
- function resolveRuntimeAuthMode(value?: string): McpRuntimeAuthMode {
837
- return value
838
- ? parseChoiceOption(value, MCP_RUNTIME_AUTH_MODES, 'Runtime auth mode')
839
- : 'inline'
840
- }
841
-
842
- function applyGeneratedAuthEnv(
843
- source: ReturnType<typeof parseMcpSourceInput>,
844
- envVar?: string,
845
- remoteAuthOptions?: Pick<InitFromMcpOptions, 'authType' | 'authHeader' | 'authTemplate'>,
846
- ) {
847
- if (!envVar) return source
848
-
849
- if (source.transport === 'stdio') {
850
- return {
851
- ...source,
852
- env: {
853
- ...(source.env ?? {}),
854
- [envVar]: `\${${envVar}}`,
855
- },
856
- }
857
- }
858
-
859
- if (!source.auth) {
860
- const auth = buildRemoteAuthConfig({
861
- authEnv: envVar,
862
- authType: remoteAuthOptions?.authType,
863
- authHeader: remoteAuthOptions?.authHeader,
864
- authTemplate: remoteAuthOptions?.authTemplate,
865
- })
866
- return {
867
- ...source,
868
- auth,
869
- }
870
- }
871
-
872
- return source
873
- }
874
-
875
- function isAuthRequiredError(error: unknown): error is McpIntrospectionError {
876
- if (!(error instanceof McpIntrospectionError)) return false
877
-
878
- if (error.status !== undefined && [401, 402, 403].includes(error.status)) {
879
- return true
880
- }
881
-
882
- const message = error.message.toLowerCase()
883
- if (message.includes('missing environment variable')) {
884
- return true
885
- }
886
-
887
- const wwwAuthenticate = error.context?.responseHeaders?.['www-authenticate']?.toLowerCase() ?? ''
888
- return wwwAuthenticate.includes('bearer') || wwwAuthenticate.includes('oauth')
889
- }
890
-
891
- function isLikelyOAuthFirstError(error: McpIntrospectionError): boolean {
892
- return discoverMcpAuthFromError(error)?.kind === 'platform'
893
- }
894
-
895
- function formatAuthRequiredMessage(
896
- commandName: 'init' | 'autopilot',
897
- error?: McpIntrospectionError,
898
- source?: McpServer,
899
- ): string {
900
- const discoveredAuth = error ? discoverMcpAuthFromError(error) : null
901
- const provider = source ? inferMcpAuthProvider(source, discoveredAuth) : null
902
- const rerun = discoveredAuth?.kind === 'header' && discoveredAuth.headerName
903
- ? `Re-run ${commandName} with --auth-env YOUR_ENV_VAR --auth-type header --auth-header ${discoveredAuth.headerName} [--auth-template '\${value}']`
904
- : `Re-run ${commandName} with --auth-env YOUR_ENV_VAR and either:
905
- - Bearer auth: --auth-type bearer
906
- - Custom header auth: --auth-type header --auth-header HEADER_NAME [--auth-template '\${value}']`
907
- const providerNote = provider?.guidance ? `\n\n${provider.guidance}` : ''
908
- const wrapperNote = provider?.wrapperCommand
909
- ? `\nLocal wrapper/proxy helper: ${provider.wrapperCommand}`
910
- : ''
911
- const oauthNote = discoveredAuth?.kind === 'platform'
912
- ? `
913
-
914
- This server appears OAuth-first${discoveredAuth.authorizationUrl ? ` (${discoveredAuth.authorizationUrl})` : ''}. Complete the provider's OAuth flow first, export the resulting token/API key to YOUR_ENV_VAR, then rerun.
915
- If the server supports public discovery and only needs OAuth at runtime, you can scaffold it with --auth-type platform --runtime-auth platform.
916
- If it requires browser-interactive OAuth during handshake, run a local stdio MCP wrapper/proxy and import that command instead.${wrapperNote}${providerNote}`
917
- : ''
918
-
919
- return `This MCP server requires authentication.
920
- ${rerun}${oauthNote}`
921
- }
922
-
923
- type OAuthImportStrategy = 'token' | 'wrapper'
924
-
925
- async function chooseOAuthImportStrategy(
926
- provider: McpAuthProviderHint | null,
927
- ): Promise<OAuthImportStrategy> {
928
- const options: Array<{ value: OAuthImportStrategy; label: string; hint?: string }> = [
929
- {
930
- value: 'token',
931
- label: 'token',
932
- hint: `Complete auth in the browser, then continue with a ${provider?.tokenLabel ?? 'token or API key'}`,
933
- },
934
- ]
935
-
936
- if (provider?.wrapperCommand) {
937
- options.push({
938
- value: 'wrapper',
939
- label: 'wrapper',
940
- hint: `Import through ${provider.wrapperCommand}`,
941
- })
942
- }
943
-
944
- return await clackSelect('OAuth import path', options, provider?.wrapperCommand ? 'wrapper' : 'token')
945
- }
946
-
947
- async function maybeCaptureOAuthCredential(
948
- provider: McpAuthProviderHint | null,
949
- authorizationUrl?: string,
950
- ): Promise<string | undefined> {
951
- if (authorizationUrl) {
952
- const opened = tryOpenBrowser(authorizationUrl)
953
- const message = opened
954
- ? `Opened ${authorizationUrl} in your browser. Finish the auth flow, then paste the resulting ${provider?.tokenLabel ?? 'token or API key'} below if you want Pluxx to retry immediately.`
955
- : `Open ${authorizationUrl} in your browser. Finish the auth flow, then paste the resulting ${provider?.tokenLabel ?? 'token or API key'} below if you want Pluxx to retry immediately.`
956
- clack.note(message, 'OAuth flow')
957
- }
958
-
959
- const credential = (await clackPassword(`Paste ${provider?.tokenLabel ?? 'token or API key'} for this session (optional)`)).trim()
960
- return credential || undefined
961
- }
962
-
963
- function applySessionCredential(envVar: string | undefined, credential: string | undefined) {
964
- if (!envVar || !credential) return
965
- process.env[envVar] = credential
966
- }
967
-
968
- function buildInitSummary(input: {
969
- pluginName: string
970
- displayName: string
971
- source: string
972
- toolCount: number
973
- targets: TargetPlatform[]
974
- grouping: McpSkillGrouping
975
- requestedHookMode: McpHookMode
976
- hookMode: McpHookMode
977
- hookEvents: string[]
978
- files: string[]
979
- createdFiles: string[]
980
- updatedFiles: string[]
981
- lint: { errors: number; warnings: number }
982
- quality: McpQualityReport
983
- dryRun?: boolean
984
- }): InitFromMcpSummary {
985
- const installTarget = input.targets[0]
986
- const installCommand = input.hookMode === 'safe'
987
- ? `Run: pluxx install --trust --target ${installTarget}`
988
- : `Run: pluxx install --target ${installTarget}`
989
- const notes: string[] = []
990
-
991
- if (input.requestedHookMode === 'safe' && input.hookMode === 'none') {
992
- notes.push('No safe hooks were generated for this MCP source. Safe hooks currently require explicit env vars in the generated MCP config.')
993
- } else if (input.hookMode === 'safe') {
994
- notes.push(`Generated install-ready hook events: ${input.hookEvents.join(', ')}`)
995
- }
996
-
997
- if (input.quality.warnings > 0 || input.quality.infos > 0) {
998
- notes.push(`MCP quality: ${input.quality.warnings} warning(s), ${input.quality.infos} info message(s)`)
999
- }
1000
-
1001
- if (input.quality.warnings > 0) {
1002
- notes.push('Consider using pluxx autopilot with --website/--docs or adding pluxx.agent.md hints before publishing this plugin.')
1003
- }
1004
-
1005
- const nextSteps = [
1006
- 'Review INSTRUCTIONS.md and the generated skills before publishing.',
1007
- 'Run: pluxx build',
1008
- installCommand,
1009
- ]
1010
-
1011
- return {
1012
- pluginName: input.pluginName,
1013
- displayName: input.displayName,
1014
- source: input.source,
1015
- toolCount: input.toolCount,
1016
- targets: input.targets,
1017
- grouping: input.grouping,
1018
- requestedHookMode: input.requestedHookMode,
1019
- hookMode: input.hookMode,
1020
- hookEvents: input.hookEvents,
1021
- files: input.files,
1022
- createdFiles: input.createdFiles,
1023
- updatedFiles: input.updatedFiles,
1024
- lint: input.lint,
1025
- quality: input.quality,
1026
- notes,
1027
- nextSteps,
1028
- dryRun: input.dryRun,
1029
- }
1030
- }
1031
-
1032
- function formatMcpQualityLines(report: McpQualityReport): string[] {
1033
- const lines = [`MCP quality: ${report.warnings} warning(s), ${report.infos} info message(s)`]
1034
-
1035
- for (const issue of report.issues) {
1036
- lines.push(`- [${issue.level}] ${issue.title}: ${issue.detail}`)
1037
- }
1038
-
1039
- return lines
1040
- }
1041
-
1042
- function formatMcpDiscoverySummary(introspection: IntrospectedMcpServer): string {
1043
- const parts = [`${introspection.tools.length} tools`]
1044
- const resourceCount = (introspection.resources?.length ?? 0) + (introspection.resourceTemplates?.length ?? 0)
1045
- const promptCount = introspection.prompts?.length ?? 0
1046
-
1047
- if (resourceCount > 0) {
1048
- parts.push(`${resourceCount} resources`)
1049
- }
1050
- if (promptCount > 0) {
1051
- parts.push(`${promptCount} prompts`)
1052
- }
1053
-
1054
- return `${parts.join(', ')} discovered`
1055
- }
1056
-
1057
- export function parseInitFromMcpOptions(rawArgs: string[], initialName?: string, initialSource?: string): InitFromMcpOptions {
1058
- return {
1059
- source: initialSource ?? readOption(rawArgs, '--from-mcp'),
1060
- assumeDefaults: rawArgs.includes('--yes'),
1061
- name: readOption(rawArgs, '--name') ?? initialName,
1062
- author: readOption(rawArgs, '--author'),
1063
- displayName: readOption(rawArgs, '--display-name'),
1064
- targets: readOption(rawArgs, '--targets'),
1065
- authEnv: readOption(rawArgs, '--auth-env'),
1066
- authType: readOption(rawArgs, '--auth-type'),
1067
- authHeader: readOption(rawArgs, '--auth-header'),
1068
- authTemplate: readOption(rawArgs, '--auth-template'),
1069
- runtimeAuth: readOption(rawArgs, '--runtime-auth'),
1070
- oauthWrapper: rawArgs.includes('--oauth-wrapper'),
1071
- grouping: readOption(rawArgs, '--grouping'),
1072
- hooks: readOption(rawArgs, '--hooks'),
1073
- transport: readOption(rawArgs, '--transport'),
1074
- jsonOutput: rawArgs.includes('--json'),
1075
- }
1076
- }
1077
-
1078
- function toKebabCase(value: string): string {
1079
- return value
1080
- .toLowerCase()
1081
- .trim()
1082
- .replace(/[^a-z0-9]+/g, '-')
1083
- .replace(/^-+|-+$/g, '')
1084
- }
1085
-
1086
- function toTsString(value: string): string {
1087
- return JSON.stringify(value)
1088
- }
1089
-
1090
- async function runInit() {
1091
- const positionalName = args[1] && !args[1].startsWith('-') ? args[1] : undefined
1092
- const fromMcpFlag = args.indexOf('--from-mcp')
1093
- const fromMcpInput = fromMcpFlag !== -1 && args[fromMcpFlag + 1] && !args[fromMcpFlag + 1].startsWith('-')
1094
- ? args[fromMcpFlag + 1]
1095
- : undefined
1096
-
1097
- if (fromMcpFlag !== -1) {
1098
- await runInitFromMcp(positionalName, fromMcpInput)
1099
- return
1100
- }
1101
-
1102
- if (!runtime.isInteractive) {
1103
- throw new Error('pluxx init requires an interactive terminal unless you use `pluxx init --from-mcp ... --yes`.')
1104
- }
1105
-
1106
- const dirName = positionalName
1107
- ? toKebabCase(positionalName)
1108
- : basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, '-')
1109
-
1110
- console.log('')
1111
- console.log(' pluxx init — Create a new plugin')
1112
- console.log(' ─────────────────────────────────')
1113
- console.log('')
1114
-
1115
- try {
1116
- // 1. Plugin identity
1117
- const name = await promptText('Plugin name', dirName)
1118
- const description = await promptText('Description')
1119
- const authorName = await promptText('Author name')
1120
-
1121
- // 2. MCP server
1122
- const hasMcp = await promptYesNo('Does your plugin connect to an MCP server?')
1123
- let mcpUrl = ''
1124
- let mcpEnvVar = ''
1125
- if (hasMcp) {
1126
- mcpUrl = await promptText('MCP server URL')
1127
- mcpEnvVar = await promptText('Auth env var name (e.g. MY_API_KEY)')
1128
- }
1129
-
1130
- // 3. Platforms
1131
- const defaultTargets = 'claude-code,cursor,codex,opencode'
1132
- const targetsRaw = await promptText('Which platforms? (comma-separated)', defaultTargets)
1133
- const targets = targetsRaw.split(',').map(t => t.trim()).filter(Boolean)
1134
-
1135
- // 4. Brand metadata
1136
- const hasBrand = await promptYesNo('Add brand metadata?')
1137
- let displayName = ''
1138
- let brandColor = ''
1139
- if (hasBrand) {
1140
- displayName = await promptText('Display name')
1141
- brandColor = await promptText('Brand color (hex)', '#000000')
1142
- }
1143
-
1144
- const pluginName = toKebabCase(name) || dirName
1145
- const skillName = pluginName
1146
-
1147
- if (pluginName !== name) {
1148
- console.log(` Normalized plugin name to ${pluginName}`)
1149
- console.log('')
1150
- }
1151
-
1152
- // Build the config file content
1153
- const targetsList = targets.map(toTsString).join(', ')
1154
- let mcpBlock = ''
1155
- if (hasMcp && mcpUrl) {
1156
- const serverName = pluginName
1157
- mcpBlock = `
1158
- // MCP servers your plugin connects to
1159
- mcp: {
1160
- ${toTsString(serverName)}: {
1161
- url: ${toTsString(mcpUrl)},${mcpEnvVar ? `
1162
- auth: {
1163
- type: 'bearer',
1164
- envVar: ${toTsString(mcpEnvVar)},
1165
- },` : ''}
1166
- },
1167
- },
1168
- `
1169
- }
1170
-
1171
- let brandBlock = ''
1172
- if (hasBrand && displayName) {
1173
- brandBlock = `
1174
- // Brand metadata
1175
- brand: {
1176
- displayName: ${toTsString(displayName)},${brandColor ? `
1177
- color: ${toTsString(brandColor)},` : ''}
1178
- },
1179
- `
1180
- }
1181
-
1182
- const template = `import { definePlugin } from 'pluxx'
1183
-
1184
- export default definePlugin({
1185
- name: ${toTsString(pluginName)},
1186
- version: '0.1.0',
1187
- description: ${toTsString(description)},
1188
- author: {
1189
- name: ${toTsString(authorName)},
1190
- },
1191
- license: 'MIT',
1192
-
1193
- // Skills directory (SKILL.md files following Agent Skills standard)
1194
- skills: './skills/',
1195
- ${mcpBlock}${brandBlock}
1196
- // Target platforms to generate
1197
- targets: [${targetsList}],
1198
- })
1199
- `
1200
-
1201
- // Write config
1202
- await Bun.write('pluxx.config.ts', template)
1203
-
1204
- // Create skills directory with a starter SKILL.md
1205
- const skillDir = `skills/${skillName}`
1206
- await mkdir(skillDir, { recursive: true })
1207
-
1208
- const skillContent = `---
1209
- name: ${JSON.stringify(skillName)}
1210
- description: ${JSON.stringify(description || `A starter skill for ${skillName}`)}
1211
- ---
1212
-
1213
- # ${displayName || pluginName}
1214
-
1215
- ${description || `TODO: Describe what ${displayName || pluginName} does.`}
1216
-
1217
- ## Usage
1218
-
1219
- Describe how agents should use this skill.
1220
-
1221
- ## Examples
1222
-
1223
- \`\`\`
1224
- Example prompt or command here
1225
- \`\`\`
1226
- `
1227
-
1228
- await Bun.write(`${skillDir}/SKILL.md`, skillContent)
1229
-
1230
- console.log('')
1231
- console.log(' Created:')
1232
- console.log(' pluxx.config.ts')
1233
- console.log(` ${skillDir}/SKILL.md`)
1234
- console.log('')
1235
- console.log(' Next steps:')
1236
- console.log(` 1. Edit ${skillDir}/SKILL.md with your skill instructions`)
1237
- console.log(' 2. Run: pluxx build')
1238
- console.log(' 3. Run: pluxx install')
1239
- console.log('')
1240
- } catch (error) {
1241
- if (error instanceof PromptCancelledError) {
1242
- console.log('Init cancelled')
1243
- return
1244
- }
1245
-
1246
- throw error instanceof Error ? error : new Error(String(error))
1247
- }
1248
- }
1249
-
1250
- async function runInitFromMcp(initialName?: string, initialSource?: string) {
1251
- const options = parseInitFromMcpOptions(args, initialName, initialSource)
1252
- const defaultTargets = DEFAULT_INIT_TARGETS.join(',')
1253
- const interactive = !options.jsonOutput && !options.assumeDefaults && runtime.isInteractive
1254
- let runtimeAuthMode = resolveRuntimeAuthMode(options.runtimeAuth)
1255
-
1256
- if (!options.jsonOutput && !runtime.quiet) {
1257
- clack.intro('pluxx init --from-mcp')
1258
- }
1259
-
1260
- try {
1261
- // ── Step 1/4 · Connecting to MCP server ──────────────────────────
1262
-
1263
- const rawSource = options.source ?? (interactive
1264
- ? await clackText('MCP server URL or local command')
1265
- : '')
1266
- if (!rawSource) {
1267
- throw new Error('Provide an MCP server URL or local command. Example: pluxx init --from-mcp https://example.com/mcp')
1268
- }
1269
-
1270
- let source = parseMcpSourceInput(rawSource, options.transport)
1271
- let introspectionSource = source
1272
- const configuredRemoteAuth = source.transport === 'stdio'
1273
- ? undefined
1274
- : buildRemoteAuthConfig(options)
1275
- if (configuredRemoteAuth && !source.auth) {
1276
- source = {
1277
- ...source,
1278
- auth: configuredRemoteAuth,
1279
- } satisfies McpServer
1280
- }
1281
-
1282
- const s = createSpinner(runtime)
1283
- s?.start('Step 1/4 \u00b7 Connecting to MCP server...')
1284
-
1285
- let introspection
1286
- try {
1287
- introspection = await introspectMcpServer(introspectionSource)
1288
- } catch (error) {
1289
- if (source.transport !== 'stdio' && isAuthRequiredError(error)) {
1290
- const discoveredAuth = error instanceof McpIntrospectionError ? discoverMcpAuthFromError(error) : null
1291
- const provider = inferMcpAuthProvider(source, discoveredAuth)
1292
- s?.stop('Server requires authentication')
1293
- let envVar = options.authEnv
1294
- let authType = options.authType
1295
- let authHeader = options.authHeader
1296
- let authTemplate = options.authTemplate
1297
- let usedOauthWrapper = false
1298
-
1299
- if (!usedOauthWrapper && discoveredAuth?.kind === 'platform' && (interactive || options.oauthWrapper)) {
1300
- const strategy = options.oauthWrapper
1301
- ? 'wrapper'
1302
- : interactive
1303
- ? await chooseOAuthImportStrategy(provider)
1304
- : 'token'
1305
-
1306
- if (strategy === 'wrapper') {
1307
- const wrapperSource = buildOauthWrapperSource(source)
1308
- if (!wrapperSource) {
1309
- throw new Error(formatAuthRequiredMessage('init', error, source))
1310
- }
1311
- introspectionSource = wrapperSource
1312
- runtimeAuthMode = 'platform'
1313
- s?.start('Step 1/4 · Reconnecting via local wrapper...')
1314
- try {
1315
- introspection = await introspectMcpServer(introspectionSource)
1316
- usedOauthWrapper = true
1317
- } catch (wrapperError) {
1318
- if (isAuthRequiredError(wrapperError)) {
1319
- throw new Error(`Wrapper-based OAuth import failed.
1320
- ${formatAuthRequiredMessage('init', wrapperError, source)}`)
1321
- }
1322
- throw new Error(`MCP introspection failed via local wrapper: ${wrapperError instanceof Error ? wrapperError.message : String(wrapperError)}`)
1323
- }
1324
- }
1325
- }
1326
-
1327
- if (!options.runtimeAuth && (discoveredAuth?.kind === 'platform' || usedOauthWrapper)) {
1328
- runtimeAuthMode = 'platform'
1329
- }
1330
-
1331
- if (usedOauthWrapper) {
1332
- envVar = envVar ?? (interactive
1333
- ? await clackText('Auth env var for generated non-platform targets (optional)', defaultAuthEnvVar(provider, discoveredAuth))
1334
- : undefined)
1335
- source = {
1336
- ...source,
1337
- auth: envVar
1338
- ? buildRemoteAuthConfig({
1339
- authEnv: envVar,
1340
- authType: provider?.tokenAuthType ?? 'bearer',
1341
- authHeader: provider?.authHeader,
1342
- authTemplate: provider?.authTemplate,
1343
- })
1344
- : { type: 'platform', mode: 'oauth' },
1345
- }
1346
- } else {
1347
- envVar = envVar ?? (interactive
1348
- ? await clackText('Auth env var for this MCP server', defaultAuthEnvVar(provider, discoveredAuth))
1349
- : '')
1350
- if (!envVar) {
1351
- throw new Error(formatAuthRequiredMessage('init', error, source))
1352
- }
1353
-
1354
- if (interactive && discoveredAuth?.kind === 'platform') {
1355
- const credential = await maybeCaptureOAuthCredential(provider, discoveredAuth.authorizationUrl)
1356
- applySessionCredential(envVar, credential)
1357
- }
1358
-
1359
- if (interactive && !authType) {
1360
- authType = await clackSelect<'bearer' | 'header'>('Auth type', [
1361
- { value: 'bearer', label: 'bearer', hint: 'Authorization: Bearer <token>' },
1362
- { value: 'header', label: 'header', hint: 'Custom header such as X-API-Key' },
1363
- ], discoveredAuth?.kind === 'header' ? 'header' : (provider?.tokenAuthType ?? 'bearer'))
1364
- }
1365
-
1366
- if (resolveRemoteAuthType({ authType, authHeader }) === 'header') {
1367
- if (interactive && !authHeader) {
1368
- authHeader = await clackText('Auth header name', discoveredAuth?.headerName ?? provider?.authHeader ?? 'X-API-Key')
1369
- }
1370
- if (interactive && !authTemplate) {
1371
- authTemplate = await clackText('Auth header template', provider?.authTemplate ?? '${value}')
1372
- }
1373
- }
1374
-
1375
- source = {
1376
- ...source,
1377
- auth: buildRemoteAuthConfig({
1378
- authEnv: envVar,
1379
- authType,
1380
- authHeader,
1381
- authTemplate,
1382
- }),
1383
- }
1384
- introspectionSource = source
1385
- s?.start('Step 1/4 · Reconnecting with auth...')
1386
- try {
1387
- introspection = await introspectMcpServer(introspectionSource)
1388
- } catch (retryError) {
1389
- if (isAuthRequiredError(retryError)) {
1390
- throw new Error(`Authentication failed after retry.
1391
- ${formatAuthRequiredMessage('init', retryError, source)}`)
1392
- }
1393
- throw new Error(`MCP introspection failed after auth retry: ${retryError instanceof Error ? retryError.message : String(retryError)}`)
1394
- }
1395
- }
1396
- } else {
1397
- s?.stop('Connection failed')
1398
- throw new Error(`MCP introspection failed: ${error instanceof Error ? error.message : String(error)}`)
1399
- }
1400
- }
1401
-
1402
- if (!introspection) {
1403
- throw new Error('MCP introspection did not return server metadata.')
1404
- }
1405
-
1406
- const serverLabel = introspection.serverInfo.title ?? introspection.serverInfo.name
1407
- s?.stop(`Connected: ${serverLabel} (${formatMcpDiscoverySummary(introspection)})`)
1408
- const quality = analyzeMcpQuality(introspection.tools)
1409
-
1410
- if (!options.jsonOutput && !runtime.quiet && quality.issues.length > 0) {
1411
- clack.note(formatMcpQualityLines(quality).join('\n'), 'MCP quality check')
1412
- }
1413
-
1414
- // Only ask for stdio auth env when the source has no env vars and no auth already
1415
- const stdioHasEnv = source.transport === 'stdio'
1416
- && source.env
1417
- && Object.keys(source.env).length > 0
1418
- const stdioNeedsAuthPrompt = source.transport === 'stdio'
1419
- && !stdioHasEnv
1420
- && !source.auth
1421
- && !options.authEnv
1422
-
1423
- const generatedAuthEnv = stdioNeedsAuthPrompt && interactive
1424
- ? await clackText('Auth env var for generated plugin (optional)', '')
1425
- : source.transport === 'stdio'
1426
- ? (options.authEnv ?? undefined)
1427
- : options.authEnv
1428
-
1429
- source = applyGeneratedAuthEnv(source, generatedAuthEnv)
1430
-
1431
- if (
1432
- interactive
1433
- && source.transport !== 'stdio'
1434
- && source.auth
1435
- && source.auth.type !== 'none'
1436
- && !options.runtimeAuth
1437
- ) {
1438
- runtimeAuthMode = source.auth.type === 'platform' ? 'platform' : runtimeAuthMode
1439
- runtimeAuthMode = await clackSelect<McpRuntimeAuthMode>('Claude/Cursor runtime auth', [
1440
- { value: 'inline', label: 'inline', hint: 'Generate env/header auth directly into plugin output' },
1441
- { value: 'platform', label: 'platform', hint: 'Use native platform-managed auth (for example OAuth/custom connector flows)' },
1442
- ], runtimeAuthMode)
1443
- }
1444
-
1445
- // ── Step 2/4 · Plugin identity ───────────────────────────────────
1446
-
1447
- if (!options.jsonOutput && !runtime.quiet) {
1448
- clack.log.step('Step 2/4 \u00b7 Plugin identity')
1449
- }
1450
-
1451
- const defaultPluginName = options.name ? toKebabCase(options.name) : derivePluginName(introspection, source)
1452
- const pluginName = toKebabCase(
1453
- options.name ?? (interactive
1454
- ? await clackText('Plugin name', defaultPluginName)
1455
- : defaultPluginName),
1456
- )
1457
- const defaultDisplayName = options.displayName ?? deriveDisplayName(introspection, pluginName)
1458
- const displayName = options.displayName ?? (interactive
1459
- ? await clackText('Display name', defaultDisplayName)
1460
- : defaultDisplayName)
1461
- const defaultAuthor = process.env.USER ?? ''
1462
- const authorName = options.author ?? (interactive
1463
- ? await clackText('Author name', defaultAuthor)
1464
- : defaultAuthor)
1465
-
1466
- // ── Step 3/4 · Build settings ────────────────────────────────────
1467
-
1468
- if (!options.jsonOutput && !runtime.quiet) {
1469
- clack.log.step('Step 3/4 \u00b7 Build settings')
1470
- }
1471
-
1472
- const defaultTargetsValue = options.targets ?? defaultTargets
1473
- const targetsRaw = options.targets ?? (interactive
1474
- ? await clackText('Platforms (comma-separated)', defaultTargetsValue)
1475
- : defaultTargetsValue)
1476
- const targets = parseTargetPlatforms(targetsRaw)
1477
-
1478
- const defaultGrouping: McpSkillGrouping = 'workflow'
1479
- const grouping: McpSkillGrouping = options.grouping
1480
- ? parseChoiceOption(options.grouping, MCP_SKILL_GROUPINGS, 'Skill grouping')
1481
- : interactive
1482
- ? await clackSelect<McpSkillGrouping>('Skill grouping', [
1483
- { value: 'workflow', label: 'workflow', hint: 'Group related tools into workflow skills' },
1484
- { value: 'tool', label: 'tool', hint: 'One skill per tool' },
1485
- ], defaultGrouping)
1486
- : defaultGrouping
1487
-
1488
- const defaultHookModeValue = defaultHookMode(source)
1489
- const hookMode: McpHookMode = options.hooks
1490
- ? parseChoiceOption(options.hooks, MCP_HOOK_MODES, 'Install-ready hooks')
1491
- : interactive
1492
- ? await clackSelect<McpHookMode>('Install-ready hooks', [
1493
- { value: 'none', label: 'none', hint: 'No install hooks' },
1494
- { value: 'safe', label: 'safe', hint: 'Auto-generate safe install hooks' },
1495
- ], defaultHookModeValue)
1496
- : defaultHookModeValue
1497
-
1498
- // ── Step 4/4 · Generating scaffold ───────────────────────────────
1499
-
1500
- const g = createSpinner(runtime)
1501
- g?.start('Step 4/4 \u00b7 Generating scaffold...')
1502
- const plan = await planMcpScaffold({
1503
- rootDir: process.cwd(),
1504
- pluginName,
1505
- authorName,
1506
- targets,
1507
- source,
1508
- runtimeAuthMode,
1509
- introspection,
1510
- displayName,
1511
- skillGrouping: grouping,
1512
- hookMode,
1513
- })
1514
- const createdFiles = plan.files
1515
- .filter((file) => file.action === 'create')
1516
- .map((file) => file.relativePath)
1517
- const updatedFiles = plan.files
1518
- .filter((file) => file.action === 'update')
1519
- .map((file) => file.relativePath)
1520
-
1521
- if (!runtime.dryRun) {
1522
- await applyMcpScaffoldPlan(process.cwd(), plan)
1523
- }
1524
-
1525
- const lintResult = runtime.dryRun
1526
- ? { errors: 0, warnings: 0, issues: [] }
1527
- : await lintProject(process.cwd())
1528
- const summary = buildInitSummary({
1529
- pluginName,
1530
- displayName,
1531
- source: rawSource,
1532
- toolCount: introspection.tools.length,
1533
- targets,
1534
- grouping,
1535
- requestedHookMode: hookMode,
1536
- hookMode: plan.generatedHookMode,
1537
- hookEvents: plan.generatedHookEvents,
1538
- files: plan.generatedFiles,
1539
- createdFiles,
1540
- updatedFiles,
1541
- lint: {
1542
- errors: lintResult.errors,
1543
- warnings: lintResult.warnings,
1544
- },
1545
- quality,
1546
- dryRun: runtime.dryRun,
1547
- })
1548
-
1549
- if (options.jsonOutput) {
1550
- printJson(summary)
1551
- return
1552
- }
1553
-
1554
- g?.stop(`${runtime.dryRun ? 'Planned' : 'Created'} ${summary.files.length} files`)
1555
-
1556
- if (runtime.quiet) {
1557
- return
1558
- }
1559
-
1560
- if (summary.createdFiles.length > 0) {
1561
- clack.log.info(`Create: ${summary.createdFiles.join(', ')}`)
1562
- }
1563
- if (summary.updatedFiles.length > 0) {
1564
- clack.log.info(`Update: ${summary.updatedFiles.join(', ')}`)
1565
- }
1566
-
1567
- if (!runtime.dryRun) {
1568
- if (lintResult.errors > 0) {
1569
- clack.log.error(`Lint: ${lintResult.errors} errors, ${lintResult.warnings} warnings`)
1570
- } else if (lintResult.warnings > 0) {
1571
- clack.log.warn(`Lint: ${lintResult.errors} errors, ${lintResult.warnings} warnings`)
1572
- } else {
1573
- clack.log.success('Lint: 0 errors, 0 warnings')
1574
- }
1575
- } else {
1576
- clack.log.info('Dry run only: scaffold files were not written and lint was skipped.')
1577
- }
1578
-
1579
- if (!runtime.dryRun && lintResult.issues.length > 0) {
1580
- for (const issue of lintResult.issues) {
1581
- const levelLabel = issue.level === 'error' ? 'ERROR' : 'WARN '
1582
- const platformLabel = issue.platform ? `[${issue.platform}] ` : ''
1583
- const loc = issue.file ? `${issue.file}: ` : ''
1584
- const message = `${levelLabel} ${issue.code} ${platformLabel}${loc}${issue.message}`
1585
- if (issue.level === 'error') {
1586
- clack.log.error(message)
1587
- } else {
1588
- clack.log.warn(message)
1589
- }
1590
- }
1591
- }
1592
-
1593
- if (summary.notes.length > 0) {
1594
- for (const n of summary.notes) {
1595
- clack.log.info(n)
1596
- }
1597
- }
1598
-
1599
- if (summary.quality.issues.length > 0) {
1600
- for (const line of formatMcpQualityLines(summary.quality)) {
1601
- clack.log.info(line)
1602
- }
1603
- }
1604
-
1605
- // Build a concrete test prompt from the first tool's example request
1606
- const firstTool = introspection.tools[0]
1607
- const testPrompt = firstTool ? buildToolExampleRequest(firstTool) : undefined
1608
- const installTarget = targets[0]
1609
- const installCommand = summary.hookMode === 'safe'
1610
- ? `pluxx install --trust --target ${installTarget}`
1611
- : `pluxx install --target ${installTarget}`
1612
-
1613
- const nextStepLines = [
1614
- '1. Review INSTRUCTIONS.md and skills/',
1615
- '2. pluxx build',
1616
- `3. ${installCommand}`,
1617
- ]
1618
- if (testPrompt) {
1619
- nextStepLines.push(`4. Test in ${installTarget}: "${testPrompt}"`)
1620
- }
1621
- nextStepLines.push('')
1622
- nextStepLines.push(`To refresh later: pluxx sync --from-mcp`)
1623
-
1624
- clack.note(nextStepLines.join('\n'), 'Next steps')
1625
-
1626
- clack.outro(runtime.dryRun ? 'Dry run complete' : 'Scaffold complete')
1627
- } catch (error) {
1628
- if (error instanceof PromptCancelledError) {
1629
- if (!options.jsonOutput && !runtime.quiet) {
1630
- clack.cancel(error.message)
1631
- }
1632
- return
1633
- }
1634
-
1635
- throw error instanceof Error ? error : new Error(String(error))
1636
- }
1637
- }
1638
-
1639
- /** Wrapper for clack.text that handles cancellation. */
1640
- async function clackText(message: string, defaultValue?: string): Promise<string> {
1641
- const result = await clack.text({
1642
- message,
1643
- defaultValue,
1644
- placeholder: defaultValue,
1645
- })
1646
- if (clack.isCancel(result)) {
1647
- throw new PromptCancelledError()
1648
- }
1649
- return result
1650
- }
1651
-
1652
- /** Wrapper for clack.password that handles cancellation. */
1653
- async function clackPassword(message: string): Promise<string> {
1654
- const result = await clack.password({
1655
- message,
1656
- mask: '*',
1657
- })
1658
- if (clack.isCancel(result)) {
1659
- throw new PromptCancelledError()
1660
- }
1661
- return result
1662
- }
1663
-
1664
- /** Wrapper for clack.select that handles cancellation. */
1665
- async function clackSelect<T extends string>(
1666
- message: string,
1667
- options: Array<{ value: T; label: string; hint?: string }>,
1668
- initialValue: T,
1669
- ): Promise<T> {
1670
- const result = await clack.select({
1671
- message,
1672
- options: options as Array<{ value: string; label: string; hint?: string }>,
1673
- initialValue: initialValue as string,
1674
- })
1675
- if (clack.isCancel(result)) {
1676
- throw new PromptCancelledError()
1677
- }
1678
- return result as T
1679
- }
1680
-
1681
- async function runSync() {
1682
- const fromMcpFlag = args.indexOf('--from-mcp')
1683
- const fromMcpInput = fromMcpFlag !== -1 && args[fromMcpFlag + 1] && !args[fromMcpFlag + 1].startsWith('-')
1684
- ? args[fromMcpFlag + 1]
1685
- : undefined
1686
- const source = fromMcpInput ? parseMcpSourceInput(fromMcpInput) : undefined
1687
- const result = runtime.dryRun
1688
- ? await planSyncFromMcp({
1689
- rootDir: process.cwd(),
1690
- source,
1691
- })
1692
- : await syncFromMcp({
1693
- rootDir: process.cwd(),
1694
- source,
1695
- })
1696
-
1697
- if (runtime.jsonOutput) {
1698
- printJson({
1699
- ...result,
1700
- dryRun: runtime.dryRun,
1701
- })
1702
- return
1703
- }
1704
-
1705
- if (runtime.quiet) {
1706
- return
1707
- }
1708
-
1709
- const lines = formatSyncSummary(result, process.cwd())
1710
- if (runtime.dryRun) {
1711
- console.log('Dry run: planned sync changes')
1712
- }
1713
- lines.forEach((line) => console.log(line))
1714
- }
1715
-
1716
- async function runDoctor() {
1717
- const consumerMode = readFlag(args, '--consumer')
1718
- const doctorPath = args.slice(1).find((value) => !value.startsWith('-'))
1719
- const rootDir = doctorPath ? resolve(process.cwd(), doctorPath) : process.cwd()
1720
- const report = consumerMode
1721
- ? await doctorConsumer(rootDir)
1722
- : await doctorProject(rootDir)
1723
-
1724
- if (runtime.jsonOutput) {
1725
- printJson(report)
1726
- } else if (!runtime.quiet) {
1727
- printDoctorReport(report)
1728
- }
1729
-
1730
- if (!report.ok) {
1731
- process.exit(1)
1732
- }
1733
- }
1734
-
1735
- async function runAgent() {
1736
- const subcommand = args[1]
1737
-
1738
- if (subcommand === 'prepare') {
1739
- const plan = await planAgentPrepare(process.cwd(), {
1740
- docsUrl: readOption(args, '--docs'),
1741
- websiteUrl: readOption(args, '--website'),
1742
- contextPaths: readMultiValueOption(args, '--context'),
1743
- })
1744
-
1745
- if (!runtime.dryRun) {
1746
- await applyAgentPreparePlan(process.cwd(), plan)
1747
- }
1748
-
1749
- const summary = {
1750
- pluginName: plan.pluginName,
1751
- targetCount: plan.targetCount,
1752
- toolCount: plan.toolCount,
1753
- skillCount: plan.skillCount,
1754
- editableFiles: plan.editableFiles,
1755
- protectedFiles: plan.protectedFiles,
1756
- generatedFiles: plan.generatedFiles,
1757
- createdFiles: plan.createdFiles,
1758
- updatedFiles: plan.updatedFiles,
1759
- lint: plan.lint,
1760
- contextInputs: plan.contextInputs,
1761
- dryRun: runtime.dryRun,
1762
- }
1763
-
1764
- if (runtime.jsonOutput) {
1765
- printJson(summary)
1766
- return
1767
- }
1768
-
1769
- if (runtime.quiet) {
1770
- return
1771
- }
1772
-
1773
- console.log(`${runtime.dryRun ? 'Planned' : 'Prepared'} agent context for ${plan.pluginName}`)
1774
- if (plan.createdFiles.length > 0) {
1775
- console.log(` Create: ${plan.createdFiles.join(', ')}`)
1776
- }
1777
- if (plan.updatedFiles.length > 0) {
1778
- console.log(` Update: ${plan.updatedFiles.join(', ')}`)
1779
- }
1780
- console.log(` Editable files: ${plan.editableFiles.join(', ')}`)
1781
- console.log(` Protected files: ${plan.protectedFiles.join(', ')}`)
1782
- console.log(` Lint snapshot: ${plan.lint.errors} error(s), ${plan.lint.warnings} warning(s)`)
1783
- if (plan.contextInputs.length > 0) {
1784
- console.log(` Context inputs: ${plan.contextInputs.join(', ')}`)
1785
- }
1786
- console.log('')
1787
- console.log('Next steps:')
1788
- console.log(' 1. Review .pluxx/agent/context.md')
1789
- console.log(' 2. Hand the context pack to Claude Code or Codex')
1790
- console.log(' 3. Keep edits inside Pluxx-managed sections')
1791
- return
1792
- }
1793
-
1794
- if (subcommand === 'prompt') {
1795
- const kind = args[2] as AgentPromptKind | undefined
1796
- if (!kind || !AGENT_PROMPT_KINDS.includes(kind)) {
1797
- console.error(`Usage: pluxx agent prompt <${AGENT_PROMPT_KINDS.join('|')}> [--json] [--dry-run] [--quiet]`)
1798
- process.exit(1)
1799
- }
1800
-
1801
- const plan = await planAgentPrompt(process.cwd(), kind)
1802
- if (!runtime.dryRun) {
1803
- await applyAgentPromptPlan(process.cwd(), plan)
1804
- }
1805
-
1806
- const summary = {
1807
- pluginName: plan.pluginName,
1808
- kind: plan.kind,
1809
- outputPath: plan.outputPath,
1810
- createdFiles: plan.createdFiles,
1811
- updatedFiles: plan.updatedFiles,
1812
- dryRun: runtime.dryRun,
1813
- }
1814
-
1815
- if (runtime.jsonOutput) {
1816
- printJson(summary)
1817
- return
1818
- }
1819
-
1820
- if (runtime.quiet) {
1821
- return
1822
- }
1823
-
1824
- console.log(`${runtime.dryRun ? 'Planned' : 'Generated'} ${plan.kind} prompt for ${plan.pluginName}`)
1825
- console.log(` Output: ${plan.outputPath}`)
1826
- return
1827
- }
1828
-
1829
- if (subcommand === 'run') {
1830
- const kind = args[2] as AgentPromptKind | undefined
1831
- if (!kind || !AGENT_PROMPT_KINDS.includes(kind)) {
1832
- console.error(`Usage: pluxx agent run <${AGENT_PROMPT_KINDS.join('|')}> --runner <${AGENT_RUNNERS.join('|')}> [--model NAME] [--attach URL (opencode only)] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`)
1833
- process.exit(1)
1834
- }
1835
-
1836
- const runnerRaw = readOption(args, '--runner')
1837
- if (!runnerRaw || !AGENT_RUNNERS.includes(runnerRaw as AgentRunner)) {
1838
- console.error(`Usage: pluxx agent run <${AGENT_PROMPT_KINDS.join('|')}> --runner <${AGENT_RUNNERS.join('|')}> [--model NAME] [--attach URL (opencode only)] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`)
1839
- process.exit(1)
1840
- }
1841
- const verboseRunner = args.includes('--verbose-runner')
1842
-
1843
- const plan = await planAgentRun(process.cwd(), kind, {
1844
- runner: runnerRaw as AgentRunner,
1845
- model: readOption(args, '--model'),
1846
- attach: readOption(args, '--attach'),
1847
- verify: !args.includes('--no-verify'),
1848
- }, {
1849
- docsUrl: readOption(args, '--docs'),
1850
- websiteUrl: readOption(args, '--website'),
1851
- contextPaths: readMultiValueOption(args, '--context'),
1852
- })
1853
-
1854
- const summary = {
1855
- pluginName: plan.pluginName,
1856
- kind: plan.kind,
1857
- runner: plan.runner,
1858
- model: plan.model,
1859
- verify: plan.verify,
1860
- command: plan.command,
1861
- commandDisplay: plan.commandDisplay,
1862
- promptPath: plan.promptPath,
1863
- contextPath: plan.contextPath,
1864
- createdFiles: plan.createdFiles,
1865
- updatedFiles: plan.updatedFiles,
1866
- contextInputs: plan.contextInputs,
1867
- dryRun: runtime.dryRun,
1868
- }
1869
-
1870
- if (runtime.dryRun) {
1871
- if (runtime.jsonOutput) {
1872
- printJson(summary)
1873
- } else if (!runtime.quiet) {
1874
- console.log(`Planned ${plan.kind} run for ${plan.pluginName}`)
1875
- console.log(` Runner: ${plan.runner}`)
1876
- console.log(` Model: ${plan.model.display}`)
1877
- console.log(` Command: ${plan.commandDisplay}`)
1878
- }
1879
- return
1880
- }
1881
-
1882
- const result = await runAgentPlan(process.cwd(), plan, {
1883
- streamOutput: verboseRunner && !runtime.jsonOutput && !runtime.quiet,
1884
- })
1885
-
1886
- if (runtime.jsonOutput) {
1887
- printJson(result)
1888
- if (!result.ok) {
1889
- process.exit(1)
1890
- }
1891
- return
1892
- }
1893
-
1894
- if (!runtime.quiet) {
1895
- console.log(`Completed ${result.kind} run for ${result.pluginName} via ${result.runner}`)
1896
- console.log(` Model: ${result.model.display}`)
1897
- if (!verboseRunner) {
1898
- console.log(' Runner logs: suppressed (use --verbose-runner to stream)')
1899
- }
1900
- if (result.verification) {
1901
- console.log(` Verification: ${result.verification.ok ? 'passed' : 'failed'}`)
1902
- }
1903
- }
1904
-
1905
- if (!result.ok) {
1906
- process.exit(1)
1907
- }
1908
- return
1909
- }
1910
-
1911
- console.error(`Usage: pluxx agent <prepare|prompt|run> [--docs URL] [--website URL] [--context <files...>] [--json] [--dry-run] [--quiet]`)
1912
- process.exit(1)
1913
- }
1914
-
1915
- async function runAutopilot() {
1916
- const initOptions = parseInitFromMcpOptions(args)
1917
- let runnerRaw = readOption(args, '--runner')
1918
- let modeRaw = readOption(args, '--mode')
1919
- let docsUrl = readOption(args, '--docs')
1920
- let websiteUrl = readOption(args, '--website')
1921
- const contextPaths = readMultiValueOption(args, '--context')
1922
- const model = readOption(args, '--model')
1923
- const attach = readOption(args, '--attach')
1924
- const reviewRequested = args.includes('--review')
1925
- const verify = !args.includes('--no-verify')
1926
- const verboseRunner = args.includes('--verbose-runner')
1927
- const interactive = !runtime.jsonOutput && runtime.isInteractive && !initOptions.assumeDefaults
1928
- let authEnv = initOptions.authEnv
1929
- let authType = initOptions.authType
1930
- let authHeader = initOptions.authHeader
1931
- let authTemplate = initOptions.authTemplate
1932
- let runtimeAuthMode = resolveRuntimeAuthMode(initOptions.runtimeAuth)
1933
-
1934
- if (!initOptions.source && !interactive) {
1935
- console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join('|')}> [--mode <${AUTOPILOT_MODES.join('|')}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--auth-env ENV] [--auth-type bearer|header|platform] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--oauth-wrapper] [--website URL] [--docs URL] [--context <files...>] [--review] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`)
1936
- process.exit(1)
1937
- }
1938
-
1939
- if ((!runnerRaw || !AGENT_RUNNERS.includes(runnerRaw as AgentRunner)) && !interactive) {
1940
- console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join('|')}> [--mode <${AUTOPILOT_MODES.join('|')}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--auth-env ENV] [--auth-type bearer|header|platform] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--oauth-wrapper] [--website URL] [--docs URL] [--context <files...>] [--review] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`)
1941
- process.exit(1)
1942
- }
1943
-
1944
- if (modeRaw && !AUTOPILOT_MODES.includes(modeRaw as AutopilotMode)) {
1945
- console.error(`Autopilot mode must be one of: ${AUTOPILOT_MODES.join(', ')}`)
1946
- process.exit(1)
1947
- }
1948
- let tempDir: string | undefined
1949
-
1950
- try {
1951
- if (!runtime.jsonOutput && !runtime.quiet && interactive) {
1952
- clack.intro('pluxx autopilot')
1953
- }
1954
-
1955
- const rawSource = initOptions.source ?? (interactive
1956
- ? await clackText('MCP server URL or local command')
1957
- : '')
1958
- if (!rawSource) {
1959
- throw new Error('Provide an MCP server URL or local command. Example: pluxx autopilot --from-mcp https://example.com/mcp --runner codex')
1960
- }
1961
-
1962
- const runner = runnerRaw && AGENT_RUNNERS.includes(runnerRaw as AgentRunner)
1963
- ? runnerRaw as AgentRunner
1964
- : interactive
1965
- ? await clackSelect<AgentRunner>('Agent runner', [
1966
- { value: 'codex', label: 'codex', hint: 'Use Codex headless mode for refinement' },
1967
- { value: 'claude', label: 'claude', hint: 'Use Claude Code headless mode for refinement' },
1968
- { value: 'cursor', label: 'cursor', hint: 'Use Cursor CLI headless mode for refinement' },
1969
- { value: 'opencode', label: 'opencode', hint: 'Use OpenCode run mode for refinement' },
1970
- ], 'codex')
1971
- : (() => { throw new Error(`Choose a runner: ${AGENT_RUNNERS.join(', ')}`) })()
1972
-
1973
- const mode = modeRaw && AUTOPILOT_MODES.includes(modeRaw as AutopilotMode)
1974
- ? modeRaw as AutopilotMode
1975
- : interactive
1976
- ? await clackSelect<AutopilotMode>('Autopilot mode', [
1977
- { value: 'quick', label: 'quick', hint: 'Fastest path; skip most agent work unless metadata is weak' },
1978
- { value: 'standard', label: 'standard', hint: 'Balanced path; run agent passes only when they add value' },
1979
- { value: 'thorough', label: 'thorough', hint: 'Always run taxonomy + instructions and include review' },
1980
- ], 'standard')
1981
- : 'standard'
1982
-
1983
- if (runner !== 'opencode' && attach) {
1984
- throw new Error('--attach is only supported for the opencode runner.')
1985
- }
1986
-
1987
- let source = parseMcpSourceInput(rawSource, initOptions.transport)
1988
- let introspectionSource = source
1989
- const configuredRemoteAuth = source.transport === 'stdio'
1990
- ? undefined
1991
- : buildRemoteAuthConfig({
1992
- authEnv,
1993
- authType,
1994
- authHeader,
1995
- authTemplate,
1996
- })
1997
-
1998
- if (configuredRemoteAuth && !source.auth) {
1999
- source = {
2000
- ...source,
2001
- auth: configuredRemoteAuth,
2002
- } satisfies McpServer
2003
- }
2004
-
2005
- const connectSpinner = createSpinner(runtime)
2006
- connectSpinner?.start('Autopilot · Connecting to MCP server...')
2007
-
2008
- let introspection
2009
- try {
2010
- introspection = await introspectMcpServer(introspectionSource)
2011
- } catch (error) {
2012
- if (source.transport !== 'stdio' && isAuthRequiredError(error)) {
2013
- const discoveredAuth = error instanceof McpIntrospectionError ? discoverMcpAuthFromError(error) : null
2014
- const provider = inferMcpAuthProvider(source, discoveredAuth)
2015
- connectSpinner?.stop('Server requires authentication')
2016
- let usedOauthWrapper = false
2017
-
2018
- if (!usedOauthWrapper && discoveredAuth?.kind === 'platform' && (interactive || initOptions.oauthWrapper)) {
2019
- const strategy = initOptions.oauthWrapper
2020
- ? 'wrapper'
2021
- : interactive
2022
- ? await chooseOAuthImportStrategy(provider)
2023
- : 'token'
2024
-
2025
- if (strategy === 'wrapper') {
2026
- const wrapperSource = buildOauthWrapperSource(source)
2027
- if (!wrapperSource) {
2028
- throw new Error(formatAuthRequiredMessage('autopilot', error, source))
2029
- }
2030
- introspectionSource = wrapperSource
2031
- runtimeAuthMode = 'platform'
2032
- connectSpinner?.start('Autopilot · Reconnecting via local wrapper...')
2033
- try {
2034
- introspection = await introspectMcpServer(introspectionSource)
2035
- usedOauthWrapper = true
2036
- } catch (wrapperError) {
2037
- if (isAuthRequiredError(wrapperError)) {
2038
- throw new Error(`Wrapper-based OAuth import failed.
2039
- ${formatAuthRequiredMessage('autopilot', wrapperError, source)}`)
2040
- }
2041
- throw new Error(`MCP introspection failed via local wrapper: ${wrapperError instanceof Error ? wrapperError.message : String(wrapperError)}`)
2042
- }
2043
- }
2044
- }
2045
-
2046
- if (!initOptions.runtimeAuth && (discoveredAuth?.kind === 'platform' || usedOauthWrapper)) {
2047
- runtimeAuthMode = 'platform'
2048
- }
2049
-
2050
- if (usedOauthWrapper) {
2051
- authEnv = authEnv ?? (interactive
2052
- ? await clackText('Auth env var for generated non-platform targets (optional)', defaultAuthEnvVar(provider, discoveredAuth))
2053
- : undefined)
2054
- source = {
2055
- ...source,
2056
- auth: authEnv
2057
- ? buildRemoteAuthConfig({
2058
- authEnv,
2059
- authType: provider?.tokenAuthType ?? 'bearer',
2060
- authHeader: provider?.authHeader,
2061
- authTemplate: provider?.authTemplate,
2062
- })
2063
- : { type: 'platform', mode: 'oauth' },
2064
- }
2065
- } else {
2066
- authEnv = authEnv ?? (interactive
2067
- ? await clackText('Auth env var for this MCP server', defaultAuthEnvVar(provider, discoveredAuth))
2068
- : '')
2069
- if (!authEnv) {
2070
- throw new Error(formatAuthRequiredMessage('autopilot', error, source))
2071
- }
2072
-
2073
- if (interactive && discoveredAuth?.kind === 'platform') {
2074
- const credential = await maybeCaptureOAuthCredential(provider, discoveredAuth.authorizationUrl)
2075
- applySessionCredential(authEnv, credential)
2076
- }
2077
-
2078
- if (interactive && !authType) {
2079
- authType = await clackSelect<'bearer' | 'header'>('Auth type', [
2080
- { value: 'bearer', label: 'bearer', hint: 'Authorization: Bearer <token>' },
2081
- { value: 'header', label: 'header', hint: 'Custom header such as X-API-Key' },
2082
- ], discoveredAuth?.kind === 'header' ? 'header' : (provider?.tokenAuthType ?? 'bearer'))
2083
- }
2084
-
2085
- if (resolveRemoteAuthType({ authType, authHeader }) === 'header') {
2086
- if (interactive && !authHeader) {
2087
- authHeader = await clackText('Auth header name', discoveredAuth?.headerName ?? provider?.authHeader ?? 'X-API-Key')
2088
- }
2089
- if (interactive && !authTemplate) {
2090
- authTemplate = await clackText('Auth header template', provider?.authTemplate ?? '${value}')
2091
- }
2092
- }
2093
-
2094
- source = {
2095
- ...source,
2096
- auth: buildRemoteAuthConfig({
2097
- authEnv,
2098
- authType,
2099
- authHeader,
2100
- authTemplate,
2101
- }),
2102
- }
2103
- introspectionSource = source
2104
- connectSpinner?.start('Autopilot · Reconnecting with auth...')
2105
- try {
2106
- introspection = await introspectMcpServer(introspectionSource)
2107
- } catch (retryError) {
2108
- if (isAuthRequiredError(retryError)) {
2109
- throw new Error(`Authentication failed after retry.
2110
- ${formatAuthRequiredMessage('autopilot', retryError, source)}`)
2111
- }
2112
- throw new Error(`MCP introspection failed after auth retry: ${retryError instanceof Error ? retryError.message : String(retryError)}`)
2113
- }
2114
- }
2115
- } else {
2116
- connectSpinner?.stop('Connection failed')
2117
- throw new Error(`MCP introspection failed: ${error instanceof Error ? error.message : String(error)}`)
2118
- }
2119
- }
2120
-
2121
- if (!introspection) {
2122
- throw new Error('MCP introspection did not return server metadata.')
2123
- }
2124
-
2125
- const stdioHasEnv = source.transport === 'stdio'
2126
- && source.env
2127
- && Object.keys(source.env).length > 0
2128
- const generatedAuthEnv = source.transport === 'stdio' && !stdioHasEnv
2129
- ? authEnv ?? undefined
2130
- : authEnv
2131
-
2132
- source = applyGeneratedAuthEnv(source, generatedAuthEnv, {
2133
- authType,
2134
- authHeader,
2135
- authTemplate,
2136
- })
2137
-
2138
- if (
2139
- interactive
2140
- && source.transport !== 'stdio'
2141
- && source.auth
2142
- && source.auth.type !== 'none'
2143
- && !initOptions.runtimeAuth
2144
- ) {
2145
- runtimeAuthMode = source.auth.type === 'platform' ? 'platform' : runtimeAuthMode
2146
- runtimeAuthMode = await clackSelect<McpRuntimeAuthMode>('Claude/Cursor runtime auth', [
2147
- { value: 'inline', label: 'inline', hint: 'Generate env/header auth directly into plugin output' },
2148
- { value: 'platform', label: 'platform', hint: 'Use native platform-managed auth (for example OAuth/custom connector flows)' },
2149
- ], runtimeAuthMode)
2150
- }
2151
- connectSpinner?.stop(`Connected: ${introspection.serverInfo.title ?? introspection.serverInfo.name} (${formatMcpDiscoverySummary(introspection)})`)
2152
- const quality = analyzeMcpQuality(introspection.tools)
2153
-
2154
- if (!runtime.jsonOutput && !runtime.quiet && quality.issues.length > 0) {
2155
- clack.note(formatMcpQualityLines(quality).join('\n'), 'MCP quality check')
2156
- }
2157
-
2158
- const defaultPluginName = initOptions.name ? toKebabCase(initOptions.name) : derivePluginName(introspection, source)
2159
- const pluginName = toKebabCase(
2160
- initOptions.name ?? (interactive
2161
- ? await clackText('Plugin name', defaultPluginName)
2162
- : defaultPluginName),
2163
- )
2164
- const defaultDisplayName = initOptions.displayName ?? deriveDisplayName(introspection, pluginName)
2165
- const displayName = initOptions.displayName ?? (interactive
2166
- ? await clackText('Display name', defaultDisplayName)
2167
- : defaultDisplayName)
2168
- const defaultAuthorName = initOptions.author ?? process.env.USER ?? ''
2169
- const authorName = initOptions.author ?? (interactive
2170
- ? await clackText('Author name', defaultAuthorName)
2171
- : defaultAuthorName)
2172
- const targetsRaw = initOptions.targets ?? (interactive
2173
- ? await clackText('Platforms (comma-separated)', DEFAULT_INIT_TARGETS.join(','))
2174
- : DEFAULT_INIT_TARGETS.join(','))
2175
- const targets = parseTargetPlatforms(targetsRaw)
2176
- const grouping = initOptions.grouping
2177
- ? parseChoiceOption(initOptions.grouping, MCP_SKILL_GROUPINGS, 'Skill grouping')
2178
- : interactive
2179
- ? await clackSelect<McpSkillGrouping>('Skill grouping', [
2180
- { value: 'workflow', label: 'workflow', hint: 'Group related tools into workflow skills' },
2181
- { value: 'tool', label: 'tool', hint: 'One skill per tool' },
2182
- ], 'workflow')
2183
- : 'workflow'
2184
- const requestedHookMode = initOptions.hooks
2185
- ? parseChoiceOption(initOptions.hooks, MCP_HOOK_MODES, 'Install-ready hooks')
2186
- : interactive
2187
- ? await clackSelect<McpHookMode>('Install-ready hooks', [
2188
- { value: 'none', label: 'none', hint: 'No install hooks' },
2189
- { value: 'safe', label: 'safe', hint: 'Auto-generate safe install hooks' },
2190
- ], defaultHookMode(source))
2191
- : defaultHookMode(source)
2192
-
2193
- if (quality.warnings > 0) {
2194
- if (!websiteUrl) {
2195
- websiteUrl = introspection.serverInfo.websiteUrl
2196
- }
2197
-
2198
- if (interactive) {
2199
- websiteUrl = await clackText('Website URL for agent context (optional)', websiteUrl ?? '')
2200
- docsUrl = await clackText('Docs URL for agent context (optional)', docsUrl ?? '')
2201
- }
2202
- }
2203
-
2204
- const passDecisions = planAutopilotPasses({
2205
- mode,
2206
- quality,
2207
- reviewRequested,
2208
- docsUrl,
2209
- websiteUrl,
2210
- contextPaths,
2211
- })
2212
- const totalSteps = countAutopilotSteps({
2213
- taxonomy: passDecisions.taxonomy,
2214
- instructions: passDecisions.instructions,
2215
- review: passDecisions.review,
2216
- verify,
2217
- })
2218
-
2219
- const workspaceRoot = runtime.dryRun
2220
- ? await mkdtemp(`${tmpdir()}/pluxx-autopilot-`)
2221
- : process.cwd()
2222
-
2223
- tempDir = runtime.dryRun ? workspaceRoot : undefined
2224
-
2225
- const scaffoldSpinner = createSpinner(runtime)
2226
- scaffoldSpinner?.start(`Autopilot 2/${totalSteps} · Planning scaffold...`)
2227
- const scaffoldPlan = await planMcpScaffold({
2228
- rootDir: workspaceRoot,
2229
- pluginName,
2230
- authorName,
2231
- targets,
2232
- source,
2233
- runtimeAuthMode,
2234
- introspection,
2235
- displayName,
2236
- skillGrouping: grouping,
2237
- hookMode: requestedHookMode,
2238
- })
2239
- const initCreatedFiles = scaffoldPlan.files.filter((file) => file.action === 'create').map((file) => file.relativePath)
2240
- const initUpdatedFiles = scaffoldPlan.files.filter((file) => file.action === 'update').map((file) => file.relativePath)
2241
-
2242
- await applyMcpScaffoldPlan(workspaceRoot, scaffoldPlan)
2243
- scaffoldSpinner?.stop(`${runtime.dryRun ? 'Planned' : 'Generated'} scaffold for ${pluginName}`)
2244
-
2245
- const agentContextOptions = {
2246
- docsUrl,
2247
- websiteUrl,
2248
- contextPaths,
2249
- }
2250
-
2251
- const agentSpinner = createSpinner(runtime)
2252
- const taxonomyPlan = passDecisions.taxonomy.enabled
2253
- ? await (async () => {
2254
- agentSpinner?.start(`Autopilot 3/${totalSteps} · Planning taxonomy pass...`)
2255
- const plan = await planAgentRun(workspaceRoot, 'taxonomy', {
2256
- runner,
2257
- model,
2258
- attach,
2259
- verify: false,
2260
- }, agentContextOptions)
2261
- agentSpinner?.stop('Planned taxonomy pass')
2262
- return plan
2263
- })()
2264
- : undefined
2265
- const instructionsPlan = passDecisions.instructions.enabled
2266
- ? await (async () => {
2267
- const step = 3 + Number(passDecisions.taxonomy.enabled)
2268
- agentSpinner?.start(`Autopilot ${step}/${totalSteps} · Planning instructions pass...`)
2269
- const plan = await planAgentRun(workspaceRoot, 'instructions', {
2270
- runner,
2271
- model,
2272
- attach,
2273
- verify: false,
2274
- }, agentContextOptions)
2275
- agentSpinner?.stop('Planned instructions pass')
2276
- return plan
2277
- })()
2278
- : undefined
2279
- const reviewPlan = passDecisions.review.enabled
2280
- ? await (async () => {
2281
- const step = 3 + Number(passDecisions.taxonomy.enabled) + Number(passDecisions.instructions.enabled)
2282
- agentSpinner?.start(`Autopilot ${step}/${totalSteps} · Planning review pass...`)
2283
- const plan = await planAgentRun(workspaceRoot, 'review', {
2284
- runner,
2285
- model,
2286
- attach,
2287
- verify: false,
2288
- }, agentContextOptions)
2289
- agentSpinner?.stop('Planned review pass')
2290
- return plan
2291
- })()
2292
- : undefined
2293
-
2294
- if (runtime.dryRun) {
2295
- const summary: AutopilotSummary = {
2296
- ok: true,
2297
- pluginName,
2298
- displayName,
2299
- source: rawSource,
2300
- mode,
2301
- runner,
2302
- model: taxonomyPlan?.model ?? instructionsPlan?.model ?? reviewPlan?.model ?? {
2303
- source: 'unknown',
2304
- display: 'local default (CLI-managed)',
2305
- },
2306
- targets,
2307
- toolCount: introspection.tools.length,
2308
- grouping,
2309
- requestedHookMode,
2310
- hookMode: scaffoldPlan.generatedHookMode,
2311
- hookEvents: scaffoldPlan.generatedHookEvents,
2312
- quality,
2313
- review: passDecisions.review.enabled,
2314
- verify,
2315
- steps: totalSteps,
2316
- init: {
2317
- createdFiles: initCreatedFiles,
2318
- updatedFiles: initUpdatedFiles,
2319
- files: scaffoldPlan.generatedFiles,
2320
- },
2321
- agent: {
2322
- taxonomy: {
2323
- enabled: passDecisions.taxonomy.enabled,
2324
- reason: passDecisions.taxonomy.reason,
2325
- command: taxonomyPlan?.command,
2326
- commandDisplay: taxonomyPlan?.commandDisplay,
2327
- createdFiles: taxonomyPlan?.createdFiles ?? [],
2328
- updatedFiles: taxonomyPlan?.updatedFiles ?? [],
2329
- },
2330
- instructions: {
2331
- enabled: passDecisions.instructions.enabled,
2332
- reason: passDecisions.instructions.reason,
2333
- command: instructionsPlan?.command,
2334
- commandDisplay: instructionsPlan?.commandDisplay,
2335
- createdFiles: instructionsPlan?.createdFiles ?? [],
2336
- updatedFiles: instructionsPlan?.updatedFiles ?? [],
2337
- },
2338
- review: {
2339
- enabled: passDecisions.review.enabled,
2340
- reason: passDecisions.review.reason,
2341
- command: reviewPlan?.command,
2342
- commandDisplay: reviewPlan?.commandDisplay,
2343
- createdFiles: reviewPlan?.createdFiles ?? [],
2344
- updatedFiles: reviewPlan?.updatedFiles ?? [],
2345
- },
2346
- },
2347
- dryRun: true,
2348
- runnerLogsStreamed: verboseRunner,
2349
- }
2350
-
2351
- if (runtime.jsonOutput) {
2352
- printJson(summary)
2353
- } else if (!runtime.quiet) {
2354
- console.log(`Planned autopilot for ${pluginName}`)
2355
- console.log(` Mode: ${mode}`)
2356
- console.log(` Import: ${introspection.tools.length} tools -> ${targets.join(', ')}`)
2357
- console.log(` Runner: ${runner}`)
2358
- console.log(` Model: ${summary.model.display}`)
2359
- console.log(` Workload: ${summarizeAutopilotWorkload({
2360
- taxonomy: passDecisions.taxonomy,
2361
- instructions: passDecisions.instructions,
2362
- review: passDecisions.review,
2363
- verify,
2364
- })}`)
2365
- console.log(` Quality: ${quality.warnings} warning(s), ${quality.infos} info message(s)`)
2366
- console.log(` Scaffold create/update: ${[...initCreatedFiles, ...initUpdatedFiles].join(', ') || 'none'}`)
2367
- console.log(` ${formatAutopilotPassLine('Taxonomy', passDecisions.taxonomy)}`)
2368
- if (taxonomyPlan?.commandDisplay) {
2369
- console.log(` ${taxonomyPlan.commandDisplay}`)
2370
- }
2371
- console.log(` ${formatAutopilotPassLine('Instructions', passDecisions.instructions)}`)
2372
- if (instructionsPlan?.commandDisplay) {
2373
- console.log(` ${instructionsPlan.commandDisplay}`)
2374
- }
2375
- console.log(` ${formatAutopilotPassLine('Review', passDecisions.review)}`)
2376
- if (reviewPlan?.commandDisplay) {
2377
- console.log(` ${reviewPlan.commandDisplay}`)
2378
- }
2379
- if (verify) {
2380
- console.log(' Verification: pluxx test')
2381
- } else {
2382
- console.log(' Verification: skipped (--no-verify)')
2383
- }
2384
- }
2385
- return
2386
- }
2387
-
2388
- const streamOutput = verboseRunner && !runtime.jsonOutput && !runtime.quiet
2389
- let stepNumber = 3
2390
- let taxonomyDurationMs: number | undefined
2391
- let instructionsDurationMs: number | undefined
2392
- let reviewDurationMs: number | undefined
2393
- let verificationDurationMs: number | undefined
2394
-
2395
- const taxonomyResult = taxonomyPlan
2396
- ? await (async () => {
2397
- logAutopilotRunnerWait(stepNumber, totalSteps, 'Running taxonomy pass', runner)
2398
- const step = stepNumber
2399
- const result = await runTimedSpinnerTask({
2400
- spinner: agentSpinner,
2401
- startLabel: `Autopilot ${step}/${totalSteps} · Starting taxonomy pass...`,
2402
- waitLabel: `Autopilot ${step}/${totalSteps} · Waiting for taxonomy result`,
2403
- successLabel: (passResult) => `Taxonomy pass ${passResult.runnerExitCode === 0 ? 'complete' : 'failed'} (exit ${passResult.runnerExitCode})`,
2404
- task: async () => {
2405
- const startedAt = Date.now()
2406
- const passResult = await runAgentPlan(workspaceRoot, taxonomyPlan, { streamOutput })
2407
- taxonomyDurationMs = Date.now() - startedAt
2408
- return passResult
2409
- },
2410
- })
2411
- stepNumber += 1
2412
- return result
2413
- })()
2414
- : undefined
2415
-
2416
- const instructionsResult = instructionsPlan
2417
- ? await (async () => {
2418
- logAutopilotRunnerWait(stepNumber, totalSteps, 'Running instructions pass', runner)
2419
- const step = stepNumber
2420
- const result = await runTimedSpinnerTask({
2421
- spinner: agentSpinner,
2422
- startLabel: `Autopilot ${step}/${totalSteps} · Starting instructions pass...`,
2423
- waitLabel: `Autopilot ${step}/${totalSteps} · Waiting for instructions result`,
2424
- successLabel: (passResult) => `Instructions pass ${passResult.runnerExitCode === 0 ? 'complete' : 'failed'} (exit ${passResult.runnerExitCode})`,
2425
- task: async () => {
2426
- const startedAt = Date.now()
2427
- const passResult = await runAgentPlan(workspaceRoot, instructionsPlan, { streamOutput })
2428
- instructionsDurationMs = Date.now() - startedAt
2429
- return passResult
2430
- },
2431
- })
2432
- stepNumber += 1
2433
- return result
2434
- })()
2435
- : undefined
2436
-
2437
- const reviewResult = reviewPlan
2438
- ? await (async () => {
2439
- logAutopilotRunnerWait(stepNumber, totalSteps, 'Running review pass', runner)
2440
- const step = stepNumber
2441
- const result = await runTimedSpinnerTask({
2442
- spinner: agentSpinner,
2443
- startLabel: `Autopilot ${step}/${totalSteps} · Starting review pass...`,
2444
- waitLabel: `Autopilot ${step}/${totalSteps} · Waiting for review result`,
2445
- successLabel: (passResult) => `Review pass ${passResult.runnerExitCode === 0 ? 'complete' : 'failed'} (exit ${passResult.runnerExitCode})`,
2446
- task: async () => {
2447
- const startedAt = Date.now()
2448
- const passResult = await runAgentPlan(workspaceRoot, reviewPlan, { streamOutput })
2449
- reviewDurationMs = Date.now() - startedAt
2450
- return passResult
2451
- },
2452
- })
2453
- stepNumber += 1
2454
- return result
2455
- })()
2456
- : undefined
2457
-
2458
- const verification = verify
2459
- ? await (async () => {
2460
- const step = stepNumber
2461
- const result = await runTimedSpinnerTask({
2462
- spinner: agentSpinner,
2463
- startLabel: `Autopilot ${step}/${totalSteps} · Starting verification...`,
2464
- waitLabel: `Autopilot ${step}/${totalSteps} · Verifying scaffold`,
2465
- successLabel: (verificationResult) => `Verification ${verificationResult.ok ? 'passed' : 'failed'}`,
2466
- task: async () => {
2467
- const startedAt = Date.now()
2468
- const verificationResult = await runTestSuite({ rootDir: workspaceRoot, targets })
2469
- verificationDurationMs = Date.now() - startedAt
2470
- return verificationResult
2471
- },
2472
- })
2473
- return result
2474
- })()
2475
- : undefined
2476
-
2477
- const ok = (taxonomyResult?.ok ?? true)
2478
- && (instructionsResult?.ok ?? true)
2479
- && (reviewResult?.ok ?? true)
2480
- && (verification?.ok ?? true)
2481
-
2482
- const failureStage: AutopilotSummary['failureStage'] = taxonomyResult && taxonomyResult.runnerExitCode !== 0
2483
- ? 'runner'
2484
- : instructionsResult && instructionsResult.runnerExitCode !== 0
2485
- ? 'runner'
2486
- : reviewResult && reviewResult.runnerExitCode !== 0
2487
- ? 'runner'
2488
- : verification && !verification.ok
2489
- ? 'verification'
2490
- : undefined
2491
- const failureMessage = failureStage === 'runner'
2492
- ? 'A headless runner command failed. Re-run with --verbose-runner to stream full runner output.'
2493
- : failureStage === 'verification'
2494
- ? 'Verification failed after scaffold/refinement. Run `pluxx test` for details.'
2495
- : undefined
2496
-
2497
- const summary: AutopilotSummary = {
2498
- ok,
2499
- pluginName,
2500
- displayName,
2501
- source: rawSource,
2502
- mode,
2503
- runner,
2504
- model: taxonomyPlan?.model ?? instructionsPlan?.model ?? reviewPlan?.model ?? {
2505
- source: 'unknown',
2506
- display: 'local default (CLI-managed)',
2507
- },
2508
- targets,
2509
- toolCount: introspection.tools.length,
2510
- grouping,
2511
- requestedHookMode,
2512
- hookMode: scaffoldPlan.generatedHookMode,
2513
- hookEvents: scaffoldPlan.generatedHookEvents,
2514
- quality,
2515
- review: passDecisions.review.enabled,
2516
- verify,
2517
- steps: totalSteps,
2518
- runnerLogsStreamed: verboseRunner,
2519
- init: {
2520
- createdFiles: initCreatedFiles,
2521
- updatedFiles: initUpdatedFiles,
2522
- files: scaffoldPlan.generatedFiles,
2523
- },
2524
- agent: {
2525
- taxonomy: {
2526
- enabled: passDecisions.taxonomy.enabled,
2527
- reason: passDecisions.taxonomy.reason,
2528
- command: taxonomyPlan?.command,
2529
- commandDisplay: taxonomyPlan?.commandDisplay,
2530
- createdFiles: taxonomyPlan?.createdFiles ?? [],
2531
- updatedFiles: taxonomyPlan?.updatedFiles ?? [],
2532
- runnerExitCode: taxonomyResult?.runnerExitCode,
2533
- durationMs: taxonomyDurationMs,
2534
- },
2535
- instructions: {
2536
- enabled: passDecisions.instructions.enabled,
2537
- reason: passDecisions.instructions.reason,
2538
- command: instructionsPlan?.command,
2539
- commandDisplay: instructionsPlan?.commandDisplay,
2540
- createdFiles: instructionsPlan?.createdFiles ?? [],
2541
- updatedFiles: instructionsPlan?.updatedFiles ?? [],
2542
- runnerExitCode: instructionsResult?.runnerExitCode,
2543
- durationMs: instructionsDurationMs,
2544
- },
2545
- review: {
2546
- enabled: passDecisions.review.enabled,
2547
- reason: passDecisions.review.reason,
2548
- command: reviewPlan?.command,
2549
- commandDisplay: reviewPlan?.commandDisplay,
2550
- createdFiles: reviewPlan?.createdFiles ?? [],
2551
- updatedFiles: reviewPlan?.updatedFiles ?? [],
2552
- runnerExitCode: reviewResult?.runnerExitCode,
2553
- durationMs: reviewDurationMs,
2554
- },
2555
- },
2556
- verification,
2557
- verificationDurationMs,
2558
- failureStage,
2559
- failureMessage,
2560
- }
2561
-
2562
- if (runtime.jsonOutput) {
2563
- printJson(summary)
2564
- } else if (!runtime.quiet) {
2565
- console.log(`Autopilot ${ok ? 'completed' : 'failed'} for ${pluginName}`)
2566
- console.log(` Mode: ${mode}`)
2567
- console.log(` Import: ${introspection.tools.length} tools -> ${targets.join(', ')}`)
2568
- console.log(` Runner: ${runner}`)
2569
- console.log(` Model: ${summary.model.display}`)
2570
- console.log(` Workload: ${summarizeAutopilotWorkload({
2571
- taxonomy: passDecisions.taxonomy,
2572
- instructions: passDecisions.instructions,
2573
- review: passDecisions.review,
2574
- verify,
2575
- })}`)
2576
- console.log(` Quality: ${quality.warnings} warning(s), ${quality.infos} info message(s)`)
2577
- if (!verboseRunner) {
2578
- console.log(' Runner logs: suppressed (use --verbose-runner to stream)')
2579
- }
2580
- console.log(` ${formatAutopilotPassLine('Taxonomy', passDecisions.taxonomy)}`)
2581
- if (taxonomyResult && formatDuration(taxonomyDurationMs)) {
2582
- console.log(` Duration: ${formatDuration(taxonomyDurationMs)}`)
2583
- }
2584
- console.log(` ${formatAutopilotPassLine('Instructions', passDecisions.instructions)}`)
2585
- if (instructionsResult && formatDuration(instructionsDurationMs)) {
2586
- console.log(` Duration: ${formatDuration(instructionsDurationMs)}`)
2587
- }
2588
- console.log(` ${formatAutopilotPassLine('Review', passDecisions.review)}`)
2589
- if (reviewResult && formatDuration(reviewDurationMs)) {
2590
- console.log(` Duration: ${formatDuration(reviewDurationMs)}`)
2591
- }
2592
- if (verification) {
2593
- console.log(` Verification: ${verification.ok ? 'passed' : 'failed'}${formatDuration(verificationDurationMs) ? ` (${formatDuration(verificationDurationMs)})` : ''}`)
2594
- } else {
2595
- console.log(' Verification: skipped (--no-verify)')
2596
- }
2597
- if (failureStage && failureMessage) {
2598
- console.log(` Failure stage: ${failureStage}`)
2599
- console.log(` Failure detail: ${failureMessage}`)
2600
- }
2601
- console.log(' Next steps:')
2602
- console.log(' 1. Review INSTRUCTIONS.md and skills/')
2603
- console.log(` 2. Run: pluxx build${mode === 'quick' && !passDecisions.taxonomy.enabled && !passDecisions.instructions.enabled && !passDecisions.review.enabled ? ' (agent refinement was skipped; only do this if the deterministic scaffold already looks good)' : ''}`)
2604
- console.log(` 3. Run: pluxx install${scaffoldPlan.generatedHookMode === 'safe' ? ' --trust' : ''} --target ${targets[0]}`)
2605
- }
2606
-
2607
- if (!ok) {
2608
- process.exit(1)
2609
- }
2610
- } finally {
2611
- if (tempDir) {
2612
- await rm(tempDir, { recursive: true, force: true })
2613
- }
2614
- }
2615
- }
2616
-
2617
- async function runTestCommand() {
2618
- const targets = parseTargetFlagValues(args)
2619
- const result = await runTestSuite({
2620
- rootDir: process.cwd(),
2621
- targets,
2622
- })
2623
- const config = result.config.ok ? await loadConfig() : null
2624
- const platforms = targets ?? config?.targets ?? []
2625
- const install = result.ok && config
2626
- ? await maybeInstallBuiltOutputs(config, platforms)
2627
- : undefined
2628
-
2629
- if (runtime.jsonOutput) {
2630
- printJson({
2631
- ...result,
2632
- install,
2633
- })
2634
- return
2635
- }
2636
-
2637
- if (!runtime.quiet) {
2638
- printTestResult(result)
2639
- if (install) {
2640
- console.log('Installed for local testing:')
2641
- for (const target of install.installTargets) {
2642
- console.log(` ${target.platform} -> ${target.pluginDir}`)
2643
- }
2644
- for (const note of install.notes) {
2645
- console.log(note)
2646
- }
2647
- }
2648
- }
2649
-
2650
- if (!result.ok) {
2651
- process.exit(1)
2652
- }
2653
- }
2654
-
2655
- async function runEvalCommand() {
2656
- const report = await runEvalSuite({
2657
- rootDir: process.cwd(),
2658
- })
2659
-
2660
- if (runtime.jsonOutput) {
2661
- printJson(report)
2662
- return
2663
- }
2664
-
2665
- if (!runtime.quiet) {
2666
- printEvalReport(report)
2667
- }
2668
-
2669
- if (!report.ok) {
2670
- process.exit(1)
2671
- }
2672
- }
2673
-
2674
- async function runInstall() {
2675
- const trust = args.includes('--trust')
2676
- const targets = parseTargetFlagValues(args)
2677
-
2678
- const config = await loadConfig()
2679
- const distDir = `${process.cwd()}/${config.outDir}`
2680
- const platforms = targets ?? config.targets
2681
- const plannedUserConfig = planInstallUserConfig(config, platforms)
2682
-
2683
- if (runtime.dryRun) {
2684
- const plan = planInstallPlugin(distDir, config.name, platforms)
2685
- const hookCommands = listHookCommands(config.hooks)
2686
- const summary = {
2687
- dryRun: true,
2688
- pluginName: config.name,
2689
- platforms,
2690
- notes: getInstallFollowupNotes(platforms),
2691
- trustRequired: hookCommands.length > 0,
2692
- userConfig: plannedUserConfig.map((entry) => ({
2693
- key: entry.field.key,
2694
- title: entry.field.title,
2695
- envVar: entry.envVar,
2696
- required: entry.field.required ?? true,
2697
- source: entry.source,
2698
- })),
2699
- installTargets: plan.map((target) => ({
2700
- platform: target.platform,
2701
- sourceDir: target.sourceDir,
2702
- pluginDir: target.description,
2703
- built: target.built,
2704
- existing: target.existing,
2705
- })),
2706
- }
2707
- if (runtime.jsonOutput) {
2708
- printJson(summary)
2709
- } else if (!runtime.quiet) {
2710
- console.log(`Dry run: would install ${config.name} for ${platforms.join(', ')}`)
2711
- plan.forEach((target) => {
2712
- console.log(` ${target.platform} -> ${target.description}${target.built ? '' : ' (not built)'}`)
2713
- })
2714
- if (plannedUserConfig.length > 0) {
2715
- console.log(' userConfig:')
2716
- plannedUserConfig.forEach((entry) => {
2717
- const envHint = entry.envVar ? ` [env: ${entry.envVar}]` : ''
2718
- console.log(` - ${entry.field.key}${envHint} (${entry.source})`)
2719
- })
2720
- }
2721
- if (listHookCommands(config.hooks).length > 0) {
2722
- console.log(' trust reminder: this plugin defines local hook commands; install requires review or --trust')
2723
- }
2724
- for (const note of getInstallFollowupNotes(platforms)) {
2725
- console.log(` note: ${note}`)
2726
- }
2727
- }
2728
- return
2729
- }
2730
-
2731
- if (!runtime.jsonOutput && !runtime.quiet) {
2732
- console.log(`Installing ${config.name} plugin...`)
2733
- }
2734
- await ensureHookTrust({
2735
- pluginName: config.name,
2736
- hooks: config.hooks,
2737
- trust,
2738
- isTTY: runtime.isInteractive,
2739
- })
2740
- const resolvedUserConfig = await resolveInstallUserConfig(config, platforms, {
2741
- isTTY: runtime.isInteractive,
2742
- })
2743
- await installPlugin(distDir, config.name, platforms, {
2744
- config,
2745
- quiet: runtime.quiet,
2746
- resolvedUserConfig,
2747
- })
2748
- }
2749
-
2750
- async function runPublishCommand() {
2751
- const config = await loadConfig()
2752
- const requestedChannels: Array<'npm' | 'github-release'> = []
2753
- if (args.includes('--npm')) requestedChannels.push('npm')
2754
- if (args.includes('--github-release')) requestedChannels.push('github-release')
2755
-
2756
- const plan = planPublish(config, {
2757
- rootDir: process.cwd(),
2758
- requestedChannels,
2759
- version: readOption(args, '--version'),
2760
- tag: readOption(args, '--tag'),
2761
- dryRun: runtime.dryRun,
2762
- })
2763
-
2764
- if (runtime.dryRun) {
2765
- if (runtime.jsonOutput) {
2766
- printJson(plan)
2767
- } else if (!runtime.quiet) {
2768
- for (const line of formatPublishPlan(plan)) {
2769
- console.log(line)
2770
- }
2771
- }
2772
-
2773
- if (plan.checks.some((check) => !check.ok)) {
2774
- process.exit(1)
2775
- }
2776
- return
2777
- }
2778
-
2779
- const result = runPublish(config, {
2780
- rootDir: process.cwd(),
2781
- requestedChannels,
2782
- version: readOption(args, '--version'),
2783
- tag: readOption(args, '--tag'),
2784
- dryRun: false,
2785
- })
2786
-
2787
- if (runtime.jsonOutput) {
2788
- printJson(result)
2789
- } else if (!runtime.quiet) {
2790
- for (const line of formatPublishPlan(result)) {
2791
- console.log(line)
2792
- }
2793
-
2794
- if (result.execution?.npm) {
2795
- console.log(`npm: ${result.execution.npm.ok ? 'ok' : 'fail'}${result.execution.npm.detail ? ` — ${result.execution.npm.detail}` : ''}`)
2796
- }
2797
- if (result.execution?.githubRelease) {
2798
- console.log(`github-release: ${result.execution.githubRelease.ok ? 'ok' : 'fail'}${result.execution.githubRelease.detail ? ` — ${result.execution.githubRelease.detail}` : ''}`)
2799
- }
2800
- }
2801
-
2802
- if (!result.ok) {
2803
- process.exit(1)
2804
- }
2805
- }
2806
-
2807
- async function runUninstall() {
2808
- const targets = parseTargetFlagValues(args)
2809
-
2810
- const config = await loadConfig()
2811
-
2812
- if (!runtime.jsonOutput && !runtime.quiet) {
2813
- console.log(`Uninstalling ${config.name} plugin...`)
2814
- }
2815
- await uninstallPlugin(config.name, targets, { quiet: runtime.quiet })
2816
- }
2817
-
2818
- async function runMigrate() {
2819
- const inputPath = args[1]
2820
- if (!inputPath) {
2821
- console.error('Usage: pluxx migrate <path>')
2822
- console.error('')
2823
- console.error(' Import an existing single-platform plugin into a pluxx.config.ts.')
2824
- console.error(' Pass the path to a plugin directory containing .claude-plugin/,')
2825
- console.error(' .cursor-plugin/, .codex-plugin/, or a package.json with @opencode-ai/plugin.')
2826
- process.exit(1)
2827
- }
2828
- await migrate(inputPath)
2829
- }
2830
-
2831
- async function runMcp() {
2832
- const subcommand = args[1]
2833
- if (subcommand !== 'proxy') {
2834
- console.error('Usage: pluxx mcp proxy --from-mcp <source> [--record <tape.json>]')
2835
- console.error(' pluxx mcp proxy --replay <tape.json>')
2836
- process.exit(1)
2837
- }
2838
-
2839
- try {
2840
- await runMcpProxy(args.slice(2))
2841
- } catch (error) {
2842
- if (error instanceof Error && error.message === 'Invalid MCP proxy arguments.') {
2843
- process.exit(1)
2844
- }
2845
- throw error
2846
- }
2847
- }
2848
-
2849
- function printHelp() {
2850
- console.log(`
2851
- pluxx — Cross-platform AI agent plugin SDK
2852
-
2853
- Usage:
2854
- pluxx build [--target <platforms...>] [--install] Generate platform-specific plugin files
2855
- pluxx dev [--target <platforms...>] Watch for changes and auto-rebuild
2856
- pluxx validate Validate your config
2857
- pluxx lint Lint skills and cross-platform metadata
2858
- pluxx doctor [path] [--consumer] Check source-project or installed-bundle health
2859
- pluxx agent prepare Generate agent context + boundary files for host agents
2860
- pluxx agent prompt <kind> Generate a prompt pack (taxonomy, instructions, review)
2861
- pluxx agent run <kind> --runner <id> Execute a prompt pack via Claude, Cursor, Codex, or OpenCode headlessly
2862
- pluxx mcp proxy ... Run a local MCP proxy with optional record/replay tapes
2863
- pluxx autopilot --from-mcp ... Run import + agent refinement + verification in one command
2864
- pluxx init [name] [--from-mcp <source>] Create a new pluxx.config.ts
2865
- pluxx sync [--from-mcp <source>] Refresh MCP-derived scaffold files
2866
- pluxx migrate <path> Import an existing plugin into pluxx
2867
- pluxx test [--target <platforms...>] [--install] Run config, lint, eval, build, and smoke checks
2868
- pluxx eval Evaluate scaffold and prompt-pack quality
2869
- pluxx install [--target <platforms>] [--trust] Install built plugins for local testing
2870
- pluxx publish [--npm] [--github-release] [--dry-run] [--json] [--tag latest] [--version x.y.z]
2871
- pluxx uninstall [--target <platforms>] Remove symlinked plugins
2872
- pluxx help Show this help
2873
-
2874
- Common flags:
2875
- --json Print machine-readable output
2876
- --quiet Suppress non-error chatter
2877
- --verbose-runner Stream runner stdout/stderr for agent run/autopilot
2878
- --dry-run Show planned work without writing files or installing anything
2879
- --mode quick|standard|thorough Control how much agent refinement autopilot performs
2880
-
2881
- Targets:
2882
- claude-code, cursor, codex, opencode, github-copilot, openhands,
2883
- warp, gemini-cli, roo-code, cline, amp
2884
-
2885
- Examples:
2886
- pluxx build Build for all configured targets
2887
- pluxx build --install Build and install all configured targets locally
2888
- pluxx build --target claude-code cursor Build for specific platforms
2889
- pluxx init my-plugin Scaffold a new plugin config
2890
- pluxx init --from-mcp https://example.com/mcp Scaffold from a remote MCP server
2891
- pluxx init --from-mcp "npx -y @acme/mcp" Scaffold from a local MCP command
2892
- pluxx init --from-mcp https://example.com/mcp --yes --name acme --display-name "Acme" --author "Acme" --targets claude-code,codex --grouping workflow --hooks safe --json
2893
- pluxx init --from-mcp https://example.com/mcp --yes --auth-env API_KEY --auth-type header --auth-header X-API-Key --auth-template "\${value}"
2894
- pluxx init --from-mcp https://example.com/mcp --yes --auth-type platform --runtime-auth platform
2895
- pluxx init --from-mcp https://mcp.linear.app/mcp --yes --oauth-wrapper --runtime-auth platform
2896
- pluxx init --from-mcp https://example.com/sse --transport sse Scaffold from an SSE-transport MCP server
2897
- pluxx init --from-mcp https://example.com/mcp --yes --dry-run Preview scaffold files without writing
2898
- pluxx sync Refresh a scaffold using .pluxx/mcp.json metadata
2899
- pluxx sync --from-mcp https://example.com/mcp Refresh using an explicit MCP source override
2900
- pluxx agent prepare --dry-run Preview agent context files without writing
2901
- pluxx agent prepare --website https://example.com --docs https://docs.example.com
2902
- pluxx agent prompt taxonomy Generate the taxonomy prompt pack
2903
- pluxx agent run taxonomy --runner claude
2904
- pluxx agent run taxonomy --runner cursor
2905
- pluxx agent run taxonomy --runner codex
2906
- pluxx agent run taxonomy --runner codex --verbose-runner
2907
- pluxx agent run review --runner opencode --attach http://localhost:4096 --no-verify
2908
- pluxx mcp proxy --from-mcp "bun ./server.js" --record .pluxx/tapes/dev.json
2909
- pluxx mcp proxy --replay .pluxx/tapes/dev.json
2910
- --attach is only supported for the opencode runner
2911
- pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode quick --yes
2912
- pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode standard --yes --name acme --display-name "Acme"
2913
- pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode thorough --yes --verbose-runner
2914
- pluxx autopilot --from-mcp https://mcp.linear.app/mcp --runner codex --yes --oauth-wrapper
2915
- pluxx autopilot --from-mcp "npx -y @acme/mcp" --runner claude --targets claude-code,codex --website https://example.com --docs https://docs.example.com
2916
- pluxx doctor --json Inspect source-project health as JSON
2917
- pluxx doctor --consumer ./dist/cursor Inspect a built or installed platform bundle
2918
- pluxx eval --json Inspect scaffold/prompt-pack quality as JSON
2919
- pluxx test --target claude-code codex Verify selected target outputs
2920
- pluxx test --install Verify and install all configured targets locally
2921
- pluxx install Install to all configured targets
2922
- pluxx install --target claude-code Install to Claude Code only
2923
- pluxx install --dry-run Preview local install paths and trust implications
2924
- pluxx install --trust Install without hook trust confirmation
2925
- `)
2926
- }
2927
-
2928
- if (import.meta.main) {
2929
- main().catch(err => {
2930
- console.error(err)
2931
- process.exit(1)
2932
- })
2933
- }