@nmakarov/cli-toolkit 0.72.0 → 0.75.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/cli-runner.cjs +243 -4
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +243 -4
- package/dist/cli-runner.js.map +1 -1
- package/dist/index.cjs +242 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +238 -3
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +13 -1
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +13 -1
- package/dist/init.js.map +1 -1
- package/dist/tasks.cjs +240 -3
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +236 -3
- package/dist/tasks.js.map +1 -1
- package/package.json +1 -1
package/dist/cli-runner.cjs
CHANGED
|
@@ -2810,6 +2810,7 @@ function setup(opts = {}) {
|
|
|
2810
2810
|
const partialContext = {
|
|
2811
2811
|
emitter: new import_events.EventEmitter(),
|
|
2812
2812
|
isStop: () => false,
|
|
2813
|
+
isKill: () => false,
|
|
2813
2814
|
cleanupFunctions: [],
|
|
2814
2815
|
registerCleanup: (fn) => {
|
|
2815
2816
|
partialContext.cleanupFunctions.push(fn);
|
|
@@ -2831,6 +2832,7 @@ function setup(opts = {}) {
|
|
|
2831
2832
|
logger,
|
|
2832
2833
|
emitter: partialContext.emitter,
|
|
2833
2834
|
isStop: partialContext.isStop,
|
|
2835
|
+
isKill: partialContext.isKill,
|
|
2834
2836
|
cleanupFunctions: partialContext.cleanupFunctions,
|
|
2835
2837
|
registerCleanup: partialContext.registerCleanup,
|
|
2836
2838
|
// For long-running scripts (servers): with --showUsedParams=top, print
|
|
@@ -2880,6 +2882,7 @@ function printAllParameters(context) {
|
|
|
2880
2882
|
}
|
|
2881
2883
|
async function init(flow2, opts = {}) {
|
|
2882
2884
|
let stop = false;
|
|
2885
|
+
let kill = false;
|
|
2883
2886
|
let context = null;
|
|
2884
2887
|
let cleanupRan = false;
|
|
2885
2888
|
const runRegisteredCleanups = async (ctx) => {
|
|
@@ -2909,6 +2912,7 @@ async function init(flow2, opts = {}) {
|
|
|
2909
2912
|
}
|
|
2910
2913
|
context = setup(opts);
|
|
2911
2914
|
context.isStop = () => stop;
|
|
2915
|
+
context.isKill = () => kill;
|
|
2912
2916
|
context = await setupModules(context, opts);
|
|
2913
2917
|
const stopAfter = context.args.get("stopAfter");
|
|
2914
2918
|
const stopAllowanceSec = Number(context.params.get("stopAllowance", "number default 60"));
|
|
@@ -2947,6 +2951,14 @@ async function init(flow2, opts = {}) {
|
|
|
2947
2951
|
);
|
|
2948
2952
|
context.emitter.emit("stop", stopAllowanceMs);
|
|
2949
2953
|
});
|
|
2954
|
+
process.on("SIGUSR2", () => {
|
|
2955
|
+
if (!context || kill) return;
|
|
2956
|
+
kill = true;
|
|
2957
|
+
stop = true;
|
|
2958
|
+
context.logger.warn(">> SIGUSR2: emitting kill (urgent stop)");
|
|
2959
|
+
context.emitter.emit("kill");
|
|
2960
|
+
context.emitter.emit("stop", stopAllowanceMs);
|
|
2961
|
+
});
|
|
2950
2962
|
await flow2(context);
|
|
2951
2963
|
} catch (error) {
|
|
2952
2964
|
const errorLocation = error instanceof Error && error.stack ? error.stack.split("\n")[1]?.trim() || "Unknown location" : "Unknown location";
|
|
@@ -2973,7 +2985,7 @@ async function init(flow2, opts = {}) {
|
|
|
2973
2985
|
}
|
|
2974
2986
|
if (process.exitCode && process.exitCode !== 0) {
|
|
2975
2987
|
process.exit(process.exitCode);
|
|
2976
|
-
} else if (stop) {
|
|
2988
|
+
} else if (stop || kill) {
|
|
2977
2989
|
process.exit(0);
|
|
2978
2990
|
}
|
|
2979
2991
|
}
|
|
@@ -5871,6 +5883,8 @@ var AbstractTask = class _AbstractTask {
|
|
|
5871
5883
|
constructor(context, task) {
|
|
5872
5884
|
this.context = context;
|
|
5873
5885
|
this.task = task;
|
|
5886
|
+
this._stopRequested = false;
|
|
5887
|
+
this._pauseRequested = false;
|
|
5874
5888
|
}
|
|
5875
5889
|
/**
|
|
5876
5890
|
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
@@ -5888,6 +5902,26 @@ var AbstractTask = class _AbstractTask {
|
|
|
5888
5902
|
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
5889
5903
|
*/
|
|
5890
5904
|
requestStop(_allowanceMs) {
|
|
5905
|
+
this._stopRequested = true;
|
|
5906
|
+
}
|
|
5907
|
+
/**
|
|
5908
|
+
* Cooperative pause signal (from `pauseTask` control-lane task). Long-running
|
|
5909
|
+
* tasks should finish the current unit of work, persist a checkpoint, and
|
|
5910
|
+
* return `{ success: true, results: { taskPaused: true, … } }` so the runner
|
|
5911
|
+
* keeps the row as `status=paused` instead of deleting it.
|
|
5912
|
+
*
|
|
5913
|
+
* @param {number} [_allowanceMs]
|
|
5914
|
+
*/
|
|
5915
|
+
requestPause(_allowanceMs) {
|
|
5916
|
+
this._pauseRequested = true;
|
|
5917
|
+
}
|
|
5918
|
+
/** @returns {boolean} */
|
|
5919
|
+
isStopRequested() {
|
|
5920
|
+
return this._stopRequested === true;
|
|
5921
|
+
}
|
|
5922
|
+
/** @returns {boolean} */
|
|
5923
|
+
isPauseRequested() {
|
|
5924
|
+
return this._pauseRequested === true;
|
|
5891
5925
|
}
|
|
5892
5926
|
/**
|
|
5893
5927
|
* Perform the task. Must be implemented by subclasses.
|
|
@@ -6586,6 +6620,8 @@ var CONTROL_LANE_TASK_NAMES = [
|
|
|
6586
6620
|
"pause",
|
|
6587
6621
|
"unpauseRunner",
|
|
6588
6622
|
"unpause",
|
|
6623
|
+
"pauseTask",
|
|
6624
|
+
"resumeTask",
|
|
6589
6625
|
"setRuntimeParam",
|
|
6590
6626
|
"setRunnerParam"
|
|
6591
6627
|
];
|
|
@@ -6790,6 +6826,190 @@ var TaskUnpauseRunner = class extends AbstractTask {
|
|
|
6790
6826
|
}
|
|
6791
6827
|
};
|
|
6792
6828
|
|
|
6829
|
+
// src/tasks/coreTasks/TaskPauseTask.js
|
|
6830
|
+
function parseProgressObject(progress) {
|
|
6831
|
+
if (progress == null || progress === "") return {};
|
|
6832
|
+
if (typeof progress === "object" && !Array.isArray(progress)) {
|
|
6833
|
+
return { ...progress };
|
|
6834
|
+
}
|
|
6835
|
+
if (typeof progress === "string") {
|
|
6836
|
+
const t = progress.trim();
|
|
6837
|
+
if (t.startsWith("{") || t.startsWith("[")) {
|
|
6838
|
+
try {
|
|
6839
|
+
const parsed = JSON.parse(t);
|
|
6840
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
|
|
6841
|
+
} catch {
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
return { message: progress };
|
|
6845
|
+
}
|
|
6846
|
+
return { message: String(progress) };
|
|
6847
|
+
}
|
|
6848
|
+
async function applyPauseTask(context, taskId) {
|
|
6849
|
+
const db = context.db;
|
|
6850
|
+
if (!db) throw new Error("pauseTask: context.db is required");
|
|
6851
|
+
const id = String(taskId ?? "").trim();
|
|
6852
|
+
if (!id) throw new ParamError('pauseTask: param "taskId" is required');
|
|
6853
|
+
const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
|
|
6854
|
+
const row = await db(tasksTable).where({ id }).first();
|
|
6855
|
+
if (!row) {
|
|
6856
|
+
return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
|
|
6857
|
+
}
|
|
6858
|
+
if (row.status === "paused") {
|
|
6859
|
+
return {
|
|
6860
|
+
success: true,
|
|
6861
|
+
results: { taskId: id, status: "paused", message: "Task already paused" }
|
|
6862
|
+
};
|
|
6863
|
+
}
|
|
6864
|
+
if (row.status === "idle") {
|
|
6865
|
+
const progress = parseProgressObject(row.progress);
|
|
6866
|
+
progress.pauseRequested = true;
|
|
6867
|
+
progress.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6868
|
+
await db(tasksTable).where({ id }).update({
|
|
6869
|
+
status: "paused",
|
|
6870
|
+
status_changed_at: db.fn.now(),
|
|
6871
|
+
progress: toJsonColumn(progress)
|
|
6872
|
+
});
|
|
6873
|
+
context.logger?.warn?.(`[pauseTask] idle task ${id} (${row.name}) \u2192 paused`);
|
|
6874
|
+
return {
|
|
6875
|
+
success: true,
|
|
6876
|
+
results: { taskId: id, status: "paused", message: "Idle task paused" }
|
|
6877
|
+
};
|
|
6878
|
+
}
|
|
6879
|
+
if (row.status === "running") {
|
|
6880
|
+
const progress = parseProgressObject(row.progress);
|
|
6881
|
+
progress.pauseRequested = true;
|
|
6882
|
+
progress.pauseRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6883
|
+
await db(tasksTable).where({ id }).update({
|
|
6884
|
+
progress: toJsonColumn(progress)
|
|
6885
|
+
});
|
|
6886
|
+
const inst = context.runningTaskInstances?.get?.(id);
|
|
6887
|
+
if (inst && typeof inst.requestPause === "function") {
|
|
6888
|
+
await inst.requestPause();
|
|
6889
|
+
context.logger?.warn?.(
|
|
6890
|
+
`[pauseTask] signaled running task ${id} (${row.name}) to pause after current unit`
|
|
6891
|
+
);
|
|
6892
|
+
return {
|
|
6893
|
+
success: true,
|
|
6894
|
+
results: {
|
|
6895
|
+
taskId: id,
|
|
6896
|
+
status: "running",
|
|
6897
|
+
signaled: true,
|
|
6898
|
+
message: "Pause requested; task will finish current unit then park as paused"
|
|
6899
|
+
}
|
|
6900
|
+
};
|
|
6901
|
+
}
|
|
6902
|
+
context.logger?.warn?.(
|
|
6903
|
+
`[pauseTask] stamped pauseRequested on ${id} (${row.name}) \u2014 not running in this process`
|
|
6904
|
+
);
|
|
6905
|
+
return {
|
|
6906
|
+
success: true,
|
|
6907
|
+
results: {
|
|
6908
|
+
taskId: id,
|
|
6909
|
+
status: "running",
|
|
6910
|
+
signaled: false,
|
|
6911
|
+
message: "pauseRequested stamped on task row; target runner must observe it (enqueue pauseTask on that service)"
|
|
6912
|
+
}
|
|
6913
|
+
};
|
|
6914
|
+
}
|
|
6915
|
+
return {
|
|
6916
|
+
success: false,
|
|
6917
|
+
results: {
|
|
6918
|
+
error: `Cannot pause task in status "${row.status}"`,
|
|
6919
|
+
taskId: id,
|
|
6920
|
+
status: row.status
|
|
6921
|
+
}
|
|
6922
|
+
};
|
|
6923
|
+
}
|
|
6924
|
+
async function applyResumeTask(context, taskId) {
|
|
6925
|
+
const db = context.db;
|
|
6926
|
+
if (!db) throw new Error("resumeTask: context.db is required");
|
|
6927
|
+
const id = String(taskId ?? "").trim();
|
|
6928
|
+
if (!id) throw new ParamError('resumeTask: param "taskId" is required');
|
|
6929
|
+
const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
|
|
6930
|
+
const row = await db(tasksTable).where({ id }).first();
|
|
6931
|
+
if (!row) {
|
|
6932
|
+
return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
|
|
6933
|
+
}
|
|
6934
|
+
if (row.status === "idle") {
|
|
6935
|
+
return {
|
|
6936
|
+
success: true,
|
|
6937
|
+
results: { taskId: id, status: "idle", message: "Task already idle (runnable)" }
|
|
6938
|
+
};
|
|
6939
|
+
}
|
|
6940
|
+
if (row.status !== "paused") {
|
|
6941
|
+
return {
|
|
6942
|
+
success: false,
|
|
6943
|
+
results: {
|
|
6944
|
+
error: `Cannot resume task in status "${row.status}" (expected paused)`,
|
|
6945
|
+
taskId: id,
|
|
6946
|
+
status: row.status
|
|
6947
|
+
}
|
|
6948
|
+
};
|
|
6949
|
+
}
|
|
6950
|
+
const progress = parseProgressObject(row.progress);
|
|
6951
|
+
delete progress.pauseRequested;
|
|
6952
|
+
delete progress.pauseRequestedAt;
|
|
6953
|
+
delete progress.pausedAt;
|
|
6954
|
+
progress.resumedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6955
|
+
await db(tasksTable).where({ id }).update({
|
|
6956
|
+
status: "idle",
|
|
6957
|
+
status_changed_at: db.fn.now(),
|
|
6958
|
+
started_at: null,
|
|
6959
|
+
completed_at: null,
|
|
6960
|
+
success: null,
|
|
6961
|
+
progress: toJsonColumn(progress),
|
|
6962
|
+
// Clear claim binding so any matching worker can pick it up.
|
|
6963
|
+
service_name: null,
|
|
6964
|
+
server_name: null,
|
|
6965
|
+
instance_number: null,
|
|
6966
|
+
next_run_at: null
|
|
6967
|
+
});
|
|
6968
|
+
context.logger?.warn?.(`[resumeTask] paused task ${id} (${row.name}) \u2192 idle`);
|
|
6969
|
+
return {
|
|
6970
|
+
success: true,
|
|
6971
|
+
results: { taskId: id, status: "idle", message: "Task resumed (idle; will be claimed)" }
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
var TaskPauseTask = class extends AbstractTask {
|
|
6975
|
+
static taskName = "pauseTask";
|
|
6976
|
+
static description = "Pause a specific task: finish current unit, persist status=paused";
|
|
6977
|
+
static defaultWaitForResult = true;
|
|
6978
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
6979
|
+
const merged = AbstractTask._mergeTypedParams(
|
|
6980
|
+
context,
|
|
6981
|
+
"task-pause-task",
|
|
6982
|
+
{ taskId: "string" },
|
|
6983
|
+
overrides
|
|
6984
|
+
);
|
|
6985
|
+
const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
|
|
6986
|
+
if (!taskId) throw new ParamError('pauseTask: param "taskId" is required');
|
|
6987
|
+
return { taskId };
|
|
6988
|
+
}
|
|
6989
|
+
async run() {
|
|
6990
|
+
return applyPauseTask(this.context, this.task?.params?.taskId);
|
|
6991
|
+
}
|
|
6992
|
+
};
|
|
6993
|
+
var TaskResumeTask = class extends AbstractTask {
|
|
6994
|
+
static taskName = "resumeTask";
|
|
6995
|
+
static description = "Resume a paused task (status paused \u2192 idle)";
|
|
6996
|
+
static defaultWaitForResult = true;
|
|
6997
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
6998
|
+
const merged = AbstractTask._mergeTypedParams(
|
|
6999
|
+
context,
|
|
7000
|
+
"task-resume-task",
|
|
7001
|
+
{ taskId: "string" },
|
|
7002
|
+
overrides
|
|
7003
|
+
);
|
|
7004
|
+
const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
|
|
7005
|
+
if (!taskId) throw new ParamError('resumeTask: param "taskId" is required');
|
|
7006
|
+
return { taskId };
|
|
7007
|
+
}
|
|
7008
|
+
async run() {
|
|
7009
|
+
return applyResumeTask(this.context, this.task?.params?.taskId);
|
|
7010
|
+
}
|
|
7011
|
+
};
|
|
7012
|
+
|
|
6793
7013
|
// src/tasks/coreTasks/TaskGetLogs.js
|
|
6794
7014
|
var TaskGetLogs = class extends AbstractTask {
|
|
6795
7015
|
/**
|
|
@@ -7008,7 +7228,7 @@ var TasksRegistry = class _TasksRegistry {
|
|
|
7008
7228
|
* @returns {TasksRegistry}
|
|
7009
7229
|
*/
|
|
7010
7230
|
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);
|
|
7231
|
+
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
7232
|
}
|
|
7013
7233
|
/**
|
|
7014
7234
|
* Register a single task class under a name. Overwrites any previous entry.
|
|
@@ -7200,11 +7420,12 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
7200
7420
|
} finally {
|
|
7201
7421
|
runningTaskInstances.delete(row.id);
|
|
7202
7422
|
}
|
|
7423
|
+
const taskPaused = success && results && typeof results === "object" && results.taskPaused === true;
|
|
7203
7424
|
await db(historyTable).insert(
|
|
7204
7425
|
taskHistoryInsertFromQueueRow(row, {
|
|
7205
7426
|
completed_at: /* @__PURE__ */ new Date(),
|
|
7206
7427
|
success,
|
|
7207
|
-
status: success ? "completed" : "failed",
|
|
7428
|
+
status: taskPaused ? "paused" : success ? "completed" : "failed",
|
|
7208
7429
|
status_changed_at: db.fn.now(),
|
|
7209
7430
|
params: toJsonColumn(row.params),
|
|
7210
7431
|
results: toJsonColumn(results)
|
|
@@ -7227,7 +7448,24 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
7227
7448
|
details: results
|
|
7228
7449
|
});
|
|
7229
7450
|
}
|
|
7230
|
-
if (
|
|
7451
|
+
if (taskPaused) {
|
|
7452
|
+
const checkpointParams = results.checkpointParams && typeof results.checkpointParams === "object" ? results.checkpointParams : row.params;
|
|
7453
|
+
const progressPayload = results.progress != null ? results.progress : { paused: true, pausedAt: (/* @__PURE__ */ new Date()).toISOString(), message: "paused" };
|
|
7454
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
7455
|
+
started_at: null,
|
|
7456
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
7457
|
+
success: true,
|
|
7458
|
+
results: toJsonColumn(results),
|
|
7459
|
+
params: toJsonColumn(checkpointParams),
|
|
7460
|
+
progress: toJsonColumn(progressPayload),
|
|
7461
|
+
status: "paused",
|
|
7462
|
+
status_changed_at: db.fn.now(),
|
|
7463
|
+
past_due: null,
|
|
7464
|
+
service_name: null,
|
|
7465
|
+
server_name: null,
|
|
7466
|
+
instance_number: null
|
|
7467
|
+
});
|
|
7468
|
+
} else if (row.schedule) {
|
|
7231
7469
|
if (success) {
|
|
7232
7470
|
let nextRunAt = null;
|
|
7233
7471
|
try {
|
|
@@ -7355,6 +7593,7 @@ async function runTasksLoop(context, options) {
|
|
|
7355
7593
|
}
|
|
7356
7594
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
7357
7595
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
7596
|
+
context.runningTaskInstances = runningTaskInstances;
|
|
7358
7597
|
let runningControlPromise = null;
|
|
7359
7598
|
let stopRequested = false;
|
|
7360
7599
|
let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;
|