From e83810bf6f5c614087c3c51145b53e660c117c20 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:43:59 +0000 Subject: [PATCH 1/2] Initial plan From c1f80da29d025ad332240f8973fe2d0e73c4092e Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:49:28 +0000 Subject: [PATCH 2/2] Optimize CLI implementation using Cobra framework - Migrate from manual flag parsing to Cobra CLI framework - Add professional help system with examples - Implement verbose mode flag for detailed logging - Enhance error messages with emoji indicators - Improve Makefile with better help and targets - Update README with comprehensive CLI documentation - Add CHANGELOG documenting improvements Inspired by official longbridge-terminal best practices Co-authored-by: JetSquirrel <20291255+JetSquirrel@users.noreply.github.com> Agent-Logs-Url: https://github.com/JetSquirrel/longbridge-fs/sessions/21d82b82-8305-44e4-b54b-9ec94b57cc13 --- CHANGELOG.md | 34 +++++ Makefile | 58 +++++++-- README.md | 49 +++++++- cmd/longbridge-fs/main.go | 255 +++++++++++++++++++++++++------------- go.mod | 3 + go.sum | 9 ++ 6 files changed, 305 insertions(+), 103 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..df09e76 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +### Added +- **Professional CLI Framework**: Migrated from manual flag parsing to [Cobra](https://github.com/spf13/cobra), following best practices from [longbridge-terminal](https://github.com/longbridge/longbridge-terminal) +- **Improved Help System**: Rich help text with examples for all commands +- **Verbose Mode**: Global `--verbose` flag for detailed logging during operations +- **Shell Completion**: Auto-generated completion scripts for bash, zsh, fish, and PowerShell +- **Better Error Messages**: User-friendly error messages with emoji indicators (✓, ⚠, ❌, 🚀, 🛑, 🔧) +- **Enhanced Makefile**: + - Default `make` now shows help (instead of building) + - New `dev-verbose` target for mock mode with verbose output + - Improved help with examples and version information + +### Changed +- **Command Structure**: + - `longbridge-fs --version` instead of `longbridge-fs version` + - Consistent flag naming across all commands + - Better organized command hierarchy +- **Controller Logging**: Enhanced with emoji indicators for better readability +- **Init Command**: Now provides verbose output showing each created file/directory + +### Improved +- CLI follows modern command-line interface conventions +- Better integration with AI agents and scripting tools +- More informative progress indicators during operations +- Professional output formatting matching industry standards + +## Inspiration + +These improvements were inspired by the official [Longbridge Terminal CLI](https://github.com/longbridge/longbridge-terminal), which demonstrates best practices for building AI-native command-line tools for financial trading platforms. diff --git a/Makefile b/Makefile index 6a48df0..45c2832 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build clean test run install +.PHONY: build clean test run install init help # Go parameters GOCMD=go @@ -23,6 +23,9 @@ VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S') LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)" +# Default target +.DEFAULT_GOAL := help + all: clean deps build build: @@ -45,8 +48,13 @@ deps: test: $(GOTEST) -v ./... +# Initialize file system (creates default directories) +init: build + @echo "Initializing file system..." + $(BUILD_DIR)/$(BINARY_NAME) init --root ./fs + run: build - @echo "Running controller..." + @echo "Running controller with real API..." $(BUILD_DIR)/$(BINARY_NAME) controller --root ./fs --credential ./configs/credential install: build @@ -59,8 +67,14 @@ uninstall: # Development helpers dev: build + @echo "Running controller in mock mode..." $(BUILD_DIR)/$(BINARY_NAME) controller --root ./fs --mock +# Run with verbose output +dev-verbose: build + @echo "Running controller in mock mode with verbose output..." + $(BUILD_DIR)/$(BINARY_NAME) controller --root ./fs --mock --verbose + fmt: @echo "Formatting code..." $(GOCMD) fmt ./... @@ -78,16 +92,34 @@ docker-run: # Help help: + @echo "Longbridge FS - File system-based stock trading framework" + @echo "" @echo "Usage: make [target]" @echo "" - @echo "Targets:" - @echo " build Build the binary" - @echo " clean Clean build artifacts" - @echo " deps Download dependencies" - @echo " test Run tests" - @echo " run Run the controller" - @echo " dev Run in mock mode (no API)" - @echo " install Install to /usr/local/bin" - @echo " fmt Format code" - @echo " lint Lint code" - @echo " help Show this help" + @echo "Available targets:" + @echo " build Build the binary" + @echo " clean Clean build artifacts" + @echo " deps Download and tidy dependencies" + @echo " test Run tests" + @echo " init Initialize FS directory structure" + @echo " run Run controller with real API" + @echo " dev Run controller in mock mode (no API)" + @echo " dev-verbose Run controller in mock mode with verbose output" + @echo " install Install to /usr/local/bin" + @echo " uninstall Remove from /usr/local/bin" + @echo " fmt Format code" + @echo " lint Lint code (requires golangci-lint)" + @echo " help Show this help message" + @echo "" + @echo "Docker targets:" + @echo " docker-build Build Docker image" + @echo " docker-run Run Docker container" + @echo "" + @echo "Examples:" + @echo " make build # Build the binary" + @echo " make init # Initialize file system" + @echo " make dev # Run in mock mode" + @echo " make run # Run with real API" + @echo "" + @echo "Version: $(VERSION)" + @echo "Build time: $(BUILD_TIME)" diff --git a/README.md b/README.md index f21b71c..bcd8f7f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Longbridge FS 是一个基于文件系统的股票交易框架,通过读写文 - **文件驱动** — 通过文件读写完成所有交易操作,AI Agent 无需学习 API - **AI 友好** — JSON 输出,天然适配 AI Agent 的文件操作能力 +- **专业 CLI** — 使用 Cobra 框架构建,提供现代化的命令行体验(参考 [longbridge-terminal](https://github.com/longbridge/longbridge-terminal) 官方实践) - **实时行情** — 支持 WebSocket 订阅,高效获取实时行情推送 - **审计追踪** — 所有交易记录在 beancount 格式的 append-only 账本中 - **盈亏追踪** — 自动生成 `pnl.json` 和 `portfolio.json`,实时追踪持仓盈亏 @@ -107,18 +108,62 @@ make run ### CLI 参数 ``` -Usage: longbridge-fs [options] +Usage: longbridge-fs [command] [flags] Commands: init 初始化 FS 目录结构 controller 启动交易控制器守护进程 + help 获取任何命令的帮助 + completion 生成 shell 自动补全脚本 -Options for controller: +Global Flags: + -v, --verbose 启用详细输出 + --version 显示版本信息 + +查看子命令帮助: + longbridge-fs [command] --help +``` + +#### init 命令 + +``` +初始化 FS 目录结构,创建必要的目录和默认配置文件 + +Flags: + --root PATH FS 根目录 (default: .) + -h, --help 显示帮助信息 +``` + +#### controller 命令 + +``` +启动交易控制器守护进程 + +功能: + - 处理 beancount.txt 中的新订单 + - 刷新账户状态和持仓 + - 通过 WebSocket 更新实时行情 + - 生成 PnL 和组合报告 + - 执行风控规则(止损/止盈) + - 压缩历史账本到区块文件 + +Flags: --root PATH FS 根目录 (default: .) --interval DURATION 轮询间隔 (default: 2s) --credential FILE 凭证文件路径 (default: credential) --mock Mock 模式,不调用 API (default: false) --compact-after N 每 N 笔执行后压缩归档, 0=禁用 (default: 10) + -h, --help 显示帮助信息 + +示例: + # 真实 API 模式 + longbridge-fs controller --root ./fs --credential ./configs/credential + + # Mock 模式(无需 API) + longbridge-fs controller --root ./fs --mock + + # 自定义轮询间隔 + longbridge-fs controller --root ./fs --interval 5s --verbose ``` ## 使用示例 diff --git a/cmd/longbridge-fs/main.go b/cmd/longbridge-fs/main.go index b4986d4..3c339ec 100644 --- a/cmd/longbridge-fs/main.go +++ b/cmd/longbridge-fs/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "flag" "fmt" "log" "os" @@ -20,6 +19,7 @@ import ( "github.com/longportapp/openapi-go/quote" "github.com/longportapp/openapi-go/trade" + "github.com/spf13/cobra" ) // Set by ldflags at build time @@ -28,99 +28,160 @@ var ( BuildTime = "unknown" ) +// Global flags +var ( + rootDir string + verbose bool + outputFormat string +) + func main() { log.SetFlags(log.Ltime) - if len(os.Args) < 2 { - printUsage() - os.Exit(1) + rootCmd := &cobra.Command{ + Use: "longbridge-fs", + Short: "AI-driven file system interface for HK/US stock trading", + Long: `Longbridge FS - File system-based stock trading framework + +Turn stock trading into file operations, making it naturally compatible +with AI agents' file manipulation capabilities.`, + Version: fmt.Sprintf("%s (built %s)", Version, BuildTime), } - switch os.Args[1] { - case "init": - cmdInit() - case "controller": - cmdController() - case "version": - fmt.Printf("longbridge-fs %s (built %s)\n", Version, BuildTime) - default: - printUsage() + // Global flags + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") + + // Add subcommands + rootCmd.AddCommand(initCmd()) + rootCmd.AddCommand(controllerCmd()) + + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } -func printUsage() { - fmt.Fprintf(os.Stderr, `Usage: %s [options] +// initCmd creates the init subcommand +func initCmd() *cobra.Command { + var root string -Commands: - init Initialize the FS directory structure - controller Start the trade controller daemon - version Print version information + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize the FS directory structure", + Long: `Initialize directory structure for longbridge-fs -Options for init: - --root PATH FS root directory (default: .) +Creates the required directories and default configuration files +for the file system-based trading framework.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runInit(root) + }, + } -Options for controller: - --root PATH FS root directory (default: .) - --interval DURATION Poll interval (default: 2s) - --credential FILE Credential file path (default: credential) - --mock Use mock execution without API (default: false) - --compact-after N Compact after N executed orders, 0=disable (default: 10) -`, os.Args[0]) + cmd.Flags().StringVar(&root, "root", ".", "FS root directory") + return cmd } -// cmdInit creates the FS directory structure. -func cmdInit() { - fs := flag.NewFlagSet("init", flag.ExitOnError) - root := fs.String("root", ".", "FS root directory") - fs.Parse(os.Args[2:]) - +func runInit(root string) error { dirs := []string{ - filepath.Join(*root, "account"), - filepath.Join(*root, "trade", "blocks"), - filepath.Join(*root, "quote", "hold"), - filepath.Join(*root, "quote", "track"), - filepath.Join(*root, "quote", "subscribe"), - filepath.Join(*root, "quote", "unsubscribe"), - filepath.Join(*root, "quote", "market"), + filepath.Join(root, "account"), + filepath.Join(root, "trade", "blocks"), + filepath.Join(root, "quote", "hold"), + filepath.Join(root, "quote", "track"), + filepath.Join(root, "quote", "subscribe"), + filepath.Join(root, "quote", "unsubscribe"), + filepath.Join(root, "quote", "market"), } for _, d := range dirs { if err := os.MkdirAll(d, 0755); err != nil { - log.Fatalf("mkdir %s: %v", d, err) + return fmt.Errorf("failed to create directory %s: %w", d, err) + } + if verbose { + log.Printf("created directory: %s", d) } } // Default beancount ledger - bcPath := filepath.Join(*root, "trade", "beancount.txt") + bcPath := filepath.Join(root, "trade", "beancount.txt") if _, err := os.Stat(bcPath); os.IsNotExist(err) { - os.WriteFile(bcPath, []byte("; beancount append-only trade ledger\n"), 0644) + if err := os.WriteFile(bcPath, []byte("; beancount append-only trade ledger\n"), 0644); err != nil { + return fmt.Errorf("failed to create beancount ledger: %w", err) + } + if verbose { + log.Printf("created file: %s", bcPath) + } } // Default account state - statePath := filepath.Join(*root, "account", "state.json") + statePath := filepath.Join(root, "account", "state.json") if _, err := os.Stat(statePath); os.IsNotExist(err) { - os.WriteFile(statePath, []byte(`{"updated_at":"","cash":[],"positions":[],"orders":[]}`+"\n"), 0644) + if err := os.WriteFile(statePath, []byte(`{"updated_at":"","cash":[],"positions":[],"orders":[]}`+"\n"), 0644); err != nil { + return fmt.Errorf("failed to create account state: %w", err) + } + if verbose { + log.Printf("created file: %s", statePath) + } } // Default risk control config - rcPath := filepath.Join(*root, "trade", "risk_control.json") + rcPath := filepath.Join(root, "trade", "risk_control.json") if _, err := os.Stat(rcPath); os.IsNotExist(err) { - os.WriteFile(rcPath, []byte("{}\n"), 0644) + if err := os.WriteFile(rcPath, []byte("{}\n"), 0644); err != nil { + return fmt.Errorf("failed to create risk control config: %w", err) + } + if verbose { + log.Printf("created file: %s", rcPath) + } } - log.Printf("initialized FS at %s", *root) + log.Printf("✓ Successfully initialized FS at %s", root) + return nil } -// cmdController starts the trade controller daemon. -func cmdController() { - fs := flag.NewFlagSet("controller", flag.ExitOnError) - root := fs.String("root", ".", "FS root directory") - interval := fs.Duration("interval", 2*time.Second, "Poll interval") - credFile := fs.String("credential", "credential", "Credential file path") - mock := fs.Bool("mock", false, "Use mock execution without API") - compactAfter := fs.Int("compact-after", 10, "Compact after N executed orders, 0=disable") - fs.Parse(os.Args[2:]) +// controllerCmd creates the controller subcommand +func controllerCmd() *cobra.Command { + var ( + root string + interval time.Duration + credFile string + mock bool + compactAfter int + ) + + cmd := &cobra.Command{ + Use: "controller", + Short: "Start the trade controller daemon", + Long: `Start the trade controller daemon + +The controller monitors the file system and automatically: + - Processes new orders from beancount.txt + - Refreshes account state and positions + - Updates real-time quotes via WebSocket subscriptions + - Generates PnL and portfolio reports + - Enforces risk control rules (stop-loss/take-profit) + - Compacts ledger history into blocks`, + Example: ` # Run with real API + longbridge-fs controller --root ./fs --credential ./configs/credential + + # Run in mock mode (no API calls) + longbridge-fs controller --root ./fs --mock + + # Custom polling interval + longbridge-fs controller --root ./fs --interval 5s`, + RunE: func(cmd *cobra.Command, args []string) error { + return runController(root, interval, credFile, mock, compactAfter) + }, + } + cmd.Flags().StringVar(&root, "root", ".", "FS root directory") + cmd.Flags().DurationVar(&interval, "interval", 2*time.Second, "Poll interval") + cmd.Flags().StringVar(&credFile, "credential", "credential", "Credential file path") + cmd.Flags().BoolVar(&mock, "mock", false, "Use mock execution without API") + cmd.Flags().IntVar(&compactAfter, "compact-after", 10, "Compact after N executed orders, 0=disable") + + return cmd +} + +func runController(root string, interval time.Duration, credFile string, mock bool, compactAfter int) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -136,18 +197,18 @@ func cmdController() { var tc *trade.TradeContext var qc *quote.QuoteContext var subManager *market.SubscriptionManager - useMock := *mock + useMock := mock if !useMock { - cfg, err := credential.Load(*credFile) + cfg, err := credential.Load(credFile) if err != nil { - log.Printf("credential load failed: %v (falling back to mock mode)", err) + log.Printf("⚠ Credential load failed: %v (falling back to mock mode)", err) useMock = true } else { // Initialize trade context tctx, err := trade.NewFromCfg(cfg) if err != nil { - log.Printf("trade context init failed: %v (falling back to mock mode)", err) + log.Printf("⚠ Trade context init failed: %v (falling back to mock mode)", err) useMock = true } else { tc = tctx @@ -156,93 +217,111 @@ func cmdController() { // Initialize quote context qctx, err := quote.NewFromCfg(cfg) if err != nil { - log.Printf("quote context init failed: %v (quote disabled)", err) + log.Printf("⚠ Quote context init failed: %v (quote disabled)", err) } else { qc = qctx } if tc != nil { - log.Println("connected to Longbridge API") + log.Println("✓ Connected to Longbridge API") } } } if useMock { - log.Println("running in MOCK mode (no API calls)") + log.Println("🔧 Running in MOCK mode (no API calls)") } // Initialize subscription manager - subManager = market.NewSubscriptionManager(qc, *root) + subManager = market.NewSubscriptionManager(qc, root) if qc != nil { - log.Println("WebSocket subscription manager initialized") + log.Println("✓ WebSocket subscription manager initialized") + } + + if verbose { + log.Printf("Controller configuration:") + log.Printf(" Root: %s", root) + log.Printf(" Interval: %s", interval) + log.Printf(" Compact after: %d orders", compactAfter) + log.Printf(" Mock mode: %v", useMock) } - log.Printf("controller started: root=%s interval=%s compact-after=%d", *root, *interval, *compactAfter) + log.Printf("🚀 Controller started (interval=%s, compact-after=%d)", interval, compactAfter) executedCount := 0 - ticker := time.NewTicker(*interval) + ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): - return + log.Println("✓ Controller stopped gracefully") + return nil case <-ticker.C: // Kill switch - killPath := filepath.Join(*root, ".kill") + killPath := filepath.Join(root, ".kill") if _, err := os.Stat(killPath); err == nil { - log.Println("kill switch activated, shutting down...") + log.Println("🛑 Kill switch activated, shutting down...") os.Remove(killPath) cancel() - return + return nil } // Process trade ledger - n, err := broker.ProcessLedger(ctx, tc, *root, useMock) + n, err := broker.ProcessLedger(ctx, tc, root, useMock) if err != nil { - log.Printf("process failed: %v", err) + log.Printf("❌ Order processing failed: %v", err) + } else if n > 0 && verbose { + log.Printf("✓ Processed %d order(s)", n) } executedCount += n // Refresh account state (only with real API) if tc != nil { - if err := account.RefreshState(ctx, tc, *root); err != nil { - log.Printf("account refresh failed: %v", err) + if err := account.RefreshState(ctx, tc, root); err != nil { + log.Printf("❌ Account refresh failed: %v", err) + } else if verbose { + log.Printf("✓ Account state refreshed") } } // Process WebSocket subscription requests (subscribe/unsubscribe) if subManager != nil { if err := subManager.ProcessSubscriptions(ctx); err != nil { - log.Printf("subscription processing failed: %v", err) + log.Printf("❌ Subscription processing failed: %v", err) } } // Refresh quotes via track files (one-shot poll-based) if qc != nil { - market.RefreshQuotes(ctx, qc, *root) + market.RefreshQuotes(ctx, qc, root) } // Generate PnL report (positions + current prices — file-only, works in mock) - if err := account.GeneratePnL(*root); err != nil { - log.Printf("pnl generation failed: %v", err) + if err := account.GeneratePnL(root); err != nil { + log.Printf("❌ PnL generation failed: %v", err) + } else if verbose { + log.Printf("✓ PnL report generated") } // Generate portfolio summary (all hold quotes + positions) - if err := account.GeneratePortfolio(*root); err != nil { - log.Printf("portfolio generation failed: %v", err) + if err := account.GeneratePortfolio(root); err != nil { + log.Printf("❌ Portfolio generation failed: %v", err) + } else if verbose { + log.Printf("✓ Portfolio summary generated") } // Risk control: stop-loss / take-profit - if err := risk.CheckRiskRules(*root); err != nil { - log.Printf("risk check failed: %v", err) + if err := risk.CheckRiskRules(root); err != nil { + log.Printf("❌ Risk check failed: %v", err) } // Compaction - if *compactAfter > 0 && executedCount >= *compactAfter { - if err := ledger.CompactBlocks(*root, executedCount); err != nil { - log.Printf("compact failed: %v", err) + if compactAfter > 0 && executedCount >= compactAfter { + if err := ledger.CompactBlocks(root, executedCount); err != nil { + log.Printf("❌ Compaction failed: %v", err) } else { + log.Printf("✓ Compacted %d executed orders into blocks", executedCount) executedCount = 0 } } diff --git a/go.mod b/go.mod index e3a3c3e..7b9ccc3 100644 --- a/go.mod +++ b/go.mod @@ -13,11 +13,14 @@ require ( github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/copier v0.3.5 // indirect github.com/joho/godotenv v1.4.0 // indirect github.com/longportapp/openapi-protobufs/gen/go v0.6.0 // indirect github.com/longportapp/openapi-protocol/go v0.4.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.9 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c6ea6a1..f301438 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d h1:wvStE9wLpws31NiWUx+38wny1msZ/tm+eL5xmm4Y7So= github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -16,6 +17,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= @@ -36,10 +39,16 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=