Skip to content
Open
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
151 changes: 142 additions & 9 deletions scripts/setup.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
param(
[switch]$SkipInstall,
[switch]$SkipBuild,
[switch]$SkipBun,
[switch]$ForceConfig,
[switch]$Uninstall,
[switch]$Overseas,
Expand Down Expand Up @@ -104,6 +105,97 @@ function Find-Chrome {
return $null
}

function Resolve-BunBin {
if ($env:STEP_BUN_BIN -and (Test-Path $env:STEP_BUN_BIN)) {
return $env:STEP_BUN_BIN
}
$cmd = Get-Command bun -ErrorAction SilentlyContinue
if ($cmd) {
return $cmd.Source
}
return $null
}

function Install-Bun {
<#
.SYNOPSIS
Ensures Bun is available on PATH. Strategy:
1. Already on PATH -> use it
2. winget -> install Oven-sh.Bun
3. Direct download -> github.com/oven-sh/bun/releases/latest
Returns the path to bun.exe, or $null if all attempts failed.
#>
$existing = Resolve-BunBin
if ($existing) {
Write-Ok "Bun already available: $existing"
return $existing
}

Write-Section "[bun] Installing Bun (needed for OpenTUI TUI on Windows)"

if (Test-Command "winget") {
Write-Info "Trying winget install Oven-sh.Bun ..."
& winget install Oven-sh.Bun --accept-source-agreements --accept-package-agreements --silent
if ($LASTEXITCODE -eq 0) {
$bunPath = Join-Path $HOME ".bun\bin\bun.exe"
if ($env:Path -notlike "*$HOME\.bun\bin*") {
$env:Path = "$HOME\.bun\bin;$env:Path"
}
$cmd = Get-Command bun -ErrorAction SilentlyContinue
$candidate = if ($cmd) { $cmd.Source } elseif (Test-Path $bunPath) { $bunPath } else { $null }
if ($candidate) {
$versionOutput = & $candidate --version 2>$null
if ($LASTEXITCODE -eq 0 -and $versionOutput) {
Write-Ok "Bun installed via winget: $candidate ($($versionOutput.Trim()))"
return $candidate
}
Write-Warn "Bun at $candidate did not respond to --version; continuing to next strategy"
}
}
Write-Warn "winget install exited $LASTEXITCODE; falling back to direct download"
}

$installDir = Join-Path $HOME ".bun"
$bunExe = Join-Path $installDir "bin\bun.exe"
if (Test-Path $bunExe) {
Write-Ok "Bun already downloaded at $bunExe"
return $bunExe
}

Write-Info "Downloading Bun for Windows x64 from github.com/oven-sh/bun ..."
$zipUrl = "https://github.com/oven-sh/bun/releases/latest/download/bun-windows-x64.zip"
$tmpZip = Join-Path $env:TEMP "bun-windows-x64.zip"
try {
Invoke-WebRequest -Uri $zipUrl -OutFile $tmpZip -UseBasicParsing
Expand-Archive -Path $tmpZip -DestinationPath $installDir -Force
# The zip extracts as `bun-windows-x64\bun.exe`; flatten into bin\
$nested = Join-Path $installDir "bun-windows-x64\bun.exe"
if (Test-Path $nested) {
New-Item -ItemType Directory -Force (Join-Path $installDir "bin") | Out-Null
Move-Item -Force $nested $bunExe
Remove-Item -Recurse -Force (Join-Path $installDir "bun-windows-x64") -ErrorAction SilentlyContinue
}
if (Test-Path $bunExe) {
$versionOutput = & $bunExe --version 2>$null
if ($LASTEXITCODE -eq 0 -and $versionOutput) {
Add-UserPath (Join-Path $installDir "bin")
Write-Ok "Bun installed via direct download: $bunExe ($($versionOutput.Trim()))"
return $bunExe
}
Write-Warn "Downloaded Bun at $bunExe did not respond to --version; install incomplete"
}
} catch {
Write-Warn "Direct download failed: $($_.Exception.Message)"
} finally {
Remove-Item -Force $tmpZip -ErrorAction SilentlyContinue
}

Write-Err "Could not install Bun. OpenTUI TUI will not work."
Write-Info "Manual install: download from https://bun.com/docs/installation/windows"
Write-Info "Or re-run this script with -SkipBun to install with Node only (text CLI fallback)."
return $null
}

Set-Location (Join-Path $PSScriptRoot "..")
$RepoRoot = (Get-Location).Path
$InstallDir = Join-Path $HOME ".step-cli\bin"
Expand All @@ -115,11 +207,12 @@ if ($Uninstall) {
Remove-UserPath $InstallDir
Write-Ok "Removed $InstallDir and its user PATH entry"
Write-Info "Config and session history under $HOME\.step-cli are preserved."
Write-Info "Bun installation (under $HOME\.bun) is preserved; uninstall via winget or by deleting the directory."
exit 0
}

if (-not (Test-Command "pnpm")) {
Write-Section "[0/7] Bootstrapping pnpm via corepack"
Write-Section "[0/8] Bootstrapping pnpm via corepack"
if (-not (Test-Command "corepack")) {
Write-Err "Neither pnpm nor corepack was found in PATH."
Write-Info "Install Node.js 20+ and then re-run this script."
Expand All @@ -135,15 +228,43 @@ if (-not (Test-Command "pnpm")) {
Write-Ok "pnpm $(pnpm --version) ready"
}

Write-Section "[1/7] Installing workspace dependencies"
Write-Section "[1/8] Installing workspace dependencies"
if ($SkipInstall) {
Write-Info "Skipped (-SkipInstall)"
} else {
pnpm install
Write-Ok "Workspace dependencies installed"
}

Write-Section "[2/7] Initializing user config"
$BunBin = $null
if ($SkipBun) {
Write-Section "[2/8] Bun (skipped via -SkipBun)"
Write-Warn "OpenTUI TUI will be unavailable; step will fall back to text CLI."
} else {
Write-Section "[2/8] Bun runtime (for OpenTUI TUI)"
$BunBin = Install-Bun
if ($BunBin) {
# WinGet installs Bun as an "app execution alias" — a zero-byte reparse
# point under %LOCALAPPDATA%\Microsoft\WinGet\Links\bun.exe. PowerShell
# and cmd.exe resolve it via shell APIs, but Node's child_process.spawn
# uses CreateProcess directly and gets ENOENT. Setting STEP_BUN_BIN to
# such a shim breaks Invoke-StepCli (which spawns via Node). When we
# detect a shim, leave STEP_BUN_BIN unset: Node spawns fall back to tsx
# and the installed step.cmd launcher resolves Bun via `where bun`.
if ($BunBin -match "WinGet\\Links\\") {
Write-Ok "Bun available at $BunBin (WinGet app execution alias)"
Write-Info "STEP_BUN_BIN not set — Node spawn cannot use WinGet aliases directly."
Write-Info "step.cmd launcher will resolve Bun via PATH (cmd handles aliases)."
} else {
$env:STEP_BUN_BIN = $BunBin
Write-Ok "STEP_BUN_BIN set to $BunBin"
}
} else {
Write-Warn "Proceeding without Bun. OpenTUI TUI will use Node fallback."
}
}

Write-Section "[3/8] Initializing user config"
if ((Test-Path $UserConfig) -and (-not $ForceConfig)) {
Write-Ok "Config already exists at $UserConfig (use -ForceConfig to overwrite)"
} else {
Expand All @@ -155,7 +276,7 @@ if ((Test-Path $UserConfig) -and (-not $ForceConfig)) {
Write-Ok "Config written to $UserConfig"
}

Write-Section "[3/7] Silero VAD"
Write-Section "[4/8] Silero VAD"
pnpm setup:silero
if ($LASTEXITCODE -ne 0) {
throw "setup:silero failed with exit code $LASTEXITCODE"
Expand All @@ -164,7 +285,7 @@ Invoke-StepCli vad set silero
Invoke-StepCli vad status
Write-Ok "Silero enabled (voice.defaults.vad = silero)"

Write-Section "[4/7] Browser audio / AEC"
Write-Section "[5/8] Browser audio / AEC"
$Chrome = Find-Chrome
if ($Chrome) {
Write-Ok "Chrome/Chromium found: $Chrome"
Expand Down Expand Up @@ -196,15 +317,15 @@ if ($Overseas) {
Write-Ok "Patched $UserConfig for api.stepfun.ai"
}

Write-Section "[5/7] Building production bundle"
Write-Section "[6/8] Building production bundle"
if ($SkipBuild) {
Write-Warn "Skipped (-SkipBuild). Will reuse existing dist."
} else {
pnpm build
Write-Ok "dist built"
}

Write-Section "[6/7] Preparing Windows launcher"
Write-Section "[7/8] Preparing Windows launcher"
if ($SkipBuild) {
if (-not (Test-Path "dist\index.js")) {
Write-Err "No existing dist\index.js found; cannot continue with -SkipBuild."
Expand All @@ -215,7 +336,7 @@ if ($SkipBuild) {
Write-Info "Using a Node-based step.cmd launcher; Bun native compilation is not required on Windows."
}

Write-Section "[7/7] Installing to $InstallDir"
Write-Section "[8/8] Installing to $InstallDir"
New-Item -ItemType Directory -Force $InstallDir | Out-Null

foreach ($dir in @("package.json", "bin", "dist", "packages", "extensions", "skills", "node_modules")) {
Expand All @@ -233,8 +354,20 @@ $Launcher = Join-Path $InstallDir "step.cmd"
$LauncherContent = @"
@echo off
setlocal
pushd "%~dp0" || exit /b 1
if defined STEP_BUN_BIN (
"%STEP_BUN_BIN%" "%~dp0bin\step-cli.js" %*
goto :exit
)
where bun >nul 2>&1 && (
bun "%~dp0bin\step-cli.js" %*
goto :exit
)
node "%~dp0bin\step-cli.js" %*
exit /b %ERRORLEVEL%
:exit
set "STEP_EXIT_CODE=%ERRORLEVEL%"
popd
exit /b %STEP_EXIT_CODE%
"@
Set-Content -Encoding ASCII -Path $Launcher -Value $LauncherContent
Write-Ok "Installed launcher: $Launcher"
Expand Down
13 changes: 13 additions & 0 deletions scripts/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,16 @@ describe("scripts/setup.sh", () => {
},
);
});

describe("scripts/setup.ps1", () => {
it("runs the launcher from its install directory before resolving runtime imports", () => {
const script = readFileSync(
join(process.cwd(), "scripts", "setup.ps1"),
"utf8",
);

expect(script).toContain('pushd "%~dp0" || exit /b 1');
expect(script).toContain('set "STEP_EXIT_CODE=%ERRORLEVEL%"');
expect(script).toContain("popd");
});
});
7 changes: 6 additions & 1 deletion src/commands/resume-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { resolveStepCliRuntimeConfig } from "../runtime/runtime-config.js";
import { createLocalCliClientApp } from "../runtime/local-cli-app.js";
import {
isOpenTuiEnabledInCurrentBuild,
isOpenTuiRuntimeSupported,
loadOpenTuiClientAppFactoryAtRuntime,
warnWhenOpenTuiRuntimeUnsupported,
} from "../runtime/open-tui-capability.js";

// Keep this build flag in the command module so rolldown can fold the bundle's
Expand Down Expand Up @@ -42,12 +44,15 @@ export async function runResumeCommand(argv: string[]): Promise<void> {
actionCommand: Command,
) => {
const cliOptionSources = readSharedRuntimeCliOptionSources(actionCommand);
const shouldUseTui =
const isTuiOtherwiseEligible =
OPEN_TUI_COMPILE_TIME_ENABLED &&
isOpenTuiEnabledInCurrentBuild() &&
!options.json &&
process.stdin.isTTY === true &&
process.stdout.isTTY === true;
const shouldUseTui =
isTuiOtherwiseEligible && isOpenTuiRuntimeSupported();
warnWhenOpenTuiRuntimeUnsupported(isTuiOtherwiseEligible);
const { stepCliConfig } = await resolveStepCliRuntimeConfig({
options,
cliOptionSources,
Expand Down
77 changes: 77 additions & 0 deletions src/commands/root-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { shouldUseTui } from "./root-command.js";

const originalBunVersion = process.versions.bun;

beforeEach(() => {
delete (process.versions as { bun?: string }).bun;
});

afterEach(() => {
if (originalBunVersion === undefined) {
delete (process.versions as { bun?: string }).bun;
} else {
(process.versions as { bun?: string }).bun = originalBunVersion;
}
});

describe("shouldUseTui", () => {
it("returns false on Node.js even when all other conditions hold", () => {
const result = shouldUseTui({
options: {},
prompt: undefined,
attachments: undefined,
stdinIsTTY: true,
stdoutIsTTY: true,
});
expect(result).toBe(false);
});

it("returns true on Bun when stdin/stdout are TTY and no prompt/attachments/json", () => {
(process.versions as { bun?: string }).bun = "1.1.0";
const result = shouldUseTui({
options: {},
prompt: undefined,
attachments: undefined,
stdinIsTTY: true,
stdoutIsTTY: true,
});
expect(result).toBe(true);
});

it("returns false when --json is passed (even on Bun)", () => {
(process.versions as { bun?: string }).bun = "1.1.0";
const result = shouldUseTui({
options: { json: true },
prompt: undefined,
attachments: undefined,
stdinIsTTY: true,
stdoutIsTTY: true,
});
expect(result).toBe(false);
});

it("returns false when a prompt is provided (one-shot mode)", () => {
(process.versions as { bun?: string }).bun = "1.1.0";
const result = shouldUseTui({
options: {},
prompt: "summarize src/index.ts",
attachments: undefined,
stdinIsTTY: true,
stdoutIsTTY: true,
});
expect(result).toBe(false);
});

it("returns false when stdin is not a TTY (piped input)", () => {
(process.versions as { bun?: string }).bun = "1.1.0";
const result = shouldUseTui({
options: {},
prompt: undefined,
attachments: undefined,
stdinIsTTY: false,
stdoutIsTTY: true,
});
expect(result).toBe(false);
});
});
Loading
Loading