@nmakarov/cli-toolkit 0.49.0 → 0.50.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.
@@ -3985,6 +3985,51 @@ async function dropLegacyTaskNameColumn(db, tableNames, { dryRun, log, label })
3985
3985
  }
3986
3986
  return actions;
3987
3987
  }
3988
+ async function enqueueTask(context, options) {
3989
+ const db = getDb(context);
3990
+ const queueName = options.queueName ?? "tasks";
3991
+ const { tasksTable } = queueToTableNames(queueName);
3992
+ const id = (0, import_node_crypto.randomUUID)();
3993
+ const name = options.name ?? options.task;
3994
+ if (!name) {
3995
+ throw new Error("enqueueTask: name (or task) is required");
3996
+ }
3997
+ const schedule = options.schedule?.trim() ? options.schedule : null;
3998
+ let nextRunAt = null;
3999
+ if (options.nextRunAt !== void 0) {
4000
+ nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
4001
+ } else if (schedule) {
4002
+ nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
4003
+ }
4004
+ const row = {
4005
+ id,
4006
+ name,
4007
+ params: toJsonColumn(options.params ?? null),
4008
+ opid: options.opid ?? null,
4009
+ priority: options.priority ?? 50,
4010
+ schedule,
4011
+ next_run_at: nextRunAt,
4012
+ service_group: options.serviceGroup ?? null,
4013
+ instance_number: options.instanceNumber ?? null,
4014
+ service_name: options.serviceName ?? null,
4015
+ server_name: options.serverName ?? null,
4016
+ status: "idle",
4017
+ status_changed_at: db.fn.now()
4018
+ };
4019
+ if (await tableHasLegacyTaskColumn(db, tasksTable)) {
4020
+ row.task = name;
4021
+ }
4022
+ await db(tasksTable).insert(row);
4023
+ return id;
4024
+ }
4025
+ var legacyTaskColumnCache = /* @__PURE__ */ new Map();
4026
+ async function tableHasLegacyTaskColumn(db, tableName) {
4027
+ const key = `${db?.config?.name ?? "db"}:${tableName}`;
4028
+ if (!legacyTaskColumnCache.has(key)) {
4029
+ legacyTaskColumnCache.set(key, await db.schema.hasColumn(tableName, "task"));
4030
+ }
4031
+ return legacyTaskColumnCache.get(key);
4032
+ }
3988
4033
  async function updateTaskProgress(context, tasksTable, taskId, progress) {
3989
4034
  const db = getDb(context);
3990
4035
  await db(tasksTable).where({ id: taskId }).update({
@@ -4000,6 +4045,19 @@ function getDb2(context) {
4000
4045
  }
4001
4046
  return db;
4002
4047
  }
4048
+ function parseMetadataColumn(value) {
4049
+ if (!value) return {};
4050
+ if (typeof value === "object" && !Array.isArray(value)) return value;
4051
+ if (typeof value === "string") {
4052
+ try {
4053
+ const p = JSON.parse(value);
4054
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
4055
+ } catch {
4056
+ return {};
4057
+ }
4058
+ }
4059
+ return {};
4060
+ }
4003
4061
  var DEFAULT_GROUP_MAX_INSTANCES = {
4004
4062
  intake: 1,
4005
4063
  harvest: 1,
@@ -4198,11 +4256,33 @@ async function touchServicesRegistry(context, registration) {
4198
4256
  pid
4199
4257
  });
4200
4258
  }
4259
+ async function updateServicesRegistryMetadata(context, registration, patch) {
4260
+ const db = getDb2(context);
4261
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
4262
+ const prev = parseMetadataColumn(row?.metadata);
4263
+ const merged = { ...prev, ...patch };
4264
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4265
+ metadata: toJsonColumn(merged),
4266
+ last_seen_at: db.fn.now()
4267
+ });
4268
+ context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
4269
+ }
4201
4270
  async function unregisterServicesRegistry(context, registration) {
4202
4271
  const db = getDb2(context);
4203
4272
  await db(registration.registryTable).where({ id: registration.rowId }).delete();
4204
4273
  context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
4205
4274
  }
4275
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
4276
+ const db = getDb2(context);
4277
+ const staleMs = options.staleMs ?? 6e4;
4278
+ const cutoff = new Date(Date.now() - staleMs);
4279
+ const table = queueToTableNames(options.queueName).registryTable;
4280
+ let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
4281
+ if (options.serviceGroup?.trim()) {
4282
+ q = q.where({ service_group: options.serviceGroup.trim() });
4283
+ }
4284
+ return await q;
4285
+ }
4206
4286
 
4207
4287
  // src/tasks/taskLogs.js
4208
4288
  var import_node_path = __toESM(require("path"), 1);
@@ -6161,6 +6241,273 @@ var TaskGetLogs = class extends AbstractTask {
6161
6241
  }
6162
6242
  };
6163
6243
 
6244
+ // src/tasks/runtimeParams.js
6245
+ var LOOP_RUNTIME_KEYS = ["maxParallel", "pollMs", "claimJitterMs", "scanLimit"];
6246
+ var LOGGER_RUNTIME_KEYS = [
6247
+ "levels",
6248
+ "silent",
6249
+ "showLevel",
6250
+ "timestamp",
6251
+ "mode",
6252
+ "route",
6253
+ "prefix",
6254
+ "progressWithTimes",
6255
+ "progressThrottleMs"
6256
+ ];
6257
+ var CONTROL_LANE_TASK_NAMES = ["stopRunner", "stop", "setRuntimeParam", "setRunnerParam"];
6258
+ function controlLaneTaskNames() {
6259
+ return [...CONTROL_LANE_TASK_NAMES];
6260
+ }
6261
+ function asPositiveInt(value, key, { min = 1 } = {}) {
6262
+ const n = Number(value);
6263
+ if (!Number.isFinite(n) || n < min) {
6264
+ throw new ParamError(
6265
+ `setRuntimeParam: ${key} must be a number >= ${min} (got ${JSON.stringify(value)})`
6266
+ );
6267
+ }
6268
+ return Math.floor(n);
6269
+ }
6270
+ function coerceRuntimeValue(key, value) {
6271
+ switch (key) {
6272
+ case "maxParallel":
6273
+ return asPositiveInt(value, key, { min: 1 });
6274
+ case "pollMs":
6275
+ return asPositiveInt(value, key, { min: 50 });
6276
+ case "claimJitterMs":
6277
+ return asPositiveInt(value, key, { min: 0 });
6278
+ case "scanLimit":
6279
+ return asPositiveInt(value, key, { min: 1 });
6280
+ case "silent":
6281
+ case "showLevel":
6282
+ case "timestamp":
6283
+ case "progressWithTimes":
6284
+ if (typeof value === "boolean") return value;
6285
+ if (value === "true" || value === "1") return true;
6286
+ if (value === "false" || value === "0") return false;
6287
+ throw new ParamError(
6288
+ `setRuntimeParam: ${key} must be boolean (got ${JSON.stringify(value)})`
6289
+ );
6290
+ case "progressThrottleMs":
6291
+ return asPositiveInt(value, key, { min: 0 });
6292
+ case "levels":
6293
+ case "mode":
6294
+ case "route":
6295
+ case "prefix":
6296
+ return value;
6297
+ default:
6298
+ return value;
6299
+ }
6300
+ }
6301
+ function ensureTasksRuntime(context, seed = {}) {
6302
+ if (!context.tasksRuntime || typeof context.tasksRuntime !== "object") {
6303
+ context.tasksRuntime = {};
6304
+ }
6305
+ const rt = context.tasksRuntime;
6306
+ if (rt.maxParallel === void 0) rt.maxParallel = seed.maxParallel ?? 32;
6307
+ if (rt.pollMs === void 0) rt.pollMs = seed.pollMs ?? 1e3;
6308
+ if (rt.claimJitterMs === void 0) rt.claimJitterMs = seed.claimJitterMs ?? 0;
6309
+ if (rt.scanLimit === void 0) rt.scanLimit = seed.scanLimit ?? 100;
6310
+ return rt;
6311
+ }
6312
+ async function applyRuntimeParam(context, key, value) {
6313
+ const k = String(key ?? "").trim();
6314
+ if (!k) throw new ParamError("setRuntimeParam: key is required");
6315
+ const runtime = ensureTasksRuntime(context);
6316
+ const next = coerceRuntimeValue(k, value);
6317
+ const previous = runtime[k];
6318
+ const applied = [];
6319
+ runtime[k] = next;
6320
+ applied.push("tasksRuntime");
6321
+ if (LOGGER_RUNTIME_KEYS.includes(k) && context.logger?.configure) {
6322
+ context.logger.configure({ [k]: next });
6323
+ applied.push("logger");
6324
+ }
6325
+ const hook = context.tasksRuntimeOnParam;
6326
+ if (typeof hook === "function") {
6327
+ await hook(k, next, runtime, context);
6328
+ applied.push("onRuntimeParam");
6329
+ }
6330
+ const reg = context.servicesRegistry;
6331
+ if (reg?.rowId && reg?.registryTable) {
6332
+ try {
6333
+ const loopSnapshot = {};
6334
+ for (const lk of LOOP_RUNTIME_KEYS) {
6335
+ if (runtime[lk] !== void 0) loopSnapshot[lk] = runtime[lk];
6336
+ }
6337
+ await updateServicesRegistryMetadata(context, reg, {
6338
+ runtime: loopSnapshot,
6339
+ runtimeUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
6340
+ });
6341
+ applied.push("servicesRegistry");
6342
+ } catch (err) {
6343
+ context.logger?.warn?.(
6344
+ `[setRuntimeParam] registry metadata update failed: ${err?.message ?? String(err)}`
6345
+ );
6346
+ }
6347
+ }
6348
+ return { key: k, previous, next, applied };
6349
+ }
6350
+ async function applyRuntimePatch(context, patch) {
6351
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
6352
+ throw new ParamError("setRuntimeParam: patch must be a plain object");
6353
+ }
6354
+ const entries = Object.entries(patch);
6355
+ if (entries.length === 0) {
6356
+ throw new ParamError("setRuntimeParam: patch is empty");
6357
+ }
6358
+ const out = [];
6359
+ for (const [key, value] of entries) {
6360
+ out.push(await applyRuntimeParam(context, key, value));
6361
+ }
6362
+ return out;
6363
+ }
6364
+ function readLoopRuntime(context) {
6365
+ const rt = ensureTasksRuntime(context);
6366
+ return {
6367
+ maxParallel: Math.max(1, Number(rt.maxParallel) || 1),
6368
+ pollMs: Math.max(50, Number(rt.pollMs) || 1e3),
6369
+ claimJitterMs: Math.max(0, Number(rt.claimJitterMs) || 0),
6370
+ scanLimit: Math.max(1, Number(rt.scanLimit) || 100)
6371
+ };
6372
+ }
6373
+
6374
+ // src/tasks/coreTasks/TaskSetRuntimeParam.js
6375
+ var TaskSetRuntimeParam = class extends AbstractTask {
6376
+ static defaultWaitForResult = true;
6377
+ /**
6378
+ * @param {object} context
6379
+ * @param {Record<string, unknown>} [overrides]
6380
+ * @returns {Promise<object>}
6381
+ */
6382
+ static async resolveParams(context, overrides = {}) {
6383
+ const main = await super.resolveParams(context, overrides);
6384
+ if (!main.serviceName && !main.serviceGroup) {
6385
+ throw new ParamError(
6386
+ "setRuntimeParam requires --serviceName (one instance) or --serviceGroup (broadcast to alive instances)"
6387
+ );
6388
+ }
6389
+ return main;
6390
+ }
6391
+ /**
6392
+ * @param {object} context
6393
+ * @param {Record<string, unknown>} [overrides]
6394
+ * @returns {Promise<{ key?: string, value?: unknown, patch?: Record<string, unknown> }>}
6395
+ */
6396
+ static async resolveCustomParams(context, overrides = {}) {
6397
+ const merged = AbstractTask._mergeTypedParams(
6398
+ context,
6399
+ "task-set-runtime-param",
6400
+ {
6401
+ paramKey: "string",
6402
+ paramValue: "string",
6403
+ key: "string",
6404
+ value: "string"
6405
+ },
6406
+ overrides
6407
+ );
6408
+ if (merged.patch && typeof merged.patch === "object" && !Array.isArray(merged.patch)) {
6409
+ if (Object.keys(merged.patch).length === 0) {
6410
+ throw new ParamError("setRuntimeParam: patch is empty");
6411
+ }
6412
+ return { patch: { ...merged.patch } };
6413
+ }
6414
+ const key = merged.paramKey || merged.key;
6415
+ const value = merged.paramValue !== void 0 ? merged.paramValue : merged.value;
6416
+ if (!key) {
6417
+ throw new ParamError(
6418
+ `setRuntimeParam requires --paramKey/--paramValue, or --paramsJson '{"key":"maxParallel","value":16}' / '{"patch":{...}}'`
6419
+ );
6420
+ }
6421
+ let parsed = value;
6422
+ if (typeof value === "string") {
6423
+ const t = value.trim();
6424
+ if (t === "true") parsed = true;
6425
+ else if (t === "false") parsed = false;
6426
+ else if (t !== "" && !Number.isNaN(Number(t)) && /^-?\d+(\.\d+)?$/.test(t)) {
6427
+ parsed = Number(t);
6428
+ } else if (t.startsWith("{") && t.endsWith("}") || t.startsWith("[") && t.endsWith("]") || t.startsWith('"') && t.endsWith('"')) {
6429
+ try {
6430
+ parsed = JSON.parse(t);
6431
+ } catch {
6432
+ parsed = value;
6433
+ }
6434
+ }
6435
+ }
6436
+ return { key: String(key), value: parsed };
6437
+ }
6438
+ /**
6439
+ * Enqueue one or many setRuntimeParam tasks. Prefer this over a bare
6440
+ * `enqueueTask` when broadcasting to a service group.
6441
+ *
6442
+ * @param {object} context
6443
+ * @param {Record<string, unknown>} [overrides]
6444
+ * @returns {Promise<{ ids: string[], targets: string[] }>}
6445
+ */
6446
+ static async enqueue(context, overrides = {}) {
6447
+ const payload = await this.resolveParams(context, { ...overrides, name: "setRuntimeParam" });
6448
+ const queueName = payload.queueName ?? "tasks";
6449
+ if (payload.serviceName) {
6450
+ const id = await enqueueTask(context, payload);
6451
+ return { ids: [id], targets: [payload.serviceName] };
6452
+ }
6453
+ const group = String(payload.serviceGroup || "").trim();
6454
+ if (!group) {
6455
+ throw new ParamError("setRuntimeParam.enqueue: serviceGroup required for broadcast");
6456
+ }
6457
+ const alive = await listServicesRegistry(context, {
6458
+ queueName,
6459
+ serviceGroup: group,
6460
+ staleMs: overrides.staleMs ?? 45e3
6461
+ });
6462
+ if (!alive.length) {
6463
+ throw new ParamError(
6464
+ `setRuntimeParam: no alive services in group="${group}" queue="${queueName}"`
6465
+ );
6466
+ }
6467
+ const ids = [];
6468
+ const targets = [];
6469
+ for (const reg of alive) {
6470
+ const id = await enqueueTask(context, {
6471
+ ...payload,
6472
+ serviceGroup: group,
6473
+ serviceName: reg.service_name,
6474
+ serverName: reg.server_name ?? null,
6475
+ instanceNumber: reg.instance_number ?? null
6476
+ });
6477
+ ids.push(id);
6478
+ targets.push(reg.service_name);
6479
+ }
6480
+ context.logger?.info?.(
6481
+ `[setRuntimeParam] broadcast to ${targets.length} instance(s) in group=${group}: ${targets.join(", ")}`
6482
+ );
6483
+ return { ids, targets };
6484
+ }
6485
+ /**
6486
+ * @returns {Promise<{ success: true, results: object }>}
6487
+ */
6488
+ async run() {
6489
+ const params = this.task?.params ?? {};
6490
+ let changes;
6491
+ if (params.patch && typeof params.patch === "object") {
6492
+ changes = await applyRuntimePatch(this.context, params.patch);
6493
+ } else {
6494
+ changes = [await applyRuntimeParam(this.context, params.key, params.value)];
6495
+ }
6496
+ const summary = changes.map((c) => `${c.key}: ${JSON.stringify(c.previous)} \u2192 ${JSON.stringify(c.next)}`);
6497
+ this.context.logger.warn?.(
6498
+ `[TaskSetRuntimeParam] applied on ${this.context.servicesRegistry?.serviceName ?? "runner"}: ${summary.join("; ")}`
6499
+ );
6500
+ return {
6501
+ success: true,
6502
+ results: {
6503
+ runtimeParamApplied: true,
6504
+ changes,
6505
+ runtime: { ...this.context.tasksRuntime ?? {} }
6506
+ }
6507
+ };
6508
+ }
6509
+ };
6510
+
6164
6511
  // src/tasks/TasksRegistry.js
6165
6512
  var TasksRegistry = class _TasksRegistry {
6166
6513
  /**
@@ -6179,7 +6526,7 @@ var TasksRegistry = class _TasksRegistry {
6179
6526
  * @returns {TasksRegistry}
6180
6527
  */
6181
6528
  static withCoreTasks() {
6182
- 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("getLogs", TaskGetLogs);
6529
+ 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("getLogs", TaskGetLogs).add("setRuntimeParam", TaskSetRuntimeParam).add("setRunnerParam", TaskSetRuntimeParam);
6183
6530
  }
6184
6531
  /**
6185
6532
  * Register a single task class under a name. Overwrites any previous entry.
@@ -6508,18 +6855,23 @@ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry
6508
6855
  async function runTasksLoop(context, options) {
6509
6856
  const queueName = options.queueName ?? "tasks";
6510
6857
  const target = options.target;
6511
- const pollMs = options.pollMs ?? 1e3;
6512
- const claimJitterMs = options.claimJitterMs ?? 0;
6513
- const maxParallel = options.maxParallel ?? 32;
6514
- const scanLimit = options.scanLimit ?? 100;
6515
6858
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
6516
6859
  const registry = normalizeRegistry(options.registry);
6517
6860
  const { tasksTable, historyTable } = queueToTableNames(queueName);
6518
6861
  if (!target) throw new Error("runTasksLoop: target is required");
6519
6862
  context.tasksQueueName = queueName;
6863
+ ensureTasksRuntime(context, {
6864
+ maxParallel: options.maxParallel ?? 32,
6865
+ pollMs: options.pollMs ?? 1e3,
6866
+ claimJitterMs: options.claimJitterMs ?? 0,
6867
+ scanLimit: options.scanLimit ?? 100
6868
+ });
6869
+ if (typeof options.onRuntimeParam === "function") {
6870
+ context.tasksRuntimeOnParam = options.onRuntimeParam;
6871
+ }
6520
6872
  const runningPromises = /* @__PURE__ */ new Set();
6521
6873
  const runningTaskInstances = /* @__PURE__ */ new Map();
6522
- let runningStopControlPromise = null;
6874
+ let runningControlPromise = null;
6523
6875
  let stopRequested = false;
6524
6876
  let stopAllowanceMs = 5e3;
6525
6877
  context.tasksRunnerStop = false;
@@ -6530,9 +6882,16 @@ async function runTasksLoop(context, options) {
6530
6882
  if (hbGroup) {
6531
6883
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
6532
6884
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
6885
+ const loop0 = readLoopRuntime(context);
6533
6886
  const defaultMeta = {
6534
6887
  component: "tasks-runner",
6535
- allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
6888
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all",
6889
+ runtime: {
6890
+ maxParallel: loop0.maxParallel,
6891
+ pollMs: loop0.pollMs,
6892
+ claimJitterMs: loop0.claimJitterMs,
6893
+ scanLimit: loop0.scanLimit
6894
+ }
6536
6895
  };
6537
6896
  registryReg = await registerInServicesRegistry(context, {
6538
6897
  queueName,
@@ -6545,6 +6904,7 @@ async function runTasksLoop(context, options) {
6545
6904
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
6546
6905
  metadata: options.runnerMetadata ?? defaultMeta
6547
6906
  });
6907
+ context.servicesRegistry = registryReg;
6548
6908
  runnerIdentity = {
6549
6909
  service_name: registryReg.serviceName,
6550
6910
  server_name: import_node_os3.default.hostname(),
@@ -6558,22 +6918,23 @@ async function runTasksLoop(context, options) {
6558
6918
  }
6559
6919
  try {
6560
6920
  while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
6561
- if (!runningStopControlPromise) {
6562
- const claimedStopTask = await claimNextRunnableTask(
6921
+ const { maxParallel, pollMs, claimJitterMs, scanLimit } = readLoopRuntime(context);
6922
+ if (!runningControlPromise) {
6923
+ const claimedControlTask = await claimNextRunnableTask(
6563
6924
  context,
6564
6925
  tasksTable,
6565
6926
  target,
6566
6927
  registry,
6567
6928
  10,
6568
- ["stopRunner", "stop"],
6929
+ controlLaneTaskNames(),
6569
6930
  runnerIdentity
6570
6931
  );
6571
- if (claimedStopTask) {
6572
- runningStopControlPromise = executeClaimedTask(
6932
+ if (claimedControlTask) {
6933
+ runningControlPromise = executeClaimedTask(
6573
6934
  context,
6574
6935
  tasksTable,
6575
6936
  historyTable,
6576
- claimedStopTask,
6937
+ claimedControlTask,
6577
6938
  registry,
6578
6939
  runningTaskInstances
6579
6940
  ).then(async (outcome) => {
@@ -6584,7 +6945,7 @@ async function runTasksLoop(context, options) {
6584
6945
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
6585
6946
  }
6586
6947
  }).finally(() => {
6587
- runningStopControlPromise = null;
6948
+ runningControlPromise = null;
6588
6949
  });
6589
6950
  }
6590
6951
  }
@@ -6615,8 +6976,8 @@ async function runTasksLoop(context, options) {
6615
6976
  runningPromises.add(p);
6616
6977
  }
6617
6978
  const wakePromises = [...runningPromises];
6618
- if (runningStopControlPromise) {
6619
- wakePromises.push(runningStopControlPromise);
6979
+ if (runningControlPromise) {
6980
+ wakePromises.push(runningControlPromise);
6620
6981
  }
6621
6982
  if (wakePromises.length === 0) {
6622
6983
  await sleepMs(pollMs);