@lishugupta652/dokploy 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ PACKAGE_DIR="dokploy-deploy-automation"
5
+ if [ ! -f "$PACKAGE_DIR/scripts/bump-version-on-commit.mjs" ]; then
6
+ PACKAGE_DIR="."
7
+ fi
8
+
9
+ node "$PACKAGE_DIR/scripts/bump-version-on-commit.mjs"
10
+ git add "$PACKAGE_DIR/package.json" "$PACKAGE_DIR/package-lock.json"
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Dokploy
2
+
3
+ YAML-driven CLI for applying Dokploy projects through the Dokploy API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install
9
+ npm run build
10
+ ```
11
+
12
+ Run locally during development:
13
+
14
+ ```bash
15
+ npm run dev -- apply -f examples/simple-app.yaml --dry-run
16
+ ```
17
+
18
+ After build, the CLI binary is `dokploy`.
19
+
20
+ For global install after publishing:
21
+
22
+ ```bash
23
+ npm install -g @lishugupta652/dokploy
24
+ dokploy --help
25
+ ```
26
+
27
+ ## Environment
28
+
29
+ ```bash
30
+ export DOKPLOY_API_KEY=your-api-key
31
+ export DOKPLOY_HOST=https://your-dokploy.example.com/api
32
+ ```
33
+
34
+ `DOKPLOY_HOST` is optional when `host` is set in the YAML file. API keys are never read from config.
35
+
36
+ ## Commands
37
+
38
+ ```bash
39
+ dokploy projects --host https://your-dokploy.example.com/api
40
+ dokploy projects --summary --host https://your-dokploy.example.com/api
41
+ dokploy projects --json --host https://your-dokploy.example.com/api
42
+ dokploy apply -f dokploy.yaml
43
+ dokploy apply -f dokploy.yaml --dry-run
44
+ dokploy deploy frontend -f dokploy.yaml
45
+ dokploy status -f dokploy.yaml
46
+ dokploy destroy frontend -f dokploy.yaml
47
+ dokploy destroy -f dokploy.yaml
48
+ dokploy init
49
+ ```
50
+
51
+ State is written next to the config as `dokploy-state.json` unless `stateFile` or `--state` is provided.
52
+
53
+ ## Publishing
54
+
55
+ The npm package name is `@lishugupta652/dokploy`; the command is `dokploy`.
56
+
57
+ ```bash
58
+ npm publish --access public
59
+ ```
60
+
61
+ `prepublishOnly` runs the TypeScript check and build before publishing.
62
+
63
+ ## Commit Version Hook
64
+
65
+ Install the repo hook once:
66
+
67
+ ```bash
68
+ npm run hooks:install
69
+ ```
70
+
71
+ The pre-commit hook bumps the package patch version and stages `package.json` plus `package-lock.json`. Set `SKIP_DOKPLOY_VERSION_BUMP=1` to skip it for a commit.
72
+
73
+ ## Notes
74
+
75
+ - `apply` is idempotent by name within the selected environment where Dokploy exposes search endpoints.
76
+ - Applications are configured in Dokploy's required order: create, source provider, build type, environment, domains, deploy.
77
+ - Database creation requires a password because the Dokploy API requires one on create.
package/dist/client.js ADDED
@@ -0,0 +1,106 @@
1
+ export class DokployApiError extends Error {
2
+ status;
3
+ endpoint;
4
+ responseBody;
5
+ constructor(message, status, endpoint, responseBody) {
6
+ super(message);
7
+ this.status = status;
8
+ this.endpoint = endpoint;
9
+ this.responseBody = responseBody;
10
+ this.name = "DokployApiError";
11
+ }
12
+ }
13
+ export class DokployClient {
14
+ baseUrl;
15
+ apiKey;
16
+ constructor(options) {
17
+ this.baseUrl = options.host.replace(/\/+$/, "");
18
+ this.apiKey = options.apiKey;
19
+ }
20
+ async get(endpoint, query) {
21
+ return this.request("GET", endpoint, undefined, query);
22
+ }
23
+ async post(endpoint, body) {
24
+ return this.request("POST", endpoint, body);
25
+ }
26
+ async request(method, endpoint, body, query) {
27
+ const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
28
+ const url = new URL(`${this.baseUrl}${normalizedEndpoint}`);
29
+ if (query) {
30
+ for (const [key, value] of Object.entries(query)) {
31
+ if (value !== undefined && value !== null) {
32
+ url.searchParams.set(key, String(value));
33
+ }
34
+ }
35
+ }
36
+ const headers = {
37
+ "x-api-key": this.apiKey,
38
+ accept: "application/json",
39
+ };
40
+ const init = {
41
+ method,
42
+ headers,
43
+ };
44
+ if (method === "POST") {
45
+ headers["content-type"] = "application/json";
46
+ init.body = JSON.stringify(body ?? {});
47
+ }
48
+ const response = await fetch(url, init);
49
+ const text = await response.text();
50
+ if (!response.ok) {
51
+ throw new DokployApiError(this.formatErrorMessage(response.status, normalizedEndpoint, text), response.status, normalizedEndpoint, text);
52
+ }
53
+ if (!text.trim()) {
54
+ return undefined;
55
+ }
56
+ const parsed = JSON.parse(text);
57
+ return this.unwrapResponse(parsed);
58
+ }
59
+ unwrapResponse(value) {
60
+ if (!isRecord(value))
61
+ return value;
62
+ const result = value.result;
63
+ if (isRecord(result)) {
64
+ const data = result.data;
65
+ if (isRecord(data) && "json" in data)
66
+ return data.json;
67
+ if (data !== undefined)
68
+ return data;
69
+ }
70
+ const data = value.data;
71
+ if (isRecord(data) && "json" in data)
72
+ return data.json;
73
+ if ("json" in value)
74
+ return value.json;
75
+ return value;
76
+ }
77
+ formatErrorMessage(status, endpoint, body) {
78
+ const detail = extractErrorDetail(body);
79
+ const hint = status === 401 || status === 403
80
+ ? " Check DOKPLOY_API_KEY and the API key permissions."
81
+ : "";
82
+ return `Dokploy API ${endpoint} failed with HTTP ${status}${detail ? `: ${detail}` : ""}.${hint}`;
83
+ }
84
+ }
85
+ function extractErrorDetail(body) {
86
+ if (!body.trim())
87
+ return undefined;
88
+ try {
89
+ const parsed = JSON.parse(body);
90
+ if (isRecord(parsed)) {
91
+ const message = parsed.message;
92
+ if (typeof message === "string")
93
+ return message;
94
+ const error = parsed.error;
95
+ if (isRecord(error) && typeof error.message === "string")
96
+ return error.message;
97
+ }
98
+ }
99
+ catch {
100
+ return body.slice(0, 400);
101
+ }
102
+ return body.slice(0, 400);
103
+ }
104
+ function isRecord(value) {
105
+ return typeof value === "object" && value !== null && !Array.isArray(value);
106
+ }
@@ -0,0 +1,38 @@
1
+ import { createRuntime } from "../runtime.js";
2
+ import { writeState } from "../state.js";
3
+ import { ensureBackupDestination, ensureScheduledBackup, ensureVolumeBackup } from "../resources/backup.js";
4
+ import { ensureApplication } from "../resources/application.js";
5
+ import { ensureCompose } from "../resources/compose.js";
6
+ import { ensureDatabase } from "../resources/database.js";
7
+ import { ensureEnvironment } from "../resources/environment.js";
8
+ import { ensureProject } from "../resources/project.js";
9
+ export async function applyCommand(options) {
10
+ const runtime = await createRuntime(options);
11
+ runtime.log.step("Applying Dokploy config");
12
+ const project = await ensureProject(runtime.config.project, runtime);
13
+ const environment = await ensureEnvironment(runtime.config.environment, project, runtime);
14
+ for (const database of runtime.config.databases) {
15
+ await ensureDatabase(database, environment, runtime);
16
+ }
17
+ for (const app of runtime.config.applications) {
18
+ await ensureApplication(app, environment, runtime);
19
+ }
20
+ for (const compose of runtime.config.compose) {
21
+ await ensureCompose(compose, environment, runtime);
22
+ }
23
+ for (const destination of runtime.config.backupDestinations) {
24
+ await ensureBackupDestination(destination, runtime);
25
+ }
26
+ for (const backup of runtime.config.backups) {
27
+ await ensureScheduledBackup(backup, runtime);
28
+ }
29
+ for (const backup of runtime.config.volumeBackups) {
30
+ await ensureVolumeBackup(backup, runtime);
31
+ }
32
+ if (runtime.dryRun) {
33
+ runtime.log.warn(`Dry run only. State was not written to ${runtime.statePath}.`);
34
+ return;
35
+ }
36
+ await writeState(runtime.statePath, runtime.state);
37
+ runtime.log.success(`State written to ${runtime.statePath}`);
38
+ }
@@ -0,0 +1,17 @@
1
+ import { createRuntime } from "../runtime.js";
2
+ import { redeployApplication } from "../resources/application.js";
3
+ import { redeployCompose } from "../resources/compose.js";
4
+ export async function deployCommand(name, options) {
5
+ const runtime = await createRuntime(options);
6
+ const app = runtime.state.applications[name];
7
+ if (app) {
8
+ await redeployApplication(app, runtime);
9
+ return;
10
+ }
11
+ const compose = runtime.state.compose[name];
12
+ if (compose) {
13
+ await redeployCompose(compose, runtime);
14
+ return;
15
+ }
16
+ throw new Error(`No application or compose named ${name} found in ${runtime.statePath}. Run apply first.`);
17
+ }
@@ -0,0 +1,73 @@
1
+ import { destroyApplication } from "../resources/application.js";
2
+ import { destroyBackupDestination, destroyScheduledBackup, destroyVolumeBackup, } from "../resources/backup.js";
3
+ import { destroyCompose } from "../resources/compose.js";
4
+ import { destroyDatabase } from "../resources/database.js";
5
+ import { destroyEnvironment } from "../resources/environment.js";
6
+ import { destroyProject } from "../resources/project.js";
7
+ import { createRuntime } from "../runtime.js";
8
+ import { writeState } from "../state.js";
9
+ export async function destroyCommand(target, options) {
10
+ const runtime = await createRuntime(options);
11
+ const deleteVolumes = Boolean(options.deleteVolumes);
12
+ if (target) {
13
+ await destroyOne(target, runtime, deleteVolumes);
14
+ }
15
+ else {
16
+ await destroyAll(runtime, deleteVolumes);
17
+ }
18
+ if (runtime.dryRun) {
19
+ runtime.log.warn(`Dry run only. State was not written to ${runtime.statePath}.`);
20
+ return;
21
+ }
22
+ await writeState(runtime.statePath, runtime.state);
23
+ runtime.log.success(`State written to ${runtime.statePath}`);
24
+ }
25
+ async function destroyOne(target, runtime, deleteVolumes) {
26
+ if (runtime.state.volumeBackups[target]) {
27
+ await destroyVolumeBackup(target, runtime);
28
+ return;
29
+ }
30
+ if (runtime.state.backups[target]) {
31
+ await destroyScheduledBackup(target, runtime);
32
+ return;
33
+ }
34
+ if (runtime.state.applications[target]) {
35
+ await destroyApplication(target, runtime);
36
+ return;
37
+ }
38
+ if (runtime.state.compose[target]) {
39
+ await destroyCompose(target, runtime, deleteVolumes);
40
+ return;
41
+ }
42
+ if (runtime.state.databases[target]) {
43
+ await destroyDatabase(target, runtime);
44
+ return;
45
+ }
46
+ if (runtime.state.backupDestinations[target]) {
47
+ await destroyBackupDestination(target, runtime);
48
+ return;
49
+ }
50
+ throw new Error(`No managed resource named ${target} found in ${runtime.statePath}.`);
51
+ }
52
+ async function destroyAll(runtime, deleteVolumes) {
53
+ for (const name of Object.keys(runtime.state.volumeBackups).reverse()) {
54
+ await destroyVolumeBackup(name, runtime);
55
+ }
56
+ for (const name of Object.keys(runtime.state.backups).reverse()) {
57
+ await destroyScheduledBackup(name, runtime);
58
+ }
59
+ for (const name of Object.keys(runtime.state.compose).reverse()) {
60
+ await destroyCompose(name, runtime, deleteVolumes);
61
+ }
62
+ for (const name of Object.keys(runtime.state.applications).reverse()) {
63
+ await destroyApplication(name, runtime);
64
+ }
65
+ for (const name of Object.keys(runtime.state.databases).reverse()) {
66
+ await destroyDatabase(name, runtime);
67
+ }
68
+ for (const name of Object.keys(runtime.state.backupDestinations).reverse()) {
69
+ await destroyBackupDestination(name, runtime);
70
+ }
71
+ await destroyEnvironment(runtime);
72
+ await destroyProject(runtime);
73
+ }
@@ -0,0 +1,51 @@
1
+ import { access, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function initCommand(options) {
4
+ const output = path.resolve(options.output ?? "dokploy.yaml");
5
+ if (!options.force && (await exists(output))) {
6
+ throw new Error(`${output} already exists. Pass --force to overwrite it.`);
7
+ }
8
+ await writeFile(output, starterConfig, "utf8");
9
+ console.log(`Wrote ${output}`);
10
+ }
11
+ async function exists(filePath) {
12
+ try {
13
+ await access(filePath);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ const starterConfig = `host: https://your-dokploy.example.com/api
21
+
22
+ project:
23
+ name: My App
24
+ description: Deployed via dokploy
25
+
26
+ environment:
27
+ name: production
28
+
29
+ applications:
30
+ - name: frontend
31
+ source: github
32
+ github:
33
+ id: your-github-provider-id
34
+ owner: your-org
35
+ repository: your-repo
36
+ branch: main
37
+ buildPath: /
38
+ build:
39
+ type: dockerfile
40
+ dockerfile: Dockerfile
41
+ contextPath: .
42
+ env:
43
+ NODE_ENV: production
44
+ domains:
45
+ - host: app.example.com
46
+ port: 80
47
+ path: /
48
+ https: true
49
+ certificate: letsencrypt
50
+ deploy: true
51
+ `;
@@ -0,0 +1,57 @@
1
+ import chalk from "chalk";
2
+ import { asArray } from "../resources/common.js";
3
+ import { createClientRuntime } from "../runtime.js";
4
+ export async function projectsCommand(options) {
5
+ const runtime = await createClientRuntime(options);
6
+ const projects = await runtime.client.get("project.all");
7
+ if (options.json) {
8
+ console.log(JSON.stringify(projects, null, 2));
9
+ return;
10
+ }
11
+ runtime.log.header("Dokploy Projects", runtime.host);
12
+ const projectList = asArray(projects);
13
+ if (projectList.length === 0) {
14
+ runtime.log.warn("No projects returned by project.all.");
15
+ runtime.log.section("Raw Response");
16
+ runtime.log.json(projects);
17
+ return;
18
+ }
19
+ runtime.log.table(projectList.map((project) => ({
20
+ name: stringField(project, "name") ?? "unnamed",
21
+ id: chalk.gray(stringField(project, "projectId") ?? stringField(project, "id") ?? "unknown-id"),
22
+ environments: countNested(project, ["environments", "environment"]),
23
+ applications: countNested(project, ["applications", "application"]),
24
+ compose: countNested(project, ["compose", "composes"]),
25
+ databases: countNested(project, ["databases", "postgres", "mysql", "mariadb", "mongo", "redis"]),
26
+ description: stringField(project, "description") ?? "",
27
+ })), [
28
+ { key: "name", label: "Project" },
29
+ { key: "id", label: "ID" },
30
+ { key: "environments", label: "Env" },
31
+ { key: "applications", label: "Apps" },
32
+ { key: "compose", label: "Compose" },
33
+ { key: "databases", label: "DB" },
34
+ { key: "description", label: "Description" },
35
+ ]);
36
+ if (!options.summary) {
37
+ runtime.log.section("Full Response");
38
+ runtime.log.json(projects);
39
+ }
40
+ }
41
+ function stringField(project, key) {
42
+ const value = project[key];
43
+ return typeof value === "string" && value.length > 0 ? value : undefined;
44
+ }
45
+ function countNested(project, keys) {
46
+ let total = 0;
47
+ for (const key of keys) {
48
+ const value = project[key];
49
+ if (Array.isArray(value)) {
50
+ total += value.length;
51
+ }
52
+ else if (value && typeof value === "object") {
53
+ total += 1;
54
+ }
55
+ }
56
+ return total === 0 ? chalk.gray("-") : chalk.yellow(String(total));
57
+ }
@@ -0,0 +1,84 @@
1
+ import chalk from "chalk";
2
+ import { DokployApiError } from "../client.js";
3
+ import { getOneById } from "../resources/common.js";
4
+ import { createRuntime } from "../runtime.js";
5
+ export async function statusCommand(options) {
6
+ const runtime = await createRuntime(options);
7
+ const rows = [];
8
+ runtime.log.header("Dokploy Status", runtime.statePath);
9
+ if (runtime.state.project) {
10
+ rows.push(await getStatus(runtime.state.project, "project", "project.one", "projectId", runtime));
11
+ }
12
+ if (runtime.state.environment) {
13
+ rows.push(await getStatus(runtime.state.environment, "environment", "environment.one", "environmentId", runtime));
14
+ }
15
+ for (const ref of Object.values(runtime.state.databases)) {
16
+ const idField = `${ref.type}Id`;
17
+ rows.push(await getStatus(ref, `${ref.type} database`, `${ref.type}.one`, idField, runtime));
18
+ }
19
+ for (const ref of Object.values(runtime.state.applications)) {
20
+ rows.push(await getStatus(ref, "application", "application.one", "applicationId", runtime));
21
+ }
22
+ for (const ref of Object.values(runtime.state.compose)) {
23
+ rows.push(await getStatus(ref, "compose", "compose.one", "composeId", runtime));
24
+ }
25
+ for (const ref of Object.values(runtime.state.backupDestinations)) {
26
+ rows.push(await getStatus(ref, "backup destination", "destination.one", "destinationId", runtime));
27
+ }
28
+ for (const ref of Object.values(runtime.state.backups)) {
29
+ rows.push(await getStatus(ref, "backup", "backup.one", "backupId", runtime));
30
+ }
31
+ for (const ref of Object.values(runtime.state.volumeBackups)) {
32
+ rows.push(await getStatus(ref, "volume backup", "volumeBackups.one", "volumeBackupId", runtime));
33
+ }
34
+ runtime.log.table(rows, [
35
+ { key: "type", label: "Type" },
36
+ { key: "name", label: "Name" },
37
+ { key: "status", label: "Status" },
38
+ { key: "id", label: "ID" },
39
+ ]);
40
+ }
41
+ async function getStatus(ref, label, endpoint, idField, runtime) {
42
+ try {
43
+ const remote = await getOneById(runtime.client, endpoint, idField, ref.id);
44
+ if (!remote) {
45
+ return statusRow(ref, label, "missing");
46
+ }
47
+ const status = pickStatus(remote);
48
+ return statusRow(ref, label, status ?? "exists");
49
+ }
50
+ catch (error) {
51
+ if (error instanceof DokployApiError && error.status === 404) {
52
+ return statusRow(ref, label, "missing");
53
+ }
54
+ throw error;
55
+ }
56
+ }
57
+ function pickStatus(remote) {
58
+ for (const key of ["applicationStatus", "composeStatus", "status"]) {
59
+ const value = remote[key];
60
+ if (typeof value === "string" && value.length > 0)
61
+ return value;
62
+ }
63
+ return undefined;
64
+ }
65
+ function statusRow(ref, type, status) {
66
+ return {
67
+ type,
68
+ name: ref.name,
69
+ status: colorStatus(status),
70
+ id: chalk.gray(ref.id),
71
+ };
72
+ }
73
+ function colorStatus(status) {
74
+ if (status === "exists" || status === "done" || status === "running") {
75
+ return chalk.green(status);
76
+ }
77
+ if (status === "missing" || status === "error") {
78
+ return chalk.red(status);
79
+ }
80
+ if (status === "idle") {
81
+ return chalk.cyan(status);
82
+ }
83
+ return status;
84
+ }