@nmakarov/cli-toolkit 0.71.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.
@@ -4381,6 +4381,9 @@ var DEFAULT_GROUP_MAX_INSTANCES = {
4381
4381
  harvest: 1,
4382
4382
  harvester: 0,
4383
4383
  loader: 0,
4384
+ toolkit: 0,
4385
+ photocare: 0,
4386
+ /** @deprecated use photocare */
4384
4387
  photos: 0,
4385
4388
  photosprocessor: 0,
4386
4389
  ingest: 0
@@ -5868,6 +5871,8 @@ var AbstractTask = class _AbstractTask {
5868
5871
  constructor(context, task) {
5869
5872
  this.context = context;
5870
5873
  this.task = task;
5874
+ this._stopRequested = false;
5875
+ this._pauseRequested = false;
5871
5876
  }
5872
5877
  /**
5873
5878
  * Return a short reason string when the task should be deferred (e.g. "locked
@@ -5885,6 +5890,26 @@ var AbstractTask = class _AbstractTask {
5885
5890
  * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
5886
5891
  */
5887
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;
5888
5913
  }
5889
5914
  /**
5890
5915
  * Perform the task. Must be implemented by subclasses.
@@ -6583,6 +6608,8 @@ var CONTROL_LANE_TASK_NAMES = [
6583
6608
  "pause",
6584
6609
  "unpauseRunner",
6585
6610
  "unpause",
6611
+ "pauseTask",
6612
+ "resumeTask",
6586
6613
  "setRuntimeParam",
6587
6614
  "setRunnerParam"
6588
6615
  ];
@@ -6787,6 +6814,190 @@ var TaskUnpauseRunner = class extends AbstractTask {
6787
6814
  }
6788
6815
  };
6789
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
+
6790
7001
  // src/tasks/coreTasks/TaskGetLogs.js
6791
7002
  var TaskGetLogs = class extends AbstractTask {
6792
7003
  /**
@@ -7005,7 +7216,7 @@ var TasksRegistry = class _TasksRegistry {
7005
7216
  * @returns {TasksRegistry}
7006
7217
  */
7007
7218
  static withCoreTasks() {
7008
- 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);
7009
7220
  }
7010
7221
  /**
7011
7222
  * Register a single task class under a name. Overwrites any previous entry.
@@ -7197,11 +7408,12 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
7197
7408
  } finally {
7198
7409
  runningTaskInstances.delete(row.id);
7199
7410
  }
7411
+ const taskPaused = success && results && typeof results === "object" && results.taskPaused === true;
7200
7412
  await db(historyTable).insert(
7201
7413
  taskHistoryInsertFromQueueRow(row, {
7202
7414
  completed_at: /* @__PURE__ */ new Date(),
7203
7415
  success,
7204
- status: success ? "completed" : "failed",
7416
+ status: taskPaused ? "paused" : success ? "completed" : "failed",
7205
7417
  status_changed_at: db.fn.now(),
7206
7418
  params: toJsonColumn(row.params),
7207
7419
  results: toJsonColumn(results)
@@ -7224,7 +7436,24 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
7224
7436
  details: results
7225
7437
  });
7226
7438
  }
7227
- 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) {
7228
7457
  if (success) {
7229
7458
  let nextRunAt = null;
7230
7459
  try {
@@ -7352,6 +7581,7 @@ async function runTasksLoop(context, options) {
7352
7581
  }
7353
7582
  const runningPromises = /* @__PURE__ */ new Set();
7354
7583
  const runningTaskInstances = /* @__PURE__ */ new Map();
7584
+ context.runningTaskInstances = runningTaskInstances;
7355
7585
  let runningControlPromise = null;
7356
7586
  let stopRequested = false;
7357
7587
  let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;