Files
esacli/main.go
ssig33 730f637518
All checks were successful
CI / Build & Test (push) Successful in 34s
[自動コミット] esa.io API を操作する Go 製 CLI ツールを新規作成
- esa.io API クライアントの実装と単体テストを追加
- 記事の取得・検索・作成・更新を行うサブコマンドを実装
- GitHub Actions による CI ワークフローを設定
- プロジェクトの基本ファイル(README、.gitignore、LICENSE、go.mod)を作成
2026-05-19 08:52:05 +09:00

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)
}