Skip to content

Commit b43e2bc

Browse files
committed
feat(output): replace startup banner with cipher animation
Run a short non-blocking HACKCTL encrypt/decrypt header alongside live progress and align start/share missing-config error rendering for cleaner startup failures.
1 parent 17553ae commit b43e2bc

8 files changed

Lines changed: 419 additions & 90 deletions

File tree

‎cmd/create.go‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,23 @@ var createCmd = &cobra.Command{
3232
Example: "hackctl create --template mern .\n" +
3333
"hackctl create --template mern my-app",
3434
RunE: func(cmd *cobra.Command, args []string) error {
35-
fmt.Print(output.ASCIIBanner())
36-
3735
createName := args[0]
38-
39-
source, err := templates.Resolve(createTemplate)
40-
if err != nil {
41-
return err
42-
}
43-
44-
targetPath, err := resolveTargetPath(createName)
45-
if err != nil {
46-
return err
47-
}
36+
var (
37+
source templates.TemplateSource
38+
targetPath string
39+
)
4840

4941
if err := output.RunSteps("Creating project", func(addStep func(string) int, completeStep func(int)) error {
5042
stepID := addStep("Preparing project directory")
43+
var err error
44+
source, err = templates.Resolve(createTemplate)
45+
if err != nil {
46+
return err
47+
}
48+
targetPath, err = resolveTargetPath(createName)
49+
if err != nil {
50+
return err
51+
}
5152
if err := ensureWritableTarget(targetPath, createName); err != nil {
5253
return err
5354
}

‎cmd/share.go‎

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,10 @@ var shareCmd = &cobra.Command{
2929
Use: "share",
3030
Short: "Share a running project publicly",
3131
RunE: func(cmd *cobra.Command, args []string) error {
32-
fmt.Print(output.ASCIIBanner())
33-
34-
rootPath, err := os.Getwd()
35-
if err != nil {
36-
return err
37-
}
38-
39-
projectConfig, err := config.LoadProjectConfig(rootPath)
40-
if err != nil {
41-
return err
42-
}
43-
44-
port, err := resolveSharePort(projectConfig)
45-
if err != nil {
46-
return err
47-
}
48-
49-
if !isPortReachable(port, 1200*time.Millisecond) {
50-
return fmt.Errorf("No services running on port %d", port)
51-
}
32+
var (
33+
rootPath string
34+
port int
35+
)
5236

5337
var cloudflaredPath string
5438
var tunnelCmd *exec.Cmd
@@ -59,6 +43,26 @@ var shareCmd = &cobra.Command{
5943
tunnelOutput := newLineTail(20)
6044

6145
if err := output.RunSteps("Sharing project", func(addStep func(string) int, completeStep func(int)) error {
46+
var err error
47+
rootPath, err = os.Getwd()
48+
if err != nil {
49+
return err
50+
}
51+
52+
projectConfig, err := config.LoadProjectConfig(rootPath)
53+
if err != nil {
54+
return err
55+
}
56+
57+
port, err = resolveSharePort(projectConfig)
58+
if err != nil {
59+
return err
60+
}
61+
62+
if !isPortReachable(port, 1200*time.Millisecond) {
63+
return fmt.Errorf("No services running on port %d", port)
64+
}
65+
6266
stepID := addStep("Preparing tunnel client")
6367
var resolveErr error
6468
cloudflaredPath, resolveErr = resolveCloudflaredBinary()

‎cmd/start.go‎

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -68,40 +68,43 @@ type startFailedMsg struct {
6868

6969
type startModel struct {
7070
spinner spinner.Model
71+
startup output.StartupAnimation
7172
rows []startServiceRow
7273
indexByName map[string]int
7374
footer string
7475
stopping bool
76+
done bool
77+
result error
7578
cancel context.CancelFunc
7679
}
7780

7881
var startCmd = &cobra.Command{
7982
Use: "start",
8083
Short: "Start project services",
8184
RunE: func(cmd *cobra.Command, args []string) error {
82-
fmt.Print(output.ASCIIBanner())
83-
8485
rootPath, err := os.Getwd()
8586
if err != nil {
8687
return err
8788
}
8889

89-
projectConfig, err := config.LoadProjectConfig(rootPath)
90-
if err != nil {
91-
return err
92-
}
93-
9490
ctx, cancel := context.WithCancel(context.Background())
9591
defer cancel()
9692

97-
model := newStartModel(projectConfig.Services, cancel)
93+
model := newStartModel(cancel)
9894
program := tea.NewProgram(model, tea.WithoutSignalHandler())
9995

100-
go runStartWorkflow(ctx, rootPath, projectConfig, program.Send)
96+
go runStartWorkflow(ctx, rootPath, program.Send)
10197

102-
if _, err := program.Run(); err != nil {
98+
finalModel, err := program.Run()
99+
if err != nil {
103100
return err
104101
}
102+
103+
final := finalModel.(startModel)
104+
if final.result != nil {
105+
return final.result
106+
}
107+
105108
return nil
106109
},
107110
}
@@ -110,36 +113,29 @@ func init() {
110113
rootCmd.AddCommand(startCmd)
111114
}
112115

113-
func newStartModel(services []config.ServiceConfig, cancel context.CancelFunc) startModel {
114-
rows := make([]startServiceRow, 0, len(services))
115-
indexByName := make(map[string]int, len(services))
116-
117-
for i, svc := range services {
118-
rows = append(rows, startServiceRow{
119-
Name: svc.Name,
120-
Status: statusPending,
121-
Detail: "waiting",
122-
})
123-
indexByName[svc.Name] = i
124-
}
125-
116+
func newStartModel(cancel context.CancelFunc) startModel {
126117
spin := spinner.New(spinner.WithSpinner(spinner.Dot))
127118

128119
return startModel{
129120
spinner: spin,
130-
rows: rows,
131-
indexByName: indexByName,
121+
startup: output.NewStartupAnimation(),
122+
rows: []startServiceRow{},
123+
indexByName: make(map[string]int),
132124
footer: "Starting services...",
133125
cancel: cancel,
134126
}
135127
}
136128

137129
func (m startModel) Init() tea.Cmd {
138-
return m.spinner.Tick
130+
return tea.Batch(m.spinner.Tick, m.startup.Init())
139131
}
140132

141133
func (m startModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
142134
switch msg := msg.(type) {
135+
case output.StartupAnimationTickMsg:
136+
var cmd tea.Cmd
137+
m.startup, cmd = m.startup.Update(msg)
138+
return m, cmd
143139
case spinner.TickMsg:
144140
var cmd tea.Cmd
145141
m.spinner, cmd = m.spinner.Update(msg)
@@ -149,20 +145,29 @@ func (m startModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
149145
if ok {
150146
m.rows[idx].Status = msg.Status
151147
m.rows[idx].Detail = msg.Detail
148+
} else {
149+
m.indexByName[msg.Name] = len(m.rows)
150+
m.rows = append(m.rows, startServiceRow{
151+
Name: msg.Name,
152+
Status: msg.Status,
153+
Detail: msg.Detail,
154+
})
152155
}
153156
return m, nil
154157
case startFooterMsg:
155158
m.footer = msg.Text
156159
return m, nil
157160
case startStoppedMsg:
158161
m.footer = "Services stopped"
162+
m.done = true
159163
return m, tea.Quit
160164
case startFailedMsg:
161165
if msg.Message == "" {
162-
m.footer = "Start failed"
166+
m.result = errors.New("start failed")
163167
} else {
164-
m.footer = "Start failed: " + msg.Message
168+
m.result = errors.New(msg.Message)
165169
}
170+
m.done = true
166171
return m, tea.Quit
167172
case tea.KeyMsg:
168173
if msg.String() == "ctrl+c" || msg.String() == "q" {
@@ -183,6 +188,16 @@ func (m startModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
183188
func (m startModel) View() string {
184189
var builder strings.Builder
185190

191+
if m.startup.Visible() {
192+
builder.WriteString("\n")
193+
if m.done {
194+
builder.WriteString(m.startup.ResolvedView())
195+
} else {
196+
builder.WriteString(m.startup.View())
197+
}
198+
builder.WriteString("\n\n")
199+
}
200+
186201
for _, row := range m.rows {
187202
line := m.rowIcon(row.Status) + " " + displayServiceName(row.Name)
188203
if row.Detail != "" {
@@ -197,7 +212,16 @@ func (m startModel) View() string {
197212
builder.WriteString("\n")
198213
}
199214

200-
builder.WriteString("\n")
215+
if m.result != nil {
216+
if len(m.rows) > 0 {
217+
builder.WriteString("\n")
218+
}
219+
return builder.String()
220+
}
221+
222+
if len(m.rows) > 0 {
223+
builder.WriteString("\n")
224+
}
201225
builder.WriteString(output.Footer(m.footer))
202226
builder.WriteString("\n")
203227

@@ -230,7 +254,13 @@ func (m startModel) rowIcon(status startServiceStatus) string {
230254
}
231255
}
232256

233-
func runStartWorkflow(ctx context.Context, rootPath string, projectConfig config.ProjectConfig, send func(msg tea.Msg)) {
257+
func runStartWorkflow(ctx context.Context, rootPath string, send func(msg tea.Msg)) {
258+
projectConfig, err := config.LoadProjectConfig(rootPath)
259+
if err != nil {
260+
send(startFailedMsg{Message: err.Error()})
261+
return
262+
}
263+
234264
if err := ensureDependencies(depNode, depNPM); err != nil {
235265
send(startFailedMsg{Message: err.Error()})
236266
return

‎internal/config/config.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func LoadProjectConfig(rootPath string) (ProjectConfig, error) {
3838
body, err := os.ReadFile(configPath)
3939
if err != nil {
4040
if errors.Is(err, os.ErrNotExist) {
41-
return ProjectConfig{}, fmt.Errorf("%s not found in %s", ProjectConfigFilename, rootPath)
41+
return ProjectConfig{}, fmt.Errorf("%s not found", ProjectConfigFilename)
4242
}
4343
return ProjectConfig{}, err
4444
}

‎internal/output/spinner.go‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ type progressLine struct {
104104

105105
type stepsModel struct {
106106
spinner spinner.Model
107+
startup StartupAnimation
107108
title string
108109
progress []progressLine
109110
indexByID map[int]int
@@ -134,6 +135,7 @@ func RunSteps(title string, action func(addStep func(string) int, completeStep f
134135

135136
model := stepsModel{
136137
spinner: spinner.New(spinner.WithSpinner(spinner.Dot)),
138+
startup: NewStartupAnimation(),
137139
title: title,
138140
indexByID: make(map[int]int),
139141
events: events,
@@ -149,11 +151,15 @@ func RunSteps(title string, action func(addStep func(string) int, completeStep f
149151
}
150152

151153
func (m stepsModel) Init() tea.Cmd {
152-
return tea.Batch(m.spinner.Tick, waitForProgressEvent(m.events))
154+
return tea.Batch(m.spinner.Tick, waitForProgressEvent(m.events), m.startup.Init())
153155
}
154156

155157
func (m stepsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
156158
switch msg := msg.(type) {
159+
case StartupAnimationTickMsg:
160+
var cmd tea.Cmd
161+
m.startup, cmd = m.startup.Update(msg)
162+
return m, cmd
157163
case spinner.TickMsg:
158164
var cmd tea.Cmd
159165
m.spinner, cmd = m.spinner.Update(msg)
@@ -194,6 +200,16 @@ func (m stepsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
194200
func (m stepsModel) View() string {
195201
var b strings.Builder
196202

203+
if m.startup.Visible() {
204+
b.WriteString("\n")
205+
if m.done {
206+
b.WriteString(m.startup.ResolvedView())
207+
} else {
208+
b.WriteString(m.startup.View())
209+
}
210+
b.WriteString("\n\n")
211+
}
212+
197213
if m.done {
198214
if m.success {
199215
b.WriteString(fmt.Sprintf("%s %s\n", OK(SuccessIcon()), m.title))
@@ -223,7 +239,9 @@ func (m stepsModel) View() string {
223239

224240
b.WriteString(fmt.Sprintf("%s\n", line))
225241
}
226-
b.WriteString("\n")
242+
if len(m.progress) > 0 {
243+
b.WriteString("\n")
244+
}
227245

228246
return b.String()
229247
}

0 commit comments

Comments
 (0)