Skip to content

Commit 1d2f7d0

Browse files
hi-leiclaude
andcommitted
feat: upgrade to bubbletea v2, add settings/theme, CI/CD, cross-platform release
Major updates: - Upgrade verdagostack to v1.1.1 (bubbletea v2, lipgloss v2) - Upgrade verdacloud-sdk-go to v1.4.2 (volume price fix) - Add `verda settings theme` command with 8 built-in themes - Add HintBarView to wizard flows (vm create, auth login) - Add hostname validation and auto-generation (petname) - Add --debug output to all API-calling subcommands - Add pager for volume trash list - Add cross-platform ~/.verda/ path handling (VERDA_HOME env var) - Add SaveSetting/GetSetting for persistent config - Fix volume hourly price calculation (730 hours/month) - Fix multiselect space key for bubbletea v2 - Move config resolution description to auth subcommand - Clean up prompt hint text (now handled by components) - Remove test temp directories from tracking CI/CD: - Add GitHub Actions: ci.yml, release.yml, security.yml - Add goreleaser for cross-platform builds (linux/darwin/windows x amd64/arm64) - Add git-cliff changelog automation - Add pre-commit hooks config - Add PR template - Add CLAUDE.md, AGENTS.md, .ai/skills/new-command.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 929f248 commit 1d2f7d0

51 files changed

Lines changed: 1610 additions & 166 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ai/skills/new-command.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# Creating a New CLI Command
2+
3+
This skill documents the conventions and checklist for adding a new subcommand to verda-cli.
4+
5+
## Project Structure
6+
7+
```
8+
internal/verda-cli/cmd/<domain>/
9+
<domain>.go # Parent command (NewCmd<Domain>)
10+
list.go # List subcommand
11+
add.go / create.go # Create/add subcommand
12+
delete.go # Delete subcommand
13+
action.go # Interactive action picker
14+
```
15+
16+
## Checklist
17+
18+
Every new command MUST follow these patterns:
19+
20+
### 1. Command Definition (cobra)
21+
22+
```go
23+
func NewCmd<Action>(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command {
24+
opts := &<action>Options{}
25+
26+
cmd := &cobra.Command{
27+
Use: "<action>",
28+
Short: "One-line description",
29+
Long: cmdutil.LongDesc(`
30+
Multi-line description.
31+
`),
32+
Example: cmdutil.Examples(`
33+
verda <domain> <action>
34+
verda <domain> <action> --flag value
35+
`),
36+
Args: cobra.NoArgs,
37+
RunE: func(cmd *cobra.Command, args []string) error {
38+
return run<Action>(cmd, f, ioStreams, opts)
39+
},
40+
}
41+
42+
flags := cmd.Flags()
43+
// Add flags here
44+
45+
return cmd
46+
}
47+
```
48+
49+
### 2. Debug Output (`--debug`)
50+
51+
`--debug` is a global persistent flag inherited by all subcommands. Every command that calls the API MUST include debug output.
52+
53+
**For mutations (create/add/delete/action)** -- log the request before execution:
54+
55+
```go
56+
cmdutil.DebugJSON(ioStreams.ErrOut, f.Debug(), "Request payload:", req)
57+
```
58+
59+
**For list/read commands** -- log the API response:
60+
61+
```go
62+
cmdutil.DebugJSON(ioStreams.ErrOut, f.Debug(), fmt.Sprintf("API response: %d item(s):", len(items)), items)
63+
```
64+
65+
**For actions** -- log the action context:
66+
67+
```go
68+
cmdutil.DebugJSON(ioStreams.ErrOut, f.Debug(), fmt.Sprintf("Action: %s on resource:", action.Label), map[string]string{
69+
"id": resource.ID,
70+
"name": resource.Name,
71+
"status": resource.Status,
72+
})
73+
```
74+
75+
### 3. Spinner Pattern
76+
77+
All API calls that may take time MUST show a spinner:
78+
79+
```go
80+
var sp interface{ Stop(string) }
81+
if status := f.Status(); status != nil {
82+
sp, _ = status.Spinner(ctx, "Loading items...")
83+
}
84+
result, err := client.Items.List(ctx)
85+
if sp != nil {
86+
sp.Stop("")
87+
}
88+
if err != nil {
89+
return err
90+
}
91+
```
92+
93+
### 4. Timeout Context
94+
95+
All API calls MUST use a timeout context:
96+
97+
```go
98+
ctx, cancel := context.WithTimeout(cmd.Context(), f.Options().Timeout)
99+
defer cancel()
100+
```
101+
102+
### 5. Credentials Check
103+
104+
Commands that need API access call `f.VerdaClient()` which returns a clear error if not authenticated:
105+
106+
```go
107+
client, err := f.VerdaClient()
108+
if err != nil {
109+
return err
110+
}
111+
```
112+
113+
### 6. Interactive + Non-Interactive
114+
115+
Commands should support both modes:
116+
- **Flags** for scripting/CI (non-interactive)
117+
- **Prompter** for interactive use when flags are missing
118+
119+
```go
120+
name := opts.Name
121+
if name == "" {
122+
name, err = prompter.TextInput(ctx, "Item name")
123+
if err != nil {
124+
return nil // User cancelled (Esc/Ctrl+C)
125+
}
126+
}
127+
```
128+
129+
### 7. Output Conventions
130+
131+
- Normal output goes to `ioStreams.Out`
132+
- Prompts, warnings, and debug go to `ioStreams.ErrOut`
133+
- Use lipgloss styles from `charm.land/lipgloss/v2`:
134+
- `lipgloss.Color("8")` -- dim/gray
135+
- `lipgloss.Color("1")` -- red/error
136+
- `lipgloss.Color("2")` -- green/price
137+
- `lipgloss.Color("3")` -- yellow/warning
138+
- `lipgloss.Color("14")` -- cyan/accent
139+
- Bold: `lipgloss.NewStyle().Bold(true)`
140+
141+
### 8. Confirmation for Destructive Actions
142+
143+
Delete and dangerous actions MUST confirm:
144+
145+
```go
146+
confirmed, err := prompter.Confirm(ctx, fmt.Sprintf("Delete %s?", name))
147+
if err != nil || !confirmed {
148+
_, _ = fmt.Fprintln(ioStreams.ErrOut, "Cancelled.")
149+
return nil
150+
}
151+
```
152+
153+
For dangerous actions, add warning styling:
154+
155+
```go
156+
warnStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true)
157+
_, _ = fmt.Fprintf(ioStreams.ErrOut, "\n %s\n", warnStyle.Render("This action cannot be undone."))
158+
```
159+
160+
### 9. Interactive Select with Cancel
161+
162+
Always append a "Cancel" option and handle Esc:
163+
164+
```go
165+
labels = append(labels, "Cancel")
166+
idx, err := prompter.Select(ctx, "Select item", labels)
167+
if err != nil {
168+
return nil // Esc/Ctrl+C
169+
}
170+
if idx == len(items) { // Cancel
171+
return nil
172+
}
173+
```
174+
175+
### 10. Register in Parent Command
176+
177+
Add the new command to its parent in `<domain>.go`:
178+
179+
```go
180+
cmd.AddCommand(
181+
NewCmd<Action>(f, ioStreams),
182+
)
183+
```
184+
185+
### 11. Register Domain in Root
186+
187+
If creating a new domain, add to `cmd/cmd.go` in the appropriate command group.
188+
189+
### 12. Long Lists
190+
191+
For commands that return potentially long lists, use the pager:
192+
193+
```go
194+
if status := f.Status(); status != nil {
195+
return status.Pager(cmd.Context(), content, tui.WithPagerTitle("Title"))
196+
}
197+
_, _ = fmt.Fprint(ioStreams.Out, content)
198+
```
199+
200+
The pager auto-detects: prints directly if content fits terminal, otherwise shows scrollable viewport.
201+
202+
## Dependencies
203+
204+
- `cmdutil "github/verda-cloud/verda-cli/internal/verda-cli/cmd/util"` -- Factory, IOStreams, DebugJSON, helpers
205+
- `"github.com/verda-cloud/verdacloud-sdk-go/pkg/verda"` -- SDK client and types
206+
- `"github.com/verda-cloud/verdagostack/pkg/tui"` -- Prompter, Status, pager options
207+
- `"charm.land/lipgloss/v2"` -- Terminal styling
208+
- `"github.com/spf13/cobra"` -- Command framework
209+
210+
## Wizard Flows
211+
212+
For complex multi-step creation flows, use the wizard engine:
213+
214+
```go
215+
flow := &wizard.Flow{
216+
Name: "resource-create",
217+
Layout: []wizard.ViewDef{
218+
{ID: "progress", View: wizard.NewProgressView(wizard.WithProgressPercent())},
219+
},
220+
Steps: []wizard.Step{ ... },
221+
}
222+
engine := wizard.NewEngine(f.Prompter(), f.Status(), wizard.WithOutput(ioStreams.ErrOut))
223+
return engine.Run(ctx, flow)
224+
```
225+
226+
Key wizard patterns:
227+
- Steps that handle their own prompting inside Loader should have no-op Setter/Resetter
228+
- Use `clientFunc` for lazy API client resolution
229+
- Use `apiCache` to share data between steps
230+
- Use `withSpinner` helper for API calls inside step Loaders
231+
232+
## Hostname Generation
233+
234+
Use `cmdutil.GenerateHostname(locationCode)` for auto-generated hostnames (3 random words + location).
235+
Use `cmdutil.ValidateHostname(s)` for validation (letters, digits, hyphens, no leading/trailing hyphen).

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## Description
2+
3+
<!-- Briefly describe what this PR does and why -->
4+
5+
## Type of Change
6+
7+
- [ ] feat: New feature
8+
- [ ] fix: Bug fix
9+
- [ ] refactor: Code refactoring
10+
- [ ] docs: Documentation
11+
- [ ] test: Tests
12+
- [ ] chore: Maintenance
13+
- [ ] ci: CI/CD changes
14+
15+
## Checklist
16+
17+
- [ ] Code follows the project conventions (see `.ai/skills/new-command.md`)
18+
- [ ] `go build ./...` passes
19+
- [ ] `go test ./...` passes
20+
- [ ] `--debug` output included for API-calling commands
21+
- [ ] Destructive actions have confirmation prompts
22+
23+
> **Note:** The CHANGELOG is auto-generated from conventional commit messages at release time. Do not edit CHANGELOG.md manually.

0 commit comments

Comments
 (0)