Andrey Antukh 992170a9a4 🐛 Remove OpenCode V1 plugin support
Remove the legacy @opencode-ai/plugin import and V1 server export so
the local plugin loads under OpenCode V2 without project dependencies.

Add a focused smoke test for the V2 export and tool registration, and
update the related script memories.

AI-assisted-by: space-bunny-free
2026-09-23 18:53:25 +02:00

170 lines
4.3 KiB
JavaScript

// Penpot OpenCode V2 plugin: custom tools for Penpot development.
//
// Tools are registered with `ctx.tool.transform()` and JSON Schema inputs.
// Keep this plugin dependency-free so the auto-discovered local plugin loads
// without project npm dependencies.
import path from "path"
import { spawn } from "child_process"
function runCommand(command, args, options = {}) {
const {
cwd,
env,
stdin,
closeStdin = false,
successMessage = "Command executed successfully",
} = options
return new Promise((resolve) => {
let stdout = ""
let stderr = ""
const proc = spawn(command, args, { cwd, env })
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("error", (error) => {
resolve(`Error: ${error.message}`)
})
proc.on("close", (exitCode) => {
const output =
exitCode === 0
? stdout.trim() || successMessage
: `Error (exit ${exitCode}): ${
(stderr || stdout).trim() || "No error output"
}`
resolve(output)
})
// Close stdin so the child cannot wait on it indefinitely. `psql -c`
// never reads stdin, so only the paren-repair pipe mode needs this, but
// closing it unconditionally is harmless there.
if (stdin !== undefined) {
proc.stdin.end(stdin)
} else if (closeStdin) {
proc.stdin.end()
}
})
}
function executePsql(sql, useTestDb, cwd) {
const host = process.env.PENPOT_DB_HOST || "postgres"
const user = process.env.PENPOT_DB_USER || "penpot"
const db = useTestDb
? "penpot_test"
: process.env.PENPOT_DB_NAME || "penpot"
const password = process.env.PENPOT_DB_PASSWORD || "penpot"
const psqlArgs = ["-h", host, "-U", user, "-d", db, "-c", sql]
return runCommand("psql", psqlArgs, {
cwd,
env: { ...process.env, PGPASSWORD: password },
successMessage: "Query executed successfully",
})
}
function executeParenRepair({ files, code }, directory) {
const script = path.join(directory, "scripts/paren-repair")
const fileList = files
? files
.split(",")
.map((file) => file.trim())
.filter(Boolean)
: []
const childArgs =
fileList.length > 0 ? [script, ...fileList] : [script]
return runCommand("bb", childArgs, {
cwd: directory,
stdin: code,
closeStdin: true,
successMessage: "No changes needed",
})
}
const penpotPsqlInputSchema = {
type: "object",
properties: {
sql: {
type: "string",
description: "SQL command to execute",
},
test: {
type: "boolean",
description: "Use the penpot_test database",
},
},
required: ["sql"],
additionalProperties: false,
}
const parenRepairInputSchema = {
type: "object",
properties: {
// A string is used instead of an array so OpenCode displays it
// in the generic tool invocation.
files: {
type: "string",
description:
"Comma-separated file paths to fix, for example: frontend/src/app/config.cljs, backend/src/core.clj",
},
code: {
type: "string",
description: "Code string to fix via stdin",
},
},
additionalProperties: false,
}
async function setup(ctx) {
// Use the plugin instance location as the working directory for both tools.
const directory =
ctx.location.directory ?? ctx.location.project?.canonical
// Keep this callback synchronous: transforms are replayable state edits.
// The async work happens later, inside each tool's `execute`.
await ctx.tool.transform((editor) => {
editor.add({
name: "penpot-psql",
description:
"Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.",
input: penpotPsqlInputSchema,
async execute(input) {
const content = await executePsql(
input.sql,
input.test === true,
directory,
)
return { content }
},
})
editor.add({
name: "paren-repair",
description:
"Fix mismatched parentheses/braces in Clojure files (.clj, .cljs, .cljc) then reformat with cljfmt.",
input: parenRepairInputSchema,
async execute(input) {
const content = await executeParenRepair(input, directory)
return { content }
},
})
})
}
export default {
id: "penpot",
setup,
}