Files
boshu2__agentops/scripts/generate-cli-reference.sh
T
Bo 041ca731d6 fix(scripts): generate-cli-reference.sh mawk portability (soc-o73v) (#323)
## What

One-line awk fix + 3 regression tests. Closes `soc-o73v`.

## Why

`scripts/generate-cli-reference.sh` line 60 used POSIX interval
expression `[[:space:]]{2,}`. Older mawk (Ubuntu LTS default,
pre-1.3.4-20240123) **silently rejects** intervals and produces an
empty/14-line `COMMANDS.md` instead of the full 4455-line command
reference.

CI runs on gawk → green. Local dev on Ubuntu mawk → empty file. No
signal until you actually inspected the output.

## Fix

```diff
- /^[[:space:]]{2,}[a-z0-9][a-z0-9-]*([[:space:]]+|$)/ {
+ /^[[:space:]][[:space:]]+[a-z0-9][a-z0-9-]*([[:space:]]+|$)/ {
```

Output is **byte-identical** under both awks. Validated by re-running
the script: `git diff cli/docs/COMMANDS.md` is empty.

## Test coverage

`tests/scripts/generate-cli-reference-portability.bats` (3 tests):

1. **'script does not use POSIX interval expressions ({N,} / {N,M})'** —
regex-greps the script for re-introduction
2. **'command-extraction regex matches indented commands under both
awks'** — runs gawk + mawk against the same fixture and verifies
identical output
3. **'regex does not match single-space indents or unindented lines'** —
guards the intent (≥2-space indent is the signal)

## Discovered

Harvested from `.agents/rpi/next-work.jsonl` set
`discovery-2026-05-12-ddd-hexagonal`, item #4 (low severity). Surfaced
during /evolve cycle 197 on 2026-05-18.

## Validation

- ✅ `bats tests/scripts/generate-cli-reference-portability.bats` — 3/3
PASS
- ✅ `scripts/pre-push-gate.sh --fast` — passed (1 pre-existing warn
unrelated)
- ✅ Byte-identical `COMMANDS.md` output before/after

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Codex <codex@example.invalid>
2026-05-18 21:07:08 +00:00

274 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Generate cli/docs/COMMANDS.md from ao --help output.
# Usage:
# ./scripts/generate-cli-reference.sh # Generate COMMANDS.md
# ./scripts/generate-cli-reference.sh --check # Check if committed version is current
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CLI_DIR="$REPO_ROOT/cli"
OUTPUT="$REPO_ROOT/cli/docs/COMMANDS.md"
CHECK_MODE=false
TMPFILE=""
TMP_AO_DIR=""
AO_BIN=""
if [[ "${1:-}" == "--check" ]]; then
CHECK_MODE=true
fi
if ! command -v go >/dev/null 2>&1; then
echo "ERROR: go is required to generate CLI docs." >&2
exit 1
fi
cleanup() {
[[ -n "$TMPFILE" ]] && rm -f "$TMPFILE"
[[ -n "$TMP_AO_DIR" ]] && rm -rf "$TMP_AO_DIR"
}
trap cleanup EXIT
build_ao() {
TMP_AO_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ao-docgen.XXXXXX")"
AO_BIN="$TMP_AO_DIR/ao-docgen"
(
cd "$CLI_DIR"
go build -o "$AO_BIN" ./cmd/ao
)
}
extract_commands() {
awk '
/^Usage:/ {after_usage=1; next}
!after_usage {next}
/^Flags:/ {exit}
/^Global Flags:/ {exit}
/^Additional help topics:/ {exit}
/^Use "/ {exit}
/^Available Commands:/ {in_commands=1; next}
in_commands && /^$/ {in_commands=0; next}
in_commands && NF > 0 {
cmd=$1
if (!seen[cmd]++) print cmd
next
}
/^[[:space:]][[:space:]]+[a-z0-9][a-z0-9-]*([[:space:]]+|$)/ {
cmd=$1
if (cmd != "ao" && cmd !~ /^-/ && cmd !~ /:$/ && !seen[cmd]++) print cmd
}
'
}
extract_usage() {
awk '
/^Usage:/ {
line=$0
sub(/^Usage:[[:space:]]*/, "", line)
if (line != "") print line
in_usage=1
next
}
in_usage {
if ($0 ~ /^$/) {
in_usage=0
next
}
line=$0
sub(/^[[:space:]]+/, "", line)
if (line != "") print line
}
' | awk '
/^ao / {
all[++all_n]=$0
if ($0 !~ />/) preferred[++pref_n]=$0
}
END {
if (pref_n > 0) print preferred[pref_n]
else if (all_n > 0) print all[all_n]
}
'
}
extract_flags() {
sed -n '/^Flags:/,/^$/{ /^Flags:/d; /^$/d; p; }'
}
extract_aliases() {
sed -n '/^Aliases:/,/^$/{ /^Aliases:/d; /^$/d; p; }'
}
has_non_help_flags() {
grep -Ev '^[[:space:]]*-h,[[:space:]]*--help[[:space:]]+help for' | grep -Ev '^[[:space:]]*$'
}
first_line() {
local text="$1"
printf '%s\n' "${text%%$'\n'*}"
}
heading_for_depth() {
local depth="$1"
local level=$((depth + 2))
local heading=""
local i
if ((level > 6)); then
level=6
fi
for ((i = 0; i < level; i++)); do
heading+="#"
done
printf '%s' "$heading"
}
command_label() {
local label="ao"
local part
for part in "$@"; do
label+=" $part"
done
printf '%s' "$label"
}
emit_command_reference() {
local depth="$1"
shift
local -a command_path=("$@")
local cmd_help
cmd_help="$("$AO_BIN" "${command_path[@]}" --help 2>&1 || true)"
local description
description="$(first_line "$cmd_help")"
local heading
heading="$(heading_for_depth "$depth")"
local label
label="$(command_label "${command_path[@]}")"
echo "${heading} \`${label}\`"
echo ""
echo "$description"
echo ""
local usage
usage="$(echo "$cmd_help" | extract_usage || true)"
if [[ -n "$usage" ]]; then
echo '```'
echo "$usage"
echo '```'
echo ""
fi
local aliases
aliases="$(echo "$cmd_help" | extract_aliases || true)"
if [[ -n "$aliases" ]]; then
echo "**Aliases:**"
echo ""
echo '```'
echo "$aliases"
echo '```'
echo ""
fi
local flags_block
flags_block="$(echo "$cmd_help" | extract_flags || true)"
if echo "$flags_block" | has_non_help_flags >/dev/null 2>&1; then
echo "**Flags:**"
echo ""
echo '```'
echo "$flags_block"
echo '```'
echo ""
fi
local subcmds
subcmds="$(echo "$cmd_help" | extract_commands | grep -v '^$' || true)"
if [[ -n "$subcmds" ]]; then
if [[ "$depth" -eq 1 ]]; then
echo "**Subcommands:**"
echo ""
fi
local sub
while IFS= read -r sub; do
[[ -z "$sub" ]] && continue
emit_command_reference "$((depth + 1))" "${command_path[@]}" "$sub"
done <<<"$subcmds"
fi
}
generate() {
cat <<DOC_HEADER
# ao CLI Reference
> Auto-generated by \`scripts/generate-cli-reference.sh\`.
> Do not edit manually. Re-run the script to update.
DOC_HEADER
local top_help
top_help="$("$AO_BIN" --help 2>&1)"
cat <<'DOC_GLOBAL_FLAGS'
## Global Flags
DOC_GLOBAL_FLAGS
echo "$top_help" | extract_flags | while IFS= read -r line; do
echo " $line"
done
echo ""
local commands
commands="$(echo "$top_help" | extract_commands | grep -v '^$')"
cat <<'DOC_COMMANDS'
---
## Commands
DOC_COMMANDS
for cmd in $commands; do
emit_command_reference 1 "$cmd"
echo "---"
echo ""
done
}
build_ao
if $CHECK_MODE; then
TMPFILE="$(mktemp)"
generate > "$TMPFILE"
if diff -q "$OUTPUT" "$TMPFILE" >/dev/null 2>&1; then
echo "cli/docs/COMMANDS.md is up to date."
exit 0
fi
echo "cli/docs/COMMANDS.md is out of date. Run: ./scripts/generate-cli-reference.sh" >&2
diff -u "$OUTPUT" "$TMPFILE" || true
exit 1
fi
mkdir -p "$(dirname "$OUTPUT")"
generate > "$OUTPUT"
echo "Generated $OUTPUT"
# Auto-sync the declared CLI heading count in docs/cli-skills-map.md so a
# fresh COMMANDS.md regen can never leave the count gate red. validate-cli-
# skills-map.sh --fix is a no-op when counts already match.
if [[ -x "$SCRIPT_DIR/validate-cli-skills-map.sh" ]]; then
bash "$SCRIPT_DIR/validate-cli-skills-map.sh" --fix >/dev/null
fi