@trillboards/edge-sdk 0.2.3 → 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 +122 -65
  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,588 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # build-macos-pkg.sh — build a self-contained, home-domain macOS installer
4
+ # (.pkg) for the Trillboards local-first CCTV connector.
5
+ #
6
+ # The resulting .pkg lays down a fully self-contained tree under the user's
7
+ # home (~/Library/Trillboards/edge) so the operator needs NO system Node,
8
+ # ffmpeg, Docker, or terminal:
9
+ #
10
+ # ~/Library/Trillboards/edge/
11
+ # bin/node real Node v22 arm64/x64 runtime (pinned, official)
12
+ # bin/ffmpeg bundled ffmpeg (pinned static darwin build, downloaded
13
+ # at build time — NO ffmpeg-static npm dep)
14
+ # bin/trillboards-edge launcher: PATH=bin:$PATH; exec node lib/.../cli.js
15
+ # bin/trillboards-pair first-run pairing helper (GUI + headless)
16
+ # lib/edge-sdk/dist/cli.js the connector CLI
17
+ # lib/node_modules/... the runtime subset (edge-core +
18
+ # edge-platform-rtsp + edge-sdk + their
19
+ # transitive deps), WITHOUT the heavy local
20
+ # ONNX/sharp inference stack.
21
+ #
22
+ # Because cloud-sense + pair are loaded WITHOUT the local-inference stack
23
+ # (cli.ts lazy-requires EdgeAgent), the bundle omits onnxruntime-node/sharp/
24
+ # better-sqlite3 — keeping the pkg small.
25
+ #
26
+ # Install (no sudo, no admin password):
27
+ # installer -pkg <pkg> -target CurrentUserHomeDirectory
28
+ #
29
+ # Usage:
30
+ # build-macos-pkg.sh [--arch arm64|x64] [--universal] [--node-version vX.Y.Z]
31
+ # [--sign-app "Developer ID Application: …"]
32
+ # [--sign-installer "Developer ID Installer: …"]
33
+ # [--bundle-model] (default; include the pinned ~2.4 GB VLM)
34
+ # [--skip-model] (engineering-only slim build)
35
+ # [--offline] (skip the official Node + ffmpeg downloads,
36
+ # copy the host `node`/`ffmpeg` instead — for
37
+ # air-gapped CI)
38
+ #
39
+ # --universal builds ONE fat arm64+x86_64 .pkg (node + ffmpeg lipo'd from both
40
+ # arches; bundled fsevents.node is already universal) that installs + runs on
41
+ # every Mac. This is the RELEASE artifact: a browser can't reliably detect a
42
+ # Mac's CPU arch (Safari reports "MacIntel" even on Apple Silicon), so the portal
43
+ # cannot hand out an arch-specific download. --arch builds a single-arch .pkg
44
+ # (faster, for local testing).
45
+ #
46
+ # ffmpeg: a pinned static darwin build (eugeneware/ffmpeg-static b6.0 — the same
47
+ # upstream the ffmpeg-static npm package ships) is downloaded at build time and
48
+ # its SHA-256 verified, exactly like the pinned Node runtime. There is NO
49
+ # ffmpeg-static npm dependency, so the workspace lockfile carries no ffmpeg
50
+ # binary churn. Override the release/checksums with FFMPEG_STATIC_TAG /
51
+ # FFMPEG_SHA256_ARM64 / FFMPEG_SHA256_X64 if upstream is re-pinned.
52
+ #
53
+ # Signing (two distinct cert classes — BOTH required for a notarized, double-
54
+ # click-clean installer):
55
+ # 1. --sign-app "Developer ID Application: …" — codesigns the embedded Mach-O
56
+ # binaries (node, ffmpeg, any native .node addons) with the hardened runtime
57
+ # + a secure timestamp. The notary service REQUIRES every embedded executable
58
+ # to carry a Developer ID Application signature + hardened runtime, so an
59
+ # unsigned ffmpeg/node would fail notarization even if the .pkg itself is
60
+ # signed.
61
+ # 2. --sign-installer "Developer ID Installer: …" — productsigns the final .pkg.
62
+ # (or set MAC_APP_IDENTITY / MAC_INSTALLER_IDENTITY). With no flags the build is
63
+ # UNSIGNED (fine for `installer -target CurrentUserHomeDirectory` local installs,
64
+ # but Gatekeeper blocks a downloaded copy). Notarize after signing — see
65
+ # README.md for the notarytool follow-on.
66
+
67
+ set -euo pipefail
68
+
69
+ # ─── Locate ourselves + the edge-sdk package root ────────────────────────────
70
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
71
+ # deploy/macos -> deploy -> edge-sdk
72
+ PKG_ROOT="$(cd "$HERE/../.." && pwd)" # …/packages/edge-sdk
73
+ WORKSPACE_ROOT="$(cd "$PKG_ROOT/../.." && pwd)" # …/trillboards-edge-sdk
74
+
75
+ BUNDLE_ID="com.trillboards.edge"
76
+ APP_NAME="TrillboardsEdge"
77
+ INSTALL_SUBDIR="Library/Trillboards/edge" # relative to the user home
78
+
79
+ # ─── Args ────────────────────────────────────────────────────────────────────
80
+ ARCH="$(uname -m)" # arm64 | x86_64
81
+ [ "$ARCH" = "x86_64" ] && ARCH="x64"
82
+ NODE_VERSION="v22.22.0"
83
+ SIGN_IDENTITY="${MAC_INSTALLER_IDENTITY:-}"
84
+ APP_IDENTITY="${MAC_APP_IDENTITY:-}"
85
+ OFFLINE=0
86
+ UNIVERSAL=0 # --universal → bundle a fat arm64+x86_64 node/ffmpeg (runs on every Mac)
87
+
88
+ # Pinned static ffmpeg (darwin) — eugeneware/ffmpeg-static release `b6.0`
89
+ # (ffmpeg 6.0). This is the exact upstream the ffmpeg-static npm package
90
+ # downloads, so the bundled binary is identical to before — we just fetch it
91
+ # directly at build time (like the Node runtime) instead of carrying an npm dep
92
+ # + lockfile entry. SHA-256 verified after download.
93
+ FFMPEG_STATIC_TAG="${FFMPEG_STATIC_TAG:-b6.0}"
94
+ FFMPEG_SHA256_ARM64="${FFMPEG_SHA256_ARM64:-a90e3db6a3fd35f6074b013f948b1aa45b31c6375489d39e572bea3f18336584}"
95
+ FFMPEG_SHA256_X64="${FFMPEG_SHA256_X64:-cfe20936c83ecf5d68e424b87e8cc45b24dd6be81787810123bb964a0df686f9}"
96
+
97
+ # Required pre-bundled on-device VLM (Gemma 4 E2B, LiteRT-LM). Customer packages
98
+ # include the pinned model so installation completes before camera pairing and
99
+ # never starts a multi-gigabyte post-pair download. GEMMA_MODEL_FILE can point
100
+ # at a verified local cache; otherwise the release build fetches the pinned URL.
101
+ # Override GEMMA_MODEL_URL / GEMMA_MODEL_SHA256 only when upstream is re-pinned.
102
+ GEMMA_MODEL_ID="gemma-4-E2B-it.litertlm"
103
+ GEMMA_MODEL_URL="${GEMMA_MODEL_URL:-https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm}"
104
+ GEMMA_MODEL_SHA256="${GEMMA_MODEL_SHA256:-181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c}"
105
+ GEMMA_MODEL_FILE="${GEMMA_MODEL_FILE:-}"
106
+ BUNDLE_MODEL=1
107
+
108
+ while [ $# -gt 0 ]; do
109
+ case "$1" in
110
+ --arch) ARCH="$2"; shift 2;;
111
+ --node-version) NODE_VERSION="$2"; shift 2;;
112
+ --sign-app) APP_IDENTITY="$2"; shift 2;;
113
+ --sign-installer) SIGN_IDENTITY="$2"; shift 2;;
114
+ --offline) OFFLINE=1; shift;;
115
+ --universal) UNIVERSAL=1; shift;;
116
+ --bundle-model) BUNDLE_MODEL=1; shift;;
117
+ --skip-model) BUNDLE_MODEL=0; shift;;
118
+ -h|--help) sed -n '2,40p' "${BASH_SOURCE[0]}"; exit 0;;
119
+ *) echo "Unknown arg: $1" >&2; exit 2;;
120
+ esac
121
+ done
122
+
123
+ # Offline customer builds must provide the model cache explicitly. Slim builds
124
+ # remain available only for focused engineering checks.
125
+ if [ "$OFFLINE" -eq 1 ] && [ "$BUNDLE_MODEL" -eq 1 ] && [ -z "$GEMMA_MODEL_FILE" ]; then
126
+ echo "--offline with the customer model bundle requires GEMMA_MODEL_FILE" >&2
127
+ exit 2
128
+ fi
129
+
130
+ # Normalize Node arch token used in the official tarball name.
131
+ case "$ARCH" in
132
+ arm64) NODE_ARCH="arm64";;
133
+ x64) NODE_ARCH="x64";;
134
+ *) echo "Unsupported --arch '$ARCH' (use arm64 or x64)" >&2; exit 2;;
135
+ esac
136
+
137
+ # A universal build produces ONE fat arm64+x86_64 .pkg that runs on every Mac —
138
+ # necessary because a browser cannot reliably detect a Mac's CPU arch (Safari
139
+ # reports "MacIntel" even on Apple Silicon), so the portal can't pick an
140
+ # arch-specific download.
141
+ ARCH_LABEL="$ARCH"
142
+ [ "$UNIVERSAL" -eq 1 ] && ARCH_LABEL="universal"
143
+
144
+ VERSION="$(node -e "process.stdout.write(require('$PKG_ROOT/package.json').version)")"
145
+ DIST_DIR="$PKG_ROOT/deploy/macos/dist-installer"
146
+ STAGE="$DIST_DIR/stage-$ARCH_LABEL" # the payload root (== the user home view)
147
+ PAYLOAD="$STAGE/$INSTALL_SUBDIR" # …/Library/Trillboards/edge
148
+ SCRATCH="$DIST_DIR/scratch-$ARCH_LABEL"
149
+ PKG_OUT="$DIST_DIR/${APP_NAME}-${VERSION}-${ARCH_LABEL}.pkg"
150
+ COMPONENT_PKG="$SCRATCH/${APP_NAME}-component.pkg"
151
+ SETUP_APP="$STAGE/Applications/Trillboards CCTV Setup.app"
152
+
153
+ log() { printf '\033[1;34m[build]\033[0m %s\n' "$*"; }
154
+ warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
155
+ die() { printf '\033[1;31m[err]\033[0m %s\n' "$*" >&2; exit 1; }
156
+
157
+ log "edge-sdk v$VERSION arch=$ARCH_LABEL node=$NODE_VERSION sign-app=${APP_IDENTITY:-<none>} sign-installer=${SIGN_IDENTITY:-<none>}"
158
+
159
+ # ─── Clean + recreate the staging tree (idempotent) ──────────────────────────
160
+ rm -rf "$STAGE" "$SCRATCH"
161
+ mkdir -p "$PAYLOAD/bin" "$PAYLOAD/lib" "$SCRATCH"
162
+
163
+ # ─── 1. Build ALL edge workspaces (tsc, dependency order) ─────────────────────
164
+ # edge-sdk's tsc imports edge-core, edge-platform-rtsp/linux/windows, edge-sensing,
165
+ # edge-ads, edge-federated AND edge-cloud — so EVERY workspace's dist/.d.ts must
166
+ # already exist when edge-sdk compiles. Building only edge-core + edge-platform-rtsp
167
+ # first (and the rest AFTER edge-sdk) made edge-sdk's tsc fail on missing dep types
168
+ # in a clean CI `npm ci` checkout. The root `build` is `npm run build --workspaces`,
169
+ # which compiles every workspace in dependency order — the only correct guarantee
170
+ # that edge-sdk's deps are present. (No output suppression: a future build error
171
+ # must surface in the CI log, not be swallowed into a bare "edge-sdk build failed".)
172
+ log "Building all edge workspaces (tsc, dependency order)…"
173
+ ( cd "$WORKSPACE_ROOT" && npm run build ) || die "edge workspace build failed (tsc)"
174
+
175
+ # ─── 2. Lay down the edge-sdk dist ───────────────────────────────────────────
176
+ log "Staging edge-sdk dist…"
177
+ mkdir -p "$PAYLOAD/lib/edge-sdk"
178
+ cp -R "$PKG_ROOT/dist" "$PAYLOAD/lib/edge-sdk/dist"
179
+ cp "$PKG_ROOT/package.json" "$PAYLOAD/lib/edge-sdk/package.json"
180
+
181
+ # ─── 3. Lay down the runtime node_modules subset (NO heavy native stack) ──────
182
+ # We copy the workspace's already-resolved node_modules (correct transitive
183
+ # closure) and EXCLUDE the heavy/native/dev packages. cloud-sense + pair never
184
+ # require them (EdgeAgent → edge-sensing → onnxruntime-node is lazy now), so
185
+ # their absence is fine and keeps the pkg ~50 MB instead of ~270 MB.
186
+ log "Staging runtime node_modules (excluding ONNX/sharp/sqlite/dev)…"
187
+ mkdir -p "$PAYLOAD/lib/node_modules"
188
+ rsync -a \
189
+ --exclude 'onnxruntime-node' \
190
+ --exclude 'sharp' \
191
+ --exclude '@img' \
192
+ --exclude 'better-sqlite3' \
193
+ --exclude 'ffmpeg-static' \
194
+ --exclude '.cache' \
195
+ --exclude '.bin' \
196
+ --exclude '.package-lock.json' \
197
+ --exclude 'typescript' \
198
+ --exclude 'jest' \
199
+ --exclude 'jest-*' \
200
+ --exclude '@jest' \
201
+ --exclude 'ts-jest' \
202
+ --exclude '@types' \
203
+ "$WORKSPACE_ROOT/node_modules/" "$PAYLOAD/lib/node_modules/"
204
+
205
+ # Overlay the @trillboards workspace packages with their REAL built dist
206
+ # (in the workspace they are symlinks → copy the materialized dist instead).
207
+ for p in edge-core edge-sensing edge-ads edge-federated edge-cloud \
208
+ edge-platform-rtsp edge-platform-linux edge-platform-windows \
209
+ edge-csi edge-sdk; do
210
+ src="$WORKSPACE_ROOT/packages/$p"
211
+ [ -d "$src" ] || continue
212
+ dest="$PAYLOAD/lib/node_modules/@trillboards/$p"
213
+ rm -rf "$dest"
214
+ mkdir -p "$dest"
215
+ [ -d "$src/dist" ] && cp -R "$src/dist" "$dest/dist"
216
+ cp "$src/package.json" "$dest/package.json"
217
+ done
218
+
219
+ # ─── 4. Bundle the Node runtime ──────────────────────────────────────────────
220
+ NODE_BIN="$PAYLOAD/bin/node"
221
+ # Download the official Node runtime for a given arch into a path (no fallback —
222
+ # a universal build must have the real per-arch slice to lipo).
223
+ fetch_node_arch() { # arch outpath
224
+ local a="$1" out="$2" tb="node-${NODE_VERSION}-darwin-$1.tar.gz"
225
+ curl -fsSL --max-time 180 -o "$SCRATCH/$tb" "https://nodejs.org/dist/${NODE_VERSION}/$tb" \
226
+ || die "Node download failed for darwin-$1"
227
+ tar -xzf "$SCRATCH/$tb" -C "$SCRATCH"
228
+ cp "$SCRATCH/node-${NODE_VERSION}-darwin-$1/bin/node" "$out"
229
+ }
230
+ if [ "$UNIVERSAL" -eq 1 ]; then
231
+ log "Building universal Node (arm64 + x64) via lipo…"
232
+ fetch_node_arch arm64 "$SCRATCH/node-arm64"
233
+ fetch_node_arch x64 "$SCRATCH/node-x64"
234
+ lipo -create "$SCRATCH/node-arm64" "$SCRATCH/node-x64" -output "$NODE_BIN" || die "lipo node failed"
235
+ elif [ "$OFFLINE" -eq 1 ]; then
236
+ log "Offline mode: copying host node ($(command -v node))…"
237
+ cp "$(command -v node)" "$NODE_BIN"
238
+ else
239
+ TARBALL="node-${NODE_VERSION}-darwin-${NODE_ARCH}.tar.gz"
240
+ URL="https://nodejs.org/dist/${NODE_VERSION}/${TARBALL}"
241
+ log "Downloading official Node runtime: $URL"
242
+ if curl -fsSL --max-time 120 -o "$SCRATCH/$TARBALL" "$URL"; then
243
+ tar -xzf "$SCRATCH/$TARBALL" -C "$SCRATCH"
244
+ cp "$SCRATCH/node-${NODE_VERSION}-darwin-${NODE_ARCH}/bin/node" "$NODE_BIN"
245
+ else
246
+ warn "Node download failed — falling back to host node ($(command -v node))."
247
+ cp "$(command -v node)" "$NODE_BIN"
248
+ fi
249
+ fi
250
+ chmod +x "$NODE_BIN"
251
+ # Verify the bundled node actually runs on this host.
252
+ "$NODE_BIN" --version >/dev/null 2>&1 || die "bundled node failed --version"
253
+ log "Bundled node: $("$NODE_BIN" --version) [$(lipo -archs "$NODE_BIN" 2>/dev/null || echo single-arch)]"
254
+
255
+ # ─── 5. Bundle ffmpeg (pinned static darwin build — no npm dep) ───────────────
256
+ # Download the pinned static ffmpeg for this arch and verify its SHA-256, exactly
257
+ # like the Node runtime above. NO ffmpeg-static npm dependency → zero lockfile
258
+ # churn. cloud-sense uses the bundled ffmpeg (mjpeg muxer → JPEG) so no image
259
+ # library (sharp) is needed.
260
+ FFMPEG_BIN="$PAYLOAD/bin/ffmpeg"
261
+ case "$NODE_ARCH" in
262
+ arm64) FFMPEG_EXPECTED_SHA="$FFMPEG_SHA256_ARM64";;
263
+ x64) FFMPEG_EXPECTED_SHA="$FFMPEG_SHA256_X64";;
264
+ esac
265
+
266
+ # Verify a copied/downloaded ffmpeg against the pinned SHA (when not host-fallback).
267
+ verify_ffmpeg_sha() {
268
+ local f="$1" expected="$2"
269
+ [ -n "$expected" ] || return 0
270
+ local actual
271
+ actual="$(shasum -a 256 "$f" 2>/dev/null | awk '{print $1}')"
272
+ [ "$actual" = "$expected" ]
273
+ }
274
+
275
+ # Download + SHA-verify a single-arch static ffmpeg into a path (for lipo).
276
+ fetch_ffmpeg_arch() { # arch outpath
277
+ local file="ffmpeg-darwin-$1" sha
278
+ case "$1" in arm64) sha="$FFMPEG_SHA256_ARM64";; x64) sha="$FFMPEG_SHA256_X64";; esac
279
+ curl -fsSL --max-time 180 -o "$SCRATCH/$file" \
280
+ "https://github.com/eugeneware/ffmpeg-static/releases/download/${FFMPEG_STATIC_TAG}/$file" \
281
+ || die "ffmpeg download failed for darwin-$1"
282
+ verify_ffmpeg_sha "$SCRATCH/$file" "$sha" \
283
+ || die "ffmpeg SHA-256 mismatch darwin-$1 (expected $sha, got $(shasum -a 256 "$SCRATCH/$file" 2>/dev/null | awk '{print $1}'))"
284
+ cp "$SCRATCH/$file" "$2"
285
+ }
286
+
287
+ if [ "$UNIVERSAL" -eq 1 ]; then
288
+ log "Building universal ffmpeg (arm64 + x64) via lipo…"
289
+ fetch_ffmpeg_arch arm64 "$SCRATCH/ffmpeg-arm64"
290
+ fetch_ffmpeg_arch x64 "$SCRATCH/ffmpeg-x64"
291
+ lipo -create "$SCRATCH/ffmpeg-arm64" "$SCRATCH/ffmpeg-x64" -output "$FFMPEG_BIN" || die "lipo ffmpeg failed"
292
+ log "Verified both arch SHA-256 + lipo'd universal ffmpeg"
293
+ elif [ "$OFFLINE" -eq 1 ]; then
294
+ HOST_FFMPEG="$(command -v ffmpeg || true)"
295
+ [ -n "$HOST_FFMPEG" ] || die "offline mode: no host ffmpeg on PATH to copy."
296
+ log "Offline mode: copying host ffmpeg ($HOST_FFMPEG)…"
297
+ cp "$HOST_FFMPEG" "$FFMPEG_BIN"
298
+ else
299
+ FFMPEG_FILE="ffmpeg-darwin-${NODE_ARCH}"
300
+ FFMPEG_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/${FFMPEG_STATIC_TAG}/${FFMPEG_FILE}"
301
+ log "Downloading pinned static ffmpeg: $FFMPEG_URL"
302
+ if curl -fsSL --max-time 180 -o "$SCRATCH/$FFMPEG_FILE" "$FFMPEG_URL"; then
303
+ if verify_ffmpeg_sha "$SCRATCH/$FFMPEG_FILE" "$FFMPEG_EXPECTED_SHA"; then
304
+ cp "$SCRATCH/$FFMPEG_FILE" "$FFMPEG_BIN"
305
+ log "Verified ffmpeg SHA-256 ($FFMPEG_EXPECTED_SHA)"
306
+ else
307
+ die "ffmpeg SHA-256 mismatch for $FFMPEG_FILE (expected $FFMPEG_EXPECTED_SHA, got $(shasum -a 256 "$SCRATCH/$FFMPEG_FILE" 2>/dev/null | awk '{print $1}')). Refusing to bundle an unverified binary."
308
+ fi
309
+ else
310
+ HOST_FFMPEG="$(command -v ffmpeg || true)"
311
+ [ -n "$HOST_FFMPEG" ] || die "ffmpeg download failed and no host ffmpeg on PATH to fall back to."
312
+ warn "ffmpeg download failed — falling back to host ffmpeg ($HOST_FFMPEG)."
313
+ cp "$HOST_FFMPEG" "$FFMPEG_BIN"
314
+ fi
315
+ fi
316
+ chmod +x "$FFMPEG_BIN"
317
+ # Verify the bundled ffmpeg actually runs on this host.
318
+ "$FFMPEG_BIN" -version >/dev/null 2>&1 || die "bundled ffmpeg failed -version"
319
+ log "Bundled ffmpeg: $("$FFMPEG_BIN" -version 2>/dev/null | head -1) [$(lipo -archs "$FFMPEG_BIN" 2>/dev/null || echo single-arch)]"
320
+
321
+ # ─── 6. Write the launcher ───────────────────────────────────────────────────
322
+ # The launcher puts the bundled bin/ first on PATH (so the connector's default
323
+ # ffmpeg='ffmpeg' resolves to OUR ffmpeg) and execs the bundled node against the
324
+ # bundled cli.js. No system Node or ffmpeg is touched.
325
+ LAUNCHER="$PAYLOAD/bin/trillboards-edge"
326
+ cat > "$LAUNCHER" <<'LAUNCH'
327
+ #!/bin/bash
328
+ # Trillboards Edge connector launcher (bundled runtime — no system deps).
329
+ set -euo pipefail
330
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # …/edge/bin
331
+ ROOT="$(cd "$HERE/.." && pwd)" # …/edge
332
+ export PATH="$HERE:$PATH" # bundled ffmpeg first
333
+ export NODE_PATH="$ROOT/lib/node_modules"
334
+
335
+ # The agent is loaded only after a complete pairing. If the local AI service
336
+ # needs repair after a reboot, restart it automatically at the next agent start.
337
+ # The setup script's lock collapses this with the pair helper's launch.
338
+ if [ "${1:-}" = "auto-sense" ]; then
339
+ LITERT_ROOT="${LITERT_LM_DIR:-$HOME/.litert-lm}"
340
+ MODEL_MARKER="$LITERT_ROOT/models/gemma-4-E2B-it.litertlm/trillboards-bundled-version.json"
341
+ SERVE_PLIST="$HOME/Library/LaunchAgents/com.trillboards.litertlm.plist"
342
+ SETUP="$ROOT/share/ensure-litertlm.sh"
343
+ LOCAL_AI_HEALTHY=0
344
+ if /usr/bin/curl --fail --silent --show-error --max-time 2 \
345
+ http://127.0.0.1:9379/v1/models >/dev/null 2>&1; then
346
+ LOCAL_AI_HEALTHY=1
347
+ fi
348
+ if { [ ! -f "$MODEL_MARKER" ] || [ ! -f "$SERVE_PLIST" ] || [ "$LOCAL_AI_HEALTHY" -ne 1 ]; } && [ -f "$SETUP" ]; then
349
+ nohup /bin/bash "$SETUP" >/dev/null 2>&1 &
350
+ fi
351
+ fi
352
+
353
+ exec "$HERE/node" "$ROOT/lib/edge-sdk/dist/cli.js" "$@"
354
+ LAUNCH
355
+ chmod +x "$LAUNCHER"
356
+
357
+ # ─── 7. Stage the pairing helper (GUI + headless) ────────────────────────────
358
+ cp "$PKG_ROOT/deploy/macos/pair.command" "$PAYLOAD/bin/trillboards-pair"
359
+ chmod +x "$PAYLOAD/bin/trillboards-pair"
360
+
361
+ # Stage the LaunchAgent template so the pair helper can read it at pair time.
362
+ mkdir -p "$PAYLOAD/share"
363
+ cp "$PKG_ROOT/deploy/macos/com.trillboards.edge.plist" "$PAYLOAD/share/com.trillboards.edge.plist"
364
+
365
+ # LOCAL-inference (LiteRT-LM) stack: the serve LaunchAgent template + the
366
+ # provisioning script. The package postinstall imports the bundled model and
367
+ # verifies the private service before it opens camera pairing.
368
+ cp "$PKG_ROOT/deploy/macos/com.trillboards.litertlm.plist" "$PAYLOAD/share/com.trillboards.litertlm.plist"
369
+ cp "$PKG_ROOT/deploy/macos/scripts/setup-litertlm.sh" "$PAYLOAD/share/setup-litertlm.sh"
370
+ cp "$PKG_ROOT/deploy/macos/scripts/ensure-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh"
371
+ chmod +x "$PAYLOAD/share/setup-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh"
372
+
373
+ # ─── 7b. Stage the pre-bundled Gemma model (.litertlm) into the payload ───────
374
+ # Stage the redistributable (Apache-2.0) Gemma 4 E2B LiteRT-LM model and verify
375
+ # its SHA-256 exactly like the pinned node/ffmpeg. Missing or corrupt model data
376
+ # is a release-blocking build failure, never a reason to ship a deferred download.
377
+ if [ "$BUNDLE_MODEL" -eq 1 ]; then
378
+ mkdir -p "$PAYLOAD/share/models"
379
+ GEMMA_OUT="$PAYLOAD/share/models/$GEMMA_MODEL_ID"
380
+ if [ -n "$GEMMA_MODEL_FILE" ]; then
381
+ [ -f "$GEMMA_MODEL_FILE" ] || die "GEMMA_MODEL_FILE does not exist: $GEMMA_MODEL_FILE"
382
+ log "Staging bundled Gemma model from local cache: $GEMMA_MODEL_FILE"
383
+ cp "$GEMMA_MODEL_FILE" "$GEMMA_OUT"
384
+ else
385
+ log "Downloading bundled Gemma model (~2.4 GB): $GEMMA_MODEL_URL"
386
+ curl -fL --retry 3 --max-time 1800 -o "$GEMMA_OUT" "$GEMMA_MODEL_URL" \
387
+ || die "Gemma model download failed. Refusing to build a customer installer without local AI."
388
+ fi
389
+ ACTUAL_SHA="$(shasum -a 256 "$GEMMA_OUT" 2>/dev/null | awk '{print $1}')"
390
+ if [ -z "$ACTUAL_SHA" ] || [ "$ACTUAL_SHA" != "$GEMMA_MODEL_SHA256" ]; then
391
+ die "Gemma model SHA-256 mismatch (expected $GEMMA_MODEL_SHA256, got ${ACTUAL_SHA:-missing}). Refusing to bundle an unverified model."
392
+ fi
393
+ printf '%s %s\n' "$GEMMA_MODEL_SHA256" "$GEMMA_MODEL_ID" > "$PAYLOAD/share/models/$GEMMA_MODEL_ID.sha256"
394
+ log "Staged + SHA-verified bundled Gemma model ($(du -h "$GEMMA_OUT" | cut -f1))"
395
+ else
396
+ warn "Building an engineering-only slim package without local AI. Do not publish this artifact to customers."
397
+ fi
398
+
399
+ # ─── 7c. Build the native customer setup application ─────────────────────────
400
+ # AppKit keeps the installer small and gives customers a real application window
401
+ # while the existing, proven setup engine runs invisibly in the background.
402
+ SETUP_APP_MACOS="$SETUP_APP/Contents/MacOS"
403
+ SETUP_APP_BIN="$SETUP_APP_MACOS/Trillboards CCTV Setup"
404
+ mkdir -p "$SETUP_APP_MACOS"
405
+ log "Building native Trillboards CCTV Setup.app…"
406
+ if [ "$UNIVERSAL" -eq 1 ]; then
407
+ clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch arm64 -arch x86_64 -framework Cocoa \
408
+ "$PKG_ROOT/deploy/macos/TrillboardsSetupApp.m" -o "$SETUP_APP_BIN" \
409
+ || die "native setup app universal build failed"
410
+ else
411
+ case "$NODE_ARCH" in
412
+ arm64) SETUP_ARCH="arm64";;
413
+ x64) SETUP_ARCH="x86_64";;
414
+ esac
415
+ clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch "$SETUP_ARCH" -framework Cocoa \
416
+ "$PKG_ROOT/deploy/macos/TrillboardsSetupApp.m" -o "$SETUP_APP_BIN" \
417
+ || die "native setup app build failed"
418
+ fi
419
+ chmod +x "$SETUP_APP_BIN"
420
+ "$SETUP_APP_BIN" --smoke-test-cancel \
421
+ || die "native setup app cancellation smoke test failed"
422
+ INSTALLER_TMP="$STAGE/PKInstallSandbox.trillboards-test/tmp"
423
+ mkdir -p "$INSTALLER_TMP"
424
+ TMPDIR="$INSTALLER_TMP" "$SETUP_APP_BIN" --smoke-test-tempdir \
425
+ || die "native setup app stale-TMPDIR smoke test failed"
426
+ rm -rf "$STAGE/PKInstallSandbox.trillboards-test"
427
+ cat > "$SETUP_APP/Contents/Info.plist" <<PLIST
428
+ <?xml version="1.0" encoding="UTF-8"?>
429
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
430
+ <plist version="1.0">
431
+ <dict>
432
+ <key>CFBundleDevelopmentRegion</key><string>en</string>
433
+ <key>CFBundleDisplayName</key><string>Trillboards CCTV Setup</string>
434
+ <key>CFBundleExecutable</key><string>Trillboards CCTV Setup</string>
435
+ <key>CFBundleIdentifier</key><string>com.trillboards.cctv-setup</string>
436
+ <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
437
+ <key>CFBundleName</key><string>Trillboards CCTV Setup</string>
438
+ <key>CFBundlePackageType</key><string>APPL</string>
439
+ <key>CFBundleShortVersionString</key><string>$VERSION</string>
440
+ <key>CFBundleVersion</key><string>$VERSION</string>
441
+ <key>LSMinimumSystemVersion</key><string>12.0</string>
442
+ <key>NSHighResolutionCapable</key><true/>
443
+ <key>NSLocalNetworkUsageDescription</key><string>Trillboards uses your local network to find your camera recorder and validate its camera feeds.</string>
444
+ </dict>
445
+ </plist>
446
+ PLIST
447
+ plutil -lint "$SETUP_APP/Contents/Info.plist" >/dev/null || die "native setup app Info.plist is invalid"
448
+ file "$SETUP_APP_BIN" | grep -q 'Mach-O' || die "native setup app executable is not Mach-O"
449
+ log "Native setup app staged: $SETUP_APP"
450
+
451
+ # ─── 8. Smoke-test the staged bundle BEFORE packaging ────────────────────────
452
+ log "Smoke-testing the staged bundle (version)…"
453
+ STAGED_VER="$("$LAUNCHER" version 2>&1 || true)"
454
+ echo "$STAGED_VER" | grep -q "edge-sdk v" || die "staged launcher failed: $STAGED_VER"
455
+ log "Staged launcher OK: $STAGED_VER"
456
+
457
+ # ─── 8.5 Codesign embedded Mach-O (Developer ID Application) ──────────────────
458
+ # The notary service rejects a .pkg unless EVERY embedded Mach-O (node, ffmpeg,
459
+ # any native .node addon) carries a Developer ID Application signature WITH the
460
+ # hardened runtime + a secure timestamp. The "Developer ID Installer" cert used
461
+ # in step 10 can only productsign the .pkg wrapper — it cannot codesign a Mach-O.
462
+ # Node's V8 JIT additionally needs allow-jit + allow-unsigned-executable-memory,
463
+ # and the connector loads our own (differently-signed/unsigned) .js + addons, so
464
+ # disable-library-validation is required too — without these the hardened-runtime
465
+ # node aborts at launch. Sign deepest-first so nested Mach-O are sealed before
466
+ # their containers.
467
+ if [ -n "$APP_IDENTITY" ]; then
468
+ log "Codesigning embedded Mach-O with '$APP_IDENTITY' (hardened runtime + timestamp)…"
469
+ ENT="$SCRATCH/hardened-entitlements.plist"
470
+ cat > "$ENT" <<'PLIST'
471
+ <?xml version="1.0" encoding="UTF-8"?>
472
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
473
+ <plist version="1.0">
474
+ <dict>
475
+ <key>com.apple.security.cs.allow-jit</key><true/>
476
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
477
+ <key>com.apple.security.cs.disable-library-validation</key><true/>
478
+ <key>com.apple.security.cs.allow-dyld-environment-variables</key><true/>
479
+ </dict>
480
+ </plist>
481
+ PLIST
482
+ signed_count=0
483
+ while IFS= read -r f; do
484
+ if file "$f" 2>/dev/null | grep -q "Mach-O"; then
485
+ codesign --force --options runtime --timestamp \
486
+ --entitlements "$ENT" --sign "$APP_IDENTITY" "$f" >/dev/null 2>&1 \
487
+ || die "codesign failed for $f"
488
+ signed_count=$((signed_count + 1))
489
+ fi
490
+ done < <(find "$PAYLOAD" -type f | sort -r)
491
+ codesign --force --options runtime --timestamp --sign "$APP_IDENTITY" "$SETUP_APP" >/dev/null 2>&1 \
492
+ || die "codesign failed for native setup app bundle"
493
+ log "Codesigned $signed_count Mach-O binar(ies)."
494
+ codesign --verify --strict "$NODE_BIN" || die "codesign verify failed: node"
495
+ codesign --verify --strict "$FFMPEG_BIN" || die "codesign verify failed: ffmpeg"
496
+ codesign --verify --strict --deep "$SETUP_APP" || die "codesign verify failed: native setup app"
497
+ log "Verified node + ffmpeg signatures (Developer ID Application, hardened runtime)."
498
+ elif [ -n "$SIGN_IDENTITY" ]; then
499
+ warn "--sign-installer was passed WITHOUT --sign-app: the embedded node/ffmpeg"
500
+ warn "stay unsigned + lack the hardened runtime, so notarization WILL FAIL."
501
+ warn "Pass --sign-app \"Developer ID Application: …\" too for a notarizable .pkg."
502
+ fi
503
+
504
+ # ─── 9. pkgbuild (component) + productbuild (home-domain distribution) ────────
505
+ # The component pkg captures the payload tree rooted at the user home; the
506
+ # distribution XML sets enable_currentUserHome so `installer -target
507
+ # CurrentUserHomeDirectory` installs without root.
508
+ log "pkgbuild component…"
509
+ pkgbuild \
510
+ --root "$STAGE" \
511
+ --identifier "$BUNDLE_ID" \
512
+ --version "$VERSION" \
513
+ --install-location "/" \
514
+ --scripts "$PKG_ROOT/deploy/macos/scripts" \
515
+ "$COMPONENT_PKG" >/dev/null
516
+
517
+ # productbuild hostArchitectures uses the "x86_64"/"arm64" tokens (NOT "x64").
518
+ case "$ARCH_LABEL" in
519
+ universal) HOST_ARCHS="x86_64,arm64";;
520
+ arm64) HOST_ARCHS="arm64";;
521
+ x64) HOST_ARCHS="x86_64";;
522
+ *) HOST_ARCHS="$NODE_ARCH";;
523
+ esac
524
+
525
+ DIST_XML="$SCRATCH/distribution.xml"
526
+ cat > "$DIST_XML" <<XML
527
+ <?xml version="1.0" encoding="utf-8"?>
528
+ <installer-gui-script minSpecVersion="2">
529
+ <title>Trillboards Edge — CCTV Connector</title>
530
+ <organization>com.trillboards</organization>
531
+ <!-- Home-domain install: no root, no admin password. -->
532
+ <domains enable_anywhere="false" enable_currentUserHome="true" enable_localSystem="false"/>
533
+ <options customize="never" require-scripts="false" hostArchitectures="$HOST_ARCHS"/>
534
+ <volume-check>
535
+ <allowed-os-versions>
536
+ <os-version min="12.0"/>
537
+ </allowed-os-versions>
538
+ </volume-check>
539
+ <choices-outline>
540
+ <line choice="default">
541
+ <line choice="$BUNDLE_ID"/>
542
+ </line>
543
+ </choices-outline>
544
+ <choice id="default"/>
545
+ <choice id="$BUNDLE_ID" visible="false">
546
+ <pkg-ref id="$BUNDLE_ID"/>
547
+ </choice>
548
+ <pkg-ref id="$BUNDLE_ID" version="$VERSION" onConclusion="none">$(basename "$COMPONENT_PKG")</pkg-ref>
549
+ </installer-gui-script>
550
+ XML
551
+
552
+ log "productbuild distribution…"
553
+ productbuild \
554
+ --distribution "$DIST_XML" \
555
+ --package-path "$SCRATCH" \
556
+ "$PKG_OUT" >/dev/null
557
+
558
+ # ─── 10. Optional sign ───────────────────────────────────────────────────────
559
+ if [ -n "$SIGN_IDENTITY" ]; then
560
+ log "productsign with '$SIGN_IDENTITY'…"
561
+ SIGNED="$DIST_DIR/${APP_NAME}-${VERSION}-${ARCH}-signed.pkg"
562
+ productsign --sign "$SIGN_IDENTITY" "$PKG_OUT" "$SIGNED"
563
+ mv "$SIGNED" "$PKG_OUT"
564
+ log "Signed: $PKG_OUT"
565
+ echo ""
566
+ echo "NEXT: notarize for Gatekeeper-clean distribution:"
567
+ echo " xcrun notarytool submit '$PKG_OUT' --keychain-profile <profile> --wait"
568
+ echo " xcrun stapler staple '$PKG_OUT'"
569
+ else
570
+ warn "UNSIGNED build (no Developer ID Installer cert / --sign-installer)."
571
+ warn "Fine for local install via 'installer -pkg … -target CurrentUserHomeDirectory'."
572
+ warn "For public distribution, build on a machine with a 'Developer ID Installer'"
573
+ warn "cert (pass --sign-installer) then notarize (see deploy/macos/README.md)."
574
+ fi
575
+
576
+ # Keep deliberately slim engineering builds bounded so an unrelated payload
577
+ # regression cannot masquerade as the bundled customer package.
578
+ PKG_BYTES="$(stat -f%z "$PKG_OUT")"
579
+ MAX_SLIM_PKG_BYTES="${MAX_SLIM_PKG_BYTES:-262144000}"
580
+ if [ "$BUNDLE_MODEL" -eq 0 ] && [ "$PKG_BYTES" -gt "$MAX_SLIM_PKG_BYTES" ]; then
581
+ die "slim engineering installer is $PKG_BYTES bytes; maximum is $MAX_SLIM_PKG_BYTES bytes"
582
+ fi
583
+
584
+ # ─── Done ────────────────────────────────────────────────────────────────────
585
+ PKG_SIZE="$(du -h "$PKG_OUT" | cut -f1)"
586
+ echo ""
587
+ log "Built: $PKG_OUT ($PKG_SIZE / $PKG_BYTES bytes)"
588
+ echo "$PKG_OUT"
@@ -0,0 +1,62 @@
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 Trillboards local-first CCTV connector.
5
+
6
+ This is a TEMPLATE: the `pair` helper (pair.command) substitutes the
7
+ __LAUNCHER__ / __CONFIG__ / __LOG_DIR__ tokens with this machine's real
8
+ absolute paths and writes the result to
9
+ ~/Library/LaunchAgents/com.trillboards.edge.plist, then loads it via
10
+ `launchctl bootstrap gui/$UID`.
11
+
12
+ It is intentionally NOT loaded by the installer's postinstall: a connector
13
+ with no paired config would just crash-loop (the chosen sense run mode exits
14
+ non-zero when the config is missing), so the agent is bootstrapped only AFTER a successful
15
+ pair has written the config. KeepAlive is therefore scoped to
16
+ SuccessfulExit=false so a clean exit (e.g. operator stop) is not relaunched,
17
+ but a crash is.
18
+ -->
19
+ <plist version="1.0">
20
+ <dict>
21
+ <key>Label</key>
22
+ <string>com.trillboards.edge</string>
23
+
24
+ <key>ProgramArguments</key>
25
+ <array>
26
+ <string>__LAUNCHER__</string>
27
+ <!-- auto-sense: use the installer-provisioned on-device LiteRT-LM (Gemma).
28
+ Cloud inference requires opt-in config. -->
29
+ <string>auto-sense</string>
30
+ <string>--config</string>
31
+ <string>__CONFIG__</string>
32
+ </array>
33
+
34
+ <!-- Start at login and keep running. Only relaunch on a crash, not on a
35
+ clean operator-initiated stop. -->
36
+ <key>RunAtLoad</key>
37
+ <true/>
38
+ <key>KeepAlive</key>
39
+ <dict>
40
+ <key>SuccessfulExit</key>
41
+ <false/>
42
+ </dict>
43
+
44
+ <!-- Throttle relaunches so a misconfigured feed can't hot-loop. -->
45
+ <key>ThrottleInterval</key>
46
+ <integer>10</integer>
47
+
48
+ <key>ProcessType</key>
49
+ <string>Background</string>
50
+
51
+ <key>StandardOutPath</key>
52
+ <string>__LOG_DIR__/edge.out.log</string>
53
+ <key>StandardErrorPath</key>
54
+ <string>__LOG_DIR__/edge.err.log</string>
55
+
56
+ <key>EnvironmentVariables</key>
57
+ <dict>
58
+ <key>NODE_ENV</key>
59
+ <string>production</string>
60
+ </dict>
61
+ </dict>
62
+ </plist>