@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/tasks.cjs CHANGED
@@ -36,7 +36,9 @@ __export(tasks_exports, {
36
36
  SERVICE_TASK_NAMES: () => SERVICE_TASK_NAMES,
37
37
  TaskGetLogs: () => TaskGetLogs,
38
38
  TaskPauseRunner: () => TaskPauseRunner,
39
+ TaskPauseTask: () => TaskPauseTask,
39
40
  TaskPing: () => TaskPing,
41
+ TaskResumeTask: () => TaskResumeTask,
40
42
  TaskSampleProcess: () => TaskSampleProcess,
41
43
  TaskSetRuntimeParam: () => TaskSetRuntimeParam,
42
44
  TaskShellCommand: () => TaskShellCommand,
@@ -47,6 +49,8 @@ __export(tasks_exports, {
47
49
  TasksManager: () => TasksManager,
48
50
  TasksRegistry: () => TasksRegistry,
49
51
  appendTaskIpcLog: () => appendTaskIpcLog,
52
+ applyPauseTask: () => applyPauseTask,
53
+ applyResumeTask: () => applyResumeTask,
50
54
  applyRunnerPaused: () => applyRunnerPaused,
51
55
  applyRuntimeParam: () => applyRuntimeParam,
52
56
  applyRuntimePatch: () => applyRuntimePatch,
@@ -2141,6 +2145,8 @@ var AbstractTask = class _AbstractTask {
2141
2145
  constructor(context, task) {
2142
2146
  this.context = context;
2143
2147
  this.task = task;
2148
+ this._stopRequested = false;
2149
+ this._pauseRequested = false;
2144
2150
  }
2145
2151
  /**
2146
2152
  * Return a short reason string when the task should be deferred (e.g. "locked
@@ -2158,6 +2164,26 @@ var AbstractTask = class _AbstractTask {
2158
2164
  * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
2159
2165
  */
2160
2166
  requestStop(_allowanceMs) {
2167
+ this._stopRequested = true;
2168
+ }
2169
+ /**
2170
+ * Cooperative pause signal (from `pauseTask` control-lane task). Long-running
2171
+ * tasks should finish the current unit of work, persist a checkpoint, and
2172
+ * return `{ success: true, results: { taskPaused: true, … } }` so the runner
2173
+ * keeps the row as `status=paused` instead of deleting it.
2174
+ *
2175
+ * @param {number} [_allowanceMs]
2176
+ */
2177
+ requestPause(_allowanceMs) {
2178
+ this._pauseRequested = true;
2179
+ }
2180
+ /** @returns {boolean} */
2181
+ isStopRequested() {
2182
+ return this._stopRequested === true;
2183
+ }
2184
+ /** @returns {boolean} */
2185
+ isPauseRequested() {
2186
+ return this._pauseRequested === true;
2161
2187
  }
2162
2188
  /**
2163
2189
  * Perform the task. Must be implemented by subclasses.
@@ -2864,6 +2890,8 @@ var CONTROL_LANE_TASK_NAMES = [
2864
2890
  "pause",
2865
2891
  "unpauseRunner",
2866
2892
  "unpause",
2893
+ "pauseTask",
2894
+ "resumeTask",
2867
2895
  "setRuntimeParam",
2868
2896
  "setRunnerParam"
2869
2897
  ];
@@ -3068,6 +3096,190 @@ var TaskUnpauseRunner = class extends AbstractTask {
3068
3096
  }
3069
3097
  };
3070
3098
 
3099
+ // src/tasks/coreTasks/TaskPauseTask.js
3100
+ function parseProgressObject(progress) {
3101
+ if (progress == null || progress === "") return {};
3102
+ if (typeof progress === "object" && !Array.isArray(progress)) {
3103
+ return { ...progress };
3104
+ }
3105
+ if (typeof progress === "string") {
3106
+ const t = progress.trim();
3107
+ if (t.startsWith("{") || t.startsWith("[")) {
3108
+ try {
3109
+ const parsed = JSON.parse(t);
3110
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
3111
+ } catch {
3112
+ }
3113
+ }
3114
+ return { message: progress };
3115
+ }
3116
+ return { message: String(progress) };
3117
+ }
3118
+ async function applyPauseTask(context, taskId) {
3119
+ const db = context.db;
3120
+ if (!db) throw new Error("pauseTask: context.db is required");
3121
+ const id = String(taskId ?? "").trim();
3122
+ if (!id) throw new ParamError('pauseTask: param "taskId" is required');
3123
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
3124
+ const row = await db(tasksTable).where({ id }).first();
3125
+ if (!row) {
3126
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
3127
+ }
3128
+ if (row.status === "paused") {
3129
+ return {
3130
+ success: true,
3131
+ results: { taskId: id, status: "paused", message: "Task already paused" }
3132
+ };
3133
+ }
3134
+ if (row.status === "idle") {
3135
+ const progress = parseProgressObject(row.progress);
3136
+ progress.pauseRequested = true;
3137
+ progress.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
3138
+ await db(tasksTable).where({ id }).update({
3139
+ status: "paused",
3140
+ status_changed_at: db.fn.now(),
3141
+ progress: toJsonColumn(progress)
3142
+ });
3143
+ context.logger?.warn?.(`[pauseTask] idle task ${id} (${row.name}) \u2192 paused`);
3144
+ return {
3145
+ success: true,
3146
+ results: { taskId: id, status: "paused", message: "Idle task paused" }
3147
+ };
3148
+ }
3149
+ if (row.status === "running") {
3150
+ const progress = parseProgressObject(row.progress);
3151
+ progress.pauseRequested = true;
3152
+ progress.pauseRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
3153
+ await db(tasksTable).where({ id }).update({
3154
+ progress: toJsonColumn(progress)
3155
+ });
3156
+ const inst = context.runningTaskInstances?.get?.(id);
3157
+ if (inst && typeof inst.requestPause === "function") {
3158
+ await inst.requestPause();
3159
+ context.logger?.warn?.(
3160
+ `[pauseTask] signaled running task ${id} (${row.name}) to pause after current unit`
3161
+ );
3162
+ return {
3163
+ success: true,
3164
+ results: {
3165
+ taskId: id,
3166
+ status: "running",
3167
+ signaled: true,
3168
+ message: "Pause requested; task will finish current unit then park as paused"
3169
+ }
3170
+ };
3171
+ }
3172
+ context.logger?.warn?.(
3173
+ `[pauseTask] stamped pauseRequested on ${id} (${row.name}) \u2014 not running in this process`
3174
+ );
3175
+ return {
3176
+ success: true,
3177
+ results: {
3178
+ taskId: id,
3179
+ status: "running",
3180
+ signaled: false,
3181
+ message: "pauseRequested stamped on task row; target runner must observe it (enqueue pauseTask on that service)"
3182
+ }
3183
+ };
3184
+ }
3185
+ return {
3186
+ success: false,
3187
+ results: {
3188
+ error: `Cannot pause task in status "${row.status}"`,
3189
+ taskId: id,
3190
+ status: row.status
3191
+ }
3192
+ };
3193
+ }
3194
+ async function applyResumeTask(context, taskId) {
3195
+ const db = context.db;
3196
+ if (!db) throw new Error("resumeTask: context.db is required");
3197
+ const id = String(taskId ?? "").trim();
3198
+ if (!id) throw new ParamError('resumeTask: param "taskId" is required');
3199
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
3200
+ const row = await db(tasksTable).where({ id }).first();
3201
+ if (!row) {
3202
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
3203
+ }
3204
+ if (row.status === "idle") {
3205
+ return {
3206
+ success: true,
3207
+ results: { taskId: id, status: "idle", message: "Task already idle (runnable)" }
3208
+ };
3209
+ }
3210
+ if (row.status !== "paused") {
3211
+ return {
3212
+ success: false,
3213
+ results: {
3214
+ error: `Cannot resume task in status "${row.status}" (expected paused)`,
3215
+ taskId: id,
3216
+ status: row.status
3217
+ }
3218
+ };
3219
+ }
3220
+ const progress = parseProgressObject(row.progress);
3221
+ delete progress.pauseRequested;
3222
+ delete progress.pauseRequestedAt;
3223
+ delete progress.pausedAt;
3224
+ progress.resumedAt = (/* @__PURE__ */ new Date()).toISOString();
3225
+ await db(tasksTable).where({ id }).update({
3226
+ status: "idle",
3227
+ status_changed_at: db.fn.now(),
3228
+ started_at: null,
3229
+ completed_at: null,
3230
+ success: null,
3231
+ progress: toJsonColumn(progress),
3232
+ // Clear claim binding so any matching worker can pick it up.
3233
+ service_name: null,
3234
+ server_name: null,
3235
+ instance_number: null,
3236
+ next_run_at: null
3237
+ });
3238
+ context.logger?.warn?.(`[resumeTask] paused task ${id} (${row.name}) \u2192 idle`);
3239
+ return {
3240
+ success: true,
3241
+ results: { taskId: id, status: "idle", message: "Task resumed (idle; will be claimed)" }
3242
+ };
3243
+ }
3244
+ var TaskPauseTask = class extends AbstractTask {
3245
+ static taskName = "pauseTask";
3246
+ static description = "Pause a specific task: finish current unit, persist status=paused";
3247
+ static defaultWaitForResult = true;
3248
+ static async resolveCustomParams(context, overrides = {}) {
3249
+ const merged = AbstractTask._mergeTypedParams(
3250
+ context,
3251
+ "task-pause-task",
3252
+ { taskId: "string" },
3253
+ overrides
3254
+ );
3255
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
3256
+ if (!taskId) throw new ParamError('pauseTask: param "taskId" is required');
3257
+ return { taskId };
3258
+ }
3259
+ async run() {
3260
+ return applyPauseTask(this.context, this.task?.params?.taskId);
3261
+ }
3262
+ };
3263
+ var TaskResumeTask = class extends AbstractTask {
3264
+ static taskName = "resumeTask";
3265
+ static description = "Resume a paused task (status paused \u2192 idle)";
3266
+ static defaultWaitForResult = true;
3267
+ static async resolveCustomParams(context, overrides = {}) {
3268
+ const merged = AbstractTask._mergeTypedParams(
3269
+ context,
3270
+ "task-resume-task",
3271
+ { taskId: "string" },
3272
+ overrides
3273
+ );
3274
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
3275
+ if (!taskId) throw new ParamError('resumeTask: param "taskId" is required');
3276
+ return { taskId };
3277
+ }
3278
+ async run() {
3279
+ return applyResumeTask(this.context, this.task?.params?.taskId);
3280
+ }
3281
+ };
3282
+
3071
3283
  // src/tasks/coreTasks/TaskGetLogs.js
3072
3284
  var TaskGetLogs = class extends AbstractTask {
3073
3285
  /**
@@ -3286,7 +3498,7 @@ var TasksRegistry = class _TasksRegistry {
3286
3498
  * @returns {TasksRegistry}
3287
3499
  */
3288
3500
  static withCoreTasks() {
3289
- 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);
3501
+ 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);
3290
3502
  }
3291
3503
  /**
3292
3504
  * Register a single task class under a name. Overwrites any previous entry.
@@ -3393,6 +3605,8 @@ var SERVICE_TASK_NAMES = [
3393
3605
  "pauseRunner",
3394
3606
  "unpause",
3395
3607
  "unpauseRunner",
3608
+ "pauseTask",
3609
+ "resumeTask",
3396
3610
  "shellCommand",
3397
3611
  "systemInfo",
3398
3612
  "info",
@@ -3688,11 +3902,12 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
3688
3902
  } finally {
3689
3903
  runningTaskInstances.delete(row.id);
3690
3904
  }
3905
+ const taskPaused = success && results && typeof results === "object" && results.taskPaused === true;
3691
3906
  await db(historyTable).insert(
3692
3907
  taskHistoryInsertFromQueueRow(row, {
3693
3908
  completed_at: /* @__PURE__ */ new Date(),
3694
3909
  success,
3695
- status: success ? "completed" : "failed",
3910
+ status: taskPaused ? "paused" : success ? "completed" : "failed",
3696
3911
  status_changed_at: db.fn.now(),
3697
3912
  params: toJsonColumn(row.params),
3698
3913
  results: toJsonColumn(results)
@@ -3715,7 +3930,24 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
3715
3930
  details: results
3716
3931
  });
3717
3932
  }
3718
- if (row.schedule) {
3933
+ if (taskPaused) {
3934
+ const checkpointParams = results.checkpointParams && typeof results.checkpointParams === "object" ? results.checkpointParams : row.params;
3935
+ const progressPayload = results.progress != null ? results.progress : { paused: true, pausedAt: (/* @__PURE__ */ new Date()).toISOString(), message: "paused" };
3936
+ await db(tasksTable).where({ id: row.id }).update({
3937
+ started_at: null,
3938
+ completed_at: /* @__PURE__ */ new Date(),
3939
+ success: true,
3940
+ results: toJsonColumn(results),
3941
+ params: toJsonColumn(checkpointParams),
3942
+ progress: toJsonColumn(progressPayload),
3943
+ status: "paused",
3944
+ status_changed_at: db.fn.now(),
3945
+ past_due: null,
3946
+ service_name: null,
3947
+ server_name: null,
3948
+ instance_number: null
3949
+ });
3950
+ } else if (row.schedule) {
3719
3951
  if (success) {
3720
3952
  let nextRunAt = null;
3721
3953
  try {
@@ -3843,6 +4075,7 @@ async function runTasksLoop(context, options) {
3843
4075
  }
3844
4076
  const runningPromises = /* @__PURE__ */ new Set();
3845
4077
  const runningTaskInstances = /* @__PURE__ */ new Map();
4078
+ context.runningTaskInstances = runningTaskInstances;
3846
4079
  let runningControlPromise = null;
3847
4080
  let stopRequested = false;
3848
4081
  let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;
@@ -4203,7 +4436,9 @@ var TasksManager = class _TasksManager {
4203
4436
  SERVICE_TASK_NAMES,
4204
4437
  TaskGetLogs,
4205
4438
  TaskPauseRunner,
4439
+ TaskPauseTask,
4206
4440
  TaskPing,
4441
+ TaskResumeTask,
4207
4442
  TaskSampleProcess,
4208
4443
  TaskSetRuntimeParam,
4209
4444
  TaskShellCommand,
@@ -4214,6 +4449,8 @@ var TasksManager = class _TasksManager {
4214
4449
  TasksManager,
4215
4450
  TasksRegistry,
4216
4451
  appendTaskIpcLog,
4452
+ applyPauseTask,
4453
+ applyResumeTask,
4217
4454
  applyRunnerPaused,
4218
4455
  applyRuntimeParam,
4219
4456
  applyRuntimePatch,