@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.
- package/README.md +25 -8
- package/bin/pluxx.js +19 -28
- package/dist/agents.d.ts +16 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/cli/agent.d.ts +62 -0
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.d.ts.map +1 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21810 -0
- package/dist/cli/init-from-mcp.d.ts +17 -1
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts +3 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.d.ts.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/primitive-summary.d.ts +14 -0
- package/dist/cli/primitive-summary.d.ts.map +1 -0
- package/dist/cli/prompt.d.ts +1 -1
- package/dist/cli/publish.d.ts +6 -1
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/cli/sync-from-mcp.d.ts.map +1 -1
- package/dist/cli/verify-install.d.ts +25 -0
- package/dist/cli/verify-install.d.ts.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/compiler-intent.d.ts +165 -0
- package/dist/compiler-intent.d.ts.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/delegation.d.ts +11 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/generators/amp/index.d.ts.map +1 -1
- package/dist/generators/base.d.ts +5 -0
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/claude-code/index.d.ts.map +1 -1
- package/dist/generators/cline/index.d.ts.map +1 -1
- package/dist/generators/codex/index.d.ts +4 -0
- package/dist/generators/codex/index.d.ts.map +1 -1
- package/dist/generators/cursor/index.d.ts +1 -0
- package/dist/generators/cursor/index.d.ts.map +1 -1
- package/dist/generators/gemini-cli/index.d.ts.map +1 -1
- package/dist/generators/github-copilot/index.d.ts.map +1 -1
- package/dist/generators/opencode/index.d.ts +1 -0
- package/dist/generators/opencode/index.d.ts.map +1 -1
- package/dist/generators/openhands/index.d.ts.map +1 -1
- package/dist/generators/roo-code/index.d.ts.map +1 -1
- package/dist/generators/shared/claude-family.d.ts.map +1 -1
- package/dist/generators/warp/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5371 -553
- package/dist/schema.d.ts +91 -42
- package/dist/schema.d.ts.map +1 -1
- package/dist/text-files.d.ts +5 -0
- package/dist/text-files.d.ts.map +1 -0
- package/dist/validation/platform-rules.d.ts +15 -1
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +15 -13
- package/src/cli/agent.ts +0 -1455
- package/src/cli/dev.ts +0 -112
- package/src/cli/doctor.ts +0 -987
- package/src/cli/eval.ts +0 -470
- package/src/cli/index.ts +0 -2933
- package/src/cli/init-from-mcp.ts +0 -2115
- package/src/cli/install.ts +0 -860
- package/src/cli/lint.ts +0 -1249
- package/src/cli/mcp-proxy.ts +0 -322
- package/src/cli/migrate.ts +0 -867
- package/src/cli/prompt.ts +0 -82
- package/src/cli/publish.ts +0 -401
- package/src/cli/runtime.ts +0 -86
- package/src/cli/sync-from-mcp.ts +0 -586
- package/src/cli/test.ts +0 -142
- package/src/compatibility/matrix.ts +0 -149
- package/src/config/define.ts +0 -20
- package/src/config/load.ts +0 -74
- package/src/generators/amp/index.ts +0 -63
- package/src/generators/base.ts +0 -188
- package/src/generators/claude-code/index.ts +0 -172
- package/src/generators/cline/index.ts +0 -35
- package/src/generators/codex/index.ts +0 -143
- package/src/generators/cursor/index.ts +0 -158
- package/src/generators/gemini-cli/index.ts +0 -83
- package/src/generators/github-copilot/index.ts +0 -32
- package/src/generators/hooks-warning.ts +0 -51
- package/src/generators/index.ts +0 -71
- package/src/generators/opencode/index.ts +0 -526
- package/src/generators/openhands/index.ts +0 -32
- package/src/generators/roo-code/index.ts +0 -35
- package/src/generators/shared/claude-family.ts +0 -215
- package/src/generators/warp/index.ts +0 -32
- package/src/hook-events.ts +0 -33
- package/src/index.ts +0 -34
- package/src/mcp/introspect.ts +0 -1107
- package/src/permissions.ts +0 -260
- package/src/schema.ts +0 -312
- package/src/user-config.ts +0 -177
- package/src/validation/platform-rules.ts +0 -686
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { Generator } from '../base'
|
|
2
|
-
import { generateClaudeFamilyOutputs } from '../shared/claude-family'
|
|
3
|
-
import type { TargetPlatform } from '../../schema'
|
|
4
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
5
|
-
import { basename, join } from 'path'
|
|
6
|
-
|
|
7
|
-
export class ClaudeCodeGenerator extends Generator {
|
|
8
|
-
readonly platform: TargetPlatform = 'claude-code'
|
|
9
|
-
|
|
10
|
-
async generate(): Promise<void> {
|
|
11
|
-
await generateClaudeFamilyOutputs({
|
|
12
|
-
config: this.config,
|
|
13
|
-
rootDir: this.rootDir,
|
|
14
|
-
platform: this.platform,
|
|
15
|
-
options: {
|
|
16
|
-
manifestPath: '.claude-plugin/plugin.json',
|
|
17
|
-
instructionsFile: 'CLAUDE.md',
|
|
18
|
-
pluginRootVar: 'CLAUDE_PLUGIN_ROOT',
|
|
19
|
-
includeStandardHooksManifest: false,
|
|
20
|
-
},
|
|
21
|
-
writeJson: (relativePath, data) => this.writeJson(relativePath, data),
|
|
22
|
-
writeFile: (relativePath, content) => this.writeFile(relativePath, content),
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
this.copySkills()
|
|
26
|
-
this.copyCommands()
|
|
27
|
-
this.copyAgents()
|
|
28
|
-
this.copyScripts()
|
|
29
|
-
this.copyAssets()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
protected copySkills(): void {
|
|
33
|
-
super.copySkills()
|
|
34
|
-
|
|
35
|
-
const collidingSkills = this.collectCollidingSkills()
|
|
36
|
-
for (const skill of collidingSkills) {
|
|
37
|
-
const outputPath = join(this.outDir, 'skills', skill.dirName, 'SKILL.md')
|
|
38
|
-
if (!existsSync(outputPath)) continue
|
|
39
|
-
|
|
40
|
-
const current = readFileSync(outputPath, 'utf-8')
|
|
41
|
-
const hiddenName = buildHiddenSkillName(skill.effectiveName)
|
|
42
|
-
const rewritten = rewriteClaudeCollidingSkill(current, hiddenName)
|
|
43
|
-
if (rewritten !== current) {
|
|
44
|
-
writeFileSync(outputPath, rewritten, 'utf-8')
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private collectCollidingSkills(): Array<{ dirName: string; effectiveName: string }> {
|
|
50
|
-
if (!this.config.commands) return []
|
|
51
|
-
|
|
52
|
-
const commandsSrc = this.resolveConfigPath(this.config.commands, 'commands')
|
|
53
|
-
const skillsSrc = this.resolveConfigPath(this.config.skills, 'skills')
|
|
54
|
-
if (!existsSync(commandsSrc) || !existsSync(skillsSrc)) return []
|
|
55
|
-
|
|
56
|
-
const commandNames = collectTopLevelCommandNames(commandsSrc)
|
|
57
|
-
const collidingSkills: Array<{ dirName: string; effectiveName: string }> = []
|
|
58
|
-
|
|
59
|
-
for (const entry of readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
60
|
-
if (!entry.isDirectory()) continue
|
|
61
|
-
const skillFile = join(skillsSrc, entry.name, 'SKILL.md')
|
|
62
|
-
if (!existsSync(skillFile)) continue
|
|
63
|
-
|
|
64
|
-
const content = readFileSync(skillFile, 'utf-8')
|
|
65
|
-
const effectiveName = getEffectiveSkillName(content, entry.name)
|
|
66
|
-
if (commandNames.has(effectiveName)) {
|
|
67
|
-
collidingSkills.push({ dirName: entry.name, effectiveName })
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return collidingSkills
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function collectTopLevelCommandNames(commandsRoot: string): Set<string> {
|
|
76
|
-
const commandNames = new Set<string>()
|
|
77
|
-
for (const entry of readdirSync(commandsRoot, { withFileTypes: true })) {
|
|
78
|
-
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
|
|
79
|
-
commandNames.add(basename(entry.name, '.md'))
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return commandNames
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function getEffectiveSkillName(content: string, fallback: string): string {
|
|
86
|
-
const frontmatter = extractFrontmatterLines(content)
|
|
87
|
-
if (!frontmatter) return fallback
|
|
88
|
-
|
|
89
|
-
for (const line of frontmatter) {
|
|
90
|
-
const match = /^name:\s*(.+)\s*$/i.exec(line.trim())
|
|
91
|
-
if (match?.[1]) {
|
|
92
|
-
return stripYamlScalar(match[1]) || fallback
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return fallback
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function buildHiddenSkillName(name: string): string {
|
|
100
|
-
const maxBaseLength = 64 - '-skill'.length
|
|
101
|
-
const trimmed = name.length > maxBaseLength ? name.slice(0, maxBaseLength) : name
|
|
102
|
-
return `${trimmed}-skill`
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function rewriteClaudeCollidingSkill(content: string, hiddenName: string): string {
|
|
106
|
-
const frontmatter = extractFrontmatterLines(content)
|
|
107
|
-
if (!frontmatter) {
|
|
108
|
-
return [
|
|
109
|
-
'---',
|
|
110
|
-
`name: ${hiddenName}`,
|
|
111
|
-
'user-invocable: false',
|
|
112
|
-
'---',
|
|
113
|
-
'',
|
|
114
|
-
content.trimStart(),
|
|
115
|
-
].join('\n')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const rewritten = [...frontmatter]
|
|
119
|
-
let sawName = false
|
|
120
|
-
let sawUserInvocable = false
|
|
121
|
-
|
|
122
|
-
for (let index = 0; index < rewritten.length; index += 1) {
|
|
123
|
-
const trimmed = rewritten[index].trim()
|
|
124
|
-
if (/^name:\s*/i.test(trimmed)) {
|
|
125
|
-
rewritten[index] = `name: ${hiddenName}`
|
|
126
|
-
sawName = true
|
|
127
|
-
continue
|
|
128
|
-
}
|
|
129
|
-
if (/^user-invocable:\s*/i.test(trimmed)) {
|
|
130
|
-
rewritten[index] = 'user-invocable: false'
|
|
131
|
-
sawUserInvocable = true
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!sawName) rewritten.push(`name: ${hiddenName}`)
|
|
136
|
-
if (!sawUserInvocable) rewritten.push('user-invocable: false')
|
|
137
|
-
|
|
138
|
-
const lines = content.split('\n')
|
|
139
|
-
const endIndex = findFrontmatterEndIndex(lines)
|
|
140
|
-
const body = endIndex === -1 ? content : lines.slice(endIndex + 1).join('\n')
|
|
141
|
-
return ['---', ...rewritten, '---', body ? `\n${body.replace(/^\n/, '')}` : ''].join('\n')
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function extractFrontmatterLines(content: string): string[] | null {
|
|
145
|
-
const lines = content.split('\n')
|
|
146
|
-
const endIndex = findFrontmatterEndIndex(lines)
|
|
147
|
-
if (endIndex === -1) return null
|
|
148
|
-
return lines.slice(1, endIndex)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function findFrontmatterEndIndex(lines: string[]): number {
|
|
152
|
-
if (lines[0]?.trim() !== '---') return -1
|
|
153
|
-
|
|
154
|
-
for (let index = 1; index < lines.length; index += 1) {
|
|
155
|
-
if (lines[index].trim() === '---') {
|
|
156
|
-
return index
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return -1
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function stripYamlScalar(value: string): string {
|
|
164
|
-
const trimmed = value.trim()
|
|
165
|
-
if (
|
|
166
|
-
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
167
|
-
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
168
|
-
) {
|
|
169
|
-
return trimmed.slice(1, -1).trim()
|
|
170
|
-
}
|
|
171
|
-
return trimmed
|
|
172
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs'
|
|
2
|
-
import { Generator } from '../base'
|
|
3
|
-
import type { TargetPlatform } from '../../schema'
|
|
4
|
-
import { collectPermissionRules } from '../../permissions'
|
|
5
|
-
|
|
6
|
-
export class CodexGenerator extends Generator {
|
|
7
|
-
readonly platform: TargetPlatform = 'codex'
|
|
8
|
-
|
|
9
|
-
async generate(): Promise<void> {
|
|
10
|
-
await Promise.all([
|
|
11
|
-
this.generateManifest(),
|
|
12
|
-
this.generateMcpConfig('.mcp.json', {
|
|
13
|
-
includeDefaultAuthHeaders: false,
|
|
14
|
-
transformRemoteEntry: ({ name, server }) => {
|
|
15
|
-
const entry: Record<string, unknown> = {
|
|
16
|
-
url: server.url,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (server.auth?.type === 'bearer' && server.auth.envVar) {
|
|
20
|
-
entry.bearer_token_env_var = server.auth.envVar
|
|
21
|
-
} else if (server.auth?.type === 'header' && server.auth.envVar) {
|
|
22
|
-
const isBearerAuthorizationHeader =
|
|
23
|
-
server.auth.headerName === 'Authorization'
|
|
24
|
-
&& server.auth.headerTemplate === 'Bearer ${value}'
|
|
25
|
-
|
|
26
|
-
if (isBearerAuthorizationHeader) {
|
|
27
|
-
entry.bearer_token_env_var = server.auth.envVar
|
|
28
|
-
} else if (server.auth.headerTemplate === '${value}') {
|
|
29
|
-
entry.env_http_headers = {
|
|
30
|
-
[server.auth.headerName]: server.auth.envVar,
|
|
31
|
-
}
|
|
32
|
-
} else if (!server.auth.headerTemplate.includes('${value}')) {
|
|
33
|
-
entry.http_headers = {
|
|
34
|
-
[server.auth.headerName]: server.auth.headerTemplate,
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
console.warn(
|
|
38
|
-
`[pluxx] codex generator: MCP server "${name}" uses auth.type "header" with a templated header Codex cannot express exactly. `
|
|
39
|
-
+ 'Supported Codex auth outputs are bearer_token_env_var, env_http_headers, and http_headers; this header was omitted.'
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return entry
|
|
45
|
-
},
|
|
46
|
-
}),
|
|
47
|
-
this.generateAgentsMd(),
|
|
48
|
-
this.generatePermissionsCompanion(),
|
|
49
|
-
])
|
|
50
|
-
|
|
51
|
-
this.copySkills()
|
|
52
|
-
this.copyAgents()
|
|
53
|
-
this.copyScripts()
|
|
54
|
-
this.copyAssets()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private async generateManifest(): Promise<void> {
|
|
58
|
-
const manifest: Record<string, unknown> = {
|
|
59
|
-
name: this.config.name,
|
|
60
|
-
version: this.config.version,
|
|
61
|
-
description: this.config.description,
|
|
62
|
-
author: this.config.author,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (this.config.repository) manifest.homepage = this.config.repository
|
|
66
|
-
if (this.config.repository) manifest.repository = this.config.repository
|
|
67
|
-
if (this.config.license) manifest.license = this.config.license
|
|
68
|
-
if (this.config.keywords) manifest.keywords = this.config.keywords
|
|
69
|
-
|
|
70
|
-
manifest.skills = './skills/'
|
|
71
|
-
if (this.config.mcp) manifest.mcpServers = './.mcp.json'
|
|
72
|
-
|
|
73
|
-
// Codex supports rich interface metadata
|
|
74
|
-
if (this.config.brand) {
|
|
75
|
-
const iface: Record<string, unknown> = {
|
|
76
|
-
displayName: this.config.brand.displayName,
|
|
77
|
-
shortDescription: this.config.brand.shortDescription ?? this.config.description,
|
|
78
|
-
category: this.config.brand.category,
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (this.config.brand.longDescription) {
|
|
82
|
-
iface.longDescription = this.config.brand.longDescription
|
|
83
|
-
}
|
|
84
|
-
if (this.config.brand.color) {
|
|
85
|
-
iface.brandColor = this.config.brand.color
|
|
86
|
-
}
|
|
87
|
-
if (this.config.brand.icon) {
|
|
88
|
-
iface.composerIcon = this.config.brand.icon
|
|
89
|
-
iface.logo = this.config.brand.icon
|
|
90
|
-
}
|
|
91
|
-
if (this.config.brand.defaultPrompts) {
|
|
92
|
-
iface.defaultPrompt = this.config.brand.defaultPrompts
|
|
93
|
-
}
|
|
94
|
-
if (this.config.brand.websiteURL) {
|
|
95
|
-
iface.websiteURL = this.config.brand.websiteURL
|
|
96
|
-
}
|
|
97
|
-
if (this.config.brand.privacyPolicyURL) {
|
|
98
|
-
iface.privacyPolicyURL = this.config.brand.privacyPolicyURL
|
|
99
|
-
}
|
|
100
|
-
if (this.config.brand.termsOfServiceURL) {
|
|
101
|
-
iface.termsOfServiceURL = this.config.brand.termsOfServiceURL
|
|
102
|
-
}
|
|
103
|
-
if (this.config.brand.screenshots) {
|
|
104
|
-
iface.screenshots = this.config.brand.screenshots
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Merge Codex-specific interface overrides
|
|
108
|
-
const codexOverrides = this.config.platforms?.codex?.interface
|
|
109
|
-
if (codexOverrides) {
|
|
110
|
-
Object.assign(iface, codexOverrides)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
iface.developerName = this.config.author.name
|
|
114
|
-
|
|
115
|
-
manifest.interface = iface
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
await this.writeJson('.codex-plugin/plugin.json', manifest)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private async generatePermissionsCompanion(): Promise<void> {
|
|
122
|
-
if (!this.config.permissions) return
|
|
123
|
-
|
|
124
|
-
const rules = collectPermissionRules(this.config.permissions)
|
|
125
|
-
if (rules.length === 0) return
|
|
126
|
-
|
|
127
|
-
await this.writeJson('.codex/permissions.generated.json', {
|
|
128
|
-
model: 'pluxx.permissions.v1',
|
|
129
|
-
enforcedByPluginBundle: false,
|
|
130
|
-
note: 'Codex permissions are configured externally. Use this file as a generated mirror of canonical rules for Codex user/admin policy or hook configuration.',
|
|
131
|
-
rules,
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private async generateAgentsMd(): Promise<void> {
|
|
136
|
-
if (!this.config.instructions) return
|
|
137
|
-
const srcPath = this.resolveConfigPath(this.config.instructions, 'instructions')
|
|
138
|
-
if (!existsSync(srcPath)) return
|
|
139
|
-
|
|
140
|
-
const content = await Bun.file(srcPath).text()
|
|
141
|
-
await this.writeFile('AGENTS.md', content)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
}
|