Compare commits
2 Commits
a9623e9219
...
c1f4830bf5
| Author | SHA1 | Date | |
|---|---|---|---|
| c1f4830bf5 | |||
| e5c4bf25b3 |
20
AGENTS.md
20
AGENTS.md
@@ -130,6 +130,26 @@ Load additional guides when the task requires them.
|
||||
- Installation and configuration are managed by Mosaic bootstrap and runtime linking.
|
||||
- If sequential-thinking is unavailable, you MUST report the failure and stop planning-intensive execution.
|
||||
|
||||
## Subagent Model Selection (Cost Optimization — Hard Rule)
|
||||
|
||||
When delegating work to subagents, you MUST select the cheapest model capable of completing the task. Do NOT default to the most expensive model for every delegation.
|
||||
|
||||
| Task Type | Model Tier | Rationale |
|
||||
|-----------|-----------|-----------|
|
||||
| File search, grep, glob, codebase exploration | **haiku** | Read-only, pattern matching, no reasoning depth needed |
|
||||
| Status checks, health monitoring, heartbeat | **haiku** | Structured API calls, pass/fail output |
|
||||
| Simple code fixes (typos, rename, one-liner) | **haiku** | Minimal reasoning, mechanical changes |
|
||||
| Code review, lint, style checks | **sonnet** | Needs judgment but not deep architectural reasoning |
|
||||
| Test writing, test fixes | **sonnet** | Pattern-based, moderate complexity |
|
||||
| Standard feature implementation | **sonnet** | Good balance of capability and cost for most coding |
|
||||
| Complex architecture, multi-file refactors | **opus** | Requires deep reasoning, large context, design judgment |
|
||||
| Security review, auth logic | **opus** | High-stakes reasoning where mistakes are costly |
|
||||
| Ambiguous requirements, design decisions | **opus** | Needs nuanced judgment and tradeoff analysis |
|
||||
|
||||
**Decision rule**: Start with the cheapest viable tier. Only escalate if the task genuinely requires deeper reasoning — not as a safety default. Most coding tasks are sonnet-tier. Reserve opus for work where wrong answers are expensive.
|
||||
|
||||
**Runtime-specific syntax**: See the runtime reference for how to specify model tier when spawning subagents (e.g., Claude Code Task tool `model` parameter).
|
||||
|
||||
## Skills Policy
|
||||
|
||||
- Use only the minimum required skills for the active task.
|
||||
|
||||
23
TOOLS.md
23
TOOLS.md
@@ -72,6 +72,27 @@ Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection a
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh -b <branch>
|
||||
```
|
||||
|
||||
### DNS — Cloudflare
|
||||
|
||||
Multi-instance support: `-a <instance>` selects a named instance (e.g. `personal`, `work`). Omit `-a` to use the default from `cloudflare.default` in credentials.json.
|
||||
|
||||
```bash
|
||||
# List zones (domains)
|
||||
~/.config/mosaic/tools/cloudflare/zone-list.sh [-a instance]
|
||||
|
||||
# List DNS records (zone by name or ID)
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z <zone> [-a instance] [-t type] [-n name]
|
||||
|
||||
# Create DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh -z <zone> -t <type> -n <name> -c <content> [-a instance] [-p] [-l ttl] [-P priority]
|
||||
|
||||
# Update DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance] [-p] [-l ttl]
|
||||
|
||||
# Delete DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-delete.sh -z <zone> -r <record-id> [-a instance]
|
||||
```
|
||||
|
||||
### IT Service — GLPI
|
||||
|
||||
```bash
|
||||
@@ -100,7 +121,7 @@ Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection a
|
||||
# Source in any script to load service credentials
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh
|
||||
load_credentials <service-name>
|
||||
# Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker
|
||||
# Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker, cloudflare
|
||||
```
|
||||
|
||||
## Git Providers
|
||||
|
||||
@@ -153,6 +153,75 @@ The human is escalation-only for missing access, hard policy conflicts, or irrev
|
||||
- Magic variables (`SERVICE_FQDN_*`) require list-style env syntax, not dict-style
|
||||
- Rate limit: 200 requests per interval
|
||||
|
||||
### Cloudflare DNS Operations
|
||||
|
||||
Use the Cloudflare tools for any DNS configuration: pointing domains at services, adding TXT verification records, managing MX records, etc.
|
||||
|
||||
**Multi-instance support**: Credentials support named instances (e.g. `personal`, `work`). A `default` key in credentials.json determines which instance is used when `-a` is omitted. Pass `-a <instance>` to target a specific account.
|
||||
|
||||
```bash
|
||||
# List all zones (domains) in the account
|
||||
~/.config/mosaic/tools/cloudflare/zone-list.sh [-a instance]
|
||||
|
||||
# List DNS records for a zone (accepts zone name or ID)
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z <zone> [-t type] [-n name]
|
||||
|
||||
# Create a DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh -z <zone> -t <type> -n <name> -c <content> [-p] [-l ttl] [-P priority]
|
||||
|
||||
# Update a DNS record (requires record ID from record-list)
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-p]
|
||||
|
||||
# Delete a DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-delete.sh -z <zone> -r <record-id>
|
||||
```
|
||||
|
||||
**Flag reference:**
|
||||
|
||||
| Flag | Purpose |
|
||||
|------|---------|
|
||||
| `-z` | Zone name (e.g. `mosaicstack.dev`) or 32-char zone ID |
|
||||
| `-a` | Named Cloudflare instance (omit for default) |
|
||||
| `-t` | Record type: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `SRV`, etc. |
|
||||
| `-n` | Record name: short (`app`) or FQDN (`app.example.com`) |
|
||||
| `-c` | Record content/value (IP, hostname, TXT string, etc.) |
|
||||
| `-r` | Record ID (from `record-list.sh` output) |
|
||||
| `-p` | Enable Cloudflare proxy (orange cloud) — omit for DNS-only (grey cloud) |
|
||||
| `-l` | TTL in seconds (default: `1` = auto) |
|
||||
| `-P` | Priority for MX/SRV records |
|
||||
| `-f` | Output format: `table` (default) or `json` |
|
||||
|
||||
**Common workflows:**
|
||||
|
||||
```bash
|
||||
# Point a new subdomain at a server (proxied through Cloudflare)
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh \
|
||||
-z example.com -t A -n myapp -c 203.0.113.10 -p
|
||||
|
||||
# Add a TXT record for domain verification (never proxied)
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh \
|
||||
-z example.com -t TXT -n _verify -c "verification=abc123"
|
||||
|
||||
# Check what records exist before making changes
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z example.com -t CNAME
|
||||
|
||||
# Update an existing record (get record ID from record-list first)
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh \
|
||||
-z example.com -r <record-id> -t A -n myapp -c 10.0.0.5 -p
|
||||
```
|
||||
|
||||
**DNS + Deployment integration**: When deploying a new service via Coolify or Portainer that needs a public domain, the typical sequence is:
|
||||
|
||||
1. Create the DNS record pointing at the host IP (with `-p` for Cloudflare proxy if desired)
|
||||
2. Deploy the service via Coolify/Portainer
|
||||
3. Verify the domain resolves and the service is reachable
|
||||
|
||||
**Proxy (`-p`) guidance:**
|
||||
|
||||
- Use proxy (orange cloud) for web services — provides CDN, DDoS protection, and hides origin IP
|
||||
- Skip proxy (grey cloud) for non-HTTP services (mail, SSH), wildcard records, or when the service handles its own TLS termination and needs direct client IP visibility
|
||||
- Proxy is NOT compatible with non-standard ports outside Cloudflare's supported range
|
||||
|
||||
### Stack Health Check
|
||||
|
||||
Verify all infrastructure services are reachable:
|
||||
|
||||
@@ -16,6 +16,36 @@ This file applies only to Claude runtime behavior.
|
||||
8. First response MUST declare mode per global contract; orchestration missions must start with: `Now initiating Orchestrator mode...`
|
||||
9. Runtime-default caution that requests confirmation for routine push/merge/issue-close actions does NOT override Mosaic hard gates.
|
||||
|
||||
## Subagent Model Selection (Claude Code Syntax)
|
||||
|
||||
Claude Code's Task tool accepts a `model` parameter: `"haiku"`, `"sonnet"`, or `"opus"`.
|
||||
|
||||
You MUST set this parameter according to the model selection table in `~/.config/mosaic/AGENTS.md`. Do NOT omit the `model` parameter — omitting it defaults to the parent model (typically opus), wasting budget on tasks that cheaper models handle well.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```
|
||||
# Codebase exploration — haiku
|
||||
Task(subagent_type="Explore", model="haiku", prompt="Find all API route handlers")
|
||||
|
||||
# Code review — sonnet
|
||||
Task(subagent_type="feature-dev:code-reviewer", model="sonnet", prompt="Review the changes in src/auth/")
|
||||
|
||||
# Standard feature work — sonnet
|
||||
Task(subagent_type="general-purpose", model="sonnet", prompt="Add validation to the user input form")
|
||||
|
||||
# Complex architecture — opus (only when justified)
|
||||
Task(subagent_type="Plan", model="opus", prompt="Design the multi-tenant isolation strategy")
|
||||
```
|
||||
|
||||
**Quick reference (from global AGENTS.md):**
|
||||
|
||||
| haiku | sonnet | opus |
|
||||
|-------|--------|------|
|
||||
| Search, grep, glob | Code review | Complex architecture |
|
||||
| Status/health checks | Test writing | Security/auth logic |
|
||||
| Simple one-liner fixes | Standard features | Ambiguous design decisions |
|
||||
|
||||
## Memory Override
|
||||
|
||||
Do NOT write durable memory to `~/.claude/projects/*/memory/`. All durable memory MUST be written to `~/.config/mosaic/memory/` per `~/.config/mosaic/guides/MEMORY.md`. Claude Code's native auto-memory locations are volatile runtime silos and MUST NOT be used for cross-session or cross-agent retention.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# Supported services:
|
||||
# portainer, coolify, authentik, glpi, github,
|
||||
# gitea-mosaicstack, gitea-usc, woodpecker
|
||||
# gitea-mosaicstack, gitea-usc, woodpecker, cloudflare
|
||||
#
|
||||
# After loading, service-specific env vars are exported.
|
||||
# Run `load_credentials --help` for details.
|
||||
@@ -49,6 +49,8 @@ Services and exported variables:
|
||||
gitea-mosaicstack → GITEA_URL, GITEA_TOKEN
|
||||
gitea-usc → GITEA_URL, GITEA_TOKEN
|
||||
woodpecker → WOODPECKER_URL, WOODPECKER_TOKEN
|
||||
cloudflare → CLOUDFLARE_API_TOKEN (uses default instance)
|
||||
cloudflare-<name> → CLOUDFLARE_API_TOKEN (specific instance, e.g. cloudflare-personal)
|
||||
EOF
|
||||
return 0
|
||||
fi
|
||||
@@ -110,9 +112,25 @@ EOF
|
||||
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
|
||||
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
|
||||
;;
|
||||
cloudflare-*)
|
||||
local cf_instance="${service#cloudflare-}"
|
||||
export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:-$(_mosaic_read_cred ".cloudflare.${cf_instance}.api_token")}"
|
||||
export CLOUDFLARE_INSTANCE="$cf_instance"
|
||||
[[ -n "$CLOUDFLARE_API_TOKEN" ]] || { echo "Error: cloudflare.${cf_instance}.api_token not found" >&2; return 1; }
|
||||
;;
|
||||
cloudflare)
|
||||
# Resolve default instance, then load it
|
||||
local cf_default
|
||||
cf_default="${CLOUDFLARE_INSTANCE:-$(_mosaic_read_cred '.cloudflare.default')}"
|
||||
if [[ -z "$cf_default" ]]; then
|
||||
echo "Error: cloudflare.default not set and no CLOUDFLARE_INSTANCE env var" >&2
|
||||
return 1
|
||||
fi
|
||||
load_credentials "cloudflare-${cf_default}"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown service '$service'" >&2
|
||||
echo "Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker" >&2
|
||||
echo "Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker, cloudflare, cloudflare-<name>" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
67
tools/cloudflare/_lib.sh
Executable file
67
tools/cloudflare/_lib.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# _lib.sh — Shared helpers for Cloudflare tool scripts
|
||||
#
|
||||
# Usage: source "$(dirname "$0")/_lib.sh"
|
||||
#
|
||||
# Provides:
|
||||
# CF_API — Base API URL
|
||||
# cf_auth — Authorization header value
|
||||
# cf_load_instance <instance> — Load credentials for a specific or default instance
|
||||
# cf_resolve_zone <name_or_id> — Resolves a zone name to its ID (passes IDs through)
|
||||
|
||||
CF_API="https://api.cloudflare.com/client/v4"
|
||||
|
||||
cf_auth() {
|
||||
echo "Bearer $CLOUDFLARE_API_TOKEN"
|
||||
}
|
||||
|
||||
# Load credentials for a Cloudflare instance.
|
||||
# If instance is empty, loads the default.
|
||||
cf_load_instance() {
|
||||
local instance="$1"
|
||||
if [[ -n "$instance" ]]; then
|
||||
load_credentials "cloudflare-${instance}"
|
||||
else
|
||||
load_credentials cloudflare
|
||||
fi
|
||||
}
|
||||
|
||||
# Resolve a zone name (e.g. "mosaicstack.dev") to its zone ID.
|
||||
# If the input is already a 32-char hex ID, passes it through.
|
||||
cf_resolve_zone() {
|
||||
local input="$1"
|
||||
|
||||
# If it looks like a zone ID (32 hex chars), pass through
|
||||
if [[ "$input" =~ ^[0-9a-f]{32}$ ]]; then
|
||||
echo "$input"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Resolve by name
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones?name=${input}&status=active")
|
||||
|
||||
local http_code
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
local body
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to resolve zone '$input' (HTTP $http_code)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local zone_id
|
||||
zone_id=$(echo "$body" | jq -r '.result[0].id // empty')
|
||||
|
||||
if [[ -z "$zone_id" ]]; then
|
||||
echo "Error: Zone '$input' not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$zone_id"
|
||||
}
|
||||
86
tools/cloudflare/record-create.sh
Executable file
86
tools/cloudflare/record-create.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-create.sh — Create a DNS record in a Cloudflare zone
|
||||
#
|
||||
# Usage: record-create.sh -z <zone> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -t type Record type: A, AAAA, CNAME, MX, TXT, etc. (required)
|
||||
# -n name Record name, e.g. "app" or "app.example.com" (required)
|
||||
# -c content Record value/content (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -l ttl TTL in seconds (default: 1 = auto)
|
||||
# -p Enable Cloudflare proxy (orange cloud)
|
||||
# -P priority MX/SRV priority (default: 10)
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
CONTENT=""
|
||||
TTL=1
|
||||
PROXIED=false
|
||||
PRIORITY=""
|
||||
|
||||
while getopts "z:a:t:n:c:l:pP:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
c) CONTENT="$OPTARG" ;;
|
||||
l) TTL="$OPTARG" ;;
|
||||
p) PROXIED=true ;;
|
||||
P) PRIORITY="$OPTARG" ;;
|
||||
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$TYPE" || -z "$NAME" || -z "$CONTENT" ]]; then
|
||||
echo "Error: -z, -t, -n, and -c are all required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
# Build JSON payload
|
||||
payload=$(jq -n \
|
||||
--arg type "$TYPE" \
|
||||
--arg name "$NAME" \
|
||||
--arg content "$CONTENT" \
|
||||
--argjson ttl "$TTL" \
|
||||
--argjson proxied "$PROXIED" \
|
||||
'{type: $type, name: $name, content: $content, ttl: $ttl, proxied: $proxied}')
|
||||
|
||||
# Add priority for MX/SRV records
|
||||
if [[ -n "$PRIORITY" ]]; then
|
||||
payload=$(echo "$payload" | jq --argjson priority "$PRIORITY" '. + {priority: $priority}')
|
||||
fi
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to create record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
record_id=$(echo "$body" | jq -r '.result.id')
|
||||
echo "Created $TYPE record: $NAME → $CONTENT (ID: $record_id)"
|
||||
55
tools/cloudflare/record-delete.sh
Executable file
55
tools/cloudflare/record-delete.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-delete.sh — Delete a DNS record from a Cloudflare zone
|
||||
#
|
||||
# Usage: record-delete.sh -z <zone> -r <record-id> [-a instance]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -r record-id DNS record ID (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
RECORD_ID=""
|
||||
|
||||
while getopts "z:a:r:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
r) RECORD_ID="$OPTARG" ;;
|
||||
h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -r <record-id> [-a instance]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$RECORD_ID" ]]; then
|
||||
echo "Error: -z and -r are both required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X DELETE \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records/${RECORD_ID}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to delete record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deleted DNS record $RECORD_ID from zone $ZONE"
|
||||
81
tools/cloudflare/record-list.sh
Executable file
81
tools/cloudflare/record-list.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-list.sh — List DNS records for a Cloudflare zone
|
||||
#
|
||||
# Usage: record-list.sh -z <zone> [-a instance] [-t type] [-n name] [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -t type Filter by record type (A, AAAA, CNAME, MX, TXT, etc.)
|
||||
# -n name Filter by record name
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "z:a:t:n:f:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> [-a instance] [-t type] [-n name] [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" ]]; then
|
||||
echo "Error: -z zone is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
# Build query params
|
||||
params="per_page=100"
|
||||
[[ -n "$TYPE" ]] && params="${params}&type=${TYPE}"
|
||||
[[ -n "$NAME" ]] && params="${params}&name=${NAME}"
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records?${params}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list records (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.result'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "RECORD ID TYPE NAME CONTENT PROXIED TTL"
|
||||
echo "-------------------------------- ----- -------------------------------------- ------------------------------- ------- -----"
|
||||
echo "$body" | jq -r '.result[] | [
|
||||
.id,
|
||||
.type,
|
||||
.name,
|
||||
.content,
|
||||
(if .proxied then "yes" else "no" end),
|
||||
(if .ttl == 1 then "auto" else (.ttl | tostring) end)
|
||||
] | @tsv' | while IFS=$'\t' read -r id type name content proxied ttl; do
|
||||
printf "%-32s %-5s %-38s %-31s %-7s %s\n" "$id" "$type" "${name:0:38}" "${content:0:31}" "$proxied" "$ttl"
|
||||
done
|
||||
86
tools/cloudflare/record-update.sh
Executable file
86
tools/cloudflare/record-update.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-update.sh — Update a DNS record in a Cloudflare zone
|
||||
#
|
||||
# Usage: record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -r record-id DNS record ID (required)
|
||||
# -t type Record type: A, AAAA, CNAME, MX, TXT, etc. (required)
|
||||
# -n name Record name (required)
|
||||
# -c content Record value/content (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -l ttl TTL in seconds (default: 1 = auto)
|
||||
# -p Enable Cloudflare proxy (orange cloud)
|
||||
# -P priority MX/SRV priority
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
RECORD_ID=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
CONTENT=""
|
||||
TTL=1
|
||||
PROXIED=false
|
||||
PRIORITY=""
|
||||
|
||||
while getopts "z:a:r:t:n:c:l:pP:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
r) RECORD_ID="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
c) CONTENT="$OPTARG" ;;
|
||||
l) TTL="$OPTARG" ;;
|
||||
p) PROXIED=true ;;
|
||||
P) PRIORITY="$OPTARG" ;;
|
||||
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$RECORD_ID" || -z "$TYPE" || -z "$NAME" || -z "$CONTENT" ]]; then
|
||||
echo "Error: -z, -r, -t, -n, and -c are all required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg type "$TYPE" \
|
||||
--arg name "$NAME" \
|
||||
--arg content "$CONTENT" \
|
||||
--argjson ttl "$TTL" \
|
||||
--argjson proxied "$PROXIED" \
|
||||
'{type: $type, name: $name, content: $content, ttl: $ttl, proxied: $proxied}')
|
||||
|
||||
if [[ -n "$PRIORITY" ]]; then
|
||||
payload=$(echo "$payload" | jq --argjson priority "$PRIORITY" '. + {priority: $priority}')
|
||||
fi
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X PUT \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records/${RECORD_ID}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to update record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updated $TYPE record: $NAME → $CONTENT (ID: $RECORD_ID)"
|
||||
59
tools/cloudflare/zone-list.sh
Executable file
59
tools/cloudflare/zone-list.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# zone-list.sh — List Cloudflare zones (domains)
|
||||
#
|
||||
# Usage: zone-list.sh [-a instance] [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
INSTANCE=""
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "a:f:h" opt; do
|
||||
case $opt in
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-a instance] [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones?per_page=50")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list zones (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.result'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ZONE ID NAME STATUS PLAN"
|
||||
echo "-------------------------------- ---------------------------- -------- ----------"
|
||||
echo "$body" | jq -r '.result[] | [
|
||||
.id,
|
||||
.name,
|
||||
.status,
|
||||
.plan.name
|
||||
] | @tsv' | while IFS=$'\t' read -r id name status plan; do
|
||||
printf "%-32s %-28s %-8s %s\n" "$id" "$name" "$status" "$plan"
|
||||
done
|
||||
Reference in New Issue
Block a user