Skip to content

Commit bfe944d

Browse files
authored
Merge pull request #9
* Login, logout, publish * Fix version help * Remove unused context * Update api.go * Merge remote-tracking branch 'origin/plugins' into plugins * Better logging
1 parent 8a81b30 commit bfe944d

File tree

11 files changed

+450
-11
lines changed

11 files changed

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

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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
// Set up a simple HTTP server to handle the OAuth callback
40+
server := &http.Server{
41+
Addr: "localhost:8085",
42+
}
43+
44+
// Define the handler for the callback
45+
http.HandleFunc("/oauth/callback", func(w http.ResponseWriter, r *http.Request) {
46+
// Get the token from the query parameters
47+
token := r.URL.Query().Get("token")
48+
if token == "" {
49+
w.WriteHeader(http.StatusBadRequest)
50+
_, _ = fmt.Fprintf(w, "Error: No token provided")
51+
return
52+
}
53+
54+
// Send the token to the channel
55+
tokenChan <- token
56+
57+
// Return a success message to the browser
58+
redirectTo := prodStagingDevStr(
59+
"https://yaak.app/login-cli/success",
60+
"https://todo.yaak.app/login-cli/success",
61+
"http://localhost:9444/login-cli/success",
62+
)
63+
http.Redirect(w, r, redirectTo, http.StatusFound)
64+
})
65+
66+
// Start the server in a goroutine
67+
go func() {
68+
if err := server.ListenAndServe(); err != http.ErrServerClosed {
69+
pterm.Error.Printf("HTTP server error: %v\n", err)
70+
os.Exit(1)
71+
}
72+
}()
73+
74+
// Set up a signal handler to gracefully shut down the server
75+
sigChan := make(chan os.Signal, 1)
76+
signal.Notify(sigChan, os.Interrupt)
77+
78+
// Open the browser to the login page
79+
redirect := "http://localhost:8085/oauth/callback"
80+
loginURL := prodStagingDevStr(
81+
"https://yaak.app/login-cli?redirect=",
82+
"https://todo.yaak.app/login-cli?redirect=",
83+
"http://localhost:9444/login-cli?redirect=",
84+
) + url.QueryEscape(redirect)
85+
86+
// Open the browser based on the operating system
87+
err = browser.OpenURL(loginURL)
88+
if err != nil {
89+
pterm.Error.Printf("Failed to open browser: %v\n", err)
90+
pterm.Info.Println("Please open the following URL manually:")
91+
pterm.Info.Println(loginURL)
92+
}
93+
94+
// Wait for either the token, a signal, or a timeout
95+
pterm.Info.Println("Waiting for authentication...")
96+
97+
select {
98+
case token := <-tokenChan:
99+
pterm.Success.Println("Authentication successful!")
100+
101+
// set password
102+
err = storeAuthToken(token)
103+
CheckError(err)
104+
105+
// Shutdown the server
106+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
107+
defer cancel()
108+
if err := server.Shutdown(ctx); err != nil {
109+
pterm.Error.Printf("Server shutdown error: %v\n", err)
110+
}
111+
112+
case <-sigChan:
113+
pterm.Warning.Println("Interrupted by user. Shutting down...")
114+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
115+
defer cancel()
116+
if err := server.Shutdown(ctx); err != nil {
117+
pterm.Error.Printf("Server shutdown error: %v\n", err)
118+
}
119+
120+
case <-time.After(5 * time.Minute):
121+
pterm.Warning.Println("Timeout waiting for authentication. Shutting down...")
122+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
123+
defer cancel()
124+
if err := server.Shutdown(ctx); err != nil {
125+
pterm.Error.Printf("Server shutdown error: %v\n", err)
126+
}
127+
}
128+
},
129+
}

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

0 commit comments

Comments
 (0)