Skip to content
Merged
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
127 changes: 127 additions & 0 deletions cmd/onecli/apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package main

import (
"encoding/json"
"fmt"

"github.com/onecli/onecli-cli/internal/api"
"github.com/onecli/onecli-cli/pkg/output"
"github.com/onecli/onecli-cli/pkg/validate"
)

// 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."`
}

// AppsListCmd is `onecli apps list`.
type AppsListCmd struct {
Fields string `optional:"" help:"Comma-separated list of fields to include in output."`
Quiet string `optional:"" name:"quiet" help:"Output only the specified field, one per line."`
Max int `optional:"" default:"20" help:"Maximum number of results to return."`
}

func (c *AppsListCmd) Run(out *output.Writer) error {
client, err := newClient()
if err != nil {
return err
}
apps, err := client.ListApps(newContext())
if err != nil {
return err
}
if c.Max > 0 && len(apps) > c.Max {
apps = apps[:c.Max]
}
if c.Quiet != "" {
return out.WriteQuiet(apps, c.Quiet)
}
return out.WriteFiltered(apps, c.Fields)
}

// AppsConnectCmd is `onecli apps connect`.
type AppsConnectCmd struct {
Provider string `required:"" help:"Provider name (e.g. 'google')."`
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
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,
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
}
}

if err := validate.ResourceID(input.Provider); err != nil {
return fmt.Errorf("invalid provider: %w", err)
}

if c.DryRun {
preview := map[string]string{
"provider": input.Provider,
"clientId": input.ClientID,
"clientSecret": "***",
}
return out.WriteDryRun("Would connect app", preview)
}

client, err := newClient()
if err != nil {
return err
}
app, err := client.ConnectApp(newContext(), input)
if 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(result)
}

// 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."`
}

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 c.DryRun {
return out.WriteDryRun("Would disconnect app", map[string]string{"id": c.ID})
}
client, err := newClient()
if err != nil {
return err
}
if err := client.DisconnectApp(newContext(), c.ID); err != nil {
return err
}
return out.Write(map[string]string{"status": "disconnected", "id": c.ID})
}
9 changes: 9 additions & 0 deletions cmd/onecli/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ 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: "--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: "rules list", Description: "List all policy rules."},
{Name: "rules create", Description: "Create a new policy rule.", Args: []ArgInfo{
{Name: "--name", Required: true, Description: "Display name for the rule."},
Expand Down
3 changes: 3 additions & 0 deletions cmd/onecli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type CLI struct {
Help HelpCmd `cmd:"" help:"Show available commands."`
Agents AgentsCmd `cmd:"" help:"Manage agents."`
Secrets SecretsCmd `cmd:"" help:"Manage secrets."`
Apps AppsCmd `cmd:"" help:"Manage app connections."`
Rules RulesCmd `cmd:"" help:"Manage policy rules."`
Auth AuthCmd `cmd:"" help:"Manage authentication."`
Config ConfigCmd `cmd:"" help:"Manage configuration settings."`
Expand Down Expand Up @@ -117,6 +118,8 @@ func hintForCommand(cmd, host string) string {
return "Manage your secrets \u2192 " + host
case "agents":
return "Manage your agents \u2192 " + host
case "apps":
return "Manage your app connections \u2192 " + host
case "rules":
return "Manage your policy rules \u2192 " + host
case "auth":
Expand Down
49 changes: 49 additions & 0 deletions internal/api/apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package api

import (
"context"
"fmt"
"net/http"
)

// App represents an app connection returned by the API.
type App struct {
ID string `json:"id"`
Provider string `json:"provider"`
Status string `json:"status"`
Docs string `json:"docs,omitempty"`
CreatedAt string `json:"createdAt"`
}

// ConnectAppInput is the request body for connecting an app.
type ConnectAppInput struct {
Provider string `json:"provider"`
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}

// ListApps returns all app connections for the authenticated user.
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 {
return nil, fmt.Errorf("listing apps: %w", err)
}
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)
}
return &app, 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 {
return fmt.Errorf("disconnecting app: %w", err)
}
return nil
}
1 change: 1 addition & 0 deletions pkg/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func NewWithWriters(out, err io.Writer) *Writer {
// property in every JSON response written to stdout.
func (w *Writer) SetHint(msg string) {
w.hint = msg
w.hintFn = nil
}

// SetHintFunc sets a function that lazily resolves the hint message at write
Expand Down
Loading