Skip to content

Commit 0bd8bc0

Browse files
committed
Login, logout, publish
1 parent ec66e2b commit 0bd8bc0

File tree

11 files changed

+412
-5
lines changed

11 files changed

+412
-5
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,32 @@ This is the CLI for developing [Yaak](https://yaak.app) plugins.
88
```shell
99
npm install -g @yaakapp/cli
1010
```
11+
12+
## Commands
13+
14+
```
15+
$ yaakcli --help
16+
17+
Generate, build, and debug plugins for Yaak, the most intuitive desktop API client
18+
19+
Usage:
20+
yaakcli [flags]
21+
yaakcli [command]
22+
23+
Available Commands:
24+
build Transpile code into a runnable plugin bundle
25+
completion Generate the autocompletion script for the specified shell
26+
dev Build plugin bundle continuously when the filesystem changes
27+
generate Generate a "Hello World" Yaak plugin
28+
help Help about any command
29+
login Login to Yaak via web browser
30+
logout Sign out of the Yaak CLI
31+
publish Publish a Yaak plugin version to the plugin registry
32+
whoami Print the current logged-in user's info
33+
34+
Flags:
35+
-h, --help help for yaakcli
36+
--version Source directory to read from
37+
38+
Use "yaakcli [command] --help" for more information about a command.
39+
```

api.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package yaakcli
2+
3+
import (
4+
"github.com/pterm/pterm"
5+
"io"
6+
"net/http"
7+
"os"
8+
"time"
9+
)
10+
11+
func NewAPIRequest(method, path string, body io.Reader) *http.Request {
12+
baseURL := prodStagingDevStr("https://api.yaak.app", "https://todo.yaak.app", "http://localhost:9444")
13+
req, err := http.NewRequest(method, baseURL+"/api/v1"+path, body)
14+
if err != nil {
15+
pterm.Error.Printf("Failed to create API request: %s\n", err)
16+
os.Exit(1)
17+
}
18+
return req
19+
}
20+
21+
func SendAPIRequest(r *http.Request) []byte {
22+
found, token, err := getAuthToken()
23+
if err != nil {
24+
ExitError(err.Error())
25+
} else if !found {
26+
pterm.Warning.Println("Not logged in")
27+
pterm.Info.Println("Please run `yaakcli login`")
28+
os.Exit(1)
29+
}
30+
31+
r.Header.Set("X-Yaak-Session", token)
32+
33+
client := &http.Client{Timeout: 10 * time.Second}
34+
resp, err := client.Do(r)
35+
CheckError(err)
36+
37+
body, err := io.ReadAll(resp.Body)
38+
CheckError(err)
39+
40+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
41+
pterm.Error.Printf("API %d → %s\n", resp.StatusCode, body)
42+
os.Exit(1)
43+
}
44+
45+
return body
46+
}

auth.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package yaakcli
2+
3+
import (
4+
"errors"
5+
"github.com/zalando/go-keyring"
6+
)
7+
8+
const keyringUser = "yaak"
9+
10+
var keyringService = prodStagingDevStr("app.yaak.cli.Token", "app.yaak.cli.staging.Token", "app.yaak.cli.dev.Token")
11+
12+
func getAuthToken() (bool, string, error) {
13+
token, err := keyring.Get(keyringService, keyringUser)
14+
if errors.Is(err, keyring.ErrNotFound) {
15+
return false, "", nil
16+
} else if err != nil {
17+
return false, "", err
18+
}
19+
20+
return true, token, nil
21+
}
22+
23+
func storeAuthToken(token string) error {
24+
return keyring.Set(keyringService, keyringUser, token)
25+
}
26+
27+
func deleteAuthToken() error {
28+
err := keyring.Delete(keyringService, keyringUser)
29+
if errors.Is(err, keyring.ErrNotFound) {
30+
return nil
31+
}
32+
return err
33+
}

cmd_login.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package yaakcli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/pkg/browser"
7+
"github.com/pterm/pterm"
8+
"github.com/spf13/cobra"
9+
"net/http"
10+
"net/url"
11+
"os"
12+
"os/signal"
13+
"time"
14+
)
15+
16+
var loginCmd = &cobra.Command{
17+
Use: "login",
18+
Short: "Login to Yaak via web browser",
19+
Long: "Open a web browser to authenticate with Yaak. Works with all browsers including Safari.",
20+
Run: func(cmd *cobra.Command, args []string) {
21+
CheckError(deleteAuthToken())
22+
23+
pterm.Info.Println("Starting browser-based login...")
24+
25+
// Save the token to a config file
26+
confirm := pterm.DefaultInteractiveConfirm
27+
confirm.DefaultValue = true
28+
open, err := confirm.Show("Open default browser")
29+
CheckError(err)
30+
31+
if !open {
32+
os.Exit(0)
33+
return
34+
}
35+
36+
// Create a channel to receive the auth token
37+
tokenChan := make(chan string, 1)
38+
39+
// Create a context that we can cancel
40+
_, cancel := context.WithCancel(context.Background())
41+
defer cancel()
42+
43+
// Set up a simple HTTP server to handle the OAuth callback
44+
server := &http.Server{
45+
Addr: "localhost:8085",
46+
}
47+
48+
// Define the handler for the callback
49+
http.HandleFunc("/oauth/callback", func(w http.ResponseWriter, r *http.Request) {
50+
// Get the token from the query parameters
51+
token := r.URL.Query().Get("token")
52+
if token == "" {
53+
w.WriteHeader(http.StatusBadRequest)
54+
_, _ = fmt.Fprintf(w, "Error: No token provided")
55+
return
56+
}
57+
58+
// Send the token to the channel
59+
tokenChan <- token
60+
61+
// Return a success message to the browser
62+
redirectTo := prodStagingDevStr(
63+
"https://yaak.app/login-cli/success",
64+
"https://todo.yaak.app/login-cli/success",
65+
"http://localhost:9444/login-cli/success",
66+
)
67+
http.Redirect(w, r, redirectTo, http.StatusFound)
68+
})
69+
70+
// Start the server in a goroutine
71+
go func() {
72+
if err := server.ListenAndServe(); err != http.ErrServerClosed {
73+
pterm.Error.Printf("HTTP server error: %v\n", err)
74+
os.Exit(1)
75+
}
76+
}()
77+
78+
// Set up a signal handler to gracefully shut down the server
79+
sigChan := make(chan os.Signal, 1)
80+
signal.Notify(sigChan, os.Interrupt)
81+
82+
// Open the browser to the login page
83+
redirect := "http://localhost:8085/oauth/callback"
84+
loginURL := prodStagingDevStr(
85+
"https://yaak.app/login-cli?redirect=",
86+
"https://todo.yaak.app/login-cli?redirect=",
87+
"http://localhost:9444/login-cli?redirect=",
88+
) + url.QueryEscape(redirect)
89+
90+
// Open the browser based on the operating system
91+
err = browser.OpenURL(loginURL)
92+
if err != nil {
93+
pterm.Error.Printf("Failed to open browser: %v\n", err)
94+
pterm.Info.Println("Please open the following URL manually:")
95+
pterm.Info.Println(loginURL)
96+
}
97+
98+
// Wait for either the token, a signal, or a timeout
99+
pterm.Info.Println("Waiting for authentication...")
100+
101+
select {
102+
case token := <-tokenChan:
103+
pterm.Success.Println("Authentication successful!")
104+
105+
// set password
106+
err = storeAuthToken(token)
107+
CheckError(err)
108+
109+
// Shutdown the server
110+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
111+
defer cancel()
112+
if err := server.Shutdown(ctx); err != nil {
113+
pterm.Error.Printf("Server shutdown error: %v\n", err)
114+
}
115+
116+
case <-sigChan:
117+
pterm.Warning.Println("Interrupted by user. Shutting down...")
118+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
119+
defer cancel()
120+
if err := server.Shutdown(ctx); err != nil {
121+
pterm.Error.Printf("Server shutdown error: %v\n", err)
122+
}
123+
124+
case <-time.After(5 * time.Minute):
125+
pterm.Warning.Println("Timeout waiting for authentication. Shutting down...")
126+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
127+
defer cancel()
128+
if err := server.Shutdown(ctx); err != nil {
129+
pterm.Error.Printf("Server shutdown error: %v\n", err)
130+
}
131+
}
132+
},
133+
}

cmd_logout.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package yaakcli
2+
3+
import (
4+
"github.com/pterm/pterm"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
var logoutCmd = &cobra.Command{
9+
Use: "logout",
10+
Short: "Sign out of the Yaak CLI",
11+
Run: func(cmd *cobra.Command, args []string) {
12+
err := deleteAuthToken()
13+
CheckError(err)
14+
pterm.Success.Println("Signed out of Yaak")
15+
},
16+
}

cmd_publish.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package yaakcli
2+
3+
import (
4+
"archive/zip"
5+
"fmt"
6+
"github.com/pterm/pterm"
7+
"github.com/spf13/cobra"
8+
"io"
9+
"os"
10+
"path/filepath"
11+
)
12+
13+
var publishCmd = &cobra.Command{
14+
Use: "publish",
15+
Short: "Publish a Yaak plugin version to the plugin registry",
16+
Args: cobra.MaximumNArgs(1),
17+
Run: func(cmd *cobra.Command, args []string) {
18+
pluginDir, err := os.Getwd()
19+
CheckError(err)
20+
21+
if len(args) > 0 {
22+
pluginDir, err = filepath.Abs(args[0])
23+
CheckError(err)
24+
}
25+
26+
zipPipeReader, zipPipeWriter := io.Pipe()
27+
28+
zipWriter := zip.NewWriter(zipPipeWriter)
29+
30+
selected := make(map[string]bool)
31+
optionalFiles := []string{"README.md"}
32+
requiredFiles := []string{"package.json", "package-lock.json", "build/index.js"}
33+
for _, name := range optionalFiles {
34+
selected[filepath.Clean(name)] = true
35+
}
36+
37+
for _, name := range requiredFiles {
38+
selected[filepath.Clean(name)] = true
39+
_, err := os.Stat(filepath.Join(pluginDir, name))
40+
if err != nil {
41+
pterm.Warning.Printf("Missing required file: %s\n", name)
42+
os.Exit(1)
43+
}
44+
}
45+
46+
pterm.Info.Println("Creating zip file ", pluginDir)
47+
48+
go func() {
49+
defer func() {
50+
CheckError(zipWriter.Close())
51+
CheckError(zipPipeWriter.Close())
52+
}()
53+
54+
err = filepath.Walk(pluginDir, func(path string, info os.FileInfo, err error) error {
55+
if err != nil {
56+
return err
57+
}
58+
59+
if info.IsDir() {
60+
return nil
61+
}
62+
63+
relPath, err := filepath.Rel(pluginDir, path)
64+
if err != nil {
65+
return err
66+
}
67+
68+
relPath = filepath.ToSlash(relPath) // Normalize for zip entries
69+
70+
if !selected[relPath] {
71+
return nil
72+
}
73+
74+
file, err := os.Open(path)
75+
if err != nil {
76+
return err
77+
}
78+
defer func(file *os.File) {
79+
err := file.Close()
80+
CheckError(err)
81+
}(file)
82+
83+
writer, err := zipWriter.Create(relPath)
84+
if err != nil {
85+
return err
86+
}
87+
88+
_, err = io.Copy(writer, file)
89+
90+
return err
91+
})
92+
CheckError(err)
93+
pterm.Info.Println("Zip file created")
94+
}()
95+
96+
req := NewAPIRequest("POST", "/plugins/publish", zipPipeReader)
97+
body := SendAPIRequest(req)
98+
pterm.Success.Println("Plugin published")
99+
fmt.Printf("%s\n", body)
100+
},
101+
}

cmd_root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ func rootCmd(version string) *cobra.Command {
2323
cmd.AddCommand(devCmd)
2424
cmd.AddCommand(buildCmd)
2525
cmd.AddCommand(generateCmd)
26+
cmd.AddCommand(whoamiCmd)
27+
cmd.AddCommand(loginCmd)
28+
cmd.AddCommand(logoutCmd)
29+
cmd.AddCommand(publishCmd)
2630

2731
cmd.Flags().BoolVar(&fVersion, "version", false, "Source directory to read from")
2832

cmd_whoami.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package yaakcli
2+
3+
import (
4+
"github.com/pterm/pterm"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
var whoamiCmd = &cobra.Command{
9+
Use: "whoami",
10+
Short: "Print the current logged-in user's info",
11+
Run: func(cmd *cobra.Command, args []string) {
12+
req := NewAPIRequest("GET", "/whoami", nil)
13+
body := SendAPIRequest(req)
14+
pterm.Info.Printf("%s\n", body)
15+
},
16+
}

0 commit comments

Comments
 (0)