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)}
+
+
+
+
+
+ {/* 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
# ---------------------------------------------