@mobius-os/mobius 0.3.38 → 0.3.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.ps1 DELETED
@@ -1,265 +0,0 @@
1
- #requires -Version 5.1
2
- <#
3
- .SYNOPSIS
4
- Mobius TUI Windows 便携安装 (自带 portable Node, 无需 admin, 不依赖 npm.ps1)
5
- .DESCRIPTION
6
- 下载 portable Node win-x64 zip 解压到 ~/.mobius/node-portable/ → 用 node.exe 直跑 npm-cli.js
7
- 装 @mobius-os/mobius 到 ~/.mobius/npm-global/ → 自建 mobius.cmd 启动器(绝对路径指向便携 node)
8
- → 加 ~/.mobius/bin 到用户 PATH (无需 admin)。
9
- npm 安装和 mobius 启动均直接使用 node.exe/tsx;安装完成后同时注册两个
10
- Explorer 右键入口:“在 Mobius 中打开”(文件夹本身 + 文件夹空白处)。
11
- .EXAMPLE
12
- irm https://serve.nutshellai.cn/publish/auto/mobiustui/install-v15.ps1 | iex
13
- #>
14
- $ErrorActionPreference = 'Stop'
15
- $ProgressPreference = 'SilentlyContinue'
16
-
17
- function Step($m){ Write-Host "[*] $m" -ForegroundColor Cyan }
18
- function Ok($m) { Write-Host "[OK] $m" -ForegroundColor Green }
19
- function Warn($m){ Write-Host "[!] $m" -ForegroundColor Yellow }
20
- function Err($m) { Write-Host "[X] $m" -ForegroundColor Red }
21
- function Fail($m) { throw $m }
22
-
23
- function Invoke-MobiusInstall {
24
- Write-Host "=== Mobius TUI Windows 便携安装 (无需 admin) ===" -ForegroundColor White
25
-
26
- $MHOME = Join-Path $env:USERPROFILE ".mobius"
27
- $NODE_DIR = Join-Path $MHOME "node-portable"
28
- $GLOBAL_DIR = Join-Path $MHOME "npm-global"
29
- $NODE_VER = "v24.18.1"
30
- $ZIP_URL = "https://serve.nutshellai.cn/publish/auto/mobius-tui/node/node-$NODE_VER-win-x64.zip"
31
- $NODE_SHA256 = "ec56b84a7551893ab2324ebdfdc4ab974a63b4781162600b68a1293cc3e53765" # node v24.18.1 win-x64
32
- $nodeExe = Join-Path $NODE_DIR "node.exe"
33
- $npmCli = Join-Path $NODE_DIR "node_modules\npm\bin\npm-cli.js"
34
-
35
- New-Item -ItemType Directory -Force -Path $MHOME | Out-Null
36
-
37
- # --- 1. portable Node ---
38
- if (Test-Path $nodeExe) {
39
- Ok "便携 Node 已存在: $NODE_DIR"
40
- } else {
41
- Step "下载便携 Node $NODE_VER (~37MB), 可能需 1-2 分钟..."
42
- $zip = Join-Path $MHOME "node.zip"
43
- Invoke-WebRequest -Uri $ZIP_URL -OutFile $zip
44
- Step "校验 sha256..."
45
- $actual = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
46
- if ($actual -ne $NODE_SHA256.ToLower()) { Fail "Node zip sha256 不匹配 (下载损坏? 期望 $NODE_SHA256 实际 $actual)" }
47
- Step "解压..."
48
- $tmp = Join-Path $MHOME "_extract"
49
- if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force }
50
- Expand-Archive -Path $zip -DestinationPath $tmp -Force
51
- $inner = Join-Path $tmp "node-$NODE_VER-win-x64"
52
- if (-not (Test-Path (Join-Path $inner "node.exe"))) { Fail "解压后未找到 node.exe" }
53
- if (Test-Path $NODE_DIR) { Remove-Item $NODE_DIR -Recurse -Force }
54
- Move-Item $inner $NODE_DIR -Force
55
- Remove-Item $tmp -Recurse -Force
56
- Remove-Item $zip -Force
57
- Ok "便携 Node 就绪: $NODE_DIR"
58
- }
59
-
60
- # --- 2. npm 装 @mobius-os/mobius (node.exe 直跑 npm-cli.js, 绕过 npm.ps1 的 ExecutionPolicy 拦截) ---
61
- Step "npm 安装 @mobius-os/mobius@latest (本地 install 到便携目录, 官方 registry)..."
62
- New-Item -ItemType Directory -Force -Path $GLOBAL_DIR | Out-Null
63
- $oldNodeModules = Join-Path $GLOBAL_DIR "node_modules"
64
- $oldPackageJson = Join-Path $GLOBAL_DIR "package.json"
65
- for ($attempt = 1; $attempt -le 3 -and (Test-Path $oldNodeModules); $attempt++) {
66
- Remove-Item $oldNodeModules -Recurse -Force -ErrorAction SilentlyContinue
67
- if (Test-Path $oldNodeModules) { Start-Sleep -Milliseconds (300 * $attempt) }
68
- }
69
- if (Test-Path $oldNodeModules) {
70
- Warn "请关闭正在运行的 mobius 终端后重试: $oldNodeModules"
71
- Fail "无法清理旧 node_modules,可能仍有 Mobius/Node 进程占用文件"
72
- }
73
- Remove-Item $oldPackageJson -Force -ErrorAction SilentlyContinue
74
- $npmLog = Join-Path $MHOME "npm-install.log"
75
- $npmStdout = Join-Path $MHOME "npm-install.stdout.log"
76
- $npmStderr = Join-Path $MHOME "npm-install.stderr.log"
77
- $previousPath = $env:Path
78
- # npm lifecycle scripts use cmd.exe and resolve `node` from PATH.
79
- $env:Path = "$NODE_DIR;$env:Path"
80
- Push-Location $GLOBAL_DIR
81
- $previousErrorActionPreference = $ErrorActionPreference
82
- $ErrorActionPreference = "Continue"
83
- try {
84
- # PowerShell 5.1 会把 npm 的 stderr warning 包装成 NativeCommandError。
85
- # 合并输出并只依据原生进程退出码判定成败,避免 warning 提前终止脚本。
86
- & $nodeExe $npmCli init -y 2>&1 | Out-Null
87
- $initExitCode = $LASTEXITCODE
88
- if ($initExitCode -ne 0) { throw "npm 初始化失败 (退出码 $initExitCode)" }
89
-
90
- # npm 11 默认拦截 esbuild postinstall,安装前明确允许该脚本。旧 npm 会忽略该字段。
91
- & $nodeExe $npmCli pkg set "allowScripts.esbuild=true" --json 2>&1 | Out-Null
92
- $configExitCode = $LASTEXITCODE
93
- if ($configExitCode -ne 0) { throw "配置 esbuild 安装脚本授权失败 (退出码 $configExitCode)" }
94
-
95
- function Invoke-NpmInstallAttempt([string]$registry, [int]$timeoutSeconds) {
96
- Remove-Item $npmStdout, $npmStderr -Force -ErrorAction SilentlyContinue
97
- # Do not call System.Diagnostics.Process instance methods here.
98
- # Windows PowerShell ConstrainedLanguage blocks those methods even
99
- # though Start-Process itself is allowed. A tiny cmd wrapper records
100
- # %ERRORLEVEL%; Wait-Process and taskkill remain CLM-safe.
101
- $attemptCmd = Join-Path $GLOBAL_DIR 'npm-install-attempt.cmd'
102
- $exitCodeFile = Join-Path $GLOBAL_DIR 'npm-install.exitcode'
103
- Remove-Item $exitCodeFile -Force -ErrorAction SilentlyContinue
104
- $attemptLines = @(
105
- '@echo off',
106
- 'setlocal',
107
- ('set "PATH={0};%PATH%"' -f $NODE_DIR),
108
- ('"{0}" "{1}" install "@mobius-os/mobius@latest" --registry "{2}" --loglevel warn >"{3}" 2>"{4}"' -f $nodeExe, $npmCli, $registry, $npmStdout, $npmStderr),
109
- 'set "EXIT_CODE=%ERRORLEVEL%"',
110
- ('>"{0}" echo %EXIT_CODE%' -f $exitCodeFile),
111
- 'exit /b %EXIT_CODE%'
112
- )
113
- Set-Content -Path $attemptCmd -Value $attemptLines -Encoding ASCII
114
- $proc = Start-Process -FilePath $env:ComSpec -ArgumentList @('/d', '/s', '/c', ('"{0}"' -f $attemptCmd)) `
115
- -WorkingDirectory $GLOBAL_DIR -PassThru -WindowStyle Hidden
116
- Wait-Process -Id $proc.Id -Timeout $timeoutSeconds -ErrorAction SilentlyContinue | Out-Null
117
- # Allow cmd a short moment to flush the marker after it exits, without
118
- # invoking any restricted Process instance methods.
119
- for ($markerAttempt = 1; $markerAttempt -le 20 -and -not (Test-Path $exitCodeFile); $markerAttempt++) {
120
- Start-Sleep -Milliseconds 100
121
- }
122
- if (-not (Test-Path $exitCodeFile)) {
123
- Warn "npm 官方源安装超过 $timeoutSeconds 秒,正在终止并切换镜像源..."
124
- & $env:ComSpec /d /s /c "taskkill /PID $($proc.Id) /T /F" 2>&1 | Out-Null
125
- Start-Sleep -Milliseconds 300
126
- Remove-Item $attemptCmd -Force -ErrorAction SilentlyContinue
127
- return @{ TimedOut = $true; ExitCode = $null }
128
- }
129
- $exitText = Get-Content -Path $exitCodeFile -Raw -ErrorAction SilentlyContinue
130
- $exitCode = $exitText -as [int]
131
- if ($null -eq $exitCode) { $exitCode = 1 }
132
- Remove-Item $attemptCmd, $exitCodeFile -Force -ErrorAction SilentlyContinue
133
- return @{ TimedOut = $false; ExitCode = $exitCode }
134
- }
135
-
136
- function Save-NpmAttemptLog([string]$label) {
137
- Add-Content -Path $npmLog -Value "`n===== $label ====="
138
- if (Test-Path $npmStdout) { Get-Content $npmStdout | Add-Content -Path $npmLog }
139
- if (Test-Path $npmStderr) { Get-Content $npmStderr | Add-Content -Path $npmLog }
140
- }
141
-
142
- Remove-Item $npmLog -Force -ErrorAction SilentlyContinue
143
- $official = Invoke-NpmInstallAttempt "https://registry.npmjs.org/" 10
144
- Save-NpmAttemptLog "official registry.npmjs.org"
145
- $npmSucceeded = (-not $official.TimedOut -and $official.ExitCode -eq 0)
146
- if (-not $npmSucceeded) {
147
- if ($official.TimedOut) {
148
- Warn "官方源超时,切换 npmmirror 镜像源重试..."
149
- } else {
150
- Warn "官方源安装失败 (退出码 $($official.ExitCode)),切换 npmmirror 镜像源重试..."
151
- }
152
- # A timed-out npm process can leave a partial tree behind; remove only
153
- # the package tree and keep the user-level install directory intact.
154
- if (Test-Path $oldNodeModules) { Remove-Item $oldNodeModules -Recurse -Force -ErrorAction SilentlyContinue }
155
- $mirror = Invoke-NpmInstallAttempt "https://registry.npmmirror.com/" 120
156
- Save-NpmAttemptLog "mirror registry.npmmirror.com"
157
- $npmSucceeded = (-not $mirror.TimedOut -and $mirror.ExitCode -eq 0)
158
- }
159
- if (-not $npmSucceeded) {
160
- Err "npm 安装失败"
161
- if (Test-Path $npmLog) {
162
- Write-Host "--- npm 最近日志: $npmLog ---" -ForegroundColor Yellow
163
- Get-Content $npmLog -Tail 120
164
- Write-Host "--- npm 日志结束 ---" -ForegroundColor Yellow
165
- }
166
- Fail "npm 安装失败,完整日志: $npmLog"
167
- }
168
- } finally {
169
- $ErrorActionPreference = $previousErrorActionPreference
170
- $env:Path = $previousPath
171
- Pop-Location
172
- }
173
- $tsxCli = Join-Path $GLOBAL_DIR "node_modules\tsx\dist\cli.mjs"
174
- $esbuildExe = Join-Path $GLOBAL_DIR "node_modules\@esbuild\win32-x64\esbuild.exe"
175
- if (-not (Test-Path $tsxCli)) { Fail "npm 包缺少 tsx 运行时依赖,日志: $npmLog" }
176
- if (-not (Test-Path $esbuildExe)) { Fail "esbuild Windows 二进制未正确安装,日志: $npmLog" }
177
- Ok "mobius 装到: $GLOBAL_DIR"
178
-
179
- # --- 3. mobius 启动器 (.cmd batch, 用绝对路径指向便携 node, 不依赖系统 PATH/ExecutionPolicy) ---
180
- $binDir = Join-Path $MHOME "bin"
181
- $entryJs = Join-Path $GLOBAL_DIR "node_modules\@mobius-os\mobius\bin\mobius-tui.js"
182
- if (-not (Test-Path $entryJs)) { Fail "未找到 mobius 入口: $entryJs" }
183
- New-Item -ItemType Directory -Force -Path $binDir | Out-Null
184
- $mainTsx = Join-Path $GLOBAL_DIR "node_modules\@mobius-os\mobius\src\main.tsx"
185
- $mobiusCmd = Join-Path $binDir "mobius.cmd"
186
- @"
187
- @echo off
188
- "$nodeExe" "$tsxCli" "$mainTsx" %*
189
- "@ | Set-Content -Path $mobiusCmd -Encoding ASCII
190
- Ok "启动器: $mobiusCmd"
191
-
192
- # --- 4. 用户 PATH (无需 admin) ---
193
- $userPath = (Get-ItemProperty -Path 'HKCU:\Environment' -Name Path -ErrorAction SilentlyContinue).Path
194
- if ($userPath -notlike "*$binDir*") {
195
- $newPath = if ($userPath) { "$userPath;$binDir" } else { $binDir }
196
- if (Test-Path 'HKCU:\Environment') {
197
- Set-ItemProperty -Path 'HKCU:\Environment' -Name Path -Value $newPath
198
- } else {
199
- New-Item -Path 'HKCU:\Environment' -Force | Out-Null
200
- New-ItemProperty -Path 'HKCU:\Environment' -Name Path -Value $newPath -PropertyType ExpandString -Force | Out-Null
201
- }
202
- $env:Path += ";$binDir"
203
- Ok "已加入用户 PATH: $binDir (重开 PowerShell 生效)"
204
- } else {
205
- Ok "PATH 已含: $binDir"
206
- }
207
-
208
- # --- 5. Explorer 右键菜单 (当前用户,无需 admin) ----------------------------
209
- # 两个入口必须同时注册:
210
- # Directory\shell\Mobius = 右键文件夹本身,目标参数 %1
211
- # Directory\Background\shell\Mobius = 右键文件夹空白处,目标参数 %V
212
- # 使用 .cmd 辅助启动器,避免右键动作依赖 PowerShell ExecutionPolicy。
213
- $openHereCmd = Join-Path $binDir "mobius-open-here.cmd"
214
- $openHereLines = @(
215
- '@echo off',
216
- 'setlocal',
217
- 'set "MOBIUS_TARGET=%~1"',
218
- 'if not defined MOBIUS_TARGET set "MOBIUS_TARGET=%CD%"',
219
- 'cd /d "%MOBIUS_TARGET%"',
220
- 'call "%~dp0mobius.cmd"',
221
- 'endlocal'
222
- )
223
- Set-Content -Path $openHereCmd -Value $openHereLines -Encoding ASCII
224
-
225
- $classes = "HKCU:\Software\Classes"
226
- $folderMenu = Join-Path $classes "Directory\shell\Mobius"
227
- $folderCommand = Join-Path $folderMenu "command"
228
- $backgroundMenu = Join-Path $classes "Directory\Background\shell\Mobius"
229
- $backgroundCommand = Join-Path $backgroundMenu "command"
230
-
231
- foreach ($key in @($folderCommand, $backgroundCommand)) {
232
- New-Item -Path $key -Force | Out-Null
233
- }
234
-
235
- foreach ($menu in @($folderMenu, $backgroundMenu)) {
236
- Set-ItemProperty -Path $menu -Name "MUIVerb" -Value "在 Mobius 中打开"
237
- Set-ItemProperty -Path $menu -Name "Icon" -Value "$env:SystemRoot\System32\cmd.exe,0"
238
- }
239
-
240
- # Explorer command quoting: cmd /c ""helper.cmd" "%1"".
241
- $folderCommandValue = '"{0}" /d /s /c ""{1}" "%1""' -f $env:ComSpec, $openHereCmd
242
- $backgroundCommandValue = '"{0}" /d /s /c ""{1}" "%V""' -f $env:ComSpec, $openHereCmd
243
- Set-Item -Path $folderCommand -Value $folderCommandValue
244
- Set-Item -Path $backgroundCommand -Value $backgroundCommandValue
245
- Ok "右键菜单已添加: 在 Mobius 中打开 (文件夹 + 空白处)"
246
-
247
- Write-Host ""
248
- Write-Host "=== 完成! 重开 PowerShell 后运行: mobius ===" -ForegroundColor Green
249
- Write-Host "(或直接运行: $mobiusCmd)" -ForegroundColor Gray
250
- Write-Host "右键文件夹或文件夹空白处,可选择: 在 Mobius 中打开" -ForegroundColor Gray
251
- }
252
-
253
- try {
254
- Invoke-MobiusInstall
255
- } catch {
256
- $errorLog = Join-Path $env:TEMP "mobius-install-v15-error.log"
257
- $errorText = $_ | Format-List * -Force | Out-String
258
- $errorText | Set-Content -Path $errorLog -Encoding UTF8
259
- Write-Host ""
260
- Err "Mobius 安装失败: $($_.Exception.Message)"
261
- Write-Host "完整错误日志: $errorLog" -ForegroundColor Yellow
262
- Write-Host $errorText -ForegroundColor DarkYellow
263
- try { Read-Host "按 Enter 返回 PowerShell(窗口不会自动关闭)" | Out-Null } catch { }
264
- return
265
- }
@@ -1,129 +0,0 @@
1
- #!/usr/bin/env bash
2
- # 打包 TUI Plan B 用的 "python + aimux" 离线运行时 zip(linux-x64 / win-x64 / mac-x64)。
3
- #
4
- # 基础: python-build-standalone (CPython 3.12.7, tag 20241002), 自带完整 ensurepip+pip。
5
- # aimux 及其依赖 (click/loguru/typer/rich) 全为纯 Python → linux 上一次 pip install 产出的
6
- # 代码三平台通吃。win/mac 通过 `pip install --target` 把纯 python 轮子跨装进各自 site-packages。
7
- # 跨装坑: click 在 Windows 依赖 colorama (marker platform_system==Windows), linux 上 pip 会漏,
8
- # 故 win 目标显式补 colorama。
9
- #
10
- # 产物: <DIST>/mobius-python-<arch>-v<BUNDLE_VER>.zip (zip 内根目录为 python/)
11
- # TUI 端 URL 默认指向 mobius CDN, 可用 MOBIUS_TUI_PYTHON_BUNDLE_URL 覆盖。
12
- set -euo pipefail
13
-
14
- TAG=20241002
15
- PYVER=3.12.7
16
- BUNDLE_VER=3
17
- AIMUX_VERSION=0.1.22
18
- PYPI_INDEX=https://pypi.org/simple
19
- WORK="${WORK:-/home/tianyi/python-bundles}"
20
- DIST="${DIST:-$WORK/dist}"
21
- mkdir -p "$WORK" "$DIST"
22
-
23
- base="https://github.com/astral-sh/python-build-standalone/releases/download/$TAG"
24
- # install_only_stripped = 去掉静态库 libpython.a / debug 符号, 专为分发瘦身 (运行 Python 应用无影响)
25
- declare -A URLS=(
26
- [linux-x64]="$base/cpython-${PYVER}+${TAG}-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
27
- [win-x64]="$base/cpython-${PYVER}+${TAG}-x86_64-pc-windows-msvc-shared-install_only_stripped.tar.gz"
28
- [mac-x64]="$base/cpython-${PYVER}+${TAG}-x86_64-apple-darwin-install_only_stripped.tar.gz"
29
- )
30
-
31
- # 运行 `python -m aimux` 用不到的部分: 静态库/构建脚本/idle/tk/ensurepip 内置 wheel/缓存
32
- prune() { # prune <python-root>
33
- local root=$1
34
- rm -rf "$root"/lib/python*/config-* "$root"/lib/python*/test "$root"/lib/python*/idlelib \
35
- "$root"/lib/python*/tkinter "$root"/lib/python*/turtledemo "$root"/lib/python*/ensurepip/_bundled \
36
- "$root"/lib/python*/site-packages/pip* "$root"/lib/python*/site-packages/setuptools* "$root"/lib/python*/site-packages/pkg_resources* \
37
- "$root"/Lib/config "$root"/Lib/test "$root"/Lib/idlelib "$root"/Lib/tkinter "$root"/Lib/turtledemo \
38
- "$root"/Lib/ensurepip/_bundled \
39
- 2>/dev/null || true
40
- find "$root" -name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true
41
- find "$root" -name '*.pyc' -delete 2>/dev/null || true
42
- rm -f "$root"/bin/2to3* "$root"/bin/idle* "$root"/bin/pydoc* "$root"/bin/*-config \
43
- "$root"/Scripts/2to3* "$root"/Scripts/idle* "$root"/Scripts/pydoc* 2>/dev/null || true
44
- }
45
-
46
- # 也许需要代理拉 github / pypi; 命令行无代理时直接跑, 失败再换 proxychains
47
- dl() { # dl <url> <out>
48
- if command -v proxychains4 >/dev/null 2>&1 && [ "${USE_PROXY:-1}" = 1 ]; then
49
- proxychains4 -q curl -fL "$1" -o "$2"
50
- else
51
- curl -fL "$1" -o "$2"
52
- fi
53
- }
54
- uv_install_python() {
55
- if command -v uv >/dev/null 2>&1; then
56
- uv pip install --python "$LINUX_PY" --index-url "$PYPI_INDEX" "$@"
57
- else
58
- "$LINUX_PY" -m pip install "$@"
59
- fi
60
- }
61
-
62
- echo "== 1) 下载并解压 python-build-standalone =="
63
- for arch in linux-x64 win-x64 mac-x64; do
64
- if [ -x "$WORK/$arch/python/bin/python3" ] || [ -f "$WORK/$arch/python/python.exe" ]; then
65
- echo " [$arch] 已存在, 跳过下载"; continue
66
- fi
67
- echo " [$arch] 下载 ${URLS[$arch]}"
68
- dl "${URLS[$arch]}" "$WORK/$arch.tar.gz"
69
- mkdir -p "$WORK/$arch"
70
- tar -xzf "$WORK/$arch.tar.gz" -C "$WORK/$arch"
71
- done
72
-
73
- LINUX_PY="$WORK/linux-x64/python/bin/python3"
74
- echo "== 2) linux-x64: 原生 pip install aimux =="
75
- USE_PROXY=1 uv_install_python --quiet "aimux==$AIMUX_VERSION" colorama || \
76
- uv_install_python "aimux==$AIMUX_VERSION" colorama
77
- echo " 验证: $($LINUX_PY -c 'import aimux, click, loguru, typer, rich; print("linux import ok", aimux.__name__)')"
78
-
79
- echo "== 3) win-x64 / mac-x64: 跨装纯 python aimux 到各自 site-packages =="
80
- # win: click 需 colorama; mac: 不需要 colorama
81
- install_target() { # arch site_packages_dir [extra...]
82
- local arch=$1 sp=$2; shift 2
83
- echo " [$arch] pip install --target $sp aimux $*"
84
- rm -rf "$sp"/* 2>/dev/null || true # 重复构建时清旧
85
- USE_PROXY=1 uv pip install --quiet --index-url "$PYPI_INDEX" --target "$sp" "aimux==$AIMUX_VERSION" "$@" || \
86
- uv pip install --index-url "$PYPI_INDEX" --target "$sp" "aimux==$AIMUX_VERSION" "$@"
87
- echo " [$arch] site-packages:"; ls "$sp" | head -20
88
- }
89
- install_target win-x64 "$WORK/win-x64/python/Lib/site-packages" colorama win32-setctime
90
- install_target mac-x64 "$WORK/mac-x64/python/lib/python3.12/site-packages"
91
-
92
- echo "== 4) 瘦身 (删运行时用不到的 test/idle/tk/ensurepip wheel/缓存) 后打 zip =="
93
- for arch in linux-x64 win-x64 mac-x64; do prune "$WORK/$arch/python"; done
94
- # 本机无 zip 命令 → 用 python 造一个等价 -ry 的打包器: external_attr 存 st_mode,
95
- # 符号链接存为 link-target + S_IFLNK 位, extract-zip 据此还原 symlink 与可执行位。
96
- zip_py="$(mktemp).py"
97
- cat > "$zip_py" <<'PYEOF'
98
- import os, sys, stat, zipfile
99
- src, out = sys.argv[1], sys.argv[2]
100
- parent = os.path.dirname(src.rstrip('/'))
101
- def add(z, full):
102
- arc = os.path.relpath(full, parent)
103
- st = os.lstat(full)
104
- zi = zipfile.ZipInfo(arc, (1980, 1, 1, 0, 0, 0))
105
- zi.external_attr = (st.st_mode & 0xFFFF) << 16
106
- zi.create_system = 3 # unix
107
- if stat.S_ISLNK(st.st_mode):
108
- z.writestr(zi, os.readlink(full)) # 符号链接: 内容=目标路径
109
- else:
110
- with open(full, 'rb') as f: z.writestr(zi, f.read())
111
- with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as z:
112
- for dp, dirs, files in os.walk(src):
113
- for name in list(dirs):
114
- full = os.path.join(dp, name)
115
- if os.path.islink(full): # 目录符号链接: 存为链接, 不下钻
116
- add(z, full); dirs.remove(name)
117
- for name in files:
118
- add(z, os.path.join(dp, name))
119
- print(' ok', out)
120
- PYEOF
121
- for arch in linux-x64 win-x64 mac-x64; do
122
- out="$DIST/mobius-python-$arch-v${BUNDLE_VER}.zip"
123
- rm -f "$out"
124
- "$LINUX_PY" "$zip_py" "$WORK/$arch/python" "$out"
125
- echo " $out $(du -h "$out" | cut -f1)"
126
- done
127
- rm -f "$zip_py"
128
- echo "== 完成. 产物: =="
129
- ls -lh "$DIST"/mobius-python-*-v${BUNDLE_VER}.zip
@@ -1,221 +0,0 @@
1
- /** AIMUX status UI + heartbeat/reconnect regression tests. */
2
- import React from 'react'
3
- import { EventEmitter } from 'node:events'
4
- import { spawn } from 'node:child_process'
5
- import { promises as fs, existsSync } from 'node:fs'
6
- import os from 'node:os'
7
- import path from 'node:path'
8
- import { render } from 'ink-testing-library'
9
- import { AimuxStatusLine } from '../src/components/AimuxStatus.js'
10
- import { AimuxSupervisor, probeAimuxBridgeConnection, bundleArch, bundleUrl, spawnLauncher, ensureFromBundle, downloadBundleForTest, reverseConnectArgs, aimuxLogPath, bundleHealthCheckCode, tuiAimuxIdentifier } from '../src/aimux.js'
11
-
12
- const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
13
- let pass = 0, fail = 0
14
- function ok(condition: boolean, message: string) {
15
- if (condition) { pass += 1; console.log(` ✓ ${message}`) }
16
- else { fail += 1; console.error(` ✗ ${message}`) }
17
- }
18
-
19
- function fakeChild(onKill: () => void): any {
20
- const child: any = new EventEmitter()
21
- child.pid = 12345
22
- child.stdout = new EventEmitter()
23
- child.stderr = new EventEmitter()
24
- child.kill = () => { onKill(); return true }
25
- return child
26
- }
27
-
28
- async function testStatusLine() {
29
- console.log('\n[AIMUX 1] status display')
30
- const { lastFrame, rerender, unmount } = render(
31
- <AimuxStatusLine status={{ state: 'starting', phase: 'install', detail: '下载并安装 aimux… 48%' }} />,
32
- )
33
- ok((lastFrame() ?? '').includes('AIMUX · 安装') && (lastFrame() ?? '').includes('48%'), 'installation phase and progress stay visible')
34
- rerender(<AimuxStatusLine status={{ state: 'failed', phase: 'retrying', detail: '心跳中断,2 秒后进行第 2 次重连…', attempt: 2 }} />)
35
- ok((lastFrame() ?? '').includes('AIMUX · 重连') && (lastFrame() ?? '').includes('第 2 次重连'), 'retry phase and attempt are explicit')
36
- unmount()
37
- }
38
-
39
- async function testProbeContract() {
40
- console.log('\n[AIMUX 2] bridge heartbeat contract')
41
- const realFetch = globalThis.fetch
42
- let requestedUrl = '', auth = ''
43
- globalThis.fetch = (async (input: any, init?: RequestInit) => {
44
- requestedUrl = String(input)
45
- auth = String((init?.headers as Record<string, string>)?.Authorization ?? '')
46
- return new Response(JSON.stringify({ identifier: 'tui-test', event_stream_connected: true }), { status: 200 })
47
- }) as typeof fetch
48
- try {
49
- const connected = await probeAimuxBridgeConnection('https://mobius.test/', 'jwt-test', 'tui-test', 100)
50
- ok(connected, 'heartbeat accepts only an active event stream for this identifier')
51
- ok(requestedUrl.endsWith('/aimux_bridge/api/remotes/tui-test/connection'), 'heartbeat calls the bridge connection endpoint')
52
- ok(auth === 'Bearer jwt-test', 'heartbeat carries the Mobius JWT')
53
- } finally { globalThis.fetch = realFetch }
54
- }
55
-
56
- async function testAutomaticReconnect() {
57
- console.log('\n[AIMUX 3] heartbeat-triggered reconnect')
58
- const statuses: string[] = []
59
- let probes = 0, spawns = 0, kills = 0
60
- const supervisor = new AimuxSupervisor({
61
- server: 'https://mobius.test', token: 'jwt-test', identifier: 'tui-test',
62
- heartbeatIntervalMs: 5, heartbeatFailureThreshold: 2, retryBaseMs: 5,
63
- probeConnection: async () => { probes += 1; return probes >= 3 },
64
- spawnProcess: () => { spawns += 1; return fakeChild(() => { kills += 1 }) },
65
- onStatus: status => statuses.push(`${status.state}:${status.phase}:${status.detail}`),
66
- })
67
- supervisor.start()
68
- for (let i = 0; i < 30 && !statuses.some(s => s.startsWith('connected:')); i += 1) await delay(5)
69
- ok(kills >= 1, 'two failed heartbeats terminate the stale AIMUX process')
70
- ok(spawns >= 2, 'supervisor starts a fresh AIMUX process after heartbeat loss')
71
- ok(statuses.some(s => s.includes('第 1 次重连')), 'reconnect status reports its retry attempt')
72
- ok(statuses.some(s => s.startsWith('connected:connected:心跳正常')), 'a later successful heartbeat restores connected state')
73
- await supervisor.stop()
74
- }
75
-
76
- async function testBundleArchAndUrl() {
77
- console.log('\n[AIMUX 4] Plan B bundle arch / url')
78
- const arch = bundleArch()
79
- ok(arch === 'linux-x64' || arch === 'win-x64' || arch === 'mac-x64', `bundleArch returns a supported arch on this host (${arch})`)
80
- const before = bundleUrl('linux-x64')
81
- ok(before.includes('mobius-python-linux-x64-v3') && before.endsWith('.zip'), 'bundleUrl follows the fixed filename pattern')
82
- const saved = process.env.MOBIUS_TUI_PYTHON_BUNDLE_URL
83
- process.env.MOBIUS_TUI_PYTHON_BUNDLE_URL = 'https://example.test/cdn/'
84
- try {
85
- ok(bundleUrl('win-x64') === 'https://example.test/cdn/mobius-python-win-x64-v3.zip', 'MOBIUS_TUI_PYTHON_BUNDLE_URL overrides the CDN base and trims trailing slash')
86
- } finally { if (saved === undefined) delete process.env.MOBIUS_TUI_PYTHON_BUNDLE_URL; else process.env.MOBIUS_TUI_PYTHON_BUNDLE_URL = saved }
87
- }
88
-
89
- async function testPersistentProcessLog() {
90
- console.log('\n[AIMUX 9] persistent process diagnostics')
91
- const home = await fs.mkdtemp(path.join(os.tmpdir(), 'mobius-tui-aimux-log-'))
92
- const savedHome = process.env.MOBIUS_TUI_HOME
93
- process.env.MOBIUS_TUI_HOME = home
94
- const statuses: string[] = []
95
- let childRef: any
96
- const supervisor = new AimuxSupervisor({
97
- server: 'https://mobius.test', token: 'secret-token', identifier: 'tui-log',
98
- retryBaseMs: 100_000,
99
- probeConnection: async () => true,
100
- spawnProcess: () => {
101
- childRef = fakeChild(() => {})
102
- return childRef
103
- },
104
- onStatus: status => statuses.push(status.detail || ''),
105
- })
106
- supervisor.start()
107
- childRef.stderr.emit('data', Buffer.from('Traceback\n File "site-packages/loguru/_ctime_functions.py", line 7\nImportError: win32_setctime missing\n'))
108
- childRef.emit('exit', 1)
109
- await delay(40)
110
- const log = await fs.readFile(aimuxLogPath(), 'utf8')
111
- ok(log.includes('win32_setctime missing') && log.includes('AIMUX exit code=1'), 'AIMUX stdout/stderr and exit code are persisted')
112
- ok(log.includes('_ctime_functions.py') && !log.includes('secret-token'), 'diagnostic log keeps traceback context without JWT')
113
- ok(statuses.some(s => s.includes('日志:') && s.includes('aimux.log')), 'failure status points to the persistent log path')
114
- await supervisor.stop()
115
- if (savedHome === undefined) delete process.env.MOBIUS_TUI_HOME; else process.env.MOBIUS_TUI_HOME = savedHome
116
- await fs.rm(home, { recursive: true, force: true })
117
- }
118
-
119
- function captureStdout(child: ReturnType<typeof spawn>): Promise<string> {
120
- let out = ''
121
- child.stdout?.on('data', d => { out += d.toString() })
122
- return new Promise(resolve => child.on('close', () => resolve(out)))
123
- }
124
-
125
- async function testSpawnLauncher() {
126
- console.log('\n[AIMUX 5] Plan B spawnLauncher routing')
127
- // exe launcher: spawn the binary directly with the given args
128
- let out = await captureStdout(spawnLauncher({ kind: 'exe', path: '/bin/echo' }, ['HELLO', 'arg']))
129
- ok(out.trim() === 'HELLO arg', `exe launcher runs the aimux binary directly (got: ${out.trim()})`)
130
- // module launcher: inject `-m aimux` in front (so `<python> -m aimux ...`)
131
- out = await captureStdout(spawnLauncher({ kind: 'module', python: '/bin/echo' }, ['reverse', 'connect']))
132
- ok(out.trim() === '-m aimux reverse connect', `module launcher prepends -m aimux (got: ${out.trim()})`)
133
- }
134
-
135
- function testReverseConnectArgs() {
136
- console.log('\n[AIMUX 6] reverse connect Windows shell visibility')
137
- const win = reverseConnectArgs('https://mobius.test/', 'tui-win', 'jwt-test', 'win32')
138
- const linux = reverseConnectArgs('https://mobius.test/', 'tui-linux', 'jwt-test', 'linux')
139
- ok(win.includes('--slient-v2'), 'Windows reverse connection always requests the no-console shell mode')
140
- ok(!linux.includes('--slient-v2'), 'non-Windows reverse connection does not receive the Windows-only flag')
141
- ok(win[2] === 'https://mobius.test/aimux_bridge', 'reverse connection normalizes the bridge URL')
142
- }
143
-
144
- function testAimuxIdentifierScopesWorkspace() {
145
- console.log('\n[AIMUX 6a] reverse client identifier workspace isolation')
146
- const first = tuiAimuxIdentifier('same-host', '/work/project-a')
147
- const firstAgain = tuiAimuxIdentifier('same-host', '/work/project-a')
148
- const second = tuiAimuxIdentifier('same-host', '/work/project-b')
149
- ok(first === firstAgain, 'identifier is stable for the same host and workspace')
150
- ok(first !== second, 'different workspaces on one host do not replace each other')
151
- ok(/^tui-same-host-[a-f0-9]{10}$/.test(first), 'identifier remains bridge-safe and recognizable')
152
- }
153
-
154
- function testBundleHealthCheck() {
155
- console.log('\n[AIMUX 6b] bundle dependency health check')
156
- const win = bundleHealthCheckCode('win32')
157
- const linux = bundleHealthCheckCode('linux')
158
- ok(win.includes('aimux.bridge_client') && win.includes('win32_setctime'), 'Windows bundle probe imports the real bridge path and its platform dependency')
159
- ok(win.includes("aimux.__version__ == '0.1.23'"), 'bundle probe rejects stale AIMUX versions')
160
- ok(!linux.includes('win32_setctime'), 'non-Windows bundle probe does not require the Windows-only package')
161
- }
162
-
163
- async function testEnsureFromBundleReady() {
164
- console.log('\n[AIMUX 7] Plan B ensureFromBundle fast-path (bundle already extracted)')
165
- const home = await fs.mkdtemp(path.join(os.tmpdir(), 'mobius-tui-bundle-'))
166
- const savedHome = process.env.MOBIUS_TUI_HOME
167
- process.env.MOBIUS_TUI_HOME = home
168
- // 放一个"假 python": 任何 `-c import aimux` 都返回 0 → bundleReady() 为真
169
- const fakePy = path.join(home, 'python-bundle', 'python', 'bin', 'python3')
170
- await fs.mkdir(path.dirname(fakePy), { recursive: true })
171
- await fs.writeFile(fakePy, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
172
- try {
173
- const r = await ensureFromBundle()
174
- ok(r.ok === true && r.launcher?.kind === 'module', 'ensureFromBundle short-circuits when the bundle is already present')
175
- ok(r.launcher?.kind === 'module' && r.launcher.python.endsWith(path.join('python-bundle', 'python', 'bin', 'python3')), 'launcher points at the bundled python')
176
- ok(!existsSync(path.join(home, 'python-bundle-v1.zip.tmp')), 'no download tmp is left behind on the fast-path')
177
- } finally { if (savedHome === undefined) delete process.env.MOBIUS_TUI_HOME; else process.env.MOBIUS_TUI_HOME = savedHome; await fs.rm(home, { recursive: true, force: true }) }
178
- }
179
-
180
- async function testDownloadBundleStream() {
181
- console.log('\n[AIMUX 8] Plan B downloadBundle streams body to file + reports progress')
182
- const home = await fs.mkdtemp(path.join(os.tmpdir(), 'mobius-tui-dl-'))
183
- const savedHome = process.env.MOBIUS_TUI_HOME
184
- process.env.MOBIUS_TUI_HOME = home
185
- const realFetch = globalThis.fetch
186
- const payload = Buffer.from(Array.from({ length: 64 * 1024 }, (_, i) => i & 0xff))
187
- globalThis.fetch = (async () => new Response(payload as any, {
188
- status: 200, headers: { 'content-length': String(payload.length) },
189
- })) as typeof fetch
190
- let progressCalls = 0
191
- try {
192
- const r = await downloadBundleForTest('linux-x64', () => { progressCalls += 1 })
193
- ok(r.ok === true && !!r.zipPath, 'downloadBundle writes the streamed body to a zip tmp')
194
- const written = await fs.readFile(r.zipPath!)
195
- ok(written.length === payload.length && written[0] === 0 && written[65535] === 255, 'downloaded bytes match the streamed payload')
196
- ok(progressCalls > 0, 'progress callback fires during streaming download')
197
- await fs.unlink(r.zipPath!)
198
- } finally {
199
- globalThis.fetch = realFetch
200
- if (savedHome === undefined) delete process.env.MOBIUS_TUI_HOME; else process.env.MOBIUS_TUI_HOME = savedHome
201
- await fs.rm(home, { recursive: true, force: true })
202
- }
203
- }
204
-
205
- async function main() {
206
- await testStatusLine()
207
- await testProbeContract()
208
- await testAutomaticReconnect()
209
- await testBundleArchAndUrl()
210
- await testSpawnLauncher()
211
- testReverseConnectArgs()
212
- testAimuxIdentifierScopesWorkspace()
213
- testBundleHealthCheck()
214
- await testEnsureFromBundleReady()
215
- await testDownloadBundleStream()
216
- await testPersistentProcessLog()
217
- console.log(`\n==== AIMUX RESULT: ${pass} passed, ${fail} failed ====\n`)
218
- process.exit(fail === 0 ? 0 : 1)
219
- }
220
-
221
- main().catch(error => { console.error('FATAL', error); process.exit(2) })