@django-stack/cli 0.1.0 → 0.3.0
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.
- package/README.md +1 -1
- package/dist/bin.js +2002 -247
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/{stack-OTrWHXrO.js → stack-CTyqYWCV.js} +122 -34
- package/package.json +2 -1
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/AGENTS.md +2 -1
- package/templates/base/README.md +4 -4
- package/templates/base/compose.yaml +7 -28
- package/templates/base/env.example +7 -4
- package/templates/base/gitignore +1 -0
- package/templates/frontend-astro/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ interface Check {
|
|
|
4
4
|
/** Not a failure: the check could not run or only warns. */
|
|
5
5
|
warning?: boolean;
|
|
6
6
|
detail?: string;
|
|
7
|
+
/** How to fix a failed check, one line each. */
|
|
8
|
+
help?: string[];
|
|
7
9
|
}
|
|
8
10
|
/** Checks a project directory: tools, .env, and the running services. */
|
|
9
11
|
export declare function runDoctor(directory: string): Promise<Check[]>;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as DEFAULT_STACK, u as FRONTENDS, n as NAMED_PRESETS, r as STACK_OPTIONS, b as VERSION, a as decodePreset, o as encodePreset, C as generateProjectSecrets, w as generateSecret, p as normalizeDatabaseUrl,
|
|
1
|
+
export { t as DEFAULT_STACK, u as FRONTENDS, n as NAMED_PRESETS, r as STACK_OPTIONS, b as VERSION, a as decodePreset, o as encodePreset, C as generateProjectSecrets, w as generateSecret, p as normalizeDatabaseUrl, D as parseEnv, s as pendingChoices, x as productionEnv, c as resolvePreset, T as runDoctor, h as scaffold, O as setEnvValues, _ as toProjectName, S as validateDomain, v as validateProjectName } from "./stack-CTyqYWCV.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
2
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { join, relative } from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
// src/env.ts
|
|
@@ -28,17 +28,10 @@ function setEnvValues(text, values) {
|
|
|
28
28
|
}
|
|
29
29
|
return result;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"API_SECRET_KEY",
|
|
36
|
-
"API_READ_KEY",
|
|
37
|
-
"PREVIEW_SECRET",
|
|
38
|
-
"REVALIDATE_SECRET",
|
|
39
|
-
"SITE_URL"
|
|
40
|
-
];
|
|
41
|
-
function command(cmd, args, cwd) {
|
|
31
|
+
/** The oldest Python the backend runs on. */
|
|
32
|
+
const MIN_PYTHON = "3.12";
|
|
33
|
+
/** Runs a command and returns its trimmed output, or undefined when it fails. */
|
|
34
|
+
const run = (cmd, args, cwd) => {
|
|
42
35
|
try {
|
|
43
36
|
return execFileSync(cmd, args, {
|
|
44
37
|
cwd,
|
|
@@ -52,32 +45,69 @@ function command(cmd, args, cwd) {
|
|
|
52
45
|
} catch {
|
|
53
46
|
return;
|
|
54
47
|
}
|
|
48
|
+
};
|
|
49
|
+
/** The first `x.y.z` version in a text, such as `Python 3.13.1` or `v22.11.0`. */
|
|
50
|
+
function parseVersion(text) {
|
|
51
|
+
return text?.match(/(\d+)\.(\d+)(?:\.(\d+))?/)?.[0];
|
|
55
52
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return;
|
|
53
|
+
/** Whether version `a` is at least `b`, compared by number per part. */
|
|
54
|
+
function atLeast(a, b) {
|
|
55
|
+
const left = a.split(".").map(Number);
|
|
56
|
+
const right = b.split(".").map(Number);
|
|
57
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
58
|
+
const x = left[i] ?? 0;
|
|
59
|
+
const y = right[i] ?? 0;
|
|
60
|
+
if (x !== y) return x > y;
|
|
65
61
|
}
|
|
62
|
+
return true;
|
|
66
63
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
function dockerHelp(platform) {
|
|
65
|
+
if (platform === "darwin") return ["Install Docker Desktop: brew install --cask docker", "or https://docs.docker.com/desktop/setup/install/mac-install/"];
|
|
66
|
+
if (platform === "win32") return ["Install Docker Desktop: winget install Docker.DockerDesktop", "or https://docs.docker.com/desktop/setup/install/windows-install/"];
|
|
67
|
+
return ["Install Docker Engine and the Compose plugin: https://docs.docker.com/engine/install/"];
|
|
68
|
+
}
|
|
69
|
+
function dockerStartHelp(platform) {
|
|
70
|
+
if (platform === "linux") return ["Start it: sudo systemctl start docker"];
|
|
71
|
+
return ["Open Docker Desktop and wait until it says it is running."];
|
|
72
|
+
}
|
|
73
|
+
function nodeHelp(platform) {
|
|
74
|
+
return [`Install it with fnm: ${platform === "darwin" ? "brew install fnm" : platform === "win32" ? "winget install Schniz.fnm" : "curl -fsSL https://fnm.vercel.app/install | bash"}, then fnm install 22`, "or https://nodejs.org/en/download"];
|
|
75
|
+
}
|
|
76
|
+
function pythonHelp(platform, hasUv) {
|
|
77
|
+
const python = "uv python install 3.13";
|
|
78
|
+
if (hasUv) return [`Install it with uv: ${python}`];
|
|
79
|
+
return [`Install uv: ${platform === "darwin" ? "brew install uv" : platform === "win32" ? "winget install astral-sh.uv" : "curl -LsSf https://astral.sh/uv/install.sh | sh"}, then ${python}`, "or https://docs.astral.sh/uv/getting-started/installation/"];
|
|
80
|
+
}
|
|
81
|
+
function pythonVersion(runCommand) {
|
|
82
|
+
for (const [cmd, args] of [
|
|
83
|
+
["python3", ["--version"]],
|
|
84
|
+
["python", ["--version"]],
|
|
85
|
+
["py", ["-3", "--version"]]
|
|
86
|
+
]) {
|
|
87
|
+
const version = parseVersion(runCommand(cmd, [...args]));
|
|
88
|
+
if (version) return version;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Docker and Docker Compose, which run the project. */
|
|
92
|
+
function checkDocker({ run: runCommand = run, platform = process.platform } = {}) {
|
|
93
|
+
const client = parseVersion(runCommand("docker", ["--version"]));
|
|
94
|
+
if (!client) return [{
|
|
95
|
+
name: "Docker is installed",
|
|
96
|
+
ok: false,
|
|
97
|
+
help: dockerHelp(platform)
|
|
98
|
+
}];
|
|
99
|
+
const server = runCommand("docker", [
|
|
71
100
|
"version",
|
|
72
101
|
"--format",
|
|
73
102
|
"{{.Server.Version}}"
|
|
74
103
|
]);
|
|
75
|
-
checks
|
|
104
|
+
const checks = [{
|
|
76
105
|
name: "Docker is running",
|
|
77
|
-
ok: Boolean(
|
|
78
|
-
detail:
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
ok: Boolean(server),
|
|
107
|
+
detail: server ? `Docker ${server}` : `Docker ${client} is installed but not running.`,
|
|
108
|
+
help: server ? void 0 : dockerStartHelp(platform)
|
|
109
|
+
}];
|
|
110
|
+
const compose = runCommand("docker", [
|
|
81
111
|
"compose",
|
|
82
112
|
"version",
|
|
83
113
|
"--short"
|
|
@@ -85,8 +115,66 @@ async function runDoctor(directory) {
|
|
|
85
115
|
checks.push({
|
|
86
116
|
name: "Docker Compose v2",
|
|
87
117
|
ok: Boolean(compose),
|
|
88
|
-
detail: compose ? `Compose ${compose}` :
|
|
118
|
+
detail: compose ? `Compose ${compose}` : void 0,
|
|
119
|
+
help: compose ? void 0 : ["Docker Compose v2 is part of Docker Desktop and the docker-compose-plugin package."]
|
|
89
120
|
});
|
|
121
|
+
return checks;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Node.js and Python, which you need to work on the frontend and the backend
|
|
125
|
+
* outside the containers. They only warn: the project runs in Docker without them.
|
|
126
|
+
*/
|
|
127
|
+
function checkDevTools({ run: runCommand = run, platform = process.platform } = {}) {
|
|
128
|
+
const checks = [];
|
|
129
|
+
const node = parseVersion(runCommand("node", ["--version"]));
|
|
130
|
+
const nodeOk = Boolean(node && atLeast(node, "22"));
|
|
131
|
+
checks.push({
|
|
132
|
+
name: `Node.js 22 or later`,
|
|
133
|
+
ok: nodeOk,
|
|
134
|
+
warning: !nodeOk,
|
|
135
|
+
detail: node ? `Node.js ${node}` : "Node.js was not found.",
|
|
136
|
+
help: nodeOk ? void 0 : nodeHelp(platform)
|
|
137
|
+
});
|
|
138
|
+
const python = pythonVersion(runCommand);
|
|
139
|
+
const pythonOk = Boolean(python && atLeast(python, "3.12"));
|
|
140
|
+
checks.push({
|
|
141
|
+
name: `Python ${MIN_PYTHON} or later`,
|
|
142
|
+
ok: pythonOk,
|
|
143
|
+
warning: !pythonOk,
|
|
144
|
+
detail: python ? `Python ${python}` : "Python was not found.",
|
|
145
|
+
help: pythonOk ? void 0 : pythonHelp(platform, Boolean(runCommand("uv", ["--version"])))
|
|
146
|
+
});
|
|
147
|
+
return checks;
|
|
148
|
+
}
|
|
149
|
+
/** Every tool a project needs on this computer. */
|
|
150
|
+
function checkTools(options = {}) {
|
|
151
|
+
return [...checkDocker(options), ...checkDevTools(options)];
|
|
152
|
+
}
|
|
153
|
+
// src/doctor.ts
|
|
154
|
+
const REQUIRED_KEYS = [
|
|
155
|
+
"DJANGO_SECRET_KEY",
|
|
156
|
+
"DATABASE_URL",
|
|
157
|
+
"API_SECRET_KEY",
|
|
158
|
+
"API_READ_KEY",
|
|
159
|
+
"PREVIEW_SECRET",
|
|
160
|
+
"REVALIDATE_SECRET",
|
|
161
|
+
"SITE_URL"
|
|
162
|
+
];
|
|
163
|
+
async function fetchStatus(url, headers = {}) {
|
|
164
|
+
try {
|
|
165
|
+
return (await fetch(url, {
|
|
166
|
+
headers,
|
|
167
|
+
redirect: "manual",
|
|
168
|
+
signal: AbortSignal.timeout(5e3)
|
|
169
|
+
})).status;
|
|
170
|
+
} catch {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Checks a project directory: tools, .env, and the running services. */
|
|
175
|
+
async function runDoctor(directory) {
|
|
176
|
+
const checks = [];
|
|
177
|
+
checks.push(...checkTools());
|
|
90
178
|
const envPath = join(directory, ".env");
|
|
91
179
|
if (!existsSync(envPath)) {
|
|
92
180
|
checks.push({
|
|
@@ -131,7 +219,7 @@ async function runDoctor(directory) {
|
|
|
131
219
|
detail: "Set EMAIL_URL to your SMTP server; Mailpit only runs in development."
|
|
132
220
|
});
|
|
133
221
|
}
|
|
134
|
-
const running =
|
|
222
|
+
const running = run("docker", [
|
|
135
223
|
"compose",
|
|
136
224
|
"ps",
|
|
137
225
|
"--services",
|
|
@@ -148,7 +236,7 @@ async function runDoctor(directory) {
|
|
|
148
236
|
detail: services.length ? services.join(", ") : `Nothing is running. Start the project with \`docker compose up${prod ? " -d" : ""}\`.`
|
|
149
237
|
});
|
|
150
238
|
if (services.length === 0) return checks;
|
|
151
|
-
const health =
|
|
239
|
+
const health = run("docker", [
|
|
152
240
|
"compose",
|
|
153
241
|
"ps",
|
|
154
242
|
backendService,
|
|
@@ -709,4 +797,4 @@ function pendingChoices(stack) {
|
|
|
709
797
|
}];
|
|
710
798
|
});
|
|
711
799
|
}
|
|
712
|
-
export { generateProjectSecrets as C,
|
|
800
|
+
export { generateProjectSecrets as C, parseEnv as D, checkTools as E, setEnvValues as O, validateDomain as S, runDoctor as T, toProjectName as _, decodePreset as a, CLI_VERSION as b, resolvePreset as c, LOCAL_S3 as d, lockBackend as f, timeZones as g, scaffold as h, applies as i, stackOption as l, relativeDirectory as m, NAMED_PRESETS as n, encodePreset as o, normalizeDatabaseUrl as p, STACK_OPTIONS as r, pendingChoices as s, DEFAULT_STACK as t, FRONTENDS as u, validateProjectName as v, generateSecret as w, productionEnv as x, validateTimeZone as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@django-stack/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The production-ready Django, batteries included stack. Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Leon van der Grient",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@bomb.sh/tab": "^0.0.22",
|
|
46
46
|
"@clack/prompts": "^1.8.1",
|
|
47
|
+
"@napi-rs/keyring": "^2.1.2",
|
|
47
48
|
"commander": "^15.0.0",
|
|
48
49
|
"picocolors": "^1.1.1"
|
|
49
50
|
},
|
|
@@ -5,7 +5,7 @@ description = "The Django backend of {{project_name}}."
|
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"Django>=6.1,<6.2",
|
|
8
|
-
"django-stack[server,s3]>=0.
|
|
8
|
+
"django-stack[server,s3]>=0.3.0",
|
|
9
9
|
"django-headless[schema]>=1.1.1",
|
|
10
10
|
"django-content-studio[images,huey]>=1.8.0",
|
|
11
11
|
]
|
package/templates/base/AGENTS.md
CHANGED
|
@@ -43,7 +43,8 @@ Run them at the project root. They need Docker; Node is only needed for the `npm
|
|
|
43
43
|
| Production settings for a server | `npx @django-stack/cli production --domain example.com` |
|
|
44
44
|
|
|
45
45
|
In production (`COMPOSE_PROFILES=prod,db`) the services are `backend-prod`,
|
|
46
|
-
`worker-prod
|
|
46
|
+
`worker-prod` (which also runs the daily backup), `frontend-prod` and
|
|
47
|
+
`proxy`; their containers are
|
|
47
48
|
read-only, so a change goes through the code and a new image, never an edit
|
|
48
49
|
in a running container.
|
|
49
50
|
|
package/templates/base/README.md
CHANGED
|
@@ -37,7 +37,7 @@ the frontend Astro's, with the source mounted from `backend/` and
|
|
|
37
37
|
| Run the tests | `npm test` |
|
|
38
38
|
| Check the setup | `npm run doctor` |
|
|
39
39
|
|
|
40
|
-
With the `dstack` command installed (`npm
|
|
40
|
+
With the `dstack` command installed (`npm i -g @django-stack/cli`),
|
|
41
41
|
`dstack dev`, `dstack check`, `dstack manage <command>` and `dstack types` do
|
|
42
42
|
the same from any directory in the project. See the
|
|
43
43
|
[CLI reference](https://dwarsbit.github.io/django-stack/docs/cli).
|
|
@@ -70,9 +70,9 @@ has every step.
|
|
|
70
70
|
| Task | Command (on the server) |
|
|
71
71
|
|------|---------|
|
|
72
72
|
| Update | `git pull && docker compose up -d --build` |
|
|
73
|
-
| Back up now | `docker compose exec
|
|
74
|
-
| List backups | `docker compose exec
|
|
75
|
-
| Restore | `docker compose exec
|
|
73
|
+
| Back up the database and media now | `docker compose exec worker-prod python manage.py backup_database` |
|
|
74
|
+
| List backups | `docker compose exec worker-prod python manage.py backup_database --list` |
|
|
75
|
+
| Restore | `docker compose exec worker-prod python manage.py restore_database <name> --media` |
|
|
76
76
|
| Logs | `docker compose logs --follow backend-prod` |
|
|
77
77
|
|
|
78
78
|
## Security
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# dev,db `docker compose up` on a laptop: dev servers with hot reload,
|
|
5
5
|
# http://localhost:3000, Mailpit at http://localhost:8025.
|
|
6
6
|
# prod,db `docker compose up -d` on a server: production images behind
|
|
7
|
-
# Caddy on ports 80 and 443, with automatic HTTPS and backups.
|
|
7
|
+
# Caddy on ports 80 and 443, with automatic HTTPS and daily backups.
|
|
8
8
|
# Leave out `db` to use an external Postgres through DATABASE_URL, and add
|
|
9
9
|
# `s3` in development for a local S3 service (see .env).
|
|
10
10
|
#
|
|
@@ -156,11 +156,17 @@ services:
|
|
|
156
156
|
<<: *hardened
|
|
157
157
|
env_file: ${ENV_FILE:-.env}
|
|
158
158
|
command: ["python", "manage.py", "run_huey"]
|
|
159
|
+
# Also runs the daily backup of the database, and of the media volume
|
|
160
|
+
# when uploads are not in S3, at BACKUP_TIME (UTC) into the backups
|
|
161
|
+
# volume and BACKUP_S3_BUCKET. See README.md.
|
|
159
162
|
environment:
|
|
160
163
|
DJANGO_DEBUG: "0"
|
|
161
164
|
NUM_PROXIES: ${NUM_PROXIES:-1}
|
|
165
|
+
BACKUP_DAILY: "1"
|
|
166
|
+
BACKUP_DIR: /app/backups
|
|
162
167
|
volumes:
|
|
163
168
|
- media:/app/media
|
|
169
|
+
- backups:/app/backups
|
|
164
170
|
depends_on:
|
|
165
171
|
backend-prod:
|
|
166
172
|
condition: service_healthy
|
|
@@ -224,33 +230,6 @@ services:
|
|
|
224
230
|
memory: 256M
|
|
225
231
|
restart: unless-stopped
|
|
226
232
|
|
|
227
|
-
# Backs up the database every day at BACKUP_TIME (UTC) into the backups
|
|
228
|
-
# volume, and into BACKUP_S3_BUCKET when it is set. See README.md.
|
|
229
|
-
backup:
|
|
230
|
-
profiles: [prod]
|
|
231
|
-
image: ${BACKEND_IMAGE:-{{project_name}}-backend}
|
|
232
|
-
build:
|
|
233
|
-
context: ./backend
|
|
234
|
-
target: prod
|
|
235
|
-
<<: *hardened
|
|
236
|
-
env_file: ${ENV_FILE:-.env}
|
|
237
|
-
command: ["python", "manage.py", "run_backups"]
|
|
238
|
-
environment:
|
|
239
|
-
DJANGO_DEBUG: "0"
|
|
240
|
-
BACKUP_DIR: /app/backups
|
|
241
|
-
volumes:
|
|
242
|
-
- backups:/app/backups
|
|
243
|
-
depends_on:
|
|
244
|
-
backend-prod:
|
|
245
|
-
condition: service_healthy
|
|
246
|
-
healthcheck:
|
|
247
|
-
disable: true
|
|
248
|
-
deploy:
|
|
249
|
-
resources:
|
|
250
|
-
limits:
|
|
251
|
-
memory: 256M
|
|
252
|
-
restart: unless-stopped
|
|
253
|
-
|
|
254
233
|
# Shared -----------------------------------------------------------------
|
|
255
234
|
|
|
256
235
|
db:
|
|
@@ -71,10 +71,13 @@ S3_ACCESS_KEY_ID=
|
|
|
71
71
|
S3_SECRET_ACCESS_KEY=
|
|
72
72
|
S3_PUBLIC_URL=
|
|
73
73
|
|
|
74
|
-
# Backups (prod profile): a daily pg_dump at BACKUP_TIME (UTC),
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
74
|
+
# Backups (prod profile): a daily pg_dump at BACKUP_TIME (UTC) and, when
|
|
75
|
+
# uploads are on disk (no S3_BUCKET) and BACKUP_MEDIA=1, an archive of the
|
|
76
|
+
# media volume; the newest BACKUP_KEEP of each kept in the backups volume.
|
|
77
|
+
# Set BACKUP_S3_BUCKET to a private bucket (never the public media bucket)
|
|
78
|
+
# to keep a copy off the server; it uses the S3_* credentials unless
|
|
79
|
+
# BACKUP_S3_* ones are set.
|
|
78
80
|
BACKUP_TIME=03:00
|
|
79
81
|
BACKUP_KEEP=14
|
|
82
|
+
BACKUP_MEDIA=1
|
|
80
83
|
BACKUP_S3_BUCKET=
|
package/templates/base/gitignore
CHANGED