- name
- playground-cli
- description
- Add or change a command in the `playground` CLI of kafka-docker-playground.
Use when editing `scripts/cli/src/bashly.yml`, adding or renaming a command /
subcommand / flag / argument, writing a command partial under
`scripts/cli/src/commands/`, adding a shared bash helper in
`scripts/cli/src/lib/`, adding a validator or filter, wiring shell
completions, or regenerating the CLI. The CLI is generated by Bashly —
`scripts/cli/playground` must never be hand-edited.
# playground CLI (Bashly)
The `playground` CLI is generated by [Bashly](https://bashly.dev) from
`scripts/cli/src/`. Editing the generated output is always wrong: the next
regeneration silently discards it.
## Never hand-edit these — all generated, all committed
| File | Produced by |
| --- | --- |
| `scripts/cli/playground` (~50k lines) | `bashly generate` |
| `scripts/cli/completions.bash` | `bashly add completions_script` + a post-processing fix |
| `scripts/cli/playground.yaml` | `bashly render templates/shell-script-command-completion .` |
| `scripts/cli/playground.json` | `yq -o=json playground.yaml` |
| `docs/cli.md` and friends | `playground update-docs` |
They are tracked in git, so a CLI change is only complete once they are
regenerated **and** committed alongside the source edit.
## The loop
1. **Declare** the command/flag in `scripts/cli/src/bashly.yml` (~5000 lines,
the single source of truth for the whole command tree).
2. **Implement** it in `scripts/cli/src/commands/<command>/<subcommand>.sh` —
one file per (sub)command, path mirroring the command tree exactly.
3. **Regenerate**: `playground bashly-reload`
(requires Docker; pulls and runs `dannyben/bashly`).
4. **Verify**: `playground <cmd> --help`, one success path, one error path.
5. **Refresh docs** if user-facing help text changed: `playground update-docs`.
6. Commit source **and** regenerated artifacts together.
Skipping step 3 is the classic failure: the flag exists in `bashly.yml`, the
partial reads `${args[--new-flag]}`, and the CLI ignores it entirely because
the parser was never rebuilt.
## Where does the code go?
| What you're adding | Where |
| --- | --- |
| Logic for one command | `src/commands/<cmd>/<sub>.sh` |
| Helper used only by the CLI (fzf pickers, prompts, connector/topic lookups) | `src/lib/cli_function.sh` (~2400 lines) |
| Helper also used by example scripts | `src/lib/utils_function.sh` (~4200 lines — the real core library) |
| `playground.ini` read/write | `src/lib/ini.sh` |
| Argument validator | `src/lib/validations/validate_<name>.sh` |
| Command precondition | `filter_<name>()` in `src/lib/cli_function.sh` |
| Version / image defaults (`TAG`, `CP_*_IMAGE`, …) | `scripts/utils.sh`, on the line ending `# default tag` |
Everything under `src/lib/` is inlined into the generated script — no sourcing
needed, and any function there is callable from any command partial.
Do **not** put general-purpose helpers in `scripts/utils.sh`. That file is only
a thin entrypoint for example scripts: it sources `utils_function.sh` and sets
version defaults.
## Writing a command partial
Partials are function bodies, not standalone scripts. No shebang, no
`set -e`, no sourcing:
```bash
connector="${args[--connector]}"
verbose="${args[--verbose]}"
connector_type=$(playground state get run.connector_type)
if [[ ! -n "$connector" ]]
then
connector=$(playground get-connector-list)
fi
log "⏸️ Pausing $connector_type connector $connector"
```
- Parsed flags/args arrive in the `args` associative array, keyed by the **long
flag including dashes** (`${args[--connector]}`) or the arg name.
- Use `log` / `logwarn` / `logerror`, never bare `echo`.
- `$root_folder` (repo root) is set by `src/before.sh` and available everywhere.
- Call the CLI recursively — `playground state get …`, `playground connector
status …`. That is the normal idiom, not a smell.
- `src/before.sh` runs before every command; it handles `--vvv` and
`--output-level`.
## bashly.yml conventions in this repo
Read `references/bashly-yml.md` for the full vocabulary as it is actually used
here (anchors, validators, filters, groups, completions). The essentials:
- **Reuse the YAML anchors.** Common flags are defined once with `&tag`,
`&connector`, `&verbose`, `&environment`, `&enable-ksqldb`, … and referenced
as `*tag`, `*connector`. Never re-declare a flag that already has an anchor.
- **Help text is emoji-prefixed** (`🔗 Connector name`, `🗺️ Show a status`) and
written for end users. Multi-line help uses `|-` with a blank line between
paragraphs; `🎓 Tip:` marks hints.
- **`group:`** places a command under a heading in `playground --help`
(Connector, Container, Debug, Kafka, Run, Schema, Topic, Tools, …).
- **`private: true`** hides a command from help and docs. Used for the
`get-*-list` completion providers and for internal commands like
`bashly-reload` and `state`.
- **Completions** call those private commands:
`completions: [$(playground get-connector-list)]`.
- **`filters:`** declare preconditions (`docker_running`,
`connect_running`, `not_mdc_environment`, `ccloud_environment`, …).
- **`validate:`** attaches a validator to an arg/flag (`validate_not_empty`,
`validate_file_exists`, `validate_json`, `validate_minimal_cp_version`, …).
## Pitfalls
- **Docker must be running** for `bashly-reload`, `update-docs`, and the
`docker_running` filter.
- `bashly-reload` applies a `perl` fix to `completions.bash` because Bashly
escapes `"$cur"` in some completion helpers, and it fails loudly if the fix
didn't take. Don't patch `completions.bash` by hand — fix it in
`bashly-reload.sh` if it regresses.
- To test completions after a change:
`source scripts/cli/completions.bash`.
- A partial whose path doesn't match the command tree is silently never called.
- Private commands produce no documentation, so `update-docs` won't show them.
- Renaming or removing a flag is a breaking change for the ~300 example scripts
that call `playground …`; grep the repo before doing it.
Ver no GitHub