diff --git a/README.md b/README.md index d0f6220..698da0f 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Set the `ZCONFIG` environment variable to point at a config somewhere else (both "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 --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) "start": { // optional zstart pre-steps @@ -101,6 +102,7 @@ Set the `ZCONFIG` environment variable to point at a config somewhere else (both Optional blocks do real work: +- **`install`** — how `zsetup` installs a python project's dependencies into its `.venv`: the pip args, e.g. `"-e ."`, `"-e backend"` (deps in a subfolder), or `"-r requirements.txt"`. Omit it and `zsetup` auto-detects a root `pyproject.toml`/`setup.py` (`-e .`) or `requirements.txt` (`-r requirements.txt`). `zstart` never installs — run `zsetup ` once, then `zstart `. - **`start`** — pre-start steps for `zstart`: `gitPull: true` runs `git pull --ff-only` in the project root first (never starts a stale checkout), and `env` sets environment variables for the dev-server process (feature flags, reload switches). - **`db`** — deploys wait for `pg_isready` and `zbackup_ec2` pulls a `pg_dump`, both against the compose service named `db`. Omit it and those steps are skipped cleanly. - **`deploy.gitPull`** — `git pull --ff-only` in the project root before zipping, so a merged PR actually ships. Since `zdeploy` zips your working tree, a checkout left behind `origin` would otherwise deploy stale code *and still bump the build number* — a silent no-op that looks like success. A failed pull (dirty tree that conflicts, diverged history) aborts the deploy rather than shipping uncertain code. @@ -116,6 +118,7 @@ The `.cmd` wrappers are the everyday interface. Every command takes one or more | Command | What it does | |---|---| +| `zsetup ...` | Create the project's Python venv + install deps (or `npm install` for node) | | `zstart ...` | Start local dev server(s) | | `zstartd ...` | Same, detached (new window, returns immediately) | | `zkill ... \| all` | Kill local dev server(s) by port | diff --git a/bash/README.md b/bash/README.md index e211ce9..8b598a3 100644 --- a/bash/README.md +++ b/bash/README.md @@ -35,6 +35,7 @@ command argument: | Command | What it does | |---|---| +| `zsetup

` | create the project's python venv + install deps (or `npm install`) | | `zstart

[--detached]` | start local dev server (python/vite/nextjs) | | `zkill

` | free the project's dev port | | `zrestart

[--detached]` | kill + start in one command | diff --git a/bash/zconfig.example.json b/bash/zconfig.example.json index dcc28da..6d054e0 100644 --- a/bash/zconfig.example.json +++ b/bash/zconfig.example.json @@ -30,6 +30,7 @@ "localRoot": "/home/youruser/code/pyapp", "startModule": "pyapp.main", "_startApp_note": "ASGI/FastAPI app? Use \"startApp\": \"app.main:app\" instead of startModule (runs uvicorn --port --reload).", + "install": "-e .", "ports": { "dev": 8080 }, "domain": "pyapp.yourdomain.com", "start": { diff --git a/bash/zsetup b/bash/zsetup new file mode 100755 index 0000000..56c7f3e --- /dev/null +++ b/bash/zsetup @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# 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. +# +# zsetup — prepare a project for local dev: create its Python venv and install +# dependencies (python kind), or run `npm install` (vite/nextjs). Idempotent - +# safe to re-run to refresh deps. `zstart` only warns when a venv is missing; +# this is the command that creates it. +# +# Usage: +# zsetup [ ...] +# zsetup --force # recreate the python venv from scratch +# +# Python install command (first match wins): +# 1. the project's "install" config field (e.g. "-e backend", "-r reqs.txt") +# 2. auto: pyproject.toml / setup.py -> "-e ." | requirements.txt -> "-r requirements.txt" + +set -uo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh" +z_track_start "$@" +z_need_config + +projects=(); force=0 +while [ $# -gt 0 ]; do + case "$1" in + --force) force=1; shift ;; + -*) err "Unknown option: $1"; exit 1 ;; + *) projects+=("$1"); shift ;; + esac +done + +if [ "${#projects[@]}" -eq 0 ]; then + printf '\n'; warn "Usage: zsetup [ ...] [--force]" + dim " Projects in zconfig.json: $(zproj_csv)" + dim " Creates the python venv + installs deps (or runs npm install for vite/nextjs)." + exit 1 +fi + +# pip install args for a python project (one per line): config .install, else +# auto-detect a root pyproject.toml/setup.py or requirements.txt. Empty = unknown. +py_install_args() { # + local key="$1" root="$2" cfg + cfg="$(zproj "$key" .install)" + if [ -n "$cfg" ]; then printf '%s\n' $cfg; return 0; fi + if [ -f "$root/pyproject.toml" ] || [ -f "$root/setup.py" ]; then printf '%s\n' -e .; return 0; fi + if [ -f "$root/requirements.txt" ]; then printf '%s\n' -r requirements.txt; return 0; fi + return 0 +} + +setup_python() { # + local key="$1" root="$2" venv="$2/.venv" args=() a + z_require python3 + while IFS= read -r a; do [ -n "$a" ] && args+=("$a"); done < <(py_install_args "$key" "$root") + if [ "${#args[@]}" -eq 0 ]; then + err " Can't determine dependencies for '$key' (no pyproject.toml/setup.py/requirements.txt at $root)." + dim " Add an \"install\" field to the project, e.g. \"install\": \"-e backend\" or \"-r requirements.txt\"." + return 1 + fi + if [ "$force" -eq 1 ] && [ -d "$venv" ]; then dim " Removing existing venv (--force)..."; rm -rf "$venv"; fi + if [ ! -x "$venv/bin/python" ]; then + info " Creating venv: $venv" + python3 -m venv "$venv" || { err " venv creation failed"; return 1; } + else + dim " venv exists: $venv (installing/updating deps)" + fi + info " pip install ${args[*]}" + ( cd "$root" && "$venv/bin/pip" install "${args[@]}" ) || { err " pip install failed"; return 1; } + ok " $key ready - zstart $key will use $venv/bin/python." +} + +setup_node() { # + local key="$1" root="$2" + [ -f "$root/package.json" ] || { err " No package.json in $root"; return 1; } + z_require npm + info " npm install (in $root)..." + ( cd "$root" && npm install ) || { err " npm install failed"; return 1; } + ok " $key node_modules ready." +} + +rc=0 +for key in "${projects[@]}"; do + zproj_require "$key" + kind="$(zproj "$key" .kind)" + root="$(zproj_root "$key")" + printf '\n'; info "=== zsetup ($(zproj "$key" .label)) ===" + [ -d "$root" ] || { err " Project root not found: $root"; rc=1; continue; } + case "$kind" in + python) setup_python "$key" "$root" || rc=1 ;; + vite|nextjs) setup_node "$key" "$root" || rc=1 ;; + *) dim " kind '$kind' - nothing to set up locally." ;; + esac +done +printf '\n' +exit "$rc" diff --git a/zconfig.example.json b/zconfig.example.json index d8f090d..682dda9 100644 --- a/zconfig.example.json +++ b/zconfig.example.json @@ -25,6 +25,7 @@ "localRoot": "C:\\YourRoot\\pyapp", "startModule": "pyapp.main", "_startApp_note": "ASGI/FastAPI app? Use \"startApp\": \"app.main:app\" instead of startModule (runs uvicorn --port --reload).", + "install": "-e .", "ports": { "dev": 8080 }, "domain": "pyapp.yourdomain.com", "start": { diff --git a/zsetup.cmd b/zsetup.cmd new file mode 100644 index 0000000..4d645bd --- /dev/null +++ b/zsetup.cmd @@ -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 "%~dp0zsetup.ps1" %* diff --git a/zsetup.ps1 b/zsetup.ps1 new file mode 100644 index 0000000..84e95d9 --- /dev/null +++ b/zsetup.ps1 @@ -0,0 +1,106 @@ +# 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. + +# zsetup.ps1 — prepare a project for local dev: create its Python venv and install +# dependencies (python kind), or run `npm install` (vite/nextjs). Idempotent - +# safe to re-run to refresh deps. `zstart` only warns when a venv is missing; +# this is the command that creates it. +# +# Usage: +# zsetup [ ...] +# zsetup -Force # recreate the python venv from scratch +# +# Python install command (first match wins): +# 1. the project's "install" config field (e.g. "-e backend", "-r reqs.txt") +# 2. auto: pyproject.toml / setup.py -> "-e ." | requirements.txt -> "-r requirements.txt" + +param( + [Parameter(Position = 0, ValueFromRemainingArguments = $true)] + [string[]]$Projects = @(), + [switch]$Force +) + +$ErrorActionPreference = "Stop" +. (Join-Path $PSScriptRoot "ZHelpers.ps1") +Start-ZTracking + +$Projects = @($Projects | ForEach-Object { $_.TrimStart('-') }) + +if ($Projects.Count -eq 0) { + Write-Host "" + Write-Host "Usage: zsetup [ ...] [-Force]" -ForegroundColor Yellow + Write-Host " Projects in zconfig.json: $((Get-ZProjectKeys) -join ', ')" -ForegroundColor Gray + Write-Host " Creates the python venv + installs deps (or runs npm install for vite/nextjs)." -ForegroundColor Gray + Stop-ZTracking; exit 1 +} + +# pip install args for a python project: config .install, else auto-detect a +# root pyproject.toml/setup.py or requirements.txt. Empty array = unknown. +function Get-PyInstallArgs { + param([string]$Root, $Proj) + if ($Proj.install) { return @([string]$Proj.install -split '\s+' | Where-Object { $_ -ne '' }) } + if ((Test-Path (Join-Path $Root "pyproject.toml")) -or (Test-Path (Join-Path $Root "setup.py"))) { return @("-e", ".") } + if (Test-Path (Join-Path $Root "requirements.txt")) { return @("-r", "requirements.txt") } + return @() +} + +function Invoke-PythonSetup { + param([string]$Key, [string]$Root, $Proj) + $installArgs = Get-PyInstallArgs -Root $Root -Proj $Proj + if ($installArgs.Count -eq 0) { + Write-Host " Can't determine dependencies for '$Key' (no pyproject.toml/setup.py/requirements.txt in $Root)." -ForegroundColor Red + Write-Host " Add an `"install`" field to the project, e.g. `"install`": `"-e backend`" or `"-r requirements.txt`"." -ForegroundColor DarkGray + return $false + } + $venv = Join-Path $Root ".venv" + $venvPython = Join-Path $venv "Scripts\python.exe" + if ($Force -and (Test-Path -LiteralPath $venv)) { + Write-Host " Removing existing venv (-Force)..." -ForegroundColor DarkGray + Remove-Item -LiteralPath $venv -Recurse -Force + } + if (-not (Test-Path -LiteralPath $venvPython)) { + Write-Host " Creating venv: $venv" -ForegroundColor Cyan + & python -m venv $venv + if ($LASTEXITCODE -ne 0) { Write-Host " venv creation failed" -ForegroundColor Red; return $false } + } else { + Write-Host " venv exists: $venv (installing/updating deps)" -ForegroundColor DarkGray + } + $pip = Join-Path $venv "Scripts\pip.exe" + Write-Host " pip install $($installArgs -join ' ')" -ForegroundColor Cyan + Push-Location -LiteralPath $Root + try { & $pip install @installArgs; $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location } + if (-not $ok) { Write-Host " pip install failed" -ForegroundColor Red; return $false } + Write-Host " $Key ready - zstart $Key will use $venvPython." -ForegroundColor Green + return $true +} + +function Invoke-NodeSetup { + param([string]$Key, [string]$Root) + if (-not (Test-Path -LiteralPath (Join-Path $Root "package.json"))) { + Write-Host " No package.json in $Root" -ForegroundColor Red; return $false + } + Write-Host " npm install (in $Root)..." -ForegroundColor Cyan + Push-Location -LiteralPath $Root + try { & npm install; $ok = ($LASTEXITCODE -eq 0) } finally { Pop-Location } + if (-not $ok) { Write-Host " npm install failed" -ForegroundColor Red; return $false } + Write-Host " $Key node_modules ready." -ForegroundColor Green + return $true +} + +$exitCode = 0 +foreach ($key in $Projects) { + $proj = Get-ZProject -Key $key + $root = $proj.localRoot + Write-Host "" + Write-Host "=== zsetup ($($proj.label)) ===" -ForegroundColor Cyan + if (-not (Test-Path -LiteralPath $root)) { Write-Host " Project root not found: $root" -ForegroundColor Red; $exitCode = 1; continue } + switch ([string]$proj.kind) { + "python" { if (-not (Invoke-PythonSetup -Key $key -Root $root -Proj $proj)) { $exitCode = 1 } } + "vite" { if (-not (Invoke-NodeSetup -Key $key -Root $root)) { $exitCode = 1 } } + "nextjs" { if (-not (Invoke-NodeSetup -Key $key -Root $root)) { $exitCode = 1 } } + default { Write-Host " kind '$($proj.kind)' - nothing to set up locally." -ForegroundColor DarkGray } + } +} +Write-Host "" +Stop-ZTracking; exit $exitCode