Compare commits

..

No commits in common. "a11755de5d3bb9ce516d076b453bff4faba1b58c" and "659fa2edf7e9e988f42d47bb3b9e0108b4358d13" have entirely different histories.

8 changed files with 248 additions and 354 deletions

View File

@ -10,7 +10,6 @@ import { Category } from "@/lib/types";
import { cn } from "@/lib/utils";
import { z } from "zod";
import { ScriptSchema } from "../_schemas/schemas";
import { memo } from "react";
type Script = z.infer<typeof ScriptSchema>;
@ -22,42 +21,7 @@ type CategoryProps = {
categories: Category[];
};
const CategoryTag = memo(({
category,
onRemove
}: {
category: Category;
onRemove: () => void;
}) => (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{category.name}
<button
type="button"
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
onClick={onRemove}
>
<span className="sr-only">Remove</span>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</span>
));
CategoryTag.displayName = 'CategoryTag';
function Categories({
export default function Categories({
script,
setScript,
categories,
@ -76,44 +40,64 @@ function Categories({
});
};
const categoryMap = new Map(categories.map(c => [c.id, c]));
return (
<div>
<Label>
Category <span className="text-red-500">*</span>
</Label>
<Select onValueChange={(value) => addCategory(Number(value))}>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id.toString()}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<div
className={cn(
"flex flex-wrap gap-2",
script.categories.length !== 0 && "mt-2",
)}
>
{script.categories.map((categoryId) => {
const category = categoryMap.get(categoryId);
return category ? (
<CategoryTag
key={categoryId}
category={category}
onRemove={() => removeCategory(categoryId)}
/>
) : null;
})}
<>
<div>
<Label>
Category <span className="text-red-500">*</span>
</Label>
<Select onValueChange={(value) => addCategory(Number(value))}>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id.toString()}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<div
className={cn(
"flex flex-wrap gap-2",
script.categories.length !== 0 && "mt-2",
)}
>
{script.categories.map((categoryId) => {
const category = categories.find((c) => c.id === categoryId);
return category ? (
<span
key={categoryId}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{category.name}
<button
type="button"
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
onClick={() => removeCategory(categoryId)}
>
<span className="sr-only">Remove</span>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</span>
) : null;
})}
</div>
</div>
</div>
</>
);
}
export default memo(Categories);

View File

@ -10,7 +10,6 @@ import { PlusCircle, Trash2 } from "lucide-react";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
import { memo, useCallback } from "react";
type Script = z.infer<typeof ScriptSchema>;
@ -21,13 +20,13 @@ type InstallMethodProps = {
setZodErrors: (zodErrors: z.ZodError | null) => void;
};
function InstallMethod({
export default function InstallMethod({
script,
setScript,
setIsValid,
setZodErrors,
}: InstallMethodProps) {
const addInstallMethod = useCallback(() => {
const addInstallMethod = () => {
setScript((prev) => {
const method = InstallMethodSchema.parse({
type: "default",
@ -45,9 +44,9 @@ function InstallMethod({
install_methods: [...prev.install_methods, method],
};
});
}, [setScript]);
};
const updateInstallMethod = useCallback((
const updateInstallMethod = (
index: number,
key: keyof Script["install_methods"][number],
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
@ -83,35 +82,14 @@ function InstallMethod({
}
return updated;
});
}, [setScript, setIsValid, setZodErrors]);
};
const removeInstallMethod = useCallback((index: number) => {
const removeInstallMethod = (index: number) => {
setScript((prev) => ({
...prev,
install_methods: prev.install_methods.filter((_, i) => i !== index),
}));
}, [setScript]);
const ResourceInput = memo(({
placeholder,
value,
onChange,
type = "text"
}: {
placeholder: string;
value: string | number | null;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
type?: string;
}) => (
<Input
placeholder={placeholder}
type={type}
value={value || ""}
onChange={onChange}
/>
));
ResourceInput.displayName = 'ResourceInput';
};
return (
<>
@ -131,33 +109,33 @@ function InstallMethod({
</SelectContent>
</Select>
<div className="flex gap-2">
<ResourceInput
<Input
placeholder="CPU in Cores"
type="number"
value={method.resources.cpu}
onChange={(e) =>
value={method.resources.cpu || ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
updateInstallMethod(index, "resources", {
...method.resources,
cpu: e.target.value ? Number(e.target.value) : null,
})
}
/>
<ResourceInput
<Input
placeholder="RAM in MB"
type="number"
value={method.resources.ram}
onChange={(e) =>
value={method.resources.ram || ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
updateInstallMethod(index, "resources", {
...method.resources,
ram: e.target.value ? Number(e.target.value) : null,
})
}
/>
<ResourceInput
placeholder="HDD in GB"
<Input
placeholder="HDD in GB"
type="number"
value={method.resources.hdd}
onChange={(e) =>
value={method.resources.hdd || ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
updateInstallMethod(index, "resources", {
...method.resources,
hdd: e.target.value ? Number(e.target.value) : null,
@ -166,21 +144,21 @@ function InstallMethod({
/>
</div>
<div className="flex gap-2">
<ResourceInput
<Input
placeholder="OS"
value={method.resources.os}
onChange={(e) =>
value={method.resources.os || ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
updateInstallMethod(index, "resources", {
...method.resources,
os: e.target.value || null,
})
}
/>
<ResourceInput
<Input
placeholder="Version"
type="number"
value={method.resources.version}
onChange={(e) =>
value={method.resources.version || ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
updateInstallMethod(index, "resources", {
...method.resources,
version: e.target.value ? Number(e.target.value) : null,
@ -190,7 +168,7 @@ function InstallMethod({
</div>
<Button
variant="destructive"
size="sm"
size={"sm"}
type="button"
onClick={() => removeInstallMethod(index)}
>
@ -200,7 +178,7 @@ function InstallMethod({
))}
<Button
type="button"
size="sm"
size={"sm"}
disabled={script.install_methods.length >= 2}
onClick={addInstallMethod}
>
@ -209,5 +187,3 @@ function InstallMethod({
</>
);
}
export default memo(InstallMethod);

View File

@ -12,7 +12,6 @@ import { cn } from "@/lib/utils";
import { PlusCircle, Trash2 } from "lucide-react";
import { z } from "zod";
import { ScriptSchema } from "../_schemas/schemas";
import { memo, useCallback } from "react";
type Script = z.infer<typeof ScriptSchema>;
@ -22,98 +21,95 @@ type NoteProps = {
setIsValid: (isValid: boolean) => void;
setZodErrors: (zodErrors: z.ZodError | null) => void;
};
function Note({
export default function Note({
script,
setScript,
setIsValid,
setZodErrors,
}: NoteProps) {
const addNote = useCallback(() => {
setScript({
const addNote = () => {
const newScript: Script = {
...script,
notes: [...script.notes, { text: "", type: "" }],
});
}, [script, setScript]);
};
setScript(newScript);
};
const updateNote = useCallback((
const updateNote = (
index: number,
key: keyof Script["notes"][number],
value: string,
) => {
const updated: Script = {
...script,
notes: script.notes.map((note, i) =>
notes: script.notes.map((note: Script["notes"][number], i: number) =>
i === index ? { ...note, [key]: value } : note,
),
};
const result = ScriptSchema.safeParse(updated);
setIsValid(result.success);
setZodErrors(result.success ? null : result.error);
if (!result.success) {
setZodErrors(result.error);
} else {
setZodErrors(null);
}
setScript(updated);
}, [script, setScript, setIsValid, setZodErrors]);
};
const removeNote = useCallback((index: number) => {
setScript({
const removeNote = (index: number) => {
const newScript: Script = {
...script,
notes: script.notes.filter((_, i) => i !== index),
});
}, [script, setScript]);
const NoteItem = memo(({ note, index }: { note: Script["notes"][number], index: number }) => (
<div className="space-y-2 border p-4 rounded">
<Input
placeholder="Note Text"
value={note.text}
onChange={(e) => updateNote(index, "text", e.target.value)}
/>
<Select
value={note.type}
onValueChange={(value) => updateNote(index, "type", value)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
{Object.keys(AlertColors).map((type) => (
<SelectItem key={type} value={type}>
<span className="flex items-center gap-2">
{type.charAt(0).toUpperCase() + type.slice(1)}{" "}
<div
className={cn(
"size-4 rounded-full border",
AlertColors[type as keyof typeof AlertColors],
)}
/>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
variant="destructive"
type="button"
onClick={() => removeNote(index)}
>
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
</Button>
</div>
));
NoteItem.displayName = 'NoteItem';
notes: script.notes.filter((_: Script["notes"][number], i: number) => i !== index),
};
setScript(newScript);
};
return (
<>
<h3 className="text-xl font-semibold">Notes</h3>
{script.notes.map((note, index) => (
<NoteItem key={index} note={note} index={index} />
<div key={index} className="space-y-2 border p-4 rounded">
<Input
placeholder="Note Text"
value={note.text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateNote(index, "text", e.target.value)}
/>
<Select
value={note.type}
onValueChange={(value: string) => updateNote(index, "type", value)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
{Object.keys(AlertColors).map((type) => (
<SelectItem key={type} value={type}>
<span className="flex items-center gap-2">
{type.charAt(0).toUpperCase() + type.slice(1)}{" "}
<div
className={cn(
"size-4 rounded-full border",
AlertColors[type as keyof typeof AlertColors],
)}
/>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size={"sm"}
variant="destructive"
type="button"
onClick={() => removeNote(index)}
>
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
</Button>
</div>
))}
<Button type="button" size="sm" onClick={addNote}>
<Button type="button" size={"sm"} onClick={addNote}>
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
</Button>
</>
);
}
export default memo(Note);

View File

@ -20,7 +20,7 @@ import { Category } from "@/lib/types";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { CalendarIcon, Check, Clipboard } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { z } from "zod";
import Categories from "./_components/Categories";
@ -30,98 +30,66 @@ import { ScriptSchema } from "./_schemas/schemas";
type Script = z.infer<typeof ScriptSchema>;
const initialScript: Script = {
name: "",
slug: "",
categories: [],
date_created: "",
type: "ct",
updateable: false,
privileged: false,
interface_port: null,
documentation: null,
website: null,
logo: null,
description: "",
install_methods: [],
default_credentials: {
username: null,
password: null,
},
notes: [],
};
export default function JSONGenerator() {
const [script, setScript] = useState<Script>(initialScript);
const [script, setScript] = useState<Script>({
name: "",
slug: "",
categories: [],
date_created: "",
type: "ct",
updateable: false,
privileged: false,
interface_port: null,
documentation: null,
website: null,
logo: null,
description: "",
install_methods: [],
default_credentials: {
username: null,
password: null,
},
notes: [],
});
const [isCopied, setIsCopied] = useState(false);
const [isValid, setIsValid] = useState(false);
const [categories, setCategories] = useState<Category[]>([]);
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
useEffect(() => {
fetchCategories()
.then(setCategories)
.then((data) => {
setCategories(data);
})
.catch((error) => console.error("Error fetching categories:", error));
}, []);
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
const updateScript = (key: keyof Script, value: Script[keyof Script]) => {
setScript((prev) => {
const updated = { ...prev, [key]: value };
// Update script paths for install methods if `type` or `slug` changed
if (key === "type" || key === "slug") {
updated.install_methods = updated.install_methods.map((method) => ({
...method,
script: method.type === "alpine"
? `/${updated.type}/alpine-${updated.slug}.sh`
: `/${updated.type}/${updated.slug}.sh`,
script:
method.type === "alpine"
? `/${updated.type}/alpine-${updated.slug}.sh`
: `/${updated.type}/${updated.slug}.sh`,
}));
}
const result = ScriptSchema.safeParse(updated);
setIsValid(result.success);
setZodErrors(result.success ? null : result.error);
if (!result.success) {
setZodErrors(result.error);
} else {
setZodErrors(null);
}
return updated;
});
}, []);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
toast.success("Copied metadata to clipboard");
}, [script]);
const handleDateSelect = useCallback((date: Date | undefined) => {
updateScript(
"date_created",
format(date || new Date(), "yyyy-MM-dd")
);
}, [updateScript]);
const formattedDate = useMemo(() =>
script.date_created ? format(script.date_created, "PPP") : undefined,
[script.date_created]
);
const validationAlert = useMemo(() => (
<Alert className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}>
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
<AlertDescription>
{isValid
? "The current JSON is valid according to the schema."
: "The current JSON does not match the required schema."}
</AlertDescription>
{zodErrors && (
<div className="mt-2 space-y-1">
{zodErrors.errors.map((error, index) => (
<AlertDescription key={index} className="p-1 text-red-500">
{error.path.join(".")} - {error.message}
</AlertDescription>
))}
</div>
)}
</Alert>
), [isValid, zodErrors]);
};
return (
<div className="flex h-screen mt-20">
@ -187,7 +155,11 @@ export default function JSONGenerator() {
!script.date_created && "text-muted-foreground",
)}
>
{formattedDate || <span>Pick a date</span>}
{script.date_created ? (
format(script.date_created, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
@ -195,7 +167,12 @@ export default function JSONGenerator() {
<Calendar
mode="single"
selected={new Date(script.date_created)}
onSelect={handleDateSelect}
onSelect={(date) =>
updateScript(
"date_created",
format(date || new Date(), "yyyy-MM-dd"),
)
}
initialFocus
/>
</PopoverContent>
@ -222,14 +199,18 @@ export default function JSONGenerator() {
<div className="flex items-center space-x-2">
<Switch
checked={script.updateable}
onCheckedChange={(checked) => updateScript("updateable", checked)}
onCheckedChange={(checked) =>
updateScript("updateable", checked)
}
/>
<label>Updateable</label>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={script.privileged}
onCheckedChange={(checked) => updateScript("privileged", checked)}
onCheckedChange={(checked) =>
updateScript("privileged", checked)
}
/>
<label>Privileged</label>
</div>
@ -238,7 +219,12 @@ export default function JSONGenerator() {
placeholder="Interface Port"
type="number"
value={script.interface_port || ""}
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
onChange={(e) =>
updateScript(
"interface_port",
e.target.value ? Number(e.target.value) : null,
)
}
/>
<div className="flex gap-2">
<Input
@ -249,7 +235,9 @@ export default function JSONGenerator() {
<Input
placeholder="Documentation URL"
value={script.documentation || ""}
onChange={(e) => updateScript("documentation", e.target.value || null)}
onChange={(e) =>
updateScript("documentation", e.target.value || null)
}
/>
</div>
<InstallMethod
@ -262,18 +250,22 @@ export default function JSONGenerator() {
<Input
placeholder="Username"
value={script.default_credentials.username || ""}
onChange={(e) => updateScript("default_credentials", {
...script.default_credentials,
username: e.target.value || null,
})}
onChange={(e) =>
updateScript("default_credentials", {
...script.default_credentials,
username: e.target.value || null,
})
}
/>
<Input
placeholder="Password"
value={script.default_credentials.password || ""}
onChange={(e) => updateScript("default_credentials", {
...script.default_credentials,
password: e.target.value || null,
})}
onChange={(e) =>
updateScript("default_credentials", {
...script.default_credentials,
password: e.target.value || null,
})
}
/>
<Note
script={script}
@ -284,13 +276,36 @@ export default function JSONGenerator() {
</form>
</div>
<div className="w-1/2 p-4 bg-background overflow-y-auto">
{validationAlert}
<Alert
className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}
>
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
<AlertDescription>
{isValid
? "The current JSON is valid according to the schema."
: "The current JSON does not match the required schema."}
</AlertDescription>
{zodErrors && (
<div className="mt-2 space-y-1">
{zodErrors.errors.map((error, index) => (
<AlertDescription key={index} className="p-1 text-red-500">
{error.path.join(".")} - {error.message}
</AlertDescription>
))}
</div>
)}
</Alert>
<div className="relative">
<Button
className="absolute right-2 top-2"
size="icon"
variant="outline"
onClick={handleCopy}
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
toast.success("Copied metadata to clipboard");
}}
>
{isCopied ? (
<Check className="h-4 w-4" />

View File

@ -28,41 +28,6 @@ $STD apt-get update
$STD apt-get install -y cloudflared
msg_ok "Installed Cloudflared"
read -r -p "Would you like to configure cloudflared as a DNS-over-HTTPS (DoH) proxy? <y/N> " prompt
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
msg_info "Creating Service"
cat <<EOF >/usr/local/etc/cloudflared/config.yml
proxy-dns: true
proxy-dns-address: 0.0.0.0
proxy-dns-port: 53
proxy-dns-max-upstream-conns: 5
proxy-dns-upstream:
- https://1.1.1.1/dns-query
- https://1.0.0.1/dns-query
#- https://8.8.8.8/dns-query
#- https://8.8.4.4/dns-query
#- https://9.9.9.9/dns-query
#- https://149.112.112.112/dns-query
EOF
cat <<EOF >/etc/systemd/system/cloudflared.service
[Unit]
Description=cloudflared DNS-over-HTTPS (DoH) proxy
After=syslog.target network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/cloudflared --config /usr/local/etc/cloudflared/config.yml
Restart=on-failure
RestartSec=10
KillMode=process
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now cloudflared.service
msg_ok "Created Service"
fi
motd_ssh
customize

View File

@ -9,7 +9,7 @@
"updateable": false,
"privileged": false,
"interface_port": null,
"documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/",
"documentation": null,
"website": "https://www.cloudflare.com/",
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/cloudflare.svg",
"description": "Cloudflared is a command-line tool that allows you to securely access resources on the Cloudflare network, such as websites and APIs, from your local computer. It works by creating a secure tunnel between your computer and the Cloudflare network, allowing you to access resources as if they were on your local network.",
@ -30,10 +30,5 @@
"username": null,
"password": null
},
"notes": [
{
"text": "With an option to configure cloudflared as a DNS-over-HTTPS (DoH) proxy",
"type": "info"
}
]
}
"notes": []
}

View File

@ -9,8 +9,8 @@
"updateable": true,
"privileged": false,
"interface_port": null,
"documentation": "https://dev.mysql.com/doc/",
"website": "https://www.mysql.com/",
"documentation": null,
"website": null,
"logo": "https://1000logos.net/wp-content/uploads/2020/08/MySQL-Logo.png",
"description": "MySQL is an open-source relational database management system (RDBMS) that uses SQL for managing and manipulating data. It is known for its scalability, reliability, and high performance, making it suitable for small to large-scale applications. Key features include support for ACID transactions, data replication for high availability, and compatibility with various programming languages like Python, PHP, and Java.",
"install_methods": [
@ -34,10 +34,6 @@
{
"text": "Database credentials: `cat mysql.creds`",
"type": "warning"
},
{
"text": "With an option to install the MySQL 8.4 LTS release instead of MySQL 8.0",
"type": "info"
}
]
}
}

View File

@ -7,7 +7,6 @@ variables() {
# This function sets various color variables using ANSI escape codes for formatting text in the terminal.
color() {
YW=$(echo "\033[33m")
YWB=$(echo "\033[93m")
BL=$(echo "\033[36m")
RD=$(echo "\033[01;31m")
BGN=$(echo "\033[4;92m")
@ -502,38 +501,6 @@ install_script() {
fi
}
check_container_resources() {
# Check actual RAM & Cores
current_ram=$(free -m | awk '/^Mem:/{print $2}')
current_cpu=$(nproc)
# Check whether the current RAM is less than the required RAM or the CPU cores are less than required
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
echo -e "\n⚠${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
exit 1
else
echo -e ""
fi
}
check_container_storage() {
# Check if the /boot partition is more than 80% full
total_size=$(df /boot --output=size | tail -n 1)
local used_size=$(df /boot --output=used | tail -n 1)
usage=$(( 100 * used_size / total_size ))
if (( usage > 80 )); then
# Prompt the user for confirmation to continue
echo -e "⚠️${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
read -r -p "Continue anyway? <y/N> " prompt
# Check if the input is 'y' or 'yes', otherwise exit with status 1
if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
echo -e "❌${HOLD} ${YWB}Exiting based on user input.${CL}"
exit 1
fi
fi
}
start() {
if command -v pveversion >/dev/null 2>&1; then
if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC" --yesno "This will create a New ${APP} LXC. Proceed?" 10 58); then