@nmakarov/cli-toolkit 0.21.0 → 0.25.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.
Files changed (68) hide show
  1. package/dist/args.cjs +1 -4
  2. package/dist/args.cjs.map +1 -1
  3. package/dist/args.js +1 -1
  4. package/dist/args.js.map +1 -1
  5. package/dist/cli-runner.cjs +1370 -623
  6. package/dist/cli-runner.cjs.map +1 -1
  7. package/dist/cli-runner.js +1415 -667
  8. package/dist/cli-runner.js.map +1 -1
  9. package/dist/db.cjs +173 -158
  10. package/dist/db.cjs.map +1 -1
  11. package/dist/db.js +172 -151
  12. package/dist/db.js.map +1 -1
  13. package/dist/errors.cjs +2 -2
  14. package/dist/errors.cjs.map +1 -1
  15. package/dist/errors.js +2 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/filedatabase.cjs +19 -19
  18. package/dist/filedatabase.cjs.map +1 -1
  19. package/dist/filedatabase.js +19 -16
  20. package/dist/filedatabase.js.map +1 -1
  21. package/dist/http-client.cjs +9 -11
  22. package/dist/http-client.cjs.map +1 -1
  23. package/dist/http-client.js +10 -9
  24. package/dist/http-client.js.map +1 -1
  25. package/dist/http-client2.cjs +34 -37
  26. package/dist/http-client2.cjs.map +1 -1
  27. package/dist/http-client2.js +34 -34
  28. package/dist/http-client2.js.map +1 -1
  29. package/dist/index.cjs +1831 -713
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +1837 -713
  32. package/dist/index.js.map +1 -1
  33. package/dist/init.cjs +93 -68
  34. package/dist/init.cjs.map +1 -1
  35. package/dist/init.js +108 -82
  36. package/dist/init.js.map +1 -1
  37. package/dist/logger.cjs +5 -5
  38. package/dist/logger.cjs.map +1 -1
  39. package/dist/logger.js +5 -4
  40. package/dist/logger.js.map +1 -1
  41. package/dist/mock-server.cjs +21 -33
  42. package/dist/mock-server.cjs.map +1 -1
  43. package/dist/mock-server.js +21 -28
  44. package/dist/mock-server.js.map +1 -1
  45. package/dist/params.cjs +18 -9
  46. package/dist/params.cjs.map +1 -1
  47. package/dist/params.js +18 -6
  48. package/dist/params.js.map +1 -1
  49. package/dist/s3.cjs +286 -0
  50. package/dist/s3.cjs.map +1 -0
  51. package/dist/s3.js +273 -0
  52. package/dist/s3.js.map +1 -0
  53. package/dist/screen.cjs +34 -39
  54. package/dist/screen.cjs.map +1 -1
  55. package/dist/screen.js +48 -46
  56. package/dist/screen.js.map +1 -1
  57. package/dist/tasks.cjs +1354 -501
  58. package/dist/tasks.cjs.map +1 -1
  59. package/dist/tasks.js +1369 -527
  60. package/dist/tasks.js.map +1 -1
  61. package/dist/utils.cjs +7 -8
  62. package/dist/utils.cjs.map +1 -1
  63. package/dist/utils.js +6 -6
  64. package/dist/utils.js.map +1 -1
  65. package/package.json +32 -47
  66. package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
  67. package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
  68. package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
2
  var __create = Object.create;
4
3
  var __defProp = Object.defineProperty;
5
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -30,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
29
  mod
31
30
  ));
32
31
 
33
- // src/screen/components.ts
32
+ // src/screen/components.js
34
33
  function getScreenWidth(maxWidth = null) {
35
34
  const terminalWidth = process.stdout.columns || 80;
36
35
  const availableWidth = Math.max(20, terminalWidth - 4);
@@ -104,14 +103,13 @@ function ScreenFooter({ lines, textStyle }) {
104
103
  }
105
104
  var import_react, import_ink;
106
105
  var init_components = __esm({
107
- "src/screen/components.ts"() {
108
- "use strict";
106
+ "src/screen/components.js"() {
109
107
  import_react = require("react");
110
108
  import_ink = require("ink");
111
109
  }
112
110
  });
113
111
 
114
- // src/screen/list-components.ts
112
+ // src/screen/list-components.js
115
113
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
116
114
  const [, forceUpdate] = (0, import_react2.useState)({});
117
115
  const termWidth = (process.stdout.columns || 80) - 8;
@@ -263,11 +261,13 @@ function MultiColumnListWithPreviewComponent({
263
261
  ...previewRows
264
262
  );
265
263
  }
266
- function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
264
+ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " ", onSelectionChange }) {
267
265
  const [, forceUpdate] = (0, import_react2.useState)({});
268
266
  const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
269
267
  const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
270
268
  const scrollStateRef = (0, import_react2.useRef)({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
269
+ const itemsRef = (0, import_react2.useRef)(items);
270
+ itemsRef.current = items;
271
271
  const defaultGetTitle = (item) => {
272
272
  return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
273
273
  };
@@ -281,18 +281,24 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
281
281
  return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
282
282
  }
283
283
  }) : items;
284
+ const displayItemsRef = (0, import_react2.useRef)(displayItems);
285
+ displayItemsRef.current = displayItems;
284
286
  const effectiveMaxHeight = maxHeight || displayItems.length;
285
- const canScroll = displayItems.length > effectiveMaxHeight;
287
+ const _canScroll = displayItems.length > effectiveMaxHeight;
286
288
  const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
287
289
  const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
288
290
  const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
289
291
  const canScrollUp = clampedScrollOffset > 0;
290
292
  const canScrollDown = clampedScrollOffset < maxScrollOffset;
291
293
  scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
294
+ const onSelectionChangeRef = (0, import_react2.useRef)(onSelectionChange);
295
+ onSelectionChangeRef.current = onSelectionChange;
292
296
  (0, import_react2.useEffect)(() => {
293
297
  ctx.setAction("moveUp", () => {
294
298
  const newIndex = Math.max(0, selectedIndexRef.current - 1);
295
299
  selectedIndexRef.current = newIndex;
300
+ const list = displayItemsRef.current;
301
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
296
302
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
297
303
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
298
304
  const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
@@ -302,18 +308,11 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
302
308
  forceUpdate({});
303
309
  });
304
310
  ctx.setAction("moveDown", () => {
305
- const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
306
- const titleA = titleGetter(a).toLowerCase();
307
- const titleB = titleGetter(b).toLowerCase();
308
- if (sortOrder === "asc") {
309
- return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
310
- } else {
311
- return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
312
- }
313
- }) : items;
314
- const maxIndex = currentItems.length - 1;
311
+ const currentItems = displayItemsRef.current;
312
+ const maxIndex = Math.max(0, currentItems.length - 1);
315
313
  const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
316
314
  selectedIndexRef.current = newIndex;
315
+ onSelectionChangeRef.current?.(newIndex, currentItems[newIndex]);
317
316
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
318
317
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
319
318
  const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
@@ -323,8 +322,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
323
322
  forceUpdate({});
324
323
  });
325
324
  ctx.setAction("scrollUp", () => {
326
- const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
327
- const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
325
+ const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
328
326
  const newScrollOffset = Math.max(0, currentScrollOffset - 1);
329
327
  setScrollOffset(newScrollOffset);
330
328
  forceUpdate({});
@@ -339,9 +337,9 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
339
337
  if (sortable) {
340
338
  ctx.setAction("toggleSort", () => {
341
339
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
342
- const currentSelectedItem = displayItems[selectedIndexRef.current];
340
+ const currentSelectedItem = displayItemsRef.current[selectedIndexRef.current];
343
341
  setSortOrder(nextSort);
344
- const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
342
+ const newSortedItems = nextSort !== "none" ? [...itemsRef.current].sort((a, b) => {
345
343
  const titleA = titleGetter(a).toLowerCase();
346
344
  const titleB = titleGetter(b).toLowerCase();
347
345
  if (nextSort === "asc") {
@@ -349,7 +347,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
349
347
  } else {
350
348
  return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
351
349
  }
352
- }) : items;
350
+ }) : itemsRef.current;
353
351
  const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
354
352
  if (newIndex !== -1) {
355
353
  selectedIndexRef.current = newIndex;
@@ -487,8 +485,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
487
485
  }
488
486
  var import_react2, import_ink2, h2;
489
487
  var init_list_components = __esm({
490
- "src/screen/list-components.ts"() {
491
- "use strict";
488
+ "src/screen/list-components.js"() {
492
489
  import_react2 = __toESM(require("react"), 1);
493
490
  import_ink2 = require("ink");
494
491
  init_components();
@@ -496,7 +493,7 @@ var init_list_components = __esm({
496
493
  }
497
494
  });
498
495
 
499
- // src/screen/screens.ts
496
+ // src/screen/screens.js
500
497
  function groupKeyBindings(bindings) {
501
498
  const groups = {};
502
499
  const enabledBindings = bindings.filter((b) => b.enabled !== false);
@@ -572,7 +569,7 @@ async function showScreen(config2) {
572
569
  let renderResult = null;
573
570
  let initialized = false;
574
571
  const Screen = () => {
575
- const [updateCounter, setUpdateCounter] = (0, import_react3.useState)(0);
572
+ const [, setUpdateCounter] = (0, import_react3.useState)(0);
576
573
  if (!initialized) {
577
574
  const defaultBindings = [
578
575
  { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
@@ -691,7 +688,7 @@ async function showScreen(config2) {
691
688
  }
692
689
  }
693
690
  if (matchedBinding && actions[matchedBinding.action]) {
694
- const actionResult = actions[matchedBinding.action]({
691
+ actions[matchedBinding.action]({
695
692
  input,
696
693
  key,
697
694
  binding: matchedBinding
@@ -821,8 +818,7 @@ async function showMultiColumnListWithPreviewScreen(config2) {
821
818
  }
822
819
  var import_react3, import_ink3, showMenuScreen, showWordGridScreen;
823
820
  var init_screens = __esm({
824
- "src/screen/screens.ts"() {
825
- "use strict";
821
+ "src/screen/screens.js"() {
826
822
  import_react3 = require("react");
827
823
  import_ink3 = require("ink");
828
824
  init_components();
@@ -832,7 +828,7 @@ var init_screens = __esm({
832
828
  }
833
829
  });
834
830
 
835
- // src/screen/ui-elements.ts
831
+ // src/screen/ui-elements.js
836
832
  function ListItem({
837
833
  children,
838
834
  isSelected = false,
@@ -857,7 +853,7 @@ function TextBlock({
857
853
  color = "white",
858
854
  dimmed = false,
859
855
  bold = false,
860
- maxWidth
856
+ maxWidth: _maxWidth
861
857
  }) {
862
858
  return (0, import_react4.createElement)(
863
859
  import_ink4.Box,
@@ -897,7 +893,7 @@ function GridCell({
897
893
  }, children)
898
894
  );
899
895
  }
900
- function InputField({ prompt, value, onChange, onSubmit }) {
896
+ function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit }) {
901
897
  return (0, import_react4.createElement)(
902
898
  import_ink4.Box,
903
899
  { flexDirection: "column" },
@@ -911,14 +907,13 @@ function InputField({ prompt, value, onChange, onSubmit }) {
911
907
  }
912
908
  var import_react4, import_ink4;
913
909
  var init_ui_elements = __esm({
914
- "src/screen/ui-elements.ts"() {
915
- "use strict";
910
+ "src/screen/ui-elements.js"() {
916
911
  import_react4 = require("react");
917
912
  import_ink4 = require("ink");
918
913
  }
919
914
  });
920
915
 
921
- // src/screen/utils.ts
916
+ // src/screen/utils.js
922
917
  function buildBreadcrumb(parts) {
923
918
  if (parts.length === 0) return "";
924
919
  if (parts.length === 1) return parts[0];
@@ -932,12 +927,11 @@ function buildDetailBreadcrumb(path6, suffix = "") {
932
927
  return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
933
928
  }
934
929
  var init_utils = __esm({
935
- "src/screen/utils.ts"() {
936
- "use strict";
930
+ "src/screen/utils.js"() {
937
931
  }
938
932
  });
939
933
 
940
- // src/screen/footer-builder.ts
934
+ // src/screen/footer-builder.js
941
935
  function buildFooter(config2 = {}) {
942
936
  const {
943
937
  navigation = null,
@@ -988,8 +982,7 @@ function organizeFooterMessages(messages) {
988
982
  }
989
983
  var FooterPresets;
990
984
  var init_footer_builder = __esm({
991
- "src/screen/footer-builder.ts"() {
992
- "use strict";
985
+ "src/screen/footer-builder.js"() {
993
986
  FooterPresets = {
994
987
  /**
995
988
  * Menu screen footer
@@ -1048,7 +1041,7 @@ var init_footer_builder = __esm({
1048
1041
  }
1049
1042
  });
1050
1043
 
1051
- // src/screen/index.ts
1044
+ // src/screen/index.js
1052
1045
  var screen_exports = {};
1053
1046
  __export(screen_exports, {
1054
1047
  Box: () => import_ink5.Box,
@@ -1074,6 +1067,7 @@ __export(screen_exports, {
1074
1067
  buildFooter: () => buildFooter,
1075
1068
  h: () => import_react5.createElement,
1076
1069
  load: () => load,
1070
+ memo: () => import_react5.memo,
1077
1071
  organizeFooterMessages: () => organizeFooterMessages,
1078
1072
  showListScreen: () => showListScreen,
1079
1073
  showMenuScreen: () => showMenuScreen,
@@ -1084,6 +1078,7 @@ __export(screen_exports, {
1084
1078
  useCallback: () => import_react5.useCallback,
1085
1079
  useEffect: () => import_react5.useEffect,
1086
1080
  useInput: () => import_ink5.useInput,
1081
+ useLayoutEffect: () => import_react5.useLayoutEffect,
1087
1082
  useMemo: () => import_react5.useMemo,
1088
1083
  useRef: () => import_react5.useRef,
1089
1084
  useState: () => import_react5.useState
@@ -1099,8 +1094,7 @@ async function load() {
1099
1094
  }
1100
1095
  var import_react5, import_ink5, loadPromise;
1101
1096
  var init_screen = __esm({
1102
- "src/screen/index.ts"() {
1103
- "use strict";
1097
+ "src/screen/index.js"() {
1104
1098
  import_react5 = __toESM(require("react"), 1);
1105
1099
  import_ink5 = require("ink");
1106
1100
  init_screens();
@@ -1117,11 +1111,11 @@ var init_screen = __esm({
1117
1111
  }
1118
1112
  });
1119
1113
 
1120
- // src/scripts/cli-runner.ts
1114
+ // src/scripts/cli-runner.js
1121
1115
  var import_node_path2 = __toESM(require("path"), 1);
1122
1116
  var import_node_url = require("url");
1123
1117
 
1124
- // src/args/index.ts
1118
+ // src/args/index.js
1125
1119
  var import_fs = require("fs");
1126
1120
  var import_path = require("path");
1127
1121
  var import_dotenv = require("dotenv");
@@ -1604,10 +1598,10 @@ var Args = class _Args {
1604
1598
  }
1605
1599
  };
1606
1600
 
1607
- // src/params/index.ts
1601
+ // src/params/index.js
1608
1602
  var import_joi = __toESM(require("joi"), 1);
1609
1603
 
1610
- // src/errors.ts
1604
+ // src/errors.js
1611
1605
  var FrameworkError = class extends Error {
1612
1606
  constructor(message) {
1613
1607
  super(message);
@@ -1633,7 +1627,7 @@ var FileDatabaseError = class extends FrameworkError {
1633
1627
  }
1634
1628
  };
1635
1629
 
1636
- // src/params/custom-types.ts
1630
+ // src/params/custom-types.js
1637
1631
  var joiEdateType = (value, helpers) => {
1638
1632
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
1639
1633
  const testDate = new Date(value);
@@ -1726,7 +1720,7 @@ function calculateTimeOffset(amount, unit, sign) {
1726
1720
  }
1727
1721
  return sign === "+" ? amount * multiplier : -amount * multiplier;
1728
1722
  }
1729
- var joiStringArrayType = (type) => (value, helpers) => {
1723
+ var joiStringArrayType = (type) => (value, _helpers) => {
1730
1724
  if (value === void 0 || typeof value === "function") {
1731
1725
  return [];
1732
1726
  }
@@ -1752,7 +1746,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
1752
1746
  return arr;
1753
1747
  };
1754
1748
 
1755
- // src/params/index.ts
1749
+ // src/params/index.js
1756
1750
  var Params = class _Params {
1757
1751
  context;
1758
1752
  // Partial context during initialization
@@ -2012,7 +2006,7 @@ var Params = class _Params {
2012
2006
  definition = val;
2013
2007
  val = val.value;
2014
2008
  }
2015
- const def = this.assignDefinition(key, definition);
2009
+ this.assignDefinition(key, definition);
2016
2010
  if (!this.runAllRegisteredSetters(key, val)) {
2017
2011
  this.params[key] = val;
2018
2012
  }
@@ -2071,6 +2065,18 @@ var Params = class _Params {
2071
2065
  this._currentModule = prev;
2072
2066
  }
2073
2067
  }
2068
+ /**
2069
+ * Async variant of {@link runWithModule} for modules that await params.get().
2070
+ */
2071
+ async runWithModuleAsync(moduleName, fn) {
2072
+ const prev = this._currentModule;
2073
+ this._currentModule = moduleName;
2074
+ try {
2075
+ return await fn();
2076
+ } finally {
2077
+ this._currentModule = prev;
2078
+ }
2079
+ }
2074
2080
  /**
2075
2081
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2076
2082
  */
@@ -2131,11 +2137,11 @@ var Params = class _Params {
2131
2137
  }
2132
2138
  };
2133
2139
 
2134
- // src/logger/index.ts
2140
+ // src/logger/index.js
2135
2141
  var import_chalk = __toESM(require("chalk"), 1);
2136
2142
  var import_util = __toESM(require("util"), 1);
2137
2143
 
2138
- // src/logger/transports.ts
2144
+ // src/logger/transports.js
2139
2145
  var ConsoleTransport = class {
2140
2146
  write(payload) {
2141
2147
  console.info(payload);
@@ -2155,7 +2161,7 @@ var ParentProcessTransport = class {
2155
2161
  }
2156
2162
  };
2157
2163
 
2158
- // src/logger/index.ts
2164
+ // src/logger/index.js
2159
2165
  var ALL_LEVELS = [
2160
2166
  "silly",
2161
2167
  "debug",
@@ -2228,6 +2234,7 @@ var Logger = class _Logger {
2228
2234
  }
2229
2235
  /**
2230
2236
  * Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
2237
+ * Params are tracked under the `logger` module for --showUsedParams.
2231
2238
  */
2232
2239
  static init(context, options) {
2233
2240
  const paramDefs = {
@@ -2241,7 +2248,7 @@ var Logger = class _Logger {
2241
2248
  progressWithTimes: "boolean default false",
2242
2249
  progressThrottleMs: "number"
2243
2250
  };
2244
- const discovered = context.params.getAllForModule(paramDefs);
2251
+ const discovered = context.params.getAllForModule("logger", paramDefs);
2245
2252
  const config2 = { ...discovered, ...options };
2246
2253
  const logger = new _Logger(context, config2);
2247
2254
  context.logger = logger;
@@ -2437,9 +2444,9 @@ var Logger = class _Logger {
2437
2444
  }
2438
2445
  };
2439
2446
 
2440
- // src/init/index.ts
2447
+ // src/init/index.js
2441
2448
  var import_events = require("events");
2442
- function extractComponentOptions(opts, componentName) {
2449
+ function extractComponentOptions(opts, _componentName) {
2443
2450
  const reservedKeys = ["overrides", "defaults", "modules"];
2444
2451
  const componentOptions = {};
2445
2452
  for (const [key, value] of Object.entries(opts)) {
@@ -2506,6 +2513,19 @@ function printAllParameters(context) {
2506
2513
  async function init(flow2, opts = {}) {
2507
2514
  let stop = false;
2508
2515
  let context = null;
2516
+ let cleanupRan = false;
2517
+ const runRegisteredCleanups = async (ctx) => {
2518
+ if (cleanupRan) return;
2519
+ cleanupRan = true;
2520
+ const fns = [...ctx.cleanupFunctions].reverse();
2521
+ for (const fn of fns) {
2522
+ try {
2523
+ await fn(ctx);
2524
+ } catch (error) {
2525
+ ctx.logger.warn("[cleanup] error in cleanup function:", error);
2526
+ }
2527
+ }
2528
+ };
2509
2529
  try {
2510
2530
  try {
2511
2531
  const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
@@ -2528,13 +2548,24 @@ async function init(flow2, opts = {}) {
2528
2548
  printAllParameters(context);
2529
2549
  process.exit(0);
2530
2550
  }
2551
+ let sigintCount = 0;
2531
2552
  process.on("SIGINT", async () => {
2532
- if (stop) {
2533
- context.logger.warn("[process] killed");
2534
- process.exit(2);
2553
+ if (!context) return;
2554
+ sigintCount += 1;
2555
+ if (sigintCount === 1) {
2556
+ stop = true;
2557
+ context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2558
+ context.emitter.emit("stop", stopAllowance);
2559
+ return;
2535
2560
  }
2561
+ context.logger.warn("[process] second SIGINT: running cleanup then exit");
2562
+ await runRegisteredCleanups(context);
2563
+ process.exit(2);
2564
+ });
2565
+ process.on("SIGTERM", () => {
2566
+ if (!context || stop) return;
2536
2567
  stop = true;
2537
- context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2568
+ context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
2538
2569
  context.emitter.emit("stop", stopAllowance);
2539
2570
  });
2540
2571
  await flow2(context);
@@ -2559,33 +2590,95 @@ async function init(flow2, opts = {}) {
2559
2590
  }
2560
2591
  } finally {
2561
2592
  if (context) {
2562
- for (const fn of context.cleanupFunctions.reverse()) {
2563
- try {
2564
- await fn(context);
2565
- } catch (error) {
2566
- context.logger.warn("[cleanup] error in cleanup function:", error);
2567
- }
2568
- }
2593
+ await runRegisteredCleanups(context);
2569
2594
  }
2570
2595
  }
2571
2596
  }
2572
2597
 
2573
- // src/db/index.ts
2598
+ // src/db/index.js
2574
2599
  var import_knex = __toESM(require("knex"), 1);
2600
+ var KNEX_DEFAULTS = {
2601
+ testConnection: true,
2602
+ pool: { min: 2, max: 10 },
2603
+ acquireConnectionTimeout: 1e4,
2604
+ ssl: { rejectUnauthorized: false }
2605
+ };
2575
2606
  var Db = class {
2576
- knexInstance = null;
2577
- config;
2578
- logger;
2579
- queriesLog = [];
2580
- isConnected = false;
2581
- /**
2582
- * Constructor - accepts config object
2583
- * Use dbInit() function to initialize with Context
2584
- */
2607
+ static async init(context, options = {}) {
2608
+ const buildConfig = async () => {
2609
+ const defs2 = {
2610
+ dbName: "string",
2611
+ dbProfile: "boolean default false"
2612
+ };
2613
+ const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
2614
+ const merged = { ...discovered, ...options };
2615
+ let { dbName, dbProfile } = merged;
2616
+ let dbConnectionString = options.dbConnectionString ?? options.connectionString;
2617
+ let connectionParam = dbConnectionString ? "options" : null;
2618
+ if (!dbConnectionString) {
2619
+ const src = context?.args?.getSource?.("dbConnectionString");
2620
+ if (src === "cli" || src === "overrides" || src === "config") {
2621
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
2622
+ connectionParam = "dbConnectionString";
2623
+ }
2624
+ }
2625
+ if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
2626
+ dbConnectionString = dbName;
2627
+ dbName = void 0;
2628
+ }
2629
+ if (!dbConnectionString && dbName) {
2630
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2631
+ dbConnectionString = await context.params.get(paramName, "string");
2632
+ connectionParam = paramName;
2633
+ if (!dbConnectionString) {
2634
+ throw new ParamError(
2635
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2636
+ );
2637
+ }
2638
+ }
2639
+ if (!dbConnectionString) {
2640
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
2641
+ if (dbConnectionString) {
2642
+ connectionParam = "dbConnectionString";
2643
+ }
2644
+ }
2645
+ if (!dbConnectionString) {
2646
+ if (!dbName) {
2647
+ dbName = "local";
2648
+ }
2649
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2650
+ dbConnectionString = await context.params.get(paramName, "string");
2651
+ connectionParam = paramName;
2652
+ if (!dbConnectionString) {
2653
+ throw new ParamError(
2654
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2655
+ );
2656
+ }
2657
+ }
2658
+ const displayName = resolveDbDisplayName(
2659
+ dbName,
2660
+ connectionParam,
2661
+ context?.args?.env,
2662
+ merged.name
2663
+ );
2664
+ return {
2665
+ ...KNEX_DEFAULTS,
2666
+ connectionString: dbConnectionString,
2667
+ name: displayName,
2668
+ profile: !!dbProfile,
2669
+ logger: context.logger
2670
+ };
2671
+ };
2672
+ const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
2673
+ return dbConnect(context, config2);
2674
+ }
2585
2675
  constructor(config2) {
2586
- if (!config2.connectionString) {
2676
+ if (!config2 || !config2.connectionString) {
2587
2677
  throw new ParamError("Db: connectionString is required");
2588
2678
  }
2679
+ this.knexInstance = null;
2680
+ this.isConnected = false;
2681
+ this.queriesLog = [];
2589
2682
  this.config = {
2590
2683
  testConnection: true,
2591
2684
  profile: false,
@@ -2593,30 +2686,27 @@ var Db = class {
2593
2686
  acquireConnectionTimeout: 1e4,
2594
2687
  ssl: { rejectUnauthorized: false },
2595
2688
  logger: console,
2596
- name: "default",
2597
2689
  ...config2
2598
2690
  };
2599
2691
  this.logger = this.config.logger;
2600
2692
  const instance = this;
2601
- const callableWrapper = function(...args) {
2693
+ const callableWrapper = function() {
2602
2694
  throw new Error("This should never be called directly");
2603
2695
  };
2604
2696
  callableWrapper._instance = instance;
2605
2697
  return new Proxy(callableWrapper, {
2606
- // Intercept function calls: db('table')
2607
- apply: (target, thisArg, argumentsList) => {
2698
+ apply: (target, _thisArg, argumentsList) => {
2608
2699
  const inst = target._instance;
2609
2700
  if (!inst.knexInstance) {
2610
2701
  throw new Error("Db: Not connected. Call connect() first.");
2611
2702
  }
2612
2703
  return inst.knexInstance(...argumentsList);
2613
2704
  },
2614
- // Intercept property access: db.schema, db.raw, etc.
2615
2705
  get: (target, prop) => {
2616
2706
  if (prop === "_instance") {
2617
2707
  return target._instance;
2618
2708
  }
2619
- const instance2 = target._instance;
2709
+ const inst = target._instance;
2620
2710
  const ownMethods = [
2621
2711
  "connect",
2622
2712
  "disconnect",
@@ -2629,26 +2719,26 @@ var Db = class {
2629
2719
  "detectClient",
2630
2720
  "attachProfiler"
2631
2721
  ];
2632
- if (prop in instance2) {
2633
- const value = instance2[prop];
2722
+ if (prop in inst) {
2723
+ const value = inst[prop];
2634
2724
  if (typeof value === "function" && ownMethods.includes(prop)) {
2635
- return value.bind(instance2);
2725
+ return value.bind(inst);
2636
2726
  }
2637
2727
  if (typeof value !== "function") {
2638
2728
  return value;
2639
2729
  }
2640
2730
  }
2641
- if (instance2.knexInstance) {
2642
- const knexProp = instance2.knexInstance[prop];
2731
+ if (inst.knexInstance) {
2732
+ const knexProp = inst.knexInstance[prop];
2643
2733
  if (typeof knexProp === "function") {
2644
- return knexProp.bind(instance2.knexInstance);
2734
+ return knexProp.bind(inst.knexInstance);
2645
2735
  }
2646
2736
  return knexProp;
2647
2737
  }
2648
- if (prop in instance2) {
2649
- const method = instance2[prop];
2738
+ if (prop in inst) {
2739
+ const method = inst[prop];
2650
2740
  if (typeof method === "function") {
2651
- return method.bind(instance2);
2741
+ return method.bind(inst);
2652
2742
  }
2653
2743
  return method;
2654
2744
  }
@@ -2656,9 +2746,6 @@ var Db = class {
2656
2746
  }
2657
2747
  });
2658
2748
  }
2659
- /**
2660
- * Detect database client type from connection string
2661
- */
2662
2749
  detectClient(connectionString) {
2663
2750
  if (connectionString.match(/^postgresql/)) {
2664
2751
  return "pg";
@@ -2668,9 +2755,6 @@ var Db = class {
2668
2755
  }
2669
2756
  return null;
2670
2757
  }
2671
- /**
2672
- * Connect to the database
2673
- */
2674
2758
  async connect() {
2675
2759
  if (this.isConnected && this.knexInstance) {
2676
2760
  this.logger.warn?.("[Db] Already connected");
@@ -2679,14 +2763,13 @@ var Db = class {
2679
2763
  const client = this.detectClient(this.config.connectionString);
2680
2764
  if (!client) {
2681
2765
  throw new ParamError(
2682
- `Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
2766
+ "Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
2683
2767
  );
2684
2768
  }
2685
2769
  try {
2686
2770
  const connectionConfig = {
2687
2771
  connectionString: this.config.connectionString,
2688
2772
  family: 4
2689
- // Force IPv4 only (disable IPv6)
2690
2773
  };
2691
2774
  this.knexInstance = (0, import_knex.default)({
2692
2775
  client,
@@ -2702,7 +2785,7 @@ var Db = class {
2702
2785
  await this.testConnection();
2703
2786
  }
2704
2787
  this.isConnected = true;
2705
- this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
2788
+ this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
2706
2789
  } catch (error) {
2707
2790
  if (error instanceof ParamError) {
2708
2791
  throw error;
@@ -2711,9 +2794,6 @@ var Db = class {
2711
2794
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
2712
2795
  }
2713
2796
  }
2714
- /**
2715
- * Disconnect from the database
2716
- */
2717
2797
  async disconnect() {
2718
2798
  if (!this.knexInstance) {
2719
2799
  return;
@@ -2723,16 +2803,13 @@ var Db = class {
2723
2803
  this.knexInstance = null;
2724
2804
  this.isConnected = false;
2725
2805
  this.queriesLog = [];
2726
- this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
2806
+ this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
2727
2807
  } catch (error) {
2728
2808
  const errorMsg = this.getErrorMessage(error);
2729
2809
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
2730
2810
  throw error;
2731
2811
  }
2732
2812
  }
2733
- /**
2734
- * Extract error message from various error types
2735
- */
2736
2813
  getErrorMessage(error) {
2737
2814
  if (error instanceof AggregateError) {
2738
2815
  const errors = error.errors || [];
@@ -2757,9 +2834,11 @@ var Db = class {
2757
2834
  return `${code} (tried: ${addresses.join(", ")})`;
2758
2835
  }
2759
2836
  }
2760
- const uniqueMessages = [...new Set(errors.map((e) => {
2761
- return e instanceof Error ? e.message : String(e);
2762
- }))];
2837
+ const uniqueMessages = [
2838
+ ...new Set(
2839
+ errors.map((e) => e instanceof Error ? e.message : String(e))
2840
+ )
2841
+ ];
2763
2842
  if (uniqueMessages.length === 1) {
2764
2843
  return uniqueMessages[0];
2765
2844
  }
@@ -2768,28 +2847,25 @@ var Db = class {
2768
2847
  return error.message || "Multiple errors occurred";
2769
2848
  }
2770
2849
  if (error instanceof Error) {
2771
- const errorWithCode = error;
2772
- if (errorWithCode.code) {
2773
- return `${errorWithCode.code}: ${error.message || String(error)}`;
2850
+ const code = error.code;
2851
+ if (code) {
2852
+ return `${code}: ${error.message || String(error)}`;
2774
2853
  }
2775
2854
  return error.message || String(error);
2776
2855
  }
2777
2856
  if (typeof error === "string") {
2778
2857
  return error;
2779
2858
  }
2780
- if (error?.message) {
2859
+ if (error && typeof error === "object" && "message" in error) {
2781
2860
  const msg = String(error.message);
2782
- const errorWithCode = error;
2783
- if (errorWithCode.code) {
2784
- return `${errorWithCode.code}: ${msg}`;
2861
+ const code = error.code;
2862
+ if (code) {
2863
+ return `${code}: ${msg}`;
2785
2864
  }
2786
2865
  return msg;
2787
2866
  }
2788
2867
  return String(error) || "Unknown error";
2789
2868
  }
2790
- /**
2791
- * Test database connection
2792
- */
2793
2869
  async testConnection() {
2794
2870
  if (!this.knexInstance) {
2795
2871
  throw new Error("Db: Not connected. Call connect() first.");
@@ -2805,9 +2881,6 @@ var Db = class {
2805
2881
  throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
2806
2882
  }
2807
2883
  }
2808
- /**
2809
- * Attach query profiler to log all queries
2810
- */
2811
2884
  attachProfiler() {
2812
2885
  if (!this.knexInstance) {
2813
2886
  return;
@@ -2817,7 +2890,7 @@ var Db = class {
2817
2890
  this.knexInstance.on("query", (query) => {
2818
2891
  query.__startTime = process.hrtime();
2819
2892
  });
2820
- this.knexInstance.on("query-response", (response, query) => {
2893
+ this.knexInstance.on("query-response", (_response, query) => {
2821
2894
  const [seconds, nanoseconds] = process.hrtime(query.__startTime);
2822
2895
  const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
2823
2896
  const logEntry = {
@@ -2832,15 +2905,9 @@ var Db = class {
2832
2905
  this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
2833
2906
  });
2834
2907
  }
2835
- /**
2836
- * Get query log (only available if profiling is enabled)
2837
- */
2838
2908
  getQueryLog() {
2839
2909
  return [...this.queriesLog];
2840
2910
  }
2841
- /**
2842
- * Check if a table exists
2843
- */
2844
2911
  async tableExists(tableName) {
2845
2912
  if (!this.knexInstance) {
2846
2913
  throw new Error("Db: Not connected. Call connect() first.");
@@ -2852,65 +2919,87 @@ var Db = class {
2852
2919
  throw error;
2853
2920
  }
2854
2921
  }
2855
- /**
2856
- * Get the underlying Knex instance (for advanced usage)
2857
- */
2858
2922
  getKnex() {
2859
2923
  if (!this.knexInstance) {
2860
2924
  throw new Error("Db: Not connected. Call connect() first.");
2861
2925
  }
2862
2926
  return this.knexInstance;
2863
2927
  }
2864
- /**
2865
- * Get connection status
2866
- */
2867
2928
  isConnectedToDb() {
2868
2929
  return this.isConnected && this.knexInstance !== null;
2869
2930
  }
2870
- /**
2871
- * Initialize Db with context (connects and registers disconnect cleanup).
2872
- * Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
2873
- */
2874
- static async init(context, dbNameOrConnectionString) {
2875
- return dbFindAndConnect(context, dbNameOrConnectionString);
2876
- }
2877
2931
  };
2878
2932
  function capitalizeFirstLetter(str) {
2879
2933
  return str.charAt(0).toUpperCase() + str.slice(1);
2880
2934
  }
2881
- async function dbConnect(context, connectionString, name, dbProfile) {
2882
- const defs2 = {
2883
- testDbConnection: "boolean default true",
2884
- name: "string",
2885
- poolMin: "number default 2",
2886
- poolMax: "number default 10",
2887
- acquireConnectionTimeout: "number default 10000",
2888
- sslRejectUnauthorized: "boolean default false"
2889
- };
2890
- const paramsConfig = context.params.getAllForModule(defs2);
2891
- const config2 = {
2892
- connectionString,
2893
- name: paramsConfig.name || name || "default",
2894
- testConnection: paramsConfig.testDbConnection,
2895
- profile: dbProfile ?? false,
2896
- pool: {
2897
- min: paramsConfig.poolMin,
2898
- max: paramsConfig.poolMax
2899
- },
2900
- acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
2901
- ssl: {
2902
- rejectUnauthorized: paramsConfig.sslRejectUnauthorized
2903
- },
2904
- logger: context.logger
2905
- };
2935
+ function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
2936
+ if (dbName) {
2937
+ return dbName;
2938
+ }
2939
+ if (mergedName) {
2940
+ return mergedName;
2941
+ }
2942
+ if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
2943
+ return connectionParam.slice("dbConnectionString".length).toLowerCase();
2944
+ }
2945
+ if (connectionParam === "dbConnectionString" && argsEnv) {
2946
+ return argsEnv;
2947
+ }
2948
+ return void 0;
2949
+ }
2950
+ function formatDbConnectMessage(name, connectionString) {
2951
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
2952
+ if (name) {
2953
+ return `[Db] Connected to database "${name}"${endpointSuffix}`;
2954
+ }
2955
+ return `[Db] Connected${endpointSuffix}`;
2956
+ }
2957
+ function formatDbDisconnectMessage(name, connectionString) {
2958
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
2959
+ if (name) {
2960
+ return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
2961
+ }
2962
+ return `[Db] Disconnected${endpointSuffix}`;
2963
+ }
2964
+ function formatDbInstanceMessage(action, name) {
2965
+ if (name) {
2966
+ return `[Db] instance "${name}" ${action}`;
2967
+ }
2968
+ return `[Db] instance ${action}`;
2969
+ }
2970
+ function formatConnectionEndpointSuffix(connectionString) {
2971
+ const endpoint = formatConnectionEndpoint(connectionString);
2972
+ return endpoint ? ` (${endpoint})` : "";
2973
+ }
2974
+ function formatConnectionEndpoint(connectionString) {
2975
+ try {
2976
+ const url = new URL(connectionString);
2977
+ const host = url.hostname;
2978
+ if (!host) {
2979
+ return null;
2980
+ }
2981
+ let port = url.port;
2982
+ if (!port) {
2983
+ if (url.protocol === "postgresql:") {
2984
+ port = "5432";
2985
+ } else if (url.protocol === "mysql:") {
2986
+ port = "3306";
2987
+ }
2988
+ }
2989
+ return port ? `${host}:${port}` : host;
2990
+ } catch {
2991
+ return null;
2992
+ }
2993
+ }
2994
+ async function dbConnect(context, config2) {
2906
2995
  try {
2907
2996
  const db = new Db(config2);
2908
2997
  context.registerCleanup(async () => {
2909
2998
  await db.disconnect();
2910
- context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
2999
+ context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
2911
3000
  });
2912
3001
  await db.connect();
2913
- context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
3002
+ context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
2914
3003
  return db;
2915
3004
  } catch (error) {
2916
3005
  if (error instanceof ParamError) {
@@ -2920,48 +3009,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
2920
3009
  throw new ParamError(`[Db] connect error: ${errorMsg}`);
2921
3010
  }
2922
3011
  }
2923
- async function dbFindAndConnect(context, dbNameOrConnectionString) {
2924
- let dbName;
2925
- let dbConnectionString;
2926
- let dbProfile;
2927
- if (dbNameOrConnectionString) {
2928
- if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
2929
- dbName = void 0;
2930
- dbConnectionString = dbNameOrConnectionString;
2931
- } else {
2932
- dbName = dbNameOrConnectionString;
2933
- }
2934
- } else {
2935
- const defs2 = {
2936
- dbName: "string",
2937
- dbConnectionString: "string",
2938
- dbProfile: "boolean default false"
2939
- };
2940
- const paramsConfig = context.params.getAll(defs2);
2941
- dbName = paramsConfig.dbName;
2942
- dbConnectionString = paramsConfig.dbConnectionString;
2943
- dbProfile = paramsConfig.dbProfile;
2944
- }
2945
- if (!dbName && !dbConnectionString) {
2946
- throw new ParamError("Db: either dbName or dbConnectionString must be specified");
2947
- }
2948
- if (dbName) {
2949
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2950
- dbConnectionString = await context.params.get(paramName, "string");
2951
- if (!dbConnectionString) {
2952
- throw new ParamError(
2953
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2954
- );
2955
- }
2956
- }
2957
- const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
2958
- return db;
2959
- }
2960
- async function dbInit(context, dbNameOrConnectionString) {
2961
- return await dbFindAndConnect(context, dbNameOrConnectionString);
2962
- }
2963
3012
 
2964
- // src/utils/date-utils.ts
3013
+ // src/tasks/index.js
3014
+ var import_node_os3 = __toESM(require("os"), 1);
3015
+
3016
+ // src/utils/date-utils.js
2965
3017
  function isTimestampFolder(folderName) {
2966
3018
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
2967
3019
  if (!isoRegex.test(folderName)) {
@@ -2971,7 +3023,7 @@ function isTimestampFolder(folderName) {
2971
3023
  return !isNaN(date.getTime()) && date.getTime() > 0;
2972
3024
  }
2973
3025
 
2974
- // src/utils/fs-utils.ts
3026
+ // src/utils/fs-utils.js
2975
3027
  var import_fs2 = __toESM(require("fs"), 1);
2976
3028
  var import_path2 = __toESM(require("path"), 1);
2977
3029
  async function ensurePath(...pathParts) {
@@ -2995,7 +3047,7 @@ function getFileExtension(dataType) {
2995
3047
  }
2996
3048
  }
2997
3049
 
2998
- // src/utils/os-utils.ts
3050
+ // src/utils/os-utils.js
2999
3051
  var import_fs3 = __toESM(require("fs"), 1);
3000
3052
  var import_path3 = __toESM(require("path"), 1);
3001
3053
  var import_child_process = require("child_process");
@@ -3024,7 +3076,7 @@ function getFreeDiskSpace(targetPath) {
3024
3076
  }
3025
3077
  }
3026
3078
 
3027
- // src/utils/format-utils.ts
3079
+ // src/utils/format-utils.js
3028
3080
  function bytesToHumanReadable(bytes) {
3029
3081
  if (bytes === 0) return "0 B";
3030
3082
  const k = 1024;
@@ -3033,7 +3085,7 @@ function bytesToHumanReadable(bytes) {
3033
3085
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
3034
3086
  }
3035
3087
 
3036
- // src/utils/core-utils.ts
3088
+ // src/utils/core-utils.js
3037
3089
  function sleepMs(ms) {
3038
3090
  return new Promise((resolve2) => setTimeout(resolve2, ms));
3039
3091
  }
@@ -3042,14 +3094,82 @@ function toJsonColumn(value) {
3042
3094
  return JSON.stringify(value);
3043
3095
  }
3044
3096
 
3045
- // src/tasks/servicesRegistry.ts
3046
- var import_node_crypto2 = require("crypto");
3047
- var import_promises = require("fs/promises");
3097
+ // src/tasks/servicesRegistry.js
3048
3098
  var import_node_os = __toESM(require("os"), 1);
3049
- var import_node_path = __toESM(require("path"), 1);
3050
3099
 
3051
- // src/tasks/taskUtils.ts
3100
+ // src/tasks/taskUtils.js
3052
3101
  var import_node_crypto = require("crypto");
3102
+
3103
+ // src/tasks/time-matcher.js
3104
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
3105
+ function resolveAsterisks(field, range) {
3106
+ return field.includes("*") ? field.replace("*", range) : field;
3107
+ }
3108
+ function resolveRanges(field) {
3109
+ const regex = /(\d+)-(\d+)/;
3110
+ let current = field;
3111
+ while (true) {
3112
+ const match = regex.exec(current);
3113
+ if (!match) break;
3114
+ const raw = match[0];
3115
+ let first = Number(match[1]);
3116
+ let last = Number(match[2]);
3117
+ if (last < first) {
3118
+ [first, last] = [last, first];
3119
+ }
3120
+ const values = [];
3121
+ for (let i = first; i <= last; i += 1) {
3122
+ values.push(i);
3123
+ }
3124
+ current = current.replace(raw, values.join(","));
3125
+ }
3126
+ return current;
3127
+ }
3128
+ function resolveSteps(field) {
3129
+ const match = /^(.+)\/(\d+)$/.exec(field);
3130
+ if (!match) return field;
3131
+ const base = match[1];
3132
+ const step = Number(match[2]);
3133
+ if (!Number.isFinite(step) || step <= 0) return field;
3134
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
3135
+ }
3136
+ function convertPattern(pattern) {
3137
+ const parts = pattern.trim().split(/\s+/);
3138
+ if (parts.length !== 6) {
3139
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
3140
+ }
3141
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
3142
+ }
3143
+ function fieldMatches(field, value) {
3144
+ const allowed = field.split(",").map((v) => Number(v));
3145
+ return allowed.includes(value);
3146
+ }
3147
+ function matchesParsedPattern(parsed, date) {
3148
+ return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
3149
+ }
3150
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
3151
+ const parsed = convertPattern(pattern);
3152
+ return matchesParsedPattern(parsed, date);
3153
+ }
3154
+ var MS_PER_SECOND = 1e3;
3155
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
3156
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
3157
+ const parsed = convertPattern(pattern);
3158
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
3159
+ const end = t + maxSearchMs;
3160
+ while (t <= end) {
3161
+ const date = new Date(t);
3162
+ if (matchesParsedPattern(parsed, date)) {
3163
+ return date;
3164
+ }
3165
+ t += MS_PER_SECOND;
3166
+ }
3167
+ throw new Error(
3168
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
3169
+ );
3170
+ }
3171
+
3172
+ // src/tasks/taskUtils.js
3053
3173
  function getDb(context) {
3054
3174
  const db = context.db;
3055
3175
  if (!db) {
@@ -3057,97 +3177,84 @@ function getDb(context) {
3057
3177
  }
3058
3178
  return db;
3059
3179
  }
3060
- function queueToTableNames(queue) {
3061
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
3062
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
3063
- }
3180
+ function queueToTableNames(queueName) {
3064
3181
  return {
3065
- tasksTable: queue,
3066
- historyTable: `${queue}_history`
3182
+ tasksTable: queueName,
3183
+ historyTable: `${queueName}_history`,
3184
+ registryTable: `${queueName}_services_registry`
3067
3185
  };
3068
3186
  }
3069
- function servicesRegistryTable(queue) {
3070
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
3071
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
3072
- }
3073
- return `${queue}_services_registry`;
3187
+ function defineTasksTable(t, db, tableNameForIndex) {
3188
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3189
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3190
+ t.timestamp("started_at");
3191
+ t.timestamp("completed_at");
3192
+ t.integer("priority").notNullable().defaultTo(50);
3193
+ t.text("schedule");
3194
+ t.timestamp("next_run_at").defaultTo(null);
3195
+ t.timestamp("past_due").defaultTo(null);
3196
+ t.text("name").notNullable();
3197
+ t.text("opid");
3198
+ t.json("params");
3199
+ t.text("service_group");
3200
+ t.integer("instance_number");
3201
+ t.text("service_name");
3202
+ t.text("server_name");
3203
+ t.text("status").notNullable().defaultTo("idle");
3204
+ t.timestamp("status_changed_at").defaultTo(null);
3205
+ t.text("progress");
3206
+ t.boolean("success");
3207
+ t.json("results");
3208
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
3209
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
3210
+ }
3211
+ function taskHistoryInsertFromQueueRow(row, overrides) {
3212
+ const { id, ...snapshot } = row;
3213
+ void id;
3214
+ return {
3215
+ ...snapshot,
3216
+ ...overrides
3217
+ };
3074
3218
  }
3075
3219
  async function ensureTaskTables(context, options = {}) {
3076
- const queue = options.queue ?? "tasks";
3220
+ const queueName = options.queueName ?? "tasks";
3077
3221
  const recreate = options.recreate ?? false;
3078
3222
  const db = getDb(context);
3079
- const { tasksTable, historyTable } = queueToTableNames(queue);
3223
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
3080
3224
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
3081
3225
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
3226
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
3082
3227
  if (recreate) {
3083
3228
  await db.schema.dropTableIfExists(historyTable);
3084
3229
  await db.schema.dropTableIfExists(tasksTable);
3230
+ await db.schema.dropTableIfExists(registryTable);
3085
3231
  }
3086
3232
  if (needsTasks) {
3087
3233
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
3088
3234
  await db.schema.createTable(tasksTable, (t) => {
3089
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3090
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3091
- t.timestamp("started_at");
3092
- t.timestamp("completed_at");
3093
- t.integer("priority").notNullable().defaultTo(0);
3094
- t.text("schedule");
3095
- t.timestamp("past_due").defaultTo(null);
3096
- t.text("target").notNullable();
3097
- t.text("task").notNullable();
3098
- t.json("params");
3099
- t.text("opid");
3100
- t.timestamp("paused_at").defaultTo(null);
3101
- t.text("progress");
3102
- t.boolean("success");
3103
- t.json("results");
3104
- });
3105
- await db.schema.alterTable(tasksTable, (t) => {
3106
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
3107
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
3108
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
3235
+ defineTasksTable(t, db, tasksTable);
3109
3236
  });
3110
3237
  }
3111
3238
  if (needsHistory) {
3112
3239
  await db.schema.createTable(historyTable, (t) => {
3113
- t.uuid("id").notNullable();
3114
- t.timestamp("created_at").notNullable();
3115
- t.timestamp("started_at");
3116
- t.timestamp("completed_at");
3117
- t.integer("priority").notNullable().defaultTo(0);
3118
- t.text("schedule");
3119
- t.timestamp("past_due").defaultTo(null);
3120
- t.text("target").notNullable();
3121
- t.text("task").notNullable();
3122
- t.json("params");
3123
- t.text("opid");
3124
- t.text("progress");
3125
- t.boolean("success");
3126
- t.json("results");
3127
- });
3128
- await db.schema.alterTable(historyTable, (t) => {
3129
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
3130
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
3240
+ defineTasksTable(t, db, historyTable);
3131
3241
  });
3132
3242
  }
3133
- const registryTable = servicesRegistryTable(queue);
3134
- const needsRegistry = !await db.tableExists(registryTable);
3135
3243
  if (needsRegistry) {
3136
3244
  await db.schema.createTable(registryTable, (t) => {
3137
3245
  t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3138
- t.uuid("instance_id").notNullable().unique();
3139
- t.text("queue").notNullable();
3246
+ t.text("queue_name").notNullable();
3140
3247
  t.text("service_group").notNullable();
3248
+ t.integer("instance_number").notNullable().defaultTo(1);
3141
3249
  t.text("service_name").notNullable();
3142
- t.text("target").notNullable();
3143
- t.text("hostname");
3250
+ t.text("server_name").notNullable();
3144
3251
  t.integer("pid");
3145
3252
  t.json("metadata");
3146
3253
  t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3147
3254
  t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
3148
- t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
3149
- t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
3150
- t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
3255
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
3256
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
3257
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
3151
3258
  });
3152
3259
  }
3153
3260
  }
@@ -3158,7 +3265,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
3158
3265
  });
3159
3266
  }
3160
3267
 
3161
- // src/tasks/servicesRegistry.ts
3268
+ // src/tasks/servicesRegistry.js
3162
3269
  function getDb2(context) {
3163
3270
  const db = context.db;
3164
3271
  if (!db) {
@@ -3169,6 +3276,7 @@ function getDb2(context) {
3169
3276
  var DEFAULT_GROUP_MAX_INSTANCES = {
3170
3277
  intake: 1,
3171
3278
  harvest: 1,
3279
+ harvester: 0,
3172
3280
  loader: 0,
3173
3281
  photos: 0,
3174
3282
  photosprocessor: 0,
@@ -3178,25 +3286,6 @@ function sanitizeNamePart(raw) {
3178
3286
  const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3179
3287
  return s.slice(0, 80) || "runner";
3180
3288
  }
3181
- function identityFilePath(identityDir, queue, serviceGroup) {
3182
- const safeQ = sanitizeNamePart(queue);
3183
- const safeG = sanitizeNamePart(serviceGroup);
3184
- return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
3185
- }
3186
- async function readIdentityFile(filePath) {
3187
- try {
3188
- const text = await (0, import_promises.readFile)(filePath, "utf8");
3189
- const parsed = JSON.parse(text);
3190
- return parsed && typeof parsed === "object" ? parsed : {};
3191
- } catch {
3192
- return {};
3193
- }
3194
- }
3195
- async function writeIdentityFile(filePath, data) {
3196
- await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
3197
- await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
3198
- `, "utf8");
3199
- }
3200
3289
  function resolveMaxInstances(serviceGroup, override) {
3201
3290
  if (override !== void 0 && Number.isFinite(override)) {
3202
3291
  return Math.max(0, Math.floor(Number(override)));
@@ -3204,184 +3293,198 @@ function resolveMaxInstances(serviceGroup, override) {
3204
3293
  const g = serviceGroup.trim().toLowerCase();
3205
3294
  return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
3206
3295
  }
3207
- async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
3296
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
3208
3297
  const cutoff = new Date(Date.now() - staleMs);
3209
- let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
3210
- if (excludeInstanceId) {
3211
- q = q.whereNot("instance_id", excludeInstanceId);
3298
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
3299
+ if (excludeRowId) {
3300
+ q = q.whereNot("id", excludeRowId);
3212
3301
  }
3213
3302
  const row = await q.count("id as count").first();
3214
3303
  return Number(row?.count ?? 0);
3215
3304
  }
3305
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
3306
+ const cutoff = new Date(Date.now() - staleMs);
3307
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
3308
+ const set = /* @__PURE__ */ new Set();
3309
+ for (const r of rows) {
3310
+ const n = Number(r.instance_number);
3311
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
3312
+ }
3313
+ return set;
3314
+ }
3216
3315
  function isUniqueViolation(error) {
3217
3316
  const code = error?.code ?? error?.errno;
3218
3317
  return code === "23505" || String(error?.message || "").includes("duplicate key");
3219
3318
  }
3319
+ function buildMetadata(options) {
3320
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
3321
+ if (options.target) {
3322
+ base.runnerTarget = options.target;
3323
+ }
3324
+ return toJsonColumn(Object.keys(base).length ? base : null);
3325
+ }
3326
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
3327
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
3328
+ const e = Math.max(1, Math.floor(Number(explicit)));
3329
+ if (occupied.has(e)) {
3330
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
3331
+ }
3332
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
3333
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
3334
+ }
3335
+ return e;
3336
+ }
3337
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
3338
+ for (let n = 1; n <= cap; n++) {
3339
+ if (!occupied.has(n)) return n;
3340
+ }
3341
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
3342
+ }
3343
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
3344
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
3345
+ }
3220
3346
  async function registerInServicesRegistry(context, options) {
3221
3347
  const db = getDb2(context);
3222
- const registryTable = servicesRegistryTable(options.queue);
3348
+ const registryTable = queueToTableNames(options.queueName).registryTable;
3223
3349
  const serviceGroup = options.serviceGroup.trim();
3224
3350
  if (!serviceGroup) {
3225
3351
  throw new Error("registerInServicesRegistry: serviceGroup is required");
3226
3352
  }
3227
- const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
3228
- let identity = await readIdentityFile(identityPath);
3229
- let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
3230
- identity.instanceId = instanceId;
3231
- await writeIdentityFile(identityPath, identity);
3232
- const hostname = import_node_os.default.hostname();
3353
+ const serverName = import_node_os.default.hostname();
3233
3354
  const pid = typeof process.pid === "number" ? process.pid : null;
3234
- const meta = toJsonColumn(options.metadata ?? null);
3235
- const existing = await db(registryTable).where({ instance_id: instanceId }).first();
3236
- if (existing) {
3237
- await db(registryTable).where({ instance_id: instanceId }).update({
3238
- target: options.target,
3239
- hostname,
3240
- pid,
3241
- metadata: meta,
3242
- last_seen_at: db.fn.now()
3243
- });
3244
- const serviceName = String(existing.service_name);
3245
- identity.serviceName = serviceName;
3246
- await writeIdentityFile(identityPath, identity);
3247
- const reg = {
3248
- instanceId,
3249
- serviceName,
3250
- serviceGroup,
3251
- queue: options.queue,
3252
- target: options.target,
3253
- rowId: String(existing.id)
3254
- };
3255
- context.servicesRegistry = reg;
3256
- context.runnerHeartbeat = reg;
3257
- context.logger.info?.(
3258
- `[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
3259
- );
3260
- return {
3261
- instanceId,
3262
- serviceName,
3263
- serviceGroup,
3264
- queue: options.queue,
3265
- target: options.target,
3266
- rowId: String(existing.id),
3267
- registryTable
3268
- };
3269
- }
3355
+ const meta = buildMetadata(options);
3356
+ const groupBase = sanitizeNamePart(serviceGroup);
3357
+ const hostBase = sanitizeNamePart(serverName);
3270
3358
  const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
3271
- const aliveOthers = await countAliveInGroup(
3272
- db,
3273
- registryTable,
3274
- options.queue,
3275
- serviceGroup,
3276
- options.staleMs,
3277
- instanceId
3278
- );
3279
- if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
3280
- const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
3359
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
3360
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
3361
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
3281
3362
  if (options.enforceMaxInstances) {
3282
3363
  throw new Error(msg);
3283
3364
  }
3284
3365
  context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
3285
3366
  }
3286
- const explicitName = options.serviceName?.trim();
3287
- const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
3288
- const hostBase = sanitizeNamePart(hostname);
3289
- const groupBase = sanitizeNamePart(serviceGroup);
3290
- const baseCandidates = [];
3291
- if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
3292
- if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
3293
- baseCandidates.push(`${groupBase}-${hostBase}`);
3294
- baseCandidates.push(groupBase);
3295
- function* eachServiceNameCandidate(bases) {
3296
- const seen = /* @__PURE__ */ new Set();
3297
- for (const rawBase of bases) {
3298
- const base = sanitizeNamePart(rawBase);
3299
- if (!base) continue;
3300
- const seq = [base];
3301
- for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
3302
- for (const c of seq) {
3303
- if (seen.has(c)) continue;
3304
- seen.add(c);
3305
- yield c;
3367
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
3368
+ const cutoff = new Date(Date.now() - options.staleMs);
3369
+ const MAX_ATTEMPTS = 8;
3370
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
3371
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
3372
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
3373
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
3374
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
3375
+ if (existing) {
3376
+ const lastSeen = new Date(existing.last_seen_at);
3377
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
3378
+ if (isAlive) {
3379
+ if (options.serviceName?.trim()) {
3380
+ throw new Error(
3381
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
3382
+ );
3383
+ }
3384
+ context.logger.warn?.(
3385
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
3386
+ );
3387
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
3388
+ throw new Error(
3389
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
3390
+ );
3391
+ }
3392
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
3393
+ continue;
3306
3394
  }
3395
+ await db(registryTable).where({ id: existing.id }).update({
3396
+ server_name: serverName,
3397
+ pid,
3398
+ metadata: meta,
3399
+ service_group: serviceGroup,
3400
+ instance_number: instanceNumber,
3401
+ last_seen_at: db.fn.now()
3402
+ });
3403
+ const reg = {
3404
+ serviceName: serviceNameRaw,
3405
+ serviceGroup,
3406
+ queueName: options.queueName,
3407
+ target: options.target,
3408
+ rowId: String(existing.id),
3409
+ registryTable,
3410
+ instanceNumber
3411
+ };
3412
+ context.servicesRegistry = reg;
3413
+ context.runnerHeartbeat = reg;
3414
+ context.logger.info?.(
3415
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
3416
+ );
3417
+ return reg;
3307
3418
  }
3308
- }
3309
- let inserted;
3310
- for (const candidate of eachServiceNameCandidate(baseCandidates)) {
3311
3419
  try {
3312
3420
  const rows = await db(registryTable).insert({
3313
- instance_id: instanceId,
3314
- queue: options.queue,
3421
+ queue_name: options.queueName,
3315
3422
  service_group: serviceGroup,
3316
- service_name: candidate,
3317
- target: options.target,
3318
- hostname,
3423
+ instance_number: instanceNumber,
3424
+ service_name: serviceNameRaw,
3425
+ server_name: serverName,
3319
3426
  pid,
3320
3427
  metadata: meta,
3321
- last_seen_at: db.fn.now()
3428
+ last_seen_at: db.fn.now(),
3429
+ created_at: db.fn.now()
3322
3430
  }).returning(["id", "service_name"]);
3323
3431
  const row = Array.isArray(rows) ? rows[0] : rows;
3324
- if (row) {
3325
- inserted = { id: String(row.id), service_name: String(row.service_name) };
3326
- break;
3432
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
3433
+ if (!rowId) {
3434
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
3435
+ rowId = again?.id != null ? String(again.id) : "";
3327
3436
  }
3437
+ if (!rowId) continue;
3438
+ const regNew = {
3439
+ serviceName: String(row?.service_name ?? serviceNameRaw),
3440
+ serviceGroup,
3441
+ queueName: options.queueName,
3442
+ target: options.target,
3443
+ rowId,
3444
+ registryTable,
3445
+ instanceNumber
3446
+ };
3447
+ context.servicesRegistry = regNew;
3448
+ context.runnerHeartbeat = regNew;
3449
+ context.logger.info?.(
3450
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
3451
+ );
3452
+ return regNew;
3328
3453
  } catch (error) {
3329
3454
  if (!isUniqueViolation(error)) {
3330
3455
  throw error;
3331
3456
  }
3457
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
3332
3458
  }
3333
3459
  }
3334
- if (!inserted) {
3335
- throw new Error(
3336
- `[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
3337
- );
3338
- }
3339
- identity.serviceName = inserted.service_name;
3340
- await writeIdentityFile(identityPath, identity);
3341
- const regNew = {
3342
- instanceId,
3343
- serviceName: inserted.service_name,
3344
- serviceGroup,
3345
- queue: options.queue,
3346
- target: options.target,
3347
- rowId: inserted.id
3348
- };
3349
- context.servicesRegistry = regNew;
3350
- context.runnerHeartbeat = regNew;
3351
- context.logger.info?.(
3352
- `[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
3460
+ throw new Error(
3461
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
3353
3462
  );
3354
- return {
3355
- instanceId,
3356
- serviceName: inserted.service_name,
3357
- serviceGroup,
3358
- queue: options.queue,
3359
- target: options.target,
3360
- rowId: inserted.id,
3361
- registryTable
3362
- };
3363
3463
  }
3364
3464
  async function touchServicesRegistry(context, registration) {
3365
3465
  const db = getDb2(context);
3366
- const hostname = import_node_os.default.hostname();
3466
+ const serverName = import_node_os.default.hostname();
3367
3467
  const pid = typeof process.pid === "number" ? process.pid : null;
3368
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
3468
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
3369
3469
  last_seen_at: db.fn.now(),
3370
- hostname,
3470
+ server_name: serverName,
3371
3471
  pid
3372
3472
  });
3373
3473
  }
3374
3474
  async function unregisterServicesRegistry(context, registration) {
3375
3475
  const db = getDb2(context);
3376
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
3377
- context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
3476
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
3477
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
3378
3478
  }
3379
3479
 
3380
- // src/filedatabase/index.ts
3480
+ // src/tasks/taskLogs.js
3481
+ var import_node_path = __toESM(require("path"), 1);
3482
+
3483
+ // src/filedatabase/index.js
3381
3484
  var import_fs4 = __toESM(require("fs"), 1);
3382
3485
  var import_path4 = __toESM(require("path"), 1);
3383
3486
 
3384
- // src/filedatabase/serializers.ts
3487
+ // src/filedatabase/serializers.js
3385
3488
  function detectDataType(data) {
3386
3489
  if (Array.isArray(data)) {
3387
3490
  return "json-array";
@@ -3413,7 +3516,7 @@ function deserializeData(rawData, dataType) {
3413
3516
  }
3414
3517
  }
3415
3518
 
3416
- // src/filedatabase/index.ts
3519
+ // src/filedatabase/index.js
3417
3520
  var FileDatabase = class _FileDatabase {
3418
3521
  basePath;
3419
3522
  namespace;
@@ -3499,7 +3602,7 @@ var FileDatabase = class _FileDatabase {
3499
3602
  if (errors.length) {
3500
3603
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
3501
3604
  }
3502
- let parts = [this.basePath, this.namespace];
3605
+ const parts = [this.basePath, this.namespace];
3503
3606
  if (this.tableName) {
3504
3607
  parts.push(...this.tableName.split("/"));
3505
3608
  }
@@ -3992,14 +4095,17 @@ var FileDatabase = class _FileDatabase {
3992
4095
  * Prepare the instance for read or write operations
3993
4096
  * This discovers state and sets up internal members based on mode and current data
3994
4097
  */
3995
- async prepare({ write, read, version }) {
4098
+ async prepare(options) {
4099
+ const { write, read, version, deferInitialVersion } = options;
3996
4100
  if (write) {
3997
4101
  if (this.versioned) {
3998
4102
  if (this.currentVersion === null) {
3999
- await this.makeNewVersion();
4000
- this.metadata = this.getDefaultMetadata();
4001
- this.metadata.version = this.currentVersion;
4002
- this.makeNewFile();
4103
+ if (!deferInitialVersion) {
4104
+ await this.makeNewVersion();
4105
+ this.metadata = this.getDefaultMetadata();
4106
+ this.metadata.version = this.currentVersion;
4107
+ this.makeNewFile();
4108
+ }
4003
4109
  } else {
4004
4110
  if (!this.metadata.files.length) {
4005
4111
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -4109,7 +4215,7 @@ var FileDatabase = class _FileDatabase {
4109
4215
  if (options.forceNewVersion && !this.versioned) {
4110
4216
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
4111
4217
  }
4112
- await this.prepare({ write: true });
4218
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
4113
4219
  const incomingDataType = detectDataType(data);
4114
4220
  this.metadata.dataType = incomingDataType;
4115
4221
  if (options.forceNewVersion) {
@@ -4403,7 +4509,7 @@ var FileDatabase = class _FileDatabase {
4403
4509
  }
4404
4510
  };
4405
4511
 
4406
- // src/tasks/taskLogs.ts
4512
+ // src/tasks/taskLogs.js
4407
4513
  function getLogsState(context) {
4408
4514
  const holder = context;
4409
4515
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -4456,33 +4562,128 @@ function getLogsState(context) {
4456
4562
  holder.__tasksLogsState = state;
4457
4563
  return state;
4458
4564
  }
4459
- function isErrorPayload(payload) {
4460
- if (!payload) return false;
4461
- if (typeof payload === "object") {
4462
- const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
4463
- if (level === "error" || level === "fatal") return true;
4464
- if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
4465
- return false;
4466
- }
4467
- if (typeof payload === "string") {
4468
- return /\berror\b/i.test(payload);
4469
- }
4470
- return false;
4565
+ function ipcLogTargetKey(target) {
4566
+ const bp = target.basePath ?? "";
4567
+ const ns = target.namespace ?? "";
4568
+ return `${bp}::${ns}::${target.tableName}`;
4471
4569
  }
4472
- function buildLogRecord(task, payload) {
4473
- const params = task.params && typeof task.params === "object" ? task.params : {};
4474
- return {
4475
- ts: (/* @__PURE__ */ new Date()).toISOString(),
4476
- opid: task.opid ?? null,
4477
- taskId: task.id,
4478
- taskName: task.task,
4479
- target: task.target,
4570
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
4571
+ const seg = (s) => {
4572
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
4573
+ return t.length ? t : "x";
4574
+ };
4575
+ return `${seg(source)}/${seg(resource)}`;
4576
+ }
4577
+ async function readTaskIpcLogsSnapshot(context, options) {
4578
+ const holder = context;
4579
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
4580
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
4581
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
4582
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
4583
+ const fd = new FileDatabase({
4584
+ basePath,
4585
+ namespace,
4586
+ tableName,
4587
+ versioned: true,
4588
+ useMetadata: true,
4589
+ maxVersions: 30,
4590
+ pageSize: 2e3,
4591
+ logger: holder.logger
4592
+ });
4593
+ const versions = await fd.getVersions();
4594
+ if (versions.length === 0) {
4595
+ return { records: [], latestTs: null };
4596
+ }
4597
+ const latest = versions[versions.length - 1];
4598
+ const raw = await fd.read({ version: latest });
4599
+ const arr = Array.isArray(raw) ? raw : [];
4600
+ let filtered = arr;
4601
+ if (options.afterTs && String(options.afterTs).trim()) {
4602
+ const cut = String(options.afterTs).trim();
4603
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
4604
+ }
4605
+ let latestTs = null;
4606
+ for (const r of filtered) {
4607
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
4608
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
4609
+ }
4610
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
4611
+ const maxReturn = incremental ? 1e4 : tail;
4612
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
4613
+ return { records: sliced, latestTs };
4614
+ }
4615
+ function getLogsStateForTarget(context, target) {
4616
+ const holder = context;
4617
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4618
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4619
+ if (!enabled) return null;
4620
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
4621
+ const map = holder.__tasksLogsTargetStates;
4622
+ const key = ipcLogTargetKey(target);
4623
+ if (map.has(key)) return map.get(key);
4624
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4625
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4626
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4627
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4628
+ const db = new FileDatabase({
4629
+ basePath,
4630
+ namespace,
4631
+ tableName: target.tableName,
4632
+ versioned: true,
4633
+ useMetadata: true,
4634
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4635
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4636
+ logger: holder.logger
4637
+ });
4638
+ const state = {
4639
+ db,
4640
+ errorDb: null,
4641
+ queue: Promise.resolve(),
4642
+ initialized: false,
4643
+ errorInitialized: false
4644
+ };
4645
+ map.set(key, state);
4646
+ return state;
4647
+ }
4648
+ function isErrorPayload(payload) {
4649
+ if (!payload) return false;
4650
+ if (typeof payload === "object") {
4651
+ const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
4652
+ if (level === "error" || level === "fatal") return true;
4653
+ if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
4654
+ return false;
4655
+ }
4656
+ if (typeof payload === "string") {
4657
+ return /\berror\b/i.test(payload);
4658
+ }
4659
+ return false;
4660
+ }
4661
+ function buildLogRecord(task, payload) {
4662
+ const params = task.params && typeof task.params === "object" ? task.params : {};
4663
+ return {
4664
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4665
+ opid: task.opid ?? null,
4666
+ taskId: task.id,
4667
+ taskName: task.name,
4668
+ target: task.service_group,
4480
4669
  source: typeof params.source === "string" ? params.source : null,
4481
4670
  resource: typeof params.resource === "string" ? params.resource : null,
4482
4671
  payload
4483
4672
  };
4484
4673
  }
4485
- function appendTaskIpcLog(context, task, payload) {
4674
+ function appendTaskIpcLog(context, task, payload, target) {
4675
+ if (target) {
4676
+ const state2 = getLogsStateForTarget(context, target);
4677
+ if (!state2?.db) return;
4678
+ const record2 = buildLogRecord(task, payload);
4679
+ state2.queue = state2.queue.then(async () => {
4680
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
4681
+ state2.initialized = true;
4682
+ }).catch((error) => {
4683
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
4684
+ });
4685
+ return;
4686
+ }
4486
4687
  const state = getLogsState(context);
4487
4688
  if (!state.db && !state.errorDb) return;
4488
4689
  const record = buildLogRecord(task, payload);
@@ -4500,83 +4701,280 @@ function appendTaskIpcLog(context, task, payload) {
4500
4701
  });
4501
4702
  }
4502
4703
 
4503
- // src/tasks/time-matcher.ts
4504
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4505
- function resolveAsterisks(field, range) {
4506
- return field.includes("*") ? field.replace("*", range) : field;
4507
- }
4508
- function resolveRanges(field) {
4509
- const regex = /(\d+)-(\d+)/;
4510
- let current = field;
4511
- while (true) {
4512
- const match = regex.exec(current);
4513
- if (!match) break;
4514
- const raw = match[0];
4515
- let first = Number(match[1]);
4516
- let last = Number(match[2]);
4517
- if (last < first) {
4518
- [first, last] = [last, first];
4519
- }
4520
- const values = [];
4521
- for (let i = first; i <= last; i += 1) {
4522
- values.push(i);
4523
- }
4524
- current = current.replace(raw, values.join(","));
4525
- }
4526
- return current;
4527
- }
4528
- function resolveSteps(field) {
4529
- const match = /^(.+)\/(\d+)$/.exec(field);
4530
- if (!match) return field;
4531
- const base = match[1];
4532
- const step = Number(match[2]);
4533
- if (!Number.isFinite(step) || step <= 0) return field;
4534
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4535
- }
4536
- function convertPattern(pattern) {
4537
- const parts = pattern.trim().split(/\s+/);
4538
- if (parts.length !== 6) {
4539
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4540
- }
4541
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4542
- }
4543
- function fieldMatches(field, value) {
4544
- const allowed = field.split(",").map((v) => Number(v));
4545
- return allowed.includes(value);
4546
- }
4547
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4548
- const parsed = convertPattern(pattern);
4549
- return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
4550
- }
4551
-
4552
- // src/tasks/TaskMaster.ts
4553
- var TaskMaster = class {
4554
- context;
4555
- task;
4704
+ // src/tasks/AbstractTask.js
4705
+ var AbstractTask = class _AbstractTask {
4706
+ /**
4707
+ * Whether `send-task` should wait for completion (and print a result
4708
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
4709
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
4710
+ *
4711
+ * @type {boolean}
4712
+ */
4713
+ static defaultWaitForResult = false;
4714
+ /**
4715
+ * @param {object} context Runner context (db, logger, params, emitter...).
4716
+ * @param {object} task Task row as claimed from the queue.
4717
+ */
4556
4718
  constructor(context, task) {
4557
4719
  this.context = context;
4558
4720
  this.task = task;
4559
4721
  }
4722
+ /**
4723
+ * Return a short reason string when the task should be deferred (e.g. "locked
4724
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
4725
+ *
4726
+ * @returns {string | false | Promise<string | false>}
4727
+ */
4560
4728
  cantRunReason() {
4561
4729
  return false;
4562
4730
  }
4731
+ /**
4732
+ * Called by the runner when a stop has been requested. Subclasses running
4733
+ * long loops should flip a flag here and check it between iterations.
4734
+ *
4735
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
4736
+ */
4563
4737
  requestStop(_allowanceMs) {
4564
4738
  }
4739
+ /**
4740
+ * Perform the task. Must be implemented by subclasses.
4741
+ *
4742
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
4743
+ * Updates the DB `progress` column. Accepts any serializable value;
4744
+ * strings are stored verbatim, objects are JSON-stringified.
4745
+ * @returns {Promise<{ success: boolean, results: unknown }>}
4746
+ */
4747
+ async run(_reportProgress) {
4748
+ throw new Error("AbstractTask.run must be implemented by subclass");
4749
+ }
4750
+ /**
4751
+ * Resolve a complete row payload for this task — envelope fields (queue,
4752
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
4753
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
4754
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
4755
+ *
4756
+ * const payload = await TaskClass.resolveParams(context, { name });
4757
+ * await enqueueTask(context, payload);
4758
+ *
4759
+ * Validation failures throw {@link ParamError} so the script aborts before
4760
+ * a malformed row hits the DB.
4761
+ *
4762
+ * @param {object} context
4763
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
4764
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
4765
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
4766
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
4767
+ * @returns {Promise<object>}
4768
+ */
4769
+ static async resolveParams(context, overrides = {}) {
4770
+ const main = _AbstractTask._resolveMainFields(context, overrides);
4771
+ const params = await this.resolveCustomParams(context, overrides);
4772
+ return { ...main, params };
4773
+ }
4774
+ /**
4775
+ * Resolve the inner JSON blob stored in the `params` column. Default
4776
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
4777
+ * overlaid with `overrides.params` when supplied; returns `null` when
4778
+ * neither is provided.
4779
+ *
4780
+ * Subclasses with typed fields should override and call
4781
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
4782
+ * resolution, then validate and throw {@link ParamError} on bad input.
4783
+ *
4784
+ * @param {object} context
4785
+ * @param {Record<string, unknown>} [overrides]
4786
+ * @returns {Promise<object|null>}
4787
+ */
4788
+ static async resolveCustomParams(context, overrides = {}) {
4789
+ return _AbstractTask._defaultParamsBlob(context, overrides);
4790
+ }
4791
+ /**
4792
+ * Read main task envelope fields from `context.params` (CLI/env), with
4793
+ * any matching key on `overrides` taking precedence. Internal; called by
4794
+ * {@link AbstractTask.resolveParams}.
4795
+ *
4796
+ * @param {object} context
4797
+ * @param {Record<string, unknown>} [overrides]
4798
+ * @returns {object}
4799
+ */
4800
+ static _resolveMainFields(context, overrides = {}) {
4801
+ const defs2 = {
4802
+ queueName: "string default tasks",
4803
+ priority: "number default 50",
4804
+ serviceGroup: "string",
4805
+ serviceName: "string",
4806
+ instanceNumber: "number",
4807
+ serverName: "string",
4808
+ opid: "string",
4809
+ schedule: "string"
4810
+ };
4811
+ const cli = context.params.getAllForModule("task-envelope", defs2);
4812
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
4813
+ if (!name) {
4814
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
4815
+ }
4816
+ let instanceNumber;
4817
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
4818
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
4819
+ const n = Number(rawInstance);
4820
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
4821
+ throw new ParamError("--instanceNumber must be a positive integer when set");
4822
+ }
4823
+ instanceNumber = n;
4824
+ } else {
4825
+ instanceNumber = null;
4826
+ }
4827
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
4828
+ const priority = Number(priorityRaw);
4829
+ if (!Number.isFinite(priority)) {
4830
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
4831
+ }
4832
+ return {
4833
+ name,
4834
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
4835
+ priority,
4836
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
4837
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
4838
+ instanceNumber,
4839
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
4840
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
4841
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
4842
+ nextRunAt: overrides.nextRunAt ?? null
4843
+ };
4844
+ }
4845
+ /**
4846
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
4847
+ * object), then overlays `overrides.params` on top. Returns `null` when
4848
+ * neither is provided.
4849
+ *
4850
+ * @param {object} context
4851
+ * @param {Record<string, unknown>} [overrides]
4852
+ * @returns {object|null}
4853
+ */
4854
+ static _defaultParamsBlob(context, overrides = {}) {
4855
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
4856
+ const fromJson = parseParamsJson(cli.paramsJson);
4857
+ const fromOverride = pickParamsObject(overrides);
4858
+ if (!fromJson && !fromOverride) return null;
4859
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
4860
+ }
4861
+ /**
4862
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
4863
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
4864
+ * merges them with explicit `overrides.params` in increasing priority:
4865
+ *
4866
+ * typed CLI flags → --paramsJson → overrides.params
4867
+ *
4868
+ * Undefined values are dropped so defaults declared in `defs` aren't
4869
+ * overwritten by missing-flag noise. Returns the merged object; the
4870
+ * caller is responsible for validation and throwing `ParamError`.
4871
+ *
4872
+ * @param {object} context
4873
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
4874
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
4875
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
4876
+ * @returns {Record<string, unknown>}
4877
+ */
4878
+ static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
4879
+ const fullDefs = { ...defs2, paramsJson: "string" };
4880
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
4881
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
4882
+ const fromCli = {};
4883
+ for (const [k, v] of Object.entries(cliRaw)) {
4884
+ if (k === "paramsJson") continue;
4885
+ if (v !== void 0 && v !== null) fromCli[k] = v;
4886
+ }
4887
+ const fromOverride = pickParamsObject(overrides) ?? {};
4888
+ return { ...fromCli, ...fromJson, ...fromOverride };
4889
+ }
4565
4890
  };
4891
+ function emptyToUndef(s) {
4892
+ if (s === void 0 || s === null) return void 0;
4893
+ if (typeof s !== "string") return s;
4894
+ const t = s.trim();
4895
+ return t.length ? t : void 0;
4896
+ }
4897
+ function parseParamsJson(raw) {
4898
+ if (raw == null) return null;
4899
+ const t = String(raw).trim();
4900
+ if (!t) return null;
4901
+ let parsed;
4902
+ try {
4903
+ parsed = JSON.parse(t);
4904
+ } catch (e) {
4905
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
4906
+ }
4907
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4908
+ throw new ParamError("--paramsJson must be a JSON object");
4909
+ }
4910
+ return parsed;
4911
+ }
4912
+ function pickParamsObject(overrides) {
4913
+ const p = overrides?.params;
4914
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
4915
+ return void 0;
4916
+ }
4566
4917
 
4567
- // src/tasks/coreTasks/TaskPing.ts
4568
- var TaskPing = class extends TaskMaster {
4918
+ // src/tasks/coreTasks/TaskPing.js
4919
+ var TaskPing = class extends AbstractTask {
4920
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
4921
+ static defaultWaitForResult = true;
4922
+ /** Ping takes no params. */
4923
+ static async resolveCustomParams() {
4924
+ return null;
4925
+ }
4926
+ /**
4927
+ * @returns {Promise<{ success: true, results: "pong" }>}
4928
+ */
4569
4929
  async run() {
4570
4930
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4571
4931
  return { success: true, results: "pong" };
4572
4932
  }
4573
4933
  };
4574
4934
 
4575
- // src/tasks/coreTasks/TaskSampleProcess.ts
4576
- var TaskSampleProcess = class extends TaskMaster {
4577
- stopRequested = false;
4578
- stopAllowanceMs = 0;
4579
- stopDecisionLogged = false;
4935
+ // src/tasks/coreTasks/TaskSampleProcess.js
4936
+ var TaskSampleProcess = class extends AbstractTask {
4937
+ /**
4938
+ * @param {object} context
4939
+ * @param {Record<string, unknown>} [overrides]
4940
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
4941
+ */
4942
+ static async resolveCustomParams(context, overrides = {}) {
4943
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
4944
+ total: "number default 10",
4945
+ delay: "number default 1000",
4946
+ name: "string"
4947
+ }, overrides);
4948
+ const total = Number(merged.total);
4949
+ const delay = Number(merged.delay);
4950
+ if (!Number.isInteger(total) || total <= 0) {
4951
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
4952
+ }
4953
+ if (!Number.isInteger(delay) || delay < 0) {
4954
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
4955
+ }
4956
+ const out = { total, delay };
4957
+ if (typeof merged.name === "string" && merged.name.trim()) {
4958
+ out.name = merged.name.trim();
4959
+ }
4960
+ return out;
4961
+ }
4962
+ /**
4963
+ * @param {object} context
4964
+ * @param {object} task
4965
+ */
4966
+ constructor(context, task) {
4967
+ super(context, task);
4968
+ this.stopRequested = false;
4969
+ this.stopAllowanceMs = 0;
4970
+ this.stopDecisionLogged = false;
4971
+ }
4972
+ /**
4973
+ * Runner-facing stop signal. Records the allowance window so the main loop
4974
+ * can decide per-iteration whether to finish or abort early.
4975
+ *
4976
+ * @param {number} allowanceMs
4977
+ */
4580
4978
  requestStop(allowanceMs) {
4581
4979
  this.stopRequested = true;
4582
4980
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -4584,6 +4982,14 @@ var TaskSampleProcess = class extends TaskMaster {
4584
4982
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4585
4983
  );
4586
4984
  }
4985
+ /**
4986
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
4987
+ * progress every iteration. Validates params up front; invalid values short-
4988
+ * circuit to a structured failure without starting the loop.
4989
+ *
4990
+ * @param {(progress: object) => Promise<void>} reportProgress
4991
+ * @returns {Promise<{ success: boolean, results: unknown }>}
4992
+ */
4587
4993
  async run(reportProgress) {
4588
4994
  const totalRaw = this.task?.params?.total ?? 10;
4589
4995
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -4665,7 +5071,7 @@ var TaskSampleProcess = class extends TaskMaster {
4665
5071
  }
4666
5072
  };
4667
5073
 
4668
- // src/tasks/coreTasks/TaskShellCommand.ts
5074
+ // src/tasks/coreTasks/TaskShellCommand.js
4669
5075
  var import_node_child_process = require("child_process");
4670
5076
  function runShellCommand(command, cwd) {
4671
5077
  return new Promise((resolve2, reject) => {
@@ -4695,7 +5101,27 @@ function runShellCommand(command, cwd) {
4695
5101
  });
4696
5102
  });
4697
5103
  }
4698
- var TaskShellCommand = class extends TaskMaster {
5104
+ var TaskShellCommand = class extends AbstractTask {
5105
+ /**
5106
+ * @param {object} context
5107
+ * @param {Record<string, unknown>} [overrides]
5108
+ * @returns {Promise<{ command: string, cwd?: string }>}
5109
+ */
5110
+ static async resolveCustomParams(context, overrides = {}) {
5111
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
5112
+ command: "string",
5113
+ cwd: "string"
5114
+ }, overrides);
5115
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
5116
+ if (!command) {
5117
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
5118
+ }
5119
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
5120
+ return cwd ? { command, cwd } : { command };
5121
+ }
5122
+ /**
5123
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5124
+ */
4699
5125
  async run() {
4700
5126
  const params = this.task?.params;
4701
5127
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -4744,9 +5170,9 @@ var TaskShellCommand = class extends TaskMaster {
4744
5170
  }
4745
5171
  };
4746
5172
 
4747
- // src/tasks/coreTasks/TaskSystemInfo.ts
5173
+ // src/tasks/coreTasks/TaskSystemInfo.js
4748
5174
  var import_node_os2 = __toESM(require("os"), 1);
4749
- var import_promises2 = __toESM(require("fs/promises"), 1);
5175
+ var import_promises = __toESM(require("fs/promises"), 1);
4750
5176
  function toGb(valueBytes) {
4751
5177
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
4752
5178
  }
@@ -4754,7 +5180,7 @@ function toMb(valueBytes) {
4754
5180
  return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
4755
5181
  }
4756
5182
  async function getDiskStats() {
4757
- const stats = await import_promises2.default.statfs("/");
5183
+ const stats = await import_promises.default.statfs("/");
4758
5184
  const total = Number(stats.bsize) * Number(stats.blocks);
4759
5185
  const free = Number(stats.bsize) * Number(stats.bavail);
4760
5186
  const used = total - free;
@@ -4764,7 +5190,16 @@ async function getDiskStats() {
4764
5190
  free: toGb(free)
4765
5191
  };
4766
5192
  }
4767
- var TaskSystemInfo = class extends TaskMaster {
5193
+ var TaskSystemInfo = class extends AbstractTask {
5194
+ /** Same UX expectation as `ping` — short probe, print the result. */
5195
+ static defaultWaitForResult = true;
5196
+ /** systemInfo takes no params. */
5197
+ static async resolveCustomParams() {
5198
+ return null;
5199
+ }
5200
+ /**
5201
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5202
+ */
4768
5203
  async run() {
4769
5204
  try {
4770
5205
  const totalMemory = import_node_os2.default.totalmem();
@@ -4816,8 +5251,31 @@ var TaskSystemInfo = class extends TaskMaster {
4816
5251
  }
4817
5252
  };
4818
5253
 
4819
- // src/tasks/coreTasks/TaskSumAB.ts
4820
- var TaskSumAB = class extends TaskMaster {
5254
+ // src/tasks/coreTasks/TaskSumAB.js
5255
+ var TaskSumAB = class extends AbstractTask {
5256
+ /** Short, deterministic — wait by default so callers see the sum. */
5257
+ static defaultWaitForResult = true;
5258
+ /**
5259
+ * @param {object} context
5260
+ * @param {Record<string, unknown>} [overrides]
5261
+ * @returns {Promise<{ a: number, b: number }>}
5262
+ */
5263
+ static async resolveCustomParams(context, overrides = {}) {
5264
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
5265
+ a: "number",
5266
+ b: "number"
5267
+ }, overrides);
5268
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
5269
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
5270
+ }
5271
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
5272
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
5273
+ }
5274
+ return { a: merged.a, b: merged.b };
5275
+ }
5276
+ /**
5277
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5278
+ */
4821
5279
  async run() {
4822
5280
  const a = this.task?.params?.a;
4823
5281
  const b = this.task?.params?.b;
@@ -4848,8 +5306,46 @@ var TaskSumAB = class extends TaskMaster {
4848
5306
  }
4849
5307
  };
4850
5308
 
4851
- // src/tasks/coreTasks/TaskStopRunner.ts
4852
- var TaskStopRunner = class extends TaskMaster {
5309
+ // src/tasks/coreTasks/TaskStopRunner.js
5310
+ var TaskStopRunner = class extends AbstractTask {
5311
+ /**
5312
+ * Stop tasks must target a concrete instance — without `serviceName` the
5313
+ * row would race against any worker on the queue. Layered on top of the
5314
+ * envelope built by {@link AbstractTask.resolveParams}.
5315
+ *
5316
+ * @param {object} context
5317
+ * @param {Record<string, unknown>} [overrides]
5318
+ * @returns {Promise<object>}
5319
+ */
5320
+ static async resolveParams(context, overrides = {}) {
5321
+ const main = await super.resolveParams(context, overrides);
5322
+ if (!main.serviceName) {
5323
+ throw new ParamError(
5324
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
5325
+ );
5326
+ }
5327
+ return main;
5328
+ }
5329
+ /**
5330
+ * @param {object} context
5331
+ * @param {Record<string, unknown>} [overrides]
5332
+ * @returns {Promise<{ allowanceMs: number }>}
5333
+ */
5334
+ static async resolveCustomParams(context, overrides = {}) {
5335
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
5336
+ allowanceMs: "number default 5000"
5337
+ }, overrides);
5338
+ const allowanceMs = Number(merged.allowanceMs);
5339
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
5340
+ throw new ParamError(
5341
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
5342
+ );
5343
+ }
5344
+ return { allowanceMs };
5345
+ }
5346
+ /**
5347
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
5348
+ */
4853
5349
  async run() {
4854
5350
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4855
5351
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -4864,42 +5360,200 @@ var TaskStopRunner = class extends TaskMaster {
4864
5360
  }
4865
5361
  };
4866
5362
 
4867
- // src/tasks/TasksRegistry.ts
5363
+ // src/tasks/coreTasks/TaskGetLogs.js
5364
+ var TaskGetLogs = class extends AbstractTask {
5365
+ /**
5366
+ * @param {object} context
5367
+ * @param {Record<string, unknown>} [overrides]
5368
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
5369
+ */
5370
+ static async resolveCustomParams(context, overrides = {}) {
5371
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
5372
+ source: "string",
5373
+ resource: "string",
5374
+ tail: "number default 100",
5375
+ afterTs: "string"
5376
+ }, overrides);
5377
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
5378
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
5379
+ if (!source) throw new ParamError('getLogs: param "source" is required');
5380
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
5381
+ let tail = Number(merged.tail);
5382
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
5383
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
5384
+ const out = { source, resource, tail };
5385
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
5386
+ out.afterTs = merged.afterTs.trim();
5387
+ }
5388
+ return out;
5389
+ }
5390
+ /**
5391
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
5392
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5393
+ */
5394
+ async run(_reportProgress) {
5395
+ const p = this.task.params ?? {};
5396
+ const source = String(p.source ?? "").trim();
5397
+ const resource = String(p.resource ?? "").trim();
5398
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
5399
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
5400
+ if (!source || !resource) {
5401
+ return {
5402
+ success: false,
5403
+ results: { error: 'getLogs requires params "source" and "resource"' }
5404
+ };
5405
+ }
5406
+ try {
5407
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
5408
+ source,
5409
+ resource,
5410
+ tail,
5411
+ afterTs
5412
+ });
5413
+ return {
5414
+ success: true,
5415
+ results: { records, latestTs, source, resource }
5416
+ };
5417
+ } catch (e) {
5418
+ return {
5419
+ success: false,
5420
+ results: { error: e?.message ?? String(e) }
5421
+ };
5422
+ }
5423
+ }
5424
+ };
5425
+
5426
+ // src/tasks/TasksRegistry.js
4868
5427
  var TasksRegistry = class _TasksRegistry {
4869
- map = {};
5428
+ /**
5429
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
5430
+ */
4870
5431
  constructor(initial) {
5432
+ this.map = {};
4871
5433
  if (initial) {
4872
5434
  this.addMany(initial);
4873
5435
  }
4874
5436
  }
5437
+ /**
5438
+ * Build a registry pre-populated with every core task plus legacy aliases.
5439
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
5440
+ *
5441
+ * @returns {TasksRegistry}
5442
+ */
4875
5443
  static withCoreTasks() {
4876
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
5444
+ 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);
4877
5445
  }
5446
+ /**
5447
+ * Register a single task class under a name. Overwrites any previous entry.
5448
+ *
5449
+ * @param {string} taskName
5450
+ * @param {Function} taskClass Subclass of `AbstractTask`.
5451
+ * @returns {this}
5452
+ */
4878
5453
  add(taskName, taskClass) {
4879
5454
  this.map[taskName] = taskClass;
4880
5455
  return this;
4881
5456
  }
5457
+ /**
5458
+ * Bulk-register a name → class map. Later calls override earlier ones.
5459
+ *
5460
+ * @param {Record<string, Function>} entries
5461
+ * @returns {this}
5462
+ */
4882
5463
  addMany(entries) {
4883
5464
  for (const [name, klass] of Object.entries(entries)) {
4884
5465
  this.add(name, klass);
4885
5466
  }
4886
5467
  return this;
4887
5468
  }
5469
+ /**
5470
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
5471
+ * the runner treats that as "some other worker may handle this" and skips.
5472
+ *
5473
+ * @param {string} taskName
5474
+ * @returns {Function | undefined}
5475
+ */
4888
5476
  get(taskName) {
4889
5477
  return this.map[taskName];
4890
5478
  }
5479
+ /**
5480
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
5481
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
5482
+ * paths where an unknown name is a hard CLI/programmer error.
5483
+ *
5484
+ * @param {string} taskName
5485
+ * @returns {Function}
5486
+ */
5487
+ requireClass(taskName) {
5488
+ const TaskClass = taskName ? this.map[taskName] : void 0;
5489
+ if (!TaskClass) {
5490
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
5491
+ throw new ParamError(
5492
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
5493
+ );
5494
+ }
5495
+ return TaskClass;
5496
+ }
5497
+ /**
5498
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
5499
+ * from `overrides` or `context.params`, look up the class, and delegate
5500
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
5501
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
5502
+ *
5503
+ * Validation failures (unknown task, missing required custom params, etc.)
5504
+ * surface as {@link ParamError} so the caller aborts cleanly before any
5505
+ * row is inserted.
5506
+ *
5507
+ * @param {object} context
5508
+ * @param {Record<string, unknown>} [overrides]
5509
+ * @returns {Promise<object>}
5510
+ */
5511
+ async resolveTaskParams(context, overrides = {}) {
5512
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
5513
+ const fromCli = context.params.get("name", "string");
5514
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
5515
+ const name = overrideName || cliName;
5516
+ if (!name) {
5517
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5518
+ }
5519
+ const TaskClass = this.requireClass(name);
5520
+ return TaskClass.resolveParams(context, { ...overrides, name });
5521
+ }
5522
+ /**
5523
+ * Names of every registered task, sorted alphabetically (useful for CLI output
5524
+ * and allowlist sanity checks).
5525
+ *
5526
+ * @returns {string[]}
5527
+ */
4891
5528
  listSupportedTasks() {
4892
5529
  return Object.keys(this.map).sort();
4893
5530
  }
5531
+ /**
5532
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
5533
+ * or for serialization.
5534
+ *
5535
+ * @returns {Record<string, Function>}
5536
+ */
4894
5537
  toObject() {
4895
5538
  return { ...this.map };
4896
5539
  }
4897
5540
  };
4898
5541
 
4899
- // src/tasks/taskScriptRunner.ts
5542
+ // src/tasks/serviceTaskAllowlist.js
5543
+ function normalizeAllowedTasks(value) {
5544
+ if (!value) return void 0;
5545
+ if (Array.isArray(value)) {
5546
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5547
+ return out2.length ? out2 : void 0;
5548
+ }
5549
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5550
+ return out.length ? out : void 0;
5551
+ }
5552
+
5553
+ // src/tasks/taskScriptRunner.js
4900
5554
  var import_node_child_process2 = require("child_process");
4901
5555
 
4902
- // src/tasks/index.ts
5556
+ // src/tasks/index.js
4903
5557
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4904
5558
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4905
5559
  function getDb3(context) {
@@ -4914,15 +5568,6 @@ function normalizeRegistry(registry) {
4914
5568
  if (registry instanceof TasksRegistry) return registry;
4915
5569
  return new TasksRegistry().addMany(registry);
4916
5570
  }
4917
- function normalizeAllowedTasks(value) {
4918
- if (!value) return void 0;
4919
- if (Array.isArray(value)) {
4920
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
4921
- return out2.length ? out2 : void 0;
4922
- }
4923
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
4924
- return out.length ? out : void 0;
4925
- }
4926
5571
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
4927
5572
  context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
4928
5573
  for (const [, taskInstance] of runningTaskInstances) {
@@ -4938,18 +5583,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
4938
5583
  }
4939
5584
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4940
5585
  const db = getDb3(context);
4941
- const taskName = row.task;
5586
+ const taskName = row.name;
4942
5587
  const TaskClass = registry.get(taskName);
4943
- const { paused_at: _pausedAt, ...rowForHistory } = row;
4944
5588
  if (!TaskClass) {
4945
5589
  const err = { message: `Unknown task "${taskName}"` };
4946
- await db(historyTable).insert({
4947
- ...rowForHistory,
4948
- completed_at: /* @__PURE__ */ new Date(),
4949
- success: false,
4950
- params: toJsonColumn(row.params),
4951
- results: toJsonColumn(err)
4952
- });
5590
+ await db(historyTable).insert(
5591
+ taskHistoryInsertFromQueueRow(row, {
5592
+ completed_at: /* @__PURE__ */ new Date(),
5593
+ success: false,
5594
+ status: "failed",
5595
+ status_changed_at: db.fn.now(),
5596
+ params: toJsonColumn(row.params),
5597
+ results: toJsonColumn(err)
5598
+ })
5599
+ );
4953
5600
  if (row.schedule) {
4954
5601
  await db(tasksTable).where({ id: row.id }).update({
4955
5602
  started_at: null,
@@ -4957,7 +5604,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4957
5604
  success: false,
4958
5605
  results: toJsonColumn(err),
4959
5606
  past_due: null,
4960
- paused_at: db.fn.now(),
5607
+ status: "paused",
5608
+ status_changed_at: db.fn.now(),
4961
5609
  progress: LOCKED_BY_ERROR_MESSAGE
4962
5610
  });
4963
5611
  } else {
@@ -4984,13 +5632,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4984
5632
  } finally {
4985
5633
  runningTaskInstances.delete(row.id);
4986
5634
  }
4987
- await db(historyTable).insert({
4988
- ...rowForHistory,
4989
- completed_at: /* @__PURE__ */ new Date(),
4990
- success,
4991
- params: toJsonColumn(row.params),
4992
- results: toJsonColumn(results)
4993
- });
5635
+ await db(historyTable).insert(
5636
+ taskHistoryInsertFromQueueRow(row, {
5637
+ completed_at: /* @__PURE__ */ new Date(),
5638
+ success,
5639
+ status: success ? "completed" : "failed",
5640
+ status_changed_at: db.fn.now(),
5641
+ params: toJsonColumn(row.params),
5642
+ results: toJsonColumn(results)
5643
+ })
5644
+ );
4994
5645
  if (!success) {
4995
5646
  const dbName = String(context?.params?.get?.("dbName") || "local");
4996
5647
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -5005,19 +5656,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5005
5656
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
5006
5657
  appendTaskIpcLog(context, row, {
5007
5658
  level: "error",
5008
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
5659
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
5009
5660
  details: results
5010
5661
  });
5011
5662
  }
5012
5663
  if (row.schedule) {
5013
5664
  if (success) {
5665
+ let nextRunAt = null;
5666
+ try {
5667
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
5668
+ } catch (e) {
5669
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
5670
+ }
5014
5671
  await db(tasksTable).where({ id: row.id }).update({
5015
5672
  started_at: null,
5016
5673
  completed_at: /* @__PURE__ */ new Date(),
5017
5674
  success,
5018
5675
  results: toJsonColumn(results),
5019
5676
  progress: null,
5020
- past_due: null
5677
+ past_due: null,
5678
+ status: "idle",
5679
+ status_changed_at: db.fn.now(),
5680
+ next_run_at: nextRunAt,
5681
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
5682
+ service_name: null,
5683
+ server_name: null,
5684
+ instance_number: null
5021
5685
  });
5022
5686
  } else {
5023
5687
  await db(tasksTable).where({ id: row.id }).update({
@@ -5025,7 +5689,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5025
5689
  completed_at: /* @__PURE__ */ new Date(),
5026
5690
  success,
5027
5691
  results: toJsonColumn(results),
5028
- paused_at: db.fn.now(),
5692
+ status: "paused",
5693
+ status_changed_at: db.fn.now(),
5029
5694
  progress: LOCKED_BY_ERROR_MESSAGE,
5030
5695
  past_due: null
5031
5696
  });
@@ -5037,58 +5702,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5037
5702
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
5038
5703
  return { stopRunnerRequested, stopAllowanceMs };
5039
5704
  }
5040
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
5705
+ function shuffleTaskRowsInPlace(rows) {
5706
+ for (let i = rows.length - 1; i > 0; i--) {
5707
+ const j = Math.floor(Math.random() * (i + 1));
5708
+ const t = rows[i];
5709
+ rows[i] = rows[j];
5710
+ rows[j] = t;
5711
+ }
5712
+ }
5713
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
5041
5714
  const db = getDb3(context);
5042
- let query = db(tasksTable).whereNull("started_at").whereNull("paused_at").where({ target }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "desc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
5715
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
5716
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
5717
+ }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "asc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
5043
5718
  if (taskNames && taskNames.length > 0) {
5044
- query = query.whereIn("task", taskNames);
5719
+ query = query.whereIn("name", taskNames);
5720
+ }
5721
+ if (runnerIdentity) {
5722
+ query = query.where(function() {
5723
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
5724
+ }).where(function() {
5725
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
5726
+ }).where(function() {
5727
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
5728
+ });
5729
+ } else {
5730
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
5045
5731
  }
5046
5732
  const candidates = await query;
5733
+ shuffleTaskRowsInPlace(candidates);
5047
5734
  for (const row of candidates) {
5048
5735
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
5049
5736
  continue;
5050
5737
  }
5051
- const TaskClass = registry.get(row.task);
5052
- if (TaskClass) {
5053
- const taskInstance = new TaskClass(context, row);
5054
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
5055
- if (reason) {
5056
- if (!row.past_due) {
5057
- await db(tasksTable).where({ id: row.id }).update({
5058
- past_due: db.fn.now(),
5059
- progress: String(reason)
5060
- });
5061
- }
5062
- continue;
5738
+ const TaskClass = registry.get(row.name);
5739
+ if (!TaskClass) {
5740
+ continue;
5741
+ }
5742
+ const taskInstance = new TaskClass(context, row);
5743
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
5744
+ if (reason) {
5745
+ if (!row.past_due) {
5746
+ await db(tasksTable).where({ id: row.id }).update({
5747
+ past_due: db.fn.now(),
5748
+ progress: String(reason)
5749
+ });
5063
5750
  }
5751
+ continue;
5752
+ }
5753
+ const claimPatch = {
5754
+ started_at: db.fn.now(),
5755
+ status: "running",
5756
+ status_changed_at: db.fn.now()
5757
+ };
5758
+ if (runnerIdentity) {
5759
+ claimPatch.service_name = runnerIdentity.service_name;
5760
+ claimPatch.server_name = runnerIdentity.server_name;
5761
+ claimPatch.instance_number = runnerIdentity.instance_number;
5064
5762
  }
5065
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
5763
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
5066
5764
  const claimed = Array.isArray(updated) ? updated[0] : null;
5067
5765
  if (claimed) return claimed;
5068
5766
  }
5069
5767
  return null;
5070
5768
  }
5071
5769
  async function runTasksLoop(context, options) {
5072
- const queue = options.queue ?? "tasks";
5770
+ const queueName = options.queueName ?? "tasks";
5073
5771
  const target = options.target;
5074
5772
  const pollMs = options.pollMs ?? 1e3;
5075
- const maxParallel = options.maxParallel ?? 1;
5773
+ const claimJitterMs = options.claimJitterMs ?? 0;
5774
+ const maxParallel = options.maxParallel ?? 32;
5076
5775
  const scanLimit = options.scanLimit ?? 100;
5077
5776
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5078
5777
  const registry = normalizeRegistry(options.registry);
5079
- const { tasksTable, historyTable } = queueToTableNames(queue);
5778
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5080
5779
  if (!target) throw new Error("runTasksLoop: target is required");
5780
+ context.tasksQueueName = queueName;
5081
5781
  const runningPromises = /* @__PURE__ */ new Set();
5082
5782
  const runningTaskInstances = /* @__PURE__ */ new Map();
5083
5783
  let runningStopControlPromise = null;
5084
5784
  let stopRequested = false;
5085
5785
  let stopAllowanceMs = 5e3;
5086
- context.__tasksRunnerStop = false;
5786
+ context.tasksRunnerStop = false;
5087
5787
  let registryReg = null;
5088
5788
  let registryInterval = null;
5789
+ let runnerIdentity = null;
5089
5790
  const hbGroup = options.runnerServiceGroup?.trim();
5090
5791
  if (hbGroup) {
5091
- const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
5092
5792
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
5093
5793
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
5094
5794
  const defaultMeta = {
@@ -5096,16 +5796,21 @@ async function runTasksLoop(context, options) {
5096
5796
  allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
5097
5797
  };
5098
5798
  registryReg = await registerInServicesRegistry(context, {
5099
- queue,
5799
+ queueName,
5100
5800
  target,
5101
5801
  serviceGroup: hbGroup,
5102
5802
  serviceName: options.runnerServiceName,
5103
- identityDir,
5803
+ instanceNumber: options.runnerInstanceNumber,
5104
5804
  staleMs,
5105
5805
  groupMaxInstances: options.runnerGroupMaxInstances,
5106
5806
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
5107
5807
  metadata: options.runnerMetadata ?? defaultMeta
5108
5808
  });
5809
+ runnerIdentity = {
5810
+ service_name: registryReg.serviceName,
5811
+ server_name: import_node_os3.default.hostname(),
5812
+ instance_number: registryReg.instanceNumber
5813
+ };
5109
5814
  registryInterval = setInterval(() => {
5110
5815
  void touchServicesRegistry(context, registryReg).catch((err) => {
5111
5816
  context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
@@ -5113,7 +5818,7 @@ async function runTasksLoop(context, options) {
5113
5818
  }, hbIntervalMs);
5114
5819
  }
5115
5820
  try {
5116
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
5821
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
5117
5822
  if (!runningStopControlPromise) {
5118
5823
  const claimedStopTask = await claimNextRunnableTask(
5119
5824
  context,
@@ -5121,7 +5826,8 @@ async function runTasksLoop(context, options) {
5121
5826
  target,
5122
5827
  registry,
5123
5828
  10,
5124
- ["stopRunner", "stop"]
5829
+ ["stopRunner", "stop"],
5830
+ runnerIdentity
5125
5831
  );
5126
5832
  if (claimedStopTask) {
5127
5833
  runningStopControlPromise = executeClaimedTask(
@@ -5135,7 +5841,7 @@ async function runTasksLoop(context, options) {
5135
5841
  if (outcome.stopRunnerRequested && !stopRequested) {
5136
5842
  stopRequested = true;
5137
5843
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5138
- context.__tasksRunnerStop = true;
5844
+ context.tasksRunnerStop = true;
5139
5845
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5140
5846
  }
5141
5847
  }).finally(() => {
@@ -5143,6 +5849,9 @@ async function runTasksLoop(context, options) {
5143
5849
  });
5144
5850
  }
5145
5851
  }
5852
+ if (claimJitterMs > 0) {
5853
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
5854
+ }
5146
5855
  while (runningPromises.size < maxParallel) {
5147
5856
  const claimed = await claimNextRunnableTask(
5148
5857
  context,
@@ -5150,14 +5859,15 @@ async function runTasksLoop(context, options) {
5150
5859
  target,
5151
5860
  registry,
5152
5861
  scanLimit,
5153
- allowedTasks
5862
+ allowedTasks,
5863
+ runnerIdentity
5154
5864
  );
5155
5865
  if (!claimed) break;
5156
5866
  const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
5157
5867
  if (outcome.stopRunnerRequested && !stopRequested) {
5158
5868
  stopRequested = true;
5159
5869
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5160
- context.__tasksRunnerStop = true;
5870
+ context.tasksRunnerStop = true;
5161
5871
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5162
5872
  }
5163
5873
  }).finally(() => {
@@ -5165,7 +5875,16 @@ async function runTasksLoop(context, options) {
5165
5875
  });
5166
5876
  runningPromises.add(p);
5167
5877
  }
5168
- await sleepMs(pollMs);
5878
+ const wakePromises = [...runningPromises];
5879
+ if (runningStopControlPromise) {
5880
+ wakePromises.push(runningStopControlPromise);
5881
+ }
5882
+ if (wakePromises.length === 0) {
5883
+ await sleepMs(pollMs);
5884
+ } else {
5885
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
5886
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
5887
+ }
5169
5888
  }
5170
5889
  if (context.isStop() && !stopRequested) {
5171
5890
  await signalRunningTasksStop(context, runningTaskInstances, 5e3);
@@ -5200,71 +5919,88 @@ async function runTasksLoop(context, options) {
5200
5919
  }
5201
5920
  }
5202
5921
  var TasksManager = class _TasksManager {
5203
- context;
5204
- queue;
5205
- target;
5206
- recreateTaskTables;
5207
- pollMs;
5208
- maxParallel;
5209
- scanLimit;
5210
- allowedTasks;
5211
- registry;
5212
- runnerServiceGroup;
5213
- runnerServiceName;
5214
- runnerIdentityDir;
5215
- runnerHeartbeatIntervalMs;
5216
- runnerHeartbeatStaleMs;
5217
- runnerGroupMaxInstances;
5218
- runnerEnforceMaxInstances;
5219
- runnerMetadata;
5922
+ /**
5923
+ * @param {object} context
5924
+ * @param {{
5925
+ * queueName?: string,
5926
+ * target?: string,
5927
+ * recreateTaskTables?: boolean,
5928
+ * pollMs?: number,
5929
+ * claimJitterMs?: number,
5930
+ * maxParallel?: number,
5931
+ * scanLimit?: number,
5932
+ * allowedTasks?: string | string[],
5933
+ * registry?: TasksRegistry | Record<string, Function>,
5934
+ * runnerServiceGroup?: string,
5935
+ * runnerServiceName?: string,
5936
+ * runnerInstanceNumber?: number,
5937
+ * runnerHeartbeatIntervalMs?: number,
5938
+ * runnerHeartbeatStaleMs?: number,
5939
+ * runnerGroupMaxInstances?: number,
5940
+ * runnerEnforceMaxInstances?: boolean,
5941
+ * runnerMetadata?: Record<string, unknown>,
5942
+ * }} [options]
5943
+ */
5220
5944
  constructor(context, options = {}) {
5221
5945
  this.context = context;
5222
- this.queue = options.queue ?? "tasks";
5946
+ this.queueName = options.queueName ?? "tasks";
5223
5947
  this.target = options.target ?? "localRunner";
5224
5948
  this.recreateTaskTables = options.recreateTaskTables ?? false;
5225
5949
  this.pollMs = options.pollMs ?? 1e3;
5950
+ this.claimJitterMs = options.claimJitterMs ?? 0;
5226
5951
  this.maxParallel = options.maxParallel ?? 1;
5227
5952
  this.scanLimit = options.scanLimit ?? 100;
5228
5953
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5229
5954
  this.registry = normalizeRegistry(options.registry);
5230
5955
  this.runnerServiceGroup = options.runnerServiceGroup;
5231
5956
  this.runnerServiceName = options.runnerServiceName;
5232
- this.runnerIdentityDir = options.runnerIdentityDir;
5957
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
5233
5958
  this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
5234
5959
  this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
5235
5960
  this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
5236
5961
  this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
5237
5962
  this.runnerMetadata = options.runnerMetadata;
5238
5963
  }
5964
+ /**
5965
+ * Preferred factory: reads defaults from `context.params` (module namespace
5966
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
5967
+ * and inline options in one consistent resolver.
5968
+ *
5969
+ * @param {object} context
5970
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
5971
+ * @returns {TasksManager}
5972
+ */
5239
5973
  static init(context, options = {}) {
5240
5974
  const defs2 = {
5241
5975
  table: "string default tasks",
5242
5976
  target: "string default localRunner",
5243
5977
  recreateTaskTables: "boolean default false",
5244
5978
  pollMs: "number default 1000",
5979
+ claimJitterMs: "number default 0",
5245
5980
  maxParallel: "number default 1",
5246
5981
  scanLimit: "number default 100",
5247
5982
  allowedTasks: "string",
5248
5983
  runnerServiceGroup: "string",
5249
5984
  runnerServiceName: "string",
5250
- runnerIdentityDir: "string default ./data/runner-identities",
5985
+ runnerInstanceNumber: "number",
5251
5986
  runnerHeartbeatIntervalMs: "number default 10000",
5252
5987
  runnerHeartbeatStaleMs: "number default 45000",
5253
5988
  runnerGroupMaxInstances: "number",
5254
5989
  runnerEnforceMaxInstances: "boolean default true"
5255
5990
  };
5256
- const discovered = context.params.getAllForModule(defs2);
5991
+ const discovered = context.params.getAllForModule("tasks", defs2);
5257
5992
  const resolved = {
5258
- queue: discovered.table,
5993
+ queueName: discovered.table,
5259
5994
  target: discovered.target,
5260
5995
  recreateTaskTables: discovered.recreateTaskTables,
5261
5996
  pollMs: discovered.pollMs,
5997
+ claimJitterMs: discovered.claimJitterMs,
5262
5998
  maxParallel: discovered.maxParallel,
5263
5999
  scanLimit: discovered.scanLimit,
5264
6000
  allowedTasks: discovered.allowedTasks,
5265
6001
  runnerServiceGroup: discovered.runnerServiceGroup,
5266
6002
  runnerServiceName: discovered.runnerServiceName,
5267
- runnerIdentityDir: discovered.runnerIdentityDir,
6003
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
5268
6004
  runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
5269
6005
  runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
5270
6006
  runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
@@ -5273,24 +6009,39 @@ var TasksManager = class _TasksManager {
5273
6009
  };
5274
6010
  return new _TasksManager(context, resolved);
5275
6011
  }
6012
+ /**
6013
+ * Idempotently ensure the three backing tables exist for this queue.
6014
+ *
6015
+ * @param {{ recreate?: boolean }} [options]
6016
+ * @returns {Promise<void>}
6017
+ */
5276
6018
  async ensureTaskTables(options = {}) {
5277
6019
  await ensureTaskTables(this.context, {
5278
- queue: this.queue,
6020
+ queueName: this.queueName,
5279
6021
  recreate: options.recreate ?? this.recreateTaskTables
5280
6022
  });
5281
6023
  }
6024
+ /**
6025
+ * Start the runner loop using this manager's resolved config. Per-call
6026
+ * options override the stored defaults, but `runnerMetadata` still falls
6027
+ * through when omitted.
6028
+ *
6029
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
6030
+ * @returns {Promise<void>}
6031
+ */
5282
6032
  async runTasksLoop(options = {}) {
5283
6033
  await runTasksLoop(this.context, {
5284
- queue: options.queue ?? this.queue,
6034
+ queueName: options.queueName ?? this.queueName,
5285
6035
  target: options.target ?? this.target,
5286
6036
  pollMs: options.pollMs ?? this.pollMs,
6037
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
5287
6038
  maxParallel: options.maxParallel ?? this.maxParallel,
5288
6039
  scanLimit: options.scanLimit ?? this.scanLimit,
5289
6040
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
5290
6041
  registry: options.registry ?? this.registry,
5291
6042
  runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
5292
6043
  runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
5293
- runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
6044
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
5294
6045
  runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
5295
6046
  runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
5296
6047
  runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
@@ -5300,9 +6051,8 @@ var TasksManager = class _TasksManager {
5300
6051
  }
5301
6052
  };
5302
6053
 
5303
- // src/scripts/cli-runner.ts
6054
+ // src/scripts/cli-runner.js
5304
6055
  var defs = {
5305
- dbName: "string default local",
5306
6056
  tasksModule: "string"
5307
6057
  };
5308
6058
  async function loadTasksModule(modulePath) {
@@ -5314,11 +6064,8 @@ async function loadTasksModule(modulePath) {
5314
6064
  return imported.tasksRegistry;
5315
6065
  }
5316
6066
  var flow = async (context) => {
5317
- const {
5318
- dbName,
5319
- tasksModule
5320
- } = context.params.getAll(defs);
5321
- const db = await dbInit(context, dbName);
6067
+ const { tasksModule } = context.params.getAll(defs);
6068
+ const db = await Db.init(context);
5322
6069
  context.db = db;
5323
6070
  const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
5324
6071
  if (tasksModule) {