fuzzyworld-tools / files / bin / fw
fw
| 1 | #!/bin/bash |
| 2 | # fw (laptop side). |
| 3 | # fw deploy [--ref <branch>] [--here] [--dry-run] build and publish this repo's app (see lib/deploy.mjs) |
| 4 | # fw repoint [DIR] [--https] [--dry-run] move working copies' origin from GitLab to DreamHost |
| 5 | # fw list | new <name> [--public] [--description TEXT] | public <name> | private <name> |
| 6 | # fw usage [--all|--now] Claude Code token use per session (see lib/usage.py) |
| 7 | # fw blog list|build|preview|deploy <config> static blog from Markdown (see server/blog.py); |
| 8 | # build takes --out DIR and --no-htaccess; |
| 9 | # deploy takes --dry-run and --yes (else it asks) |
| 10 | # fw blog import-wp <config> --wp URL [--dry-run] [--force] [--refresh] [--probe-links] |
| 11 | # import the WordPress blog (see lib/wp_import.py) |
| 12 | # fw blog check <config> [--out DIR] compare the built site with the cached WordPress text |
| 13 | # fw blog watch <config> [--once] [--dry-run] mirror the source into git and auto-deploy edits (see server/blog_watch.py); |
| 14 | # fw blog watch install|uninstall <config> [--print] the launchd agent that runs it every 60 s (ONE machine only) |
| 15 | # fw describe <name> TEXT | publish <name> | publish-all | ci <name> [branch] | ci-list | ci-watch |
| 16 | # fw admin [--port N] [--no-open] [--code-dir DIR] local web page for reports and blog comments (see lib/admin.py); |
| 17 | # runs the server-side fw commands over ssh (FW_HOST); FW_CODE_DIR = app folders |
| 18 | # fw comments list|show|approve|reject|remove|untrust|publish|export|ingest|imap-check (blog comments) |
| 19 | # fw issues <repo> public|reviewed|private | list|show|approve|hide|close|reopen|publish|publish-all|ingest|imap-check (bug-report issues) |
| 20 | # (run on DreamHost over ssh) |
| 21 | set -euo pipefail |
| 22 | HOST=${FW_HOST:-dh} |
| 23 | # Follow the symlink npm creates in node_modules/.bin to find this package. |
| 24 | HERE=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd) |
| 25 | |
| 26 | # fw blog: runs on the laptop. Markdown and Pygments live in ~/fw/pylib (installed here when missing). |
| 27 | blog() { |
| 28 | local sub=${1:-} config=${2:-} tmp=${TMPDIR:-/tmp} |
| 29 | tmp=${tmp%/} |
| 30 | local pylib=${FW_PYLIB:-$HOME/fw/pylib} blogpy="$HERE/server/blog.py" |
| 31 | case "$sub" in |
| 32 | list|build|preview|deploy|import-wp|check|watch) ;; |
| 33 | *) echo "usage: fw blog list|build|preview|deploy|import-wp|check|watch <config> [options]" >&2; exit 2 ;; |
| 34 | esac |
| 35 | [ -n "$config" ] || { echo "fw blog $sub: needs the config file (e.g. ~/Blog/fw-blog.json)" >&2; exit 2; } |
| 36 | shift 2 |
| 37 | if ! PYTHONPATH="$pylib" python3 -c 'import markdown, pygments' 2>/dev/null; then |
| 38 | # The launchd agent (watch --once) must never run pip: only an interactive command installs the libraries. |
| 39 | if [ "$sub" = watch ] && [[ " $* " == *" --once "* ]]; then |
| 40 | echo "fw: Markdown and Pygments are missing from $pylib; run any other fw blog command once to install them" >&2 |
| 41 | exit 1 |
| 42 | fi |
| 43 | echo "fw: installing Markdown and Pygments into $pylib" >&2 |
| 44 | python3 -m pip install --quiet --target "$pylib" Markdown==3.7 Pygments==2.21.0 |
| 45 | fi |
| 46 | case "$sub" in |
| 47 | import-wp) |
| 48 | # Reads the WordPress REST API (cached in <source>/wordpress/_api/); --dry-run writes only that cache and the report. |
| 49 | exec python3 "$HERE/lib/wp_import.py" import "$config" "$@" ;; |
| 50 | check) |
| 51 | exec python3 "$HERE/lib/wp_import.py" check "$config" "$@" ;; |
| 52 | list) |
| 53 | python3 "$blogpy" list "$config" ;; |
| 54 | build) |
| 55 | local out=$tmp/fw-blog-build extra=() |
| 56 | while [ $# -gt 0 ]; do |
| 57 | case "$1" in |
| 58 | --out) out=${2:?--out needs a folder}; shift 2 ;; |
| 59 | --no-htaccess) extra+=(--no-htaccess); shift ;; |
| 60 | *) echo "fw blog build: unknown option $1 (use --out DIR, --no-htaccess)" >&2; exit 2 ;; |
| 61 | esac |
| 62 | done |
| 63 | python3 "$blogpy" build "$config" --out "$out" ${extra[@]+"${extra[@]}"} ;; |
| 64 | preview) |
| 65 | # No .htaccess here (python's server ignores it, so redirects are not tested); the feed is at /feed/index.xml. |
| 66 | local out=$tmp/fw-blog-preview port=${1:-8000} |
| 67 | python3 "$blogpy" build "$config" --out "$out" --no-htaccess |
| 68 | echo "preview: http://127.0.0.1:$port/ (ctrl-c to stop)" |
| 69 | exec python3 -m http.server --bind 127.0.0.1 --directory "$out" "$port" ;; |
| 70 | watch) |
| 71 | # One tick (--once, what the launchd agent runs) or a loop; or install/uninstall the agent: `watch install <config>`. |
| 72 | exec python3 "$HERE/server/blog_watch.py" "$config" "$@" ;; |
| 73 | deploy) |
| 74 | # Uploads ONLY a freshly built, audited output folder: never the source folder. Asks first unless --yes. |
| 75 | local args=() a |
| 76 | for a in "$@"; do |
| 77 | case "$a" in --dry-run|--yes) args+=("$a") ;; *) echo "fw blog deploy: unknown option $a" >&2; exit 2 ;; esac |
| 78 | done |
| 79 | exec python3 "$blogpy" deploy "$config" --out "$tmp/fw-blog-deploy" ${args[@]+"${args[@]}"} ;; |
| 80 | esac |
| 81 | } |
| 82 | |
| 83 | case "${1:-help}" in |
| 84 | blog) |
| 85 | shift; blog "$@" ;; |
| 86 | deploy) |
| 87 | shift; exec node "$HERE/lib/deploy.mjs" "$@" ;; |
| 88 | usage) |
| 89 | shift; exec python3 "$HERE/lib/usage.py" "$@" ;; |
| 90 | admin) |
| 91 | shift; exec python3 "$HERE/lib/admin.py" "$@" ;; |
| 92 | repoint) |
| 93 | shift; exec bash "$HERE/lib/repoint.sh" "$@" ;; |
| 94 | list|new|public|private|describe|publish|publish-all|ci|ci-list|ci-watch|comments|issues|help|-h|--help) |
| 95 | # printf %q keeps arguments (e.g. a description with spaces) intact through ssh. |
| 96 | exec ssh "$HOST" "~/bin/fw $(printf '%q ' "$@")" ;; |
| 97 | *) |
| 98 | echo "fw: unknown command '$1' (try: fw help)" >&2; exit 2 ;; |
| 99 | esac |