@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.cjs CHANGED
@@ -1153,6 +1153,8 @@ __export(src_exports, {
1153
1153
  installDeps: () => installDeps,
1154
1154
  installOperatorShell: () => installOperatorShell,
1155
1155
  ipcFileLogsTableNameForSourceResource: () => ipcFileLogsTableNameForSourceResource,
1156
+ isFreshOnline: () => isFreshOnline,
1157
+ isPidAlive: () => isPidAlive,
1156
1158
  joiEdateType: () => joiEdateType,
1157
1159
  joiStringArrayType: () => joiStringArrayType,
1158
1160
  listAliveRunnerHeartbeats: () => listServicesRegistry,
@@ -1269,6 +1271,10 @@ var Args = class _Args {
1269
1271
  this.parseArgs(args);
1270
1272
  this.env = this.get("env")?.toLowerCase() || "local";
1271
1273
  this.loadDotEnv();
1274
+ const envAfterDotEnv = this.get("env")?.toLowerCase();
1275
+ if (envAfterDotEnv) {
1276
+ this.env = envAfterDotEnv;
1277
+ }
1272
1278
  this.loadConfigFiles();
1273
1279
  this.checkConflicts();
1274
1280
  if (context && typeof context.registerCleanup === "function") {
@@ -5431,6 +5437,8 @@ function servicePaths(service) {
5431
5437
  current: (0, import_node_path.join)(root, "current"),
5432
5438
  sharedEnv: (0, import_node_path.join)(root, "shared", ".env"),
5433
5439
  ecosystem: (0, import_node_path.join)(root, "shared", "ecosystem.config.cjs"),
5440
+ /** Preloaded by pm2 `node_args --require` so ENV survives flaky restarts. */
5441
+ ensureEnv: (0, import_node_path.join)(root, "shared", "ensure-env.cjs"),
5434
5442
  deployLog: (0, import_node_path.join)(root, "logs", "deploy.log"),
5435
5443
  lockHashFile: (0, import_node_path.join)(root, "shared", ".package-lock.sha256")
5436
5444
  };
@@ -5749,6 +5757,16 @@ async function pruneReleases(service, paths, options = {}) {
5749
5757
  function sleep(ms) {
5750
5758
  return new Promise((resolve3) => setTimeout(resolve3, ms));
5751
5759
  }
5760
+ function isPidAlive(pid) {
5761
+ const n = Number(pid);
5762
+ if (!Number.isFinite(n) || n <= 0) return false;
5763
+ try {
5764
+ process.kill(n, 0);
5765
+ return true;
5766
+ } catch {
5767
+ return false;
5768
+ }
5769
+ }
5752
5770
  async function getPm2Process(appName, options = {}) {
5753
5771
  const { logger } = options;
5754
5772
  const { stdout } = await runCapture("bash", ["-lc", "pm2 jlist"], { logger });
@@ -5785,21 +5803,44 @@ async function waitPm2(appName, predicate, options = {}) {
5785
5803
  `pm2 wait timed out after ${timeoutMs}ms for ${appName} (${label}); last status=${lastStatus}`
5786
5804
  );
5787
5805
  }
5806
+ function isFreshOnline(proc, oldPid) {
5807
+ if (proc?.pm2_env?.status !== "online") return false;
5808
+ const pid = Number(proc.pid);
5809
+ if (!Number.isFinite(pid) || pid <= 0) return false;
5810
+ if (oldPid != null) {
5811
+ if (pid === oldPid) return false;
5812
+ if (isPidAlive(oldPid)) return false;
5813
+ }
5814
+ return true;
5815
+ }
5788
5816
  async function reloadPm2(paths, options = {}) {
5789
5817
  const { dryRun = false, logger = console, appName = null, waitTimeoutMs = 65e3 } = options;
5790
5818
  if (dryRun) {
5791
5819
  logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
5792
5820
  return;
5793
5821
  }
5822
+ let oldPid = null;
5823
+ if (appName) {
5824
+ const before = await getPm2Process(appName, { logger });
5825
+ const n = Number(before?.pid);
5826
+ if (Number.isFinite(n) && n > 0 && before?.pm2_env?.status === "online") {
5827
+ oldPid = n;
5828
+ logger.info(`pm2 ${appName}: reloading (old pid=${oldPid})`);
5829
+ }
5830
+ }
5794
5831
  await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
5795
5832
  logger.info("pm2 reloaded");
5796
5833
  if (appName) {
5797
- await waitPm2(
5834
+ const proc = await waitPm2(
5798
5835
  appName,
5799
- (proc) => proc?.pm2_env?.status === "online",
5800
- { timeoutMs: waitTimeoutMs, logger, label: "online after reload" }
5836
+ (p) => isFreshOnline(p, oldPid),
5837
+ {
5838
+ timeoutMs: waitTimeoutMs,
5839
+ logger,
5840
+ label: oldPid ? `online on new pid (old ${oldPid} gone)` : "online after reload"
5841
+ }
5801
5842
  );
5802
- logger.info(`pm2 ${appName} online`);
5843
+ logger.info(`pm2 ${appName} online pid=${proc?.pid ?? "?"}`);
5803
5844
  }
5804
5845
  }
5805
5846
  async function stopPm2(appName, options = {}) {
@@ -5817,10 +5858,15 @@ async function stopPm2(appName, options = {}) {
5817
5858
  logger.info(`pm2 ${appName}: already stopped`);
5818
5859
  return;
5819
5860
  }
5861
+ const oldPid = Number(before.pid);
5820
5862
  await runShell(`pm2 stop "${appName}"`, { logger });
5821
5863
  await waitPm2(
5822
5864
  appName,
5823
- (proc) => !proc || proc.pm2_env?.status === "stopped",
5865
+ (proc) => {
5866
+ if (proc && proc.pm2_env?.status !== "stopped") return false;
5867
+ if (Number.isFinite(oldPid) && oldPid > 0 && isPidAlive(oldPid)) return false;
5868
+ return true;
5869
+ },
5824
5870
  { timeoutMs: waitTimeoutMs, logger, label: "stopped" }
5825
5871
  );
5826
5872
  logger.info(`pm2 ${appName} stopped`);
@@ -5834,12 +5880,12 @@ async function startPm2(paths, options = {}) {
5834
5880
  await runShell(`pm2 start "${paths.ecosystem}" --update-env`, { logger });
5835
5881
  logger.info("pm2 started");
5836
5882
  if (appName) {
5837
- await waitPm2(
5883
+ const proc = await waitPm2(
5838
5884
  appName,
5839
- (proc) => proc?.pm2_env?.status === "online",
5885
+ (p) => isFreshOnline(p, null),
5840
5886
  { timeoutMs: waitTimeoutMs, logger, label: "online after start" }
5841
5887
  );
5842
- logger.info(`pm2 ${appName} online`);
5888
+ logger.info(`pm2 ${appName} online pid=${proc?.pid ?? "?"}`);
5843
5889
  }
5844
5890
  }
5845
5891
 
@@ -6083,12 +6129,23 @@ function resolvePm2Args(pm2) {
6083
6129
  if (/(?:^|\s)--stopAllowance=/.test(base)) return base;
6084
6130
  return `${base}${base ? " " : ""}--stopAllowance=${Math.floor(stopSec)}`.trim();
6085
6131
  }
6132
+ function buildEnsureEnvScript() {
6133
+ return `/**
6134
+ * Written by cli-toolkit deploy (init-structure). Do not hand-edit.
6135
+ * Preloaded via pm2 node_args --require so Args sees ENV=production even when
6136
+ * a post-SIGKILL restart briefly omits ecosystem env (otherwise Db \u2192 local:6032).
6137
+ */
6138
+ if (!process.env.ENV) process.env.ENV = "production";
6139
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
6140
+ `;
6141
+ }
6086
6142
  function buildEcosystemConfig(service, paths) {
6087
6143
  const { pm2 } = service;
6088
6144
  const outLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.out.log`);
6089
6145
  const errLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.err.log`);
6090
6146
  const killTimeoutMs = resolveKillTimeoutMs(pm2);
6091
6147
  const args = resolvePm2Args(pm2);
6148
+ const ensureEnv = paths.ensureEnv;
6092
6149
  return `/**
6093
6150
  * pm2 ecosystem for ${service.name} \u2014 written by cli-toolkit deploy from the
6094
6151
  * service manifest (init/deploy). Do not hand-edit; change pm2.* in the
@@ -6101,6 +6158,8 @@ module.exports = {
6101
6158
  script: "${pm2.script}",
6102
6159
  cwd: "${paths.current}",
6103
6160
  args: ${JSON.stringify(args)},
6161
+ // Absolute --require so ENV is set before Args constructs (see shared/ensure-env.cjs).
6162
+ node_args: ${JSON.stringify(`--require ${ensureEnv}`)},
6104
6163
  instances: 1,
6105
6164
  exec_mode: "fork",
6106
6165
  autorestart: true,
@@ -6142,9 +6201,17 @@ async function initServiceStructure(service, options = {}) {
6142
6201
  created.push(dir);
6143
6202
  }
6144
6203
  const ecosystemExisted = await pathExists4(paths.ecosystem);
6204
+ const ensureEnvExisted = await pathExists4(paths.ensureEnv);
6145
6205
  if (!dryRun) {
6206
+ await (0, import_promises11.writeFile)(paths.ensureEnv, buildEnsureEnvScript(), { mode: 420 });
6146
6207
  await (0, import_promises11.writeFile)(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
6147
6208
  }
6209
+ if (ensureEnvExisted) {
6210
+ logger.info(`synced ${paths.ensureEnv}`);
6211
+ } else {
6212
+ created.push(paths.ensureEnv);
6213
+ logger.info(`seeded ${paths.ensureEnv}`);
6214
+ }
6148
6215
  if (ecosystemExisted) {
6149
6216
  logger.info(`synced ${paths.ecosystem} from manifest`);
6150
6217
  } else {
@@ -9369,6 +9436,8 @@ var TasksManager = class _TasksManager {
9369
9436
  installDeps,
9370
9437
  installOperatorShell,
9371
9438
  ipcFileLogsTableNameForSourceResource,
9439
+ isFreshOnline,
9440
+ isPidAlive,
9372
9441
  joiEdateType,
9373
9442
  joiStringArrayType,
9374
9443
  listAliveRunnerHeartbeats,