[CmdletBinding()] param( [string]$RepoRoot = "", [string]$RemoteHost = "", [string]$RemoteUser = "", [string]$RemoteRoot = "", [string]$SshKeyPath = "", [string]$ServiceName = "tradebot", [switch]$NoRestart, [switch]$DryRun ) $ErrorActionPreference = "Stop" if (-not $RepoRoot) { $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path } if (-not $RemoteHost -and $env:TORCH_DEPLOY_PI_HOST) { $RemoteHost = $env:TORCH_DEPLOY_PI_HOST } if (-not $RemoteUser -and $env:TORCH_DEPLOY_PI_USER) { $RemoteUser = $env:TORCH_DEPLOY_PI_USER } if (-not $RemoteRoot -and $env:TORCH_DEPLOY_PI_ROOT) { $RemoteRoot = $env:TORCH_DEPLOY_PI_ROOT } if (-not $SshKeyPath -and $env:TORCH_DEPLOY_PI_SSH_KEY) { $SshKeyPath = $env:TORCH_DEPLOY_PI_SSH_KEY } if (-not $RemoteHost) { $RemoteHost = "192.168.0.185" } if (-not $RemoteUser) { $RemoteUser = "sevenhill" } if (-not $RemoteRoot) { $RemoteRoot = "/mnt/data/tradebot" } $RuntimeDir = Join-Path $RepoRoot "runtime" $artifactNames = @( "lstm_forecaster.json", "torch_retrain_guard.json", "torch_threshold_calibration.json" ) $localFiles = @() foreach ($name in $artifactNames) { $path = Join-Path $RuntimeDir $name if (Test-Path $path) { $localFiles += (Resolve-Path $path).Path } } if ($localFiles.Count -eq 0) { throw "No Torch artifacts found in $RuntimeDir." } function ConvertTo-RemoteSingleQuoted { param([string]$Value) return "'" + ($Value -replace "'", "'\''") + "'" } function Invoke-LoggedCommand { param( [string]$Exe, [string[]]$Arguments ) $rendered = @($Exe) + $Arguments Write-Host ($rendered -join " ") if ($DryRun) { return } & $Exe @Arguments if ($LASTEXITCODE -ne 0) { throw "$Exe failed with exit code $LASTEXITCODE." } } $ssh = (Get-Command "ssh.exe" -ErrorAction SilentlyContinue) if (-not $ssh) { $ssh = Get-Command "ssh" -ErrorAction Stop } $scp = (Get-Command "scp.exe" -ErrorAction SilentlyContinue) if (-not $scp) { $scp = Get-Command "scp" -ErrorAction Stop } $commonSshArgs = @("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=15") if ($SshKeyPath) { $expandedKey = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SshKeyPath) $commonSshArgs += @("-i", $expandedKey) } $remote = "${RemoteUser}@${RemoteHost}" $remoteRuntime = "$RemoteRoot/runtime" $remoteIncoming = "$remoteRuntime/.incoming-torch" $mkdirCommand = "mkdir -p $(ConvertTo-RemoteSingleQuoted $remoteIncoming) $(ConvertTo-RemoteSingleQuoted $remoteRuntime)" Invoke-LoggedCommand $ssh.Source (@($commonSshArgs + @($remote, $mkdirCommand))) $destination = "${remote}:$remoteIncoming/" Invoke-LoggedCommand $scp.Source (@($commonSshArgs + $localFiles + @($destination))) $moveParts = @() foreach ($path in $localFiles) { $name = Split-Path $path -Leaf $moveParts += "mv -f $(ConvertTo-RemoteSingleQuoted "$remoteIncoming/$name") $(ConvertTo-RemoteSingleQuoted "$remoteRuntime/$name")" } $moveCommand = $moveParts -join " && " Invoke-LoggedCommand $ssh.Source (@($commonSshArgs + @($remote, $moveCommand))) if (-not $NoRestart) { $restartCommand = "cd $(ConvertTo-RemoteSingleQuoted $RemoteRoot) && docker compose restart $(ConvertTo-RemoteSingleQuoted $ServiceName)" Invoke-LoggedCommand $ssh.Source (@($commonSshArgs + @($remote, $restartCommand))) } Write-Host "Synced Torch artifacts to ${remote}:$remoteRuntime"