Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .cursor/skills/verify-cf/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
name: verify-cf
description: "Drive the Cloudflare CLI (`cf`) the way a user/agent does — launch deps, doctor health, run real commands with `--json`, and capture exit/stdout/stderr proof. Use when verifying conduit-ui/cloudflare behavior, agent JSON envelopes, setup, zones, DNS, or tunnels."
---

# verify-cf

Project-local verification for the **Cloudflare management CLI** (`./cf`). Primary surface is the terminal. There is no local HTTP server, database, or long-lived daemon. Cloudflare's HTTP API is the production boundary; live network calls need `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`. Without credentials, only missing-creds / setup-validation journeys are driveable against the real binary.

Helper (always prefer this over ad-hoc paths):

```bash
.cursor/skills/verify-cf/bin/verify-cf <launch|doctor|drive|isolate-setup|cleanup|evidence-dir>
```

Evidence root: `.cursor/skills/verify-cf/artifacts/<run-id>/` (survives cleanup). Set `CF_VERIFY_RUN_ID` to pin a run id across commands.

## Launch

No server to keep alive. Launch means: PHP 8.3+, Composer deps installed, `./cf` executable and advertising commands.

```bash
export CF_VERIFY_RUN_ID="${CF_VERIFY_RUN_ID:-$(date +%Y%m%dT%H%M%S)-$$}"
.cursor/skills/verify-cf/bin/verify-cf launch
# ready marker: "verify-cf ready repo=... run=... evidence=..."
```

Teardown for isolate trees only:

```bash
.cursor/skills/verify-cf/bin/verify-cf cleanup
```

Cleanup never deletes evidence under `artifacts/`.

## Doctor

Read-only. Run first whenever anything looks off:

```bash
.cursor/skills/verify-cf/bin/verify-cf doctor
```

Worth driving only when exit `0` and output includes `vendor=ok`, `entrypoint=ok`, `cf_list=ok`. `live_creds=present` means zones/DNS/tunnel live journeys may proceed; `live_creds=absent` means stick to missing-creds / setup failure journeys (or Pest mocks for offline CI — not a substitute for a mapped live path).

## Drive

Harness: the real `./cf` process via the helper. Always pass `--json` for agent-facing commands (see `docs/agent-output.md`). Prefer process env for credentials over writing the project `.env`.

```bash
.cursor/skills/verify-cf/bin/verify-cf drive zones --json
# prints: exit=N stdout=... stderr=... meta=...
```

For `setup` (writes `.env`), **never** use the developer's checkout `.env`. Create an isolate first:

```bash
ISOLATE="$(.cursor/skills/verify-cf/bin/verify-cf isolate-setup)"
# or: export CF_BIN="$(cat artifacts/<run>/isolate_home.txt)/cf" after isolate-setup
export CF_BIN="$(.cursor/skills/verify-cf/bin/verify-cf isolate-setup | sed -n 's/^isolate=//p')/cf"
.cursor/skills/verify-cf/bin/verify-cf drive setup --token=bad --account-id=bad --non-interactive --json
```

Stable handles: command names (`zones`, `dns:list`, `setup`), flags (`--json`, `--non-interactive`, `--force`), and JSON keys (`ok`, `data`, `error`). Do not scrape human tables when `--json` is available.

`setup --json` uses a different success shape (`success` / `message` / `account_id`), not `{ok,data}` — see feature map.

## Evidence

Proof lives under `.cursor/skills/verify-cf/artifacts/<run-id>/`:

| File | Contents |
|------|----------|
| `*.stdout` | Full stdout (JSON envelopes) |
| `*.stderr` | Full stderr |
| `*.meta.json` | `exit_code` + argv |

Proof standards:

1. Exercise the real `./cf` user path (not `Artisan::call` inside Pest) for skill-driven proofs.
2. Capture the command (meta argv), the exit code, and the resulting stdout/stderr — not only a summary.
3. For mutations (DNS/tunnel create/delete, successful setup), follow with a second read (`dns:list`, `zones`, or re-read isolate `.env`) that shows the side effect.
4. Mocks belong in Pest (`composer test`) at the Saloon/Cloudflare HTTP boundary. Do not mock inside a verify-cf drive. Live API failures with valid-looking creds are real failures.
5. When `live_creds=absent`, report live journeys as unmet preconditions — do not weaken them to a mocked Pest test and call them verified.

## Cleanup

```bash
.cursor/skills/verify-cf/bin/verify-cf cleanup
```

Removes only `/tmp/cf-verify-$CF_VERIFY_RUN_ID` (or `CF_VERIFY_HOME`) created by `isolate-setup`. Does not kill by process name (nothing long-lived). Does not delete `artifacts/`.

## Helpers

| Invocation | Purpose |
|------------|---------|
| `bin/verify-cf launch` | Install deps if needed; assert `cf list` usable |
| `bin/verify-cf doctor` | Read-only health |
| `bin/verify-cf drive <args...>` | Run `./cf` + capture evidence |
| `bin/verify-cf isolate-setup` | Disposable tree + vendor symlink for setup |
| `bin/verify-cf evidence-dir` | Print current evidence directory |
| `bin/verify-cf cleanup` | Drop isolate home; keep evidence |

## Feature map

See [`features/README.md`](features/README.md). Drive the mapped entry points there; a proof that uses one convenient command is incomplete when the map lists others for that feature.
2 changes: 2 additions & 0 deletions .cursor/skills/verify-cf/artifacts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
202 changes: 202 additions & 0 deletions .cursor/skills/verify-cf/bin/verify-cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
# Helpers for the verify-cf skill. Invoke from anywhere; resolves the repo root.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# .cursor/skills/verify-cf → repository root
REPO_ROOT="$(cd "$SKILL_DIR/../../.." && pwd)"

CF="${CF_BIN:-$REPO_ROOT/cf}"
EVIDENCE_ROOT="${CF_VERIFY_EVIDENCE:-$SKILL_DIR/artifacts}"
RUN_ID="${CF_VERIFY_RUN_ID:-$(date +%Y%m%dT%H%M%S)-$$}"
RUN_DIR="$EVIDENCE_ROOT/$RUN_ID"
ISOLATE_ROOT="${CF_VERIFY_HOME:-/tmp/cf-verify-$RUN_ID}"

usage() {
cat <<EOF
Usage: verify-cf <command> [args]

Commands:
launch Ensure PHP/Composer deps; print ready marker
doctor Read-only health check (exit 0 only if driveable)
drive <cmd...> Run ./cf with args; capture exit/stdout/stderr under artifacts
evidence-dir Print this run's evidence directory
cleanup Remove isolate home created by this run (keeps evidence)
isolate-setup Create disposable copy for setup/.env mutation proofs

Env:
CF_BIN Path to cf entrypoint (default: <repo>/cf)
CF_VERIFY_EVIDENCE Evidence root (default: skill artifacts/)
CF_VERIFY_RUN_ID Stable run id (default: timestamp-pid)
CF_VERIFY_HOME Disposable home for isolate-setup
CLOUDFLARE_API_TOKEN Optional live credentials (process env preferred)
CLOUDFLARE_ACCOUNT_ID Optional live credentials
EOF
}

ensure_run_dir() {
mkdir -p "$RUN_DIR"
}

cmd_launch() {
if ! command -v php >/dev/null; then
echo "doctor-fail: php not on PATH" >&2
exit 1
fi
php_ver="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')"
if [[ "$(printf '%s\n' "$php_ver" "8.3" | sort -V | head -n1)" != "8.3" ]]; then
echo "doctor-fail: need PHP >= 8.3 (got $php_ver)" >&2
exit 1
fi
if [[ ! -f "$REPO_ROOT/vendor/autoload.php" ]]; then
(cd "$REPO_ROOT" && composer install --no-interaction)
fi
if [[ ! -x "$CF" && -f "$CF" ]]; then
chmod +x "$CF"
fi
if [[ ! -f "$CF" ]]; then
echo "doctor-fail: missing cf entrypoint at $CF" >&2
exit 1
fi
# Ready = usable CLI, not merely that PHP starts.
out="$("$CF" list 2>&1)" || {
echo "doctor-fail: cf list failed" >&2
echo "$out" >&2
exit 1
}
if ! grep -qE '^[[:space:]]*zones[[:space:]]' <<<"$out"; then
echo "doctor-fail: cf list did not advertise zones" >&2
echo "$out" >&2
exit 1
fi
ensure_run_dir
echo "verify-cf ready repo=$REPO_ROOT run=$RUN_ID evidence=$RUN_DIR"
}

cmd_doctor() {
local fail=0
echo "repo=$REPO_ROOT"
echo "cf=$CF"
echo "run_id=$RUN_ID"
echo "evidence=$RUN_DIR"
if ! command -v php >/dev/null; then
echo "php=MISSING"; fail=1
else
echo "php=$(php -v | head -n1)"
fi
if [[ -f "$REPO_ROOT/vendor/autoload.php" ]]; then
echo "vendor=ok"
else
echo "vendor=MISSING"; fail=1
fi
if [[ -f "$CF" ]]; then
echo "entrypoint=ok"
else
echo "entrypoint=MISSING"; fail=1
fi
if list_out="$("$CF" list 2>&1)"; then
if grep -qE '^[[:space:]]*zones[[:space:]]' <<<"$list_out"; then
echo "cf_list=ok"
else
echo "cf_list=UNEXPECTED"; fail=1
fi
else
echo "cf_list=FAIL"; fail=1
fi
if [[ -n "${CLOUDFLARE_API_TOKEN:-}" && -n "${CLOUDFLARE_ACCOUNT_ID:-}" ]]; then
echo "live_creds=present"
# Do not treat API failure as doctor failure — token may be invalid; drive journeys decide.
if zones_out="$(env CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CLOUDFLARE_ACCOUNT_ID" "$CF" zones --json 2>&1)"; then
echo "zones_json=ok"
else
echo "zones_json=error"
echo "zones_json_body=$(echo "$zones_out" | tr '\n' ' ' | head -c 200)"
fi
else
echo "live_creds=absent (read-only missing-creds journeys only)"
fi
# Refuse to claim ownership of a foreign long-lived process: this CLI has none.
echo "isolation=process-local (no daemon); setup mutations require isolate-setup"
exit "$fail"
}

cmd_drive() {
if [[ $# -lt 1 ]]; then
echo "drive requires cf arguments, e.g. verify-cf drive zones --json" >&2
exit 2
fi
ensure_run_dir
local stamp slug
stamp="$(date +%H%M%S)"
slug="$(printf '%s' "$*" | tr -c 'A-Za-z0-9._:-' '_' | cut -c1-80)"
local base="$RUN_DIR/${stamp}_${slug}"
local stdout_f="${base}.stdout"
local stderr_f="${base}.stderr"
local meta_f="${base}.meta.json"
set +e
"$CF" "$@" >"$stdout_f" 2>"$stderr_f"
local code=$?
set -e
python3 - "$meta_f" "$code" "$CF" "$@" <<'PY'
import json, sys
meta_path = sys.argv[1]
code = int(sys.argv[2])
argv = sys.argv[3:]
with open(meta_path, "w", encoding="utf-8") as f:
json.dump({"exit_code": code, "argv": argv}, f, indent=2)
f.write("\n")
PY
echo "exit=$code stdout=$stdout_f stderr=$stderr_f meta=$meta_f"
return "$code"
}

cmd_isolate_setup() {
# Disposable tree so setup can write .env without touching the developer's project .env
rm -rf "$ISOLATE_ROOT"
mkdir -p "$ISOLATE_ROOT"
# Copy app sources; share vendor via symlink to keep isolate cheap.
for item in app bootstrap config cf composer.json composer.lock box.json; do
if [[ -e "$REPO_ROOT/$item" ]]; then
cp -a "$REPO_ROOT/$item" "$ISOLATE_ROOT/"
fi
done
ln -s "$REPO_ROOT/vendor" "$ISOLATE_ROOT/vendor"
chmod +x "$ISOLATE_ROOT/cf"
ensure_run_dir
echo "$ISOLATE_ROOT" >"$RUN_DIR/isolate_home.txt"
echo "isolate=$ISOLATE_ROOT"
}

cmd_cleanup() {
if [[ -d "$ISOLATE_ROOT" ]]; then
rm -rf "$ISOLATE_ROOT"
echo "removed isolate $ISOLATE_ROOT"
else
echo "no isolate at $ISOLATE_ROOT"
fi
# Never delete evidence.
echo "evidence retained at $RUN_DIR"
}

cmd_evidence_dir() {
ensure_run_dir
echo "$RUN_DIR"
}

main() {
local cmd="${1:-}"
shift || true
case "$cmd" in
launch) cmd_launch "$@" ;;
doctor) cmd_doctor "$@" ;;
drive) cmd_drive "$@" ;;
isolate-setup) cmd_isolate_setup "$@" ;;
cleanup) cmd_cleanup "$@" ;;
evidence-dir) cmd_evidence_dir "$@" ;;
-h|--help|help|"") usage; [[ -n "$cmd" ]] || exit 2 ;;
*) echo "unknown command: $cmd" >&2; usage; exit 2 ;;
esac
}

main "$@"
46 changes: 46 additions & 0 deletions .cursor/skills/verify-cf/features/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Cloudflare CLI (`cf`) verification map

This directory is the maintained source for verifying user-facing behavior of `conduit-ui/cloudflare`. Read this index before driving the app, then use the matching feature file as the recipe.

## Baseline preconditions

- Repo root has PHP **>= 8.3** and a completed `composer install` (`vendor/autoload.php` present).
- Drive through `.cursor/skills/verify-cf/bin/verify-cf` with a pinned `CF_VERIFY_RUN_ID`.
- Run `bin/verify-cf doctor` and require `vendor=ok`, `entrypoint=ok`, `cf_list=ok`.
- Prefer process env `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` over the project `.env`.
- Never run mutating `setup` against the developer's checkout — use `isolate-setup` first.
- Never drive a long-lived foreign process: this CLI is one-shot per invocation.
- Concurrent drives are safe for read-only commands; remote Cloudflare mutations on a shared account can collide.

## Driving conventions

- Start every recipe from baseline unless its preconditions say otherwise.
- Always pass `--json` on agent-facing commands (`zones`, `dns:*`, `tunnel:*`).
- Treat every command as literal. Keep flags and argument order unchanged.
- Capture evidence with `bin/verify-cf drive ...` (stdout, stderr, meta).
- Restore or delete remote fixtures you create (DNS records, tunnels). Keep proof artifacts.

## Proof and skip reporting

- Capture the user action (argv + exit code) and the resulting stdout JSON.
- Mutation proof includes a second user-facing read of the changed state.
- Record the feature ID and entry point with every artifact (filename slug is enough).
- Report an unreachable path with the attempted command and the unmet precondition (e.g. `live_creds=absent`).
- Do not report a skipped live entry point as verified through Pest mocks.

## Feature entry contract

Each feature file starts with an H1 title and one paragraph describing the user-visible behavior. It then uses exactly four H2 sections in this order:

1. `Sub-features`
2. `How to get to it (user POV)`
3. `Driving it with verify-cf`
4. `Gotchas`

## Features

- [Agent JSON / missing credentials](./agent-json.md) — stable `--json` failure envelope without creds.
- [Setup](./setup.md) — non-interactive credential onboarding (isolate tree).
- [Zones](./zones.md) — list zones with live API.
- [DNS list](./dns-list.md) — list DNS records for a zone.
- [Tunnel list](./tunnel-list.md) — list Cloudflare tunnels.
31 changes: 31 additions & 0 deletions .cursor/skills/verify-cf/features/agent-json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Agent JSON / missing credentials

When credentials are unset, agent-facing commands fail with a stable JSON envelope on stdout and exit code `1`, so Conduit agents can parse errors without scraping human tables.

## Sub-features

- `json-missing-creds` returns `{"ok":false,"error":"CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID must be set"}` for `zones`.
- `json-exit-code` uses exit `1` on that failure (not `0`).
- `json-stdout-only` places the envelope on stdout (stderr may be empty).

## How to get to it (user POV)

- Run `./cf zones --json` with neither env var nor `.env` credentials set.
- An agent always passes `--json` (see `docs/agent-output.md`).

## Driving it with verify-cf

Preconditions:

- `bin/verify-cf doctor` reports `cf_list=ok`.
- Process env has no `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` (or they are empty).
- Project `.env` either absent or those keys empty for this drive (override with `env -u` if needed).

- **Clear creds and list zones.** Run `env -u CLOUDFLARE_API_TOKEN -u CLOUDFLARE_ACCOUNT_ID CF_VERIFY_RUN_ID=$CF_VERIFY_RUN_ID .cursor/skills/verify-cf/bin/verify-cf drive zones --json`. Exit code `1`. Stdout JSON has `ok: false`, no `data` key, and `error` exactly `CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID must be set`.
- **Proof.** Keep the `*.stdout`, `*.stderr`, and `*.meta.json` files under `artifacts/$CF_VERIFY_RUN_ID/`. Meta `exit_code` must be `1`; stdout must decode as the failure envelope above.

## Gotchas

- `setup --json` uses `{success,message}` — not this `{ok,error}` envelope. Do not mix contracts.
- A populated project `.env` can hide missing process env. Use `env -u` or an isolate with no `.env`.
- Do not treat Pest's `Artisan::call('zones')` as verification of this skill path.
Loading
Loading