From 55440e7483cd4f1ad0898ac1a6429041f4b000f0 Mon Sep 17 00:00:00 2001 From: Guy Ben Aharon Date: Mon, 6 Apr 2026 14:29:29 +0300 Subject: [PATCH] fix: unify REST API under /api/apps/:provider --- cmd/onecli/apps.go | 83 +++++++++++++++++++++++++------------------- cmd/onecli/help.go | 13 ++++--- internal/api/apps.go | 60 ++++++++++++++++++++++---------- 3 files changed, 96 insertions(+), 60 deletions(-) diff --git a/cmd/onecli/apps.go b/cmd/onecli/apps.go index a5cd3ee..f37ad73 100644 --- a/cmd/onecli/apps.go +++ b/cmd/onecli/apps.go @@ -11,9 +11,10 @@ import ( // AppsCmd is the `onecli apps` command group. type AppsCmd struct { - List AppsListCmd `cmd:"" help:"List all app connections."` - Connect AppsConnectCmd `cmd:"" help:"Connect an OAuth app (e.g. Google)."` - Disconnect AppsDisconnectCmd `cmd:"" help:"Disconnect an app."` + List AppsListCmd `cmd:"" help:"List all apps with config and connection status."` + Configure AppsConfigureCmd `cmd:"" help:"Save OAuth credentials (BYOC) for a provider."` + Remove AppsRemoveCmd `cmd:"" help:"Remove OAuth credentials for a provider."` + Disconnect AppsDisconnectCmd `cmd:"" help:"Disconnect an app connection."` } // AppsListCmd is `onecli apps list`. @@ -41,87 +42,97 @@ func (c *AppsListCmd) Run(out *output.Writer) error { return out.WriteFiltered(apps, c.Fields) } -// AppsConnectCmd is `onecli apps connect`. -type AppsConnectCmd struct { - Provider string `required:"" help:"Provider name (e.g. 'google')."` +// AppsConfigureCmd is `onecli apps configure`. +type AppsConfigureCmd struct { + Provider string `required:"" help:"Provider name (e.g. 'github', 'gmail')."` ClientID string `required:"" name:"client-id" help:"OAuth client ID."` ClientSecret string `required:"" name:"client-secret" help:"OAuth client secret."` Json string `optional:"" help:"Raw JSON payload. Overrides individual flags."` DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."` } -const docsBaseURL = "https://onecli.sh/docs/guides/credential-stubs" - -// connectResult wraps the API response with onboarding guidance as structured fields. -type connectResult struct { - api.App - NextSteps string `json:"next_steps"` - DocsURL string `json:"docs_url"` -} - -func (c *AppsConnectCmd) Run(out *output.Writer) error { - var input api.ConnectAppInput +func (c *AppsConfigureCmd) Run(out *output.Writer) error { + var input api.ConfigAppInput if c.Json != "" { if err := json.Unmarshal([]byte(c.Json), &input); err != nil { return fmt.Errorf("invalid JSON payload: %w", err) } } else { - input = api.ConnectAppInput{ - Provider: c.Provider, + input = api.ConfigAppInput{ ClientID: c.ClientID, ClientSecret: c.ClientSecret, } } - if err := validate.ResourceID(input.Provider); err != nil { + if err := validate.ResourceID(c.Provider); err != nil { return fmt.Errorf("invalid provider: %w", err) } if c.DryRun { preview := map[string]string{ - "provider": input.Provider, + "provider": c.Provider, "clientId": input.ClientID, "clientSecret": "***", } - return out.WriteDryRun("Would connect app", preview) + return out.WriteDryRun("Would configure app", preview) } client, err := newClient() if err != nil { return err } - app, err := client.ConnectApp(newContext(), input) - if err != nil { + if err := client.ConfigureApp(newContext(), c.Provider, input); err != nil { return err } - result := connectResult{ - App: *app, - NextSteps: "Create local credential stub files using 'onecli-managed' as placeholder for all secrets. The OneCLI gateway handles real OAuth token exchange at request time.", - DocsURL: docsBaseURL + "/" + input.Provider + ".md", + return out.Write(map[string]string{ + "status": "configured", + "provider": c.Provider, + }) +} + +// AppsRemoveCmd is `onecli apps remove`. +type AppsRemoveCmd struct { + Provider string `required:"" help:"Provider name (e.g. 'github', 'gmail')."` + DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."` +} + +func (c *AppsRemoveCmd) Run(out *output.Writer) error { + if err := validate.ResourceID(c.Provider); err != nil { + return fmt.Errorf("invalid provider: %w", err) } - return out.Write(result) + if c.DryRun { + return out.WriteDryRun("Would remove app config", map[string]string{"provider": c.Provider}) + } + client, err := newClient() + if err != nil { + return err + } + if err := client.UnconfigureApp(newContext(), c.Provider); err != nil { + return err + } + return out.Write(map[string]string{"status": "removed", "provider": c.Provider}) } // AppsDisconnectCmd is `onecli apps disconnect`. type AppsDisconnectCmd struct { - ID string `required:"" help:"ID of the app connection to disconnect."` - DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."` + Provider string `required:"" help:"Provider name (e.g. 'github', 'gmail')."` + DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."` } func (c *AppsDisconnectCmd) Run(out *output.Writer) error { - if err := validate.ResourceID(c.ID); err != nil { - return fmt.Errorf("invalid app ID: %w", err) + if err := validate.ResourceID(c.Provider); err != nil { + return fmt.Errorf("invalid provider: %w", err) } if c.DryRun { - return out.WriteDryRun("Would disconnect app", map[string]string{"id": c.ID}) + return out.WriteDryRun("Would disconnect app", map[string]string{"provider": c.Provider}) } client, err := newClient() if err != nil { return err } - if err := client.DisconnectApp(newContext(), c.ID); err != nil { + if err := client.DisconnectApp(newContext(), c.Provider); err != nil { return err } - return out.Write(map[string]string{"status": "disconnected", "id": c.ID}) + return out.Write(map[string]string{"status": "disconnected", "provider": c.Provider}) } diff --git a/cmd/onecli/help.go b/cmd/onecli/help.go index c598e57..73fc5d3 100644 --- a/cmd/onecli/help.go +++ b/cmd/onecli/help.go @@ -79,14 +79,17 @@ func (cmd *HelpCmd) Run(out *output.Writer) error { {Name: "secrets delete", Description: "Delete a secret.", Args: []ArgInfo{ {Name: "--id", Required: true, Description: "ID of the secret to delete."}, }}, - {Name: "apps list", Description: "List all app connections."}, - {Name: "apps connect", Description: "Connect an OAuth app.", Args: []ArgInfo{ - {Name: "--provider", Required: true, Description: "Provider name (e.g. 'google')."}, + {Name: "apps list", Description: "List all apps with config and connection status."}, + {Name: "apps configure", Description: "Save OAuth credentials (BYOC) for a provider.", Args: []ArgInfo{ + {Name: "--provider", Required: true, Description: "Provider name (e.g. 'github', 'gmail')."}, {Name: "--client-id", Required: true, Description: "OAuth client ID."}, {Name: "--client-secret", Required: true, Description: "OAuth client secret."}, }}, - {Name: "apps disconnect", Description: "Disconnect an app.", Args: []ArgInfo{ - {Name: "--id", Required: true, Description: "ID of the app connection to disconnect."}, + {Name: "apps remove", Description: "Remove OAuth credentials for a provider.", Args: []ArgInfo{ + {Name: "--provider", Required: true, Description: "Provider name (e.g. 'github', 'gmail')."}, + }}, + {Name: "apps disconnect", Description: "Disconnect an app connection.", Args: []ArgInfo{ + {Name: "--provider", Required: true, Description: "Provider name (e.g. 'github', 'gmail')."}, }}, {Name: "rules list", Description: "List all policy rules."}, {Name: "rules create", Description: "Create a new policy rule.", Args: []ArgInfo{ diff --git a/internal/api/apps.go b/internal/api/apps.go index c7f6da2..7df68cc 100644 --- a/internal/api/apps.go +++ b/internal/api/apps.go @@ -6,23 +6,37 @@ import ( "net/http" ) -// App represents an app connection returned by the API. +// App represents an app from the unified /api/apps listing. type App struct { - ID string `json:"id"` - Provider string `json:"provider"` - Status string `json:"status"` - Docs string `json:"docs,omitempty"` - CreatedAt string `json:"createdAt"` + ID string `json:"id"` + Name string `json:"name"` + Available bool `json:"available"` + ConnectionType string `json:"connectionType"` + Configurable bool `json:"configurable"` + Config *AppConfig `json:"config"` + Connection *AppConnection `json:"connection"` } -// ConnectAppInput is the request body for connecting an app. -type ConnectAppInput struct { - Provider string `json:"provider"` +// AppConfig is the BYOC credential configuration status. +type AppConfig struct { + HasCredentials bool `json:"hasCredentials"` + Enabled bool `json:"enabled"` +} + +// AppConnection is the OAuth connection status. +type AppConnection struct { + Status string `json:"status"` + Scopes []string `json:"scopes"` + ConnectedAt string `json:"connectedAt"` +} + +// ConfigAppInput is the request body for saving BYOC credentials. +type ConfigAppInput struct { ClientID string `json:"clientId"` ClientSecret string `json:"clientSecret"` } -// ListApps returns all app connections for the authenticated user. +// ListApps returns all apps with their config and connection status. func (c *Client) ListApps(ctx context.Context) ([]App, error) { var apps []App if err := c.do(ctx, http.MethodGet, "/api/apps", nil, &apps); err != nil { @@ -31,18 +45,26 @@ func (c *Client) ListApps(ctx context.Context) ([]App, error) { return apps, nil } -// ConnectApp creates a new app connection. -func (c *Client) ConnectApp(ctx context.Context, input ConnectAppInput) (*App, error) { - var app App - if err := c.do(ctx, http.MethodPost, "/api/apps", input, &app); err != nil { - return nil, fmt.Errorf("connecting app: %w", err) +// ConfigureApp saves BYOC credentials for a provider. +func (c *Client) ConfigureApp(ctx context.Context, provider string, input ConfigAppInput) error { + var resp SuccessResponse + if err := c.do(ctx, http.MethodPost, "/api/apps/"+provider+"/config", input, &resp); err != nil { + return fmt.Errorf("configuring app: %w", err) } - return &app, nil + return nil +} + +// UnconfigureApp removes BYOC credentials for a provider. +func (c *Client) UnconfigureApp(ctx context.Context, provider string) error { + if err := c.do(ctx, http.MethodDelete, "/api/apps/"+provider+"/config", nil, nil); err != nil { + return fmt.Errorf("unconfiguring app: %w", err) + } + return nil } -// DisconnectApp removes an app connection by ID. -func (c *Client) DisconnectApp(ctx context.Context, id string) error { - if err := c.do(ctx, http.MethodDelete, "/api/apps/"+id, nil, nil); err != nil { +// DisconnectApp removes the OAuth connection for a provider. +func (c *Client) DisconnectApp(ctx context.Context, provider string) error { + if err := c.do(ctx, http.MethodDelete, "/api/apps/"+provider+"/connection", nil, nil); err != nil { return fmt.Errorf("disconnecting app: %w", err) } return nil