Skip to content

Commit 4570261

Browse files
authored
Add --payload-file flag to kernel invoke (#66)
## Summary Add a `--payload-file` / `-f` flag to `kernel invoke` that allows reading the JSON payload from a file instead of passing it inline. This is useful for: - Large payloads that are unwieldy on the command line - Payloads with special characters that need escaping - Scripting and automation workflows - Piping data from other commands ## Usage ```bash # Read payload from a file kernel invoke myapp action -f payload.json # Read payload from stdin cat payload.json | kernel invoke myapp action -f - # Pipe from another command echo '{"key": "value"}' | kernel invoke myapp action -f - # Combine with other tools jq '.data' response.json | kernel invoke myapp process -f - ``` ## Details - The `--payload` and `--payload-file` flags are mutually exclusive (enforced by cobra) - Supports `-` as a special filename to read from stdin - Validates that the file contents are valid JSON before invoking - Trims whitespace from file contents ## Test plan - [x] `go build ./...` passes - [x] `go test ./...` passes - [x] Help output shows new flag correctly - [ ] Manual test: `kernel invoke app action -f payload.json` - [ ] Manual test: `echo '{}' | kernel invoke app action -f -` - [ ] Manual test: verify `--payload` and `--payload-file` cannot be used together <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds file/stdin-based payload input to `kernel invoke` with JSON validation and updates docs. > > - Introduces `--payload-file`, `-f` flag in `invoke` (mutually exclusive with `--payload`) > - New `getPayload` helper supports reading from file or `-` (stdin), trims whitespace, and validates JSON before invoking > - Wires payload into invocation params when provided; preserves existing sync/async behavior > - README updated with new flag in command reference and examples for file/stdin/pipe usage > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d7205d5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 54b919a commit 4570261

2 files changed

Lines changed: 70 additions & 9 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com).
150150

151151
- `--version <version>`, `-v` - Specify app version (default: latest)
152152
- `--payload <json>`, `-p` - JSON payload for the action
153+
- `--payload-file <path>`, `-f` - Read JSON payload from a file (use `-` for stdin)
153154
- `--sync`, `-s` - Invoke synchronously (timeout after 60s)
154155

155156
- `kernel app list` - List deployed apps
@@ -423,6 +424,15 @@ kernel invoke my-scraper scrape-page
423424
# With JSON payload
424425
kernel invoke my-scraper scrape-page --payload '{"url": "https://example.com"}'
425426

427+
# Read payload from a file
428+
kernel invoke my-scraper scrape-page --payload-file payload.json
429+
430+
# Read payload from stdin
431+
cat payload.json | kernel invoke my-scraper scrape-page --payload-file -
432+
433+
# Pipe from another command
434+
echo '{"url": "https://example.com"}' | kernel invoke my-scraper scrape-page -f -
435+
426436
# Synchronous invoke (wait for completion)
427437
kernel invoke my-scraper quick-task --sync
428438
```

cmd/invoke.go

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"io"
89
"os"
910
"os/signal"
1011
"strings"
@@ -37,7 +38,9 @@ var invocationHistoryCmd = &cobra.Command{
3738
func init() {
3839
invokeCmd.Flags().StringP("version", "v", "latest", "Specify a version of the app to invoke (optional, defaults to 'latest')")
3940
invokeCmd.Flags().StringP("payload", "p", "", "JSON payload for the invocation (optional)")
41+
invokeCmd.Flags().StringP("payload-file", "f", "", "Path to a JSON file containing the payload (use '-' for stdin)")
4042
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.")
43+
invokeCmd.MarkFlagsMutuallyExclusive("payload", "payload-file")
4144

4245
invocationHistoryCmd.Flags().Int("limit", 100, "Max invocations to return (default 100)")
4346
invocationHistoryCmd.Flags().StringP("app", "a", "", "Filter by app name")
@@ -65,15 +68,11 @@ func runInvoke(cmd *cobra.Command, args []string) error {
6568
Async: kernel.Opt(!isSync),
6669
}
6770

68-
payloadStr, _ := cmd.Flags().GetString("payload")
69-
if cmd.Flags().Changed("payload") {
70-
// validate JSON unless empty string explicitly set
71-
if payloadStr != "" {
72-
var v interface{}
73-
if err := json.Unmarshal([]byte(payloadStr), &v); err != nil {
74-
return fmt.Errorf("invalid JSON payload: %w", err)
75-
}
76-
}
71+
payloadStr, hasPayload, err := getPayload(cmd)
72+
if err != nil {
73+
return err
74+
}
75+
if hasPayload {
7776
params.Payload = kernel.Opt(payloadStr)
7877
}
7978
// we don't really care to cancel the context, we just want to handle signals
@@ -218,6 +217,58 @@ func printResult(success bool, output string) {
218217
}
219218
}
220219

220+
// getPayload reads the payload from either --payload flag or --payload-file flag.
221+
// Returns the payload string, whether a payload was explicitly provided, and any error.
222+
// The second return value (hasPayload) is true when the user explicitly set a payload,
223+
// even if that payload is an empty string.
224+
func getPayload(cmd *cobra.Command) (payload string, hasPayload bool, err error) {
225+
payloadStr, _ := cmd.Flags().GetString("payload")
226+
payloadFile, _ := cmd.Flags().GetString("payload-file")
227+
228+
// If --payload was explicitly set, use it (even if empty string)
229+
if cmd.Flags().Changed("payload") {
230+
// Validate JSON unless empty string explicitly set
231+
if payloadStr != "" {
232+
var v interface{}
233+
if err := json.Unmarshal([]byte(payloadStr), &v); err != nil {
234+
return "", false, fmt.Errorf("invalid JSON payload: %w", err)
235+
}
236+
}
237+
return payloadStr, true, nil
238+
}
239+
240+
// If --payload-file was set, read from file
241+
if cmd.Flags().Changed("payload-file") {
242+
var data []byte
243+
244+
if payloadFile == "-" {
245+
// Read from stdin
246+
data, err = io.ReadAll(os.Stdin)
247+
if err != nil {
248+
return "", false, fmt.Errorf("failed to read payload from stdin: %w", err)
249+
}
250+
} else {
251+
// Read from file
252+
data, err = os.ReadFile(payloadFile)
253+
if err != nil {
254+
return "", false, fmt.Errorf("failed to read payload file: %w", err)
255+
}
256+
}
257+
258+
payloadStr = strings.TrimSpace(string(data))
259+
// Validate JSON unless empty
260+
if payloadStr != "" {
261+
var v interface{}
262+
if err := json.Unmarshal([]byte(payloadStr), &v); err != nil {
263+
return "", false, fmt.Errorf("invalid JSON in payload file: %w", err)
264+
}
265+
}
266+
return payloadStr, true, nil
267+
}
268+
269+
return "", false, nil
270+
}
271+
221272
func runInvocationHistory(cmd *cobra.Command, args []string) error {
222273
client := getKernelClient(cmd)
223274

0 commit comments

Comments
 (0)