@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/deploy.cjs CHANGED
@@ -38,6 +38,8 @@ __export(deploy_exports, {
38
38
  initServiceStructure: () => initServiceStructure,
39
39
  installDeps: () => installDeps,
40
40
  installOperatorShell: () => installOperatorShell,
41
+ isFreshOnline: () => isFreshOnline,
42
+ isPidAlive: () => isPidAlive,
41
43
  listReleases: () => listReleases,
42
44
  loadServices: () => loadServices,
43
45
  npmEnv: () => npmEnv,
@@ -129,6 +131,8 @@ function servicePaths(service) {
129
131
  current: (0, import_node_path.join)(root, "current"),
130
132
  sharedEnv: (0, import_node_path.join)(root, "shared", ".env"),
131
133
  ecosystem: (0, import_node_path.join)(root, "shared", "ecosystem.config.cjs"),
134
+ /** Preloaded by pm2 `node_args --require` so ENV survives flaky restarts. */
135
+ ensureEnv: (0, import_node_path.join)(root, "shared", "ensure-env.cjs"),
132
136
  deployLog: (0, import_node_path.join)(root, "logs", "deploy.log"),
133
137
  lockHashFile: (0, import_node_path.join)(root, "shared", ".package-lock.sha256")
134
138
  };
@@ -447,6 +451,16 @@ async function pruneReleases(service, paths, options = {}) {
447
451
  function sleep(ms) {
448
452
  return new Promise((resolve2) => setTimeout(resolve2, ms));
449
453
  }
454
+ function isPidAlive(pid) {
455
+ const n = Number(pid);
456
+ if (!Number.isFinite(n) || n <= 0) return false;
457
+ try {
458
+ process.kill(n, 0);
459
+ return true;
460
+ } catch {
461
+ return false;
462
+ }
463
+ }
450
464
  async function getPm2Process(appName, options = {}) {
451
465
  const { logger } = options;
452
466
  const { stdout } = await runCapture("bash", ["-lc", "pm2 jlist"], { logger });
@@ -483,21 +497,44 @@ async function waitPm2(appName, predicate, options = {}) {
483
497
  `pm2 wait timed out after ${timeoutMs}ms for ${appName} (${label}); last status=${lastStatus}`
484
498
  );
485
499
  }
500
+ function isFreshOnline(proc, oldPid) {
501
+ if (proc?.pm2_env?.status !== "online") return false;
502
+ const pid = Number(proc.pid);
503
+ if (!Number.isFinite(pid) || pid <= 0) return false;
504
+ if (oldPid != null) {
505
+ if (pid === oldPid) return false;
506
+ if (isPidAlive(oldPid)) return false;
507
+ }
508
+ return true;
509
+ }
486
510
  async function reloadPm2(paths, options = {}) {
487
511
  const { dryRun = false, logger = console, appName = null, waitTimeoutMs = 65e3 } = options;
488
512
  if (dryRun) {
489
513
  logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
490
514
  return;
491
515
  }
516
+ let oldPid = null;
517
+ if (appName) {
518
+ const before = await getPm2Process(appName, { logger });
519
+ const n = Number(before?.pid);
520
+ if (Number.isFinite(n) && n > 0 && before?.pm2_env?.status === "online") {
521
+ oldPid = n;
522
+ logger.info(`pm2 ${appName}: reloading (old pid=${oldPid})`);
523
+ }
524
+ }
492
525
  await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
493
526
  logger.info("pm2 reloaded");
494
527
  if (appName) {
495
- await waitPm2(
528
+ const proc = await waitPm2(
496
529
  appName,
497
- (proc) => proc?.pm2_env?.status === "online",
498
- { timeoutMs: waitTimeoutMs, logger, label: "online after reload" }
530
+ (p) => isFreshOnline(p, oldPid),
531
+ {
532
+ timeoutMs: waitTimeoutMs,
533
+ logger,
534
+ label: oldPid ? `online on new pid (old ${oldPid} gone)` : "online after reload"
535
+ }
499
536
  );
500
- logger.info(`pm2 ${appName} online`);
537
+ logger.info(`pm2 ${appName} online pid=${proc?.pid ?? "?"}`);
501
538
  }
502
539
  }
503
540
  async function stopPm2(appName, options = {}) {
@@ -515,10 +552,15 @@ async function stopPm2(appName, options = {}) {
515
552
  logger.info(`pm2 ${appName}: already stopped`);
516
553
  return;
517
554
  }
555
+ const oldPid = Number(before.pid);
518
556
  await runShell(`pm2 stop "${appName}"`, { logger });
519
557
  await waitPm2(
520
558
  appName,
521
- (proc) => !proc || proc.pm2_env?.status === "stopped",
559
+ (proc) => {
560
+ if (proc && proc.pm2_env?.status !== "stopped") return false;
561
+ if (Number.isFinite(oldPid) && oldPid > 0 && isPidAlive(oldPid)) return false;
562
+ return true;
563
+ },
522
564
  { timeoutMs: waitTimeoutMs, logger, label: "stopped" }
523
565
  );
524
566
  logger.info(`pm2 ${appName} stopped`);
@@ -532,12 +574,12 @@ async function startPm2(paths, options = {}) {
532
574
  await runShell(`pm2 start "${paths.ecosystem}" --update-env`, { logger });
533
575
  logger.info("pm2 started");
534
576
  if (appName) {
535
- await waitPm2(
577
+ const proc = await waitPm2(
536
578
  appName,
537
- (proc) => proc?.pm2_env?.status === "online",
579
+ (p) => isFreshOnline(p, null),
538
580
  { timeoutMs: waitTimeoutMs, logger, label: "online after start" }
539
581
  );
540
- logger.info(`pm2 ${appName} online`);
582
+ logger.info(`pm2 ${appName} online pid=${proc?.pid ?? "?"}`);
541
583
  }
542
584
  }
543
585
 
@@ -781,12 +823,23 @@ function resolvePm2Args(pm2) {
781
823
  if (/(?:^|\s)--stopAllowance=/.test(base)) return base;
782
824
  return `${base}${base ? " " : ""}--stopAllowance=${Math.floor(stopSec)}`.trim();
783
825
  }
826
+ function buildEnsureEnvScript() {
827
+ return `/**
828
+ * Written by cli-toolkit deploy (init-structure). Do not hand-edit.
829
+ * Preloaded via pm2 node_args --require so Args sees ENV=production even when
830
+ * a post-SIGKILL restart briefly omits ecosystem env (otherwise Db \u2192 local:6032).
831
+ */
832
+ if (!process.env.ENV) process.env.ENV = "production";
833
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
834
+ `;
835
+ }
784
836
  function buildEcosystemConfig(service, paths) {
785
837
  const { pm2 } = service;
786
838
  const outLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.out.log`);
787
839
  const errLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.err.log`);
788
840
  const killTimeoutMs = resolveKillTimeoutMs(pm2);
789
841
  const args = resolvePm2Args(pm2);
842
+ const ensureEnv = paths.ensureEnv;
790
843
  return `/**
791
844
  * pm2 ecosystem for ${service.name} \u2014 written by cli-toolkit deploy from the
792
845
  * service manifest (init/deploy). Do not hand-edit; change pm2.* in the
@@ -799,6 +852,8 @@ module.exports = {
799
852
  script: "${pm2.script}",
800
853
  cwd: "${paths.current}",
801
854
  args: ${JSON.stringify(args)},
855
+ // Absolute --require so ENV is set before Args constructs (see shared/ensure-env.cjs).
856
+ node_args: ${JSON.stringify(`--require ${ensureEnv}`)},
802
857
  instances: 1,
803
858
  exec_mode: "fork",
804
859
  autorestart: true,
@@ -840,9 +895,17 @@ async function initServiceStructure(service, options = {}) {
840
895
  created.push(dir);
841
896
  }
842
897
  const ecosystemExisted = await pathExists4(paths.ecosystem);
898
+ const ensureEnvExisted = await pathExists4(paths.ensureEnv);
843
899
  if (!dryRun) {
900
+ await (0, import_promises11.writeFile)(paths.ensureEnv, buildEnsureEnvScript(), { mode: 420 });
844
901
  await (0, import_promises11.writeFile)(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
845
902
  }
903
+ if (ensureEnvExisted) {
904
+ logger.info(`synced ${paths.ensureEnv}`);
905
+ } else {
906
+ created.push(paths.ensureEnv);
907
+ logger.info(`seeded ${paths.ensureEnv}`);
908
+ }
846
909
  if (ecosystemExisted) {
847
910
  logger.info(`synced ${paths.ecosystem} from manifest`);
848
911
  } else {
@@ -1352,6 +1415,8 @@ function resolveServiceFrom(serviceMap, name, { appsRoot } = {}) {
1352
1415
  initServiceStructure,
1353
1416
  installDeps,
1354
1417
  installOperatorShell,
1418
+ isFreshOnline,
1419
+ isPidAlive,
1355
1420
  listReleases,
1356
1421
  loadServices,
1357
1422
  npmEnv,