Skip to content

Commit db04de9

Browse files
committed
[FIX] TUI.
1 parent a0dbe00 commit db04de9

4 files changed

Lines changed: 199 additions & 82 deletions

File tree

internal/engine/install/engine.go

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1906,6 +1906,9 @@ func fetchSystemStatus(ctx context.Context) (domain.SystemStatus, error) {
19061906
return domain.SystemStatus{}, err
19071907
}
19081908

1909+
ctx, cancel := context.WithTimeout(ctx, 25*time.Second)
1910+
defer cancel()
1911+
19091912
cmd := exec.CommandContext(ctx, "php", entry, "system-status", "--format=json", "--no-ansi", "--no-interaction")
19101913
var stderr bytes.Buffer
19111914
cmd.Stderr = &stderr
@@ -1958,22 +1961,70 @@ func fetchSystemStatus(ctx context.Context) (domain.SystemStatus, error) {
19581961
}
19591962

19601963
func findPHPInstallerCLIEntry() (string, error) {
1961-
candidates := []string{
1964+
candidates := []string{}
1965+
1966+
// Prefer the bootstrapper on PATH (installed alongside the Go binary).
1967+
if p, err := exec.LookPath("evo"); err == nil && p != "" {
1968+
candidates = append(candidates, p)
1969+
}
1970+
1971+
// Prefer a sibling `evo` script next to the running executable (common install layout:
1972+
// `evo` bootstrapper + `evo.bin` Go binary in the same directory).
1973+
if exe, err := os.Executable(); err == nil && exe != "" {
1974+
exeDir := filepath.Dir(exe)
1975+
if exeDir != "" && exeDir != "." {
1976+
candidates = append(candidates, filepath.Join(exeDir, "evo"))
1977+
}
1978+
}
1979+
1980+
// Repo-local fallbacks (when running from source checkout).
1981+
candidates = append(candidates,
19621982
filepath.Join("installer", "bin", "evo"),
19631983
filepath.Join("bin", "evo"),
1964-
}
1984+
)
19651985
for _, p := range candidates {
1966-
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
1967-
abs, absErr := filepath.Abs(p)
1968-
if absErr != nil {
1969-
return p, nil
1970-
}
1971-
return abs, nil
1986+
if strings.TrimSpace(p) == "" {
1987+
continue
1988+
}
1989+
fi, err := os.Stat(p)
1990+
if err != nil || fi.IsDir() {
1991+
continue
19721992
}
1993+
if !looksLikePHPScript(p) {
1994+
continue
1995+
}
1996+
1997+
abs, absErr := filepath.Abs(p)
1998+
if absErr != nil {
1999+
return p, nil
2000+
}
2001+
return abs, nil
19732002
}
19742003
return "", fmt.Errorf("unable to find installer PHP CLI entry (tried: %s)", strings.Join(candidates, ", "))
19752004
}
19762005

2006+
func looksLikePHPScript(path string) bool {
2007+
f, err := os.Open(path)
2008+
if err != nil {
2009+
return false
2010+
}
2011+
defer f.Close()
2012+
2013+
buf := make([]byte, 256)
2014+
n, _ := f.Read(buf)
2015+
if n <= 0 {
2016+
return false
2017+
}
2018+
head := strings.ToLower(string(buf[:n]))
2019+
if strings.Contains(head, "<?php") {
2020+
return true
2021+
}
2022+
if strings.HasPrefix(head, "#!") && strings.Contains(head, "php") {
2023+
return true
2024+
}
2025+
return false
2026+
}
2027+
19772028
func detectExistingEvoInstall(dir string) (bool, string) {
19782029
dir = strings.TrimSpace(dir)
19792030
if dir == "" {

internal/engine/mock/engine.go

Lines changed: 122 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, _ <-chan domai
151151
Severity: domain.SeverityWarn,
152152
Payload: domain.StepDonePayload{OK: false},
153153
})
154-
} else {
154+
} else {
155155
tag := releaseInfo.Tag
156156
if tag == "" && releaseInfo.HighestVersion != "" {
157157
tag = "v" + releaseInfo.HighestVersion
@@ -180,83 +180,83 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, _ <-chan domai
180180
Unit: "pct",
181181
},
182182
})
183-
_ = emit(domain.Event{
184-
Type: domain.EventStepDone,
185-
StepID: releaseStepID,
186-
Source: "mock",
187-
Severity: domain.SeverityInfo,
188-
Payload: releaseInfo,
189-
})
190-
}
183+
_ = emit(domain.Event{
184+
Type: domain.EventStepDone,
185+
StepID: releaseStepID,
186+
Source: "mock",
187+
Severity: domain.SeverityInfo,
188+
Payload: releaseInfo,
189+
})
190+
}
191+
192+
// Step: check system status via PHP adapter.
193+
const sysStepID = "check_system_status"
194+
_ = emit(domain.Event{
195+
Type: domain.EventStepStart,
196+
StepID: sysStepID,
197+
Source: "mock",
198+
Severity: domain.SeverityInfo,
199+
Payload: domain.StepStartPayload{
200+
Label: "Check system status",
201+
Index: 0,
202+
Total: 0,
203+
},
204+
})
205+
_ = emit(domain.Event{
206+
Type: domain.EventLog,
207+
StepID: sysStepID,
208+
Source: "mock",
209+
Severity: domain.SeverityInfo,
210+
Payload: domain.LogPayload{
211+
Message: "Checking system status…",
212+
},
213+
})
191214

192-
// Step: check system status via PHP adapter.
193-
const sysStepID = "check_system_status"
215+
status, err := fetchSystemStatus(ctx)
216+
if err != nil {
194217
_ = emit(domain.Event{
195-
Type: domain.EventStepStart,
218+
Type: domain.EventWarning,
196219
StepID: sysStepID,
197220
Source: "mock",
198-
Severity: domain.SeverityInfo,
199-
Payload: domain.StepStartPayload{
200-
Label: "Check system status",
201-
Index: 0,
202-
Total: 0,
221+
Severity: domain.SeverityWarn,
222+
Payload: domain.LogPayload{
223+
Message: "Unable to check system status; continuing…",
224+
Fields: map[string]string{"error": err.Error()},
203225
},
204226
})
205227
_ = emit(domain.Event{
206-
Type: domain.EventLog,
228+
Type: domain.EventSystemStatus,
207229
StepID: sysStepID,
208230
Source: "mock",
209-
Severity: domain.SeverityInfo,
210-
Payload: domain.LogPayload{
211-
Message: "Checking system status…",
231+
Severity: domain.SeverityWarn,
232+
Payload: domain.SystemStatus{
233+
Items: nil,
234+
UpdatedAt: time.Now(),
212235
},
213236
})
214-
215-
status, err := fetchSystemStatus(ctx)
216-
if err != nil {
217-
_ = emit(domain.Event{
218-
Type: domain.EventWarning,
219-
StepID: sysStepID,
220-
Source: "mock",
221-
Severity: domain.SeverityWarn,
222-
Payload: domain.LogPayload{
223-
Message: "Unable to check system status; continuing…",
224-
Fields: map[string]string{"error": err.Error()},
225-
},
226-
})
227-
_ = emit(domain.Event{
228-
Type: domain.EventSystemStatus,
229-
StepID: sysStepID,
230-
Source: "mock",
231-
Severity: domain.SeverityWarn,
232-
Payload: domain.SystemStatus{
233-
Items: nil,
234-
UpdatedAt: time.Now(),
235-
},
236-
})
237-
_ = emit(domain.Event{
238-
Type: domain.EventStepDone,
239-
StepID: sysStepID,
240-
Source: "mock",
241-
Severity: domain.SeverityWarn,
242-
Payload: domain.StepDonePayload{OK: false},
243-
})
244-
} else {
245-
_ = emit(domain.Event{
246-
Type: domain.EventSystemStatus,
247-
StepID: sysStepID,
248-
Source: "mock",
249-
Severity: domain.SeverityInfo,
250-
Payload: status,
251-
})
252-
_ = emit(domain.Event{
253-
Type: domain.EventStepDone,
254-
StepID: sysStepID,
255-
Source: "mock",
256-
Severity: domain.SeverityInfo,
257-
Payload: domain.StepDonePayload{OK: true},
258-
})
259-
}
237+
_ = emit(domain.Event{
238+
Type: domain.EventStepDone,
239+
StepID: sysStepID,
240+
Source: "mock",
241+
Severity: domain.SeverityWarn,
242+
Payload: domain.StepDonePayload{OK: false},
243+
})
244+
} else {
245+
_ = emit(domain.Event{
246+
Type: domain.EventSystemStatus,
247+
StepID: sysStepID,
248+
Source: "mock",
249+
Severity: domain.SeverityInfo,
250+
Payload: status,
251+
})
252+
_ = emit(domain.Event{
253+
Type: domain.EventStepDone,
254+
StepID: sysStepID,
255+
Source: "mock",
256+
Severity: domain.SeverityInfo,
257+
Payload: domain.StepDonePayload{OK: true},
258+
})
259+
}
260260

261261
// A sample question to drive UI selection (engine does not consume the answer yet).
262262
_ = emit(domain.Event{
@@ -369,7 +369,7 @@ func (e *Engine) Run(ctx context.Context, ch chan<- domain.Event, _ <-chan domai
369369
StepID: s.id,
370370
Source: "mock",
371371
Severity: domain.SeverityInfo,
372-
Payload: domain.StepDonePayload{OK: true},
372+
Payload: domain.StepDonePayload{OK: true},
373373
})
374374
}
375375
}()
@@ -389,7 +389,7 @@ func envInt(key string, def int) int {
389389

390390
type systemStatusJSON struct {
391391
Overall string `json:"overall"`
392-
Items []struct {
392+
Items []struct {
393393
Key string `json:"key"`
394394
Label string `json:"label"`
395395
Level string `json:"level"`
@@ -403,6 +403,9 @@ func fetchSystemStatus(ctx context.Context) (domain.SystemStatus, error) {
403403
return domain.SystemStatus{}, err
404404
}
405405

406+
ctx, cancel := context.WithTimeout(ctx, 25*time.Second)
407+
defer cancel()
408+
406409
cmd := exec.CommandContext(ctx, "php", entry, "system-status", "--format=json", "--no-ansi", "--no-interaction")
407410
var stderr bytes.Buffer
408411
cmd.Stderr = &stderr
@@ -455,14 +458,60 @@ func fetchSystemStatus(ctx context.Context) (domain.SystemStatus, error) {
455458
}
456459

457460
func findPHPInstallerCLIEntry() (string, error) {
458-
candidates := []string{
461+
candidates := []string{}
462+
463+
// Prefer the bootstrapper on PATH (installed alongside the Go binary).
464+
if p, err := exec.LookPath("evo"); err == nil && p != "" {
465+
candidates = append(candidates, p)
466+
}
467+
468+
// Prefer a sibling `evo` script next to the running executable (common install layout).
469+
if exe, err := os.Executable(); err == nil && exe != "" {
470+
exeDir := filepath.Dir(exe)
471+
if exeDir != "" && exeDir != "." {
472+
candidates = append(candidates, filepath.Join(exeDir, "evo"))
473+
}
474+
}
475+
476+
// Repo-local fallbacks (when running from source checkout).
477+
candidates = append(candidates,
459478
filepath.Join("installer", "bin", "evo"),
460479
filepath.Join("bin", "evo"),
461-
}
480+
)
462481
for _, p := range candidates {
463-
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
464-
return p, nil
482+
if strings.TrimSpace(p) == "" {
483+
continue
484+
}
485+
fi, err := os.Stat(p)
486+
if err != nil || fi.IsDir() {
487+
continue
465488
}
489+
if !looksLikePHPScript(p) {
490+
continue
491+
}
492+
return p, nil
466493
}
467494
return "", fmt.Errorf("unable to find installer PHP CLI entry (tried: %s)", strings.Join(candidates, ", "))
468495
}
496+
497+
func looksLikePHPScript(path string) bool {
498+
f, err := os.Open(path)
499+
if err != nil {
500+
return false
501+
}
502+
defer f.Close()
503+
504+
buf := make([]byte, 256)
505+
n, _ := f.Read(buf)
506+
if n <= 0 {
507+
return false
508+
}
509+
head := strings.ToLower(string(buf[:n]))
510+
if strings.Contains(head, "<?php") {
511+
return true
512+
}
513+
if strings.HasPrefix(head, "#!") && strings.Contains(head, "php") {
514+
return true
515+
}
516+
return false
517+
}

internal/services/github/releases.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func FetchReleasesPage(ctx context.Context, owner string, repo string, page int)
5454
return nil, err
5555
}
5656
req.Header.Set("Accept", "application/vnd.github+json")
57+
req.Header.Set("User-Agent", "evo-installer")
5758
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
5859
req.Header.Set("Authorization", "Bearer "+token)
5960
}
@@ -75,4 +76,3 @@ func FetchReleasesPage(ctx context.Context, owner string, repo string, page int)
7576
}
7677
return releases, nil
7778
}
78-

internal/ui/run.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package ui
22

33
import (
44
"context"
5+
"os"
56

67
tea "github.com/charmbracelet/bubbletea"
8+
"github.com/charmbracelet/x/term"
79

810
"github.com/evolution-cms/installer/internal/domain"
911
)
@@ -14,6 +16,21 @@ func Run(ctx context.Context, mode Mode, events <-chan domain.Event, meta Meta)
1416

1517
func RunWithCancel(ctx context.Context, mode Mode, events <-chan domain.Event, actions chan<- domain.Action, meta Meta, cancel func()) error {
1618
m := NewModel(ctx, mode, events, actions, meta, cancel)
19+
20+
// Bubble Tea relies on terminal size messages to render the UI. In some environments
21+
// (e.g., wrapped CLIs, certain PTYs, or misconfigured terminals) the size cannot be
22+
// detected, leaving the UI stuck at the "Loading…" placeholder. Seed a sensible
23+
// initial size so the UI can render and progress even if WindowSizeMsg never arrives.
24+
if w, h, err := term.GetSize(os.Stdout.Fd()); err == nil && w > 0 && h > 0 {
25+
m.width = w
26+
m.height = h
27+
m.reflow()
28+
} else {
29+
m.width = 80
30+
m.height = 24
31+
m.reflow()
32+
}
33+
1734
p := tea.NewProgram(m, tea.WithAltScreen())
1835
_, err := p.Run()
1936
return err

0 commit comments

Comments
 (0)