@trillboards/edge-sdk 0.2.4 → 0.2.6

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.
Files changed (200) hide show
  1. package/README.md +4 -3
  2. package/deploy/docker/Dockerfile.cloud-sense +117 -0
  3. package/deploy/docker/Dockerfile.cpu +30 -21
  4. package/deploy/docker/Dockerfile.cuda +30 -21
  5. package/deploy/docker/Dockerfile.openvino +30 -20
  6. package/deploy/docker/README.md +9 -4
  7. package/deploy/docker/connector-entrypoint.sh +122 -0
  8. package/deploy/helm/README.md +1 -1
  9. package/deploy/k8s/daemonset.yaml +1 -1
  10. package/deploy/macos/README.md +161 -0
  11. package/deploy/macos/TrillboardsSetupApp.m +603 -0
  12. package/deploy/macos/build-macos-pkg.sh +588 -0
  13. package/deploy/macos/com.trillboards.edge.plist +62 -0
  14. package/deploy/macos/com.trillboards.litertlm.plist +45 -0
  15. package/deploy/macos/pair.command +519 -0
  16. package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
  17. package/deploy/macos/scripts/postinstall +153 -0
  18. package/deploy/macos/scripts/setup-litertlm.sh +272 -0
  19. package/deploy/macos/test-install-pair.sh +383 -0
  20. package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
  21. package/deploy/systemd/install.sh +25 -15
  22. package/deploy/systemd/trillboards-edge.service +4 -1
  23. package/deploy/windows/README.md +70 -0
  24. package/deploy/windows/build-windows.ps1 +225 -0
  25. package/deploy/windows/bundle-windows-installer.ps1 +153 -0
  26. package/deploy/windows/check-feeds.js +26 -0
  27. package/deploy/windows/e2e/fixtures/person.jpg +0 -0
  28. package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
  29. package/deploy/windows/install-update.ps1 +101 -0
  30. package/deploy/windows/installer.nsi +138 -0
  31. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  32. package/deploy/windows/register-task.ps1 +44 -0
  33. package/deploy/windows/run-connector.ps1 +69 -0
  34. package/deploy/windows/setup-litertlm.ps1 +361 -0
  35. package/deploy/windows/stop-existing.ps1 +72 -0
  36. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  37. package/deploy/windows/trillboards-edge.cmd +33 -0
  38. package/deploy/windows/trillboards-pair.cmd +276 -0
  39. package/deploy/windows/trillboards-setup.ps1 +456 -0
  40. package/deploy/windows/trillboards-setup.vbs +13 -0
  41. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  42. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  43. package/dist/AudienceAnalyzeClient.js +246 -0
  44. package/dist/AudienceAnalyzeClient.js.map +1 -0
  45. package/dist/ChannelProber.d.ts +150 -0
  46. package/dist/ChannelProber.d.ts.map +1 -0
  47. package/dist/ChannelProber.js +887 -0
  48. package/dist/ChannelProber.js.map +1 -0
  49. package/dist/CloudEscalationPolicy.d.ts +143 -0
  50. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  51. package/dist/CloudEscalationPolicy.js +137 -0
  52. package/dist/CloudEscalationPolicy.js.map +1 -0
  53. package/dist/CloudSensingManager.d.ts +533 -0
  54. package/dist/CloudSensingManager.d.ts.map +1 -0
  55. package/dist/CloudSensingManager.js +1254 -0
  56. package/dist/CloudSensingManager.js.map +1 -0
  57. package/dist/DiscoveryReportClient.d.ts +66 -0
  58. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  59. package/dist/DiscoveryReportClient.js +85 -0
  60. package/dist/DiscoveryReportClient.js.map +1 -0
  61. package/dist/EdgeAgent.d.ts +12 -2
  62. package/dist/EdgeAgent.d.ts.map +1 -1
  63. package/dist/EdgeAgent.js +55 -13
  64. package/dist/EdgeAgent.js.map +1 -1
  65. package/dist/LiteRtVlmProvider.d.ts +193 -0
  66. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  67. package/dist/LiteRtVlmProvider.js +436 -0
  68. package/dist/LiteRtVlmProvider.js.map +1 -0
  69. package/dist/MosaicSource.d.ts +108 -0
  70. package/dist/MosaicSource.d.ts.map +1 -0
  71. package/dist/MosaicSource.js +274 -0
  72. package/dist/MosaicSource.js.map +1 -0
  73. package/dist/OnvifClient.d.ts +83 -0
  74. package/dist/OnvifClient.d.ts.map +1 -0
  75. package/dist/OnvifClient.js +209 -0
  76. package/dist/OnvifClient.js.map +1 -0
  77. package/dist/PairClient.d.ts +106 -0
  78. package/dist/PairClient.d.ts.map +1 -0
  79. package/dist/PairClient.js +186 -0
  80. package/dist/PairClient.js.map +1 -0
  81. package/dist/RecorderAddressRecovery.d.ts +53 -0
  82. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  83. package/dist/RecorderAddressRecovery.js +359 -0
  84. package/dist/RecorderAddressRecovery.js.map +1 -0
  85. package/dist/SceneChangeDetector.d.ts +291 -0
  86. package/dist/SceneChangeDetector.d.ts.map +1 -0
  87. package/dist/SceneChangeDetector.js +682 -0
  88. package/dist/SceneChangeDetector.js.map +1 -0
  89. package/dist/SensingProfileClient.d.ts +59 -0
  90. package/dist/SensingProfileClient.d.ts.map +1 -0
  91. package/dist/SensingProfileClient.js +78 -0
  92. package/dist/SensingProfileClient.js.map +1 -0
  93. package/dist/SetupSessionClient.d.ts +114 -0
  94. package/dist/SetupSessionClient.d.ts.map +1 -0
  95. package/dist/SetupSessionClient.js +95 -0
  96. package/dist/SetupSessionClient.js.map +1 -0
  97. package/dist/VlmAdmissionGate.d.ts +126 -0
  98. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  99. package/dist/VlmAdmissionGate.js +166 -0
  100. package/dist/VlmAdmissionGate.js.map +1 -0
  101. package/dist/autoSense.d.ts +216 -0
  102. package/dist/autoSense.d.ts.map +1 -0
  103. package/dist/autoSense.js +521 -0
  104. package/dist/autoSense.js.map +1 -0
  105. package/dist/cli.d.ts +27 -1
  106. package/dist/cli.d.ts.map +1 -1
  107. package/dist/cli.js +511 -21
  108. package/dist/cli.js.map +1 -1
  109. package/dist/combinedSense.d.ts +128 -0
  110. package/dist/combinedSense.d.ts.map +1 -0
  111. package/dist/combinedSense.js +261 -0
  112. package/dist/combinedSense.js.map +1 -0
  113. package/dist/config.d.ts +351 -0
  114. package/dist/config.d.ts.map +1 -1
  115. package/dist/config.js +99 -2
  116. package/dist/config.js.map +1 -1
  117. package/dist/connectorCadence.d.ts +135 -0
  118. package/dist/connectorCadence.d.ts.map +1 -0
  119. package/dist/connectorCadence.js +151 -0
  120. package/dist/connectorCadence.js.map +1 -0
  121. package/dist/dataDir.d.ts +33 -0
  122. package/dist/dataDir.d.ts.map +1 -0
  123. package/dist/dataDir.js +76 -0
  124. package/dist/dataDir.js.map +1 -0
  125. package/dist/deployedCadence.d.ts +167 -0
  126. package/dist/deployedCadence.d.ts.map +1 -0
  127. package/dist/deployedCadence.js +209 -0
  128. package/dist/deployedCadence.js.map +1 -0
  129. package/dist/deployedCadenceSource.d.ts +48 -0
  130. package/dist/deployedCadenceSource.d.ts.map +1 -0
  131. package/dist/deployedCadenceSource.js +74 -0
  132. package/dist/deployedCadenceSource.js.map +1 -0
  133. package/dist/diagnose.d.ts +1 -1
  134. package/dist/diagnose.d.ts.map +1 -1
  135. package/dist/diagnose.js +13 -12
  136. package/dist/diagnose.js.map +1 -1
  137. package/dist/discovery.d.ts +131 -0
  138. package/dist/discovery.d.ts.map +1 -0
  139. package/dist/discovery.js +499 -0
  140. package/dist/discovery.js.map +1 -0
  141. package/dist/gemmaModelOta.d.ts +118 -0
  142. package/dist/gemmaModelOta.d.ts.map +1 -0
  143. package/dist/gemmaModelOta.js +291 -0
  144. package/dist/gemmaModelOta.js.map +1 -0
  145. package/dist/index.d.ts +16 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +40 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/init.d.ts.map +1 -1
  150. package/dist/init.js +41 -18
  151. package/dist/init.js.map +1 -1
  152. package/dist/keepAwake.d.ts +33 -0
  153. package/dist/keepAwake.d.ts.map +1 -0
  154. package/dist/keepAwake.js +54 -0
  155. package/dist/keepAwake.js.map +1 -0
  156. package/dist/legacyRecorderProfile.d.ts +20 -0
  157. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  158. package/dist/legacyRecorderProfile.js +27 -0
  159. package/dist/legacyRecorderProfile.js.map +1 -0
  160. package/dist/localInferenceSchema.d.ts +132 -0
  161. package/dist/localInferenceSchema.d.ts.map +1 -0
  162. package/dist/localInferenceSchema.js +274 -0
  163. package/dist/localInferenceSchema.js.map +1 -0
  164. package/dist/localSenseE2eHarness.d.ts +101 -0
  165. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  166. package/dist/localSenseE2eHarness.js +279 -0
  167. package/dist/localSenseE2eHarness.js.map +1 -0
  168. package/dist/localSensing.d.ts +268 -0
  169. package/dist/localSensing.d.ts.map +1 -0
  170. package/dist/localSensing.js +492 -0
  171. package/dist/localSensing.js.map +1 -0
  172. package/dist/modelIntegrity.d.ts +59 -0
  173. package/dist/modelIntegrity.d.ts.map +1 -0
  174. package/dist/modelIntegrity.js +165 -0
  175. package/dist/modelIntegrity.js.map +1 -0
  176. package/dist/pairVerify.d.ts +100 -0
  177. package/dist/pairVerify.d.ts.map +1 -0
  178. package/dist/pairVerify.js +296 -0
  179. package/dist/pairVerify.js.map +1 -0
  180. package/dist/qrScanner.d.ts +33 -0
  181. package/dist/qrScanner.d.ts.map +1 -0
  182. package/dist/qrScanner.js +103 -0
  183. package/dist/qrScanner.js.map +1 -0
  184. package/dist/runtimePaths.d.ts +10 -0
  185. package/dist/runtimePaths.d.ts.map +1 -0
  186. package/dist/runtimePaths.js +79 -0
  187. package/dist/runtimePaths.js.map +1 -0
  188. package/dist/sensingRunMode.d.ts +68 -0
  189. package/dist/sensingRunMode.d.ts.map +1 -0
  190. package/dist/sensingRunMode.js +346 -0
  191. package/dist/sensingRunMode.js.map +1 -0
  192. package/dist/setupMode.d.ts +98 -0
  193. package/dist/setupMode.d.ts.map +1 -0
  194. package/dist/setupMode.js +828 -0
  195. package/dist/setupMode.js.map +1 -0
  196. package/dist/setupProgress.d.ts +31 -0
  197. package/dist/setupProgress.d.ts.map +1 -0
  198. package/dist/setupProgress.js +173 -0
  199. package/dist/setupProgress.js.map +1 -0
  200. package/package.json +13 -4
@@ -0,0 +1,45 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!--
4
+ Per-user LaunchAgent for the LiteRT-LM on-device VLM server (Gemma 4 E2B).
5
+
6
+ This is a TEMPLATE: setup-litertlm.sh substitutes __LITERTLM__ (absolute path
7
+ to the uv-installed `litert-lm` binary) / __PORT__ / __LOG_DIR__ and writes the
8
+ result to ~/Library/LaunchAgents/com.trillboards.litertlm.plist, then loads it
9
+ via `launchctl bootstrap gui/$UID`.
10
+
11
+ Runs the OpenAI-compatible server the connector's LiteRtVlmProvider talks to.
12
+ KeepAlive=true (always-on local inference endpoint); ThrottleInterval guards a
13
+ hot crash-loop if the installed runtime becomes temporarily unavailable.
14
+ -->
15
+ <plist version="1.0">
16
+ <dict>
17
+ <key>Label</key>
18
+ <string>com.trillboards.litertlm</string>
19
+
20
+ <key>ProgramArguments</key>
21
+ <array>
22
+ <string>__LITERTLM__</string>
23
+ <string>serve</string>
24
+ <string>--host</string>
25
+ <string>127.0.0.1</string>
26
+ <string>--port</string>
27
+ <string>__PORT__</string>
28
+ </array>
29
+
30
+ <key>RunAtLoad</key>
31
+ <true/>
32
+ <key>KeepAlive</key>
33
+ <true/>
34
+ <key>ThrottleInterval</key>
35
+ <integer>30</integer>
36
+
37
+ <key>ProcessType</key>
38
+ <string>Background</string>
39
+
40
+ <key>StandardOutPath</key>
41
+ <string>__LOG_DIR__/litertlm.out.log</string>
42
+ <key>StandardErrorPath</key>
43
+ <string>__LOG_DIR__/litertlm.err.log</string>
44
+ </dict>
45
+ </plist>
@@ -0,0 +1,519 @@
1
+ #!/bin/bash
2
+ #
3
+ # pair.command — first-run pairing helper for the Trillboards local-first CCTV
4
+ # connector. Installed as ~/Library/Trillboards/edge/bin/trillboards-pair.
5
+ #
6
+ # Three modes:
7
+ # Native app (`--setup --status-file <path>`):
8
+ # runs validate-first setup without Terminal or AppleScript dialogs and
9
+ # reports credential-free progress to Trillboards CCTV Setup.app.
10
+ # Legacy GUI (no arguments):
11
+ # osascript prompts for the pairing code, runs the bundled
12
+ # `trillboards-edge pair`, bootstraps the per-user LaunchAgent, and shows
13
+ # a success/failure dialog. Kept for compatibility with older launchers.
14
+ # Headless (CI / scripted):
15
+ # trillboards-pair <CODE> [--api-base URL]
16
+ # Pairs, bootstraps the agent, prints status, exits 0/1. Drives the same
17
+ # code path as the GUI without any dialog (so the local test can use it).
18
+ #
19
+ # The device token is delivered ONLY by pairing — it is never baked into the
20
+ # pkg. The paired config is written to
21
+ # ~/Library/Application Support/Trillboards/trillboards.config.json (mode 0600).
22
+
23
+ set -euo pipefail
24
+
25
+ # ─── Resolve paths ───────────────────────────────────────────────────────────
26
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # …/edge/bin
27
+ ROOT="$(cd "$HERE/.." && pwd)" # …/edge
28
+ USER_HOME="${HOME:-$(eval echo "~$(id -un)")}"
29
+
30
+ LAUNCHER="$HERE/trillboards-edge"
31
+ NODE_BIN="$HERE/node" # bundled node runtime
32
+ APP_SUPPORT="$USER_HOME/Library/Application Support/Trillboards"
33
+ CONFIG="$APP_SUPPORT/trillboards.config.json"
34
+ LOG_DIR="$USER_HOME/Library/Logs/Trillboards"
35
+ LA_DIR="$USER_HOME/Library/LaunchAgents"
36
+ PLIST="$LA_DIR/com.trillboards.edge.plist"
37
+ PLIST_TEMPLATE="$ROOT/share/com.trillboards.edge.plist"
38
+ LOCAL_AI_SETUP="$ROOT/share/setup-litertlm.sh"
39
+ LABEL="com.trillboards.edge"
40
+
41
+ API_BASE="${TRILLBOARDS_API_BASE:-}"
42
+ HEADLESS_CODE=""
43
+ UI_STATUS_FILE=""
44
+
45
+ # Parse args. First non-flag positional == the code (headless mode).
46
+ while [ $# -gt 0 ]; do
47
+ case "$1" in
48
+ --api-base) API_BASE="$2"; shift 2;;
49
+ --setup) SETUP_MODE=1; shift;;
50
+ --status-file) UI_STATUS_FILE="$2"; shift 2;;
51
+ -h|--help) sed -n '2,20p' "${BASH_SOURCE[0]}"; exit 0;;
52
+ *) if [ -z "$HEADLESS_CODE" ]; then HEADLESS_CODE="$1"; fi; shift;;
53
+ esac
54
+ done
55
+
56
+ mkdir -p "$APP_SUPPORT" "$LOG_DIR" "$LA_DIR"
57
+ chmod 700 "$APP_SUPPORT" 2>/dev/null || true
58
+
59
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
60
+ is_headless() { [ -n "$HEADLESS_CODE" ]; }
61
+ is_ui() { [ -n "$UI_STATUS_FILE" ]; }
62
+
63
+ # Milestone log (rolling transcript for "send diagnostics" — never codes/tokens).
64
+ plog() { printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$*" >> "$LOG_DIR/pairing.log" 2>/dev/null || true; }
65
+
66
+ # Advance the credential-free state consumed by the native setup app. The CLI
67
+ # writer merges prior metrics (for example cameraCount) and writes atomically.
68
+ write_ui_status() { # stage title message
69
+ is_ui || return 0
70
+ "$LAUNCHER" setup-status \
71
+ --status-file "$UI_STATUS_FILE" \
72
+ --stage "$1" \
73
+ --title "$2" \
74
+ --message "$3" >/dev/null 2>&1 || true
75
+ }
76
+
77
+ # Confirm the installer-owned local AI service is still healthy before starting
78
+ # the camera connector. Required-bundle mode can repair the runtime or restart
79
+ # the service, but it can never download a missing model from the network.
80
+ verify_local_ai_setup() {
81
+ if [ ! -f "$LOCAL_AI_SETUP" ]; then
82
+ plog "local-ai: setup script missing at $LOCAL_AI_SETUP"
83
+ return 1
84
+ fi
85
+ chmod +x "$LOCAL_AI_SETUP" 2>/dev/null || true
86
+ if TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1 /bin/bash "$LOCAL_AI_SETUP" >> "$LOG_DIR/litertlm-setup.log" 2>&1; then
87
+ plog "local-ai: bundled model and service verified"
88
+ return 0
89
+ fi
90
+ plog "local-ai: verification failed"
91
+ return 1
92
+ }
93
+
94
+ # The LaunchAgent must actually RUN, not just load — "bootstrapped" alone is the
95
+ # same false success the Windows helper shipped (task registered ≠ task running).
96
+ agent_running() {
97
+ launchctl print "gui/$(id -u)/$LABEL" 2>/dev/null | grep -q 'state = running'
98
+ }
99
+ wait_agent_running() { # up to ~12s
100
+ local i
101
+ for i in $(seq 1 12); do
102
+ agent_running && return 0
103
+ sleep 1
104
+ done
105
+ return 1
106
+ }
107
+
108
+ run_with_timeout() {
109
+ local seconds="$1"; shift
110
+ "$@" &
111
+ local pid=$!
112
+ local elapsed=0
113
+ while kill -0 "$pid" 2>/dev/null; do
114
+ if [ "$elapsed" -ge "$seconds" ]; then
115
+ kill "$pid" 2>/dev/null || true
116
+ sleep 1
117
+ kill -9 "$pid" 2>/dev/null || true
118
+ wait "$pid" 2>/dev/null || true
119
+ return 124
120
+ fi
121
+ sleep 1
122
+ elapsed=$((elapsed + 1))
123
+ done
124
+ wait "$pid"
125
+ local rc=$?
126
+ return "$rc"
127
+ }
128
+
129
+ # Verify the SAVED pairing works from this machine (network + token). Exit codes
130
+ # from `trillboards-edge verify-connection`: 0 ok / 2 network / 3 auth /
131
+ # 4 no-config / 5 server — resume logic branches on these.
132
+ verify_saved_pairing() {
133
+ "$LAUNCHER" verify-connection --config "$CONFIG" >/dev/null 2>&1
134
+ }
135
+
136
+ dialog_get_code() {
137
+ # Returns the entered code on stdout, or exits 2 if cancelled.
138
+ osascript <<'OSA' 2>/dev/null
139
+ try
140
+ set theResult to display dialog "Paste your Trillboards pairing code" & return & "(Get one from the Trillboards portal → Add a camera → Install.)" default answer "" with title "Trillboards Edge — Pair this Mac" buttons {"Cancel", "Pair"} default button "Pair" with icon note
141
+ set theCode to text returned of theResult
142
+ return theCode
143
+ on error
144
+ error "cancelled" number 2
145
+ end try
146
+ OSA
147
+ }
148
+
149
+ # Dual-mode chooser. DEFAULT = the validate-first SETUP flow (this Mac shows a
150
+ # short code; the portal proves every camera stream BEFORE anything is
151
+ # created). "Classic code…" keeps the legacy portal-minted claim-code flow
152
+ # working (old wizard versions, scripted installs). Echoes "setup"|"classic",
153
+ # exits 2 on cancel.
154
+ dialog_choose_mode() {
155
+ osascript <<'OSA' 2>/dev/null
156
+ try
157
+ set theResult to display dialog "Connect this Mac's cameras to Trillboards." & return & return & "Start setup: this window shows a code, you type it in the portal, and the portal checks every camera BEFORE anything is created." with title "Trillboards Edge — Camera setup" buttons {"Cancel", "Classic code…", "Start setup"} default button "Start setup" with icon note
158
+ if button returned of theResult is "Start setup" then
159
+ return "setup"
160
+ else
161
+ return "classic"
162
+ end if
163
+ on error
164
+ error "cancelled" number 2
165
+ end try
166
+ OSA
167
+ }
168
+
169
+ dialog_info() { # $1 = message
170
+ osascript -e "display dialog \"$1\" with title \"Trillboards Edge\" buttons {\"OK\"} default button \"OK\" with icon note" >/dev/null 2>&1 || true
171
+ }
172
+ dialog_error() { # $1 = message
173
+ osascript -e "display dialog \"$1\" with title \"Trillboards Edge — Pairing failed\" buttons {\"OK\"} default button \"OK\" with icon stop" >/dev/null 2>&1 || true
174
+ }
175
+
176
+ # The ONE true remediation for "config dir/file owned by another account" — the
177
+ # signature of a .pkg installed with admin rights (its postinstall ran as root
178
+ # and left ~/Library/Application Support/Trillboards ROOT-OWNED). Names the REAL
179
+ # cause + the exact one-line fix; deliberately NEVER "check the code". Kept in
180
+ # one place so the pre-write guard AND the post-pair failure branch say the same
181
+ # thing (and match cli.ts configWritePermissionMessage). Real newlines mirror the
182
+ # working multi-line dialog below; single-quotes make the command copy-pasteable
183
+ # and are safe to interpolate into osascript's double-quoted string (no " / \).
184
+ # $1 = "fresh" when this fires AFTER the connector already spent the one-time
185
+ # claim code (post-pair net), so the operator must get a NEW code — not reuse the
186
+ # consumed one. The pre-write guards fire BEFORE the code is spent → default.
187
+ perm_remediation_msg() {
188
+ cat <<MSG
189
+ Trillboards can't save its settings because
190
+ $APP_SUPPORT
191
+ (or the config file inside it) is owned by another account — the installer ran
192
+ with admin rights. This is NOT a code problem.
193
+
194
+ Fix ownership once by pasting this in Terminal:
195
+
196
+ sudo chown -R $(whoami) '$APP_SUPPORT'
197
+ MSG
198
+ if [ "${1:-}" = "fresh" ]; then
199
+ printf '\n%s\n' "Then generate a NEW pairing code in the portal (the last one was already used) and run pairing again."
200
+ else
201
+ printf '\n%s\n' "Then run pairing again."
202
+ fi
203
+ }
204
+
205
+ # Substitute the plist template tokens with this machine's real paths and load
206
+ # the per-user LaunchAgent (bootstrap on modern macOS, load as fallback).
207
+ bootstrap_agent() {
208
+ /usr/bin/sed \
209
+ -e "s|__LAUNCHER__|$LAUNCHER|g" \
210
+ -e "s|__CONFIG__|$CONFIG|g" \
211
+ -e "s|__LOG_DIR__|$LOG_DIR|g" \
212
+ "$PLIST_TEMPLATE" > "$PLIST"
213
+
214
+ local uid; uid="$(id -u)"
215
+ # Reload cleanly: tear down any prior instance first (ignore errors).
216
+ launchctl bootout "gui/$uid/$LABEL" >/dev/null 2>&1 || true
217
+ launchctl unload "$PLIST" >/dev/null 2>&1 || true
218
+
219
+ if launchctl bootstrap "gui/$uid" "$PLIST" >/dev/null 2>&1; then
220
+ run_with_timeout 10 launchctl kickstart -k "gui/$uid/$LABEL" >/dev/null 2>&1 \
221
+ || plog "agent: launchctl kickstart did not finish cleanly (continuing to running check)"
222
+ return 0
223
+ fi
224
+ # Fallback for older macOS.
225
+ if launchctl load -w "$PLIST" >/dev/null 2>&1; then
226
+ return 0
227
+ fi
228
+ return 1
229
+ }
230
+
231
+ # Count paired feeds that still need an rtspUrl (creds-stay-on-prem mode: the
232
+ # server delivers the screen + token, the operator pastes the camera URL into
233
+ # this machine's config). Echoes the count (0 = all feeds ready). Mirrors the
234
+ # `needRtsp` guard in cli.ts runPair so the GUI helper never bootstraps a
235
+ # LaunchAgent that would crash-loop on a config whose feeds have no URL.
236
+ count_feeds_missing_rtsp() {
237
+ [ -f "$CONFIG" ] || { echo 0; return; }
238
+ [ -x "$NODE_BIN" ] || { echo 0; return; } # can't parse → don't block
239
+ "$NODE_BIN" -e '
240
+ try {
241
+ const cfg = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
242
+ const feeds = Array.isArray(cfg.cameras)
243
+ ? cfg.cameras
244
+ : (cfg.camera && (cfg.camera.rtspUrl || cfg.deviceToken) ? [cfg.camera] : []);
245
+ const missing = feeds.filter((f) => !f || !f.rtspUrl).length;
246
+ process.stdout.write(String(missing));
247
+ } catch (e) { process.stdout.write("0"); }
248
+ ' "$CONFIG" 2>/dev/null || echo 0
249
+ }
250
+
251
+ # ─── Guard: the config dir must be WRITABLE by us ────────────────────────────
252
+ # If the .pkg was installed with admin rights (system domain), its postinstall
253
+ # ran as root and may have left $APP_SUPPORT ROOT-OWNED. We run as the normal
254
+ # user, so writing the paired config there dies with EACCES — which historically
255
+ # surfaced as the misleading "Double-check the code (codes expire)". Detect it up
256
+ # front (before we even ask for a code), self-heal the common empty-dir case,
257
+ # else FAIL LOUDLY with the real cause + the one-line fix.
258
+ if [ -e "$APP_SUPPORT" ] && [ ! -w "$APP_SUPPORT" ]; then
259
+ # Best-effort self-heal. We run as the user, so we can't chown a root-owned
260
+ # dir — but we CAN (a) fix perms on a dir we already own, or (b) replace an
261
+ # EMPTY dir, since its parent (~/Library/Application Support) is ours. rmdir
262
+ # only succeeds when the dir is empty, so this can NEVER destroy a real config.
263
+ chmod u+rwx "$APP_SUPPORT" 2>/dev/null || true
264
+ if [ ! -w "$APP_SUPPORT" ]; then
265
+ { rmdir "$APP_SUPPORT" 2>/dev/null && mkdir -p "$APP_SUPPORT" 2>/dev/null; } || true
266
+ chmod 700 "$APP_SUPPORT" 2>/dev/null || true
267
+ fi
268
+ fi
269
+ if [ -e "$APP_SUPPORT" ] && [ ! -w "$APP_SUPPORT" ]; then
270
+ if is_ui; then
271
+ write_ui_status error "Camera settings need permission" "Reinstall Trillboards for this account. If the problem continues, contact Trillboards support."
272
+ elif is_headless; then
273
+ printf '[trillboards-pair] ERROR: %s\n' "$(perm_remediation_msg)" >&2
274
+ else
275
+ dialog_error "$(perm_remediation_msg)"
276
+ fi
277
+ exit 1
278
+ fi
279
+
280
+ # The config FILE itself may be root-owned inside an otherwise-writable dir (a
281
+ # prior admin run wrote it as root). The paired-config write would then EACCES
282
+ # AFTER the connector already CONSUMED the one-time claim code — so heal it HERE,
283
+ # before we spend the code. We own the parent dir, so we can just remove the
284
+ # stale file (pairing rewrites it); only if that's somehow impossible do we stop
285
+ # — and because this is still before the launcher, the code is unspent, so the
286
+ # default "run pairing again" is correct.
287
+ if [ -e "$CONFIG" ] && [ ! -w "$CONFIG" ]; then
288
+ rm -f "$CONFIG" 2>/dev/null || true
289
+ fi
290
+ if [ -e "$CONFIG" ] && [ ! -w "$CONFIG" ]; then
291
+ if is_ui; then
292
+ write_ui_status error "Camera settings need permission" "Reinstall Trillboards for this account. If the problem continues, contact Trillboards support."
293
+ elif is_headless; then
294
+ printf '[trillboards-pair] ERROR: %s\n' "$(perm_remediation_msg)" >&2
295
+ else
296
+ dialog_error "$(perm_remediation_msg)"
297
+ fi
298
+ exit 1
299
+ fi
300
+
301
+ # ─── Battery caution (non-blocking) ──────────────────────────────────────────
302
+ # Sensing dies silently if this Mac sleeps or shuts down. The connector holds a
303
+ # caffeinate keep-awake while running, but a closed lid still stops it.
304
+ if pmset -g batt 2>/dev/null | grep -q "Battery Power"; then
305
+ BATT_NOTE="Note: this Mac is on battery. Keep it plugged in and awake — your cameras stop reporting if it sleeps or shuts down."
306
+ if is_ui; then plog "power: running on battery"; elif is_headless; then echo "[trillboards-pair] $BATT_NOTE"; else dialog_info "$BATT_NOTE"; fi
307
+ fi
308
+
309
+ # ─── Resume path: a saved, complete pairing skips the code prompt ─────────────
310
+ # Branch on WHY verification failed: only an auth rejection (3) or a token-less
311
+ # config (4) needs a new code — a network/server failure (2/5) means the saved
312
+ # pairing is fine and the operator just fixes connectivity (NO new code).
313
+ if [ -f "$CONFIG" ] && [ "$(count_feeds_missing_rtsp)" = "0" ]; then
314
+ set +e
315
+ verify_saved_pairing
316
+ VERIFY_RC=$?
317
+ set -e
318
+ case "$VERIFY_RC" in
319
+ 0)
320
+ plog "resume: existing config verified — skipping code prompt"
321
+ if is_headless; then
322
+ echo "[trillboards-pair] already paired (connection verified) — ensuring the background agent runs"
323
+ fi
324
+ CODE="" # sentinel: skip the pair exchange below
325
+ ;;
326
+ 2|5)
327
+ plog "resume: saved config, connection check failed (rc=$VERIFY_RC)"
328
+ MSG="This Mac already has a saved Trillboards pairing, but it can't reach Trillboards right now. Check the internet connection, then run this helper again — you will NOT need a new code."
329
+ if is_ui; then write_ui_status error "This computer is offline" "Check the internet connection, then try again. Your saved pairing is still secure."; elif is_headless; then echo "[trillboards-pair] $MSG" >&2; else dialog_error "$MSG"; fi
330
+ exit 1
331
+ ;;
332
+ *) : ;; # 3/4 → genuinely re-pair below
333
+ esac
334
+ fi
335
+
336
+ # ─── Choose mode / get the code ──────────────────────────────────────────────
337
+ if [ "${CODE+set}" = "set" ] && [ -z "$CODE" ]; then
338
+ : # resume path — already paired + verified, jump straight to the agent stage
339
+ elif [ "${SETUP_MODE:-0}" = "1" ]; then
340
+ : # --setup flag (headless/scripted) — handled below
341
+ elif is_headless; then
342
+ CODE="$HEADLESS_CODE"
343
+ else
344
+ MODE="$(dialog_choose_mode)" || exit 0 # cancelled — nothing to do
345
+ if [ "$MODE" = "setup" ]; then
346
+ SETUP_MODE=1
347
+ else
348
+ if ! CODE="$(dialog_get_code)"; then
349
+ exit 0 # user cancelled — nothing to do
350
+ fi
351
+ if [ -z "$CODE" ]; then
352
+ dialog_error "No code entered. Re-run the pairing helper and paste the code from the portal."
353
+ exit 1
354
+ fi
355
+ fi
356
+ fi
357
+
358
+ # ─── Validate-first setup mode ────────────────────────────────────────────────
359
+ # The CLI displays progress in legacy/headless mode or writes structured status
360
+ # for the native app. It exits 0 once the portal finalizes cameras, then the same
361
+ # LaunchAgent stages at the bottom of this script run. rc=2 = expired/timeout.
362
+ if [ "${SETUP_MODE:-0}" = "1" ]; then
363
+ plog "setup: starting validate-first setup mode"
364
+ set +e
365
+ if is_ui; then
366
+ "$LAUNCHER" setup --config "$CONFIG" --status-file "$UI_STATUS_FILE"
367
+ else
368
+ "$LAUNCHER" setup --config "$CONFIG"
369
+ fi
370
+ SETUP_RC=$?
371
+ set -e
372
+ if [ "$SETUP_RC" -eq 0 ]; then
373
+ plog "setup: config received"
374
+ CODE="" # fall through to the agent stage exactly like the resume path
375
+ elif [ "$SETUP_RC" -eq 3 ]; then
376
+ plog "setup: server 404 (rc=3) — classic fallback"
377
+ if is_ui; then
378
+ write_ui_status error "Setup is temporarily unavailable" "Try again shortly or contact Trillboards support."
379
+ exit 1
380
+ fi
381
+ if ! CODE="$(dialog_get_code)"; then exit 0; fi
382
+ if [ -z "$CODE" ]; then
383
+ dialog_error "No code entered. Re-run the pairing helper and paste the code from the portal."
384
+ exit 1
385
+ fi
386
+ SETUP_MODE=0
387
+ elif [ "$SETUP_RC" -eq 2 ]; then
388
+ plog "setup: expired/timeout (rc=2)"
389
+ if is_ui; then write_ui_status error "Setup ended before finishing" "Try again when you are ready. No cameras were created."; else dialog_info "Setup ended without finishing (expired or timed out). Open this helper again to start over — nothing was created, nothing is lost."; fi
390
+ exit 1
391
+ else
392
+ plog "setup: failed (rc=$SETUP_RC)"
393
+ if is_ui; then write_ui_status error "Setup needs attention" "Try again. If the problem continues, contact Trillboards support."; else dialog_error "Setup hit an error. Details were saved to: $LOG_DIR/pairing.log — send that file to Trillboards support if it keeps happening."; fi
394
+ exit 1
395
+ fi
396
+ fi
397
+
398
+ # ─── Pair (skipped on the verified-resume path) ──────────────────────────────
399
+ # `pair` exit codes: 0 = paired + connection VERIFIED; 6 = paired + config saved
400
+ # but the connection check failed (code is SPENT — re-running needs NO new code);
401
+ # anything else = pairing failed with a classified, owner-grade reason already
402
+ # printed by the CLI (bad/expired code vs network vs rate-limit vs server).
403
+ if [ -n "${CODE:-}" ]; then
404
+ PAIR_ARGS=(pair --code "$CODE" --config "$CONFIG")
405
+ [ -n "$API_BASE" ] && PAIR_ARGS+=(--api-base "$API_BASE")
406
+
407
+ SAVED_UNVERIFIED_MSG="Paired and saved, but this Mac can't reach Trillboards right now. Fix the internet connection, then run this helper again — it resumes automatically and you will NOT need a new code."
408
+ # rc=7: the API REJECTED the fresh pairing (auth) — unlike rc=6 the saved
409
+ # config is unusable and the owner genuinely needs a new code.
410
+ AUTH_REJECTED_MSG="Trillboards rejected this new pairing. Generate a FRESH code in the portal and run this helper again."
411
+
412
+ if is_headless; then
413
+ echo "[trillboards-pair] pairing (headless)…"
414
+ set +e
415
+ "$LAUNCHER" "${PAIR_ARGS[@]}"
416
+ PAIR_RC=$?
417
+ set -e
418
+ if [ "$PAIR_RC" -eq 0 ]; then
419
+ echo "[trillboards-pair] paired + connection verified → $CONFIG"
420
+ plog "pair: ok (verified)"
421
+ elif [ "$PAIR_RC" -eq 6 ]; then
422
+ echo "[trillboards-pair] $SAVED_UNVERIFIED_MSG" >&2
423
+ plog "pair: saved but unverified (rc=6)"
424
+ exit 1
425
+ elif [ "$PAIR_RC" -eq 7 ]; then
426
+ echo "[trillboards-pair] $AUTH_REJECTED_MSG" >&2
427
+ plog "pair: saved but auth-rejected (rc=7)"
428
+ exit 1
429
+ else
430
+ echo "[trillboards-pair] PAIRING FAILED" >&2
431
+ plog "pair: failed (rc=$PAIR_RC)"
432
+ exit 1
433
+ fi
434
+ else
435
+ set +e
436
+ PAIR_OUT="$("$LAUNCHER" "${PAIR_ARGS[@]}" 2>&1)"
437
+ PAIR_RC=$?
438
+ set -e
439
+ if [ "$PAIR_RC" -eq 6 ]; then
440
+ plog "pair: saved but unverified (rc=6)"
441
+ dialog_info "$SAVED_UNVERIFIED_MSG"
442
+ exit 1
443
+ elif [ "$PAIR_RC" -eq 7 ]; then
444
+ plog "pair: saved but auth-rejected (rc=7)"
445
+ dialog_error "$AUTH_REJECTED_MSG"
446
+ exit 1
447
+ elif [ "$PAIR_RC" -ne 0 ]; then
448
+ plog "pair: failed (rc=$PAIR_RC)"
449
+ # If the connector failed for a PERMISSION reason (config dir/file owned by
450
+ # another account — an admin install left it root-owned), say THAT + the
451
+ # one-line fix; do NOT tell the operator to re-check a code that is fine. The
452
+ # writability pre-check above catches the common root-owned-DIR case first;
453
+ # this net covers a root-owned config FILE inside an otherwise-writable dir.
454
+ # Matches a raw EACCES AND cli.ts's clean message ("owned by another
455
+ # account" / "sudo chown").
456
+ if printf '%s' "$PAIR_OUT" | grep -qiE 'permission denied|EACCES|EPERM|owned by another account|sudo chown'; then
457
+ # Reached only if a permission failure slipped past the pre-write guards
458
+ # above — by now the connector has already spent the one-time code, so tell
459
+ # the operator to fix ownership AND get a fresh code.
460
+ dialog_error "$(perm_remediation_msg fresh)"
461
+ else
462
+ # The CLI's last lines carry the classified reason — show them verbatim
463
+ # instead of the historical blanket "Double-check the code" (a lie for
464
+ # network/server failures; only the CLI knows which it was).
465
+ dialog_error "Pairing did not complete.\n\n$(printf '%s' "$PAIR_OUT" | tail -6)"
466
+ fi
467
+ exit 1
468
+ fi
469
+ plog "pair: ok (verified)"
470
+ fi
471
+ fi
472
+
473
+ # ─── Gate on RTSP URLs, then bootstrap the LaunchAgent ───────────────────────
474
+ # In creds-stay-on-prem mode the server returns cameras WITHOUT an rtspUrl, and
475
+ # the connector writes those as empty placeholders. Starting the LaunchAgent now
476
+ # would crash-loop (`cloud-sense` skips URL-less feeds and exits with nothing to
477
+ # run) while the dialog claims "Paired and running" — so if ANY feed still needs
478
+ # a URL, skip the bootstrap and tell the operator to add the URL(s) first, then
479
+ # start. Mirrors the `needRtsp` guard in cli.ts runPair.
480
+ MISSING_RTSP="$(count_feeds_missing_rtsp)"
481
+ if [ "${MISSING_RTSP:-0}" -gt 0 ] 2>/dev/null; then
482
+ MSG="Paired ($MISSING_RTSP camera feed(s) still need an RTSP URL). Camera credentials stay on this Mac — add the rtsp:// URL(s) to:
483
+ $CONFIG
484
+ then run 'trillboards-edge auto-sense' (or re-run this pairing helper) to start. The background agent was NOT started yet so it won't crash-loop on an incomplete config."
485
+ if is_ui; then
486
+ write_ui_status error "Camera links are incomplete" "One or more cameras still need a stream link. Contact Trillboards support."
487
+ elif is_headless; then
488
+ echo "[trillboards-pair] $MISSING_RTSP feed(s) missing rtspUrl — NOT bootstrapping LaunchAgent. Add URL(s) to $CONFIG then run auto-sense." >&2
489
+ else
490
+ dialog_info "$MSG"
491
+ fi
492
+ exit 0
493
+ fi
494
+
495
+ # The config is complete and verified. Local AI was installed before this wizard
496
+ # opened; confirm it is still healthy before starting the camera service.
497
+ write_ui_status local_ai "Checking local AI" "The cameras are ready. Verifying the private AI service installed with Trillboards."
498
+ if ! verify_local_ai_setup; then
499
+ write_ui_status local_ai_error "Local AI needs attention" "The included local AI could not be started. Reinstall Trillboards CCTV Setup; your camera pairing is saved."
500
+ exit 1
501
+ fi
502
+
503
+ write_ui_status starting_service "Starting Trillboards" "Installing and verifying the background camera service."
504
+
505
+ # "Bootstrapped" alone is not success — verify the agent PROCESS is actually
506
+ # running before telling the owner anything is running (the Windows twin of this
507
+ # lie left a 16-camera install "paired" with a never-registered task).
508
+ if bootstrap_agent && wait_agent_running; then
509
+ plog "agent: bootstrapped + running"
510
+ MSG="Paired and running. Local AI is installed and healthy. Trillboards starts automatically at login."
511
+ write_ui_status ready "Camera setup is complete" "Trillboards and private local AI are running in the background."
512
+ if is_ui; then :; elif is_headless; then echo "[trillboards-pair] LaunchAgent running ($LABEL)"; else dialog_info "$MSG"; fi
513
+ exit 0
514
+ fi
515
+
516
+ plog "agent: bootstrap or start FAILED (not running after wait)"
517
+ MSG="Paired successfully, but the background agent did not start. Your pairing IS saved — nothing is lost. Try restarting this Mac (the agent starts at login). If it still fails, send this file to Trillboards support: $LOG_DIR/pairing.log (plus edge.err.log in the same folder)."
518
+ if is_ui; then write_ui_status error "Cameras paired, service not running" "Restart this Mac, then open Trillboards CCTV Setup again. Your pairing is saved."; elif is_headless; then echo "[trillboards-pair] ERROR: $MSG" >&2; else dialog_error "$MSG"; fi
519
+ exit 1
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # Keep installer-provisioned local AI healthy across transient service or
3
+ # package-runtime failures. The inner setup script is idempotent and owns its
4
+ # own lock; this wrapper owns a second lock so pairing and auto-sense launches
5
+ # collapse into one retry loop.
6
+
7
+ set -u
8
+ USER_HOME="${HOME:-$(eval echo "~$(id -un)")}"
9
+ ROOT="$USER_HOME/Library/Trillboards/edge"
10
+ SETUP="$ROOT/share/setup-litertlm.sh"
11
+ LOG_DIR="$USER_HOME/Library/Logs/Trillboards"
12
+ LOCK_DIR="$LOG_DIR/litertlm-ensure.lock"
13
+ HEALTH_URL="${LITERT_LM_HEALTH_URL:-http://127.0.0.1:9379/v1/models}"
14
+ MODEL_ID="gemma-4-E2B-it.litertlm"
15
+ MAX_DELAY_SECONDS="${LITERT_LM_RETRY_MAX_SECONDS:-300}"
16
+
17
+ mkdir -p "$LOG_DIR" 2>/dev/null || true
18
+ LOG="$LOG_DIR/litertlm-setup.log"
19
+ log() { printf '[trillboards ensure-litertlm] %s %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOG" 2>&1; }
20
+ local_ai_healthy() {
21
+ /usr/bin/curl --fail --silent --show-error --max-time 5 "$HEALTH_URL" 2>/dev/null |
22
+ /usr/bin/grep -Fq "$MODEL_ID"
23
+ }
24
+
25
+ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
26
+ LOCK_PID="$(cat "$LOCK_DIR/pid" 2>/dev/null || echo '')"
27
+ if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
28
+ log "provisioning retry loop is already running (pid $LOCK_PID)"
29
+ exit 0
30
+ fi
31
+ rm -rf "$LOCK_DIR" 2>/dev/null || true
32
+ mkdir "$LOCK_DIR" 2>/dev/null || exit 0
33
+ fi
34
+ printf '%s\n' "$$" >"$LOCK_DIR/pid" 2>/dev/null || true
35
+ cleanup() { rm -rf "$LOCK_DIR" 2>/dev/null || true; }
36
+ trap cleanup EXIT
37
+ trap 'exit 130' INT
38
+ trap 'exit 143' TERM
39
+
40
+ if [ ! -f "$SETUP" ]; then
41
+ log "setup script is missing at $SETUP"
42
+ exit 1
43
+ fi
44
+
45
+ delay=30
46
+ attempt=0
47
+ while ! local_ai_healthy; do
48
+ attempt=$((attempt + 1))
49
+ log "provisioning attempt $attempt"
50
+ TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1 /bin/bash "$SETUP" >>"$LOG" 2>&1 || true
51
+ if local_ai_healthy; then
52
+ log "local AI is healthy"
53
+ exit 0
54
+ fi
55
+ log "local AI is not ready; retrying in ${delay}s"
56
+ sleep "$delay"
57
+ delay=$((delay * 2))
58
+ if [ "$delay" -gt "$MAX_DELAY_SECONDS" ]; then delay="$MAX_DELAY_SECONDS"; fi
59
+ done
60
+
61
+ log "local AI was already healthy"
62
+ exit 0