From 2ba9cdc6223044da9bcc04888809ae9b4fabf51b Mon Sep 17 00:00:00 2001 From: fccview Date: Wed, 5 Nov 2025 19:52:20 +0000 Subject: [PATCH] remove the annoying HOST_PROJECT_DIR and DOCKER env variables --- .../Cronjobs/CronJobList.tsx | 3 +- .../Layout/TabbedInterface.tsx | 2 +- .../Modals/CloneScriptModal.tsx | 2 +- .../Modals/DeleteScriptModal.tsx | 2 +- .../Modals/EditScriptModal.tsx | 2 +- .../Modals/SelectScriptModal.tsx | 2 +- .../Scripts/ScriptsManager.tsx | 2 +- app/_consts/file.ts | 5 +++ app/_consts/nsenter.ts | 3 ++ app/_server/actions/cronjobs/index.ts | 26 ++++++++++++--- app/_server/actions/global/index.ts | 20 ++++++++++++ app/_server/actions/scripts/index.ts | 30 ++++++++--------- app/_utils/cron/files-manipulation.ts | 9 +++--- app/_utils/scriptScanner.ts | 5 +-- app/_utils/scripts.ts | 24 ++++---------- app/_utils/snippetScanner.ts | 5 +-- app/_utils/system/cron.ts | 9 +++--- app/_utils/system/hostCrontab.ts | 32 ++++++------------- package.json | 2 +- scripts/demo-script.sh | 2 +- yarn.lock | 8 ++--- 21 files changed, 107 insertions(+), 88 deletions(-) create mode 100644 app/_consts/file.ts create mode 100644 app/_consts/nsenter.ts create mode 100644 app/_server/actions/global/index.ts diff --git a/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx b/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx index 8e3d134..006840e 100644 --- a/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx +++ b/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx @@ -22,7 +22,8 @@ import { CloneTaskModal } from "@/app/_components/FeatureComponents/Modals/Clone import { UserFilter } from "@/app/_components/FeatureComponents/User/UserFilter"; import { ErrorBadge } from "@/app/_components/GlobalComponents/Badges/ErrorBadge"; import { ErrorDetailsModal } from "@/app/_components/FeatureComponents/Modals/ErrorDetailsModal"; -import { type Script } from "@/app/_server/actions/scripts"; +import { Script } from "@/app/_utils/scriptScanner"; + import { getJobErrorsByJobId, JobError, diff --git a/app/_components/FeatureComponents/Layout/TabbedInterface.tsx b/app/_components/FeatureComponents/Layout/TabbedInterface.tsx index b9355b4..0046f48 100644 --- a/app/_components/FeatureComponents/Layout/TabbedInterface.tsx +++ b/app/_components/FeatureComponents/Layout/TabbedInterface.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { CronJobList } from "@/app/_components/FeatureComponents/Cronjobs/CronJobList"; import { ScriptsManager } from "@/app/_components/FeatureComponents/Scripts/ScriptsManager"; import { CronJob } from "@/app/_utils/system"; -import { type Script } from "@/app/_server/actions/scripts"; +import { Script } from "@/app/_utils/scriptScanner"; import { Clock, FileText } from "lucide-react"; interface TabbedInterfaceProps { diff --git a/app/_components/FeatureComponents/Modals/CloneScriptModal.tsx b/app/_components/FeatureComponents/Modals/CloneScriptModal.tsx index 4a7b3a3..20beaa1 100644 --- a/app/_components/FeatureComponents/Modals/CloneScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/CloneScriptModal.tsx @@ -5,7 +5,7 @@ import { Copy } from "lucide-react"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Input } from "@/app/_components/GlobalComponents/FormElements/Input"; -import { type Script } from "@/app/_server/actions/scripts"; +import { Script } from "@/app/_utils/scriptScanner"; interface CloneScriptModalProps { script: Script | null; diff --git a/app/_components/FeatureComponents/Modals/DeleteScriptModal.tsx b/app/_components/FeatureComponents/Modals/DeleteScriptModal.tsx index e131c1c..f043fc7 100644 --- a/app/_components/FeatureComponents/Modals/DeleteScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/DeleteScriptModal.tsx @@ -3,7 +3,7 @@ import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { FileText, AlertCircle, Trash2 } from "lucide-react"; -import { type Script } from "@/app/_server/actions/scripts"; +import { Script } from "@/app/_utils/scriptScanner"; interface DeleteScriptModalProps { script: Script | null; diff --git a/app/_components/FeatureComponents/Modals/EditScriptModal.tsx b/app/_components/FeatureComponents/Modals/EditScriptModal.tsx index aed2ecf..f8a52fb 100644 --- a/app/_components/FeatureComponents/Modals/EditScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/EditScriptModal.tsx @@ -1,7 +1,7 @@ "use client"; import { Edit } from "lucide-react"; -import { type Script } from "@/app/_server/actions/scripts"; +import { Script } from "@/app/_utils/scriptScanner"; import { ScriptModal } from "@/app/_components/FeatureComponents/Modals/ScriptModal"; interface EditScriptModalProps { diff --git a/app/_components/FeatureComponents/Modals/SelectScriptModal.tsx b/app/_components/FeatureComponents/Modals/SelectScriptModal.tsx index ed079f7..0e8e65f 100644 --- a/app/_components/FeatureComponents/Modals/SelectScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/SelectScriptModal.tsx @@ -5,7 +5,7 @@ import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { Input } from "@/app/_components/GlobalComponents/FormElements/Input"; import { FileText, Search, Check, Terminal } from "lucide-react"; -import { type Script } from "@/app/_server/actions/scripts"; +import { Script } from "@/app/_utils/scriptScanner"; import { getScriptContent } from "@/app/_server/actions/scripts"; import { getHostScriptPath } from "@/app/_utils/scripts"; diff --git a/app/_components/FeatureComponents/Scripts/ScriptsManager.tsx b/app/_components/FeatureComponents/Scripts/ScriptsManager.tsx index e44f135..81b89e6 100644 --- a/app/_components/FeatureComponents/Scripts/ScriptsManager.tsx +++ b/app/_components/FeatureComponents/Scripts/ScriptsManager.tsx @@ -13,7 +13,7 @@ import { CheckCircle, Files, } from "lucide-react"; -import { type Script } from "@/app/_server/actions/scripts"; +import { Script } from "@/app/_utils/scriptScanner"; import { createScript, updateScript, diff --git a/app/_consts/file.ts b/app/_consts/file.ts new file mode 100644 index 0000000..0874d43 --- /dev/null +++ b/app/_consts/file.ts @@ -0,0 +1,5 @@ +import path from "path"; + +export const SCRIPTS_DIR = path.join("scripts"); +export const SNIPPETS_DIR = path.join("snippets"); +export const DATA_DIR = path.join("data"); diff --git a/app/_consts/nsenter.ts b/app/_consts/nsenter.ts new file mode 100644 index 0000000..bcd8bd3 --- /dev/null +++ b/app/_consts/nsenter.ts @@ -0,0 +1,3 @@ +export const NSENTER_RUN_JOB = (executionUser: string, escapedCommand: string) => `nsenter -t 1 -m -u -i -n -p su - ${executionUser} -c '${escapedCommand}'`; + +export const NSENTER_HOST_CRONTAB = (command: string) => `nsenter -t 1 -m -u -i -n -p sh -c "${command}"`; \ No newline at end of file diff --git a/app/_server/actions/cronjobs/index.ts b/app/_server/actions/cronjobs/index.ts index 6c5d5e6..0b4d661 100644 --- a/app/_server/actions/cronjobs/index.ts +++ b/app/_server/actions/cronjobs/index.ts @@ -18,6 +18,8 @@ import { revalidatePath } from "next/cache"; import { getScriptPath } from "@/app/_utils/scripts"; import { exec } from "child_process"; import { promisify } from "util"; +import { isDocker } from "@/app/_server/actions/global"; +import { NSENTER_RUN_JOB } from "@/app/_consts/nsenter"; const execAsync = promisify(exec); @@ -260,11 +262,25 @@ export const runCronJob = async ( return { success: false, message: "Cannot run paused cron job" }; } - const userInfo = await getUserInfo(job.user); - const executionUser = userInfo ? userInfo.username : "root"; - const escapedCommand = job.command.replace(/'/g, "'\\''"); + let command: string; + const docker = await isDocker(); - const command = `nsenter -t 1 -m -u -i -n -p su - ${executionUser} -c '${escapedCommand}'`; + if (docker) { + const userInfo = await getUserInfo(job.user); + const executionUser = userInfo ? userInfo.username : "root"; + const escapedCommand = job.command.replace(/'/g, "'\\''"); + + command = NSENTER_RUN_JOB(executionUser, escapedCommand); + } else { + command = job.command; + + const appUser = process.env.USER || "unknown"; + if (job.user !== appUser) { + console.warn( + `[Native Mode] Running job ${job.id} as current user (${appUser}) instead of target user (${job.user}).` + ); + } + } const { stdout, stderr } = await execAsync(command, { timeout: 30000, @@ -285,7 +301,7 @@ export const runCronJob = async ( return { success: false, message: "Failed to execute cron job", - output: errorMessage, + output: errorMessage.trim(), details: error.stack, }; } diff --git a/app/_server/actions/global/index.ts b/app/_server/actions/global/index.ts new file mode 100644 index 0000000..e3aba77 --- /dev/null +++ b/app/_server/actions/global/index.ts @@ -0,0 +1,20 @@ +"use server"; + +import { existsSync, readFileSync } from "fs"; + +export const isDocker = async (): Promise => { + try { + if (existsSync("/.dockerenv")) { + return true; + } + + if (existsSync("/proc/1/cgroup")) { + const cgroupContent = readFileSync("/proc/1/cgroup", "utf8"); + return cgroupContent.includes("/docker/"); + } + + return false; + } catch (error) { + return false; + } +}; \ No newline at end of file diff --git a/app/_server/actions/scripts/index.ts b/app/_server/actions/scripts/index.ts index 0377d88..e5c2dee 100644 --- a/app/_server/actions/scripts/index.ts +++ b/app/_server/actions/scripts/index.ts @@ -6,13 +6,13 @@ import { join } from "path"; import { existsSync } from "fs"; import { exec } from "child_process"; import { promisify } from "util"; -import { SCRIPTS_DIR, normalizeLineEndings } from "@/app/_utils/scripts"; -import { loadAllScripts, type Script } from "@/app/_utils/scriptScanner"; +import { normalizeLineEndings } from "@/app/_utils/scripts"; +import { SCRIPTS_DIR } from "@/app/_consts/file"; +import { loadAllScripts, Script } from "@/app/_utils/scriptScanner"; +import { isDocker } from "@/app/_server/actions/global"; const execAsync = promisify(exec); -export type { Script } from "@/app/_utils/scriptScanner"; - const sanitizeScriptName = (name: string): string => { return name .toLowerCase() @@ -37,24 +37,22 @@ const generateUniqueFilename = async (baseName: string): Promise => { }; const ensureScriptsDirectory = async () => { - const scriptsDir = await SCRIPTS_DIR(); + const scriptsDir = join(process.cwd(), SCRIPTS_DIR); if (!existsSync(scriptsDir)) { await mkdir(scriptsDir, { recursive: true }); } }; const ensureHostScriptsDirectory = async () => { - const hostProjectDir = process.env.HOST_PROJECT_DIR || process.cwd(); - - const hostScriptsDir = join(hostProjectDir, "scripts"); + const hostScriptsDir = join(process.cwd(), SCRIPTS_DIR); if (!existsSync(hostScriptsDir)) { await mkdir(hostScriptsDir, { recursive: true }); } }; const saveScriptFile = async (filename: string, content: string) => { - const isDocker = process.env.DOCKER === "true"; - const scriptsDir = isDocker ? "/app/scripts" : await SCRIPTS_DIR(); + const docker = await isDocker(); + const scriptsDir = docker ? "/app/scripts" : join(process.cwd(), SCRIPTS_DIR); await ensureScriptsDirectory(); const scriptPath = join(scriptsDir, filename); @@ -68,8 +66,8 @@ const saveScriptFile = async (filename: string, content: string) => { }; const deleteScriptFile = async (filename: string) => { - const isDocker = process.env.DOCKER === "true"; - const scriptsDir = isDocker ? "/app/scripts" : await SCRIPTS_DIR(); + const docker = await isDocker(); + const scriptsDir = docker ? "/app/scripts" : join(process.cwd(), SCRIPTS_DIR); const scriptPath = join(scriptsDir, filename); if (existsSync(scriptPath)) { await unlink(scriptPath); @@ -240,8 +238,8 @@ export const cloneScript = async ( export const getScriptContent = async (filename: string): Promise => { try { - const isDocker = process.env.DOCKER === "true"; - const scriptPath = isDocker + const docker = await isDocker(); + const scriptPath = docker ? join("/app/scripts", filename) : join(process.cwd(), "scripts", filename); @@ -280,8 +278,8 @@ export const executeScript = async ( }> => { try { await ensureHostScriptsDirectory(); - const isDocker = process.env.DOCKER === "true"; - const hostScriptPath = isDocker + const docker = await isDocker(); + const hostScriptPath = docker ? join("/app/scripts", filename) : join(process.cwd(), "scripts", filename); diff --git a/app/_utils/cron/files-manipulation.ts b/app/_utils/cron/files-manipulation.ts index 4799062..f74c7b2 100644 --- a/app/_utils/cron/files-manipulation.ts +++ b/app/_utils/cron/files-manipulation.ts @@ -3,6 +3,7 @@ import { exec } from "child_process"; import { promisify } from "util"; import { readHostCrontab, writeHostCrontab } from "@/app/_utils/system/hostCrontab"; +import { isDocker } from "@/app/_server/actions/global"; const execAsync = promisify(exec); @@ -27,9 +28,9 @@ export const cleanCrontabContent = async (content: string): Promise => { } export const readCronFiles = async (): Promise => { - const isDocker = process.env.DOCKER === "true"; + const docker = await isDocker(); - if (!isDocker) { + if (!docker) { try { const { stdout } = await execAsync('crontab -l 2>/dev/null || echo ""'); return stdout; @@ -43,9 +44,9 @@ export const readCronFiles = async (): Promise => { } export const writeCronFiles = async (content: string): Promise => { - const isDocker = process.env.DOCKER === "true"; + const docker = await isDocker(); - if (!isDocker) { + if (!docker) { try { await execAsync('echo "' + content + '" | crontab -'); return true; diff --git a/app/_utils/scriptScanner.ts b/app/_utils/scriptScanner.ts index 65a1ce6..30d7d8a 100644 --- a/app/_utils/scriptScanner.ts +++ b/app/_utils/scriptScanner.ts @@ -1,5 +1,6 @@ import { promises as fs } from "fs"; import path from "path"; +import { isDocker } from "../_server/actions/global"; export interface Script { id: string; @@ -67,8 +68,8 @@ const scanScriptsDirectory = async (dirPath: string): Promise => { } export const loadAllScripts = async (): Promise => { - const isDocker = process.env.DOCKER === "true"; - const scriptsDir = isDocker + const docker = await isDocker(); + const scriptsDir = docker ? "/app/scripts" : path.join(process.cwd(), "scripts"); return await scanScriptsDirectory(scriptsDir); diff --git a/app/_utils/scripts.ts b/app/_utils/scripts.ts index 2c43af8..bbb1b1f 100644 --- a/app/_utils/scripts.ts +++ b/app/_utils/scripts.ts @@ -1,28 +1,16 @@ "use server"; import { join } from "path"; +import { SCRIPTS_DIR } from "@/app/_consts/file"; -const isDocker = process.env.DOCKER === "true"; -const SCRIPTS_DIR = async () => { - if (isDocker && process.env.HOST_PROJECT_DIR) { - return `${process.env.HOST_PROJECT_DIR}/scripts`; - } - return join(process.cwd(), "scripts"); +export const getScriptPath = (filename: string): string => { + return join(process.cwd(), SCRIPTS_DIR, filename); }; -export const getScriptPath = async (filename: string): Promise => { - return join(await SCRIPTS_DIR(), filename); -} - -export const getHostScriptPath = async (filename: string): Promise => { - const hostProjectDir = process.env.HOST_PROJECT_DIR || process.cwd(); - - const hostScriptsDir = join(hostProjectDir, "scripts"); - return `bash ${join(hostScriptsDir, filename)}`; -} +export const getHostScriptPath = (filename: string): string => { + return `bash ${join(process.cwd(), SCRIPTS_DIR, filename)}`; +}; export const normalizeLineEndings = (content: string): string => { return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); }; - -export { SCRIPTS_DIR }; diff --git a/app/_utils/snippetScanner.ts b/app/_utils/snippetScanner.ts index e48da10..63353e1 100644 --- a/app/_utils/snippetScanner.ts +++ b/app/_utils/snippetScanner.ts @@ -1,5 +1,6 @@ import { promises as fs } from "fs"; import path from "path"; +import { isDocker } from "../_server/actions/global"; export interface BashSnippet { id: string; @@ -118,12 +119,12 @@ const scanSnippetDirectory = async ( } export const loadAllSnippets = async (): Promise => { - const isDocker = process.env.DOCKER === "true"; + const docker = await isDocker(); let builtinSnippetsPath: string; let userSnippetsPath: string; - if (isDocker) { + if (docker) { builtinSnippetsPath = "/app/app/_utils/snippets"; userSnippetsPath = "/app/snippets"; } else { diff --git a/app/_utils/system/cron.ts b/app/_utils/system/cron.ts index 8144b8a..cd6c578 100644 --- a/app/_utils/system/cron.ts +++ b/app/_utils/system/cron.ts @@ -6,6 +6,7 @@ import { } from "@/app/_utils/system/hostCrontab"; import { parseJobsFromLines, deleteJobInLines, updateJobInLines, pauseJobInLines, resumeJobInLines } from "@/app/_utils/cron/line-manipulation"; import { cleanCrontabContent, readCronFiles, writeCronFiles } from "@/app/_utils/cron/files-manipulation"; +import { isDocker } from "@/app/_server/actions/global"; const execAsync = promisify(exec); @@ -18,10 +19,8 @@ export interface CronJob { paused?: boolean; } -const isDocker = (): boolean => process.env.DOCKER === "true"; - const readUserCrontab = async (user: string): Promise => { - if (isDocker()) { + if (await isDocker()) { const userCrontabs = await readAllHostCrontabs(); const targetUserCrontab = userCrontabs.find((uc) => uc.user === user); return targetUserCrontab?.content || ""; @@ -34,7 +33,7 @@ const readUserCrontab = async (user: string): Promise => { }; const writeUserCrontab = async (user: string, content: string): Promise => { - if (isDocker()) { + if (await isDocker()) { return await writeHostCrontabForUser(user, content); } else { try { @@ -48,7 +47,7 @@ const writeUserCrontab = async (user: string, content: string): Promise }; const getAllUsers = async (): Promise<{ user: string; content: string }[]> => { - if (isDocker()) { + if (await isDocker()) { return await readAllHostCrontabs(); } else { const { getAllTargetUsers } = await import("@/app/_utils/system/hostCrontab"); diff --git a/app/_utils/system/hostCrontab.ts b/app/_utils/system/hostCrontab.ts index 3a21a55..2b5cae4 100644 --- a/app/_utils/system/hostCrontab.ts +++ b/app/_utils/system/hostCrontab.ts @@ -1,3 +1,4 @@ +import { NSENTER_HOST_CRONTAB } from "@/app/_consts/nsenter"; import { exec } from "child_process"; import { promisify } from "util"; @@ -11,15 +12,13 @@ export interface UserInfo { const execHostCrontab = async (command: string): Promise => { try { - const { stdout } = await execAsync( - `nsenter -t 1 -m -u -i -n -p sh -c "${command}"` - ); + const { stdout } = await execAsync(NSENTER_HOST_CRONTAB(command?.trim())); return stdout; } catch (error: any) { console.error("Error executing host crontab command:", error); throw error; } -} +}; const getTargetUser = async (): Promise => { try { @@ -31,19 +30,6 @@ const getTargetUser = async (): Promise => { const dockerSocketOwner = stdout.trim(); if (dockerSocketOwner === "root") { - try { - const projectDir = process.env.HOST_PROJECT_DIR; - - if (projectDir) { - const dirOwner = await execHostCrontab( - `stat -c "%U" "${projectDir}"` - ); - return dirOwner.trim(); - } - } catch (error) { - console.warn("Could not detect user from project directory:", error); - } - try { const users = await execHostCrontab( 'getent passwd | grep ":/home/" | head -1 | cut -d: -f1' @@ -64,7 +50,7 @@ const getTargetUser = async (): Promise => { console.error("Error detecting target user:", error); return "root"; } -} +}; export const getAllTargetUsers = async (): Promise => { try { @@ -91,7 +77,7 @@ export const getAllTargetUsers = async (): Promise => { console.error("Error getting all target users:", error); return ["root"]; } -} +}; export const readHostCrontab = async (): Promise => { try { @@ -103,7 +89,7 @@ export const readHostCrontab = async (): Promise => { console.error("Error reading host crontab:", error); return ""; } -} +}; export const readAllHostCrontabs = async (): Promise< { user: string; content: string }[] @@ -129,7 +115,7 @@ export const readAllHostCrontabs = async (): Promise< console.error("Error reading all host crontabs:", error); return []; } -} +}; export const writeHostCrontab = async (content: string): Promise => { try { @@ -148,7 +134,7 @@ export const writeHostCrontab = async (content: string): Promise => { console.error("Error writing host crontab:", error); return false; } -} +}; export const writeHostCrontabForUser = async ( user: string, @@ -169,7 +155,7 @@ export const writeHostCrontabForUser = async ( console.error(`Error writing host crontab for user ${user}:`, error); return false; } -} +}; export async function getUserInfo(username: string): Promise { try { diff --git a/package.json b/package.json index ab87cbd..42dc692 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "react": "^18", "react-dom": "^18", "react-syntax-highlighter": "^15.6.1", - "systeminformation": "^5.27.8", + "systeminformation": "^5.27.11", "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", "typescript": "^5" diff --git a/scripts/demo-script.sh b/scripts/demo-script.sh index f063b51..d11ca78 100755 --- a/scripts/demo-script.sh +++ b/scripts/demo-script.sh @@ -1,5 +1,5 @@ # @id: demo-script -# @title: Hi, this is a demo script +# @title: Hi, this is a demo scripttttt # @description: This script logs a "hello world" to teach you how scripts work. #!/bin/bash diff --git a/yarn.lock b/yarn.lock index c73f139..9f41cee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4708,10 +4708,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -systeminformation@^5.27.8: - version "5.27.8" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.27.8.tgz#f13d180104a0df2e7222c5d4aa85aea147428de5" - integrity sha512-d3Z0gaQO1MlUxzDUKsmXz5y4TOBCMZ8IyijzaYOykV3AcNOTQ7mT+tpndUOXYNSxzLK3la8G32xiUFvZ0/s6PA== +systeminformation@^5.27.11: + version "5.27.11" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.27.11.tgz#286c8eca3947cfde0da684221ee9afb2ae313441" + integrity sha512-K3Lto/2m3K2twmKHdgx5B+0in9qhXK4YnoT9rIlgwN/4v7OV5c8IjbeAUkuky/6VzCQC7iKCAqi8rZathCdjHg== tailwind-merge@^2.0.0: version "2.6.0"