Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7062645bcb | ||
|
|
bb90a927aa |
No files matched your search
@@ -20,6 +20,17 @@ is only released once its **exit criterion** is met, not when its code lands.
|
||||
Until v1.0.0 the minor version is the breaking one: configuration formats and Go
|
||||
APIs change between minors without a deprecation period.
|
||||
|
||||
## v0.10.2 — 2026-09-29
|
||||
|
||||
- After `server init`'s screen closes, only the administrator's password and
|
||||
the next steps are printed, not the whole run again under the screen that
|
||||
just showed it. The whole run is kept in `/var/lib/llmgw/init.log`, root's
|
||||
only, and printed in full when init fails.
|
||||
- The screens' spinner and text field are llmgw's own rather than from
|
||||
bubbles, one dependency fewer. Every llmgw command still asks the terminal
|
||||
for its colours as it starts, which bubbletea v1 does on purpose; a terminal
|
||||
that never answers waits for its timeout.
|
||||
|
||||
## v0.10.1 — 2026-09-29
|
||||
|
||||
### `llmgw ctl fabric-test`: two nodes, rail by rail
|
||||
|
||||
@@ -6,7 +6,6 @@ require (
|
||||
connectrpc.com/connect v1.21.0
|
||||
github.com/NVIDIA/go-nvml v0.13.4-0
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
@@ -28,7 +27,6 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
|
||||
@@ -2,16 +2,12 @@ connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs=
|
||||
connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
github.com/NVIDIA/go-nvml v0.13.4-0 h1:o3jp9u2x1R9ShFE3v+Aesp55XOSIQFMJz/VGNUcJaNE=
|
||||
github.com/NVIDIA/go-nvml v0.13.4-0/go.mod h1:id63qwpoDWpFXwnwM6psDCSqW4BmNu6mWpr4YeQtPGo=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
|
||||
+33
-15
@@ -5,11 +5,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
@@ -65,8 +65,8 @@ type initModel struct {
|
||||
// The form, until it is submitted.
|
||||
form bool
|
||||
focus int
|
||||
admin textinput.Model
|
||||
hosts textinput.Model
|
||||
admin textField
|
||||
hosts textField
|
||||
toggles [fCount]bool
|
||||
formError string
|
||||
|
||||
@@ -75,7 +75,7 @@ type initModel struct {
|
||||
lines []string // init's lines, as it wrote them
|
||||
password string
|
||||
next []string // the closing "Next:" text
|
||||
spin spinner.Model
|
||||
spin spinnerModel
|
||||
width int
|
||||
done bool
|
||||
err error
|
||||
@@ -84,19 +84,17 @@ type initModel struct {
|
||||
}
|
||||
|
||||
func newInitModel(in *serverInit, found []string, askForm bool) initModel {
|
||||
admin := textinput.New()
|
||||
admin := newTextField()
|
||||
admin.Placeholder = "you@example.com"
|
||||
admin.SetValue(in.admin)
|
||||
admin.CharLimit = 200
|
||||
admin.Width = 40
|
||||
hosts := textinput.New()
|
||||
hosts := newTextField()
|
||||
hosts.Placeholder = "llm.example.com, 203.0.113.10"
|
||||
hosts.SetValue(strings.Join(in.hosts, ", "))
|
||||
hosts.CharLimit = 400
|
||||
hosts.Width = 40
|
||||
sp := spinner.New()
|
||||
sp.Spinner = spinner.MiniDot
|
||||
sp.Style = lipgloss.NewStyle().Foreground(tuiAccent)
|
||||
sp := newSpinner()
|
||||
m := initModel{in: in, found: found, form: askForm, admin: admin, hosts: hosts, spin: sp, width: 100}
|
||||
m.toggles[fPostgres] = in.postgres
|
||||
m.toggles[fMirror] = !in.noMirror
|
||||
@@ -110,7 +108,7 @@ func newInitModel(in *serverInit, found []string, askForm bool) initModel {
|
||||
|
||||
func (m initModel) Init() tea.Cmd {
|
||||
if m.form {
|
||||
return textinput.Blink
|
||||
return nil
|
||||
}
|
||||
return tea.Batch(m.spin.Tick, func() tea.Msg { return startInitMsg{} })
|
||||
}
|
||||
@@ -121,7 +119,7 @@ func (m initModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
case spinner.TickMsg:
|
||||
case spinTickMsg:
|
||||
var cmd tea.Cmd
|
||||
m.spin, cmd = m.spin.Update(msg)
|
||||
return m, cmd
|
||||
@@ -391,13 +389,33 @@ func (in *serverInit) runInitTUI(ctx context.Context, out io.Writer) error {
|
||||
return fmt.Errorf("stopped; run init again to finish what was started")
|
||||
}
|
||||
|
||||
// The screen showed the last lines; the whole run goes to the terminal's
|
||||
// history, the password with it.
|
||||
// The whole run is kept, root's only, since it holds the password. On a
|
||||
// failure it is printed too, for the lines the screen had no room for;
|
||||
// otherwise the screen's last frame says it, and only what must outlive
|
||||
// the screen is printed again: the password, and what to do next.
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
whole := strings.Join(log, "\n") + "\n"
|
||||
saved := ""
|
||||
if in.stateDir != "" {
|
||||
p := filepath.Join(in.stateDir, "init.log")
|
||||
if err := os.WriteFile(p, []byte(whole), 0o600); err == nil {
|
||||
saved = p
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
for _, l := range log {
|
||||
if fm.err != nil {
|
||||
fmt.Fprint(out, whole)
|
||||
return fm.err
|
||||
}
|
||||
if fm.password != "" {
|
||||
fmt.Fprintf(out, "Administrator %s, password %s (shown once)\n\n", in.admin, fm.password)
|
||||
}
|
||||
for _, l := range fm.next {
|
||||
fmt.Fprintln(out, l)
|
||||
}
|
||||
if saved != "" {
|
||||
fmt.Fprintf(out, "\nThe whole run is in %s.\n", saved)
|
||||
}
|
||||
return fm.err
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-isatty"
|
||||
@@ -67,7 +65,7 @@ type nodeModel struct {
|
||||
title string
|
||||
steps map[string]stepState
|
||||
notes []onboard.Event // warnings and failures, with their fixes
|
||||
spin spinner.Model
|
||||
spin spinnerModel
|
||||
cancel context.CancelFunc
|
||||
width int
|
||||
done bool
|
||||
@@ -77,16 +75,14 @@ type nodeModel struct {
|
||||
|
||||
// The question being asked, if any.
|
||||
fabrics *askFabricsMsg
|
||||
inputs []textinput.Model
|
||||
inputs []textField
|
||||
focus int
|
||||
confirm *askContinueMsg
|
||||
quitAsked bool
|
||||
}
|
||||
|
||||
func newNodeModel(title string, cancel context.CancelFunc) nodeModel {
|
||||
sp := spinner.New()
|
||||
sp.Spinner = spinner.MiniDot
|
||||
sp.Style = lipgloss.NewStyle().Foreground(tuiAccent)
|
||||
sp := newSpinner()
|
||||
return nodeModel{title: title, steps: map[string]stepState{}, spin: sp, cancel: cancel, width: 100}
|
||||
}
|
||||
|
||||
@@ -96,7 +92,7 @@ func (m nodeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
case spinner.TickMsg:
|
||||
case spinTickMsg:
|
||||
var cmd tea.Cmd
|
||||
m.spin, cmd = m.spin.Update(msg)
|
||||
return m, cmd
|
||||
@@ -121,7 +117,7 @@ func (m nodeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.fabrics = &msg
|
||||
m.inputs = nil
|
||||
for i, f := range msg.suggested {
|
||||
in := textinput.New()
|
||||
in := newTextField()
|
||||
in.Prompt = ""
|
||||
in.SetValue(f.Name)
|
||||
in.CharLimit = 40
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Two small widgets, written here rather than taken from bubbles: its
|
||||
// packages ask the terminal for its colours as they load, so every llmgw
|
||||
// command — llmgw --version included — sent a query and, in a terminal that
|
||||
// never answers one, waited five seconds for it. Only the screens need to
|
||||
// know the colours, and lipgloss asks when they first draw.
|
||||
|
||||
// spinTickMsg advances a spinner.
|
||||
type spinTickMsg struct{}
|
||||
|
||||
var spinFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||
|
||||
type spinnerModel struct {
|
||||
frame int
|
||||
Style lipgloss.Style
|
||||
}
|
||||
|
||||
func newSpinner() spinnerModel { return spinnerModel{Style: lipgloss.NewStyle().Foreground(tuiAccent)} }
|
||||
|
||||
// Tick starts the spinner turning; it is a tea.Cmd.
|
||||
func (s spinnerModel) Tick() tea.Msg { return spinTickMsg{} }
|
||||
|
||||
func (s spinnerModel) Update(msg tea.Msg) (spinnerModel, tea.Cmd) {
|
||||
if _, ok := msg.(spinTickMsg); !ok {
|
||||
return s, nil
|
||||
}
|
||||
s.frame = (s.frame + 1) % len(spinFrames)
|
||||
return s, tea.Tick(100*time.Millisecond, func(time.Time) tea.Msg { return spinTickMsg{} })
|
||||
}
|
||||
|
||||
func (s spinnerModel) View() string { return s.Style.Render(spinFrames[s.frame]) }
|
||||
|
||||
// textField is one line of text typed at its end: all a form here needs.
|
||||
type textField struct {
|
||||
value []rune
|
||||
focused bool
|
||||
Prompt string
|
||||
Placeholder string
|
||||
CharLimit int
|
||||
Width int
|
||||
}
|
||||
|
||||
func newTextField() textField { return textField{Prompt: "> "} }
|
||||
|
||||
func (f *textField) SetValue(s string) { f.value = []rune(s) }
|
||||
func (f textField) Value() string { return string(f.value) }
|
||||
func (f *textField) Focus() { f.focused = true }
|
||||
func (f *textField) Blur() { f.focused = false }
|
||||
|
||||
func (f textField) Update(msg tea.Msg) (textField, tea.Cmd) {
|
||||
k, ok := msg.(tea.KeyMsg)
|
||||
if !ok || !f.focused {
|
||||
return f, nil
|
||||
}
|
||||
switch k.Type {
|
||||
case tea.KeyRunes, tea.KeySpace:
|
||||
r := k.Runes
|
||||
if k.Type == tea.KeySpace {
|
||||
r = []rune{' '}
|
||||
}
|
||||
for _, c := range r {
|
||||
if f.CharLimit == 0 || len(f.value) < f.CharLimit {
|
||||
f.value = append(f.value, c)
|
||||
}
|
||||
}
|
||||
case tea.KeyBackspace:
|
||||
if len(f.value) > 0 {
|
||||
f.value = f.value[:len(f.value)-1]
|
||||
}
|
||||
case tea.KeyCtrlU:
|
||||
f.value = nil
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f textField) View() string {
|
||||
text := string(f.value)
|
||||
shown := text
|
||||
if text == "" && f.Placeholder != "" {
|
||||
shown = sDim.Render(f.Placeholder)
|
||||
}
|
||||
if f.focused {
|
||||
shown += "█"
|
||||
}
|
||||
if pad := f.Width - len([]rune(text)); pad > 0 && text != "" {
|
||||
shown += strings.Repeat(" ", pad)
|
||||
}
|
||||
return f.Prompt + shown
|
||||
}
|
||||
Reference in new issue
Block a user