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
33 changes: 25 additions & 8 deletions cmd/asobi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -31,7 +32,7 @@ var (
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(1)
exit(1)
}

switch os.Args[1] {
Expand Down Expand Up @@ -80,10 +81,10 @@ func main() {
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
usage()
os.Exit(1)
exit(1)
}

update.Notify(version, os.Args[1], os.Stderr)
notifyUpdate()
}

func usage() {
Expand Down Expand Up @@ -355,7 +356,7 @@ func firstArg(args []string) string {
func cmdConfig() {
if len(os.Args) < 3 {
fmt.Println("Usage: asobi config <set|show>")
os.Exit(1)
exit(1)
}

switch os.Args[2] {
Expand All @@ -376,7 +377,7 @@ func cmdConfig() {
if len(os.Args) < 5 {
fmt.Println("Usage: asobi config set <key> <value>")
fmt.Println("Keys: url, api_key, saas_url")
os.Exit(1)
exit(1)
}
key, value := os.Args[3], os.Args[4]
cfg, err := config.Load()
Expand All @@ -401,7 +402,7 @@ func cmdConfig() {

default:
fmt.Println("Usage: asobi config <set|show>")
os.Exit(1)
exit(1)
}
}

Expand Down Expand Up @@ -433,7 +434,23 @@ func startSpinner() func() {

func fatal(format string, args ...any) {
fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...)
os.Exit(1)
exit(1)
}

// exit is the single process-exit chokepoint. It surfaces the update notice
// before terminating, so a failing command (which reaches os.Exit via fatal,
// bypassing any deferred main-level notice) still nudges the user to upgrade.
func exit(code int) {
notifyUpdate()
os.Exit(code)
}

func notifyUpdate() { notifyUpdateTo(os.Stderr) }

func notifyUpdateTo(w io.Writer) {
if len(os.Args) > 1 {
update.Notify(version, os.Args[1], w)
}
}

// --- Destroy ---
Expand All @@ -460,7 +477,7 @@ func cmdDestroy() {
func cmdEnv() {
if len(os.Args) < 3 {
fmt.Println("Usage: asobi env list [--ephemeral] [--json]")
os.Exit(1)
exit(1)
}

switch os.Args[2] {
Expand Down
39 changes: 38 additions & 1 deletion cmd/asobi/main_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
package main

import "testing"
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"
)

// A command that exits via fatal() must still surface the upgrade notice:
// os.Exit skips deferred main-level notices, so failing commands route
// through exit() -> notifyUpdate. This drives that wiring off a seeded
// cache so it never touches the network.
func TestNotifyUpdateWiring(t *testing.T) {
dir := t.TempDir()
t.Setenv("HOME", dir)
t.Setenv("ASOBI_NO_UPDATE_CHECK", "")
t.Setenv("CI", "")
asobiDir := filepath.Join(dir, ".asobi")
if err := os.MkdirAll(asobiDir, 0o700); err != nil {
t.Fatal(err)
}
cache := `{"checked_at":"` + time.Now().Format(time.RFC3339) + `","latest":"v9.9.9"}`
if err := os.WriteFile(filepath.Join(asobiDir, "version_check.json"), []byte(cache), 0o600); err != nil {
t.Fatal(err)
}

oldVersion, oldArgs := version, os.Args
version = "v0.0.1"
os.Args = []string{"asobi", "health"}
t.Cleanup(func() { version, os.Args = oldVersion, oldArgs })

var buf bytes.Buffer
notifyUpdateTo(&buf)
if !strings.Contains(buf.String(), "v9.9.9") {
t.Errorf("failing-command exit path did not surface upgrade notice; got %q", buf.String())
}
}

func TestIsHealthy(t *testing.T) {
for _, s := range []string{"ok", "healthy"} {
Expand Down