Skip to content

Commit b6957c8

Browse files
committed
feat: add apps connect/list/disconnect commands for OAuth app connections
1 parent df39619 commit b6957c8

File tree

4 files changed

+176
-0
lines changed

4 files changed

+176
-0
lines changed

cmd/onecli/apps.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/onecli/onecli-cli/internal/api"
7+
"github.com/onecli/onecli-cli/pkg/output"
8+
"github.com/onecli/onecli-cli/pkg/validate"
9+
)
10+
11+
// AppsCmd is the `onecli apps` command group.
12+
type AppsCmd struct {
13+
List AppsListCmd `cmd:"" help:"List all app connections."`
14+
Connect AppsConnectCmd `cmd:"" help:"Connect an OAuth app (e.g. Google)."`
15+
Disconnect AppsDisconnectCmd `cmd:"" help:"Disconnect an app."`
16+
}
17+
18+
// AppsListCmd is `onecli apps list`.
19+
type AppsListCmd struct {
20+
Fields string `optional:"" help:"Comma-separated list of fields to include in output."`
21+
Quiet string `optional:"" name:"quiet" help:"Output only the specified field, one per line."`
22+
}
23+
24+
func (c *AppsListCmd) Run(out *output.Writer) error {
25+
client, err := newClient()
26+
if err != nil {
27+
return err
28+
}
29+
apps, err := client.ListApps(newContext())
30+
if err != nil {
31+
return err
32+
}
33+
if c.Quiet != "" {
34+
return out.WriteQuiet(apps, c.Quiet)
35+
}
36+
return out.WriteFiltered(apps, c.Fields)
37+
}
38+
39+
// AppsConnectCmd is `onecli apps connect`.
40+
type AppsConnectCmd struct {
41+
Provider string `required:"" help:"Provider name (e.g. 'google')."`
42+
ClientID string `required:"" name:"client-id" help:"OAuth client ID."`
43+
ClientSecret string `required:"" name:"client-secret" help:"OAuth client secret."`
44+
DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."`
45+
}
46+
47+
// connectResponse wraps the API response with agent-facing guidance.
48+
type connectResponse struct {
49+
ID string `json:"id"`
50+
Provider string `json:"provider"`
51+
Status string `json:"status"`
52+
CreatedAt string `json:"createdAt"`
53+
}
54+
55+
const docsBaseURL = "https://docs.onecli.sh/guides/credential-stubs"
56+
57+
func (c *AppsConnectCmd) Run(out *output.Writer) error {
58+
input := api.ConnectAppInput{
59+
Provider: c.Provider,
60+
ClientID: c.ClientID,
61+
ClientSecret: c.ClientSecret,
62+
}
63+
64+
if c.DryRun {
65+
preview := map[string]string{
66+
"provider": input.Provider,
67+
"clientId": input.ClientID,
68+
"clientSecret": "***",
69+
}
70+
return out.WriteDryRun("Would connect app", preview)
71+
}
72+
73+
client, err := newClient()
74+
if err != nil {
75+
return err
76+
}
77+
app, err := client.ConnectApp(newContext(), input)
78+
if err != nil {
79+
return err
80+
}
81+
82+
docsURL := docsBaseURL + "/" + input.Provider + ".md"
83+
fallbackURL := docsBaseURL + "/general-app.md"
84+
out.SetHint("Your MCP server needs local credential stub files to start. See " + docsURL + " for examples (fallback: " + fallbackURL + " ). The OneCLI gateway handles real OAuth token exchange at request time.")
85+
resp := connectResponse{
86+
ID: app.ID,
87+
Provider: app.Provider,
88+
Status: app.Status,
89+
CreatedAt: app.CreatedAt,
90+
}
91+
return out.Write(resp)
92+
}
93+
94+
// AppsDisconnectCmd is `onecli apps disconnect`.
95+
type AppsDisconnectCmd struct {
96+
ID string `required:"" help:"ID of the app connection to disconnect."`
97+
DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."`
98+
}
99+
100+
func (c *AppsDisconnectCmd) Run(out *output.Writer) error {
101+
if err := validate.ResourceID(c.ID); err != nil {
102+
return fmt.Errorf("invalid app ID: %w", err)
103+
}
104+
if c.DryRun {
105+
return out.WriteDryRun("Would disconnect app", map[string]string{"id": c.ID})
106+
}
107+
client, err := newClient()
108+
if err != nil {
109+
return err
110+
}
111+
if err := client.DisconnectApp(newContext(), c.ID); err != nil {
112+
return err
113+
}
114+
return out.Write(map[string]string{"status": "disconnected", "id": c.ID})
115+
}

cmd/onecli/help.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ func (cmd *HelpCmd) Run(out *output.Writer) error {
7979
{Name: "secrets delete", Description: "Delete a secret.", Args: []ArgInfo{
8080
{Name: "--id", Required: true, Description: "ID of the secret to delete."},
8181
}},
82+
{Name: "apps list", Description: "List all app connections."},
83+
{Name: "apps connect", Description: "Connect an OAuth app.", Args: []ArgInfo{
84+
{Name: "--provider", Required: true, Description: "Provider name (e.g. 'google')."},
85+
{Name: "--client-id", Required: true, Description: "OAuth client ID."},
86+
{Name: "--client-secret", Required: true, Description: "OAuth client secret."},
87+
}},
88+
{Name: "apps disconnect", Description: "Disconnect an app.", Args: []ArgInfo{
89+
{Name: "--id", Required: true, Description: "ID of the app connection to disconnect."},
90+
}},
8291
{Name: "rules list", Description: "List all policy rules."},
8392
{Name: "rules create", Description: "Create a new policy rule.", Args: []ArgInfo{
8493
{Name: "--name", Required: true, Description: "Display name for the rule."},

cmd/onecli/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type CLI struct {
2323
Help HelpCmd `cmd:"" help:"Show available commands."`
2424
Agents AgentsCmd `cmd:"" help:"Manage agents."`
2525
Secrets SecretsCmd `cmd:"" help:"Manage secrets."`
26+
Apps AppsCmd `cmd:"" help:"Manage app connections."`
2627
Rules RulesCmd `cmd:"" help:"Manage policy rules."`
2728
Auth AuthCmd `cmd:"" help:"Manage authentication."`
2829
Config ConfigCmd `cmd:"" help:"Manage configuration settings."`
@@ -114,6 +115,8 @@ func hintForCommand(cmd, host string) string {
114115
return "Manage your secrets \u2192 " + host
115116
case "agents":
116117
return "Manage your agents \u2192 " + host
118+
case "apps":
119+
return "Manage your app connections \u2192 " + host
117120
case "rules":
118121
return "Manage your policy rules \u2192 " + host
119122
case "auth":

internal/api/apps.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
)
8+
9+
// App represents an app connection returned by the API.
10+
type App struct {
11+
ID string `json:"id"`
12+
Provider string `json:"provider"`
13+
Status string `json:"status"`
14+
Docs string `json:"docs,omitempty"`
15+
CreatedAt string `json:"createdAt"`
16+
}
17+
18+
// ConnectAppInput is the request body for connecting an app.
19+
type ConnectAppInput struct {
20+
Provider string `json:"provider"`
21+
ClientID string `json:"clientId"`
22+
ClientSecret string `json:"clientSecret"`
23+
}
24+
25+
// ListApps returns all app connections for the authenticated user.
26+
func (c *Client) ListApps(ctx context.Context) ([]App, error) {
27+
var apps []App
28+
if err := c.do(ctx, http.MethodGet, "/api/apps", nil, &apps); err != nil {
29+
return nil, fmt.Errorf("listing apps: %w", err)
30+
}
31+
return apps, nil
32+
}
33+
34+
// ConnectApp creates a new app connection.
35+
func (c *Client) ConnectApp(ctx context.Context, input ConnectAppInput) (*App, error) {
36+
var app App
37+
if err := c.do(ctx, http.MethodPost, "/api/apps", input, &app); err != nil {
38+
return nil, fmt.Errorf("connecting app: %w", err)
39+
}
40+
return &app, nil
41+
}
42+
43+
// DisconnectApp removes an app connection by ID.
44+
func (c *Client) DisconnectApp(ctx context.Context, id string) error {
45+
if err := c.do(ctx, http.MethodDelete, "/api/apps/"+id, nil, nil); err != nil {
46+
return fmt.Errorf("disconnecting app: %w", err)
47+
}
48+
return nil
49+
}

0 commit comments

Comments
 (0)