Quick reference for AI coding agents (Cursor, Copilot, Windsurf, etc.) working on this Go CLI codebase.
make ci # Format, vet, lint, unit tests, race detection, security, vuln, build
make ci-full # Complete CI: ci + integration tests + cleanup# All unit tests
make test-unit
# Specific package
make test-pkg PKG=email
# Single test by name
go test ./internal/cli/email/... -v -run TestSpecificName
# With race detection
go test ./internal/cli/email/... -v -race -run TestSpecificName
# Integration tests (requires NYLAS_API_KEY, NYLAS_GRANT_ID)
make test-integrationmake build # Build binary to bin/nylas
make install # Install to GOPATH/bin- Go 1.24.2 - Use modern features:
anyinstead ofinterface{}slicesandmapspackages instead of manual loops- Generic functions where appropriate
import (
"context" // 1. Standard library
"fmt"
"github.com/spf13/cobra" // 2. External packages
"github.com/nylas/cli/internal/ports" // 3. Internal packages
)// Always wrap errors with context
if err != nil {
return fmt.Errorf("failed to fetch emails: %w", err)
}
// Check errors immediately, don't defer
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()// Always use table-driven tests with t.Run()
func TestFormatSize(t *testing.T) {
tests := []struct {
name string
input int64
expected string
}{
{"zero bytes", 0, "0 B"},
{"kilobytes", 1024, "1.0 KB"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FormatSize(tt.input)
if got != tt.expected {
t.Errorf("got %q, want %q", got, tt.expected)
}
})
}
}- Ideal: ≤500 lines per file
- Maximum: ≤600 lines per file
- Split large files by responsibility (helpers, types, handlers)
// CLI client - use directly, no package wrappers
client := common.GetNylasClient()
// Grant ID from args
grantID := common.GetGrantID(args)
// Output formatting
common.PrintSuccess("Email sent successfully")
common.PrintError("Failed to send email", err)
common.FormatSize(bytes) // "1.5 MB"
common.FormatTimeAgo(time) // "2 hours ago"
common.PrintJSON(data) // Pretty-print JSON
// Structured output (use in list commands)
out := common.GetOutputWriter(cmd) // Gets writer based on --json/--yaml/--quiet
out.Write(data) // Outputs in correct format
// Client helpers (reduce boilerplate)
common.WithClient(args, func(ctx, client, grantID) (T, error) {
return client.DoSomething(ctx, grantID)
})
common.WithClientNoGrant(func(ctx, client) (T, error) {
return client.DoSomething(ctx)
})
// Flag helpers (use instead of inline flag definitions)
common.AddJSONFlag(cmd, &jsonOutput) // --json
common.AddLimitFlag(cmd, &limit, 25) // --limit/-n
common.AddYesFlag(cmd, &yes) // --yes/-y
common.AddFormatFlag(cmd, &format) // --format/-f
common.AddIDFlag(cmd, &showID) // --id
common.AddPageTokenFlag(cmd, &token) // --page-token
// Validation helpers (use instead of inline checks)
common.ValidateRequired("event ID", eventID)
common.ValidateRequiredFlag("--to", toEmail)
common.ValidateRequiredArg(args, "message ID")
common.ValidateURL("webhook URL", webhookURL)
common.ValidateEmail("recipient", email)
common.ValidateOneOf("status", status, []string{"pending", "active"})
common.ValidateAtLeastOne("update field", url, description, status)
// HTTP handlers (in adapters)
httputil.WriteJSON(w, http.StatusOK, data)
body, err := httputil.LimitedBody(r, maxSize)
httputil.DecodeJSON(r, &target)// In adapters/ai/ - use shared base_client.go helpers
ConvertMessagesToMaps(messages)
ConvertToolsOpenAIFormat(tools)
FallbackStreamChat(ctx, messages, opts)Hexagonal architecture with three layers:
CLI (internal/cli/)
↓ calls
Ports (internal/ports/) - Interfaces
↓ implemented by
Adapters (internal/adapters/) - Implementations
| Package | Purpose |
|---|---|
internal/domain/ |
Domain types (Email, Calendar, etc.) |
internal/ports/nylas.go |
Main NylasClient interface |
internal/ports/output.go |
OutputWriter interface |
internal/adapters/nylas/ |
Nylas API client implementation |
internal/adapters/output/ |
Table, JSON, YAML, Quiet formatters |
internal/httputil/ |
HTTP response helpers |
internal/cli/common/ |
Shared CLI helpers |
internal/air/ |
Web email client |
- Domain:
internal/domain/<feature>.go- Define types - Port:
internal/ports/nylas.go- Add interface methods - Adapter:
internal/adapters/nylas/<feature>.go- Implement - Mock:
internal/adapters/nylas/mock.go- Add mock methods - CLI:
internal/cli/<feature>/- Add commands - Register:
cmd/nylas/main.go- Wire command - Tests: Unit + integration tests
- Docs: Update
docs/COMMANDS.md
.env*,**/secrets/**- Contains secrets*.pem,*.key- Certificatesgo.sum- Auto-generated.git/,vendor/- Managed externally
Credentials are stored securely in the system keyring under service name "nylas".
| Key | Constant | Description |
|---|---|---|
client_id |
ports.KeyClientID |
Nylas Application/Client ID (auto-detected or manual) |
api_key |
ports.KeyAPIKey |
Nylas API key (required, used for Bearer auth) |
client_secret |
ports.KeyClientSecret |
Provider OAuth client secret (Google/Microsoft), optional |
org_id |
ports.KeyOrgID |
Nylas Organization ID (auto-detected) |
grants |
grantsKey |
JSON array of grant info (ID, email, provider) |
default_grant |
defaultGrantKey |
Default grant ID for CLI operations |
grant_token_<id> |
ports.GrantTokenKey() |
Per-grant access tokens |
| File | Purpose |
|---|---|
internal/ports/secrets.go |
Key constants (KeyClientID, KeyAPIKey, etc.) |
internal/adapters/keyring/keyring.go |
System keyring implementation |
internal/adapters/keyring/grants.go |
Grant storage (grants, default_grant keys) |
internal/app/auth/config.go |
SetupConfig() saves credentials |
- Linux: Secret Service (GNOME Keyring, KWallet)
- macOS: Keychain
- Windows: Windows Credential Manager
- Fallback: Encrypted file store (
~/.config/nylas/)
Set NYLAS_DISABLE_KEYRING=true to force encrypted file store (useful for testing).
- Nylas API v3 ONLY - Never use v1/v2
- Docs: https://developer.nylas.com/docs/api/v3/