Share profile merge, workflow filename/path helpers, HTTP_METHODS (with OPTIONS), and isPlainObject so web and server stop drifting. Core script sandboxes keep local isPlainObject copies. Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
3.1 KiB
JavaScript
105 lines
3.1 KiB
JavaScript
import yaml from "yaml";
|
|
import {
|
|
ensureWorkflowFilename,
|
|
suggestCopyFilename,
|
|
} from "@jerapah-flow/shared";
|
|
import {
|
|
newWorkflowFilename,
|
|
workflowFileStem,
|
|
} from "./workflow-normalize.js";
|
|
|
|
export { ensureWorkflowFilename, suggestCopyFilename };
|
|
|
|
/**
|
|
* UUID-based duplicate filename (default for new duplicates).
|
|
* @param {string[]} existingFiles
|
|
*/
|
|
export function suggestDuplicateFilename(existingFiles = []) {
|
|
const existing = new Set(existingFiles);
|
|
let file = newWorkflowFilename();
|
|
while (existing.has(file)) {
|
|
file = newWorkflowFilename();
|
|
}
|
|
return file;
|
|
}
|
|
|
|
export function nextCopyName(name) {
|
|
const trimmed = String(name ?? "").trim();
|
|
if (!trimmed) return "copy";
|
|
const match = trimmed.match(/^(.*) \(copy(?: (\d+))?\)$/);
|
|
if (!match) return `${trimmed} (copy)`;
|
|
const n = match[2] ? Number(match[2]) + 1 : 2;
|
|
return `${match[1]} (copy ${n})`;
|
|
}
|
|
|
|
export function httpPathCopySuffix(sourceFile, destFile) {
|
|
const src = workflowFileStem(sourceFile);
|
|
const dest = workflowFileStem(destFile);
|
|
if (dest.startsWith(`${src}-`) && dest.length > src.length + 1) {
|
|
return dest.slice(src.length + 1);
|
|
}
|
|
if (dest === src) return "copy";
|
|
return dest || "copy";
|
|
}
|
|
|
|
function suffixHttpPath(path, suffix) {
|
|
const trimmed = String(path).replace(/\/+$/, "");
|
|
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
const safe =
|
|
String(suffix)
|
|
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
.replace(/^-+|-+$/g, "") || "copy";
|
|
return `${withSlash}-${safe}`;
|
|
}
|
|
|
|
function rewriteHttpTriggerPaths(doc, suffix) {
|
|
const triggers = doc.get("triggers");
|
|
if (!yaml.isSeq(triggers)) return;
|
|
for (const item of triggers.items) {
|
|
if (!yaml.isMap(item)) continue;
|
|
if (String(item.get("type") ?? "").toLowerCase() !== "http") continue;
|
|
const path = item.get("path");
|
|
if (typeof path !== "string" || !path.trim()) continue;
|
|
item.set("path", suffixHttpPath(path, suffix));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy workflow YAML: append " (copy)" to name, disable, optionally rewrite HTTP paths.
|
|
* Preserves comments via YAML CST.
|
|
* @param {string} content
|
|
* @param {{ sourceFile: string, destFile: string, rewriteHttpPaths?: boolean }} opts
|
|
*/
|
|
export function duplicateWorkflowYaml(content, opts) {
|
|
const sourceFile = opts?.sourceFile ?? "";
|
|
const destFile = opts?.destFile ?? "";
|
|
const rewriteHttpPaths = opts?.rewriteHttpPaths !== false;
|
|
|
|
const doc = yaml.parseDocument(content);
|
|
if (doc.errors?.length) {
|
|
const err = new Error(doc.errors[0]?.message ?? "invalid yaml");
|
|
err.statusCode = 400;
|
|
throw err;
|
|
}
|
|
const parsed = doc.toJSON();
|
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
const err = new Error("workflow yaml must be an object");
|
|
err.statusCode = 400;
|
|
throw err;
|
|
}
|
|
|
|
const currentName = doc.get("name");
|
|
if (typeof currentName === "string" && currentName.trim()) {
|
|
doc.set("name", nextCopyName(currentName));
|
|
} else {
|
|
doc.set("name", workflowFileStem(destFile) || "copy");
|
|
}
|
|
doc.set("enabled", false);
|
|
|
|
if (rewriteHttpPaths) {
|
|
rewriteHttpTriggerPaths(doc, httpPathCopySuffix(sourceFile, destFile));
|
|
}
|
|
|
|
return String(doc);
|
|
}
|