clawdex-mobile 5.0.6 → 5.0.8-internal.0
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/README.md +1 -1
- package/docs/setup-and-operations.md +1 -1
- package/package.json +4 -1
- package/scripts/setup-wizard.sh +51 -15
- package/scripts/start-bridge-secure.js +140 -49
- package/scripts/stop-services.sh +22 -0
- package/services/rust-bridge/Cargo.lock +1 -1
- package/services/rust-bridge/Cargo.toml +1 -1
- package/services/rust-bridge/src/main.rs +76 -1
- package/vendor/bridge-binaries/darwin-arm64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/darwin-x64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-arm64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-armv7l/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-x64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/win32-x64/codex-rust-bridge.exe +0 -0
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ npm install -g clawdex-mobile@latest
|
|
|
38
38
|
clawdex init
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Then open the mobile app and connect using the printed bridge URL/token.
|
|
41
|
+
Then open the mobile app and connect using the printed bridge URL/token or pairing QR.
|
|
42
42
|
`clawdex init` now writes config, starts the bridge in the background, and returns you to the shell. Bridge logs go to `.bridge.log`.
|
|
43
43
|
|
|
44
44
|
The npm package is bridge-only. It does not install Expo or the mobile source tree. On supported macOS, Linux, and Windows hosts it uses bundled bridge binaries, so normal startup does not compile Rust.
|
|
@@ -28,7 +28,7 @@ After `clawdex init`, expected sequence:
|
|
|
28
28
|
|
|
29
29
|
1. Secure config is written or reused
|
|
30
30
|
2. The bridge starts in the background
|
|
31
|
-
3. The wizard prints the bridge URL
|
|
31
|
+
3. The wizard prints the bridge URL, token, and pairing QR for mobile onboarding
|
|
32
32
|
4. Bridge logs are written to `.bridge.log`
|
|
33
33
|
|
|
34
34
|
Published npm releases bundle prebuilt bridge binaries for `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `linux-armv7l`, and `win32-x64`. On those hosts, normal bridge startup does not require a Rust compile.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawdex-mobile",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.8-internal.0",
|
|
4
4
|
"description": "Private-network mobile bridge and CLI for Codex and OpenCode",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"qrcode-terminal": "^0.12.0"
|
|
24
|
+
},
|
|
22
25
|
"bin": {
|
|
23
26
|
"clawdex": "./bin/clawdex.js"
|
|
24
27
|
},
|
package/scripts/setup-wizard.sh
CHANGED
|
@@ -194,7 +194,7 @@ sync_active_engine_from_selection() {
|
|
|
194
194
|
return
|
|
195
195
|
fi
|
|
196
196
|
|
|
197
|
-
if
|
|
197
|
+
if selected_engines_contains "$ACTIVE_ENGINE"; then
|
|
198
198
|
return
|
|
199
199
|
fi
|
|
200
200
|
|
|
@@ -215,6 +215,30 @@ format_engine_list() {
|
|
|
215
215
|
printf '%s' "$result"
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
selected_engines_contains() {
|
|
219
|
+
local needle="$1"
|
|
220
|
+
|
|
221
|
+
if (( ${#SELECTED_ENGINES[@]} == 0 )); then
|
|
222
|
+
return 1
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
engine_list_contains "$needle" "${SELECTED_ENGINES[@]}"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
format_selected_engines() {
|
|
229
|
+
if (( ${#SELECTED_ENGINES[@]} == 0 )); then
|
|
230
|
+
printf ''
|
|
231
|
+
return 0
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
format_engine_list "${SELECTED_ENGINES[@]}"
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
selected_engines_csv() {
|
|
238
|
+
local IFS=','
|
|
239
|
+
printf '%s' "${SELECTED_ENGINES[*]-}"
|
|
240
|
+
}
|
|
241
|
+
|
|
218
242
|
engine_from_menu_label() {
|
|
219
243
|
case "$1" in
|
|
220
244
|
"Codex")
|
|
@@ -957,7 +981,11 @@ print_existing_setup_summary() {
|
|
|
957
981
|
local harnesses=""
|
|
958
982
|
local source_path=""
|
|
959
983
|
local saved_active_engine="$ACTIVE_ENGINE"
|
|
960
|
-
local -a saved_selected_engines=(
|
|
984
|
+
local -a saved_selected_engines=()
|
|
985
|
+
|
|
986
|
+
if (( ${#SELECTED_ENGINES[@]} > 0 )); then
|
|
987
|
+
saved_selected_engines=("${SELECTED_ENGINES[@]}")
|
|
988
|
+
fi
|
|
961
989
|
|
|
962
990
|
if [[ ! -f "$SECURE_ENV_FILE" ]]; then
|
|
963
991
|
return 1
|
|
@@ -971,7 +999,7 @@ print_existing_setup_summary() {
|
|
|
971
999
|
if [[ -n "$harnesses" ]] && ! parse_existing_engine_list_csv "$harnesses"; then
|
|
972
1000
|
harnesses=""
|
|
973
1001
|
elif [[ -n "$harnesses" ]]; then
|
|
974
|
-
harnesses="$(
|
|
1002
|
+
harnesses="$(selected_engines_csv)"
|
|
975
1003
|
fi
|
|
976
1004
|
if [[ -z "$harnesses" ]]; then
|
|
977
1005
|
harnesses="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ACTIVE_ENGINE")"
|
|
@@ -999,7 +1027,10 @@ print_existing_setup_summary() {
|
|
|
999
1027
|
fi
|
|
1000
1028
|
echo "source: $source_path"
|
|
1001
1029
|
|
|
1002
|
-
SELECTED_ENGINES=(
|
|
1030
|
+
SELECTED_ENGINES=()
|
|
1031
|
+
if (( ${#saved_selected_engines[@]} > 0 )); then
|
|
1032
|
+
SELECTED_ENGINES=("${saved_selected_engines[@]}")
|
|
1033
|
+
fi
|
|
1003
1034
|
ACTIVE_ENGINE="$saved_active_engine"
|
|
1004
1035
|
}
|
|
1005
1036
|
|
|
@@ -1082,12 +1113,12 @@ choose_bridge_network_mode() {
|
|
|
1082
1113
|
choose_runtime_engine() {
|
|
1083
1114
|
local label=""
|
|
1084
1115
|
MENU_MULTI_PRESELECTED="Codex"
|
|
1085
|
-
if
|
|
1116
|
+
if selected_engines_contains "codex"; then
|
|
1086
1117
|
MENU_MULTI_PRESELECTED="Codex"
|
|
1087
1118
|
else
|
|
1088
1119
|
MENU_MULTI_PRESELECTED=""
|
|
1089
1120
|
fi
|
|
1090
|
-
if
|
|
1121
|
+
if selected_engines_contains "opencode"; then
|
|
1091
1122
|
if [[ -n "$MENU_MULTI_PRESELECTED" ]]; then
|
|
1092
1123
|
MENU_MULTI_PRESELECTED+=","
|
|
1093
1124
|
fi
|
|
@@ -1101,7 +1132,7 @@ choose_runtime_engine() {
|
|
|
1101
1132
|
SELECTED_ENGINES+=("$(engine_from_menu_label "$label")")
|
|
1102
1133
|
done
|
|
1103
1134
|
sync_active_engine_from_selection
|
|
1104
|
-
info "Selected harnesses: $(
|
|
1135
|
+
info "Selected harnesses: $(format_selected_engines)."
|
|
1105
1136
|
}
|
|
1106
1137
|
|
|
1107
1138
|
infer_network_mode_from_host() {
|
|
@@ -1217,6 +1248,11 @@ ensure_opencode_cli() {
|
|
|
1217
1248
|
|
|
1218
1249
|
ensure_selected_engine_clis() {
|
|
1219
1250
|
local engine=""
|
|
1251
|
+
|
|
1252
|
+
if (( ${#SELECTED_ENGINES[@]} == 0 )); then
|
|
1253
|
+
abort_wizard "Select at least one harness before continuing."
|
|
1254
|
+
fi
|
|
1255
|
+
|
|
1220
1256
|
for engine in "${SELECTED_ENGINES[@]}"; do
|
|
1221
1257
|
case "$engine" in
|
|
1222
1258
|
codex)
|
|
@@ -1613,16 +1649,16 @@ fi
|
|
|
1613
1649
|
load_existing_engine_selection
|
|
1614
1650
|
if [[ "$CONFIG_ACTION" == "keep" ]]; then
|
|
1615
1651
|
if [[ "$ENGINE_SELECTION_PRESET" == "false" ]]; then
|
|
1616
|
-
info "Keeping existing harnesses: $(
|
|
1652
|
+
info "Keeping existing harnesses: $(format_selected_engines)."
|
|
1617
1653
|
else
|
|
1618
|
-
info "Harness selection preset via flag: $(
|
|
1654
|
+
info "Harness selection preset via flag: $(format_selected_engines)."
|
|
1619
1655
|
fi
|
|
1620
1656
|
else
|
|
1621
1657
|
section "Harnesses"
|
|
1622
1658
|
if [[ "$ENGINE_SELECTION_PRESET" == "false" ]]; then
|
|
1623
1659
|
choose_runtime_engine
|
|
1624
1660
|
else
|
|
1625
|
-
info "Harness selection preset via flag: $(
|
|
1661
|
+
info "Harness selection preset via flag: $(format_selected_engines)."
|
|
1626
1662
|
fi
|
|
1627
1663
|
fi
|
|
1628
1664
|
|
|
@@ -1652,7 +1688,7 @@ if [[ "$CONFIG_ACTION" != "keep" ]]; then
|
|
|
1652
1688
|
esac
|
|
1653
1689
|
|
|
1654
1690
|
section "Write secure config"
|
|
1655
|
-
BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(
|
|
1691
|
+
BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(selected_engines_csv)" "$SCRIPT_DIR/setup-secure-dev.sh"
|
|
1656
1692
|
else
|
|
1657
1693
|
ok "Keeping existing secure config."
|
|
1658
1694
|
NETWORK_MODE="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_NETWORK_MODE")"
|
|
@@ -1677,10 +1713,10 @@ else
|
|
|
1677
1713
|
fi
|
|
1678
1714
|
|
|
1679
1715
|
section "Write secure config"
|
|
1680
|
-
BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(
|
|
1681
|
-
elif [[ "$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ENABLED_ENGINES")" != "$(
|
|
1716
|
+
BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(selected_engines_csv)" "$SCRIPT_DIR/setup-secure-dev.sh"
|
|
1717
|
+
elif [[ "$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ENABLED_ENGINES")" != "$(selected_engines_csv)" ]]; then
|
|
1682
1718
|
section "Write secure config"
|
|
1683
|
-
BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(
|
|
1719
|
+
BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(selected_engines_csv)" "$SCRIPT_DIR/setup-secure-dev.sh"
|
|
1684
1720
|
fi
|
|
1685
1721
|
fi
|
|
1686
1722
|
|
|
@@ -1711,7 +1747,7 @@ BRIDGE_PORT="${BRIDGE_PORT:-8787}"
|
|
|
1711
1747
|
section "Summary"
|
|
1712
1748
|
rail_echo "Bridge mode: $NETWORK_MODE"
|
|
1713
1749
|
rail_echo "Bridge endpoint: http://$BRIDGE_HOST:$BRIDGE_PORT"
|
|
1714
|
-
rail_echo "Harnesses: $(
|
|
1750
|
+
rail_echo "Harnesses: $(format_selected_engines)"
|
|
1715
1751
|
rail_echo "Secure env: $SECURE_ENV_FILE"
|
|
1716
1752
|
if [[ "$FLOW" == "quickstart" ]]; then
|
|
1717
1753
|
rail_echo "${DIM}Tip: re-run with Manual mode for full control at each step.${RESET}"
|
|
@@ -17,6 +17,10 @@ const {
|
|
|
17
17
|
|
|
18
18
|
const DEFAULT_HEALTH_TIMEOUT_MS = 15000;
|
|
19
19
|
const DEV_HEALTH_TIMEOUT_MS = 60000;
|
|
20
|
+
let qrcodeTerminal = null;
|
|
21
|
+
let qrcodeTerminalLoaded = false;
|
|
22
|
+
let qrcodeTerminalLoadError = null;
|
|
23
|
+
let pairingQrRenderError = null;
|
|
20
24
|
|
|
21
25
|
function resolveRootDir() {
|
|
22
26
|
let rootDir = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : path.resolve(__dirname, "..");
|
|
@@ -59,6 +63,11 @@ function sleep(ms) {
|
|
|
59
63
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
function readNonEmptyEnv(env, key) {
|
|
67
|
+
const value = env[key];
|
|
68
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
function formatHostForUrl(host) {
|
|
63
72
|
if (host.includes(":") && !host.startsWith("[")) {
|
|
64
73
|
return `[${host}]`;
|
|
@@ -66,71 +75,148 @@ function formatHostForUrl(host) {
|
|
|
66
75
|
return host;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
function
|
|
70
|
-
return
|
|
78
|
+
function buildBridgeUrl(host, port) {
|
|
79
|
+
return `http://${formatHostForUrl(host)}:${port}`;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
function
|
|
74
|
-
|
|
82
|
+
function isUnspecifiedBindHost(host) {
|
|
83
|
+
const normalized = String(host || "").trim().toLowerCase();
|
|
84
|
+
return normalized === "0.0.0.0" || normalized === "::" || normalized === "[::]";
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
function
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const endIndex = lines.findIndex(
|
|
83
|
-
(entry, offset) =>
|
|
84
|
-
offset > index && entry.includes("QR contains bridge URL + token for one-tap onboarding.")
|
|
85
|
-
);
|
|
86
|
-
if (endIndex !== -1) {
|
|
87
|
-
return lines.slice(index, endIndex + 1).join("\n").trimEnd();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
87
|
+
function buildPairingPayload(env, endpoint) {
|
|
88
|
+
const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
|
|
89
|
+
if (!token || isUnspecifiedBindHost(endpoint.host)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
return JSON.stringify({
|
|
94
|
+
type: "clawdex-bridge-pair",
|
|
95
|
+
bridgeUrl: buildBridgeUrl(endpoint.host, endpoint.port),
|
|
96
|
+
bridgeToken: token,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildTokenOnlyPairingPayload(env) {
|
|
101
|
+
const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
|
|
102
|
+
if (!token) {
|
|
103
|
+
return null;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
return
|
|
106
|
+
return JSON.stringify({
|
|
107
|
+
type: "clawdex-bridge-token",
|
|
108
|
+
bridgeToken: token,
|
|
109
|
+
});
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
function
|
|
112
|
+
function loadQrcodeTerminal() {
|
|
113
|
+
if (qrcodeTerminalLoaded) {
|
|
114
|
+
return qrcodeTerminal;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
qrcodeTerminalLoaded = true;
|
|
118
|
+
try {
|
|
119
|
+
qrcodeTerminal = require("qrcode-terminal");
|
|
120
|
+
} catch (error) {
|
|
121
|
+
qrcodeTerminal = null;
|
|
122
|
+
qrcodeTerminalLoadError = error;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return qrcodeTerminal;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function printPairingQr(env, endpoint) {
|
|
129
|
+
pairingQrRenderError = null;
|
|
130
|
+
const qr = loadQrcodeTerminal();
|
|
131
|
+
if (!qr) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
const payload = buildPairingPayload(env, endpoint);
|
|
137
|
+
if (payload) {
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log("Bridge pairing QR (scan from mobile onboarding):");
|
|
140
|
+
qr.generate(payload, { small: true });
|
|
141
|
+
console.log("QR contains bridge URL + token for one-tap onboarding.");
|
|
142
|
+
console.log("");
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const tokenPayload = buildTokenOnlyPairingPayload(env);
|
|
147
|
+
if (!tokenPayload) {
|
|
112
148
|
return false;
|
|
113
149
|
}
|
|
150
|
+
|
|
114
151
|
console.log("");
|
|
115
|
-
console.log(
|
|
152
|
+
console.log("Bridge token QR fallback (scan from mobile onboarding):");
|
|
153
|
+
qr.generate(tokenPayload, { small: true });
|
|
154
|
+
console.log(
|
|
155
|
+
`Full pairing QR unavailable because BRIDGE_HOST=${endpoint.host} is a bind address. Enter URL manually in onboarding.`
|
|
156
|
+
);
|
|
116
157
|
console.log("");
|
|
117
158
|
return true;
|
|
118
|
-
} catch {
|
|
159
|
+
} catch (error) {
|
|
160
|
+
pairingQrRenderError = error;
|
|
119
161
|
return false;
|
|
120
162
|
}
|
|
121
163
|
}
|
|
122
164
|
|
|
123
|
-
|
|
124
|
-
const
|
|
165
|
+
function printPairingQrUnavailableMessage(env) {
|
|
166
|
+
const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
|
|
167
|
+
if (!token) {
|
|
168
|
+
console.log(
|
|
169
|
+
"Pairing QR unavailable because BRIDGE_AUTH_TOKEN is not set. Bridge URL is above for manual onboarding."
|
|
170
|
+
);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
125
173
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
174
|
+
if (pairingQrRenderError) {
|
|
175
|
+
console.log(
|
|
176
|
+
`Pairing QR unavailable because terminal rendering failed: ${pairingQrRenderError.message}. Bridge URL/token are above for manual onboarding.`
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (qrcodeTerminalLoaded && !qrcodeTerminal) {
|
|
182
|
+
const detail =
|
|
183
|
+
qrcodeTerminalLoadError && qrcodeTerminalLoadError.message
|
|
184
|
+
? ` (${qrcodeTerminalLoadError.message})`
|
|
185
|
+
: "";
|
|
186
|
+
console.log(
|
|
187
|
+
`Pairing QR unavailable because the terminal QR renderer could not be loaded${detail}. Bridge URL/token are above for manual onboarding.`
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(
|
|
193
|
+
"Pairing QR unavailable due to an unexpected startup condition. Bridge URL/token are above for manual onboarding."
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function shouldShowPairingQr(env) {
|
|
198
|
+
const raw = readNonEmptyEnv(env, "BRIDGE_SHOW_PAIRING_QR");
|
|
199
|
+
return raw ? raw.toLowerCase() !== "false" : true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function printBridgeAccessDetails(env, endpoint) {
|
|
203
|
+
const bridgeUrl = buildBridgeUrl(endpoint.host, endpoint.port);
|
|
204
|
+
console.log(`Bridge URL: ${bridgeUrl}`);
|
|
205
|
+
|
|
206
|
+
const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
|
|
207
|
+
if (token) {
|
|
208
|
+
console.log(`Bridge token: ${token}`);
|
|
131
209
|
}
|
|
132
210
|
|
|
133
|
-
return
|
|
211
|
+
return bridgeUrl;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function bridgePidFile(rootDir) {
|
|
215
|
+
return path.join(rootDir, ".bridge.pid");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function bridgeLogFile(rootDir) {
|
|
219
|
+
return path.join(rootDir, ".bridge.log");
|
|
134
220
|
}
|
|
135
221
|
|
|
136
222
|
function readPidFile(rootDir) {
|
|
@@ -317,8 +403,12 @@ async function spawnDetachedAndWait(command, args, options) {
|
|
|
317
403
|
if (await probeHealth(healthUrl)) {
|
|
318
404
|
console.log(`Bridge already running (pid ${existingPid}).`);
|
|
319
405
|
console.log(`Logs: ${logPath}`);
|
|
320
|
-
console.log(
|
|
321
|
-
|
|
406
|
+
console.log("Bridge is healthy.");
|
|
407
|
+
const endpoint = { host, port };
|
|
408
|
+
printBridgeAccessDetails(env, endpoint);
|
|
409
|
+
if (shouldShowPairingQr(env) && !printPairingQr(env, endpoint)) {
|
|
410
|
+
printPairingQrUnavailableMessage(env);
|
|
411
|
+
}
|
|
322
412
|
return;
|
|
323
413
|
}
|
|
324
414
|
} else if (existingPid) {
|
|
@@ -334,7 +424,6 @@ async function spawnDetachedAndWait(command, args, options) {
|
|
|
334
424
|
|
|
335
425
|
const output = fs.openSync(logPath, "a");
|
|
336
426
|
const error = fs.openSync(logPath, "a");
|
|
337
|
-
const logStartOffset = fs.existsSync(logPath) ? fs.statSync(logPath).size : 0;
|
|
338
427
|
|
|
339
428
|
const child = spawn(command, args, {
|
|
340
429
|
cwd,
|
|
@@ -362,9 +451,11 @@ async function spawnDetachedAndWait(command, args, options) {
|
|
|
362
451
|
|
|
363
452
|
try {
|
|
364
453
|
const endpoint = await waitForHealth(env, child.pid, healthTimeoutMs);
|
|
365
|
-
console.log(
|
|
366
|
-
|
|
367
|
-
|
|
454
|
+
console.log("Bridge is healthy.");
|
|
455
|
+
printBridgeAccessDetails(env, endpoint);
|
|
456
|
+
|
|
457
|
+
if (shouldShowPairingQr(env) && !printPairingQr(env, endpoint)) {
|
|
458
|
+
printPairingQrUnavailableMessage(env);
|
|
368
459
|
}
|
|
369
460
|
} catch (error) {
|
|
370
461
|
removePidFile(rootDir);
|
package/scripts/stop-services.sh
CHANGED
|
@@ -73,6 +73,23 @@ extract_env_value() {
|
|
|
73
73
|
' "$file"
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
stop_launchctl_job() {
|
|
77
|
+
local label="$1"
|
|
78
|
+
|
|
79
|
+
if [[ -z "$label" ]] || ! command -v launchctl >/dev/null 2>&1; then
|
|
80
|
+
return 1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
local domain="gui/$(id -u)/$label"
|
|
84
|
+
if ! launchctl print "$domain" >/dev/null 2>&1; then
|
|
85
|
+
return 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
echo "Stopping launchd job: $label"
|
|
89
|
+
launchctl bootout "$domain" >/dev/null 2>&1 || launchctl remove "$label" >/dev/null 2>&1 || true
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
stop_process_group() {
|
|
77
94
|
local label="$1"
|
|
78
95
|
local pattern="$2"
|
|
@@ -111,7 +128,12 @@ stop_process_group() {
|
|
|
111
128
|
|
|
112
129
|
echo "Stopping Clawdex services for project: $ROOT_DIR"
|
|
113
130
|
|
|
131
|
+
BRIDGE_PORT="${BRIDGE_PORT:-$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_PORT" || true)}"
|
|
132
|
+
BRIDGE_PORT="${BRIDGE_PORT:-8787}"
|
|
133
|
+
|
|
114
134
|
stop_process_group "Expo" "$ROOT_DIR/.*/expo start|$ROOT_DIR/node_modules/.bin/expo start"
|
|
135
|
+
stop_launchctl_job "clawdex.bridge.$BRIDGE_PORT" || true
|
|
136
|
+
stop_process_group "Bridge launcher" "$ROOT_DIR/scripts/start-bridge-secure\\.js|node .*start-bridge-secure\\.js"
|
|
115
137
|
stop_pid_file_process "Rust bridge" "$BRIDGE_PID_FILE" || true
|
|
116
138
|
stop_process_group "Rust bridge" "$ROOT_DIR/services/rust-bridge|codex-rust-bridge|@codex/rust-bridge"
|
|
117
139
|
stop_process_group "Legacy TS bridge" "$ROOT_DIR/services/mac-bridge|@codex/mac-bridge"
|
|
@@ -2,7 +2,7 @@ use std::{
|
|
|
2
2
|
collections::{HashMap, HashSet, VecDeque},
|
|
3
3
|
env,
|
|
4
4
|
hash::{Hash, Hasher},
|
|
5
|
-
io::SeekFrom,
|
|
5
|
+
io::{SeekFrom, Write},
|
|
6
6
|
path::{Component, Path, PathBuf},
|
|
7
7
|
process::Stdio,
|
|
8
8
|
sync::{
|
|
@@ -5404,6 +5404,11 @@ fn build_token_only_pairing_payload(config: &BridgeConfig) -> Option<String> {
|
|
|
5404
5404
|
)
|
|
5405
5405
|
}
|
|
5406
5406
|
|
|
5407
|
+
fn flush_pairing_output() {
|
|
5408
|
+
let _ = std::io::stdout().flush();
|
|
5409
|
+
let _ = std::io::stderr().flush();
|
|
5410
|
+
}
|
|
5411
|
+
|
|
5407
5412
|
fn maybe_print_pairing_qr(config: &BridgeConfig) {
|
|
5408
5413
|
if !config.show_pairing_qr {
|
|
5409
5414
|
return;
|
|
@@ -5414,15 +5419,18 @@ fn maybe_print_pairing_qr(config: &BridgeConfig) {
|
|
|
5414
5419
|
println!("Bridge pairing QR (scan from mobile onboarding):");
|
|
5415
5420
|
if let Err(error) = qr2term::print_qr(payload.as_bytes()) {
|
|
5416
5421
|
eprintln!("failed to render pairing QR: {error}");
|
|
5422
|
+
flush_pairing_output();
|
|
5417
5423
|
return;
|
|
5418
5424
|
}
|
|
5419
5425
|
println!("QR contains bridge URL + token for one-tap onboarding.");
|
|
5420
5426
|
println!();
|
|
5427
|
+
flush_pairing_output();
|
|
5421
5428
|
return;
|
|
5422
5429
|
}
|
|
5423
5430
|
|
|
5424
5431
|
let Some(payload) = build_token_only_pairing_payload(config) else {
|
|
5425
5432
|
eprintln!("bridge token QR skipped because BRIDGE_AUTH_TOKEN is not set");
|
|
5433
|
+
flush_pairing_output();
|
|
5426
5434
|
return;
|
|
5427
5435
|
};
|
|
5428
5436
|
|
|
@@ -5430,6 +5438,7 @@ fn maybe_print_pairing_qr(config: &BridgeConfig) {
|
|
|
5430
5438
|
println!("Bridge token QR fallback (scan from mobile onboarding):");
|
|
5431
5439
|
if let Err(error) = qr2term::print_qr(payload.as_bytes()) {
|
|
5432
5440
|
eprintln!("failed to render pairing QR: {error}");
|
|
5441
|
+
flush_pairing_output();
|
|
5433
5442
|
return;
|
|
5434
5443
|
}
|
|
5435
5444
|
println!(
|
|
@@ -5437,6 +5446,7 @@ fn maybe_print_pairing_qr(config: &BridgeConfig) {
|
|
|
5437
5446
|
config.host
|
|
5438
5447
|
);
|
|
5439
5448
|
println!();
|
|
5449
|
+
flush_pairing_output();
|
|
5440
5450
|
}
|
|
5441
5451
|
|
|
5442
5452
|
fn parse_bool_env(name: &str) -> bool {
|
|
@@ -9009,6 +9019,71 @@ mod tests {
|
|
|
9009
9019
|
assert!(!constant_time_eq("secret-token", "secret-token-extra"));
|
|
9010
9020
|
}
|
|
9011
9021
|
|
|
9022
|
+
#[test]
|
|
9023
|
+
fn build_pairing_payload_includes_url_and_token_for_connectable_host() {
|
|
9024
|
+
let config = BridgeConfig {
|
|
9025
|
+
host: "127.0.0.1".to_string(),
|
|
9026
|
+
port: 8787,
|
|
9027
|
+
workdir: PathBuf::from("/tmp/workdir"),
|
|
9028
|
+
cli_bin: "codex".to_string(),
|
|
9029
|
+
opencode_cli_bin: "opencode".to_string(),
|
|
9030
|
+
active_engine: BridgeRuntimeEngine::Codex,
|
|
9031
|
+
enabled_engines: vec![BridgeRuntimeEngine::Codex],
|
|
9032
|
+
opencode_host: "127.0.0.1".to_string(),
|
|
9033
|
+
opencode_port: 4090,
|
|
9034
|
+
opencode_server_username: "opencode".to_string(),
|
|
9035
|
+
opencode_server_password: Some("secret-token".to_string()),
|
|
9036
|
+
auth_token: Some("secret-token".to_string()),
|
|
9037
|
+
auth_enabled: true,
|
|
9038
|
+
allow_insecure_no_auth: false,
|
|
9039
|
+
allow_query_token_auth: false,
|
|
9040
|
+
allow_outside_root_cwd: false,
|
|
9041
|
+
disable_terminal_exec: false,
|
|
9042
|
+
terminal_allowed_commands: HashSet::new(),
|
|
9043
|
+
show_pairing_qr: true,
|
|
9044
|
+
};
|
|
9045
|
+
|
|
9046
|
+
let payload = build_pairing_payload(&config).expect("pairing payload");
|
|
9047
|
+
let parsed: Value = serde_json::from_str(&payload).expect("valid json");
|
|
9048
|
+
|
|
9049
|
+
assert_eq!(parsed["type"], "clawdex-bridge-pair");
|
|
9050
|
+
assert_eq!(parsed["bridgeUrl"], "http://127.0.0.1:8787");
|
|
9051
|
+
assert_eq!(parsed["bridgeToken"], "secret-token");
|
|
9052
|
+
}
|
|
9053
|
+
|
|
9054
|
+
#[test]
|
|
9055
|
+
fn build_pairing_payload_uses_token_only_fallback_for_unspecified_bind_host() {
|
|
9056
|
+
let config = BridgeConfig {
|
|
9057
|
+
host: "0.0.0.0".to_string(),
|
|
9058
|
+
port: 8787,
|
|
9059
|
+
workdir: PathBuf::from("/tmp/workdir"),
|
|
9060
|
+
cli_bin: "codex".to_string(),
|
|
9061
|
+
opencode_cli_bin: "opencode".to_string(),
|
|
9062
|
+
active_engine: BridgeRuntimeEngine::Codex,
|
|
9063
|
+
enabled_engines: vec![BridgeRuntimeEngine::Codex],
|
|
9064
|
+
opencode_host: "127.0.0.1".to_string(),
|
|
9065
|
+
opencode_port: 4090,
|
|
9066
|
+
opencode_server_username: "opencode".to_string(),
|
|
9067
|
+
opencode_server_password: Some("secret-token".to_string()),
|
|
9068
|
+
auth_token: Some("secret-token".to_string()),
|
|
9069
|
+
auth_enabled: true,
|
|
9070
|
+
allow_insecure_no_auth: false,
|
|
9071
|
+
allow_query_token_auth: false,
|
|
9072
|
+
allow_outside_root_cwd: false,
|
|
9073
|
+
disable_terminal_exec: false,
|
|
9074
|
+
terminal_allowed_commands: HashSet::new(),
|
|
9075
|
+
show_pairing_qr: true,
|
|
9076
|
+
};
|
|
9077
|
+
|
|
9078
|
+
assert!(build_pairing_payload(&config).is_none());
|
|
9079
|
+
|
|
9080
|
+
let fallback = build_token_only_pairing_payload(&config).expect("token-only payload");
|
|
9081
|
+
let parsed: Value = serde_json::from_str(&fallback).expect("valid json");
|
|
9082
|
+
|
|
9083
|
+
assert_eq!(parsed["type"], "clawdex-bridge-token");
|
|
9084
|
+
assert_eq!(parsed["bridgeToken"], "secret-token");
|
|
9085
|
+
}
|
|
9086
|
+
|
|
9012
9087
|
#[test]
|
|
9013
9088
|
fn bridge_config_authorization_validates_header_and_query_token_paths() {
|
|
9014
9089
|
let base = BridgeConfig {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|