@nmakarov/cli-toolkit 0.72.0 → 0.75.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.
@@ -2810,6 +2810,7 @@ function setup(opts = {}) {
2810
2810
  const partialContext = {
2811
2811
  emitter: new import_events.EventEmitter(),
2812
2812
  isStop: () => false,
2813
+ isKill: () => false,
2813
2814
  cleanupFunctions: [],
2814
2815
  registerCleanup: (fn) => {
2815
2816
  partialContext.cleanupFunctions.push(fn);
@@ -2831,6 +2832,7 @@ function setup(opts = {}) {
2831
2832
  logger,
2832
2833
  emitter: partialContext.emitter,
2833
2834
  isStop: partialContext.isStop,
2835
+ isKill: partialContext.isKill,
2834
2836
  cleanupFunctions: partialContext.cleanupFunctions,
2835
2837
  registerCleanup: partialContext.registerCleanup,
2836
2838
  // For long-running scripts (servers): with --showUsedParams=top, print
@@ -2880,6 +2882,7 @@ function printAllParameters(context) {
2880
2882
  }
2881
2883
  async function init(flow2, opts = {}) {
2882
2884
  let stop = false;
2885
+ let kill = false;
2883
2886
  let context = null;
2884
2887
  let cleanupRan = false;
2885
2888
  const runRegisteredCleanups = async (ctx) => {
@@ -2909,6 +2912,7 @@ async function init(flow2, opts = {}) {
2909
2912
  }
2910
2913
  context = setup(opts);
2911
2914
  context.isStop = () => stop;
2915
+ context.isKill = () => kill;
2912
2916
  context = await setupModules(context, opts);
2913
2917
  const stopAfter = context.args.get("stopAfter");
2914
2918
  const stopAllowanceSec = Number(context.params.get("stopAllowance", "number default 60"));
@@ -2947,6 +2951,14 @@ async function init(flow2, opts = {}) {
2947
2951
  );
2948
2952
  context.emitter.emit("stop", stopAllowanceMs);
2949
2953
  });
2954
+ process.on("SIGUSR2", () => {
2955
+ if (!context || kill) return;
2956
+ kill = true;
2957
+ stop = true;
2958
+ context.logger.warn(">> SIGUSR2: emitting kill (urgent stop)");
2959
+ context.emitter.emit("kill");
2960
+ context.emitter.emit("stop", stopAllowanceMs);
2961
+ });
2950
2962
  await flow2(context);
2951
2963
  } catch (error) {
2952
2964
  const errorLocation = error instanceof Error && error.stack ? error.stack.split("\n")[1]?.trim() || "Unknown location" : "Unknown location";
@@ -2973,7 +2985,7 @@ async function init(flow2, opts = {}) {
2973
2985
  }
2974
2986
  if (process.exitCode && process.exitCode !== 0) {
2975
2987
  process.exit(process.exitCode);
2976
- } else if (stop) {
2988
+ } else if (stop || kill) {
2977
2989
  process.exit(0);
2978
2990
  }
2979
2991
  }
@@ -5871,6 +5883,8 @@ var AbstractTask = class _AbstractTask {
5871
5883
  constructor(context, task) {
5872
5884
  this.context = context;
5873
5885
  this.task = task;
5886
+ this._stopRequested = false;
5887
+ this._pauseRequested = false;
5874
5888
  }
5875
5889
  /**
5876
5890
  * Return a short reason string when the task should be deferred (e.g. "locked
@@ -5888,6 +5902,26 @@ var AbstractTask = class _AbstractTask {
5888
5902
  * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
5889
5903
  */
5890
5904
  requestStop(_allowanceMs) {
5905
+ this._stopRequested = true;
5906
+ }
5907
+ /**
5908
+ * Cooperative pause signal (from `pauseTask` control-lane task). Long-running
5909
+ * tasks should finish the current unit of work, persist a checkpoint, and
5910
+ * return `{ success: true, results: { taskPaused: true, … } }` so the runner
5911
+ * keeps the row as `status=paused` instead of deleting it.
5912
+ *
5913
+ * @param {number} [_allowanceMs]
5914
+ */
5915
+ requestPause(_allowanceMs) {
5916
+ this._pauseRequested = true;
5917
+ }
5918
+ /** @returns {boolean} */
5919
+ isStopRequested() {
5920
+ return this._stopRequested === true;
5921
+ }
5922
+ /** @returns {boolean} */
5923
+ isPauseRequested() {
5924
+ return this._pauseRequested === true;
5891
5925
  }
5892
5926
  /**
5893
5927
  * Perform the task. Must be implemented by subclasses.
@@ -6586,6 +6620,8 @@ var CONTROL_LANE_TASK_NAMES = [
6586
6620
  "pause",
6587
6621
  "unpauseRunner",
6588
6622
  "unpause",
6623
+ "pauseTask",
6624
+ "resumeTask",
6589
6625
  "setRuntimeParam",
6590
6626
  "setRunnerParam"
6591
6627
  ];
@@ -6790,6 +6826,190 @@ var TaskUnpauseRunner = class extends AbstractTask {
6790
6826
  }
6791
6827
  };
6792
6828
 
6829
+ // src/tasks/coreTasks/TaskPauseTask.js
6830
+ function parseProgressObject(progress) {
6831
+ if (progress == null || progress === "") return {};
6832
+ if (typeof progress === "object" && !Array.isArray(progress)) {
6833
+ return { ...progress };
6834
+ }
6835
+ if (typeof progress === "string") {
6836
+ const t = progress.trim();
6837
+ if (t.startsWith("{") || t.startsWith("[")) {
6838
+ try {
6839
+ const parsed = JSON.parse(t);
6840
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
6841
+ } catch {
6842
+ }
6843
+ }
6844
+ return { message: progress };
6845
+ }
6846
+ return { message: String(progress) };
6847
+ }
6848
+ async function applyPauseTask(context, taskId) {
6849
+ const db = context.db;
6850
+ if (!db) throw new Error("pauseTask: context.db is required");
6851
+ const id = String(taskId ?? "").trim();
6852
+ if (!id) throw new ParamError('pauseTask: param "taskId" is required');
6853
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
6854
+ const row = await db(tasksTable).where({ id }).first();
6855
+ if (!row) {
6856
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
6857
+ }
6858
+ if (row.status === "paused") {
6859
+ return {
6860
+ success: true,
6861
+ results: { taskId: id, status: "paused", message: "Task already paused" }
6862
+ };
6863
+ }
6864
+ if (row.status === "idle") {
6865
+ const progress = parseProgressObject(row.progress);
6866
+ progress.pauseRequested = true;
6867
+ progress.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
6868
+ await db(tasksTable).where({ id }).update({
6869
+ status: "paused",
6870
+ status_changed_at: db.fn.now(),
6871
+ progress: toJsonColumn(progress)
6872
+ });
6873
+ context.logger?.warn?.(`[pauseTask] idle task ${id} (${row.name}) \u2192 paused`);
6874
+ return {
6875
+ success: true,
6876
+ results: { taskId: id, status: "paused", message: "Idle task paused" }
6877
+ };
6878
+ }
6879
+ if (row.status === "running") {
6880
+ const progress = parseProgressObject(row.progress);
6881
+ progress.pauseRequested = true;
6882
+ progress.pauseRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
6883
+ await db(tasksTable).where({ id }).update({
6884
+ progress: toJsonColumn(progress)
6885
+ });
6886
+ const inst = context.runningTaskInstances?.get?.(id);
6887
+ if (inst && typeof inst.requestPause === "function") {
6888
+ await inst.requestPause();
6889
+ context.logger?.warn?.(
6890
+ `[pauseTask] signaled running task ${id} (${row.name}) to pause after current unit`
6891
+ );
6892
+ return {
6893
+ success: true,
6894
+ results: {
6895
+ taskId: id,
6896
+ status: "running",
6897
+ signaled: true,
6898
+ message: "Pause requested; task will finish current unit then park as paused"
6899
+ }
6900
+ };
6901
+ }
6902
+ context.logger?.warn?.(
6903
+ `[pauseTask] stamped pauseRequested on ${id} (${row.name}) \u2014 not running in this process`
6904
+ );
6905
+ return {
6906
+ success: true,
6907
+ results: {
6908
+ taskId: id,
6909
+ status: "running",
6910
+ signaled: false,
6911
+ message: "pauseRequested stamped on task row; target runner must observe it (enqueue pauseTask on that service)"
6912
+ }
6913
+ };
6914
+ }
6915
+ return {
6916
+ success: false,
6917
+ results: {
6918
+ error: `Cannot pause task in status "${row.status}"`,
6919
+ taskId: id,
6920
+ status: row.status
6921
+ }
6922
+ };
6923
+ }
6924
+ async function applyResumeTask(context, taskId) {
6925
+ const db = context.db;
6926
+ if (!db) throw new Error("resumeTask: context.db is required");
6927
+ const id = String(taskId ?? "").trim();
6928
+ if (!id) throw new ParamError('resumeTask: param "taskId" is required');
6929
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
6930
+ const row = await db(tasksTable).where({ id }).first();
6931
+ if (!row) {
6932
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
6933
+ }
6934
+ if (row.status === "idle") {
6935
+ return {
6936
+ success: true,
6937
+ results: { taskId: id, status: "idle", message: "Task already idle (runnable)" }
6938
+ };
6939
+ }
6940
+ if (row.status !== "paused") {
6941
+ return {
6942
+ success: false,
6943
+ results: {
6944
+ error: `Cannot resume task in status "${row.status}" (expected paused)`,
6945
+ taskId: id,
6946
+ status: row.status
6947
+ }
6948
+ };
6949
+ }
6950
+ const progress = parseProgressObject(row.progress);
6951
+ delete progress.pauseRequested;
6952
+ delete progress.pauseRequestedAt;
6953
+ delete progress.pausedAt;
6954
+ progress.resumedAt = (/* @__PURE__ */ new Date()).toISOString();
6955
+ await db(tasksTable).where({ id }).update({
6956
+ status: "idle",
6957
+ status_changed_at: db.fn.now(),
6958
+ started_at: null,
6959
+ completed_at: null,
6960
+ success: null,
6961
+ progress: toJsonColumn(progress),
6962
+ // Clear claim binding so any matching worker can pick it up.
6963
+ service_name: null,
6964
+ server_name: null,
6965
+ instance_number: null,
6966
+ next_run_at: null
6967
+ });
6968
+ context.logger?.warn?.(`[resumeTask] paused task ${id} (${row.name}) \u2192 idle`);
6969
+ return {
6970
+ success: true,
6971
+ results: { taskId: id, status: "idle", message: "Task resumed (idle; will be claimed)" }
6972
+ };
6973
+ }
6974
+ var TaskPauseTask = class extends AbstractTask {
6975
+ static taskName = "pauseTask";
6976
+ static description = "Pause a specific task: finish current unit, persist status=paused";
6977
+ static defaultWaitForResult = true;
6978
+ static async resolveCustomParams(context, overrides = {}) {
6979
+ const merged = AbstractTask._mergeTypedParams(
6980
+ context,
6981
+ "task-pause-task",
6982
+ { taskId: "string" },
6983
+ overrides
6984
+ );
6985
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
6986
+ if (!taskId) throw new ParamError('pauseTask: param "taskId" is required');
6987
+ return { taskId };
6988
+ }
6989
+ async run() {
6990
+ return applyPauseTask(this.context, this.task?.params?.taskId);
6991
+ }
6992
+ };
6993
+ var TaskResumeTask = class extends AbstractTask {
6994
+ static taskName = "resumeTask";
6995
+ static description = "Resume a paused task (status paused \u2192 idle)";
6996
+ static defaultWaitForResult = true;
6997
+ static async resolveCustomParams(context, overrides = {}) {
6998
+ const merged = AbstractTask._mergeTypedParams(
6999
+ context,
7000
+ "task-resume-task",
7001
+ { taskId: "string" },
7002
+ overrides
7003
+ );
7004
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
7005
+ if (!taskId) throw new ParamError('resumeTask: param "taskId" is required');
7006
+ return { taskId };
7007
+ }
7008
+ async run() {
7009
+ return applyResumeTask(this.context, this.task?.params?.taskId);
7010
+ }
7011
+ };
7012
+
6793
7013
  // src/tasks/coreTasks/TaskGetLogs.js
6794
7014
  var TaskGetLogs = class extends AbstractTask {
6795
7015
  /**
@@ -7008,7 +7228,7 @@ var TasksRegistry = class _TasksRegistry {
7008
7228
  * @returns {TasksRegistry}
7009
7229
  */
7010
7230
  static withCoreTasks() {
7011
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("pauseRunner", TaskPauseRunner).add("pause", TaskPauseRunner).add("unpauseRunner", TaskUnpauseRunner).add("unpause", TaskUnpauseRunner).add("getLogs", TaskGetLogs).add("setRuntimeParam", TaskSetRuntimeParam).add("setRunnerParam", TaskSetRuntimeParam);
7231
+ return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("pauseRunner", TaskPauseRunner).add("pause", TaskPauseRunner).add("unpauseRunner", TaskUnpauseRunner).add("unpause", TaskUnpauseRunner).add("pauseTask", TaskPauseTask).add("resumeTask", TaskResumeTask).add("getLogs", TaskGetLogs).add("setRuntimeParam", TaskSetRuntimeParam).add("setRunnerParam", TaskSetRuntimeParam);
7012
7232
  }
7013
7233
  /**
7014
7234
  * Register a single task class under a name. Overwrites any previous entry.
@@ -7200,11 +7420,12 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
7200
7420
  } finally {
7201
7421
  runningTaskInstances.delete(row.id);
7202
7422
  }
7423
+ const taskPaused = success && results && typeof results === "object" && results.taskPaused === true;
7203
7424
  await db(historyTable).insert(
7204
7425
  taskHistoryInsertFromQueueRow(row, {
7205
7426
  completed_at: /* @__PURE__ */ new Date(),
7206
7427
  success,
7207
- status: success ? "completed" : "failed",
7428
+ status: taskPaused ? "paused" : success ? "completed" : "failed",
7208
7429
  status_changed_at: db.fn.now(),
7209
7430
  params: toJsonColumn(row.params),
7210
7431
  results: toJsonColumn(results)
@@ -7227,7 +7448,24 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
7227
7448
  details: results
7228
7449
  });
7229
7450
  }
7230
- if (row.schedule) {
7451
+ if (taskPaused) {
7452
+ const checkpointParams = results.checkpointParams && typeof results.checkpointParams === "object" ? results.checkpointParams : row.params;
7453
+ const progressPayload = results.progress != null ? results.progress : { paused: true, pausedAt: (/* @__PURE__ */ new Date()).toISOString(), message: "paused" };
7454
+ await db(tasksTable).where({ id: row.id }).update({
7455
+ started_at: null,
7456
+ completed_at: /* @__PURE__ */ new Date(),
7457
+ success: true,
7458
+ results: toJsonColumn(results),
7459
+ params: toJsonColumn(checkpointParams),
7460
+ progress: toJsonColumn(progressPayload),
7461
+ status: "paused",
7462
+ status_changed_at: db.fn.now(),
7463
+ past_due: null,
7464
+ service_name: null,
7465
+ server_name: null,
7466
+ instance_number: null
7467
+ });
7468
+ } else if (row.schedule) {
7231
7469
  if (success) {
7232
7470
  let nextRunAt = null;
7233
7471
  try {
@@ -7355,6 +7593,7 @@ async function runTasksLoop(context, options) {
7355
7593
  }
7356
7594
  const runningPromises = /* @__PURE__ */ new Set();
7357
7595
  const runningTaskInstances = /* @__PURE__ */ new Map();
7596
+ context.runningTaskInstances = runningTaskInstances;
7358
7597
  let runningControlPromise = null;
7359
7598
  let stopRequested = false;
7360
7599
  let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;