@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.
Files changed (2) hide show
  1. package/dist/pushpals-cli.js +443 -49
  2. package/package.json +1 -1
@@ -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
- if (packageVersion)
913
- return `v${packageVersion}`;
914
- return await fetchLatestReleaseTag();
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 hasAssets = existsSync2(join2(runtimeRoot, ".env.example")) && existsSync2(join2(runtimeRoot, "configs", "default.toml")) && existsSync2(join2(runtimeRoot, "prompts"));
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
- const copied = copyBundledRuntimeAssets(runtimeRoot);
962
- if (!copied) {
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: "ignore",
1029
- stderr: "ignore"
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.kill();
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
- ...process.env,
1105
- PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
1106
- PUSHPALS_PROJECT_ROOT_OVERRIDE: opts.repoRoot,
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
- services.push(spawnRuntimeService("server", [runtimeBinaries.server], opts.repoRoot, runtimeEnv));
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
- services.push(spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv));
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
- services.push(spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv));
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
- if (!scmHealthy) {
1254
+ const scmRemoteAvailable = await repoHasRemote(opts.repoRoot, opts.sourceControlManagerRemote);
1255
+ if (!scmHealthy && scmRemoteAvailable) {
1124
1256
  console.log("[pushpals] Starting embedded SourceControlManager...");
1125
- services.push(spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv));
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
- async function resolveMonitoringHubUrl(preferredUrl, fallbackPort) {
1199
- const explicit = normalizeUrl(preferredUrl);
1200
- if (explicit)
1201
- return explicit;
1202
- const basePort = fallbackPort;
1203
- for (let port = basePort;port < basePort + MONITOR_SCAN_PORTS; port++) {
1204
- const candidate = `http://localhost:${port}`;
1205
- if (await looksLikeMonitoringHub(candidate))
1206
- return candidate;
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, "&amp;")
1417
+ .replace(/</g, "&lt;")
1418
+ .replace(/>/g, "&gt;");
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
- return `http://localhost:${basePort}`;
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
- const escaped = url.replace(/'/g, "''");
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 parsed = parseArgs(process.argv.slice(2));
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 monitoringHubUrl = await resolveMonitoringHubUrl(preferredHubUrl, monitorPort);
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
- console.log(`monitoringHubUrl=${monitoringHubUrl}`);
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=${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
- main().catch((err) => {
1560
- console.error(`[pushpals] Fatal: ${String(err)}`);
1561
- process.exit(1);
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {