Skip to content

Commit 204298e

Browse files
fix: restore environment settings for container compatibility
1 parent 89f331b commit 204298e

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed

claude-code/commands/expose.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# /expose - Expose Local Services via Tailscale
2+
3+
Expose a local service on Tailscale with a unique path, preserving the root path for the main application.
4+
5+
## Usage
6+
```
7+
/expose <port> [service-name]
8+
```
9+
10+
## Examples
11+
```
12+
/expose # Auto-detects running dev server and exposes it
13+
/expose 3000 # Exposes localhost:3000 with auto-generated path
14+
/expose 8080 api # Exposes localhost:8080 as /api
15+
/expose 5173 vite-app # Exposes localhost:5173 as /vite-app
16+
```
17+
18+
## Implementation
19+
20+
When called, this command will:
21+
22+
1. **Auto-Detect Dev Server (if no port specified)**
23+
- Search for common dev server ports: 3000, 3001, 4200, 5173, 5174, 8000, 8080, 8081
24+
- Check for running processes with common dev commands: `npm run dev`, `yarn dev`, `vite`, `next dev`
25+
- Use `lsof` to find the actual port being used
26+
- If multiple found, prompt user to choose
27+
28+
2. **Check Tailscale Status**
29+
- Verify Tailscale is running
30+
- Get current Tailscale IP and hostname
31+
32+
3. **Generate Unique Path**
33+
- If service-name provided: use `/service-name`
34+
- Otherwise: generate random path like `/svc-<random-8-chars>`
35+
- Ensure path doesn't conflict with existing services
36+
37+
3. **Preserve Root Path**
38+
- Never override the root path `/`
39+
- Keep track of which service owns root (if any)
40+
41+
4. **Expose Service**
42+
```bash
43+
tailscale serve --set-path /<unique-path> --bg http://localhost:<port>
44+
```
45+
46+
5. **Store Service Mapping**
47+
- Save to `.claude/tailscale-services.json`:
48+
```json
49+
{
50+
"root_service": {
51+
"port": 6802,
52+
"name": "lipi",
53+
"protected": true
54+
},
55+
"services": [
56+
{
57+
"port": 3000,
58+
"path": "/svc-a8f3d2c1",
59+
"name": "frontend",
60+
"url": "https://mb1412-1.tailae910a.ts.net/svc-a8f3d2c1",
61+
"direct": "http://100.98.203.32:3000",
62+
"created": "2024-01-15T10:30:00Z"
63+
}
64+
]
65+
}
66+
```
67+
68+
6. **Display Access Info**
69+
```
70+
✅ Service exposed successfully!
71+
72+
📍 Service: frontend (port 3000)
73+
🔗 HTTPS Path: https://mb1412-1.tailae910a.ts.net/svc-a8f3d2c1
74+
🔗 Direct Access: http://100.98.203.32:3000
75+
🔗 Hostname Access: http://mb1412-1:3000
76+
77+
💡 For SPAs, use direct access URLs to avoid routing issues
78+
```
79+
80+
## Command Logic
81+
82+
```bash
83+
#!/bin/bash
84+
85+
# Auto-detect running dev server
86+
auto_detect_dev_server() {
87+
# Common dev server ports
88+
COMMON_PORTS=(3000 3001 3002 4200 4321 5173 5174 8000 8080 8081 6802 4013)
89+
90+
echo "🔍 Searching for running dev servers..."
91+
92+
FOUND_SERVICES=()
93+
for port in "${COMMON_PORTS[@]}"; do
94+
if lsof -i :$port >/dev/null 2>&1; then
95+
# Get process info
96+
PROCESS=$(lsof -i :$port | grep LISTEN | awk '{print $1}' | head -1)
97+
FOUND_SERVICES+=("$port:$PROCESS")
98+
echo " Found: $PROCESS on port $port"
99+
fi
100+
done
101+
102+
# Also check for common dev processes
103+
DEV_PROCESSES=("next" "vite" "webpack" "parcel" "snowpack" "turbopack")
104+
for proc in "${DEV_PROCESSES[@]}"; do
105+
PORTS=$(lsof -i -P | grep -i "$proc" | grep LISTEN | awk '{print $9}' | cut -d: -f2 | sort -u)
106+
for port in $PORTS; do
107+
if [[ ! " ${FOUND_SERVICES[@]} " =~ " $port:" ]]; then
108+
FOUND_SERVICES+=("$port:$proc")
109+
echo " Found: $proc on port $port"
110+
fi
111+
done
112+
done
113+
114+
if [ ${#FOUND_SERVICES[@]} -eq 0 ]; then
115+
echo "❌ No running dev servers found"
116+
echo " Start your dev server first, then run /expose"
117+
return 1
118+
elif [ ${#FOUND_SERVICES[@]} -eq 1 ]; then
119+
# Only one found, use it
120+
PORT=$(echo "${FOUND_SERVICES[0]}" | cut -d: -f1)
121+
PROCESS=$(echo "${FOUND_SERVICES[0]}" | cut -d: -f2)
122+
echo "✅ Auto-detected: $PROCESS on port $PORT"
123+
return $PORT
124+
else
125+
# Multiple found, let user choose
126+
echo ""
127+
echo "Multiple dev servers found. Which one to expose?"
128+
for i in "${!FOUND_SERVICES[@]}"; do
129+
PORT=$(echo "${FOUND_SERVICES[$i]}" | cut -d: -f1)
130+
PROCESS=$(echo "${FOUND_SERVICES[$i]}" | cut -d: -f2)
131+
echo " $((i+1)). $PROCESS on port $PORT"
132+
done
133+
read -p "Enter number (1-${#FOUND_SERVICES[@]}): " choice
134+
135+
if [[ $choice -ge 1 && $choice -le ${#FOUND_SERVICES[@]} ]]; then
136+
PORT=$(echo "${FOUND_SERVICES[$((choice-1))]}" | cut -d: -f1)
137+
return $PORT
138+
else
139+
echo "❌ Invalid choice"
140+
return 1
141+
fi
142+
fi
143+
}
144+
145+
expose_service() {
146+
local PORT=$1
147+
local SERVICE_NAME=$2
148+
149+
# If no port specified, auto-detect
150+
if [ -z "$PORT" ]; then
151+
auto_detect_dev_server
152+
PORT=$?
153+
if [ $PORT -eq 1 ]; then
154+
return 1
155+
fi
156+
# Auto-generate service name based on detected process
157+
if [ -z "$SERVICE_NAME" ]; then
158+
SERVICE_NAME="dev-$PORT"
159+
fi
160+
fi
161+
162+
# Get Tailscale info
163+
TAILSCALE_IP=$(tailscale ip -4)
164+
TAILSCALE_HOSTNAME=$(tailscale status --self --peers=false | awk '{print $2}')
165+
TAILSCALE_DOMAIN="${TAILSCALE_HOSTNAME}.tailae910a.ts.net"
166+
167+
# Generate path
168+
if [ -z "$SERVICE_NAME" ]; then
169+
RANDOM_ID=$(openssl rand -hex 4)
170+
PATH_NAME="/svc-${RANDOM_ID}"
171+
SERVICE_NAME="service-${PORT}"
172+
else
173+
PATH_NAME="/${SERVICE_NAME}"
174+
fi
175+
176+
# Check if port is already exposed
177+
EXISTING=$(tailscale serve status --json | jq -r ".Web[\"${TAILSCALE_DOMAIN}:443\"].Handlers[\"${PATH_NAME}\"].Proxy")
178+
179+
if [ "$EXISTING" != "null" ]; then
180+
echo "⚠️ Path ${PATH_NAME} is already in use"
181+
echo " Current proxy: ${EXISTING}"
182+
echo " Generate new path? (y/n)"
183+
read -r response
184+
if [ "$response" = "y" ]; then
185+
RANDOM_ID=$(openssl rand -hex 4)
186+
PATH_NAME="/svc-${RANDOM_ID}"
187+
else
188+
return 1
189+
fi
190+
fi
191+
192+
# Expose the service
193+
tailscale serve --set-path ${PATH_NAME} --bg http://localhost:${PORT}
194+
195+
# Save to tracking file
196+
SERVICES_FILE="$HOME/.claude/tailscale-services.json"
197+
198+
# Create file if doesn't exist
199+
if [ ! -f "$SERVICES_FILE" ]; then
200+
echo '{"root_service": null, "services": []}' > "$SERVICES_FILE"
201+
fi
202+
203+
# Add service to tracking
204+
NEW_SERVICE=$(jq -n \
205+
--arg port "$PORT" \
206+
--arg path "$PATH_NAME" \
207+
--arg name "$SERVICE_NAME" \
208+
--arg url "https://${TAILSCALE_DOMAIN}${PATH_NAME}" \
209+
--arg direct "http://${TAILSCALE_IP}:${PORT}" \
210+
--arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
211+
'{port: $port, path: $path, name: $name, url: $url, direct: $direct, created: $created}')
212+
213+
jq ".services += [$NEW_SERVICE]" "$SERVICES_FILE" > "${SERVICES_FILE}.tmp" && mv "${SERVICES_FILE}.tmp" "$SERVICES_FILE"
214+
215+
# Display results
216+
echo "✅ Service exposed successfully!"
217+
echo ""
218+
echo "📍 Service: ${SERVICE_NAME} (port ${PORT})"
219+
echo "🔗 HTTPS Path: https://${TAILSCALE_DOMAIN}${PATH_NAME}"
220+
echo "🔗 Direct Access: http://${TAILSCALE_IP}:${PORT}"
221+
echo "🔗 Hostname Access: http://${TAILSCALE_HOSTNAME}:${PORT}"
222+
echo ""
223+
echo "💡 For SPAs, use direct access URLs to avoid routing issues"
224+
}
225+
226+
# List exposed services
227+
list_services() {
228+
echo "🌐 Exposed Services on Tailscale"
229+
echo "================================"
230+
231+
# Show Tailscale serve status
232+
tailscale serve status
233+
234+
# Show tracked services
235+
if [ -f "$HOME/.claude/tailscale-services.json" ]; then
236+
echo ""
237+
echo "📋 Service Registry:"
238+
jq -r '.services[] | " \(.name): \(.direct) (path: \(.path))"' "$HOME/.claude/tailscale-services.json"
239+
fi
240+
}
241+
242+
# Remove service
243+
unexpose_service() {
244+
local PATH_OR_PORT=$1
245+
246+
if [[ "$PATH_OR_PORT" =~ ^[0-9]+$ ]]; then
247+
# It's a port, find the path
248+
PATH_NAME=$(jq -r ".services[] | select(.port == \"$PATH_OR_PORT\") | .path" "$HOME/.claude/tailscale-services.json")
249+
else
250+
PATH_NAME="$PATH_OR_PORT"
251+
fi
252+
253+
if [ -z "$PATH_NAME" ]; then
254+
echo "❌ Service not found"
255+
return 1
256+
fi
257+
258+
# Remove from Tailscale
259+
tailscale serve clear ${PATH_NAME}
260+
261+
# Remove from tracking
262+
jq "del(.services[] | select(.path == \"$PATH_NAME\"))" "$HOME/.claude/tailscale-services.json" > "${SERVICES_FILE}.tmp" && mv "${SERVICES_FILE}.tmp" "$SERVICES_FILE"
263+
264+
echo "✅ Service removed from ${PATH_NAME}"
265+
}
266+
```
267+
268+
## Features
269+
270+
- **Auto-generates unique paths** to avoid conflicts
271+
- **Preserves root path** for main application
272+
- **Tracks all exposed services** in JSON file
273+
- **Provides both HTTPS and direct access URLs**
274+
- **Handles SPAs correctly** by recommending direct port access
275+
- **Supports custom service names** or auto-generation
276+
- **Lists all exposed services** with `/expose list`
277+
- **Removes services** with `/expose remove <port|path>`
278+
279+
## Related Commands
280+
- `/expose list` - Show all exposed services
281+
- `/expose remove <port>` - Unexpose a service
282+
- `/expose clear` - Remove all exposed services (except root)

0 commit comments

Comments
 (0)