@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.
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,71 +0,0 @@
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 }
@@ -1,526 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
2
- import { extname, relative, resolve } from 'path'
3
- import { Generator } from '../base'
4
- import type { HookEntry, TargetPlatform } from '../../schema'
5
- import { buildOpenCodePermissionMap } from '../../permissions'
6
-
7
- type GeneratedHook = {
8
- command: string
9
- timeout?: number
10
- matcher?: HookEntry['matcher']
11
- failClosed?: boolean
12
- }
13
-
14
- interface OpenCodeMcpDefinition {
15
- transport: 'http' | 'sse' | 'stdio'
16
- url?: string
17
- command?: string
18
- args?: string[]
19
- env?: Record<string, string>
20
- auth?: {
21
- type: 'bearer' | 'header' | 'none'
22
- envVar?: string
23
- headerName?: string
24
- headerTemplate?: string
25
- }
26
- }
27
-
28
- interface OpenCodeHookPlan {
29
- event: Record<string, GeneratedHook[]>
30
- toolBefore: {
31
- all: GeneratedHook[]
32
- read: GeneratedHook[]
33
- mcp: GeneratedHook[]
34
- }
35
- toolAfter: {
36
- all: GeneratedHook[]
37
- edit: GeneratedHook[]
38
- mcp: GeneratedHook[]
39
- }
40
- shellEnv: GeneratedHook[]
41
- chatMessage: GeneratedHook[]
42
- }
43
-
44
- export class OpenCodeGenerator extends Generator {
45
- readonly platform: TargetPlatform = 'opencode'
46
-
47
- async generate(): Promise<void> {
48
- await Promise.all([
49
- this.generatePackageJson(),
50
- this.generatePluginWrapper(),
51
- ])
52
-
53
- this.copySkills()
54
- this.copyCommands()
55
- this.copyScripts()
56
- this.copyAssets()
57
- }
58
-
59
- private async generatePackageJson(): Promise<void> {
60
- const npmName = this.config.platforms?.opencode?.npmPackage
61
- ?? `opencode-${this.config.name}`
62
-
63
- const pkg = {
64
- name: npmName,
65
- version: this.config.version,
66
- description: `${this.config.description} (OpenCode plugin)`,
67
- main: 'index.ts',
68
- type: 'module',
69
- keywords: [
70
- 'opencode-plugin',
71
- ...(this.config.keywords ?? []),
72
- ],
73
- author: this.config.author.name,
74
- license: this.config.license,
75
- peerDependencies: {
76
- '@opencode-ai/plugin': '*',
77
- },
78
- }
79
-
80
- await this.writeJson('package.json', pkg)
81
- }
82
-
83
- private async generatePluginWrapper(): Promise<void> {
84
- const pluginName = toPascalCase(this.config.name) + 'Plugin'
85
- const envVars = this.getRequiredEnvVars()
86
- const mcpDefinitions = this.getOpenCodeMcpDefinitions()
87
- const commandDefinitions = this.getOpenCodeCommandDefinitions()
88
- const hookPlan = this.getOpenCodeHookPlan()
89
- const instructions = this.getInstructionsContent()
90
- const permissionMap = buildOpenCodePermissionMap(this.config.permissions)
91
-
92
- const lines: string[] = [
93
- `import type { Config, Plugin } from "@opencode-ai/plugin"`,
94
- `import { existsSync, readFileSync } from "fs"`,
95
- `import { resolve } from "path"`,
96
- '',
97
- `type GeneratedHook = {`,
98
- ` command: string`,
99
- ` timeout?: number`,
100
- ` matcher?: string`,
101
- ` failClosed?: boolean`,
102
- `}`,
103
- '',
104
- `const REQUIRED_ENV_VARS = ${JSON.stringify(envVars, null, 2)}`,
105
- '',
106
- `const MCP_DEFINITIONS = ${JSON.stringify(mcpDefinitions, null, 2)}`,
107
- '',
108
- `const TUI_COMMANDS = ${JSON.stringify(commandDefinitions, null, 2)}`,
109
- '',
110
- `const EVENT_HOOKS: Record<string, GeneratedHook[]> = ${JSON.stringify(hookPlan.event, null, 2)}`,
111
- '',
112
- `const TOOL_BEFORE_HOOKS = ${JSON.stringify(hookPlan.toolBefore, null, 2)}`,
113
- '',
114
- `const TOOL_AFTER_HOOKS = ${JSON.stringify(hookPlan.toolAfter, null, 2)}`,
115
- '',
116
- `const SHELL_ENV_HOOKS = ${JSON.stringify(hookPlan.shellEnv, null, 2)}`,
117
- '',
118
- `const CHAT_MESSAGE_HOOKS = ${JSON.stringify(hookPlan.chatMessage, null, 2)}`,
119
- '',
120
- `const INSTRUCTIONS = ${JSON.stringify(instructions)}`,
121
- '',
122
- `const PERMISSIONS = ${JSON.stringify(permissionMap, null, 2)}`,
123
- '',
124
- `const isMcpTool = (tool: string): boolean =>`,
125
- ` tool === "mcp" || tool.startsWith("mcp.") || tool.startsWith("mcp_")`,
126
- '',
127
- `const loadUserConfig = (directory: string): { values?: Record<string, string | number | boolean>; env?: Record<string, string> } => {`,
128
- ` const filepath = resolve(directory, ".pluxx-user.json")`,
129
- ` if (!existsSync(filepath)) return {}`,
130
- ` try {`,
131
- ` return JSON.parse(readFileSync(filepath, "utf-8"))`,
132
- ` } catch {`,
133
- ` return {}`,
134
- ` }`,
135
- `}`,
136
- '',
137
- `const resolveRuntimeValue = (name: string, userEnv: Record<string, string>): string | undefined =>`,
138
- ` userEnv[name] ?? process.env[name]`,
139
- '',
140
- `const materializeEnv = (input: Record<string, string> | undefined, userEnv: Record<string, string>): Record<string, string> | undefined => {`,
141
- ` if (!input) return undefined`,
142
- ` const output: Record<string, string> = {}`,
143
- ` for (const [key, value] of Object.entries(input)) {`,
144
- ` output[key] = value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => resolveRuntimeValue(name, userEnv) ?? \`\\\${\${name}}\`)`,
145
- ` }`,
146
- ` return output`,
147
- `}`,
148
- '',
149
- `const buildMcpConfig = (directory: string): NonNullable<Config["mcp"]> => {`,
150
- ` const config: NonNullable<Config["mcp"]> = {}`,
151
- ` const userEnv = loadUserConfig(directory).env ?? {}`,
152
- '',
153
- ` for (const [name, definition] of Object.entries(MCP_DEFINITIONS)) {`,
154
- ` if (definition.transport === "stdio" && definition.command) {`,
155
- ` config[name] = {`,
156
- ` type: "local",`,
157
- ` command: [definition.command, ...(definition.args ?? [])],`,
158
- ` ...(definition.env ? { environment: materializeEnv(definition.env, userEnv) } : {}),`,
159
- ` }`,
160
- ` continue`,
161
- ` }`,
162
- '',
163
- ` if (!definition.url) continue`,
164
- '',
165
- ` const remote: {`,
166
- ` type: "remote"`,
167
- ` url: string`,
168
- ` headers?: Record<string, string>`,
169
- ` } = {`,
170
- ` type: "remote",`,
171
- ` url: definition.url,`,
172
- ` }`,
173
- '',
174
- ` if (definition.auth?.type === "bearer" && definition.auth.envVar) {`,
175
- ` const token = resolveRuntimeValue(definition.auth.envVar, userEnv)`,
176
- ` if (token) remote.headers = { Authorization: \`Bearer \${token}\` }`,
177
- ` }`,
178
- '',
179
- ` if (definition.auth?.type === "header" && definition.auth.envVar && definition.auth.headerName && definition.auth.headerTemplate) {`,
180
- ` const value = resolveRuntimeValue(definition.auth.envVar, userEnv)`,
181
- ` if (value) {`,
182
- ` remote.headers = {`,
183
- ` ...(remote.headers ?? {}),`,
184
- ` [definition.auth.headerName]: definition.auth.headerTemplate.replace("\${value}", value),`,
185
- ` }`,
186
- ` }`,
187
- ` }`,
188
- '',
189
- ` config[name] = remote`,
190
- ` }`,
191
- '',
192
- ` return config`,
193
- `}`,
194
- '',
195
- `const applyInstructions = (system: string[]): void => {`,
196
- ` if (!INSTRUCTIONS) return`,
197
- ` if (!system.includes(INSTRUCTIONS)) {`,
198
- ` system.unshift(INSTRUCTIONS)`,
199
- ` }`,
200
- `}`,
201
- '',
202
- `/**`,
203
- ` * ${this.config.description}`,
204
- ` * Generated by pluxx — do not edit manually.`,
205
- ` */`,
206
- `export const ${pluginName}: Plugin = async ({ project, client, $, directory }) => {`,
207
- ` const runHook = async (hook: GeneratedHook, context: Record<string, string>): Promise<void> => {`,
208
- ` try {`,
209
- ` const command = hook.command.replaceAll("\${PLUGIN_ROOT}", directory)`,
210
- ` const execution = $\`bash -lc \${command}\``,
211
- ` if (hook.timeout) {`,
212
- ` await Promise.race([`,
213
- ` execution,`,
214
- ` new Promise((_, reject) => {`,
215
- ` setTimeout(() => reject(new Error(\`Hook timed out after \${hook.timeout}ms: \${command}\`)), hook.timeout)`,
216
- ` }),`,
217
- ` ])`,
218
- ` } else {`,
219
- ` await execution`,
220
- ` }`,
221
- ` } catch (error) {`,
222
- ` await client.app.log({`,
223
- ` body: {`,
224
- ` service: "${this.config.name}",`,
225
- ` level: "error",`,
226
- ` message: "OpenCode hook execution failed",`,
227
- ` extra: {`,
228
- ` ...context,`,
229
- ` hook: hook.command,`,
230
- ` error: error instanceof Error ? error.message : String(error),`,
231
- ` },`,
232
- ` },`,
233
- ` })`,
234
- ` if (hook.failClosed) throw error`,
235
- ` }`,
236
- ` }`,
237
- '',
238
- ` const runHooks = async (hooks: GeneratedHook[], context: Record<string, string>): Promise<void> => {`,
239
- ` for (const hook of hooks) {`,
240
- ` await runHook(hook, context)`,
241
- ` }`,
242
- ` }`,
243
- '',
244
- ` return {`,
245
- ` config: async (config) => {`,
246
- ` if (Object.keys(MCP_DEFINITIONS).length > 0) {`,
247
- ` config.mcp = {`,
248
- ` ...(config.mcp ?? {}),`,
249
- ` ...buildMcpConfig(directory),`,
250
- ` }`,
251
- ` }`,
252
- '',
253
- ` if (Object.keys(TUI_COMMANDS).length > 0) {`,
254
- ` config.command = {`,
255
- ` ...(config.command ?? {}),`,
256
- ` ...TUI_COMMANDS,`,
257
- ` }`,
258
- ` }`,
259
- '',
260
- ` if (Object.keys(PERMISSIONS).length > 0) {`,
261
- ` config.permission = {`,
262
- ` ...(config.permission ?? {}),`,
263
- ` ...PERMISSIONS,`,
264
- ` }`,
265
- ` }`,
266
- ` },`,
267
- '',
268
- ` "tool.execute.before": async (input, output) => {`,
269
- ` await runHooks(TOOL_BEFORE_HOOKS.all, { hookType: "tool.execute.before", tool: input.tool })`,
270
- ` if (input.tool === "read") {`,
271
- ` await runHooks(TOOL_BEFORE_HOOKS.read, { hookType: "tool.execute.before", tool: input.tool })`,
272
- ` }`,
273
- ` if (isMcpTool(input.tool)) {`,
274
- ` await runHooks(TOOL_BEFORE_HOOKS.mcp, { hookType: "tool.execute.before", tool: input.tool })`,
275
- ` }`,
276
- ` },`,
277
- '',
278
- ` "tool.execute.after": async (input, output) => {`,
279
- ` await runHooks(TOOL_AFTER_HOOKS.all, { hookType: "tool.execute.after", tool: input.tool })`,
280
- ` if (input.tool === "edit" || input.tool === "write") {`,
281
- ` await runHooks(TOOL_AFTER_HOOKS.edit, { hookType: "tool.execute.after", tool: input.tool })`,
282
- ` }`,
283
- ` if (isMcpTool(input.tool)) {`,
284
- ` await runHooks(TOOL_AFTER_HOOKS.mcp, { hookType: "tool.execute.after", tool: input.tool })`,
285
- ` }`,
286
- ` },`,
287
- '',
288
- ` "shell.env": async (input, output) => {`,
289
- ` await runHooks(SHELL_ENV_HOOKS, { hookType: "shell.env", cwd: input.cwd })`,
290
- ` },`,
291
- '',
292
- ` "chat.message": async (input, output) => {`,
293
- ` await runHooks(CHAT_MESSAGE_HOOKS, { hookType: "chat.message", sessionID: input.sessionID })`,
294
- ` },`,
295
- '',
296
- ` "experimental.chat.system.transform": async (input, output) => {`,
297
- ` applyInstructions(output.system)`,
298
- ` },`,
299
- '',
300
- ` "experimental.session.compacting": async (input, output) => {`,
301
- ` if (INSTRUCTIONS && !output.context.includes(INSTRUCTIONS)) {`,
302
- ` output.context.push(INSTRUCTIONS)`,
303
- ` }`,
304
- ` },`,
305
- '',
306
- ` event: async ({ event }) => {`,
307
- ` if (event.type === "session.created") {`,
308
- ]
309
-
310
- for (const envVar of envVars) {
311
- lines.push(` if (!process.env.${envVar}) {`)
312
- lines.push(` await client.app.log({`)
313
- lines.push(` body: {`)
314
- lines.push(` service: "${this.config.name}",`)
315
- lines.push(` level: "warn",`)
316
- lines.push(` message: "${envVar} is not set. ${this.config.brand?.displayName ?? this.config.name} plugin may not work correctly.",`)
317
- lines.push(` },`)
318
- lines.push(` })`)
319
- lines.push(` }`)
320
- }
321
- lines.push(` }`)
322
- lines.push(` const hooks = EVENT_HOOKS[event.type] ?? []`)
323
- lines.push(` await runHooks(hooks, { hookType: "event", event: event.type })`)
324
- lines.push(` },`)
325
-
326
- lines.push(` }`)
327
- lines.push(`}`)
328
- lines.push('')
329
-
330
- await this.writeFile('index.ts', lines.join('\n'))
331
- }
332
-
333
- private getRequiredEnvVars(): string[] {
334
- const vars = new Set<string>()
335
- if (this.config.mcp) {
336
- for (const server of Object.values(this.config.mcp)) {
337
- if (server.auth && 'envVar' in server.auth && server.auth.envVar) {
338
- vars.add(server.auth.envVar)
339
- }
340
- }
341
- }
342
- return [...vars]
343
- }
344
-
345
- private getOpenCodeMcpDefinitions(): Record<string, OpenCodeMcpDefinition> {
346
- if (!this.config.mcp) return {}
347
-
348
- const output: Record<string, OpenCodeMcpDefinition> = {}
349
- for (const [name, server] of Object.entries(this.config.mcp)) {
350
- const auth = server.auth?.type === 'platform'
351
- ? undefined
352
- : server.auth
353
-
354
- output[name] = {
355
- transport: server.transport,
356
- ...(server.url ? { url: server.url } : {}),
357
- ...(server.command ? { command: server.command } : {}),
358
- ...(server.args ? { args: server.args } : {}),
359
- ...(server.env ? { env: server.env } : {}),
360
- ...(auth ? { auth } : {}),
361
- }
362
- }
363
- return output
364
- }
365
-
366
- private getOpenCodeCommandDefinitions(): Record<string, { template: string; description?: string }> {
367
- if (!this.config.commands) return {}
368
-
369
- const commandsDir = this.resolveConfigPath(this.config.commands, 'commands')
370
- if (!existsSync(commandsDir)) return {}
371
-
372
- const files = this.walkFiles(commandsDir).filter(file => extname(file) === '.md')
373
- const output: Record<string, { template: string; description?: string }> = {}
374
-
375
- for (const file of files) {
376
- const raw = readFileSync(file, 'utf-8')
377
- const { description, body } = parseMarkdownCommand(raw)
378
- const relativePath = relative(commandsDir, file)
379
- .replace(/\\/g, '/')
380
- .replace(/\.md$/i, '')
381
- const name = relativePath.replace(/\//g, '-').toLowerCase()
382
-
383
- output[name] = {
384
- template: body.trim(),
385
- ...(description ? { description } : {}),
386
- }
387
- }
388
-
389
- return output
390
- }
391
-
392
- private getInstructionsContent(): string | null {
393
- if (!this.config.instructions) return null
394
- const instructionsPath = this.resolveConfigPath(this.config.instructions, 'instructions')
395
- if (!existsSync(instructionsPath)) return null
396
- return readFileSync(instructionsPath, 'utf-8').trim()
397
- }
398
-
399
- private getOpenCodeHookPlan(): OpenCodeHookPlan {
400
- const plan: OpenCodeHookPlan = {
401
- event: {},
402
- toolBefore: { all: [], read: [], mcp: [] },
403
- toolAfter: { all: [], edit: [], mcp: [] },
404
- shellEnv: [],
405
- chatMessage: [],
406
- }
407
-
408
- if (!this.config.hooks) return plan
409
-
410
- for (const [event, entries] of Object.entries(this.config.hooks)) {
411
- if (!entries || entries.length === 0) continue
412
-
413
- const hooks = entries
414
- .filter(entry => entry.type !== 'prompt' && entry.command)
415
- .map(entry => ({
416
- command: entry.command!,
417
- ...(entry.timeout ? { timeout: entry.timeout } : {}),
418
- ...(entry.matcher ? { matcher: entry.matcher } : {}),
419
- ...(entry.failClosed !== undefined ? { failClosed: entry.failClosed } : {}),
420
- }))
421
-
422
- if (hooks.length === 0) continue
423
-
424
- switch (event) {
425
- case 'preToolUse':
426
- plan.toolBefore.all.push(...hooks)
427
- break
428
- case 'beforeReadFile':
429
- plan.toolBefore.read.push(...hooks)
430
- break
431
- case 'beforeMCPExecution':
432
- plan.toolBefore.mcp.push(...hooks)
433
- break
434
- case 'postToolUse':
435
- plan.toolAfter.all.push(...hooks)
436
- break
437
- case 'afterFileEdit':
438
- plan.toolAfter.edit.push(...hooks)
439
- break
440
- case 'afterMCPExecution':
441
- plan.toolAfter.mcp.push(...hooks)
442
- break
443
- case 'beforeShellExecution':
444
- plan.shellEnv.push(...hooks)
445
- break
446
- case 'beforeSubmitPrompt':
447
- plan.chatMessage.push(...hooks)
448
- break
449
- default: {
450
- const opencodeEvent = mapHookEventName(event)
451
- if (!plan.event[opencodeEvent]) {
452
- plan.event[opencodeEvent] = []
453
- }
454
- plan.event[opencodeEvent].push(...hooks)
455
- }
456
- }
457
- }
458
-
459
- return plan
460
- }
461
-
462
- private walkFiles(dir: string): string[] {
463
- const entries = readdirSync(dir)
464
- const files: string[] = []
465
-
466
- for (const entry of entries) {
467
- const fullPath = resolve(dir, entry)
468
- const stat = statSync(fullPath)
469
- if (stat.isDirectory()) {
470
- files.push(...this.walkFiles(fullPath))
471
- } else if (stat.isFile()) {
472
- files.push(fullPath)
473
- }
474
- }
475
-
476
- return files
477
- }
478
- }
479
-
480
- function parseMarkdownCommand(content: string): { description?: string; body: string } {
481
- const trimmed = content.trim()
482
- if (!trimmed.startsWith('---')) {
483
- return { body: content }
484
- }
485
-
486
- const endFrontmatter = trimmed.indexOf('\n---', 3)
487
- if (endFrontmatter === -1) {
488
- return { body: content }
489
- }
490
-
491
- const frontmatter = trimmed.slice(3, endFrontmatter).trim()
492
- const body = trimmed.slice(endFrontmatter + 4)
493
- const descriptionLine = frontmatter
494
- .split('\n')
495
- .find(line => line.trim().startsWith('description:'))
496
- const description = descriptionLine
497
- ? descriptionLine.split(':').slice(1).join(':').trim().replace(/^["']|["']$/g, '')
498
- : undefined
499
-
500
- return { description, body }
501
- }
502
-
503
- function mapHookEventName(event: string): string {
504
- const map: Record<string, string> = {
505
- sessionStart: 'session.created',
506
- sessionEnd: 'session.idle',
507
- stop: 'session.idle',
508
- beforeShellExecution: 'shell.env',
509
- afterShellExecution: 'command.executed',
510
- preToolUse: 'tool.execute.before',
511
- postToolUse: 'tool.execute.after',
512
- beforeMCPExecution: 'tool.execute.before',
513
- afterMCPExecution: 'tool.execute.after',
514
- afterFileEdit: 'file.edited',
515
- beforeReadFile: 'tool.execute.before',
516
- beforeSubmitPrompt: 'chat.message',
517
- }
518
- return map[event] ?? event
519
- }
520
-
521
- function toPascalCase(str: string): string {
522
- return str
523
- .split('-')
524
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
525
- .join('')
526
- }
@@ -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
- * OpenHands uses .plugin/plugin.json (Claude Code-compatible format).
7
- * Discovery dirs: .openhands/skills/, .claude/skills/, .agents/skills/
8
- */
9
- export class OpenHandsGenerator extends Generator {
10
- readonly platform: TargetPlatform = 'openhands'
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: '.plugin/plugin.json',
19
- instructionsFile: 'AGENTS.md',
20
- pluginRootVar: '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,35 +0,0 @@
1
- import { existsSync } from 'fs'
2
- import { Generator } from '../base'
3
- import type { TargetPlatform } from '../../schema'
4
-
5
- /**
6
- * Roo Code uses .roo/mcp.json for MCP config, .roorules for instructions,
7
- * and .roo/skills/ for skills. No plugin manifest.
8
- */
9
- export class RooCodeGenerator extends Generator {
10
- readonly platform: TargetPlatform = 'roo-code'
11
-
12
- async generate(): Promise<void> {
13
- await Promise.all([
14
- this.generateMcpConfig('.roo/mcp.json'),
15
- this.generateRules(),
16
- ])
17
-
18
- this.copyRooSkills()
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('.roorules', content)
29
- }
30
-
31
- /** Copy skills into .roo/skills/ instead of the default skills/ */
32
- private copyRooSkills(): void {
33
- this.copyDir(this.config.skills, '.roo/skills/', 'skills')
34
- }
35
- }