@django-stack/cli 0.7.0 → 0.8.1

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 CHANGED
@@ -14,6 +14,7 @@ docker compose up
14
14
  | `production [directory]` | Write `.env.production` for a server. Flags: `--domain`, `--email-url`, `--force`, `--yes` |
15
15
  | `preset [preset]` | Print the preset code of a stack, or the stack of a code |
16
16
  | `doctor [directory]` | Check a project and its running services |
17
+ | `self-update` | Update `dstack` to the latest version, with the package manager that installed it |
17
18
  | `secret [bytes]` | Print a random secret |
18
19
  | `completion <shell>` | Print a Tab completion script for zsh, bash, fish or PowerShell |
19
20
 
package/dist/bin.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { A as generateSecret, B as isDockerDown, C as applies, D as resolvePreset, E as pendingChoices, F as portValues, H as quoteEnvValue, I as runDoctor, L as checkTools, M as chooseFreePorts, N as describeMove, O as stackOption, P as envPorts, R as dockerNotRunning, S as STACK_OPTIONS, T as encodePreset, U as setEnvValues, V as parseEnv, _ as validateTimeZone, a as LOCAL_S3, b as NAMED_PRESETS, c as lockBackend, d as scaffold, f as timeZones, g as validateProjectName, h as usesValkey, j as DEV_PORTS, k as generateProjectSecrets, m as umamiEnv, n as productionEnv$1, o as cacheEnv, p as toProjectName, r as validateDomain, s as localS3, t as missingS3, u as relativeDirectory, v as CLI_VERSION, w as decodePreset, x as PROMPT_ORDER, y as DEFAULT_STACK, z as dockerRunning } from "./production-DBpWWx6i.js";
2
+ import { $ as isDockerDown, A as projectUrls, B as encodePreset, C as RUN_ENV, D as findProjectDirectory, E as composeOutput, F as NAMED_PRESETS, G as chooseFreePorts, H as resolvePreset, I as PROMPT_ORDER, J as portValues, K as describeMove, L as STACK_OPTIONS, M as readProject, N as runningServices, O as parseProfiles, P as DEFAULT_STACK, Q as dockerRunning, R as applies, S as RUN_DOMAIN, T as compose, U as stackOption, V as pendingChoices, W as DEV_PORTS, X as checkTools, Y as runDoctor, Z as dockerNotRunning, _ as validateTimeZone, a as LOCAL_S3, b as generateProjectSecrets, c as lockBackend, d as scaffold, et as parseEnv, f as timeZones, g as validateProjectName, h as usesValkey, j as readLocalRun, k as projectStack, m as umamiEnv, n as productionEnv$1, nt as setEnvValues, o as cacheEnv, p as toProjectName, q as envPorts, r as validateDomain, s as localS3, t as missingS3, tt as quoteEnvValue, u as relativeDirectory, v as CLI_VERSION, w as activeProject, x as generateSecret, y as PACKAGE_NAME } from "./production-CGkI7oNe.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, relative, resolve } from "node:path";
6
6
  import { execFileSync, spawn, spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
7
8
  import pc from "picocolors";
8
9
  import tab from "@bomb.sh/tab/commander";
9
10
  import * as p from "@clack/prompts";
@@ -2495,228 +2496,6 @@ function getProvider(id) {
2495
2496
  if (!provider) throw new Error(`Unknown deploy provider ${id}. Choose one of: ${PROVIDERS.map((p) => p.id).join(", ")}.`);
2496
2497
  return provider;
2497
2498
  }
2498
- // src/project.ts
2499
- /** The `.env` of `dstack run` on a development machine: the prod profile at https://site.localhost. */
2500
- const RUN_ENV = ".env.run";
2501
- const RUN_DOMAIN = "site.localhost";
2502
- /** The project directory: `start` or the nearest parent with `compose.yaml` and `.env`. */
2503
- function findProjectDirectory(start) {
2504
- let directory = resolve(start);
2505
- for (;;) {
2506
- if (existsSync(join(directory, "compose.yaml")) && existsSync(join(directory, ".env"))) return directory;
2507
- const parent = dirname(directory);
2508
- if (parent === directory) return void 0;
2509
- directory = parent;
2510
- }
2511
- }
2512
- function parseProfiles(value) {
2513
- return (value ?? "").split(",").map((profile) => profile.trim()).filter(Boolean);
2514
- }
2515
- function projectName(directory) {
2516
- try {
2517
- const match = readFileSync(join(directory, "compose.yaml"), "utf8").match(/^name:\s*["']?([\w.-]+)/m);
2518
- if (match?.[1]) return match[1];
2519
- } catch {}
2520
- return basename(directory).toLowerCase();
2521
- }
2522
- function fromEnv(directory, name, envFile, localRun) {
2523
- const env = parseEnv(readFileSync(join(directory, envFile), "utf8"));
2524
- const profiles = parseProfiles(env.COMPOSE_PROFILES);
2525
- const prod = profiles.includes("prod");
2526
- const suffix = prod ? "-prod" : "";
2527
- return {
2528
- directory,
2529
- name: localRun ? `${name}-run` : name,
2530
- env,
2531
- profiles,
2532
- prod,
2533
- localRun,
2534
- services: {
2535
- backend: `backend${suffix}`,
2536
- frontend: `frontend${suffix}`,
2537
- worker: `worker${suffix}`
2538
- },
2539
- composeArgs: localRun ? [
2540
- "--env-file",
2541
- envFile,
2542
- "--project-name",
2543
- `${name}-run`
2544
- ] : [],
2545
- composeEnv: localRun ? {
2546
- ENV_FILE: envFile,
2547
- COMPOSE_PROFILES: env.COMPOSE_PROFILES ?? ""
2548
- } : {}
2549
- };
2550
- }
2551
- /** Reads the project in or above `start`. Throws with a one-line reason when there is none. */
2552
- function readProject(start = ".") {
2553
- const directory = findProjectDirectory(start);
2554
- if (!directory) throw new Error(`No DjangoStack project in ${resolve(start)}: run this where compose.yaml and .env are.`);
2555
- return fromEnv(directory, projectName(directory), ".env", false);
2556
- }
2557
- /** The local production run of a development project, when `.env.run` exists. */
2558
- function readLocalRun(project) {
2559
- if (project.prod || project.localRun || !existsSync(join(project.directory, ".env.run"))) return void 0;
2560
- return fromEnv(project.directory, projectName(project.directory), RUN_ENV, true);
2561
- }
2562
- /** The `django-stack` section of the project's `package.json`. */
2563
- function projectManifest(project) {
2564
- try {
2565
- return JSON.parse(readFileSync(join(project.directory, "package.json"), "utf8"))["django-stack"] ?? {};
2566
- } catch {
2567
- return {};
2568
- }
2569
- }
2570
- /**
2571
- * The project's stack: from the preset `init` saved in `package.json`, or,
2572
- * for projects from before that, the defaults with the storage from `.env`.
2573
- * Analytics follow the `analytics` profile: projects from before Umami was
2574
- * generated have the default in their preset without running it.
2575
- */
2576
- function projectStack(project) {
2577
- const fromEnv = {
2578
- analytics: project.profiles.includes("analytics") ? "umami" : "off",
2579
- ...project.env.CACHE_URL === void 0 ? {} : cacheStack(project.env)
2580
- };
2581
- const { preset } = projectManifest(project);
2582
- if (preset) try {
2583
- return {
2584
- ...decodePreset(preset),
2585
- ...fromEnv
2586
- };
2587
- } catch {}
2588
- return {
2589
- ...DEFAULT_STACK,
2590
- ...fromEnv,
2591
- storage: project.profiles.includes("s3") ? "s3" : "local"
2592
- };
2593
- }
2594
- const REDIS = /^(redis|rediss|valkey|valkeys):/;
2595
- /**
2596
- * The cache and the tasks a project's `.env` runs. Projects from before 0.7
2597
- * have no TASKS_URL: their tasks follow a Redis cache, like the backend does.
2598
- */
2599
- function cacheStack(env) {
2600
- const cacheUrl = env.CACHE_URL ?? "";
2601
- const cache = cacheUrl.startsWith("db:") ? "database" : REDIS.test(cacheUrl) ? "valkey" : "off";
2602
- const tasksUrl = env.TASKS_URL || (cache === "valkey" ? cacheUrl : "immediate://");
2603
- if (tasksUrl.startsWith("immediate:")) return {
2604
- cache,
2605
- tasks: "off"
2606
- };
2607
- return {
2608
- cache,
2609
- tasks: REDIS.test(tasksUrl) && cache !== "valkey" ? "huey-valkey" : "huey"
2610
- };
2611
- }
2612
- /** The URLs of a running project. */
2613
- function projectUrls(project) {
2614
- if (project.prod) {
2615
- const site = (project.env.SITE_URL ?? "").replace(/\/$/, "");
2616
- return [{
2617
- label: "Site",
2618
- url: `${site}/`
2619
- }, {
2620
- label: "Admin",
2621
- url: `${site}/admin/`
2622
- }];
2623
- }
2624
- const ports = envPorts(project.env);
2625
- const site = (project.env.SITE_URL || `http://localhost:${ports.SITE_PORT}`).replace(/\/$/, "");
2626
- const urls = [
2627
- {
2628
- label: "Site",
2629
- url: site
2630
- },
2631
- {
2632
- label: "Admin",
2633
- url: `${site}/admin/`
2634
- },
2635
- {
2636
- label: "Mail",
2637
- url: `http://localhost:${ports.MAIL_PORT}`
2638
- }
2639
- ];
2640
- if (project.profiles.includes("s3")) urls.push({
2641
- label: "S3",
2642
- url: `http://localhost:${ports.S3_PORT}`
2643
- });
2644
- return urls;
2645
- }
2646
- /** Runs a command with the terminal attached and returns its exit status. */
2647
- const runInherit = (cmd, args, options) => {
2648
- const result = spawnSync(cmd, args, {
2649
- stdio: "inherit",
2650
- ...options
2651
- });
2652
- if (result.error) return 127;
2653
- return result.status ?? 1;
2654
- };
2655
- /** The full `docker compose` argument list for a project. */
2656
- function composeCommand(project, args) {
2657
- return [
2658
- "compose",
2659
- ...project.composeArgs,
2660
- ...args
2661
- ];
2662
- }
2663
- function composeOptions(project) {
2664
- return {
2665
- cwd: project.directory,
2666
- env: {
2667
- ...process.env,
2668
- ...project.composeEnv
2669
- }
2670
- };
2671
- }
2672
- /** Runs `docker compose <args>` for the project with the terminal attached. */
2673
- function compose(project, args, run = runInherit) {
2674
- return run("docker", composeCommand(project, args), composeOptions(project));
2675
- }
2676
- /** Runs `docker compose <args>` and captures the output. */
2677
- function composeOutput(project, args) {
2678
- const result = spawnSync("docker", composeCommand(project, args), {
2679
- ...composeOptions(project),
2680
- encoding: "utf8",
2681
- stdio: [
2682
- "ignore",
2683
- "pipe",
2684
- "pipe"
2685
- ]
2686
- });
2687
- return {
2688
- status: result.status ?? 1,
2689
- output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
2690
- };
2691
- }
2692
- /** The running services of the project. */
2693
- function runningServices(project) {
2694
- const result = spawnSync("docker", composeCommand(project, [
2695
- "ps",
2696
- "--services",
2697
- "--filter",
2698
- "status=running"
2699
- ]), {
2700
- ...composeOptions(project),
2701
- encoding: "utf8",
2702
- stdio: [
2703
- "ignore",
2704
- "pipe",
2705
- "ignore"
2706
- ]
2707
- });
2708
- return result.status === 0 ? result.stdout.split("\n").filter(Boolean) : [];
2709
- }
2710
- /**
2711
- * The project a command acts on: the local production run when it is the
2712
- * one running, otherwise the project `.env` describes.
2713
- */
2714
- function activeProject(project) {
2715
- const localRun = readLocalRun(project);
2716
- if (!localRun) return project;
2717
- if (runningServices(project).length) return project;
2718
- return runningServices(localRun).length ? localRun : project;
2719
- }
2720
2499
  // src/ui.ts
2721
2500
  /** The DjangoStack badge with the version and an optional title. */
2722
2501
  function intro(title) {
@@ -3195,90 +2974,6 @@ async function doctor(directoryArgument) {
3195
2974
  }
3196
2975
  p.outro(pc.green("All checks passed"));
3197
2976
  }
3198
- // src/update-check.ts
3199
- const PACKAGE_NAME = "@django-stack/cli";
3200
- const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
3201
- const DAY = 864e5;
3202
- /** `$XDG_CACHE_HOME/django-stack/update-check.json`, or under `~/.cache`. */
3203
- function defaultCachePath(env = process.env) {
3204
- const base = env.XDG_CACHE_HOME || process.platform === "win32" && env.LOCALAPPDATA || join(homedir(), ".cache");
3205
- return join(base, "django-stack", "update-check.json");
3206
- }
3207
- /** Whether to check at all: never in CI, without a terminal, or when turned off. */
3208
- function updateCheckEnabled(env = process.env, isTTY = Boolean(process.stdout.isTTY)) {
3209
- if (!isTTY) return false;
3210
- if (env.CI && env.CI !== "0" && env.CI !== "false") return false;
3211
- for (const key of ["DJANGO_STACK_NO_UPDATE_CHECK", "NO_UPDATE_NOTIFIER"]) if (env[key] && env[key] !== "0") return false;
3212
- return true;
3213
- }
3214
- function parse(version) {
3215
- const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(version.trim());
3216
- if (!match) return void 0;
3217
- return [
3218
- Number(match[1]),
3219
- Number(match[2]),
3220
- Number(match[3]),
3221
- match[4] ?? ""
3222
- ];
3223
- }
3224
- /** Whether `latest` is a newer version than `current`. Prereleases of `latest` never count. */
3225
- function isNewer(latest, current) {
3226
- const a = parse(latest);
3227
- const b = parse(current);
3228
- if (!a || !b || a[3]) return false;
3229
- for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
3230
- return b[3] !== "";
3231
- }
3232
- function readCache(path) {
3233
- try {
3234
- const data = JSON.parse(readFileSync(path, "utf8"));
3235
- if (typeof data.checkedAt === "number" && typeof data.latest === "string") return data;
3236
- } catch {}
3237
- }
3238
- function writeCache(path, cache) {
3239
- try {
3240
- mkdirSync(dirname(path), { recursive: true });
3241
- writeFileSync(path, JSON.stringify(cache));
3242
- } catch {}
3243
- }
3244
- /**
3245
- * The latest version on npm when it is newer than `current`, otherwise
3246
- * undefined. Uses a cached result for a day; never throws.
3247
- */
3248
- async function checkForUpdate(options) {
3249
- const cachePath = options.cachePath ?? defaultCachePath();
3250
- const now = options.now ?? Date.now;
3251
- const maxAge = options.maxAge ?? DAY;
3252
- let latest = readCache(cachePath);
3253
- if (!latest || now() - latest.checkedAt > maxAge || latest.checkedAt > now()) try {
3254
- const response = await (options.fetch ?? fetch)(REGISTRY_URL, {
3255
- headers: { accept: "application/json" },
3256
- signal: AbortSignal.timeout(options.timeout ?? 1500)
3257
- });
3258
- if (!response.ok) return void 0;
3259
- const { version } = await response.json();
3260
- if (typeof version !== "string") return void 0;
3261
- latest = {
3262
- checkedAt: now(),
3263
- latest: version
3264
- };
3265
- writeCache(cachePath, latest);
3266
- } catch {
3267
- return;
3268
- }
3269
- return isNewer(latest.latest, options.current) ? latest.latest : void 0;
3270
- }
3271
- /** The command that runs or installs the latest version, depending on how this one was started. */
3272
- function updateCommand(location = import.meta.url, env = process.env) {
3273
- const path = decodeURIComponent(location).replaceAll("\\", "/");
3274
- if (path.includes("/_npx/")) return `npx ${PACKAGE_NAME}@latest`;
3275
- if (path.includes("/dlx/") || path.includes("/dlx-")) return `pnpm dlx ${PACKAGE_NAME}@latest`;
3276
- if (path.includes("/bunx-")) return `bunx ${PACKAGE_NAME}@latest`;
3277
- if (path.includes("/pnpm/global/")) return `pnpm add -g ${PACKAGE_NAME}@latest`;
3278
- if (path.includes("/.bun/install/global/")) return `bun add -g ${PACKAGE_NAME}@latest`;
3279
- if (env.npm_config_user_agent?.startsWith("pnpm")) return `pnpm add -g ${PACKAGE_NAME}@latest`;
3280
- return `npm i -g ${PACKAGE_NAME}@latest`;
3281
- }
3282
2977
  // src/global-install.ts
3283
2978
  /** The name of the installed command. */
3284
2979
  const BIN = "dstack";
@@ -3297,6 +2992,37 @@ function temporaryRunner(location = import.meta.url) {
3297
2992
  if (path.includes("/dlx/") || path.includes("/dlx-")) return "pnpm";
3298
2993
  if (path.includes("/bunx-")) return "bun";
3299
2994
  }
2995
+ /** How this copy runs, and with which package manager, from where its files are. */
2996
+ function installation(location = import.meta.url, env = process.env) {
2997
+ const temporary = temporaryRunner(location);
2998
+ if (temporary) return {
2999
+ kind: "temporary",
3000
+ runner: temporary
3001
+ };
3002
+ const path = normalize(location);
3003
+ if (!path.includes("/node_modules/")) return { kind: "checkout" };
3004
+ const lower = path.toLowerCase();
3005
+ if (lower.includes("/pnpm/global/")) return {
3006
+ kind: "installed",
3007
+ runner: "pnpm"
3008
+ };
3009
+ if (lower.includes("/.bun/install/global/")) return {
3010
+ kind: "installed",
3011
+ runner: "bun"
3012
+ };
3013
+ if (lower.includes("/yarn/global/") || lower.includes("/yarn/data/global/")) return {
3014
+ kind: "installed",
3015
+ runner: "yarn"
3016
+ };
3017
+ if (env.npm_config_user_agent?.startsWith("pnpm")) return {
3018
+ kind: "installed",
3019
+ runner: "pnpm"
3020
+ };
3021
+ return {
3022
+ kind: "installed",
3023
+ runner: "npm"
3024
+ };
3025
+ }
3300
3026
  /** Whether `dstack` is on the `PATH`, leaving out the temporary directories npx and friends add. */
3301
3027
  function isOnPath(env = process.env, platform = process.platform) {
3302
3028
  const names = platform === "win32" ? [
@@ -3321,6 +3047,12 @@ function installCommand(runner = "npm") {
3321
3047
  "--global",
3322
3048
  `${PACKAGE_NAME}@latest`
3323
3049
  ];
3050
+ if (runner === "yarn") return [
3051
+ "yarn",
3052
+ "global",
3053
+ "add",
3054
+ `${PACKAGE_NAME}@latest`
3055
+ ];
3324
3056
  return [
3325
3057
  "npm",
3326
3058
  "i",
@@ -3375,7 +3107,7 @@ function explainFailure(runner, output) {
3375
3107
  const last = output.trim().split("\n").pop()?.trim();
3376
3108
  return `${last ? `${last} ` : ""}Run ${manual} to try again.`;
3377
3109
  }
3378
- function run$1(args) {
3110
+ function runInstall(args) {
3379
3111
  return new Promise((resolve) => {
3380
3112
  const [cmd, ...rest] = args;
3381
3113
  let output = "";
@@ -3433,7 +3165,7 @@ async function offerGlobalInstall() {
3433
3165
  }
3434
3166
  const spinner = p.spinner();
3435
3167
  spinner.start(`Installing with ${command(args.join(" "))}`);
3436
- const result = await run$1(args);
3168
+ const result = await runInstall(args);
3437
3169
  if (!result.ok) {
3438
3170
  saveInstallAnswer("failed");
3439
3171
  spinner.error(`Couldn't install ${BIN}`);
@@ -4437,7 +4169,8 @@ function checkSteps(project, tests) {
4437
4169
  }
4438
4170
  /** `dstack check`: the doctor's checks, Django's checks, the linters and, with --tests, the tests. */
4439
4171
  async function check(options) {
4440
- const project = activeProject(open(options));
4172
+ const base = open(options);
4173
+ const project = activeProject(base);
4441
4174
  intro("Check");
4442
4175
  let failed = 0;
4443
4176
  const spinner = p.spinner();
@@ -4518,11 +4251,12 @@ async function update(options) {
4518
4251
  } else if (existsSync(lock)) p.log.warn("uv is not installed, so backend/uv.lock stays as it is: https://docs.astral.sh/uv/");
4519
4252
  for (const target of targets) {
4520
4253
  if (compose(target, ["pull", "--ignore-buildable"]) !== 0) fail("Pulling the images failed.");
4521
- if (compose(target, [
4254
+ const build = [
4522
4255
  "build",
4523
4256
  "--pull",
4524
4257
  ...options.cache ? [] : ["--no-cache"]
4525
- ]) !== 0) fail("Building the images failed.");
4258
+ ];
4259
+ if (compose(target, build) !== 0) fail("Building the images failed.");
4526
4260
  }
4527
4261
  const volume = project.prod ? void 0 : nodeModulesVolume(project);
4528
4262
  if (volume) {
@@ -4593,12 +4327,184 @@ async function config(options) {
4593
4327
  if (result.env === envText) p.outro(pc.green("Done"));
4594
4328
  else p.outro(`Restart to apply it: ${command("dstack stop")}, then ${command("dstack dev")}`);
4595
4329
  }
4330
+ // src/update-check.ts
4331
+ const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
4332
+ const DAY = 864e5;
4333
+ /** `$XDG_CACHE_HOME/django-stack/update-check.json`, or under `~/.cache`. */
4334
+ function defaultCachePath(env = process.env) {
4335
+ const base = env.XDG_CACHE_HOME || process.platform === "win32" && env.LOCALAPPDATA || join(homedir(), ".cache");
4336
+ return join(base, "django-stack", "update-check.json");
4337
+ }
4338
+ /** Whether to check at all: never in CI, without a terminal, or when turned off. */
4339
+ function updateCheckEnabled(env = process.env, isTTY = Boolean(process.stdout.isTTY)) {
4340
+ if (!isTTY) return false;
4341
+ if (env.CI && env.CI !== "0" && env.CI !== "false") return false;
4342
+ for (const key of ["DJANGO_STACK_NO_UPDATE_CHECK", "NO_UPDATE_NOTIFIER"]) if (env[key] && env[key] !== "0") return false;
4343
+ return true;
4344
+ }
4345
+ function parse(version) {
4346
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(version.trim());
4347
+ if (!match) return void 0;
4348
+ return [
4349
+ Number(match[1]),
4350
+ Number(match[2]),
4351
+ Number(match[3]),
4352
+ match[4] ?? ""
4353
+ ];
4354
+ }
4355
+ /** Whether `latest` is a newer version than `current`. Prereleases of `latest` never count. */
4356
+ function isNewer(latest, current) {
4357
+ const a = parse(latest);
4358
+ const b = parse(current);
4359
+ if (!a || !b || a[3]) return false;
4360
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
4361
+ return b[3] !== "";
4362
+ }
4363
+ function readCache(path) {
4364
+ try {
4365
+ const data = JSON.parse(readFileSync(path, "utf8"));
4366
+ if (typeof data.checkedAt === "number" && typeof data.latest === "string") return data;
4367
+ } catch {}
4368
+ }
4369
+ function writeCache(path, cache) {
4370
+ try {
4371
+ mkdirSync(dirname(path), { recursive: true });
4372
+ writeFileSync(path, JSON.stringify(cache));
4373
+ } catch {}
4374
+ }
4375
+ /** The latest version on npm. Throws when the registry can't be reached. */
4376
+ async function fetchLatestVersion(options = {}) {
4377
+ const response = await (options.fetch ?? fetch)(REGISTRY_URL, {
4378
+ headers: { accept: "application/json" },
4379
+ signal: AbortSignal.timeout(options.timeout ?? 1500)
4380
+ });
4381
+ if (!response.ok) throw new Error(`The npm registry answered ${response.status}.`);
4382
+ const { version } = await response.json();
4383
+ if (typeof version !== "string") throw new Error("The npm registry sent no version.");
4384
+ return version;
4385
+ }
4386
+ /**
4387
+ * The latest version on npm when it is newer than `current`, otherwise
4388
+ * undefined. Uses a cached result for a day; never throws.
4389
+ */
4390
+ async function checkForUpdate(options) {
4391
+ const cachePath = options.cachePath ?? defaultCachePath();
4392
+ const now = options.now ?? Date.now;
4393
+ const maxAge = options.maxAge ?? DAY;
4394
+ let latest = readCache(cachePath);
4395
+ if (!latest || now() - latest.checkedAt > maxAge || latest.checkedAt > now()) try {
4396
+ latest = {
4397
+ checkedAt: now(),
4398
+ latest: await fetchLatestVersion(options)
4399
+ };
4400
+ writeCache(cachePath, latest);
4401
+ } catch {
4402
+ return;
4403
+ }
4404
+ return isNewer(latest.latest, options.current) ? latest.latest : void 0;
4405
+ }
4406
+ /**
4407
+ * The command that gets the latest version: through the runner for npx,
4408
+ * pnpm dlx and bunx, otherwise `dstack self-update`.
4409
+ */
4410
+ function updateCommand(location = import.meta.url, env = process.env) {
4411
+ const install = installation(location, env);
4412
+ if (install.kind === "temporary") {
4413
+ if (install.runner === "pnpm") return `pnpm dlx ${PACKAGE_NAME}@latest`;
4414
+ if (install.runner === "bun") return `bunx ${PACKAGE_NAME}@latest`;
4415
+ return `npx ${PACKAGE_NAME}@latest`;
4416
+ }
4417
+ if (install.kind === "checkout") return "git pull";
4418
+ return "dstack self-update";
4419
+ }
4420
+ // src/commands/self-update.ts
4421
+ /** The command each package manager runs a package with, without installing it. */
4422
+ const RUN_WITHOUT_INSTALL = {
4423
+ npm: "npx",
4424
+ pnpm: "pnpm dlx",
4425
+ bun: "bunx",
4426
+ yarn: "yarn dlx"
4427
+ };
4428
+ /**
4429
+ * The npm prefix this copy is installed in, such as `/usr/local` for
4430
+ * `/usr/local/lib/node_modules/@django-stack/cli`, so the update replaces this
4431
+ * copy even when another Node.js version's npm comes first on the `PATH`.
4432
+ */
4433
+ function npmPrefix(location = import.meta.url) {
4434
+ if (!location.startsWith("file:")) return void 0;
4435
+ const path = fileURLToPath(location);
4436
+ return /^(.+)\/lib\/node_modules\/@django-stack\/cli\//.exec(path)?.[1];
4437
+ }
4438
+ /** The version of the `dstack` on the `PATH`, or undefined when there is none. */
4439
+ function versionOnPath() {
4440
+ try {
4441
+ return execFileSync(BIN, ["--version"], {
4442
+ encoding: "utf8",
4443
+ shell: process.platform === "win32",
4444
+ stdio: [
4445
+ "ignore",
4446
+ "pipe",
4447
+ "ignore"
4448
+ ],
4449
+ env: {
4450
+ ...process.env,
4451
+ DJANGO_STACK_NO_UPDATE_CHECK: "1"
4452
+ }
4453
+ }).trim();
4454
+ } catch {
4455
+ return;
4456
+ }
4457
+ }
4458
+ /** Update the installed CLI to the latest version on npm, with the package manager that installed it. */
4459
+ async function selfUpdate() {
4460
+ intro("Self-update");
4461
+ const install = installation();
4462
+ if (install.kind === "checkout") fail(`This ${BIN} runs from a DjangoStack checkout. Update it with git pull and pnpm build.`);
4463
+ if (install.kind === "temporary") {
4464
+ const runWith = RUN_WITHOUT_INSTALL[install.runner];
4465
+ fail(`This ${BIN} runs through ${runWith} and is not installed. Run ${runWith} ${PACKAGE_NAME}@latest for the latest version, or install it with ${installCommand(install.runner).join(" ")}.`);
4466
+ }
4467
+ const { runner } = install;
4468
+ const spinner = p.spinner();
4469
+ spinner.start("Checking npm for the latest version");
4470
+ let latest;
4471
+ try {
4472
+ latest = await fetchLatestVersion({ timeout: 1e4 });
4473
+ } catch (error) {
4474
+ spinner.error("Couldn't check npm");
4475
+ fail(`Couldn't reach the npm registry: ${error.message}`);
4476
+ }
4477
+ if (!isNewer(latest, CLI_VERSION)) {
4478
+ spinner.stop(`${BIN} ${CLI_VERSION} is the latest version`);
4479
+ p.outro(pc.green("Nothing to update"));
4480
+ return;
4481
+ }
4482
+ spinner.stop(`Version ${pc.green(pc.bold(latest))} is available`);
4483
+ const prefix = runner === "npm" ? npmPrefix() : void 0;
4484
+ const args = prefix ? [
4485
+ ...installCommand(runner),
4486
+ "--prefix",
4487
+ prefix
4488
+ ] : installCommand(runner);
4489
+ if (!has(runner)) fail(`${runner} installed ${BIN} but is not on your PATH. Run ${args.join(" ")} yourself.`);
4490
+ spinner.start(`Updating with ${command(args.join(" "))}`);
4491
+ const result = await runInstall(args);
4492
+ if (!result.ok) {
4493
+ spinner.error(`Couldn't update ${BIN}`);
4494
+ fail(explainFailure(runner, result.output));
4495
+ }
4496
+ spinner.stop(`Updated ${BIN} ${pc.dim(CLI_VERSION)} ${pc.dim("→")} ${pc.green(pc.bold(latest))}`);
4497
+ const onPath = versionOnPath();
4498
+ if (onPath && onPath !== latest) p.log.warn(`The ${command(BIN)} on your PATH is still ${onPath}: another install comes first. Remove it, or update it with its own package manager.`);
4499
+ p.outro(pc.green("Done"));
4500
+ }
4596
4501
  // src/program.ts
4597
- /** Commands that print machine-readable output, so no update notice. */
4502
+ /** Commands that print machine-readable output or update the CLI, so no update notice. */
4598
4503
  const QUIET_COMMANDS = /* @__PURE__ */ new Set([
4599
4504
  "secret",
4600
4505
  "complete",
4601
- "completion"
4506
+ "completion",
4507
+ "self-update"
4602
4508
  ]);
4603
4509
  /** A flag for each stack option, such as `--cache <cache>`. */
4604
4510
  function stackFlags() {
@@ -4670,6 +4576,7 @@ function createProgram() {
4670
4576
  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);
4671
4577
  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);
4672
4578
  program.command("doctor").description("Check a project and its services").argument("[directory]", "The project directory", ".").action(doctor);
4579
+ program.command("self-update").description(`Update ${BIN} to the latest version on npm`).action(selfUpdate);
4673
4580
  program.command("secret").description("Print a random secret").argument("[bytes]", "The number of random bytes", positiveInteger, 32).action((bytes) => console.log(generateSecret(bytes)));
4674
4581
  tab(program, { completionCommandName: "completion" });
4675
4582
  program.commands.find((cmd) => cmd.name() === "completion")?.description("Print a shell completion script (zsh, bash, fish or powershell)").addHelpText("after", `\nAdd it to your shell, for example in ~/.zshrc:\n ${pc.cyan("source <(dstack completion zsh)")}`);
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { y as DEFAULT_STACK, i as FRONTENDS, b as NAMED_PRESETS, S as STACK_OPTIONS, v as VERSION, o as cacheEnv, w as decodePreset, T as encodePreset, k as generateProjectSecrets, A as generateSecret, l as normalizeDatabaseUrl, V as parseEnv, E as pendingChoices, n as productionEnv, D as resolvePreset, I as runDoctor, d as scaffold, U as setEnvValues, p as toProjectName, h as usesValkey, r as validateDomain, g as validateProjectName } from "./production-DBpWWx6i.js";
1
+ export { P as DEFAULT_STACK, i as FRONTENDS, F as NAMED_PRESETS, L as STACK_OPTIONS, v as VERSION, o as cacheEnv, z as decodePreset, B as encodePreset, b as generateProjectSecrets, x as generateSecret, l as normalizeDatabaseUrl, et as parseEnv, V as pendingChoices, n as productionEnv, H as resolvePreset, Y as runDoctor, d as scaffold, nt as setEnvValues, p as toProjectName, h as usesValkey, r as validateDomain, g as validateProjectName } from "./production-CGkI7oNe.js";
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
- import { join, relative } from "node:path";
4
- import { execFileSync } from "node:child_process";
2
+ import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { execFileSync, spawnSync } from "node:child_process";
5
5
  import { randomBytes, randomUUID } from "node:crypto";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { createConnection, createServer } from "node:net";
@@ -436,25 +436,6 @@ async function chooseFreePorts(env, profiles, isFree = isPortFree) {
436
436
  function describeMove(move) {
437
437
  return `Port ${move.from} is in use, so the ${move.label} uses ${move.to}.`;
438
438
  }
439
- // src/secrets.ts
440
- /** A URL-safe random secret with `bytes` bytes of randomness. */
441
- function generateSecret(bytes = 32) {
442
- return randomBytes(bytes).toString("base64url");
443
- }
444
- /** The secrets a project's .env needs, by the name used in `{{secret:<name>}}`. */
445
- function generateProjectSecrets() {
446
- return {
447
- django: generateSecret(48),
448
- postgres: generateSecret(24),
449
- api: generateSecret(32),
450
- api_read: generateSecret(32),
451
- preview: generateSecret(32),
452
- revalidate: generateSecret(32),
453
- admin: generateSecret(12),
454
- umami_app: generateSecret(32),
455
- umami_admin: generateSecret(24)
456
- };
457
- }
458
439
  // src/stack.ts
459
440
  const STACK_OPTIONS = [
460
441
  {
@@ -865,9 +846,270 @@ function pendingChoices(stack) {
865
846
  }];
866
847
  });
867
848
  }
849
+ // src/project.ts
850
+ /** The `.env` of `dstack run` on a development machine: the prod profile at https://site.localhost. */
851
+ const RUN_ENV = ".env.run";
852
+ const RUN_DOMAIN = "site.localhost";
853
+ /**
854
+ * The project's scripts that must be executable. npm and pnpm packages do
855
+ * not keep the executable bit of non-bin files, so the templates arrive
856
+ * without it.
857
+ */
858
+ const EXECUTABLES = ["backend/entrypoint.sh", "backend/manage.py"];
859
+ /** Makes the project's scripts executable for everyone who can read them. */
860
+ function makeExecutable(directory) {
861
+ for (const file of EXECUTABLES) {
862
+ const path = join(directory, file);
863
+ try {
864
+ const { mode } = statSync(path);
865
+ const executable = mode | (mode & 292) >> 2;
866
+ if (executable !== mode) chmodSync(path, executable & 4095);
867
+ } catch {}
868
+ }
869
+ }
870
+ /** The project directory: `start` or the nearest parent with `compose.yaml` and `.env`. */
871
+ function findProjectDirectory(start) {
872
+ let directory = resolve(start);
873
+ for (;;) {
874
+ if (existsSync(join(directory, "compose.yaml")) && existsSync(join(directory, ".env"))) return directory;
875
+ const parent = dirname(directory);
876
+ if (parent === directory) return void 0;
877
+ directory = parent;
878
+ }
879
+ }
880
+ function parseProfiles(value) {
881
+ return (value ?? "").split(",").map((profile) => profile.trim()).filter(Boolean);
882
+ }
883
+ function projectName(directory) {
884
+ try {
885
+ const match = readFileSync(join(directory, "compose.yaml"), "utf8").match(/^name:\s*["']?([\w.-]+)/m);
886
+ if (match?.[1]) return match[1];
887
+ } catch {}
888
+ return basename(directory).toLowerCase();
889
+ }
890
+ function fromEnv(directory, name, envFile, localRun) {
891
+ const env = parseEnv(readFileSync(join(directory, envFile), "utf8"));
892
+ const profiles = parseProfiles(env.COMPOSE_PROFILES);
893
+ const prod = profiles.includes("prod");
894
+ const suffix = prod ? "-prod" : "";
895
+ return {
896
+ directory,
897
+ name: localRun ? `${name}-run` : name,
898
+ env,
899
+ profiles,
900
+ prod,
901
+ localRun,
902
+ services: {
903
+ backend: `backend${suffix}`,
904
+ frontend: `frontend${suffix}`,
905
+ worker: `worker${suffix}`
906
+ },
907
+ composeArgs: localRun ? [
908
+ "--env-file",
909
+ envFile,
910
+ "--project-name",
911
+ `${name}-run`
912
+ ] : [],
913
+ composeEnv: localRun ? {
914
+ ENV_FILE: envFile,
915
+ COMPOSE_PROFILES: env.COMPOSE_PROFILES ?? ""
916
+ } : {}
917
+ };
918
+ }
919
+ /** Reads the project in or above `start`. Throws with a one-line reason when there is none. */
920
+ function readProject(start = ".") {
921
+ const directory = findProjectDirectory(start);
922
+ if (!directory) throw new Error(`No DjangoStack project in ${resolve(start)}: run this where compose.yaml and .env are.`);
923
+ makeExecutable(directory);
924
+ return fromEnv(directory, projectName(directory), ".env", false);
925
+ }
926
+ /** The local production run of a development project, when `.env.run` exists. */
927
+ function readLocalRun(project) {
928
+ if (project.prod || project.localRun || !existsSync(join(project.directory, ".env.run"))) return void 0;
929
+ return fromEnv(project.directory, projectName(project.directory), RUN_ENV, true);
930
+ }
931
+ /** The `django-stack` section of the project's `package.json`. */
932
+ function projectManifest(project) {
933
+ try {
934
+ return JSON.parse(readFileSync(join(project.directory, "package.json"), "utf8"))["django-stack"] ?? {};
935
+ } catch {
936
+ return {};
937
+ }
938
+ }
939
+ /**
940
+ * The project's stack: from the preset `init` saved in `package.json`, or,
941
+ * for projects from before that, the defaults with the storage from `.env`.
942
+ * Analytics follow the `analytics` profile: projects from before Umami was
943
+ * generated have the default in their preset without running it.
944
+ */
945
+ function projectStack(project) {
946
+ const fromEnv = {
947
+ analytics: project.profiles.includes("analytics") ? "umami" : "off",
948
+ ...project.env.CACHE_URL === void 0 ? {} : cacheStack(project.env)
949
+ };
950
+ const { preset } = projectManifest(project);
951
+ if (preset) try {
952
+ return {
953
+ ...decodePreset(preset),
954
+ ...fromEnv
955
+ };
956
+ } catch {}
957
+ return {
958
+ ...DEFAULT_STACK,
959
+ ...fromEnv,
960
+ storage: project.profiles.includes("s3") ? "s3" : "local"
961
+ };
962
+ }
963
+ const REDIS = /^(redis|rediss|valkey|valkeys):/;
964
+ /**
965
+ * The cache and the tasks a project's `.env` runs. Projects from before 0.7
966
+ * have no TASKS_URL: their tasks follow a Redis cache, like the backend does.
967
+ */
968
+ function cacheStack(env) {
969
+ const cacheUrl = env.CACHE_URL ?? "";
970
+ const cache = cacheUrl.startsWith("db:") ? "database" : REDIS.test(cacheUrl) ? "valkey" : "off";
971
+ const tasksUrl = env.TASKS_URL || (cache === "valkey" ? cacheUrl : "immediate://");
972
+ if (tasksUrl.startsWith("immediate:")) return {
973
+ cache,
974
+ tasks: "off"
975
+ };
976
+ return {
977
+ cache,
978
+ tasks: REDIS.test(tasksUrl) && cache !== "valkey" ? "huey-valkey" : "huey"
979
+ };
980
+ }
981
+ /** The URLs of a running project. */
982
+ function projectUrls(project) {
983
+ if (project.prod) {
984
+ const site = (project.env.SITE_URL ?? "").replace(/\/$/, "");
985
+ return [{
986
+ label: "Site",
987
+ url: `${site}/`
988
+ }, {
989
+ label: "Admin",
990
+ url: `${site}/admin/`
991
+ }];
992
+ }
993
+ const ports = envPorts(project.env);
994
+ const site = (project.env.SITE_URL || `http://localhost:${ports.SITE_PORT}`).replace(/\/$/, "");
995
+ const urls = [
996
+ {
997
+ label: "Site",
998
+ url: site
999
+ },
1000
+ {
1001
+ label: "Admin",
1002
+ url: `${site}/admin/`
1003
+ },
1004
+ {
1005
+ label: "Mail",
1006
+ url: `http://localhost:${ports.MAIL_PORT}`
1007
+ }
1008
+ ];
1009
+ if (project.profiles.includes("s3")) urls.push({
1010
+ label: "S3",
1011
+ url: `http://localhost:${ports.S3_PORT}`
1012
+ });
1013
+ return urls;
1014
+ }
1015
+ /** Runs a command with the terminal attached and returns its exit status. */
1016
+ const runInherit = (cmd, args, options) => {
1017
+ const result = spawnSync(cmd, args, {
1018
+ stdio: "inherit",
1019
+ ...options
1020
+ });
1021
+ if (result.error) return 127;
1022
+ return result.status ?? 1;
1023
+ };
1024
+ /** The full `docker compose` argument list for a project. */
1025
+ function composeCommand(project, args) {
1026
+ return [
1027
+ "compose",
1028
+ ...project.composeArgs,
1029
+ ...args
1030
+ ];
1031
+ }
1032
+ function composeOptions(project) {
1033
+ return {
1034
+ cwd: project.directory,
1035
+ env: {
1036
+ ...process.env,
1037
+ ...project.composeEnv
1038
+ }
1039
+ };
1040
+ }
1041
+ /** Runs `docker compose <args>` for the project with the terminal attached. */
1042
+ function compose(project, args, run = runInherit) {
1043
+ return run("docker", composeCommand(project, args), composeOptions(project));
1044
+ }
1045
+ /** Runs `docker compose <args>` and captures the output. */
1046
+ function composeOutput(project, args) {
1047
+ const result = spawnSync("docker", composeCommand(project, args), {
1048
+ ...composeOptions(project),
1049
+ encoding: "utf8",
1050
+ stdio: [
1051
+ "ignore",
1052
+ "pipe",
1053
+ "pipe"
1054
+ ]
1055
+ });
1056
+ return {
1057
+ status: result.status ?? 1,
1058
+ output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
1059
+ };
1060
+ }
1061
+ /** The running services of the project. */
1062
+ function runningServices(project) {
1063
+ const result = spawnSync("docker", composeCommand(project, [
1064
+ "ps",
1065
+ "--services",
1066
+ "--filter",
1067
+ "status=running"
1068
+ ]), {
1069
+ ...composeOptions(project),
1070
+ encoding: "utf8",
1071
+ stdio: [
1072
+ "ignore",
1073
+ "pipe",
1074
+ "ignore"
1075
+ ]
1076
+ });
1077
+ return result.status === 0 ? result.stdout.split("\n").filter(Boolean) : [];
1078
+ }
1079
+ /**
1080
+ * The project a command acts on: the local production run when it is the
1081
+ * one running, otherwise the project `.env` describes.
1082
+ */
1083
+ function activeProject(project) {
1084
+ const localRun = readLocalRun(project);
1085
+ if (!localRun) return project;
1086
+ if (runningServices(project).length) return project;
1087
+ return runningServices(localRun).length ? localRun : project;
1088
+ }
1089
+ // src/secrets.ts
1090
+ /** A URL-safe random secret with `bytes` bytes of randomness. */
1091
+ function generateSecret(bytes = 32) {
1092
+ return randomBytes(bytes).toString("base64url");
1093
+ }
1094
+ /** The secrets a project's .env needs, by the name used in `{{secret:<name>}}`. */
1095
+ function generateProjectSecrets() {
1096
+ return {
1097
+ django: generateSecret(48),
1098
+ postgres: generateSecret(24),
1099
+ api: generateSecret(32),
1100
+ api_read: generateSecret(32),
1101
+ preview: generateSecret(32),
1102
+ revalidate: generateSecret(32),
1103
+ admin: generateSecret(12),
1104
+ umami_app: generateSecret(32),
1105
+ umami_admin: generateSecret(24)
1106
+ };
1107
+ }
868
1108
  // src/version.ts
869
1109
  /** The version of this package, the same for every @django-stack package. */
870
1110
  const CLI_VERSION = createRequire(import.meta.url)("../package.json").version;
1111
+ /** The npm package of the CLI. */
1112
+ const PACKAGE_NAME = "@django-stack/cli";
871
1113
  // src/scaffold.ts
872
1114
  /** The frontends this version of the CLI generates. */
873
1115
  const FRONTENDS = stackOption("frontend").values.filter((value) => value.generated).map((value) => value.value);
@@ -992,6 +1234,7 @@ function scaffold(options) {
992
1234
  cpSync(join(templates, "backend"), join(target, "backend"), { recursive: true });
993
1235
  cpSync(frontendTemplate, join(target, "frontend"), { recursive: true });
994
1236
  renameSync(join(target, "gitignore"), join(target, ".gitignore"));
1237
+ makeExecutable(target);
995
1238
  const about = join(target, "frontend", "template.json");
996
1239
  const frontend = JSON.parse(readFileSync(about, "utf8"));
997
1240
  rmSync(about);
@@ -1198,4 +1441,4 @@ function productionEnv(devEnv, options) {
1198
1441
  todo
1199
1442
  };
1200
1443
  }
1201
- export { generateSecret as A, isDockerDown as B, applies as C, resolvePreset as D, pendingChoices as E, portValues as F, quoteEnvValue as H, runDoctor as I, checkTools as L, chooseFreePorts as M, describeMove as N, stackOption as O, envPorts as P, dockerNotRunning as R, STACK_OPTIONS as S, encodePreset as T, setEnvValues as U, parseEnv as V, validateTimeZone as _, LOCAL_S3 as a, NAMED_PRESETS as b, lockBackend as c, scaffold as d, timeZones as f, validateProjectName as g, usesValkey as h, FRONTENDS as i, DEV_PORTS as j, generateProjectSecrets as k, normalizeDatabaseUrl as l, umamiEnv as m, productionEnv as n, cacheEnv as o, toProjectName as p, validateDomain as r, localS3 as s, missingS3 as t, relativeDirectory as u, CLI_VERSION as v, decodePreset as w, PROMPT_ORDER as x, DEFAULT_STACK as y, dockerRunning as z };
1444
+ export { isDockerDown as $, projectUrls as A, encodePreset as B, RUN_ENV as C, findProjectDirectory as D, composeOutput as E, NAMED_PRESETS as F, chooseFreePorts as G, resolvePreset as H, PROMPT_ORDER as I, portValues as J, describeMove as K, STACK_OPTIONS as L, readProject as M, runningServices as N, parseProfiles as O, DEFAULT_STACK as P, dockerRunning as Q, applies as R, RUN_DOMAIN as S, compose as T, stackOption as U, pendingChoices as V, DEV_PORTS as W, checkTools as X, runDoctor as Y, dockerNotRunning as Z, validateTimeZone as _, LOCAL_S3 as a, generateProjectSecrets as b, lockBackend as c, scaffold as d, parseEnv as et, timeZones as f, validateProjectName as g, usesValkey as h, FRONTENDS as i, readLocalRun as j, projectStack as k, normalizeDatabaseUrl as l, umamiEnv as m, productionEnv as n, setEnvValues as nt, cacheEnv as o, toProjectName as p, envPorts as q, validateDomain as r, localS3 as s, missingS3 as t, quoteEnvValue as tt, relativeDirectory as u, CLI_VERSION as v, activeProject as w, generateSecret as x, PACKAGE_NAME as y, decodePreset as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@django-stack/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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",
@@ -25,10 +25,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
25
25
 
26
26
  # Development runs as root, so files the container writes into the mounted
27
27
  # source (such as new migrations) work on every host. Production does not.
28
+ # The entrypoint runs with sh, so it works without the executable bit (the
29
+ # mounted source keeps the host's file modes).
28
30
  FROM deps AS dev
29
31
  RUN --mount=type=cache,target=/root/.cache/uv \
30
32
  uv sync --no-install-project
31
- ENTRYPOINT ["/app/entrypoint.sh"]
33
+ ENTRYPOINT ["sh", "/app/entrypoint.sh"]
32
34
  CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
33
35
 
34
36
  FROM base AS prod
@@ -41,5 +43,5 @@ USER app
41
43
  EXPOSE 8000
42
44
  HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
43
45
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/readyz', timeout=4)"
44
- ENTRYPOINT ["/app/entrypoint.sh"]
46
+ ENTRYPOINT ["sh", "/app/entrypoint.sh"]
45
47
  CMD ["gunicorn", "config.asgi:application", "--worker-class", "uvicorn_worker.UvicornWorker", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-", "--no-control-socket"]
@@ -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.7.0",
8
+ "django-stack[server,s3]>=0.8.1",
9
9
  "django-headless[schema]>=1.1.1",
10
10
  "django-content-studio[images,huey]>=1.11.0",
11
11
  ]
@@ -14,7 +14,10 @@ RUN --mount=type=cache,target=/root/.npm \
14
14
 
15
15
  FROM deps AS dev
16
16
  EXPOSE 3000
17
- CMD ["npm", "run", "dev"]
17
+ # A container runs one dev server, so a lock file in the mounted .astro/ is
18
+ # always stale: from a stopped container, whose process IDs the new one reuses,
19
+ # or from `npm run dev` on the host. Astro would refuse to start next to it.
20
+ CMD ["sh", "-c", "rm -f .astro/dev.json && exec npm run dev"]
18
21
 
19
22
  FROM deps AS build
20
23
  COPY . .
@@ -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.7.0",
19
+ "@django-stack/utils": "^0.8.1",
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.7.0",
17
- "@django-stack/react": "^0.7.0",
18
- "@django-stack/utils": "^0.7.0",
16
+ "@django-stack/next": "^0.8.1",
17
+ "@django-stack/react": "^0.8.1",
18
+ "@django-stack/utils": "^0.8.1",
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.7.0",
19
- "@django-stack/vue": "^0.7.0",
18
+ "@django-stack/utils": "^0.8.1",
19
+ "@django-stack/vue": "^0.8.1",
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.7.0",
23
- "@django-stack/utils": "^0.7.0"
22
+ "@django-stack/svelte": "^0.8.1",
23
+ "@django-stack/utils": "^0.8.1"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@sveltejs/adapter-node": "^6.0.1",