326 lines
15 KiB
Markdown
326 lines
15 KiB
Markdown
---
|
|
name: cli-tooling
|
|
description: >-
|
|
Use this skill whenever invoking a command-line tool, choosing between a CLI and an ad-hoc
|
|
script, writing a standalone utility script, or orienting on unfamiliar work before starting.
|
|
Covers discovering a CLI's real interface instead of guessing flags, non-interactive and
|
|
machine-readable invocation, checking an API's payload rather than its exit status, keeping
|
|
credentials out of argv, eventually-consistent systems that briefly report the old state,
|
|
script placement and symlinking, and care around destructive or outward-facing commands —
|
|
including additive changes that silently take over a name already serving traffic.
|
|
---
|
|
|
|
# CLI & Tooling — Discovery, Invocation, and Script Placement
|
|
|
|
How to use command-line tools correctly, and where utility scripts belong.
|
|
Companion to [`coding-dev.md`](skills-public/coding-dev.md), which covers writing the code itself.
|
|
|
|
---
|
|
|
|
## 1. Discovery Over Guessing
|
|
|
|
> [!IMPORTANT]
|
|
> **Read the tool's own help before using it. Never guess subcommands or flags.**
|
|
> ```bash
|
|
> <tool> --help
|
|
> <tool> <subcommand> --help
|
|
> ```
|
|
|
|
A tool's built-in help is authoritative and current; memory and training data are neither.
|
|
Flags get renamed, subcommands get restructured, and a confidently-invented flag either
|
|
errors out or — far worse — silently means something else.
|
|
|
|
**This applies to listing capabilities, too.** When asked what a tool can do, run its help
|
|
and report what it actually says. Do not produce a speculative command list; a plausible-
|
|
looking invented flag is indistinguishable from a real one to whoever reads it next.
|
|
|
|
### Walk the whole help tree
|
|
|
|
Multi-level CLIs hide most of their surface one level down. `<tool> --help` shows the
|
|
subcommand list; the flags that matter usually live in `<tool> <subcommand> --help`.
|
|
|
|
### Don't reinvent a tool that exists
|
|
|
|
**When a dedicated CLI already provides the functionality, use it** rather than writing an
|
|
ad-hoc script, a one-off `curl` pipeline, or a bespoke API client. The CLI already handles
|
|
authentication, retries, pagination, error formatting, and the edge cases you have not
|
|
thought of yet.
|
|
|
|
Before writing a script to do something, check whether:
|
|
- a dedicated CLI for that system is already installed,
|
|
- the tool you are already using has a subcommand for it,
|
|
- the project's own `scripts/` directory already contains it.
|
|
|
|
### Desktop Steam Client Lifecycle (`steamcli`)
|
|
|
|
> [!TIP]
|
|
> **Never spawn Steam via raw shell backgrounding (`nohup steam &` or `steam -silent &`) or kill it with `pkill`.**
|
|
> On desktop Linux/Bazzite, raw spawning leaves orphaned bash wrappers, misses custom client flags, or fails to initialize properly.
|
|
> Instead, manage the desktop Steam client through **`steamcli client`**:
|
|
> ```bash
|
|
> # Graceful exit and wait for it to finish flushing configuration:
|
|
> steamcli client shutdown
|
|
>
|
|
> # Launch the desktop client cleanly with configured defaults:
|
|
> steamcli client launch
|
|
>
|
|
> # Launch with custom Steam arguments:
|
|
> steamcli client launch -- -silent
|
|
> ```
|
|
|
|
|
|
---
|
|
|
|
## 2. Non-Interactive Invocation
|
|
|
|
Anything run from a script, a CI job, or an agent must not depend on a human being present.
|
|
|
|
- **Pass required targets explicitly.** A CLI that manages multiple objects will, when no
|
|
target is given, either prompt interactively or fail — both of which break automation.
|
|
Always pass `--server <id>`, `--profile <name>`, `--target <x>` rather than relying on a
|
|
default or a prompt.
|
|
- **Use the tool's own blocking flag** (`--wait`, `--follow`, `--no-detach`) when you need an
|
|
operation to finish before the next step. Do not build a polling loop around a command that
|
|
already knows how to wait — and never write a shell `until`/`while` wait loop
|
|
(see [`code-languages/bash.md`](skills-public/code-languages/bash.md)).
|
|
- **Prefer a `--dry-run` rehearsal** for anything that deploys, releases, or deletes, when the
|
|
tool offers one.
|
|
- Set `--yes` / `--non-interactive` deliberately and only where the action is genuinely safe —
|
|
not as a reflex to stop a tool from asking.
|
|
|
|
---
|
|
|
|
## 3. Machine-Readable Output
|
|
|
|
When output feeds another program — or an agent's context — ask the tool for a structured
|
|
format rather than parsing its human-facing table.
|
|
|
|
```bash
|
|
tool list -o json # full fidelity
|
|
tool list -o compact # minimal tokens, good for agent context
|
|
tool list -o csv # structured columns for a spreadsheet or script
|
|
```
|
|
|
|
- **Never scrape a table meant for humans.** Column widths, truncation, colour codes, and
|
|
alignment all change without notice; a structured format is a contract.
|
|
- **Check the payload, not just the exit status.** Plenty of HTTP APIs answer `200 OK` with
|
|
`{"success": false, "errors": [...]}` in the body, and `curl` exits `0` because the
|
|
*request* worked. A script that trusts the exit code reports a clean success and writes
|
|
nothing. Assert on the field the API uses for this, and print `errors[]` when it trips:
|
|
```bash
|
|
if [[ "$(jq -r '.success' <<<"$response")" != true ]]; then
|
|
jq -r '.errors[]? | " - \(.message)"' <<<"$response" >&2
|
|
exit 1
|
|
fi
|
|
```
|
|
- Strip colour explicitly (`--color never`, `NO_COLOR=1`) when capturing output, or escape
|
|
sequences end up in your parsed values.
|
|
- Keep in mind that a "compact" or summary format is the right default for feeding an agent,
|
|
while `json` is right for a script that needs every field.
|
|
|
|
> [!WARNING]
|
|
> **Never pipe build, lint, or test output through a filter to make it shorter.** That is a
|
|
> different situation and the rule there is the opposite — see
|
|
> [`coding-dev.md` §2](skills-public/coding-dev.md). Structured output flags are for data; `grep` on
|
|
> diagnostics hides warnings.
|
|
|
|
---
|
|
|
|
## 4. Orient Before Starting
|
|
|
|
Before beginning work in an unfamiliar project or one you have not touched recently, spend a
|
|
moment establishing what has already happened. Cheap, and it prevents the most expensive
|
|
failure mode: rebuilding something that already exists.
|
|
|
|
In rough order of value:
|
|
1. **The project's own instructions** — `AGENTS.md`, `CLAUDE.md`, `.rules`, `CONTRIBUTING.md`,
|
|
`README.md`. Read them before touching code.
|
|
2. **Recent history** — `git log --oneline -20`, and `git status --short` to see what is
|
|
already in flight so you do not clobber it.
|
|
3. **Prior work records** — session/conversation history tooling, issue trackers, or notes
|
|
files, scoped to the project directory.
|
|
4. **The code itself** — `grep` for the symbol before trusting any document that says it is
|
|
missing.
|
|
5. **Whether the thing is already running** — before editing a live app's config or starting a
|
|
second copy. Ask the supervisor (`systemctl --user is-active <unit>`) and, for a bare
|
|
process, `pgrep -ax <name>` — never bare `pgrep -f <name>`, which matches your own shell and
|
|
reads as a false positive. See
|
|
[`code-languages/bash.md`](code-languages/bash.md#pgrep--f-matches-its-own-shell).
|
|
|
|
> [!CAUTION]
|
|
> **Task documents are leads, not facts.** A `TODO.md` entry can be stale, duplicated, or
|
|
> simply wrong — including when it sounds confident and especially when it sounds familiar.
|
|
> Verify against the code before acting on one. A single `grep` is cheaper than rediscovering
|
|
> that the work shipped last week.
|
|
|
|
---
|
|
|
|
## 5. Where Scripts Live
|
|
|
|
> [!IMPORTANT]
|
|
> **Never write a standalone script directly into a `PATH` directory** (`~/.local/bin`,
|
|
> `/usr/local/bin`, or similar). Write it into a **dedicated scripts directory** that is
|
|
> version-controlled, then **symlink** it into `PATH`.
|
|
|
|
```bash
|
|
# write the script to its real home
|
|
$EDITOR /path/to/scripts/my-tool.sh
|
|
chmod +x /path/to/scripts/my-tool.sh
|
|
|
|
# then expose it
|
|
ln -s /path/to/scripts/my-tool.sh ~/.local/bin/my-tool
|
|
```
|
|
|
|
**Why:** one versioned source of truth. A script living directly in `PATH` is invisible to
|
|
version control, easy to clobber on a reinstall, impossible to diff, and indistinguishable
|
|
from a package-manager-owned binary sitting beside it. The symlink makes the provenance
|
|
obvious — `ls -l` shows exactly where the real file is.
|
|
|
|
The same applies within a project: scripts belong in `scripts/`, referenced by the build
|
|
system, not scattered at the repository root.
|
|
|
|
---
|
|
|
|
## 6. Destructive and Outward-Facing Commands
|
|
|
|
- **Look at the target before deleting or overwriting it.** Inspect it first — the cost of a
|
|
wrong `rm` or a wrong overwrite is unbounded and the check is one command.
|
|
- **Read-only first.** When exploring an unfamiliar tool, use its inspection subcommands
|
|
(`status`, `list`, `show`, `get`) before any mutating one.
|
|
- **Mutating commands that rewrite shared configuration** — syncing settings across machines,
|
|
purging caches or state, patching installed applications — should be confirmed before
|
|
running, even when they are reversible in principle.
|
|
- **Never use real data as scratch space.** Real accounts, real configuration directories,
|
|
live services, and the user's actual files are not disposable test fixtures. Use temporary
|
|
directories and disposable targets.
|
|
- Anything that sends, publishes, deploys, or purchases is outward-facing: confirm first, and
|
|
treat approval as applying to that one action rather than the category.
|
|
|
|
### Adding something can take something over
|
|
|
|
> [!CAUTION]
|
|
> **A change that deletes nothing can still break a live service.** The dangerous operations
|
|
> are not only the ones named `delete`. Adding a routing rule, a proxy entry, a redirect, a
|
|
> DNS override or a higher-priority config can silently intercept a name that is already
|
|
> serving traffic — while the thing it shadows still looks perfectly healthy in every
|
|
> listing, because it was never touched.
|
|
>
|
|
> That is the worst shape a failure can take: the old config is still there, still correct,
|
|
> still pointing where it should, and the service is dead anyway. Nothing in the inventory
|
|
> says what happened.
|
|
|
|
**Before claiming a name, ask what currently answers to it** — and refuse by default when
|
|
something does:
|
|
|
|
```bash
|
|
# Only an unrouteable placeholder, or an entry this tool made, is safe to assume is free.
|
|
if [[ "$(current_target "$host")" != "$PLACEHOLDER" && "$(owner "$host")" != "$MARKER" ]]; then
|
|
[[ "$FORCE" == true ]] || die "$host already serves $(current_target "$host") — pass --force"
|
|
fi
|
|
```
|
|
|
|
Two things make that check possible:
|
|
|
|
- **Mark what you create.** A comment field, a tag, a description prefix — anything that
|
|
distinguishes *your* entries from ones a human made. It turns "is this mine to replace?"
|
|
into a lookup rather than a guess, and it is what lets the matching `--delete` refuse to
|
|
remove something it did not create.
|
|
- **Make `--force` the only way past**, and name in the refusal what is there now. A tool that
|
|
silently overwrites is one nobody can safely run against production.
|
|
|
|
> [!TIP]
|
|
> Test such a tool against a name you **expect** it to reject, not only against a free one.
|
|
> The rejection path is where the damage would have been, and it is the half a happy-path test
|
|
> never exercises.
|
|
|
|
---
|
|
|
|
## 7. Credentials Belong on stdin, Not in `argv`
|
|
|
|
> [!WARNING]
|
|
> **`argv` is world-readable.** Any user on the box can run `ps` and read the full command
|
|
> line of every process. A token passed as `-H "Authorization: Bearer $TOKEN"`, `--password`,
|
|
> or `--token` is therefore exposed for as long as the call runs — and lands in shell history
|
|
> and process accounting too.
|
|
|
|
Most CLIs that accept a secret as a flag also accept it somewhere safer. Use that instead:
|
|
|
|
```bash
|
|
# WRONG — the token is visible in `ps` for the life of the request
|
|
curl -H "Authorization: Bearer $TOKEN" "$url"
|
|
|
|
# RIGHT — curl reads its options from stdin; argv holds nothing sensitive
|
|
printf 'header = "Authorization: Bearer %s"\n' "$TOKEN" | curl --config - "$url"
|
|
```
|
|
|
|
The equivalent elsewhere: `--password-stdin` (docker login, many package tools), `-u` with a
|
|
`.netrc`, `--token-file`, or simply an environment variable the tool reads by name. Reach for
|
|
the env var when nothing better exists — it is readable through `/proc/<pid>/environ` but only
|
|
by the owner and root, which is a meaningfully smaller exposure than `argv`.
|
|
|
|
See [`code-languages/bash.md` §8](skills-public/code-languages/bash.md) for the surrounding rules on
|
|
where secrets are stored and why they are never echoed.
|
|
|
|
---
|
|
|
|
## 8. Eventually-Consistent Systems Lie Briefly
|
|
|
|
Edge networks, DNS, CDNs, object stores, container registries and clustered schedulers all
|
|
acknowledge a write before it is visible everywhere. **A read taken immediately after a
|
|
successful write is not evidence of anything.**
|
|
|
|
The failure mode is believing the tool is broken and "fixing" it:
|
|
|
|
- A changed rule still returns the **old** value for a while — which reads as "my write was
|
|
ignored".
|
|
- A brand-new name returns an **error** for a while — a 5xx, an NXDOMAIN, a 404 — which reads
|
|
much worse than it is.
|
|
|
|
So: take the write's own success response as the result, and treat the first few reads as
|
|
unreliable. When a verification read matters, **say how long propagation takes** in the tool's
|
|
own help or header comment, so the next person does not rediscover it at 2am. State it as a
|
|
number — "~30s for a rule, ~40s for a new name" — because "eventually" tells nobody when to
|
|
start worrying.
|
|
|
|
Do not wrap this in a polling loop (see
|
|
[`code-languages/bash.md` §3](skills-public/code-languages/bash.md)); take one sample per
|
|
invocation and let the caller run it again.
|
|
|
|
> [!WARNING]
|
|
> **Checking too early can make the delay much worse.** A failed lookup is itself cached. Query
|
|
> a DNS name before it exists and the resolver stores the `NXDOMAIN` for the SOA's negative TTL
|
|
> — commonly 300-1800s — so the name stays invisible to *you* long after it is live for
|
|
> everyone else. Polling during propagation is what creates this, which is a second reason not
|
|
> to loop.
|
|
>
|
|
> When a check does come back negative, confirm where the negative lives before concluding
|
|
> anything: query the authoritative source directly and compare. The answer is often "the
|
|
> record is fine, my resolver is lying", and flushing the local cache will not help if the
|
|
> stale entry is on an upstream resolver you do not control.
|
|
>
|
|
> ```bash
|
|
> dig +short @1.1.1.1 "$name" # authoritative-ish: is it actually published?
|
|
> dig +short "$name" # what my resolver believes
|
|
> curl --resolve "$name:443:<ip>" "https://$name/" # bypass DNS entirely to test the service
|
|
> ```
|
|
|
|
---
|
|
|
|
## 9. Environment Constraints
|
|
|
|
- **Entering a container or changing shells resets the working directory.** Chain `cd` inside
|
|
the same command string rather than relying on it persisting:
|
|
```bash
|
|
container-run <name> -- bash -c "cd /path/to/project && make"
|
|
```
|
|
- **Verify what actually exists rather than assuming from documentation.** Container names,
|
|
tool versions, and installed toolchains drift. A one-command check (`<tool> list`,
|
|
`<tool> --version`, `command -v <tool>`) costs nothing and prevents a confident invocation
|
|
of something that was removed months ago.
|
|
- **Check for an optional dependency before the step that needs it**, and fail loudly if it
|
|
is missing. A packaging step that silently skips when its tool is absent ships an
|
|
incomplete release.
|
|
- **Don't saturate the machine.** A full build consuming every core makes the workstation
|
|
unusable. When something latency-sensitive is running, reduce the job count and `nice` the
|
|
work — and say that you did.
|