initial half-vibe coded commit
This commit is contained in:
commit
50fe6a6af4
6 changed files with 402 additions and 0 deletions
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Ignore the Go binary executables
|
||||
/bin/
|
||||
/obj/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Go Modules and Dependency files
|
||||
/vendor/
|
||||
.golangci.yml
|
||||
.golangci-lint-cache/
|
||||
|
||||
# IDE/Editor settings
|
||||
.vscode/
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test binary and coverage files
|
||||
*.test
|
||||
*.coverprofile
|
||||
|
||||
# Debugging files
|
||||
*.log
|
||||
*.out
|
||||
|
||||
# Go test binaries and profiles
|
||||
profile/
|
||||
|
||||
# User-specific config files
|
||||
.env
|
||||
|
||||
# Go vendor directory (if you use a vendoring strategy)
|
||||
# /vendor
|
||||
|
||||
# Go build cache
|
||||
/go-build/
|
||||
|
||||
# Other ignored files (e.g., CI/CD)
|
||||
*.pid
|
||||
*.swp
|
||||
|
||||
|
||||
7
LICENSE
Normal file
7
LICENSE
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Copyright 2025 Hendrik Flick
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
92
README.md
Normal file
92
README.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# klog
|
||||
|
||||
`klog` is a small command-line tool to log coffee brewing events to the Kaffeelogger-API via HTTP POST requests.
|
||||
I have half-vibe-coded it with the help of ChatGPT.
|
||||
It is built with Go and uses a simple configuration file to store your API settings and token.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Commands**
|
||||
- `klog config token <token>` – store the access token
|
||||
- `klog config url <url>` – set API endpoint
|
||||
- `klog config insecure <true|false>` – ignore self-signed SSL certs
|
||||
- `klog config show` – show current configuration
|
||||
- `klog c` (aliases: `klog k`, `klog coffee`, `klog kaffee`) – log a coffee event
|
||||
- `klog brew <waterIn> [--notify=<true|false>] [--plan=<true|false>] [--planTime=<minutes>]` – log a brew with options
|
||||
|
||||
- **Formatted output**
|
||||
Responses from the server (usually JSON) are displayed as a table for easier reading.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Build from source
|
||||
Make sure you have [Go installed](https://go.dev/dl/).
|
||||
|
||||
```bash
|
||||
git clone https://dev.hflabs.de/henner/kaffeecli.git
|
||||
cd klog
|
||||
go build -o bin/klog main.go
|
||||
```
|
||||
|
||||
### Prebuilt binaries
|
||||
On tagged releases, prebuilt binaries for Linux and Windows are available under the [Releases](../../../releases) section.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Configure the tool
|
||||
|
||||
```bash
|
||||
# Set API token
|
||||
klog config token my-secret-token
|
||||
|
||||
# Set Kaffeelogger URL
|
||||
klog config url https://example.com/
|
||||
|
||||
# Show current configuration
|
||||
klog config show
|
||||
```
|
||||
|
||||
### Log events
|
||||
|
||||
```bash
|
||||
# Quick coffee log
|
||||
klog c
|
||||
|
||||
# Brew with parameters
|
||||
klog brew 1250 # plans a brew with 1250mL and notifies in 8 min.
|
||||
klog brew 250 --notify=true --plan=true --planTime=5 # plans a brew with 250mL and notifies in 5 min.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
Run locally:
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Compile for Linux:
|
||||
|
||||
```bash
|
||||
GOOS=linux GOARCH=amd64 go build -o bin/klog main.go
|
||||
```
|
||||
|
||||
Compile for Windows:
|
||||
|
||||
```bash
|
||||
GOOS=windows GOARCH=amd64 go build -o bin/klog.exe main.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License. See [LICENSE](LICENSE) for details.
|
||||
9
go.mod
Normal file
9
go.mod
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
module dev.hflabs.de/henner/kaffeecli
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
)
|
||||
10
go.sum
Normal file
10
go.sum
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
238
main.go
Normal file
238
main.go
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
APIUrl string `json:"api_url"`
|
||||
InsecureSkipVerify bool `json:"insecure_skip_verify"`
|
||||
}
|
||||
|
||||
var configPath = filepath.Join(os.Getenv("HOME"), ".klog", "config.json")
|
||||
|
||||
func saveConfig(cfg Config) error {
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.MkdirAll(filepath.Dir(configPath), 0700)
|
||||
return ioutil.WriteFile(configPath, data, 0600)
|
||||
}
|
||||
|
||||
func loadConfig() (Config, error) {
|
||||
var cfg Config
|
||||
data, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
err = json.Unmarshal(data, &cfg)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func buildAPIEndpoint(baseURL, method string) string {
|
||||
// ensure /api.php?method=... is appended correctly
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
return fmt.Sprintf("%sapi.php?method=%s", baseURL, method)
|
||||
}
|
||||
return fmt.Sprintf("%s/api.php?method=%s", baseURL, method)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func postFormRequest(method string, formValues url.Values) error {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
endpoint := buildAPIEndpoint(cfg.APIUrl, method)
|
||||
|
||||
req, err := http.NewRequest("POST", endpoint, strings.NewReader(formValues.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.AccessToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify},
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
// Try to parse as JSON array first
|
||||
var arr []map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &arr); err == nil {
|
||||
if len(arr) == 0 {
|
||||
fmt.Println("(no data)")
|
||||
return nil
|
||||
}
|
||||
printTable(arr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to parse as single object
|
||||
var obj map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &obj); err == nil {
|
||||
printTable([]map[string]interface{}{obj})
|
||||
return nil
|
||||
}
|
||||
|
||||
// fallback: print raw
|
||||
fmt.Println(string(respBody))
|
||||
return nil
|
||||
}
|
||||
|
||||
func printTable(data []map[string]interface{}) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
|
||||
// print headers
|
||||
for k := range data[0] {
|
||||
fmt.Fprintf(w, "%s\t", k)
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
|
||||
// print separator
|
||||
for range data[0] {
|
||||
fmt.Fprintf(w, "--------\t")
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
|
||||
// print rows
|
||||
for _, row := range data {
|
||||
for _, v := range row {
|
||||
fmt.Fprintf(w, "%v\t", v)
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
var rootCmd = &cobra.Command{Use: "klog"}
|
||||
|
||||
// config command
|
||||
var configCmd = &cobra.Command{Use: "config", Short: "Configure klog settings"}
|
||||
|
||||
var configTokenCmd = &cobra.Command{
|
||||
Use: "token [token]",
|
||||
Short: "Save access token",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, _ := loadConfig()
|
||||
cfg.AccessToken = args[0]
|
||||
return saveConfig(cfg)
|
||||
},
|
||||
}
|
||||
|
||||
var configURLCmd = &cobra.Command{
|
||||
Use: "url [url]",
|
||||
Short: "Save API base URL",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, _ := loadConfig()
|
||||
cfg.APIUrl = args[0]
|
||||
return saveConfig(cfg)
|
||||
},
|
||||
}
|
||||
|
||||
var configInsecureCmd = &cobra.Command{
|
||||
Use: "insecure [true|false]",
|
||||
Short: "Ignore self-signed certificates",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, _ := loadConfig()
|
||||
val := strings.ToLower(args[0])
|
||||
cfg.InsecureSkipVerify = val == "true" || val == "1" || val == "yes"
|
||||
return saveConfig(cfg)
|
||||
},
|
||||
}
|
||||
|
||||
var configShowCmd = &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Show current configuration",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Access Token: %s\nAPI URL: %s\nIgnore Self-Signed Certs: %v\n", cfg.AccessToken, cfg.APIUrl, cfg.InsecureSkipVerify)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
configCmd.AddCommand(configTokenCmd, configURLCmd, configInsecureCmd, configShowCmd)
|
||||
|
||||
// coffee command
|
||||
var cCmd = &cobra.Command{
|
||||
Use: "c",
|
||||
Aliases: []string{"k", "coffee", "kaffee"},
|
||||
Short: "Log a coffee",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return postFormRequest("coffee", nil)
|
||||
},
|
||||
}
|
||||
|
||||
var balanceCmd = &cobra.Command{
|
||||
Use: "balance",
|
||||
Short: "Show my balance",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return postFormRequest("balance", nil)
|
||||
},
|
||||
}
|
||||
|
||||
// brew command
|
||||
var planTime int
|
||||
var notify bool
|
||||
var plan bool
|
||||
var brewCmd = &cobra.Command{
|
||||
Use: "brew [waterIn]",
|
||||
Short: "Log a brew with parameters",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
cmd.Help()
|
||||
return nil
|
||||
}
|
||||
form := url.Values{}
|
||||
form.Set("waterIn", args[0])
|
||||
form.Set("notify", strconv.FormatBool(notify))
|
||||
form.Set("plan", strconv.FormatBool(plan))
|
||||
if plan {
|
||||
form.Set("planTime", strconv.Itoa(planTime))
|
||||
}
|
||||
return postFormRequest("brew", form)
|
||||
},
|
||||
}
|
||||
brewCmd.Flags().IntVar(&planTime, "planTime", 8, "Planning time in minutes (e.g. 4, 5, 6, 7, 8) (relevant only if plan=true)")
|
||||
brewCmd.Flags().BoolVar(¬ify, "notify", true, "Enable/disable notifications")
|
||||
brewCmd.Flags().BoolVar(&plan, "plan", true, "Enable/disable planning")
|
||||
|
||||
rootCmd.AddCommand(configCmd, balanceCmd, cCmd, brewCmd)
|
||||
rootCmd.Execute()
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue