Files
go-garth/cmd/garth/health.go
sstent c1993ba022 fix: resolve build errors and implement missing health data types
- Fix various build errors in the CLI application.
- Implement missing health data types (VO2 Max, Heart Rate Zones).
- Corrected `tablewriter` usage from `SetHeader` to `Header`.
- Removed unused imports and fixed syntax errors.
2025-09-19 05:19:02 -07:00

802 lines
22 KiB
Go

package main
import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"strconv"
"time"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"github.com/spf13/viper"
types "go-garth/internal/types"
"go-garth/internal/utils"
"go-garth/pkg/garmin"
)
var (
healthCmd = &cobra.Command{
Use: "health",
Short: "Manage Garmin Connect health data",
Long: `Provides commands to fetch various health metrics like sleep, HRV, stress, and body battery.`,
}
sleepCmd = &cobra.Command{
Use: "sleep",
Short: "Get sleep data",
Long: `Fetch sleep data for a specified date range.`,
RunE: runSleep,
}
hrvCmd = &cobra.Command{
Use: "hrv",
Short: "Get HRV data",
Long: `Fetch Heart Rate Variability (HRV) data.`,
RunE: runHrv,
}
stressCmd = &cobra.Command{
Use: "stress",
Short: "Get stress data",
Long: `Fetch stress data.`,
RunE: runStress,
}
bodyBatteryCmd = &cobra.Command{
Use: "bodybattery",
Short: "Get Body Battery data",
Long: `Fetch Body Battery data.`,
RunE: runBodyBattery,
}
vo2maxCmd = &cobra.Command{
Use: "vo2max",
Short: "Get VO2 Max data",
Long: `Fetch VO2 Max data for a specified date range.`,
RunE: runVO2Max,
}
hrZonesCmd = &cobra.Command{
Use: "hr-zones",
Short: "Get Heart Rate Zones data",
Long: `Fetch Heart Rate Zones data.`,
RunE: runHRZones,
}
healthDateFrom string
healthDateTo string
healthDays int
healthWeek bool
healthYesterday bool
healthAggregate string
)
func init() {
rootCmd.AddCommand(healthCmd)
healthCmd.AddCommand(sleepCmd)
sleepCmd.Flags().StringVar(&healthDateFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
sleepCmd.Flags().StringVar(&healthDateTo, "to", "", "End date for data fetching (YYYY-MM-DD)")
sleepCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
healthCmd.AddCommand(hrvCmd)
hrvCmd.Flags().IntVar(&healthDays, "days", 0, "Number of past days to fetch data for")
hrvCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
healthCmd.AddCommand(stressCmd)
stressCmd.Flags().BoolVar(&healthWeek, "week", false, "Fetch data for the current week")
stressCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
healthCmd.AddCommand(bodyBatteryCmd)
bodyBatteryCmd.Flags().BoolVar(&healthYesterday, "yesterday", false, "Fetch data for yesterday")
bodyBatteryCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
// VO2 Max Command
vo2maxCmd = &cobra.Command{
Use: "vo2max",
Short: "Get VO2 Max data",
Long: `Fetch VO2 Max data for a specified date range.`,
RunE: runVO2Max,
}
healthCmd.AddCommand(vo2maxCmd)
vo2maxCmd.Flags().StringVar(&healthDateFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
vo2maxCmd.Flags().StringVar(&healthDateTo, "to", "", "End date for data fetching (YYYY-MM-DD)")
vo2maxCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
// Heart Rate Zones Command
hrZonesCmd = &cobra.Command{
Use: "hr-zones",
Short: "Get Heart Rate Zones data",
Long: `Fetch Heart Rate Zones data.`,
RunE: runHRZones,
}
healthCmd.AddCommand(hrZonesCmd)
// Wellness Command
wellnessCmd = &cobra.Command{
Use: "wellness",
Short: "Get comprehensive wellness data",
Long: `Fetch comprehensive wellness data including body composition and resting heart rate trends.`,
RunE: runWellness,
}
healthCmd.AddCommand(wellnessCmd)
wellnessCmd.Flags().StringVar(&healthDateFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
wellnessCmd.Flags().StringVar(&healthDateTo, "to", "", "End date for data fetching (YYYY-MM-DD)")
wellnessCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
}
func runSleep(cmd *cobra.Command, args []string) error {
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
sessionFile := "garmin_session.json" // TODO: Make session file configurable
if err := garminClient.LoadSession(sessionFile); err != nil {
return fmt.Errorf("not logged in: %w", err)
}
var startDate, endDate time.Time
if healthDateFrom != "" {
startDate, err = time.Parse("2006-01-02", healthDateFrom)
if err != nil {
return fmt.Errorf("invalid date format for --from: %w", err)
}
} else {
startDate = time.Now().AddDate(0, 0, -7) // Default to last 7 days
}
if healthDateTo != "" {
endDate, err = time.Parse("2006-01-02", healthDateTo)
if err != nil {
return fmt.Errorf("invalid date format for --to: %w", err)
}
} else {
endDate = time.Now() // Default to today
}
sleepData, err := garminClient.GetSleepData(startDate, endDate)
if err != nil {
return fmt.Errorf("failed to get sleep data: %w", err)
}
if len(sleepData) == 0 {
fmt.Println("No sleep data found.")
return nil
}
// Apply aggregation if requested
if healthAggregate != "" {
aggregatedSleep := make(map[string]struct {
TotalSleepSeconds int
SleepScore int
Count int
})
for _, data := range sleepData {
key := ""
switch healthAggregate {
case "day":
key = data.Date.Format("2006-01-02")
case "week":
year, week := data.Date.ISOWeek()
key = fmt.Sprintf("%d-W%02d", year, week)
case "month":
key = data.Date.Format("2006-01")
case "year":
key = data.Date.Format("2006")
default:
return fmt.Errorf("unsupported aggregation period: %s", healthAggregate)
}
entry := aggregatedSleep[key]
entry.TotalSleepSeconds += data.TotalSleepSeconds
entry.SleepScore += data.SleepScore
entry.Count++
aggregatedSleep[key] = entry
}
// Convert aggregated data back to a slice for output
sleepData = []types.SleepData{}
for key, entry := range aggregatedSleep {
sleepData = append(sleepData, types.SleepData{
Date: utils.ParseAggregationKey(key, healthAggregate),
TotalSleepSeconds: entry.TotalSleepSeconds / entry.Count,
SleepScore: entry.SleepScore / entry.Count,
DeepSleepSeconds: 0,
LightSleepSeconds: 0,
RemSleepSeconds: 0,
AwakeSleepSeconds: 0,
})
}
}
outputFormat := viper.GetString("output")
switch outputFormat {
case "json":
data, err := json.MarshalIndent(sleepData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal sleep data to JSON: %w", err)
}
fmt.Println(string(data))
case "csv":
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
writer.Write([]string{"Date", "SleepScore", "TotalSleepSeconds", "DeepSleepSeconds", "LightSleepSeconds", "RemSleepSeconds", "AwakeSleepSeconds"})
for _, data := range sleepData {
writer.Write([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%d", data.SleepScore),
fmt.Sprintf("%d", data.TotalSleepSeconds),
fmt.Sprintf("%d", data.DeepSleepSeconds),
fmt.Sprintf("%d", data.LightSleepSeconds),
fmt.Sprintf("%d", data.RemSleepSeconds),
fmt.Sprintf("%d", data.AwakeSleepSeconds),
})
}
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"Date", "Score", "Total Sleep", "Deep", "Light", "REM", "Awake"})
for _, data := range sleepData {
table.Append([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%d", data.SleepScore),
(time.Duration(data.TotalSleepSeconds) * time.Second).String(),
(time.Duration(data.DeepSleepSeconds) * time.Second).String(),
(time.Duration(data.LightSleepSeconds) * time.Second).String(),
(time.Duration(data.RemSleepSeconds) * time.Second).String(),
(time.Duration(data.AwakeSleepSeconds) * time.Second).String(),
})
}
table.Render()
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
}
return nil
}
func runHrv(cmd *cobra.Command, args []string) error {
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
sessionFile := "garmin_session.json" // TODO: Make session file configurable
if err := garminClient.LoadSession(sessionFile); err != nil {
return fmt.Errorf("not logged in: %w", err)
}
days := healthDays
if days == 0 {
days = 7 // Default to 7 days if not specified
}
hrvData, err := garminClient.GetHrvData(days)
if err != nil {
return fmt.Errorf("failed to get HRV data: %w", err)
}
if len(hrvData) == 0 {
fmt.Println("No HRV data found.")
return nil
}
// Apply aggregation if requested
if healthAggregate != "" {
aggregatedHrv := make(map[string]struct {
HrvValue float64
Count int
})
for _, data := range hrvData {
key := ""
switch healthAggregate {
case "day":
key = data.Date.Format("2006-01-02")
case "week":
year, week := data.Date.ISOWeek()
key = fmt.Sprintf("%d-W%02d", year, week)
case "month":
key = data.Date.Format("2006-01")
case "year":
key = data.Date.Format("2006")
default:
return fmt.Errorf("unsupported aggregation period: %s", healthAggregate)
}
entry := aggregatedHrv[key]
entry.HrvValue += data.HrvValue
entry.Count++
aggregatedHrv[key] = entry
}
// Convert aggregated data back to a slice for output
hrvData = []types.HrvData{}
for key, entry := range aggregatedHrv {
hrvData = append(hrvData, types.HrvData{
Date: utils.ParseAggregationKey(key, healthAggregate),
HrvValue: entry.HrvValue / float64(entry.Count),
})
}
}
outputFormat := viper.GetString("output")
switch outputFormat {
case "json":
data, err := json.MarshalIndent(hrvData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal HRV data to JSON: %w", err)
}
fmt.Println(string(data))
case "csv":
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
writer.Write([]string{"Date", "HRV Value"})
for _, data := range hrvData {
writer.Write([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%.2f", data.HrvValue),
})
}
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"Date", "HRV Value"})
for _, data := range hrvData {
table.Append([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%.2f", data.HrvValue),
})
}
table.Render()
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
}
return nil
}
func runStress(cmd *cobra.Command, args []string) error {
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
sessionFile := "garmin_session.json" // TODO: Make session file configurable
if err := garminClient.LoadSession(sessionFile); err != nil {
return fmt.Errorf("not logged in: %w", err)
}
var startDate, endDate time.Time
if healthWeek {
now := time.Now()
weekday := now.Weekday()
// Calculate the start of the current week (Sunday)
startDate = now.AddDate(0, 0, -int(weekday))
endDate = startDate.AddDate(0, 0, 6) // End of the current week (Saturday)
} else {
// Default to today if no specific range or week is given
startDate = time.Now()
endDate = time.Now()
}
stressData, err := garminClient.GetStressData(startDate, endDate)
if err != nil {
return fmt.Errorf("failed to get stress data: %w", err)
}
if len(stressData) == 0 {
fmt.Println("No stress data found.")
return nil
}
// Apply aggregation if requested
if healthAggregate != "" {
aggregatedStress := make(map[string]struct {
StressLevel int
RestStressLevel int
Count int
})
for _, data := range stressData {
key := ""
switch healthAggregate {
case "day":
key = data.Date.Format("2006-01-02")
case "week":
year, week := data.Date.ISOWeek()
key = fmt.Sprintf("%d-W%02d", year, week)
case "month":
key = data.Date.Format("2006-01")
case "year":
key = data.Date.Format("2006")
default:
return fmt.Errorf("unsupported aggregation period: %s", healthAggregate)
}
entry := aggregatedStress[key]
entry.StressLevel += data.StressLevel
entry.RestStressLevel += data.RestStressLevel
entry.Count++
aggregatedStress[key] = entry
}
// Convert aggregated data back to a slice for output
stressData = []types.StressData{}
for key, entry := range aggregatedStress {
stressData = append(stressData, types.StressData{
Date: utils.ParseAggregationKey(key, healthAggregate),
StressLevel: entry.StressLevel / entry.Count,
RestStressLevel: entry.RestStressLevel / entry.Count,
})
}
}
outputFormat := viper.GetString("output")
switch outputFormat {
case "json":
data, err := json.MarshalIndent(stressData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal stress data to JSON: %w", err)
}
fmt.Println(string(data))
case "csv":
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
writer.Write([]string{"Date", "StressLevel", "RestStressLevel"})
for _, data := range stressData {
writer.Write([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%d", data.StressLevel),
fmt.Sprintf("%d", data.RestStressLevel),
})
}
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"Date", "Stress Level", "Rest Stress Level"})
for _, data := range stressData {
table.Append([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%d", data.StressLevel),
fmt.Sprintf("%d", data.RestStressLevel),
})
}
table.Render()
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
}
return nil
}
func runBodyBattery(cmd *cobra.Command, args []string) error {
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
sessionFile := "garmin_session.json" // TODO: Make session file configurable
if err := garminClient.LoadSession(sessionFile); err != nil {
return fmt.Errorf("not logged in: %w", err)
}
var startDate, endDate time.Time
if healthYesterday {
startDate = time.Now().AddDate(0, 0, -1)
endDate = startDate
} else {
// Default to today if no specific range or yesterday is given
startDate = time.Now()
endDate = time.Now()
}
bodyBatteryData, err := garminClient.GetBodyBatteryData(startDate, endDate)
if err != nil {
return fmt.Errorf("failed to get Body Battery data: %w", err)
}
if len(bodyBatteryData) == 0 {
fmt.Println("No Body Battery data found.")
return nil
}
// Apply aggregation if requested
if healthAggregate != "" {
aggregatedBodyBattery := make(map[string]struct {
BatteryLevel int
Charge int
Drain int
Count int
})
for _, data := range bodyBatteryData {
key := ""
switch healthAggregate {
case "day":
key = data.Date.Format("2006-01-02")
case "week":
year, week := data.Date.ISOWeek()
key = fmt.Sprintf("%d-W%02d", year, week)
case "month":
key = data.Date.Format("2006-01")
case "year":
key = data.Date.Format("2006")
default:
return fmt.Errorf("unsupported aggregation period: %s", healthAggregate)
}
entry := aggregatedBodyBattery[key]
entry.BatteryLevel += data.BatteryLevel
entry.Charge += data.Charge
entry.Drain += data.Drain
entry.Count++
aggregatedBodyBattery[key] = entry
}
// Convert aggregated data back to a slice for output
bodyBatteryData = []types.BodyBatteryData{}
for key, entry := range aggregatedBodyBattery {
bodyBatteryData = append(bodyBatteryData, types.BodyBatteryData{
Date: utils.ParseAggregationKey(key, healthAggregate),
BatteryLevel: entry.BatteryLevel / entry.Count,
Charge: entry.Charge / entry.Count,
Drain: entry.Drain / entry.Count,
})
}
}
outputFormat := viper.GetString("output")
switch outputFormat {
case "json":
data, err := json.MarshalIndent(bodyBatteryData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal Body Battery data to JSON: %w", err)
}
fmt.Println(string(data))
case "csv":
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
writer.Write([]string{"Date", "BatteryLevel", "Charge", "Drain"})
for _, data := range bodyBatteryData {
writer.Write([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%d", data.BatteryLevel),
fmt.Sprintf("%d", data.Charge),
fmt.Sprintf("%d", data.Drain),
})
}
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"Date", "Battery Level", "Charge", "Drain"})
for _, data := range bodyBatteryData {
table.Append([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%d", data.BatteryLevel),
fmt.Sprintf("%d", data.Charge),
fmt.Sprintf("%d", data.Drain),
})
}
table.Render()
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
}
return nil
}
func runVO2Max(cmd *cobra.Command, args []string) error {
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
sessionFile := "garmin_session.json" // TODO: Make session file configurable
if err := garminClient.LoadSession(sessionFile); err != nil {
return fmt.Errorf("not logged in: %w", err)
}
var startDate, endDate time.Time
if healthDateFrom != "" {
startDate, err = time.Parse("2006-01-02", healthDateFrom)
if err != nil {
return fmt.Errorf("invalid date format for --from: %w", err)
}
} else {
startDate = time.Now().AddDate(0, 0, -7) // Default to last 7 days
}
if healthDateTo != "" {
endDate = time.Now() // Default to today
parsedDate, err := time.Parse("2006-01-02", healthDateTo)
if err != nil {
return fmt.Errorf("invalid date format for --to: %w", err)
}
endDate = parsedDate
}
vo2maxData, err := garminClient.GetVO2MaxData(startDate, endDate)
if err != nil {
return fmt.Errorf("failed to get VO2 Max data: %w", err)
}
if len(vo2maxData) == 0 {
fmt.Println("No VO2 Max data found.")
return nil
}
// Apply aggregation if requested
if healthAggregate != "" {
aggregatedVO2Max := make(map[string]struct {
VO2MaxRunning float64
VO2MaxCycling float64
Count int
})
for _, data := range vo2maxData {
key := ""
switch healthAggregate {
case "day":
key = data.Date.Format("2006-01-02")
case "week":
year, week := data.Date.ISOWeek()
key = fmt.Sprintf("%d-W%02d", year, week)
case "month":
key = data.Date.Format("2006-01")
case "year":
key = data.Date.Format("2006")
default:
return fmt.Errorf("unsupported aggregation period: %s", healthAggregate)
}
entry := aggregatedVO2Max[key]
entry.VO2MaxRunning += data.VO2MaxRunning
entry.VO2MaxCycling += data.VO2MaxCycling
entry.Count++
aggregatedVO2Max[key] = entry
}
// Convert aggregated data back to a slice for output
vo2maxData = []types.VO2MaxData{}
for key, entry := range aggregatedVO2Max {
vo2maxData = append(vo2maxData, types.VO2MaxData{
Date: utils.ParseAggregationKey(key, healthAggregate),
VO2MaxRunning: entry.VO2MaxRunning / float64(entry.Count),
VO2MaxCycling: entry.VO2MaxCycling / float64(entry.Count),
})
}
}
outputFormat := viper.GetString("output")
switch outputFormat {
case "json":
data, err := json.MarshalIndent(vo2maxData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal VO2 Max data to JSON: %w", err)
}
fmt.Println(string(data))
case "csv":
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
writer.Write([]string{"Date", "VO2MaxRunning", "VO2MaxCycling"})
for _, data := range vo2maxData {
writer.Write([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%.2f", data.VO2MaxRunning),
fmt.Sprintf("%.2f", data.VO2MaxCycling),
})
}
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"Date", "VO2 Max Running", "VO2 Max Cycling"})
for _, data := range vo2maxData {
table.Append([]string{
data.Date.Format("2006-01-02"),
fmt.Sprintf("%.2f", data.VO2MaxRunning),
fmt.Sprintf("%.2f", data.VO2MaxCycling),
})
}
table.Render()
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
}
return nil
}
func runHRZones(cmd *cobra.Command, args []string) error {
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
sessionFile := "garmin_session.json" // TODO: Make session file configurable
if err := garminClient.LoadSession(sessionFile); err != nil {
return fmt.Errorf("not logged in: %w", err)
}
hrZonesData, err := garminClient.GetHeartRateZones()
if err != nil {
return fmt.Errorf("failed to get Heart Rate Zones data: %w", err)
}
if hrZonesData == nil {
fmt.Println("No Heart Rate Zones data found.")
return nil
}
outputFormat := viper.GetString("output")
switch outputFormat {
case "json":
data, err := json.MarshalIndent(hrZonesData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal Heart Rate Zones data to JSON: %w", err)
}
fmt.Println(string(data))
case "csv":
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
writer.Write([]string{"Zone", "MinBPM", "MaxBPM", "Name"})
for _, zone := range hrZonesData.Zones {
writer.Write([]string{
strconv.Itoa(zone.Zone),
strconv.Itoa(zone.MinBPM),
strconv.Itoa(zone.MaxBPM),
zone.Name,
})
}
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"Resting HR", "Max HR", "Lactate Threshold", "Updated At"})
table.Append([]string{
strconv.Itoa(hrZonesData.RestingHR),
strconv.Itoa(hrZonesData.MaxHR),
strconv.Itoa(hrZonesData.LactateThreshold),
hrZonesData.UpdatedAt.Format("2006-01-02 15:04:05"),
})
table.Render()
fmt.Println()
zonesTable := tablewriter.NewWriter(os.Stdout)
zonesTable.Header([]string{"Zone", "Min BPM", "Max BPM", "Name"})
for _, zone := range hrZonesData.Zones {
zonesTable.Append([]string{
strconv.Itoa(zone.Zone),
strconv.Itoa(zone.MinBPM),
strconv.Itoa(zone.MaxBPM),
zone.Name,
})
}
zonesTable.Render()
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
}
return nil
}
var wellnessCmd *cobra.Command
func runWellness(cmd *cobra.Command, args []string) error {
return fmt.Errorf("not implemented")
}