@orchid-labs/pluxx 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,860 +0,0 @@
1
- import { resolve, basename, dirname } from 'path'
2
- import { existsSync, symlinkSync, mkdirSync, rmSync, readFileSync, writeFileSync, cpSync } from 'fs'
3
- import { spawnSync } from 'child_process'
4
- import * as readline from 'readline'
5
- import type { PluginConfig, TargetPlatform, UserConfigEntry } from '../schema'
6
- import {
7
- buildUserConfigEnvMap,
8
- buildUserConfigValueMap,
9
- collectUserConfigEntries,
10
- defaultUserConfigEnvVar,
11
- resolveUserConfigEntriesForTarget,
12
- type ResolvedUserConfigEntry,
13
- } from '../user-config'
14
-
15
- interface InstallTarget {
16
- platform: TargetPlatform
17
- pluginDir: string
18
- description: string
19
- }
20
-
21
- export interface PlannedInstallTarget extends InstallTarget {
22
- sourceDir: string
23
- built: boolean
24
- existing: boolean
25
- }
26
-
27
- interface CommandResult {
28
- status: number | null
29
- stdout: string
30
- stderr: string
31
- }
32
-
33
- interface CodexMarketplaceFile {
34
- name?: string
35
- interface?: {
36
- displayName?: string
37
- }
38
- plugins?: Array<{
39
- name: string
40
- source?: {
41
- source?: string
42
- path?: string
43
- }
44
- policy?: {
45
- installation?: string
46
- authentication?: string
47
- }
48
- category?: string
49
- }>
50
- }
51
-
52
- type CommandRunner = (command: string, args: string[]) => CommandResult
53
-
54
- export interface HookCommand {
55
- event: string
56
- command: string
57
- }
58
-
59
- type PluginHooks = PluginConfig['hooks']
60
- type UserConfigPrimitive = string | number | boolean
61
-
62
- interface PlannedUserConfigEntry {
63
- field: UserConfigEntry
64
- envVar?: string
65
- source: 'env' | 'default' | 'missing'
66
- value?: UserConfigPrimitive
67
- }
68
-
69
- export function listHookCommands(hooks?: PluginHooks): HookCommand[] {
70
- if (!hooks) return []
71
-
72
- const commands: HookCommand[] = []
73
- for (const [event, entries] of Object.entries(hooks)) {
74
- for (const entry of entries) {
75
- if (entry.type === 'command' && entry.command) {
76
- commands.push({ event, command: entry.command })
77
- }
78
- }
79
- }
80
-
81
- return commands
82
- }
83
-
84
- export function planInstallUserConfig(
85
- config: PluginConfig,
86
- platforms: TargetPlatform[] = config.targets,
87
- ): PlannedUserConfigEntry[] {
88
- const entries = collectUserConfigEntries(config, platforms)
89
-
90
- return entries.map((field) => {
91
- const envVar = field.envVar ?? defaultUserConfigEnvVar(field.key)
92
- const envValue = process.env[envVar]
93
- if (envValue !== undefined && envValue !== '') {
94
- return {
95
- field,
96
- envVar,
97
- source: 'env',
98
- value: parseUserConfigValue(field, envValue),
99
- }
100
- }
101
-
102
- if (field.defaultValue !== undefined) {
103
- return {
104
- field,
105
- envVar,
106
- source: 'default',
107
- value: field.defaultValue,
108
- }
109
- }
110
-
111
- return {
112
- field,
113
- envVar,
114
- source: 'missing',
115
- }
116
- })
117
- }
118
-
119
- export async function resolveInstallUserConfig(
120
- config: PluginConfig,
121
- platforms: TargetPlatform[] = config.targets,
122
- options: { isTTY?: boolean } = {},
123
- ): Promise<ResolvedUserConfigEntry[]> {
124
- const planned = planInstallUserConfig(config, platforms)
125
- const resolved: ResolvedUserConfigEntry[] = []
126
- const isTTY = options.isTTY ?? process.stdin.isTTY === true
127
-
128
- for (const entry of planned) {
129
- if (entry.value !== undefined) {
130
- resolved.push({
131
- field: entry.field,
132
- value: entry.value,
133
- envVar: entry.envVar,
134
- })
135
- continue
136
- }
137
-
138
- if (entry.field.required === false) {
139
- continue
140
- }
141
-
142
- if (!isTTY) {
143
- const hint = entry.envVar ? ` Export ${entry.envVar} or install interactively.` : ' Re-run interactively to provide it.'
144
- throw new Error(`Missing required userConfig "${entry.field.key}".${hint}`)
145
- }
146
-
147
- const promptLabel = entry.field.title || entry.field.key
148
- const envHint = entry.envVar ? ` [env: ${entry.envVar}]` : ''
149
- const answer = await promptTextValue(`${promptLabel}${envHint}: `)
150
- const value = parseUserConfigValue(entry.field, answer)
151
-
152
- resolved.push({
153
- field: entry.field,
154
- value,
155
- envVar: entry.envVar,
156
- })
157
- }
158
-
159
- return resolved
160
- }
161
-
162
- async function promptTextValue(question: string): Promise<string> {
163
- const rl = readline.createInterface({
164
- input: process.stdin,
165
- output: process.stdout,
166
- })
167
-
168
- try {
169
- const answer = await new Promise<string>((resolveAnswer) => {
170
- rl.question(question, (value) => resolveAnswer(value))
171
- })
172
- return answer
173
- } finally {
174
- rl.close()
175
- }
176
- }
177
-
178
- function parseUserConfigValue(field: UserConfigEntry, rawValue: string): UserConfigPrimitive {
179
- if (field.type === 'number') {
180
- const parsed = Number(rawValue)
181
- if (!Number.isFinite(parsed)) {
182
- throw new Error(`Expected a numeric value for userConfig "${field.key}".`)
183
- }
184
- return parsed
185
- }
186
-
187
- if (field.type === 'boolean') {
188
- const normalized = rawValue.trim().toLowerCase()
189
- if (['true', '1', 'yes', 'y'].includes(normalized)) return true
190
- if (['false', '0', 'no', 'n'].includes(normalized)) return false
191
- throw new Error(`Expected a boolean value for userConfig "${field.key}".`)
192
- }
193
-
194
- return rawValue
195
- }
196
-
197
- async function promptTrustConfirmation(question: string): Promise<boolean> {
198
- const rl = readline.createInterface({
199
- input: process.stdin,
200
- output: process.stdout,
201
- })
202
-
203
- try {
204
- const answer = await new Promise<string>((resolveAnswer) => {
205
- rl.question(question, (value) => resolveAnswer(value))
206
- })
207
- const normalized = answer.trim().toLowerCase()
208
- return normalized === 'y' || normalized === 'yes'
209
- } finally {
210
- rl.close()
211
- }
212
- }
213
-
214
- interface EnsureHookTrustOptions {
215
- pluginName: string
216
- hooks?: PluginHooks
217
- trust?: boolean
218
- isTTY?: boolean
219
- confirmPrompt?: (question: string) => Promise<boolean>
220
- }
221
-
222
- export async function ensureHookTrust(options: EnsureHookTrustOptions): Promise<void> {
223
- const commands = listHookCommands(options.hooks)
224
- if (commands.length === 0) return
225
- if (options.trust) return
226
-
227
- console.warn('\n⚠️ This plugin defines hook commands that run shell code on your machine:')
228
- console.warn('')
229
- for (const { event, command } of commands) {
230
- console.warn(` - ${event}: ${command}`)
231
- }
232
- console.warn('')
233
- console.warn(
234
- `Installing "${options.pluginName}" means trusting this plugin author with local command execution.`
235
- )
236
-
237
- const isTTY = options.isTTY ?? process.stdin.isTTY === true
238
- if (!isTTY) {
239
- throw new Error(
240
- `Refusing to install plugin with hooks in non-interactive mode. Re-run with --trust to continue.`
241
- )
242
- }
243
-
244
- const confirm = options.confirmPrompt ?? promptTrustConfirmation
245
- const approved = await confirm('Continue install? (y/N): ')
246
- if (!approved) {
247
- throw new Error('Install cancelled. Re-run with --trust to bypass confirmation.')
248
- }
249
- }
250
-
251
- function getInstallTargets(pluginName: string): InstallTarget[] {
252
- const home = process.env.HOME ?? '~'
253
- return [
254
- {
255
- platform: 'claude-code',
256
- pluginDir: resolve(home, '.claude/plugins', pluginName),
257
- description: `claude plugin install ${pluginName}@${getClaudeMarketplaceName(pluginName)}`,
258
- },
259
- {
260
- platform: 'cursor',
261
- pluginDir: resolve(home, '.cursor/plugins/local', pluginName),
262
- description: `~/.cursor/plugins/local/${pluginName}`,
263
- },
264
- {
265
- platform: 'codex',
266
- pluginDir: resolve(home, '.codex/plugins', pluginName),
267
- description: `~/.codex/plugins/${pluginName} (via ~/.agents/plugins/marketplace.json)`,
268
- },
269
- {
270
- platform: 'opencode',
271
- pluginDir: resolve(home, '.config/opencode/plugins', pluginName),
272
- description: `~/.config/opencode/plugins/${pluginName}`,
273
- },
274
- {
275
- platform: 'github-copilot',
276
- pluginDir: resolve(home, '.github-copilot/plugins', pluginName),
277
- description: `~/.github-copilot/plugins/${pluginName}`,
278
- },
279
- {
280
- platform: 'openhands',
281
- pluginDir: resolve(home, '.openhands/plugins', pluginName),
282
- description: `~/.openhands/plugins/${pluginName}`,
283
- },
284
- {
285
- platform: 'warp',
286
- pluginDir: resolve(home, '.warp/plugins', pluginName),
287
- description: `~/.warp/plugins/${pluginName}`,
288
- },
289
- {
290
- platform: 'gemini-cli',
291
- pluginDir: resolve(home, '.gemini/extensions', pluginName),
292
- description: `~/.gemini/extensions/${pluginName}`,
293
- },
294
- {
295
- platform: 'roo-code',
296
- pluginDir: resolve(home, '.roo/plugins', pluginName),
297
- description: `~/.roo/plugins/${pluginName}`,
298
- },
299
- {
300
- platform: 'cline',
301
- pluginDir: resolve(home, '.cline/plugins', pluginName),
302
- description: `~/.cline/plugins/${pluginName}`,
303
- },
304
- {
305
- platform: 'amp',
306
- pluginDir: resolve(home, '.amp/plugins', pluginName),
307
- description: `~/.amp/plugins/${pluginName}`,
308
- },
309
- ]
310
- }
311
-
312
- export function getInstallFollowupNotes(platforms: TargetPlatform[]): string[] {
313
- const notes: string[] = []
314
-
315
- if (platforms.includes('claude-code')) {
316
- notes.push('Claude Code note: if Claude is already open, run /reload-plugins in the session to pick up the new install.')
317
- }
318
-
319
- return notes
320
- }
321
-
322
- function runCommandDefault(command: string, args: string[]): CommandResult {
323
- const result = spawnSync(command, args, { encoding: 'utf-8' })
324
- return {
325
- status: result.status,
326
- stdout: result.stdout ?? '',
327
- stderr: result.stderr ?? '',
328
- }
329
- }
330
-
331
- function createSymlinkInstall(target: PlannedInstallTarget): void {
332
- const parentDir = resolve(target.pluginDir, '..')
333
- mkdirSync(parentDir, { recursive: true })
334
-
335
- if (existsSync(target.pluginDir)) {
336
- rmSync(target.pluginDir, { recursive: true, force: true })
337
- }
338
-
339
- symlinkSync(target.sourceDir, target.pluginDir)
340
- }
341
-
342
- function getCodexMarketplacePath(): string {
343
- const home = process.env.HOME ?? '~'
344
- return resolve(home, '.agents/plugins/marketplace.json')
345
- }
346
-
347
- function getCodexMarketplacePluginPath(pluginName: string): string {
348
- return `./.codex/plugins/${pluginName}`
349
- }
350
-
351
- function readCodexMarketplace(filepath: string): CodexMarketplaceFile {
352
- if (!existsSync(filepath)) {
353
- return {
354
- name: 'pluxx-local',
355
- interface: {
356
- displayName: 'Pluxx Local',
357
- },
358
- plugins: [],
359
- }
360
- }
361
-
362
- const raw = readFileSync(filepath, 'utf-8')
363
- const parsed = JSON.parse(raw) as CodexMarketplaceFile
364
- return {
365
- name: parsed.name ?? 'pluxx-local',
366
- interface: parsed.interface ?? { displayName: 'Pluxx Local' },
367
- plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [],
368
- }
369
- }
370
-
371
- function ensureCodexMarketplace(pluginName: string): void {
372
- const filepath = getCodexMarketplacePath()
373
- mkdirSync(dirname(filepath), { recursive: true })
374
-
375
- const marketplace = readCodexMarketplace(filepath)
376
- const nextPlugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName)
377
- nextPlugins.push({
378
- name: pluginName,
379
- source: {
380
- source: 'local',
381
- path: getCodexMarketplacePluginPath(pluginName),
382
- },
383
- policy: {
384
- installation: 'AVAILABLE',
385
- authentication: 'ON_INSTALL',
386
- },
387
- category: 'Productivity',
388
- })
389
-
390
- writeFileSync(
391
- filepath,
392
- JSON.stringify({
393
- name: marketplace.name ?? 'pluxx-local',
394
- interface: marketplace.interface ?? { displayName: 'Pluxx Local' },
395
- plugins: nextPlugins,
396
- }, null, 2) + '\n',
397
- )
398
- }
399
-
400
- function removeCodexMarketplacePlugin(pluginName: string): void {
401
- const filepath = getCodexMarketplacePath()
402
- if (!existsSync(filepath)) return
403
-
404
- const marketplace = readCodexMarketplace(filepath)
405
- const nextPlugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName)
406
-
407
- if (nextPlugins.length === (marketplace.plugins ?? []).length) {
408
- return
409
- }
410
-
411
- if (nextPlugins.length === 0) {
412
- rmSync(filepath, { force: true })
413
- return
414
- }
415
-
416
- writeFileSync(
417
- filepath,
418
- JSON.stringify({
419
- name: marketplace.name ?? 'pluxx-local',
420
- interface: marketplace.interface ?? { displayName: 'Pluxx Local' },
421
- plugins: nextPlugins,
422
- }, null, 2) + '\n',
423
- )
424
- }
425
-
426
- function createCopiedInstall(target: PlannedInstallTarget): void {
427
- const parentDir = resolve(target.pluginDir, '..')
428
- mkdirSync(parentDir, { recursive: true })
429
-
430
- if (existsSync(target.pluginDir)) {
431
- rmSync(target.pluginDir, { recursive: true, force: true })
432
- }
433
-
434
- cpSync(target.sourceDir, target.pluginDir, { recursive: true })
435
- }
436
-
437
- function materializeTemplateValue(value: string, env: Record<string, string>): string {
438
- return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name: string) => env[name] ?? `\${${name}}`)
439
- }
440
-
441
- function materializeEnvRecord(
442
- input: Record<string, string> | undefined,
443
- env: Record<string, string>,
444
- ): Record<string, string> {
445
- const output: Record<string, string> = {}
446
-
447
- for (const [key, value] of Object.entries(input ?? {})) {
448
- output[key] = materializeTemplateValue(value, env)
449
- }
450
-
451
- return output
452
- }
453
-
454
- function patchInstalledMcpConfig(
455
- pluginDir: string,
456
- platform: TargetPlatform,
457
- config: PluginConfig,
458
- entries: ResolvedUserConfigEntry[],
459
- ): void {
460
- if (!config.mcp) return
461
-
462
- const env = buildUserConfigEnvMap(entries)
463
-
464
- if (platform === 'claude-code' || platform === 'cursor') {
465
- const filepath = resolve(pluginDir, platform === 'claude-code' ? '.mcp.json' : 'mcp.json')
466
- if (!existsSync(filepath)) return
467
-
468
- const mcpServers: Record<string, unknown> = {}
469
- const usesPlatformManagedAuth = platform === 'claude-code'
470
- ? config.platforms?.['claude-code']?.mcpAuth === 'platform'
471
- : config.platforms?.cursor?.mcpAuth === 'platform'
472
-
473
- for (const [name, server] of Object.entries(config.mcp)) {
474
- if (server.transport === 'stdio') {
475
- mcpServers[name] = {
476
- command: server.command,
477
- args: server.args ?? [],
478
- env: materializeEnvRecord(server.env, env),
479
- }
480
- continue
481
- }
482
-
483
- const entry: Record<string, unknown> = {
484
- type: server.transport === 'sse' ? 'sse' : 'http',
485
- url: server.url,
486
- }
487
-
488
- if (!usesPlatformManagedAuth && server.auth?.type === 'bearer' && server.auth.envVar && env[server.auth.envVar]) {
489
- entry.headers = {
490
- Authorization: `Bearer ${env[server.auth.envVar]}`,
491
- }
492
- } else if (!usesPlatformManagedAuth && server.auth?.type === 'header' && server.auth.envVar && env[server.auth.envVar]) {
493
- entry.headers = {
494
- [server.auth.headerName]: server.auth.headerTemplate.replace('${value}', env[server.auth.envVar]),
495
- }
496
- }
497
-
498
- mcpServers[name] = entry
499
- }
500
-
501
- writeFileSync(filepath, JSON.stringify({ mcpServers }, null, 2) + '\n')
502
- return
503
- }
504
-
505
- if (platform === 'codex') {
506
- const filepath = resolve(pluginDir, '.mcp.json')
507
- if (!existsSync(filepath)) return
508
-
509
- const mcpServers: Record<string, unknown> = {}
510
-
511
- for (const [name, server] of Object.entries(config.mcp)) {
512
- if (server.transport === 'stdio') {
513
- mcpServers[name] = {
514
- command: server.command,
515
- args: server.args ?? [],
516
- env: materializeEnvRecord(server.env, env),
517
- }
518
- continue
519
- }
520
-
521
- const entry: Record<string, unknown> = {
522
- url: server.url,
523
- }
524
-
525
- if (server.auth?.type === 'bearer' && server.auth.envVar && env[server.auth.envVar]) {
526
- entry.http_headers = {
527
- Authorization: `Bearer ${env[server.auth.envVar]}`,
528
- }
529
- } else if (server.auth?.type === 'header' && server.auth.envVar && env[server.auth.envVar]) {
530
- entry.http_headers = {
531
- [server.auth.headerName]: server.auth.headerTemplate.replace('${value}', env[server.auth.envVar]),
532
- }
533
- }
534
-
535
- mcpServers[name] = entry
536
- }
537
-
538
- writeFileSync(filepath, JSON.stringify({ mcpServers }, null, 2) + '\n')
539
- }
540
- }
541
-
542
- function writeInstalledUserConfig(
543
- pluginDir: string,
544
- entries: ResolvedUserConfigEntry[],
545
- ): void {
546
- if (entries.length === 0) return
547
-
548
- const filepath = resolve(pluginDir, '.pluxx-user.json')
549
- const payload = {
550
- values: buildUserConfigValueMap(entries),
551
- env: buildUserConfigEnvMap(entries),
552
- }
553
-
554
- writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\n')
555
- }
556
-
557
- function disableInstalledEnvValidation(pluginDir: string, entries: ResolvedUserConfigEntry[]): void {
558
- if (entries.length === 0) return
559
-
560
- const filepath = resolve(pluginDir, 'scripts/check-env.sh')
561
- if (!existsSync(filepath)) return
562
-
563
- writeFileSync(
564
- filepath,
565
- '#!/usr/bin/env bash\nset -euo pipefail\n# pluxx install materialized required config for this local plugin install.\nexit 0\n',
566
- )
567
- }
568
-
569
- function materializeInstalledPlugin(
570
- pluginDir: string,
571
- platform: TargetPlatform,
572
- config: PluginConfig,
573
- entries: ResolvedUserConfigEntry[],
574
- ): void {
575
- if (entries.length === 0) return
576
-
577
- writeInstalledUserConfig(pluginDir, entries)
578
- disableInstalledEnvValidation(pluginDir, entries)
579
- patchInstalledMcpConfig(pluginDir, platform, config, entries)
580
- }
581
-
582
- function getClaudeMarketplaceName(pluginName: string): string {
583
- return `pluxx-local-${pluginName}`
584
- }
585
-
586
- function getClaudeMarketplaceRoot(pluginName: string): string {
587
- const home = process.env.HOME ?? '~'
588
- return resolve(home, '.claude/plugins/data', getClaudeMarketplaceName(pluginName))
589
- }
590
-
591
- function ensureClaudeMarketplace(
592
- pluginName: string,
593
- sourceDir: string,
594
- materialized?: {
595
- config: PluginConfig
596
- entries: ResolvedUserConfigEntry[]
597
- },
598
- ): { marketplaceName: string; marketplaceRoot: string } {
599
- const marketplaceName = getClaudeMarketplaceName(pluginName)
600
- const marketplaceRoot = getClaudeMarketplaceRoot(pluginName)
601
- const marketplaceManifestDir = resolve(marketplaceRoot, '.claude-plugin')
602
- const marketplacePluginDir = resolve(marketplaceRoot, 'plugins', pluginName)
603
- const pluginManifestPath = resolve(sourceDir, '.claude-plugin/plugin.json')
604
-
605
- const pluginManifest = JSON.parse(readFileSync(pluginManifestPath, 'utf-8')) as {
606
- description?: string
607
- version?: string
608
- author?: unknown
609
- license?: string
610
- homepage?: string
611
- repository?: string
612
- keywords?: string[]
613
- }
614
-
615
- rmSync(marketplaceRoot, { recursive: true, force: true })
616
- mkdirSync(marketplaceManifestDir, { recursive: true })
617
- mkdirSync(resolve(marketplaceRoot, 'plugins'), { recursive: true })
618
- if (materialized && materialized.entries.length > 0) {
619
- cpSync(sourceDir, marketplacePluginDir, { recursive: true })
620
- materializeInstalledPlugin(marketplacePluginDir, 'claude-code', materialized.config, materialized.entries)
621
- } else {
622
- symlinkSync(sourceDir, marketplacePluginDir)
623
- }
624
-
625
- writeFileSync(
626
- resolve(marketplaceManifestDir, 'marketplace.json'),
627
- JSON.stringify({
628
- name: marketplaceName,
629
- owner: {
630
- name: 'Pluxx',
631
- },
632
- plugins: [
633
- {
634
- name: pluginName,
635
- source: `./plugins/${pluginName}`,
636
- description: pluginManifest.description ?? `Local Pluxx-built ${pluginName} plugin.`,
637
- version: pluginManifest.version ?? '0.1.0',
638
- author: pluginManifest.author ?? { name: 'Pluxx' },
639
- license: pluginManifest.license ?? 'MIT',
640
- ...(pluginManifest.homepage ? { homepage: pluginManifest.homepage } : {}),
641
- ...(pluginManifest.repository ? { repository: pluginManifest.repository } : {}),
642
- ...(pluginManifest.keywords ? { keywords: pluginManifest.keywords } : {}),
643
- },
644
- ],
645
- }, null, 2),
646
- )
647
-
648
- return { marketplaceName, marketplaceRoot }
649
- }
650
-
651
- function ensureClaudeMarketplaceRegistered(
652
- pluginName: string,
653
- sourceDir: string,
654
- runCommand: CommandRunner,
655
- materialized?: {
656
- config: PluginConfig
657
- entries: ResolvedUserConfigEntry[]
658
- },
659
- ): string {
660
- const { marketplaceName, marketplaceRoot } = ensureClaudeMarketplace(pluginName, sourceDir, materialized)
661
- const marketplaces = runCommand('claude', ['plugin', 'marketplace', 'list', '--json'])
662
-
663
- if (marketplaces.status !== 0) {
664
- throw new Error(`Failed to list Claude marketplaces: ${marketplaces.stderr || marketplaces.stdout}`)
665
- }
666
-
667
- const known = JSON.parse(marketplaces.stdout) as Array<{ name?: string }>
668
- if (!known.some(entry => entry.name === marketplaceName)) {
669
- const add = runCommand('claude', ['plugin', 'marketplace', 'add', marketplaceRoot])
670
- if (add.status !== 0) {
671
- throw new Error(`Failed to add Claude marketplace: ${add.stderr || add.stdout}`)
672
- }
673
- }
674
-
675
- return marketplaceName
676
- }
677
-
678
- function installClaudePlugin(
679
- target: PlannedInstallTarget,
680
- pluginName: string,
681
- runCommand: CommandRunner,
682
- materialized?: {
683
- config: PluginConfig
684
- entries: ResolvedUserConfigEntry[]
685
- },
686
- ): void {
687
- const marketplaceName = ensureClaudeMarketplaceRegistered(pluginName, target.sourceDir, runCommand, materialized)
688
-
689
- if (existsSync(target.pluginDir)) {
690
- rmSync(target.pluginDir, { recursive: true, force: true })
691
- }
692
-
693
- runCommand('claude', ['plugin', 'uninstall', `${pluginName}@${marketplaceName}`])
694
-
695
- const install = runCommand('claude', ['plugin', 'install', `${pluginName}@${marketplaceName}`, '--scope', 'user'])
696
- if (install.status !== 0) {
697
- throw new Error(`Failed to install Claude plugin: ${install.stderr || install.stdout}`)
698
- }
699
- }
700
-
701
- function uninstallClaudePlugin(
702
- target: InstallTarget,
703
- pluginName: string,
704
- runCommand: CommandRunner,
705
- options: { quiet?: boolean } = {},
706
- ): boolean {
707
- const marketplaceName = getClaudeMarketplaceName(pluginName)
708
- const uninstall = runCommand('claude', ['plugin', 'uninstall', `${pluginName}@${marketplaceName}`])
709
-
710
- if (uninstall.status !== 0 && !options.quiet) {
711
- const detail = uninstall.stderr || uninstall.stdout
712
- if (detail.trim().length > 0) {
713
- console.warn(` warning claude-code uninstall: ${detail.trim()}`)
714
- }
715
- }
716
-
717
- const marketplaceRoot = getClaudeMarketplaceRoot(pluginName)
718
- const hadMarketplaceRoot = existsSync(marketplaceRoot)
719
- rmSync(marketplaceRoot, { recursive: true, force: true })
720
-
721
- const hadLegacyPluginDir = existsSync(target.pluginDir)
722
- if (hadLegacyPluginDir) {
723
- rmSync(target.pluginDir, { recursive: true, force: true })
724
- }
725
-
726
- return uninstall.status === 0 || hadMarketplaceRoot || hadLegacyPluginDir
727
- }
728
-
729
- export function planInstallPlugin(
730
- distDir: string,
731
- pluginName: string,
732
- platforms?: TargetPlatform[],
733
- ): PlannedInstallTarget[] {
734
- const targets = getInstallTargets(pluginName)
735
- const filtered = platforms
736
- ? targets.filter(t => platforms.includes(t.platform))
737
- : targets
738
-
739
- return filtered.map((target) => {
740
- const sourceDir = resolve(distDir, target.platform)
741
- return {
742
- ...target,
743
- sourceDir,
744
- built: existsSync(sourceDir),
745
- existing: existsSync(target.pluginDir),
746
- }
747
- })
748
- }
749
-
750
- export async function installPlugin(
751
- distDir: string,
752
- pluginName: string,
753
- platforms?: TargetPlatform[],
754
- options: {
755
- config?: PluginConfig
756
- quiet?: boolean
757
- useNativeClaudeInstall?: boolean
758
- runCommand?: CommandRunner
759
- resolvedUserConfig?: ResolvedUserConfigEntry[]
760
- } = {},
761
- ): Promise<void> {
762
- const filtered = planInstallPlugin(distDir, pluginName, platforms)
763
- const runCommand = options.runCommand ?? runCommandDefault
764
- const useNativeClaudeInstall = options.useNativeClaudeInstall ?? true
765
-
766
- let installed = 0
767
-
768
- for (const target of filtered) {
769
- if (!target.built) {
770
- if (!options.quiet) {
771
- console.log(` skip ${target.platform} (not built)`)
772
- }
773
- continue
774
- }
775
-
776
- const targetConfigEntries = options.resolvedUserConfig
777
- ? resolveUserConfigEntriesForTarget(options.resolvedUserConfig, target.platform)
778
- : []
779
- const shouldMaterialize = targetConfigEntries.length > 0 && options.config
780
-
781
- if (target.platform === 'claude-code' && useNativeClaudeInstall) {
782
- installClaudePlugin(
783
- target,
784
- pluginName,
785
- runCommand,
786
- shouldMaterialize
787
- ? {
788
- config: options.config!,
789
- entries: targetConfigEntries,
790
- }
791
- : undefined,
792
- )
793
- } else if (shouldMaterialize) {
794
- createCopiedInstall(target)
795
- materializeInstalledPlugin(target.pluginDir, target.platform, options.config!, targetConfigEntries)
796
- } else {
797
- createSymlinkInstall(target)
798
- }
799
- if (target.platform === 'codex') {
800
- ensureCodexMarketplace(pluginName)
801
- }
802
- if (!options.quiet) {
803
- console.log(` ${target.platform} -> ${target.description}`)
804
- }
805
- installed++
806
- }
807
-
808
- if (installed === 0 && !options.quiet) {
809
- console.log('Nothing to install. Run `pluxx build` first.')
810
- } else if (!options.quiet) {
811
- console.log(`\nInstalled ${installed} plugin(s). Reload or restart your tools to pick them up.`)
812
- for (const note of getInstallFollowupNotes(filtered.map((target) => target.platform))) {
813
- console.log(note)
814
- }
815
- }
816
- }
817
-
818
- export async function uninstallPlugin(
819
- pluginName: string,
820
- platforms?: TargetPlatform[],
821
- options: { quiet?: boolean; runCommand?: CommandRunner } = {},
822
- ): Promise<void> {
823
- const targets = getInstallTargets(pluginName)
824
- const filtered = platforms
825
- ? targets.filter(t => platforms.includes(t.platform))
826
- : targets
827
- const runCommand = options.runCommand ?? runCommandDefault
828
-
829
- let removed = 0
830
-
831
- for (const target of filtered) {
832
- if (target.platform === 'claude-code') {
833
- const removedClaude = uninstallClaudePlugin(target, pluginName, runCommand, { quiet: options.quiet })
834
- if (removedClaude) {
835
- if (!options.quiet) {
836
- console.log(` removed ${target.description}`)
837
- }
838
- removed++
839
- }
840
- continue
841
- }
842
-
843
- if (existsSync(target.pluginDir)) {
844
- rmSync(target.pluginDir, { recursive: true, force: true })
845
- if (!options.quiet) {
846
- console.log(` removed ${target.description}`)
847
- }
848
- removed++
849
- }
850
- if (target.platform === 'codex') {
851
- removeCodexMarketplacePlugin(pluginName)
852
- }
853
- }
854
-
855
- if (removed === 0 && !options.quiet) {
856
- console.log('Nothing to uninstall.')
857
- } else if (!options.quiet) {
858
- console.log(`\nRemoved ${removed} plugin(s).`)
859
- }
860
- }