pairling 0.0.1 → 0.1.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/package.json +5 -1
- package/payload/mac/SOURCE_BRANCH +1 -0
- package/payload/mac/SOURCE_DIRTY +1 -0
- package/payload/mac/SOURCE_REVISION +1 -0
- package/payload/mac/VERSION +1 -0
- package/payload/mac/companiond/integrations/__init__.py +1 -0
- package/payload/mac/companiond/integrations/aperture_cli/__init__.py +23 -0
- package/payload/mac/companiond/integrations/aperture_cli/launch.py +456 -0
- package/payload/mac/companiond/integrations/aperture_cli/status.py +393 -0
- package/payload/mac/companiond/live_activity_publisher.py +380 -0
- package/payload/mac/companiond/llm_route.py +108 -0
- package/payload/mac/companiond/local_mcp_bridge.py +156 -0
- package/payload/mac/companiond/model_status_contract.py +101 -0
- package/payload/mac/companiond/pairdrop_store.py +920 -0
- package/payload/mac/companiond/pairling_connectd_status.py +149 -0
- package/payload/mac/companiond/pairling_devices.py +459 -0
- package/payload/mac/companiond/pairling_pairing.py +404 -0
- package/payload/mac/companiond/pairling_relay_claims.py +232 -0
- package/payload/mac/companiond/pairling_tools.py +706 -0
- package/payload/mac/companiond/pairlingd.py +18438 -0
- package/payload/mac/companiond/providers/__init__.py +1 -0
- package/payload/mac/companiond/providers/base.py +255 -0
- package/payload/mac/companiond/providers/claude.py +127 -0
- package/payload/mac/companiond/providers/codex.py +124 -0
- package/payload/mac/companiond/providers/external.py +46 -0
- package/payload/mac/companiond/providers/registry.py +70 -0
- package/payload/mac/companiond/pty_broker.py +887 -0
- package/payload/mac/companiond/push_dispatcher.py +1990 -0
- package/payload/mac/companiond/push_event_catalog.py +566 -0
- package/payload/mac/companiond/request_proof.py +142 -0
- package/payload/mac/companiond/runtime_contract.py +47 -0
- package/payload/mac/companiond/runtime_manifest.py +197 -0
- package/payload/mac/companiond/runtime_paths.py +87 -0
- package/payload/mac/companiond/safety_monitor.py +542 -0
- package/payload/mac/companiond/sentinel_notifications.py +491 -0
- package/payload/mac/companiond/standard_push_publisher.py +516 -0
- package/payload/mac/companiond/substrate_status_contract.py +139 -0
- package/payload/mac/companiond/terminal_screen_backend.py +332 -0
- package/payload/mac/companiond/terminal_text_sanitizer.py +54 -0
- package/payload/mac/companiond/workstate_feed_contract.py +108 -0
- package/payload/mac/connectd/cmd/pairling-connectd/auth_open_test.go +116 -0
- package/payload/mac/connectd/cmd/pairling-connectd/main.go +345 -0
- package/payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go +33 -0
- package/payload/mac/connectd/go.mod +51 -0
- package/payload/mac/connectd/go.sum +229 -0
- package/payload/mac/connectd/internal/gateway/proxy.go +597 -0
- package/payload/mac/connectd/internal/gateway/proxy_test.go +531 -0
- package/payload/mac/connectd/internal/runtime/config.go +99 -0
- package/payload/mac/connectd/internal/runtime/config_test.go +29 -0
- package/payload/mac/connectd/internal/status/status.go +300 -0
- package/payload/mac/connectd/internal/status/status_test.go +263 -0
- package/payload/mac/guardian/companion-power-guardian.py +613 -0
- package/payload/mac/guardian/guardian_contract.py +67 -0
- package/payload/mac/install/bootstrap-first-run.sh +206 -0
- package/payload/mac/install/doctor.sh +660 -0
- package/payload/mac/install/install-runtime.sh +1241 -0
- package/payload/mac/install/render-launchd.py +119 -0
- package/payload/mac/install/uninstall-runtime.sh +136 -0
- package/payload/mac/mcp/phone_tools.py +210 -0
- package/payload/mac/packaging/bin/pairling +63 -0
- package/payload-manifest.json +255 -0
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
export PYTHONDONTWRITEBYTECODE=1
|
|
5
|
+
if [[ -z "${PYTHONPYCACHEPREFIX:-}" ]]; then
|
|
6
|
+
PYTHONPYCACHEPREFIX="${TMPDIR:-/tmp}/pairling-pycache-$(id -u)"
|
|
7
|
+
mkdir -p "$PYTHONPYCACHEPREFIX" 2>/dev/null || true
|
|
8
|
+
export PYTHONPYCACHEPREFIX
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
12
|
+
VERSION="$(tr -d '[:space:]' < "$REPO_ROOT/mac/VERSION")"
|
|
13
|
+
read_source_stamp() {
|
|
14
|
+
local path="$1"
|
|
15
|
+
if [[ -f "$path" ]]; then
|
|
16
|
+
tr -d '[:space:]' < "$path"
|
|
17
|
+
fi
|
|
18
|
+
}
|
|
19
|
+
REVISION="${PAIRLING_SOURCE_REVISION:-$(read_source_stamp "$REPO_ROOT/mac/SOURCE_REVISION")}"
|
|
20
|
+
if [[ -z "$REVISION" ]]; then
|
|
21
|
+
REVISION="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true)"
|
|
22
|
+
fi
|
|
23
|
+
REVISION="${REVISION:-unknown}"
|
|
24
|
+
BRANCH="${PAIRLING_SOURCE_BRANCH:-$(read_source_stamp "$REPO_ROOT/mac/SOURCE_BRANCH")}"
|
|
25
|
+
if [[ -z "$BRANCH" ]]; then
|
|
26
|
+
BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
27
|
+
fi
|
|
28
|
+
BRANCH="${BRANCH:-unknown}"
|
|
29
|
+
PACKAGED_SOURCE_PATHS=(
|
|
30
|
+
"mac/VERSION"
|
|
31
|
+
"mac/companiond"
|
|
32
|
+
"mac/connectd/cmd"
|
|
33
|
+
"mac/connectd/internal"
|
|
34
|
+
"mac/connectd/go.mod"
|
|
35
|
+
"mac/connectd/go.sum"
|
|
36
|
+
"mac/guardian"
|
|
37
|
+
"mac/helper-assistant/PairlingHelperAssistant.swift"
|
|
38
|
+
"mac/install"
|
|
39
|
+
"mac/mcp"
|
|
40
|
+
)
|
|
41
|
+
SOURCE_DIRTY="${PAIRLING_SOURCE_DIRTY:-$(read_source_stamp "$REPO_ROOT/mac/SOURCE_DIRTY")}"
|
|
42
|
+
if [[ -z "$SOURCE_DIRTY" ]]; then
|
|
43
|
+
SOURCE_DIRTY="false"
|
|
44
|
+
if git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1 && \
|
|
45
|
+
[[ -n "$(git -C "$REPO_ROOT" status --porcelain=v1 --untracked-files=all -- "${PACKAGED_SOURCE_PATHS[@]}" 2>/dev/null)" ]]; then
|
|
46
|
+
SOURCE_DIRTY="true"
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
PAIRLING_RUNTIME_PORT="${PAIRLING_RUNTIME_PORT:-7773}"
|
|
51
|
+
PAIRLING_DAEMON_LABEL="dev.pairling.companiond"
|
|
52
|
+
PAIRLING_GUARDIAN_LABEL="dev.pairling.power-guardian"
|
|
53
|
+
PAIRLING_CONNECTD_LABEL="dev.pairling.connectd"
|
|
54
|
+
LEGACY_DAEMON_LABEL="com.mghome.notify-webhook"
|
|
55
|
+
APP_SUPPORT="${PAIRLING_APP_SUPPORT_ROOT:-${COMPANION_APP_SUPPORT_ROOT:-$HOME/Library/Application Support/Pairling}}"
|
|
56
|
+
RUNTIME_ROOT="$APP_SUPPORT/runtime"
|
|
57
|
+
RELEASES_ROOT="$RUNTIME_ROOT/releases"
|
|
58
|
+
STATE_ROOT="$APP_SUPPORT/state"
|
|
59
|
+
PAIR_ROOT="$APP_SUPPORT/pair"
|
|
60
|
+
LOGS_ROOT="${PAIRLING_LOGS_ROOT:-${COMPANION_LOGS_ROOT:-$HOME/Library/Logs/Pairling}}"
|
|
61
|
+
PLIST_BUILD_DIR="$RUNTIME_ROOT/plists"
|
|
62
|
+
CURRENT_LINK="$RUNTIME_ROOT/current"
|
|
63
|
+
PREVIOUS_LINK="$RUNTIME_ROOT/previous"
|
|
64
|
+
RELEASE_NAME="$VERSION-$REVISION"
|
|
65
|
+
RELEASE_ROOT="$RELEASES_ROOT/$RELEASE_NAME"
|
|
66
|
+
CONFIG_FILE="$APP_SUPPORT/config.json"
|
|
67
|
+
DEVICES_DB="$APP_SUPPORT/devices.sqlite"
|
|
68
|
+
MCP_CREDENTIAL="$APP_SUPPORT/mcp-bridge.json"
|
|
69
|
+
INSTALL_HISTORY="$STATE_ROOT/install-history.jsonl"
|
|
70
|
+
USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_DAEMON_LABEL.plist"
|
|
71
|
+
CONNECTD_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_CONNECTD_LABEL.plist"
|
|
72
|
+
LEGACY_USER_PLIST="$HOME/Library/LaunchAgents/$LEGACY_DAEMON_LABEL.plist"
|
|
73
|
+
SYSTEM_PLIST="/Library/LaunchDaemons/$PAIRLING_GUARDIAN_LABEL.plist"
|
|
74
|
+
LEGACY_SYSTEM_PLIST="/Library/LaunchDaemons/com.mghome.companion-power-guardian.plist"
|
|
75
|
+
MCP_SERVER_DIR="$HOME/.claude/mcp-servers"
|
|
76
|
+
MCP_SERVER_SHIM="$MCP_SERVER_DIR/phone-tools.py"
|
|
77
|
+
PYTHON3_BIN="${PAIRLING_DAEMON_PYTHON:-${COMPANION_DAEMON_PYTHON:-$(command -v python3)}}"
|
|
78
|
+
GUARDIAN_PYTHON_BIN="${PAIRLING_GUARDIAN_PYTHON:-${COMPANION_GUARDIAN_PYTHON:-/usr/bin/python3}}"
|
|
79
|
+
DRY_RUN="${PAIRLING_DRY_RUN:-0}"
|
|
80
|
+
|
|
81
|
+
log() {
|
|
82
|
+
printf '%s\n' "$*"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
is_dry_run() {
|
|
86
|
+
[[ "$DRY_RUN" == "1" || "$DRY_RUN" == "true" || "$DRY_RUN" == "TRUE" ]]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
append_history() {
|
|
90
|
+
local status="$1"
|
|
91
|
+
local detail="$2"
|
|
92
|
+
mkdir -p "$STATE_ROOT"
|
|
93
|
+
python3 - "$INSTALL_HISTORY" "$status" "$detail" "$VERSION" "$REVISION" "$RELEASE_ROOT" <<'PY'
|
|
94
|
+
import json
|
|
95
|
+
import sys
|
|
96
|
+
import time
|
|
97
|
+
path, status, detail, version, revision, release_root = sys.argv[1:]
|
|
98
|
+
row = {
|
|
99
|
+
"ts": time.time(),
|
|
100
|
+
"status": status,
|
|
101
|
+
"detail": detail,
|
|
102
|
+
"runtime_version": version,
|
|
103
|
+
"source_revision": revision,
|
|
104
|
+
"release_root": release_root,
|
|
105
|
+
}
|
|
106
|
+
with open(path, "a") as fh:
|
|
107
|
+
fh.write(json.dumps(row, sort_keys=True) + "\n")
|
|
108
|
+
PY
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
run_compile_checks() {
|
|
112
|
+
local pycache_root
|
|
113
|
+
pycache_root="$(mktemp -d)"
|
|
114
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairlingd.py"
|
|
115
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/runtime_contract.py"
|
|
116
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/runtime_manifest.py"
|
|
117
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/runtime_paths.py"
|
|
118
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairdrop_store.py"
|
|
119
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_connectd_status.py"
|
|
120
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_devices.py"
|
|
121
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/local_mcp_bridge.py"
|
|
122
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/llm_route.py"
|
|
123
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_tools.py"
|
|
124
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_pairing.py"
|
|
125
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_relay_claims.py"
|
|
126
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/request_proof.py"
|
|
127
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pty_broker.py"
|
|
128
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/terminal_screen_backend.py"
|
|
129
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/terminal_text_sanitizer.py"
|
|
130
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/push_dispatcher.py"
|
|
131
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/push_event_catalog.py"
|
|
132
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/live_activity_publisher.py"
|
|
133
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/standard_push_publisher.py"
|
|
134
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/safety_monitor.py"
|
|
135
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/sentinel_notifications.py"
|
|
136
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/workstate_feed_contract.py"
|
|
137
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/model_status_contract.py"
|
|
138
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/substrate_status_contract.py"
|
|
139
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/integrations/__init__.py"
|
|
140
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/integrations/aperture_cli/__init__.py"
|
|
141
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/integrations/aperture_cli/launch.py"
|
|
142
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/integrations/aperture_cli/status.py"
|
|
143
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/providers/__init__.py"
|
|
144
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/providers/base.py"
|
|
145
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/providers/claude.py"
|
|
146
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/providers/codex.py"
|
|
147
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/providers/external.py"
|
|
148
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/providers/registry.py"
|
|
149
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/mcp/phone_tools.py"
|
|
150
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/guardian/companion-power-guardian.py"
|
|
151
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/guardian/guardian_contract.py"
|
|
152
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/install/render-launchd.py"
|
|
153
|
+
rm -rf "$pycache_root"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
ensure_state() {
|
|
157
|
+
mkdir -p "$RELEASES_ROOT" "$STATE_ROOT" "$PAIR_ROOT" "$LOGS_ROOT" "$PLIST_BUILD_DIR" "$APP_SUPPORT/modules"
|
|
158
|
+
chmod 700 "$APP_SUPPORT" "$PAIR_ROOT" 2>/dev/null || true
|
|
159
|
+
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
160
|
+
python3 - "$CONFIG_FILE" "$PAIRLING_RUNTIME_PORT" <<'PY'
|
|
161
|
+
import json
|
|
162
|
+
import secrets
|
|
163
|
+
import sys
|
|
164
|
+
from datetime import datetime, timezone
|
|
165
|
+
path, port = sys.argv[1:]
|
|
166
|
+
payload = {
|
|
167
|
+
"schema_version": 1,
|
|
168
|
+
"product": "Pairling",
|
|
169
|
+
"install_id": "inst_" + secrets.token_urlsafe(18),
|
|
170
|
+
"runtime": {
|
|
171
|
+
"label": "dev.pairling.companiond",
|
|
172
|
+
"port": int(port),
|
|
173
|
+
},
|
|
174
|
+
"created_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
|
175
|
+
}
|
|
176
|
+
with open(path, "w") as fh:
|
|
177
|
+
json.dump(payload, fh, indent=2, sort_keys=True)
|
|
178
|
+
fh.write("\n")
|
|
179
|
+
PY
|
|
180
|
+
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
|
|
181
|
+
fi
|
|
182
|
+
python3 - "$DEVICES_DB" <<'PY'
|
|
183
|
+
import sqlite3
|
|
184
|
+
import sys
|
|
185
|
+
path = sys.argv[1]
|
|
186
|
+
with sqlite3.connect(path) as db:
|
|
187
|
+
db.executescript("""
|
|
188
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
189
|
+
device_id TEXT PRIMARY KEY,
|
|
190
|
+
device_name TEXT NOT NULL,
|
|
191
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
192
|
+
scopes_json TEXT NOT NULL,
|
|
193
|
+
install_id TEXT NOT NULL,
|
|
194
|
+
created_at REAL NOT NULL,
|
|
195
|
+
last_seen_at REAL,
|
|
196
|
+
revoked_at REAL
|
|
197
|
+
);
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_devices_token_hash ON devices(token_hash);
|
|
199
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
200
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
201
|
+
ts REAL NOT NULL,
|
|
202
|
+
event TEXT NOT NULL,
|
|
203
|
+
device_id TEXT,
|
|
204
|
+
outcome TEXT NOT NULL,
|
|
205
|
+
path TEXT,
|
|
206
|
+
detail_json TEXT NOT NULL
|
|
207
|
+
);
|
|
208
|
+
""")
|
|
209
|
+
PY
|
|
210
|
+
chmod 600 "$DEVICES_DB" 2>/dev/null || true
|
|
211
|
+
PAIRLING_APP_SUPPORT_ROOT="$APP_SUPPORT" PAIRLING_MCP_CREDENTIAL="$MCP_CREDENTIAL" python3 - "$REPO_ROOT" <<'PY'
|
|
212
|
+
import sys
|
|
213
|
+
|
|
214
|
+
repo_root = sys.argv[1]
|
|
215
|
+
sys.path.insert(0, repo_root + "/mac/companiond")
|
|
216
|
+
|
|
217
|
+
from local_mcp_bridge import ensure_local_mcp_bridge_device
|
|
218
|
+
|
|
219
|
+
ensure_local_mcp_bridge_device()
|
|
220
|
+
PY
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
clear_release_quarantine() {
|
|
224
|
+
local target="$1"
|
|
225
|
+
if command -v xattr >/dev/null 2>&1; then
|
|
226
|
+
xattr -dr com.apple.quarantine "$target" >/dev/null 2>&1 || true
|
|
227
|
+
fi
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
copy_release() {
|
|
231
|
+
local tmp="$RELEASE_ROOT.tmp"
|
|
232
|
+
rm -rf "$tmp"
|
|
233
|
+
mkdir -p "$tmp/bin" "$tmp/companiond" "$tmp/companiond/providers" "$tmp/companiond/integrations/aperture_cli" "$tmp/connectd" "$tmp/guardian" "$tmp/mac" "$tmp/mcp"
|
|
234
|
+
cp "$REPO_ROOT/mac/companiond/pairlingd.py" "$tmp/companiond/"
|
|
235
|
+
cp "$REPO_ROOT/mac/companiond/runtime_contract.py" "$tmp/companiond/"
|
|
236
|
+
cp "$REPO_ROOT/mac/companiond/runtime_manifest.py" "$tmp/companiond/"
|
|
237
|
+
cp "$REPO_ROOT/mac/companiond/runtime_paths.py" "$tmp/companiond/"
|
|
238
|
+
cp "$REPO_ROOT/mac/companiond/pairdrop_store.py" "$tmp/companiond/"
|
|
239
|
+
cp "$REPO_ROOT/mac/companiond/pairling_connectd_status.py" "$tmp/companiond/"
|
|
240
|
+
cp "$REPO_ROOT/mac/companiond/pairling_devices.py" "$tmp/companiond/"
|
|
241
|
+
cp "$REPO_ROOT/mac/companiond/local_mcp_bridge.py" "$tmp/companiond/"
|
|
242
|
+
cp "$REPO_ROOT/mac/companiond/llm_route.py" "$tmp/companiond/"
|
|
243
|
+
cp "$REPO_ROOT/mac/companiond/pairling_tools.py" "$tmp/companiond/"
|
|
244
|
+
cp "$REPO_ROOT/mac/companiond/pairling_pairing.py" "$tmp/companiond/"
|
|
245
|
+
cp "$REPO_ROOT/mac/companiond/pairling_relay_claims.py" "$tmp/companiond/"
|
|
246
|
+
cp "$REPO_ROOT/mac/companiond/request_proof.py" "$tmp/companiond/"
|
|
247
|
+
cp "$REPO_ROOT/mac/companiond/pty_broker.py" "$tmp/companiond/"
|
|
248
|
+
cp "$REPO_ROOT/mac/companiond/terminal_screen_backend.py" "$tmp/companiond/"
|
|
249
|
+
cp "$REPO_ROOT/mac/companiond/terminal_text_sanitizer.py" "$tmp/companiond/"
|
|
250
|
+
cp "$REPO_ROOT/mac/companiond/push_dispatcher.py" "$tmp/companiond/"
|
|
251
|
+
cp "$REPO_ROOT/mac/companiond/push_event_catalog.py" "$tmp/companiond/"
|
|
252
|
+
cp "$REPO_ROOT/mac/companiond/live_activity_publisher.py" "$tmp/companiond/"
|
|
253
|
+
cp "$REPO_ROOT/mac/companiond/standard_push_publisher.py" "$tmp/companiond/"
|
|
254
|
+
cp "$REPO_ROOT/mac/companiond/safety_monitor.py" "$tmp/companiond/"
|
|
255
|
+
cp "$REPO_ROOT/mac/companiond/sentinel_notifications.py" "$tmp/companiond/"
|
|
256
|
+
cp "$REPO_ROOT/mac/companiond/workstate_feed_contract.py" "$tmp/companiond/"
|
|
257
|
+
cp "$REPO_ROOT/mac/companiond/model_status_contract.py" "$tmp/companiond/"
|
|
258
|
+
cp "$REPO_ROOT/mac/companiond/substrate_status_contract.py" "$tmp/companiond/"
|
|
259
|
+
cp "$REPO_ROOT/mac/companiond/integrations/__init__.py" "$tmp/companiond/integrations/"
|
|
260
|
+
cp "$REPO_ROOT/mac/companiond/integrations/aperture_cli/"*.py "$tmp/companiond/integrations/aperture_cli/"
|
|
261
|
+
cp "$REPO_ROOT/mac/companiond/providers/"*.py "$tmp/companiond/providers/"
|
|
262
|
+
cp "$REPO_ROOT/mac/mcp/phone_tools.py" "$tmp/mcp/"
|
|
263
|
+
cp "$REPO_ROOT/mac/guardian/companion-power-guardian.py" "$tmp/guardian/"
|
|
264
|
+
cp "$REPO_ROOT/mac/guardian/guardian_contract.py" "$tmp/guardian/"
|
|
265
|
+
build_connectd_binary "$tmp/connectd/pairling-connectd"
|
|
266
|
+
copy_runtime_source_tree "$tmp/mac" "$tmp/connectd/pairling-connectd"
|
|
267
|
+
write_installed_pairling_launcher "$tmp/bin/pairling"
|
|
268
|
+
chmod 755 "$tmp/bin/pairling" "$tmp/companiond/pairlingd.py" "$tmp/mcp/phone_tools.py" "$tmp/guardian/companion-power-guardian.py"
|
|
269
|
+
chmod 755 "$tmp/connectd/pairling-connectd"
|
|
270
|
+
chmod 644 "$tmp/companiond/"*.py "$tmp/mcp/"*.py "$tmp/guardian/"*.py
|
|
271
|
+
chmod 644 "$tmp/companiond/providers/"*.py
|
|
272
|
+
chmod 644 "$tmp/companiond/integrations/"*.py "$tmp/companiond/integrations/aperture_cli/"*.py
|
|
273
|
+
chmod 755 "$tmp/companiond/pairlingd.py" "$tmp/mcp/phone_tools.py" "$tmp/guardian/companion-power-guardian.py"
|
|
274
|
+
clear_release_quarantine "$tmp"
|
|
275
|
+
rm -rf "$RELEASE_ROOT"
|
|
276
|
+
mv "$tmp" "$RELEASE_ROOT"
|
|
277
|
+
write_manifest "$RELEASE_ROOT"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
copy_runtime_source_tree() {
|
|
281
|
+
local mac_root="$1"
|
|
282
|
+
local connectd_binary="$2"
|
|
283
|
+
mkdir -p \
|
|
284
|
+
"$mac_root/companiond" \
|
|
285
|
+
"$mac_root/companiond/providers" \
|
|
286
|
+
"$mac_root/companiond/integrations/aperture_cli" \
|
|
287
|
+
"$mac_root/connectd/bin" \
|
|
288
|
+
"$mac_root/guardian" \
|
|
289
|
+
"$mac_root/install" \
|
|
290
|
+
"$mac_root/mcp" \
|
|
291
|
+
"$mac_root/packaging/bin"
|
|
292
|
+
cp "$REPO_ROOT/mac/VERSION" "$mac_root/"
|
|
293
|
+
printf '%s\n' "$REVISION" > "$mac_root/SOURCE_REVISION"
|
|
294
|
+
printf '%s\n' "$BRANCH" > "$mac_root/SOURCE_BRANCH"
|
|
295
|
+
printf '%s\n' "$SOURCE_DIRTY" > "$mac_root/SOURCE_DIRTY"
|
|
296
|
+
cp "$REPO_ROOT/mac/companiond/"*.py "$mac_root/companiond/"
|
|
297
|
+
cp "$REPO_ROOT/mac/companiond/providers/"*.py "$mac_root/companiond/providers/"
|
|
298
|
+
cp "$REPO_ROOT/mac/companiond/integrations/__init__.py" "$mac_root/companiond/integrations/"
|
|
299
|
+
cp "$REPO_ROOT/mac/companiond/integrations/aperture_cli/"*.py "$mac_root/companiond/integrations/aperture_cli/"
|
|
300
|
+
cp "$REPO_ROOT/mac/connectd/go.mod" "$mac_root/connectd/"
|
|
301
|
+
cp "$REPO_ROOT/mac/connectd/go.sum" "$mac_root/connectd/"
|
|
302
|
+
cp -R "$REPO_ROOT/mac/connectd/cmd" "$mac_root/connectd/"
|
|
303
|
+
cp -R "$REPO_ROOT/mac/connectd/internal" "$mac_root/connectd/"
|
|
304
|
+
cp "$connectd_binary" "$mac_root/connectd/bin/pairling-connectd"
|
|
305
|
+
cp "$REPO_ROOT/mac/guardian/"*.py "$mac_root/guardian/"
|
|
306
|
+
cp "$REPO_ROOT/mac/install/"*.sh "$mac_root/install/"
|
|
307
|
+
cp "$REPO_ROOT/mac/install/"*.py "$mac_root/install/"
|
|
308
|
+
cp "$REPO_ROOT/mac/mcp/"*.py "$mac_root/mcp/"
|
|
309
|
+
cp "$REPO_ROOT/mac/packaging/bin/pairling" "$mac_root/packaging/bin/"
|
|
310
|
+
chmod 755 "$mac_root/connectd/bin/pairling-connectd" "$mac_root/install/"*.sh "$mac_root/mcp/phone_tools.py" "$mac_root/packaging/bin/pairling"
|
|
311
|
+
chmod 644 "$mac_root/VERSION" "$mac_root/SOURCE_REVISION" "$mac_root/SOURCE_BRANCH" "$mac_root/SOURCE_DIRTY"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
write_installed_pairling_launcher() {
|
|
315
|
+
local out="$1"
|
|
316
|
+
cat >"$out" <<'SH'
|
|
317
|
+
#!/usr/bin/env bash
|
|
318
|
+
set -euo pipefail
|
|
319
|
+
|
|
320
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
321
|
+
exec "$ROOT/mac/packaging/bin/pairling" "$@"
|
|
322
|
+
SH
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
build_connectd_binary() {
|
|
326
|
+
local out="$1"
|
|
327
|
+
# npm-delivered binary: the shim points PAIRLING_CONNECTD_PREBUILT at the
|
|
328
|
+
# platform runtime package. This path is fail-closed: the binary must carry
|
|
329
|
+
# a valid signature from the pinned Team ID or setup refuses to stage it.
|
|
330
|
+
local prebuilt_env="${PAIRLING_CONNECTD_PREBUILT:-}"
|
|
331
|
+
if [[ -n "$prebuilt_env" ]]; then
|
|
332
|
+
if [[ ! -f "$prebuilt_env" ]]; then
|
|
333
|
+
log "ERROR: PAIRLING_CONNECTD_PREBUILT points at a missing file: $prebuilt_env" >&2
|
|
334
|
+
exit 1
|
|
335
|
+
fi
|
|
336
|
+
local required_team="${PAIRLING_CONNECTD_TEAM_ID:-965AVD34A3}"
|
|
337
|
+
if [[ "$required_team" == "-" ]]; then
|
|
338
|
+
log "WARNING: connectd signature verification disabled (PAIRLING_CONNECTD_TEAM_ID=-). Dev builds only."
|
|
339
|
+
else
|
|
340
|
+
if ! /usr/bin/codesign --verify --strict "$prebuilt_env" >/dev/null 2>&1; then
|
|
341
|
+
log "ERROR: connectd binary failed codesign verification; refusing to stage: $prebuilt_env" >&2
|
|
342
|
+
exit 1
|
|
343
|
+
fi
|
|
344
|
+
local team
|
|
345
|
+
team="$(/usr/bin/codesign -dvv "$prebuilt_env" 2>&1 | sed -n 's/^TeamIdentifier=//p')"
|
|
346
|
+
if [[ "$team" != "$required_team" ]]; then
|
|
347
|
+
log "ERROR: connectd binary TeamIdentifier '${team:-none}' does not match required '$required_team'; refusing to stage: $prebuilt_env" >&2
|
|
348
|
+
exit 1
|
|
349
|
+
fi
|
|
350
|
+
fi
|
|
351
|
+
cp "$prebuilt_env" "$out"
|
|
352
|
+
chmod 755 "$out"
|
|
353
|
+
return
|
|
354
|
+
fi
|
|
355
|
+
local prebuilt="$REPO_ROOT/mac/connectd/bin/pairling-connectd"
|
|
356
|
+
if [[ -x "$prebuilt" ]]; then
|
|
357
|
+
cp "$prebuilt" "$out"
|
|
358
|
+
chmod 755 "$out"
|
|
359
|
+
return
|
|
360
|
+
fi
|
|
361
|
+
local go_bin
|
|
362
|
+
go_bin="$(command -v go || true)"
|
|
363
|
+
if [[ -z "$go_bin" ]]; then
|
|
364
|
+
for candidate in /opt/homebrew/bin/go /usr/local/go/bin/go /usr/local/bin/go; do
|
|
365
|
+
if [[ -x "$candidate" ]]; then
|
|
366
|
+
go_bin="$candidate"
|
|
367
|
+
break
|
|
368
|
+
fi
|
|
369
|
+
done
|
|
370
|
+
fi
|
|
371
|
+
if [[ -z "$go_bin" ]]; then
|
|
372
|
+
log "ERROR: go is required to build pairling-connectd" >&2
|
|
373
|
+
exit 1
|
|
374
|
+
fi
|
|
375
|
+
(
|
|
376
|
+
cd "$REPO_ROOT/mac/connectd"
|
|
377
|
+
"$go_bin" build -o "$out" ./cmd/pairling-connectd
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
write_manifest() {
|
|
382
|
+
local root="$1"
|
|
383
|
+
python3 - "$REPO_ROOT" "$root" "$VERSION" "$REVISION" "$BRANCH" "$SOURCE_DIRTY" "$APP_SUPPORT" "$LOGS_ROOT" "$DEVICES_DB" "$PAIRLING_RUNTIME_PORT" <<'PY'
|
|
384
|
+
import getpass
|
|
385
|
+
import hashlib
|
|
386
|
+
import json
|
|
387
|
+
import sys
|
|
388
|
+
from datetime import datetime, timezone
|
|
389
|
+
from pathlib import Path
|
|
390
|
+
|
|
391
|
+
repo_root, install_root, version, revision, branch, dirty, app_support, logs_root, devices_db, port = sys.argv[1:]
|
|
392
|
+
root = Path(install_root)
|
|
393
|
+
files = []
|
|
394
|
+
for rel in [
|
|
395
|
+
"bin/pairling",
|
|
396
|
+
"companiond/pairlingd.py",
|
|
397
|
+
"companiond/runtime_contract.py",
|
|
398
|
+
"companiond/runtime_manifest.py",
|
|
399
|
+
"companiond/runtime_paths.py",
|
|
400
|
+
"companiond/pairdrop_store.py",
|
|
401
|
+
"companiond/pairling_connectd_status.py",
|
|
402
|
+
"companiond/pairling_devices.py",
|
|
403
|
+
"companiond/local_mcp_bridge.py",
|
|
404
|
+
"companiond/llm_route.py",
|
|
405
|
+
"companiond/pairling_tools.py",
|
|
406
|
+
"companiond/pairling_pairing.py",
|
|
407
|
+
"companiond/pairling_relay_claims.py",
|
|
408
|
+
"companiond/request_proof.py",
|
|
409
|
+
"companiond/pty_broker.py",
|
|
410
|
+
"companiond/terminal_screen_backend.py",
|
|
411
|
+
"companiond/terminal_text_sanitizer.py",
|
|
412
|
+
"companiond/push_dispatcher.py",
|
|
413
|
+
"companiond/push_event_catalog.py",
|
|
414
|
+
"companiond/live_activity_publisher.py",
|
|
415
|
+
"companiond/standard_push_publisher.py",
|
|
416
|
+
"companiond/safety_monitor.py",
|
|
417
|
+
"companiond/sentinel_notifications.py",
|
|
418
|
+
"companiond/workstate_feed_contract.py",
|
|
419
|
+
"companiond/model_status_contract.py",
|
|
420
|
+
"companiond/substrate_status_contract.py",
|
|
421
|
+
"companiond/integrations/__init__.py",
|
|
422
|
+
"companiond/integrations/aperture_cli/__init__.py",
|
|
423
|
+
"companiond/integrations/aperture_cli/launch.py",
|
|
424
|
+
"companiond/integrations/aperture_cli/status.py",
|
|
425
|
+
"companiond/providers/__init__.py",
|
|
426
|
+
"companiond/providers/base.py",
|
|
427
|
+
"companiond/providers/claude.py",
|
|
428
|
+
"companiond/providers/codex.py",
|
|
429
|
+
"companiond/providers/external.py",
|
|
430
|
+
"companiond/providers/registry.py",
|
|
431
|
+
"connectd/pairling-connectd",
|
|
432
|
+
"mcp/phone_tools.py",
|
|
433
|
+
"guardian/companion-power-guardian.py",
|
|
434
|
+
"guardian/guardian_contract.py",
|
|
435
|
+
]:
|
|
436
|
+
path = root / rel
|
|
437
|
+
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
438
|
+
files.append({"path": rel, "sha256": digest})
|
|
439
|
+
|
|
440
|
+
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
441
|
+
manifest = {
|
|
442
|
+
"schema_version": 1,
|
|
443
|
+
"runtime_name": "pairlingd",
|
|
444
|
+
"runtime_version": version,
|
|
445
|
+
"contract_version": "pairling-runtime-v1",
|
|
446
|
+
"source_revision": revision,
|
|
447
|
+
"source_branch": branch,
|
|
448
|
+
"source_dirty": dirty == "true",
|
|
449
|
+
"built_at": now,
|
|
450
|
+
"installed_at": now,
|
|
451
|
+
"installed_by": getpass.getuser(),
|
|
452
|
+
"repo_path": repo_root,
|
|
453
|
+
"install_root": str(root),
|
|
454
|
+
"current_symlink": str(root.parent.parent / "current"),
|
|
455
|
+
"runtime": {
|
|
456
|
+
"port": int(port),
|
|
457
|
+
"auth": "per-device-scoped-bearer",
|
|
458
|
+
"token_registry": devices_db,
|
|
459
|
+
},
|
|
460
|
+
"launchd": {
|
|
461
|
+
"daemon_label": "dev.pairling.companiond",
|
|
462
|
+
"connectd_label": "dev.pairling.connectd",
|
|
463
|
+
"guardian_label": "dev.pairling.power-guardian",
|
|
464
|
+
"legacy_daemon_label": "com.mghome.notify-webhook",
|
|
465
|
+
},
|
|
466
|
+
"paths": {
|
|
467
|
+
"app_support": app_support,
|
|
468
|
+
"logs": logs_root,
|
|
469
|
+
"pair_records": str(Path(app_support) / "pair"),
|
|
470
|
+
"guardian_state": "/var/run/pairling-power-state.json",
|
|
471
|
+
},
|
|
472
|
+
"migration": {
|
|
473
|
+
"legacy_port": 7723,
|
|
474
|
+
"legacy_daemon_unloaded_by_setup": True,
|
|
475
|
+
"public_v1_dual_bind": False,
|
|
476
|
+
},
|
|
477
|
+
"packaging": {
|
|
478
|
+
"helper_bundle_id": "dev.pairling.helper",
|
|
479
|
+
"homebrew_tap": "pairling-app/tap",
|
|
480
|
+
"homebrew_cask": "pairling-helper",
|
|
481
|
+
},
|
|
482
|
+
"files": files,
|
|
483
|
+
}
|
|
484
|
+
(root / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
|
|
485
|
+
PY
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
switch_current() {
|
|
489
|
+
if [[ -L "$CURRENT_LINK" ]]; then
|
|
490
|
+
local old
|
|
491
|
+
old="$(readlink "$CURRENT_LINK")"
|
|
492
|
+
if [[ -n "$old" ]]; then
|
|
493
|
+
rm -f "$PREVIOUS_LINK"
|
|
494
|
+
ln -s "$old" "$PREVIOUS_LINK"
|
|
495
|
+
fi
|
|
496
|
+
fi
|
|
497
|
+
rm -f "$CURRENT_LINK"
|
|
498
|
+
ln -s "$RELEASE_ROOT" "$CURRENT_LINK"
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
install_mcp_adapter_shim() {
|
|
502
|
+
mkdir -p "$MCP_SERVER_DIR"
|
|
503
|
+
python3 - "$MCP_SERVER_SHIM" "$CURRENT_LINK/mcp/phone_tools.py" <<'PY'
|
|
504
|
+
import os
|
|
505
|
+
import sys
|
|
506
|
+
from pathlib import Path
|
|
507
|
+
|
|
508
|
+
shim = Path(sys.argv[1])
|
|
509
|
+
adapter = Path(sys.argv[2])
|
|
510
|
+
shim.write_text(f'''#!/usr/bin/env python3
|
|
511
|
+
"""Installed shim for the Pairling daemon-first phone-tools MCP server."""
|
|
512
|
+
|
|
513
|
+
from __future__ import annotations
|
|
514
|
+
|
|
515
|
+
import runpy
|
|
516
|
+
import sys
|
|
517
|
+
from pathlib import Path
|
|
518
|
+
|
|
519
|
+
PAIRLING_MCP_ADAPTER = Path({str(adapter)!r})
|
|
520
|
+
|
|
521
|
+
if not PAIRLING_MCP_ADAPTER.is_file():
|
|
522
|
+
print(
|
|
523
|
+
f"FATAL: Pairling MCP adapter is missing at {{PAIRLING_MCP_ADAPTER}}. "
|
|
524
|
+
"Run Pairling setup or restore the runtime install.",
|
|
525
|
+
file=sys.stderr,
|
|
526
|
+
)
|
|
527
|
+
raise SystemExit(1)
|
|
528
|
+
|
|
529
|
+
runpy.run_path(str(PAIRLING_MCP_ADAPTER), run_name="__main__")
|
|
530
|
+
''')
|
|
531
|
+
os.chmod(shim, 0o755)
|
|
532
|
+
PY
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
install_shell_wrapper() {
|
|
536
|
+
local user_bin="${PAIRLING_USER_BIN_DIR:-$HOME/.local/bin}"
|
|
537
|
+
local target="$user_bin/pairling"
|
|
538
|
+
local tmp="$target.tmp"
|
|
539
|
+
mkdir -p "$user_bin"
|
|
540
|
+
cat >"$tmp" <<'SH'
|
|
541
|
+
#!/usr/bin/env bash
|
|
542
|
+
set -euo pipefail
|
|
543
|
+
|
|
544
|
+
if [[ -n "${PAIRLING_REPO_ROOT:-}" ]]; then
|
|
545
|
+
exec "$PAIRLING_REPO_ROOT/mac/packaging/bin/pairling" "$@"
|
|
546
|
+
fi
|
|
547
|
+
|
|
548
|
+
APP_SUPPORT="${PAIRLING_APP_SUPPORT_ROOT:-${COMPANION_APP_SUPPORT_ROOT:-$HOME/Library/Application Support/Pairling}}"
|
|
549
|
+
RUNTIME_PAIRLING="$APP_SUPPORT/runtime/current/bin/pairling"
|
|
550
|
+
if [[ -x "$RUNTIME_PAIRLING" ]]; then
|
|
551
|
+
exec "$RUNTIME_PAIRLING" "$@"
|
|
552
|
+
fi
|
|
553
|
+
|
|
554
|
+
printf 'Pairling runtime command is not installed. Open Pairling Helper and run Start setup, or use a repo-local mac/packaging/bin/pairling.\n' >&2
|
|
555
|
+
exit 127
|
|
556
|
+
SH
|
|
557
|
+
chmod 755 "$tmp"
|
|
558
|
+
mv "$tmp" "$target"
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
render_plists() {
|
|
562
|
+
python3 "$REPO_ROOT/mac/install/render-launchd.py" \
|
|
563
|
+
--current-root "$CURRENT_LINK" \
|
|
564
|
+
--logs-root "$LOGS_ROOT" \
|
|
565
|
+
--output-dir "$PLIST_BUILD_DIR" \
|
|
566
|
+
--daemon-python "$PYTHON3_BIN" \
|
|
567
|
+
--guardian-python "$GUARDIAN_PYTHON_BIN"
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
unload_legacy_daemon() {
|
|
571
|
+
if is_dry_run; then
|
|
572
|
+
log "dry-run: would unload $LEGACY_DAEMON_LABEL"
|
|
573
|
+
return
|
|
574
|
+
fi
|
|
575
|
+
launchctl bootout "gui/$(id -u)/$LEGACY_DAEMON_LABEL" >/dev/null 2>&1 || true
|
|
576
|
+
launchctl bootout "gui/$(id -u)" "$LEGACY_USER_PLIST" >/dev/null 2>&1 || true
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
start_user_agent() {
|
|
580
|
+
mkdir -p "$HOME/Library/LaunchAgents"
|
|
581
|
+
cp "$PLIST_BUILD_DIR/$PAIRLING_DAEMON_LABEL.plist" "$USER_PLIST"
|
|
582
|
+
chmod 644 "$USER_PLIST"
|
|
583
|
+
if is_dry_run; then
|
|
584
|
+
log "dry-run: rendered $USER_PLIST"
|
|
585
|
+
return
|
|
586
|
+
fi
|
|
587
|
+
launchctl bootout "gui/$(id -u)" "$USER_PLIST" >/dev/null 2>&1 || true
|
|
588
|
+
launchctl bootstrap "gui/$(id -u)" "$USER_PLIST" >/dev/null 2>&1 || true
|
|
589
|
+
launchctl kickstart -k "gui/$(id -u)/$PAIRLING_DAEMON_LABEL"
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
start_connectd_agent() {
|
|
593
|
+
mkdir -p "$HOME/Library/LaunchAgents"
|
|
594
|
+
cp "$PLIST_BUILD_DIR/$PAIRLING_CONNECTD_LABEL.plist" "$CONNECTD_USER_PLIST"
|
|
595
|
+
chmod 644 "$CONNECTD_USER_PLIST"
|
|
596
|
+
if is_dry_run; then
|
|
597
|
+
log "dry-run: rendered $CONNECTD_USER_PLIST"
|
|
598
|
+
return
|
|
599
|
+
fi
|
|
600
|
+
launchctl bootout "gui/$(id -u)" "$CONNECTD_USER_PLIST" >/dev/null 2>&1 || true
|
|
601
|
+
launchctl bootstrap "gui/$(id -u)" "$CONNECTD_USER_PLIST" >/dev/null 2>&1 || true
|
|
602
|
+
launchctl kickstart -k "gui/$(id -u)/$PAIRLING_CONNECTD_LABEL"
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
stop_user_agent() {
|
|
606
|
+
if is_dry_run; then
|
|
607
|
+
log "dry-run: would stop $PAIRLING_DAEMON_LABEL"
|
|
608
|
+
return
|
|
609
|
+
fi
|
|
610
|
+
launchctl bootout "gui/$(id -u)/$PAIRLING_DAEMON_LABEL" >/dev/null 2>&1 || true
|
|
611
|
+
launchctl bootout "gui/$(id -u)" "$USER_PLIST" >/dev/null 2>&1 || true
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
stop_connectd_agent() {
|
|
615
|
+
if is_dry_run; then
|
|
616
|
+
log "dry-run: would stop $PAIRLING_CONNECTD_LABEL"
|
|
617
|
+
return
|
|
618
|
+
fi
|
|
619
|
+
launchctl bootout "gui/$(id -u)/$PAIRLING_CONNECTD_LABEL" >/dev/null 2>&1 || true
|
|
620
|
+
launchctl bootout "gui/$(id -u)" "$CONNECTD_USER_PLIST" >/dev/null 2>&1 || true
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
install_guardian_if_possible() {
|
|
624
|
+
local rendered="$PLIST_BUILD_DIR/$PAIRLING_GUARDIAN_LABEL.plist"
|
|
625
|
+
if [[ "${PAIRLING_INSTALL_GUARDIAN:-0}" != "1" ]]; then
|
|
626
|
+
log "Guardian LaunchDaemon rendered but not installed. Set PAIRLING_INSTALL_GUARDIAN=1 to install it."
|
|
627
|
+
return
|
|
628
|
+
fi
|
|
629
|
+
if is_dry_run; then
|
|
630
|
+
log "dry-run: would install $PAIRLING_GUARDIAN_LABEL"
|
|
631
|
+
return
|
|
632
|
+
fi
|
|
633
|
+
if sudo -n true >/dev/null 2>&1; then
|
|
634
|
+
sudo cp "$rendered" "$SYSTEM_PLIST"
|
|
635
|
+
sudo chown root:wheel "$SYSTEM_PLIST"
|
|
636
|
+
sudo chmod 644 "$SYSTEM_PLIST"
|
|
637
|
+
sudo launchctl bootout system "$SYSTEM_PLIST" >/dev/null 2>&1 || true
|
|
638
|
+
sudo launchctl bootstrap system "$SYSTEM_PLIST" >/dev/null 2>&1 || true
|
|
639
|
+
sudo launchctl kickstart -k "system/$PAIRLING_GUARDIAN_LABEL"
|
|
640
|
+
else
|
|
641
|
+
log "Skipping guardian install: passwordless sudo is unavailable. Re-run with privileges when ready."
|
|
642
|
+
fi
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
run_doctor() {
|
|
646
|
+
"$REPO_ROOT/mac/install/doctor.sh" --json
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
rollback() {
|
|
650
|
+
if [[ ! -L "$PREVIOUS_LINK" ]]; then
|
|
651
|
+
log "ERROR: no previous runtime symlink exists at $PREVIOUS_LINK" >&2
|
|
652
|
+
exit 1
|
|
653
|
+
fi
|
|
654
|
+
local current_target previous_target
|
|
655
|
+
current_target="$(readlink "$CURRENT_LINK" 2>/dev/null || true)"
|
|
656
|
+
previous_target="$(readlink "$PREVIOUS_LINK")"
|
|
657
|
+
rm -f "$CURRENT_LINK"
|
|
658
|
+
ln -s "$previous_target" "$CURRENT_LINK"
|
|
659
|
+
rm -f "$PREVIOUS_LINK"
|
|
660
|
+
if [[ -n "$current_target" ]]; then
|
|
661
|
+
ln -s "$current_target" "$PREVIOUS_LINK"
|
|
662
|
+
fi
|
|
663
|
+
render_plists
|
|
664
|
+
start_user_agent
|
|
665
|
+
start_connectd_agent
|
|
666
|
+
append_history "rollback" "rolled back to $previous_target"
|
|
667
|
+
run_doctor
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
install_runtime() {
|
|
671
|
+
log "Pairling setup preview:"
|
|
672
|
+
log " app support: $APP_SUPPORT"
|
|
673
|
+
log " logs: $LOGS_ROOT"
|
|
674
|
+
log " LaunchAgent: $PAIRLING_DAEMON_LABEL"
|
|
675
|
+
log " Connect LaunchAgent: $PAIRLING_CONNECTD_LABEL"
|
|
676
|
+
log " runtime port: $PAIRLING_RUNTIME_PORT"
|
|
677
|
+
log " old Pairling predecessor cleanup label: $LEGACY_DAEMON_LABEL"
|
|
678
|
+
run_compile_checks
|
|
679
|
+
ensure_state
|
|
680
|
+
copy_release
|
|
681
|
+
switch_current
|
|
682
|
+
install_mcp_adapter_shim
|
|
683
|
+
install_shell_wrapper
|
|
684
|
+
render_plists
|
|
685
|
+
unload_legacy_daemon
|
|
686
|
+
start_user_agent
|
|
687
|
+
start_connectd_agent
|
|
688
|
+
install_guardian_if_possible
|
|
689
|
+
append_history "installed" "installed $RELEASE_NAME"
|
|
690
|
+
if is_dry_run; then
|
|
691
|
+
log "dry-run: skipping doctor gate"
|
|
692
|
+
else
|
|
693
|
+
run_doctor || true
|
|
694
|
+
fi
|
|
695
|
+
log "Installed Pairling runtime $RELEASE_NAME"
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
status_runtime() {
|
|
699
|
+
"$REPO_ROOT/mac/install/doctor.sh" --json || true
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
start_runtime() {
|
|
703
|
+
ensure_state
|
|
704
|
+
render_plists
|
|
705
|
+
unload_legacy_daemon
|
|
706
|
+
start_user_agent
|
|
707
|
+
start_connectd_agent
|
|
708
|
+
log "Started $PAIRLING_DAEMON_LABEL"
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
stop_runtime() {
|
|
712
|
+
stop_connectd_agent
|
|
713
|
+
stop_user_agent
|
|
714
|
+
log "Stopped $PAIRLING_DAEMON_LABEL"
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
pair_runtime() {
|
|
718
|
+
local ttl="180"
|
|
719
|
+
local show_qr="0"
|
|
720
|
+
local json_requested="0"
|
|
721
|
+
while [[ $# -gt 0 ]]; do
|
|
722
|
+
case "$1" in
|
|
723
|
+
--json)
|
|
724
|
+
json_requested="1"
|
|
725
|
+
;;
|
|
726
|
+
--qr)
|
|
727
|
+
show_qr="1"
|
|
728
|
+
;;
|
|
729
|
+
--ttl)
|
|
730
|
+
shift
|
|
731
|
+
ttl="${1:-}"
|
|
732
|
+
if [[ -z "$ttl" ]]; then
|
|
733
|
+
log "usage: pairling pair [--ttl seconds] [--json] [--qr]" >&2
|
|
734
|
+
exit 2
|
|
735
|
+
fi
|
|
736
|
+
;;
|
|
737
|
+
--help|-h)
|
|
738
|
+
log "usage: pairling pair [--ttl seconds] [--json] [--qr]"
|
|
739
|
+
return
|
|
740
|
+
;;
|
|
741
|
+
*)
|
|
742
|
+
log "usage: pairling pair [--ttl seconds] [--json] [--qr]" >&2
|
|
743
|
+
exit 2
|
|
744
|
+
;;
|
|
745
|
+
esac
|
|
746
|
+
shift
|
|
747
|
+
done
|
|
748
|
+
local payload_file
|
|
749
|
+
payload_file="$(mktemp)"
|
|
750
|
+
if python3 - "$PAIRLING_RUNTIME_PORT" "$ttl" "$REPO_ROOT" >"$payload_file" <<'PY'
|
|
751
|
+
import json
|
|
752
|
+
import os
|
|
753
|
+
import socket
|
|
754
|
+
import subprocess
|
|
755
|
+
import sys
|
|
756
|
+
import urllib.parse
|
|
757
|
+
import urllib.error
|
|
758
|
+
import urllib.request
|
|
759
|
+
|
|
760
|
+
port, ttl_raw, repo_root = sys.argv[1:]
|
|
761
|
+
sys.path.insert(0, os.path.join(repo_root, "mac", "companiond"))
|
|
762
|
+
from pairling_connectd_status import advertised_pairling_connect_routes, fetch_connectd_status
|
|
763
|
+
|
|
764
|
+
try:
|
|
765
|
+
ttl = int(ttl_raw)
|
|
766
|
+
except ValueError:
|
|
767
|
+
print(json.dumps({
|
|
768
|
+
"ok": False,
|
|
769
|
+
"error": {"code": "invalid_ttl", "message": "ttl must be an integer"},
|
|
770
|
+
}, indent=2, sort_keys=True), file=sys.stderr)
|
|
771
|
+
raise SystemExit(2)
|
|
772
|
+
|
|
773
|
+
url = f"http://127.0.0.1:{int(port)}/pair/start"
|
|
774
|
+
body = json.dumps({"ttl_seconds": ttl}).encode("utf-8")
|
|
775
|
+
request = urllib.request.Request(
|
|
776
|
+
url,
|
|
777
|
+
data=body,
|
|
778
|
+
method="POST",
|
|
779
|
+
headers={"Content-Type": "application/json"},
|
|
780
|
+
)
|
|
781
|
+
try:
|
|
782
|
+
with urllib.request.urlopen(request, timeout=5) as response:
|
|
783
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
784
|
+
except urllib.error.HTTPError as exc:
|
|
785
|
+
try:
|
|
786
|
+
payload = json.loads(exc.read().decode("utf-8"))
|
|
787
|
+
except Exception:
|
|
788
|
+
payload = {
|
|
789
|
+
"ok": False,
|
|
790
|
+
"error": {"code": "http_error", "message": str(exc)},
|
|
791
|
+
}
|
|
792
|
+
print(json.dumps(payload, indent=2, sort_keys=True), file=sys.stderr)
|
|
793
|
+
raise SystemExit(1)
|
|
794
|
+
except Exception as exc:
|
|
795
|
+
print(json.dumps({
|
|
796
|
+
"ok": False,
|
|
797
|
+
"error": {
|
|
798
|
+
"code": "runtime_unreachable",
|
|
799
|
+
"message": f"Pairling runtime is not reachable at {url}: {type(exc).__name__}: {exc}",
|
|
800
|
+
},
|
|
801
|
+
"repair": "Run `pairling start` or `pairling doctor --json`, then retry `pairling pair`.",
|
|
802
|
+
}, indent=2, sort_keys=True), file=sys.stderr)
|
|
803
|
+
raise SystemExit(1)
|
|
804
|
+
|
|
805
|
+
pair_id = str(payload.get("pair_id") or (payload.get("claim") or {}).get("pair_id") or "")
|
|
806
|
+
secret = str(
|
|
807
|
+
payload.get("secret")
|
|
808
|
+
or payload.get("secret_qr")
|
|
809
|
+
or (payload.get("claim") or {}).get("secret")
|
|
810
|
+
or ""
|
|
811
|
+
)
|
|
812
|
+
install_id = str(payload.get("install_id") or "")
|
|
813
|
+
mac_name = str(((payload.get("pair_service") or {}).get("txt") or {}).get("mac_name") or socket.gethostname())
|
|
814
|
+
|
|
815
|
+
def detected_tailnet_ip() -> str:
|
|
816
|
+
override = os.environ.get("PAIRLING_TEST_TAILSCALE_IP")
|
|
817
|
+
if override is not None:
|
|
818
|
+
value = override.strip()
|
|
819
|
+
return value if value.startswith("100.") else ""
|
|
820
|
+
try:
|
|
821
|
+
proc = subprocess.run(["tailscale", "ip", "-4"], capture_output=True, text=True, timeout=3)
|
|
822
|
+
except Exception:
|
|
823
|
+
return ""
|
|
824
|
+
if proc.returncode != 0:
|
|
825
|
+
return ""
|
|
826
|
+
for line in (proc.stdout or "").splitlines():
|
|
827
|
+
ip = line.strip()
|
|
828
|
+
if ip.startswith("100."):
|
|
829
|
+
return ip
|
|
830
|
+
return ""
|
|
831
|
+
|
|
832
|
+
def default_pair_route(port_number: int) -> dict:
|
|
833
|
+
for key in ("PAIRLING_PAIR_BASE_URL", "PAIRLING_PUBLIC_BASE_URL"):
|
|
834
|
+
value = os.environ.get(key)
|
|
835
|
+
if value:
|
|
836
|
+
return {"base_url": value, "source": "explicit_override", "status": "override"}
|
|
837
|
+
connect_routes = advertised_pairling_connect_routes(fetch_connectd_status(timeout_seconds=0.7))
|
|
838
|
+
if connect_routes:
|
|
839
|
+
route = connect_routes[0]
|
|
840
|
+
return {
|
|
841
|
+
"base_url": route["base_url"],
|
|
842
|
+
"source": route["source"],
|
|
843
|
+
"status": route["status"],
|
|
844
|
+
"kind": route["kind"],
|
|
845
|
+
}
|
|
846
|
+
tailnet_ip = detected_tailnet_ip()
|
|
847
|
+
if tailnet_ip:
|
|
848
|
+
return {"base_url": f"http://{tailnet_ip}:{port_number}", "source": "standalone_tailnet", "status": "fallback"}
|
|
849
|
+
try:
|
|
850
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
851
|
+
try:
|
|
852
|
+
sock.connect(("8.8.8.8", 80))
|
|
853
|
+
ip = sock.getsockname()[0]
|
|
854
|
+
finally:
|
|
855
|
+
sock.close()
|
|
856
|
+
if ip and not ip.startswith(("127.", "169.254.")):
|
|
857
|
+
return {"base_url": f"http://{ip}:{port_number}", "source": "lan", "status": "fallback"}
|
|
858
|
+
except Exception:
|
|
859
|
+
pass
|
|
860
|
+
return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback"}
|
|
861
|
+
|
|
862
|
+
pair_route = default_pair_route(int(port))
|
|
863
|
+
base_url = str(pair_route.get("base_url") or "")
|
|
864
|
+
if pair_id and secret:
|
|
865
|
+
pair_params = {
|
|
866
|
+
"base": base_url,
|
|
867
|
+
"pair_id": pair_id,
|
|
868
|
+
"secret": secret,
|
|
869
|
+
}
|
|
870
|
+
if pair_route.get("source") == "pairling_connectd" and pair_route.get("status") == "ready":
|
|
871
|
+
pair_params["route_source"] = "pairling_connectd"
|
|
872
|
+
pair_params["route_status"] = "ready"
|
|
873
|
+
pair_params["route_kind"] = str(pair_route.get("kind") or "tailnet")
|
|
874
|
+
pair_params["route_contract"] = "pairling-runtime-v1"
|
|
875
|
+
manual = {
|
|
876
|
+
"base_url": base_url,
|
|
877
|
+
"pair_id": pair_id,
|
|
878
|
+
"secret": secret,
|
|
879
|
+
}
|
|
880
|
+
if install_id:
|
|
881
|
+
pair_params["install_id"] = install_id
|
|
882
|
+
pair_params["mac_name"] = mac_name
|
|
883
|
+
manual["install_id"] = install_id
|
|
884
|
+
manual["mac_name"] = mac_name
|
|
885
|
+
payload.setdefault("pair_url", "pairling://pair?" + urllib.parse.urlencode(pair_params))
|
|
886
|
+
payload.setdefault("manual", manual)
|
|
887
|
+
|
|
888
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
889
|
+
raise SystemExit(0 if payload.get("ok") else 1)
|
|
890
|
+
PY
|
|
891
|
+
then
|
|
892
|
+
:
|
|
893
|
+
else
|
|
894
|
+
local code=$?
|
|
895
|
+
cat "$payload_file" >&2
|
|
896
|
+
rm -f "$payload_file"
|
|
897
|
+
exit "$code"
|
|
898
|
+
fi
|
|
899
|
+
|
|
900
|
+
if [[ "$show_qr" == "0" ]]; then
|
|
901
|
+
cat "$payload_file"
|
|
902
|
+
rm -f "$payload_file"
|
|
903
|
+
return
|
|
904
|
+
fi
|
|
905
|
+
|
|
906
|
+
local pair_url
|
|
907
|
+
pair_url="$(python3 - "$payload_file" <<'PY'
|
|
908
|
+
import json
|
|
909
|
+
import sys
|
|
910
|
+
payload = json.load(open(sys.argv[1]))
|
|
911
|
+
print(payload.get("pair_url", ""))
|
|
912
|
+
PY
|
|
913
|
+
)"
|
|
914
|
+
|
|
915
|
+
python3 - "$payload_file" <<'PY'
|
|
916
|
+
import json
|
|
917
|
+
import sys
|
|
918
|
+
payload = json.load(open(sys.argv[1]))
|
|
919
|
+
manual = payload.get("manual") or {}
|
|
920
|
+
print("Pairling pairing invitation ready")
|
|
921
|
+
print("")
|
|
922
|
+
print("Scan this QR in Pairling, or paste the pair URL below.")
|
|
923
|
+
print("")
|
|
924
|
+
if payload.get("pair_url"):
|
|
925
|
+
print("Pair URL:")
|
|
926
|
+
print(payload["pair_url"])
|
|
927
|
+
print("")
|
|
928
|
+
if manual:
|
|
929
|
+
print("Manual values:")
|
|
930
|
+
print(" base_url:", manual.get("base_url", ""))
|
|
931
|
+
print(" pair_id:", manual.get("pair_id", ""))
|
|
932
|
+
print(" secret:", manual.get("secret", ""))
|
|
933
|
+
print("")
|
|
934
|
+
PY
|
|
935
|
+
if [[ -n "$pair_url" ]]; then
|
|
936
|
+
if ! render_pair_qr "$pair_url"; then
|
|
937
|
+
log "QR rendering unavailable because Swift/CoreImage is not available. Use the pair URL above."
|
|
938
|
+
fi
|
|
939
|
+
fi
|
|
940
|
+
if [[ "$json_requested" == "1" ]]; then
|
|
941
|
+
log ""
|
|
942
|
+
log "JSON:"
|
|
943
|
+
cat "$payload_file"
|
|
944
|
+
fi
|
|
945
|
+
rm -f "$payload_file"
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
devices_runtime() {
|
|
949
|
+
python3 - "$DEVICES_DB" <<'PY'
|
|
950
|
+
import json
|
|
951
|
+
import sqlite3
|
|
952
|
+
import sys
|
|
953
|
+
path = sys.argv[1]
|
|
954
|
+
try:
|
|
955
|
+
with sqlite3.connect(path) as db:
|
|
956
|
+
rows = db.execute("SELECT device_id, device_name, scopes_json, created_at, last_seen_at, revoked_at FROM devices ORDER BY created_at").fetchall()
|
|
957
|
+
except Exception as exc:
|
|
958
|
+
print(json.dumps({"ok": False, "error": str(exc)}))
|
|
959
|
+
raise SystemExit(1)
|
|
960
|
+
print(json.dumps({
|
|
961
|
+
"ok": True,
|
|
962
|
+
"devices": [
|
|
963
|
+
{
|
|
964
|
+
"device_id": row[0],
|
|
965
|
+
"device_name": row[1],
|
|
966
|
+
"scopes": json.loads(row[2]),
|
|
967
|
+
"created_at": row[3],
|
|
968
|
+
"last_seen_at": row[4],
|
|
969
|
+
"revoked_at": row[5],
|
|
970
|
+
}
|
|
971
|
+
for row in rows
|
|
972
|
+
],
|
|
973
|
+
}, indent=2, sort_keys=True))
|
|
974
|
+
PY
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
unpair_runtime() {
|
|
978
|
+
local device_id="${1:-}"
|
|
979
|
+
if [[ -z "$device_id" ]]; then
|
|
980
|
+
log "usage: pairling unpair <device_id>" >&2
|
|
981
|
+
exit 2
|
|
982
|
+
fi
|
|
983
|
+
python3 - "$REPO_ROOT" "$DEVICES_DB" "$LOGS_ROOT/audit.jsonl" "$device_id" <<'PY'
|
|
984
|
+
import json
|
|
985
|
+
import sys
|
|
986
|
+
from pathlib import Path
|
|
987
|
+
|
|
988
|
+
repo_root, db_path, audit_path, device_id = sys.argv[1:]
|
|
989
|
+
sys.path.insert(0, str(Path(repo_root) / "mac" / "companiond"))
|
|
990
|
+
from pairling_devices import DeviceRegistry
|
|
991
|
+
|
|
992
|
+
registry = DeviceRegistry(Path(db_path), Path(audit_path))
|
|
993
|
+
ok = registry.revoke_device(device_id, reason="cli")
|
|
994
|
+
payload = {"ok": ok, "device_id": device_id}
|
|
995
|
+
if not ok:
|
|
996
|
+
payload["error"] = {"code": "device_not_found", "message": "device was not found or is already revoked"}
|
|
997
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
998
|
+
raise SystemExit(0 if ok else 1)
|
|
999
|
+
PY
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
rotate_runtime() {
|
|
1003
|
+
local device_id="${1:-}"
|
|
1004
|
+
if [[ -z "$device_id" ]]; then
|
|
1005
|
+
log "usage: pairling rotate-token <device_id>" >&2
|
|
1006
|
+
exit 2
|
|
1007
|
+
fi
|
|
1008
|
+
python3 - "$REPO_ROOT" "$DEVICES_DB" "$LOGS_ROOT/audit.jsonl" "$device_id" <<'PY'
|
|
1009
|
+
import json
|
|
1010
|
+
import sys
|
|
1011
|
+
from pathlib import Path
|
|
1012
|
+
|
|
1013
|
+
repo_root, db_path, audit_path, device_id = sys.argv[1:]
|
|
1014
|
+
sys.path.insert(0, str(Path(repo_root) / "mac" / "companiond"))
|
|
1015
|
+
from pairling_devices import DeviceRegistry
|
|
1016
|
+
|
|
1017
|
+
registry = DeviceRegistry(Path(db_path), Path(audit_path))
|
|
1018
|
+
token = registry.rotate_token(device_id)
|
|
1019
|
+
payload = {"ok": token is not None, "device_id": device_id}
|
|
1020
|
+
if token is None:
|
|
1021
|
+
payload["error"] = {"code": "device_not_found", "message": "device was not found"}
|
|
1022
|
+
else:
|
|
1023
|
+
payload["token"] = token
|
|
1024
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1025
|
+
raise SystemExit(0 if token is not None else 1)
|
|
1026
|
+
PY
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
logs_runtime() {
|
|
1030
|
+
log "$LOGS_ROOT"
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
connect_auth_open() {
|
|
1034
|
+
local json_mode="false"
|
|
1035
|
+
while [[ $# -gt 0 ]]; do
|
|
1036
|
+
case "$1" in
|
|
1037
|
+
--json)
|
|
1038
|
+
json_mode="true"
|
|
1039
|
+
;;
|
|
1040
|
+
--help|-h)
|
|
1041
|
+
log "usage: pairling connect-auth-open [--json]"
|
|
1042
|
+
return
|
|
1043
|
+
;;
|
|
1044
|
+
*)
|
|
1045
|
+
log "usage: pairling connect-auth-open [--json]" >&2
|
|
1046
|
+
exit 2
|
|
1047
|
+
;;
|
|
1048
|
+
esac
|
|
1049
|
+
shift
|
|
1050
|
+
done
|
|
1051
|
+
local output
|
|
1052
|
+
if output="$(/usr/bin/curl -sS --max-time 5 -X POST http://127.0.0.1:7774/auth/open 2>/dev/null)"; then
|
|
1053
|
+
local response_status
|
|
1054
|
+
if python3 -c 'import json,sys; sys.exit(0 if json.load(sys.stdin).get("ok") else 1)' <<<"$output"; then
|
|
1055
|
+
response_status=0
|
|
1056
|
+
else
|
|
1057
|
+
response_status=1
|
|
1058
|
+
fi
|
|
1059
|
+
if [[ "$json_mode" == "true" ]]; then
|
|
1060
|
+
printf '%s\n' "$output"
|
|
1061
|
+
else
|
|
1062
|
+
python3 -c 'import json,sys; data=json.load(sys.stdin); print("Pairling Connect browser approval opened." if data.get("opened") else data.get("error", "Pairling Connect browser approval is not available."))' <<<"$output"
|
|
1063
|
+
fi
|
|
1064
|
+
exit "$response_status"
|
|
1065
|
+
fi
|
|
1066
|
+
if [[ "$json_mode" == "true" ]]; then
|
|
1067
|
+
printf '{"ok":false,"opened":false,"auth_url_present":false,"error":"Pairling Connect auth endpoint unavailable."}\n'
|
|
1068
|
+
else
|
|
1069
|
+
printf 'Pairling Connect auth endpoint unavailable.\n' >&2
|
|
1070
|
+
fi
|
|
1071
|
+
exit 1
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
render_pair_qr() {
|
|
1075
|
+
local pair_url="$1"
|
|
1076
|
+
if ! command -v swift >/dev/null 2>&1; then
|
|
1077
|
+
return 1
|
|
1078
|
+
fi
|
|
1079
|
+
swift - "$pair_url" <<'SWIFT'
|
|
1080
|
+
import CoreGraphics
|
|
1081
|
+
import CoreImage
|
|
1082
|
+
import Foundation
|
|
1083
|
+
|
|
1084
|
+
guard CommandLine.arguments.count > 1,
|
|
1085
|
+
let message = CommandLine.arguments[1].data(using: .utf8),
|
|
1086
|
+
let filter = CIFilter(name: "CIQRCodeGenerator") else {
|
|
1087
|
+
exit(2)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
filter.setValue(message, forKey: "inputMessage")
|
|
1091
|
+
filter.setValue("M", forKey: "inputCorrectionLevel")
|
|
1092
|
+
|
|
1093
|
+
guard let output = filter.outputImage else {
|
|
1094
|
+
exit(2)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
let extent = output.extent.integral
|
|
1098
|
+
let ciContext = CIContext(options: nil)
|
|
1099
|
+
guard let cgImage = ciContext.createCGImage(output, from: extent) else {
|
|
1100
|
+
exit(2)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
let width = cgImage.width
|
|
1104
|
+
let height = cgImage.height
|
|
1105
|
+
let bytesPerRow = width * 4
|
|
1106
|
+
var raw = [UInt8](repeating: 255, count: height * bytesPerRow)
|
|
1107
|
+
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
1108
|
+
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
|
|
1109
|
+
|
|
1110
|
+
guard let bitmapContext = CGContext(
|
|
1111
|
+
data: &raw,
|
|
1112
|
+
width: width,
|
|
1113
|
+
height: height,
|
|
1114
|
+
bitsPerComponent: 8,
|
|
1115
|
+
bytesPerRow: bytesPerRow,
|
|
1116
|
+
space: colorSpace,
|
|
1117
|
+
bitmapInfo: bitmapInfo
|
|
1118
|
+
) else {
|
|
1119
|
+
exit(2)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
bitmapContext.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 1))
|
|
1123
|
+
bitmapContext.fill(CGRect(x: 0, y: 0, width: width, height: height))
|
|
1124
|
+
bitmapContext.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
1125
|
+
|
|
1126
|
+
func isDark(_ x: Int, _ y: Int) -> Bool {
|
|
1127
|
+
let index = y * bytesPerRow + x * 4
|
|
1128
|
+
return raw[index] < 128 && raw[index + 1] < 128 && raw[index + 2] < 128
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
let quietZone = 4
|
|
1132
|
+
let reset = "\u{001B}[0m"
|
|
1133
|
+
let black = "\u{001B}[40m "
|
|
1134
|
+
let white = "\u{001B}[47m "
|
|
1135
|
+
|
|
1136
|
+
for y in (-quietZone)..<(height + quietZone) {
|
|
1137
|
+
var line = ""
|
|
1138
|
+
for x in (-quietZone)..<(width + quietZone) {
|
|
1139
|
+
let dark = x >= 0 && y >= 0 && x < width && y < height && isDark(x, y)
|
|
1140
|
+
line += dark ? black : white
|
|
1141
|
+
}
|
|
1142
|
+
print(line + reset)
|
|
1143
|
+
}
|
|
1144
|
+
SWIFT
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
diagnose_runtime() {
|
|
1148
|
+
"$REPO_ROOT/mac/install/doctor.sh" --json | python3 -c 'import json,sys; data=json.load(sys.stdin); print(json.dumps(data, indent=2, sort_keys=True))' || true
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
usage() {
|
|
1152
|
+
cat <<EOF
|
|
1153
|
+
usage: pairling <command>
|
|
1154
|
+
|
|
1155
|
+
commands:
|
|
1156
|
+
setup|install
|
|
1157
|
+
setup --first-run
|
|
1158
|
+
first-run
|
|
1159
|
+
start
|
|
1160
|
+
stop
|
|
1161
|
+
restart
|
|
1162
|
+
status
|
|
1163
|
+
doctor --json
|
|
1164
|
+
doctor --first-run --json
|
|
1165
|
+
pair
|
|
1166
|
+
connect-auth-open
|
|
1167
|
+
devices
|
|
1168
|
+
unpair <device_id>
|
|
1169
|
+
rotate-token <device_id>
|
|
1170
|
+
logs
|
|
1171
|
+
diagnose --redact
|
|
1172
|
+
uninstall
|
|
1173
|
+
rollback
|
|
1174
|
+
EOF
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
cmd="${1:-setup}"
|
|
1178
|
+
shift || true
|
|
1179
|
+
case "$cmd" in
|
|
1180
|
+
setup|install)
|
|
1181
|
+
if [[ "${1:-}" == "--first-run" ]]; then
|
|
1182
|
+
shift
|
|
1183
|
+
"$REPO_ROOT/mac/install/bootstrap-first-run.sh" "$@"
|
|
1184
|
+
else
|
|
1185
|
+
install_runtime
|
|
1186
|
+
fi
|
|
1187
|
+
;;
|
|
1188
|
+
first-run)
|
|
1189
|
+
"$REPO_ROOT/mac/install/bootstrap-first-run.sh" "$@"
|
|
1190
|
+
;;
|
|
1191
|
+
start)
|
|
1192
|
+
start_runtime
|
|
1193
|
+
;;
|
|
1194
|
+
stop)
|
|
1195
|
+
stop_runtime
|
|
1196
|
+
;;
|
|
1197
|
+
restart)
|
|
1198
|
+
stop_runtime
|
|
1199
|
+
start_runtime
|
|
1200
|
+
;;
|
|
1201
|
+
status)
|
|
1202
|
+
status_runtime
|
|
1203
|
+
;;
|
|
1204
|
+
doctor)
|
|
1205
|
+
"$REPO_ROOT/mac/install/doctor.sh" "$@"
|
|
1206
|
+
;;
|
|
1207
|
+
pair)
|
|
1208
|
+
pair_runtime "$@"
|
|
1209
|
+
;;
|
|
1210
|
+
devices)
|
|
1211
|
+
devices_runtime
|
|
1212
|
+
;;
|
|
1213
|
+
unpair)
|
|
1214
|
+
unpair_runtime "$@"
|
|
1215
|
+
;;
|
|
1216
|
+
rotate-token)
|
|
1217
|
+
rotate_runtime "$@"
|
|
1218
|
+
;;
|
|
1219
|
+
logs)
|
|
1220
|
+
logs_runtime
|
|
1221
|
+
;;
|
|
1222
|
+
connect-auth-open)
|
|
1223
|
+
connect_auth_open "$@"
|
|
1224
|
+
;;
|
|
1225
|
+
diagnose)
|
|
1226
|
+
diagnose_runtime "$@"
|
|
1227
|
+
;;
|
|
1228
|
+
uninstall)
|
|
1229
|
+
"$REPO_ROOT/mac/install/uninstall-runtime.sh" "$@"
|
|
1230
|
+
;;
|
|
1231
|
+
rollback|--rollback)
|
|
1232
|
+
rollback
|
|
1233
|
+
;;
|
|
1234
|
+
help|--help|-h)
|
|
1235
|
+
usage
|
|
1236
|
+
;;
|
|
1237
|
+
*)
|
|
1238
|
+
usage >&2
|
|
1239
|
+
exit 2
|
|
1240
|
+
;;
|
|
1241
|
+
esac
|