@nmakarov/cli-toolkit 0.72.0 → 0.73.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.
@@ -5855,6 +5855,8 @@ var AbstractTask = class _AbstractTask {
5855
5855
  constructor(context, task) {
5856
5856
  this.context = context;
5857
5857
  this.task = task;
5858
+ this._stopRequested = false;
5859
+ this._pauseRequested = false;
5858
5860
  }
5859
5861
  /**
5860
5862
  * Return a short reason string when the task should be deferred (e.g. "locked
@@ -5872,6 +5874,26 @@ var AbstractTask = class _AbstractTask {
5872
5874
  * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
5873
5875
  */
5874
5876
  requestStop(_allowanceMs) {
5877
+ this._stopRequested = true;
5878
+ }
5879
+ /**
5880
+ * Cooperative pause signal (from `pauseTask` control-lane task). Long-running
5881
+ * tasks should finish the current unit of work, persist a checkpoint, and
5882
+ * return `{ success: true, results: { taskPaused: true, … } }` so the runner
5883
+ * keeps the row as `status=paused` instead of deleting it.
5884
+ *
5885
+ * @param {number} [_allowanceMs]
5886
+ */
5887
+ requestPause(_allowanceMs) {
5888
+ this._pauseRequested = true;
5889
+ }
5890
+ /** @returns {boolean} */
5891
+ isStopRequested() {
5892
+ return this._stopRequested === true;
5893
+ }
5894
+ /** @returns {boolean} */
5895
+ isPauseRequested() {
5896
+ return this._pauseRequested === true;
5875
5897
  }
5876
5898
  /**
5877
5899
  * Perform the task. Must be implemented by subclasses.
@@ -6570,6 +6592,8 @@ var CONTROL_LANE_TASK_NAMES = [
6570
6592
  "pause",
6571
6593
  "unpauseRunner",
6572
6594
  "unpause",
6595
+ "pauseTask",
6596
+ "resumeTask",
6573
6597
  "setRuntimeParam",
6574
6598
  "setRunnerParam"
6575
6599
  ];
@@ -6774,6 +6798,190 @@ var TaskUnpauseRunner = class extends AbstractTask {
6774
6798
  }
6775
6799
  };
6776
6800
 
6801
+ // src/tasks/coreTasks/TaskPauseTask.js
6802
+ function parseProgressObject(progress) {
6803
+ if (progress == null || progress === "") return {};
6804
+ if (typeof progress === "object" && !Array.isArray(progress)) {
6805
+ return { ...progress };
6806
+ }
6807
+ if (typeof progress === "string") {
6808
+ const t = progress.trim();
6809
+ if (t.startsWith("{") || t.startsWith("[")) {
6810
+ try {
6811
+ const parsed = JSON.parse(t);
6812
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
6813
+ } catch {
6814
+ }
6815
+ }
6816
+ return { message: progress };
6817
+ }
6818
+ return { message: String(progress) };
6819
+ }
6820
+ async function applyPauseTask(context, taskId) {
6821
+ const db = context.db;
6822
+ if (!db) throw new Error("pauseTask: context.db is required");
6823
+ const id = String(taskId ?? "").trim();
6824
+ if (!id) throw new ParamError('pauseTask: param "taskId" is required');
6825
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
6826
+ const row = await db(tasksTable).where({ id }).first();
6827
+ if (!row) {
6828
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
6829
+ }
6830
+ if (row.status === "paused") {
6831
+ return {
6832
+ success: true,
6833
+ results: { taskId: id, status: "paused", message: "Task already paused" }
6834
+ };
6835
+ }
6836
+ if (row.status === "idle") {
6837
+ const progress = parseProgressObject(row.progress);
6838
+ progress.pauseRequested = true;
6839
+ progress.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
6840
+ await db(tasksTable).where({ id }).update({
6841
+ status: "paused",
6842
+ status_changed_at: db.fn.now(),
6843
+ progress: toJsonColumn(progress)
6844
+ });
6845
+ context.logger?.warn?.(`[pauseTask] idle task ${id} (${row.name}) \u2192 paused`);
6846
+ return {
6847
+ success: true,
6848
+ results: { taskId: id, status: "paused", message: "Idle task paused" }
6849
+ };
6850
+ }
6851
+ if (row.status === "running") {
6852
+ const progress = parseProgressObject(row.progress);
6853
+ progress.pauseRequested = true;
6854
+ progress.pauseRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
6855
+ await db(tasksTable).where({ id }).update({
6856
+ progress: toJsonColumn(progress)
6857
+ });
6858
+ const inst = context.runningTaskInstances?.get?.(id);
6859
+ if (inst && typeof inst.requestPause === "function") {
6860
+ await inst.requestPause();
6861
+ context.logger?.warn?.(
6862
+ `[pauseTask] signaled running task ${id} (${row.name}) to pause after current unit`
6863
+ );
6864
+ return {
6865
+ success: true,
6866
+ results: {
6867
+ taskId: id,
6868
+ status: "running",
6869
+ signaled: true,
6870
+ message: "Pause requested; task will finish current unit then park as paused"
6871
+ }
6872
+ };
6873
+ }
6874
+ context.logger?.warn?.(
6875
+ `[pauseTask] stamped pauseRequested on ${id} (${row.name}) \u2014 not running in this process`
6876
+ );
6877
+ return {
6878
+ success: true,
6879
+ results: {
6880
+ taskId: id,
6881
+ status: "running",
6882
+ signaled: false,
6883
+ message: "pauseRequested stamped on task row; target runner must observe it (enqueue pauseTask on that service)"
6884
+ }
6885
+ };
6886
+ }
6887
+ return {
6888
+ success: false,
6889
+ results: {
6890
+ error: `Cannot pause task in status "${row.status}"`,
6891
+ taskId: id,
6892
+ status: row.status
6893
+ }
6894
+ };
6895
+ }
6896
+ async function applyResumeTask(context, taskId) {
6897
+ const db = context.db;
6898
+ if (!db) throw new Error("resumeTask: context.db is required");
6899
+ const id = String(taskId ?? "").trim();
6900
+ if (!id) throw new ParamError('resumeTask: param "taskId" is required');
6901
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
6902
+ const row = await db(tasksTable).where({ id }).first();
6903
+ if (!row) {
6904
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
6905
+ }
6906
+ if (row.status === "idle") {
6907
+ return {
6908
+ success: true,
6909
+ results: { taskId: id, status: "idle", message: "Task already idle (runnable)" }
6910
+ };
6911
+ }
6912
+ if (row.status !== "paused") {
6913
+ return {
6914
+ success: false,
6915
+ results: {
6916
+ error: `Cannot resume task in status "${row.status}" (expected paused)`,
6917
+ taskId: id,
6918
+ status: row.status
6919
+ }
6920
+ };
6921
+ }
6922
+ const progress = parseProgressObject(row.progress);
6923
+ delete progress.pauseRequested;
6924
+ delete progress.pauseRequestedAt;
6925
+ delete progress.pausedAt;
6926
+ progress.resumedAt = (/* @__PURE__ */ new Date()).toISOString();
6927
+ await db(tasksTable).where({ id }).update({
6928
+ status: "idle",
6929
+ status_changed_at: db.fn.now(),
6930
+ started_at: null,
6931
+ completed_at: null,
6932
+ success: null,
6933
+ progress: toJsonColumn(progress),
6934
+ // Clear claim binding so any matching worker can pick it up.
6935
+ service_name: null,
6936
+ server_name: null,
6937
+ instance_number: null,
6938
+ next_run_at: null
6939
+ });
6940
+ context.logger?.warn?.(`[resumeTask] paused task ${id} (${row.name}) \u2192 idle`);
6941
+ return {
6942
+ success: true,
6943
+ results: { taskId: id, status: "idle", message: "Task resumed (idle; will be claimed)" }
6944
+ };
6945
+ }
6946
+ var TaskPauseTask = class extends AbstractTask {
6947
+ static taskName = "pauseTask";
6948
+ static description = "Pause a specific task: finish current unit, persist status=paused";
6949
+ static defaultWaitForResult = true;
6950
+ static async resolveCustomParams(context, overrides = {}) {
6951
+ const merged = AbstractTask._mergeTypedParams(
6952
+ context,
6953
+ "task-pause-task",
6954
+ { taskId: "string" },
6955
+ overrides
6956
+ );
6957
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
6958
+ if (!taskId) throw new ParamError('pauseTask: param "taskId" is required');
6959
+ return { taskId };
6960
+ }
6961
+ async run() {
6962
+ return applyPauseTask(this.context, this.task?.params?.taskId);
6963
+ }
6964
+ };
6965
+ var TaskResumeTask = class extends AbstractTask {
6966
+ static taskName = "resumeTask";
6967
+ static description = "Resume a paused task (status paused \u2192 idle)";
6968
+ static defaultWaitForResult = true;
6969
+ static async resolveCustomParams(context, overrides = {}) {
6970
+ const merged = AbstractTask._mergeTypedParams(
6971
+ context,
6972
+ "task-resume-task",
6973
+ { taskId: "string" },
6974
+ overrides
6975
+ );
6976
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
6977
+ if (!taskId) throw new ParamError('resumeTask: param "taskId" is required');
6978
+ return { taskId };
6979
+ }
6980
+ async run() {
6981
+ return applyResumeTask(this.context, this.task?.params?.taskId);
6982
+ }
6983
+ };
6984
+
6777
6985
  // src/tasks/coreTasks/TaskGetLogs.js
6778
6986
  var TaskGetLogs = class extends AbstractTask {
6779
6987
  /**
@@ -6992,7 +7200,7 @@ var TasksRegistry = class _TasksRegistry {
6992
7200
  * @returns {TasksRegistry}
6993
7201
  */
6994
7202
  static withCoreTasks() {
6995
- 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);
7203
+ 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);
6996
7204
  }
6997
7205
  /**
6998
7206
  * Register a single task class under a name. Overwrites any previous entry.
@@ -7184,11 +7392,12 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
7184
7392
  } finally {
7185
7393
  runningTaskInstances.delete(row.id);
7186
7394
  }
7395
+ const taskPaused = success && results && typeof results === "object" && results.taskPaused === true;
7187
7396
  await db(historyTable).insert(
7188
7397
  taskHistoryInsertFromQueueRow(row, {
7189
7398
  completed_at: /* @__PURE__ */ new Date(),
7190
7399
  success,
7191
- status: success ? "completed" : "failed",
7400
+ status: taskPaused ? "paused" : success ? "completed" : "failed",
7192
7401
  status_changed_at: db.fn.now(),
7193
7402
  params: toJsonColumn(row.params),
7194
7403
  results: toJsonColumn(results)
@@ -7211,7 +7420,24 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
7211
7420
  details: results
7212
7421
  });
7213
7422
  }
7214
- if (row.schedule) {
7423
+ if (taskPaused) {
7424
+ const checkpointParams = results.checkpointParams && typeof results.checkpointParams === "object" ? results.checkpointParams : row.params;
7425
+ const progressPayload = results.progress != null ? results.progress : { paused: true, pausedAt: (/* @__PURE__ */ new Date()).toISOString(), message: "paused" };
7426
+ await db(tasksTable).where({ id: row.id }).update({
7427
+ started_at: null,
7428
+ completed_at: /* @__PURE__ */ new Date(),
7429
+ success: true,
7430
+ results: toJsonColumn(results),
7431
+ params: toJsonColumn(checkpointParams),
7432
+ progress: toJsonColumn(progressPayload),
7433
+ status: "paused",
7434
+ status_changed_at: db.fn.now(),
7435
+ past_due: null,
7436
+ service_name: null,
7437
+ server_name: null,
7438
+ instance_number: null
7439
+ });
7440
+ } else if (row.schedule) {
7215
7441
  if (success) {
7216
7442
  let nextRunAt = null;
7217
7443
  try {
@@ -7339,6 +7565,7 @@ async function runTasksLoop(context, options) {
7339
7565
  }
7340
7566
  const runningPromises = /* @__PURE__ */ new Set();
7341
7567
  const runningTaskInstances = /* @__PURE__ */ new Map();
7568
+ context.runningTaskInstances = runningTaskInstances;
7342
7569
  let runningControlPromise = null;
7343
7570
  let stopRequested = false;
7344
7571
  let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;