chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/zzet/gortex/internal/daemon"
|
||||
)
|
||||
|
||||
// cloud is the umbrella command for all cloud-side flows that run
|
||||
// from the local CLI: login, list, logout. These commands talk to
|
||||
// the gortex-cloud control plane (API) — they never run cloud code
|
||||
// in-process. The two repos are decoupled at the module boundary.
|
||||
var cloudCmd = &cobra.Command{
|
||||
Use: "cloud",
|
||||
Short: "Manage Gortex Cloud connections (login, list, logout)",
|
||||
}
|
||||
|
||||
var (
|
||||
cloudEndpoint string
|
||||
cloudWorkspace string
|
||||
cloudToken string
|
||||
)
|
||||
|
||||
var cloudLoginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Connect this machine to a Gortex Cloud workspace",
|
||||
Long: `Add a [[server]] entry to ~/.gortex/servers.toml pointing at the
|
||||
gortex-cloud control plane (mcp.gortex.dev/v1 by default) for one
|
||||
workspace. The token is the gtx_live_… string the admin console
|
||||
prints once at issuance.
|
||||
|
||||
Example:
|
||||
gortex cloud login --endpoint https://mcp.gortex.dev/v1 \
|
||||
--workspace tuck --token gtx_live_abc...
|
||||
`,
|
||||
RunE: runCloudLogin,
|
||||
}
|
||||
|
||||
var cloudListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Show every cloud workspace this machine is connected to",
|
||||
RunE: runCloudList,
|
||||
}
|
||||
|
||||
var cloudLogoutCmd = &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Remove a cloud workspace from ~/.gortex/servers.toml",
|
||||
RunE: runCloudLogout,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cloudLoginCmd.Flags().StringVar(&cloudEndpoint, "endpoint", "https://mcp.gortex.dev/v1", "cloud control-plane base URL")
|
||||
cloudLoginCmd.Flags().StringVar(&cloudWorkspace, "workspace", "", "workspace slug to bind this token to (required)")
|
||||
cloudLoginCmd.Flags().StringVar(&cloudToken, "token", "", "API token (gtx_live_… or gtx_test_…) issued in the admin console (required)")
|
||||
_ = cloudLoginCmd.MarkFlagRequired("workspace")
|
||||
_ = cloudLoginCmd.MarkFlagRequired("token")
|
||||
|
||||
cloudLogoutCmd.Flags().StringVar(&cloudWorkspace, "workspace", "", "workspace slug to disconnect (required)")
|
||||
_ = cloudLogoutCmd.MarkFlagRequired("workspace")
|
||||
|
||||
cloudCmd.AddCommand(cloudLoginCmd, cloudListCmd, cloudLogoutCmd)
|
||||
rootCmd.AddCommand(cloudCmd)
|
||||
}
|
||||
|
||||
func runCloudLogin(_ *cobra.Command, _ []string) error {
|
||||
cloudToken = strings.TrimSpace(cloudToken)
|
||||
if cloudToken == "" {
|
||||
return errors.New("--token is required")
|
||||
}
|
||||
if !strings.HasPrefix(cloudToken, "gtx_live_") && !strings.HasPrefix(cloudToken, "gtx_test_") {
|
||||
return errors.New("--token must start with gtx_live_ or gtx_test_")
|
||||
}
|
||||
if cloudWorkspace == "" {
|
||||
return errors.New("--workspace is required")
|
||||
}
|
||||
|
||||
// Verify the token by hitting /v1/workspaces/repos against the
|
||||
// endpoint. If the call returns 401 we surface it before
|
||||
// persisting any config.
|
||||
if err := pingCloudEndpoint(cloudEndpoint, cloudToken); err != nil {
|
||||
return fmt.Errorf("verify cloud connection: %w", err)
|
||||
}
|
||||
|
||||
path := daemon.ServersConfigPath()
|
||||
cfg, err := daemon.LoadServersConfig(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load servers config: %w", err)
|
||||
}
|
||||
|
||||
slug := "cloud-" + cloudWorkspace
|
||||
for i, s := range cfg.Server {
|
||||
if s.Slug == slug {
|
||||
cfg.Server = append(cfg.Server[:i], cfg.Server[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := cfg.AddServer(daemon.ServerEntry{
|
||||
Slug: slug,
|
||||
URL: cloudEndpoint,
|
||||
AuthToken: cloudToken,
|
||||
Workspaces: []string{cloudWorkspace},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("add server: %w", err)
|
||||
}
|
||||
if err := cfg.Save(path); err != nil {
|
||||
return fmt.Errorf("save servers config: %w", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "logged in: workspace=%s endpoint=%s slug=%s\n", cloudWorkspace, cloudEndpoint, slug)
|
||||
fmt.Fprintf(os.Stdout, "next: gortex daemon start (or already running)\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCloudList(_ *cobra.Command, _ []string) error {
|
||||
cfg, err := daemon.LoadServersConfig(daemon.ServersConfigPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cfg.Server) == 0 {
|
||||
fmt.Fprintln(os.Stdout, "no [[server]] entries — run `gortex cloud login` to add one")
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "%-24s %-46s %s\n", "SLUG", "ENDPOINT", "WORKSPACES")
|
||||
for _, s := range cfg.Server {
|
||||
fmt.Fprintf(os.Stdout, "%-24s %-46s %s\n", s.Slug, s.URL, strings.Join(s.Workspaces, ","))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCloudLogout(_ *cobra.Command, _ []string) error {
|
||||
path := daemon.ServersConfigPath()
|
||||
cfg, err := daemon.LoadServersConfig(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slug := "cloud-" + cloudWorkspace
|
||||
out := cfg.Server[:0]
|
||||
removed := false
|
||||
for _, s := range cfg.Server {
|
||||
if s.Slug == slug {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
cfg.Server = out
|
||||
if !removed {
|
||||
return fmt.Errorf("no entry with slug %q found", slug)
|
||||
}
|
||||
if err := cfg.Save(path); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "logged out: %s\n", slug)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pingCloudEndpoint(endpoint, token string) error {
|
||||
if !strings.HasSuffix(endpoint, "/") {
|
||||
endpoint += "/"
|
||||
}
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet,
|
||||
strings.TrimSuffix(endpoint, "/")+"/workspaces/repos", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
c := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
return errors.New("token rejected (401)")
|
||||
}
|
||||
// Show a short tail of the response to help debug.
|
||||
if len(body) > 200 {
|
||||
body = body[:200]
|
||||
}
|
||||
return fmt.Errorf("unexpected %d: %s", resp.StatusCode, bytes.TrimSpace(body))
|
||||
}
|
||||
|
||||
// Keep encoding/json import — `gortex cloud login` will grow to
|
||||
// parse a /v1/me response in a follow-up; declaring it now keeps
|
||||
// the import section stable for that change.
|
||||
var _ = json.RawMessage(nil)
|
||||
Reference in New Issue
Block a user