Compare commits

...
Author SHA1 Message Date
Vasanthdev2004 1febcb8168 fix(tui): use term.IsTerminal for the stdin guard; pin exit code in test
Address review on the no-TTY guard:
- Replace the os.Stdin.Stat()/ModeCharDevice heuristic with term.IsTerminal
  (github.com/charmbracelet/x/term, already in the module graph via bubbletea).
  It is a true TTY check — it rejects pipes, regular files, and non-terminal
  char devices like /dev/null — and fails closed: anything not a verified
  terminal blocks the interactive shell.
- The test now asserts the documented exit code 2 from the guard, not just a
  non-zero result, so it proves the stdin guard fired.
2026-06-28 16:12:14 +05:30
Vasanthdev2004 36d45b96ab chore(launch): MIT license field, windows-arm64 install guard, TUI TTY guard
Three launch-readiness fixes the docs cleanup doesn't cover:

- package.json: set "license": "MIT" (was the placeholder "SEE LICENSE IN
  LICENSE"), matching the LICENSE file and the README badge.
- scripts/postinstall.mjs: skip gracefully on windows-arm64. (win32, arm64)
  resolves to a valid platform/arch but the release matrix ships no
  windows-arm64 artifact, so the installer would download a 404 and hard-fail
  npm install. Treat it like any unsupported target (warnSkip, exit 0) and point
  to the x64 build (which runs under emulation on Windows on ARM) or source.
- internal/tui/run.go: fail fast when stdin is not a terminal. The interactive
  shell otherwise blocks forever in the Bubble Tea event loop on piped/redirected
  input (e.g. `echo "" | zero`); now it prints guidance toward `zero exec` and
  exits non-zero. Dependency-free ModeCharDevice check (no x/term added).

Tests: windows-arm64 skips with exit 0; the interactive shell returns non-zero
(not a hang) on non-TTY stdin.
2026-06-28 16:05:12 +05:30
6 changed files with 81 additions and 3 deletions

No files matched your search

+1 -1
View File
@@ -13,6 +13,7 @@ require (
github.com/aymanbagabas/go-udiff v0.4.1
github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/x/ansi v0.11.7
github.com/charmbracelet/x/term v0.2.2
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728
golang.org/x/sys v0.46.0
mvdan.cc/sh/v3 v3.13.1
@@ -21,7 +22,6 @@ require (
require (
github.com/charmbracelet/ultraviolet v0.0.0-20260615092913-2399af76d5b1 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20260615092313-b57e5e6d29bb // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
+20
View File
@@ -140,6 +140,26 @@ func TestPostinstallSkipsUnsupportedPlatform(t *testing.T) {
}
}
func TestPostinstallSkipsWindowsArm64(t *testing.T) {
// (win32, arm64) resolves to a valid platform/arch but the release matrix has
// no windows-arm64 artifact, so the install must skip gracefully (exit 0)
// rather than hard-fail on a 404 download.
stdout, stderr, err := runPostinstall(t,
"ZERO_INSTALL_DRY_RUN=1",
"ZERO_INSTALL_PLATFORM=win32",
"ZERO_INSTALL_ARCH=arm64",
)
if err != nil {
t.Fatalf("windows-arm64 should exit 0, got err=%v stderr=%s", err, stderr)
}
if strings.TrimSpace(stdout) != "" {
t.Fatalf("windows-arm64 should not print a plan, got %q", stdout)
}
if !strings.Contains(stderr, "no prebuilt binary for windows-arm64") {
t.Fatalf("stderr=%q, want the windows-arm64 skip message", stderr)
}
}
func TestPostinstallHonorsSkipEnv(t *testing.T) {
stdout, stderr, err := runPostinstall(t, "ZERO_SKIP_DOWNLOAD=1")
if err != nil {
+12
View File
@@ -7,10 +7,22 @@ import (
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/x/term"
)
// Run starts the Zero Bubble Tea shell and returns a process-style exit code.
func Run(ctx context.Context, options Options) int {
// The interactive shell needs a real terminal on stdin: with piped or
// redirected input Bubble Tea blocks forever waiting for events that never
// arrive (e.g. `echo "" | zero`). Fail fast with guidance toward the headless
// path instead of hanging. term.IsTerminal is a true TTY check (it rejects
// pipes, regular files, and non-terminal char devices like /dev/null) and
// fails closed — anything that is not a verified terminal blocks the shell.
if !term.IsTerminal(os.Stdin.Fd()) {
fmt.Fprintln(os.Stderr, "zero: the interactive shell needs a terminal (stdin is not a TTY). For non-interactive use, run: zero exec \"<prompt>\"")
return 2
}
externalSink := options.RuntimeMessageSink
var program *tea.Program
options.RuntimeMessageSink = func(msg tea.Msg) {
+34 -1
View File
@@ -1,6 +1,11 @@
package tui
import "testing"
import (
"context"
"os"
"testing"
"time"
)
func TestUseAltScreenForInteractiveChat(t *testing.T) {
if !useAltScreen(Options{}) {
@@ -10,3 +15,31 @@ func TestUseAltScreenForInteractiveChat(t *testing.T) {
t.Fatal("setup takeover should also use the alternate screen")
}
}
// TestRunRejectsNonTTYStdin pins that the interactive shell fails fast with a
// non-zero code when stdin is not a terminal, instead of blocking forever in the
// Bubble Tea event loop (e.g. `echo "" | zero`). The guard runs before any model
// construction, so empty Options are fine.
func TestRunRejectsNonTTYStdin(t *testing.T) {
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe: %v", err)
}
w.Close() // a pipe read-end is not a character device
old := os.Stdin
os.Stdin = r
defer func() { os.Stdin = old; r.Close() }()
done := make(chan int, 1)
go func() { done <- Run(context.Background(), Options{}) }()
select {
case code := <-done:
if code != 2 {
t.Fatalf("Run with non-TTY stdin returned %d; want exit code 2 from the stdin TTY guard", code)
}
case <-time.After(5 * time.Second):
t.Fatal("Run blocked on non-TTY stdin instead of failing fast")
}
}
+1 -1
View File
@@ -38,5 +38,5 @@
"bugs": {
"url": "https://github.com/Gitlawb/zero/issues"
},
"license": "SEE LICENSE IN LICENSE"
"license": "MIT"
}
+13
View File
@@ -102,6 +102,19 @@ if (!platform || !arch) {
);
}
// The release matrix builds linux/macos {x64,arm64} and windows-x64 — there is no
// windows-arm64 artifact. (win32, arm64) otherwise resolves to a valid
// platform/arch, so it would proceed to download a non-existent asset and hard
// -fail npm install on a 404. Guard it explicitly to skip gracefully; Windows on
// ARM runs the x64 build under emulation, so the x64 package is the fallback.
if (platform === 'windows' && arch === 'arm64') {
warnSkip(
`no prebuilt binary for windows-arm64 (the windows-x64 build runs under ` +
`emulation on Windows on ARM). Build from source or install the x64 package: ` +
`https://github.com/${REPO} (go run ./cmd/zero).`,
);
}
const ext = platform === 'windows' ? 'zip' : 'tar.gz';
const binaryName = platform === 'windows' ? 'zero.exe' : 'zero';
const tag = `v${VERSION}`;