@nmakarov/cli-toolkit 0.69.0 → 0.71.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.
@@ -94,9 +94,48 @@ var init_components = __esm({
94
94
  }
95
95
  });
96
96
 
97
+ // src/screen/scrollbar.js
98
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
99
+ const view = Math.max(1, Math.floor(viewportRows));
100
+ const total = Math.max(0, Math.floor(totalLines));
101
+ if (total <= view) return null;
102
+ const maxScroll = total - view;
103
+ const thumbSize = Math.max(1, Math.round(view / total * view));
104
+ const travel = Math.max(0, view - thumbSize);
105
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
106
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
107
+ const glyphs = [];
108
+ for (let i = 0; i < view; i++) {
109
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
110
+ }
111
+ return glyphs;
112
+ }
113
+ var BAR_THUMB, BAR_TRACK, PAGE_SCROLL_KEY_BINDINGS;
114
+ var init_scrollbar = __esm({
115
+ "src/screen/scrollbar.js"() {
116
+ BAR_THUMB = "#";
117
+ BAR_TRACK = "|";
118
+ PAGE_SCROLL_KEY_BINDINGS = [
119
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
120
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
121
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
122
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
123
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
124
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
125
+ ];
126
+ }
127
+ });
128
+
97
129
  // src/screen/list-components.js
98
130
  import React, { useState, useEffect, useRef, createElement } from "react";
99
131
  import { Box as Box2, Text as Text2 } from "ink";
132
+ function clipNameForScrollbar(name, needsBar) {
133
+ if (!needsBar || name == null) return name;
134
+ const s = String(name);
135
+ if (s.length <= 1) return s;
136
+ if (/\s$/.test(s)) return s.slice(0, -1);
137
+ return s;
138
+ }
100
139
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
101
140
  const [, forceUpdate] = useState({});
102
141
  const termWidth = (process.stdout.columns || 80) - 8;
@@ -310,17 +349,42 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
310
349
  });
311
350
  ctx.setAction("scrollUp", () => {
312
351
  const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
313
- const newScrollOffset = Math.max(0, currentScrollOffset - 1);
314
- setScrollOffset(newScrollOffset);
352
+ setScrollOffset(Math.max(0, currentScrollOffset - 1));
315
353
  forceUpdate({});
316
354
  });
317
355
  ctx.setAction("scrollDown", () => {
318
356
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
319
357
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
320
- const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
321
- setScrollOffset(newScrollOffset);
358
+ setScrollOffset(Math.min(currentMaxScrollOffset, currentScrollOffset + 1));
359
+ forceUpdate({});
360
+ });
361
+ ctx.setAction("pageUp", () => {
362
+ const { maxHeight: vh } = scrollStateRef.current;
363
+ const page = Math.max(1, vh || 1);
364
+ const newIndex = Math.max(0, selectedIndexRef.current - page);
365
+ selectedIndexRef.current = newIndex;
366
+ const list = displayItemsRef.current;
367
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
368
+ setScrollOffset(newIndex);
369
+ forceUpdate({});
370
+ });
371
+ ctx.setAction("pageDown", () => {
372
+ const { maxHeight: vh, totalItems } = scrollStateRef.current;
373
+ const page = Math.max(1, vh || 1);
374
+ const maxIndex = Math.max(0, totalItems - 1);
375
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + page);
376
+ selectedIndexRef.current = newIndex;
377
+ const list = displayItemsRef.current;
378
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
379
+ const maxScroll = Math.max(0, totalItems - page);
380
+ setScrollOffset(Math.min(maxScroll, Math.max(0, newIndex - page + 1)));
322
381
  forceUpdate({});
323
382
  });
383
+ const navBindings = [
384
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
385
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
386
+ ...PAGE_SCROLL_KEY_BINDINGS
387
+ ];
324
388
  if (sortable) {
325
389
  ctx.setAction("toggleSort", () => {
326
390
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
@@ -367,8 +431,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
367
431
  }
368
432
  };
369
433
  ctx.setKeyBinding([
370
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
371
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
434
+ ...navBindings,
372
435
  {
373
436
  key: "s",
374
437
  caption: sortCaption,
@@ -378,47 +441,47 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
378
441
  ]);
379
442
  ctx.update();
380
443
  } else {
381
- ctx.setKeyBinding([
382
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
383
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
384
- ]);
444
+ ctx.setKeyBinding(navBindings);
385
445
  }
386
446
  }, [sortOrder, sortable]);
387
447
  const selectedIndex = selectedIndexRef.current;
448
+ const needsBar = displayItems.length > effectiveMaxHeight;
449
+ const barGlyphs = needsBar ? scrollbarGlyphs(effectiveMaxHeight, displayItems.length, clampedScrollOffset) : null;
450
+ const appendBar = (rowContent, displayIndex) => {
451
+ if (!barGlyphs) return rowContent;
452
+ const glyph = barGlyphs[displayIndex] ?? BAR_TRACK;
453
+ return h2(
454
+ Box2,
455
+ { flexDirection: "row" },
456
+ rowContent,
457
+ h2(Text2, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
458
+ );
459
+ };
388
460
  const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
389
461
  const isFirstVisible = displayIndex === 0;
390
462
  const isLastVisible = displayIndex === visibleItems.length - 1;
391
- let arrowPrefix = "";
392
- let selectionPrefix = "";
393
- if (isFirstVisible && canScrollUp) {
394
- arrowPrefix = "\u2191 ";
395
- } else if (isLastVisible && canScrollDown) {
396
- arrowPrefix = "\u2193 ";
397
- } else {
398
- arrowPrefix = " ";
399
- }
400
- if (isSelected) {
401
- selectionPrefix = selectionMarker;
402
- } else {
403
- selectionPrefix = " ".repeat(selectionMarker.length);
463
+ let arrowPrefix = " ";
464
+ if (!needsBar) {
465
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
466
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
404
467
  }
468
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
469
+ const label = clipNameForScrollbar(item.name, needsBar);
405
470
  return h2(
406
471
  Box2,
407
472
  { flexDirection: "row" },
408
- // Arrow (clickable if functional, not highlighted)
409
- h2(Text2, {
410
- key: `arrow-${actualIndex}`,
411
- color: "white"
412
- }, arrowPrefix),
413
- // Selection marker space (always same width, not highlighted)
473
+ h2(Text2, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
414
474
  h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
415
- // Item name (highlighted if selected)
416
- h2(Text2, {
417
- key: `name-${actualIndex}`,
418
- color: isSelected ? "black" : "white",
419
- backgroundColor: isSelected ? "cyan" : void 0,
420
- bold: isSelected
421
- }, item.name)
475
+ h2(
476
+ Text2,
477
+ {
478
+ key: `name-${actualIndex}`,
479
+ color: isSelected ? "black" : "white",
480
+ backgroundColor: isSelected ? "cyan" : void 0,
481
+ bold: isSelected
482
+ },
483
+ label
484
+ )
422
485
  );
423
486
  };
424
487
  const itemRenderer = renderItem || defaultRenderItem;
@@ -431,42 +494,32 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
431
494
  if (renderItem) {
432
495
  const isFirstVisible = displayIndex === 0;
433
496
  const isLastVisible = displayIndex === visibleItems.length - 1;
434
- let arrowPrefix = "";
435
- let selectionPrefix = "";
436
- if (isFirstVisible && canScrollUp) {
437
- arrowPrefix = "\u2191 ";
438
- } else if (isLastVisible && canScrollDown) {
439
- arrowPrefix = "\u2193 ";
440
- } else {
441
- arrowPrefix = " ";
442
- }
443
- if (isSelected) {
444
- selectionPrefix = selectionMarker;
445
- } else {
446
- selectionPrefix = " ".repeat(selectionMarker.length);
497
+ let arrowPrefix = " ";
498
+ if (!needsBar) {
499
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
500
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
447
501
  }
502
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
503
+ const clipped = needsBar ? { ...item, name: clipNameForScrollbar(item.name, true) } : item;
504
+ const row = h2(
505
+ Box2,
506
+ { flexDirection: "row" },
507
+ h2(Text2, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
508
+ h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
509
+ renderItem(clipped, isSelected, displayIndex)
510
+ );
448
511
  return h2(ScreenRow, {
449
512
  key: `item-${actualIndex}`,
450
- children: h2(
451
- Box2,
452
- { flexDirection: "row" },
453
- // Arrow (clickable if functional, not highlighted)
454
- h2(Text2, {
455
- key: `arrow-${actualIndex}`,
456
- color: "white"
457
- }, arrowPrefix),
458
- // Selection marker space (always same width, not highlighted)
459
- h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
460
- // Custom rendered content
461
- renderItem(item, isSelected, displayIndex)
462
- )
463
- });
464
- } else {
465
- return h2(ScreenRow, {
466
- key: `item-${actualIndex}`,
467
- children: itemRenderer(item, isSelected, displayIndex, actualIndex)
513
+ children: appendBar(row, displayIndex)
468
514
  });
469
515
  }
516
+ return h2(ScreenRow, {
517
+ key: `item-${actualIndex}`,
518
+ children: appendBar(
519
+ itemRenderer(item, isSelected, displayIndex, actualIndex),
520
+ displayIndex
521
+ )
522
+ });
470
523
  })
471
524
  );
472
525
  }
@@ -474,6 +527,7 @@ var h2;
474
527
  var init_list_components = __esm({
475
528
  "src/screen/list-components.js"() {
476
529
  init_components();
530
+ init_scrollbar();
477
531
  h2 = createElement;
478
532
  }
479
533
  });
@@ -968,21 +1022,6 @@ function wrapTextLines(text, cols) {
968
1022
  }
969
1023
  return out;
970
1024
  }
971
- function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
972
- const view = Math.max(1, Math.floor(viewportRows));
973
- const total = Math.max(0, Math.floor(totalLines));
974
- if (total <= view) return null;
975
- const maxScroll = total - view;
976
- const thumbSize = Math.max(1, Math.round(view / total * view));
977
- const travel = Math.max(0, view - thumbSize);
978
- const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
979
- const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
980
- const glyphs = [];
981
- for (let i = 0; i < view; i++) {
982
- glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
983
- }
984
- return glyphs;
985
- }
986
1025
  function padEndVisible(s, w) {
987
1026
  const t = String(s ?? "");
988
1027
  if (t.length >= w) return t.slice(0, w);
@@ -1083,22 +1122,17 @@ function ScrollableText({
1083
1122
  ...rowNodes
1084
1123
  );
1085
1124
  }
1086
- var h5, BAR_THUMB, BAR_TRACK, SCROLL_KEYS;
1125
+ var h5, SCROLL_KEYS;
1087
1126
  var init_scrollable_text = __esm({
1088
1127
  "src/screen/scrollable-text.js"() {
1089
1128
  init_components();
1129
+ init_scrollbar();
1130
+ init_scrollbar();
1090
1131
  h5 = createElement2;
1091
- BAR_THUMB = "#";
1092
- BAR_TRACK = "|";
1093
1132
  SCROLL_KEYS = [
1094
1133
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
1095
1134
  { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
1096
- { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
1097
- { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
1098
- { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
1099
- { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
1100
- { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
1101
- { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
1135
+ ...PAGE_SCROLL_KEY_BINDINGS
1102
1136
  ];
1103
1137
  }
1104
1138
  });
@@ -1234,6 +1268,8 @@ var init_footer_builder = __esm({
1234
1268
  // src/screen/index.js
1235
1269
  var screen_exports = {};
1236
1270
  __export(screen_exports, {
1271
+ BAR_THUMB: () => BAR_THUMB,
1272
+ BAR_TRACK: () => BAR_TRACK,
1237
1273
  Box: () => Box5,
1238
1274
  Divider: () => Divider,
1239
1275
  FooterPresets: () => FooterPresets,
@@ -1243,6 +1279,7 @@ __export(screen_exports, {
1243
1279
  ListItem: () => ListItem,
1244
1280
  MultiColumnListComponent: () => MultiColumnListComponent,
1245
1281
  MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1282
+ PAGE_SCROLL_KEY_BINDINGS: () => PAGE_SCROLL_KEY_BINDINGS,
1246
1283
  React: () => React2,
1247
1284
  ScreenBody: () => ScreenBody,
1248
1285
  ScreenContainer: () => ScreenContainer,
@@ -1298,6 +1335,7 @@ var init_screen = __esm({
1298
1335
  init_components();
1299
1336
  init_ui_elements();
1300
1337
  init_scrollable_text();
1338
+ init_scrollbar();
1301
1339
  init_key_bindings();
1302
1340
  init_utils();
1303
1341
  init_footer_builder();
@@ -4150,12 +4188,13 @@ function tasksSchemaSpec(queueName = "tasks") {
4150
4188
  }
4151
4189
  };
4152
4190
  }
4153
- function taskHistoryInsertFromQueueRow(row, overrides) {
4191
+ function taskHistoryInsertFromQueueRow(row, overrides = {}) {
4154
4192
  const { id, ...snapshot } = row;
4155
- void id;
4193
+ const opid = overrides.opid !== void 0 ? overrides.opid : snapshot.opid != null && String(snapshot.opid).trim() !== "" ? snapshot.opid : id;
4156
4194
  return {
4157
4195
  ...snapshot,
4158
- ...overrides
4196
+ ...overrides,
4197
+ opid
4159
4198
  };
4160
4199
  }
4161
4200
  async function ensureTaskTables(context, options = {}) {
@@ -6455,69 +6494,6 @@ var TaskStopRunner = class extends AbstractTask {
6455
6494
  }
6456
6495
  };
6457
6496
 
6458
- // src/tasks/coreTasks/TaskGetLogs.js
6459
- var TaskGetLogs = class extends AbstractTask {
6460
- /**
6461
- * @param {object} context
6462
- * @param {Record<string, unknown>} [overrides]
6463
- * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
6464
- */
6465
- static async resolveCustomParams(context, overrides = {}) {
6466
- const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
6467
- source: "string",
6468
- resource: "string",
6469
- tail: "number default 100",
6470
- afterTs: "string"
6471
- }, overrides);
6472
- const source = typeof merged.source === "string" ? merged.source.trim() : "";
6473
- const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
6474
- if (!source) throw new ParamError('getLogs: param "source" is required');
6475
- if (!resource) throw new ParamError('getLogs: param "resource" is required');
6476
- let tail = Number(merged.tail);
6477
- if (!Number.isFinite(tail) || tail < 1) tail = 100;
6478
- tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
6479
- const out = { source, resource, tail };
6480
- if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
6481
- out.afterTs = merged.afterTs.trim();
6482
- }
6483
- return out;
6484
- }
6485
- /**
6486
- * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
6487
- * @returns {Promise<{ success: boolean, results: unknown }>}
6488
- */
6489
- async run(_reportProgress) {
6490
- const p = this.task.params ?? {};
6491
- const source = String(p.source ?? "").trim();
6492
- const resource = String(p.resource ?? "").trim();
6493
- const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
6494
- const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
6495
- if (!source || !resource) {
6496
- return {
6497
- success: false,
6498
- results: { error: 'getLogs requires params "source" and "resource"' }
6499
- };
6500
- }
6501
- try {
6502
- const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
6503
- source,
6504
- resource,
6505
- tail,
6506
- afterTs
6507
- });
6508
- return {
6509
- success: true,
6510
- results: { records, latestTs, source, resource }
6511
- };
6512
- } catch (e) {
6513
- return {
6514
- success: false,
6515
- results: { error: e?.message ?? String(e) }
6516
- };
6517
- }
6518
- }
6519
- };
6520
-
6521
6497
  // src/tasks/runtimeParams.js
6522
6498
  var LOOP_RUNTIME_KEYS = ["maxParallel", "pollMs", "claimJitterMs", "scanLimit"];
6523
6499
  var LOGGER_RUNTIME_KEYS = [
@@ -6531,7 +6507,69 @@ var LOGGER_RUNTIME_KEYS = [
6531
6507
  "progressWithTimes",
6532
6508
  "progressThrottleMs"
6533
6509
  ];
6534
- var CONTROL_LANE_TASK_NAMES = ["stopRunner", "stop", "setRuntimeParam", "setRunnerParam"];
6510
+ var DEFAULT_RUNTIME_PARAM_SPECS = [
6511
+ {
6512
+ key: "maxParallel",
6513
+ type: "number",
6514
+ label: "Max parallel",
6515
+ description: "Worker-lane concurrency (how many tasks claim at once)"
6516
+ },
6517
+ {
6518
+ key: "pollMs",
6519
+ type: "number",
6520
+ label: "Poll ms",
6521
+ description: "Idle poll interval between claim attempts"
6522
+ },
6523
+ {
6524
+ key: "claimJitterMs",
6525
+ type: "number",
6526
+ label: "Claim jitter ms",
6527
+ description: "Random delay before worker claims (0 = off)"
6528
+ },
6529
+ {
6530
+ key: "scanLimit",
6531
+ type: "number",
6532
+ label: "Scan limit",
6533
+ description: "Max idle rows scanned per claim attempt"
6534
+ }
6535
+ ];
6536
+ function mergeRuntimeParamSpecs(extra) {
6537
+ const byKey = new Map(DEFAULT_RUNTIME_PARAM_SPECS.map((s) => [s.key, { ...s }]));
6538
+ if (Array.isArray(extra)) {
6539
+ for (const raw of extra) {
6540
+ if (!raw || typeof raw !== "object") continue;
6541
+ const key = String(raw.key ?? "").trim();
6542
+ if (!key) continue;
6543
+ const prev = byKey.get(key) ?? {};
6544
+ const type = ["number", "boolean", "string"].includes(raw.type) ? raw.type : prev.type ?? "string";
6545
+ byKey.set(key, {
6546
+ key,
6547
+ type,
6548
+ label: String(raw.label ?? prev.label ?? key),
6549
+ description: raw.description != null ? String(raw.description) : prev.description != null ? String(prev.description) : void 0
6550
+ });
6551
+ }
6552
+ }
6553
+ return Array.from(byKey.values());
6554
+ }
6555
+ function runtimeValuesForSpecs(context, specs = DEFAULT_RUNTIME_PARAM_SPECS) {
6556
+ const rt = ensureTasksRuntime(context);
6557
+ const out = {};
6558
+ for (const s of specs) {
6559
+ if (rt[s.key] !== void 0) out[s.key] = rt[s.key];
6560
+ }
6561
+ return out;
6562
+ }
6563
+ var CONTROL_LANE_TASK_NAMES = [
6564
+ "stopRunner",
6565
+ "stop",
6566
+ "pauseRunner",
6567
+ "pause",
6568
+ "unpauseRunner",
6569
+ "unpause",
6570
+ "setRuntimeParam",
6571
+ "setRunnerParam"
6572
+ ];
6535
6573
  function controlLaneTaskNames(extra) {
6536
6574
  const names = [...CONTROL_LANE_TASK_NAMES];
6537
6575
  if (extra == null || extra === "") return names;
@@ -6594,6 +6632,7 @@ function ensureTasksRuntime(context, seed = {}) {
6594
6632
  if (rt.pollMs === void 0) rt.pollMs = seed.pollMs ?? 1e3;
6595
6633
  if (rt.claimJitterMs === void 0) rt.claimJitterMs = seed.claimJitterMs ?? 0;
6596
6634
  if (rt.scanLimit === void 0) rt.scanLimit = seed.scanLimit ?? 100;
6635
+ if (rt.paused === void 0) rt.paused = seed.paused === true;
6597
6636
  return rt;
6598
6637
  }
6599
6638
  async function applyRuntimeParam(context, key, value) {
@@ -6617,12 +6656,15 @@ async function applyRuntimeParam(context, key, value) {
6617
6656
  const reg = context.servicesRegistry;
6618
6657
  if (reg?.rowId && reg?.registryTable) {
6619
6658
  try {
6620
- const loopSnapshot = {};
6659
+ const specs = Array.isArray(context.tasksRuntimeParamSpecs) ? context.tasksRuntimeParamSpecs : DEFAULT_RUNTIME_PARAM_SPECS;
6660
+ const runtimeSnapshot = runtimeValuesForSpecs(context, specs);
6621
6661
  for (const lk of LOOP_RUNTIME_KEYS) {
6622
- if (runtime[lk] !== void 0) loopSnapshot[lk] = runtime[lk];
6662
+ if (runtime[lk] !== void 0 && runtimeSnapshot[lk] === void 0) {
6663
+ runtimeSnapshot[lk] = runtime[lk];
6664
+ }
6623
6665
  }
6624
6666
  await updateServicesRegistryMetadata(context, reg, {
6625
- runtime: loopSnapshot,
6667
+ runtime: runtimeSnapshot,
6626
6668
  runtimeUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
6627
6669
  });
6628
6670
  applied.push("servicesRegistry");
@@ -6658,6 +6700,140 @@ function readLoopRuntime(context) {
6658
6700
  };
6659
6701
  }
6660
6702
 
6703
+ // src/tasks/coreTasks/TaskPauseRunner.js
6704
+ async function applyRunnerPaused(context, paused) {
6705
+ const runtime = ensureTasksRuntime(context);
6706
+ const was = runtime.paused === true;
6707
+ runtime.paused = paused === true;
6708
+ const registry = context.servicesRegistry;
6709
+ if (registry && typeof registry === "object") {
6710
+ await updateServicesRegistryMetadata(context, registry, {
6711
+ paused: runtime.paused,
6712
+ pausedAt: runtime.paused ? (/* @__PURE__ */ new Date()).toISOString() : null
6713
+ });
6714
+ }
6715
+ const message = runtime.paused ? was ? "Runner already paused (no new worker tasks)." : "Runner paused: finishing in-flight work; no new worker tasks until unpause." : was ? "Runner unpaused: claiming worker tasks again." : "Runner already running (not paused).";
6716
+ context.logger?.warn?.(
6717
+ runtime.paused ? `[TaskPauseRunner] ${message}` : `[TaskUnpauseRunner] ${message}`
6718
+ );
6719
+ return {
6720
+ success: true,
6721
+ results: {
6722
+ paused: runtime.paused,
6723
+ message
6724
+ }
6725
+ };
6726
+ }
6727
+ var TaskPauseRunner = class extends AbstractTask {
6728
+ static taskName = "pauseRunner";
6729
+ static description = "Pause a runner: finish in-flight tasks, claim no new worker tasks until unpaused";
6730
+ static aliases = ["pause"];
6731
+ static defaultWaitForResult = true;
6732
+ /**
6733
+ * @param {object} context
6734
+ * @param {Record<string, unknown>} [overrides]
6735
+ * @returns {Promise<object>}
6736
+ */
6737
+ static async resolveParams(context, overrides = {}) {
6738
+ const main = await super.resolveParams(context, overrides);
6739
+ if (!main.serviceName) {
6740
+ throw new ParamError(
6741
+ "pause/pauseRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
6742
+ );
6743
+ }
6744
+ return main;
6745
+ }
6746
+ async run() {
6747
+ return applyRunnerPaused(this.context, true);
6748
+ }
6749
+ };
6750
+ var TaskUnpauseRunner = class extends AbstractTask {
6751
+ static taskName = "unpauseRunner";
6752
+ static description = "Unpause a runner: resume claiming worker tasks";
6753
+ static aliases = ["unpause"];
6754
+ static defaultWaitForResult = true;
6755
+ /**
6756
+ * @param {object} context
6757
+ * @param {Record<string, unknown>} [overrides]
6758
+ * @returns {Promise<object>}
6759
+ */
6760
+ static async resolveParams(context, overrides = {}) {
6761
+ const main = await super.resolveParams(context, overrides);
6762
+ if (!main.serviceName) {
6763
+ throw new ParamError(
6764
+ "unpause/unpauseRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
6765
+ );
6766
+ }
6767
+ return main;
6768
+ }
6769
+ async run() {
6770
+ return applyRunnerPaused(this.context, false);
6771
+ }
6772
+ };
6773
+
6774
+ // src/tasks/coreTasks/TaskGetLogs.js
6775
+ var TaskGetLogs = class extends AbstractTask {
6776
+ /**
6777
+ * @param {object} context
6778
+ * @param {Record<string, unknown>} [overrides]
6779
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
6780
+ */
6781
+ static async resolveCustomParams(context, overrides = {}) {
6782
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
6783
+ source: "string",
6784
+ resource: "string",
6785
+ tail: "number default 100",
6786
+ afterTs: "string"
6787
+ }, overrides);
6788
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
6789
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
6790
+ if (!source) throw new ParamError('getLogs: param "source" is required');
6791
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
6792
+ let tail = Number(merged.tail);
6793
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
6794
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
6795
+ const out = { source, resource, tail };
6796
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
6797
+ out.afterTs = merged.afterTs.trim();
6798
+ }
6799
+ return out;
6800
+ }
6801
+ /**
6802
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
6803
+ * @returns {Promise<{ success: boolean, results: unknown }>}
6804
+ */
6805
+ async run(_reportProgress) {
6806
+ const p = this.task.params ?? {};
6807
+ const source = String(p.source ?? "").trim();
6808
+ const resource = String(p.resource ?? "").trim();
6809
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
6810
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
6811
+ if (!source || !resource) {
6812
+ return {
6813
+ success: false,
6814
+ results: { error: 'getLogs requires params "source" and "resource"' }
6815
+ };
6816
+ }
6817
+ try {
6818
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
6819
+ source,
6820
+ resource,
6821
+ tail,
6822
+ afterTs
6823
+ });
6824
+ return {
6825
+ success: true,
6826
+ results: { records, latestTs, source, resource }
6827
+ };
6828
+ } catch (e) {
6829
+ return {
6830
+ success: false,
6831
+ results: { error: e?.message ?? String(e) }
6832
+ };
6833
+ }
6834
+ }
6835
+ };
6836
+
6661
6837
  // src/tasks/coreTasks/TaskSetRuntimeParam.js
6662
6838
  var TaskSetRuntimeParam = class extends AbstractTask {
6663
6839
  static defaultWaitForResult = true;
@@ -6813,7 +6989,7 @@ var TasksRegistry = class _TasksRegistry {
6813
6989
  * @returns {TasksRegistry}
6814
6990
  */
6815
6991
  static withCoreTasks() {
6816
- 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);
6992
+ 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);
6817
6993
  }
6818
6994
  /**
6819
6995
  * Register a single task class under a name. Overwrites any previous entry.
@@ -7171,17 +7347,25 @@ async function runTasksLoop(context, options) {
7171
7347
  if (hbGroup) {
7172
7348
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
7173
7349
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
7174
- const loop0 = readLoopRuntime(context);
7350
+ const runtimeParamSpecs = mergeRuntimeParamSpecs(options.runnerRuntimeParams);
7351
+ context.tasksRuntimeParamSpecs = runtimeParamSpecs;
7175
7352
  const defaultMeta = {
7176
7353
  component: "tasks-runner",
7177
7354
  allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all",
7178
- runtime: {
7179
- maxParallel: loop0.maxParallel,
7180
- pollMs: loop0.pollMs,
7181
- claimJitterMs: loop0.claimJitterMs,
7182
- scanLimit: loop0.scanLimit
7183
- }
7355
+ paused: false,
7356
+ runtimeParams: runtimeParamSpecs,
7357
+ runtime: runtimeValuesForSpecs(context, runtimeParamSpecs)
7358
+ };
7359
+ const metadata = {
7360
+ ...defaultMeta,
7361
+ ...options.runnerMetadata && typeof options.runnerMetadata === "object" ? options.runnerMetadata : {}
7184
7362
  };
7363
+ if (!Array.isArray(metadata.runtimeParams) || metadata.runtimeParams.length === 0) {
7364
+ metadata.runtimeParams = defaultMeta.runtimeParams;
7365
+ }
7366
+ if (!metadata.runtime || typeof metadata.runtime !== "object") {
7367
+ metadata.runtime = defaultMeta.runtime;
7368
+ }
7185
7369
  registryReg = await registerInServicesRegistry(context, {
7186
7370
  queueName,
7187
7371
  target,
@@ -7191,7 +7375,7 @@ async function runTasksLoop(context, options) {
7191
7375
  staleMs,
7192
7376
  groupMaxInstances: options.runnerGroupMaxInstances,
7193
7377
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
7194
- metadata: options.runnerMetadata ?? defaultMeta
7378
+ metadata
7195
7379
  });
7196
7380
  context.servicesRegistry = registryReg;
7197
7381
  runnerIdentity = {
@@ -7241,28 +7425,38 @@ async function runTasksLoop(context, options) {
7241
7425
  if (claimJitterMs > 0) {
7242
7426
  await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
7243
7427
  }
7244
- while (runningPromises.size < maxParallel) {
7245
- const claimed = await claimNextRunnableTask(
7246
- context,
7247
- tasksTable,
7248
- target,
7249
- registry,
7250
- scanLimit,
7251
- allowedTasks,
7252
- runnerIdentity
7253
- );
7254
- if (!claimed) break;
7255
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
7256
- if (outcome.stopRunnerRequested && !stopRequested) {
7257
- stopRequested = true;
7258
- stopAllowanceMs = outcome.stopAllowanceMs || stopAllowanceMs;
7259
- context.tasksRunnerStop = true;
7260
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
7261
- }
7262
- }).finally(() => {
7263
- runningPromises.delete(p);
7264
- });
7265
- runningPromises.add(p);
7428
+ const paused = context.tasksRuntime?.paused === true;
7429
+ if (!paused) {
7430
+ while (runningPromises.size < maxParallel) {
7431
+ const claimed = await claimNextRunnableTask(
7432
+ context,
7433
+ tasksTable,
7434
+ target,
7435
+ registry,
7436
+ scanLimit,
7437
+ allowedTasks,
7438
+ runnerIdentity
7439
+ );
7440
+ if (!claimed) break;
7441
+ const p = executeClaimedTask(
7442
+ context,
7443
+ tasksTable,
7444
+ historyTable,
7445
+ claimed,
7446
+ registry,
7447
+ runningTaskInstances
7448
+ ).then(async (outcome) => {
7449
+ if (outcome.stopRunnerRequested && !stopRequested) {
7450
+ stopRequested = true;
7451
+ stopAllowanceMs = outcome.stopAllowanceMs || stopAllowanceMs;
7452
+ context.tasksRunnerStop = true;
7453
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
7454
+ }
7455
+ }).finally(() => {
7456
+ runningPromises.delete(p);
7457
+ });
7458
+ runningPromises.add(p);
7459
+ }
7266
7460
  }
7267
7461
  const wakePromises = [...runningPromises];
7268
7462
  if (runningControlPromise) {