production ready?

This commit is contained in:
fccview
2025-08-18 14:06:20 +01:00
parent f5e88e6aad
commit 08cf574902
7 changed files with 211 additions and 128 deletions

View File

@@ -1,28 +1,18 @@
"use server";
import { revalidatePath } from "next/cache";
import { writeFile, readFile, unlink, mkdir, readdir } from "fs/promises";
import { writeFile, readFile, unlink, mkdir } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
import { exec } from "child_process";
import { promisify } from "util";
import { SCRIPTS_DIR } from "@/app/_utils/scripts";
import { loadAllScripts, type Script } from "@/app/_utils/scriptScanner";
const execAsync = promisify(exec);
export interface Script {
id: string;
name: string;
description: string;
createdAt: string;
filename: string;
}
const SCRIPTS_METADATA_FILE = join(
process.cwd(),
"data",
"scripts-metadata.json"
);
// Re-export the Script interface for components
export type { Script } from "@/app/_utils/scriptScanner";
function sanitizeScriptName(name: string): string {
return name
@@ -35,13 +25,12 @@ function sanitizeScriptName(name: string): string {
}
async function generateUniqueFilename(baseName: string): Promise<string> {
const scripts = await readScriptsMetadata();
const sanitizedName = sanitizeScriptName(baseName);
let filename = `${sanitizedName}.sh`;
const scripts = await loadAllScripts();
let filename = `${sanitizeScriptName(baseName)}.sh`;
let counter = 1;
while (scripts.some((script) => script.filename === filename)) {
filename = `${sanitizedName}-${counter}.sh`;
filename = `${sanitizeScriptName(baseName)}-${counter}.sh`;
counter++;
}
@@ -52,15 +41,6 @@ async function ensureScriptsDirectory() {
if (!existsSync(SCRIPTS_DIR)) {
await mkdir(SCRIPTS_DIR, { recursive: true });
}
const dataDir = join(process.cwd(), "data");
if (!existsSync(dataDir)) {
await mkdir(dataDir, { recursive: true });
}
if (!existsSync(SCRIPTS_METADATA_FILE)) {
await writeFile(SCRIPTS_METADATA_FILE, JSON.stringify([], null, 2));
}
}
async function ensureHostScriptsDirectory() {
@@ -70,40 +50,10 @@ async function ensureHostScriptsDirectory() {
}
}
async function readScriptsMetadata(): Promise<Script[]> {
await ensureScriptsDirectory();
try {
const data = await readFile(SCRIPTS_METADATA_FILE, "utf8");
return JSON.parse(data);
} catch (error) {
console.error("Error reading scripts metadata:", error);
return [];
}
}
async function writeScriptsMetadata(scripts: Script[]) {
await ensureScriptsDirectory();
await writeFile(SCRIPTS_METADATA_FILE, JSON.stringify(scripts, null, 2));
}
async function saveScriptFile(filename: string, content: string) {
await ensureScriptsDirectory();
await ensureHostScriptsDirectory();
const scriptPath = join(SCRIPTS_DIR, filename);
const scriptContent = content.startsWith("#!/")
? content
: `#!/bin/bash\n${content}`;
await writeFile(scriptPath, scriptContent, "utf8");
try {
await execAsync(`chmod +x "${scriptPath}"`);
} catch (error) {
console.error("Error making script executable:", error);
}
await writeFile(scriptPath, content, "utf8");
}
async function deleteScriptFile(filename: string) {
@@ -114,7 +64,7 @@ async function deleteScriptFile(filename: string) {
}
export async function fetchScripts(): Promise<Script[]> {
return await readScriptsMetadata();
return await loadAllScripts();
}
export async function createScript(
@@ -129,26 +79,32 @@ export async function createScript(
return { success: false, message: "Name and content are required" };
}
const scripts = await readScriptsMetadata();
const scriptId = `script_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
const filename = await generateUniqueFilename(name);
// Create metadata header
const metadataHeader = `# @id: ${scriptId}
# @title: ${name}
# @description: ${description || ""}
`;
const fullContent = metadataHeader + content;
await saveScriptFile(filename, fullContent);
revalidatePath("/");
// Return the created script
const newScript: Script = {
id: scriptId,
name,
description: description || "",
createdAt: new Date().toISOString(),
filename,
createdAt: new Date().toISOString(),
};
await saveScriptFile(filename, content);
scripts.push(newScript);
await writeScriptsMetadata(scripts);
revalidatePath("/");
return {
success: true,
message: "Script created successfully",
@@ -170,27 +126,26 @@ export async function updateScript(
const content = formData.get("content") as string;
if (!id || !name || !content) {
return { success: false, message: "ID, name, and content are required" };
return { success: false, message: "ID, name and content are required" };
}
const scripts = await readScriptsMetadata();
const scriptIndex = scripts.findIndex((s) => s.id === id);
const scripts = await loadAllScripts();
const existingScript = scripts.find((s) => s.id === id);
if (scriptIndex === -1) {
if (!existingScript) {
return { success: false, message: "Script not found" };
}
const oldScript = scripts[scriptIndex];
// Update metadata header
const metadataHeader = `# @id: ${id}
# @title: ${name}
# @description: ${description || ""}
await saveScriptFile(oldScript.filename, content);
`;
scripts[scriptIndex] = {
...oldScript,
name,
description: description || "",
};
const fullContent = metadataHeader + content;
await writeScriptsMetadata(scripts);
await saveScriptFile(existingScript.filename, fullContent);
revalidatePath("/");
return { success: true, message: "Script updated successfully" };
@@ -204,17 +159,14 @@ export async function deleteScript(
id: string
): Promise<{ success: boolean; message: string }> {
try {
const scripts = await readScriptsMetadata();
const scriptToDelete = scripts.find((s) => s.id === id);
const scripts = await loadAllScripts();
const script = scripts.find((s) => s.id === id);
if (!scriptToDelete) {
if (!script) {
return { success: false, message: "Script not found" };
}
await deleteScriptFile(scriptToDelete.filename);
const filteredScripts = scripts.filter((s) => s.id !== id);
await writeScriptsMetadata(filteredScripts);
await deleteScriptFile(script.filename);
revalidatePath("/");
return { success: true, message: "Script deleted successfully" };
@@ -229,34 +181,41 @@ export async function cloneScript(
newName: string
): Promise<{ success: boolean; message: string; script?: Script }> {
try {
const scripts = await readScriptsMetadata();
const scripts = await loadAllScripts();
const originalScript = scripts.find((s) => s.id === id);
if (!originalScript) {
return { success: false, message: "Script not found" };
}
const content = await getScriptContent(originalScript.filename);
const scriptId = `script_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
const filename = await generateUniqueFilename(newName);
// Get the original content
const originalContent = await getScriptContent(originalScript.filename);
// Create metadata header for cloned script
const metadataHeader = `# @id: ${scriptId}
# @title: ${newName}
# @description: ${originalScript.description}
`;
const fullContent = metadataHeader + originalContent;
await saveScriptFile(filename, fullContent);
revalidatePath("/");
const newScript: Script = {
id: scriptId,
name: newName,
description: originalScript.description,
createdAt: new Date().toISOString(),
filename,
createdAt: new Date().toISOString(),
};
await saveScriptFile(filename, content);
scripts.push(newScript);
await writeScriptsMetadata(scripts);
revalidatePath("/");
return {
success: true,
message: "Script cloned successfully",
@@ -272,7 +231,24 @@ export async function getScriptContent(filename: string): Promise<string> {
try {
const scriptPath = join(SCRIPTS_DIR, filename);
if (existsSync(scriptPath)) {
return await readFile(scriptPath, "utf8");
const content = await readFile(scriptPath, "utf8");
// Extract content without metadata
const lines = content.split("\n");
const contentLines: string[] = [];
let inMetadata = true;
for (const line of lines) {
if (line.trim().startsWith("# @")) {
continue;
}
if (line.trim() === "" && inMetadata) {
continue;
}
inMetadata = false;
contentLines.push(line);
}
return contentLines.join("\n").trim();
}
return "";
} catch (error) {
@@ -280,3 +256,38 @@ export async function getScriptContent(filename: string): Promise<string> {
return "";
}
}
export async function executeScript(filename: string): Promise<{
success: boolean;
output: string;
error: string;
}> {
try {
await ensureHostScriptsDirectory();
const hostScriptPath = join(process.cwd(), "scripts", filename);
if (!existsSync(hostScriptPath)) {
return {
success: false,
output: "",
error: "Script file not found",
};
}
const { stdout, stderr } = await execAsync(`bash "${hostScriptPath}"`, {
timeout: 30000, // 30 second timeout
});
return {
success: true,
output: stdout,
error: stderr,
};
} catch (error: any) {
return {
success: false,
output: "",
error: error.message || "Unknown error",
};
}
}

View File

@@ -9,7 +9,6 @@ export interface CronExplanation {
export function parseCronExpression(expression: string): CronExplanation {
try {
// Clean the expression
const cleanExpression = expression.trim();
if (!cleanExpression) {
@@ -21,14 +20,11 @@ export function parseCronExpression(expression: string): CronExplanation {
};
}
// Get human readable description
const humanReadable = cronstrue.toString(cleanExpression, {
verbose: true,
throwExceptionOnParseError: false,
});
// For now, just return the human readable description
// We can add next run times later when we fix the parser
return {
humanReadable,
nextRuns: [],
@@ -44,7 +40,6 @@ export function parseCronExpression(expression: string): CronExplanation {
}
}
// Common cron patterns with explanations
export const cronPatterns = [
{
category: "Common Intervals",

View File

@@ -0,0 +1,88 @@
import { promises as fs } from "fs";
import path from "path";
export interface Script {
id: string;
name: string;
description: string;
createdAt: string;
filename: string;
}
interface ScriptMetadata {
id?: string;
title?: string;
description?: string;
}
function parseMetadata(content: string): ScriptMetadata {
const metadata: ScriptMetadata = {};
const lines = content.split("\n");
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith("# @id:")) {
metadata.id = trimmedLine.substring(6).trim();
} else if (trimmedLine.startsWith("# @title:")) {
metadata.title = trimmedLine.substring(9).trim();
} else if (trimmedLine.startsWith("# @description:")) {
metadata.description = trimmedLine.substring(15).trim();
}
}
return metadata;
}
async function scanScriptsDirectory(dirPath: string): Promise<Script[]> {
const scripts: Script[] = [];
try {
const files = await fs.readdir(dirPath);
for (const file of files) {
if (file.endsWith(".sh")) {
const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, "utf-8");
const metadata = parseMetadata(content);
if (metadata.id && metadata.title && metadata.description) {
const stats = await fs.stat(filePath);
scripts.push({
id: metadata.id,
name: metadata.title,
description: metadata.description,
filename: file,
createdAt: stats.birthtime.toISOString(),
});
}
}
}
} catch (error) {
console.warn(`Warning: Could not scan directory ${dirPath}:`, error);
}
return scripts;
}
export async function loadAllScripts(): Promise<Script[]> {
const scriptsDir = path.join(process.cwd(), "scripts");
return await scanScriptsDirectory(scriptsDir);
}
export function searchScripts(scripts: Script[], query: string): Script[] {
const lowercaseQuery = query.toLowerCase();
return scripts.filter(
(script) =>
script.name.toLowerCase().includes(lowercaseQuery) ||
script.description.toLowerCase().includes(lowercaseQuery)
);
}
export function getScriptById(
scripts: Script[],
id: string
): Script | undefined {
return scripts.find((script) => script.id === id);
}

View File

@@ -1,23 +0,0 @@
[
{
"id": "script_1755518122712_bakgyx86a",
"name": "Logs Hello World",
"description": "This script logs a \"hello world\" to test the cronjob functionality",
"createdAt": "2025-08-18T11:55:22.713Z",
"filename": "test.sh"
},
{
"id": "script_1755521191146_rcwo406sv",
"name": "Backup with rsync",
"description": "Creates a backup using rsync",
"createdAt": "2025-08-18T12:46:31.148Z",
"filename": "backup-with-rsync.sh"
},
{
"id": "script_1755521222411_bfu13ip4b",
"name": "Clear backups",
"description": "Clears old backups after a successful job",
"createdAt": "2025-08-18T12:47:02.411Z",
"filename": "clear-backups.sh"
}
]

View File

@@ -1,8 +1,12 @@
#!/bin/bash
# @id: backup-with-rsync
# @title: Backup with rsync
# @description: Creates a backup using rsync
#!/bin/bash
# Backup source directory to destination
# Change SOURCE_DIR and DEST_DIR to your paths
SOURCE_DIR="/path/to/source"
DEST_DIR="/path/to/backup"
DEST_DIR="/path/to/backuppp"
rsync -av --progress --delete \
--exclude='*.tmp' \

View File

@@ -1,4 +1,8 @@
#!/bin/bash
# @id: clear-backups
# @title: Clear backups
# @description: Clears old backups after a successful job
#!/bin/bash
# Clean old backups
# Remove backup files older than 30 days

View File

@@ -1,3 +1,7 @@
# @id: test
# @title: Logs Hello World
# @description: This script logs a "hello world" to test the cronjob functionality
#!/bin/bash
# Your script here
echo 'Hello World' > hello.txt