Merge branch 'canary' into feat/github-triggerType

This commit is contained in:
Mauricio Siu 2025-04-26 21:09:23 -06:00
commit 120646c77b
73 changed files with 12118 additions and 456 deletions

View File

@ -52,7 +52,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.9.0 We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
```bash ```bash
git clone https://github.com/dokploy/dokploy.git git clone https://github.com/dokploy/dokploy.git
@ -87,6 +87,8 @@ pnpm run dokploy:dev
Go to http://localhost:3000 to see the development server Go to http://localhost:3000 to see the development server
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
## Build ## Build
```bash ```bash
@ -145,11 +147,9 @@ curl -sSL https://railpack.com/install.sh | sh
```bash ```bash
# Install Buildpacks # Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
``` ```
## Pull Request ## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release. - The `main` branch is the source of truth and should always reflect the latest stable release.
@ -167,7 +167,6 @@ Thank you for your contribution!
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file. To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
### Recommendations ### Recommendations
- Use the same name of the folder as the id of the template. - Use the same name of the folder as the id of the template.

View File

@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx # Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash # | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.29.1 ARG NIXPACKS_VERSION=1.35.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \ && chmod +x install.sh \
&& ./install.sh \ && ./install.sh \

View File

@ -34,6 +34,7 @@ const baseApp: ApplicationNested = {
giteaRepository: "", giteaRepository: "",
cleanCache: false, cleanCache: false,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
applicationStatus: "done", applicationStatus: "done",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,

View File

@ -51,6 +51,35 @@ describe("processTemplate", () => {
expect(result.domains).toHaveLength(0); expect(result.domains).toHaveLength(0);
expect(result.mounts).toHaveLength(0); expect(result.mounts).toHaveLength(0);
}); });
it("should allow creation of real jwt secret", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {
jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ",
anon_payload: JSON.stringify({
role: "tester",
iss: "dockploy",
iat: "${timestamps:2025-01-01T00:00:00Z}",
exp: "${timestamps:2030-01-01T00:00:00Z}",
}),
anon_key: "${jwt:jwt_secret:anon_payload}",
},
config: {
domains: [],
env: {
ANON_KEY: "${anon_key}",
},
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(1);
expect(result.envs).toContain(
"ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY",
);
expect(result.mounts).toHaveLength(0);
expect(result.domains).toHaveLength(0);
});
}); });
describe("domains processing", () => { describe("domains processing", () => {

View File

@ -0,0 +1,232 @@
import type { Schema } from "@dokploy/server/templates";
import { processValue } from "@dokploy/server/templates/processors";
import { describe, expect, it } from "vitest";
describe("helpers functions", () => {
// Mock schema for testing
const mockSchema: Schema = {
projectName: "test",
serverIp: "127.0.0.1",
};
// some helpers to test jwt
type JWTParts = [string, string, string];
const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
const jwtBase64Decode = (str: string) => {
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8");
return JSON.parse(decoded);
};
const jwtCheckHeader = (jwtHeader: string) => {
const decodedHeader = jwtBase64Decode(jwtHeader);
expect(decodedHeader).toHaveProperty("alg");
expect(decodedHeader).toHaveProperty("typ");
expect(decodedHeader.alg).toEqual("HS256");
expect(decodedHeader.typ).toEqual("JWT");
};
describe("${domain}", () => {
it("should generate a random domain", () => {
const domain = processValue("${domain}", {}, mockSchema);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect(
domain.endsWith(
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
).toBeTruthy();
});
});
describe("${base64}", () => {
it("should generate a base64 string", () => {
const base64 = processValue("${base64}", {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
});
it.each([
[4, 8],
[8, 12],
[16, 24],
[32, 44],
[64, 88],
[128, 172],
])(
"should generate a base64 string from parameter %d bytes length",
(length, finalLength) => {
const base64 = processValue(`\${base64:${length}}`, {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
expect(base64.length).toBe(finalLength);
},
);
});
describe("${password}", () => {
it("should generate a password string", () => {
const password = processValue("${password}", {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a password string respecting parameter %d length",
(length) => {
const password = processValue(`\${password:${length}}`, {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
expect(password.length).toBe(length);
},
);
});
describe("${hash}", () => {
it("should generate a hash string", () => {
const hash = processValue("${hash}", {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a hash string respecting parameter %d length",
(length) => {
const hash = processValue(`\${hash:${length}}`, {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
expect(hash.length).toBe(length);
},
);
});
describe("${uuid}", () => {
it("should generate a UUID string", () => {
const uuid = processValue("${uuid}", {}, mockSchema);
expect(uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
});
});
describe("${timestamp}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestamp}", {}, mockSchema);
const nowLength = Math.floor(Date.now()).toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
});
describe("${timestampms}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestampms}", {}, mockSchema);
const nowLength = Date.now().toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
it("should generate a timestamp string in milliseconds from parameter", () => {
const timestamp = processValue(
"${timestampms:2025-01-01}",
{},
mockSchema,
);
expect(timestamp).toEqual("1735689600000");
});
});
describe("${timestamps}", () => {
it("should generate a timestamp string in seconds", () => {
const timestamps = processValue("${timestamps}", {}, mockSchema);
const nowLength = Math.floor(Date.now() / 1000).toString().length;
expect(timestamps).toMatch(/^\d+$/);
expect(timestamps.length).toBe(nowLength);
});
it("should generate a timestamp string in seconds from parameter", () => {
const timestamps = processValue(
"${timestamps:2025-01-01}",
{},
mockSchema,
);
expect(timestamps).toEqual("1735689600");
});
});
describe("${randomPort}", () => {
it("should generate a random port string", () => {
const randomPort = processValue("${randomPort}", {}, mockSchema);
expect(randomPort).toMatch(/^\d+$/);
expect(Number(randomPort)).toBeLessThan(65536);
});
});
describe("${username}", () => {
it("should generate a username string", () => {
const username = processValue("${username}", {}, mockSchema);
expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/);
});
});
describe("${email}", () => {
it("should generate an email string", () => {
const email = processValue("${email}", {}, mockSchema);
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
});
});
describe("${jwt}", () => {
it("should generate a JWT string", () => {
const jwt = processValue("${jwt}", {}, mockSchema);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
it.each([6, 8, 12, 16, 32])(
"should generate a random hex string from parameter %d byte length",
(length) => {
const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema);
expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/);
expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length
expect(jwt.length).toBeLessThanOrEqual(length * 2);
},
);
});
describe("${jwt:secret}", () => {
it("should generate a JWT string respecting parameter secret from variable", () => {
const jwt = processValue(
"${jwt:secret}",
{ secret: "mysecret" },
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
});
describe("${jwt:secret:payload}", () => {
it("should generate a JWT string respecting parameters secret and payload from variables", () => {
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
const expiry = iat + 3600;
const jwt = processValue(
"${jwt:secret:payload}",
{
secret: "mysecret",
payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`,
},
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
jwtCheckHeader(parts[0]);
const decodedPayload = jwtBase64Decode(parts[1]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload.iat).toEqual(iat);
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload.iss).toEqual("test-issuer");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.exp).toEqual(expiry);
expect(decodedPayload).toHaveProperty("customprop");
expect(decodedPayload.customprop).toEqual("customvalue");
expect(jwt).toEqual(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
);
});
});
});

View File

@ -16,6 +16,7 @@ const baseApp: ApplicationNested = {
applicationStatus: "done", applicationStatus: "done",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
enableSubmodules: false,
serverId: "", serverId: "",
branch: null, branch: null,
dockerBuildStage: "", dockerBuildStage: "",

View File

@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(),
}); });
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>; type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@ -84,6 +86,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: "", bitbucketId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(BitbucketProviderSchema), resolver: zodResolver(BitbucketProviderSchema),
}); });
@ -130,6 +133,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
buildPath: data.bitbucketBuildPath || "/", buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "", bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -143,6 +147,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: data.bitbucketId, bitbucketId: data.bitbucketId,
applicationId, applicationId,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@ -467,6 +472,21 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@ -23,6 +23,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react"; import { KeyRoundIcon, LockIcon, X } from "lucide-react";
@ -44,6 +45,7 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"), branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitProvider = z.infer<typeof GitProviderSchema>; type GitProvider = z.infer<typeof GitProviderSchema>;
@ -67,6 +69,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
repositoryURL: "", repositoryURL: "",
sshKey: undefined, sshKey: undefined,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitProviderSchema), resolver: zodResolver(GitProviderSchema),
}); });
@ -79,6 +82,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
buildPath: data.customGitBuildPath || "/", buildPath: data.customGitBuildPath || "/",
repositoryURL: data.customGitUrl || "", repositoryURL: data.customGitUrl || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -91,6 +95,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey, customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
applicationId, applicationId,
watchPaths: values.watchPaths || [], watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Git Provider Saved"); toast.success("Git Provider Saved");
@ -294,6 +299,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">

View File

@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -74,6 +75,7 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]), watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(),
}); });
type GiteaProvider = z.infer<typeof GiteaProviderSchema>; type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@ -99,6 +101,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: "", giteaId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GiteaProviderSchema), resolver: zodResolver(GiteaProviderSchema),
}); });
@ -152,6 +155,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
buildPath: data.giteaBuildPath || "/", buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "", giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -165,6 +169,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: data.giteaId, giteaId: data.giteaId,
applicationId, applicationId,
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules || false,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provider Saved"); toast.success("Service Provider Saved");
@ -498,6 +503,21 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@ -30,6 +30,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -58,6 +59,7 @@ const GithubProviderSchema = z.object({
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"), triggerType: z.enum(["push", "tag"]).default("push"),
enableSubmodules: z.boolean().default(false),
}); });
type GithubProvider = z.infer<typeof GithubProviderSchema>; type GithubProvider = z.infer<typeof GithubProviderSchema>;
@ -82,6 +84,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
}, },
githubId: "", githubId: "",
branch: "", branch: "",
triggerType: "push",
enableSubmodules: false,
}, },
resolver: zodResolver(GithubProviderSchema), resolver: zodResolver(GithubProviderSchema),
}); });
@ -126,6 +130,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
githubId: data.githubId || "", githubId: data.githubId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
triggerType: data.triggerType || "push", triggerType: data.triggerType || "push",
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -140,6 +145,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
githubId: data.githubId, githubId: data.githubId,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
triggerType: data.triggerType, triggerType: data.triggerType,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@ -501,6 +507,22 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitlabProvider = z.infer<typeof GitlabProviderSchema>; type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@ -86,6 +88,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
}, },
gitlabId: "", gitlabId: "",
branch: "", branch: "",
enableSubmodules: false,
}, },
resolver: zodResolver(GitlabProviderSchema), resolver: zodResolver(GitlabProviderSchema),
}); });
@ -135,6 +138,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
buildPath: data.gitlabBuildPath || "/", buildPath: data.gitlabBuildPath || "/",
gitlabId: data.gitlabId || "", gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -150,6 +154,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
gitlabProjectId: data.repository.id, gitlabProjectId: data.repository.id,
gitlabPathNamespace: data.repository.gitlabPathNamespace, gitlabPathNamespace: data.repository.gitlabPathNamespace,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@ -483,6 +488,21 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState); setSab(e as TabState);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden"> <TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger <TabsTrigger
value="github" value="github"

View File

@ -298,7 +298,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
}) })
.then(() => { .then(() => {
refetch(); refetch();
toast.success("Preview deployments enabled"); toast.success(
checked
? "Preview deployments enabled"
: "Preview deployments disabled",
);
}) })
.catch((error) => { .catch((error) => {
toast.error(error.message); toast.error(error.message);

View File

@ -79,6 +79,22 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
toast.error("Error updating the Compose config"); toast.error("Error updating the Compose config");
}); });
}; };
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [form, onSubmit, isLoading]);
return ( return (
<> <>
<div className="w-full flex flex-col gap-4 "> <div className="w-full flex flex-col gap-4 ">

View File

@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>; type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@ -84,6 +86,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
bitbucketId: "", bitbucketId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(BitbucketProviderSchema), resolver: zodResolver(BitbucketProviderSchema),
}); });
@ -130,6 +133,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
bitbucketId: data.bitbucketId || "", bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -145,6 +149,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
sourceType: "bitbucket", sourceType: "bitbucket",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@ -469,6 +474,21 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@ -19,6 +19,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -43,6 +44,7 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"), branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitProvider = z.infer<typeof GitProviderSchema>; type GitProvider = z.infer<typeof GitProviderSchema>;
@ -65,6 +67,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: "./docker-compose.yml", composePath: "./docker-compose.yml",
sshKey: undefined, sshKey: undefined,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitProviderSchema), resolver: zodResolver(GitProviderSchema),
}); });
@ -77,6 +80,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
repositoryURL: data.customGitUrl || "", repositoryURL: data.customGitUrl || "",
composePath: data.composePath, composePath: data.composePath,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -91,6 +95,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: values.composePath, composePath: values.composePath,
composeStatus: "idle", composeStatus: "idle",
watchPaths: values.watchPaths || [], watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Git Provider Saved"); toast.success("Git Provider Saved");
@ -295,6 +300,21 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">

View File

@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -59,6 +60,7 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GiteaProvider = z.infer<typeof GiteaProviderSchema>; type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@ -83,6 +85,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
giteaId: "", giteaId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GiteaProviderSchema), resolver: zodResolver(GiteaProviderSchema),
}); });
@ -136,6 +139,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath || "./docker-compose.yml", composePath: data.composePath || "./docker-compose.yml",
giteaId: data.giteaId || "", giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -151,6 +155,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
sourceType: "gitea", sourceType: "gitea",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
} as any) } as any)
.then(async () => { .then(async () => {
toast.success("Service Provider Saved"); toast.success("Service Provider Saved");
@ -469,6 +474,21 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">

View File

@ -30,6 +30,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GithubProvider = z.infer<typeof GithubProviderSchema>; type GithubProvider = z.infer<typeof GithubProviderSchema>;
@ -82,6 +84,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
githubId: "", githubId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GithubProviderSchema), resolver: zodResolver(GithubProviderSchema),
}); });
@ -125,6 +128,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
githubId: data.githubId || "", githubId: data.githubId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -140,6 +144,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
sourceType: "github", sourceType: "github",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@ -460,6 +465,21 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitlabProvider = z.infer<typeof GitlabProviderSchema>; type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@ -87,6 +89,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
gitlabId: "", gitlabId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitlabProviderSchema), resolver: zodResolver(GitlabProviderSchema),
}); });
@ -136,6 +139,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
gitlabId: data.gitlabId || "", gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -153,6 +157,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
sourceType: "gitlab", sourceType: "gitlab",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@ -485,6 +490,21 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@ -84,6 +84,7 @@ export const RestoreBackup = ({
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const { data: destinations = [] } = api.destination.all.useQuery(); const { data: destinations = [] } = api.destination.all.useQuery();
@ -99,13 +100,18 @@ export const RestoreBackup = ({
const destionationId = form.watch("destinationId"); const destionationId = form.watch("destinationId");
const debouncedSetSearch = debounce((value: string) => { const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 150);
const handleSearchChange = (value: string) => {
setSearch(value); setSearch(value);
}, 300); debouncedSetSearch(value);
};
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery( const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
{ {
destinationId: destionationId, destinationId: destionationId,
search, search: debouncedSearchTerm,
serverId: serverId ?? "", serverId: serverId ?? "",
}, },
{ {
@ -284,7 +290,8 @@ export const RestoreBackup = ({
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search backup files..." placeholder="Search backup files..."
onValueChange={debouncedSetSearch} value={search}
onValueChange={handleSearchChange}
className="h-9" className="h-9"
/> />
{isLoading ? ( {isLoading ? (
@ -308,6 +315,8 @@ export const RestoreBackup = ({
key={file} key={file}
onSelect={() => { onSelect={() => {
form.setValue("backupFile", file); form.setValue("backupFile", file);
setSearch(file);
setDebouncedSearchTerm(file);
}} }}
> >
<div className="flex w-full justify-between"> <div className="flex w-full justify-between">

View File

@ -55,7 +55,7 @@ export const AiForm = () => {
key={config.aiId} key={config.aiId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div> <div>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{config.name} {config.name}

View File

@ -70,7 +70,7 @@ export const ShowCertificates = () => {
key={certificate.certificateId} key={certificate.certificateId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 flex-col"> <div className="flex gap-2 flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@ -54,7 +54,7 @@ export const ShowRegistry = () => {
key={registry.registryId} key={registry.registryId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 flex-col"> <div className="flex gap-2 flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@ -55,7 +55,7 @@ export const ShowDestinations = () => {
key={destination.destinationId} key={destination.destinationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-sm"> <span className="text-sm">
{index + 1}. {destination.name} {index + 1}. {destination.name}

View File

@ -248,7 +248,9 @@ export const AddGitlabProvider = () => {
name="groupName" name="groupName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Group Name (Optional)</FormLabel> <FormLabel>
Group Name (Optional, Comma-Separated List)
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="For organization/group access use the slugish name of the group eg: my-org" placeholder="For organization/group access use the slugish name of the group eg: my-org"

View File

@ -156,7 +156,9 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
name="groupName" name="groupName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Group Name (Optional)</FormLabel> <FormLabel>
Group Name (Optional, Comma-Separated List)
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="For organization/group access use the slugish name of the group eg: my-org" placeholder="For organization/group access use the slugish name of the group eg: my-org"

View File

@ -61,7 +61,7 @@ export const ShowNotifications = () => {
key={notification.notificationId} key={notification.notificationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<span className="text-sm flex flex-row items-center gap-4"> <span className="text-sm flex flex-row items-center gap-4">
{notification.notificationType === "slack" && ( {notification.notificationType === "slack" && (
<div className="flex items-center justify-center rounded-lg"> <div className="flex items-center justify-center rounded-lg">

View File

@ -36,6 +36,7 @@ const PasswordSchema = z.object({
password: z.string().min(8, { password: z.string().min(8, {
message: "Password is required", message: "Password is required",
}), }),
issuer: z.string().optional(),
}); });
const PinSchema = z.object({ const PinSchema = z.object({
@ -60,12 +61,86 @@ export const Enable2FA = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [step, setStep] = useState<"password" | "verify">("password"); const [step, setStep] = useState<"password" | "verify">("password");
const [isPasswordLoading, setIsPasswordLoading] = useState(false); const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const [otpValue, setOtpValue] = useState("");
const handleVerifySubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await authClient.twoFactor.verifyTotp({
code: otpValue,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.user.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
toast.error(errorMessage);
} else {
toast.error("Error verifying 2FA code", {
description: error instanceof Error ? error.message : "Unknown error",
});
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
});
useEffect(() => {
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
setOtpValue("");
passwordForm.reset({
password: "",
issuer: "",
});
}
}, [isDialogOpen, passwordForm]);
useEffect(() => {
if (step === "verify") {
setOtpValue("");
}
}, [step]);
const handlePasswordSubmit = async (formData: PasswordForm) => { const handlePasswordSubmit = async (formData: PasswordForm) => {
setIsPasswordLoading(true); setIsPasswordLoading(true);
try { try {
const { data: enableData, error } = await authClient.twoFactor.enable({ const { data: enableData, error } = await authClient.twoFactor.enable({
password: formData.password, password: formData.password,
issuer: formData.issuer,
}); });
if (!enableData) { if (!enableData) {
@ -103,75 +178,6 @@ export const Enable2FA = () => {
} }
}; };
const handleVerifySubmit = async (formData: PinForm) => {
try {
const result = await authClient.twoFactor.verifyTotp({
code: formData.pin,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
pinForm.setError("pin", {
message: "Invalid code. Please try again.",
});
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.user.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
pinForm.setError("pin", {
message: errorMessage,
});
toast.error(errorMessage);
} else {
pinForm.setError("pin", {
message: "Error verifying code",
});
toast.error("Error verifying 2FA code");
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
});
useEffect(() => {
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
passwordForm.reset();
pinForm.reset();
}
}, [isDialogOpen, passwordForm, pinForm]);
return ( return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -217,6 +223,27 @@ export const Enable2FA = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={passwordForm.control}
name="issuer"
render={({ field }) => (
<FormItem>
<FormLabel>Issuer</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Enter your issuer"
{...field}
/>
</FormControl>
<FormDescription>
Use a custom issuer to identify the service you're
authenticating with.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
@ -228,11 +255,7 @@ export const Enable2FA = () => {
</Form> </Form>
) : ( ) : (
<Form {...pinForm}> <Form {...pinForm}>
<form <form onSubmit={handleVerifySubmit} className="space-y-6">
id="pin-form"
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
className="space-y-6"
>
<div className="flex flex-col gap-6 justify-center items-center"> <div className="flex flex-col gap-6 justify-center items-center">
{data?.qrCodeUrl ? ( {data?.qrCodeUrl ? (
<> <>
@ -284,36 +307,33 @@ export const Enable2FA = () => {
)} )}
</div> </div>
<FormField <div className="flex flex-col justify-center items-center">
control={pinForm.control} <FormLabel>Verification Code</FormLabel>
name="pin" <InputOTP
render={({ field }) => ( maxLength={6}
<FormItem className="flex flex-col justify-center items-center"> value={otpValue}
<FormLabel>Verification Code</FormLabel> onChange={setOtpValue}
<FormControl> autoComplete="off"
<InputOTP maxLength={6} {...field}> >
<InputOTPGroup> <InputOTPGroup>
<InputOTPSlot index={0} /> <InputOTPSlot index={0} />
<InputOTPSlot index={1} /> <InputOTPSlot index={1} />
<InputOTPSlot index={2} /> <InputOTPSlot index={2} />
<InputOTPSlot index={3} /> <InputOTPSlot index={3} />
<InputOTPSlot index={4} /> <InputOTPSlot index={4} />
<InputOTPSlot index={5} /> <InputOTPSlot index={5} />
</InputOTPGroup> </InputOTPGroup>
</InputOTP> </InputOTP>
</FormControl> <FormDescription>
<FormDescription> Enter the 6-digit code from your authenticator app
Enter the 6-digit code from your authenticator app </FormDescription>
</FormDescription> </div>
<FormMessage />
</FormItem>
)}
/>
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
isLoading={isPasswordLoading} isLoading={isPasswordLoading}
disabled={otpValue.length !== 6}
> >
Enable 2FA Enable 2FA
</Button> </Button>

View File

@ -56,6 +56,7 @@ const randomImages = [
export const ProfileForm = () => { export const ProfileForm = () => {
const _utils = api.useUtils(); const _utils = api.useUtils();
const { data, refetch, isLoading } = api.user.get.useQuery(); const { data, refetch, isLoading } = api.user.get.useQuery();
const { const {
mutateAsync, mutateAsync,
isLoading: isUpdating, isLoading: isUpdating,
@ -84,12 +85,17 @@ export const ProfileForm = () => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset(
email: data?.user?.email || "", {
password: "", email: data?.user?.email || "",
image: data?.user?.image || "", password: form.getValues("password") || "",
currentPassword: "", image: data?.user?.image || "",
}); currentPassword: form.getValues("currentPassword") || "",
},
{
keepValues: true,
},
);
if (data.user.email) { if (data.user.email) {
generateSHA256Hash(data.user.email).then((hash) => { generateSHA256Hash(data.user.email).then((hash) => {
@ -97,8 +103,7 @@ export const ProfileForm = () => {
}); });
} }
} }
form.reset(); }, [form, data]);
}, [form, form.reset, data]);
const onSubmit = async (values: Profile) => { const onSubmit = async (values: Profile) => {
await mutateAsync({ await mutateAsync({
@ -110,7 +115,12 @@ export const ProfileForm = () => {
.then(async () => { .then(async () => {
await refetch(); await refetch();
toast.success("Profile Updated"); toast.success("Profile Updated");
form.reset(); form.reset({
email: values.email,
password: "",
image: values.image,
currentPassword: "",
});
}) })
.catch(() => { .catch(() => {
toast.error("Error updating the profile"); toast.error("Error updating the profile");

View File

@ -56,7 +56,7 @@ export const ShowDestinations = () => {
key={sshKey.sshKeyId} key={sshKey.sshKeyId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@ -133,17 +133,6 @@ export const UserNav = () => {
Servers Servers
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</> </>
)} )}
</DropdownMenuGroup> </DropdownMenuGroup>

View File

@ -26,15 +26,20 @@ const dockerComposeServices = [
{ label: "secrets", type: "keyword", info: "Define secrets" }, { label: "secrets", type: "keyword", info: "Define secrets" },
].map((opt) => ({ ].map((opt) => ({
...opt, ...opt,
apply: (view: EditorView, completion: Completion) => { apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
const insert = `${completion.label}:`; const insert = `${completion.label}:`;
view.dispatch({ view.dispatch({
changes: { changes: {
from: view.state.selection.main.from, from,
to: view.state.selection.main.to, to,
insert, insert,
}, },
selection: { anchor: view.state.selection.main.from + insert.length }, selection: { anchor: from + insert.length },
}); });
}, },
})); }));
@ -74,15 +79,20 @@ const dockerComposeServiceOptions = [
{ label: "networks", type: "keyword", info: "Networks to join" }, { label: "networks", type: "keyword", info: "Networks to join" },
].map((opt) => ({ ].map((opt) => ({
...opt, ...opt,
apply: (view: EditorView, completion: Completion) => { apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
const insert = `${completion.label}: `; const insert = `${completion.label}: `;
view.dispatch({ view.dispatch({
changes: { changes: {
from: view.state.selection.main.from, from,
to: view.state.selection.main.to, to,
insert, insert,
}, },
selection: { anchor: view.state.selection.main.from + insert.length }, selection: { anchor: from + insert.length },
}); });
}, },
})); }));
@ -99,6 +109,7 @@ function dockerComposeComplete(
const line = context.state.doc.lineAt(context.pos); const line = context.state.doc.lineAt(context.pos);
const indentation = /^\s*/.exec(line.text)?.[0].length || 0; const indentation = /^\s*/.exec(line.text)?.[0].length || 0;
// If we're at the root level
if (indentation === 0) { if (indentation === 0) {
return { return {
from: word.from, from: word.from,

View File

@ -0,0 +1,2 @@
ALTER TABLE "application" ADD COLUMN "enableSubmodules" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "enableSubmodules" boolean DEFAULT false;

View File

@ -0,0 +1,2 @@
ALTER TABLE "application" ALTER COLUMN "enableSubmodules" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "compose" ALTER COLUMN "enableSubmodules" SET NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -596,6 +596,20 @@
"when": 1743923992280, "when": 1743923992280,
"tag": "0084_thin_iron_lad", "tag": "0084_thin_iron_lad",
"breakpoints": true "breakpoints": true
},
{
"idx": 85,
"version": "7",
"when": 1745705609181,
"tag": "0085_equal_captain_stacy",
"breakpoints": true
},
{
"idx": 86,
"version": "7",
"when": 1745706676004,
"tag": "0086_rainy_gertrude_yorkes",
"breakpoints": true
} }
] ]
} }

View File

@ -1,23 +1,27 @@
/**
* Sorted list based off of population of the country / speakers of the language.
*/
export const Languages = { export const Languages = {
english: { code: "en", name: "English" }, english: { code: "en", name: "English" },
spanish: { code: "es", name: "Español" },
chineseSimplified: { code: "zh-Hans", name: "简体中文" },
chineseTraditional: { code: "zh-Hant", name: "繁體中文" },
portuguese: { code: "pt-br", name: "Português" },
russian: { code: "ru", name: "Русский" },
japanese: { code: "ja", name: "日本語" },
german: { code: "de", name: "Deutsch" },
korean: { code: "ko", name: "한국어" },
french: { code: "fr", name: "Français" },
turkish: { code: "tr", name: "Türkçe" },
italian: { code: "it", name: "Italiano" },
polish: { code: "pl", name: "Polski" }, polish: { code: "pl", name: "Polski" },
ukrainian: { code: "uk", name: "Українська" }, ukrainian: { code: "uk", name: "Українська" },
russian: { code: "ru", name: "Русский" },
french: { code: "fr", name: "Français" },
german: { code: "de", name: "Deutsch" },
chineseTraditional: { code: "zh-Hant", name: "繁體中文" },
chineseSimplified: { code: "zh-Hans", name: "简体中文" },
turkish: { code: "tr", name: "Türkçe" },
kazakh: { code: "kz", name: "Қазақ" },
persian: { code: "fa", name: "فارسی" }, persian: { code: "fa", name: "فارسی" },
korean: { code: "ko", name: "한국어" }, dutch: { code: "nl", name: "Nederlands" },
portuguese: { code: "pt-br", name: "Português" }, indonesian: { code: "id", name: "Bahasa Indonesia" },
italian: { code: "it", name: "Italiano" }, kazakh: { code: "kz", name: "Қазақ" },
japanese: { code: "ja", name: "日本語" },
spanish: { code: "es", name: "Español" },
norwegian: { code: "no", name: "Norsk" }, norwegian: { code: "no", name: "Norsk" },
azerbaijani: { code: "az", name: "Azərbaycan" }, azerbaijani: { code: "az", name: "Azərbaycan" },
indonesian: { code: "id", name: "Bahasa Indonesia" },
malayalam: { code: "ml", name: "മലയാളം" }, malayalam: { code: "ml", name: "മലയാളം" },
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.21.3", "version": "v0.21.7",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@ -92,7 +92,7 @@
"adm-zip": "^0.5.14", "adm-zip": "^0.5.14",
"ai": "^4.0.23", "ai": "^4.0.23",
"bcrypt": "5.1.1", "bcrypt": "5.1.1",
"better-auth": "1.2.4", "better-auth": "1.2.6",
"bl": "6.0.11", "bl": "6.0.11",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"bullmq": "5.4.2", "bullmq": "5.4.2",

View File

@ -215,7 +215,7 @@ const Service = (
router.push(newPath); router.push(newPath);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden", "flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",

View File

@ -212,15 +212,15 @@ const Service = (
router.push(newPath); router.push(newPath);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId isCloud && data?.serverId
? "md:grid-cols-7" ? "lg:grid-cols-7"
: data?.serverId : data?.serverId
? "md:grid-cols-6" ? "lg:grid-cols-6"
: "md:grid-cols-7", : "lg:grid-cols-7",
)} )}
> >
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>

View File

@ -182,7 +182,7 @@ const Mariadb = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@ -183,7 +183,7 @@ const Mongo = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@ -183,7 +183,7 @@ const MySql = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ", "md:grid md:w-fit max-md:overflow-y-scroll justify-start ",

View File

@ -182,7 +182,7 @@ const Postgresql = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@ -182,7 +182,7 @@ const Redis = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,58 @@
{
"settings.common.save": "Opslaan",
"settings.common.enterTerminal": "Terminal",
"settings.server.domain.title": "Server Domein",
"settings.server.domain.description": "Voeg een domein toe aan jouw server applicatie.",
"settings.server.domain.form.domain": "Domein",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
"settings.server.domain.form.certificate.label": "Certificaat Aanbieder",
"settings.server.domain.form.certificate.placeholder": "Select een certificaat",
"settings.server.domain.form.certificateOptions.none": "Geen",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Web Server",
"settings.server.webServer.description": "Herlaad of maak de web server schoon.",
"settings.server.webServer.actions": "Acties",
"settings.server.webServer.reload": "Herladen",
"settings.server.webServer.watchLogs": "Bekijk Logs",
"settings.server.webServer.updateServerIp": "Update de Server IP",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Bewerk Omgeving",
"settings.server.webServer.traefik.managePorts": "Extra Poort Mappings",
"settings.server.webServer.traefik.managePortsDescription": "Bewerk extra Poorten voor Traefik",
"settings.server.webServer.traefik.targetPort": "Doel Poort",
"settings.server.webServer.traefik.publishedPort": "Gepubliceerde Poort",
"settings.server.webServer.traefik.addPort": "Voeg Poort toe",
"settings.server.webServer.traefik.portsUpdated": "Poorten succesvol aangepast",
"settings.server.webServer.traefik.portsUpdateError": "Poorten niet succesvol aangepast",
"settings.server.webServer.traefik.publishMode": "Publiceer Mode",
"settings.server.webServer.storage.label": "Opslag",
"settings.server.webServer.storage.cleanUnusedImages": "Maak ongebruikte images schoon",
"settings.server.webServer.storage.cleanUnusedVolumes": "Maak ongebruikte volumes schoon",
"settings.server.webServer.storage.cleanStoppedContainers": "Maak gestopte containers schoon",
"settings.server.webServer.storage.cleanDockerBuilder": "Maak Docker Builder & Systeem schoon",
"settings.server.webServer.storage.cleanMonitoring": "Maak monitoor schoon",
"settings.server.webServer.storage.cleanAll": "Maak alles schoon",
"settings.profile.title": "Account",
"settings.profile.description": "Veramder details van account.",
"settings.profile.email": "Email",
"settings.profile.password": "Wachtwoord",
"settings.profile.avatar": "Profiel Icoon",
"settings.appearance.title": "Uiterlijk",
"settings.appearance.description": "Verander het thema van je dashboard.",
"settings.appearance.theme": "Thema",
"settings.appearance.themeDescription": "Selecteer een thema voor je dashboard.",
"settings.appearance.themes.light": "Licht",
"settings.appearance.themes.dark": "Donker",
"settings.appearance.themes.system": "Systeem",
"settings.appearance.language": "Taal",
"settings.appearance.languageDescription": "Selecteer een taal voor je dashboard.",
"settings.terminal.connectionSettings": "Verbindings instellingen",
"settings.terminal.ipAddress": "IP Address",
"settings.terminal.port": "Poort",
"settings.terminal.username": "Gebruikersnaam"
}

View File

@ -1 +1,78 @@
{} {
"dashboard.title": "仪表盘",
"dashboard.overview": "概览",
"dashboard.projects": "项目",
"dashboard.servers": "服务器",
"dashboard.docker": "Docker",
"dashboard.monitoring": "监控",
"dashboard.settings": "设置",
"dashboard.logout": "退出登录",
"dashboard.profile": "个人资料",
"dashboard.terminal": "终端",
"dashboard.containers": "容器",
"dashboard.images": "镜像",
"dashboard.volumes": "卷",
"dashboard.networks": "网络",
"button.create": "创建",
"button.edit": "编辑",
"button.delete": "删除",
"button.cancel": "取消",
"button.save": "保存",
"button.confirm": "确认",
"button.back": "返回",
"button.next": "下一步",
"button.finish": "完成",
"status.running": "运行中",
"status.stopped": "已停止",
"status.error": "错误",
"status.pending": "等待中",
"status.success": "成功",
"status.failed": "失败",
"form.required": "必填",
"form.invalid": "无效",
"form.submit": "提交",
"form.reset": "重置",
"notification.success": "操作成功",
"notification.error": "操作失败",
"notification.warning": "警告",
"notification.info": "信息",
"time.now": "刚刚",
"time.minutes": "分钟前",
"time.hours": "小时前",
"time.days": "天前",
"filter.all": "全部",
"filter.active": "活跃",
"filter.inactive": "不活跃",
"sort.asc": "升序",
"sort.desc": "降序",
"search.placeholder": "搜索...",
"search.noResults": "无结果",
"pagination.prev": "上一页",
"pagination.next": "下一页",
"pagination.of": "共 {0} 页",
"error.notFound": "未找到",
"error.serverError": "服务器错误",
"error.unauthorized": "未授权",
"error.forbidden": "禁止访问",
"loading": "加载中...",
"empty": "暂无数据",
"more": "更多",
"less": "收起",
"project.create": "创建项目",
"project.edit": "编辑项目",
"project.delete": "删除项目",
"project.name": "项目名称",
"project.description": "项目描述",
"service.create": "创建服务",
"service.edit": "编辑服务",
"service.delete": "删除服务",
"service.name": "服务名称",
"service.type": "服务类型",
"domain.add": "添加域名",
"domain.remove": "移除域名",
"environment.variables": "环境变量",
"environment.add": "添加环境变量",
"environment.edit": "编辑环境变量",
"environment.name": "变量名",
"environment.value": "变量值"
}

View File

@ -1,17 +1,16 @@
{ {
"settings.common.save": "保存", "settings.common.save": "保存",
"settings.common.enterTerminal": "进入终端", "settings.common.enterTerminal": "终端",
"settings.server.domain.title": "域名设置", "settings.server.domain.title": "服务器域名",
"settings.server.domain.description": "添加域名到服务器", "settings.server.domain.description": "为您的服务器应用添加域名。",
"settings.server.domain.form.domain": "域名", "settings.server.domain.form.domain": "域名",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱", "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱",
"settings.server.domain.form.certificate.label": "证书", "settings.server.domain.form.certificate.label": "证书提供商",
"settings.server.domain.form.certificate.placeholder": "选择一个证书", "settings.server.domain.form.certificate.placeholder": "选择证书",
"settings.server.domain.form.certificateOptions.none": "无", "settings.server.domain.form.certificateOptions.none": "无",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Web 服务器",
"settings.server.webServer.title": "服务器设置", "settings.server.webServer.description": "重载或清理 Web 服务器。",
"settings.server.webServer.description": "管理服务器",
"settings.server.webServer.actions": "操作", "settings.server.webServer.actions": "操作",
"settings.server.webServer.reload": "重新加载", "settings.server.webServer.reload": "重新加载",
"settings.server.webServer.watchLogs": "查看日志", "settings.server.webServer.watchLogs": "查看日志",
@ -19,40 +18,50 @@
"settings.server.webServer.server.label": "服务器", "settings.server.webServer.server.label": "服务器",
"settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "修改环境变量", "settings.server.webServer.traefik.modifyEnv": "修改环境变量",
"settings.server.webServer.traefik.managePorts": "端口转发", "settings.server.webServer.traefik.managePorts": "额外端口映射",
"settings.server.webServer.traefik.managePortsDescription": "添加或删除 Traefik 的其他端口", "settings.server.webServer.traefik.managePortsDescription": "为 Traefik 添加或删除额外端口",
"settings.server.webServer.traefik.targetPort": "目标端口", "settings.server.webServer.traefik.targetPort": "目标端口",
"settings.server.webServer.traefik.publishedPort": "对外端口", "settings.server.webServer.traefik.publishedPort": "发布端口",
"settings.server.webServer.traefik.addPort": "添加端口", "settings.server.webServer.traefik.addPort": "添加端口",
"settings.server.webServer.traefik.portsUpdated": "端口更新成功", "settings.server.webServer.traefik.portsUpdated": "端口更新成功",
"settings.server.webServer.traefik.portsUpdateError": "端口更新失败", "settings.server.webServer.traefik.portsUpdateError": "端口更新失败",
"settings.server.webServer.traefik.publishMode": "端口映射", "settings.server.webServer.traefik.publishMode": "发布模式",
"settings.server.webServer.storage.label": "存储空间", "settings.server.webServer.storage.label": "存储空间",
"settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像", "settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像",
"settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷", "settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷",
"settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器", "settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器",
"settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 与 系统缓存", "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统",
"settings.server.webServer.storage.cleanMonitoring": "清理监控数据", "settings.server.webServer.storage.cleanMonitoring": "清理监控数据",
"settings.server.webServer.storage.cleanAll": "清理所有内容", "settings.server.webServer.storage.cleanAll": "清理所有内容",
"settings.profile.title": "账户", "settings.profile.title": "账户",
"settings.profile.description": "更改您的个人资料", "settings.profile.description": "在此更改您的个人资料详情。",
"settings.profile.email": "邮箱", "settings.profile.email": "邮箱",
"settings.profile.password": "密码", "settings.profile.password": "密码",
"settings.profile.avatar": "头像", "settings.profile.avatar": "头像",
"settings.appearance.title": "外观", "settings.appearance.title": "外观",
"settings.appearance.description": "自定义面板主题", "settings.appearance.description": "自定义您的仪表盘主题。",
"settings.appearance.theme": "主题", "settings.appearance.theme": "主题",
"settings.appearance.themeDescription": "选择面板主题", "settings.appearance.themeDescription": "为您的仪表盘选择主题",
"settings.appearance.themes.light": "明亮", "settings.appearance.themes.light": "明亮",
"settings.appearance.themes.dark": "暗", "settings.appearance.themes.dark": "",
"settings.appearance.themes.system": "系统主题", "settings.appearance.themes.system": "跟随系统",
"settings.appearance.language": "语言", "settings.appearance.language": "语言",
"settings.appearance.languageDescription": "选择面板语言", "settings.appearance.languageDescription": "为您的仪表盘选择语言",
"settings.terminal.connectionSettings": "连接设置",
"settings.terminal.connectionSettings": "终端设置", "settings.terminal.ipAddress": "IP 地址",
"settings.terminal.ipAddress": "IP",
"settings.terminal.port": "端口", "settings.terminal.port": "端口",
"settings.terminal.username": "用户名" "settings.terminal.username": "用户名",
"settings.settings": "设置",
"settings.general": "通用设置",
"settings.security": "安全",
"settings.users": "用户管理",
"settings.roles": "角色管理",
"settings.permissions": "权限",
"settings.api": "API设置",
"settings.certificates": "证书管理",
"settings.ssh": "SSH密钥",
"settings.backups": "备份",
"settings.logs": "日志",
"settings.updates": "更新",
"settings.network": "网络"
} }

View File

@ -356,6 +356,7 @@ export const applicationRouter = createTRPCRouter({
githubId: input.githubId, githubId: input.githubId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
triggerType: input.triggerType, triggerType: input.triggerType,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@ -383,6 +384,7 @@ export const applicationRouter = createTRPCRouter({
gitlabProjectId: input.gitlabProjectId, gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace, gitlabPathNamespace: input.gitlabPathNamespace,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@ -408,6 +410,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
bitbucketId: input.bitbucketId, bitbucketId: input.bitbucketId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@ -433,6 +436,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
giteaId: input.giteaId, giteaId: input.giteaId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@ -480,6 +484,7 @@ export const applicationRouter = createTRPCRouter({
sourceType: "git", sourceType: "git",
applicationStatus: "idle", applicationStatus: "idle",
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;

View File

@ -31,7 +31,6 @@ export const mountRouter = createTRPCRouter({
update: protectedProcedure update: protectedProcedure
.input(apiUpdateMount) .input(apiUpdateMount)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await updateMount(input.mountId, input); return await updateMount(input.mountId, input);
return true;
}), }),
}); });

View File

@ -21,6 +21,7 @@ import { setupTerminalWebSocketServer } from "./wss/terminal";
config({ path: ".env" }); config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10); const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const HOST = process.env.HOST || "0.0.0.0";
const dev = process.env.NODE_ENV !== "production"; const dev = process.env.NODE_ENV !== "production";
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" }); const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
const handle = app.getRequestHandler(); const handle = app.getRequestHandler();
@ -55,8 +56,8 @@ void app.prepare().then(async () => {
await migration(); await migration();
} }
server.listen(PORT); server.listen(PORT, HOST);
console.log("Server Started:", PORT); console.log(`Server Started on: http://${HOST}:${PORT}`);
if (!IS_CLOUD) { if (!IS_CLOUD) {
console.log("Starting Deployment Worker"); console.log("Starting Deployment Worker");
const { deploymentWorker } = await import("./queues/deployments-queue"); const { deploymentWorker } = await import("./queues/deployments-queue");

View File

@ -36,11 +36,11 @@
"@ai-sdk/mistral": "^1.0.6", "@ai-sdk/mistral": "^1.0.6",
"@ai-sdk/openai": "^1.0.12", "@ai-sdk/openai": "^1.0.12",
"@ai-sdk/openai-compatible": "^0.0.13", "@ai-sdk/openai-compatible": "^0.0.13",
"@better-auth/utils": "0.2.3", "@better-auth/utils": "0.2.4",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"drizzle-dbml-generator": "0.10.0", "drizzle-dbml-generator": "0.10.0",
"better-auth": "1.2.4", "better-auth": "1.2.6",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.0.4", "@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21", "@react-email/components": "^0.0.21",

View File

@ -183,6 +183,7 @@ export const applications = pgTable("application", {
onDelete: "set null", onDelete: "set null",
}, },
), ),
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
dockerfile: text("dockerfile"), dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"), dockerContextPath: text("dockerContextPath"),
dockerBuildStage: text("dockerBuildStage"), dockerBuildStage: text("dockerBuildStage"),
@ -471,6 +472,7 @@ export const apiSaveGithubProvider = createSchema
buildPath: true, buildPath: true,
githubId: true, githubId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required() .required()
.extend({ .extend({
@ -488,6 +490,7 @@ export const apiSaveGitlabProvider = createSchema
gitlabProjectId: true, gitlabProjectId: true,
gitlabPathNamespace: true, gitlabPathNamespace: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@ -500,6 +503,7 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketId: true, bitbucketId: true,
applicationId: true, applicationId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@ -512,6 +516,7 @@ export const apiSaveGiteaProvider = createSchema
giteaRepository: true, giteaRepository: true,
giteaId: true, giteaId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@ -532,6 +537,7 @@ export const apiSaveGitProvider = createSchema
customGitBuildPath: true, customGitBuildPath: true,
customGitUrl: true, customGitUrl: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required() .required()
.merge( .merge(

View File

@ -72,6 +72,7 @@ export const compose = pgTable("compose", {
), ),
command: text("command").notNull().default(""), command: text("command").notNull().default(""),
// //
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
composePath: text("composePath").notNull().default("./docker-compose.yml"), composePath: text("composePath").notNull().default("./docker-compose.yml"),
suffix: text("suffix").notNull().default(""), suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false), randomize: boolean("randomize").notNull().default(false),

View File

@ -201,7 +201,7 @@ const { handler, api } = betterAuth({
const host = const host =
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? "http://localhost:3000" ? "http://localhost:3000"
: "https://dokploy.com"; : "https://app.dokploy.com";
const inviteLink = `${host}/invitation?token=${data.id}`; const inviteLink = `${host}/invitation?token=${data.id}`;
await sendEmail({ await sendEmail({

View File

@ -356,6 +356,7 @@ export const deployRemoteCompose = async ({
deployment.logPath, deployment.logPath,
true, true,
); );
console.log(command);
} else if (compose.sourceType === "raw") { } else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose, deployment.logPath); command += getCreateComposeFileCommand(compose, deployment.logPath);
} else if (compose.sourceType === "gitea") { } else if (compose.sourceType === "gitea") {

View File

@ -144,7 +144,8 @@ export const updateMount = async (
await deleteFileMount(mountId); await deleteFileMount(mountId);
await createFileMount(mountId); await createFileMount(mountId);
} }
return mount;
return await findMountById(mountId);
}); });
}; };

View File

@ -76,7 +76,7 @@ CURRENT_USER=$USER
echo "Installing requirements for: OS: $OS_TYPE" echo "Installing requirements for: OS: $OS_TYPE"
if [ $EUID != 0 ]; then if [ $EUID != 0 ]; then
echo "Please run this script as root or with sudo ❌" echo "Please run this script as root or with sudo ❌"
exit exit
fi fi
@ -263,7 +263,7 @@ const setupMainDirectory = () => `
# Create the /etc/dokploy directory # Create the /etc/dokploy directory
mkdir -p /etc/dokploy mkdir -p /etc/dokploy
chmod 777 /etc/dokploy chmod 777 /etc/dokploy
echo "Directory /etc/dokploy created ✅" echo "Directory /etc/dokploy created ✅"
fi fi
`; `;
@ -276,16 +276,16 @@ export const setupSwarm = () => `
# Get IP address # Get IP address
get_ip() { get_ip() {
local ip="" local ip=""
# Try IPv4 with multiple services # Try IPv4 with multiple services
# First attempt: ifconfig.io # First attempt: ifconfig.io
ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Second attempt: icanhazip.com # Second attempt: icanhazip.com
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi fi
# Third attempt: ipecho.net # Third attempt: ipecho.net
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@ -295,12 +295,12 @@ export const setupSwarm = () => `
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
# Try IPv6 with ifconfig.io # Try IPv6 with ifconfig.io
ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Try IPv6 with icanhazip.com # Try IPv6 with icanhazip.com
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi fi
# Try IPv6 with ipecho.net # Try IPv6 with ipecho.net
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@ -549,7 +549,7 @@ export const createTraefikInstance = () => {
sleep 8 sleep 8
echo "Traefik migrated to Standalone ✅" echo "Traefik migrated to Standalone ✅"
fi fi
if docker inspect dokploy-traefik > /dev/null 2>&1; then if docker inspect dokploy-traefik > /dev/null 2>&1; then
echo "Traefik already exists ✅" echo "Traefik already exists ✅"
else else
@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then if command_exists nixpacks; then
echo "Nixpacks already installed ✅" echo "Nixpacks already installed ✅"
else else
export NIXPACKS_VERSION=1.29.1 export NIXPACKS_VERSION=1.35.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅" echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi fi

View File

@ -1,4 +1,4 @@
import { randomBytes } from "node:crypto"; import { randomBytes, createHmac } from "node:crypto";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
@ -24,6 +24,12 @@ export interface Template {
domains: DomainSchema[]; domains: DomainSchema[];
} }
export interface GenerateJWTOptions {
length?: number;
secret?: string;
payload?: Record<string, unknown> | undefined;
}
export const generateRandomDomain = ({ export const generateRandomDomain = ({
serverIp, serverIp,
projectName, projectName,
@ -61,8 +67,48 @@ export function generateBase64(bytes = 32): string {
return randomBytes(bytes).toString("base64"); return randomBytes(bytes).toString("base64");
} }
export function generateJwt(length = 256): string { function safeBase64(str: string): string {
return randomBytes(length).toString("hex"); return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function objToJWTBase64(obj: any): string {
return safeBase64(
Buffer.from(JSON.stringify(obj), "utf8").toString("base64"),
);
}
export function generateJwt(options: GenerateJWTOptions = {}): string {
let { length, secret, payload = {} } = options;
if (length) {
return randomBytes(length).toString("hex");
}
const encodedHeader = objToJWTBase64({
alg: "HS256",
typ: "JWT",
});
if (!payload.iss) {
payload.iss = "dokploy";
}
if (!payload.iat) {
payload.iat = Math.floor(Date.now() / 1000);
}
if (!payload.exp) {
payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000);
}
const encodedPayload = objToJWTBase64({
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000),
...payload,
});
if (!secret) {
secret = randomBytes(32).toString("hex");
}
const signature = safeBase64(
createHmac("SHA256", secret)
.update(`${encodedHeader}.${encodedPayload}`)
.digest("base64"),
);
return `${encodedHeader}.${encodedPayload}.${signature}`;
} }
/** /**

View File

@ -65,7 +65,7 @@ export interface Template {
/** /**
* Process a string value and replace variables * Process a string value and replace variables
*/ */
function processValue( export function processValue(
value: string, value: string,
variables: Record<string, string>, variables: Record<string, string>,
schema: Schema, schema: Schema,
@ -84,11 +84,11 @@ function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 32; const length = Number.parseInt(varName.split(":")[1], 10) || 32;
return generateBase64(length); return generateBase64(length);
} }
if (varName.startsWith("password:")) { if (varName.startsWith("password:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 16; const length = Number.parseInt(varName.split(":")[1], 10) || 16;
return generatePassword(length); return generatePassword(length);
} }
if (varName === "password") { if (varName === "password") {
return generatePassword(16); return generatePassword(16);
} }
@ -97,14 +97,31 @@ function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 8; const length = Number.parseInt(varName.split(":")[1], 10) || 8;
return generateHash(length); return generateHash(length);
} }
if (varName === "hash") {
return generateHash();
}
if (varName === "uuid") { if (varName === "uuid") {
return crypto.randomUUID(); return crypto.randomUUID();
} }
if (varName === "timestamp") { if (varName === "timestamp" || varName === "timestampms") {
return Date.now().toString(); return Date.now().toString();
} }
if (varName === "timestamps") {
return Math.round(Date.now() / 1000).toString();
}
if (varName.startsWith("timestampms:")) {
return new Date(varName.slice(12)).getTime().toString();
}
if (varName.startsWith("timestamps:")) {
return Math.round(
new Date(varName.slice(11)).getTime() / 1000,
).toString();
}
if (varName === "randomPort") { if (varName === "randomPort") {
return Math.floor(Math.random() * 65535).toString(); return Math.floor(Math.random() * 65535).toString();
} }
@ -114,8 +131,34 @@ function processValue(
} }
if (varName.startsWith("jwt:")) { if (varName.startsWith("jwt:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 256; const params: string[] = varName.split(":").slice(1);
return generateJwt(length); if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) {
return generateJwt({ length: Number.parseInt(params[0], 10) });
}
let [secret, payload] = params;
if (typeof payload === "string" && variables[payload]) {
payload = variables[payload];
}
if (
typeof payload === "string" &&
payload.startsWith("{") &&
payload.endsWith("}")
) {
try {
payload = JSON.parse(payload);
} catch (e) {
// If payload is not a valid JSON, invalid it
payload = undefined;
console.error("Invalid JWT payload", e);
}
}
if (typeof payload !== "object") {
payload = undefined;
}
return generateJwt({
secret: secret ? variables[secret] || secret : undefined,
payload: payload as any,
});
} }
if (varName === "username") { if (varName === "username") {
@ -147,7 +190,7 @@ export function processVariables(
): Record<string, string> { ): Record<string, string> {
const variables: Record<string, string> = {}; const variables: Record<string, string> = {};
// First pass: Process variables that don't depend on other variables // First pass: Process some variables that don't depend on other variables
for (const [key, value] of Object.entries(template.variables)) { for (const [key, value] of Object.entries(template.variables)) {
if (typeof value !== "string") continue; if (typeof value !== "string") continue;
@ -161,6 +204,8 @@ export function processVariables(
const match = value.match(/\${password:(\d+)}/); const match = value.match(/\${password:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 16; const length = match?.[1] ? Number.parseInt(match[1], 10) : 16;
variables[key] = generatePassword(length); variables[key] = generatePassword(length);
} else if (value === "${hash}") {
variables[key] = generateHash();
} else if (value.startsWith("${hash:")) { } else if (value.startsWith("${hash:")) {
const match = value.match(/\${hash:(\d+)}/); const match = value.match(/\${hash:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 8; const length = match?.[1] ? Number.parseInt(match[1], 10) : 8;

View File

@ -23,7 +23,17 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
try { try {
await execAsync(`mkdir -p ${tempDir}/filesystem`); await execAsync(`mkdir -p ${tempDir}/filesystem`);
const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`; // First get the container ID
const { stdout: containerId } = await execAsync(
"docker ps --filter 'name=dokploy-postgres' -q",
);
if (!containerId) {
throw new Error("PostgreSQL container not found");
}
// Then run pg_dump with the container ID
const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
await execAsync(postgresCommand); await execAsync(postgresCommand);
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);

View File

@ -84,7 +84,7 @@ export const buildRailpack = async (
for (const envVar of envVariables) { for (const envVar of envVariables) {
const [key, value] = envVar.split("="); const [key, value] = envVar.split("=");
if (key && value) { if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`); buildArgs.push("--secret", `id=${key},env='${key}'`);
env[key] = value; env[key] = value;
} }
} }
@ -132,7 +132,7 @@ export const getRailpackCommand = (
]; ];
for (const env of envVariables) { for (const env of envVariables) {
prepareArgs.push("--env", env); prepareArgs.push("--env", `'${env}'`);
} }
// Calculate secrets hash for layer invalidation // Calculate secrets hash for layer invalidation
@ -164,7 +164,7 @@ export const getRailpackCommand = (
for (const envVar of envVariables) { for (const envVar of envVariables) {
const [key, value] = envVar.split("="); const [key, value] = envVar.split("=");
if (key && value) { if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`); buildArgs.push("--secret", `id=${key},env='${key}'`);
exportEnvs.push(`export ${key}=${value}`); exportEnvs.push(`export ${key}=${value}`);
} }
} }

View File

@ -249,6 +249,11 @@ export const addDomainToCompose = async (
labels.unshift("traefik.enable=true"); labels.unshift("traefik.enable=true");
} }
labels.unshift(...httpLabels); labels.unshift(...httpLabels);
if (!compose.isolatedDeployment) {
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
}
} }
if (!compose.isolatedDeployment) { if (!compose.isolatedDeployment) {

View File

@ -40,68 +40,84 @@ export const sendDokployRestartNotifications = async () => {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim(); `${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, { try {
title: decorate(">", "`✅` Dokploy Server Restarted"), await sendDiscordNotification(discord, {
color: 0x57f287, title: decorate(">", "`✅` Dokploy Server Restarted"),
fields: [ color: 0x57f287,
{ fields: [
name: decorate("`📅`", "Date"), {
value: `<t:${unixDate}:D>`, name: decorate("`📅`", "Date"),
inline: true, value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
}, },
{ });
name: decorate("`⌚`", "Time"), } catch (error) {
value: `<t:${unixDate}:t>`, console.log(error);
inline: true, }
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
},
});
} }
if (gotify) { if (gotify) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`; `${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification( try {
gotify, await sendGotifyNotification(
decorate("✅", "Dokploy Server Restarted"), gotify,
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`, decorate("✅", "Dokploy Server Restarted"),
); `${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
} catch (error) {
console.log(error);
}
} }
if (telegram) { if (telegram) {
await sendTelegramNotification( try {
telegram, await sendTelegramNotification(
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`, telegram,
); `<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
} catch (error) {
console.log(error);
}
} }
if (slack) { if (slack) {
const { channel } = slack; const { channel } = slack;
await sendSlackNotification(slack, { try {
channel: channel, await sendSlackNotification(slack, {
attachments: [ channel: channel,
{ attachments: [
color: "#00FF00", {
pretext: ":white_check_mark: *Dokploy Server Restarted*", color: "#00FF00",
fields: [ pretext: ":white_check_mark: *Dokploy Server Restarted*",
{ fields: [
title: "Time", {
value: date.toLocaleString(), title: "Time",
short: true, value: date.toLocaleString(),
}, short: true,
], },
}, ],
], },
}); ],
});
} catch (error) {
console.log(error);
}
} }
} }
}; };

View File

@ -37,6 +37,7 @@ export const cloneBitbucketRepository = async (
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
bitbucket, bitbucket,
enableSubmodules,
} = entity; } = entity;
if (!bitbucketId) { if (!bitbucketId) {
@ -53,25 +54,23 @@ export const cloneBitbucketRepository = async (
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
try { try {
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync( const cloneArgs = [
"git", "clone",
[ "--branch",
"clone", bitbucketBranch!,
"--branch", "--depth",
bitbucketBranch!, "1",
"--depth", ...(enableSubmodules ? ["--recurse-submodules"] : []),
"1", cloneUrl,
"--recurse-submodules", outputPath,
cloneUrl, "--progress",
outputPath, ];
"--progress",
], await spawnAsync("git", cloneArgs, (data) => {
(data) => { if (writeStream.writable) {
if (writeStream.writable) { writeStream.write(data);
writeStream.write(data); }
} });
},
);
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@ -89,6 +88,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
bitbucketOwner, bitbucketOwner,
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
enableSubmodules,
} = entity; } = entity;
if (!bitbucketId) { if (!bitbucketId) {
@ -106,17 +106,19 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try { try {
await spawnAsync("git", [ const cloneArgs = [
"clone", "clone",
"--branch", "--branch",
bitbucketBranch!, bitbucketBranch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]); ];
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -131,6 +133,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@ -153,11 +156,11 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try { try {
const command = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, cloneCommand);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -176,6 +179,7 @@ export const getBitbucketCloneCommand = async (
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
serverId, serverId,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@ -207,7 +211,7 @@ export const getBitbucketCloneCommand = async (
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi

View File

@ -17,12 +17,19 @@ export const cloneGitRepository = async (
customGitUrl?: string | null; customGitUrl?: string | null;
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
}, },
logPath: string, logPath: string,
isCompose = false, isCompose = false,
) => { ) => {
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(); const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity; const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
throw new TRPCError({ throw new TRPCError({
@ -70,19 +77,21 @@ export const cloneGitRepository = async (
} }
const { port } = sanitizeRepoPathSSH(customGitUrl); const { port } = sanitizeRepoPathSSH(customGitUrl);
const cloneArgs = [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync( await spawnAsync(
"git", "git",
[ cloneArgs,
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
"--recurse-submodules",
customGitUrl,
outputPath,
"--progress",
],
(data) => { (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data); writeStream.write(data);
@ -114,6 +123,7 @@ export const getCustomGitCloneCommand = async (
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
serverId: string | null; serverId: string | null;
enableSubmodules: boolean;
}, },
logPath: string, logPath: string,
isCompose = false, isCompose = false,
@ -125,6 +135,7 @@ export const getCustomGitCloneCommand = async (
customGitBranch, customGitBranch,
customGitSSHKeyId, customGitSSHKeyId,
serverId, serverId,
enableSubmodules,
} = entity; } = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
@ -181,7 +192,7 @@ export const getCustomGitCloneCommand = async (
} }
command.push( command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
exit 1; exit 1;
fi fi
@ -261,8 +272,15 @@ export const cloneGitRawRepository = async (entity: {
customGitUrl?: string | null; customGitUrl?: string | null;
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
}) => { }) => {
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity; const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
throw new TRPCError({ throw new TRPCError({
@ -307,29 +325,26 @@ export const cloneGitRawRepository = async (entity: {
} }
const { port } = sanitizeRepoPathSSH(customGitUrl); const { port } = sanitizeRepoPathSSH(customGitUrl);
await spawnAsync( const cloneArgs = [
"git", "clone",
[ "--branch",
"clone", customGitBranch,
"--branch", "--depth",
customGitBranch, "1",
"--depth", ...(enableSubmodules ? ["--recurse-submodules"] : []),
"1", customGitUrl,
"--recurse-submodules", outputPath,
customGitUrl, "--progress",
outputPath, ];
"--progress",
], await spawnAsync("git", cloneArgs, (_data) => {}, {
(_data) => {}, env: {
{ ...process.env,
env: { ...(customGitSSHKeyId && {
...process.env, GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
...(customGitSSHKeyId && { }),
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
}, },
); });
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -342,6 +357,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
customGitUrl, customGitUrl,
customGitSSHKeyId, customGitSSHKeyId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@ -396,7 +412,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
} }
command.push( command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} ; then `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then
echo "[ERROR] Fail to clone the repository "; echo "[ERROR] Fail to clone the repository ";
exit 1; exit 1;
fi fi

View File

@ -119,6 +119,7 @@ export const getGiteaCloneCommand = async (
giteaRepository, giteaRepository,
serverId, serverId,
gitea, gitea,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@ -155,7 +156,7 @@ export const getGiteaCloneCommand = async (
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath}; echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1; exit 1;
fi fi
@ -174,7 +175,14 @@ export const cloneGiteaRepository = async (
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity; const {
appName,
giteaBranch,
giteaId,
giteaOwner,
giteaRepository,
enableSubmodules,
} = entity;
if (!giteaId) { if (!giteaId) {
throw new TRPCError({ throw new TRPCError({
@ -211,7 +219,7 @@ export const cloneGiteaRepository = async (
giteaBranch!, giteaBranch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
@ -232,7 +240,14 @@ export const cloneGiteaRepository = async (
}; };
export const cloneRawGiteaRepository = async (entity: Compose) => { export const cloneRawGiteaRepository = async (entity: Compose) => {
const { appName, giteaRepository, giteaOwner, giteaBranch, giteaId } = entity; const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
enableSubmodules,
} = entity;
const { COMPOSE_PATH } = paths(); const { COMPOSE_PATH } = paths();
if (!giteaId) { if (!giteaId) {
@ -265,7 +280,7 @@ export const cloneRawGiteaRepository = async (entity: Compose) => {
giteaBranch!, giteaBranch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
@ -283,6 +298,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
giteaBranch, giteaBranch,
giteaId, giteaId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@ -307,7 +323,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${giteaBranch} --depth 1 ${cloneUrl} ${outputPath} git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {

View File

@ -83,6 +83,7 @@ interface CloneGithubRepository {
repository: string | null; repository: string | null;
logPath: string; logPath: string;
type?: "application" | "compose"; type?: "application" | "compose";
enableSubmodules: boolean;
} }
export const cloneGithubRepository = async ({ export const cloneGithubRepository = async ({
logPath, logPath,
@ -92,7 +93,8 @@ export const cloneGithubRepository = async ({
const isCompose = type === "compose"; const isCompose = type === "compose";
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, repository, owner, branch, githubId } = entity; const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) { if (!githubId) {
throw new TRPCError({ throw new TRPCError({
@ -128,25 +130,23 @@ export const cloneGithubRepository = async ({
try { try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync( const cloneArgs = [
"git", "clone",
[ "--branch",
"clone", branch!,
"--branch", "--depth",
branch!, "1",
"--depth", ...(enableSubmodules ? ["--recurse-submodules"] : []),
"1", cloneUrl,
"--recurse-submodules", outputPath,
cloneUrl, "--progress",
outputPath, ];
"--progress",
], await spawnAsync("git", cloneArgs, (data) => {
(data) => { if (writeStream.writable) {
if (writeStream.writable) { writeStream.write(data);
writeStream.write(data); }
} });
},
);
writeStream.write(`\nCloned ${repoclone}: ✅\n`); writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@ -161,7 +161,15 @@ export const getGithubCloneCommand = async ({
type = "application", type = "application",
...entity ...entity
}: CloneGithubRepository & { serverId: string }) => { }: CloneGithubRepository & { serverId: string }) => {
const { appName, repository, owner, branch, githubId, serverId } = entity; const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = entity;
const isCompose = type === "compose"; const isCompose = type === "compose";
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@ -216,7 +224,7 @@ export const getGithubCloneCommand = async ({
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi
@ -227,7 +235,8 @@ echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
}; };
export const cloneRawGithubRepository = async (entity: Compose) => { export const cloneRawGithubRepository = async (entity: Compose) => {
const { appName, repository, owner, branch, githubId } = entity; const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) { if (!githubId) {
throw new TRPCError({ throw new TRPCError({
@ -245,24 +254,33 @@ export const cloneRawGithubRepository = async (entity: Compose) => {
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`; const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try { try {
await spawnAsync("git", [ const cloneArgs = [
"clone", "clone",
"--branch", "--branch",
branch!, branch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]); ];
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => { export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
const { appName, repository, owner, branch, githubId, serverId } = compose; const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@ -288,7 +306,7 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath} git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {

View File

@ -90,8 +90,14 @@ export const cloneGitlabRepository = async (
isCompose = false, isCompose = false,
) => { ) => {
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } = const {
entity; appName,
gitlabBranch,
gitlabId,
gitlab,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) { if (!gitlabId) {
throw new TRPCError({ throw new TRPCError({
@ -127,25 +133,23 @@ export const cloneGitlabRepository = async (
try { try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync( const cloneArgs = [
"git", "clone",
[ "--branch",
"clone", gitlabBranch!,
"--branch", "--depth",
gitlabBranch!, "1",
"--depth", ...(enableSubmodules ? ["--recurse-submodules"] : []),
"1", cloneUrl,
"--recurse-submodules", outputPath,
cloneUrl, "--progress",
outputPath, ];
"--progress",
], await spawnAsync("git", cloneArgs, (data) => {
(data) => { if (writeStream.writable) {
if (writeStream.writable) { writeStream.write(data);
writeStream.write(data); }
} });
},
);
writeStream.write(`\nCloned ${repoclone}: ✅\n`); writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@ -167,6 +171,7 @@ export const getGitlabCloneCommand = async (
gitlabId, gitlabId,
serverId, serverId,
gitlab, gitlab,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@ -222,7 +227,7 @@ export const getGitlabCloneCommand = async (
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${gitlabBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi
@ -264,7 +269,11 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const groupName = gitlabProvider.groupName?.toLowerCase(); const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) { if (groupName) {
return full_path.toLowerCase().includes(groupName) && kind === "group"; const isIncluded = groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
return isIncluded && kind === "group";
} }
return kind === "user"; return kind === "user";
}); });
@ -326,7 +335,13 @@ export const getGitlabBranches = async (input: {
}; };
export const cloneRawGitlabRepository = async (entity: Compose) => { export const cloneRawGitlabRepository = async (entity: Compose) => {
const { appName, gitlabBranch, gitlabId, gitlabPathNamespace } = entity; const {
appName,
gitlabBranch,
gitlabId,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) { if (!gitlabId) {
throw new TRPCError({ throw new TRPCError({
@ -347,24 +362,32 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`; const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
try { try {
await spawnAsync("git", [ const cloneArgs = [
"clone", "clone",
"--branch", "--branch",
gitlabBranch!, gitlabBranch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]); ];
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => { export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose; const {
appName,
gitlabPathNamespace,
branch,
gitlabId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@ -388,7 +411,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {
@ -431,7 +454,9 @@ export const testGitlabConnection = async (
const { full_path, kind } = repo.namespace; const { full_path, kind } = repo.namespace;
if (groupName) { if (groupName) {
return full_path.toLowerCase().includes(groupName) && kind === "group"; return groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
} }
return kind === "user"; return kind === "user";
}); });

View File

@ -37,9 +37,9 @@ export const updateServerTraefik = (
servers: [ servers: [
{ {
url: `http://dokploy:${process.env.PORT || 3000}`, url: `http://dokploy:${process.env.PORT || 3000}`,
passHostHeader: true,
}, },
], ],
passHostHeader: true,
}, },
}, },
}; };

View File

@ -17,7 +17,7 @@ importers:
version: 1.9.4 version: 1.9.4
'@commitlint/cli': '@commitlint/cli':
specifier: ^19.3.0 specifier: ^19.3.0
version: 19.3.0(@types/node@18.19.42)(typescript@5.7.2) version: 19.3.0(@types/node@18.19.42)(typescript@5.8.3)
'@commitlint/config-conventional': '@commitlint/config-conventional':
specifier: ^19.2.2 specifier: ^19.2.2
version: 19.2.2 version: 19.2.2
@ -266,8 +266,8 @@ importers:
specifier: 5.1.1 specifier: 5.1.1
version: 5.1.1(encoding@0.1.13) version: 5.1.1(encoding@0.1.13)
better-auth: better-auth:
specifier: 1.2.4 specifier: 1.2.6
version: 1.2.4(typescript@5.5.3) version: 1.2.6
bl: bl:
specifier: 6.0.11 specifier: 6.0.11
version: 6.0.11 version: 6.0.11
@ -607,8 +607,8 @@ importers:
specifier: ^0.0.13 specifier: ^0.0.13
version: 0.0.13(zod@3.23.8) version: 0.0.13(zod@3.23.8)
'@better-auth/utils': '@better-auth/utils':
specifier: 0.2.3 specifier: 0.2.4
version: 0.2.3 version: 0.2.4
'@faker-js/faker': '@faker-js/faker':
specifier: ^8.4.1 specifier: ^8.4.1
version: 8.4.1 version: 8.4.1
@ -640,8 +640,8 @@ importers:
specifier: 5.1.1 specifier: 5.1.1
version: 5.1.1(encoding@0.1.13) version: 5.1.1(encoding@0.1.13)
better-auth: better-auth:
specifier: 1.2.4 specifier: 1.2.6
version: 1.2.4(typescript@5.5.3) version: 1.2.6
bl: bl:
specifier: 6.0.11 specifier: 6.0.11
version: 6.0.11 version: 6.0.11
@ -924,11 +924,11 @@ packages:
'@balena/dockerignore@1.0.2': '@balena/dockerignore@1.0.2':
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
'@better-auth/utils@0.2.3': '@better-auth/utils@0.2.4':
resolution: {integrity: sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw==} resolution: {integrity: sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ==}
'@better-fetch/fetch@1.1.15': '@better-fetch/fetch@1.1.18':
resolution: {integrity: sha512-0Bl8YYj1f8qCTNHeSn5+1DWv2hy7rLBrQ8rS8Y9XYloiwZEfc3k4yspIG0llRxafxqhGCwlGRg+F8q1HZRCMXA==} resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
'@biomejs/biome@1.9.4': '@biomejs/biome@1.9.4':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
@ -3836,11 +3836,11 @@ packages:
before-after-hook@2.2.3: before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
better-auth@1.2.4: better-auth@1.2.6:
resolution: {integrity: sha512-/ZK2jbUjm8JwdeCLFrUWUBmexPyI9PkaLVXWLWtN60sMDHTY8B5G72wcHglo1QMFBaw4G0qFkP5ayl9k6XfDaA==} resolution: {integrity: sha512-RVy6nfNCXpohx49zP2ChUO3zN0nvz5UXuETJIhWU+dshBKpFMk4P4hAQauM3xqTJdd9hfeB5y+segmG1oYGTJQ==}
better-call@1.0.3: better-call@1.0.7:
resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==} resolution: {integrity: sha512-p5kEthErx3HsW9dCCvvEx+uuEdncn0ZrlqrOG3TkR1aVYgynpwYbTVU90nY8/UwfMhROzqZWs8vryainSQxrNg==}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
@ -7093,8 +7093,8 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
typescript@5.7.2: typescript@5.8.3:
resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
@ -7210,14 +7210,6 @@ packages:
v8-compile-cache-lib@3.0.1: v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
valibot@1.0.0-beta.15:
resolution: {integrity: sha512-BKy8XosZkDHWmYC+cJG74LBzP++Gfntwi33pP3D3RKztz2XV9jmFWnkOi21GoqARP8wAWARwhV6eTr1JcWzjGw==}
peerDependencies:
typescript: '>=5'
peerDependenciesMeta:
typescript:
optional: true
vfile-message@4.0.2: vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
@ -7567,11 +7559,12 @@ snapshots:
'@balena/dockerignore@1.0.2': {} '@balena/dockerignore@1.0.2': {}
'@better-auth/utils@0.2.3': '@better-auth/utils@0.2.4':
dependencies: dependencies:
typescript: 5.8.3
uncrypto: 0.1.3 uncrypto: 0.1.3
'@better-fetch/fetch@1.1.15': {} '@better-fetch/fetch@1.1.18': {}
'@biomejs/biome@1.9.4': '@biomejs/biome@1.9.4':
optionalDependencies: optionalDependencies:
@ -7678,11 +7671,11 @@ snapshots:
style-mod: 4.1.2 style-mod: 4.1.2
w3c-keyname: 2.2.8 w3c-keyname: 2.2.8
'@commitlint/cli@19.3.0(@types/node@18.19.42)(typescript@5.7.2)': '@commitlint/cli@19.3.0(@types/node@18.19.42)(typescript@5.8.3)':
dependencies: dependencies:
'@commitlint/format': 19.3.0 '@commitlint/format': 19.3.0
'@commitlint/lint': 19.2.2 '@commitlint/lint': 19.2.2
'@commitlint/load': 19.2.0(@types/node@18.19.42)(typescript@5.7.2) '@commitlint/load': 19.2.0(@types/node@18.19.42)(typescript@5.8.3)
'@commitlint/read': 19.2.1 '@commitlint/read': 19.2.1
'@commitlint/types': 19.0.3 '@commitlint/types': 19.0.3
execa: 8.0.1 execa: 8.0.1
@ -7729,15 +7722,15 @@ snapshots:
'@commitlint/rules': 19.0.3 '@commitlint/rules': 19.0.3
'@commitlint/types': 19.0.3 '@commitlint/types': 19.0.3
'@commitlint/load@19.2.0(@types/node@18.19.42)(typescript@5.7.2)': '@commitlint/load@19.2.0(@types/node@18.19.42)(typescript@5.8.3)':
dependencies: dependencies:
'@commitlint/config-validator': 19.0.3 '@commitlint/config-validator': 19.0.3
'@commitlint/execute-rule': 19.0.0 '@commitlint/execute-rule': 19.0.0
'@commitlint/resolve-extends': 19.1.0 '@commitlint/resolve-extends': 19.1.0
'@commitlint/types': 19.0.3 '@commitlint/types': 19.0.3
chalk: 5.3.0 chalk: 5.3.0
cosmiconfig: 9.0.0(typescript@5.7.2) cosmiconfig: 9.0.0(typescript@5.8.3)
cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2) cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3)
lodash.isplainobject: 4.0.6 lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2 lodash.merge: 4.6.2
lodash.uniq: 4.5.0 lodash.uniq: 4.5.0
@ -10547,27 +10540,24 @@ snapshots:
before-after-hook@2.2.3: {} before-after-hook@2.2.3: {}
better-auth@1.2.4(typescript@5.5.3): better-auth@1.2.6:
dependencies: dependencies:
'@better-auth/utils': 0.2.3 '@better-auth/utils': 0.2.4
'@better-fetch/fetch': 1.1.15 '@better-fetch/fetch': 1.1.18
'@noble/ciphers': 0.6.0 '@noble/ciphers': 0.6.0
'@noble/hashes': 1.7.1 '@noble/hashes': 1.7.1
'@simplewebauthn/browser': 13.1.0 '@simplewebauthn/browser': 13.1.0
'@simplewebauthn/server': 13.1.1 '@simplewebauthn/server': 13.1.1
better-call: 1.0.3 better-call: 1.0.7
defu: 6.1.4 defu: 6.1.4
jose: 5.9.6 jose: 5.9.6
kysely: 0.27.6 kysely: 0.27.6
nanostores: 0.11.3 nanostores: 0.11.3
valibot: 1.0.0-beta.15(typescript@5.5.3)
zod: 3.24.1 zod: 3.24.1
transitivePeerDependencies:
- typescript
better-call@1.0.3: better-call@1.0.7:
dependencies: dependencies:
'@better-fetch/fetch': 1.1.15 '@better-fetch/fetch': 1.1.18
rou3: 0.5.1 rou3: 0.5.1
set-cookie-parser: 2.7.1 set-cookie-parser: 2.7.1
uncrypto: 0.1.3 uncrypto: 0.1.3
@ -10942,21 +10932,21 @@ snapshots:
core-js@3.39.0: {} core-js@3.39.0: {}
cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3):
dependencies: dependencies:
'@types/node': 18.19.42 '@types/node': 18.19.42
cosmiconfig: 9.0.0(typescript@5.7.2) cosmiconfig: 9.0.0(typescript@5.8.3)
jiti: 1.21.6 jiti: 1.21.6
typescript: 5.7.2 typescript: 5.8.3
cosmiconfig@9.0.0(typescript@5.7.2): cosmiconfig@9.0.0(typescript@5.8.3):
dependencies: dependencies:
env-paths: 2.2.1 env-paths: 2.2.1
import-fresh: 3.3.0 import-fresh: 3.3.0
js-yaml: 4.1.0 js-yaml: 4.1.0
parse-json: 5.2.0 parse-json: 5.2.0
optionalDependencies: optionalDependencies:
typescript: 5.7.2 typescript: 5.8.3
cpu-features@0.0.10: cpu-features@0.0.10:
dependencies: dependencies:
@ -14171,7 +14161,7 @@ snapshots:
typescript@5.5.3: {} typescript@5.5.3: {}
typescript@5.7.2: {} typescript@5.8.3: {}
ufo@1.5.4: {} ufo@1.5.4: {}
@ -14292,10 +14282,6 @@ snapshots:
v8-compile-cache-lib@3.0.1: v8-compile-cache-lib@3.0.1:
optional: true optional: true
valibot@1.0.0-beta.15(typescript@5.5.3):
optionalDependencies:
typescript: 5.5.3
vfile-message@4.0.2: vfile-message@4.0.2:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3