@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.
@@ -5871,6 +5871,8 @@ var AbstractTask = class _AbstractTask {
5871
5871
  constructor(context, task) {
5872
5872
  this.context = context;
5873
5873
  this.task = task;
5874
+ this._stopRequested = false;
5875
+ this._pauseRequested = false;
5874
5876
  }
5875
5877
  /**
5876
5878
  * Return a short reason string when the task should be deferred (e.g. "locked
@@ -5888,6 +5890,26 @@ var AbstractTask = class _AbstractTask {
5888
5890
  * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
5889
5891
  */
5890
5892
  requestStop(_allowanceMs) {
5893
+ this._stopRequested = true;
5894
+ }
5895
+ /**
5896
+ * Cooperative pause signal (from `pauseTask` control-lane task). Long-running
5897
+ * tasks should finish the current unit of work, persist a checkpoint, and
5898
+ * return `{ success: true, results: { taskPaused: true, … } }` so the runner
5899
+ * keeps the row as `status=paused` instead of deleting it.
5900
+ *
5901
+ * @param {number} [_allowanceMs]
5902
+ */
5903
+ requestPause(_allowanceMs) {
5904
+ this._pauseRequested = true;
5905
+ }
5906
+ /** @returns {boolean} */
5907
+ isStopRequested() {
5908
+ return this._stopRequested === true;
5909
+ }
5910
+ /** @returns {boolean} */
5911
+ isPauseRequested() {
5912
+ return this._pauseRequested === true;
5891
5913
  }
5892
5914
  /**
5893
5915
  * Perform the task. Must be implemented by subclasses.
@@ -6586,6 +6608,8 @@ var CONTROL_LANE_TASK_NAMES = [
6586
6608
  "pause",
6587
6609
  "unpauseRunner",
6588
6610
  "unpause",
6611
+ "pauseTask",
6612
+ "resumeTask",
6589
6613
  "setRuntimeParam",
6590
6614
  "setRunnerParam"
6591
6615
  ];
@@ -6790,6 +6814,190 @@ var TaskUnpauseRunner = class extends AbstractTask {
6790
6814
  }
6791
6815
  };
6792
6816
 
6817
+ // src/tasks/coreTasks/TaskPauseTask.js
6818
+ function parseProgressObject(progress) {
6819
+ if (progress == null || progress === "") return {};
6820
+ if (typeof progress === "object" && !Array.isArray(progress)) {
6821
+ return { ...progress };
6822
+ }
6823
+ if (typeof progress === "string") {
6824
+ const t = progress.trim();
6825
+ if (t.startsWith("{") || t.startsWith("[")) {
6826
+ try {
6827
+ const parsed = JSON.parse(t);
6828
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
6829
+ } catch {
6830
+ }
6831
+ }
6832
+ return { message: progress };
6833
+ }
6834
+ return { message: String(progress) };
6835
+ }
6836
+ async function applyPauseTask(context, taskId) {
6837
+ const db = context.db;
6838
+ if (!db) throw new Error("pauseTask: context.db is required");
6839
+ const id = String(taskId ?? "").trim();
6840
+ if (!id) throw new ParamError('pauseTask: param "taskId" is required');
6841
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
6842
+ const row = await db(tasksTable).where({ id }).first();
6843
+ if (!row) {
6844
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
6845
+ }
6846
+ if (row.status === "paused") {
6847
+ return {
6848
+ success: true,
6849
+ results: { taskId: id, status: "paused", message: "Task already paused" }
6850
+ };
6851
+ }
6852
+ if (row.status === "idle") {
6853
+ const progress = parseProgressObject(row.progress);
6854
+ progress.pauseRequested = true;
6855
+ progress.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
6856
+ await db(tasksTable).where({ id }).update({
6857
+ status: "paused",
6858
+ status_changed_at: db.fn.now(),
6859
+ progress: toJsonColumn(progress)
6860
+ });
6861
+ context.logger?.warn?.(`[pauseTask] idle task ${id} (${row.name}) \u2192 paused`);
6862
+ return {
6863
+ success: true,
6864
+ results: { taskId: id, status: "paused", message: "Idle task paused" }
6865
+ };
6866
+ }
6867
+ if (row.status === "running") {
6868
+ const progress = parseProgressObject(row.progress);
6869
+ progress.pauseRequested = true;
6870
+ progress.pauseRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
6871
+ await db(tasksTable).where({ id }).update({
6872
+ progress: toJsonColumn(progress)
6873
+ });
6874
+ const inst = context.runningTaskInstances?.get?.(id);
6875
+ if (inst && typeof inst.requestPause === "function") {
6876
+ await inst.requestPause();
6877
+ context.logger?.warn?.(
6878
+ `[pauseTask] signaled running task ${id} (${row.name}) to pause after current unit`
6879
+ );
6880
+ return {
6881
+ success: true,
6882
+ results: {
6883
+ taskId: id,
6884
+ status: "running",
6885
+ signaled: true,
6886
+ message: "Pause requested; task will finish current unit then park as paused"
6887
+ }
6888
+ };
6889
+ }
6890
+ context.logger?.warn?.(
6891
+ `[pauseTask] stamped pauseRequested on ${id} (${row.name}) \u2014 not running in this process`
6892
+ );
6893
+ return {
6894
+ success: true,
6895
+ results: {
6896
+ taskId: id,
6897
+ status: "running",
6898
+ signaled: false,
6899
+ message: "pauseRequested stamped on task row; target runner must observe it (enqueue pauseTask on that service)"
6900
+ }
6901
+ };
6902
+ }
6903
+ return {
6904
+ success: false,
6905
+ results: {
6906
+ error: `Cannot pause task in status "${row.status}"`,
6907
+ taskId: id,
6908
+ status: row.status
6909
+ }
6910
+ };
6911
+ }
6912
+ async function applyResumeTask(context, taskId) {
6913
+ const db = context.db;
6914
+ if (!db) throw new Error("resumeTask: context.db is required");
6915
+ const id = String(taskId ?? "").trim();
6916
+ if (!id) throw new ParamError('resumeTask: param "taskId" is required');
6917
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
6918
+ const row = await db(tasksTable).where({ id }).first();
6919
+ if (!row) {
6920
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
6921
+ }
6922
+ if (row.status === "idle") {
6923
+ return {
6924
+ success: true,
6925
+ results: { taskId: id, status: "idle", message: "Task already idle (runnable)" }
6926
+ };
6927
+ }
6928
+ if (row.status !== "paused") {
6929
+ return {
6930
+ success: false,
6931
+ results: {
6932
+ error: `Cannot resume task in status "${row.status}" (expected paused)`,
6933
+ taskId: id,
6934
+ status: row.status
6935
+ }
6936
+ };
6937
+ }
6938
+ const progress = parseProgressObject(row.progress);
6939
+ delete progress.pauseRequested;
6940
+ delete progress.pauseRequestedAt;
6941
+ delete progress.pausedAt;
6942
+ progress.resumedAt = (/* @__PURE__ */ new Date()).toISOString();
6943
+ await db(tasksTable).where({ id }).update({
6944
+ status: "idle",
6945
+ status_changed_at: db.fn.now(),
6946
+ started_at: null,
6947
+ completed_at: null,
6948
+ success: null,
6949
+ progress: toJsonColumn(progress),
6950
+ // Clear claim binding so any matching worker can pick it up.
6951
+ service_name: null,
6952
+ server_name: null,
6953
+ instance_number: null,
6954
+ next_run_at: null
6955
+ });
6956
+ context.logger?.warn?.(`[resumeTask] paused task ${id} (${row.name}) \u2192 idle`);
6957
+ return {
6958
+ success: true,
6959
+ results: { taskId: id, status: "idle", message: "Task resumed (idle; will be claimed)" }
6960
+ };
6961
+ }
6962
+ var TaskPauseTask = class extends AbstractTask {
6963
+ static taskName = "pauseTask";
6964
+ static description = "Pause a specific task: finish current unit, persist status=paused";
6965
+ static defaultWaitForResult = true;
6966
+ static async resolveCustomParams(context, overrides = {}) {
6967
+ const merged = AbstractTask._mergeTypedParams(
6968
+ context,
6969
+ "task-pause-task",
6970
+ { taskId: "string" },
6971
+ overrides
6972
+ );
6973
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
6974
+ if (!taskId) throw new ParamError('pauseTask: param "taskId" is required');
6975
+ return { taskId };
6976
+ }
6977
+ async run() {
6978
+ return applyPauseTask(this.context, this.task?.params?.taskId);
6979
+ }
6980
+ };
6981
+ var TaskResumeTask = class extends AbstractTask {
6982
+ static taskName = "resumeTask";
6983
+ static description = "Resume a paused task (status paused \u2192 idle)";
6984
+ static defaultWaitForResult = true;
6985
+ static async resolveCustomParams(context, overrides = {}) {
6986
+ const merged = AbstractTask._mergeTypedParams(
6987
+ context,
6988
+ "task-resume-task",
6989
+ { taskId: "string" },
6990
+ overrides
6991
+ );
6992
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
6993
+ if (!taskId) throw new ParamError('resumeTask: param "taskId" is required');
6994
+ return { taskId };
6995
+ }
6996
+ async run() {
6997
+ return applyResumeTask(this.context, this.task?.params?.taskId);
6998
+ }
6999
+ };
7000
+
6793
7001
  // src/tasks/coreTasks/TaskGetLogs.js
6794
7002
  var TaskGetLogs = class extends AbstractTask {
6795
7003
  /**
@@ -7008,7 +7216,7 @@ var TasksRegistry = class _TasksRegistry {
7008
7216
  * @returns {TasksRegistry}
7009
7217
  */
7010
7218
  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);
7219
+ 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
7220
  }
7013
7221
  /**
7014
7222
  * Register a single task class under a name. Overwrites any previous entry.
@@ -7200,11 +7408,12 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
7200
7408
  } finally {
7201
7409
  runningTaskInstances.delete(row.id);
7202
7410
  }
7411
+ const taskPaused = success && results && typeof results === "object" && results.taskPaused === true;
7203
7412
  await db(historyTable).insert(
7204
7413
  taskHistoryInsertFromQueueRow(row, {
7205
7414
  completed_at: /* @__PURE__ */ new Date(),
7206
7415
  success,
7207
- status: success ? "completed" : "failed",
7416
+ status: taskPaused ? "paused" : success ? "completed" : "failed",
7208
7417
  status_changed_at: db.fn.now(),
7209
7418
  params: toJsonColumn(row.params),
7210
7419
  results: toJsonColumn(results)
@@ -7227,7 +7436,24 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
7227
7436
  details: results
7228
7437
  });
7229
7438
  }
7230
- if (row.schedule) {
7439
+ if (taskPaused) {
7440
+ const checkpointParams = results.checkpointParams && typeof results.checkpointParams === "object" ? results.checkpointParams : row.params;
7441
+ const progressPayload = results.progress != null ? results.progress : { paused: true, pausedAt: (/* @__PURE__ */ new Date()).toISOString(), message: "paused" };
7442
+ await db(tasksTable).where({ id: row.id }).update({
7443
+ started_at: null,
7444
+ completed_at: /* @__PURE__ */ new Date(),
7445
+ success: true,
7446
+ results: toJsonColumn(results),
7447
+ params: toJsonColumn(checkpointParams),
7448
+ progress: toJsonColumn(progressPayload),
7449
+ status: "paused",
7450
+ status_changed_at: db.fn.now(),
7451
+ past_due: null,
7452
+ service_name: null,
7453
+ server_name: null,
7454
+ instance_number: null
7455
+ });
7456
+ } else if (row.schedule) {
7231
7457
  if (success) {
7232
7458
  let nextRunAt = null;
7233
7459
  try {
@@ -7355,6 +7581,7 @@ async function runTasksLoop(context, options) {
7355
7581
  }
7356
7582
  const runningPromises = /* @__PURE__ */ new Set();
7357
7583
  const runningTaskInstances = /* @__PURE__ */ new Map();
7584
+ context.runningTaskInstances = runningTaskInstances;
7358
7585
  let runningControlPromise = null;
7359
7586
  let stopRequested = false;
7360
7587
  let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;