#!/usr/bin/env bash # ===================================================================== # LogShip template — step 5 (on every clone, after 03-neutralize-env.sh): make the # instance's .env EFFECTIVE without a rebuild. # # Every `runtimeConfig` value read from `process.env.X` in nuxt.config.ts is baked into # .output/server/chunks/nitro/nitro.mjs at BUILD time (on prod, with prod's .env). A clone # therefore keeps prod's service token, Laravel URL, carrier/marketplace credentials, # Sentry DSN and public URLs no matter what 03-neutralize-env.sh writes — only Nitro's # runtime override layer (`NUXT_` env vars, read at every start) changes them # (see .claude/rules/ops/runtimeconfig-env-reload.md). # # This script parses the runtimeConfig block of the deployed nuxt.config.ts, resolves each # `process.env.VAR` leaf against the instance .env (honouring `|| 'default'`, `Number(... || n)`, # `=== 'true'`, `!== 'false'`) and (re)writes a managed block of NUXT_* lines at the end of # .env. ecosystem.config.cjs loads .env into the process env, so a plain # pm2 delete erpfrontend && pm2 start ecosystem.config.cjs --only erpfrontend --update-env # (NOT `pm2 restart`, which keeps the old env) applies them. Idempotent; run after every # .env edit on an instance that was not rebuilt with that .env. # # 05-apply-env-overrides.sh [--dry-run] [ENV_FILE] [NUXT_CONFIG] # ===================================================================== set -euo pipefail DRY=0; [ "${1:-}" = "--dry-run" ] && { DRY=1; shift; } ENVF="${1:-/opt/erp-nuxt-frontend/.env}" CFG="${2:-/opt/erp-nuxt-frontend/nuxt.config.ts}" [ -f "$ENVF" ] && [ -f "$CFG" ] || { echo "FATAL: $ENVF or $CFG missing" >&2; exit 1; } python3 - "$ENVF" "$CFG" "$DRY" <<'PY' import re, sys envf, cfg, dry = sys.argv[1], sys.argv[2], sys.argv[3] == "1" BEGIN = "# >>> nitro runtime overrides (generated by 05-apply-env-overrides.sh — do not edit, re-run the script) >>>" END = "# <<< nitro runtime overrides <<<" # --- .env values ------------------------------------------------------------------------------ env = {} raw = open(envf, encoding="utf-8").read().split("\n") for l in raw: m = re.match(r"^([A-Z_0-9]+)=(.*)$", l) if not m: continue v = m.group(2).strip() if len(v) >= 2 and v[0] == v[-1] and v[0] in "'\"": v = v[1:-1] env[m.group(1)] = v # --- runtimeConfig block of nuxt.config.ts ------------------------------------------------------ src = open(cfg, encoding="utf-8").read().split("\n") start = next(i for i, l in enumerate(src) if re.match(r"^\s*runtimeConfig:\s*\{\s*$", l)) indent = len(src[start]) - len(src[start].lstrip()) path, leaves = [], [] def snake(s): # scule snakeCase: camelCase -> camel_case, keeps digits attached s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s) s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s) return s.upper() for l in src[start + 1:]: stripped = l.strip() if re.match(r"^\},?$", stripped): if not path: break path.pop(); continue if not stripped or stripped.startswith("//"): continue m = re.match(r"^(\w+):\s*\{\s*$", stripped) if m: path.append(m.group(1)); continue m = re.match(r"^(\w+):\s*(.+?),?\s*(//.*)?$", stripped) if not m: continue key, expr = m.group(1), m.group(2) vars_ = re.findall(r"process\.env\.([A-Z_0-9]+)", expr) if not vars_: continue var = vars_[-1] # non-capacitor branch of `isCapacitor ? … : process.env.X` v = env.get(var) if "=== 'true'" in expr: out = "true" if v == "true" else "false" elif "!== 'false'" in expr: out = "false" if v == "false" else "true" else: dflt = re.search(r"\|\|\s*('([^']*)'|\"([^\"]*)\"|([0-9.]+))", expr) or re.search(r"\?\?\s*('([^']*)'|\"([^\"]*)\"|([0-9.]+))", expr) if v: out = v elif dflt: out = dflt.group(2) if dflt.group(2) is not None else (dflt.group(3) if dflt.group(3) is not None else dflt.group(4)) else: out = "" name = "NUXT_" + "_".join(snake(p) for p in path + [key]) leaves.append((name, var, out)) # --- rewrite the managed block -------------------------------------------------------------------- lines = raw if BEGIN in lines: a, b = lines.index(BEGIN), lines.index(END) lines = lines[:a] + lines[b + 1:] managed = {n for n, _, _ in leaves} lines = [l for l in lines if not (re.match(r"^(NUXT_[A-Z_0-9]+)=", l) and re.match(r"^(NUXT_[A-Z_0-9]+)=", l).group(1) in managed)] while lines and lines[-1] == "": lines.pop() block = [BEGIN] + [f"{n}='{o.replace(chr(39), chr(39)+chr(92)+chr(39)+chr(39))}'" for n, _, o in leaves] + [END, ""] if dry: for n, var, o in leaves: print(f"{n:48s} <- {var:34s} = {o[:60]!r}") print(f"\n{len(leaves)} overrides (dry run, {envf} unchanged)") else: open(envf, "w", encoding="utf-8").write("\n".join(lines + [""] + block)) print(f"{len(leaves)} NUXT_* overrides written to {envf}. Apply with:\n cd /opt/erp-nuxt-frontend && pm2 delete erpfrontend; pm2 start ecosystem.config.cjs --only erpfrontend --update-env") PY