fix(bash): translate Windows config paths to WSL/Unix form

A shared zconfig.json (one file used from a Windows checkout and from
WSL via the symlink) holds Windows paths like "F:\evomedia.net\app".
The bash port passed those to local file ops verbatim, so every
path-using command failed on WSL (e.g. zbackup: "Root not found:
F:\evomedia.net\evomedia-docs").

Add a z_path helper that converts drive-letter paths to the host's
native form (wslpath, with a /mnt fallback) and is a no-op for Unix
paths and empty strings — so a bash-native config is unaffected. Route
every LOCAL path through it: localRoot (new zproj_root accessor),
paths.* (temp, backupsLocal, backupsEc2, scriptsRoot, oneDriveBackups),
ec2.pemKey (zec2_pem), and ztokens.dataDir. Server-side paths
(remote.path, composeDir, stackRoot, certsSource) are left verbatim.

Verified on WSL: all 11 project roots, all paths.*, and the C:/G: pem
and OneDrive paths resolve to existing dirs; zbackup evodocs (the
failing case) now runs clean end-to-end.
This commit is contained in:
KellyMichels 2026-07-22 17:46:51 -05:00
parent 09f86c1cba
commit 48a5e7f7d6
6 changed files with 47 additions and 21 deletions

View File

@ -29,7 +29,7 @@ while [ $# -gt 0 ]; do
esac
done
BACKUP_ROOT="$(zq '.paths.backupsLocal')"
BACKUP_ROOT="$(z_path "$(zq '.paths.backupsLocal')")"
include_scripts=0
if [ "${#projects[@]}" -eq 0 ]; then
mapfile -t projects < <(zproj_keys)
@ -76,7 +76,7 @@ exit_code=0
backup_project() { # <key>
local key="$1" root out_dir zip_path dump_dir db_dump extra="-"
zproj_require "$key"
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
out_dir="$BACKUP_ROOT/$key"; mkdir -p "$out_dir"
zip_path="$out_dir/$(ts)_${key}$(tag_suffix).zip"
@ -103,7 +103,7 @@ backup_project() { # <key>
backup_scripts() {
local out_dir zip_path scripts_root
scripts_root="$(zq '.paths.scriptsRoot')"
scripts_root="$(z_path "$(zq '.paths.scriptsRoot')")"
out_dir="$BACKUP_ROOT/scripts"; mkdir -p "$out_dir"
zip_path="$out_dir/$(ts)_scripts$(tag_suffix).zip"

View File

@ -25,7 +25,7 @@ if [ "${#projects[@]}" -eq 0 ]; then
mapfile -t projects < <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.remote.path != null) | .key' "$ZCONFIG")
fi
EC2_BACKUP_ROOT="$(zq '.paths.backupsEc2')"
EC2_BACKUP_ROOT="$(z_path "$(zq '.paths.backupsEc2')")"
REMOTE_HOME="/home/$(zec2_user)"
scp_down() { # <remote_path> <local_path>

View File

@ -40,7 +40,7 @@ EC2_IP="$(zec2_ip)"
STACK_ROOT="$(zq '.ec2.stackRoot')"
REMOTE_HOME="/home/$(zec2_user)"
EC2_USER="$(zec2_user)"
TEMP_ROOT="$(zq '.paths.temp')"
TEMP_ROOT="$(z_path "$(zq '.paths.temp')")"
{ [ -z "$TEMP_ROOT" ] || [ "$TEMP_ROOT" = "null" ]; } && TEMP_ROOT="${TMPDIR:-/tmp}"
mkdir -p "$TEMP_ROOT"
@ -167,7 +167,7 @@ wait_verify_static() { # <key> <pre_product_version> <pre_build_number>
deploy_git_pull() { # <key>
local key="$1" root branch
[ "$(zproj "$key" .deploy.gitPull)" = "true" ] || return 0
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
if [ ! -d "$root/.git" ]; then
warn " gitPull set but '$root' is not a git repo - skipping pull."
return 0
@ -187,7 +187,7 @@ deploy_git_pull() { # <key>
deploy_python() { # <key>
local key="$1" start=$SECONDS root remote_path compose_dir app_svc zip_name zip_local
local bv="" has_version_tool=0 excl=() attempt out label domain
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
remote_path="$(zproj "$key" .remote.path)"
compose_dir="$(zremote_compose_dir "$key")"
app_svc="$(zproj "$key" .remote.appService)"; [ -n "$app_svc" ] || app_svc="app"
@ -272,7 +272,7 @@ deploy_python() { # <key>
deploy_vite() { # <key>
local key="$1" start=$SECONDS root remote_path zip_name zip_local excl=()
local pre_pv="" pre_bn="" label domain edge_pc
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
remote_path="$(zproj "$key" .remote.path)"
zip_name="$(deploy_zip_name "$key")"
zip_local="$TEMP_ROOT/$zip_name"
@ -328,7 +328,7 @@ deploy_vite() { # <key>
deploy_next() { # <key>
local key="$1" start=$SECONDS root remote_path app_svc zip_name zip_local excl=()
local pre_pv="" pre_bn="" label domain db_user db_name prod_port
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
remote_path="$(zproj "$key" .remote.path)"
app_svc="$(zproj "$key" .remote.appService)"; [ -n "$app_svc" ] || app_svc="web"
zip_name="$(deploy_zip_name "$key")"
@ -412,7 +412,7 @@ deploy_next() { # <key>
deploy_edge() { # <key>
local key="$1" root remote_path pc label cert_mount="" rm_stale="" f
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
remote_path="$(zproj "$key" .remote.path)"
pc="$(zproj "$key" .proxyContainer)"
label="$(zproj "$key" .label)"
@ -452,7 +452,7 @@ deploy_edge() { # <key>
deploy_docker() { # <key>
local key="$1" root remote_path label f domain
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
remote_path="$(zproj "$key" .remote.path)"
label="$(zproj "$key" .label)"
domain="$(zproj "$key" .domain)"

View File

@ -33,6 +33,30 @@ z_require() {
[ "$miss" -eq 0 ] || exit 1
}
# ---- local path translation -------------------------------------------------
# A shared zconfig.json may hold Windows paths (e.g. "F:\evomedia.net\app") when
# the same file is used from a Windows checkout and from WSL. Translate a
# drive-letter path to the host's native form so local file ops work; leave
# anything else (Unix paths, empty strings) untouched — so this is a no-op on
# native Linux/macOS. Only LOCAL paths get run through this; server-side paths
# (remote.path, composeDir, stackRoot, certsSource) stay verbatim.
z_path() {
local p="$1"
case "$p" in
[A-Za-z]:[\\/]*)
if command -v wslpath >/dev/null 2>&1; then
wslpath -u "$p" 2>/dev/null || printf '%s' "$p"
else
# Fallback when wslpath is absent: F:\a\b -> /mnt/f/a/b
local drive="${p%%:*}" rest="${p#*:}"
rest="${rest//\\//}"
printf '/mnt/%s%s' "${drive,,}" "$rest"
fi
;;
*) printf '%s' "$p" ;;
esac
}
# ---- config accessors (jq) --------------------------------------------------
z_need_config() {
z_require jq
@ -52,6 +76,8 @@ zproj_keys_with_domain() {
}
# Field of a project: zproj <key> <dotpath> e.g. zproj sp .kind | zproj sp .ports.dev
zproj() { jq -r --arg k "$1" ".projects[\$k]$2 // empty" "$ZCONFIG"; }
# A project's local root, translated to the host's native path form.
zproj_root() { z_path "$(zproj "$1" .localRoot)"; }
zproj_require() {
if [ "$(jq -r --arg k "$1" '(.projects[$k] != null)' "$ZCONFIG")" != "true" ]; then
err "Unknown project key '$1'. Available: $(zproj_csv)"
@ -60,7 +86,7 @@ zproj_require() {
}
zec2_ip() { zq '.ec2.ip'; }
zec2_user() { zq '.ec2.user'; }
zec2_pem() { zq '.ec2.pemKey'; }
zec2_pem() { z_path "$(zq '.ec2.pemKey')"; }
zec2_target() { printf '%s@%s' "$(zec2_user)" "$(zec2_ip)"; }
# Remote compose directory: remote.composeDir if set, else remote.path.
zremote_compose_dir() {
@ -124,7 +150,7 @@ json_build_label() {
# Local build label by project kind (mirrors Get-LocalVersionLabel).
local_version_label() {
local key="$1" kind root out
kind="$(zproj "$key" .kind)"; root="$(zproj "$key" .localRoot)"
kind="$(zproj "$key" .kind)"; root="$(zproj_root "$key")"
case "$kind" in
python)
if [ -f "$root/scripts/build_version_tool.py" ]; then
@ -294,7 +320,7 @@ z_record() { # <lines> <chars> <est>
command -v jq >/dev/null 2>&1 || return
local lines="$1" chars="$2" est="$3" dir model projects
dir="${ZTOKENS_DATA:-}"
[ -z "$dir" ] && [ -f "$ZCONFIG" ] && dir="$(jq -r '.ztokens.dataDir // empty' "$ZCONFIG" 2>/dev/null)"
[ -z "$dir" ] && [ -f "$ZCONFIG" ] && dir="$(z_path "$(jq -r '.ztokens.dataDir // empty' "$ZCONFIG" 2>/dev/null)")"
if [ -z "$dir" ]; then
if [ -d "$_ZDIR/../../ztokens/data" ]; then dir="$_ZDIR/../../ztokens/data"; else dir="$HOME/.ztokens/data"; fi
fi

View File

@ -52,7 +52,7 @@ detach() { # <key> <workdir> <cmd...>
# "start": { "gitPull": true, "env": { "SOME_FLAG": "1" } }
start_prep() { # <key>
local key="$1" root kv name val
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
while IFS=$'\t' read -r name val; do
[ -n "$name" ] || continue
export "$name=$val"
@ -68,7 +68,7 @@ start_prep() { # <key>
start_python() { # <key> <port>
local key="$1" port="$2" root module exe bv=""
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
module="$(zproj "$key" .startModule)"
[ -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; }
@ -105,7 +105,7 @@ start_python() { # <key> <port>
start_vite() { # <key> <port>
local key="$1" port="$2" root
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
[ -f "$root/package.json" ] || { err "package.json not found in $root"; return 1; }
@ -133,7 +133,7 @@ start_vite() { # <key> <port>
start_next() { # <key> <port>
local key="$1" port="$2" root
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
[ -f "$root/package.json" ] || { err "package.json not found in $root"; return 1; }

View File

@ -29,8 +29,8 @@ done
# ---- no-args mode: backups -> offsite, new files only ------------------------
if [ "${#projects[@]}" -eq 0 ]; then
BACKUPS_ROOT="$(dirname "$(zq '.paths.backupsLocal')")"
OFFSITE_ROOT="$(zq '.paths.oneDriveBackups')"
BACKUPS_ROOT="$(dirname "$(z_path "$(zq '.paths.backupsLocal')")")"
OFFSITE_ROOT="$(z_path "$(zq '.paths.oneDriveBackups')")"
printf '\n'; info "=== zsync (backups -> offsite) ==="
dim " Source: $BACKUPS_ROOT"
@ -73,7 +73,7 @@ for key in "${projects[@]}"; do
printf '\n'; warn "zsync project mode only supports vite kind (builds and mirrors dist/). '$key' is kind '$kind'."
exit 1
fi
root="$(zproj "$key" .localRoot)"
root="$(zproj_root "$key")"
if [ -z "$dest" ]; then
printf '\n'; info "=== zsync ($(zproj "$key" .label)) ==="