Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conductor/tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file tracks all major tracks for the project. Each track has its own detail

---

## [ ] Track: 增强 Shim 解析逻辑与项目上下文检测
## [~] Track: 增强 Shim 解析逻辑与项目上下文检测

*Link: [./conductor/tracks/shim_context_20251222/](./conductor/tracks/shim_context_20251222/)*

Expand Down
5 changes: 5 additions & 0 deletions conductor/tracks/fix_windows_bunx_20260128/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Track fix_windows_bunx_20260128 Context

- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)
8 changes: 8 additions & 0 deletions conductor/tracks/fix_windows_bunx_20260128/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"track_id": "fix_windows_bunx_20260128",
"type": "bug",
"status": "new",
"created_at": "2026-01-28T10:00:00Z",
"updated_at": "2026-01-28T10:00:00Z",
"description": "修复 BVM 在 Windows 环境下 bunx 命令失效的问题,主要涉及身份识别和显式路由优化。"
}
24 changes: 24 additions & 0 deletions conductor/tracks/fix_windows_bunx_20260128/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
### Implementation Plan: fix_windows_bunx_20260128

#### Phase 1: Core Shim & Template Optimization
*目标:在核心代码层面修复身份识别和路由逻辑。*

- [ ] Task: 更新 Windows JS Shim 模板 (`src/templates/win/bvm-shim.js`),实现 `bunx` 到 `bun x` 的显式指令路由。 [x]
- [ ] Task: 更新 Windows CMD 模板 (`src/templates/win/bunx.cmd` & `bun.cmd`),为快速通道添加 `x` 路由支持。 [x]
- [ ] Task: 优化 Unix Shim 模板 (`src/templates/unix/bvm-shim.sh`),使用 `exec -a` 增强身份识别稳定性。 [x]
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Core Shim & Template Optimization' (Protocol in workflow.md)

#### Phase 2: Installer & Self-Repair Evolution
*目标:确保新安装及现有环境均能自动补全所需的物理文件。*

- [ ] Task: 改进 `install.ps1` (Windows),增加 `bunx.exe` 副本生成逻辑及旧版本扫描修复。 [x]
- [ ] Task: 改进 `install.sh` (Unix),完善运行时下载后的 `bunx` 软链接创建。 [x]
- [ ] Task: 更新 `src/commands/setup.ts`,确保运行 `bvm setup` 时能递归修复所有已安装版本的 `bunx` 文件。 [ ]
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Installer & Self-Repair Evolution' (Protocol in workflow.md)

#### Phase 3: Verification & TDD
*目标:通过测试验证修复效果,并确保发布版本同步。*

- [ ] Task: 编写或更新 `test/bunx_consistency.test.ts`,在模拟环境下验证 Windows 路由和文件补全。 [ ]
- [ ] Task: 执行 `bun run build` 同步所有安装脚本版本,并运行全量测试。 [x]
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Verification & TDD' (Protocol in workflow.md)
27 changes: 27 additions & 0 deletions conductor/tracks/fix_windows_bunx_20260128/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
### Track Specification: fix_windows_bunx_20260128

#### Overview
修复 BVM 在 Windows 环境下 `bunx` 命令失效的问题。

**当前问题证据(用户截图/日志):**
```powershell
PS C:\Users\steph\OneDrive\Desktop> bunx
Bun is a fast JavaScript runtime... (输出了帮助信息,未识别为 bunx)
PS C:\Users\steph\OneDrive\Desktop> bunx skills add microsoft/PowerToys
error: Script not found "skills" (身份识别错误导致路由到了 bun run 逻辑)
```

#### Functional Requirements
1. **显式路由逻辑**:修改 Windows 垫片(Shim)逻辑,将 `bunx <args>` 显式路由为 `bun x <args>`,确保 100% 触发 Bun 的扩展包运行逻辑。
2. **物理文件补全**:在安装 Bun 运行时或执行 `bvm setup` 时,确保版本 bin 目录下存在 `bunx.exe`。
3. **安装脚本自愈**:
* 更新 `install.ps1`,在安装新版本或检测到系统 Bun 时,自动补全缺失的 `bunx.exe`。
* 更新 `install.sh`,同步确保 Unix 环境下的 `bunx` 软链接正确。
4. **环境一键修复**:通过 `npm i . -g` 或 `bvm setup` 即可自动修复所有已安装版本的 `bunx` 坏道。
5. **版本切换支持**:`bunx` 必须严格遵循 `.bvmrc` 或全局默认版本的切换逻辑。

#### Acceptance Criteria
- [ ] **身份验证**:在 Windows 中运行 `bunx --version` 返回版本号,不再返回 Bun 的通用帮助信息。
- [ ] **功能验证**:运行 `bunx skills add ...` 能正确识别 `skills` 为包名并执行,不再报 "Script not found"。
- [ ] **物理验证**:重新运行 `bvm setup` 后,所有已安装版本的 `bin` 目录下均出现 `bunx` 文件。
- [ ] **安装验证**:运行 `install.sh` 或 `install.ps1` 完成后,新安装的版本自带可运行的 `bunx`。
6 changes: 4 additions & 2 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function Detect-NetworkZone {
$BVM_REGION = Detect-NetworkZone
$REGISTRY = if ($BVM_REGION -eq "cn") { "registry.npmmirror.com" } else { "registry.npmjs.org" }

$DEFAULT_BVM_VER = "v1.1.35"
$DEFAULT_BVM_VER = "v1.1.36"
$BVM_VER = if ($env:BVM_INSTALL_VERSION) { $env:BVM_INSTALL_VERSION } else { "" }
if (-not $BVM_VER) {
try {
Expand Down Expand Up @@ -97,6 +97,7 @@ if ($SYSTEM_BUN_BIN) {
$SYS_BIN_DIR = Join-Path $SYS_VER_DIR "bin"
if (-not (Test-Path $SYS_BIN_DIR)) { New-Item -ItemType Directory -Path $SYS_BIN_DIR -Force | Out-Null }
Copy-Item $SYSTEM_BUN_BIN (Join-Path $SYS_BIN_DIR "bun.exe") -Force
Copy-Item $SYSTEM_BUN_BIN (Join-Path $SYS_BIN_DIR "bunx.exe") -Force

# Smoke Test
$BvmIndex = Join-Path $BVM_SRC_DIR "index.js"
Expand Down Expand Up @@ -129,6 +130,7 @@ if ($USE_SYSTEM_AS_RUNTIME) {
$BIN_DEST = Join-Path $TARGET_DIR "bin"
if (-not (Test-Path $BIN_DEST)) { New-Item -ItemType Directory -Path $BIN_DEST -Force | Out-Null }
Move-Item -Path $FoundBun.FullName -Destination (Join-Path $BIN_DEST "bun.exe") -Force
Copy-Item (Join-Path $BIN_DEST "bun.exe") (Join-Path $BIN_DEST "bunx.exe") -Force
Remove-Item $TMP -Force
Remove-Item $EXT -Recurse -Force
}
Expand Down Expand Up @@ -163,7 +165,7 @@ set "BVM_DIR=$WinBvmDir"
set "BUN_INSTALL=%BVM_DIR%\current"

if not exist ".bvmrc" (
"%BVM_DIR%\runtime\current\bin\bun.exe" %*
"%BVM_DIR%\runtime\current\bin\%~n0.exe" %*
exit /b %errorlevel%
)

Expand Down
4 changes: 3 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
set -e

# --- Configuration ---
DEFAULT_BVM_VERSION="v1.1.35" # Fallback
DEFAULT_BVM_VERSION="v1.1.36" # Fallback
FALLBACK_BUN_VERSION="1.3.6"
BVM_SRC_VERSION="${BVM_INSTALL_VERSION}" # If empty, will resolve dynamically

Expand Down Expand Up @@ -153,6 +153,7 @@ if [ -n "$SYSTEM_BUN_BIN" ]; then
cp "$SYSTEM_BUN_BIN" "${SYS_VER_DIR}/bin/bun"
fi
chmod +x "${SYS_VER_DIR}/bin/bun"
ln -sf "./bun" "${SYS_VER_DIR}/bin/bunx"

# Smoke Test
if "${SYS_VER_DIR}/bin/bun" "${BVM_SRC_DIR}/index.js" --version >/dev/null 2>&1; then
Expand Down Expand Up @@ -206,6 +207,7 @@ else
mkdir -p "${TARGET_RUNTIME_DIR}/bin"
mv "$(find "$TEMP_DIR_BUN" -type f -name "bun" | head -n 1)" "${TARGET_RUNTIME_DIR}/bin/bun"
chmod +x "${TARGET_RUNTIME_DIR}/bin/bun"
ln -sf "./bun" "${TARGET_RUNTIME_DIR}/bin/bunx"
rm -rf "$TEMP_DIR_BUN"
fi
fi
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "bvm-core",
"version": "1.1.35",
"description": "The native version manager for Bun. Cross-platform, shell-agnostic, and zero-dependency.",
"version": "1.1.36",
"description": "Bun Version Manager (BVM) - Native, fast, and cross-platform.",
"main": "dist/index.js",
"bin": {
"bvm": "bin/bvm-npm.js"
Expand Down
182 changes: 115 additions & 67 deletions scripts/verify-e2e-npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,130 @@
import { $ } from "bun";
import { join } from "path";
import { homedir } from "os";
import { existsSync, readdirSync } from "fs";
import { existsSync, readdirSync, mkdirSync, writeFileSync, chmodSync, rmSync } from "fs";

const HOME = homedir();
const BVM_DIR = join(HOME, ".bvm");
const SHIMS_DIR = join(BVM_DIR, "shims");
const BIN_DIR = join(BVM_DIR, "bin");

console.log("\n🚀 Starting E2E NPM Verification Protocol...\n");

try {
// 1. Cleanup
console.log(`🧹 Cleaning up environment (${BVM_DIR})...`);
await $`rm -rf ${BVM_DIR}`;
await $`rm -f bvm-core-*.tgz`;

// 2. Build & Pack
console.log("📦 Building and Packing...");
// Using simple shell command to ensure npm run build executes correctly
await $`npm run build`.quiet();
await $`npm pack`.quiet();

// Find the tarball
const files = readdirSync(process.cwd());
const tarball = files.find(f => f.startsWith("bvm-core-") && f.endsWith(".tgz"));
if (!tarball) throw new Error("Tarball not found after packing!");
console.log(`📝 Found tarball: ${tarball}`);

// 3. Install
console.log("💿 Installing globally via NPM (this may take a moment)...");
// We capture stdout/stderr to show it only on failure, or stream it?
// Let's stream it to be transparent as requested.
await $`npm install -g ./${tarball} --foreground-scripts --force`;

// 4. Verify Physical Structure
console.log("🔍 Verifying filesystem structure...");

if (!existsSync(SHIMS_DIR)) throw new Error(`❌ Shims directory missing: ${SHIMS_DIR}`);
if (!existsSync(join(SHIMS_DIR, "bun"))) throw new Error(`❌ Bun shim missing`);
if (!existsSync(join(BIN_DIR, "bvm"))) throw new Error(`❌ BVM binary missing`);

console.log(" ✅ Shims directory exists.");
console.log(" ✅ Bun shim exists.");

// 5. Functional Verification
console.log("🧪 Verifying BVM functionality...");
const bvmExec = join(BIN_DIR, "bvm");
const output = await $`${bvmExec} ls`.text();
console.log(output.trim());

if (!output.includes("Locally installed Bun versions")) {
throw new Error("❌ 'bvm ls' output is unexpected.");
export class E2ESandbox {
public bvmDir: string;
public shimsDir: string;
public binDir: string;
private tempHome: string;

constructor() {
this.tempHome = join(process.cwd(), `.tmp-npm-verify-${Date.now()}`);
this.bvmDir = join(this.tempHome, ".bvm");
this.shimsDir = join(this.bvmDir, "shims");
this.binDir = join(this.bvmDir, "bin");
if (!existsSync(this.tempHome)) mkdirSync(this.tempHome, { recursive: true });
}

async installLocal() {
// Mock implementation for testing safety/upgrade logic without full NPM network hit
if (!existsSync(this.bvmDir)) mkdirSync(this.bvmDir, { recursive: true });
if (!existsSync(this.shimsDir)) mkdirSync(this.shimsDir, { recursive: true });
if (!existsSync(this.binDir)) mkdirSync(this.binDir, { recursive: true });

const bvmBin = join(this.binDir, "bvm");
const bvmSrcDir = join(this.bvmDir, "src");
if (!existsSync(bvmSrcDir)) mkdirSync(bvmSrcDir, { recursive: true });

// Marker for NPM install
const marker = join(this.bvmDir, ".npm-install");
writeFileSync(marker, "true");

// Dummy wrapper
writeFileSync(bvmBin, `#!/bin/bash\nexport BVM_INSTALL_SOURCE="npm"\nexec ${process.execPath} ${join(this.bvmDir, "src", "index.js")} "$@"\n`);
chmodSync(bvmBin, 0o755);

// Dummy source
writeFileSync(join(bvmSrcDir, "index.js"), "import '...'; console.log('bvm')");
}

// 6. Global Package Isolation Verification (NEW)
console.log("📦 Verifying Global Package Isolation (Option B)...");
// Ensure we are using a version
await $`${bvmExec} use default`.quiet();
// Simulate bun install -g
console.log(" Installing dummy global package...");
await $`${join(SHIMS_DIR, "bun")} install -g fake-pkg-test-bvm`.quiet().catch(() => {});

const currentBin = join(BVM_DIR, "current", "bin");
console.log(` Checking if current bin path is correctly set up: ${currentBin}`);

// We check if current/bin is in the PATH reported by setup
const zshrc = await $`cat ${join(HOME, ".zshrc")}`.text();
if (!zshrc.includes("current/bin")) {
throw new Error("❌ .zshrc does not contain 'current/bin' in PATH");
cleanup() {
if (existsSync(this.tempHome)) rmSync(this.tempHome, { recursive: true, force: true });
}
}

async function runProtocol() {
console.log("\n🚀 Starting E2E NPM Verification Protocol...\n");

try {
// 1. Cleanup
console.log(`🧹 Cleaning up environment (${BVM_DIR})...`);
await $`rm -rf ${BVM_DIR}`;
await $`rm -f bvm-core-*.tgz`.nothrow(); // Use nothrow to avoid error if no tgz exists

// 2. Build & Pack
console.log("📦 Building and Packing...");
// Using simple shell command to ensure npm run build executes correctly
await $`npm run build`.quiet();
await $`npm pack`.quiet();

// Find the tarball
const files = readdirSync(process.cwd());
const tarball = files.find(f => f.startsWith("bvm-core-") && f.endsWith(".tgz"));
if (!tarball) throw new Error("Tarball not found after packing!");
console.log(`📝 Found tarball: ${tarball}`);

// 3. Install
console.log("💿 Installing globally via NPM (this may take a moment)...");
// We capture stdout/stderr to show it only on failure, or stream it?
// Let's stream it to be transparent as requested.
await $`npm install -g ./${tarball} --foreground-scripts --force`;

console.log("\n✅ \x1b[32mE2E VERIFICATION PASSED!\x1b[0m");
console.log(" BVM is installed, shims exist, and CLI works.");
console.log(` Run 'source ~/.zshrc' (or your shell config) to start using it.`);
// 4. Verify Physical Structure
console.log("🔍 Verifying filesystem structure...");

if (!existsSync(SHIMS_DIR)) throw new Error(`❌ Shims directory missing: ${SHIMS_DIR}`);
if (!existsSync(join(SHIMS_DIR, "bun"))) throw new Error(`❌ Bun shim missing`);
if (!existsSync(join(BIN_DIR, "bvm"))) throw new Error(`❌ BVM binary missing`);

console.log(" ✅ Shims directory exists.");
console.log(" ✅ Bun shim exists.");

// 5. Functional Verification
console.log("🧪 Verifying BVM functionality...");
const bvmExec = join(BIN_DIR, "bvm");
const output = await $`${bvmExec} ls`.text();
console.log(output.trim());

if (!output.includes("Locally installed Bun versions")) {
throw new Error("❌ 'bvm ls' output is unexpected.");
}

// 6. Global Package Isolation Verification (NEW)
console.log("📦 Verifying Global Package Isolation (Option B)...");
// Ensure we are using a version
await $`${bvmExec} use default`.quiet();
// Simulate bun install -g
console.log(" Installing dummy global package...");
await $`${join(SHIMS_DIR, "bun")} install -g fake-pkg-test-bvm`.quiet().catch(() => {});

const currentBin = join(BVM_DIR, "current", "bin");
console.log(` Checking if current bin path is correctly set up: ${currentBin}`);

// We check if current/bin is in the PATH reported by setup
const zshrc = await $`cat ${join(HOME, ".zshrc")}`.text();
if (!zshrc.includes("current/bin")) {
throw new Error("❌ .zshrc does not contain 'current/bin' in PATH");
}

console.log("\n✅ \x1b[32mE2E VERIFICATION PASSED!\x1b[0m");
console.log(" BVM is installed, shims exist, and CLI works.");
console.log(` Run 'source ~/.zshrc' (or your shell config) to start using it.`);

} catch (e) {
console.error("\n💥 \x1b[31mVERIFICATION FAILED:\x1b[0m");
console.error(e);
process.exit(1);
}
}

} catch (e) {
console.error("\n💥 \x1b[31mVERIFICATION FAILED:\x1b[0m");
console.error(e);
process.exit(1);
// Check if this script is being run directly
if (import.meta.main) {
runProtocol();
}
Loading