pairling 0.2.11 → 0.2.12
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 +6 -7
- package/package.json +3 -3
- package/payload/mac/SOURCE_REVISION +1 -1
- package/payload/mac/VERSION +1 -1
- package/payload/mac/companiond/pairling_pairing.py +13 -7
- package/payload/mac/companiond/pairlingd.py +160 -307
- package/payload/mac/companiond/runtime_contract.py +0 -3
- package/payload/mac/companiond/runtime_manifest.py +2 -1
- package/payload/mac/companiond/runtime_paths.py +2 -6
- package/payload/mac/companiond/safety_monitor.py +56 -1
- package/payload/mac/connectd/internal/gateway/proxy.go +13 -1
- package/payload/mac/connectd/internal/gateway/proxy_test.go +24 -0
- package/payload/mac/install/bootstrap-first-run.sh +32 -1
- package/payload/mac/install/doctor.sh +43 -14
- package/payload/mac/install/install-runtime.sh +812 -50
- package/payload/mac/install/render-launchd.py +1 -28
- package/payload/mac/install/uninstall-runtime.sh +0 -3
- package/payload/mac/install/verify-payload-manifest.py +71 -0
- package/payload-manifest.json +23 -27
- package/payload/mac/guardian/companion-power-guardian.py +0 -613
- package/payload/mac/guardian/guardian_contract.py +0 -67
|
@@ -33,7 +33,6 @@ PACKAGED_SOURCE_PATHS=(
|
|
|
33
33
|
"mac/connectd/internal"
|
|
34
34
|
"mac/connectd/go.mod"
|
|
35
35
|
"mac/connectd/go.sum"
|
|
36
|
-
"mac/guardian"
|
|
37
36
|
"mac/install"
|
|
38
37
|
"mac/mcp"
|
|
39
38
|
)
|
|
@@ -48,7 +47,6 @@ fi
|
|
|
48
47
|
|
|
49
48
|
PAIRLING_RUNTIME_PORT="${PAIRLING_RUNTIME_PORT:-7773}"
|
|
50
49
|
PAIRLING_DAEMON_LABEL="dev.pairling.companiond"
|
|
51
|
-
PAIRLING_GUARDIAN_LABEL="dev.pairling.power-guardian"
|
|
52
50
|
PAIRLING_CONNECTD_LABEL="dev.pairling.connectd"
|
|
53
51
|
PAIRLING_PTYBROKER_LABEL="dev.pairling.ptybroker"
|
|
54
52
|
APP_SUPPORT="${PAIRLING_APP_SUPPORT_ROOT:-${COMPANION_APP_SUPPORT_ROOT:-$HOME/Library/Application Support/Pairling}}"
|
|
@@ -57,6 +55,7 @@ RELEASES_ROOT="$RUNTIME_ROOT/releases"
|
|
|
57
55
|
STATE_ROOT="$APP_SUPPORT/state"
|
|
58
56
|
PAIR_ROOT="$APP_SUPPORT/pair"
|
|
59
57
|
LOGS_ROOT="${PAIRLING_LOGS_ROOT:-${COMPANION_LOGS_ROOT:-$HOME/Library/Logs/Pairling}}"
|
|
58
|
+
PAIRDROP_ROOT="${PAIRLING_PAIRDROP_ROOT:-$HOME/PairDrop}"
|
|
60
59
|
PLIST_BUILD_DIR="$RUNTIME_ROOT/plists"
|
|
61
60
|
CURRENT_LINK="$RUNTIME_ROOT/current"
|
|
62
61
|
PREVIOUS_LINK="$RUNTIME_ROOT/previous"
|
|
@@ -69,11 +68,9 @@ INSTALL_HISTORY="$STATE_ROOT/install-history.jsonl"
|
|
|
69
68
|
USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_DAEMON_LABEL.plist"
|
|
70
69
|
CONNECTD_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_CONNECTD_LABEL.plist"
|
|
71
70
|
PTYBROKER_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_PTYBROKER_LABEL.plist"
|
|
72
|
-
SYSTEM_PLIST="/Library/LaunchDaemons/$PAIRLING_GUARDIAN_LABEL.plist"
|
|
73
71
|
MCP_SERVER_DIR="$HOME/.claude/mcp-servers"
|
|
74
72
|
MCP_SERVER_SHIM="$MCP_SERVER_DIR/phone-tools.py"
|
|
75
73
|
PYTHON3_BIN="${PAIRLING_DAEMON_PYTHON:-${COMPANION_DAEMON_PYTHON:-$(command -v python3)}}"
|
|
76
|
-
GUARDIAN_PYTHON_BIN="${PAIRLING_GUARDIAN_PYTHON:-${COMPANION_GUARDIAN_PYTHON:-/usr/bin/python3}}"
|
|
77
74
|
# P3 Python custody: the npm shim points PAIRLING_DAEMON_PYTHON at the vendored
|
|
78
75
|
# CPython inside the platform runtime package (…/python/bin/python3). When that
|
|
79
76
|
# is in play we stage the whole interpreter into the release tree and run the
|
|
@@ -100,6 +97,686 @@ is_dry_run() {
|
|
|
100
97
|
[[ "$DRY_RUN" == "1" || "$DRY_RUN" == "true" || "$DRY_RUN" == "TRUE" ]]
|
|
101
98
|
}
|
|
102
99
|
|
|
100
|
+
# launchd_skipped is true when a testing smoke run asked us not to touch launchd,
|
|
101
|
+
# so a real setup can render the polished screen without booting the dev.pairling
|
|
102
|
+
# agents. is_dry_run already suppresses these launchctl calls in a preview. This
|
|
103
|
+
# adds the explicit testing skip on top of that, mirroring the pattern in
|
|
104
|
+
# mac/testing/reset-first-run-state.sh. A normal run leaves the variable unset,
|
|
105
|
+
# so the check is false and launchctl runs exactly as before.
|
|
106
|
+
launchd_skipped() {
|
|
107
|
+
[ "${PAIRLING_TESTING_SKIP_LAUNCHD:-0}" = 1 ]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setup_intro() {
|
|
111
|
+
# When the guided screen is on and this is not a dry run, draw the optional
|
|
112
|
+
# one-shot splash tied to a real recheck. It is bash-only and skippable, and it
|
|
113
|
+
# never aborts setup. The three plain log lines below are the WIZARD_TUI=0
|
|
114
|
+
# fallback and they always print.
|
|
115
|
+
if [ "${WIZARD_TUI:-0}" = 1 ] && ! is_dry_run; then
|
|
116
|
+
wizard_splash || true
|
|
117
|
+
fi
|
|
118
|
+
log "Pairling setup"
|
|
119
|
+
log "This stages the Mac runtime and opens a pairing code for the iPhone."
|
|
120
|
+
log ""
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ----- Guided setup: TTY-aware stage/progress wrapper -----
|
|
124
|
+
# All guided output is plain printf and gated on one IS_TTY check, so it reads as
|
|
125
|
+
# a clear numbered flow in a normal terminal and degrades to plain prefixed lines
|
|
126
|
+
# in a pipe, in CI, or under the bootstrap-first-run.sh log capture. Setting
|
|
127
|
+
# PAIRLING_GUIDED_PLAIN=1 forces the ASCII form even on a TTY.
|
|
128
|
+
if [ -t 1 ] && [ "${PAIRLING_GUIDED_PLAIN:-0}" != "1" ]; then
|
|
129
|
+
GUIDED_TTY=1
|
|
130
|
+
else
|
|
131
|
+
GUIDED_TTY=0
|
|
132
|
+
fi
|
|
133
|
+
GUIDED_STAGE_TOTAL=8
|
|
134
|
+
GUIDED_STAGE_N=0
|
|
135
|
+
GUIDED_STAGE_CURRENT=""
|
|
136
|
+
GUIDED_COMPLETE=0
|
|
137
|
+
# Set to 1 only before an integrity or code signature exit so guided_on_exit
|
|
138
|
+
# shows the no-bypass fatal recovery menu instead of the recoverable one. It
|
|
139
|
+
# stays 0 for every recoverable failure. Declared here so it is always defined
|
|
140
|
+
# under set -u.
|
|
141
|
+
WIZARD_FATAL=0
|
|
142
|
+
|
|
143
|
+
# _machine_path_blocks: returns success when a machine condition should keep the
|
|
144
|
+
# polished screen off, so both gate functions share one authoritative guard list.
|
|
145
|
+
# It returns success for NO_COLOR, for CI, for a dry run, for the --json or
|
|
146
|
+
# --plan-only arguments, and for a dumb, unknown, or empty TERM. It returns
|
|
147
|
+
# failure when no machine condition blocks the screen. A future sixth guard is
|
|
148
|
+
# added here once, so want_tui and want_tui_tty stay authoritative on every path.
|
|
149
|
+
_machine_path_blocks() {
|
|
150
|
+
[ -n "${NO_COLOR:-}" ] && return 0
|
|
151
|
+
[ -n "${CI:-}" ] && return 0
|
|
152
|
+
[ -n "${PAIRLING_DRY_RUN:-}" ] && return 0
|
|
153
|
+
case " $* " in *" --json "*|*" --plan-only "*) return 0;; esac
|
|
154
|
+
case "${TERM:-dumb}" in dumb|unknown|"") return 0;; esac
|
|
155
|
+
return 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# want_tui: the single decision that keeps the polished screen off every machine
|
|
159
|
+
# path. It returns success only when the guided screen should render. It returns
|
|
160
|
+
# failure for a non-terminal stdout and for every machine condition in
|
|
161
|
+
# _machine_path_blocks, which are NO_COLOR, CI, a dry run, the --json or
|
|
162
|
+
# --plan-only arguments, and a dumb or unknown terminal. The bash spine runs the
|
|
163
|
+
# plain numbered flow whenever this returns failure, so machine output stays
|
|
164
|
+
# byte-for-byte the same. The --json and --plan-only checks are defensive only,
|
|
165
|
+
# because those flags do not reach install_runtime on the setup arm today. The
|
|
166
|
+
# gate checks them so the function stays correct if a future caller ever threads
|
|
167
|
+
# them in.
|
|
168
|
+
want_tui() {
|
|
169
|
+
[ -t 1 ] || return 1
|
|
170
|
+
_machine_path_blocks "$@" && return 1
|
|
171
|
+
return 0
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# want_tui_tty: the first-run variant of the gate. The first-run flow pipes our
|
|
175
|
+
# stdout through tee, so [ -t 1 ] is false even though a controlling terminal
|
|
176
|
+
# still exists, and want_tui returns failure for that reason alone. This variant
|
|
177
|
+
# skips only the stdout tty check. It still fails for every machine condition in
|
|
178
|
+
# _machine_path_blocks, so NO_COLOR, CI, a dry run, --json, --plan-only, and a
|
|
179
|
+
# dumb or unknown TERM still disable the screen on the first-run path. It then
|
|
180
|
+
# proves a controlling terminal with a real write to /dev/tty, so only a true
|
|
181
|
+
# controlling terminal enables the screen. The device node can test as writable
|
|
182
|
+
# with no controlling terminal, so the guard writes one empty line instead of
|
|
183
|
+
# testing -w.
|
|
184
|
+
want_tui_tty() {
|
|
185
|
+
_machine_path_blocks "$@" && return 1
|
|
186
|
+
{ : >/dev/tty; } 2>/dev/null || return 1
|
|
187
|
+
return 0
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# wizard_splash_verify: the real recheck the splash result is tied to. It runs a
|
|
191
|
+
# bounded integrity recheck of the staged release. Before staging there is no
|
|
192
|
+
# staged binary, so it returns 0, which means there is nothing to contradict yet.
|
|
193
|
+
# The splash runs in setup_intro, before copy_release, so this is bash-only and
|
|
194
|
+
# starts no python. A real per-file hash recheck happens later in the doctor gate,
|
|
195
|
+
# so the splash result is honest about what it can confirm at intro time.
|
|
196
|
+
wizard_splash_verify() {
|
|
197
|
+
local binary="$CURRENT_LINK/connectd/pairling-connectd"
|
|
198
|
+
if [ -x "$binary" ]; then
|
|
199
|
+
/usr/bin/codesign --verify --strict "$binary" >/dev/null 2>&1 || return 1
|
|
200
|
+
fi
|
|
201
|
+
return 0
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# wizard_splash: an optional one-shot brand beat under about 800 milliseconds.
|
|
205
|
+
# It is bash-only, because it runs before the python is staged. It draws a boxed
|
|
206
|
+
# brand header, the brand word in the accent-to-e-ink gradient and the tagline in
|
|
207
|
+
# paper, then a short non-interactive spinner with sleep 0.08, which bash
|
|
208
|
+
# 3.2.57 allows, then prints a result tied to the recheck. It renders only when
|
|
209
|
+
# GUIDED_TTY is 1, so it follows the same guided screen gate as the numbered
|
|
210
|
+
# stages. GUIDED_TTY is 1 exactly when WIZARD_TUI is 1 and PAIRLING_GUIDED_PLAIN
|
|
211
|
+
# is not 1, so the splash renders on the first-run path where GUIDED_TTY is 1 and
|
|
212
|
+
# the knob is unset, it honors the PAIRLING_GUIDED_PLAIN opt-out and stays plain
|
|
213
|
+
# when that knob is set, and it stays silent on every machine path where
|
|
214
|
+
# GUIDED_TTY is 0. Whether it drops the spinner is decided by reduced_motion_on,
|
|
215
|
+
# which honors PAIRLING_REDUCED_MOTION first and otherwise reads the macOS
|
|
216
|
+
# ReduceMotionEnabled setting. When reduce motion is on it skips the spinner and
|
|
217
|
+
# still prints the brand line and the result line. This
|
|
218
|
+
# is the most cuttable piece of the wizard. If it ever adds risk, drop it and keep
|
|
219
|
+
# the plain intro.
|
|
220
|
+
# reduced_motion_on: decide whether the splash should drop its spinner. The
|
|
221
|
+
# explicit PAIRLING_REDUCED_MOTION knob wins when set to any non-empty value,
|
|
222
|
+
# matching the prior behavior. When it is unset, fall back to the macOS system
|
|
223
|
+
# setting com.apple.Accessibility ReduceMotionEnabled. defaults exits non-zero
|
|
224
|
+
# and prints nothing when that key was never written, so a missing key reads as
|
|
225
|
+
# reduce motion off and the helper returns non-zero, so the spinner runs. A
|
|
226
|
+
# stored value of 1 means reduce motion
|
|
227
|
+
# is on. DEFAULTS_BIN defaults to the pinned /usr/bin/defaults and exists only
|
|
228
|
+
# so tests can point it at a stub.
|
|
229
|
+
reduced_motion_on() {
|
|
230
|
+
if [ "${PAIRLING_REDUCED_MOTION:-}" != "" ]; then
|
|
231
|
+
return 0
|
|
232
|
+
fi
|
|
233
|
+
[ "$("${DEFAULTS_BIN:-/usr/bin/defaults}" read com.apple.Accessibility ReduceMotionEnabled 2>/dev/null)" = 1 ]
|
|
234
|
+
}
|
|
235
|
+
# wizard_palette_init: detect the terminal color tier and set the brand palette
|
|
236
|
+
# variables the guided splash, the box helpers, and the stage markers read. It
|
|
237
|
+
# mirrors the tier detection in mac/docs/setup-wizard-mockup-bash.sh. The tier is
|
|
238
|
+
# true when COLORTERM is truecolor or 24bit, else 256 when tput reports at least
|
|
239
|
+
# 256 colors, else 16 when tput reports at least 8, else none. NO_COLOR forces the
|
|
240
|
+
# none tier. Each palette variable carries a real escape string spelled with the
|
|
241
|
+
# file's \033[ CSI prefix, and is empty in the none tier so a no-color terminal
|
|
242
|
+
# gets plain text. WZ_ERR is a clear error red for the splash failure marker, kept
|
|
243
|
+
# distinct from the warm brand accent and consistent across tiers, so a failure
|
|
244
|
+
# never reads as the brand. The function is idempotent through the WZ_PALETTE_READY guard
|
|
245
|
+
# and never prints, so the load-time call, the defensive call in wizard_splash, and
|
|
246
|
+
# the calls in the stage markers cost only one string test after the first.
|
|
247
|
+
wizard_palette_init() {
|
|
248
|
+
[ "${WZ_PALETTE_READY:-0}" = 1 ] && return 0
|
|
249
|
+
WZ_TIER="none"
|
|
250
|
+
if [ -z "${NO_COLOR:-}" ]; then
|
|
251
|
+
if [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then
|
|
252
|
+
WZ_TIER="true"
|
|
253
|
+
elif [ "$(tput colors 2>/dev/null || echo 0)" -ge 256 ]; then
|
|
254
|
+
WZ_TIER="256"
|
|
255
|
+
elif [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
|
|
256
|
+
WZ_TIER="16"
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
259
|
+
case "$WZ_TIER" in
|
|
260
|
+
true)
|
|
261
|
+
WZ_ACCENT=$'\033[38;2;226;70;42m'; WZ_EINK=$'\033[38;2;168;51;28m'
|
|
262
|
+
WZ_PAPER=$'\033[38;2;231;228;216m'; WZ_OK=$'\033[38;2;120;170;90m'
|
|
263
|
+
WZ_GREY=$'\033[38;2;150;145;135m'; WZ_ERR=$'\033[38;2;208;48;48m' ;;
|
|
264
|
+
256)
|
|
265
|
+
WZ_ACCENT=$'\033[38;5;166m'; WZ_EINK=$'\033[38;5;130m'
|
|
266
|
+
WZ_PAPER=$'\033[38;5;223m'; WZ_OK=$'\033[38;5;107m'; WZ_GREY=$'\033[38;5;244m'
|
|
267
|
+
WZ_ERR=$'\033[38;5;160m' ;;
|
|
268
|
+
16)
|
|
269
|
+
WZ_ACCENT=$'\033[91m'; WZ_EINK=$'\033[31m'; WZ_PAPER=$'\033[37m'
|
|
270
|
+
WZ_OK=$'\033[32m'; WZ_GREY=$'\033[90m'; WZ_ERR=$'\033[31m' ;;
|
|
271
|
+
*)
|
|
272
|
+
WZ_ACCENT=""; WZ_EINK=""; WZ_PAPER=""; WZ_OK=""; WZ_GREY=""; WZ_ERR="" ;;
|
|
273
|
+
esac
|
|
274
|
+
WZ_PALETTE_READY=1
|
|
275
|
+
return 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# wizard_gradient: print text in the brand gradient, accent (226,70,42) sweeping to
|
|
279
|
+
# e-ink (168,51,28), one color step per character. It ports the mockup gradient and
|
|
280
|
+
# runs the per-character sweep only in the true tier, where 24-bit color exists. In
|
|
281
|
+
# every other tier it prints the whole text in bold accent. bash 3.2.57 arithmetic
|
|
282
|
+
# evaluates the n>1 ternary, and the d guard keeps a single-character word from
|
|
283
|
+
# dividing by zero. It emits no trailing reset, so the caller closes the color.
|
|
284
|
+
wizard_gradient() {
|
|
285
|
+
local text="$1"
|
|
286
|
+
if [ "${WZ_TIER:-none}" != "true" ]; then
|
|
287
|
+
printf '\033[1m%s%s\033[0m' "${WZ_ACCENT:-}" "$text"
|
|
288
|
+
return 0
|
|
289
|
+
fi
|
|
290
|
+
local n=${#text} i=0 ch r g b d
|
|
291
|
+
d=$(( n > 1 ? n - 1 : 1 ))
|
|
292
|
+
while [ "$i" -lt "$n" ]; do
|
|
293
|
+
ch="${text:$i:1}"
|
|
294
|
+
r=$(( 226 - (58 * i / d) ))
|
|
295
|
+
g=$(( 70 - (19 * i / d) ))
|
|
296
|
+
b=$(( 42 - (14 * i / d) ))
|
|
297
|
+
printf '\033[38;2;%d;%d;%dm\033[1m%s' "$r" "$g" "$b" "$ch"
|
|
298
|
+
i=$((i + 1))
|
|
299
|
+
done
|
|
300
|
+
return 0
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# wizard_progress_bar: print a fixed-width brand progress bar, accent-filled cells
|
|
304
|
+
# for the completed fraction and grey empty cells for the rest. It ports the bar in
|
|
305
|
+
# mac/docs/setup-wizard-mockup-bash.sh and reads the palette WZ_ACCENT and WZ_GREY,
|
|
306
|
+
# each with a set -u default so a direct caller before wizard_palette_init still
|
|
307
|
+
# runs. The fill glyph and the empty glyph are held in variables and appended as
|
|
308
|
+
# ${s}${glyph}, never as a glyph written right after a variable expansion, because
|
|
309
|
+
# bash 3.2.57 under set -u misparses a multibyte glyph placed directly after $var.
|
|
310
|
+
# The total is guarded to at least one so a zero total never divides by zero, even
|
|
311
|
+
# though every caller passes the fixed stage total. A single trailing reset closes
|
|
312
|
+
# the color, matching the stage header, which already emits escapes in its TTY branch.
|
|
313
|
+
wizard_progress_bar() {
|
|
314
|
+
local done="$1" total="$2" width=34 fill i s='' full='█' empty='░'
|
|
315
|
+
[ "$total" -gt 0 ] || total=1
|
|
316
|
+
fill=$(( width * done / total ))
|
|
317
|
+
i=0
|
|
318
|
+
while [ "$i" -lt "$width" ]; do
|
|
319
|
+
if [ "$i" -lt "$fill" ]; then
|
|
320
|
+
s="${s}${WZ_ACCENT:-}${full}"
|
|
321
|
+
else
|
|
322
|
+
s="${s}${WZ_GREY:-}${empty}"
|
|
323
|
+
fi
|
|
324
|
+
i=$((i + 1))
|
|
325
|
+
done
|
|
326
|
+
printf '%s\033[0m' "$s"
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# wizard_line_h: print one character repeated width times, used for the horizontal
|
|
330
|
+
# rules inside the box borders. It holds the repeat character in a variable and
|
|
331
|
+
# appends it as ${s}${c}, never as a literal glyph placed directly after $s, because
|
|
332
|
+
# bash 3.2.57 under set -u misparses a multibyte glyph written right after a
|
|
333
|
+
# variable expansion as part of the variable name.
|
|
334
|
+
wizard_line_h() {
|
|
335
|
+
local w="$1" c="$2" s="" i=0
|
|
336
|
+
while [ "$i" -lt "$w" ]; do s="${s}${c}"; i=$((i + 1)); done
|
|
337
|
+
printf '%s' "$s"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# wizard_box_top / wizard_box_bot: draw the rounded top and bottom borders of the
|
|
341
|
+
# header box in e-ink. The box glyphs sit in the printf format string, not after a
|
|
342
|
+
# variable expansion, so the multibyte parse hazard does not apply here.
|
|
343
|
+
wizard_box_top() {
|
|
344
|
+
printf ' %s╭%s╮\033[0m\n' "${WZ_EINK:-}" "$(wizard_line_h "$1" "─")"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
wizard_box_bot() {
|
|
348
|
+
printf ' %s╰%s╯\033[0m\n' "${WZ_EINK:-}" "$(wizard_line_h "$1" "─")"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# wizard_box_row: draw one content row of the header box. It takes the inner width,
|
|
352
|
+
# a plain copy of the text used only to size the right pad, and the styled text
|
|
353
|
+
# printed between the borders. It pads to the width with spaces so the right border
|
|
354
|
+
# lines up, and clamps a negative pad to zero when the text is wider than the box.
|
|
355
|
+
wizard_box_row() {
|
|
356
|
+
local w="$1" plain="$2" styled="$3" pad
|
|
357
|
+
pad=$(( w - 2 - ${#plain} )); [ "$pad" -lt 0 ] && pad=0
|
|
358
|
+
printf ' %s│\033[0m %s%s %s│\033[0m\n' "${WZ_EINK:-}" "$styled" "$(wizard_line_h "$pad" " ")" "${WZ_EINK:-}"
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
wizard_splash() {
|
|
362
|
+
[ "${GUIDED_TTY:-0}" = 1 ] || return 0
|
|
363
|
+
wizard_palette_init
|
|
364
|
+
local inner=54 row1 row2
|
|
365
|
+
# printf turns \033 into a real escape byte, so the captured rows carry real
|
|
366
|
+
# escapes for wizard_box_row to place between the borders. row1 is the gradient
|
|
367
|
+
# brand word, a reset to drop the gradient bold, then the paper tagline. The
|
|
368
|
+
# plain-copy arguments size the right pad and never reach the screen.
|
|
369
|
+
row1="$(wizard_gradient "Pairling"; printf '\033[0m%s Pair your iPhone with your coding agents' "$WZ_PAPER")"
|
|
370
|
+
row2="${WZ_PAPER} on this Mac."
|
|
371
|
+
wizard_box_top "$inner"
|
|
372
|
+
wizard_box_row "$inner" "Pairling Pair your iPhone with your coding agents" "$row1"
|
|
373
|
+
wizard_box_row "$inner" " on this Mac." "$row2"
|
|
374
|
+
wizard_box_bot "$inner"
|
|
375
|
+
if ! reduced_motion_on; then
|
|
376
|
+
local frames='⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏' f
|
|
377
|
+
for f in $frames; do
|
|
378
|
+
printf '\r %s%s\033[0m %sVerifying the staged runtime\033[0m' "$WZ_ACCENT" "$f" "$WZ_GREY"
|
|
379
|
+
sleep 0.08
|
|
380
|
+
done
|
|
381
|
+
printf '\r'
|
|
382
|
+
fi
|
|
383
|
+
if wizard_splash_verify; then
|
|
384
|
+
printf ' %sok\033[0m Runtime verified. \n' "$WZ_OK"
|
|
385
|
+
else
|
|
386
|
+
printf ' %sx\033[0m Runtime check failed. See the steps below.\n' "${WZ_ERR:-}"
|
|
387
|
+
fi
|
|
388
|
+
return 0
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
# safety_status_line: print the live SafetyMonitorBridge status as one parseable
|
|
392
|
+
# line. It runs as "$PYTHON3_BIN", the staged vendored python, and imports the
|
|
393
|
+
# bridge from the staged companiond, exactly as doctor.sh and pairlingd.py do. It
|
|
394
|
+
# constructs the bridge as SafetyMonitorBridge(APP_SUPPORT_ROOT, HOME), the same
|
|
395
|
+
# construction the daemon uses. It never reads a TCC store and never calls the
|
|
396
|
+
# safety HTTP routes, which need a device bearer token the local wizard does not
|
|
397
|
+
# have. The companiond dir and the app support root are passed as argv, so an
|
|
398
|
+
# install path with a space is passed whole. On any failure it prints
|
|
399
|
+
# installed=false, so the caller fails closed to the not-installed advisory.
|
|
400
|
+
safety_status_line() {
|
|
401
|
+
"$PYTHON3_BIN" - "$CURRENT_LINK/companiond" "$APP_SUPPORT" <<'PY' 2>/dev/null || printf 'installed=false full_disk_access=unknown\n'
|
|
402
|
+
import os
|
|
403
|
+
import sys
|
|
404
|
+
from pathlib import Path
|
|
405
|
+
|
|
406
|
+
companiond_dir = sys.argv[1]
|
|
407
|
+
app_support_root = Path(sys.argv[2])
|
|
408
|
+
sys.path.insert(0, companiond_dir)
|
|
409
|
+
try:
|
|
410
|
+
from safety_monitor import SafetyMonitorBridge
|
|
411
|
+
bridge = SafetyMonitorBridge(app_support_root, Path(os.path.expanduser("~")))
|
|
412
|
+
status = bridge.status()
|
|
413
|
+
installed = "true" if status.get("installed") else "false"
|
|
414
|
+
fda = str(status.get("full_disk_access") or "unknown")
|
|
415
|
+
except Exception:
|
|
416
|
+
installed, fda = "false", "unknown"
|
|
417
|
+
print("installed=%s full_disk_access=%s" % (installed, fda))
|
|
418
|
+
PY
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# safety_status_installed: return 0 when the bridge reports installed true, 1
|
|
422
|
+
# when it reports installed false, and the not-installed advisory path handles
|
|
423
|
+
# both 1 and any read failure, which also prints installed=false. The reader is
|
|
424
|
+
# the single source of truth for whether the future PairlingSafety.app is present.
|
|
425
|
+
safety_status_installed() {
|
|
426
|
+
case "$(safety_status_line)" in
|
|
427
|
+
*"installed=true"*) return 0 ;;
|
|
428
|
+
*) return 1 ;;
|
|
429
|
+
esac
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# stage_begin: print the numbered header for one guided stage. The TTY branch keeps
|
|
433
|
+
# the bold [N/total] prefix, pads the title to a fixed field so the bar aligns across
|
|
434
|
+
# stages, then draws the brand progress bar filled to the current stage over the
|
|
435
|
+
# total, so the bar fills as the flow advances. It calls wizard_palette_init, which is
|
|
436
|
+
# idempotent, so the bar colors exist even if a caller reaches here first. The plain
|
|
437
|
+
# GUIDED_TTY=0 branch is unchanged and stays byte-identical, so every machine path
|
|
438
|
+
# keeps the exact \n[N/total] Title\n form with no bar and no escape byte.
|
|
439
|
+
stage_begin() {
|
|
440
|
+
GUIDED_STAGE_N=$((GUIDED_STAGE_N + 1))
|
|
441
|
+
GUIDED_STAGE_CURRENT="$1"
|
|
442
|
+
if [ "$GUIDED_TTY" = 1 ]; then
|
|
443
|
+
wizard_palette_init
|
|
444
|
+
printf '\n\033[1m[%d/%d] %-30s\033[0m %s\n' \
|
|
445
|
+
"$GUIDED_STAGE_N" "$GUIDED_STAGE_TOTAL" "$1" \
|
|
446
|
+
"$(wizard_progress_bar "$GUIDED_STAGE_N" "$GUIDED_STAGE_TOTAL")"
|
|
447
|
+
else
|
|
448
|
+
printf '\n[%d/%d] %s\n' "$GUIDED_STAGE_N" "$GUIDED_STAGE_TOTAL" "$1"
|
|
449
|
+
fi
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
stage_ok() {
|
|
453
|
+
if [ "$GUIDED_TTY" = 1 ]; then
|
|
454
|
+
wizard_palette_init
|
|
455
|
+
printf ' %sok\033[0m %s\n' "$WZ_OK" "$1"
|
|
456
|
+
else
|
|
457
|
+
printf ' ok: %s\n' "$1"
|
|
458
|
+
fi
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
stage_skip() {
|
|
462
|
+
if [ "$GUIDED_TTY" = 1 ]; then
|
|
463
|
+
wizard_palette_init
|
|
464
|
+
printf ' %s--\033[0m %s\n' "$WZ_GREY" "$1"
|
|
465
|
+
else
|
|
466
|
+
printf ' skip: %s\n' "$1"
|
|
467
|
+
fi
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
stage_note() {
|
|
471
|
+
printf ' %s\n' "$1"
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# wizard_recovery_menu: a plain bash recovery menu. It uses a plain blocking
|
|
475
|
+
# read with no timeout, which bash 3.2.57 supports, so it works without any
|
|
476
|
+
# python. The recoverable kind offers open Full Disk Access settings, skip, and
|
|
477
|
+
# quit and resume. The fatal kind, an integrity or signature failure,
|
|
478
|
+
# has no retry and no skip, which mirrors the install script that exits with no
|
|
479
|
+
# bypass on a hash mismatch or a code-signature failure. When stdin is not a
|
|
480
|
+
# terminal it prints the options and returns, so a headless or piped run never
|
|
481
|
+
# blocks. open_full_disk_access_pane is defined by the safety step and runs the
|
|
482
|
+
# bridge open_full_disk_access method.
|
|
483
|
+
wizard_recovery_menu() {
|
|
484
|
+
local kind="$1" stage="$2"
|
|
485
|
+
if [ "$kind" = "fatal" ]; then
|
|
486
|
+
stage_note "Pairling stopped to protect your Mac at the $stage step."
|
|
487
|
+
stage_note "A file did not match its signed checksum, so setup will not continue. There is no way to skip this check."
|
|
488
|
+
stage_note "Options: [1] reinstall from a verified copy [2] view logs [q] quit"
|
|
489
|
+
else
|
|
490
|
+
stage_note "Setup needs your help at the $stage step."
|
|
491
|
+
stage_note "Options: [o] open Full Disk Access settings [s] skip for now [q] quit and resume"
|
|
492
|
+
fi
|
|
493
|
+
# Off a terminal, print the options and return without blocking.
|
|
494
|
+
[ -t 0 ] || return 0
|
|
495
|
+
local choice=""
|
|
496
|
+
while :; do
|
|
497
|
+
printf ' Choose an option: '
|
|
498
|
+
read -r choice || return 0
|
|
499
|
+
case "$kind:$choice" in
|
|
500
|
+
# Retry was removed here because no caller loops on the menu return today.
|
|
501
|
+
# In guided_on_exit the process is already exiting, and in safety_step the
|
|
502
|
+
# evidence poll has already timed out before the menu shows, so an [r] key
|
|
503
|
+
# did nothing. When the future PairlingSafety.app makes the evidence poll
|
|
504
|
+
# live, add a real retry loop in safety_step with a distinct menu return
|
|
505
|
+
# code, then reinstate an [r] option that maps to it. An r keypress now
|
|
506
|
+
# falls through to the reprompt below, which is correct.
|
|
507
|
+
recoverable:o|recoverable:O) open_full_disk_access_pane ;;
|
|
508
|
+
recoverable:s|recoverable:S) stage_note "Skipped. You can grant it later and run pairling setup again."; return 0 ;;
|
|
509
|
+
*:q|*:Q) stage_note "Quitting. Run pairling setup again to resume right here."; return 0 ;;
|
|
510
|
+
fatal:1) stage_note "Reinstall Pairling from a verified copy, then run pairling setup again."; return 0 ;;
|
|
511
|
+
fatal:2) stage_note "The full details are in the setup log under the audit folder."; return 0 ;;
|
|
512
|
+
*) stage_note "Pick one of the listed options." ;;
|
|
513
|
+
esac
|
|
514
|
+
done
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
# safety_call_method: run one no-argument SafetyMonitorBridge method as the
|
|
518
|
+
# staged python and print whatever the method returns as a single status word.
|
|
519
|
+
# It imports the bridge from the staged companiond and constructs it the same
|
|
520
|
+
# way the daemon does. The method name is passed as argv, so the python source
|
|
521
|
+
# is fixed. It is used by request_safety_activation, open_full_disk_access_pane,
|
|
522
|
+
# and poll_evidence_test, so they all share one import path.
|
|
523
|
+
safety_call_method() {
|
|
524
|
+
local method="$1"
|
|
525
|
+
"$PYTHON3_BIN" - "$CURRENT_LINK/companiond" "$APP_SUPPORT" "$method" <<'PY' 2>/dev/null || printf 'error\n'
|
|
526
|
+
import os
|
|
527
|
+
import sys
|
|
528
|
+
from pathlib import Path
|
|
529
|
+
|
|
530
|
+
companiond_dir, app_support_root, method = sys.argv[1], Path(sys.argv[2]), sys.argv[3]
|
|
531
|
+
sys.path.insert(0, companiond_dir)
|
|
532
|
+
try:
|
|
533
|
+
from safety_monitor import SafetyMonitorBridge
|
|
534
|
+
bridge = SafetyMonitorBridge(app_support_root, Path(os.path.expanduser("~")))
|
|
535
|
+
result = getattr(bridge, method)()
|
|
536
|
+
# request_activation returns "state", run_evidence_test returns "status",
|
|
537
|
+
# open_full_disk_access returns "state". Print the first present one.
|
|
538
|
+
word = result.get("status") or result.get("state") or ("ok" if result.get("ok") else "error")
|
|
539
|
+
print(word)
|
|
540
|
+
except Exception:
|
|
541
|
+
print("error")
|
|
542
|
+
PY
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# request_safety_activation: guide System Extension approval through the bridge.
|
|
546
|
+
# It only runs when the app is installed, so the bridge launches
|
|
547
|
+
# PairlingSafety.app --pairling-request-activation. The wizard never claims it
|
|
548
|
+
# installed the app.
|
|
549
|
+
request_safety_activation() {
|
|
550
|
+
local state
|
|
551
|
+
state="$(safety_call_method request_activation)"
|
|
552
|
+
if [ "$state" = "approval_requested" ]; then
|
|
553
|
+
stage_note "Approve Pairling Safety Monitor in System Settings, then come back here."
|
|
554
|
+
else
|
|
555
|
+
stage_note "Could not request Safety Monitor approval right now. You can approve it later in System Settings."
|
|
556
|
+
fi
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
# open_full_disk_access_pane: open the Full Disk Access settings page through the
|
|
560
|
+
# bridge open_full_disk_access method, which runs the open command for the
|
|
561
|
+
# Privacy_AllFiles anchor. The wizard does not change any privacy setting itself.
|
|
562
|
+
open_full_disk_access_pane() {
|
|
563
|
+
safety_call_method open_full_disk_access >/dev/null 2>&1 || true
|
|
564
|
+
stage_note "Turn on Full Disk Access for Pairling Safety Monitor, then come back here."
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# poll_evidence_test: re-run the bridge run_evidence_test every two seconds until
|
|
568
|
+
# it reports passed or the time runs out. It uses sleep, not a fractional read,
|
|
569
|
+
# because bash 3.2.57 rejects a sub-second read timeout. It returns 0 on a passed
|
|
570
|
+
# result and 124 on timeout. A "limited" result means process evidence passed but
|
|
571
|
+
# file visibility still needs Full Disk Access, so the loop keeps waiting. The
|
|
572
|
+
# interval and timeout are overridable for tests. The step clamp keeps waited
|
|
573
|
+
# advancing even when the interval is 0, so an always-limited result cannot loop
|
|
574
|
+
# forever.
|
|
575
|
+
poll_evidence_test() {
|
|
576
|
+
local interval="${PAIRLING_SAFETY_POLL_INTERVAL:-2}"
|
|
577
|
+
local timeout="${PAIRLING_SAFETY_POLL_TIMEOUT:-300}"
|
|
578
|
+
local step="$interval"
|
|
579
|
+
[ "$step" -lt 1 ] && step=1
|
|
580
|
+
local waited=0 result
|
|
581
|
+
while [ "$waited" -le "$timeout" ]; do
|
|
582
|
+
result="$(safety_call_method run_evidence_test)"
|
|
583
|
+
if [ "$result" = "passed" ]; then
|
|
584
|
+
return 0
|
|
585
|
+
fi
|
|
586
|
+
[ "$waited" -ge "$timeout" ] && break
|
|
587
|
+
sleep "$interval"
|
|
588
|
+
waited=$((waited + step))
|
|
589
|
+
done
|
|
590
|
+
return 124
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
# safety_step: the one safety gate in v1. It reads the live SafetyMonitorBridge
|
|
594
|
+
# status. Today the app is not installed, so the bridge reports installed false,
|
|
595
|
+
# and this prints one plain advisory line that the Safety Monitor is a future
|
|
596
|
+
# feature and is not installed, and that pairing works without it, then continues.
|
|
597
|
+
# It never claims it installed the app. It never shows or blocks on Full Disk
|
|
598
|
+
# Access when the app is absent. When a future PairlingSafety.app reports
|
|
599
|
+
# installed true, the same flow guides System Extension approval, then Full Disk
|
|
600
|
+
# Access, then polls the evidence test until file evidence passes, advancing on
|
|
601
|
+
# pass or showing the recovery menu on timeout. It never blocks pairing and
|
|
602
|
+
# always returns 0.
|
|
603
|
+
safety_step() {
|
|
604
|
+
if safety_status_installed; then
|
|
605
|
+
stage_note "Pairling Safety Monitor is installed. Setting it up so it can watch your agent sessions."
|
|
606
|
+
request_safety_activation
|
|
607
|
+
open_full_disk_access_pane
|
|
608
|
+
stage_note "Checking that the Safety Monitor can see process and file evidence. We check every 2 seconds."
|
|
609
|
+
if poll_evidence_test; then
|
|
610
|
+
stage_note "The Safety Monitor sees full evidence. Thank you."
|
|
611
|
+
else
|
|
612
|
+
stage_note "The Safety Monitor did not reach full file evidence within the time limit."
|
|
613
|
+
# A skip never blocks pairing. File visibility is the only thing limited
|
|
614
|
+
# until Full Disk Access is granted.
|
|
615
|
+
wizard_recovery_menu recoverable "macOS permissions" || true
|
|
616
|
+
fi
|
|
617
|
+
else
|
|
618
|
+
# The not-installed advisory. This is today's path. It states the truth: the
|
|
619
|
+
# Safety Monitor is a future feature, it is not installed, and pairing works
|
|
620
|
+
# without it. It never claims setup installed anything.
|
|
621
|
+
stage_note "Pairling Safety Monitor is a future feature and is not installed yet. Pairing works without it."
|
|
622
|
+
fi
|
|
623
|
+
stage_note "On first pair your iPhone asks for Local Network access, so allow it. This Mac and the iPhone must be on the same Wi-Fi so the Mac can see the phone."
|
|
624
|
+
stage_note "If Local Network is allowed and pairing still stalls, the block is on the Mac or the network side, not the iPhone."
|
|
625
|
+
stage_note "Accessibility and Automation are only needed later if you enable typing into Terminal from the phone. Run pairling doctor --json to see the exact Mac grantee path before enabling it."
|
|
626
|
+
return 0
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
# guided_on_exit — fires on ANY premature exit during setup (a set -e abort or an
|
|
630
|
+
# explicit exit 1 in staging, service startup, the QR, or auth), so a failure
|
|
631
|
+
# always leaves a clear recovery path. Suppressed once setup sets
|
|
632
|
+
# GUIDED_COMPLETE=1, so a clean run prints nothing here.
|
|
633
|
+
guided_on_exit() {
|
|
634
|
+
local code=$?
|
|
635
|
+
if [ "$GUIDED_COMPLETE" != 1 ] && [ "$code" != 0 ]; then
|
|
636
|
+
if [ "${WIZARD_TUI:-0}" = 1 ]; then
|
|
637
|
+
# Show the bash recovery menu. The kind is recoverable by default. The
|
|
638
|
+
# caller sets WIZARD_FATAL=1 before a fatal integrity or signature exit, so
|
|
639
|
+
# the menu drops retry and skip to mirror this script's no-bypass behavior.
|
|
640
|
+
if [ "${WIZARD_FATAL:-0}" = 1 ]; then
|
|
641
|
+
wizard_recovery_menu fatal "${GUIDED_STAGE_CURRENT:-startup}" || true
|
|
642
|
+
else
|
|
643
|
+
wizard_recovery_menu recoverable "${GUIDED_STAGE_CURRENT:-startup}" || true
|
|
644
|
+
fi
|
|
645
|
+
fi
|
|
646
|
+
printf '\nSetup did not finish (stage: %s, exit %s).\n' "${GUIDED_STAGE_CURRENT:-startup}" "$code" >&2
|
|
647
|
+
printf 'Recovery: run `pairling doctor --json` to inspect, then re-run `pairling setup`.\n' >&2
|
|
648
|
+
printf 'Retry pairing only: `pairling pair --qr`. Sign in to Pairling Connect: `pairling connect-auth-open`.\n' >&2
|
|
649
|
+
fi
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
# guided_permission_notice — advisory only. Surfaces ONLY the permissions the
|
|
653
|
+
# code actually uses (verified against doctor.sh permission_readiness): the
|
|
654
|
+
# iPhone shows a Local Network prompt on first pair, and this Mac needs no
|
|
655
|
+
# privacy permission for basic pairing. It never reads or modifies any privacy
|
|
656
|
+
# setting and never blocks setup.
|
|
657
|
+
guided_permission_notice() {
|
|
658
|
+
stage_note "This Mac needs no special privacy permission to pair."
|
|
659
|
+
stage_note "On your iPhone allow Local Network access when Pairling asks. This Mac and the iPhone must be on the same Wi-Fi so the Mac can see the phone."
|
|
660
|
+
stage_note "If you already allowed Local Network on the iPhone and pairing still stalls, the block is on the Mac or the network, not the iPhone. Check that both devices are on the same Wi-Fi."
|
|
661
|
+
stage_note "Accessibility and Automation are only needed if you later enable typing into Terminal from the phone. Run pairling doctor --json to see the exact Mac grantee path before enabling it."
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# guided_route_proof — one bounded, best-effort read of connectd /status that
|
|
665
|
+
# tells the user whether the remote Pairling Connect route is live yet. Local and
|
|
666
|
+
# LAN pairing already work regardless of the result. This never blocks or fails
|
|
667
|
+
# setup (the whole probe is wrapped in `|| true`).
|
|
668
|
+
guided_route_proof() {
|
|
669
|
+
python3 - "$REPO_ROOT" <<'PY' || true
|
|
670
|
+
import os
|
|
671
|
+
import sys
|
|
672
|
+
|
|
673
|
+
repo_root = sys.argv[1]
|
|
674
|
+
sys.path.insert(0, os.path.join(repo_root, "mac", "companiond"))
|
|
675
|
+
try:
|
|
676
|
+
from pairling_connectd_status import fetch_connectd_status, advertised_pairling_connect_routes
|
|
677
|
+
except Exception:
|
|
678
|
+
sys.exit(0)
|
|
679
|
+
|
|
680
|
+
status = fetch_connectd_status(timeout_seconds=0.7) or {}
|
|
681
|
+
try:
|
|
682
|
+
routes = advertised_pairling_connect_routes(status)
|
|
683
|
+
except Exception:
|
|
684
|
+
routes = []
|
|
685
|
+
|
|
686
|
+
if routes:
|
|
687
|
+
print(" Route check: the Pairling Connect remote route is ready.")
|
|
688
|
+
elif str(status.get("auth_state") or "") == "authenticated":
|
|
689
|
+
print(" Route check: Pairling Connect is signed in; the remote route will advertise shortly.")
|
|
690
|
+
else:
|
|
691
|
+
print(" Route check: local pairing is ready now; the remote route hardens after you sign in and the phone joins.")
|
|
692
|
+
PY
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
# guided_pairing_seen_proof is one bounded, best-effort, read-only check of the
|
|
696
|
+
# devices database that tells the user whether this Mac has recorded the iPhone
|
|
697
|
+
# finishing pairing in this session. It takes the session-start epoch as its
|
|
698
|
+
# first argument, so a device paired in an earlier run does not read as seen on a
|
|
699
|
+
# re-run. It opens the database read-only with mode=ro, so it never locks the
|
|
700
|
+
# file the daemon is writing, and it treats a missing or empty database or any
|
|
701
|
+
# sqlite error as "not seen". It polls up to about 6 seconds at 1 second steps
|
|
702
|
+
# and exits early the moment a matching device appears, so a scanned phone is
|
|
703
|
+
# confirmed within about a second and only an unscanned run waits the full
|
|
704
|
+
# window. The whole probe is wrapped in `|| true`, so it never blocks or fails
|
|
705
|
+
# setup.
|
|
706
|
+
guided_pairing_seen_proof() {
|
|
707
|
+
local since="${1:-0}"
|
|
708
|
+
PAIRLING_PAIRING_SEEN_POLL_STEPS="${PAIRLING_PAIRING_SEEN_POLL_STEPS:-6}" \
|
|
709
|
+
python3 - "$DEVICES_DB" "$since" <<'PY' || true
|
|
710
|
+
import os
|
|
711
|
+
import sqlite3
|
|
712
|
+
import sys
|
|
713
|
+
import time
|
|
714
|
+
|
|
715
|
+
db_path = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
716
|
+
try:
|
|
717
|
+
since = float(sys.argv[2])
|
|
718
|
+
except (IndexError, ValueError):
|
|
719
|
+
since = 0.0
|
|
720
|
+
try:
|
|
721
|
+
steps = int(os.environ.get("PAIRLING_PAIRING_SEEN_POLL_STEPS") or "6")
|
|
722
|
+
except ValueError:
|
|
723
|
+
steps = 6
|
|
724
|
+
if steps < 1:
|
|
725
|
+
steps = 1
|
|
726
|
+
|
|
727
|
+
def count_session_devices():
|
|
728
|
+
# Open read-only with mode=ro so this never locks or creates the database the
|
|
729
|
+
# daemon is writing. Count only non-revoked rows created at or after the
|
|
730
|
+
# session start, so a device from an earlier run does not read as seen.
|
|
731
|
+
con = sqlite3.connect("file:" + db_path + "?mode=ro", uri=True, timeout=0.5)
|
|
732
|
+
try:
|
|
733
|
+
row = con.execute(
|
|
734
|
+
"SELECT COUNT(*) FROM devices WHERE revoked_at IS NULL AND created_at >= ?",
|
|
735
|
+
(since,),
|
|
736
|
+
).fetchone()
|
|
737
|
+
return int(row[0]) if row else 0
|
|
738
|
+
finally:
|
|
739
|
+
con.close()
|
|
740
|
+
|
|
741
|
+
seen = 0
|
|
742
|
+
try:
|
|
743
|
+
for step in range(steps):
|
|
744
|
+
try:
|
|
745
|
+
seen = count_session_devices()
|
|
746
|
+
except Exception:
|
|
747
|
+
# A missing or empty database, or any sqlite error, means not seen.
|
|
748
|
+
seen = 0
|
|
749
|
+
if seen > 0:
|
|
750
|
+
break
|
|
751
|
+
if step < steps - 1:
|
|
752
|
+
time.sleep(1)
|
|
753
|
+
if seen > 0:
|
|
754
|
+
print(" Pairing check: this Mac saw your iPhone connect and finish pairing.")
|
|
755
|
+
else:
|
|
756
|
+
print(" Pairing check: this Mac has not recorded your iPhone finishing pairing yet.")
|
|
757
|
+
print(" If you just scanned the code, give it a moment and it should appear.")
|
|
758
|
+
print(" If it keeps stalling, confirm the iPhone allowed Local Network and both devices are on the same Wi-Fi.")
|
|
759
|
+
except Exception:
|
|
760
|
+
# Any unexpected error means not seen. Never raise, so setup continues.
|
|
761
|
+
pass
|
|
762
|
+
sys.exit(0)
|
|
763
|
+
PY
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
# guided_finish_summary — the success and recovery surface. It states the next
|
|
767
|
+
# device step, proves the route, and prints the exact re-run commands for any
|
|
768
|
+
# step the operator may need to repeat.
|
|
769
|
+
guided_finish_summary() {
|
|
770
|
+
stage_note "The pairing code is shown above. Open Pairling on your iPhone, scan it, then approve this Mac."
|
|
771
|
+
if ! is_dry_run; then
|
|
772
|
+
guided_route_proof || true
|
|
773
|
+
fi
|
|
774
|
+
if ! is_dry_run; then guided_pairing_seen_proof "${PAIRLING_PAIRING_STARTED_AT:-0}" || true; fi
|
|
775
|
+
stage_note "Inspect status anytime: pairling doctor --json"
|
|
776
|
+
stage_note "Re-show the pairing code: pairling pair --qr"
|
|
777
|
+
stage_note "Sign in for the remote route: pairling connect-auth-open"
|
|
778
|
+
}
|
|
779
|
+
|
|
103
780
|
append_history() {
|
|
104
781
|
local status="$1"
|
|
105
782
|
local detail="$2"
|
|
@@ -165,8 +842,6 @@ run_compile_checks() {
|
|
|
165
842
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/providers/external.py"
|
|
166
843
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/providers/registry.py"
|
|
167
844
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/mcp/phone_tools.py"
|
|
168
|
-
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/guardian/companion-power-guardian.py"
|
|
169
|
-
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/guardian/guardian_contract.py"
|
|
170
845
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/install/render-launchd.py"
|
|
171
846
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/install/psk_dependency_check.py"
|
|
172
847
|
rm -rf "$pycache_root"
|
|
@@ -266,10 +941,43 @@ clear_release_quarantine() {
|
|
|
266
941
|
fi
|
|
267
942
|
}
|
|
268
943
|
|
|
944
|
+
ensure_pairdrop_folder() {
|
|
945
|
+
mkdir -p "$PAIRDROP_ROOT"
|
|
946
|
+
chmod 700 "$PAIRDROP_ROOT" 2>/dev/null || true
|
|
947
|
+
local probe="$PAIRDROP_ROOT/.pairling-write-test.$$"
|
|
948
|
+
if ! printf 'ok\n' > "$probe" 2>/dev/null; then
|
|
949
|
+
log "ERROR: PairDrop folder is not writable: $(display_path "$PAIRDROP_ROOT")" >&2
|
|
950
|
+
exit 1
|
|
951
|
+
fi
|
|
952
|
+
rm -f "$probe"
|
|
953
|
+
log "PairDrop folder: $(display_path "$PAIRDROP_ROOT")"
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
payload_manifest_path() {
|
|
957
|
+
local candidate="$REPO_ROOT/../payload-manifest.json"
|
|
958
|
+
if [[ -f "$candidate" ]]; then
|
|
959
|
+
printf '%s\n' "$candidate"
|
|
960
|
+
fi
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
verify_payload_manifest() {
|
|
964
|
+
local manifest
|
|
965
|
+
manifest="$(payload_manifest_path)"
|
|
966
|
+
if [[ -z "$manifest" ]]; then
|
|
967
|
+
return 0
|
|
968
|
+
fi
|
|
969
|
+
log "Verifying npm payload manifest"
|
|
970
|
+
if ! "$PYTHON3_BIN" "$REPO_ROOT/mac/install/verify-payload-manifest.py" "$REPO_ROOT" "$manifest"; then
|
|
971
|
+
WIZARD_FATAL=1
|
|
972
|
+
exit 1
|
|
973
|
+
fi
|
|
974
|
+
}
|
|
975
|
+
|
|
269
976
|
copy_release() {
|
|
270
977
|
local tmp="$RELEASE_ROOT.tmp"
|
|
271
978
|
rm -rf "$tmp"
|
|
272
|
-
|
|
979
|
+
verify_payload_manifest
|
|
980
|
+
mkdir -p "$tmp/bin" "$tmp/companiond" "$tmp/companiond/providers" "$tmp/companiond/integrations/aperture_cli" "$tmp/connectd" "$tmp/mac" "$tmp/mcp"
|
|
273
981
|
cp "$REPO_ROOT/mac/companiond/pairlingd.py" "$tmp/companiond/"
|
|
274
982
|
cp "$REPO_ROOT/mac/companiond/runtime_contract.py" "$tmp/companiond/"
|
|
275
983
|
cp "$REPO_ROOT/mac/companiond/runtime_manifest.py" "$tmp/companiond/"
|
|
@@ -303,19 +1011,17 @@ copy_release() {
|
|
|
303
1011
|
cp "$REPO_ROOT/mac/companiond/integrations/aperture_cli/"*.py "$tmp/companiond/integrations/aperture_cli/"
|
|
304
1012
|
cp "$REPO_ROOT/mac/companiond/providers/"*.py "$tmp/companiond/providers/"
|
|
305
1013
|
cp "$REPO_ROOT/mac/mcp/phone_tools.py" "$tmp/mcp/"
|
|
306
|
-
cp "$REPO_ROOT/mac/guardian/companion-power-guardian.py" "$tmp/guardian/"
|
|
307
|
-
cp "$REPO_ROOT/mac/guardian/guardian_contract.py" "$tmp/guardian/"
|
|
308
1014
|
build_connectd_binary "$tmp/connectd/pairling-connectd"
|
|
309
1015
|
stage_vendored_python "$tmp/python"
|
|
310
1016
|
run_staged_psk_dependency_checks "$tmp"
|
|
311
1017
|
copy_runtime_source_tree "$tmp/mac" "$tmp/connectd/pairling-connectd"
|
|
312
1018
|
write_installed_pairling_launcher "$tmp/bin/pairling"
|
|
313
|
-
chmod 755 "$tmp/bin/pairling" "$tmp/companiond/pairlingd.py" "$tmp/mcp/phone_tools.py"
|
|
1019
|
+
chmod 755 "$tmp/bin/pairling" "$tmp/companiond/pairlingd.py" "$tmp/mcp/phone_tools.py"
|
|
314
1020
|
chmod 755 "$tmp/connectd/pairling-connectd"
|
|
315
|
-
chmod 644 "$tmp/companiond/"*.py "$tmp/mcp/"*.py
|
|
1021
|
+
chmod 644 "$tmp/companiond/"*.py "$tmp/mcp/"*.py
|
|
316
1022
|
chmod 644 "$tmp/companiond/providers/"*.py
|
|
317
1023
|
chmod 644 "$tmp/companiond/integrations/"*.py "$tmp/companiond/integrations/aperture_cli/"*.py
|
|
318
|
-
chmod 755 "$tmp/companiond/pairlingd.py" "$tmp/mcp/phone_tools.py"
|
|
1024
|
+
chmod 755 "$tmp/companiond/pairlingd.py" "$tmp/mcp/phone_tools.py"
|
|
319
1025
|
clear_release_quarantine "$tmp"
|
|
320
1026
|
rm -rf "$RELEASE_ROOT"
|
|
321
1027
|
mv "$tmp" "$RELEASE_ROOT"
|
|
@@ -330,7 +1036,6 @@ copy_runtime_source_tree() {
|
|
|
330
1036
|
"$mac_root/companiond/providers" \
|
|
331
1037
|
"$mac_root/companiond/integrations/aperture_cli" \
|
|
332
1038
|
"$mac_root/connectd/bin" \
|
|
333
|
-
"$mac_root/guardian" \
|
|
334
1039
|
"$mac_root/install" \
|
|
335
1040
|
"$mac_root/mcp" \
|
|
336
1041
|
"$mac_root/packaging/bin"
|
|
@@ -351,7 +1056,6 @@ copy_runtime_source_tree() {
|
|
|
351
1056
|
cp -R "$REPO_ROOT/mac/connectd/cmd" "$mac_root/connectd/"
|
|
352
1057
|
cp -R "$REPO_ROOT/mac/connectd/internal" "$mac_root/connectd/"
|
|
353
1058
|
cp "$connectd_binary" "$mac_root/connectd/bin/pairling-connectd"
|
|
354
|
-
cp "$REPO_ROOT/mac/guardian/"*.py "$mac_root/guardian/"
|
|
355
1059
|
cp "$REPO_ROOT/mac/install/"*.sh "$mac_root/install/"
|
|
356
1060
|
cp "$REPO_ROOT/mac/install/"*.py "$mac_root/install/"
|
|
357
1061
|
cp "$REPO_ROOT/mac/mcp/"*.py "$mac_root/mcp/"
|
|
@@ -396,12 +1100,14 @@ stage_vendored_python() {
|
|
|
396
1100
|
# switch (-) disables that one check for local ad-hoc builds.
|
|
397
1101
|
if ! /usr/bin/codesign --verify --strict "$src_tree/bin/python3" >/dev/null 2>&1; then
|
|
398
1102
|
log "ERROR: vendored python failed codesign verification; refusing to stage: $src_tree/bin/python3" >&2
|
|
1103
|
+
WIZARD_FATAL=1
|
|
399
1104
|
exit 1
|
|
400
1105
|
fi
|
|
401
1106
|
local team identifier
|
|
402
1107
|
identifier="$(/usr/bin/codesign -dvv "$src_tree/bin/python3" 2>&1 | sed -n 's/^Identifier=//p')"
|
|
403
1108
|
if [[ "$identifier" != "$PYTHON_CODESIGN_IDENTIFIER" ]]; then
|
|
404
1109
|
log "ERROR: vendored python identifier '${identifier:-none}' is not '$PYTHON_CODESIGN_IDENTIFIER'; refusing to stage." >&2
|
|
1110
|
+
WIZARD_FATAL=1
|
|
405
1111
|
exit 1
|
|
406
1112
|
fi
|
|
407
1113
|
if [[ "$required_team" == "-" ]]; then
|
|
@@ -410,6 +1116,7 @@ stage_vendored_python() {
|
|
|
410
1116
|
team="$(/usr/bin/codesign -dvv "$src_tree/bin/python3" 2>&1 | sed -n 's/^TeamIdentifier=//p')"
|
|
411
1117
|
if [[ "$team" != "$required_team" ]]; then
|
|
412
1118
|
log "ERROR: vendored python TeamIdentifier '${team:-none}' does not match required '$required_team'; refusing to stage." >&2
|
|
1119
|
+
WIZARD_FATAL=1
|
|
413
1120
|
exit 1
|
|
414
1121
|
fi
|
|
415
1122
|
fi
|
|
@@ -441,12 +1148,14 @@ build_connectd_binary() {
|
|
|
441
1148
|
else
|
|
442
1149
|
if ! /usr/bin/codesign --verify --strict "$prebuilt_env" >/dev/null 2>&1; then
|
|
443
1150
|
log "ERROR: connectd binary failed codesign verification; refusing to stage: $prebuilt_env" >&2
|
|
1151
|
+
WIZARD_FATAL=1
|
|
444
1152
|
exit 1
|
|
445
1153
|
fi
|
|
446
1154
|
local team
|
|
447
1155
|
team="$(/usr/bin/codesign -dvv "$prebuilt_env" 2>&1 | sed -n 's/^TeamIdentifier=//p')"
|
|
448
1156
|
if [[ "$team" != "$required_team" ]]; then
|
|
449
1157
|
log "ERROR: connectd binary TeamIdentifier '${team:-none}' does not match required '$required_team'; refusing to stage: $prebuilt_env" >&2
|
|
1158
|
+
WIZARD_FATAL=1
|
|
450
1159
|
exit 1
|
|
451
1160
|
fi
|
|
452
1161
|
fi
|
|
@@ -536,8 +1245,6 @@ for rel in [
|
|
|
536
1245
|
"companiond/providers/registry.py",
|
|
537
1246
|
"connectd/pairling-connectd",
|
|
538
1247
|
"mcp/phone_tools.py",
|
|
539
|
-
"guardian/companion-power-guardian.py",
|
|
540
|
-
"guardian/guardian_contract.py",
|
|
541
1248
|
]:
|
|
542
1249
|
path = root / rel
|
|
543
1250
|
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
@@ -567,13 +1274,11 @@ manifest = {
|
|
|
567
1274
|
"daemon_label": "dev.pairling.companiond",
|
|
568
1275
|
"ptybroker_label": "dev.pairling.ptybroker",
|
|
569
1276
|
"connectd_label": "dev.pairling.connectd",
|
|
570
|
-
"guardian_label": "dev.pairling.power-guardian",
|
|
571
1277
|
},
|
|
572
1278
|
"paths": {
|
|
573
1279
|
"app_support": app_support,
|
|
574
1280
|
"logs": logs_root,
|
|
575
1281
|
"pair_records": str(Path(app_support) / "pair"),
|
|
576
|
-
"guardian_state": "/var/run/pairling-power-state.json",
|
|
577
1282
|
},
|
|
578
1283
|
"migration": {
|
|
579
1284
|
"legacy_port": 7723,
|
|
@@ -702,7 +1407,6 @@ render_plists() {
|
|
|
702
1407
|
--logs-root "$LOGS_ROOT"
|
|
703
1408
|
--output-dir "$PLIST_BUILD_DIR"
|
|
704
1409
|
--daemon-python "$daemon_python"
|
|
705
|
-
--guardian-python "$GUARDIAN_PYTHON_BIN"
|
|
706
1410
|
)
|
|
707
1411
|
python3 "$REPO_ROOT/mac/install/render-launchd.py" "${render_args[@]}"
|
|
708
1412
|
}
|
|
@@ -715,6 +1419,7 @@ start_user_agent() {
|
|
|
715
1419
|
log "dry-run: rendered $USER_PLIST"
|
|
716
1420
|
return
|
|
717
1421
|
fi
|
|
1422
|
+
if launchd_skipped; then return 0; fi
|
|
718
1423
|
launchctl bootout "gui/$(id -u)" "$USER_PLIST" >/dev/null 2>&1 || true
|
|
719
1424
|
launchctl bootstrap "gui/$(id -u)" "$USER_PLIST" >/dev/null 2>&1 || true
|
|
720
1425
|
launchctl kickstart -k "gui/$(id -u)/$PAIRLING_DAEMON_LABEL"
|
|
@@ -728,6 +1433,7 @@ start_connectd_agent() {
|
|
|
728
1433
|
log "dry-run: rendered $CONNECTD_USER_PLIST"
|
|
729
1434
|
return
|
|
730
1435
|
fi
|
|
1436
|
+
if launchd_skipped; then return 0; fi
|
|
731
1437
|
launchctl bootout "gui/$(id -u)" "$CONNECTD_USER_PLIST" >/dev/null 2>&1 || true
|
|
732
1438
|
launchctl bootstrap "gui/$(id -u)" "$CONNECTD_USER_PLIST" >/dev/null 2>&1 || true
|
|
733
1439
|
launchctl kickstart -k "gui/$(id -u)/$PAIRLING_CONNECTD_LABEL"
|
|
@@ -988,6 +1694,7 @@ ensure_ptybroker_agent() {
|
|
|
988
1694
|
fi
|
|
989
1695
|
return
|
|
990
1696
|
fi
|
|
1697
|
+
if launchd_skipped; then return 0; fi
|
|
991
1698
|
if ! launchctl print "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL" >/dev/null 2>&1; then
|
|
992
1699
|
launchctl bootstrap "gui/$(id -u)" "$PTYBROKER_USER_PLIST" >/dev/null 2>&1 || true
|
|
993
1700
|
launchctl kickstart "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL" >/dev/null 2>&1 || true
|
|
@@ -1069,28 +1776,6 @@ stop_connectd_agent() {
|
|
|
1069
1776
|
launchctl bootout "gui/$(id -u)" "$CONNECTD_USER_PLIST" >/dev/null 2>&1 || true
|
|
1070
1777
|
}
|
|
1071
1778
|
|
|
1072
|
-
install_guardian_if_possible() {
|
|
1073
|
-
local rendered="$PLIST_BUILD_DIR/$PAIRLING_GUARDIAN_LABEL.plist"
|
|
1074
|
-
if [[ "${PAIRLING_INSTALL_GUARDIAN:-0}" != "1" ]]; then
|
|
1075
|
-
log "Optional power guardian not installed; pairing can continue without the privileged sleep helper."
|
|
1076
|
-
return
|
|
1077
|
-
fi
|
|
1078
|
-
if is_dry_run; then
|
|
1079
|
-
log "dry-run: would install $PAIRLING_GUARDIAN_LABEL"
|
|
1080
|
-
return
|
|
1081
|
-
fi
|
|
1082
|
-
if sudo -n true >/dev/null 2>&1; then
|
|
1083
|
-
sudo cp "$rendered" "$SYSTEM_PLIST"
|
|
1084
|
-
sudo chown root:wheel "$SYSTEM_PLIST"
|
|
1085
|
-
sudo chmod 644 "$SYSTEM_PLIST"
|
|
1086
|
-
sudo launchctl bootout system "$SYSTEM_PLIST" >/dev/null 2>&1 || true
|
|
1087
|
-
sudo launchctl bootstrap system "$SYSTEM_PLIST" >/dev/null 2>&1 || true
|
|
1088
|
-
sudo launchctl kickstart -k "system/$PAIRLING_GUARDIAN_LABEL"
|
|
1089
|
-
else
|
|
1090
|
-
log "Skipping guardian install: passwordless sudo is unavailable. Re-run with privileges when ready."
|
|
1091
|
-
fi
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
1779
|
run_doctor() {
|
|
1095
1780
|
"$REPO_ROOT/mac/install/doctor.sh"
|
|
1096
1781
|
}
|
|
@@ -1118,6 +1803,41 @@ rollback() {
|
|
|
1118
1803
|
}
|
|
1119
1804
|
|
|
1120
1805
|
install_runtime() {
|
|
1806
|
+
local setup_args=("$@")
|
|
1807
|
+
# Guard the empty-array expansion: install_runtime is called with no args
|
|
1808
|
+
# today, and under bash 3.2 with set -u "${setup_args[@]}" raises an unbound
|
|
1809
|
+
# variable error when the array is empty. The length check avoids that.
|
|
1810
|
+
# The first-run flow pipes our stdout through tee, so want_tui fails its
|
|
1811
|
+
# [ -t 1 ] check even with a real terminal present. When PAIRLING_WIZARD is set,
|
|
1812
|
+
# want_tui_tty re-enables the screen, but only when a /dev/tty write probe
|
|
1813
|
+
# succeeds and no machine condition blocks it, so NO_COLOR, CI, dry-run, --json,
|
|
1814
|
+
# --plan-only, and a dumb TERM still disable it on the first-run path too.
|
|
1815
|
+
if [ "${#setup_args[@]}" -gt 0 ]; then
|
|
1816
|
+
{ want_tui "${setup_args[@]:-}" || { [ "${PAIRLING_WIZARD:-0}" = 1 ] && want_tui_tty "${setup_args[@]:-}"; }; } \
|
|
1817
|
+
&& [ -x "$PYTHON3_BIN" ] && WIZARD_TUI=1 || WIZARD_TUI=0
|
|
1818
|
+
else
|
|
1819
|
+
{ want_tui || { [ "${PAIRLING_WIZARD:-0}" = 1 ] && want_tui_tty; }; } \
|
|
1820
|
+
&& [ -x "$PYTHON3_BIN" ] && WIZARD_TUI=1 || WIZARD_TUI=0
|
|
1821
|
+
fi
|
|
1822
|
+
# The stage color and the splash follow WIZARD_TUI, not raw stdout, because the
|
|
1823
|
+
# first-run flow pipes our stdout through tee to the terminal, so [ -t 1 ] is
|
|
1824
|
+
# false there even with a real terminal. A machine path keeps WIZARD_TUI 0, so
|
|
1825
|
+
# it stays plain and byte-stable. GUIDED_TTY follows WIZARD_TUI except when
|
|
1826
|
+
# PAIRLING_GUIDED_PLAIN forces the plain form, which keeps the user opt-out the
|
|
1827
|
+
# load-time GUIDED_TTY honors. The knob is unset on the first-run path, so
|
|
1828
|
+
# first-run rendering is unaffected.
|
|
1829
|
+
if [ "$WIZARD_TUI" = 1 ] && [ "${PAIRLING_GUIDED_PLAIN:-0}" != "1" ]; then GUIDED_TTY=1; else GUIDED_TTY=0; fi
|
|
1830
|
+
# Load the brand palette exactly when the guided screen turns on. It sits on its
|
|
1831
|
+
# own line, not inside the gate above, because a contract test extracts that gate
|
|
1832
|
+
# by its literal one-line form. The call is idempotent, so the later defensive
|
|
1833
|
+
# calls in wizard_splash and the stage markers cost only one string test.
|
|
1834
|
+
if [ "$GUIDED_TTY" = 1 ]; then wizard_palette_init; fi
|
|
1835
|
+
if is_dry_run; then GUIDED_STAGE_TOTAL=5; else GUIDED_STAGE_TOTAL=8; fi
|
|
1836
|
+
trap guided_on_exit EXIT
|
|
1837
|
+
# When WIZARD_TUI is 1 the guided stages add the splash, the live safety step,
|
|
1838
|
+
# and the bash recovery menu, all behind a WIZARD_TUI check and the dry-run
|
|
1839
|
+
# guard. When it is 0 the existing plain printf flow runs unchanged.
|
|
1840
|
+
setup_intro
|
|
1121
1841
|
log "Pairling setup preview:"
|
|
1122
1842
|
log " app support: $(display_path "$APP_SUPPORT")"
|
|
1123
1843
|
log " logs: $(display_path "$LOGS_ROOT")"
|
|
@@ -1125,18 +1845,31 @@ install_runtime() {
|
|
|
1125
1845
|
log " PTY Broker LaunchAgent: $PAIRLING_PTYBROKER_LABEL"
|
|
1126
1846
|
log " Connect LaunchAgent: $PAIRLING_CONNECTD_LABEL"
|
|
1127
1847
|
log " runtime port: $PAIRLING_RUNTIME_PORT"
|
|
1848
|
+
|
|
1849
|
+
stage_begin "Preparing the Mac runtime"
|
|
1128
1850
|
run_compile_checks
|
|
1129
1851
|
run_psk_dependency_checks
|
|
1130
1852
|
ensure_state
|
|
1853
|
+
stage_ok "checks passed and state is ready"
|
|
1854
|
+
|
|
1855
|
+
stage_begin "PairDrop folder"
|
|
1856
|
+
ensure_pairdrop_folder
|
|
1857
|
+
stage_ok "$(display_path "$PAIRDROP_ROOT") is ready (private, mode 0700)"
|
|
1858
|
+
|
|
1859
|
+
stage_begin "Staging runtime"
|
|
1131
1860
|
copy_release
|
|
1132
1861
|
switch_current
|
|
1133
1862
|
install_mcp_adapter_shim
|
|
1134
1863
|
install_shell_wrapper
|
|
1864
|
+
stage_ok "staged $RELEASE_NAME"
|
|
1865
|
+
|
|
1866
|
+
stage_begin "Starting Pairling services"
|
|
1135
1867
|
render_plists
|
|
1136
1868
|
ensure_ptybroker_agent
|
|
1137
1869
|
start_user_agent
|
|
1138
1870
|
start_connectd_agent
|
|
1139
|
-
|
|
1871
|
+
stage_ok "companiond, connectd, and ptybroker are running"
|
|
1872
|
+
|
|
1140
1873
|
append_history "installed" "installed $RELEASE_NAME"
|
|
1141
1874
|
if is_dry_run; then
|
|
1142
1875
|
log "dry-run: skipping doctor gate"
|
|
@@ -1144,17 +1877,46 @@ install_runtime() {
|
|
|
1144
1877
|
run_doctor || true
|
|
1145
1878
|
fi
|
|
1146
1879
|
log "Installed Pairling runtime $RELEASE_NAME"
|
|
1880
|
+
|
|
1881
|
+
stage_begin "macOS permissions"
|
|
1882
|
+
if [ "${WIZARD_TUI:-0}" = 1 ] && ! is_dry_run; then
|
|
1883
|
+
# The safety step reads the live SafetyMonitorBridge status. Today it reports
|
|
1884
|
+
# not installed, so it prints one advisory line and continues. When a future
|
|
1885
|
+
# PairlingSafety.app is installed, it guides approval, Full Disk Access, and
|
|
1886
|
+
# the evidence test. It never blocks pairing. The plain advisory notice is the
|
|
1887
|
+
# WIZARD_TUI=0 fallback.
|
|
1888
|
+
safety_step
|
|
1889
|
+
else
|
|
1890
|
+
guided_permission_notice
|
|
1891
|
+
fi
|
|
1892
|
+
stage_ok "no Mac privacy permission is required to pair"
|
|
1893
|
+
|
|
1147
1894
|
if ! is_dry_run; then
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1895
|
+
stage_begin "Pairing code for the iPhone"
|
|
1896
|
+
if [ "${WIZARD_TUI:-0}" = 1 ]; then
|
|
1897
|
+
stage_note "Open Pairling on your iPhone and scan this code. The pair address is printed below it too."
|
|
1898
|
+
fi
|
|
1899
|
+
# Record when this pairing attempt started, so the seen probe in
|
|
1900
|
+
# guided_finish_summary counts only a device paired during this session.
|
|
1901
|
+
export PAIRLING_PAIRING_STARTED_AT="$(python3 -c 'import time;print(time.time())')"
|
|
1902
|
+
if ! PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS="${PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS:-0}" pair_runtime --qr; then
|
|
1154
1903
|
log "Pairling installed, but setup could not generate a pairing invitation. Run: pairling doctor --json; pairling pair --qr" >&2
|
|
1155
1904
|
exit 1
|
|
1156
1905
|
fi
|
|
1906
|
+
stage_ok "pairing code displayed (local-first)"
|
|
1907
|
+
log ""
|
|
1908
|
+
# Browser auth is useful, but it must not block or precede the first pairing
|
|
1909
|
+
# code. First-pair bootstrap is local-first; Pairling Connect hardens after
|
|
1910
|
+
# the phone has joined.
|
|
1911
|
+
stage_begin "Pairling Connect sign-in (Mac)"
|
|
1912
|
+
auto_open_connect_auth
|
|
1913
|
+
stage_ok "Pairling Connect sign-in handled"
|
|
1914
|
+
|
|
1915
|
+
stage_begin "Finish and next steps"
|
|
1916
|
+
guided_finish_summary
|
|
1917
|
+
stage_ok "setup complete"
|
|
1157
1918
|
fi
|
|
1919
|
+
GUIDED_COMPLETE=1
|
|
1158
1920
|
}
|
|
1159
1921
|
|
|
1160
1922
|
status_runtime() {
|
|
@@ -1610,7 +2372,7 @@ connect_auth_open() {
|
|
|
1610
2372
|
exit 1
|
|
1611
2373
|
}
|
|
1612
2374
|
|
|
1613
|
-
# auto_open_connect_auth — kicked off by install_runtime
|
|
2375
|
+
# auto_open_connect_auth — kicked off by install_runtime after the first QR.
|
|
1614
2376
|
# Polls connectd /status for readiness (reusing fetch_connectd_status, the same
|
|
1615
2377
|
# helper pair_runtime imports). When connectd is in interactive mode and not yet
|
|
1616
2378
|
# authenticated (auth_url_present == true AND auth_state != "authenticated"), it
|
|
@@ -1824,7 +2586,7 @@ case "$cmd" in
|
|
|
1824
2586
|
shift
|
|
1825
2587
|
"$REPO_ROOT/mac/install/bootstrap-first-run.sh" "$@"
|
|
1826
2588
|
else
|
|
1827
|
-
install_runtime
|
|
2589
|
+
install_runtime "$@"
|
|
1828
2590
|
fi
|
|
1829
2591
|
;;
|
|
1830
2592
|
first-run)
|