goke 6.6.1 → 6.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +142 -2
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -654,6 +654,60 @@ When using brackets in option name, angled brackets indicate that a string / num
654
654
 
655
655
  **Optionality is determined solely by bracket syntax, not by the schema.** `[square brackets]` makes an option optional regardless of whether the schema is `z.string()` or `z.string().optional()`. The schema's `.optional()` is never consulted for this — it only affects type coercion. This means `z.string()` with `[--name]` is treated as optional: if the flag is omitted, `options.name` is `undefined` even though the schema has no `.optional()`.
656
656
 
657
+ ### Optional-value flags — `--flag` vs `--flag value` vs omitted
658
+
659
+ A flag declared with square brackets (`--host [host]`) has **three distinct runtime states**, not two. The user can:
660
+
661
+ 1. **Omit the flag entirely** — no `--host` on the command line at all
662
+ 2. **Pass the flag bare** — `--host` by itself, with no value following it
663
+ 3. **Pass the flag with a value** — `--host example.com`
664
+
665
+ goke surfaces all three cases through a single `string | undefined` type. There is no `boolean` in the union — bare flags are normalized to the **empty string `''`** so callers only ever deal with strings:
666
+
667
+ ```ts
668
+ cli
669
+ .command('serve', 'Start the server')
670
+ .option('--host [host]', 'Optional host override')
671
+ .action((options) => {
672
+ // options.host: string | undefined
673
+ // --host → '' (flag present, no value)
674
+ // --host example.com → 'example.com'
675
+ // (omitted) → undefined
676
+ })
677
+ ```
678
+
679
+ **Detecting each case:**
680
+
681
+ ```ts
682
+ .action((options) => {
683
+ if (options.host === undefined) {
684
+ // Flag was not passed at all — use a sensible default
685
+ console.log('using default host: localhost')
686
+ } else if (options.host === '') {
687
+ // Flag was passed bare: `--host` with no value following it
688
+ // Treat this as an explicit "opt in, but use the default/automatic value"
689
+ console.log('host flag passed with no value — enabling auto-discovery')
690
+ } else {
691
+ // Flag was passed with an explicit value
692
+ console.log(`host = ${options.host}`)
693
+ }
694
+ })
695
+ ```
696
+
697
+ **In most cases you don't need the three-way distinction** — a plain truthy check collapses "omitted" and "bare flag" into the same "fall back to default" branch:
698
+
699
+ ```ts
700
+ .action((options) => {
701
+ // `--host` bare AND omitted both fall through to the default
702
+ const host = options.host || 'localhost'
703
+ startServer({ host })
704
+ })
705
+ ```
706
+
707
+ Reserve the `=== ''` check for cases where "opt in without a value" is a meaningful signal distinct from "flag omitted" — for example, `--direct` meaning "auto-discover a Chrome instance" vs `--direct ws://…` meaning "connect to this specific endpoint" vs no `--direct` meaning "don't use direct mode".
708
+
709
+ > **Breaking change note (goke 6.6.0):** prior versions surfaced bare flags as `boolean` `true` inside a `string | boolean | undefined` union, forcing every call site to write `typeof options.host === 'string' ? options.host : undefined`. Code that used `options.host === true` to detect the bare-flag case must be updated to `options.host === ''`. Schema-based optional flags with `.default(...)` are unaffected — the default still kicks in when the flag is passed bare.
710
+
657
711
  ### Negated Options
658
712
 
659
713
  To allow an option whose value is `false`, you need to manually specify a negated option:
@@ -684,6 +738,8 @@ cli
684
738
 
685
739
  The `--` token signals the end of options. Everything after `--` is available via `options['--']` as a separate array, not mixed into positional args. This lets you distinguish between your command's own arguments and passthrough args — the same pattern used by `doppler`, `npm`, `pnpm`, and `docker`.
686
740
 
741
+ `options['--']` is **always present** on the inferred options type as `string[]`. When no `--` token appears on the command line, it's the empty array — you never need to guard with `||` or `?.` or an `Array.isArray` cast.
742
+
687
743
  ```ts
688
744
  import { goke } from 'goke'
689
745
  import { z } from 'zod'
@@ -700,10 +756,10 @@ cli
700
756
  // runner run --env staging server.js -- --port 3000 --verbose
701
757
  // script = 'server.js' (positional arg)
702
758
  // options.env = 'staging' (runner's own option)
703
- // options['--'] = ['--port', '3000', '--verbose'] (passthrough)
759
+ // options['--'] = ['--port', '3000', '--verbose'] (passthrough, always string[])
704
760
 
705
761
  const secrets = loadSecrets(options.env)
706
- const extraArgs = (options['--'] || []).join(' ')
762
+ const extraArgs = options['--'].join(' ')
707
763
  execSync(`node ${script} ${extraArgs}`, {
708
764
  env: { ...process.env, ...secrets },
709
765
  stdio: 'inherit',
@@ -915,6 +971,90 @@ Always run `acme --help` before using this CLI.
915
971
  For subcommand details: `acme <command> --help`
916
972
  ````
917
973
 
974
+ ## YAML Output for Agent-Friendly CLIs
975
+
976
+ When a command returns structured data, print it as YAML on stdout. YAML is the best middle ground between human-readable output and machine-processable output:
977
+
978
+ - Humans can read it at a glance — no surrounding quotes on keys, less punctuation noise than JSON.
979
+ - Agents can process it with [`yq`](https://github.com/mikefarah/yq), the YAML equivalent of `jq`, to extract specific fields or filter results.
980
+ - It is more context-efficient than verbose prose: a compact YAML block conveys the same information in fewer tokens.
981
+
982
+ ```ts
983
+ import { goke } from 'goke'
984
+ import { stringify } from 'yaml'
985
+
986
+ const cli = goke('deploy')
987
+
988
+ cli
989
+ .command('status', 'Show deployment status')
990
+ .action(async (options, { console }) => {
991
+ const status = await fetchStatus()
992
+ // Output structured data as YAML on stdout
993
+ console.log(stringify(status))
994
+ })
995
+ ```
996
+
997
+ Example output:
998
+
999
+ ```yaml
1000
+ deployment: prod-v2
1001
+ status: running
1002
+ replicas: 3
1003
+ lastDeploy: "2026-01-15T10:30:00Z"
1004
+ health:
1005
+ cpu: 42%
1006
+ memory: 1.2GB
1007
+ ```
1008
+
1009
+ ### Processing YAML output with yq
1010
+
1011
+ Agents can pipe the output through `yq` to extract specific fields or filter results — the same way they would use `jq` with JSON, but with cleaner, more readable output:
1012
+
1013
+ ```bash
1014
+ # Extract a single field
1015
+ deploy status | yq '.deployment'
1016
+
1017
+ # Access nested fields
1018
+ deploy status | yq '.health.cpu'
1019
+
1020
+ # Filter an array of results
1021
+ deploy list | yq '.[] | select(.status == "running")'
1022
+
1023
+ # Combine multiple fields
1024
+ deploy list | yq '.[] | {name: .name, status: .status}'
1025
+
1026
+ # Count items matching a condition
1027
+ deploy list | yq '[.[] | select(.status == "error")] | length'
1028
+ ```
1029
+
1030
+ ### Keep stdout clean — send non-YAML to stderr
1031
+
1032
+ If a command outputs YAML on stdout, all unrelated content must go to stderr: error messages, progress indicators, informational logs, warnings. This keeps stdout pipeable through `yq` without breaking the YAML parse.
1033
+
1034
+ ```ts
1035
+ cli
1036
+ .command('deploy <env>', 'Deploy to environment')
1037
+ .action(async (env, options, { console }) => {
1038
+ // Progress and logs → stderr (won't pollute yq pipes)
1039
+ console.error(`Deploying to ${env}...`)
1040
+ console.error('Building artifacts...')
1041
+
1042
+ const result = await deploy(env)
1043
+
1044
+ // Structured result → stdout as YAML
1045
+ console.log(stringify(result))
1046
+ })
1047
+ ```
1048
+
1049
+ Now agents can process the output cleanly:
1050
+
1051
+ ```bash
1052
+ # Only the YAML result reaches yq — progress lines go to the terminal
1053
+ deploy deploy production | yq '.version'
1054
+ ```
1055
+
1056
+ If an error occurs, throw or write to `console.error` / `process.stderr`, and exit with a non-zero code. Never mix error text into stdout when the command is expected to output YAML.
1057
+
918
1058
  ## Contributor Notes
919
1059
 
920
1060
  ### Rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goke",
3
- "version": "6.6.1",
3
+ "version": "6.6.2",
4
4
  "type": "module",
5
5
  "description": "Simple yet powerful framework for building command-line apps. Inspired by cac.",
6
6
  "repository": {