@orchid-labs/pluxx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +574 -0
- package/bin/pluxx.js +37 -0
- package/dist/cli/agent.d.ts +90 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts +19 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/init-from-mcp.d.ts +145 -0
- package/dist/cli/init-from-mcp.d.ts.map +1 -0
- package/dist/cli/install.d.ts +56 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/lint.d.ts +18 -0
- package/dist/cli/lint.d.ts.map +1 -0
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/prompt.d.ts +20 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/publish.d.ts +70 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/runtime.d.ts +20 -0
- package/dist/cli/runtime.d.ts.map +1 -0
- package/dist/cli/sync-from-mcp.d.ts +32 -0
- package/dist/cli/sync-from-mcp.d.ts.map +1 -0
- package/dist/cli/test.d.ts +33 -0
- package/dist/cli/test.d.ts.map +1 -0
- package/dist/compatibility/matrix.d.ts +14 -0
- package/dist/compatibility/matrix.d.ts.map +1 -0
- package/dist/config/define.d.ts +18 -0
- package/dist/config/define.d.ts.map +1 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/generators/amp/index.d.ts +13 -0
- package/dist/generators/amp/index.d.ts.map +1 -0
- package/dist/generators/base.d.ts +49 -0
- package/dist/generators/base.d.ts.map +1 -0
- package/dist/generators/claude-code/index.d.ts +7 -0
- package/dist/generators/claude-code/index.d.ts.map +1 -0
- package/dist/generators/cline/index.d.ts +14 -0
- package/dist/generators/cline/index.d.ts.map +1 -0
- package/dist/generators/codex/index.d.ts +9 -0
- package/dist/generators/codex/index.d.ts.map +1 -0
- package/dist/generators/cursor/index.d.ts +11 -0
- package/dist/generators/cursor/index.d.ts.map +1 -0
- package/dist/generators/gemini-cli/index.d.ts +13 -0
- package/dist/generators/gemini-cli/index.d.ts.map +1 -0
- package/dist/generators/github-copilot/index.d.ts +11 -0
- package/dist/generators/github-copilot/index.d.ts.map +1 -0
- package/dist/generators/hooks-warning.d.ts +3 -0
- package/dist/generators/hooks-warning.d.ts.map +1 -0
- package/dist/generators/index.d.ts +11 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/opencode/index.d.ts +15 -0
- package/dist/generators/opencode/index.d.ts.map +1 -0
- package/dist/generators/openhands/index.d.ts +11 -0
- package/dist/generators/openhands/index.d.ts.map +1 -0
- package/dist/generators/roo-code/index.d.ts +14 -0
- package/dist/generators/roo-code/index.d.ts.map +1 -0
- package/dist/generators/shared/claude-family.d.ts +18 -0
- package/dist/generators/shared/claude-family.d.ts.map +1 -0
- package/dist/generators/warp/index.d.ts +13 -0
- package/dist/generators/warp/index.d.ts.map +1 -0
- package/dist/hook-events.d.ts +4 -0
- package/dist/hook-events.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5302 -0
- package/dist/mcp/introspect.d.ts +34 -0
- package/dist/mcp/introspect.d.ts.map +1 -0
- package/dist/permissions.d.ts +18 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/schema.d.ts +9457 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/user-config.d.ts +19 -0
- package/dist/user-config.d.ts.map +1 -0
- package/dist/validation/platform-rules.d.ts +64 -0
- package/dist/validation/platform-rules.d.ts.map +1 -0
- package/package.json +76 -0
- package/src/cli/agent.ts +1030 -0
- package/src/cli/dev.ts +112 -0
- package/src/cli/doctor.ts +588 -0
- package/src/cli/index.ts +2414 -0
- package/src/cli/init-from-mcp.ts +1611 -0
- package/src/cli/install.ts +698 -0
- package/src/cli/lint.ts +1219 -0
- package/src/cli/migrate.ts +614 -0
- package/src/cli/prompt.ts +82 -0
- package/src/cli/publish.ts +401 -0
- package/src/cli/runtime.ts +86 -0
- package/src/cli/sync-from-mcp.ts +563 -0
- package/src/cli/test.ts +134 -0
- package/src/compatibility/matrix.ts +149 -0
- package/src/config/define.ts +20 -0
- package/src/config/load.ts +74 -0
- package/src/generators/amp/index.ts +63 -0
- package/src/generators/base.ts +188 -0
- package/src/generators/claude-code/index.ts +29 -0
- package/src/generators/cline/index.ts +35 -0
- package/src/generators/codex/index.ts +120 -0
- package/src/generators/cursor/index.ts +158 -0
- package/src/generators/gemini-cli/index.ts +83 -0
- package/src/generators/github-copilot/index.ts +32 -0
- package/src/generators/hooks-warning.ts +51 -0
- package/src/generators/index.ts +71 -0
- package/src/generators/opencode/index.ts +526 -0
- package/src/generators/openhands/index.ts +32 -0
- package/src/generators/roo-code/index.ts +35 -0
- package/src/generators/shared/claude-family.ts +215 -0
- package/src/generators/warp/index.ts +32 -0
- package/src/hook-events.ts +33 -0
- package/src/index.ts +23 -0
- package/src/mcp/introspect.ts +834 -0
- package/src/permissions.ts +258 -0
- package/src/schema.ts +312 -0
- package/src/user-config.ts +177 -0
- package/src/validation/platform-rules.ts +565 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { TargetPlatform } from '../schema'
|
|
2
|
+
import { PLATFORM_VALIDATION_RULES, type PlatformRules } from '../validation/platform-rules'
|
|
3
|
+
|
|
4
|
+
export interface PlatformCompatibilityRow {
|
|
5
|
+
platform: TargetPlatform
|
|
6
|
+
status: 'Primary' | 'Beta'
|
|
7
|
+
docsBasis: string
|
|
8
|
+
manifest: string
|
|
9
|
+
hooks: string
|
|
10
|
+
mcp: string
|
|
11
|
+
instructions: string
|
|
12
|
+
verification: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ALL_TARGET_PLATFORMS: TargetPlatform[] = [
|
|
16
|
+
'claude-code',
|
|
17
|
+
'cursor',
|
|
18
|
+
'codex',
|
|
19
|
+
'opencode',
|
|
20
|
+
'github-copilot',
|
|
21
|
+
'openhands',
|
|
22
|
+
'warp',
|
|
23
|
+
'gemini-cli',
|
|
24
|
+
'roo-code',
|
|
25
|
+
'cline',
|
|
26
|
+
'amp',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const PRIMARY_TARGETS = new Set<TargetPlatform>(['claude-code', 'cursor', 'codex', 'opencode'])
|
|
30
|
+
const RELEASE_SMOKE_TARGETS = new Set<TargetPlatform>(['claude-code', 'cursor', 'codex', 'opencode'])
|
|
31
|
+
const GENERATOR_FIXTURE_TARGETS = new Set<TargetPlatform>(ALL_TARGET_PLATFORMS)
|
|
32
|
+
|
|
33
|
+
const INHERITED_RULES: Partial<Record<TargetPlatform, keyof typeof PLATFORM_VALIDATION_RULES>> = {
|
|
34
|
+
'github-copilot': 'claude-code',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getRuleEntry(platform: TargetPlatform): {
|
|
38
|
+
rules: PlatformRules
|
|
39
|
+
docsBasis: string
|
|
40
|
+
} {
|
|
41
|
+
const directRule = PLATFORM_VALIDATION_RULES[platform as keyof typeof PLATFORM_VALIDATION_RULES]
|
|
42
|
+
if (directRule) {
|
|
43
|
+
return {
|
|
44
|
+
rules: directRule,
|
|
45
|
+
docsBasis: 'Official docs audited',
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const inheritedPlatform = INHERITED_RULES[platform]
|
|
50
|
+
if (!inheritedPlatform) {
|
|
51
|
+
throw new Error(`No platform rules available for ${platform}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
rules: PLATFORM_VALIDATION_RULES[inheritedPlatform],
|
|
56
|
+
docsBasis: `Inherits ${humanizePlatformName(inheritedPlatform)} packaging`,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PLATFORM_LABELS: Record<TargetPlatform, string> = {
|
|
61
|
+
'claude-code': 'Claude Code',
|
|
62
|
+
cursor: 'Cursor',
|
|
63
|
+
codex: 'Codex',
|
|
64
|
+
opencode: 'OpenCode',
|
|
65
|
+
'github-copilot': 'GitHub Copilot',
|
|
66
|
+
openhands: 'OpenHands',
|
|
67
|
+
warp: 'Warp',
|
|
68
|
+
'gemini-cli': 'Gemini CLI',
|
|
69
|
+
'roo-code': 'Roo Code',
|
|
70
|
+
cline: 'Cline',
|
|
71
|
+
amp: 'AMP',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function humanizePlatformName(platform: TargetPlatform): string {
|
|
75
|
+
return PLATFORM_LABELS[platform]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatFiles(files: string[], emptyLabel: string): string {
|
|
79
|
+
return files.length > 0 ? files.join(', ') : emptyLabel
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getVerificationLabel(platform: TargetPlatform): string {
|
|
83
|
+
const labels: string[] = []
|
|
84
|
+
|
|
85
|
+
if (RELEASE_SMOKE_TARGETS.has(platform)) {
|
|
86
|
+
labels.push('Release smoke')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (GENERATOR_FIXTURE_TARGETS.has(platform)) {
|
|
90
|
+
labels.push('Generator fixture')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return labels.join(' + ')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getPlatformCompatibilityMatrix(): PlatformCompatibilityRow[] {
|
|
97
|
+
return ALL_TARGET_PLATFORMS.map((platform) => {
|
|
98
|
+
const { rules, docsBasis } = getRuleEntry(platform)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
platform,
|
|
102
|
+
status: PRIMARY_TARGETS.has(platform) ? 'Primary' : 'Beta',
|
|
103
|
+
docsBasis,
|
|
104
|
+
manifest: formatFiles(rules.manifest.files, 'No dedicated manifest'),
|
|
105
|
+
hooks: rules.hooks.supported
|
|
106
|
+
? formatFiles(rules.hooks.files, 'Supported')
|
|
107
|
+
: 'No documented standalone hook surface',
|
|
108
|
+
mcp: formatFiles(rules.mcp.files, 'None documented'),
|
|
109
|
+
instructions: formatFiles(rules.instructions.files, 'None documented'),
|
|
110
|
+
verification: getVerificationLabel(platform),
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function renderCompatibilityMatrixMarkdown(): string {
|
|
116
|
+
const rows = getPlatformCompatibilityMatrix()
|
|
117
|
+
|
|
118
|
+
const lines = [
|
|
119
|
+
'<!-- Generated by `bun scripts/generate-compatibility-matrix.ts`. Do not edit by hand. -->',
|
|
120
|
+
'# Compatibility Matrix',
|
|
121
|
+
'',
|
|
122
|
+
'This matrix is generated from the repo-owned platform rules and verification surface.',
|
|
123
|
+
'',
|
|
124
|
+
'Verification labels:',
|
|
125
|
+
'- `Release smoke`: exercised end to end by the real CLI against the real example plugins on the core four.',
|
|
126
|
+
'- `Generator fixture`: covered by the repo build/test fixture layer.',
|
|
127
|
+
'',
|
|
128
|
+
'| Platform | Status | Docs Basis | Manifest | Hooks | MCP | Instructions | Verification |',
|
|
129
|
+
'|----------|--------|------------|----------|-------|-----|--------------|--------------|',
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
lines.push(
|
|
134
|
+
`| ${humanizePlatformName(row.platform)} | ${row.status} | ${row.docsBasis} | ${row.manifest} | ${row.hooks} | ${row.mcp} | ${row.instructions} | ${row.verification} |`,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
lines.push(
|
|
139
|
+
'',
|
|
140
|
+
'## Notes',
|
|
141
|
+
'',
|
|
142
|
+
'- The prime-time launch path remains Claude Code, Cursor, Codex, and OpenCode.',
|
|
143
|
+
'- GitHub Copilot currently inherits the Claude Code packaging model in Pluxx rather than using its own separately audited plugin surface.',
|
|
144
|
+
'- Beta platforms are generated and fixture-tested, but they do not yet have the same release-smoke coverage as the core four.',
|
|
145
|
+
'',
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return `${lines.join('\n')}\n`
|
|
149
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type PluginConfig, PluginConfigSchema } from '../schema'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Define a plugin configuration with full type checking and validation.
|
|
5
|
+
*
|
|
6
|
+
* Usage in pluxx.config.ts:
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { definePlugin } from 'pluxx'
|
|
9
|
+
*
|
|
10
|
+
* export default definePlugin({
|
|
11
|
+
* name: 'my-plugin',
|
|
12
|
+
* description: 'My awesome plugin',
|
|
13
|
+
* author: { name: 'Me' },
|
|
14
|
+
* // ...
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function definePlugin(config: PluginConfig): PluginConfig {
|
|
19
|
+
return PluginConfigSchema.parse(config)
|
|
20
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolve, extname, dirname } from 'path'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import { rm } from 'fs/promises'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
5
|
+
import { PluginConfigSchema, type PluginConfig } from '../schema'
|
|
6
|
+
|
|
7
|
+
export const CONFIG_FILES = [
|
|
8
|
+
'pluxx.config.ts',
|
|
9
|
+
'pluxx.config.js',
|
|
10
|
+
'pluxx.config.json',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
function getRuntimePackageEntry(): string {
|
|
14
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
15
|
+
const distEntry = resolve(packageRoot, 'dist', 'index.js')
|
|
16
|
+
|
|
17
|
+
if (existsSync(distEntry)) {
|
|
18
|
+
return distEntry
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return resolve(packageRoot, 'src', 'index.ts')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function importConfigModule(filepath: string): Promise<unknown> {
|
|
25
|
+
const ext = extname(filepath)
|
|
26
|
+
const runtimeEntryUrl = pathToFileURL(getRuntimePackageEntry()).href
|
|
27
|
+
const source = await Bun.file(filepath).text()
|
|
28
|
+
const rewritten = source
|
|
29
|
+
.replace(/from\s+(['"])pluxx\1/g, `from ${JSON.stringify(runtimeEntryUrl)}`)
|
|
30
|
+
.replace(/import\s+(['"])pluxx\1/g, `import ${JSON.stringify(runtimeEntryUrl)}`)
|
|
31
|
+
|
|
32
|
+
const transpiler = new Bun.Transpiler({
|
|
33
|
+
loader: ext === '.ts' ? 'ts' : 'js',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const tempFile = resolve(
|
|
37
|
+
dirname(filepath),
|
|
38
|
+
`.pluxx-load-config-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`,
|
|
39
|
+
)
|
|
40
|
+
await Bun.write(tempFile, transpiler.transformSync(rewritten))
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const mod = await import(`${pathToFileURL(tempFile).href}?t=${Date.now()}`)
|
|
44
|
+
return mod.default ?? mod
|
|
45
|
+
} finally {
|
|
46
|
+
await rm(tempFile, { force: true })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load and validate a pluxx config from the given directory.
|
|
52
|
+
*/
|
|
53
|
+
export async function loadConfig(dir: string = process.cwd()): Promise<PluginConfig> {
|
|
54
|
+
for (const filename of CONFIG_FILES) {
|
|
55
|
+
const filepath = resolve(dir, filename)
|
|
56
|
+
if (!existsSync(filepath)) continue
|
|
57
|
+
|
|
58
|
+
const ext = extname(filename)
|
|
59
|
+
|
|
60
|
+
if (ext === '.ts' || ext === '.js') {
|
|
61
|
+
const raw = await importConfigModule(filepath)
|
|
62
|
+
return PluginConfigSchema.parse(raw)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (ext === '.json') {
|
|
66
|
+
const text = await Bun.file(filepath).text()
|
|
67
|
+
return PluginConfigSchema.parse(JSON.parse(text))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(
|
|
72
|
+
`No pluxx config found in ${dir}. Expected one of: ${CONFIG_FILES.join(', ')}`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { Generator } from '../base'
|
|
3
|
+
import { warnDroppedHookFields } from '../hooks-warning'
|
|
4
|
+
import type { TargetPlatform } from '../../schema'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Amp uses AGENT.md for instructions, .amp/settings.json for hooks,
|
|
8
|
+
* MCP config, and skills/ for skills. No plugin manifest.
|
|
9
|
+
*/
|
|
10
|
+
export class AmpGenerator extends Generator {
|
|
11
|
+
readonly platform: TargetPlatform = 'amp'
|
|
12
|
+
|
|
13
|
+
async generate(): Promise<void> {
|
|
14
|
+
await Promise.all([
|
|
15
|
+
this.generateAgentMd(),
|
|
16
|
+
this.generateSettings(),
|
|
17
|
+
this.generateMcpConfig('mcp.json'),
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
this.copySkills()
|
|
21
|
+
this.copyScripts()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async generateAgentMd(): Promise<void> {
|
|
25
|
+
if (!this.config.instructions) return
|
|
26
|
+
const srcPath = this.resolveConfigPath(this.config.instructions, 'instructions')
|
|
27
|
+
if (!existsSync(srcPath)) return
|
|
28
|
+
|
|
29
|
+
const content = await Bun.file(srcPath).text()
|
|
30
|
+
|
|
31
|
+
const agentMd = [
|
|
32
|
+
`# ${this.config.brand?.displayName ?? this.config.name}`,
|
|
33
|
+
'',
|
|
34
|
+
this.config.brand?.shortDescription ?? this.config.description,
|
|
35
|
+
'',
|
|
36
|
+
content,
|
|
37
|
+
].join('\n')
|
|
38
|
+
|
|
39
|
+
await this.writeFile('AGENT.md', agentMd)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async generateSettings(): Promise<void> {
|
|
43
|
+
if (!this.config.hooks) return
|
|
44
|
+
|
|
45
|
+
// Amp hooks use a post-execute format
|
|
46
|
+
const hooks: Record<string, unknown[]> = {}
|
|
47
|
+
|
|
48
|
+
for (const [event, entries] of Object.entries(this.config.hooks)) {
|
|
49
|
+
if (!entries) continue
|
|
50
|
+
warnDroppedHookFields(this.platform, event, entries)
|
|
51
|
+
const commandEntries = entries.filter(entry => entry.type !== 'prompt' && entry.command)
|
|
52
|
+
if (commandEntries.length === 0) continue
|
|
53
|
+
hooks[event] = commandEntries.map(entry => ({
|
|
54
|
+
type: 'post-execute',
|
|
55
|
+
command: entry.command!.replace('${PLUGIN_ROOT}', '.'),
|
|
56
|
+
...(entry.timeout ? { timeout: entry.timeout } : {}),
|
|
57
|
+
}))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await this.writeJson('.amp/settings.json', { hooks })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { resolve, join, relative } from 'path'
|
|
2
|
+
import { mkdirSync, existsSync, cpSync } from 'fs'
|
|
3
|
+
import type { PluginConfig, TargetPlatform, McpServer } from '../schema'
|
|
4
|
+
|
|
5
|
+
type McpRemoteServer = Exclude<McpServer, { transport: 'stdio' }>
|
|
6
|
+
|
|
7
|
+
interface McpConfigOptions {
|
|
8
|
+
includeDefaultAuthHeaders?: boolean
|
|
9
|
+
transformRemoteEntry?: (context: {
|
|
10
|
+
name: string
|
|
11
|
+
server: McpRemoteServer
|
|
12
|
+
entry: Record<string, unknown>
|
|
13
|
+
}) => Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export abstract class Generator {
|
|
17
|
+
abstract readonly platform: TargetPlatform
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
protected config: PluginConfig,
|
|
21
|
+
protected rootDir: string,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
/** The output directory for this platform */
|
|
25
|
+
get outDir(): string {
|
|
26
|
+
return resolve(this.rootDir, this.config.outDir, this.platform)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Generate all platform-specific files */
|
|
30
|
+
abstract generate(): Promise<void>
|
|
31
|
+
|
|
32
|
+
/** Write a file to the output directory */
|
|
33
|
+
protected async writeFile(relativePath: string, content: string): Promise<void> {
|
|
34
|
+
const filepath = join(this.outDir, relativePath)
|
|
35
|
+
const dir = filepath.substring(0, filepath.lastIndexOf('/'))
|
|
36
|
+
mkdirSync(dir, { recursive: true })
|
|
37
|
+
await Bun.write(filepath, content)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Write JSON to the output directory */
|
|
41
|
+
protected async writeJson(relativePath: string, data: unknown): Promise<void> {
|
|
42
|
+
await this.writeFile(relativePath, JSON.stringify(data, null, 2) + '\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Copy a directory from source to output */
|
|
46
|
+
protected copyDir(srcRelative: string, destRelative: string, configKey: string): void {
|
|
47
|
+
const src = this.resolveConfigPath(srcRelative, configKey)
|
|
48
|
+
if (!existsSync(src)) return
|
|
49
|
+
const dest = join(this.outDir, destRelative)
|
|
50
|
+
mkdirSync(dest, { recursive: true })
|
|
51
|
+
cpSync(src, dest, { recursive: true })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resolve a user-configured path and ensure it stays within the project root. */
|
|
55
|
+
protected resolveConfigPath(configPath: string, configKey: string): string {
|
|
56
|
+
const resolvedPath = resolve(this.rootDir, configPath)
|
|
57
|
+
const rel = relative(this.rootDir, resolvedPath)
|
|
58
|
+
if (rel.startsWith('..')) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${configKey} path "${configPath}" resolves outside the project root.`
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return resolvedPath
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Copy skills directory, applying any platform-specific frontmatter */
|
|
67
|
+
protected copySkills(): void {
|
|
68
|
+
this.copyDir(this.config.skills, 'skills/', 'skills')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Copy commands directory if it exists */
|
|
72
|
+
protected copyCommands(): void {
|
|
73
|
+
if (this.config.commands) {
|
|
74
|
+
this.copyDir(this.config.commands, 'commands/', 'commands')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Copy agents directory if it exists */
|
|
79
|
+
protected copyAgents(): void {
|
|
80
|
+
if (this.config.agents) {
|
|
81
|
+
this.copyDir(this.config.agents, 'agents/', 'agents')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Copy scripts directory if it exists */
|
|
86
|
+
protected copyScripts(): void {
|
|
87
|
+
if (this.config.scripts) {
|
|
88
|
+
this.copyDir(this.config.scripts, 'scripts/', 'scripts')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Copy assets directory if it exists */
|
|
93
|
+
protected copyAssets(): void {
|
|
94
|
+
if (this.config.assets) {
|
|
95
|
+
this.copyDir(this.config.assets, 'assets/', 'assets')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Build canonical MCP server configs for target-specific output shaping. */
|
|
100
|
+
protected buildMcpServers(options: McpConfigOptions = {}): Record<string, unknown> | undefined {
|
|
101
|
+
if (!this.config.mcp) return undefined
|
|
102
|
+
|
|
103
|
+
const {
|
|
104
|
+
includeDefaultAuthHeaders = true,
|
|
105
|
+
transformRemoteEntry,
|
|
106
|
+
} = options
|
|
107
|
+
|
|
108
|
+
const mcpServers: Record<string, unknown> = {}
|
|
109
|
+
|
|
110
|
+
for (const [name, server] of Object.entries(this.config.mcp)) {
|
|
111
|
+
if (server.transport === 'stdio') {
|
|
112
|
+
mcpServers[name] = {
|
|
113
|
+
command: server.command,
|
|
114
|
+
args: server.args ?? [],
|
|
115
|
+
env: server.env ?? {},
|
|
116
|
+
}
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const remoteServer: McpRemoteServer = server
|
|
121
|
+
|
|
122
|
+
let entry: Record<string, unknown> = {
|
|
123
|
+
url: remoteServer.url,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (includeDefaultAuthHeaders) {
|
|
127
|
+
const headers = this.getMcpAuthHeaders(remoteServer)
|
|
128
|
+
if (headers) {
|
|
129
|
+
entry.headers = headers
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (transformRemoteEntry) {
|
|
134
|
+
entry = transformRemoteEntry({ name, server: remoteServer, entry })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
mcpServers[name] = entry
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return mcpServers
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected getMcpAuthMode(): 'inline' | 'platform' {
|
|
144
|
+
if (this.platform === 'claude-code') {
|
|
145
|
+
return this.config.platforms?.['claude-code']?.mcpAuth ?? 'inline'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this.platform === 'cursor') {
|
|
149
|
+
return this.config.platforms?.cursor?.mcpAuth ?? 'inline'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return 'inline'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Generate MCP config JSON in the common `{ mcpServers }` shape. */
|
|
156
|
+
protected async generateMcpConfig(relativePath: string, options: McpConfigOptions = {}): Promise<void> {
|
|
157
|
+
const mcpServers = this.buildMcpServers(options)
|
|
158
|
+
if (!mcpServers) return
|
|
159
|
+
await this.writeJson(relativePath, { mcpServers })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private getMcpAuthHeaders(server: McpRemoteServer): Record<string, string> | undefined {
|
|
163
|
+
if (this.getMcpAuthMode() === 'platform' || server.auth?.type === 'platform') {
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (server.auth?.type === 'bearer' && server.auth.envVar) {
|
|
168
|
+
return {
|
|
169
|
+
Authorization: `Bearer ${this.getEnvVarReference(server.auth.envVar)}`,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (server.auth?.type === 'header' && server.auth.envVar) {
|
|
174
|
+
return {
|
|
175
|
+
[server.auth.headerName]: server.auth.headerTemplate.replace(
|
|
176
|
+
'${value}',
|
|
177
|
+
this.getEnvVarReference(server.auth.envVar),
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private getEnvVarReference(envVar: string): string {
|
|
186
|
+
return `\${${envVar}}`
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Generator } from '../base'
|
|
2
|
+
import { generateClaudeFamilyOutputs } from '../shared/claude-family'
|
|
3
|
+
import type { TargetPlatform } from '../../schema'
|
|
4
|
+
|
|
5
|
+
export class ClaudeCodeGenerator extends Generator {
|
|
6
|
+
readonly platform: TargetPlatform = 'claude-code'
|
|
7
|
+
|
|
8
|
+
async generate(): Promise<void> {
|
|
9
|
+
await generateClaudeFamilyOutputs({
|
|
10
|
+
config: this.config,
|
|
11
|
+
rootDir: this.rootDir,
|
|
12
|
+
platform: this.platform,
|
|
13
|
+
options: {
|
|
14
|
+
manifestPath: '.claude-plugin/plugin.json',
|
|
15
|
+
instructionsFile: 'CLAUDE.md',
|
|
16
|
+
pluginRootVar: 'CLAUDE_PLUGIN_ROOT',
|
|
17
|
+
includeStandardHooksManifest: false,
|
|
18
|
+
},
|
|
19
|
+
writeJson: (relativePath, data) => this.writeJson(relativePath, data),
|
|
20
|
+
writeFile: (relativePath, content) => this.writeFile(relativePath, content),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
this.copySkills()
|
|
24
|
+
this.copyCommands()
|
|
25
|
+
this.copyAgents()
|
|
26
|
+
this.copyScripts()
|
|
27
|
+
this.copyAssets()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { Generator } from '../base'
|
|
3
|
+
import type { TargetPlatform } from '../../schema'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cline uses .clinerules for instructions, .cline/mcp.json for MCP config,
|
|
7
|
+
* and .cline/skills/ for skills. No plugin manifest.
|
|
8
|
+
*/
|
|
9
|
+
export class ClineGenerator extends Generator {
|
|
10
|
+
readonly platform: TargetPlatform = 'cline'
|
|
11
|
+
|
|
12
|
+
async generate(): Promise<void> {
|
|
13
|
+
await Promise.all([
|
|
14
|
+
this.generateMcpConfig('.cline/mcp.json'),
|
|
15
|
+
this.generateRules(),
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
this.copyClineSkills()
|
|
19
|
+
this.copyScripts()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async generateRules(): Promise<void> {
|
|
23
|
+
if (!this.config.instructions) return
|
|
24
|
+
const srcPath = this.resolveConfigPath(this.config.instructions, 'instructions')
|
|
25
|
+
if (!existsSync(srcPath)) return
|
|
26
|
+
|
|
27
|
+
const content = await Bun.file(srcPath).text()
|
|
28
|
+
await this.writeFile('.clinerules', content)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Copy skills into .cline/skills/ instead of the default skills/ */
|
|
32
|
+
private copyClineSkills(): void {
|
|
33
|
+
this.copyDir(this.config.skills, '.cline/skills/', 'skills')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { Generator } from '../base'
|
|
3
|
+
import type { TargetPlatform } from '../../schema'
|
|
4
|
+
|
|
5
|
+
export class CodexGenerator extends Generator {
|
|
6
|
+
readonly platform: TargetPlatform = 'codex'
|
|
7
|
+
|
|
8
|
+
async generate(): Promise<void> {
|
|
9
|
+
await Promise.all([
|
|
10
|
+
this.generateManifest(),
|
|
11
|
+
this.generateMcpConfig('.mcp.json', {
|
|
12
|
+
includeDefaultAuthHeaders: false,
|
|
13
|
+
transformRemoteEntry: ({ name, server }) => {
|
|
14
|
+
const entry: Record<string, unknown> = {
|
|
15
|
+
url: server.url,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (server.auth?.type === 'bearer' && server.auth.envVar) {
|
|
19
|
+
entry.bearer_token_env_var = server.auth.envVar
|
|
20
|
+
} else if (server.auth?.type === 'header' && server.auth.envVar) {
|
|
21
|
+
const isBearerAuthorizationHeader =
|
|
22
|
+
server.auth.headerName === 'Authorization'
|
|
23
|
+
&& server.auth.headerTemplate === 'Bearer ${value}'
|
|
24
|
+
|
|
25
|
+
if (isBearerAuthorizationHeader) {
|
|
26
|
+
entry.bearer_token_env_var = server.auth.envVar
|
|
27
|
+
} else if (server.auth.headerTemplate === '${value}') {
|
|
28
|
+
entry.env_http_headers = {
|
|
29
|
+
[server.auth.headerName]: server.auth.envVar,
|
|
30
|
+
}
|
|
31
|
+
} else if (!server.auth.headerTemplate.includes('${value}')) {
|
|
32
|
+
entry.http_headers = {
|
|
33
|
+
[server.auth.headerName]: server.auth.headerTemplate,
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
console.warn(
|
|
37
|
+
`[pluxx] codex generator: MCP server "${name}" uses auth.type "header" with a templated header Codex cannot express exactly. `
|
|
38
|
+
+ 'Supported Codex auth outputs are bearer_token_env_var, env_http_headers, and http_headers; this header was omitted.'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return entry
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
this.generateAgentsMd(),
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
this.copySkills()
|
|
50
|
+
this.copyAgents()
|
|
51
|
+
this.copyScripts()
|
|
52
|
+
this.copyAssets()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async generateManifest(): Promise<void> {
|
|
56
|
+
const manifest: Record<string, unknown> = {
|
|
57
|
+
name: this.config.name,
|
|
58
|
+
version: this.config.version,
|
|
59
|
+
description: this.config.description,
|
|
60
|
+
author: this.config.author,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.config.repository) manifest.homepage = this.config.repository
|
|
64
|
+
if (this.config.repository) manifest.repository = this.config.repository
|
|
65
|
+
if (this.config.license) manifest.license = this.config.license
|
|
66
|
+
if (this.config.keywords) manifest.keywords = this.config.keywords
|
|
67
|
+
|
|
68
|
+
manifest.skills = './skills/'
|
|
69
|
+
if (this.config.mcp) manifest.mcpServers = './.mcp.json'
|
|
70
|
+
|
|
71
|
+
// Codex supports rich interface metadata
|
|
72
|
+
if (this.config.brand) {
|
|
73
|
+
const iface: Record<string, unknown> = {
|
|
74
|
+
displayName: this.config.brand.displayName,
|
|
75
|
+
shortDescription: this.config.brand.shortDescription ?? this.config.description,
|
|
76
|
+
category: this.config.brand.category,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.config.brand.longDescription) {
|
|
80
|
+
iface.longDescription = this.config.brand.longDescription
|
|
81
|
+
}
|
|
82
|
+
if (this.config.brand.color) {
|
|
83
|
+
iface.brandColor = this.config.brand.color
|
|
84
|
+
}
|
|
85
|
+
if (this.config.brand.icon) {
|
|
86
|
+
iface.composerIcon = this.config.brand.icon
|
|
87
|
+
iface.logo = this.config.brand.icon
|
|
88
|
+
}
|
|
89
|
+
if (this.config.brand.defaultPrompts) {
|
|
90
|
+
iface.defaultPrompt = this.config.brand.defaultPrompts
|
|
91
|
+
}
|
|
92
|
+
if (this.config.brand.websiteURL) {
|
|
93
|
+
iface.websiteURL = this.config.brand.websiteURL
|
|
94
|
+
}
|
|
95
|
+
if (this.config.brand.screenshots) {
|
|
96
|
+
iface.screenshots = this.config.brand.screenshots
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Merge Codex-specific interface overrides
|
|
100
|
+
const codexOverrides = this.config.platforms?.codex?.interface
|
|
101
|
+
if (codexOverrides) {
|
|
102
|
+
Object.assign(iface, codexOverrides)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
iface.developerName = this.config.author.name
|
|
106
|
+
|
|
107
|
+
manifest.interface = iface
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await this.writeJson('.codex-plugin/plugin.json', manifest)
|
|
111
|
+
}
|
|
112
|
+
private async generateAgentsMd(): Promise<void> {
|
|
113
|
+
if (!this.config.instructions) return
|
|
114
|
+
const srcPath = this.resolveConfigPath(this.config.instructions, 'instructions')
|
|
115
|
+
if (!existsSync(srcPath)) return
|
|
116
|
+
|
|
117
|
+
const content = await Bun.file(srcPath).text()
|
|
118
|
+
await this.writeFile('AGENTS.md', content)
|
|
119
|
+
}
|
|
120
|
+
}
|