@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,158 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { Generator } from '../base'
|
|
3
|
+
import type { TargetPlatform } from '../../schema'
|
|
4
|
+
import { buildGeneratedPermissionHookScript } from '../../permissions'
|
|
5
|
+
|
|
6
|
+
export class CursorGenerator extends Generator {
|
|
7
|
+
readonly platform: TargetPlatform = 'cursor'
|
|
8
|
+
|
|
9
|
+
async generate(): Promise<void> {
|
|
10
|
+
await Promise.all([
|
|
11
|
+
this.generateManifest(),
|
|
12
|
+
this.generateMcpConfig('mcp.json'),
|
|
13
|
+
this.generateHooks(),
|
|
14
|
+
this.generateRules(),
|
|
15
|
+
this.generateAgentsMd(),
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
this.copySkills()
|
|
19
|
+
this.copyCommands()
|
|
20
|
+
this.copyAgents()
|
|
21
|
+
this.copyScripts()
|
|
22
|
+
this.copyAssets()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async generateManifest(): Promise<void> {
|
|
26
|
+
const manifest: Record<string, unknown> = {
|
|
27
|
+
name: this.config.name,
|
|
28
|
+
description: this.config.description,
|
|
29
|
+
version: this.config.version,
|
|
30
|
+
author: this.config.author,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (this.config.repository) manifest.repository = this.config.repository
|
|
34
|
+
if (this.config.license) manifest.license = this.config.license
|
|
35
|
+
if (this.config.keywords) manifest.keywords = this.config.keywords
|
|
36
|
+
if (this.config.brand?.websiteURL) manifest.homepage = this.config.brand.websiteURL
|
|
37
|
+
if (this.config.brand?.icon) manifest.logo = this.config.brand.icon
|
|
38
|
+
|
|
39
|
+
manifest.skills = './skills/'
|
|
40
|
+
if (this.config.commands) manifest.commands = './commands/'
|
|
41
|
+
if (this.config.agents) manifest.agents = './agents/'
|
|
42
|
+
if (this.config.platforms?.cursor?.rules?.length) manifest.rules = './rules/'
|
|
43
|
+
if (this.config.hooks || this.config.permissions) manifest.hooks = './hooks/hooks.json'
|
|
44
|
+
if (this.config.mcp) manifest.mcpServers = './mcp.json'
|
|
45
|
+
|
|
46
|
+
await this.writeJson('.cursor-plugin/plugin.json', manifest)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async generateHooks(): Promise<void> {
|
|
50
|
+
const permissionScript = buildGeneratedPermissionHookScript(this.config.permissions)
|
|
51
|
+
if (!this.config.hooks && !permissionScript) return
|
|
52
|
+
const usesPlatformManagedAuth = this.config.platforms?.cursor?.mcpAuth === 'platform'
|
|
53
|
+
|
|
54
|
+
// Cursor hooks format matches the canonical format closely
|
|
55
|
+
const hooks: Record<string, unknown[]> = {}
|
|
56
|
+
|
|
57
|
+
if (permissionScript) {
|
|
58
|
+
await this.writeFile('hooks/pluxx-permissions.mjs', permissionScript)
|
|
59
|
+
hooks.preToolUse = [{
|
|
60
|
+
command: 'node ./hooks/pluxx-permissions.mjs cursor-pretool',
|
|
61
|
+
}]
|
|
62
|
+
hooks.beforeShellExecution = [{
|
|
63
|
+
command: 'node ./hooks/pluxx-permissions.mjs cursor-shell',
|
|
64
|
+
}]
|
|
65
|
+
hooks.beforeReadFile = [{
|
|
66
|
+
command: 'node ./hooks/pluxx-permissions.mjs cursor-read',
|
|
67
|
+
}]
|
|
68
|
+
hooks.beforeMCPExecution = [{
|
|
69
|
+
command: 'node ./hooks/pluxx-permissions.mjs cursor-mcp',
|
|
70
|
+
}]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!this.config.hooks) {
|
|
74
|
+
await this.writeJson('hooks/hooks.json', { version: 1, hooks })
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const [event, entries] of Object.entries(this.config.hooks)) {
|
|
79
|
+
if (!entries) continue
|
|
80
|
+
const filteredEntries = entries.filter((entry) => {
|
|
81
|
+
if (
|
|
82
|
+
usesPlatformManagedAuth
|
|
83
|
+
&& entry.type !== 'prompt'
|
|
84
|
+
&& entry.command?.includes('check-env.sh')
|
|
85
|
+
) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
return true
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (filteredEntries.length === 0) continue
|
|
92
|
+
|
|
93
|
+
hooks[event] = [
|
|
94
|
+
...(hooks[event] ?? []),
|
|
95
|
+
...filteredEntries.map(entry => {
|
|
96
|
+
const hookDef: Record<string, unknown> = {}
|
|
97
|
+
if (entry.type === 'prompt') {
|
|
98
|
+
hookDef.type = 'prompt'
|
|
99
|
+
hookDef.prompt = entry.prompt
|
|
100
|
+
if (entry.model) hookDef.model = entry.model
|
|
101
|
+
} else if (entry.command) {
|
|
102
|
+
hookDef.command = entry.command.replace('${PLUGIN_ROOT}', '.')
|
|
103
|
+
}
|
|
104
|
+
if (entry.timeout) hookDef.timeout = entry.timeout
|
|
105
|
+
if (entry.matcher) hookDef.matcher = entry.matcher
|
|
106
|
+
if (entry.failClosed) hookDef.failClosed = entry.failClosed
|
|
107
|
+
if (entry.loop_limit !== undefined) hookDef.loop_limit = entry.loop_limit
|
|
108
|
+
return hookDef
|
|
109
|
+
}),
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await this.writeJson('hooks/hooks.json', { version: 1, hooks })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async generateRules(): Promise<void> {
|
|
117
|
+
const overrides = this.config.platforms?.cursor
|
|
118
|
+
if (!overrides?.rules?.length) return
|
|
119
|
+
|
|
120
|
+
for (const rule of overrides.rules) {
|
|
121
|
+
const frontmatter = [
|
|
122
|
+
'---',
|
|
123
|
+
`description: "${rule.description}"`,
|
|
124
|
+
]
|
|
125
|
+
if (rule.globs) {
|
|
126
|
+
if (Array.isArray(rule.globs)) {
|
|
127
|
+
frontmatter.push(`globs: ${JSON.stringify(rule.globs)}`)
|
|
128
|
+
} else {
|
|
129
|
+
frontmatter.push(`globs: "${rule.globs}"`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (rule.alwaysApply !== undefined) {
|
|
133
|
+
frontmatter.push(`alwaysApply: ${rule.alwaysApply}`)
|
|
134
|
+
}
|
|
135
|
+
frontmatter.push('---')
|
|
136
|
+
|
|
137
|
+
const content = rule.content ?? ''
|
|
138
|
+
const filename = rule.description
|
|
139
|
+
.toLowerCase()
|
|
140
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
141
|
+
.replace(/^-|-$/g, '')
|
|
142
|
+
|
|
143
|
+
await this.writeFile(
|
|
144
|
+
`rules/${filename}.mdc`,
|
|
145
|
+
frontmatter.join('\n') + '\n\n' + content
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async generateAgentsMd(): Promise<void> {
|
|
151
|
+
if (!this.config.instructions) return
|
|
152
|
+
const srcPath = this.resolveConfigPath(this.config.instructions, 'instructions')
|
|
153
|
+
if (!existsSync(srcPath)) return
|
|
154
|
+
|
|
155
|
+
const content = await Bun.file(srcPath).text()
|
|
156
|
+
await this.writeFile('AGENTS.md', content)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
* Gemini CLI uses gemini-extension.json for the plugin manifest,
|
|
8
|
+
* GEMINI.md for instructions, and mcpServers in the same format as Claude Code.
|
|
9
|
+
*/
|
|
10
|
+
export class GeminiCliGenerator extends Generator {
|
|
11
|
+
readonly platform: TargetPlatform = 'gemini-cli'
|
|
12
|
+
|
|
13
|
+
async generate(): Promise<void> {
|
|
14
|
+
await Promise.all([
|
|
15
|
+
this.generateManifest(),
|
|
16
|
+
this.generateInstructions(),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
this.copySkills()
|
|
20
|
+
this.copyScripts()
|
|
21
|
+
this.copyAssets()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async generateManifest(): Promise<void> {
|
|
25
|
+
const manifest: Record<string, unknown> = {
|
|
26
|
+
name: this.config.name,
|
|
27
|
+
version: this.config.version,
|
|
28
|
+
description: this.config.description,
|
|
29
|
+
author: this.config.author,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// MCP servers block
|
|
33
|
+
const mcpServers = this.buildMcpServers({
|
|
34
|
+
transformRemoteEntry: ({ server, entry }) => ({
|
|
35
|
+
type: server.transport === 'sse' ? 'sse' : 'http',
|
|
36
|
+
...entry,
|
|
37
|
+
}),
|
|
38
|
+
})
|
|
39
|
+
if (mcpServers) {
|
|
40
|
+
manifest.mcpServers = mcpServers
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Skills paths
|
|
44
|
+
manifest.skills = ['./skills/']
|
|
45
|
+
|
|
46
|
+
// Hooks block
|
|
47
|
+
if (this.config.hooks) {
|
|
48
|
+
const hooks: Record<string, unknown[]> = {}
|
|
49
|
+
|
|
50
|
+
for (const [event, entries] of Object.entries(this.config.hooks)) {
|
|
51
|
+
if (!entries) continue
|
|
52
|
+
warnDroppedHookFields(this.platform, event, entries)
|
|
53
|
+
const commandEntries = entries.filter(entry => entry.type !== 'prompt' && entry.command)
|
|
54
|
+
if (commandEntries.length === 0) continue
|
|
55
|
+
hooks[event] = commandEntries.map(entry => ({
|
|
56
|
+
command: entry.command!.replace('${PLUGIN_ROOT}', '.'),
|
|
57
|
+
}))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
manifest.hooks = hooks
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await this.writeJson('gemini-extension.json', manifest)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async generateInstructions(): Promise<void> {
|
|
67
|
+
if (!this.config.instructions) return
|
|
68
|
+
const srcPath = this.resolveConfigPath(this.config.instructions, 'instructions')
|
|
69
|
+
if (!existsSync(srcPath)) return
|
|
70
|
+
|
|
71
|
+
const content = await Bun.file(srcPath).text()
|
|
72
|
+
|
|
73
|
+
const geminiMd = [
|
|
74
|
+
`# ${this.config.brand?.displayName ?? this.config.name}`,
|
|
75
|
+
'',
|
|
76
|
+
this.config.brand?.shortDescription ?? this.config.description,
|
|
77
|
+
'',
|
|
78
|
+
content,
|
|
79
|
+
].join('\n')
|
|
80
|
+
|
|
81
|
+
await this.writeFile('GEMINI.md', geminiMd)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Generator } from '../base'
|
|
2
|
+
import { generateClaudeFamilyOutputs } from '../shared/claude-family'
|
|
3
|
+
import type { TargetPlatform } from '../../schema'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GitHub Copilot CLI uses the same plugin manifest format as Claude Code.
|
|
7
|
+
* Discovery dirs: .github/skills/, .claude/skills/, .agents/skills/
|
|
8
|
+
*/
|
|
9
|
+
export class GitHubCopilotGenerator extends Generator {
|
|
10
|
+
readonly platform: TargetPlatform = 'github-copilot'
|
|
11
|
+
|
|
12
|
+
async generate(): Promise<void> {
|
|
13
|
+
await generateClaudeFamilyOutputs({
|
|
14
|
+
config: this.config,
|
|
15
|
+
rootDir: this.rootDir,
|
|
16
|
+
platform: this.platform,
|
|
17
|
+
options: {
|
|
18
|
+
manifestPath: '.claude-plugin/plugin.json',
|
|
19
|
+
instructionsFile: 'CLAUDE.md',
|
|
20
|
+
pluginRootVar: 'CLAUDE_PLUGIN_ROOT',
|
|
21
|
+
},
|
|
22
|
+
writeJson: (relativePath, data) => this.writeJson(relativePath, data),
|
|
23
|
+
writeFile: (relativePath, content) => this.writeFile(relativePath, content),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
this.copySkills()
|
|
27
|
+
this.copyCommands()
|
|
28
|
+
this.copyAgents()
|
|
29
|
+
this.copyScripts()
|
|
30
|
+
this.copyAssets()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { HookEntry, TargetPlatform } from '../schema'
|
|
2
|
+
|
|
3
|
+
const MATCHER_PASSTHROUGH_PLATFORMS = new Set<TargetPlatform>([
|
|
4
|
+
'claude-code',
|
|
5
|
+
'cursor',
|
|
6
|
+
'github-copilot',
|
|
7
|
+
'openhands',
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
const FAIL_CLOSED_PASSTHROUGH_PLATFORMS = new Set<TargetPlatform>([
|
|
11
|
+
'cursor',
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
const LOOP_LIMIT_PASSTHROUGH_PLATFORMS = new Set<TargetPlatform>([
|
|
15
|
+
'cursor',
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
export function warnDroppedHookFields(
|
|
19
|
+
platform: TargetPlatform,
|
|
20
|
+
event: string,
|
|
21
|
+
entries: HookEntry[],
|
|
22
|
+
): void {
|
|
23
|
+
const hasPromptHooks = entries.some(entry => entry.type === 'prompt')
|
|
24
|
+
const hasMatcher = entries.some(entry => entry.matcher !== undefined)
|
|
25
|
+
const hasFailClosed = entries.some(entry => entry.failClosed !== undefined)
|
|
26
|
+
const hasLoopLimit = entries.some(entry => entry.loop_limit !== undefined)
|
|
27
|
+
|
|
28
|
+
if (hasPromptHooks) {
|
|
29
|
+
console.warn(
|
|
30
|
+
`[pluxx] ${platform} generator dropped unsupported prompt-based hook for event "${event}".`
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (hasMatcher && !MATCHER_PASSTHROUGH_PLATFORMS.has(platform)) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[pluxx] ${platform} generator dropped unsupported hook field "matcher" for event "${event}".`
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (hasFailClosed && !FAIL_CLOSED_PASSTHROUGH_PLATFORMS.has(platform)) {
|
|
41
|
+
console.warn(
|
|
42
|
+
`[pluxx] ${platform} generator dropped unsupported hook field "failClosed" for event "${event}".`
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (hasLoopLimit && !LOOP_LIMIT_PASSTHROUGH_PLATFORMS.has(platform)) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`[pluxx] ${platform} generator dropped unsupported hook field "loop_limit" for event "${event}".`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { rmSync, mkdirSync } from 'fs'
|
|
2
|
+
import { resolve, relative } from 'path'
|
|
3
|
+
import type { PluginConfig, TargetPlatform } from '../schema'
|
|
4
|
+
import { Generator } from './base'
|
|
5
|
+
import { ClaudeCodeGenerator } from './claude-code'
|
|
6
|
+
import { CursorGenerator } from './cursor'
|
|
7
|
+
import { CodexGenerator } from './codex'
|
|
8
|
+
import { OpenCodeGenerator } from './opencode'
|
|
9
|
+
import { GitHubCopilotGenerator } from './github-copilot'
|
|
10
|
+
import { OpenHandsGenerator } from './openhands'
|
|
11
|
+
import { WarpGenerator } from './warp'
|
|
12
|
+
import { GeminiCliGenerator } from './gemini-cli'
|
|
13
|
+
import { RooCodeGenerator } from './roo-code'
|
|
14
|
+
import { ClineGenerator } from './cline'
|
|
15
|
+
import { AmpGenerator } from './amp'
|
|
16
|
+
|
|
17
|
+
const GENERATORS: Record<TargetPlatform, new (config: PluginConfig, rootDir: string) => Generator> = {
|
|
18
|
+
'claude-code': ClaudeCodeGenerator,
|
|
19
|
+
cursor: CursorGenerator,
|
|
20
|
+
codex: CodexGenerator,
|
|
21
|
+
opencode: OpenCodeGenerator,
|
|
22
|
+
'github-copilot': GitHubCopilotGenerator,
|
|
23
|
+
openhands: OpenHandsGenerator,
|
|
24
|
+
warp: WarpGenerator,
|
|
25
|
+
'gemini-cli': GeminiCliGenerator,
|
|
26
|
+
'roo-code': RooCodeGenerator,
|
|
27
|
+
cline: ClineGenerator,
|
|
28
|
+
amp: AmpGenerator,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BuildOptions {
|
|
32
|
+
/** Override targets from config */
|
|
33
|
+
targets?: TargetPlatform[]
|
|
34
|
+
/** Clean output directory before building */
|
|
35
|
+
clean?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function build(
|
|
39
|
+
config: PluginConfig,
|
|
40
|
+
rootDir: string,
|
|
41
|
+
options: BuildOptions = {},
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const targets = options.targets ?? config.targets
|
|
44
|
+
const outDir = resolve(rootDir, config.outDir)
|
|
45
|
+
|
|
46
|
+
// CRITICAL: Guard against path traversal — outDir must stay within rootDir
|
|
47
|
+
const rel = relative(rootDir, outDir)
|
|
48
|
+
if (rel.startsWith('..') || resolve(outDir) === resolve(rootDir)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`outDir "${config.outDir}" resolves outside the project root. Refusing to delete.`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (options.clean !== false) {
|
|
55
|
+
rmSync(outDir, { recursive: true, force: true })
|
|
56
|
+
}
|
|
57
|
+
mkdirSync(outDir, { recursive: true })
|
|
58
|
+
|
|
59
|
+
const generators = targets.map(target => {
|
|
60
|
+
const GeneratorClass = GENERATORS[target]
|
|
61
|
+
if (!GeneratorClass) {
|
|
62
|
+
throw new Error(`Unknown target platform: ${target}`)
|
|
63
|
+
}
|
|
64
|
+
return new GeneratorClass(config, rootDir)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Build all targets in parallel
|
|
68
|
+
await Promise.all(generators.map(g => g.generate()))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { Generator }
|