diff --git a/.github/workflows/release-msi.nu b/.github/workflows/release-msi.nu
new file mode 100755
index 000000000..6bb4b525a
--- /dev/null
+++ b/.github/workflows/release-msi.nu
@@ -0,0 +1,62 @@
+#!/usr/bin/env nu
+
+# Created: 2025/05/21 19:05:20
+# Description:
+# A script to build Windows MSI packages for NuShell. Need wix 6.0 to be installed.
+# The script will download the specified NuShell release, extract it, and create an MSI package.
+# Can be run locally or in GitHub Actions.
+# To run this script locally:
+# load-env { TARGET: 'x86_64-pc-windows-msvc' REF: '0.103.0' GITHUB_REPOSITORY: 'nushell/nushell' }
+# nu .github/workflows/release-msi.nu
+
+def build-msi [] {
+ let target = $env.TARGET
+ # We should read the version from the environment variable first
+ # As we may build the MSI package for a specific version not the latest one
+ let version = $env.MSI_VERSION
+ let arch = if $nu.os-info.arch =~ 'x86_64' { 'x64' } else { 'arm64' }
+
+ print $'Building msi package for (ansi g)($target)(ansi reset) with version (ansi g)($version)(ansi reset) from tag (ansi g)($env.REF)(ansi reset)...'
+ fetch-nu-pkg
+ # Create extra Windows msi release package if dotnet and wix are available
+ let installed = [dotnet wix] | all { (which $in | length) > 0 }
+ if $installed and (wix --version | split row . | first | into int) >= 6 {
+
+ print $'(char nl)Start creating Windows msi package with the following contents...'
+ cd wix; hr-line
+ cp nu/README.txt .
+ ls -f nu/* | print
+ ./nu/nu.exe -c $'NU_RELEASE_VERSION=($version) dotnet build -c Release -p:Platform=($arch)'
+ glob **/*.msi | print
+ # Workaround for https://github.com/softprops/action-gh-release/issues/280
+ let wixRelease = (glob **/*.msi | where $it =~ bin | get 0 | str replace --all '\' '/')
+ let msi = $'($wixRelease | path dirname)/nu-($version)-($target).msi'
+ mv $wixRelease $msi
+ print $'MSI archive: ---> ($msi)';
+ # Run only in GitHub Actions
+ if ($env.GITHUB_ACTIONS? | default false | into bool) {
+ echo $"msi=($msi)(char nl)" o>> $env.GITHUB_OUTPUT
+ }
+ }
+}
+
+def fetch-nu-pkg [] {
+ mkdir wix/nu
+ # See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
+ gh release download $env.REF --repo nushell/nushell --pattern $'*-($env.TARGET).zip' --dir wix/nu
+ cd wix/nu
+ let pkg = ls *.zip | get name.0
+ unzip $pkg
+ rm $pkg
+ ls | print
+}
+
+# Print a horizontal line marker
+def 'hr-line' [
+ --blank-line(-b)
+] {
+ print $'(ansi g)---------------------------------------------------------------------------->(ansi reset)'
+ if $blank_line { char nl }
+}
+
+alias main = build-msi
diff --git a/.github/workflows/release-msi.yml b/.github/workflows/release-msi.yml
new file mode 100644
index 000000000..67e2926dd
--- /dev/null
+++ b/.github/workflows/release-msi.yml
@@ -0,0 +1,103 @@
+#
+# REF:
+# 1. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude
+#
+name: Build Windows MSI
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag:
+ required: true
+ description: 'Tag of nushell/nushell'
+ release:
+ description: 'Release Tag of Integrations'
+
+permissions:
+ contents: write
+ packages: write
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ release:
+ name: Nu
+
+ strategy:
+ fail-fast: false
+ matrix:
+ target:
+ - x86_64-pc-windows-msvc
+ - aarch64-pc-windows-msvc
+ extra: ['bin']
+
+ include:
+ - target: x86_64-pc-windows-msvc
+ os: windows-latest
+ - target: aarch64-pc-windows-msvc
+ os: windows-11-arm
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Wix Toolset 6 for Windows
+ shell: pwsh
+ if: ${{ startsWith(matrix.os, 'windows') }}
+ run: |
+ dotnet tool install --global wix --version 6.0.0
+ dotnet workload install wix
+ $wixPath = "$env:USERPROFILE\.dotnet\tools"
+ echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+ $env:PATH = "$wixPath;$env:PATH"
+ wix --version
+
+ - name: Setup Nushell
+ uses: hustcer/setup-nu@v3
+ with:
+ version: 0.105.1
+
+ - name: Release MSI Packages
+ id: nu
+ run: nu .github/workflows/release-msi.nu
+ env:
+ OS: ${{ matrix.os }}
+ REF: ${{ inputs.tag }}
+ TARGET: ${{ matrix.target }}
+ MSI_VERSION: ${{ inputs.tag }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ # REF: https://github.com/marketplace/actions/gh-release
+ - name: Publish Archive
+ uses: softprops/action-gh-release@v2.0.5
+ with:
+ tag_name: ${{ inputs.release }}
+ files: ${{ steps.nu.outputs.msi }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ sha256sum:
+ needs: release
+ name: Create Sha256sum
+ runs-on: ubuntu-latest
+ steps:
+ - name: Download Release Archives
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: >-
+ gh release download ${{ inputs.release }}
+ --repo ${{ github.repository }}
+ --pattern '*'
+ --dir release
+ - name: Create Checksums
+ run: cd release && rm -f SHA256SUMS && shasum -a 256 * > ../SHA256SUMS
+ - name: Publish Checksums
+ uses: softprops/action-gh-release@v2.0.5
+ with:
+ files: SHA256SUMS
+ tag_name: ${{ inputs.release }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Justfile b/Justfile
index f7eaffb78..fdf41d57d 100644
--- a/Justfile
+++ b/Justfile
@@ -24,7 +24,7 @@ set positional-arguments := true
# Used to handle the path separator issue
NU_DISTRO_PATH := parent_directory(justfile())
-NU_DIR := parent_directory(`(which nu).path.0`)
+NU_DIR := parent_directory(`$nu.current-exe`)
_query_plugin := if os_family() == 'windows' { 'nu_plugin_query.exe' } else { 'nu_plugin_query' }
# To pass arguments to a dependency, put the dependency
diff --git a/wix/License.rtf b/wix/License.rtf
new file mode 100644
index 000000000..9e78b15ae
Binary files /dev/null and b/wix/License.rtf differ
diff --git a/wix/MSI.md b/wix/MSI.md
new file mode 100644
index 000000000..3dff44640
--- /dev/null
+++ b/wix/MSI.md
@@ -0,0 +1,90 @@
+## 使用说明
+
+1. 准备工作 :
+
+ - 确保已安装 Wix Toolset 6.0: `dotnet tool install --global wix --version 6.0.0`
+
+2. 构建 MSI :
+
+ - 对于 x64 架构: `dotnet build -c Release -p:Platform=x64`
+ - 对于 ARM64 架构: `dotnet build -c Release -p:Platform=arm64`
+
+3. 安装选项 :
+
+ - 用户范围安装: `winget install Nushell.Nushell --scope user`
+ - 机器范围安装: `winget install Nushell.Nushell --scope machine` (需要管理员权限)
+
+ # For Per-User Installation
+ `msiexec /i bin\x64\Release\nu-x64.msi MSIINSTALLPERUSER=1`
+
+ # For Per-Machine Installation (Requires Admin Privileges)
+ `msiexec /i bin\x64\Release\nu-x64.msi ALLUSERS=1`
+
+ # MSI Install with Logs
+ `msiexec /i bin\x64\Release\nu-x64.msi ALLUSERS=1 /l*v log.txt`
+
+## 特性说明
+
+1. 双重安装范围 :支持用户和机器范围安装
+2. `PATH` 环境变量 :自动将安装目录添加到系统 `PATH`
+3. 升级保留 :升级时保留原安装路径
+4. 多架构支持 :支持 `x86_64` 和 `ARM64` 架构
+5. 系统兼容性 :兼容 Windows 7/10/11
+
+## User-Facing Changes
+
+- Nushell should be possible to be installed via winget with both user and machine scope and The default should be user scope
+ - User scope install by winget: `winget install Nushell.Nushell`
+ - User scope install by msiexec: `msiexec /i nu-0.104.1-x86_64-pc-windows-msvc.msi /quiet /qn`
+ - Machine scope install by winget: `winget install Nushell.Nushell --override 'ALLUSERS=1'`
+ - Machine scope install by msiexec: `msiexec /i nu-0.104.1-x86_64-pc-windows-msvc.msi ALLUSERS=1`
+ - Note that `--scope` flag for `winget install` does not work use `--override` instead
+ - Default user scope install dir: `$'($nu.home-path)\AppData\Local\Programs\nu\'`
+ - Default machine scope install dir: `C:\Program Files\nu\`
+- When a standard user runs the installer and selects "Install for everyone" (per-machine installation), Windows will automatically trigger a UAC prompt, the user can enter admin credentials and the installation will proceed with elevated privileges
+
+## Test Case
+
+- 为当前用户安装 Nushell:
+ - 检查默认安装路径是否正确
+ - 检查静默安装: `msiexec /i $pkg MSIINSTALLPERUSER=1 /quiet /qn` 是否正常
+ - 检查静默安装: `winget install --manifest manifests\n\Nushell\Nushell\0.104.1 --scope user` 是否正常
+ - 安装过程中是否不会出现 UAC prompt
+ - 检查安装完成后环境变量是否正确添加
+ - 检查注册表变量是否正确设置
+ - 检查 Window Terminal 配置文件是否添加
+ - 如果没有选择 Window Terminal 配置 Feature 则不会被安装
+ - 如果是升级安装是否保持原来的安装路径
+ - 卸载 Nushell 并检查文件/环境变量/注册表/Windows Terminal 配置文件是否被清理掉
+
+- 为所有用户安装 Nushell
+ - 如果是普通用户安装过程中是否会出现 UAC prompt
+ - 检查默认安装路径是否正确
+ - 检查静默安装: `winget install --manifest manifests\n\Nushell\Nushell\0.104.1 --scope machine` 是否正常
+ - 检查是否允许用户选择自定义安装路径
+ - 选择自定义安装路径是否能成功安装
+ - 如果是升级安装是否保持原来的安装路径
+ - 检查安装完成后环境变量是否正确添加
+ - 检查注册表变量是否正确设置
+ - 检查 Window Terminal 配置文件是否添加
+ - 如果没有选择 Window Terminal 配置 Feature 则不会被安装
+ - 卸载 Nushell 并检查文件/环境变量/注册表/Windows Terminal 配置文件是否被清理掉
+
+## 当前已知问题
+
+- [x] 卸载的时候环境变量没有被清理掉;
+- 支持通过 INSTALLDIR 属性指定安装路径
+- `winget install --scope machine` 不支持
+- [x] `winget install --override 'ALLUSERS=1'` 可以正常在全局范围安装应用
+- [x] `msiexec /i $pkg MSIINSTALLPERUSER=1 /quiet /qn` 静默安装路径异常
+- [x] 为所有用户安装的时候没有正确安装 WindowsTerminalProfileFeature
+- [x] 为所有用户安装的时候默认安装路径是 C:\Program Files (x86)\nu 而不是 C:\Program Files\nu;
+- [x] 安装 Scope 选择切换到为所有用户安装时默认选中的路径是 C:\Program Files (x86)\nu 而不是 C:\Program Files\nu;
+
+## REF
+
+- https://docs.firegiant.com/quick-start/
+- https://docs.firegiant.com/wix/schema/wxs/package/
+- https://learn.microsoft.com/en-ca/windows/win32/msi/formatted
+- https://learn.microsoft.com/en-us/windows/win32/msi/single-package-authoring
+- https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec
diff --git a/wix/README.txt b/wix/README.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/wix/main.wixproj b/wix/main.wixproj
new file mode 100644
index 000000000..da43a12bc
--- /dev/null
+++ b/wix/main.wixproj
@@ -0,0 +1,36 @@
+
+
+ Package
+ nu-$(Platform)
+ 82D756D2-19FA-4F09-B10F-64942E89F364
+
+ SourceDir=$(MSBuildProjectDirectory)\nu;
+
+ true
+ ICE80
+
+
+
+ x64
+ $(DefineConstants)
+
+
+
+ arm64
+ $(DefineConstants)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wix/main.wxs b/wix/main.wxs
new file mode 100644
index 000000000..bb631dd58
--- /dev/null
+++ b/wix/main.wxs
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wix/nu.ico b/wix/nu.ico
new file mode 100644
index 000000000..b74f2a336
Binary files /dev/null and b/wix/nu.ico differ
diff --git a/wix/prepare-winget.nu b/wix/prepare-winget.nu
new file mode 100644
index 000000000..579d36430
--- /dev/null
+++ b/wix/prepare-winget.nu
@@ -0,0 +1,57 @@
+const FROM_REF = '0.105.1'
+const PREV_VER = '0.105.0'
+const LAST_VER = '0.105.1'
+const OSS_DEST = 'oss://terminus-new-trantor/open-tools/misc'
+const OSS_ENDPOINT = 'https://terminus-new-trantor.oss-cn-hangzhou.aliyuncs.com/open-tools/misc'
+
+export def prepare [] {
+
+ rm -rf obj/ bin/
+ nu -c $'NU_RELEASE_VERSION=($PREV_VER) dotnet build -c Release -p:Platform=x64'
+ ossutil cp -f bin\x64\Release\nu-x64.msi $'($OSS_DEST)/nu-($PREV_VER)-x86_64-pc-windows-msvc.msi'
+
+ rm -rf obj/ bin/
+ nu -c $'NU_RELEASE_VERSION=($LAST_VER) dotnet build -c Release -p:Platform=x64'
+ ossutil cp -f bin\x64\Release\nu-x64.msi $'($OSS_DEST)/nu-($LAST_VER)-x86_64-pc-windows-msvc.msi'
+}
+
+export def upgrade-nu-by-winget [
+ --no-hash(-n),
+] {
+
+ let prevUrl = $'($OSS_ENDPOINT)/nu-($PREV_VER)-x86_64-pc-windows-msvc.msi'
+ let lastUrl = $'($OSS_ENDPOINT)/nu-($LAST_VER)-x86_64-pc-windows-msvc.msi'
+ # sudo winget settings --enable InstallerHashOverride
+ winget uninstall nushell | complete
+ if ($no_hash) {
+ winget install --manifest manifests\n\Nushell\Nushell\($PREV_VER) --ignore-security-hash --silent
+ winget upgrade --manifest manifests\n\Nushell\Nushell\($LAST_VER) --ignore-security-hash --silent
+ return
+ }
+ komac update Nushell.Nushell --dry-run -v $PREV_VER -u $prevUrl --output .
+ komac update Nushell.Nushell --dry-run -v $LAST_VER -u $lastUrl --output .
+ winget install --manifest manifests\n\Nushell\Nushell\($PREV_VER) --ignore-security-hash --silent
+ winget upgrade --manifest manifests\n\Nushell\Nushell\($LAST_VER) --ignore-security-hash --silent
+}
+
+def rebuild [--fetch(-f)] {
+ if $fetch { fetch-nu-pkg }
+ rm -rf obj/ bin/
+ nu -c $'NU_RELEASE_VERSION=($LAST_VER) dotnet build -c Release -p:Platform=x64'
+ ossutil cp -f bin\x64\Release\nu-x64.msi $'($OSS_DEST)/nu-($LAST_VER)-x86_64-pc-windows-msvc.msi'
+ winget uninstall nushell | complete
+ # msiexec /i bin\x64\Release\nu-x64.msi ALLUSERS=1
+ winget install --manifest manifests\n\Nushell\Nushell\($LAST_VER) --ignore-security-hash --silent --scope machine
+}
+
+def fetch-nu-pkg [] {
+ mkdir nu
+ gh release download $FROM_REF --repo nushell/nushell --pattern $'*-x86_64-*.zip' --dir nu
+ cd nu
+ let pkg = ls *.zip | get name.0
+ unzip $pkg
+ rm $pkg
+ ls | print
+}
+
+alias main = rebuild
diff --git a/wix/windows-terminal-profile.json b/wix/windows-terminal-profile.json
new file mode 100644
index 000000000..59866bb52
--- /dev/null
+++ b/wix/windows-terminal-profile.json
@@ -0,0 +1,11 @@
+{
+ "profiles": [
+ {
+ "guid": "{47302f9c-1ac4-566c-aa3e-8cf29889d6ab}",
+ "name": "Nushell",
+ "commandline": "nu.exe",
+ "icon": "nu.ico",
+ "startingDirectory": "%USERPROFILE%"
+ }
+ ]
+}