diff --git a/apps/dokploy/__test__/drop/drop.test.ts b/apps/dokploy/__test__/drop/drop.test.ts index 301278dc3..a9bc178a2 100644 --- a/apps/dokploy/__test__/drop/drop.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.ts @@ -27,6 +27,7 @@ if (typeof window === "undefined") { const baseApp: ApplicationNested = { railpackVersion: "0.2.2", applicationId: "", + previewLabels: [], herokuVersion: "", giteaBranch: "", giteaBuildPath: "", diff --git a/apps/dokploy/__test__/env/shared.test.ts b/apps/dokploy/__test__/env/shared.test.ts index 4a8448aa9..5e231a5cc 100644 --- a/apps/dokploy/__test__/env/shared.test.ts +++ b/apps/dokploy/__test__/env/shared.test.ts @@ -177,3 +177,77 @@ COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'" ]); }); }); + +describe("prepareEnvironmentVariables (self references)", () => { + it("resolves self references correctly", () => { + const serviceEnv = ` +ENVIRONMENT=staging +DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db +SELF_REF=\${{ENVIRONMENT}} +`; + + const resolved = prepareEnvironmentVariables(serviceEnv, ""); + + expect(resolved).toEqual([ + "ENVIRONMENT=staging", + "DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db", + "SELF_REF=staging", + ]); + }); + + it("throws on undefined self references", () => { + const serviceEnv = ` +MISSING_VAR=\${{UNDEFINED_VAR}} +`; + + expect(() => prepareEnvironmentVariables(serviceEnv, "")).toThrow( + "Invalid service environment variable: UNDEFINED_VAR", + ); + }); + + it("allows overriding and still resolving from self", () => { + const serviceEnv = ` +ENVIRONMENT=production +OVERRIDE_ENV=\${{ENVIRONMENT}} +`; + + const resolved = prepareEnvironmentVariables(serviceEnv, ""); + + expect(resolved).toEqual([ + "ENVIRONMENT=production", + "OVERRIDE_ENV=production", + ]); + }); + + it("resolves multiple self references inside one value", () => { + const serviceEnv = ` +ENVIRONMENT=staging +APP_NAME=MyApp +COMPLEX=\${{APP_NAME}}-\${{ENVIRONMENT}}-\${{APP_NAME}} +`; + + const resolved = prepareEnvironmentVariables(serviceEnv, ""); + + expect(resolved).toEqual([ + "ENVIRONMENT=staging", + "APP_NAME=MyApp", + "COMPLEX=MyApp-staging-MyApp", + ]); + }); + + it("handles quotes with self references", () => { + const serviceEnv = ` +ENVIRONMENT=production +QUOTED="'\${{ENVIRONMENT}}'" +MIXED="\"Double \${{ENVIRONMENT}}\"" +`; + + const resolved = prepareEnvironmentVariables(serviceEnv, ""); + + expect(resolved).toEqual([ + "ENVIRONMENT=production", + "QUOTED='production'", + 'MIXED="Double production"', + ]); + }); +}); diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 8d9f78aba..ff8a99620 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -6,6 +6,7 @@ const baseApp: ApplicationNested = { railpackVersion: "0.2.2", rollbackActive: false, applicationId: "", + previewLabels: [], herokuVersion: "", giteaRepository: "", giteaOwner: "", diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index 3605480a0..16c916d93 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -1,9 +1,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { Settings2 } from "lucide-react"; +import { HelpCircle, Plus, Settings2, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -33,6 +34,12 @@ import { SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { api } from "@/utils/api"; const schema = z @@ -42,6 +49,7 @@ const schema = z wildcardDomain: z.string(), port: z.number(), previewLimit: z.number(), + previewLabels: z.array(z.string()).optional(), previewHttps: z.boolean(), previewPath: z.string(), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]), @@ -81,6 +89,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { wildcardDomain: "*.traefik.me", port: 3000, previewLimit: 3, + previewLabels: [], previewHttps: false, previewPath: "/", previewCertificateType: "none", @@ -102,6 +111,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { buildArgs: data.previewBuildArgs || "", wildcardDomain: data.previewWildcard || "*.traefik.me", port: data.previewPort || 3000, + previewLabels: data.previewLabels || [], previewLimit: data.previewLimit || 3, previewHttps: data.previewHttps || false, previewPath: data.previewPath || "/", @@ -119,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { previewBuildArgs: formData.buildArgs, previewWildcard: formData.wildcardDomain, previewPort: formData.port, + previewLabels: formData.previewLabels, applicationId, previewLimit: formData.previewLimit, previewHttps: formData.previewHttps, @@ -200,6 +211,90 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { )} /> + ( + +
+ Preview Labels + + + + + + +

+ Add a labels that will trigger a preview + deployment for a pull request. If no labels + are specified, all pull requests will trigger + a preview deployment. +

+
+
+
+
+
+ {field.value?.map((label, index) => ( + + {label} + { + const newLabels = [...(field.value || [])]; + newLabels.splice(index, 1); + field.onChange(newLabels); + }} + /> + + ))} +
+
+ + { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const label = input.value.trim(); + if (label) { + field.onChange([ + ...(field.value || []), + label, + ]); + input.value = ""; + } + } + }} + /> + + +
+ +
+ )} + /> { const [isOpen, setIsOpen] = useState(false); const [cacheType, setCacheType] = useState("cache"); + const [keepLatestCountInput, setKeepLatestCountInput] = useState(""); const utils = api.useUtils(); const form = useForm>({ @@ -117,7 +123,7 @@ export const HandleVolumeBackups = ({ cronExpression: "", volumeName: "", prefix: "", - // keepLatestCount: undefined, + keepLatestCount: undefined, turnOff: false, enabled: true, serviceName: "", @@ -173,13 +179,19 @@ export const HandleVolumeBackups = ({ cronExpression: volumeBackup.cronExpression, volumeName: volumeBackup.volumeName || "", prefix: volumeBackup.prefix, - // keepLatestCount: volumeBackup.keepLatestCount || undefined, + keepLatestCount: volumeBackup.keepLatestCount || undefined, turnOff: volumeBackup.turnOff, enabled: volumeBackup.enabled || false, serviceName: volumeBackup.serviceName || "", destinationId: volumeBackup.destinationId, serviceType: volumeBackup.serviceType, }); + setKeepLatestCountInput( + volumeBackup.keepLatestCount !== null && + volumeBackup.keepLatestCount !== undefined + ? String(volumeBackup.keepLatestCount) + : "", + ); } }, [form, volumeBackup, volumeBackupId]); @@ -190,8 +202,12 @@ export const HandleVolumeBackups = ({ const onSubmit = async (values: z.infer) => { if (!id && !volumeBackupId) return; + const preparedKeepLatestCount = + keepLatestCountInput === "" ? null : (values.keepLatestCount ?? null); + await mutateAsync({ ...values, + keepLatestCount: preparedKeepLatestCount, destinationId: values.destinationId, volumeBackupId: volumeBackupId || "", serviceType: volumeBackupType, @@ -257,9 +273,8 @@ export const HandleVolumeBackups = ({ @@ -600,29 +615,38 @@ export const HandleVolumeBackups = ({ )} /> - {/* ( - Keep Latest Count + Keep Latest Backups - field.onChange(Number(e.target.value) || undefined) - } + type="number" + min={1} + autoComplete="off" + placeholder="Leave empty to keep all" + value={keepLatestCountInput} + onChange={(e) => { + const raw = e.target.value; + setKeepLatestCountInput(raw); + if (raw === "") { + field.onChange(undefined); + } else if (/^\d+$/.test(raw)) { + field.onChange(Number(raw)); + } + }} /> - Number of backup files to keep (optional) + How many recent backups to keep. Empty means no cleanup. )} - /> */} + /> { } }; + const isDisabled = + (data && + "applicationStatus" in data && + data?.applicationStatus === "running") || + (data && "composeStatus" in data && data?.composeStatus === "running"); + return ( @@ -202,6 +209,12 @@ export const DeleteService = ({ id, type }: Props) => { + {isDisabled && ( + + Cannot delete the service while it is running. Please wait for the + build to finish and then try again. + + )} + + +

+ Are you sure you want to delete{" "} + {selectedServices.length} services? This + action cannot be undone. +

+ {selectedServicesWithRunningStatus.length > + 0 && ( + + Warning:{" "} + { + selectedServicesWithRunningStatus.length + }{" "} + of the selected services are currently + running. Please stop these services + first before deleting:{" "} + {selectedServicesWithRunningStatus + .map((s) => s.name) + .join(", ")} + + )} + + } type="destructive" - onClick={handleBulkDelete} + disabled={ + selectedServicesWithRunningStatus.length > 0 + } + onClick={() => setIsBulkDeleteDialogOpen(true)} >
+ + {/* Bulk Delete Dialog */} + + + + Delete Services + + Are you sure you want to delete{" "} + {selectedServices.length} service + {selectedServices.length !== 1 ? "s" : ""}? + This action cannot be undone. + + + +
+ {/* Show services to be deleted */} +
+ {selectedServices.map((serviceId) => { + const service = filteredServices.find( + (s) => s.id === serviceId, + ); + return service ? ( +
+ + {service.type} + + {service.name} +
+ ) : null; + })} +
+ + {/* Volume deletion option for compose services */} + {(() => { + const servicesWithVolumeSupport = + selectedServices.filter((serviceId) => { + const service = filteredServices.find( + (s) => s.id === serviceId, + ); + // Currently only compose services support volume deletion + return service?.type === "compose"; + }); + + if (servicesWithVolumeSupport.length === 0) + return null; + + return ( +
+
+ + setDeleteVolumes(checked === true) + } + /> + +
+

+ Volume deletion is available for:{" "} + {servicesWithVolumeSupport.length}{" "} + compose service + {servicesWithVolumeSupport.length !== 1 + ? "s" + : ""} +

+
+ ); + })()} +
+ + + + + +
+
diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index df7b34e11..8127b41fd 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -329,6 +329,7 @@ export default function Home({ IS_CLOUD }: Props) { maxLength={6} pattern={REGEXP_ONLY_DIGITS} autoComplete="off" + autoFocus > diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index ed4277c1d..51b9ba95c 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -20,6 +20,7 @@ import { } from "@dokploy/server/services/user"; import { getProviderHeaders, + getProviderName, type Model, } from "@dokploy/server/utils/ai/select-ai-provider"; import { TRPCError } from "@trpc/server"; @@ -47,11 +48,24 @@ export const aiRouter = createTRPCRouter({ }), getModels: protectedProcedure - .input(z.object({ apiUrl: z.string().min(1), apiKey: z.string().min(1) })) + .input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() })) .query(async ({ input }) => { try { + const providerName = getProviderName(input.apiUrl); const headers = getProviderHeaders(input.apiUrl, input.apiKey); - const response = await fetch(`${input.apiUrl}/models`, { headers }); + let response = null; + switch (providerName) { + case "ollama": + response = await fetch(`${input.apiUrl}/api/tags`, { headers }); + break; + default: + if (!input.apiKey) + throw new TRPCError({ + code: "BAD_REQUEST", + message: "API key must contain at least 1 character(s)", + }); + response = await fetch(`${input.apiUrl}/models`, { headers }); + } if (!response.ok) { const errorText = await response.text(); diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index a403f8768..ad1ade43d 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -80,7 +80,7 @@ export const redisRouter = createTRPCRouter({ type: "volume", }); - return true; + return newRedis; } catch (error) { throw error; } diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 7ae0b6e85..02678b990 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -45,7 +45,7 @@ import { } from "@dokploy/server"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; import { TRPCError } from "@trpc/server"; -import { sql } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { dump, load } from "js-yaml"; import { scheduledJobs, scheduleJob } from "node-schedule"; import { z } from "zod"; @@ -60,6 +60,8 @@ import { apiServerSchema, apiTraefikConfig, apiUpdateDockerCleanup, + projects, + server, } from "@/server/db/schema"; import { removeJob, schedule } from "@/server/utils/backup"; import packageInfo from "../../../package.json"; @@ -706,6 +708,18 @@ export const settingsRouter = createTRPCRouter({ isCloud: publicProcedure.query(async () => { return IS_CLOUD; }), + isUserSubscribed: protectedProcedure.query(async ({ ctx }) => { + const haveServers = await db.query.server.findMany({ + where: eq(server.organizationId, ctx.session?.activeOrganizationId || ""), + }); + const haveProjects = await db.query.projects.findMany({ + where: eq( + projects.organizationId, + ctx.session?.activeOrganizationId || "", + ), + }); + return haveServers.length > 0 || haveProjects.length > 0; + }), health: publicProcedure.query(async () => { if (IS_CLOUD) { try { diff --git a/apps/dokploy/setup.ts b/apps/dokploy/setup.ts index 7abf9fa2d..13590e4e7 100644 --- a/apps/dokploy/setup.ts +++ b/apps/dokploy/setup.ts @@ -21,7 +21,7 @@ import { await initializeNetwork(); createDefaultTraefikConfig(); createDefaultServerTraefikConfig(); - await execAsync("docker pull traefik:v3.1.2"); + await execAsync("docker pull traefik:v3.5.0"); await initializeStandaloneTraefik(); await initializeRedis(); await initializePostgres(); diff --git a/packages/server/package.json b/packages/server/package.json index eeee8f831..dbf7d3c65 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -28,13 +28,13 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@ai-sdk/anthropic": "^1.2.12", - "@ai-sdk/azure": "^1.3.23", - "@ai-sdk/cohere": "^1.2.10", - "@ai-sdk/deepinfra": "^0.0.4", - "@ai-sdk/mistral": "^1.2.8", - "@ai-sdk/openai": "^1.3.22", - "@ai-sdk/openai-compatible": "^0.0.13", + "@ai-sdk/anthropic": "^2.0.5", + "@ai-sdk/azure": "^2.0.16", + "@ai-sdk/cohere": "^2.0.4", + "@ai-sdk/deepinfra": "^1.0.10", + "@ai-sdk/mistral": "^2.0.7", + "@ai-sdk/openai": "^2.0.16", + "@ai-sdk/openai-compatible": "^1.0.10", "@better-auth/utils": "0.2.4", "@faker-js/faker": "^8.4.1", "@octokit/auth-app": "^6.1.3", @@ -44,7 +44,8 @@ "@react-email/components": "^0.0.21", "@trpc/server": "^10.45.2", "adm-zip": "^0.5.16", - "ai": "^4.3.16", + "ai": "^5.0.17", + "ai-sdk-ollama": "^0.5.1", "bcrypt": "5.1.1", "better-auth": "v1.2.8-beta.7", "bl": "6.0.11", @@ -65,7 +66,6 @@ "node-schedule": "2.1.1", "nodemailer": "6.9.14", "octokit": "3.1.2", - "ollama-ai-provider": "^1.2.0", "otpauth": "^9.4.0", "pino": "9.4.0", "pino-pretty": "11.2.2", diff --git a/packages/server/src/db/schema/ai.ts b/packages/server/src/db/schema/ai.ts index 3704c3dd9..0ee5af4bd 100644 --- a/packages/server/src/db/schema/ai.ts +++ b/packages/server/src/db/schema/ai.ts @@ -32,7 +32,7 @@ export const aiRelations = relations(ai, ({ one }) => ({ const createSchema = createInsertSchema(ai, { name: z.string().min(1, { message: "Name is required" }), apiUrl: z.string().url({ message: "Please enter a valid URL" }), - apiKey: z.string().min(1, { message: "API Key is required" }), + apiKey: z.string(), model: z.string().min(1, { message: "Model is required" }), isEnabled: z.boolean().optional(), }); diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index 1b03989ef..198611a77 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -79,6 +79,7 @@ export const applications = pgTable("application", { previewEnv: text("previewEnv"), watchPaths: text("watchPaths").array(), previewBuildArgs: text("previewBuildArgs"), + previewLabels: text("previewLabels").array(), previewWildcard: text("previewWildcard"), previewPort: integer("previewPort").default(3000), previewHttps: boolean("previewHttps").notNull().default(false), @@ -308,6 +309,7 @@ const createSchema = createInsertSchema(applications, { previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(), previewRequireCollaboratorPermissions: z.boolean().optional(), watchPaths: z.array(z.string()).optional(), + previewLabels: z.array(z.string()).optional(), cleanCache: z.boolean().optional(), }); diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts index 1b358946f..44d1604ae 100644 --- a/packages/server/src/services/preview-deployment.ts +++ b/packages/server/src/services/preview-deployment.ts @@ -70,7 +70,7 @@ export const findApplicationByPreview = async (applicationId: string) => { if (!application) { throw new TRPCError({ code: "NOT_FOUND", - message: "Applicationnot found", + message: "Application not found", }); } return application; @@ -78,35 +78,41 @@ export const findApplicationByPreview = async (applicationId: string) => { export const removePreviewDeployment = async (previewDeploymentId: string) => { try { - const application = await findApplicationByPreview(previewDeploymentId); const previewDeployment = await findPreviewDeploymentById(previewDeploymentId); - - const deployment = await db - .delete(previewDeployments) - .where(eq(previewDeployments.previewDeploymentId, previewDeploymentId)) - .returning(); + const application = await findApplicationById( + previewDeployment.applicationId, + ); application.appName = previewDeployment.appName; const cleanupOperations = [ + async () => + await removeService(application?.appName, application?.serverId), async () => await removeDeploymentsByPreviewDeploymentId( previewDeployment, - application.serverId, + application?.serverId, ), async () => - await removeDirectoryCode(application.appName, application.serverId), + await removeDirectoryCode(application?.appName, application?.serverId), async () => - await removeTraefikConfig(application.appName, application.serverId), + await removeTraefikConfig(application?.appName, application?.serverId), async () => - await removeService(application?.appName, application.serverId), + await db + .delete(previewDeployments) + .where( + eq(previewDeployments.previewDeploymentId, previewDeploymentId), + ) + .returning(), ]; for (const operation of cleanupOperations) { try { await operation(); - } catch {} + } catch (error) { + console.error(error); + } } - return deployment[0]; + return previewDeployment; } catch (error) { const message = error instanceof Error diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index ccdfa30f8..17c48d0ff 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -13,7 +13,7 @@ export const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80; export const TRAEFIK_HTTP3_PORT = Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443; -export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2"; +export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.5.0"; export interface TraefikOptions { env?: string[]; diff --git a/packages/server/src/utils/ai/select-ai-provider.ts b/packages/server/src/utils/ai/select-ai-provider.ts index 39d7ae13c..c0715030b 100644 --- a/packages/server/src/utils/ai/select-ai-provider.ts +++ b/packages/server/src/utils/ai/select-ai-provider.ts @@ -5,17 +5,16 @@ import { createDeepInfra } from "@ai-sdk/deepinfra"; import { createMistral } from "@ai-sdk/mistral"; import { createOpenAI } from "@ai-sdk/openai"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; -import { createOllama } from "ollama-ai-provider"; +import { createOllama } from "ai-sdk-ollama"; -function getProviderName(apiUrl: string) { +export function getProviderName(apiUrl: string) { if (apiUrl.includes("api.openai.com")) return "openai"; if (apiUrl.includes("azure.com")) return "azure"; if (apiUrl.includes("api.anthropic.com")) return "anthropic"; if (apiUrl.includes("api.cohere.ai")) return "cohere"; if (apiUrl.includes("api.perplexity.ai")) return "perplexity"; if (apiUrl.includes("api.mistral.ai")) return "mistral"; - if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama")) - return "ollama"; + if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama"; if (apiUrl.includes("api.deepinfra.com")) return "deepinfra"; return "custom"; } diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 0ce138d70..4bd38f874 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -254,6 +254,9 @@ export const addDomainToCompose = async ( if (!labels.includes("traefik.docker.network=dokploy-network")) { labels.unshift("traefik.docker.network=dokploy-network"); } + if (!labels.includes("traefik.swarm.network=dokploy-network")) { + labels.unshift("traefik.swarm.network=dokploy-network"); + } } } diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 12f46218f..ef7abf613 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -273,6 +273,14 @@ export const prepareEnvironmentVariables = ( throw new Error(`Invalid project environment variable: project.${ref}`); }); } + + resolvedValue = resolvedValue.replace(/\$\{\{(.*?)\}\}/g, (_, ref) => { + if (serviceVars[ref] !== undefined) { + return serviceVars[ref]; + } + throw new Error(`Invalid service environment variable: ${ref}`); + }); + return `${key}=${resolvedValue}`; }); diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index 5e21fc67e..5b55c240c 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -2,11 +2,14 @@ import { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import { scheduledJobs, scheduleJob } from "node-schedule"; import { createDeploymentVolumeBackup, + updateDeploymentStatus, +} from "@dokploy/server/services/deployment"; +import { execAsync, execAsyncRemote, - updateDeploymentStatus, -} from "../.."; +} from "@dokploy/server/utils/process/execAsync"; import { backupVolume } from "./backup"; +import { getS3Credentials, normalizeS3Path } from "../backups/utils"; export const scheduleVolumeBackup = async (volumeBackupId: string) => { const volumeBackup = await findVolumeBackupById(volumeBackupId); @@ -20,6 +23,33 @@ export const removeVolumeBackupJob = async (volumeBackupId: string) => { currentJob?.cancel(); }; +const cleanupOldVolumeBackups = async ( + volumeBackup: Awaited>, + serverId?: string | null, +) => { + const { keepLatestCount, destination, prefix, volumeName } = volumeBackup; + + if (!keepLatestCount) return; + + try { + const rcloneFlags = getS3Credentials(destination); + const normalizedPrefix = normalizeS3Path(prefix); + const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`; + const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`; + const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`; + const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`; + const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`; + + if (serverId) { + await execAsyncRemote(serverId, fullCommand); + } else { + await execAsync(fullCommand); + } + } catch (error) { + console.error("Volume backup retention error", error); + } +}; + export const runVolumeBackup = async (volumeBackupId: string) => { const volumeBackup = await findVolumeBackupById(volumeBackupId); const serverId = @@ -40,6 +70,10 @@ export const runVolumeBackup = async (volumeBackupId: string) => { await execAsync(commandWithLog); } + if (volumeBackup.keepLatestCount && volumeBackup.keepLatestCount > 0) { + await cleanupOldVolumeBackups(volumeBackup, serverId); + } + await updateDeploymentStatus(deployment.deploymentId, "done"); } catch (error) { await updateDeploymentStatus(deployment.deploymentId, "error"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db93bb4da..94dedb322 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,26 +101,26 @@ importers: apps/dokploy: dependencies: '@ai-sdk/anthropic': - specifier: ^1.2.12 - version: 1.2.12(zod@3.25.32) + specifier: ^2.0.5 + version: 2.0.5(zod@3.25.32) '@ai-sdk/azure': - specifier: ^1.3.23 - version: 1.3.23(zod@3.25.32) + specifier: ^2.0.16 + version: 2.0.16(zod@3.25.32) '@ai-sdk/cohere': - specifier: ^1.2.10 - version: 1.2.10(zod@3.25.32) + specifier: ^2.0.4 + version: 2.0.4(zod@3.25.32) '@ai-sdk/deepinfra': - specifier: ^0.0.4 - version: 0.0.4(zod@3.25.32) + specifier: ^1.0.10 + version: 1.0.10(zod@3.25.32) '@ai-sdk/mistral': - specifier: ^1.2.8 - version: 1.2.8(zod@3.25.32) + specifier: ^2.0.7 + version: 2.0.7(zod@3.25.32) '@ai-sdk/openai': - specifier: ^1.3.22 - version: 1.3.22(zod@3.25.32) + specifier: ^2.0.16 + version: 2.0.16(zod@3.25.32) '@ai-sdk/openai-compatible': - specifier: ^0.0.13 - version: 0.0.13(zod@3.25.32) + specifier: ^1.0.10 + version: 1.0.10(zod@3.25.32) '@codemirror/autocomplete': specifier: ^6.18.6 version: 6.18.6 @@ -263,8 +263,11 @@ importers: specifier: ^0.5.16 version: 0.5.16 ai: - specifier: ^4.3.16 - version: 4.3.16(react@18.2.0)(zod@3.25.32) + specifier: ^5.0.17 + version: 5.0.17(zod@3.25.32) + ai-sdk-ollama: + specifier: ^0.5.1 + version: 0.5.1(zod@3.25.32) bcrypt: specifier: 5.1.1 version: 5.1.1 @@ -361,9 +364,6 @@ importers: octokit: specifier: 3.1.2 version: 3.1.2 - ollama-ai-provider: - specifier: ^1.2.0 - version: 1.2.0(zod@3.25.32) otpauth: specifier: ^9.4.0 version: 9.4.0 @@ -595,26 +595,26 @@ importers: packages/server: dependencies: '@ai-sdk/anthropic': - specifier: ^1.2.12 - version: 1.2.12(zod@3.25.32) + specifier: ^2.0.5 + version: 2.0.5(zod@3.25.32) '@ai-sdk/azure': - specifier: ^1.3.23 - version: 1.3.23(zod@3.25.32) + specifier: ^2.0.16 + version: 2.0.16(zod@3.25.32) '@ai-sdk/cohere': - specifier: ^1.2.10 - version: 1.2.10(zod@3.25.32) + specifier: ^2.0.4 + version: 2.0.4(zod@3.25.32) '@ai-sdk/deepinfra': - specifier: ^0.0.4 - version: 0.0.4(zod@3.25.32) + specifier: ^1.0.10 + version: 1.0.10(zod@3.25.32) '@ai-sdk/mistral': - specifier: ^1.2.8 - version: 1.2.8(zod@3.25.32) + specifier: ^2.0.7 + version: 2.0.7(zod@3.25.32) '@ai-sdk/openai': - specifier: ^1.3.22 - version: 1.3.22(zod@3.25.32) + specifier: ^2.0.16 + version: 2.0.16(zod@3.25.32) '@ai-sdk/openai-compatible': - specifier: ^0.0.13 - version: 0.0.13(zod@3.25.32) + specifier: ^1.0.10 + version: 1.0.10(zod@3.25.32) '@better-auth/utils': specifier: 0.2.4 version: 0.2.4 @@ -643,8 +643,11 @@ importers: specifier: ^0.5.16 version: 0.5.16 ai: - specifier: ^4.3.16 - version: 4.3.16(react@18.2.0)(zod@3.25.32) + specifier: ^5.0.17 + version: 5.0.17(zod@3.25.32) + ai-sdk-ollama: + specifier: ^0.5.1 + version: 0.5.1(zod@3.25.32) bcrypt: specifier: 5.1.1 version: 5.1.1 @@ -705,9 +708,6 @@ importers: octokit: specifier: 3.1.2 version: 3.1.2 - ollama-ai-provider: - specifier: ^1.2.0 - version: 1.2.0(zod@3.25.32) otpauth: specifier: ^9.4.0 version: 9.4.0 @@ -823,87 +823,64 @@ importers: packages: - '@ai-sdk/anthropic@1.2.12': - resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} + '@ai-sdk/anthropic@2.0.5': + resolution: {integrity: sha512-f0+mD3c5D+ImCWqxFxkT3buGeBg9vFOd2aTaLd1jjIJmWO8O4INLxBC2ETif7z0BfegTIw528B6acBRIeg3jIw==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4 - '@ai-sdk/azure@1.3.23': - resolution: {integrity: sha512-vpsaPtU24RBVk/IMM5UylR/N4RtAuL2NZLWc7LJ3tvMTHu6pI46a7w+1qIwR3F6yO9ehWR8qvfLaBefJNFxaVw==} + '@ai-sdk/azure@2.0.16': + resolution: {integrity: sha512-Q8Fq7aJP9tJOCNicfEUDxU763NkX646zCePayy2Nse+5Gz6ElQEJ9MApIdC4LAyR/IsEuY8G5iY477GAF+iBjg==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4 - '@ai-sdk/cohere@1.2.10': - resolution: {integrity: sha512-OaUwd5xj4bxSO8hdCbX1a5uUlTouU8FcodSuRON6xDSsmjZIvQL4O2G1XzcidxiQVL8JQuA+M0tHZOwGxSL96A==} + '@ai-sdk/cohere@2.0.4': + resolution: {integrity: sha512-GkQsTmhDNVDv8OiwWEGBhkjWYXFzBSbRtmyaM5y4yr9h6rqNwRiwueJG89/aX4mqpGf431mnJCPYxD+eSV6vgQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4 - '@ai-sdk/deepinfra@0.0.4': - resolution: {integrity: sha512-0YZpe7bFWpSZpk2swBhYsKyd2DjxyMa0bJTajJjwec4UGUpuiDyhjXkBgEHY85JjlTubEPby8Ix+FgU0E3ofnw==} + '@ai-sdk/deepinfra@1.0.10': + resolution: {integrity: sha512-bAvg29LkPI04o9ehRyqwjkKAUlX6W8AXlBTzX/k/gMez9qBbiW6uDpRRXTNYbaEuMONrEHgOQLh2OQ5WYJLDPg==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4 - '@ai-sdk/mistral@1.2.8': - resolution: {integrity: sha512-lv857D9UJqCVxiq2Fcu7mSPTypEHBUqLl1K+lCaP6X/7QAkcaxI36QDONG+tOhGHJOXTsS114u8lrUTaEiGXbg==} + '@ai-sdk/gateway@1.0.8': + resolution: {integrity: sha512-yiHYz0bAHEvhL+fSUBI2dNmyj0LOI8zw5qrYpa4gp1ojPgZq/7T1WXoIWRmVdjQwvT4PzSmRKLtbMPfz+umgfw==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4 - '@ai-sdk/openai-compatible@0.0.13': - resolution: {integrity: sha512-fuauXYKac6PBuf8m52tWcWQW0UCScEkwTaOyr00TcPeK3dd8nPP+ZJzSYE5QhFg7rwi9EH3ahIFqSX1biXhdkQ==} + '@ai-sdk/mistral@2.0.7': + resolution: {integrity: sha512-wWBacWHZHx+WxwjSqb5iIdtK76tmBiEb35ZBLmjODFEdh/dMIaj+g/qMVFT9PY7hHxZ1sT9C58KS671l8nAISw==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4 - '@ai-sdk/openai@1.3.22': - resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==} + '@ai-sdk/openai-compatible@1.0.10': + resolution: {integrity: sha512-NInkII/DOvrMvO/mS0BxGUGi3r+wuXxbdzAh9k2gFGT+xVoP6OePikhogQu8qZuti8loUZJGYq3GJ/DCftOzhQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4 - '@ai-sdk/provider-utils@2.0.5': - resolution: {integrity: sha512-2M7vLhYN0ThGjNlzow7oO/lsL+DyMxvGMIYmVQvEYaCWhDzxH5dOp78VNjJIVwHzVLMbBDigX3rJuzAs853idw==} + '@ai-sdk/openai@2.0.16': + resolution: {integrity: sha512-Boe715q4SkSJedFfAtbP0yuo8DmF9iYElAaDH2g4YgqJqqkskIJJx4hlCYGMMk1eMesRrB2NqZvtOeyTZ/u4fA==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true + zod: ^3.25.76 || ^4 - '@ai-sdk/provider-utils@2.2.8': - resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + '@ai-sdk/provider-utils@3.0.4': + resolution: {integrity: sha512-/3Z6lfUp8r+ewFd9yzHkCmPlMOJUXup2Sx3aoUyrdXLhOmAfHRl6Z4lDbIdV0uvw/QYoBcVLJnvXN7ncYeS3uQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.23.8 + zod: ^3.25.76 || ^4 - '@ai-sdk/provider@1.0.3': - resolution: {integrity: sha512-WiuJEpHTrltOIzv3x2wx4gwksAHW0h6nK3SoDzjqCOJLu/2OJ1yASESTIX+f07ChFykHElVoP80Ol/fe9dw6tQ==} + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} - '@ai-sdk/provider@1.1.3': - resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} - engines: {node: '>=18'} - - '@ai-sdk/react@1.2.12': - resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} - engines: {node: '>=18'} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - zod: - optional: true - - '@ai-sdk/ui-utils@1.2.11': - resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.23.8 - '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -3716,6 +3693,9 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@stepperize/react@4.0.1': resolution: {integrity: sha512-LAOcfi3d2mM/Jn740Xy35qsuTwmoLIuitvWZTZRURYeGsc7a6sIKAkk3+L1joZGkLFvf5q4I6V7LxWWfB5hDvg==} peerDependencies: @@ -3958,9 +3938,6 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - '@types/diff-match-patch@1.0.36': - resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} - '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} @@ -4201,15 +4178,15 @@ packages: resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} engines: {node: '>=12'} - ai@4.3.16: - resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==} + ai-sdk-ollama@0.5.1: + resolution: {integrity: sha512-VPE2yagxtowepiPROaP/7YUpiZxqZO4SDHHM2Tdw0wyatPCggct142eQI34UO2/PPJ1iXKpraWI5+n0/pcz69Q==} + engines: {node: '>=22'} + + ai@5.0.17: + resolution: {integrity: sha512-DLZikqZZJdwSkRhFikw6Mt7pUmPZ7Ue38TjdOcw2U6iZtBbuiyWGIhHyJXlUpLcZrtBE5yqPTozyZri1lRjduw==} engines: {node: '>=18'} peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - react: - optional: true + zod: ^3.25.76 || ^4 ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -4846,9 +4823,6 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - diff-match-patch@1.0.5: - resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} - diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5135,9 +5109,9 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.0.2: - resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} - engines: {node: '>=18.0.0'} + eventsource-parser@3.0.5: + resolution: {integrity: sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==} + engines: {node: '>=20.0.0'} execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} @@ -5730,11 +5704,6 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - jsondiffpatch@0.6.0: - resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} @@ -6395,14 +6364,8 @@ packages: resolution: {integrity: sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==} engines: {node: '>= 18'} - ollama-ai-provider@1.2.0: - resolution: {integrity: sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true + ollama@0.5.17: + resolution: {integrity: sha512-q5LmPtk6GLFouS+3aURIVl+qcAOPC4+Msmx7uBb3pd+fxI55WnGjmLZ0yijI/CYy79x0QPGx3BwC3u5zv9fBvQ==} on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} @@ -6485,9 +6448,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - partial-json@0.1.7: - resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -7349,11 +7309,6 @@ packages: react: '>=16.8.0 <19' react-dom: '>=16.8.0 <19' - swr@2.3.3: - resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - symbol-observable@1.2.0: resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} engines: {node: '>=0.10.0'} @@ -7414,10 +7369,6 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - throttleit@2.1.0: - resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} - engines: {node: '>=18'} - through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -7735,6 +7686,9 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -7873,91 +7827,68 @@ packages: snapshots: - '@ai-sdk/anthropic@1.2.12(zod@3.25.32)': + '@ai-sdk/anthropic@2.0.5(zod@3.25.32)': dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) zod: 3.25.32 - '@ai-sdk/azure@1.3.23(zod@3.25.32)': + '@ai-sdk/azure@2.0.16(zod@3.25.32)': dependencies: - '@ai-sdk/openai': 1.3.22(zod@3.25.32) - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) + '@ai-sdk/openai': 2.0.16(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) zod: 3.25.32 - '@ai-sdk/cohere@1.2.10(zod@3.25.32)': + '@ai-sdk/cohere@2.0.4(zod@3.25.32)': dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) zod: 3.25.32 - '@ai-sdk/deepinfra@0.0.4(zod@3.25.32)': + '@ai-sdk/deepinfra@1.0.10(zod@3.25.32)': dependencies: - '@ai-sdk/openai-compatible': 0.0.13(zod@3.25.32) - '@ai-sdk/provider': 1.0.3 - '@ai-sdk/provider-utils': 2.0.5(zod@3.25.32) + '@ai-sdk/openai-compatible': 1.0.10(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) zod: 3.25.32 - '@ai-sdk/mistral@1.2.8(zod@3.25.32)': + '@ai-sdk/gateway@1.0.8(zod@3.25.32)': dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) zod: 3.25.32 - '@ai-sdk/openai-compatible@0.0.13(zod@3.25.32)': + '@ai-sdk/mistral@2.0.7(zod@3.25.32)': dependencies: - '@ai-sdk/provider': 1.0.3 - '@ai-sdk/provider-utils': 2.0.5(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) zod: 3.25.32 - '@ai-sdk/openai@1.3.22(zod@3.25.32)': + '@ai-sdk/openai-compatible@1.0.10(zod@3.25.32)': dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) zod: 3.25.32 - '@ai-sdk/provider-utils@2.0.5(zod@3.25.32)': + '@ai-sdk/openai@2.0.16(zod@3.25.32)': dependencies: - '@ai-sdk/provider': 1.0.3 - eventsource-parser: 3.0.2 - nanoid: 3.3.11 - secure-json-parse: 2.7.0 - optionalDependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) zod: 3.25.32 - '@ai-sdk/provider-utils@2.2.8(zod@3.25.32)': + '@ai-sdk/provider-utils@3.0.4(zod@3.25.32)': dependencies: - '@ai-sdk/provider': 1.1.3 - nanoid: 3.3.11 - secure-json-parse: 2.7.0 - zod: 3.25.32 - - '@ai-sdk/provider@1.0.3': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/provider@1.1.3': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/react@1.2.12(react@18.2.0)(zod@3.25.32)': - dependencies: - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) - '@ai-sdk/ui-utils': 1.2.11(zod@3.25.32) - react: 18.2.0 - swr: 2.3.3(react@18.2.0) - throttleit: 2.1.0 - optionalDependencies: - zod: 3.25.32 - - '@ai-sdk/ui-utils@1.2.11(zod@3.25.32)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.5 zod: 3.25.32 zod-to-json-schema: 3.24.5(zod@3.25.32) + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + '@alloc/quick-lru@5.2.0': {} '@babel/code-frame@7.27.1': @@ -10767,6 +10698,8 @@ snapshots: '@sindresorhus/is@5.6.0': {} + '@standard-schema/spec@1.0.0': {} + '@stepperize/react@4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: react: 18.2.0 @@ -11257,8 +11190,6 @@ snapshots: dependencies: '@types/ms': 2.1.0 - '@types/diff-match-patch@1.0.36': {} - '@types/docker-modem@3.0.6': dependencies: '@types/node': 20.17.51 @@ -11347,7 +11278,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 20.17.51 + '@types/node': 18.19.104 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -11534,17 +11465,22 @@ snapshots: clean-stack: 4.2.0 indent-string: 5.0.0 - ai@4.3.16(react@18.2.0)(zod@3.25.32): + ai-sdk-ollama@0.5.1(zod@3.25.32): dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) - '@ai-sdk/react': 1.2.12(react@18.2.0)(zod@3.25.32) - '@ai-sdk/ui-utils': 1.2.11(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) + ai: 5.0.17(zod@3.25.32) + ollama: 0.5.17 + transitivePeerDependencies: + - zod + + ai@5.0.17(zod@3.25.32): + dependencies: + '@ai-sdk/gateway': 1.0.8(zod@3.25.32) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32) '@opentelemetry/api': 1.9.0 - jsondiffpatch: 0.6.0 zod: 3.25.32 - optionalDependencies: - react: 18.2.0 ajv@8.17.1: dependencies: @@ -12181,8 +12117,6 @@ snapshots: didyoumean@1.2.2: {} - diff-match-patch@1.0.5: {} - diff-sequences@29.6.3: {} dijkstrajs@1.0.3: {} @@ -12469,7 +12403,7 @@ snapshots: events@3.3.0: {} - eventsource-parser@3.0.2: {} + eventsource-parser@3.0.5: {} execa@8.0.1: dependencies: @@ -13088,12 +13022,6 @@ snapshots: json-stringify-safe@5.0.1: {} - jsondiffpatch@0.6.0: - dependencies: - '@types/diff-match-patch': 1.0.36 - chalk: 5.4.1 - diff-match-patch: 1.0.5 - jsonparse@1.3.1: {} jsonwebtoken@9.0.2: @@ -13895,13 +13823,9 @@ snapshots: '@octokit/request-error': 5.1.1 '@octokit/types': 12.6.0 - ollama-ai-provider@1.2.0(zod@3.25.32): + ollama@0.5.17: dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32) - partial-json: 0.1.7 - optionalDependencies: - zod: 3.25.32 + whatwg-fetch: 3.6.20 on-exit-leak-free@2.1.2: {} @@ -13994,8 +13918,6 @@ snapshots: parseurl@1.3.3: {} - partial-json@0.1.7: {} - path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -14957,12 +14879,6 @@ snapshots: - '@types/react' - debug - swr@2.3.3(react@18.2.0): - dependencies: - dequal: 2.0.3 - react: 18.2.0 - use-sync-external-store: 1.5.0(react@18.2.0) - symbol-observable@1.2.0: {} tailwind-merge@2.6.0: {} @@ -15054,8 +14970,6 @@ snapshots: dependencies: real-require: 0.2.0 - throttleit@2.1.0: {} - through@2.3.8: {} tiny-invariant@1.3.3: {} @@ -15359,6 +15273,8 @@ snapshots: webidl-conversions@3.0.1: {} + whatwg-fetch@3.6.20: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3