@openscout/scout 0.2.63 → 0.2.65

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.
@@ -0,0 +1,906 @@
1
+ // @bun
2
+ // packages/runtime/src/broker-process-manager.ts
3
+ import { spawnSync } from "child_process";
4
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5
+ import { homedir as homedir3 } from "os";
6
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ // packages/runtime/src/broker-api.ts
10
+ import { request as httpRequest } from "http";
11
+ function normalizeBaseUrl(baseUrl) {
12
+ const trimmed = baseUrl.trim();
13
+ if (!trimmed) {
14
+ return trimmed;
15
+ }
16
+ try {
17
+ return new URL(trimmed).toString();
18
+ } catch {
19
+ return trimmed;
20
+ }
21
+ }
22
+ function requestHeaders(options) {
23
+ const headers = {
24
+ accept: "application/json",
25
+ ...options.headers ?? {}
26
+ };
27
+ if (options.body !== undefined && !("content-type" in lowercaseKeys(headers))) {
28
+ headers["content-type"] = "application/json";
29
+ }
30
+ return headers;
31
+ }
32
+ function lowercaseKeys(value) {
33
+ const result = {};
34
+ for (const key of Object.keys(value)) {
35
+ result[key.toLowerCase()] = value[key] ?? "";
36
+ }
37
+ return result;
38
+ }
39
+ function requestBody(options) {
40
+ return options.body === undefined ? undefined : JSON.stringify(options.body);
41
+ }
42
+ async function requestBrokerOverHttp(baseUrl, path, options) {
43
+ const response = await fetch(new URL(path, normalizeBaseUrl(baseUrl)), {
44
+ method: options.method ?? (options.body === undefined ? "GET" : "POST"),
45
+ headers: requestHeaders(options),
46
+ body: requestBody(options),
47
+ signal: options.signal
48
+ });
49
+ return {
50
+ ok: response.ok,
51
+ status: response.status,
52
+ text: await response.text()
53
+ };
54
+ }
55
+ function shouldFallbackFromUnixSocket(error) {
56
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
57
+ return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "FailedToOpenSocket";
58
+ }
59
+ function requestBrokerOverUnixSocket(socketPath, baseUrl, path, options) {
60
+ return new Promise((resolve, reject) => {
61
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
62
+ const url = new URL(path, normalizedBaseUrl);
63
+ const body = requestBody(options);
64
+ const headers = requestHeaders(options);
65
+ if (body !== undefined) {
66
+ headers["content-length"] = Buffer.byteLength(body).toString();
67
+ }
68
+ const request = httpRequest({
69
+ socketPath,
70
+ path: `${url.pathname}${url.search}`,
71
+ method: options.method ?? (body === undefined ? "GET" : "POST"),
72
+ headers
73
+ }, (response) => {
74
+ let text = "";
75
+ response.setEncoding("utf8");
76
+ response.on("data", (chunk) => {
77
+ text += String(chunk);
78
+ });
79
+ response.on("end", () => {
80
+ options.signal?.removeEventListener("abort", handleAbort);
81
+ resolve({
82
+ ok: Boolean(response.statusCode && response.statusCode >= 200 && response.statusCode < 300),
83
+ status: response.statusCode ?? 0,
84
+ text
85
+ });
86
+ });
87
+ });
88
+ const handleAbort = () => {
89
+ request.destroy(options.signal?.reason instanceof Error ? options.signal.reason : new Error("Scout broker request aborted"));
90
+ };
91
+ request.on("error", (error) => {
92
+ options.signal?.removeEventListener("abort", handleAbort);
93
+ reject(error);
94
+ });
95
+ if (options.signal) {
96
+ if (options.signal.aborted) {
97
+ handleAbort();
98
+ return;
99
+ }
100
+ options.signal.addEventListener("abort", handleAbort, { once: true });
101
+ }
102
+ if (body !== undefined) {
103
+ request.write(body);
104
+ }
105
+ request.end();
106
+ });
107
+ }
108
+ async function requestBrokerWire(baseUrl, path, options) {
109
+ const socketPath = options.socketPath?.trim();
110
+ if (socketPath) {
111
+ try {
112
+ return await requestBrokerOverUnixSocket(socketPath, baseUrl, path, options);
113
+ } catch (error) {
114
+ if (!shouldFallbackFromUnixSocket(error)) {
115
+ throw error;
116
+ }
117
+ }
118
+ }
119
+ return requestBrokerOverHttp(baseUrl, path, options);
120
+ }
121
+ async function requestScoutBrokerJson(baseUrl, path, options = {}) {
122
+ const response = await requestBrokerWire(baseUrl, path, options);
123
+ let parsed;
124
+ let parsedJson = false;
125
+ if (response.text.length > 0) {
126
+ try {
127
+ parsed = JSON.parse(response.text);
128
+ parsedJson = true;
129
+ } catch {
130
+ parsedJson = false;
131
+ }
132
+ }
133
+ if (!response.ok) {
134
+ if (parsedJson && options.acceptErrorJson?.(parsed)) {
135
+ return parsed;
136
+ }
137
+ throw new Error(`${path} returned ${response.status}: ${response.text}`);
138
+ }
139
+ if (parsedJson) {
140
+ return parsed;
141
+ }
142
+ return;
143
+ }
144
+
145
+ // packages/runtime/src/support-paths.ts
146
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
147
+ import { homedir } from "os";
148
+ import { join } from "path";
149
+ var OPENSCOUT_RPC_CUTOVER_MARKER = "rpc-runtime-cutover-v1";
150
+ function resolveOpenScoutSupportPaths() {
151
+ const home = homedir();
152
+ const supportDirectory = process.env.OPENSCOUT_SUPPORT_DIRECTORY ?? join(home, "Library", "Application Support", "OpenScout");
153
+ const logsDirectory = join(supportDirectory, "logs");
154
+ const runtimeDirectory = join(supportDirectory, "runtime");
155
+ const catalogDirectory = join(supportDirectory, "catalog");
156
+ return {
157
+ supportDirectory,
158
+ logsDirectory,
159
+ appLogsDirectory: join(logsDirectory, "app"),
160
+ brokerLogsDirectory: join(logsDirectory, "broker"),
161
+ runtimeDirectory,
162
+ catalogDirectory,
163
+ relayAgentsDirectory: join(runtimeDirectory, "agents"),
164
+ settingsPath: join(supportDirectory, "settings.json"),
165
+ harnessCatalogPath: join(catalogDirectory, "harness-catalog.json"),
166
+ relayAgentsRegistryPath: join(supportDirectory, "relay-agents.json"),
167
+ relayHubDirectory: process.env.OPENSCOUT_RELAY_HUB ?? join(home, ".openscout", "relay"),
168
+ controlHome: process.env.OPENSCOUT_CONTROL_HOME ?? join(home, ".openscout", "control-plane"),
169
+ desktopStatusPath: join(supportDirectory, "agent-status.json"),
170
+ workspaceStatePath: join(supportDirectory, "workspace-state.json"),
171
+ cutoverMarkerPath: join(supportDirectory, OPENSCOUT_RPC_CUTOVER_MARKER)
172
+ };
173
+ }
174
+ function ensureOpenScoutCleanSlateSync() {
175
+ const paths = resolveOpenScoutSupportPaths();
176
+ if (existsSync(paths.cutoverMarkerPath)) {
177
+ return;
178
+ }
179
+ rmSync(paths.supportDirectory, { recursive: true, force: true });
180
+ rmSync(paths.controlHome, { recursive: true, force: true });
181
+ rmSync(paths.relayHubDirectory, { recursive: true, force: true });
182
+ mkdirSync(paths.supportDirectory, { recursive: true });
183
+ writeFileSync(paths.cutoverMarkerPath, `${Date.now()}
184
+ `, "utf8");
185
+ }
186
+
187
+ // packages/runtime/src/tool-resolution.ts
188
+ import { accessSync, constants, existsSync as existsSync2 } from "fs";
189
+ import { homedir as homedir2 } from "os";
190
+ import { basename, dirname, join as join2, resolve } from "path";
191
+ var DEFAULT_COMMON_EXECUTABLE_DIRECTORIES = [
192
+ join2(homedir2(), ".bun", "bin"),
193
+ "/opt/homebrew/bin",
194
+ "/usr/local/bin"
195
+ ];
196
+ function expandHomePath(value, env = process.env) {
197
+ const home = env.HOME ?? homedir2();
198
+ if (value === "~") {
199
+ return home;
200
+ }
201
+ if (value.startsWith("~/")) {
202
+ return join2(home, value.slice(2));
203
+ }
204
+ return value;
205
+ }
206
+ function isExecutablePath(candidate) {
207
+ if (!candidate) {
208
+ return false;
209
+ }
210
+ try {
211
+ accessSync(candidate, constants.X_OK);
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+ function splitPathEntries(env = process.env) {
218
+ const separator = process.platform === "win32" ? ";" : ":";
219
+ return (env.PATH ?? "").split(separator).filter(Boolean);
220
+ }
221
+ function dedupeDirectories(values, env) {
222
+ const seen = new Set;
223
+ const resolvedEntries = [];
224
+ for (const value of values) {
225
+ const normalized = resolve(expandHomePath(value, env));
226
+ if (seen.has(normalized)) {
227
+ continue;
228
+ }
229
+ seen.add(normalized);
230
+ resolvedEntries.push(normalized);
231
+ }
232
+ return resolvedEntries;
233
+ }
234
+ function resolveExecutableFromSearch(options) {
235
+ const env = options.env ?? process.env;
236
+ const envKeys = options.envKeys ?? [];
237
+ for (const envKey of envKeys) {
238
+ const explicit = env[envKey]?.trim();
239
+ if (!explicit) {
240
+ continue;
241
+ }
242
+ const expanded = expandHomePath(explicit, env);
243
+ if (isExecutablePath(expanded)) {
244
+ return { path: resolve(expanded), source: "env" };
245
+ }
246
+ const foundOnPath = findExecutableOnSearchPath(explicit, env);
247
+ if (foundOnPath) {
248
+ return { path: foundOnPath.path, source: "env" };
249
+ }
250
+ }
251
+ const commonDirectories = dedupeDirectories([...options.commonDirectories ?? DEFAULT_COMMON_EXECUTABLE_DIRECTORIES], env);
252
+ const searchDirectories = dedupeDirectories([
253
+ ...splitPathEntries(env),
254
+ ...options.extraDirectories ?? [],
255
+ ...commonDirectories
256
+ ], env);
257
+ for (const directory of searchDirectories) {
258
+ for (const name of options.names) {
259
+ const candidate = join2(directory, name);
260
+ if (isExecutablePath(candidate)) {
261
+ return {
262
+ path: candidate,
263
+ source: commonDirectories.includes(directory) ? "common-path" : "path"
264
+ };
265
+ }
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+ function resolveBunExecutable(env = process.env) {
271
+ return resolveExecutableFromSearch({
272
+ env,
273
+ envKeys: ["OPENSCOUT_BUN_BIN", "SCOUT_BUN_BIN", "BUN_BIN"],
274
+ names: ["bun"]
275
+ });
276
+ }
277
+ function findExecutableOnSearchPath(name, env) {
278
+ const searchDirectories = dedupeDirectories([
279
+ ...splitPathEntries(env),
280
+ ...DEFAULT_COMMON_EXECUTABLE_DIRECTORIES
281
+ ], env);
282
+ for (const directory of searchDirectories) {
283
+ const candidate = join2(directory, name);
284
+ if (isExecutablePath(candidate)) {
285
+ return { path: candidate, source: "path" };
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+
291
+ // packages/runtime/src/broker-process-manager.ts
292
+ function isTmpPath(p) {
293
+ return /^\/(?:private\/)?tmp\//.test(p);
294
+ }
295
+ var DEFAULT_BROKER_HOST = "127.0.0.1";
296
+ var DEFAULT_BROKER_HOST_MESH = "0.0.0.0";
297
+ var DEFAULT_BROKER_PORT = 65535;
298
+ var DEFAULT_ADVERTISE_SCOPE = "local";
299
+ function resolveAdvertiseScope() {
300
+ const raw = (process.env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
301
+ if (raw === "mesh")
302
+ return "mesh";
303
+ if (raw === "local")
304
+ return "local";
305
+ return DEFAULT_ADVERTISE_SCOPE;
306
+ }
307
+ function resolveBrokerHost(scope = resolveAdvertiseScope()) {
308
+ const explicit = process.env.OPENSCOUT_BROKER_HOST;
309
+ if (typeof explicit === "string" && explicit.trim().length > 0) {
310
+ return explicit;
311
+ }
312
+ return scope === "mesh" ? DEFAULT_BROKER_HOST_MESH : DEFAULT_BROKER_HOST;
313
+ }
314
+ var BROKER_SERVICE_POLL_INTERVAL_MS = 100;
315
+ var DEFAULT_BROKER_START_TIMEOUT_MS = 15000;
316
+ function buildDefaultBrokerUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
317
+ return `http://${host}:${port}`;
318
+ }
319
+ function buildDefaultBrokerSocketPath(runtimeDirectory) {
320
+ return join3(runtimeDirectory, "broker.sock");
321
+ }
322
+ var DEFAULT_BROKER_URL = buildDefaultBrokerUrl();
323
+ function runtimePackageDir() {
324
+ const explicit = process.env.OPENSCOUT_RUNTIME_PACKAGE_DIR?.trim();
325
+ if (explicit)
326
+ return explicit;
327
+ const fromBundledPackage = findBundledRuntimeDir();
328
+ if (fromBundledPackage)
329
+ return fromBundledPackage;
330
+ const fromCwd = findWorkspaceRuntimeDir(process.cwd());
331
+ if (fromCwd)
332
+ return fromCwd;
333
+ const fromGlobal = findGlobalRuntimeDir();
334
+ if (fromGlobal)
335
+ return fromGlobal;
336
+ const moduleDir = dirname2(fileURLToPath(import.meta.url));
337
+ return resolve2(moduleDir, "..");
338
+ }
339
+ function isInstalledRuntimePackageDir(candidate) {
340
+ return existsSync3(join3(candidate, "package.json")) && existsSync3(join3(candidate, "bin", "openscout-runtime.mjs"));
341
+ }
342
+ function findGlobalRuntimeDir() {
343
+ const candidates = [
344
+ join3(homedir3(), ".bun", "node_modules", "@openscout", "runtime"),
345
+ join3(homedir3(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
346
+ join3(homedir3(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
347
+ ];
348
+ for (const c of candidates) {
349
+ if (isInstalledRuntimePackageDir(c))
350
+ return c;
351
+ }
352
+ try {
353
+ const result = spawnSync("which", ["scout"], { encoding: "utf8", timeout: 3000 });
354
+ const scoutBin = result.stdout?.trim();
355
+ if (scoutBin) {
356
+ const scoutPkg = resolve2(scoutBin, "..", "..");
357
+ if (isInstalledRuntimePackageDir(scoutPkg))
358
+ return scoutPkg;
359
+ const nested = join3(scoutPkg, "node_modules", "@openscout", "runtime");
360
+ if (isInstalledRuntimePackageDir(nested))
361
+ return nested;
362
+ const sibling = resolve2(scoutPkg, "..", "runtime");
363
+ if (isInstalledRuntimePackageDir(sibling))
364
+ return sibling;
365
+ }
366
+ } catch {}
367
+ return null;
368
+ }
369
+ function findBundledRuntimeDir() {
370
+ const moduleDir = dirname2(fileURLToPath(import.meta.url));
371
+ const candidate = resolve2(moduleDir, "..");
372
+ return isInstalledRuntimePackageDir(candidate) ? candidate : null;
373
+ }
374
+ function findWorkspaceRuntimeDir(startDir) {
375
+ let current = resolve2(startDir);
376
+ while (true) {
377
+ const candidate = join3(current, "packages", "runtime");
378
+ if (existsSync3(join3(candidate, "package.json")) && existsSync3(join3(candidate, "src"))) {
379
+ return candidate;
380
+ }
381
+ const parent = dirname2(current);
382
+ if (parent === current)
383
+ return null;
384
+ current = parent;
385
+ }
386
+ }
387
+ function resolveBunExecutable2() {
388
+ const bun = resolveBunExecutable(process.env);
389
+ if (bun) {
390
+ return bun.path;
391
+ }
392
+ throw new Error("Unable to locate Bun for broker service management. Install Bun or set OPENSCOUT_BUN_BIN.");
393
+ }
394
+ function resolveBrokerServiceMode() {
395
+ const explicit = (process.env.OPENSCOUT_BROKER_SERVICE_MODE ?? "").trim().toLowerCase();
396
+ if (explicit === "prod" || explicit === "production") {
397
+ return "prod";
398
+ }
399
+ if (explicit === "custom") {
400
+ return "custom";
401
+ }
402
+ return "dev";
403
+ }
404
+ function resolveBrokerStartTimeoutMs() {
405
+ const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_START_TIMEOUT_MS ?? "", 10);
406
+ if (Number.isFinite(explicit) && explicit > 0) {
407
+ return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
408
+ }
409
+ return DEFAULT_BROKER_START_TIMEOUT_MS;
410
+ }
411
+ function resolveBrokerServiceLabel(mode) {
412
+ const explicit = process.env.OPENSCOUT_SERVICE_LABEL?.trim() || process.env.OPENSCOUT_BROKER_SERVICE_LABEL?.trim();
413
+ if (explicit) {
414
+ return explicit;
415
+ }
416
+ switch (mode) {
417
+ case "prod":
418
+ return "com.openscout";
419
+ case "custom":
420
+ return "com.openscout.custom";
421
+ case "dev":
422
+ default:
423
+ return "dev.openscout";
424
+ }
425
+ }
426
+ function legacyBrokerServiceLabel(mode) {
427
+ switch (mode) {
428
+ case "prod":
429
+ return "com.openscout.broker";
430
+ case "custom":
431
+ return "com.openscout.broker.custom";
432
+ case "dev":
433
+ default:
434
+ return "dev.openscout.broker";
435
+ }
436
+ }
437
+ function resolveBrokerServiceConfig() {
438
+ const mode = resolveBrokerServiceMode();
439
+ const label = resolveBrokerServiceLabel(mode);
440
+ const uid = typeof process.getuid === "function" ? process.getuid() : Number.parseInt(process.env.UID ?? "0", 10);
441
+ const supportPaths = resolveOpenScoutSupportPaths();
442
+ const defaultSupportDir = join3(homedir3(), "Library", "Application Support", "OpenScout");
443
+ const supportDirectory = isTmpPath(supportPaths.supportDirectory) ? defaultSupportDir : supportPaths.supportDirectory;
444
+ const runtimeDirectory = join3(supportDirectory, "runtime");
445
+ const logsDirectory = join3(supportDirectory, "logs", "broker");
446
+ const controlHome = isTmpPath(supportPaths.controlHome) ? join3(homedir3(), ".openscout", "control-plane") : supportPaths.controlHome;
447
+ const advertiseScope = resolveAdvertiseScope();
448
+ const brokerHost = resolveBrokerHost(advertiseScope);
449
+ const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(DEFAULT_BROKER_PORT), 10);
450
+ const brokerUrl = process.env.OPENSCOUT_BROKER_URL ?? buildDefaultBrokerUrl(brokerHost, brokerPort);
451
+ const brokerSocketPath = process.env.OPENSCOUT_BROKER_SOCKET_PATH ?? buildDefaultBrokerSocketPath(runtimeDirectory);
452
+ const launchAgentPath = join3(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
453
+ return {
454
+ label,
455
+ mode,
456
+ uid,
457
+ domainTarget: `gui/${uid}`,
458
+ serviceTarget: `gui/${uid}/${label}`,
459
+ launchAgentPath,
460
+ supportDirectory,
461
+ runtimeDirectory,
462
+ logsDirectory,
463
+ stdoutLogPath: join3(logsDirectory, "stdout.log"),
464
+ stderrLogPath: join3(logsDirectory, "stderr.log"),
465
+ controlHome,
466
+ runtimePackageDir: runtimePackageDir(),
467
+ bunExecutable: resolveBunExecutable2(),
468
+ brokerHost,
469
+ brokerPort,
470
+ brokerUrl,
471
+ brokerSocketPath,
472
+ advertiseScope
473
+ };
474
+ }
475
+ function renderLaunchAgentPlist(config) {
476
+ const launchPath = resolveLaunchAgentPATH();
477
+ const envEntries = {
478
+ OPENSCOUT_BROKER_HOST: config.brokerHost,
479
+ OPENSCOUT_BROKER_PORT: String(config.brokerPort),
480
+ OPENSCOUT_BROKER_URL: config.brokerUrl,
481
+ OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
482
+ OPENSCOUT_CONTROL_HOME: config.controlHome,
483
+ OPENSCOUT_BROKER_SERVICE_MODE: config.mode,
484
+ OPENSCOUT_BROKER_SERVICE_LABEL: config.label,
485
+ OPENSCOUT_SERVICE_LABEL: config.label,
486
+ OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope,
487
+ HOME: homedir3(),
488
+ PATH: launchPath,
489
+ ...collectOptionalEnvVars([
490
+ "OPENSCOUT_MESH_ID",
491
+ "OPENSCOUT_MESH_SEEDS",
492
+ "OPENSCOUT_MESH_DISCOVERY_INTERVAL_MS",
493
+ "OPENSCOUT_NODE_NAME",
494
+ "OPENSCOUT_NODE_ID",
495
+ "OPENSCOUT_NODE_QUALIFIER",
496
+ "OPENSCOUT_TAILSCALE_BIN",
497
+ "OPENSCOUT_TAILSCALE_STATUS_JSON",
498
+ "OPENSCOUT_SSE_KEEPALIVE_MS"
499
+ ])
500
+ };
501
+ const envBlock = Object.entries(envEntries).map(([key, value]) => `
502
+ <key>${xmlEscape(key)}</key>
503
+ <string>${xmlEscape(value)}</string>`).join("");
504
+ return `<?xml version="1.0" encoding="UTF-8"?>
505
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
506
+ <plist version="1.0">
507
+ <dict>
508
+ <key>Label</key>
509
+ <string>${xmlEscape(config.label)}</string>
510
+ <key>ProgramArguments</key>
511
+ <array>
512
+ <string>${xmlEscape(config.bunExecutable)}</string>
513
+ <string>${xmlEscape(join3(config.runtimePackageDir, "bin", "openscout-runtime.mjs"))}</string>
514
+ <string>base</string>
515
+ </array>
516
+ <key>WorkingDirectory</key>
517
+ <string>${xmlEscape(config.runtimePackageDir)}</string>
518
+ <key>RunAtLoad</key>
519
+ <true/>
520
+ <key>KeepAlive</key>
521
+ <dict>
522
+ <key>SuccessfulExit</key>
523
+ <false/>
524
+ </dict>
525
+ <key>StandardOutPath</key>
526
+ <string>${xmlEscape(config.stdoutLogPath)}</string>
527
+ <key>StandardErrorPath</key>
528
+ <string>${xmlEscape(config.stderrLogPath)}</string>
529
+ <key>EnvironmentVariables</key>
530
+ <dict>${envBlock}
531
+ </dict>
532
+ </dict>
533
+ </plist>
534
+ `;
535
+ }
536
+ function collectOptionalEnvVars(keys) {
537
+ const entries = {};
538
+ for (const key of keys) {
539
+ const value = process.env[key];
540
+ if (typeof value === "string" && value.trim().length > 0) {
541
+ entries[key] = value;
542
+ }
543
+ }
544
+ return entries;
545
+ }
546
+ function resolveLaunchAgentPATH() {
547
+ const entries = [
548
+ join3(homedir3(), ".bun", "bin"),
549
+ ...(process.env.PATH ?? "").split(":").filter(Boolean),
550
+ "/opt/homebrew/bin",
551
+ "/usr/local/bin",
552
+ "/usr/bin",
553
+ "/bin",
554
+ "/usr/sbin",
555
+ "/sbin"
556
+ ];
557
+ return Array.from(new Set(entries)).filter((e) => !isTmpPath(e)).join(":");
558
+ }
559
+ function xmlEscape(value) {
560
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
561
+ }
562
+ function ensureParentDirectory(filePath) {
563
+ mkdirSync2(dirname2(filePath), { recursive: true });
564
+ }
565
+ function ensureServiceDirectories(config) {
566
+ ensureOpenScoutCleanSlateSync();
567
+ mkdirSync2(config.supportDirectory, { recursive: true });
568
+ mkdirSync2(config.runtimeDirectory, { recursive: true, mode: 448 });
569
+ chmodSync(config.runtimeDirectory, 448);
570
+ mkdirSync2(config.logsDirectory, { recursive: true });
571
+ mkdirSync2(config.controlHome, { recursive: true });
572
+ ensureParentDirectory(config.launchAgentPath);
573
+ }
574
+ function writeLaunchAgentPlist(config) {
575
+ ensureServiceDirectories(config);
576
+ writeFileSync2(config.launchAgentPath, renderLaunchAgentPlist(config), "utf8");
577
+ }
578
+ function bootoutCommand(config) {
579
+ return `${launchctlPath()} bootout ${config.serviceTarget}`;
580
+ }
581
+ function legacyLaunchAgentPath(config) {
582
+ return join3(homedir3(), "Library", "LaunchAgents", `${legacyBrokerServiceLabel(config.mode)}.plist`);
583
+ }
584
+ function legacyServiceTarget(config) {
585
+ return `${config.domainTarget}/${legacyBrokerServiceLabel(config.mode)}`;
586
+ }
587
+ function bootoutLegacyBrokerService(config) {
588
+ const legacyTarget = legacyServiceTarget(config);
589
+ if (legacyTarget === config.serviceTarget) {
590
+ return;
591
+ }
592
+ runCommand(launchctlPath(), ["bootout", legacyTarget], { allowFailure: true });
593
+ }
594
+ function runCommand(command, args, options) {
595
+ const result = spawnSync(command, args, {
596
+ env: {
597
+ ...process.env,
598
+ ...options?.env ?? {}
599
+ },
600
+ encoding: "utf8"
601
+ });
602
+ const stdout = (result.stdout ?? "").trim();
603
+ const stderr = (result.stderr ?? "").trim();
604
+ const exitCode = result.status ?? 1;
605
+ if (exitCode !== 0 && !options?.allowFailure) {
606
+ throw new Error(stderr || stdout || `${command} exited with status ${exitCode}`);
607
+ }
608
+ return { exitCode, stdout, stderr };
609
+ }
610
+ function launchctlPath() {
611
+ return "/bin/launchctl";
612
+ }
613
+ function readLogLines(path) {
614
+ if (!existsSync3(path)) {
615
+ return [];
616
+ }
617
+ return readFileSync(path, "utf8").split(`
618
+ `).map((line) => line.trim()).filter(Boolean);
619
+ }
620
+ function isPackageScriptBanner(line) {
621
+ return /^\$\s*(bun run|npm run|pnpm\b|yarn\b)/.test(line);
622
+ }
623
+ function selectLastRelevantLogLine(lines) {
624
+ for (let index = lines.length - 1;index >= 0; index -= 1) {
625
+ const line = lines[index];
626
+ if (!isPackageScriptBanner(line)) {
627
+ return line;
628
+ }
629
+ }
630
+ return lines.at(-1) ?? null;
631
+ }
632
+ function readLastLogLine(paths) {
633
+ let fallback = null;
634
+ for (const path of paths) {
635
+ const lines = readLogLines(path);
636
+ if (lines.length === 0) {
637
+ continue;
638
+ }
639
+ const relevantLine = selectLastRelevantLogLine(lines);
640
+ if (!relevantLine) {
641
+ continue;
642
+ }
643
+ if (!isPackageScriptBanner(relevantLine)) {
644
+ return relevantLine;
645
+ }
646
+ fallback ??= relevantLine;
647
+ }
648
+ return fallback;
649
+ }
650
+ function parseLaunchctlPrint(output) {
651
+ const pidMatch = output.match(/\bpid = (\d+)/);
652
+ const stateMatch = output.match(/\bstate = ([^\n]+)/);
653
+ const lastExitMatch = output.match(/\blast exit code = (-?\d+)/i) || output.match(/\blast exit status = (-?\d+)/i);
654
+ return {
655
+ pid: pidMatch ? Number.parseInt(pidMatch[1] ?? "0", 10) : null,
656
+ launchdState: stateMatch?.[1]?.trim() ?? null,
657
+ lastExitStatus: lastExitMatch ? Number.parseInt(lastExitMatch[1] ?? "0", 10) : null
658
+ };
659
+ }
660
+ function inspectLaunchctl(config) {
661
+ const printResult = runCommand(launchctlPath(), ["print", config.serviceTarget], { allowFailure: true });
662
+ if (printResult.exitCode !== 0) {
663
+ return {
664
+ loaded: false,
665
+ pid: null,
666
+ launchdState: null,
667
+ lastExitStatus: null,
668
+ raw: printResult.stderr || printResult.stdout
669
+ };
670
+ }
671
+ return {
672
+ loaded: true,
673
+ raw: printResult.stdout,
674
+ ...parseLaunchctlPrint(printResult.stdout)
675
+ };
676
+ }
677
+ async function fetchHealthSnapshot(config) {
678
+ const controller = new AbortController;
679
+ const timeout = setTimeout(() => controller.abort(), 1000);
680
+ try {
681
+ const payload = await requestScoutBrokerJson(config.brokerUrl, "/health", {
682
+ signal: controller.signal,
683
+ socketPath: config.brokerSocketPath
684
+ });
685
+ return {
686
+ reachable: true,
687
+ ok: Boolean(payload.ok),
688
+ nodeId: payload.nodeId,
689
+ meshId: payload.meshId,
690
+ counts: payload.counts
691
+ };
692
+ } catch (error) {
693
+ return {
694
+ reachable: false,
695
+ ok: false,
696
+ error: error instanceof Error ? error.message : String(error)
697
+ };
698
+ } finally {
699
+ clearTimeout(timeout);
700
+ }
701
+ }
702
+ async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
703
+ ensureServiceDirectories(config);
704
+ const launchctl = inspectLaunchctl(config);
705
+ const health = await fetchHealthSnapshot(config);
706
+ const installed = existsSync3(config.launchAgentPath);
707
+ const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
708
+ return {
709
+ label: config.label,
710
+ mode: config.mode,
711
+ launchAgentPath: config.launchAgentPath,
712
+ bootoutCommand: bootoutCommand(config),
713
+ brokerUrl: config.brokerUrl,
714
+ brokerSocketPath: config.brokerSocketPath,
715
+ supportDirectory: config.supportDirectory,
716
+ runtimeDirectory: config.runtimeDirectory,
717
+ controlHome: config.controlHome,
718
+ stdoutLogPath: config.stdoutLogPath,
719
+ stderrLogPath: config.stderrLogPath,
720
+ installed,
721
+ loaded: launchctl.loaded,
722
+ pid: launchctl.pid,
723
+ launchdState: launchctl.launchdState,
724
+ lastExitStatus: launchctl.lastExitStatus,
725
+ usesLaunchAgent: installed || launchctl.loaded,
726
+ reachable: health.reachable,
727
+ health,
728
+ lastLogLine
729
+ };
730
+ }
731
+ async function installBrokerService(config = resolveBrokerServiceConfig()) {
732
+ bootoutLegacyBrokerService(config);
733
+ writeLaunchAgentPlist(config);
734
+ return brokerServiceStatus(config);
735
+ }
736
+ async function startBrokerService(config = resolveBrokerServiceConfig()) {
737
+ bootoutLegacyBrokerService(config);
738
+ writeLaunchAgentPlist(config);
739
+ runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
740
+ runCommand(launchctlPath(), ["bootstrap", config.domainTarget, config.launchAgentPath], { allowFailure: true });
741
+ runCommand(launchctlPath(), ["kickstart", "-k", config.serviceTarget], { allowFailure: true });
742
+ const attempts = Math.ceil(resolveBrokerStartTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
743
+ for (let attempt = 0;attempt < attempts; attempt += 1) {
744
+ const status2 = await brokerServiceStatus(config);
745
+ if (status2.health.reachable) {
746
+ return status2;
747
+ }
748
+ await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
749
+ }
750
+ const status = await brokerServiceStatus(config);
751
+ throw new Error(status.lastLogLine ?? status.health.error ?? "Broker service did not become healthy.");
752
+ }
753
+ function sleep(ms) {
754
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
755
+ }
756
+ async function stopBrokerService(config = resolveBrokerServiceConfig()) {
757
+ runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
758
+ for (let attempt = 0;attempt < 30; attempt += 1) {
759
+ const status = await brokerServiceStatus(config);
760
+ if (!status.health.reachable) {
761
+ return status;
762
+ }
763
+ await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
764
+ }
765
+ return brokerServiceStatus(config);
766
+ }
767
+ async function restartBrokerService(config = resolveBrokerServiceConfig()) {
768
+ await stopBrokerService(config);
769
+ return startBrokerService(config);
770
+ }
771
+ async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
772
+ await stopBrokerService(config);
773
+ if (existsSync3(config.launchAgentPath)) {
774
+ rmSync2(config.launchAgentPath, { force: true });
775
+ }
776
+ const legacyPath = legacyLaunchAgentPath(config);
777
+ bootoutLegacyBrokerService(config);
778
+ if (existsSync3(legacyPath)) {
779
+ rmSync2(legacyPath, { force: true });
780
+ }
781
+ return brokerServiceStatus(config);
782
+ }
783
+ async function main() {
784
+ const command = process.argv[2] ?? "status";
785
+ const json = process.argv.includes("--json");
786
+ const config = resolveBrokerServiceConfig();
787
+ let status;
788
+ switch (command) {
789
+ case "install":
790
+ status = await installBrokerService(config);
791
+ break;
792
+ case "start":
793
+ status = await startBrokerService(config);
794
+ break;
795
+ case "stop":
796
+ status = await stopBrokerService(config);
797
+ break;
798
+ case "restart":
799
+ status = await restartBrokerService(config);
800
+ break;
801
+ case "uninstall":
802
+ status = await uninstallBrokerService(config);
803
+ break;
804
+ case "status":
805
+ status = await brokerServiceStatus(config);
806
+ break;
807
+ default:
808
+ console.error(`Unknown broker service command: ${command}`);
809
+ process.exit(1);
810
+ }
811
+ if (json) {
812
+ console.log(JSON.stringify(status, null, 2));
813
+ return;
814
+ }
815
+ console.log(formatBrokerServiceStatus(status));
816
+ }
817
+ function formatBrokerServiceStatus(status) {
818
+ const lines = [
819
+ `label: ${status.label}`,
820
+ `mode: ${status.mode}`,
821
+ `launch agent: ${status.installed ? status.launchAgentPath : "not installed"}`,
822
+ `bootout: ${status.bootoutCommand}`,
823
+ `loaded: ${status.loaded ? "yes" : "no"}`,
824
+ `pid: ${status.pid ?? "\u2014"}`,
825
+ `launchd state: ${status.launchdState ?? "\u2014"}`,
826
+ `broker url: ${status.brokerUrl}`,
827
+ `broker socket: ${status.brokerSocketPath}`,
828
+ `reachable: ${status.reachable ? "yes" : "no"}`,
829
+ `health: ${status.health.ok ? "ok" : status.health.error ?? "unreachable"}`,
830
+ `logs: ${status.stdoutLogPath}`
831
+ ];
832
+ if (status.lastLogLine) {
833
+ lines.push(`last log: ${status.lastLogLine}`);
834
+ }
835
+ return lines.join(`
836
+ `);
837
+ }
838
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1] && !process.argv[1].endsWith("/main.mjs")) {
839
+ main().catch((error) => {
840
+ console.error(error instanceof Error ? error.message : String(error));
841
+ process.exit(1);
842
+ });
843
+ }
844
+
845
+ // packages/runtime/src/mesh-discover.ts
846
+ function resolveBrokerUrl() {
847
+ return (process.env.OPENSCOUT_BROKER_URL ?? DEFAULT_BROKER_URL).replace(/\/$/, "");
848
+ }
849
+ function collectSeeds(argv) {
850
+ return argv.map((value) => value.trim()).filter(Boolean);
851
+ }
852
+ async function postJson(url, payload) {
853
+ const response = await fetch(url, {
854
+ method: "POST",
855
+ headers: {
856
+ "content-type": "application/json",
857
+ accept: "application/json"
858
+ },
859
+ body: JSON.stringify(payload)
860
+ });
861
+ if (!response.ok) {
862
+ const detail = await response.text();
863
+ throw new Error(`request failed: ${response.status} ${response.statusText}${detail ? `
864
+ ${detail}` : ""}`);
865
+ }
866
+ return await response.json();
867
+ }
868
+ async function getJson(url) {
869
+ const response = await fetch(url, {
870
+ headers: { accept: "application/json" }
871
+ });
872
+ if (!response.ok) {
873
+ const detail = await response.text();
874
+ throw new Error(`request failed: ${response.status} ${response.statusText}${detail ? `
875
+ ${detail}` : ""}`);
876
+ }
877
+ return await response.json();
878
+ }
879
+ async function main2() {
880
+ const brokerUrl = resolveBrokerUrl();
881
+ const seeds = collectSeeds(process.argv.slice(2));
882
+ const health = await getJson(`${brokerUrl}/health`);
883
+ console.log(`Broker: ${brokerUrl}`);
884
+ console.log(`Node: ${health.nodeId}`);
885
+ console.log(`Mesh: ${health.meshId}`);
886
+ const result = await postJson(`${brokerUrl}/v1/mesh/discover`, { seeds });
887
+ console.log("");
888
+ if (result.discovered.length === 0) {
889
+ console.log("No peers discovered.");
890
+ } else {
891
+ console.log("Discovered peers:");
892
+ for (const node of result.discovered) {
893
+ console.log(`- ${node.name} (${node.id}) -> ${node.brokerUrl ?? "no broker url"}`);
894
+ }
895
+ }
896
+ const nodes = await getJson(`${brokerUrl}/v1/mesh/nodes`);
897
+ console.log("");
898
+ console.log("Known mesh nodes:");
899
+ for (const [id, node] of Object.entries(nodes)) {
900
+ console.log(`- ${node.name} (${id}) [${node.advertiseScope}]${node.brokerUrl ? ` -> ${node.brokerUrl}` : ""}`);
901
+ }
902
+ }
903
+ main2().catch((error) => {
904
+ console.error(error instanceof Error ? error.message : String(error));
905
+ process.exit(1);
906
+ });