feat: parse all Go CLIs with flaggy; dump bash complete. (#36)
Tests / Test (push) Skipped
Tests / OCR (tesseract fixture) (push) Skipped
Tests / Release (semver) (push) Skipped
Tests / Test (push) Skipped
Tests / OCR (tesseract fixture) (push) Skipped
Tests / Release (semver) (push) Skipped
stdlib flag dropped --hop after the query. One wrapper in internal/cli, source <(./bin/cli/complete.go bash). Gitea #34.
This commit is contained in:
+49
-51
@@ -3,12 +3,15 @@ package rank
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
const Usage = `usage: bin/brain/search.go "query" [--root facts|info] [--repo REPO] [-n N] [--hop N] [--json] [--no-web]
|
||||
bin/brain/search.go serve [port]
|
||||
bin/brain/search.go --list-model`
|
||||
bin/brain/search.go --list-model
|
||||
source <(./bin/cli/complete.go bash)`
|
||||
|
||||
type Options struct {
|
||||
Query string
|
||||
@@ -21,62 +24,57 @@ type Options struct {
|
||||
NoWeb bool
|
||||
}
|
||||
|
||||
// NewParser is the flaggy schema for search (also used by bin/cli/complete.go).
|
||||
func NewParser(opt *Options) *flaggy.Parser {
|
||||
if opt.Limit == 0 {
|
||||
opt.Limit = 20
|
||||
}
|
||||
p := cli.New("brain-search")
|
||||
p.Description = "deduction search: facts → info → web"
|
||||
p.String(&opt.Root, "", "root", "facts or info")
|
||||
p.String(&opt.Repo, "", "repo", "filter by repo")
|
||||
p.Int(&opt.Limit, "n", "n", "max hits")
|
||||
p.Int(&opt.Hop, "", "hop", "walk FROM_FILE depth 1-3")
|
||||
p.Bool(&opt.JSONOut, "", "json", "JSON output")
|
||||
p.Bool(&opt.NoWeb, "", "no-web", "stay local")
|
||||
p.Bool(&opt.ListModel, "", "list-model", "print embedding model")
|
||||
return p
|
||||
}
|
||||
|
||||
// ParseArgs reads flags. Unknown flags are an error: silently dropping them
|
||||
// meant `--hop 1` vanished and its argument `1` was appended to the query.
|
||||
func ParseArgs(args []string) (Options, error) {
|
||||
opt := Options{Limit: 20}
|
||||
var queryArgs []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
wantsValue := arg == "--root" || arg == "--repo" || arg == "-n" || arg == "--hop"
|
||||
if wantsValue && i+1 >= len(args) {
|
||||
return opt, fmt.Errorf("%s needs a value", arg)
|
||||
}
|
||||
switch arg {
|
||||
case "--root":
|
||||
i++
|
||||
opt.Root = args[i]
|
||||
if opt.Root != "facts" && opt.Root != "info" {
|
||||
return opt, fmt.Errorf("--root must be facts or info, got %q", opt.Root)
|
||||
}
|
||||
case "--repo":
|
||||
i++
|
||||
opt.Repo = args[i]
|
||||
case "-n":
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opt, fmt.Errorf("-n must be a positive integer, got %q", args[i])
|
||||
}
|
||||
opt.Limit = n
|
||||
case "--hop":
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opt, fmt.Errorf("--hop must be a positive integer, got %q", args[i])
|
||||
}
|
||||
if n > 3 {
|
||||
return opt, fmt.Errorf("--hop max is 3 (File → Commit → Person)")
|
||||
}
|
||||
opt.Hop = n
|
||||
case "--json":
|
||||
opt.JSONOut = true
|
||||
case "--no-web":
|
||||
opt.NoWeb = true
|
||||
case "--list-model":
|
||||
opt.ListModel = true
|
||||
default:
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
return opt, fmt.Errorf("unknown flag %q", arg)
|
||||
}
|
||||
queryArgs = append(queryArgs, arg)
|
||||
}
|
||||
p := NewParser(&opt)
|
||||
var q string
|
||||
p.AddPositionalValue(&q, "query", 1, false, "search query")
|
||||
if err := cli.Parse(p, args); err != nil {
|
||||
return opt, err
|
||||
}
|
||||
opt.Query = cli.Query(q, p.TrailingArguments)
|
||||
if opt.Root != "" && opt.Root != "facts" && opt.Root != "info" {
|
||||
return opt, fmt.Errorf("--root must be facts or info, got %q", opt.Root)
|
||||
}
|
||||
if opt.Limit < 1 {
|
||||
return opt, fmt.Errorf("-n must be a positive integer, got %q", strconv.Itoa(opt.Limit))
|
||||
}
|
||||
if opt.Hop < 0 {
|
||||
return opt, fmt.Errorf("--hop must be a positive integer, got %q", strconv.Itoa(opt.Hop))
|
||||
}
|
||||
if opt.Hop > 3 {
|
||||
return opt, fmt.Errorf("--hop max is 3 (File → Commit → Person)")
|
||||
}
|
||||
|
||||
opt.Query = strings.TrimSpace(strings.Join(queryArgs, " "))
|
||||
if opt.Query == "" && !opt.ListModel {
|
||||
return opt, fmt.Errorf("no query given")
|
||||
}
|
||||
return opt, nil
|
||||
}
|
||||
|
||||
// Parser is the search schema for bin/cli/complete.go.
|
||||
func Parser() *flaggy.Parser {
|
||||
opt := Options{Limit: 20}
|
||||
p := NewParser(&opt)
|
||||
var q string
|
||||
p.AddPositionalValue(&q, "query", 1, false, "search query")
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package rank
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
type GetOptions struct {
|
||||
ID string
|
||||
Body bool
|
||||
JSONOut bool
|
||||
}
|
||||
|
||||
func GetParser(opt *GetOptions) *flaggy.Parser {
|
||||
p := cli.New("brain-get")
|
||||
p.Description = "read one leaf"
|
||||
p.Bool(&opt.Body, "", "body", "full text instead of snippet")
|
||||
p.Bool(&opt.JSONOut, "", "json", "JSON output")
|
||||
p.AddPositionalValue(&opt.ID, "id", 1, false, "leaf id")
|
||||
return p
|
||||
}
|
||||
|
||||
func ParseGet(args []string) (GetOptions, error) {
|
||||
var opt GetOptions
|
||||
if err := cli.Parse(GetParser(&opt), args); err != nil {
|
||||
return opt, err
|
||||
}
|
||||
if opt.ID == "" {
|
||||
return opt, fmt.Errorf("id required")
|
||||
}
|
||||
return opt, nil
|
||||
}
|
||||
|
||||
type JSONFlag struct {
|
||||
JSONOut bool
|
||||
}
|
||||
|
||||
func bindJSON(name string, opt *JSONFlag) *flaggy.Parser {
|
||||
p := cli.New(name)
|
||||
p.Bool(&opt.JSONOut, "", "json", "JSON output")
|
||||
return p
|
||||
}
|
||||
|
||||
func StatsParser() *flaggy.Parser {
|
||||
opt := JSONFlag{}
|
||||
return bindJSON("brain-stats", &opt)
|
||||
}
|
||||
|
||||
func EvalParser() *flaggy.Parser {
|
||||
opt := JSONFlag{}
|
||||
return bindJSON("brain-eval", &opt)
|
||||
}
|
||||
|
||||
func ParseJSONFlag(name string, args []string) (JSONFlag, error) {
|
||||
var opt JSONFlag
|
||||
return opt, cli.Parse(bindJSON(name, &opt), args)
|
||||
}
|
||||
+13
-48
@@ -11,30 +11,15 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/eSlider/2dph/internal/brain/rank"
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
)
|
||||
|
||||
func MainGet(args []string) int {
|
||||
id, body, jsonOut := "", false, false
|
||||
for _, a := range args {
|
||||
switch {
|
||||
case a == "--body":
|
||||
body = true
|
||||
case a == "--json":
|
||||
jsonOut = true
|
||||
case a == "-h" || a == "--help":
|
||||
fmt.Fprintln(os.Stderr, `usage: bin/brain/get.go <id> [--body] [--json]`)
|
||||
return 0
|
||||
case strings.HasPrefix(a, "-"):
|
||||
fmt.Fprintf(os.Stderr, "brain/get: unknown flag %s\n", a)
|
||||
return 2
|
||||
default:
|
||||
id = a
|
||||
}
|
||||
}
|
||||
if id == "" {
|
||||
fmt.Fprintln(os.Stderr, "brain/get: id required")
|
||||
return 2
|
||||
opt, err := rank.ParseGet(args)
|
||||
if err != nil {
|
||||
return cli.Fail(err)
|
||||
}
|
||||
id, body, jsonOut := opt.ID, opt.Body, opt.JSONOut
|
||||
if err := openBrain(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "open brain: %v\n", err)
|
||||
return 1
|
||||
@@ -72,21 +57,11 @@ func MainGet(args []string) int {
|
||||
}
|
||||
|
||||
func MainStats(args []string) int {
|
||||
jsonOut := false
|
||||
for _, a := range args {
|
||||
switch a {
|
||||
case "--json":
|
||||
jsonOut = true
|
||||
case "-h", "--help":
|
||||
fmt.Fprintln(os.Stderr, `usage: bin/brain/stats.go [--json]`)
|
||||
return 0
|
||||
default:
|
||||
if strings.HasPrefix(a, "-") {
|
||||
fmt.Fprintf(os.Stderr, "brain/stats: unknown flag %s\n", a)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
opt, err := rank.ParseJSONFlag("brain-stats", args)
|
||||
if err != nil {
|
||||
return cli.Fail(err)
|
||||
}
|
||||
jsonOut := opt.JSONOut
|
||||
if err := openBrain(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "open brain: %v\n", err)
|
||||
return 1
|
||||
@@ -124,21 +99,11 @@ func MainStats(args []string) int {
|
||||
}
|
||||
|
||||
func MainEval(args []string) int {
|
||||
jsonOut := false
|
||||
for _, a := range args {
|
||||
switch a {
|
||||
case "--json":
|
||||
jsonOut = true
|
||||
case "-h", "--help":
|
||||
fmt.Fprintln(os.Stderr, `usage: bin/brain/eval.go [--json]`)
|
||||
return 0
|
||||
default:
|
||||
if strings.HasPrefix(a, "-") {
|
||||
fmt.Fprintf(os.Stderr, "brain/eval: unknown flag %s\n", a)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
opt, err := rank.ParseJSONFlag("brain-eval", args)
|
||||
if err != nil {
|
||||
return cli.Fail(err)
|
||||
}
|
||||
jsonOut := opt.JSONOut
|
||||
if err := openBrain(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "open brain: %v\n", err)
|
||||
return 1
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
lbug "github.com/LadybugDB/go-ladybug"
|
||||
"github.com/eSlider/2dph/internal/brain/rank"
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
)
|
||||
|
||||
const defaultPort = 17830
|
||||
@@ -29,6 +30,9 @@ const healthPath = "/health"
|
||||
func runSearch(args []string) int {
|
||||
opt, err := rank.ParseArgs(args)
|
||||
if err != nil {
|
||||
if errors.Is(err, cli.ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "brain/search: %v\n%s\n", err, rank.Usage)
|
||||
return 2
|
||||
}
|
||||
|
||||
+15
-21
@@ -3,21 +3,22 @@ package chats
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
cliparse "github.com/eSlider/2dph/internal/cli"
|
||||
)
|
||||
|
||||
type ooContact struct {
|
||||
ID int `json:"id"`
|
||||
ID int `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
About string `json:"about"`
|
||||
CommonData []struct {
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
About string `json:"about"`
|
||||
CommonData []struct {
|
||||
InfoType int `json:"infoType"`
|
||||
Data string `json:"data"`
|
||||
Category string `json:"categoryName"`
|
||||
@@ -25,16 +26,9 @@ type ooContact struct {
|
||||
}
|
||||
|
||||
func RunApply(args []string) int {
|
||||
fs := flag.NewFlagSet("chats apply", flag.ContinueOnError)
|
||||
dryRun := fs.Bool("dry-run", false, "show what would be done without writing")
|
||||
help := fs.Bool("help", false, "")
|
||||
fs.SetOutput(os.Stderr)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *help {
|
||||
fmt.Fprintln(os.Stderr, "usage: chats apply [--dry-run]")
|
||||
return 0
|
||||
dryRun, err := parseApplyFlags(args)
|
||||
if err != nil {
|
||||
return cliparse.Fail(err)
|
||||
}
|
||||
|
||||
ooCLI := findOO()
|
||||
@@ -60,10 +54,10 @@ func RunApply(args []string) int {
|
||||
emailFacts = dedupeFacts(emailFacts)
|
||||
|
||||
type resolvedFact struct {
|
||||
Fact ExtractedFact
|
||||
OoID int
|
||||
OoName string
|
||||
Action string // "info-add" or "persons-create"
|
||||
Fact ExtractedFact
|
||||
OoID int
|
||||
OoName string
|
||||
Action string // "info-add" or "persons-create"
|
||||
}
|
||||
|
||||
var resolved []resolvedFact
|
||||
@@ -128,7 +122,7 @@ func RunApply(args []string) int {
|
||||
|
||||
fmt.Printf("\nchats apply: %d actions to apply\n", len(resolved))
|
||||
|
||||
if *dryRun {
|
||||
if dryRun {
|
||||
for _, r := range resolved {
|
||||
switch r.Action {
|
||||
case "info-add":
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package chats
|
||||
|
||||
import (
|
||||
cliparse "github.com/eSlider/2dph/internal/cli"
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
type syncTelegramFlags struct {
|
||||
Limit int
|
||||
Phone string
|
||||
}
|
||||
|
||||
type syncLinkedInFlags struct {
|
||||
Limit int
|
||||
Refresh bool
|
||||
}
|
||||
|
||||
func SyncParser() *flaggy.Parser {
|
||||
p := cliparse.New("chats-sync")
|
||||
p.Description = "download chats to var/chats"
|
||||
tg := flaggy.NewSubcommand("telegram")
|
||||
li := flaggy.NewSubcommand("linkedin")
|
||||
var limit int
|
||||
var phone string
|
||||
var refresh bool
|
||||
tg.Int(&limit, "", "limit", "max messages per chat")
|
||||
tg.String(&phone, "", "phone", "phone (default TELEGRAM_PHONE)")
|
||||
li.Int(&limit, "", "limit", "max messages per conversation")
|
||||
li.Bool(&refresh, "", "refresh", "refresh webtop session")
|
||||
p.AttachSubcommand(tg, 1)
|
||||
p.AttachSubcommand(li, 1)
|
||||
return p
|
||||
}
|
||||
|
||||
func ImportParser() *flaggy.Parser {
|
||||
return cliparse.New("chats-import")
|
||||
}
|
||||
|
||||
func FactsParser() *flaggy.Parser {
|
||||
return cliparse.New("chats-facts")
|
||||
}
|
||||
|
||||
func ApplyParser() *flaggy.Parser {
|
||||
p := cliparse.New("chats-apply")
|
||||
dry := false
|
||||
p.Bool(&dry, "", "dry-run", "show without writing")
|
||||
return p
|
||||
}
|
||||
|
||||
func parseTelegramFlags(args []string) (syncTelegramFlags, error) {
|
||||
var f syncTelegramFlags
|
||||
p := cliparse.New("chats-sync-telegram")
|
||||
p.Int(&f.Limit, "", "limit", "max messages per chat")
|
||||
p.String(&f.Phone, "", "phone", "phone (default TELEGRAM_PHONE)")
|
||||
return f, cliparse.Parse(p, args)
|
||||
}
|
||||
|
||||
func parseLinkedInFlags(args []string) (syncLinkedInFlags, error) {
|
||||
var f syncLinkedInFlags
|
||||
p := cliparse.New("chats-sync-linkedin")
|
||||
p.Int(&f.Limit, "", "limit", "max messages per conversation")
|
||||
p.Bool(&f.Refresh, "", "refresh", "refresh webtop session")
|
||||
return f, cliparse.Parse(p, args)
|
||||
}
|
||||
|
||||
func parseApplyFlags(args []string) (dryRun bool, err error) {
|
||||
p := cliparse.New("chats-apply")
|
||||
p.Bool(&dryRun, "", "dry-run", "show without writing")
|
||||
return dryRun, cliparse.Parse(p, args)
|
||||
}
|
||||
|
||||
func parseNoFlags(name string, args []string) error {
|
||||
return cliparse.Parse(cliparse.New(name), args)
|
||||
}
|
||||
+4
-10
@@ -3,12 +3,13 @@ package chats
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
cliparse "github.com/eSlider/2dph/internal/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -76,15 +77,8 @@ type ExtractedFact struct {
|
||||
}
|
||||
|
||||
func RunFacts(args []string) int {
|
||||
fs := flag.NewFlagSet("chats facts", flag.ContinueOnError)
|
||||
help := fs.Bool("help", false, "")
|
||||
fs.SetOutput(os.Stderr)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *help {
|
||||
fmt.Fprintln(os.Stderr, "usage: chats facts")
|
||||
return 0
|
||||
if err := parseNoFlags("chats-facts", args); err != nil {
|
||||
return cliparse.Fail(err)
|
||||
}
|
||||
|
||||
root := Dir()
|
||||
|
||||
@@ -4,25 +4,19 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
cliparse "github.com/eSlider/2dph/internal/cli"
|
||||
)
|
||||
|
||||
func RunImport(args []string) int {
|
||||
fs := flag.NewFlagSet("chats import", flag.ContinueOnError)
|
||||
help := fs.Bool("help", false, "")
|
||||
fs.SetOutput(os.Stderr)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *help {
|
||||
fmt.Fprintln(os.Stderr, "usage: chats import")
|
||||
return 0
|
||||
if err := parseNoFlags("chats-import", args); err != nil {
|
||||
return cliparse.Fail(err)
|
||||
}
|
||||
|
||||
root := Dir()
|
||||
|
||||
@@ -2,12 +2,13 @@ package chats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
cliparse "github.com/eSlider/2dph/internal/cli"
|
||||
)
|
||||
|
||||
func checkLinkedInSession(userDataDir string) (bool, error) {
|
||||
@@ -29,18 +30,12 @@ func checkLinkedInSession(userDataDir string) (bool, error) {
|
||||
}
|
||||
|
||||
func RunSyncLinkedIn(args []string) int {
|
||||
fs := flag.NewFlagSet("chats sync linkedin", flag.ContinueOnError)
|
||||
limit := fs.Int("limit", 0, "max messages per conversation (0 = all)")
|
||||
refresh := fs.Bool("refresh", false, "refresh session from live webtop browser before sync")
|
||||
help := fs.Bool("help", false, "")
|
||||
fs.SetOutput(os.Stderr)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *help {
|
||||
fmt.Fprintln(os.Stderr, "usage: chats sync linkedin [--limit N] [--refresh]")
|
||||
return 0
|
||||
f, err := parseLinkedInFlags(args)
|
||||
if err != nil {
|
||||
return cliparse.Fail(err)
|
||||
}
|
||||
limit := f.Limit
|
||||
refresh := f.Refresh
|
||||
|
||||
userDataDir := envVar("LINKEDIN_USER_DATA_DIR", "")
|
||||
if userDataDir == "" {
|
||||
@@ -48,7 +43,7 @@ func RunSyncLinkedIn(args []string) int {
|
||||
userDataDir = home + "/.linkedin-mcp/profile"
|
||||
}
|
||||
|
||||
if *refresh {
|
||||
if refresh {
|
||||
if code := refreshLinkedInSession(userDataDir); code != 0 {
|
||||
return code
|
||||
}
|
||||
@@ -72,7 +67,7 @@ func RunSyncLinkedIn(args []string) int {
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
if err := src.Sync(ctx, Dir(), *limit); err != nil {
|
||||
if err := src.Sync(ctx, Dir(), limit); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chats sync linkedin: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -2,33 +2,28 @@ package chats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cliparse "github.com/eSlider/2dph/internal/cli"
|
||||
)
|
||||
|
||||
func RunSyncTelegram(args []string) int {
|
||||
fs := flag.NewFlagSet("chats sync telegram", flag.ContinueOnError)
|
||||
limit := fs.Int("limit", 0, "max messages per chat (0 = all)")
|
||||
phone := fs.String("phone", "", "phone number (default env TELEGRAM_PHONE)")
|
||||
help := fs.Bool("help", false, "")
|
||||
fs.SetOutput(os.Stderr)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *help {
|
||||
fmt.Fprintln(os.Stderr, "usage: chats sync telegram [--limit N] [--phone PHONE]")
|
||||
return 0
|
||||
f, err := parseTelegramFlags(args)
|
||||
if err != nil {
|
||||
return cliparse.Fail(err)
|
||||
}
|
||||
limit := f.Limit
|
||||
phone := f.Phone
|
||||
|
||||
apiIDStr := envVar("TELEGRAM_API_ID", "")
|
||||
apiHash := envVar("TELEGRAM_API_HASH", "")
|
||||
sessionStr := envVar("TELEGRAM_SESSION_STRING", "")
|
||||
phoneNum := *phone
|
||||
phoneNum := phone
|
||||
if phoneNum == "" {
|
||||
phoneNum = envVar("TELEGRAM_PHONE", "")
|
||||
}
|
||||
@@ -78,7 +73,7 @@ func RunSyncTelegram(args []string) int {
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
if err := src.Sync(ctx, Dir(), *limit); err != nil {
|
||||
if err := src.Sync(ctx, Dir(), limit); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chats sync telegram: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
// Package cli is the shared flaggy wrapper (D23).
|
||||
//
|
||||
// flaggy: zero deps, flags at any position, shell completion scripts.
|
||||
// Individual tools keep ShowCompletion off so a query like "completion" is
|
||||
// not stolen; dump scripts with bin/cli/complete.go.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
// ErrHelp means -h/--help was requested (exit 0).
|
||||
var ErrHelp = errors.New("help")
|
||||
|
||||
var parseMu sync.Mutex
|
||||
|
||||
// New returns a per-call parser. Never reuse: flaggy parses once.
|
||||
func New(name string) *flaggy.Parser {
|
||||
p := flaggy.NewParser(name)
|
||||
p.ShowVersionWithVersionFlag = false
|
||||
p.ShowCompletion = false
|
||||
// Extra positionals become TrailingArguments (search "two words --json").
|
||||
// Unknown dash tokens are rejected in Parse after flaggy returns.
|
||||
p.ShowHelpOnUnexpected = false
|
||||
p.ShowHelpWithHFlag = true
|
||||
return p
|
||||
}
|
||||
|
||||
// Parse runs p.ParseArgs and turns flaggy's os.Exit into an error.
|
||||
// Not safe to call in parallel (flaggy.PanicInsteadOfExit is process-global).
|
||||
func Parse(p *flaggy.Parser, args []string) error {
|
||||
parseMu.Lock()
|
||||
defer parseMu.Unlock()
|
||||
prev := flaggy.PanicInsteadOfExit
|
||||
flaggy.PanicInsteadOfExit = true
|
||||
defer func() { flaggy.PanicInsteadOfExit = prev }()
|
||||
|
||||
var exitMsg string
|
||||
err := func() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
exitMsg = fmt.Sprint(r)
|
||||
}
|
||||
}()
|
||||
return p.ParseArgs(args)
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exitMsg != "" {
|
||||
if strings.Contains(exitMsg, "code: 0") {
|
||||
return ErrHelp
|
||||
}
|
||||
return errors.New(exitMsg)
|
||||
}
|
||||
if u := unknownFlags(p, args); len(u) > 0 {
|
||||
return fmt.Errorf("unknown flag %q", u[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unknownFlags(p *flaggy.Parser, args []string) []string {
|
||||
flags := collectFlags(&p.Subcommand)
|
||||
var out []string
|
||||
skipNext := false
|
||||
for _, a := range args {
|
||||
if skipNext {
|
||||
skipNext = false
|
||||
continue
|
||||
}
|
||||
if a == "--" {
|
||||
break
|
||||
}
|
||||
name, inline := flagName(a)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if name == "h" || name == "help" {
|
||||
continue
|
||||
}
|
||||
f := findFlag(flags, name)
|
||||
if f == nil {
|
||||
out = append(out, a)
|
||||
continue
|
||||
}
|
||||
if !inline && !isBoolFlag(f) {
|
||||
skipNext = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flagName(a string) (name string, inline bool) {
|
||||
if a == "-" || !strings.HasPrefix(a, "-") {
|
||||
return "", false
|
||||
}
|
||||
rest := strings.TrimLeft(a, "-")
|
||||
name, _, inline = strings.Cut(rest, "=")
|
||||
return name, inline
|
||||
}
|
||||
|
||||
func collectFlags(sc *flaggy.Subcommand) []*flaggy.Flag {
|
||||
out := append([]*flaggy.Flag{}, sc.Flags...)
|
||||
for _, sub := range sc.Subcommands {
|
||||
out = append(out, collectFlags(sub)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func findFlag(flags []*flaggy.Flag, name string) *flaggy.Flag {
|
||||
for _, f := range flags {
|
||||
if f.HasName(name) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isBoolFlag(f *flaggy.Flag) bool {
|
||||
_, ok := f.AssignmentVar.(*bool)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Query joins the first positional with leftover trailing words.
|
||||
func Query(first string, trailing []string) string {
|
||||
parts := make([]string, 0, 1+len(trailing))
|
||||
if s := strings.TrimSpace(first); s != "" {
|
||||
parts = append(parts, s)
|
||||
}
|
||||
for _, t := range trailing {
|
||||
if s := strings.TrimSpace(t); s != "" {
|
||||
parts = append(parts, s)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// Code maps parse errors to process exit codes (0 help, 2 usage).
|
||||
func Code(err error) int {
|
||||
if err == nil || errors.Is(err, ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
// Fail prints err unless it is help or a flaggy exit that already wrote stderr.
|
||||
func Fail(err error) int {
|
||||
if err == nil || errors.Is(err, ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), "Panic instead of exit") {
|
||||
return 2
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 2
|
||||
}
|
||||
|
||||
// Tool is one shebang CLI for completion dump.
|
||||
type Tool struct {
|
||||
Path string
|
||||
Name string
|
||||
New func() *flaggy.Parser
|
||||
}
|
||||
|
||||
// BashScript concatenates flaggy bash complete scripts and binds each
|
||||
// function to the shebang path (./bin/subject/method.go).
|
||||
func BashScript(tools []Tool) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# 2dph flaggy completions (D23). source <(./bin/cli/complete.go bash)\n")
|
||||
for _, t := range tools {
|
||||
p := t.New()
|
||||
p.Name = t.Name
|
||||
script := flaggy.GenerateBashCompletion(p)
|
||||
b.WriteString(script)
|
||||
fn := "_" + strings.ReplaceAll(t.Name, "-", "_") + "_complete"
|
||||
if t.Path != "" && t.Path != t.Name {
|
||||
fmt.Fprintf(&b, "complete -F %s %s\n", fn, t.Path)
|
||||
if !strings.HasPrefix(t.Path, "./") {
|
||||
fmt.Fprintf(&b, "complete -F %s ./%s\n", fn, t.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
func TestParseBoolAndIntAnyPosition(t *testing.T) {
|
||||
p := New("t")
|
||||
jsonOut := false
|
||||
n := 20
|
||||
q := ""
|
||||
p.Bool(&jsonOut, "", "json", "JSON")
|
||||
p.Int(&n, "n", "n", "limit")
|
||||
p.AddPositionalValue(&q, "query", 1, false, "q")
|
||||
if err := Parse(p, []string{"two", "words", "--json", "-n", "5"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := Query(q, p.TrailingArguments)
|
||||
if got != "two words" || !jsonOut || n != 5 {
|
||||
t.Fatalf("q=%q json=%v n=%d", got, jsonOut, n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUnknownFlagIsError(t *testing.T) {
|
||||
p := New("t")
|
||||
jsonOut := false
|
||||
p.Bool(&jsonOut, "", "json", "JSON")
|
||||
if err := Parse(p, []string{"--nope"}); err == nil {
|
||||
t.Fatal("unknown flag accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHelpIsErrHelp(t *testing.T) {
|
||||
p := New("t")
|
||||
jsonOut := false
|
||||
p.Bool(&jsonOut, "", "json", "JSON")
|
||||
err := Parse(p, []string{"--help"})
|
||||
if !errors.Is(err, ErrHelp) {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMissingFlagValueIsError(t *testing.T) {
|
||||
p := New("t")
|
||||
n := 0
|
||||
p.Int(&n, "", "hop", "hop")
|
||||
if err := Parse(p, []string{"--hop"}); err == nil {
|
||||
t.Fatal("expected missing value error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBashScriptNamesShebangPath(t *testing.T) {
|
||||
out := BashScript([]Tool{{
|
||||
Path: "bin/brain/search.go",
|
||||
Name: "brain-search",
|
||||
New: newSearchLike,
|
||||
}})
|
||||
if !strings.Contains(out, "--json") || !strings.Contains(out, "--hop") {
|
||||
t.Fatalf("flags missing:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "complete -F") || !strings.Contains(out, "bin/brain/search.go") {
|
||||
t.Fatalf("shebang complete missing:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func newSearchLike() *flaggy.Parser {
|
||||
p := New("brain-search")
|
||||
jsonOut := false
|
||||
hop := 0
|
||||
p.Bool(&jsonOut, "", "json", "JSON")
|
||||
p.Int(&hop, "", "hop", "graph hop")
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cli
|
||||
|
||||
import "github.com/integrii/flaggy"
|
||||
|
||||
type QAStats struct {
|
||||
JSONL string
|
||||
}
|
||||
|
||||
func QAParser() *flaggy.Parser {
|
||||
c := QAStats{}
|
||||
return BindQA(&c)
|
||||
}
|
||||
|
||||
func BindQA(c *QAStats) *flaggy.Parser {
|
||||
p := New("qa-stats")
|
||||
p.Description = "DuckDB quantiles / JSONL count"
|
||||
p.String(&c.JSONL, "", "jsonl", "JSONL file (else stdin JSON [float,…])")
|
||||
return p
|
||||
}
|
||||
|
||||
func ParseQAStats(args []string) (QAStats, error) {
|
||||
var c QAStats
|
||||
return c, Parse(BindQA(&c), args)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package gitlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
type CLI struct {
|
||||
Repo, Root, Since string
|
||||
Limit int
|
||||
JSONOut bool
|
||||
}
|
||||
|
||||
func Parser() *flaggy.Parser {
|
||||
c := CLI{}
|
||||
return Bind(&c)
|
||||
}
|
||||
|
||||
func Bind(c *CLI) *flaggy.Parser {
|
||||
p := cli.New("git-import")
|
||||
p.Description = "go-git history → commit leafs"
|
||||
p.Bool(&c.JSONOut, "", "json", "JSON output")
|
||||
p.Int(&c.Limit, "", "limit", "max commits (0 = all)")
|
||||
p.String(&c.Since, "", "since", "RFC3339 or YYYY-MM-DD")
|
||||
p.String(&c.Root, "", "root", "scan dir for git repos")
|
||||
p.AddPositionalValue(&c.Repo, "repo", 1, false, "git repo path")
|
||||
return p
|
||||
}
|
||||
|
||||
func ParseArgs(args []string) (CLI, error) {
|
||||
var c CLI
|
||||
if err := cli.Parse(Bind(&c), args); err != nil {
|
||||
return c, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func ParseSince(s string) (time.Time, error) {
|
||||
if s == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
for _, layout := range []string{time.RFC3339, "2006-01-02"} {
|
||||
if t, err := time.Parse(layout, s); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("cannot parse --since %q", s)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package mdleaves
|
||||
|
||||
import (
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
type CLI struct {
|
||||
Root string
|
||||
Files string
|
||||
JSONOut bool
|
||||
}
|
||||
|
||||
func Parser() *flaggy.Parser {
|
||||
c := CLI{Root: "."}
|
||||
return Bind(&c)
|
||||
}
|
||||
|
||||
func Bind(c *CLI) *flaggy.Parser {
|
||||
if c.Root == "" {
|
||||
c.Root = "."
|
||||
}
|
||||
p := cli.New("markdown-import")
|
||||
p.Description = "split markdown H2 leafs"
|
||||
p.Bool(&c.JSONOut, "", "json", "JSON output")
|
||||
p.String(&c.Files, "", "files", "comma-separated paths")
|
||||
p.AddPositionalValue(&c.Root, "dir", 1, false, "markdown root")
|
||||
return p
|
||||
}
|
||||
|
||||
func ParseArgs(args []string) (CLI, error) {
|
||||
c := CLI{Root: "."}
|
||||
p := Bind(&c)
|
||||
if err := cli.Parse(p, args); err != nil {
|
||||
return c, err
|
||||
}
|
||||
if extra := cli.Query("", p.TrailingArguments); extra != "" && c.Root == "." {
|
||||
c.Root = extra
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
type CLI struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func Parser() *flaggy.Parser {
|
||||
c := CLI{}
|
||||
return Bind(&c)
|
||||
}
|
||||
|
||||
func Bind(c *CLI) *flaggy.Parser {
|
||||
p := cli.New("mail-ocr")
|
||||
p.Description = "tesseract eng+deu on image or scanned PDF"
|
||||
p.AddPositionalValue(&c.Path, "file", 1, false, "image or pdf")
|
||||
return p
|
||||
}
|
||||
|
||||
func ParseArgs(args []string) (CLI, error) {
|
||||
var c CLI
|
||||
if err := cli.Parse(Bind(&c), args); err != nil {
|
||||
return c, err
|
||||
}
|
||||
if c.Path == "" {
|
||||
return c, fmt.Errorf("usage: bin/mail/ocr.go <image|pdf>")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package reasoner
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
type CLI struct {
|
||||
Base string
|
||||
Model string
|
||||
Device string
|
||||
JSONOut bool
|
||||
}
|
||||
|
||||
func Parser() *flaggy.Parser {
|
||||
c := NewCLI()
|
||||
return Bind(&c)
|
||||
}
|
||||
|
||||
func NewCLI() CLI {
|
||||
base := os.Getenv("REASONER_BASE_URL")
|
||||
if base == "" {
|
||||
base = "http://127.0.0.1:11435/v1"
|
||||
}
|
||||
model := os.Getenv("REASONER_MODEL")
|
||||
if model == "" {
|
||||
model = OllamaRAM
|
||||
}
|
||||
return CLI{Base: base, Model: model, Device: "cpu"}
|
||||
}
|
||||
|
||||
func Bind(c *CLI) *flaggy.Parser {
|
||||
p := cli.New("reasoner-bakeoff")
|
||||
p.Description = "CPU tool-call bake-off"
|
||||
p.Bool(&c.JSONOut, "", "json", "JSON output")
|
||||
p.String(&c.Model, "", "model", "Ollama/HF model id")
|
||||
p.String(&c.Base, "", "base-url", "OpenAI-compatible URL")
|
||||
p.String(&c.Device, "", "device", "cpu")
|
||||
return p
|
||||
}
|
||||
|
||||
func ParseArgs(args []string) (CLI, error) {
|
||||
c := NewCLI()
|
||||
return c, cli.Parse(Bind(&c), args)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package websearch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/eSlider/2dph/internal/cli"
|
||||
"github.com/integrii/flaggy"
|
||||
)
|
||||
|
||||
type CLI struct {
|
||||
Query, Site, Lang, Fresh, Category, Engines string
|
||||
Limit int
|
||||
JSONOut, Refresh, Force bool
|
||||
TTL float64
|
||||
Timeout int
|
||||
}
|
||||
|
||||
func NewCLI() CLI {
|
||||
return CLI{Limit: DefaultLimit, TTL: float64(CacheTTL), Timeout: 25}
|
||||
}
|
||||
|
||||
func Parser() *flaggy.Parser {
|
||||
c := NewCLI()
|
||||
return Bind(&c)
|
||||
}
|
||||
|
||||
func Bind(c *CLI) *flaggy.Parser {
|
||||
p := cli.New("web-search")
|
||||
p.Description = "SearXNG second source (throttled ≠ absence)"
|
||||
p.Bool(&c.JSONOut, "", "json", "JSON output")
|
||||
p.Bool(&c.Refresh, "", "refresh", "bypass cache")
|
||||
p.Bool(&c.Force, "", "force", "allow PII in query")
|
||||
p.Int(&c.Limit, "n", "limit", "max hits")
|
||||
p.String(&c.Site, "", "site", "restrict to host")
|
||||
p.String(&c.Lang, "", "lang", "language")
|
||||
p.String(&c.Fresh, "", "fresh", "day|week|month|year")
|
||||
p.String(&c.Category, "", "category", "searx category")
|
||||
p.String(&c.Engines, "", "engines", "engine list")
|
||||
p.Float64(&c.TTL, "", "ttl", "cache ttl seconds")
|
||||
p.Int(&c.Timeout, "", "timeout", "http timeout seconds")
|
||||
return p
|
||||
}
|
||||
|
||||
func ParseArgs(args []string) (CLI, error) {
|
||||
c := NewCLI()
|
||||
p := Bind(&c)
|
||||
var q string
|
||||
p.AddPositionalValue(&q, "query", 1, false, "search query")
|
||||
if err := cli.Parse(p, args); err != nil {
|
||||
return c, err
|
||||
}
|
||||
c.Query = cli.Query(q, p.TrailingArguments)
|
||||
if c.Query == "" {
|
||||
return c, fmt.Errorf("query required")
|
||||
}
|
||||
if c.Limit < 0 {
|
||||
return c, fmt.Errorf("--limit must be a non-negative integer")
|
||||
}
|
||||
if c.Timeout <= 0 {
|
||||
return c, fmt.Errorf("--timeout must be a positive integer")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
Reference in New Issue
Block a user