@nmakarov/cli-toolkit 0.53.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1081,6 +1081,10 @@ var Args = class _Args {
1081
1081
  this.parseArgs(args);
1082
1082
  this.env = this.get("env")?.toLowerCase() || "local";
1083
1083
  this.loadDotEnv();
1084
+ const envAfterDotEnv = this.get("env")?.toLowerCase();
1085
+ if (envAfterDotEnv) {
1086
+ this.env = envAfterDotEnv;
1087
+ }
1084
1088
  this.loadConfigFiles();
1085
1089
  this.checkConflicts();
1086
1090
  if (context && typeof context.registerCleanup === "function") {
@@ -5269,6 +5273,8 @@ function servicePaths(service) {
5269
5273
  current: join2(root, "current"),
5270
5274
  sharedEnv: join2(root, "shared", ".env"),
5271
5275
  ecosystem: join2(root, "shared", "ecosystem.config.cjs"),
5276
+ /** Preloaded by pm2 `node_args --require` so ENV survives flaky restarts. */
5277
+ ensureEnv: join2(root, "shared", "ensure-env.cjs"),
5272
5278
  deployLog: join2(root, "logs", "deploy.log"),
5273
5279
  lockHashFile: join2(root, "shared", ".package-lock.sha256")
5274
5280
  };
@@ -5587,6 +5593,16 @@ async function pruneReleases(service, paths, options = {}) {
5587
5593
  function sleep(ms) {
5588
5594
  return new Promise((resolve3) => setTimeout(resolve3, ms));
5589
5595
  }
5596
+ function isPidAlive(pid) {
5597
+ const n = Number(pid);
5598
+ if (!Number.isFinite(n) || n <= 0) return false;
5599
+ try {
5600
+ process.kill(n, 0);
5601
+ return true;
5602
+ } catch {
5603
+ return false;
5604
+ }
5605
+ }
5590
5606
  async function getPm2Process(appName, options = {}) {
5591
5607
  const { logger } = options;
5592
5608
  const { stdout } = await runCapture("bash", ["-lc", "pm2 jlist"], { logger });
@@ -5623,21 +5639,44 @@ async function waitPm2(appName, predicate, options = {}) {
5623
5639
  `pm2 wait timed out after ${timeoutMs}ms for ${appName} (${label}); last status=${lastStatus}`
5624
5640
  );
5625
5641
  }
5642
+ function isFreshOnline(proc, oldPid) {
5643
+ if (proc?.pm2_env?.status !== "online") return false;
5644
+ const pid = Number(proc.pid);
5645
+ if (!Number.isFinite(pid) || pid <= 0) return false;
5646
+ if (oldPid != null) {
5647
+ if (pid === oldPid) return false;
5648
+ if (isPidAlive(oldPid)) return false;
5649
+ }
5650
+ return true;
5651
+ }
5626
5652
  async function reloadPm2(paths, options = {}) {
5627
5653
  const { dryRun = false, logger = console, appName = null, waitTimeoutMs = 65e3 } = options;
5628
5654
  if (dryRun) {
5629
5655
  logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
5630
5656
  return;
5631
5657
  }
5658
+ let oldPid = null;
5659
+ if (appName) {
5660
+ const before = await getPm2Process(appName, { logger });
5661
+ const n = Number(before?.pid);
5662
+ if (Number.isFinite(n) && n > 0 && before?.pm2_env?.status === "online") {
5663
+ oldPid = n;
5664
+ logger.info(`pm2 ${appName}: reloading (old pid=${oldPid})`);
5665
+ }
5666
+ }
5632
5667
  await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
5633
5668
  logger.info("pm2 reloaded");
5634
5669
  if (appName) {
5635
- await waitPm2(
5670
+ const proc = await waitPm2(
5636
5671
  appName,
5637
- (proc) => proc?.pm2_env?.status === "online",
5638
- { timeoutMs: waitTimeoutMs, logger, label: "online after reload" }
5672
+ (p) => isFreshOnline(p, oldPid),
5673
+ {
5674
+ timeoutMs: waitTimeoutMs,
5675
+ logger,
5676
+ label: oldPid ? `online on new pid (old ${oldPid} gone)` : "online after reload"
5677
+ }
5639
5678
  );
5640
- logger.info(`pm2 ${appName} online`);
5679
+ logger.info(`pm2 ${appName} online pid=${proc?.pid ?? "?"}`);
5641
5680
  }
5642
5681
  }
5643
5682
  async function stopPm2(appName, options = {}) {
@@ -5655,10 +5694,15 @@ async function stopPm2(appName, options = {}) {
5655
5694
  logger.info(`pm2 ${appName}: already stopped`);
5656
5695
  return;
5657
5696
  }
5697
+ const oldPid = Number(before.pid);
5658
5698
  await runShell(`pm2 stop "${appName}"`, { logger });
5659
5699
  await waitPm2(
5660
5700
  appName,
5661
- (proc) => !proc || proc.pm2_env?.status === "stopped",
5701
+ (proc) => {
5702
+ if (proc && proc.pm2_env?.status !== "stopped") return false;
5703
+ if (Number.isFinite(oldPid) && oldPid > 0 && isPidAlive(oldPid)) return false;
5704
+ return true;
5705
+ },
5662
5706
  { timeoutMs: waitTimeoutMs, logger, label: "stopped" }
5663
5707
  );
5664
5708
  logger.info(`pm2 ${appName} stopped`);
@@ -5672,12 +5716,12 @@ async function startPm2(paths, options = {}) {
5672
5716
  await runShell(`pm2 start "${paths.ecosystem}" --update-env`, { logger });
5673
5717
  logger.info("pm2 started");
5674
5718
  if (appName) {
5675
- await waitPm2(
5719
+ const proc = await waitPm2(
5676
5720
  appName,
5677
- (proc) => proc?.pm2_env?.status === "online",
5721
+ (p) => isFreshOnline(p, null),
5678
5722
  { timeoutMs: waitTimeoutMs, logger, label: "online after start" }
5679
5723
  );
5680
- logger.info(`pm2 ${appName} online`);
5724
+ logger.info(`pm2 ${appName} online pid=${proc?.pid ?? "?"}`);
5681
5725
  }
5682
5726
  }
5683
5727
 
@@ -5921,12 +5965,23 @@ function resolvePm2Args(pm2) {
5921
5965
  if (/(?:^|\s)--stopAllowance=/.test(base)) return base;
5922
5966
  return `${base}${base ? " " : ""}--stopAllowance=${Math.floor(stopSec)}`.trim();
5923
5967
  }
5968
+ function buildEnsureEnvScript() {
5969
+ return `/**
5970
+ * Written by cli-toolkit deploy (init-structure). Do not hand-edit.
5971
+ * Preloaded via pm2 node_args --require so Args sees ENV=production even when
5972
+ * a post-SIGKILL restart briefly omits ecosystem env (otherwise Db \u2192 local:6032).
5973
+ */
5974
+ if (!process.env.ENV) process.env.ENV = "production";
5975
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
5976
+ `;
5977
+ }
5924
5978
  function buildEcosystemConfig(service, paths) {
5925
5979
  const { pm2 } = service;
5926
5980
  const outLog = join9(paths.logs, `${pm2.appName}.out.log`);
5927
5981
  const errLog = join9(paths.logs, `${pm2.appName}.err.log`);
5928
5982
  const killTimeoutMs = resolveKillTimeoutMs(pm2);
5929
5983
  const args = resolvePm2Args(pm2);
5984
+ const ensureEnv = paths.ensureEnv;
5930
5985
  return `/**
5931
5986
  * pm2 ecosystem for ${service.name} \u2014 written by cli-toolkit deploy from the
5932
5987
  * service manifest (init/deploy). Do not hand-edit; change pm2.* in the
@@ -5939,6 +5994,8 @@ module.exports = {
5939
5994
  script: "${pm2.script}",
5940
5995
  cwd: "${paths.current}",
5941
5996
  args: ${JSON.stringify(args)},
5997
+ // Absolute --require so ENV is set before Args constructs (see shared/ensure-env.cjs).
5998
+ node_args: ${JSON.stringify(`--require ${ensureEnv}`)},
5942
5999
  instances: 1,
5943
6000
  exec_mode: "fork",
5944
6001
  autorestart: true,
@@ -5980,9 +6037,17 @@ async function initServiceStructure(service, options = {}) {
5980
6037
  created.push(dir);
5981
6038
  }
5982
6039
  const ecosystemExisted = await pathExists4(paths.ecosystem);
6040
+ const ensureEnvExisted = await pathExists4(paths.ensureEnv);
5983
6041
  if (!dryRun) {
6042
+ await writeFile5(paths.ensureEnv, buildEnsureEnvScript(), { mode: 420 });
5984
6043
  await writeFile5(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
5985
6044
  }
6045
+ if (ensureEnvExisted) {
6046
+ logger.info(`synced ${paths.ensureEnv}`);
6047
+ } else {
6048
+ created.push(paths.ensureEnv);
6049
+ logger.info(`seeded ${paths.ensureEnv}`);
6050
+ }
5986
6051
  if (ecosystemExisted) {
5987
6052
  logger.info(`synced ${paths.ecosystem} from manifest`);
5988
6053
  } else {
@@ -9206,6 +9271,8 @@ export {
9206
9271
  installDeps,
9207
9272
  installOperatorShell,
9208
9273
  ipcFileLogsTableNameForSourceResource,
9274
+ isFreshOnline,
9275
+ isPidAlive,
9209
9276
  joiEdateType,
9210
9277
  joiStringArrayType,
9211
9278
  listServicesRegistry as listAliveRunnerHeartbeats,