@django-stack/cli 0.10.0 → 0.12.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.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/dist/bin.js +122 -52
  3. package/dist/index.d.ts +13 -4
  4. package/dist/index.js +1 -1
  5. package/dist/{production-Ct_qoeXk.js → production-1O8-1QUl.js} +126 -24
  6. package/package.json +3 -3
  7. package/templates/backend/Dockerfile +7 -2
  8. package/templates/backend/config/settings.py +12 -0
  9. package/templates/backend/config/studio.py +35 -3
  10. package/templates/backend/config/urls.py +11 -0
  11. package/templates/backend/content/admin.py +22 -5
  12. package/templates/backend/content/dashboard.py +112 -0
  13. package/templates/backend/content/seed.py +8 -1
  14. package/templates/backend/content/tests/test_api.py +15 -0
  15. package/templates/backend/content/tests/test_dashboard.py +77 -0
  16. package/templates/backend/pyproject.toml +1 -1
  17. package/templates/base/.github/workflows/deploy.yml +10 -1
  18. package/templates/base/AGENTS.md +78 -0
  19. package/templates/base/Caddyfile +24 -0
  20. package/templates/base/README.md +74 -6
  21. package/templates/base/SECURITY.md +2 -0
  22. package/templates/base/compose.yaml +21 -2
  23. package/templates/base/env.example +25 -0
  24. package/templates/frontend-astro/package.json +1 -1
  25. package/templates/frontend-next/package.json +3 -3
  26. package/templates/frontend-nuxt/package.json +2 -2
  27. package/templates/frontend-sveltekit/package.json +2 -2
  28. package/templates/frontend-templates/template.json +5 -0
  29. package/templates/frontend-templates/website/__init__.py +0 -0
  30. package/templates/frontend-templates/website/apps.py +6 -0
  31. package/templates/frontend-templates/website/context_processors.py +16 -0
  32. package/templates/frontend-templates/website/forms.py +20 -0
  33. package/templates/frontend-templates/website/static/website/favicon.svg +1 -0
  34. package/templates/frontend-templates/website/static/website/global.css +232 -0
  35. package/templates/frontend-templates/website/templates/website/404.html +10 -0
  36. package/templates/frontend-templates/website/templates/website/base.html +44 -0
  37. package/templates/frontend-templates/website/templates/website/blocks/call_to_action.html +5 -0
  38. package/templates/frontend-templates/website/templates/website/blocks/image.html +4 -0
  39. package/templates/frontend-templates/website/templates/website/blocks/quote.html +4 -0
  40. package/templates/frontend-templates/website/templates/website/blocks/text.html +2 -0
  41. package/templates/frontend-templates/website/templates/website/home.html +8 -0
  42. package/templates/frontend-templates/website/templates/website/includes/contact_form.html +21 -0
  43. package/templates/frontend-templates/website/templates/website/includes/post_card.html +9 -0
  44. package/templates/frontend-templates/website/templates/website/includes/post_list.html +11 -0
  45. package/templates/frontend-templates/website/templates/website/page.html +10 -0
  46. package/templates/frontend-templates/website/templates/website/post.html +20 -0
  47. package/templates/frontend-templates/website/templates/website/post_list.html +11 -0
  48. package/templates/frontend-templates/website/templates/website/robots.txt +5 -0
  49. package/templates/frontend-templates/website/templates/website/rss.xml +16 -0
  50. package/templates/frontend-templates/website/templates/website/sitemap.xml +8 -0
  51. package/templates/frontend-templates/website/tests/__init__.py +0 -0
  52. package/templates/frontend-templates/website/tests/test_site.py +102 -0
  53. package/templates/frontend-templates/website/urls.py +17 -0
  54. package/templates/frontend-templates/website/views.py +176 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @django-stack/cli
2
2
 
3
- The production-ready Django, batteries included stack. Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.
3
+ The production-ready batteries included Django stack. Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.
4
4
 
5
5
  ```bash
6
6
  npx @django-stack/cli init my-site
@@ -27,7 +27,7 @@ Run `dstack help <command>` for the options of a command. Each command
27
27
  checks for a newer version once a day; turn that off with
28
28
  `--no-update-check` or `DJANGO_STACK_NO_UPDATE_CHECK=1`.
29
29
 
30
- Read the [documentation](https://github.com/dwarsbit/django-stack/tree/main/website/docs) of
30
+ Read the [documentation](https://djangostack.org/) of
31
31
  [DjangoStack](https://github.com/dwarsbit/django-stack).
32
32
 
33
33
  ## License
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as envPorts, A as compose, B as NAMED_PRESETS, C as PACKAGE_NAME, D as RUN_DOMAIN, E as CAPTCHA_APP, F as projectUrls, H as PROMPT_ORDER, I as readLocalRun, J as resolvePreset, K as encodePreset, L as readProject, M as findProjectDirectory, N as parseProfiles, O as RUN_ENV, P as projectStack, Q as describeMove, R as runningServices, S as CLI_VERSION, T as generateSecret, U as STACK_OPTIONS, V as NODE_FRONTENDS, W as applies, X as DEV_PORTS, Y as stackOption, Z as chooseFreePorts, _ as umamiEnv, a as LOCAL_S3, at as isDockerDown, b as validateProjectName, c as localS3, ct as setEnvValues, et as portValues, f as relativeDirectory, g as toProjectName, h as timeZones, it as dockerRunning, j as composeOutput, k as activeProject, l as lockBackend, m as setNotificationsApp, n as productionEnv$1, nt as checkTools, o as cacheEnv, ot as parseEnv, p as scaffold, q as pendingChoices, r as validateDomain, rt as dockerNotRunning, s as frontendPackageManager, st as quoteEnvValue, t as missingS3, tt as runDoctor, u as lockFrontend, v as updateFrontendLock, w as generateProjectSecrets, x as validateTimeZone, y as usesValkey, z as DEFAULT_STACK } from "./production-Ct_qoeXk.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 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";
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";
@@ -3929,10 +3929,9 @@ function choices(key, stack = {}) {
3929
3929
  }
3930
3930
  /**
3931
3931
  * The stack from, in order of precedence, the flags, the preset, the prompts
3932
- * and the defaults. `askDatabase` asks for the Postgres version together with
3933
- * the external database choice; it returns the version, or a URL.
3932
+ * and the defaults.
3934
3933
  */
3935
- async function chooseStack(options, interactive, askDatabase) {
3934
+ async function chooseStack(options, interactive) {
3936
3935
  let preset;
3937
3936
  if (options.preset) try {
3938
3937
  preset = resolvePreset(options.preset);
@@ -3940,18 +3939,12 @@ async function chooseStack(options, interactive, askDatabase) {
3940
3939
  fail(error.message);
3941
3940
  }
3942
3941
  const stack = { ...DEFAULT_STACK };
3943
- let databaseUrl;
3944
3942
  for (const key of PROMPT_ORDER) {
3945
3943
  const option = stackOption(key);
3946
3944
  if (options.fromCli(key)) stack[key] = options[key];
3947
3945
  else if (key === "storage" && options.fromCli("localS3") && options.localS3) stack[key] = "s3";
3948
3946
  else if (preset) stack[key] = preset[key];
3949
3947
  else if (!interactive || !applies(key, stack)) continue;
3950
- else if (key === "postgres" && askDatabase && !options.databaseUrl) {
3951
- const answer = await askDatabase(stack[key]);
3952
- if ("databaseUrl" in answer) databaseUrl = answer.databaseUrl;
3953
- else stack[key] = answer.postgres;
3954
- } else if (key === "postgres" && options.databaseUrl) continue;
3955
3948
  else {
3956
3949
  const initialValue = key === "storage" && stack.media === "s3" ? "s3" : stack[key];
3957
3950
  stack[key] = unwrap(await p.select({
@@ -3961,10 +3954,7 @@ async function chooseStack(options, interactive, askDatabase) {
3961
3954
  }));
3962
3955
  }
3963
3956
  }
3964
- return {
3965
- stack,
3966
- databaseUrl
3967
- };
3957
+ return stack;
3968
3958
  }
3969
3959
  function systemTimeZone() {
3970
3960
  try {
@@ -4009,28 +3999,11 @@ async function init(directoryArgument, options) {
4009
3999
  validate: (value) => validateProjectName(value ?? "")
4010
4000
  }));
4011
4001
  }
4012
- const chosen = await chooseStack(options, interactive, async (initial) => {
4013
- const choice = unwrap(await p.select({
4014
- message: "Database",
4015
- initialValue: initial,
4016
- options: [...choices("postgres").map((value) => ({
4017
- ...value,
4018
- label: `Bundled Postgres, ${value.label.toLowerCase()}`
4019
- })), {
4020
- value: "external",
4021
- label: "External Postgres",
4022
- hint: "a URL to your own server"
4023
- }]
4024
- }));
4025
- if (choice !== "external") return { postgres: choice };
4026
- return { databaseUrl: unwrap(await p.text({
4027
- message: "Postgres URL",
4028
- placeholder: "postgres://user:password@host:5432/name",
4029
- validate: (value) => value?.startsWith("postgres") ? void 0 : "Enter a postgres:// URL."
4030
- })) };
4031
- });
4032
- const stack = chosen.stack;
4033
- const databaseUrl = options.databaseUrl ?? chosen.databaseUrl;
4002
+ const stack = await chooseStack(options, interactive);
4003
+ if (!applies("analytics", stack) && stack.analytics !== "off") {
4004
+ if (options.fromCli("analytics")) fail("--analytics umami needs a frontend: a headless project has no pages to count.");
4005
+ stack.analytics = "off";
4006
+ }
4034
4007
  const pending = pendingChoices(stack);
4035
4008
  const unavailable = pending.filter((item) => !item.isDefault);
4036
4009
  if (unavailable.length) fail(`${unavailable.map((item) => item.choice).join(", ")} ${unavailable.length > 1 ? "are" : "is"} not available yet. Leave ${unavailable.length > 1 ? "them" : "it"} at the default.`);
@@ -4065,7 +4038,8 @@ async function init(directoryArgument, options) {
4065
4038
  name,
4066
4039
  frontend: stack.frontend,
4067
4040
  demoContent,
4068
- databaseUrl,
4041
+ databaseUrl: options.databaseUrl,
4042
+ postgres: stack.postgres,
4069
4043
  localS3: stack.storage === "s3",
4070
4044
  analytics: stack.analytics === "umami",
4071
4045
  cache: stack.cache,
@@ -4127,7 +4101,7 @@ async function init(directoryArgument, options) {
4127
4101
  command(`cd ${relativeDirectory(target)}`),
4128
4102
  command(startCommand),
4129
4103
  "",
4130
- `${pc.bold("Site")} ${url(`http://localhost:${ports.ports.SITE_PORT}`)}`,
4104
+ stack.frontend === "headless" ? `${pc.bold("API")} ${url(`http://localhost:${ports.ports.SITE_PORT}/api/`)}` : `${pc.bold("Site")} ${url(`http://localhost:${ports.ports.SITE_PORT}`)}`,
4131
4105
  `${pc.bold("Admin")} ${url(`http://localhost:${ports.ports.SITE_PORT}/admin/`)}`,
4132
4106
  `${pc.bold("Mail")} ${url(`http://localhost:${ports.ports.MAIL_PORT}`)}`,
4133
4107
  "",
@@ -4166,7 +4140,7 @@ async function preset(code, options) {
4166
4140
  p.outro(`Use it with ${command(`${selfCommand()} init --preset ${code in NAMED_PRESETS ? code : encodePreset(stack)}`)}`);
4167
4141
  return;
4168
4142
  }
4169
- const { stack } = await chooseStack(options, isInteractive(options.yes));
4143
+ const stack = await chooseStack(options, isInteractive(options.yes));
4170
4144
  const encoded = encodePreset(stack);
4171
4145
  p.outro(`Create this stack with ${command(`${selfCommand()} init --preset ${encoded}`)}`);
4172
4146
  }
@@ -4185,7 +4159,7 @@ function validateUrl(value) {
4185
4159
  if (!value) return void 0;
4186
4160
  return /^https?:\/\/[^\s/]+/.test(value) ? void 0 : "Enter a URL that starts with https://.";
4187
4161
  }
4188
- const required = (label) => (value) => value.trim() ? void 0 : `Enter the ${label}.`;
4162
+ const required$1 = (label) => (value) => value.trim() ? void 0 : `Enter the ${label}.`;
4189
4163
  /**
4190
4164
  * An SMTP URL for EMAIL_URL: smtps:// on port 465, smtp:// on 25, otherwise
4191
4165
  * smtp+tls:// (STARTTLS), with the user name and password URL-encoded.
@@ -4281,7 +4255,7 @@ const SETTING_GROUPS = [
4281
4255
  name: "S3_BUCKET",
4282
4256
  message: "S3 bucket",
4283
4257
  placeholder: "example-media",
4284
- validate: required("bucket")
4258
+ validate: required$1("bucket")
4285
4259
  },
4286
4260
  {
4287
4261
  name: "S3_ENDPOINT_URL",
@@ -4294,18 +4268,18 @@ const SETTING_GROUPS = [
4294
4268
  name: "S3_REGION",
4295
4269
  message: "S3 region",
4296
4270
  placeholder: "eu-central-1",
4297
- validate: required("region")
4271
+ validate: required$1("region")
4298
4272
  },
4299
4273
  {
4300
4274
  name: "S3_ACCESS_KEY_ID",
4301
4275
  message: "S3 access key ID",
4302
- validate: required("access key ID")
4276
+ validate: required$1("access key ID")
4303
4277
  },
4304
4278
  {
4305
4279
  name: "S3_SECRET_ACCESS_KEY",
4306
4280
  message: "S3 secret access key",
4307
4281
  secret: true,
4308
- validate: required("secret access key")
4282
+ validate: required$1("secret access key")
4309
4283
  },
4310
4284
  {
4311
4285
  name: "S3_PUBLIC_URL",
@@ -4460,6 +4434,86 @@ async function settings(options) {
4460
4434
  else p.outro(pc.green("Done"));
4461
4435
  }
4462
4436
  // src/commands/production.ts
4437
+ const required = (label) => (value) => value?.trim() ? void 0 : `Enter the ${label}.`;
4438
+ /**
4439
+ * Asks for the server's database: the bundled Postgres, or your own as a URL
4440
+ * or as separate values. Returns the URL of your own, or undefined.
4441
+ */
4442
+ async function askDatabase() {
4443
+ if (unwrap(await p.select({
4444
+ message: "Database on the server",
4445
+ initialValue: "bundled",
4446
+ options: [{
4447
+ value: "bundled",
4448
+ label: "Bundled Postgres",
4449
+ hint: "a container next to the backend"
4450
+ }, {
4451
+ value: "external",
4452
+ label: "Your own Postgres",
4453
+ hint: "a managed database or another server"
4454
+ }]
4455
+ })) === "bundled") return void 0;
4456
+ if (unwrap(await p.select({
4457
+ message: "How do you want to enter it?",
4458
+ initialValue: "parts",
4459
+ options: [{
4460
+ value: "parts",
4461
+ label: "Host, port, name, user and password",
4462
+ hint: "any password works"
4463
+ }, {
4464
+ value: "url",
4465
+ label: "A postgres:// URL",
4466
+ hint: "special characters in the password URL-encoded"
4467
+ }]
4468
+ })) === "url") {
4469
+ const value = unwrap(await p.text({
4470
+ message: "Postgres URL",
4471
+ placeholder: "postgres://user:password@db.example.com:5432/app",
4472
+ validate: (value) => {
4473
+ try {
4474
+ normalizeDatabaseUrl((value ?? "").trim());
4475
+ return;
4476
+ } catch (error) {
4477
+ return error.message;
4478
+ }
4479
+ }
4480
+ }));
4481
+ return normalizeDatabaseUrl(value.trim());
4482
+ }
4483
+ const host = unwrap(await p.text({
4484
+ message: "Host",
4485
+ placeholder: "db.example.com",
4486
+ validate: required("host")
4487
+ }));
4488
+ const port = unwrap(await p.text({
4489
+ message: "Port",
4490
+ placeholder: "5432",
4491
+ defaultValue: "5432",
4492
+ validate: (value) => {
4493
+ if (!value) return void 0;
4494
+ const port = Number(value);
4495
+ return Number.isInteger(port) && port > 0 && port < 65536 ? void 0 : "Enter a port, such as 5432.";
4496
+ }
4497
+ }));
4498
+ const name = unwrap(await p.text({
4499
+ message: "Database name",
4500
+ placeholder: "app",
4501
+ validate: required("name")
4502
+ }));
4503
+ const user = unwrap(await p.text({
4504
+ message: "User",
4505
+ placeholder: "app",
4506
+ validate: required("user")
4507
+ }));
4508
+ const password = unwrap(await p.password({ message: "Password" }));
4509
+ return postgresUrl({
4510
+ host: host.trim(),
4511
+ port: port.trim(),
4512
+ name: name.trim(),
4513
+ user: user.trim(),
4514
+ password
4515
+ });
4516
+ }
4463
4517
  async function production(directoryArgument, options) {
4464
4518
  const interactive = isInteractive(options.yes);
4465
4519
  const directory = resolve(directoryArgument ?? ".");
@@ -4477,12 +4531,21 @@ async function production(directoryArgument, options) {
4477
4531
  validate: (v) => validateDomain(v ?? "")
4478
4532
  }));
4479
4533
  }
4534
+ const devEnv = readFileSync(envPath, "utf8");
4535
+ let databaseUrl = options.databaseUrl;
4536
+ if (databaseUrl) try {
4537
+ normalizeDatabaseUrl(databaseUrl);
4538
+ } catch (error) {
4539
+ fail(error.message);
4540
+ }
4541
+ else if (interactive && !parseEnv(devEnv).PRODUCTION_DATABASE_URL) databaseUrl = await askDatabase();
4480
4542
  const context = projectContext(directory);
4481
4543
  let result;
4482
4544
  try {
4483
- result = productionEnv$1(readFileSync(envPath, "utf8"), {
4545
+ result = productionEnv$1(devEnv, {
4484
4546
  domain,
4485
4547
  emailUrl: options.emailUrl,
4548
+ databaseUrl,
4486
4549
  media: context.media
4487
4550
  });
4488
4551
  } catch (error) {
@@ -4541,6 +4604,7 @@ function applyStack(envText, env, from, to) {
4541
4604
  if (from[key] === to[key]) continue;
4542
4605
  const value = option.values.find((candidate) => candidate.value === to[key]);
4543
4606
  if (!value) throw new Error(`${JSON.stringify(to[key])} is not a ${option.key}.`);
4607
+ if (key === "postgres") throw new Error("--postgres can't change on an existing project: a new major version needs a backup and a restore. See the docs on backups.");
4544
4608
  if (!SWITCHABLE.includes(key) || !value.generated) throw new Error(`${option.flag} ${to[key]} can't be set on an existing project in this version yet.`);
4545
4609
  if (key === "storage") {
4546
4610
  if (profiles.includes("prod")) throw new Error("--storage only changes development; this .env runs the prod profile.");
@@ -4639,7 +4703,8 @@ function ensureRunEnv(project) {
4639
4703
  if (!existsSync(path)) {
4640
4704
  const result = productionEnv$1(readFileSync(join(project.directory, ".env"), "utf8"), {
4641
4705
  domain: RUN_DOMAIN,
4642
- emailUrl: "console://"
4706
+ emailUrl: "console://",
4707
+ bundledDatabase: true
4643
4708
  });
4644
4709
  writeFileSync(path, result.text, { mode: 384 });
4645
4710
  p.log.success(`Wrote ${pc.bold(RUN_ENV)}: the prod profile at ${url(`https://${RUN_DOMAIN}`)}, with its own secrets and data.`);
@@ -4695,7 +4760,8 @@ async function dev(options) {
4695
4760
  intro("Develop");
4696
4761
  project = await freeDevPorts(project);
4697
4762
  urlsNote(project, "Starting");
4698
- p.log.message(pc.dim("Changes in backend/ and frontend/ reload. Press Ctrl+C to stop."));
4763
+ const reloads = project.services.frontend ? "backend/ and frontend/" : "backend/";
4764
+ p.log.message(pc.dim(`Changes in ${reloads} reload. Press Ctrl+C to stop.`));
4699
4765
  exitWith(compose(project, ["up", ...options.build ? ["--build"] : []]));
4700
4766
  }
4701
4767
  /** `dstack stop`: stop the containers of the project and of its local run; volumes and data stay. */
@@ -4844,6 +4910,10 @@ async function types(options) {
4844
4910
  "--file",
4845
4911
  "openapi.json"
4846
4912
  ], false)) !== 0) fail("The backend could not write its schema.");
4913
+ if (!project.services.frontend) {
4914
+ p.outro(`Wrote ${pc.bold("backend/openapi.json")}`);
4915
+ return;
4916
+ }
4847
4917
  const generate = [
4848
4918
  "npx",
4849
4919
  "headless",
@@ -4931,7 +5001,7 @@ function checkSteps(project, tests) {
4931
5001
  ],
4932
5002
  devOnly: true
4933
5003
  });
4934
- return steps.filter((step) => !(step.devOnly && project.prod));
5004
+ return steps.filter((step) => !(step.devOnly && project.prod) && (step.service !== "frontend" || project.services.frontend));
4935
5005
  }
4936
5006
  /** `dstack check`: the doctor's checks, Django's checks, the linters and, with --tests, the tests. */
4937
5007
  async function check(options) {
@@ -5029,8 +5099,8 @@ async function update(options) {
5029
5099
  ];
5030
5100
  if (compose(target, build) !== 0) fail("Building the images failed.");
5031
5101
  }
5032
- const volume = project.prod ? void 0 : nodeModulesVolume(project);
5033
- if (volume) {
5102
+ const volume = project.prod || !project.services.frontend ? void 0 : nodeModulesVolume(project);
5103
+ if (volume && project.services.frontend) {
5034
5104
  compose(project, [
5035
5105
  "rm",
5036
5106
  "--stop",
@@ -5313,7 +5383,7 @@ function createProgram() {
5313
5383
  styleArgumentText: (text) => pc.yellow(text),
5314
5384
  styleDescriptionText: (text) => text
5315
5385
  }).configureOutput({ outputError: (text, write) => write(pc.red(text)) });
5316
- const initCommand = program.command("init").alias("create").description("Create a project").argument("[directory]", "Where to create the project").option("--name <name>", "The project name (default: from the directory)").option("--preset <preset>", `A preset code from a previous init, or ${Object.keys(NAMED_PRESETS).join(", ")}`).option("--database-url <url>", "Use an external Postgres instead of the bundled one").option("--time-zone <zone>", "The time zone of the backend, such as Europe/Amsterdam (default: UTC)").addOption(new Option("--local-s3", "Same as --storage s3").hideHelp()).option("--no-demo-content", "Start with an empty site").option("--no-git", "Don't create a git repository").option("--start", "Run `docker compose up` when done").option("-y, --yes", "Accept the defaults without asking").addOption(new Option("--link <repository>", "Take the packages from a DjangoStack checkout").hideHelp()).action((directory, options, cmd) => init(directory, {
5386
+ const initCommand = program.command("init").alias("create").description("Create a project").argument("[directory]", "Where to create the project").option("--name <name>", "The project name (default: from the directory)").option("--preset <preset>", `A preset code from a previous init, or ${Object.keys(NAMED_PRESETS).join(", ")}`).option("--database-url <url>", "An external Postgres for production; development runs the bundled one").option("--time-zone <zone>", "The time zone of the backend, such as Europe/Amsterdam (default: UTC)").addOption(new Option("--local-s3", "Same as --storage s3").hideHelp()).option("--no-demo-content", "Start with an empty site").option("--no-git", "Don't create a git repository").option("--start", "Run `docker compose up` when done").option("-y, --yes", "Accept the defaults without asking").addOption(new Option("--link <repository>", "Take the packages from a DjangoStack checkout").hideHelp()).action((directory, options, cmd) => init(directory, {
5317
5387
  ...options,
5318
5388
  installPrompt: program.opts().installPrompt,
5319
5389
  fromCli: (option) => cmd.getOptionValueSource(option) === "cli"
@@ -5359,7 +5429,7 @@ function createProgram() {
5359
5429
  fromCli: (option) => cmd.getOptionValueSource(option) === "cli"
5360
5430
  }));
5361
5431
  for (const flag of stackFlags()) presetCommand.addOption(flag);
5362
- program.command("production").description("Write .env.production for a server").argument("[directory]", "The project directory", ".").option("--domain <domain>", "The domain of the site, such as example.com").option("--email-url <url>", "The SMTP server, such as smtp+tls://user:pass@host:587").option("--force", "Overwrite an existing .env.production").option("-y, --yes", "Don't ask; fail when --domain is missing").action(production);
5432
+ program.command("production").description("Write .env.production for a server").argument("[directory]", "The project directory", ".").option("--domain <domain>", "The domain of the site, such as example.com").option("--email-url <url>", "The SMTP server, such as smtp+tls://user:pass@host:587").option("--database-url <url>", "Your own Postgres for the server instead of the bundled one").option("--force", "Overwrite an existing .env.production").option("-y, --yes", "Don't ask; fail when --domain is missing").action(production);
5363
5433
  program.command("settings").description("Ask for the missing production settings and credentials, and save them").addOption(dirOption()).option("--file <file>", "The file to change (default: .env.production, or .env on a server)").option("--set <NAME=value>", "Set a value without asking, such as --set ADMIN_EMAIL=you@example.com", (value, previous = []) => [...previous, value]).option("--all", "Ask for every setting, also the ones that are set").option("-y, --yes", "Don't ask; fail when a setting is still missing").action(settings);
5364
5434
  program.command("doctor").description("Check a project and its services").argument("[directory]", "The project directory", ".").action(doctor);
5365
5435
  program.command("self-update").description(`Update ${BIN} to the latest version on npm`).action(selfUpdate);
package/dist/index.d.ts CHANGED
@@ -20,6 +20,10 @@ interface ProductionOptions {
20
20
  emailUrl?: string;
21
21
  /** Where uploads go: `s3` asks for the S3 settings (the `media` stack option). */
22
22
  media?: string;
23
+ /** Keep the bundled database even with PRODUCTION_DATABASE_URL: the local run on this machine. */
24
+ bundledDatabase?: boolean;
25
+ /** An external Postgres for the server; it takes the place of PRODUCTION_DATABASE_URL. */
26
+ databaseUrl?: string;
23
27
  }
24
28
  interface ProductionEnv {
25
29
  text: string;
@@ -54,12 +58,12 @@ export declare const STACK_OPTIONS: readonly [{
54
58
  readonly value: "headless";
55
59
  readonly label: "Headless";
56
60
  readonly hint: "just the API and the admin";
57
- readonly generated: false;
61
+ readonly generated: true;
58
62
  }, {
59
63
  readonly value: "templates";
60
64
  readonly label: "Django templates";
61
65
  readonly hint: "no Node, Django renders the site";
62
- readonly generated: false;
66
+ readonly generated: true;
63
67
  }, {
64
68
  readonly value: "nuxt";
65
69
  readonly label: "Nuxt";
@@ -86,7 +90,7 @@ export declare const STACK_OPTIONS: readonly [{
86
90
  readonly value: "latest";
87
91
  readonly label: "Latest";
88
92
  readonly hint: "Postgres 18";
89
- readonly generated: false;
93
+ readonly generated: true;
90
94
  }];
91
95
  }, {
92
96
  readonly key: "analytics";
@@ -290,8 +294,13 @@ interface ScaffoldOptions {
290
294
  frontend: Frontend;
291
295
  /** Demo pages and posts on the first start. */
292
296
  demoContent: boolean;
293
- /** An external Postgres URL; the bundled Postgres when not set. */
297
+ /**
298
+ * An external Postgres URL for production, saved as PRODUCTION_DATABASE_URL;
299
+ * development runs the bundled Postgres either way.
300
+ */
294
301
  databaseUrl?: string;
302
+ /** The bundled Postgres's version, and the backend's Postgres client; stable by default. */
303
+ postgres?: Stack["postgres"];
295
304
  /** Store uploads in the local S3 service in development, like S3 in production. */
296
305
  localS3?: boolean;
297
306
  /** Page analytics with the bundled Umami (the `analytics` profile). */
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { z as DEFAULT_STACK, i as FRONTENDS, B as NAMED_PRESETS, U as STACK_OPTIONS, S as VERSION, o as cacheEnv, G as decodePreset, K as encodePreset, w as generateProjectSecrets, T as generateSecret, d as normalizeDatabaseUrl, ot as parseEnv, q as pendingChoices, n as productionEnv, J as resolvePreset, tt as runDoctor, p as scaffold, ct as setEnvValues, m as setNotificationsApp, g as toProjectName, y as usesValkey, r as validateDomain, b as validateProjectName } from "./production-Ct_qoeXk.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, 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";