@pushpalsdev/cli 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pushpals-cli.js +443 -49
- package/package.json +1 -1
package/dist/pushpals-cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// ../../scripts/pushpals-cli.ts
|
|
5
|
-
import { chmodSync, cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
5
|
+
import { appendFileSync, chmodSync, cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
6
6
|
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
7
7
|
import { createInterface } from "readline";
|
|
8
8
|
|
|
@@ -742,11 +742,14 @@ function loadPushPalsConfig(options = {}) {
|
|
|
742
742
|
// ../../scripts/pushpals-cli.ts
|
|
743
743
|
var DEFAULT_MONITOR_PORT = 8081;
|
|
744
744
|
var MONITOR_SCAN_PORTS = 32;
|
|
745
|
+
var MONITOR_POLL_MS = 2000;
|
|
745
746
|
var HTTP_TIMEOUT_MS = 2500;
|
|
746
747
|
var LOCALBUDDY_TIMEOUT_MS = 4000;
|
|
747
748
|
var SSE_RECONNECT_MS = 1500;
|
|
748
749
|
var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
|
|
749
750
|
var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
|
|
751
|
+
var DEFAULT_SERVER_BOOT_TIMEOUT_MS = 20000;
|
|
752
|
+
var DEFAULT_SERVICE_STABILITY_GRACE_MS = 4000;
|
|
750
753
|
var GITHUB_OWNER = "PushPalsDev";
|
|
751
754
|
var GITHUB_REPO = "pushpals";
|
|
752
755
|
var GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`;
|
|
@@ -756,6 +759,16 @@ var GITHUB_HEADERS = {
|
|
|
756
759
|
"User-Agent": "pushpals-cli"
|
|
757
760
|
};
|
|
758
761
|
var stateVersion = 1;
|
|
762
|
+
function logCliInvocation(argv) {
|
|
763
|
+
const startedAt = new Date().toISOString();
|
|
764
|
+
const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
|
|
765
|
+
const argsText = argv.length > 0 ? argv.join(" ") : "(none)";
|
|
766
|
+
console.log(`[pushpals] invocation=${startedAt}`);
|
|
767
|
+
console.log(`[pushpals] version=${cliVersion} runtime=bun@${Bun.version}`);
|
|
768
|
+
console.log(`[pushpals] platform=${process.platform}/${process.arch}`);
|
|
769
|
+
console.log(`[pushpals] cwd=${process.cwd()}`);
|
|
770
|
+
console.log(`[pushpals] args=${argsText}`);
|
|
771
|
+
}
|
|
759
772
|
function printUsage() {
|
|
760
773
|
console.log("PushPals CLI");
|
|
761
774
|
console.log("");
|
|
@@ -847,6 +860,9 @@ function normalizePath(value) {
|
|
|
847
860
|
return normalized.toLowerCase();
|
|
848
861
|
return normalized;
|
|
849
862
|
}
|
|
863
|
+
function jsonHtmlBootstrap(value) {
|
|
864
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
865
|
+
}
|
|
850
866
|
async function runGit(args, cwd) {
|
|
851
867
|
const proc = Bun.spawn(["git", ...args], {
|
|
852
868
|
cwd,
|
|
@@ -909,9 +925,16 @@ async function resolveRuntimeReleaseTag(explicitTag) {
|
|
|
909
925
|
if (fromEnv)
|
|
910
926
|
return fromEnv;
|
|
911
927
|
const packageVersion = parseSemverFromPackageVersion(process.env.PUSHPALS_CLI_PACKAGE_VERSION);
|
|
912
|
-
|
|
913
|
-
return
|
|
914
|
-
|
|
928
|
+
try {
|
|
929
|
+
return await fetchLatestReleaseTag();
|
|
930
|
+
} catch (err) {
|
|
931
|
+
if (packageVersion) {
|
|
932
|
+
const fallbackTag = `v${packageVersion}`;
|
|
933
|
+
console.warn(`[pushpals] Could not resolve latest runtime tag; falling back to package version tag ${fallbackTag}: ${String(err)}`);
|
|
934
|
+
return fallbackTag;
|
|
935
|
+
}
|
|
936
|
+
throw err;
|
|
937
|
+
}
|
|
915
938
|
}
|
|
916
939
|
function writeTextFileIfMissing(pathValue, text) {
|
|
917
940
|
if (existsSync2(pathValue))
|
|
@@ -940,7 +963,7 @@ async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
|
|
|
940
963
|
throw new Error(`Failed to fetch runtime source tree for ${tag} (HTTP ${treeResponse.status})`);
|
|
941
964
|
}
|
|
942
965
|
const treePayload = await treeResponse.json();
|
|
943
|
-
const paths = (treePayload.tree ?? []).filter((entry) => entry.type === "blob" && typeof entry.path === "string").map((entry) => String(entry.path)).filter((pathValue) => pathValue === ".env.example" || pathValue.startsWith("configs/") || pathValue.startsWith("prompts/"));
|
|
966
|
+
const paths = (treePayload.tree ?? []).filter((entry) => entry.type === "blob" && typeof entry.path === "string").map((entry) => String(entry.path)).filter((pathValue) => pathValue === ".env.example" || pathValue.startsWith("configs/") || pathValue.startsWith("prompts/") || pathValue.startsWith("packages/protocol/src/schemas/"));
|
|
944
967
|
if (paths.length === 0) {
|
|
945
968
|
throw new Error(`Runtime source tree for ${tag} did not include prompts/config assets`);
|
|
946
969
|
}
|
|
@@ -948,7 +971,7 @@ async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
|
|
|
948
971
|
for (const pathValue of sorted) {
|
|
949
972
|
const rawUrl = `https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/${encodeURIComponent(tag)}/${pathValue}`;
|
|
950
973
|
const body = await fetchTextFromUrl(rawUrl, 20000);
|
|
951
|
-
const outPath = join2(runtimeRoot, pathValue);
|
|
974
|
+
const outPath = pathValue.startsWith("packages/protocol/src/schemas/") ? join2(runtimeRoot, "protocol", "schemas", pathValue.slice("packages/protocol/src/schemas/".length)) : join2(runtimeRoot, pathValue);
|
|
952
975
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
953
976
|
writeFileSync(outPath, body, "utf8");
|
|
954
977
|
}
|
|
@@ -956,10 +979,14 @@ async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
|
|
|
956
979
|
async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
|
|
957
980
|
const markerPath = join2(runtimeRoot, ".runtime-assets-tag");
|
|
958
981
|
const currentTag = existsSync2(markerPath) ? readFileSync2(markerPath, "utf8").trim() : "";
|
|
959
|
-
const
|
|
982
|
+
const protocolSchemasDir = join2(runtimeRoot, "protocol", "schemas");
|
|
983
|
+
const hasProtocolSchemas = existsSync2(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync2(join2(protocolSchemasDir, "events.schema.json"));
|
|
984
|
+
const hasAssets = existsSync2(join2(runtimeRoot, ".env.example")) && existsSync2(join2(runtimeRoot, "configs", "default.toml")) && existsSync2(join2(runtimeRoot, "prompts")) && hasProtocolSchemas;
|
|
960
985
|
if (!hasAssets || currentTag !== runtimeTag) {
|
|
961
|
-
|
|
962
|
-
|
|
986
|
+
copyBundledRuntimeAssets(runtimeRoot);
|
|
987
|
+
const hasProtocolSchemasAfterCopy = existsSync2(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync2(join2(protocolSchemasDir, "events.schema.json"));
|
|
988
|
+
const hasAssetsAfterCopy = existsSync2(join2(runtimeRoot, ".env.example")) && existsSync2(join2(runtimeRoot, "configs", "default.toml")) && existsSync2(join2(runtimeRoot, "prompts")) && hasProtocolSchemasAfterCopy;
|
|
989
|
+
if (!hasAssetsAfterCopy) {
|
|
963
990
|
await downloadRuntimeAssetsFromSourceTag(runtimeRoot, runtimeTag);
|
|
964
991
|
}
|
|
965
992
|
writeFileSync(markerPath, `${runtimeTag}
|
|
@@ -980,6 +1007,30 @@ function runtimeBinaryFilename(serviceName, platformKey) {
|
|
|
980
1007
|
const extension = platformKey.startsWith("windows-") ? ".exe" : "";
|
|
981
1008
|
return `pushpals-runtime-${serviceToken}-${platformKey}${extension}`;
|
|
982
1009
|
}
|
|
1010
|
+
function buildEmbeddedRuntimeEnv(baseEnv, opts) {
|
|
1011
|
+
return {
|
|
1012
|
+
...baseEnv,
|
|
1013
|
+
PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
|
|
1014
|
+
PUSHPALS_PROJECT_ROOT_OVERRIDE: opts.repoRoot,
|
|
1015
|
+
PUSHPALS_CONFIG_DIR_OVERRIDE: join2(opts.runtimeRoot, "configs"),
|
|
1016
|
+
PUSHPALS_PROMPTS_ROOT_OVERRIDE: opts.runtimeRoot,
|
|
1017
|
+
PUSHPALS_PROTOCOL_SCHEMAS_DIR: join2(opts.runtimeRoot, "protocol", "schemas"),
|
|
1018
|
+
REMOTEBUDDY_AUTONOMY_ENABLED: String(baseEnv.REMOTEBUDDY_AUTONOMY_ENABLED ?? "").trim() || "false"
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
function timestampFileToken() {
|
|
1022
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
1023
|
+
}
|
|
1024
|
+
function readLogTail(logPath, maxLines = 40) {
|
|
1025
|
+
if (!existsSync2(logPath))
|
|
1026
|
+
return "";
|
|
1027
|
+
const raw = readFileSync2(logPath, "utf8");
|
|
1028
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.length > 0);
|
|
1029
|
+
if (lines.length === 0)
|
|
1030
|
+
return "";
|
|
1031
|
+
return lines.slice(-maxLines).join(`
|
|
1032
|
+
`);
|
|
1033
|
+
}
|
|
983
1034
|
async function downloadBinaryAsset(tag, assetName, outPath) {
|
|
984
1035
|
const url = `${GITHUB_RELEASE_URL}/${encodeURIComponent(tag)}/${assetName}`;
|
|
985
1036
|
const response = await fetchWithTimeout(url, { headers: GITHUB_HEADERS }, 60000);
|
|
@@ -1021,16 +1072,50 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
|
|
|
1021
1072
|
}
|
|
1022
1073
|
return runtimeBinaries;
|
|
1023
1074
|
}
|
|
1024
|
-
function spawnRuntimeService(name, command, cwd, env) {
|
|
1075
|
+
function spawnRuntimeService(name, command, cwd, env, logPath) {
|
|
1076
|
+
writeFileSync(logPath, `[pushpals] service=${name} command=${command.join(" ")} cwd=${cwd}
|
|
1077
|
+
`, "utf8");
|
|
1025
1078
|
const proc = Bun.spawn(command, {
|
|
1026
1079
|
cwd,
|
|
1027
1080
|
env,
|
|
1028
|
-
stdout: "
|
|
1029
|
-
stderr: "
|
|
1081
|
+
stdout: "pipe",
|
|
1082
|
+
stderr: "pipe"
|
|
1030
1083
|
});
|
|
1084
|
+
const pipeToLog = async (stream, channel) => {
|
|
1085
|
+
if (!stream)
|
|
1086
|
+
return;
|
|
1087
|
+
const reader = stream.getReader();
|
|
1088
|
+
const decoder = new TextDecoder;
|
|
1089
|
+
let pending = "";
|
|
1090
|
+
while (true) {
|
|
1091
|
+
const { done, value } = await reader.read();
|
|
1092
|
+
if (done)
|
|
1093
|
+
break;
|
|
1094
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1095
|
+
if (!chunk)
|
|
1096
|
+
continue;
|
|
1097
|
+
pending += chunk;
|
|
1098
|
+
const lines = pending.split(/\r?\n/);
|
|
1099
|
+
pending = lines.pop() ?? "";
|
|
1100
|
+
for (const line of lines) {
|
|
1101
|
+
appendFileSync(logPath, `[${channel}] ${line}
|
|
1102
|
+
`, "utf8");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const rest = decoder.decode();
|
|
1106
|
+
if (rest)
|
|
1107
|
+
pending += rest;
|
|
1108
|
+
if (pending.trim().length > 0) {
|
|
1109
|
+
appendFileSync(logPath, `[${channel}] ${pending.trimEnd()}
|
|
1110
|
+
`, "utf8");
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
pipeToLog(proc.stdout, "stdout");
|
|
1114
|
+
pipeToLog(proc.stderr, "stderr");
|
|
1031
1115
|
const service = {
|
|
1032
1116
|
name,
|
|
1033
1117
|
proc,
|
|
1118
|
+
logPath,
|
|
1034
1119
|
exited: false,
|
|
1035
1120
|
exitCode: null
|
|
1036
1121
|
};
|
|
@@ -1043,10 +1128,25 @@ function spawnRuntimeService(name, command, cwd, env) {
|
|
|
1043
1128
|
function stopRuntimeServices(services) {
|
|
1044
1129
|
for (const service of services) {
|
|
1045
1130
|
try {
|
|
1046
|
-
service.proc.
|
|
1131
|
+
if (process.platform === "win32" && typeof service.proc.pid === "number" && service.proc.pid > 0) {
|
|
1132
|
+
Bun.spawnSync(["taskkill", "/PID", String(service.proc.pid), "/T", "/F"], {
|
|
1133
|
+
stdin: "ignore",
|
|
1134
|
+
stdout: "ignore",
|
|
1135
|
+
stderr: "ignore"
|
|
1136
|
+
});
|
|
1137
|
+
} else {
|
|
1138
|
+
service.proc.kill();
|
|
1139
|
+
}
|
|
1047
1140
|
} catch {}
|
|
1048
1141
|
}
|
|
1049
1142
|
}
|
|
1143
|
+
async function repoHasRemote(repoRoot, remote) {
|
|
1144
|
+
const normalizedRemote = remote.trim();
|
|
1145
|
+
if (!normalizedRemote)
|
|
1146
|
+
return false;
|
|
1147
|
+
const result = await runGit(["remote", "get-url", normalizedRemote], repoRoot);
|
|
1148
|
+
return result.ok && Boolean(result.stdout);
|
|
1149
|
+
}
|
|
1050
1150
|
async function probeServer(serverUrl) {
|
|
1051
1151
|
try {
|
|
1052
1152
|
const response = await fetchWithTimeout(`${serverUrl}/healthz`, {}, HTTP_TIMEOUT_MS);
|
|
@@ -1100,29 +1200,65 @@ async function autoStartRuntimeServices(opts) {
|
|
|
1100
1200
|
console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
|
|
1101
1201
|
console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
|
|
1102
1202
|
console.log(`[pushpals] runtimeTag=${runtimeTag}`);
|
|
1103
|
-
const runtimeEnv = {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
PUSHPALS_CONFIG_DIR_OVERRIDE: join2(runtimeRoot, "configs"),
|
|
1108
|
-
PUSHPALS_PROMPTS_ROOT_OVERRIDE: runtimeRoot
|
|
1109
|
-
};
|
|
1203
|
+
const runtimeEnv = buildEmbeddedRuntimeEnv(process.env, {
|
|
1204
|
+
repoRoot: opts.repoRoot,
|
|
1205
|
+
runtimeRoot
|
|
1206
|
+
});
|
|
1110
1207
|
const services = [];
|
|
1208
|
+
const runToken = timestampFileToken();
|
|
1209
|
+
const logDir = join2(runtimeRoot, "logs", "bootstrap");
|
|
1210
|
+
mkdirSync(logDir, { recursive: true });
|
|
1211
|
+
const logPathFor = (name) => join2(logDir, `${runToken}-${name}.log`);
|
|
1111
1212
|
const serverHealthy = await probeServer(opts.serverUrl);
|
|
1112
1213
|
if (!serverHealthy) {
|
|
1113
1214
|
console.log("[pushpals] Starting embedded server...");
|
|
1114
|
-
|
|
1215
|
+
const serverService = spawnRuntimeService("server", [runtimeBinaries.server], opts.repoRoot, runtimeEnv, logPathFor("server"));
|
|
1216
|
+
services.push(serverService);
|
|
1217
|
+
console.log(`[pushpals] server log: ${serverService.logPath}`);
|
|
1218
|
+
const serverDeadline = Date.now() + DEFAULT_SERVER_BOOT_TIMEOUT_MS;
|
|
1219
|
+
let serverIsReady = false;
|
|
1220
|
+
while (Date.now() < serverDeadline) {
|
|
1221
|
+
if (serverService.exited) {
|
|
1222
|
+
const tail = readLogTail(serverService.logPath);
|
|
1223
|
+
stopRuntimeServices(services);
|
|
1224
|
+
throw new Error(`Embedded server exited during bootstrap (code=${serverService.exitCode ?? "unknown"}). ` + `See ${serverService.logPath}${tail ? `
|
|
1225
|
+
--- server log tail ---
|
|
1226
|
+
${tail}` : ""}`);
|
|
1227
|
+
}
|
|
1228
|
+
if (await probeServer(opts.serverUrl)) {
|
|
1229
|
+
serverIsReady = true;
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
|
|
1233
|
+
}
|
|
1234
|
+
if (!serverIsReady) {
|
|
1235
|
+
const tail = readLogTail(serverService.logPath);
|
|
1236
|
+
stopRuntimeServices(services);
|
|
1237
|
+
throw new Error(`Embedded server did not become healthy within ${DEFAULT_SERVER_BOOT_TIMEOUT_MS}ms. ` + `See ${serverService.logPath}${tail ? `
|
|
1238
|
+
--- server log tail ---
|
|
1239
|
+
${tail}` : ""}`);
|
|
1240
|
+
}
|
|
1241
|
+
console.log("[pushpals] Embedded server is healthy.");
|
|
1115
1242
|
} else {
|
|
1116
1243
|
console.log("[pushpals] Server already healthy; skipping embedded server start.");
|
|
1117
1244
|
}
|
|
1118
1245
|
console.log("[pushpals] Starting embedded LocalBuddy...");
|
|
1119
|
-
|
|
1246
|
+
const localbuddyService = spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv, logPathFor("localbuddy"));
|
|
1247
|
+
services.push(localbuddyService);
|
|
1248
|
+
console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath}`);
|
|
1120
1249
|
console.log("[pushpals] Starting embedded RemoteBuddy...");
|
|
1121
|
-
|
|
1250
|
+
const remotebuddyService = spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv, logPathFor("remotebuddy"));
|
|
1251
|
+
services.push(remotebuddyService);
|
|
1252
|
+
console.log(`[pushpals] remotebuddy log: ${remotebuddyService.logPath}`);
|
|
1122
1253
|
const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
|
|
1123
|
-
|
|
1254
|
+
const scmRemoteAvailable = await repoHasRemote(opts.repoRoot, opts.sourceControlManagerRemote);
|
|
1255
|
+
if (!scmHealthy && scmRemoteAvailable) {
|
|
1124
1256
|
console.log("[pushpals] Starting embedded SourceControlManager...");
|
|
1125
|
-
|
|
1257
|
+
const sourceControlManagerService = spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv, logPathFor("source_control_manager"));
|
|
1258
|
+
services.push(sourceControlManagerService);
|
|
1259
|
+
console.log(`[pushpals] source_control_manager log: ${sourceControlManagerService.logPath}`);
|
|
1260
|
+
} else if (!scmRemoteAvailable) {
|
|
1261
|
+
console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
|
|
1126
1262
|
} else {
|
|
1127
1263
|
console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
|
|
1128
1264
|
}
|
|
@@ -1133,15 +1269,36 @@ async function autoStartRuntimeServices(opts) {
|
|
|
1133
1269
|
if (service.exited) {
|
|
1134
1270
|
if (service.name === "source_control_manager") {
|
|
1135
1271
|
console.warn(`[pushpals] Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}); continuing without SCM.`);
|
|
1272
|
+
const tail2 = readLogTail(service.logPath);
|
|
1273
|
+
if (tail2) {
|
|
1274
|
+
console.warn(`[pushpals] ${service.name} log tail:
|
|
1275
|
+
${tail2}`);
|
|
1276
|
+
}
|
|
1136
1277
|
services.splice(i, 1);
|
|
1137
1278
|
continue;
|
|
1138
1279
|
}
|
|
1280
|
+
const tail = readLogTail(service.logPath);
|
|
1139
1281
|
stopRuntimeServices(services);
|
|
1140
|
-
throw new Error(`Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"})`
|
|
1282
|
+
throw new Error(`Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}). ` + `See ${service.logPath}${tail ? `
|
|
1283
|
+
--- ${service.name} log tail ---
|
|
1284
|
+
${tail}` : ""}`);
|
|
1141
1285
|
}
|
|
1142
1286
|
}
|
|
1143
1287
|
const health = await probeLocalBuddy(opts.localAgentUrl, opts.authToken);
|
|
1144
1288
|
if (health?.ok) {
|
|
1289
|
+
const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
|
|
1290
|
+
while (Date.now() < stabilityDeadline) {
|
|
1291
|
+
for (const service of services) {
|
|
1292
|
+
if (!service.exited)
|
|
1293
|
+
continue;
|
|
1294
|
+
const tail = readLogTail(service.logPath);
|
|
1295
|
+
stopRuntimeServices(services);
|
|
1296
|
+
throw new Error(`Embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}). ` + `See ${service.logPath}${tail ? `
|
|
1297
|
+
--- ${service.name} log tail ---
|
|
1298
|
+
${tail}` : ""}`);
|
|
1299
|
+
}
|
|
1300
|
+
await Bun.sleep(250);
|
|
1301
|
+
}
|
|
1145
1302
|
console.log("[pushpals] Embedded runtime is ready.");
|
|
1146
1303
|
return services;
|
|
1147
1304
|
}
|
|
@@ -1195,17 +1352,221 @@ async function looksLikeMonitoringHub(url) {
|
|
|
1195
1352
|
return false;
|
|
1196
1353
|
}
|
|
1197
1354
|
}
|
|
1198
|
-
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1355
|
+
function buildEmbeddedMonitoringHubHtml(opts) {
|
|
1356
|
+
const bootstrap = jsonHtmlBootstrap(opts);
|
|
1357
|
+
return `<!doctype html>
|
|
1358
|
+
<html lang="en">
|
|
1359
|
+
<head>
|
|
1360
|
+
<meta charset="utf-8" />
|
|
1361
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1362
|
+
<title>PushPals CLI Monitor</title>
|
|
1363
|
+
<style>
|
|
1364
|
+
:root { color-scheme: dark; --bg:#08111b; --panel:#112235; --panel2:#16324a; --line:#2b5876; --fg:#edf6ff; --muted:#90b5d6; --accent:#58d8c3; --warn:#ffbf5f; --bad:#ff7f7f; }
|
|
1365
|
+
* { box-sizing:border-box; }
|
|
1366
|
+
body { margin:0; font-family:Consolas, "SFMono-Regular", monospace; background:radial-gradient(circle at top, #0d2233, var(--bg) 56%); color:var(--fg); }
|
|
1367
|
+
main { max-width:1200px; margin:0 auto; padding:24px; }
|
|
1368
|
+
h1,h2 { margin:0 0 12px; }
|
|
1369
|
+
p { color:var(--muted); }
|
|
1370
|
+
.row { display:grid; gap:16px; margin-top:16px; }
|
|
1371
|
+
.cards { grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); }
|
|
1372
|
+
.panels { grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); }
|
|
1373
|
+
.card, .panel { border:1px solid var(--line); background:linear-gradient(180deg,var(--panel),var(--panel2)); border-radius:16px; padding:16px; box-shadow:0 12px 40px rgba(0,0,0,.22); }
|
|
1374
|
+
.label { font-size:12px; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); margin-bottom:10px; }
|
|
1375
|
+
.value { font-size:32px; font-weight:700; color:var(--accent); }
|
|
1376
|
+
.sub { margin-top:8px; color:var(--muted); white-space:pre-wrap; word-break:break-word; }
|
|
1377
|
+
.list { display:grid; gap:10px; margin-top:12px; }
|
|
1378
|
+
.item { border:1px solid rgba(88,216,195,.18); border-radius:12px; padding:12px; background:rgba(8,17,27,.42); }
|
|
1379
|
+
.meta { display:flex; flex-wrap:wrap; gap:8px; margin:12px 0 0; }
|
|
1380
|
+
.pill { border:1px solid var(--line); border-radius:999px; padding:6px 10px; color:var(--muted); }
|
|
1381
|
+
a { color:var(--accent); }
|
|
1382
|
+
</style>
|
|
1383
|
+
</head>
|
|
1384
|
+
<body>
|
|
1385
|
+
<main>
|
|
1386
|
+
<h1>PushPals CLI Monitor</h1>
|
|
1387
|
+
<p>Lightweight embedded monitor for CLI-managed runtimes.</p>
|
|
1388
|
+
<div class="meta" id="meta"></div>
|
|
1389
|
+
<section class="row cards" id="cards"></section>
|
|
1390
|
+
<section class="row panels">
|
|
1391
|
+
<div class="panel">
|
|
1392
|
+
<h2>Requests</h2>
|
|
1393
|
+
<div id="requests" class="list"></div>
|
|
1394
|
+
</div>
|
|
1395
|
+
<div class="panel">
|
|
1396
|
+
<h2>Jobs</h2>
|
|
1397
|
+
<div id="jobs" class="list"></div>
|
|
1398
|
+
</div>
|
|
1399
|
+
<div class="panel">
|
|
1400
|
+
<h2>Completions</h2>
|
|
1401
|
+
<div id="completions" class="list"></div>
|
|
1402
|
+
</div>
|
|
1403
|
+
</section>
|
|
1404
|
+
</main>
|
|
1405
|
+
<script>
|
|
1406
|
+
const boot = ${bootstrap};
|
|
1407
|
+
const pollMs = ${MONITOR_POLL_MS};
|
|
1408
|
+
const metaEl = document.getElementById("meta");
|
|
1409
|
+
const cardsEl = document.getElementById("cards");
|
|
1410
|
+
const requestsEl = document.getElementById("requests");
|
|
1411
|
+
const jobsEl = document.getElementById("jobs");
|
|
1412
|
+
const completionsEl = document.getElementById("completions");
|
|
1413
|
+
|
|
1414
|
+
function esc(value) {
|
|
1415
|
+
return String(value ?? "")
|
|
1416
|
+
.replace(/&/g, "&")
|
|
1417
|
+
.replace(/</g, "<")
|
|
1418
|
+
.replace(/>/g, ">");
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
async function fetchJson(path) {
|
|
1422
|
+
const res = await fetch(path, { cache: "no-store" });
|
|
1423
|
+
if (!res.ok) throw new Error(path + " -> HTTP " + res.status);
|
|
1424
|
+
return await res.json();
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function setList(target, rows, emptyLabel, formatter) {
|
|
1428
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1429
|
+
target.innerHTML = '<div class="item">' + esc(emptyLabel) + "</div>";
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
target.innerHTML = rows.map((row) => '<div class="item">' + formatter(row) + "</div>").join("");
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function renderStatus(status) {
|
|
1436
|
+
const workers = status?.workers ?? {};
|
|
1437
|
+
const queues = status?.queues ?? {};
|
|
1438
|
+
const runtime = status?.runtime ?? {};
|
|
1439
|
+
const repo = status?.repo ?? {};
|
|
1440
|
+
const llmUsage = status?.llmUsage ?? {};
|
|
1441
|
+
const cards = [
|
|
1442
|
+
{ label: "Server uptime", value: Math.round((Number(runtime.uptimeMs ?? 0) / 60000)) + "m", sub: runtime.startedAt ?? "unknown" },
|
|
1443
|
+
{ label: "Workers online", value: String(workers.online ?? 0), sub: "busy " + String(workers.busy ?? 0) + " | idle " + String(workers.idle ?? 0) },
|
|
1444
|
+
{ label: "Pending requests", value: String(queues.requests?.pending ?? 0), sub: "claimed " + String(queues.requests?.claimed ?? 0) },
|
|
1445
|
+
{ label: "Pending jobs", value: String(queues.jobs?.pending ?? 0), sub: "claimed " + String(queues.jobs?.claimed ?? 0) },
|
|
1446
|
+
{ label: "Completions", value: String(queues.completions?.pending ?? 0), sub: "processed " + String(queues.completions?.processed ?? 0) },
|
|
1447
|
+
{ label: "LLM usage (24h)", value: String(llmUsage.totalTokens ?? 0), sub: "calls " + String(llmUsage.totalCalls ?? 0) }
|
|
1448
|
+
];
|
|
1449
|
+
cardsEl.innerHTML = cards.map((card) => '<div class="card"><div class="label">' + esc(card.label) + '</div><div class="value">' + esc(card.value) + '</div><div class="sub">' + esc(card.sub) + '</div></div>').join("");
|
|
1450
|
+
metaEl.innerHTML = [
|
|
1451
|
+
'<span class="pill">server ' + esc(boot.serverUrl) + '</span>',
|
|
1452
|
+
'<span class="pill">localbuddy ' + esc(boot.localAgentUrl) + '</span>',
|
|
1453
|
+
'<span class="pill">session ' + esc(boot.sessionId) + '</span>',
|
|
1454
|
+
'<span class="pill">repo ' + esc(repo?.root ?? repo?.remoteUrl ?? "current repo") + '</span>'
|
|
1455
|
+
].join("");
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function render() {
|
|
1459
|
+
Promise.all([
|
|
1460
|
+
fetchJson('/api/status'),
|
|
1461
|
+
fetchJson('/api/requests'),
|
|
1462
|
+
fetchJson('/api/jobs'),
|
|
1463
|
+
fetchJson('/api/completions')
|
|
1464
|
+
]).then(([status, requests, jobs, completions]) => {
|
|
1465
|
+
renderStatus(status);
|
|
1466
|
+
setList(requestsEl, requests?.requests?.slice(0, 8), 'No requests', (row) =>
|
|
1467
|
+
'<strong>' + esc(row?.priority ?? 'request') + '</strong><div class="sub">' +
|
|
1468
|
+
esc((row?.status ?? 'unknown') + ' | ' + (row?.id ?? '')) + '</div><div class="sub">' +
|
|
1469
|
+
esc(String(row?.prompt ?? '').slice(0, 220)) + '</div>');
|
|
1470
|
+
setList(jobsEl, jobs?.jobs?.slice(0, 8), 'No jobs', (row) =>
|
|
1471
|
+
'<strong>' + esc(row?.kind ?? 'job') + '</strong><div class="sub">' +
|
|
1472
|
+
esc((row?.status ?? 'unknown') + ' | worker ' + (row?.workerId ?? '--')) + '</div><div class="sub">' +
|
|
1473
|
+
esc((row?.summary ?? row?.error ?? row?.id ?? '').slice(0, 220)) + '</div>');
|
|
1474
|
+
setList(completionsEl, completions?.completions?.slice(0, 8), 'No completions', (row) =>
|
|
1475
|
+
'<strong>' + esc(row?.status ?? 'completion') + '</strong><div class="sub">' +
|
|
1476
|
+
esc((row?.jobId ?? '') + ' | ' + (row?.commitSha ?? '')) + '</div><div class="sub">' +
|
|
1477
|
+
esc((row?.message ?? '').slice(0, 220)) + '</div>');
|
|
1478
|
+
}).catch((err) => {
|
|
1479
|
+
cardsEl.innerHTML = '<div class="card"><div class="label">Monitor error</div><div class="sub">' + esc(err?.message ?? err) + '</div></div>';
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
render();
|
|
1484
|
+
setInterval(render, pollMs);
|
|
1485
|
+
</script>
|
|
1486
|
+
</body>
|
|
1487
|
+
</html>`;
|
|
1488
|
+
}
|
|
1489
|
+
async function proxyMonitoringHubRequest(serverUrl, authToken, pathValue) {
|
|
1490
|
+
const target = `${serverUrl}${pathValue}`;
|
|
1491
|
+
const upstream = await fetchWithTimeout(target, { headers: authHeaders(authToken) }, 1e4);
|
|
1492
|
+
const body = await upstream.text();
|
|
1493
|
+
return new Response(body, {
|
|
1494
|
+
status: upstream.status,
|
|
1495
|
+
headers: {
|
|
1496
|
+
"content-type": String(upstream.headers.get("content-type") ?? "application/json"),
|
|
1497
|
+
"cache-control": "no-store"
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
async function startEmbeddedMonitoringHub(opts) {
|
|
1502
|
+
const html = buildEmbeddedMonitoringHubHtml({
|
|
1503
|
+
serverUrl: opts.serverUrl,
|
|
1504
|
+
localAgentUrl: opts.localAgentUrl,
|
|
1505
|
+
sessionId: opts.sessionId
|
|
1506
|
+
});
|
|
1507
|
+
const candidatePorts = Array.from({ length: MONITOR_SCAN_PORTS }, (_, index) => opts.preferredPort + index).concat(0);
|
|
1508
|
+
for (const port of candidatePorts) {
|
|
1509
|
+
try {
|
|
1510
|
+
const server = Bun.serve({
|
|
1511
|
+
port,
|
|
1512
|
+
idleTimeout: 30,
|
|
1513
|
+
fetch: async (req) => {
|
|
1514
|
+
const url = new URL(req.url);
|
|
1515
|
+
if (url.pathname === "/") {
|
|
1516
|
+
return new Response(html, {
|
|
1517
|
+
headers: {
|
|
1518
|
+
"content-type": "text/html; charset=utf-8",
|
|
1519
|
+
"cache-control": "no-store"
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
if (url.pathname === "/healthz") {
|
|
1524
|
+
return Response.json({ ok: true, port, serverUrl: opts.serverUrl, sessionId: opts.sessionId });
|
|
1525
|
+
}
|
|
1526
|
+
if (url.pathname === "/api/status") {
|
|
1527
|
+
return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/system/status");
|
|
1528
|
+
}
|
|
1529
|
+
if (url.pathname === "/api/requests") {
|
|
1530
|
+
return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/requests?status=all&limit=20");
|
|
1531
|
+
}
|
|
1532
|
+
if (url.pathname === "/api/jobs") {
|
|
1533
|
+
return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/jobs?status=all&limit=20");
|
|
1534
|
+
}
|
|
1535
|
+
if (url.pathname === "/api/completions") {
|
|
1536
|
+
return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/completions?status=all&limit=20");
|
|
1537
|
+
}
|
|
1538
|
+
return new Response("Not found", { status: 404 });
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
return {
|
|
1542
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
1543
|
+
port: Number(server.port),
|
|
1544
|
+
embedded: true,
|
|
1545
|
+
stop: () => server.stop(true)
|
|
1546
|
+
};
|
|
1547
|
+
} catch {}
|
|
1548
|
+
}
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
async function resolveMonitoringHub(opts) {
|
|
1552
|
+
const explicit = normalizeUrl(opts.preferredUrl);
|
|
1553
|
+
if (explicit) {
|
|
1554
|
+
if (await looksLikeMonitoringHub(explicit)) {
|
|
1555
|
+
return { url: explicit, port: 0, stop: () => {}, embedded: false };
|
|
1556
|
+
}
|
|
1557
|
+
console.warn(`[pushpals] Preferred monitoring hub ${explicit} is unavailable; starting embedded monitor instead.`);
|
|
1207
1558
|
}
|
|
1208
|
-
|
|
1559
|
+
for (let port = opts.fallbackPort;port < opts.fallbackPort + MONITOR_SCAN_PORTS; port++) {
|
|
1560
|
+
const candidate = `http://127.0.0.1:${port}`;
|
|
1561
|
+
if (await looksLikeMonitoringHub(candidate)) {
|
|
1562
|
+
return { url: candidate, port, stop: () => {}, embedded: false };
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const embedded = await startEmbeddedMonitoringHub(opts);
|
|
1566
|
+
if (!embedded) {
|
|
1567
|
+
console.warn("[pushpals] Embedded monitoring hub could not start on any expected local port.");
|
|
1568
|
+
}
|
|
1569
|
+
return embedded;
|
|
1209
1570
|
}
|
|
1210
1571
|
async function sendMessageToLocalBuddy(localAgentUrl, text) {
|
|
1211
1572
|
let response;
|
|
@@ -1379,8 +1740,7 @@ async function runSessionStream(serverUrl, sessionId, authToken, print, signal)
|
|
|
1379
1740
|
async function openMonitoringHub(url) {
|
|
1380
1741
|
let cmd = null;
|
|
1381
1742
|
if (process.platform === "win32") {
|
|
1382
|
-
|
|
1383
|
-
cmd = ["powershell", "-NoProfile", "-Command", `Start-Process '${escaped}'`];
|
|
1743
|
+
cmd = ["cmd", "/c", "start", "", url];
|
|
1384
1744
|
} else if (process.platform === "darwin") {
|
|
1385
1745
|
cmd = ["open", url];
|
|
1386
1746
|
} else {
|
|
@@ -1395,7 +1755,9 @@ async function openMonitoringHub(url) {
|
|
|
1395
1755
|
return code === 0;
|
|
1396
1756
|
}
|
|
1397
1757
|
async function main() {
|
|
1398
|
-
const
|
|
1758
|
+
const argv = process.argv.slice(2);
|
|
1759
|
+
logCliInvocation(argv);
|
|
1760
|
+
const parsed = parseArgs(argv);
|
|
1399
1761
|
if (!parsed)
|
|
1400
1762
|
return;
|
|
1401
1763
|
const config = loadPushPalsConfig();
|
|
@@ -1426,6 +1788,7 @@ async function main() {
|
|
|
1426
1788
|
serverUrl,
|
|
1427
1789
|
localAgentUrl,
|
|
1428
1790
|
sourceControlManagerPort: config.sourceControlManager.port,
|
|
1791
|
+
sourceControlManagerRemote: config.sourceControlManager.remote,
|
|
1429
1792
|
authToken,
|
|
1430
1793
|
runtimeRoot: parsed.runtimeRoot,
|
|
1431
1794
|
runtimeTag: parsed.runtimeTag
|
|
@@ -1467,16 +1830,31 @@ async function main() {
|
|
|
1467
1830
|
const saved = readCliState(statePath);
|
|
1468
1831
|
const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
|
|
1469
1832
|
const monitorPort = parsePositiveInt(process.env.PUSHPALS_CLIENT_PORT, DEFAULT_MONITOR_PORT);
|
|
1470
|
-
const
|
|
1833
|
+
const monitoringHub = await resolveMonitoringHub({
|
|
1834
|
+
preferredUrl: preferredHubUrl,
|
|
1835
|
+
fallbackPort: monitorPort,
|
|
1836
|
+
serverUrl,
|
|
1837
|
+
localAgentUrl,
|
|
1838
|
+
sessionId: localBuddySessionId,
|
|
1839
|
+
authToken
|
|
1840
|
+
});
|
|
1841
|
+
const monitoringHubUrl = monitoringHub?.url ?? "";
|
|
1471
1842
|
writeCliState(statePath, {
|
|
1472
|
-
monitoringHubUrl,
|
|
1843
|
+
monitoringHubUrl: monitoringHubUrl || undefined,
|
|
1473
1844
|
serverUrl,
|
|
1474
1845
|
localAgentUrl,
|
|
1475
1846
|
sessionId: localBuddySessionId,
|
|
1476
1847
|
repoRoot
|
|
1477
1848
|
});
|
|
1478
1849
|
console.log("[pushpals] Connected.");
|
|
1479
|
-
|
|
1850
|
+
if (monitoringHubUrl) {
|
|
1851
|
+
console.log(`monitoringHubUrl=${monitoringHubUrl}`);
|
|
1852
|
+
if (monitoringHub?.embedded) {
|
|
1853
|
+
console.log("[pushpals] Embedded monitoring hub is running.");
|
|
1854
|
+
}
|
|
1855
|
+
} else {
|
|
1856
|
+
console.log("monitoringHubUrl=unavailable");
|
|
1857
|
+
}
|
|
1480
1858
|
console.log(`serverUrl=${serverUrl}`);
|
|
1481
1859
|
console.log(`localAgentUrl=${localAgentUrl}`);
|
|
1482
1860
|
console.log(`sessionId=${localBuddySessionId}`);
|
|
@@ -1506,10 +1884,14 @@ ${line}
|
|
|
1506
1884
|
streamAbort.abort();
|
|
1507
1885
|
if (rl)
|
|
1508
1886
|
rl.close();
|
|
1887
|
+
try {
|
|
1888
|
+
monitoringHub?.stop();
|
|
1889
|
+
} catch {}
|
|
1509
1890
|
stopAutoStartedServices();
|
|
1510
1891
|
};
|
|
1511
1892
|
process.once("SIGINT", requestStop);
|
|
1512
1893
|
process.once("SIGTERM", requestStop);
|
|
1894
|
+
process.once("exit", requestStop);
|
|
1513
1895
|
rl = createInterface({
|
|
1514
1896
|
input: process.stdin,
|
|
1515
1897
|
output: process.stdout,
|
|
@@ -1528,20 +1910,25 @@ ${line}
|
|
|
1528
1910
|
break;
|
|
1529
1911
|
}
|
|
1530
1912
|
if (text === "/hub") {
|
|
1531
|
-
console.log(`monitoringHubUrl=${monitoringHubUrl}`);
|
|
1913
|
+
console.log(monitoringHubUrl ? `monitoringHubUrl=${monitoringHubUrl}` : "monitoringHubUrl=unavailable");
|
|
1532
1914
|
rl.prompt();
|
|
1533
1915
|
continue;
|
|
1534
1916
|
}
|
|
1535
1917
|
if (text === "/status") {
|
|
1536
1918
|
console.log(`serverUrl=${serverUrl}`);
|
|
1537
1919
|
console.log(`localAgentUrl=${localAgentUrl}`);
|
|
1538
|
-
console.log(`sessionId=${
|
|
1920
|
+
console.log(`sessionId=${localBuddySessionId}`);
|
|
1539
1921
|
console.log(`repoRoot=${repoRoot}`);
|
|
1540
|
-
console.log(`monitoringHubUrl=${monitoringHubUrl}`);
|
|
1922
|
+
console.log(monitoringHubUrl ? `monitoringHubUrl=${monitoringHubUrl}` : "monitoringHubUrl=unavailable");
|
|
1541
1923
|
rl.prompt();
|
|
1542
1924
|
continue;
|
|
1543
1925
|
}
|
|
1544
1926
|
if (text === "/open") {
|
|
1927
|
+
if (!monitoringHubUrl) {
|
|
1928
|
+
console.log("[pushpals] Monitoring hub is unavailable.");
|
|
1929
|
+
rl.prompt();
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1545
1932
|
const opened = await openMonitoringHub(monitoringHubUrl);
|
|
1546
1933
|
console.log(opened ? `[pushpals] Opened ${monitoringHubUrl}` : `[pushpals] Failed to open browser. Use this link: ${monitoringHubUrl}`);
|
|
1547
1934
|
rl.prompt();
|
|
@@ -1556,7 +1943,14 @@ ${line}
|
|
|
1556
1943
|
requestStop();
|
|
1557
1944
|
await Promise.race([streamTask, Bun.sleep(2000)]);
|
|
1558
1945
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1946
|
+
if (import.meta.main) {
|
|
1947
|
+
main().catch((err) => {
|
|
1948
|
+
console.error(`[pushpals] Fatal: ${String(err)}`);
|
|
1949
|
+
process.exit(1);
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
export {
|
|
1953
|
+
startEmbeddedMonitoringHub,
|
|
1954
|
+
buildEmbeddedRuntimeEnv,
|
|
1955
|
+
buildEmbeddedMonitoringHubHtml
|
|
1956
|
+
};
|