|
#!/bin/bash |
|
|
|
PGROK_VERSION="0.0.2" |
|
PGROK_AUTHOR="nijikokun" |
|
PROFILE_DIR="$HOME/.ngrok-profiles" |
|
BACKUP_FILE="$PROFILE_DIR/.backup" |
|
CURRENT_FILE="$PROFILE_DIR/.current" |
|
DEFAULT_PROFILE="default" |
|
|
|
# Determine if we're running in dev mode |
|
USE_ND=0 |
|
if [[ "$1" == "--nd" ]]; then |
|
USE_ND=1 |
|
shift |
|
fi |
|
|
|
# Use the appropriate ngrok runner |
|
if [[ "$USE_ND" -eq 1 ]]; then |
|
NGROK_PREFIX="nd ngrok --" |
|
else |
|
NGROK_PREFIX="ngrok" |
|
fi |
|
|
|
# Detect default config path via ngrok itself |
|
CONFIG_FILE=$(eval "$NGROK_PREFIX config check 2>/dev/null" | grep -oE '/.*ngrok.yml') |
|
if [ -z "$CONFIG_FILE" ]; then |
|
echo "Could not detect ngrok config path using '$NGROK_PREFIX config check'" |
|
exit 1 |
|
fi |
|
|
|
first_run_setup() { |
|
# Create dirs if missing |
|
mkdir -p "$PROFILE_DIR" |
|
mkdir -p "$(dirname "$CONFIG_FILE")" |
|
|
|
# First run setup |
|
if [ ! -f "$CURRENT_FILE" ]; then |
|
echo "[ngrok-profile] First-time setup..." |
|
|
|
# Backup current config (if any) |
|
if [ -f "$CONFIG_FILE" ]; then |
|
cp "$CONFIG_FILE" "$PROFILE_DIR/.backup" |
|
echo "[ngrok-profile] Backed up $CONFIG_FILE to $PROFILE_DIR/.backup" |
|
|
|
mkdir -p "$PROFILE_DIR/default" |
|
cp "$CONFIG_FILE" "$PROFILE_DIR/default/ngrok.yml" |
|
echo "[ngrok-profile] Created default profile from current config" |
|
else |
|
# Create empty default profile |
|
mkdir -p "$PROFILE_DIR/default" |
|
echo -e "version: \"3\"\nagent:\n authtoken: \"\"" > "$PROFILE_DIR/default/ngrok.yml" |
|
echo "[ngrok-profile] Created blank default profile" |
|
fi |
|
|
|
# Link default profile |
|
ln -sf "$PROFILE_DIR/default/ngrok.yml" "$CONFIG_FILE" |
|
|
|
# Double check again |
|
[ -z "$CONFIG_FILE" ] && { |
|
echo "Could not detect ngrok config path using 'ngrok config check'"; |
|
exit 1; |
|
} |
|
|
|
echo "default" > "$CURRENT_FILE" |
|
echo "[ngrok-profile] Linked default profile to ngrok config path" |
|
echo "[ngrok-profile] Setup complete. Now using profile: default" |
|
fi |
|
} |
|
|
|
edit_profile_config() { |
|
local file="$1" |
|
if [ -n "$EDITOR" ]; then |
|
"$EDITOR" "$file" |
|
elif command -v nano >/dev/null 2>&1; then |
|
nano "$file" |
|
else |
|
vi "$file" |
|
fi |
|
} |
|
|
|
get_path() { |
|
echo "$PROFILE_DIR/$1/ngrok.yml" |
|
} |
|
|
|
get_current() { |
|
[ -f "$CURRENT_FILE" ] && cat "$CURRENT_FILE" || echo "$DEFAULT_PROFILE" |
|
} |
|
|
|
set_current() { |
|
echo "$1" > "$CURRENT_FILE" |
|
} |
|
|
|
backup_original_config() { |
|
if [ ! -f "$BACKUP_FILE" ] && [ -f "$CONFIG_FILE" ]; then |
|
cp "$CONFIG_FILE" "$BACKUP_FILE" |
|
echo "Backed up original config to $BACKUP_FILE" |
|
fi |
|
} |
|
|
|
restore_original_config() { |
|
if [ -f "$BACKUP_FILE" ]; then |
|
cp "$BACKUP_FILE" "$CONFIG_FILE" |
|
echo "Restored original ngrok config" |
|
else |
|
echo "No backup found to restore" |
|
fi |
|
} |
|
|
|
link_profile() { |
|
local profile="$1" |
|
local profile_config |
|
profile_config="$(get_path "$profile")" |
|
[ -f "$profile_config" ] || { echo "Profile '$profile' not found at $profile_config"; exit 1; } |
|
backup_original_config |
|
ln -sf "$profile_config" "$CONFIG_FILE" |
|
set_current "$profile" |
|
echo "Now using profile: $profile" |
|
} |
|
|
|
print_service_row() { |
|
local type="$1" |
|
local name="$2" |
|
local url="$3" |
|
local upstream_url="$4" |
|
|
|
# If upstream_url or addr is just digits, show as localhost:port |
|
if [[ "$upstream_url" =~ ^[0-9]+$ ]]; then |
|
upstream_url="::$upstream_url" |
|
fi |
|
if [[ "$url" =~ ^[0-9]+$ ]]; then |
|
url="::$url" |
|
fi |
|
|
|
printf "%-10s %-15s %-35s %-15s\n" "$type" "$name" "$url" "$upstream_url" |
|
} |
|
|
|
check_for_update() { |
|
# Set this to your public Gist raw version URL |
|
GIST_ID="87d3be3a47c7614190fed00b2d4af7c5" |
|
GIST_VERSION_URL="https://gist.githubusercontent.com/raw/${GIST_ID}/pgrok-version.txt" |
|
GIST_SOURCE_URL="https://gist.githubusercontent.com/raw/${GIST_ID}/pgrok.sh" |
|
latest_version=$(curl -fsSL "$GIST_VERSION_URL" 2>/dev/null | head -1 | tr -d '[:space:]') |
|
|
|
if [ -z "$latest_version" ]; then |
|
return |
|
fi |
|
|
|
if [ "$PGROK_VERSION" != "$latest_version" ]; then |
|
echo |
|
echo "🔔 A new version of pgrok is available: $latest_version (you have $PGROK_VERSION)" |
|
echo |
|
echo " To update run: " |
|
echo " curl -fsSL ${GIST_SOURCE_URL} -o ~/pgrok && chmod +x ~/pgrok && sudo mv ~/pgrok /usr/lib/bin" |
|
echo |
|
fi |
|
} |
|
|
|
usage() { |
|
cat <<EOF |
|
pgrok - Easily work across multiple ngrok accounts (profiles). |
|
|
|
Usage: |
|
pgrok use <name> # Activate profile by symlinking config |
|
pgrok list # List all profiles |
|
pgrok current # Show current profile |
|
pgrok create <name> [from] [--authtoken <token>] # Create new profile, optional copy or token |
|
pgrok delete <name> # Delete profile |
|
pgrok view [<name>] # View contents of current or specified profile |
|
pgrok services [<name>] # List the services that can be started on this profile |
|
pgrok edit [<name>] # Edit profile config |
|
pgrok set authtoken [<value>] # Set authtoken for current profile |
|
pgrok set api_key [<value>] # Set api_key for current profile |
|
pgrok cleanup # Remove all profiles and restore original config |
|
pgrok restore # Restore backed-up original config |
|
pgrok run [--nd] -- <args> # Run ngrok or nd ngrok with current profile |
|
|
|
Examples: |
|
pgrok create dev --authtoken <token> && ngrok http 80 # create new profile and run ngrok on it |
|
pgrok set authtoken mytoken123 # sets directly |
|
pgrok set authtoken # prompts securely |
|
pgrok set api_key # prompts securely |
|
pgrok services # list of services for current profile |
|
|
|
Notes: |
|
- the services and set commands require yq (https://github.com/mikefarah/yq) |
|
- the edit command respects the global \$EDITOR variable, yours is "$EDITOR" |
|
EOF |
|
exit 1 |
|
} |
|
|
|
# Check for updates |
|
check_for_update |
|
|
|
# First time setup |
|
first_run_setup |
|
|
|
# Command Handler |
|
case "$1" in |
|
use) |
|
[ -z "$2" ] && usage |
|
link_profile "$2" |
|
;; |
|
list) |
|
current="$(get_current)" |
|
echo "Available profiles:" |
|
find "$PROFILE_DIR" -name "ngrok.yml" | sed "s|.*/\([^/]*\)/ngrok.yml|\1|" | while read -r name; do |
|
if [ "$name" = "$current" ]; then |
|
echo " * $name (current)" |
|
else |
|
echo " $name" |
|
fi |
|
done |
|
;; |
|
current) |
|
echo "Current profile: $(get_current)" |
|
;; |
|
create) |
|
[ -z "$2" ] && usage |
|
name="$2" |
|
from="" |
|
authtoken="" |
|
shift 2 |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
--authtoken) |
|
authtoken="$2" |
|
shift 2 |
|
;; |
|
*) |
|
from="$1" |
|
shift |
|
;; |
|
esac |
|
done |
|
dest="$(get_path "$name")" |
|
mkdir -p "$(dirname "$dest")" |
|
if [ -n "$from" ]; then |
|
cp "$(get_path "$from")" "$dest" |
|
echo "Created profile '$name' from '$from'" |
|
link_profile "$name" |
|
else |
|
echo -e "version: \"3\"\nagent:" > "$dest" |
|
if [ -n "$authtoken" ]; then |
|
echo " authtoken: \"$authtoken\"" >> "$dest" |
|
else |
|
echo " authtoken: \"\"" >> "$dest" |
|
fi |
|
echo "Created new profile '$name'" |
|
link_profile "$name" |
|
fi |
|
;; |
|
delete) |
|
[ -z "$2" ] && usage |
|
rm -rf "$PROFILE_DIR/$2" |
|
echo "Deleted profile '$2'" |
|
;; |
|
view) |
|
profile="${2:-$(get_current)}" |
|
config="$(get_path "$profile")" |
|
if [ -f "$config" ]; then |
|
cat "$config" |
|
echo "" # avoid `%` at the end sometimes |
|
else |
|
echo "[ngrok-profile] Profile '$profile' does not exist at $config" |
|
exit 1 |
|
fi |
|
;; |
|
services) |
|
profile="${2:-$(get_current)}" |
|
config="$(get_path "$profile")" |
|
|
|
if [ ! -f "$config" ]; then |
|
echo "[ngrok-profile] Profile '$profile' does not exist at $config" |
|
exit 1 |
|
fi |
|
|
|
if ! command -v yq >/dev/null 2>&1; then |
|
echo "[ngrok-profile] 'yq' is required for this command. Install via 'brew install yq' or https://github.com/mikefarah/yq" |
|
exit 1 |
|
fi |
|
|
|
# Collect v3 endpoints: name, url, upstream_url, type |
|
endpoints=$(yq -r ' |
|
.endpoints[]? | |
|
["endpoint", .name, .url, (.upstream.url // "")] |
|
| @tsv |
|
' "$config") |
|
|
|
# Collect v2 tunnels: name, proto://domain, addr, type |
|
tunnels=$(yq -r ' |
|
.tunnels // {} | to_entries[] | |
|
[ |
|
"tunnel", |
|
.key, |
|
((.value.proto // "") + "://" + (.value.domain // "")), |
|
(.value.addr // "") |
|
] | @tsv |
|
' "$config") |
|
|
|
# Print merged table header |
|
printf "%-10s %-15s %-35s %-15s\n" "TYPE" "SERVICE NAME" "URL" "UPSTREAM URL" |
|
printf "%-10s %-15s %-35s %-15s\n" "----" "------------" "---" "------------" |
|
|
|
# Print endpoints |
|
if [ -n "$endpoints" ]; then |
|
echo "$endpoints" | while IFS=$'\t' read -r type name url upstream_url; do |
|
print_service_row "$type" "$name" "$url" "$upstream_url" |
|
done |
|
fi |
|
|
|
# Print tunnels |
|
if [ -n "$tunnels" ]; then |
|
echo "$tunnels" | while IFS=$'\t' read -r type name url upstream_url; do |
|
print_service_row "$type" "$name" "$url" "$upstream_url" |
|
done |
|
fi |
|
|
|
# If nothing found |
|
if [ -z "$endpoints" ] && [ -z "$tunnels" ]; then |
|
echo "[ngrok-profile] No endpoints or tunnels found in profile '$profile'." |
|
fi |
|
;; |
|
set) |
|
profile="$(get_current)" |
|
config="$(get_path "$profile")" |
|
[ ! -f "$config" ] && { echo "[ngrok-profile] Profile '$profile' does not exist at $config"; exit 1; } |
|
|
|
shift # remove 'set' |
|
|
|
if [[ $# -eq 2 ]]; then |
|
key="$1" |
|
value="$2" |
|
elif [[ $# -eq 1 ]]; then |
|
key="$1" |
|
# Prompt securely |
|
if [[ "$key" != "authtoken" && "$key" != "api_key" ]]; then |
|
echo "Error: Can only set authtoken or api_key" |
|
exit 1 |
|
fi |
|
echo -n "Enter value for $key (input hidden): " |
|
read -s value |
|
echo |
|
else |
|
echo "Usage: pgrok set authtoken <value>" |
|
echo " or: pgrok set api_key <value>" |
|
echo " or: pgrok set authtoken # will prompt securely" |
|
exit 1 |
|
fi |
|
|
|
if [[ "$key" != "authtoken" && "$key" != "api_key" ]]; then |
|
echo "Error: Can only set authtoken or api_key" |
|
exit 1 |
|
fi |
|
|
|
if ! command -v yq >/dev/null 2>&1; then |
|
echo "[ngrok-profile] 'yq' is required for this command." |
|
exit 1 |
|
fi |
|
|
|
yq -i ".agent.$key = \"$value\"" "$config" |
|
echo "[ngrok-profile] Set $key for profile '$profile'." |
|
;; |
|
|
|
edit) |
|
profile="${2:-$(get_current)}" |
|
${EDITOR:-nano} "$(get_path "$profile")" |
|
;; |
|
cleanup) |
|
echo "[ngrok-profile] Cleaning up all profiles and restoring original ngrok config..." |
|
|
|
# If the config file is a symlink, remove it to avoid broken links |
|
if [ -L "$CONFIG_FILE" ]; then |
|
rm "$CONFIG_FILE" |
|
echo "[ngrok-profile] Removed symlinked ngrok config." |
|
fi |
|
|
|
# Restore backup if it exists |
|
if [ -f "$BACKUP_FILE" ]; then |
|
cp "$BACKUP_FILE" "$CONFIG_FILE" |
|
if cmp -s "$BACKUP_FILE" "$CONFIG_FILE"; then |
|
echo "[ngrok-profile] Restored original ngrok config from backup." |
|
else |
|
echo "[ngrok-profile] ERROR: Failed to restore config from backup!" >&2 |
|
exit 1 |
|
fi |
|
else |
|
# No backup, create blank |
|
echo -e 'version: "3"\nagent:\n authtoken: ""' > "$CONFIG_FILE" |
|
if [ -s "$CONFIG_FILE" ]; then |
|
echo "[ngrok-profile] No backup found, created blank ngrok config." |
|
else |
|
echo "[ngrok-profile] ERROR: Failed to create blank ngrok config!" >&2 |
|
exit 1 |
|
fi |
|
fi |
|
|
|
# Remove profiles |
|
if [ -d "$PROFILE_DIR" ]; then |
|
rm -rf "$PROFILE_DIR" |
|
echo "[ngrok-profile] Removed all profiles and profile metadata." |
|
fi |
|
|
|
echo "[ngrok-profile] Cleanup complete." |
|
;; |
|
restore) |
|
restore_original_config |
|
;; |
|
run) |
|
[ "$1" == "--" ] && shift |
|
exec $NGROK_RUN "$@" |
|
;; |
|
*) |
|
usage |
|
;; |
|
esac |