Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ out/
.github/copilot-instructions.md

# LLM files
.remember/
.remember/
9 changes: 9 additions & 0 deletions internal/cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s
return fmt.Errorf("failed to parse command inputs: %w", err)
}

if suggestion := suggestTypedCommand(inputs.Method, inputs.RawURI); suggestion != "" {
cli.renderer.Warnf(
"This endpoint has a dedicated command: `%s`. "+
"It offers schema discovery (--schema), input validation (--data), and structured output (--json). "+
"Running the raw request anyway.",
suggestion,
)
}

if inputs.Method == http.MethodDelete && !cli.force && cli.agentMode {
return errDestructiveNoConfirm
}
Expand Down
193 changes: 193 additions & 0 deletions internal/cli/api_typed_hints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package cli

import (
"net/http"
"strings"
)

// typedCommandHint links a Management API operation to the dedicated auth0-cli
// command that covers it. Typed commands add schema discovery (--schema),
// local payload validation (--data), and structured output (--json) on top of
// the raw request, so we nudge users toward them for discoverability. The raw
// request still runs β€” the hint never blocks the escape hatch.
type typedCommandHint struct {
// The HTTP method of the operation.
method string
// The API path relative to /api/v2/, where a "{...}" segment matches any
// single path segment (e.g. "clients/{id}").
path string
// The equivalent typed command to recommend.
command string
}

// typedCommandHints maps common Management API operations to their dedicated
// typed commands. It is intentionally NOT exhaustive: endpoints that have no
// typed command (for example /connections) are omitted so that no hint is
// printed for them and the raw request is used as intended.
var typedCommandHints = []typedCommandHint{
// Applications (clients).
{http.MethodGet, "clients", "auth0 apps list"},
{http.MethodPost, "clients", "auth0 apps create"},
{http.MethodGet, "clients/{id}", "auth0 apps show"},
{http.MethodPatch, "clients/{id}", "auth0 apps update"},
{http.MethodDelete, "clients/{id}", "auth0 apps delete"},

// APIs (resource servers).
{http.MethodGet, "resource-servers", "auth0 apis list"},
{http.MethodPost, "resource-servers", "auth0 apis create"},
{http.MethodGet, "resource-servers/{id}", "auth0 apis show"},
{http.MethodPatch, "resource-servers/{id}", "auth0 apis update"},
{http.MethodDelete, "resource-servers/{id}", "auth0 apis delete"},

// Actions.
{http.MethodGet, "actions/actions", "auth0 actions list"},
{http.MethodPost, "actions/actions", "auth0 actions create"},
{http.MethodGet, "actions/actions/{id}", "auth0 actions show"},
{http.MethodPatch, "actions/actions/{id}", "auth0 actions update"},
{http.MethodDelete, "actions/actions/{id}", "auth0 actions delete"},

// Users.
{http.MethodGet, "users", "auth0 users search"},
{http.MethodPost, "users", "auth0 users create"},
{http.MethodGet, "users/{id}", "auth0 users show"},
{http.MethodPatch, "users/{id}", "auth0 users update"},
{http.MethodDelete, "users/{id}", "auth0 users delete"},

// Roles.
{http.MethodGet, "roles", "auth0 roles list"},
{http.MethodPost, "roles", "auth0 roles create"},
{http.MethodGet, "roles/{id}", "auth0 roles show"},
{http.MethodPatch, "roles/{id}", "auth0 roles update"},
{http.MethodDelete, "roles/{id}", "auth0 roles delete"},
{http.MethodGet, "roles/{id}/permissions", "auth0 roles permissions list"},
{http.MethodPost, "roles/{id}/permissions", "auth0 roles permissions add"},
{http.MethodDelete, "roles/{id}/permissions", "auth0 roles permissions remove"},

// Organizations.
{http.MethodGet, "organizations", "auth0 orgs list"},
{http.MethodPost, "organizations", "auth0 orgs create"},
{http.MethodGet, "organizations/{id}", "auth0 orgs show"},
{http.MethodPatch, "organizations/{id}", "auth0 orgs update"},
{http.MethodDelete, "organizations/{id}", "auth0 orgs delete"},
{http.MethodGet, "organizations/{id}/members", "auth0 orgs members list"},
{http.MethodGet, "organizations/{id}/invitations", "auth0 orgs invitations list"},
{http.MethodPost, "organizations/{id}/invitations", "auth0 orgs invitations create"},

// Client grants.
{http.MethodGet, "client-grants", "auth0 client-grants list"},
{http.MethodPost, "client-grants", "auth0 client-grants create"},
{http.MethodPatch, "client-grants/{id}", "auth0 client-grants update"},
{http.MethodDelete, "client-grants/{id}", "auth0 client-grants delete"},

// Custom domains.
{http.MethodGet, "custom-domains", "auth0 domains list"},
{http.MethodPost, "custom-domains", "auth0 domains create"},
{http.MethodGet, "custom-domains/{id}", "auth0 domains show"},
{http.MethodDelete, "custom-domains/{id}", "auth0 domains delete"},

// Log streams.
{http.MethodGet, "log-streams", "auth0 logs streams list"},
{http.MethodPost, "log-streams", "auth0 logs streams create"},
{http.MethodGet, "log-streams/{id}", "auth0 logs streams show"},
{http.MethodPatch, "log-streams/{id}", "auth0 logs streams update"},
{http.MethodDelete, "log-streams/{id}", "auth0 logs streams delete"},

// Rules.
{http.MethodGet, "rules", "auth0 rules list"},
{http.MethodPost, "rules", "auth0 rules create"},
{http.MethodGet, "rules/{id}", "auth0 rules show"},
{http.MethodPatch, "rules/{id}", "auth0 rules update"},
{http.MethodDelete, "rules/{id}", "auth0 rules delete"},

// Network ACLs.
{http.MethodGet, "network-acls", "auth0 network-acl list"},
{http.MethodPost, "network-acls", "auth0 network-acl create"},
{http.MethodGet, "network-acls/{id}", "auth0 network-acl show"},
{http.MethodPatch, "network-acls/{id}", "auth0 network-acl update"},
{http.MethodDelete, "network-acls/{id}", "auth0 network-acl delete"},

// Event streams.
{http.MethodGet, "event-streams", "auth0 event-streams list"},
{http.MethodPost, "event-streams", "auth0 event-streams create"},
{http.MethodGet, "event-streams/{id}", "auth0 event-streams show"},
{http.MethodPatch, "event-streams/{id}", "auth0 event-streams update"},
{http.MethodDelete, "event-streams/{id}", "auth0 event-streams delete"},

// Tenant settings.
{http.MethodGet, "tenants/settings", "auth0 tenant-settings show"},
{http.MethodPatch, "tenants/settings", "auth0 tenant-settings update"},

// Attack protection.
{http.MethodGet, "attack-protection/brute-force-protection", "auth0 protection brute-force-protection show"},
{http.MethodPatch, "attack-protection/brute-force-protection", "auth0 protection brute-force-protection update"},
{http.MethodGet, "attack-protection/breached-password-detection", "auth0 protection breached-password-detection show"},
{http.MethodPatch, "attack-protection/breached-password-detection", "auth0 protection breached-password-detection update"},
{http.MethodGet, "attack-protection/suspicious-ip-throttling", "auth0 protection suspicious-ip-throttling show"},
{http.MethodPatch, "attack-protection/suspicious-ip-throttling", "auth0 protection suspicious-ip-throttling update"},

// Email provider.
{http.MethodGet, "emails/provider", "auth0 email provider show"},
{http.MethodPost, "emails/provider", "auth0 email provider create"},
{http.MethodPatch, "emails/provider", "auth0 email provider update"},
{http.MethodDelete, "emails/provider", "auth0 email provider delete"},
}

// suggestTypedCommand returns the recommended typed command for the given raw
// API method and URI, or "" when the endpoint has no dedicated typed command.
func suggestTypedCommand(method, rawURI string) string {
reqSegments := splitAPIPath(rawURI)
if len(reqSegments) == 0 {
return ""
}

for _, hint := range typedCommandHints {
if !strings.EqualFold(hint.method, method) {
continue
}

if pathMatchesPattern(reqSegments, strings.Split(hint.path, "/")) {
return hint.command
}
}

return ""
}

// splitAPIPath normalizes a raw API URI into its path segments: it drops any
// query or fragment, an optional "api/v2" prefix, and surrounding slashes.
func splitAPIPath(rawURI string) []string {
path := rawURI
if i := strings.IndexAny(path, "?#"); i >= 0 {
path = path[:i]
}

path = strings.Trim(path, "/")
path = strings.TrimPrefix(path, "api/v2")
path = strings.Trim(path, "/")
if path == "" {
return nil
}

return strings.Split(path, "/")
}

// pathMatchesPattern reports whether the request path segments match the
// pattern segments, where a "{...}" pattern segment matches any single
// segment and literal segments are compared case-insensitively.
func pathMatchesPattern(reqSegments, patternSegments []string) bool {
if len(reqSegments) != len(patternSegments) {
return false
}

for i, pattern := range patternSegments {
if strings.HasPrefix(pattern, "{") && strings.HasSuffix(pattern, "}") {
continue
}

if !strings.EqualFold(pattern, reqSegments[i]) {
return false
}
}

return true
}
115 changes: 115 additions & 0 deletions internal/cli/api_typed_hints_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package cli

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
)

func TestSuggestTypedCommand(t *testing.T) {
tests := []struct {
name string
method string
rawURI string
expected string
}{
{
name: "collection create maps to typed create",
method: http.MethodPost,
rawURI: "clients",
expected: "auth0 apps create",
},
{
name: "collection list maps to typed list",
method: http.MethodGet,
rawURI: "clients",
expected: "auth0 apps list",
},
{
name: "id path maps to typed show",
method: http.MethodGet,
rawURI: "clients/abc123",
expected: "auth0 apps show",
},
{
name: "id path maps to typed delete",
method: http.MethodDelete,
rawURI: "clients/abc123",
expected: "auth0 apps delete",
},
{
name: "nested actions path",
method: http.MethodPost,
rawURI: "actions/actions",
expected: "auth0 actions create",
},
{
name: "nested actions id path",
method: http.MethodPatch,
rawURI: "actions/actions/act_1",
expected: "auth0 actions update",
},
{
name: "sub-resource with placeholder before literal",
method: http.MethodGet,
rawURI: "roles/role_1/permissions",
expected: "auth0 roles permissions list",
},
{
name: "leading and trailing slashes are ignored",
method: http.MethodGet,
rawURI: "/resource-servers/",
expected: "auth0 apis list",
},
{
name: "query string is ignored",
method: http.MethodGet,
rawURI: "users?fields=email",
expected: "auth0 users search",
},
{
name: "method is matched case-insensitively",
method: "get",
rawURI: "roles",
expected: "auth0 roles list",
},
{
name: "no hint for endpoint without a typed command",
method: http.MethodGet,
rawURI: "connections",
expected: "",
},
{
name: "no hint when method has no typed equivalent",
method: http.MethodPut,
rawURI: "clients/abc123",
expected: "",
},
{
name: "no hint for unknown deeper path",
method: http.MethodGet,
rawURI: "clients/abc123/credentials",
expected: "",
},
{
name: "empty path returns no hint",
method: http.MethodGet,
rawURI: "",
expected: "",
},
{
name: "tenant settings singleton path",
method: http.MethodPatch,
rawURI: "tenants/settings",
expected: "auth0 tenant-settings update",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := suggestTypedCommand(test.method, test.rawURI)
assert.Equal(t, test.expected, actual)
})
}
}
Loading