@orchid-labs/pluxx 0.1.0 → 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 +110 -515
- 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 +69 -0
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +3 -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/eval.d.ts +22 -0
- package/dist/cli/eval.d.ts.map +1 -0
- package/dist/cli/index.d.ts +26 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21810 -0
- package/dist/cli/init-from-mcp.d.ts +34 -3
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +3 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts +7 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.d.ts +10 -0
- package/dist/cli/mcp-proxy.d.ts.map +1 -0
- 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/test.d.ts +2 -0
- package/dist/cli/test.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 +2 -0
- 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 +5 -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 +5464 -548
- package/dist/mcp/introspect.d.ts +43 -1
- package/dist/mcp/introspect.d.ts.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- 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 +35 -1
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +16 -14
- package/src/cli/agent.ts +0 -1030
- package/src/cli/dev.ts +0 -112
- package/src/cli/doctor.ts +0 -588
- package/src/cli/index.ts +0 -2414
- package/src/cli/init-from-mcp.ts +0 -1611
- package/src/cli/install.ts +0 -698
- package/src/cli/lint.ts +0 -1219
- package/src/cli/migrate.ts +0 -614
- 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 -563
- package/src/cli/test.ts +0 -134
- 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 -29
- package/src/generators/cline/index.ts +0 -35
- package/src/generators/codex/index.ts +0 -120
- 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 -23
- package/src/mcp/introspect.ts +0 -834
- package/src/permissions.ts +0 -258
- package/src/schema.ts +0 -312
- package/src/user-config.ts +0 -177
- package/src/validation/platform-rules.ts +0 -565
|
@@ -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
|
-
}
|