diff --git a/cmd/app.go b/cmd/app.go index 58cc21e..16cfbbf 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "strings" "github.com/onkernel/cli/pkg/util" @@ -40,15 +41,37 @@ func init() { // Add optional filters for list appListCmd.Flags().String("name", "", "Filter by application name") appListCmd.Flags().String("version", "", "Filter by version label") + appListCmd.Flags().Int("limit", 20, "Max apps to return (default 20)") + appListCmd.Flags().Int("per-page", 20, "Items per page (alias of --limit)") + appListCmd.Flags().Int("page", 1, "Page number (1-based)") // Limit rows returned for app history (0 = all) - appHistoryCmd.Flags().Int("limit", 100, "Max deployments to return (default 100)") + appHistoryCmd.Flags().Int("limit", 20, "Max deployments to return (default 20)") } func runAppList(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) appName, _ := cmd.Flags().GetString("name") version, _ := cmd.Flags().GetString("version") + lim, _ := cmd.Flags().GetInt("limit") + perPage, _ := cmd.Flags().GetInt("per-page") + page, _ := cmd.Flags().GetInt("page") + + // Determine pagination inputs: prefer page/per-page if provided; else map legacy --limit + usePager := cmd.Flags().Changed("per-page") || cmd.Flags().Changed("page") + if !usePager && cmd.Flags().Changed("limit") { + if lim < 0 { + lim = 0 + } + perPage = lim + page = 1 + } + if perPage <= 0 { + perPage = 20 + } + if page <= 0 { + page = 1 + } pterm.Debug.Println("Fetching deployed applications...") @@ -59,6 +82,9 @@ func runAppList(cmd *cobra.Command, args []string) error { if version != "" { params.Version = kernel.Opt(version) } + // Apply server-side pagination (request one extra to detect hasMore) + params.Limit = kernel.Opt(int64(perPage + 1)) + params.Offset = kernel.Opt(int64((page - 1) * perPage)) apps, err := client.Apps.List(cmd.Context(), params) if err != nil { @@ -66,17 +92,27 @@ func runAppList(cmd *cobra.Command, args []string) error { return nil } - if apps == nil || len(*apps) == 0 { + if apps == nil || len(apps.Items) == 0 { pterm.Info.Println("No applications found") return nil } + // Determine hasMore using +1 item trick and keep only perPage items for display + items := apps.Items + hasMore := false + if len(items) > perPage { + hasMore = true + items = items[:perPage] + } + itemsThisPage := len(items) + // Prepare table data tableData := pterm.TableData{ {"App Name", "Version", "App Version ID", "Region", "Actions", "Env Vars"}, } - for _, app := range *apps { + rows := 0 + for _, app := range items { // Format env vars envVarsStr := "-" if len(app.EnvVars) > 0 { @@ -98,9 +134,58 @@ func runAppList(cmd *cobra.Command, args []string) error { actionsStr, envVarsStr, }) + rows++ } PrintTableNoPad(tableData, true) + + // Footer with pagination details and next command suggestion + fmt.Printf("\nPage: %d Per-page: %d Items this page: %d Has more: %s\n", page, perPage, itemsThisPage, lo.Ternary(hasMore, "yes", "no")) + if hasMore { + nextPage := page + 1 + nextCmd := fmt.Sprintf("kernel app list --page %d --per-page %d", nextPage, perPage) + if appName != "" { + nextCmd += fmt.Sprintf(" --name %s", appName) + } + if version != "" { + nextCmd += fmt.Sprintf(" --version %s", version) + } + fmt.Printf("Next: %s\n", nextCmd) + } + // Concise notes when user-specified per-page/limit/page are outside API-allowed range + if cmd.Flags().Changed("per-page") { + if v, _ := cmd.Flags().GetInt("per-page"); v > 100 { + pterm.Warning.Printfln("Requested --per-page %d; capped to 100.", v) + } else if v < 1 { + if cmd.Flags().Changed("page") { + if p, _ := cmd.Flags().GetInt("page"); p < 1 { + pterm.Warning.Println("Requested --per-page <1 and --page <1; using per-page=20, page=1.") + } else { + pterm.Warning.Println("Requested --per-page <1; using per-page=20.") + } + } else { + pterm.Warning.Println("Requested --per-page <1; using per-page=20.") + } + } + } else if !usePager && cmd.Flags().Changed("limit") { + if lim > 100 { + pterm.Warning.Printfln("Requested --limit %d; capped to 100.", lim) + } else if lim < 1 { + if cmd.Flags().Changed("page") { + if p, _ := cmd.Flags().GetInt("page"); p < 1 { + pterm.Warning.Println("Requested --limit <1 and --page <1; using per-page=20, page=1.") + } else { + pterm.Warning.Println("Requested --limit <1; using per-page=20.") + } + } else { + pterm.Warning.Println("Requested --limit <1; using per-page=20.") + } + } + } else if cmd.Flags().Changed("page") { + if p, _ := cmd.Flags().GetInt("page"); p < 1 { + pterm.Warning.Println("Requested --page <1; using page=1.") + } + } return nil } diff --git a/cmd/deploy.go b/cmd/deploy.go index 822c35e..600dca6 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -19,6 +19,7 @@ import ( kernel "github.com/onkernel/kernel-go-sdk" "github.com/onkernel/kernel-go-sdk/option" "github.com/pterm/pterm" + "github.com/samber/lo" "github.com/spf13/cobra" ) @@ -63,7 +64,9 @@ func init() { deployLogsCmd.Flags().BoolP("with-timestamps", "t", false, "Include timestamps in each log line") deployCmd.AddCommand(deployLogsCmd) - deployHistoryCmd.Flags().Int("limit", 100, "Max deployments to return (default 100)") + deployHistoryCmd.Flags().Int("limit", 20, "Max deployments to return (default 20)") + deployHistoryCmd.Flags().Int("per-page", 20, "Items per page (alias of --limit)") + deployHistoryCmd.Flags().Int("page", 1, "Page number (1-based)") deployCmd.AddCommand(deployHistoryCmd) // Flags for GitHub deploy @@ -354,65 +357,116 @@ func runDeployHistory(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) lim, _ := cmd.Flags().GetInt("limit") + perPage, _ := cmd.Flags().GetInt("per-page") + page, _ := cmd.Flags().GetInt("page") + + // Prefer page/per-page when provided; map legacy --limit otherwise + usePager := cmd.Flags().Changed("per-page") || cmd.Flags().Changed("page") + if !usePager && cmd.Flags().Changed("limit") { + if lim < 0 { + lim = 0 + } + perPage = lim + page = 1 + } + if perPage <= 0 { + perPage = 20 + } + if page <= 0 { + page = 1 + } - var appNames []string + // Build server-side paginated request + var appNameFilter string if len(args) == 1 { - appNames = []string{args[0]} - } else { - apps, err := client.Apps.List(cmd.Context(), kernel.AppListParams{}) - if err != nil { - pterm.Error.Printf("Failed to list applications: %v\n", err) - return nil - } - for _, a := range *apps { - appNames = append(appNames, a.AppName) - } - // de-duplicate app names - seenApps := map[string]struct{}{} - uniq := make([]string, 0, len(appNames)) - for _, n := range appNames { - if _, ok := seenApps[n]; ok { - continue - } - seenApps[n] = struct{}{} - uniq = append(uniq, n) - } - appNames = uniq + appNameFilter = strings.TrimSpace(args[0]) } - rows := 0 - table := pterm.TableData{{"Deployment ID", "Created At", "Region", "Status", "Entrypoint", "Reason"}} -AppsLoop: - for _, appName := range appNames { - params := kernel.DeploymentListParams{AppName: kernel.Opt(appName)} - pterm.Debug.Printf("Listing deployments for app '%s'...\n", appName) - deployments, err := client.Deployments.List(cmd.Context(), params) - if err != nil { - pterm.Error.Printf("Failed to list deployments for '%s': %v\n", appName, err) - continue - } - for _, dep := range deployments.Items { - created := util.FormatLocal(dep.CreatedAt) - status := string(dep.Status) - table = append(table, []string{ - dep.ID, - created, - string(dep.Region), - status, - dep.EntrypointRelPath, - dep.StatusReason, - }) - rows++ - if lim > 0 && rows >= lim { - break AppsLoop - } - } + params := kernel.DeploymentListParams{} + if appNameFilter != "" { + params.AppName = kernel.Opt(appNameFilter) + } + // Request one extra item to detect hasMore + params.Limit = kernel.Opt(int64(perPage + 1)) + params.Offset = kernel.Opt(int64((page - 1) * perPage)) + + pterm.Debug.Println("Fetching deployments...") + deployments, err := client.Deployments.List(cmd.Context(), params) + if err != nil { + pterm.Error.Printf("Failed to list deployments: %v\n", err) + return nil } - if len(table) == 1 { + if deployments == nil || len(deployments.Items) == 0 { pterm.Info.Println("No deployments found") return nil } + + items := deployments.Items + hasMore := false + if len(items) > perPage { + hasMore = true + items = items[:perPage] + } + itemsThisPage := len(items) + + table := pterm.TableData{{"Deployment ID", "Created At", "Region", "Status", "Entrypoint", "Reason"}} + for _, dep := range items { + created := util.FormatLocal(dep.CreatedAt) + status := string(dep.Status) + table = append(table, []string{ + dep.ID, + created, + string(dep.Region), + status, + dep.EntrypointRelPath, + dep.StatusReason, + }) + } pterm.DefaultTable.WithHasHeader().WithData(table).Render() + + fmt.Printf("\nPage: %d Per-page: %d Items this page: %d Has more: %s\n", page, perPage, itemsThisPage, lo.Ternary(hasMore, "yes", "no")) + if hasMore { + nextPage := page + 1 + nextCmd := fmt.Sprintf("kernel deploy history --page %d --per-page %d", nextPage, perPage) + if appNameFilter != "" { + nextCmd = fmt.Sprintf("kernel deploy history %s --page %d --per-page %d", appNameFilter, nextPage, perPage) + } + fmt.Printf("Next: %s\n", nextCmd) + } + // Concise notes when user-specified per-page/limit/page are outside API-allowed range + if cmd.Flags().Changed("per-page") { + if v, _ := cmd.Flags().GetInt("per-page"); v > 100 { + pterm.Warning.Printfln("Requested --per-page %d; capped to 100.", v) + } else if v < 1 { + if cmd.Flags().Changed("page") { + if p, _ := cmd.Flags().GetInt("page"); p < 1 { + pterm.Warning.Println("Requested --per-page <1 and --page <1; using per-page=20, page=1.") + } else { + pterm.Warning.Println("Requested --per-page <1; using per-page=20.") + } + } else { + pterm.Warning.Println("Requested --per-page <1; using per-page=20.") + } + } + } else if !usePager && cmd.Flags().Changed("limit") { + if lim > 100 { + pterm.Warning.Printfln("Requested --limit %d; capped to 100.", lim) + } else if lim < 1 { + if cmd.Flags().Changed("page") { + if p, _ := cmd.Flags().GetInt("page"); p < 1 { + pterm.Warning.Println("Requested --limit <1 and --page <1; using per-page=20, page=1.") + } else { + pterm.Warning.Println("Requested --limit <1; using per-page=20.") + } + } else { + pterm.Warning.Println("Requested --limit <1; using per-page=20.") + } + } + } else if cmd.Flags().Changed("page") { + if p, _ := cmd.Flags().GetInt("page"); p < 1 { + pterm.Warning.Println("Requested --page <1; using page=1.") + } + } return nil } diff --git a/cmd/logs.go b/cmd/logs.go index 00db84e..7d7dbf4 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -140,13 +140,13 @@ func runLogs(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("failed to list apps: %w", err) } - if apps == nil || len(*apps) == 0 { + if apps == nil || len(apps.Items) == 0 { return fmt.Errorf("app \"%s\" not found", appName) } - if len(*apps) > 1 { + if len(apps.Items) > 1 { return fmt.Errorf("multiple apps found for \"%s\", please specify a version", appName) } - app := (*apps)[0] + app := apps.Items[0] pterm.Info.Printf("Streaming logs for app \"%s\" (version: %s, id: %s)...\n", appName, version, app.ID) if follow { diff --git a/go.mod b/go.mod index c57bcea..d4596f0 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/charmbracelet/fang v0.2.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/onkernel/kernel-go-sdk v0.17.0 + github.com/onkernel/kernel-go-sdk v0.18.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index a2a65bb..366a3af 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= -github.com/onkernel/kernel-go-sdk v0.17.0 h1:3q7hrfiLTJbUwcJTtPhdnNIXUI4/TUnoklPjUGBoeas= -github.com/onkernel/kernel-go-sdk v0.17.0/go.mod h1:MjUR92i8UPqjrmneyVykae6GuB3GGSmnQtnjf1v74Dc= +github.com/onkernel/kernel-go-sdk v0.18.0 h1:hlBqxL2sEUto6h449b93C0YkAQeRdxrhn5cAScbhjaQ= +github.com/onkernel/kernel-go-sdk v0.18.0/go.mod h1:MjUR92i8UPqjrmneyVykae6GuB3GGSmnQtnjf1v74Dc= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=