diff --git a/README.md b/README.md index d92215a..390bcba 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ hdi Interactive picker - shows all sections (default) hdi install Just install/setup commands (aliases: setup, i) hdi run Just run/start commands (aliases: start, r) hdi test Just test commands (alias: t) -hdi deploy Just deploy/release commands (alias: d) +hdi deploy Just deploy/release commands and platform detection (alias: d) hdi all All sections (aliases: a) hdi check Check if required tools are installed (alias: c) hdi /path/to/project Scan a different directory @@ -100,6 +100,24 @@ Example: `hdi --raw | pbcopy` to copy commands to clipboard. | `c` | Copy highlighted command to clipboard | | `q` / `Esc` | Quit | +### Deployment platform detection + +The `deploy` (or `d`) subcommand makes a best effort to extract what platform(s) a project uses for deployment (eg. Cloudflare, Heroku, Vercel, Netlify, AWS, etc), and displays this in the output: + +```bash +$ hdi d +[hdi] example-project [deploy → Cloudflare Pages] +... +``` + +Or if the certainty is low: + +```bash +$ hdi d +[hdi] example-project [deploy → Netlify?] +... +``` + ## How it works `hdi` parses a given README's Markdown headings looking for keywords like *install*, *setup*, *prerequisites*, *run*, *usage*, *getting started*, etc. It extracts the fenced code blocks from matching sections (skipping JSON/YAML response examples) and presents them as an interactive, executable list. diff --git a/build b/build index 43f036c..8eeaff1 100755 --- a/build +++ b/build @@ -17,6 +17,7 @@ sources=( src/render.sh src/picker.sh src/check.sh + src/platform.sh src/json.sh src/main.sh ) diff --git a/hdi b/hdi index fc8242b..d0a3395 100755 --- a/hdi +++ b/hdi @@ -2,14 +2,14 @@ # hdi - "How do I..." - Extracts setup/run/test commands from a README. # # Usage: -# hdi Interactive command picker (default in a terminal) -# hdi install Just install/setup commands -# hdi run Just run/start commands -# hdi test Just test commands -# hdi deploy Just deploy/release commands -# hdi all Show all matched sections +# hdi Interactive picker - shows all sections (default) +# hdi install Just install/setup commands (aliases: setup, i) +# hdi run Just run/start commands (aliases: start, r) +# hdi test Just test commands (alias: t) +# hdi deploy Just deploy/release commands and platform detection (alias: d) +# hdi all Show all matched sections (currently the default mode) # hdi check Check if required tools are installed (experimental) -# hdi [mode] --no-interactive Print commands without the picker +# hdi [mode] --no-interactive Print commands without the picker (alias: --ni) # hdi [mode] --full Include prose around commands # hdi [mode] --raw Plain markdown output (no colour, good for piping) # hdi --json Structured JSON output (includes all sections) @@ -22,9 +22,6 @@ # Enter Execute the highlighted command # c Copy highlighted command to clipboard # q / Esc / Ctrl+C Quit -# -# Aliases: "install" = "setup" = "i", "run" = "start" = "r", "test" = "t", -# "deploy" = "d", "check" = "c" set -euo pipefail @@ -938,10 +935,16 @@ draw_picker() { install) hdr+=" ${DIM}[install]${RESET}" ;; run) hdr+=" ${DIM}[run]${RESET}" ;; test) hdr+=" ${DIM}[test]${RESET}" ;; + deploy) + if [[ -n "${_PLATFORM_DISPLAY:-}" ]]; then + hdr+=" ${DIM}[deploy → ${RESET}${CYAN}${_PLATFORM_DISPLAY}${DIM}]${RESET}" + else + hdr+=" ${DIM}[deploy]${RESET}" + fi + ;; all) hdr+=" ${DIM}[all]${RESET}" ;; esac _line "$hdr" - local chrome=3 # Scroll-up indicator (only if meaningful content is above the viewport) @@ -1054,6 +1057,11 @@ run_interactive() { local num_cmds=${#CMD_INDICES[@]} if (( num_cmds == 0 )); then + if [[ -n "${_PLATFORM_DISPLAY:-}" ]]; then + printf "%s%s[hdi] %s%s %s[deploy → %s%s%s%s]%s\n\n" \ + "$BOLD" "$YELLOW" "$PROJECT_NAME" "$RESET" \ + "$DIM" "$RESET" "$CYAN" "$_PLATFORM_DISPLAY" "$DIM" "$RESET" + fi echo "${YELLOW}hdi: no commands to pick from${RESET}" >&2 echo "${DIM}Try: hdi all --full${RESET}" >&2 exit 1 @@ -1338,6 +1346,144 @@ run_check() { fi } +# ── Platform detection ──────────────────────────────────────────────────────── +# Detects deployment platforms from three sources: +# 1. Config files in the project directory (high confidence) +# 2. CLI tools in extracted commands (high confidence) +# 3. Platform names mentioned in deploy section prose (low confidence) + +declare -a PLATFORM_GROUPS=() +declare -a PLATFORM_NAMES=() +declare -a PLATFORM_CONFIDENCE=() # "high" | "low" + +# Add or upgrade a platform detection. Deduplicates by group key +# If the group already exists: upgrade confidence to high if applicable, +# and prefer the longer (more specific) name +_platform_add() { + local group="$1" name="$2" confidence="$3" + for i in "${!PLATFORM_GROUPS[@]}"; do + if [[ "${PLATFORM_GROUPS[$i]}" == "$group" ]]; then + if [[ "$confidence" == "high" ]]; then + PLATFORM_CONFIDENCE[i]="high" + fi + if (( ${#name} > ${#PLATFORM_NAMES[i]} )); then + PLATFORM_NAMES[i]="$name" + fi + return + fi + done + PLATFORM_GROUPS+=("$group") + PLATFORM_NAMES+=("$name") + PLATFORM_CONFIDENCE+=("$confidence") +} + +# Layer 1: Config file detection (high confidence) +detect_platforms_from_files() { + local dir="$1" + + if [[ -f "$dir/wrangler.toml" || -f "$dir/wrangler.json" ]]; then _platform_add "cloudflare" "Cloudflare" "high"; fi + if [[ -f "$dir/vercel.json" ]]; then _platform_add "vercel" "Vercel" "high"; fi + if [[ -f "$dir/netlify.toml" ]]; then _platform_add "netlify" "Netlify" "high"; fi + if [[ -f "$dir/fly.toml" ]]; then _platform_add "fly" "Fly.io" "high"; fi + if [[ -f "$dir/Procfile" ]]; then _platform_add "heroku" "Heroku" "high"; fi + if [[ -f "$dir/render.yaml" ]]; then _platform_add "render" "Render" "high"; fi + if [[ -f "$dir/firebase.json" ]]; then _platform_add "firebase" "Firebase" "high"; fi + if [[ -f "$dir/amplify.yml" ]]; then _platform_add "amplify" "AWS Amplify" "high"; fi + if [[ -f "$dir/serverless.yml" || -f "$dir/serverless.ts" ]]; then _platform_add "serverless" "Serverless" "high"; fi + if [[ -f "$dir/cdk.json" ]]; then _platform_add "awscdk" "AWS CDK" "high"; fi + if [[ -f "$dir/pulumi.yaml" ]]; then _platform_add "pulumi" "Pulumi" "high"; fi + if [[ -f "$dir/railway.json" || -f "$dir/railway.toml" ]]; then _platform_add "railway" "Railway" "high"; fi + if [[ -f "$dir/Chart.yaml" ]]; then _platform_add "helm" "Helm" "high"; fi + if [[ -f "$dir/CNAME" ]]; then _platform_add "ghpages" "GitHub Pages" "high"; fi + if [[ -d "$dir/k8s" || -d "$dir/kubernetes" ]]; then _platform_add "kubernetes" "Kubernetes" "high"; fi + if [[ -d "$dir/.kamal" || -f "$dir/config/deploy.yml" ]]; then _platform_add "kamal" "Kamal" "high"; fi + + # Terraform: glob for *.tf files + local _tf + for _tf in "$dir"/*.tf; do + if [[ -f "$_tf" ]]; then + _platform_add "terraform" "Terraform" "high" + break + fi + done + + return 0 +} + +# Layer 2: CLI tool detection from extracted commands (high confidence) +detect_platforms_from_commands() { + local tool + for idx in "${!DISPLAY_LINES[@]}"; do + [[ "${LINE_TYPES[$idx]}" != "command" ]] && continue + _check_tool_name "${LINE_CMDS[$idx]}" + [[ -z "$_CT_RESULT" ]] && continue + tool="$_CT_RESULT" + + case "$tool" in + wrangler) _platform_add "cloudflare" "Cloudflare" "high" ;; + vercel) _platform_add "vercel" "Vercel" "high" ;; + netlify) _platform_add "netlify" "Netlify" "high" ;; + flyctl|fly) _platform_add "fly" "Fly.io" "high" ;; + heroku) _platform_add "heroku" "Heroku" "high" ;; + firebase) _platform_add "firebase" "Firebase" "high" ;; + kubectl) _platform_add "kubernetes" "Kubernetes" "high" ;; + helm) _platform_add "helm" "Helm" "high" ;; + kamal) _platform_add "kamal" "Kamal" "high" ;; + terraform) _platform_add "terraform" "Terraform" "high" ;; + pulumi) _platform_add "pulumi" "Pulumi" "high" ;; + railway) _platform_add "railway" "Railway" "high" ;; + serverless|sls) _platform_add "serverless" "Serverless" "high" ;; + sam) _platform_add "awssam" "AWS SAM" "high" ;; + cdk) _platform_add "awscdk" "AWS CDK" "high" ;; + dokku) _platform_add "dokku" "Dokku" "high" ;; + surge) _platform_add "surge" "Surge" "high" ;; + esac + done +} + +# Layer 3: Prose mention detection in deploy section bodies (low confidence) +# Matches are case-insensitive. Captures the most specific variant mentioned. +detect_platforms_from_prose() { + local body + for body in "${SECTION_BODIES[@]+"${SECTION_BODIES[@]}"}"; do + [[ -z "$body" ]] && continue + + shopt -s nocasematch + if [[ "$body" =~ Cloudflare[[:space:]]Pages ]]; then _platform_add "cloudflare" "Cloudflare Pages" "low"; fi + if [[ "$body" =~ Cloudflare[[:space:]]Workers ]]; then _platform_add "cloudflare" "Cloudflare Workers" "low"; fi + if [[ "$body" =~ Vercel ]]; then _platform_add "vercel" "Vercel" "low"; fi + if [[ "$body" =~ Netlify ]]; then _platform_add "netlify" "Netlify" "low"; fi + if [[ "$body" =~ Heroku ]]; then _platform_add "heroku" "Heroku" "low"; fi + if [[ "$body" =~ Fly\.io ]]; then _platform_add "fly" "Fly.io" "low"; fi + if [[ "$body" =~ GitHub[[:space:]]Pages ]]; then _platform_add "ghpages" "GitHub Pages" "low"; fi + if [[ "$body" =~ Dokku ]]; then _platform_add "dokku" "Dokku" "low"; fi + if [[ "$body" =~ Railway ]]; then _platform_add "railway" "Railway" "low"; fi + if [[ "$body" =~ Render ]]; then _platform_add "render" "Render" "low"; fi + if [[ "$body" =~ Firebase ]]; then _platform_add "firebase" "Firebase" "low"; fi + if [[ "$body" =~ AWS[[:space:]]Amplify ]]; then _platform_add "amplify" "AWS Amplify" "low"; fi + if [[ "$body" =~ DigitalOcean ]]; then _platform_add "digitalocean" "DigitalOcean" "low"; fi + if [[ "$body" =~ Kamal ]]; then _platform_add "kamal" "Kamal" "low"; fi + if [[ "$body" =~ Surge ]]; then _platform_add "surge" "Surge" "low"; fi + shopt -u nocasematch + done +} + +# Build a display string from detected platforms. +# High-confidence names are plain; low-confidence get a "?" suffix +# Sets _PLATFORM_DISPLAY (empty string if no platforms detected) +_PLATFORM_DISPLAY="" +build_platform_display() { + _PLATFORM_DISPLAY="" + if (( ${#PLATFORM_NAMES[@]} == 0 )); then return; fi + local parts="" + for i in "${!PLATFORM_NAMES[@]}"; do + if [[ -n "$parts" ]]; then parts+=", "; fi + parts+="${PLATFORM_NAMES[$i]}" + if [[ "${PLATFORM_CONFIDENCE[$i]}" == "low" ]]; then parts+="?"; fi + done + _PLATFORM_DISPLAY="$parts" +} + # ── JSON output ─────────────────────────────────────────────────────────────── # Escape a string for safe embedding in JSON @@ -1540,6 +1686,25 @@ _json_check() { printf '\n ]' } +# Print the platforms array as JSON using the current PLATFORM_* arrays +_json_platforms() { + printf '[' + local first=true + for i in "${!PLATFORM_NAMES[@]}"; do + $first || printf ',' + first=false + printf '\n {"name": "%s", "group": "%s", "confidence": "%s"}' \ + "$(_json_esc "${PLATFORM_NAMES[$i]}")" \ + "$(_json_esc "${PLATFORM_GROUPS[$i]}")" \ + "${PLATFORM_CONFIDENCE[$i]}" + done + if (( ${#PLATFORM_NAMES[@]} > 0 )); then + printf '\n ]' + else + printf ']' + fi +} + # Main JSON renderer: outputs all modes, fullProse, and check render_json() { local _modes=("default" "install" "run" "test" "deploy" "all") @@ -1578,6 +1743,22 @@ render_json() { build_display_list _json_check + # Platform detection (uses deploy pattern) + printf ',\n "platforms": ' + _json_set_pattern "deploy" + SECTION_TITLES=(); SECTION_BODIES=() + parse_sections < "$README" + DISPLAY_LINES=(); LINE_TYPES=(); LINE_CMDS=(); CMD_INDICES=() + SECTION_FIRST_CMD=() + build_display_list + PLATFORM_GROUPS=(); PLATFORM_NAMES=(); PLATFORM_CONFIDENCE=() + local _pdir="$DIR" + if [[ -n "$FILE" ]]; then _pdir="${FILE%/*}"; fi + detect_platforms_from_files "$_pdir" + detect_platforms_from_commands + detect_platforms_from_prose + _json_platforms + printf '\n}\n' } @@ -1599,6 +1780,16 @@ fi PROJECT_NAME=$(basename "$(cd "$DIR" && pwd)") build_display_list +# Platform detection (deploy mode only, including interactive) +if [[ "$MODE" == "deploy" ]]; then + _project_dir="$DIR" + if [[ -n "$FILE" ]]; then _project_dir="${FILE%/*}"; fi + detect_platforms_from_files "$_project_dir" + detect_platforms_from_commands + detect_platforms_from_prose + build_platform_display +fi + if [[ "$MODE" == "check" ]]; then run_check elif [[ "$INTERACTIVE" == "yes" ]] && ! $FULL; then @@ -1610,7 +1801,13 @@ else install) printf " %s[install]%s" "$DIM" "$RESET" ;; run) printf " %s[run]%s" "$DIM" "$RESET" ;; test) printf " %s[test]%s" "$DIM" "$RESET" ;; - deploy) printf " %s[deploy]%s" "$DIM" "$RESET" ;; + deploy) + if [[ -n "${_PLATFORM_DISPLAY:-}" ]]; then + printf " %s[deploy → %s%s%s%s]%s" "$DIM" "$RESET" "$CYAN" "$_PLATFORM_DISPLAY" "$DIM" "$RESET" + else + printf " %s[deploy]%s" "$DIM" "$RESET" + fi + ;; all) printf " %s[all]%s" "$DIM" "$RESET" ;; esac printf "\n" diff --git a/src/header.sh b/src/header.sh index 72601a9..03ed3c9 100644 --- a/src/header.sh +++ b/src/header.sh @@ -2,14 +2,14 @@ # hdi - "How do I..." - Extracts setup/run/test commands from a README. # # Usage: -# hdi Interactive command picker (default in a terminal) -# hdi install Just install/setup commands -# hdi run Just run/start commands -# hdi test Just test commands -# hdi deploy Just deploy/release commands -# hdi all Show all matched sections +# hdi Interactive picker - shows all sections (default) +# hdi install Just install/setup commands (aliases: setup, i) +# hdi run Just run/start commands (aliases: start, r) +# hdi test Just test commands (alias: t) +# hdi deploy Just deploy/release commands and platform detection (alias: d) +# hdi all Show all matched sections (currently the default mode) # hdi check Check if required tools are installed (experimental) -# hdi [mode] --no-interactive Print commands without the picker +# hdi [mode] --no-interactive Print commands without the picker (alias: --ni) # hdi [mode] --full Include prose around commands # hdi [mode] --raw Plain markdown output (no colour, good for piping) # hdi --json Structured JSON output (includes all sections) @@ -22,9 +22,6 @@ # Enter Execute the highlighted command # c Copy highlighted command to clipboard # q / Esc / Ctrl+C Quit -# -# Aliases: "install" = "setup" = "i", "run" = "start" = "r", "test" = "t", -# "deploy" = "d", "check" = "c" set -euo pipefail diff --git a/src/json.sh b/src/json.sh index a03daf8..469a1c3 100644 --- a/src/json.sh +++ b/src/json.sh @@ -200,6 +200,25 @@ _json_check() { printf '\n ]' } +# Print the platforms array as JSON using the current PLATFORM_* arrays +_json_platforms() { + printf '[' + local first=true + for i in "${!PLATFORM_NAMES[@]}"; do + $first || printf ',' + first=false + printf '\n {"name": "%s", "group": "%s", "confidence": "%s"}' \ + "$(_json_esc "${PLATFORM_NAMES[$i]}")" \ + "$(_json_esc "${PLATFORM_GROUPS[$i]}")" \ + "${PLATFORM_CONFIDENCE[$i]}" + done + if (( ${#PLATFORM_NAMES[@]} > 0 )); then + printf '\n ]' + else + printf ']' + fi +} + # Main JSON renderer: outputs all modes, fullProse, and check render_json() { local _modes=("default" "install" "run" "test" "deploy" "all") @@ -238,5 +257,21 @@ render_json() { build_display_list _json_check + # Platform detection (uses deploy pattern) + printf ',\n "platforms": ' + _json_set_pattern "deploy" + SECTION_TITLES=(); SECTION_BODIES=() + parse_sections < "$README" + DISPLAY_LINES=(); LINE_TYPES=(); LINE_CMDS=(); CMD_INDICES=() + SECTION_FIRST_CMD=() + build_display_list + PLATFORM_GROUPS=(); PLATFORM_NAMES=(); PLATFORM_CONFIDENCE=() + local _pdir="$DIR" + if [[ -n "$FILE" ]]; then _pdir="${FILE%/*}"; fi + detect_platforms_from_files "$_pdir" + detect_platforms_from_commands + detect_platforms_from_prose + _json_platforms + printf '\n}\n' } diff --git a/src/main.sh b/src/main.sh index 56903ac..cb3e0b7 100644 --- a/src/main.sh +++ b/src/main.sh @@ -16,6 +16,16 @@ fi PROJECT_NAME=$(basename "$(cd "$DIR" && pwd)") build_display_list +# Platform detection (deploy mode only, including interactive) +if [[ "$MODE" == "deploy" ]]; then + _project_dir="$DIR" + if [[ -n "$FILE" ]]; then _project_dir="${FILE%/*}"; fi + detect_platforms_from_files "$_project_dir" + detect_platforms_from_commands + detect_platforms_from_prose + build_platform_display +fi + if [[ "$MODE" == "check" ]]; then run_check elif [[ "$INTERACTIVE" == "yes" ]] && ! $FULL; then @@ -27,7 +37,13 @@ else install) printf " %s[install]%s" "$DIM" "$RESET" ;; run) printf " %s[run]%s" "$DIM" "$RESET" ;; test) printf " %s[test]%s" "$DIM" "$RESET" ;; - deploy) printf " %s[deploy]%s" "$DIM" "$RESET" ;; + deploy) + if [[ -n "${_PLATFORM_DISPLAY:-}" ]]; then + printf " %s[deploy → %s%s%s%s]%s" "$DIM" "$RESET" "$CYAN" "$_PLATFORM_DISPLAY" "$DIM" "$RESET" + else + printf " %s[deploy]%s" "$DIM" "$RESET" + fi + ;; all) printf " %s[all]%s" "$DIM" "$RESET" ;; esac printf "\n" diff --git a/src/picker.sh b/src/picker.sh index 0b16e30..d638257 100644 --- a/src/picker.sh +++ b/src/picker.sh @@ -148,10 +148,16 @@ draw_picker() { install) hdr+=" ${DIM}[install]${RESET}" ;; run) hdr+=" ${DIM}[run]${RESET}" ;; test) hdr+=" ${DIM}[test]${RESET}" ;; + deploy) + if [[ -n "${_PLATFORM_DISPLAY:-}" ]]; then + hdr+=" ${DIM}[deploy → ${RESET}${CYAN}${_PLATFORM_DISPLAY}${DIM}]${RESET}" + else + hdr+=" ${DIM}[deploy]${RESET}" + fi + ;; all) hdr+=" ${DIM}[all]${RESET}" ;; esac _line "$hdr" - local chrome=3 # Scroll-up indicator (only if meaningful content is above the viewport) @@ -264,6 +270,11 @@ run_interactive() { local num_cmds=${#CMD_INDICES[@]} if (( num_cmds == 0 )); then + if [[ -n "${_PLATFORM_DISPLAY:-}" ]]; then + printf "%s%s[hdi] %s%s %s[deploy → %s%s%s%s]%s\n\n" \ + "$BOLD" "$YELLOW" "$PROJECT_NAME" "$RESET" \ + "$DIM" "$RESET" "$CYAN" "$_PLATFORM_DISPLAY" "$DIM" "$RESET" + fi echo "${YELLOW}hdi: no commands to pick from${RESET}" >&2 echo "${DIM}Try: hdi all --full${RESET}" >&2 exit 1 diff --git a/src/platform.sh b/src/platform.sh new file mode 100644 index 0000000..b8ccca9 --- /dev/null +++ b/src/platform.sh @@ -0,0 +1,137 @@ +# ── Platform detection ──────────────────────────────────────────────────────── +# Detects deployment platforms from three sources: +# 1. Config files in the project directory (high confidence) +# 2. CLI tools in extracted commands (high confidence) +# 3. Platform names mentioned in deploy section prose (low confidence) + +declare -a PLATFORM_GROUPS=() +declare -a PLATFORM_NAMES=() +declare -a PLATFORM_CONFIDENCE=() # "high" | "low" + +# Add or upgrade a platform detection. Deduplicates by group key +# If the group already exists: upgrade confidence to high if applicable, +# and prefer the longer (more specific) name +_platform_add() { + local group="$1" name="$2" confidence="$3" + for i in "${!PLATFORM_GROUPS[@]}"; do + if [[ "${PLATFORM_GROUPS[$i]}" == "$group" ]]; then + if [[ "$confidence" == "high" ]]; then + PLATFORM_CONFIDENCE[i]="high" + fi + if (( ${#name} > ${#PLATFORM_NAMES[i]} )); then + PLATFORM_NAMES[i]="$name" + fi + return + fi + done + PLATFORM_GROUPS+=("$group") + PLATFORM_NAMES+=("$name") + PLATFORM_CONFIDENCE+=("$confidence") +} + +# Layer 1: Config file detection (high confidence) +detect_platforms_from_files() { + local dir="$1" + + if [[ -f "$dir/wrangler.toml" || -f "$dir/wrangler.json" ]]; then _platform_add "cloudflare" "Cloudflare" "high"; fi + if [[ -f "$dir/vercel.json" ]]; then _platform_add "vercel" "Vercel" "high"; fi + if [[ -f "$dir/netlify.toml" ]]; then _platform_add "netlify" "Netlify" "high"; fi + if [[ -f "$dir/fly.toml" ]]; then _platform_add "fly" "Fly.io" "high"; fi + if [[ -f "$dir/Procfile" ]]; then _platform_add "heroku" "Heroku" "high"; fi + if [[ -f "$dir/render.yaml" ]]; then _platform_add "render" "Render" "high"; fi + if [[ -f "$dir/firebase.json" ]]; then _platform_add "firebase" "Firebase" "high"; fi + if [[ -f "$dir/amplify.yml" ]]; then _platform_add "amplify" "AWS Amplify" "high"; fi + if [[ -f "$dir/serverless.yml" || -f "$dir/serverless.ts" ]]; then _platform_add "serverless" "Serverless" "high"; fi + if [[ -f "$dir/cdk.json" ]]; then _platform_add "awscdk" "AWS CDK" "high"; fi + if [[ -f "$dir/pulumi.yaml" ]]; then _platform_add "pulumi" "Pulumi" "high"; fi + if [[ -f "$dir/railway.json" || -f "$dir/railway.toml" ]]; then _platform_add "railway" "Railway" "high"; fi + if [[ -f "$dir/Chart.yaml" ]]; then _platform_add "helm" "Helm" "high"; fi + if [[ -f "$dir/CNAME" ]]; then _platform_add "ghpages" "GitHub Pages" "high"; fi + if [[ -d "$dir/k8s" || -d "$dir/kubernetes" ]]; then _platform_add "kubernetes" "Kubernetes" "high"; fi + if [[ -d "$dir/.kamal" || -f "$dir/config/deploy.yml" ]]; then _platform_add "kamal" "Kamal" "high"; fi + + # Terraform: glob for *.tf files + local _tf + for _tf in "$dir"/*.tf; do + if [[ -f "$_tf" ]]; then + _platform_add "terraform" "Terraform" "high" + break + fi + done + + return 0 +} + +# Layer 2: CLI tool detection from extracted commands (high confidence) +detect_platforms_from_commands() { + local tool + for idx in "${!DISPLAY_LINES[@]}"; do + [[ "${LINE_TYPES[$idx]}" != "command" ]] && continue + _check_tool_name "${LINE_CMDS[$idx]}" + [[ -z "$_CT_RESULT" ]] && continue + tool="$_CT_RESULT" + + case "$tool" in + wrangler) _platform_add "cloudflare" "Cloudflare" "high" ;; + vercel) _platform_add "vercel" "Vercel" "high" ;; + netlify) _platform_add "netlify" "Netlify" "high" ;; + flyctl|fly) _platform_add "fly" "Fly.io" "high" ;; + heroku) _platform_add "heroku" "Heroku" "high" ;; + firebase) _platform_add "firebase" "Firebase" "high" ;; + kubectl) _platform_add "kubernetes" "Kubernetes" "high" ;; + helm) _platform_add "helm" "Helm" "high" ;; + kamal) _platform_add "kamal" "Kamal" "high" ;; + terraform) _platform_add "terraform" "Terraform" "high" ;; + pulumi) _platform_add "pulumi" "Pulumi" "high" ;; + railway) _platform_add "railway" "Railway" "high" ;; + serverless|sls) _platform_add "serverless" "Serverless" "high" ;; + sam) _platform_add "awssam" "AWS SAM" "high" ;; + cdk) _platform_add "awscdk" "AWS CDK" "high" ;; + dokku) _platform_add "dokku" "Dokku" "high" ;; + surge) _platform_add "surge" "Surge" "high" ;; + esac + done +} + +# Layer 3: Prose mention detection in deploy section bodies (low confidence) +# Matches are case-insensitive. Captures the most specific variant mentioned. +detect_platforms_from_prose() { + local body + for body in "${SECTION_BODIES[@]+"${SECTION_BODIES[@]}"}"; do + [[ -z "$body" ]] && continue + + shopt -s nocasematch + if [[ "$body" =~ Cloudflare[[:space:]]Pages ]]; then _platform_add "cloudflare" "Cloudflare Pages" "low"; fi + if [[ "$body" =~ Cloudflare[[:space:]]Workers ]]; then _platform_add "cloudflare" "Cloudflare Workers" "low"; fi + if [[ "$body" =~ Vercel ]]; then _platform_add "vercel" "Vercel" "low"; fi + if [[ "$body" =~ Netlify ]]; then _platform_add "netlify" "Netlify" "low"; fi + if [[ "$body" =~ Heroku ]]; then _platform_add "heroku" "Heroku" "low"; fi + if [[ "$body" =~ Fly\.io ]]; then _platform_add "fly" "Fly.io" "low"; fi + if [[ "$body" =~ GitHub[[:space:]]Pages ]]; then _platform_add "ghpages" "GitHub Pages" "low"; fi + if [[ "$body" =~ Dokku ]]; then _platform_add "dokku" "Dokku" "low"; fi + if [[ "$body" =~ Railway ]]; then _platform_add "railway" "Railway" "low"; fi + if [[ "$body" =~ Render ]]; then _platform_add "render" "Render" "low"; fi + if [[ "$body" =~ Firebase ]]; then _platform_add "firebase" "Firebase" "low"; fi + if [[ "$body" =~ AWS[[:space:]]Amplify ]]; then _platform_add "amplify" "AWS Amplify" "low"; fi + if [[ "$body" =~ DigitalOcean ]]; then _platform_add "digitalocean" "DigitalOcean" "low"; fi + if [[ "$body" =~ Kamal ]]; then _platform_add "kamal" "Kamal" "low"; fi + if [[ "$body" =~ Surge ]]; then _platform_add "surge" "Surge" "low"; fi + shopt -u nocasematch + done +} + +# Build a display string from detected platforms. +# High-confidence names are plain; low-confidence get a "?" suffix +# Sets _PLATFORM_DISPLAY (empty string if no platforms detected) +_PLATFORM_DISPLAY="" +build_platform_display() { + _PLATFORM_DISPLAY="" + if (( ${#PLATFORM_NAMES[@]} == 0 )); then return; fi + local parts="" + for i in "${!PLATFORM_NAMES[@]}"; do + if [[ -n "$parts" ]]; then parts+=", "; fi + parts+="${PLATFORM_NAMES[$i]}" + if [[ "${PLATFORM_CONFIDENCE[$i]}" == "low" ]]; then parts+="?"; fi + done + _PLATFORM_DISPLAY="$parts" +} diff --git a/test/fixtures/platform-cloudflare/README.md b/test/fixtures/platform-cloudflare/README.md new file mode 100644 index 0000000..0da11db --- /dev/null +++ b/test/fixtures/platform-cloudflare/README.md @@ -0,0 +1,17 @@ +# my-worker + +A Cloudflare Workers project. + +## Install + +```bash +npm install +``` + +## Deploy + +Deploy to Cloudflare Pages: + +```bash +wrangler deploy +``` diff --git a/test/fixtures/platform-cloudflare/wrangler.toml b/test/fixtures/platform-cloudflare/wrangler.toml new file mode 100644 index 0000000..35521f5 --- /dev/null +++ b/test/fixtures/platform-cloudflare/wrangler.toml @@ -0,0 +1,3 @@ +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2024-01-01" diff --git a/test/fixtures/platform-cmd/README.md b/test/fixtures/platform-cmd/README.md new file mode 100644 index 0000000..75df10d --- /dev/null +++ b/test/fixtures/platform-cmd/README.md @@ -0,0 +1,16 @@ +# kamal-app + +A Rails app deployed with Kamal. + +## Install + +```bash +bundle install +``` + +## Deploy + +```bash +kamal setup +kamal deploy +``` diff --git a/test/fixtures/platform-multi/README.md b/test/fixtures/platform-multi/README.md new file mode 100644 index 0000000..3cd22a3 --- /dev/null +++ b/test/fixtures/platform-multi/README.md @@ -0,0 +1,23 @@ +# multi-deploy + +An app with multiple deploy targets. + +## Install + +```bash +npm install +``` + +## Deploy + +Deploy the frontend to Vercel: + +```bash +npx vercel --prod +``` + +Deploy the API to Netlify: + +```bash +netlify deploy --prod +``` diff --git a/test/fixtures/platform-multi/netlify.toml b/test/fixtures/platform-multi/netlify.toml new file mode 100644 index 0000000..a1680b2 --- /dev/null +++ b/test/fixtures/platform-multi/netlify.toml @@ -0,0 +1,3 @@ +[build] + command = "npm run build" + publish = "dist" diff --git a/test/fixtures/platform-multi/vercel.json b/test/fixtures/platform-multi/vercel.json new file mode 100644 index 0000000..45f2c43 --- /dev/null +++ b/test/fixtures/platform-multi/vercel.json @@ -0,0 +1,4 @@ +{ + "buildCommand": "npm run build", + "outputDirectory": "dist" +} diff --git a/test/fixtures/platform-none/README.md b/test/fixtures/platform-none/README.md new file mode 100644 index 0000000..a1308e7 --- /dev/null +++ b/test/fixtures/platform-none/README.md @@ -0,0 +1,7 @@ +# my-app + +## Deploy + +```bash +./scripts/deploy.sh +``` diff --git a/test/fixtures/platform-prose/README.md b/test/fixtures/platform-prose/README.md new file mode 100644 index 0000000..0a1fe29 --- /dev/null +++ b/test/fixtures/platform-prose/README.md @@ -0,0 +1,14 @@ +# static-site + +A static site deployed automatically. + +## Install + +```bash +npm install +``` + +## Deploy + +This site is deployed on GitHub Pages via the GitHub Actions workflow. +See `.github/workflows/pages.yml` for details. diff --git a/test/hdi.bats b/test/hdi.bats index 3fae04c..f2e1dc7 100644 --- a/test/hdi.bats +++ b/test/hdi.bats @@ -1509,3 +1509,107 @@ texts = [i['text'] for i in d['modes']['all'] if i['type'] == 'command'] assert '\"status\": \"ok\",' not in texts " } + +# ── Platform detection ───────────────────────────────────────────────────── + +@test "platform: config file detected (cloudflare)" { + run "$HDI" deploy --ni "$FIXTURES/platform-cloudflare" + [ "$status" -eq 0 ] + [[ "$output" == *"deploy → Cloudflare Pages"* ]] + # High confidence — no question mark + [[ "$output" != *"Cloudflare Pages?"* ]] +} + +@test "platform: multiple platforms detected" { + run "$HDI" deploy --ni "$FIXTURES/platform-multi" + [ "$status" -eq 0 ] + [[ "$output" == *"deploy → Vercel, Netlify"* ]] +} + +@test "platform: CLI tool detection (kamal)" { + run "$HDI" deploy --ni "$FIXTURES/platform-cmd" + [ "$status" -eq 0 ] + [[ "$output" == *"deploy → Kamal"* ]] + # High confidence from CLI tool — no question mark + [[ "$output" != *"Kamal?"* ]] +} + +@test "platform: prose-only detection is low confidence" { + run "$HDI" deploy --ni "$FIXTURES/platform-prose" + [ "$status" -eq 0 ] + [[ "$output" == *"deploy → GitHub Pages?"* ]] +} + +@test "platform: no platform detected shows plain [deploy]" { + run "$HDI" deploy --ni "$FIXTURES/platform-none" + [ "$status" -eq 0 ] + [[ "$output" == *"[deploy]"* ]] + [[ "$output" != *"deploy →"* ]] +} + +@test "platform: dedup — config + prose same group shows high confidence" { + run "$HDI" deploy --ni "$FIXTURES/platform-cloudflare" + [ "$status" -eq 0 ] + # wrangler.toml (file) + "Cloudflare Pages" (prose) = high confidence + [[ "$output" == *"deploy → Cloudflare Pages"* ]] + [[ "$output" != *"Cloudflare Pages?"* ]] +} + +@test "platform: --raw skips header (no platform shown)" { + run "$HDI" deploy --raw "$FIXTURES/platform-cloudflare" + [ "$status" -eq 0 ] + [[ "$output" != *"deploy →"* ]] + [[ "$output" == *"wrangler deploy"* ]] +} + +@test "platform: json output includes platforms array" { + run "$HDI" --json "$FIXTURES/platform-cloudflare" + [ "$status" -eq 0 ] + echo "$output" | python3 -c " +import json,sys +d=json.load(sys.stdin) +platforms = d['platforms'] +assert len(platforms) > 0, 'expected at least one platform' +names = [p['name'] for p in platforms] +assert 'Cloudflare Pages' in names, f'expected Cloudflare Pages in {names}' +cf = [p for p in platforms if p['name'] == 'Cloudflare Pages'][0] +assert cf['confidence'] == 'high', f'expected high confidence, got {cf[\"confidence\"]}' +assert cf['group'] == 'cloudflare' +" +} + +@test "platform: json output with no platforms returns empty array" { + run "$HDI" --json "$FIXTURES/platform-none" + [ "$status" -eq 0 ] + echo "$output" | python3 -c " +import json,sys +d=json.load(sys.stdin) +assert d['platforms'] == [], f'expected empty platforms, got {d[\"platforms\"]}' +" +} + +@test "platform: json output with multiple platforms" { + run "$HDI" --json "$FIXTURES/platform-multi" + [ "$status" -eq 0 ] + echo "$output" | python3 -c " +import json,sys +d=json.load(sys.stdin) +platforms = d['platforms'] +names = [p['name'] for p in platforms] +assert 'Vercel' in names, f'expected Vercel in {names}' +assert 'Netlify' in names, f'expected Netlify in {names}' +assert all(p['confidence'] == 'high' for p in platforms), 'expected all high confidence' +" +} + +@test "platform: not shown in install mode" { + run "$HDI" install --ni "$FIXTURES/platform-cloudflare" + [ "$status" -eq 0 ] + [[ "$output" != *"Cloudflare"* ]] +} + +@test "platform: not shown in run mode" { + run "$HDI" run --ni "$FIXTURES/react-nextjs" + [ "$status" -eq 0 ] + [[ "$output" != *"Vercel"* ]] +}