feat(scripts): add sidecar icons and tags in the admin UI
Serve optional PNG/JPG next to each script and show meta tags in lists, cards, and the editor. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -55,11 +55,42 @@ export function writeScript(name, content) {
|
||||
fs.writeFileSync(path.join(SCRIPTS_DIR, name), content, "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon next to the script: `fetch-html.js` → `fetch-html.png` or `.jpg`.
|
||||
* @returns {{ filePath: string, contentType: string } | null}
|
||||
*/
|
||||
export function resolveScriptIcon(name) {
|
||||
assertScriptName(name);
|
||||
const base = name.slice(0, -3);
|
||||
for (const { ext, contentType } of [
|
||||
{ ext: "png", contentType: "image/png" },
|
||||
{ ext: "jpg", contentType: "image/jpeg" },
|
||||
]) {
|
||||
const filePath = path.join(SCRIPTS_DIR, `${base}.${ext}`);
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||
return { filePath, contentType };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function scriptHasIcon(name) {
|
||||
return resolveScriptIcon(name) != null;
|
||||
}
|
||||
|
||||
export function deleteScript(name) {
|
||||
assertScriptName(name);
|
||||
const filePath = path.join(SCRIPTS_DIR, name);
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
const icon = resolveScriptIcon(name);
|
||||
fs.unlinkSync(filePath);
|
||||
if (icon) {
|
||||
try {
|
||||
fs.unlinkSync(icon.filePath);
|
||||
} catch {
|
||||
// ignore missing icon
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ async function fetchHtml(ctx) {
|
||||
fetchHtml.meta = {
|
||||
description: "Fetch HTML and optionally select elements or transform with JSONata",
|
||||
previewConfigKey: "url",
|
||||
tags: ["HTTP"],
|
||||
config: {
|
||||
url: { type: "string", required: true, description: "Page URL" },
|
||||
selector: { type: "string", required: false, description: "CSS selector" },
|
||||
|
||||
@@ -116,6 +116,7 @@ async function fetchHttp(ctx) {
|
||||
fetchHttp.meta = {
|
||||
description: "Fetch a URL and store the response body.",
|
||||
previewConfigKey: "url",
|
||||
tags: ["HTTP"],
|
||||
config: {
|
||||
url: { type: "string", required: true, description: "Request URL" },
|
||||
method: {
|
||||
|
||||
@@ -69,6 +69,7 @@ async function fetchRssFeed(ctx) {
|
||||
fetchRssFeed.meta = {
|
||||
description: "Fetch an RSS/Atom feed and optionally transform it with JSONata",
|
||||
previewConfigKey: "url",
|
||||
tags: ["RSS"],
|
||||
config: {
|
||||
url: { type: "string", required: false, description: "Feed URL (or pass data.url)" },
|
||||
outputVar: {
|
||||
|
||||
@@ -29,6 +29,7 @@ triggerWorkflow.meta = {
|
||||
description:
|
||||
"Fire-and-forget another workflow by YAML name (same owner). Destination must declare triggers: [{ type: workflow }]. Optionally reshape ctx.data with JSONata before sending.",
|
||||
previewConfigKey: "name",
|
||||
tags: ["trigger"],
|
||||
config: {
|
||||
name: {
|
||||
type: "string",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "fs";
|
||||
import {
|
||||
clearScriptCache,
|
||||
inspectScriptSource,
|
||||
@@ -21,11 +22,27 @@ export default function scriptsPluginFactory(registry) {
|
||||
content == null
|
||||
? { meta: null, metaError: "script not found" }
|
||||
: inspectScriptSource(name, content);
|
||||
return { name, ...inspected };
|
||||
return { name, hasIcon: fsStore.scriptHasIcon(name), ...inspected };
|
||||
});
|
||||
return { scripts };
|
||||
});
|
||||
|
||||
fastify.get("/scripts/:name/icon", async (req, reply) => {
|
||||
const { name } = /** @type {{ name: string }} */ (req.params);
|
||||
try {
|
||||
fsStore.assertScriptName(name);
|
||||
} catch (err) {
|
||||
return reply.code(err.statusCode ?? 400).send({ error: err.message });
|
||||
}
|
||||
const icon = fsStore.resolveScriptIcon(name);
|
||||
if (icon == null) return reply.code(404).send({ error: "icon not found" });
|
||||
const body = fs.readFileSync(icon.filePath);
|
||||
return reply
|
||||
.type(icon.contentType)
|
||||
.header("Cache-Control", "private, max-age=60")
|
||||
.send(body);
|
||||
});
|
||||
|
||||
fastify.get("/scripts/:name", async (req, reply) => {
|
||||
const { name } = /** @type {{ name: string }} */ (req.params);
|
||||
try {
|
||||
@@ -35,7 +52,7 @@ export default function scriptsPluginFactory(registry) {
|
||||
}
|
||||
const content = fsStore.readScript(name);
|
||||
if (content == null) return reply.code(404).send({ error: "script not found" });
|
||||
return { name, content, ...inspectScriptSource(name, content) };
|
||||
return { name, content, hasIcon: fsStore.scriptHasIcon(name), ...inspectScriptSource(name, content) };
|
||||
});
|
||||
|
||||
fastify.put("/scripts/:name", async (req, reply) => {
|
||||
|
||||
Reference in New Issue
Block a user