1
package main
1
package main
2
2
3
import "bufio"
3
import "bufio"
4
import "fmt"
4
import "fmt"
5
import "io"
5
import "io"
6
import "os"
6
import "os"
7
import "strings"
7
import "strings"
8
8
9
func runAuth(args []string) error {
9
func runAuth(args []string) error {
10
if len(args) == 0 {
10
if len(args) == 0 {
11
return fmt.Errorf("usage: okg auth login")
11
return fmt.Errorf("usage: okg auth login")
12
}
12
}
13
switch args[0] {
13
switch args[0] {
14
case "login":
14
case "login":
15
return runAuthLogin(args[1:])
15
return runAuthLogin(args[1:])
16
default:
16
default:
17
return fmt.Errorf(
17
return fmt.Errorf(
18
"unknown auth command: %s", args[0])
18
"unknown auth command: %s", args[0])
19
}
19
}
20
}
20
}
21
21
22
// runAuthLogin saves a host + API key to ~/.config/okg/config.json.
22
// runAuthLogin saves a host + API key to ~/.config/okg/config.json.
23
// The key can come from --key, from stdin (pipe), or — when stdin
23
// The key can come from --key, from stdin (pipe), or — when stdin
24
// is a TTY — from an interactive prompt. Host defaults to
24
// is a TTY — from an interactive prompt. Host defaults to
25
// production unless --host is given (and, in interactive mode, the
25
// production unless --host is given (and, in interactive mode, the
26
// prompt offers an override).
26
// prompt offers an override).
27
func runAuthLogin(args []string) error {
27
func runAuthLogin(args []string) error {
28
host := ""
28
host := ""
29
codeHost := ""
29
key := ""
30
key := ""
30
for i := 0; i < len(args); i++ {
31
for i := 0; i < len(args); i++ {
31
switch args[i] {
32
switch args[i] {
32
case "--host":
33
case "--host":
33
i++
34
i++
34
if i >= len(args) {
35
if i >= len(args) {
35
return fmt.Errorf(
36
return fmt.Errorf(
36
"--host requires a value")
37
"--host requires a value")
37
}
38
}
38
host = args[i]
39
host = args[i]
40
case "--code-host":
41
i++
42
if i >= len(args) {
43
return fmt.Errorf(
44
"--code-host requires a value")
45
}
46
codeHost = args[i]
39
case "--key":
47
case "--key":
40
i++
48
i++
41
if i >= len(args) {
49
if i >= len(args) {
42
return fmt.Errorf(
50
return fmt.Errorf(
43
"--key requires a value")
51
"--key requires a value")
44
}
52
}
45
key = args[i]
53
key = args[i]
46
default:
54
default:
47
return fmt.Errorf(
55
return fmt.Errorf(
48
"unknown flag: %s", args[i])
56
"unknown flag: %s", args[i])
49
}
57
}
50
}
58
}
51
59
52
// Resolve API key: --key wins; otherwise read from stdin
60
// Resolve API key: --key wins; otherwise read from stdin
53
// (piped) or prompt interactively.
61
// (piped) or prompt interactively.
54
switch {
62
switch {
55
case key != "":
63
case key != "":
56
// Use as-is.
64
// Use as-is.
57
case stdinIsPiped():
65
case stdinIsPiped():
58
line, err := bufio.NewReader(os.Stdin).
66
line, err := bufio.NewReader(os.Stdin).
59
ReadString('\n')
67
ReadString('\n')
60
if err != nil && err != io.EOF {
68
if err != nil && err != io.EOF {
61
return fmt.Errorf("reading stdin: %v", err)
69
return fmt.Errorf("reading stdin: %v", err)
62
}
70
}
63
key = strings.TrimSpace(line)
71
key = strings.TrimSpace(line)
64
default:
72
default:
65
// Interactive: opto p host, then key.
73
// Interactive: opto p host, then key.
74
// CodeHost stays at its default unless --code-host was
75
// given; uncommon enough to keep off the prompt.
66
reader := bufio.NewReader(os.Stdin)
76
reader := bufio.NewReader(os.Stdin)
67
if host == "" {
77
if host == "" {
68
fmt.Printf("Host (default %s): ", DefaultHost)
78
fmt.Printf("Host (default %s): ", DefaultHost)
69
line, _ := reader.ReadString('\n')
79
line, _ := reader.ReadString('\n')
70
host = strings.TrimSpace(line)
80
host = strings.TrimSpace(line)
71
}
81
}
72
fmt.Print("API key: ")
82
fmt.Print("API key: ")
73
line, _ := reader.ReadString('\n')
83
line, _ := reader.ReadString('\n')
74
key = strings.TrimSpace(line)
84
key = strings.TrimSpace(line)
75
}
85
}
76
86
77
if host == "" {
87
if host == "" {
78
host = DefaultHost
88
host = DefaultHost
79
}
89
}
90
if codeHost == "" {
91
codeHost = DefaultCodeHost
92
}
80
if key == "" {
93
if key == "" {
81
return fmt.Errorf(
94
return fmt.Errorf(
82
"API key required " +
95
"API key required " +
83
"(pass --key, pipe via stdin, or " +
96
"(pass --key, pipe via stdin, or " +
84
"enter at the prompt)")
97
"enter at the prompt)")
85
}
98
}
86
99
87
cfg := &Config{
100
cfg := &Config{
101
Host: host, CodeHost: codeHost, ApiKey: key,
102
}
88
if err := saveConfig(cfg); err != nil {
103
if err := saveConfig(cfg); err != nil {
89
return fmt.Errorf("saving config: %v", err)
104
return fmt.Errorf("saving config: %v", err)
90
}
105
}
91
fmt.Printf("Saved config to %s\n", configPath())
106
fmt.Printf("Saved config to %s\n", configPath())
92
return nil
107
return nil
93
}
108
}
94
109
95
// stdinIsPiped reports whether stdin is not a terminal — i.e.
110
// stdinIsPiped reports whether stdin is not a terminal — i.e.
96
// a pipe or a redirected file. Used to decide between reading
111
// a pipe or a redirected file. Used to decide between reading
97
// stdin directly and prompting interactively.
112
// stdin directly and prompting interactively.
98
func stdinIsPiped() bool {
113
func stdinIsPiped() bool {
99
fi, err := os.Stdin.Stat()
114
fi, err := os.Stdin.Stat()
100
if err != nil {
115
if err != nil {
101
return false
116
return false
102
}
117
}
103
return (fi.Mode() & os.ModeCharDevice) == 0
118
return (fi.Mode() & os.ModeCharDevice) == 0
104
}
119
}
1
package main
1
package main
2
2
3
import "encoding/json"
3
import "encoding/json"
4
import "flag"
4
import "flag"
5
import "fmt"
5
import "fmt"
6
import "os"
7
6
8
import "oscarkilo.com/okg/chat"
7
import "oscarkilo.com/okg/chat"
9
8
10
// ChatDefaultUrl is where //chat lives in production. Override
11
// via the CHAT_URL env var for dev/tests (typically
12
// http://localhost:9100, matching the //chat binary's default
13
// port).
14
//
15
// FUTURE: this URL is aspirational until //chat is deployed.
16
// Today, agents need to set CHAT_URL explicitly.
17
const ChatDefaultUrl = "https://oscarkilo.com"
18
19
func runChat(args []string) error {
9
func runChat(args []string) error {
20
if len(args) == 0 {
10
if len(args) == 0 {
21
return fmt.Errorf(
11
return fmt.Errorf(
22
"usage: okg chat SUBCOMMAND ... " +
12
"usage: okg chat SUBCOMMAND ... " +
23
"(try `okg --help`)")
13
"(try `okg --help`)")
24
}
14
}
25
switch args[0] {
15
switch args[0] {
26
case "send":
16
case "send":
27
return runChatSend(args[1:])
17
return runChatSend(args[1:])
28
case "fetch":
18
case "fetch":
29
return runChatFetch(args[1:])
19
return runChatFetch(args[1:])
30
default:
20
default:
31
return fmt.Errorf(
21
return fmt.Errorf(
32
"unknown chat subcommand: %s", args[0])
22
"unknown chat subcommand: %s", args[0])
33
}
23
}
34
}
24
}
35
25
36
func runChatSend(args []string) error {
26
func runChatSend(args []string) error {
37
fs := flag.NewFlagSet("chat send", flag.ContinueOnError)
27
fs := flag.NewFlagSet("chat send", flag.ContinueOnError)
38
if err := fs.Parse(args); err != nil {
28
if err := fs.Parse(args); err != nil {
39
return err
29
return err
40
}
30
}
41
positional := fs.Args()
31
positional := fs.Args()
42
if len(positional) != 2 {
32
if len(positional) != 2 {
43
return fmt.Errorf("usage: okg chat send TO TEXT")
33
return fmt.Errorf("usage: okg chat send TO TEXT")
44
}
34
}
45
to := positional[0]
35
to := positional[0]
46
text := positional[1]
36
text := positional[1]
47
37
48
c, err := newChatClient()
38
c, err := newChatClient()
49
if err != nil {
39
if err != nil {
50
return err
40
return err
51
}
41
}
52
msg, err := c.Send(chat.SendRequest{To: to, Text: text})
42
msg, err := c.Send(chat.SendRequest{To: to, Text: text})
53
if err != nil {
43
if err != nil {
54
return err
44
return err
55
}
45
}
56
fmt.Printf(
46
fmt.Printf(
57
"Sent (from=%s, to=%s, at=%s)\n",
47
"Sent (from=%s, to=%s, at=%s)\n",
58
msg.From, msg.To,
48
msg.From, msg.To,
59
msg.CreatedAt.Format("2006-01-02 15:04:05"))
49
msg.CreatedAt.Format("2006-01-02 15:04:05"))
60
return nil
50
return nil
61
}
51
}
62
52
63
func runChatFetch(args []string) error {
53
func runChatFetch(args []string) error {
64
fs := flag.NewFlagSet("chat fetch", flag.ContinueOnError)
54
fs := flag.NewFlagSet("chat fetch", flag.ContinueOnError)
65
to := fs.String("to", "",
55
to := fs.String("to", "",
66
"filter by destination group (default: all visible)")
56
"filter by destination group (default: all visible)")
67
asJSON := fs.Bool("json", false, "output raw JSON")
57
asJSON := fs.Bool("json", false, "output raw JSON")
68
if err := fs.Parse(args); err != nil {
58
if err := fs.Parse(args); err != nil {
69
return err
59
return err
70
}
60
}
71
61
72
c, err := newChatClient()
62
c, err := newChatClient()
73
if err != nil {
63
if err != nil {
74
return err
64
return err
75
}
65
}
76
msgs, err := c.Search(chat.SearchRequest{To: *to})
66
msgs, err := c.Search(chat.SearchRequest{To: *to})
77
if err != nil {
67
if err != nil {
78
return err
68
return err
79
}
69
}
80
70
81
if *asJSON {
71
if *asJSON {
82
buf, err := json.MarshalIndent(msgs, "", " ")
72
buf, err := json.MarshalIndent(msgs, "", " ")
83
if err != nil {
73
if err != nil {
84
return err
74
return err
85
}
75
}
86
fmt.Println(string(buf))
76
fmt.Println(string(buf))
87
return nil
77
return nil
88
}
78
}
89
79
90
for _, m := range msgs {
80
for _, m := range msgs {
91
fmt.Printf(
81
fmt.Printf(
92
"[%s] %s → %s: %s\n",
82
"[%s] %s → %s: %s\n",
93
m.CreatedAt.Format("2006-01-02 15:04:05"),
83
m.CreatedAt.Format("2006-01-02 15:04:05"),
94
m.From, m.To, m.Text)
84
m.From, m.To, m.Text)
95
}
85
}
96
return nil
86
return nil
97
}
87
}
98
88
99
// newChatClient builds a //chat client from saved config
89
// newChatClient builds a //chat client from saved config
100
// the CHAT_URL env var.
101
func newChatClient() (*chat.HTTPClient, error) {
90
func newChatClient() (*chat.HTTPClient, error) {
102
cfg, err := loadConfig()
91
cfg, err := loadConfig()
103
if err != nil {
92
if err != nil {
104
return nil, err
93
return nil, err
105
}
94
}
106
if cfg.ApiKey == "" {
95
if cfg.ApiKey == "" {
107
return nil, fmt.Errorf(
96
return nil, fmt.Errorf(
108
"no API key — run `okg auth login --key sk-...`")
97
"no API key — run `okg auth login --key sk-...`")
109
}
98
}
110
ur hateltl
99
ur hateltl
111
if v := os.Getenv("CHAT_URL"); v != "" {
112
url = v
113
}
114
return chat.NewHTTPClient(url, cfg.ApiKey), nil
115
}
100
}
1
package main
1
package main
2
2
3
import "fmt"
3
import "fmt"
4
import "os"
4
import "os"
5
import "os/exec"
5
import "os/exec"
6
import "regexp"
6
import "regexp"
7
import "strings"
7
import "strings"
8
8
9
import "oscarkilo.com/okg/klee"
9
import "oscarkilo.com/okg/klee"
10
10
11
var repoRegex = regexp.MustCompile(
11
var repoRegex = regexp.MustCompile(
12
`code\.oscarkilo\.com/` +
12
`code\.oscarkilo\.com/` +
13
`([a-z][-a-z0-9]*?)(?:\.git)?$`)
13
`([a-z][-a-z0-9]*?)(?:\.git)?$`)
14
14
15
// detectRepo parses the git remote URL for the
15
// detectRepo parses the git remote URL for the
16
// klee repo name.
16
// klee repo name.
17
func detectRepo() (string, error) {
17
func detectRepo() (string, error) {
18
cmd := exec.Command(
18
cmd := exec.Command(
19
"git", "remote", "get-url", "origin")
19
"git", "remote", "get-url", "origin")
20
out, err := cmd.Output()
20
out, err := cmd.Output()
21
if err != nil {
21
if err != nil {
22
return "", fmt.Errorf(
22
return "", fmt.Errorf(
23
"not a git repo or no remote 'origin': %v",
23
"not a git repo or no remote 'origin': %v",
24
err)
24
err)
25
}
25
}
26
url := strings.TrimSpace(string(out))
26
url := strings.TrimSpace(string(out))
27
m := repoRegex.FindStringSubmatch(url)
27
m := repoRegex.FindStringSubmatch(url)
28
if m == nil {
28
if m == nil {
29
return "", fmt.Errorf(
29
return "", fmt.Errorf(
30
"remote URL %q is not a klee repo", url)
30
"remote URL %q is not a klee repo", url)
31
}
31
}
32
return m[1], nil
32
return m[1], nil
33
}
33
}
34
34
35
// resolveRepo returns the repo name from --repo
35
// resolveRepo returns the repo name from --repo
36
// flag, OKG_REPO env var, or git remote detection.
36
// flag, OKG_REPO env var, or git remote detection.
37
func resolveRepo(flag_repo string) (string, error) {
37
func resolveRepo(flag_repo string) (string, error) {
38
if flag_repo != "" {
38
if flag_repo != "" {
39
return flag_repo, nil
39
return flag_repo, nil
40
}
40
}
41
if v := os.Getenv("OKG_REPO"); v != "" {
41
if v := os.Getenv("OKG_REPO"); v != "" {
42
return v, nil
42
return v, nil
43
}
43
}
44
return detectRepo()
44
return detectRepo()
45
}
45
}
46
46
47
func newKleeClient(cfg *Config) *klee.Client {
47
func newKleeClient(cfg *Config) *klee.Client {
48
return klee.NewClient(cfg.Host, cfg.ApiKey)
48
return klee.NewClient(cfg.Host, cfg.ApiKey)
49
}
49
}
1
package main
1
package main
2
2
3
import "encoding/json"
3
import "encoding/json"
4
import "fmt"
4
import "fmt"
5
import "os"
5
import "os"
6
import "path/filepath"
6
import "path/filepath"
7
7
8
// DefaultHost is the pc host here at
8
// DefaultHost is the pc host here at
9
// e ts bd e
9
// e ts bd e
10
cn auto hc
10
cn auto hc
11
11
12
// DefaultCodeHost is the //klee git server's host. Klee is
13
// served from a different name than the rest of the public
14
// surface, so it gets its own config field.
15
const DefaultCodeHost = "https://code.oscarkilo.com"
12
16
13
type Config struct {
17
type Config struct {
14
Host string `json:"host"`
18
Host string `json:"host"`
15
string `json:"_"`
19
string `json:"_"`
20
ApiKey string `json:"api_key"`
16
}
21
}
17
22
18
func configPath() string {
23
func configPath() string {
19
home, err := os.UserHomeDir()
24
home, err := os.UserHomeDir()
20
if err != nil {
25
if err != nil {
21
return ""
26
return ""
22
}
27
}
23
return filepath.Join(home, ".config", "okg", "config.json")
28
return filepath.Join(home, ".config", "okg", "config.json")
24
}
29
}
25
30
26
func loadConfig() (*Config, error) {
31
func loadConfig() (*Config, error) {
27
c := &Config{}
32
c := &Config{}
28
33
29
// Load from file.
34
// Load from file.
30
path := configPath()
35
path := configPath()
31
if path != "" {
36
if path != "" {
32
data, err := os.ReadFile(path)
37
data, err := os.ReadFile(path)
33
if err == nil {
38
if err == nil {
34
json.Unmarshal(data, c)
39
json.Unmarshal(data, c)
35
}
40
}
36
}
41
}
37
42
38
// nv override exist for tests (TestMain points
43
// nv override exist for tests (TestMain points
39
// localhost so accidental hits to prod fail fast). N
44
// localhost so accidental hits to prod fail fast). N
40
// user-facing config knob; `okg auth login` is.
45
// user-facing config knob; `okg auth login` is.
41
if v := os.Getenv("OKG_HOST"); v != "" {
46
if v := os.Getenv("OKG_HOST"); v != "" {
42
c.Host = v
47
c.Host = v
43
}
48
}
49
if v := os.Getenv("OKG_CODE_HOST"); v != "" {
50
c.CodeHost = v
51
}
52
53
// Migrate old configs whose Host field stored the klee URL.
54
// Treat that value as CodeHost and reset Host to default.
55
if c.CodeHost == "" && c.Host == DefaultCodeHost {
56
c.CodeHost = c.Host
57
c.Host = ""
58
}
44
59
45
// Default to prod. Tests set OKG_HOST in TestMain to a localhost
46
// URL so they can't accidentally reach it.
47
if c.Host == "" {
60
if c.Host == "" {
48
c.Host = DefaultHost
61
c.Host = DefaultHost
49
}
62
}
63
if c.CodeHost == "" {
64
c.CodeHost = DefaultCodeHost
65
}
50
66
51
return c, nil
67
return c, nil
52
}
68
}
53
69
54
func saveConfig(c *Config) error {
70
func saveConfig(c *Config) error {
55
path := configPath()
71
path := configPath()
56
if path == "" {
72
if path == "" {
57
return fmt.Errorf("cannot determine home directory")
73
return fmt.Errorf("cannot determine home directory")
58
}
74
}
59
dir := filepath.Dir(path)
75
dir := filepath.Dir(path)
60
if err := os.MkdirAll(dir, 0700); err != nil {
76
if err := os.MkdirAll(dir, 0700); err != nil {
61
return fmt.Errorf("mkdir %s: %v", dir, err)
77
return fmt.Errorf("mkdir %s: %v", dir, err)
62
}
78
}
63
data, err := json.MarshalIndent(c, "", " ")
79
data, err := json.MarshalIndent(c, "", " ")
64
if err != nil {
80
if err != nil {
65
return err
81
return err
66
}
82
}
67
return os.WriteFile(path, data, 0600)
83
return os.WriteFile(path, data, 0600)
68
}
84
}
1
package main
1
package main
2
2
3
import "encoding/json"
3
import "encoding/json"
4
import "flag"
4
import "flag"
5
import "fmt"
5
import "fmt"
6
import "os"
6
import "os"
7
import "text/tabwriter"
7
import "text/tabwriter"
8
8
9
import "oscarkilo.com/okg/who"
9
import "oscarkilo.com/okg/who"
10
10
11
func runGroup(args []string) error {
11
func runGroup(args []string) error {
12
if len(args) == 0 {
12
if len(args) == 0 {
13
return fmt.Errorf(
13
return fmt.Errorf(
14
"usage: okg group SUBCOMMAND ... " +
14
"usage: okg group SUBCOMMAND ... " +
15
"(try `okg --help`)")
15
"(try `okg --help`)")
16
}
16
}
17
switch args[0] {
17
switch args[0] {
18
case "list":
18
case "list":
19
return runGroupList(args[1:])
19
return runGroupList(args[1:])
20
case "create":
20
case "create":
21
return runGroupCreate(args[1:])
21
return runGroupCreate(args[1:])
22
case "add-member":
22
case "add-member":
23
return runGroupAddMember(args[1:])
23
return runGroupAddMember(args[1:])
24
case "remove-member":
24
case "remove-member":
25
return runGroupRemoveMember(args[1:])
25
return runGroupRemoveMember(args[1:])
26
case "members":
26
case "members":
27
return runGroupMembers(args[1:])
27
return runGroupMembers(args[1:])
28
case "delete":
28
case "delete":
29
return runGroupDelete(args[1:])
29
return runGroupDelete(args[1:])
30
default:
30
default:
31
return fmt.Errorf(
31
return fmt.Errorf(
32
"unknown group subcommand: %s", args[0])
32
"unknown group subcommand: %s", args[0])
33
}
33
}
34
}
34
}
35
35
36
func runGroupList(args []string) error {
36
func runGroupList(args []string) error {
37
fs := flag.NewFlagSet("group list", flag.ContinueOnError)
37
fs := flag.NewFlagSet("group list", flag.ContinueOnError)
38
asJSON := fs.Bool("json", false, "output raw JSON")
38
asJSON := fs.Bool("json", false, "output raw JSON")
39
if err := fs.Parse(args); err != nil {
39
if err := fs.Parse(args); err != nil {
40
return err
40
return err
41
}
41
}
42
42
43
c, err := newWhoClient()
43
c, err := newWhoClient()
44
if err != nil {
44
if err != nil {
45
return err
45
return err
46
}
46
}
47
47
48
groups, err := c.ListGroups()
48
groups, err := c.ListGroups()
49
if err != nil {
49
if err != nil {
50
return err
50
return err
51
}
51
}
52
52
53
if *asJSON {
53
if *asJSON {
54
buf, err := json.MarshalIndent(groups, "", " ")
54
buf, err := json.MarshalIndent(groups, "", " ")
55
if err != nil {
55
if err != nil {
56
return err
56
return err
57
}
57
}
58
fmt.Println(string(buf))
58
fmt.Println(string(buf))
59
return nil
59
return nil
60
}
60
}
61
61
62
tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
62
tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
63
fmt.Fprintln(tw, "USERNAME\tFULL NAME\tOWNER")
63
fmt.Fprintln(tw, "USERNAME\tFULL NAME\tOWNER")
64
for _, g := range groups {
64
for _, g := range groups {
65
fmt.Fprintf(tw, "%s\t%s\t%s\n",
65
fmt.Fprintf(tw, "%s\t%s\t%s\n",
66
g.Username, g.Name, g.OwnerUsername)
66
g.Username, g.Name, g.OwnerUsername)
67
}
67
}
68
return tw.Flush()
68
return tw.Flush()
69
}
69
}
70
70
71
func runGroupCreate(args []string) error {
71
func runGroupCreate(args []string) error {
72
fs := flag.NewFlagSet(
72
fs := flag.NewFlagSet(
73
"group create", flag.ContinueOnError)
73
"group create", flag.ContinueOnError)
74
fullName := fs.String("full-name", "",
74
fullName := fs.String("full-name", "",
75
"display name (default: NAME)")
75
"display name (default: NAME)")
76
owner := fs.String("owner", "",
76
owner := fs.String("owner", "",
77
"owner username (default: caller)")
77
"owner username (default: caller)")
78
if err := fs.Parse(args); err != nil {
78
if err := fs.Parse(args); err != nil {
79
return err
79
return err
80
}
80
}
81
positional := fs.Args()
81
positional := fs.Args()
82
if len(positional) != 1 {
82
if len(positional) != 1 {
83
return fmt.Errorf("usage: okg group create NAME")
83
return fmt.Errorf("usage: okg group create NAME")
84
}
84
}
85
name := positional[0]
85
name := positional[0]
86
86
87
c, err := newWhoClient()
87
c, err := newWhoClient()
88
if err != nil {
88
if err != nil {
89
return err
89
return err
90
}
90
}
91
91
92
if *owner == "" {
92
if *owner == "" {
93
me, err := c.GetProfile()
93
me, err := c.GetProfile()
94
if err != nil {
94
if err != nil {
95
return fmt.Errorf("resolve caller: %v", err)
95
return fmt.Errorf("resolve caller: %v", err)
96
}
96
}
97
*owner = me.Username
97
*owner = me.Username
98
}
98
}
99
99
100
displayName := *fullName
100
displayName := *fullName
101
if displayName == "" {
101
if displayName == "" {
102
displayName = name
102
displayName = name
103
}
103
}
104
104
105
if err := c.CreateGroup(who.CreateGroupRequest{
105
if err := c.CreateGroup(who.CreateGroupRequest{
106
Username: name,
106
Username: name,
107
Name: displayName,
107
Name: displayName,
108
OwnerUsername: *owner,
108
OwnerUsername: *owner,
109
}); err != nil {
109
}); err != nil {
110
return err
110
return err
111
}
111
}
112
fmt.Printf(
112
fmt.Printf(
113
"Created group %s (owner: %s)\n", name, *owner)
113
"Created group %s (owner: %s)\n", name, *owner)
114
return nil
114
return nil
115
}
115
}
116
116
117
func runGroupAddMember(args []string) error {
117
func runGroupAddMember(args []string) error {
118
fs := flag.NewFlagSet(
118
fs := flag.NewFlagSet(
119
"group add-member", flag.ContinueOnError)
119
"group add-member", flag.ContinueOnError)
120
if err := fs.Parse(args); err != nil {
120
if err := fs.Parse(args); err != nil {
121
return err
121
return err
122
}
122
}
123
positional := fs.Args()
123
positional := fs.Args()
124
if len(positional) < 2 {
124
if len(positional) < 2 {
125
return fmt.Errorf(
125
return fmt.Errorf(
126
"usage: okg group add-member GROUP USER [USER ...]")
126
"usage: okg group add-member GROUP USER [USER ...]")
127
}
127
}
128
group := positional[0]
128
group := positional[0]
129
members := positional[1:]
129
members := positional[1:]
130
130
131
c, err := newWhoClient()
131
c, err := newWhoClient()
132
if err != nil {
132
if err != nil {
133
return err
133
return err
134
}
134
}
135
if err := c.JoinGroups(who.JoinGroupsRequest{
135
if err := c.JoinGroups(who.JoinGroupsRequest{
136
GroupUsernames: []string{group},
136
GroupUsernames: []string{group},
137
MemberUsernames: members,
137
MemberUsernames: members,
138
}); err != nil {
138
}); err != nil {
139
return err
139
return err
140
}
140
}
141
fmt.Printf(
141
fmt.Printf(
142
"Added %d member(s) to %s\n", len(members), group)
142
"Added %d member(s) to %s\n", len(members), group)
143
return nil
143
return nil
144
}
144
}
145
145
146
func runGroupRemoveMember(args []string) error {
146
func runGroupRemoveMember(args []string) error {
147
fs := flag.NewFlagSet(
147
fs := flag.NewFlagSet(
148
"group remove-member", flag.ContinueOnError)
148
"group remove-member", flag.ContinueOnError)
149
if err := fs.Parse(args); err != nil {
149
if err := fs.Parse(args); err != nil {
150
return err
150
return err
151
}
151
}
152
positional := fs.Args()
152
positional := fs.Args()
153
if len(positional) < 2 {
153
if len(positional) < 2 {
154
return fmt.Errorf(
154
return fmt.Errorf(
155
"usage: okg group remove-member " +
155
"usage: okg group remove-member " +
156
"GROUP USER [USER ...]")
156
"GROUP USER [USER ...]")
157
}
157
}
158
groupName := positional[0]
158
groupName := positional[0]
159
members := positional[1:]
159
members := positional[1:]
160
160
161
c, err := newWhoClient()
161
c, err := newWhoClient()
162
if err != nil {
162
if err != nil {
163
return err
163
return err
164
}
164
}
165
165
166
// /groups/leave wants owids, not usernames. Resolve the
166
// /groups/leave wants owids, not usernames. Resolve the
167
// group's owid via ListGroups, then the members' owids via
167
// group's owid via ListGroups, then the members' owids via
168
// GroupMembers. Two round-trips on top of the actual leave
168
// GroupMembers. Two round-trips on top of the actual leave
169
// calls; shared across all members in this invocation.
169
// calls; shared across all members in this invocation.
170
groupOwid, err := resolveGroupOwid(c, groupName)
170
groupOwid, err := resolveGroupOwid(c, groupName)
171
if err != nil {
171
if err != nil {
172
return err
172
return err
173
}
173
}
174
ms, err := c.GroupMembers(groupOwid)
174
ms, err := c.GroupMembers(groupOwid)
175
if err != nil {
175
if err != nil {
176
return err
176
return err
177
}
177
}
178
username2owid := make(map[string]string)
178
username2owid := make(map[string]string)
179
for owid, name := range ms.Usernames {
179
for owid, name := range ms.Usernames {
180
username2owid[name] = owid
180
username2owid[name] = owid
181
}
181
}
182
182
183
for _, m := range members {
183
for _, m := range members {
184
memberOwid, ok := username2owid[m]
184
memberOwid, ok := username2owid[m]
185
if !ok {
185
if !ok {
186
return fmt.Errorf(
186
return fmt.Errorf(
187
"user %q is not a member of %s", m, groupName)
187
"user %q is not a member of %s", m, groupName)
188
}
188
}
189
if err := c.LeaveGroup(who.LeaveGroupRequest{
189
if err := c.LeaveGroup(who.LeaveGroupRequest{
190
GroupOwid: groupOwid,
190
GroupOwid: groupOwid,
191
MemberOwid: memberOwid,
191
MemberOwid: memberOwid,
192
}); err != nil {
192
}); err != nil {
193
return fmt.Errorf(
193
return fmt.Errorf(
194
"remove %s from %s: %v", m, groupName, err)
194
"remove %s from %s: %v", m, groupName, err)
195
}
195
}
196
}
196
}
197
fmt.Printf(
197
fmt.Printf(
198
"Removed %d member(s) from %s\n",
198
"Removed %d member(s) from %s\n",
199
len(members), groupName)
199
len(members), groupName)
200
return nil
200
return nil
201
}
201
}
202
202
203
func runGroupMembers(args []string) error {
203
func runGroupMembers(args []string) error {
204
fs := flag.NewFlagSet(
204
fs := flag.NewFlagSet(
205
"group members", flag.ContinueOnError)
205
"group members", flag.ContinueOnError)
206
asJSON := fs.Bool("json", false,
206
asJSON := fs.Bool("json", false,
207
"output full DAG (Up/Down/Usernames) as raw JSON")
207
"output full DAG (Up/Down/Usernames) as raw JSON")
208
if err := fs.Parse(args); err != nil {
208
if err := fs.Parse(args); err != nil {
209
return err
209
return err
210
}
210
}
211
positional := fs.Args()
211
positional := fs.Args()
212
if len(positional) != 1 {
212
if len(positional) != 1 {
213
return fmt.Errorf("usage: okg group members GROUP")
213
return fmt.Errorf("usage: okg group members GROUP")
214
}
214
}
215
groupName := positional[0]
215
groupName := positional[0]
216
216
217
c, err := newWhoClient()
217
c, err := newWhoClient()
218
if err != nil {
218
if err != nil {
219
return err
219
return err
220
}
220
}
221
groupOwid, err := resolveGroupOwid(c, groupName)
221
groupOwid, err := resolveGroupOwid(c, groupName)
222
if err != nil {
222
if err != nil {
223
return err
223
return err
224
}
224
}
225
ms, err := c.GroupMembers(groupOwid)
225
ms, err := c.GroupMembers(groupOwid)
226
if err != nil {
226
if err != nil {
227
return err
227
return err
228
}
228
}
229
229
230
if *asJSON {
230
if *asJSON {
231
buf, err := json.MarshalIndent(ms, "", " ")
231
buf, err := json.MarshalIndent(ms, "", " ")
232
if err != nil {
232
if err != nil {
233
return err
233
return err
234
}
234
}
235
fmt.Println(string(buf))
235
fmt.Println(string(buf))
236
return nil
236
return nil
237
}
237
}
238
238
239
// Direct members are the first level of Down. Print one
239
// Direct members are the first level of Down. Print one
240
// username per line; deeper nesting (group-of-groups) is
240
// username per line; deeper nesting (group-of-groups) is
241
// available via --json.
241
// available via --json.
242
if len(ms.Down) == 0 {
242
if len(ms.Down) == 0 {
243
return nil
243
return nil
244
}
244
}
245
for _, owid := range ms.Down[0] {
245
for _, owid := range ms.Down[0] {
246
fmt.Println(ms.Usernames[owid])
246
fmt.Println(ms.Usernames[owid])
247
}
247
}
248
return nil
248
return nil
249
}
249
}
250
250
251
func runGroupDelete(args []string) error {
251
func runGroupDelete(args []string) error {
252
fs := flag.NewFlagSet(
252
fs := flag.NewFlagSet(
253
"group delete", flag.ContinueOnError)
253
"group delete", flag.ContinueOnError)
254
if err := fs.Parse(args); err != nil {
254
if err := fs.Parse(args); err != nil {
255
return err
255
return err
256
}
256
}
257
positional := fs.Args()
257
positional := fs.Args()
258
if len(positional) != 1 {
258
if len(positional) != 1 {
259
return fmt.Errorf("usage: okg group delete NAME")
259
return fmt.Errorf("usage: okg group delete NAME")
260
}
260
}
261
groupName := positional[0]
261
groupName := positional[0]
262
262
263
c, err := newWhoClient()
263
c, err := newWhoClient()
264
if err != nil {
264
if err != nil {
265
return err
265
return err
266
}
266
}
267
// /groups/delete takes an owid; resolve via /groups/list.
267
// /groups/delete takes an owid; resolve via /groups/list.
268
groupOwid, err := resolveGroupOwid(c, groupName)
268
groupOwid, err := resolveGroupOwid(c, groupName)
269
if err != nil {
269
if err != nil {
270
return err
270
return err
271
}
271
}
272
if err := c.DeleteGroup(who.DeleteGroupRequest{
272
if err := c.DeleteGroup(who.DeleteGroupRequest{
273
Owid: groupOwid,
273
Owid: groupOwid,
274
}); err != nil {
274
}); err != nil {
275
return err
275
return err
276
}
276
}
277
fmt.Printf("Deleted group %s\n", groupName)
277
fmt.Printf("Deleted group %s\n", groupName)
278
return nil
278
return nil
279
}
279
}
280
280
281
// resolveGroupOwid translates a group username to its owid via
281
// resolveGroupOwid translates a group username to its owid via
282
// /groups/list. Returns an error if the group isn't visible to
282
// /groups/list. Returns an error if the group isn't visible to
283
// the caller.
283
// the caller.
284
func resolveGroupOwid(
284
func resolveGroupOwid(
285
c *who.HTTPClient, name string,
285
c *who.HTTPClient, name string,
286
) (string, error) {
286
) (string, error) {
287
groups, err := c.ListGroups()
287
groups, err := c.ListGroups()
288
if err != nil {
288
if err != nil {
289
return "", err
289
return "", err
290
}
290
}
291
for _, g := range groups {
291
for _, g := range groups {
292
if g.Username == name {
292
if g.Username == name {
293
return g.Owid, nil
293
return g.Owid, nil
294
}
294
}
295
}
295
}
296
return "", fmt.Errorf(
296
return "", fmt.Errorf(
297
"group %q not found (or not visible to caller)", name)
297
"group %q not found (or not visible to caller)", name)
298
}
298
}
299
299
300
// newWhoClient builds a //who client from saved config. Shared
300
// newWhoClient builds a //who client from saved config. Shared
301
// by every `okg group` subcommand.
301
// by every `okg group` subcommand.
302
func newWhoClient() (*who.HTTPClient, error) {
302
func newWhoClient() (*who.HTTPClient, error) {
303
cfg, err := loadConfig()
303
cfg, err := loadConfig()
304
if err != nil {
304
if err != nil {
305
return nil, err
305
return nil, err
306
}
306
}
307
if cfg.ApiKey == "" {
307
if cfg.ApiKey == "" {
308
return nil, fmt.Errorf(
308
return nil, fmt.Errorf(
309
"no API key — run `okg auth login --key sk-...`")
309
"no API key — run `okg auth login --key sk-...`")
310
}
310
}
311
return who.NewHTTPClient(cfg.Host, cfg.ApiKey), nil
311
return who.NewHTTPClient(cfg.Host, cfg.ApiKey), nil
312
}
312
}
313
1
package main
1
package main
2
2
3
import "encoding/json"
3
import "encoding/json"
4
import "net/http"
4
import "net/http"
5
import "net/http/httptest"
5
import "net/http/httptest"
6
import "os"
6
import "os"
7
import "strings"
7
import "strings"
8
import "testing"
8
import "testing"
9
import "time"
9
import "time"
10
10
11
import "oscarkilo.com/okg/klee"
11
import "oscarkilo.com/okg/klee"
12
12
13
// TestMain pnts t st i c i
13
// TestMain pnts t st i c i
14
// dt ii et an
14
// dt ii et an
15
// e ttt ee ee
15
// e ttt ee ee
16
// O_HOST themselves.
16
// O_HOST themselves.
17
func TestMain(m *testing.M) {
17
func TestMain(m *testing.M) {
18
os.Setenv("OKG_HOST", "http://localhost:42069")
18
os.Setenv("OKG_HOST", "http://localhost:42069")
19
os.Setenv("OKG_CODE_HOST", "http://localhost:42069")
19
os.Exit(m.Run())
20
os.Exit(m.Run())
20
}
21
}
21
22
22
func TestRepoRegex(t *testing.T) {
23
func TestRepoRegex(t *testing.T) {
23
check := func(url, want string) {
24
check := func(url, want string) {
24
t.Helper()
25
t.Helper()
25
m := repoRegex.FindStringSubmatch(url)
26
m := repoRegex.FindStringSubmatch(url)
26
if want == "" {
27
if want == "" {
27
if m != nil {
28
if m != nil {
28
t.Errorf(
29
t.Errorf(
29
"%q: want no match, got %q", url, m[1])
30
"%q: want no match, got %q", url, m[1])
30
}
31
}
31
return
32
return
32
}
33
}
33
if m == nil {
34
if m == nil {
34
t.Errorf(
35
t.Errorf(
35
"%q: want %q, got no match", url, want)
36
"%q: want %q, got no match", url, want)
36
return
37
return
37
}
38
}
38
if m[1] != want {
39
if m[1] != want {
39
t.Errorf(
40
t.Errorf(
40
"%q: want %q, got %q", url, want, m[1])
41
"%q: want %q, got %q", url, want, m[1])
41
}
42
}
42
}
43
}
43
check(
44
check(
44
"https://code.oscarkilo.com/widget.git",
45
"https://code.oscarkilo.com/widget.git",
45
"widget")
46
"widget")
46
check(
47
check(
47
"https://code.oscarkilo.com/klee.git",
48
"https://code.oscarkilo.com/klee.git",
48
"klee")
49
"klee")
49
check(
50
check(
50
"https://code.oscarkilo.com/my-repo.git",
51
"https://code.oscarkilo.com/my-repo.git",
51
"my-repo")
52
"my-repo")
52
check(
53
check(
53
"https://code.oscarkilo.com/a123.git",
54
"https://code.oscarkilo.com/a123.git",
54
"a123")
55
"a123")
55
check(
56
check(
56
"https://github.com/foo/bar.git",
57
"https://github.com/foo/bar.git",
57
"")
58
"")
58
check("not-a-url", "")
59
check("not-a-url", "")
59
}
60
}
60
61
61
func TestResolveRepo(t *testing.T) {
62
func TestResolveRepo(t *testing.T) {
62
// Flag takes priority.
63
// Flag takes priority.
63
repo, err := resolveRepo("from-flag")
64
repo, err := resolveRepo("from-flag")
64
if err != nil {
65
if err != nil {
65
t.Fatal(err)
66
t.Fatal(err)
66
}
67
}
67
if repo != "from-flag" {
68
if repo != "from-flag" {
68
t.Errorf("want from-flag, got %q", repo)
69
t.Errorf("want from-flag, got %q", repo)
69
}
70
}
70
71
71
// Env var takes priority over detection.
72
// Env var takes priority over detection.
72
os.Setenv("OKG_REPO", "from-env")
73
os.Setenv("OKG_REPO", "from-env")
73
defer os.Unsetenv("OKG_REPO")
74
defer os.Unsetenv("OKG_REPO")
74
repo, err = resolveRepo("")
75
repo, err = resolveRepo("")
75
if err != nil {
76
if err != nil {
76
t.Fatal(err)
77
t.Fatal(err)
77
}
78
}
78
if repo != "from-env" {
79
if repo != "from-env" {
79
t.Errorf("want from-env, got %q", repo)
80
t.Errorf("want from-env, got %q", repo)
80
}
81
}
81
}
82
}
82
83
83
func TestParsePRFlags(t *testing.T) {
84
func TestParsePRFlags(t *testing.T) {
84
f, rest, err := parsePRFlags([]string{
85
f, rest, err := parsePRFlags([]string{
85
"--repo", "widget", "--json", "42",
86
"--repo", "widget", "--json", "42",
86
})
87
})
87
if err != nil {
88
if err != nil {
88
t.Fatal(err)
89
t.Fatal(err)
89
}
90
}
90
if f.repo != "widget" {
91
if f.repo != "widget" {
91
t.Errorf(
92
t.Errorf(
92
"repo: want widget, got %q", f.repo)
93
"repo: want widget, got %q", f.repo)
93
}
94
}
94
if !f.asJSON {
95
if !f.asJSON {
95
t.Error("asJSON: want true")
96
t.Error("asJSON: want true")
96
}
97
}
97
if len(rest) != 1 || rest[0] != "42" {
98
if len(rest) != 1 || rest[0] != "42" {
98
t.Errorf("rest: want [42], got %v", rest)
99
t.Errorf("rest: want [42], got %v", rest)
99
}
100
}
100
}
101
}
101
102
102
func TestParsePRFlagsEmpty(t *testing.T) {
103
func TestParsePRFlagsEmpty(t *testing.T) {
103
f, rest, err := parsePRFlags(nil)
104
f, rest, err := parsePRFlags(nil)
104
if err != nil {
105
if err != nil {
105
t.Fatal(err)
106
t.Fatal(err)
106
}
107
}
107
if f.repo != "" {
108
if f.repo != "" {
108
t.Errorf(
109
t.Errorf(
109
"repo: want empty, got %q", f.repo)
110
"repo: want empty, got %q", f.repo)
110
}
111
}
111
if f.asJSON {
112
if f.asJSON {
112
t.Error("asJSON: want false")
113
t.Error("asJSON: want false")
113
}
114
}
114
if len(rest) != 0 {
115
if len(rest) != 0 {
115
t.Errorf("rest: want empty, got %v", rest)
116
t.Errorf("rest: want empty, got %v", rest)
116
}
117
}
117
}
118
}
118
119
119
func TestParsePRFlagsMissingValue(t *testing.T) {
120
func TestParsePRFlagsMissingValue(t *testing.T) {
120
_, _, err := parsePRFlags([]string{"--repo"})
121
_, _, err := parsePRFlags([]string{"--repo"})
121
if err == nil {
122
if err == nil {
122
t.Error("want error for --repo without value")
123
t.Error("want error for --repo without value")
123
}
124
}
124
}
125
}
125
126
126
func TestAge(t *testing.T) {
127
func TestAge(t *testing.T) {
127
check := func(d time.Duration, want string) {
128
check := func(d time.Duration, want string) {
128
t.Helper()
129
t.Helper()
129
got := age(time.Now().Add(-d))
130
got := age(time.Now().Add(-d))
130
if got != want {
131
if got != want {
131
t.Errorf(
132
t.Errorf(
132
"age(-%v): want %q, got %q",
133
"age(-%v): want %q, got %q",
133
d, want, got)
134
d, want, got)
134
}
135
}
135
}
136
}
136
check(30*time.Second, "just now")
137
check(30*time.Second, "just now")
137
check(5*time.Minute, "5m")
138
check(5*time.Minute, "5m")
138
check(3*time.Hour, "3h")
139
check(3*time.Hour, "3h")
139
check(48*time.Hour, "2d")
140
check(48*time.Hour, "2d")
140
}
141
}
141
142
142
func TestConfigOKGHostOverride(t *testing.T) {
143
func TestConfigOKGHostOverride(t *testing.T) {
143
t.Setenv("OKG_HOST", "http://test:1234")
144
t.Setenv("OKG_HOST", "http://test:1234")
144
cfg, err := loadConfig()
145
cfg, err := loadConfig()
145
if err != nil {
146
if err != nil {
146
t.Fatal(err)
147
t.Fatal(err)
147
}
148
}
148
if cfg.Host != "http://test:1234" {
149
if cfg.Host != "http://test:1234" {
149
t.Errorf(
150
t.Errorf(
150
"Host: want http://test:1234, got %q",
151
"Host: want http://test:1234, got %q",
151
cfg.Host)
152
cfg.Host)
152
}
153
}
153
}
154
}
154
155
155
func TestConfigDefaultHost(t *testing.T) {
156
func TestConfigDefaultHost(t *testing.T) {
156
// Bypass the TestMain guardrail and isolate from any real
157
// Bypass the TestMain guardrail and isolate from any real
157
// ~/.config/okg/config.json so we can verify the prod default.
158
// ~/.config/okg/config.json so we can verify the prod default.
158
t.Setenv("HOME", t.TempDir())
159
t.Setenv("HOME", t.TempDir())
159
t.Setenv("OKG_HOST", "")
160
t.Setenv("OKG_HOST", "")
161
t.Setenv("OKG_CODE_HOST", "")
160
cfg, err := loadConfig()
162
cfg, err := loadConfig()
161
if err != nil {
163
if err != nil {
162
t.Fatal(err)
164
t.Fatal(err)
163
}
165
}
164
if cfg.Host != "https://oscarkilo.com" {
166
if cfg.Host != "https://oscarkilo.com" {
165
t.Errorf(
167
t.Errorf(
166
"Host: want https://oscarkilo.com, got %q",
168
"Host: want https://oscarkilo.com, got %q",
167
cfg.Host)
169
cfg.Host)
168
}
170
}
171
if cfg.CodeHost != "https://code.oscarkilo.com" {
172
t.Errorf(
173
"CodeHost: want https://code.oscarkilo.com, got %q",
174
cfg.CodeHost)
175
}
169
}
176
}
170
177
171
// writeTestKey creates an isolated ~/.config/okg/config.json
178
// writeTestKey creates an isolated ~/.config/okg/config.json
172
// in a temp dir with the given API key. Used by tests that need
179
// in a temp dir with the given API key. Used by tests that need
173
// loadConfig to return a key without touching the user's real
180
// loadConfig to return a key without touching the user's real
174
// config.
181
// config.
175
func writeTestKey(t *testing.T, key string) {
182
func writeTestKey(t *testing.T, key string) {
176
t.Helper()
183
t.Helper()
177
t.Setenv("HOME", t.TempDir())
184
t.Setenv("HOME", t.TempDir())
178
if err := saveConfig(&Config{ApiKey: key}); err != nil {
185
if err := saveConfig(&Config{ApiKey: key}); err != nil {
179
t.Fatal(err)
186
t.Fatal(err)
180
}
187
}
181
}
188
}
182
189
183
func TestAuthLoginKeyFlag(t *testing.T) {
190
func TestAuthLoginKeyFlag(t *testing.T) {
184
t.Setenv("HOME", t.TempDir())
191
t.Setenv("HOME", t.TempDir())
185
if err := runAuthLogin(
192
if err := runAuthLogin(
186
[]string{"--key", "sk-flag"},
193
[]string{"--key", "sk-flag"},
187
); err != nil {
194
); err != nil {
188
t.Fatal(err)
195
t.Fatal(err)
189
}
196
}
190
cfg, err := loadConfig()
197
cfg, err := loadConfig()
191
if err != nil {
198
if err != nil {
192
t.Fatal(err)
199
t.Fatal(err)
193
}
200
}
194
if cfg.ApiKey != "sk-flag" {
201
if cfg.ApiKey != "sk-flag" {
195
t.Errorf(
202
t.Errorf(
196
"ApiKey: want sk-flag, got %q", cfg.ApiKey)
203
"ApiKey: want sk-flag, got %q", cfg.ApiKey)
197
}
204
}
198
}
205
}
199
206
200
func TestAuthLoginStdin(t *testing.T) {
207
func TestAuthLoginStdin(t *testing.T) {
201
t.Setenv("HOME", t.TempDir())
208
t.Setenv("HOME", t.TempDir())
202
209
203
r, w, err := os.Pipe()
210
r, w, err := os.Pipe()
204
if err != nil {
211
if err != nil {
205
t.Fatal(err)
212
t.Fatal(err)
206
}
213
}
207
orig := os.Stdin
214
orig := os.Stdin
208
os.Stdin = r
215
os.Stdin = r
209
defer func() { os.Stdin = orig }()
216
defer func() { os.Stdin = orig }()
210
go func() {
217
go func() {
211
w.Write([]byte("sk-piped\n"))
218
w.Write([]byte("sk-piped\n"))
212
w.Close()
219
w.Close()
213
}()
220
}()
214
221
215
if err := runAuthLogin(nil); err != nil {
222
if err := runAuthLogin(nil); err != nil {
216
t.Fatal(err)
223
t.Fatal(err)
217
}
224
}
218
cfg, err := loadConfig()
225
cfg, err := loadConfig()
219
if err != nil {
226
if err != nil {
220
t.Fatal(err)
227
t.Fatal(err)
221
}
228
}
222
if cfg.ApiKey != "sk-piped" {
229
if cfg.ApiKey != "sk-piped" {
223
t.Errorf(
230
t.Errorf(
224
"ApiKey: want sk-piped, got %q", cfg.ApiKey)
231
"ApiKey: want sk-piped, got %q", cfg.ApiKey)
225
}
232
}
226
}
233
}
227
234
228
func TestRunRepoCreateArgs(t *testing.T) {
235
func TestRunRepoCreateArgs(t *testing.T) {
229
mock := newMockKleeRepo(t, 204, "")
236
mock := newMockKleeRepo(t, 204, "")
230
defer mock.Close()
237
defer mock.Close()
231
238
232
writeTestKey(t, "test-key")
239
writeTestKey(t, "test-key")
233
t.Setenv("OKG_HOST", mock.URL)
240
t.Setenv("OKG_HOST", mock.URL)
234
241
235
err := runRepoCreate([]string{
242
err := runRepoCreate([]string{
236
"my-repo", "--reader", "igor.agents",
243
"my-repo", "--reader", "igor.agents",
237
})
244
})
238
if err != nil {
245
if err != nil {
239
t.Fatal(err)
246
t.Fatal(err)
240
}
247
}
241
if mock.req.RepoName != "my-repo" {
248
if mock.req.RepoName != "my-repo" {
242
t.Errorf(
249
t.Errorf(
243
"repo_name: want my-repo, got %q",
250
"repo_name: want my-repo, got %q",
244
mock.req.RepoName)
251
mock.req.RepoName)
245
}
252
}
246
if mock.req.ReaderUsername != "igor.agents" {
253
if mock.req.ReaderUsername != "igor.agents" {
247
t.Errorf(
254
t.Errorf(
248
"reader: want igor.agents, got %q",
255
"reader: want igor.agents, got %q",
249
mock.req.ReaderUsername)
256
mock.req.ReaderUsername)
250
}
257
}
251
}
258
}
252
259
253
func TestRunRepoCreateNoReader(t *testing.T) {
260
func TestRunRepoCreateNoReader(t *testing.T) {
254
mock := newMockKleeRepo(t, 204, "")
261
mock := newMockKleeRepo(t, 204, "")
255
defer mock.Close()
262
defer mock.Close()
256
263
257
writeTestKey(t, "test-key")
264
writeTestKey(t, "test-key")
258
t.Setenv("OKG_HOST", mock.URL)
265
t.Setenv("OKG_HOST", mock.URL)
259
266
260
err := runRepoCreate([]string{"my-repo"})
267
err := runRepoCreate([]string{"my-repo"})
261
if err != nil {
268
if err != nil {
262
t.Fatal(err)
269
t.Fatal(err)
263
}
270
}
264
if mock.req.ReaderUsername != "" {
271
if mock.req.ReaderUsername != "" {
265
t.Errorf(
272
t.Errorf(
266
"reader: want empty, got %q",
273
"reader: want empty, got %q",
267
mock.req.ReaderUsername)
274
mock.req.ReaderUsername)
268
}
275
}
269
}
276
}
270
277
271
func TestRunRepoCreateMissingName(t *testing.T) {
278
func TestRunRepoCreateMissingName(t *testing.T) {
272
err := runRepoCreate(nil)
279
err := runRepoCreate(nil)
273
if err == nil {
280
if err == nil {
274
t.Fatal("want error for missing name")
281
t.Fatal("want error for missing name")
275
}
282
}
276
}
283
}
277
284
278
func TestRunRepoCreateReaderMissingValue(
285
func TestRunRepoCreateReaderMissingValue(
279
t *testing.T,
286
t *testing.T,
280
) {
287
) {
281
err := runRepoCreate([]string{
288
err := runRepoCreate([]string{
282
"my-repo", "--reader",
289
"my-repo", "--reader",
283
})
290
})
284
if err == nil {
291
if err == nil {
285
t.Fatal(
292
t.Fatal(
286
"want error for --reader without value")
293
"want error for --reader without value")
287
}
294
}
288
}
295
}
289
296
290
func TestRunRepoCreateUnknownFlag(t *testing.T) {
297
func TestRunRepoCreateUnknownFlag(t *testing.T) {
291
err := runRepoCreate([]string{
298
err := runRepoCreate([]string{
292
"my-repo", "--bogus",
299
"my-repo", "--bogus",
293
})
300
})
294
if err == nil {
301
if err == nil {
295
t.Fatal("want error for unknown flag")
302
t.Fatal("want error for unknown flag")
296
}
303
}
297
}
304
}
298
305
299
func TestRunRepoCreateDuplicateName(
306
func TestRunRepoCreateDuplicateName(
300
t *testing.T,
307
t *testing.T,
301
) {
308
) {
302
err := runRepoCreate([]string{
309
err := runRepoCreate([]string{
303
"my-repo", "extra",
310
"my-repo", "extra",
304
})
311
})
305
if err == nil {
312
if err == nil {
306
t.Fatal(
313
t.Fatal(
307
"want error for extra positional arg")
314
"want error for extra positional arg")
308
}
315
}
309
}
316
}
310
317
311
func TestRunRepoCreateServerError(t *testing.T) {
318
func TestRunRepoCreateServerError(t *testing.T) {
312
mock := newMockKleeRepo(t, 403,
319
mock := newMockKleeRepo(t, 403,
313
"you are not an owner of "+
320
"you are not an owner of "+
314
"klee://code.oscarkilo.com/.new-repo")
321
"klee://code.oscarkilo.com/.new-repo")
315
defer mock.Close()
322
defer mock.Close()
316
323
317
writeTestKey(t, "test-key")
324
writeTestKey(t, "test-key")
318
t.Setenv("OKG_HOST", mock.URL)
325
t.Setenv("OKG_HOST", mock.URL)
319
326
320
err := runRepoCreate([]string{"my-repo"})
327
err := runRepoCreate([]string{"my-repo"})
321
if err == nil {
328
if err == nil {
322
t.Fatal("want error on 403")
329
t.Fatal("want error on 403")
323
}
330
}
324
if !strings.Contains(err.Error(), "403") {
331
if !strings.Contains(err.Error(), "403") {
325
t.Errorf("want 403 in error, got %q", err)
332
t.Errorf("want 403 in error, got %q", err)
326
}
333
}
327
}
334
}
328
335
329
// mockKleeRepo captures the /.add-repo request.
336
// mockKleeRepo captures the /.add-repo request.
330
type mockKleeRepo struct {
337
type mockKleeRepo struct {
331
*httptest.Server
338
*httptest.Server
332
req klee.CreateRepoRequest
339
req klee.CreateRepoRequest
333
}
340
}
334
341
335
func newMockKleeRepo(
342
func newMockKleeRepo(
336
t *testing.T, status int, body string,
343
t *testing.T, status int, body string,
337
) *mockKleeRepo {
344
) *mockKleeRepo {
338
t.Helper()
345
t.Helper()
339
mk := &mockKleeRepo{}
346
mk := &mockKleeRepo{}
340
mk.Server = httptest.NewServer(
347
mk.Server = httptest.NewServer(
341
http.HandlerFunc(func(
348
http.HandlerFunc(func(
342
w http.ResponseWriter, r *http.Request,
349
w http.ResponseWriter, r *http.Request,
343
) {
350
) {
344
json.NewDecoder(r.Body).Decode(&mk.req)
351
json.NewDecoder(r.Body).Decode(&mk.req)
345
w.WriteHeader(status)
352
w.WriteHeader(status)
346
if body != "" {
353
if body != "" {
347
w.Write([]byte(body))
354
w.Write([]byte(body))
348
}
355
}
349
}))
356
}))
350
return mk
357
return mk
351
}
358
}
1
package main
1
package main
2
2
3
import "fmt"
3
import "fmt"
4
import "os"
4
import "os"
5
import "strings"
5
import "strings"
6
import "text/tabwriter"
6
import "text/tabwriter"
7
7
8
import "oscarkilo.com/okg/klee"
8
import "oscarkilo.com/okg/klee"
9
9
10
func runRepo(args []string) error {
10
func runRepo(args []string) error {
11
if len(args) == 0 {
11
if len(args) == 0 {
12
return fmt.Errorf(
12
return fmt.Errorf(
13
"usage: okg repo <list|create>")
13
"usage: okg repo <list|create>")
14
}
14
}
15
switch args[0] {
15
switch args[0] {
16
case "list":
16
case "list":
17
return runRepoList(args[1:])
17
return runRepoList(args[1:])
18
case "create":
18
case "create":
19
return runRepoCreate(args[1:])
19
return runRepoCreate(args[1:])
20
default:
20
default:
21
return fmt.Errorf(
21
return fmt.Errorf(
22
"unknown repo command: %s", args[0])
22
"unknown repo command: %s", args[0])
23
}
23
}
24
}
24
}
25
25
26
func runRepoCreate(args []string) error {
26
func runRepoCreate(args []string) error {
27
reader := ""
27
reader := ""
28
name := ""
28
name := ""
29
for i := 0; i < len(args); i++ {
29
for i := 0; i < len(args); i++ {
30
switch args[i] {
30
switch args[i] {
31
case "--reader":
31
case "--reader":
32
i++
32
i++
33
if i >= len(args) {
33
if i >= len(args) {
34
return fmt.Errorf(
34
return fmt.Errorf(
35
"--reader requires a value")
35
"--reader requires a value")
36
}
36
}
37
reader = args[i]
37
reader = args[i]
38
default:
38
default:
39
if strings.HasPrefix(args[i], "-") {
39
if strings.HasPrefix(args[i], "-") {
40
return fmt.Errorf(
40
return fmt.Errorf(
41
"unknown flag: %s", args[i])
41
"unknown flag: %s", args[i])
42
}
42
}
43
if name != "" {
43
if name != "" {
44
return fmt.Errorf(
44
return fmt.Errorf(
45
"unexpected argument: %s", args[i])
45
"unexpected argument: %s", args[i])
46
}
46
}
47
name = args[i]
47
name = args[i]
48
}
48
}
49
}
49
}
50
if name == "" {
50
if name == "" {
51
return fmt.Errorf(
51
return fmt.Errorf(
52
"usage: okg repo create NAME " +
52
"usage: okg repo create NAME " +
53
"[--reader USER]")
53
"[--reader USER]")
54
}
54
}
55
55
56
cfg, err := loadConfig()
56
cfg, err := loadConfig()
57
if err != nil {
57
if err != nil {
58
return err
58
return err
59
}
59
}
60
cl := newKleeClient(cfg)
60
cl := newKleeClient(cfg)
61
61
62
err = cl.CreateRepo(klee.CreateRepoRequest{
62
err = cl.CreateRepo(klee.CreateRepoRequest{
63
RepoName: name,
63
RepoName: name,
64
ReaderUsername: reader,
64
ReaderUsername: reader,
65
})
65
})
66
if err != nil {
66
if err != nil {
67
return err
67
return err
68
}
68
}
69
fmt.Printf("Created repo %s\n", name)
69
fmt.Printf("Created repo %s\n", name)
70
if reader != "" {
70
if reader != "" {
71
fmt.Printf(" reader: %s\n", reader)
71
fmt.Printf(" reader: %s\n", reader)
72
}
72
}
73
fmt.Printf(" url: %s/%s\n", cfg.Host, name)
73
fmt.Printf(" url: %s/%s\n", cfg.Host, name)
74
return nil
74
return nil
75
}
75
}
76
76
77
func runRepoList(args []string) error {
77
func runRepoList(args []string) error {
78
as_json := false
78
as_json := false
79
for _, a := range args {
79
for _, a := range args {
80
if a == "--json" {
80
if a == "--json" {
81
as_json = true
81
as_json = true
82
}
82
}
83
}
83
}
84
84
85
cfg, err := loadConfig()
85
cfg, err := loadConfig()
86
if err != nil {
86
if err != nil {
87
return err
87
return err
88
}
88
}
89
cl := newKleeClient(cfg)
89
cl := newKleeClient(cfg)
90
90
91
res, err := cl.ListRepos()
91
res, err := cl.ListRepos()
92
if err != nil {
92
if err != nil {
93
return err
93
return err
94
}
94
}
95
95
96
if as_json {
96
if as_json {
97
return outputJSON(res)
97
return outputJSON(res)
98
}
98
}
99
99
100
tw := tabwriter.NewWriter(
100
tw := tabwriter.NewWriter(
101
os.Stdout, 0, 4, 2, ' ', 0)
101
os.Stdout, 0, 4, 2, ' ', 0)
102
fmt.Fprintln(tw, "NAME\tOWNER\tREADER")
102
fmt.Fprintln(tw, "NAME\tOWNER\tREADER")
103
for _, r := range res.Repos {
103
for _, r := range res.Repos {
104
owner := ""
104
owner := ""
105
reader := ""
105
reader := ""
106
if r.Authz != nil {
106
if r.Authz != nil {
107
owner = r.Authz.OwnerUsername
107
owner = r.Authz.OwnerUsername
108
reader = r.Authz.ReaderUsername
108
reader = r.Authz.ReaderUsername
109
}
109
}
110
fmt.Fprintf(tw, "%s\t%s\t%s\n",
110
fmt.Fprintf(tw, "%s\t%s\t%s\n",
111
r.Name, owner, reader)
111
r.Name, owner, reader)
112
}
112
}
113
return tw.Flush()
113
return tw.Flush()
114
}
114
}