feat(server): add rename command for dedicated servers#219
Conversation
The Zeabur dashboard supports renaming dedicated servers, but the CLI had no equivalent. Wire up the existing `updateServerName` GraphQL mutation through a new `zeabur server rename` subcommand with both interactive and non-interactive modes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Client-side trim prevents passing " " as --name, which would otherwise round-trip to the backend and surface a generic "rename server was not accepted" error. Addresses review feedback from PR #218. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mention both the positional server-id arg and the --id flag so users know either form is accepted. Addresses CodeRabbit review on PR #218. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WalkthroughIntroduces a new Changes
Sequence DiagramsequenceDiagram
actor User
participant CLI as CLI Rename Command
participant APIClient as API Client
participant APIServer as API Server
User->>CLI: invoke rename (interactive mode, no --id)
CLI->>APIClient: ListServers()
APIClient->>APIServer: GraphQL query: listServers
APIServer-->>APIClient: server list with names/locations
APIClient-->>CLI: server list
CLI->>User: display selectable server list
User->>CLI: select server
CLI->>User: prompt for new server name
User->>CLI: provide new name
CLI->>APIClient: RenameServer(id, name)
APIClient->>APIServer: GraphQL mutation: updateServerName
APIServer-->>APIClient: mutation result with acceptance flag
APIClient-->>CLI: error or nil
CLI->>User: log success/failure
Estimated Code Review Effort🎯 2 (Simple) | ⏱️ ~12 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
Adds a zeabur server rename CLI subcommand to rename dedicated servers by wiring the existing updateServerName GraphQL mutation, supporting both interactive and non-interactive workflows.
Changes:
- Add
RenameServer(ctx, id, name)to the API client + interface, backed by theupdateServerNameGraphQL mutation. - Register a new
server renamesubcommand underzeabur server. - Implement interactive (server picker + name prompt) and non-interactive (
--id/positional +--name) rename flows with whitespace trimming.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
pkg/api/server.go |
Adds RenameServer mutation call and handles false-return rejection. |
pkg/api/interface.go |
Extends ServerAPI with RenameServer. |
internal/cmd/server/server.go |
Registers the new rename subcommand under server. |
internal/cmd/server/rename/rename.go |
Implements the zeabur server rename command (interactive + non-interactive). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| func runRenameNonInteractive(f *cmdutil.Factory, opts *Options) error { | ||
| if opts.id == "" { | ||
| return fmt.Errorf("server-id or --id is required") |
There was a problem hiding this comment.
In non-interactive mode, the missing-ID error message here differs from the established pattern used by other server subcommands that accept a positional server-id (e.g. server reboot/get/ssh all return "--id is required"). If the goal is to match those conventions (as noted in the PR description), consider aligning this message (or standardizing all of them) so UX and scripts are consistent.
| return fmt.Errorf("server-id or --id is required") | |
| return fmt.Errorf("--id is required") |
| func runRenameInteractive(f *cmdutil.Factory, opts *Options) error { | ||
| if opts.id == "" { | ||
| servers, err := f.ApiClient.ListServers(context.Background()) | ||
| if err != nil { | ||
| return fmt.Errorf("list servers failed: %w", err) | ||
| } | ||
| if len(servers) == 0 { | ||
| return fmt.Errorf("no servers found") | ||
| } | ||
|
|
||
| options := make([]string, len(servers)) | ||
| for i, s := range servers { | ||
| location := s.IP | ||
| if s.City != nil && s.Country != nil { | ||
| location = fmt.Sprintf("%s, %s", *s.City, *s.Country) | ||
| } else if s.Country != nil { | ||
| location = *s.Country | ||
| } | ||
| options[i] = fmt.Sprintf("%s (%s)", s.Name, location) | ||
| } | ||
|
|
||
| idx, err := f.Prompter.Select("Select a server to rename", "", options) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| opts.id = servers[idx].ID |
There was a problem hiding this comment.
This interactive server-picker logic (list servers, build location strings, prompt select) is now duplicated across multiple server subcommands (get/reboot/ssh/rename). Consider extracting a shared helper (e.g. in internal/cmd/server or cmdutil) to keep the formatting and selection behavior consistent and reduce future maintenance drift.
| func runRenameInteractive(f *cmdutil.Factory, opts *Options) error { | |
| if opts.id == "" { | |
| servers, err := f.ApiClient.ListServers(context.Background()) | |
| if err != nil { | |
| return fmt.Errorf("list servers failed: %w", err) | |
| } | |
| if len(servers) == 0 { | |
| return fmt.Errorf("no servers found") | |
| } | |
| options := make([]string, len(servers)) | |
| for i, s := range servers { | |
| location := s.IP | |
| if s.City != nil && s.Country != nil { | |
| location = fmt.Sprintf("%s, %s", *s.City, *s.Country) | |
| } else if s.Country != nil { | |
| location = *s.Country | |
| } | |
| options[i] = fmt.Sprintf("%s (%s)", s.Name, location) | |
| } | |
| idx, err := f.Prompter.Select("Select a server to rename", "", options) | |
| if err != nil { | |
| return err | |
| } | |
| opts.id = servers[idx].ID | |
| func promptServerIDForRename(f *cmdutil.Factory) (string, error) { | |
| servers, err := f.ApiClient.ListServers(context.Background()) | |
| if err != nil { | |
| return "", fmt.Errorf("list servers failed: %w", err) | |
| } | |
| if len(servers) == 0 { | |
| return "", fmt.Errorf("no servers found") | |
| } | |
| options := make([]string, len(servers)) | |
| for i, s := range servers { | |
| location := s.IP | |
| if s.City != nil && s.Country != nil { | |
| location = fmt.Sprintf("%s, %s", *s.City, *s.Country) | |
| } else if s.Country != nil { | |
| location = *s.Country | |
| } | |
| options[i] = fmt.Sprintf("%s (%s)", s.Name, location) | |
| } | |
| idx, err := f.Prompter.Select("Select a server to rename", "", options) | |
| if err != nil { | |
| return "", err | |
| } | |
| return servers[idx].ID, nil | |
| } | |
| func runRenameInteractive(f *cmdutil.Factory, opts *Options) error { | |
| if opts.id == "" { | |
| id, err := promptServerIDForRename(f) | |
| if err != nil { | |
| return err | |
| } | |
| opts.id = id |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/cmd/server/rename/rename.go (1)
60-65: Harden server location label formatting for empty-string fields.This block only checks pointer nil-ness. Empty
City/Countryvalues can render awkward labels (for example", "), and city-only values currently fall back to IP.♻️ Suggested tweak
for i, s := range servers { location := s.IP - if s.City != nil && s.Country != nil { + if s.City != nil && *s.City != "" && s.Country != nil && *s.Country != "" { location = fmt.Sprintf("%s, %s", *s.City, *s.Country) - } else if s.Country != nil { + } else if s.Country != nil && *s.Country != "" { location = *s.Country + } else if s.City != nil && *s.City != "" { + location = *s.City } options[i] = fmt.Sprintf("%s (%s)", s.Name, location) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/cmd/server/rename/rename.go` around lines 60 - 65, The location formatting currently only checks nil pointers and can produce empty labels (e.g., ", ") or fall back to IP when City is present but empty; update the logic around variable location and struct fields s.City and s.Country to treat a field as present only if the pointer is non-nil and the dereferenced string is not empty (e.g., len(strings.TrimSpace(*s.City)) > 0). Build location as: both non-empty => "City, Country"; only city non-empty => city; only country non-empty => country; otherwise fallback to s.IP; update the block that sets location to use these checks (referencing s.City, s.Country, and variable location).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@internal/cmd/server/rename/rename.go`:
- Around line 60-65: The location formatting currently only checks nil pointers
and can produce empty labels (e.g., ", ") or fall back to IP when City is
present but empty; update the logic around variable location and struct fields
s.City and s.Country to treat a field as present only if the pointer is non-nil
and the dereferenced string is not empty (e.g., len(strings.TrimSpace(*s.City))
> 0). Build location as: both non-empty => "City, Country"; only city non-empty
=> city; only country non-empty => country; otherwise fallback to s.IP; update
the block that sets location to use these checks (referencing s.City, s.Country,
and variable location).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7a4f66f3-8e27-48fe-bba4-dbfbcba500fa
📒 Files selected for processing (4)
internal/cmd/server/rename/rename.gointernal/cmd/server/server.gopkg/api/interface.gopkg/api/server.go
Summary
updateServerNameGraphQL mutation through a newzeabur server renamesubcommand.--id/--nameflags or positional arg) modes, matching the convention ofserver reboot.Supersedes #218 (branch renamed for SEI-406 tracking).
Test plan
go build ./...passesgo vet ./...passesgo test ./...passes (no regressions)zeabur server rename --helpshows expected flags--id🤖 Generated with Claude Code
Summary by CodeRabbit
server renamecommand with both interactive and non-interactive modes.