--- name: scripting-cli-automation description: Writing small scripts and command-line tools to automate repeatable tasks in Node and TypeScript (default) or shell - when to script versus do by hand, idempotent and safe-by-default behaviour (dry-run, confirmation for destructive actions), argument parsing, exit codes, stdout versus stderr, machine-readable JSON output, logging verbosity, config and environment variables, keeping secrets out of arguments, cross-platform paths, shell safety (quoting, strict mode, avoiding eval), testing scripts, usage via --help, script locations and pnpm package scripts, scheduled runs with failure alerts, and keeping scripts small and boring. Load when automating a repeated task, adding a script or CLI to a project, or reviewing a script for safety. --- # Scripting and CLI automation Types and style: `typescript-conventions`. Secrets: `secret-handling` and `application-security`. Script tests: `software-testing-strategy`. Dependencies: `dependency-management`. ## Script or not - Done twice by hand and likely a third time: script it. Done once and never again: do it by hand. - Script when errors are costly, steps are many (over 4), or an AI session must repeat it reliably. - Do not script what a built-in tool or existing package script already does. - Write the manual steps down first; the script is those steps made exact. ## Language - Node/TypeScript by default: typed, testable, cross-platform, same toolchain as the project. - Shell only for under about 30 lines of gluing commands. Past that, or with loops over structured data, switch to TypeScript. - Run TypeScript with the project's runner (for example `pnpm exec tsx scripts/x.ts`); do not add a build step for a script. ## Safe by default - Idempotent: running twice gives the same end state. Check before creating, use upsert, skip what exists. - Destructive actions (delete, overwrite, push, send) require `--yes` or an interactive confirmation; non-interactive runs without the flag abort. - Provide `--dry-run` that prints exactly what would change and touches nothing. Make dry-run the default for anything irreversible. - Validate inputs before the first change; fail early, change nothing on bad input. - Write outputs to a new file or temp path, then rename, so a crash leaves no half file. ## Interface - Positional args for the required subject; flags for options. Long flags (`--output`), short only for the common ones. - Use a parser (`node:util` `parseArgs`, or the project's CLI library); never hand-split `process.argv`. - Exit codes: 0 success, 1 failure, 2 bad usage. Distinct codes only when callers branch on them. - stdout holds results only; stderr holds logs, progress, and errors. This keeps pipes clean. - Offer `--json` for machine-readable output: a single JSON document on stdout, stable keys, no decoration. - Verbosity: quiet by default, `-v` for steps, `-vv` for debug; `--quiet` for none. - `--help` prints usage, flags, defaults, one example, and exit codes. Missing args print usage to stderr and exit 2. ## Config and secrets - Order of precedence: flags, environment variables, config file, defaults. Document it. - Secrets come from env vars or a secret store; never from arguments (visible in process lists and shell history) and never committed or logged. - Print resolved config with secrets masked in debug mode. ## Paths and portability - Use `node:path` (`path.join`, `path.resolve`), never string-concatenated separators. - Resolve relative to the script or repo root, not the current directory, unless the user passes a path. - Avoid platform-only commands in TypeScript; use Node APIs (`fs`, `child_process.spawn` with an args array, no shell string). ## Shell rules ```bash #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' dir="${1:?usage: clean.sh