# Native Windows PowerShell public installer; CMD uses install.bat. # SourceDir installs a downloaded SCM artifact without contacting the CDN. #Requires -Version 5.1 [CmdletBinding()] param([string]$SourceDir) & { $ErrorActionPreference = 'Stop' $DefaultDownloadBase = 'https://jimeng.jianying.com/canvas-cli/install/1_0_0_46' $downloadBase = $DefaultDownloadBase if ($env:DREAMINA_CANVAS_DOWNLOAD_BASE) { $downloadBase = $env:DREAMINA_CANVAS_DOWNLOAD_BASE } $downloadBase = $downloadBase.TrimEnd('/') if ($env:OS -ne 'Windows_NT') { throw 'Run this installer in native Windows PowerShell. Use install.sh inside WSL.' } $architecture = $env:PROCESSOR_ARCHITEW6432 if (-not $architecture) { $architecture = $env:PROCESSOR_ARCHITECTURE } if ($architecture -ne 'AMD64') { throw "Unsupported Windows architecture: $architecture" } $tar = (Get-Command tar.exe -CommandType Application -ErrorAction Stop).Source if (-not $SourceDir -and ([uri]$downloadBase).Scheme -ne 'https') { throw 'Download base must use HTTPS.' } $installDir = $env:DREAMINA_CANVAS_INSTALL_DIR if (-not $installDir) { $installDir = $env:INSTALL_DIR } if (-not $installDir) { $existing = Get-Command dreamina-canvas.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($existing) { $installDir = Split-Path -Parent $existing.Source } } if (-not $installDir) { $installDir = Join-Path $env:USERPROFILE 'bin' } $skillRoot = $env:DREAMINA_CANVAS_SKILL_DIR if (-not $skillRoot) { $skillRoot = Join-Path $env:USERPROFILE '.agents\skills' } $null = New-Item -ItemType Directory -Force -Path $installDir, $skillRoot $installDir = (Resolve-Path -LiteralPath $installDir).Path $skillRoot = (Resolve-Path -LiteralPath $skillRoot).Path $target = Join-Path $installDir 'dreamina-canvas.exe' $skillTarget = Join-Path $skillRoot 'dreamina-canvas-cli' $skillTargetItem = Get-Item -LiteralPath $skillTarget -Force -ErrorAction SilentlyContinue if ($skillTargetItem -and (($skillTargetItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) { throw 'Skill target is a link. Select its actual parent directory using DREAMINA_CANVAS_SKILL_DIR.' } # Lock the shared Skill destination even when callers choose different binary directories. $lock = Join-Path $skillRoot '.dreamina-canvas.install-lock' $null = New-Item -ItemType Directory -Path $lock -ErrorAction Stop $stage = Join-Path $installDir ('.dreamina-canvas.install.' + [guid]::NewGuid()) $keepBackup = $false $skillChanged = $false $binaryChanged = $false $binaryReplaceStarted = $false $hadSkill = Test-Path -LiteralPath $skillTarget $hadBinary = Test-Path -LiteralPath $target $failure = $null try { $null = New-Item -ItemType Directory -Path $stage foreach ($name in @('dreamina-canvas_windows_amd64.exe', 'skill.tar.gz')) { $destination = Join-Path $stage $name if ($SourceDir) { Copy-Item -LiteralPath (Join-Path $SourceDir $name) -Destination $destination } else { Invoke-WebRequest -UseBasicParsing -Uri "$downloadBase/$name" -OutFile $destination } if ((Get-Item -LiteralPath $destination).Length -eq 0) { throw "Empty artifact: $name" } } $archive = Join-Path $stage 'skill.tar.gz' $entries = @(& $tar -tzf $archive) if ($LASTEXITCODE -ne 0) { throw 'Invalid Skill archive.' } foreach ($entry in $entries) { if ($entry -notmatch '^dreamina-canvas-cli(/|$)' -or $entry -match '(^|/)\.\.?(/|$)|\\|:') { throw "Unsafe Skill archive path: $entry" } } if ($entries -cnotcontains 'dreamina-canvas-cli/SKILL.md') { throw 'Skill archive is missing SKILL.md.' } $entryDetails = @(& $tar -tvzf $archive) if ($LASTEXITCODE -ne 0) { throw 'Cannot inspect Skill archive entry types.' } foreach ($entryDetail in $entryDetails) { if ($entryDetail -notmatch '^[-d]') { throw "Unsupported Skill archive entry type: $entryDetail" } } & $tar -xzf $archive -C $stage if ($LASTEXITCODE -ne 0) { throw 'Cannot extract Skill archive.' } $stagedSkill = Join-Path $stage 'dreamina-canvas-cli' if (-not (Test-Path -LiteralPath (Join-Path $stagedSkill 'SKILL.md') -PathType Leaf)) { throw 'Missing extracted SKILL.md.' } $skillBackup = Join-Path $stage 'skill-backup' $binaryBackup = Join-Path $stage 'binary-backup.exe' if ($hadSkill) { Copy-Item -LiteralPath $skillTarget -Destination $skillBackup -Recurse } if ($hadBinary) { Copy-Item -LiteralPath $target -Destination $binaryBackup } # Replace the executable first: a running Windows executable fails before changing Skill. $stagedBinary = Join-Path $stage 'dreamina-canvas_windows_amd64.exe' Write-Output 'Verifying downloaded CLI:' & $stagedBinary --format json version if ($LASTEXITCODE -ne 0) { throw 'Downloaded CLI cannot run; existing installation was not changed.' } # Use NullString to pass a true .NET null; $null becomes an empty string. if ($hadBinary) { $binaryReplaceStarted = $true [IO.File]::Replace($stagedBinary, $target, [NullString]::Value) } else { [IO.File]::Move($stagedBinary, $target) } $binaryChanged = $true $null = New-Item -ItemType Directory -Force -Path $skillTarget $skillChanged = $true Get-ChildItem -LiteralPath $skillTarget -Force | Remove-Item -Recurse -Force Get-ChildItem -LiteralPath $stagedSkill -Force | Copy-Item -Destination $skillTarget -Recurse -Force Write-Output "Installed $target" Write-Output "Installed $skillTarget" } catch { $failure = $_ try { if ($skillChanged) { if ($hadSkill) { Get-ChildItem -LiteralPath $skillTarget -Force | Remove-Item -Recurse -Force Get-ChildItem -LiteralPath $skillBackup -Force | Copy-Item -Destination $skillTarget -Recurse -Force } else { Remove-Item -LiteralPath $skillTarget -Recurse -Force } } } catch { $keepBackup = $true Write-Warning "Skill recovery failed: $_. Backups retained at $stage" } try { # ReplaceFile can fail after removing the original target name. if ($binaryChanged -or ($binaryReplaceStarted -and -not (Test-Path -LiteralPath $target))) { if ($hadBinary) { Copy-Item -LiteralPath $binaryBackup -Destination $target -Force } else { Remove-Item -LiteralPath $target -Force } } } catch { $keepBackup = $true Write-Warning "Binary recovery failed: $_. Backups retained at $stage" } } finally { try { Remove-Item -LiteralPath $lock -ErrorAction Stop } catch { if (-not $failure) { $failure = $_ } Write-Warning "Lock cleanup failed: $_" } try { if (-not $keepBackup -and (Test-Path -LiteralPath $stage)) { Remove-Item -LiteralPath $stage -Recurse -Force } } catch { if (-not $failure) { $failure = $_ } Write-Warning "Stage cleanup failed: $_. Temporary files retained at $stage" } } if ($failure) { throw $failure } if ($env:DREAMINA_CANVAS_SKIP_PATH -ne '1') { try { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if (([string]$userPath -split ';').TrimEnd('\') -notcontains $installDir.TrimEnd('\')) { [Environment]::SetEnvironmentVariable('Path', "$installDir;$userPath", 'User') Write-Output "Saved user PATH: $installDir" } } catch { Write-Warning "Could not save user PATH. Add $installDir manually. $_" } if (($env:PATH -split ';').TrimEnd('\') -notcontains $installDir.TrimEnd('\')) { $env:PATH = "$installDir;$env:PATH" } } else { Write-Output "Automatic PATH update skipped. Add $installDir to your User PATH manually." } $quotedTarget = $target.Replace("'", "''") Write-Output "PowerShell verify: & '$quotedTarget' --format json version" Write-Output "PowerShell login: & '$quotedTarget' auth login" Write-Output ('CMD verify: "{0}" --format json version' -f $target) Write-Output ('CMD login: "{0}" auth login' -f $target) Write-Output 'Restart your terminal and Agent application to reload PATH and the Skill. A child PowerShell cannot update its parent CMD.' }