Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Binaries
sure-cli
/sure-cli
*.exe
*.exe~
*.dll
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,24 @@ sure-cli export transactions --months 12 --format csv --out transactions.csv
sure-cli status

# Holdings (requires Sure investment API)
sure-cli holdings list
sure-cli holdings performance --period 1m
sure-cli holdings list --account-id <account_id> --date 2026-05-01
sure-cli holdings show <holding_id>

# Securities and prices
sure-cli securities list --ticker AAPL
sure-cli securities show <security_id>
sure-cli security-prices list --security-id <security_id> --start-date 2026-01-01

# Trades (requires Sure investment API)
sure-cli trades list
sure-cli trades show <trade_id>
sure-cli trades create --account-id <account_id> --date 2026-05-01 --type buy --qty 1 --price 100 --security-id <security_id>
sure-cli trades create --account-id <account_id> --date 2026-05-01 --type buy --qty 1 --price 100 --security-id <security_id> --apply

# Recurring transactions
sure-cli recurring-transactions list --status active
sure-cli recurring-transactions create --name Rent --last-occurrence-date 2026-04-01 --next-expected-date 2026-05-01
sure-cli recurring-transactions create --name Rent --last-occurrence-date 2026-04-01 --next-expected-date 2026-05-01 --apply

# Sync
sure-cli sync
Expand Down
83 changes: 33 additions & 50 deletions cmd/sure-cli/root/holdings_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,60 @@ package root

import (
"fmt"
"net/url"

"github.com/we-promise/sure-cli/internal/api"
"github.com/we-promise/sure-cli/internal/output"
"github.com/spf13/cobra"
)

func newHoldingsCmd() *cobra.Command {
cmd := &cobra.Command{Use: "holdings", Short: "Investment holdings (requires Sure API support)"}
cmd.AddCommand(newHoldingsListCmd())
cmd.AddCommand(newHoldingsPerformanceCmd())
cmd.AddCommand(&cobra.Command{
Use: "show <id>",
Short: "Show investment holding",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
printInvestmentGet(fmt.Sprintf("/api/v1/holdings/%s", url.PathEscape(args[0])))
},
})
return cmd
}

func newHoldingsListCmd() *cobra.Command {
var page, perPage int
var accountID, date, startDate, endDate, securityID string
var accountIDs []string
cmd := &cobra.Command{
Use: "list",
Short: "List investment holdings",
Run: func(cmd *cobra.Command, args []string) {
client := api.New()

// Try the holdings endpoint (may not exist in all Sure versions)
var res any
r, err := client.Get("/api/v1/holdings", &res)
if err != nil {
output.Fail("request_failed", err.Error(), nil)
q := url.Values{}
addInvestmentPagingQuery(q, page, perPage)
if accountID != "" {
q.Set("account_id", accountID)
}

if r.StatusCode() == 404 {
output.Fail("not_implemented", "Holdings API not available. This requires Sure with investment account support.", map[string]any{
"endpoint": "/api/v1/holdings",
"hint": "Ensure your Sure instance supports investment accounts",
})
addRepeatedInvestmentQuery(q, "account_ids", accountIDs)
if date != "" {
q.Set("date", date)
}

_ = output.Print(format, output.Envelope{Data: res, Meta: &output.Meta{Status: r.StatusCode()}})
},
}
return cmd
}

func newHoldingsPerformanceCmd() *cobra.Command {
var period string

cmd := &cobra.Command{
Use: "performance",
Short: "Investment performance summary",
Run: func(cmd *cobra.Command, args []string) {
client := api.New()

// Try the performance endpoint
path := "/api/v1/holdings/performance"
if period != "" {
path = fmt.Sprintf("%s?period=%s", path, period)
if startDate != "" {
q.Set("start_date", startDate)
}

var res any
r, err := client.Get(path, &res)
if err != nil {
output.Fail("request_failed", err.Error(), nil)
if endDate != "" {
q.Set("end_date", endDate)
}

if r.StatusCode() == 404 {
output.Fail("not_implemented", "Holdings performance API not available.", map[string]any{
"endpoint": path,
"hint": "This feature requires Sure with investment tracking enabled",
})
if securityID != "" {
q.Set("security_id", securityID)
}

_ = output.Print(format, output.Envelope{Data: res, Meta: &output.Meta{Status: r.StatusCode()}})
printInvestmentGet(investmentPathWithQuery("/api/v1/holdings", q))
},
}
cmd.Flags().StringVar(&period, "period", "1m", "performance period (1w|1m|3m|6m|1y|ytd|all)")
addInvestmentPagingFlags(cmd, &page, &perPage)
cmd.Flags().StringVar(&accountID, "account-id", "", "account id")
cmd.Flags().StringSliceVar(&accountIDs, "account-ids", nil, "account ids (repeat or comma-separated)")
cmd.Flags().StringVar(&date, "date", "", "exact holding date (YYYY-MM-DD)")
cmd.Flags().StringVar(&startDate, "start-date", "", "start date (YYYY-MM-DD)")
cmd.Flags().StringVar(&endDate, "end-date", "", "end date (YYYY-MM-DD)")
cmd.Flags().StringVar(&securityID, "security-id", "", "security id")
return cmd
}
112 changes: 112 additions & 0 deletions cmd/sure-cli/root/investment_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package root

import (
"fmt"
"net/url"

"github.com/spf13/cobra"
"github.com/we-promise/sure-cli/internal/api"
"github.com/we-promise/sure-cli/internal/output"
)

func addInvestmentPagingFlags(cmd *cobra.Command, page, perPage *int) {
cmd.Flags().IntVar(page, "page", 1, "page number")
cmd.Flags().IntVar(perPage, "per-page", 25, "items per page (maps to per_page)")
}

func addInvestmentPagingQuery(q url.Values, page, perPage int) {
if page > 0 {
q.Set("page", fmt.Sprintf("%d", page))
}
if perPage > 0 {
q.Set("per_page", fmt.Sprintf("%d", perPage))
}
}

func addRepeatedInvestmentQuery(q url.Values, key string, values []string) {
for _, v := range values {
if v != "" {
q.Add(key+"[]", v)
}
}
}

func investmentPathWithQuery(path string, q url.Values) string {
if encoded := q.Encode(); encoded != "" {
return path + "?" + encoded
}
return path
}

func printInvestmentGet(path string) {
client := api.New()
var res any
r, err := client.Get(path, &res)
if err != nil {
output.Fail("request_failed", err.Error(), nil)
return
}
if err := output.Print(format, output.Envelope{Data: res, Meta: &output.Meta{Status: r.StatusCode()}}); err != nil {
output.Fail("output_failed", err.Error(), nil)
return
}
}

func printInvestmentPost(path string, body any) {
client := api.New()
var res any
r, err := client.Post(path, body, &res)
if err != nil {
output.Fail("request_failed", err.Error(), nil)
return
}
if err := output.Print(format, output.Envelope{Data: res, Meta: &output.Meta{Status: r.StatusCode()}}); err != nil {
output.Fail("output_failed", err.Error(), nil)
return
}
}

func printInvestmentPatch(path string, body any) {
client := api.New()
var res any
r, err := client.Patch(path, body, &res)
if err != nil {
output.Fail("request_failed", err.Error(), nil)
return
}
if err := output.Print(format, output.Envelope{Data: res, Meta: &output.Meta{Status: r.StatusCode()}}); err != nil {
output.Fail("output_failed", err.Error(), nil)
return
}
}

func printInvestmentDelete(path string) {
client := api.New()
var res any
r, err := client.Delete(path, &res)
if err != nil {
output.Fail("request_failed", err.Error(), nil)
return
}
if err := output.Print(format, output.Envelope{Data: res, Meta: &output.Meta{Status: r.StatusCode()}}); err != nil {
output.Fail("output_failed", err.Error(), nil)
return
}
}

func printInvestmentDryRun(method, path string, body any) {
request := map[string]any{
"method": method,
"path": path,
}
if body != nil {
request["body"] = body
}
if err := output.Print(format, output.Envelope{Data: map[string]any{
"dry_run": true,
"request": request,
}}); err != nil {
output.Fail("output_failed", err.Error(), nil)
return
}
}
Loading
Loading