hypertwist/scripts/run-hypertwist-remote-windows-file-pull.sh
2026-06-24 21:51:01 +00:00

163 lines
4.2 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
HYPERTWIST_REMOTE_WINDOWS_PASSWORD=... \
scripts/run-hypertwist-remote-windows-file-pull.sh \
--map "C:\HyperTwist_worktrees\phase10validate\docs\generated\higher_dimensional_training_maps\phase6c_dedicated_family_package_validation_report.json::docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json"
Options:
--map <remote_windows_path>::<local_repo_path>
Copy one remote Windows file back into the local repo. May be repeated.
--dry-run
Print the pull plan without transferring files.
Environment:
HYPERTWIST_REMOTE_WINDOWS_PASSWORD Required password for the reverse-SSH Windows user.
HYPERTWIST_REMOTE_TUNNEL_PORT Optional, defaults to 22022.
HYPERTWIST_REMOTE_WINDOWS_USER Optional, defaults to "anthracite ace".
HYPERTWIST_REMOTE_TUNNEL_HOST Optional, defaults to "localhost".
EOF
}
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required but was not found on PATH." >&2
exit 1
fi
if ! command -v sha256sum >/dev/null 2>&1; then
echo "sha256sum is required but was not found on PATH." >&2
exit 1
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
remote_ps_wrapper="${repo_root}/scripts/run-hypertwist-remote-windows-powershell.sh"
if [[ ! -x "$remote_ps_wrapper" ]]; then
echo "Expected executable wrapper at $remote_ps_wrapper" >&2
exit 1
fi
dry_run="false"
maps=()
while [[ $# -gt 0 ]]; do
case "$1" in
--map)
maps+=("${2:-}")
shift 2
;;
--dry-run)
dry_run="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 1
;;
esac
done
if [[ ${#maps[@]} -eq 0 ]]; then
echo "Provide at least one --map." >&2
exit 1
fi
ps_single_quote() {
printf "'%s'" "${1//\'/\'\'}"
}
write_base64_to_file() {
local base64_payload="$1"
local destination_path="$2"
python3 - <<'PY' "$base64_payload" "$destination_path"
import base64
import pathlib
import sys
payload = sys.argv[1]
destination = pathlib.Path(sys.argv[2])
destination.parent.mkdir(parents=True, exist_ok=True)
if payload:
destination.write_bytes(base64.b64decode(payload.encode("ascii")))
else:
destination.write_bytes(b"")
PY
}
for mapping in "${maps[@]}"; do
if [[ "$mapping" != *"::"* ]]; then
echo "Map must use <remote_windows_path>::<local_repo_path>: $mapping" >&2
exit 1
fi
remote_path="${mapping%%::*}"
local_path="${mapping#*::}"
if [[ -z "$remote_path" || -z "$local_path" ]]; then
echo "Map must include both remote and local paths: $mapping" >&2
exit 1
fi
if [[ "$local_path" = /* ]]; then
local_destination="$local_path"
else
local_destination="${repo_root}/${local_path}"
fi
if [[ "$dry_run" == "true" ]]; then
printf '%s -> %s\n' "$remote_path" "$local_destination"
continue
fi
remote_payload="$(
cat <<EOF
\$Path = $(ps_single_quote "$remote_path")
if (-not (Test-Path -LiteralPath \$Path)) {
throw "Remote file was not found at '\$Path'."
}
\$Bytes = [System.IO.File]::ReadAllBytes(\$Path)
\$Hash = (Get-FileHash -LiteralPath \$Path -Algorithm SHA256).Hash.ToLowerInvariant()
Write-Output ('REMOTE_HASH=' + \$Hash)
Write-Output 'REMOTE_BASE64_BEGIN'
[Console]::Out.Write([Convert]::ToBase64String(\$Bytes))
Write-Output ''
Write-Output 'REMOTE_BASE64_END'
EOF
)"
remote_output="$("$remote_ps_wrapper" --command "$remote_payload" | tr -d '\r')"
remote_hash="$(printf '%s\n' "$remote_output" | sed -n 's/^REMOTE_HASH=//p' | head -n 1)"
if [[ -z "$remote_hash" ]]; then
echo "Did not receive REMOTE_HASH for $remote_path" >&2
exit 1
fi
base64_payload="$(
printf '%s\n' "$remote_output" \
| awk '/^REMOTE_BASE64_BEGIN$/{flag=1; next} /^REMOTE_BASE64_END$/{flag=0} flag' \
| tr -d '\n'
)"
write_base64_to_file "$base64_payload" "$local_destination"
local_hash="$(sha256sum "$local_destination" | awk '{print tolower($1)}')"
if [[ "$local_hash" != "$remote_hash" ]]; then
echo "Hash mismatch for $remote_path" >&2
echo " remote: $remote_hash" >&2
echo " local : $local_hash" >&2
exit 1
fi
printf 'Pulled %s -> %s (%s)\n' "$remote_path" "$local_destination" "$local_hash"
done