elit 3.6.7 → 3.6.9

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 (42) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +20 -1
  4. package/dist/cli.cjs +2777 -321
  5. package/dist/cli.mjs +2764 -308
  6. package/dist/config.d.ts +6 -6
  7. package/dist/{contracts-BeW9k0yZ.d.ts → contracts-_0p1-15U.d.ts} +1 -1
  8. package/dist/coverage.d.ts +1 -1
  9. package/dist/dev-build.d.ts +3 -3
  10. package/dist/http.cjs +160 -41
  11. package/dist/http.d.ts +5 -11
  12. package/dist/http.js +160 -41
  13. package/dist/http.mjs +160 -41
  14. package/dist/https.cjs +194 -46
  15. package/dist/https.d.ts +3 -6
  16. package/dist/https.js +194 -46
  17. package/dist/https.mjs +194 -46
  18. package/dist/pm-node-shared-listener-bootstrap.cjs +75 -0
  19. package/dist/pm.cjs +2390 -160
  20. package/dist/pm.d.ts +83 -8
  21. package/dist/pm.js +2388 -188
  22. package/dist/pm.mjs +2380 -165
  23. package/dist/preview-build.d.ts +3 -3
  24. package/dist/server.cjs +417 -168
  25. package/dist/server.d.ts +4 -4
  26. package/dist/server.js +453 -179
  27. package/dist/server.mjs +417 -168
  28. package/dist/smtp-server.js +37 -12
  29. package/dist/test-reporter.d.ts +1 -1
  30. package/dist/test-runtime.d.ts +1 -1
  31. package/dist/{types-tJn88E1N.d.ts → types-BayMVo_k.d.ts} +39 -3
  32. package/dist/{types-CIhpN1-K.d.ts → types-C70T-42Z.d.ts} +1 -1
  33. package/dist/{types-DAisuVr5.d.ts → types-DPOgoGs-.d.ts} +7 -1
  34. package/dist/{state-DvEkDehk.d.ts → types-fiLday0L.d.ts} +96 -92
  35. package/dist/types.d.ts +4 -0
  36. package/dist/{websocket-XfyK23zD.d.ts → websocket-BLBEAnhp.d.ts} +1 -1
  37. package/dist/ws.d.ts +3 -3
  38. package/dist/wss.cjs +194 -46
  39. package/dist/wss.d.ts +3 -3
  40. package/dist/wss.js +194 -46
  41. package/dist/wss.mjs +194 -46
  42. package/package.json +11 -6
package/dist/pm.cjs CHANGED
@@ -480,6 +480,12 @@ __export(pm_exports, {
480
480
  DEFAULT_MIN_UPTIME: () => DEFAULT_MIN_UPTIME,
481
481
  DEFAULT_PM_DATA_DIR: () => DEFAULT_PM_DATA_DIR,
482
482
  DEFAULT_PM_DUMP_FILE: () => DEFAULT_PM_DUMP_FILE,
483
+ DEFAULT_PM_EXP_BACKOFF_MAX_DELAY: () => DEFAULT_PM_EXP_BACKOFF_MAX_DELAY,
484
+ DEFAULT_PM_KILL_TIMEOUT: () => DEFAULT_PM_KILL_TIMEOUT,
485
+ DEFAULT_PM_LISTEN_TIMEOUT: () => DEFAULT_PM_LISTEN_TIMEOUT,
486
+ DEFAULT_PM_MEMORY_CHECK_INTERVAL: () => DEFAULT_PM_MEMORY_CHECK_INTERVAL,
487
+ DEFAULT_PM_PROXY_STRATEGY: () => DEFAULT_PM_PROXY_STRATEGY,
488
+ DEFAULT_PM_RESTART_WINDOW: () => DEFAULT_PM_RESTART_WINDOW,
483
489
  DEFAULT_PM_STOP_GRACE_PERIOD_MS: () => DEFAULT_PM_STOP_GRACE_PERIOD_MS,
484
490
  DEFAULT_PM_STOP_POLL_MS: () => DEFAULT_PM_STOP_POLL_MS,
485
491
  DEFAULT_RESTART_DELAY: () => DEFAULT_RESTART_DELAY,
@@ -498,7 +504,10 @@ __export(pm_exports, {
498
504
  countDefinedPmWapkSources: () => countDefinedPmWapkSources,
499
505
  deriveDefaultWatchPaths: () => deriveDefaultWatchPaths,
500
506
  ensurePmDirectories: () => ensurePmDirectories,
507
+ expandPmInstanceDefinitions: () => expandPmInstanceDefinitions,
508
+ findPmGroupMatches: () => findPmGroupMatches,
501
509
  findPmRecordMatch: () => findPmRecordMatch,
510
+ formatPmInstanceName: () => formatPmInstanceName,
502
511
  getPmRecordPath: () => getPmRecordPath,
503
512
  hasPmGoogleDriveConfig: () => hasPmGoogleDriveConfig,
504
513
  hasPmWapkRunConfig: () => hasPmWapkRunConfig,
@@ -517,6 +526,10 @@ __export(pm_exports, {
517
526
  normalizeHealthCheckConfig: () => normalizeHealthCheckConfig,
518
527
  normalizeIntegerOption: () => normalizeIntegerOption,
519
528
  normalizeNonEmptyString: () => normalizeNonEmptyString,
529
+ normalizePmMemoryAction: () => normalizePmMemoryAction,
530
+ normalizePmMemoryLimit: () => normalizePmMemoryLimit,
531
+ normalizePmProxyConfig: () => normalizePmProxyConfig,
532
+ normalizePmProxyStrategy: () => normalizePmProxyStrategy,
520
533
  normalizePmRestartPolicy: () => normalizePmRestartPolicy,
521
534
  normalizePmRuntime: () => normalizePmRuntime,
522
535
  normalizeResolvedWatchIgnorePaths: () => normalizeResolvedWatchIgnorePaths,
@@ -540,7 +553,9 @@ __export(pm_exports, {
540
553
  runManagedProcessLoop: () => runManagedProcessLoop,
541
554
  runPmCommand: () => runPmCommand,
542
555
  runPmRunner: () => runPmRunner,
556
+ samplePmProcessMetrics: () => samplePmProcessMetrics,
543
557
  sanitizePmProcessName: () => sanitizePmProcessName,
558
+ sendPmSignal: () => sendPmSignal,
544
559
  startManagedProcess: () => startManagedProcess,
545
560
  stopPmMatches: () => stopPmMatches,
546
561
  stripPmWapkSourceFromRunConfig: () => stripPmWapkSourceFromRunConfig,
@@ -564,6 +579,7 @@ var DEFAULT_RESTART_DELAY = 1e3;
564
579
  var DEFAULT_MAX_RESTARTS = 10;
565
580
  var DEFAULT_WATCH_DEBOUNCE = 250;
566
581
  var DEFAULT_MIN_UPTIME = 0;
582
+ var DEFAULT_PM_LISTEN_TIMEOUT = 3e3;
567
583
  var DEFAULT_HEALTHCHECK_GRACE_PERIOD = 5e3;
568
584
  var DEFAULT_HEALTHCHECK_INTERVAL = 1e4;
569
585
  var DEFAULT_HEALTHCHECK_TIMEOUT = 3e3;
@@ -571,6 +587,11 @@ var DEFAULT_HEALTHCHECK_MAX_FAILURES = 3;
571
587
  var DEFAULT_LOG_LINES = 40;
572
588
  var DEFAULT_PM_STOP_POLL_MS = 100;
573
589
  var DEFAULT_PM_STOP_GRACE_PERIOD_MS = 5e3;
590
+ var DEFAULT_PM_KILL_TIMEOUT = DEFAULT_PM_STOP_GRACE_PERIOD_MS;
591
+ var DEFAULT_PM_EXP_BACKOFF_MAX_DELAY = 15e3;
592
+ var DEFAULT_PM_MEMORY_CHECK_INTERVAL = 500;
593
+ var DEFAULT_PM_RESTART_WINDOW = 0;
594
+ var DEFAULT_PM_PROXY_STRATEGY = "proxy";
574
595
  var PM_WAPK_ONLINE_STDIN_SHUTDOWN_ENV = "ELIT_PM_WAPK_ONLINE_STDIN_SHUTDOWN";
575
596
  var PM_WAPK_ONLINE_SHUTDOWN_COMMAND = "__ELIT_PM_WAPK_ONLINE_SHUTDOWN__";
576
597
  var PM_WAPK_ONLINE_SHUTDOWN_TIMEOUT_MS = 8e3;
@@ -608,6 +629,32 @@ function normalizePmRestartPolicy(value, optionName = "--restart-policy") {
608
629
  }
609
630
  throw new Error(`${optionName} must be one of: always, on-failure, never`);
610
631
  }
632
+ function normalizePmMemoryAction(value, optionName = "--memory-action") {
633
+ if (value === void 0 || value === null || value === "") {
634
+ return void 0;
635
+ }
636
+ if (typeof value !== "string") {
637
+ throw new Error(`${optionName} must be one of: restart, stop`);
638
+ }
639
+ const action = value.trim().toLowerCase();
640
+ if (action === "restart" || action === "stop") {
641
+ return action;
642
+ }
643
+ throw new Error(`${optionName} must be one of: restart, stop`);
644
+ }
645
+ function normalizePmProxyStrategy(value, optionName = "--proxy-strategy") {
646
+ if (value === void 0 || value === null || value === "") {
647
+ return void 0;
648
+ }
649
+ if (typeof value !== "string") {
650
+ throw new Error(`${optionName} must be one of: proxy, inherit`);
651
+ }
652
+ const strategy = value.trim().toLowerCase();
653
+ if (strategy === "proxy" || strategy === "inherit") {
654
+ return strategy;
655
+ }
656
+ throw new Error(`${optionName} must be one of: proxy, inherit`);
657
+ }
611
658
  function normalizeIntegerOption(value, optionName, min = 0) {
612
659
  const parsed = Number.parseInt(value, 10);
613
660
  if (!Number.isFinite(parsed) || parsed < min) {
@@ -615,6 +662,30 @@ function normalizeIntegerOption(value, optionName, min = 0) {
615
662
  }
616
663
  return parsed;
617
664
  }
665
+ function normalizePmMemoryLimit(value, optionName = "--max-memory") {
666
+ if (value === void 0 || value === null || value === "") {
667
+ return void 0;
668
+ }
669
+ if (typeof value === "number") {
670
+ const normalized2 = Math.trunc(value);
671
+ if (!Number.isFinite(normalized2) || normalized2 < 1) {
672
+ throw new Error(`${optionName} must be a number >= 1 or a size like 256M.`);
673
+ }
674
+ return normalized2;
675
+ }
676
+ if (typeof value !== "string") {
677
+ throw new Error(`${optionName} must be a number >= 1 or a size like 256M.`);
678
+ }
679
+ const normalized = value.trim();
680
+ const match = /^(\d+)(b|kb|mb|gb|tb|k|m|g|t)?$/i.exec(normalized);
681
+ if (!match) {
682
+ throw new Error(`${optionName} must be a number >= 1 or a size like 256M.`);
683
+ }
684
+ const amount = normalizeIntegerOption(match[1] ?? "", optionName, 1);
685
+ const unit = (match[2] ?? "b").toLowerCase();
686
+ const multiplier = unit === "b" ? 1 : unit === "k" || unit === "kb" ? 1024 : unit === "m" || unit === "mb" ? 1024 ** 2 : unit === "g" || unit === "gb" ? 1024 ** 3 : 1024 ** 4;
687
+ return amount * multiplier;
688
+ }
618
689
  function normalizeNonEmptyString(value) {
619
690
  if (typeof value !== "string") {
620
691
  return void 0;
@@ -891,6 +962,30 @@ function readRequiredValue(args, index, optionName) {
891
962
  }
892
963
  return value;
893
964
  }
965
+ function normalizePmProxyConfig(value, optionName = "pm proxy") {
966
+ if (value === void 0 || value === null) {
967
+ return void 0;
968
+ }
969
+ if (typeof value !== "object") {
970
+ throw new Error(`${optionName} must be an object with at least a port.`);
971
+ }
972
+ const candidate = value;
973
+ const hasAnyValue = candidate.port !== void 0 || candidate.host !== void 0 || candidate.targetHost !== void 0 || candidate.envVar !== void 0;
974
+ if (!hasAnyValue) {
975
+ return void 0;
976
+ }
977
+ if (candidate.port === void 0 || candidate.port === null || candidate.port === "") {
978
+ throw new Error(`${optionName}.port is required.`);
979
+ }
980
+ const port = typeof candidate.port === "number" ? normalizeIntegerOption(String(Math.trunc(candidate.port)), `${optionName}.port`, 1) : normalizeIntegerOption(String(candidate.port), `${optionName}.port`, 1);
981
+ return {
982
+ port,
983
+ strategy: normalizePmProxyStrategy(candidate.strategy, `${optionName}.strategy`) ?? DEFAULT_PM_PROXY_STRATEGY,
984
+ host: normalizeNonEmptyString(candidate.host),
985
+ targetHost: normalizeNonEmptyString(candidate.targetHost),
986
+ envVar: normalizeNonEmptyString(candidate.envVar)
987
+ };
988
+ }
894
989
 
895
990
  // src/cli/pm/records.ts
896
991
  var import_node_fs2 = require("fs");
@@ -923,18 +1018,31 @@ function writePmRecord(filePath, record) {
923
1018
  function toSavedAppDefinition(record) {
924
1019
  return {
925
1020
  name: record.name,
1021
+ baseName: record.baseName,
1022
+ instanceIndex: record.instanceIndex,
1023
+ instances: record.instances,
926
1024
  type: record.type,
927
1025
  cwd: record.cwd,
928
1026
  runtime: record.runtime,
929
1027
  env: record.env,
1028
+ proxy: record.proxy,
930
1029
  script: record.script,
931
1030
  file: record.file,
932
1031
  wapk: record.wapk,
933
1032
  password: record.password,
934
1033
  wapkRun: record.wapkRun,
935
1034
  restartPolicy: record.restartPolicy,
1035
+ maxMemoryBytes: record.maxMemoryBytes,
1036
+ memoryAction: record.memoryAction,
1037
+ cronRestart: record.cronRestart,
1038
+ expBackoffRestartDelay: record.expBackoffRestartDelay,
1039
+ expBackoffRestartMaxDelay: record.expBackoffRestartMaxDelay,
1040
+ restartWindow: record.restartWindow,
1041
+ waitReady: record.waitReady,
1042
+ listenTimeout: record.listenTimeout,
936
1043
  autorestart: record.autorestart,
937
1044
  restartDelay: record.restartDelay,
1045
+ killTimeout: record.killTimeout,
938
1046
  maxRestarts: record.maxRestarts,
939
1047
  minUptime: record.minUptime,
940
1048
  watch: record.watch,
@@ -1001,6 +1109,9 @@ function findPmRecordMatch(paths, nameOrId) {
1001
1109
  }
1002
1110
  return listPmRecordMatches(paths).find((match) => match.record.name === nameOrId);
1003
1111
  }
1112
+ function findPmGroupMatches(paths, baseName) {
1113
+ return listPmRecordMatches(paths).filter((match) => match.record.baseName === baseName).sort((left, right) => left.record.instanceIndex - right.record.instanceIndex);
1114
+ }
1004
1115
  function isProcessAlive(pid) {
1005
1116
  if (!pid || pid <= 0) {
1006
1117
  return false;
@@ -1050,11 +1161,22 @@ function toPmAppConfig(record) {
1050
1161
  runtime: record.runtime,
1051
1162
  cwd: record.cwd,
1052
1163
  env: record.env,
1164
+ proxy: record.proxy,
1165
+ instances: record.instances,
1053
1166
  autorestart: record.autorestart,
1054
1167
  restartDelay: record.restartDelay,
1168
+ killTimeout: record.killTimeout,
1055
1169
  maxRestarts: record.maxRestarts,
1056
1170
  password: record.password,
1057
1171
  restartPolicy: record.restartPolicy,
1172
+ maxMemory: record.maxMemoryBytes,
1173
+ memoryAction: record.memoryAction,
1174
+ cronRestart: record.cronRestart,
1175
+ expBackoffRestartDelay: record.expBackoffRestartDelay,
1176
+ expBackoffRestartMaxDelay: record.expBackoffRestartMaxDelay,
1177
+ restartWindow: record.restartWindow,
1178
+ waitReady: record.waitReady,
1179
+ listenTimeout: record.listenTimeout,
1058
1180
  minUptime: record.minUptime,
1059
1181
  watch: record.watch,
1060
1182
  watchPaths: record.watchPaths,
@@ -1073,11 +1195,22 @@ function toSavedPmAppConfig(app) {
1073
1195
  runtime: app.runtime,
1074
1196
  cwd: app.cwd,
1075
1197
  env: app.env,
1198
+ proxy: app.proxy,
1199
+ instances: app.instances,
1076
1200
  autorestart: app.autorestart,
1077
1201
  restartDelay: app.restartDelay,
1202
+ killTimeout: app.killTimeout,
1078
1203
  maxRestarts: app.maxRestarts,
1079
1204
  password: app.password,
1080
1205
  restartPolicy: app.restartPolicy,
1206
+ maxMemory: app.maxMemoryBytes,
1207
+ memoryAction: app.memoryAction,
1208
+ cronRestart: app.cronRestart,
1209
+ expBackoffRestartDelay: app.expBackoffRestartDelay,
1210
+ expBackoffRestartMaxDelay: app.expBackoffRestartMaxDelay,
1211
+ restartWindow: app.restartWindow,
1212
+ waitReady: app.waitReady,
1213
+ listenTimeout: app.listenTimeout,
1081
1214
  minUptime: app.minUptime,
1082
1215
  watch: app.watch,
1083
1216
  watchPaths: app.watchPaths,
@@ -1089,6 +1222,170 @@ function toSavedPmAppConfig(app) {
1089
1222
 
1090
1223
  // src/cli/pm/config.ts
1091
1224
  var import_node_path4 = require("path");
1225
+
1226
+ // src/cli/pm/schedule.ts
1227
+ var PM_EVERY_PATTERN = /^@every\s+(\d+)(ms|s|m|h|d)$/i;
1228
+ function parsePositiveInteger(value, optionName, min = 0) {
1229
+ const parsed = Number.parseInt(value, 10);
1230
+ if (!Number.isFinite(parsed) || parsed < min) {
1231
+ throw new Error(`${optionName} must be a number >= ${min}`);
1232
+ }
1233
+ return parsed;
1234
+ }
1235
+ function fillRange(values, start, end, step, min, max, optionName) {
1236
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < min || end > max || start > end) {
1237
+ throw new Error(`${optionName} contains an invalid range: ${start}-${end}`);
1238
+ }
1239
+ if (!Number.isInteger(step) || step < 1) {
1240
+ throw new Error(`${optionName} contains an invalid step: ${step}`);
1241
+ }
1242
+ for (let current = start; current <= end; current += step) {
1243
+ values.add(current);
1244
+ }
1245
+ }
1246
+ function parseCronSegment(segment, values, min, max, optionName) {
1247
+ const normalized = segment.trim();
1248
+ if (!normalized) {
1249
+ throw new Error(`${optionName} contains an empty field segment.`);
1250
+ }
1251
+ const [rangeToken, stepToken] = normalized.split("/");
1252
+ if (normalized.split("/").length > 2) {
1253
+ throw new Error(`${optionName} contains an invalid step segment: ${normalized}`);
1254
+ }
1255
+ const step = stepToken === void 0 ? 1 : parsePositiveInteger(stepToken, optionName, 1);
1256
+ if (rangeToken === "*") {
1257
+ fillRange(values, min, max, step, min, max, optionName);
1258
+ return;
1259
+ }
1260
+ if (rangeToken.includes("-")) {
1261
+ const [startToken, endToken] = rangeToken.split("-");
1262
+ if (!startToken || !endToken) {
1263
+ throw new Error(`${optionName} contains an invalid range: ${normalized}`);
1264
+ }
1265
+ const start2 = parsePositiveInteger(startToken, optionName, min);
1266
+ const end = parsePositiveInteger(endToken, optionName, min);
1267
+ fillRange(values, start2, end, step, min, max, optionName);
1268
+ return;
1269
+ }
1270
+ const start = parsePositiveInteger(rangeToken, optionName, min);
1271
+ fillRange(values, start, stepToken === void 0 ? start : max, step, min, max, optionName);
1272
+ }
1273
+ function parseCronField(token, min, max, optionName) {
1274
+ const normalized = token.trim();
1275
+ if (!normalized) {
1276
+ throw new Error(`${optionName} contains an empty field.`);
1277
+ }
1278
+ if (normalized === "*") {
1279
+ const values2 = /* @__PURE__ */ new Set();
1280
+ fillRange(values2, min, max, 1, min, max, optionName);
1281
+ return {
1282
+ values: values2,
1283
+ wildcard: true
1284
+ };
1285
+ }
1286
+ const values = /* @__PURE__ */ new Set();
1287
+ for (const segment of normalized.split(",")) {
1288
+ parseCronSegment(segment, values, min, max, optionName);
1289
+ }
1290
+ if (values.size === 0) {
1291
+ throw new Error(`${optionName} must match at least one value.`);
1292
+ }
1293
+ return {
1294
+ values,
1295
+ wildcard: false
1296
+ };
1297
+ }
1298
+ function matchesCronField(field, value) {
1299
+ if (!field) {
1300
+ return true;
1301
+ }
1302
+ return field.wildcard || field.values.has(value);
1303
+ }
1304
+ function matchesCronDate(schedule, candidate) {
1305
+ if (schedule.kind !== "cron") {
1306
+ return false;
1307
+ }
1308
+ const secondMatches = schedule.hasSeconds ? matchesCronField(schedule.seconds, candidate.getSeconds()) : true;
1309
+ const minuteMatches = matchesCronField(schedule.minutes, candidate.getMinutes());
1310
+ const hourMatches = matchesCronField(schedule.hours, candidate.getHours());
1311
+ const monthMatches = matchesCronField(schedule.months, candidate.getMonth() + 1);
1312
+ const dayOfMonthMatches = matchesCronField(schedule.daysOfMonth, candidate.getDate());
1313
+ const dayOfWeekMatches = matchesCronField(schedule.daysOfWeek, candidate.getDay());
1314
+ const domWildcard = schedule.daysOfMonth?.wildcard ?? true;
1315
+ const dowWildcard = schedule.daysOfWeek?.wildcard ?? true;
1316
+ const dayMatches = domWildcard && dowWildcard ? true : domWildcard ? dayOfWeekMatches : dowWildcard ? dayOfMonthMatches : dayOfMonthMatches || dayOfWeekMatches;
1317
+ return secondMatches && minuteMatches && hourMatches && monthMatches && dayMatches;
1318
+ }
1319
+ function parsePmRestartSchedule(expression, optionName = "--cron-restart") {
1320
+ const normalized = expression.trim();
1321
+ const everyMatch = PM_EVERY_PATTERN.exec(normalized);
1322
+ if (everyMatch) {
1323
+ const [, valueText, unit] = everyMatch;
1324
+ const value = parsePositiveInteger(valueText ?? "", optionName, 1);
1325
+ const multiplier = unit?.toLowerCase() === "ms" ? 1 : unit?.toLowerCase() === "s" ? 1e3 : unit?.toLowerCase() === "m" ? 6e4 : unit?.toLowerCase() === "h" ? 36e5 : 864e5;
1326
+ return {
1327
+ expression: normalized,
1328
+ kind: "every",
1329
+ intervalMs: value * multiplier,
1330
+ hasSeconds: true
1331
+ };
1332
+ }
1333
+ const fields = normalized.split(/\s+/).filter((field) => field.length > 0);
1334
+ if (fields.length !== 5 && fields.length !== 6) {
1335
+ throw new Error(`${optionName} must be a 5-field cron expression or @every <duration>.`);
1336
+ }
1337
+ const hasSeconds = fields.length === 6;
1338
+ const offset = hasSeconds ? 1 : 0;
1339
+ return {
1340
+ expression: normalized,
1341
+ kind: "cron",
1342
+ hasSeconds,
1343
+ seconds: hasSeconds ? parseCronField(fields[0] ?? "", 0, 59, optionName) : void 0,
1344
+ minutes: parseCronField(fields[0 + offset] ?? "", 0, 59, optionName),
1345
+ hours: parseCronField(fields[1 + offset] ?? "", 0, 23, optionName),
1346
+ daysOfMonth: parseCronField(fields[2 + offset] ?? "", 1, 31, optionName),
1347
+ months: parseCronField(fields[3 + offset] ?? "", 1, 12, optionName),
1348
+ daysOfWeek: parseCronField(fields[4 + offset] ?? "", 0, 6, optionName)
1349
+ };
1350
+ }
1351
+ function normalizePmRestartSchedule(value, optionName = "--cron-restart") {
1352
+ if (value === void 0 || value === null || value === "") {
1353
+ return void 0;
1354
+ }
1355
+ if (typeof value !== "string") {
1356
+ throw new Error(`${optionName} must be a cron string or @every <duration>.`);
1357
+ }
1358
+ const normalized = value.trim();
1359
+ if (!normalized) {
1360
+ throw new Error(`${optionName} must be a non-empty cron string or @every <duration>.`);
1361
+ }
1362
+ parsePmRestartSchedule(normalized, optionName);
1363
+ return normalized;
1364
+ }
1365
+ function resolveNextPmScheduleOccurrence(schedule, after = /* @__PURE__ */ new Date()) {
1366
+ if (schedule.kind === "every") {
1367
+ return new Date(after.getTime() + (schedule.intervalMs ?? 0));
1368
+ }
1369
+ const stepMs = schedule.hasSeconds ? 1e3 : 6e4;
1370
+ const candidate = new Date(after.getTime());
1371
+ if (schedule.hasSeconds) {
1372
+ candidate.setMilliseconds(0);
1373
+ candidate.setSeconds(candidate.getSeconds() + 1);
1374
+ } else {
1375
+ candidate.setSeconds(0, 0);
1376
+ candidate.setMinutes(candidate.getMinutes() + 1);
1377
+ }
1378
+ const maxIterations = schedule.hasSeconds ? 2678400 : 527040;
1379
+ for (let index = 0; index < maxIterations; index++) {
1380
+ if (matchesCronDate(schedule, candidate)) {
1381
+ return new Date(candidate.getTime());
1382
+ }
1383
+ candidate.setTime(candidate.getTime() + stepMs);
1384
+ }
1385
+ return void 0;
1386
+ }
1387
+
1388
+ // src/cli/pm/config.ts
1092
1389
  function parsePmTarget(parsed, workspaceRoot) {
1093
1390
  if (parsed.script) {
1094
1391
  return { script: parsed.script };
@@ -1131,6 +1428,20 @@ function defaultProcessName(base, explicitName) {
1131
1428
  }
1132
1429
  return "process";
1133
1430
  }
1431
+ function formatPmInstanceName(baseName, instanceIndex) {
1432
+ return instanceIndex <= 1 ? baseName : `${baseName}:${instanceIndex}`;
1433
+ }
1434
+ function expandPmInstanceDefinitions(definition, targetInstances = definition.instances) {
1435
+ return Array.from({ length: targetInstances }, (_, index) => {
1436
+ const instanceIndex = index + 1;
1437
+ return {
1438
+ ...definition,
1439
+ name: formatPmInstanceName(definition.baseName, instanceIndex),
1440
+ instanceIndex,
1441
+ instances: targetInstances
1442
+ };
1443
+ });
1444
+ }
1134
1445
  function countDefinedTargets(app) {
1135
1446
  return [app.script, app.file, app.wapk].filter(Boolean).length;
1136
1447
  }
@@ -1168,8 +1479,25 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1168
1479
  throw new Error("A pm app must define exactly one of script, file, or wapk.");
1169
1480
  }
1170
1481
  const name = defaultProcessName({ script, file, wapk }, parsed.name ?? base?.name);
1482
+ const instances = parsed.instances ?? base?.instances ?? 1;
1483
+ if (!Number.isInteger(instances) || instances < 1) {
1484
+ throw new Error("pm instances must be a number >= 1.");
1485
+ }
1486
+ const proxy = normalizePmProxyConfig(
1487
+ parsed.proxy ? { ...base?.proxy ?? {}, ...parsed.proxy } : base?.proxy,
1488
+ parsed.proxy ? "pm proxy" : "pm.apps[].proxy"
1489
+ );
1490
+ if (proxy?.strategy === "inherit" && instances > 1) {
1491
+ throw new Error('pm proxy strategy "inherit" currently supports only one managed instance per app.');
1492
+ }
1171
1493
  const mergedWapkRun = mergePmWapkRunConfig(base?.wapkRun, parsed.wapkRun);
1172
1494
  const runtime2 = normalizePmRuntime(parsed.runtime ?? mergedWapkRun?.runtime ?? base?.runtime, "--runtime");
1495
+ const maxMemoryBytes = parsed.maxMemoryBytes ?? normalizePmMemoryLimit(base?.maxMemory, "pm.apps[].maxMemory");
1496
+ const memoryAction = parsed.memoryAction ?? normalizePmMemoryAction(base?.memoryAction, "pm.apps[].memoryAction") ?? "restart";
1497
+ const cronRestart = parsed.cronRestart ?? normalizePmRestartSchedule(base?.cronRestart, "pm.apps[].cronRestart");
1498
+ const expBackoffRestartDelay = parsed.expBackoffRestartDelay ?? (base?.expBackoffRestartDelay === void 0 ? void 0 : normalizeIntegerOption(String(base.expBackoffRestartDelay), "pm.apps[].expBackoffRestartDelay", 1));
1499
+ const expBackoffRestartMaxDelay = parsed.expBackoffRestartMaxDelay ?? (base?.expBackoffRestartMaxDelay === void 0 ? void 0 : normalizeIntegerOption(String(base.expBackoffRestartMaxDelay), "pm.apps[].expBackoffRestartMaxDelay", 1));
1500
+ const restartWindow = parsed.restartWindow ?? (base?.restartWindow === void 0 ? void 0 : normalizeIntegerOption(String(base.restartWindow), "pm.apps[].restartWindow", 1));
1173
1501
  let restartPolicy = normalizePmRestartPolicy(parsed.restartPolicy ?? base?.restartPolicy, "--restart-policy") ?? (base?.autorestart ?? true ? "always" : "never");
1174
1502
  if (parsed.autorestart === false) {
1175
1503
  restartPolicy = "never";
@@ -1189,6 +1517,7 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1189
1517
  timeout: parsed.healthCheckTimeout,
1190
1518
  maxFailures: parsed.healthCheckMaxFailures
1191
1519
  } : base?.healthCheck);
1520
+ const waitReady = parsed.waitReady ?? base?.waitReady ?? false;
1192
1521
  const password = parsed.password ?? mergedWapkRun?.password ?? base?.password;
1193
1522
  const wapkRun = stripPmWapkSourceFromRunConfig(mergedWapkRun);
1194
1523
  if (password && !wapk) {
@@ -1197,8 +1526,14 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1197
1526
  if (wapkRun && !wapk) {
1198
1527
  throw new Error("WAPK run options are only supported when starting a WAPK app.");
1199
1528
  }
1529
+ if (waitReady && !healthCheck) {
1530
+ throw new Error("--wait-ready requires --health-url or pm.apps[].healthCheck.");
1531
+ }
1200
1532
  return {
1201
- name,
1533
+ name: formatPmInstanceName(name, 1),
1534
+ baseName: name,
1535
+ instanceIndex: 1,
1536
+ instances,
1202
1537
  type: script ? "script" : wapk ? "wapk" : "file",
1203
1538
  source,
1204
1539
  cwd: resolvedCwd,
@@ -1213,9 +1548,19 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1213
1548
  wapkRun,
1214
1549
  autorestart,
1215
1550
  restartDelay: parsed.restartDelay ?? base?.restartDelay ?? DEFAULT_RESTART_DELAY,
1551
+ proxy,
1552
+ killTimeout: parsed.killTimeout ?? base?.killTimeout ?? DEFAULT_PM_KILL_TIMEOUT,
1216
1553
  maxRestarts: parsed.maxRestarts ?? base?.maxRestarts ?? DEFAULT_MAX_RESTARTS,
1217
1554
  password,
1218
1555
  restartPolicy,
1556
+ maxMemoryBytes,
1557
+ memoryAction,
1558
+ cronRestart,
1559
+ expBackoffRestartDelay,
1560
+ expBackoffRestartMaxDelay,
1561
+ restartWindow,
1562
+ waitReady,
1563
+ listenTimeout: parsed.listenTimeout ?? base?.listenTimeout ?? DEFAULT_PM_LISTEN_TIMEOUT,
1219
1564
  minUptime: parsed.minUptime ?? base?.minUptime ?? DEFAULT_MIN_UPTIME,
1220
1565
  watch: watch2,
1221
1566
  watchPaths: watch2 ? normalizeResolvedWatchPaths(configuredWatchPaths, resolvedCwd, script ? "script" : wapk ? "wapk" : "file", file, wapk) : [],
@@ -1227,16 +1572,17 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1227
1572
  function resolvePmStartDefinitions(parsed, config, workspaceRoot) {
1228
1573
  const configApps = getConfiguredPmApps(config);
1229
1574
  const selection = resolveStartSelection(configApps, parsed, workspaceRoot);
1575
+ const expandDefinitions = (definitions) => definitions.flatMap((definition) => expandPmInstanceDefinitions(definition));
1230
1576
  if (selection.startAll) {
1231
1577
  if (configApps.length === 0) {
1232
1578
  throw new Error("No pm apps configured in elit.config.* and no start target was provided.");
1233
1579
  }
1234
- return configApps.map((app) => resolvePmAppDefinition(app, { ...parsed, name: app.name }, workspaceRoot, "config"));
1580
+ return expandDefinitions(configApps.map((app) => resolvePmAppDefinition(app, { ...parsed, name: app.name }, workspaceRoot, "config")));
1235
1581
  }
1236
1582
  if (selection.selected) {
1237
- return [resolvePmAppDefinition(selection.selected, parsed, workspaceRoot, "config")];
1583
+ return expandDefinitions([resolvePmAppDefinition(selection.selected, parsed, workspaceRoot, "config")]);
1238
1584
  }
1239
- return [resolvePmAppDefinition(void 0, parsed, workspaceRoot, "cli")];
1585
+ return expandDefinitions([resolvePmAppDefinition(void 0, parsed, workspaceRoot, "cli")]);
1240
1586
  }
1241
1587
  function parsePmStartArgs(args) {
1242
1588
  const parsed = {
@@ -1309,6 +1655,39 @@ function parsePmStartArgs(args) {
1309
1655
  parsed.env[key] = value;
1310
1656
  break;
1311
1657
  }
1658
+ case "--instances":
1659
+ parsed.instances = normalizeIntegerOption(readRequiredValue(args, ++index, "--instances"), "--instances", 1);
1660
+ break;
1661
+ case "--proxy-port":
1662
+ parsed.proxy = {
1663
+ ...parsed.proxy,
1664
+ port: normalizeIntegerOption(readRequiredValue(args, ++index, "--proxy-port"), "--proxy-port", 1)
1665
+ };
1666
+ break;
1667
+ case "--proxy-strategy":
1668
+ parsed.proxy = {
1669
+ ...parsed.proxy,
1670
+ strategy: normalizePmProxyStrategy(readRequiredValue(args, ++index, "--proxy-strategy"), "--proxy-strategy")
1671
+ };
1672
+ break;
1673
+ case "--proxy-host":
1674
+ parsed.proxy = {
1675
+ ...parsed.proxy,
1676
+ host: readRequiredValue(args, ++index, "--proxy-host")
1677
+ };
1678
+ break;
1679
+ case "--proxy-target-host":
1680
+ parsed.proxy = {
1681
+ ...parsed.proxy,
1682
+ targetHost: readRequiredValue(args, ++index, "--proxy-target-host")
1683
+ };
1684
+ break;
1685
+ case "--proxy-env":
1686
+ parsed.proxy = {
1687
+ ...parsed.proxy,
1688
+ envVar: readRequiredValue(args, ++index, "--proxy-env")
1689
+ };
1690
+ break;
1312
1691
  case "--password":
1313
1692
  parsed.password = readRequiredValue(args, ++index, "--password");
1314
1693
  break;
@@ -1359,6 +1738,30 @@ function parsePmStartArgs(args) {
1359
1738
  case "--restart-policy":
1360
1739
  parsed.restartPolicy = normalizePmRestartPolicy(readRequiredValue(args, ++index, "--restart-policy"));
1361
1740
  break;
1741
+ case "--max-memory":
1742
+ parsed.maxMemoryBytes = normalizePmMemoryLimit(readRequiredValue(args, ++index, "--max-memory"), "--max-memory");
1743
+ break;
1744
+ case "--memory-action":
1745
+ parsed.memoryAction = normalizePmMemoryAction(readRequiredValue(args, ++index, "--memory-action"), "--memory-action");
1746
+ break;
1747
+ case "--cron-restart":
1748
+ parsed.cronRestart = normalizePmRestartSchedule(readRequiredValue(args, ++index, "--cron-restart"), "--cron-restart");
1749
+ break;
1750
+ case "--exp-backoff-restart-delay":
1751
+ parsed.expBackoffRestartDelay = normalizeIntegerOption(readRequiredValue(args, ++index, "--exp-backoff-restart-delay"), "--exp-backoff-restart-delay", 1);
1752
+ break;
1753
+ case "--exp-backoff-restart-max-delay":
1754
+ parsed.expBackoffRestartMaxDelay = normalizeIntegerOption(readRequiredValue(args, ++index, "--exp-backoff-restart-max-delay"), "--exp-backoff-restart-max-delay", 1);
1755
+ break;
1756
+ case "--restart-window":
1757
+ parsed.restartWindow = normalizeIntegerOption(readRequiredValue(args, ++index, "--restart-window"), "--restart-window", 1);
1758
+ break;
1759
+ case "--wait-ready":
1760
+ parsed.waitReady = true;
1761
+ break;
1762
+ case "--listen-timeout":
1763
+ parsed.listenTimeout = normalizeIntegerOption(readRequiredValue(args, ++index, "--listen-timeout"), "--listen-timeout", 1);
1764
+ break;
1362
1765
  case "--min-uptime":
1363
1766
  parsed.minUptime = normalizeIntegerOption(readRequiredValue(args, ++index, "--min-uptime"), "--min-uptime");
1364
1767
  break;
@@ -1398,6 +1801,9 @@ function parsePmStartArgs(args) {
1398
1801
  case "--restart-delay":
1399
1802
  parsed.restartDelay = normalizeIntegerOption(readRequiredValue(args, ++index, "--restart-delay"), "--restart-delay");
1400
1803
  break;
1804
+ case "--kill-timeout":
1805
+ parsed.killTimeout = normalizeIntegerOption(readRequiredValue(args, ++index, "--kill-timeout"), "--kill-timeout", 1);
1806
+ break;
1401
1807
  case "--max-restarts":
1402
1808
  parsed.maxRestarts = normalizeIntegerOption(readRequiredValue(args, ++index, "--max-restarts"), "--max-restarts");
1403
1809
  break;
@@ -1439,8 +1845,15 @@ function printPmHelp() {
1439
1845
  " elit pm start my-app",
1440
1846
  " elit pm start",
1441
1847
  " elit pm list",
1848
+ " elit pm list --json",
1849
+ " elit pm show <name>",
1850
+ " elit pm describe <name> --json",
1442
1851
  " elit pm stop <name|all>",
1443
1852
  " elit pm restart <name|all>",
1853
+ " elit pm reload <name|all>",
1854
+ " elit pm scale <name> <count>",
1855
+ " elit pm reset <name|all>",
1856
+ " elit pm send-signal <signal> <name|all>",
1444
1857
  " elit pm delete <name|all>",
1445
1858
  " elit pm save",
1446
1859
  " elit pm resurrect",
@@ -1456,6 +1869,12 @@ function printPmHelp() {
1456
1869
  " --google-drive-shared-drive Forward supportsAllDrives=true for shared drives",
1457
1870
  " --runtime, -r <name> Runtime override: node, bun, deno",
1458
1871
  " --name, -n <name> Process name used by list/stop/restart",
1872
+ " --instances <count> Start multiple managed instances for the same app name",
1873
+ " --proxy-port <port> Own a public HTTP port through a PM proxy for single-instance reload handoff",
1874
+ " --proxy-strategy <mode> Public socket mode: proxy or inherit (default: proxy)",
1875
+ " --proxy-host <host> Public host bound by the PM proxy (default: 0.0.0.0)",
1876
+ " --proxy-target-host <host> Internal host used for upstream child traffic (default: 127.0.0.1)",
1877
+ " --proxy-env <name> Env var populated with the child private port (default: PORT)",
1459
1878
  " --cwd <dir> Working directory for the managed process",
1460
1879
  " --env KEY=VALUE Add or override an environment variable",
1461
1880
  " --password <value> Password for locked WAPK archives",
@@ -1467,6 +1886,14 @@ function printPmHelp() {
1467
1886
  " --no-archive-watch Disable archive-source read sync for WAPK apps",
1468
1887
  " --archive-sync-interval <ms> Forward WAPK archive read-sync interval (>= 50ms)",
1469
1888
  " --restart-policy <mode> Restart policy: always, on-failure, never",
1889
+ " --max-memory <bytes|size> Trigger a memory action after a limit like 268435456 or 256M",
1890
+ " --memory-action <mode> Action on max-memory: restart, stop",
1891
+ " --cron-restart <expr> Restart on a cron schedule or @every <duration>",
1892
+ " --exp-backoff-restart-delay <ms> Exponential unstable-restart backoff base delay",
1893
+ " --exp-backoff-restart-max-delay <ms> Maximum unstable-restart backoff delay (default 15000)",
1894
+ " --restart-window <ms> Rolling time window used when counting restart attempts",
1895
+ " --wait-ready Keep the process in starting state until its health check succeeds",
1896
+ " --listen-timeout <ms> Startup wait limit when --wait-ready is enabled (default 3000)",
1470
1897
  " --min-uptime <ms> Reset crash counter after this healthy uptime",
1471
1898
  " --watch Restart when watched files change",
1472
1899
  " --watch-path <path> Add a file or directory to watch",
@@ -1479,6 +1906,7 @@ function printPmHelp() {
1479
1906
  " --health-max-failures <n> Consecutive failures before restart (default 3)",
1480
1907
  " --no-autorestart Disable automatic restart",
1481
1908
  " --restart-delay <ms> Delay between restart attempts (default 1000)",
1909
+ " --kill-timeout <ms> Grace period before force-killing a stop or restart (default 5000)",
1482
1910
  " --max-restarts <count> Maximum restart attempts (default 10)",
1483
1911
  "",
1484
1912
  "Config:",
@@ -1502,6 +1930,17 @@ function printPmHelp() {
1502
1930
  " - elit pm save persists running apps to pm.dumpFile or ./.elit/pm/dump.json.",
1503
1931
  " - elit pm resurrect restarts whatever was last saved by elit pm save.",
1504
1932
  " - elit pm start <name> starts a configured app by name.",
1933
+ " - elit pm reload <name|all> performs a rolling stop/start and waits for each instance to return online before continuing.",
1934
+ " - elit pm scale <name> <count> changes the number of managed instances for a running app group.",
1935
+ " - elit pm reset <name|all> clears restart count, last exit code, and saved error metadata.",
1936
+ " - elit pm send-signal <signal> <name|all> forwards a POSIX-style signal such as SIGUSR2 or TERM.",
1937
+ " - maxMemory works with memoryAction=restart|stop, cronRestart accepts cron or @every schedules, expBackoffRestartDelay doubles unstable restart delays, expBackoffRestartMaxDelay caps them, and restartWindow limits how long restart attempts keep counting toward maxRestarts.",
1938
+ " - killTimeout controls how long PM waits before force-killing an app that ignores stop or restart requests.",
1939
+ " - waitReady uses the configured health check as a startup gate and errors if it never becomes healthy within listenTimeout.",
1940
+ " - elit pm list shows live cpu, memory, and uptime columns when the child process is running.",
1941
+ " - elit pm list --json and elit pm jlist print machine-readable process state.",
1942
+ " - elit pm list --json, elit pm show --json, and elit pm describe --json include liveMetrics when available.",
1943
+ " - elit pm show <name> and elit pm describe <name> expose the full saved process record.",
1505
1944
  " - TypeScript files with runtime node require tsx, otherwise use --runtime bun.",
1506
1945
  " - WAPK processes are executed through elit wapk run inside the manager.",
1507
1946
  " - WAPK PM apps can use local archives, gdrive://<fileId>, or pm.apps[].wapkRun.googleDrive.",
@@ -1557,16 +1996,94 @@ function resolveTsxExecutable(cwd) {
1557
1996
  const globalCommand = process.platform === "win32" ? "tsx.cmd" : "tsx";
1558
1997
  return commandExists(globalCommand) ? globalCommand : void 0;
1559
1998
  }
1999
+ function resolvePmNodeSharedListenerBootstrapPath() {
2000
+ const cliEntry = process.argv[1];
2001
+ const candidates = [
2002
+ (0, import_node_path5.join)(__dirname, "node-shared-listener-bootstrap.cjs"),
2003
+ (0, import_node_path5.join)(__dirname, "..", "..", "..", "dist", "pm-node-shared-listener-bootstrap.cjs")
2004
+ ];
2005
+ if (cliEntry) {
2006
+ const resolvedCliEntry = (0, import_node_path5.resolve)(cliEntry);
2007
+ const baseDir = (0, import_node_path5.dirname)(resolvedCliEntry);
2008
+ candidates.push(
2009
+ (0, import_node_path5.join)(baseDir, "cli", "pm", "node-shared-listener-bootstrap.cjs"),
2010
+ (0, import_node_path5.join)(baseDir, "pm-node-shared-listener-bootstrap.cjs")
2011
+ );
2012
+ }
2013
+ return candidates.find((candidate) => (0, import_node_fs3.existsSync)(candidate));
2014
+ }
2015
+ function appendNodeOption(existing, option) {
2016
+ return existing && existing.trim().length > 0 ? `${existing.trim()} ${option}` : option;
2017
+ }
2018
+ function supportsPmInheritedListener(record, runtime2) {
2019
+ return (record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "inherit" && record.type === "file" && runtime2 === "node" && !isTypescriptFile(record.file);
2020
+ }
1560
2021
  function inferRuntimeFromFile(filePath) {
1561
2022
  if (isTypescriptFile(filePath) && commandExists("bun")) {
1562
2023
  return "bun";
1563
2024
  }
1564
2025
  return "node";
1565
2026
  }
2027
+ function sampleWindowsPmProcessMetrics(pid) {
2028
+ const script = [
2029
+ '$ErrorActionPreference = "Stop"',
2030
+ `$sample = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfProc_Process -Filter "IDProcess = ${pid}" | Select-Object -First 1`,
2031
+ "if (-not $sample) { exit 2 }",
2032
+ "$cpu = [double]$sample.PercentProcessorTime",
2033
+ `$memory = if ($sample.PSObject.Properties.Match('WorkingSetPrivate').Count -gt 0) { [int64]$sample.WorkingSetPrivate } else { [int64](Get-Process -Id ${pid} -ErrorAction Stop).WorkingSet64 }`,
2034
+ 'Write-Output ($cpu.ToString([System.Globalization.CultureInfo]::InvariantCulture) + "," + $memory)'
2035
+ ].join("; ");
2036
+ const result = (0, import_node_child_process.spawnSync)(
2037
+ "powershell.exe",
2038
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
2039
+ {
2040
+ encoding: "utf8",
2041
+ stdio: ["ignore", "pipe", "ignore"],
2042
+ windowsHide: true
2043
+ }
2044
+ );
2045
+ if (result.error || result.status !== 0) {
2046
+ return {};
2047
+ }
2048
+ const [cpuText, memoryText] = result.stdout.trim().split(",");
2049
+ const cpuPercent = Number.parseFloat((cpuText ?? "").replace(",", "."));
2050
+ const memoryRssBytes = Number.parseInt(memoryText ?? "", 10);
2051
+ return {
2052
+ cpuPercent: Number.isFinite(cpuPercent) ? cpuPercent : void 0,
2053
+ memoryRssBytes: Number.isFinite(memoryRssBytes) ? memoryRssBytes : void 0
2054
+ };
2055
+ }
2056
+ function samplePosixPmProcessMetrics(pid) {
2057
+ const result = (0, import_node_child_process.spawnSync)(
2058
+ "ps",
2059
+ ["-p", String(pid), "-o", "%cpu=", "-o", "rss="],
2060
+ {
2061
+ encoding: "utf8",
2062
+ stdio: ["ignore", "pipe", "ignore"],
2063
+ windowsHide: true
2064
+ }
2065
+ );
2066
+ if (result.error || result.status !== 0) {
2067
+ return {};
2068
+ }
2069
+ const [cpuText, memoryText] = result.stdout.trim().split(/\s+/, 2);
2070
+ const cpuPercent = Number.parseFloat((cpuText ?? "").replace(",", "."));
2071
+ const rssKilobytes = Number.parseInt(memoryText ?? "", 10);
2072
+ return {
2073
+ cpuPercent: Number.isFinite(cpuPercent) ? cpuPercent : void 0,
2074
+ memoryRssBytes: Number.isFinite(rssKilobytes) ? rssKilobytes * 1024 : void 0
2075
+ };
2076
+ }
2077
+ function samplePmProcessMetrics(pid) {
2078
+ return process.platform === "win32" ? sampleWindowsPmProcessMetrics(pid) : samplePosixPmProcessMetrics(pid);
2079
+ }
1566
2080
  function isPmOnlineWapkRecord(record) {
1567
2081
  return record.type === "wapk" && isPmWapkOnlineRunConfig(record.wapkRun);
1568
2082
  }
1569
2083
  function buildPmCommand(record) {
2084
+ if ((record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "inherit" && record.type !== "file") {
2085
+ throw new Error('pm proxy strategy "inherit" currently supports only Node .js/.mjs/.cjs file targets.');
2086
+ }
1570
2087
  if (record.type === "script") {
1571
2088
  return {
1572
2089
  command: record.script,
@@ -1638,9 +2155,23 @@ function buildPmCommand(record) {
1638
2155
  }
1639
2156
  const executable = preferCurrentExecutable("node");
1640
2157
  ensureCommandAvailable(executable, "Node.js runtime");
2158
+ const wantsInheritedListener = (record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "inherit";
2159
+ if (wantsInheritedListener && !supportsPmInheritedListener(record, runtime2)) {
2160
+ throw new Error('pm proxy strategy "inherit" currently supports only Node .js/.mjs/.cjs file targets.');
2161
+ }
2162
+ const bootstrapPath = wantsInheritedListener ? resolvePmNodeSharedListenerBootstrapPath() : void 0;
2163
+ if (wantsInheritedListener && !bootstrapPath) {
2164
+ throw new Error("Unable to resolve the PM shared-listener bootstrap module.");
2165
+ }
1641
2166
  return {
1642
2167
  command: executable,
1643
2168
  args: [record.file],
2169
+ env: bootstrapPath ? {
2170
+ NODE_OPTIONS: appendNodeOption(process.env.NODE_OPTIONS, `--require=${bootstrapPath}`),
2171
+ ELIT_PM_LISTEN_MODE: "ipc",
2172
+ ELIT_PM_PUBLIC_PORT: String(record.proxy?.port ?? record.env.PORT ?? "")
2173
+ } : void 0,
2174
+ ipc: Boolean(bootstrapPath),
1644
2175
  runtime: runtime2,
1645
2176
  preview: `${(0, import_node_path5.basename)(executable)} ${quoteCommandSegment(record.file)}`
1646
2177
  };
@@ -1652,20 +2183,33 @@ function createRecordFromDefinition(definition, paths, existing) {
1652
2183
  return {
1653
2184
  id,
1654
2185
  name: definition.name,
2186
+ baseName: definition.baseName,
2187
+ instanceIndex: definition.instanceIndex,
2188
+ instances: definition.instances,
1655
2189
  type: definition.type,
1656
2190
  source: definition.source,
1657
2191
  cwd: definition.cwd,
1658
2192
  runtime: definition.runtime,
1659
2193
  env: definition.env,
2194
+ proxy: definition.proxy,
1660
2195
  script: definition.script,
1661
2196
  file: definition.file,
1662
2197
  wapk: definition.wapk,
1663
2198
  wapkRun: definition.wapkRun,
1664
2199
  autorestart: definition.autorestart,
1665
2200
  restartDelay: definition.restartDelay,
2201
+ killTimeout: definition.killTimeout,
1666
2202
  maxRestarts: definition.maxRestarts,
1667
2203
  password: definition.password,
1668
2204
  restartPolicy: definition.restartPolicy,
2205
+ maxMemoryBytes: definition.maxMemoryBytes,
2206
+ memoryAction: definition.memoryAction,
2207
+ cronRestart: definition.cronRestart,
2208
+ expBackoffRestartDelay: definition.expBackoffRestartDelay,
2209
+ expBackoffRestartMaxDelay: definition.expBackoffRestartMaxDelay,
2210
+ restartWindow: definition.restartWindow,
2211
+ waitReady: definition.waitReady,
2212
+ listenTimeout: definition.listenTimeout,
1669
2213
  minUptime: definition.minUptime,
1670
2214
  watch: definition.watch,
1671
2215
  watchPaths: definition.watchPaths,
@@ -1681,16 +2225,20 @@ function createRecordFromDefinition(definition, paths, existing) {
1681
2225
  stoppedAt: void 0,
1682
2226
  runnerPid: void 0,
1683
2227
  childPid: void 0,
2228
+ proxyTargetPort: void 0,
1684
2229
  restartCount: existing?.restartCount ?? 0,
2230
+ reloadRequestedAt: void 0,
2231
+ lastRestartAt: existing?.lastRestartAt,
1685
2232
  lastExitCode: existing?.lastExitCode,
1686
2233
  error: void 0,
2234
+ proxyReadyAt: void 0,
1687
2235
  logFiles: existing?.logFiles ?? {
1688
2236
  out: (0, import_node_path5.join)(paths.logsDir, `${id}.out.log`),
1689
2237
  err: (0, import_node_path5.join)(paths.logsDir, `${id}.err.log`)
1690
2238
  }
1691
2239
  };
1692
2240
  }
1693
- function terminateProcessTree(pid) {
2241
+ function terminateProcessTree(pid, options) {
1694
2242
  if (process.platform === "win32") {
1695
2243
  const result = (0, import_node_child_process.spawnSync)("taskkill", ["/PID", String(pid), "/T", "/F"], {
1696
2244
  stdio: "ignore",
@@ -1702,7 +2250,17 @@ function terminateProcessTree(pid) {
1702
2250
  return;
1703
2251
  }
1704
2252
  try {
1705
- process.kill(pid, "SIGTERM");
2253
+ process.kill(pid, options?.force ? "SIGKILL" : "SIGTERM");
2254
+ } catch (error) {
2255
+ const code = error.code;
2256
+ if (code !== "ESRCH") {
2257
+ throw error;
2258
+ }
2259
+ }
2260
+ }
2261
+ function sendPmSignal(pid, signal) {
2262
+ try {
2263
+ process.kill(pid, signal);
1706
2264
  } catch (error) {
1707
2265
  const code = error.code;
1708
2266
  if (code !== "ESRCH") {
@@ -1754,6 +2312,7 @@ async function startManagedProcess(definition, paths) {
1754
2312
 
1755
2313
  // src/cli/pm/runner.ts
1756
2314
  var import_node_child_process2 = require("child_process");
2315
+ var import_node_http2 = require("http");
1757
2316
  var import_node_fs4 = require("fs");
1758
2317
  var import_node_os = require("os");
1759
2318
  var import_node_path6 = require("path");
@@ -1987,13 +2546,13 @@ var win32 = createPathOps(true);
1987
2546
  function join5(...paths) {
1988
2547
  return joinPaths(paths, isWindows);
1989
2548
  }
1990
- function resolve4(...paths) {
2549
+ function resolve5(...paths) {
1991
2550
  return resolvePaths(paths, isWindows);
1992
2551
  }
1993
2552
  function relative(from, to) {
1994
2553
  return relativePath(from, to, isWindows);
1995
2554
  }
1996
- function dirname2(path) {
2555
+ function dirname3(path) {
1997
2556
  return getDirname(path, isWindows);
1998
2557
  }
1999
2558
 
@@ -2031,14 +2590,14 @@ function getBaseDirectory(pattern) {
2031
2590
  }
2032
2591
  if (normalizedPattern && existsSync4(normalizedPattern)) {
2033
2592
  try {
2034
- return statSync2(normalizedPattern).isDirectory() ? normalizedPattern : normalizePath2(dirname2(normalizedPattern)) || ".";
2593
+ return statSync2(normalizedPattern).isDirectory() ? normalizedPattern : normalizePath2(dirname3(normalizedPattern)) || ".";
2035
2594
  } catch {
2036
- return normalizePath2(dirname2(normalizedPattern)) || ".";
2595
+ return normalizePath2(dirname3(normalizedPattern)) || ".";
2037
2596
  }
2038
2597
  }
2039
2598
  const lastSegment = parts[parts.length - 1] || "";
2040
2599
  if (lastSegment.includes(".") && !lastSegment.startsWith(".")) {
2041
- return normalizePath2(dirname2(normalizedPattern)) || ".";
2600
+ return normalizePath2(dirname3(normalizedPattern)) || ".";
2042
2601
  }
2043
2602
  return normalizedPattern || ".";
2044
2603
  }
@@ -2233,84 +2792,877 @@ function watch(paths, options) {
2233
2792
  return watcher;
2234
2793
  }
2235
2794
 
2236
- // src/cli/pm/runner.ts
2237
- function writePmLog(stream, message) {
2238
- stream.write(`[elit pm] ${(/* @__PURE__ */ new Date()).toISOString()} ${message}${import_node_os.EOL}`);
2239
- }
2240
- function waitForExit(code, signal) {
2241
- if (typeof code === "number") {
2242
- return code;
2795
+ // src/cli/pm/proxy.ts
2796
+ var import_node_http = require("http");
2797
+ var import_node_https = require("https");
2798
+ var import_node_net = require("net");
2799
+ var import_promises = require("dns/promises");
2800
+ var BLOCKED_IPV4_PREFIXES = [
2801
+ "0.",
2802
+ "10.",
2803
+ "100.64.",
2804
+ "100.65.",
2805
+ "100.66.",
2806
+ "100.67.",
2807
+ "100.68.",
2808
+ "100.69.",
2809
+ "100.70.",
2810
+ "100.71.",
2811
+ "100.72.",
2812
+ "100.73.",
2813
+ "100.74.",
2814
+ "100.75.",
2815
+ "100.76.",
2816
+ "100.77.",
2817
+ "100.78.",
2818
+ "100.79.",
2819
+ "100.80.",
2820
+ "100.81.",
2821
+ "100.82.",
2822
+ "100.83.",
2823
+ "100.84.",
2824
+ "100.85.",
2825
+ "100.86.",
2826
+ "100.87.",
2827
+ "100.88.",
2828
+ "100.89.",
2829
+ "100.90.",
2830
+ "100.91.",
2831
+ "100.92.",
2832
+ "100.93.",
2833
+ "100.94.",
2834
+ "100.95.",
2835
+ "100.96.",
2836
+ "100.97.",
2837
+ "100.98.",
2838
+ "100.99.",
2839
+ "100.100.",
2840
+ "100.101.",
2841
+ "100.102.",
2842
+ "100.103.",
2843
+ "100.104.",
2844
+ "100.105.",
2845
+ "100.106.",
2846
+ "100.107.",
2847
+ "100.108.",
2848
+ "100.109.",
2849
+ "100.110.",
2850
+ "100.111.",
2851
+ "100.112.",
2852
+ "100.113.",
2853
+ "100.114.",
2854
+ "100.115.",
2855
+ "100.116.",
2856
+ "100.117.",
2857
+ "100.118.",
2858
+ "100.119.",
2859
+ "100.120.",
2860
+ "100.121.",
2861
+ "100.122.",
2862
+ "100.123.",
2863
+ "100.124.",
2864
+ "100.125.",
2865
+ "100.126.",
2866
+ "100.127.",
2867
+ "127.",
2868
+ "169.254.",
2869
+ "172.16.",
2870
+ "172.17.",
2871
+ "172.18.",
2872
+ "172.19.",
2873
+ "172.20.",
2874
+ "172.21.",
2875
+ "172.22.",
2876
+ "172.23.",
2877
+ "172.24.",
2878
+ "172.25.",
2879
+ "172.26.",
2880
+ "172.27.",
2881
+ "172.28.",
2882
+ "172.29.",
2883
+ "172.30.",
2884
+ "172.31.",
2885
+ "192.0.2.",
2886
+ "192.88.99.",
2887
+ "192.168.",
2888
+ "198.18.",
2889
+ "198.19.",
2890
+ "198.51.100.",
2891
+ "203.0.113.",
2892
+ "224.",
2893
+ "225.",
2894
+ "226.",
2895
+ "227.",
2896
+ "228.",
2897
+ "229.",
2898
+ "230.",
2899
+ "231.",
2900
+ "232.",
2901
+ "233.",
2902
+ "234.",
2903
+ "235.",
2904
+ "236.",
2905
+ "237.",
2906
+ "238.",
2907
+ "239.",
2908
+ "240.",
2909
+ "241.",
2910
+ "242.",
2911
+ "243.",
2912
+ "244.",
2913
+ "245.",
2914
+ "246.",
2915
+ "247.",
2916
+ "248.",
2917
+ "249.",
2918
+ "250.",
2919
+ "251.",
2920
+ "252.",
2921
+ "253.",
2922
+ "254.",
2923
+ "255."
2924
+ ];
2925
+ var ALLOWED_PROXY_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
2926
+ function isBlockedIpv4(hostname) {
2927
+ const octets = hostname.split(".");
2928
+ if (octets.length !== 4) return false;
2929
+ const joined = hostname;
2930
+ return BLOCKED_IPV4_PREFIXES.some((prefix) => joined.startsWith(prefix));
2931
+ }
2932
+ function isBlockedIpv6(hostname) {
2933
+ const lower = hostname.toLowerCase();
2934
+ if (lower === "::1" || lower === "::" || lower === "0:0:0:0:0:0:0:1" || lower === "0:0:0:0:0:0:0:0") {
2935
+ return true;
2243
2936
  }
2244
- if (signal === "SIGINT" || signal === "SIGTERM") {
2245
- return 0;
2937
+ const ffffMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
2938
+ if (ffffMatch) {
2939
+ return isBlockedIpv4(ffffMatch[1]);
2246
2940
  }
2247
- return 1;
2941
+ const compatMatch = lower.match(/^::(\d+\.\d+\.\d+\.\d+)$/);
2942
+ if (compatMatch) {
2943
+ return isBlockedIpv4(compatMatch[1]);
2944
+ }
2945
+ return false;
2248
2946
  }
2249
- async function delay(milliseconds) {
2250
- await new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
2947
+ function isSafeHostname(hostname) {
2948
+ if (!hostname) return false;
2949
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) return !isBlockedIpv4(hostname);
2950
+ if (/^\[.*\]$/.test(hostname)) return !isBlockedIpv6(hostname.slice(1, -1));
2951
+ if (hostname.includes(":")) return !isBlockedIpv6(hostname);
2952
+ return true;
2251
2953
  }
2252
- async function waitForProcessTermination(pid, timeoutMs) {
2253
- if (!pid || !isProcessAlive(pid)) {
2254
- return true;
2954
+ async function safeResolveHostname(hostname) {
2955
+ try {
2956
+ const result = await (0, import_promises.lookup)(hostname);
2957
+ const ip = result.address;
2958
+ if (isBlockedIpv4(ip) || isBlockedIpv6(ip)) {
2959
+ throw new Error(`PM proxy target resolved to a blocked address: ${ip}`);
2960
+ }
2961
+ return ip;
2962
+ } catch (error) {
2963
+ if (error instanceof Error && error.message.includes("blocked address")) {
2964
+ throw error;
2965
+ }
2966
+ throw new Error(`PM proxy failed to resolve target hostname "${hostname}": ${error instanceof Error ? error.message : String(error)}`);
2255
2967
  }
2256
- const deadline = Date.now() + timeoutMs;
2257
- while (Date.now() < deadline) {
2258
- if (!isProcessAlive(pid)) {
2259
- return true;
2968
+ }
2969
+ async function validateProxyTargetUrl(target) {
2970
+ if (!ALLOWED_PROXY_PROTOCOLS.has(target.protocol)) {
2971
+ throw new Error(`PM proxy target protocol "${target.protocol}" is not allowed. Only http: and https: are permitted.`);
2972
+ }
2973
+ const hostname = target.hostname;
2974
+ if (!isSafeHostname(hostname)) {
2975
+ throw new Error(`PM proxy target "${hostname}" resolves to a blocked address and is not allowed.`);
2976
+ }
2977
+ if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) && !/^\[.*\]$/.test(hostname)) {
2978
+ await safeResolveHostname(hostname);
2979
+ }
2980
+ }
2981
+ function sanitizeProxyRequestPath(requestUrl) {
2982
+ if (!requestUrl || requestUrl === "/") return "/";
2983
+ try {
2984
+ const normalizedInput = requestUrl.replace(/\\/g, "/");
2985
+ const parsed = new URL(normalizedInput, "http://placeholder");
2986
+ if (parsed.username || parsed.password || parsed.hostname !== "placeholder" || parsed.port) {
2987
+ return "/";
2260
2988
  }
2261
- await delay(DEFAULT_PM_STOP_POLL_MS);
2989
+ const pathname = parsed.pathname || "/";
2990
+ let decodedPathname = pathname;
2991
+ try {
2992
+ decodedPathname = decodeURIComponent(pathname);
2993
+ } catch {
2994
+ return "/";
2995
+ }
2996
+ const lowerPath = pathname.toLowerCase();
2997
+ if (lowerPath.includes("%2f") || lowerPath.includes("%5c") || lowerPath.includes("%40") || lowerPath.includes("%00")) {
2998
+ return "/";
2999
+ }
3000
+ const segments = decodedPathname.split("/");
3001
+ if (segments.some((segment) => segment === "." || segment === "..")) {
3002
+ return "/";
3003
+ }
3004
+ const sanitized = pathname + parsed.search;
3005
+ return sanitized.startsWith("/") ? sanitized || "/" : `/${sanitized}`;
3006
+ } catch {
3007
+ return "/";
2262
3008
  }
2263
- return !isProcessAlive(pid);
2264
3009
  }
2265
- async function waitForManagedChildExit(child) {
2266
- return await new Promise((resolvePromise) => {
2267
- let resolved = false;
2268
- child.once("error", (error) => {
2269
- if (resolved) {
2270
- return;
2271
- }
2272
- resolved = true;
2273
- resolvePromise({ code: 1, signal: null, error: error instanceof Error ? error.message : String(error) });
2274
- });
2275
- child.once("close", (code, signal) => {
2276
- if (resolved) {
3010
+ function resolvePmProxyHost(proxy) {
3011
+ return proxy.host?.trim() || "0.0.0.0";
3012
+ }
3013
+ function resolvePmProxyTargetHost(proxy) {
3014
+ return proxy.targetHost?.trim() || "127.0.0.1";
3015
+ }
3016
+ function resolvePmProxyEnvVar(proxy) {
3017
+ return proxy.envVar?.trim() || "PORT";
3018
+ }
3019
+ function buildPmProxyTargetUrl(proxy, targetPort) {
3020
+ return `http://${resolvePmProxyTargetHost(proxy)}:${targetPort}`;
3021
+ }
3022
+ function rewritePmProxyHealthCheckUrl(url, targetHost, targetPort) {
3023
+ const targetUrl = new URL(url);
3024
+ targetUrl.hostname = targetHost;
3025
+ targetUrl.port = String(targetPort);
3026
+ return targetUrl.toString();
3027
+ }
3028
+ async function allocatePmProxyTargetPort(host = "127.0.0.1") {
3029
+ const server = (0, import_node_net.createServer)();
3030
+ return await new Promise((resolve7, reject) => {
3031
+ server.once("error", reject);
3032
+ server.listen(0, host, () => {
3033
+ const address = server.address();
3034
+ if (!address || typeof address === "string") {
3035
+ server.close(() => reject(new Error("Failed to allocate an internal PM proxy port.")));
2277
3036
  return;
2278
3037
  }
2279
- resolved = true;
2280
- resolvePromise({ code, signal });
3038
+ const port = address.port;
3039
+ server.close((error) => {
3040
+ if (error) {
3041
+ reject(error);
3042
+ return;
3043
+ }
3044
+ resolve7(port);
3045
+ });
2281
3046
  });
2282
3047
  });
2283
3048
  }
2284
- async function createPmWatchController(record, onChange, onError) {
2285
- if (!record.watch || record.watchPaths.length === 0) {
2286
- return {
2287
- async close() {
3049
+ function buildPmProxyHeaders(headersInput, host) {
3050
+ const headers = {};
3051
+ for (const [key, value] of Object.entries(headersInput)) {
3052
+ if (value !== void 0) {
3053
+ headers[key] = value;
3054
+ }
3055
+ }
3056
+ headers.host = host;
3057
+ return headers;
3058
+ }
3059
+ function writeRawHttpResponse(socket, statusCode, statusMessage, headers) {
3060
+ const lines = [`HTTP/1.1 ${statusCode} ${statusMessage}`];
3061
+ for (const [key, value] of Object.entries(headers)) {
3062
+ if (value === void 0) {
3063
+ continue;
3064
+ }
3065
+ if (Array.isArray(value)) {
3066
+ for (const item of value) {
3067
+ lines.push(`${key}: ${item}`);
2288
3068
  }
2289
- };
3069
+ continue;
3070
+ }
3071
+ lines.push(`${key}: ${value}`);
2290
3072
  }
2291
- const watcher = watch(record.watchPaths);
2292
- let debounceTimer = null;
2293
- const scheduleRestart = (filePath) => {
2294
- const normalizedPath = filePath.replace(/\\/g, "/");
2295
- if (isIgnoredWatchPath(normalizedPath, record.watchIgnore)) {
3073
+ socket.write(`${lines.join("\r\n")}\r
3074
+ \r
3075
+ `);
3076
+ }
3077
+ async function createPmProxyController(proxy) {
3078
+ let targets = [];
3079
+ let nextTargetIndex = 0;
3080
+ const setResolvedTargets = (nextTargets) => {
3081
+ const unchanged = nextTargets.length === targets.length && nextTargets.every((target, index) => targets[index]?.href === target.href);
3082
+ targets = nextTargets;
3083
+ if (targets.length === 0) {
3084
+ nextTargetIndex = 0;
2296
3085
  return;
2297
3086
  }
2298
- if (debounceTimer) {
2299
- clearTimeout(debounceTimer);
3087
+ if (!unchanged) {
3088
+ nextTargetIndex = nextTargetIndex % targets.length;
2300
3089
  }
2301
- debounceTimer = setTimeout(() => {
2302
- debounceTimer = null;
2303
- onChange(normalizedPath);
2304
- }, record.watchDebounce);
2305
- debounceTimer.unref?.();
2306
3090
  };
2307
- watcher.on("add", scheduleRestart);
2308
- watcher.on("change", scheduleRestart);
2309
- watcher.on("unlink", scheduleRestart);
2310
- watcher.on("error", (error) => onError(error instanceof Error ? error.message : String(error)));
2311
- return {
2312
- async close() {
2313
- if (debounceTimer) {
3091
+ const pickTarget = () => {
3092
+ if (targets.length === 0) {
3093
+ return null;
3094
+ }
3095
+ const target = targets[nextTargetIndex % targets.length];
3096
+ nextTargetIndex = (nextTargetIndex + 1) % targets.length;
3097
+ return target;
3098
+ };
3099
+ const validateTarget = async (target) => {
3100
+ await validateProxyTargetUrl(target);
3101
+ };
3102
+ const server = (0, import_node_http.createServer)((req, res) => {
3103
+ const target = pickTarget();
3104
+ if (!target) {
3105
+ res.statusCode = 503;
3106
+ res.end("PM proxy target is not ready.");
3107
+ return;
3108
+ }
3109
+ const sanitizedPath = sanitizeProxyRequestPath(req.url || "/");
3110
+ const requestLib = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
3111
+ const headers = buildPmProxyHeaders(req.headers, target.host);
3112
+ if (!ALLOWED_PROXY_PROTOCOLS.has(target.protocol)) {
3113
+ res.statusCode = 400;
3114
+ res.end("PM proxy rejected unsafe target protocol.");
3115
+ return;
3116
+ }
3117
+ validateTarget(target).then(() => {
3118
+ const proxyReq = requestLib({
3119
+ protocol: target.protocol,
3120
+ hostname: target.hostname,
3121
+ port: target.port || void 0,
3122
+ path: sanitizedPath,
3123
+ method: req.method,
3124
+ headers
3125
+ }, (proxyRes) => {
3126
+ const outgoingHeaders = {};
3127
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
3128
+ if (value !== void 0) {
3129
+ outgoingHeaders[key] = value;
3130
+ }
3131
+ }
3132
+ res.writeHead(proxyRes.statusCode || 200, outgoingHeaders);
3133
+ proxyRes.pipe(res);
3134
+ });
3135
+ proxyReq.on("error", (error) => {
3136
+ if (!res.headersSent) {
3137
+ res.statusCode = 502;
3138
+ }
3139
+ res.end(`PM proxy error: ${error.message}`);
3140
+ });
3141
+ req.pipe(proxyReq);
3142
+ }).catch((error) => {
3143
+ if (!res.headersSent) {
3144
+ res.statusCode = 403;
3145
+ }
3146
+ res.end(`PM proxy blocked target: ${error instanceof Error ? error.message : String(error)}`);
3147
+ });
3148
+ });
3149
+ server.on("upgrade", (req, socket, head) => {
3150
+ const target = pickTarget();
3151
+ if (!target) {
3152
+ writeRawHttpResponse(socket, 503, "Service Unavailable", {
3153
+ connection: "close",
3154
+ "content-length": 0
3155
+ });
3156
+ socket.destroy();
3157
+ return;
3158
+ }
3159
+ const sanitizedPath = sanitizeProxyRequestPath(req.url || "/");
3160
+ const requestLib = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
3161
+ const targetUrl = new URL(sanitizedPath, target);
3162
+ if (targetUrl.hostname !== target.hostname || targetUrl.port !== target.port || !ALLOWED_PROXY_PROTOCOLS.has(targetUrl.protocol)) {
3163
+ writeRawHttpResponse(socket, 400, "Bad Request", {
3164
+ connection: "close",
3165
+ "content-length": 0
3166
+ });
3167
+ socket.destroy();
3168
+ return;
3169
+ }
3170
+ validateTarget(target).then(() => {
3171
+ const proxyReq = requestLib(targetUrl, {
3172
+ method: req.method,
3173
+ headers: buildPmProxyHeaders(req.headers, target.host)
3174
+ });
3175
+ proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
3176
+ writeRawHttpResponse(socket, proxyRes.statusCode || 101, proxyRes.statusMessage || "Switching Protocols", proxyRes.headers);
3177
+ if (head.length > 0) {
3178
+ proxySocket.write(head);
3179
+ }
3180
+ if (proxyHead.length > 0) {
3181
+ socket.write(proxyHead);
3182
+ }
3183
+ socket.on("error", () => proxySocket.destroy());
3184
+ proxySocket.on("error", () => socket.destroy());
3185
+ proxySocket.pipe(socket);
3186
+ socket.pipe(proxySocket);
3187
+ });
3188
+ proxyReq.on("response", (proxyRes) => {
3189
+ writeRawHttpResponse(socket, proxyRes.statusCode || 502, proxyRes.statusMessage || "Bad Gateway", proxyRes.headers);
3190
+ proxyRes.pipe(socket);
3191
+ });
3192
+ proxyReq.on("error", (error) => {
3193
+ writeRawHttpResponse(socket, 502, "Bad Gateway", {
3194
+ connection: "close",
3195
+ "content-type": "text/plain; charset=utf-8",
3196
+ "content-length": Buffer.byteLength(`PM proxy error: ${error.message}`)
3197
+ });
3198
+ socket.end(`PM proxy error: ${error.message}`);
3199
+ });
3200
+ proxyReq.end();
3201
+ }).catch((error) => {
3202
+ writeRawHttpResponse(socket, 403, "Forbidden", {
3203
+ connection: "close",
3204
+ "content-type": "text/plain; charset=utf-8",
3205
+ "content-length": Buffer.byteLength(`PM proxy blocked target: ${error instanceof Error ? error.message : String(error)}`)
3206
+ });
3207
+ socket.end(`PM proxy blocked target: ${error instanceof Error ? error.message : String(error)}`);
3208
+ });
3209
+ });
3210
+ await new Promise((resolve7, reject) => {
3211
+ server.once("error", reject);
3212
+ server.listen(proxy.port, resolvePmProxyHost(proxy), () => resolve7());
3213
+ });
3214
+ return {
3215
+ setTarget(targetUrl) {
3216
+ if (targetUrl) {
3217
+ const parsed = new URL(targetUrl);
3218
+ validateProxyTargetUrl(parsed).then(() => {
3219
+ setResolvedTargets([parsed]);
3220
+ }).catch((error) => {
3221
+ console.error(`[PM proxy] Blocked setTarget: ${error instanceof Error ? error.message : String(error)}`);
3222
+ setResolvedTargets([]);
3223
+ });
3224
+ } else {
3225
+ setResolvedTargets([]);
3226
+ }
3227
+ },
3228
+ setTargets(targetUrls) {
3229
+ Promise.all(targetUrls.map((url) => validateProxyTargetUrl(new URL(url)))).then(() => {
3230
+ setResolvedTargets(targetUrls.map((targetUrl) => new URL(targetUrl)));
3231
+ }).catch((error) => {
3232
+ console.error(`[PM proxy] Blocked setTargets: ${error instanceof Error ? error.message : String(error)}`);
3233
+ setResolvedTargets([]);
3234
+ });
3235
+ },
3236
+ close() {
3237
+ return new Promise((resolve7, reject) => {
3238
+ server.close((error) => {
3239
+ if (error) {
3240
+ reject(error);
3241
+ return;
3242
+ }
3243
+ resolve7();
3244
+ });
3245
+ });
3246
+ }
3247
+ };
3248
+ }
3249
+
3250
+ // src/cli/pm/runner.ts
3251
+ function writePmLog(stream, message) {
3252
+ if (stream.writableEnded || stream.destroyed) {
3253
+ return;
3254
+ }
3255
+ try {
3256
+ stream.write(`[elit pm] ${(/* @__PURE__ */ new Date()).toISOString()} ${message}${import_node_os.EOL}`);
3257
+ } catch (error) {
3258
+ if (error.code !== "ERR_STREAM_WRITE_AFTER_END") {
3259
+ throw error;
3260
+ }
3261
+ }
3262
+ }
3263
+ function waitForExit(code, signal) {
3264
+ if (typeof code === "number") {
3265
+ return code;
3266
+ }
3267
+ if (signal === "SIGINT" || signal === "SIGTERM") {
3268
+ return 0;
3269
+ }
3270
+ return 1;
3271
+ }
3272
+ async function delay(milliseconds) {
3273
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
3274
+ }
3275
+ function resolveRunnerPathsFromRecordFile(filePath) {
3276
+ const appsDir = (0, import_node_path6.dirname)(filePath);
3277
+ const dataDir = (0, import_node_path6.dirname)(appsDir);
3278
+ return {
3279
+ dataDir,
3280
+ appsDir,
3281
+ logsDir: (0, import_node_path6.join)(dataDir, "logs"),
3282
+ dumpFile: (0, import_node_path6.join)(dataDir, DEFAULT_PM_DUMP_FILE)
3283
+ };
3284
+ }
3285
+ function usesPmProxyController(record) {
3286
+ return Boolean(record.proxy) && (record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "proxy";
3287
+ }
3288
+ function usesPmInheritedListener(record) {
3289
+ return Boolean(record.proxy) && (record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "inherit";
3290
+ }
3291
+ function isPmProxyOwner(record) {
3292
+ return usesPmProxyController(record) && record.instanceIndex === 1;
3293
+ }
3294
+ function resolvePmProxyTargetUrls(paths, baseName) {
3295
+ return listPmRecordMatches(paths).filter((match) => match.record.baseName === baseName).filter((match) => usesPmProxyController(match.record)).filter((match) => match.record.desiredState === "running" && Boolean(match.record.proxyReadyAt)).filter((match) => typeof match.record.proxyTargetPort === "number" && match.record.proxyTargetPort > 0).sort((left, right) => left.record.instanceIndex - right.record.instanceIndex).map((match) => buildPmProxyTargetUrl(match.record.proxy, match.record.proxyTargetPort));
3296
+ }
3297
+ async function createPmInheritedListener(proxy) {
3298
+ const server = (0, import_node_http2.createServer)();
3299
+ await new Promise((resolvePromise, reject) => {
3300
+ server.once("error", reject);
3301
+ server.listen(proxy.port, resolvePmProxyHost(proxy), () => resolvePromise());
3302
+ });
3303
+ return server;
3304
+ }
3305
+ function createPmChildIpcState(child, sharedListener) {
3306
+ let bootstrapReady = false;
3307
+ let listenerReady = false;
3308
+ let sharedHandleSent = false;
3309
+ const sendSharedHandle = () => {
3310
+ if (!sharedListener || !bootstrapReady || sharedHandleSent || !child.connected) {
3311
+ return;
3312
+ }
3313
+ sharedHandleSent = true;
3314
+ child.send?.({ type: "elit:pm:listen-handle" }, sharedListener);
3315
+ };
3316
+ const onMessage = (message) => {
3317
+ if (!message || typeof message !== "object") {
3318
+ return;
3319
+ }
3320
+ if (message.type === "elit:pm:bootstrap-ready") {
3321
+ bootstrapReady = true;
3322
+ sendSharedHandle();
3323
+ return;
3324
+ }
3325
+ if (message.type === "elit:pm:listener-ready") {
3326
+ listenerReady = true;
3327
+ }
3328
+ };
3329
+ child.on("message", onMessage);
3330
+ return {
3331
+ get bootstrapReady() {
3332
+ return bootstrapReady;
3333
+ },
3334
+ get listenerReady() {
3335
+ return listenerReady;
3336
+ },
3337
+ stop() {
3338
+ child.off("message", onMessage);
3339
+ }
3340
+ };
3341
+ }
3342
+ function buildPmChildEnv(record, command, targetPort) {
3343
+ return {
3344
+ ...process.env,
3345
+ ...record.env,
3346
+ ...command.env,
3347
+ ...usesPmProxyController(record) && targetPort ? {
3348
+ [resolvePmProxyEnvVar(record.proxy)]: String(targetPort),
3349
+ ELIT_PM_PUBLIC_PORT: String(record.proxy.port)
3350
+ } : {},
3351
+ ELIT_PM_NAME: record.name,
3352
+ ELIT_PM_ID: record.id
3353
+ };
3354
+ }
3355
+ function buildPmChildStdio(command, onlineStdinShutdownEnabled) {
3356
+ return [
3357
+ onlineStdinShutdownEnabled ? "pipe" : "ignore",
3358
+ "pipe",
3359
+ "pipe",
3360
+ ...command.ipc ? ["ipc"] : []
3361
+ ];
3362
+ }
3363
+ function createPmReadinessMonitor(record, onReady, onFailure, options) {
3364
+ if (options?.ipcController) {
3365
+ let stopped2 = false;
3366
+ let timer2 = null;
3367
+ let timeoutTimer2 = null;
3368
+ const clearTimers2 = () => {
3369
+ if (timer2) {
3370
+ clearInterval(timer2);
3371
+ timer2 = null;
3372
+ }
3373
+ if (timeoutTimer2) {
3374
+ clearTimeout(timeoutTimer2);
3375
+ timeoutTimer2 = null;
3376
+ }
3377
+ };
3378
+ const host = record.proxy?.host ?? "0.0.0.0";
3379
+ const port = record.proxy?.port ?? 0;
3380
+ timer2 = setInterval(() => {
3381
+ if (stopped2 || !options.ipcController?.listenerReady) {
3382
+ return;
3383
+ }
3384
+ stopped2 = true;
3385
+ clearTimers2();
3386
+ onReady(`shared listener ready on ${host}:${port}`);
3387
+ }, 25);
3388
+ timer2.unref?.();
3389
+ timeoutTimer2 = setTimeout(() => {
3390
+ if (stopped2) {
3391
+ return;
3392
+ }
3393
+ stopped2 = true;
3394
+ clearTimers2();
3395
+ onFailure(`listen timeout reached after ${record.listenTimeout}ms while waiting for shared listener ${host}:${port}`);
3396
+ }, record.listenTimeout);
3397
+ timeoutTimer2.unref?.();
3398
+ return {
3399
+ stop() {
3400
+ stopped2 = true;
3401
+ clearTimers2();
3402
+ }
3403
+ };
3404
+ }
3405
+ if (!record.waitReady || !record.healthCheck) {
3406
+ return {
3407
+ stop() {
3408
+ }
3409
+ };
3410
+ }
3411
+ const healthCheck = record.healthCheck;
3412
+ const pollInterval = Math.max(50, Math.min(healthCheck.interval, 250));
3413
+ let stopped = false;
3414
+ let timer = null;
3415
+ let timeoutTimer = null;
3416
+ let inFlight = false;
3417
+ const clearTimers = () => {
3418
+ if (timer) {
3419
+ clearInterval(timer);
3420
+ timer = null;
3421
+ }
3422
+ if (timeoutTimer) {
3423
+ clearTimeout(timeoutTimer);
3424
+ timeoutTimer = null;
3425
+ }
3426
+ };
3427
+ const runHealthCheck = async () => {
3428
+ if (stopped || inFlight) {
3429
+ return;
3430
+ }
3431
+ inFlight = true;
3432
+ const controller = new AbortController();
3433
+ const timeoutId = setTimeout(() => controller.abort(), healthCheck.timeout);
3434
+ timeoutId.unref?.();
3435
+ try {
3436
+ const response = await fetch(healthCheck.url, {
3437
+ method: "GET",
3438
+ signal: controller.signal
3439
+ });
3440
+ if (stopped) {
3441
+ return;
3442
+ }
3443
+ if (!response.ok) {
3444
+ throw new Error(`health check returned ${response.status}`);
3445
+ }
3446
+ stopped = true;
3447
+ clearTimers();
3448
+ onReady(`readiness check passed: ${healthCheck.url}`);
3449
+ } catch {
3450
+ } finally {
3451
+ clearTimeout(timeoutId);
3452
+ inFlight = false;
3453
+ }
3454
+ };
3455
+ timeoutTimer = setTimeout(() => {
3456
+ if (stopped) {
3457
+ return;
3458
+ }
3459
+ stopped = true;
3460
+ clearTimers();
3461
+ onFailure(`listen timeout reached after ${record.listenTimeout}ms while waiting for ${healthCheck.url}`);
3462
+ }, record.listenTimeout);
3463
+ timeoutTimer.unref?.();
3464
+ void runHealthCheck();
3465
+ timer = setInterval(() => {
3466
+ void runHealthCheck();
3467
+ }, pollInterval);
3468
+ timer.unref?.();
3469
+ return {
3470
+ stop() {
3471
+ stopped = true;
3472
+ clearTimers();
3473
+ }
3474
+ };
3475
+ }
3476
+ function resolvePmStopTimeout(record) {
3477
+ if (isPmOnlineWapkRecord(record)) {
3478
+ return Math.max(record.killTimeout, PM_WAPK_ONLINE_SHUTDOWN_TIMEOUT_MS);
3479
+ }
3480
+ return record.killTimeout;
3481
+ }
3482
+ function supportsPmProxyReload(record) {
3483
+ return Boolean(record.proxy) && record.instances === 1;
3484
+ }
3485
+ function buildPmMonitorRecord(record, targetPort) {
3486
+ if (!record.proxy || !targetPort) {
3487
+ return record;
3488
+ }
3489
+ const targetHost = resolvePmProxyTargetHost(record.proxy);
3490
+ return {
3491
+ ...record,
3492
+ proxyTargetPort: targetPort,
3493
+ healthCheck: record.healthCheck ? {
3494
+ ...record.healthCheck,
3495
+ url: rewritePmProxyHealthCheckUrl(record.healthCheck.url, targetHost, targetPort)
3496
+ } : void 0
3497
+ };
3498
+ }
3499
+ async function waitForProcessTermination(pid, timeoutMs) {
3500
+ if (!pid || !isProcessAlive(pid)) {
3501
+ return true;
3502
+ }
3503
+ const deadline = Date.now() + timeoutMs;
3504
+ while (Date.now() < deadline) {
3505
+ if (!isProcessAlive(pid)) {
3506
+ return true;
3507
+ }
3508
+ await delay(DEFAULT_PM_STOP_POLL_MS);
3509
+ }
3510
+ return !isProcessAlive(pid);
3511
+ }
3512
+ async function waitForManagedChildExit(child) {
3513
+ return await new Promise((resolvePromise) => {
3514
+ let resolved = false;
3515
+ child.once("error", (error) => {
3516
+ if (resolved) {
3517
+ return;
3518
+ }
3519
+ resolved = true;
3520
+ resolvePromise({ code: 1, signal: null, error: error instanceof Error ? error.message : String(error) });
3521
+ });
3522
+ child.once("close", (code, signal) => {
3523
+ if (resolved) {
3524
+ return;
3525
+ }
3526
+ resolved = true;
3527
+ resolvePromise({ code, signal });
3528
+ });
3529
+ });
3530
+ }
3531
+ async function createPmChildControllers(record, child, stdoutLog, stderrLog, requestPlannedRestart, onReady, options) {
3532
+ let healthMonitor = {
3533
+ stop() {
3534
+ }
3535
+ };
3536
+ let memoryMonitor = {
3537
+ stop() {
3538
+ }
3539
+ };
3540
+ let scheduleMonitor = {
3541
+ stop() {
3542
+ }
3543
+ };
3544
+ const startHealthMonitor = () => {
3545
+ healthMonitor = createPmHealthMonitor(
3546
+ record,
3547
+ (message) => requestPlannedRestart("health", message),
3548
+ (message) => writePmLog(stdoutLog, message)
3549
+ );
3550
+ };
3551
+ const needsReadySignal = Boolean(options?.ipcController) || record.waitReady;
3552
+ const readinessMonitor = options?.ready || !needsReadySignal ? { stop() {
3553
+ } } : createPmReadinessMonitor(
3554
+ record,
3555
+ (message) => {
3556
+ onReady(message);
3557
+ startHealthMonitor();
3558
+ },
3559
+ (message) => requestPlannedRestart("startup", message),
3560
+ { ipcController: options?.ipcController }
3561
+ );
3562
+ const watchController = await createPmWatchController(
3563
+ record,
3564
+ (changedPath) => requestPlannedRestart("watch", changedPath),
3565
+ (message) => writePmLog(stderrLog, `watch error: ${message}`)
3566
+ );
3567
+ memoryMonitor = createPmMemoryMonitor(
3568
+ record,
3569
+ child.pid,
3570
+ (kind, message) => requestPlannedRestart(kind, message)
3571
+ );
3572
+ scheduleMonitor = createPmScheduleMonitor(
3573
+ record,
3574
+ (message) => requestPlannedRestart("cron", message),
3575
+ (message) => writePmLog(stdoutLog, message)
3576
+ );
3577
+ if (options?.ready || !needsReadySignal) {
3578
+ startHealthMonitor();
3579
+ }
3580
+ return {
3581
+ async stop() {
3582
+ await watchController.close();
3583
+ readinessMonitor.stop();
3584
+ healthMonitor.stop();
3585
+ memoryMonitor.stop();
3586
+ scheduleMonitor.stop();
3587
+ }
3588
+ };
3589
+ }
3590
+ async function waitForPmChildReady(record, child, ipcController) {
3591
+ if (!ipcController && (!record.waitReady || !record.healthCheck)) {
3592
+ return { ready: true };
3593
+ }
3594
+ let readyMessage;
3595
+ let failureMessage;
3596
+ let exitResult;
3597
+ const readinessMonitor = createPmReadinessMonitor(
3598
+ record,
3599
+ (message) => {
3600
+ readyMessage = message;
3601
+ },
3602
+ (message) => {
3603
+ failureMessage = message;
3604
+ },
3605
+ { ipcController }
3606
+ );
3607
+ void waitForManagedChildExit(child).then((result) => {
3608
+ exitResult = result;
3609
+ });
3610
+ while (!readyMessage && !failureMessage && !exitResult) {
3611
+ await delay(25);
3612
+ }
3613
+ readinessMonitor.stop();
3614
+ if (readyMessage) {
3615
+ return { ready: true, message: readyMessage };
3616
+ }
3617
+ return {
3618
+ ready: false,
3619
+ message: failureMessage,
3620
+ exitResult
3621
+ };
3622
+ }
3623
+ async function stopProxyManagedChild(child, record, stderrLog) {
3624
+ if (!child.pid || !isProcessAlive(child.pid)) {
3625
+ return;
3626
+ }
3627
+ terminateProcessTree(child.pid);
3628
+ const stopTimeout = resolvePmStopTimeout(record);
3629
+ const stopped = await waitForProcessTermination(child.pid, stopTimeout);
3630
+ if (!stopped && child.pid && isProcessAlive(child.pid)) {
3631
+ writePmLog(stderrLog, `proxy handoff shutdown timed out after ${stopTimeout}ms; forcing process termination`);
3632
+ terminateProcessTree(child.pid, { force: true });
3633
+ await waitForProcessTermination(child.pid, DEFAULT_PM_STOP_POLL_MS);
3634
+ }
3635
+ }
3636
+ async function createPmWatchController(record, onChange, onError) {
3637
+ if (!record.watch || record.watchPaths.length === 0) {
3638
+ return {
3639
+ async close() {
3640
+ }
3641
+ };
3642
+ }
3643
+ const watcher = watch(record.watchPaths);
3644
+ let debounceTimer = null;
3645
+ const scheduleRestart = (filePath) => {
3646
+ const normalizedPath = filePath.replace(/\\/g, "/");
3647
+ if (isIgnoredWatchPath(normalizedPath, record.watchIgnore)) {
3648
+ return;
3649
+ }
3650
+ if (debounceTimer) {
3651
+ clearTimeout(debounceTimer);
3652
+ }
3653
+ debounceTimer = setTimeout(() => {
3654
+ debounceTimer = null;
3655
+ onChange(normalizedPath);
3656
+ }, record.watchDebounce);
3657
+ debounceTimer.unref?.();
3658
+ };
3659
+ watcher.on("add", scheduleRestart);
3660
+ watcher.on("change", scheduleRestart);
3661
+ watcher.on("unlink", scheduleRestart);
3662
+ watcher.on("error", (error) => onError(error instanceof Error ? error.message : String(error)));
3663
+ return {
3664
+ async close() {
3665
+ if (debounceTimer) {
2314
3666
  clearTimeout(debounceTimer);
2315
3667
  debounceTimer = null;
2316
3668
  }
@@ -2344,11 +3696,17 @@ function createPmHealthMonitor(record, onFailure, onLog) {
2344
3696
  method: "GET",
2345
3697
  signal: controller.signal
2346
3698
  });
3699
+ if (stopped) {
3700
+ return;
3701
+ }
2347
3702
  if (!response.ok) {
2348
3703
  throw new Error(`health check returned ${response.status}`);
2349
3704
  }
2350
3705
  failureCount = 0;
2351
3706
  } catch (error) {
3707
+ if (stopped) {
3708
+ return;
3709
+ }
2352
3710
  failureCount += 1;
2353
3711
  const message = error instanceof Error ? error.message : String(error);
2354
3712
  onLog(`health check failed (${failureCount}/${healthCheck.maxFailures}): ${message}`);
@@ -2383,6 +3741,90 @@ function createPmHealthMonitor(record, onFailure, onLog) {
2383
3741
  }
2384
3742
  };
2385
3743
  }
3744
+ function createPmMemoryMonitor(record, pid, onFailure) {
3745
+ if (!record.maxMemoryBytes || !pid) {
3746
+ return {
3747
+ stop() {
3748
+ }
3749
+ };
3750
+ }
3751
+ const memoryLimit = record.maxMemoryBytes;
3752
+ let stopped = false;
3753
+ const timer = setInterval(() => {
3754
+ if (stopped) {
3755
+ return;
3756
+ }
3757
+ const memoryRssBytes = samplePmProcessMetrics(pid).memoryRssBytes;
3758
+ if (memoryRssBytes === void 0 || memoryRssBytes <= memoryLimit) {
3759
+ return;
3760
+ }
3761
+ stopped = true;
3762
+ onFailure(record.memoryAction === "stop" ? "memory-stop" : "memory", `memory usage ${memoryRssBytes} exceeded limit ${memoryLimit}`);
3763
+ }, DEFAULT_PM_MEMORY_CHECK_INTERVAL);
3764
+ timer.unref?.();
3765
+ return {
3766
+ stop() {
3767
+ stopped = true;
3768
+ clearInterval(timer);
3769
+ }
3770
+ };
3771
+ }
3772
+ function createPmScheduleMonitor(record, onTrigger, onLog) {
3773
+ if (!record.cronRestart) {
3774
+ return {
3775
+ stop() {
3776
+ }
3777
+ };
3778
+ }
3779
+ const schedule = parsePmRestartSchedule(record.cronRestart, "pm cronRestart");
3780
+ let stopped = false;
3781
+ let timer = null;
3782
+ const armTimer = (from = /* @__PURE__ */ new Date()) => {
3783
+ const nextOccurrence = resolveNextPmScheduleOccurrence(schedule, from);
3784
+ if (!nextOccurrence) {
3785
+ onLog(`schedule has no next occurrence: ${record.cronRestart}`);
3786
+ return;
3787
+ }
3788
+ const delayMs = Math.max(0, nextOccurrence.getTime() - Date.now());
3789
+ timer = setTimeout(() => {
3790
+ timer = null;
3791
+ if (stopped) {
3792
+ return;
3793
+ }
3794
+ onTrigger(`restart schedule matched: ${record.cronRestart}`);
3795
+ }, delayMs);
3796
+ timer.unref?.();
3797
+ };
3798
+ armTimer();
3799
+ return {
3800
+ stop() {
3801
+ stopped = true;
3802
+ if (timer) {
3803
+ clearTimeout(timer);
3804
+ timer = null;
3805
+ }
3806
+ }
3807
+ };
3808
+ }
3809
+ function resolvePmRestartDelay(record, restartCount, shouldApplyBackoff) {
3810
+ if (!shouldApplyBackoff || !record.expBackoffRestartDelay) {
3811
+ return record.restartDelay;
3812
+ }
3813
+ const exponent = Math.max(0, restartCount - 1);
3814
+ return Math.min(record.expBackoffRestartDelay * 2 ** exponent, record.expBackoffRestartMaxDelay ?? DEFAULT_PM_EXP_BACKOFF_MAX_DELAY);
3815
+ }
3816
+ function resolvePmRestartCountBase(record, wasStable, restartKind) {
3817
+ if (wasStable) {
3818
+ return 0;
3819
+ }
3820
+ if (record.restartWindow && record.lastRestartAt) {
3821
+ const lastRestartTime = Date.parse(record.lastRestartAt);
3822
+ if (!Number.isNaN(lastRestartTime) && Date.now() - lastRestartTime > record.restartWindow) {
3823
+ return 0;
3824
+ }
3825
+ }
3826
+ return restartKind === "watch" ? record.restartCount ?? 0 : record.restartCount ?? 0;
3827
+ }
2386
3828
  function readPlannedRestartRequest(state) {
2387
3829
  return state.request;
2388
3830
  }
@@ -2396,6 +3838,16 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2396
3838
  (0, import_node_fs4.mkdirSync)((0, import_node_path6.dirname)(initialRecord.logFiles.err), { recursive: true });
2397
3839
  const stdoutLog = (0, import_node_fs4.createWriteStream)(initialRecord.logFiles.out, { flags: "a" });
2398
3840
  const stderrLog = (0, import_node_fs4.createWriteStream)(initialRecord.logFiles.err, { flags: "a" });
3841
+ let proxyController = null;
3842
+ let proxyTargetSyncTimer = null;
3843
+ let inheritedListener = null;
3844
+ const runnerPaths = resolveRunnerPathsFromRecordFile(filePath);
3845
+ const syncOwnedProxyTargets = (baseName) => {
3846
+ if (!proxyController) {
3847
+ return;
3848
+ }
3849
+ proxyController.setTargets(resolvePmProxyTargetUrls(runnerPaths, baseName));
3850
+ };
2399
3851
  const persist = (mutator) => {
2400
3852
  const current = readLatestPmRecord(filePath, record);
2401
3853
  record = mutator(current);
@@ -2408,26 +3860,30 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2408
3860
  activeChildStopTimer = null;
2409
3861
  }
2410
3862
  };
3863
+ const scheduleForcedActiveChildStop = (timeoutMs, reason) => {
3864
+ if (!activeChild?.pid || process.platform === "win32") {
3865
+ return;
3866
+ }
3867
+ clearActiveChildStopTimer();
3868
+ activeChildStopTimer = setTimeout(() => {
3869
+ if (activeChild?.pid && isProcessAlive(activeChild.pid)) {
3870
+ writePmLog(stderrLog, `${reason} after ${timeoutMs}ms; forcing process termination`);
3871
+ terminateProcessTree(activeChild.pid, { force: true });
3872
+ }
3873
+ }, timeoutMs);
3874
+ activeChildStopTimer.unref?.();
3875
+ };
2411
3876
  const stopActiveChild = () => {
2412
3877
  if (!activeChild?.pid || !isProcessAlive(activeChild.pid)) {
2413
3878
  return;
2414
3879
  }
2415
3880
  const current = readLatestPmRecord(filePath, record);
3881
+ const stopTimeout = resolvePmStopTimeout(current);
2416
3882
  if (isPmOnlineWapkRecord(current) && activeChild.stdin && !activeChild.stdin.destroyed && activeChild.stdin.writable) {
2417
3883
  try {
2418
3884
  activeChild.stdin.end(`${PM_WAPK_ONLINE_SHUTDOWN_COMMAND}
2419
3885
  `);
2420
- clearActiveChildStopTimer();
2421
- activeChildStopTimer = setTimeout(() => {
2422
- if (activeChild?.pid && isProcessAlive(activeChild.pid)) {
2423
- writePmLog(
2424
- stderrLog,
2425
- `graceful WAPK online shutdown timed out after ${PM_WAPK_ONLINE_SHUTDOWN_TIMEOUT_MS}ms; forcing process termination`
2426
- );
2427
- terminateProcessTree(activeChild.pid);
2428
- }
2429
- }, PM_WAPK_ONLINE_SHUTDOWN_TIMEOUT_MS);
2430
- activeChildStopTimer.unref?.();
3886
+ scheduleForcedActiveChildStop(stopTimeout, "graceful WAPK online shutdown timed out");
2431
3887
  return;
2432
3888
  } catch (error) {
2433
3889
  const message = error instanceof Error ? error.message : String(error);
@@ -2435,6 +3891,7 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2435
3891
  }
2436
3892
  }
2437
3893
  terminateProcessTree(activeChild.pid);
3894
+ scheduleForcedActiveChildStop(stopTimeout, "graceful shutdown timed out");
2438
3895
  };
2439
3896
  const requestManagedStop = (reason) => {
2440
3897
  if (stopRequested) {
@@ -2472,6 +3929,17 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2472
3929
  let command;
2473
3930
  try {
2474
3931
  command = buildPmCommand(latest);
3932
+ if (latest.proxy && isPmProxyOwner(latest) && !proxyController) {
3933
+ proxyController = await createPmProxyController(latest.proxy);
3934
+ syncOwnedProxyTargets(latest.baseName);
3935
+ if (!proxyTargetSyncTimer) {
3936
+ proxyTargetSyncTimer = setInterval(() => syncOwnedProxyTargets(latest.baseName), 50);
3937
+ proxyTargetSyncTimer.unref?.();
3938
+ }
3939
+ }
3940
+ if (latest.proxy && usesPmInheritedListener(latest) && !inheritedListener) {
3941
+ inheritedListener = await createPmInheritedListener(latest.proxy);
3942
+ }
2475
3943
  } catch (error) {
2476
3944
  const message = error instanceof Error ? error.message : String(error);
2477
3945
  writePmLog(stderrLog, message);
@@ -2481,25 +3949,24 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2481
3949
  error: message,
2482
3950
  runnerPid: void 0,
2483
3951
  childPid: void 0,
3952
+ proxyTargetPort: void 0,
3953
+ proxyReadyAt: void 0,
2484
3954
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2485
3955
  }));
2486
3956
  return;
2487
3957
  }
2488
3958
  const onlineStdinShutdownEnabled = isPmOnlineWapkRecord(latest);
2489
- const child = (0, import_node_child_process2.spawn)(command.command, command.args, {
3959
+ const initialTargetPort = usesPmProxyController(latest) ? await allocatePmProxyTargetPort(resolvePmProxyTargetHost(latest.proxy)) : void 0;
3960
+ const monitorRecord = buildPmMonitorRecord(latest, initialTargetPort);
3961
+ let child = (0, import_node_child_process2.spawn)(command.command, command.args, {
2490
3962
  cwd: latest.cwd,
2491
- env: {
2492
- ...process.env,
2493
- ...latest.env,
2494
- ...command.env,
2495
- ELIT_PM_NAME: latest.name,
2496
- ELIT_PM_ID: latest.id
2497
- },
2498
- stdio: [onlineStdinShutdownEnabled ? "pipe" : "ignore", "pipe", "pipe"],
3963
+ env: buildPmChildEnv(latest, command, initialTargetPort),
3964
+ stdio: buildPmChildStdio(command, onlineStdinShutdownEnabled),
2499
3965
  windowsHide: true,
2500
3966
  shell: command.shell
2501
3967
  });
2502
- const childStartedAt = Date.now();
3968
+ let childStartedAt = Date.now();
3969
+ let childIpcState = command.ipc ? createPmChildIpcState(child, inheritedListener) : void 0;
2503
3970
  activeChild = child;
2504
3971
  if (child.stdout) {
2505
3972
  child.stdout.pipe(stdoutLog, { end: false });
@@ -2507,26 +3974,38 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2507
3974
  if (child.stderr) {
2508
3975
  child.stderr.pipe(stderrLog, { end: false });
2509
3976
  }
3977
+ let childWaitState = { settled: false, result: void 0 };
3978
+ void waitForManagedChildExit(child).then((result) => {
3979
+ childWaitState.result = result;
3980
+ childWaitState.settled = true;
3981
+ });
2510
3982
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3983
+ const waitingForReady = latest.waitReady || Boolean(childIpcState);
2511
3984
  persist((current2) => ({
2512
3985
  ...current2,
2513
- status: "online",
3986
+ status: waitingForReady ? "starting" : "online",
2514
3987
  commandPreview: command.preview,
2515
3988
  runtime: command.runtime ?? current2.runtime,
2516
3989
  runnerPid: process.pid,
2517
3990
  childPid: child.pid,
3991
+ proxyTargetPort: initialTargetPort,
3992
+ proxyReadyAt: !waitingForReady && usesPmProxyController(latest) ? startedAt : void 0,
2518
3993
  startedAt,
2519
3994
  stoppedAt: void 0,
3995
+ reloadRequestedAt: void 0,
2520
3996
  error: void 0,
2521
3997
  updatedAt: startedAt
2522
3998
  }));
2523
3999
  writePmLog(stdoutLog, `started ${command.preview}${child.pid ? ` (pid ${child.pid})` : ""}`);
4000
+ if (isPmProxyOwner(latest) && !waitingForReady) {
4001
+ syncOwnedProxyTargets(latest.baseName);
4002
+ }
2524
4003
  const requestPlannedRestart = (kind, detail) => {
2525
4004
  if (stopRequested || restartState.request) {
2526
4005
  return;
2527
4006
  }
2528
4007
  restartState.request = { kind, detail };
2529
- writePmLog(kind === "health" ? stderrLog : stdoutLog, `${kind} restart requested: ${detail}`);
4008
+ writePmLog(kind === "watch" || kind === "cron" ? stdoutLog : stderrLog, `${kind} restart requested: ${detail}`);
2530
4009
  persist((current2) => ({
2531
4010
  ...current2,
2532
4011
  status: "restarting",
@@ -2534,27 +4013,124 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2534
4013
  }));
2535
4014
  stopActiveChild();
2536
4015
  };
2537
- const watchController = await createPmWatchController(
2538
- latest,
2539
- (changedPath) => requestPlannedRestart("watch", changedPath),
2540
- (message) => writePmLog(stderrLog, `watch error: ${message}`)
2541
- );
2542
- const healthMonitor = createPmHealthMonitor(
2543
- latest,
2544
- (message) => requestPlannedRestart("health", message),
2545
- (message) => writePmLog(stdoutLog, message)
4016
+ let controllers = await createPmChildControllers(
4017
+ monitorRecord,
4018
+ child,
4019
+ stdoutLog,
4020
+ stderrLog,
4021
+ requestPlannedRestart,
4022
+ (message) => {
4023
+ const readyAt = (/* @__PURE__ */ new Date()).toISOString();
4024
+ if (isPmProxyOwner(latest)) {
4025
+ syncOwnedProxyTargets(latest.baseName);
4026
+ }
4027
+ persist((current2) => ({
4028
+ ...current2,
4029
+ status: "online",
4030
+ proxyTargetPort: initialTargetPort,
4031
+ proxyReadyAt: readyAt,
4032
+ updatedAt: readyAt
4033
+ }));
4034
+ writePmLog(stdoutLog, message);
4035
+ },
4036
+ { ready: !waitingForReady, ipcController: childIpcState }
2546
4037
  );
2547
- const desiredStatePoller = setInterval(() => {
4038
+ let handledReloadAt = latest.reloadRequestedAt;
4039
+ while (!childWaitState.settled) {
2548
4040
  const latestRecord = readLatestPmRecord(filePath, record);
2549
- if (!stopRequested && latestRecord.desiredState === "stopped") {
4041
+ if (latestRecord.desiredState === "stopped" && !stopRequested) {
2550
4042
  requestManagedStop("stop requested by PM control state");
2551
4043
  }
2552
- }, DEFAULT_PM_STOP_POLL_MS);
2553
- desiredStatePoller.unref?.();
2554
- const exitResult = await waitForManagedChildExit(child);
2555
- clearInterval(desiredStatePoller);
2556
- await watchController.close();
2557
- healthMonitor.stop();
4044
+ const reloadRequestedAt = latestRecord.reloadRequestedAt;
4045
+ if (!stopRequested && supportsPmProxyReload(latestRecord) && reloadRequestedAt && reloadRequestedAt !== handledReloadAt && latestRecord.proxy) {
4046
+ handledReloadAt = reloadRequestedAt;
4047
+ const replacementTargetPort = usesPmProxyController(latestRecord) ? await allocatePmProxyTargetPort(resolvePmProxyTargetHost(latestRecord.proxy)) : void 0;
4048
+ const replacementMonitorRecord = buildPmMonitorRecord(latestRecord, replacementTargetPort);
4049
+ const replacementChild = (0, import_node_child_process2.spawn)(command.command, command.args, {
4050
+ cwd: latestRecord.cwd,
4051
+ env: buildPmChildEnv(latestRecord, command, replacementTargetPort),
4052
+ stdio: buildPmChildStdio(command, onlineStdinShutdownEnabled),
4053
+ windowsHide: true,
4054
+ shell: command.shell
4055
+ });
4056
+ const replacementIpcState = command.ipc ? createPmChildIpcState(replacementChild, inheritedListener) : void 0;
4057
+ if (replacementChild.stdout) {
4058
+ replacementChild.stdout.pipe(stdoutLog, { end: false });
4059
+ }
4060
+ if (replacementChild.stderr) {
4061
+ replacementChild.stderr.pipe(stderrLog, { end: false });
4062
+ }
4063
+ writePmLog(stdoutLog, `starting ${usesPmInheritedListener(latestRecord) ? "shared-listener" : "proxy handoff"} replacement${replacementChild.pid ? ` (pid ${replacementChild.pid})` : ""}`);
4064
+ const readyResult = await waitForPmChildReady(replacementMonitorRecord, replacementChild, replacementIpcState);
4065
+ if (!readyResult.ready) {
4066
+ writePmLog(stderrLog, readyResult.message ?? "replacement exited before becoming ready");
4067
+ replacementIpcState?.stop();
4068
+ await stopProxyManagedChild(replacementChild, latestRecord, stderrLog);
4069
+ persist((current2) => ({
4070
+ ...current2,
4071
+ status: "online",
4072
+ proxyReadyAt: current2.proxyReadyAt,
4073
+ reloadRequestedAt: void 0,
4074
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4075
+ }));
4076
+ continue;
4077
+ }
4078
+ const previousChild = child;
4079
+ const previousControllers = controllers;
4080
+ const previousIpcState = childIpcState;
4081
+ const handoffAt = (/* @__PURE__ */ new Date()).toISOString();
4082
+ if (usesPmProxyController(latestRecord) && replacementTargetPort) {
4083
+ proxyController?.setTarget(buildPmProxyTargetUrl(latestRecord.proxy, replacementTargetPort));
4084
+ }
4085
+ persist((current2) => ({
4086
+ ...current2,
4087
+ status: "online",
4088
+ childPid: replacementChild.pid,
4089
+ proxyTargetPort: replacementTargetPort,
4090
+ proxyReadyAt: handoffAt,
4091
+ startedAt: handoffAt,
4092
+ reloadRequestedAt: void 0,
4093
+ error: void 0,
4094
+ updatedAt: handoffAt
4095
+ }));
4096
+ if (readyResult.message) {
4097
+ writePmLog(stdoutLog, readyResult.message);
4098
+ }
4099
+ writePmLog(stdoutLog, `${usesPmInheritedListener(latestRecord) ? "shared listener" : "proxy handoff"} activated on ${latestRecord.proxy.host ?? "0.0.0.0"}:${latestRecord.proxy.port}`);
4100
+ child = replacementChild;
4101
+ childIpcState = replacementIpcState;
4102
+ childStartedAt = Date.now();
4103
+ activeChild = replacementChild;
4104
+ childWaitState = { settled: false, result: void 0 };
4105
+ void waitForManagedChildExit(replacementChild).then((result) => {
4106
+ childWaitState.result = result;
4107
+ childWaitState.settled = true;
4108
+ });
4109
+ controllers = await createPmChildControllers(
4110
+ replacementMonitorRecord,
4111
+ replacementChild,
4112
+ stdoutLog,
4113
+ stderrLog,
4114
+ requestPlannedRestart,
4115
+ () => {
4116
+ },
4117
+ { ready: true, ipcController: replacementIpcState }
4118
+ );
4119
+ await delay(250);
4120
+ await previousControllers.stop();
4121
+ previousIpcState?.stop();
4122
+ await stopProxyManagedChild(previousChild, latestRecord, stderrLog);
4123
+ clearActiveChildStopTimer();
4124
+ if (isPmProxyOwner(latestRecord)) {
4125
+ syncOwnedProxyTargets(latestRecord.baseName);
4126
+ }
4127
+ continue;
4128
+ }
4129
+ await delay(25);
4130
+ }
4131
+ const exitResult = childWaitState.result;
4132
+ await controllers.stop();
4133
+ childIpcState?.stop();
2558
4134
  clearActiveChildStopTimer();
2559
4135
  activeChild = null;
2560
4136
  const exitCode = waitForExit(exitResult.code, exitResult.signal);
@@ -2570,56 +4146,77 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2570
4146
  if (stopRequested || current.desiredState === "stopped") {
2571
4147
  break;
2572
4148
  }
2573
- const shouldRestartForExit = plannedRestart ? true : current.restartPolicy === "always" ? true : current.restartPolicy === "on-failure" ? exitCode !== 0 || Boolean(exitResult.error) : false;
4149
+ const shouldRestartForExit = plannedRestart?.kind === "memory-stop" ? false : plannedRestart ? true : current.restartPolicy === "always" ? true : current.restartPolicy === "on-failure" ? exitCode !== 0 || Boolean(exitResult.error) : false;
2574
4150
  if (!shouldRestartForExit) {
2575
4151
  persist((latestRecord) => ({
2576
4152
  ...latestRecord,
2577
- status: exitCode === 0 && !exitResult.error ? "exited" : "errored",
4153
+ status: plannedRestart?.kind === "memory-stop" ? "errored" : exitCode === 0 && !exitResult.error ? "exited" : "errored",
2578
4154
  childPid: void 0,
4155
+ proxyTargetPort: void 0,
4156
+ proxyReadyAt: void 0,
2579
4157
  runnerPid: void 0,
2580
4158
  lastExitCode: exitCode,
2581
- error: exitCode === 0 && !exitResult.error ? void 0 : exitResult.error ?? `Process exited with code ${exitCode}.`,
4159
+ reloadRequestedAt: void 0,
4160
+ error: plannedRestart?.kind === "memory-stop" ? plannedRestart.detail : exitCode === 0 && !exitResult.error ? void 0 : exitResult.error ?? `Process exited with code ${exitCode}.`,
2582
4161
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString(),
2583
4162
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2584
4163
  }));
4164
+ if (isPmProxyOwner(current)) {
4165
+ syncOwnedProxyTargets(current.baseName);
4166
+ }
2585
4167
  return;
2586
4168
  }
2587
4169
  const shouldCountRestart = plannedRestart?.kind !== "watch";
2588
- const baseRestartCount = wasStable ? 0 : current.restartCount ?? 0;
4170
+ const baseRestartCount = resolvePmRestartCountBase(current, wasStable, plannedRestart?.kind);
2589
4171
  const nextRestartCount = shouldCountRestart ? baseRestartCount + 1 : current.restartCount ?? 0;
2590
4172
  if (nextRestartCount > current.maxRestarts) {
2591
4173
  persist((latestRecord) => ({
2592
4174
  ...latestRecord,
2593
4175
  status: "errored",
2594
4176
  childPid: void 0,
4177
+ proxyTargetPort: void 0,
4178
+ proxyReadyAt: void 0,
2595
4179
  runnerPid: void 0,
2596
4180
  restartCount: nextRestartCount,
4181
+ lastRestartAt: (/* @__PURE__ */ new Date()).toISOString(),
2597
4182
  lastExitCode: exitCode,
4183
+ reloadRequestedAt: void 0,
2598
4184
  error: plannedRestart ? `Reached max restart attempts (${current.maxRestarts}) after ${plannedRestart.kind} restart requests.` : `Reached max restart attempts (${current.maxRestarts}).`,
2599
4185
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString(),
2600
4186
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2601
4187
  }));
2602
4188
  writePmLog(stderrLog, `max restart attempts reached (${current.maxRestarts})`);
4189
+ if (isPmProxyOwner(current)) {
4190
+ syncOwnedProxyTargets(current.baseName);
4191
+ }
2603
4192
  return;
2604
4193
  }
2605
4194
  persist((latestRecord) => ({
2606
4195
  ...latestRecord,
2607
4196
  status: "restarting",
2608
4197
  childPid: void 0,
4198
+ proxyTargetPort: void 0,
4199
+ proxyReadyAt: void 0,
2609
4200
  lastExitCode: exitCode,
2610
4201
  restartCount: nextRestartCount,
4202
+ lastRestartAt: (/* @__PURE__ */ new Date()).toISOString(),
4203
+ reloadRequestedAt: void 0,
2611
4204
  error: void 0,
2612
4205
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2613
4206
  }));
4207
+ if (isPmProxyOwner(current)) {
4208
+ syncOwnedProxyTargets(current.baseName);
4209
+ }
4210
+ const resolvedRestartDelay = resolvePmRestartDelay(current, nextRestartCount, shouldCountRestart && !wasStable && plannedRestart?.kind !== "watch");
2614
4211
  if (plannedRestart) {
2615
4212
  writePmLog(
2616
- plannedRestart.kind === "health" ? stderrLog : stdoutLog,
2617
- `restarting in ${current.restartDelay}ms after ${plannedRestart.kind}: ${plannedRestart.detail}`
4213
+ plannedRestart.kind === "health" || plannedRestart.kind === "memory" || plannedRestart.kind === "memory-stop" || plannedRestart.kind === "startup" ? stderrLog : stdoutLog,
4214
+ `restarting in ${resolvedRestartDelay}ms after ${plannedRestart.kind}: ${plannedRestart.detail}`
2618
4215
  );
2619
4216
  } else {
2620
- writePmLog(stdoutLog, `restarting in ${current.restartDelay}ms`);
4217
+ writePmLog(stdoutLog, `restarting in ${resolvedRestartDelay}ms`);
2621
4218
  }
2622
- await delay(current.restartDelay);
4219
+ await delay(resolvedRestartDelay);
2623
4220
  }
2624
4221
  } finally {
2625
4222
  stopRequested = true;
@@ -2632,11 +4229,26 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2632
4229
  status: finalRecord.status === "errored" ? "errored" : finalRecord.status === "exited" ? "exited" : "stopped",
2633
4230
  runnerPid: void 0,
2634
4231
  childPid: void 0,
4232
+ proxyTargetPort: void 0,
4233
+ proxyReadyAt: void 0,
4234
+ reloadRequestedAt: void 0,
2635
4235
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString(),
2636
4236
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2637
4237
  });
2638
4238
  process.off("SIGINT", handleStopSignal);
2639
4239
  process.off("SIGTERM", handleStopSignal);
4240
+ if (proxyTargetSyncTimer) {
4241
+ clearInterval(proxyTargetSyncTimer);
4242
+ proxyTargetSyncTimer = null;
4243
+ }
4244
+ if (proxyController) {
4245
+ await proxyController.close().catch(() => void 0);
4246
+ proxyController = null;
4247
+ }
4248
+ if (inheritedListener) {
4249
+ await new Promise((resolvePromise) => inheritedListener?.close(() => resolvePromise()));
4250
+ inheritedListener = null;
4251
+ }
2640
4252
  await new Promise((resolvePromise) => stdoutLog.end(resolvePromise));
2641
4253
  await new Promise((resolvePromise) => stderrLog.end(resolvePromise));
2642
4254
  }
@@ -2690,23 +4302,26 @@ async function stopPmMatches(matches) {
2690
4302
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString()
2691
4303
  };
2692
4304
  writePmRecord(match.filePath, updated);
2693
- const runnerStopped = await waitForProcessTermination(match.record.runnerPid, DEFAULT_PM_STOP_GRACE_PERIOD_MS);
4305
+ const stopTimeout = resolvePmStopTimeout(match.record);
4306
+ const runnerStopped = await waitForProcessTermination(match.record.runnerPid, stopTimeout);
2694
4307
  const childStopped = await waitForProcessTermination(
2695
4308
  match.record.childPid,
2696
- runnerStopped ? DEFAULT_PM_STOP_POLL_MS : DEFAULT_PM_STOP_GRACE_PERIOD_MS
4309
+ runnerStopped ? DEFAULT_PM_STOP_POLL_MS : stopTimeout
2697
4310
  );
2698
4311
  if (!runnerStopped && match.record.runnerPid && isProcessAlive(match.record.runnerPid)) {
2699
- terminateProcessTree(match.record.runnerPid);
4312
+ terminateProcessTree(match.record.runnerPid, { force: true });
2700
4313
  await waitForProcessTermination(match.record.runnerPid, DEFAULT_PM_STOP_POLL_MS);
2701
4314
  }
2702
4315
  if (!childStopped && match.record.childPid && isProcessAlive(match.record.childPid)) {
2703
- terminateProcessTree(match.record.childPid);
4316
+ terminateProcessTree(match.record.childPid, { force: true });
2704
4317
  await waitForProcessTermination(match.record.childPid, DEFAULT_PM_STOP_POLL_MS);
2705
4318
  }
2706
4319
  writePmRecord(match.filePath, {
2707
4320
  ...updated,
2708
4321
  runnerPid: void 0,
2709
4322
  childPid: void 0,
4323
+ proxyTargetPort: void 0,
4324
+ reloadRequestedAt: void 0,
2710
4325
  status: "stopped",
2711
4326
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2712
4327
  });
@@ -2775,13 +4390,13 @@ function readPackageJson(filePath) {
2775
4390
 
2776
4391
  // src/shares/workspace-package/roots.ts
2777
4392
  function findPackageDirectory(startDir, resolveMatch) {
2778
- let currentDir = resolve4(startDir);
4393
+ let currentDir = resolve5(startDir);
2779
4394
  while (true) {
2780
4395
  const match = resolveMatch(currentDir);
2781
4396
  if (match) {
2782
4397
  return match;
2783
4398
  }
2784
- const parentDir = dirname2(currentDir);
4399
+ const parentDir = dirname3(currentDir);
2785
4400
  if (parentDir === currentDir) {
2786
4401
  return void 0;
2787
4402
  }
@@ -2809,17 +4424,17 @@ init_fs();
2809
4424
  function getWorkspacePackageImportCandidates(packageRoot, specifier, options = {}) {
2810
4425
  const subpath = specifier === "elit" ? "index" : specifier.slice("elit/".length);
2811
4426
  const builtCandidates = options.preferredBuiltFormat === "cjs" ? [
2812
- resolve4(packageRoot, "dist", `${subpath}.cjs`),
2813
- resolve4(packageRoot, "dist", `${subpath}.js`),
2814
- resolve4(packageRoot, "dist", `${subpath}.mjs`)
4427
+ resolve5(packageRoot, "dist", `${subpath}.cjs`),
4428
+ resolve5(packageRoot, "dist", `${subpath}.js`),
4429
+ resolve5(packageRoot, "dist", `${subpath}.mjs`)
2815
4430
  ] : [
2816
- resolve4(packageRoot, "dist", `${subpath}.mjs`),
2817
- resolve4(packageRoot, "dist", `${subpath}.js`),
2818
- resolve4(packageRoot, "dist", `${subpath}.cjs`)
4431
+ resolve5(packageRoot, "dist", `${subpath}.mjs`),
4432
+ resolve5(packageRoot, "dist", `${subpath}.js`),
4433
+ resolve5(packageRoot, "dist", `${subpath}.cjs`)
2819
4434
  ];
2820
4435
  const sourceCandidates = [
2821
- resolve4(packageRoot, "src", `${subpath}.ts`),
2822
- resolve4(packageRoot, "src", `${subpath}.tsx`)
4436
+ resolve5(packageRoot, "src", `${subpath}.ts`),
4437
+ resolve5(packageRoot, "src", `${subpath}.tsx`)
2823
4438
  ];
2824
4439
  return options.preferBuilt ? [...builtCandidates, ...sourceCandidates] : [...sourceCandidates, ...builtCandidates];
2825
4440
  }
@@ -2854,7 +4469,7 @@ function resolveWorkspacePackageImport(specifier, startDir, options = {}) {
2854
4469
  // src/shares/config/loader.ts
2855
4470
  function resolveConfigPath(cwd = process.cwd()) {
2856
4471
  for (const configFile of ELIT_CONFIG_FILES) {
2857
- const configPath = resolve4(cwd, configFile);
4472
+ const configPath = resolve5(cwd, configFile);
2858
4473
  if (existsSync4(configPath)) {
2859
4474
  return configPath;
2860
4475
  }
@@ -2883,7 +4498,7 @@ async function loadConfigFile(configPath) {
2883
4498
  if (ext === "ts" || ext === "mts") {
2884
4499
  try {
2885
4500
  const { build } = await import("esbuild");
2886
- const configDir = dirname2(configPath);
4501
+ const configDir = dirname3(configPath);
2887
4502
  const tempFile = join5(configDir, `.elit-config-${Date.now()}.mjs`);
2888
4503
  const externalAllPlugin = {
2889
4504
  name: "external-all",
@@ -2957,6 +4572,43 @@ async function loadConfigFile(configPath) {
2957
4572
  }
2958
4573
 
2959
4574
  // src/cli/pm/commands.ts
4575
+ var PM_SIGNAL_NAMES = /* @__PURE__ */ new Set([
4576
+ "SIGABRT",
4577
+ "SIGALRM",
4578
+ "SIGBREAK",
4579
+ "SIGBUS",
4580
+ "SIGCHLD",
4581
+ "SIGCONT",
4582
+ "SIGFPE",
4583
+ "SIGHUP",
4584
+ "SIGILL",
4585
+ "SIGINT",
4586
+ "SIGIO",
4587
+ "SIGIOT",
4588
+ "SIGKILL",
4589
+ "SIGPIPE",
4590
+ "SIGPOLL",
4591
+ "SIGPROF",
4592
+ "SIGPWR",
4593
+ "SIGQUIT",
4594
+ "SIGSEGV",
4595
+ "SIGSTKFLT",
4596
+ "SIGSTOP",
4597
+ "SIGSYS",
4598
+ "SIGTERM",
4599
+ "SIGTRAP",
4600
+ "SIGTSTP",
4601
+ "SIGTTIN",
4602
+ "SIGTTOU",
4603
+ "SIGUNUSED",
4604
+ "SIGURG",
4605
+ "SIGUSR1",
4606
+ "SIGUSR2",
4607
+ "SIGVTALRM",
4608
+ "SIGWINCH",
4609
+ "SIGXCPU",
4610
+ "SIGXFSZ"
4611
+ ]);
2960
4612
  async function runPmStart(args) {
2961
4613
  const parsed = parsePmStartArgs(args);
2962
4614
  const workspaceRoot = process.cwd();
@@ -2989,9 +4641,130 @@ function resolveNamedMatches(paths, value) {
2989
4641
  if (value === "all") {
2990
4642
  return listPmRecordMatches(paths).map(syncPmRecordLiveness);
2991
4643
  }
4644
+ const groupMatches = findPmGroupMatches(paths, value);
4645
+ if (groupMatches.length > 0) {
4646
+ return groupMatches.map(syncPmRecordLiveness);
4647
+ }
2992
4648
  const match = findPmRecordMatch(paths, value);
2993
4649
  return match ? [syncPmRecordLiveness(match)] : [];
2994
4650
  }
4651
+ function sortPmMatchesByInstance(matches) {
4652
+ return [...matches].sort((left, right) => left.record.instanceIndex - right.record.instanceIndex);
4653
+ }
4654
+ function resolveInspectableMatch(paths, value) {
4655
+ const exactMatch = findPmRecordMatch(paths, value);
4656
+ const groupMatches = findPmGroupMatches(paths, value).map(syncPmRecordLiveness);
4657
+ if (groupMatches.length > 1 && exactMatch?.record.baseName === value && exactMatch.record.name === value) {
4658
+ throw new Error(`Multiple managed processes found for: ${value}. Use a specific instance name such as ${groupMatches[0]?.record.name} or ${groupMatches[1]?.record.name}.`);
4659
+ }
4660
+ if (exactMatch) {
4661
+ return syncPmRecordLiveness(exactMatch);
4662
+ }
4663
+ return groupMatches[0];
4664
+ }
4665
+ function rebuildPmRecordDefinition(record, targetInstances = record.instances) {
4666
+ const definition = resolvePmAppDefinition(
4667
+ toPmAppConfig(record),
4668
+ { name: record.baseName, env: {}, watchPaths: [], watchIgnore: [], instances: targetInstances },
4669
+ process.cwd(),
4670
+ record.source
4671
+ );
4672
+ return {
4673
+ ...definition,
4674
+ name: record.name,
4675
+ baseName: record.baseName,
4676
+ instanceIndex: record.instanceIndex,
4677
+ instances: targetInstances
4678
+ };
4679
+ }
4680
+ function rebuildPmSavedDefinition(app) {
4681
+ const definition = resolvePmAppDefinition(
4682
+ toSavedPmAppConfig(app),
4683
+ { name: app.baseName, env: {}, watchPaths: [], watchIgnore: [], instances: app.instances },
4684
+ process.cwd(),
4685
+ "cli"
4686
+ );
4687
+ return {
4688
+ ...definition,
4689
+ name: app.name,
4690
+ baseName: app.baseName,
4691
+ instanceIndex: app.instanceIndex,
4692
+ instances: app.instances
4693
+ };
4694
+ }
4695
+ function deletePmMatches(matches) {
4696
+ for (const match of matches) {
4697
+ if ((0, import_node_fs5.existsSync)(match.record.logFiles.out)) {
4698
+ (0, import_node_fs5.rmSync)(match.record.logFiles.out, { force: true });
4699
+ }
4700
+ if ((0, import_node_fs5.existsSync)(match.record.logFiles.err)) {
4701
+ (0, import_node_fs5.rmSync)(match.record.logFiles.err, { force: true });
4702
+ }
4703
+ (0, import_node_fs5.rmSync)(match.filePath, { force: true });
4704
+ }
4705
+ }
4706
+ function updatePmInstanceCount(matches, instances) {
4707
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4708
+ for (const match of matches) {
4709
+ writePmRecord(match.filePath, {
4710
+ ...match.record,
4711
+ instances,
4712
+ updatedAt: now
4713
+ });
4714
+ }
4715
+ }
4716
+ function normalizePmSignalName(value) {
4717
+ const trimmed = value.trim().toUpperCase();
4718
+ const signalName = trimmed.startsWith("SIG") ? trimmed : `SIG${trimmed}`;
4719
+ if (!PM_SIGNAL_NAMES.has(signalName)) {
4720
+ throw new Error(`Unsupported pm signal: ${value}`);
4721
+ }
4722
+ return signalName;
4723
+ }
4724
+ function resolveSignalablePid(record) {
4725
+ if (record.childPid && record.childPid > 0) {
4726
+ return record.childPid;
4727
+ }
4728
+ if (record.runnerPid && record.runnerPid > 0) {
4729
+ return record.runnerPid;
4730
+ }
4731
+ return void 0;
4732
+ }
4733
+ function groupPmMatchesByBaseName(matches) {
4734
+ const grouped = /* @__PURE__ */ new Map();
4735
+ for (const match of matches) {
4736
+ const group = grouped.get(match.record.baseName);
4737
+ if (group) {
4738
+ group.push(match);
4739
+ continue;
4740
+ }
4741
+ grouped.set(match.record.baseName, [match]);
4742
+ }
4743
+ return [...grouped.entries()].sort((left, right) => left[0].localeCompare(right[0])).map(([, group]) => sortPmMatchesByInstance(group));
4744
+ }
4745
+ async function waitForPmRecordOnline(filePath, timeoutMs) {
4746
+ const deadline = Date.now() + timeoutMs;
4747
+ while (Date.now() < deadline) {
4748
+ if (!(0, import_node_fs5.existsSync)(filePath)) {
4749
+ break;
4750
+ }
4751
+ const record = readPmRecord(filePath);
4752
+ if (record.status === "online") {
4753
+ return record;
4754
+ }
4755
+ if (record.status === "errored" || record.status === "exited" || record.status === "stopped") {
4756
+ throw new Error(record.error ?? `Process ${record.name} failed while reloading.`);
4757
+ }
4758
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
4759
+ }
4760
+ throw new Error(`Timed out waiting for the reloaded process to become online after ${timeoutMs}ms.`);
4761
+ }
4762
+ function resolvePmReloadReadyTimeout(record) {
4763
+ return record.waitReady || record.proxy?.strategy === "inherit" ? Math.max(record.listenTimeout + 1e3, 2e3) : Math.max(record.restartDelay + 1e3, 2e3);
4764
+ }
4765
+ function supportsPmProxyReload2(record) {
4766
+ return Boolean(record.proxy) && record.instances === 1 && Boolean(record.runnerPid);
4767
+ }
2995
4768
  function padCell(value, width) {
2996
4769
  return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
2997
4770
  }
@@ -3002,8 +4775,255 @@ function tailLogFile(filePath, lineCount) {
3002
4775
  const lines = (0, import_node_fs5.readFileSync)(filePath, "utf8").split(/\r?\n/).filter((line) => line.length > 0);
3003
4776
  return lines.slice(-lineCount).join(import_node_os2.EOL);
3004
4777
  }
3005
- function printPmList(paths) {
3006
- const matches = listPmRecordMatches(paths).map(syncPmRecordLiveness);
4778
+ function listPmMatches(paths) {
4779
+ return listPmRecordMatches(paths).map(syncPmRecordLiveness);
4780
+ }
4781
+ function isPmRecordActive(record) {
4782
+ return record.desiredState === "running" && (record.status === "starting" || record.status === "online" || record.status === "restarting");
4783
+ }
4784
+ function resolvePmUptimeMs(record) {
4785
+ if (!isPmRecordActive(record) || !record.startedAt) {
4786
+ return void 0;
4787
+ }
4788
+ const startedTime = Date.parse(record.startedAt);
4789
+ if (Number.isNaN(startedTime)) {
4790
+ return void 0;
4791
+ }
4792
+ return Math.max(0, Date.now() - startedTime);
4793
+ }
4794
+ function resolvePmLiveMetrics(record) {
4795
+ const uptimeMs = resolvePmUptimeMs(record);
4796
+ if (!isPmRecordActive(record) || !record.childPid) {
4797
+ return { uptimeMs };
4798
+ }
4799
+ const sampledMetrics = samplePmProcessMetrics(record.childPid);
4800
+ return {
4801
+ ...sampledMetrics,
4802
+ uptimeMs,
4803
+ updatedAt: sampledMetrics.cpuPercent !== void 0 || sampledMetrics.memoryRssBytes !== void 0 ? (/* @__PURE__ */ new Date()).toISOString() : void 0
4804
+ };
4805
+ }
4806
+ function toPmDisplayRecord(record) {
4807
+ return {
4808
+ record,
4809
+ liveMetrics: resolvePmLiveMetrics(record)
4810
+ };
4811
+ }
4812
+ function serializePmRecord(record) {
4813
+ return {
4814
+ ...record,
4815
+ liveMetrics: resolvePmLiveMetrics(record)
4816
+ };
4817
+ }
4818
+ function parsePmFormatOption(args, index, option) {
4819
+ let value;
4820
+ if (option.startsWith("--format=")) {
4821
+ value = option.slice("--format=".length);
4822
+ } else {
4823
+ value = readRequiredValue(args, index + 1, "--format");
4824
+ index += 1;
4825
+ }
4826
+ if (value !== "table" && value !== "json") {
4827
+ throw new Error(`Unsupported pm output format: ${value}`);
4828
+ }
4829
+ return {
4830
+ format: value,
4831
+ nextIndex: index
4832
+ };
4833
+ }
4834
+ function parsePmListArgs(args) {
4835
+ let format = "table";
4836
+ for (let index = 0; index < args.length; index++) {
4837
+ const arg = args[index];
4838
+ switch (arg) {
4839
+ case "--json":
4840
+ format = "json";
4841
+ break;
4842
+ case "--format":
4843
+ default:
4844
+ if (arg === "--format" || arg.startsWith("--format=")) {
4845
+ const parsed = parsePmFormatOption(args, index, arg);
4846
+ format = parsed.format;
4847
+ index = parsed.nextIndex;
4848
+ break;
4849
+ }
4850
+ throw new Error(`Unknown pm list option: ${arg}`);
4851
+ }
4852
+ }
4853
+ return { format };
4854
+ }
4855
+ function parsePmShowArgs(args) {
4856
+ let format = "text";
4857
+ let name;
4858
+ for (let index = 0; index < args.length; index++) {
4859
+ const arg = args[index];
4860
+ switch (arg) {
4861
+ case "--json":
4862
+ format = "json";
4863
+ break;
4864
+ case "--format":
4865
+ default:
4866
+ if (arg === "--format" || arg.startsWith("--format=")) {
4867
+ const parsed = parsePmFormatOption(args, index, arg);
4868
+ format = parsed.format === "json" ? "json" : "text";
4869
+ index = parsed.nextIndex;
4870
+ break;
4871
+ }
4872
+ if (arg.startsWith("-")) {
4873
+ throw new Error(`Unknown pm show option: ${arg}`);
4874
+ }
4875
+ if (name) {
4876
+ throw new Error("pm show accepts exactly one process name.");
4877
+ }
4878
+ name = arg;
4879
+ break;
4880
+ }
4881
+ }
4882
+ if (!name) {
4883
+ throw new Error("Usage: elit pm show <name> [--json]");
4884
+ }
4885
+ return { name, format };
4886
+ }
4887
+ function formatPmDuration(durationMs) {
4888
+ if (durationMs < 1e3) {
4889
+ return `${durationMs}ms`;
4890
+ }
4891
+ const totalSeconds = Math.floor(durationMs / 1e3);
4892
+ const days = Math.floor(totalSeconds / 86400);
4893
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
4894
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
4895
+ const seconds = totalSeconds % 60;
4896
+ const parts = [];
4897
+ if (days > 0) {
4898
+ parts.push(`${days}d`);
4899
+ }
4900
+ if (hours > 0) {
4901
+ parts.push(`${hours}h`);
4902
+ }
4903
+ if (minutes > 0) {
4904
+ parts.push(`${minutes}m`);
4905
+ }
4906
+ if (seconds > 0 || parts.length === 0) {
4907
+ parts.push(`${seconds}s`);
4908
+ }
4909
+ return parts.slice(0, 2).join(" ");
4910
+ }
4911
+ function formatPmCpuPercent(cpuPercent) {
4912
+ if (cpuPercent === void 0 || !Number.isFinite(cpuPercent)) {
4913
+ return "-";
4914
+ }
4915
+ return `${cpuPercent >= 100 ? cpuPercent.toFixed(0) : cpuPercent.toFixed(1)}%`;
4916
+ }
4917
+ function formatPmMemory(memoryRssBytes) {
4918
+ if (memoryRssBytes === void 0 || !Number.isFinite(memoryRssBytes) || memoryRssBytes < 0) {
4919
+ return "-";
4920
+ }
4921
+ const units = ["B", "KB", "MB", "GB", "TB"];
4922
+ let value = memoryRssBytes;
4923
+ let unitIndex = 0;
4924
+ while (value >= 1024 && unitIndex < units.length - 1) {
4925
+ value /= 1024;
4926
+ unitIndex += 1;
4927
+ }
4928
+ const formatted = value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1);
4929
+ return `${formatted}${units[unitIndex]}`;
4930
+ }
4931
+ function formatPmUptime(uptimeMs) {
4932
+ if (uptimeMs === void 0 || !Number.isFinite(uptimeMs)) {
4933
+ return "-";
4934
+ }
4935
+ return formatPmDuration(Math.max(0, uptimeMs));
4936
+ }
4937
+ function formatPmTarget(record) {
4938
+ if (record.script) {
4939
+ return record.script;
4940
+ }
4941
+ if (record.file) {
4942
+ return record.file;
4943
+ }
4944
+ if (record.wapk) {
4945
+ return record.wapk;
4946
+ }
4947
+ return "-";
4948
+ }
4949
+ function pushPmDetail(lines, label, value) {
4950
+ lines.push(`${padCell(`${label}:`, 18)} ${value}`);
4951
+ }
4952
+ function pushPmDetailList(lines, label, values) {
4953
+ if (values.length === 0) {
4954
+ pushPmDetail(lines, label, "-");
4955
+ return;
4956
+ }
4957
+ pushPmDetail(lines, label, values[0] ?? "-");
4958
+ for (const value of values.slice(1)) {
4959
+ lines.push(`${" ".repeat(20)} ${value}`);
4960
+ }
4961
+ }
4962
+ function formatPmRecordDetails(record, liveMetrics) {
4963
+ const lines = [`Process: ${record.name}`];
4964
+ pushPmDetail(lines, "id", record.id);
4965
+ pushPmDetail(lines, "status", record.status);
4966
+ pushPmDetail(lines, "desired state", record.desiredState);
4967
+ pushPmDetail(lines, "instance", `${record.instanceIndex}/${record.instances}`);
4968
+ pushPmDetail(lines, "cpu", formatPmCpuPercent(liveMetrics.cpuPercent));
4969
+ pushPmDetail(lines, "memory", formatPmMemory(liveMetrics.memoryRssBytes));
4970
+ pushPmDetail(lines, "uptime", formatPmUptime(liveMetrics.uptimeMs));
4971
+ pushPmDetail(lines, "type", record.type);
4972
+ pushPmDetail(lines, "source", record.source);
4973
+ pushPmDetail(lines, "runtime", record.runtime ?? "-");
4974
+ pushPmDetail(lines, "cwd", record.cwd);
4975
+ pushPmDetail(lines, "target", formatPmTarget(record));
4976
+ pushPmDetail(lines, "command", record.commandPreview || "-");
4977
+ pushPmDetail(lines, "runner pid", record.runnerPid ? String(record.runnerPid) : "-");
4978
+ pushPmDetail(lines, "child pid", record.childPid ? String(record.childPid) : "-");
4979
+ pushPmDetail(lines, "restart count", `${record.restartCount}/${record.maxRestarts}`);
4980
+ pushPmDetail(lines, "restart policy", record.restartPolicy);
4981
+ pushPmDetail(lines, "proxy", record.proxy ? `http://${record.proxy.host ?? "0.0.0.0"}:${record.proxy.port}` : "-");
4982
+ pushPmDetail(lines, "proxy strategy", record.proxy?.strategy ?? "-");
4983
+ pushPmDetail(lines, "proxy target", record.proxy && record.proxyTargetPort ? `${record.proxy.targetHost ?? "127.0.0.1"}:${record.proxyTargetPort}` : "-");
4984
+ pushPmDetail(lines, "max memory", record.maxMemoryBytes ? formatPmMemory(record.maxMemoryBytes) : "-");
4985
+ pushPmDetail(lines, "memory action", record.memoryAction ?? "-");
4986
+ pushPmDetail(lines, "cron restart", record.cronRestart ?? "-");
4987
+ pushPmDetail(lines, "exp backoff", record.expBackoffRestartDelay ? formatPmDuration(record.expBackoffRestartDelay) : "-");
4988
+ pushPmDetail(lines, "exp backoff max", record.expBackoffRestartMaxDelay ? formatPmDuration(record.expBackoffRestartMaxDelay) : "-");
4989
+ pushPmDetail(lines, "restart window", record.restartWindow ? formatPmDuration(record.restartWindow) : "-");
4990
+ pushPmDetail(lines, "wait ready", record.waitReady ? "enabled" : "disabled");
4991
+ pushPmDetail(lines, "listen timeout", record.waitReady ? formatPmDuration(record.listenTimeout) : "-");
4992
+ pushPmDetail(lines, "restart delay", formatPmDuration(record.restartDelay));
4993
+ pushPmDetail(lines, "kill timeout", formatPmDuration(record.killTimeout));
4994
+ pushPmDetail(lines, "min uptime", formatPmDuration(record.minUptime));
4995
+ pushPmDetail(lines, "autorestart", record.autorestart ? "enabled" : "disabled");
4996
+ pushPmDetail(lines, "watch", record.watch ? "enabled" : "disabled");
4997
+ pushPmDetail(lines, "watch debounce", record.watch ? formatPmDuration(record.watchDebounce) : "-");
4998
+ pushPmDetailList(lines, "watch paths", record.watchPaths);
4999
+ pushPmDetailList(lines, "watch ignore", record.watchIgnore);
5000
+ if (record.healthCheck) {
5001
+ pushPmDetail(lines, "health check", record.healthCheck.url);
5002
+ pushPmDetail(lines, "health grace", formatPmDuration(record.healthCheck.gracePeriod));
5003
+ pushPmDetail(lines, "health interval", formatPmDuration(record.healthCheck.interval));
5004
+ pushPmDetail(lines, "health timeout", formatPmDuration(record.healthCheck.timeout));
5005
+ pushPmDetail(lines, "health failures", String(record.healthCheck.maxFailures));
5006
+ } else {
5007
+ pushPmDetail(lines, "health check", "-");
5008
+ }
5009
+ pushPmDetailList(lines, "env", Object.entries(record.env).map(([key, value]) => `${key}=${value}`));
5010
+ pushPmDetail(lines, "stdout log", record.logFiles.out);
5011
+ pushPmDetail(lines, "stderr log", record.logFiles.err);
5012
+ pushPmDetail(lines, "created at", record.createdAt);
5013
+ pushPmDetail(lines, "updated at", record.updatedAt);
5014
+ pushPmDetail(lines, "metrics at", liveMetrics.updatedAt ?? "-");
5015
+ pushPmDetail(lines, "started at", record.startedAt ?? "-");
5016
+ pushPmDetail(lines, "stopped at", record.stoppedAt ?? "-");
5017
+ pushPmDetail(lines, "last exit", record.lastExitCode === void 0 ? "-" : String(record.lastExitCode));
5018
+ pushPmDetail(lines, "error", record.error ?? "-");
5019
+ return lines.join(import_node_os2.EOL);
5020
+ }
5021
+ function printPmList(paths, format = "table") {
5022
+ const matches = listPmMatches(paths).map((match) => toPmDisplayRecord(match.record));
5023
+ if (format === "json") {
5024
+ console.log(JSON.stringify(matches.map((match) => ({ ...match.record, liveMetrics: match.liveMetrics })), null, 2));
5025
+ return;
5026
+ }
3007
5027
  if (matches.length === 0) {
3008
5028
  console.log("No managed processes found.");
3009
5029
  return;
@@ -3012,22 +5032,47 @@ function printPmList(paths) {
3012
5032
  padCell("name", 20),
3013
5033
  padCell("status", 12),
3014
5034
  padCell("pid", 8),
5035
+ padCell("cpu", 8),
5036
+ padCell("memory", 10),
5037
+ padCell("uptime", 10),
3015
5038
  padCell("restarts", 10),
3016
5039
  padCell("type", 8),
3017
5040
  "runtime"
3018
5041
  ];
3019
5042
  console.log(headers.join(" "));
3020
- for (const { record } of matches) {
5043
+ for (const { record, liveMetrics } of matches) {
3021
5044
  console.log([
3022
5045
  padCell(record.name, 20),
3023
5046
  padCell(record.status, 12),
3024
5047
  padCell(record.childPid ? String(record.childPid) : "-", 8),
5048
+ padCell(formatPmCpuPercent(liveMetrics.cpuPercent), 8),
5049
+ padCell(formatPmMemory(liveMetrics.memoryRssBytes), 10),
5050
+ padCell(formatPmUptime(liveMetrics.uptimeMs), 10),
3025
5051
  padCell(String(record.restartCount ?? 0), 10),
3026
5052
  padCell(record.type, 8),
3027
5053
  record.runtime ?? "-"
3028
5054
  ].join(" "));
3029
5055
  }
3030
5056
  }
5057
+ async function runPmList(args) {
5058
+ const options = parsePmListArgs(args);
5059
+ const { paths } = await loadPmContext();
5060
+ printPmList(paths, options.format);
5061
+ }
5062
+ async function runPmShow(args) {
5063
+ const options = parsePmShowArgs(args);
5064
+ const { paths } = await loadPmContext();
5065
+ const match = resolveInspectableMatch(paths, options.name);
5066
+ if (!match) {
5067
+ throw new Error(`No managed process found for: ${options.name}`);
5068
+ }
5069
+ const synced = syncPmRecordLiveness(match);
5070
+ if (options.format === "json") {
5071
+ console.log(JSON.stringify(serializePmRecord(synced.record), null, 2));
5072
+ return;
5073
+ }
5074
+ console.log(formatPmRecordDetails(synced.record, resolvePmLiveMetrics(synced.record)));
5075
+ }
3031
5076
  async function runPmStop(args) {
3032
5077
  const target = args[0];
3033
5078
  if (!target) {
@@ -3054,17 +5099,62 @@ async function runPmRestart(args) {
3054
5099
  await stopPmMatches(matches);
3055
5100
  const restarted = [];
3056
5101
  for (const match of matches) {
3057
- const definition = resolvePmAppDefinition(
3058
- toPmAppConfig(match.record),
3059
- { name: match.record.name, env: {}, watchPaths: [], watchIgnore: [] },
3060
- process.cwd(),
3061
- match.record.source
3062
- );
5102
+ const definition = rebuildPmRecordDefinition(match.record);
3063
5103
  await startManagedProcess(definition, paths);
3064
5104
  restarted.push(match.record.name);
3065
5105
  }
3066
5106
  console.log(`[pm] restarted ${restarted.join(", ")}`);
3067
5107
  }
5108
+ async function runPmReload(args) {
5109
+ const target = args[0];
5110
+ if (!target) {
5111
+ throw new Error("Usage: elit pm reload <name|all>");
5112
+ }
5113
+ const { paths } = await loadPmContext();
5114
+ const matches = resolveNamedMatches(paths, target);
5115
+ if (matches.length === 0) {
5116
+ throw new Error(`No managed process found for: ${target}`);
5117
+ }
5118
+ const reloaded = [];
5119
+ const errors = [];
5120
+ for (const group of groupPmMatchesByBaseName(matches)) {
5121
+ for (const match of group) {
5122
+ try {
5123
+ if (supportsPmProxyReload2(match.record)) {
5124
+ const reloadRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
5125
+ writePmRecord(match.filePath, {
5126
+ ...match.record,
5127
+ status: "restarting",
5128
+ reloadRequestedAt,
5129
+ updatedAt: reloadRequestedAt,
5130
+ error: void 0
5131
+ });
5132
+ await waitForPmRecordOnline(
5133
+ getPmRecordPath(paths, match.record.id),
5134
+ resolvePmReloadReadyTimeout(match.record)
5135
+ );
5136
+ reloaded.push(match.record.name);
5137
+ continue;
5138
+ }
5139
+ await stopPmMatches([match]);
5140
+ const definition = rebuildPmRecordDefinition(match.record);
5141
+ const startedRecord = await startManagedProcess(definition, paths);
5142
+ await waitForPmRecordOnline(
5143
+ getPmRecordPath(paths, startedRecord.id),
5144
+ resolvePmReloadReadyTimeout(startedRecord)
5145
+ );
5146
+ reloaded.push(match.record.name);
5147
+ } catch (error) {
5148
+ const message = error instanceof Error ? error.message : String(error);
5149
+ errors.push(`[pm] ${match.record.name}: ${message}`);
5150
+ }
5151
+ }
5152
+ }
5153
+ if (errors.length > 0) {
5154
+ throw new Error([`[pm] reloaded ${reloaded.length} process${reloaded.length === 1 ? "" : "es"}`, ...errors].join(import_node_os2.EOL));
5155
+ }
5156
+ console.log(`[pm] reloaded ${reloaded.join(", ")}`);
5157
+ }
3068
5158
  async function runPmSave() {
3069
5159
  const { paths } = await loadPmContext();
3070
5160
  ensurePmDirectories(paths);
@@ -3086,12 +5176,7 @@ async function runPmResurrect() {
3086
5176
  let restored = 0;
3087
5177
  for (const app of dump.apps) {
3088
5178
  try {
3089
- const definition = resolvePmAppDefinition(
3090
- toSavedPmAppConfig(app),
3091
- { name: app.name, env: {}, watchPaths: [], watchIgnore: [] },
3092
- process.cwd(),
3093
- "cli"
3094
- );
5179
+ const definition = rebuildPmSavedDefinition(app);
3095
5180
  await startManagedProcess(definition, paths);
3096
5181
  restored += 1;
3097
5182
  } catch (error) {
@@ -3115,16 +5200,127 @@ async function runPmDelete(args) {
3115
5200
  throw new Error(`No managed process found for: ${target}`);
3116
5201
  }
3117
5202
  await stopPmMatches(matches);
5203
+ deletePmMatches(matches);
5204
+ console.log(`[pm] deleted ${matches.length} process${matches.length === 1 ? "" : "es"}`);
5205
+ }
5206
+ async function runPmScale(args) {
5207
+ const target = args[0];
5208
+ const countArg = args[1];
5209
+ if (!target || countArg === void 0 || args.length > 2) {
5210
+ throw new Error("Usage: elit pm scale <name> <count>");
5211
+ }
5212
+ const desiredCount = normalizeIntegerOption(countArg, "pm scale <count>", 0);
5213
+ const { config, paths } = await loadPmContext();
5214
+ const exactMatch = findPmRecordMatch(paths, target);
5215
+ const currentMatches = sortPmMatchesByInstance(resolveNamedMatches(paths, exactMatch?.record.baseName ?? target));
5216
+ if (currentMatches.length === 0) {
5217
+ if (desiredCount === 0) {
5218
+ console.log(`[pm] ${target} already scaled to 0 instances`);
5219
+ return;
5220
+ }
5221
+ const definitions = resolvePmStartDefinitions(
5222
+ { name: target, env: {}, watchPaths: [], watchIgnore: [], instances: desiredCount },
5223
+ config,
5224
+ process.cwd()
5225
+ );
5226
+ for (const definition of definitions) {
5227
+ await startManagedProcess(definition, paths);
5228
+ }
5229
+ console.log(`[pm] scaled ${target} to ${desiredCount} instance${desiredCount === 1 ? "" : "s"}`);
5230
+ return;
5231
+ }
5232
+ const baseName = currentMatches[0]?.record.baseName ?? target;
5233
+ if (desiredCount === currentMatches.length) {
5234
+ console.log(`[pm] ${baseName} already scaled to ${desiredCount} instance${desiredCount === 1 ? "" : "s"}`);
5235
+ return;
5236
+ }
5237
+ if (desiredCount === 0) {
5238
+ await stopPmMatches(currentMatches);
5239
+ deletePmMatches(currentMatches);
5240
+ console.log(`[pm] scaled ${baseName} to 0 instances`);
5241
+ return;
5242
+ }
5243
+ if (desiredCount < currentMatches.length) {
5244
+ const toRemove = [...currentMatches].sort((left, right) => right.record.instanceIndex - left.record.instanceIndex).slice(0, currentMatches.length - desiredCount);
5245
+ const remaining = currentMatches.filter((match) => !toRemove.some((removal) => removal.record.id === match.record.id));
5246
+ await stopPmMatches(toRemove);
5247
+ deletePmMatches(toRemove);
5248
+ updatePmInstanceCount(remaining, desiredCount);
5249
+ console.log(`[pm] scaled ${baseName} to ${desiredCount} instance${desiredCount === 1 ? "" : "s"}`);
5250
+ return;
5251
+ }
5252
+ updatePmInstanceCount(currentMatches, desiredCount);
5253
+ const baseRecord = currentMatches[0]?.record;
5254
+ if (!baseRecord) {
5255
+ throw new Error(`No managed process found for: ${target}`);
5256
+ }
5257
+ const baseDefinition = rebuildPmRecordDefinition(baseRecord, desiredCount);
5258
+ const expandedDefinitions = expandPmInstanceDefinitions(baseDefinition, desiredCount);
5259
+ const existingNames = new Set(currentMatches.map((match) => match.record.name));
5260
+ for (const definition of expandedDefinitions) {
5261
+ if (existingNames.has(definition.name)) {
5262
+ continue;
5263
+ }
5264
+ await startManagedProcess(definition, paths);
5265
+ }
5266
+ console.log(`[pm] scaled ${baseName} to ${desiredCount} instance${desiredCount === 1 ? "" : "s"}`);
5267
+ }
5268
+ async function runPmSendSignal(args) {
5269
+ const signalArg = args[0];
5270
+ const target = args[1];
5271
+ if (!signalArg || !target || args.length > 2) {
5272
+ throw new Error("Usage: elit pm send-signal <signal> <name|all>");
5273
+ }
5274
+ const signalName = normalizePmSignalName(signalArg);
5275
+ const { paths } = await loadPmContext();
5276
+ const matches = resolveNamedMatches(paths, target);
5277
+ if (matches.length === 0) {
5278
+ throw new Error(`No managed process found for: ${target}`);
5279
+ }
5280
+ let signaled = 0;
5281
+ const errors = [];
3118
5282
  for (const match of matches) {
3119
- if ((0, import_node_fs5.existsSync)(match.record.logFiles.out)) {
3120
- (0, import_node_fs5.rmSync)(match.record.logFiles.out, { force: true });
5283
+ const pid = resolveSignalablePid(match.record);
5284
+ if (!pid) {
5285
+ continue;
3121
5286
  }
3122
- if ((0, import_node_fs5.existsSync)(match.record.logFiles.err)) {
3123
- (0, import_node_fs5.rmSync)(match.record.logFiles.err, { force: true });
5287
+ try {
5288
+ sendPmSignal(pid, signalName);
5289
+ signaled += 1;
5290
+ } catch (error) {
5291
+ const message = error instanceof Error ? error.message : String(error);
5292
+ errors.push(`[pm] ${match.record.name}: ${message}`);
3124
5293
  }
3125
- (0, import_node_fs5.rmSync)(match.filePath, { force: true });
3126
5294
  }
3127
- console.log(`[pm] deleted ${matches.length} process${matches.length === 1 ? "" : "es"}`);
5295
+ if (errors.length > 0) {
5296
+ throw new Error([`[pm] sent ${signalName} to ${signaled} process${signaled === 1 ? "" : "es"}`, ...errors].join(import_node_os2.EOL));
5297
+ }
5298
+ if (signaled === 0) {
5299
+ throw new Error(`No running managed process found for: ${target}`);
5300
+ }
5301
+ console.log(`[pm] sent ${signalName} to ${signaled} process${signaled === 1 ? "" : "es"}`);
5302
+ }
5303
+ async function runPmReset(args) {
5304
+ const target = args[0];
5305
+ if (!target) {
5306
+ throw new Error("Usage: elit pm reset <name|all>");
5307
+ }
5308
+ const { paths } = await loadPmContext();
5309
+ const matches = resolveNamedMatches(paths, target);
5310
+ if (matches.length === 0) {
5311
+ throw new Error(`No managed process found for: ${target}`);
5312
+ }
5313
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5314
+ for (const match of matches) {
5315
+ writePmRecord(match.filePath, {
5316
+ ...match.record,
5317
+ restartCount: 0,
5318
+ lastExitCode: void 0,
5319
+ error: void 0,
5320
+ updatedAt: now
5321
+ });
5322
+ }
5323
+ console.log(`[pm] reset ${matches.length} process${matches.length === 1 ? "" : "es"}`);
3128
5324
  }
3129
5325
  async function runPmLogs(args) {
3130
5326
  if (args.length === 0) {
@@ -3157,7 +5353,7 @@ async function runPmLogs(args) {
3157
5353
  throw new Error("Usage: elit pm logs <name> [--lines <n>] [--stderr]");
3158
5354
  }
3159
5355
  const { paths } = await loadPmContext();
3160
- const match = findPmRecordMatch(paths, name);
5356
+ const match = resolveInspectableMatch(paths, name);
3161
5357
  if (!match) {
3162
5358
  throw new Error(`No managed process found for: ${name}`);
3163
5359
  }
@@ -3181,17 +5377,36 @@ async function runPmCommand(args) {
3181
5377
  await runPmStart(args.slice(1));
3182
5378
  return;
3183
5379
  case "list":
3184
- case "ls": {
3185
- const { paths } = await loadPmContext();
3186
- printPmList(paths);
5380
+ case "ls":
5381
+ await runPmList(args.slice(1));
5382
+ return;
5383
+ case "jlist":
5384
+ await runPmList(["--json", ...args.slice(1)]);
5385
+ return;
5386
+ case "show":
5387
+ case "describe":
5388
+ await runPmShow(args.slice(1));
3187
5389
  return;
3188
- }
3189
5390
  case "stop":
3190
5391
  await runPmStop(args.slice(1));
3191
5392
  return;
3192
5393
  case "restart":
3193
5394
  await runPmRestart(args.slice(1));
3194
5395
  return;
5396
+ case "reload":
5397
+ await runPmReload(args.slice(1));
5398
+ return;
5399
+ case "scale":
5400
+ await runPmScale(args.slice(1));
5401
+ return;
5402
+ case "send-signal":
5403
+ case "signal":
5404
+ case "sendSignal":
5405
+ await runPmSendSignal(args.slice(1));
5406
+ return;
5407
+ case "reset":
5408
+ await runPmReset(args.slice(1));
5409
+ return;
3195
5410
  case "delete":
3196
5411
  case "remove":
3197
5412
  case "rm":
@@ -3224,6 +5439,12 @@ async function runPmCommand(args) {
3224
5439
  DEFAULT_MIN_UPTIME,
3225
5440
  DEFAULT_PM_DATA_DIR,
3226
5441
  DEFAULT_PM_DUMP_FILE,
5442
+ DEFAULT_PM_EXP_BACKOFF_MAX_DELAY,
5443
+ DEFAULT_PM_KILL_TIMEOUT,
5444
+ DEFAULT_PM_LISTEN_TIMEOUT,
5445
+ DEFAULT_PM_MEMORY_CHECK_INTERVAL,
5446
+ DEFAULT_PM_PROXY_STRATEGY,
5447
+ DEFAULT_PM_RESTART_WINDOW,
3227
5448
  DEFAULT_PM_STOP_GRACE_PERIOD_MS,
3228
5449
  DEFAULT_PM_STOP_POLL_MS,
3229
5450
  DEFAULT_RESTART_DELAY,
@@ -3242,7 +5463,10 @@ async function runPmCommand(args) {
3242
5463
  countDefinedPmWapkSources,
3243
5464
  deriveDefaultWatchPaths,
3244
5465
  ensurePmDirectories,
5466
+ expandPmInstanceDefinitions,
5467
+ findPmGroupMatches,
3245
5468
  findPmRecordMatch,
5469
+ formatPmInstanceName,
3246
5470
  getPmRecordPath,
3247
5471
  hasPmGoogleDriveConfig,
3248
5472
  hasPmWapkRunConfig,
@@ -3261,6 +5485,10 @@ async function runPmCommand(args) {
3261
5485
  normalizeHealthCheckConfig,
3262
5486
  normalizeIntegerOption,
3263
5487
  normalizeNonEmptyString,
5488
+ normalizePmMemoryAction,
5489
+ normalizePmMemoryLimit,
5490
+ normalizePmProxyConfig,
5491
+ normalizePmProxyStrategy,
3264
5492
  normalizePmRestartPolicy,
3265
5493
  normalizePmRuntime,
3266
5494
  normalizeResolvedWatchIgnorePaths,
@@ -3284,7 +5512,9 @@ async function runPmCommand(args) {
3284
5512
  runManagedProcessLoop,
3285
5513
  runPmCommand,
3286
5514
  runPmRunner,
5515
+ samplePmProcessMetrics,
3287
5516
  sanitizePmProcessName,
5517
+ sendPmSignal,
3288
5518
  startManagedProcess,
3289
5519
  stopPmMatches,
3290
5520
  stripPmWapkSourceFromRunConfig,