feat(workflows): add revision history, trash, UUID naming, and backup

- Store up to 50 revisions per workflow in SQLite with normalized SHA dedup
- Soft-delete workflows to trash (7-day retention) with restore and permanent purge
- Assign UUID filenames for new and duplicated workflows; show name + file in UI
- Warn on invalid YAML, unknown scripts, and plaintext secrets with save-anyway option
- Add workflow backup zip (workflows + plugins) and merge/replace restore
- Trash page, history panel on editor, and smoke test

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-20 02:14:50 +00:00
co-authored by nsrb
parent 611511ae60
commit 7d51ff433f
20 changed files with 1943 additions and 206 deletions
+24 -7
View File
@@ -1,4 +1,10 @@
import yaml from "yaml";
import {
newWorkflowFilename,
workflowFileStem,
} from "./workflow-normalize.js";
export { workflowFileStem };
export function ensureWorkflowFilename(file) {
const trimmed = String(file ?? "").trim();
@@ -6,13 +12,8 @@ export function ensureWorkflowFilename(file) {
return /\.ya?ml$/i.test(trimmed) ? trimmed : `${trimmed}.yaml`;
}
export function workflowFileStem(file) {
return String(file).replace(/\.ya?ml$/i, "");
}
/**
* Next unused copy filename: `track.yaml` → `track-copy.yaml`,
* `track-copy.yaml` → `track-copy-2.yaml`.
* Legacy human-readable copy name (kept for UI hints).
* @param {string} file
* @param {string[]} existingFiles
*/
@@ -33,6 +34,19 @@ export function suggestCopyFilename(file, existingFiles = []) {
return candidate(n);
}
/**
* 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";
@@ -55,7 +69,10 @@ export function httpPathCopySuffix(sourceFile, destFile) {
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";
const safe =
String(suffix)
.replace(/[^A-Za-z0-9._-]+/g, "-")
.replace(/^-+|-+$/g, "") || "copy";
return `${withSlash}-${safe}`;
}