@openscout/scout 0.2.72 → 0.2.73

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 (46) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +41 -7
  3. package/bin/scout +34 -0
  4. package/bin/scout.mjs +195 -18
  5. package/bin/scoutd +0 -0
  6. package/dist/client/apple-touch-icon.png +0 -0
  7. package/dist/client/assets/RepoDiffViewer-D8gg36W3.js +56 -0
  8. package/dist/client/assets/{addon-fit-C5ueiijZ.js → addon-fit-D4KdY-pj.js} +1 -1
  9. package/dist/client/assets/{addon-webgl-Cm7sl8RO.js → addon-webgl-Bo_tmKBp.js} +1 -1
  10. package/dist/client/assets/arc.es-D0gujIKy.js +188 -0
  11. package/dist/client/assets/embed-entry-CUVNwyB_.js +1 -0
  12. package/dist/client/assets/index-CRkjiso5.js +257 -0
  13. package/dist/client/assets/index-D_RX2cRb.css +1 -0
  14. package/dist/client/assets/{xterm-FFux-PdF.js → xterm-RfflDFod.js} +1 -1
  15. package/dist/client/favicon-16.png +0 -0
  16. package/dist/client/favicon-32.png +0 -0
  17. package/dist/client/favicon.ico +0 -0
  18. package/dist/client/favicon.svg +48 -0
  19. package/dist/client/index.html +8 -2
  20. package/dist/client/openscout-icon.png +0 -0
  21. package/dist/client/site.webmanifest +19 -0
  22. package/dist/client/web-app-icon-192.png +0 -0
  23. package/dist/client/web-app-icon-512.png +0 -0
  24. package/dist/drizzle/0000_curly_iron_monger.sql +592 -0
  25. package/dist/drizzle/0001_invocation-status-columns.sql +7 -0
  26. package/dist/drizzle/0002_invocation-flight-metadata.sql +21 -0
  27. package/dist/drizzle/README.md +81 -0
  28. package/dist/drizzle/meta/0000_snapshot.json +4575 -0
  29. package/dist/drizzle/meta/0001_snapshot.json +4624 -0
  30. package/dist/drizzle/meta/0002_snapshot.json +4631 -0
  31. package/dist/drizzle/meta/_journal.json +27 -0
  32. package/dist/main.mjs +72583 -60970
  33. package/dist/node/main.mjs +7607 -0
  34. package/dist/openscout-terminal-relay.mjs +3819 -158
  35. package/dist/{pair-supervisor.mjs → pairing-runtime-controller.mjs} +51525 -46801
  36. package/dist/runtime/base-daemon.mjs +2153 -502
  37. package/dist/runtime/broker-daemon.mjs +34730 -27036
  38. package/dist/runtime/broker-process-manager.mjs +2038 -421
  39. package/dist/runtime/mesh-discover.mjs +2002 -420
  40. package/dist/scout-control-plane-web.mjs +54522 -39921
  41. package/dist/scout-web-server.mjs +54522 -39921
  42. package/dist/statusline.mjs +287 -0
  43. package/package.json +7 -3
  44. package/dist/client/assets/RepoDiffViewer-DRzXs-TP.js +0 -1
  45. package/dist/client/assets/index-CYs6TkOv.js +0 -191
  46. package/dist/client/assets/index-n_fpvwq8.css +0 -1
@@ -1,19 +1,20 @@
1
1
  // @bun
2
2
  // packages/runtime/src/base-daemon.ts
3
- import { spawn, spawnSync as spawnSync2 } from "child_process";
4
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, openSync, writeFileSync as writeFileSync4 } from "fs";
5
- import { homedir as homedir5 } from "os";
6
- import { dirname as dirname4, join as join5, resolve as resolve3 } from "path";
3
+ import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
4
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, openSync, writeFileSync as writeFileSync2 } from "fs";
5
+ import { homedir as homedir6 } from "os";
6
+ import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
7
7
  import { fileURLToPath as fileURLToPath2 } from "url";
8
8
 
9
9
  // packages/runtime/src/broker-process-manager.ts
10
- import { spawnSync } from "child_process";
11
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
12
- import { homedir as homedir3 } from "os";
13
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
10
+ import { spawn as spawn2, spawnSync } from "child_process";
11
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
12
+ import { homedir as homedir5 } from "os";
13
+ import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
14
14
  import { fileURLToPath } from "url";
15
15
 
16
16
  // packages/runtime/src/broker-api.ts
17
+ import { existsSync } from "fs";
17
18
  import { request as httpRequest } from "http";
18
19
  var DEFAULT_BROKER_JSON_REQUEST_TIMEOUT_MS = 30000;
19
20
  function normalizeBaseUrl(baseUrl) {
@@ -62,13 +63,17 @@ async function requestBrokerOverHttp(baseUrl, path, options) {
62
63
  }
63
64
  function shouldFallbackFromUnixSocket(error) {
64
65
  const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
65
- return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "FailedToOpenSocket";
66
+ return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "EINVAL" || code === "FailedToOpenSocket";
66
67
  }
67
68
  function errorMessage(error) {
68
69
  return error instanceof Error ? error.message : String(error);
69
70
  }
70
71
  function requestBrokerOverUnixSocket(socketPath, baseUrl, path, options) {
71
72
  return new Promise((resolve, reject) => {
73
+ if (!existsSync(socketPath)) {
74
+ reject(Object.assign(new Error(`Scout broker socket does not exist: ${socketPath}`), { code: "ENOENT" }));
75
+ return;
76
+ }
72
77
  const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
73
78
  const url = new URL(path, normalizedBaseUrl);
74
79
  const body = requestBody(options);
@@ -180,7 +185,6 @@ async function requestScoutBrokerJsonWithTrace(baseUrl, path, options = {}) {
180
185
  }
181
186
 
182
187
  // packages/runtime/src/support-paths.ts
183
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
184
188
  import { homedir } from "os";
185
189
  import { join } from "path";
186
190
  var OPENSCOUT_RPC_CUTOVER_MARKER = "rpc-runtime-cutover-v1";
@@ -204,6 +208,7 @@ function resolveOpenScoutSupportPaths() {
204
208
  harnessCatalogPath: join(catalogDirectory, "harness-catalog.json"),
205
209
  relayAgentsRegistryPath: join(supportDirectory, "relay-agents.json"),
206
210
  managedInstallsPath: join(supportDirectory, "managed-installs.json"),
211
+ hostInfoPath: join(supportDirectory, ".host-info"),
207
212
  relayHubDirectory: process.env.OPENSCOUT_RELAY_HUB ?? join(home, ".openscout", "relay"),
208
213
  controlHome,
209
214
  knowledgeDirectory,
@@ -214,35 +219,1552 @@ function resolveOpenScoutSupportPaths() {
214
219
  cutoverMarkerPath: join(supportDirectory, OPENSCOUT_RPC_CUTOVER_MARKER)
215
220
  };
216
221
  }
217
- function ensureOpenScoutCleanSlateSync() {
218
- const paths = resolveOpenScoutSupportPaths();
219
- if (existsSync(paths.cutoverMarkerPath)) {
222
+
223
+ // packages/runtime/src/open-scout-network.ts
224
+ import { readFileSync } from "fs";
225
+ var DEFAULT_OPENSCOUT_NETWORK_RENDEZVOUS_URL = "https://mesh.oscout.net";
226
+ var DEFAULT_OPENSCOUT_NETWORK_PAIRING_RELAY_URL = "wss://mesh.oscout.net/v1/relay";
227
+ function defaultOpenScoutNetworkSettings() {
228
+ return {
229
+ discoveryEnabled: false,
230
+ rendezvousUrl: DEFAULT_OPENSCOUT_NETWORK_RENDEZVOUS_URL,
231
+ pairingRelayUrl: DEFAULT_OPENSCOUT_NETWORK_PAIRING_RELAY_URL,
232
+ keepPairingRelayRunning: true
233
+ };
234
+ }
235
+ function normalizeOpenScoutNetworkSettings(input) {
236
+ const base = defaultOpenScoutNetworkSettings();
237
+ const record = isRecord(input) ? input : {};
238
+ return {
239
+ discoveryEnabled: typeof record.discoveryEnabled === "boolean" ? record.discoveryEnabled : base.discoveryEnabled,
240
+ rendezvousUrl: normalizeUrlString(record.rendezvousUrl, base.rendezvousUrl),
241
+ pairingRelayUrl: normalizeUrlString(record.pairingRelayUrl, base.pairingRelayUrl),
242
+ keepPairingRelayRunning: typeof record.keepPairingRelayRunning === "boolean" ? record.keepPairingRelayRunning : base.keepPairingRelayRunning
243
+ };
244
+ }
245
+ function readOpenScoutNetworkSettingsSync() {
246
+ try {
247
+ const raw = readFileSync(resolveOpenScoutSupportPaths().settingsPath, "utf8");
248
+ const parsed = JSON.parse(raw);
249
+ const network = isRecord(parsed) && isRecord(parsed.network) ? parsed.network : {};
250
+ return normalizeOpenScoutNetworkSettings(isRecord(network.openScoutNetwork) ? network.openScoutNetwork : {});
251
+ } catch {
252
+ return defaultOpenScoutNetworkSettings();
253
+ }
254
+ }
255
+ function openScoutNetworkDiscoveryEnabled(env = process.env) {
256
+ const explicit = readBooleanEnv(env.OPENSCOUT_NETWORK_DISCOVERY_ENABLED) ?? readBooleanEnv(env.OPENSCOUT_OSN_DISCOVERY_ENABLED);
257
+ if (explicit !== undefined) {
258
+ return explicit;
259
+ }
260
+ return readOpenScoutNetworkSettingsSync().discoveryEnabled;
261
+ }
262
+ function openScoutNetworkServiceEnvironment(env = process.env) {
263
+ const settings = readOpenScoutNetworkSettingsSync();
264
+ if (!settings.discoveryEnabled) {
265
+ return {};
266
+ }
267
+ const next = {};
268
+ if (!hasEnv(env, "OPENSCOUT_MESH_RENDEZVOUS_URL")) {
269
+ next.OPENSCOUT_MESH_RENDEZVOUS_URL = settings.rendezvousUrl;
270
+ }
271
+ if (!hasEnv(env, "OPENSCOUT_PAIRING_RELAY_URL") && !hasEnv(env, "OPENSCOUT_MOBILE_PAIRING_RELAY_URL")) {
272
+ next.OPENSCOUT_PAIRING_RELAY_URL = settings.pairingRelayUrl;
273
+ }
274
+ return next;
275
+ }
276
+ function normalizeUrlString(value, fallback) {
277
+ if (typeof value !== "string") {
278
+ return fallback;
279
+ }
280
+ const trimmed = value.trim().replace(/\/$/, "");
281
+ return trimmed || fallback;
282
+ }
283
+ function readBooleanEnv(value) {
284
+ const normalized = value?.trim().toLowerCase();
285
+ if (!normalized)
286
+ return;
287
+ if (["1", "true", "yes", "on"].includes(normalized))
288
+ return true;
289
+ if (["0", "false", "no", "off"].includes(normalized))
290
+ return false;
291
+ return;
292
+ }
293
+ function hasEnv(env, key) {
294
+ return typeof env[key] === "string" && env[key].trim().length > 0;
295
+ }
296
+ function isRecord(value) {
297
+ return typeof value === "object" && value !== null && !Array.isArray(value);
298
+ }
299
+
300
+ // packages/runtime/src/tailscale.ts
301
+ import { readFileSync as readFileSync2 } from "fs";
302
+ import { execFileSync } from "child_process";
303
+
304
+ // packages/runtime/src/system-probes/tailscale-status.ts
305
+ import { readFile } from "fs/promises";
306
+
307
+ // packages/runtime/src/system-probes/registry.ts
308
+ var DEFAULT_MIN_MAX_STALE_MS = 2 * 60000;
309
+ var PROBE_RUN_OUTPUT = Symbol("openscout.probeRunOutput");
310
+ var registeredProbes = [];
311
+
312
+ class ProbeBackendError extends Error {
313
+ backend;
314
+ fallbackSince;
315
+ fallbackReason;
316
+ code;
317
+ timedOut;
318
+ constructor(message, metadata, cause) {
319
+ super(message);
320
+ this.name = "ProbeBackendError";
321
+ this.backend = metadata.backend;
322
+ this.fallbackSince = metadata.fallbackSince;
323
+ this.fallbackReason = metadata.fallbackReason;
324
+ const details = probeErrorDetails(cause);
325
+ if (details.code) {
326
+ this.code = details.code;
327
+ }
328
+ if (details.timedOut !== undefined) {
329
+ this.timedOut = details.timedOut;
330
+ }
331
+ }
332
+ }
333
+ function probeRunOutput(value, metadata) {
334
+ return {
335
+ [PROBE_RUN_OUTPUT]: true,
336
+ value,
337
+ ...metadata
338
+ };
339
+ }
340
+ function isProbeRunOutput(value) {
341
+ return typeof value === "object" && value !== null && value[PROBE_RUN_OUTPUT] === true;
342
+ }
343
+ function backendMetadataFromError(error) {
344
+ if (error instanceof ProbeBackendError) {
345
+ return {
346
+ backend: error.backend,
347
+ fallbackSince: error.fallbackSince,
348
+ fallbackReason: error.fallbackReason
349
+ };
350
+ }
351
+ if (typeof error === "object" && error !== null) {
352
+ const record = error;
353
+ if (record.backend === "local" || record.backend === "scoutd" || record.backend === "local-fallback") {
354
+ return {
355
+ backend: record.backend,
356
+ fallbackSince: typeof record.fallbackSince === "number" ? record.fallbackSince : undefined,
357
+ fallbackReason: typeof record.fallbackReason === "string" ? record.fallbackReason : undefined
358
+ };
359
+ }
360
+ }
361
+ return null;
362
+ }
363
+ function probeErrorDetails(error) {
364
+ if (typeof error !== "object" || error === null) {
365
+ return {};
366
+ }
367
+ const record = error;
368
+ const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : undefined;
369
+ const timedOut = record.timedOut === true || code === "timeout" ? true : record.timedOut === false ? false : undefined;
370
+ return { code, timedOut };
371
+ }
372
+ function assertProbeSpec(spec) {
373
+ if (!spec.id.trim()) {
374
+ throw new Error("Probe id is required");
375
+ }
376
+ if (!Number.isFinite(spec.ttlMs) || spec.ttlMs <= 0) {
377
+ throw new Error(`Probe ${spec.id} must declare a positive ttlMs`);
378
+ }
379
+ if (!Number.isFinite(spec.timeoutMs) || spec.timeoutMs <= 0) {
380
+ throw new Error(`Probe ${spec.id} must declare a positive timeoutMs`);
381
+ }
382
+ if (spec.maxStaleMs !== undefined && (!Number.isFinite(spec.maxStaleMs) || spec.maxStaleMs <= 0)) {
383
+ throw new Error(`Probe ${spec.id} maxStaleMs must be positive when set`);
384
+ }
385
+ }
386
+ function maxStaleMsFor(spec) {
387
+ return spec.maxStaleMs ?? Math.max(DEFAULT_MIN_MAX_STALE_MS, spec.ttlMs * 10);
388
+ }
389
+ function failureBackoffMs(consecutiveFailures) {
390
+ if (consecutiveFailures <= 0) {
391
+ return 0;
392
+ }
393
+ return Math.min(30000, 1000 * 2 ** Math.min(consecutiveFailures - 1, 5));
394
+ }
395
+ function probeErrorFromUnknown(error, at, timedOut = false) {
396
+ if (typeof error === "object" && error !== null) {
397
+ const record = error;
398
+ const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : timedOut ? "timeout" : typeof record.name === "string" && record.name.trim() ? record.name.trim() : "error";
399
+ const message = typeof record.message === "string" && record.message.trim() ? record.message : String(error);
400
+ return { code, message, at, ...timedOut ? { timedOut: true } : {} };
401
+ }
402
+ return {
403
+ code: timedOut ? "timeout" : "error",
404
+ message: String(error),
405
+ at,
406
+ ...timedOut ? { timedOut: true } : {}
407
+ };
408
+ }
409
+ function timeoutError(probeId, timeoutMs) {
410
+ return {
411
+ code: "timeout",
412
+ message: `Probe ${probeId} timed out after ${timeoutMs}ms`,
413
+ at: Date.now(),
414
+ timedOut: true
415
+ };
416
+ }
417
+ function staleTooLongError(probeId, ageMs, maxStaleMs) {
418
+ return {
419
+ code: "max_stale_exceeded",
420
+ message: `Probe ${probeId} last good snapshot is ${ageMs}ms old, exceeding maxStaleMs ${maxStaleMs}`,
421
+ at: Date.now()
422
+ };
423
+ }
424
+ function isFreshEnough(state, maxAgeMs) {
425
+ if (state.at === null || state.invalidated) {
426
+ return false;
427
+ }
428
+ return Date.now() - state.at <= maxAgeMs;
429
+ }
430
+ function logBackendTransition(input) {
431
+ if (input.from === input.to) {
220
432
  return;
221
433
  }
222
- rmSync(paths.supportDirectory, { recursive: true, force: true });
223
- rmSync(paths.controlHome, { recursive: true, force: true });
224
- rmSync(paths.relayHubDirectory, { recursive: true, force: true });
225
- mkdirSync(paths.supportDirectory, { recursive: true });
226
- writeFileSync(paths.cutoverMarkerPath, `${Date.now()}
227
- `, "utf8");
434
+ const keySuffix = input.key === undefined ? "" : ` key=${JSON.stringify(input.key)}`;
435
+ const reasonSuffix = input.reason ? ` (${input.reason})` : "";
436
+ console.warn(`[openscout] system probe ${input.id}${keySuffix} backend ${input.from} -> ${input.to}${reasonSuffix}`);
228
437
  }
229
438
 
230
- // packages/runtime/src/tool-resolution.ts
231
- import { accessSync, constants, existsSync as existsSync2 } from "fs";
439
+ class ProbeInstance {
440
+ spec;
441
+ key;
442
+ runProbe;
443
+ scheduleRun;
444
+ state = {
445
+ value: null,
446
+ at: null,
447
+ error: null,
448
+ consecutiveFailures: 0,
449
+ inFlight: null,
450
+ invalidated: false,
451
+ invalidationReason: null,
452
+ invalidationSerial: 0,
453
+ nextRetryAt: 0,
454
+ backend: "local",
455
+ fallbackSince: null,
456
+ fallbackReason: null
457
+ };
458
+ metricState;
459
+ lastAccessAt = Date.now();
460
+ constructor(options) {
461
+ this.spec = options.spec;
462
+ this.key = options.key;
463
+ this.runProbe = options.run;
464
+ this.scheduleRun = options.scheduleRun ?? ((task) => task());
465
+ this.metricState = {
466
+ id: options.spec.id,
467
+ ...options.key !== undefined ? { key: options.key } : {},
468
+ backend: "local",
469
+ runCount: 0,
470
+ failureCount: 0,
471
+ timeoutCount: 0,
472
+ staleServedCount: 0,
473
+ lastRunAt: null,
474
+ lastDurationMs: null,
475
+ lastSuccessAt: null
476
+ };
477
+ }
478
+ read() {
479
+ this.touch();
480
+ if (this.shouldRefreshForRead(Date.now())) {
481
+ this.ensureRefresh(false, this.spec.ttlMs);
482
+ }
483
+ const snap = this.snapshot();
484
+ if (snap.status === "stale" || snap.status === "failed" && snap.at !== null) {
485
+ this.metricState.staleServedCount += 1;
486
+ }
487
+ return snap;
488
+ }
489
+ async fresh(options = {}) {
490
+ this.touch();
491
+ const maxAgeMs = options.maxAgeMs ?? this.spec.ttlMs;
492
+ for (let attempt = 0;attempt < 3; attempt += 1) {
493
+ if (isFreshEnough(this.state, maxAgeMs)) {
494
+ return this.snapshot();
495
+ }
496
+ await this.ensureRefresh(true, maxAgeMs);
497
+ if (!this.state.invalidated) {
498
+ return this.snapshot();
499
+ }
500
+ }
501
+ return this.snapshot();
502
+ }
503
+ snapshot() {
504
+ const now = Date.now();
505
+ const ageMs = this.state.at === null ? null : Math.max(0, now - this.state.at);
506
+ const maxStaleMs = maxStaleMsFor(this.spec);
507
+ const exceededMaxStale = ageMs !== null && ageMs > maxStaleMs;
508
+ const fresh = ageMs !== null && !this.state.invalidated && ageMs <= this.spec.ttlMs;
509
+ const status = this.state.at === null ? this.state.error ? "failed" : "empty" : exceededMaxStale ? "failed" : fresh ? "fresh" : "stale";
510
+ const error = exceededMaxStale ? this.state.error ?? staleTooLongError(this.spec.id, ageMs ?? 0, maxStaleMs) : this.state.error;
511
+ return {
512
+ id: this.spec.id,
513
+ ...this.key !== undefined ? { key: this.key } : {},
514
+ value: status === "failed" && exceededMaxStale ? null : this.state.value,
515
+ at: this.state.at,
516
+ ageMs,
517
+ stale: status === "stale" || status === "failed" && this.state.at !== null,
518
+ refreshing: this.state.inFlight !== null,
519
+ status,
520
+ error,
521
+ consecutiveFailures: this.state.consecutiveFailures,
522
+ backend: this.state.backend,
523
+ ...this.state.fallbackSince !== null ? { fallbackSince: this.state.fallbackSince } : {},
524
+ ...this.state.fallbackReason !== null ? { fallbackReason: this.state.fallbackReason } : {}
525
+ };
526
+ }
527
+ invalidate(reason) {
528
+ this.touch();
529
+ this.state.invalidated = true;
530
+ this.state.invalidationReason = reason ?? null;
531
+ this.state.invalidationSerial += 1;
532
+ this.state.nextRetryAt = 0;
533
+ }
534
+ metrics() {
535
+ return {
536
+ ...this.metricState,
537
+ consecutiveFailures: this.state.consecutiveFailures,
538
+ inFlight: this.state.inFlight !== null,
539
+ ...this.state.fallbackSince !== null ? { fallbackSince: this.state.fallbackSince } : {},
540
+ ...this.state.fallbackReason !== null ? { fallbackReason: this.state.fallbackReason } : {}
541
+ };
542
+ }
543
+ isRefreshing() {
544
+ return this.state.inFlight !== null;
545
+ }
546
+ touch() {
547
+ this.lastAccessAt = Date.now();
548
+ }
549
+ shouldRefreshForRead(now) {
550
+ if (this.state.inFlight !== null) {
551
+ return false;
552
+ }
553
+ if (this.state.nextRetryAt > now) {
554
+ return false;
555
+ }
556
+ if (this.state.at === null) {
557
+ return true;
558
+ }
559
+ if (this.state.invalidated) {
560
+ return true;
561
+ }
562
+ return now - this.state.at > this.spec.ttlMs;
563
+ }
564
+ ensureRefresh(force, maxAgeMs) {
565
+ if (this.state.inFlight) {
566
+ return this.state.inFlight;
567
+ }
568
+ const now = Date.now();
569
+ if (!force && this.state.nextRetryAt > now) {
570
+ return Promise.resolve();
571
+ }
572
+ const inFlight = this.scheduleRun(() => this.executeRun(maxAgeMs)).finally(() => {
573
+ if (this.state.inFlight === inFlight) {
574
+ this.state.inFlight = null;
575
+ }
576
+ });
577
+ this.state.inFlight = inFlight;
578
+ return inFlight;
579
+ }
580
+ async executeRun(maxAgeMs) {
581
+ const startedAt = Date.now();
582
+ const previousBackend = this.state.backend;
583
+ const invalidationSerialAtStart = this.state.invalidationSerial;
584
+ const controller = new AbortController;
585
+ let timeout = null;
586
+ this.metricState.runCount += 1;
587
+ this.metricState.lastRunAt = startedAt;
588
+ const ctx = {
589
+ probeId: this.spec.id,
590
+ ...this.key !== undefined ? { key: this.key } : {},
591
+ signal: controller.signal,
592
+ timeoutMs: this.spec.timeoutMs,
593
+ maxAgeMs,
594
+ startedAt
595
+ };
596
+ const timeoutProbeError = timeoutError(this.spec.id, this.spec.timeoutMs);
597
+ const runPromise = this.runProbe(ctx);
598
+ runPromise.catch(() => {
599
+ return;
600
+ });
601
+ try {
602
+ const result = await new Promise((resolve, reject) => {
603
+ timeout = setTimeout(() => {
604
+ controller.abort(timeoutProbeError);
605
+ reject(timeoutProbeError);
606
+ }, this.spec.timeoutMs);
607
+ runPromise.then(resolve, reject);
608
+ });
609
+ const output = isProbeRunOutput(result) ? result : probeRunOutput(result, { backend: "local" });
610
+ this.state.value = output.value;
611
+ this.state.at = output.generatedAt ?? Date.now();
612
+ this.state.error = null;
613
+ this.state.consecutiveFailures = 0;
614
+ if (this.state.invalidationSerial === invalidationSerialAtStart) {
615
+ this.state.invalidated = false;
616
+ this.state.invalidationReason = null;
617
+ }
618
+ this.state.nextRetryAt = 0;
619
+ this.state.backend = output.backend;
620
+ this.state.fallbackSince = output.fallbackSince ?? null;
621
+ this.state.fallbackReason = output.fallbackReason ?? null;
622
+ this.metricState.backend = output.backend;
623
+ this.metricState.lastSuccessAt = this.state.at;
624
+ logBackendTransition({
625
+ id: this.spec.id,
626
+ key: this.key,
627
+ from: previousBackend,
628
+ to: output.backend,
629
+ reason: output.fallbackReason
630
+ });
631
+ } catch (error) {
632
+ const timedOut = error === timeoutProbeError || typeof error === "object" && error !== null && (error.code === "timeout" || error.timedOut === true);
633
+ const at = Date.now();
634
+ this.state.error = error === timeoutProbeError ? timeoutProbeError : probeErrorFromUnknown(error, at, timedOut);
635
+ this.state.consecutiveFailures += 1;
636
+ this.state.nextRetryAt = at + failureBackoffMs(this.state.consecutiveFailures);
637
+ const backendMetadata = backendMetadataFromError(error);
638
+ if (backendMetadata) {
639
+ this.state.backend = backendMetadata.backend;
640
+ this.state.fallbackSince = backendMetadata.fallbackSince ?? null;
641
+ this.state.fallbackReason = backendMetadata.fallbackReason ?? null;
642
+ this.metricState.backend = backendMetadata.backend;
643
+ logBackendTransition({
644
+ id: this.spec.id,
645
+ key: this.key,
646
+ from: previousBackend,
647
+ to: backendMetadata.backend,
648
+ reason: backendMetadata.fallbackReason
649
+ });
650
+ }
651
+ this.metricState.failureCount += 1;
652
+ if (this.state.error.timedOut || this.state.error.code === "timeout") {
653
+ this.metricState.timeoutCount += 1;
654
+ }
655
+ } finally {
656
+ if (timeout) {
657
+ clearTimeout(timeout);
658
+ }
659
+ this.metricState.lastDurationMs = Date.now() - startedAt;
660
+ }
661
+ }
662
+ }
663
+
664
+ class FamilyLimiter {
665
+ maxConcurrent;
666
+ active = 0;
667
+ queue = [];
668
+ constructor(maxConcurrent) {
669
+ this.maxConcurrent = maxConcurrent;
670
+ }
671
+ queued() {
672
+ return this.queue.length;
673
+ }
674
+ async run(task) {
675
+ if (this.active >= this.maxConcurrent) {
676
+ await new Promise((resolve) => this.queue.push(resolve));
677
+ }
678
+ this.active += 1;
679
+ try {
680
+ return await task();
681
+ } finally {
682
+ this.active -= 1;
683
+ const next = this.queue.shift();
684
+ if (next) {
685
+ next();
686
+ }
687
+ }
688
+ }
689
+ }
690
+
691
+ class ProbeFamily {
692
+ spec;
693
+ entries = new Map;
694
+ limiter;
695
+ constructor(spec) {
696
+ this.spec = spec;
697
+ assertProbeSpec(spec);
698
+ if (!Number.isInteger(spec.maxKeys) || spec.maxKeys <= 0) {
699
+ throw new Error(`Probe family ${spec.id} must declare a positive maxKeys`);
700
+ }
701
+ if (!Number.isFinite(spec.idleKeyTtlMs) || spec.idleKeyTtlMs <= 0) {
702
+ throw new Error(`Probe family ${spec.id} must declare a positive idleKeyTtlMs`);
703
+ }
704
+ if (!Number.isInteger(spec.maxConcurrentKeys) || spec.maxConcurrentKeys <= 0) {
705
+ throw new Error(`Probe family ${spec.id} must declare a positive maxConcurrentKeys`);
706
+ }
707
+ this.limiter = new FamilyLimiter(spec.maxConcurrentKeys);
708
+ }
709
+ for(rawKey) {
710
+ const key = this.normalize(rawKey);
711
+ this.cleanupIdle(Date.now());
712
+ let entry = this.entries.get(key);
713
+ if (!entry) {
714
+ entry = new ProbeInstance({
715
+ spec: this.spec,
716
+ key,
717
+ run: (ctx) => this.spec.run(key, ctx),
718
+ scheduleRun: (task) => this.limiter.run(task)
719
+ });
720
+ this.entries.set(key, entry);
721
+ }
722
+ entry.lastAccessAt = Date.now();
723
+ this.evictLruIfNeeded();
724
+ return entry;
725
+ }
726
+ snapshot(key) {
727
+ return this.for(key).snapshot();
728
+ }
729
+ invalidate(key, reason) {
730
+ this.for(key).invalidate(reason);
731
+ }
732
+ metrics() {
733
+ this.cleanupIdle(Date.now());
734
+ return {
735
+ id: this.spec.id,
736
+ keyCount: this.entries.size,
737
+ maxKeys: this.spec.maxKeys,
738
+ activeRuns: this.limiter.active,
739
+ queuedRuns: this.limiter.queued(),
740
+ keys: Array.from(this.entries.values(), (entry) => entry.metrics())
741
+ };
742
+ }
743
+ keys() {
744
+ this.cleanupIdle(Date.now());
745
+ return Array.from(this.entries.keys());
746
+ }
747
+ normalize(rawKey) {
748
+ const key = this.spec.normalizeKey(rawKey).trim();
749
+ if (!key) {
750
+ throw new Error(`Probe family ${this.spec.id} normalized an empty key`);
751
+ }
752
+ return key;
753
+ }
754
+ cleanupIdle(now) {
755
+ for (const [key, entry] of this.entries) {
756
+ if (!entry.isRefreshing() && now - entry.lastAccessAt > this.spec.idleKeyTtlMs) {
757
+ this.entries.delete(key);
758
+ }
759
+ }
760
+ }
761
+ evictLruIfNeeded() {
762
+ while (this.entries.size > this.spec.maxKeys) {
763
+ let oldestKey = null;
764
+ let oldestAccess = Number.POSITIVE_INFINITY;
765
+ for (const [key, entry] of this.entries) {
766
+ if (entry.isRefreshing()) {
767
+ continue;
768
+ }
769
+ if (entry.lastAccessAt < oldestAccess) {
770
+ oldestAccess = entry.lastAccessAt;
771
+ oldestKey = key;
772
+ }
773
+ }
774
+ if (!oldestKey) {
775
+ return;
776
+ }
777
+ this.entries.delete(oldestKey);
778
+ }
779
+ }
780
+ }
781
+ function defineProbe(spec) {
782
+ assertProbeSpec(spec);
783
+ const handle = new ProbeInstance({
784
+ spec,
785
+ run: spec.run
786
+ });
787
+ registeredProbes.push({
788
+ kind: "probe",
789
+ id: spec.id,
790
+ handle
791
+ });
792
+ return handle;
793
+ }
794
+
795
+ // packages/runtime/src/system-probes/exec.ts
796
+ import { spawn } from "child_process";
797
+
798
+ // packages/runtime/src/system-probes/scoutd-client.ts
799
+ import { existsSync as existsSync2 } from "fs";
232
800
  import { homedir as homedir2 } from "os";
233
- import { basename, dirname, join as join2, resolve } from "path";
801
+ import { join as join2 } from "path";
802
+ import { Socket } from "net";
803
+
804
+ // packages/runtime/src/system-probes/scout-host-catalog.ts
805
+ var SCOUT_HOST_PROBE_SCHEMA_VERSIONS = {
806
+ "tailscale.status": 1,
807
+ "git.buildInfo": 1,
808
+ "git.revParse": 1,
809
+ "git.diffShortstat": 1,
810
+ "git.statusPorcelain": 1,
811
+ "git.mergeBase": 1,
812
+ "git.logLastCommitUnix": 1,
813
+ "git.worktreeListPorcelain": 1,
814
+ "tmux.sessions": 1,
815
+ "tmux.panes": 1,
816
+ "zellij.sessions": 1,
817
+ "ps.runtime": 1,
818
+ "ps.discovery": 1,
819
+ "ps.cwd": 1,
820
+ "net.listeners": 1,
821
+ "repo.scan": 1,
822
+ "repo.diff": 1
823
+ };
824
+ var SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS = {
825
+ "tmux.sendKeys": 1,
826
+ "tmux.sendKeysLiteral": 1,
827
+ "tmux.loadBuffer": 1,
828
+ "tmux.pasteBuffer": 1,
829
+ "tmux.deleteBuffer": 1,
830
+ "tmux.killSession": 1,
831
+ "tmux.newSession": 1,
832
+ "tmux.detachClient": 1,
833
+ "tailscale.cert": 1,
834
+ "reveal.open": 1
835
+ };
836
+ function expectedScoutHostProbeSchemaVersion(probeId) {
837
+ return Object.hasOwn(SCOUT_HOST_PROBE_SCHEMA_VERSIONS, probeId) ? SCOUT_HOST_PROBE_SCHEMA_VERSIONS[probeId] : null;
838
+ }
839
+ function expectedScoutHostExecVerbSchemaVersion(verb) {
840
+ return Object.hasOwn(SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS, verb) ? SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS[verb] : null;
841
+ }
842
+
843
+ // packages/runtime/src/system-probes/scoutd-client.ts
844
+ var CAPABILITIES_SCHEMA = "openscout.probe.capabilities/v1";
845
+ var REQUEST_SCHEMA = "openscout.probe.request/v1";
846
+ var SNAPSHOT_SCHEMA = "openscout.probe.snapshot/v1";
847
+ var ERROR_SCHEMA = "openscout.probe.error/v1";
848
+ var EXEC_REQUEST_SCHEMA = "openscout.exec.request/v1";
849
+ var EXEC_RESPONSE_SCHEMA = "openscout.exec.response/v1";
850
+ var CAPABILITY_RECHECK_MS = 1e4;
851
+ var SOCKET_TIMEOUT_MS = 900;
852
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
853
+ function readString(value) {
854
+ return typeof value === "string" && value.trim() ? value.trim() : null;
855
+ }
856
+ function readNumber(value) {
857
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
858
+ }
859
+ function isRecord2(value) {
860
+ return typeof value === "object" && value !== null && !Array.isArray(value);
861
+ }
862
+ function fallbackMessage(error) {
863
+ if (error instanceof Error && error.message.trim()) {
864
+ return error.message;
865
+ }
866
+ return String(error);
867
+ }
868
+
869
+ class ScoutdExecResponseError extends Error {
870
+ code;
871
+ timedOut;
872
+ constructor(message, options = {}) {
873
+ super(message);
874
+ this.name = "ScoutdExecResponseError";
875
+ this.code = options.code ?? "exec_error";
876
+ this.timedOut = options.timedOut ?? false;
877
+ }
878
+ }
879
+
880
+ class ScoutdProbeResponseError extends Error {
881
+ code;
882
+ timedOut;
883
+ constructor(message, options = {}) {
884
+ super(message);
885
+ this.name = "ScoutdProbeResponseError";
886
+ this.code = options.code ?? "probe_error";
887
+ this.timedOut = options.timedOut ?? false;
888
+ }
889
+ }
890
+ function resolveScoutdProbesSocketPath(env = process.env) {
891
+ const explicit = env.OPENSCOUT_PROBES_SOCKET?.trim();
892
+ if (explicit) {
893
+ return explicit;
894
+ }
895
+ const openScoutHome = env.OPENSCOUT_HOME?.trim() || join2(homedir2(), ".openscout");
896
+ return join2(openScoutHome, "run", "scoutd-probes.sock");
897
+ }
898
+
899
+ class ScoutdProbeClient {
900
+ env;
901
+ capabilities = null;
902
+ lastCapabilityCheckAt = null;
903
+ lastError = null;
904
+ daemonObserved = false;
905
+ fallbackByProbe = new Map;
906
+ fallbackByExec = new Map;
907
+ constructor(env = process.env) {
908
+ this.env = env;
909
+ }
910
+ async requestProbe(input) {
911
+ const socketPath = resolveScoutdProbesSocketPath(this.env);
912
+ const socketTimeoutMs = probeSocketTimeoutMs(input.timeoutMs);
913
+ const socketExists = existsSync2(socketPath);
914
+ if (!socketExists) {
915
+ this.capabilities = null;
916
+ this.lastError = null;
917
+ return this.daemonObserved ? this.fallback(input.probeId, input.key, `probe socket is missing: ${socketPath}`) : { state: "local" };
918
+ }
919
+ this.daemonObserved = true;
920
+ const capabilities = await this.ensureCapabilities(socketPath, socketTimeoutMs);
921
+ if (!capabilities) {
922
+ return this.fallback(input.probeId, input.key, this.lastError ?? "probe capabilities unavailable");
923
+ }
924
+ const capability = capabilities.families.get(input.probeId);
925
+ if (!capability) {
926
+ return this.fallback(input.probeId, input.key, `scoutd does not serve ${input.probeId}`);
927
+ }
928
+ const expectedSchemaVersion = expectedScoutHostProbeSchemaVersion(input.probeId);
929
+ if (expectedSchemaVersion === null) {
930
+ return this.fallback(input.probeId, input.key, `client has no compiled schema version for ${input.probeId}`);
931
+ }
932
+ if (capability.schemaVersion !== expectedSchemaVersion) {
933
+ return this.fallback(input.probeId, input.key, `scoutd serves ${input.probeId} schema v${capability.schemaVersion}, expected v${expectedSchemaVersion}`);
934
+ }
935
+ try {
936
+ const response = await requestJson(socketPath, {
937
+ schema: REQUEST_SCHEMA,
938
+ schemaVersion: expectedSchemaVersion,
939
+ probeId: input.probeId,
940
+ key: input.key ?? null,
941
+ maxAgeMs: input.maxAgeMs,
942
+ opTimeoutMs: requestOpTimeoutMs(input.timeoutMs)
943
+ }, { timeoutMs: socketTimeoutMs });
944
+ if (!isRecord2(response) || response.schema !== SNAPSHOT_SCHEMA) {
945
+ throw new Error("scoutd returned an invalid probe snapshot envelope");
946
+ }
947
+ if (response.error) {
948
+ const error = isRecord2(response.error) ? response.error : {};
949
+ const code = readString(error.code) ?? "probe_error";
950
+ const message = readString(error.message) ?? code;
951
+ const timedOut = error.timedOut === true || error.timed_out === true;
952
+ throw new ScoutdProbeResponseError(message, { code, timedOut });
953
+ }
954
+ this.fallbackByProbe.delete(fallbackKey(input.probeId, input.key));
955
+ this.lastError = null;
956
+ return {
957
+ state: "scoutd",
958
+ value: response.value,
959
+ generatedAt: readNumber(response.generatedAt) ?? Date.now(),
960
+ daemonVersion: readString(response.daemonVersion) ?? capabilities.daemonVersion
961
+ };
962
+ } catch (error) {
963
+ if (!(error instanceof ScoutdProbeResponseError)) {
964
+ this.capabilities = null;
965
+ this.lastCapabilityCheckAt = null;
966
+ }
967
+ this.lastError = fallbackMessage(error);
968
+ return this.fallback(input.probeId, input.key, this.lastError);
969
+ }
970
+ }
971
+ async requestExecVerb(input) {
972
+ const socketPath = resolveScoutdProbesSocketPath(this.env);
973
+ const socketTimeoutMs = execSocketTimeoutMs(input.args);
974
+ const socketExists = existsSync2(socketPath);
975
+ if (!socketExists) {
976
+ this.capabilities = null;
977
+ this.lastError = null;
978
+ return this.daemonObserved ? this.execFallback(input.verb, `probe socket is missing: ${socketPath}`) : { state: "local" };
979
+ }
980
+ this.daemonObserved = true;
981
+ const capabilities = await this.ensureCapabilities(socketPath, socketTimeoutMs);
982
+ if (!capabilities) {
983
+ return this.execFallback(input.verb, this.lastError ?? "probe capabilities unavailable");
984
+ }
985
+ const capability = capabilities.verbs.get(input.verb);
986
+ if (!capability) {
987
+ return this.execFallback(input.verb, `scoutd does not serve ${input.verb}`);
988
+ }
989
+ const expectedSchemaVersion = expectedScoutHostExecVerbSchemaVersion(input.verb);
990
+ if (expectedSchemaVersion === null) {
991
+ return this.execFallback(input.verb, `client has no compiled schema version for ${input.verb}`);
992
+ }
993
+ if (capability.schemaVersion !== expectedSchemaVersion) {
994
+ return this.execFallback(input.verb, `scoutd serves ${input.verb} schema v${capability.schemaVersion}, expected v${expectedSchemaVersion}`);
995
+ }
996
+ try {
997
+ const response = await requestJson(socketPath, {
998
+ schema: EXEC_REQUEST_SCHEMA,
999
+ schemaVersion: expectedSchemaVersion,
1000
+ verb: input.verb,
1001
+ args: input.args
1002
+ }, { timeoutMs: socketTimeoutMs });
1003
+ if (!isRecord2(response) || response.schema !== EXEC_RESPONSE_SCHEMA) {
1004
+ throw new Error("scoutd returned an invalid exec response envelope");
1005
+ }
1006
+ if (!response.ok) {
1007
+ const error = isRecord2(response.error) ? response.error : {};
1008
+ const code = readString(error.code) ?? "exec_error";
1009
+ const message = readString(error.message) ?? code;
1010
+ const timedOut = error.timedOut === true || error.timed_out === true;
1011
+ throw new ScoutdExecResponseError(message, { code, timedOut });
1012
+ }
1013
+ this.fallbackByExec.delete(input.verb);
1014
+ this.lastError = null;
1015
+ return {
1016
+ state: "scoutd",
1017
+ value: response.value,
1018
+ daemonVersion: readString(response.daemonVersion) ?? capabilities.daemonVersion
1019
+ };
1020
+ } catch (error) {
1021
+ if (error instanceof ScoutdExecResponseError) {
1022
+ throw error;
1023
+ }
1024
+ this.capabilities = null;
1025
+ this.lastCapabilityCheckAt = null;
1026
+ this.lastError = fallbackMessage(error);
1027
+ return this.execFallback(input.verb, this.lastError);
1028
+ }
1029
+ }
1030
+ diagnostics() {
1031
+ const socketPath = resolveScoutdProbesSocketPath(this.env);
1032
+ return {
1033
+ socketPath,
1034
+ socketExists: existsSync2(socketPath),
1035
+ daemonObserved: this.daemonObserved,
1036
+ daemonVersion: this.capabilities?.daemonVersion ?? null,
1037
+ supportedProbeIds: this.capabilities ? [...this.capabilities.families.keys()].sort() : [],
1038
+ supportedExecVerbs: this.capabilities ? [...this.capabilities.verbs.keys()].sort() : [],
1039
+ lastCapabilityCheckAt: this.lastCapabilityCheckAt,
1040
+ lastError: this.lastError
1041
+ };
1042
+ }
1043
+ resetForTests() {
1044
+ this.capabilities = null;
1045
+ this.lastCapabilityCheckAt = null;
1046
+ this.lastError = null;
1047
+ this.daemonObserved = false;
1048
+ this.fallbackByProbe.clear();
1049
+ this.fallbackByExec.clear();
1050
+ }
1051
+ async ensureCapabilities(socketPath, timeoutMs = SOCKET_TIMEOUT_MS) {
1052
+ const now = Date.now();
1053
+ if (this.capabilities && this.lastCapabilityCheckAt !== null && now - this.lastCapabilityCheckAt < CAPABILITY_RECHECK_MS) {
1054
+ return this.capabilities;
1055
+ }
1056
+ this.lastCapabilityCheckAt = now;
1057
+ try {
1058
+ const response = await requestJson(socketPath, { schema: CAPABILITIES_SCHEMA }, { timeoutMs });
1059
+ if (!isRecord2(response) || response.schema !== CAPABILITIES_SCHEMA) {
1060
+ throw new Error("scoutd returned an invalid capabilities envelope");
1061
+ }
1062
+ const daemonVersion = readString(response.daemonVersion) ?? "unknown";
1063
+ const families = new Map;
1064
+ if (Array.isArray(response.families)) {
1065
+ for (const entry of response.families) {
1066
+ if (!isRecord2(entry))
1067
+ continue;
1068
+ const probeId = readString(entry.probeId);
1069
+ const schemaVersion = readNumber(entry.schemaVersion);
1070
+ const ttlMs = readNumber(entry.ttlMs);
1071
+ if (probeId && schemaVersion !== null && ttlMs !== null) {
1072
+ families.set(probeId, { probeId, schemaVersion, ttlMs });
1073
+ }
1074
+ }
1075
+ }
1076
+ const verbs = new Map;
1077
+ if (Array.isArray(response.verbs)) {
1078
+ for (const entry of response.verbs) {
1079
+ if (!isRecord2(entry))
1080
+ continue;
1081
+ const verb = readString(entry.verb);
1082
+ const schemaVersion = readNumber(entry.schemaVersion);
1083
+ if (verb && schemaVersion !== null) {
1084
+ verbs.set(verb, { verb, schemaVersion });
1085
+ }
1086
+ }
1087
+ }
1088
+ this.capabilities = { daemonVersion, families, verbs };
1089
+ this.daemonObserved = true;
1090
+ this.lastError = null;
1091
+ return this.capabilities;
1092
+ } catch (error) {
1093
+ this.capabilities = null;
1094
+ this.lastError = fallbackMessage(error);
1095
+ return null;
1096
+ }
1097
+ }
1098
+ fallback(probeId, key, reason) {
1099
+ const id = fallbackKey(probeId, key);
1100
+ let state = this.fallbackByProbe.get(id);
1101
+ if (!state || state.reason !== reason) {
1102
+ state = { since: Date.now(), reason };
1103
+ this.fallbackByProbe.set(id, state);
1104
+ }
1105
+ return {
1106
+ state: "local",
1107
+ fallbackSince: state.since,
1108
+ fallbackReason: state.reason
1109
+ };
1110
+ }
1111
+ execFallback(verb, reason) {
1112
+ let state = this.fallbackByExec.get(verb);
1113
+ if (!state || state.reason !== reason) {
1114
+ state = { since: Date.now(), reason };
1115
+ this.fallbackByExec.set(verb, state);
1116
+ }
1117
+ return {
1118
+ state: "local",
1119
+ fallbackSince: state.since,
1120
+ fallbackReason: state.reason
1121
+ };
1122
+ }
1123
+ }
1124
+ var singletonClient = null;
1125
+ function getScoutdProbeClient() {
1126
+ singletonClient ??= new ScoutdProbeClient;
1127
+ return singletonClient;
1128
+ }
1129
+ async function runWithScoutdFallback(input) {
1130
+ const client = getScoutdProbeClient();
1131
+ const scoutd = await client.requestProbe({
1132
+ probeId: input.probeId,
1133
+ key: input.key,
1134
+ maxAgeMs: input.ctx.maxAgeMs,
1135
+ timeoutMs: input.ctx.timeoutMs
1136
+ });
1137
+ if (scoutd.state === "scoutd") {
1138
+ return probeRunOutput(scoutd.value, {
1139
+ backend: "scoutd",
1140
+ generatedAt: scoutd.generatedAt
1141
+ });
1142
+ }
1143
+ const metadata = scoutd.fallbackSince ? {
1144
+ backend: "local-fallback",
1145
+ fallbackSince: scoutd.fallbackSince,
1146
+ fallbackReason: scoutd.fallbackReason ?? "scoutd unavailable"
1147
+ } : { backend: "local" };
1148
+ try {
1149
+ const local = await input.local();
1150
+ return probeRunOutput(local, metadata);
1151
+ } catch (error) {
1152
+ throw new ProbeBackendError(error instanceof Error ? error.message : String(error), metadata, error);
1153
+ }
1154
+ }
1155
+ function probeSocketTimeoutMs(timeoutMs) {
1156
+ if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
1157
+ return SOCKET_TIMEOUT_MS;
1158
+ }
1159
+ return Math.max(SOCKET_TIMEOUT_MS, Math.min(Math.max(0, timeoutMs) + 1000, 31000));
1160
+ }
1161
+ function requestOpTimeoutMs(timeoutMs) {
1162
+ if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
1163
+ return;
1164
+ }
1165
+ return Math.max(0, Math.floor(timeoutMs));
1166
+ }
1167
+ function execSocketTimeoutMs(args) {
1168
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? Math.max(0, args.timeoutMs) : 5000;
1169
+ return Math.max(SOCKET_TIMEOUT_MS, Math.min(timeoutMs + 1000, 31000));
1170
+ }
1171
+ async function requestJson(socketPath, payload, options = {}) {
1172
+ const bun = globalThis.Bun;
1173
+ if (bun?.connect) {
1174
+ return await requestJsonWithBun(bun.connect, socketPath, payload, options);
1175
+ }
1176
+ return await new Promise((resolve, reject) => {
1177
+ const socket = new Socket;
1178
+ let response = "";
1179
+ let settled = false;
1180
+ const finish = (error, value) => {
1181
+ if (settled)
1182
+ return;
1183
+ settled = true;
1184
+ clearTimeout(timer);
1185
+ socket.removeAllListeners();
1186
+ socket.destroy();
1187
+ if (error) {
1188
+ reject(error);
1189
+ } else {
1190
+ resolve(value);
1191
+ }
1192
+ };
1193
+ const timeoutMs = options.timeoutMs ?? SOCKET_TIMEOUT_MS;
1194
+ const timer = setTimeout(() => {
1195
+ finish(new Error(`scoutd probe socket timed out after ${timeoutMs}ms`));
1196
+ }, timeoutMs);
1197
+ timer.unref?.();
1198
+ const finishFromResponse = () => {
1199
+ if (settled)
1200
+ return;
1201
+ try {
1202
+ const parsed = JSON.parse(response);
1203
+ if (isRecord2(parsed) && parsed.schema === ERROR_SCHEMA) {
1204
+ const error = isRecord2(parsed.error) ? parsed.error : {};
1205
+ const message = readString(error.message) ?? readString(error.code) ?? "scoutd probe request failed";
1206
+ finish(new Error(message));
1207
+ return;
1208
+ }
1209
+ finish(null, parsed);
1210
+ } catch (error) {
1211
+ finish(new Error(`scoutd probe response was not JSON: ${fallbackMessage(error)}`));
1212
+ }
1213
+ };
1214
+ socket.setEncoding("utf8");
1215
+ socket.on("connect", () => {
1216
+ socket.write(`${JSON.stringify(payload)}
1217
+ `);
1218
+ });
1219
+ socket.on("data", (chunk) => {
1220
+ response += chunk;
1221
+ if (Buffer.byteLength(response, "utf8") > MAX_RESPONSE_BYTES) {
1222
+ finish(new Error("scoutd probe response exceeded output limit"));
1223
+ }
1224
+ });
1225
+ socket.on("error", (error) => finish(error));
1226
+ socket.on("end", finishFromResponse);
1227
+ socket.on("close", () => {
1228
+ if (!settled) {
1229
+ if (response.length === 0) {
1230
+ finish(new Error("scoutd probe socket closed without a response"));
1231
+ } else {
1232
+ finishFromResponse();
1233
+ }
1234
+ }
1235
+ });
1236
+ socket.connect(socketPath);
1237
+ });
1238
+ }
1239
+ async function requestJsonWithBun(connect, socketPath, payload, options = {}) {
1240
+ return await new Promise((resolve, reject) => {
1241
+ let response = "";
1242
+ let settled = false;
1243
+ let socketRef = null;
1244
+ const decoder = new TextDecoder;
1245
+ const finish = (error, value) => {
1246
+ if (settled)
1247
+ return;
1248
+ settled = true;
1249
+ clearTimeout(timer);
1250
+ try {
1251
+ socketRef?.end?.();
1252
+ } catch {}
1253
+ if (error) {
1254
+ reject(error);
1255
+ } else {
1256
+ resolve(value);
1257
+ }
1258
+ };
1259
+ const finishFromResponse = () => {
1260
+ if (settled)
1261
+ return;
1262
+ try {
1263
+ const parsed = JSON.parse(response);
1264
+ if (isRecord2(parsed) && parsed.schema === ERROR_SCHEMA) {
1265
+ const error = isRecord2(parsed.error) ? parsed.error : {};
1266
+ const message = readString(error.message) ?? readString(error.code) ?? "scoutd probe request failed";
1267
+ finish(new Error(message));
1268
+ return;
1269
+ }
1270
+ finish(null, parsed);
1271
+ } catch (error) {
1272
+ finish(new Error(`scoutd probe response was not JSON: ${fallbackMessage(error)}`));
1273
+ }
1274
+ };
1275
+ const timeoutMs = options.timeoutMs ?? SOCKET_TIMEOUT_MS;
1276
+ const timer = setTimeout(() => {
1277
+ finish(new Error(`scoutd probe socket timed out after ${timeoutMs}ms`));
1278
+ }, timeoutMs);
1279
+ timer.unref?.();
1280
+ connect({
1281
+ unix: socketPath,
1282
+ socket: {
1283
+ open(socket) {
1284
+ socketRef = socket;
1285
+ socket.write(`${JSON.stringify(payload)}
1286
+ `);
1287
+ },
1288
+ data(_socket, data) {
1289
+ response += decoder.decode(data, { stream: true });
1290
+ if (Buffer.byteLength(response, "utf8") > MAX_RESPONSE_BYTES) {
1291
+ finish(new Error("scoutd probe response exceeded output limit"));
1292
+ }
1293
+ },
1294
+ close() {
1295
+ if (response.length === 0) {
1296
+ finish(new Error("scoutd probe socket closed without a response"));
1297
+ } else {
1298
+ response += decoder.decode();
1299
+ finishFromResponse();
1300
+ }
1301
+ },
1302
+ error(_socket, error) {
1303
+ finish(error);
1304
+ }
1305
+ }
1306
+ }).then((socket) => {
1307
+ socketRef = socket;
1308
+ }, (error) => {
1309
+ finish(error instanceof Error ? error : new Error(String(error)));
1310
+ });
1311
+ });
1312
+ }
1313
+ function fallbackKey(probeId, key) {
1314
+ return `${probeId}\x00${key ?? ""}`;
1315
+ }
1316
+
1317
+ // packages/runtime/src/system-probes/exec.ts
1318
+ class ProbeCommandError extends Error {
1319
+ code;
1320
+ exitCode;
1321
+ signal;
1322
+ constructor(message, options) {
1323
+ super(message);
1324
+ this.name = "ProbeCommandError";
1325
+ this.code = options.code;
1326
+ this.exitCode = options.exitCode;
1327
+ this.signal = options.signal;
1328
+ }
1329
+ }
1330
+ var DEFAULT_STDOUT_CAP_BYTES = 1024 * 1024;
1331
+ var DEFAULT_STDERR_CAP_BYTES = 128 * 1024;
1332
+ var SIGKILL_DELAY_MS = 500;
1333
+ var EXEC_SYSTEM_TRANSPORT_METADATA = Symbol.for("openscout.execSystem.transport");
1334
+ var spawnProcess = spawn;
1335
+ function bufferByteLength(chunks) {
1336
+ return chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
1337
+ }
1338
+ function abortMessage(ctx) {
1339
+ const reason = ctx.signal.reason;
1340
+ if (typeof reason === "object" && reason !== null && "message" in reason) {
1341
+ const message = reason.message;
1342
+ if (typeof message === "string" && message.trim()) {
1343
+ return message;
1344
+ }
1345
+ }
1346
+ return `Probe ${ctx.probeId} aborted`;
1347
+ }
1348
+ async function execProbeFile(ctx, file, args, options = {}) {
1349
+ const maxStdoutBytes = options.maxStdoutBytes ?? DEFAULT_STDOUT_CAP_BYTES;
1350
+ const maxStderrBytes = options.maxStderrBytes ?? DEFAULT_STDERR_CAP_BYTES;
1351
+ return await new Promise((resolve, reject) => {
1352
+ if (ctx.signal.aborted) {
1353
+ reject(new ProbeCommandError(abortMessage(ctx), { code: "aborted" }));
1354
+ return;
1355
+ }
1356
+ const stdoutChunks = [];
1357
+ const stderrChunks = [];
1358
+ let settled = false;
1359
+ let killTimer = null;
1360
+ const child = spawnProcess(file, [...args], {
1361
+ cwd: options.cwd,
1362
+ env: options.env,
1363
+ stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
1364
+ windowsHide: true
1365
+ });
1366
+ const cleanup = () => {
1367
+ ctx.signal.removeEventListener("abort", onAbort);
1368
+ if (killTimer) {
1369
+ clearTimeout(killTimer);
1370
+ killTimer = null;
1371
+ }
1372
+ };
1373
+ const fail = (error) => {
1374
+ if (settled) {
1375
+ return;
1376
+ }
1377
+ settled = true;
1378
+ cleanup();
1379
+ if (!child.killed) {
1380
+ child.kill("SIGTERM");
1381
+ }
1382
+ reject(error);
1383
+ };
1384
+ const onAbort = () => {
1385
+ if (settled) {
1386
+ return;
1387
+ }
1388
+ settled = true;
1389
+ ctx.signal.removeEventListener("abort", onAbort);
1390
+ child.kill("SIGTERM");
1391
+ killTimer = setTimeout(() => {
1392
+ child.kill("SIGKILL");
1393
+ }, SIGKILL_DELAY_MS);
1394
+ reject(new ProbeCommandError(abortMessage(ctx), { code: "timeout" }));
1395
+ };
1396
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
1397
+ if (options.input !== undefined) {
1398
+ child.stdin?.end(options.input);
1399
+ }
1400
+ child.stdout?.on("data", (chunk) => {
1401
+ if (settled) {
1402
+ return;
1403
+ }
1404
+ stdoutChunks.push(chunk);
1405
+ if (bufferByteLength(stdoutChunks) > maxStdoutBytes) {
1406
+ fail(new ProbeCommandError(`Probe ${ctx.probeId} stdout exceeded ${maxStdoutBytes} bytes`, { code: "output_cap" }));
1407
+ }
1408
+ });
1409
+ child.stderr?.on("data", (chunk) => {
1410
+ if (settled) {
1411
+ return;
1412
+ }
1413
+ stderrChunks.push(chunk);
1414
+ if (bufferByteLength(stderrChunks) > maxStderrBytes) {
1415
+ fail(new ProbeCommandError(`Probe ${ctx.probeId} stderr exceeded ${maxStderrBytes} bytes`, { code: "output_cap" }));
1416
+ }
1417
+ });
1418
+ child.once("error", (error) => {
1419
+ if (settled) {
1420
+ return;
1421
+ }
1422
+ settled = true;
1423
+ cleanup();
1424
+ const code = typeof error.code === "string" ? String(error.code) : "spawn";
1425
+ reject(new ProbeCommandError(error.message, { code }));
1426
+ });
1427
+ child.once("close", (exitCode, signal) => {
1428
+ if (settled) {
1429
+ cleanup();
1430
+ return;
1431
+ }
1432
+ settled = true;
1433
+ cleanup();
1434
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
1435
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
1436
+ if (exitCode === 0) {
1437
+ resolve({ stdout, stderr, exitCode });
1438
+ return;
1439
+ }
1440
+ reject(new ProbeCommandError(`${file} exited with ${exitCode ?? signal ?? "unknown status"}`, { code: "exit", exitCode, signal }));
1441
+ });
1442
+ });
1443
+ }
1444
+
1445
+ // packages/runtime/src/system-probes/tailscale-status.ts
1446
+ var DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS = 1500;
1447
+ function parseTailscaleStatusJson(raw) {
1448
+ return JSON.parse(raw);
1449
+ }
1450
+ function parsePeers(status) {
1451
+ const peers = Object.entries(status.Peer ?? {});
1452
+ return peers.map(([fallbackId, peer]) => ({
1453
+ id: peer.ID ?? fallbackId,
1454
+ name: peer.HostName ?? peer.DNSName ?? fallbackId,
1455
+ dnsName: peer.DNSName,
1456
+ addresses: peer.TailscaleIPs ?? [],
1457
+ online: peer.Online ?? false,
1458
+ hostName: peer.HostName,
1459
+ os: peer.OS,
1460
+ tags: peer.Tags ?? []
1461
+ }));
1462
+ }
1463
+ function parseSelf(status) {
1464
+ const self = status.Self;
1465
+ if (!self) {
1466
+ return null;
1467
+ }
1468
+ return {
1469
+ id: self.ID ?? self.DNSName ?? self.HostName ?? "self",
1470
+ name: self.HostName ?? self.DNSName ?? "self",
1471
+ dnsName: self.DNSName,
1472
+ addresses: self.TailscaleIPs ?? [],
1473
+ online: self.Online ?? true,
1474
+ hostName: self.HostName,
1475
+ os: self.OS,
1476
+ tailnetName: status.CurrentTailnet?.Name,
1477
+ magicDnsSuffix: status.CurrentTailnet?.MagicDNSSuffix
1478
+ };
1479
+ }
1480
+ function isTailscaleBackendRunning(status) {
1481
+ return (status.BackendState ?? "").trim().toLowerCase() === "running";
1482
+ }
1483
+ function normalizeHost(value) {
1484
+ const normalized = value?.trim().replace(/\.$/, "").toLowerCase();
1485
+ if (!normalized || normalized.includes("/") || /\s/.test(normalized)) {
1486
+ return null;
1487
+ }
1488
+ return normalized;
1489
+ }
1490
+ function uniq(values) {
1491
+ const seen = new Set;
1492
+ const out = [];
1493
+ for (const value of values) {
1494
+ const normalized = normalizeHost(value ?? undefined);
1495
+ if (!normalized || seen.has(normalized)) {
1496
+ continue;
1497
+ }
1498
+ seen.add(normalized);
1499
+ out.push(normalized);
1500
+ }
1501
+ return out;
1502
+ }
1503
+ function tailscaleSelfWebHosts(self) {
1504
+ if (!self) {
1505
+ return [];
1506
+ }
1507
+ return uniq([
1508
+ self.dnsName,
1509
+ self.hostName && self.magicDnsSuffix ? `${self.hostName}.${self.magicDnsSuffix}` : undefined,
1510
+ ...self.addresses
1511
+ ]);
1512
+ }
1513
+ function summarizeTailscaleStatus(status) {
1514
+ return {
1515
+ backendState: status.BackendState ?? null,
1516
+ running: isTailscaleBackendRunning(status),
1517
+ health: status.Health ?? [],
1518
+ peers: parsePeers(status),
1519
+ self: parseSelf(status)
1520
+ };
1521
+ }
1522
+ function statusTimeoutMs(env) {
1523
+ const parsed = Number.parseInt(env.OPENSCOUT_TAILSCALE_STATUS_TIMEOUT_MS ?? "", 10);
1524
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS;
1525
+ }
1526
+ async function readStatusJsonFromFile(filePath) {
1527
+ try {
1528
+ const raw = await readFile(filePath, "utf8");
1529
+ return parseTailscaleStatusJson(raw);
1530
+ } catch {
1531
+ return null;
1532
+ }
1533
+ }
1534
+ function isDomainUnavailableError(error) {
1535
+ if (!(error instanceof ProbeCommandError)) {
1536
+ return false;
1537
+ }
1538
+ return error.code === "ENOENT" || error.code === "spawn" || error.code === "exit";
1539
+ }
1540
+ async function readTailscaleStatusSummaryLocal(ctx) {
1541
+ const fixturePath = process.env.OPENSCOUT_TAILSCALE_STATUS_JSON;
1542
+ if (fixturePath) {
1543
+ const status = await readStatusJsonFromFile(fixturePath);
1544
+ return status ? summarizeTailscaleStatus(status) : null;
1545
+ }
1546
+ const tailscaleBin = process.env.OPENSCOUT_TAILSCALE_BIN ?? "tailscale";
1547
+ try {
1548
+ const { stdout } = await execProbeFile(ctx, tailscaleBin, ["status", "--json"], {
1549
+ maxStdoutBytes: 4 * 1024 * 1024,
1550
+ maxStderrBytes: 256 * 1024
1551
+ });
1552
+ return summarizeTailscaleStatus(parseTailscaleStatusJson(stdout));
1553
+ } catch (error) {
1554
+ if (isDomainUnavailableError(error)) {
1555
+ return null;
1556
+ }
1557
+ throw error;
1558
+ }
1559
+ }
1560
+ var tailscaleStatusProbe = defineProbe({
1561
+ id: "tailscale.status",
1562
+ ttlMs: 30000,
1563
+ timeoutMs: statusTimeoutMs(process.env),
1564
+ run: (ctx) => runWithScoutdFallback({
1565
+ probeId: "tailscale.status",
1566
+ ctx,
1567
+ local: () => readTailscaleStatusSummaryLocal(ctx)
1568
+ })
1569
+ });
1570
+
1571
+ // packages/runtime/src/tailscale.ts
1572
+ var DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS2 = 1500;
1573
+ function readStatusJsonFromFileSync(filePath) {
1574
+ const raw = readFileSync2(filePath, "utf8");
1575
+ return parseTailscaleStatusJson(raw);
1576
+ }
1577
+ function statusTimeoutMs2(env) {
1578
+ const parsed = Number.parseInt(env.OPENSCOUT_TAILSCALE_STATUS_TIMEOUT_MS ?? "", 10);
1579
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS2;
1580
+ }
1581
+ function readStatusJsonSync(env = process.env) {
1582
+ if (env.OPENSCOUT_TAILSCALE_AUTO_HOSTS === "0") {
1583
+ return null;
1584
+ }
1585
+ const fixturePath = env.OPENSCOUT_TAILSCALE_STATUS_JSON;
1586
+ if (fixturePath) {
1587
+ try {
1588
+ return readStatusJsonFromFileSync(fixturePath);
1589
+ } catch {
1590
+ return null;
1591
+ }
1592
+ }
1593
+ const tailscaleBin = env.OPENSCOUT_TAILSCALE_BIN ?? (env === process.env ? "tailscale" : undefined);
1594
+ if (!tailscaleBin) {
1595
+ return null;
1596
+ }
1597
+ try {
1598
+ const stdout = execFileSync(tailscaleBin, ["status", "--json"], {
1599
+ encoding: "utf8",
1600
+ stdio: ["ignore", "pipe", "pipe"],
1601
+ timeout: statusTimeoutMs2(env),
1602
+ windowsHide: true
1603
+ });
1604
+ return parseTailscaleStatusJson(stdout);
1605
+ } catch {
1606
+ return null;
1607
+ }
1608
+ }
1609
+ function readTailscaleSelfWebHostsSync(env = process.env) {
1610
+ const status = readStatusJsonSync(env);
1611
+ if (!status || !isTailscaleBackendRunning(status)) {
1612
+ return [];
1613
+ }
1614
+ return tailscaleSelfWebHosts(summarizeTailscaleStatus(status).self);
1615
+ }
1616
+
1617
+ // packages/runtime/src/local-config.ts
1618
+ import {
1619
+ existsSync as existsSync3,
1620
+ mkdirSync,
1621
+ readFileSync as readFileSync3,
1622
+ renameSync,
1623
+ writeFileSync
1624
+ } from "fs";
1625
+ import { homedir as homedir3, hostname as osHostname } from "os";
1626
+ import { dirname, join as join3 } from "path";
1627
+ var LOCAL_CONFIG_VERSION = 1;
1628
+ var DEFAULT_SCOUT_WEB_PORTAL_HOST = "scout.local";
1629
+ var DEFAULT_SCOUT_WEB_DEV_HOST = `dev.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
1630
+ var DEFAULT_SCOUT_WEB_VITE_HMR_PATH = "/ws/hmr";
1631
+ var OPENSCOUT_PORTS = {
1632
+ broker: 43110,
1633
+ web: 43120,
1634
+ webTerminalRelay: 43121,
1635
+ vite: 43122,
1636
+ pairingBridge: 43130,
1637
+ pairingRelay: 43131,
1638
+ pairingFileServer: 43132,
1639
+ studio: 43140
1640
+ };
1641
+ var DEFAULT_LOCAL_CONFIG = {
1642
+ version: LOCAL_CONFIG_VERSION,
1643
+ host: "127.0.0.1",
1644
+ webLocalName: undefined,
1645
+ ports: {
1646
+ broker: OPENSCOUT_PORTS.broker,
1647
+ web: OPENSCOUT_PORTS.web,
1648
+ pairing: OPENSCOUT_PORTS.pairingBridge
1649
+ }
1650
+ };
1651
+ function normalizeLocalHostnameLabel(value) {
1652
+ const firstLabel = value?.trim().replace(/\.local\.?$/i, "").split(".").find((part) => part.trim().length > 0)?.trim();
1653
+ const normalized = firstLabel?.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
1654
+ return normalized || "localhost";
1655
+ }
1656
+ function normalizeLocalHostname(value) {
1657
+ const trimmed = value?.trim().replace(/\.$/, "").toLowerCase();
1658
+ const labels = trimmed?.split(".").map((label) => label.trim().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-")).filter(Boolean);
1659
+ return labels && labels.length > 0 ? labels.join(".") : "localhost";
1660
+ }
1661
+ function resolveScoutWebNamedHostname(name) {
1662
+ const normalized = normalizeLocalHostname(name);
1663
+ return normalized.includes(".") ? normalized : `${normalized}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
1664
+ }
1665
+ function resolveScoutWebDevHostname(portalHost = DEFAULT_SCOUT_WEB_PORTAL_HOST) {
1666
+ const normalized = portalHost.trim().replace(/\.$/, "").toLowerCase();
1667
+ if (!normalized || normalized.startsWith("dev.")) {
1668
+ return normalized || DEFAULT_SCOUT_WEB_DEV_HOST;
1669
+ }
1670
+ return `dev.${normalized}`;
1671
+ }
1672
+ function resolveConfiguredScoutWebHostname(config = loadLocalConfig(), machineHostname = osHostname()) {
1673
+ if (config.webLocalName) {
1674
+ return resolveScoutWebNamedHostname(config.webLocalName);
1675
+ }
1676
+ return `${normalizeLocalHostnameLabel(machineHostname)}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
1677
+ }
1678
+ function localConfigHome() {
1679
+ return process.env.OPENSCOUT_HOME ?? join3(homedir3(), ".openscout");
1680
+ }
1681
+ function localConfigPath() {
1682
+ return join3(localConfigHome(), "config.json");
1683
+ }
1684
+ function loadLocalConfig() {
1685
+ const configPath = localConfigPath();
1686
+ if (!existsSync3(configPath))
1687
+ return { version: LOCAL_CONFIG_VERSION };
1688
+ try {
1689
+ return validateLocalConfig(JSON.parse(readFileSync3(configPath, "utf8")));
1690
+ } catch {
1691
+ return { version: LOCAL_CONFIG_VERSION };
1692
+ }
1693
+ }
1694
+ function validateLocalConfig(input) {
1695
+ if (!input || typeof input !== "object")
1696
+ return { version: LOCAL_CONFIG_VERSION };
1697
+ const raw = input;
1698
+ const out = { version: LOCAL_CONFIG_VERSION };
1699
+ if (typeof raw.host === "string" && raw.host.trim().length > 0) {
1700
+ out.host = raw.host.trim();
1701
+ }
1702
+ if (typeof raw.webLocalName === "string" && raw.webLocalName.trim().length > 0) {
1703
+ out.webLocalName = raw.webLocalName.trim();
1704
+ }
1705
+ if (raw.ports && typeof raw.ports === "object") {
1706
+ const ports = raw.ports;
1707
+ const p = {};
1708
+ if (isValidPort(ports.broker))
1709
+ p.broker = ports.broker;
1710
+ if (isValidPort(ports.web))
1711
+ p.web = ports.web;
1712
+ if (isValidPort(ports.pairing))
1713
+ p.pairing = ports.pairing;
1714
+ if (Object.keys(p).length > 0)
1715
+ out.ports = p;
1716
+ }
1717
+ return out;
1718
+ }
1719
+ function isValidPort(value) {
1720
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
1721
+ }
1722
+ function parseEnvPort(name) {
1723
+ const raw = process.env[name]?.trim();
1724
+ if (!raw) {
1725
+ return;
1726
+ }
1727
+ const parsed = Number.parseInt(raw, 10);
1728
+ return isValidPort(parsed) ? parsed : undefined;
1729
+ }
1730
+ function resolveEffectiveLocalConfig() {
1731
+ const file = loadLocalConfig();
1732
+ const host = process.env.OPENSCOUT_BROKER_HOST?.trim() || process.env.OPENSCOUT_HOST?.trim() || file.host;
1733
+ const ports = {
1734
+ broker: parseEnvPort("OPENSCOUT_BROKER_PORT") ?? file.ports?.broker,
1735
+ web: parseEnvPort("OPENSCOUT_WEB_PORT") ?? parseEnvPort("SCOUT_WEB_PORT") ?? file.ports?.web,
1736
+ pairing: parseEnvPort("OPENSCOUT_PAIRING_PORT") ?? file.ports?.pairing
1737
+ };
1738
+ return {
1739
+ version: LOCAL_CONFIG_VERSION,
1740
+ ...host ? { host } : {},
1741
+ ...file.webLocalName ? { webLocalName: file.webLocalName } : {},
1742
+ ...ports.broker || ports.web || ports.pairing ? { ports } : {}
1743
+ };
1744
+ }
1745
+ function resolveBrokerPort() {
1746
+ return resolveEffectiveLocalConfig().ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
1747
+ }
1748
+ function resolveWebPort() {
1749
+ return resolveEffectiveLocalConfig().ports?.web ?? DEFAULT_LOCAL_CONFIG.ports.web;
1750
+ }
1751
+
1752
+ // packages/runtime/src/tool-resolution.ts
1753
+ import { accessSync, constants, existsSync as existsSync4 } from "fs";
1754
+ import { homedir as homedir4 } from "os";
1755
+ import { basename, dirname as dirname2, join as join4, resolve } from "path";
234
1756
  var DEFAULT_COMMON_EXECUTABLE_DIRECTORIES = [
235
- join2(homedir2(), ".bun", "bin"),
1757
+ join4(homedir4(), ".bun", "bin"),
236
1758
  "/opt/homebrew/bin",
237
1759
  "/usr/local/bin"
238
1760
  ];
239
1761
  function expandHomePath(value, env = process.env) {
240
- const home = env.HOME ?? homedir2();
1762
+ const home = env.HOME ?? homedir4();
241
1763
  if (value === "~") {
242
1764
  return home;
243
1765
  }
244
1766
  if (value.startsWith("~/")) {
245
- return join2(home, value.slice(2));
1767
+ return join4(home, value.slice(2));
246
1768
  }
247
1769
  return value;
248
1770
  }
@@ -299,7 +1821,7 @@ function resolveExecutableFromSearch(options) {
299
1821
  ], env);
300
1822
  for (const directory of searchDirectories) {
301
1823
  for (const name of options.names) {
302
- const candidate = join2(directory, name);
1824
+ const candidate = join4(directory, name);
303
1825
  if (isExecutablePath(candidate)) {
304
1826
  return {
305
1827
  path: candidate,
@@ -323,12 +1845,50 @@ function findExecutableOnSearchPath(name, env) {
323
1845
  ...DEFAULT_COMMON_EXECUTABLE_DIRECTORIES
324
1846
  ], env);
325
1847
  for (const directory of searchDirectories) {
326
- const candidate = join2(directory, name);
1848
+ const candidate = join4(directory, name);
327
1849
  if (isExecutablePath(candidate)) {
328
1850
  return { path: candidate, source: "path" };
329
1851
  }
330
1852
  }
331
- return null;
1853
+ return null;
1854
+ }
1855
+
1856
+ // packages/runtime/src/runtime-adapters.ts
1857
+ function normalizeRuntimeHost(value) {
1858
+ const normalized = value?.trim().toLowerCase();
1859
+ if (normalized === "bun")
1860
+ return "bun";
1861
+ if (normalized === "node")
1862
+ return "node";
1863
+ return null;
1864
+ }
1865
+ function normalizeRuntimeServiceAdapter(value) {
1866
+ const normalized = value?.trim().toLowerCase().replaceAll("_", "-");
1867
+ if (normalized === "macos-scoutd" || normalized === "macos-launchd")
1868
+ return "macos-scoutd";
1869
+ if (normalized === "linux-systemd-user" || normalized === "systemd-user")
1870
+ return "linux-systemd-user";
1871
+ if (normalized === "headless-foreground" || normalized === "foreground" || normalized === "headless") {
1872
+ return "headless-foreground";
1873
+ }
1874
+ if (normalized === "windows-service")
1875
+ return "windows-service";
1876
+ return null;
1877
+ }
1878
+ function defaultServiceAdapterForPlatform(platform = process.platform, env = process.env) {
1879
+ const explicit = normalizeRuntimeServiceAdapter(env.OPENSCOUT_SERVICE_ADAPTER);
1880
+ if (explicit)
1881
+ return explicit;
1882
+ if (normalizeRuntimeHost(env.OPENSCOUT_RUNTIME_HOST) === "node") {
1883
+ return "headless-foreground";
1884
+ }
1885
+ if (platform === "darwin")
1886
+ return "macos-scoutd";
1887
+ if (platform === "linux")
1888
+ return "headless-foreground";
1889
+ if (platform === "win32")
1890
+ return "windows-service";
1891
+ return "headless-foreground";
332
1892
  }
333
1893
 
334
1894
  // packages/runtime/src/broker-process-manager.ts
@@ -337,31 +1897,72 @@ function isTmpPath(p) {
337
1897
  }
338
1898
  var DEFAULT_BROKER_HOST = "127.0.0.1";
339
1899
  var DEFAULT_BROKER_HOST_MESH = "0.0.0.0";
340
- var DEFAULT_BROKER_PORT = 65535;
1900
+ var DEFAULT_BROKER_PORT = OPENSCOUT_PORTS.broker;
341
1901
  var DEFAULT_ADVERTISE_SCOPE = "local";
342
- function resolveAdvertiseScope() {
343
- const raw = (process.env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
1902
+ function resolveAdvertiseScope(env = process.env) {
1903
+ if (openScoutNetworkDiscoveryEnabled(env))
1904
+ return "mesh";
1905
+ const raw = (env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
344
1906
  if (raw === "mesh")
345
1907
  return "mesh";
346
1908
  if (raw === "local")
347
1909
  return "local";
348
1910
  return DEFAULT_ADVERTISE_SCOPE;
349
1911
  }
350
- function resolveBrokerHost(scope = resolveAdvertiseScope()) {
351
- const explicit = process.env.OPENSCOUT_BROKER_HOST;
352
- if (typeof explicit === "string" && explicit.trim().length > 0) {
1912
+ function resolveBrokerHost(scope = resolveAdvertiseScope(), env = process.env) {
1913
+ const explicit = env.OPENSCOUT_BROKER_HOST?.trim();
1914
+ if (explicit) {
1915
+ if (scope === "mesh" && isLoopbackHost(explicit)) {
1916
+ return DEFAULT_BROKER_HOST_MESH;
1917
+ }
353
1918
  return explicit;
354
1919
  }
355
1920
  return scope === "mesh" ? DEFAULT_BROKER_HOST_MESH : DEFAULT_BROKER_HOST;
356
1921
  }
357
- var BROKER_SERVICE_POLL_INTERVAL_MS = 100;
358
- var DEFAULT_BROKER_START_TIMEOUT_MS = 15000;
359
- var DEFAULT_BROKER_STOP_TIMEOUT_MS = 20000;
1922
+ function isLoopbackHost(host) {
1923
+ const trimmed = host.trim();
1924
+ return trimmed === "127.0.0.1" || trimmed === "::1" || trimmed === "localhost";
1925
+ }
1926
+ function localBrokerControlHost(host) {
1927
+ const trimmed = host.trim();
1928
+ if (!trimmed || isWildcardHost(trimmed)) {
1929
+ return DEFAULT_BROKER_HOST;
1930
+ }
1931
+ return trimmed;
1932
+ }
360
1933
  function buildDefaultBrokerUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
361
1934
  return `http://${host}:${port}`;
362
1935
  }
1936
+ function resolveBrokerUrl(host, port, scope, env = process.env) {
1937
+ const explicit = env.OPENSCOUT_BROKER_URL?.trim();
1938
+ if (explicit && !(scope === "mesh" && isUnreachableMeshBrokerUrl(explicit))) {
1939
+ return explicit;
1940
+ }
1941
+ if (scope === "mesh") {
1942
+ const tailnetHost = readTailscaleSelfWebHostsSync(env)[0];
1943
+ if (tailnetHost) {
1944
+ return buildDefaultBrokerUrl(tailnetHost, port);
1945
+ }
1946
+ }
1947
+ return buildDefaultBrokerUrl(host, port);
1948
+ }
1949
+ function isUnreachableMeshBrokerUrl(value) {
1950
+ try {
1951
+ const host = new URL(value).hostname;
1952
+ return isLoopbackHost(host) || isWildcardHost(host);
1953
+ } catch {
1954
+ return false;
1955
+ }
1956
+ }
1957
+ function isWildcardHost(host) {
1958
+ const trimmed = host.trim();
1959
+ return trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]";
1960
+ }
1961
+ function buildLocalBrokerControlUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
1962
+ return buildDefaultBrokerUrl(localBrokerControlHost(host), port);
1963
+ }
363
1964
  function buildDefaultBrokerSocketPath(runtimeDirectory) {
364
- return join3(runtimeDirectory, "broker.sock");
1965
+ return join5(runtimeDirectory, "broker.sock");
365
1966
  }
366
1967
  var DEFAULT_BROKER_URL = buildDefaultBrokerUrl();
367
1968
  function runtimePackageDir() {
@@ -377,17 +1978,17 @@ function runtimePackageDir() {
377
1978
  const fromGlobal = findGlobalRuntimeDir();
378
1979
  if (fromGlobal)
379
1980
  return fromGlobal;
380
- const moduleDir = dirname2(fileURLToPath(import.meta.url));
1981
+ const moduleDir = dirname3(fileURLToPath(import.meta.url));
381
1982
  return resolve2(moduleDir, "..");
382
1983
  }
383
1984
  function isInstalledRuntimePackageDir(candidate) {
384
- return existsSync3(join3(candidate, "package.json")) && existsSync3(join3(candidate, "bin", "openscout-runtime.mjs"));
1985
+ return existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "bin", "openscout-runtime.mjs"));
385
1986
  }
386
1987
  function findGlobalRuntimeDir() {
387
1988
  const candidates = [
388
- join3(homedir3(), ".bun", "node_modules", "@openscout", "runtime"),
389
- join3(homedir3(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
390
- join3(homedir3(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
1989
+ join5(homedir5(), ".bun", "node_modules", "@openscout", "runtime"),
1990
+ join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
1991
+ join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
391
1992
  ];
392
1993
  for (const c of candidates) {
393
1994
  if (isInstalledRuntimePackageDir(c))
@@ -400,7 +2001,7 @@ function findGlobalRuntimeDir() {
400
2001
  const scoutPkg = resolve2(scoutBin, "..", "..");
401
2002
  if (isInstalledRuntimePackageDir(scoutPkg))
402
2003
  return scoutPkg;
403
- const nested = join3(scoutPkg, "node_modules", "@openscout", "runtime");
2004
+ const nested = join5(scoutPkg, "node_modules", "@openscout", "runtime");
404
2005
  if (isInstalledRuntimePackageDir(nested))
405
2006
  return nested;
406
2007
  const sibling = resolve2(scoutPkg, "..", "runtime");
@@ -411,30 +2012,43 @@ function findGlobalRuntimeDir() {
411
2012
  return null;
412
2013
  }
413
2014
  function findBundledRuntimeDir() {
414
- const moduleDir = dirname2(fileURLToPath(import.meta.url));
415
- const candidate = resolve2(moduleDir, "..");
416
- return isInstalledRuntimePackageDir(candidate) ? candidate : null;
2015
+ const moduleDir = dirname3(fileURLToPath(import.meta.url));
2016
+ return resolveBundledRuntimeDirFromModuleDir(moduleDir);
2017
+ }
2018
+ function resolveBundledRuntimeDirFromModuleDir(moduleDir) {
2019
+ const candidates = [
2020
+ resolve2(moduleDir, ".."),
2021
+ resolve2(moduleDir, "..", "..")
2022
+ ];
2023
+ for (const candidate of candidates) {
2024
+ if (isInstalledRuntimePackageDir(candidate))
2025
+ return candidate;
2026
+ }
2027
+ return null;
417
2028
  }
418
2029
  function findWorkspaceRuntimeDir(startDir) {
419
2030
  let current = resolve2(startDir);
420
2031
  while (true) {
421
- const candidate = join3(current, "packages", "runtime");
422
- if (existsSync3(join3(candidate, "package.json")) && existsSync3(join3(candidate, "src"))) {
2032
+ const candidate = join5(current, "packages", "runtime");
2033
+ if (existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "src"))) {
423
2034
  return candidate;
424
2035
  }
425
- const parent = dirname2(current);
2036
+ const parent = dirname3(current);
426
2037
  if (parent === current)
427
2038
  return null;
428
2039
  current = parent;
429
2040
  }
430
2041
  }
431
2042
  function resolveBunExecutable2() {
432
- const bun = resolveBunExecutable(process.env);
2043
+ const bun = resolveOptionalBunExecutable();
433
2044
  if (bun) {
434
- return bun.path;
2045
+ return bun;
435
2046
  }
436
2047
  throw new Error("Unable to locate Bun for broker service management. Install Bun or set OPENSCOUT_BUN_BIN.");
437
2048
  }
2049
+ function resolveOptionalBunExecutable() {
2050
+ return resolveBunExecutable(process.env)?.path ?? null;
2051
+ }
438
2052
  function resolveBrokerServiceMode() {
439
2053
  const explicit = (process.env.OPENSCOUT_BROKER_SERVICE_MODE ?? "").trim().toLowerCase();
440
2054
  if (explicit === "prod" || explicit === "production") {
@@ -445,20 +2059,6 @@ function resolveBrokerServiceMode() {
445
2059
  }
446
2060
  return "dev";
447
2061
  }
448
- function resolveBrokerStartTimeoutMs() {
449
- const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_START_TIMEOUT_MS ?? "", 10);
450
- if (Number.isFinite(explicit) && explicit > 0) {
451
- return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
452
- }
453
- return DEFAULT_BROKER_START_TIMEOUT_MS;
454
- }
455
- function resolveBrokerStopTimeoutMs() {
456
- const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_STOP_TIMEOUT_MS ?? "", 10);
457
- if (Number.isFinite(explicit) && explicit > 0) {
458
- return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
459
- }
460
- return DEFAULT_BROKER_STOP_TIMEOUT_MS;
461
- }
462
2062
  function resolveBrokerServiceLabel(mode) {
463
2063
  const explicit = process.env.OPENSCOUT_SERVICE_LABEL?.trim() || process.env.OPENSCOUT_BROKER_SERVICE_LABEL?.trim();
464
2064
  if (explicit) {
@@ -474,33 +2074,23 @@ function resolveBrokerServiceLabel(mode) {
474
2074
  return "dev.openscout";
475
2075
  }
476
2076
  }
477
- function legacyBrokerServiceLabel(mode) {
478
- switch (mode) {
479
- case "prod":
480
- return "com.openscout.broker";
481
- case "custom":
482
- return "com.openscout.broker.custom";
483
- case "dev":
484
- default:
485
- return "dev.openscout.broker";
486
- }
487
- }
488
2077
  function resolveBrokerServiceConfig() {
489
2078
  const mode = resolveBrokerServiceMode();
490
2079
  const label = resolveBrokerServiceLabel(mode);
2080
+ const serviceAdapter = resolveBrokerServiceAdapter();
491
2081
  const uid = typeof process.getuid === "function" ? process.getuid() : Number.parseInt(process.env.UID ?? "0", 10);
492
2082
  const supportPaths = resolveOpenScoutSupportPaths();
493
- const defaultSupportDir = join3(homedir3(), "Library", "Application Support", "OpenScout");
2083
+ const defaultSupportDir = join5(homedir5(), "Library", "Application Support", "OpenScout");
494
2084
  const supportDirectory = isTmpPath(supportPaths.supportDirectory) ? defaultSupportDir : supportPaths.supportDirectory;
495
- const runtimeDirectory = join3(supportDirectory, "runtime");
496
- const logsDirectory = join3(supportDirectory, "logs", "broker");
497
- const controlHome = isTmpPath(supportPaths.controlHome) ? join3(homedir3(), ".openscout", "control-plane") : supportPaths.controlHome;
2085
+ const runtimeDirectory = join5(supportDirectory, "runtime");
2086
+ const logsDirectory = join5(supportDirectory, "logs", "broker");
2087
+ const controlHome = isTmpPath(supportPaths.controlHome) ? join5(homedir5(), ".openscout", "control-plane") : supportPaths.controlHome;
498
2088
  const advertiseScope = resolveAdvertiseScope();
499
2089
  const brokerHost = resolveBrokerHost(advertiseScope);
500
- const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(DEFAULT_BROKER_PORT), 10);
501
- const brokerUrl = process.env.OPENSCOUT_BROKER_URL ?? buildDefaultBrokerUrl(brokerHost, brokerPort);
2090
+ const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(resolveBrokerPort()), 10);
2091
+ const brokerUrl = resolveBrokerUrl(brokerHost, brokerPort, advertiseScope);
502
2092
  const brokerSocketPath = process.env.OPENSCOUT_BROKER_SOCKET_PATH ?? buildDefaultBrokerSocketPath(runtimeDirectory);
503
- const launchAgentPath = join3(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
2093
+ const launchAgentPath = join5(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
504
2094
  return {
505
2095
  label,
506
2096
  mode,
@@ -511,11 +2101,11 @@ function resolveBrokerServiceConfig() {
511
2101
  supportDirectory,
512
2102
  runtimeDirectory,
513
2103
  logsDirectory,
514
- stdoutLogPath: join3(logsDirectory, "stdout.log"),
515
- stderrLogPath: join3(logsDirectory, "stderr.log"),
2104
+ stdoutLogPath: join5(logsDirectory, "stdout.log"),
2105
+ stderrLogPath: join5(logsDirectory, "stderr.log"),
516
2106
  controlHome,
517
2107
  runtimePackageDir: runtimePackageDir(),
518
- bunExecutable: resolveBunExecutable2(),
2108
+ bunExecutable: serviceAdapter === "macos-scoutd" ? resolveBunExecutable2() : resolveOptionalBunExecutable(),
519
2109
  brokerHost,
520
2110
  brokerPort,
521
2111
  brokerUrl,
@@ -524,86 +2114,259 @@ function resolveBrokerServiceConfig() {
524
2114
  coreAgents: readCoreAgentsSync()
525
2115
  };
526
2116
  }
527
- function renderLaunchAgentPlist(config) {
528
- const launchPath = resolveLaunchAgentPATH();
529
- const coreAgentsValue = (config.coreAgents ?? []).join(",");
530
- const envEntries = {
2117
+ function resolveBrokerServiceAdapter(env = process.env, platform = process.platform) {
2118
+ return defaultServiceAdapterForPlatform(platform, env);
2119
+ }
2120
+ function executableCandidate(path) {
2121
+ return isExecutablePath(path) ? path : null;
2122
+ }
2123
+ function resolveExecutableName(name) {
2124
+ return resolveExecutableFromSearch({ names: [name] })?.path ?? null;
2125
+ }
2126
+ function resolveEnvExecutable(value) {
2127
+ const trimmed = value?.trim();
2128
+ if (!trimmed)
2129
+ return null;
2130
+ if (trimmed.includes("/") || trimmed.startsWith(".")) {
2131
+ const expanded = resolve2(expandHomePath(trimmed));
2132
+ return executableCandidate(expanded);
2133
+ }
2134
+ return resolveExecutableName(trimmed);
2135
+ }
2136
+ function findWorkspaceRootFromRuntimeDir(runtimePackageDir2) {
2137
+ let current = resolve2(runtimePackageDir2);
2138
+ while (true) {
2139
+ if (existsSync5(join5(current, "Cargo.toml")) && existsSync5(join5(current, "crates", "scoutd", "Cargo.toml"))) {
2140
+ return current;
2141
+ }
2142
+ const parent = dirname3(current);
2143
+ if (parent === current)
2144
+ return null;
2145
+ current = parent;
2146
+ }
2147
+ }
2148
+ function workspaceScoutdAllowed() {
2149
+ const raw = process.env.OPENSCOUT_ALLOW_WORKSPACE_SCOUTD?.trim().toLowerCase();
2150
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
2151
+ }
2152
+ function resolveScoutdCommand(config = resolveBrokerServiceConfig()) {
2153
+ const explicit = resolveEnvExecutable(process.env.OPENSCOUT_SCOUTD_BIN);
2154
+ if (explicit) {
2155
+ return { path: explicit, source: "env" };
2156
+ }
2157
+ const workspaceRoot = findWorkspaceRootFromRuntimeDir(config.runtimePackageDir);
2158
+ const packageCandidates = [
2159
+ join5(config.runtimePackageDir, "bin", "scoutd"),
2160
+ join5(config.runtimePackageDir, "native", "scoutd"),
2161
+ join5(config.runtimePackageDir, "scoutd"),
2162
+ workspaceRoot ? join5(workspaceRoot, "packages", "cli", "bin", "scoutd") : null,
2163
+ workspaceRoot ? join5(workspaceRoot, "packages", "runtime", "bin", "scoutd") : null,
2164
+ join5(config.runtimeDirectory, "scoutd"),
2165
+ join5(dirname3(config.runtimePackageDir), "scout", "bin", "scoutd")
2166
+ ];
2167
+ for (const candidate of packageCandidates) {
2168
+ const resolved = executableCandidate(candidate);
2169
+ if (resolved) {
2170
+ return { path: resolved, source: "package" };
2171
+ }
2172
+ }
2173
+ const fromPath = resolveExecutableName("scoutd");
2174
+ if (fromPath) {
2175
+ return { path: fromPath, source: "path" };
2176
+ }
2177
+ if (workspaceRoot && workspaceScoutdAllowed()) {
2178
+ for (const candidate of [
2179
+ join5(workspaceRoot, "target", "release", "scoutd"),
2180
+ join5(workspaceRoot, "target", "debug", "scoutd")
2181
+ ]) {
2182
+ const resolved = executableCandidate(candidate);
2183
+ if (resolved) {
2184
+ return { path: resolved, source: "workspace" };
2185
+ }
2186
+ }
2187
+ }
2188
+ return null;
2189
+ }
2190
+ function nativeServiceEnvironment(config, scoutdPath) {
2191
+ const env = {
2192
+ ...process.env,
2193
+ OPENSCOUT_SCOUTD_BIN: scoutdPath,
2194
+ OPENSCOUT_RUNTIME_PACKAGE_DIR: config.runtimePackageDir,
2195
+ OPENSCOUT_SUPPORT_DIRECTORY: config.supportDirectory,
2196
+ OPENSCOUT_CONTROL_HOME: config.controlHome,
531
2197
  OPENSCOUT_BROKER_HOST: config.brokerHost,
532
2198
  OPENSCOUT_BROKER_PORT: String(config.brokerPort),
533
2199
  OPENSCOUT_BROKER_URL: config.brokerUrl,
534
2200
  OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
535
- OPENSCOUT_CONTROL_HOME: config.controlHome,
536
2201
  OPENSCOUT_BROKER_SERVICE_MODE: config.mode,
537
2202
  OPENSCOUT_BROKER_SERVICE_LABEL: config.label,
538
2203
  OPENSCOUT_SERVICE_LABEL: config.label,
539
- OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope,
540
- HOME: homedir3(),
541
- PATH: launchPath,
542
- ...collectOptionalEnvVars([
543
- "OPENSCOUT_MESH_ID",
544
- "OPENSCOUT_MESH_SEEDS",
545
- "OPENSCOUT_MESH_DISCOVERY_INTERVAL_MS",
546
- "OPENSCOUT_NODE_NAME",
547
- "OPENSCOUT_NODE_ID",
548
- "OPENSCOUT_NODE_QUALIFIER",
549
- "OPENSCOUT_TAILSCALE_BIN",
550
- "OPENSCOUT_TAILSCALE_STATUS_JSON",
551
- "OPENSCOUT_SSE_KEEPALIVE_MS",
552
- "OPENSCOUT_WEB_EDGE_SCHEME",
553
- "OPENSCOUT_WEB_PUBLIC_ORIGIN",
554
- "OPENSCOUT_WEB_PORTAL_HOST",
555
- "OPENSCOUT_WEB_LOCAL_NAME",
556
- "OPENSCOUT_WEB_ADVERTISED_HOST",
557
- "OPENSCOUT_WEB_TRUSTED_HOSTS",
558
- "OPENSCOUT_WEB_TRUSTED_ORIGINS",
559
- "OPENSCOUT_WEB_PORT",
560
- "OPENSCOUT_WEB_FLAG_BUNDLE",
561
- "OPENSCOUT_WEB_EXPERIENCE",
562
- "OPENSCOUT_WEB_AB_VARIANT"
563
- ])
2204
+ OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
564
2205
  };
565
- if (coreAgentsValue) {
566
- envEntries["OPENSCOUT_CORE_AGENTS"] = coreAgentsValue;
567
- }
568
- const envBlock = Object.entries(envEntries).map(([key, value]) => `
569
- <key>${xmlEscape(key)}</key>
570
- <string>${xmlEscape(value)}</string>`).join("");
571
- return `<?xml version="1.0" encoding="UTF-8"?>
572
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
573
- <plist version="1.0">
574
- <dict>
575
- <key>Label</key>
576
- <string>${xmlEscape(config.label)}</string>
577
- <key>ProgramArguments</key>
578
- <array>
579
- <string>${xmlEscape(config.bunExecutable)}</string>
580
- <string>${xmlEscape(join3(config.runtimePackageDir, "bin", "openscout-runtime.mjs"))}</string>
581
- <string>base</string>
582
- </array>
583
- <key>WorkingDirectory</key>
584
- <string>${xmlEscape(config.runtimePackageDir)}</string>
585
- <key>RunAtLoad</key>
586
- <true/>
587
- <key>KeepAlive</key>
588
- <dict>
589
- <key>SuccessfulExit</key>
590
- <false/>
591
- </dict>
592
- <key>StandardOutPath</key>
593
- <string>${xmlEscape(config.stdoutLogPath)}</string>
594
- <key>StandardErrorPath</key>
595
- <string>${xmlEscape(config.stderrLogPath)}</string>
596
- <key>EnvironmentVariables</key>
597
- <dict>${envBlock}
598
- </dict>
599
- </dict>
600
- </plist>
601
- `;
2206
+ if (config.bunExecutable) {
2207
+ env.OPENSCOUT_BUN_BIN = config.bunExecutable;
2208
+ }
2209
+ if (config.coreAgents.length > 0) {
2210
+ env.OPENSCOUT_CORE_AGENTS = config.coreAgents.join(",");
2211
+ }
2212
+ return env;
2213
+ }
2214
+ function isRecord3(value) {
2215
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2216
+ }
2217
+ function readString2(value) {
2218
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
2219
+ }
2220
+ function readBoolean(value) {
2221
+ return typeof value === "boolean" ? value : undefined;
2222
+ }
2223
+ function readNumber2(value) {
2224
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2225
+ }
2226
+ function readHealthTransport(value) {
2227
+ return value === "unix_socket" || value === "http" || value === "in_process" ? value : undefined;
2228
+ }
2229
+ function normalizeNativeServiceStatus(input, config) {
2230
+ const healthRecord = isRecord3(input.health) ? input.health : {};
2231
+ const healthReachable = readBoolean(healthRecord.reachable) ?? readBoolean(input.reachable) ?? false;
2232
+ const healthOk = readBoolean(healthRecord.ok) ?? (typeof input.health === "boolean" ? input.health : undefined) ?? false;
2233
+ const healthError = readString2(healthRecord.error) ?? readString2(input.healthError);
2234
+ const healthTransport = readHealthTransport(healthRecord.transport) ?? readHealthTransport(input.healthTransport);
2235
+ const healthNodeId = readString2(healthRecord.nodeId);
2236
+ const healthMeshId = readString2(healthRecord.meshId);
2237
+ const healthSocketFallbackError = readString2(healthRecord.socketFallbackError);
2238
+ const healthCounts = isRecord3(healthRecord.counts) ? healthRecord.counts : undefined;
2239
+ const installed = readBoolean(input.installed) ?? existsSync5(config.launchAgentPath);
2240
+ const loaded = readBoolean(input.loaded) ?? false;
2241
+ const stdoutLogPath = readString2(input.stdoutLogPath) ?? config.stdoutLogPath;
2242
+ const stderrLogPath = readString2(input.stderrLogPath) ?? config.stderrLogPath;
2243
+ const lastLogLine = readString2(input.lastLogLine) ?? (healthReachable ? readLastLogLine([stdoutLogPath, stderrLogPath]) : readLastLogLine([stderrLogPath, stdoutLogPath]));
2244
+ return {
2245
+ label: readString2(input.label) ?? config.label,
2246
+ mode: readString2(input.mode) ?? config.mode,
2247
+ launchAgentPath: readString2(input.launchAgentPath) ?? config.launchAgentPath,
2248
+ bootoutCommand: readString2(input.bootoutCommand) ?? bootoutCommand(config),
2249
+ brokerUrl: readString2(input.brokerUrl) ?? config.brokerUrl,
2250
+ brokerSocketPath: readString2(input.brokerSocketPath) ?? config.brokerSocketPath,
2251
+ supportDirectory: readString2(input.supportDirectory) ?? config.supportDirectory,
2252
+ runtimeDirectory: readString2(input.runtimeDirectory) ?? config.runtimeDirectory,
2253
+ controlHome: readString2(input.controlHome) ?? config.controlHome,
2254
+ stdoutLogPath,
2255
+ stderrLogPath,
2256
+ installed,
2257
+ loaded,
2258
+ pid: readNumber2(input.pid) ?? null,
2259
+ launchdState: readString2(input.launchdState) ?? null,
2260
+ lastExitStatus: readNumber2(input.lastExitStatus) ?? null,
2261
+ usesLaunchAgent: readBoolean(input.usesLaunchAgent) ?? (installed || loaded),
2262
+ reachable: healthReachable,
2263
+ serviceAdapter: "macos-scoutd",
2264
+ health: {
2265
+ reachable: healthReachable,
2266
+ ok: healthOk,
2267
+ checkedAt: readNumber2(healthRecord.checkedAt) ?? Date.now(),
2268
+ transport: healthTransport,
2269
+ socketPath: config.brokerSocketPath,
2270
+ ...healthSocketFallbackError ? { socketFallbackError: healthSocketFallbackError } : {},
2271
+ ...healthNodeId ? { nodeId: healthNodeId } : {},
2272
+ ...healthMeshId ? { meshId: healthMeshId } : {},
2273
+ ...healthCounts ? { counts: healthCounts } : {},
2274
+ ...isRecord3(healthRecord.build) ? { build: healthRecord.build } : {},
2275
+ ...isRecord3(healthRecord.services) ? { services: healthRecord.services } : {},
2276
+ error: healthError
2277
+ },
2278
+ lastLogLine
2279
+ };
2280
+ }
2281
+ var SCOUTD_MAX_BUFFER = 2 * 1024 * 1024;
2282
+ var SCOUTD_DEFAULT_TIMEOUT_MS = 45000;
2283
+ var SCOUTD_KILL_GRACE_MS = 250;
2284
+ function spawnScoutdJson(scoutdPath, command, env, timeoutMs) {
2285
+ return new Promise((resolvePromise, reject) => {
2286
+ const child = spawn2(scoutdPath, [command, "--json"], {
2287
+ env,
2288
+ stdio: ["ignore", "pipe", "pipe"]
2289
+ });
2290
+ let stdout = "";
2291
+ let stderr = "";
2292
+ let settled = false;
2293
+ let timedOut = false;
2294
+ const killTimer = setTimeout(() => {
2295
+ timedOut = true;
2296
+ terminate();
2297
+ fail(new Error(`scoutd ${command} timed out after ${timeoutMs}ms`));
2298
+ }, timeoutMs);
2299
+ killTimer.unref?.();
2300
+ function terminate() {
2301
+ child.kill("SIGTERM");
2302
+ const hardKillTimer = setTimeout(() => child.kill("SIGKILL"), SCOUTD_KILL_GRACE_MS);
2303
+ hardKillTimer.unref?.();
2304
+ }
2305
+ function cleanup() {
2306
+ clearTimeout(killTimer);
2307
+ }
2308
+ function fail(error) {
2309
+ if (settled)
2310
+ return;
2311
+ settled = true;
2312
+ cleanup();
2313
+ reject(error);
2314
+ }
2315
+ function succeed(output) {
2316
+ if (settled)
2317
+ return;
2318
+ settled = true;
2319
+ cleanup();
2320
+ resolvePromise(output);
2321
+ }
2322
+ function append(kind, chunk) {
2323
+ const text = typeof chunk === "string" ? chunk : String(chunk);
2324
+ if (kind === "stdout")
2325
+ stdout += text;
2326
+ else
2327
+ stderr += text;
2328
+ if (stdout.length + stderr.length > SCOUTD_MAX_BUFFER) {
2329
+ terminate();
2330
+ fail(new Error(`scoutd ${command} exceeded output limit`));
2331
+ }
2332
+ }
2333
+ child.stdout.setEncoding("utf8");
2334
+ child.stderr.setEncoding("utf8");
2335
+ child.stdout.on("data", (chunk) => append("stdout", chunk));
2336
+ child.stderr.on("data", (chunk) => append("stderr", chunk));
2337
+ child.on("error", (error) => fail(new Error(`scoutd ${command} failed: ${error.message}`)));
2338
+ child.on("close", (code, signal) => {
2339
+ if (settled || timedOut)
2340
+ return;
2341
+ const trimmedStdout = stdout.trim();
2342
+ const trimmedStderr = stderr.trim();
2343
+ if ((code ?? 1) !== 0) {
2344
+ const detail = trimmedStderr || trimmedStdout || `exit ${signal ?? code ?? "unknown status"}`;
2345
+ fail(new Error(`scoutd ${command} failed: ${detail}`));
2346
+ return;
2347
+ }
2348
+ succeed(trimmedStdout);
2349
+ });
2350
+ });
2351
+ }
2352
+ async function runScoutdServiceCommand(command, config, timeoutMs = SCOUTD_DEFAULT_TIMEOUT_MS, runScoutdJson = spawnScoutdJson) {
2353
+ const scoutd = resolveScoutdCommand(config);
2354
+ if (!scoutd) {
2355
+ throw new Error("Unable to locate scoutd for broker service management. Build scoutd with `npm run scoutd:build`, install a package that includes scoutd, or set OPENSCOUT_SCOUTD_BIN.");
2356
+ }
2357
+ const stdout = await runScoutdJson(scoutd.path, command, nativeServiceEnvironment(config, scoutd.path), timeoutMs);
2358
+ let parsed;
2359
+ try {
2360
+ parsed = JSON.parse(stdout);
2361
+ } catch {
2362
+ throw new Error(`scoutd ${command} returned non-JSON stdout: ${stdout.slice(0, 400)}`);
2363
+ }
2364
+ return normalizeNativeServiceStatus(parsed, config);
602
2365
  }
603
2366
  function readCoreAgentsSync() {
604
2367
  try {
605
2368
  const settingsPath = resolveOpenScoutSupportPaths().settingsPath;
606
- const raw = readFileSync(settingsPath, "utf8");
2369
+ const raw = readFileSync4(settingsPath, "utf8");
607
2370
  const settings = JSON.parse(raw);
608
2371
  const raw_agents = settings?.agents?.coreAgents;
609
2372
  if (Array.isArray(raw_agents)) {
@@ -612,88 +2375,14 @@ function readCoreAgentsSync() {
612
2375
  } catch {}
613
2376
  return [];
614
2377
  }
615
- function collectOptionalEnvVars(keys) {
616
- const entries = {};
617
- for (const key of keys) {
618
- const value = process.env[key];
619
- if (typeof value === "string" && value.trim().length > 0) {
620
- entries[key] = value;
621
- }
622
- }
623
- return entries;
624
- }
625
- function resolveLaunchAgentPATH() {
626
- const entries = [
627
- join3(homedir3(), ".bun", "bin"),
628
- ...(process.env.PATH ?? "").split(":").filter(Boolean),
629
- "/opt/homebrew/bin",
630
- "/usr/local/bin",
631
- "/usr/bin",
632
- "/bin",
633
- "/usr/sbin",
634
- "/sbin"
635
- ];
636
- return Array.from(new Set(entries)).filter((e) => !isTmpPath(e)).join(":");
637
- }
638
- function xmlEscape(value) {
639
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
640
- }
641
- function ensureParentDirectory(filePath) {
642
- mkdirSync2(dirname2(filePath), { recursive: true });
643
- }
644
- function ensureServiceDirectories(config) {
645
- ensureOpenScoutCleanSlateSync();
646
- mkdirSync2(config.supportDirectory, { recursive: true });
647
- mkdirSync2(config.runtimeDirectory, { recursive: true, mode: 448 });
648
- chmodSync(config.runtimeDirectory, 448);
649
- mkdirSync2(config.logsDirectory, { recursive: true });
650
- mkdirSync2(config.controlHome, { recursive: true });
651
- ensureParentDirectory(config.launchAgentPath);
652
- }
653
- function writeLaunchAgentPlist(config) {
654
- ensureServiceDirectories(config);
655
- writeFileSync2(config.launchAgentPath, renderLaunchAgentPlist(config), "utf8");
656
- }
657
2378
  function bootoutCommand(config) {
658
- return `${launchctlPath()} bootout ${config.serviceTarget}`;
659
- }
660
- function legacyLaunchAgentPath(config) {
661
- return join3(homedir3(), "Library", "LaunchAgents", `${legacyBrokerServiceLabel(config.mode)}.plist`);
662
- }
663
- function legacyServiceTarget(config) {
664
- return `${config.domainTarget}/${legacyBrokerServiceLabel(config.mode)}`;
665
- }
666
- function bootoutLegacyBrokerService(config) {
667
- const legacyTarget = legacyServiceTarget(config);
668
- if (legacyTarget === config.serviceTarget) {
669
- return;
670
- }
671
- runCommand(launchctlPath(), ["bootout", legacyTarget], { allowFailure: true });
672
- }
673
- function runCommand(command, args, options) {
674
- const result = spawnSync(command, args, {
675
- env: {
676
- ...process.env,
677
- ...options?.env ?? {}
678
- },
679
- encoding: "utf8"
680
- });
681
- const stdout = (result.stdout ?? "").trim();
682
- const stderr = (result.stderr ?? "").trim();
683
- const exitCode = result.status ?? 1;
684
- if (exitCode !== 0 && !options?.allowFailure) {
685
- throw new Error(stderr || stdout || `${command} exited with status ${exitCode}`);
686
- }
687
- return { exitCode, stdout, stderr };
688
- }
689
- function launchctlPath() {
690
- return "/bin/launchctl";
2379
+ return `/bin/launchctl bootout ${config.serviceTarget}`;
691
2380
  }
692
2381
  function readLogLines(path) {
693
- if (!existsSync3(path)) {
2382
+ if (!existsSync5(path)) {
694
2383
  return [];
695
2384
  }
696
- return readFileSync(path, "utf8").split(`
2385
+ return readFileSync4(path, "utf8").split(`
697
2386
  `).map((line) => line.trim()).filter(Boolean);
698
2387
  }
699
2388
  function isPackageScriptBanner(line) {
@@ -726,170 +2415,120 @@ function readLastLogLine(paths) {
726
2415
  }
727
2416
  return fallback;
728
2417
  }
729
- function parseLaunchctlPrint(output) {
730
- const pidMatch = output.match(/\bpid = (\d+)/);
731
- const stateMatch = output.match(/\bstate = ([^\n]+)/);
732
- const lastExitMatch = output.match(/\blast exit code = (-?\d+)/i) || output.match(/\blast exit status = (-?\d+)/i);
2418
+ async function readHeadlessBrokerHealth(config) {
2419
+ const result = await requestScoutBrokerJsonWithTrace(config.brokerUrl, "/health", { socketPath: config.brokerSocketPath });
733
2420
  return {
734
- pid: pidMatch ? Number.parseInt(pidMatch[1] ?? "0", 10) : null,
735
- launchdState: stateMatch?.[1]?.trim() ?? null,
736
- lastExitStatus: lastExitMatch ? Number.parseInt(lastExitMatch[1] ?? "0", 10) : null
2421
+ health: result.value,
2422
+ trace: result.trace
737
2423
  };
738
2424
  }
739
- function inspectLaunchctl(config) {
740
- const printResult = runCommand(launchctlPath(), ["print", config.serviceTarget], { allowFailure: true });
741
- if (printResult.exitCode !== 0) {
742
- return {
743
- loaded: false,
744
- pid: null,
745
- launchdState: null,
746
- lastExitStatus: null,
747
- raw: printResult.stderr || printResult.stdout
748
- };
749
- }
750
- return {
751
- loaded: true,
752
- raw: printResult.stdout,
753
- ...parseLaunchctlPrint(printResult.stdout)
754
- };
2425
+ function headlessLifecycleError(command) {
2426
+ return new Error(`Next step: run \`openscout-runtime broker\` in this shell or under your process manager. ` + `The headless service adapter leaves broker ${command} to that foreground process.`);
755
2427
  }
756
- async function fetchHealthSnapshot(config) {
757
- const controller = new AbortController;
758
- const timeout = setTimeout(() => controller.abort(), 1000);
2428
+ async function runHeadlessForegroundServiceCommand(command, config, readHealth = readHeadlessBrokerHealth) {
2429
+ if (command !== "status") {
2430
+ throw headlessLifecycleError(command);
2431
+ }
759
2432
  const checkedAt = Date.now();
760
2433
  try {
761
- const { value: payload, trace } = await requestScoutBrokerJsonWithTrace(config.brokerUrl, "/health", {
762
- signal: controller.signal,
763
- socketPath: config.brokerSocketPath
764
- });
2434
+ const { health, trace } = await readHealth(config);
2435
+ const reachable = true;
765
2436
  return {
766
- reachable: true,
767
- ok: Boolean(payload.ok),
768
- checkedAt,
769
- transport: trace.transport,
770
- socketPath: trace.socketPath,
771
- socketFallbackError: trace.socketFallbackError,
772
- nodeId: payload.nodeId,
773
- meshId: payload.meshId,
774
- counts: payload.counts ? {
775
- nodes: payload.counts.nodes ?? 0,
776
- actors: payload.counts.actors ?? 0,
777
- agents: payload.counts.agents ?? 0,
778
- agentRecords: payload.counts.agentRecords,
779
- rawAgentRecords: payload.counts.rawAgentRecords,
780
- configuredAgents: payload.counts.configuredAgents,
781
- scoutManagedAgents: payload.counts.scoutManagedAgents,
782
- currentAgentRegistrations: payload.counts.currentAgentRegistrations,
783
- localAgentRegistrations: payload.counts.localAgentRegistrations,
784
- remoteAgentRegistrations: payload.counts.remoteAgentRegistrations,
785
- staleAgentRegistrations: payload.counts.staleAgentRegistrations,
786
- retiredAgentRegistrations: payload.counts.retiredAgentRegistrations,
787
- oneTimeAgentCards: payload.counts.oneTimeAgentCards,
788
- persistentAgentCards: payload.counts.persistentAgentCards,
789
- conversations: payload.counts.conversations ?? 0,
790
- messages: payload.counts.messages ?? 0,
791
- flights: payload.counts.flights ?? 0,
792
- collaborationRecords: payload.counts.collaborationRecords ?? 0
793
- } : undefined
2437
+ serviceAdapter: "headless-foreground",
2438
+ label: config.label,
2439
+ mode: config.mode,
2440
+ launchAgentPath: config.launchAgentPath,
2441
+ bootoutCommand: "headless-foreground does not use launchd",
2442
+ brokerUrl: config.brokerUrl,
2443
+ brokerSocketPath: config.brokerSocketPath,
2444
+ supportDirectory: config.supportDirectory,
2445
+ runtimeDirectory: config.runtimeDirectory,
2446
+ controlHome: config.controlHome,
2447
+ stdoutLogPath: config.stdoutLogPath,
2448
+ stderrLogPath: config.stderrLogPath,
2449
+ installed: false,
2450
+ loaded: reachable,
2451
+ pid: null,
2452
+ launchdState: null,
2453
+ lastExitStatus: null,
2454
+ usesLaunchAgent: false,
2455
+ reachable,
2456
+ health: {
2457
+ reachable,
2458
+ ok: Boolean(health.ok),
2459
+ checkedAt,
2460
+ transport: trace.transport,
2461
+ socketPath: trace.socketPath,
2462
+ socketFallbackError: trace.socketFallbackError,
2463
+ nodeId: health.nodeId ?? undefined,
2464
+ meshId: health.meshId ?? undefined,
2465
+ counts: health.counts ?? undefined,
2466
+ build: health.build,
2467
+ services: health.services
2468
+ },
2469
+ lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
794
2470
  };
795
2471
  } catch (error) {
796
2472
  return {
2473
+ serviceAdapter: "headless-foreground",
2474
+ label: config.label,
2475
+ mode: config.mode,
2476
+ launchAgentPath: config.launchAgentPath,
2477
+ bootoutCommand: "headless-foreground does not use launchd",
2478
+ brokerUrl: config.brokerUrl,
2479
+ brokerSocketPath: config.brokerSocketPath,
2480
+ supportDirectory: config.supportDirectory,
2481
+ runtimeDirectory: config.runtimeDirectory,
2482
+ controlHome: config.controlHome,
2483
+ stdoutLogPath: config.stdoutLogPath,
2484
+ stderrLogPath: config.stderrLogPath,
2485
+ installed: false,
2486
+ loaded: false,
2487
+ pid: null,
2488
+ launchdState: null,
2489
+ lastExitStatus: null,
2490
+ usesLaunchAgent: false,
797
2491
  reachable: false,
798
- ok: false,
799
- checkedAt,
800
- socketPath: config.brokerSocketPath,
801
- error: error instanceof Error ? error.message : String(error)
2492
+ health: {
2493
+ reachable: false,
2494
+ ok: false,
2495
+ checkedAt,
2496
+ error: error instanceof Error ? error.message : String(error)
2497
+ },
2498
+ lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
802
2499
  };
803
- } finally {
804
- clearTimeout(timeout);
2500
+ }
2501
+ }
2502
+ async function runBrokerServiceCommand(command, config) {
2503
+ const adapter = resolveBrokerServiceAdapter();
2504
+ switch (adapter) {
2505
+ case "macos-scoutd":
2506
+ return await runScoutdServiceCommand(command, config);
2507
+ case "headless-foreground":
2508
+ return await runHeadlessForegroundServiceCommand(command, config);
2509
+ case "linux-systemd-user":
2510
+ throw new Error("The linux-systemd-user service adapter is not implemented yet. " + "Use OPENSCOUT_SERVICE_ADAPTER=headless-foreground and run `openscout-runtime broker` under your process manager.");
2511
+ case "windows-service":
2512
+ throw new Error("The windows-service adapter is not implemented yet. " + "Use OPENSCOUT_SERVICE_ADAPTER=headless-foreground and run `openscout-runtime broker` from a shell.");
805
2513
  }
806
2514
  }
807
2515
  async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
808
- ensureServiceDirectories(config);
809
- const launchctl = inspectLaunchctl(config);
810
- const health = await fetchHealthSnapshot(config);
811
- const installed = existsSync3(config.launchAgentPath);
812
- const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
813
- return {
814
- label: config.label,
815
- mode: config.mode,
816
- launchAgentPath: config.launchAgentPath,
817
- bootoutCommand: bootoutCommand(config),
818
- brokerUrl: config.brokerUrl,
819
- brokerSocketPath: config.brokerSocketPath,
820
- supportDirectory: config.supportDirectory,
821
- runtimeDirectory: config.runtimeDirectory,
822
- controlHome: config.controlHome,
823
- stdoutLogPath: config.stdoutLogPath,
824
- stderrLogPath: config.stderrLogPath,
825
- installed,
826
- loaded: launchctl.loaded,
827
- pid: launchctl.pid,
828
- launchdState: launchctl.launchdState,
829
- lastExitStatus: launchctl.lastExitStatus,
830
- usesLaunchAgent: installed || launchctl.loaded,
831
- reachable: health.reachable,
832
- health,
833
- lastLogLine
834
- };
2516
+ return runBrokerServiceCommand("status", config);
835
2517
  }
836
2518
  async function installBrokerService(config = resolveBrokerServiceConfig()) {
837
- bootoutLegacyBrokerService(config);
838
- writeLaunchAgentPlist(config);
839
- return brokerServiceStatus(config);
2519
+ return runBrokerServiceCommand("install", config);
840
2520
  }
841
2521
  async function startBrokerService(config = resolveBrokerServiceConfig()) {
842
- bootoutLegacyBrokerService(config);
843
- writeLaunchAgentPlist(config);
844
- runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
845
- await waitForBrokerServiceStopped(config);
846
- runCommand(launchctlPath(), ["bootstrap", config.domainTarget, config.launchAgentPath], { allowFailure: true });
847
- runCommand(launchctlPath(), ["kickstart", "-k", config.serviceTarget], { allowFailure: true });
848
- const attempts = Math.ceil(resolveBrokerStartTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
849
- for (let attempt = 0;attempt < attempts; attempt += 1) {
850
- const status2 = await brokerServiceStatus(config);
851
- if (status2.health.reachable) {
852
- return status2;
853
- }
854
- await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
855
- }
856
- const status = await brokerServiceStatus(config);
857
- throw new Error(status.lastLogLine ?? status.health.error ?? "Broker service did not become healthy.");
858
- }
859
- function sleep(ms) {
860
- return new Promise((resolve3) => setTimeout(resolve3, ms));
861
- }
862
- async function waitForBrokerServiceStopped(config) {
863
- let status = await brokerServiceStatus(config);
864
- const attempts = Math.ceil(resolveBrokerStopTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
865
- for (let attempt = 0;attempt < attempts; attempt += 1) {
866
- status = await brokerServiceStatus(config);
867
- if (!status.loaded && !status.health.reachable) {
868
- return status;
869
- }
870
- await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
871
- }
872
- return status;
2522
+ return runBrokerServiceCommand("start", config);
873
2523
  }
874
2524
  async function stopBrokerService(config = resolveBrokerServiceConfig()) {
875
- runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
876
- return waitForBrokerServiceStopped(config);
2525
+ return runBrokerServiceCommand("stop", config);
877
2526
  }
878
2527
  async function restartBrokerService(config = resolveBrokerServiceConfig()) {
879
- await stopBrokerService(config);
880
- return startBrokerService(config);
2528
+ return runBrokerServiceCommand("restart", config);
881
2529
  }
882
2530
  async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
883
- await stopBrokerService(config);
884
- if (existsSync3(config.launchAgentPath)) {
885
- rmSync2(config.launchAgentPath, { force: true });
886
- }
887
- const legacyPath = legacyLaunchAgentPath(config);
888
- bootoutLegacyBrokerService(config);
889
- if (existsSync3(legacyPath)) {
890
- rmSync2(legacyPath, { force: true });
891
- }
892
- return brokerServiceStatus(config);
2531
+ return runBrokerServiceCommand("uninstall", config);
893
2532
  }
894
2533
  async function main() {
895
2534
  const command = process.argv[2] ?? "status";
@@ -927,6 +2566,7 @@ async function main() {
927
2566
  }
928
2567
  function formatBrokerServiceStatus(status) {
929
2568
  const lines = [
2569
+ `service adapter: ${status.serviceAdapter ?? "unknown"}`,
930
2570
  `label: ${status.label}`,
931
2571
  `mode: ${status.mode}`,
932
2572
  `launch agent: ${status.installed ? status.launchAgentPath : "not installed"}`,
@@ -957,113 +2597,59 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1] && !pr
957
2597
  });
958
2598
  }
959
2599
 
960
- // packages/runtime/src/local-config.ts
961
- import {
962
- existsSync as existsSync4,
963
- mkdirSync as mkdirSync3,
964
- readFileSync as readFileSync2,
965
- renameSync,
966
- writeFileSync as writeFileSync3
967
- } from "fs";
968
- import { homedir as homedir4, hostname as osHostname } from "os";
969
- import { dirname as dirname3, join as join4 } from "path";
970
- var LOCAL_CONFIG_VERSION = 1;
971
- var DEFAULT_SCOUT_WEB_PORTAL_HOST = "scout.local";
972
- var DEFAULT_LOCAL_CONFIG = {
973
- version: LOCAL_CONFIG_VERSION,
974
- host: "127.0.0.1",
975
- webLocalName: undefined,
976
- ports: {
977
- broker: 65535,
978
- web: 3200,
979
- pairing: 7888
2600
+ // packages/runtime/src/local-edge.ts
2601
+ function normalizeViteHmrPath(value) {
2602
+ const trimmed = value?.trim();
2603
+ if (!trimmed) {
2604
+ return DEFAULT_SCOUT_WEB_VITE_HMR_PATH;
980
2605
  }
981
- };
982
- function normalizeLocalHostnameLabel(value) {
983
- const firstLabel = value?.trim().replace(/\.local\.?$/i, "").split(".").find((part) => part.trim().length > 0)?.trim();
984
- const normalized = firstLabel?.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
985
- return normalized || "localhost";
986
- }
987
- function normalizeLocalHostname(value) {
988
- const trimmed = value?.trim().replace(/\.$/, "").toLowerCase();
989
- const labels = trimmed?.split(".").map((label) => label.trim().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-")).filter(Boolean);
990
- return labels && labels.length > 0 ? labels.join(".") : "localhost";
991
- }
992
- function resolveScoutWebNamedHostname(name) {
993
- const normalized = normalizeLocalHostname(name);
994
- return normalized.includes(".") ? normalized : `${normalized}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
2606
+ const normalized = `${trimmed.startsWith("/") ? "" : "/"}${trimmed}`.replace(/\/{2,}/g, "/").replace(/\/+$/g, "");
2607
+ return normalized || DEFAULT_SCOUT_WEB_VITE_HMR_PATH;
995
2608
  }
996
- function resolveConfiguredScoutWebHostname(config = loadLocalConfig(), machineHostname = osHostname()) {
997
- if (config.webLocalName) {
998
- return resolveScoutWebNamedHostname(config.webLocalName);
2609
+ function resolveViteUpstream(input) {
2610
+ const url = input.viteDevUrl?.trim();
2611
+ if (url) {
2612
+ try {
2613
+ return new URL(url).host;
2614
+ } catch {
2615
+ return;
2616
+ }
999
2617
  }
1000
- return `${normalizeLocalHostnameLabel(machineHostname)}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
1001
- }
1002
- function localConfigHome() {
1003
- return process.env.OPENSCOUT_HOME ?? join4(homedir4(), ".openscout");
1004
- }
1005
- function localConfigPath() {
1006
- return join4(localConfigHome(), "config.json");
1007
- }
1008
- function loadLocalConfig() {
1009
- const configPath = localConfigPath();
1010
- if (!existsSync4(configPath))
1011
- return { version: LOCAL_CONFIG_VERSION };
1012
- try {
1013
- return validateLocalConfig(JSON.parse(readFileSync2(configPath, "utf8")));
1014
- } catch {
1015
- return { version: LOCAL_CONFIG_VERSION };
2618
+ if (typeof input.vitePort === "number" && input.vitePort > 0) {
2619
+ return `127.0.0.1:${input.vitePort}`;
1016
2620
  }
2621
+ return;
1017
2622
  }
1018
- function validateLocalConfig(input) {
1019
- if (!input || typeof input !== "object")
1020
- return { version: LOCAL_CONFIG_VERSION };
1021
- const raw = input;
1022
- const out = { version: LOCAL_CONFIG_VERSION };
1023
- if (typeof raw.host === "string" && raw.host.trim().length > 0) {
1024
- out.host = raw.host.trim();
1025
- }
1026
- if (typeof raw.webLocalName === "string" && raw.webLocalName.trim().length > 0) {
1027
- out.webLocalName = raw.webLocalName.trim();
1028
- }
1029
- if (raw.ports && typeof raw.ports === "object") {
1030
- const ports = raw.ports;
1031
- const p = {};
1032
- if (isValidPort(ports.broker))
1033
- p.broker = ports.broker;
1034
- if (isValidPort(ports.web))
1035
- p.web = ports.web;
1036
- if (isValidPort(ports.pairing))
1037
- p.pairing = ports.pairing;
1038
- if (Object.keys(p).length > 0)
1039
- out.ports = p;
2623
+ function normalizeRouteHost(value) {
2624
+ const normalized = value?.trim().replace(/\.$/, "").toLowerCase();
2625
+ if (!normalized || normalized.includes("/") || /\s/.test(normalized)) {
2626
+ return null;
1040
2627
  }
1041
- return out;
2628
+ return normalized;
1042
2629
  }
1043
- function isValidPort(value) {
1044
- return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
1045
- }
1046
- function resolveBrokerPort() {
1047
- return loadLocalConfig().ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
1048
- }
1049
- function resolveWebPort() {
1050
- return loadLocalConfig().ports?.web ?? DEFAULT_LOCAL_CONFIG.ports.web;
1051
- }
1052
-
1053
- // packages/runtime/src/local-edge.ts
1054
2630
  function uniqRoutes(routes) {
1055
2631
  const seen = new Set;
1056
2632
  const out = [];
1057
2633
  for (const route of routes) {
1058
- const key = `${route.host.toLowerCase()} ${route.upstream}`;
2634
+ const host = normalizeRouteHost(route.host);
2635
+ if (!host) {
2636
+ continue;
2637
+ }
2638
+ const key = `${host} ${route.upstream}`;
1059
2639
  if (seen.has(key)) {
1060
2640
  continue;
1061
2641
  }
1062
2642
  seen.add(key);
1063
- out.push(route);
2643
+ out.push({ ...route, host });
1064
2644
  }
1065
2645
  return out;
1066
2646
  }
2647
+ function formatCaddyHost(host) {
2648
+ if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
2649
+ return `[${host}]`;
2650
+ }
2651
+ return host;
2652
+ }
1067
2653
  function resolveOpenScoutLocalEdgeConfig(input = {}) {
1068
2654
  const portalHost = resolveScoutWebNamedHostname(input.portalHost ?? DEFAULT_SCOUT_WEB_PORTAL_HOST);
1069
2655
  const nodeHost = resolveScoutWebNamedHostname(input.nodeHost ?? resolveConfiguredScoutWebHostname());
@@ -1071,6 +2657,7 @@ function resolveOpenScoutLocalEdgeConfig(input = {}) {
1071
2657
  const scheme = input.scheme ?? "http";
1072
2658
  const upstream = `127.0.0.1:${input.webPort ?? resolveWebPort()}`;
1073
2659
  const brokerUpstream = `127.0.0.1:${input.brokerPort ?? resolveBrokerPort()}`;
2660
+ const viteUpstream = resolveViteUpstream(input);
1074
2661
  return {
1075
2662
  portalHost,
1076
2663
  nodeHost,
@@ -1079,8 +2666,18 @@ function resolveOpenScoutLocalEdgeConfig(input = {}) {
1079
2666
  brokerUpstream,
1080
2667
  routes: uniqRoutes([
1081
2668
  { host: portalHost, upstream },
1082
- { host: wildcardHost, upstream }
1083
- ])
2669
+ { host: wildcardHost, upstream },
2670
+ ...viteUpstream ? [{
2671
+ host: resolveScoutWebDevHostname(portalHost),
2672
+ upstream: viteUpstream,
2673
+ mode: "vite-dev"
2674
+ }] : [],
2675
+ ...(input.extraHosts ?? []).map((host) => ({ host, upstream }))
2676
+ ]),
2677
+ ...viteUpstream ? {
2678
+ viteUpstream,
2679
+ viteHmrPath: normalizeViteHmrPath(input.viteHmrPath)
2680
+ } : {}
1084
2681
  };
1085
2682
  }
1086
2683
  function renderOpenScoutStartPage(config) {
@@ -1281,12 +2878,10 @@ function renderOpenScoutStartPage(config) {
1281
2878
  </body>
1282
2879
  </html>`;
1283
2880
  }
1284
- function renderOpenScoutCaddyfile(config) {
1285
- const schemes = config.scheme === "both" ? ["http", "https"] : [config.scheme];
1286
- const startPage = renderOpenScoutStartPage(config);
1287
- const blocks = schemes.flatMap((scheme) => config.routes.map((route) => {
1288
- const host = scheme === "http" ? `http://${route.host}` : route.host;
1289
- return `${host} {
2881
+ function renderCaddyDefaultRouteBlock(config, route, scheme, startPage) {
2882
+ const routeHost = formatCaddyHost(route.host);
2883
+ const host = scheme === "http" ? `http://${routeHost}` : routeHost;
2884
+ return `${host} {
1290
2885
  ` + (scheme === "https" ? ` tls internal
1291
2886
  ` : "") + ` handle /__openscout/web/start {
1292
2887
  ` + ` rewrite * /v1/web/start
@@ -1298,8 +2893,8 @@ function renderOpenScoutCaddyfile(config) {
1298
2893
  ` + ` }
1299
2894
  ` + ` handle {
1300
2895
  ` + ` reverse_proxy ${route.upstream} {
1301
- ` + ` lb_try_duration 1s
1302
- ` + ` lb_try_interval 250ms
2896
+ ` + ` lb_try_duration 15s
2897
+ ` + ` lb_try_interval 500ms
1303
2898
  ` + ` }
1304
2899
  ` + ` }
1305
2900
  ` + ` handle_errors {
@@ -1309,7 +2904,24 @@ function renderOpenScoutCaddyfile(config) {
1309
2904
  ` + `HTML 200
1310
2905
  ` + ` }
1311
2906
  ` + `}`;
1312
- })).join(`
2907
+ }
2908
+ function renderCaddyViteDevRouteBlock(route, scheme) {
2909
+ const routeHost = formatCaddyHost(route.host);
2910
+ const host = scheme === "http" ? `http://${routeHost}` : routeHost;
2911
+ return `${host} {
2912
+ ` + (scheme === "https" ? ` tls internal
2913
+ ` : "") + ` handle {
2914
+ ` + ` reverse_proxy ${route.upstream} {
2915
+ ` + ` lb_try_duration 15s
2916
+ ` + ` lb_try_interval 500ms
2917
+ ` + ` }
2918
+ ` + ` }
2919
+ ` + `}`;
2920
+ }
2921
+ function renderOpenScoutCaddyfile(config) {
2922
+ const schemes = config.scheme === "both" ? ["http", "https"] : [config.scheme];
2923
+ const startPage = renderOpenScoutStartPage(config);
2924
+ const blocks = schemes.flatMap((scheme) => config.routes.map((route) => route.mode === "vite-dev" ? renderCaddyViteDevRouteBlock(route, scheme) : renderCaddyDefaultRouteBlock(config, route, scheme, startPage))).join(`
1313
2925
 
1314
2926
  `);
1315
2927
  return `${blocks}
@@ -1322,8 +2934,8 @@ var RESTART_MAX_DELAY_MS = 30000;
1322
2934
  var BROKER_HEALTH_TIMEOUT_MS = 30000;
1323
2935
  var BROKER_HEALTH_POLL_MS = 250;
1324
2936
  var CHILD_SHUTDOWN_TIMEOUT_MS = 12000;
1325
- var MENU_BUNDLE_ID = "com.openscout.menu";
1326
- var MENU_PROCESS_NAME = "OpenScoutMenu";
2937
+ var MENU_BUNDLE_ID = "app.openscout.scout.menu";
2938
+ var MENU_PROCESS_NAME = "ScoutMenu";
1327
2939
  var PROCESS_NAME = "scout-base";
1328
2940
  var BROKER_LAUNCHER_PROCESS_NAME = "scout-broker-run";
1329
2941
  var EDGE_PROCESS_NAME = "scout-edge";
@@ -1336,8 +2948,9 @@ var mdnsProcesses = [];
1336
2948
  var brokerRestartDelayMs = RESTART_MIN_DELAY_MS;
1337
2949
  var edgeRestartDelayMs = RESTART_MIN_DELAY_MS;
1338
2950
  var supervisedWebPid = null;
1339
- var supervisorKeepAlive = null;
2951
+ var baseKeepAlive = null;
1340
2952
  var config = resolveBrokerServiceConfig();
2953
+ var brokerControlUrl = buildLocalBrokerControlUrl(config.brokerHost, config.brokerPort);
1341
2954
  function log(message, details) {
1342
2955
  if (details === undefined) {
1343
2956
  console.log(`[openscout-base] ${message}`);
@@ -1352,24 +2965,54 @@ function warn(message, details) {
1352
2965
  }
1353
2966
  console.warn(`[openscout-base] ${message}`, details);
1354
2967
  }
2968
+ function splitCsv(value) {
2969
+ return (value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
2970
+ }
2971
+ function appendCsvValues(input, values) {
2972
+ const seen = new Set;
2973
+ const out = [];
2974
+ for (const value of [...splitCsv(input), ...values]) {
2975
+ const normalized = value.trim().toLowerCase();
2976
+ if (!normalized || seen.has(normalized)) {
2977
+ continue;
2978
+ }
2979
+ seen.add(normalized);
2980
+ out.push(normalized);
2981
+ }
2982
+ return out.length > 0 ? out.join(",") : undefined;
2983
+ }
2984
+ function resolveTailnetWebHosts() {
2985
+ return readTailscaleSelfWebHostsSync();
2986
+ }
2987
+ function resolveWebTrustedHostsEnv() {
2988
+ return appendCsvValues(process.env.OPENSCOUT_WEB_TRUSTED_HOSTS, resolveTailnetWebHosts());
2989
+ }
1355
2990
  function ensureDirectory(path) {
1356
- mkdirSync4(path, { recursive: true });
2991
+ mkdirSync2(path, { recursive: true });
1357
2992
  }
1358
2993
  function logFile(name) {
1359
- const dir = join5(config.supportDirectory, "logs", "base");
2994
+ const dir = join6(config.supportDirectory, "logs", "base");
1360
2995
  ensureDirectory(dir);
1361
- return openSync(join5(dir, name), "a");
2996
+ return openSync(join6(dir, name), "a");
1362
2997
  }
1363
2998
  function runtimeEntrypoint(config2) {
1364
- return join5(config2.runtimePackageDir, "bin", "openscout-runtime.mjs");
2999
+ return join6(config2.runtimePackageDir, "bin", "openscout-runtime.mjs");
1365
3000
  }
1366
3001
  function spawnBroker() {
1367
3002
  if (shuttingDown || brokerProcess) {
1368
3003
  return;
1369
3004
  }
3005
+ if (!config.bunExecutable) {
3006
+ warn("broker supervisor cannot start broker without Bun", {
3007
+ hint: "Use the headless broker entrypoint under Node or install Bun for the macOS base supervisor."
3008
+ });
3009
+ scheduleBrokerRestart();
3010
+ return;
3011
+ }
3012
+ const webTrustedHostsEnv = resolveWebTrustedHostsEnv();
1370
3013
  const stdout = logFile("broker.stdout.log");
1371
3014
  const stderr = logFile("broker.stderr.log");
1372
- brokerProcess = spawn(config.bunExecutable, [
3015
+ brokerProcess = spawn3(config.bunExecutable, [
1373
3016
  "run",
1374
3017
  runtimeEntrypoint(config),
1375
3018
  "broker"
@@ -1384,7 +3027,9 @@ function spawnBroker() {
1384
3027
  OPENSCOUT_BROKER_URL: config.brokerUrl,
1385
3028
  OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
1386
3029
  OPENSCOUT_CONTROL_HOME: config.controlHome,
1387
- OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
3030
+ OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope,
3031
+ ...openScoutNetworkServiceEnvironment(process.env),
3032
+ ...webTrustedHostsEnv ? { OPENSCOUT_WEB_TRUSTED_HOSTS: webTrustedHostsEnv } : {}
1388
3033
  },
1389
3034
  stdio: ["ignore", stdout, stderr]
1390
3035
  });
@@ -1412,7 +3057,7 @@ async function waitForBrokerHealth(timeoutMs = BROKER_HEALTH_TIMEOUT_MS) {
1412
3057
  const deadline = Date.now() + timeoutMs;
1413
3058
  while (Date.now() < deadline) {
1414
3059
  try {
1415
- const response = await fetch(new URL("/health", config.brokerUrl), {
3060
+ const response = await fetch(new URL("/health", brokerControlUrl), {
1416
3061
  headers: { accept: "application/json" },
1417
3062
  signal: AbortSignal.timeout(1000)
1418
3063
  });
@@ -1424,7 +3069,7 @@ async function waitForBrokerHealth(timeoutMs = BROKER_HEALTH_TIMEOUT_MS) {
1424
3069
  }
1425
3070
  }
1426
3071
  } catch {}
1427
- await sleep2(BROKER_HEALTH_POLL_MS);
3072
+ await sleep(BROKER_HEALTH_POLL_MS);
1428
3073
  }
1429
3074
  return false;
1430
3075
  }
@@ -1446,19 +3091,25 @@ function resolveEdgeConfig() {
1446
3091
  nodeHost,
1447
3092
  scheme: resolveEdgeScheme(),
1448
3093
  brokerPort: config.brokerPort,
1449
- webPort: Number.parseInt(process.env.OPENSCOUT_WEB_PORT ?? "", 10) || resolveWebPort()
3094
+ webPort: Number.parseInt(process.env.OPENSCOUT_WEB_PORT ?? "", 10) || resolveWebPort(),
3095
+ viteDevUrl: process.env.OPENSCOUT_WEB_VITE_URL,
3096
+ viteHmrPath: process.env.OPENSCOUT_WEB_VITE_HMR_PATH,
3097
+ extraHosts: [
3098
+ ...splitCsv(process.env.OPENSCOUT_WEB_TRUSTED_HOSTS),
3099
+ ...resolveTailnetWebHosts()
3100
+ ]
1450
3101
  });
1451
3102
  }
1452
3103
  function resolveLocalEdgeCaddyfilePath() {
1453
- const dir = join5(homedir5(), ".scout", "local-edge");
3104
+ const dir = join6(homedir6(), ".scout", "local-edge");
1454
3105
  ensureDirectory(dir);
1455
- return join5(dir, "Caddyfile");
3106
+ return join6(dir, "Caddyfile");
1456
3107
  }
1457
3108
  function resolveCaddyExecutable() {
1458
3109
  return process.env.OPENSCOUT_CADDY_BIN?.trim() || "caddy";
1459
3110
  }
1460
3111
  function spawnMdnsProxy(input) {
1461
- return spawn("/usr/bin/dns-sd", [
3112
+ return spawn3("/usr/bin/dns-sd", [
1462
3113
  "-P",
1463
3114
  input.name,
1464
3115
  input.scheme === "https" ? "_https._tcp" : "_http._tcp",
@@ -1494,7 +3145,7 @@ function startLocalEdge() {
1494
3145
  const edgeConfig = resolveEdgeConfig();
1495
3146
  const schemes = edgeConfig.scheme === "both" ? ["http", "https"] : [edgeConfig.scheme];
1496
3147
  const caddyfilePath = resolveLocalEdgeCaddyfilePath();
1497
- writeFileSync4(caddyfilePath, renderOpenScoutCaddyfile(edgeConfig), "utf8");
3148
+ writeFileSync2(caddyfilePath, renderOpenScoutCaddyfile(edgeConfig), "utf8");
1498
3149
  mdnsProcesses = schemes.flatMap((scheme) => {
1499
3150
  const edgePort = scheme === "https" ? 443 : 80;
1500
3151
  const suffix = scheme.toUpperCase();
@@ -1513,7 +3164,7 @@ function startLocalEdge() {
1513
3164
  })
1514
3165
  ];
1515
3166
  });
1516
- caddyProcess = spawn(resolveCaddyExecutable(), [
3167
+ caddyProcess = spawn3(resolveCaddyExecutable(), [
1517
3168
  "run",
1518
3169
  "--config",
1519
3170
  caddyfilePath,
@@ -1563,7 +3214,7 @@ async function startWebWhenBrokerIsReady() {
1563
3214
  try {
1564
3215
  const edgeConfig = resolveEdgeConfig();
1565
3216
  const scheme = forwardedProtoForEdgeScheme(edgeConfig.scheme);
1566
- const response = await fetch(new URL("/v1/web/start", config.brokerUrl), {
3217
+ const response = await fetch(new URL("/v1/web/start", brokerControlUrl), {
1567
3218
  method: "POST",
1568
3219
  headers: {
1569
3220
  accept: "application/json",
@@ -1596,8 +3247,8 @@ function resolvePortListenerPid(port) {
1596
3247
  function findRepoMenuBundle() {
1597
3248
  let current = dirname4(fileURLToPath2(import.meta.url));
1598
3249
  while (true) {
1599
- const candidate = resolve3(current, "apps", "macos", "dist", "OpenScoutMenu.app");
1600
- if (existsSync5(candidate)) {
3250
+ const candidate = resolve3(current, "apps", "macos", "dist", "ScoutMenu.app");
3251
+ if (existsSync6(candidate)) {
1601
3252
  return candidate;
1602
3253
  }
1603
3254
  const parent = dirname4(current);
@@ -1612,9 +3263,9 @@ function startMenuBarApp() {
1612
3263
  return;
1613
3264
  }
1614
3265
  const explicitBundle = process.env.OPENSCOUT_MENU_BUNDLE_PATH?.trim();
1615
- const repoBundle = explicitBundle && existsSync5(explicitBundle) ? explicitBundle : findRepoMenuBundle();
3266
+ const repoBundle = explicitBundle && existsSync6(explicitBundle) ? explicitBundle : findRepoMenuBundle();
1616
3267
  const args = repoBundle ? [repoBundle] : ["-b", MENU_BUNDLE_ID];
1617
- const child = spawn("open", args, {
3268
+ const child = spawn3("open", args, {
1618
3269
  stdio: ["ignore", logFile("menu.stdout.log"), logFile("menu.stderr.log")]
1619
3270
  });
1620
3271
  child.once("exit", (code) => {
@@ -1629,7 +3280,7 @@ function stopMenuBarApp() {
1629
3280
  if (process.platform !== "darwin" || process.env.OPENSCOUT_BASE_MENU_ENABLED === "0") {
1630
3281
  return;
1631
3282
  }
1632
- spawn("pkill", ["-x", MENU_PROCESS_NAME], { stdio: "ignore" }).unref();
3283
+ spawn3("pkill", ["-x", MENU_PROCESS_NAME], { stdio: "ignore" }).unref();
1633
3284
  }
1634
3285
  function isChildExited(child) {
1635
3286
  return child.exitCode !== null || child.signalCode !== null;
@@ -1682,9 +3333,9 @@ async function shutdown(exitCode = 0) {
1682
3333
  return;
1683
3334
  }
1684
3335
  shuttingDown = true;
1685
- if (supervisorKeepAlive) {
1686
- clearInterval(supervisorKeepAlive);
1687
- supervisorKeepAlive = null;
3336
+ if (baseKeepAlive) {
3337
+ clearInterval(baseKeepAlive);
3338
+ baseKeepAlive = null;
1688
3339
  }
1689
3340
  stopSupervisedWeb();
1690
3341
  stopMenuBarApp();
@@ -1694,14 +3345,14 @@ async function shutdown(exitCode = 0) {
1694
3345
  await terminateChildProcess(activeCaddyProcess, "local edge", 2000);
1695
3346
  process.exit(exitCode);
1696
3347
  }
1697
- function sleep2(ms) {
3348
+ function sleep(ms) {
1698
3349
  return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
1699
3350
  }
1700
- function keepSupervisorAlive() {
1701
- if (supervisorKeepAlive) {
3351
+ function anchorBaseDaemonLifetime() {
3352
+ if (baseKeepAlive) {
1702
3353
  return;
1703
3354
  }
1704
- supervisorKeepAlive = setInterval(() => {
3355
+ baseKeepAlive = setInterval(() => {
1705
3356
  return;
1706
3357
  }, 60000);
1707
3358
  }
@@ -1718,7 +3369,7 @@ log("starting Scout base service", {
1718
3369
  brokerUrl: config.brokerUrl,
1719
3370
  bootout: `launchctl bootout ${config.serviceTarget}`
1720
3371
  });
1721
- keepSupervisorAlive();
3372
+ anchorBaseDaemonLifetime();
1722
3373
  spawnBroker();
1723
3374
  startLocalEdge();
1724
3375
  startMenuBarApp();