crewly 1.4.15 → 1.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +9 -0
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +28 -2
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js +7 -4
- package/dist/backend/backend/src/services/messaging/message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts +28 -15
- package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js +86 -3
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
- package/package.json +1 -1
- package/config/skills/agent/chrome-attach/SKILL.md +0 -84
- package/config/skills/agent/chrome-attach/execute.sh +0 -279
- package/config/skills/agent/vnc-browser/SKILL.md +0 -140
- package/config/skills/agent/vnc-browser/execute.sh +0 -261
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# Chrome Live Attach — One-click attach to user's running Chrome browser
|
|
4
|
-
#
|
|
5
|
-
# Auto-discovers Chrome processes with CDP (Chrome DevTools Protocol) enabled,
|
|
6
|
-
# or offers to enable CDP on the user's existing Chrome session.
|
|
7
|
-
#
|
|
8
|
-
# Modes:
|
|
9
|
-
# discover — Scan for Chrome instances (default)
|
|
10
|
-
# attach — Connect to a specific CDP port
|
|
11
|
-
# launch — Launch Chrome with CDP on user's default profile
|
|
12
|
-
#
|
|
13
|
-
# Usage:
|
|
14
|
-
# execute.sh '{"mode":"discover"}'
|
|
15
|
-
# execute.sh '{"mode":"attach","port":9222}'
|
|
16
|
-
# execute.sh '{"mode":"launch","port":9222}'
|
|
17
|
-
#
|
|
18
|
-
# @see https://github.com/stevehuang0115/crewly/issues/175
|
|
19
|
-
# =============================================================================
|
|
20
|
-
|
|
21
|
-
set -euo pipefail
|
|
22
|
-
|
|
23
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
-
source "$SCRIPT_DIR/../../_common/lib.sh"
|
|
25
|
-
|
|
26
|
-
# Parse input
|
|
27
|
-
INPUT="${1:-{}}"
|
|
28
|
-
MODE=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('mode','discover'))" 2>/dev/null || echo "discover")
|
|
29
|
-
PORT=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('port',9222))" 2>/dev/null || echo "9222")
|
|
30
|
-
|
|
31
|
-
# ── Helper functions ────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
log() { echo "[chrome-attach] $*" >&2; }
|
|
34
|
-
|
|
35
|
-
check_cdp() {
|
|
36
|
-
local port="$1"
|
|
37
|
-
curl -sf --max-time 2 "http://127.0.0.1:${port}/json/version" 2>/dev/null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
get_ws_url() {
|
|
41
|
-
local port="$1"
|
|
42
|
-
curl -sf --max-time 2 "http://127.0.0.1:${port}/json/version" \
|
|
43
|
-
| python3 -c "import sys,json; print(json.load(sys.stdin).get('webSocketDebuggerUrl',''))" 2>/dev/null || true
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
is_chrome_running() {
|
|
47
|
-
pgrep -f "Google Chrome" >/dev/null 2>&1
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
find_chrome_binary() {
|
|
51
|
-
local candidates=(
|
|
52
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
53
|
-
"google-chrome"
|
|
54
|
-
"google-chrome-stable"
|
|
55
|
-
)
|
|
56
|
-
for c in "${candidates[@]}"; do
|
|
57
|
-
if command -v "$c" &>/dev/null || [ -x "$c" ]; then
|
|
58
|
-
echo "$c"
|
|
59
|
-
return 0
|
|
60
|
-
fi
|
|
61
|
-
done
|
|
62
|
-
return 1
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
get_default_profile() {
|
|
66
|
-
case "$(uname)" in
|
|
67
|
-
Darwin) echo "$HOME/Library/Application Support/Google/Chrome" ;;
|
|
68
|
-
Linux) echo "$HOME/.config/google-chrome" ;;
|
|
69
|
-
*) echo "$HOME/.config/google-chrome" ;;
|
|
70
|
-
esac
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
# ── Mode: Discover ─────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
discover_chrome() {
|
|
76
|
-
local found_instances="[]"
|
|
77
|
-
local chrome_running="false"
|
|
78
|
-
|
|
79
|
-
if is_chrome_running; then
|
|
80
|
-
chrome_running="true"
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
# Scan common CDP ports
|
|
84
|
-
for p in 9222 9229 9223 9224; do
|
|
85
|
-
local version_json
|
|
86
|
-
version_json=$(check_cdp "$p" 2>/dev/null) || continue
|
|
87
|
-
|
|
88
|
-
local ws_url browser_version
|
|
89
|
-
ws_url=$(echo "$version_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('webSocketDebuggerUrl',''))" 2>/dev/null || echo "")
|
|
90
|
-
browser_version=$(echo "$version_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Browser','unknown'))" 2>/dev/null || echo "unknown")
|
|
91
|
-
|
|
92
|
-
found_instances=$(echo "$found_instances" | python3 -c "
|
|
93
|
-
import sys, json
|
|
94
|
-
instances = json.load(sys.stdin)
|
|
95
|
-
instances.append({
|
|
96
|
-
'port': $p,
|
|
97
|
-
'wsUrl': '$ws_url',
|
|
98
|
-
'httpEndpoint': 'http://127.0.0.1:$p',
|
|
99
|
-
'version': '$browser_version',
|
|
100
|
-
'isPrimary': $p == 9222
|
|
101
|
-
})
|
|
102
|
-
json.dump(instances, sys.stdout)
|
|
103
|
-
" 2>/dev/null || echo "$found_instances")
|
|
104
|
-
done
|
|
105
|
-
|
|
106
|
-
local count
|
|
107
|
-
count=$(echo "$found_instances" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
|
|
108
|
-
|
|
109
|
-
local suggestion=""
|
|
110
|
-
if [ "$count" = "0" ] && [ "$chrome_running" = "true" ]; then
|
|
111
|
-
suggestion="Chrome is running but without CDP. Restart Chrome with: open -a 'Google Chrome' --args --remote-debugging-port=9222"
|
|
112
|
-
elif [ "$count" = "0" ]; then
|
|
113
|
-
suggestion="No Chrome detected. Use mode=launch to start Chrome with CDP."
|
|
114
|
-
fi
|
|
115
|
-
|
|
116
|
-
cat <<EOF
|
|
117
|
-
{
|
|
118
|
-
"success": true,
|
|
119
|
-
"mode": "discover",
|
|
120
|
-
"found": $([ "$count" != "0" ] && echo "true" || echo "false"),
|
|
121
|
-
"chromeRunning": $chrome_running,
|
|
122
|
-
"instances": $found_instances,
|
|
123
|
-
"suggestion": $([ -n "$suggestion" ] && echo "\"$suggestion\"" || echo "null")
|
|
124
|
-
}
|
|
125
|
-
EOF
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
# ── Mode: Attach ───────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
attach_chrome() {
|
|
131
|
-
local port="$1"
|
|
132
|
-
|
|
133
|
-
local version_json
|
|
134
|
-
version_json=$(check_cdp "$port" 2>/dev/null) || {
|
|
135
|
-
cat <<EOF
|
|
136
|
-
{
|
|
137
|
-
"success": false,
|
|
138
|
-
"mode": "attach",
|
|
139
|
-
"error": "No CDP endpoint found on port $port",
|
|
140
|
-
"suggestion": "Start Chrome with: open -a 'Google Chrome' --args --remote-debugging-port=$port"
|
|
141
|
-
}
|
|
142
|
-
EOF
|
|
143
|
-
return 1
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
local ws_url browser_version
|
|
147
|
-
ws_url=$(echo "$version_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('webSocketDebuggerUrl',''))" 2>/dev/null || echo "")
|
|
148
|
-
browser_version=$(echo "$version_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Browser','unknown'))" 2>/dev/null || echo "unknown")
|
|
149
|
-
|
|
150
|
-
# Get list of open tabs/pages
|
|
151
|
-
local pages_json
|
|
152
|
-
pages_json=$(curl -sf --max-time 2 "http://127.0.0.1:${port}/json" 2>/dev/null || echo "[]")
|
|
153
|
-
local page_count
|
|
154
|
-
page_count=$(echo "$pages_json" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
|
|
155
|
-
|
|
156
|
-
cat <<EOF
|
|
157
|
-
{
|
|
158
|
-
"success": true,
|
|
159
|
-
"mode": "attach",
|
|
160
|
-
"connected": true,
|
|
161
|
-
"port": $port,
|
|
162
|
-
"wsUrl": "$ws_url",
|
|
163
|
-
"httpEndpoint": "http://127.0.0.1:$port",
|
|
164
|
-
"version": "$browser_version",
|
|
165
|
-
"openPages": $page_count,
|
|
166
|
-
"message": "Successfully attached to Chrome ($browser_version) on port $port with $page_count open pages"
|
|
167
|
-
}
|
|
168
|
-
EOF
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
# ── Mode: Launch ───────────────────────────────────────────
|
|
172
|
-
|
|
173
|
-
launch_chrome() {
|
|
174
|
-
local port="$1"
|
|
175
|
-
|
|
176
|
-
# Check if CDP is already available
|
|
177
|
-
if check_cdp "$port" >/dev/null 2>&1; then
|
|
178
|
-
log "CDP already available on port $port, attaching..."
|
|
179
|
-
attach_chrome "$port"
|
|
180
|
-
return
|
|
181
|
-
fi
|
|
182
|
-
|
|
183
|
-
# Find Chrome binary
|
|
184
|
-
local chrome_bin
|
|
185
|
-
chrome_bin=$(find_chrome_binary 2>/dev/null) || {
|
|
186
|
-
cat <<EOF
|
|
187
|
-
{
|
|
188
|
-
"success": false,
|
|
189
|
-
"mode": "launch",
|
|
190
|
-
"error": "Chrome not found. Install Google Chrome."
|
|
191
|
-
}
|
|
192
|
-
EOF
|
|
193
|
-
return 1
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
# Use the user's default Chrome profile for Live Attach (preserves logins)
|
|
197
|
-
local profile_dir
|
|
198
|
-
profile_dir=$(get_default_profile)
|
|
199
|
-
|
|
200
|
-
if ! is_chrome_running; then
|
|
201
|
-
# Chrome not running — launch with CDP on user's profile
|
|
202
|
-
log "Launching Chrome with CDP on port $port (user profile)..."
|
|
203
|
-
"$chrome_bin" \
|
|
204
|
-
--remote-debugging-port="$port" \
|
|
205
|
-
--no-first-run \
|
|
206
|
-
--no-default-browser-check \
|
|
207
|
-
--disable-background-timer-throttling \
|
|
208
|
-
--disable-backgrounding-occluded-windows \
|
|
209
|
-
--disable-renderer-backgrounding \
|
|
210
|
-
&>/dev/null &
|
|
211
|
-
|
|
212
|
-
# Wait for CDP to become available
|
|
213
|
-
for _ in $(seq 1 20); do
|
|
214
|
-
if check_cdp "$port" >/dev/null 2>&1; then
|
|
215
|
-
log "Chrome launched with CDP on port $port"
|
|
216
|
-
attach_chrome "$port"
|
|
217
|
-
return
|
|
218
|
-
fi
|
|
219
|
-
sleep 0.5
|
|
220
|
-
done
|
|
221
|
-
|
|
222
|
-
cat <<EOF
|
|
223
|
-
{
|
|
224
|
-
"success": false,
|
|
225
|
-
"mode": "launch",
|
|
226
|
-
"error": "Chrome launched but CDP did not become available within 10 seconds"
|
|
227
|
-
}
|
|
228
|
-
EOF
|
|
229
|
-
return 1
|
|
230
|
-
fi
|
|
231
|
-
|
|
232
|
-
# Chrome IS running without CDP — use alt profile to avoid conflict
|
|
233
|
-
local alt_profile="${HOME}/.crewly/chrome-attach-profile"
|
|
234
|
-
mkdir -p "$alt_profile"
|
|
235
|
-
|
|
236
|
-
log "Chrome is running without CDP. Launching alt instance on port $port..."
|
|
237
|
-
"$chrome_bin" \
|
|
238
|
-
--remote-debugging-port="$port" \
|
|
239
|
-
--user-data-dir="$alt_profile" \
|
|
240
|
-
--no-first-run \
|
|
241
|
-
--no-default-browser-check \
|
|
242
|
-
&>/dev/null &
|
|
243
|
-
|
|
244
|
-
for _ in $(seq 1 20); do
|
|
245
|
-
if check_cdp "$port" >/dev/null 2>&1; then
|
|
246
|
-
log "Alt Chrome launched with CDP on port $port"
|
|
247
|
-
attach_chrome "$port"
|
|
248
|
-
return
|
|
249
|
-
fi
|
|
250
|
-
sleep 0.5
|
|
251
|
-
done
|
|
252
|
-
|
|
253
|
-
cat <<EOF
|
|
254
|
-
{
|
|
255
|
-
"success": false,
|
|
256
|
-
"mode": "launch",
|
|
257
|
-
"error": "Failed to launch Chrome with CDP. Try closing Chrome and retrying.",
|
|
258
|
-
"suggestion": "Close all Chrome windows, then retry, or run: open -a 'Google Chrome' --args --remote-debugging-port=$port"
|
|
259
|
-
}
|
|
260
|
-
EOF
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
# ── Main ───────────────────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
case "$MODE" in
|
|
266
|
-
discover)
|
|
267
|
-
discover_chrome
|
|
268
|
-
;;
|
|
269
|
-
attach)
|
|
270
|
-
attach_chrome "$PORT"
|
|
271
|
-
;;
|
|
272
|
-
launch)
|
|
273
|
-
launch_chrome "$PORT"
|
|
274
|
-
;;
|
|
275
|
-
*)
|
|
276
|
-
echo '{"success": false, "error": "Unknown mode. Use: discover, attach, or launch"}'
|
|
277
|
-
exit 1
|
|
278
|
-
;;
|
|
279
|
-
esac
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: VNC Remote Browser Access
|
|
3
|
-
description: Launch a virtual display with VNC + noVNC + cloudflared for remote browser viewing and control. Enables human-in-the-loop login/verification when Playwright needs manual interaction.
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
category: browser
|
|
6
|
-
skillType: claude-skill
|
|
7
|
-
assignableRoles:
|
|
8
|
-
- developer
|
|
9
|
-
- qa
|
|
10
|
-
- qa-engineer
|
|
11
|
-
- fullstack-dev
|
|
12
|
-
- backend-developer
|
|
13
|
-
- frontend-developer
|
|
14
|
-
- generalist
|
|
15
|
-
triggers:
|
|
16
|
-
- vnc
|
|
17
|
-
- remote browser
|
|
18
|
-
- browser login
|
|
19
|
-
- manual login
|
|
20
|
-
- human verification
|
|
21
|
-
- captcha
|
|
22
|
-
- 2fa
|
|
23
|
-
- playwright headful
|
|
24
|
-
tags:
|
|
25
|
-
- vnc
|
|
26
|
-
- browser
|
|
27
|
-
- remote-access
|
|
28
|
-
- playwright
|
|
29
|
-
- noVNC
|
|
30
|
-
- cloudflared
|
|
31
|
-
execution:
|
|
32
|
-
type: script
|
|
33
|
-
script:
|
|
34
|
-
file: execute.sh
|
|
35
|
-
interpreter: bash
|
|
36
|
-
timeoutMs: 60000
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
# VNC Remote Browser Access
|
|
40
|
-
|
|
41
|
-
Bridge macOS Screen Sharing to a public HTTPS URL via noVNC + cloudflared. This lets humans remotely view and control the Mac desktop (and any browser on it) from anywhere — essential when Playwright hits a login page, CAPTCHA, or 2FA prompt.
|
|
42
|
-
|
|
43
|
-
The stack: **macOS Screen Sharing** (built-in VNC, port 5900) → **websockify + noVNC** (web client, port 6080) → **cloudflared** (public HTTPS tunnel).
|
|
44
|
-
|
|
45
|
-
## Prerequisites
|
|
46
|
-
|
|
47
|
-
**macOS Screen Sharing must be enabled manually** before using this skill:
|
|
48
|
-
|
|
49
|
-
1. Open **System Settings** → **General** → **Sharing**
|
|
50
|
-
2. Turn on **Screen Sharing**
|
|
51
|
-
3. (Optional) Set a VNC password under Screen Sharing options
|
|
52
|
-
|
|
53
|
-
This only needs to be done once. Screen Sharing runs as a system service and persists across reboots.
|
|
54
|
-
|
|
55
|
-
## When to Use
|
|
56
|
-
|
|
57
|
-
- Playwright hits a login page that requires human credentials
|
|
58
|
-
- A website shows a CAPTCHA or anti-bot challenge
|
|
59
|
-
- Two-factor authentication (2FA/MFA) requires a code from the user
|
|
60
|
-
- You need the user to visually verify something in the browser
|
|
61
|
-
- Any browser automation step that needs human-in-the-loop interaction
|
|
62
|
-
|
|
63
|
-
## Parameters
|
|
64
|
-
|
|
65
|
-
| Parameter | Required | Description |
|
|
66
|
-
|-----------|----------|-------------|
|
|
67
|
-
| `action` | Yes | One of: `start`, `stop`, `status`, `get-url` |
|
|
68
|
-
|
|
69
|
-
### Actions
|
|
70
|
-
|
|
71
|
-
- **start** — Check Screen Sharing is on, install deps if needed, launch websockify + noVNC + cloudflared. Returns the public URL.
|
|
72
|
-
- **stop** — Shut down websockify + cloudflared. Does NOT touch Screen Sharing.
|
|
73
|
-
- **status** — Check Screen Sharing (port 5900), websockify, and cloudflared status.
|
|
74
|
-
- **get-url** — Retrieve the cloudflared public URL.
|
|
75
|
-
|
|
76
|
-
## Examples
|
|
77
|
-
|
|
78
|
-
### Start and get the public URL
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
bash config/skills/agent/vnc-browser/execute.sh '{"action":"start"}'
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Output:
|
|
85
|
-
```json
|
|
86
|
-
{
|
|
87
|
-
"success": true,
|
|
88
|
-
"status": "started",
|
|
89
|
-
"publicUrl": "https://abc-xyz.trycloudflare.com/vnc.html?autoconnect=true",
|
|
90
|
-
"localUrl": "http://localhost:6080/vnc.html?autoconnect=true",
|
|
91
|
-
"hint": "Share the publicUrl with the user..."
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Send the URL to the user via Slack
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
URL=$(bash config/skills/agent/vnc-browser/execute.sh '{"action":"get-url"}' | jq -r '.publicUrl')
|
|
99
|
-
# Then send via reply-slack or report-status
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Check status
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
bash config/skills/agent/vnc-browser/execute.sh '{"action":"status"}'
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Stop when done
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
bash config/skills/agent/vnc-browser/execute.sh '{"action":"stop"}'
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Workflow
|
|
115
|
-
|
|
116
|
-
1. **Ensure Screen Sharing is on** (one-time setup)
|
|
117
|
-
2. **Start VNC bridge** → get the public URL
|
|
118
|
-
3. **Launch Playwright** in headful mode (the browser appears on the Mac desktop)
|
|
119
|
-
4. **Navigate** to the page requiring human interaction
|
|
120
|
-
5. **Send the public URL** to the user via Slack
|
|
121
|
-
6. **Wait** for the user to complete the manual step (login, CAPTCHA, etc.)
|
|
122
|
-
7. **Continue automation** once the browser is past the manual step
|
|
123
|
-
8. **Stop VNC bridge** when no longer needed
|
|
124
|
-
|
|
125
|
-
## Technical Details
|
|
126
|
-
|
|
127
|
-
- **VNC source**: macOS Screen Sharing on port 5900 (system-managed)
|
|
128
|
-
- **Web client**: noVNC on port 6080 via websockify
|
|
129
|
-
- **Public tunnel**: cloudflared Quick Tunnel (random `*.trycloudflare.com` subdomain)
|
|
130
|
-
- **Security**: Screen Sharing requires macOS user credentials; cloudflared provides HTTPS
|
|
131
|
-
- **Dependencies**: websockify (pip), noVNC (GitHub release), cloudflared (Homebrew) — auto-installed on first run
|
|
132
|
-
- **PID files**: `~/.crewly/vnc/` for websockify and cloudflared lifecycle
|
|
133
|
-
- **Note**: This skill does NOT start or stop macOS Screen Sharing — that is user-managed
|
|
134
|
-
|
|
135
|
-
## Requirements
|
|
136
|
-
|
|
137
|
-
- macOS with Screen Sharing enabled
|
|
138
|
-
- Homebrew installed
|
|
139
|
-
- Python 3 with pip (for websockify)
|
|
140
|
-
- Internet access (for cloudflared tunnel and dependency installation)
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# VNC Remote Browser Access — bridge macOS Screen Sharing to a public URL via noVNC
|
|
3
|
-
# Allows remote viewing/control of the Mac desktop (and any browser on it) via a web URL.
|
|
4
|
-
#
|
|
5
|
-
# Architecture:
|
|
6
|
-
# macOS Screen Sharing (built-in VNC server on port 5900)
|
|
7
|
-
# → websockify (WebSocket bridge + noVNC web server, port 6080)
|
|
8
|
-
# → cloudflared (Quick Tunnel → public HTTPS URL)
|
|
9
|
-
#
|
|
10
|
-
# Prerequisites: macOS Screen Sharing must be enabled manually in System Settings.
|
|
11
|
-
#
|
|
12
|
-
# Usage: execute.sh '{"action":"start|stop|status|get-url"}'
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
-
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
16
|
-
|
|
17
|
-
# ---------------------------------------------------------------------------
|
|
18
|
-
# Configuration
|
|
19
|
-
# ---------------------------------------------------------------------------
|
|
20
|
-
VNC_PORT=5900
|
|
21
|
-
NOVNC_PORT=6080
|
|
22
|
-
PID_DIR="${HOME}/.crewly/vnc"
|
|
23
|
-
NOVNC_DIR="${HOME}/.crewly/novnc"
|
|
24
|
-
CLOUDFLARED_LOG="${PID_DIR}/cloudflared.log"
|
|
25
|
-
|
|
26
|
-
INPUT="${1:-}"
|
|
27
|
-
[ -z "$INPUT" ] && error_exit "Usage: execute.sh '{\"action\":\"start|stop|status|get-url\"}'"
|
|
28
|
-
|
|
29
|
-
ACTION=$(echo "$INPUT" | jq -r '.action // empty')
|
|
30
|
-
require_param "action" "$ACTION"
|
|
31
|
-
|
|
32
|
-
mkdir -p "$PID_DIR"
|
|
33
|
-
|
|
34
|
-
# ---------------------------------------------------------------------------
|
|
35
|
-
# Dependency installation (macOS / Homebrew)
|
|
36
|
-
# ---------------------------------------------------------------------------
|
|
37
|
-
install_deps() {
|
|
38
|
-
local missing=()
|
|
39
|
-
|
|
40
|
-
# websockify — WebSocket-to-TCP bridge (Python)
|
|
41
|
-
if ! command -v websockify &>/dev/null; then
|
|
42
|
-
missing+=("websockify")
|
|
43
|
-
echo '{"status":"installing","dep":"websockify"}' >&2
|
|
44
|
-
pip3 install websockify 2>/dev/null \
|
|
45
|
-
|| error_exit "Failed to install websockify. Run: pip3 install websockify"
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
# noVNC — HTML5 VNC client
|
|
49
|
-
if ! [ -d "$NOVNC_DIR" ] || ! [ -f "$NOVNC_DIR/vnc.html" ]; then
|
|
50
|
-
missing+=("novnc")
|
|
51
|
-
echo '{"status":"installing","dep":"novnc"}' >&2
|
|
52
|
-
local novnc_version="1.5.0"
|
|
53
|
-
local novnc_url="https://github.com/novnc/noVNC/archive/refs/tags/v${novnc_version}.tar.gz"
|
|
54
|
-
mkdir -p "$NOVNC_DIR"
|
|
55
|
-
curl -sL "$novnc_url" | tar -xz -C "$NOVNC_DIR" --strip-components=1
|
|
56
|
-
if ! [ -f "$NOVNC_DIR/vnc.html" ]; then
|
|
57
|
-
rm -rf "$NOVNC_DIR"
|
|
58
|
-
error_exit "Failed to download noVNC web client"
|
|
59
|
-
fi
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
# cloudflared — Cloudflare Tunnel for public HTTPS URL
|
|
63
|
-
if ! command -v cloudflared &>/dev/null; then
|
|
64
|
-
missing+=("cloudflared")
|
|
65
|
-
echo '{"status":"installing","dep":"cloudflared"}' >&2
|
|
66
|
-
brew install cloudflare/cloudflare/cloudflared 2>/dev/null \
|
|
67
|
-
|| error_exit "Failed to install cloudflared. Run: brew install cloudflare/cloudflare/cloudflared"
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
if [ ${#missing[@]} -gt 0 ]; then
|
|
71
|
-
echo '{"status":"deps_installed","installed":"'"$(IFS=,; echo "${missing[*]}")"'"}' >&2
|
|
72
|
-
fi
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
# ---------------------------------------------------------------------------
|
|
76
|
-
# Helper: check if a service is running via its PID file
|
|
77
|
-
# ---------------------------------------------------------------------------
|
|
78
|
-
is_running() {
|
|
79
|
-
local pid_file="$PID_DIR/${1}.pid"
|
|
80
|
-
[ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
# ---------------------------------------------------------------------------
|
|
84
|
-
# Helper: check if macOS Screen Sharing VNC is listening on port 5900
|
|
85
|
-
# ---------------------------------------------------------------------------
|
|
86
|
-
check_screen_sharing() {
|
|
87
|
-
nc -z localhost "$VNC_PORT" 2>/dev/null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
# ---------------------------------------------------------------------------
|
|
91
|
-
# start — Launch websockify + cloudflared (requires Screen Sharing on)
|
|
92
|
-
# ---------------------------------------------------------------------------
|
|
93
|
-
start_vnc() {
|
|
94
|
-
# Already running?
|
|
95
|
-
if is_running websockify && is_running cloudflared; then
|
|
96
|
-
local url=""
|
|
97
|
-
[ -f "$PID_DIR/tunnel_url.txt" ] && url=$(cat "$PID_DIR/tunnel_url.txt")
|
|
98
|
-
jq -n --arg url "$url" \
|
|
99
|
-
'{success: true, status: "already_running", publicUrl: (if $url != "" then ($url + "/vnc.html?autoconnect=true") else null end), localUrl: ("http://localhost:'"$NOVNC_PORT"'/vnc.html?autoconnect=true")}'
|
|
100
|
-
return 0
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
# Check macOS Screen Sharing is enabled
|
|
104
|
-
if ! check_screen_sharing; then
|
|
105
|
-
error_exit "macOS Screen Sharing is not running (port $VNC_PORT not listening). Enable it: System Settings → General → Sharing → Screen Sharing → ON"
|
|
106
|
-
fi
|
|
107
|
-
|
|
108
|
-
# Install missing dependencies
|
|
109
|
-
install_deps
|
|
110
|
-
|
|
111
|
-
# 1. Start websockify (WebSocket bridge + noVNC web server)
|
|
112
|
-
# Bridges noVNC web client (port 6080) to macOS VNC (port 5900)
|
|
113
|
-
websockify --web "$NOVNC_DIR" "$NOVNC_PORT" "localhost:$VNC_PORT" \
|
|
114
|
-
> "$PID_DIR/websockify.log" 2>&1 &
|
|
115
|
-
echo $! > "$PID_DIR/websockify.pid"
|
|
116
|
-
sleep 1
|
|
117
|
-
|
|
118
|
-
if ! is_running websockify; then
|
|
119
|
-
error_exit "websockify failed to start. Check $PID_DIR/websockify.log"
|
|
120
|
-
fi
|
|
121
|
-
|
|
122
|
-
# 2. Start cloudflared Quick Tunnel (zero-config public URL)
|
|
123
|
-
cloudflared tunnel --url "http://localhost:$NOVNC_PORT" \
|
|
124
|
-
> "$CLOUDFLARED_LOG" 2>&1 &
|
|
125
|
-
echo $! > "$PID_DIR/cloudflared.pid"
|
|
126
|
-
|
|
127
|
-
# Wait for cloudflared to print the tunnel URL (up to 30s)
|
|
128
|
-
local url=""
|
|
129
|
-
for _ in $(seq 1 30); do
|
|
130
|
-
url=$(grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' "$CLOUDFLARED_LOG" 2>/dev/null | head -1 || true)
|
|
131
|
-
[ -n "$url" ] && break
|
|
132
|
-
sleep 1
|
|
133
|
-
done
|
|
134
|
-
|
|
135
|
-
if [ -n "$url" ]; then
|
|
136
|
-
echo "$url" > "$PID_DIR/tunnel_url.txt"
|
|
137
|
-
jq -n --arg url "$url" \
|
|
138
|
-
'{success: true, status: "started", publicUrl: ($url + "/vnc.html?autoconnect=true"), localUrl: ("http://localhost:'"$NOVNC_PORT"'/vnc.html?autoconnect=true"), hint: "Share the publicUrl with the user. They will see the Mac desktop and can interact with the browser."}'
|
|
139
|
-
else
|
|
140
|
-
jq -n \
|
|
141
|
-
'{success: true, status: "started_no_tunnel", publicUrl: null, localUrl: ("http://localhost:'"$NOVNC_PORT"'/vnc.html?autoconnect=true"), hint: "Cloudflared URL not ready yet. Use get-url action to retrieve it later."}'
|
|
142
|
-
fi
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
# ---------------------------------------------------------------------------
|
|
146
|
-
# stop — Tear down websockify + cloudflared (does NOT touch Screen Sharing)
|
|
147
|
-
# ---------------------------------------------------------------------------
|
|
148
|
-
stop_vnc() {
|
|
149
|
-
local stopped=0
|
|
150
|
-
|
|
151
|
-
for service in cloudflared websockify; do
|
|
152
|
-
local pid_file="$PID_DIR/${service}.pid"
|
|
153
|
-
if [ -f "$pid_file" ]; then
|
|
154
|
-
local pid
|
|
155
|
-
pid=$(cat "$pid_file")
|
|
156
|
-
if kill -0 "$pid" 2>/dev/null; then
|
|
157
|
-
kill "$pid" 2>/dev/null || true
|
|
158
|
-
stopped=$((stopped + 1))
|
|
159
|
-
fi
|
|
160
|
-
rm -f "$pid_file"
|
|
161
|
-
fi
|
|
162
|
-
done
|
|
163
|
-
|
|
164
|
-
rm -f "$PID_DIR/tunnel_url.txt"
|
|
165
|
-
|
|
166
|
-
jq -n --argjson stopped "$stopped" \
|
|
167
|
-
'{success: true, status: "stopped", stoppedServices: $stopped, note: "macOS Screen Sharing was not touched"}'
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
# ---------------------------------------------------------------------------
|
|
171
|
-
# status — Check which services are running
|
|
172
|
-
# ---------------------------------------------------------------------------
|
|
173
|
-
status_vnc() {
|
|
174
|
-
local services='[]'
|
|
175
|
-
local all_running=true
|
|
176
|
-
|
|
177
|
-
# Check macOS Screen Sharing (port 5900)
|
|
178
|
-
local screen_sharing=false
|
|
179
|
-
if check_screen_sharing; then
|
|
180
|
-
screen_sharing=true
|
|
181
|
-
else
|
|
182
|
-
all_running=false
|
|
183
|
-
fi
|
|
184
|
-
services=$(echo "$services" | jq \
|
|
185
|
-
--arg name "screen-sharing" \
|
|
186
|
-
--argjson running "$screen_sharing" \
|
|
187
|
-
--arg pid "system" \
|
|
188
|
-
'. + [{name: $name, running: $running, pid: $pid}]')
|
|
189
|
-
|
|
190
|
-
# Check managed services
|
|
191
|
-
for service in websockify cloudflared; do
|
|
192
|
-
local pid_file="$PID_DIR/${service}.pid"
|
|
193
|
-
local running=false
|
|
194
|
-
local pid=""
|
|
195
|
-
|
|
196
|
-
if [ -f "$pid_file" ]; then
|
|
197
|
-
pid=$(cat "$pid_file")
|
|
198
|
-
if kill -0 "$pid" 2>/dev/null; then
|
|
199
|
-
running=true
|
|
200
|
-
fi
|
|
201
|
-
fi
|
|
202
|
-
|
|
203
|
-
[ "$running" = false ] && all_running=false
|
|
204
|
-
|
|
205
|
-
services=$(echo "$services" | jq \
|
|
206
|
-
--arg name "$service" \
|
|
207
|
-
--argjson running "$running" \
|
|
208
|
-
--arg pid "$pid" \
|
|
209
|
-
'. + [{name: $name, running: $running, pid: $pid}]')
|
|
210
|
-
done
|
|
211
|
-
|
|
212
|
-
local url=""
|
|
213
|
-
[ -f "$PID_DIR/tunnel_url.txt" ] && url=$(cat "$PID_DIR/tunnel_url.txt")
|
|
214
|
-
|
|
215
|
-
jq -n \
|
|
216
|
-
--argjson allRunning "$all_running" \
|
|
217
|
-
--argjson services "$services" \
|
|
218
|
-
--arg publicUrl "${url:-}" \
|
|
219
|
-
'{success: true, allRunning: $allRunning, publicUrl: (if $publicUrl != "" then ($publicUrl + "/vnc.html?autoconnect=true") else null end), services: $services}'
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
# ---------------------------------------------------------------------------
|
|
223
|
-
# get-url — Retrieve the public tunnel URL
|
|
224
|
-
# ---------------------------------------------------------------------------
|
|
225
|
-
get_url() {
|
|
226
|
-
# Try saved URL
|
|
227
|
-
if [ -f "$PID_DIR/tunnel_url.txt" ]; then
|
|
228
|
-
local url
|
|
229
|
-
url=$(cat "$PID_DIR/tunnel_url.txt")
|
|
230
|
-
if [ -n "$url" ]; then
|
|
231
|
-
jq -n --arg url "$url" \
|
|
232
|
-
'{success: true, publicUrl: ($url + "/vnc.html?autoconnect=true"), localUrl: ("http://localhost:'"$NOVNC_PORT"'/vnc.html?autoconnect=true")}'
|
|
233
|
-
return 0
|
|
234
|
-
fi
|
|
235
|
-
fi
|
|
236
|
-
|
|
237
|
-
# Try parsing from cloudflared log
|
|
238
|
-
if [ -f "$CLOUDFLARED_LOG" ]; then
|
|
239
|
-
local url
|
|
240
|
-
url=$(grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' "$CLOUDFLARED_LOG" 2>/dev/null | head -1 || true)
|
|
241
|
-
if [ -n "$url" ]; then
|
|
242
|
-
echo "$url" > "$PID_DIR/tunnel_url.txt"
|
|
243
|
-
jq -n --arg url "$url" \
|
|
244
|
-
'{success: true, publicUrl: ($url + "/vnc.html?autoconnect=true"), localUrl: ("http://localhost:'"$NOVNC_PORT"'/vnc.html?autoconnect=true")}'
|
|
245
|
-
return 0
|
|
246
|
-
fi
|
|
247
|
-
fi
|
|
248
|
-
|
|
249
|
-
error_exit "No tunnel URL available. Is VNC started? Try: execute.sh '{\"action\":\"start\"}'"
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
# ---------------------------------------------------------------------------
|
|
253
|
-
# Dispatch
|
|
254
|
-
# ---------------------------------------------------------------------------
|
|
255
|
-
case "$ACTION" in
|
|
256
|
-
start) start_vnc ;;
|
|
257
|
-
stop) stop_vnc ;;
|
|
258
|
-
status) status_vnc ;;
|
|
259
|
-
get-url) get_url ;;
|
|
260
|
-
*) error_exit "Unknown action: $ACTION. Valid actions: start, stop, status, get-url" ;;
|
|
261
|
-
esac
|