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.mjs CHANGED
@@ -461,6 +461,7 @@ var DEFAULT_RESTART_DELAY = 1e3;
461
461
  var DEFAULT_MAX_RESTARTS = 10;
462
462
  var DEFAULT_WATCH_DEBOUNCE = 250;
463
463
  var DEFAULT_MIN_UPTIME = 0;
464
+ var DEFAULT_PM_LISTEN_TIMEOUT = 3e3;
464
465
  var DEFAULT_HEALTHCHECK_GRACE_PERIOD = 5e3;
465
466
  var DEFAULT_HEALTHCHECK_INTERVAL = 1e4;
466
467
  var DEFAULT_HEALTHCHECK_TIMEOUT = 3e3;
@@ -468,6 +469,11 @@ var DEFAULT_HEALTHCHECK_MAX_FAILURES = 3;
468
469
  var DEFAULT_LOG_LINES = 40;
469
470
  var DEFAULT_PM_STOP_POLL_MS = 100;
470
471
  var DEFAULT_PM_STOP_GRACE_PERIOD_MS = 5e3;
472
+ var DEFAULT_PM_KILL_TIMEOUT = DEFAULT_PM_STOP_GRACE_PERIOD_MS;
473
+ var DEFAULT_PM_EXP_BACKOFF_MAX_DELAY = 15e3;
474
+ var DEFAULT_PM_MEMORY_CHECK_INTERVAL = 500;
475
+ var DEFAULT_PM_RESTART_WINDOW = 0;
476
+ var DEFAULT_PM_PROXY_STRATEGY = "proxy";
471
477
  var PM_WAPK_ONLINE_STDIN_SHUTDOWN_ENV = "ELIT_PM_WAPK_ONLINE_STDIN_SHUTDOWN";
472
478
  var PM_WAPK_ONLINE_SHUTDOWN_COMMAND = "__ELIT_PM_WAPK_ONLINE_SHUTDOWN__";
473
479
  var PM_WAPK_ONLINE_SHUTDOWN_TIMEOUT_MS = 8e3;
@@ -505,6 +511,32 @@ function normalizePmRestartPolicy(value, optionName = "--restart-policy") {
505
511
  }
506
512
  throw new Error(`${optionName} must be one of: always, on-failure, never`);
507
513
  }
514
+ function normalizePmMemoryAction(value, optionName = "--memory-action") {
515
+ if (value === void 0 || value === null || value === "") {
516
+ return void 0;
517
+ }
518
+ if (typeof value !== "string") {
519
+ throw new Error(`${optionName} must be one of: restart, stop`);
520
+ }
521
+ const action = value.trim().toLowerCase();
522
+ if (action === "restart" || action === "stop") {
523
+ return action;
524
+ }
525
+ throw new Error(`${optionName} must be one of: restart, stop`);
526
+ }
527
+ function normalizePmProxyStrategy(value, optionName = "--proxy-strategy") {
528
+ if (value === void 0 || value === null || value === "") {
529
+ return void 0;
530
+ }
531
+ if (typeof value !== "string") {
532
+ throw new Error(`${optionName} must be one of: proxy, inherit`);
533
+ }
534
+ const strategy = value.trim().toLowerCase();
535
+ if (strategy === "proxy" || strategy === "inherit") {
536
+ return strategy;
537
+ }
538
+ throw new Error(`${optionName} must be one of: proxy, inherit`);
539
+ }
508
540
  function normalizeIntegerOption(value, optionName, min = 0) {
509
541
  const parsed = Number.parseInt(value, 10);
510
542
  if (!Number.isFinite(parsed) || parsed < min) {
@@ -512,6 +544,30 @@ function normalizeIntegerOption(value, optionName, min = 0) {
512
544
  }
513
545
  return parsed;
514
546
  }
547
+ function normalizePmMemoryLimit(value, optionName = "--max-memory") {
548
+ if (value === void 0 || value === null || value === "") {
549
+ return void 0;
550
+ }
551
+ if (typeof value === "number") {
552
+ const normalized2 = Math.trunc(value);
553
+ if (!Number.isFinite(normalized2) || normalized2 < 1) {
554
+ throw new Error(`${optionName} must be a number >= 1 or a size like 256M.`);
555
+ }
556
+ return normalized2;
557
+ }
558
+ if (typeof value !== "string") {
559
+ throw new Error(`${optionName} must be a number >= 1 or a size like 256M.`);
560
+ }
561
+ const normalized = value.trim();
562
+ const match = /^(\d+)(b|kb|mb|gb|tb|k|m|g|t)?$/i.exec(normalized);
563
+ if (!match) {
564
+ throw new Error(`${optionName} must be a number >= 1 or a size like 256M.`);
565
+ }
566
+ const amount = normalizeIntegerOption(match[1] ?? "", optionName, 1);
567
+ const unit = (match[2] ?? "b").toLowerCase();
568
+ const multiplier = unit === "b" ? 1 : unit === "k" || unit === "kb" ? 1024 : unit === "m" || unit === "mb" ? 1024 ** 2 : unit === "g" || unit === "gb" ? 1024 ** 3 : 1024 ** 4;
569
+ return amount * multiplier;
570
+ }
515
571
  function normalizeNonEmptyString(value) {
516
572
  if (typeof value !== "string") {
517
573
  return void 0;
@@ -788,6 +844,30 @@ function readRequiredValue(args, index, optionName) {
788
844
  }
789
845
  return value;
790
846
  }
847
+ function normalizePmProxyConfig(value, optionName = "pm proxy") {
848
+ if (value === void 0 || value === null) {
849
+ return void 0;
850
+ }
851
+ if (typeof value !== "object") {
852
+ throw new Error(`${optionName} must be an object with at least a port.`);
853
+ }
854
+ const candidate = value;
855
+ const hasAnyValue = candidate.port !== void 0 || candidate.host !== void 0 || candidate.targetHost !== void 0 || candidate.envVar !== void 0;
856
+ if (!hasAnyValue) {
857
+ return void 0;
858
+ }
859
+ if (candidate.port === void 0 || candidate.port === null || candidate.port === "") {
860
+ throw new Error(`${optionName}.port is required.`);
861
+ }
862
+ const port = typeof candidate.port === "number" ? normalizeIntegerOption(String(Math.trunc(candidate.port)), `${optionName}.port`, 1) : normalizeIntegerOption(String(candidate.port), `${optionName}.port`, 1);
863
+ return {
864
+ port,
865
+ strategy: normalizePmProxyStrategy(candidate.strategy, `${optionName}.strategy`) ?? DEFAULT_PM_PROXY_STRATEGY,
866
+ host: normalizeNonEmptyString(candidate.host),
867
+ targetHost: normalizeNonEmptyString(candidate.targetHost),
868
+ envVar: normalizeNonEmptyString(candidate.envVar)
869
+ };
870
+ }
791
871
 
792
872
  // src/cli/pm/records.ts
793
873
  import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
@@ -820,18 +900,31 @@ function writePmRecord(filePath, record) {
820
900
  function toSavedAppDefinition(record) {
821
901
  return {
822
902
  name: record.name,
903
+ baseName: record.baseName,
904
+ instanceIndex: record.instanceIndex,
905
+ instances: record.instances,
823
906
  type: record.type,
824
907
  cwd: record.cwd,
825
908
  runtime: record.runtime,
826
909
  env: record.env,
910
+ proxy: record.proxy,
827
911
  script: record.script,
828
912
  file: record.file,
829
913
  wapk: record.wapk,
830
914
  password: record.password,
831
915
  wapkRun: record.wapkRun,
832
916
  restartPolicy: record.restartPolicy,
917
+ maxMemoryBytes: record.maxMemoryBytes,
918
+ memoryAction: record.memoryAction,
919
+ cronRestart: record.cronRestart,
920
+ expBackoffRestartDelay: record.expBackoffRestartDelay,
921
+ expBackoffRestartMaxDelay: record.expBackoffRestartMaxDelay,
922
+ restartWindow: record.restartWindow,
923
+ waitReady: record.waitReady,
924
+ listenTimeout: record.listenTimeout,
833
925
  autorestart: record.autorestart,
834
926
  restartDelay: record.restartDelay,
927
+ killTimeout: record.killTimeout,
835
928
  maxRestarts: record.maxRestarts,
836
929
  minUptime: record.minUptime,
837
930
  watch: record.watch,
@@ -898,6 +991,9 @@ function findPmRecordMatch(paths, nameOrId) {
898
991
  }
899
992
  return listPmRecordMatches(paths).find((match) => match.record.name === nameOrId);
900
993
  }
994
+ function findPmGroupMatches(paths, baseName) {
995
+ return listPmRecordMatches(paths).filter((match) => match.record.baseName === baseName).sort((left, right) => left.record.instanceIndex - right.record.instanceIndex);
996
+ }
901
997
  function isProcessAlive(pid) {
902
998
  if (!pid || pid <= 0) {
903
999
  return false;
@@ -947,11 +1043,22 @@ function toPmAppConfig(record) {
947
1043
  runtime: record.runtime,
948
1044
  cwd: record.cwd,
949
1045
  env: record.env,
1046
+ proxy: record.proxy,
1047
+ instances: record.instances,
950
1048
  autorestart: record.autorestart,
951
1049
  restartDelay: record.restartDelay,
1050
+ killTimeout: record.killTimeout,
952
1051
  maxRestarts: record.maxRestarts,
953
1052
  password: record.password,
954
1053
  restartPolicy: record.restartPolicy,
1054
+ maxMemory: record.maxMemoryBytes,
1055
+ memoryAction: record.memoryAction,
1056
+ cronRestart: record.cronRestart,
1057
+ expBackoffRestartDelay: record.expBackoffRestartDelay,
1058
+ expBackoffRestartMaxDelay: record.expBackoffRestartMaxDelay,
1059
+ restartWindow: record.restartWindow,
1060
+ waitReady: record.waitReady,
1061
+ listenTimeout: record.listenTimeout,
955
1062
  minUptime: record.minUptime,
956
1063
  watch: record.watch,
957
1064
  watchPaths: record.watchPaths,
@@ -970,11 +1077,22 @@ function toSavedPmAppConfig(app) {
970
1077
  runtime: app.runtime,
971
1078
  cwd: app.cwd,
972
1079
  env: app.env,
1080
+ proxy: app.proxy,
1081
+ instances: app.instances,
973
1082
  autorestart: app.autorestart,
974
1083
  restartDelay: app.restartDelay,
1084
+ killTimeout: app.killTimeout,
975
1085
  maxRestarts: app.maxRestarts,
976
1086
  password: app.password,
977
1087
  restartPolicy: app.restartPolicy,
1088
+ maxMemory: app.maxMemoryBytes,
1089
+ memoryAction: app.memoryAction,
1090
+ cronRestart: app.cronRestart,
1091
+ expBackoffRestartDelay: app.expBackoffRestartDelay,
1092
+ expBackoffRestartMaxDelay: app.expBackoffRestartMaxDelay,
1093
+ restartWindow: app.restartWindow,
1094
+ waitReady: app.waitReady,
1095
+ listenTimeout: app.listenTimeout,
978
1096
  minUptime: app.minUptime,
979
1097
  watch: app.watch,
980
1098
  watchPaths: app.watchPaths,
@@ -986,6 +1104,170 @@ function toSavedPmAppConfig(app) {
986
1104
 
987
1105
  // src/cli/pm/config.ts
988
1106
  import { resolve as resolve3 } from "path";
1107
+
1108
+ // src/cli/pm/schedule.ts
1109
+ var PM_EVERY_PATTERN = /^@every\s+(\d+)(ms|s|m|h|d)$/i;
1110
+ function parsePositiveInteger(value, optionName, min = 0) {
1111
+ const parsed = Number.parseInt(value, 10);
1112
+ if (!Number.isFinite(parsed) || parsed < min) {
1113
+ throw new Error(`${optionName} must be a number >= ${min}`);
1114
+ }
1115
+ return parsed;
1116
+ }
1117
+ function fillRange(values, start, end, step, min, max, optionName) {
1118
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < min || end > max || start > end) {
1119
+ throw new Error(`${optionName} contains an invalid range: ${start}-${end}`);
1120
+ }
1121
+ if (!Number.isInteger(step) || step < 1) {
1122
+ throw new Error(`${optionName} contains an invalid step: ${step}`);
1123
+ }
1124
+ for (let current = start; current <= end; current += step) {
1125
+ values.add(current);
1126
+ }
1127
+ }
1128
+ function parseCronSegment(segment, values, min, max, optionName) {
1129
+ const normalized = segment.trim();
1130
+ if (!normalized) {
1131
+ throw new Error(`${optionName} contains an empty field segment.`);
1132
+ }
1133
+ const [rangeToken, stepToken] = normalized.split("/");
1134
+ if (normalized.split("/").length > 2) {
1135
+ throw new Error(`${optionName} contains an invalid step segment: ${normalized}`);
1136
+ }
1137
+ const step = stepToken === void 0 ? 1 : parsePositiveInteger(stepToken, optionName, 1);
1138
+ if (rangeToken === "*") {
1139
+ fillRange(values, min, max, step, min, max, optionName);
1140
+ return;
1141
+ }
1142
+ if (rangeToken.includes("-")) {
1143
+ const [startToken, endToken] = rangeToken.split("-");
1144
+ if (!startToken || !endToken) {
1145
+ throw new Error(`${optionName} contains an invalid range: ${normalized}`);
1146
+ }
1147
+ const start2 = parsePositiveInteger(startToken, optionName, min);
1148
+ const end = parsePositiveInteger(endToken, optionName, min);
1149
+ fillRange(values, start2, end, step, min, max, optionName);
1150
+ return;
1151
+ }
1152
+ const start = parsePositiveInteger(rangeToken, optionName, min);
1153
+ fillRange(values, start, stepToken === void 0 ? start : max, step, min, max, optionName);
1154
+ }
1155
+ function parseCronField(token, min, max, optionName) {
1156
+ const normalized = token.trim();
1157
+ if (!normalized) {
1158
+ throw new Error(`${optionName} contains an empty field.`);
1159
+ }
1160
+ if (normalized === "*") {
1161
+ const values2 = /* @__PURE__ */ new Set();
1162
+ fillRange(values2, min, max, 1, min, max, optionName);
1163
+ return {
1164
+ values: values2,
1165
+ wildcard: true
1166
+ };
1167
+ }
1168
+ const values = /* @__PURE__ */ new Set();
1169
+ for (const segment of normalized.split(",")) {
1170
+ parseCronSegment(segment, values, min, max, optionName);
1171
+ }
1172
+ if (values.size === 0) {
1173
+ throw new Error(`${optionName} must match at least one value.`);
1174
+ }
1175
+ return {
1176
+ values,
1177
+ wildcard: false
1178
+ };
1179
+ }
1180
+ function matchesCronField(field, value) {
1181
+ if (!field) {
1182
+ return true;
1183
+ }
1184
+ return field.wildcard || field.values.has(value);
1185
+ }
1186
+ function matchesCronDate(schedule, candidate) {
1187
+ if (schedule.kind !== "cron") {
1188
+ return false;
1189
+ }
1190
+ const secondMatches = schedule.hasSeconds ? matchesCronField(schedule.seconds, candidate.getSeconds()) : true;
1191
+ const minuteMatches = matchesCronField(schedule.minutes, candidate.getMinutes());
1192
+ const hourMatches = matchesCronField(schedule.hours, candidate.getHours());
1193
+ const monthMatches = matchesCronField(schedule.months, candidate.getMonth() + 1);
1194
+ const dayOfMonthMatches = matchesCronField(schedule.daysOfMonth, candidate.getDate());
1195
+ const dayOfWeekMatches = matchesCronField(schedule.daysOfWeek, candidate.getDay());
1196
+ const domWildcard = schedule.daysOfMonth?.wildcard ?? true;
1197
+ const dowWildcard = schedule.daysOfWeek?.wildcard ?? true;
1198
+ const dayMatches = domWildcard && dowWildcard ? true : domWildcard ? dayOfWeekMatches : dowWildcard ? dayOfMonthMatches : dayOfMonthMatches || dayOfWeekMatches;
1199
+ return secondMatches && minuteMatches && hourMatches && monthMatches && dayMatches;
1200
+ }
1201
+ function parsePmRestartSchedule(expression, optionName = "--cron-restart") {
1202
+ const normalized = expression.trim();
1203
+ const everyMatch = PM_EVERY_PATTERN.exec(normalized);
1204
+ if (everyMatch) {
1205
+ const [, valueText, unit] = everyMatch;
1206
+ const value = parsePositiveInteger(valueText ?? "", optionName, 1);
1207
+ const multiplier = unit?.toLowerCase() === "ms" ? 1 : unit?.toLowerCase() === "s" ? 1e3 : unit?.toLowerCase() === "m" ? 6e4 : unit?.toLowerCase() === "h" ? 36e5 : 864e5;
1208
+ return {
1209
+ expression: normalized,
1210
+ kind: "every",
1211
+ intervalMs: value * multiplier,
1212
+ hasSeconds: true
1213
+ };
1214
+ }
1215
+ const fields = normalized.split(/\s+/).filter((field) => field.length > 0);
1216
+ if (fields.length !== 5 && fields.length !== 6) {
1217
+ throw new Error(`${optionName} must be a 5-field cron expression or @every <duration>.`);
1218
+ }
1219
+ const hasSeconds = fields.length === 6;
1220
+ const offset = hasSeconds ? 1 : 0;
1221
+ return {
1222
+ expression: normalized,
1223
+ kind: "cron",
1224
+ hasSeconds,
1225
+ seconds: hasSeconds ? parseCronField(fields[0] ?? "", 0, 59, optionName) : void 0,
1226
+ minutes: parseCronField(fields[0 + offset] ?? "", 0, 59, optionName),
1227
+ hours: parseCronField(fields[1 + offset] ?? "", 0, 23, optionName),
1228
+ daysOfMonth: parseCronField(fields[2 + offset] ?? "", 1, 31, optionName),
1229
+ months: parseCronField(fields[3 + offset] ?? "", 1, 12, optionName),
1230
+ daysOfWeek: parseCronField(fields[4 + offset] ?? "", 0, 6, optionName)
1231
+ };
1232
+ }
1233
+ function normalizePmRestartSchedule(value, optionName = "--cron-restart") {
1234
+ if (value === void 0 || value === null || value === "") {
1235
+ return void 0;
1236
+ }
1237
+ if (typeof value !== "string") {
1238
+ throw new Error(`${optionName} must be a cron string or @every <duration>.`);
1239
+ }
1240
+ const normalized = value.trim();
1241
+ if (!normalized) {
1242
+ throw new Error(`${optionName} must be a non-empty cron string or @every <duration>.`);
1243
+ }
1244
+ parsePmRestartSchedule(normalized, optionName);
1245
+ return normalized;
1246
+ }
1247
+ function resolveNextPmScheduleOccurrence(schedule, after = /* @__PURE__ */ new Date()) {
1248
+ if (schedule.kind === "every") {
1249
+ return new Date(after.getTime() + (schedule.intervalMs ?? 0));
1250
+ }
1251
+ const stepMs = schedule.hasSeconds ? 1e3 : 6e4;
1252
+ const candidate = new Date(after.getTime());
1253
+ if (schedule.hasSeconds) {
1254
+ candidate.setMilliseconds(0);
1255
+ candidate.setSeconds(candidate.getSeconds() + 1);
1256
+ } else {
1257
+ candidate.setSeconds(0, 0);
1258
+ candidate.setMinutes(candidate.getMinutes() + 1);
1259
+ }
1260
+ const maxIterations = schedule.hasSeconds ? 2678400 : 527040;
1261
+ for (let index = 0; index < maxIterations; index++) {
1262
+ if (matchesCronDate(schedule, candidate)) {
1263
+ return new Date(candidate.getTime());
1264
+ }
1265
+ candidate.setTime(candidate.getTime() + stepMs);
1266
+ }
1267
+ return void 0;
1268
+ }
1269
+
1270
+ // src/cli/pm/config.ts
989
1271
  function parsePmTarget(parsed, workspaceRoot) {
990
1272
  if (parsed.script) {
991
1273
  return { script: parsed.script };
@@ -1028,6 +1310,20 @@ function defaultProcessName(base, explicitName) {
1028
1310
  }
1029
1311
  return "process";
1030
1312
  }
1313
+ function formatPmInstanceName(baseName, instanceIndex) {
1314
+ return instanceIndex <= 1 ? baseName : `${baseName}:${instanceIndex}`;
1315
+ }
1316
+ function expandPmInstanceDefinitions(definition, targetInstances = definition.instances) {
1317
+ return Array.from({ length: targetInstances }, (_, index) => {
1318
+ const instanceIndex = index + 1;
1319
+ return {
1320
+ ...definition,
1321
+ name: formatPmInstanceName(definition.baseName, instanceIndex),
1322
+ instanceIndex,
1323
+ instances: targetInstances
1324
+ };
1325
+ });
1326
+ }
1031
1327
  function countDefinedTargets(app) {
1032
1328
  return [app.script, app.file, app.wapk].filter(Boolean).length;
1033
1329
  }
@@ -1065,8 +1361,25 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1065
1361
  throw new Error("A pm app must define exactly one of script, file, or wapk.");
1066
1362
  }
1067
1363
  const name = defaultProcessName({ script, file, wapk }, parsed.name ?? base?.name);
1364
+ const instances = parsed.instances ?? base?.instances ?? 1;
1365
+ if (!Number.isInteger(instances) || instances < 1) {
1366
+ throw new Error("pm instances must be a number >= 1.");
1367
+ }
1368
+ const proxy = normalizePmProxyConfig(
1369
+ parsed.proxy ? { ...base?.proxy ?? {}, ...parsed.proxy } : base?.proxy,
1370
+ parsed.proxy ? "pm proxy" : "pm.apps[].proxy"
1371
+ );
1372
+ if (proxy?.strategy === "inherit" && instances > 1) {
1373
+ throw new Error('pm proxy strategy "inherit" currently supports only one managed instance per app.');
1374
+ }
1068
1375
  const mergedWapkRun = mergePmWapkRunConfig(base?.wapkRun, parsed.wapkRun);
1069
1376
  const runtime2 = normalizePmRuntime(parsed.runtime ?? mergedWapkRun?.runtime ?? base?.runtime, "--runtime");
1377
+ const maxMemoryBytes = parsed.maxMemoryBytes ?? normalizePmMemoryLimit(base?.maxMemory, "pm.apps[].maxMemory");
1378
+ const memoryAction = parsed.memoryAction ?? normalizePmMemoryAction(base?.memoryAction, "pm.apps[].memoryAction") ?? "restart";
1379
+ const cronRestart = parsed.cronRestart ?? normalizePmRestartSchedule(base?.cronRestart, "pm.apps[].cronRestart");
1380
+ const expBackoffRestartDelay = parsed.expBackoffRestartDelay ?? (base?.expBackoffRestartDelay === void 0 ? void 0 : normalizeIntegerOption(String(base.expBackoffRestartDelay), "pm.apps[].expBackoffRestartDelay", 1));
1381
+ const expBackoffRestartMaxDelay = parsed.expBackoffRestartMaxDelay ?? (base?.expBackoffRestartMaxDelay === void 0 ? void 0 : normalizeIntegerOption(String(base.expBackoffRestartMaxDelay), "pm.apps[].expBackoffRestartMaxDelay", 1));
1382
+ const restartWindow = parsed.restartWindow ?? (base?.restartWindow === void 0 ? void 0 : normalizeIntegerOption(String(base.restartWindow), "pm.apps[].restartWindow", 1));
1070
1383
  let restartPolicy = normalizePmRestartPolicy(parsed.restartPolicy ?? base?.restartPolicy, "--restart-policy") ?? (base?.autorestart ?? true ? "always" : "never");
1071
1384
  if (parsed.autorestart === false) {
1072
1385
  restartPolicy = "never";
@@ -1086,6 +1399,7 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1086
1399
  timeout: parsed.healthCheckTimeout,
1087
1400
  maxFailures: parsed.healthCheckMaxFailures
1088
1401
  } : base?.healthCheck);
1402
+ const waitReady = parsed.waitReady ?? base?.waitReady ?? false;
1089
1403
  const password = parsed.password ?? mergedWapkRun?.password ?? base?.password;
1090
1404
  const wapkRun = stripPmWapkSourceFromRunConfig(mergedWapkRun);
1091
1405
  if (password && !wapk) {
@@ -1094,8 +1408,14 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1094
1408
  if (wapkRun && !wapk) {
1095
1409
  throw new Error("WAPK run options are only supported when starting a WAPK app.");
1096
1410
  }
1411
+ if (waitReady && !healthCheck) {
1412
+ throw new Error("--wait-ready requires --health-url or pm.apps[].healthCheck.");
1413
+ }
1097
1414
  return {
1098
- name,
1415
+ name: formatPmInstanceName(name, 1),
1416
+ baseName: name,
1417
+ instanceIndex: 1,
1418
+ instances,
1099
1419
  type: script ? "script" : wapk ? "wapk" : "file",
1100
1420
  source,
1101
1421
  cwd: resolvedCwd,
@@ -1110,9 +1430,19 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1110
1430
  wapkRun,
1111
1431
  autorestart,
1112
1432
  restartDelay: parsed.restartDelay ?? base?.restartDelay ?? DEFAULT_RESTART_DELAY,
1433
+ proxy,
1434
+ killTimeout: parsed.killTimeout ?? base?.killTimeout ?? DEFAULT_PM_KILL_TIMEOUT,
1113
1435
  maxRestarts: parsed.maxRestarts ?? base?.maxRestarts ?? DEFAULT_MAX_RESTARTS,
1114
1436
  password,
1115
1437
  restartPolicy,
1438
+ maxMemoryBytes,
1439
+ memoryAction,
1440
+ cronRestart,
1441
+ expBackoffRestartDelay,
1442
+ expBackoffRestartMaxDelay,
1443
+ restartWindow,
1444
+ waitReady,
1445
+ listenTimeout: parsed.listenTimeout ?? base?.listenTimeout ?? DEFAULT_PM_LISTEN_TIMEOUT,
1116
1446
  minUptime: parsed.minUptime ?? base?.minUptime ?? DEFAULT_MIN_UPTIME,
1117
1447
  watch: watch2,
1118
1448
  watchPaths: watch2 ? normalizeResolvedWatchPaths(configuredWatchPaths, resolvedCwd, script ? "script" : wapk ? "wapk" : "file", file, wapk) : [],
@@ -1124,16 +1454,17 @@ function resolvePmAppDefinition(base, parsed, workspaceRoot, source) {
1124
1454
  function resolvePmStartDefinitions(parsed, config, workspaceRoot) {
1125
1455
  const configApps = getConfiguredPmApps(config);
1126
1456
  const selection = resolveStartSelection(configApps, parsed, workspaceRoot);
1457
+ const expandDefinitions = (definitions) => definitions.flatMap((definition) => expandPmInstanceDefinitions(definition));
1127
1458
  if (selection.startAll) {
1128
1459
  if (configApps.length === 0) {
1129
1460
  throw new Error("No pm apps configured in elit.config.* and no start target was provided.");
1130
1461
  }
1131
- return configApps.map((app) => resolvePmAppDefinition(app, { ...parsed, name: app.name }, workspaceRoot, "config"));
1462
+ return expandDefinitions(configApps.map((app) => resolvePmAppDefinition(app, { ...parsed, name: app.name }, workspaceRoot, "config")));
1132
1463
  }
1133
1464
  if (selection.selected) {
1134
- return [resolvePmAppDefinition(selection.selected, parsed, workspaceRoot, "config")];
1465
+ return expandDefinitions([resolvePmAppDefinition(selection.selected, parsed, workspaceRoot, "config")]);
1135
1466
  }
1136
- return [resolvePmAppDefinition(void 0, parsed, workspaceRoot, "cli")];
1467
+ return expandDefinitions([resolvePmAppDefinition(void 0, parsed, workspaceRoot, "cli")]);
1137
1468
  }
1138
1469
  function parsePmStartArgs(args) {
1139
1470
  const parsed = {
@@ -1206,6 +1537,39 @@ function parsePmStartArgs(args) {
1206
1537
  parsed.env[key] = value;
1207
1538
  break;
1208
1539
  }
1540
+ case "--instances":
1541
+ parsed.instances = normalizeIntegerOption(readRequiredValue(args, ++index, "--instances"), "--instances", 1);
1542
+ break;
1543
+ case "--proxy-port":
1544
+ parsed.proxy = {
1545
+ ...parsed.proxy,
1546
+ port: normalizeIntegerOption(readRequiredValue(args, ++index, "--proxy-port"), "--proxy-port", 1)
1547
+ };
1548
+ break;
1549
+ case "--proxy-strategy":
1550
+ parsed.proxy = {
1551
+ ...parsed.proxy,
1552
+ strategy: normalizePmProxyStrategy(readRequiredValue(args, ++index, "--proxy-strategy"), "--proxy-strategy")
1553
+ };
1554
+ break;
1555
+ case "--proxy-host":
1556
+ parsed.proxy = {
1557
+ ...parsed.proxy,
1558
+ host: readRequiredValue(args, ++index, "--proxy-host")
1559
+ };
1560
+ break;
1561
+ case "--proxy-target-host":
1562
+ parsed.proxy = {
1563
+ ...parsed.proxy,
1564
+ targetHost: readRequiredValue(args, ++index, "--proxy-target-host")
1565
+ };
1566
+ break;
1567
+ case "--proxy-env":
1568
+ parsed.proxy = {
1569
+ ...parsed.proxy,
1570
+ envVar: readRequiredValue(args, ++index, "--proxy-env")
1571
+ };
1572
+ break;
1209
1573
  case "--password":
1210
1574
  parsed.password = readRequiredValue(args, ++index, "--password");
1211
1575
  break;
@@ -1256,6 +1620,30 @@ function parsePmStartArgs(args) {
1256
1620
  case "--restart-policy":
1257
1621
  parsed.restartPolicy = normalizePmRestartPolicy(readRequiredValue(args, ++index, "--restart-policy"));
1258
1622
  break;
1623
+ case "--max-memory":
1624
+ parsed.maxMemoryBytes = normalizePmMemoryLimit(readRequiredValue(args, ++index, "--max-memory"), "--max-memory");
1625
+ break;
1626
+ case "--memory-action":
1627
+ parsed.memoryAction = normalizePmMemoryAction(readRequiredValue(args, ++index, "--memory-action"), "--memory-action");
1628
+ break;
1629
+ case "--cron-restart":
1630
+ parsed.cronRestart = normalizePmRestartSchedule(readRequiredValue(args, ++index, "--cron-restart"), "--cron-restart");
1631
+ break;
1632
+ case "--exp-backoff-restart-delay":
1633
+ parsed.expBackoffRestartDelay = normalizeIntegerOption(readRequiredValue(args, ++index, "--exp-backoff-restart-delay"), "--exp-backoff-restart-delay", 1);
1634
+ break;
1635
+ case "--exp-backoff-restart-max-delay":
1636
+ parsed.expBackoffRestartMaxDelay = normalizeIntegerOption(readRequiredValue(args, ++index, "--exp-backoff-restart-max-delay"), "--exp-backoff-restart-max-delay", 1);
1637
+ break;
1638
+ case "--restart-window":
1639
+ parsed.restartWindow = normalizeIntegerOption(readRequiredValue(args, ++index, "--restart-window"), "--restart-window", 1);
1640
+ break;
1641
+ case "--wait-ready":
1642
+ parsed.waitReady = true;
1643
+ break;
1644
+ case "--listen-timeout":
1645
+ parsed.listenTimeout = normalizeIntegerOption(readRequiredValue(args, ++index, "--listen-timeout"), "--listen-timeout", 1);
1646
+ break;
1259
1647
  case "--min-uptime":
1260
1648
  parsed.minUptime = normalizeIntegerOption(readRequiredValue(args, ++index, "--min-uptime"), "--min-uptime");
1261
1649
  break;
@@ -1295,6 +1683,9 @@ function parsePmStartArgs(args) {
1295
1683
  case "--restart-delay":
1296
1684
  parsed.restartDelay = normalizeIntegerOption(readRequiredValue(args, ++index, "--restart-delay"), "--restart-delay");
1297
1685
  break;
1686
+ case "--kill-timeout":
1687
+ parsed.killTimeout = normalizeIntegerOption(readRequiredValue(args, ++index, "--kill-timeout"), "--kill-timeout", 1);
1688
+ break;
1298
1689
  case "--max-restarts":
1299
1690
  parsed.maxRestarts = normalizeIntegerOption(readRequiredValue(args, ++index, "--max-restarts"), "--max-restarts");
1300
1691
  break;
@@ -1336,8 +1727,15 @@ function printPmHelp() {
1336
1727
  " elit pm start my-app",
1337
1728
  " elit pm start",
1338
1729
  " elit pm list",
1730
+ " elit pm list --json",
1731
+ " elit pm show <name>",
1732
+ " elit pm describe <name> --json",
1339
1733
  " elit pm stop <name|all>",
1340
1734
  " elit pm restart <name|all>",
1735
+ " elit pm reload <name|all>",
1736
+ " elit pm scale <name> <count>",
1737
+ " elit pm reset <name|all>",
1738
+ " elit pm send-signal <signal> <name|all>",
1341
1739
  " elit pm delete <name|all>",
1342
1740
  " elit pm save",
1343
1741
  " elit pm resurrect",
@@ -1353,6 +1751,12 @@ function printPmHelp() {
1353
1751
  " --google-drive-shared-drive Forward supportsAllDrives=true for shared drives",
1354
1752
  " --runtime, -r <name> Runtime override: node, bun, deno",
1355
1753
  " --name, -n <name> Process name used by list/stop/restart",
1754
+ " --instances <count> Start multiple managed instances for the same app name",
1755
+ " --proxy-port <port> Own a public HTTP port through a PM proxy for single-instance reload handoff",
1756
+ " --proxy-strategy <mode> Public socket mode: proxy or inherit (default: proxy)",
1757
+ " --proxy-host <host> Public host bound by the PM proxy (default: 0.0.0.0)",
1758
+ " --proxy-target-host <host> Internal host used for upstream child traffic (default: 127.0.0.1)",
1759
+ " --proxy-env <name> Env var populated with the child private port (default: PORT)",
1356
1760
  " --cwd <dir> Working directory for the managed process",
1357
1761
  " --env KEY=VALUE Add or override an environment variable",
1358
1762
  " --password <value> Password for locked WAPK archives",
@@ -1364,6 +1768,14 @@ function printPmHelp() {
1364
1768
  " --no-archive-watch Disable archive-source read sync for WAPK apps",
1365
1769
  " --archive-sync-interval <ms> Forward WAPK archive read-sync interval (>= 50ms)",
1366
1770
  " --restart-policy <mode> Restart policy: always, on-failure, never",
1771
+ " --max-memory <bytes|size> Trigger a memory action after a limit like 268435456 or 256M",
1772
+ " --memory-action <mode> Action on max-memory: restart, stop",
1773
+ " --cron-restart <expr> Restart on a cron schedule or @every <duration>",
1774
+ " --exp-backoff-restart-delay <ms> Exponential unstable-restart backoff base delay",
1775
+ " --exp-backoff-restart-max-delay <ms> Maximum unstable-restart backoff delay (default 15000)",
1776
+ " --restart-window <ms> Rolling time window used when counting restart attempts",
1777
+ " --wait-ready Keep the process in starting state until its health check succeeds",
1778
+ " --listen-timeout <ms> Startup wait limit when --wait-ready is enabled (default 3000)",
1367
1779
  " --min-uptime <ms> Reset crash counter after this healthy uptime",
1368
1780
  " --watch Restart when watched files change",
1369
1781
  " --watch-path <path> Add a file or directory to watch",
@@ -1376,6 +1788,7 @@ function printPmHelp() {
1376
1788
  " --health-max-failures <n> Consecutive failures before restart (default 3)",
1377
1789
  " --no-autorestart Disable automatic restart",
1378
1790
  " --restart-delay <ms> Delay between restart attempts (default 1000)",
1791
+ " --kill-timeout <ms> Grace period before force-killing a stop or restart (default 5000)",
1379
1792
  " --max-restarts <count> Maximum restart attempts (default 10)",
1380
1793
  "",
1381
1794
  "Config:",
@@ -1399,6 +1812,17 @@ function printPmHelp() {
1399
1812
  " - elit pm save persists running apps to pm.dumpFile or ./.elit/pm/dump.json.",
1400
1813
  " - elit pm resurrect restarts whatever was last saved by elit pm save.",
1401
1814
  " - elit pm start <name> starts a configured app by name.",
1815
+ " - elit pm reload <name|all> performs a rolling stop/start and waits for each instance to return online before continuing.",
1816
+ " - elit pm scale <name> <count> changes the number of managed instances for a running app group.",
1817
+ " - elit pm reset <name|all> clears restart count, last exit code, and saved error metadata.",
1818
+ " - elit pm send-signal <signal> <name|all> forwards a POSIX-style signal such as SIGUSR2 or TERM.",
1819
+ " - 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.",
1820
+ " - killTimeout controls how long PM waits before force-killing an app that ignores stop or restart requests.",
1821
+ " - waitReady uses the configured health check as a startup gate and errors if it never becomes healthy within listenTimeout.",
1822
+ " - elit pm list shows live cpu, memory, and uptime columns when the child process is running.",
1823
+ " - elit pm list --json and elit pm jlist print machine-readable process state.",
1824
+ " - elit pm list --json, elit pm show --json, and elit pm describe --json include liveMetrics when available.",
1825
+ " - elit pm show <name> and elit pm describe <name> expose the full saved process record.",
1402
1826
  " - TypeScript files with runtime node require tsx, otherwise use --runtime bun.",
1403
1827
  " - WAPK processes are executed through elit wapk run inside the manager.",
1404
1828
  " - WAPK PM apps can use local archives, gdrive://<fileId>, or pm.apps[].wapkRun.googleDrive.",
@@ -1409,7 +1833,7 @@ function printPmHelp() {
1409
1833
  // src/cli/pm/process.ts
1410
1834
  import { spawn, spawnSync } from "child_process";
1411
1835
  import { existsSync as existsSync3 } from "fs";
1412
- import { basename, join as join4 } from "path";
1836
+ import { basename, dirname as dirname2, join as join4, resolve as resolve4 } from "path";
1413
1837
  function readCurrentCliInvocation() {
1414
1838
  const cliEntry = process.argv[1];
1415
1839
  if (!cliEntry) {
@@ -1454,16 +1878,94 @@ function resolveTsxExecutable(cwd) {
1454
1878
  const globalCommand = process.platform === "win32" ? "tsx.cmd" : "tsx";
1455
1879
  return commandExists(globalCommand) ? globalCommand : void 0;
1456
1880
  }
1881
+ function resolvePmNodeSharedListenerBootstrapPath() {
1882
+ const cliEntry = process.argv[1];
1883
+ const candidates = [
1884
+ join4(__dirname, "node-shared-listener-bootstrap.cjs"),
1885
+ join4(__dirname, "..", "..", "..", "dist", "pm-node-shared-listener-bootstrap.cjs")
1886
+ ];
1887
+ if (cliEntry) {
1888
+ const resolvedCliEntry = resolve4(cliEntry);
1889
+ const baseDir = dirname2(resolvedCliEntry);
1890
+ candidates.push(
1891
+ join4(baseDir, "cli", "pm", "node-shared-listener-bootstrap.cjs"),
1892
+ join4(baseDir, "pm-node-shared-listener-bootstrap.cjs")
1893
+ );
1894
+ }
1895
+ return candidates.find((candidate) => existsSync3(candidate));
1896
+ }
1897
+ function appendNodeOption(existing, option) {
1898
+ return existing && existing.trim().length > 0 ? `${existing.trim()} ${option}` : option;
1899
+ }
1900
+ function supportsPmInheritedListener(record, runtime2) {
1901
+ return (record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "inherit" && record.type === "file" && runtime2 === "node" && !isTypescriptFile(record.file);
1902
+ }
1457
1903
  function inferRuntimeFromFile(filePath) {
1458
1904
  if (isTypescriptFile(filePath) && commandExists("bun")) {
1459
1905
  return "bun";
1460
1906
  }
1461
1907
  return "node";
1462
1908
  }
1909
+ function sampleWindowsPmProcessMetrics(pid) {
1910
+ const script = [
1911
+ '$ErrorActionPreference = "Stop"',
1912
+ `$sample = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfProc_Process -Filter "IDProcess = ${pid}" | Select-Object -First 1`,
1913
+ "if (-not $sample) { exit 2 }",
1914
+ "$cpu = [double]$sample.PercentProcessorTime",
1915
+ `$memory = if ($sample.PSObject.Properties.Match('WorkingSetPrivate').Count -gt 0) { [int64]$sample.WorkingSetPrivate } else { [int64](Get-Process -Id ${pid} -ErrorAction Stop).WorkingSet64 }`,
1916
+ 'Write-Output ($cpu.ToString([System.Globalization.CultureInfo]::InvariantCulture) + "," + $memory)'
1917
+ ].join("; ");
1918
+ const result = spawnSync(
1919
+ "powershell.exe",
1920
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
1921
+ {
1922
+ encoding: "utf8",
1923
+ stdio: ["ignore", "pipe", "ignore"],
1924
+ windowsHide: true
1925
+ }
1926
+ );
1927
+ if (result.error || result.status !== 0) {
1928
+ return {};
1929
+ }
1930
+ const [cpuText, memoryText] = result.stdout.trim().split(",");
1931
+ const cpuPercent = Number.parseFloat((cpuText ?? "").replace(",", "."));
1932
+ const memoryRssBytes = Number.parseInt(memoryText ?? "", 10);
1933
+ return {
1934
+ cpuPercent: Number.isFinite(cpuPercent) ? cpuPercent : void 0,
1935
+ memoryRssBytes: Number.isFinite(memoryRssBytes) ? memoryRssBytes : void 0
1936
+ };
1937
+ }
1938
+ function samplePosixPmProcessMetrics(pid) {
1939
+ const result = spawnSync(
1940
+ "ps",
1941
+ ["-p", String(pid), "-o", "%cpu=", "-o", "rss="],
1942
+ {
1943
+ encoding: "utf8",
1944
+ stdio: ["ignore", "pipe", "ignore"],
1945
+ windowsHide: true
1946
+ }
1947
+ );
1948
+ if (result.error || result.status !== 0) {
1949
+ return {};
1950
+ }
1951
+ const [cpuText, memoryText] = result.stdout.trim().split(/\s+/, 2);
1952
+ const cpuPercent = Number.parseFloat((cpuText ?? "").replace(",", "."));
1953
+ const rssKilobytes = Number.parseInt(memoryText ?? "", 10);
1954
+ return {
1955
+ cpuPercent: Number.isFinite(cpuPercent) ? cpuPercent : void 0,
1956
+ memoryRssBytes: Number.isFinite(rssKilobytes) ? rssKilobytes * 1024 : void 0
1957
+ };
1958
+ }
1959
+ function samplePmProcessMetrics(pid) {
1960
+ return process.platform === "win32" ? sampleWindowsPmProcessMetrics(pid) : samplePosixPmProcessMetrics(pid);
1961
+ }
1463
1962
  function isPmOnlineWapkRecord(record) {
1464
1963
  return record.type === "wapk" && isPmWapkOnlineRunConfig(record.wapkRun);
1465
1964
  }
1466
1965
  function buildPmCommand(record) {
1966
+ if ((record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "inherit" && record.type !== "file") {
1967
+ throw new Error('pm proxy strategy "inherit" currently supports only Node .js/.mjs/.cjs file targets.');
1968
+ }
1467
1969
  if (record.type === "script") {
1468
1970
  return {
1469
1971
  command: record.script,
@@ -1535,9 +2037,23 @@ function buildPmCommand(record) {
1535
2037
  }
1536
2038
  const executable = preferCurrentExecutable("node");
1537
2039
  ensureCommandAvailable(executable, "Node.js runtime");
2040
+ const wantsInheritedListener = (record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "inherit";
2041
+ if (wantsInheritedListener && !supportsPmInheritedListener(record, runtime2)) {
2042
+ throw new Error('pm proxy strategy "inherit" currently supports only Node .js/.mjs/.cjs file targets.');
2043
+ }
2044
+ const bootstrapPath = wantsInheritedListener ? resolvePmNodeSharedListenerBootstrapPath() : void 0;
2045
+ if (wantsInheritedListener && !bootstrapPath) {
2046
+ throw new Error("Unable to resolve the PM shared-listener bootstrap module.");
2047
+ }
1538
2048
  return {
1539
2049
  command: executable,
1540
2050
  args: [record.file],
2051
+ env: bootstrapPath ? {
2052
+ NODE_OPTIONS: appendNodeOption(process.env.NODE_OPTIONS, `--require=${bootstrapPath}`),
2053
+ ELIT_PM_LISTEN_MODE: "ipc",
2054
+ ELIT_PM_PUBLIC_PORT: String(record.proxy?.port ?? record.env.PORT ?? "")
2055
+ } : void 0,
2056
+ ipc: Boolean(bootstrapPath),
1541
2057
  runtime: runtime2,
1542
2058
  preview: `${basename(executable)} ${quoteCommandSegment(record.file)}`
1543
2059
  };
@@ -1549,20 +2065,33 @@ function createRecordFromDefinition(definition, paths, existing) {
1549
2065
  return {
1550
2066
  id,
1551
2067
  name: definition.name,
2068
+ baseName: definition.baseName,
2069
+ instanceIndex: definition.instanceIndex,
2070
+ instances: definition.instances,
1552
2071
  type: definition.type,
1553
2072
  source: definition.source,
1554
2073
  cwd: definition.cwd,
1555
2074
  runtime: definition.runtime,
1556
2075
  env: definition.env,
2076
+ proxy: definition.proxy,
1557
2077
  script: definition.script,
1558
2078
  file: definition.file,
1559
2079
  wapk: definition.wapk,
1560
2080
  wapkRun: definition.wapkRun,
1561
2081
  autorestart: definition.autorestart,
1562
2082
  restartDelay: definition.restartDelay,
2083
+ killTimeout: definition.killTimeout,
1563
2084
  maxRestarts: definition.maxRestarts,
1564
2085
  password: definition.password,
1565
2086
  restartPolicy: definition.restartPolicy,
2087
+ maxMemoryBytes: definition.maxMemoryBytes,
2088
+ memoryAction: definition.memoryAction,
2089
+ cronRestart: definition.cronRestart,
2090
+ expBackoffRestartDelay: definition.expBackoffRestartDelay,
2091
+ expBackoffRestartMaxDelay: definition.expBackoffRestartMaxDelay,
2092
+ restartWindow: definition.restartWindow,
2093
+ waitReady: definition.waitReady,
2094
+ listenTimeout: definition.listenTimeout,
1566
2095
  minUptime: definition.minUptime,
1567
2096
  watch: definition.watch,
1568
2097
  watchPaths: definition.watchPaths,
@@ -1578,16 +2107,20 @@ function createRecordFromDefinition(definition, paths, existing) {
1578
2107
  stoppedAt: void 0,
1579
2108
  runnerPid: void 0,
1580
2109
  childPid: void 0,
2110
+ proxyTargetPort: void 0,
1581
2111
  restartCount: existing?.restartCount ?? 0,
2112
+ reloadRequestedAt: void 0,
2113
+ lastRestartAt: existing?.lastRestartAt,
1582
2114
  lastExitCode: existing?.lastExitCode,
1583
2115
  error: void 0,
2116
+ proxyReadyAt: void 0,
1584
2117
  logFiles: existing?.logFiles ?? {
1585
2118
  out: join4(paths.logsDir, `${id}.out.log`),
1586
2119
  err: join4(paths.logsDir, `${id}.err.log`)
1587
2120
  }
1588
2121
  };
1589
2122
  }
1590
- function terminateProcessTree(pid) {
2123
+ function terminateProcessTree(pid, options) {
1591
2124
  if (process.platform === "win32") {
1592
2125
  const result = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
1593
2126
  stdio: "ignore",
@@ -1599,7 +2132,17 @@ function terminateProcessTree(pid) {
1599
2132
  return;
1600
2133
  }
1601
2134
  try {
1602
- process.kill(pid, "SIGTERM");
2135
+ process.kill(pid, options?.force ? "SIGKILL" : "SIGTERM");
2136
+ } catch (error) {
2137
+ const code = error.code;
2138
+ if (code !== "ESRCH") {
2139
+ throw error;
2140
+ }
2141
+ }
2142
+ }
2143
+ function sendPmSignal(pid, signal) {
2144
+ try {
2145
+ process.kill(pid, signal);
1603
2146
  } catch (error) {
1604
2147
  const code = error.code;
1605
2148
  if (code !== "ESRCH") {
@@ -1651,9 +2194,10 @@ async function startManagedProcess(definition, paths) {
1651
2194
 
1652
2195
  // src/cli/pm/runner.ts
1653
2196
  import { spawn as spawn2 } from "child_process";
2197
+ import { createServer as createHttpServer } from "http";
1654
2198
  import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1655
2199
  import { EOL } from "os";
1656
- import { dirname as dirname3, join as join6, resolve as resolve5 } from "path";
2200
+ import { dirname as dirname4, join as join6, resolve as resolve6 } from "path";
1657
2201
 
1658
2202
  // src/server/chokidar/watch.ts
1659
2203
  init_runtime();
@@ -1884,13 +2428,13 @@ var win32 = createPathOps(true);
1884
2428
  function join5(...paths) {
1885
2429
  return joinPaths(paths, isWindows);
1886
2430
  }
1887
- function resolve4(...paths) {
2431
+ function resolve5(...paths) {
1888
2432
  return resolvePaths(paths, isWindows);
1889
2433
  }
1890
2434
  function relative(from, to) {
1891
2435
  return relativePath(from, to, isWindows);
1892
2436
  }
1893
- function dirname2(path) {
2437
+ function dirname3(path) {
1894
2438
  return getDirname(path, isWindows);
1895
2439
  }
1896
2440
 
@@ -1928,14 +2472,14 @@ function getBaseDirectory(pattern) {
1928
2472
  }
1929
2473
  if (normalizedPattern && existsSync4(normalizedPattern)) {
1930
2474
  try {
1931
- return statSync2(normalizedPattern).isDirectory() ? normalizedPattern : normalizePath2(dirname2(normalizedPattern)) || ".";
2475
+ return statSync2(normalizedPattern).isDirectory() ? normalizedPattern : normalizePath2(dirname3(normalizedPattern)) || ".";
1932
2476
  } catch {
1933
- return normalizePath2(dirname2(normalizedPattern)) || ".";
2477
+ return normalizePath2(dirname3(normalizedPattern)) || ".";
1934
2478
  }
1935
2479
  }
1936
2480
  const lastSegment = parts[parts.length - 1] || "";
1937
2481
  if (lastSegment.includes(".") && !lastSegment.startsWith(".")) {
1938
- return normalizePath2(dirname2(normalizedPattern)) || ".";
2482
+ return normalizePath2(dirname3(normalizedPattern)) || ".";
1939
2483
  }
1940
2484
  return normalizedPattern || ".";
1941
2485
  }
@@ -2130,84 +2674,877 @@ function watch(paths, options) {
2130
2674
  return watcher;
2131
2675
  }
2132
2676
 
2133
- // src/cli/pm/runner.ts
2134
- function writePmLog(stream, message) {
2135
- stream.write(`[elit pm] ${(/* @__PURE__ */ new Date()).toISOString()} ${message}${EOL}`);
2136
- }
2137
- function waitForExit(code, signal) {
2138
- if (typeof code === "number") {
2139
- return code;
2677
+ // src/cli/pm/proxy.ts
2678
+ import { createServer, request as httpRequest } from "http";
2679
+ import { request as httpsRequest } from "https";
2680
+ import { createServer as createNetServer } from "net";
2681
+ import { lookup as dnsLookup } from "dns/promises";
2682
+ var BLOCKED_IPV4_PREFIXES = [
2683
+ "0.",
2684
+ "10.",
2685
+ "100.64.",
2686
+ "100.65.",
2687
+ "100.66.",
2688
+ "100.67.",
2689
+ "100.68.",
2690
+ "100.69.",
2691
+ "100.70.",
2692
+ "100.71.",
2693
+ "100.72.",
2694
+ "100.73.",
2695
+ "100.74.",
2696
+ "100.75.",
2697
+ "100.76.",
2698
+ "100.77.",
2699
+ "100.78.",
2700
+ "100.79.",
2701
+ "100.80.",
2702
+ "100.81.",
2703
+ "100.82.",
2704
+ "100.83.",
2705
+ "100.84.",
2706
+ "100.85.",
2707
+ "100.86.",
2708
+ "100.87.",
2709
+ "100.88.",
2710
+ "100.89.",
2711
+ "100.90.",
2712
+ "100.91.",
2713
+ "100.92.",
2714
+ "100.93.",
2715
+ "100.94.",
2716
+ "100.95.",
2717
+ "100.96.",
2718
+ "100.97.",
2719
+ "100.98.",
2720
+ "100.99.",
2721
+ "100.100.",
2722
+ "100.101.",
2723
+ "100.102.",
2724
+ "100.103.",
2725
+ "100.104.",
2726
+ "100.105.",
2727
+ "100.106.",
2728
+ "100.107.",
2729
+ "100.108.",
2730
+ "100.109.",
2731
+ "100.110.",
2732
+ "100.111.",
2733
+ "100.112.",
2734
+ "100.113.",
2735
+ "100.114.",
2736
+ "100.115.",
2737
+ "100.116.",
2738
+ "100.117.",
2739
+ "100.118.",
2740
+ "100.119.",
2741
+ "100.120.",
2742
+ "100.121.",
2743
+ "100.122.",
2744
+ "100.123.",
2745
+ "100.124.",
2746
+ "100.125.",
2747
+ "100.126.",
2748
+ "100.127.",
2749
+ "127.",
2750
+ "169.254.",
2751
+ "172.16.",
2752
+ "172.17.",
2753
+ "172.18.",
2754
+ "172.19.",
2755
+ "172.20.",
2756
+ "172.21.",
2757
+ "172.22.",
2758
+ "172.23.",
2759
+ "172.24.",
2760
+ "172.25.",
2761
+ "172.26.",
2762
+ "172.27.",
2763
+ "172.28.",
2764
+ "172.29.",
2765
+ "172.30.",
2766
+ "172.31.",
2767
+ "192.0.2.",
2768
+ "192.88.99.",
2769
+ "192.168.",
2770
+ "198.18.",
2771
+ "198.19.",
2772
+ "198.51.100.",
2773
+ "203.0.113.",
2774
+ "224.",
2775
+ "225.",
2776
+ "226.",
2777
+ "227.",
2778
+ "228.",
2779
+ "229.",
2780
+ "230.",
2781
+ "231.",
2782
+ "232.",
2783
+ "233.",
2784
+ "234.",
2785
+ "235.",
2786
+ "236.",
2787
+ "237.",
2788
+ "238.",
2789
+ "239.",
2790
+ "240.",
2791
+ "241.",
2792
+ "242.",
2793
+ "243.",
2794
+ "244.",
2795
+ "245.",
2796
+ "246.",
2797
+ "247.",
2798
+ "248.",
2799
+ "249.",
2800
+ "250.",
2801
+ "251.",
2802
+ "252.",
2803
+ "253.",
2804
+ "254.",
2805
+ "255."
2806
+ ];
2807
+ var ALLOWED_PROXY_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
2808
+ function isBlockedIpv4(hostname) {
2809
+ const octets = hostname.split(".");
2810
+ if (octets.length !== 4) return false;
2811
+ const joined = hostname;
2812
+ return BLOCKED_IPV4_PREFIXES.some((prefix) => joined.startsWith(prefix));
2813
+ }
2814
+ function isBlockedIpv6(hostname) {
2815
+ const lower = hostname.toLowerCase();
2816
+ if (lower === "::1" || lower === "::" || lower === "0:0:0:0:0:0:0:1" || lower === "0:0:0:0:0:0:0:0") {
2817
+ return true;
2140
2818
  }
2141
- if (signal === "SIGINT" || signal === "SIGTERM") {
2142
- return 0;
2819
+ const ffffMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
2820
+ if (ffffMatch) {
2821
+ return isBlockedIpv4(ffffMatch[1]);
2143
2822
  }
2144
- return 1;
2823
+ const compatMatch = lower.match(/^::(\d+\.\d+\.\d+\.\d+)$/);
2824
+ if (compatMatch) {
2825
+ return isBlockedIpv4(compatMatch[1]);
2826
+ }
2827
+ return false;
2145
2828
  }
2146
- async function delay(milliseconds) {
2147
- await new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
2829
+ function isSafeHostname(hostname) {
2830
+ if (!hostname) return false;
2831
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) return !isBlockedIpv4(hostname);
2832
+ if (/^\[.*\]$/.test(hostname)) return !isBlockedIpv6(hostname.slice(1, -1));
2833
+ if (hostname.includes(":")) return !isBlockedIpv6(hostname);
2834
+ return true;
2148
2835
  }
2149
- async function waitForProcessTermination(pid, timeoutMs) {
2150
- if (!pid || !isProcessAlive(pid)) {
2151
- return true;
2836
+ async function safeResolveHostname(hostname) {
2837
+ try {
2838
+ const result = await dnsLookup(hostname);
2839
+ const ip = result.address;
2840
+ if (isBlockedIpv4(ip) || isBlockedIpv6(ip)) {
2841
+ throw new Error(`PM proxy target resolved to a blocked address: ${ip}`);
2842
+ }
2843
+ return ip;
2844
+ } catch (error) {
2845
+ if (error instanceof Error && error.message.includes("blocked address")) {
2846
+ throw error;
2847
+ }
2848
+ throw new Error(`PM proxy failed to resolve target hostname "${hostname}": ${error instanceof Error ? error.message : String(error)}`);
2152
2849
  }
2153
- const deadline = Date.now() + timeoutMs;
2154
- while (Date.now() < deadline) {
2155
- if (!isProcessAlive(pid)) {
2156
- return true;
2850
+ }
2851
+ async function validateProxyTargetUrl(target) {
2852
+ if (!ALLOWED_PROXY_PROTOCOLS.has(target.protocol)) {
2853
+ throw new Error(`PM proxy target protocol "${target.protocol}" is not allowed. Only http: and https: are permitted.`);
2854
+ }
2855
+ const hostname = target.hostname;
2856
+ if (!isSafeHostname(hostname)) {
2857
+ throw new Error(`PM proxy target "${hostname}" resolves to a blocked address and is not allowed.`);
2858
+ }
2859
+ if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) && !/^\[.*\]$/.test(hostname)) {
2860
+ await safeResolveHostname(hostname);
2861
+ }
2862
+ }
2863
+ function sanitizeProxyRequestPath(requestUrl) {
2864
+ if (!requestUrl || requestUrl === "/") return "/";
2865
+ try {
2866
+ const normalizedInput = requestUrl.replace(/\\/g, "/");
2867
+ const parsed = new URL(normalizedInput, "http://placeholder");
2868
+ if (parsed.username || parsed.password || parsed.hostname !== "placeholder" || parsed.port) {
2869
+ return "/";
2157
2870
  }
2158
- await delay(DEFAULT_PM_STOP_POLL_MS);
2871
+ const pathname = parsed.pathname || "/";
2872
+ let decodedPathname = pathname;
2873
+ try {
2874
+ decodedPathname = decodeURIComponent(pathname);
2875
+ } catch {
2876
+ return "/";
2877
+ }
2878
+ const lowerPath = pathname.toLowerCase();
2879
+ if (lowerPath.includes("%2f") || lowerPath.includes("%5c") || lowerPath.includes("%40") || lowerPath.includes("%00")) {
2880
+ return "/";
2881
+ }
2882
+ const segments = decodedPathname.split("/");
2883
+ if (segments.some((segment) => segment === "." || segment === "..")) {
2884
+ return "/";
2885
+ }
2886
+ const sanitized = pathname + parsed.search;
2887
+ return sanitized.startsWith("/") ? sanitized || "/" : `/${sanitized}`;
2888
+ } catch {
2889
+ return "/";
2159
2890
  }
2160
- return !isProcessAlive(pid);
2161
2891
  }
2162
- async function waitForManagedChildExit(child) {
2163
- return await new Promise((resolvePromise) => {
2164
- let resolved = false;
2165
- child.once("error", (error) => {
2166
- if (resolved) {
2167
- return;
2168
- }
2169
- resolved = true;
2170
- resolvePromise({ code: 1, signal: null, error: error instanceof Error ? error.message : String(error) });
2171
- });
2172
- child.once("close", (code, signal) => {
2173
- if (resolved) {
2892
+ function resolvePmProxyHost(proxy) {
2893
+ return proxy.host?.trim() || "0.0.0.0";
2894
+ }
2895
+ function resolvePmProxyTargetHost(proxy) {
2896
+ return proxy.targetHost?.trim() || "127.0.0.1";
2897
+ }
2898
+ function resolvePmProxyEnvVar(proxy) {
2899
+ return proxy.envVar?.trim() || "PORT";
2900
+ }
2901
+ function buildPmProxyTargetUrl(proxy, targetPort) {
2902
+ return `http://${resolvePmProxyTargetHost(proxy)}:${targetPort}`;
2903
+ }
2904
+ function rewritePmProxyHealthCheckUrl(url, targetHost, targetPort) {
2905
+ const targetUrl = new URL(url);
2906
+ targetUrl.hostname = targetHost;
2907
+ targetUrl.port = String(targetPort);
2908
+ return targetUrl.toString();
2909
+ }
2910
+ async function allocatePmProxyTargetPort(host = "127.0.0.1") {
2911
+ const server = createNetServer();
2912
+ return await new Promise((resolve7, reject) => {
2913
+ server.once("error", reject);
2914
+ server.listen(0, host, () => {
2915
+ const address = server.address();
2916
+ if (!address || typeof address === "string") {
2917
+ server.close(() => reject(new Error("Failed to allocate an internal PM proxy port.")));
2174
2918
  return;
2175
2919
  }
2176
- resolved = true;
2177
- resolvePromise({ code, signal });
2920
+ const port = address.port;
2921
+ server.close((error) => {
2922
+ if (error) {
2923
+ reject(error);
2924
+ return;
2925
+ }
2926
+ resolve7(port);
2927
+ });
2178
2928
  });
2179
2929
  });
2180
2930
  }
2181
- async function createPmWatchController(record, onChange, onError) {
2182
- if (!record.watch || record.watchPaths.length === 0) {
2183
- return {
2184
- async close() {
2931
+ function buildPmProxyHeaders(headersInput, host) {
2932
+ const headers = {};
2933
+ for (const [key, value] of Object.entries(headersInput)) {
2934
+ if (value !== void 0) {
2935
+ headers[key] = value;
2936
+ }
2937
+ }
2938
+ headers.host = host;
2939
+ return headers;
2940
+ }
2941
+ function writeRawHttpResponse(socket, statusCode, statusMessage, headers) {
2942
+ const lines = [`HTTP/1.1 ${statusCode} ${statusMessage}`];
2943
+ for (const [key, value] of Object.entries(headers)) {
2944
+ if (value === void 0) {
2945
+ continue;
2946
+ }
2947
+ if (Array.isArray(value)) {
2948
+ for (const item of value) {
2949
+ lines.push(`${key}: ${item}`);
2185
2950
  }
2186
- };
2951
+ continue;
2952
+ }
2953
+ lines.push(`${key}: ${value}`);
2187
2954
  }
2188
- const watcher = watch(record.watchPaths);
2189
- let debounceTimer = null;
2190
- const scheduleRestart = (filePath) => {
2191
- const normalizedPath = filePath.replace(/\\/g, "/");
2192
- if (isIgnoredWatchPath(normalizedPath, record.watchIgnore)) {
2955
+ socket.write(`${lines.join("\r\n")}\r
2956
+ \r
2957
+ `);
2958
+ }
2959
+ async function createPmProxyController(proxy) {
2960
+ let targets = [];
2961
+ let nextTargetIndex = 0;
2962
+ const setResolvedTargets = (nextTargets) => {
2963
+ const unchanged = nextTargets.length === targets.length && nextTargets.every((target, index) => targets[index]?.href === target.href);
2964
+ targets = nextTargets;
2965
+ if (targets.length === 0) {
2966
+ nextTargetIndex = 0;
2193
2967
  return;
2194
2968
  }
2195
- if (debounceTimer) {
2196
- clearTimeout(debounceTimer);
2969
+ if (!unchanged) {
2970
+ nextTargetIndex = nextTargetIndex % targets.length;
2197
2971
  }
2198
- debounceTimer = setTimeout(() => {
2199
- debounceTimer = null;
2200
- onChange(normalizedPath);
2201
- }, record.watchDebounce);
2202
- debounceTimer.unref?.();
2203
2972
  };
2204
- watcher.on("add", scheduleRestart);
2205
- watcher.on("change", scheduleRestart);
2206
- watcher.on("unlink", scheduleRestart);
2207
- watcher.on("error", (error) => onError(error instanceof Error ? error.message : String(error)));
2208
- return {
2209
- async close() {
2210
- if (debounceTimer) {
2973
+ const pickTarget = () => {
2974
+ if (targets.length === 0) {
2975
+ return null;
2976
+ }
2977
+ const target = targets[nextTargetIndex % targets.length];
2978
+ nextTargetIndex = (nextTargetIndex + 1) % targets.length;
2979
+ return target;
2980
+ };
2981
+ const validateTarget = async (target) => {
2982
+ await validateProxyTargetUrl(target);
2983
+ };
2984
+ const server = createServer((req, res) => {
2985
+ const target = pickTarget();
2986
+ if (!target) {
2987
+ res.statusCode = 503;
2988
+ res.end("PM proxy target is not ready.");
2989
+ return;
2990
+ }
2991
+ const sanitizedPath = sanitizeProxyRequestPath(req.url || "/");
2992
+ const requestLib = target.protocol === "https:" ? httpsRequest : httpRequest;
2993
+ const headers = buildPmProxyHeaders(req.headers, target.host);
2994
+ if (!ALLOWED_PROXY_PROTOCOLS.has(target.protocol)) {
2995
+ res.statusCode = 400;
2996
+ res.end("PM proxy rejected unsafe target protocol.");
2997
+ return;
2998
+ }
2999
+ validateTarget(target).then(() => {
3000
+ const proxyReq = requestLib({
3001
+ protocol: target.protocol,
3002
+ hostname: target.hostname,
3003
+ port: target.port || void 0,
3004
+ path: sanitizedPath,
3005
+ method: req.method,
3006
+ headers
3007
+ }, (proxyRes) => {
3008
+ const outgoingHeaders = {};
3009
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
3010
+ if (value !== void 0) {
3011
+ outgoingHeaders[key] = value;
3012
+ }
3013
+ }
3014
+ res.writeHead(proxyRes.statusCode || 200, outgoingHeaders);
3015
+ proxyRes.pipe(res);
3016
+ });
3017
+ proxyReq.on("error", (error) => {
3018
+ if (!res.headersSent) {
3019
+ res.statusCode = 502;
3020
+ }
3021
+ res.end(`PM proxy error: ${error.message}`);
3022
+ });
3023
+ req.pipe(proxyReq);
3024
+ }).catch((error) => {
3025
+ if (!res.headersSent) {
3026
+ res.statusCode = 403;
3027
+ }
3028
+ res.end(`PM proxy blocked target: ${error instanceof Error ? error.message : String(error)}`);
3029
+ });
3030
+ });
3031
+ server.on("upgrade", (req, socket, head) => {
3032
+ const target = pickTarget();
3033
+ if (!target) {
3034
+ writeRawHttpResponse(socket, 503, "Service Unavailable", {
3035
+ connection: "close",
3036
+ "content-length": 0
3037
+ });
3038
+ socket.destroy();
3039
+ return;
3040
+ }
3041
+ const sanitizedPath = sanitizeProxyRequestPath(req.url || "/");
3042
+ const requestLib = target.protocol === "https:" ? httpsRequest : httpRequest;
3043
+ const targetUrl = new URL(sanitizedPath, target);
3044
+ if (targetUrl.hostname !== target.hostname || targetUrl.port !== target.port || !ALLOWED_PROXY_PROTOCOLS.has(targetUrl.protocol)) {
3045
+ writeRawHttpResponse(socket, 400, "Bad Request", {
3046
+ connection: "close",
3047
+ "content-length": 0
3048
+ });
3049
+ socket.destroy();
3050
+ return;
3051
+ }
3052
+ validateTarget(target).then(() => {
3053
+ const proxyReq = requestLib(targetUrl, {
3054
+ method: req.method,
3055
+ headers: buildPmProxyHeaders(req.headers, target.host)
3056
+ });
3057
+ proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
3058
+ writeRawHttpResponse(socket, proxyRes.statusCode || 101, proxyRes.statusMessage || "Switching Protocols", proxyRes.headers);
3059
+ if (head.length > 0) {
3060
+ proxySocket.write(head);
3061
+ }
3062
+ if (proxyHead.length > 0) {
3063
+ socket.write(proxyHead);
3064
+ }
3065
+ socket.on("error", () => proxySocket.destroy());
3066
+ proxySocket.on("error", () => socket.destroy());
3067
+ proxySocket.pipe(socket);
3068
+ socket.pipe(proxySocket);
3069
+ });
3070
+ proxyReq.on("response", (proxyRes) => {
3071
+ writeRawHttpResponse(socket, proxyRes.statusCode || 502, proxyRes.statusMessage || "Bad Gateway", proxyRes.headers);
3072
+ proxyRes.pipe(socket);
3073
+ });
3074
+ proxyReq.on("error", (error) => {
3075
+ writeRawHttpResponse(socket, 502, "Bad Gateway", {
3076
+ connection: "close",
3077
+ "content-type": "text/plain; charset=utf-8",
3078
+ "content-length": Buffer.byteLength(`PM proxy error: ${error.message}`)
3079
+ });
3080
+ socket.end(`PM proxy error: ${error.message}`);
3081
+ });
3082
+ proxyReq.end();
3083
+ }).catch((error) => {
3084
+ writeRawHttpResponse(socket, 403, "Forbidden", {
3085
+ connection: "close",
3086
+ "content-type": "text/plain; charset=utf-8",
3087
+ "content-length": Buffer.byteLength(`PM proxy blocked target: ${error instanceof Error ? error.message : String(error)}`)
3088
+ });
3089
+ socket.end(`PM proxy blocked target: ${error instanceof Error ? error.message : String(error)}`);
3090
+ });
3091
+ });
3092
+ await new Promise((resolve7, reject) => {
3093
+ server.once("error", reject);
3094
+ server.listen(proxy.port, resolvePmProxyHost(proxy), () => resolve7());
3095
+ });
3096
+ return {
3097
+ setTarget(targetUrl) {
3098
+ if (targetUrl) {
3099
+ const parsed = new URL(targetUrl);
3100
+ validateProxyTargetUrl(parsed).then(() => {
3101
+ setResolvedTargets([parsed]);
3102
+ }).catch((error) => {
3103
+ console.error(`[PM proxy] Blocked setTarget: ${error instanceof Error ? error.message : String(error)}`);
3104
+ setResolvedTargets([]);
3105
+ });
3106
+ } else {
3107
+ setResolvedTargets([]);
3108
+ }
3109
+ },
3110
+ setTargets(targetUrls) {
3111
+ Promise.all(targetUrls.map((url) => validateProxyTargetUrl(new URL(url)))).then(() => {
3112
+ setResolvedTargets(targetUrls.map((targetUrl) => new URL(targetUrl)));
3113
+ }).catch((error) => {
3114
+ console.error(`[PM proxy] Blocked setTargets: ${error instanceof Error ? error.message : String(error)}`);
3115
+ setResolvedTargets([]);
3116
+ });
3117
+ },
3118
+ close() {
3119
+ return new Promise((resolve7, reject) => {
3120
+ server.close((error) => {
3121
+ if (error) {
3122
+ reject(error);
3123
+ return;
3124
+ }
3125
+ resolve7();
3126
+ });
3127
+ });
3128
+ }
3129
+ };
3130
+ }
3131
+
3132
+ // src/cli/pm/runner.ts
3133
+ function writePmLog(stream, message) {
3134
+ if (stream.writableEnded || stream.destroyed) {
3135
+ return;
3136
+ }
3137
+ try {
3138
+ stream.write(`[elit pm] ${(/* @__PURE__ */ new Date()).toISOString()} ${message}${EOL}`);
3139
+ } catch (error) {
3140
+ if (error.code !== "ERR_STREAM_WRITE_AFTER_END") {
3141
+ throw error;
3142
+ }
3143
+ }
3144
+ }
3145
+ function waitForExit(code, signal) {
3146
+ if (typeof code === "number") {
3147
+ return code;
3148
+ }
3149
+ if (signal === "SIGINT" || signal === "SIGTERM") {
3150
+ return 0;
3151
+ }
3152
+ return 1;
3153
+ }
3154
+ async function delay(milliseconds) {
3155
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
3156
+ }
3157
+ function resolveRunnerPathsFromRecordFile(filePath) {
3158
+ const appsDir = dirname4(filePath);
3159
+ const dataDir = dirname4(appsDir);
3160
+ return {
3161
+ dataDir,
3162
+ appsDir,
3163
+ logsDir: join6(dataDir, "logs"),
3164
+ dumpFile: join6(dataDir, DEFAULT_PM_DUMP_FILE)
3165
+ };
3166
+ }
3167
+ function usesPmProxyController(record) {
3168
+ return Boolean(record.proxy) && (record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "proxy";
3169
+ }
3170
+ function usesPmInheritedListener(record) {
3171
+ return Boolean(record.proxy) && (record.proxy?.strategy ?? DEFAULT_PM_PROXY_STRATEGY) === "inherit";
3172
+ }
3173
+ function isPmProxyOwner(record) {
3174
+ return usesPmProxyController(record) && record.instanceIndex === 1;
3175
+ }
3176
+ function resolvePmProxyTargetUrls(paths, baseName) {
3177
+ 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));
3178
+ }
3179
+ async function createPmInheritedListener(proxy) {
3180
+ const server = createHttpServer();
3181
+ await new Promise((resolvePromise, reject) => {
3182
+ server.once("error", reject);
3183
+ server.listen(proxy.port, resolvePmProxyHost(proxy), () => resolvePromise());
3184
+ });
3185
+ return server;
3186
+ }
3187
+ function createPmChildIpcState(child, sharedListener) {
3188
+ let bootstrapReady = false;
3189
+ let listenerReady = false;
3190
+ let sharedHandleSent = false;
3191
+ const sendSharedHandle = () => {
3192
+ if (!sharedListener || !bootstrapReady || sharedHandleSent || !child.connected) {
3193
+ return;
3194
+ }
3195
+ sharedHandleSent = true;
3196
+ child.send?.({ type: "elit:pm:listen-handle" }, sharedListener);
3197
+ };
3198
+ const onMessage = (message) => {
3199
+ if (!message || typeof message !== "object") {
3200
+ return;
3201
+ }
3202
+ if (message.type === "elit:pm:bootstrap-ready") {
3203
+ bootstrapReady = true;
3204
+ sendSharedHandle();
3205
+ return;
3206
+ }
3207
+ if (message.type === "elit:pm:listener-ready") {
3208
+ listenerReady = true;
3209
+ }
3210
+ };
3211
+ child.on("message", onMessage);
3212
+ return {
3213
+ get bootstrapReady() {
3214
+ return bootstrapReady;
3215
+ },
3216
+ get listenerReady() {
3217
+ return listenerReady;
3218
+ },
3219
+ stop() {
3220
+ child.off("message", onMessage);
3221
+ }
3222
+ };
3223
+ }
3224
+ function buildPmChildEnv(record, command, targetPort) {
3225
+ return {
3226
+ ...process.env,
3227
+ ...record.env,
3228
+ ...command.env,
3229
+ ...usesPmProxyController(record) && targetPort ? {
3230
+ [resolvePmProxyEnvVar(record.proxy)]: String(targetPort),
3231
+ ELIT_PM_PUBLIC_PORT: String(record.proxy.port)
3232
+ } : {},
3233
+ ELIT_PM_NAME: record.name,
3234
+ ELIT_PM_ID: record.id
3235
+ };
3236
+ }
3237
+ function buildPmChildStdio(command, onlineStdinShutdownEnabled) {
3238
+ return [
3239
+ onlineStdinShutdownEnabled ? "pipe" : "ignore",
3240
+ "pipe",
3241
+ "pipe",
3242
+ ...command.ipc ? ["ipc"] : []
3243
+ ];
3244
+ }
3245
+ function createPmReadinessMonitor(record, onReady, onFailure, options) {
3246
+ if (options?.ipcController) {
3247
+ let stopped2 = false;
3248
+ let timer2 = null;
3249
+ let timeoutTimer2 = null;
3250
+ const clearTimers2 = () => {
3251
+ if (timer2) {
3252
+ clearInterval(timer2);
3253
+ timer2 = null;
3254
+ }
3255
+ if (timeoutTimer2) {
3256
+ clearTimeout(timeoutTimer2);
3257
+ timeoutTimer2 = null;
3258
+ }
3259
+ };
3260
+ const host = record.proxy?.host ?? "0.0.0.0";
3261
+ const port = record.proxy?.port ?? 0;
3262
+ timer2 = setInterval(() => {
3263
+ if (stopped2 || !options.ipcController?.listenerReady) {
3264
+ return;
3265
+ }
3266
+ stopped2 = true;
3267
+ clearTimers2();
3268
+ onReady(`shared listener ready on ${host}:${port}`);
3269
+ }, 25);
3270
+ timer2.unref?.();
3271
+ timeoutTimer2 = setTimeout(() => {
3272
+ if (stopped2) {
3273
+ return;
3274
+ }
3275
+ stopped2 = true;
3276
+ clearTimers2();
3277
+ onFailure(`listen timeout reached after ${record.listenTimeout}ms while waiting for shared listener ${host}:${port}`);
3278
+ }, record.listenTimeout);
3279
+ timeoutTimer2.unref?.();
3280
+ return {
3281
+ stop() {
3282
+ stopped2 = true;
3283
+ clearTimers2();
3284
+ }
3285
+ };
3286
+ }
3287
+ if (!record.waitReady || !record.healthCheck) {
3288
+ return {
3289
+ stop() {
3290
+ }
3291
+ };
3292
+ }
3293
+ const healthCheck = record.healthCheck;
3294
+ const pollInterval = Math.max(50, Math.min(healthCheck.interval, 250));
3295
+ let stopped = false;
3296
+ let timer = null;
3297
+ let timeoutTimer = null;
3298
+ let inFlight = false;
3299
+ const clearTimers = () => {
3300
+ if (timer) {
3301
+ clearInterval(timer);
3302
+ timer = null;
3303
+ }
3304
+ if (timeoutTimer) {
3305
+ clearTimeout(timeoutTimer);
3306
+ timeoutTimer = null;
3307
+ }
3308
+ };
3309
+ const runHealthCheck = async () => {
3310
+ if (stopped || inFlight) {
3311
+ return;
3312
+ }
3313
+ inFlight = true;
3314
+ const controller = new AbortController();
3315
+ const timeoutId = setTimeout(() => controller.abort(), healthCheck.timeout);
3316
+ timeoutId.unref?.();
3317
+ try {
3318
+ const response = await fetch(healthCheck.url, {
3319
+ method: "GET",
3320
+ signal: controller.signal
3321
+ });
3322
+ if (stopped) {
3323
+ return;
3324
+ }
3325
+ if (!response.ok) {
3326
+ throw new Error(`health check returned ${response.status}`);
3327
+ }
3328
+ stopped = true;
3329
+ clearTimers();
3330
+ onReady(`readiness check passed: ${healthCheck.url}`);
3331
+ } catch {
3332
+ } finally {
3333
+ clearTimeout(timeoutId);
3334
+ inFlight = false;
3335
+ }
3336
+ };
3337
+ timeoutTimer = setTimeout(() => {
3338
+ if (stopped) {
3339
+ return;
3340
+ }
3341
+ stopped = true;
3342
+ clearTimers();
3343
+ onFailure(`listen timeout reached after ${record.listenTimeout}ms while waiting for ${healthCheck.url}`);
3344
+ }, record.listenTimeout);
3345
+ timeoutTimer.unref?.();
3346
+ void runHealthCheck();
3347
+ timer = setInterval(() => {
3348
+ void runHealthCheck();
3349
+ }, pollInterval);
3350
+ timer.unref?.();
3351
+ return {
3352
+ stop() {
3353
+ stopped = true;
3354
+ clearTimers();
3355
+ }
3356
+ };
3357
+ }
3358
+ function resolvePmStopTimeout(record) {
3359
+ if (isPmOnlineWapkRecord(record)) {
3360
+ return Math.max(record.killTimeout, PM_WAPK_ONLINE_SHUTDOWN_TIMEOUT_MS);
3361
+ }
3362
+ return record.killTimeout;
3363
+ }
3364
+ function supportsPmProxyReload(record) {
3365
+ return Boolean(record.proxy) && record.instances === 1;
3366
+ }
3367
+ function buildPmMonitorRecord(record, targetPort) {
3368
+ if (!record.proxy || !targetPort) {
3369
+ return record;
3370
+ }
3371
+ const targetHost = resolvePmProxyTargetHost(record.proxy);
3372
+ return {
3373
+ ...record,
3374
+ proxyTargetPort: targetPort,
3375
+ healthCheck: record.healthCheck ? {
3376
+ ...record.healthCheck,
3377
+ url: rewritePmProxyHealthCheckUrl(record.healthCheck.url, targetHost, targetPort)
3378
+ } : void 0
3379
+ };
3380
+ }
3381
+ async function waitForProcessTermination(pid, timeoutMs) {
3382
+ if (!pid || !isProcessAlive(pid)) {
3383
+ return true;
3384
+ }
3385
+ const deadline = Date.now() + timeoutMs;
3386
+ while (Date.now() < deadline) {
3387
+ if (!isProcessAlive(pid)) {
3388
+ return true;
3389
+ }
3390
+ await delay(DEFAULT_PM_STOP_POLL_MS);
3391
+ }
3392
+ return !isProcessAlive(pid);
3393
+ }
3394
+ async function waitForManagedChildExit(child) {
3395
+ return await new Promise((resolvePromise) => {
3396
+ let resolved = false;
3397
+ child.once("error", (error) => {
3398
+ if (resolved) {
3399
+ return;
3400
+ }
3401
+ resolved = true;
3402
+ resolvePromise({ code: 1, signal: null, error: error instanceof Error ? error.message : String(error) });
3403
+ });
3404
+ child.once("close", (code, signal) => {
3405
+ if (resolved) {
3406
+ return;
3407
+ }
3408
+ resolved = true;
3409
+ resolvePromise({ code, signal });
3410
+ });
3411
+ });
3412
+ }
3413
+ async function createPmChildControllers(record, child, stdoutLog, stderrLog, requestPlannedRestart, onReady, options) {
3414
+ let healthMonitor = {
3415
+ stop() {
3416
+ }
3417
+ };
3418
+ let memoryMonitor = {
3419
+ stop() {
3420
+ }
3421
+ };
3422
+ let scheduleMonitor = {
3423
+ stop() {
3424
+ }
3425
+ };
3426
+ const startHealthMonitor = () => {
3427
+ healthMonitor = createPmHealthMonitor(
3428
+ record,
3429
+ (message) => requestPlannedRestart("health", message),
3430
+ (message) => writePmLog(stdoutLog, message)
3431
+ );
3432
+ };
3433
+ const needsReadySignal = Boolean(options?.ipcController) || record.waitReady;
3434
+ const readinessMonitor = options?.ready || !needsReadySignal ? { stop() {
3435
+ } } : createPmReadinessMonitor(
3436
+ record,
3437
+ (message) => {
3438
+ onReady(message);
3439
+ startHealthMonitor();
3440
+ },
3441
+ (message) => requestPlannedRestart("startup", message),
3442
+ { ipcController: options?.ipcController }
3443
+ );
3444
+ const watchController = await createPmWatchController(
3445
+ record,
3446
+ (changedPath) => requestPlannedRestart("watch", changedPath),
3447
+ (message) => writePmLog(stderrLog, `watch error: ${message}`)
3448
+ );
3449
+ memoryMonitor = createPmMemoryMonitor(
3450
+ record,
3451
+ child.pid,
3452
+ (kind, message) => requestPlannedRestart(kind, message)
3453
+ );
3454
+ scheduleMonitor = createPmScheduleMonitor(
3455
+ record,
3456
+ (message) => requestPlannedRestart("cron", message),
3457
+ (message) => writePmLog(stdoutLog, message)
3458
+ );
3459
+ if (options?.ready || !needsReadySignal) {
3460
+ startHealthMonitor();
3461
+ }
3462
+ return {
3463
+ async stop() {
3464
+ await watchController.close();
3465
+ readinessMonitor.stop();
3466
+ healthMonitor.stop();
3467
+ memoryMonitor.stop();
3468
+ scheduleMonitor.stop();
3469
+ }
3470
+ };
3471
+ }
3472
+ async function waitForPmChildReady(record, child, ipcController) {
3473
+ if (!ipcController && (!record.waitReady || !record.healthCheck)) {
3474
+ return { ready: true };
3475
+ }
3476
+ let readyMessage;
3477
+ let failureMessage;
3478
+ let exitResult;
3479
+ const readinessMonitor = createPmReadinessMonitor(
3480
+ record,
3481
+ (message) => {
3482
+ readyMessage = message;
3483
+ },
3484
+ (message) => {
3485
+ failureMessage = message;
3486
+ },
3487
+ { ipcController }
3488
+ );
3489
+ void waitForManagedChildExit(child).then((result) => {
3490
+ exitResult = result;
3491
+ });
3492
+ while (!readyMessage && !failureMessage && !exitResult) {
3493
+ await delay(25);
3494
+ }
3495
+ readinessMonitor.stop();
3496
+ if (readyMessage) {
3497
+ return { ready: true, message: readyMessage };
3498
+ }
3499
+ return {
3500
+ ready: false,
3501
+ message: failureMessage,
3502
+ exitResult
3503
+ };
3504
+ }
3505
+ async function stopProxyManagedChild(child, record, stderrLog) {
3506
+ if (!child.pid || !isProcessAlive(child.pid)) {
3507
+ return;
3508
+ }
3509
+ terminateProcessTree(child.pid);
3510
+ const stopTimeout = resolvePmStopTimeout(record);
3511
+ const stopped = await waitForProcessTermination(child.pid, stopTimeout);
3512
+ if (!stopped && child.pid && isProcessAlive(child.pid)) {
3513
+ writePmLog(stderrLog, `proxy handoff shutdown timed out after ${stopTimeout}ms; forcing process termination`);
3514
+ terminateProcessTree(child.pid, { force: true });
3515
+ await waitForProcessTermination(child.pid, DEFAULT_PM_STOP_POLL_MS);
3516
+ }
3517
+ }
3518
+ async function createPmWatchController(record, onChange, onError) {
3519
+ if (!record.watch || record.watchPaths.length === 0) {
3520
+ return {
3521
+ async close() {
3522
+ }
3523
+ };
3524
+ }
3525
+ const watcher = watch(record.watchPaths);
3526
+ let debounceTimer = null;
3527
+ const scheduleRestart = (filePath) => {
3528
+ const normalizedPath = filePath.replace(/\\/g, "/");
3529
+ if (isIgnoredWatchPath(normalizedPath, record.watchIgnore)) {
3530
+ return;
3531
+ }
3532
+ if (debounceTimer) {
3533
+ clearTimeout(debounceTimer);
3534
+ }
3535
+ debounceTimer = setTimeout(() => {
3536
+ debounceTimer = null;
3537
+ onChange(normalizedPath);
3538
+ }, record.watchDebounce);
3539
+ debounceTimer.unref?.();
3540
+ };
3541
+ watcher.on("add", scheduleRestart);
3542
+ watcher.on("change", scheduleRestart);
3543
+ watcher.on("unlink", scheduleRestart);
3544
+ watcher.on("error", (error) => onError(error instanceof Error ? error.message : String(error)));
3545
+ return {
3546
+ async close() {
3547
+ if (debounceTimer) {
2211
3548
  clearTimeout(debounceTimer);
2212
3549
  debounceTimer = null;
2213
3550
  }
@@ -2241,11 +3578,17 @@ function createPmHealthMonitor(record, onFailure, onLog) {
2241
3578
  method: "GET",
2242
3579
  signal: controller.signal
2243
3580
  });
3581
+ if (stopped) {
3582
+ return;
3583
+ }
2244
3584
  if (!response.ok) {
2245
3585
  throw new Error(`health check returned ${response.status}`);
2246
3586
  }
2247
3587
  failureCount = 0;
2248
3588
  } catch (error) {
3589
+ if (stopped) {
3590
+ return;
3591
+ }
2249
3592
  failureCount += 1;
2250
3593
  const message = error instanceof Error ? error.message : String(error);
2251
3594
  onLog(`health check failed (${failureCount}/${healthCheck.maxFailures}): ${message}`);
@@ -2280,6 +3623,90 @@ function createPmHealthMonitor(record, onFailure, onLog) {
2280
3623
  }
2281
3624
  };
2282
3625
  }
3626
+ function createPmMemoryMonitor(record, pid, onFailure) {
3627
+ if (!record.maxMemoryBytes || !pid) {
3628
+ return {
3629
+ stop() {
3630
+ }
3631
+ };
3632
+ }
3633
+ const memoryLimit = record.maxMemoryBytes;
3634
+ let stopped = false;
3635
+ const timer = setInterval(() => {
3636
+ if (stopped) {
3637
+ return;
3638
+ }
3639
+ const memoryRssBytes = samplePmProcessMetrics(pid).memoryRssBytes;
3640
+ if (memoryRssBytes === void 0 || memoryRssBytes <= memoryLimit) {
3641
+ return;
3642
+ }
3643
+ stopped = true;
3644
+ onFailure(record.memoryAction === "stop" ? "memory-stop" : "memory", `memory usage ${memoryRssBytes} exceeded limit ${memoryLimit}`);
3645
+ }, DEFAULT_PM_MEMORY_CHECK_INTERVAL);
3646
+ timer.unref?.();
3647
+ return {
3648
+ stop() {
3649
+ stopped = true;
3650
+ clearInterval(timer);
3651
+ }
3652
+ };
3653
+ }
3654
+ function createPmScheduleMonitor(record, onTrigger, onLog) {
3655
+ if (!record.cronRestart) {
3656
+ return {
3657
+ stop() {
3658
+ }
3659
+ };
3660
+ }
3661
+ const schedule = parsePmRestartSchedule(record.cronRestart, "pm cronRestart");
3662
+ let stopped = false;
3663
+ let timer = null;
3664
+ const armTimer = (from = /* @__PURE__ */ new Date()) => {
3665
+ const nextOccurrence = resolveNextPmScheduleOccurrence(schedule, from);
3666
+ if (!nextOccurrence) {
3667
+ onLog(`schedule has no next occurrence: ${record.cronRestart}`);
3668
+ return;
3669
+ }
3670
+ const delayMs = Math.max(0, nextOccurrence.getTime() - Date.now());
3671
+ timer = setTimeout(() => {
3672
+ timer = null;
3673
+ if (stopped) {
3674
+ return;
3675
+ }
3676
+ onTrigger(`restart schedule matched: ${record.cronRestart}`);
3677
+ }, delayMs);
3678
+ timer.unref?.();
3679
+ };
3680
+ armTimer();
3681
+ return {
3682
+ stop() {
3683
+ stopped = true;
3684
+ if (timer) {
3685
+ clearTimeout(timer);
3686
+ timer = null;
3687
+ }
3688
+ }
3689
+ };
3690
+ }
3691
+ function resolvePmRestartDelay(record, restartCount, shouldApplyBackoff) {
3692
+ if (!shouldApplyBackoff || !record.expBackoffRestartDelay) {
3693
+ return record.restartDelay;
3694
+ }
3695
+ const exponent = Math.max(0, restartCount - 1);
3696
+ return Math.min(record.expBackoffRestartDelay * 2 ** exponent, record.expBackoffRestartMaxDelay ?? DEFAULT_PM_EXP_BACKOFF_MAX_DELAY);
3697
+ }
3698
+ function resolvePmRestartCountBase(record, wasStable, restartKind) {
3699
+ if (wasStable) {
3700
+ return 0;
3701
+ }
3702
+ if (record.restartWindow && record.lastRestartAt) {
3703
+ const lastRestartTime = Date.parse(record.lastRestartAt);
3704
+ if (!Number.isNaN(lastRestartTime) && Date.now() - lastRestartTime > record.restartWindow) {
3705
+ return 0;
3706
+ }
3707
+ }
3708
+ return restartKind === "watch" ? record.restartCount ?? 0 : record.restartCount ?? 0;
3709
+ }
2283
3710
  function readPlannedRestartRequest(state) {
2284
3711
  return state.request;
2285
3712
  }
@@ -2289,10 +3716,20 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2289
3716
  let activeChildStopTimer = null;
2290
3717
  let stopRequested = false;
2291
3718
  const restartState = { request: null };
2292
- mkdirSync3(dirname3(initialRecord.logFiles.out), { recursive: true });
2293
- mkdirSync3(dirname3(initialRecord.logFiles.err), { recursive: true });
3719
+ mkdirSync3(dirname4(initialRecord.logFiles.out), { recursive: true });
3720
+ mkdirSync3(dirname4(initialRecord.logFiles.err), { recursive: true });
2294
3721
  const stdoutLog = createWriteStream(initialRecord.logFiles.out, { flags: "a" });
2295
3722
  const stderrLog = createWriteStream(initialRecord.logFiles.err, { flags: "a" });
3723
+ let proxyController = null;
3724
+ let proxyTargetSyncTimer = null;
3725
+ let inheritedListener = null;
3726
+ const runnerPaths = resolveRunnerPathsFromRecordFile(filePath);
3727
+ const syncOwnedProxyTargets = (baseName) => {
3728
+ if (!proxyController) {
3729
+ return;
3730
+ }
3731
+ proxyController.setTargets(resolvePmProxyTargetUrls(runnerPaths, baseName));
3732
+ };
2296
3733
  const persist = (mutator) => {
2297
3734
  const current = readLatestPmRecord(filePath, record);
2298
3735
  record = mutator(current);
@@ -2305,26 +3742,30 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2305
3742
  activeChildStopTimer = null;
2306
3743
  }
2307
3744
  };
3745
+ const scheduleForcedActiveChildStop = (timeoutMs, reason) => {
3746
+ if (!activeChild?.pid || process.platform === "win32") {
3747
+ return;
3748
+ }
3749
+ clearActiveChildStopTimer();
3750
+ activeChildStopTimer = setTimeout(() => {
3751
+ if (activeChild?.pid && isProcessAlive(activeChild.pid)) {
3752
+ writePmLog(stderrLog, `${reason} after ${timeoutMs}ms; forcing process termination`);
3753
+ terminateProcessTree(activeChild.pid, { force: true });
3754
+ }
3755
+ }, timeoutMs);
3756
+ activeChildStopTimer.unref?.();
3757
+ };
2308
3758
  const stopActiveChild = () => {
2309
3759
  if (!activeChild?.pid || !isProcessAlive(activeChild.pid)) {
2310
3760
  return;
2311
3761
  }
2312
3762
  const current = readLatestPmRecord(filePath, record);
3763
+ const stopTimeout = resolvePmStopTimeout(current);
2313
3764
  if (isPmOnlineWapkRecord(current) && activeChild.stdin && !activeChild.stdin.destroyed && activeChild.stdin.writable) {
2314
3765
  try {
2315
3766
  activeChild.stdin.end(`${PM_WAPK_ONLINE_SHUTDOWN_COMMAND}
2316
3767
  `);
2317
- clearActiveChildStopTimer();
2318
- activeChildStopTimer = setTimeout(() => {
2319
- if (activeChild?.pid && isProcessAlive(activeChild.pid)) {
2320
- writePmLog(
2321
- stderrLog,
2322
- `graceful WAPK online shutdown timed out after ${PM_WAPK_ONLINE_SHUTDOWN_TIMEOUT_MS}ms; forcing process termination`
2323
- );
2324
- terminateProcessTree(activeChild.pid);
2325
- }
2326
- }, PM_WAPK_ONLINE_SHUTDOWN_TIMEOUT_MS);
2327
- activeChildStopTimer.unref?.();
3768
+ scheduleForcedActiveChildStop(stopTimeout, "graceful WAPK online shutdown timed out");
2328
3769
  return;
2329
3770
  } catch (error) {
2330
3771
  const message = error instanceof Error ? error.message : String(error);
@@ -2332,6 +3773,7 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2332
3773
  }
2333
3774
  }
2334
3775
  terminateProcessTree(activeChild.pid);
3776
+ scheduleForcedActiveChildStop(stopTimeout, "graceful shutdown timed out");
2335
3777
  };
2336
3778
  const requestManagedStop = (reason) => {
2337
3779
  if (stopRequested) {
@@ -2369,6 +3811,17 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2369
3811
  let command;
2370
3812
  try {
2371
3813
  command = buildPmCommand(latest);
3814
+ if (latest.proxy && isPmProxyOwner(latest) && !proxyController) {
3815
+ proxyController = await createPmProxyController(latest.proxy);
3816
+ syncOwnedProxyTargets(latest.baseName);
3817
+ if (!proxyTargetSyncTimer) {
3818
+ proxyTargetSyncTimer = setInterval(() => syncOwnedProxyTargets(latest.baseName), 50);
3819
+ proxyTargetSyncTimer.unref?.();
3820
+ }
3821
+ }
3822
+ if (latest.proxy && usesPmInheritedListener(latest) && !inheritedListener) {
3823
+ inheritedListener = await createPmInheritedListener(latest.proxy);
3824
+ }
2372
3825
  } catch (error) {
2373
3826
  const message = error instanceof Error ? error.message : String(error);
2374
3827
  writePmLog(stderrLog, message);
@@ -2378,25 +3831,24 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2378
3831
  error: message,
2379
3832
  runnerPid: void 0,
2380
3833
  childPid: void 0,
3834
+ proxyTargetPort: void 0,
3835
+ proxyReadyAt: void 0,
2381
3836
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2382
3837
  }));
2383
3838
  return;
2384
3839
  }
2385
3840
  const onlineStdinShutdownEnabled = isPmOnlineWapkRecord(latest);
2386
- const child = spawn2(command.command, command.args, {
3841
+ const initialTargetPort = usesPmProxyController(latest) ? await allocatePmProxyTargetPort(resolvePmProxyTargetHost(latest.proxy)) : void 0;
3842
+ const monitorRecord = buildPmMonitorRecord(latest, initialTargetPort);
3843
+ let child = spawn2(command.command, command.args, {
2387
3844
  cwd: latest.cwd,
2388
- env: {
2389
- ...process.env,
2390
- ...latest.env,
2391
- ...command.env,
2392
- ELIT_PM_NAME: latest.name,
2393
- ELIT_PM_ID: latest.id
2394
- },
2395
- stdio: [onlineStdinShutdownEnabled ? "pipe" : "ignore", "pipe", "pipe"],
3845
+ env: buildPmChildEnv(latest, command, initialTargetPort),
3846
+ stdio: buildPmChildStdio(command, onlineStdinShutdownEnabled),
2396
3847
  windowsHide: true,
2397
3848
  shell: command.shell
2398
3849
  });
2399
- const childStartedAt = Date.now();
3850
+ let childStartedAt = Date.now();
3851
+ let childIpcState = command.ipc ? createPmChildIpcState(child, inheritedListener) : void 0;
2400
3852
  activeChild = child;
2401
3853
  if (child.stdout) {
2402
3854
  child.stdout.pipe(stdoutLog, { end: false });
@@ -2404,26 +3856,38 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2404
3856
  if (child.stderr) {
2405
3857
  child.stderr.pipe(stderrLog, { end: false });
2406
3858
  }
3859
+ let childWaitState = { settled: false, result: void 0 };
3860
+ void waitForManagedChildExit(child).then((result) => {
3861
+ childWaitState.result = result;
3862
+ childWaitState.settled = true;
3863
+ });
2407
3864
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3865
+ const waitingForReady = latest.waitReady || Boolean(childIpcState);
2408
3866
  persist((current2) => ({
2409
3867
  ...current2,
2410
- status: "online",
3868
+ status: waitingForReady ? "starting" : "online",
2411
3869
  commandPreview: command.preview,
2412
3870
  runtime: command.runtime ?? current2.runtime,
2413
3871
  runnerPid: process.pid,
2414
3872
  childPid: child.pid,
3873
+ proxyTargetPort: initialTargetPort,
3874
+ proxyReadyAt: !waitingForReady && usesPmProxyController(latest) ? startedAt : void 0,
2415
3875
  startedAt,
2416
3876
  stoppedAt: void 0,
3877
+ reloadRequestedAt: void 0,
2417
3878
  error: void 0,
2418
3879
  updatedAt: startedAt
2419
3880
  }));
2420
3881
  writePmLog(stdoutLog, `started ${command.preview}${child.pid ? ` (pid ${child.pid})` : ""}`);
3882
+ if (isPmProxyOwner(latest) && !waitingForReady) {
3883
+ syncOwnedProxyTargets(latest.baseName);
3884
+ }
2421
3885
  const requestPlannedRestart = (kind, detail) => {
2422
3886
  if (stopRequested || restartState.request) {
2423
3887
  return;
2424
3888
  }
2425
3889
  restartState.request = { kind, detail };
2426
- writePmLog(kind === "health" ? stderrLog : stdoutLog, `${kind} restart requested: ${detail}`);
3890
+ writePmLog(kind === "watch" || kind === "cron" ? stdoutLog : stderrLog, `${kind} restart requested: ${detail}`);
2427
3891
  persist((current2) => ({
2428
3892
  ...current2,
2429
3893
  status: "restarting",
@@ -2431,27 +3895,124 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2431
3895
  }));
2432
3896
  stopActiveChild();
2433
3897
  };
2434
- const watchController = await createPmWatchController(
2435
- latest,
2436
- (changedPath) => requestPlannedRestart("watch", changedPath),
2437
- (message) => writePmLog(stderrLog, `watch error: ${message}`)
2438
- );
2439
- const healthMonitor = createPmHealthMonitor(
2440
- latest,
2441
- (message) => requestPlannedRestart("health", message),
2442
- (message) => writePmLog(stdoutLog, message)
3898
+ let controllers = await createPmChildControllers(
3899
+ monitorRecord,
3900
+ child,
3901
+ stdoutLog,
3902
+ stderrLog,
3903
+ requestPlannedRestart,
3904
+ (message) => {
3905
+ const readyAt = (/* @__PURE__ */ new Date()).toISOString();
3906
+ if (isPmProxyOwner(latest)) {
3907
+ syncOwnedProxyTargets(latest.baseName);
3908
+ }
3909
+ persist((current2) => ({
3910
+ ...current2,
3911
+ status: "online",
3912
+ proxyTargetPort: initialTargetPort,
3913
+ proxyReadyAt: readyAt,
3914
+ updatedAt: readyAt
3915
+ }));
3916
+ writePmLog(stdoutLog, message);
3917
+ },
3918
+ { ready: !waitingForReady, ipcController: childIpcState }
2443
3919
  );
2444
- const desiredStatePoller = setInterval(() => {
3920
+ let handledReloadAt = latest.reloadRequestedAt;
3921
+ while (!childWaitState.settled) {
2445
3922
  const latestRecord = readLatestPmRecord(filePath, record);
2446
- if (!stopRequested && latestRecord.desiredState === "stopped") {
3923
+ if (latestRecord.desiredState === "stopped" && !stopRequested) {
2447
3924
  requestManagedStop("stop requested by PM control state");
2448
3925
  }
2449
- }, DEFAULT_PM_STOP_POLL_MS);
2450
- desiredStatePoller.unref?.();
2451
- const exitResult = await waitForManagedChildExit(child);
2452
- clearInterval(desiredStatePoller);
2453
- await watchController.close();
2454
- healthMonitor.stop();
3926
+ const reloadRequestedAt = latestRecord.reloadRequestedAt;
3927
+ if (!stopRequested && supportsPmProxyReload(latestRecord) && reloadRequestedAt && reloadRequestedAt !== handledReloadAt && latestRecord.proxy) {
3928
+ handledReloadAt = reloadRequestedAt;
3929
+ const replacementTargetPort = usesPmProxyController(latestRecord) ? await allocatePmProxyTargetPort(resolvePmProxyTargetHost(latestRecord.proxy)) : void 0;
3930
+ const replacementMonitorRecord = buildPmMonitorRecord(latestRecord, replacementTargetPort);
3931
+ const replacementChild = spawn2(command.command, command.args, {
3932
+ cwd: latestRecord.cwd,
3933
+ env: buildPmChildEnv(latestRecord, command, replacementTargetPort),
3934
+ stdio: buildPmChildStdio(command, onlineStdinShutdownEnabled),
3935
+ windowsHide: true,
3936
+ shell: command.shell
3937
+ });
3938
+ const replacementIpcState = command.ipc ? createPmChildIpcState(replacementChild, inheritedListener) : void 0;
3939
+ if (replacementChild.stdout) {
3940
+ replacementChild.stdout.pipe(stdoutLog, { end: false });
3941
+ }
3942
+ if (replacementChild.stderr) {
3943
+ replacementChild.stderr.pipe(stderrLog, { end: false });
3944
+ }
3945
+ writePmLog(stdoutLog, `starting ${usesPmInheritedListener(latestRecord) ? "shared-listener" : "proxy handoff"} replacement${replacementChild.pid ? ` (pid ${replacementChild.pid})` : ""}`);
3946
+ const readyResult = await waitForPmChildReady(replacementMonitorRecord, replacementChild, replacementIpcState);
3947
+ if (!readyResult.ready) {
3948
+ writePmLog(stderrLog, readyResult.message ?? "replacement exited before becoming ready");
3949
+ replacementIpcState?.stop();
3950
+ await stopProxyManagedChild(replacementChild, latestRecord, stderrLog);
3951
+ persist((current2) => ({
3952
+ ...current2,
3953
+ status: "online",
3954
+ proxyReadyAt: current2.proxyReadyAt,
3955
+ reloadRequestedAt: void 0,
3956
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3957
+ }));
3958
+ continue;
3959
+ }
3960
+ const previousChild = child;
3961
+ const previousControllers = controllers;
3962
+ const previousIpcState = childIpcState;
3963
+ const handoffAt = (/* @__PURE__ */ new Date()).toISOString();
3964
+ if (usesPmProxyController(latestRecord) && replacementTargetPort) {
3965
+ proxyController?.setTarget(buildPmProxyTargetUrl(latestRecord.proxy, replacementTargetPort));
3966
+ }
3967
+ persist((current2) => ({
3968
+ ...current2,
3969
+ status: "online",
3970
+ childPid: replacementChild.pid,
3971
+ proxyTargetPort: replacementTargetPort,
3972
+ proxyReadyAt: handoffAt,
3973
+ startedAt: handoffAt,
3974
+ reloadRequestedAt: void 0,
3975
+ error: void 0,
3976
+ updatedAt: handoffAt
3977
+ }));
3978
+ if (readyResult.message) {
3979
+ writePmLog(stdoutLog, readyResult.message);
3980
+ }
3981
+ writePmLog(stdoutLog, `${usesPmInheritedListener(latestRecord) ? "shared listener" : "proxy handoff"} activated on ${latestRecord.proxy.host ?? "0.0.0.0"}:${latestRecord.proxy.port}`);
3982
+ child = replacementChild;
3983
+ childIpcState = replacementIpcState;
3984
+ childStartedAt = Date.now();
3985
+ activeChild = replacementChild;
3986
+ childWaitState = { settled: false, result: void 0 };
3987
+ void waitForManagedChildExit(replacementChild).then((result) => {
3988
+ childWaitState.result = result;
3989
+ childWaitState.settled = true;
3990
+ });
3991
+ controllers = await createPmChildControllers(
3992
+ replacementMonitorRecord,
3993
+ replacementChild,
3994
+ stdoutLog,
3995
+ stderrLog,
3996
+ requestPlannedRestart,
3997
+ () => {
3998
+ },
3999
+ { ready: true, ipcController: replacementIpcState }
4000
+ );
4001
+ await delay(250);
4002
+ await previousControllers.stop();
4003
+ previousIpcState?.stop();
4004
+ await stopProxyManagedChild(previousChild, latestRecord, stderrLog);
4005
+ clearActiveChildStopTimer();
4006
+ if (isPmProxyOwner(latestRecord)) {
4007
+ syncOwnedProxyTargets(latestRecord.baseName);
4008
+ }
4009
+ continue;
4010
+ }
4011
+ await delay(25);
4012
+ }
4013
+ const exitResult = childWaitState.result;
4014
+ await controllers.stop();
4015
+ childIpcState?.stop();
2455
4016
  clearActiveChildStopTimer();
2456
4017
  activeChild = null;
2457
4018
  const exitCode = waitForExit(exitResult.code, exitResult.signal);
@@ -2467,56 +4028,77 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2467
4028
  if (stopRequested || current.desiredState === "stopped") {
2468
4029
  break;
2469
4030
  }
2470
- const shouldRestartForExit = plannedRestart ? true : current.restartPolicy === "always" ? true : current.restartPolicy === "on-failure" ? exitCode !== 0 || Boolean(exitResult.error) : false;
4031
+ const shouldRestartForExit = plannedRestart?.kind === "memory-stop" ? false : plannedRestart ? true : current.restartPolicy === "always" ? true : current.restartPolicy === "on-failure" ? exitCode !== 0 || Boolean(exitResult.error) : false;
2471
4032
  if (!shouldRestartForExit) {
2472
4033
  persist((latestRecord) => ({
2473
4034
  ...latestRecord,
2474
- status: exitCode === 0 && !exitResult.error ? "exited" : "errored",
4035
+ status: plannedRestart?.kind === "memory-stop" ? "errored" : exitCode === 0 && !exitResult.error ? "exited" : "errored",
2475
4036
  childPid: void 0,
4037
+ proxyTargetPort: void 0,
4038
+ proxyReadyAt: void 0,
2476
4039
  runnerPid: void 0,
2477
4040
  lastExitCode: exitCode,
2478
- error: exitCode === 0 && !exitResult.error ? void 0 : exitResult.error ?? `Process exited with code ${exitCode}.`,
4041
+ reloadRequestedAt: void 0,
4042
+ error: plannedRestart?.kind === "memory-stop" ? plannedRestart.detail : exitCode === 0 && !exitResult.error ? void 0 : exitResult.error ?? `Process exited with code ${exitCode}.`,
2479
4043
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString(),
2480
4044
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2481
4045
  }));
4046
+ if (isPmProxyOwner(current)) {
4047
+ syncOwnedProxyTargets(current.baseName);
4048
+ }
2482
4049
  return;
2483
4050
  }
2484
4051
  const shouldCountRestart = plannedRestart?.kind !== "watch";
2485
- const baseRestartCount = wasStable ? 0 : current.restartCount ?? 0;
4052
+ const baseRestartCount = resolvePmRestartCountBase(current, wasStable, plannedRestart?.kind);
2486
4053
  const nextRestartCount = shouldCountRestart ? baseRestartCount + 1 : current.restartCount ?? 0;
2487
4054
  if (nextRestartCount > current.maxRestarts) {
2488
4055
  persist((latestRecord) => ({
2489
4056
  ...latestRecord,
2490
4057
  status: "errored",
2491
4058
  childPid: void 0,
4059
+ proxyTargetPort: void 0,
4060
+ proxyReadyAt: void 0,
2492
4061
  runnerPid: void 0,
2493
4062
  restartCount: nextRestartCount,
4063
+ lastRestartAt: (/* @__PURE__ */ new Date()).toISOString(),
2494
4064
  lastExitCode: exitCode,
4065
+ reloadRequestedAt: void 0,
2495
4066
  error: plannedRestart ? `Reached max restart attempts (${current.maxRestarts}) after ${plannedRestart.kind} restart requests.` : `Reached max restart attempts (${current.maxRestarts}).`,
2496
4067
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString(),
2497
4068
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2498
4069
  }));
2499
4070
  writePmLog(stderrLog, `max restart attempts reached (${current.maxRestarts})`);
4071
+ if (isPmProxyOwner(current)) {
4072
+ syncOwnedProxyTargets(current.baseName);
4073
+ }
2500
4074
  return;
2501
4075
  }
2502
4076
  persist((latestRecord) => ({
2503
4077
  ...latestRecord,
2504
4078
  status: "restarting",
2505
4079
  childPid: void 0,
4080
+ proxyTargetPort: void 0,
4081
+ proxyReadyAt: void 0,
2506
4082
  lastExitCode: exitCode,
2507
4083
  restartCount: nextRestartCount,
4084
+ lastRestartAt: (/* @__PURE__ */ new Date()).toISOString(),
4085
+ reloadRequestedAt: void 0,
2508
4086
  error: void 0,
2509
4087
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2510
4088
  }));
4089
+ if (isPmProxyOwner(current)) {
4090
+ syncOwnedProxyTargets(current.baseName);
4091
+ }
4092
+ const resolvedRestartDelay = resolvePmRestartDelay(current, nextRestartCount, shouldCountRestart && !wasStable && plannedRestart?.kind !== "watch");
2511
4093
  if (plannedRestart) {
2512
4094
  writePmLog(
2513
- plannedRestart.kind === "health" ? stderrLog : stdoutLog,
2514
- `restarting in ${current.restartDelay}ms after ${plannedRestart.kind}: ${plannedRestart.detail}`
4095
+ plannedRestart.kind === "health" || plannedRestart.kind === "memory" || plannedRestart.kind === "memory-stop" || plannedRestart.kind === "startup" ? stderrLog : stdoutLog,
4096
+ `restarting in ${resolvedRestartDelay}ms after ${plannedRestart.kind}: ${plannedRestart.detail}`
2515
4097
  );
2516
4098
  } else {
2517
- writePmLog(stdoutLog, `restarting in ${current.restartDelay}ms`);
4099
+ writePmLog(stdoutLog, `restarting in ${resolvedRestartDelay}ms`);
2518
4100
  }
2519
- await delay(current.restartDelay);
4101
+ await delay(resolvedRestartDelay);
2520
4102
  }
2521
4103
  } finally {
2522
4104
  stopRequested = true;
@@ -2529,11 +4111,26 @@ async function runManagedProcessLoop(filePath, initialRecord) {
2529
4111
  status: finalRecord.status === "errored" ? "errored" : finalRecord.status === "exited" ? "exited" : "stopped",
2530
4112
  runnerPid: void 0,
2531
4113
  childPid: void 0,
4114
+ proxyTargetPort: void 0,
4115
+ proxyReadyAt: void 0,
4116
+ reloadRequestedAt: void 0,
2532
4117
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString(),
2533
4118
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2534
4119
  });
2535
4120
  process.off("SIGINT", handleStopSignal);
2536
4121
  process.off("SIGTERM", handleStopSignal);
4122
+ if (proxyTargetSyncTimer) {
4123
+ clearInterval(proxyTargetSyncTimer);
4124
+ proxyTargetSyncTimer = null;
4125
+ }
4126
+ if (proxyController) {
4127
+ await proxyController.close().catch(() => void 0);
4128
+ proxyController = null;
4129
+ }
4130
+ if (inheritedListener) {
4131
+ await new Promise((resolvePromise) => inheritedListener?.close(() => resolvePromise()));
4132
+ inheritedListener = null;
4133
+ }
2537
4134
  await new Promise((resolvePromise) => stdoutLog.end(resolvePromise));
2538
4135
  await new Promise((resolvePromise) => stderrLog.end(resolvePromise));
2539
4136
  }
@@ -2558,7 +4155,7 @@ function parseRunnerArgs(args) {
2558
4155
  throw new Error("Usage: elit pm __run --data-dir <dir> --id <name>");
2559
4156
  }
2560
4157
  return {
2561
- dataDir: resolve5(dataDir),
4158
+ dataDir: resolve6(dataDir),
2562
4159
  id
2563
4160
  };
2564
4161
  }
@@ -2587,23 +4184,26 @@ async function stopPmMatches(matches) {
2587
4184
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString()
2588
4185
  };
2589
4186
  writePmRecord(match.filePath, updated);
2590
- const runnerStopped = await waitForProcessTermination(match.record.runnerPid, DEFAULT_PM_STOP_GRACE_PERIOD_MS);
4187
+ const stopTimeout = resolvePmStopTimeout(match.record);
4188
+ const runnerStopped = await waitForProcessTermination(match.record.runnerPid, stopTimeout);
2591
4189
  const childStopped = await waitForProcessTermination(
2592
4190
  match.record.childPid,
2593
- runnerStopped ? DEFAULT_PM_STOP_POLL_MS : DEFAULT_PM_STOP_GRACE_PERIOD_MS
4191
+ runnerStopped ? DEFAULT_PM_STOP_POLL_MS : stopTimeout
2594
4192
  );
2595
4193
  if (!runnerStopped && match.record.runnerPid && isProcessAlive(match.record.runnerPid)) {
2596
- terminateProcessTree(match.record.runnerPid);
4194
+ terminateProcessTree(match.record.runnerPid, { force: true });
2597
4195
  await waitForProcessTermination(match.record.runnerPid, DEFAULT_PM_STOP_POLL_MS);
2598
4196
  }
2599
4197
  if (!childStopped && match.record.childPid && isProcessAlive(match.record.childPid)) {
2600
- terminateProcessTree(match.record.childPid);
4198
+ terminateProcessTree(match.record.childPid, { force: true });
2601
4199
  await waitForProcessTermination(match.record.childPid, DEFAULT_PM_STOP_POLL_MS);
2602
4200
  }
2603
4201
  writePmRecord(match.filePath, {
2604
4202
  ...updated,
2605
4203
  runnerPid: void 0,
2606
4204
  childPid: void 0,
4205
+ proxyTargetPort: void 0,
4206
+ reloadRequestedAt: void 0,
2607
4207
  status: "stopped",
2608
4208
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2609
4209
  });
@@ -2672,13 +4272,13 @@ function readPackageJson(filePath) {
2672
4272
 
2673
4273
  // src/shares/workspace-package/roots.ts
2674
4274
  function findPackageDirectory(startDir, resolveMatch) {
2675
- let currentDir = resolve4(startDir);
4275
+ let currentDir = resolve5(startDir);
2676
4276
  while (true) {
2677
4277
  const match = resolveMatch(currentDir);
2678
4278
  if (match) {
2679
4279
  return match;
2680
4280
  }
2681
- const parentDir = dirname2(currentDir);
4281
+ const parentDir = dirname3(currentDir);
2682
4282
  if (parentDir === currentDir) {
2683
4283
  return void 0;
2684
4284
  }
@@ -2706,17 +4306,17 @@ init_fs();
2706
4306
  function getWorkspacePackageImportCandidates(packageRoot, specifier, options = {}) {
2707
4307
  const subpath = specifier === "elit" ? "index" : specifier.slice("elit/".length);
2708
4308
  const builtCandidates = options.preferredBuiltFormat === "cjs" ? [
2709
- resolve4(packageRoot, "dist", `${subpath}.cjs`),
2710
- resolve4(packageRoot, "dist", `${subpath}.js`),
2711
- resolve4(packageRoot, "dist", `${subpath}.mjs`)
4309
+ resolve5(packageRoot, "dist", `${subpath}.cjs`),
4310
+ resolve5(packageRoot, "dist", `${subpath}.js`),
4311
+ resolve5(packageRoot, "dist", `${subpath}.mjs`)
2712
4312
  ] : [
2713
- resolve4(packageRoot, "dist", `${subpath}.mjs`),
2714
- resolve4(packageRoot, "dist", `${subpath}.js`),
2715
- resolve4(packageRoot, "dist", `${subpath}.cjs`)
4313
+ resolve5(packageRoot, "dist", `${subpath}.mjs`),
4314
+ resolve5(packageRoot, "dist", `${subpath}.js`),
4315
+ resolve5(packageRoot, "dist", `${subpath}.cjs`)
2716
4316
  ];
2717
4317
  const sourceCandidates = [
2718
- resolve4(packageRoot, "src", `${subpath}.ts`),
2719
- resolve4(packageRoot, "src", `${subpath}.tsx`)
4318
+ resolve5(packageRoot, "src", `${subpath}.ts`),
4319
+ resolve5(packageRoot, "src", `${subpath}.tsx`)
2720
4320
  ];
2721
4321
  return options.preferBuilt ? [...builtCandidates, ...sourceCandidates] : [...sourceCandidates, ...builtCandidates];
2722
4322
  }
@@ -2751,7 +4351,7 @@ function resolveWorkspacePackageImport(specifier, startDir, options = {}) {
2751
4351
  // src/shares/config/loader.ts
2752
4352
  function resolveConfigPath(cwd = process.cwd()) {
2753
4353
  for (const configFile of ELIT_CONFIG_FILES) {
2754
- const configPath = resolve4(cwd, configFile);
4354
+ const configPath = resolve5(cwd, configFile);
2755
4355
  if (existsSync4(configPath)) {
2756
4356
  return configPath;
2757
4357
  }
@@ -2780,7 +4380,7 @@ async function loadConfigFile(configPath) {
2780
4380
  if (ext === "ts" || ext === "mts") {
2781
4381
  try {
2782
4382
  const { build } = await import("esbuild");
2783
- const configDir = dirname2(configPath);
4383
+ const configDir = dirname3(configPath);
2784
4384
  const tempFile = join5(configDir, `.elit-config-${Date.now()}.mjs`);
2785
4385
  const externalAllPlugin = {
2786
4386
  name: "external-all",
@@ -2854,6 +4454,43 @@ async function loadConfigFile(configPath) {
2854
4454
  }
2855
4455
 
2856
4456
  // src/cli/pm/commands.ts
4457
+ var PM_SIGNAL_NAMES = /* @__PURE__ */ new Set([
4458
+ "SIGABRT",
4459
+ "SIGALRM",
4460
+ "SIGBREAK",
4461
+ "SIGBUS",
4462
+ "SIGCHLD",
4463
+ "SIGCONT",
4464
+ "SIGFPE",
4465
+ "SIGHUP",
4466
+ "SIGILL",
4467
+ "SIGINT",
4468
+ "SIGIO",
4469
+ "SIGIOT",
4470
+ "SIGKILL",
4471
+ "SIGPIPE",
4472
+ "SIGPOLL",
4473
+ "SIGPROF",
4474
+ "SIGPWR",
4475
+ "SIGQUIT",
4476
+ "SIGSEGV",
4477
+ "SIGSTKFLT",
4478
+ "SIGSTOP",
4479
+ "SIGSYS",
4480
+ "SIGTERM",
4481
+ "SIGTRAP",
4482
+ "SIGTSTP",
4483
+ "SIGTTIN",
4484
+ "SIGTTOU",
4485
+ "SIGUNUSED",
4486
+ "SIGURG",
4487
+ "SIGUSR1",
4488
+ "SIGUSR2",
4489
+ "SIGVTALRM",
4490
+ "SIGWINCH",
4491
+ "SIGXCPU",
4492
+ "SIGXFSZ"
4493
+ ]);
2857
4494
  async function runPmStart(args) {
2858
4495
  const parsed = parsePmStartArgs(args);
2859
4496
  const workspaceRoot = process.cwd();
@@ -2886,9 +4523,130 @@ function resolveNamedMatches(paths, value) {
2886
4523
  if (value === "all") {
2887
4524
  return listPmRecordMatches(paths).map(syncPmRecordLiveness);
2888
4525
  }
4526
+ const groupMatches = findPmGroupMatches(paths, value);
4527
+ if (groupMatches.length > 0) {
4528
+ return groupMatches.map(syncPmRecordLiveness);
4529
+ }
2889
4530
  const match = findPmRecordMatch(paths, value);
2890
4531
  return match ? [syncPmRecordLiveness(match)] : [];
2891
4532
  }
4533
+ function sortPmMatchesByInstance(matches) {
4534
+ return [...matches].sort((left, right) => left.record.instanceIndex - right.record.instanceIndex);
4535
+ }
4536
+ function resolveInspectableMatch(paths, value) {
4537
+ const exactMatch = findPmRecordMatch(paths, value);
4538
+ const groupMatches = findPmGroupMatches(paths, value).map(syncPmRecordLiveness);
4539
+ if (groupMatches.length > 1 && exactMatch?.record.baseName === value && exactMatch.record.name === value) {
4540
+ 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}.`);
4541
+ }
4542
+ if (exactMatch) {
4543
+ return syncPmRecordLiveness(exactMatch);
4544
+ }
4545
+ return groupMatches[0];
4546
+ }
4547
+ function rebuildPmRecordDefinition(record, targetInstances = record.instances) {
4548
+ const definition = resolvePmAppDefinition(
4549
+ toPmAppConfig(record),
4550
+ { name: record.baseName, env: {}, watchPaths: [], watchIgnore: [], instances: targetInstances },
4551
+ process.cwd(),
4552
+ record.source
4553
+ );
4554
+ return {
4555
+ ...definition,
4556
+ name: record.name,
4557
+ baseName: record.baseName,
4558
+ instanceIndex: record.instanceIndex,
4559
+ instances: targetInstances
4560
+ };
4561
+ }
4562
+ function rebuildPmSavedDefinition(app) {
4563
+ const definition = resolvePmAppDefinition(
4564
+ toSavedPmAppConfig(app),
4565
+ { name: app.baseName, env: {}, watchPaths: [], watchIgnore: [], instances: app.instances },
4566
+ process.cwd(),
4567
+ "cli"
4568
+ );
4569
+ return {
4570
+ ...definition,
4571
+ name: app.name,
4572
+ baseName: app.baseName,
4573
+ instanceIndex: app.instanceIndex,
4574
+ instances: app.instances
4575
+ };
4576
+ }
4577
+ function deletePmMatches(matches) {
4578
+ for (const match of matches) {
4579
+ if (existsSync5(match.record.logFiles.out)) {
4580
+ rmSync(match.record.logFiles.out, { force: true });
4581
+ }
4582
+ if (existsSync5(match.record.logFiles.err)) {
4583
+ rmSync(match.record.logFiles.err, { force: true });
4584
+ }
4585
+ rmSync(match.filePath, { force: true });
4586
+ }
4587
+ }
4588
+ function updatePmInstanceCount(matches, instances) {
4589
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4590
+ for (const match of matches) {
4591
+ writePmRecord(match.filePath, {
4592
+ ...match.record,
4593
+ instances,
4594
+ updatedAt: now
4595
+ });
4596
+ }
4597
+ }
4598
+ function normalizePmSignalName(value) {
4599
+ const trimmed = value.trim().toUpperCase();
4600
+ const signalName = trimmed.startsWith("SIG") ? trimmed : `SIG${trimmed}`;
4601
+ if (!PM_SIGNAL_NAMES.has(signalName)) {
4602
+ throw new Error(`Unsupported pm signal: ${value}`);
4603
+ }
4604
+ return signalName;
4605
+ }
4606
+ function resolveSignalablePid(record) {
4607
+ if (record.childPid && record.childPid > 0) {
4608
+ return record.childPid;
4609
+ }
4610
+ if (record.runnerPid && record.runnerPid > 0) {
4611
+ return record.runnerPid;
4612
+ }
4613
+ return void 0;
4614
+ }
4615
+ function groupPmMatchesByBaseName(matches) {
4616
+ const grouped = /* @__PURE__ */ new Map();
4617
+ for (const match of matches) {
4618
+ const group = grouped.get(match.record.baseName);
4619
+ if (group) {
4620
+ group.push(match);
4621
+ continue;
4622
+ }
4623
+ grouped.set(match.record.baseName, [match]);
4624
+ }
4625
+ return [...grouped.entries()].sort((left, right) => left[0].localeCompare(right[0])).map(([, group]) => sortPmMatchesByInstance(group));
4626
+ }
4627
+ async function waitForPmRecordOnline(filePath, timeoutMs) {
4628
+ const deadline = Date.now() + timeoutMs;
4629
+ while (Date.now() < deadline) {
4630
+ if (!existsSync5(filePath)) {
4631
+ break;
4632
+ }
4633
+ const record = readPmRecord(filePath);
4634
+ if (record.status === "online") {
4635
+ return record;
4636
+ }
4637
+ if (record.status === "errored" || record.status === "exited" || record.status === "stopped") {
4638
+ throw new Error(record.error ?? `Process ${record.name} failed while reloading.`);
4639
+ }
4640
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
4641
+ }
4642
+ throw new Error(`Timed out waiting for the reloaded process to become online after ${timeoutMs}ms.`);
4643
+ }
4644
+ function resolvePmReloadReadyTimeout(record) {
4645
+ return record.waitReady || record.proxy?.strategy === "inherit" ? Math.max(record.listenTimeout + 1e3, 2e3) : Math.max(record.restartDelay + 1e3, 2e3);
4646
+ }
4647
+ function supportsPmProxyReload2(record) {
4648
+ return Boolean(record.proxy) && record.instances === 1 && Boolean(record.runnerPid);
4649
+ }
2892
4650
  function padCell(value, width) {
2893
4651
  return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
2894
4652
  }
@@ -2899,8 +4657,255 @@ function tailLogFile(filePath, lineCount) {
2899
4657
  const lines = readFileSync3(filePath, "utf8").split(/\r?\n/).filter((line) => line.length > 0);
2900
4658
  return lines.slice(-lineCount).join(EOL2);
2901
4659
  }
2902
- function printPmList(paths) {
2903
- const matches = listPmRecordMatches(paths).map(syncPmRecordLiveness);
4660
+ function listPmMatches(paths) {
4661
+ return listPmRecordMatches(paths).map(syncPmRecordLiveness);
4662
+ }
4663
+ function isPmRecordActive(record) {
4664
+ return record.desiredState === "running" && (record.status === "starting" || record.status === "online" || record.status === "restarting");
4665
+ }
4666
+ function resolvePmUptimeMs(record) {
4667
+ if (!isPmRecordActive(record) || !record.startedAt) {
4668
+ return void 0;
4669
+ }
4670
+ const startedTime = Date.parse(record.startedAt);
4671
+ if (Number.isNaN(startedTime)) {
4672
+ return void 0;
4673
+ }
4674
+ return Math.max(0, Date.now() - startedTime);
4675
+ }
4676
+ function resolvePmLiveMetrics(record) {
4677
+ const uptimeMs = resolvePmUptimeMs(record);
4678
+ if (!isPmRecordActive(record) || !record.childPid) {
4679
+ return { uptimeMs };
4680
+ }
4681
+ const sampledMetrics = samplePmProcessMetrics(record.childPid);
4682
+ return {
4683
+ ...sampledMetrics,
4684
+ uptimeMs,
4685
+ updatedAt: sampledMetrics.cpuPercent !== void 0 || sampledMetrics.memoryRssBytes !== void 0 ? (/* @__PURE__ */ new Date()).toISOString() : void 0
4686
+ };
4687
+ }
4688
+ function toPmDisplayRecord(record) {
4689
+ return {
4690
+ record,
4691
+ liveMetrics: resolvePmLiveMetrics(record)
4692
+ };
4693
+ }
4694
+ function serializePmRecord(record) {
4695
+ return {
4696
+ ...record,
4697
+ liveMetrics: resolvePmLiveMetrics(record)
4698
+ };
4699
+ }
4700
+ function parsePmFormatOption(args, index, option) {
4701
+ let value;
4702
+ if (option.startsWith("--format=")) {
4703
+ value = option.slice("--format=".length);
4704
+ } else {
4705
+ value = readRequiredValue(args, index + 1, "--format");
4706
+ index += 1;
4707
+ }
4708
+ if (value !== "table" && value !== "json") {
4709
+ throw new Error(`Unsupported pm output format: ${value}`);
4710
+ }
4711
+ return {
4712
+ format: value,
4713
+ nextIndex: index
4714
+ };
4715
+ }
4716
+ function parsePmListArgs(args) {
4717
+ let format = "table";
4718
+ for (let index = 0; index < args.length; index++) {
4719
+ const arg = args[index];
4720
+ switch (arg) {
4721
+ case "--json":
4722
+ format = "json";
4723
+ break;
4724
+ case "--format":
4725
+ default:
4726
+ if (arg === "--format" || arg.startsWith("--format=")) {
4727
+ const parsed = parsePmFormatOption(args, index, arg);
4728
+ format = parsed.format;
4729
+ index = parsed.nextIndex;
4730
+ break;
4731
+ }
4732
+ throw new Error(`Unknown pm list option: ${arg}`);
4733
+ }
4734
+ }
4735
+ return { format };
4736
+ }
4737
+ function parsePmShowArgs(args) {
4738
+ let format = "text";
4739
+ let name;
4740
+ for (let index = 0; index < args.length; index++) {
4741
+ const arg = args[index];
4742
+ switch (arg) {
4743
+ case "--json":
4744
+ format = "json";
4745
+ break;
4746
+ case "--format":
4747
+ default:
4748
+ if (arg === "--format" || arg.startsWith("--format=")) {
4749
+ const parsed = parsePmFormatOption(args, index, arg);
4750
+ format = parsed.format === "json" ? "json" : "text";
4751
+ index = parsed.nextIndex;
4752
+ break;
4753
+ }
4754
+ if (arg.startsWith("-")) {
4755
+ throw new Error(`Unknown pm show option: ${arg}`);
4756
+ }
4757
+ if (name) {
4758
+ throw new Error("pm show accepts exactly one process name.");
4759
+ }
4760
+ name = arg;
4761
+ break;
4762
+ }
4763
+ }
4764
+ if (!name) {
4765
+ throw new Error("Usage: elit pm show <name> [--json]");
4766
+ }
4767
+ return { name, format };
4768
+ }
4769
+ function formatPmDuration(durationMs) {
4770
+ if (durationMs < 1e3) {
4771
+ return `${durationMs}ms`;
4772
+ }
4773
+ const totalSeconds = Math.floor(durationMs / 1e3);
4774
+ const days = Math.floor(totalSeconds / 86400);
4775
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
4776
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
4777
+ const seconds = totalSeconds % 60;
4778
+ const parts = [];
4779
+ if (days > 0) {
4780
+ parts.push(`${days}d`);
4781
+ }
4782
+ if (hours > 0) {
4783
+ parts.push(`${hours}h`);
4784
+ }
4785
+ if (minutes > 0) {
4786
+ parts.push(`${minutes}m`);
4787
+ }
4788
+ if (seconds > 0 || parts.length === 0) {
4789
+ parts.push(`${seconds}s`);
4790
+ }
4791
+ return parts.slice(0, 2).join(" ");
4792
+ }
4793
+ function formatPmCpuPercent(cpuPercent) {
4794
+ if (cpuPercent === void 0 || !Number.isFinite(cpuPercent)) {
4795
+ return "-";
4796
+ }
4797
+ return `${cpuPercent >= 100 ? cpuPercent.toFixed(0) : cpuPercent.toFixed(1)}%`;
4798
+ }
4799
+ function formatPmMemory(memoryRssBytes) {
4800
+ if (memoryRssBytes === void 0 || !Number.isFinite(memoryRssBytes) || memoryRssBytes < 0) {
4801
+ return "-";
4802
+ }
4803
+ const units = ["B", "KB", "MB", "GB", "TB"];
4804
+ let value = memoryRssBytes;
4805
+ let unitIndex = 0;
4806
+ while (value >= 1024 && unitIndex < units.length - 1) {
4807
+ value /= 1024;
4808
+ unitIndex += 1;
4809
+ }
4810
+ const formatted = value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1);
4811
+ return `${formatted}${units[unitIndex]}`;
4812
+ }
4813
+ function formatPmUptime(uptimeMs) {
4814
+ if (uptimeMs === void 0 || !Number.isFinite(uptimeMs)) {
4815
+ return "-";
4816
+ }
4817
+ return formatPmDuration(Math.max(0, uptimeMs));
4818
+ }
4819
+ function formatPmTarget(record) {
4820
+ if (record.script) {
4821
+ return record.script;
4822
+ }
4823
+ if (record.file) {
4824
+ return record.file;
4825
+ }
4826
+ if (record.wapk) {
4827
+ return record.wapk;
4828
+ }
4829
+ return "-";
4830
+ }
4831
+ function pushPmDetail(lines, label, value) {
4832
+ lines.push(`${padCell(`${label}:`, 18)} ${value}`);
4833
+ }
4834
+ function pushPmDetailList(lines, label, values) {
4835
+ if (values.length === 0) {
4836
+ pushPmDetail(lines, label, "-");
4837
+ return;
4838
+ }
4839
+ pushPmDetail(lines, label, values[0] ?? "-");
4840
+ for (const value of values.slice(1)) {
4841
+ lines.push(`${" ".repeat(20)} ${value}`);
4842
+ }
4843
+ }
4844
+ function formatPmRecordDetails(record, liveMetrics) {
4845
+ const lines = [`Process: ${record.name}`];
4846
+ pushPmDetail(lines, "id", record.id);
4847
+ pushPmDetail(lines, "status", record.status);
4848
+ pushPmDetail(lines, "desired state", record.desiredState);
4849
+ pushPmDetail(lines, "instance", `${record.instanceIndex}/${record.instances}`);
4850
+ pushPmDetail(lines, "cpu", formatPmCpuPercent(liveMetrics.cpuPercent));
4851
+ pushPmDetail(lines, "memory", formatPmMemory(liveMetrics.memoryRssBytes));
4852
+ pushPmDetail(lines, "uptime", formatPmUptime(liveMetrics.uptimeMs));
4853
+ pushPmDetail(lines, "type", record.type);
4854
+ pushPmDetail(lines, "source", record.source);
4855
+ pushPmDetail(lines, "runtime", record.runtime ?? "-");
4856
+ pushPmDetail(lines, "cwd", record.cwd);
4857
+ pushPmDetail(lines, "target", formatPmTarget(record));
4858
+ pushPmDetail(lines, "command", record.commandPreview || "-");
4859
+ pushPmDetail(lines, "runner pid", record.runnerPid ? String(record.runnerPid) : "-");
4860
+ pushPmDetail(lines, "child pid", record.childPid ? String(record.childPid) : "-");
4861
+ pushPmDetail(lines, "restart count", `${record.restartCount}/${record.maxRestarts}`);
4862
+ pushPmDetail(lines, "restart policy", record.restartPolicy);
4863
+ pushPmDetail(lines, "proxy", record.proxy ? `http://${record.proxy.host ?? "0.0.0.0"}:${record.proxy.port}` : "-");
4864
+ pushPmDetail(lines, "proxy strategy", record.proxy?.strategy ?? "-");
4865
+ pushPmDetail(lines, "proxy target", record.proxy && record.proxyTargetPort ? `${record.proxy.targetHost ?? "127.0.0.1"}:${record.proxyTargetPort}` : "-");
4866
+ pushPmDetail(lines, "max memory", record.maxMemoryBytes ? formatPmMemory(record.maxMemoryBytes) : "-");
4867
+ pushPmDetail(lines, "memory action", record.memoryAction ?? "-");
4868
+ pushPmDetail(lines, "cron restart", record.cronRestart ?? "-");
4869
+ pushPmDetail(lines, "exp backoff", record.expBackoffRestartDelay ? formatPmDuration(record.expBackoffRestartDelay) : "-");
4870
+ pushPmDetail(lines, "exp backoff max", record.expBackoffRestartMaxDelay ? formatPmDuration(record.expBackoffRestartMaxDelay) : "-");
4871
+ pushPmDetail(lines, "restart window", record.restartWindow ? formatPmDuration(record.restartWindow) : "-");
4872
+ pushPmDetail(lines, "wait ready", record.waitReady ? "enabled" : "disabled");
4873
+ pushPmDetail(lines, "listen timeout", record.waitReady ? formatPmDuration(record.listenTimeout) : "-");
4874
+ pushPmDetail(lines, "restart delay", formatPmDuration(record.restartDelay));
4875
+ pushPmDetail(lines, "kill timeout", formatPmDuration(record.killTimeout));
4876
+ pushPmDetail(lines, "min uptime", formatPmDuration(record.minUptime));
4877
+ pushPmDetail(lines, "autorestart", record.autorestart ? "enabled" : "disabled");
4878
+ pushPmDetail(lines, "watch", record.watch ? "enabled" : "disabled");
4879
+ pushPmDetail(lines, "watch debounce", record.watch ? formatPmDuration(record.watchDebounce) : "-");
4880
+ pushPmDetailList(lines, "watch paths", record.watchPaths);
4881
+ pushPmDetailList(lines, "watch ignore", record.watchIgnore);
4882
+ if (record.healthCheck) {
4883
+ pushPmDetail(lines, "health check", record.healthCheck.url);
4884
+ pushPmDetail(lines, "health grace", formatPmDuration(record.healthCheck.gracePeriod));
4885
+ pushPmDetail(lines, "health interval", formatPmDuration(record.healthCheck.interval));
4886
+ pushPmDetail(lines, "health timeout", formatPmDuration(record.healthCheck.timeout));
4887
+ pushPmDetail(lines, "health failures", String(record.healthCheck.maxFailures));
4888
+ } else {
4889
+ pushPmDetail(lines, "health check", "-");
4890
+ }
4891
+ pushPmDetailList(lines, "env", Object.entries(record.env).map(([key, value]) => `${key}=${value}`));
4892
+ pushPmDetail(lines, "stdout log", record.logFiles.out);
4893
+ pushPmDetail(lines, "stderr log", record.logFiles.err);
4894
+ pushPmDetail(lines, "created at", record.createdAt);
4895
+ pushPmDetail(lines, "updated at", record.updatedAt);
4896
+ pushPmDetail(lines, "metrics at", liveMetrics.updatedAt ?? "-");
4897
+ pushPmDetail(lines, "started at", record.startedAt ?? "-");
4898
+ pushPmDetail(lines, "stopped at", record.stoppedAt ?? "-");
4899
+ pushPmDetail(lines, "last exit", record.lastExitCode === void 0 ? "-" : String(record.lastExitCode));
4900
+ pushPmDetail(lines, "error", record.error ?? "-");
4901
+ return lines.join(EOL2);
4902
+ }
4903
+ function printPmList(paths, format = "table") {
4904
+ const matches = listPmMatches(paths).map((match) => toPmDisplayRecord(match.record));
4905
+ if (format === "json") {
4906
+ console.log(JSON.stringify(matches.map((match) => ({ ...match.record, liveMetrics: match.liveMetrics })), null, 2));
4907
+ return;
4908
+ }
2904
4909
  if (matches.length === 0) {
2905
4910
  console.log("No managed processes found.");
2906
4911
  return;
@@ -2909,22 +4914,47 @@ function printPmList(paths) {
2909
4914
  padCell("name", 20),
2910
4915
  padCell("status", 12),
2911
4916
  padCell("pid", 8),
4917
+ padCell("cpu", 8),
4918
+ padCell("memory", 10),
4919
+ padCell("uptime", 10),
2912
4920
  padCell("restarts", 10),
2913
4921
  padCell("type", 8),
2914
4922
  "runtime"
2915
4923
  ];
2916
4924
  console.log(headers.join(" "));
2917
- for (const { record } of matches) {
4925
+ for (const { record, liveMetrics } of matches) {
2918
4926
  console.log([
2919
4927
  padCell(record.name, 20),
2920
4928
  padCell(record.status, 12),
2921
4929
  padCell(record.childPid ? String(record.childPid) : "-", 8),
4930
+ padCell(formatPmCpuPercent(liveMetrics.cpuPercent), 8),
4931
+ padCell(formatPmMemory(liveMetrics.memoryRssBytes), 10),
4932
+ padCell(formatPmUptime(liveMetrics.uptimeMs), 10),
2922
4933
  padCell(String(record.restartCount ?? 0), 10),
2923
4934
  padCell(record.type, 8),
2924
4935
  record.runtime ?? "-"
2925
4936
  ].join(" "));
2926
4937
  }
2927
4938
  }
4939
+ async function runPmList(args) {
4940
+ const options = parsePmListArgs(args);
4941
+ const { paths } = await loadPmContext();
4942
+ printPmList(paths, options.format);
4943
+ }
4944
+ async function runPmShow(args) {
4945
+ const options = parsePmShowArgs(args);
4946
+ const { paths } = await loadPmContext();
4947
+ const match = resolveInspectableMatch(paths, options.name);
4948
+ if (!match) {
4949
+ throw new Error(`No managed process found for: ${options.name}`);
4950
+ }
4951
+ const synced = syncPmRecordLiveness(match);
4952
+ if (options.format === "json") {
4953
+ console.log(JSON.stringify(serializePmRecord(synced.record), null, 2));
4954
+ return;
4955
+ }
4956
+ console.log(formatPmRecordDetails(synced.record, resolvePmLiveMetrics(synced.record)));
4957
+ }
2928
4958
  async function runPmStop(args) {
2929
4959
  const target = args[0];
2930
4960
  if (!target) {
@@ -2951,17 +4981,62 @@ async function runPmRestart(args) {
2951
4981
  await stopPmMatches(matches);
2952
4982
  const restarted = [];
2953
4983
  for (const match of matches) {
2954
- const definition = resolvePmAppDefinition(
2955
- toPmAppConfig(match.record),
2956
- { name: match.record.name, env: {}, watchPaths: [], watchIgnore: [] },
2957
- process.cwd(),
2958
- match.record.source
2959
- );
4984
+ const definition = rebuildPmRecordDefinition(match.record);
2960
4985
  await startManagedProcess(definition, paths);
2961
4986
  restarted.push(match.record.name);
2962
4987
  }
2963
4988
  console.log(`[pm] restarted ${restarted.join(", ")}`);
2964
4989
  }
4990
+ async function runPmReload(args) {
4991
+ const target = args[0];
4992
+ if (!target) {
4993
+ throw new Error("Usage: elit pm reload <name|all>");
4994
+ }
4995
+ const { paths } = await loadPmContext();
4996
+ const matches = resolveNamedMatches(paths, target);
4997
+ if (matches.length === 0) {
4998
+ throw new Error(`No managed process found for: ${target}`);
4999
+ }
5000
+ const reloaded = [];
5001
+ const errors = [];
5002
+ for (const group of groupPmMatchesByBaseName(matches)) {
5003
+ for (const match of group) {
5004
+ try {
5005
+ if (supportsPmProxyReload2(match.record)) {
5006
+ const reloadRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
5007
+ writePmRecord(match.filePath, {
5008
+ ...match.record,
5009
+ status: "restarting",
5010
+ reloadRequestedAt,
5011
+ updatedAt: reloadRequestedAt,
5012
+ error: void 0
5013
+ });
5014
+ await waitForPmRecordOnline(
5015
+ getPmRecordPath(paths, match.record.id),
5016
+ resolvePmReloadReadyTimeout(match.record)
5017
+ );
5018
+ reloaded.push(match.record.name);
5019
+ continue;
5020
+ }
5021
+ await stopPmMatches([match]);
5022
+ const definition = rebuildPmRecordDefinition(match.record);
5023
+ const startedRecord = await startManagedProcess(definition, paths);
5024
+ await waitForPmRecordOnline(
5025
+ getPmRecordPath(paths, startedRecord.id),
5026
+ resolvePmReloadReadyTimeout(startedRecord)
5027
+ );
5028
+ reloaded.push(match.record.name);
5029
+ } catch (error) {
5030
+ const message = error instanceof Error ? error.message : String(error);
5031
+ errors.push(`[pm] ${match.record.name}: ${message}`);
5032
+ }
5033
+ }
5034
+ }
5035
+ if (errors.length > 0) {
5036
+ throw new Error([`[pm] reloaded ${reloaded.length} process${reloaded.length === 1 ? "" : "es"}`, ...errors].join(EOL2));
5037
+ }
5038
+ console.log(`[pm] reloaded ${reloaded.join(", ")}`);
5039
+ }
2965
5040
  async function runPmSave() {
2966
5041
  const { paths } = await loadPmContext();
2967
5042
  ensurePmDirectories(paths);
@@ -2983,12 +5058,7 @@ async function runPmResurrect() {
2983
5058
  let restored = 0;
2984
5059
  for (const app of dump.apps) {
2985
5060
  try {
2986
- const definition = resolvePmAppDefinition(
2987
- toSavedPmAppConfig(app),
2988
- { name: app.name, env: {}, watchPaths: [], watchIgnore: [] },
2989
- process.cwd(),
2990
- "cli"
2991
- );
5061
+ const definition = rebuildPmSavedDefinition(app);
2992
5062
  await startManagedProcess(definition, paths);
2993
5063
  restored += 1;
2994
5064
  } catch (error) {
@@ -3012,16 +5082,127 @@ async function runPmDelete(args) {
3012
5082
  throw new Error(`No managed process found for: ${target}`);
3013
5083
  }
3014
5084
  await stopPmMatches(matches);
5085
+ deletePmMatches(matches);
5086
+ console.log(`[pm] deleted ${matches.length} process${matches.length === 1 ? "" : "es"}`);
5087
+ }
5088
+ async function runPmScale(args) {
5089
+ const target = args[0];
5090
+ const countArg = args[1];
5091
+ if (!target || countArg === void 0 || args.length > 2) {
5092
+ throw new Error("Usage: elit pm scale <name> <count>");
5093
+ }
5094
+ const desiredCount = normalizeIntegerOption(countArg, "pm scale <count>", 0);
5095
+ const { config, paths } = await loadPmContext();
5096
+ const exactMatch = findPmRecordMatch(paths, target);
5097
+ const currentMatches = sortPmMatchesByInstance(resolveNamedMatches(paths, exactMatch?.record.baseName ?? target));
5098
+ if (currentMatches.length === 0) {
5099
+ if (desiredCount === 0) {
5100
+ console.log(`[pm] ${target} already scaled to 0 instances`);
5101
+ return;
5102
+ }
5103
+ const definitions = resolvePmStartDefinitions(
5104
+ { name: target, env: {}, watchPaths: [], watchIgnore: [], instances: desiredCount },
5105
+ config,
5106
+ process.cwd()
5107
+ );
5108
+ for (const definition of definitions) {
5109
+ await startManagedProcess(definition, paths);
5110
+ }
5111
+ console.log(`[pm] scaled ${target} to ${desiredCount} instance${desiredCount === 1 ? "" : "s"}`);
5112
+ return;
5113
+ }
5114
+ const baseName = currentMatches[0]?.record.baseName ?? target;
5115
+ if (desiredCount === currentMatches.length) {
5116
+ console.log(`[pm] ${baseName} already scaled to ${desiredCount} instance${desiredCount === 1 ? "" : "s"}`);
5117
+ return;
5118
+ }
5119
+ if (desiredCount === 0) {
5120
+ await stopPmMatches(currentMatches);
5121
+ deletePmMatches(currentMatches);
5122
+ console.log(`[pm] scaled ${baseName} to 0 instances`);
5123
+ return;
5124
+ }
5125
+ if (desiredCount < currentMatches.length) {
5126
+ const toRemove = [...currentMatches].sort((left, right) => right.record.instanceIndex - left.record.instanceIndex).slice(0, currentMatches.length - desiredCount);
5127
+ const remaining = currentMatches.filter((match) => !toRemove.some((removal) => removal.record.id === match.record.id));
5128
+ await stopPmMatches(toRemove);
5129
+ deletePmMatches(toRemove);
5130
+ updatePmInstanceCount(remaining, desiredCount);
5131
+ console.log(`[pm] scaled ${baseName} to ${desiredCount} instance${desiredCount === 1 ? "" : "s"}`);
5132
+ return;
5133
+ }
5134
+ updatePmInstanceCount(currentMatches, desiredCount);
5135
+ const baseRecord = currentMatches[0]?.record;
5136
+ if (!baseRecord) {
5137
+ throw new Error(`No managed process found for: ${target}`);
5138
+ }
5139
+ const baseDefinition = rebuildPmRecordDefinition(baseRecord, desiredCount);
5140
+ const expandedDefinitions = expandPmInstanceDefinitions(baseDefinition, desiredCount);
5141
+ const existingNames = new Set(currentMatches.map((match) => match.record.name));
5142
+ for (const definition of expandedDefinitions) {
5143
+ if (existingNames.has(definition.name)) {
5144
+ continue;
5145
+ }
5146
+ await startManagedProcess(definition, paths);
5147
+ }
5148
+ console.log(`[pm] scaled ${baseName} to ${desiredCount} instance${desiredCount === 1 ? "" : "s"}`);
5149
+ }
5150
+ async function runPmSendSignal(args) {
5151
+ const signalArg = args[0];
5152
+ const target = args[1];
5153
+ if (!signalArg || !target || args.length > 2) {
5154
+ throw new Error("Usage: elit pm send-signal <signal> <name|all>");
5155
+ }
5156
+ const signalName = normalizePmSignalName(signalArg);
5157
+ const { paths } = await loadPmContext();
5158
+ const matches = resolveNamedMatches(paths, target);
5159
+ if (matches.length === 0) {
5160
+ throw new Error(`No managed process found for: ${target}`);
5161
+ }
5162
+ let signaled = 0;
5163
+ const errors = [];
3015
5164
  for (const match of matches) {
3016
- if (existsSync5(match.record.logFiles.out)) {
3017
- rmSync(match.record.logFiles.out, { force: true });
5165
+ const pid = resolveSignalablePid(match.record);
5166
+ if (!pid) {
5167
+ continue;
3018
5168
  }
3019
- if (existsSync5(match.record.logFiles.err)) {
3020
- rmSync(match.record.logFiles.err, { force: true });
5169
+ try {
5170
+ sendPmSignal(pid, signalName);
5171
+ signaled += 1;
5172
+ } catch (error) {
5173
+ const message = error instanceof Error ? error.message : String(error);
5174
+ errors.push(`[pm] ${match.record.name}: ${message}`);
3021
5175
  }
3022
- rmSync(match.filePath, { force: true });
3023
5176
  }
3024
- console.log(`[pm] deleted ${matches.length} process${matches.length === 1 ? "" : "es"}`);
5177
+ if (errors.length > 0) {
5178
+ throw new Error([`[pm] sent ${signalName} to ${signaled} process${signaled === 1 ? "" : "es"}`, ...errors].join(EOL2));
5179
+ }
5180
+ if (signaled === 0) {
5181
+ throw new Error(`No running managed process found for: ${target}`);
5182
+ }
5183
+ console.log(`[pm] sent ${signalName} to ${signaled} process${signaled === 1 ? "" : "es"}`);
5184
+ }
5185
+ async function runPmReset(args) {
5186
+ const target = args[0];
5187
+ if (!target) {
5188
+ throw new Error("Usage: elit pm reset <name|all>");
5189
+ }
5190
+ const { paths } = await loadPmContext();
5191
+ const matches = resolveNamedMatches(paths, target);
5192
+ if (matches.length === 0) {
5193
+ throw new Error(`No managed process found for: ${target}`);
5194
+ }
5195
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5196
+ for (const match of matches) {
5197
+ writePmRecord(match.filePath, {
5198
+ ...match.record,
5199
+ restartCount: 0,
5200
+ lastExitCode: void 0,
5201
+ error: void 0,
5202
+ updatedAt: now
5203
+ });
5204
+ }
5205
+ console.log(`[pm] reset ${matches.length} process${matches.length === 1 ? "" : "es"}`);
3025
5206
  }
3026
5207
  async function runPmLogs(args) {
3027
5208
  if (args.length === 0) {
@@ -3054,7 +5235,7 @@ async function runPmLogs(args) {
3054
5235
  throw new Error("Usage: elit pm logs <name> [--lines <n>] [--stderr]");
3055
5236
  }
3056
5237
  const { paths } = await loadPmContext();
3057
- const match = findPmRecordMatch(paths, name);
5238
+ const match = resolveInspectableMatch(paths, name);
3058
5239
  if (!match) {
3059
5240
  throw new Error(`No managed process found for: ${name}`);
3060
5241
  }
@@ -3078,17 +5259,36 @@ async function runPmCommand(args) {
3078
5259
  await runPmStart(args.slice(1));
3079
5260
  return;
3080
5261
  case "list":
3081
- case "ls": {
3082
- const { paths } = await loadPmContext();
3083
- printPmList(paths);
5262
+ case "ls":
5263
+ await runPmList(args.slice(1));
5264
+ return;
5265
+ case "jlist":
5266
+ await runPmList(["--json", ...args.slice(1)]);
5267
+ return;
5268
+ case "show":
5269
+ case "describe":
5270
+ await runPmShow(args.slice(1));
3084
5271
  return;
3085
- }
3086
5272
  case "stop":
3087
5273
  await runPmStop(args.slice(1));
3088
5274
  return;
3089
5275
  case "restart":
3090
5276
  await runPmRestart(args.slice(1));
3091
5277
  return;
5278
+ case "reload":
5279
+ await runPmReload(args.slice(1));
5280
+ return;
5281
+ case "scale":
5282
+ await runPmScale(args.slice(1));
5283
+ return;
5284
+ case "send-signal":
5285
+ case "signal":
5286
+ case "sendSignal":
5287
+ await runPmSendSignal(args.slice(1));
5288
+ return;
5289
+ case "reset":
5290
+ await runPmReset(args.slice(1));
5291
+ return;
3092
5292
  case "delete":
3093
5293
  case "remove":
3094
5294
  case "rm":
@@ -3120,6 +5320,12 @@ export {
3120
5320
  DEFAULT_MIN_UPTIME,
3121
5321
  DEFAULT_PM_DATA_DIR,
3122
5322
  DEFAULT_PM_DUMP_FILE,
5323
+ DEFAULT_PM_EXP_BACKOFF_MAX_DELAY,
5324
+ DEFAULT_PM_KILL_TIMEOUT,
5325
+ DEFAULT_PM_LISTEN_TIMEOUT,
5326
+ DEFAULT_PM_MEMORY_CHECK_INTERVAL,
5327
+ DEFAULT_PM_PROXY_STRATEGY,
5328
+ DEFAULT_PM_RESTART_WINDOW,
3123
5329
  DEFAULT_PM_STOP_GRACE_PERIOD_MS,
3124
5330
  DEFAULT_PM_STOP_POLL_MS,
3125
5331
  DEFAULT_RESTART_DELAY,
@@ -3138,7 +5344,10 @@ export {
3138
5344
  countDefinedPmWapkSources,
3139
5345
  deriveDefaultWatchPaths,
3140
5346
  ensurePmDirectories,
5347
+ expandPmInstanceDefinitions,
5348
+ findPmGroupMatches,
3141
5349
  findPmRecordMatch,
5350
+ formatPmInstanceName,
3142
5351
  getPmRecordPath,
3143
5352
  hasPmGoogleDriveConfig,
3144
5353
  hasPmWapkRunConfig,
@@ -3157,6 +5366,10 @@ export {
3157
5366
  normalizeHealthCheckConfig,
3158
5367
  normalizeIntegerOption,
3159
5368
  normalizeNonEmptyString,
5369
+ normalizePmMemoryAction,
5370
+ normalizePmMemoryLimit,
5371
+ normalizePmProxyConfig,
5372
+ normalizePmProxyStrategy,
3160
5373
  normalizePmRestartPolicy,
3161
5374
  normalizePmRuntime,
3162
5375
  normalizeResolvedWatchIgnorePaths,
@@ -3180,7 +5393,9 @@ export {
3180
5393
  runManagedProcessLoop,
3181
5394
  runPmCommand,
3182
5395
  runPmRunner,
5396
+ samplePmProcessMetrics,
3183
5397
  sanitizePmProcessName,
5398
+ sendPmSignal,
3184
5399
  startManagedProcess,
3185
5400
  stopPmMatches,
3186
5401
  stripPmWapkSourceFromRunConfig,