diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8e91a40 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Conduit / Jordan Partridge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a8df02e..055c3ae 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,138 @@ -# Cloudflare CLI +# Conduit Cloudflare CLI (`cf`) -Lightweight Cloudflare management CLI built with Laravel Zero and Saloon. +**Zones, DNS, and tunnels for Conduit agents — a Laravel Zero + Saloon CLI that turns Cloudflare into a scriptable, `--json`-ready toolbox.** -## Installation +[![CI](https://img.shields.io/github/actions/workflow/status/conduit-ui/cloudflare/ci.yml?branch=main&style=flat-square)](https://github.com/conduit-ui/cloudflare/actions/workflows/ci.yml) +[![PHP](https://img.shields.io/badge/PHP-8.3+-777BB4?style=flat-square&logo=php&logoColor=white)](https://www.php.net/) +[![License](https://img.shields.io/github/license/conduit-ui/cloudflare?style=flat-square)](LICENSE) +[![Release](https://img.shields.io/github/v/release/conduit-ui/cloudflare?style=flat-square&include_prereleases&sort=semver)](https://github.com/conduit-ui/cloudflare/releases) + +Part of the [Conduit](https://github.com/conduit-ui) agent ecosystem: agents and humans share the same `cf` surface for listing zones, managing DNS, and provisioning Cloudflare Tunnels. + +--- + +## Quick install ```bash git clone https://github.com/conduit-ui/cloudflare.git cd cloudflare composer install cp .env.example .env +# Edit .env with your Cloudflare credentials +./cf zones +``` + +Composer registers the `cf` binary. When a PHAR or global Composer package is published, install with: + +```bash +# PHAR (when available) +# curl -L https://github.com/conduit-ui/cloudflare/releases/latest/download/cf.phar -o cf +# chmod +x cf && sudo mv cf /usr/local/bin/cf + +# Global Composer (when published on Packagist) +# composer global require conduit-ui/cloudflare ``` +Build a local PHAR with [Box](https://github.com/box-project/box) using the included `box.json` when you want a single-file binary. + +--- + ## Configuration Edit `.env`: + ```bash CLOUDFLARE_API_TOKEN=your_token_here CLOUDFLARE_ACCOUNT_ID=your_account_id ``` -Get credentials from: -- **API Token**: https://dash.cloudflare.com/profile/api-tokens -- **Account ID**: Right sidebar on any zone page +| Variable | Where to get it | +|----------|-----------------| +| `CLOUDFLARE_API_TOKEN` | [API Tokens](https://dash.cloudflare.com/profile/api-tokens) | +| `CLOUDFLARE_ACCOUNT_ID` | Right sidebar on any zone overview in the dashboard | + +### Token permissions -### Token Permissions +Create a custom token with the minimum scopes your workflow needs: -| Command | Required Permission | +| Command | Required permission | |---------|---------------------| -| `zones` | Zone:Read | -| `dns:*` | DNS:Edit | -| `tunnel:*` | Cloudflare Tunnel:Edit | +| `zones` | **Zone → Zone → Read** | +| `dns:list`, `dns:create` | **Zone → DNS → Edit** | +| `tunnel:list`, `tunnel:create`, `tunnel:expose`, `tunnel:delete` | **Account → Cloudflare Tunnel → Edit** | -## Commands +`tunnel:*` also requires `CLOUDFLARE_ACCOUNT_ID` to be set. + +--- + +## Agent-friendly `--json` + +Every read/write command that returns data accepts `--json`. Agents and scripts get structured stdout instead of tables — ideal for Conduit pipelines and shell glue. + +```bash +./cf zones --json +./cf dns:list example.com --type=A --json +./cf tunnel:list --json +./cf tunnel:create my-app --json +``` + +Human-readable tables remain the default when `--json` is omitted. + +--- + +## Command reference ### Zones + ```bash -./cf zones # List all zones -./cf zones --json # JSON output +./cf zones # List all zones +./cf zones --name=example.com # Filter by zone name +./cf zones --json # Machine-readable output ``` -### DNS Records +### DNS records + +Zone argument accepts a **zone ID** (32-char hex) or a **domain name** (resolved via the Zones API). + ```bash -./cf dns:list # List records (zone ID or domain) -./cf dns:list jordanpartridge.us --type=A # Filter by type -./cf dns:create A api 1.2.3.4 # Create A record +./cf dns:list # List records +./cf dns:list example.com --type=A # Filter by type +./cf dns:list example.com --name=api # Filter by name +./cf dns:list example.com --json + +./cf dns:create A api 1.2.3.4 # Create A record ./cf dns:create CNAME www example.com --proxied +./cf dns:create TXT _verify "token=abc" --ttl=3600 +./cf dns:create A api 1.2.3.4 --json ``` +| Option | Description | +|--------|-------------| +| `--type=` | Filter list by record type (`A`, `CNAME`, `TXT`, …) | +| `--name=` | Filter list by record name | +| `--proxied` | Enable Cloudflare proxy on create | +| `--ttl=1` | TTL in seconds (`1` = auto) | +| `--json` | JSON output | + ### Tunnels + ```bash -./cf tunnel:list # List all tunnels -./cf tunnel:create # Create tunnel (prints token/credentials) +./cf tunnel:list # List tunnels +./cf tunnel:list --json + +./cf tunnel:create # Create a tunnel (prints token/credentials) +./cf tunnel:create my-app --json + ./cf tunnel:expose [hostname] [url] # Create + route + cloudflared steps -./cf tunnel:delete # Delete tunnel +./cf tunnel:expose my-app --json + +./cf tunnel:delete # Delete (prompts for confirmation) +./cf tunnel:delete --force # Skip confirmation ``` -## Expose a local app +`tunnel:create` masks secrets in the summary table and prints full token / credentials once. Use `--json` for the raw API result. + +### Expose a local app One guided flow: create a tunnel, print the connector token, and (when a hostname is given) attempt remote ingress + DNS CNAME via the API. Falls back to exact `cloudflared` commands when the API cannot route. @@ -82,19 +159,48 @@ If DNS was not created via API: cloudflared tunnel route dns my-app app.example.com ``` -`tunnel:create` masks secrets in the summary table and prints full token / credentials once. Use `--json` for the raw API result. +--- ## Architecture +Laravel Zero drives the CLI; Saloon models the Cloudflare HTTP API. + ``` app/ -├── Commands/ # CLI commands +├── Commands/ # zones, dns:*, tunnel:* ├── Integrations/Cloudflare/ -│ ├── CloudflareConnector.php # Saloon connector -│ ├── Resources/ # API resource classes -│ └── Requests/ # Individual API requests +│ ├── CloudflareConnector.php # Saloon connector + auth +│ ├── Resources/ # Zone, Dns, Tunnel facades +│ └── Requests/ # One class per API call +│ ├── Zones/ +│ ├── Dns/ +│ └── Tunnels/ +└── Providers/ ``` +Commands resolve credentials from `.env`, call resource methods on `CloudflareConnector`, and format either a console table or pretty-printed JSON. + +--- + +## Demo walkthrough + +Follow a scripted zones → DNS → tunnel session in [`docs/demo.md`](docs/demo.md). Sample terminal output (asciinema-style) lives in [`docs/demo.cast.md`](docs/demo.cast.md). + +--- + +## Development + +```bash +composer install +vendor/bin/pest # Test suite +vendor/bin/pint # Code style (Laravel Pint) +./cf list # List registered commands +``` + +Requires **PHP 8.3+** with `mbstring` and `xml`. + +--- + ## License -MIT +[MIT](LICENSE) diff --git a/docs/demo.cast.md b/docs/demo.cast.md new file mode 100644 index 0000000..6180b4c --- /dev/null +++ b/docs/demo.cast.md @@ -0,0 +1,98 @@ +# Sample terminal session (`demo.cast`) + +Fake/sample output for docs and recordings. No live Cloudflare API calls required. + +To turn this into a real [asciinema](https://asciinema.org/) cast later: + +```bash +# asciinema rec docs/demo.cast +# …run the commands from demo.md… +# asciinema upload docs/demo.cast +``` + +Until a binary `.cast` is checked in, use the transcript below as the expected session. + +--- + +```text +$ ./cf zones ++----------------------------------+-------------------+--------+----------+ +| ID | Name | Status | Plan | ++----------------------------------+-------------------+--------+----------+ +| a1b2c3d4e5f6789012345678abcdef01 | example.com | active | Free | +| 9f8e7d6c5b4a3210987654321fedcba0 | staging.example | active | Pro | ++----------------------------------+-------------------+--------+----------+ + +$ ./cf zones --json +[ + { + "id": "a1b2c3d4e5f6789012345678abcdef01", + "name": "example.com", + "status": "active", + "plan": { + "name": "Free Website" + } + } +] + +$ ./cf dns:list example.com --type=A ++------+------------------+-------------+---------+------+ +| Type | Name | Content | Proxied | TTL | ++------+------------------+-------------+---------+------+ +| A | example.com | 203.0.113.1 | Yes | Auto | +| A | api.example.com | 203.0.113.2 | No | Auto | ++------+------------------+-------------+---------+------+ + +$ ./cf dns:create example.com A demo-cf 203.0.113.10 +DNS record created successfully! ++---------+----------------------------------+ +| Field | Value | ++---------+----------------------------------+ +| ID | rec01abcdefghijklmnopqrstuvwxyz | +| Type | A | +| Name | demo-cf.example.com | +| Content | 203.0.113.10 | +| Proxied | No | ++---------+----------------------------------+ + +$ ./cf tunnel:create demo-conduit +Creating tunnel: demo-conduit... + +Tunnel created successfully! ++---------+--------------------------------------+ +| Field | Value | ++---------+--------------------------------------+ +| ID | 11111111-2222-3333-4444-555555555555 | +| Name | demo-conduit | +| Status | inactive | +| Created | 2026-03-26T12:00:00Z | ++---------+--------------------------------------+ + +Next steps: + 1. Configure ingress rules in ~/.cloudflared/config.yml + 2. Run: cloudflared tunnel route dns demo-conduit + 3. Start tunnel: cloudflared tunnel run demo-conduit + +$ ./cf tunnel:list --json +[ + { + "id": "11111111-2222-3333-4444-555555555555", + "name": "demo-conduit", + "status": "inactive", + "created_at": "2026-03-26T12:00:00Z", + "connections": [] + } +] + +$ ./cf tunnel:delete 11111111-2222-3333-4444-555555555555 --force +Tunnel deleted successfully. +``` + +--- + +**Agent tip:** pipe any of these with `--json` into `jq` for Conduit workflows: + +```bash +./cf zones --json | jq '.[].name' +./cf tunnel:create demo-conduit --json | jq -r '.id' +``` diff --git a/docs/demo.md b/docs/demo.md new file mode 100644 index 0000000..a6b4bbd --- /dev/null +++ b/docs/demo.md @@ -0,0 +1,109 @@ +# Demo walkthrough: zones → DNS → tunnel + +A short scripted session you can run against a real Cloudflare account, or follow while recording a terminal demo (see [`demo.cast.md`](demo.cast.md) for sample output). + +**Prerequisites** + +- PHP 8.3+, Composer +- Cloudflare API token with Zone:Read, DNS:Edit, and Cloudflare Tunnel:Edit +- Account ID set for tunnel commands +- Optional: [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/) for post-create routing + +Replace `example.com` with a zone you own. + +--- + +## 0. Bootstrap + +```bash +git clone https://github.com/conduit-ui/cloudflare.git +cd cloudflare +composer install +cp .env.example .env +``` + +Edit `.env`: + +```bash +CLOUDFLARE_API_TOKEN=... +CLOUDFLARE_ACCOUNT_ID=... +``` + +Confirm the binary: + +```bash +./cf list +``` + +--- + +## 1. List zones + +Human-readable table: + +```bash +./cf zones +``` + +Filter and JSON (agent-friendly): + +```bash +./cf zones --name=example.com +./cf zones --json | head +``` + +Note a zone ID or domain for the next steps. + +--- + +## 2. Inspect DNS + +```bash +./cf dns:list example.com +./cf dns:list example.com --type=A +./cf dns:list example.com --json +``` + +Create a non-production record (use a safe subdomain): + +```bash +./cf dns:create example.com A demo-cf 203.0.113.10 +./cf dns:create example.com CNAME demo-www example.com --proxied +./cf dns:list example.com --name=demo-cf +``` + +Clean up in the dashboard (or a future `dns:delete` command) when finished recording. + +--- + +## 3. Provision a tunnel + +```bash +./cf tunnel:list +./cf tunnel:create demo-conduit --json +./cf tunnel:list +``` + +Capture the tunnel `id` from create output. Optional `cloudflared` follow-up: + +```bash +# cloudflared tunnel route dns demo-conduit demo-tunnel.example.com +# cloudflared tunnel run demo-conduit +``` + +When done with the demo tunnel: + +```bash +./cf tunnel:delete --force +``` + +--- + +## Recording tips + +1. Use a throwaway subdomain and tunnel name (`demo-*`). +2. Prefer `--json` for one clip and table output for another so both modes show. +3. Mask or redact tokens if your shell history or env dumps appear on screen. +4. Keep the session under ~2 minutes: zones → dns:list → dns:create → tunnel:create → tunnel:delete. + +For expected sample output without live API calls, see [`demo.cast.md`](demo.cast.md).