diff --git a/LICENSE.MD b/LICENSE.MD index 6cbef2c6d..bcef8b36e 100644 --- a/LICENSE.MD +++ b/LICENSE.MD @@ -1,8 +1,13 @@ -# License +Copyright 2026-present Dokploy Technology, Inc. -## Core License (Apache License 2.0) +Portions of this software are licensed as follows: -Copyright 2025 Mauricio Siu. +* All content that resides under a "/proprietary" directory of this repository, if that directory exists, is licensed under the license defined in "LICENSE_PROPRIETARY". +* Content outside of the above mentioned directories or restrictions above is available under the "Apache License 2.0" license as defined below. + +## Apache License 2.0 + +Copyright 2026-present Dokploy Technology, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,12 +20,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -## Additional Terms for Specific Features -The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: - -- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server, will always be free to use in the self-hosted version. -- **Restriction on Resale**: The multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. -- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service. - -For further inquiries or permissions, please contact us directly. diff --git a/LICENSE_PROPRIETARY.md b/LICENSE_PROPRIETARY.md new file mode 100644 index 000000000..0f4957575 --- /dev/null +++ b/LICENSE_PROPRIETARY.md @@ -0,0 +1,11 @@ +The Dokploy Source Available license (DSAL) version 1.0 + +Copyright (c) 2026-present Dokploy Technology, Inc. + +With regard to the Dokploy Software:This software and associated documentation files (the "Software") may only beused in production, if you (and any entity that you represent) have agreed to, and are in compliance with, a valid commercial agreement from Dokploy.Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software. You agree that Dokploy and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid Dokploy Source Available License.  Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes, without requiring a subscription.  You agree that Dokploy and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications.  You are not granted any other rights beyond what is expressly stated herein.  Subject to theforegoing, it is forbidden to copy, merge, publish, distribute, sublicense,and/or sell the Software. + +This Dokploy Source Available license applies only to the part of this Software that is in a /proprietary folder. The full text of this License shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THESOFTWARE. + +For all third party components incorporated into the Dokploy Software, thosecomponents are licensed under the original license provided by the owner of the applicable component. \ No newline at end of file diff --git a/README.md b/README.md index 23fcd0c9d..e97735597 100644 --- a/README.md +++ b/README.md @@ -68,53 +68,21 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). [Github Sponsors](https://github.com/sponsors/Siumauricio) - +## Sponsors - - -### Hero Sponsors 🎖 - -
- Hostinger - LX Aer - - - - - - -
- - - - - -### Premium Supporters 🥇 - -
- Supafort.com - agentdock.ai -
- - - - - -### Elite Contributors 🥈 - -
- AmericanCloud - Tolgee -
- -### Supporting Members 🥉 - -
- - Cloudblast.io - - Synexa -
+| Sponsor | Logo | Supporter Level | +|---------|:----:|----------------| +| [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) | Hostinger | 🎖 Hero Sponsor | +| [LX Aer](https://www.lxaer.com/?ref=dokploy) | LX Aer | 🎖 Hero Sponsor | +| [LinkDR](https://linkdr.com/?ref=dokploy) | LinkDR | 🎖 Hero Sponsor | +| [LambdaTest](https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor) | LambdaTest | 🎖 Hero Sponsor | +| [Awesome Tools](https://awesome.tools/) | Awesome Tools | 🎖 Hero Sponsor | +| [Supafort](https://supafort.com/?ref=dokploy) | Supafort.com | 🥇 Premium Supporter | +| [Agentdock](https://agentdock.ai/?ref=dokploy) | agentdock.ai | 🥇 Premium Supporter | +| [AmericanCloud](https://americancloud.com/?ref=dokploy) | AmericanCloud | 🥈 Elite Contributor | +| [Tolgee](https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy) | Tolgee | 🥈 Elite Contributor | +| [Cloudblast](https://cloudblast.io/?ref=dokploy) | Cloudblast.io | 🥉 Supporting Member | +| [Synexa](https://synexa.ai/?ref=dokploy) | Synexa | 🥉 Supporting Member | ### Community Backers 🤝 diff --git a/apps/dokploy/.env.production.example b/apps/dokploy/.env.production.example index 41e934c3a..560faf9e6 100644 --- a/apps/dokploy/.env.production.example +++ b/apps/dokploy/.env.production.example @@ -1,3 +1,2 @@ -DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy" PORT=3000 NODE_ENV=production \ No newline at end of file diff --git a/apps/dokploy/__test__/env/environment-access-fallback.test.ts b/apps/dokploy/__test__/env/environment-access-fallback.test.ts new file mode 100644 index 000000000..a4b56393a --- /dev/null +++ b/apps/dokploy/__test__/env/environment-access-fallback.test.ts @@ -0,0 +1,294 @@ +import { describe, expect, it } from "vitest"; + +// Type definitions matching the project structure +type Environment = { + environmentId: string; + name: string; + isDefault: boolean; +}; + +type Project = { + projectId: string; + name: string; + environments: Environment[]; +}; + +/** + * Helper function that selects the appropriate environment for a user + * This matches the logic used in search-command.tsx and show.tsx + */ +function selectAccessibleEnvironment( + project: Project | null | undefined, +): Environment | null { + if (!project || !project.environments || project.environments.length === 0) { + return null; + } + + // Find default environment from accessible environments, or fall back to first accessible environment + const defaultEnvironment = + project.environments.find((environment) => environment.isDefault) || + project.environments[0]; + + return defaultEnvironment || null; +} + +describe("Environment Access Fallback", () => { + describe("selectAccessibleEnvironment", () => { + it("should return default environment when user has access to it", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-prod", + name: "production", + isDefault: true, + }, + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-prod"); + expect(result?.isDefault).toBe(true); + }); + + it("should return first accessible environment when user doesn't have access to default", () => { + // Simulating filtered environments (user only has access to development) + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + // Note: production is not in the list because user doesn't have access + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + { + environmentId: "env-staging", + name: "staging", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-dev"); + expect(result?.name).toBe("development"); + }); + + it("should return first environment when no default is marked but environments exist", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + { + environmentId: "env-staging", + name: "staging", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-dev"); + }); + + it("should return null when project has no accessible environments", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).toBeNull(); + }); + + it("should return null when project is null", () => { + const result = selectAccessibleEnvironment(null); + + expect(result).toBeNull(); + }); + + it("should return null when project is undefined", () => { + const result = selectAccessibleEnvironment(undefined); + + expect(result).toBeNull(); + }); + + it("should handle project with single accessible environment", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-dev"); + }); + + it("should prioritize default environment even when it's not first in the array", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + { + environmentId: "env-staging", + name: "staging", + isDefault: false, + }, + { + environmentId: "env-prod", + name: "production", + isDefault: true, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-prod"); + expect(result?.isDefault).toBe(true); + }); + + it("should handle multiple default environments by returning the first one found", () => { + // Edge case: multiple environments marked as default (shouldn't happen, but test it) + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-prod-1", + name: "production-1", + isDefault: true, + }, + { + environmentId: "env-prod-2", + name: "production-2", + isDefault: true, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.isDefault).toBe(true); + // Should return the first default found + expect(result?.environmentId).toBe("env-prod-1"); + }); + + it("should work correctly when user has access to multiple environments including default", () => { + const project: Project = { + projectId: "proj-1", + name: "Test Project", + environments: [ + { + environmentId: "env-prod", + name: "production", + isDefault: true, + }, + { + environmentId: "env-dev", + name: "development", + isDefault: false, + }, + { + environmentId: "env-staging", + name: "staging", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-prod"); + expect(result?.isDefault).toBe(true); + }); + + it("should handle real-world scenario: user with only development access", () => { + // This simulates the exact bug we're fixing: + // User has access to development but not production (default) + // The filtered environments array only contains development + const project: Project = { + projectId: "proj-1", + name: "My Project", + environments: [ + // Only development is accessible (production was filtered out) + { + environmentId: "env-dev-123", + name: "development", + isDefault: false, + }, + ], + }; + + const result = selectAccessibleEnvironment(project); + + expect(result).not.toBeNull(); + expect(result?.environmentId).toBe("env-dev-123"); + expect(result?.name).toBe("development"); + // Should not be null even though it's not the default + }); + }); + + describe("Environment selection edge cases", () => { + it("should handle project with environments property as undefined", () => { + const project = { + projectId: "proj-1", + name: "Test Project", + environments: undefined, + } as unknown as Project; + + const result = selectAccessibleEnvironment(project); + + expect(result).toBeNull(); + }); + + it("should handle project with null environments array", () => { + const project = { + projectId: "proj-1", + name: "Test Project", + environments: null, + } as unknown as Project; + + const result = selectAccessibleEnvironment(project); + + expect(result).toBeNull(); + }); + }); +}); diff --git a/apps/dokploy/__test__/env/stack-environment.test.ts b/apps/dokploy/__test__/env/stack-environment.test.ts new file mode 100644 index 000000000..13f5adb53 --- /dev/null +++ b/apps/dokploy/__test__/env/stack-environment.test.ts @@ -0,0 +1,184 @@ +import { getEnviromentVariablesObject } from "@dokploy/server/index"; +import { describe, expect, it } from "vitest"; + +const projectEnv = ` +ENVIRONMENT=staging +DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db +PORT=3000 +`; + +const environmentEnv = ` +NODE_ENV=development +API_URL=https://api.dev.example.com +REDIS_URL=redis://localhost:6379 +DATABASE_NAME=dev_database +SECRET_KEY=env-secret-123 +`; + +describe("getEnviromentVariablesObject with environment variables (Stack compose)", () => { + it("resolves environment variables correctly for Stack compose", () => { + const serviceEnv = ` +FOO=\${{environment.NODE_ENV}} +BAR=\${{environment.API_URL}} +BAZ=test +`; + + const result = getEnviromentVariablesObject( + serviceEnv, + projectEnv, + environmentEnv, + ); + + expect(result).toEqual({ + FOO: "development", + BAR: "https://api.dev.example.com", + BAZ: "test", + }); + }); + + it("resolves both project and environment variables for Stack compose", () => { + const serviceEnv = ` +ENVIRONMENT=\${{project.ENVIRONMENT}} +NODE_ENV=\${{environment.NODE_ENV}} +API_URL=\${{environment.API_URL}} +DATABASE_URL=\${{project.DATABASE_URL}} +SERVICE_PORT=4000 +`; + + const result = getEnviromentVariablesObject( + serviceEnv, + projectEnv, + environmentEnv, + ); + + expect(result).toEqual({ + ENVIRONMENT: "staging", + NODE_ENV: "development", + API_URL: "https://api.dev.example.com", + DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db", + SERVICE_PORT: "4000", + }); + }); + + it("handles multiple environment references in single value for Stack compose", () => { + const multiRefEnv = ` +HOST=localhost +PORT=5432 +USERNAME=postgres +PASSWORD=secret123 +`; + + const serviceEnv = ` +DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb +`; + + const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv); + + expect(result).toEqual({ + DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb", + }); + }); + + it("throws error for undefined environment variables in Stack compose", () => { + const serviceWithUndefined = ` +UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}} +`; + + expect(() => + getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv), + ).toThrow("Invalid environment variable: environment.UNDEFINED_VAR"); + }); + + it("allows service variables to override environment variables in Stack compose", () => { + const serviceOverrideEnv = ` +NODE_ENV=production +API_URL=\${{environment.API_URL}} +`; + + const result = getEnviromentVariablesObject( + serviceOverrideEnv, + "", + environmentEnv, + ); + + expect(result).toEqual({ + NODE_ENV: "production", + API_URL: "https://api.dev.example.com", + }); + }); + + it("resolves complex references with project, environment, and service variables for Stack compose", () => { + const complexServiceEnv = ` +FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}} +API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api +SERVICE_NAME=my-service +COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}} +`; + + const result = getEnviromentVariablesObject( + complexServiceEnv, + projectEnv, + environmentEnv, + ); + + expect(result).toEqual({ + FULL_DATABASE_URL: + "postgres://postgres:postgres@localhost:5432/project_db/dev_database", + API_ENDPOINT: "https://api.dev.example.com/staging/api", + SERVICE_NAME: "my-service", + COMPLEX_VAR: "my-service-development-staging", + }); + }); + + it("maintains precedence: service > environment > project in Stack compose", () => { + const conflictingProjectEnv = ` +NODE_ENV=production-project +API_URL=https://project.api.com +DATABASE_NAME=project_db +`; + + const conflictingEnvironmentEnv = ` +NODE_ENV=development-environment +API_URL=https://environment.api.com +DATABASE_NAME=env_db +`; + + const serviceWithConflicts = ` +NODE_ENV=service-override +PROJECT_ENV=\${{project.NODE_ENV}} +ENV_VAR=\${{environment.API_URL}} +DB_NAME=\${{environment.DATABASE_NAME}} +`; + + const result = getEnviromentVariablesObject( + serviceWithConflicts, + conflictingProjectEnv, + conflictingEnvironmentEnv, + ); + + expect(result).toEqual({ + NODE_ENV: "service-override", + PROJECT_ENV: "production-project", + ENV_VAR: "https://environment.api.com", + DB_NAME: "env_db", + }); + }); + + it("handles empty environment variables in Stack compose", () => { + const serviceWithEmpty = ` +SERVICE_VAR=test +PROJECT_VAR=\${{project.ENVIRONMENT}} +`; + + const result = getEnviromentVariablesObject( + serviceWithEmpty, + projectEnv, + "", + ); + + expect(result).toEqual({ + SERVICE_VAR: "test", + PROJECT_VAR: "staging", + }); + }); +}); diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index 739bd87a5..ee427feca 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -1,205 +1,106 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { HelpCircle, Settings } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; +import { Settings } from "lucide-react"; +import { useState } from "react"; import { AlertBlock } from "@/components/shared/alert-block"; -import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, - DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { api } from "@/utils/api"; +import { cn } from "@/lib/utils"; +import { + EndpointSpecForm, + HealthCheckForm, + LabelsForm, + ModeForm, + PlacementForm, + RestartPolicyForm, + RollbackConfigForm, + StopGracePeriodForm, + UpdateConfigForm, +} from "./swarm-forms"; -const HealthCheckSwarmSchema = z - .object({ - Test: z.array(z.string()).optional(), - Interval: z.number().optional(), - Timeout: z.number().optional(), - StartPeriod: z.number().optional(), - Retries: z.number().optional(), - }) - .strict(); - -const RestartPolicySwarmSchema = z - .object({ - Condition: z.string().optional(), - Delay: z.number().optional(), - MaxAttempts: z.number().optional(), - Window: z.number().optional(), - }) - .strict(); - -const PreferenceSchema = z - .object({ - Spread: z.object({ - SpreadDescriptor: z.string(), - }), - }) - .strict(); - -const PlatformSchema = z - .object({ - Architecture: z.string(), - OS: z.string(), - }) - .strict(); - -const PlacementSwarmSchema = z - .object({ - Constraints: z.array(z.string()).optional(), - Preferences: z.array(PreferenceSchema).optional(), - MaxReplicas: z.number().optional(), - Platforms: z.array(PlatformSchema).optional(), - }) - .strict(); - -const UpdateConfigSwarmSchema = z - .object({ - Parallelism: z.number(), - Delay: z.number().optional(), - FailureAction: z.string().optional(), - Monitor: z.number().optional(), - MaxFailureRatio: z.number().optional(), - Order: z.string(), - }) - .strict(); - -const ReplicatedSchema = z - .object({ - Replicas: z.number().optional(), - }) - .strict(); - -const ReplicatedJobSchema = z - .object({ - MaxConcurrent: z.number().optional(), - TotalCompletions: z.number().optional(), - }) - .strict(); - -const ServiceModeSwarmSchema = z - .object({ - Replicated: ReplicatedSchema.optional(), - Global: z.object({}).optional(), - ReplicatedJob: ReplicatedJobSchema.optional(), - GlobalJob: z.object({}).optional(), - }) - .strict(); - -const NetworkSwarmSchema = z.array( - z - .object({ - Target: z.string().optional(), - Aliases: z.array(z.string()).optional(), - DriverOpts: z.object({}).optional(), - }) - .strict(), -); - -const LabelsSwarmSchema = z.record(z.string()); - -const EndpointPortConfigSwarmSchema = z - .object({ - Protocol: z.string().optional(), - TargetPort: z.number().optional(), - PublishedPort: z.number().optional(), - PublishMode: z.string().optional(), - }) - .strict(); - -const EndpointSpecSwarmSchema = z - .object({ - Mode: z.string().optional(), - Ports: z.array(EndpointPortConfigSwarmSchema).optional(), - }) - .strict(); - -const createStringToJSONSchema = (schema: z.ZodTypeAny) => { - return z - .string() - .transform((str, ctx) => { - if (str === null || str === "") { - return null; - } - try { - return JSON.parse(str); - } catch { - ctx.addIssue({ code: "custom", message: "Invalid JSON format" }); - return z.NEVER; - } - }) - .superRefine((data, ctx) => { - if (data === null) { - return; - } - - if (Object.keys(data).length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Object cannot be empty", - }); - return; - } - - const parseResult = schema.safeParse(data); - if (!parseResult.success) { - for (const error of parseResult.error.issues) { - const path = error.path.join("."); - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `${path} ${error.message}`, - }); - } - } - }); +type MenuItem = { + id: string; + label: string; + description: string; + docDescription: string; }; -const addSwarmSettings = z.object({ - healthCheckSwarm: createStringToJSONSchema(HealthCheckSwarmSchema).nullable(), - restartPolicySwarm: createStringToJSONSchema( - RestartPolicySwarmSchema, - ).nullable(), - placementSwarm: createStringToJSONSchema(PlacementSwarmSchema).nullable(), - updateConfigSwarm: createStringToJSONSchema( - UpdateConfigSwarmSchema, - ).nullable(), - rollbackConfigSwarm: createStringToJSONSchema( - UpdateConfigSwarmSchema, - ).nullable(), - modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(), - labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(), - networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(), - stopGracePeriodSwarm: z.bigint().nullable(), - endpointSpecSwarm: createStringToJSONSchema( - EndpointSpecSwarmSchema, - ).nullable(), -}); - -type AddSwarmSettings = z.infer; +const menuItems: MenuItem[] = [ + { + id: "health-check", + label: "Health Check", + description: "Configure health check settings", + docDescription: + "Configure HEALTHCHECK to test a container's health. Determines if a container is healthy by running a command inside the container. Test, Interval, Timeout, StartPeriod, and Retries control health monitoring.", + }, + { + id: "restart-policy", + label: "Restart Policy", + description: "Configure restart policy", + docDescription: + "Configure the restart policy for containers in the service. Condition (none, on-failure, any), Delay (nanoseconds between restarts), MaxAttempts, and Window control restart behavior.", + }, + { + id: "placement", + label: "Placement", + description: "Configure placement constraints", + docDescription: + "Control which nodes service tasks can be scheduled on. Constraints (node.id==xyz), Preferences (spread.node.labels.zone), MaxReplicas, and Platforms specify task placement rules.", + }, + { + id: "update-config", + label: "Update Config", + description: "Configure update strategy", + docDescription: + "Configure how the service should be updated. Parallelism (tasks updated simultaneously), Delay, FailureAction (pause, continue, rollback), Monitor, MaxFailureRatio, and Order (stop-first, start-first) control updates.", + }, + { + id: "rollback-config", + label: "Rollback Config", + description: "Configure rollback strategy", + docDescription: + "Configure automated rollback on update failure. Uses same parameters as UpdateConfig: Parallelism, Delay, FailureAction, Monitor, MaxFailureRatio, and Order.", + }, + { + id: "mode", + label: "Mode", + description: "Configure service mode", + docDescription: + "Set service mode to either 'Replicated' with a specified number of tasks (Replicas), or 'Global' (one task per node).", + }, + { + id: "labels", + label: "Labels", + description: "Configure service labels", + docDescription: + "Add metadata to services using labels. Labels are key-value pairs (e.g., com.example.foo=bar) for organizing and filtering services.", + }, + { + id: "stop-grace-period", + label: "Stop Grace Period", + description: "Configure stop grace period", + docDescription: + "Time to wait before forcefully killing a container. Specified in nanoseconds (e.g., 10000000000 = 10 seconds). Allows containers to shutdown gracefully.", + }, + { + id: "endpoint-spec", + label: "Endpoint Spec", + description: "Configure endpoint specification", + docDescription: + "Configure endpoint mode for service discovery. Mode 'vip' (virtual IP - default) uses a single virtual IP. Mode 'dnsrr' (DNS round-robin) returns DNS entries for all tasks.", + }, +]; const hasStopGracePeriodSwarm = ( value: unknown, @@ -214,137 +115,23 @@ interface Props { } export const AddSwarmSettings = ({ id, type }: Props) => { - const queryMap = { - postgres: () => - api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), - redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), - mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), - mariadb: () => - api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), - application: () => - api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), - mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), - }; - const { data, refetch } = queryMap[type] - ? queryMap[type]() - : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); - - const mutationMap = { - postgres: () => api.postgres.update.useMutation(), - redis: () => api.redis.update.useMutation(), - mysql: () => api.mysql.update.useMutation(), - mariadb: () => api.mariadb.update.useMutation(), - application: () => api.application.update.useMutation(), - mongo: () => api.mongo.update.useMutation(), - }; - - const { mutateAsync, isError, error, isLoading } = mutationMap[type] - ? mutationMap[type]() - : api.mongo.update.useMutation(); - - const form = useForm({ - defaultValues: { - healthCheckSwarm: null, - restartPolicySwarm: null, - placementSwarm: null, - updateConfigSwarm: null, - rollbackConfigSwarm: null, - modeSwarm: null, - labelsSwarm: null, - networkSwarm: null, - stopGracePeriodSwarm: null, - endpointSpecSwarm: null, - }, - resolver: zodResolver(addSwarmSettings), - }); - - useEffect(() => { - if (data) { - const stopGracePeriodValue = hasStopGracePeriodSwarm(data) - ? data.stopGracePeriodSwarm - : null; - const normalizedStopGracePeriod = - stopGracePeriodValue === null || stopGracePeriodValue === undefined - ? null - : typeof stopGracePeriodValue === "bigint" - ? stopGracePeriodValue - : BigInt(stopGracePeriodValue); - form.reset({ - healthCheckSwarm: data.healthCheckSwarm - ? JSON.stringify(data.healthCheckSwarm, null, 2) - : null, - restartPolicySwarm: data.restartPolicySwarm - ? JSON.stringify(data.restartPolicySwarm, null, 2) - : null, - placementSwarm: data.placementSwarm - ? JSON.stringify(data.placementSwarm, null, 2) - : null, - updateConfigSwarm: data.updateConfigSwarm - ? JSON.stringify(data.updateConfigSwarm, null, 2) - : null, - rollbackConfigSwarm: data.rollbackConfigSwarm - ? JSON.stringify(data.rollbackConfigSwarm, null, 2) - : null, - modeSwarm: data.modeSwarm - ? JSON.stringify(data.modeSwarm, null, 2) - : null, - labelsSwarm: data.labelsSwarm - ? JSON.stringify(data.labelsSwarm, null, 2) - : null, - networkSwarm: data.networkSwarm - ? JSON.stringify(data.networkSwarm, null, 2) - : null, - stopGracePeriodSwarm: normalizedStopGracePeriod, - endpointSpecSwarm: data.endpointSpecSwarm - ? JSON.stringify(data.endpointSpecSwarm, null, 2) - : null, - }); - } - }, [form, form.reset, data]); - - const onSubmit = async (data: AddSwarmSettings) => { - await mutateAsync({ - applicationId: id || "", - postgresId: id || "", - redisId: id || "", - mysqlId: id || "", - mariadbId: id || "", - mongoId: id || "", - healthCheckSwarm: data.healthCheckSwarm, - restartPolicySwarm: data.restartPolicySwarm, - placementSwarm: data.placementSwarm, - updateConfigSwarm: data.updateConfigSwarm, - rollbackConfigSwarm: data.rollbackConfigSwarm, - modeSwarm: data.modeSwarm, - labelsSwarm: data.labelsSwarm, - networkSwarm: data.networkSwarm, - stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null, - endpointSpecSwarm: data.endpointSpecSwarm, - }) - .then(async () => { - toast.success("Swarm settings updated"); - refetch(); - }) - .catch(() => { - toast.error("Error updating the swarm settings"); - }); - }; + const [activeMenu, setActiveMenu] = useState("health-check"); + const [open, setOpen] = useState(false); return ( - + - + Swarm Settings - Update certain settings using a json object. + Configure swarm settings for your service. - {isError && {error?.message}}
Changing settings such as placements may cause the logs/monitoring, @@ -352,596 +139,66 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
-
- - ( - - Health Check - - - - - Check the interface - - - - + {/* Left Column - Menu */} +
+ +
- - - -
-										
-									
-
- )} - /> - - ( - - Restart Policy - - - - - Check the interface - - - - - -
-														{`{
-	Condition?: string | undefined;
-	Delay?: number | undefined;
-	MaxAttempts?: number | undefined;
-	Window?: number | undefined;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - ( - - Placement - - - - - Check the interface - - - - - -
-														{`{
-	Constraints?: string[] | undefined;
-	Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
-	MaxReplicas?: number | undefined;
-	Platforms?:
-		| Array<{
-				Architecture: string;
-				OS: string;
-		  }>
-		| undefined;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - ( - - Update Config - - - - - Check the interface - - - - - -
-														{`{
-	Parallelism?: number;
-	Delay?: number | undefined;
-	FailureAction?: string | undefined;
-	Monitor?: number | undefined;
-	MaxFailureRatio?: number | undefined;
-	Order: string;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - ( - - Rollback Config - - - - - Check the interface - - - - - -
-														{`{
-	Parallelism?: number;
-	Delay?: number | undefined;
-	FailureAction?: string | undefined;
-	Monitor?: number | undefined;
-	MaxFailureRatio?: number | undefined;
-	Order: string;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - ( - - Mode - - - - - Check the interface - - - - - -
-														{`{
-	Replicated?: { Replicas?: number | undefined } | undefined;
-	Global?: {} | undefined;
-	ReplicatedJob?:
-		| {
-				MaxConcurrent?: number | undefined;
-				TotalCompletions?: number | undefined;
-		  }
-		| undefined;
-	GlobalJob?: {} | undefined;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - ( - - Network - - - - - Check the interface - - - - - -
-														{`[
-  {
-	"Target" : string | undefined;
-	"Aliases" : string[] | undefined;
-	"DriverOpts" : { [key: string]: string } | undefined;
-  }
-]`}
-													
-
-
-
-
- - - -
-										
-									
-
- )} - /> - ( - - Labels - - - - - Check the interface - - - - - -
-														{`{
-	[name: string]: string;
-}`}
-													
-
-
-
-
- - - -
-										
-									
-
- )} - /> - ( - - Stop Grace Period (nanoseconds) - - - - - Duration in nanoseconds - - - - - -
-														{`Enter duration in nanoseconds:
-														• 30000000000 - 30 seconds
-														• 120000000000 - 2 minutes  
-														• 3600000000000 - 1 hour
-														• 0 - no grace period`}
-													
-
-
-
-
- - - field.onChange( - e.target.value ? BigInt(e.target.value) : null, - ) - } - /> - -
-										
-									
-
- )} - /> - ( - - Endpoint Spec - - - - - Check the interface - - - - - -
-														{`{
-	Mode?: string | undefined;
-	Ports?: Array<{
-		Protocol?: string | undefined;
-		TargetPort?: number | undefined;
-		PublishedPort?: number | undefined;
-		PublishMode?: string | undefined;
-	}> | undefined;
-}`}
-													
-
-
-
-
- - - - -
-										
-									
-
- )} - /> - - - - - + {/* Right Column - Form */} +
+ {activeMenu === "health-check" && ( + + )} + {activeMenu === "restart-policy" && ( + + )} + {activeMenu === "placement" && ( + + )} + {activeMenu === "update-config" && ( + + )} + {activeMenu === "rollback-config" && ( + + )} + {activeMenu === "mode" && } + {activeMenu === "labels" && } + {activeMenu === "stop-grace-period" && ( + + )} + {activeMenu === "endpoint-spec" && ( + + )} +
+
); diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx new file mode 100644 index 000000000..7ee31e5b6 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx @@ -0,0 +1,154 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +export const endpointSpecFormSchema = z.object({ + Mode: z.string().optional(), +}); + +interface EndpointSpecFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(endpointSpecFormSchema), + defaultValues: { + Mode: undefined, + }, + }); + + useEffect(() => { + if (data?.endpointSpecSwarm) { + const es = data.endpointSpecSwarm; + form.reset({ + Mode: es.Mode, + }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = + formData.Mode !== undefined && + formData.Mode !== null && + formData.Mode !== ""; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + endpointSpecSwarm: hasAnyValue ? formData : null, + }); + + toast.success("Endpoint spec updated successfully"); + refetch(); + } catch { + toast.error("Error updating endpoint spec"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Mode + Endpoint mode (vip or dnsrr) + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx new file mode 100644 index 000000000..b2fc49ef3 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx @@ -0,0 +1,267 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +export const healthCheckFormSchema = z.object({ + Test: z.array(z.string()).optional(), + Interval: z.coerce.number().optional(), + Timeout: z.coerce.number().optional(), + StartPeriod: z.coerce.number().optional(), + Retries: z.coerce.number().optional(), +}); + +interface HealthCheckFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => { + const [isLoading, setIsLoading] = useState(false); + const [testCommands, setTestCommands] = useState([]); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(healthCheckFormSchema), + defaultValues: { + Test: [], + Interval: undefined, + Timeout: undefined, + StartPeriod: undefined, + Retries: undefined, + }, + }); + + useEffect(() => { + if (data?.healthCheckSwarm) { + const hc = data.healthCheckSwarm; + form.reset({ + Test: hc.Test || [], + Interval: hc.Interval, + Timeout: hc.Timeout, + StartPeriod: hc.StartPeriod, + Retries: hc.Retries, + }); + setTestCommands(hc.Test || []); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = + (formData.Test && formData.Test.length > 0) || + formData.Interval !== undefined || + formData.Timeout !== undefined || + formData.StartPeriod !== undefined || + formData.Retries !== undefined; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + healthCheckSwarm: hasAnyValue ? formData : null, + }); + + toast.success("Health check updated successfully"); + refetch(); + } catch { + toast.error("Error updating health check"); + } finally { + setIsLoading(false); + } + }; + + const addTestCommand = () => { + setTestCommands([...testCommands, ""]); + }; + + const updateTestCommand = (index: number, value: string) => { + const newCommands = [...testCommands]; + newCommands[index] = value; + setTestCommands(newCommands); + }; + + const removeTestCommand = (index: number) => { + setTestCommands(testCommands.filter((_, i) => i !== index)); + }; + + return ( +
+ +
+ Test Commands + + Command to run for health check (e.g., ["CMD-SHELL", "curl -f + http://localhost:3000/health"]) + +
+ {testCommands.map((cmd, index) => ( +
+ updateTestCommand(index, e.target.value)} + placeholder={ + index === 0 + ? "CMD-SHELL" + : "curl -f http://localhost:3000/health" + } + /> + +
+ ))} + +
+
+ + ( + + Interval (nanoseconds) + + Time between health checks (e.g., 10000000000 for 10 seconds) + + + + + + + )} + /> + + ( + + Timeout (nanoseconds) + + Maximum time to wait for health check response + + + + + + + )} + /> + + ( + + Start Period (nanoseconds) + + Initial grace period before health checks begin + + + + + + + )} + /> + + ( + + Retries + + Number of consecutive failures needed to consider container + unhealthy + + + + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts new file mode 100644 index 000000000..ebd00abcd --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts @@ -0,0 +1,10 @@ +export { HealthCheckForm } from "./health-check-form"; +export { RestartPolicyForm } from "./restart-policy-form"; +export { PlacementForm } from "./placement-form"; +export { UpdateConfigForm } from "./update-config-form"; +export { RollbackConfigForm } from "./rollback-config-form"; +export { ModeForm } from "./mode-form"; +export { LabelsForm } from "./labels-form"; +export { StopGracePeriodForm } from "./stop-grace-period-form"; +export { EndpointSpecForm } from "./endpoint-spec-form"; +export { filterEmptyValues, hasValues } from "./utils"; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx new file mode 100644 index 000000000..d1681dcd0 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx @@ -0,0 +1,200 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +export const labelsFormSchema = z.object({ + labels: z + .array( + z.object({ + key: z.string(), + value: z.string(), + }), + ) + .optional(), +}); + +interface LabelsFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const LabelsForm = ({ id, type }: LabelsFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(labelsFormSchema), + defaultValues: { + labels: [], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "labels", + }); + + useEffect(() => { + if (data?.labelsSwarm && typeof data.labelsSwarm === "object") { + const labelEntries = Object.entries(data.labelsSwarm).map( + ([key, value]) => ({ + key, + value: value as string, + }), + ); + form.reset({ labels: labelEntries }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + const labelsObject = + formData.labels?.reduce( + (acc, { key, value }) => { + if (key && value) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ) || {}; + + // If no labels, send null to clear the database + const labelsToSend = + Object.keys(labelsObject).length > 0 ? labelsObject : null; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + labelsSwarm: labelsToSend, + }); + + toast.success("Labels updated successfully"); + refetch(); + } catch { + toast.error("Error updating labels"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ +
+ Labels + + Add key-value labels to your service + +
+ {fields.map((field, index) => ( +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
+ ))} + +
+
+ +
+ + +
+
+ + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx new file mode 100644 index 000000000..839f5d519 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx @@ -0,0 +1,195 @@ +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +interface ModeFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const ModeForm = ({ id, type }: ModeFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + defaultValues: { + type: undefined, + Replicas: undefined, + }, + }); + + const modeType = form.watch("type"); + + useEffect(() => { + if (data?.modeSwarm) { + const mode = data.modeSwarm; + if (mode.Replicated) { + form.reset({ + type: "Replicated", + Replicas: mode.Replicated.Replicas, + }); + } else if (mode.Global) { + form.reset({ + type: "Global", + Replicas: undefined, + }); + } + } + }, [data, form]); + + const onSubmit = async (formData: any) => { + setIsLoading(true); + try { + // If no type is selected, send null to clear the database + if (!formData.type) { + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + modeSwarm: null, + }); + toast.success("Mode updated successfully"); + refetch(); + setIsLoading(false); + return; + } + + const modeData = + formData.type === "Replicated" + ? { Replicated: { Replicas: formData.Replicas } } + : { Global: {} }; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + modeSwarm: modeData, + }); + + toast.success("Mode updated successfully"); + refetch(); + } catch { + toast.error("Error updating mode"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Mode Type + + Choose between replicated or global service mode + + + + + )} + /> + + {modeType === "Replicated" && ( + ( + + Replicas + Number of replicas to run + + + + + + )} + /> + )} + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx new file mode 100644 index 000000000..b0c354513 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx @@ -0,0 +1,342 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +const PreferenceSchema = z.object({ + Spread: z.object({ + SpreadDescriptor: z.string(), + }), +}); + +const PlatformSchema = z.object({ + Architecture: z.string(), + OS: z.string(), +}); + +export const placementFormSchema = z.object({ + Constraints: z.array(z.string()).optional(), + Preferences: z.array(PreferenceSchema).optional(), + MaxReplicas: z.coerce.number().optional(), + Platforms: z.array(PlatformSchema).optional(), +}); + +interface PlacementFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const PlacementForm = ({ id, type }: PlacementFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(placementFormSchema), + defaultValues: { + Constraints: [], + Preferences: [], + MaxReplicas: undefined, + Platforms: [], + }, + }); + + const constraints = form.watch("Constraints") || []; + const preferences = form.watch("Preferences") || []; + const platforms = form.watch("Platforms") || []; + + useEffect(() => { + if (data?.placementSwarm) { + const placement = data.placementSwarm; + form.reset({ + Constraints: placement.Constraints || [], + Preferences: + placement.Preferences?.map((p: any) => ({ + SpreadDescriptor: p.Spread?.SpreadDescriptor || "", + })) || [], + MaxReplicas: placement.MaxReplicas, + Platforms: placement.Platforms || [], + }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = + (formData.Constraints && formData.Constraints.length > 0) || + (formData.Preferences && formData.Preferences.length > 0) || + (formData.Platforms && formData.Platforms.length > 0) || + formData.MaxReplicas !== undefined; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + placementSwarm: hasAnyValue ? formData : null, + }); + + toast.success("Placement updated successfully"); + refetch(); + } catch { + toast.error("Error updating placement"); + } finally { + setIsLoading(false); + } + }; + + const addConstraint = () => { + form.setValue("Constraints", [...constraints, ""]); + }; + + const updateConstraint = (index: number, value: string) => { + const newConstraints = [...constraints]; + newConstraints[index] = value; + form.setValue("Constraints", newConstraints); + }; + + const removeConstraint = (index: number) => { + form.setValue( + "Constraints", + constraints.filter((_: string, i: number) => i !== index), + ); + }; + + const addPreference = () => { + form.setValue("Preferences", [...preferences, { SpreadDescriptor: "" }]); + }; + + const updatePreference = (index: number, value: string) => { + const newPreferences = [...preferences]; + if (newPreferences[index]) { + newPreferences[index].SpreadDescriptor = value; + form.setValue("Preferences", newPreferences); + } + }; + + const removePreference = (index: number) => { + form.setValue( + "Preferences", + preferences.filter((_: any, i: number) => i !== index), + ); + }; + + const addPlatform = () => { + form.setValue("Platforms", [...platforms, { Architecture: "", OS: "" }]); + }; + + const updatePlatform = ( + index: number, + field: "Architecture" | "OS", + value: string, + ) => { + const newPlatforms = [...platforms]; + if (newPlatforms[index]) { + newPlatforms[index][field] = value; + form.setValue("Platforms", newPlatforms); + } + }; + + const removePlatform = (index: number) => { + form.setValue( + "Platforms", + platforms.filter((_: any, i: number) => i !== index), + ); + }; + + return ( +
+ +
+ Constraints + + Placement constraints (e.g., "node.role==manager") + +
+ {constraints.map((constraint: string, index: number) => ( +
+ updateConstraint(index, e.target.value)} + placeholder="node.role==manager" + /> + +
+ ))} + +
+
+ +
+ Preferences + + Spread preferences for task distribution (e.g., + "node.labels.region") + +
+ {preferences.map((pref: any, index: number) => ( +
+ updatePreference(index, e.target.value)} + placeholder="node.labels.region" + /> + +
+ ))} + +
+
+ + ( + + Max Replicas + + Maximum number of replicas per node + + + + + + + )} + /> + +
+ Platforms + + Target platforms for task scheduling + +
+ {platforms.map((platform: any, index: number) => ( +
+ + updatePlatform(index, "Architecture", e.target.value) + } + placeholder="amd64" + /> + updatePlatform(index, "OS", e.target.value)} + placeholder="linux" + /> + +
+ ))} + +
+
+ +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx new file mode 100644 index 000000000..b7fb649be --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx @@ -0,0 +1,219 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +export const restartPolicyFormSchema = z.object({ + Condition: z.string().optional(), + Delay: z.coerce.number().optional(), + MaxAttempts: z.coerce.number().optional(), + Window: z.coerce.number().optional(), +}); + +interface RestartPolicyFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(restartPolicyFormSchema), + defaultValues: { + Condition: undefined, + Delay: undefined, + MaxAttempts: undefined, + Window: undefined, + }, + }); + + useEffect(() => { + if (data?.restartPolicySwarm) { + form.reset({ + Condition: data.restartPolicySwarm.Condition, + Delay: data.restartPolicySwarm.Delay, + MaxAttempts: data.restartPolicySwarm.MaxAttempts, + Window: data.restartPolicySwarm.Window, + }); + } + }, [data, form]); + + const onSubmit = async ( + formData: z.infer, + ) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = Object.values(formData).some( + (value) => value !== undefined && value !== null && value !== "", + ); + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + restartPolicySwarm: hasAnyValue ? formData : null, + }); + + toast.success("Restart policy updated successfully"); + refetch(); + } catch { + toast.error("Error updating restart policy"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Condition + When to restart the container + + + + )} + /> + + ( + + Delay (nanoseconds) + + Wait time between restart attempts + + + + + + + )} + /> + + ( + + Max Attempts + + Maximum number of restart attempts + + + + + + + )} + /> + + ( + + Window (nanoseconds) + + Time window to evaluate restart policy + + + + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx new file mode 100644 index 000000000..d53215348 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx @@ -0,0 +1,257 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +export const rollbackConfigFormSchema = z.object({ + Parallelism: z.coerce.number().optional(), + Delay: z.coerce.number().optional(), + FailureAction: z.string().optional(), + Monitor: z.coerce.number().optional(), + MaxFailureRatio: z.coerce.number().optional(), + Order: z.string().optional(), +}); + +interface RollbackConfigFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(rollbackConfigFormSchema), + defaultValues: { + Parallelism: undefined, + Delay: undefined, + FailureAction: undefined, + Monitor: undefined, + MaxFailureRatio: undefined, + Order: undefined, + }, + }); + + useEffect(() => { + if (data?.rollbackConfigSwarm) { + form.reset(data.rollbackConfigSwarm); + } + }, [data, form]); + + const onSubmit = async ( + formData: z.infer, + ) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = Object.values(formData).some( + (value) => value !== undefined && value !== null && value !== "", + ); + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + rollbackConfigSwarm: (hasAnyValue ? formData : null) as any, + }); + + toast.success("Rollback config updated successfully"); + refetch(); + } catch { + toast.error("Error updating rollback config"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Parallelism + + Number of tasks to rollback simultaneously + + + + + + + )} + /> + + ( + + Delay (nanoseconds) + Delay between task rollbacks + + + + + + )} + /> + + ( + + Failure Action + Action on rollback failure + + + + )} + /> + + ( + + Monitor (nanoseconds) + + Duration to monitor for failure after rollback + + + + + + + )} + /> + + ( + + Max Failure Ratio + + Maximum failure ratio tolerated (0-1) + + + + + + + )} + /> + + ( + + Order + Rollback order strategy + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx new file mode 100644 index 000000000..a324da31b --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/stop-grace-period-form.tsx @@ -0,0 +1,158 @@ +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +const hasStopGracePeriodSwarm = ( + value: unknown, +): value is { stopGracePeriodSwarm: bigint | number | string | null } => + typeof value === "object" && + value !== null && + "stopGracePeriodSwarm" in value; + +interface StopGracePeriodFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + defaultValues: { + value: null as bigint | null, + }, + }); + + useEffect(() => { + if (hasStopGracePeriodSwarm(data)) { + const value = data.stopGracePeriodSwarm; + const normalizedValue = + value === null || value === undefined + ? null + : typeof value === "bigint" + ? value + : BigInt(value); + form.reset({ + value: normalizedValue, + }); + } + }, [data, form]); + + const onSubmit = async (formData: any) => { + setIsLoading(true); + try { + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + stopGracePeriodSwarm: formData.value, + }); + + toast.success("Stop grace period updated successfully"); + refetch(); + } catch { + toast.error("Error updating stop grace period"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Stop Grace Period (nanoseconds) + + Time to wait before forcefully killing the container +
+ Examples: 30000000000 (30s), 120000000000 (2m) +
+ + + field.onChange( + e.target.value ? BigInt(e.target.value) : null, + ) + } + /> + + +
+ )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx new file mode 100644 index 000000000..4119c41f8 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx @@ -0,0 +1,264 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; + +export const updateConfigFormSchema = z.object({ + Parallelism: z.coerce.number().optional(), + Delay: z.coerce.number().optional(), + FailureAction: z.string().optional(), + Monitor: z.coerce.number().optional(), + MaxFailureRatio: z.coerce.number().optional(), + Order: z.string().optional(), +}); + +interface UpdateConfigFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(updateConfigFormSchema), + defaultValues: { + Parallelism: undefined, + Delay: undefined, + FailureAction: undefined, + Monitor: undefined, + MaxFailureRatio: undefined, + Order: undefined, + }, + }); + + useEffect(() => { + if (data?.updateConfigSwarm) { + const config = data.updateConfigSwarm; + form.reset({ + Parallelism: config.Parallelism, + Delay: config.Delay, + FailureAction: config.FailureAction, + Monitor: config.Monitor, + MaxFailureRatio: config.MaxFailureRatio, + Order: config.Order, + }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + // Check if all values are empty, if so, send null to clear the database + const hasAnyValue = Object.values(formData).some( + (value) => value !== undefined && value !== null && value !== "", + ); + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + updateConfigSwarm: (hasAnyValue ? formData : null) as any, + }); + + toast.success("Update config updated successfully"); + refetch(); + } catch { + toast.error("Error updating update config"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + ( + + Parallelism + + Number of tasks to update simultaneously + + + + + + + )} + /> + + ( + + Delay (nanoseconds) + Delay between task updates + + + + + + )} + /> + + ( + + Failure Action + Action on update failure + + + + )} + /> + + ( + + Monitor (nanoseconds) + + Duration to monitor for failure after update + + + + + + + )} + /> + + ( + + Max Failure Ratio + + Maximum failure ratio tolerated (0-1) + + + + + + + )} + /> + + ( + + Order + Update order strategy + + + + )} + /> + +
+ + +
+ + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts new file mode 100644 index 000000000..58793c02e --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/utils.ts @@ -0,0 +1,31 @@ +/** + * Filters out undefined, null, and empty string values from form data + * Only returns fields that have actual values + */ +export const filterEmptyValues = ( + formData: Record, +): Record => { + return Object.entries(formData).reduce( + (acc, [key, value]) => { + // Keep arrays even if empty (they might be intentionally cleared) + if (Array.isArray(value)) { + if (value.length > 0) { + acc[key] = value; + } + } + // For other values, filter out undefined, null, and empty strings + else if (value !== undefined && value !== null && value !== "") { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); +}; + +/** + * Checks if filtered data has any values to save + */ +export const hasValues = (data: Record): boolean => { + return Object.keys(data).length > 0; +}; diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 3dd030c4e..7f92157f2 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -207,6 +207,11 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { } }, [data, form]); + // Hide builder section when Docker provider is selected + if (data?.sourceType === "docker") { + return null; + } + const onSubmit = async (data: AddTemplate) => { await mutateAsync({ applicationId, diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 23127383a..6cf8d8830 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -1,3 +1,4 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { ExternalLink, FileText, @@ -29,7 +30,6 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { api } from "@/utils/api"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index a618a20ac..8234593e1 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -288,9 +288,12 @@ export const ShowProjects = () => { ) .some(Boolean); - const productionEnvironment = project?.environments.find( - (env) => env.isDefault, - ); + // Find default environment from accessible environments, or fall back to first accessible environment + const accessibleEnvironment = + project?.environments.find((env) => env.isDefault) || + project?.environments?.[0]; + + const hasNoEnvironments = !accessibleEnvironment; return (
{ className="w-full lg:max-w-md" > { + if (hasNoEnvironments) { + e.preventDefault(); + } + }} > {haveServicesWithDomains ? ( @@ -419,7 +431,7 @@ export const ShowProjects = () => { ) : null} - +
@@ -427,9 +439,19 @@ export const ShowProjects = () => {
- + {project.description} + + {hasNoEnvironments && ( +
+ + + You have access to this project but no + environments are available + +
+ )}
diff --git a/apps/dokploy/components/dashboard/search-command.tsx b/apps/dokploy/components/dashboard/search-command.tsx index d53fe0037..6fd798955 100644 --- a/apps/dokploy/components/dashboard/search-command.tsx +++ b/apps/dokploy/components/dashboard/search-command.tsx @@ -89,7 +89,7 @@ export const SearchCommand = () => { {data?.map((project) => { - // Find default environment, or fall back to first environment + // Find default environment from accessible environments, or fall back to first accessible environment const defaultEnvironment = project.environments.find( (environment) => environment.isDefault, diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index ad66aa0fa..461a4c17c 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -41,7 +41,7 @@ const profileSchema = z.object({ password: z.string().nullable(), currentPassword: z.string().nullable(), image: z.string().optional(), - name: z.string().optional(), + firstName: z.string().optional(), lastName: z.string().optional(), allowImpersonation: z.boolean().optional().default(false), }); @@ -91,7 +91,7 @@ export const ProfileForm = () => { image: data?.user?.image || "", currentPassword: "", allowImpersonation: data?.user?.allowImpersonation || false, - name: data?.user?.firstName || "", + firstName: data?.user?.firstName || "", lastName: data?.user?.lastName || "", }, resolver: zodResolver(profileSchema), @@ -106,7 +106,7 @@ export const ProfileForm = () => { image: data?.user?.image || "", currentPassword: form.getValues("currentPassword") || "", allowImpersonation: data?.user?.allowImpersonation, - name: data?.user?.firstName || "", + firstName: data?.user?.firstName || "", lastName: data?.user?.lastName || "", }, { @@ -131,7 +131,7 @@ export const ProfileForm = () => { image: values.image, currentPassword: values.currentPassword || undefined, allowImpersonation: values.allowImpersonation, - name: values.name || undefined, + firstName: values.firstName || undefined, lastName: values.lastName || undefined, }); await refetch(); @@ -141,7 +141,7 @@ export const ProfileForm = () => { password: "", image: values.image, currentPassword: "", - name: values.name || "", + firstName: values.firstName || "", lastName: values.lastName || "", }); } catch (error) { @@ -184,7 +184,7 @@ export const ProfileForm = () => {
( First Name diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 45b6a7e3a..d256a5119 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -18,7 +18,6 @@ import { Forward, GalleryVerticalEnd, GitBranch, - HeartIcon, KeyRound, Loader2, type LucideIcon, @@ -410,18 +409,6 @@ const MENU: Menu = { url: "https://discord.gg/2tBnJ3jDJc", icon: CircleHelp, }, - { - name: "Sponsor", - url: "https://opencollective.com/dokploy", - icon: ({ className }) => ( - - ), - }, ], } as const; diff --git a/apps/dokploy/drizzle/0057_damp_prism.sql b/apps/dokploy/drizzle/0057_damp_prism.sql deleted file mode 100644 index 363c2a9f4..000000000 --- a/apps/dokploy/drizzle/0057_damp_prism.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS "ai" ( - "aiId" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "apiUrl" text NOT NULL, - "apiKey" text NOT NULL, - "model" text NOT NULL, - "isEnabled" boolean DEFAULT true NOT NULL, - "adminId" text NOT NULL, - "createdAt" text NOT NULL -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "ai" ADD CONSTRAINT "ai_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; diff --git a/apps/dokploy/migration.ts b/apps/dokploy/migration.ts index d52066c88..984197b2a 100644 --- a/apps/dokploy/migration.ts +++ b/apps/dokploy/migration.ts @@ -1,10 +1,9 @@ +import { dbUrl } from "@dokploy/server/db"; import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; -const connectionString = process.env.DATABASE_URL!; - -const sql = postgres(connectionString, { max: 1 }); +const sql = postgres(dbUrl, { max: 1 }); const db = drizzle(sql); await migrate(db, { migrationsFolder: "drizzle" }) diff --git a/apps/dokploy/next.config.mjs b/apps/dokploy/next.config.mjs index a1b19d722..48231114a 100644 --- a/apps/dokploy/next.config.mjs +++ b/apps/dokploy/next.config.mjs @@ -19,6 +19,32 @@ const nextConfig = { locales: ["en"], defaultLocale: "en", }, + async headers() { + return [ + { + // Apply security headers to all routes + source: "/:path*", + headers: [ + { + key: "X-Frame-Options", + value: "DENY", + }, + { + key: "Content-Security-Policy", + value: "frame-ancestors 'none'", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "Referrer-Policy", + value: "strict-origin-when-cross-origin", + }, + ], + }, + ]; + }, }; export default nextConfig; diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index dbf8493e3..758f4697a 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.26.5", + "version": "v0.26.6", "private": true, "license": "Apache-2.0", "type": "module", @@ -196,10 +196,5 @@ "*": [ "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true" ] - }, - "commitlint": { - "extends": [ - "@commitlint/config-conventional" - ] } } diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index c32ad8f35..af901311e 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -1624,9 +1624,39 @@ export async function getServerSideProps( projectId: params.projectId, }); - await helpers.environment.one.fetch({ - environmentId: params.environmentId, - }); + // Try to fetch the requested environment + try { + await helpers.environment.one.fetch({ + environmentId: params.environmentId, + }); + } catch (error) { + // If user doesn't have access to requested environment, redirect to accessible one + const accessibleEnvironments = + await helpers.environment.byProjectId.fetch({ + projectId: params.projectId, + }); + + if (accessibleEnvironments.length > 0) { + // Try to find default, otherwise use first accessible + const targetEnv = + accessibleEnvironments.find((env) => env.isDefault) || + accessibleEnvironments[0]; + + return { + redirect: { + permanent: false, + destination: `/dashboard/project/${params.projectId}/environment/${targetEnv.environmentId}`, + }, + }; + } + // No accessible environments, redirect to home + return { + redirect: { + permanent: false, + destination: "/", + }, + }; + } await helpers.environment.byProjectId.fetch({ projectId: params.projectId, diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx index 2be9e5edf..7917bd97c 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx @@ -108,6 +108,7 @@ const Service = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx index b03392c45..1d6902c59 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx @@ -97,6 +97,7 @@ const Service = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx index e496dd928..0a1e8501d 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx @@ -79,6 +79,7 @@ const Mariadb = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx index 077add05b..bae83cb2b 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx @@ -78,6 +78,7 @@ const Mongo = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx index acf7280aa..ba2b9d8a0 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx @@ -77,6 +77,7 @@ const MySql = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx index d8bd94ca2..1d90e3e13 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx @@ -77,6 +77,7 @@ const Postgresql = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx index 0f4bd4a88..47eb82a74 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx @@ -77,6 +77,7 @@ const Redis = ( { name: "Projects", href: "/dashboard/projects" }, { name: data?.environment?.project?.name || "", + href: `/dashboard/project/${projectId}/environment/${environmentId}`, }, { name: data?.environment?.name || "", diff --git a/apps/dokploy/proprietary/README.md b/apps/dokploy/proprietary/README.md new file mode 100644 index 000000000..b1af288e6 --- /dev/null +++ b/apps/dokploy/proprietary/README.md @@ -0,0 +1,18 @@ +# Proprietary Features + +This directory contains all proprietary functionality of Dokploy. + +## Purpose + +This folder will house all **paid features** and premium functionality that are not part of the open source code. + +## License + +The code in this directory is under Dokploy's proprietary license. See [LICENSE_PROPRIETARY.md](../../../LICENSE_PROPRIETARY.md) for more details. + +## Contact + +If you want to learn more about our paid features or have any questions, please contact us at: + +- Email: [sales@dokploy.com](mailto:sales@dokploy.com) +- Contact Form: [https://dokploy.com/contact](https://dokploy.com/contact) diff --git a/apps/dokploy/server/db/drizzle.config.ts b/apps/dokploy/server/db/drizzle.config.ts index 60a3bb937..8f6a4a60a 100644 --- a/apps/dokploy/server/db/drizzle.config.ts +++ b/apps/dokploy/server/db/drizzle.config.ts @@ -1,10 +1,11 @@ +import { dbUrl } from "@dokploy/server/db"; import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./server/db/schema/index.ts", dialect: "postgresql", dbCredentials: { - url: process.env.DATABASE_URL!, + url: dbUrl, }, out: "drizzle", migrations: { diff --git a/apps/dokploy/server/db/index.ts b/apps/dokploy/server/db/index.ts index 55d6d3a46..2112c4f67 100644 --- a/apps/dokploy/server/db/index.ts +++ b/apps/dokploy/server/db/index.ts @@ -1,3 +1,4 @@ +import { dbUrl } from "@dokploy/server/db/constants"; import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; @@ -6,10 +7,6 @@ declare global { var db: PostgresJsDatabase | undefined; } -const dbUrl = - process.env.DATABASE_URL || - "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy"; - export let db: PostgresJsDatabase; if (process.env.NODE_ENV === "production") { db = drizzle(postgres(dbUrl!), { diff --git a/apps/dokploy/server/db/migration.ts b/apps/dokploy/server/db/migration.ts index fa2e1a80f..8a24afdc5 100644 --- a/apps/dokploy/server/db/migration.ts +++ b/apps/dokploy/server/db/migration.ts @@ -1,10 +1,9 @@ +import { dbUrl } from "@dokploy/server/db"; import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; -const connectionString = process.env.DATABASE_URL!; - -const sql = postgres(connectionString, { max: 1 }); +const sql = postgres(dbUrl, { max: 1 }); const db = drizzle(sql); export const migration = async () => diff --git a/apps/dokploy/server/db/reset.ts b/apps/dokploy/server/db/reset.ts index c22291478..4c6e3736e 100644 --- a/apps/dokploy/server/db/reset.ts +++ b/apps/dokploy/server/db/reset.ts @@ -1,11 +1,10 @@ +import { dbUrl } from "@dokploy/server/db"; import { sql } from "drizzle-orm"; // Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406 import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; -const connectionString = process.env.DATABASE_URL!; - -const pg = postgres(connectionString, { max: 1 }); +const pg = postgres(dbUrl, { max: 1 }); const db = drizzle(pg); const clearDb = async (): Promise => { diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index eaefa21f1..c3f902475 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -1,9 +1,9 @@ import type http from "node:http"; -import { findServerById, validateRequest } from "@dokploy/server"; +import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { spawn } from "node-pty"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; -import { getShell } from "./utils"; +import { getShell, isValidContainerId } from "./utils"; export const setupDockerContainerLogsWebSocketServer = ( server: http.Server, @@ -42,6 +42,12 @@ export const setupDockerContainerLogsWebSocketServer = ( return; } + // Security: Validate containerId to prevent command injection + if (!isValidContainerId(containerId)) { + ws.close(4000, "Invalid container ID format"); + return; + } + if (!user || !session) { ws.close(); return; @@ -111,6 +117,11 @@ export const setupDockerContainerLogsWebSocketServer = ( client.end(); }); } else { + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } const shell = getShell(); const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps ${ runType === "swarm" ? "--raw" : "" diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index 155d7f0cc..a2c242d95 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -1,9 +1,9 @@ import type http from "node:http"; -import { findServerById, validateRequest } from "@dokploy/server"; +import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { spawn } from "node-pty"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; -import { getShell } from "./utils"; +import { isValidContainerId, isValidShell } from "./utils"; export const setupDockerContainerTerminalWebSocketServer = ( server: http.Server, @@ -35,10 +35,25 @@ export const setupDockerContainerTerminalWebSocketServer = ( const { user, session } = await validateRequest(req); if (!containerId) { - ws.close(4000, "containerId no provided"); + ws.close(4000, "containerId not provided"); return; } + // Security: Validate containerId to prevent command injection + if (!isValidContainerId(containerId)) { + ws.close(4000, "Invalid container ID format"); + return; + } + + // Security: Validate shell to prevent command injection + if (activeWay && !isValidShell(activeWay)) { + ws.close(4000, "Invalid shell specified"); + return; + } + + // Default to 'sh' if no shell specified + const shell = activeWay || "sh"; + if (!user || !session) { ws.close(); return; @@ -54,55 +69,61 @@ export const setupDockerContainerTerminalWebSocketServer = ( let _stderr = ""; conn .once("ready", () => { - conn.exec( - `docker exec -it -w / ${containerId} ${activeWay}`, - { pty: true }, - (err, stream) => { - if (err) { - console.error("SSH exec error:", err); - ws.close(); + // Use array-style arguments to prevent shell injection + const dockerCommand = [ + "docker", + "exec", + "-it", + "-w", + "/", + containerId, + shell, + ].join(" "); + conn.exec(dockerCommand, { pty: true }, (err, stream) => { + if (err) { + console.error("SSH exec error:", err); + ws.close(); + conn.end(); + return; + } + + stream + .on("close", (code: number, _signal: string) => { + ws.send(`\nContainer closed with code: ${code}\n`); conn.end(); - return; - } + }) + .on("data", (data: string) => { + _stdout += data.toString(); + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + _stderr += data.toString(); + ws.send(data.toString()); + console.error("Error: ", data.toString()); + }); - stream - .on("close", (code: number, _signal: string) => { - ws.send(`\nContainer closed with code: ${code}\n`); - conn.end(); - }) - .on("data", (data: string) => { - _stdout += data.toString(); - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - _stderr += data.toString(); - ws.send(data.toString()); - console.error("Error: ", data.toString()); - }); - - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - stream.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; } - }); + stream.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); - ws.on("close", () => { - stream.end(); - // Ensure SSH connection is closed when WebSocket closes - conn.end(); - }); - }, - ); + ws.on("close", () => { + stream.end(); + // Ensure SSH connection is closed when WebSocket closes + conn.end(); + }); + }); }) .on("error", (err) => { console.error("SSH connection error:", err); @@ -119,10 +140,14 @@ export const setupDockerContainerTerminalWebSocketServer = ( privateKey: server.sshKey?.privateKey, }); } else { - const shell = getShell(); + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } const ptyProcess = spawn( - shell, - ["-c", `docker exec -it -w / ${containerId} ${activeWay}`], + "docker", + ["exec", "-it", "-w", "/", containerId, shell], {}, ); diff --git a/apps/dokploy/server/wss/docker-stats.ts b/apps/dokploy/server/wss/docker-stats.ts index bd740e976..b5f2439bf 100644 --- a/apps/dokploy/server/wss/docker-stats.ts +++ b/apps/dokploy/server/wss/docker-stats.ts @@ -4,6 +4,7 @@ import { execAsync, getHostSystemStats, getLastAdvancedStatsFile, + IS_CLOUD, recordAdvancedStats, validateRequest, } from "@dokploy/server"; @@ -32,6 +33,12 @@ export const setupDockerStatsMonitoringSocketServer = ( wssTerm.on("connection", async (ws, req) => { const url = new URL(req.url || "", `http://${req.headers.host}`); + + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } const appName = url.searchParams.get("appName"); const appType = (url.searchParams.get("appType") || "application") as | "application" diff --git a/apps/dokploy/server/wss/listen-deployment.ts b/apps/dokploy/server/wss/listen-deployment.ts index ca49cea29..8aeee2410 100644 --- a/apps/dokploy/server/wss/listen-deployment.ts +++ b/apps/dokploy/server/wss/listen-deployment.ts @@ -1,8 +1,9 @@ import { spawn } from "node:child_process"; import type http from "node:http"; -import { findServerById, validateRequest } from "@dokploy/server"; +import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; +import { readValidDirectory } from "./utils"; export const setupDeploymentLogsWebSocketServer = ( server: http.Server, @@ -40,6 +41,11 @@ export const setupDeploymentLogsWebSocketServer = ( return; } + if (!readValidDirectory(logPath)) { + ws.close(4000, "Invalid log path"); + return; + } + if (!user || !session) { ws.close(); return; @@ -108,6 +114,11 @@ export const setupDeploymentLogsWebSocketServer = ( } }); } else { + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } tailProcess = spawn("tail", ["-n", "+1", "-f", logPath]); const stdout = tailProcess.stdout; diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index fa37d492b..00b0e2c2c 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -97,7 +97,12 @@ export const setupTerminalWebSocketServer = ( const isLocalServer = serverId === "local"; - if (isLocalServer && !IS_CLOUD) { + if (isLocalServer) { + if (IS_CLOUD) { + ws.send("This feature is not available in the cloud version."); + ws.close(); + return; + } const port = Number(url.searchParams.get("port")); const username = url.searchParams.get("username"); diff --git a/apps/dokploy/server/wss/utils.ts b/apps/dokploy/server/wss/utils.ts index 1a65fc520..c749fbc51 100644 --- a/apps/dokploy/server/wss/utils.ts +++ b/apps/dokploy/server/wss/utils.ts @@ -1,9 +1,52 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { execAsync, paths } from "@dokploy/server"; +import { execAsync, IS_CLOUD, paths } from "@dokploy/server"; +/** + * Validates that the container ID matches Docker's expected format. + * Docker container IDs are 64-character hex strings (or 12-char short form). + * Also allows container names: alphanumeric, underscores, hyphens, and dots. + */ +export const isValidContainerId = (id: string): boolean => { + // Match full ID (64 hex chars), short ID (12 hex chars), or container name + const hexPattern = /^[a-f0-9]{12,64}$/i; + const namePattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; + return hexPattern.test(id) || (namePattern.test(id) && id.length <= 128); +}; + +/** + * Validates that the shell is one of the allowed shells. + */ +export const isValidShell = (shell: string): boolean => { + const allowedShells = [ + "sh", + "bash", + "zsh", + "ash", + "/bin/sh", + "/bin/bash", + "/bin/zsh", + "/bin/ash", + ]; + return allowedShells.includes(shell); +}; + +export const readValidDirectory = (directory: string) => { + const { BASE_PATH } = paths(); + + const resolvedBase = path.resolve(BASE_PATH); + const resolvedDir = path.resolve(directory); + + return ( + resolvedDir === resolvedBase || + resolvedDir.startsWith(resolvedBase + path.sep) + ); +}; export const getShell = () => { + if (IS_CLOUD) { + return "NO_AVAILABLE"; + } switch (os.platform()) { case "win32": return "powershell.exe"; diff --git a/lefthook.yml b/lefthook.yml deleted file mode 100644 index 3f5a6d09f..000000000 --- a/lefthook.yml +++ /dev/null @@ -1,45 +0,0 @@ -# EXAMPLE USAGE: -# -# Refer for explanation to following link: -# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md -# -# pre-push: -# commands: -# packages-audit: -# tags: frontend security -# run: yarn audit -# gems-audit: -# tags: backend security -# run: bundle audit -# -# pre-commit: -# parallel: true -# commands: -# eslint: -# glob: "*.{js,ts,jsx,tsx}" -# run: yarn eslint {staged_files} -# rubocop: -# tags: backend style -# glob: "*.rb" -# exclude: '(^|/)(application|routes)\.rb$' -# run: bundle exec rubocop --force-exclusion {all_files} -# govet: -# tags: backend style -# files: git ls-files -m -# glob: "*.go" -# run: go vet {files} -# scripts: -# "hello.js": -# runner: node -# "any.go": -# runner: go run - -commit-msg: - commands: - commitlint: - # run: "npx commitlint --edit $1" - -pre-commit: - commands: - check: - # run: "pnpm check" diff --git a/package.json b/package.json index 1f59cc661..9a920c59c 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,9 @@ }, "devDependencies": { "@biomejs/biome": "2.1.1", - "@commitlint/cli": "^19.8.1", - "@commitlint/config-conventional": "^19.8.1", "@types/node": "^18.19.104", "dotenv": "16.4.5", "esbuild": "0.20.2", - "lefthook": "1.8.4", "lint-staged": "^15.5.2", "tsx": "4.16.2" }, @@ -43,11 +40,6 @@ "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true" ] }, - "commitlint": { - "extends": [ - "@commitlint/config-conventional" - ] - }, "resolutions": { "@types/react": "18.3.5", "@types/react-dom": "18.3.0" diff --git a/packages/server/src/db/constants.ts b/packages/server/src/db/constants.ts new file mode 100644 index 000000000..1d4ec2f1f --- /dev/null +++ b/packages/server/src/db/constants.ts @@ -0,0 +1,39 @@ +import fs from "node:fs"; + +export const { + DATABASE_URL, + POSTGRES_PASSWORD_FILE, + POSTGRES_USER = "dokploy", + POSTGRES_DB = "dokploy", + POSTGRES_HOST = "dokploy-postgres", + POSTGRES_PORT = "5432", +} = process.env; + +function readSecret(path: string): string { + try { + return fs.readFileSync(path, "utf8").trim(); + } catch { + throw new Error(`Cannot read secret at ${path}`); + } +} +export let dbUrl: string; +if (DATABASE_URL) { + // Compatibilidad legacy / overrides + dbUrl = DATABASE_URL; +} else if (POSTGRES_PASSWORD_FILE) { + const password = readSecret(POSTGRES_PASSWORD_FILE); + dbUrl = `postgres://${POSTGRES_USER}:${encodeURIComponent( + password, + )}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`; +} else { + console.warn(` + ⚠️ [DEPRECATED DATABASE CONFIG] + You are using the legacy hardcoded database credentials. + This mode WILL BE REMOVED in a future release. + + Please migrate to Docker Secrets using POSTGRES_PASSWORD_FILE. + Please execute this command in your server: curl -sSL https://dokploy.com/security/0.26.6.sh | bash + `); + dbUrl = + "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy"; +} diff --git a/packages/server/src/db/index.ts b/packages/server/src/db/index.ts index 3ac6e3940..e17002de9 100644 --- a/packages/server/src/db/index.ts +++ b/packages/server/src/db/index.ts @@ -1,5 +1,6 @@ import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; +import { dbUrl } from "./constants"; import * as schema from "./schema"; declare global { @@ -8,14 +9,16 @@ declare global { export let db: PostgresJsDatabase; if (process.env.NODE_ENV === "production") { - db = drizzle(postgres(process.env.DATABASE_URL!), { + db = drizzle(postgres(dbUrl), { schema, }); } else { if (!global.db) - global.db = drizzle(postgres(process.env.DATABASE_URL!), { + global.db = drizzle(postgres(dbUrl), { schema, }); db = global.db; } + +export { dbUrl }; diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index 51be7a7ea..088f68efa 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -214,6 +214,6 @@ export const apiUpdateUser = createSchema.partial().extend({ .optional(), password: z.string().optional(), currentPassword: z.string().optional(), - name: z.string().optional(), + firstName: z.string().optional(), lastName: z.string().optional(), }); diff --git a/packages/server/src/db/schema/web-server-settings.ts b/packages/server/src/db/schema/web-server-settings.ts index 92219091d..fe5cc5ad1 100644 --- a/packages/server/src/db/schema/web-server-settings.ts +++ b/packages/server/src/db/schema/web-server-settings.ts @@ -131,7 +131,10 @@ export const apiAssignDomain = z .object({ host: z.string(), certificateType: z.enum(["letsencrypt", "none", "custom"]), - letsEncryptEmail: z.string().email().optional().nullable(), + letsEncryptEmail: z + .union([z.string().email(), z.literal("")]) + .optional() + .nullable(), https: z.boolean().optional(), }) .required() diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index f28711dbf..c05ac1ab7 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,5 +1,6 @@ export * from "./auth/random-password"; export * from "./constants/index"; +export * from "./db/constants"; export * from "./db/validations/domain"; export * from "./db/validations/index"; export * from "./lib/auth"; diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 7508fadea..32e5e4a7e 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -1,10 +1,14 @@ import path from "node:path"; -import { paths } from "@dokploy/server/constants"; +import { IS_CLOUD, paths } from "@dokploy/server/constants"; +import { getDokployUrl } from "@dokploy/server/services/admin"; import { createServerDeployment, updateDeploymentStatus, } from "@dokploy/server/services/deployment"; -import { findServerById } from "@dokploy/server/services/server"; +import { + findServerById, + updateServerById, +} from "@dokploy/server/services/server"; import { getDefaultMiddlewares, getDefaultServerTraefikConfig, @@ -16,6 +20,15 @@ import { import slug from "slugify"; import { Client } from "ssh2"; import { recreateDirectory } from "../utils/filesystem/directory"; +import { setupMonitoring } from "./monitoring-setup"; + +const generateToken = () => { + const array = new Uint8Array(64); + crypto.getRandomValues(array); + return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join( + "", + ); +}; export const slugify = (text: string | undefined) => { if (!text) { @@ -59,6 +72,29 @@ export const serverSetup = async ( ); await installRequirements(serverId, onData); + if (IS_CLOUD) { + onData?.("\nConfiguring Monitoring: 🔄\n"); + + const baseUrl = await getDokployUrl(); + const token = generateToken(); + const urlCallback = `${baseUrl}/api/trpc/notification.receiveNotification`; + + // Update server with monitoring configuration + await updateServerById(serverId, { + metricsConfig: { + server: { + ...server.metricsConfig.server, + token: token, + urlCallback: urlCallback, + }, + containers: server.metricsConfig.containers, + }, + }); + + await setupMonitoring(serverId); + onData?.("\nMonitoring Configured: ✅\n"); + } + await updateDeploymentStatus(deployment.deploymentId, "done"); onData?.("\nSetup Server: ✅\n"); diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 5a0184a3d..5eede59d5 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -134,6 +134,7 @@ const getExportEnvCommand = (compose: ComposeNested) => { const envVars = getEnviromentVariablesObject( compose.env, compose.environment.project.env, + compose.environment.env, ); const exports = Object.entries(envVars) .map(([key, value]) => `${key}=${quote([value])}`) diff --git a/packages/server/src/utils/schedules/utils.ts b/packages/server/src/utils/schedules/utils.ts index 1a43d2af6..64657c9a6 100644 --- a/packages/server/src/utils/schedules/utils.ts +++ b/packages/server/src/utils/schedules/utils.ts @@ -1,6 +1,6 @@ import { createWriteStream } from "node:fs"; import path from "node:path"; -import { paths } from "@dokploy/server/constants"; +import { IS_CLOUD, paths } from "@dokploy/server/constants"; import type { Schedule } from "@dokploy/server/db/schema/schedule"; import { createDeploymentSchedule, @@ -93,6 +93,13 @@ export const runCommand = async (scheduleId: string) => { const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); try { + if (IS_CLOUD) { + writeStream.write( + "This feature is not available in the cloud version.", + ); + writeStream.end(); + return; + } writeStream.write( `docker exec ${containerId} ${shellType} -c ${command}\n`, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd5ef0a96..d5fc7f074 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,12 +15,6 @@ importers: '@biomejs/biome': specifier: 2.1.1 version: 2.1.1 - '@commitlint/cli': - specifier: ^19.8.1 - version: 19.8.1(@types/node@18.19.104)(typescript@5.8.3) - '@commitlint/config-conventional': - specifier: ^19.8.1 - version: 19.8.1 '@types/node': specifier: ^18.19.104 version: 18.19.104 @@ -30,9 +24,6 @@ importers: esbuild: specifier: 0.20.2 version: 0.20.2 - lefthook: - specifier: 1.8.4 - version: 1.8.4 lint-staged: specifier: ^15.5.2 version: 15.5.2 @@ -879,14 +870,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - '@babel/runtime-corejs3@7.27.3': resolution: {integrity: sha512-ZYcgrwb+dkWNcDlsTe4fH1CMdqMDSJ5lWFd1by8Si2pI54XcQjte/+ViIPqAk7EAWisaUxvQ89grv+bNX2x8zg==} engines: {node: '>=6.9.0'} @@ -1002,75 +985,6 @@ packages: '@codemirror/view@6.36.8': resolution: {integrity: sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==} - '@commitlint/cli@19.8.1': - resolution: {integrity: sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==} - engines: {node: '>=v18'} - hasBin: true - - '@commitlint/config-conventional@19.8.1': - resolution: {integrity: sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==} - engines: {node: '>=v18'} - - '@commitlint/config-validator@19.8.1': - resolution: {integrity: sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==} - engines: {node: '>=v18'} - - '@commitlint/ensure@19.8.1': - resolution: {integrity: sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==} - engines: {node: '>=v18'} - - '@commitlint/execute-rule@19.8.1': - resolution: {integrity: sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==} - engines: {node: '>=v18'} - - '@commitlint/format@19.8.1': - resolution: {integrity: sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==} - engines: {node: '>=v18'} - - '@commitlint/is-ignored@19.8.1': - resolution: {integrity: sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==} - engines: {node: '>=v18'} - - '@commitlint/lint@19.8.1': - resolution: {integrity: sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==} - engines: {node: '>=v18'} - - '@commitlint/load@19.8.1': - resolution: {integrity: sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==} - engines: {node: '>=v18'} - - '@commitlint/message@19.8.1': - resolution: {integrity: sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==} - engines: {node: '>=v18'} - - '@commitlint/parse@19.8.1': - resolution: {integrity: sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==} - engines: {node: '>=v18'} - - '@commitlint/read@19.8.1': - resolution: {integrity: sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==} - engines: {node: '>=v18'} - - '@commitlint/resolve-extends@19.8.1': - resolution: {integrity: sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==} - engines: {node: '>=v18'} - - '@commitlint/rules@19.8.1': - resolution: {integrity: sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==} - engines: {node: '>=v18'} - - '@commitlint/to-lines@19.8.1': - resolution: {integrity: sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==} - engines: {node: '>=v18'} - - '@commitlint/top-level@19.8.1': - resolution: {integrity: sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==} - engines: {node: '>=v18'} - - '@commitlint/types@19.8.1': - resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} - engines: {node: '>=v18'} - '@dokploy/trpc-openapi@0.0.4': resolution: {integrity: sha512-a7VKunKu9arq57bP9MPH7ikJuKfT5SILnNy70vMqf1stm5IrqMG3Y7CIFprFe0DZiw3bwjue0KpETIATBftN6w==} peerDependencies: @@ -3932,9 +3846,6 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/conventional-commits-parser@5.0.1': - resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} - '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -4152,10 +4063,6 @@ packages: '@xterm/xterm@5.5.0': resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} - JSONStream@1.3.5: - resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} - hasBin: true - abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -4215,9 +4122,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -4280,9 +4184,6 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} - array-ify@1.0.0: - resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -4423,10 +4324,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -4595,9 +4492,6 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} - compare-func@2.0.0: - resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -4614,19 +4508,6 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - conventional-changelog-angular@7.0.0: - resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} - engines: {node: '>=16'} - - conventional-changelog-conventionalcommits@7.0.2: - resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} - engines: {node: '>=16'} - - conventional-commits-parser@5.0.0: - resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} - engines: {node: '>=16'} - hasBin: true - cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} @@ -4643,23 +4524,6 @@ packages: core-js@3.42.0: resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} - cosmiconfig-typescript-loader@6.1.0: - resolution: {integrity: sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==} - engines: {node: '>=v18'} - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=9' - typescript: '>=5' - - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - cpu-features@0.0.10: resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} engines: {node: '>=10.0.0'} @@ -4742,10 +4606,6 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} - dargs@8.1.0: - resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} - engines: {node: '>=12'} - date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -4889,10 +4749,6 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} - dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -5036,10 +4892,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5048,9 +4900,6 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -5146,9 +4995,6 @@ packages: fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-equals@5.2.2: resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} engines: {node: '>=6.0.0'} @@ -5167,9 +5013,6 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -5184,10 +5027,6 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - find-up@7.0.0: - resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} - engines: {node: '>=18'} - follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -5297,11 +5136,6 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} - git-raw-commits@4.0.0: - resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} - engines: {node: '>=16'} - hasBin: true - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5318,10 +5152,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - global-directory@4.0.1: - resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} - engines: {node: '>=18'} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -5458,16 +5288,9 @@ packages: resolution: {integrity: sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==} engines: {node: '>=0.10.0'} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - import-in-the-middle@1.14.2: resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} - indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -5490,10 +5313,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ini@4.1.1: - resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} @@ -5569,9 +5388,6 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -5623,10 +5439,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -5639,10 +5451,6 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-text-path@2.0.0: - resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} - engines: {node: '>=8'} - is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -5661,10 +5469,6 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true - jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -5703,22 +5507,12 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - jsonparse@1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} - jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -5781,60 +5575,6 @@ packages: leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - lefthook-darwin-arm64@1.8.4: - resolution: {integrity: sha512-OS5MsU0gvd8LYSpuQCHtmDUqwNrJ/LjCO0LGC1wNepY4OkuVl9DfX+rQ506CVUQYZiGVcwy2/qPOOBjNzA5+wQ==} - cpu: [arm64] - os: [darwin] - - lefthook-darwin-x64@1.8.4: - resolution: {integrity: sha512-QLRsqK9aTMRcVW8qz4pzI2OWnGCEcaEPJlIiFjwstYsS+wfkooxOS0UkfVMjy+QoGgEcki+cxF/FoY7lE7DDtw==} - cpu: [x64] - os: [darwin] - - lefthook-freebsd-arm64@1.8.4: - resolution: {integrity: sha512-chnQ1m/Cmn9c0sLdk5HL2SToE5LBJv5uQMdH1IGRRcw+nEqWqrMnDXvM75caiJAyjmUGvPH3czKTJDzTFV1E+A==} - cpu: [arm64] - os: [freebsd] - - lefthook-freebsd-x64@1.8.4: - resolution: {integrity: sha512-KQi+WBUdnGLnK0rHOR58kbMH5TDVN1ZjZLu66Pv9FCG7Y7shR1qtaTXu+wmxdRhMvaLeQIXRsUEPjNRC66yMmA==} - cpu: [x64] - os: [freebsd] - - lefthook-linux-arm64@1.8.4: - resolution: {integrity: sha512-CXNcqIskLwTwQARidGdFqmNxpvOU3jsWPK4KA7pq2+QmlWJ64w98ebMvNBoUmRUCXqzmUm7Udf/jpfz2fobewQ==} - cpu: [arm64] - os: [linux] - - lefthook-linux-x64@1.8.4: - resolution: {integrity: sha512-pVNITkFBxUCEtamWSM/res2Gd48+m9YKbNyIBndAuZVC5pKV5aGKZy2DNq6PWUPYiUDPx+7hoAtCJg/tlAiqhw==} - cpu: [x64] - os: [linux] - - lefthook-openbsd-arm64@1.8.4: - resolution: {integrity: sha512-l+i/Dg5X36kYzhpMGSPE3rMbWy1KSytbLB9lY1PmxYb6LRH6iQTYIoxvLabVUwSBPSq8HtIFa50+bvC5+scfVA==} - cpu: [arm64] - os: [openbsd] - - lefthook-openbsd-x64@1.8.4: - resolution: {integrity: sha512-CqhDDPPX8oHzMLgNi/Reba823DRzj+eMNWQ8axvSiIG+zmG1w20xZH5QSs/mD3tjrND90yfDd90mWMt181qPyA==} - cpu: [x64] - os: [openbsd] - - lefthook-windows-arm64@1.8.4: - resolution: {integrity: sha512-dvpvorICmVjmw29Aiczg7DcaSzkd86bEBomiGq4UsAEk3+7ExLrlWJDLFsI6xLjMKmTxy+F7eXb2uDtuFC1N4g==} - cpu: [arm64] - os: [win32] - - lefthook-windows-x64@1.8.4: - resolution: {integrity: sha512-e+y8Jt4/7PnoplhOuK48twjGVJEsU4T3J5kxD4mWfl6Cbit0YSn4bme9nW41eqCqTUqOm+ky29XlfnPHFX5ZNA==} - cpu: [x64] - os: [win32] - - lefthook@1.8.4: - resolution: {integrity: sha512-XNyMaTWNRuADOaocYiHidgNkNDz8SCekpdNJ7lqceFcBT2zjumnb28/o7IMaNROpLBZdQkLkJXSeaQWGqn3kog==} - hasBin: true - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -5859,10 +5599,6 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - locate-path@7.2.0: - resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -5899,30 +5635,12 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - - lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - - lodash.upperfirst@4.3.1: - resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -6018,10 +5736,6 @@ packages: resolution: {integrity: sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==} engines: {node: '>= 4.0.0'} - meow@12.1.1: - resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} - engines: {node: '>=16.10'} - merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -6423,10 +6137,6 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@5.0.0: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} @@ -6435,10 +6145,6 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-locate@6.0.0: - resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -6446,20 +6152,12 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - parse-entities@2.0.0: resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -6471,10 +6169,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-exists@5.0.0: - resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -6979,10 +6673,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - require-in-the-middle@7.5.2: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} @@ -6999,14 +6689,6 @@ packages: resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -7354,10 +7036,6 @@ packages: temporal-spec@0.2.4: resolution: {integrity: sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==} - text-extensions@2.4.0: - resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} - engines: {node: '>=8'} - theming@3.3.0: resolution: {integrity: sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==} engines: {node: '>=8'} @@ -7380,9 +7058,6 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -7392,9 +7067,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.1: - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} - tinypool@0.8.4: resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} @@ -7527,10 +7199,6 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7914,14 +7582,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.27.1 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/helper-validator-identifier@7.27.1': {} - '@babel/runtime-corejs3@7.27.3': dependencies: core-js-pure: 3.42.0 @@ -8058,116 +7718,6 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 - '@commitlint/cli@19.8.1(@types/node@18.19.104)(typescript@5.8.3)': - dependencies: - '@commitlint/format': 19.8.1 - '@commitlint/lint': 19.8.1 - '@commitlint/load': 19.8.1(@types/node@18.19.104)(typescript@5.8.3) - '@commitlint/read': 19.8.1 - '@commitlint/types': 19.8.1 - tinyexec: 1.0.1 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - typescript - - '@commitlint/config-conventional@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - conventional-changelog-conventionalcommits: 7.0.2 - - '@commitlint/config-validator@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - ajv: 8.17.1 - - '@commitlint/ensure@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - lodash.snakecase: 4.1.1 - lodash.startcase: 4.4.0 - lodash.upperfirst: 4.3.1 - - '@commitlint/execute-rule@19.8.1': {} - - '@commitlint/format@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - chalk: 5.4.1 - - '@commitlint/is-ignored@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - semver: 7.7.3 - - '@commitlint/lint@19.8.1': - dependencies: - '@commitlint/is-ignored': 19.8.1 - '@commitlint/parse': 19.8.1 - '@commitlint/rules': 19.8.1 - '@commitlint/types': 19.8.1 - - '@commitlint/load@19.8.1(@types/node@18.19.104)(typescript@5.8.3)': - dependencies: - '@commitlint/config-validator': 19.8.1 - '@commitlint/execute-rule': 19.8.1 - '@commitlint/resolve-extends': 19.8.1 - '@commitlint/types': 19.8.1 - chalk: 5.4.1 - cosmiconfig: 9.0.0(typescript@5.8.3) - cosmiconfig-typescript-loader: 6.1.0(@types/node@18.19.104)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - lodash.uniq: 4.5.0 - transitivePeerDependencies: - - '@types/node' - - typescript - - '@commitlint/message@19.8.1': {} - - '@commitlint/parse@19.8.1': - dependencies: - '@commitlint/types': 19.8.1 - conventional-changelog-angular: 7.0.0 - conventional-commits-parser: 5.0.0 - - '@commitlint/read@19.8.1': - dependencies: - '@commitlint/top-level': 19.8.1 - '@commitlint/types': 19.8.1 - git-raw-commits: 4.0.0 - minimist: 1.2.8 - tinyexec: 1.0.1 - - '@commitlint/resolve-extends@19.8.1': - dependencies: - '@commitlint/config-validator': 19.8.1 - '@commitlint/types': 19.8.1 - global-directory: 4.0.1 - import-meta-resolve: 4.1.0 - lodash.mergewith: 4.6.2 - resolve-from: 5.0.0 - - '@commitlint/rules@19.8.1': - dependencies: - '@commitlint/ensure': 19.8.1 - '@commitlint/message': 19.8.1 - '@commitlint/to-lines': 19.8.1 - '@commitlint/types': 19.8.1 - - '@commitlint/to-lines@19.8.1': {} - - '@commitlint/top-level@19.8.1': - dependencies: - find-up: 7.0.0 - - '@commitlint/types@19.8.1': - dependencies: - '@types/conventional-commits-parser': 5.0.1 - chalk: 5.4.1 - '@dokploy/trpc-openapi@0.0.4(@trpc/server@10.45.2)(@types/node@18.19.104)(zod@3.25.32)': dependencies: '@trpc/server': 10.45.2 @@ -11254,10 +10804,6 @@ snapshots: dependencies: '@types/node': 20.17.51 - '@types/conventional-commits-parser@5.0.1': - dependencies: - '@types/node': 20.17.51 - '@types/d3-array@3.2.1': {} '@types/d3-color@3.1.3': {} @@ -11513,11 +11059,6 @@ snapshots: '@xterm/xterm@5.5.0': {} - JSONStream@1.3.5: - dependencies: - jsonparse: 1.3.1 - through: 2.3.8 - abbrev@1.1.1: {} abbrev@2.0.0: {} @@ -11578,13 +11119,6 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.32 - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -11635,8 +11169,6 @@ snapshots: dependencies: tslib: 2.8.1 - array-ify@1.0.0: {} - array-union@2.1.0: {} asn1@0.2.6: @@ -11828,8 +11360,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - camelcase-css@2.0.1: {} camelcase@5.3.1: {} @@ -11988,11 +11518,6 @@ snapshots: commander@9.5.0: {} - compare-func@2.0.0: - dependencies: - array-ify: 1.0.0 - dot-prop: 5.3.0 - concat-map@0.0.1: {} confbox@0.1.8: {} @@ -12008,21 +11533,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - conventional-changelog-angular@7.0.0: - dependencies: - compare-func: 2.0.0 - - conventional-changelog-conventionalcommits@7.0.2: - dependencies: - compare-func: 2.0.0 - - conventional-commits-parser@5.0.0: - dependencies: - JSONStream: 1.3.5 - is-text-path: 2.0.0 - meow: 12.1.1 - split2: 4.2.0 - cookie-es@1.2.2: {} copy-anything@3.0.5: @@ -12037,22 +11547,6 @@ snapshots: core-js@3.42.0: {} - cosmiconfig-typescript-loader@6.1.0(@types/node@18.19.104)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): - dependencies: - '@types/node': 18.19.104 - cosmiconfig: 9.0.0(typescript@5.8.3) - jiti: 2.4.2 - typescript: 5.8.3 - - cosmiconfig@9.0.0(typescript@5.8.3): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.8.3 - cpu-features@0.0.10: dependencies: buildcheck: 0.0.6 @@ -12136,8 +11630,6 @@ snapshots: d3-timer@3.0.1: {} - dargs@8.1.0: {} - date-fns@3.6.0: {} dateformat@4.6.3: {} @@ -12261,10 +11753,6 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dot-prop@5.3.0: - dependencies: - is-obj: 2.0.0 - dotenv@16.4.5: {} drange@1.1.1: {} @@ -12328,16 +11816,10 @@ snapshots: entities@4.5.0: {} - env-paths@2.2.1: {} - env-paths@3.0.0: {} environment@1.1.0: {} - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -12509,8 +11991,6 @@ snapshots: fast-deep-equal@2.0.1: {} - fast-deep-equal@3.1.3: {} - fast-equals@5.2.2: {} fast-glob@3.3.3: @@ -12527,8 +12007,6 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.0.6: {} - fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -12546,12 +12024,6 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - find-up@7.0.0: - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - unicorn-magic: 0.1.0 - follow-redirects@1.15.9: {} foreground-child@3.3.1: @@ -12668,12 +12140,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - git-raw-commits@4.0.0: - dependencies: - dargs: 8.1.0 - meow: 12.1.1 - split2: 4.2.0 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -12700,10 +12166,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - global-directory@4.0.1: - dependencies: - ini: 4.1.1 - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -12882,11 +12344,6 @@ snapshots: immutable@3.8.2: {} - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - import-in-the-middle@1.14.2: dependencies: acorn: 8.14.1 @@ -12894,8 +12351,6 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 - import-meta-resolve@4.1.0: {} - indent-string@4.0.0: {} indent-string@5.0.0: {} @@ -12911,8 +12366,6 @@ snapshots: ini@1.3.8: {} - ini@4.1.1: {} - inline-style-parser@0.2.4: {} inngest@3.40.1(h3@1.15.3)(hono@4.7.10)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3): @@ -12991,8 +12444,6 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - is-arrayish@0.2.1: {} - is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -13031,18 +12482,12 @@ snapshots: is-number@7.0.0: {} - is-obj@2.0.0: {} - is-plain-obj@4.1.0: {} is-stream@2.0.1: {} is-stream@3.0.0: {} - is-text-path@2.0.0: - dependencies: - text-extensions: 2.4.0 - is-what@4.1.16: {} isexe@2.0.0: {} @@ -13057,8 +12502,6 @@ snapshots: jiti@1.21.7: {} - jiti@2.4.2: {} - jose@5.10.0: {} joycon@3.1.1: {} @@ -13091,16 +12534,10 @@ snapshots: json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@1.0.0: {} - json-schema@0.4.0: {} json-stringify-safe@5.0.1: {} - jsonparse@1.3.1: {} - jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 @@ -13225,49 +12662,6 @@ snapshots: leac@0.6.0: {} - lefthook-darwin-arm64@1.8.4: - optional: true - - lefthook-darwin-x64@1.8.4: - optional: true - - lefthook-freebsd-arm64@1.8.4: - optional: true - - lefthook-freebsd-x64@1.8.4: - optional: true - - lefthook-linux-arm64@1.8.4: - optional: true - - lefthook-linux-x64@1.8.4: - optional: true - - lefthook-openbsd-arm64@1.8.4: - optional: true - - lefthook-openbsd-x64@1.8.4: - optional: true - - lefthook-windows-arm64@1.8.4: - optional: true - - lefthook-windows-x64@1.8.4: - optional: true - - lefthook@1.8.4: - optionalDependencies: - lefthook-darwin-arm64: 1.8.4 - lefthook-darwin-x64: 1.8.4 - lefthook-freebsd-arm64: 1.8.4 - lefthook-freebsd-x64: 1.8.4 - lefthook-linux-arm64: 1.8.4 - lefthook-linux-x64: 1.8.4 - lefthook-openbsd-arm64: 1.8.4 - lefthook-openbsd-x64: 1.8.4 - lefthook-windows-arm64: 1.8.4 - lefthook-windows-x64: 1.8.4 - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -13305,10 +12699,6 @@ snapshots: dependencies: p-locate: 4.1.0 - locate-path@7.2.0: - dependencies: - p-locate: 6.0.0 - lodash.camelcase@4.3.0: {} lodash.castarray@4.4.0: {} @@ -13333,22 +12723,10 @@ snapshots: lodash.isstring@4.0.1: {} - lodash.kebabcase@4.1.1: {} - lodash.merge@4.6.2: {} - lodash.mergewith@4.6.2: {} - lodash.once@4.1.1: {} - lodash.snakecase@4.1.1: {} - - lodash.startcase@4.4.0: {} - - lodash.uniq@4.5.0: {} - - lodash.upperfirst@4.3.1: {} - lodash@4.17.21: {} log-update@6.1.0: @@ -13503,8 +12881,6 @@ snapshots: tree-dump: 1.0.3(tslib@2.8.1) tslib: 2.8.1 - meow@12.1.1: {} - merge-descriptors@1.0.3: {} merge-stream@2.0.0: {} @@ -13942,10 +13318,6 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@4.0.0: - dependencies: - yocto-queue: 1.2.1 - p-limit@5.0.0: dependencies: yocto-queue: 1.2.1 @@ -13954,18 +13326,10 @@ snapshots: dependencies: p-limit: 2.3.0 - p-locate@6.0.0: - dependencies: - p-limit: 4.0.0 - p-try@2.2.0: {} package-json-from-dist@1.0.1: {} - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - parse-entities@2.0.0: dependencies: character-entities: 1.2.4 @@ -13985,13 +13349,6 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - parseley@0.12.1: dependencies: leac: 0.6.0 @@ -14001,8 +13358,6 @@ snapshots: path-exists@4.0.0: {} - path-exists@5.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -14545,8 +13900,6 @@ snapshots: require-directory@2.1.1: {} - require-from-string@2.0.2: {} - require-in-the-middle@7.5.2: dependencies: debug: 4.4.1 @@ -14563,10 +13916,6 @@ snapshots: resolve-alpn@1.2.1: {} - resolve-from@4.0.0: {} - - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} resolve@1.22.10: @@ -15021,8 +14370,6 @@ snapshots: temporal-spec@0.2.4: {} - text-extensions@2.4.0: {} - theming@3.3.0(react@18.2.0): dependencies: hoist-non-react-statics: 3.3.2 @@ -15047,16 +14394,12 @@ snapshots: dependencies: real-require: 0.2.0 - through@2.3.8: {} - tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} tinybench@2.9.0: {} - tinyexec@1.0.1: {} - tinypool@0.8.4: {} tinyspy@2.2.1: {} @@ -15160,8 +14503,6 @@ snapshots: undici@6.21.3: {} - unicorn-magic@0.1.0: {} - unified@11.0.5: dependencies: '@types/unist': 3.0.3