@orchid-labs/pluxx 0.1.0 → 0.1.3

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