@orchid-labs/pluxx 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +25 -8
  2. package/bin/pluxx.js +19 -28
  3. package/dist/agents.d.ts +16 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/cli/agent.d.ts +62 -0
  6. package/dist/cli/agent.d.ts.map +1 -1
  7. package/dist/cli/doctor.d.ts +2 -0
  8. package/dist/cli/doctor.d.ts.map +1 -1
  9. package/dist/cli/entry.d.ts +2 -0
  10. package/dist/cli/entry.d.ts.map +1 -0
  11. package/dist/cli/index.d.ts +7 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +21810 -0
  14. package/dist/cli/init-from-mcp.d.ts +17 -1
  15. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  16. package/dist/cli/install.d.ts +1 -0
  17. package/dist/cli/install.d.ts.map +1 -1
  18. package/dist/cli/lint.d.ts +3 -1
  19. package/dist/cli/lint.d.ts.map +1 -1
  20. package/dist/cli/mcp-proxy.d.ts.map +1 -1
  21. package/dist/cli/migrate.d.ts.map +1 -1
  22. package/dist/cli/primitive-summary.d.ts +14 -0
  23. package/dist/cli/primitive-summary.d.ts.map +1 -0
  24. package/dist/cli/prompt.d.ts +1 -1
  25. package/dist/cli/publish.d.ts +6 -1
  26. package/dist/cli/publish.d.ts.map +1 -1
  27. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  28. package/dist/cli/verify-install.d.ts +25 -0
  29. package/dist/cli/verify-install.d.ts.map +1 -0
  30. package/dist/commands.d.ts +10 -0
  31. package/dist/commands.d.ts.map +1 -0
  32. package/dist/compiler-intent.d.ts +165 -0
  33. package/dist/compiler-intent.d.ts.map +1 -0
  34. package/dist/config/load.d.ts.map +1 -1
  35. package/dist/delegation.d.ts +11 -0
  36. package/dist/delegation.d.ts.map +1 -0
  37. package/dist/generators/amp/index.d.ts.map +1 -1
  38. package/dist/generators/base.d.ts +5 -0
  39. package/dist/generators/base.d.ts.map +1 -1
  40. package/dist/generators/claude-code/index.d.ts.map +1 -1
  41. package/dist/generators/cline/index.d.ts.map +1 -1
  42. package/dist/generators/codex/index.d.ts +4 -0
  43. package/dist/generators/codex/index.d.ts.map +1 -1
  44. package/dist/generators/cursor/index.d.ts +1 -0
  45. package/dist/generators/cursor/index.d.ts.map +1 -1
  46. package/dist/generators/gemini-cli/index.d.ts.map +1 -1
  47. package/dist/generators/github-copilot/index.d.ts.map +1 -1
  48. package/dist/generators/opencode/index.d.ts +1 -0
  49. package/dist/generators/opencode/index.d.ts.map +1 -1
  50. package/dist/generators/openhands/index.d.ts.map +1 -1
  51. package/dist/generators/roo-code/index.d.ts.map +1 -1
  52. package/dist/generators/shared/claude-family.d.ts.map +1 -1
  53. package/dist/generators/warp/index.d.ts.map +1 -1
  54. package/dist/index.d.ts +4 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +5371 -553
  57. package/dist/schema.d.ts +91 -42
  58. package/dist/schema.d.ts.map +1 -1
  59. package/dist/text-files.d.ts +5 -0
  60. package/dist/text-files.d.ts.map +1 -0
  61. package/dist/validation/platform-rules.d.ts +15 -1
  62. package/dist/validation/platform-rules.d.ts.map +1 -1
  63. package/package.json +15 -13
  64. package/src/cli/agent.ts +0 -1455
  65. package/src/cli/dev.ts +0 -112
  66. package/src/cli/doctor.ts +0 -987
  67. package/src/cli/eval.ts +0 -470
  68. package/src/cli/index.ts +0 -2933
  69. package/src/cli/init-from-mcp.ts +0 -2115
  70. package/src/cli/install.ts +0 -860
  71. package/src/cli/lint.ts +0 -1249
  72. package/src/cli/mcp-proxy.ts +0 -322
  73. package/src/cli/migrate.ts +0 -867
  74. package/src/cli/prompt.ts +0 -82
  75. package/src/cli/publish.ts +0 -401
  76. package/src/cli/runtime.ts +0 -86
  77. package/src/cli/sync-from-mcp.ts +0 -586
  78. package/src/cli/test.ts +0 -142
  79. package/src/compatibility/matrix.ts +0 -149
  80. package/src/config/define.ts +0 -20
  81. package/src/config/load.ts +0 -74
  82. package/src/generators/amp/index.ts +0 -63
  83. package/src/generators/base.ts +0 -188
  84. package/src/generators/claude-code/index.ts +0 -172
  85. package/src/generators/cline/index.ts +0 -35
  86. package/src/generators/codex/index.ts +0 -143
  87. package/src/generators/cursor/index.ts +0 -158
  88. package/src/generators/gemini-cli/index.ts +0 -83
  89. package/src/generators/github-copilot/index.ts +0 -32
  90. package/src/generators/hooks-warning.ts +0 -51
  91. package/src/generators/index.ts +0 -71
  92. package/src/generators/opencode/index.ts +0 -526
  93. package/src/generators/openhands/index.ts +0 -32
  94. package/src/generators/roo-code/index.ts +0 -35
  95. package/src/generators/shared/claude-family.ts +0 -215
  96. package/src/generators/warp/index.ts +0 -32
  97. package/src/hook-events.ts +0 -33
  98. package/src/index.ts +0 -34
  99. package/src/mcp/introspect.ts +0 -1107
  100. package/src/permissions.ts +0 -260
  101. package/src/schema.ts +0 -312
  102. package/src/user-config.ts +0 -177
  103. package/src/validation/platform-rules.ts +0 -686
@@ -1,867 +0,0 @@
1
- import { resolve, basename } from 'path'
2
- import { existsSync, readdirSync, mkdirSync, cpSync, readFileSync } from 'fs'
3
- import {
4
- MCP_SCAFFOLD_METADATA_PATH,
5
- MCP_TAXONOMY_PATH,
6
- type McpScaffoldMetadata,
7
- type PersistedSkill,
8
- } from './init-from-mcp'
9
- import type { McpServer } from '../schema'
10
-
11
- type DetectedPlatform = 'claude-code' | 'cursor' | 'codex' | 'opencode'
12
-
13
- interface DetectionResult {
14
- platform: DetectedPlatform
15
- manifestPath: string
16
- }
17
-
18
- interface ParsedManifest {
19
- name?: string
20
- version?: string
21
- description?: string
22
- author?: { name: string; url?: string; email?: string }
23
- repository?: string
24
- license?: string
25
- keywords?: string[]
26
- }
27
-
28
- interface ParsedMcp {
29
- [serverName: string]: {
30
- url?: string
31
- transport?: 'http' | 'sse' | 'stdio'
32
- command?: string
33
- args?: string[]
34
- env?: Record<string, string>
35
- auth?: {
36
- type: 'bearer' | 'header' | 'none'
37
- envVar?: string
38
- headerName?: string
39
- headerTemplate?: string
40
- }
41
- }
42
- }
43
-
44
- interface ParsedHooks {
45
- [event: string]: Array<{
46
- command: string
47
- timeout?: number
48
- matcher?: string
49
- }>
50
- }
51
-
52
- interface MigrateResult {
53
- platform: DetectedPlatform
54
- manifest: ParsedManifest
55
- mcp: ParsedMcp
56
- hooks: ParsedHooks
57
- instructions?: string
58
- directories: {
59
- skills: boolean
60
- commands: boolean
61
- agents: boolean
62
- scripts: boolean
63
- assets: boolean
64
- }
65
- persistedSkills: PersistedSkill[]
66
- }
67
-
68
- // ── Platform Detection ──────────────────────────────────────────
69
-
70
- function detectPlatform(pluginDir: string): DetectionResult | null {
71
- const checks: Array<{ dir: string; platform: DetectedPlatform }> = [
72
- { dir: '.claude-plugin', platform: 'claude-code' },
73
- { dir: '.cursor-plugin', platform: 'cursor' },
74
- { dir: '.codex-plugin', platform: 'codex' },
75
- ]
76
-
77
- for (const check of checks) {
78
- const manifestPath = resolve(pluginDir, check.dir, 'plugin.json')
79
- if (existsSync(manifestPath)) {
80
- return { platform: check.platform, manifestPath }
81
- }
82
- }
83
-
84
- // Check for OpenCode (package.json with @opencode-ai/plugin)
85
- const pkgPath = resolve(pluginDir, 'package.json')
86
- if (existsSync(pkgPath)) {
87
- try {
88
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
89
- const deps = {
90
- ...pkg.dependencies,
91
- ...pkg.devDependencies,
92
- ...pkg.peerDependencies,
93
- }
94
- if (deps && '@opencode-ai/plugin' in deps) {
95
- return { platform: 'opencode', manifestPath: pkgPath }
96
- }
97
- } catch {
98
- // not valid JSON, skip
99
- }
100
- }
101
-
102
- return null
103
- }
104
-
105
- // ── Manifest Parsing ────────────────────────────────────────────
106
-
107
- function parseManifest(detection: DetectionResult): ParsedManifest {
108
- const raw = JSON.parse(readFileSync(detection.manifestPath, 'utf-8'))
109
-
110
- const result: ParsedManifest = {}
111
-
112
- if (raw.name) result.name = raw.name
113
- if (raw.version) result.version = raw.version
114
- if (raw.description) result.description = raw.description
115
- if (raw.license) result.license = raw.license
116
- if (raw.keywords) result.keywords = raw.keywords
117
- if (raw.repository) {
118
- result.repository = typeof raw.repository === 'string'
119
- ? raw.repository
120
- : raw.repository?.url
121
- }
122
-
123
- if (raw.author) {
124
- if (typeof raw.author === 'string') {
125
- result.author = { name: raw.author }
126
- } else {
127
- result.author = {
128
- name: raw.author.name,
129
- ...(raw.author.url && { url: raw.author.url }),
130
- ...(raw.author.email && { email: raw.author.email }),
131
- }
132
- }
133
- }
134
-
135
- return result
136
- }
137
-
138
- // ── MCP Parsing ─────────────────────────────────────────────────
139
-
140
- function parseMcp(pluginDir: string, detection: DetectionResult): ParsedMcp {
141
- const mcpPaths = [
142
- resolve(pluginDir, '.mcp.json'),
143
- resolve(pluginDir, 'mcp.json'),
144
- ]
145
-
146
- // Also check if the manifest references an mcpServers file
147
- try {
148
- const manifest = JSON.parse(readFileSync(detection.manifestPath, 'utf-8'))
149
- if (manifest.mcpServers && typeof manifest.mcpServers === 'string') {
150
- mcpPaths.unshift(resolve(pluginDir, manifest.mcpServers))
151
- }
152
- } catch {
153
- // ignore
154
- }
155
-
156
- for (const mcpPath of mcpPaths) {
157
- if (!existsSync(mcpPath)) continue
158
- try {
159
- const raw = JSON.parse(readFileSync(mcpPath, 'utf-8'))
160
- const servers = raw.mcpServers ?? raw
161
- if (!servers || typeof servers !== 'object') continue
162
-
163
- const result: ParsedMcp = {}
164
-
165
- for (const [name, config] of Object.entries(servers)) {
166
- const cfg = config as Record<string, unknown>
167
- const entry: ParsedMcp[string] = {}
168
-
169
- if (cfg.url) entry.url = cfg.url as string
170
-
171
- // Detect transport
172
- if (cfg.type === 'stdio' || cfg.command) {
173
- entry.transport = 'stdio'
174
- if (cfg.command) entry.command = cfg.command as string
175
- if (cfg.args) entry.args = cfg.args as string[]
176
- if (cfg.env) entry.env = cfg.env as Record<string, string>
177
- } else if (cfg.type === 'sse') {
178
- entry.transport = 'sse'
179
- } else {
180
- entry.transport = 'http'
181
- }
182
-
183
- // Parse auth from headers
184
- if (cfg.headers && typeof cfg.headers === 'object') {
185
- const headers = cfg.headers as Record<string, string>
186
- const authHeader = headers['Authorization'] ?? headers['authorization']
187
- if (authHeader) {
188
- // Extract env var from patterns like "Bearer ${SOME_KEY}"
189
- const envMatch = authHeader.match(/\$\{(\w+)\}/)
190
- if (envMatch) {
191
- entry.auth = {
192
- type: 'bearer',
193
- envVar: envMatch[1],
194
- headerTemplate: authHeader.replace(/\$\{\w+\}/, '${value}'),
195
- }
196
- }
197
- }
198
- }
199
-
200
- // Codex-style bearer_token_env_var
201
- if (cfg.bearer_token_env_var) {
202
- entry.auth = {
203
- type: 'bearer',
204
- envVar: cfg.bearer_token_env_var as string,
205
- }
206
- }
207
-
208
- // Codex-style env_http_headers
209
- if (cfg.env_http_headers && typeof cfg.env_http_headers === 'object') {
210
- const envHeaders = Object.entries(cfg.env_http_headers as Record<string, string>)
211
- if (envHeaders.length > 0) {
212
- const [headerName, envVar] = envHeaders[0]
213
- entry.auth = {
214
- type: 'header',
215
- envVar,
216
- headerName,
217
- headerTemplate: '${value}',
218
- }
219
- }
220
- }
221
-
222
- result[name] = entry
223
- }
224
-
225
- return result
226
- } catch {
227
- continue
228
- }
229
- }
230
-
231
- return {}
232
- }
233
-
234
- // ── Hooks Parsing ───────────────────────────────────────────────
235
-
236
- // Maps platform-specific hook event names to pluxx schema names
237
- const HOOK_EVENT_MAP: Record<string, string> = {
238
- SessionStart: 'sessionStart',
239
- SessionEnd: 'sessionEnd',
240
- PreToolUse: 'preToolUse',
241
- PostToolUse: 'postToolUse',
242
- BeforeShellExecution: 'beforeShellExecution',
243
- AfterShellExecution: 'afterShellExecution',
244
- BeforeMCPExecution: 'beforeMCPExecution',
245
- AfterMCPExecution: 'afterMCPExecution',
246
- AfterFileEdit: 'afterFileEdit',
247
- BeforeReadFile: 'beforeReadFile',
248
- BeforeSubmitPrompt: 'beforeSubmitPrompt',
249
- Stop: 'stop',
250
- // Also handle already-normalized names
251
- sessionStart: 'sessionStart',
252
- sessionEnd: 'sessionEnd',
253
- preToolUse: 'preToolUse',
254
- postToolUse: 'postToolUse',
255
- }
256
-
257
- function parseHooks(pluginDir: string, detection: DetectionResult): ParsedHooks {
258
- const hooksPaths = [
259
- resolve(pluginDir, '.codex', 'hooks.json'),
260
- resolve(pluginDir, 'hooks.json'),
261
- resolve(pluginDir, 'hooks', 'hooks.json'),
262
- ]
263
-
264
- // Check if manifest references a hooks file
265
- try {
266
- const manifest = JSON.parse(readFileSync(detection.manifestPath, 'utf-8'))
267
- if (manifest.hooks && typeof manifest.hooks === 'string') {
268
- hooksPaths.unshift(resolve(pluginDir, manifest.hooks))
269
- }
270
- } catch {
271
- // ignore
272
- }
273
-
274
- for (const hooksPath of hooksPaths) {
275
- if (!existsSync(hooksPath)) continue
276
- try {
277
- const raw = JSON.parse(readFileSync(hooksPath, 'utf-8'))
278
- const hooksObj = raw.hooks ?? raw
279
- if (!hooksObj || typeof hooksObj !== 'object') continue
280
-
281
- const result: ParsedHooks = {}
282
-
283
- for (const [event, entries] of Object.entries(hooksObj)) {
284
- const normalizedEvent = HOOK_EVENT_MAP[event] ?? event
285
- const hookEntries: ParsedHooks[string] = []
286
-
287
- if (!Array.isArray(entries)) continue
288
-
289
- for (const entry of entries) {
290
- // Claude Code format: { hooks: [{ type: 'command', command: '...' }] }
291
- if (entry.hooks && Array.isArray(entry.hooks)) {
292
- for (const hook of entry.hooks) {
293
- if (hook.command) {
294
- hookEntries.push({
295
- command: hook.command,
296
- ...(hook.timeout && { timeout: hook.timeout }),
297
- })
298
- }
299
- }
300
- }
301
- // Direct format: { command: '...' }
302
- else if (entry.command) {
303
- hookEntries.push({
304
- command: entry.command,
305
- ...(entry.timeout && { timeout: entry.timeout }),
306
- ...(entry.matcher && { matcher: entry.matcher }),
307
- })
308
- }
309
- }
310
-
311
- if (hookEntries.length > 0) {
312
- result[normalizedEvent] = hookEntries
313
- }
314
- }
315
-
316
- return result
317
- } catch {
318
- continue
319
- }
320
- }
321
-
322
- return {}
323
- }
324
-
325
- // ── Instructions Detection ──────────────────────────────────────
326
-
327
- function findInstructions(pluginDir: string): string | undefined {
328
- const candidates = [
329
- 'CLAUDE.md',
330
- 'AGENTS.md',
331
- 'instructions.md',
332
- 'INSTRUCTIONS.md',
333
- 'README.md',
334
- ]
335
-
336
- for (const candidate of candidates) {
337
- const filePath = resolve(pluginDir, candidate)
338
- if (existsSync(filePath)) {
339
- return `./${candidate}`
340
- }
341
- }
342
-
343
- return undefined
344
- }
345
-
346
- // ── Directory Detection ─────────────────────────────────────────
347
-
348
- function detectDirectories(pluginDir: string) {
349
- return {
350
- skills: existsSync(resolve(pluginDir, 'skills')),
351
- commands: existsSync(resolve(pluginDir, 'commands')),
352
- agents: existsSync(resolve(pluginDir, 'agents')),
353
- scripts: existsSync(resolve(pluginDir, 'scripts')),
354
- assets: existsSync(resolve(pluginDir, 'assets')),
355
- }
356
- }
357
-
358
- function toKebabCase(value: string): string {
359
- return value
360
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
361
- .replace(/[^A-Za-z0-9]+/g, '-')
362
- .replace(/^-+|-+$/g, '')
363
- .toLowerCase()
364
- }
365
-
366
- function titleCaseFromDirName(value: string): string {
367
- return value
368
- .split(/[-_]+/)
369
- .filter(Boolean)
370
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
371
- .join(' ')
372
- }
373
-
374
- function firstHeading(content: string): string | undefined {
375
- for (const line of content.split(/\r?\n/)) {
376
- const trimmed = line.trim()
377
- const match = trimmed.match(/^#\s+(.+)$/)
378
- if (match) {
379
- return match[1].trim()
380
- }
381
- }
382
- return undefined
383
- }
384
-
385
- function extractFrontmatterField(content: string, key: 'name' | 'description'): string | undefined {
386
- const lines = content.split(/\r?\n/)
387
- if (lines[0]?.trim() !== '---') return undefined
388
-
389
- let endIndex = -1
390
- for (let i = 1; i < lines.length; i += 1) {
391
- if (lines[i].trim() === '---') {
392
- endIndex = i
393
- break
394
- }
395
- }
396
-
397
- if (endIndex === -1) return undefined
398
-
399
- for (const line of lines.slice(1, endIndex)) {
400
- const match = line.match(new RegExp(`^${key}:\\s*(.*)$`))
401
- if (!match) continue
402
- return match[1].trim().replace(/^['"]|['"]$/g, '')
403
- }
404
-
405
- return undefined
406
- }
407
-
408
- function readMigratedSkills(pluginDir: string, dirs: MigrateResult['directories']): PersistedSkill[] {
409
- const skills: PersistedSkill[] = []
410
-
411
- if (dirs.skills) {
412
- const skillsDir = resolve(pluginDir, 'skills')
413
- const entries = readdirSync(skillsDir, { withFileTypes: true })
414
-
415
- for (const entry of entries) {
416
- if (!entry.isDirectory()) continue
417
- const dirName = entry.name
418
- const skillPath = resolve(skillsDir, dirName, 'SKILL.md')
419
- let title = titleCaseFromDirName(dirName)
420
- let description: string | undefined
421
-
422
- if (existsSync(skillPath)) {
423
- const content = readFileSync(skillPath, 'utf-8')
424
- title = extractFrontmatterField(content, 'name')
425
- ?? firstHeading(content)
426
- ?? title
427
- description = extractFrontmatterField(content, 'description')
428
- }
429
-
430
- skills.push({
431
- dirName,
432
- title,
433
- description,
434
- toolNames: [],
435
- })
436
- }
437
- }
438
-
439
- if (skills.length === 0 && dirs.commands) {
440
- const commandsDir = resolve(pluginDir, 'commands')
441
- const entries = readdirSync(commandsDir, { withFileTypes: true })
442
-
443
- for (const entry of entries) {
444
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue
445
- const dirName = toKebabCase(entry.name.replace(/\.md$/, '')) || 'command'
446
- const content = readFileSync(resolve(commandsDir, entry.name), 'utf-8')
447
- skills.push({
448
- dirName,
449
- title: firstHeading(content) ?? titleCaseFromDirName(dirName),
450
- description: extractFrontmatterField(content, 'description'),
451
- toolNames: [],
452
- })
453
- }
454
- }
455
-
456
- return skills.sort((a, b) => a.dirName.localeCompare(b.dirName))
457
- }
458
-
459
- function primarySource(result: MigrateResult): McpServer {
460
- const [serverName, server] = Object.entries(result.mcp)[0] ?? []
461
- const auth = normalizeMigrateAuth(server?.auth)
462
- if (server) {
463
- if (server.transport === 'stdio') {
464
- return {
465
- transport: 'stdio',
466
- command: server.command ?? 'TODO_MCP_COMMAND',
467
- args: server.args ?? [],
468
- ...(server.env ? { env: server.env } : {}),
469
- ...(auth ? { auth } : {}),
470
- }
471
- }
472
-
473
- if (server.transport === 'sse') {
474
- return {
475
- transport: 'sse',
476
- url: server.url ?? `https://example.com/${serverName ?? 'mcp'}`,
477
- ...(auth ? { auth } : {}),
478
- }
479
- }
480
-
481
- return {
482
- transport: 'http',
483
- url: server.url ?? `https://example.com/${serverName ?? 'mcp'}`,
484
- ...(auth ? { auth } : {}),
485
- }
486
- }
487
-
488
- return {
489
- transport: 'stdio',
490
- command: 'TODO_MCP_COMMAND',
491
- args: [],
492
- }
493
- }
494
-
495
- function normalizeMigrateAuth(auth: ParsedMcp[string]['auth']) {
496
- if (!auth) return undefined
497
- if (auth.type === 'none') {
498
- return { type: 'none' as const }
499
- }
500
- if (auth.type === 'bearer' && auth.envVar) {
501
- return {
502
- type: 'bearer' as const,
503
- envVar: auth.envVar,
504
- headerName: auth.headerName ?? 'Authorization',
505
- headerTemplate: auth.headerTemplate ?? 'Bearer ${value}',
506
- }
507
- }
508
- if (auth.type === 'header' && auth.envVar && auth.headerName) {
509
- return {
510
- type: 'header' as const,
511
- envVar: auth.envVar,
512
- headerName: auth.headerName,
513
- headerTemplate: auth.headerTemplate ?? '${value}',
514
- }
515
- }
516
- return undefined
517
- }
518
-
519
- function buildMigratedScaffoldMetadata(result: MigrateResult, outputDir: string): McpScaffoldMetadata {
520
- const pluginName = result.manifest.name ?? 'my-plugin'
521
- const displayName = result.manifest.name ? titleCaseFromDirName(result.manifest.name) : 'Migrated Plugin'
522
- const description = result.manifest.description ?? 'Migrated plugin scaffold.'
523
- const generatedHookEvents = Object.keys(result.hooks)
524
- const managedFiles = [
525
- ...(result.instructions ? [result.instructions.replace(/^\.\//, '')] : []),
526
- ...(['skills', 'commands', 'agents', 'scripts', 'assets'] as const).flatMap((dir) => {
527
- if (!result.directories[dir]) return []
528
- const baseDir = dir
529
- const dirPath = resolve(outputDir, baseDir)
530
- if (!existsSync(dirPath)) return []
531
- const entries = readdirSync(dirPath, { withFileTypes: true })
532
- const files: string[] = []
533
- for (const entry of entries) {
534
- if (entry.isDirectory()) {
535
- const nestedDir = resolve(dirPath, entry.name)
536
- for (const nested of readdirSync(nestedDir, { withFileTypes: true })) {
537
- if (nested.isFile()) {
538
- files.push(`${baseDir}/${entry.name}/${nested.name}`)
539
- }
540
- }
541
- continue
542
- }
543
- if (entry.isFile()) {
544
- files.push(`${baseDir}/${entry.name}`)
545
- }
546
- }
547
- return files
548
- }),
549
- 'pluxx.config.ts',
550
- MCP_TAXONOMY_PATH,
551
- MCP_SCAFFOLD_METADATA_PATH,
552
- ]
553
-
554
- return {
555
- version: 1,
556
- source: primarySource(result),
557
- serverInfo: {
558
- name: pluginName,
559
- title: displayName,
560
- version: result.manifest.version ?? '0.1.0',
561
- description,
562
- ...(result.manifest.repository ? { websiteUrl: result.manifest.repository } : {}),
563
- },
564
- settings: {
565
- pluginName,
566
- displayName,
567
- description,
568
- skillGrouping: 'workflow',
569
- requestedHookMode: generatedHookEvents.length > 0 ? 'safe' : 'none',
570
- generatedHookMode: generatedHookEvents.length > 0 ? 'safe' : 'none',
571
- generatedHookEvents,
572
- runtimeAuthMode: 'inline',
573
- },
574
- userConfig: [],
575
- tools: [],
576
- resources: [],
577
- resourceTemplates: [],
578
- prompts: [],
579
- skills: result.persistedSkills.map((skill) => ({
580
- dirName: skill.dirName,
581
- title: skill.title,
582
- description: skill.description,
583
- toolNames: skill.toolNames,
584
- })),
585
- managedFiles: [...new Set(managedFiles)].sort(),
586
- }
587
- }
588
-
589
- // ── Copy Directories ────────────────────────────────────────────
590
-
591
- function copyDirectories(
592
- pluginDir: string,
593
- outputDir: string,
594
- dirs: MigrateResult['directories'],
595
- ): string[] {
596
- const copied: string[] = []
597
- const toCopy = ['skills', 'commands', 'agents', 'scripts', 'assets'] as const
598
-
599
- for (const dir of toCopy) {
600
- if (!dirs[dir]) continue
601
- const src = resolve(pluginDir, dir)
602
- const dest = resolve(outputDir, dir)
603
- if (existsSync(dest)) {
604
- console.log(` skip ./${dir}/ (already exists)`)
605
- continue
606
- }
607
- cpSync(src, dest, { recursive: true })
608
- copied.push(dir)
609
- }
610
-
611
- return copied
612
- }
613
-
614
- // ── Config Generation ───────────────────────────────────────────
615
-
616
- function generateConfigTs(result: MigrateResult): string {
617
- const lines: string[] = []
618
- lines.push(`import { definePlugin } from 'pluxx'`)
619
- lines.push('')
620
- lines.push('export default definePlugin({')
621
-
622
- // Identity
623
- const name = result.manifest.name ?? 'my-plugin'
624
- lines.push(` name: ${quote(name)},`)
625
- if (result.manifest.version) {
626
- lines.push(` version: ${quote(result.manifest.version)},`)
627
- }
628
- lines.push(` description: ${quote(result.manifest.description ?? 'TODO: Describe your plugin')},`)
629
-
630
- // Author
631
- if (result.manifest.author) {
632
- const a = result.manifest.author
633
- lines.push(' author: {')
634
- lines.push(` name: ${quote(a.name)},`)
635
- if (a.url) lines.push(` url: ${quote(a.url)},`)
636
- if (a.email) lines.push(` email: ${quote(a.email)},`)
637
- lines.push(' },')
638
- } else {
639
- lines.push(' author: { name: \'TODO: Your Name\' },')
640
- }
641
-
642
- if (result.manifest.repository) {
643
- lines.push(` repository: ${quote(result.manifest.repository)},`)
644
- }
645
- if (result.manifest.license) {
646
- lines.push(` license: ${quote(result.manifest.license)},`)
647
- }
648
- if (result.manifest.keywords && result.manifest.keywords.length > 0) {
649
- lines.push(` keywords: [${result.manifest.keywords.map(k => quote(k)).join(', ')}],`)
650
- }
651
-
652
- lines.push('')
653
-
654
- // Directories
655
- if (result.directories.skills) {
656
- lines.push(` skills: './skills/',`)
657
- }
658
- if (result.directories.commands) {
659
- lines.push(` commands: './commands/',`)
660
- }
661
- if (result.directories.agents) {
662
- lines.push(` agents: './agents/',`)
663
- }
664
- if (result.directories.scripts) {
665
- lines.push(` scripts: './scripts/',`)
666
- }
667
- if (result.directories.assets) {
668
- lines.push(` assets: './assets/',`)
669
- }
670
- if (result.instructions) {
671
- lines.push(` instructions: ${quote(result.instructions)},`)
672
- }
673
-
674
- // MCP
675
- const mcpNames = Object.keys(result.mcp)
676
- if (mcpNames.length > 0) {
677
- lines.push('')
678
- lines.push(' mcp: {')
679
- for (const name of mcpNames) {
680
- const server = result.mcp[name]
681
- lines.push(` ${quote(name)}: {`)
682
- if (server.url) lines.push(` url: ${quote(server.url)},`)
683
- if (server.transport && server.transport !== 'http') {
684
- lines.push(` transport: ${quote(server.transport)},`)
685
- }
686
- if (server.command) lines.push(` command: ${quote(server.command)},`)
687
- if (server.args && server.args.length > 0) {
688
- lines.push(` args: [${server.args.map(a => quote(a)).join(', ')}],`)
689
- }
690
- if (server.env) {
691
- lines.push(' env: {')
692
- for (const [k, v] of Object.entries(server.env)) {
693
- lines.push(` ${quote(k)}: ${quote(v)},`)
694
- }
695
- lines.push(' },')
696
- }
697
- if (server.auth) {
698
- lines.push(' auth: {')
699
- lines.push(` type: ${quote(server.auth.type)},`)
700
- if (server.auth.envVar) lines.push(` envVar: ${quote(server.auth.envVar)},`)
701
- if (server.auth.headerName && server.auth.headerName !== 'Authorization') {
702
- lines.push(` headerName: ${quote(server.auth.headerName)},`)
703
- }
704
- if (server.auth.headerTemplate && server.auth.headerTemplate !== 'Bearer ${value}') {
705
- lines.push(` headerTemplate: ${quote(server.auth.headerTemplate)},`)
706
- }
707
- lines.push(' },')
708
- }
709
- lines.push(' },')
710
- }
711
- lines.push(' },')
712
- }
713
-
714
- // Hooks
715
- const hookEvents = Object.keys(result.hooks)
716
- if (hookEvents.length > 0) {
717
- lines.push('')
718
- lines.push(' hooks: {')
719
- for (const event of hookEvents) {
720
- const entries = result.hooks[event]
721
- lines.push(` ${event}: [`)
722
- for (const entry of entries) {
723
- const parts: string[] = [`command: ${quote(entry.command)}`]
724
- if (entry.timeout) parts.push(`timeout: ${entry.timeout}`)
725
- if (entry.matcher) parts.push(`matcher: ${quote(entry.matcher)}`)
726
- lines.push(` { ${parts.join(', ')} },`)
727
- }
728
- lines.push(' ],')
729
- }
730
- lines.push(' },')
731
- }
732
-
733
- lines.push('')
734
- lines.push(` // Migrated from ${result.platform} plugin`)
735
- lines.push(` targets: ['claude-code', 'cursor', 'codex', 'opencode'],`)
736
- lines.push('})')
737
- lines.push('')
738
-
739
- return lines.join('\n')
740
- }
741
-
742
- function quote(s: string): string {
743
- // Use single quotes, escape single quotes inside
744
- return `'${s.replace(/'/g, "\\'")}'`
745
- }
746
-
747
- // ── Main Migrate Function ───────────────────────────────────────
748
-
749
- export async function migrate(inputPath: string): Promise<void> {
750
- const pluginDir = resolve(inputPath)
751
- const outputDir = process.cwd()
752
-
753
- if (!existsSync(pluginDir)) {
754
- console.error(`Error: Path does not exist: ${pluginDir}`)
755
- process.exit(1)
756
- }
757
-
758
- console.log(`Scanning ${pluginDir} ...`)
759
-
760
- // 1. Detect platform
761
- const detection = detectPlatform(pluginDir)
762
- if (!detection) {
763
- console.error('Error: Could not detect plugin platform.')
764
- console.error('Expected one of:')
765
- console.error(' .claude-plugin/plugin.json')
766
- console.error(' .cursor-plugin/plugin.json')
767
- console.error(' .codex-plugin/plugin.json')
768
- console.error(' package.json with @opencode-ai/plugin dependency')
769
- process.exit(1)
770
- }
771
-
772
- console.log(`Detected: ${detection.platform} plugin`)
773
-
774
- // 2. Parse manifest
775
- const manifest = parseManifest(detection)
776
- console.log(` name: ${manifest.name ?? '(none)'}`)
777
- console.log(` version: ${manifest.version ?? '(none)'}`)
778
-
779
- // 3. Parse MCP
780
- const mcp = parseMcp(pluginDir, detection)
781
- const mcpCount = Object.keys(mcp).length
782
- if (mcpCount > 0) {
783
- console.log(` mcp servers: ${Object.keys(mcp).join(', ')}`)
784
- }
785
-
786
- // 4. Parse hooks
787
- const hooks = parseHooks(pluginDir, detection)
788
- const hookCount = Object.keys(hooks).length
789
- if (hookCount > 0) {
790
- console.log(` hooks: ${Object.keys(hooks).join(', ')}`)
791
- }
792
-
793
- // 5. Find instructions
794
- const instructions = findInstructions(pluginDir)
795
- if (instructions) {
796
- console.log(` instructions: ${instructions}`)
797
- }
798
-
799
- // 6. Detect directories
800
- const directories = detectDirectories(pluginDir)
801
- const persistedSkills = readMigratedSkills(pluginDir, directories)
802
- const dirNames = Object.entries(directories)
803
- .filter(([_, exists]) => exists)
804
- .map(([name]) => name)
805
- if (dirNames.length > 0) {
806
- console.log(` directories: ${dirNames.join(', ')}`)
807
- }
808
- if (persistedSkills.length > 0) {
809
- console.log(` migrated skills: ${persistedSkills.map((skill) => skill.dirName).join(', ')}`)
810
- }
811
-
812
- // 7. Build result
813
- const result: MigrateResult = {
814
- platform: detection.platform,
815
- manifest,
816
- mcp,
817
- hooks,
818
- instructions,
819
- directories,
820
- persistedSkills,
821
- }
822
-
823
- // 8. Generate config
824
- const configContent = generateConfigTs(result)
825
- const configPath = resolve(outputDir, 'pluxx.config.ts')
826
-
827
- if (existsSync(configPath)) {
828
- console.error(`\nError: pluxx.config.ts already exists in ${outputDir}`)
829
- console.error('Remove it first or run from a different directory.')
830
- process.exit(1)
831
- }
832
-
833
- await Bun.write(configPath, configContent)
834
- console.log(`\nGenerated pluxx.config.ts`)
835
-
836
- // 9. Copy directories
837
- const copied = copyDirectories(pluginDir, outputDir, directories)
838
- if (copied.length > 0) {
839
- console.log(`Copied: ${copied.map(d => `./${d}/`).join(', ')}`)
840
- }
841
-
842
- // 10. Copy instructions file if it exists and is not README.md
843
- if (instructions && instructions !== './README.md') {
844
- const srcInstr = resolve(pluginDir, instructions)
845
- const destInstr = resolve(outputDir, instructions)
846
- if (!existsSync(destInstr)) {
847
- const content = readFileSync(srcInstr, 'utf-8')
848
- await Bun.write(destInstr, content)
849
- console.log(`Copied: ${instructions}`)
850
- }
851
- }
852
-
853
- // 11. Create synthetic migration metadata/taxonomy so Agent Mode and evals work.
854
- const taxonomyPath = resolve(outputDir, MCP_TAXONOMY_PATH)
855
- const metadataPath = resolve(outputDir, MCP_SCAFFOLD_METADATA_PATH)
856
- mkdirSync(resolve(outputDir, '.pluxx'), { recursive: true })
857
- await Bun.write(taxonomyPath, `${JSON.stringify(result.persistedSkills, null, 2)}\n`)
858
- await Bun.write(metadataPath, `${JSON.stringify(buildMigratedScaffoldMetadata(result, outputDir), null, 2)}\n`)
859
- console.log(`Generated: ${MCP_TAXONOMY_PATH}, ${MCP_SCAFFOLD_METADATA_PATH}`)
860
-
861
- console.log('')
862
- console.log('Migration complete! Next steps:')
863
- console.log(' 1. Review pluxx.config.ts and fill in any TODOs')
864
- console.log(' 2. Run: pluxx doctor')
865
- console.log(' 3. Run: pluxx eval')
866
- console.log(' 4. Run: pluxx build')
867
- }