@orchid-labs/pluxx 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +25 -8
  2. package/bin/pluxx.js +19 -28
  3. package/dist/agents.d.ts +16 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/cli/agent.d.ts +62 -0
  6. package/dist/cli/agent.d.ts.map +1 -1
  7. package/dist/cli/doctor.d.ts +2 -0
  8. package/dist/cli/doctor.d.ts.map +1 -1
  9. package/dist/cli/entry.d.ts +2 -0
  10. package/dist/cli/entry.d.ts.map +1 -0
  11. package/dist/cli/index.d.ts +7 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +21810 -0
  14. package/dist/cli/init-from-mcp.d.ts +17 -1
  15. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  16. package/dist/cli/install.d.ts +1 -0
  17. package/dist/cli/install.d.ts.map +1 -1
  18. package/dist/cli/lint.d.ts +3 -1
  19. package/dist/cli/lint.d.ts.map +1 -1
  20. package/dist/cli/mcp-proxy.d.ts.map +1 -1
  21. package/dist/cli/migrate.d.ts.map +1 -1
  22. package/dist/cli/primitive-summary.d.ts +14 -0
  23. package/dist/cli/primitive-summary.d.ts.map +1 -0
  24. package/dist/cli/prompt.d.ts +1 -1
  25. package/dist/cli/publish.d.ts +6 -1
  26. package/dist/cli/publish.d.ts.map +1 -1
  27. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  28. package/dist/cli/verify-install.d.ts +25 -0
  29. package/dist/cli/verify-install.d.ts.map +1 -0
  30. package/dist/commands.d.ts +10 -0
  31. package/dist/commands.d.ts.map +1 -0
  32. package/dist/compiler-intent.d.ts +165 -0
  33. package/dist/compiler-intent.d.ts.map +1 -0
  34. package/dist/config/load.d.ts.map +1 -1
  35. package/dist/delegation.d.ts +11 -0
  36. package/dist/delegation.d.ts.map +1 -0
  37. package/dist/generators/amp/index.d.ts.map +1 -1
  38. package/dist/generators/base.d.ts +5 -0
  39. package/dist/generators/base.d.ts.map +1 -1
  40. package/dist/generators/claude-code/index.d.ts.map +1 -1
  41. package/dist/generators/cline/index.d.ts.map +1 -1
  42. package/dist/generators/codex/index.d.ts +4 -0
  43. package/dist/generators/codex/index.d.ts.map +1 -1
  44. package/dist/generators/cursor/index.d.ts +1 -0
  45. package/dist/generators/cursor/index.d.ts.map +1 -1
  46. package/dist/generators/gemini-cli/index.d.ts.map +1 -1
  47. package/dist/generators/github-copilot/index.d.ts.map +1 -1
  48. package/dist/generators/opencode/index.d.ts +1 -0
  49. package/dist/generators/opencode/index.d.ts.map +1 -1
  50. package/dist/generators/openhands/index.d.ts.map +1 -1
  51. package/dist/generators/roo-code/index.d.ts.map +1 -1
  52. package/dist/generators/shared/claude-family.d.ts.map +1 -1
  53. package/dist/generators/warp/index.d.ts.map +1 -1
  54. package/dist/index.d.ts +4 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +5371 -553
  57. package/dist/schema.d.ts +91 -42
  58. package/dist/schema.d.ts.map +1 -1
  59. package/dist/text-files.d.ts +5 -0
  60. package/dist/text-files.d.ts.map +1 -0
  61. package/dist/validation/platform-rules.d.ts +15 -1
  62. package/dist/validation/platform-rules.d.ts.map +1 -1
  63. package/package.json +15 -13
  64. package/src/cli/agent.ts +0 -1455
  65. package/src/cli/dev.ts +0 -112
  66. package/src/cli/doctor.ts +0 -987
  67. package/src/cli/eval.ts +0 -470
  68. package/src/cli/index.ts +0 -2933
  69. package/src/cli/init-from-mcp.ts +0 -2115
  70. package/src/cli/install.ts +0 -860
  71. package/src/cli/lint.ts +0 -1249
  72. package/src/cli/mcp-proxy.ts +0 -322
  73. package/src/cli/migrate.ts +0 -867
  74. package/src/cli/prompt.ts +0 -82
  75. package/src/cli/publish.ts +0 -401
  76. package/src/cli/runtime.ts +0 -86
  77. package/src/cli/sync-from-mcp.ts +0 -586
  78. package/src/cli/test.ts +0 -142
  79. package/src/compatibility/matrix.ts +0 -149
  80. package/src/config/define.ts +0 -20
  81. package/src/config/load.ts +0 -74
  82. package/src/generators/amp/index.ts +0 -63
  83. package/src/generators/base.ts +0 -188
  84. package/src/generators/claude-code/index.ts +0 -172
  85. package/src/generators/cline/index.ts +0 -35
  86. package/src/generators/codex/index.ts +0 -143
  87. package/src/generators/cursor/index.ts +0 -158
  88. package/src/generators/gemini-cli/index.ts +0 -83
  89. package/src/generators/github-copilot/index.ts +0 -32
  90. package/src/generators/hooks-warning.ts +0 -51
  91. package/src/generators/index.ts +0 -71
  92. package/src/generators/opencode/index.ts +0 -526
  93. package/src/generators/openhands/index.ts +0 -32
  94. package/src/generators/roo-code/index.ts +0 -35
  95. package/src/generators/shared/claude-family.ts +0 -215
  96. package/src/generators/warp/index.ts +0 -32
  97. package/src/hook-events.ts +0 -33
  98. package/src/index.ts +0 -34
  99. package/src/mcp/introspect.ts +0 -1107
  100. package/src/permissions.ts +0 -260
  101. package/src/schema.ts +0 -312
  102. package/src/user-config.ts +0 -177
  103. package/src/validation/platform-rules.ts +0 -686
@@ -1,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
- }