mirror of
https://github.com/penpot/penpot.git
synced 2026-09-24 12:56:15 +00:00
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
170 lines
4.3 KiB
JavaScript
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,
|
|
}
|