Skip to content

Latest commit

 

History

History
217 lines (167 loc) · 7.29 KB

File metadata and controls

217 lines (167 loc) · 7.29 KB

Repository Guidelines

Last verified: 2026-02-10

Essential guidance for AI coding agents working in the Render CLI.

Project Overview

What: Terminal interface for Render platform (Go + Bubble Tea TUI) Why: Manage deployments, services, logs, and infrastructure from the command line

Before You Code

  1. Search existing code for similar implementations
  2. Follow established patterns in pkg/tui/
  3. Check if types exist in pkg/client/ before defining new ones

Boundaries

Never do (without explicit approval):

  • Edit files in pkg/client/ (generated code)
  • Modify *_gen.go files
  • Remove existing tests
  • Change navigation stack architecture in pkg/tui/stack.go

Always ask first:

  • Major TUI architectural changes
  • Adding new dependencies
  • Changes to command flag APIs (breaking changes)

Safe to do:

  • Add new views in pkg/tui/views/
  • Add new commands in cmd/
  • Write tests anywhere
  • Refactor within a single package

When Uncertain

  • Requirements unclear: Ask before implementing
  • Architecture questions: Propose minimal approach, get feedback
  • TUI interactions: Describe intended flow, get confirmation

Prefer small, incremental changes over large speculative implementations.

Essential Commands

# Building
go build -o render .              # Build binary

# Testing
go test ./cmd/... ./pkg/... ./internal/...     # Default development loop
go test <package> -run '<test-name-or-regex>'  # Focused test pattern

# Linting & Formatting
golangci-lint run                 # Lint
prek run --all-files              # All hooks (see prek.toml)

# Type Generation (from public-api-schema)
export RENDER_API_PATH=/path/to/api
cd ../public-api-schema && ./generate-cli.sh

Project Structure

  • cmd/ - Cobra command definitions
  • pkg/client/ - Generated API client (READ-ONLY)
  • pkg/tui/ - Bubble Tea TUI framework (see AGENTS.md)
  • pkg/config/ - User config file (~/.render/cli.yaml)
  • pkg/cfg/ - Environment defaults (different from config!)
  • pkg/command/ - Output formats, context utilities
  • pkg/dependencies/ - Dependency injection container
  • pkg/style/ - Lipgloss styling system

Key Patterns

TUI: Elm Architecture (Message → Update → View). See pkg/tui/AGENTS.md.

Data Access: Service → Repo → Client (three-layer architecture):

  • Client (pkg/client/): Generated HTTP client - never edit
  • Repo (pkg/*/repo.go): Wraps client, handles pagination & error parsing
  • Service (pkg/*/service.go): Business logic, orchestrates multiple repos, enriches data
// Service combines data from multiple repos
svc, _ := s.repo.ListServices(ctx, params)    // calls client internally
proj, _ := s.projectRepo.ListProjects(ctx)    // different repo
return s.enrich(svc, proj)                    // business logic

Command construction: New Cobra commands should use factory functions that accept dependencies, then be registered from setup functions in cmd/root.go. Avoid package-global command singletons and init-time subcommand registration for new command / subcommands.

// cmd/pgget.go
func newPgGetCmd(deps *dependencies.Dependencies) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "get <postgresID|postgresName>",
		Short: "Get a Postgres database",
		RunE: func(cmd *cobra.Command, args []string) error {
			return deps.PostgresService().Get(cmd.Context(), args[0])
		},
	}
	return cmd
}

// cmd/root.go
func setupPGCommands(earlyAccess *cobra.Command, deps *dependencies.Dependencies) {
	earlyAccess.AddCommand(newPgCmd(newPgGetCmd(deps)))
}

Output formats: Use command.IsInteractive(ctx) for branching:

if command.IsInteractive(ctx) {
    return runTUI(deps)
}
return runNonInteractive(ctx, deps)  // JSON, YAML, TEXT

For commands without an interactive/TUI path, default interactive output to text at the top of RunE:

cmd.RunE = func(cmd *cobra.Command, args []string) error {
    command.DefaultFormatNonInteractive(cmd)

    // parse input and run the non-interactive command path
}

Naming:

Element Pattern Example
Commands New{Action}Cmd() NewServiceListCmd()
Models {Entity}Model ServiceListModel
Messages {Action}Msg LoadServicesMsg

Testing Against Local Dev API

# Start API
tilt up api
curl -k https://api.localhost.render.com:8443/health

# Configure CLI
export RENDER_HOST="https://api.localhost.render.com:8443/v1/"
export RENDER_API_KEY="your-api-key"
./render services list
Variable Description
RENDER_HOST API base URL
RENDER_API_KEY API key (skips OAuth)
RENDER_WORKSPACE Workspace ID override
RENDER_CLI_CONFIG_PATH Config file path override

Testing

  • Table-driven tests with stretchr/testify
  • Manual fakes in pkg/tui/testhelper/
  • Hooks (prek.toml): golangci-lint, shellcheck, shfmt, yaml checks, large file detection
  • REST API fake in internal/fakes/renderapi for command tests whose code paths hit the API; see cmd/pglist_test.go for the preferred harness pattern.

Run go test ./cmd/... ./pkg/... ./internal/... by default. e2e/e2e_test.go logs in and hits the configured Render API (api.render.com by default), so leave it out of the normal development loop. When using -run, target the package under test when you know it, e.g. go test ./cmd -run TestPGList; use the broader default package set when you want normal pre-review confidence. To run e2e/e2e_test.go locally, the human operator must first follow the CLI E2E tests Slab doc.

Prefer the renderapi fake over one-off HTTP servers or API-layer mocks. Seed state with renderapi.NewServer(t), resource factories, and server helpers; if coverage is missing, expand the shared fake unless a one-off server or mock is much more ergonomic for an edge case.

server := renderapi.NewServer(t)
server.Owners.Add(renderapi.NewOwner(client.Owner{Id: activeWorkspaceID}))
t.Setenv("RENDER_WORKSPACE", activeWorkspaceID)

project := server.CreateProject(
    renderapi.ProjectAttrs{Name: "My Project", OwnerId: activeWorkspaceID},
    renderapi.EnvAttrs{Name: "production"},
)
server.Postgres.Add(renderapi.NewPostgres(client.PostgresDetail{
    Name:          "prod-db",
    Owner:         client.Owner{Id: activeWorkspaceID},
    EnvironmentId: pointers.From(project.Env("production").Id),
}))

Common Gotchas

  • Don't block in Update() - use tea.Cmd for async work
  • Handle tea.KeyCtrlC and tea.KeyCtrlD for proper exit
  • Run reset if terminal breaks after a crash
  • Push() returns a tea.Cmd that must be returned from Update()
  • Generated types are read-only - regenerate via generate-cli.sh

Package-Specific Guides

Style Guide

All naming conventions, field formatting rules, and flag standards are defined in STYLE.md. See the Agent quick reference for the condensed spec used when creating or reviewing commands.

Reference