All checks were successful
CI / Build & Test (push) Successful in 34s
- esa.io API クライアントの実装と単体テストを追加 - 記事の取得・検索・作成・更新を行うサブコマンドを実装 - GitHub Actions による CI ワークフローを設定 - プロジェクトの基本ファイル(README、.gitignore、LICENSE、go.mod)を作成
262 lines
6.1 KiB
Go
262 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.ssig33.com/ssig33/esacli/esa"
|
|
)
|
|
|
|
const usage = `esacli - esa.io API CLI
|
|
|
|
Usage:
|
|
esacli <command> [options]
|
|
|
|
Commands:
|
|
get Fetch a single post
|
|
search Search posts
|
|
create Create a new post
|
|
update Update an existing post
|
|
help Show this help
|
|
|
|
Environment variables:
|
|
ESA_TOKEN esa.io API access token (required)
|
|
ESA_TEAM esa.io team name (required)
|
|
|
|
Run "esacli <command> -h" for command-specific options.
|
|
`
|
|
|
|
func main() {
|
|
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
|
|
fmt.Fprintln(os.Stderr, "error:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run(args []string, stdout, stderr io.Writer) error {
|
|
if len(args) == 0 {
|
|
fmt.Fprint(stderr, usage)
|
|
return errors.New("no command specified")
|
|
}
|
|
|
|
cmd := args[0]
|
|
rest := args[1:]
|
|
|
|
switch cmd {
|
|
case "help", "-h", "--help":
|
|
fmt.Fprint(stdout, usage)
|
|
return nil
|
|
case "get":
|
|
return runGet(rest, stdout)
|
|
case "search":
|
|
return runSearch(rest, stdout)
|
|
case "create":
|
|
return runCreate(rest, stdout)
|
|
case "update":
|
|
return runUpdate(rest, stdout)
|
|
default:
|
|
fmt.Fprint(stderr, usage)
|
|
return fmt.Errorf("unknown command: %s", cmd)
|
|
}
|
|
}
|
|
|
|
func newClient() (*esa.Client, error) {
|
|
token := os.Getenv("ESA_TOKEN")
|
|
if token == "" {
|
|
return nil, errors.New("ESA_TOKEN is not set")
|
|
}
|
|
team := os.Getenv("ESA_TEAM")
|
|
if team == "" {
|
|
return nil, errors.New("ESA_TEAM is not set")
|
|
}
|
|
c := esa.NewClient(team, token)
|
|
if base := os.Getenv("ESA_BASE_URL"); base != "" {
|
|
c.BaseURL = base
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func runGet(args []string, stdout io.Writer) error {
|
|
fs := flag.NewFlagSet("get", flag.ContinueOnError)
|
|
number := fs.Int("number", 0, "post number (required)")
|
|
bodyOnly := fs.Bool("body", false, "print only body_md instead of JSON")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if *number <= 0 {
|
|
return errors.New("get: -number is required")
|
|
}
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
post, err := c.GetPost(context.Background(), *number)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *bodyOnly {
|
|
fmt.Fprint(stdout, post.BodyMD)
|
|
return nil
|
|
}
|
|
return printJSON(stdout, post)
|
|
}
|
|
|
|
func runSearch(args []string, stdout io.Writer) error {
|
|
fs := flag.NewFlagSet("search", flag.ContinueOnError)
|
|
query := fs.String("q", "", "search query")
|
|
page := fs.Int("page", 0, "page number")
|
|
perPage := fs.Int("per-page", 0, "results per page (max 100)")
|
|
sort := fs.String("sort", "", "sort field (updated, created, number, stars, watches, comments, best_match)")
|
|
order := fs.String("order", "", "order (desc, asc)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.SearchPosts(context.Background(), esa.SearchOptions{
|
|
Query: *query,
|
|
Page: *page,
|
|
PerPage: *perPage,
|
|
Sort: *sort,
|
|
Order: *order,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printJSON(stdout, resp)
|
|
}
|
|
|
|
func runCreate(args []string, stdout io.Writer) error {
|
|
fs := flag.NewFlagSet("create", flag.ContinueOnError)
|
|
name := fs.String("name", "", "post title (required)")
|
|
bodyFile := fs.String("body-file", "", "path to file containing body_md ('-' for stdin)")
|
|
body := fs.String("body", "", "body_md (use -body-file for longer content)")
|
|
tagsCSV := fs.String("tags", "", "comma-separated tags")
|
|
category := fs.String("category", "", "category")
|
|
wip := fs.Bool("wip", true, "WIP flag")
|
|
message := fs.String("message", "", "commit-like message")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if *name == "" {
|
|
return errors.New("create: -name is required")
|
|
}
|
|
bodyMD, err := resolveBody(*body, *bodyFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wipVal := *wip
|
|
post, err := c.CreatePost(context.Background(), esa.PostInput{
|
|
Name: *name,
|
|
BodyMD: bodyMD,
|
|
Tags: splitCSV(*tagsCSV),
|
|
Category: *category,
|
|
WIP: &wipVal,
|
|
Message: *message,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printJSON(stdout, post)
|
|
}
|
|
|
|
func runUpdate(args []string, stdout io.Writer) error {
|
|
fs := flag.NewFlagSet("update", flag.ContinueOnError)
|
|
number := fs.Int("number", 0, "post number (required)")
|
|
name := fs.String("name", "", "new title")
|
|
bodyFile := fs.String("body-file", "", "path to file containing body_md ('-' for stdin)")
|
|
body := fs.String("body", "", "body_md (use -body-file for longer content)")
|
|
tagsCSV := fs.String("tags", "", "comma-separated tags")
|
|
category := fs.String("category", "", "category")
|
|
wipFlag := fs.String("wip", "", "WIP flag (true/false); empty leaves unchanged")
|
|
message := fs.String("message", "", "commit-like message")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if *number <= 0 {
|
|
return errors.New("update: -number is required")
|
|
}
|
|
bodyMD, err := resolveBody(*body, *bodyFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
input := esa.PostInput{
|
|
Name: *name,
|
|
BodyMD: bodyMD,
|
|
Tags: splitCSV(*tagsCSV),
|
|
Category: *category,
|
|
Message: *message,
|
|
}
|
|
if *wipFlag != "" {
|
|
v, err := strconv.ParseBool(*wipFlag)
|
|
if err != nil {
|
|
return fmt.Errorf("update: -wip: %w", err)
|
|
}
|
|
input.WIP = &v
|
|
}
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
post, err := c.UpdatePost(context.Background(), *number, input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printJSON(stdout, post)
|
|
}
|
|
|
|
func resolveBody(inline, file string) (string, error) {
|
|
if inline != "" && file != "" {
|
|
return "", errors.New("specify only one of -body or -body-file")
|
|
}
|
|
if file == "" {
|
|
return inline, nil
|
|
}
|
|
if file == "-" {
|
|
b, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read stdin: %w", err)
|
|
}
|
|
return string(b), nil
|
|
}
|
|
b, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read body file: %w", err)
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
func splitCSV(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(s, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func printJSON(w io.Writer, v any) error {
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(v)
|
|
}
|