docs: 重构 README 新手使用说明,添加一键安装脚本文档

This commit is contained in:
2026-06-22 15:42:44 +08:00
parent cba7f9fb55
commit 04ef9b65a5
4 changed files with 529 additions and 70 deletions

133
README.md
View File

@@ -17,26 +17,94 @@
## 新手使用说明
如果你是第一次使用命令行工具,请按以下步骤操作。
如果你是第一次使用命令行工具,请按以下步骤操作。我们提供了一键安装脚本,尽量让你少输入命令。
### 准备工作
### 安装前准备
- **PowerPoint**:需要安装 Microsoft PowerPointOffice 2016 或更新版本)
- **网络连接**:安装过程中需要下载工具
- **理解命令行工具**:你需要理解命令行的基本操作,包括文件路径、参数传递等。
- **解压项目**:把下载的 `pptopic.zip` 解压到任意位置,例如桌面
### PowerShell的基本知识
### 一键安装(推荐)
PowerShell在Windows中是自带的所以一般情况下无需额外安装。如果你不知道如何打开可以在开始菜单搜索 "PowerShell",通常就能看见,点击后即可打开。
项目根目录下有一个 `setup.bat`,它会自动完成:
在PowerShell中跳转目录只需要输入 `cd` 加上目录路径即可。目录路径可以从资源管理器中在地址栏中复制。如果你的目录路径中包含空格,需要在路径中添加引号。
1. 安装 `uv`Python 包管理器)
2. 安装 Python 3.13
3. 安装 `pngquant`(图片压缩工具,强烈推荐)
4. 安装 `pptopic`
5. 验证安装结果
#### 方法 A双击运行最简单
1. 打开解压后的 `pptopic` 文件夹
2. 找到 `setup.bat` 文件
3. **双击** `setup.bat`
4. 等待安装完成,窗口会显示版本信息
> 如果 Windows 提示“Windows 已保护你的电脑”,请点击“更多信息” → “仍要运行”。
#### 方法 B右键在 PowerShell 中运行
如果双击运行被系统拦截,可以按以下步骤:
1. 打开解压后的 `pptopic` 文件夹
2. 在文件夹**空白处**按住 `Shift` 键,同时点击**鼠标右键**
3. 选择“在此处打开 PowerShell 窗口”Windows 11 可能显示为“在终端中打开”)
4. 在弹出的蓝色窗口中输入以下命令,然后按回车:
```powershell
# 这是一个例子
cd "C:\Users\User SomeX\Desktop\OneFolder"
.\setup.bat
```
### 第一步:安装 uv
等待安装完成即可。
#### 安装完成后
如果看到类似下面的输出,说明安装成功:
```
--- uv 版本 ---
uv 0.11.20
--- pngquant 版本 ---
2.17.0
--- pptopic 版本 ---
pptopic 0.3.2
```
然后你可以直接在当前窗口使用 pptopic
```powershell
# 导出 PPTX 为长图
pptopic export presentation.pptx
# 工作中常用命令:导出 PPTX 为长图并优化图片
# 其中29999 是微信接受的最大高度
pptopic export presentation.pptx --optimize --max-height 29999 -o result.png
```
### 一键安装脚本的参数(可选)
如果你不想安装 pngquant可以使用以下方式运行
```powershell
# 在 PowerShell 中直接运行 setup.ps1跳过 pngquant
powershell -ExecutionPolicy Bypass -File .\setup.ps1 -SkipPngquant
```
其他可用参数:
- `-SkipPngquant`:跳过 pngquant 安装
- `-Force`:强制重新安装 uv 和 pngquant
- `-SkipPptopicInstall`:只安装环境,不安装 pptopic
### 手动安装(备用)
如果一键安装脚本在你的电脑上无法运行,可以按以下步骤手动安装。
#### 第一步:安装 uv
`uv` 是一个 Python 包管理器,用来安装 pptopic 及其依赖。
@@ -46,7 +114,7 @@ cd "C:\Users\User SomeX\Desktop\OneFolder"
cd "<你的解压目录>\pptopic"
```
赋予PowerShell执行权限
赋予 PowerShell 执行权限:
```powershell
Set-ExecutionPolicy Bypass -Scope CurrentUser
@@ -70,7 +138,7 @@ uv --version
> - 如果你已安装 [scoop](https://scoop.sh/),也可以直接 `scoop install uv`。
> - 如果想安装特定版本的 uv可以在运行脚本前设置环境变量`$env:UV_INSTALLER_VERSION = "0.11.20"`
### 第二步:安装 Python
#### 第二步:安装 Python
使用 uv 安装 Python 3.13 或更新版本:
@@ -78,9 +146,9 @@ uv --version
uv python install 3.13
```
### 第三步(可选):安装 pngquant 图片优化工具
#### 第三步(可选):安装 pngquant 图片优化工具
虽然是可选安装但我超级建议你安装因为ppt导出后图片通常较大不进行图片无损压缩会导致文件大小过大。
虽然是可选安装,但我超级建议你安装,因为 ppt 导出后,图片通常较大,不进行图片无损压缩,会导致文件大小过大。
如果你需要对导出的图片进行压缩优化,运行:
@@ -101,13 +169,13 @@ pngquant --version
> - 自定义安装目录:`.\install-pngquant.ps1 -InstallDir "D:\Tools\pngquant"`
> - 强制重新安装:`.\install-pngquant.ps1 -Force`
### 第四步安装pptopic
#### 第四步:安装 pptopic
```powershell
uv tool install -e .
```
### 第五步:开始使用
#### 第五步:开始使用
```powershell
# 导出 PPTX 为长图
@@ -117,11 +185,11 @@ pptopic export presentation.pptx
pptopic optimize image.png
# 工作中常用命令:导出 PPTX 为长图并优化图片
# 其中29999是微信接受的最大高度
# 其中29999 是微信接受的最大高度
pptopic export presentation.pptx --optimize --max-height 29999 -o result.png
```
至此安装完成。如果任何步骤遇到问题,请参考下方详细说明。
至此安装完成。
## 一般安装说明
@@ -222,6 +290,37 @@ scoop install pngquant
或从 [pngquant.org](https://pngquant.org/) 下载 Windows 版本手动配置。
## 常见问题
### 运行 `setup.bat` 时提示“无法加载文件,因为在此系统上禁止运行脚本”
这说明当前电脑通过组策略严格限制了脚本执行。`setup.bat` 已经尝试用 `-ExecutionPolicy Bypass` 绕过,但仍可能被拦截。
解决方法:使用 **方法 B**,在 PowerShell 窗口中手动运行:
```powershell
powershell -ExecutionPolicy Bypass -File .\setup.ps1
```
不需要以管理员身份运行,本安装脚本只修改当前用户的环境变量。
### 安装完成后关闭窗口,新窗口中找不到 `pptopic` 命令
请重新运行一次 `setup.bat`。如果问题依旧,请检查系统环境变量中的 PATH 是否包含以下目录:
- `%USERPROFILE%\.local\bin`uv 和 pptopic 的位置)
- `%APPDATA%\pngquant`pngquant 的位置)
### 不想安装 pngquant 怎么办?
可以运行:
```powershell
powershell -ExecutionPolicy Bypass -File .\setup.ps1 -SkipPngquant
```
但强烈建议安装,否则导出的长图文件会比较大。
## License
MIT License

24
setup.bat Normal file
View File

@@ -0,0 +1,24 @@
@echo off
chcp 65001 >nul
echo.
echo ========================================
echo pptopic 一键安装程序
echo ========================================
echo.
echo 即将自动安装uv、Python 3.13、pngquant 和 pptopic
echo 安装过程中请勿关闭此窗口...
echo.
powershell -ExecutionPolicy Bypass -File "%~dp0setup.ps1" %*
if %errorlevel% neq 0 (
echo.
echo 安装过程中出现错误,请查看上方提示。
echo.
pause
exit /b %errorlevel%
)
echo.
echo 按任意键关闭此窗口...
pause >nul

281
setup.ps1 Normal file
View File

@@ -0,0 +1,281 @@
#Requires -Version 5.1
<#
.SYNOPSIS
pptopic 一键安装脚本
.DESCRIPTION
自动完成以下安装步骤:
1. 安装 uvPython 包管理器)
2. 安装 Python 3.13
3. 安装 pngquant可选但推荐
4. 安装 pptopic
5. 验证安装结果
本脚本会自动处理 PowerShell 执行策略和当前会话 PATH 刷新,
安装完成后无需手动重启 PowerShell。
.PARAMETER SkipPngquant
跳过 pngquant 安装。
.PARAMETER Force
强制重新安装 uv 和 pngquant。
.PARAMETER SkipPptopicInstall
只安装环境,不安装 pptopic调试用
.EXAMPLE
.\setup.ps1
.EXAMPLE
.\setup.ps1 -SkipPngquant
#>
param(
[switch]$SkipPngquant,
[switch]$Force,
[switch]$SkipPptopicInstall
)
$ErrorActionPreference = "Stop"
$InformationPreference = "Continue"
$ProjectRoot = $PSScriptRoot
if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
$ProjectRoot = (Get-Location).Path
}
# ============================================================
# 工具函数
# ============================================================
function Test-CommandExists {
param([string]$Name)
try {
$cmd = Get-Command $Name -ErrorAction Stop
return $cmd.Source
} catch {
return $null
}
}
function Add-ToCurrentPath {
param([string]$LiteralPath)
if ([string]::IsNullOrWhiteSpace($LiteralPath)) {
return
}
$currentPaths = $env:Path -split ';' | ForEach-Object { $_.TrimEnd('\') }
$normalizedPath = $LiteralPath.TrimEnd('\')
if ($normalizedPath -in $currentPaths) {
return
}
if (-not (Test-Path $LiteralPath)) {
return
}
$env:Path = "$LiteralPath;$env:Path"
Write-Information "已临时将 $LiteralPath 加入当前会话 PATH"
}
function Get-UvInstallDir {
# uv-installer.ps1 的安装位置优先级
$candidates = @()
if ($env:XDG_BIN_HOME) {
$candidates += $env:XDG_BIN_HOME
}
if ($env:XDG_DATA_HOME) {
$candidates += (Join-Path $env:XDG_DATA_HOME "../bin")
}
$candidates += (Join-Path $HOME ".local\bin")
foreach ($candidate in $candidates) {
$resolved = $null
try {
$resolved = (Resolve-Path $candidate -ErrorAction SilentlyContinue).Path
} catch {
$resolved = $candidate
}
if ($resolved -and (Test-Path $resolved)) {
return $resolved
}
}
# 默认返回最后一个候选
return Join-Path $HOME ".local\bin"
}
function Invoke-Step {
param(
[Parameter(Mandatory)][string]$Title,
[Parameter(Mandatory)][scriptblock]$Action
)
Write-Information ""
Write-Information "========================================"
Write-Information " $Title"
Write-Information "========================================"
try {
& $Action
} catch {
Write-Information ""
Write-Information "失败:$_"
throw
}
}
# ============================================================
# 主流程
# ============================================================
Write-Information ""
Write-Information "========================================"
Write-Information " pptopic 一键安装程序"
Write-Information "========================================"
Write-Information ""
Write-Information "项目目录:$ProjectRoot"
if ($SkipPngquant) {
Write-Information "已指定 -SkipPngquant将跳过 pngquant 安装。"
}
if ($Force) {
Write-Information "已指定 -Force将强制重新安装 uv / pngquant。"
}
# 第一步:安装 uv
Invoke-Step -Title "步骤 1/5安装 uv" -Action {
$uvPath = Test-CommandExists -Name "uv"
if ($uvPath -and -not $Force) {
Write-Information "uv 已存在:$uvPath"
} else {
$installer = Join-Path $ProjectRoot "uv-installer.ps1"
if (-not (Test-Path $installer)) {
throw "找不到 uv 安装脚本:$installer"
}
Write-Information "正在运行 uv-installer.ps1..."
& $installer
}
# uv-installer.ps1 会修改注册表 PATH但当前会话不会立即生效需要手动加入
$uvInstallDir = Get-UvInstallDir
Add-ToCurrentPath -LiteralPath $uvInstallDir
# 再次验证
$uvPath = Test-CommandExists -Name "uv"
if (-not $uvPath) {
throw "uv 安装后仍无法在当前会话中找到,请尝试重启终端后重试。"
}
Write-Information "uv 路径:$uvPath"
}
# 第二步:安装 Python 3.13
Invoke-Step -Title "步骤 2/5安装 Python 3.13" -Action {
Write-Information "正在使用 uv 安装 Python 3.13..."
uv python install 3.13
Write-Information "Python 安装完成。"
}
# 第三步:安装 pngquant
if (-not $SkipPngquant) {
Invoke-Step -Title "步骤 3/5安装 pngquant" -Action {
$pngquantPath = Test-CommandExists -Name "pngquant"
if ($pngquantPath -and -not $Force) {
Write-Information "pngquant 已存在:$pngquantPath"
} else {
$installer = Join-Path $ProjectRoot "install-pngquant.ps1"
if (-not (Test-Path $installer)) {
throw "找不到 pngquant 安装脚本:$installer"
}
if ($Force) {
Write-Information "正在运行 install-pngquant.ps1 -Force..."
& $installer -Force
} else {
Write-Information "正在运行 install-pngquant.ps1..."
& $installer
}
}
# install-pngquant.ps1 内部已刷新当前会话 PATH这里再做一次保险
Add-ToCurrentPath -LiteralPath "$env:APPDATA\pngquant"
$pngquantPath = Test-CommandExists -Name "pngquant"
if (-not $pngquantPath) {
throw "pngquant 安装后仍无法在当前会话中找到,请尝试重启终端后重试。"
}
Write-Information "pngquant 路径:$pngquantPath"
}
} else {
Write-Information ""
Write-Information "========================================"
Write-Information " 步骤 3/5跳过 pngquant 安装"
Write-Information "========================================"
}
# 第四步:安装 pptopic
if (-not $SkipPptopicInstall) {
Invoke-Step -Title "步骤 4/5安装 pptopic" -Action {
$pyproject = Join-Path $ProjectRoot "pyproject.toml"
if (-not (Test-Path $pyproject)) {
throw "找不到 pyproject.toml$pyproject"
}
Write-Information "正在使用 uv tool install -e $ProjectRoot 安装 pptopic..."
uv tool install -e $ProjectRoot
Write-Information "pptopic 安装完成。"
}
} else {
Write-Information ""
Write-Information "========================================"
Write-Information " 步骤 4/5跳过 pptopic 安装"
Write-Information "========================================"
}
# 第五步:验证
Invoke-Step -Title "步骤 5/5验证安装" -Action {
Write-Information ""
Write-Information "--- uv 版本 ---"
uv --version
if (-not $SkipPngquant) {
Write-Information ""
Write-Information "--- pngquant 版本 ---"
pngquant --version
}
if (-not $SkipPptopicInstall) {
Write-Information ""
Write-Information "--- pptopic 版本 ---"
pptopic version
}
}
# ============================================================
# 完成
# ============================================================
Write-Information ""
Write-Information "========================================"
Write-Information " pptopic 一键安装完成!"
Write-Information "========================================"
Write-Information ""
if (-not $SkipPptopicInstall) {
Write-Information "你可以立即在当前窗口使用以下命令:"
Write-Information " pptopic export 你的文件.pptx"
Write-Information " pptopic export 你的文件.pptx --optimize --max-height 29999 -o result.png"
}
Write-Information ""
Write-Information "提示:本脚本已自动刷新当前 PowerShell 会话的 PATH无需重启终端。"
Write-Information "如果关闭此窗口后在新窗口中找不到命令,请重新运行一次 setup.bat。"
Write-Information ""

View File

@@ -33,6 +33,90 @@ param (
[Parameter(HelpMessage = "Print Help")]
[switch]$Help
)
function WebProxyFromUrl {
param([string]$ProxyUrl)
if ([string]::IsNullOrWhiteSpace($ProxyUrl)) {
return $null
}
try {
# Parse the proxy URL
$uri = [System.Uri]$ProxyUrl
# Create WebProxy instance
$webProxy = New-Object System.Net.WebProxy($uri)
# Set credentials if provided in URL
if (-not [string]::IsNullOrEmpty($uri.UserInfo)) {
$userInfo = $uri.UserInfo.Split(':')
$username = [System.Uri]::UnescapeDataString($userInfo[0])
$password = if ($null -eq $userInfo[1]) { "" } else { [System.Uri]::UnescapeDataString($userInfo[1]) }
$webProxy.Credentials = New-Object System.Net.NetworkCredential($username, $password)
}
return $webProxy
}
catch {
Write-Verbose("Failed to parse proxy URL '$ProxyUrl': $($_.Exception.Message)")
return $null
}
}
function WebProxyFromEnvironment {
$httpsProxy = [System.Environment]::GetEnvironmentVariable("HTTPS_PROXY")
$allProxy = [System.Environment]::GetEnvironmentVariable("ALL_PROXY")
$proxyUrl = if (-not [string]::IsNullOrWhiteSpace($httpsProxy)) { $httpsProxy } else { $allProxy }
$webProxy = WebProxyFromUrl -ProxyUrl $proxyUrl
return $webProxy
}
# Downloads a URL to a local file using HttpClient with:
# * a configurable per-request timeout (WebClient has none)
# * proxy support from HTTPS_PROXY / ALL_PROXY
# * bearer auth, attached ONLY when the target host is a trusted first-party
# (github.com / astral.sh). This prevents leaking UV_GITHUB_TOKEN to
# third-party mirrors configured in $ArtifactDownloadUrls.
function Invoke-HttpDownload {
param(
[Parameter(Mandatory = $true)][string]$Url,
[Parameter(Mandatory = $true)][string]$Destination,
[int]$TimeoutSec = 30,
[switch]$IncludeAuth
)
$handler = New-Object System.Net.Http.HttpClientHandler
$proxy = WebProxyFromEnvironment
if ($null -ne $proxy) {
$handler.Proxy = $proxy
$handler.UseProxy = $true
}
$client = New-Object System.Net.Http.HttpClient($handler)
$client.Timeout = [TimeSpan]::FromSeconds($TimeoutSec)
$client.DefaultRequestHeaders.Add("User-Agent", "PowerShell/uv-installer")
if ($IncludeAuth -and $auth_token -and ($Url -match 'github\.com|astral\.sh')) {
$client.DefaultRequestHeaders.Add("Authorization", "Bearer $auth_token")
}
try {
$response = $client.GetAsync($Url, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
if (-not $response.IsSuccessStatusCode) {
throw "HTTP $([int]$response.StatusCode) downloading $Url"
}
$stream = $response.Content.ReadAsStreamAsync().Result
$fileStream = [System.IO.File]::Create($Destination)
try {
$stream.CopyTo($fileStream)
} finally {
$fileStream.Dispose()
}
} finally {
$client.Dispose()
}
}
function Get-LatestVersion {
if ($env:UV_INSTALLER_VERSION) {
return $env:UV_INSTALLER_VERSION
@@ -41,10 +125,22 @@ function Get-LatestVersion {
$fallback_version = "0.11.20"
try {
$http = New-Object System.Net.Http.HttpClient
$http.Timeout = [TimeSpan]::FromSeconds(5)
$handler = New-Object System.Net.Http.HttpClientHandler
$proxy = WebProxyFromEnvironment
if ($null -ne $proxy) {
$handler.Proxy = $proxy
$handler.UseProxy = $true
}
$http = New-Object System.Net.Http.HttpClient($handler)
# Bumped from 5s -> 10s. The API call is small but in proxy/restricted
# networks 5s was almost always too tight and forced the fallback version.
$http.Timeout = [TimeSpan]::FromSeconds(10)
$http.DefaultRequestHeaders.Add("User-Agent", "PowerShell/uv-installer")
$http.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json")
if ($auth_token) {
$http.DefaultRequestHeaders.Add("Authorization", "Bearer $auth_token")
}
$response = $http.GetStringAsync("https://api.github.com/repos/astral-sh/uv/releases/latest").Result
$json = $response | ConvertFrom-Json
$version = $json.tag_name
@@ -60,6 +156,9 @@ function Get-LatestVersion {
}
$app_name = 'uv'
# NOTE: $auth_token must be defined *before* Get-LatestVersion / Invoke-HttpDownload
# are invoked, since both reference it.
$auth_token = $env:UV_GITHUB_TOKEN
$app_version = Get-LatestVersion
if ($env:UV_DOWNLOAD_URL) {
$ArtifactDownloadUrls = @($env:UV_DOWNLOAD_URL)
@@ -72,6 +171,10 @@ if ($env:UV_DOWNLOAD_URL) {
$installer_base_url = $env:UV_INSTALLER_GITHUB_BASE_URL
$ArtifactDownloadUrls = @("$installer_base_url/astral-sh/uv/releases/download/$app_version")
} else {
# Default download sources, ordered by priority.
# Mirror entries exist to work around GitHub connectivity issues in mainland China.
# If you are outside China or these mirrors become unavailable, you can safely
# delete the two mirror lines and rely on the official URLs (first + last).
$ArtifactDownloadUrls = @(
"https://releases.astral.sh/github/uv/releases/download/$app_version",
"https://mirror.ghproxy.com/https://github.com/astral-sh/uv/releases/download/$app_version",
@@ -80,8 +183,6 @@ if ($env:UV_DOWNLOAD_URL) {
)
}
$auth_token = $env:UV_GITHUB_TOKEN
$receipt = @"
{"binaries":["CARGO_DIST_BINS"],"binary_aliases":{},"cdylibs":["CARGO_DIST_DYLIBS"],"cstaticlibs":["CARGO_DIST_STATICLIBS"],"install_layout":"unspecified","install_prefix":"AXO_INSTALL_PREFIX","modify_path":true,"provider":{"source":"cargo-dist","version":"0.31.0"},"source":{"app_name":"uv","name":"uv","owner":"astral-sh","release_type":"github"},"version":"$app_version"}
"@
@@ -269,44 +370,6 @@ function Get-Arch() {
}
}
function WebProxyFromUrl {
param([string]$ProxyUrl)
if ([string]::IsNullOrWhiteSpace($ProxyUrl)) {
return $null
}
try {
# Parse the proxy URL
$uri = [System.Uri]$ProxyUrl
# Create WebProxy instance
$webProxy = New-Object System.Net.WebProxy($uri)
# Set credentials if provided in URL
if (-not [string]::IsNullOrEmpty($uri.UserInfo)) {
$userInfo = $uri.UserInfo.Split(':')
$username = [System.Uri]::UnescapeDataString($userInfo[0])
$password = if ($null -eq $userInfo[1]) { "" } else { [System.Uri]::UnescapeDataString($userInfo[1]) }
$webProxy.Credentials = New-Object System.Net.NetworkCredential($username, $password)
}
return $webProxy
}
catch {
Write-Verbose("Failed to parse proxy URL '$ProxyUrl': $($_.Exception.Message)")
return $null
}
}
function WebProxyFromEnvironment {
$httpsProxy = [System.Environment]::GetEnvironmentVariable("HTTPS_PROXY")
$allProxy = [System.Environment]::GetEnvironmentVariable("ALL_PROXY")
$proxyUrl = if (-not [string]::IsNullOrWhiteSpace($httpsProxy)) { $httpsProxy } else { $allProxy }
$webProxy = WebProxyFromUrl -ProxyUrl $proxyUrl
return $webProxy
}
function Download($download_url, $platforms, $arch) {
# Lookup what we expect this platform to look like
$info = $platforms[$arch]
@@ -324,15 +387,7 @@ function Download($download_url, $platforms, $arch) {
$url = "$download_url/$artifact_name"
Write-Verbose " from $url"
Write-Verbose " to $dir_path"
$wc = New-Object Net.Webclient
$proxy = WebProxyFromEnvironment
if ($null -ne $proxy) {
$wc.Proxy = $proxy
}
if ($auth_token) {
$wc.Headers["Authorization"] = "Bearer $auth_token"
}
$wc.downloadFile($url, $dir_path)
Invoke-HttpDownload -Url $url -Destination $dir_path -TimeoutSec 30 -IncludeAuth
Write-Verbose "Unpacking to $tmp"
@@ -377,7 +432,7 @@ function Download($download_url, $platforms, $arch) {
$updater_url = "$download_url/$updater_id"
$out_name = "$tmp\uv-update.exe"
$wc.downloadFile($updater_url, $out_name)
Invoke-HttpDownload -Url $updater_url -Destination $out_name -TimeoutSec 30 -IncludeAuth
$bin_paths += $out_name
}
@@ -549,7 +604,7 @@ function Invoke-Installer($artifacts, $platforms) {
# .NET's APIs which actually do what you tell them (also apparently utf8NoBOM is the
# default in newer .NETs but I'd rather not rely on that at this point).
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
[IO.File]::WriteAllLines("$receipt_home/uv-receipt.json", "$receipt", $Utf8NoBomEncoding)
[IO.File]::WriteAllLines("$receipt_home\uv-receipt.json", "$receipt", $Utf8NoBomEncoding)
}
# Respect the environment, but CLI takes precedence