@django-stack/cli 0.12.0 → 0.12.2

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/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as describeMove, A as activeProject, B as DEFAULT_STACK, C as CLI_VERSION, D as CAPTCHA_APP, E as generateSecret, F as projectStack, G as applies, H as NODE_FRONTENDS, I as projectUrls, J as pendingChoices, L as readLocalRun, M as composeOutput, N as findProjectDirectory, O as RUN_DOMAIN, P as parseProfiles, Q as chooseFreePorts, R as readProject, S as validateTimeZone, T as generateProjectSecrets, U as PROMPT_ORDER, V as NAMED_PRESETS, W as STACK_OPTIONS, X as stackOption, Y as resolvePreset, Z as DEV_PORTS, _ as toProjectName, at as dockerRunning, b as usesValkey, c as frontendPackageManager, ct as quoteEnvValue, d as lockFrontend, et as envPorts, f as normalizeDatabaseUrl, g as timeZones, h as setNotificationsApp, i as validateDomain, it as dockerNotRunning, j as compose, k as RUN_ENV, l as localS3, lt as setEnvValues, m as scaffold, n as postgresUrl, nt as runDoctor, o as LOCAL_S3, ot as isDockerDown, p as relativeDirectory, q as encodePreset, r as productionEnv$1, rt as checkTools, s as cacheEnv, st as parseEnv, t as missingS3, tt as portValues, u as lockBackend, v as umamiEnv, w as PACKAGE_NAME, x as validateProjectName, y as updateFrontendLock, z as runningServices } from "./production-1O8-1QUl.js";
2
+ import { $ as describeMove, A as activeProject, B as DEFAULT_STACK, C as CLI_VERSION, D as CAPTCHA_APP, E as generateSecret, F as projectStack, G as applies, H as NODE_FRONTENDS, I as projectUrls, J as pendingChoices, L as readLocalRun, M as composeOutput, N as findProjectDirectory, O as RUN_DOMAIN, P as parseProfiles, Q as chooseFreePorts, R as readProject, S as validateTimeZone, T as generateProjectSecrets, U as PROMPT_ORDER, V as NAMED_PRESETS, W as STACK_OPTIONS, X as stackOption, Y as resolvePreset, Z as DEV_PORTS, _ as toProjectName, at as dockerRunning, b as usesValkey, c as frontendPackageManager, ct as removeLeftovers, d as lockFrontend, dt as setEnvValues, et as envPorts, f as normalizeDatabaseUrl, g as timeZones, h as setNotificationsApp, i as validateDomain, it as dockerNotRunning, j as compose, k as RUN_ENV, l as localS3, lt as parseEnv, m as scaffold, n as postgresUrl, nt as runDoctor, o as LOCAL_S3, ot as isDockerDown, p as relativeDirectory, q as encodePreset, r as productionEnv$1, rt as checkTools, s as cacheEnv, st as leftoverVolumes, t as missingS3, tt as portValues, u as lockBackend, ut as quoteEnvValue, v as umamiEnv, w as PACKAGE_NAME, x as validateProjectName, y as updateFrontendLock, z as runningServices } from "./production-lgZRSpyw.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { basename, delimiter, dirname, join, posix, relative, resolve, sep } from "node:path";
@@ -1029,6 +1029,24 @@ function probeRequest(probe, failureThreshold) {
1029
1029
  ...check
1030
1030
  };
1031
1031
  }
1032
+ /**
1033
+ * Which registry a Magic Containers registry is. Bunny returns no type, so
1034
+ * this goes by the host name, and by the display name or a type field when
1035
+ * there is no host name. Public Docker Hub and GitHub are built in.
1036
+ */
1037
+ function registryKind(item) {
1038
+ const host = (text(item.hostName) ?? text(item.host) ?? text(item.url) ?? "").toLowerCase();
1039
+ const label = `${text(item.type) ?? ""} ${text(item.displayName) ?? ""}`.toLowerCase().replace(/[\s_-]/g, "");
1040
+ if (host) {
1041
+ if (/(^|\.|\/)(docker\.io|docker\.com)(\/|$)/.test(host)) return "dockerHub";
1042
+ if (/(^|\.|\/)ghcr\.io(\/|$)/.test(host)) return "gitHub";
1043
+ if (/(^|\.|\/)bunny\.net(\/|$)/.test(host)) return "bunny";
1044
+ return;
1045
+ }
1046
+ if (label.includes("dockerhub")) return "dockerHub";
1047
+ if (label.includes("github") || label.includes("ghcr")) return "gitHub";
1048
+ if (label.includes("bunny")) return "bunny";
1049
+ }
1032
1050
  /** The image fields of a container. */
1033
1051
  function imageFields(container, options) {
1034
1052
  const { image } = container;
@@ -1310,25 +1328,29 @@ var BunnyClient = class {
1310
1328
  const list = listOf(await this.api.mc("GET", "/registries"));
1311
1329
  const registries = {};
1312
1330
  let namespace;
1313
- for (const item of list) {
1314
- const type = (text(item.type) ?? "").toLowerCase();
1331
+ const listed = [];
1332
+ const ordered = [...list].sort((a, b) => Number(b.isPublic === true) - Number(a.isPublic === true));
1333
+ for (const item of ordered) {
1315
1334
  const id = text(item.id);
1316
1335
  if (!id) continue;
1317
- if (type === "dockerhub" && !registries.dockerHub) registries.dockerHub = id;
1318
- else if (type === "github" && !registries.gitHub) registries.gitHub = id;
1319
- else if ((type === "bunnyregistry" || type === "bunny") && !registries.bunny) {
1336
+ const kind = registryKind(item);
1337
+ listed.push(text(item.displayName) ?? text(item.hostName) ?? id);
1338
+ if (kind === "dockerHub" && !registries.dockerHub) registries.dockerHub = id;
1339
+ else if (kind === "gitHub" && !registries.gitHub) registries.gitHub = id;
1340
+ else if (kind === "bunny" && !registries.bunny) {
1320
1341
  registries.bunny = id;
1321
1342
  namespace = text(item.namespace ?? item.accountId ?? item.userName ?? item.username);
1322
1343
  }
1323
1344
  }
1324
1345
  return {
1325
1346
  registries,
1326
- namespace
1347
+ namespace,
1348
+ listed
1327
1349
  };
1328
1350
  }
1329
1351
  async ensureRegistries() {
1330
1352
  const found = await this.registries();
1331
- if (!found.registries.dockerHub) throw new Error("Bunny lists no public Docker Hub registry for this account. Check Magic Containers › Image Registries.");
1353
+ if (!found.registries.dockerHub) throw new Error(`Could not find the public Docker Hub registry among the registries Bunny returned (${found.listed.join(", ") || "none"}). Please report this at https://github.com/dwarsbit/django-stack/issues.`);
1332
1354
  if (!found.registries.bunny) {
1333
1355
  const created = await this.api.mc("POST", "/registries", {
1334
1356
  displayName: "Bunny Container Registry",
@@ -3978,6 +4000,24 @@ function checkComputer(checks = checkTools()) {
3978
4000
  });
3979
4001
  return !checks.some((check) => !check.ok && !check.warning);
3980
4002
  }
4003
+ /**
4004
+ * Docker keeps the volumes of a deleted project. A new project with the same
4005
+ * name would start on the old database, whose password is not the new one in
4006
+ * .env: "password authentication failed for user app". Offers to remove them.
4007
+ */
4008
+ async function handleLeftovers(name, interactive) {
4009
+ const volumes = leftoverVolumes(name);
4010
+ if (!volumes.length) return;
4011
+ const removeCommand = command("docker compose down --volumes");
4012
+ p.log.warn(`Docker still has the data of an earlier project named ${pc.bold(name)} (${volumes.join(", ")}). The new project gets a new database password, so it cannot start on that database.`);
4013
+ if (interactive && unwrap(await p.confirm({
4014
+ message: `Remove the data of the earlier ${name}? Its database and uploads are deleted for good.`,
4015
+ initialValue: false
4016
+ }))) {
4017
+ if (removeLeftovers(name, volumes)) p.log.success(`Removed the data of the earlier ${name}.`);
4018
+ else fail("Could not remove it. Stop what uses it, or choose another --name.");
4019
+ } else p.log.warn(`Run ${removeCommand} in the new project before you start it, or choose another --name.`);
4020
+ }
3981
4021
  async function init(directoryArgument, options) {
3982
4022
  const interactive = isInteractive(options.yes);
3983
4023
  intro("Create a project");
@@ -3999,6 +4039,7 @@ async function init(directoryArgument, options) {
3999
4039
  validate: (value) => validateProjectName(value ?? "")
4000
4040
  }));
4001
4041
  }
4042
+ if (dockerReady) await handleLeftovers(name, interactive);
4002
4043
  const stack = await chooseStack(options, interactive);
4003
4044
  if (!applies("analytics", stack) && stack.analytics !== "off") {
4004
4045
  if (options.fromCli("analytics")) fail("--analytics umami needs a frontend: a headless project has no pages to count.");
@@ -5066,6 +5107,12 @@ function nodeModulesVolume(project) {
5066
5107
  return;
5067
5108
  }
5068
5109
  }
5110
+ /** The backend packages `dstack update` upgrades without `--all`. */
5111
+ const BACKEND_PACKAGES = [
5112
+ "django-stack",
5113
+ "django-headless",
5114
+ "django-content-studio"
5115
+ ];
5069
5116
  /** `dstack update`: newer images, newer DjangoStack packages, and a restart of what was running. */
5070
5117
  async function update(options) {
5071
5118
  const project = open(options);
@@ -5074,16 +5121,12 @@ async function update(options) {
5074
5121
  const running = targets.filter((target) => runningServices(target).length > 0);
5075
5122
  const lock = join(project.directory, "backend", "uv.lock");
5076
5123
  if (existsSync(lock) && has("uv")) {
5077
- const args = options.all ? ["lock", "--upgrade"] : [
5078
- "lock",
5079
- "--upgrade-package",
5080
- "django-stack"
5081
- ];
5124
+ const args = options.all ? ["lock", "--upgrade"] : ["lock", ...BACKEND_PACKAGES.flatMap((name) => ["--upgrade-package", name])];
5082
5125
  if (spawnSync("uv", args, {
5083
5126
  cwd: join(project.directory, "backend"),
5084
5127
  stdio: "inherit"
5085
5128
  }).status !== 0) fail("uv could not update backend/uv.lock.");
5086
- p.log.success(options.all ? "Updated backend/uv.lock" : "Updated django-stack in backend/uv.lock");
5129
+ p.log.success(options.all ? "Updated backend/uv.lock" : `Updated ${BACKEND_PACKAGES.join(", ")} in backend/uv.lock`);
5087
5130
  } else if (existsSync(lock)) p.log.warn("uv is not installed, so backend/uv.lock stays as it is: https://docs.astral.sh/uv/");
5088
5131
  if (frontendPackageManager(project.directory)) {
5089
5132
  const lockfile = updateFrontendLock(project.directory, options.all);
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { B as DEFAULT_STACK, a as FRONTENDS, V as NAMED_PRESETS, W as STACK_OPTIONS, C as VERSION, s as cacheEnv, K as decodePreset, q as encodePreset, T as generateProjectSecrets, E as generateSecret, f as normalizeDatabaseUrl, st as parseEnv, J as pendingChoices, r as productionEnv, Y as resolvePreset, nt as runDoctor, m as scaffold, lt as setEnvValues, h as setNotificationsApp, _ as toProjectName, b as usesValkey, i as validateDomain, x as validateProjectName } from "./production-1O8-1QUl.js";
1
+ export { B as DEFAULT_STACK, a as FRONTENDS, V as NAMED_PRESETS, W as STACK_OPTIONS, C as VERSION, s as cacheEnv, K as decodePreset, q as encodePreset, T as generateProjectSecrets, E as generateSecret, f as normalizeDatabaseUrl, lt as parseEnv, J as pendingChoices, r as productionEnv, Y as resolvePreset, nt as runDoctor, m as scaffold, dt as setEnvValues, h as setNotificationsApp, _ as toProjectName, b as usesValkey, i as validateDomain, x as validateProjectName } from "./production-lgZRSpyw.js";
@@ -117,6 +117,40 @@ function dockerRunning(runCommand = run) {
117
117
  "{{.Server.Version}}"
118
118
  ]));
119
119
  }
120
+ /**
121
+ * The Docker volumes of an earlier Compose project with this name, such as
122
+ * `<name>_pgdata`. A new project with the same name would reuse them, and
123
+ * Postgres keeps the password of the data it was created with.
124
+ */
125
+ function leftoverVolumes(name, runCommand = run) {
126
+ return (runCommand("docker", [
127
+ "volume",
128
+ "ls",
129
+ "--quiet",
130
+ "--filter",
131
+ `label=com.docker.compose.project=${name}`
132
+ ]) ?? "").split("\n").filter(Boolean);
133
+ }
134
+ /** Removes the containers and volumes of the Compose project with this name. Returns whether it worked. */
135
+ function removeLeftovers(name, volumes, runCommand = run) {
136
+ const containers = (runCommand("docker", [
137
+ "ps",
138
+ "--all",
139
+ "--quiet",
140
+ "--filter",
141
+ `label=com.docker.compose.project=${name}`
142
+ ]) ?? "").split("\n").filter(Boolean);
143
+ if (containers.length && runCommand("docker", [
144
+ "rm",
145
+ "--force",
146
+ ...containers
147
+ ]) === void 0) return false;
148
+ return runCommand("docker", [
149
+ "volume",
150
+ "rm",
151
+ ...volumes
152
+ ]) !== void 0;
153
+ }
120
154
  /** Docker and Docker Compose, which run the project. */
121
155
  function checkDocker({ run: runCommand = run, platform = process.platform } = {}) {
122
156
  const client = parseVersion(runCommand("docker", ["--version"]));
@@ -1746,4 +1780,4 @@ function productionEnv(devEnv, options) {
1746
1780
  todo
1747
1781
  };
1748
1782
  }
1749
- export { describeMove as $, activeProject as A, DEFAULT_STACK as B, CLI_VERSION as C, CAPTCHA_APP as D, generateSecret as E, projectStack as F, applies as G, NODE_FRONTENDS as H, projectUrls as I, pendingChoices as J, decodePreset as K, readLocalRun as L, composeOutput as M, findProjectDirectory as N, RUN_DOMAIN as O, parseProfiles as P, chooseFreePorts as Q, readProject as R, validateTimeZone as S, generateProjectSecrets as T, PROMPT_ORDER as U, NAMED_PRESETS as V, STACK_OPTIONS as W, stackOption as X, resolvePreset as Y, DEV_PORTS as Z, toProjectName as _, FRONTENDS as a, dockerRunning as at, usesValkey as b, frontendPackageManager as c, quoteEnvValue as ct, lockFrontend as d, envPorts as et, normalizeDatabaseUrl as f, timeZones as g, setNotificationsApp as h, validateDomain as i, dockerNotRunning as it, compose as j, RUN_ENV as k, localS3 as l, setEnvValues as lt, scaffold as m, postgresUrl as n, runDoctor as nt, LOCAL_S3 as o, isDockerDown as ot, relativeDirectory as p, encodePreset as q, productionEnv as r, checkTools as rt, cacheEnv as s, parseEnv as st, missingS3 as t, portValues as tt, lockBackend as u, umamiEnv as v, PACKAGE_NAME as w, validateProjectName as x, updateFrontendLock as y, runningServices as z };
1783
+ export { describeMove as $, activeProject as A, DEFAULT_STACK as B, CLI_VERSION as C, CAPTCHA_APP as D, generateSecret as E, projectStack as F, applies as G, NODE_FRONTENDS as H, projectUrls as I, pendingChoices as J, decodePreset as K, readLocalRun as L, composeOutput as M, findProjectDirectory as N, RUN_DOMAIN as O, parseProfiles as P, chooseFreePorts as Q, readProject as R, validateTimeZone as S, generateProjectSecrets as T, PROMPT_ORDER as U, NAMED_PRESETS as V, STACK_OPTIONS as W, stackOption as X, resolvePreset as Y, DEV_PORTS as Z, toProjectName as _, FRONTENDS as a, dockerRunning as at, usesValkey as b, frontendPackageManager as c, removeLeftovers as ct, lockFrontend as d, setEnvValues as dt, envPorts as et, normalizeDatabaseUrl as f, timeZones as g, setNotificationsApp as h, validateDomain as i, dockerNotRunning as it, compose as j, RUN_ENV as k, localS3 as l, parseEnv as lt, scaffold as m, postgresUrl as n, runDoctor as nt, LOCAL_S3 as o, isDockerDown as ot, relativeDirectory as p, encodePreset as q, productionEnv as r, checkTools as rt, cacheEnv as s, leftoverVolumes as st, missingS3 as t, portValues as tt, lockBackend as u, quoteEnvValue as ut, umamiEnv as v, PACKAGE_NAME as w, validateProjectName as x, updateFrontendLock as y, runningServices as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@django-stack/cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "The production-ready batteries included Django 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",
@@ -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.12.0",
8
+ "django-stack[server,s3]>=0.12.2",
9
9
  "django-headless[schema]>=1.1.1",
10
10
  "django-content-studio[images,huey]>=1.11.0",
11
11
  ]
@@ -19,6 +19,28 @@ There is no Node and no frontend service: the pages read the content with
19
19
  the ORM.
20
20
  <!-- @end -->
21
21
 
22
+ ## Skills
23
+
24
+ Django Headless and Django Content Studio each publish an agent skill. Read the skill before you change what it
25
+ covers, and use the options it shows instead of writing your own
26
+ serializers, views or admin code:
27
+
28
+ - Django Headless, for the API (`@expose()`, permissions, filters, the
29
+ TypeScript client):
30
+ https://raw.githubusercontent.com/dwarsbit/django-headless/main/skills/django-headless/SKILL.md
31
+ - Django Content Studio, for the admin (`ModelStudio`, the edit page,
32
+ widgets, the dashboard, the media library):
33
+ https://djangocontentstudio.org/skills/django-content-studio/SKILL.md
34
+
35
+ Their full documentation is available as Markdown at
36
+ https://djangoheadless.org/llms.txt and
37
+ https://djangocontentstudio.org/llms.txt. If a skill is installed in your
38
+ skills folder, such as `.claude/skills/`, it takes the place of the link.
39
+
40
+ The skills follow the latest release; `backend/uv.lock` pins the installed
41
+ one. When a skill shows an option the project does not have, upgrade both
42
+ packages with `npx @django-stack/cli update` instead of working around it.
43
+
22
44
  ## Layout
23
45
 
24
46
  ```
@@ -16,7 +16,7 @@
16
16
  "@astrojs/node": "^11.1.9",
17
17
  "@astrojs/rss": "^4.0.19",
18
18
  "@django-headless/client": "^1.1.1",
19
- "@django-stack/utils": "^0.12.0",
19
+ "@django-stack/utils": "^0.12.2",
20
20
  "astro": "^7.4.5"
21
21
  },
22
22
  "devDependencies": {
@@ -13,9 +13,9 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@django-headless/client": "^1.1.1",
16
- "@django-stack/next": "^0.12.0",
17
- "@django-stack/react": "^0.12.0",
18
- "@django-stack/utils": "^0.12.0",
16
+ "@django-stack/next": "^0.12.2",
17
+ "@django-stack/react": "^0.12.2",
18
+ "@django-stack/utils": "^0.12.2",
19
19
  "next": "^16.4.4",
20
20
  "react": "^19.3.1",
21
21
  "react-dom": "^19.3.1",
@@ -15,8 +15,8 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@django-headless/client": "^1.1.1",
18
- "@django-stack/utils": "^0.12.0",
19
- "@django-stack/vue": "^0.12.0",
18
+ "@django-stack/utils": "^0.12.2",
19
+ "@django-stack/vue": "^0.12.2",
20
20
  "nuxt": "^4.6.3",
21
21
  "nuxt-security": "^2.7.0",
22
22
  "vue": "^3.5.44",
@@ -19,8 +19,8 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@django-headless/client": "^1.1.1",
22
- "@django-stack/svelte": "^0.12.0",
23
- "@django-stack/utils": "^0.12.0"
22
+ "@django-stack/svelte": "^0.12.2",
23
+ "@django-stack/utils": "^0.12.2"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@sveltejs/adapter-node": "^6.0.1",