@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.
package/dist/index.js CHANGED
@@ -7683,6 +7683,8 @@ var AbstractTask = class _AbstractTask {
7683
7683
  constructor(context, task) {
7684
7684
  this.context = context;
7685
7685
  this.task = task;
7686
+ this._stopRequested = false;
7687
+ this._pauseRequested = false;
7686
7688
  }
7687
7689
  /**
7688
7690
  * Return a short reason string when the task should be deferred (e.g. "locked
@@ -7700,6 +7702,26 @@ var AbstractTask = class _AbstractTask {
7700
7702
  * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
7701
7703
  */
7702
7704
  requestStop(_allowanceMs) {
7705
+ this._stopRequested = true;
7706
+ }
7707
+ /**
7708
+ * Cooperative pause signal (from `pauseTask` control-lane task). Long-running
7709
+ * tasks should finish the current unit of work, persist a checkpoint, and
7710
+ * return `{ success: true, results: { taskPaused: true, … } }` so the runner
7711
+ * keeps the row as `status=paused` instead of deleting it.
7712
+ *
7713
+ * @param {number} [_allowanceMs]
7714
+ */
7715
+ requestPause(_allowanceMs) {
7716
+ this._pauseRequested = true;
7717
+ }
7718
+ /** @returns {boolean} */
7719
+ isStopRequested() {
7720
+ return this._stopRequested === true;
7721
+ }
7722
+ /** @returns {boolean} */
7723
+ isPauseRequested() {
7724
+ return this._pauseRequested === true;
7703
7725
  }
7704
7726
  /**
7705
7727
  * Perform the task. Must be implemented by subclasses.
@@ -8406,6 +8428,8 @@ var CONTROL_LANE_TASK_NAMES = [
8406
8428
  "pause",
8407
8429
  "unpauseRunner",
8408
8430
  "unpause",
8431
+ "pauseTask",
8432
+ "resumeTask",
8409
8433
  "setRuntimeParam",
8410
8434
  "setRunnerParam"
8411
8435
  ];
@@ -8610,6 +8634,190 @@ var TaskUnpauseRunner = class extends AbstractTask {
8610
8634
  }
8611
8635
  };
8612
8636
 
8637
+ // src/tasks/coreTasks/TaskPauseTask.js
8638
+ function parseProgressObject(progress) {
8639
+ if (progress == null || progress === "") return {};
8640
+ if (typeof progress === "object" && !Array.isArray(progress)) {
8641
+ return { ...progress };
8642
+ }
8643
+ if (typeof progress === "string") {
8644
+ const t = progress.trim();
8645
+ if (t.startsWith("{") || t.startsWith("[")) {
8646
+ try {
8647
+ const parsed = JSON.parse(t);
8648
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
8649
+ } catch {
8650
+ }
8651
+ }
8652
+ return { message: progress };
8653
+ }
8654
+ return { message: String(progress) };
8655
+ }
8656
+ async function applyPauseTask(context, taskId) {
8657
+ const db = context.db;
8658
+ if (!db) throw new Error("pauseTask: context.db is required");
8659
+ const id = String(taskId ?? "").trim();
8660
+ if (!id) throw new ParamError('pauseTask: param "taskId" is required');
8661
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
8662
+ const row = await db(tasksTable).where({ id }).first();
8663
+ if (!row) {
8664
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
8665
+ }
8666
+ if (row.status === "paused") {
8667
+ return {
8668
+ success: true,
8669
+ results: { taskId: id, status: "paused", message: "Task already paused" }
8670
+ };
8671
+ }
8672
+ if (row.status === "idle") {
8673
+ const progress = parseProgressObject(row.progress);
8674
+ progress.pauseRequested = true;
8675
+ progress.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
8676
+ await db(tasksTable).where({ id }).update({
8677
+ status: "paused",
8678
+ status_changed_at: db.fn.now(),
8679
+ progress: toJsonColumn(progress)
8680
+ });
8681
+ context.logger?.warn?.(`[pauseTask] idle task ${id} (${row.name}) \u2192 paused`);
8682
+ return {
8683
+ success: true,
8684
+ results: { taskId: id, status: "paused", message: "Idle task paused" }
8685
+ };
8686
+ }
8687
+ if (row.status === "running") {
8688
+ const progress = parseProgressObject(row.progress);
8689
+ progress.pauseRequested = true;
8690
+ progress.pauseRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
8691
+ await db(tasksTable).where({ id }).update({
8692
+ progress: toJsonColumn(progress)
8693
+ });
8694
+ const inst = context.runningTaskInstances?.get?.(id);
8695
+ if (inst && typeof inst.requestPause === "function") {
8696
+ await inst.requestPause();
8697
+ context.logger?.warn?.(
8698
+ `[pauseTask] signaled running task ${id} (${row.name}) to pause after current unit`
8699
+ );
8700
+ return {
8701
+ success: true,
8702
+ results: {
8703
+ taskId: id,
8704
+ status: "running",
8705
+ signaled: true,
8706
+ message: "Pause requested; task will finish current unit then park as paused"
8707
+ }
8708
+ };
8709
+ }
8710
+ context.logger?.warn?.(
8711
+ `[pauseTask] stamped pauseRequested on ${id} (${row.name}) \u2014 not running in this process`
8712
+ );
8713
+ return {
8714
+ success: true,
8715
+ results: {
8716
+ taskId: id,
8717
+ status: "running",
8718
+ signaled: false,
8719
+ message: "pauseRequested stamped on task row; target runner must observe it (enqueue pauseTask on that service)"
8720
+ }
8721
+ };
8722
+ }
8723
+ return {
8724
+ success: false,
8725
+ results: {
8726
+ error: `Cannot pause task in status "${row.status}"`,
8727
+ taskId: id,
8728
+ status: row.status
8729
+ }
8730
+ };
8731
+ }
8732
+ async function applyResumeTask(context, taskId) {
8733
+ const db = context.db;
8734
+ if (!db) throw new Error("resumeTask: context.db is required");
8735
+ const id = String(taskId ?? "").trim();
8736
+ if (!id) throw new ParamError('resumeTask: param "taskId" is required');
8737
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
8738
+ const row = await db(tasksTable).where({ id }).first();
8739
+ if (!row) {
8740
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
8741
+ }
8742
+ if (row.status === "idle") {
8743
+ return {
8744
+ success: true,
8745
+ results: { taskId: id, status: "idle", message: "Task already idle (runnable)" }
8746
+ };
8747
+ }
8748
+ if (row.status !== "paused") {
8749
+ return {
8750
+ success: false,
8751
+ results: {
8752
+ error: `Cannot resume task in status "${row.status}" (expected paused)`,
8753
+ taskId: id,
8754
+ status: row.status
8755
+ }
8756
+ };
8757
+ }
8758
+ const progress = parseProgressObject(row.progress);
8759
+ delete progress.pauseRequested;
8760
+ delete progress.pauseRequestedAt;
8761
+ delete progress.pausedAt;
8762
+ progress.resumedAt = (/* @__PURE__ */ new Date()).toISOString();
8763
+ await db(tasksTable).where({ id }).update({
8764
+ status: "idle",
8765
+ status_changed_at: db.fn.now(),
8766
+ started_at: null,
8767
+ completed_at: null,
8768
+ success: null,
8769
+ progress: toJsonColumn(progress),
8770
+ // Clear claim binding so any matching worker can pick it up.
8771
+ service_name: null,
8772
+ server_name: null,
8773
+ instance_number: null,
8774
+ next_run_at: null
8775
+ });
8776
+ context.logger?.warn?.(`[resumeTask] paused task ${id} (${row.name}) \u2192 idle`);
8777
+ return {
8778
+ success: true,
8779
+ results: { taskId: id, status: "idle", message: "Task resumed (idle; will be claimed)" }
8780
+ };
8781
+ }
8782
+ var TaskPauseTask = class extends AbstractTask {
8783
+ static taskName = "pauseTask";
8784
+ static description = "Pause a specific task: finish current unit, persist status=paused";
8785
+ static defaultWaitForResult = true;
8786
+ static async resolveCustomParams(context, overrides = {}) {
8787
+ const merged = AbstractTask._mergeTypedParams(
8788
+ context,
8789
+ "task-pause-task",
8790
+ { taskId: "string" },
8791
+ overrides
8792
+ );
8793
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
8794
+ if (!taskId) throw new ParamError('pauseTask: param "taskId" is required');
8795
+ return { taskId };
8796
+ }
8797
+ async run() {
8798
+ return applyPauseTask(this.context, this.task?.params?.taskId);
8799
+ }
8800
+ };
8801
+ var TaskResumeTask = class extends AbstractTask {
8802
+ static taskName = "resumeTask";
8803
+ static description = "Resume a paused task (status paused \u2192 idle)";
8804
+ static defaultWaitForResult = true;
8805
+ static async resolveCustomParams(context, overrides = {}) {
8806
+ const merged = AbstractTask._mergeTypedParams(
8807
+ context,
8808
+ "task-resume-task",
8809
+ { taskId: "string" },
8810
+ overrides
8811
+ );
8812
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
8813
+ if (!taskId) throw new ParamError('resumeTask: param "taskId" is required');
8814
+ return { taskId };
8815
+ }
8816
+ async run() {
8817
+ return applyResumeTask(this.context, this.task?.params?.taskId);
8818
+ }
8819
+ };
8820
+
8613
8821
  // src/tasks/coreTasks/TaskGetLogs.js
8614
8822
  var TaskGetLogs = class extends AbstractTask {
8615
8823
  /**
@@ -8828,7 +9036,7 @@ var TasksRegistry = class _TasksRegistry {
8828
9036
  * @returns {TasksRegistry}
8829
9037
  */
8830
9038
  static withCoreTasks() {
8831
- 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);
9039
+ 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);
8832
9040
  }
8833
9041
  /**
8834
9042
  * Register a single task class under a name. Overwrites any previous entry.
@@ -8935,6 +9143,8 @@ var SERVICE_TASK_NAMES = [
8935
9143
  "pauseRunner",
8936
9144
  "unpause",
8937
9145
  "unpauseRunner",
9146
+ "pauseTask",
9147
+ "resumeTask",
8938
9148
  "shellCommand",
8939
9149
  "systemInfo",
8940
9150
  "info",
@@ -9230,11 +9440,12 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
9230
9440
  } finally {
9231
9441
  runningTaskInstances.delete(row.id);
9232
9442
  }
9443
+ const taskPaused = success && results && typeof results === "object" && results.taskPaused === true;
9233
9444
  await db(historyTable).insert(
9234
9445
  taskHistoryInsertFromQueueRow(row, {
9235
9446
  completed_at: /* @__PURE__ */ new Date(),
9236
9447
  success,
9237
- status: success ? "completed" : "failed",
9448
+ status: taskPaused ? "paused" : success ? "completed" : "failed",
9238
9449
  status_changed_at: db.fn.now(),
9239
9450
  params: toJsonColumn(row.params),
9240
9451
  results: toJsonColumn(results)
@@ -9257,7 +9468,24 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
9257
9468
  details: results
9258
9469
  });
9259
9470
  }
9260
- if (row.schedule) {
9471
+ if (taskPaused) {
9472
+ const checkpointParams = results.checkpointParams && typeof results.checkpointParams === "object" ? results.checkpointParams : row.params;
9473
+ const progressPayload = results.progress != null ? results.progress : { paused: true, pausedAt: (/* @__PURE__ */ new Date()).toISOString(), message: "paused" };
9474
+ await db(tasksTable).where({ id: row.id }).update({
9475
+ started_at: null,
9476
+ completed_at: /* @__PURE__ */ new Date(),
9477
+ success: true,
9478
+ results: toJsonColumn(results),
9479
+ params: toJsonColumn(checkpointParams),
9480
+ progress: toJsonColumn(progressPayload),
9481
+ status: "paused",
9482
+ status_changed_at: db.fn.now(),
9483
+ past_due: null,
9484
+ service_name: null,
9485
+ server_name: null,
9486
+ instance_number: null
9487
+ });
9488
+ } else if (row.schedule) {
9261
9489
  if (success) {
9262
9490
  let nextRunAt = null;
9263
9491
  try {
@@ -9385,6 +9613,7 @@ async function runTasksLoop(context, options) {
9385
9613
  }
9386
9614
  const runningPromises = /* @__PURE__ */ new Set();
9387
9615
  const runningTaskInstances = /* @__PURE__ */ new Map();
9616
+ context.runningTaskInstances = runningTaskInstances;
9388
9617
  let runningControlPromise = null;
9389
9618
  let stopRequested = false;
9390
9619
  let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;
@@ -9772,7 +10001,9 @@ export {
9772
10001
  ScrollableText,
9773
10002
  TaskGetLogs,
9774
10003
  TaskPauseRunner,
10004
+ TaskPauseTask,
9775
10005
  TaskPing,
10006
+ TaskResumeTask,
9776
10007
  TaskSampleProcess,
9777
10008
  TaskSetRuntimeParam,
9778
10009
  TaskShellCommand,
@@ -9787,6 +10018,8 @@ export {
9787
10018
  activateRelease,
9788
10019
  appendDeployLog,
9789
10020
  appendTaskIpcLog,
10021
+ applyPauseTask,
10022
+ applyResumeTask,
9790
10023
  applyRunnerPaused,
9791
10024
  applyRuntimeParam,
9792
10025
  applyRuntimePatch,