@camstack/server 1.0.0 → 1.0.1

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 (234) hide show
  1. package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
  2. package/dist/api/addon-upload.js +441 -0
  3. package/dist/api/addons-custom.router.js +91 -0
  4. package/dist/api/auth-whoami.js +55 -0
  5. package/dist/api/bridge-addons.router.js +109 -0
  6. package/dist/api/capabilities.router.js +229 -0
  7. package/dist/api/core/addon-settings.router.js +117 -0
  8. package/dist/api/core/agents.router.js +73 -0
  9. package/dist/api/core/auth.router.js +286 -0
  10. package/dist/api/core/bulk-update-coordinator.js +229 -0
  11. package/dist/api/core/cap-providers.js +1124 -0
  12. package/dist/api/core/capabilities.router.js +138 -0
  13. package/dist/api/core/collection-preference.js +17 -0
  14. package/dist/api/core/event-bus-proxy.router.js +45 -0
  15. package/dist/api/core/hwaccel.router.js +91 -0
  16. package/dist/api/core/live-events.router.js +61 -0
  17. package/dist/api/core/logs.router.js +172 -0
  18. package/dist/api/core/notifications.router.js +67 -0
  19. package/dist/api/core/repl.router.js +35 -0
  20. package/dist/api/core/settings-backend.router.js +121 -0
  21. package/dist/api/core/stream-probe.router.js +58 -0
  22. package/dist/api/core/system-events.router.js +100 -0
  23. package/dist/api/health/health.routes.js +68 -0
  24. package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
  25. package/dist/api/oauth2/oauth2-routes.js +219 -0
  26. package/dist/api/trpc/cap-mount-helpers.js +194 -0
  27. package/dist/api/trpc/cap-route-error-formatter.js +133 -0
  28. package/dist/api/trpc/client-ip.js +147 -0
  29. package/dist/api/trpc/core-cap-bridge.js +115 -0
  30. package/dist/api/trpc/generated-cap-mounts.js +388 -0
  31. package/dist/api/trpc/generated-cap-routers.js +7635 -0
  32. package/dist/api/trpc/scope-access.js +93 -0
  33. package/dist/api/trpc/trpc.context.js +184 -0
  34. package/dist/api/trpc/trpc.middleware.js +139 -0
  35. package/dist/api/trpc/trpc.router.js +188 -0
  36. package/dist/auth/session-cookie.js +47 -0
  37. package/dist/boot/boot-config.js +241 -0
  38. package/dist/boot/integration-id-backfill.js +76 -0
  39. package/dist/boot/post-boot.service.js +85 -0
  40. package/dist/core/addon/addon-call-gateway.js +99 -0
  41. package/dist/core/addon/addon-package.service.js +1560 -0
  42. package/dist/core/addon/addon-registry.service.js +2739 -0
  43. package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
  44. package/dist/core/addon/addon-search.service.js +62 -0
  45. package/dist/core/addon/addon-settings-provider.js +102 -0
  46. package/dist/core/addon/addon.tokens.js +5 -0
  47. package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
  48. package/dist/core/addon-pages/addon-pages.service.js +107 -0
  49. package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
  50. package/dist/core/agent/agent-registry.service.js +477 -0
  51. package/dist/core/auth/auth.service.js +10 -0
  52. package/dist/core/capability/capability.service.js +58 -0
  53. package/dist/core/config/config.schema.js +7 -0
  54. package/dist/core/config/config.service.js +10 -0
  55. package/dist/core/events/event-bus.service.js +83 -0
  56. package/dist/core/feature/feature.service.js +10 -0
  57. package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
  58. package/dist/core/logging/log-ring-buffer.js +6 -0
  59. package/dist/core/logging/logging.service.js +130 -0
  60. package/dist/core/logging/scoped-logger.js +6 -0
  61. package/dist/core/moleculer/cap-call-fn.js +50 -0
  62. package/dist/core/moleculer/cap-route-authority.js +122 -0
  63. package/dist/core/moleculer/moleculer.service.js +898 -0
  64. package/dist/core/network/network-quality.service.js +7 -0
  65. package/dist/core/notification/notification-wrapper.service.js +33 -0
  66. package/dist/core/notification/toast-wrapper.service.js +25 -0
  67. package/dist/core/provider/provider.tokens.js +4 -0
  68. package/dist/core/repl/repl-engine.service.js +140 -0
  69. package/dist/core/storage/fs-storage-backend.js +6 -0
  70. package/dist/core/storage/storage-location-manager.js +6 -0
  71. package/dist/core/storage/storage.service.js +7 -0
  72. package/dist/core/streaming/stream-probe.service.js +209 -0
  73. package/dist/core/topology/topology-emitter.service.js +106 -0
  74. package/dist/launcher.js +325 -0
  75. package/dist/main.js +1098 -0
  76. package/dist/manual-boot.js +227 -0
  77. package/package.json +5 -1
  78. package/src/__tests__/addon-install-e2e.test.ts +0 -74
  79. package/src/__tests__/addon-pages-e2e.test.ts +0 -200
  80. package/src/__tests__/addon-route-session.test.ts +0 -17
  81. package/src/__tests__/addon-settings-router.spec.ts +0 -67
  82. package/src/__tests__/addon-upload.spec.ts +0 -475
  83. package/src/__tests__/agent-registry.spec.ts +0 -179
  84. package/src/__tests__/agent-status-page.spec.ts +0 -82
  85. package/src/__tests__/auth-session-cookie.test.ts +0 -48
  86. package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
  87. package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
  88. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
  89. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
  90. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
  91. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
  92. package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
  93. package/src/__tests__/cap-route-adapter.spec.ts +0 -302
  94. package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
  95. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
  96. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
  97. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
  98. package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
  99. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
  100. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
  101. package/src/__tests__/cap-routers/harness.ts +0 -163
  102. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
  103. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
  104. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
  105. package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
  106. package/src/__tests__/capability-e2e.test.ts +0 -384
  107. package/src/__tests__/cli-e2e.test.ts +0 -150
  108. package/src/__tests__/core-cap-bridge.spec.ts +0 -91
  109. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
  110. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
  111. package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
  112. package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
  113. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
  114. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
  115. package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
  116. package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
  117. package/src/__tests__/framework-allowlist.spec.ts +0 -96
  118. package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
  119. package/src/__tests__/https-e2e.test.ts +0 -124
  120. package/src/__tests__/lifecycle-e2e.test.ts +0 -189
  121. package/src/__tests__/live-events-subscription.spec.ts +0 -149
  122. package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
  123. package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
  124. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
  125. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
  126. package/src/__tests__/native-cap-route.spec.ts +0 -427
  127. package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
  128. package/src/__tests__/post-boot-restart.spec.ts +0 -161
  129. package/src/__tests__/singleton-contention.test.ts +0 -499
  130. package/src/__tests__/streaming-diagnostic.test.ts +0 -615
  131. package/src/__tests__/streaming-scale.test.ts +0 -314
  132. package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
  133. package/src/__tests__/uds-log-ingest.spec.ts +0 -183
  134. package/src/api/__tests__/addons-custom.spec.ts +0 -148
  135. package/src/api/__tests__/capabilities.router.test.ts +0 -56
  136. package/src/api/addon-upload.ts +0 -529
  137. package/src/api/addons-custom.router.ts +0 -101
  138. package/src/api/auth-whoami.ts +0 -101
  139. package/src/api/bridge-addons.router.ts +0 -122
  140. package/src/api/capabilities.router.ts +0 -265
  141. package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
  142. package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
  143. package/src/api/core/addon-settings.router.ts +0 -127
  144. package/src/api/core/agents.router.ts +0 -86
  145. package/src/api/core/auth.router.ts +0 -322
  146. package/src/api/core/bulk-update-coordinator.ts +0 -305
  147. package/src/api/core/cap-providers.ts +0 -1339
  148. package/src/api/core/capabilities.router.ts +0 -149
  149. package/src/api/core/collection-preference.ts +0 -40
  150. package/src/api/core/event-bus-proxy.router.ts +0 -45
  151. package/src/api/core/hwaccel.router.ts +0 -108
  152. package/src/api/core/live-events.router.ts +0 -67
  153. package/src/api/core/logs.router.ts +0 -195
  154. package/src/api/core/notifications.router.ts +0 -66
  155. package/src/api/core/repl.router.ts +0 -39
  156. package/src/api/core/settings-backend.router.ts +0 -140
  157. package/src/api/core/stream-probe.router.ts +0 -57
  158. package/src/api/core/system-events.router.ts +0 -125
  159. package/src/api/health/health.routes.ts +0 -117
  160. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
  161. package/src/api/oauth2/oauth2-routes.ts +0 -281
  162. package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
  163. package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
  164. package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
  165. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
  166. package/src/api/trpc/cap-mount-helpers.ts +0 -245
  167. package/src/api/trpc/cap-route-error-formatter.ts +0 -171
  168. package/src/api/trpc/client-ip.ts +0 -147
  169. package/src/api/trpc/core-cap-bridge.ts +0 -154
  170. package/src/api/trpc/generated-cap-mounts.ts +0 -1240
  171. package/src/api/trpc/generated-cap-routers.ts +0 -11523
  172. package/src/api/trpc/scope-access.ts +0 -110
  173. package/src/api/trpc/trpc.context.ts +0 -258
  174. package/src/api/trpc/trpc.middleware.ts +0 -146
  175. package/src/api/trpc/trpc.router.ts +0 -389
  176. package/src/auth/session-cookie.ts +0 -54
  177. package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
  178. package/src/boot/boot-config.ts +0 -259
  179. package/src/boot/integration-id-backfill.ts +0 -109
  180. package/src/boot/post-boot.service.ts +0 -105
  181. package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
  182. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
  183. package/src/core/addon/addon-call-gateway.ts +0 -171
  184. package/src/core/addon/addon-package.service.ts +0 -1787
  185. package/src/core/addon/addon-registry.service.ts +0 -3130
  186. package/src/core/addon/addon-search.service.ts +0 -91
  187. package/src/core/addon/addon-settings-provider.ts +0 -220
  188. package/src/core/addon/addon.tokens.ts +0 -2
  189. package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
  190. package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
  191. package/src/core/addon-pages/addon-pages.service.ts +0 -82
  192. package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
  193. package/src/core/agent/agent-registry.service.ts +0 -529
  194. package/src/core/auth/auth.service.spec.ts +0 -86
  195. package/src/core/auth/auth.service.ts +0 -8
  196. package/src/core/capability/capability.service.ts +0 -66
  197. package/src/core/config/config.schema.ts +0 -3
  198. package/src/core/config/config.service.spec.ts +0 -175
  199. package/src/core/config/config.service.ts +0 -7
  200. package/src/core/events/event-bus.service.spec.ts +0 -235
  201. package/src/core/events/event-bus.service.ts +0 -89
  202. package/src/core/feature/feature.service.spec.ts +0 -99
  203. package/src/core/feature/feature.service.ts +0 -8
  204. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
  205. package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
  206. package/src/core/logging/log-ring-buffer.ts +0 -3
  207. package/src/core/logging/logging.service.spec.ts +0 -287
  208. package/src/core/logging/logging.service.ts +0 -143
  209. package/src/core/logging/scoped-logger.ts +0 -3
  210. package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
  211. package/src/core/moleculer/cap-call-fn.ts +0 -107
  212. package/src/core/moleculer/cap-route-authority.ts +0 -194
  213. package/src/core/moleculer/moleculer.service.ts +0 -1072
  214. package/src/core/network/network-quality.service.spec.ts +0 -53
  215. package/src/core/network/network-quality.service.ts +0 -5
  216. package/src/core/notification/notification-wrapper.service.ts +0 -34
  217. package/src/core/notification/toast-wrapper.service.ts +0 -27
  218. package/src/core/provider/provider.tokens.ts +0 -1
  219. package/src/core/repl/repl-engine.service.spec.ts +0 -444
  220. package/src/core/repl/repl-engine.service.ts +0 -155
  221. package/src/core/storage/fs-storage-backend.spec.ts +0 -70
  222. package/src/core/storage/fs-storage-backend.ts +0 -3
  223. package/src/core/storage/storage-location-manager.spec.ts +0 -130
  224. package/src/core/storage/storage-location-manager.ts +0 -3
  225. package/src/core/storage/storage.service.spec.ts +0 -73
  226. package/src/core/storage/storage.service.ts +0 -3
  227. package/src/core/streaming/stream-probe.service.ts +0 -221
  228. package/src/core/topology/topology-emitter.service.ts +0 -105
  229. package/src/launcher.ts +0 -314
  230. package/src/main.ts +0 -1245
  231. package/src/manual-boot.ts +0 -301
  232. package/tsconfig.build.json +0 -8
  233. package/tsconfig.json +0 -33
  234. package/vitest.config.ts +0 -26
@@ -1,105 +0,0 @@
1
- /**
2
- * TopologyEmitterService — push the cluster topology over the event bus.
3
- *
4
- * Subscribes to agent connect/disconnect + addon lifecycle events,
5
- * recomputes the topology snapshot on each change (debounced), and
6
- * emits `cluster.topology-snapshot`. Also fires a periodic safety-net
7
- * snapshot every TOPOLOGY_HEARTBEAT_MS so consumers that boot mid-
8
- * stream get a fresh payload without waiting for the next lifecycle
9
- * change.
10
- *
11
- * Replaces UI polling on `nodes.topology`. Admin-ui dashboards
12
- * subscribe to the snapshot category and read state directly from the
13
- * payload — zero round-trip.
14
- */
15
- import { randomUUID } from 'node:crypto'
16
- import { EventCategory } from '@camstack/types'
17
-
18
- type Unsubscribe = () => void
19
- import type { EventBusService } from '../events/event-bus.service'
20
- import type { AgentRegistryService } from '../agent/agent-registry.service'
21
- import type { AddonRegistryService } from '../addon/addon-registry.service'
22
- import { computeTopology } from '../../api/core/cap-providers'
23
-
24
- const DEBOUNCE_MS = 200
25
- const HEARTBEAT_MS = 30_000
26
-
27
- const LIFECYCLE_CATEGORIES: readonly EventCategory[] = [
28
- EventCategory.AgentOnline,
29
- EventCategory.AgentOffline,
30
- EventCategory.WorkerOnline,
31
- EventCategory.WorkerOffline,
32
- EventCategory.AgentUnregistered,
33
- EventCategory.AddonStarted,
34
- EventCategory.AddonStopped,
35
- EventCategory.AddonRestarted,
36
- EventCategory.AddonInstalled,
37
- EventCategory.AddonUninstalled,
38
- EventCategory.AddonUpdated,
39
- EventCategory.AddonCrashed,
40
- ]
41
-
42
- export class TopologyEmitterService {
43
- private readonly unsubscribers: Unsubscribe[] = []
44
- private debounceTimer: ReturnType<typeof setTimeout> | null = null
45
- private heartbeatTimer: ReturnType<typeof setInterval> | null = null
46
- private emitting = false
47
-
48
- constructor(
49
- private readonly eventBus: EventBusService,
50
- private readonly agentRegistry: AgentRegistryService,
51
- private readonly addonRegistry: AddonRegistryService,
52
- ) {}
53
-
54
- onModuleInit(): void {
55
- for (const category of LIFECYCLE_CATEGORIES) {
56
- const unsub = this.eventBus.subscribe({ category }, () => this.scheduleEmit())
57
- this.unsubscribers.push(unsub)
58
- }
59
- // Heartbeat fires the first emit ~immediately so consumers that
60
- // mount before any lifecycle change still see a snapshot.
61
- void this.emitNow()
62
- this.heartbeatTimer = setInterval(() => this.scheduleEmit(), HEARTBEAT_MS)
63
- }
64
-
65
- onModuleDestroy(): void {
66
- if (this.debounceTimer) clearTimeout(this.debounceTimer)
67
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)
68
- for (const unsub of this.unsubscribers) {
69
- try {
70
- unsub()
71
- } catch {
72
- /* idempotent */
73
- }
74
- }
75
- this.unsubscribers.length = 0
76
- }
77
-
78
- private scheduleEmit(): void {
79
- if (this.debounceTimer) clearTimeout(this.debounceTimer)
80
- this.debounceTimer = setTimeout(() => {
81
- this.debounceTimer = null
82
- void this.emitNow()
83
- }, DEBOUNCE_MS)
84
- }
85
-
86
- private async emitNow(): Promise<void> {
87
- if (this.emitting) return
88
- this.emitting = true
89
- try {
90
- const nodes = await computeTopology(this.agentRegistry, this.addonRegistry)
91
- this.eventBus.emit({
92
- id: randomUUID(),
93
- timestamp: new Date(),
94
- source: { type: 'core', id: 'topology-emitter' },
95
- category: EventCategory.ClusterTopologySnapshot,
96
- data: { nodes, timestamp: Date.now() },
97
- })
98
- } catch {
99
- // Best-effort observability path. Next lifecycle event or the
100
- // heartbeat will retry — no need to log here.
101
- } finally {
102
- this.emitting = false
103
- }
104
- }
105
- }
package/src/launcher.ts DELETED
@@ -1,314 +0,0 @@
1
- /**
2
- * Launcher -- entry point that ensures required addons are installed
3
- * before main.ts loads. This solves the chicken-and-egg problem:
4
- * main.ts has static imports from @camstack/core, but core lives in
5
- * data/addons/ and may not exist on first boot.
6
- *
7
- * 1. Run AddonInstaller.ensureRequiredPackages (zero core dependencies)
8
- * 2. Create symlinks so @camstack/core resolves from data/addons/
9
- * 3. Dynamically import main.ts (which has static @camstack/core imports)
10
- */
11
- import * as fs from 'node:fs'
12
- import * as path from 'node:path'
13
- import * as tar from 'tar'
14
- import * as yaml from 'js-yaml'
15
- import { AddonInstaller, bootstrapSchema, detectWorkspacePackagesDir } from '@camstack/kernel'
16
-
17
- /** Path of the manifest file embedded inside every archive. */
18
- const ARCHIVE_MANIFEST_NAME = '.camstack-backup-manifest.json'
19
-
20
- /** Resolve the data directory from env or default */
21
- const DATA_DIR = process.env['CAMSTACK_DATA'] ?? 'camstack-data'
22
-
23
- const RESTORE_MARKER_FILE = '.pending-restore.json'
24
-
25
- /**
26
- * Apply a pending system-restore archive over `dataDir` if a marker is
27
- * present. Runs BEFORE the addon installer so the restored snapshot's
28
- * `addons/` folder is what `ensureRequiredPackages` picks up.
29
- *
30
- * Marker contract is owned by `SystemBackupService.scheduleRestoreMarker`
31
- * (packages/core/src/builtins/system-backup) — kept inline here so the
32
- * launcher remains zero-core-deps. Schema:
33
- * { archivePath: string, requestedAt: number, source: string }
34
- *
35
- * Failure modes:
36
- * - marker malformed → log + leave it in place (operator visibility)
37
- * - archive missing → log + clear marker (auto-recover from stale)
38
- * - tar fails → throw → launcher exits non-zero, marker preserved
39
- */
40
- async function applyPendingRestore(dataDir: string): Promise<void> {
41
- const markerPath = path.join(dataDir, RESTORE_MARKER_FILE)
42
- if (!fs.existsSync(markerPath)) return
43
-
44
- console.log(`[launcher] Pending restore marker found at ${markerPath}`)
45
- let archivePath: string | null = null
46
- let locations: string[] | null = null
47
- try {
48
- const raw = fs.readFileSync(markerPath, 'utf-8')
49
- const parsed: unknown = JSON.parse(raw)
50
- if (
51
- typeof parsed === 'object' &&
52
- parsed != null &&
53
- typeof (parsed as { archivePath?: unknown }).archivePath === 'string'
54
- ) {
55
- archivePath = (parsed as { archivePath: string }).archivePath
56
- }
57
- const locField = (parsed as { locations?: unknown }).locations
58
- if (Array.isArray(locField) && locField.every((l) => typeof l === 'string')) {
59
- locations = locField as string[]
60
- }
61
- } catch (err) {
62
- console.error('[launcher] Restore marker malformed — leaving in place:', err)
63
- return
64
- }
65
-
66
- if (archivePath == null) {
67
- console.error('[launcher] Restore marker missing archivePath — clearing')
68
- fs.rmSync(markerPath, { force: true })
69
- return
70
- }
71
-
72
- if (!fs.existsSync(archivePath)) {
73
- console.error(`[launcher] Restore archive ${archivePath} missing — clearing marker`)
74
- fs.rmSync(markerPath, { force: true })
75
- return
76
- }
77
-
78
- const scope = locations ? ` (only: ${locations.join(', ')})` : ''
79
- console.log(`[launcher] Restoring snapshot from ${archivePath} into ${dataDir}${scope}`)
80
- fs.mkdirSync(dataDir, { recursive: true })
81
-
82
- // Same filter logic SystemBackupService.extractArchive uses — kept
83
- // inline here so the launcher stays zero-core-deps. The shape is
84
- // straightforward enough to duplicate.
85
- const allowedPrefixes = locations
86
- ? locations.map((loc) => loc.replace(/^\.\//, '').replace(/\/+$/, ''))
87
- : null
88
- await tar.extract({
89
- file: archivePath,
90
- cwd: dataDir,
91
- filter: (p) => {
92
- if (p === ARCHIVE_MANIFEST_NAME || p === `./${ARCHIVE_MANIFEST_NAME}`) return false
93
- if (!allowedPrefixes) return true
94
- const normalized = p.replace(/^\.\//, '')
95
- return allowedPrefixes.some(
96
- (prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`),
97
- )
98
- },
99
- })
100
- fs.rmSync(markerPath, { force: true })
101
- console.log('[launcher] Restore complete; marker cleared')
102
- }
103
-
104
- /** Narrow `CAMSTACK_INSTALL_SOURCE` env into the typed union. */
105
- function parseInstallSource(value: string | undefined): 'npm' | 'local' | 'symlink' | undefined {
106
- if (value === 'npm' || value === 'local' || value === 'symlink') return value
107
- return undefined
108
- }
109
-
110
- /**
111
- * Locate + parse `config.yaml` relative to the data dir. Returns the
112
- * raw object on success, `null` when missing or unparseable. The
113
- * launcher uses this BEFORE @camstack/core is imported so it must stay
114
- * free of core deps — just js-yaml + the kernel's bootstrapSchema for
115
- * shape validation by the call site.
116
- */
117
- function readConfigYaml(dataDir: string): unknown {
118
- const candidates = [
119
- path.resolve(dataDir, '..', 'config.yaml'),
120
- path.resolve(dataDir, 'config.yaml'),
121
- path.resolve(process.cwd(), 'config.yaml'),
122
- ]
123
- const found = candidates.find((p) => fs.existsSync(p))
124
- if (!found) return null
125
- try {
126
- return yaml.load(fs.readFileSync(found, 'utf-8')) ?? {}
127
- } catch (err) {
128
- console.warn(
129
- `[launcher] Could not parse ${found}: ${err instanceof Error ? err.message : String(err)}`,
130
- )
131
- return null
132
- }
133
- }
134
-
135
- /**
136
- * Read `bootstrap.installSource` from `<dataDir>/config.yaml` if present.
137
- * Returns the configured value or `null` when absent.
138
- *
139
- * Production deployments should leave this unset (defaults to 'npm');
140
- * `symlink` is a dev-time convenience that points data/addons/<pkg>
141
- * directly at the workspace source dir — never set in shipped
142
- * containers or Electron bundles.
143
- */
144
- function readBootstrapInstallSource(dataDir: string): 'npm' | 'local' | 'symlink' | undefined {
145
- const raw = readConfigYaml(dataDir)
146
- if (raw === null) return undefined
147
- const validation = bootstrapSchema.safeParse(raw)
148
- if (!validation.success) return undefined
149
- return parseInstallSource(validation.data.bootstrap.installSource)
150
- }
151
-
152
- /**
153
- * Read `bootstrap.requiredAddons` from `<dataDir>/config.yaml` if present.
154
- * Returns the parsed array, an empty array when explicitly set to `[]`,
155
- * or `null` when the field is absent (caller should fall back to the
156
- * kernel's REQUIRED_PACKAGES constant).
157
- *
158
- * Inline YAML parse rather than going through ConfigManager: the
159
- * launcher runs before @camstack/core is imported and must stay free of
160
- * core dependencies. Just yaml.load + bootstrapSchema (kernel) for
161
- * shape validation.
162
- */
163
- function readBootstrapRequiredAddons(dataDir: string): readonly string[] | null {
164
- const raw = readConfigYaml(dataDir)
165
- if (raw === null) return null
166
-
167
- const validation = bootstrapSchema.safeParse(raw)
168
- if (!validation.success) {
169
- console.warn(
170
- `[launcher] config.yaml failed bootstrapSchema validation: ${validation.error.message}`,
171
- )
172
- return null
173
- }
174
-
175
- return validation.data.bootstrap.requiredAddons ?? null
176
- }
177
-
178
- async function launch(): Promise<void> {
179
- const dataDir = DATA_DIR
180
- const addonsDir = path.resolve(dataDir, 'addons')
181
-
182
- // Restore must happen first so the snapshot's `addons/` is what the
183
- // installer + symlink step pick up — otherwise we'd install a stale
184
- // set and overwrite it a step later.
185
- await applyPendingRestore(dataDir)
186
-
187
- // Install source resolution:
188
- // 1. CAMSTACK_BUNDLED_ADDONS_DIR — set by Electron-packaged builds
189
- // to <resourcesPath>/addons. Pre-built addons ship with the
190
- // bundle, copied at first launch (`'local'` mode).
191
- // 2. CAMSTACK_INSTALL_SOURCE=local — explicit opt-in to copy from
192
- // the monorepo workspace (used by e2e + dev bootstrap). Hub
193
- // auto-detects `packages/` via detectWorkspacePackagesDir.
194
- // 3. Otherwise: 'npm' from the registry. Local development pushes
195
- // via `camstack deploy` (CLI tarball upload), bypassing this
196
- // bootstrap entirely.
197
- console.log('[launcher] Checking required packages...')
198
- // Install source resolution:
199
- // CAMSTACK_INSTALL_SOURCE env: 'npm' | 'local' | 'symlink' | undefined.
200
- // CAMSTACK_BUNDLED_ADDONS_DIR: Electron path, forces 'local'.
201
- // bootstrap.installSource (config.yaml): same set as env. Env wins.
202
- const rawEnvSource = process.env['CAMSTACK_INSTALL_SOURCE']
203
- const yamlSource = readBootstrapInstallSource(dataDir)
204
- const explicitSource = parseInstallSource(rawEnvSource) ?? yamlSource
205
- const bundledDir = process.env['CAMSTACK_BUNDLED_ADDONS_DIR']
206
- let workspaceDir: string | null = null
207
- let resolvedSource: 'npm' | 'local' | 'symlink' | undefined = explicitSource
208
- if (bundledDir && fs.existsSync(bundledDir)) {
209
- workspaceDir = bundledDir
210
- resolvedSource = 'local'
211
- console.log(`[launcher] Using bundled addons from ${bundledDir}`)
212
- } else if (explicitSource === 'local' || explicitSource === 'symlink') {
213
- workspaceDir = detectWorkspacePackagesDir(__dirname)
214
- if (workspaceDir === null) {
215
- console.warn(
216
- `[launcher] installSource=${explicitSource} requested but no workspace ` +
217
- `packages/ dir found from ${__dirname} — falling back to 'npm'`,
218
- )
219
- resolvedSource = 'npm'
220
- }
221
- }
222
- const installer = new AddonInstaller({
223
- addonsDir,
224
- workspacePackagesDir: workspaceDir ?? undefined,
225
- installSource: resolvedSource,
226
- })
227
-
228
- // bootstrap.requiredAddons from config.yaml — if set, REPLACES the
229
- // kernel's hard-coded REQUIRED_PACKAGES. The kernel never reaches into
230
- // SQL before this list is installed; everything here must be a known
231
- // package name that can be resolved from the workspace, the bundle, or
232
- // npm. Default (undefined) → fall back to the in-kernel constant.
233
- const bootstrapRequired = readBootstrapRequiredAddons(dataDir)
234
- if (bootstrapRequired !== null) {
235
- console.log(
236
- `[launcher] bootstrap.requiredAddons from config.yaml: ${bootstrapRequired.length} packages`,
237
- )
238
- await installer.ensureRequiredPackages(bootstrapRequired)
239
- } else {
240
- await installer.ensureRequiredPackages()
241
- }
242
-
243
- // Self-contained addon bundles (build preset `self-contained`) inline
244
- // @camstack/types + zod + @camstack/sdk into each addon's dist. The
245
- // hub no longer plants peer-dep symlinks under
246
- // addonsDir/node_modules/@camstack/ — addons resolve everything from
247
- // their own bundle. Kept the serverDir derivation in case future
248
- // tooling needs it (NODE_PATH extension below still uses it).
249
- const serverDir = path.resolve(__dirname, '..')
250
- const nodeModulesDir = path.join(serverDir, 'node_modules')
251
-
252
- // Extend Node's module resolution so addons in `data/addons/`
253
- // (which sit outside the server tree on packaged builds — e.g.
254
- // `~/Library/Application Support/.../data/addons/`) can resolve
255
- // third-party peer deps from the hub's own `node_modules`.
256
- //
257
- // Why: an addon's runtime file `data/addons/@scope/<x>/dist/x.js`
258
- // walks UP looking for `node_modules/<pkg>`. With user-data
259
- // outside the workspace, the walk dead-ends well before hitting
260
- // the hub's tree. NODE_PATH instructs Node to ALSO check the
261
- // listed dirs after the regular walk fails.
262
- //
263
- // The workspace-root node_modules holds hoisted deps (zod, werift,
264
- // etc); the server-local node_modules has direct deps. Listing
265
- // both covers every realistic resolution.
266
- const workspaceRootNodeModules = path.resolve(serverDir, '..', '..', 'node_modules')
267
- const extraNodePaths = [nodeModulesDir, workspaceRootNodeModules].filter((p) => fs.existsSync(p))
268
- if (extraNodePaths.length > 0) {
269
- const sep = process.platform === 'win32' ? ';' : ':'
270
- process.env['NODE_PATH'] = process.env['NODE_PATH']
271
- ? `${process.env['NODE_PATH']}${sep}${extraNodePaths.join(sep)}`
272
- : extraNodePaths.join(sep)
273
- // Re-init Node's module path cache so the change takes effect for
274
- // subsequent require()/import() calls. Without this the resolver
275
- // already cached the empty list at startup.
276
- const Module = require('node:module') as { _initPaths: () => void }
277
- Module._initPaths()
278
- console.log(`[launcher] NODE_PATH extended with: ${extraNodePaths.join(sep)}`)
279
- }
280
-
281
- // Now safe to load main.ts (which has static imports from @camstack/core)
282
- console.log('[launcher] Starting server...')
283
- await import('./main')
284
- }
285
-
286
- // Signal handlers — forward to default exit so NestJS shutdown hooks
287
- // (Moleculer broker stopped() → kills spawned workers) run cleanly.
288
- // Without these, TTY close / `npm run serve` kill leaves workers orphaned.
289
- let shuttingDown = false
290
- const handleSignal = (signal: NodeJS.Signals) => {
291
- if (shuttingDown) return
292
- shuttingDown = true
293
- console.log(`[launcher] Received ${signal} — shutting down`)
294
- process.exit(0)
295
- }
296
- process.on('SIGHUP', handleSignal)
297
- process.on('SIGTERM', handleSignal)
298
- process.on('SIGINT', handleSignal)
299
-
300
- // Parent-death watchdog (macOS: no PR_SET_PDEATHSIG). If tsx watch / npm
301
- // get orphaned to init, self-destruct so NestJS shutdown hooks fire before
302
- // the process tree turns into a zombie.
303
- const initialPpid = process.ppid
304
- setInterval(() => {
305
- if (process.ppid === 1 && initialPpid !== 1) {
306
- console.error('[launcher] Parent died (reparented to init) — exiting')
307
- process.exit(0)
308
- }
309
- }, 2000).unref()
310
-
311
- launch().catch((err) => {
312
- console.error('[launcher] FATAL:', err)
313
- process.exit(1)
314
- })