@nmakarov/cli-toolkit 0.50.0 → 0.53.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
@@ -5226,8 +5226,13 @@ function defineService(service) {
5226
5226
  const pm2 = {
5227
5227
  appName: service.name,
5228
5228
  args: "",
5229
+ stopAllowance: 60,
5229
5230
  ...service.pm2
5230
5231
  };
5232
+ if (pm2.killTimeout == null) {
5233
+ const stopSec = Number(pm2.stopAllowance);
5234
+ pm2.killTimeout = Number.isFinite(stopSec) && stopSec > 0 ? Math.floor(stopSec * 1e3) + 5e3 : 65e3;
5235
+ }
5231
5236
  const nginx = service.nginx ? { siteName: service.name, ...service.nginx } : null;
5232
5237
  return {
5233
5238
  repoDirName: deriveRepoDirName(service.repoUrl),
@@ -5308,6 +5313,30 @@ function run(cmd, args, options = {}) {
5308
5313
  function runShell(command, options = {}) {
5309
5314
  return run("bash", ["-lc", command], options);
5310
5315
  }
5316
+ function runCapture(cmd, args, options = {}) {
5317
+ const { cwd, env, logger, allowFail = false } = options;
5318
+ return new Promise((resolve3, reject) => {
5319
+ logger?.info?.(`$ ${cmd} ${args.join(" ")}${cwd ? ` (cwd=${cwd})` : ""}`);
5320
+ const child = spawn(cmd, args, {
5321
+ cwd,
5322
+ env: env ?? process.env,
5323
+ stdio: ["ignore", "pipe", "pipe"]
5324
+ });
5325
+ let stdout = "";
5326
+ let stderr = "";
5327
+ child.stdout?.on("data", (chunk) => {
5328
+ stdout += chunk.toString();
5329
+ });
5330
+ child.stderr?.on("data", (chunk) => {
5331
+ stderr += chunk.toString();
5332
+ });
5333
+ child.on("error", reject);
5334
+ child.on("close", (code) => {
5335
+ if (code === 0 || allowFail) resolve3({ stdout, stderr, code });
5336
+ else reject(new Error(`${cmd} exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
5337
+ });
5338
+ });
5339
+ }
5311
5340
 
5312
5341
  // src/deploy/log.js
5313
5342
  import { appendFile, mkdir } from "fs/promises";
@@ -5525,7 +5554,7 @@ async function runReleaseTests(service, releasePath, paths, options = {}) {
5525
5554
  // src/deploy/prune.js
5526
5555
  import { rm as rm2, readlink as readlink2 } from "fs/promises";
5527
5556
  async function pruneReleases(service, paths, options = {}) {
5528
- const { dryRun = false, logger = console } = options;
5557
+ const { dryRun = false, logger = console, protect = [] } = options;
5529
5558
  const keep = service.keepReleases ?? 3;
5530
5559
  const releases = await listReleases(paths);
5531
5560
  let activeName = null;
@@ -5539,6 +5568,9 @@ async function pruneReleases(service, paths, options = {}) {
5539
5568
  if (keepSet.size < keep) keepSet.add(rel.name);
5540
5569
  }
5541
5570
  if (activeName) keepSet.add(activeName);
5571
+ for (const name of protect) {
5572
+ if (name) keepSet.add(name);
5573
+ }
5542
5574
  const toRemove = releases.filter((rel) => !keepSet.has(rel.name));
5543
5575
  for (const rel of toRemove) {
5544
5576
  if (dryRun) {
@@ -5552,14 +5584,101 @@ async function pruneReleases(service, paths, options = {}) {
5552
5584
  }
5553
5585
 
5554
5586
  // src/deploy/pm2.js
5587
+ function sleep(ms) {
5588
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
5589
+ }
5590
+ async function getPm2Process(appName, options = {}) {
5591
+ const { logger } = options;
5592
+ const { stdout } = await runCapture("bash", ["-lc", "pm2 jlist"], { logger });
5593
+ let list;
5594
+ try {
5595
+ list = JSON.parse(stdout || "[]");
5596
+ } catch (err) {
5597
+ throw new Error(`pm2 jlist: invalid JSON (${err?.message ?? err})`);
5598
+ }
5599
+ if (!Array.isArray(list)) return null;
5600
+ return list.find((p) => p?.name === appName) ?? null;
5601
+ }
5602
+ async function waitPm2(appName, predicate, options = {}) {
5603
+ const {
5604
+ timeoutMs = 65e3,
5605
+ pollMs = 500,
5606
+ dryRun = false,
5607
+ logger = console,
5608
+ label = "condition"
5609
+ } = options;
5610
+ if (dryRun) {
5611
+ logger.info?.(`[dryRun] would wait pm2 ${appName} for ${label}`);
5612
+ return null;
5613
+ }
5614
+ const deadline = Date.now() + timeoutMs;
5615
+ let lastStatus = "(unknown)";
5616
+ while (Date.now() < deadline) {
5617
+ const proc = await getPm2Process(appName, { logger });
5618
+ lastStatus = proc?.pm2_env?.status ?? "(missing)";
5619
+ if (predicate(proc)) return proc;
5620
+ await sleep(pollMs);
5621
+ }
5622
+ throw new Error(
5623
+ `pm2 wait timed out after ${timeoutMs}ms for ${appName} (${label}); last status=${lastStatus}`
5624
+ );
5625
+ }
5555
5626
  async function reloadPm2(paths, options = {}) {
5556
- const { dryRun = false, logger = console } = options;
5627
+ const { dryRun = false, logger = console, appName = null, waitTimeoutMs = 65e3 } = options;
5557
5628
  if (dryRun) {
5558
5629
  logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
5559
5630
  return;
5560
5631
  }
5561
5632
  await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
5562
5633
  logger.info("pm2 reloaded");
5634
+ if (appName) {
5635
+ await waitPm2(
5636
+ appName,
5637
+ (proc) => proc?.pm2_env?.status === "online",
5638
+ { timeoutMs: waitTimeoutMs, logger, label: "online after reload" }
5639
+ );
5640
+ logger.info(`pm2 ${appName} online`);
5641
+ }
5642
+ }
5643
+ async function stopPm2(appName, options = {}) {
5644
+ const { dryRun = false, logger = console, waitTimeoutMs = 65e3 } = options;
5645
+ if (dryRun) {
5646
+ logger.info(`[dryRun] would pm2 stop ${appName}`);
5647
+ return;
5648
+ }
5649
+ const before = await getPm2Process(appName, { logger });
5650
+ if (!before) {
5651
+ logger.info(`pm2 ${appName}: not present (already stopped)`);
5652
+ return;
5653
+ }
5654
+ if (before.pm2_env?.status === "stopped") {
5655
+ logger.info(`pm2 ${appName}: already stopped`);
5656
+ return;
5657
+ }
5658
+ await runShell(`pm2 stop "${appName}"`, { logger });
5659
+ await waitPm2(
5660
+ appName,
5661
+ (proc) => !proc || proc.pm2_env?.status === "stopped",
5662
+ { timeoutMs: waitTimeoutMs, logger, label: "stopped" }
5663
+ );
5664
+ logger.info(`pm2 ${appName} stopped`);
5665
+ }
5666
+ async function startPm2(paths, options = {}) {
5667
+ const { dryRun = false, logger = console, appName = null, waitTimeoutMs = 65e3 } = options;
5668
+ if (dryRun) {
5669
+ logger.info(`[dryRun] would pm2 start ${paths.ecosystem} --update-env`);
5670
+ return;
5671
+ }
5672
+ await runShell(`pm2 start "${paths.ecosystem}" --update-env`, { logger });
5673
+ logger.info("pm2 started");
5674
+ if (appName) {
5675
+ await waitPm2(
5676
+ appName,
5677
+ (proc) => proc?.pm2_env?.status === "online",
5678
+ { timeoutMs: waitTimeoutMs, logger, label: "online after start" }
5679
+ );
5680
+ logger.info(`pm2 ${appName} online`);
5681
+ }
5563
5682
  }
5564
5683
 
5565
5684
  // src/deploy/nginx.js
@@ -5788,10 +5907,26 @@ async function requireDeployRoot(parentDir, serviceRoot) {
5788
5907
  This is meant to run on the target host (where ${parentDir} exists). For a local dry run use --appsRoot=/tmp/<service>.`
5789
5908
  );
5790
5909
  }
5910
+ function resolveKillTimeoutMs(pm2) {
5911
+ const explicit = Number(pm2?.killTimeout);
5912
+ if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
5913
+ const stopSec = Number(pm2?.stopAllowance);
5914
+ if (Number.isFinite(stopSec) && stopSec > 0) return Math.floor(stopSec * 1e3) + 5e3;
5915
+ return 65e3;
5916
+ }
5917
+ function resolvePm2Args(pm2) {
5918
+ const base = String(pm2?.args ?? "").trim();
5919
+ const stopSec = Number(pm2?.stopAllowance);
5920
+ if (!Number.isFinite(stopSec) || stopSec <= 0) return base;
5921
+ if (/(?:^|\s)--stopAllowance=/.test(base)) return base;
5922
+ return `${base}${base ? " " : ""}--stopAllowance=${Math.floor(stopSec)}`.trim();
5923
+ }
5791
5924
  function buildEcosystemConfig(service, paths) {
5792
5925
  const { pm2 } = service;
5793
5926
  const outLog = join9(paths.logs, `${pm2.appName}.out.log`);
5794
5927
  const errLog = join9(paths.logs, `${pm2.appName}.err.log`);
5928
+ const killTimeoutMs = resolveKillTimeoutMs(pm2);
5929
+ const args = resolvePm2Args(pm2);
5795
5930
  return `/**
5796
5931
  * pm2 ecosystem for ${service.name} \u2014 written by cli-toolkit deploy from the
5797
5932
  * service manifest (init/deploy). Do not hand-edit; change pm2.* in the
@@ -5803,7 +5938,7 @@ module.exports = {
5803
5938
  name: "${pm2.appName}",
5804
5939
  script: "${pm2.script}",
5805
5940
  cwd: "${paths.current}",
5806
- args: "${pm2.args ?? ""}",
5941
+ args: ${JSON.stringify(args)},
5807
5942
  instances: 1,
5808
5943
  exec_mode: "fork",
5809
5944
  autorestart: true,
@@ -5811,6 +5946,8 @@ module.exports = {
5811
5946
  max_restarts: 10,
5812
5947
  restart_delay: 2000,
5813
5948
  max_memory_restart: "1500M",
5949
+ // Grace window after SIGINT/SIGTERM before SIGKILL (ms). Align with --stopAllowance.
5950
+ kill_timeout: ${killTimeoutMs},
5814
5951
  out_file: "${outLog}",
5815
5952
  error_file: "${errLog}",
5816
5953
  merge_logs: true,
@@ -6011,15 +6148,27 @@ async function bootstrapHost(service, options = {}) {
6011
6148
  }
6012
6149
 
6013
6150
  // src/deploy/deploy-service.js
6151
+ import { readlink as readlink3 } from "fs/promises";
6152
+ function resolveKillTimeoutMs2(service) {
6153
+ const explicit = Number(service.pm2?.killTimeout);
6154
+ if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
6155
+ const stopSec = Number(service.pm2?.stopAllowance);
6156
+ if (Number.isFinite(stopSec) && stopSec > 0) return Math.floor(stopSec * 1e3) + 5e3;
6157
+ return 65e3;
6158
+ }
6014
6159
  async function deployService(service, options = {}) {
6015
6160
  const {
6016
6161
  dryRun = false,
6017
6162
  skipPull = false,
6018
6163
  skipTests = false,
6019
6164
  skipNginx = false,
6165
+ stopFirst = false,
6020
6166
  logger = console
6021
6167
  } = options;
6022
6168
  const paths = servicePaths(service);
6169
+ const appName = service.pm2.appName;
6170
+ const killTimeoutMs = resolveKillTimeoutMs2(service);
6171
+ const waitTimeoutMs = killTimeoutMs + 1e4;
6023
6172
  await initServiceStructure(service, { dryRun, logger });
6024
6173
  if (!skipPull) await pullRepo(service, { dryRun, logger });
6025
6174
  await syncEnv(service, { dryRun, logger });
@@ -6027,14 +6176,31 @@ async function deployService(service, options = {}) {
6027
6176
  await writeReleaseBuildInfo(service, releasePath, { stamp, dryRun, logger });
6028
6177
  await installDeps(service, releasePath, paths, { dryRun, logger });
6029
6178
  if (!skipTests) await runReleaseTests(service, releasePath, paths, { dryRun, logger });
6030
- await activateRelease(releasePath, paths, { dryRun, logger });
6031
- await pruneReleases(service, paths, { dryRun, logger });
6032
- await reloadPm2(paths, { dryRun, logger });
6179
+ let previousReleaseName = null;
6180
+ try {
6181
+ const prev = await readlink3(paths.current);
6182
+ previousReleaseName = prev.split("/").pop() || null;
6183
+ } catch {
6184
+ }
6185
+ if (stopFirst) {
6186
+ logger.info(`deploy mode=stopFirst app=${appName} killTimeoutMs=${killTimeoutMs}`);
6187
+ await stopPm2(appName, { dryRun, logger, waitTimeoutMs });
6188
+ await activateRelease(releasePath, paths, { dryRun, logger });
6189
+ await pruneReleases(service, paths, { dryRun, logger });
6190
+ await startPm2(paths, { dryRun, logger, appName, waitTimeoutMs });
6191
+ } else {
6192
+ logger.info(
6193
+ `deploy mode=rolling app=${appName} killTimeoutMs=${killTimeoutMs}` + (previousReleaseName ? ` previous=${previousReleaseName}` : "")
6194
+ );
6195
+ await activateRelease(releasePath, paths, { dryRun, logger });
6196
+ await reloadPm2(paths, { dryRun, logger, appName, waitTimeoutMs });
6197
+ await pruneReleases(service, paths, { dryRun, logger });
6198
+ }
6033
6199
  if (!skipNginx) await enableNginxUpstream(service, { dryRun, logger });
6034
- const summary = `deploy complete stamp=${stamp} dryRun=${dryRun}`;
6200
+ const summary = `deploy complete stamp=${stamp} mode=${stopFirst ? "stopFirst" : "rolling"} dryRun=${dryRun}`;
6035
6201
  logger.info(summary);
6036
6202
  if (!dryRun) await appendDeployLog(paths.deployLog, summary);
6037
- return { stamp, releasePath };
6203
+ return { stamp, releasePath, mode: stopFirst ? "stopFirst" : "rolling" };
6038
6204
  }
6039
6205
 
6040
6206
  // src/deploy/provision-service.js
@@ -6075,7 +6241,7 @@ async function provisionService(service, options = {}) {
6075
6241
  }
6076
6242
 
6077
6243
  // src/deploy/rollback-service.js
6078
- import { readlink as readlink3 } from "fs/promises";
6244
+ import { readlink as readlink4 } from "fs/promises";
6079
6245
  async function rollbackService(service, options = {}) {
6080
6246
  const { release: targetName, dryRun = false, logger = console } = options;
6081
6247
  const paths = servicePaths(service);
@@ -6083,7 +6249,7 @@ async function rollbackService(service, options = {}) {
6083
6249
  if (releases.length === 0) throw new Error("No releases to roll back to");
6084
6250
  let activeName = null;
6085
6251
  try {
6086
- const target = await readlink3(paths.current);
6252
+ const target = await readlink4(paths.current);
6087
6253
  activeName = target.split("/").pop();
6088
6254
  } catch {
6089
6255
  throw new Error("No active release (current symlink missing)");
@@ -7706,7 +7872,7 @@ var TaskStopRunner = class extends AbstractTask {
7706
7872
  */
7707
7873
  static async resolveCustomParams(context, overrides = {}) {
7708
7874
  const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
7709
- allowanceMs: "number default 5000"
7875
+ allowanceMs: "number default 60000"
7710
7876
  }, overrides);
7711
7877
  const allowanceMs = Number(merged.allowanceMs);
7712
7878
  if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
@@ -7720,7 +7886,7 @@ var TaskStopRunner = class extends AbstractTask {
7720
7886
  * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
7721
7887
  */
7722
7888
  async run() {
7723
- const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
7889
+ const allowanceMs = Number(this.task?.params?.allowanceMs ?? 6e4);
7724
7890
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
7725
7891
  return {
7726
7892
  success: true,
@@ -8405,7 +8571,7 @@ function normalizeRegistry(registry) {
8405
8571
  if (registry instanceof TasksRegistry) return registry;
8406
8572
  return new TasksRegistry().addMany(registry);
8407
8573
  }
8408
- async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
8574
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 6e4) {
8409
8575
  return enqueueTask(context, {
8410
8576
  queueName,
8411
8577
  name: "stopRunner",
@@ -8544,7 +8710,7 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
8544
8710
  await db(tasksTable).where({ id: row.id }).delete();
8545
8711
  }
8546
8712
  const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
8547
- const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
8713
+ const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 6e4) : 0;
8548
8714
  return { stopRunnerRequested, stopAllowanceMs };
8549
8715
  }
8550
8716
  function shuffleTaskRowsInPlace(rows) {
@@ -8634,7 +8800,7 @@ async function runTasksLoop(context, options) {
8634
8800
  const runningTaskInstances = /* @__PURE__ */ new Map();
8635
8801
  let runningControlPromise = null;
8636
8802
  let stopRequested = false;
8637
- let stopAllowanceMs = 5e3;
8803
+ let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;
8638
8804
  context.tasksRunnerStop = false;
8639
8805
  let registryReg = null;
8640
8806
  let registryInterval = null;
@@ -8701,7 +8867,7 @@ async function runTasksLoop(context, options) {
8701
8867
  ).then(async (outcome) => {
8702
8868
  if (outcome.stopRunnerRequested && !stopRequested) {
8703
8869
  stopRequested = true;
8704
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
8870
+ stopAllowanceMs = outcome.stopAllowanceMs || stopAllowanceMs;
8705
8871
  context.tasksRunnerStop = true;
8706
8872
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
8707
8873
  }
@@ -8727,7 +8893,7 @@ async function runTasksLoop(context, options) {
8727
8893
  const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
8728
8894
  if (outcome.stopRunnerRequested && !stopRequested) {
8729
8895
  stopRequested = true;
8730
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
8896
+ stopAllowanceMs = outcome.stopAllowanceMs || stopAllowanceMs;
8731
8897
  context.tasksRunnerStop = true;
8732
8898
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
8733
8899
  }
@@ -8748,7 +8914,9 @@ async function runTasksLoop(context, options) {
8748
8914
  }
8749
8915
  }
8750
8916
  if (context.isStop() && !stopRequested) {
8751
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
8917
+ stopRequested = true;
8918
+ stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : stopAllowanceMs;
8919
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
8752
8920
  }
8753
8921
  if (runningPromises.size > 0) {
8754
8922
  if (stopRequested) {
@@ -9032,6 +9200,7 @@ export {
9032
9200
  ensureTasksRuntime,
9033
9201
  flushTaskIpcLogs,
9034
9202
  getArgsInstance,
9203
+ getPm2Process,
9035
9204
  createElement2 as h,
9036
9205
  initServiceStructure,
9037
9206
  installDeps,
@@ -9078,6 +9247,7 @@ export {
9078
9247
  resolveSteps,
9079
9248
  rollbackService,
9080
9249
  run,
9250
+ runCapture,
9081
9251
  runNodeTaskScript,
9082
9252
  runReleaseTests,
9083
9253
  runRemoteCli,
@@ -9095,6 +9265,8 @@ export {
9095
9265
  showScreen,
9096
9266
  showWordGridScreen,
9097
9267
  sshRun,
9268
+ startPm2,
9269
+ stopPm2,
9098
9270
  syncEnv,
9099
9271
  taskHistoryInsertFromQueueRow,
9100
9272
  timeMatcher,
@@ -9112,6 +9284,7 @@ export {
9112
9284
  useRef2 as useRef,
9113
9285
  useState3 as useState,
9114
9286
  waitForTaskResult,
9287
+ waitPm2,
9115
9288
  writeReleaseBuildInfo
9116
9289
  };
9117
9290
  //# sourceMappingURL=index.js.map