more knecht
This commit is contained in:
157
knecht/tui/form.go
Normal file
157
knecht/tui/form.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
|
||||
labelStyle = lipgloss.NewStyle().Width(32)
|
||||
blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
cursorStyle = focusedStyle
|
||||
)
|
||||
|
||||
// secretKey returns true for keys that likely contain sensitive values.
|
||||
func secretKey(key string) bool {
|
||||
upper := strings.ToUpper(key)
|
||||
for _, word := range []string{"KEY", "TOKEN", "PASSWORD", "SECRET", "PASS"} {
|
||||
if strings.Contains(upper, word) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type formModel struct {
|
||||
title string
|
||||
keys []string
|
||||
inputs []textinput.Model
|
||||
cursor int
|
||||
done bool
|
||||
}
|
||||
|
||||
func newFormModel(title string, keys []string) formModel {
|
||||
inputs := make([]textinput.Model, len(keys))
|
||||
for i, key := range keys {
|
||||
t := textinput.New()
|
||||
t.Placeholder = "enter value"
|
||||
t.CursorStyle = cursorStyle
|
||||
if secretKey(key) {
|
||||
t.EchoMode = textinput.EchoPassword
|
||||
t.EchoCharacter = '•'
|
||||
}
|
||||
if i == 0 {
|
||||
t.Focus()
|
||||
t.PromptStyle = focusedStyle
|
||||
t.TextStyle = focusedStyle
|
||||
}
|
||||
inputs[i] = t
|
||||
}
|
||||
return formModel{title: title, keys: keys, inputs: inputs}
|
||||
}
|
||||
|
||||
func (m formModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
m.done = true
|
||||
m.keys = nil // signal cancelled
|
||||
return m, tea.Quit
|
||||
|
||||
case "tab", "down":
|
||||
m.cursor = (m.cursor + 1) % len(m.inputs)
|
||||
return m, m.focusAt(m.cursor)
|
||||
|
||||
case "shift+tab", "up":
|
||||
m.cursor = (m.cursor - 1 + len(m.inputs)) % len(m.inputs)
|
||||
return m, m.focusAt(m.cursor)
|
||||
|
||||
case "enter":
|
||||
if m.cursor < len(m.inputs)-1 {
|
||||
m.cursor++
|
||||
return m, m.focusAt(m.cursor)
|
||||
}
|
||||
// Last field — submit
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
// Forward key events to the focused input
|
||||
var cmd tea.Cmd
|
||||
m.inputs[m.cursor], cmd = m.inputs[m.cursor].Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m formModel) focusAt(idx int) tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
for i := range m.inputs {
|
||||
if i == idx {
|
||||
m.inputs[i].PromptStyle = focusedStyle
|
||||
m.inputs[i].TextStyle = focusedStyle
|
||||
cmds = append(cmds, m.inputs[i].Focus())
|
||||
} else {
|
||||
m.inputs[i].Blur()
|
||||
m.inputs[i].PromptStyle = blurredStyle
|
||||
m.inputs[i].TextStyle = blurredStyle
|
||||
}
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m formModel) View() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(titleStyle.Render(m.title) + "\n\n")
|
||||
|
||||
for i, key := range m.keys {
|
||||
label := key
|
||||
if secretKey(key) {
|
||||
label += mutedStyle.Render(" (hidden)")
|
||||
}
|
||||
if i == m.cursor {
|
||||
b.WriteString(focusedStyle.Render("› ") + labelStyle.Render(label) + m.inputs[i].View() + "\n")
|
||||
} else {
|
||||
b.WriteString(blurredStyle.Render(" ") + labelStyle.Render(label) + m.inputs[i].View() + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n" + mutedStyle.Render("tab/↑↓ navigate enter confirm esc cancel"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// PromptMissingEnv runs a TUI form for the given keys and returns a map of
|
||||
// key → value. Returns nil if the user cancelled.
|
||||
func PromptMissingEnv(stackName string, keys []string) (map[string]string, error) {
|
||||
if len(keys) == 0 {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("Missing env vars for %q", stackName)
|
||||
m := newFormModel(title, keys)
|
||||
p := tea.NewProgram(m)
|
||||
result, err := p.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
final := result.(formModel)
|
||||
if final.keys == nil {
|
||||
return nil, nil // cancelled
|
||||
}
|
||||
|
||||
values := make(map[string]string, len(keys))
|
||||
for i, key := range keys {
|
||||
values[key] = final.inputs[i].Value()
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
@@ -15,13 +15,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
successStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10"))
|
||||
mutedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
driftStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("11"))
|
||||
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
|
||||
addStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
|
||||
removeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
|
||||
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
successStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10"))
|
||||
mutedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
driftStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("11"))
|
||||
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
|
||||
addStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
|
||||
removeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
|
||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("208"))
|
||||
)
|
||||
|
||||
type viewMode int
|
||||
@@ -31,23 +32,37 @@ const (
|
||||
modeDiff
|
||||
)
|
||||
|
||||
// ── messages ─────────────────────────────────────────────────────────────────
|
||||
// ── messages ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type stacksLoadedMsg []stackItem
|
||||
type updateDoneMsg struct{ err error }
|
||||
type deployDoneMsg struct{ err error }
|
||||
type envExampleWrittenMsg struct{ err error }
|
||||
type errMsg error
|
||||
|
||||
// ── list item ────────────────────────────────────────────────────────────────
|
||||
// ── list item ─────────────────────────────────────────────────────────────────
|
||||
|
||||
type stackItem struct {
|
||||
remote portainer.Stack
|
||||
remote *portainer.Stack // nil = not deployed
|
||||
local *stack.Local
|
||||
driftResult *drift.Result
|
||||
}
|
||||
|
||||
func (s stackItem) name() string {
|
||||
if s.remote != nil {
|
||||
return s.remote.Name
|
||||
}
|
||||
if s.local != nil {
|
||||
return s.local.Name
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (s stackItem) Title() string {
|
||||
name := s.remote.Name
|
||||
name := s.name()
|
||||
if s.remote == nil {
|
||||
return warnStyle.Render(name)
|
||||
}
|
||||
if s.driftResult != nil && s.driftResult.HasDrift() {
|
||||
name += driftStyle.Render(" ~")
|
||||
}
|
||||
@@ -55,6 +70,9 @@ func (s stackItem) Title() string {
|
||||
}
|
||||
|
||||
func (s stackItem) Description() string {
|
||||
if s.remote == nil {
|
||||
return warnStyle.Render("not deployed")
|
||||
}
|
||||
status := "running"
|
||||
if s.remote.Status != 1 {
|
||||
status = mutedStyle.Render("stopped")
|
||||
@@ -65,43 +83,44 @@ func (s stackItem) Description() string {
|
||||
return status + " · " + s.driftResult.Summary()
|
||||
}
|
||||
|
||||
func (s stackItem) FilterValue() string { return s.remote.Name }
|
||||
func (s stackItem) FilterValue() string { return s.name() }
|
||||
|
||||
// ── model ────────────────────────────────────────────────────────────────────
|
||||
// ── model ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
type model struct {
|
||||
list list.Model
|
||||
spinner spinner.Model
|
||||
client *portainer.Client
|
||||
svcPath string
|
||||
ignore map[string]bool
|
||||
mode viewMode
|
||||
updating bool
|
||||
status string // transient success/error message
|
||||
status string
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func initialModel(client *portainer.Client, svcPath string) model {
|
||||
func initialModel(client *portainer.Client, svcPath string, ignore []string) model {
|
||||
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
||||
l.Title = "knecht"
|
||||
l.Styles.Title = titleStyle
|
||||
l.SetShowHelp(false) // we render our own key hints
|
||||
l.SetShowHelp(false)
|
||||
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Dot
|
||||
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
|
||||
|
||||
return model{
|
||||
list: l,
|
||||
spinner: s,
|
||||
client: client,
|
||||
svcPath: svcPath,
|
||||
ignoreSet := make(map[string]bool, len(ignore))
|
||||
for _, name := range ignore {
|
||||
ignoreSet[name] = true
|
||||
}
|
||||
|
||||
return model{list: l, spinner: s, client: client, svcPath: svcPath, ignore: ignoreSet}
|
||||
}
|
||||
|
||||
// ── commands ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func loadStacks(client *portainer.Client, svcPath string) tea.Cmd {
|
||||
func loadStacks(client *portainer.Client, svcPath string, ignore map[string]bool) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
remotes, err := client.ListStacks()
|
||||
if err != nil {
|
||||
@@ -114,34 +133,34 @@ func loadStacks(client *portainer.Client, svcPath string) tea.Cmd {
|
||||
localByName[l.Name] = l
|
||||
}
|
||||
|
||||
items := make([]stackItem, 0, len(remotes))
|
||||
// Remote stacks (deployed), with optional local match
|
||||
remoteNames := make(map[string]bool, len(remotes))
|
||||
items := make([]stackItem, 0, len(remotes)+len(locals))
|
||||
for _, r := range remotes {
|
||||
item := stackItem{remote: r}
|
||||
if ignore[r.Name] {
|
||||
continue
|
||||
}
|
||||
rCopy := r
|
||||
remoteNames[r.Name] = true
|
||||
item := stackItem{remote: &rCopy}
|
||||
if l, ok := localByName[r.Name]; ok {
|
||||
lCopy := l
|
||||
item.local = &lCopy
|
||||
item.driftResult = computeDrift(client, &r, &lCopy)
|
||||
item.driftResult = computeDrift(client, &rCopy, &lCopy)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return stacksLoadedMsg(items)
|
||||
}
|
||||
}
|
||||
|
||||
func writeEnvExample(s stackItem) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if s.local == nil || s.driftResult == nil {
|
||||
return envExampleWrittenMsg{fmt.Errorf("no local stack info")}
|
||||
// Local-only stacks (not deployed)
|
||||
for _, l := range locals {
|
||||
if ignore[l.Name] || remoteNames[l.Name] {
|
||||
continue
|
||||
}
|
||||
lCopy := l
|
||||
items = append(items, stackItem{local: &lCopy})
|
||||
}
|
||||
// No .env.example at all — create from scratch
|
||||
if len(s.driftResult.PortainerOnlyKeys) > 0 {
|
||||
return envExampleWrittenMsg{s.local.WriteEnvExample(s.driftResult.PortainerOnlyKeys)}
|
||||
}
|
||||
// .env.example exists but has unknown keys — append them
|
||||
if len(s.driftResult.UnknownKeys) > 0 {
|
||||
return envExampleWrittenMsg{s.local.AppendEnvExample(s.driftResult.UnknownKeys)}
|
||||
}
|
||||
return envExampleWrittenMsg{fmt.Errorf("nothing to write")}
|
||||
|
||||
return stacksLoadedMsg(items)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,12 +175,44 @@ func updateStack(client *portainer.Client, s stackItem) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// ── bubbletea ────────────────────────────────────────────────────────────────
|
||||
func deployStack(client *portainer.Client, s stackItem) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
compose, err := s.local.ReadCompose()
|
||||
if err != nil {
|
||||
return deployDoneMsg{err}
|
||||
}
|
||||
exampleKeys, _ := s.local.EnvExampleKeys()
|
||||
env := make([]portainer.EnvVar, len(exampleKeys))
|
||||
for i, k := range exampleKeys {
|
||||
env[i] = portainer.EnvVar{Name: k}
|
||||
}
|
||||
_, err = client.CreateStack(s.local.Name, compose, env)
|
||||
return deployDoneMsg{err}
|
||||
}
|
||||
}
|
||||
|
||||
func writeEnvExample(s stackItem) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if s.local == nil || s.driftResult == nil {
|
||||
return envExampleWrittenMsg{fmt.Errorf("no local stack info")}
|
||||
}
|
||||
if len(s.driftResult.PortainerOnlyKeys) > 0 {
|
||||
return envExampleWrittenMsg{s.local.WriteEnvExample(s.driftResult.PortainerOnlyKeys)}
|
||||
}
|
||||
if len(s.driftResult.UnknownKeys) > 0 {
|
||||
return envExampleWrittenMsg{s.local.AppendEnvExample(s.driftResult.UnknownKeys)}
|
||||
}
|
||||
return envExampleWrittenMsg{fmt.Errorf("nothing to write")}
|
||||
}
|
||||
}
|
||||
|
||||
// ── bubbletea ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(loadStacks(m.client, m.svcPath), m.spinner.Tick)
|
||||
return tea.Batch(loadStacks(m.client, m.svcPath, m.ignore), m.spinner.Tick)
|
||||
}
|
||||
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
@@ -186,16 +237,25 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if msg.err != nil {
|
||||
m.status = errorStyle.Render("Update failed: " + msg.err.Error())
|
||||
} else {
|
||||
m.status = successStyle.Render("Updated successfully — refreshing...")
|
||||
cmds = append(cmds, loadStacks(m.client, m.svcPath))
|
||||
m.status = successStyle.Render("Updated — refreshing...")
|
||||
cmds = append(cmds, loadStacks(m.client, m.svcPath, m.ignore))
|
||||
}
|
||||
|
||||
case deployDoneMsg:
|
||||
m.updating = false
|
||||
if msg.err != nil {
|
||||
m.status = errorStyle.Render("Deploy failed: " + msg.err.Error())
|
||||
} else {
|
||||
m.status = successStyle.Render("Deployed — refreshing...")
|
||||
cmds = append(cmds, loadStacks(m.client, m.svcPath, m.ignore))
|
||||
}
|
||||
|
||||
case envExampleWrittenMsg:
|
||||
if msg.err != nil {
|
||||
m.status = errorStyle.Render("Failed to write .env.example: " + msg.err.Error())
|
||||
} else {
|
||||
m.status = successStyle.Render(".env.example created — refreshing...")
|
||||
cmds = append(cmds, loadStacks(m.client, m.svcPath))
|
||||
m.status = successStyle.Render(".env.example written — refreshing...")
|
||||
cmds = append(cmds, loadStacks(m.client, m.svcPath, m.ignore))
|
||||
}
|
||||
|
||||
case errMsg:
|
||||
@@ -210,7 +270,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
case tea.KeyMsg:
|
||||
// Don't pass keys to the list while filtering is active
|
||||
if m.list.SettingFilter() {
|
||||
break
|
||||
}
|
||||
@@ -222,10 +281,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "r":
|
||||
m.status = ""
|
||||
m.mode = modeSummary
|
||||
cmds = append(cmds, loadStacks(m.client, m.svcPath))
|
||||
cmds = append(cmds, loadStacks(m.client, m.svcPath, m.ignore))
|
||||
return m, tea.Batch(cmds...)
|
||||
|
||||
case "d":
|
||||
item, ok := m.list.SelectedItem().(stackItem)
|
||||
if !ok || item.remote == nil {
|
||||
return m, nil // no diff for undeployed stacks
|
||||
}
|
||||
if m.mode == modeDiff {
|
||||
m.mode = modeSummary
|
||||
} else {
|
||||
@@ -235,7 +298,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
case "u":
|
||||
item, ok := m.list.SelectedItem().(stackItem)
|
||||
if !ok || item.local == nil || m.updating {
|
||||
if !ok || item.local == nil || item.remote == nil || m.updating {
|
||||
return m, nil
|
||||
}
|
||||
m.updating = true
|
||||
@@ -244,6 +307,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmds = append(cmds, updateStack(m.client, item), m.spinner.Tick)
|
||||
return m, tea.Batch(cmds...)
|
||||
|
||||
case "D":
|
||||
item, ok := m.list.SelectedItem().(stackItem)
|
||||
if !ok || item.local == nil || item.remote != nil || m.updating {
|
||||
return m, nil
|
||||
}
|
||||
m.updating = true
|
||||
m.status = ""
|
||||
cmds = append(cmds, deployStack(m.client, item), m.spinner.Tick)
|
||||
return m, tea.Batch(cmds...)
|
||||
|
||||
case "e":
|
||||
item, ok := m.list.SelectedItem().(stackItem)
|
||||
if !ok || item.driftResult == nil {
|
||||
@@ -277,7 +350,7 @@ func (m model) View() string {
|
||||
|
||||
var detail string
|
||||
if m.updating {
|
||||
detail = m.spinner.View() + " Updating..."
|
||||
detail = m.spinner.View() + " Working..."
|
||||
} else if item, ok := m.list.SelectedItem().(stackItem); ok {
|
||||
switch m.mode {
|
||||
case modeDiff:
|
||||
@@ -295,6 +368,7 @@ func (m model) View() string {
|
||||
Width(m.width/2 - 2).
|
||||
Height(m.height - 4).
|
||||
Padding(1, 2).
|
||||
MarginLeft(4).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
Render(detail)
|
||||
|
||||
@@ -302,7 +376,13 @@ func (m model) View() string {
|
||||
}
|
||||
|
||||
func renderSummary(s stackItem) string {
|
||||
out := titleStyle.Render(s.remote.Name) + "\n\n"
|
||||
out := titleStyle.Render(s.name()) + "\n\n"
|
||||
|
||||
if s.remote == nil {
|
||||
out += fmt.Sprintf("status: %s\n", warnStyle.Render("not deployed"))
|
||||
out += "\n" + mutedStyle.Render("[D] deploy [r] refresh [q] quit")
|
||||
return out
|
||||
}
|
||||
|
||||
if s.remote.Status == 1 {
|
||||
out += fmt.Sprintf("status: %s\n", successStyle.Render("running"))
|
||||
@@ -312,6 +392,7 @@ func renderSummary(s stackItem) string {
|
||||
|
||||
if s.driftResult == nil {
|
||||
out += mutedStyle.Render("no local compose file\n")
|
||||
out += "\n" + mutedStyle.Render("[r] refresh [q] quit")
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -343,13 +424,12 @@ func renderSummary(s stackItem) string {
|
||||
}
|
||||
|
||||
func renderDiff(s stackItem) string {
|
||||
out := titleStyle.Render(s.remote.Name+" — diff") + "\n\n"
|
||||
out := titleStyle.Render(s.name()+" — diff") + "\n\n"
|
||||
|
||||
if s.driftResult == nil {
|
||||
return out + mutedStyle.Render("no local compose file")
|
||||
}
|
||||
|
||||
// Compose diff
|
||||
out += mutedStyle.Render("compose:") + "\n"
|
||||
if len(s.driftResult.ComposeDiff) == 0 {
|
||||
out += " " + successStyle.Render("in sync") + "\n"
|
||||
@@ -363,9 +443,8 @@ func renderDiff(s stackItem) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Env key diff
|
||||
out += "\n" + mutedStyle.Render("env keys:") + "\n"
|
||||
if len(s.driftResult.MissingKeys) == 0 && len(s.driftResult.UnknownKeys) == 0 {
|
||||
if len(s.driftResult.MissingKeys) == 0 && len(s.driftResult.UnknownKeys) == 0 && len(s.driftResult.PortainerOnlyKeys) == 0 {
|
||||
out += " " + successStyle.Render("in sync") + "\n"
|
||||
}
|
||||
for _, k := range s.driftResult.MissingKeys {
|
||||
@@ -405,7 +484,6 @@ func computeDrift(client *portainer.Client, s *portainer.Stack, local *stack.Loc
|
||||
}
|
||||
|
||||
if exampleKeys == nil && len(portainerKeys) > 0 {
|
||||
// No .env.example but Portainer has keys — surface as its own drift type
|
||||
result.PortainerOnlyKeys = portainerKeys
|
||||
} else {
|
||||
result.MissingKeys, result.UnknownKeys = drift.EnvKeys(exampleKeys, portainerKeys)
|
||||
@@ -414,8 +492,8 @@ func computeDrift(client *portainer.Client, s *portainer.Stack, local *stack.Loc
|
||||
return result
|
||||
}
|
||||
|
||||
func Run(client *portainer.Client, svcPath string, _ *config.Config) error {
|
||||
p := tea.NewProgram(initialModel(client, svcPath), tea.WithAltScreen())
|
||||
func Run(client *portainer.Client, svcPath string, cfg *config.Config) error {
|
||||
p := tea.NewProgram(initialModel(client, svcPath, cfg.Ignore), tea.WithAltScreen())
|
||||
_, err := p.Run()
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user