From 3edae2e31771958cdcabe9cab1ebeeff1c71982b Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Thu, 8 Jan 2026 14:09:24 -0500 Subject: [PATCH 1/3] Add --payload-file flag to kernel invoke Allow reading JSON payload from a file instead of passing it inline. Supports reading from stdin with '-' as the filename. Examples: kernel invoke myapp action -f payload.json cat payload.json | kernel invoke myapp action -f - echo '{"key": "value"}' | kernel invoke myapp action -f - --- cmd/invoke.go | 69 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/cmd/invoke.go b/cmd/invoke.go index c034588..5c35364 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "io" "os" "os/signal" "strings" @@ -36,7 +37,9 @@ var invocationHistoryCmd = &cobra.Command{ func init() { invokeCmd.Flags().StringP("version", "v", "latest", "Specify a version of the app to invoke (optional, defaults to 'latest')") invokeCmd.Flags().StringP("payload", "p", "", "JSON payload for the invocation (optional)") + invokeCmd.Flags().StringP("payload-file", "f", "", "Path to a JSON file containing the payload (use '-' for stdin)") invokeCmd.Flags().BoolP("sync", "s", false, "Invoke synchronously (default false). A synchronous invocation will open a long-lived HTTP POST to the Kernel API to wait for the invocation to complete. This will time out after 60 seconds, so only use this option if you expect your invocation to complete in less than 60 seconds. The default is to invoke asynchronously, in which case the CLI will open an SSE connection to the Kernel API after submitting the invocation and wait for the invocation to complete.") + invokeCmd.MarkFlagsMutuallyExclusive("payload", "payload-file") invocationHistoryCmd.Flags().Int("limit", 100, "Max invocations to return (default 100)") invocationHistoryCmd.Flags().StringP("app", "a", "", "Filter by app name") @@ -64,15 +67,11 @@ func runInvoke(cmd *cobra.Command, args []string) error { Async: kernel.Opt(!isSync), } - payloadStr, _ := cmd.Flags().GetString("payload") - if cmd.Flags().Changed("payload") { - // validate JSON unless empty string explicitly set - if payloadStr != "" { - var v interface{} - if err := json.Unmarshal([]byte(payloadStr), &v); err != nil { - return fmt.Errorf("invalid JSON payload: %w", err) - } - } + payloadStr, err := getPayload(cmd) + if err != nil { + return err + } + if payloadStr != "" { params.Payload = kernel.Opt(payloadStr) } // we don't really care to cancel the context, we just want to handle signals @@ -213,6 +212,58 @@ func printResult(success bool, output string) { } } +// getPayload reads the payload from either --payload flag or --payload-file flag. +// Returns the validated JSON payload string, or empty string if no payload was specified. +func getPayload(cmd *cobra.Command) (string, error) { + payloadStr, _ := cmd.Flags().GetString("payload") + payloadFile, _ := cmd.Flags().GetString("payload-file") + + // If --payload was explicitly set, use it + if cmd.Flags().Changed("payload") { + if payloadStr == "" { + return "", nil + } + var v interface{} + if err := json.Unmarshal([]byte(payloadStr), &v); err != nil { + return "", fmt.Errorf("invalid JSON payload: %w", err) + } + return payloadStr, nil + } + + // If --payload-file was set, read from file + if cmd.Flags().Changed("payload-file") { + var data []byte + var err error + + if payloadFile == "-" { + // Read from stdin + data, err = io.ReadAll(os.Stdin) + if err != nil { + return "", fmt.Errorf("failed to read payload from stdin: %w", err) + } + } else { + // Read from file + data, err = os.ReadFile(payloadFile) + if err != nil { + return "", fmt.Errorf("failed to read payload file: %w", err) + } + } + + payloadStr = strings.TrimSpace(string(data)) + if payloadStr == "" { + return "", nil + } + + var v interface{} + if err := json.Unmarshal([]byte(payloadStr), &v); err != nil { + return "", fmt.Errorf("invalid JSON in payload file: %w", err) + } + return payloadStr, nil + } + + return "", nil +} + func runInvocationHistory(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) From b4752ad493ee6bbf6119ba527b269cddb49da4a7 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Thu, 8 Jan 2026 14:18:11 -0500 Subject: [PATCH 2/3] Fix: preserve empty payload behavior Address review feedback: ensure --payload "" still explicitly sets an empty payload, preserving the original behavior where the API can distinguish between "no payload" and "empty payload". --- cmd/invoke.go | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/cmd/invoke.go b/cmd/invoke.go index 5c35364..fcc6e5b 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -67,11 +67,11 @@ func runInvoke(cmd *cobra.Command, args []string) error { Async: kernel.Opt(!isSync), } - payloadStr, err := getPayload(cmd) + payloadStr, hasPayload, err := getPayload(cmd) if err != nil { return err } - if payloadStr != "" { + if hasPayload { params.Payload = kernel.Opt(payloadStr) } // we don't really care to cancel the context, we just want to handle signals @@ -213,55 +213,55 @@ func printResult(success bool, output string) { } // getPayload reads the payload from either --payload flag or --payload-file flag. -// Returns the validated JSON payload string, or empty string if no payload was specified. -func getPayload(cmd *cobra.Command) (string, error) { +// Returns the payload string, whether a payload was explicitly provided, and any error. +// The second return value (hasPayload) is true when the user explicitly set a payload, +// even if that payload is an empty string. +func getPayload(cmd *cobra.Command) (payload string, hasPayload bool, err error) { payloadStr, _ := cmd.Flags().GetString("payload") payloadFile, _ := cmd.Flags().GetString("payload-file") - // If --payload was explicitly set, use it + // If --payload was explicitly set, use it (even if empty string) if cmd.Flags().Changed("payload") { - if payloadStr == "" { - return "", nil - } - var v interface{} - if err := json.Unmarshal([]byte(payloadStr), &v); err != nil { - return "", fmt.Errorf("invalid JSON payload: %w", err) + // Validate JSON unless empty string explicitly set + if payloadStr != "" { + var v interface{} + if err := json.Unmarshal([]byte(payloadStr), &v); err != nil { + return "", false, fmt.Errorf("invalid JSON payload: %w", err) + } } - return payloadStr, nil + return payloadStr, true, nil } // If --payload-file was set, read from file if cmd.Flags().Changed("payload-file") { var data []byte - var err error if payloadFile == "-" { // Read from stdin data, err = io.ReadAll(os.Stdin) if err != nil { - return "", fmt.Errorf("failed to read payload from stdin: %w", err) + return "", false, fmt.Errorf("failed to read payload from stdin: %w", err) } } else { // Read from file data, err = os.ReadFile(payloadFile) if err != nil { - return "", fmt.Errorf("failed to read payload file: %w", err) + return "", false, fmt.Errorf("failed to read payload file: %w", err) } } payloadStr = strings.TrimSpace(string(data)) - if payloadStr == "" { - return "", nil - } - - var v interface{} - if err := json.Unmarshal([]byte(payloadStr), &v); err != nil { - return "", fmt.Errorf("invalid JSON in payload file: %w", err) + // Validate JSON unless empty + if payloadStr != "" { + var v interface{} + if err := json.Unmarshal([]byte(payloadStr), &v); err != nil { + return "", false, fmt.Errorf("invalid JSON in payload file: %w", err) + } } - return payloadStr, nil + return payloadStr, true, nil } - return "", nil + return "", false, nil } func runInvocationHistory(cmd *cobra.Command, args []string) error { From d7205d59773536c2c170b15fb42d42aca615d15e Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Thu, 8 Jan 2026 14:20:30 -0500 Subject: [PATCH 3/3] Update README with --payload-file documentation Add documentation for the new --payload-file / -f flag including: - Flag description in the commands reference - Usage examples showing file and stdin input --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index e4392cd..5adee9c 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `--version `, `-v` - Specify app version (default: latest) - `--payload `, `-p` - JSON payload for the action + - `--payload-file `, `-f` - Read JSON payload from a file (use `-` for stdin) - `--sync`, `-s` - Invoke synchronously (timeout after 60s) - `kernel app list` - List deployed apps @@ -423,6 +424,15 @@ kernel invoke my-scraper scrape-page # With JSON payload kernel invoke my-scraper scrape-page --payload '{"url": "https://example.com"}' +# Read payload from a file +kernel invoke my-scraper scrape-page --payload-file payload.json + +# Read payload from stdin +cat payload.json | kernel invoke my-scraper scrape-page --payload-file - + +# Pipe from another command +echo '{"url": "https://example.com"}' | kernel invoke my-scraper scrape-page -f - + # Synchronous invoke (wait for completion) kernel invoke my-scraper quick-task --sync ```