mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-07 07:18:18 +00:00
merge master into feat/zsetup-command, keeping both zsetup and startApp keys
Five PRs (#11, #23, #10, #12, #24) landed on master since this branch opened. Conflicts were all additive and in the same spots this PR documents its new 'install' key: README's config reference and both zconfig.example.json files, where #12 had added the startApp note on the adjacent line. Resolved by keeping both keys, with the startApp note next to startModule (it documents that key) and install after it. Nothing dropped from either side.
This commit is contained in:
commit
d34dfd535a
22
CHANGELOG.md
22
CHANGELOG.md
@ -22,6 +22,18 @@ Notable changes to the Evomedia.net Token Savers.
|
||||
credentials and RSA signing keys.
|
||||
|
||||
### Added
|
||||
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
||||
tool for when a secret leaks or a deploy overwrites a production `.env`
|
||||
with dev values. `-Rotate KEY` regenerates a key **on the server**
|
||||
(`openssl rand -hex 32`) so the new value never leaves the box; `-Set KEY`
|
||||
takes an operator-known value (e.g. `DATABASE_URL`, `ADMIN_EMAIL`) from a
|
||||
masked prompt and streams it over SSH stdin — never a command argument,
|
||||
never echoed. Backs the server `.env` up to a timestamped `.bak` first,
|
||||
updates the key atomically (matches or appends), auto-detects
|
||||
`backend/.env` from `deploy.preserve`, and with `-Restart` **recreates**
|
||||
the container (`up -d --force-recreate`, so the new values actually load —
|
||||
a plain restart keeps the old environment). `-WhatIf` previews the plan
|
||||
without touching anything.
|
||||
- **`zkill all`** — `zkill` now accepts `all`, stopping the dev server of
|
||||
every project that has a `ports.dev` (edge/docker stacks with no local dev
|
||||
server are skipped). Brings it in line with `zdeploy all` / `zbackup all`;
|
||||
@ -49,6 +61,16 @@ Notable changes to the Evomedia.net Token Savers.
|
||||
grew up on per-project switches.
|
||||
|
||||
### Changed
|
||||
- **`zbackup` / `zbackup_and_sync` require an explicit target** — running
|
||||
them bare now shows usage instead of quietly backing up every project;
|
||||
`all` does what bare invocation used to (matching `zdeploy`). The
|
||||
scheduled task created by `setup_backup_schedule.ps1` passes `all` —
|
||||
re-run it if your task was registered before this change.
|
||||
- **`zbackup` parses more `DATABASE_URL` styles** — double/single-quoted
|
||||
values (Prisma convention), `postgres://` and `postgresql+driver://`
|
||||
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
||||
previously these skipped the Postgres dump with "Could not parse
|
||||
DATABASE_URL".
|
||||
- **`zkill` / port cleanup kills the whole process tree** — listeners on a
|
||||
project's port are now terminated children-first. Auto-reloading servers
|
||||
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
||||
|
||||
20
README.md
20
README.md
@ -72,6 +72,7 @@ The example config ships with sample projects named by their kind — `pyapp`, `
|
||||
"kind": "python", // python | vite | nextjs | edge | docker
|
||||
"localRoot": "C:\\dev\\myapp", // project folder on this machine
|
||||
"startModule": "myapp.main", // python kind: runs "python -m myapp.main"
|
||||
// "startApp": "app.main:app", // ...or, for ASGI/FastAPI: uvicorn app.main:app --port <dev> --reload
|
||||
"install": "-e .", // optional: pip args `zsetup` uses (auto-detects "-e ." / "-r requirements.txt")
|
||||
"ports": { "dev": 8080, "prod": 3000 }, // local dev port / direct server port
|
||||
"domain": "www.myapp.com", // public domain (health checks + verification)
|
||||
@ -125,6 +126,7 @@ The `.cmd` wrappers are the everyday interface. Every command takes one or more
|
||||
| `zec2 [<key> ...]` | Quick reachability check (TCP + HTTP + live build version) |
|
||||
| `zec2online [<key> ...]` | Deep health check; auto-starts downed stacks, streams diagnostics |
|
||||
| `zrepair <key> ...` | Audit + repair compose/proxy state on the server |
|
||||
| `zec2_rotatekeys <key>` | Rotate/reset secret keys in a project's server-side `.env` (values generated server-side; never printed) |
|
||||
| `zbackup <key> ... \| all` | Zip local project sources (+ DB dump) to the backups folder |
|
||||
| `zbackup_ec2 [<key> ...]` | Pull DB dumps + server-side data files down from the server |
|
||||
| `zsync [<key>]` | Copy new backups offsite (or build + mirror a vite dist) |
|
||||
@ -138,7 +140,7 @@ The `.cmd` wrappers are the everyday interface. Every command takes one or more
|
||||
zstart <project> [<project> ...] [-Port N] [-BindHost <host>] [-Detached]
|
||||
```
|
||||
|
||||
Starts each project's dev server using the handler for its `kind`: **python** runs `python -m <startModule>` (preferring the project's `.venv`), **vite** runs `npm run dev -- --host --port`, **nextjs** runs `npm run dev` with `PORT` set. Runs `npm install` automatically if `node_modules` is missing. A project's optional `start` config block runs first — `gitPull` fast-forwards the checkout and `env` sets process environment variables. Two more opt-in conveniences: if the project has a `motd/` folder of `.txt` files, one is shown (rotating) at startup; if it has `scripts/build_version_tool.py`, the build number is bumped on each start.
|
||||
Starts each project's dev server using the handler for its `kind`: **python** runs `python -m <startModule>` — or, for an ASGI/FastAPI app, `uvicorn <startApp>` (e.g. `app.main:app`) with the dev port and `--reload` — preferring the project's `.venv`; **vite** runs `npm run dev -- --host --port`, **nextjs** runs `npm run dev` with `PORT` set. Runs `npm install` automatically if `node_modules` is missing. A project's optional `start` config block runs first — `gitPull` fast-forwards the checkout and `env` sets process environment variables. Two more opt-in conveniences: if the project has a `motd/` folder of `.txt` files, one is shown (rotating) at startup; if it has `scripts/build_version_tool.py`, the build number is bumped on each start.
|
||||
|
||||
```powershell
|
||||
zstart viteapp # dev server on its configured port
|
||||
@ -225,6 +227,22 @@ zstop <project> [<project> ...]
|
||||
|
||||
`docker compose down` for the selected stacks on the server. Data volumes are preserved; `zdeploy <project>` brings a stack back. (PowerShell script only, no `.cmd` wrapper.)
|
||||
|
||||
#### `zec2_rotatekeys` — rotate server-side secrets
|
||||
|
||||
```
|
||||
zec2_rotatekeys <project> [-Rotate KEY,KEY] [-Set KEY,KEY] [-EnvFile rel/path] [-Restart] [-WhatIf]
|
||||
```
|
||||
|
||||
For when a secret leaks or a deploy overwrites a production `.env` with dev values: rotate or reset keys in a project's **server-side** `.env` without the values ever passing through this machine's shell history, a command argument, or your screen. `-Rotate` keys are regenerated **on the server** with `openssl rand -hex 32` — the new value is written straight into the `.env` there and never leaves the box. `-Set` keys are typed into a masked prompt and streamed to the server over SSH stdin (never a command argument, never echoed), for operator-known values like `DATABASE_URL` or `ADMIN_EMAIL`. The current server `.env` is copied to a timestamped `.bak` before any change; the KEY line is updated atomically, matching an existing key or appending it. The env file is auto-detected from the project's `deploy.preserve` (first `*.env`) or defaults to `.env` — override with `-EnvFile backend/.env`. Nothing touches the running app unless you pass `-Restart`, which **recreates** the container (`docker compose up -d --force-recreate <svc>`) so it actually reloads the new `.env` — a plain `restart` would keep the old environment. Being high-impact, it confirms before writing; `-WhatIf` prints the exact plan and changes nothing.
|
||||
|
||||
```powershell
|
||||
# Preview only — see exactly what would change, change nothing:
|
||||
zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL -WhatIf
|
||||
|
||||
# Regenerate the JWT secret, restore the operator-known values, then restart:
|
||||
zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL,ADMIN_PASSWORD -Restart
|
||||
```
|
||||
|
||||
### Backups
|
||||
|
||||
#### `zbackup` — local backups
|
||||
|
||||
21
ZHelpers.ps1
21
ZHelpers.ps1
@ -109,7 +109,16 @@ function Invoke-Ec2Step {
|
||||
)
|
||||
$cfg = Get-ZConfig
|
||||
Write-Host " >> $Label" -ForegroundColor DarkCyan
|
||||
ssh -o StrictHostKeyChecking=no -i $cfg.ec2.pemKey (Get-Ec2Target) $Bash
|
||||
# ssh can emit warnings on stderr (e.g. Docker's "COMPOSE_BAKE is
|
||||
# deprecated" notice during a compose build). The deploy runs under
|
||||
# ErrorActionPreference='Stop', and PowerShell 5.1 turns any native stderr
|
||||
# line into a terminating NativeCommandError — aborting the deploy before we
|
||||
# ever read the real exit code, even though the remote step succeeded. Drop
|
||||
# to Continue locally (function-scoped, auto-reverts) and flatten stderr
|
||||
# into normal output, so only the actual exit status decides success.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
ssh -o StrictHostKeyChecking=no -i $cfg.ec2.pemKey (Get-Ec2Target) $Bash 2>&1 |
|
||||
ForEach-Object { "$_" }
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$msg = "Remote step failed: '$Label' (exit $LASTEXITCODE)."
|
||||
if ($FailHint) { $msg += " $FailHint" }
|
||||
@ -177,10 +186,16 @@ function Get-ArchiveExcludes {
|
||||
$byKind = switch ([string]$Project.kind) {
|
||||
"python" {
|
||||
$list = @(".venv", "venv", "__pycache__", ".pytest_cache", ".nicegui", "archive", "dist", "build", "htmlcov")
|
||||
if (-not $ForBackup) { $list += "uploads" } # deploys exclude user uploads; backups keep them
|
||||
# Deploys exclude user uploads + local .env secrets (server keeps its
|
||||
# own, preserved across deploys); backups keep both for completeness.
|
||||
if (-not $ForBackup) { $list += @("uploads", ".env", ".env.local", ".env.production") }
|
||||
$list
|
||||
}
|
||||
"vite" {
|
||||
$list = @("node_modules", "dist")
|
||||
if (-not $ForBackup) { $list += @(".env", ".env.local", ".env.production") }
|
||||
$list
|
||||
}
|
||||
"vite" { @("node_modules", "dist") }
|
||||
"nextjs" { @("node_modules", ".next", ".env", ".env.local", ".env.production", ".vercel", "coverage", "out", "build", "next-env.d.ts") }
|
||||
default { @() }
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ if ! [[ "$hh" =~ ^[0-9]{1,2}$ && "$mm" =~ ^[0-9]{2}$ ]] || [ "$hh" -gt 23 ] || [
|
||||
err "Invalid --time '$at_time' (expected HH:MM, 24h)"; exit 1
|
||||
fi
|
||||
|
||||
cron_line="${mm#0} ${hh#0} * * * $_HERE/zbackup_and_sync >> \$HOME/zbackup_and_sync.log 2>&1"
|
||||
cron_line="${mm#0} ${hh#0} * * * $_HERE/zbackup_and_sync all >> \$HOME/zbackup_and_sync.log 2>&1"
|
||||
|
||||
info "=== Backup schedule (cron) ==="
|
||||
dim "Daily at $at_time:"
|
||||
|
||||
41
bash/zbackup
41
bash/zbackup
@ -7,8 +7,8 @@
|
||||
# project's .env has a DATABASE_URL) into the backups folder.
|
||||
#
|
||||
# Usage:
|
||||
# zbackup # every project in zconfig.json + this scripts folder
|
||||
# zbackup <project> [<project> ...]
|
||||
# zbackup all # every project in zconfig.json + this scripts folder
|
||||
# zbackup scripts # just this scripts folder ('scripts' is a reserved word)
|
||||
# zbackup pyapp --tag "pre-migration"
|
||||
#
|
||||
@ -30,8 +30,18 @@ while [ $# -gt 0 ]; do
|
||||
done
|
||||
|
||||
BACKUP_ROOT="$(z_path "$(zq '.paths.backupsLocal')")"
|
||||
include_scripts=0
|
||||
|
||||
# Require an explicit target: bare invocation shows usage instead of quietly
|
||||
# backing up everything - 'all' is explicit, matching zdeploy.
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zbackup <project> [<project> ...] | all | scripts [--tag \"label\"]"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' backs up every project plus this scripts folder; 'scripts' just this folder."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
include_scripts=0
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
projects=(); while IFS= read -r _zl || [ -n "$_zl" ]; do projects+=("$_zl"); done < <(zproj_keys)
|
||||
include_scripts=1
|
||||
else
|
||||
@ -46,24 +56,34 @@ ts() { date +%Y%m%d-%H%M%S; }
|
||||
tag_suffix() { [ -n "$tag" ] && printf '_%s' "$(printf '%s' "$tag" | tr -s '[:space:]' '_')"; }
|
||||
|
||||
# Dump the project's Postgres database if its .env declares a DATABASE_URL.
|
||||
# Checks <root>/.env, then <root>/backend/.env. Returns 0 and writes $2 on success.
|
||||
# Checks <root>/.env, then <root>/backend/.env. Prints its own status line for
|
||||
# every outcome; returns 0 (dumped, $2 written) or 1 (nothing dumped).
|
||||
# A not-running database (connection refused) is a calm, expected skip; a real
|
||||
# failure (version mismatch, auth, missing db) prints the loud pg_dump error.
|
||||
local_pg_dump() { # <root> <out_path>
|
||||
local root="$1" out="$2" env_file db_url
|
||||
local root="$1" out="$2" env_file db_url err_out rc
|
||||
env_file="$root/.env"; [ -f "$env_file" ] || env_file="$root/backend/.env"
|
||||
[ -f "$env_file" ] || return 1
|
||||
if [ ! -f "$env_file" ]; then dim " No local DATABASE_URL - source-only backup."; return 1; fi
|
||||
db_url="$(grep -m1 '^DATABASE_URL=' "$env_file" | cut -d= -f2- | tr -d '[:space:]')"
|
||||
[ -n "$db_url" ] || return 1
|
||||
if [ -z "$db_url" ]; then dim " No local DATABASE_URL - source-only backup."; return 1; fi
|
||||
db_url="$(printf '%s' "$db_url" | sed -E 's|^postgresql\+[^:]+://|postgresql://|')"
|
||||
if [[ "$db_url" =~ ^postgresql://([^:]+):([^@]+)@([^:]+):([0-9]+)/([^?]+) ]]; then
|
||||
local user="${BASH_REMATCH[1]}" pass="${BASH_REMATCH[2]}" host="${BASH_REMATCH[3]}"
|
||||
local port="${BASH_REMATCH[4]}" db="${BASH_REMATCH[5]}"
|
||||
command -v pg_dump >/dev/null 2>&1 || { err ' pg_dump not installed - skipping PG backup'; return 1; }
|
||||
if PGPASSWORD="$(z_urldecode "$pass")" pg_dump -h "$host" -p "$port" -U "$user" -d "$db" -F p -f "$out" 2>/dev/null \
|
||||
&& [ -f "$out" ]; then
|
||||
err_out="$(PGPASSWORD="$(z_urldecode "$pass")" pg_dump -h "$host" -p "$port" -U "$user" -d "$db" -F p -f "$out" 2>&1)"; rc=$?
|
||||
if [ "$rc" -eq 0 ] && [ -f "$out" ]; then
|
||||
ok " PostgreSQL dump: $(awk -v b="$(wc -c < "$out")" 'BEGIN{printf "%.1f", b/1024}') KB"
|
||||
return 0
|
||||
fi
|
||||
err " pg_dump failed"
|
||||
# Not reachable (usually just not running locally) is expected - stay calm.
|
||||
if printf '%s' "$err_out" | grep -qiE 'connection refused|could not connect|no route to host|could not translate host|timeout expired'; then
|
||||
dim " Local database not running at $host:$port - source-only backup."
|
||||
return 1
|
||||
fi
|
||||
# A real failure (version mismatch, auth, missing db) - show the reason.
|
||||
err " pg_dump failed ($host:$port/$db as $user):"
|
||||
printf '%s\n' "$err_out" | grep -v '^[[:space:]]*$' | sed 's/^/ /' >&2
|
||||
return 1
|
||||
fi
|
||||
err ' Could not parse DATABASE_URL - skipping PG backup'
|
||||
@ -87,8 +107,7 @@ backup_project() { # <key>
|
||||
dump_dir="$(mktemp -d "${TMPDIR:-/tmp}/zbackup_${key}_XXXXXX")"
|
||||
db_dump="$dump_dir/database_pg.sql"
|
||||
warn " [1/3] Checking for a local database to dump..."
|
||||
if local_pg_dump "$root" "$db_dump"; then extra="$db_dump"
|
||||
else dim " No local DATABASE_URL - source-only backup."; fi
|
||||
if local_pg_dump "$root" "$db_dump"; then extra="$db_dump"; fi
|
||||
|
||||
warn " [2/3] Archiving source..."
|
||||
local excl=()
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
# zbackup_and_sync — run backups, then sync the backups folder offsite.
|
||||
#
|
||||
# Usage:
|
||||
# zbackup_and_sync # backup everything + sync
|
||||
# zbackup_and_sync <project> [<project> ...]
|
||||
# zbackup_and_sync all # backup everything + sync
|
||||
#
|
||||
# Cron example (see setup_backup_schedule):
|
||||
# 0 2 * * * /path/to/zscripts/bash/zbackup_and_sync >> ~/zbackup.log 2>&1
|
||||
# 0 2 * * * /path/to/zscripts/bash/zbackup_and_sync all >> ~/zbackup.log 2>&1
|
||||
|
||||
set -uo pipefail
|
||||
_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@ -18,6 +18,14 @@ source "$_HERE/zhelpers.sh"
|
||||
z_track_start "$@"
|
||||
z_need_config
|
||||
|
||||
# Require an explicit target (matching zbackup/zdeploy); 'all' backs up everything.
|
||||
if [ $# -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zbackup_and_sync <project> [<project> ...] | all"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' backs up every project plus the scripts folder, then syncs offsite."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
info "============================================"
|
||||
info " Backup & Sync - $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
# with a "db" config block, plus a zip of server-side data dirs (uploads/archive/dist).
|
||||
#
|
||||
# Usage:
|
||||
# zbackup_ec2 # every project with a remote.path
|
||||
# zbackup_ec2 <project> [<project> ...]
|
||||
# zbackup_ec2 all # every project with a remote.path
|
||||
#
|
||||
# Output:
|
||||
# <paths.backupsEc2>/<project>/<timestamp>_<project>_db.sql (projects with a db block)
|
||||
@ -21,7 +21,14 @@ z_need_config
|
||||
z_require ssh scp
|
||||
|
||||
projects=("$@")
|
||||
# Require an explicit target; 'all' pulls every project with a remote.path.
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zbackup_ec2 <project> [<project> ...] | all"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' pulls a server backup of every project with a remote.path."
|
||||
exit 1
|
||||
fi
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
projects=(); while IFS= read -r _zl || [ -n "$_zl" ]; do projects+=("$_zl"); done \
|
||||
< <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.remote.path != null) | .key' "$ZCONFIG")
|
||||
fi
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"kind": "python",
|
||||
"localRoot": "/home/youruser/code/pyapp",
|
||||
"startModule": "pyapp.main",
|
||||
"_startApp_note": "ASGI/FastAPI app? Use \"startApp\": \"app.main:app\" instead of startModule (runs uvicorn --port <dev> --reload).",
|
||||
"install": "-e .",
|
||||
"ports": { "dev": 8080 },
|
||||
"domain": "pyapp.yourdomain.com",
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
# zec2 — quick reachability check (TCP + HTTP + live build version) for deployed projects.
|
||||
#
|
||||
# Usage:
|
||||
# zec2 # every project with a "domain" in zconfig.json
|
||||
# zec2 <project> [<project> ...]
|
||||
# zec2 all # every project with a "domain" in zconfig.json
|
||||
# zec2 viteapp --host 203.0.113.10
|
||||
|
||||
set -uo pipefail
|
||||
@ -28,6 +28,12 @@ done
|
||||
HOST="${host_override:-$(zec2_ip)}"
|
||||
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zec2 <project> [<project> ...] | all [--host <ip>]"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' checks every project with a 'domain' configured."
|
||||
exit 1
|
||||
fi
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
projects=(); while IFS= read -r _zl || [ -n "$_zl" ]; do projects+=("$_zl"); done < <(zproj_keys_with_domain)
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
warn "No projects with a 'domain' configured in zconfig.json."
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
# build; auto-start downed stacks via docker compose and stream diagnostics.
|
||||
#
|
||||
# Usage:
|
||||
# zec2online # every project with a "domain" in zconfig.json
|
||||
# zec2online <project> [<project> ...]
|
||||
# zec2online all # every project with a "domain" in zconfig.json
|
||||
#
|
||||
# A plain HTTP-200 check passes even when a stale cached build is live — the
|
||||
# local-vs-server version match is what proves the deployed build is the one running.
|
||||
@ -32,6 +32,12 @@ HOST="${host_override:-$(zec2_ip)}"
|
||||
EDGE_KEY="$(zedge_key)"
|
||||
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zec2online <project> [<project> ...] | all [--host <ip>]"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' deep-checks every project with a 'domain' (and auto-starts any that are down)."
|
||||
exit 1
|
||||
fi
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
projects=(); while IFS= read -r _zl || [ -n "$_zl" ]; do projects+=("$_zl"); done < <(zproj_keys_with_domain)
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
warn "No projects with a 'domain' configured in zconfig.json."
|
||||
|
||||
@ -217,8 +217,11 @@ z_archive_excludes() {
|
||||
case "$kind" in
|
||||
python)
|
||||
printf '%s\n' .venv venv __pycache__ .pytest_cache .nicegui archive dist build htmlcov
|
||||
[ "$for_backup" -eq 1 ] || printf '%s\n' uploads ;; # deploys exclude user uploads; backups keep them
|
||||
vite) printf '%s\n' node_modules dist ;;
|
||||
# deploys exclude user uploads + local .env secrets; backups keep both
|
||||
[ "$for_backup" -eq 1 ] || printf '%s\n' uploads .env .env.local .env.production ;;
|
||||
vite)
|
||||
printf '%s\n' node_modules dist
|
||||
[ "$for_backup" -eq 1 ] || printf '%s\n' .env .env.local .env.production ;;
|
||||
nextjs) printf '%s\n' node_modules .next .env .env.local .env.production .vercel coverage out build next-env.d.ts ;;
|
||||
esac
|
||||
jq -r --arg k "$key" '.projects[$k].deploy.exclude // [] | .[]' "$ZCONFIG"
|
||||
|
||||
55
bash/zstart
55
bash/zstart
@ -8,8 +8,9 @@
|
||||
# Usage:
|
||||
# zstart <project> [<project> ...] [--port N] [--bind-host host] [--detached]
|
||||
#
|
||||
# Handlers by kind: python (python -m <startModule>, prefers .venv),
|
||||
# vite (npm run dev -- --host --port), nextjs (npm run dev with PORT).
|
||||
# Handlers by kind: python (python -m <startModule>, or uvicorn <startApp> for
|
||||
# ASGI/FastAPI apps; prefers .venv), vite (npm run dev -- --host --port),
|
||||
# nextjs (npm run dev with PORT).
|
||||
# Detached servers log to /tmp/zstart-<project>.log ; stop them with zkill.
|
||||
|
||||
set -uo pipefail
|
||||
@ -43,7 +44,13 @@ warn_if_privileged_port() { # <port>
|
||||
detach() { # <key> <workdir> <cmd...>
|
||||
local key="$1" dir="$2"; shift 2
|
||||
local log="/tmp/zstart-${key}.log"
|
||||
( cd "$dir" && nohup "$@" >"$log" 2>&1 & )
|
||||
# Sever every fd tied to this shell before exec'ing the long-lived server:
|
||||
# stdin<-/dev/null, stdout/stderr->log, and CLOSE the ztokens tracking fds
|
||||
# (3/4 = saved stdout/stderr; 1/2 fed the capture FIFO). If the server keeps
|
||||
# any open, the parent's EXIT-trap footer (a `tee` on the FIFO) never gets
|
||||
# EOF and `zstart --detached` hangs instead of returning.
|
||||
( cd "$dir" && exec </dev/null >"$log" 2>&1 3>&- 4>&-; exec nohup "$@" ) &
|
||||
disown 2>/dev/null || true
|
||||
ok "Started in detached mode (logs: $log)."
|
||||
dim "Use zkill $key to stop it."
|
||||
}
|
||||
@ -59,19 +66,22 @@ start_prep() { # <key>
|
||||
dim " env $name=$val"
|
||||
done < <(jq -r --arg k "$key" '.projects[$k].start.env // {} | to_entries[] | "\(.key)\t\(.value)"' "$ZCONFIG")
|
||||
if [ "$(zproj "$key" .start.gitPull)" = "true" ] && [ -d "$root/.git" ]; then
|
||||
local last
|
||||
last="$( (cd "$root" && git pull --ff-only 2>&1) | tail -1 )"
|
||||
dim " git pull: $last"
|
||||
(cd "$root" && git rev-parse HEAD >/dev/null 2>&1) || warn " Auto-pull failed - run 'git pull' manually if needed."
|
||||
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast instead
|
||||
# of blocking the server start on an interactive "Username for ..." prompt.
|
||||
local out rc
|
||||
out="$( (cd "$root" && GIT_TERMINAL_PROMPT=0 git pull --ff-only 2>&1) )"; rc=$?
|
||||
dim " git pull: $(printf '%s\n' "$out" | tail -1)"
|
||||
[ "$rc" -eq 0 ] || warn " Auto-pull skipped (exit $rc) - starting with the current checkout. (git needs credentials here, or set start.gitPull=false)"
|
||||
fi
|
||||
}
|
||||
|
||||
start_python() { # <key> <port>
|
||||
local key="$1" port="$2" root module exe bv=""
|
||||
local key="$1" port="$2" root module app exe bv="" run_cmd=() what using_venv=1
|
||||
root="$(zproj_root "$key")"
|
||||
module="$(zproj "$key" .startModule)"
|
||||
app="$(zproj "$key" .startApp)"
|
||||
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||
[ -n "$module" ] || { err "Project '$key' (kind=python) needs 'startModule' in zconfig.json (e.g. \"startModule\": \"pyapp.main\")."; return 1; }
|
||||
[ -n "$module" ] || [ -n "$app" ] || { err "Project '$key' (kind=python) needs 'startModule' (python -m ...) or 'startApp' (uvicorn app:app) in zconfig.json."; return 1; }
|
||||
|
||||
# Prefer the project venv. The Scripts/python.exe branch is a *Windows* venv
|
||||
# layout — only runnable from a Windows-family shell (Git Bash/MSYS/Cygwin).
|
||||
@ -79,8 +89,8 @@ start_python() { # <key> <port>
|
||||
# executable but can't exec, so guard that branch to fall through to python3.
|
||||
if [ -x "$root/.venv/bin/python" ]; then exe="$root/.venv/bin/python"
|
||||
elif [ -x "$root/.venv/Scripts/python.exe" ] && [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]]; then exe="$root/.venv/Scripts/python.exe"
|
||||
elif command -v python3 >/dev/null 2>&1; then exe="python3"
|
||||
else exe="python"; fi
|
||||
elif command -v python3 >/dev/null 2>&1; then exe="python3"; using_venv=0
|
||||
else exe="python"; using_venv=0; fi
|
||||
|
||||
# Optional convention: scripts/build_version_tool.py bumps the version on dev start.
|
||||
if [ -f "$root/scripts/build_version_tool.py" ]; then
|
||||
@ -88,8 +98,25 @@ start_python() { # <key> <port>
|
||||
[ -n "$bv" ] || bv="$(cd "$root" && "$exe" scripts/build_version_tool.py get 2>/dev/null | tail -1)"
|
||||
fi
|
||||
|
||||
# startApp (uvicorn ASGI target, e.g. app.main:app) takes precedence over
|
||||
# startModule (python -m ...). uvicorn is run via the venv python's -m so no
|
||||
# PATH juggling is needed, and it gets zstart's bind-host + dev port.
|
||||
if [ -n "$app" ]; then
|
||||
run_cmd=("$exe" -m uvicorn "$app" --host "$bind_host" --port "$port" --reload)
|
||||
what="uvicorn $app"
|
||||
else
|
||||
run_cmd=("$exe" -m "$module")
|
||||
what="python -m $module"
|
||||
fi
|
||||
|
||||
printf '\n'; info "=== zstart ($(zproj "$key" .label)) ==="
|
||||
info "Starting python -m $module on port $port..."
|
||||
# No project venv found - warn, since the system interpreter usually lacks the
|
||||
# app's deps (the failure would otherwise be a cryptic ModuleNotFoundError).
|
||||
if [ "$using_venv" -eq 0 ]; then
|
||||
warn "No project venv at $root/.venv - using system '$exe' (its deps may be missing)."
|
||||
dim " Create one: (cd \"$root\" && python3 -m venv .venv && .venv/bin/pip install -e .)"
|
||||
fi
|
||||
info "Starting $what on port $port..."
|
||||
[ -n "$bv" ] && note "Build Version: $bv"
|
||||
show_project_motd "$root"
|
||||
dim "Press Ctrl+C to stop the server"
|
||||
@ -97,9 +124,9 @@ start_python() { # <key> <port>
|
||||
printf '\n'
|
||||
|
||||
if [ "$detached" -eq 1 ]; then
|
||||
detach "$key" "$root" "$exe" -m "$module"
|
||||
detach "$key" "$root" "${run_cmd[@]}"
|
||||
else
|
||||
( cd "$root" && exec "$exe" -m "$module" )
|
||||
( cd "$root" && exec "${run_cmd[@]}" )
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ if ($existingTask) {
|
||||
$trigger = New-ScheduledTaskTrigger -Daily -At "02:00"
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute "powershell.exe" `
|
||||
-Argument "-ExecutionPolicy Bypass -NoProfile -File `"$ScriptPath`""
|
||||
-Argument "-ExecutionPolicy Bypass -NoProfile -File `"$ScriptPath`" all"
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
|
||||
65
zbackup.ps1
65
zbackup.ps1
@ -6,8 +6,8 @@
|
||||
# project's .env has a DATABASE_URL) into the backups folder.
|
||||
#
|
||||
# Usage:
|
||||
# zbackup # every project in zconfig.json + this scripts folder
|
||||
# zbackup <project> [<project> ...]
|
||||
# zbackup all # every project in zconfig.json + this scripts folder
|
||||
# zbackup scripts # just this scripts folder ('scripts' is a reserved word)
|
||||
# zbackup pyapp -Tag "pre-migration"
|
||||
#
|
||||
@ -26,8 +26,22 @@ Start-ZTracking
|
||||
$cfg = Get-ZConfig
|
||||
$ProjectsBackupRoot = $cfg.paths.backupsLocal
|
||||
|
||||
$includeScripts = $false
|
||||
# Tolerate switch-style args (zbackup -myproject) from muscle memory.
|
||||
$Projects = @($Projects | ForEach-Object { $_.TrimStart('-') })
|
||||
|
||||
# No args shows usage instead of quietly backing up everything — 'all' is
|
||||
# explicit, matching zdeploy and the other z-commands.
|
||||
if ($Projects.Count -eq 0) {
|
||||
$keys = (Get-ZProjectKeys) -join ', '
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zbackup <project> [<project> ...] | all | scripts [-Tag `"label`"]" -ForegroundColor Yellow
|
||||
Write-Host " Projects in zconfig.json: $keys" -ForegroundColor Gray
|
||||
Write-Host " 'all' backs up every project plus this scripts folder; 'scripts' just this folder." -ForegroundColor Gray
|
||||
Stop-ZTracking; exit 1
|
||||
}
|
||||
|
||||
$includeScripts = $false
|
||||
if ($Projects -contains 'all') {
|
||||
$Projects = @(Get-ZProjectKeys)
|
||||
$includeScripts = $true
|
||||
} elseif ($Projects -contains 'scripts') {
|
||||
@ -53,19 +67,29 @@ function Ensure-BackupDir {
|
||||
|
||||
# Dump the project's Postgres database if its .env declares a DATABASE_URL.
|
||||
# Checks <root>\.env, then <root>\backend\.env (frontend/backend split projects).
|
||||
# Prints its own status line for every outcome; returns $true (dumped) or $false.
|
||||
# A not-running database (connection refused) is a calm, expected skip; a real
|
||||
# failure (version mismatch, auth, missing db) prints the loud pg_dump error.
|
||||
function Invoke-LocalPgDump {
|
||||
param([string]$Root, [string]$OutPath)
|
||||
$envFile = Join-Path $Root ".env"
|
||||
if (-not (Test-Path -LiteralPath $envFile)) { $envFile = Join-Path $Root "backend\.env" }
|
||||
if (-not (Test-Path -LiteralPath $envFile)) { return $false }
|
||||
if (-not (Test-Path -LiteralPath $envFile)) {
|
||||
Write-Host " No local DATABASE_URL - source-only backup." -ForegroundColor DarkGray; return $false
|
||||
}
|
||||
$dbLine = @(Get-Content -LiteralPath $envFile | Where-Object { $_ -match "^DATABASE_URL=" } | Select-Object -First 1)
|
||||
if ($dbLine.Count -eq 0) { return $false }
|
||||
if ($dbLine.Count -eq 0) {
|
||||
Write-Host " No local DATABASE_URL - source-only backup." -ForegroundColor DarkGray; return $false
|
||||
}
|
||||
|
||||
$dbUrl = ($dbLine[0] -replace "^DATABASE_URL=", "").Trim()
|
||||
$dbUrl = $dbUrl -replace '^postgresql\+[^:]+://', 'postgresql://'
|
||||
# Strip surrounding quotes (Prisma-style .env values are double-quoted),
|
||||
# accept postgres:// and postgresql+driver:// schemes, and treat the
|
||||
# port as optional (Postgres default 5432).
|
||||
$dbUrl = ($dbLine[0] -replace "^DATABASE_URL=", "").Trim().Trim('"').Trim("'")
|
||||
$dbUrl = $dbUrl -replace '^postgres(ql)?(\+[^:]+)?://', 'postgresql://'
|
||||
$rx = [regex]::Match(
|
||||
$dbUrl,
|
||||
'^postgresql://(?<user>[^:]+):(?<pass>[^@]+)@(?<host>[^:]+):(?<port>\d+)/(?<db>[^?]+)'
|
||||
'^postgresql://(?<user>[^:@/]+):(?<pass>[^@]+)@(?<host>[^:/?]+)(:(?<port>\d+))?/(?<db>[^?\s]+)'
|
||||
)
|
||||
if (-not $rx.Success) {
|
||||
Write-Host ' Could not parse DATABASE_URL - skipping PG backup' -ForegroundColor Red
|
||||
@ -74,7 +98,7 @@ function Invoke-LocalPgDump {
|
||||
$env:PGPASSWORD = [Uri]::UnescapeDataString($rx.Groups['pass'].Value)
|
||||
$pgUser = $rx.Groups['user'].Value
|
||||
$pgHost = $rx.Groups['host'].Value
|
||||
$pgPort = $rx.Groups['port'].Value
|
||||
$pgPort = if ($rx.Groups['port'].Success) { $rx.Groups['port'].Value } else { '5432' }
|
||||
$pgDb = $rx.Groups['db'].Value
|
||||
|
||||
$pgDump = "pg_dump"
|
||||
@ -87,16 +111,31 @@ function Invoke-LocalPgDump {
|
||||
foreach ($candidate in $pgBinPaths) {
|
||||
if (Test-Path $candidate) { $pgDump = $candidate; break }
|
||||
}
|
||||
& $pgDump -h $pgHost -p $pgPort -U $pgUser -d $pgDb -F p -f $OutPath 2>$null
|
||||
# Capture stderr so a failure is diagnosable (version mismatch, unreachable
|
||||
# host, auth) instead of a bare "pg_dump failed". PS 5.1 turns native stderr
|
||||
# into a terminating NativeCommandError under 'Stop', so drop to Continue
|
||||
# locally (function-scoped, auto-reverts) while running pg_dump.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$errOut = & $pgDump -h $pgHost -p $pgPort -U $pgUser -d $pgDb -F p -f $OutPath 2>&1
|
||||
$ok = ($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $OutPath)
|
||||
Remove-Item Env:\PGPASSWORD -ErrorAction SilentlyContinue
|
||||
if ($ok) {
|
||||
$size = [math]::Round((Get-Item $OutPath).Length / 1KB, 1)
|
||||
Write-Host " PostgreSQL dump: ${size} KB" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " pg_dump failed (exit $LASTEXITCODE)" -ForegroundColor Red
|
||||
return $true
|
||||
}
|
||||
return $ok
|
||||
$errText = ($errOut | Out-String).Trim()
|
||||
# Not reachable (usually just not running locally) is expected - stay calm.
|
||||
if ($errText -match '(?i)connection refused|could not connect|no route to host|could not translate host|timeout expired') {
|
||||
Write-Host " Local database not running at ${pgHost}:${pgPort} - source-only backup." -ForegroundColor DarkGray
|
||||
return $false
|
||||
}
|
||||
# A real failure (version mismatch, auth, missing db) - show the reason.
|
||||
Write-Host " pg_dump failed (${pgHost}:${pgPort}/${pgDb} as ${pgUser}):" -ForegroundColor Red
|
||||
foreach ($line in ($errText -split '\r?\n' | Where-Object { $_.Trim() -ne '' })) {
|
||||
Write-Host " $line" -ForegroundColor Red
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Invoke-ProjectBackup {
|
||||
@ -124,8 +163,6 @@ function Invoke-ProjectBackup {
|
||||
$dbDumpPath = Join-Path $dumpDir "database_pg.sql"
|
||||
if (Invoke-LocalPgDump -Root $root -OutPath $dbDumpPath) {
|
||||
$extraFiles += $dbDumpPath
|
||||
} else {
|
||||
Write-Host " No local DATABASE_URL - source-only backup." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
Write-Host " [2/3] Archiving source..." -ForegroundColor Yellow
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
# zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite.
|
||||
#
|
||||
# Usage:
|
||||
# zbackup_and_sync.ps1 # backup everything + sync
|
||||
# zbackup_and_sync.ps1 <project> [<project> ...]
|
||||
# zbackup_and_sync.ps1 all # backup everything + sync
|
||||
#
|
||||
# Scheduled Task example (see setup_backup_schedule.ps1):
|
||||
# powershell -ExecutionPolicy Bypass -NoProfile -File "<scriptsRoot>\zbackup_and_sync.ps1"
|
||||
# powershell -ExecutionPolicy Bypass -NoProfile -File "<scriptsRoot>\zbackup_and_sync.ps1" all
|
||||
|
||||
param(
|
||||
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
||||
@ -21,6 +21,18 @@ $ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
. (Join-Path $ScriptRoot "ZHelpers.ps1")
|
||||
Start-ZTracking
|
||||
|
||||
# Tolerate switch-style args from muscle memory; require an explicit target
|
||||
# ('all' included) — same convention as zbackup/zdeploy.
|
||||
$Projects = @($Projects | ForEach-Object { $_.TrimStart('-') })
|
||||
if ($Projects.Count -eq 0) {
|
||||
$keys = (Get-ZProjectKeys) -join ', '
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zbackup_and_sync <project> [<project> ...] | all" -ForegroundColor Yellow
|
||||
Write-Host " Projects in zconfig.json: $keys" -ForegroundColor Gray
|
||||
Write-Host " 'all' backs up every project plus the scripts folder, then syncs offsite." -ForegroundColor Gray
|
||||
Stop-ZTracking; exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host " Backup & Sync - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan
|
||||
@ -29,11 +41,7 @@ Write-Host ""
|
||||
|
||||
Write-Host "[1/2] Running backups..." -ForegroundColor Yellow
|
||||
$backupPath = Join-Path $ScriptRoot "zbackup.ps1"
|
||||
if ($Projects.Count -gt 0) {
|
||||
& powershell -NoProfile -File $backupPath @Projects
|
||||
} else {
|
||||
& powershell -NoProfile -File $backupPath
|
||||
}
|
||||
$backupExitCode = $LASTEXITCODE
|
||||
|
||||
if ($backupExitCode -ne 0) {
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
# with a "db" config block, plus a zip of server-side data dirs (uploads/archive/dist).
|
||||
#
|
||||
# Usage:
|
||||
# zbackup_ec2 # every project with a remote.path
|
||||
# zbackup_ec2 <project> [<project> ...]
|
||||
# zbackup_ec2 all # every project with a remote.path
|
||||
#
|
||||
# Output:
|
||||
# <paths.backupsEc2>\<project>\<timestamp>_<project>_db.sql (projects with a db block)
|
||||
@ -29,6 +29,13 @@ $RemoteHome = Get-Ec2Home
|
||||
$Ec2BackupRoot = $cfg.paths.backupsEc2
|
||||
|
||||
if ($Projects.Count -eq 0) {
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zbackup_ec2 <project> [<project> ...] | all" -ForegroundColor Yellow
|
||||
Write-Host " Projects in zconfig.json: $((Get-ZProjectKeys) -join ', ')" -ForegroundColor Gray
|
||||
Write-Host " 'all' pulls a server backup of every project with a remote.path." -ForegroundColor Gray
|
||||
Stop-ZTracking; exit 1
|
||||
}
|
||||
if ($Projects -contains 'all') {
|
||||
$Projects = @(Get-ZProjectKeys | Where-Object { $cfg.projects.$_.remote -and $cfg.projects.$_.remote.path })
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
"kind": "python",
|
||||
"localRoot": "C:\\YourRoot\\pyapp",
|
||||
"startModule": "pyapp.main",
|
||||
"_startApp_note": "ASGI/FastAPI app? Use \"startApp\": \"app.main:app\" instead of startModule (runs uvicorn --port <dev> --reload).",
|
||||
"install": "-e .",
|
||||
"ports": { "dev": 8080 },
|
||||
"domain": "pyapp.yourdomain.com",
|
||||
|
||||
9
zec2.ps1
9
zec2.ps1
@ -5,8 +5,8 @@
|
||||
# zec2.ps1 — quick reachability check (TCP + HTTP + live build version) for deployed projects.
|
||||
#
|
||||
# Usage:
|
||||
# zec2 # every project with a "domain" in zconfig.json
|
||||
# zec2 <project> [<project> ...]
|
||||
# zec2 all # every project with a "domain" in zconfig.json
|
||||
# zec2 viteapp -HostName 203.0.113.10
|
||||
#
|
||||
param(
|
||||
@ -23,6 +23,13 @@ $cfg = Get-ZConfig
|
||||
if (-not $HostName) { $HostName = $cfg.ec2.ip }
|
||||
|
||||
if ($Projects.Count -eq 0) {
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zec2 <project> [<project> ...] | all [-HostName <ip>]" -ForegroundColor Yellow
|
||||
Write-Host " Projects in zconfig.json: $((Get-ZProjectKeys) -join ', ')" -ForegroundColor Gray
|
||||
Write-Host " 'all' checks every project with a 'domain' configured." -ForegroundColor Gray
|
||||
Stop-ZTracking; exit 1
|
||||
}
|
||||
if ($Projects -contains 'all') {
|
||||
$Projects = @(Get-ZProjectKeys | Where-Object { $cfg.projects.$_.domain })
|
||||
if ($Projects.Count -eq 0) {
|
||||
Write-Host "No projects with a 'domain' configured in zconfig.json." -ForegroundColor Yellow
|
||||
|
||||
6
zec2_rotatekeys.cmd
Normal file
6
zec2_rotatekeys.cmd
Normal file
@ -0,0 +1,6 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2_rotatekeys.ps1" %*
|
||||
288
zec2_rotatekeys.ps1
Normal file
288
zec2_rotatekeys.ps1
Normal file
@ -0,0 +1,288 @@
|
||||
# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
# zec2_rotatekeys.ps1 - rotate / reset secret keys in a project's SERVER-SIDE
|
||||
# .env, in place, without the values ever passing through this machine's shell
|
||||
# history, command args, or the operator's screen.
|
||||
#
|
||||
# Why this exists: if a deploy ever ships a dev .env over a project's production
|
||||
# .env (or a secret leaks), you need to (1) regenerate the machine secrets and
|
||||
# (2) restore the correct operator-known values on the server - safely.
|
||||
#
|
||||
# How it stays safe:
|
||||
# * -Rotate keys are regenerated ON THE SERVER (openssl rand -hex 32). The new
|
||||
# value is created on the box and written straight into the .env there; it is
|
||||
# never sent from here, never printed.
|
||||
# * -Set keys are typed into a masked prompt and streamed to the server over
|
||||
# SSH stdin (the encrypted channel) - never placed in a command argument
|
||||
# (where `ps`/history would capture it) and never echoed back.
|
||||
# * The current server .env is copied to a timestamped .bak before any change.
|
||||
# * -WhatIf prints the exact plan and touches nothing. High-impact, so it
|
||||
# confirms before writing unless you pass -Confirm:$false.
|
||||
# * It does NOT touch the running app unless you pass -Restart, which
|
||||
# recreates the container (up -d --force-recreate) so it reloads the new
|
||||
# .env - a plain `restart` reuses the old environment. Prod restarts are a
|
||||
# deliberate, separate decision.
|
||||
#
|
||||
# Usage:
|
||||
# zec2_rotatekeys <project> [-Rotate K1,K2] [-Set K1,K2] [-EnvFile rel/path]
|
||||
# [-Restart] [-HostName ip] [-WhatIf] [-Confirm:$false]
|
||||
#
|
||||
# Examples:
|
||||
# # Preview only - see exactly what would change, change nothing:
|
||||
# zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL -WhatIf
|
||||
#
|
||||
# # Regenerate the JWT secret and restore the operator-known values, then
|
||||
# # restart the app so it picks them up:
|
||||
# zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL,ADMIN_PASSWORD -Restart
|
||||
#
|
||||
# The env file is auto-detected from the project's deploy.preserve (first *.env
|
||||
# entry) or defaults to ".env"; override with -EnvFile (relative to remote.path,
|
||||
# e.g. -EnvFile backend/.env).
|
||||
|
||||
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
|
||||
param(
|
||||
[Parameter(Position = 0)][string]$Project,
|
||||
[string[]]$Rotate = @(),
|
||||
[string[]]$Set = @(),
|
||||
[string]$EnvFile,
|
||||
[switch]$Restart,
|
||||
[string]$HostName
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
. (Join-Path $PSScriptRoot "ZHelpers.ps1")
|
||||
Start-ZTracking
|
||||
|
||||
function Show-Usage {
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zec2_rotatekeys <project> [-Rotate K1,K2] [-Set K1,K2] [-EnvFile rel/path] [-Restart] [-WhatIf]" -ForegroundColor Yellow
|
||||
Write-Host " -Rotate keys regenerated on the server with a fresh random secret (openssl rand -hex 32)" -ForegroundColor Gray
|
||||
Write-Host " -Set keys set from a masked prompt, streamed over SSH stdin (for operator-known values)" -ForegroundColor Gray
|
||||
try { $keys = (Get-ZProjectKeys) -join ', '; Write-Host " Projects: $keys" -ForegroundColor Gray } catch { }
|
||||
Write-Host ""
|
||||
Write-Host " Example: zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL,ADMIN_PASSWORD -Restart" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# Server-side worker. Static (no secrets): -Rotate generates its value here on
|
||||
# the box; -Set reads it from stdin. Uploaded base64-encoded so line endings and
|
||||
# quoting survive the trip intact. Updates the KEY line atomically via python.
|
||||
$rkHelper = @'
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
env_file="${1:-}"; key="${2:-}"; mode="${3:-}"
|
||||
if [ -z "$env_file" ] || [ -z "$key" ] || [ -z "$mode" ]; then echo "BAD_ARGS"; exit 5; fi
|
||||
if [ ! -f "$env_file" ]; then echo "ENV_MISSING:$env_file"; exit 2; fi
|
||||
case "$mode" in
|
||||
rotate)
|
||||
command -v openssl >/dev/null 2>&1 || { echo "NO_OPENSSL"; exit 6; }
|
||||
val="$(openssl rand -hex 32)"
|
||||
;;
|
||||
set)
|
||||
IFS= read -r val || true
|
||||
val="${val%$'\r'}"
|
||||
if [ -z "$val" ]; then echo "EMPTY_VALUE:$key"; exit 4; fi
|
||||
;;
|
||||
*) echo "BAD_MODE:$mode"; exit 3 ;;
|
||||
esac
|
||||
KEY="$key" VAL="$val" python3 - "$env_file" <<'PY'
|
||||
import os, sys, tempfile
|
||||
path = sys.argv[1]
|
||||
k = os.environ['KEY']; v = os.environ['VAL']
|
||||
with open(path, 'r') as fh:
|
||||
lines = fh.read().splitlines()
|
||||
out = []
|
||||
found = False
|
||||
for ln in lines:
|
||||
s = ln.lstrip()
|
||||
if (not s.startswith('#')) and ('=' in ln) and (ln.split('=', 1)[0].strip() == k):
|
||||
out.append(k + '=' + v)
|
||||
found = True
|
||||
else:
|
||||
out.append(ln)
|
||||
if not found:
|
||||
out.append(k + '=' + v)
|
||||
d = os.path.dirname(path) or '.'
|
||||
fd, tmp = tempfile.mkstemp(dir=d)
|
||||
try:
|
||||
with os.fdopen(fd, 'w') as fh:
|
||||
fh.write('\n'.join(out) + '\n')
|
||||
os.chmod(tmp, 0o600)
|
||||
os.replace(tmp, path)
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
print('OK:' + k)
|
||||
PY
|
||||
'@
|
||||
|
||||
try {
|
||||
if (-not $Project) { Show-Usage; Stop-ZTracking; exit 1 }
|
||||
|
||||
$Rotate = @($Rotate | Where-Object { $_ -and $_.Trim() } | ForEach-Object { $_.Trim() })
|
||||
$Set = @($Set | Where-Object { $_ -and $_.Trim() } | ForEach-Object { $_.Trim() })
|
||||
|
||||
if ($Rotate.Count -eq 0 -and $Set.Count -eq 0) {
|
||||
Write-Host "ERROR: nothing to do - pass -Rotate and/or -Set with at least one key." -ForegroundColor Red
|
||||
Show-Usage; Stop-ZTracking; exit 1
|
||||
}
|
||||
|
||||
# A key can't be both regenerated and set - -Set (explicit value) wins.
|
||||
$overlap = @($Rotate | Where-Object { $Set -contains $_ })
|
||||
if ($overlap.Count -gt 0) {
|
||||
Write-Host "NOTE: $($overlap -join ', ') given to both -Rotate and -Set; using -Set (explicit value)." -ForegroundColor Yellow
|
||||
$Rotate = @($Rotate | Where-Object { $Set -notcontains $_ })
|
||||
}
|
||||
|
||||
$cfg = Get-ZConfig
|
||||
$proj = Get-ZProject -Key $Project # exits with a clear error on a bad key
|
||||
$remotePath = $proj.remote.path
|
||||
if (-not $remotePath) {
|
||||
Write-Host "ERROR: project '$Project' has no remote.path in zconfig.json - nothing to rotate on the server." -ForegroundColor Red
|
||||
Stop-ZTracking; exit 1
|
||||
}
|
||||
|
||||
# Env file location: -EnvFile wins; else first *.env in deploy.preserve; else ".env".
|
||||
if (-not $EnvFile) {
|
||||
$EnvFile = ".env"
|
||||
if ($proj.deploy -and $proj.deploy.preserve) {
|
||||
$cand = @($proj.deploy.preserve | Where-Object { $_ -match '\.env$' }) | Select-Object -First 1
|
||||
if ($cand) { $EnvFile = $cand }
|
||||
}
|
||||
}
|
||||
$EnvFile = $EnvFile -replace '\\', '/'
|
||||
$remoteEnv = "$remotePath/$EnvFile"
|
||||
|
||||
$ip = if ($HostName) { $HostName } else { $cfg.ec2.ip }
|
||||
$pem = $cfg.ec2.pemKey
|
||||
$target = "$($cfg.ec2.user)@$ip"
|
||||
$sshOpts = @('-o', 'StrictHostKeyChecking=no', '-o', 'ConnectTimeout=15', '-i', $pem)
|
||||
$remoteHelper = "/tmp/zrk_$PID.sh"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== zec2_rotatekeys ($($proj.label)) ===" -ForegroundColor Cyan
|
||||
Write-Host " Server: $target" -ForegroundColor DarkGray
|
||||
Write-Host " Env file: $remoteEnv" -ForegroundColor DarkGray
|
||||
if ($Rotate.Count) { Write-Host " Rotate (fresh random, server-side): $($Rotate -join ', ')" -ForegroundColor Gray }
|
||||
if ($Set.Count) { Write-Host " Set (masked prompt, streamed): $($Set -join ', ')" -ForegroundColor Gray }
|
||||
if ($Restart) { Write-Host " Then: recreate the app container (reloads the new .env)" -ForegroundColor Gray }
|
||||
Write-Host ""
|
||||
|
||||
$action = @()
|
||||
if ($Rotate.Count) { $action += "rotate [$($Rotate -join ',')]" }
|
||||
if ($Set.Count) { $action += "set [$($Set -join ',')]" }
|
||||
if ($Restart) { $action += "recreate" }
|
||||
if (-not $PSCmdlet.ShouldProcess("${target}:$remoteEnv", ($action -join ' + '))) {
|
||||
Write-Host "Preview only - no changes made." -ForegroundColor Yellow
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
|
||||
# --- confirm the env file actually exists before we touch anything --------
|
||||
$exists = (ssh @sshOpts $target "test -f $remoteEnv && echo EXISTS || echo MISSING") | Select-Object -Last 1
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not reach $target over SSH (exit $LASTEXITCODE)." }
|
||||
if ($exists -ne "EXISTS") {
|
||||
throw "Env file not found on the server: $remoteEnv. Check remote.path / -EnvFile."
|
||||
}
|
||||
|
||||
# --- back up the current server .env --------------------------------------
|
||||
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backup = "$remoteEnv.bak.$ts"
|
||||
ssh @sshOpts $target "cp -p $remoteEnv $backup"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Backup of $remoteEnv failed (exit $LASTEXITCODE) - aborting before any change." }
|
||||
Write-Host " Backed up server .env -> $backup" -ForegroundColor DarkGray
|
||||
|
||||
# --- upload the worker (base64: no CR / quoting surprises) -----------------
|
||||
$helperLf = $rkHelper -replace "`r`n", "`n"
|
||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($helperLf))
|
||||
ssh @sshOpts $target "echo $b64 | base64 -d > $remoteHelper && chmod 700 $remoteHelper"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to stage the rotation helper on the server (exit $LASTEXITCODE)." }
|
||||
|
||||
$done = @()
|
||||
$failed = @()
|
||||
try {
|
||||
# --- rotate: value generated on the server, never seen here -----------
|
||||
foreach ($key in $Rotate) {
|
||||
$r = (ssh @sshOpts $target "bash $remoteHelper $remoteEnv $key rotate") | Select-Object -Last 1
|
||||
if ($LASTEXITCODE -eq 0 -and $r -like "OK:*") {
|
||||
Write-Host " rotated $key" -ForegroundColor Green
|
||||
$done += "$key (rotated)"
|
||||
} else {
|
||||
Write-Host " FAILED $key ($r)" -ForegroundColor Red
|
||||
$failed += "$key ($r)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- set: masked prompt -> SSH stdin, never in args or on screen ------
|
||||
foreach ($key in $Set) {
|
||||
$secure = Read-Host -Prompt " New value for $key" -AsSecureString
|
||||
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
||||
try {
|
||||
$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
|
||||
} finally {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||
}
|
||||
if ([string]::IsNullOrEmpty($plain)) {
|
||||
Write-Host " skipped $key (no value entered)" -ForegroundColor Yellow
|
||||
$failed += "$key (empty - skipped)"
|
||||
$plain = $null
|
||||
continue
|
||||
}
|
||||
$r = ($plain | ssh @sshOpts $target "bash $remoteHelper $remoteEnv $key set") | Select-Object -Last 1
|
||||
$rc = $LASTEXITCODE
|
||||
$plain = $null # drop the plaintext from memory promptly
|
||||
if ($rc -eq 0 -and $r -like "OK:*") {
|
||||
Write-Host " set $key" -ForegroundColor Green
|
||||
$done += "$key (set)"
|
||||
} else {
|
||||
Write-Host " FAILED $key ($r)" -ForegroundColor Red
|
||||
$failed += "$key ($r)"
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ssh @sshOpts $target "rm -f $remoteHelper" | Out-Null
|
||||
}
|
||||
|
||||
# --- optional app recreate ------------------------------------------------
|
||||
# A plain `docker compose restart` reuses the container's existing
|
||||
# environment, so it would NOT pick up the .env we just edited. `up -d
|
||||
# --force-recreate` rebuilds the container from current config, reloading
|
||||
# env_file / environment - the reliable way to apply the new secrets.
|
||||
$restarted = $false
|
||||
if ($Restart -and $done.Count -gt 0) {
|
||||
$composeDir = if ($proj.remote.composeDir) { $proj.remote.composeDir } else { $remotePath }
|
||||
$svc = if ($proj.remote.appService) { $proj.remote.appService } else { "app" }
|
||||
Write-Host ""
|
||||
Write-Host " Recreating service '$svc' in $composeDir (to load the new .env) ..." -ForegroundColor Cyan
|
||||
ssh @sshOpts $target "cd $composeDir && sudo COMPOSE_BAKE=false docker compose up -d --force-recreate $svc"
|
||||
if ($LASTEXITCODE -eq 0) { Write-Host " Recreated $svc." -ForegroundColor Green; $restarted = $true }
|
||||
else { Write-Host " WARNING: recreate of '$svc' failed (exit $LASTEXITCODE) - apply it manually: docker compose up -d --force-recreate $svc" -ForegroundColor Red }
|
||||
} elseif ($Restart) {
|
||||
Write-Host " Skipping recreate - no keys were changed." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# --- summary --------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "=== Summary ===" -ForegroundColor Cyan
|
||||
Write-Host " Changed: $(if ($done.Count) { $done -join ', ' } else { 'none' })" -ForegroundColor $(if ($done.Count) { 'Green' } else { 'Yellow' })
|
||||
if ($failed.Count) { Write-Host " Failed: $($failed -join ', ')" -ForegroundColor Red }
|
||||
Write-Host " Backup: $backup (delete once verified)" -ForegroundColor DarkGray
|
||||
if ($Restart -and -not $restarted -and $done.Count -gt 0) {
|
||||
Write-Host " Recreate: NOT done - apply the new values with: docker compose up -d --force-recreate <svc>" -ForegroundColor Yellow
|
||||
} elseif (-not $Restart -and $done.Count -gt 0) {
|
||||
Write-Host " Note: the app is still running with the OLD values - re-run with -Restart, or 'docker compose up -d --force-recreate <svc>' on the server." -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
Stop-ZTracking
|
||||
if ($failed.Count) { exit 2 }
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
||||
Stop-ZTracking
|
||||
exit 1
|
||||
}
|
||||
@ -6,8 +6,8 @@
|
||||
# build; auto-start downed stacks via docker compose and stream diagnostics.
|
||||
#
|
||||
# Usage:
|
||||
# zec2online # every project with a "domain" in zconfig.json
|
||||
# zec2online <project> [<project> ...]
|
||||
# zec2online all # every project with a "domain" in zconfig.json
|
||||
#
|
||||
# Verification compares the LOCAL build version against what the server is actually
|
||||
# serving. A plain HTTP-200 check passes even when a stale cached build is live —
|
||||
@ -30,6 +30,13 @@ $SshTarget = Get-Ec2Target
|
||||
$edgeProj = Get-ZEdgeProject
|
||||
|
||||
if ($Projects.Count -eq 0) {
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zec2online <project> [<project> ...] | all [-HostName <ip>]" -ForegroundColor Yellow
|
||||
Write-Host " Projects in zconfig.json: $((Get-ZProjectKeys) -join ', ')" -ForegroundColor Gray
|
||||
Write-Host " 'all' deep-checks every project with a 'domain' (and auto-starts any that are down)." -ForegroundColor Gray
|
||||
Stop-ZTracking; exit 1
|
||||
}
|
||||
if ($Projects -contains 'all') {
|
||||
$Projects = @(Get-ZProjectKeys | Where-Object { $cfg.projects.$_.domain })
|
||||
if ($Projects.Count -eq 0) {
|
||||
Write-Host "No projects with a 'domain' configured in zconfig.json." -ForegroundColor Yellow
|
||||
|
||||
44
zstart.ps1
44
zstart.ps1
@ -12,8 +12,9 @@
|
||||
# zstart viteapp -Port 3000
|
||||
# zstart pyapp nextapp -Detached
|
||||
#
|
||||
# Handlers by kind: python (python -m <startModule>, prefers .venv),
|
||||
# vite (npm run dev -- --host --port), nextjs (npm run dev with PORT).
|
||||
# Handlers by kind: python (python -m <startModule>, or uvicorn <startApp> for
|
||||
# ASGI/FastAPI apps; prefers .venv), vite (npm run dev -- --host --port),
|
||||
# nextjs (npm run dev with PORT).
|
||||
#
|
||||
param(
|
||||
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
||||
@ -45,16 +46,18 @@ function Test-PortNeedsAdmin {
|
||||
}
|
||||
|
||||
function Start-PythonProject {
|
||||
param([string]$Key, $Proj, [int]$ListenPort, [bool]$RunDetached)
|
||||
param([string]$Key, $Proj, [int]$ListenPort, [bool]$RunDetached, [string]$HostBind = "127.0.0.1")
|
||||
$root = $Proj.localRoot
|
||||
if (-not (Test-Path -LiteralPath $root)) { throw "Project root not found: $root" }
|
||||
$module = $Proj.startModule
|
||||
if (-not $module) {
|
||||
throw "Project '$Key' (kind=python) needs 'startModule' in zconfig.json (e.g. `"startModule`": `"pyapp.main`" runs 'python -m pyapp.main')."
|
||||
$app = $Proj.startApp
|
||||
if (-not $module -and -not $app) {
|
||||
throw "Project '$Key' (kind=python) needs 'startModule' (python -m ...) or 'startApp' (uvicorn app:app) in zconfig.json."
|
||||
}
|
||||
Set-Location -LiteralPath $root
|
||||
$venvPython = Join-Path $root ".venv\Scripts\python.exe"
|
||||
$exe = if (Test-Path -LiteralPath $venvPython) { $venvPython } else { "python" }
|
||||
$usingVenv = Test-Path -LiteralPath $venvPython
|
||||
$exe = if ($usingVenv) { $venvPython } else { "python" }
|
||||
|
||||
# Optional convention: if the project ships scripts/build_version_tool.py,
|
||||
# bump (or at least read) the build version on every dev start.
|
||||
@ -77,9 +80,24 @@ function Start-PythonProject {
|
||||
}
|
||||
}
|
||||
|
||||
# startApp (uvicorn ASGI target, e.g. app.main:app) takes precedence over
|
||||
# startModule (python -m ...). uvicorn runs via the venv python's -m, and
|
||||
# gets zstart's bind-host + dev port.
|
||||
if ($app) {
|
||||
$runArgs = @("-m", "uvicorn", $app, "--host", $HostBind, "--port", "$ListenPort", "--reload")
|
||||
$what = "uvicorn $app"
|
||||
} else {
|
||||
$runArgs = @("-m", $module)
|
||||
$what = "python -m $module"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== zstart ($($Proj.label)) ===" -ForegroundColor Cyan
|
||||
Write-Host "Starting python -m $module on port $ListenPort..." -ForegroundColor Cyan
|
||||
if (-not $usingVenv) {
|
||||
Write-Host "No project venv at $root\.venv - using system 'python' (its deps may be missing)." -ForegroundColor Yellow
|
||||
Write-Host " Create one: python -m venv .venv; .\.venv\Scripts\pip install -e ." -ForegroundColor DarkGray
|
||||
}
|
||||
Write-Host "Starting $what on port $ListenPort..." -ForegroundColor Cyan
|
||||
if (-not [string]::IsNullOrWhiteSpace($buildVersion)) {
|
||||
Write-Host "Build Version: $buildVersion" -ForegroundColor Magenta
|
||||
}
|
||||
@ -91,11 +109,11 @@ function Start-PythonProject {
|
||||
Write-Host ""
|
||||
|
||||
if ($RunDetached) {
|
||||
Start-Process -FilePath $exe -ArgumentList "-m", $module -WorkingDirectory $root | Out-Null
|
||||
Start-Process -FilePath $exe -ArgumentList $runArgs -WorkingDirectory $root | Out-Null
|
||||
Write-Host "Started in detached mode." -ForegroundColor Green
|
||||
Write-Host "Use zkill $Key to stop it." -ForegroundColor DarkGray
|
||||
} else {
|
||||
& $exe -m $module
|
||||
& $exe @runArgs
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,14 +199,18 @@ function Invoke-ProjectStartPrep {
|
||||
}
|
||||
if ($Proj.start.gitPull -and (Test-Path (Join-Path $Proj.localRoot ".git"))) {
|
||||
Push-Location -LiteralPath $Proj.localRoot
|
||||
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast
|
||||
# instead of blocking the server start on a "Username for ..." prompt.
|
||||
$prev = $env:GIT_TERMINAL_PROMPT; $env:GIT_TERMINAL_PROMPT = "0"
|
||||
try {
|
||||
$pullOut = git pull --ff-only 2>&1
|
||||
$last = ($pullOut | Select-Object -Last 1)
|
||||
Write-Host " git pull: $last" -ForegroundColor DarkGray
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " Auto-pull failed - run 'git pull' manually if needed." -ForegroundColor Yellow
|
||||
Write-Host " Auto-pull skipped - starting with the current checkout. (git needs credentials here, or set start.gitPull=false)" -ForegroundColor Yellow
|
||||
}
|
||||
} finally {
|
||||
$env:GIT_TERMINAL_PROMPT = $prev
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
@ -201,7 +223,7 @@ foreach ($key in $Projects) {
|
||||
Invoke-ProjectStartPrep -Proj $proj
|
||||
|
||||
switch ([string]$proj.kind) {
|
||||
"python" { Start-PythonProject -Key $key -Proj $proj -ListenPort $devPort -RunDetached $Detached.IsPresent }
|
||||
"python" { Start-PythonProject -Key $key -Proj $proj -ListenPort $devPort -HostBind $BindHost -RunDetached $Detached.IsPresent }
|
||||
"vite" { Start-ViteProject -Key $key -Proj $proj -ListenPort $devPort -HostBind $BindHost -RunDetached $Detached.IsPresent }
|
||||
"nextjs" { Start-NextProject -Key $key -Proj $proj -ListenPort $devPort -RunDetached $Detached.IsPresent }
|
||||
default {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user