mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-26 08:15:25 +02:00
Compare commits
78 Commits
v0.24.11
...
feat/concu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2da2b2dd39 | ||
|
|
7273c636a0 | ||
|
|
d6a0585bae | ||
|
|
935d1686f2 | ||
|
|
349248105a | ||
|
|
d922568510 | ||
|
|
44ae4df151 | ||
|
|
77fdda4c09 | ||
|
|
8a1e36cc3b | ||
|
|
1635bab44f | ||
|
|
4a52459015 | ||
|
|
17f333ac2a | ||
|
|
d770307d64 | ||
|
|
aa434cbdea | ||
|
|
c42054b965 | ||
|
|
03588bf375 | ||
|
|
8c420ff4f5 | ||
|
|
cbf6f95891 | ||
|
|
2d2a3d74ec | ||
|
|
56b9fb531a | ||
|
|
59aaa1a47a | ||
|
|
5e4444610c | ||
|
|
34e6cd87df | ||
|
|
31b13b8d34 | ||
|
|
746cf76cf3 | ||
|
|
46c53a05bf | ||
|
|
f97f6d8178 | ||
|
|
c653dd604f | ||
|
|
40877e4370 | ||
|
|
65203036f2 | ||
|
|
2ef5f967a9 | ||
|
|
b20c95ffbc | ||
|
|
09b2492585 | ||
|
|
ca1fa7c4f7 | ||
|
|
112b898d98 | ||
|
|
8185482bcd | ||
|
|
dd8f5dba09 | ||
|
|
e72a468c7e | ||
|
|
02dd793dfb | ||
|
|
64ef033950 | ||
|
|
32f7bdf398 | ||
|
|
8d73b77a19 | ||
|
|
2e3d4f1021 | ||
|
|
ba1f4dbd3a | ||
|
|
653beac3d9 | ||
|
|
37c34fdadc | ||
|
|
69d676178f | ||
|
|
6612c92b4f | ||
|
|
88c8fe4614 | ||
|
|
623fc26de5 | ||
|
|
220576fd63 | ||
|
|
07c23292da | ||
|
|
72fca80047 | ||
|
|
1e7f614bb6 | ||
|
|
e2662a0ec5 | ||
|
|
c96c25ca9f | ||
|
|
4afd2d11fa | ||
|
|
ff20bb2731 | ||
|
|
8a802b0739 | ||
|
|
e511173283 | ||
|
|
763b1a344a | ||
|
|
a4041185f1 | ||
|
|
522dcd6c08 | ||
|
|
b4d5935875 | ||
|
|
8cc054389a | ||
|
|
6d9a1db8af | ||
|
|
e3979d2c48 | ||
|
|
2c591cbd03 | ||
|
|
3864c50deb | ||
|
|
15e62961e8 | ||
|
|
429c1e4cd8 | ||
|
|
1904a3d1e9 | ||
|
|
025d439f71 | ||
|
|
9baafb83ff | ||
|
|
1f9ef473f1 | ||
|
|
a0bbf7be23 | ||
|
|
a5bc384d77 | ||
|
|
f2ae39aa86 |
2
.github/workflows/dokploy.yml
vendored
2
.github/workflows/dokploy.yml
vendored
@@ -2,7 +2,7 @@ name: Dokploy Docker Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, canary]
|
branches: [main, canary, "fix/re-apply-database-migration-fix"]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ if (typeof window === "undefined") {
|
|||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
railpackVersion: "0.2.2",
|
railpackVersion: "0.2.2",
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
|
previewLabels: [],
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
giteaBranch: "",
|
giteaBranch: "",
|
||||||
giteaBuildPath: "",
|
giteaBuildPath: "",
|
||||||
|
|||||||
74
apps/dokploy/__test__/env/shared.test.ts
vendored
74
apps/dokploy/__test__/env/shared.test.ts
vendored
@@ -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"',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const baseApp: ApplicationNested = {
|
|||||||
railpackVersion: "0.2.2",
|
railpackVersion: "0.2.2",
|
||||||
rollbackActive: false,
|
rollbackActive: false,
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
|
previewLabels: [],
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
giteaRepository: "",
|
giteaRepository: "",
|
||||||
giteaOwner: "",
|
giteaOwner: "",
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
// isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -33,6 +34,12 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
@@ -42,6 +49,7 @@ const schema = z
|
|||||||
wildcardDomain: z.string(),
|
wildcardDomain: z.string(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
previewLimit: z.number(),
|
previewLimit: z.number(),
|
||||||
|
previewLabels: z.array(z.string()).optional(),
|
||||||
previewHttps: z.boolean(),
|
previewHttps: z.boolean(),
|
||||||
previewPath: z.string(),
|
previewPath: z.string(),
|
||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
@@ -81,6 +89,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
wildcardDomain: "*.traefik.me",
|
wildcardDomain: "*.traefik.me",
|
||||||
port: 3000,
|
port: 3000,
|
||||||
previewLimit: 3,
|
previewLimit: 3,
|
||||||
|
previewLabels: [],
|
||||||
previewHttps: false,
|
previewHttps: false,
|
||||||
previewPath: "/",
|
previewPath: "/",
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
@@ -102,6 +111,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
buildArgs: data.previewBuildArgs || "",
|
buildArgs: data.previewBuildArgs || "",
|
||||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||||
port: data.previewPort || 3000,
|
port: data.previewPort || 3000,
|
||||||
|
previewLabels: data.previewLabels || [],
|
||||||
previewLimit: data.previewLimit || 3,
|
previewLimit: data.previewLimit || 3,
|
||||||
previewHttps: data.previewHttps || false,
|
previewHttps: data.previewHttps || false,
|
||||||
previewPath: data.previewPath || "/",
|
previewPath: data.previewPath || "/",
|
||||||
@@ -119,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
previewBuildArgs: formData.buildArgs,
|
previewBuildArgs: formData.buildArgs,
|
||||||
previewWildcard: formData.wildcardDomain,
|
previewWildcard: formData.wildcardDomain,
|
||||||
previewPort: formData.port,
|
previewPort: formData.port,
|
||||||
|
previewLabels: formData.previewLabels,
|
||||||
applicationId,
|
applicationId,
|
||||||
previewLimit: formData.previewLimit,
|
previewLimit: formData.previewLimit,
|
||||||
previewHttps: formData.previewHttps,
|
previewHttps: formData.previewHttps,
|
||||||
@@ -200,6 +211,90 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewLabels"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Preview Labels</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((label, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<X
|
||||||
|
className="size-3 cursor-pointer hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
const newLabels = [...(field.value || [])];
|
||||||
|
newLabels.splice(index, 1);
|
||||||
|
field.onChange(newLabels);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a label (e.g. enhancements, needs-review)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const label = input.value.trim();
|
||||||
|
if (label) {
|
||||||
|
field.onChange([
|
||||||
|
...(field.value || []),
|
||||||
|
label,
|
||||||
|
]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a label"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const label = input.value.trim();
|
||||||
|
if (label) {
|
||||||
|
field.onChange([...(field.value || []), label]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="previewLimit"
|
name="previewLimit"
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ const formSchema = z
|
|||||||
cronExpression: z.string().min(1, "Cron expression is required"),
|
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||||
volumeName: z.string().min(1, "Volume name is required"),
|
volumeName: z.string().min(1, "Volume name is required"),
|
||||||
prefix: z.string(),
|
prefix: z.string(),
|
||||||
// keepLatestCount: z.coerce.number().optional(),
|
keepLatestCount: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.gte(1, "Must be at least 1")
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
turnOff: z.boolean().default(false),
|
turnOff: z.boolean().default(false),
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
serviceType: z.enum([
|
serviceType: z.enum([
|
||||||
@@ -108,6 +113,7 @@ export const HandleVolumeBackups = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
@@ -117,7 +123,7 @@ export const HandleVolumeBackups = ({
|
|||||||
cronExpression: "",
|
cronExpression: "",
|
||||||
volumeName: "",
|
volumeName: "",
|
||||||
prefix: "",
|
prefix: "",
|
||||||
// keepLatestCount: undefined,
|
keepLatestCount: undefined,
|
||||||
turnOff: false,
|
turnOff: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
serviceName: "",
|
serviceName: "",
|
||||||
@@ -173,13 +179,19 @@ export const HandleVolumeBackups = ({
|
|||||||
cronExpression: volumeBackup.cronExpression,
|
cronExpression: volumeBackup.cronExpression,
|
||||||
volumeName: volumeBackup.volumeName || "",
|
volumeName: volumeBackup.volumeName || "",
|
||||||
prefix: volumeBackup.prefix,
|
prefix: volumeBackup.prefix,
|
||||||
// keepLatestCount: volumeBackup.keepLatestCount || undefined,
|
keepLatestCount: volumeBackup.keepLatestCount || undefined,
|
||||||
turnOff: volumeBackup.turnOff,
|
turnOff: volumeBackup.turnOff,
|
||||||
enabled: volumeBackup.enabled || false,
|
enabled: volumeBackup.enabled || false,
|
||||||
serviceName: volumeBackup.serviceName || "",
|
serviceName: volumeBackup.serviceName || "",
|
||||||
destinationId: volumeBackup.destinationId,
|
destinationId: volumeBackup.destinationId,
|
||||||
serviceType: volumeBackup.serviceType,
|
serviceType: volumeBackup.serviceType,
|
||||||
});
|
});
|
||||||
|
setKeepLatestCountInput(
|
||||||
|
volumeBackup.keepLatestCount !== null &&
|
||||||
|
volumeBackup.keepLatestCount !== undefined
|
||||||
|
? String(volumeBackup.keepLatestCount)
|
||||||
|
: "",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [form, volumeBackup, volumeBackupId]);
|
}, [form, volumeBackup, volumeBackupId]);
|
||||||
|
|
||||||
@@ -190,8 +202,12 @@ export const HandleVolumeBackups = ({
|
|||||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
if (!id && !volumeBackupId) return;
|
if (!id && !volumeBackupId) return;
|
||||||
|
|
||||||
|
const preparedKeepLatestCount =
|
||||||
|
keepLatestCountInput === "" ? null : (values.keepLatestCount ?? null);
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
...values,
|
...values,
|
||||||
|
keepLatestCount: preparedKeepLatestCount,
|
||||||
destinationId: values.destinationId,
|
destinationId: values.destinationId,
|
||||||
volumeBackupId: volumeBackupId || "",
|
volumeBackupId: volumeBackupId || "",
|
||||||
serviceType: volumeBackupType,
|
serviceType: volumeBackupType,
|
||||||
@@ -257,9 +273,8 @@ export const HandleVolumeBackups = ({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-y-auto",
|
|
||||||
volumeBackupType === "compose" || volumeBackupType === "application"
|
volumeBackupType === "compose" || volumeBackupType === "application"
|
||||||
? "max-h-[95vh] sm:max-w-2xl"
|
? "sm:max-w-2xl"
|
||||||
: " sm:max-w-lg",
|
: " sm:max-w-lg",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -600,29 +615,38 @@ export const HandleVolumeBackups = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="keepLatestCount"
|
name="keepLatestCount"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Keep Latest Count</FormLabel>
|
<FormLabel>Keep Latest Backups</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
|
||||||
placeholder="5"
|
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) =>
|
type="number"
|
||||||
field.onChange(Number(e.target.value) || undefined)
|
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));
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Number of backup files to keep (optional)
|
How many recent backups to keep. Empty means no cleanup.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/> */}
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
@@ -114,6 +115,12 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
(data &&
|
||||||
|
"applicationStatus" in data &&
|
||||||
|
data?.applicationStatus === "running") ||
|
||||||
|
(data && "composeStatus" in data && data?.composeStatus === "running");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -202,6 +209,12 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
{isDisabled && (
|
||||||
|
<AlertBlock type="warning" className="w-full mt-5">
|
||||||
|
Cannot delete the service while it is running. Please wait for the
|
||||||
|
build to finish and then try again.
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -211,8 +224,10 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
disabled={isDisabled}
|
||||||
form="hook-form-delete-compose"
|
form="hook-form-delete-compose"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const organizationSchema = z.object({
|
const organizationSchema = z.object({
|
||||||
@@ -54,6 +55,8 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
const { mutateAsync, isLoading } = organizationId
|
const { mutateAsync, isLoading } = organizationId
|
||||||
? api.organization.update.useMutation()
|
? api.organization.update.useMutation()
|
||||||
: api.organization.create.useMutation();
|
: api.organization.create.useMutation();
|
||||||
|
const { refetch: refetchActiveOrganization } =
|
||||||
|
authClient.useActiveOrganization();
|
||||||
|
|
||||||
const form = useForm<OrganizationFormValues>({
|
const form = useForm<OrganizationFormValues>({
|
||||||
resolver: zodResolver(organizationSchema),
|
resolver: zodResolver(organizationSchema),
|
||||||
@@ -84,6 +87,10 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
`Organization ${organizationId ? "updated" : "created"} successfully`,
|
`Organization ${organizationId ? "updated" : "created"} successfully`,
|
||||||
);
|
);
|
||||||
utils.organization.all.invalidate();
|
utils.organization.all.invalidate();
|
||||||
|
if (organizationId) {
|
||||||
|
utils.organization.one.invalidate({ organizationId });
|
||||||
|
refetchActiveOrganization();
|
||||||
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { api } from "@/utils/api";
|
|||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
name: z.string().min(1, { message: "Name is required" }),
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
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" }),
|
model: z.string().min(1, { message: "Model is required" }),
|
||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
});
|
});
|
||||||
@@ -71,7 +71,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
name: "",
|
name: "",
|
||||||
apiUrl: "",
|
apiUrl: "",
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
model: "gpt-3.5-turbo",
|
model: "",
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -81,7 +81,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
name: data?.name ?? "",
|
name: data?.name ?? "",
|
||||||
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
|
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
|
||||||
apiKey: data?.apiKey ?? "",
|
apiKey: data?.apiKey ?? "",
|
||||||
model: data?.model ?? "gpt-3.5-turbo",
|
model: data?.model ?? "",
|
||||||
isEnabled: data?.isEnabled ?? true,
|
isEnabled: data?.isEnabled ?? true,
|
||||||
});
|
});
|
||||||
}, [aiId, form, data]);
|
}, [aiId, form, data]);
|
||||||
@@ -89,6 +89,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
const apiUrl = form.watch("apiUrl");
|
const apiUrl = form.watch("apiUrl");
|
||||||
const apiKey = form.watch("apiKey");
|
const apiKey = form.watch("apiKey");
|
||||||
|
|
||||||
|
const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
|
||||||
const { data: models, isLoading: isLoadingServerModels } =
|
const { data: models, isLoading: isLoadingServerModels } =
|
||||||
api.ai.getModels.useQuery(
|
api.ai.getModels.useQuery(
|
||||||
{
|
{
|
||||||
@@ -96,7 +97,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
apiKey: apiKey ?? "",
|
apiKey: apiKey ?? "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!apiUrl && !!apiKey,
|
enabled: !!apiUrl && (isOllama || !!apiKey),
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setError(`Failed to fetch models: ${error.message}`);
|
setError(`Failed to fetch models: ${error.message}`);
|
||||||
},
|
},
|
||||||
@@ -191,22 +192,29 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{!isOllama && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="apiKey"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="apiKey"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>API Key</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>API Key</FormLabel>
|
||||||
<Input type="password" placeholder="sk-..." {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<Input
|
||||||
<FormDescription>
|
type="password"
|
||||||
Your API key for authentication
|
placeholder="sk-..."
|
||||||
</FormDescription>
|
autoComplete="one-time-code"
|
||||||
<FormMessage />
|
{...field}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormDescription>
|
||||||
|
Your API key for authentication
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoadingServerModels && (
|
{isLoadingServerModels && (
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -214,6 +222,12 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isLoadingServerModels && !models?.length && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
No models available
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isLoadingServerModels && models && models.length > 0 && (
|
{!isLoadingServerModels && models && models.length > 0 && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -11,11 +11,20 @@ interface Props {
|
|||||||
export const DashboardLayout = ({ children }: Props) => {
|
export const DashboardLayout = ({ children }: Props) => {
|
||||||
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
const { data: isUserSubscribed } = api.settings.isUserSubscribed.useQuery(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
enabled: isCloud === true,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Page>{children}</Page>
|
<Page>{children}</Page>
|
||||||
{isCloud === true && (
|
{isCloud === true && isUserSubscribed === true && (
|
||||||
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
|
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -129,10 +129,11 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed left-[50%] top-[50%] z-50 pointer-events-auto w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
"flex flex-col max-h-[90vh]",
|
"flex flex-col max-h-[90vh]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
style={{ pointerEvents: "auto" }}
|
||||||
onInteractOutside={(event) => event.preventDefault()}
|
onInteractOutside={(event) => event.preventDefault()}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
"group/sidebar-wrapper flex h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -329,10 +329,11 @@ const SidebarInset = React.forwardRef<
|
|||||||
<main
|
<main
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex min-h-svh overflow-auto w-full flex-col bg-background",
|
"relative flex min-h-svh overflow-auto w-full flex-col bg-background",
|
||||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
style={{ scrollbarGutter: "stable" }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0106_purple_maggott.sql
Normal file
1
apps/dokploy/drizzle/0106_purple_maggott.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "previewLabels" text[];
|
||||||
2
apps/dokploy/drizzle/0107_calm_power_pack.sql
Normal file
2
apps/dokploy/drizzle/0107_calm_power_pack.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "serverConcurrency" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "server" ADD COLUMN "concurrency" integer DEFAULT 1 NOT NULL;
|
||||||
6424
apps/dokploy/drizzle/meta/0106_snapshot.json
Normal file
6424
apps/dokploy/drizzle/meta/0106_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6438
apps/dokploy/drizzle/meta/0107_snapshot.json
Normal file
6438
apps/dokploy/drizzle/meta/0107_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -743,6 +743,20 @@
|
|||||||
"when": 1754259281559,
|
"when": 1754259281559,
|
||||||
"tag": "0105_clumsy_quicksilver",
|
"tag": "0105_clumsy_quicksilver",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 106,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754912062243,
|
||||||
|
"tag": "0106_purple_maggott",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 107,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1756436825081,
|
||||||
|
"tag": "0107_calm_power_pack",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,10 @@ function prepareDefine(config: DotenvParseOutput | undefined) {
|
|||||||
const define = {};
|
const define = {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
for (const [key, value] of Object.entries(config)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
// Skip DATABASE_URL to allow runtime environment variable override
|
||||||
|
if (key === "DATABASE_URL") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
define[`process.env.${key}`] = JSON.stringify(value);
|
define[`process.env.${key}`] = JSON.stringify(value);
|
||||||
}
|
}
|
||||||
@@ -14,6 +18,7 @@ function prepareDefine(config: DotenvParseOutput | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const define = prepareDefine(result.parsed);
|
const define = prepareDefine(result.parsed);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
esbuild
|
esbuild
|
||||||
.build({
|
.build({
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
// import { drizzle } from "drizzle-orm/postgres-js";
|
|
||||||
// import { nanoid } from "nanoid";
|
|
||||||
// import postgres from "postgres";
|
|
||||||
// import * as schema from "./server/db/schema";
|
|
||||||
|
|
||||||
// const connectionString = process.env.DATABASE_URL!;
|
|
||||||
|
|
||||||
// const sql = postgres(connectionString, { max: 1 });
|
|
||||||
// const db = drizzle(sql, {
|
|
||||||
// schema,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// await db
|
|
||||||
// .transaction(async (db) => {
|
|
||||||
// const admins = await db.query.admins.findMany({
|
|
||||||
// with: {
|
|
||||||
// auth: true,
|
|
||||||
// users: {
|
|
||||||
// with: {
|
|
||||||
// auth: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// for (const admin of admins) {
|
|
||||||
// const user = await db
|
|
||||||
// .insert(schema.users_temp)
|
|
||||||
// .values({
|
|
||||||
// id: admin.adminId,
|
|
||||||
// email: admin.auth.email,
|
|
||||||
// token: admin.auth.token || "",
|
|
||||||
// emailVerified: true,
|
|
||||||
// updatedAt: new Date(),
|
|
||||||
// role: "admin",
|
|
||||||
// serverIp: admin.serverIp,
|
|
||||||
// image: admin.auth.image,
|
|
||||||
// certificateType: admin.certificateType,
|
|
||||||
// host: admin.host,
|
|
||||||
// letsEncryptEmail: admin.letsEncryptEmail,
|
|
||||||
// sshPrivateKey: admin.sshPrivateKey,
|
|
||||||
// enableDockerCleanup: admin.enableDockerCleanup,
|
|
||||||
// enableLogRotation: admin.enableLogRotation,
|
|
||||||
// enablePaidFeatures: admin.enablePaidFeatures,
|
|
||||||
// metricsConfig: admin.metricsConfig,
|
|
||||||
// cleanupCacheApplications: admin.cleanupCacheApplications,
|
|
||||||
// cleanupCacheOnPreviews: admin.cleanupCacheOnPreviews,
|
|
||||||
// cleanupCacheOnCompose: admin.cleanupCacheOnCompose,
|
|
||||||
// stripeCustomerId: admin.stripeCustomerId,
|
|
||||||
// stripeSubscriptionId: admin.stripeSubscriptionId,
|
|
||||||
// serversQuantity: admin.serversQuantity,
|
|
||||||
// })
|
|
||||||
// .returning()
|
|
||||||
// .then((user) => user[0]);
|
|
||||||
|
|
||||||
// await db.insert(schema.account).values({
|
|
||||||
// providerId: "credential",
|
|
||||||
// userId: user?.id || "",
|
|
||||||
// password: admin.auth.password,
|
|
||||||
// is2FAEnabled: admin.auth.is2FAEnabled || false,
|
|
||||||
// createdAt: new Date(admin.auth.createdAt) || new Date(),
|
|
||||||
// updatedAt: new Date(admin.auth.createdAt) || new Date(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const organization = await db
|
|
||||||
// .insert(schema.organization)
|
|
||||||
// .values({
|
|
||||||
// name: "My Organization",
|
|
||||||
// slug: nanoid(),
|
|
||||||
// ownerId: user?.id || "",
|
|
||||||
// createdAt: new Date(admin.createdAt) || new Date(),
|
|
||||||
// })
|
|
||||||
// .returning()
|
|
||||||
// .then((organization) => organization[0]);
|
|
||||||
|
|
||||||
// for (const member of admin.users) {
|
|
||||||
// const userTemp = await db
|
|
||||||
// .insert(schema.users_temp)
|
|
||||||
// .values({
|
|
||||||
// id: member.userId,
|
|
||||||
// email: member.auth.email,
|
|
||||||
// token: member.token || "",
|
|
||||||
// emailVerified: true,
|
|
||||||
// updatedAt: new Date(admin.createdAt) || new Date(),
|
|
||||||
// role: "user",
|
|
||||||
// image: member.auth.image,
|
|
||||||
// createdAt: admin.createdAt,
|
|
||||||
// canAccessToAPI: member.canAccessToAPI || false,
|
|
||||||
// canAccessToDocker: member.canAccessToDocker || false,
|
|
||||||
// canAccessToGitProviders: member.canAccessToGitProviders || false,
|
|
||||||
// canAccessToSSHKeys: member.canAccessToSSHKeys || false,
|
|
||||||
// canAccessToTraefikFiles: member.canAccessToTraefikFiles || false,
|
|
||||||
// canCreateProjects: member.canCreateProjects || false,
|
|
||||||
// canCreateServices: member.canCreateServices || false,
|
|
||||||
// canDeleteProjects: member.canDeleteProjects || false,
|
|
||||||
// canDeleteServices: member.canDeleteServices || false,
|
|
||||||
// accessedProjects: member.accessedProjects || [],
|
|
||||||
// accessedServices: member.accessedServices || [],
|
|
||||||
// })
|
|
||||||
// .returning()
|
|
||||||
// .then((userTemp) => userTemp[0]);
|
|
||||||
|
|
||||||
// await db.insert(schema.account).values({
|
|
||||||
// providerId: "credential",
|
|
||||||
// userId: member?.userId || "",
|
|
||||||
// password: member.auth.password,
|
|
||||||
// is2FAEnabled: member.auth.is2FAEnabled || false,
|
|
||||||
// createdAt: new Date(member.auth.createdAt) || new Date(),
|
|
||||||
// updatedAt: new Date(member.auth.createdAt) || new Date(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// await db.insert(schema.member).values({
|
|
||||||
// organizationId: organization?.id || "",
|
|
||||||
// userId: userTemp?.id || "",
|
|
||||||
// role: "admin",
|
|
||||||
// createdAt: new Date(member.createdAt) || new Date(),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then(() => {
|
|
||||||
// console.log("Migration finished");
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// console.error(error);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// await db
|
|
||||||
// .transaction(async (db) => {
|
|
||||||
// const projects = await db.query.projects.findMany({
|
|
||||||
// with: {
|
|
||||||
// user: {
|
|
||||||
// with: {
|
|
||||||
// organizations: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// for (const project of projects) {
|
|
||||||
// const _user = await db.update(schema.projects).set({
|
|
||||||
// organizationId: project.user.organizations[0]?.id || "",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then(() => {
|
|
||||||
// console.log("Migration finished");
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// console.error(error);
|
|
||||||
// });
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.24.11",
|
"version": "v0.25.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -37,13 +37,13 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^1.2.12",
|
"@ai-sdk/anthropic": "^2.0.5",
|
||||||
"@ai-sdk/azure": "^1.3.23",
|
"@ai-sdk/azure": "^2.0.16",
|
||||||
"@ai-sdk/cohere": "^1.2.10",
|
"@ai-sdk/cohere": "^2.0.4",
|
||||||
"@ai-sdk/deepinfra": "^0.0.4",
|
"@ai-sdk/deepinfra": "^1.0.10",
|
||||||
"@ai-sdk/mistral": "^1.2.8",
|
"@ai-sdk/mistral": "^2.0.7",
|
||||||
"@ai-sdk/openai": "^1.3.22",
|
"@ai-sdk/openai": "^2.0.16",
|
||||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
"@ai-sdk/openai-compatible": "^1.0.10",
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
@@ -91,12 +91,12 @@
|
|||||||
"@xterm/addon-clipboard": "0.1.0",
|
"@xterm/addon-clipboard": "0.1.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"ai": "^4.3.16",
|
"ai": "^5.0.17",
|
||||||
|
"ai-sdk-ollama": "^0.5.1",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "v1.2.8-beta.7",
|
"better-auth": "v1.2.8-beta.7",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^0.2.1",
|
"cmdk": "^0.2.1",
|
||||||
@@ -124,8 +124,8 @@
|
|||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
"nodemailer": "6.9.14",
|
"nodemailer": "6.9.14",
|
||||||
"octokit": "3.1.2",
|
"octokit": "3.1.2",
|
||||||
"ollama-ai-provider": "^1.2.0",
|
|
||||||
"otpauth": "^9.4.0",
|
"otpauth": "^9.4.0",
|
||||||
|
"p-limit": "^7.1.1",
|
||||||
"pino": "9.4.0",
|
"pino": "9.4.0",
|
||||||
"pino-pretty": "11.2.2",
|
"pino-pretty": "11.2.2",
|
||||||
"postgres": "3.4.4",
|
"postgres": "3.4.4",
|
||||||
|
|||||||
@@ -343,7 +343,9 @@ export default async function handler(
|
|||||||
if (
|
if (
|
||||||
action === "opened" ||
|
action === "opened" ||
|
||||||
action === "synchronize" ||
|
action === "synchronize" ||
|
||||||
action === "reopened"
|
action === "reopened" ||
|
||||||
|
action === "labeled" ||
|
||||||
|
action === "unlabeled"
|
||||||
) {
|
) {
|
||||||
const repository = githubBody?.repository?.name;
|
const repository = githubBody?.repository?.name;
|
||||||
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
||||||
@@ -442,6 +444,19 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const app of secureApps) {
|
for (const app of secureApps) {
|
||||||
|
// check for labels
|
||||||
|
if (app?.previewLabels && app?.previewLabels?.length > 0) {
|
||||||
|
let hasLabel = false;
|
||||||
|
const labels = githubBody?.pull_request?.labels;
|
||||||
|
for (const label of labels) {
|
||||||
|
if (app?.previewLabels?.includes(label.name)) {
|
||||||
|
hasLabel = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasLabel) continue;
|
||||||
|
}
|
||||||
|
|
||||||
const previewLimit = app?.previewLimit || 0;
|
const previewLimit = app?.previewLimit || 0;
|
||||||
if (app?.previewDeployments?.length > previewLimit) {
|
if (app?.previewDeployments?.length > previewLimit) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
FolderInput,
|
FolderInput,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Play,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
@@ -40,10 +41,12 @@ import {
|
|||||||
RedisIcon,
|
RedisIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -288,6 +291,8 @@ const Project = (
|
|||||||
const [openCombobox, setOpenCombobox] = useState(false);
|
const [openCombobox, setOpenCombobox] = useState(false);
|
||||||
const [selectedServices, setSelectedServices] = useState<string[]>([]);
|
const [selectedServices, setSelectedServices] = useState<string[]>([]);
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||||
|
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
if (selectedServices.length === filteredServices.length) {
|
if (selectedServices.length === filteredServices.length) {
|
||||||
@@ -311,6 +316,7 @@ const Project = (
|
|||||||
stop: api.compose.stop.useMutation(),
|
stop: api.compose.stop.useMutation(),
|
||||||
move: api.compose.move.useMutation(),
|
move: api.compose.move.useMutation(),
|
||||||
delete: api.compose.delete.useMutation(),
|
delete: api.compose.delete.useMutation(),
|
||||||
|
deploy: api.compose.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const applicationActions = {
|
const applicationActions = {
|
||||||
@@ -318,6 +324,7 @@ const Project = (
|
|||||||
stop: api.application.stop.useMutation(),
|
stop: api.application.stop.useMutation(),
|
||||||
move: api.application.move.useMutation(),
|
move: api.application.move.useMutation(),
|
||||||
delete: api.application.delete.useMutation(),
|
delete: api.application.delete.useMutation(),
|
||||||
|
deploy: api.application.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const postgresActions = {
|
const postgresActions = {
|
||||||
@@ -325,6 +332,7 @@ const Project = (
|
|||||||
stop: api.postgres.stop.useMutation(),
|
stop: api.postgres.stop.useMutation(),
|
||||||
move: api.postgres.move.useMutation(),
|
move: api.postgres.move.useMutation(),
|
||||||
delete: api.postgres.remove.useMutation(),
|
delete: api.postgres.remove.useMutation(),
|
||||||
|
deploy: api.postgres.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mysqlActions = {
|
const mysqlActions = {
|
||||||
@@ -332,6 +340,7 @@ const Project = (
|
|||||||
stop: api.mysql.stop.useMutation(),
|
stop: api.mysql.stop.useMutation(),
|
||||||
move: api.mysql.move.useMutation(),
|
move: api.mysql.move.useMutation(),
|
||||||
delete: api.mysql.remove.useMutation(),
|
delete: api.mysql.remove.useMutation(),
|
||||||
|
deploy: api.mysql.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mariadbActions = {
|
const mariadbActions = {
|
||||||
@@ -339,6 +348,7 @@ const Project = (
|
|||||||
stop: api.mariadb.stop.useMutation(),
|
stop: api.mariadb.stop.useMutation(),
|
||||||
move: api.mariadb.move.useMutation(),
|
move: api.mariadb.move.useMutation(),
|
||||||
delete: api.mariadb.remove.useMutation(),
|
delete: api.mariadb.remove.useMutation(),
|
||||||
|
deploy: api.mariadb.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const redisActions = {
|
const redisActions = {
|
||||||
@@ -346,6 +356,7 @@ const Project = (
|
|||||||
stop: api.redis.stop.useMutation(),
|
stop: api.redis.stop.useMutation(),
|
||||||
move: api.redis.move.useMutation(),
|
move: api.redis.move.useMutation(),
|
||||||
delete: api.redis.remove.useMutation(),
|
delete: api.redis.remove.useMutation(),
|
||||||
|
deploy: api.redis.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mongoActions = {
|
const mongoActions = {
|
||||||
@@ -353,6 +364,7 @@ const Project = (
|
|||||||
stop: api.mongo.stop.useMutation(),
|
stop: api.mongo.stop.useMutation(),
|
||||||
move: api.mongo.move.useMutation(),
|
move: api.mongo.move.useMutation(),
|
||||||
delete: api.mongo.remove.useMutation(),
|
delete: api.mongo.remove.useMutation(),
|
||||||
|
deploy: api.mongo.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkStart = async () => {
|
const handleBulkStart = async () => {
|
||||||
@@ -523,7 +535,7 @@ const Project = (
|
|||||||
setIsBulkActionLoading(false);
|
setIsBulkActionLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkDelete = async () => {
|
const handleBulkDelete = async (deleteVolumes = false) => {
|
||||||
let success = 0;
|
let success = 0;
|
||||||
setIsBulkActionLoading(true);
|
setIsBulkActionLoading(true);
|
||||||
for (const serviceId of selectedServices) {
|
for (const serviceId of selectedServices) {
|
||||||
@@ -540,7 +552,7 @@ const Project = (
|
|||||||
case "compose":
|
case "compose":
|
||||||
await composeActions.delete.mutateAsync({
|
await composeActions.delete.mutateAsync({
|
||||||
composeId: serviceId,
|
composeId: serviceId,
|
||||||
deleteVolumes: false,
|
deleteVolumes,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "postgres":
|
case "postgres":
|
||||||
@@ -585,6 +597,83 @@ const Project = (
|
|||||||
setIsBulkActionLoading(false);
|
setIsBulkActionLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBulkDeploy = async () => {
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
setIsBulkActionLoading(true);
|
||||||
|
|
||||||
|
for (const serviceId of selectedServices) {
|
||||||
|
try {
|
||||||
|
const service = filteredServices.find((s) => s.id === serviceId);
|
||||||
|
if (!service) continue;
|
||||||
|
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
await applicationActions.deploy.mutateAsync({
|
||||||
|
applicationId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compose":
|
||||||
|
await composeActions.deploy.mutateAsync({
|
||||||
|
composeId: serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "postgres":
|
||||||
|
await postgresActions.deploy.mutateAsync({
|
||||||
|
postgresId: serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
await mysqlActions.deploy.mutateAsync({
|
||||||
|
mysqlId: serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
await mariadbActions.deploy.mutateAsync({
|
||||||
|
mariadbId: serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "redis":
|
||||||
|
await redisActions.deploy.mutateAsync({
|
||||||
|
redisId: serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "mongo":
|
||||||
|
await mongoActions.deploy.mutateAsync({
|
||||||
|
mongoId: serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
success++;
|
||||||
|
} catch (error) {
|
||||||
|
failed++;
|
||||||
|
toast.error(
|
||||||
|
`Error deploying service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success > 0) {
|
||||||
|
toast.success(
|
||||||
|
`${success} service${success !== 1 ? "s" : ""} deployed successfully`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (failed > 0) {
|
||||||
|
toast.error(
|
||||||
|
`${failed} service${failed !== 1 ? "s" : ""} failed to deploy`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedServices([]);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
setIsBulkActionLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const filteredServices = useMemo(() => {
|
const filteredServices = useMemo(() => {
|
||||||
if (!applications) return [];
|
if (!applications) return [];
|
||||||
const filtered = applications.filter(
|
const filtered = applications.filter(
|
||||||
@@ -598,6 +687,13 @@ const Project = (
|
|||||||
return sortServices(filtered);
|
return sortServices(filtered);
|
||||||
}, [applications, searchQuery, selectedTypes, sortBy]);
|
}, [applications, searchQuery, selectedTypes, sortBy]);
|
||||||
|
|
||||||
|
const selectedServicesWithRunningStatus = useMemo(() => {
|
||||||
|
return filteredServices.filter(
|
||||||
|
(service) =>
|
||||||
|
selectedServices.includes(service.id) && service.status === "running",
|
||||||
|
);
|
||||||
|
}, [filteredServices, selectedServices]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<BreadcrumbSidebar
|
<BreadcrumbSidebar
|
||||||
@@ -721,6 +817,24 @@ const Project = (
|
|||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Deploy Services"
|
||||||
|
description={`Are you sure you want to deploy ${selectedServices.length} service${selectedServices.length !== 1 ? "s" : ""}? This will redeploy/restart the selected services.`}
|
||||||
|
onClick={handleBulkDeploy}
|
||||||
|
type="default"
|
||||||
|
disabled={
|
||||||
|
selectedServices.length === 0 ||
|
||||||
|
isBulkActionLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start"
|
||||||
|
>
|
||||||
|
<Play className="mr-2 h-4 w-4" />
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Services"
|
title="Stop Services"
|
||||||
description={`Are you sure you want to stop ${selectedServices.length} services?`}
|
description={`Are you sure you want to stop ${selectedServices.length} services?`}
|
||||||
@@ -740,9 +854,35 @@ const Project = (
|
|||||||
<>
|
<>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Services"
|
title="Delete Services"
|
||||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
description={
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete{" "}
|
||||||
|
{selectedServices.length} services? This
|
||||||
|
action cannot be undone.
|
||||||
|
</p>
|
||||||
|
{selectedServicesWithRunningStatus.length >
|
||||||
|
0 && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Warning:{" "}
|
||||||
|
{
|
||||||
|
selectedServicesWithRunningStatus.length
|
||||||
|
}{" "}
|
||||||
|
of the selected services are currently
|
||||||
|
running. Please stop these services
|
||||||
|
first before deleting:{" "}
|
||||||
|
{selectedServicesWithRunningStatus
|
||||||
|
.map((s) => s.name)
|
||||||
|
.join(", ")}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
type="destructive"
|
type="destructive"
|
||||||
onClick={handleBulkDelete}
|
disabled={
|
||||||
|
selectedServicesWithRunningStatus.length > 0
|
||||||
|
}
|
||||||
|
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -838,6 +978,113 @@ const Project = (
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Bulk Delete Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={isBulkDeleteDialogOpen}
|
||||||
|
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Services</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete{" "}
|
||||||
|
{selectedServices.length} service
|
||||||
|
{selectedServices.length !== 1 ? "s" : ""}?
|
||||||
|
This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Show services to be deleted */}
|
||||||
|
<div className="max-h-40 overflow-y-auto space-y-2">
|
||||||
|
{selectedServices.map((serviceId) => {
|
||||||
|
const service = filteredServices.find(
|
||||||
|
(s) => s.id === serviceId,
|
||||||
|
);
|
||||||
|
return service ? (
|
||||||
|
<div
|
||||||
|
key={serviceId}
|
||||||
|
className="flex items-center space-x-2 text-sm"
|
||||||
|
>
|
||||||
|
<span className="px-2 py-1 text-xs bg-secondary rounded">
|
||||||
|
{service.type}
|
||||||
|
</span>
|
||||||
|
<span>{service.name}</span>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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 (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="deleteVolumes"
|
||||||
|
checked={deleteVolumes}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setDeleteVolumes(checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="deleteVolumes"
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
Delete volumes associated with
|
||||||
|
services
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Volume deletion is available for:{" "}
|
||||||
|
{servicesWithVolumeSupport.length}{" "}
|
||||||
|
compose service
|
||||||
|
{servicesWithVolumeSupport.length !== 1
|
||||||
|
? "s"
|
||||||
|
: ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsBulkDeleteDialogOpen(false);
|
||||||
|
setDeleteVolumes(false); // Reset checkbox
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
handleBulkDelete(deleteVolumes);
|
||||||
|
setIsBulkDeleteDialogOpen(false);
|
||||||
|
setDeleteVolumes(false); // Reset checkbox
|
||||||
|
}}
|
||||||
|
disabled={isBulkActionLoading}
|
||||||
|
>
|
||||||
|
Delete Services
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
maxLength={6}
|
maxLength={6}
|
||||||
pattern={REGEXP_ONLY_DIGITS}
|
pattern={REGEXP_ONLY_DIGITS}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
autoFocus
|
||||||
>
|
>
|
||||||
<InputOTPGroup>
|
<InputOTPGroup>
|
||||||
<InputOTPSlot index={0} className="border-border" />
|
<InputOTPSlot index={0} className="border-border" />
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "@dokploy/server/services/user";
|
} from "@dokploy/server/services/user";
|
||||||
import {
|
import {
|
||||||
getProviderHeaders,
|
getProviderHeaders,
|
||||||
|
getProviderName,
|
||||||
type Model,
|
type Model,
|
||||||
} from "@dokploy/server/utils/ai/select-ai-provider";
|
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -47,11 +48,24 @@ export const aiRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
getModels: protectedProcedure
|
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 }) => {
|
.query(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
|
const providerName = getProviderName(input.apiUrl);
|
||||||
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
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) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
|
|||||||
@@ -54,7 +54,11 @@ import {
|
|||||||
applications,
|
applications,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
import {
|
||||||
|
addJobWithUserContext,
|
||||||
|
cleanQueuesByApplication,
|
||||||
|
myQueue,
|
||||||
|
} from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { uploadFileSchema } from "@/utils/schema";
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
|
|
||||||
@@ -668,14 +672,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await addJobWithUserContext({ ...jobData }, ctx.user.id);
|
||||||
"deployments",
|
|
||||||
{ ...jobData },
|
|
||||||
{
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cleanQueues: protectedProcedure
|
cleanQueues: protectedProcedure
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
type: "volume",
|
type: "volume",
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return newRedis;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -60,6 +60,8 @@ import {
|
|||||||
apiServerSchema,
|
apiServerSchema,
|
||||||
apiTraefikConfig,
|
apiTraefikConfig,
|
||||||
apiUpdateDockerCleanup,
|
apiUpdateDockerCleanup,
|
||||||
|
projects,
|
||||||
|
server,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { removeJob, schedule } from "@/server/utils/backup";
|
import { removeJob, schedule } from "@/server/utils/backup";
|
||||||
import packageInfo from "../../../package.json";
|
import packageInfo from "../../../package.json";
|
||||||
@@ -706,6 +708,18 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
isCloud: publicProcedure.query(async () => {
|
isCloud: publicProcedure.query(async () => {
|
||||||
return IS_CLOUD;
|
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 () => {
|
health: publicProcedure.query(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,14 +6,18 @@ declare global {
|
|||||||
var db: PostgresJsDatabase<typeof schema> | undefined;
|
var db: PostgresJsDatabase<typeof schema> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbUrl =
|
||||||
|
process.env.DATABASE_URL ||
|
||||||
|
"postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy";
|
||||||
|
|
||||||
export let db: PostgresJsDatabase<typeof schema>;
|
export let db: PostgresJsDatabase<typeof schema>;
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
db = drizzle(postgres(process.env.DATABASE_URL!), {
|
db = drizzle(postgres(dbUrl!), {
|
||||||
schema,
|
schema,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!global.db)
|
if (!global.db)
|
||||||
global.db = drizzle(postgres(process.env.DATABASE_URL!), {
|
global.db = drizzle(postgres(dbUrl!), {
|
||||||
schema,
|
schema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
|
||||||
import postgres from "postgres";
|
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL!;
|
|
||||||
|
|
||||||
const pg = postgres(connectionString, { max: 1 });
|
|
||||||
const _db = drizzle(pg);
|
|
||||||
|
|
||||||
async function seed() {
|
|
||||||
console.log("> Seed:", process.env.DATABASE_PATH, "\n");
|
|
||||||
|
|
||||||
// const authenticationR = await db
|
|
||||||
// .insert(users)
|
|
||||||
// .values([
|
|
||||||
// {
|
|
||||||
// email: "user1@hotmail.com",
|
|
||||||
// password: password("12345671"),
|
|
||||||
// },
|
|
||||||
// ])
|
|
||||||
// .onConflictDoNothing()
|
|
||||||
// .returning();
|
|
||||||
|
|
||||||
// console.log("\nSemillas Update:", authenticationR.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
seed().catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
104
apps/dokploy/server/queues/README.md
Normal file
104
apps/dokploy/server/queues/README.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Queue System Migration - BullMQ to p-limit
|
||||||
|
|
||||||
|
This directory contains the new queue system that replaces BullMQ with [p-limit](https://github.com/sindresorhus/p-limit) for deployment queues.
|
||||||
|
|
||||||
|
## Why the Migration?
|
||||||
|
|
||||||
|
- **Resource Issues**: Users experienced freezing during builds due to resource constraints
|
||||||
|
- **Cancellation Problems**: BullMQ workers couldn't be properly canceled when Docker processes restart
|
||||||
|
- **Retry Loops**: Unwanted automatic retries when processes are killed
|
||||||
|
|
||||||
|
## New Architecture
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
1. **Per-Server Queues**: Deployments are grouped by server (local "dokploy-server" or remote servers)
|
||||||
|
2. **Ordered Processing**: Within each server, deployments are processed based on server concurrency settings
|
||||||
|
3. **Global User Concurrency**: User's `serverConcurrency` controls total deployments across all servers
|
||||||
|
4. **Proper Cancellation**: Jobs can be canceled using AbortController
|
||||||
|
5. **No Redis Dependency**: In-memory queues eliminate Redis dependency issues
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
- `service-queue.ts` - New p-limit based queue implementation
|
||||||
|
- `queueSetup.ts` - Compatibility layer for existing code
|
||||||
|
- `deployments-queue.ts` - Legacy compatibility exports
|
||||||
|
- `queue-types.ts` - Shared type definitions
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { addJobWithUserContext, cancelDeploymentJobs, getDeploymentQueueStatus } from './queueSetup';
|
||||||
|
|
||||||
|
// Add a deployment job with user context (recommended for API routes)
|
||||||
|
const result = await addJobWithUserContext({
|
||||||
|
applicationType: 'application',
|
||||||
|
applicationId: '123',
|
||||||
|
type: 'deploy',
|
||||||
|
titleLog: 'Deploying app',
|
||||||
|
descriptionLog: 'Starting deployment',
|
||||||
|
serverId: 'server-456' // Optional - for remote deployments
|
||||||
|
}, 'user-id-789'); // User ID for concurrency settings
|
||||||
|
|
||||||
|
// Cancel jobs for a service
|
||||||
|
const cancelled = cancelDeploymentJobs('app-123');
|
||||||
|
|
||||||
|
// Get queue status
|
||||||
|
const status = getDeploymentQueueStatus('app-123');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database-Driven Concurrency
|
||||||
|
|
||||||
|
The system now automatically reads concurrency settings from the database:
|
||||||
|
|
||||||
|
1. **Global User Concurrency**: From `users_temp.serverConcurrency` field
|
||||||
|
- Controls the **TOTAL** number of deployments that can run simultaneously for a user
|
||||||
|
- Example: If `serverConcurrency = 1`, only 1 deployment across ALL services at a time
|
||||||
|
- Example: If `serverConcurrency = 3`, maximum 3 deployments can run simultaneously across all services
|
||||||
|
|
||||||
|
2. **Server Concurrency**: From `server.concurrency` field
|
||||||
|
- Controls how many deployments can run simultaneously **on a specific server**
|
||||||
|
- Only applies when deploying to remote servers (`serverId` is present)
|
||||||
|
- Example: Server A can handle 2 concurrent deployments, Server B can handle 1
|
||||||
|
|
||||||
|
### Concurrency Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
User Global Limit (users_temp.serverConcurrency)
|
||||||
|
├── dokploy-server (local deployments)
|
||||||
|
│ ├── App A deployment
|
||||||
|
│ ├── App B deployment
|
||||||
|
│ └── Compose C deployment
|
||||||
|
├── remote-server-1 (server.concurrency = 2)
|
||||||
|
│ ├── App D deployment
|
||||||
|
│ └── App E deployment
|
||||||
|
└── remote-server-2 (server.concurrency = 1)
|
||||||
|
└── App F deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Scenarios:**
|
||||||
|
|
||||||
|
- **User has `serverConcurrency = 1`**: Only 1 deployment total across ALL servers
|
||||||
|
- **User has `serverConcurrency = 3`**: Maximum 3 deployments simultaneously across all servers
|
||||||
|
- **Local server**: All local apps/compose share the "dokploy-server" queue
|
||||||
|
- **Remote server with `concurrency = 2`**: That server can handle up to 2 concurrent deployments
|
||||||
|
- **Queue grouping**: `app-123` and `app-456` on same server share the same queue
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **Global Concurrency**: Set how many services can deploy simultaneously
|
||||||
|
- **Service Concurrency**: Each service processes 1 deployment at a time (FIFO)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setGlobalConcurrency } from './service-queue';
|
||||||
|
|
||||||
|
// Allow 5 services to deploy simultaneously
|
||||||
|
setGlobalConcurrency(5);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
- The schedules app still uses BullMQ for cron/repeatable jobs (different use case)
|
||||||
|
- All existing API endpoints work unchanged due to compatibility layer
|
||||||
|
- No breaking changes to existing functionality
|
||||||
|
- Improved resource usage and cancellation capabilities
|
||||||
@@ -1,122 +1,58 @@
|
|||||||
import {
|
// This file is kept for backward compatibility but now uses the new service-queue system
|
||||||
deployApplication,
|
// The actual queue logic has been moved to service-queue.ts using p-limit
|
||||||
deployCompose,
|
|
||||||
deployPreviewApplication,
|
|
||||||
deployRemoteApplication,
|
|
||||||
deployRemoteCompose,
|
|
||||||
deployRemotePreviewApplication,
|
|
||||||
rebuildApplication,
|
|
||||||
rebuildCompose,
|
|
||||||
rebuildRemoteApplication,
|
|
||||||
rebuildRemoteCompose,
|
|
||||||
updateApplicationStatus,
|
|
||||||
updateCompose,
|
|
||||||
updatePreviewDeployment,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import { type Job, Worker } from "bullmq";
|
|
||||||
import type { DeploymentJob } from "./queue-types";
|
|
||||||
import { redisConfig } from "./redis-connection";
|
|
||||||
|
|
||||||
export const deploymentWorker = new Worker(
|
import { serviceQueueManager } from "./service-queue";
|
||||||
"deployments",
|
|
||||||
async (job: Job<DeploymentJob>) => {
|
|
||||||
try {
|
|
||||||
if (job.data.applicationType === "application") {
|
|
||||||
await updateApplicationStatus(job.data.applicationId, "running");
|
|
||||||
|
|
||||||
if (job.data.server) {
|
// Legacy compatibility - this is no longer used but kept to avoid breaking imports
|
||||||
if (job.data.type === "redeploy") {
|
export const deploymentWorker = {
|
||||||
await rebuildRemoteApplication({
|
run: async () => {
|
||||||
applicationId: job.data.applicationId,
|
console.log(
|
||||||
titleLog: job.data.titleLog,
|
"Legacy deploymentWorker.run() called - now using service-queue system",
|
||||||
descriptionLog: job.data.descriptionLog,
|
);
|
||||||
});
|
// The service queue manager starts automatically, no need to do anything
|
||||||
} else if (job.data.type === "deploy") {
|
return Promise.resolve();
|
||||||
await deployRemoteApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (job.data.type === "redeploy") {
|
|
||||||
await rebuildApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
} else if (job.data.type === "deploy") {
|
|
||||||
await deployApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (job.data.applicationType === "compose") {
|
|
||||||
await updateCompose(job.data.composeId, {
|
|
||||||
composeStatus: "running",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (job.data.server) {
|
|
||||||
if (job.data.type === "redeploy") {
|
|
||||||
await rebuildRemoteCompose({
|
|
||||||
composeId: job.data.composeId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
} else if (job.data.type === "deploy") {
|
|
||||||
await deployRemoteCompose({
|
|
||||||
composeId: job.data.composeId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (job.data.type === "deploy") {
|
|
||||||
await deployCompose({
|
|
||||||
composeId: job.data.composeId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
} else if (job.data.type === "redeploy") {
|
|
||||||
await rebuildCompose({
|
|
||||||
composeId: job.data.composeId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (job.data.applicationType === "application-preview") {
|
|
||||||
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
|
||||||
previewStatus: "running",
|
|
||||||
});
|
|
||||||
if (job.data.server) {
|
|
||||||
if (job.data.type === "deploy") {
|
|
||||||
await deployRemotePreviewApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (job.data.type === "deploy") {
|
|
||||||
await deployPreviewApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error", error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
close: async () => {
|
||||||
autorun: false,
|
console.log("Legacy deploymentWorker.close() called");
|
||||||
connection: redisConfig,
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
);
|
};
|
||||||
|
|
||||||
|
// Legacy exports for backward compatibility
|
||||||
|
export const getWorkersMap = () => {
|
||||||
|
console.warn(
|
||||||
|
"getWorkersMap() is deprecated - use serviceQueueManager instead",
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWorker = (_serverId?: string) => {
|
||||||
|
console.warn("getWorker() is deprecated - use serviceQueueManager instead");
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDeploymentWorker = (defaultConcurrency = 1) => {
|
||||||
|
console.warn(
|
||||||
|
"createDeploymentWorker() is deprecated - use serviceQueueManager instead",
|
||||||
|
);
|
||||||
|
serviceQueueManager.setGlobalConcurrency(defaultConcurrency);
|
||||||
|
return deploymentWorker;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createServerDeploymentWorker = (
|
||||||
|
_serverId: string,
|
||||||
|
_concurrency = 1,
|
||||||
|
) => {
|
||||||
|
console.warn(
|
||||||
|
"createServerDeploymentWorker() is deprecated - use serviceQueueManager instead",
|
||||||
|
);
|
||||||
|
// The new system automatically creates queues per service, no need for explicit worker creation
|
||||||
|
return deploymentWorker;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeServerDeploymentWorker = (serverId: string) => {
|
||||||
|
console.warn(
|
||||||
|
"removeServerDeploymentWorker() is deprecated - use removeServiceQueue instead",
|
||||||
|
);
|
||||||
|
serviceQueueManager.removeServiceQueue(serverId);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,44 +1,101 @@
|
|||||||
import { Queue } from "bullmq";
|
import type { DeploymentJob } from "./queue-types";
|
||||||
import { redisConfig } from "./redis-connection";
|
import {
|
||||||
|
addDeploymentJob,
|
||||||
|
cancelDeploymentJobs,
|
||||||
|
getDeploymentQueueStatus,
|
||||||
|
setGlobalConcurrency,
|
||||||
|
} from "./service-queue";
|
||||||
|
|
||||||
const myQueue = new Queue("deployments", {
|
// Default queue name for local deployments
|
||||||
connection: redisConfig,
|
export const DEFAULT_QUEUE = "default";
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
// Initialize with default concurrency of 3 services
|
||||||
myQueue.close();
|
setGlobalConcurrency(3);
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
myQueue.on("error", (error) => {
|
// Helper function to determine service ID from job data
|
||||||
if ((error as any).code === "ECONNREFUSED") {
|
// Groups deployments by SERVER, not by individual application/compose
|
||||||
console.error(
|
const getServiceId = (jobData: DeploymentJob): string => {
|
||||||
"Make sure you have installed Redis and it is running.",
|
// If it has a serverId, group by that server
|
||||||
error,
|
if (jobData.serverId) {
|
||||||
);
|
return jobData.serverId;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// For local deployments (no serverId), group all under the main Dokploy server
|
||||||
|
return "dokploy-server";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compatibility functions to replace BullMQ usage
|
||||||
|
export const myQueue = {
|
||||||
|
add: async (
|
||||||
|
_name: string,
|
||||||
|
jobData: DeploymentJob,
|
||||||
|
_options?: any,
|
||||||
|
userId?: string,
|
||||||
|
) => {
|
||||||
|
const serviceId = getServiceId(jobData);
|
||||||
|
const jobId = await addDeploymentJob(serviceId, jobData, userId);
|
||||||
|
console.log(`Added deployment job ${jobId} to service ${serviceId}`);
|
||||||
|
return { id: jobId };
|
||||||
|
},
|
||||||
|
|
||||||
|
close: () => {
|
||||||
|
console.log("Service queue manager shutdown initiated");
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
// Cancel jobs for this specific application across all servers
|
||||||
|
let totalCancelled = 0;
|
||||||
|
|
||||||
for (const job of jobs) {
|
// Check the local Dokploy server
|
||||||
if (job?.data?.applicationId === applicationId) {
|
const localCancelled = cancelDeploymentJobs(
|
||||||
await job.remove();
|
"dokploy-server",
|
||||||
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
applicationId,
|
||||||
}
|
undefined,
|
||||||
}
|
);
|
||||||
|
totalCancelled += localCancelled;
|
||||||
|
|
||||||
|
// TODO: Also check remote servers if we need to track which servers have this application
|
||||||
|
// For now, we only clean from the local server queue
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Cancelled ${totalCancelled} jobs for application ${applicationId}`,
|
||||||
|
);
|
||||||
|
return totalCancelled;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
// Cancel jobs for this specific compose across all servers
|
||||||
|
let totalCancelled = 0;
|
||||||
|
|
||||||
for (const job of jobs) {
|
// Check the local Dokploy server
|
||||||
if (job?.data?.composeId === composeId) {
|
const localCancelled = cancelDeploymentJobs(
|
||||||
await job.remove();
|
"dokploy-server",
|
||||||
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
undefined,
|
||||||
}
|
composeId,
|
||||||
}
|
);
|
||||||
|
totalCancelled += localCancelled;
|
||||||
|
|
||||||
|
// TODO: Also check remote servers if we need to track which servers have this compose
|
||||||
|
// For now, we only clean from the local server queue
|
||||||
|
|
||||||
|
console.log(`Cancelled ${totalCancelled} jobs for compose ${composeId}`);
|
||||||
|
return totalCancelled;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { myQueue };
|
// Export queue status for monitoring
|
||||||
|
export const getQueueStatus = getDeploymentQueueStatus;
|
||||||
|
|
||||||
|
// New function to add jobs with user context (for API routes)
|
||||||
|
export const addJobWithUserContext = async (
|
||||||
|
jobData: DeploymentJob,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<{ id: string }> => {
|
||||||
|
const serviceId = getServiceId(jobData);
|
||||||
|
const jobId = await addDeploymentJob(serviceId, jobData, userId);
|
||||||
|
console.log(
|
||||||
|
`Added deployment job ${jobId} to service ${serviceId} with user context ${userId || "none"}`,
|
||||||
|
);
|
||||||
|
return { id: jobId };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { ConnectionOptions } from "bullmq";
|
|
||||||
|
|
||||||
export const redisConfig: ConnectionOptions = {
|
|
||||||
host:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? process.env.REDIS_HOST || "dokploy-redis"
|
|
||||||
: "127.0.0.1",
|
|
||||||
};
|
|
||||||
500
apps/dokploy/server/queues/service-queue.ts
Normal file
500
apps/dokploy/server/queues/service-queue.ts
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
import {
|
||||||
|
deployApplication,
|
||||||
|
deployCompose,
|
||||||
|
deployPreviewApplication,
|
||||||
|
deployRemoteApplication,
|
||||||
|
deployRemoteCompose,
|
||||||
|
deployRemotePreviewApplication,
|
||||||
|
findServerById,
|
||||||
|
rebuildApplication,
|
||||||
|
rebuildCompose,
|
||||||
|
rebuildRemoteApplication,
|
||||||
|
rebuildRemoteCompose,
|
||||||
|
updateApplicationStatus,
|
||||||
|
updateCompose,
|
||||||
|
updatePreviewDeployment,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { db } from "@dokploy/server/db";
|
||||||
|
import { users_temp } from "@dokploy/server/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import pLimit from "p-limit";
|
||||||
|
import type { DeploymentJob } from "./queue-types";
|
||||||
|
|
||||||
|
// Types for our p-limit based queue system
|
||||||
|
export interface QueueJob {
|
||||||
|
id: string;
|
||||||
|
data: DeploymentJob;
|
||||||
|
createdAt: Date;
|
||||||
|
status: "waiting" | "processing" | "completed" | "failed" | "cancelled";
|
||||||
|
abortController: AbortController;
|
||||||
|
promise?: Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceQueue {
|
||||||
|
serviceId: string;
|
||||||
|
jobs: QueueJob[];
|
||||||
|
limit: ReturnType<typeof pLimit>; // p-limit instance with concurrency 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global queue management using p-limit
|
||||||
|
class ServiceQueueManager {
|
||||||
|
private queues: Map<string, ServiceQueue> = new Map();
|
||||||
|
private globalLimit: ReturnType<typeof pLimit>;
|
||||||
|
private isShuttingDown = false;
|
||||||
|
|
||||||
|
constructor(globalConcurrency = 3) {
|
||||||
|
// Global limit controls how many services can deploy simultaneously
|
||||||
|
this.globalLimit = pLimit(globalConcurrency);
|
||||||
|
this.setupShutdownHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global concurrency (how many services can deploy simultaneously)
|
||||||
|
setGlobalConcurrency(concurrency: number) {
|
||||||
|
this.globalLimit = pLimit(concurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get concurrency settings from database
|
||||||
|
private async getConcurrencySettings(jobData: DeploymentJob): Promise<{
|
||||||
|
serviceConcurrency: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Default: Each service processes 1 deployment at a time (FIFO within service)
|
||||||
|
let serviceConcurrency = 1;
|
||||||
|
|
||||||
|
// If it's a server deployment, get server-specific concurrency
|
||||||
|
// This controls how many deployments can run simultaneously ON THAT SERVER
|
||||||
|
if (jobData.serverId) {
|
||||||
|
try {
|
||||||
|
const serverData = await findServerById(jobData.serverId);
|
||||||
|
serviceConcurrency = serverData.concurrency || 1;
|
||||||
|
console.log(
|
||||||
|
`Server ${jobData.serverId} can handle ${serviceConcurrency} concurrent deployments`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Could not get server concurrency for ${jobData.serverId}, using default: 1`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceConcurrency,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"Error getting concurrency settings, using defaults:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
serviceConcurrency: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create a queue for a service with dynamic concurrency
|
||||||
|
private async getOrCreateQueue(
|
||||||
|
serviceId: string,
|
||||||
|
jobData?: DeploymentJob,
|
||||||
|
): Promise<ServiceQueue> {
|
||||||
|
if (!this.queues.has(serviceId)) {
|
||||||
|
let serviceConcurrency = 1; // Default
|
||||||
|
|
||||||
|
// Get concurrency from database if we have job data
|
||||||
|
if (jobData) {
|
||||||
|
const settings = await this.getConcurrencySettings(jobData);
|
||||||
|
serviceConcurrency = settings.serviceConcurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queues.set(serviceId, {
|
||||||
|
serviceId,
|
||||||
|
jobs: [],
|
||||||
|
// Service concurrency from database or default to 1
|
||||||
|
limit: pLimit(serviceConcurrency),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Created queue for service ${serviceId} with concurrency: ${serviceConcurrency}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.queues.get(serviceId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a job to a service queue
|
||||||
|
async addJob(
|
||||||
|
serviceId: string,
|
||||||
|
jobData: DeploymentJob,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
if (this.isShuttingDown) {
|
||||||
|
throw new Error("Queue manager is shutting down");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update global concurrency based on user settings if provided
|
||||||
|
// This controls the TOTAL number of deployments across ALL services for this user
|
||||||
|
if (userId) {
|
||||||
|
try {
|
||||||
|
const userData = await db.query.users_temp.findFirst({
|
||||||
|
where: eq(users_temp.id, userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userData?.serverConcurrency) {
|
||||||
|
// This is GLOBAL concurrency - total deployments across all services
|
||||||
|
this.globalLimit = pLimit(userData.serverConcurrency);
|
||||||
|
console.log(
|
||||||
|
`Set GLOBAL concurrency to ${userData.serverConcurrency} deployments total for user ${userId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Could not get user concurrency settings for ${userId}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = await this.getOrCreateQueue(serviceId, jobData);
|
||||||
|
const jobId = `${serviceId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
const job: QueueJob = {
|
||||||
|
id: jobId,
|
||||||
|
data: jobData,
|
||||||
|
createdAt: new Date(),
|
||||||
|
status: "waiting",
|
||||||
|
abortController: new AbortController(),
|
||||||
|
};
|
||||||
|
|
||||||
|
queue.jobs.push(job);
|
||||||
|
console.log(
|
||||||
|
`Added job ${jobId} to service ${serviceId} queue. Queue length: ${queue.jobs.length}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start processing the job using p-limit
|
||||||
|
this.processJob(queue, job);
|
||||||
|
|
||||||
|
return jobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a job using both global and service-level p-limit
|
||||||
|
private processJob(queue: ServiceQueue, job: QueueJob) {
|
||||||
|
// Use global limit to control cross-service concurrency
|
||||||
|
job.promise = this.globalLimit(() =>
|
||||||
|
// Use service limit to ensure ordered processing within service
|
||||||
|
queue.limit(async () => {
|
||||||
|
if (job.status === "cancelled" || this.isShuttingDown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
job.status = "processing";
|
||||||
|
console.log(`Processing job ${job.id} for service ${queue.serviceId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.executeJob(job);
|
||||||
|
job.status = "completed";
|
||||||
|
console.log(`Completed job ${job.id} for service ${queue.serviceId}`);
|
||||||
|
} catch (error) {
|
||||||
|
if (job.abortController.signal.aborted) {
|
||||||
|
job.status = "cancelled";
|
||||||
|
console.log(
|
||||||
|
`Job ${job.id} was cancelled for service ${queue.serviceId}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
job.status = "failed";
|
||||||
|
console.error(
|
||||||
|
`Job ${job.id} failed for service ${queue.serviceId}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up completed/failed jobs after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
queue.jobs = queue.jobs.filter((j) => j.id !== job.id);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove/cancel jobs for a specific service
|
||||||
|
cancelJobsByService(
|
||||||
|
serviceId: string,
|
||||||
|
applicationId?: string,
|
||||||
|
composeId?: string,
|
||||||
|
): number {
|
||||||
|
const queue = this.queues.get(serviceId);
|
||||||
|
if (!queue) return 0;
|
||||||
|
|
||||||
|
let cancelledCount = 0;
|
||||||
|
|
||||||
|
// Cancel waiting and processing jobs
|
||||||
|
for (const job of queue.jobs) {
|
||||||
|
if (job.status === "waiting" || job.status === "processing") {
|
||||||
|
// Check if this job matches the filter criteria
|
||||||
|
const matchesApplication = applicationId
|
||||||
|
? (job.data.applicationType === "application" ||
|
||||||
|
job.data.applicationType === "application-preview") &&
|
||||||
|
job.data.applicationId === applicationId
|
||||||
|
: true;
|
||||||
|
const matchesCompose = composeId
|
||||||
|
? job.data.applicationType === "compose" &&
|
||||||
|
job.data.composeId === composeId
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (matchesApplication && matchesCompose) {
|
||||||
|
job.status = "cancelled";
|
||||||
|
job.abortController.abort();
|
||||||
|
cancelledCount++;
|
||||||
|
console.log(`Cancelled job ${job.id} for service ${serviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove cancelled jobs from queue immediately
|
||||||
|
queue.jobs = queue.jobs.filter((job) => job.status !== "cancelled");
|
||||||
|
|
||||||
|
return cancelledCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get queue status for a service
|
||||||
|
getQueueStatus(serviceId: string) {
|
||||||
|
const queue = this.queues.get(serviceId);
|
||||||
|
if (!queue) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceId,
|
||||||
|
totalJobs: queue.jobs.length,
|
||||||
|
waitingJobs: queue.jobs.filter((j) => j.status === "waiting").length,
|
||||||
|
processingJobs: queue.jobs.filter((j) => j.status === "processing")
|
||||||
|
.length,
|
||||||
|
completedJobs: queue.jobs.filter((j) => j.status === "completed").length,
|
||||||
|
failedJobs: queue.jobs.filter((j) => j.status === "failed").length,
|
||||||
|
// p-limit queue status
|
||||||
|
activeCount: queue.limit.activeCount,
|
||||||
|
pendingCount: queue.limit.pendingCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all queues status
|
||||||
|
getAllQueuesStatus() {
|
||||||
|
const status: Record<string, any> = {};
|
||||||
|
for (const [serviceId] of this.queues) {
|
||||||
|
status[serviceId] = this.getQueueStatus(serviceId);
|
||||||
|
}
|
||||||
|
status.global = {
|
||||||
|
activeCount: this.globalLimit.activeCount,
|
||||||
|
pendingCount: this.globalLimit.pendingCount,
|
||||||
|
concurrency: this.globalLimit.concurrency,
|
||||||
|
};
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear pending jobs from a service queue using p-limit's clearQueue
|
||||||
|
clearServiceQueue(serviceId: string) {
|
||||||
|
const queue = this.queues.get(serviceId);
|
||||||
|
if (queue) {
|
||||||
|
// Cancel all waiting jobs
|
||||||
|
for (const job of queue.jobs) {
|
||||||
|
if (job.status === "waiting") {
|
||||||
|
job.status = "cancelled";
|
||||||
|
job.abortController.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear p-limit's internal queue
|
||||||
|
queue.limit.clearQueue();
|
||||||
|
|
||||||
|
// Remove cancelled jobs
|
||||||
|
queue.jobs = queue.jobs.filter((job) => job.status !== "cancelled");
|
||||||
|
|
||||||
|
console.log(`Cleared service queue for ${serviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeJob(job: QueueJob): Promise<void> {
|
||||||
|
const { data } = job;
|
||||||
|
|
||||||
|
// Check if job was cancelled before execution
|
||||||
|
if (job.abortController.signal.aborted) {
|
||||||
|
throw new Error("Job was cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (data.applicationType === "application") {
|
||||||
|
await updateApplicationStatus(data.applicationId, "running");
|
||||||
|
|
||||||
|
if (data.server) {
|
||||||
|
if (data.type === "redeploy") {
|
||||||
|
await rebuildRemoteApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (data.type === "deploy") {
|
||||||
|
await deployRemoteApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.type === "redeploy") {
|
||||||
|
await rebuildApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (data.type === "deploy") {
|
||||||
|
await deployApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.applicationType === "compose") {
|
||||||
|
await updateCompose(data.composeId, {
|
||||||
|
composeStatus: "running",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.server) {
|
||||||
|
if (data.type === "redeploy") {
|
||||||
|
await rebuildRemoteCompose({
|
||||||
|
composeId: data.composeId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (data.type === "deploy") {
|
||||||
|
await deployRemoteCompose({
|
||||||
|
composeId: data.composeId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.type === "deploy") {
|
||||||
|
await deployCompose({
|
||||||
|
composeId: data.composeId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (data.type === "redeploy") {
|
||||||
|
await rebuildCompose({
|
||||||
|
composeId: data.composeId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(data.previewDeploymentId, {
|
||||||
|
previewStatus: "running",
|
||||||
|
});
|
||||||
|
if (data.server) {
|
||||||
|
if (data.type === "deploy") {
|
||||||
|
await deployRemotePreviewApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
previewDeploymentId: data.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.type === "deploy") {
|
||||||
|
await deployPreviewApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
previewDeploymentId: data.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Deployment Error", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupShutdownHandlers() {
|
||||||
|
const gracefulShutdown = async () => {
|
||||||
|
console.log("Shutting down service queue manager...");
|
||||||
|
this.isShuttingDown = true;
|
||||||
|
|
||||||
|
// Cancel all jobs
|
||||||
|
for (const queue of this.queues.values()) {
|
||||||
|
for (const job of queue.jobs) {
|
||||||
|
job.abortController.abort();
|
||||||
|
}
|
||||||
|
// Clear p-limit queues
|
||||||
|
queue.limit.clearQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear global queue
|
||||||
|
this.globalLimit.clearQueue();
|
||||||
|
|
||||||
|
// Wait a bit for jobs to finish cancelling
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGTERM", gracefulShutdown);
|
||||||
|
process.on("SIGINT", gracefulShutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a specific service queue entirely
|
||||||
|
removeServiceQueue(serviceId: string) {
|
||||||
|
const queue = this.queues.get(serviceId);
|
||||||
|
if (queue) {
|
||||||
|
// Cancel all jobs in the queue
|
||||||
|
for (const job of queue.jobs) {
|
||||||
|
job.abortController.abort();
|
||||||
|
}
|
||||||
|
// Clear p-limit queue
|
||||||
|
queue.limit.clearQueue();
|
||||||
|
this.queues.delete(serviceId);
|
||||||
|
console.log(`Removed service queue for ${serviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global instance
|
||||||
|
export const serviceQueueManager = new ServiceQueueManager();
|
||||||
|
|
||||||
|
// Helper functions to maintain compatibility with existing code
|
||||||
|
export const addDeploymentJob = async (
|
||||||
|
serviceId: string,
|
||||||
|
jobData: DeploymentJob,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
return await serviceQueueManager.addJob(serviceId, jobData, userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cancelDeploymentJobs = (
|
||||||
|
serviceId: string,
|
||||||
|
applicationId?: string,
|
||||||
|
composeId?: string,
|
||||||
|
): number => {
|
||||||
|
return serviceQueueManager.cancelJobsByService(
|
||||||
|
serviceId,
|
||||||
|
applicationId,
|
||||||
|
composeId,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeploymentQueueStatus = (serviceId?: string) => {
|
||||||
|
if (serviceId) {
|
||||||
|
return serviceQueueManager.getQueueStatus(serviceId);
|
||||||
|
}
|
||||||
|
return serviceQueueManager.getAllQueuesStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setGlobalConcurrency = (concurrency: number) => {
|
||||||
|
serviceQueueManager.setGlobalConcurrency(concurrency);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeServiceQueue = (serviceId: string) => {
|
||||||
|
serviceQueueManager.removeServiceQueue(serviceId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearServiceQueue = (serviceId: string) => {
|
||||||
|
serviceQueueManager.clearServiceQueue(serviceId);
|
||||||
|
};
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
await initializeNetwork();
|
await initializeNetwork();
|
||||||
createDefaultTraefikConfig();
|
createDefaultTraefikConfig();
|
||||||
createDefaultServerTraefikConfig();
|
createDefaultServerTraefikConfig();
|
||||||
await execAsync("docker pull traefik:v3.1.2");
|
await execAsync("docker pull traefik:v3.5.0");
|
||||||
await initializeStandaloneTraefik();
|
await initializeStandaloneTraefik();
|
||||||
await initializeRedis();
|
await initializeRedis();
|
||||||
await initializePostgres();
|
await initializePostgres();
|
||||||
|
|||||||
@@ -28,13 +28,13 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^1.2.12",
|
"@ai-sdk/anthropic": "^2.0.5",
|
||||||
"@ai-sdk/azure": "^1.3.23",
|
"@ai-sdk/azure": "^2.0.16",
|
||||||
"@ai-sdk/cohere": "^1.2.10",
|
"@ai-sdk/cohere": "^2.0.4",
|
||||||
"@ai-sdk/deepinfra": "^0.0.4",
|
"@ai-sdk/deepinfra": "^1.0.10",
|
||||||
"@ai-sdk/mistral": "^1.2.8",
|
"@ai-sdk/mistral": "^2.0.7",
|
||||||
"@ai-sdk/openai": "^1.3.22",
|
"@ai-sdk/openai": "^2.0.16",
|
||||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
"@ai-sdk/openai-compatible": "^1.0.10",
|
||||||
"@better-auth/utils": "0.2.4",
|
"@better-auth/utils": "0.2.4",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@octokit/auth-app": "^6.1.3",
|
"@octokit/auth-app": "^6.1.3",
|
||||||
@@ -44,7 +44,8 @@
|
|||||||
"@react-email/components": "^0.0.21",
|
"@react-email/components": "^0.0.21",
|
||||||
"@trpc/server": "^10.45.2",
|
"@trpc/server": "^10.45.2",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"ai": "^4.3.16",
|
"ai": "^5.0.17",
|
||||||
|
"ai-sdk-ollama": "^0.5.1",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "v1.2.8-beta.7",
|
"better-auth": "v1.2.8-beta.7",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
@@ -65,7 +66,6 @@
|
|||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
"nodemailer": "6.9.14",
|
"nodemailer": "6.9.14",
|
||||||
"octokit": "3.1.2",
|
"octokit": "3.1.2",
|
||||||
"ollama-ai-provider": "^1.2.0",
|
|
||||||
"otpauth": "^9.4.0",
|
"otpauth": "^9.4.0",
|
||||||
"pino": "9.4.0",
|
"pino": "9.4.0",
|
||||||
"pino-pretty": "11.2.2",
|
"pino-pretty": "11.2.2",
|
||||||
@@ -111,4 +111,4 @@
|
|||||||
"node": "^20.16.0",
|
"node": "^20.16.0",
|
||||||
"pnpm": ">=9.12.0"
|
"pnpm": ">=9.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { defineConfig } from "drizzle-kit";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
schema: "./server/db/schema/index.ts",
|
|
||||||
dialect: "postgresql",
|
|
||||||
dbCredentials: {
|
|
||||||
url: process.env.DATABASE_URL!,
|
|
||||||
},
|
|
||||||
out: "drizzle",
|
|
||||||
migrations: {
|
|
||||||
table: "migrations",
|
|
||||||
schema: "public",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// 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 db = drizzle(sql);
|
|
||||||
|
|
||||||
// export const migration = async () =>
|
|
||||||
// await migrate(db, { migrationsFolder: "drizzle" })
|
|
||||||
// .then(() => {
|
|
||||||
// console.log("Migration complete");
|
|
||||||
// sql.end();
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// console.log("Migration failed", error);
|
|
||||||
// })
|
|
||||||
// .finally(() => {
|
|
||||||
// sql.end();
|
|
||||||
// });
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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 db = drizzle(pg);
|
|
||||||
|
|
||||||
const clearDb = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const tablesQuery = sql<string>`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`;
|
|
||||||
const tables = await db.execute(tablesQuery);
|
|
||||||
console.log(tables);
|
|
||||||
await pg.end();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error cleaning database", error);
|
|
||||||
} finally {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
clearDb();
|
|
||||||
@@ -32,7 +32,7 @@ export const aiRelations = relations(ai, ({ one }) => ({
|
|||||||
const createSchema = createInsertSchema(ai, {
|
const createSchema = createInsertSchema(ai, {
|
||||||
name: z.string().min(1, { message: "Name is required" }),
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
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" }),
|
model: z.string().min(1, { message: "Model is required" }),
|
||||||
isEnabled: z.boolean().optional(),
|
isEnabled: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const applications = pgTable("application", {
|
|||||||
previewEnv: text("previewEnv"),
|
previewEnv: text("previewEnv"),
|
||||||
watchPaths: text("watchPaths").array(),
|
watchPaths: text("watchPaths").array(),
|
||||||
previewBuildArgs: text("previewBuildArgs"),
|
previewBuildArgs: text("previewBuildArgs"),
|
||||||
|
previewLabels: text("previewLabels").array(),
|
||||||
previewWildcard: text("previewWildcard"),
|
previewWildcard: text("previewWildcard"),
|
||||||
previewPort: integer("previewPort").default(3000),
|
previewPort: integer("previewPort").default(3000),
|
||||||
previewHttps: boolean("previewHttps").notNull().default(false),
|
previewHttps: boolean("previewHttps").notNull().default(false),
|
||||||
@@ -308,6 +309,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
previewLabels: z.array(z.string()).optional(),
|
||||||
cleanCache: z.boolean().optional(),
|
cleanCache: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const server = pgTable("server", {
|
|||||||
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
concurrency: integer("concurrency").notNull().default(1),
|
||||||
metricsConfig: jsonb("metricsConfig")
|
metricsConfig: jsonb("metricsConfig")
|
||||||
.$type<{
|
.$type<{
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const users_temp = pgTable("user_temp", {
|
|||||||
// Metrics
|
// Metrics
|
||||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||||
|
serverConcurrency: integer("serverConcurrency").notNull().default(1),
|
||||||
metricsConfig: jsonb("metricsConfig")
|
metricsConfig: jsonb("metricsConfig")
|
||||||
.$type<{
|
.$type<{
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
// import bc from "bcrypt";
|
|
||||||
// import { drizzle } from "drizzle-orm/postgres-js";
|
|
||||||
// import postgres from "postgres";
|
|
||||||
// import { users } from "./schema";
|
|
||||||
|
|
||||||
// const connectionString = process.env.DATABASE_URL!;
|
|
||||||
|
|
||||||
// const pg = postgres(connectionString, { max: 1 });
|
|
||||||
// const db = drizzle(pg);
|
|
||||||
|
|
||||||
// function password(txt: string) {
|
|
||||||
// return bc.hashSync(txt, 10);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async function seed() {
|
|
||||||
// console.log("> Seed:", process.env.DATABASE_PATH, "\n");
|
|
||||||
|
|
||||||
// // const authenticationR = await db
|
|
||||||
// // .insert(users)
|
|
||||||
// // .values([
|
|
||||||
// // {
|
|
||||||
// // email: "user1@hotmail.com",
|
|
||||||
// // password: password("12345671"),
|
|
||||||
// // },
|
|
||||||
// // ])
|
|
||||||
// // .onConflictDoNothing()
|
|
||||||
// // .returning();
|
|
||||||
|
|
||||||
// // console.log("\nSemillas Update:", authenticationR.length);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// seed().catch((e) => {
|
|
||||||
// console.error(e);
|
|
||||||
// process.exit(1);
|
|
||||||
// });
|
|
||||||
@@ -70,7 +70,7 @@ export const findApplicationByPreview = async (applicationId: string) => {
|
|||||||
if (!application) {
|
if (!application) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "Applicationnot found",
|
message: "Application not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return application;
|
return application;
|
||||||
@@ -78,35 +78,41 @@ export const findApplicationByPreview = async (applicationId: string) => {
|
|||||||
|
|
||||||
export const removePreviewDeployment = async (previewDeploymentId: string) => {
|
export const removePreviewDeployment = async (previewDeploymentId: string) => {
|
||||||
try {
|
try {
|
||||||
const application = await findApplicationByPreview(previewDeploymentId);
|
|
||||||
const previewDeployment =
|
const previewDeployment =
|
||||||
await findPreviewDeploymentById(previewDeploymentId);
|
await findPreviewDeploymentById(previewDeploymentId);
|
||||||
|
const application = await findApplicationById(
|
||||||
const deployment = await db
|
previewDeployment.applicationId,
|
||||||
.delete(previewDeployments)
|
);
|
||||||
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
|
async () =>
|
||||||
|
await removeService(application?.appName, application?.serverId),
|
||||||
async () =>
|
async () =>
|
||||||
await removeDeploymentsByPreviewDeploymentId(
|
await removeDeploymentsByPreviewDeploymentId(
|
||||||
previewDeployment,
|
previewDeployment,
|
||||||
application.serverId,
|
application?.serverId,
|
||||||
),
|
),
|
||||||
async () =>
|
async () =>
|
||||||
await removeDirectoryCode(application.appName, application.serverId),
|
await removeDirectoryCode(application?.appName, application?.serverId),
|
||||||
async () =>
|
async () =>
|
||||||
await removeTraefikConfig(application.appName, application.serverId),
|
await removeTraefikConfig(application?.appName, application?.serverId),
|
||||||
async () =>
|
async () =>
|
||||||
await removeService(application?.appName, application.serverId),
|
await db
|
||||||
|
.delete(previewDeployments)
|
||||||
|
.where(
|
||||||
|
eq(previewDeployments.previewDeploymentId, previewDeploymentId),
|
||||||
|
)
|
||||||
|
.returning(),
|
||||||
];
|
];
|
||||||
for (const operation of cleanupOperations) {
|
for (const operation of cleanupOperations) {
|
||||||
try {
|
try {
|
||||||
await operation();
|
await operation();
|
||||||
} catch {}
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return deployment[0];
|
return previewDeployment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const TRAEFIK_PORT =
|
|||||||
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||||
export const TRAEFIK_HTTP3_PORT =
|
export const TRAEFIK_HTTP3_PORT =
|
||||||
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
|
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 {
|
export interface TraefikOptions {
|
||||||
env?: string[];
|
env?: string[];
|
||||||
|
|||||||
@@ -5,17 +5,16 @@ import { createDeepInfra } from "@ai-sdk/deepinfra";
|
|||||||
import { createMistral } from "@ai-sdk/mistral";
|
import { createMistral } from "@ai-sdk/mistral";
|
||||||
import { createOpenAI } from "@ai-sdk/openai";
|
import { createOpenAI } from "@ai-sdk/openai";
|
||||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
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("api.openai.com")) return "openai";
|
||||||
if (apiUrl.includes("azure.com")) return "azure";
|
if (apiUrl.includes("azure.com")) return "azure";
|
||||||
if (apiUrl.includes("api.anthropic.com")) return "anthropic";
|
if (apiUrl.includes("api.anthropic.com")) return "anthropic";
|
||||||
if (apiUrl.includes("api.cohere.ai")) return "cohere";
|
if (apiUrl.includes("api.cohere.ai")) return "cohere";
|
||||||
if (apiUrl.includes("api.perplexity.ai")) return "perplexity";
|
if (apiUrl.includes("api.perplexity.ai")) return "perplexity";
|
||||||
if (apiUrl.includes("api.mistral.ai")) return "mistral";
|
if (apiUrl.includes("api.mistral.ai")) return "mistral";
|
||||||
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
|
if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
|
||||||
return "ollama";
|
|
||||||
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
||||||
return "custom";
|
return "custom";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,9 @@ export const addDomainToCompose = async (
|
|||||||
if (!labels.includes("traefik.docker.network=dokploy-network")) {
|
if (!labels.includes("traefik.docker.network=dokploy-network")) {
|
||||||
labels.unshift("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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,14 @@ export const prepareEnvironmentVariables = (
|
|||||||
throw new Error(`Invalid project environment variable: project.${ref}`);
|
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}`;
|
return `${key}=${resolvedValue}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
|||||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||||
import {
|
import {
|
||||||
createDeploymentVolumeBackup,
|
createDeploymentVolumeBackup,
|
||||||
|
updateDeploymentStatus,
|
||||||
|
} from "@dokploy/server/services/deployment";
|
||||||
|
import {
|
||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
updateDeploymentStatus,
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
} from "../..";
|
|
||||||
import { backupVolume } from "./backup";
|
import { backupVolume } from "./backup";
|
||||||
|
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||||
|
|
||||||
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
|
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
|
||||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||||
@@ -20,6 +23,33 @@ export const removeVolumeBackupJob = async (volumeBackupId: string) => {
|
|||||||
currentJob?.cancel();
|
currentJob?.cancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cleanupOldVolumeBackups = async (
|
||||||
|
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||||
|
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) => {
|
export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||||
const serverId =
|
const serverId =
|
||||||
@@ -40,6 +70,10 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
|||||||
await execAsync(commandWithLog);
|
await execAsync(commandWithLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (volumeBackup.keepLatestCount && volumeBackup.keepLatestCount > 0) {
|
||||||
|
await cleanupOldVolumeBackups(volumeBackup, serverId);
|
||||||
|
}
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
|||||||
378
pnpm-lock.yaml
generated
378
pnpm-lock.yaml
generated
@@ -101,26 +101,26 @@ importers:
|
|||||||
apps/dokploy:
|
apps/dokploy:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/anthropic':
|
'@ai-sdk/anthropic':
|
||||||
specifier: ^1.2.12
|
specifier: ^2.0.5
|
||||||
version: 1.2.12(zod@3.25.32)
|
version: 2.0.5(zod@3.25.32)
|
||||||
'@ai-sdk/azure':
|
'@ai-sdk/azure':
|
||||||
specifier: ^1.3.23
|
specifier: ^2.0.16
|
||||||
version: 1.3.23(zod@3.25.32)
|
version: 2.0.16(zod@3.25.32)
|
||||||
'@ai-sdk/cohere':
|
'@ai-sdk/cohere':
|
||||||
specifier: ^1.2.10
|
specifier: ^2.0.4
|
||||||
version: 1.2.10(zod@3.25.32)
|
version: 2.0.4(zod@3.25.32)
|
||||||
'@ai-sdk/deepinfra':
|
'@ai-sdk/deepinfra':
|
||||||
specifier: ^0.0.4
|
specifier: ^1.0.10
|
||||||
version: 0.0.4(zod@3.25.32)
|
version: 1.0.10(zod@3.25.32)
|
||||||
'@ai-sdk/mistral':
|
'@ai-sdk/mistral':
|
||||||
specifier: ^1.2.8
|
specifier: ^2.0.7
|
||||||
version: 1.2.8(zod@3.25.32)
|
version: 2.0.7(zod@3.25.32)
|
||||||
'@ai-sdk/openai':
|
'@ai-sdk/openai':
|
||||||
specifier: ^1.3.22
|
specifier: ^2.0.16
|
||||||
version: 1.3.22(zod@3.25.32)
|
version: 2.0.16(zod@3.25.32)
|
||||||
'@ai-sdk/openai-compatible':
|
'@ai-sdk/openai-compatible':
|
||||||
specifier: ^0.0.13
|
specifier: ^1.0.10
|
||||||
version: 0.0.13(zod@3.25.32)
|
version: 1.0.10(zod@3.25.32)
|
||||||
'@codemirror/autocomplete':
|
'@codemirror/autocomplete':
|
||||||
specifier: ^6.18.6
|
specifier: ^6.18.6
|
||||||
version: 6.18.6
|
version: 6.18.6
|
||||||
@@ -263,8 +263,11 @@ importers:
|
|||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16
|
version: 0.5.16
|
||||||
ai:
|
ai:
|
||||||
specifier: ^4.3.16
|
specifier: ^5.0.17
|
||||||
version: 4.3.16(react@18.2.0)(zod@3.25.32)
|
version: 5.0.17(zod@3.25.32)
|
||||||
|
ai-sdk-ollama:
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1(zod@3.25.32)
|
||||||
bcrypt:
|
bcrypt:
|
||||||
specifier: 5.1.1
|
specifier: 5.1.1
|
||||||
version: 5.1.1
|
version: 5.1.1
|
||||||
@@ -277,9 +280,6 @@ importers:
|
|||||||
boxen:
|
boxen:
|
||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.1
|
version: 7.1.1
|
||||||
bullmq:
|
|
||||||
specifier: 5.4.2
|
|
||||||
version: 5.4.2
|
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -361,12 +361,12 @@ importers:
|
|||||||
octokit:
|
octokit:
|
||||||
specifier: 3.1.2
|
specifier: 3.1.2
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
ollama-ai-provider:
|
|
||||||
specifier: ^1.2.0
|
|
||||||
version: 1.2.0(zod@3.25.32)
|
|
||||||
otpauth:
|
otpauth:
|
||||||
specifier: ^9.4.0
|
specifier: ^9.4.0
|
||||||
version: 9.4.0
|
version: 9.4.0
|
||||||
|
p-limit:
|
||||||
|
specifier: ^7.1.1
|
||||||
|
version: 7.1.1
|
||||||
pino:
|
pino:
|
||||||
specifier: 9.4.0
|
specifier: 9.4.0
|
||||||
version: 9.4.0
|
version: 9.4.0
|
||||||
@@ -595,26 +595,26 @@ importers:
|
|||||||
packages/server:
|
packages/server:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/anthropic':
|
'@ai-sdk/anthropic':
|
||||||
specifier: ^1.2.12
|
specifier: ^2.0.5
|
||||||
version: 1.2.12(zod@3.25.32)
|
version: 2.0.5(zod@3.25.32)
|
||||||
'@ai-sdk/azure':
|
'@ai-sdk/azure':
|
||||||
specifier: ^1.3.23
|
specifier: ^2.0.16
|
||||||
version: 1.3.23(zod@3.25.32)
|
version: 2.0.16(zod@3.25.32)
|
||||||
'@ai-sdk/cohere':
|
'@ai-sdk/cohere':
|
||||||
specifier: ^1.2.10
|
specifier: ^2.0.4
|
||||||
version: 1.2.10(zod@3.25.32)
|
version: 2.0.4(zod@3.25.32)
|
||||||
'@ai-sdk/deepinfra':
|
'@ai-sdk/deepinfra':
|
||||||
specifier: ^0.0.4
|
specifier: ^1.0.10
|
||||||
version: 0.0.4(zod@3.25.32)
|
version: 1.0.10(zod@3.25.32)
|
||||||
'@ai-sdk/mistral':
|
'@ai-sdk/mistral':
|
||||||
specifier: ^1.2.8
|
specifier: ^2.0.7
|
||||||
version: 1.2.8(zod@3.25.32)
|
version: 2.0.7(zod@3.25.32)
|
||||||
'@ai-sdk/openai':
|
'@ai-sdk/openai':
|
||||||
specifier: ^1.3.22
|
specifier: ^2.0.16
|
||||||
version: 1.3.22(zod@3.25.32)
|
version: 2.0.16(zod@3.25.32)
|
||||||
'@ai-sdk/openai-compatible':
|
'@ai-sdk/openai-compatible':
|
||||||
specifier: ^0.0.13
|
specifier: ^1.0.10
|
||||||
version: 0.0.13(zod@3.25.32)
|
version: 1.0.10(zod@3.25.32)
|
||||||
'@better-auth/utils':
|
'@better-auth/utils':
|
||||||
specifier: 0.2.4
|
specifier: 0.2.4
|
||||||
version: 0.2.4
|
version: 0.2.4
|
||||||
@@ -643,8 +643,11 @@ importers:
|
|||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16
|
version: 0.5.16
|
||||||
ai:
|
ai:
|
||||||
specifier: ^4.3.16
|
specifier: ^5.0.17
|
||||||
version: 4.3.16(react@18.2.0)(zod@3.25.32)
|
version: 5.0.17(zod@3.25.32)
|
||||||
|
ai-sdk-ollama:
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1(zod@3.25.32)
|
||||||
bcrypt:
|
bcrypt:
|
||||||
specifier: 5.1.1
|
specifier: 5.1.1
|
||||||
version: 5.1.1
|
version: 5.1.1
|
||||||
@@ -705,9 +708,6 @@ importers:
|
|||||||
octokit:
|
octokit:
|
||||||
specifier: 3.1.2
|
specifier: 3.1.2
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
ollama-ai-provider:
|
|
||||||
specifier: ^1.2.0
|
|
||||||
version: 1.2.0(zod@3.25.32)
|
|
||||||
otpauth:
|
otpauth:
|
||||||
specifier: ^9.4.0
|
specifier: ^9.4.0
|
||||||
version: 9.4.0
|
version: 9.4.0
|
||||||
@@ -823,87 +823,64 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@ai-sdk/anthropic@1.2.12':
|
'@ai-sdk/anthropic@2.0.5':
|
||||||
resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==}
|
resolution: {integrity: sha512-f0+mD3c5D+ImCWqxFxkT3buGeBg9vFOd2aTaLd1jjIJmWO8O4INLxBC2ETif7z0BfegTIw528B6acBRIeg3jIw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.25.76 || ^4
|
||||||
|
|
||||||
'@ai-sdk/azure@1.3.23':
|
'@ai-sdk/azure@2.0.16':
|
||||||
resolution: {integrity: sha512-vpsaPtU24RBVk/IMM5UylR/N4RtAuL2NZLWc7LJ3tvMTHu6pI46a7w+1qIwR3F6yO9ehWR8qvfLaBefJNFxaVw==}
|
resolution: {integrity: sha512-Q8Fq7aJP9tJOCNicfEUDxU763NkX646zCePayy2Nse+5Gz6ElQEJ9MApIdC4LAyR/IsEuY8G5iY477GAF+iBjg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.25.76 || ^4
|
||||||
|
|
||||||
'@ai-sdk/cohere@1.2.10':
|
'@ai-sdk/cohere@2.0.4':
|
||||||
resolution: {integrity: sha512-OaUwd5xj4bxSO8hdCbX1a5uUlTouU8FcodSuRON6xDSsmjZIvQL4O2G1XzcidxiQVL8JQuA+M0tHZOwGxSL96A==}
|
resolution: {integrity: sha512-GkQsTmhDNVDv8OiwWEGBhkjWYXFzBSbRtmyaM5y4yr9h6rqNwRiwueJG89/aX4mqpGf431mnJCPYxD+eSV6vgQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.25.76 || ^4
|
||||||
|
|
||||||
'@ai-sdk/deepinfra@0.0.4':
|
'@ai-sdk/deepinfra@1.0.10':
|
||||||
resolution: {integrity: sha512-0YZpe7bFWpSZpk2swBhYsKyd2DjxyMa0bJTajJjwec4UGUpuiDyhjXkBgEHY85JjlTubEPby8Ix+FgU0E3ofnw==}
|
resolution: {integrity: sha512-bAvg29LkPI04o9ehRyqwjkKAUlX6W8AXlBTzX/k/gMez9qBbiW6uDpRRXTNYbaEuMONrEHgOQLh2OQ5WYJLDPg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.25.76 || ^4
|
||||||
|
|
||||||
'@ai-sdk/mistral@1.2.8':
|
'@ai-sdk/gateway@1.0.8':
|
||||||
resolution: {integrity: sha512-lv857D9UJqCVxiq2Fcu7mSPTypEHBUqLl1K+lCaP6X/7QAkcaxI36QDONG+tOhGHJOXTsS114u8lrUTaEiGXbg==}
|
resolution: {integrity: sha512-yiHYz0bAHEvhL+fSUBI2dNmyj0LOI8zw5qrYpa4gp1ojPgZq/7T1WXoIWRmVdjQwvT4PzSmRKLtbMPfz+umgfw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.25.76 || ^4
|
||||||
|
|
||||||
'@ai-sdk/openai-compatible@0.0.13':
|
'@ai-sdk/mistral@2.0.7':
|
||||||
resolution: {integrity: sha512-fuauXYKac6PBuf8m52tWcWQW0UCScEkwTaOyr00TcPeK3dd8nPP+ZJzSYE5QhFg7rwi9EH3ahIFqSX1biXhdkQ==}
|
resolution: {integrity: sha512-wWBacWHZHx+WxwjSqb5iIdtK76tmBiEb35ZBLmjODFEdh/dMIaj+g/qMVFT9PY7hHxZ1sT9C58KS671l8nAISw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.25.76 || ^4
|
||||||
|
|
||||||
'@ai-sdk/openai@1.3.22':
|
'@ai-sdk/openai-compatible@1.0.10':
|
||||||
resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==}
|
resolution: {integrity: sha512-NInkII/DOvrMvO/mS0BxGUGi3r+wuXxbdzAh9k2gFGT+xVoP6OePikhogQu8qZuti8loUZJGYq3GJ/DCftOzhQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.25.76 || ^4
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@2.0.5':
|
'@ai-sdk/openai@2.0.16':
|
||||||
resolution: {integrity: sha512-2M7vLhYN0ThGjNlzow7oO/lsL+DyMxvGMIYmVQvEYaCWhDzxH5dOp78VNjJIVwHzVLMbBDigX3rJuzAs853idw==}
|
resolution: {integrity: sha512-Boe715q4SkSJedFfAtbP0yuo8DmF9iYElAaDH2g4YgqJqqkskIJJx4hlCYGMMk1eMesRrB2NqZvtOeyTZ/u4fA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.25.76 || ^4
|
||||||
peerDependenciesMeta:
|
|
||||||
zod:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@2.2.8':
|
'@ai-sdk/provider-utils@3.0.4':
|
||||||
resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
|
resolution: {integrity: sha512-/3Z6lfUp8r+ewFd9yzHkCmPlMOJUXup2Sx3aoUyrdXLhOmAfHRl6Z4lDbIdV0uvw/QYoBcVLJnvXN7ncYeS3uQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.23.8
|
zod: ^3.25.76 || ^4
|
||||||
|
|
||||||
'@ai-sdk/provider@1.0.3':
|
'@ai-sdk/provider@2.0.0':
|
||||||
resolution: {integrity: sha512-WiuJEpHTrltOIzv3x2wx4gwksAHW0h6nK3SoDzjqCOJLu/2OJ1yASESTIX+f07ChFykHElVoP80Ol/fe9dw6tQ==}
|
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
|
||||||
engines: {node: '>=18'}
|
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':
|
'@alloc/quick-lru@5.2.0':
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3716,6 +3693,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.0.0':
|
||||||
|
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||||
|
|
||||||
'@stepperize/react@4.0.1':
|
'@stepperize/react@4.0.1':
|
||||||
resolution: {integrity: sha512-LAOcfi3d2mM/Jn740Xy35qsuTwmoLIuitvWZTZRURYeGsc7a6sIKAkk3+L1joZGkLFvf5q4I6V7LxWWfB5hDvg==}
|
resolution: {integrity: sha512-LAOcfi3d2mM/Jn740Xy35qsuTwmoLIuitvWZTZRURYeGsc7a6sIKAkk3+L1joZGkLFvf5q4I6V7LxWWfB5hDvg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3958,9 +3938,6 @@ packages:
|
|||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
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':
|
'@types/docker-modem@3.0.6':
|
||||||
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
|
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
|
||||||
|
|
||||||
@@ -4201,15 +4178,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
|
resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
ai@4.3.16:
|
ai-sdk-ollama@0.5.1:
|
||||||
resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==}
|
resolution: {integrity: sha512-VPE2yagxtowepiPROaP/7YUpiZxqZO4SDHHM2Tdw0wyatPCggct142eQI34UO2/PPJ1iXKpraWI5+n0/pcz69Q==}
|
||||||
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
|
ai@5.0.17:
|
||||||
|
resolution: {integrity: sha512-DLZikqZZJdwSkRhFikw6Mt7pUmPZ7Ue38TjdOcw2U6iZtBbuiyWGIhHyJXlUpLcZrtBE5yqPTozyZri1lRjduw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18 || ^19 || ^19.0.0-rc
|
zod: ^3.25.76 || ^4
|
||||||
zod: ^3.23.8
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ajv@8.17.1:
|
ajv@8.17.1:
|
||||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||||
@@ -4846,9 +4823,6 @@ packages:
|
|||||||
didyoumean@1.2.2:
|
didyoumean@1.2.2:
|
||||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
|
|
||||||
diff-match-patch@1.0.5:
|
|
||||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
|
||||||
|
|
||||||
diff-sequences@29.6.3:
|
diff-sequences@29.6.3:
|
||||||
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
|
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
@@ -5135,9 +5109,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
|
|
||||||
eventsource-parser@3.0.2:
|
eventsource-parser@3.0.5:
|
||||||
resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==}
|
resolution: {integrity: sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
execa@8.0.1:
|
execa@8.0.1:
|
||||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||||
@@ -5730,11 +5704,6 @@ packages:
|
|||||||
json-stringify-safe@5.0.1:
|
json-stringify-safe@5.0.1:
|
||||||
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
|
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:
|
jsonparse@1.3.1:
|
||||||
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
||||||
engines: {'0': node >= 0.2.0}
|
engines: {'0': node >= 0.2.0}
|
||||||
@@ -6395,14 +6364,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==}
|
resolution: {integrity: sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
ollama-ai-provider@1.2.0:
|
ollama@0.5.17:
|
||||||
resolution: {integrity: sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww==}
|
resolution: {integrity: sha512-q5LmPtk6GLFouS+3aURIVl+qcAOPC4+Msmx7uBb3pd+fxI55WnGjmLZ0yijI/CYy79x0QPGx3BwC3u5zv9fBvQ==}
|
||||||
engines: {node: '>=18'}
|
|
||||||
peerDependencies:
|
|
||||||
zod: ^3.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
zod:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2:
|
on-exit-leak-free@2.1.2:
|
||||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||||
@@ -6449,6 +6412,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
|
resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
p-limit@7.1.1:
|
||||||
|
resolution: {integrity: sha512-i8PyM2JnsNChVSYWLr2BAjNoLi0BAYC+wecOnZnVV+YSNJkzP7cWmvI34dk0WArWfH9KwBHNoZI3P3MppImlIA==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
p-locate@4.1.0:
|
p-locate@4.1.0:
|
||||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6485,9 +6452,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
partial-json@0.1.7:
|
|
||||||
resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==}
|
|
||||||
|
|
||||||
path-exists@4.0.0:
|
path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -7349,11 +7313,6 @@ packages:
|
|||||||
react: '>=16.8.0 <19'
|
react: '>=16.8.0 <19'
|
||||||
react-dom: '>=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:
|
symbol-observable@1.2.0:
|
||||||
resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
|
resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -7414,10 +7373,6 @@ packages:
|
|||||||
thread-stream@3.1.0:
|
thread-stream@3.1.0:
|
||||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||||
|
|
||||||
throttleit@2.1.0:
|
|
||||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
through@2.3.8:
|
through@2.3.8:
|
||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
|
|
||||||
@@ -7735,6 +7690,9 @@ packages:
|
|||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
|
whatwg-fetch@3.6.20:
|
||||||
|
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
@@ -7873,91 +7831,68 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ai-sdk/anthropic@1.2.12(zod@3.25.32)':
|
'@ai-sdk/anthropic@2.0.5(zod@3.25.32)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
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:
|
dependencies:
|
||||||
'@ai-sdk/openai': 1.3.22(zod@3.25.32)
|
'@ai-sdk/openai': 2.0.16(zod@3.25.32)
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
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:
|
dependencies:
|
||||||
'@ai-sdk/openai-compatible': 0.0.13(zod@3.25.32)
|
'@ai-sdk/openai-compatible': 1.0.10(zod@3.25.32)
|
||||||
'@ai-sdk/provider': 1.0.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 2.0.5(zod@3.25.32)
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.0.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 2.0.5(zod@3.25.32)
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.0.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
eventsource-parser: 3.0.2
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
nanoid: 3.3.11
|
|
||||||
secure-json-parse: 2.7.0
|
|
||||||
optionalDependencies:
|
|
||||||
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
nanoid: 3.3.11
|
'@standard-schema/spec': 1.0.0
|
||||||
secure-json-parse: 2.7.0
|
eventsource-parser: 3.0.5
|
||||||
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)
|
|
||||||
zod: 3.25.32
|
zod: 3.25.32
|
||||||
zod-to-json-schema: 3.24.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': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
'@babel/code-frame@7.27.1':
|
'@babel/code-frame@7.27.1':
|
||||||
@@ -10767,6 +10702,8 @@ snapshots:
|
|||||||
|
|
||||||
'@sindresorhus/is@5.6.0': {}
|
'@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)':
|
'@stepperize/react@4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
@@ -11257,8 +11194,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
|
|
||||||
'@types/diff-match-patch@1.0.36': {}
|
|
||||||
|
|
||||||
'@types/docker-modem@3.0.6':
|
'@types/docker-modem@3.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.51
|
'@types/node': 20.17.51
|
||||||
@@ -11347,7 +11282,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/pg@8.6.1':
|
'@types/pg@8.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.51
|
'@types/node': 18.19.104
|
||||||
pg-protocol: 1.10.3
|
pg-protocol: 1.10.3
|
||||||
pg-types: 2.2.0
|
pg-types: 2.2.0
|
||||||
|
|
||||||
@@ -11534,17 +11469,22 @@ snapshots:
|
|||||||
clean-stack: 4.2.0
|
clean-stack: 4.2.0
|
||||||
indent-string: 5.0.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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 2.0.0
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
|
'@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
|
||||||
'@ai-sdk/react': 1.2.12(react@18.2.0)(zod@3.25.32)
|
ai: 5.0.17(zod@3.25.32)
|
||||||
'@ai-sdk/ui-utils': 1.2.11(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
|
'@opentelemetry/api': 1.9.0
|
||||||
jsondiffpatch: 0.6.0
|
|
||||||
zod: 3.25.32
|
zod: 3.25.32
|
||||||
optionalDependencies:
|
|
||||||
react: 18.2.0
|
|
||||||
|
|
||||||
ajv@8.17.1:
|
ajv@8.17.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -12181,8 +12121,6 @@ snapshots:
|
|||||||
|
|
||||||
didyoumean@1.2.2: {}
|
didyoumean@1.2.2: {}
|
||||||
|
|
||||||
diff-match-patch@1.0.5: {}
|
|
||||||
|
|
||||||
diff-sequences@29.6.3: {}
|
diff-sequences@29.6.3: {}
|
||||||
|
|
||||||
dijkstrajs@1.0.3: {}
|
dijkstrajs@1.0.3: {}
|
||||||
@@ -12469,7 +12407,7 @@ snapshots:
|
|||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
eventsource-parser@3.0.2: {}
|
eventsource-parser@3.0.5: {}
|
||||||
|
|
||||||
execa@8.0.1:
|
execa@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13088,12 +13026,6 @@ snapshots:
|
|||||||
|
|
||||||
json-stringify-safe@5.0.1: {}
|
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: {}
|
jsonparse@1.3.1: {}
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
jsonwebtoken@9.0.2:
|
||||||
@@ -13895,13 +13827,9 @@ snapshots:
|
|||||||
'@octokit/request-error': 5.1.1
|
'@octokit/request-error': 5.1.1
|
||||||
'@octokit/types': 12.6.0
|
'@octokit/types': 12.6.0
|
||||||
|
|
||||||
ollama-ai-provider@1.2.0(zod@3.25.32):
|
ollama@0.5.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
whatwg-fetch: 3.6.20
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
|
|
||||||
partial-json: 0.1.7
|
|
||||||
optionalDependencies:
|
|
||||||
zod: 3.25.32
|
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
on-exit-leak-free@2.1.2: {}
|
||||||
|
|
||||||
@@ -13945,6 +13873,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yocto-queue: 1.2.1
|
yocto-queue: 1.2.1
|
||||||
|
|
||||||
|
p-limit@7.1.1:
|
||||||
|
dependencies:
|
||||||
|
yocto-queue: 1.2.1
|
||||||
|
|
||||||
p-locate@4.1.0:
|
p-locate@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 2.3.0
|
p-limit: 2.3.0
|
||||||
@@ -13994,8 +13926,6 @@ snapshots:
|
|||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
partial-json@0.1.7: {}
|
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-exists@5.0.0: {}
|
path-exists@5.0.0: {}
|
||||||
@@ -14957,12 +14887,6 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- debug
|
- 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: {}
|
symbol-observable@1.2.0: {}
|
||||||
|
|
||||||
tailwind-merge@2.6.0: {}
|
tailwind-merge@2.6.0: {}
|
||||||
@@ -15054,8 +14978,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
real-require: 0.2.0
|
real-require: 0.2.0
|
||||||
|
|
||||||
throttleit@2.1.0: {}
|
|
||||||
|
|
||||||
through@2.3.8: {}
|
through@2.3.8: {}
|
||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
@@ -15359,6 +15281,8 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
|
whatwg-fetch@3.6.20: {}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user