@openscout/scout 0.2.64 → 0.2.68
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.
- package/README.md +54 -13
- package/bin/openscout-runtime.mjs +42 -0
- package/dist/client/assets/addon-fit_hudsonkit_false-DSvcGNZx.js +1 -0
- package/dist/client/assets/addon-webgl_hudsonkit_false-1rrLEmjL.js +1 -0
- package/dist/client/assets/index-R2y6GOWl.js +184 -0
- package/dist/client/assets/index-bOmc2x1_.css +1 -0
- package/dist/client/assets/xterm_hudsonkit_false-B2TbElXJ.js +1 -0
- package/dist/client/index.html +3 -3
- package/dist/main.mjs +20295 -8189
- package/dist/pair-supervisor.mjs +25432 -21330
- package/dist/runtime/base-daemon.mjs +1626 -0
- package/dist/runtime/broker-daemon.mjs +47985 -0
- package/dist/runtime/broker-process-manager.mjs +938 -0
- package/dist/runtime/mesh-discover.mjs +964 -0
- package/dist/scout-control-plane-web.mjs +19794 -3344
- package/dist/scout-web-server.mjs +19794 -3344
- package/package.json +17 -7
- package/dist/client/assets/addon-fit-DX4qG4td.js +0 -1
- package/dist/client/assets/addon-webgl-DCtw1yLn.js +0 -64
- package/dist/client/assets/arc.es-DglF81f4.js +0 -188
- package/dist/client/assets/index-BRvY3oC-.js +0 -159
- package/dist/client/assets/index-CUNnl56m.css +0 -1
- package/dist/client/assets/index-TVkH_WDG.js +0 -1
- package/dist/client/assets/xterm-B-qIQCd3.js +0 -16
|
@@ -0,0 +1,1626 @@
|
|
|
1
|
+
// @bun
|
|
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";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
|
+
|
|
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";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
// packages/runtime/src/broker-api.ts
|
|
17
|
+
import { request as httpRequest } from "http";
|
|
18
|
+
function normalizeBaseUrl(baseUrl) {
|
|
19
|
+
const trimmed = baseUrl.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return new URL(trimmed).toString();
|
|
25
|
+
} catch {
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function requestHeaders(options) {
|
|
30
|
+
const headers = {
|
|
31
|
+
accept: "application/json",
|
|
32
|
+
...options.headers ?? {}
|
|
33
|
+
};
|
|
34
|
+
if (options.body !== undefined && !("content-type" in lowercaseKeys(headers))) {
|
|
35
|
+
headers["content-type"] = "application/json";
|
|
36
|
+
}
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
function lowercaseKeys(value) {
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const key of Object.keys(value)) {
|
|
42
|
+
result[key.toLowerCase()] = value[key] ?? "";
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function requestBody(options) {
|
|
47
|
+
return options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
48
|
+
}
|
|
49
|
+
async function requestBrokerOverHttp(baseUrl, path, options) {
|
|
50
|
+
const response = await fetch(new URL(path, normalizeBaseUrl(baseUrl)), {
|
|
51
|
+
method: options.method ?? (options.body === undefined ? "GET" : "POST"),
|
|
52
|
+
headers: requestHeaders(options),
|
|
53
|
+
body: requestBody(options),
|
|
54
|
+
signal: options.signal
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
ok: response.ok,
|
|
58
|
+
status: response.status,
|
|
59
|
+
text: await response.text()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function shouldFallbackFromUnixSocket(error) {
|
|
63
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
64
|
+
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "FailedToOpenSocket";
|
|
65
|
+
}
|
|
66
|
+
function errorMessage(error) {
|
|
67
|
+
return error instanceof Error ? error.message : String(error);
|
|
68
|
+
}
|
|
69
|
+
function requestBrokerOverUnixSocket(socketPath, baseUrl, path, options) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
72
|
+
const url = new URL(path, normalizedBaseUrl);
|
|
73
|
+
const body = requestBody(options);
|
|
74
|
+
const headers = requestHeaders(options);
|
|
75
|
+
if (body !== undefined) {
|
|
76
|
+
headers["content-length"] = Buffer.byteLength(body).toString();
|
|
77
|
+
}
|
|
78
|
+
const request = httpRequest({
|
|
79
|
+
socketPath,
|
|
80
|
+
path: `${url.pathname}${url.search}`,
|
|
81
|
+
method: options.method ?? (body === undefined ? "GET" : "POST"),
|
|
82
|
+
headers
|
|
83
|
+
}, (response) => {
|
|
84
|
+
let text = "";
|
|
85
|
+
response.setEncoding("utf8");
|
|
86
|
+
response.on("data", (chunk) => {
|
|
87
|
+
text += String(chunk);
|
|
88
|
+
});
|
|
89
|
+
response.on("end", () => {
|
|
90
|
+
options.signal?.removeEventListener("abort", handleAbort);
|
|
91
|
+
resolve({
|
|
92
|
+
ok: Boolean(response.statusCode && response.statusCode >= 200 && response.statusCode < 300),
|
|
93
|
+
status: response.statusCode ?? 0,
|
|
94
|
+
text
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
const handleAbort = () => {
|
|
99
|
+
request.destroy(options.signal?.reason instanceof Error ? options.signal.reason : new Error("Scout broker request aborted"));
|
|
100
|
+
};
|
|
101
|
+
request.on("error", (error) => {
|
|
102
|
+
options.signal?.removeEventListener("abort", handleAbort);
|
|
103
|
+
reject(error);
|
|
104
|
+
});
|
|
105
|
+
if (options.signal) {
|
|
106
|
+
if (options.signal.aborted) {
|
|
107
|
+
handleAbort();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
options.signal.addEventListener("abort", handleAbort, { once: true });
|
|
111
|
+
}
|
|
112
|
+
if (body !== undefined) {
|
|
113
|
+
request.write(body);
|
|
114
|
+
}
|
|
115
|
+
request.end();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async function requestBrokerWire(baseUrl, path, options) {
|
|
119
|
+
const socketPath = options.socketPath?.trim();
|
|
120
|
+
if (socketPath) {
|
|
121
|
+
try {
|
|
122
|
+
return {
|
|
123
|
+
response: await requestBrokerOverUnixSocket(socketPath, baseUrl, path, options),
|
|
124
|
+
trace: {
|
|
125
|
+
transport: "unix_socket",
|
|
126
|
+
socketPath
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (!shouldFallbackFromUnixSocket(error)) {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
response: await requestBrokerOverHttp(baseUrl, path, options),
|
|
135
|
+
trace: {
|
|
136
|
+
transport: "http",
|
|
137
|
+
socketPath,
|
|
138
|
+
socketFallbackError: `${socketPath}: ${errorMessage(error)}`
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
response: await requestBrokerOverHttp(baseUrl, path, options),
|
|
145
|
+
trace: {
|
|
146
|
+
transport: "http"
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function requestScoutBrokerJsonWithTrace(baseUrl, path, options = {}) {
|
|
151
|
+
const { response, trace } = await requestBrokerWire(baseUrl, path, options);
|
|
152
|
+
let parsed;
|
|
153
|
+
let parsedJson = false;
|
|
154
|
+
if (response.text.length > 0) {
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(response.text);
|
|
157
|
+
parsedJson = true;
|
|
158
|
+
} catch {
|
|
159
|
+
parsedJson = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
if (parsedJson && options.acceptErrorJson?.(parsed)) {
|
|
164
|
+
return { value: parsed, trace };
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`${path} returned ${response.status}: ${response.text}`);
|
|
167
|
+
}
|
|
168
|
+
if (parsedJson) {
|
|
169
|
+
return { value: parsed, trace };
|
|
170
|
+
}
|
|
171
|
+
return { value: undefined, trace };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// packages/runtime/src/support-paths.ts
|
|
175
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
176
|
+
import { homedir } from "os";
|
|
177
|
+
import { join } from "path";
|
|
178
|
+
var OPENSCOUT_RPC_CUTOVER_MARKER = "rpc-runtime-cutover-v1";
|
|
179
|
+
function resolveOpenScoutSupportPaths() {
|
|
180
|
+
const home = homedir();
|
|
181
|
+
const supportDirectory = process.env.OPENSCOUT_SUPPORT_DIRECTORY ?? join(home, "Library", "Application Support", "OpenScout");
|
|
182
|
+
const logsDirectory = join(supportDirectory, "logs");
|
|
183
|
+
const runtimeDirectory = join(supportDirectory, "runtime");
|
|
184
|
+
const catalogDirectory = join(supportDirectory, "catalog");
|
|
185
|
+
return {
|
|
186
|
+
supportDirectory,
|
|
187
|
+
logsDirectory,
|
|
188
|
+
appLogsDirectory: join(logsDirectory, "app"),
|
|
189
|
+
brokerLogsDirectory: join(logsDirectory, "broker"),
|
|
190
|
+
runtimeDirectory,
|
|
191
|
+
catalogDirectory,
|
|
192
|
+
relayAgentsDirectory: join(runtimeDirectory, "agents"),
|
|
193
|
+
settingsPath: join(supportDirectory, "settings.json"),
|
|
194
|
+
harnessCatalogPath: join(catalogDirectory, "harness-catalog.json"),
|
|
195
|
+
relayAgentsRegistryPath: join(supportDirectory, "relay-agents.json"),
|
|
196
|
+
relayHubDirectory: process.env.OPENSCOUT_RELAY_HUB ?? join(home, ".openscout", "relay"),
|
|
197
|
+
controlHome: process.env.OPENSCOUT_CONTROL_HOME ?? join(home, ".openscout", "control-plane"),
|
|
198
|
+
desktopStatusPath: join(supportDirectory, "agent-status.json"),
|
|
199
|
+
workspaceStatePath: join(supportDirectory, "workspace-state.json"),
|
|
200
|
+
cutoverMarkerPath: join(supportDirectory, OPENSCOUT_RPC_CUTOVER_MARKER)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function ensureOpenScoutCleanSlateSync() {
|
|
204
|
+
const paths = resolveOpenScoutSupportPaths();
|
|
205
|
+
if (existsSync(paths.cutoverMarkerPath)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
rmSync(paths.supportDirectory, { recursive: true, force: true });
|
|
209
|
+
rmSync(paths.controlHome, { recursive: true, force: true });
|
|
210
|
+
rmSync(paths.relayHubDirectory, { recursive: true, force: true });
|
|
211
|
+
mkdirSync(paths.supportDirectory, { recursive: true });
|
|
212
|
+
writeFileSync(paths.cutoverMarkerPath, `${Date.now()}
|
|
213
|
+
`, "utf8");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// packages/runtime/src/tool-resolution.ts
|
|
217
|
+
import { accessSync, constants, existsSync as existsSync2 } from "fs";
|
|
218
|
+
import { homedir as homedir2 } from "os";
|
|
219
|
+
import { basename, dirname, join as join2, resolve } from "path";
|
|
220
|
+
var DEFAULT_COMMON_EXECUTABLE_DIRECTORIES = [
|
|
221
|
+
join2(homedir2(), ".bun", "bin"),
|
|
222
|
+
"/opt/homebrew/bin",
|
|
223
|
+
"/usr/local/bin"
|
|
224
|
+
];
|
|
225
|
+
function expandHomePath(value, env = process.env) {
|
|
226
|
+
const home = env.HOME ?? homedir2();
|
|
227
|
+
if (value === "~") {
|
|
228
|
+
return home;
|
|
229
|
+
}
|
|
230
|
+
if (value.startsWith("~/")) {
|
|
231
|
+
return join2(home, value.slice(2));
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
function isExecutablePath(candidate) {
|
|
236
|
+
if (!candidate) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
accessSync(candidate, constants.X_OK);
|
|
241
|
+
return true;
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function splitPathEntries(env = process.env) {
|
|
247
|
+
const separator = process.platform === "win32" ? ";" : ":";
|
|
248
|
+
return (env.PATH ?? "").split(separator).filter(Boolean);
|
|
249
|
+
}
|
|
250
|
+
function dedupeDirectories(values, env) {
|
|
251
|
+
const seen = new Set;
|
|
252
|
+
const resolvedEntries = [];
|
|
253
|
+
for (const value of values) {
|
|
254
|
+
const normalized = resolve(expandHomePath(value, env));
|
|
255
|
+
if (seen.has(normalized)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
seen.add(normalized);
|
|
259
|
+
resolvedEntries.push(normalized);
|
|
260
|
+
}
|
|
261
|
+
return resolvedEntries;
|
|
262
|
+
}
|
|
263
|
+
function resolveExecutableFromSearch(options) {
|
|
264
|
+
const env = options.env ?? process.env;
|
|
265
|
+
const envKeys = options.envKeys ?? [];
|
|
266
|
+
for (const envKey of envKeys) {
|
|
267
|
+
const explicit = env[envKey]?.trim();
|
|
268
|
+
if (!explicit) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const expanded = expandHomePath(explicit, env);
|
|
272
|
+
if (isExecutablePath(expanded)) {
|
|
273
|
+
return { path: resolve(expanded), source: "env" };
|
|
274
|
+
}
|
|
275
|
+
const foundOnPath = findExecutableOnSearchPath(explicit, env);
|
|
276
|
+
if (foundOnPath) {
|
|
277
|
+
return { path: foundOnPath.path, source: "env" };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const commonDirectories = dedupeDirectories([...options.commonDirectories ?? DEFAULT_COMMON_EXECUTABLE_DIRECTORIES], env);
|
|
281
|
+
const searchDirectories = dedupeDirectories([
|
|
282
|
+
...splitPathEntries(env),
|
|
283
|
+
...options.extraDirectories ?? [],
|
|
284
|
+
...commonDirectories
|
|
285
|
+
], env);
|
|
286
|
+
for (const directory of searchDirectories) {
|
|
287
|
+
for (const name of options.names) {
|
|
288
|
+
const candidate = join2(directory, name);
|
|
289
|
+
if (isExecutablePath(candidate)) {
|
|
290
|
+
return {
|
|
291
|
+
path: candidate,
|
|
292
|
+
source: commonDirectories.includes(directory) ? "common-path" : "path"
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
function resolveBunExecutable(env = process.env) {
|
|
300
|
+
return resolveExecutableFromSearch({
|
|
301
|
+
env,
|
|
302
|
+
envKeys: ["OPENSCOUT_BUN_BIN", "SCOUT_BUN_BIN", "BUN_BIN"],
|
|
303
|
+
names: ["bun"]
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function findExecutableOnSearchPath(name, env) {
|
|
307
|
+
const searchDirectories = dedupeDirectories([
|
|
308
|
+
...splitPathEntries(env),
|
|
309
|
+
...DEFAULT_COMMON_EXECUTABLE_DIRECTORIES
|
|
310
|
+
], env);
|
|
311
|
+
for (const directory of searchDirectories) {
|
|
312
|
+
const candidate = join2(directory, name);
|
|
313
|
+
if (isExecutablePath(candidate)) {
|
|
314
|
+
return { path: candidate, source: "path" };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// packages/runtime/src/broker-process-manager.ts
|
|
321
|
+
function isTmpPath(p) {
|
|
322
|
+
return /^\/(?:private\/)?tmp\//.test(p);
|
|
323
|
+
}
|
|
324
|
+
var DEFAULT_BROKER_HOST = "127.0.0.1";
|
|
325
|
+
var DEFAULT_BROKER_HOST_MESH = "0.0.0.0";
|
|
326
|
+
var DEFAULT_BROKER_PORT = 65535;
|
|
327
|
+
var DEFAULT_ADVERTISE_SCOPE = "local";
|
|
328
|
+
function resolveAdvertiseScope() {
|
|
329
|
+
const raw = (process.env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
|
|
330
|
+
if (raw === "mesh")
|
|
331
|
+
return "mesh";
|
|
332
|
+
if (raw === "local")
|
|
333
|
+
return "local";
|
|
334
|
+
return DEFAULT_ADVERTISE_SCOPE;
|
|
335
|
+
}
|
|
336
|
+
function resolveBrokerHost(scope = resolveAdvertiseScope()) {
|
|
337
|
+
const explicit = process.env.OPENSCOUT_BROKER_HOST;
|
|
338
|
+
if (typeof explicit === "string" && explicit.trim().length > 0) {
|
|
339
|
+
return explicit;
|
|
340
|
+
}
|
|
341
|
+
return scope === "mesh" ? DEFAULT_BROKER_HOST_MESH : DEFAULT_BROKER_HOST;
|
|
342
|
+
}
|
|
343
|
+
var BROKER_SERVICE_POLL_INTERVAL_MS = 100;
|
|
344
|
+
var DEFAULT_BROKER_START_TIMEOUT_MS = 15000;
|
|
345
|
+
function buildDefaultBrokerUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
|
|
346
|
+
return `http://${host}:${port}`;
|
|
347
|
+
}
|
|
348
|
+
function buildDefaultBrokerSocketPath(runtimeDirectory) {
|
|
349
|
+
return join3(runtimeDirectory, "broker.sock");
|
|
350
|
+
}
|
|
351
|
+
var DEFAULT_BROKER_URL = buildDefaultBrokerUrl();
|
|
352
|
+
function runtimePackageDir() {
|
|
353
|
+
const explicit = process.env.OPENSCOUT_RUNTIME_PACKAGE_DIR?.trim();
|
|
354
|
+
if (explicit)
|
|
355
|
+
return explicit;
|
|
356
|
+
const fromBundledPackage = findBundledRuntimeDir();
|
|
357
|
+
if (fromBundledPackage)
|
|
358
|
+
return fromBundledPackage;
|
|
359
|
+
const fromCwd = findWorkspaceRuntimeDir(process.cwd());
|
|
360
|
+
if (fromCwd)
|
|
361
|
+
return fromCwd;
|
|
362
|
+
const fromGlobal = findGlobalRuntimeDir();
|
|
363
|
+
if (fromGlobal)
|
|
364
|
+
return fromGlobal;
|
|
365
|
+
const moduleDir = dirname2(fileURLToPath(import.meta.url));
|
|
366
|
+
return resolve2(moduleDir, "..");
|
|
367
|
+
}
|
|
368
|
+
function isInstalledRuntimePackageDir(candidate) {
|
|
369
|
+
return existsSync3(join3(candidate, "package.json")) && existsSync3(join3(candidate, "bin", "openscout-runtime.mjs"));
|
|
370
|
+
}
|
|
371
|
+
function findGlobalRuntimeDir() {
|
|
372
|
+
const candidates = [
|
|
373
|
+
join3(homedir3(), ".bun", "node_modules", "@openscout", "runtime"),
|
|
374
|
+
join3(homedir3(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
|
|
375
|
+
join3(homedir3(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
|
|
376
|
+
];
|
|
377
|
+
for (const c of candidates) {
|
|
378
|
+
if (isInstalledRuntimePackageDir(c))
|
|
379
|
+
return c;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const result = spawnSync("which", ["scout"], { encoding: "utf8", timeout: 3000 });
|
|
383
|
+
const scoutBin = result.stdout?.trim();
|
|
384
|
+
if (scoutBin) {
|
|
385
|
+
const scoutPkg = resolve2(scoutBin, "..", "..");
|
|
386
|
+
if (isInstalledRuntimePackageDir(scoutPkg))
|
|
387
|
+
return scoutPkg;
|
|
388
|
+
const nested = join3(scoutPkg, "node_modules", "@openscout", "runtime");
|
|
389
|
+
if (isInstalledRuntimePackageDir(nested))
|
|
390
|
+
return nested;
|
|
391
|
+
const sibling = resolve2(scoutPkg, "..", "runtime");
|
|
392
|
+
if (isInstalledRuntimePackageDir(sibling))
|
|
393
|
+
return sibling;
|
|
394
|
+
}
|
|
395
|
+
} catch {}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
function findBundledRuntimeDir() {
|
|
399
|
+
const moduleDir = dirname2(fileURLToPath(import.meta.url));
|
|
400
|
+
const candidate = resolve2(moduleDir, "..");
|
|
401
|
+
return isInstalledRuntimePackageDir(candidate) ? candidate : null;
|
|
402
|
+
}
|
|
403
|
+
function findWorkspaceRuntimeDir(startDir) {
|
|
404
|
+
let current = resolve2(startDir);
|
|
405
|
+
while (true) {
|
|
406
|
+
const candidate = join3(current, "packages", "runtime");
|
|
407
|
+
if (existsSync3(join3(candidate, "package.json")) && existsSync3(join3(candidate, "src"))) {
|
|
408
|
+
return candidate;
|
|
409
|
+
}
|
|
410
|
+
const parent = dirname2(current);
|
|
411
|
+
if (parent === current)
|
|
412
|
+
return null;
|
|
413
|
+
current = parent;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function resolveBunExecutable2() {
|
|
417
|
+
const bun = resolveBunExecutable(process.env);
|
|
418
|
+
if (bun) {
|
|
419
|
+
return bun.path;
|
|
420
|
+
}
|
|
421
|
+
throw new Error("Unable to locate Bun for broker service management. Install Bun or set OPENSCOUT_BUN_BIN.");
|
|
422
|
+
}
|
|
423
|
+
function resolveBrokerServiceMode() {
|
|
424
|
+
const explicit = (process.env.OPENSCOUT_BROKER_SERVICE_MODE ?? "").trim().toLowerCase();
|
|
425
|
+
if (explicit === "prod" || explicit === "production") {
|
|
426
|
+
return "prod";
|
|
427
|
+
}
|
|
428
|
+
if (explicit === "custom") {
|
|
429
|
+
return "custom";
|
|
430
|
+
}
|
|
431
|
+
return "dev";
|
|
432
|
+
}
|
|
433
|
+
function resolveBrokerStartTimeoutMs() {
|
|
434
|
+
const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_START_TIMEOUT_MS ?? "", 10);
|
|
435
|
+
if (Number.isFinite(explicit) && explicit > 0) {
|
|
436
|
+
return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
437
|
+
}
|
|
438
|
+
return DEFAULT_BROKER_START_TIMEOUT_MS;
|
|
439
|
+
}
|
|
440
|
+
function resolveBrokerServiceLabel(mode) {
|
|
441
|
+
const explicit = process.env.OPENSCOUT_SERVICE_LABEL?.trim() || process.env.OPENSCOUT_BROKER_SERVICE_LABEL?.trim();
|
|
442
|
+
if (explicit) {
|
|
443
|
+
return explicit;
|
|
444
|
+
}
|
|
445
|
+
switch (mode) {
|
|
446
|
+
case "prod":
|
|
447
|
+
return "com.openscout";
|
|
448
|
+
case "custom":
|
|
449
|
+
return "com.openscout.custom";
|
|
450
|
+
case "dev":
|
|
451
|
+
default:
|
|
452
|
+
return "dev.openscout";
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function legacyBrokerServiceLabel(mode) {
|
|
456
|
+
switch (mode) {
|
|
457
|
+
case "prod":
|
|
458
|
+
return "com.openscout.broker";
|
|
459
|
+
case "custom":
|
|
460
|
+
return "com.openscout.broker.custom";
|
|
461
|
+
case "dev":
|
|
462
|
+
default:
|
|
463
|
+
return "dev.openscout.broker";
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function resolveBrokerServiceConfig() {
|
|
467
|
+
const mode = resolveBrokerServiceMode();
|
|
468
|
+
const label = resolveBrokerServiceLabel(mode);
|
|
469
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : Number.parseInt(process.env.UID ?? "0", 10);
|
|
470
|
+
const supportPaths = resolveOpenScoutSupportPaths();
|
|
471
|
+
const defaultSupportDir = join3(homedir3(), "Library", "Application Support", "OpenScout");
|
|
472
|
+
const supportDirectory = isTmpPath(supportPaths.supportDirectory) ? defaultSupportDir : supportPaths.supportDirectory;
|
|
473
|
+
const runtimeDirectory = join3(supportDirectory, "runtime");
|
|
474
|
+
const logsDirectory = join3(supportDirectory, "logs", "broker");
|
|
475
|
+
const controlHome = isTmpPath(supportPaths.controlHome) ? join3(homedir3(), ".openscout", "control-plane") : supportPaths.controlHome;
|
|
476
|
+
const advertiseScope = resolveAdvertiseScope();
|
|
477
|
+
const brokerHost = resolveBrokerHost(advertiseScope);
|
|
478
|
+
const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(DEFAULT_BROKER_PORT), 10);
|
|
479
|
+
const brokerUrl = process.env.OPENSCOUT_BROKER_URL ?? buildDefaultBrokerUrl(brokerHost, brokerPort);
|
|
480
|
+
const brokerSocketPath = process.env.OPENSCOUT_BROKER_SOCKET_PATH ?? buildDefaultBrokerSocketPath(runtimeDirectory);
|
|
481
|
+
const launchAgentPath = join3(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
482
|
+
return {
|
|
483
|
+
label,
|
|
484
|
+
mode,
|
|
485
|
+
uid,
|
|
486
|
+
domainTarget: `gui/${uid}`,
|
|
487
|
+
serviceTarget: `gui/${uid}/${label}`,
|
|
488
|
+
launchAgentPath,
|
|
489
|
+
supportDirectory,
|
|
490
|
+
runtimeDirectory,
|
|
491
|
+
logsDirectory,
|
|
492
|
+
stdoutLogPath: join3(logsDirectory, "stdout.log"),
|
|
493
|
+
stderrLogPath: join3(logsDirectory, "stderr.log"),
|
|
494
|
+
controlHome,
|
|
495
|
+
runtimePackageDir: runtimePackageDir(),
|
|
496
|
+
bunExecutable: resolveBunExecutable2(),
|
|
497
|
+
brokerHost,
|
|
498
|
+
brokerPort,
|
|
499
|
+
brokerUrl,
|
|
500
|
+
brokerSocketPath,
|
|
501
|
+
advertiseScope,
|
|
502
|
+
coreAgents: readCoreAgentsSync()
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function renderLaunchAgentPlist(config) {
|
|
506
|
+
const launchPath = resolveLaunchAgentPATH();
|
|
507
|
+
const coreAgentsValue = (config.coreAgents ?? []).join(",");
|
|
508
|
+
const envEntries = {
|
|
509
|
+
OPENSCOUT_BROKER_HOST: config.brokerHost,
|
|
510
|
+
OPENSCOUT_BROKER_PORT: String(config.brokerPort),
|
|
511
|
+
OPENSCOUT_BROKER_URL: config.brokerUrl,
|
|
512
|
+
OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
|
|
513
|
+
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
514
|
+
OPENSCOUT_BROKER_SERVICE_MODE: config.mode,
|
|
515
|
+
OPENSCOUT_BROKER_SERVICE_LABEL: config.label,
|
|
516
|
+
OPENSCOUT_SERVICE_LABEL: config.label,
|
|
517
|
+
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope,
|
|
518
|
+
HOME: homedir3(),
|
|
519
|
+
PATH: launchPath,
|
|
520
|
+
...collectOptionalEnvVars([
|
|
521
|
+
"OPENSCOUT_MESH_ID",
|
|
522
|
+
"OPENSCOUT_MESH_SEEDS",
|
|
523
|
+
"OPENSCOUT_MESH_DISCOVERY_INTERVAL_MS",
|
|
524
|
+
"OPENSCOUT_NODE_NAME",
|
|
525
|
+
"OPENSCOUT_NODE_ID",
|
|
526
|
+
"OPENSCOUT_NODE_QUALIFIER",
|
|
527
|
+
"OPENSCOUT_TAILSCALE_BIN",
|
|
528
|
+
"OPENSCOUT_TAILSCALE_STATUS_JSON",
|
|
529
|
+
"OPENSCOUT_SSE_KEEPALIVE_MS"
|
|
530
|
+
])
|
|
531
|
+
};
|
|
532
|
+
if (coreAgentsValue) {
|
|
533
|
+
envEntries["OPENSCOUT_CORE_AGENTS"] = coreAgentsValue;
|
|
534
|
+
}
|
|
535
|
+
const envBlock = Object.entries(envEntries).map(([key, value]) => `
|
|
536
|
+
<key>${xmlEscape(key)}</key>
|
|
537
|
+
<string>${xmlEscape(value)}</string>`).join("");
|
|
538
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
539
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
540
|
+
<plist version="1.0">
|
|
541
|
+
<dict>
|
|
542
|
+
<key>Label</key>
|
|
543
|
+
<string>${xmlEscape(config.label)}</string>
|
|
544
|
+
<key>ProgramArguments</key>
|
|
545
|
+
<array>
|
|
546
|
+
<string>${xmlEscape(config.bunExecutable)}</string>
|
|
547
|
+
<string>${xmlEscape(join3(config.runtimePackageDir, "bin", "openscout-runtime.mjs"))}</string>
|
|
548
|
+
<string>base</string>
|
|
549
|
+
</array>
|
|
550
|
+
<key>WorkingDirectory</key>
|
|
551
|
+
<string>${xmlEscape(config.runtimePackageDir)}</string>
|
|
552
|
+
<key>RunAtLoad</key>
|
|
553
|
+
<true/>
|
|
554
|
+
<key>KeepAlive</key>
|
|
555
|
+
<dict>
|
|
556
|
+
<key>SuccessfulExit</key>
|
|
557
|
+
<false/>
|
|
558
|
+
</dict>
|
|
559
|
+
<key>StandardOutPath</key>
|
|
560
|
+
<string>${xmlEscape(config.stdoutLogPath)}</string>
|
|
561
|
+
<key>StandardErrorPath</key>
|
|
562
|
+
<string>${xmlEscape(config.stderrLogPath)}</string>
|
|
563
|
+
<key>EnvironmentVariables</key>
|
|
564
|
+
<dict>${envBlock}
|
|
565
|
+
</dict>
|
|
566
|
+
</dict>
|
|
567
|
+
</plist>
|
|
568
|
+
`;
|
|
569
|
+
}
|
|
570
|
+
function readCoreAgentsSync() {
|
|
571
|
+
try {
|
|
572
|
+
const settingsPath = resolveOpenScoutSupportPaths().settingsPath;
|
|
573
|
+
const raw = readFileSync(settingsPath, "utf8");
|
|
574
|
+
const settings = JSON.parse(raw);
|
|
575
|
+
const raw_agents = settings?.agents?.coreAgents;
|
|
576
|
+
if (Array.isArray(raw_agents)) {
|
|
577
|
+
return raw_agents.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
578
|
+
}
|
|
579
|
+
} catch {}
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
function collectOptionalEnvVars(keys) {
|
|
583
|
+
const entries = {};
|
|
584
|
+
for (const key of keys) {
|
|
585
|
+
const value = process.env[key];
|
|
586
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
587
|
+
entries[key] = value;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return entries;
|
|
591
|
+
}
|
|
592
|
+
function resolveLaunchAgentPATH() {
|
|
593
|
+
const entries = [
|
|
594
|
+
join3(homedir3(), ".bun", "bin"),
|
|
595
|
+
...(process.env.PATH ?? "").split(":").filter(Boolean),
|
|
596
|
+
"/opt/homebrew/bin",
|
|
597
|
+
"/usr/local/bin",
|
|
598
|
+
"/usr/bin",
|
|
599
|
+
"/bin",
|
|
600
|
+
"/usr/sbin",
|
|
601
|
+
"/sbin"
|
|
602
|
+
];
|
|
603
|
+
return Array.from(new Set(entries)).filter((e) => !isTmpPath(e)).join(":");
|
|
604
|
+
}
|
|
605
|
+
function xmlEscape(value) {
|
|
606
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
607
|
+
}
|
|
608
|
+
function ensureParentDirectory(filePath) {
|
|
609
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
610
|
+
}
|
|
611
|
+
function ensureServiceDirectories(config) {
|
|
612
|
+
ensureOpenScoutCleanSlateSync();
|
|
613
|
+
mkdirSync2(config.supportDirectory, { recursive: true });
|
|
614
|
+
mkdirSync2(config.runtimeDirectory, { recursive: true, mode: 448 });
|
|
615
|
+
chmodSync(config.runtimeDirectory, 448);
|
|
616
|
+
mkdirSync2(config.logsDirectory, { recursive: true });
|
|
617
|
+
mkdirSync2(config.controlHome, { recursive: true });
|
|
618
|
+
ensureParentDirectory(config.launchAgentPath);
|
|
619
|
+
}
|
|
620
|
+
function writeLaunchAgentPlist(config) {
|
|
621
|
+
ensureServiceDirectories(config);
|
|
622
|
+
writeFileSync2(config.launchAgentPath, renderLaunchAgentPlist(config), "utf8");
|
|
623
|
+
}
|
|
624
|
+
function bootoutCommand(config) {
|
|
625
|
+
return `${launchctlPath()} bootout ${config.serviceTarget}`;
|
|
626
|
+
}
|
|
627
|
+
function legacyLaunchAgentPath(config) {
|
|
628
|
+
return join3(homedir3(), "Library", "LaunchAgents", `${legacyBrokerServiceLabel(config.mode)}.plist`);
|
|
629
|
+
}
|
|
630
|
+
function legacyServiceTarget(config) {
|
|
631
|
+
return `${config.domainTarget}/${legacyBrokerServiceLabel(config.mode)}`;
|
|
632
|
+
}
|
|
633
|
+
function bootoutLegacyBrokerService(config) {
|
|
634
|
+
const legacyTarget = legacyServiceTarget(config);
|
|
635
|
+
if (legacyTarget === config.serviceTarget) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
runCommand(launchctlPath(), ["bootout", legacyTarget], { allowFailure: true });
|
|
639
|
+
}
|
|
640
|
+
function runCommand(command, args, options) {
|
|
641
|
+
const result = spawnSync(command, args, {
|
|
642
|
+
env: {
|
|
643
|
+
...process.env,
|
|
644
|
+
...options?.env ?? {}
|
|
645
|
+
},
|
|
646
|
+
encoding: "utf8"
|
|
647
|
+
});
|
|
648
|
+
const stdout = (result.stdout ?? "").trim();
|
|
649
|
+
const stderr = (result.stderr ?? "").trim();
|
|
650
|
+
const exitCode = result.status ?? 1;
|
|
651
|
+
if (exitCode !== 0 && !options?.allowFailure) {
|
|
652
|
+
throw new Error(stderr || stdout || `${command} exited with status ${exitCode}`);
|
|
653
|
+
}
|
|
654
|
+
return { exitCode, stdout, stderr };
|
|
655
|
+
}
|
|
656
|
+
function launchctlPath() {
|
|
657
|
+
return "/bin/launchctl";
|
|
658
|
+
}
|
|
659
|
+
function readLogLines(path) {
|
|
660
|
+
if (!existsSync3(path)) {
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
return readFileSync(path, "utf8").split(`
|
|
664
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
665
|
+
}
|
|
666
|
+
function isPackageScriptBanner(line) {
|
|
667
|
+
return /^\$\s*(bun run|npm run|pnpm\b|yarn\b)/.test(line);
|
|
668
|
+
}
|
|
669
|
+
function selectLastRelevantLogLine(lines) {
|
|
670
|
+
for (let index = lines.length - 1;index >= 0; index -= 1) {
|
|
671
|
+
const line = lines[index];
|
|
672
|
+
if (!isPackageScriptBanner(line)) {
|
|
673
|
+
return line;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return lines.at(-1) ?? null;
|
|
677
|
+
}
|
|
678
|
+
function readLastLogLine(paths) {
|
|
679
|
+
let fallback = null;
|
|
680
|
+
for (const path of paths) {
|
|
681
|
+
const lines = readLogLines(path);
|
|
682
|
+
if (lines.length === 0) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const relevantLine = selectLastRelevantLogLine(lines);
|
|
686
|
+
if (!relevantLine) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!isPackageScriptBanner(relevantLine)) {
|
|
690
|
+
return relevantLine;
|
|
691
|
+
}
|
|
692
|
+
fallback ??= relevantLine;
|
|
693
|
+
}
|
|
694
|
+
return fallback;
|
|
695
|
+
}
|
|
696
|
+
function parseLaunchctlPrint(output) {
|
|
697
|
+
const pidMatch = output.match(/\bpid = (\d+)/);
|
|
698
|
+
const stateMatch = output.match(/\bstate = ([^\n]+)/);
|
|
699
|
+
const lastExitMatch = output.match(/\blast exit code = (-?\d+)/i) || output.match(/\blast exit status = (-?\d+)/i);
|
|
700
|
+
return {
|
|
701
|
+
pid: pidMatch ? Number.parseInt(pidMatch[1] ?? "0", 10) : null,
|
|
702
|
+
launchdState: stateMatch?.[1]?.trim() ?? null,
|
|
703
|
+
lastExitStatus: lastExitMatch ? Number.parseInt(lastExitMatch[1] ?? "0", 10) : null
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function inspectLaunchctl(config) {
|
|
707
|
+
const printResult = runCommand(launchctlPath(), ["print", config.serviceTarget], { allowFailure: true });
|
|
708
|
+
if (printResult.exitCode !== 0) {
|
|
709
|
+
return {
|
|
710
|
+
loaded: false,
|
|
711
|
+
pid: null,
|
|
712
|
+
launchdState: null,
|
|
713
|
+
lastExitStatus: null,
|
|
714
|
+
raw: printResult.stderr || printResult.stdout
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
loaded: true,
|
|
719
|
+
raw: printResult.stdout,
|
|
720
|
+
...parseLaunchctlPrint(printResult.stdout)
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function fetchHealthSnapshot(config) {
|
|
724
|
+
const controller = new AbortController;
|
|
725
|
+
const timeout = setTimeout(() => controller.abort(), 1000);
|
|
726
|
+
const checkedAt = Date.now();
|
|
727
|
+
try {
|
|
728
|
+
const { value: payload, trace } = await requestScoutBrokerJsonWithTrace(config.brokerUrl, "/health", {
|
|
729
|
+
signal: controller.signal,
|
|
730
|
+
socketPath: config.brokerSocketPath
|
|
731
|
+
});
|
|
732
|
+
return {
|
|
733
|
+
reachable: true,
|
|
734
|
+
ok: Boolean(payload.ok),
|
|
735
|
+
checkedAt,
|
|
736
|
+
transport: trace.transport,
|
|
737
|
+
socketPath: trace.socketPath,
|
|
738
|
+
socketFallbackError: trace.socketFallbackError,
|
|
739
|
+
nodeId: payload.nodeId,
|
|
740
|
+
meshId: payload.meshId,
|
|
741
|
+
counts: payload.counts ? {
|
|
742
|
+
nodes: payload.counts.nodes ?? 0,
|
|
743
|
+
actors: payload.counts.actors ?? 0,
|
|
744
|
+
agents: payload.counts.agents ?? 0,
|
|
745
|
+
conversations: payload.counts.conversations ?? 0,
|
|
746
|
+
messages: payload.counts.messages ?? 0,
|
|
747
|
+
flights: payload.counts.flights ?? 0,
|
|
748
|
+
collaborationRecords: payload.counts.collaborationRecords ?? 0
|
|
749
|
+
} : undefined
|
|
750
|
+
};
|
|
751
|
+
} catch (error) {
|
|
752
|
+
return {
|
|
753
|
+
reachable: false,
|
|
754
|
+
ok: false,
|
|
755
|
+
checkedAt,
|
|
756
|
+
socketPath: config.brokerSocketPath,
|
|
757
|
+
error: error instanceof Error ? error.message : String(error)
|
|
758
|
+
};
|
|
759
|
+
} finally {
|
|
760
|
+
clearTimeout(timeout);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
|
|
764
|
+
ensureServiceDirectories(config);
|
|
765
|
+
const launchctl = inspectLaunchctl(config);
|
|
766
|
+
const health = await fetchHealthSnapshot(config);
|
|
767
|
+
const installed = existsSync3(config.launchAgentPath);
|
|
768
|
+
const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
|
|
769
|
+
return {
|
|
770
|
+
label: config.label,
|
|
771
|
+
mode: config.mode,
|
|
772
|
+
launchAgentPath: config.launchAgentPath,
|
|
773
|
+
bootoutCommand: bootoutCommand(config),
|
|
774
|
+
brokerUrl: config.brokerUrl,
|
|
775
|
+
brokerSocketPath: config.brokerSocketPath,
|
|
776
|
+
supportDirectory: config.supportDirectory,
|
|
777
|
+
runtimeDirectory: config.runtimeDirectory,
|
|
778
|
+
controlHome: config.controlHome,
|
|
779
|
+
stdoutLogPath: config.stdoutLogPath,
|
|
780
|
+
stderrLogPath: config.stderrLogPath,
|
|
781
|
+
installed,
|
|
782
|
+
loaded: launchctl.loaded,
|
|
783
|
+
pid: launchctl.pid,
|
|
784
|
+
launchdState: launchctl.launchdState,
|
|
785
|
+
lastExitStatus: launchctl.lastExitStatus,
|
|
786
|
+
usesLaunchAgent: installed || launchctl.loaded,
|
|
787
|
+
reachable: health.reachable,
|
|
788
|
+
health,
|
|
789
|
+
lastLogLine
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async function installBrokerService(config = resolveBrokerServiceConfig()) {
|
|
793
|
+
bootoutLegacyBrokerService(config);
|
|
794
|
+
writeLaunchAgentPlist(config);
|
|
795
|
+
return brokerServiceStatus(config);
|
|
796
|
+
}
|
|
797
|
+
async function startBrokerService(config = resolveBrokerServiceConfig()) {
|
|
798
|
+
bootoutLegacyBrokerService(config);
|
|
799
|
+
writeLaunchAgentPlist(config);
|
|
800
|
+
runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
|
|
801
|
+
runCommand(launchctlPath(), ["bootstrap", config.domainTarget, config.launchAgentPath], { allowFailure: true });
|
|
802
|
+
runCommand(launchctlPath(), ["kickstart", "-k", config.serviceTarget], { allowFailure: true });
|
|
803
|
+
const attempts = Math.ceil(resolveBrokerStartTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
804
|
+
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
805
|
+
const status2 = await brokerServiceStatus(config);
|
|
806
|
+
if (status2.health.reachable) {
|
|
807
|
+
return status2;
|
|
808
|
+
}
|
|
809
|
+
await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
810
|
+
}
|
|
811
|
+
const status = await brokerServiceStatus(config);
|
|
812
|
+
throw new Error(status.lastLogLine ?? status.health.error ?? "Broker service did not become healthy.");
|
|
813
|
+
}
|
|
814
|
+
function sleep(ms) {
|
|
815
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
816
|
+
}
|
|
817
|
+
async function stopBrokerService(config = resolveBrokerServiceConfig()) {
|
|
818
|
+
runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
|
|
819
|
+
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
820
|
+
const status = await brokerServiceStatus(config);
|
|
821
|
+
if (!status.health.reachable) {
|
|
822
|
+
return status;
|
|
823
|
+
}
|
|
824
|
+
await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
825
|
+
}
|
|
826
|
+
return brokerServiceStatus(config);
|
|
827
|
+
}
|
|
828
|
+
async function restartBrokerService(config = resolveBrokerServiceConfig()) {
|
|
829
|
+
await stopBrokerService(config);
|
|
830
|
+
return startBrokerService(config);
|
|
831
|
+
}
|
|
832
|
+
async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
|
|
833
|
+
await stopBrokerService(config);
|
|
834
|
+
if (existsSync3(config.launchAgentPath)) {
|
|
835
|
+
rmSync2(config.launchAgentPath, { force: true });
|
|
836
|
+
}
|
|
837
|
+
const legacyPath = legacyLaunchAgentPath(config);
|
|
838
|
+
bootoutLegacyBrokerService(config);
|
|
839
|
+
if (existsSync3(legacyPath)) {
|
|
840
|
+
rmSync2(legacyPath, { force: true });
|
|
841
|
+
}
|
|
842
|
+
return brokerServiceStatus(config);
|
|
843
|
+
}
|
|
844
|
+
async function main() {
|
|
845
|
+
const command = process.argv[2] ?? "status";
|
|
846
|
+
const json = process.argv.includes("--json");
|
|
847
|
+
const config = resolveBrokerServiceConfig();
|
|
848
|
+
let status;
|
|
849
|
+
switch (command) {
|
|
850
|
+
case "install":
|
|
851
|
+
status = await installBrokerService(config);
|
|
852
|
+
break;
|
|
853
|
+
case "start":
|
|
854
|
+
status = await startBrokerService(config);
|
|
855
|
+
break;
|
|
856
|
+
case "stop":
|
|
857
|
+
status = await stopBrokerService(config);
|
|
858
|
+
break;
|
|
859
|
+
case "restart":
|
|
860
|
+
status = await restartBrokerService(config);
|
|
861
|
+
break;
|
|
862
|
+
case "uninstall":
|
|
863
|
+
status = await uninstallBrokerService(config);
|
|
864
|
+
break;
|
|
865
|
+
case "status":
|
|
866
|
+
status = await brokerServiceStatus(config);
|
|
867
|
+
break;
|
|
868
|
+
default:
|
|
869
|
+
console.error(`Unknown broker service command: ${command}`);
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
if (json) {
|
|
873
|
+
console.log(JSON.stringify(status, null, 2));
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
console.log(formatBrokerServiceStatus(status));
|
|
877
|
+
}
|
|
878
|
+
function formatBrokerServiceStatus(status) {
|
|
879
|
+
const lines = [
|
|
880
|
+
`label: ${status.label}`,
|
|
881
|
+
`mode: ${status.mode}`,
|
|
882
|
+
`launch agent: ${status.installed ? status.launchAgentPath : "not installed"}`,
|
|
883
|
+
`bootout: ${status.bootoutCommand}`,
|
|
884
|
+
`loaded: ${status.loaded ? "yes" : "no"}`,
|
|
885
|
+
`pid: ${status.pid ?? "\u2014"}`,
|
|
886
|
+
`launchd state: ${status.launchdState ?? "\u2014"}`,
|
|
887
|
+
`broker url: ${status.brokerUrl}`,
|
|
888
|
+
`broker socket: ${status.brokerSocketPath}`,
|
|
889
|
+
`reachable: ${status.reachable ? "yes" : "no"}`,
|
|
890
|
+
`health: ${status.health.ok ? "ok" : status.health.error ?? "unreachable"}`,
|
|
891
|
+
`health transport: ${status.health.transport ?? "unknown"}`,
|
|
892
|
+
`logs: ${status.stdoutLogPath}`
|
|
893
|
+
];
|
|
894
|
+
if (status.health.socketFallbackError) {
|
|
895
|
+
lines.push(`socket fallback: ${status.health.socketFallbackError}`);
|
|
896
|
+
}
|
|
897
|
+
if (status.lastLogLine) {
|
|
898
|
+
lines.push(`last log: ${status.lastLogLine}`);
|
|
899
|
+
}
|
|
900
|
+
return lines.join(`
|
|
901
|
+
`);
|
|
902
|
+
}
|
|
903
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1] && !process.argv[1].endsWith("/main.mjs")) {
|
|
904
|
+
main().catch((error) => {
|
|
905
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
906
|
+
process.exit(1);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// packages/runtime/src/local-config.ts
|
|
911
|
+
import {
|
|
912
|
+
existsSync as existsSync4,
|
|
913
|
+
mkdirSync as mkdirSync3,
|
|
914
|
+
readFileSync as readFileSync2,
|
|
915
|
+
renameSync,
|
|
916
|
+
writeFileSync as writeFileSync3
|
|
917
|
+
} from "fs";
|
|
918
|
+
import { homedir as homedir4, hostname as osHostname } from "os";
|
|
919
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
920
|
+
var LOCAL_CONFIG_VERSION = 1;
|
|
921
|
+
var DEFAULT_SCOUT_WEB_PORTAL_HOST = "scout.local";
|
|
922
|
+
var DEFAULT_LOCAL_CONFIG = {
|
|
923
|
+
version: LOCAL_CONFIG_VERSION,
|
|
924
|
+
host: "127.0.0.1",
|
|
925
|
+
webLocalName: undefined,
|
|
926
|
+
ports: {
|
|
927
|
+
broker: 65535,
|
|
928
|
+
web: 3200,
|
|
929
|
+
pairing: 7888
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
function normalizeLocalHostnameLabel(value) {
|
|
933
|
+
const firstLabel = value?.trim().replace(/\.local\.?$/i, "").split(".").find((part) => part.trim().length > 0)?.trim();
|
|
934
|
+
const normalized = firstLabel?.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
|
|
935
|
+
return normalized || "localhost";
|
|
936
|
+
}
|
|
937
|
+
function normalizeLocalHostname(value) {
|
|
938
|
+
const trimmed = value?.trim().replace(/\.$/, "").toLowerCase();
|
|
939
|
+
const labels = trimmed?.split(".").map((label) => label.trim().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-")).filter(Boolean);
|
|
940
|
+
return labels && labels.length > 0 ? labels.join(".") : "localhost";
|
|
941
|
+
}
|
|
942
|
+
function resolveScoutWebNamedHostname(name) {
|
|
943
|
+
const normalized = normalizeLocalHostname(name);
|
|
944
|
+
return normalized.includes(".") ? normalized : `${normalized}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
|
|
945
|
+
}
|
|
946
|
+
function resolveConfiguredScoutWebHostname(config = loadLocalConfig(), machineHostname = osHostname()) {
|
|
947
|
+
if (config.webLocalName) {
|
|
948
|
+
return resolveScoutWebNamedHostname(config.webLocalName);
|
|
949
|
+
}
|
|
950
|
+
return `${normalizeLocalHostnameLabel(machineHostname)}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
|
|
951
|
+
}
|
|
952
|
+
function localConfigHome() {
|
|
953
|
+
return process.env.OPENSCOUT_HOME ?? join4(homedir4(), ".openscout");
|
|
954
|
+
}
|
|
955
|
+
function localConfigPath() {
|
|
956
|
+
return join4(localConfigHome(), "config.json");
|
|
957
|
+
}
|
|
958
|
+
function loadLocalConfig() {
|
|
959
|
+
const configPath = localConfigPath();
|
|
960
|
+
if (!existsSync4(configPath))
|
|
961
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
962
|
+
try {
|
|
963
|
+
return validateLocalConfig(JSON.parse(readFileSync2(configPath, "utf8")));
|
|
964
|
+
} catch {
|
|
965
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function validateLocalConfig(input) {
|
|
969
|
+
if (!input || typeof input !== "object")
|
|
970
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
971
|
+
const raw = input;
|
|
972
|
+
const out = { version: LOCAL_CONFIG_VERSION };
|
|
973
|
+
if (typeof raw.host === "string" && raw.host.trim().length > 0) {
|
|
974
|
+
out.host = raw.host.trim();
|
|
975
|
+
}
|
|
976
|
+
if (typeof raw.webLocalName === "string" && raw.webLocalName.trim().length > 0) {
|
|
977
|
+
out.webLocalName = raw.webLocalName.trim();
|
|
978
|
+
}
|
|
979
|
+
if (raw.ports && typeof raw.ports === "object") {
|
|
980
|
+
const ports = raw.ports;
|
|
981
|
+
const p = {};
|
|
982
|
+
if (isValidPort(ports.broker))
|
|
983
|
+
p.broker = ports.broker;
|
|
984
|
+
if (isValidPort(ports.web))
|
|
985
|
+
p.web = ports.web;
|
|
986
|
+
if (isValidPort(ports.pairing))
|
|
987
|
+
p.pairing = ports.pairing;
|
|
988
|
+
if (Object.keys(p).length > 0)
|
|
989
|
+
out.ports = p;
|
|
990
|
+
}
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
function isValidPort(value) {
|
|
994
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
|
|
995
|
+
}
|
|
996
|
+
function resolveBrokerPort() {
|
|
997
|
+
return loadLocalConfig().ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
|
|
998
|
+
}
|
|
999
|
+
function resolveWebPort() {
|
|
1000
|
+
return loadLocalConfig().ports?.web ?? DEFAULT_LOCAL_CONFIG.ports.web;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// packages/runtime/src/local-edge.ts
|
|
1004
|
+
function uniqRoutes(routes) {
|
|
1005
|
+
const seen = new Set;
|
|
1006
|
+
const out = [];
|
|
1007
|
+
for (const route of routes) {
|
|
1008
|
+
const key = `${route.host.toLowerCase()} ${route.upstream}`;
|
|
1009
|
+
if (seen.has(key)) {
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
seen.add(key);
|
|
1013
|
+
out.push(route);
|
|
1014
|
+
}
|
|
1015
|
+
return out;
|
|
1016
|
+
}
|
|
1017
|
+
function resolveOpenScoutLocalEdgeConfig(input = {}) {
|
|
1018
|
+
const portalHost = resolveScoutWebNamedHostname(input.portalHost ?? DEFAULT_SCOUT_WEB_PORTAL_HOST);
|
|
1019
|
+
const nodeHost = resolveScoutWebNamedHostname(input.nodeHost ?? resolveConfiguredScoutWebHostname());
|
|
1020
|
+
const wildcardHost = `*.${portalHost}`;
|
|
1021
|
+
const scheme = input.scheme ?? "http";
|
|
1022
|
+
const upstream = `127.0.0.1:${input.webPort ?? resolveWebPort()}`;
|
|
1023
|
+
const brokerUpstream = `127.0.0.1:${input.brokerPort ?? resolveBrokerPort()}`;
|
|
1024
|
+
return {
|
|
1025
|
+
portalHost,
|
|
1026
|
+
nodeHost,
|
|
1027
|
+
wildcardHost,
|
|
1028
|
+
scheme,
|
|
1029
|
+
brokerUpstream,
|
|
1030
|
+
routes: uniqRoutes([
|
|
1031
|
+
{ host: portalHost, upstream },
|
|
1032
|
+
{ host: wildcardHost, upstream }
|
|
1033
|
+
])
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function renderOpenScoutStartPage(config) {
|
|
1037
|
+
const pageConfig = JSON.stringify({
|
|
1038
|
+
startPath: "/__openscout/web/start"
|
|
1039
|
+
});
|
|
1040
|
+
return `<!doctype html>
|
|
1041
|
+
<html lang="en">
|
|
1042
|
+
<head>
|
|
1043
|
+
<meta charset="utf-8">
|
|
1044
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1045
|
+
<title>Start Scout</title>
|
|
1046
|
+
<style>
|
|
1047
|
+
:root {
|
|
1048
|
+
color-scheme: light dark;
|
|
1049
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1050
|
+
background: #0f1720;
|
|
1051
|
+
color: #f5f7fb;
|
|
1052
|
+
}
|
|
1053
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1054
|
+
body {
|
|
1055
|
+
min-height: 100vh;
|
|
1056
|
+
display: grid;
|
|
1057
|
+
place-items: center;
|
|
1058
|
+
padding: 32px;
|
|
1059
|
+
background:
|
|
1060
|
+
radial-gradient(ellipse at 15% 30%, rgba(91, 141, 239, 0.16) 0%, transparent 50%),
|
|
1061
|
+
radial-gradient(ellipse at 85% 70%, rgba(74, 222, 128, 0.06) 0%, transparent 40%),
|
|
1062
|
+
#111827;
|
|
1063
|
+
}
|
|
1064
|
+
main {
|
|
1065
|
+
width: min(400px, 100%);
|
|
1066
|
+
padding: 28px 30px 26px;
|
|
1067
|
+
border: 1px solid rgba(255, 255, 255, 0.09);
|
|
1068
|
+
border-radius: 10px;
|
|
1069
|
+
background: rgba(15, 23, 32, 0.9);
|
|
1070
|
+
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 20px 60px rgba(0, 0, 0, 0.45);
|
|
1071
|
+
}
|
|
1072
|
+
.badge {
|
|
1073
|
+
display: inline-flex;
|
|
1074
|
+
align-items: center;
|
|
1075
|
+
gap: 6px;
|
|
1076
|
+
margin-bottom: 18px;
|
|
1077
|
+
font-size: 11px;
|
|
1078
|
+
font-weight: 600;
|
|
1079
|
+
letter-spacing: 0.07em;
|
|
1080
|
+
text-transform: uppercase;
|
|
1081
|
+
color: rgba(74, 222, 128, 0.8);
|
|
1082
|
+
}
|
|
1083
|
+
@keyframes blink {
|
|
1084
|
+
0%, 100% { opacity: 1; }
|
|
1085
|
+
50% { opacity: 0.2; }
|
|
1086
|
+
}
|
|
1087
|
+
.badge-dot {
|
|
1088
|
+
width: 6px;
|
|
1089
|
+
height: 6px;
|
|
1090
|
+
border-radius: 50%;
|
|
1091
|
+
background: #4ade80;
|
|
1092
|
+
flex-shrink: 0;
|
|
1093
|
+
animation: blink 2.4s ease-in-out infinite;
|
|
1094
|
+
}
|
|
1095
|
+
h1 {
|
|
1096
|
+
font-size: 22px;
|
|
1097
|
+
font-weight: 700;
|
|
1098
|
+
line-height: 1.2;
|
|
1099
|
+
margin-bottom: 8px;
|
|
1100
|
+
color: #f5f7fb;
|
|
1101
|
+
}
|
|
1102
|
+
p {
|
|
1103
|
+
font-size: 14px;
|
|
1104
|
+
color: rgba(245, 247, 251, 0.5);
|
|
1105
|
+
line-height: 1.55;
|
|
1106
|
+
margin-bottom: 22px;
|
|
1107
|
+
}
|
|
1108
|
+
button {
|
|
1109
|
+
width: 100%;
|
|
1110
|
+
min-height: 44px;
|
|
1111
|
+
border: 0;
|
|
1112
|
+
border-radius: 7px;
|
|
1113
|
+
background: #f4d35e;
|
|
1114
|
+
color: #17202a;
|
|
1115
|
+
font: inherit;
|
|
1116
|
+
font-size: 14px;
|
|
1117
|
+
font-weight: 700;
|
|
1118
|
+
cursor: pointer;
|
|
1119
|
+
transition: background 0.1s, opacity 0.1s;
|
|
1120
|
+
}
|
|
1121
|
+
button:hover:not(:disabled) { background: #f7dc74; }
|
|
1122
|
+
button:active:not(:disabled) { background: #e8c44a; }
|
|
1123
|
+
button:disabled { cursor: progress; opacity: 0.6; }
|
|
1124
|
+
.progress {
|
|
1125
|
+
height: 2px;
|
|
1126
|
+
margin-top: 14px;
|
|
1127
|
+
border-radius: 2px;
|
|
1128
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1129
|
+
overflow: hidden;
|
|
1130
|
+
}
|
|
1131
|
+
@keyframes sweep {
|
|
1132
|
+
0% { transform: translateX(-100%); }
|
|
1133
|
+
100% { transform: translateX(500%); }
|
|
1134
|
+
}
|
|
1135
|
+
.progress-bar {
|
|
1136
|
+
height: 100%;
|
|
1137
|
+
width: 20%;
|
|
1138
|
+
border-radius: 2px;
|
|
1139
|
+
background: rgba(244, 211, 94, 0.7);
|
|
1140
|
+
transform: translateX(-100%);
|
|
1141
|
+
}
|
|
1142
|
+
.progress-bar.running {
|
|
1143
|
+
animation: sweep 1.5s ease-in-out infinite;
|
|
1144
|
+
}
|
|
1145
|
+
output {
|
|
1146
|
+
display: block;
|
|
1147
|
+
min-height: 16px;
|
|
1148
|
+
margin-top: 10px;
|
|
1149
|
+
color: rgba(245, 247, 251, 0.38);
|
|
1150
|
+
font-family: ui-monospace, "SF Mono", Menlo, "Cascadia Code", monospace;
|
|
1151
|
+
font-size: 12px;
|
|
1152
|
+
line-height: 1.4;
|
|
1153
|
+
}
|
|
1154
|
+
</style>
|
|
1155
|
+
</head>
|
|
1156
|
+
<body>
|
|
1157
|
+
<main>
|
|
1158
|
+
<div class="badge"><span class="badge-dot"></span>Broker online</div>
|
|
1159
|
+
<h1>Start Scout</h1>
|
|
1160
|
+
<p>The web app is not running yet. Click to start it on this machine.</p>
|
|
1161
|
+
<button id="start" type="button">Start Scout</button>
|
|
1162
|
+
<div class="progress"><div class="progress-bar" id="bar"></div></div>
|
|
1163
|
+
<output id="status" role="status"></output>
|
|
1164
|
+
</main>
|
|
1165
|
+
<script>
|
|
1166
|
+
const config = ${pageConfig};
|
|
1167
|
+
const button = document.getElementById('start');
|
|
1168
|
+
const bar = document.getElementById('bar');
|
|
1169
|
+
const status = document.getElementById('status');
|
|
1170
|
+
const targetPath = window.location.pathname + window.location.search + window.location.hash;
|
|
1171
|
+
const healthUrl = new URL('/api/health', window.location.origin);
|
|
1172
|
+
const startUrl = new URL(config.startPath, window.location.origin);
|
|
1173
|
+
|
|
1174
|
+
function setStatus(message) {
|
|
1175
|
+
status.textContent = message;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function setWaiting(on) {
|
|
1179
|
+
bar.classList.toggle('running', on);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async function waitForWeb() {
|
|
1183
|
+
const deadline = Date.now() + 20000;
|
|
1184
|
+
while (Date.now() < deadline) {
|
|
1185
|
+
try {
|
|
1186
|
+
const response = await fetch(healthUrl, { headers: { accept: 'application/json' }, cache: 'no-store' });
|
|
1187
|
+
if (response.ok) {
|
|
1188
|
+
const body = await response.json();
|
|
1189
|
+
if (body && body.ok === true) {
|
|
1190
|
+
window.location.replace(targetPath || '/');
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
} catch {}
|
|
1195
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
1196
|
+
}
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
button.addEventListener('click', async () => {
|
|
1201
|
+
button.disabled = true;
|
|
1202
|
+
setWaiting(true);
|
|
1203
|
+
setStatus('Starting Scout web...');
|
|
1204
|
+
try {
|
|
1205
|
+
const response = await fetch(startUrl, {
|
|
1206
|
+
method: 'POST',
|
|
1207
|
+
headers: { accept: 'application/json' },
|
|
1208
|
+
});
|
|
1209
|
+
const contentType = response.headers.get('content-type') || '';
|
|
1210
|
+
if (!contentType.includes('application/json')) {
|
|
1211
|
+
throw new Error('Scout broker is not reachable yet.');
|
|
1212
|
+
}
|
|
1213
|
+
const body = await response.json().catch(() => ({}));
|
|
1214
|
+
if (!response.ok || body.error) {
|
|
1215
|
+
throw new Error(body.error || 'Scout web did not start.');
|
|
1216
|
+
}
|
|
1217
|
+
setStatus('Waiting for the web app...');
|
|
1218
|
+
const ready = await waitForWeb();
|
|
1219
|
+
if (!ready) {
|
|
1220
|
+
setWaiting(false);
|
|
1221
|
+
setStatus('Scout web did not become ready. Try again in a moment.');
|
|
1222
|
+
button.disabled = false;
|
|
1223
|
+
}
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
setWaiting(false);
|
|
1226
|
+
setStatus(error instanceof Error ? error.message : String(error));
|
|
1227
|
+
button.disabled = false;
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
</script>
|
|
1231
|
+
</body>
|
|
1232
|
+
</html>`;
|
|
1233
|
+
}
|
|
1234
|
+
function renderOpenScoutCaddyfile(config) {
|
|
1235
|
+
const schemes = config.scheme === "both" ? ["http", "https"] : [config.scheme];
|
|
1236
|
+
const startPage = renderOpenScoutStartPage(config);
|
|
1237
|
+
const blocks = schemes.flatMap((scheme) => config.routes.map((route) => {
|
|
1238
|
+
const host = scheme === "http" ? `http://${route.host}` : route.host;
|
|
1239
|
+
return `${host} {
|
|
1240
|
+
` + (scheme === "https" ? ` tls internal
|
|
1241
|
+
` : "") + ` handle /__openscout/web/start {
|
|
1242
|
+
` + ` rewrite * /v1/web/start
|
|
1243
|
+
` + ` reverse_proxy ${config.brokerUpstream}
|
|
1244
|
+
` + ` }
|
|
1245
|
+
` + ` handle /__openscout/web/status {
|
|
1246
|
+
` + ` rewrite * /v1/web/status
|
|
1247
|
+
` + ` reverse_proxy ${config.brokerUpstream}
|
|
1248
|
+
` + ` }
|
|
1249
|
+
` + ` handle {
|
|
1250
|
+
` + ` reverse_proxy ${route.upstream} {
|
|
1251
|
+
` + ` lb_try_duration 1s
|
|
1252
|
+
` + ` lb_try_interval 250ms
|
|
1253
|
+
` + ` }
|
|
1254
|
+
` + ` }
|
|
1255
|
+
` + ` handle_errors {
|
|
1256
|
+
` + ` header Content-Type "text/html; charset=utf-8"
|
|
1257
|
+
` + ` respond <<HTML
|
|
1258
|
+
` + `${startPage}
|
|
1259
|
+
` + `HTML 200
|
|
1260
|
+
` + ` }
|
|
1261
|
+
` + `}`;
|
|
1262
|
+
})).join(`
|
|
1263
|
+
|
|
1264
|
+
`);
|
|
1265
|
+
return `${blocks}
|
|
1266
|
+
`;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// packages/runtime/src/base-daemon.ts
|
|
1270
|
+
var RESTART_MIN_DELAY_MS = 1000;
|
|
1271
|
+
var RESTART_MAX_DELAY_MS = 30000;
|
|
1272
|
+
var BROKER_HEALTH_TIMEOUT_MS = 30000;
|
|
1273
|
+
var BROKER_HEALTH_POLL_MS = 250;
|
|
1274
|
+
var MENU_BUNDLE_ID = "com.openscout.menu";
|
|
1275
|
+
var MENU_PROCESS_NAME = "OpenScoutMenu";
|
|
1276
|
+
var shuttingDown = false;
|
|
1277
|
+
var brokerProcess = null;
|
|
1278
|
+
var caddyProcess = null;
|
|
1279
|
+
var mdnsProcesses = [];
|
|
1280
|
+
var brokerRestartDelayMs = RESTART_MIN_DELAY_MS;
|
|
1281
|
+
var edgeRestartDelayMs = RESTART_MIN_DELAY_MS;
|
|
1282
|
+
var supervisedWebPid = null;
|
|
1283
|
+
var supervisorKeepAlive = null;
|
|
1284
|
+
var config = resolveBrokerServiceConfig();
|
|
1285
|
+
function log(message, details) {
|
|
1286
|
+
if (details === undefined) {
|
|
1287
|
+
console.log(`[openscout-base] ${message}`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
console.log(`[openscout-base] ${message}`, details);
|
|
1291
|
+
}
|
|
1292
|
+
function warn(message, details) {
|
|
1293
|
+
if (details === undefined) {
|
|
1294
|
+
console.warn(`[openscout-base] ${message}`);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
console.warn(`[openscout-base] ${message}`, details);
|
|
1298
|
+
}
|
|
1299
|
+
function ensureDirectory(path) {
|
|
1300
|
+
mkdirSync4(path, { recursive: true });
|
|
1301
|
+
}
|
|
1302
|
+
function logFile(name) {
|
|
1303
|
+
const dir = join5(config.supportDirectory, "logs", "base");
|
|
1304
|
+
ensureDirectory(dir);
|
|
1305
|
+
return openSync(join5(dir, name), "a");
|
|
1306
|
+
}
|
|
1307
|
+
function runtimeEntrypoint(config2) {
|
|
1308
|
+
return join5(config2.runtimePackageDir, "bin", "openscout-runtime.mjs");
|
|
1309
|
+
}
|
|
1310
|
+
function spawnBroker() {
|
|
1311
|
+
if (shuttingDown || brokerProcess) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const stdout = logFile("broker.stdout.log");
|
|
1315
|
+
const stderr = logFile("broker.stderr.log");
|
|
1316
|
+
brokerProcess = spawn(config.bunExecutable, [
|
|
1317
|
+
"run",
|
|
1318
|
+
runtimeEntrypoint(config),
|
|
1319
|
+
"broker"
|
|
1320
|
+
], {
|
|
1321
|
+
cwd: config.runtimePackageDir,
|
|
1322
|
+
env: {
|
|
1323
|
+
...process.env,
|
|
1324
|
+
OPENSCOUT_PARENT_PID: String(process.pid),
|
|
1325
|
+
OPENSCOUT_BROKER_HOST: config.brokerHost,
|
|
1326
|
+
OPENSCOUT_BROKER_PORT: String(config.brokerPort),
|
|
1327
|
+
OPENSCOUT_BROKER_URL: config.brokerUrl,
|
|
1328
|
+
OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
|
|
1329
|
+
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
1330
|
+
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
|
|
1331
|
+
},
|
|
1332
|
+
stdio: ["ignore", stdout, stderr]
|
|
1333
|
+
});
|
|
1334
|
+
log("broker started", { pid: brokerProcess.pid, url: config.brokerUrl });
|
|
1335
|
+
brokerProcess.once("exit", (code, signal) => {
|
|
1336
|
+
log("broker exited", { code, signal });
|
|
1337
|
+
brokerProcess = null;
|
|
1338
|
+
supervisedWebPid = null;
|
|
1339
|
+
if (!shuttingDown) {
|
|
1340
|
+
scheduleBrokerRestart();
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
function scheduleBrokerRestart() {
|
|
1345
|
+
const delay = brokerRestartDelayMs;
|
|
1346
|
+
brokerRestartDelayMs = Math.min(brokerRestartDelayMs * 2, RESTART_MAX_DELAY_MS);
|
|
1347
|
+
setTimeout(() => {
|
|
1348
|
+
if (!shuttingDown) {
|
|
1349
|
+
spawnBroker();
|
|
1350
|
+
startWebWhenBrokerIsReady();
|
|
1351
|
+
}
|
|
1352
|
+
}, delay).unref();
|
|
1353
|
+
}
|
|
1354
|
+
async function waitForBrokerHealth(timeoutMs = BROKER_HEALTH_TIMEOUT_MS) {
|
|
1355
|
+
const deadline = Date.now() + timeoutMs;
|
|
1356
|
+
while (Date.now() < deadline) {
|
|
1357
|
+
try {
|
|
1358
|
+
const response = await fetch(new URL("/health", config.brokerUrl), {
|
|
1359
|
+
headers: { accept: "application/json" },
|
|
1360
|
+
signal: AbortSignal.timeout(1000)
|
|
1361
|
+
});
|
|
1362
|
+
if (response.ok) {
|
|
1363
|
+
const body = await response.json();
|
|
1364
|
+
if (body.ok === true) {
|
|
1365
|
+
brokerRestartDelayMs = RESTART_MIN_DELAY_MS;
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
} catch {}
|
|
1370
|
+
await sleep2(BROKER_HEALTH_POLL_MS);
|
|
1371
|
+
}
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
function resolveEdgeScheme() {
|
|
1375
|
+
const value = process.env.OPENSCOUT_WEB_EDGE_SCHEME?.trim().toLowerCase();
|
|
1376
|
+
if (value === "http" || value === "https" || value === "both") {
|
|
1377
|
+
return value;
|
|
1378
|
+
}
|
|
1379
|
+
return "http";
|
|
1380
|
+
}
|
|
1381
|
+
function resolveEdgeConfig() {
|
|
1382
|
+
const portalHost = process.env.OPENSCOUT_WEB_PORTAL_HOST?.trim() || DEFAULT_SCOUT_WEB_PORTAL_HOST;
|
|
1383
|
+
const nodeHost = process.env.OPENSCOUT_WEB_ADVERTISED_HOST?.trim() || (process.env.OPENSCOUT_WEB_LOCAL_NAME?.trim() ? resolveScoutWebNamedHostname(process.env.OPENSCOUT_WEB_LOCAL_NAME) : resolveConfiguredScoutWebHostname());
|
|
1384
|
+
return resolveOpenScoutLocalEdgeConfig({
|
|
1385
|
+
portalHost,
|
|
1386
|
+
nodeHost,
|
|
1387
|
+
scheme: resolveEdgeScheme(),
|
|
1388
|
+
brokerPort: config.brokerPort,
|
|
1389
|
+
webPort: Number.parseInt(process.env.OPENSCOUT_WEB_PORT ?? "", 10) || resolveWebPort()
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
function resolveLocalEdgeCaddyfilePath() {
|
|
1393
|
+
const dir = join5(homedir5(), ".scout", "local-edge");
|
|
1394
|
+
ensureDirectory(dir);
|
|
1395
|
+
return join5(dir, "Caddyfile");
|
|
1396
|
+
}
|
|
1397
|
+
function resolveCaddyExecutable() {
|
|
1398
|
+
return process.env.OPENSCOUT_CADDY_BIN?.trim() || "caddy";
|
|
1399
|
+
}
|
|
1400
|
+
function spawnMdnsProxy(input) {
|
|
1401
|
+
return spawn("/usr/bin/dns-sd", [
|
|
1402
|
+
"-P",
|
|
1403
|
+
input.name,
|
|
1404
|
+
input.scheme === "https" ? "_https._tcp" : "_http._tcp",
|
|
1405
|
+
"local",
|
|
1406
|
+
String(input.port),
|
|
1407
|
+
input.host,
|
|
1408
|
+
"127.0.0.1",
|
|
1409
|
+
"path=/"
|
|
1410
|
+
], {
|
|
1411
|
+
stdio: ["ignore", logFile("mdns.stdout.log"), logFile("mdns.stderr.log")]
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
function stopEdgeProcesses() {
|
|
1415
|
+
for (const processRef of mdnsProcesses) {
|
|
1416
|
+
if (!processRef.killed) {
|
|
1417
|
+
processRef.kill("SIGTERM");
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
mdnsProcesses = [];
|
|
1421
|
+
if (caddyProcess && !caddyProcess.killed) {
|
|
1422
|
+
caddyProcess.kill("SIGTERM");
|
|
1423
|
+
}
|
|
1424
|
+
caddyProcess = null;
|
|
1425
|
+
}
|
|
1426
|
+
function startLocalEdge() {
|
|
1427
|
+
if (process.env.OPENSCOUT_BASE_EDGE_ENABLED === "0" || process.platform !== "darwin") {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (shuttingDown || caddyProcess) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const edgeConfig = resolveEdgeConfig();
|
|
1434
|
+
const schemes = edgeConfig.scheme === "both" ? ["http", "https"] : [edgeConfig.scheme];
|
|
1435
|
+
const caddyfilePath = resolveLocalEdgeCaddyfilePath();
|
|
1436
|
+
writeFileSync4(caddyfilePath, renderOpenScoutCaddyfile(edgeConfig), "utf8");
|
|
1437
|
+
mdnsProcesses = schemes.flatMap((scheme) => {
|
|
1438
|
+
const edgePort = scheme === "https" ? 443 : 80;
|
|
1439
|
+
const suffix = scheme.toUpperCase();
|
|
1440
|
+
return [
|
|
1441
|
+
spawnMdnsProxy({
|
|
1442
|
+
name: `Scout Local ${suffix}`,
|
|
1443
|
+
host: edgeConfig.portalHost,
|
|
1444
|
+
port: edgePort,
|
|
1445
|
+
scheme
|
|
1446
|
+
}),
|
|
1447
|
+
spawnMdnsProxy({
|
|
1448
|
+
name: `Scout ${edgeConfig.nodeHost} ${suffix}`,
|
|
1449
|
+
host: edgeConfig.nodeHost,
|
|
1450
|
+
port: edgePort,
|
|
1451
|
+
scheme
|
|
1452
|
+
})
|
|
1453
|
+
];
|
|
1454
|
+
});
|
|
1455
|
+
caddyProcess = spawn(resolveCaddyExecutable(), [
|
|
1456
|
+
"run",
|
|
1457
|
+
"--config",
|
|
1458
|
+
caddyfilePath,
|
|
1459
|
+
"--adapter",
|
|
1460
|
+
"caddyfile"
|
|
1461
|
+
], {
|
|
1462
|
+
env: process.env,
|
|
1463
|
+
stdio: ["ignore", logFile("edge.stdout.log"), logFile("edge.stderr.log")]
|
|
1464
|
+
});
|
|
1465
|
+
log("local edge started", {
|
|
1466
|
+
pid: caddyProcess.pid,
|
|
1467
|
+
portal: edgeConfig.portalHost,
|
|
1468
|
+
node: edgeConfig.nodeHost,
|
|
1469
|
+
caddyfile: caddyfilePath
|
|
1470
|
+
});
|
|
1471
|
+
caddyProcess.once("error", (error) => {
|
|
1472
|
+
stopEdgeProcesses();
|
|
1473
|
+
warn("local edge failed to start", error.message);
|
|
1474
|
+
scheduleEdgeRestart();
|
|
1475
|
+
});
|
|
1476
|
+
caddyProcess.once("exit", (code, signal) => {
|
|
1477
|
+
log("local edge exited", { code, signal });
|
|
1478
|
+
stopEdgeProcesses();
|
|
1479
|
+
if (!shuttingDown) {
|
|
1480
|
+
scheduleEdgeRestart();
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
function scheduleEdgeRestart() {
|
|
1485
|
+
const delay = edgeRestartDelayMs;
|
|
1486
|
+
edgeRestartDelayMs = Math.min(edgeRestartDelayMs * 2, RESTART_MAX_DELAY_MS);
|
|
1487
|
+
setTimeout(() => {
|
|
1488
|
+
if (!shuttingDown) {
|
|
1489
|
+
startLocalEdge();
|
|
1490
|
+
}
|
|
1491
|
+
}, delay).unref();
|
|
1492
|
+
}
|
|
1493
|
+
async function startWebWhenBrokerIsReady() {
|
|
1494
|
+
if (process.env.OPENSCOUT_BASE_START_WEB === "0") {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (!await waitForBrokerHealth()) {
|
|
1498
|
+
warn("broker did not become healthy before web startup timeout");
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
try {
|
|
1502
|
+
const edgeConfig = resolveEdgeConfig();
|
|
1503
|
+
const scheme = edgeConfig.scheme === "https" ? "https" : "http";
|
|
1504
|
+
const response = await fetch(new URL("/v1/web/start", config.brokerUrl), {
|
|
1505
|
+
method: "POST",
|
|
1506
|
+
headers: {
|
|
1507
|
+
accept: "application/json",
|
|
1508
|
+
"x-forwarded-host": edgeConfig.portalHost,
|
|
1509
|
+
"x-forwarded-proto": scheme
|
|
1510
|
+
},
|
|
1511
|
+
signal: AbortSignal.timeout(15000)
|
|
1512
|
+
});
|
|
1513
|
+
const body = await response.json();
|
|
1514
|
+
supervisedWebPid = typeof body.pid === "number" ? body.pid : resolvePortListenerPid(Number.parseInt(process.env.OPENSCOUT_WEB_PORT ?? "", 10) || resolveWebPort());
|
|
1515
|
+
if (!response.ok || body.ok !== true) {
|
|
1516
|
+
warn("web server did not report healthy", body.error ?? response.statusText);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
log("web server ready", { pid: supervisedWebPid });
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
warn("web server startup failed", error instanceof Error ? error.message : String(error));
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function resolvePortListenerPid(port) {
|
|
1525
|
+
const result = spawnSync2("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
1526
|
+
encoding: "utf8"
|
|
1527
|
+
});
|
|
1528
|
+
if ((result.status ?? 1) !== 0) {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
const pid = Number.parseInt(result.stdout.trim().split(/\s+/)[0] ?? "", 10);
|
|
1532
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
1533
|
+
}
|
|
1534
|
+
function findRepoMenuBundle() {
|
|
1535
|
+
let current = dirname4(fileURLToPath2(import.meta.url));
|
|
1536
|
+
while (true) {
|
|
1537
|
+
const candidate = resolve3(current, "apps", "macos", "dist", "OpenScoutMenu.app");
|
|
1538
|
+
if (existsSync5(candidate)) {
|
|
1539
|
+
return candidate;
|
|
1540
|
+
}
|
|
1541
|
+
const parent = dirname4(current);
|
|
1542
|
+
if (parent === current) {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
current = parent;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function startMenuBarApp() {
|
|
1549
|
+
if (process.env.OPENSCOUT_BASE_MENU_ENABLED === "0" || process.platform !== "darwin") {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const explicitBundle = process.env.OPENSCOUT_MENU_BUNDLE_PATH?.trim();
|
|
1553
|
+
const repoBundle = explicitBundle && existsSync5(explicitBundle) ? explicitBundle : findRepoMenuBundle();
|
|
1554
|
+
const args = repoBundle ? [repoBundle] : ["-b", MENU_BUNDLE_ID];
|
|
1555
|
+
const child = spawn("open", args, {
|
|
1556
|
+
stdio: ["ignore", logFile("menu.stdout.log"), logFile("menu.stderr.log")]
|
|
1557
|
+
});
|
|
1558
|
+
child.once("exit", (code) => {
|
|
1559
|
+
if (code === 0) {
|
|
1560
|
+
log("menu bar app launch requested", { target: repoBundle ?? MENU_BUNDLE_ID });
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
warn("menu bar app launch failed", { target: repoBundle ?? MENU_BUNDLE_ID, code });
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
function stopMenuBarApp() {
|
|
1567
|
+
if (process.platform !== "darwin" || process.env.OPENSCOUT_BASE_MENU_ENABLED === "0") {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
spawn("pkill", ["-x", MENU_PROCESS_NAME], { stdio: "ignore" }).unref();
|
|
1571
|
+
}
|
|
1572
|
+
function stopSupervisedWeb() {
|
|
1573
|
+
if (supervisedWebPid && supervisedWebPid > 0) {
|
|
1574
|
+
try {
|
|
1575
|
+
process.kill(supervisedWebPid, "SIGTERM");
|
|
1576
|
+
} catch {}
|
|
1577
|
+
}
|
|
1578
|
+
supervisedWebPid = null;
|
|
1579
|
+
}
|
|
1580
|
+
async function shutdown(exitCode = 0) {
|
|
1581
|
+
if (shuttingDown) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
shuttingDown = true;
|
|
1585
|
+
if (supervisorKeepAlive) {
|
|
1586
|
+
clearInterval(supervisorKeepAlive);
|
|
1587
|
+
supervisorKeepAlive = null;
|
|
1588
|
+
}
|
|
1589
|
+
stopSupervisedWeb();
|
|
1590
|
+
stopMenuBarApp();
|
|
1591
|
+
stopEdgeProcesses();
|
|
1592
|
+
if (brokerProcess && !brokerProcess.killed) {
|
|
1593
|
+
brokerProcess.kill("SIGTERM");
|
|
1594
|
+
}
|
|
1595
|
+
await sleep2(500);
|
|
1596
|
+
process.exit(exitCode);
|
|
1597
|
+
}
|
|
1598
|
+
function sleep2(ms) {
|
|
1599
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
1600
|
+
}
|
|
1601
|
+
function keepSupervisorAlive() {
|
|
1602
|
+
if (supervisorKeepAlive) {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
supervisorKeepAlive = setInterval(() => {
|
|
1606
|
+
return;
|
|
1607
|
+
}, 60000);
|
|
1608
|
+
}
|
|
1609
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1610
|
+
process.on(signal, () => {
|
|
1611
|
+
shutdown(0).catch((error) => {
|
|
1612
|
+
warn("shutdown failed", error instanceof Error ? error.message : String(error));
|
|
1613
|
+
process.exit(1);
|
|
1614
|
+
});
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
log("starting Scout base service", {
|
|
1618
|
+
label: config.label,
|
|
1619
|
+
brokerUrl: config.brokerUrl,
|
|
1620
|
+
bootout: `launchctl bootout ${config.serviceTarget}`
|
|
1621
|
+
});
|
|
1622
|
+
keepSupervisorAlive();
|
|
1623
|
+
spawnBroker();
|
|
1624
|
+
startLocalEdge();
|
|
1625
|
+
startMenuBarApp();
|
|
1626
|
+
startWebWhenBrokerIsReady();
|