diff --git a/Data/Server/WebUI/src/App.jsx b/Data/Server/WebUI/src/App.jsx index 5bd1c21..6686702 100644 --- a/Data/Server/WebUI/src/App.jsx +++ b/Data/Server/WebUI/src/App.jsx @@ -26,7 +26,7 @@ import StatusBar from "./Status_Bar"; import NavigationSidebar from "./Navigation_Sidebar"; import WorkflowList from "./Workflows/Workflow_List"; import DeviceList from "./Devices/Device_List"; -import ScriptList from "./Scripting/Script_List"; +import ScriptEditor from "./Scripting/Script_Editor"; import ScheduledJobsList from "./Scheduling/Scheduled_Jobs_List"; import Login from "./Login.jsx"; import DeviceDetails from "./Devices/Device_Details"; @@ -371,7 +371,7 @@ export default function App() { ); case "scripts": - return ; + return ; case "workflow-editor": return ( diff --git a/Data/Server/WebUI/src/Scripting/Script_Editor.jsx b/Data/Server/WebUI/src/Scripting/Script_Editor.jsx new file mode 100644 index 0000000..64147b4 --- /dev/null +++ b/Data/Server/WebUI/src/Scripting/Script_Editor.jsx @@ -0,0 +1,693 @@ +////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Scripting/Script_Editor.jsx + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { + Paper, + Box, + Typography, + Button, + Menu, + MenuItem, + Select, + FormControl, + InputLabel, + TextField +} from "@mui/material"; +import { Folder as FolderIcon, Description as DescriptionIcon } from "@mui/icons-material"; +import { + SimpleTreeView, + TreeItem, + useTreeViewApiRef +} from "@mui/x-tree-view"; + +// Reuse shared dialogs for consistency +import { ConfirmDeleteDialog, RenameFolderDialog } from "../Dialogs"; + +// Prism for syntax highlighting (same theme as JSON node) +import Prism from "prismjs"; +import "prismjs/components/prism-yaml"; +import "prismjs/components/prism-bash"; +import "prismjs/components/prism-powershell"; +import "prismjs/components/prism-batch"; +import "prismjs/themes/prism-okaidia.css"; + +// ---------- helpers ---------- +const TYPE_OPTIONS = [ + { key: "ansible", label: "Ansible Playbook", ext: ".yml", prism: "yaml" }, + { key: "powershell", label: "Powershell Script", ext: ".ps1", prism: "powershell" }, + { key: "batch", label: "Batch Script", ext: ".bat", prism: "batch" }, + { key: "bash", label: "Bash Script", ext: ".sh", prism: "bash" } +]; + +const keyBy = (arr) => Object.fromEntries(arr.map((o) => [o.key, o])); +const TYPES = keyBy(TYPE_OPTIONS); + +function typeFromFilename(name = "") { + const n = name.toLowerCase(); + if (n.endsWith(".yml")) return "ansible"; + if (n.endsWith(".ps1")) return "powershell"; + if (n.endsWith(".bat")) return "batch"; + if (n.endsWith(".sh")) return "bash"; + return "ansible"; // default +} + +function ensureExt(baseName, typeKey) { + const t = TYPES[typeKey] || TYPES.ansible; + const bn = baseName.replace(/\.(yml|ps1|bat|sh)$/i, ""); + return bn + t.ext; +} + +function buildTree(scripts, folders) { + const map = {}; + const rootNode = { + id: "root", + label: "Scripts", + path: "", + isFolder: true, + children: [] + }; + map[rootNode.id] = rootNode; + + (folders || []).forEach((f) => { + const parts = (f || "").split("/"); + let children = rootNode.children; + let parentPath = ""; + parts.forEach((part) => { + const path = parentPath ? `${parentPath}/${part}` : part; + let node = children.find((n) => n.id === path); + if (!node) { + node = { id: path, label: part, path, isFolder: true, children: [] }; + children.push(node); + map[path] = node; + } + children = node.children; + parentPath = path; + }); + }); + + (scripts || []).forEach((s) => { + const parts = (s.rel_path || "").split("/"); + let children = rootNode.children; + let parentPath = ""; + parts.forEach((part, idx) => { + const path = parentPath ? `${parentPath}/${part}` : part; + const isFile = idx === parts.length - 1; + let node = children.find((n) => n.id === path); + if (!node) { + node = { + id: path, + label: isFile ? s.file_name : part, + path, + isFolder: !isFile, + fileName: s.file_name, + script: isFile ? s : null, + children: [] + }; + children.push(node); + map[path] = node; + } + if (!isFile) { + children = node.children; + parentPath = path; + } + }); + }); + + return { root: [rootNode], map }; +} + +function highlightedHtml(code, prismLang) { + try { + const grammar = Prism.languages[prismLang] || Prism.languages.markup; + return Prism.highlight(code ?? "", grammar, prismLang); + } catch { + return (code ?? "").replace(/[&<>]/g, (c) => ({"&":"&","<":"<",">":">"}[c])); + } +} + +// Local dialog: rename script file +function RenameScriptDialog({ open, value, onChange, onCancel, onSave }) { + return ( + ( +
+ )} + sx={{ display: open ? "block" : "none" }} + > +
+ + Rename Script + onChange(e.target.value)} + sx={{ + "& .MuiOutlinedInput-root": { + backgroundColor: "#2a2a2a", + color: "#ccc", + "& fieldset": { borderColor: "#444" }, + "&:hover fieldset": { borderColor: "#666" } + }, + label: { color: "#aaa" }, + mt: 1 + }} + /> + + + + + +
+ + ); +} + +// Local dialog: new script (name + type) +function NewScriptDialog({ open, name, type, onChangeName, onChangeType, onCancel, onCreate }) { + return ( +
} sx={{ display: open ? "block" : "none" }}> +
+ + New Script + onChangeName(e.target.value)} + sx={{ + "& .MuiOutlinedInput-root": { + backgroundColor: "#2a2a2a", + color: "#ccc", + "& fieldset": { borderColor: "#444" }, + "&:hover fieldset": { borderColor: "#666" } + }, + label: { color: "#aaa" }, + mt: 1 + }} + /> + + Type + + + + + + + +
+ + ); +} + +export default function ScriptEditor() { + // Tree state + const [tree, setTree] = useState([]); + const [nodeMap, setNodeMap] = useState({}); + const [contextMenu, setContextMenu] = useState(null); + const [selectedNode, setSelectedNode] = useState(null); + const apiRef = useTreeViewApiRef(); + const [dragNode, setDragNode] = useState(null); + + // Editor state + const [currentPath, setCurrentPath] = useState(""); + const [currentFolder, setCurrentFolder] = useState(""); + const [fileName, setFileName] = useState(""); + const [type, setType] = useState("ansible"); + const [code, setCode] = useState(""); + + // Dialog state + const [renameOpen, setRenameOpen] = useState(false); + const [renameValue, setRenameValue] = useState(""); + const [renameFolderOpen, setRenameFolderOpen] = useState(false); + const [folderDialogMode, setFolderDialogMode] = useState("rename"); + const [newScriptOpen, setNewScriptOpen] = useState(false); + const [newScriptName, setNewScriptName] = useState(""); + const [newScriptType, setNewScriptType] = useState("ansible"); + const [deleteOpen, setDeleteOpen] = useState(false); + + const prismLang = useMemo(() => (TYPES[type]?.prism || "yaml"), [type]); + + const loadTree = useCallback(async () => { + try { + const resp = await fetch("/api/scripts/list"); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.json(); + const { root, map } = buildTree(data.scripts || [], data.folders || []); + setTree(root); + setNodeMap(map); + } catch (err) { + console.error("Failed to load scripts:", err); + setTree([]); + setNodeMap({}); + } + }, []); + + useEffect(() => { loadTree(); }, [loadTree]); + + // Context menu + const handleContextMenu = (e, node) => { + e.preventDefault(); + setSelectedNode(node); + setContextMenu( + contextMenu === null + ? { mouseX: e.clientX - 2, mouseY: e.clientY - 4 } + : null + ); + }; + + const handleDrop = async (target) => { + if (!dragNode || !target.isFolder) return; + // Prevent dropping into itself or its descendants + if (dragNode.path === target.path || target.path.startsWith(`${dragNode.path}/`)) { + setDragNode(null); + return; + } + const newPath = target.path ? `${target.path}/${dragNode.fileName}` : dragNode.fileName; + try { + await fetch("/api/scripts/move_file", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: dragNode.path, new_path: newPath }) + }); + loadTree(); + } catch (err) { + console.error("Failed to move:", err); + } + setDragNode(null); + }; + + const handleNodeSelect = async (_e, itemId) => { + const node = nodeMap[itemId]; + if (node && !node.isFolder) { + try { + const resp = await fetch(`/api/scripts/load?path=${encodeURIComponent(node.path)}`); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.json(); + const folder = node.path.split("/").slice(0, -1).join("/"); + setCurrentFolder(folder); + setCurrentPath(node.path); + setFileName(data.file_name || node.fileName || node.label); + setType(typeFromFilename(data.file_name || node.fileName || node.label)); + setCode(data.content || ""); + } catch (err) { + console.error("Failed to load script:", err); + } + } + }; + + const saveScript = async () => { + if (!currentPath && !fileName) { + // prompt for name/type first if user started typing without creating + setNewScriptName(""); + setNewScriptType(type); + setNewScriptOpen(true); + return; + } + const payload = { + path: currentPath || undefined, + name: currentPath ? undefined : fileName, + content: code, + type + }; + try { + const resp = await fetch("/api/scripts/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data?.error || `HTTP ${resp.status}`); + // Update state with normalized rel_path (may include ext changes) + if (data.rel_path) { + setCurrentPath(data.rel_path); + const fname = data.rel_path.split("/").pop(); + setFileName(fname); + setType(typeFromFilename(fname)); + // ensure tree refresh + loadTree(); + } + } catch (err) { + console.error("Failed to save script:", err); + } + }; + + const handleRename = () => { + setContextMenu(null); + if (!selectedNode) return; + setRenameValue(selectedNode.label); + if (selectedNode.isFolder) { + setFolderDialogMode("rename"); + setRenameFolderOpen(true); + } else setRenameOpen(true); + }; + + const handleNewFolder = () => { + if (!selectedNode) return; + setContextMenu(null); + setFolderDialogMode("create"); + setRenameValue(""); + setRenameFolderOpen(true); + }; + + const handleNewScript = () => { + if (!selectedNode) return; + setContextMenu(null); + setNewScriptName(""); + setNewScriptType("ansible"); + setNewScriptOpen(true); + }; + + const saveRenameFile = async () => { + try { + const res = await fetch("/api/scripts/rename_file", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: selectedNode.path, new_name: renameValue, type }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`); + if (currentPath === selectedNode.path) { + setCurrentPath(data.rel_path || selectedNode.path); + const fname = (data.rel_path || selectedNode.path).split("/").pop(); + setFileName(fname); + setType(typeFromFilename(fname)); + } + setRenameOpen(false); + loadTree(); + } catch (err) { + console.error("Failed to rename file:", err); + setRenameOpen(false); + } + }; + + const saveRenameFolder = async () => { + try { + if (folderDialogMode === "rename" && selectedNode) { + await fetch("/api/scripts/rename_folder", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: selectedNode.path, new_name: renameValue }) + }); + } else { + const basePath = selectedNode ? selectedNode.path : ""; + const newPath = basePath ? `${basePath}/${renameValue}` : renameValue; + await fetch("/api/scripts/create_folder", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: newPath }) + }); + } + setRenameFolderOpen(false); + loadTree(); + } catch (err) { + console.error("Folder operation failed:", err); + setRenameFolderOpen(false); + } + }; + + const confirmDelete = async () => { + if (!selectedNode) return; + try { + if (selectedNode.isFolder) { + await fetch("/api/scripts/delete_folder", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: selectedNode.path }) + }); + } else { + await fetch("/api/scripts/delete_file", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: selectedNode.path }) + }); + if (currentPath === selectedNode.path) { + setCurrentPath(""); + setFileName(""); + setCode(""); + } + } + setDeleteOpen(false); + loadTree(); + } catch (err) { + console.error("Failed to delete:", err); + setDeleteOpen(false); + } + }; + + const createNewScript = () => { + const folder = selectedNode?.isFolder + ? selectedNode.path + : (selectedNode?.path?.split("/").slice(0, -1).join("/") || currentFolder || ""); + const finalName = ensureExt(newScriptName || "script", newScriptType); + const newPath = folder ? `${folder}/${finalName}` : finalName; + setCurrentFolder(folder); + setCurrentPath(newPath); + setFileName(finalName); + setType(newScriptType); + setCode(""); + setNewScriptOpen(false); + // Note: not saved until user clicks Save + }; + + const rootChildIds = tree[0]?.children?.map((c) => c.id) || []; + const highlighted = useMemo(() => highlightedHtml(code, prismLang), [code, prismLang]); + + const renderItems = (nodes) => + (nodes || []).map((n) => ( + !n.isFolder && setDragNode(n)} + onDragOver={(e) => { if (dragNode && n.isFolder) e.preventDefault(); }} + onDrop={(e) => { e.preventDefault(); handleDrop(n); }} + onContextMenu={(e) => handleContextMenu(e, n)} + > + {n.isFolder ? ( + + ) : ( + + )} + {n.label} + + } + > + {n.children && n.children.length > 0 ? renderItems(n.children) : null} + + )); + + return ( + + {/* Left: Tree */} + + + Scripts + + Create, edit, and rearrange scripts in folders. + + + { if (dragNode) e.preventDefault(); }} + onDrop={(e) => { e.preventDefault(); handleDrop({ path: "", isFolder: true }); }} + > + + {renderItems(tree)} + + + setContextMenu(null)} + anchorReference="anchorPosition" + anchorPosition={contextMenu ? { top: contextMenu.mouseY, left: contextMenu.mouseX } : undefined} + PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }} + > + {selectedNode?.isFolder && ( + <> + New Script + New Subfolder + {selectedNode.id !== "root" && ( + Rename + )} + {selectedNode.id !== "root" && ( + { setContextMenu(null); setDeleteOpen(true); }}>Delete + )} + + )} + {!selectedNode?.isFolder && ( + <> + { setContextMenu(null); handleNodeSelect(null, selectedNode.id); }}>Edit + Rename + { setContextMenu(null); setDeleteOpen(true); }}>Delete + + )} + + + + {/* Right: Editor */} + + + + Type + + + + + + {fileName && ( + + )} + + + + + {/* Editor input */} + setCode(e.target.value)} + sx={{ + flex: 0, + mb: 2, + "& .MuiOutlinedInput-root": { + backgroundColor: "#121212", + color: "#e6edf3", + fontFamily: "monospace", + fontSize: "0.9rem", + lineHeight: 1.4, + "& fieldset": { borderColor: "#444" }, + "&:hover fieldset": { borderColor: "#666" } + } + }} + /> + + {/* Highlighted preview */} + + Preview (syntax highlighted) +
+            
+          
+
+
+ + {/* Dialogs */} + setRenameOpen(false)} + onSave={saveRenameFile} + /> + + setRenameFolderOpen(false)} + onSave={saveRenameFolder} + title={folderDialogMode === "rename" ? "Rename Folder" : "New Folder"} + confirmText={folderDialogMode === "rename" ? "Save" : "Create"} + /> + + setNewScriptOpen(false)} + onCreate={createNewScript} + /> + + setDeleteOpen(false)} + onConfirm={confirmDelete} + /> +
+ ); +} diff --git a/Data/Server/server.py b/Data/Server/server.py index f342dc1..7558be5 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -15,6 +15,7 @@ import json # For reading workflow JSON files import shutil # For moving workflow files and folders from typing import List, Dict import sqlite3 +import io # Borealis Python API Endpoints from Python_API_Endpoints.ocr_engines import run_ocr_on_base64 @@ -371,6 +372,266 @@ def rename_workflow(): except Exception as e: return jsonify({"error": str(e)}), 500 + +# --------------------------------------------- +# Scripts Storage API Endpoints +# --------------------------------------------- +def _scripts_root() -> str: + return os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "Scripts") + ) + + +def _detect_script_type(filename: str) -> str: + fn = (filename or "").lower() + if fn.endswith(".yml"): + return "ansible" + if fn.endswith(".ps1"): + return "powershell" + if fn.endswith(".bat"): + return "batch" + if fn.endswith(".sh"): + return "bash" + return "unknown" + + +def _ext_for_type(script_type: str) -> str: + t = (script_type or "").lower() + if t == "ansible": + return ".yml" + if t == "powershell": + return ".ps1" + if t == "batch": + return ".bat" + if t == "bash": + return ".sh" + return "" + + +@app.route("/api/scripts/list", methods=["GET"]) +def list_scripts(): + """Scan /Scripts for known script files and return list + folders.""" + scripts_root = _scripts_root() + results: List[Dict] = [] + folders: List[str] = [] + + if not os.path.isdir(scripts_root): + return jsonify({ + "root": scripts_root, + "scripts": [], + "folders": [] + }), 200 + + exts = (".yml", ".ps1", ".bat", ".sh") + for root, dirs, files in os.walk(scripts_root): + rel_root = os.path.relpath(root, scripts_root) + if rel_root != ".": + folders.append(rel_root.replace(os.sep, "/")) + for fname in files: + if not fname.lower().endswith(exts): + continue + + full_path = os.path.join(root, fname) + rel_path = os.path.relpath(full_path, scripts_root) + parts = rel_path.split(os.sep) + folder_parts = parts[:-1] + breadcrumb_prefix = " > ".join(folder_parts) if folder_parts else "" + display_name = f"{breadcrumb_prefix} > {fname}" if breadcrumb_prefix else fname + + try: + mtime = os.path.getmtime(full_path) + except Exception: + mtime = 0.0 + + results.append({ + "name": display_name, + "breadcrumb_prefix": breadcrumb_prefix, + "file_name": fname, + "rel_path": rel_path.replace(os.sep, "/"), + "type": _detect_script_type(fname), + "last_edited": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(mtime)), + "last_edited_epoch": mtime + }) + + results.sort(key=lambda x: x.get("last_edited_epoch", 0.0), reverse=True) + + return jsonify({ + "root": scripts_root, + "scripts": results, + "folders": folders + }) + + +@app.route("/api/scripts/load", methods=["GET"]) +def load_script(): + rel_path = request.args.get("path", "") + scripts_root = _scripts_root() + abs_path = os.path.abspath(os.path.join(scripts_root, rel_path)) + if not abs_path.startswith(scripts_root) or not os.path.isfile(abs_path): + return jsonify({"error": "Script not found"}), 404 + try: + with open(abs_path, "r", encoding="utf-8", errors="replace") as fh: + content = fh.read() + return jsonify({ + "file_name": os.path.basename(abs_path), + "rel_path": os.path.relpath(abs_path, scripts_root).replace(os.sep, "/"), + "type": _detect_script_type(abs_path), + "content": content + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/scripts/save", methods=["POST"]) +def save_script(): + data = request.get_json(silent=True) or {} + rel_path = (data.get("path") or "").strip() + name = (data.get("name") or "").strip() + content = data.get("content") + script_type = (data.get("type") or "").strip().lower() + + if content is None: + return jsonify({"error": "Missing content"}), 400 + + scripts_root = _scripts_root() + os.makedirs(scripts_root, exist_ok=True) + + # Determine target path + if rel_path: + # Ensure extension matches type if provided + base, ext = os.path.splitext(rel_path) + desired_ext = _ext_for_type(script_type) or ext + if desired_ext and not rel_path.lower().endswith(desired_ext): + rel_path = base + desired_ext + abs_path = os.path.abspath(os.path.join(scripts_root, rel_path)) + else: + if not name: + return jsonify({"error": "Missing name"}), 400 + desired_ext = _ext_for_type(script_type) or os.path.splitext(name)[1] or ".txt" + if not name.lower().endswith(desired_ext): + name = os.path.splitext(name)[0] + desired_ext + abs_path = os.path.abspath(os.path.join(scripts_root, os.path.basename(name))) + + if not abs_path.startswith(scripts_root): + return jsonify({"error": "Invalid path"}), 400 + + os.makedirs(os.path.dirname(abs_path), exist_ok=True) + try: + with open(abs_path, "w", encoding="utf-8", newline="\n") as fh: + fh.write(str(content)) + rel_new = os.path.relpath(abs_path, scripts_root).replace(os.sep, "/") + return jsonify({"status": "ok", "rel_path": rel_new}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/scripts/rename_file", methods=["POST"]) +def rename_script_file(): + data = request.get_json(silent=True) or {} + rel_path = (data.get("path") or "").strip() + new_name = (data.get("new_name") or "").strip() + script_type = (data.get("type") or "").strip().lower() + scripts_root = _scripts_root() + old_abs = os.path.abspath(os.path.join(scripts_root, rel_path)) + if not old_abs.startswith(scripts_root) or not os.path.isfile(old_abs): + return jsonify({"error": "File not found"}), 404 + if not new_name: + return jsonify({"error": "Invalid new_name"}), 400 + desired_ext = _ext_for_type(script_type) or os.path.splitext(new_name)[1] + if desired_ext and not new_name.lower().endswith(desired_ext): + new_name = os.path.splitext(new_name)[0] + desired_ext + new_abs = os.path.join(os.path.dirname(old_abs), os.path.basename(new_name)) + try: + os.rename(old_abs, new_abs) + rel_new = os.path.relpath(new_abs, scripts_root).replace(os.sep, "/") + return jsonify({"status": "ok", "rel_path": rel_new}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/scripts/move_file", methods=["POST"]) +def move_script_file(): + data = request.get_json(silent=True) or {} + rel_path = (data.get("path") or "").strip() + new_rel = (data.get("new_path") or "").strip() + scripts_root = _scripts_root() + old_abs = os.path.abspath(os.path.join(scripts_root, rel_path)) + new_abs = os.path.abspath(os.path.join(scripts_root, new_rel)) + if not old_abs.startswith(scripts_root) or not os.path.isfile(old_abs): + return jsonify({"error": "File not found"}), 404 + if not new_abs.startswith(scripts_root): + return jsonify({"error": "Invalid destination"}), 400 + os.makedirs(os.path.dirname(new_abs), exist_ok=True) + try: + shutil.move(old_abs, new_abs) + return jsonify({"status": "ok"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/scripts/delete_file", methods=["POST"]) +def delete_script_file(): + data = request.get_json(silent=True) or {} + rel_path = (data.get("path") or "").strip() + scripts_root = _scripts_root() + abs_path = os.path.abspath(os.path.join(scripts_root, rel_path)) + if not abs_path.startswith(scripts_root) or not os.path.isfile(abs_path): + return jsonify({"error": "File not found"}), 404 + try: + os.remove(abs_path) + return jsonify({"status": "ok"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/scripts/create_folder", methods=["POST"]) +def scripts_create_folder(): + data = request.get_json(silent=True) or {} + rel_path = (data.get("path") or "").strip() + scripts_root = _scripts_root() + abs_path = os.path.abspath(os.path.join(scripts_root, rel_path)) + if not abs_path.startswith(scripts_root): + return jsonify({"error": "Invalid path"}), 400 + try: + os.makedirs(abs_path, exist_ok=True) + return jsonify({"status": "ok"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/scripts/delete_folder", methods=["POST"]) +def scripts_delete_folder(): + data = request.get_json(silent=True) or {} + rel_path = (data.get("path") or "").strip() + scripts_root = _scripts_root() + abs_path = os.path.abspath(os.path.join(scripts_root, rel_path)) + if not abs_path.startswith(scripts_root) or not os.path.isdir(abs_path): + return jsonify({"error": "Folder not found"}), 404 + try: + shutil.rmtree(abs_path) + return jsonify({"status": "ok"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/scripts/rename_folder", methods=["POST"]) +def scripts_rename_folder(): + data = request.get_json(silent=True) or {} + rel_path = (data.get("path") or "").strip() + new_name = (data.get("new_name") or "").strip() + scripts_root = _scripts_root() + old_abs = os.path.abspath(os.path.join(scripts_root, rel_path)) + if not old_abs.startswith(scripts_root) or not os.path.isdir(old_abs): + return jsonify({"error": "Folder not found"}), 404 + if not new_name: + return jsonify({"error": "Invalid new_name"}), 400 + new_abs = os.path.join(os.path.dirname(old_abs), new_name) + try: + os.rename(old_abs, new_abs) + return jsonify({"status": "ok"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + # --------------------------------------------- # Borealis Agent API Endpoints # ---------------------------------------------