@orchid-labs/pluxx 0.1.0 → 0.1.3

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