@orchid-labs/pluxx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +574 -0
- package/bin/pluxx.js +37 -0
- package/dist/cli/agent.d.ts +90 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts +19 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/init-from-mcp.d.ts +145 -0
- package/dist/cli/init-from-mcp.d.ts.map +1 -0
- package/dist/cli/install.d.ts +56 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/lint.d.ts +18 -0
- package/dist/cli/lint.d.ts.map +1 -0
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/prompt.d.ts +20 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/publish.d.ts +70 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/runtime.d.ts +20 -0
- package/dist/cli/runtime.d.ts.map +1 -0
- package/dist/cli/sync-from-mcp.d.ts +32 -0
- package/dist/cli/sync-from-mcp.d.ts.map +1 -0
- package/dist/cli/test.d.ts +33 -0
- package/dist/cli/test.d.ts.map +1 -0
- package/dist/compatibility/matrix.d.ts +14 -0
- package/dist/compatibility/matrix.d.ts.map +1 -0
- package/dist/config/define.d.ts +18 -0
- package/dist/config/define.d.ts.map +1 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/generators/amp/index.d.ts +13 -0
- package/dist/generators/amp/index.d.ts.map +1 -0
- package/dist/generators/base.d.ts +49 -0
- package/dist/generators/base.d.ts.map +1 -0
- package/dist/generators/claude-code/index.d.ts +7 -0
- package/dist/generators/claude-code/index.d.ts.map +1 -0
- package/dist/generators/cline/index.d.ts +14 -0
- package/dist/generators/cline/index.d.ts.map +1 -0
- package/dist/generators/codex/index.d.ts +9 -0
- package/dist/generators/codex/index.d.ts.map +1 -0
- package/dist/generators/cursor/index.d.ts +11 -0
- package/dist/generators/cursor/index.d.ts.map +1 -0
- package/dist/generators/gemini-cli/index.d.ts +13 -0
- package/dist/generators/gemini-cli/index.d.ts.map +1 -0
- package/dist/generators/github-copilot/index.d.ts +11 -0
- package/dist/generators/github-copilot/index.d.ts.map +1 -0
- package/dist/generators/hooks-warning.d.ts +3 -0
- package/dist/generators/hooks-warning.d.ts.map +1 -0
- package/dist/generators/index.d.ts +11 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/opencode/index.d.ts +15 -0
- package/dist/generators/opencode/index.d.ts.map +1 -0
- package/dist/generators/openhands/index.d.ts +11 -0
- package/dist/generators/openhands/index.d.ts.map +1 -0
- package/dist/generators/roo-code/index.d.ts +14 -0
- package/dist/generators/roo-code/index.d.ts.map +1 -0
- package/dist/generators/shared/claude-family.d.ts +18 -0
- package/dist/generators/shared/claude-family.d.ts.map +1 -0
- package/dist/generators/warp/index.d.ts +13 -0
- package/dist/generators/warp/index.d.ts.map +1 -0
- package/dist/hook-events.d.ts +4 -0
- package/dist/hook-events.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5302 -0
- package/dist/mcp/introspect.d.ts +34 -0
- package/dist/mcp/introspect.d.ts.map +1 -0
- package/dist/permissions.d.ts +18 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/schema.d.ts +9457 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/user-config.d.ts +19 -0
- package/dist/user-config.d.ts.map +1 -0
- package/dist/validation/platform-rules.d.ts +64 -0
- package/dist/validation/platform-rules.d.ts.map +1 -0
- package/package.json +76 -0
- package/src/cli/agent.ts +1030 -0
- package/src/cli/dev.ts +112 -0
- package/src/cli/doctor.ts +588 -0
- package/src/cli/index.ts +2414 -0
- package/src/cli/init-from-mcp.ts +1611 -0
- package/src/cli/install.ts +698 -0
- package/src/cli/lint.ts +1219 -0
- package/src/cli/migrate.ts +614 -0
- package/src/cli/prompt.ts +82 -0
- package/src/cli/publish.ts +401 -0
- package/src/cli/runtime.ts +86 -0
- package/src/cli/sync-from-mcp.ts +563 -0
- package/src/cli/test.ts +134 -0
- package/src/compatibility/matrix.ts +149 -0
- package/src/config/define.ts +20 -0
- package/src/config/load.ts +74 -0
- package/src/generators/amp/index.ts +63 -0
- package/src/generators/base.ts +188 -0
- package/src/generators/claude-code/index.ts +29 -0
- package/src/generators/cline/index.ts +35 -0
- package/src/generators/codex/index.ts +120 -0
- package/src/generators/cursor/index.ts +158 -0
- package/src/generators/gemini-cli/index.ts +83 -0
- package/src/generators/github-copilot/index.ts +32 -0
- package/src/generators/hooks-warning.ts +51 -0
- package/src/generators/index.ts +71 -0
- package/src/generators/opencode/index.ts +526 -0
- package/src/generators/openhands/index.ts +32 -0
- package/src/generators/roo-code/index.ts +35 -0
- package/src/generators/shared/claude-family.ts +215 -0
- package/src/generators/warp/index.ts +32 -0
- package/src/hook-events.ts +33 -0
- package/src/index.ts +23 -0
- package/src/mcp/introspect.ts +834 -0
- package/src/permissions.ts +258 -0
- package/src/schema.ts +312 -0
- package/src/user-config.ts +177 -0
- package/src/validation/platform-rules.ts +565 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,2414 @@
|
|
|
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
|
+
}
|