feat(ce-polish-beta): human-in-the-loop polish phase between /ce:review and merge (#568)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
308
plugins/compound-engineering/skills/ce-polish-beta/scripts/resolve-port.sh
Executable file
308
plugins/compound-engineering/skills/ce-polish-beta/scripts/resolve-port.sh
Executable file
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# resolve-port.sh -- resolve the dev-server port for a project.
|
||||
#
|
||||
# Usage:
|
||||
# resolve-port.sh [path] [--type <type>] [--port <n>]
|
||||
#
|
||||
# Arguments:
|
||||
# path (optional) -- project root directory. Defaults to the git repo root.
|
||||
# --type (optional) -- framework type to scope probes (rails|next|vite|nuxt|
|
||||
# astro|remix|sveltekit|procfile). Unset runs all probes.
|
||||
# --port (optional) -- explicit port override. Emitted immediately when present.
|
||||
#
|
||||
# Output:
|
||||
# Single line on stdout: the resolved port number.
|
||||
# stderr is reserved for ERROR: messages only.
|
||||
#
|
||||
# Probe order (FIRST HIT WINS):
|
||||
#
|
||||
# 1. Explicit --port flag
|
||||
# 2. Framework config files (next.config.*, vite.config.*, nuxt.config.*,
|
||||
# astro.config.*) -- conservative regex matching only numeric literal
|
||||
# port values. Variable references like process.env.PORT or getPort()
|
||||
# are deliberately not matched; the probe falls through.
|
||||
# 3. Rails: config/puma.rb for `port <n>`
|
||||
# 4. Procfile.dev: web line scanned for -p/-p=<n>/--port/--port=<n>
|
||||
# 5. docker-compose.yml: line-anchored grep for "- "<n>:<n>"" port mapping
|
||||
# 6. package.json: dev/start script for --port/-p flags
|
||||
# 7. .env files in override order: .env.local -> .env.development -> .env
|
||||
# (first hit wins). Values are parsed with quote stripping (" and ')
|
||||
# and comment truncation (at #, after trimming whitespace).
|
||||
# 8. Framework default lookup table
|
||||
#
|
||||
# Why config-before-prose: framework config files are the most reliable source
|
||||
# of truth for the intended port; instruction files and env files are often
|
||||
# stale or overridden. Prose files (AGENTS.md, CLAUDE.md) are deliberately NOT
|
||||
# scanned -- they carry natural language that may mention ports in contexts
|
||||
# unrelated to the dev server (documentation, examples, troubleshooting).
|
||||
# Scanning them produces false positives that are hard to debug.
|
||||
#
|
||||
# .env parsing contract: surrounding double or single quotes are stripped.
|
||||
# Inline comments (# ...) are truncated after trimming whitespace. This is
|
||||
# intentionally more aggressive than the test-browser skill's inline cascade,
|
||||
# which does neither. See dev-server-detection.md for the divergence notes.
|
||||
|
||||
set -u
|
||||
|
||||
# ── Argument parsing ─────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT_ROOT=""
|
||||
PROJ_TYPE=""
|
||||
EXPLICIT_PORT=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--type)
|
||||
PROJ_TYPE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--port)
|
||||
EXPLICIT_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
if [ -z "$PROJECT_ROOT" ]; then
|
||||
PROJECT_ROOT="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Default to git repo root when no positional path is given.
|
||||
if [ -z "$PROJECT_ROOT" ]; then
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [ -z "$PROJECT_ROOT" ]; then
|
||||
echo "ERROR: not in a git repository and no path provided" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$PROJECT_ROOT" ]; then
|
||||
echo "ERROR: path does not exist: $PROJECT_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# should_probe TYPE PROBE_NAME
|
||||
# Returns 0 (true) if the probe should run for the given --type.
|
||||
should_probe() {
|
||||
local ptype="$1"
|
||||
local probe="$2"
|
||||
|
||||
if [ -z "$ptype" ]; then
|
||||
return 0 # no type filter -- run all probes
|
||||
fi
|
||||
|
||||
case "$ptype" in
|
||||
rails)
|
||||
case "$probe" in
|
||||
puma|procfile|docker-compose|env|default) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
next|nuxt|astro|remix|vite|sveltekit)
|
||||
case "$probe" in
|
||||
framework-config|package-json|env|default) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
procfile)
|
||||
case "$probe" in
|
||||
procfile|docker-compose|env|default) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
return 0 # unknown type -- run all probes
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# parse_env_port FILE
|
||||
# Parses PORT=<n> from the given file. Strips surrounding quotes and inline
|
||||
# comments. Prints the port on stdout or nothing.
|
||||
parse_env_port() {
|
||||
local envfile="$1"
|
||||
if [ ! -f "$envfile" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local line
|
||||
line=$(grep -E '^PORT=' "$envfile" 2>/dev/null | tail -1)
|
||||
if [ -z "$line" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract value after PORT=
|
||||
local value
|
||||
value="${line#PORT=}"
|
||||
|
||||
# Trim whitespace, then truncate at # (inline comment) -- comment stripping
|
||||
# must happen BEFORE quote stripping so PORT="3001" # comment -> "3001" -> 3001
|
||||
value=$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*#.*$//;s/[[:space:]]*$//')
|
||||
|
||||
# Strip surrounding double quotes
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
|
||||
# Strip surrounding single quotes
|
||||
value="${value%\'}"
|
||||
value="${value#\'}"
|
||||
|
||||
# Trim any remaining whitespace
|
||||
value=$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
if [ -n "$value" ]; then
|
||||
printf '%s' "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Probe 1: Explicit --port flag ────────────────────────────────────────────
|
||||
|
||||
if [ -n "$EXPLICIT_PORT" ]; then
|
||||
echo "$EXPLICIT_PORT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Probe 2: Framework config files ─────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "framework-config"; then
|
||||
for cfg in \
|
||||
"$PROJECT_ROOT"/next.config.js \
|
||||
"$PROJECT_ROOT"/next.config.ts \
|
||||
"$PROJECT_ROOT"/next.config.mjs \
|
||||
"$PROJECT_ROOT"/next.config.cjs \
|
||||
"$PROJECT_ROOT"/vite.config.js \
|
||||
"$PROJECT_ROOT"/vite.config.ts \
|
||||
"$PROJECT_ROOT"/vite.config.mjs \
|
||||
"$PROJECT_ROOT"/vite.config.cjs \
|
||||
"$PROJECT_ROOT"/nuxt.config.js \
|
||||
"$PROJECT_ROOT"/nuxt.config.ts \
|
||||
"$PROJECT_ROOT"/nuxt.config.mjs \
|
||||
"$PROJECT_ROOT"/nuxt.config.cjs \
|
||||
"$PROJECT_ROOT"/astro.config.js \
|
||||
"$PROJECT_ROOT"/astro.config.ts \
|
||||
"$PROJECT_ROOT"/astro.config.mjs \
|
||||
"$PROJECT_ROOT"/astro.config.cjs \
|
||||
; do
|
||||
if [ ! -f "$cfg" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Conservative regex: match "port:" + digits, then verify nothing non-numeric
|
||||
# follows (rejects variable references like "port: process.env.PORT || 3000").
|
||||
local_line=$(grep -E 'port:[[:space:]]*["'"'"']?[0-9]+' "$cfg" 2>/dev/null | head -1)
|
||||
if [ -z "$local_line" ]; then continue; fi
|
||||
|
||||
local_port=$(printf '%s' "$local_line" | grep -Eo 'port:[[:space:]]*["'"'"']?[0-9]+["'"'"']?' | head -1 | grep -Eo '[0-9]+')
|
||||
if [ -n "$local_port" ]; then
|
||||
local_after=$(printf '%s' "$local_line" | sed "s/.*port:[[:space:]]*[\"']*${local_port}[\"']*//" )
|
||||
if [ -z "$local_after" ] || printf '%s' "$local_after" | grep -qE '^[[:space:],})]*$'; then
|
||||
echo "$local_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Probe 3: Rails config/puma.rb ───────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "puma"; then
|
||||
puma_file="$PROJECT_ROOT/config/puma.rb"
|
||||
if [ -f "$puma_file" ]; then
|
||||
puma_port=$(grep -Eo 'port[[:space:]]+[0-9]+' "$puma_file" 2>/dev/null | head -1 | grep -Eo '[0-9]+')
|
||||
if [ -n "$puma_port" ]; then
|
||||
echo "$puma_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Probe 4: Procfile.dev ───────────────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "procfile"; then
|
||||
procfile="$PROJECT_ROOT/Procfile.dev"
|
||||
if [ -f "$procfile" ]; then
|
||||
# Extract the web line
|
||||
web_line=$(grep -E '^web:' "$procfile" 2>/dev/null | head -1)
|
||||
if [ -n "$web_line" ]; then
|
||||
# Match -p <n>, -p<n>, --port <n>, -p=<n>, --port=<n>
|
||||
proc_port=$(printf '%s' "$web_line" | grep -Eo '(-p[= ]*|--port[= ]+)[0-9]+' | head -1 | grep -Eo '[0-9]+')
|
||||
if [ -n "$proc_port" ]; then
|
||||
echo "$proc_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Probe 5: docker-compose.yml ─────────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "docker-compose"; then
|
||||
compose_file="$PROJECT_ROOT/docker-compose.yml"
|
||||
if [ -f "$compose_file" ]; then
|
||||
# Simple line-anchored grep for port mappings: - "NNNN:NNNN" or - NNNN:NNNN
|
||||
compose_port=$(grep -Eo '"[0-9]+:[0-9]+"' "$compose_file" 2>/dev/null | head -1 | grep -Eo '[0-9]+' | head -1)
|
||||
if [ -n "$compose_port" ]; then
|
||||
echo "$compose_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Probe 6: package.json scripts ───────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "package-json"; then
|
||||
pkg_file="$PROJECT_ROOT/package.json"
|
||||
if [ -f "$pkg_file" ]; then
|
||||
# Look for --port or -p in dev/start scripts
|
||||
pkg_port=$(grep -Eo '(-p[= ]+|--port[= ]+)[0-9]+' "$pkg_file" 2>/dev/null | head -1 | grep -Eo '[0-9]+')
|
||||
if [ -n "$pkg_port" ]; then
|
||||
echo "$pkg_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Probe 7: .env files ─────────────────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "env"; then
|
||||
for envfile in \
|
||||
"$PROJECT_ROOT/.env.local" \
|
||||
"$PROJECT_ROOT/.env.development" \
|
||||
"$PROJECT_ROOT/.env" \
|
||||
; do
|
||||
env_port=$(parse_env_port "$envfile")
|
||||
if [ -n "$env_port" ]; then
|
||||
echo "$env_port"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Probe 8: Framework default lookup table ──────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "default"; then
|
||||
case "$PROJ_TYPE" in
|
||||
rails|next|nuxt|remix|procfile|"")
|
||||
echo "3000"
|
||||
;;
|
||||
vite|sveltekit)
|
||||
echo "5173"
|
||||
;;
|
||||
astro)
|
||||
echo "4321"
|
||||
;;
|
||||
*)
|
||||
echo "3000"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Final fallback (should not normally be reached)
|
||||
echo "3000"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user