@nmakarov/cli-toolkit 0.18.0 → 0.23.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 (69) hide show
  1. package/README.md +9 -0
  2. package/dist/args.cjs +1 -4
  3. package/dist/args.cjs.map +1 -1
  4. package/dist/args.js +1 -1
  5. package/dist/args.js.map +1 -1
  6. package/dist/cli-runner.cjs +1493 -516
  7. package/dist/cli-runner.cjs.map +1 -1
  8. package/dist/cli-runner.js +1509 -531
  9. package/dist/cli-runner.js.map +1 -1
  10. package/dist/db.cjs +85 -157
  11. package/dist/db.cjs.map +1 -1
  12. package/dist/db.js +84 -150
  13. package/dist/db.js.map +1 -1
  14. package/dist/errors.cjs +2 -2
  15. package/dist/errors.cjs.map +1 -1
  16. package/dist/errors.js +2 -1
  17. package/dist/errors.js.map +1 -1
  18. package/dist/filedatabase.cjs +19 -19
  19. package/dist/filedatabase.cjs.map +1 -1
  20. package/dist/filedatabase.js +19 -16
  21. package/dist/filedatabase.js.map +1 -1
  22. package/dist/http-client.cjs +9 -11
  23. package/dist/http-client.cjs.map +1 -1
  24. package/dist/http-client.js +10 -9
  25. package/dist/http-client.js.map +1 -1
  26. package/dist/http-client2.cjs +34 -33
  27. package/dist/http-client2.cjs.map +1 -1
  28. package/dist/http-client2.js +34 -30
  29. package/dist/http-client2.js.map +1 -1
  30. package/dist/index.cjs +2063 -658
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +2063 -663
  33. package/dist/index.js.map +1 -1
  34. package/dist/init.cjs +97 -69
  35. package/dist/init.cjs.map +1 -1
  36. package/dist/init.js +112 -83
  37. package/dist/init.js.map +1 -1
  38. package/dist/logger.cjs +5 -5
  39. package/dist/logger.cjs.map +1 -1
  40. package/dist/logger.js +5 -4
  41. package/dist/logger.js.map +1 -1
  42. package/dist/mock-server.cjs +21 -33
  43. package/dist/mock-server.cjs.map +1 -1
  44. package/dist/mock-server.js +21 -28
  45. package/dist/mock-server.js.map +1 -1
  46. package/dist/params.cjs +22 -10
  47. package/dist/params.cjs.map +1 -1
  48. package/dist/params.js +22 -7
  49. package/dist/params.js.map +1 -1
  50. package/dist/s3.cjs +286 -0
  51. package/dist/s3.cjs.map +1 -0
  52. package/dist/s3.js +273 -0
  53. package/dist/s3.js.map +1 -0
  54. package/dist/screen.cjs +34 -39
  55. package/dist/screen.cjs.map +1 -1
  56. package/dist/screen.js +48 -46
  57. package/dist/screen.js.map +1 -1
  58. package/dist/tasks.cjs +1640 -416
  59. package/dist/tasks.cjs.map +1 -1
  60. package/dist/tasks.js +1614 -412
  61. package/dist/tasks.js.map +1 -1
  62. package/dist/utils.cjs +7 -8
  63. package/dist/utils.cjs.map +1 -1
  64. package/dist/utils.js +6 -6
  65. package/dist/utils.js.map +1 -1
  66. package/package.json +36 -44
  67. package/scripts/ssm/parse-cli.js +35 -0
  68. package/scripts/ssm/ssm-admin.js +151 -0
  69. package/scripts/ssm/ssm-pull.js +147 -0
@@ -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,33 +907,31 @@ 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];
925
920
  return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
926
921
  }
927
- function buildDetailBreadcrumb(path5, suffix = "") {
928
- if (path5.length <= 1) {
929
- return suffix ? `\u2190 ${suffix}` : path5[0] || "";
922
+ function buildDetailBreadcrumb(path6, suffix = "") {
923
+ if (path6.length <= 1) {
924
+ return suffix ? `\u2190 ${suffix}` : path6[0] || "";
930
925
  }
931
- const breadcrumb = buildBreadcrumb(path5);
926
+ const breadcrumb = buildBreadcrumb(path6);
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
1121
- var import_node_path = __toESM(require("path"), 1);
1114
+ // src/scripts/cli-runner.js
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
@@ -1944,7 +1938,7 @@ var Params = class _Params {
1944
1938
  throw new ParamError(`default value "${defValObj.value}" type mismatch`);
1945
1939
  }
1946
1940
  type = type.default(defValObj.value);
1947
- } else if (str.match(/required/)) {
1941
+ } else if (str.match(/\s*required\s*/)) {
1948
1942
  type = type.required();
1949
1943
  } else {
1950
1944
  type = type.optional();
@@ -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
  }
@@ -2020,6 +2014,8 @@ var Params = class _Params {
2020
2014
  /**
2021
2015
  * Get all parameters from definitions (main script).
2022
2016
  * Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
2017
+ * Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
2018
+ * around {@link get}) so --showUsedParams groups usage correctly.
2023
2019
  */
2024
2020
  getAll(defs2) {
2025
2021
  return this.getAllForModule("script", defs2);
@@ -2056,6 +2052,19 @@ var Params = class _Params {
2056
2052
  this._currentModule = prev;
2057
2053
  }
2058
2054
  }
2055
+ /**
2056
+ * Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
2057
+ * under the same module (for --showUsedParams / getFiguredByModule).
2058
+ */
2059
+ runWithModule(moduleName, fn) {
2060
+ const prev = this._currentModule;
2061
+ this._currentModule = moduleName;
2062
+ try {
2063
+ return fn();
2064
+ } finally {
2065
+ this._currentModule = prev;
2066
+ }
2067
+ }
2059
2068
  /**
2060
2069
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2061
2070
  */
@@ -2069,9 +2078,9 @@ var Params = class _Params {
2069
2078
  if (!parenMatch) continue;
2070
2079
  const parts = parenMatch[1].split(":");
2071
2080
  if (parts.length < 3) continue;
2072
- const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2073
- if (!path5 || path5.includes(paramsIndexPath)) continue;
2074
- const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2081
+ const path6 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2082
+ if (!path6 || path6.includes(paramsIndexPath)) continue;
2083
+ const srcMatch = path6.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2075
2084
  if (srcMatch) return srcMatch[1];
2076
2085
  }
2077
2086
  return "script";
@@ -2116,11 +2125,11 @@ var Params = class _Params {
2116
2125
  }
2117
2126
  };
2118
2127
 
2119
- // src/logger/index.ts
2128
+ // src/logger/index.js
2120
2129
  var import_chalk = __toESM(require("chalk"), 1);
2121
2130
  var import_util = __toESM(require("util"), 1);
2122
2131
 
2123
- // src/logger/transports.ts
2132
+ // src/logger/transports.js
2124
2133
  var ConsoleTransport = class {
2125
2134
  write(payload) {
2126
2135
  console.info(payload);
@@ -2140,7 +2149,7 @@ var ParentProcessTransport = class {
2140
2149
  }
2141
2150
  };
2142
2151
 
2143
- // src/logger/index.ts
2152
+ // src/logger/index.js
2144
2153
  var ALL_LEVELS = [
2145
2154
  "silly",
2146
2155
  "debug",
@@ -2213,6 +2222,7 @@ var Logger = class _Logger {
2213
2222
  }
2214
2223
  /**
2215
2224
  * Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
2225
+ * Params are tracked under the `logger` module for --showUsedParams.
2216
2226
  */
2217
2227
  static init(context, options) {
2218
2228
  const paramDefs = {
@@ -2226,7 +2236,7 @@ var Logger = class _Logger {
2226
2236
  progressWithTimes: "boolean default false",
2227
2237
  progressThrottleMs: "number"
2228
2238
  };
2229
- const discovered = context.params.getAllForModule(paramDefs);
2239
+ const discovered = context.params.getAllForModule("logger", paramDefs);
2230
2240
  const config2 = { ...discovered, ...options };
2231
2241
  const logger = new _Logger(context, config2);
2232
2242
  context.logger = logger;
@@ -2422,9 +2432,9 @@ var Logger = class _Logger {
2422
2432
  }
2423
2433
  };
2424
2434
 
2425
- // src/init/index.ts
2435
+ // src/init/index.js
2426
2436
  var import_events = require("events");
2427
- function extractComponentOptions(opts, componentName) {
2437
+ function extractComponentOptions(opts, _componentName) {
2428
2438
  const reservedKeys = ["overrides", "defaults", "modules"];
2429
2439
  const componentOptions = {};
2430
2440
  for (const [key, value] of Object.entries(opts)) {
@@ -2491,6 +2501,19 @@ function printAllParameters(context) {
2491
2501
  async function init(flow2, opts = {}) {
2492
2502
  let stop = false;
2493
2503
  let context = null;
2504
+ let cleanupRan = false;
2505
+ const runRegisteredCleanups = async (ctx) => {
2506
+ if (cleanupRan) return;
2507
+ cleanupRan = true;
2508
+ const fns = [...ctx.cleanupFunctions].reverse();
2509
+ for (const fn of fns) {
2510
+ try {
2511
+ await fn(ctx);
2512
+ } catch (error) {
2513
+ ctx.logger.warn("[cleanup] error in cleanup function:", error);
2514
+ }
2515
+ }
2516
+ };
2494
2517
  try {
2495
2518
  try {
2496
2519
  const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
@@ -2513,13 +2536,24 @@ async function init(flow2, opts = {}) {
2513
2536
  printAllParameters(context);
2514
2537
  process.exit(0);
2515
2538
  }
2539
+ let sigintCount = 0;
2516
2540
  process.on("SIGINT", async () => {
2517
- if (stop) {
2518
- context.logger.warn("[process] killed");
2519
- process.exit(2);
2541
+ if (!context) return;
2542
+ sigintCount += 1;
2543
+ if (sigintCount === 1) {
2544
+ stop = true;
2545
+ context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2546
+ context.emitter.emit("stop", stopAllowance);
2547
+ return;
2520
2548
  }
2549
+ context.logger.warn("[process] second SIGINT: running cleanup then exit");
2550
+ await runRegisteredCleanups(context);
2551
+ process.exit(2);
2552
+ });
2553
+ process.on("SIGTERM", () => {
2554
+ if (!context || stop) return;
2521
2555
  stop = true;
2522
- context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2556
+ context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
2523
2557
  context.emitter.emit("stop", stopAllowance);
2524
2558
  });
2525
2559
  await flow2(context);
@@ -2544,33 +2578,62 @@ async function init(flow2, opts = {}) {
2544
2578
  }
2545
2579
  } finally {
2546
2580
  if (context) {
2547
- for (const fn of context.cleanupFunctions.reverse()) {
2548
- try {
2549
- await fn(context);
2550
- } catch (error) {
2551
- context.logger.warn("[cleanup] error in cleanup function:", error);
2552
- }
2553
- }
2581
+ await runRegisteredCleanups(context);
2554
2582
  }
2555
2583
  }
2556
2584
  }
2557
2585
 
2558
- // src/db/index.ts
2586
+ // src/db/index.js
2559
2587
  var import_knex = __toESM(require("knex"), 1);
2588
+ var KNEX_DEFAULTS = {
2589
+ testConnection: true,
2590
+ pool: { min: 2, max: 10 },
2591
+ acquireConnectionTimeout: 1e4,
2592
+ ssl: { rejectUnauthorized: false }
2593
+ };
2560
2594
  var Db = class {
2561
- knexInstance = null;
2562
- config;
2563
- logger;
2564
- queriesLog = [];
2565
- isConnected = false;
2566
- /**
2567
- * Constructor - accepts config object
2568
- * Use dbInit() function to initialize with Context
2569
- */
2595
+ static async init(context, options = {}) {
2596
+ const defs2 = {
2597
+ dbName: "string",
2598
+ dbConnectionString: "string",
2599
+ dbProfile: "boolean default false"
2600
+ };
2601
+ const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
2602
+ const merged = { ...discovered, ...options };
2603
+ let { dbName, dbConnectionString } = merged;
2604
+ const { dbProfile } = merged;
2605
+ if (!dbName && !dbConnectionString) {
2606
+ dbName = "local";
2607
+ }
2608
+ if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
2609
+ dbConnectionString = dbName;
2610
+ dbName = void 0;
2611
+ }
2612
+ if (dbName && !dbConnectionString) {
2613
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2614
+ dbConnectionString = await context.params.get(paramName, "string");
2615
+ if (!dbConnectionString) {
2616
+ throw new ParamError(
2617
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2618
+ );
2619
+ }
2620
+ }
2621
+ const config2 = {
2622
+ ...KNEX_DEFAULTS,
2623
+ connectionString: dbConnectionString,
2624
+ name: dbName || merged.name || "default",
2625
+ profile: !!dbProfile,
2626
+ logger: context.logger
2627
+ };
2628
+ return dbConnect(context, config2);
2629
+ }
2570
2630
  constructor(config2) {
2571
- if (!config2.connectionString) {
2631
+ if (!config2 || !config2.connectionString) {
2572
2632
  throw new ParamError("Db: connectionString is required");
2573
2633
  }
2634
+ this.knexInstance = null;
2635
+ this.isConnected = false;
2636
+ this.queriesLog = [];
2574
2637
  this.config = {
2575
2638
  testConnection: true,
2576
2639
  profile: false,
@@ -2583,25 +2646,23 @@ var Db = class {
2583
2646
  };
2584
2647
  this.logger = this.config.logger;
2585
2648
  const instance = this;
2586
- const callableWrapper = function(...args) {
2649
+ const callableWrapper = function() {
2587
2650
  throw new Error("This should never be called directly");
2588
2651
  };
2589
2652
  callableWrapper._instance = instance;
2590
2653
  return new Proxy(callableWrapper, {
2591
- // Intercept function calls: db('table')
2592
- apply: (target, thisArg, argumentsList) => {
2654
+ apply: (target, _thisArg, argumentsList) => {
2593
2655
  const inst = target._instance;
2594
2656
  if (!inst.knexInstance) {
2595
2657
  throw new Error("Db: Not connected. Call connect() first.");
2596
2658
  }
2597
2659
  return inst.knexInstance(...argumentsList);
2598
2660
  },
2599
- // Intercept property access: db.schema, db.raw, etc.
2600
2661
  get: (target, prop) => {
2601
2662
  if (prop === "_instance") {
2602
2663
  return target._instance;
2603
2664
  }
2604
- const instance2 = target._instance;
2665
+ const inst = target._instance;
2605
2666
  const ownMethods = [
2606
2667
  "connect",
2607
2668
  "disconnect",
@@ -2614,26 +2675,26 @@ var Db = class {
2614
2675
  "detectClient",
2615
2676
  "attachProfiler"
2616
2677
  ];
2617
- if (prop in instance2) {
2618
- const value = instance2[prop];
2678
+ if (prop in inst) {
2679
+ const value = inst[prop];
2619
2680
  if (typeof value === "function" && ownMethods.includes(prop)) {
2620
- return value.bind(instance2);
2681
+ return value.bind(inst);
2621
2682
  }
2622
2683
  if (typeof value !== "function") {
2623
2684
  return value;
2624
2685
  }
2625
2686
  }
2626
- if (instance2.knexInstance) {
2627
- const knexProp = instance2.knexInstance[prop];
2687
+ if (inst.knexInstance) {
2688
+ const knexProp = inst.knexInstance[prop];
2628
2689
  if (typeof knexProp === "function") {
2629
- return knexProp.bind(instance2.knexInstance);
2690
+ return knexProp.bind(inst.knexInstance);
2630
2691
  }
2631
2692
  return knexProp;
2632
2693
  }
2633
- if (prop in instance2) {
2634
- const method = instance2[prop];
2694
+ if (prop in inst) {
2695
+ const method = inst[prop];
2635
2696
  if (typeof method === "function") {
2636
- return method.bind(instance2);
2697
+ return method.bind(inst);
2637
2698
  }
2638
2699
  return method;
2639
2700
  }
@@ -2641,9 +2702,6 @@ var Db = class {
2641
2702
  }
2642
2703
  });
2643
2704
  }
2644
- /**
2645
- * Detect database client type from connection string
2646
- */
2647
2705
  detectClient(connectionString) {
2648
2706
  if (connectionString.match(/^postgresql/)) {
2649
2707
  return "pg";
@@ -2653,9 +2711,6 @@ var Db = class {
2653
2711
  }
2654
2712
  return null;
2655
2713
  }
2656
- /**
2657
- * Connect to the database
2658
- */
2659
2714
  async connect() {
2660
2715
  if (this.isConnected && this.knexInstance) {
2661
2716
  this.logger.warn?.("[Db] Already connected");
@@ -2664,14 +2719,13 @@ var Db = class {
2664
2719
  const client = this.detectClient(this.config.connectionString);
2665
2720
  if (!client) {
2666
2721
  throw new ParamError(
2667
- `Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
2722
+ "Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
2668
2723
  );
2669
2724
  }
2670
2725
  try {
2671
2726
  const connectionConfig = {
2672
2727
  connectionString: this.config.connectionString,
2673
2728
  family: 4
2674
- // Force IPv4 only (disable IPv6)
2675
2729
  };
2676
2730
  this.knexInstance = (0, import_knex.default)({
2677
2731
  client,
@@ -2687,7 +2741,9 @@ var Db = class {
2687
2741
  await this.testConnection();
2688
2742
  }
2689
2743
  this.isConnected = true;
2690
- this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
2744
+ this.logger.debug?.(
2745
+ `[Db] Connected to database "${this.config.name || this.config.connectionString}"`
2746
+ );
2691
2747
  } catch (error) {
2692
2748
  if (error instanceof ParamError) {
2693
2749
  throw error;
@@ -2696,9 +2752,6 @@ var Db = class {
2696
2752
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
2697
2753
  }
2698
2754
  }
2699
- /**
2700
- * Disconnect from the database
2701
- */
2702
2755
  async disconnect() {
2703
2756
  if (!this.knexInstance) {
2704
2757
  return;
@@ -2708,16 +2761,15 @@ var Db = class {
2708
2761
  this.knexInstance = null;
2709
2762
  this.isConnected = false;
2710
2763
  this.queriesLog = [];
2711
- this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
2764
+ this.logger.debug?.(
2765
+ `[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
2766
+ );
2712
2767
  } catch (error) {
2713
2768
  const errorMsg = this.getErrorMessage(error);
2714
2769
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
2715
2770
  throw error;
2716
2771
  }
2717
2772
  }
2718
- /**
2719
- * Extract error message from various error types
2720
- */
2721
2773
  getErrorMessage(error) {
2722
2774
  if (error instanceof AggregateError) {
2723
2775
  const errors = error.errors || [];
@@ -2742,9 +2794,11 @@ var Db = class {
2742
2794
  return `${code} (tried: ${addresses.join(", ")})`;
2743
2795
  }
2744
2796
  }
2745
- const uniqueMessages = [...new Set(errors.map((e) => {
2746
- return e instanceof Error ? e.message : String(e);
2747
- }))];
2797
+ const uniqueMessages = [
2798
+ ...new Set(
2799
+ errors.map((e) => e instanceof Error ? e.message : String(e))
2800
+ )
2801
+ ];
2748
2802
  if (uniqueMessages.length === 1) {
2749
2803
  return uniqueMessages[0];
2750
2804
  }
@@ -2753,28 +2807,25 @@ var Db = class {
2753
2807
  return error.message || "Multiple errors occurred";
2754
2808
  }
2755
2809
  if (error instanceof Error) {
2756
- const errorWithCode = error;
2757
- if (errorWithCode.code) {
2758
- return `${errorWithCode.code}: ${error.message || String(error)}`;
2810
+ const code = error.code;
2811
+ if (code) {
2812
+ return `${code}: ${error.message || String(error)}`;
2759
2813
  }
2760
2814
  return error.message || String(error);
2761
2815
  }
2762
2816
  if (typeof error === "string") {
2763
2817
  return error;
2764
2818
  }
2765
- if (error?.message) {
2819
+ if (error && typeof error === "object" && "message" in error) {
2766
2820
  const msg = String(error.message);
2767
- const errorWithCode = error;
2768
- if (errorWithCode.code) {
2769
- return `${errorWithCode.code}: ${msg}`;
2821
+ const code = error.code;
2822
+ if (code) {
2823
+ return `${code}: ${msg}`;
2770
2824
  }
2771
2825
  return msg;
2772
2826
  }
2773
2827
  return String(error) || "Unknown error";
2774
2828
  }
2775
- /**
2776
- * Test database connection
2777
- */
2778
2829
  async testConnection() {
2779
2830
  if (!this.knexInstance) {
2780
2831
  throw new Error("Db: Not connected. Call connect() first.");
@@ -2790,9 +2841,6 @@ var Db = class {
2790
2841
  throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
2791
2842
  }
2792
2843
  }
2793
- /**
2794
- * Attach query profiler to log all queries
2795
- */
2796
2844
  attachProfiler() {
2797
2845
  if (!this.knexInstance) {
2798
2846
  return;
@@ -2802,7 +2850,7 @@ var Db = class {
2802
2850
  this.knexInstance.on("query", (query) => {
2803
2851
  query.__startTime = process.hrtime();
2804
2852
  });
2805
- this.knexInstance.on("query-response", (response, query) => {
2853
+ this.knexInstance.on("query-response", (_response, query) => {
2806
2854
  const [seconds, nanoseconds] = process.hrtime(query.__startTime);
2807
2855
  const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
2808
2856
  const logEntry = {
@@ -2817,15 +2865,9 @@ var Db = class {
2817
2865
  this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
2818
2866
  });
2819
2867
  }
2820
- /**
2821
- * Get query log (only available if profiling is enabled)
2822
- */
2823
2868
  getQueryLog() {
2824
2869
  return [...this.queriesLog];
2825
2870
  }
2826
- /**
2827
- * Check if a table exists
2828
- */
2829
2871
  async tableExists(tableName) {
2830
2872
  if (!this.knexInstance) {
2831
2873
  throw new Error("Db: Not connected. Call connect() first.");
@@ -2837,65 +2879,28 @@ var Db = class {
2837
2879
  throw error;
2838
2880
  }
2839
2881
  }
2840
- /**
2841
- * Get the underlying Knex instance (for advanced usage)
2842
- */
2843
2882
  getKnex() {
2844
2883
  if (!this.knexInstance) {
2845
2884
  throw new Error("Db: Not connected. Call connect() first.");
2846
2885
  }
2847
2886
  return this.knexInstance;
2848
2887
  }
2849
- /**
2850
- * Get connection status
2851
- */
2852
2888
  isConnectedToDb() {
2853
2889
  return this.isConnected && this.knexInstance !== null;
2854
2890
  }
2855
- /**
2856
- * Initialize Db with context (connects and registers disconnect cleanup).
2857
- * Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
2858
- */
2859
- static async init(context, dbNameOrConnectionString) {
2860
- return dbFindAndConnect(context, dbNameOrConnectionString);
2861
- }
2862
2891
  };
2863
2892
  function capitalizeFirstLetter(str) {
2864
2893
  return str.charAt(0).toUpperCase() + str.slice(1);
2865
2894
  }
2866
- async function dbConnect(context, connectionString, name, dbProfile) {
2867
- const defs2 = {
2868
- testDbConnection: "boolean default true",
2869
- name: "string",
2870
- poolMin: "number default 2",
2871
- poolMax: "number default 10",
2872
- acquireConnectionTimeout: "number default 10000",
2873
- sslRejectUnauthorized: "boolean default false"
2874
- };
2875
- const paramsConfig = context.params.getAllForModule(defs2);
2876
- const config2 = {
2877
- connectionString,
2878
- name: paramsConfig.name || name || "default",
2879
- testConnection: paramsConfig.testDbConnection,
2880
- profile: dbProfile ?? false,
2881
- pool: {
2882
- min: paramsConfig.poolMin,
2883
- max: paramsConfig.poolMax
2884
- },
2885
- acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
2886
- ssl: {
2887
- rejectUnauthorized: paramsConfig.sslRejectUnauthorized
2888
- },
2889
- logger: context.logger
2890
- };
2895
+ async function dbConnect(context, config2) {
2891
2896
  try {
2892
2897
  const db = new Db(config2);
2893
2898
  context.registerCleanup(async () => {
2894
2899
  await db.disconnect();
2895
- context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
2900
+ context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
2896
2901
  });
2897
2902
  await db.connect();
2898
- context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
2903
+ context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
2899
2904
  return db;
2900
2905
  } catch (error) {
2901
2906
  if (error instanceof ParamError) {
@@ -2905,48 +2910,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
2905
2910
  throw new ParamError(`[Db] connect error: ${errorMsg}`);
2906
2911
  }
2907
2912
  }
2908
- async function dbFindAndConnect(context, dbNameOrConnectionString) {
2909
- let dbName;
2910
- let dbConnectionString;
2911
- let dbProfile;
2912
- if (dbNameOrConnectionString) {
2913
- if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
2914
- dbName = void 0;
2915
- dbConnectionString = dbNameOrConnectionString;
2916
- } else {
2917
- dbName = dbNameOrConnectionString;
2918
- }
2919
- } else {
2920
- const defs2 = {
2921
- dbName: "string",
2922
- dbConnectionString: "string",
2923
- dbProfile: "boolean default false"
2924
- };
2925
- const paramsConfig = context.params.getAllForModule(defs2);
2926
- dbName = paramsConfig.dbName;
2927
- dbConnectionString = paramsConfig.dbConnectionString;
2928
- dbProfile = paramsConfig.dbProfile;
2929
- }
2930
- if (!dbName && !dbConnectionString) {
2931
- throw new ParamError("Db: either dbName or dbConnectionString must be specified");
2932
- }
2933
- if (dbName) {
2934
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2935
- dbConnectionString = await context.params.get(paramName, "string");
2936
- if (!dbConnectionString) {
2937
- throw new ParamError(
2938
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2939
- );
2940
- }
2941
- }
2942
- const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
2943
- return db;
2944
- }
2945
- async function dbInit(context, dbNameOrConnectionString) {
2946
- return await dbFindAndConnect(context, dbNameOrConnectionString);
2947
- }
2948
2913
 
2949
- // src/utils/date-utils.ts
2914
+ // src/tasks/index.js
2915
+ var import_node_os3 = __toESM(require("os"), 1);
2916
+
2917
+ // src/utils/date-utils.js
2950
2918
  function isTimestampFolder(folderName) {
2951
2919
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
2952
2920
  if (!isoRegex.test(folderName)) {
@@ -2956,7 +2924,7 @@ function isTimestampFolder(folderName) {
2956
2924
  return !isNaN(date.getTime()) && date.getTime() > 0;
2957
2925
  }
2958
2926
 
2959
- // src/utils/fs-utils.ts
2927
+ // src/utils/fs-utils.js
2960
2928
  var import_fs2 = __toESM(require("fs"), 1);
2961
2929
  var import_path2 = __toESM(require("path"), 1);
2962
2930
  async function ensurePath(...pathParts) {
@@ -2980,7 +2948,7 @@ function getFileExtension(dataType) {
2980
2948
  }
2981
2949
  }
2982
2950
 
2983
- // src/utils/os-utils.ts
2951
+ // src/utils/os-utils.js
2984
2952
  var import_fs3 = __toESM(require("fs"), 1);
2985
2953
  var import_path3 = __toESM(require("path"), 1);
2986
2954
  var import_child_process = require("child_process");
@@ -3009,7 +2977,7 @@ function getFreeDiskSpace(targetPath) {
3009
2977
  }
3010
2978
  }
3011
2979
 
3012
- // src/utils/format-utils.ts
2980
+ // src/utils/format-utils.js
3013
2981
  function bytesToHumanReadable(bytes) {
3014
2982
  if (bytes === 0) return "0 B";
3015
2983
  const k = 1024;
@@ -3018,7 +2986,7 @@ function bytesToHumanReadable(bytes) {
3018
2986
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
3019
2987
  }
3020
2988
 
3021
- // src/utils/core-utils.ts
2989
+ // src/utils/core-utils.js
3022
2990
  function sleepMs(ms) {
3023
2991
  return new Promise((resolve2) => setTimeout(resolve2, ms));
3024
2992
  }
@@ -3027,8 +2995,82 @@ function toJsonColumn(value) {
3027
2995
  return JSON.stringify(value);
3028
2996
  }
3029
2997
 
3030
- // src/tasks/taskUtils.ts
2998
+ // src/tasks/servicesRegistry.js
2999
+ var import_node_os = __toESM(require("os"), 1);
3000
+
3001
+ // src/tasks/taskUtils.js
3031
3002
  var import_node_crypto = require("crypto");
3003
+
3004
+ // src/tasks/time-matcher.js
3005
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
3006
+ function resolveAsterisks(field, range) {
3007
+ return field.includes("*") ? field.replace("*", range) : field;
3008
+ }
3009
+ function resolveRanges(field) {
3010
+ const regex = /(\d+)-(\d+)/;
3011
+ let current = field;
3012
+ while (true) {
3013
+ const match = regex.exec(current);
3014
+ if (!match) break;
3015
+ const raw = match[0];
3016
+ let first = Number(match[1]);
3017
+ let last = Number(match[2]);
3018
+ if (last < first) {
3019
+ [first, last] = [last, first];
3020
+ }
3021
+ const values = [];
3022
+ for (let i = first; i <= last; i += 1) {
3023
+ values.push(i);
3024
+ }
3025
+ current = current.replace(raw, values.join(","));
3026
+ }
3027
+ return current;
3028
+ }
3029
+ function resolveSteps(field) {
3030
+ const match = /^(.+)\/(\d+)$/.exec(field);
3031
+ if (!match) return field;
3032
+ const base = match[1];
3033
+ const step = Number(match[2]);
3034
+ if (!Number.isFinite(step) || step <= 0) return field;
3035
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
3036
+ }
3037
+ function convertPattern(pattern) {
3038
+ const parts = pattern.trim().split(/\s+/);
3039
+ if (parts.length !== 6) {
3040
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
3041
+ }
3042
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
3043
+ }
3044
+ function fieldMatches(field, value) {
3045
+ const allowed = field.split(",").map((v) => Number(v));
3046
+ return allowed.includes(value);
3047
+ }
3048
+ function matchesParsedPattern(parsed, date) {
3049
+ 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());
3050
+ }
3051
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
3052
+ const parsed = convertPattern(pattern);
3053
+ return matchesParsedPattern(parsed, date);
3054
+ }
3055
+ var MS_PER_SECOND = 1e3;
3056
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
3057
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
3058
+ const parsed = convertPattern(pattern);
3059
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
3060
+ const end = t + maxSearchMs;
3061
+ while (t <= end) {
3062
+ const date = new Date(t);
3063
+ if (matchesParsedPattern(parsed, date)) {
3064
+ return date;
3065
+ }
3066
+ t += MS_PER_SECOND;
3067
+ }
3068
+ throw new Error(
3069
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
3070
+ );
3071
+ }
3072
+
3073
+ // src/tasks/taskUtils.js
3032
3074
  function getDb(context) {
3033
3075
  const db = context.db;
3034
3076
  if (!db) {
@@ -3036,89 +3078,84 @@ function getDb(context) {
3036
3078
  }
3037
3079
  return db;
3038
3080
  }
3039
- function queueToTableNames(queue) {
3040
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
3041
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
3042
- }
3081
+ function queueToTableNames(queueName) {
3082
+ return {
3083
+ tasksTable: queueName,
3084
+ historyTable: `${queueName}_history`,
3085
+ registryTable: `${queueName}_services_registry`
3086
+ };
3087
+ }
3088
+ function defineTasksTable(t, db, tableNameForIndex) {
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(50);
3094
+ t.text("schedule");
3095
+ t.timestamp("next_run_at").defaultTo(null);
3096
+ t.timestamp("past_due").defaultTo(null);
3097
+ t.text("name").notNullable();
3098
+ t.text("opid");
3099
+ t.json("params");
3100
+ t.text("service_group");
3101
+ t.integer("instance_number");
3102
+ t.text("service_name");
3103
+ t.text("server_name");
3104
+ t.text("status").notNullable().defaultTo("idle");
3105
+ t.timestamp("status_changed_at").defaultTo(null);
3106
+ t.text("progress");
3107
+ t.boolean("success");
3108
+ t.json("results");
3109
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
3110
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
3111
+ }
3112
+ function taskHistoryInsertFromQueueRow(row, overrides) {
3113
+ const { id, ...snapshot } = row;
3114
+ void id;
3043
3115
  return {
3044
- tasksTable: queue,
3045
- historyTable: `${queue}_history`
3116
+ ...snapshot,
3117
+ ...overrides
3046
3118
  };
3047
3119
  }
3048
3120
  async function ensureTaskTables(context, options = {}) {
3049
- const queue = options.queue ?? "tasks";
3121
+ const queueName = options.queueName ?? "tasks";
3050
3122
  const recreate = options.recreate ?? false;
3051
3123
  const db = getDb(context);
3052
- const { tasksTable, historyTable } = queueToTableNames(queue);
3124
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
3053
3125
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
3054
3126
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
3127
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
3055
3128
  if (recreate) {
3056
3129
  await db.schema.dropTableIfExists(historyTable);
3057
3130
  await db.schema.dropTableIfExists(tasksTable);
3131
+ await db.schema.dropTableIfExists(registryTable);
3058
3132
  }
3059
3133
  if (needsTasks) {
3060
3134
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
3061
3135
  await db.schema.createTable(tasksTable, (t) => {
3062
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3063
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3064
- t.timestamp("started_at");
3065
- t.timestamp("completed_at");
3066
- t.integer("priority").notNullable().defaultTo(0);
3067
- t.text("schedule");
3068
- t.timestamp("past_due").defaultTo(null);
3069
- t.text("target").notNullable();
3070
- t.text("task").notNullable();
3071
- t.json("params");
3072
- t.text("opid");
3073
- t.timestamp("paused_at").defaultTo(null);
3074
- t.text("progress");
3075
- t.boolean("success");
3076
- t.json("results");
3077
- });
3078
- await db.schema.alterTable(tasksTable, (t) => {
3079
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
3080
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
3081
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
3082
- });
3083
- }
3084
- const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
3085
- if (!tasksHasOpid) {
3086
- await db.schema.alterTable(tasksTable, (t) => {
3087
- t.text("opid");
3088
- });
3089
- }
3090
- const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
3091
- if (!tasksHasPausedAt) {
3092
- await db.schema.alterTable(tasksTable, (t) => {
3093
- t.timestamp("paused_at").defaultTo(null);
3136
+ defineTasksTable(t, db, tasksTable);
3094
3137
  });
3095
3138
  }
3096
3139
  if (needsHistory) {
3097
3140
  await db.schema.createTable(historyTable, (t) => {
3098
- t.uuid("id").notNullable();
3099
- t.timestamp("created_at").notNullable();
3100
- t.timestamp("started_at");
3101
- t.timestamp("completed_at");
3102
- t.integer("priority").notNullable().defaultTo(0);
3103
- t.text("schedule");
3104
- t.timestamp("past_due").defaultTo(null);
3105
- t.text("target").notNullable();
3106
- t.text("task").notNullable();
3107
- t.json("params");
3108
- t.text("opid");
3109
- t.text("progress");
3110
- t.boolean("success");
3111
- t.json("results");
3112
- });
3113
- await db.schema.alterTable(historyTable, (t) => {
3114
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
3115
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
3141
+ defineTasksTable(t, db, historyTable);
3116
3142
  });
3117
3143
  }
3118
- const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
3119
- if (!historyHasOpid) {
3120
- await db.schema.alterTable(historyTable, (t) => {
3121
- t.text("opid");
3144
+ if (needsRegistry) {
3145
+ await db.schema.createTable(registryTable, (t) => {
3146
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3147
+ t.text("queue_name").notNullable();
3148
+ t.text("service_group").notNullable();
3149
+ t.integer("instance_number").notNullable().defaultTo(1);
3150
+ t.text("service_name").notNullable();
3151
+ t.text("server_name").notNullable();
3152
+ t.integer("pid");
3153
+ t.json("metadata");
3154
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3155
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
3156
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
3157
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
3158
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
3122
3159
  });
3123
3160
  }
3124
3161
  }
@@ -3129,11 +3166,226 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
3129
3166
  });
3130
3167
  }
3131
3168
 
3132
- // src/filedatabase/index.ts
3169
+ // src/tasks/servicesRegistry.js
3170
+ function getDb2(context) {
3171
+ const db = context.db;
3172
+ if (!db) {
3173
+ throw new Error("Services registry requires context.db");
3174
+ }
3175
+ return db;
3176
+ }
3177
+ var DEFAULT_GROUP_MAX_INSTANCES = {
3178
+ intake: 1,
3179
+ harvest: 1,
3180
+ harvester: 0,
3181
+ loader: 0,
3182
+ photos: 0,
3183
+ photosprocessor: 0,
3184
+ ingest: 0
3185
+ };
3186
+ function sanitizeNamePart(raw) {
3187
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3188
+ return s.slice(0, 80) || "runner";
3189
+ }
3190
+ function resolveMaxInstances(serviceGroup, override) {
3191
+ if (override !== void 0 && Number.isFinite(override)) {
3192
+ return Math.max(0, Math.floor(Number(override)));
3193
+ }
3194
+ const g = serviceGroup.trim().toLowerCase();
3195
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
3196
+ }
3197
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
3198
+ const cutoff = new Date(Date.now() - staleMs);
3199
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
3200
+ if (excludeRowId) {
3201
+ q = q.whereNot("id", excludeRowId);
3202
+ }
3203
+ const row = await q.count("id as count").first();
3204
+ return Number(row?.count ?? 0);
3205
+ }
3206
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
3207
+ const cutoff = new Date(Date.now() - staleMs);
3208
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
3209
+ const set = /* @__PURE__ */ new Set();
3210
+ for (const r of rows) {
3211
+ const n = Number(r.instance_number);
3212
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
3213
+ }
3214
+ return set;
3215
+ }
3216
+ function isUniqueViolation(error) {
3217
+ const code = error?.code ?? error?.errno;
3218
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
3219
+ }
3220
+ function buildMetadata(options) {
3221
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
3222
+ if (options.target) {
3223
+ base.runnerTarget = options.target;
3224
+ }
3225
+ return toJsonColumn(Object.keys(base).length ? base : null);
3226
+ }
3227
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
3228
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
3229
+ const e = Math.max(1, Math.floor(Number(explicit)));
3230
+ if (occupied.has(e)) {
3231
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
3232
+ }
3233
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
3234
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
3235
+ }
3236
+ return e;
3237
+ }
3238
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
3239
+ for (let n = 1; n <= cap; n++) {
3240
+ if (!occupied.has(n)) return n;
3241
+ }
3242
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
3243
+ }
3244
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
3245
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
3246
+ }
3247
+ async function registerInServicesRegistry(context, options) {
3248
+ const db = getDb2(context);
3249
+ const registryTable = queueToTableNames(options.queueName).registryTable;
3250
+ const serviceGroup = options.serviceGroup.trim();
3251
+ if (!serviceGroup) {
3252
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
3253
+ }
3254
+ const serverName = import_node_os.default.hostname();
3255
+ const pid = typeof process.pid === "number" ? process.pid : null;
3256
+ const meta = buildMetadata(options);
3257
+ const groupBase = sanitizeNamePart(serviceGroup);
3258
+ const hostBase = sanitizeNamePart(serverName);
3259
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
3260
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
3261
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
3262
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
3263
+ if (options.enforceMaxInstances) {
3264
+ throw new Error(msg);
3265
+ }
3266
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
3267
+ }
3268
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
3269
+ const cutoff = new Date(Date.now() - options.staleMs);
3270
+ const MAX_ATTEMPTS = 8;
3271
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
3272
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
3273
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
3274
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
3275
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
3276
+ if (existing) {
3277
+ const lastSeen = new Date(existing.last_seen_at);
3278
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
3279
+ if (isAlive) {
3280
+ if (options.serviceName?.trim()) {
3281
+ throw new Error(
3282
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
3283
+ );
3284
+ }
3285
+ context.logger.warn?.(
3286
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
3287
+ );
3288
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
3289
+ throw new Error(
3290
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
3291
+ );
3292
+ }
3293
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
3294
+ continue;
3295
+ }
3296
+ await db(registryTable).where({ id: existing.id }).update({
3297
+ server_name: serverName,
3298
+ pid,
3299
+ metadata: meta,
3300
+ service_group: serviceGroup,
3301
+ instance_number: instanceNumber,
3302
+ last_seen_at: db.fn.now()
3303
+ });
3304
+ const reg = {
3305
+ serviceName: serviceNameRaw,
3306
+ serviceGroup,
3307
+ queueName: options.queueName,
3308
+ target: options.target,
3309
+ rowId: String(existing.id),
3310
+ registryTable,
3311
+ instanceNumber
3312
+ };
3313
+ context.servicesRegistry = reg;
3314
+ context.runnerHeartbeat = reg;
3315
+ context.logger.info?.(
3316
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
3317
+ );
3318
+ return reg;
3319
+ }
3320
+ try {
3321
+ const rows = await db(registryTable).insert({
3322
+ queue_name: options.queueName,
3323
+ service_group: serviceGroup,
3324
+ instance_number: instanceNumber,
3325
+ service_name: serviceNameRaw,
3326
+ server_name: serverName,
3327
+ pid,
3328
+ metadata: meta,
3329
+ last_seen_at: db.fn.now(),
3330
+ created_at: db.fn.now()
3331
+ }).returning(["id", "service_name"]);
3332
+ const row = Array.isArray(rows) ? rows[0] : rows;
3333
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
3334
+ if (!rowId) {
3335
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
3336
+ rowId = again?.id != null ? String(again.id) : "";
3337
+ }
3338
+ if (!rowId) continue;
3339
+ const regNew = {
3340
+ serviceName: String(row?.service_name ?? serviceNameRaw),
3341
+ serviceGroup,
3342
+ queueName: options.queueName,
3343
+ target: options.target,
3344
+ rowId,
3345
+ registryTable,
3346
+ instanceNumber
3347
+ };
3348
+ context.servicesRegistry = regNew;
3349
+ context.runnerHeartbeat = regNew;
3350
+ context.logger.info?.(
3351
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
3352
+ );
3353
+ return regNew;
3354
+ } catch (error) {
3355
+ if (!isUniqueViolation(error)) {
3356
+ throw error;
3357
+ }
3358
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
3359
+ }
3360
+ }
3361
+ throw new Error(
3362
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
3363
+ );
3364
+ }
3365
+ async function touchServicesRegistry(context, registration) {
3366
+ const db = getDb2(context);
3367
+ const serverName = import_node_os.default.hostname();
3368
+ const pid = typeof process.pid === "number" ? process.pid : null;
3369
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
3370
+ last_seen_at: db.fn.now(),
3371
+ server_name: serverName,
3372
+ pid
3373
+ });
3374
+ }
3375
+ async function unregisterServicesRegistry(context, registration) {
3376
+ const db = getDb2(context);
3377
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
3378
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
3379
+ }
3380
+
3381
+ // src/tasks/taskLogs.js
3382
+ var import_node_path = __toESM(require("path"), 1);
3383
+
3384
+ // src/filedatabase/index.js
3133
3385
  var import_fs4 = __toESM(require("fs"), 1);
3134
3386
  var import_path4 = __toESM(require("path"), 1);
3135
3387
 
3136
- // src/filedatabase/serializers.ts
3388
+ // src/filedatabase/serializers.js
3137
3389
  function detectDataType(data) {
3138
3390
  if (Array.isArray(data)) {
3139
3391
  return "json-array";
@@ -3165,7 +3417,7 @@ function deserializeData(rawData, dataType) {
3165
3417
  }
3166
3418
  }
3167
3419
 
3168
- // src/filedatabase/index.ts
3420
+ // src/filedatabase/index.js
3169
3421
  var FileDatabase = class _FileDatabase {
3170
3422
  basePath;
3171
3423
  namespace;
@@ -3251,7 +3503,7 @@ var FileDatabase = class _FileDatabase {
3251
3503
  if (errors.length) {
3252
3504
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
3253
3505
  }
3254
- let parts = [this.basePath, this.namespace];
3506
+ const parts = [this.basePath, this.namespace];
3255
3507
  if (this.tableName) {
3256
3508
  parts.push(...this.tableName.split("/"));
3257
3509
  }
@@ -3744,14 +3996,17 @@ var FileDatabase = class _FileDatabase {
3744
3996
  * Prepare the instance for read or write operations
3745
3997
  * This discovers state and sets up internal members based on mode and current data
3746
3998
  */
3747
- async prepare({ write, read, version }) {
3999
+ async prepare(options) {
4000
+ const { write, read, version, deferInitialVersion } = options;
3748
4001
  if (write) {
3749
4002
  if (this.versioned) {
3750
4003
  if (this.currentVersion === null) {
3751
- await this.makeNewVersion();
3752
- this.metadata = this.getDefaultMetadata();
3753
- this.metadata.version = this.currentVersion;
3754
- this.makeNewFile();
4004
+ if (!deferInitialVersion) {
4005
+ await this.makeNewVersion();
4006
+ this.metadata = this.getDefaultMetadata();
4007
+ this.metadata.version = this.currentVersion;
4008
+ this.makeNewFile();
4009
+ }
3755
4010
  } else {
3756
4011
  if (!this.metadata.files.length) {
3757
4012
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -3861,7 +4116,7 @@ var FileDatabase = class _FileDatabase {
3861
4116
  if (options.forceNewVersion && !this.versioned) {
3862
4117
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
3863
4118
  }
3864
- await this.prepare({ write: true });
4119
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
3865
4120
  const incomingDataType = detectDataType(data);
3866
4121
  this.metadata.dataType = incomingDataType;
3867
4122
  if (options.forceNewVersion) {
@@ -4155,7 +4410,7 @@ var FileDatabase = class _FileDatabase {
4155
4410
  }
4156
4411
  };
4157
4412
 
4158
- // src/tasks/taskLogs.ts
4413
+ // src/tasks/taskLogs.js
4159
4414
  function getLogsState(context) {
4160
4415
  const holder = context;
4161
4416
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -4208,6 +4463,89 @@ function getLogsState(context) {
4208
4463
  holder.__tasksLogsState = state;
4209
4464
  return state;
4210
4465
  }
4466
+ function ipcLogTargetKey(target) {
4467
+ const bp = target.basePath ?? "";
4468
+ const ns = target.namespace ?? "";
4469
+ return `${bp}::${ns}::${target.tableName}`;
4470
+ }
4471
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
4472
+ const seg = (s) => {
4473
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
4474
+ return t.length ? t : "x";
4475
+ };
4476
+ return `${seg(source)}/${seg(resource)}`;
4477
+ }
4478
+ async function readTaskIpcLogsSnapshot(context, options) {
4479
+ const holder = context;
4480
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
4481
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
4482
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
4483
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
4484
+ const fd = new FileDatabase({
4485
+ basePath,
4486
+ namespace,
4487
+ tableName,
4488
+ versioned: true,
4489
+ useMetadata: true,
4490
+ maxVersions: 30,
4491
+ pageSize: 2e3,
4492
+ logger: holder.logger
4493
+ });
4494
+ const versions = await fd.getVersions();
4495
+ if (versions.length === 0) {
4496
+ return { records: [], latestTs: null };
4497
+ }
4498
+ const latest = versions[versions.length - 1];
4499
+ const raw = await fd.read({ version: latest });
4500
+ const arr = Array.isArray(raw) ? raw : [];
4501
+ let filtered = arr;
4502
+ if (options.afterTs && String(options.afterTs).trim()) {
4503
+ const cut = String(options.afterTs).trim();
4504
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
4505
+ }
4506
+ let latestTs = null;
4507
+ for (const r of filtered) {
4508
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
4509
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
4510
+ }
4511
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
4512
+ const maxReturn = incremental ? 1e4 : tail;
4513
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
4514
+ return { records: sliced, latestTs };
4515
+ }
4516
+ function getLogsStateForTarget(context, target) {
4517
+ const holder = context;
4518
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4519
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4520
+ if (!enabled) return null;
4521
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
4522
+ const map = holder.__tasksLogsTargetStates;
4523
+ const key = ipcLogTargetKey(target);
4524
+ if (map.has(key)) return map.get(key);
4525
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4526
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4527
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4528
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4529
+ const db = new FileDatabase({
4530
+ basePath,
4531
+ namespace,
4532
+ tableName: target.tableName,
4533
+ versioned: true,
4534
+ useMetadata: true,
4535
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4536
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4537
+ logger: holder.logger
4538
+ });
4539
+ const state = {
4540
+ db,
4541
+ errorDb: null,
4542
+ queue: Promise.resolve(),
4543
+ initialized: false,
4544
+ errorInitialized: false
4545
+ };
4546
+ map.set(key, state);
4547
+ return state;
4548
+ }
4211
4549
  function isErrorPayload(payload) {
4212
4550
  if (!payload) return false;
4213
4551
  if (typeof payload === "object") {
@@ -4227,14 +4565,26 @@ function buildLogRecord(task, payload) {
4227
4565
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4228
4566
  opid: task.opid ?? null,
4229
4567
  taskId: task.id,
4230
- taskName: task.task,
4231
- target: task.target,
4568
+ taskName: task.name,
4569
+ target: task.service_group,
4232
4570
  source: typeof params.source === "string" ? params.source : null,
4233
4571
  resource: typeof params.resource === "string" ? params.resource : null,
4234
4572
  payload
4235
4573
  };
4236
4574
  }
4237
- function appendTaskIpcLog(context, task, payload) {
4575
+ function appendTaskIpcLog(context, task, payload, target) {
4576
+ if (target) {
4577
+ const state2 = getLogsStateForTarget(context, target);
4578
+ if (!state2?.db) return;
4579
+ const record2 = buildLogRecord(task, payload);
4580
+ state2.queue = state2.queue.then(async () => {
4581
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
4582
+ state2.initialized = true;
4583
+ }).catch((error) => {
4584
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
4585
+ });
4586
+ return;
4587
+ }
4238
4588
  const state = getLogsState(context);
4239
4589
  if (!state.db && !state.errorDb) return;
4240
4590
  const record = buildLogRecord(task, payload);
@@ -4252,83 +4602,280 @@ function appendTaskIpcLog(context, task, payload) {
4252
4602
  });
4253
4603
  }
4254
4604
 
4255
- // src/tasks/time-matcher.ts
4256
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4257
- function resolveAsterisks(field, range) {
4258
- return field.includes("*") ? field.replace("*", range) : field;
4259
- }
4260
- function resolveRanges(field) {
4261
- const regex = /(\d+)-(\d+)/;
4262
- let current = field;
4263
- while (true) {
4264
- const match = regex.exec(current);
4265
- if (!match) break;
4266
- const raw = match[0];
4267
- let first = Number(match[1]);
4268
- let last = Number(match[2]);
4269
- if (last < first) {
4270
- [first, last] = [last, first];
4271
- }
4272
- const values = [];
4273
- for (let i = first; i <= last; i += 1) {
4274
- values.push(i);
4275
- }
4276
- current = current.replace(raw, values.join(","));
4277
- }
4278
- return current;
4279
- }
4280
- function resolveSteps(field) {
4281
- const match = /^(.+)\/(\d+)$/.exec(field);
4282
- if (!match) return field;
4283
- const base = match[1];
4284
- const step = Number(match[2]);
4285
- if (!Number.isFinite(step) || step <= 0) return field;
4286
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4287
- }
4288
- function convertPattern(pattern) {
4289
- const parts = pattern.trim().split(/\s+/);
4290
- if (parts.length !== 6) {
4291
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4292
- }
4293
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4294
- }
4295
- function fieldMatches(field, value) {
4296
- const allowed = field.split(",").map((v) => Number(v));
4297
- return allowed.includes(value);
4298
- }
4299
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4300
- const parsed = convertPattern(pattern);
4301
- 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());
4302
- }
4303
-
4304
- // src/tasks/TaskMaster.ts
4305
- var TaskMaster = class {
4306
- context;
4307
- task;
4605
+ // src/tasks/AbstractTask.js
4606
+ var AbstractTask = class _AbstractTask {
4607
+ /**
4608
+ * Whether `send-task` should wait for completion (and print a result
4609
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
4610
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
4611
+ *
4612
+ * @type {boolean}
4613
+ */
4614
+ static defaultWaitForResult = false;
4615
+ /**
4616
+ * @param {object} context Runner context (db, logger, params, emitter...).
4617
+ * @param {object} task Task row as claimed from the queue.
4618
+ */
4308
4619
  constructor(context, task) {
4309
4620
  this.context = context;
4310
4621
  this.task = task;
4311
4622
  }
4623
+ /**
4624
+ * Return a short reason string when the task should be deferred (e.g. "locked
4625
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
4626
+ *
4627
+ * @returns {string | false | Promise<string | false>}
4628
+ */
4312
4629
  cantRunReason() {
4313
4630
  return false;
4314
4631
  }
4632
+ /**
4633
+ * Called by the runner when a stop has been requested. Subclasses running
4634
+ * long loops should flip a flag here and check it between iterations.
4635
+ *
4636
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
4637
+ */
4315
4638
  requestStop(_allowanceMs) {
4316
4639
  }
4640
+ /**
4641
+ * Perform the task. Must be implemented by subclasses.
4642
+ *
4643
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
4644
+ * Updates the DB `progress` column. Accepts any serializable value;
4645
+ * strings are stored verbatim, objects are JSON-stringified.
4646
+ * @returns {Promise<{ success: boolean, results: unknown }>}
4647
+ */
4648
+ async run(_reportProgress) {
4649
+ throw new Error("AbstractTask.run must be implemented by subclass");
4650
+ }
4651
+ /**
4652
+ * Resolve a complete row payload for this task — envelope fields (queue,
4653
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
4654
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
4655
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
4656
+ *
4657
+ * const payload = await TaskClass.resolveParams(context, { name });
4658
+ * await enqueueTask(context, payload);
4659
+ *
4660
+ * Validation failures throw {@link ParamError} so the script aborts before
4661
+ * a malformed row hits the DB.
4662
+ *
4663
+ * @param {object} context
4664
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
4665
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
4666
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
4667
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
4668
+ * @returns {Promise<object>}
4669
+ */
4670
+ static async resolveParams(context, overrides = {}) {
4671
+ const main = _AbstractTask._resolveMainFields(context, overrides);
4672
+ const params = await this.resolveCustomParams(context, overrides);
4673
+ return { ...main, params };
4674
+ }
4675
+ /**
4676
+ * Resolve the inner JSON blob stored in the `params` column. Default
4677
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
4678
+ * overlaid with `overrides.params` when supplied; returns `null` when
4679
+ * neither is provided.
4680
+ *
4681
+ * Subclasses with typed fields should override and call
4682
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
4683
+ * resolution, then validate and throw {@link ParamError} on bad input.
4684
+ *
4685
+ * @param {object} context
4686
+ * @param {Record<string, unknown>} [overrides]
4687
+ * @returns {Promise<object|null>}
4688
+ */
4689
+ static async resolveCustomParams(context, overrides = {}) {
4690
+ return _AbstractTask._defaultParamsBlob(context, overrides);
4691
+ }
4692
+ /**
4693
+ * Read main task envelope fields from `context.params` (CLI/env), with
4694
+ * any matching key on `overrides` taking precedence. Internal; called by
4695
+ * {@link AbstractTask.resolveParams}.
4696
+ *
4697
+ * @param {object} context
4698
+ * @param {Record<string, unknown>} [overrides]
4699
+ * @returns {object}
4700
+ */
4701
+ static _resolveMainFields(context, overrides = {}) {
4702
+ const defs2 = {
4703
+ queueName: "string default tasks",
4704
+ priority: "number default 50",
4705
+ serviceGroup: "string",
4706
+ serviceName: "string",
4707
+ instanceNumber: "number",
4708
+ serverName: "string",
4709
+ opid: "string",
4710
+ schedule: "string"
4711
+ };
4712
+ const cli = context.params.getAllForModule("task-envelope", defs2);
4713
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
4714
+ if (!name) {
4715
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
4716
+ }
4717
+ let instanceNumber;
4718
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
4719
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
4720
+ const n = Number(rawInstance);
4721
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
4722
+ throw new ParamError("--instanceNumber must be a positive integer when set");
4723
+ }
4724
+ instanceNumber = n;
4725
+ } else {
4726
+ instanceNumber = null;
4727
+ }
4728
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
4729
+ const priority = Number(priorityRaw);
4730
+ if (!Number.isFinite(priority)) {
4731
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
4732
+ }
4733
+ return {
4734
+ name,
4735
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
4736
+ priority,
4737
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
4738
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
4739
+ instanceNumber,
4740
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
4741
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
4742
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
4743
+ nextRunAt: overrides.nextRunAt ?? null
4744
+ };
4745
+ }
4746
+ /**
4747
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
4748
+ * object), then overlays `overrides.params` on top. Returns `null` when
4749
+ * neither is provided.
4750
+ *
4751
+ * @param {object} context
4752
+ * @param {Record<string, unknown>} [overrides]
4753
+ * @returns {object|null}
4754
+ */
4755
+ static _defaultParamsBlob(context, overrides = {}) {
4756
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
4757
+ const fromJson = parseParamsJson(cli.paramsJson);
4758
+ const fromOverride = pickParamsObject(overrides);
4759
+ if (!fromJson && !fromOverride) return null;
4760
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
4761
+ }
4762
+ /**
4763
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
4764
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
4765
+ * merges them with explicit `overrides.params` in increasing priority:
4766
+ *
4767
+ * typed CLI flags → --paramsJson → overrides.params
4768
+ *
4769
+ * Undefined values are dropped so defaults declared in `defs` aren't
4770
+ * overwritten by missing-flag noise. Returns the merged object; the
4771
+ * caller is responsible for validation and throwing `ParamError`.
4772
+ *
4773
+ * @param {object} context
4774
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
4775
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
4776
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
4777
+ * @returns {Record<string, unknown>}
4778
+ */
4779
+ static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
4780
+ const fullDefs = { ...defs2, paramsJson: "string" };
4781
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
4782
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
4783
+ const fromCli = {};
4784
+ for (const [k, v] of Object.entries(cliRaw)) {
4785
+ if (k === "paramsJson") continue;
4786
+ if (v !== void 0 && v !== null) fromCli[k] = v;
4787
+ }
4788
+ const fromOverride = pickParamsObject(overrides) ?? {};
4789
+ return { ...fromCli, ...fromJson, ...fromOverride };
4790
+ }
4317
4791
  };
4792
+ function emptyToUndef(s) {
4793
+ if (s === void 0 || s === null) return void 0;
4794
+ if (typeof s !== "string") return s;
4795
+ const t = s.trim();
4796
+ return t.length ? t : void 0;
4797
+ }
4798
+ function parseParamsJson(raw) {
4799
+ if (raw == null) return null;
4800
+ const t = String(raw).trim();
4801
+ if (!t) return null;
4802
+ let parsed;
4803
+ try {
4804
+ parsed = JSON.parse(t);
4805
+ } catch (e) {
4806
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
4807
+ }
4808
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4809
+ throw new ParamError("--paramsJson must be a JSON object");
4810
+ }
4811
+ return parsed;
4812
+ }
4813
+ function pickParamsObject(overrides) {
4814
+ const p = overrides?.params;
4815
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
4816
+ return void 0;
4817
+ }
4318
4818
 
4319
- // src/tasks/coreTasks/TaskPing.ts
4320
- var TaskPing = class extends TaskMaster {
4819
+ // src/tasks/coreTasks/TaskPing.js
4820
+ var TaskPing = class extends AbstractTask {
4821
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
4822
+ static defaultWaitForResult = true;
4823
+ /** Ping takes no params. */
4824
+ static async resolveCustomParams() {
4825
+ return null;
4826
+ }
4827
+ /**
4828
+ * @returns {Promise<{ success: true, results: "pong" }>}
4829
+ */
4321
4830
  async run() {
4322
4831
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4323
4832
  return { success: true, results: "pong" };
4324
4833
  }
4325
4834
  };
4326
4835
 
4327
- // src/tasks/coreTasks/TaskSampleProcess.ts
4328
- var TaskSampleProcess = class extends TaskMaster {
4329
- stopRequested = false;
4330
- stopAllowanceMs = 0;
4331
- stopDecisionLogged = false;
4836
+ // src/tasks/coreTasks/TaskSampleProcess.js
4837
+ var TaskSampleProcess = class extends AbstractTask {
4838
+ /**
4839
+ * @param {object} context
4840
+ * @param {Record<string, unknown>} [overrides]
4841
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
4842
+ */
4843
+ static async resolveCustomParams(context, overrides = {}) {
4844
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
4845
+ total: "number default 10",
4846
+ delay: "number default 1000",
4847
+ name: "string"
4848
+ }, overrides);
4849
+ const total = Number(merged.total);
4850
+ const delay = Number(merged.delay);
4851
+ if (!Number.isInteger(total) || total <= 0) {
4852
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
4853
+ }
4854
+ if (!Number.isInteger(delay) || delay < 0) {
4855
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
4856
+ }
4857
+ const out = { total, delay };
4858
+ if (typeof merged.name === "string" && merged.name.trim()) {
4859
+ out.name = merged.name.trim();
4860
+ }
4861
+ return out;
4862
+ }
4863
+ /**
4864
+ * @param {object} context
4865
+ * @param {object} task
4866
+ */
4867
+ constructor(context, task) {
4868
+ super(context, task);
4869
+ this.stopRequested = false;
4870
+ this.stopAllowanceMs = 0;
4871
+ this.stopDecisionLogged = false;
4872
+ }
4873
+ /**
4874
+ * Runner-facing stop signal. Records the allowance window so the main loop
4875
+ * can decide per-iteration whether to finish or abort early.
4876
+ *
4877
+ * @param {number} allowanceMs
4878
+ */
4332
4879
  requestStop(allowanceMs) {
4333
4880
  this.stopRequested = true;
4334
4881
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -4336,6 +4883,14 @@ var TaskSampleProcess = class extends TaskMaster {
4336
4883
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4337
4884
  );
4338
4885
  }
4886
+ /**
4887
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
4888
+ * progress every iteration. Validates params up front; invalid values short-
4889
+ * circuit to a structured failure without starting the loop.
4890
+ *
4891
+ * @param {(progress: object) => Promise<void>} reportProgress
4892
+ * @returns {Promise<{ success: boolean, results: unknown }>}
4893
+ */
4339
4894
  async run(reportProgress) {
4340
4895
  const totalRaw = this.task?.params?.total ?? 10;
4341
4896
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -4417,7 +4972,7 @@ var TaskSampleProcess = class extends TaskMaster {
4417
4972
  }
4418
4973
  };
4419
4974
 
4420
- // src/tasks/coreTasks/TaskShellCommand.ts
4975
+ // src/tasks/coreTasks/TaskShellCommand.js
4421
4976
  var import_node_child_process = require("child_process");
4422
4977
  function runShellCommand(command, cwd) {
4423
4978
  return new Promise((resolve2, reject) => {
@@ -4447,7 +5002,27 @@ function runShellCommand(command, cwd) {
4447
5002
  });
4448
5003
  });
4449
5004
  }
4450
- var TaskShellCommand = class extends TaskMaster {
5005
+ var TaskShellCommand = class extends AbstractTask {
5006
+ /**
5007
+ * @param {object} context
5008
+ * @param {Record<string, unknown>} [overrides]
5009
+ * @returns {Promise<{ command: string, cwd?: string }>}
5010
+ */
5011
+ static async resolveCustomParams(context, overrides = {}) {
5012
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
5013
+ command: "string",
5014
+ cwd: "string"
5015
+ }, overrides);
5016
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
5017
+ if (!command) {
5018
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
5019
+ }
5020
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
5021
+ return cwd ? { command, cwd } : { command };
5022
+ }
5023
+ /**
5024
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5025
+ */
4451
5026
  async run() {
4452
5027
  const params = this.task?.params;
4453
5028
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -4496,8 +5071,8 @@ var TaskShellCommand = class extends TaskMaster {
4496
5071
  }
4497
5072
  };
4498
5073
 
4499
- // src/tasks/coreTasks/TaskSystemInfo.ts
4500
- var import_node_os = __toESM(require("os"), 1);
5074
+ // src/tasks/coreTasks/TaskSystemInfo.js
5075
+ var import_node_os2 = __toESM(require("os"), 1);
4501
5076
  var import_promises = __toESM(require("fs/promises"), 1);
4502
5077
  function toGb(valueBytes) {
4503
5078
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
@@ -4516,13 +5091,22 @@ async function getDiskStats() {
4516
5091
  free: toGb(free)
4517
5092
  };
4518
5093
  }
4519
- var TaskSystemInfo = class extends TaskMaster {
5094
+ var TaskSystemInfo = class extends AbstractTask {
5095
+ /** Same UX expectation as `ping` — short probe, print the result. */
5096
+ static defaultWaitForResult = true;
5097
+ /** systemInfo takes no params. */
5098
+ static async resolveCustomParams() {
5099
+ return null;
5100
+ }
5101
+ /**
5102
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5103
+ */
4520
5104
  async run() {
4521
5105
  try {
4522
- const totalMemory = import_node_os.default.totalmem();
4523
- const freeMemory = import_node_os.default.freemem();
5106
+ const totalMemory = import_node_os2.default.totalmem();
5107
+ const freeMemory = import_node_os2.default.freemem();
4524
5108
  const usedMemory = totalMemory - freeMemory;
4525
- const cpus = import_node_os.default.cpus();
5109
+ const cpus = import_node_os2.default.cpus();
4526
5110
  const cpuUtilization = cpus.map((cpu) => {
4527
5111
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
4528
5112
  const usage = (total - cpu.times.idle) / total * 100;
@@ -4548,10 +5132,10 @@ var TaskSystemInfo = class extends TaskMaster {
4548
5132
  utilization: cpuUtilization
4549
5133
  },
4550
5134
  runtime: {
4551
- platform: import_node_os.default.platform(),
4552
- arch: import_node_os.default.arch(),
4553
- uptimeSec: import_node_os.default.uptime(),
4554
- hostname: import_node_os.default.hostname()
5135
+ platform: import_node_os2.default.platform(),
5136
+ arch: import_node_os2.default.arch(),
5137
+ uptimeSec: import_node_os2.default.uptime(),
5138
+ hostname: import_node_os2.default.hostname()
4555
5139
  }
4556
5140
  };
4557
5141
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -4568,8 +5152,31 @@ var TaskSystemInfo = class extends TaskMaster {
4568
5152
  }
4569
5153
  };
4570
5154
 
4571
- // src/tasks/coreTasks/TaskSumAB.ts
4572
- var TaskSumAB = class extends TaskMaster {
5155
+ // src/tasks/coreTasks/TaskSumAB.js
5156
+ var TaskSumAB = class extends AbstractTask {
5157
+ /** Short, deterministic — wait by default so callers see the sum. */
5158
+ static defaultWaitForResult = true;
5159
+ /**
5160
+ * @param {object} context
5161
+ * @param {Record<string, unknown>} [overrides]
5162
+ * @returns {Promise<{ a: number, b: number }>}
5163
+ */
5164
+ static async resolveCustomParams(context, overrides = {}) {
5165
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
5166
+ a: "number",
5167
+ b: "number"
5168
+ }, overrides);
5169
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
5170
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
5171
+ }
5172
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
5173
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
5174
+ }
5175
+ return { a: merged.a, b: merged.b };
5176
+ }
5177
+ /**
5178
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5179
+ */
4573
5180
  async run() {
4574
5181
  const a = this.task?.params?.a;
4575
5182
  const b = this.task?.params?.b;
@@ -4600,8 +5207,46 @@ var TaskSumAB = class extends TaskMaster {
4600
5207
  }
4601
5208
  };
4602
5209
 
4603
- // src/tasks/coreTasks/TaskStopRunner.ts
4604
- var TaskStopRunner = class extends TaskMaster {
5210
+ // src/tasks/coreTasks/TaskStopRunner.js
5211
+ var TaskStopRunner = class extends AbstractTask {
5212
+ /**
5213
+ * Stop tasks must target a concrete instance — without `serviceName` the
5214
+ * row would race against any worker on the queue. Layered on top of the
5215
+ * envelope built by {@link AbstractTask.resolveParams}.
5216
+ *
5217
+ * @param {object} context
5218
+ * @param {Record<string, unknown>} [overrides]
5219
+ * @returns {Promise<object>}
5220
+ */
5221
+ static async resolveParams(context, overrides = {}) {
5222
+ const main = await super.resolveParams(context, overrides);
5223
+ if (!main.serviceName) {
5224
+ throw new ParamError(
5225
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
5226
+ );
5227
+ }
5228
+ return main;
5229
+ }
5230
+ /**
5231
+ * @param {object} context
5232
+ * @param {Record<string, unknown>} [overrides]
5233
+ * @returns {Promise<{ allowanceMs: number }>}
5234
+ */
5235
+ static async resolveCustomParams(context, overrides = {}) {
5236
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
5237
+ allowanceMs: "number default 5000"
5238
+ }, overrides);
5239
+ const allowanceMs = Number(merged.allowanceMs);
5240
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
5241
+ throw new ParamError(
5242
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
5243
+ );
5244
+ }
5245
+ return { allowanceMs };
5246
+ }
5247
+ /**
5248
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
5249
+ */
4605
5250
  async run() {
4606
5251
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4607
5252
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -4616,45 +5261,203 @@ var TaskStopRunner = class extends TaskMaster {
4616
5261
  }
4617
5262
  };
4618
5263
 
4619
- // src/tasks/TasksRegistry.ts
5264
+ // src/tasks/coreTasks/TaskGetLogs.js
5265
+ var TaskGetLogs = class extends AbstractTask {
5266
+ /**
5267
+ * @param {object} context
5268
+ * @param {Record<string, unknown>} [overrides]
5269
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
5270
+ */
5271
+ static async resolveCustomParams(context, overrides = {}) {
5272
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
5273
+ source: "string",
5274
+ resource: "string",
5275
+ tail: "number default 100",
5276
+ afterTs: "string"
5277
+ }, overrides);
5278
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
5279
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
5280
+ if (!source) throw new ParamError('getLogs: param "source" is required');
5281
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
5282
+ let tail = Number(merged.tail);
5283
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
5284
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
5285
+ const out = { source, resource, tail };
5286
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
5287
+ out.afterTs = merged.afterTs.trim();
5288
+ }
5289
+ return out;
5290
+ }
5291
+ /**
5292
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
5293
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5294
+ */
5295
+ async run(_reportProgress) {
5296
+ const p = this.task.params ?? {};
5297
+ const source = String(p.source ?? "").trim();
5298
+ const resource = String(p.resource ?? "").trim();
5299
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
5300
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
5301
+ if (!source || !resource) {
5302
+ return {
5303
+ success: false,
5304
+ results: { error: 'getLogs requires params "source" and "resource"' }
5305
+ };
5306
+ }
5307
+ try {
5308
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
5309
+ source,
5310
+ resource,
5311
+ tail,
5312
+ afterTs
5313
+ });
5314
+ return {
5315
+ success: true,
5316
+ results: { records, latestTs, source, resource }
5317
+ };
5318
+ } catch (e) {
5319
+ return {
5320
+ success: false,
5321
+ results: { error: e?.message ?? String(e) }
5322
+ };
5323
+ }
5324
+ }
5325
+ };
5326
+
5327
+ // src/tasks/TasksRegistry.js
4620
5328
  var TasksRegistry = class _TasksRegistry {
4621
- map = {};
5329
+ /**
5330
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
5331
+ */
4622
5332
  constructor(initial) {
5333
+ this.map = {};
4623
5334
  if (initial) {
4624
5335
  this.addMany(initial);
4625
5336
  }
4626
5337
  }
5338
+ /**
5339
+ * Build a registry pre-populated with every core task plus legacy aliases.
5340
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
5341
+ *
5342
+ * @returns {TasksRegistry}
5343
+ */
4627
5344
  static withCoreTasks() {
4628
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
5345
+ 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);
4629
5346
  }
5347
+ /**
5348
+ * Register a single task class under a name. Overwrites any previous entry.
5349
+ *
5350
+ * @param {string} taskName
5351
+ * @param {Function} taskClass Subclass of `AbstractTask`.
5352
+ * @returns {this}
5353
+ */
4630
5354
  add(taskName, taskClass) {
4631
5355
  this.map[taskName] = taskClass;
4632
5356
  return this;
4633
5357
  }
5358
+ /**
5359
+ * Bulk-register a name → class map. Later calls override earlier ones.
5360
+ *
5361
+ * @param {Record<string, Function>} entries
5362
+ * @returns {this}
5363
+ */
4634
5364
  addMany(entries) {
4635
5365
  for (const [name, klass] of Object.entries(entries)) {
4636
5366
  this.add(name, klass);
4637
5367
  }
4638
5368
  return this;
4639
5369
  }
5370
+ /**
5371
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
5372
+ * the runner treats that as "some other worker may handle this" and skips.
5373
+ *
5374
+ * @param {string} taskName
5375
+ * @returns {Function | undefined}
5376
+ */
4640
5377
  get(taskName) {
4641
5378
  return this.map[taskName];
4642
5379
  }
5380
+ /**
5381
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
5382
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
5383
+ * paths where an unknown name is a hard CLI/programmer error.
5384
+ *
5385
+ * @param {string} taskName
5386
+ * @returns {Function}
5387
+ */
5388
+ requireClass(taskName) {
5389
+ const TaskClass = taskName ? this.map[taskName] : void 0;
5390
+ if (!TaskClass) {
5391
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
5392
+ throw new ParamError(
5393
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
5394
+ );
5395
+ }
5396
+ return TaskClass;
5397
+ }
5398
+ /**
5399
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
5400
+ * from `overrides` or `context.params`, look up the class, and delegate
5401
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
5402
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
5403
+ *
5404
+ * Validation failures (unknown task, missing required custom params, etc.)
5405
+ * surface as {@link ParamError} so the caller aborts cleanly before any
5406
+ * row is inserted.
5407
+ *
5408
+ * @param {object} context
5409
+ * @param {Record<string, unknown>} [overrides]
5410
+ * @returns {Promise<object>}
5411
+ */
5412
+ async resolveTaskParams(context, overrides = {}) {
5413
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
5414
+ const fromCli = context.params.get("name", "string");
5415
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
5416
+ const name = overrideName || cliName;
5417
+ if (!name) {
5418
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5419
+ }
5420
+ const TaskClass = this.requireClass(name);
5421
+ return TaskClass.resolveParams(context, { ...overrides, name });
5422
+ }
5423
+ /**
5424
+ * Names of every registered task, sorted alphabetically (useful for CLI output
5425
+ * and allowlist sanity checks).
5426
+ *
5427
+ * @returns {string[]}
5428
+ */
4643
5429
  listSupportedTasks() {
4644
5430
  return Object.keys(this.map).sort();
4645
5431
  }
5432
+ /**
5433
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
5434
+ * or for serialization.
5435
+ *
5436
+ * @returns {Record<string, Function>}
5437
+ */
4646
5438
  toObject() {
4647
5439
  return { ...this.map };
4648
5440
  }
4649
5441
  };
4650
5442
 
4651
- // src/tasks/taskScriptRunner.ts
5443
+ // src/tasks/serviceTaskAllowlist.js
5444
+ function normalizeAllowedTasks(value) {
5445
+ if (!value) return void 0;
5446
+ if (Array.isArray(value)) {
5447
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5448
+ return out2.length ? out2 : void 0;
5449
+ }
5450
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5451
+ return out.length ? out : void 0;
5452
+ }
5453
+
5454
+ // src/tasks/taskScriptRunner.js
4652
5455
  var import_node_child_process2 = require("child_process");
4653
5456
 
4654
- // src/tasks/index.ts
5457
+ // src/tasks/index.js
4655
5458
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4656
5459
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4657
- function getDb2(context) {
5460
+ function getDb3(context) {
4658
5461
  const db = context.db;
4659
5462
  if (!db) {
4660
5463
  throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
@@ -4666,15 +5469,6 @@ function normalizeRegistry(registry) {
4666
5469
  if (registry instanceof TasksRegistry) return registry;
4667
5470
  return new TasksRegistry().addMany(registry);
4668
5471
  }
4669
- function normalizeAllowedTasks(value) {
4670
- if (!value) return void 0;
4671
- if (Array.isArray(value)) {
4672
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
4673
- return out2.length ? out2 : void 0;
4674
- }
4675
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
4676
- return out.length ? out : void 0;
4677
- }
4678
5472
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
4679
5473
  context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
4680
5474
  for (const [, taskInstance] of runningTaskInstances) {
@@ -4689,19 +5483,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
4689
5483
  context.emitter.emit("stop", allowanceMs);
4690
5484
  }
4691
5485
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4692
- const db = getDb2(context);
4693
- const taskName = row.task;
5486
+ const db = getDb3(context);
5487
+ const taskName = row.name;
4694
5488
  const TaskClass = registry.get(taskName);
4695
- const { paused_at: _pausedAt, ...rowForHistory } = row;
4696
5489
  if (!TaskClass) {
4697
5490
  const err = { message: `Unknown task "${taskName}"` };
4698
- await db(historyTable).insert({
4699
- ...rowForHistory,
4700
- completed_at: /* @__PURE__ */ new Date(),
4701
- success: false,
4702
- params: toJsonColumn(row.params),
4703
- results: toJsonColumn(err)
4704
- });
5491
+ await db(historyTable).insert(
5492
+ taskHistoryInsertFromQueueRow(row, {
5493
+ completed_at: /* @__PURE__ */ new Date(),
5494
+ success: false,
5495
+ status: "failed",
5496
+ status_changed_at: db.fn.now(),
5497
+ params: toJsonColumn(row.params),
5498
+ results: toJsonColumn(err)
5499
+ })
5500
+ );
4705
5501
  if (row.schedule) {
4706
5502
  await db(tasksTable).where({ id: row.id }).update({
4707
5503
  started_at: null,
@@ -4709,7 +5505,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4709
5505
  success: false,
4710
5506
  results: toJsonColumn(err),
4711
5507
  past_due: null,
4712
- paused_at: db.fn.now(),
5508
+ status: "paused",
5509
+ status_changed_at: db.fn.now(),
4713
5510
  progress: LOCKED_BY_ERROR_MESSAGE
4714
5511
  });
4715
5512
  } else {
@@ -4736,13 +5533,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4736
5533
  } finally {
4737
5534
  runningTaskInstances.delete(row.id);
4738
5535
  }
4739
- await db(historyTable).insert({
4740
- ...rowForHistory,
4741
- completed_at: /* @__PURE__ */ new Date(),
4742
- success,
4743
- params: toJsonColumn(row.params),
4744
- results: toJsonColumn(results)
4745
- });
5536
+ await db(historyTable).insert(
5537
+ taskHistoryInsertFromQueueRow(row, {
5538
+ completed_at: /* @__PURE__ */ new Date(),
5539
+ success,
5540
+ status: success ? "completed" : "failed",
5541
+ status_changed_at: db.fn.now(),
5542
+ params: toJsonColumn(row.params),
5543
+ results: toJsonColumn(results)
5544
+ })
5545
+ );
4746
5546
  if (!success) {
4747
5547
  const dbName = String(context?.params?.get?.("dbName") || "local");
4748
5548
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -4757,19 +5557,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4757
5557
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
4758
5558
  appendTaskIpcLog(context, row, {
4759
5559
  level: "error",
4760
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
5560
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
4761
5561
  details: results
4762
5562
  });
4763
5563
  }
4764
5564
  if (row.schedule) {
4765
5565
  if (success) {
5566
+ let nextRunAt = null;
5567
+ try {
5568
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
5569
+ } catch (e) {
5570
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
5571
+ }
4766
5572
  await db(tasksTable).where({ id: row.id }).update({
4767
5573
  started_at: null,
4768
5574
  completed_at: /* @__PURE__ */ new Date(),
4769
5575
  success,
4770
5576
  results: toJsonColumn(results),
4771
5577
  progress: null,
4772
- past_due: null
5578
+ past_due: null,
5579
+ status: "idle",
5580
+ status_changed_at: db.fn.now(),
5581
+ next_run_at: nextRunAt,
5582
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
5583
+ service_name: null,
5584
+ server_name: null,
5585
+ instance_number: null
4773
5586
  });
4774
5587
  } else {
4775
5588
  await db(tasksTable).where({ id: row.id }).update({
@@ -4777,7 +5590,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4777
5590
  completed_at: /* @__PURE__ */ new Date(),
4778
5591
  success,
4779
5592
  results: toJsonColumn(results),
4780
- paused_at: db.fn.now(),
5593
+ status: "paused",
5594
+ status_changed_at: db.fn.now(),
4781
5595
  progress: LOCKED_BY_ERROR_MESSAGE,
4782
5596
  past_due: null
4783
5597
  });
@@ -4789,195 +5603,361 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4789
5603
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
4790
5604
  return { stopRunnerRequested, stopAllowanceMs };
4791
5605
  }
4792
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
4793
- const db = getDb2(context);
4794
- 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);
5606
+ function shuffleTaskRowsInPlace(rows) {
5607
+ for (let i = rows.length - 1; i > 0; i--) {
5608
+ const j = Math.floor(Math.random() * (i + 1));
5609
+ const t = rows[i];
5610
+ rows[i] = rows[j];
5611
+ rows[j] = t;
5612
+ }
5613
+ }
5614
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
5615
+ const db = getDb3(context);
5616
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
5617
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
5618
+ }).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);
4795
5619
  if (taskNames && taskNames.length > 0) {
4796
- query = query.whereIn("task", taskNames);
5620
+ query = query.whereIn("name", taskNames);
5621
+ }
5622
+ if (runnerIdentity) {
5623
+ query = query.where(function() {
5624
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
5625
+ }).where(function() {
5626
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
5627
+ }).where(function() {
5628
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
5629
+ });
5630
+ } else {
5631
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
4797
5632
  }
4798
5633
  const candidates = await query;
5634
+ shuffleTaskRowsInPlace(candidates);
4799
5635
  for (const row of candidates) {
4800
5636
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
4801
5637
  continue;
4802
5638
  }
4803
- const TaskClass = registry.get(row.task);
4804
- if (TaskClass) {
4805
- const taskInstance = new TaskClass(context, row);
4806
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
4807
- if (reason) {
4808
- if (!row.past_due) {
4809
- await db(tasksTable).where({ id: row.id }).update({
4810
- past_due: db.fn.now(),
4811
- progress: String(reason)
4812
- });
4813
- }
4814
- continue;
5639
+ const TaskClass = registry.get(row.name);
5640
+ if (!TaskClass) {
5641
+ continue;
5642
+ }
5643
+ const taskInstance = new TaskClass(context, row);
5644
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
5645
+ if (reason) {
5646
+ if (!row.past_due) {
5647
+ await db(tasksTable).where({ id: row.id }).update({
5648
+ past_due: db.fn.now(),
5649
+ progress: String(reason)
5650
+ });
4815
5651
  }
5652
+ continue;
4816
5653
  }
4817
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
5654
+ const claimPatch = {
5655
+ started_at: db.fn.now(),
5656
+ status: "running",
5657
+ status_changed_at: db.fn.now()
5658
+ };
5659
+ if (runnerIdentity) {
5660
+ claimPatch.service_name = runnerIdentity.service_name;
5661
+ claimPatch.server_name = runnerIdentity.server_name;
5662
+ claimPatch.instance_number = runnerIdentity.instance_number;
5663
+ }
5664
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
4818
5665
  const claimed = Array.isArray(updated) ? updated[0] : null;
4819
5666
  if (claimed) return claimed;
4820
5667
  }
4821
5668
  return null;
4822
5669
  }
4823
5670
  async function runTasksLoop(context, options) {
4824
- const queue = options.queue ?? "tasks";
5671
+ const queueName = options.queueName ?? "tasks";
4825
5672
  const target = options.target;
4826
5673
  const pollMs = options.pollMs ?? 1e3;
4827
- const maxParallel = options.maxParallel ?? 1;
5674
+ const claimJitterMs = options.claimJitterMs ?? 0;
5675
+ const maxParallel = options.maxParallel ?? 32;
4828
5676
  const scanLimit = options.scanLimit ?? 100;
4829
5677
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
4830
5678
  const registry = normalizeRegistry(options.registry);
4831
- const { tasksTable, historyTable } = queueToTableNames(queue);
5679
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
4832
5680
  if (!target) throw new Error("runTasksLoop: target is required");
5681
+ context.tasksQueueName = queueName;
4833
5682
  const runningPromises = /* @__PURE__ */ new Set();
4834
5683
  const runningTaskInstances = /* @__PURE__ */ new Map();
4835
5684
  let runningStopControlPromise = null;
4836
5685
  let stopRequested = false;
4837
5686
  let stopAllowanceMs = 5e3;
4838
- context.__tasksRunnerStop = false;
4839
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
4840
- if (!runningStopControlPromise) {
4841
- const claimedStopTask = await claimNextRunnableTask(
4842
- context,
4843
- tasksTable,
4844
- target,
4845
- registry,
4846
- 10,
4847
- ["stopRunner", "stop"]
4848
- );
4849
- if (claimedStopTask) {
4850
- runningStopControlPromise = executeClaimedTask(
5687
+ context.tasksRunnerStop = false;
5688
+ let registryReg = null;
5689
+ let registryInterval = null;
5690
+ let runnerIdentity = null;
5691
+ const hbGroup = options.runnerServiceGroup?.trim();
5692
+ if (hbGroup) {
5693
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
5694
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
5695
+ const defaultMeta = {
5696
+ component: "tasks-runner",
5697
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
5698
+ };
5699
+ registryReg = await registerInServicesRegistry(context, {
5700
+ queueName,
5701
+ target,
5702
+ serviceGroup: hbGroup,
5703
+ serviceName: options.runnerServiceName,
5704
+ instanceNumber: options.runnerInstanceNumber,
5705
+ staleMs,
5706
+ groupMaxInstances: options.runnerGroupMaxInstances,
5707
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
5708
+ metadata: options.runnerMetadata ?? defaultMeta
5709
+ });
5710
+ runnerIdentity = {
5711
+ service_name: registryReg.serviceName,
5712
+ server_name: import_node_os3.default.hostname(),
5713
+ instance_number: registryReg.instanceNumber
5714
+ };
5715
+ registryInterval = setInterval(() => {
5716
+ void touchServicesRegistry(context, registryReg).catch((err) => {
5717
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
5718
+ });
5719
+ }, hbIntervalMs);
5720
+ }
5721
+ try {
5722
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
5723
+ if (!runningStopControlPromise) {
5724
+ const claimedStopTask = await claimNextRunnableTask(
5725
+ context,
5726
+ tasksTable,
5727
+ target,
5728
+ registry,
5729
+ 10,
5730
+ ["stopRunner", "stop"],
5731
+ runnerIdentity
5732
+ );
5733
+ if (claimedStopTask) {
5734
+ runningStopControlPromise = executeClaimedTask(
5735
+ context,
5736
+ tasksTable,
5737
+ historyTable,
5738
+ claimedStopTask,
5739
+ registry,
5740
+ runningTaskInstances
5741
+ ).then(async (outcome) => {
5742
+ if (outcome.stopRunnerRequested && !stopRequested) {
5743
+ stopRequested = true;
5744
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5745
+ context.tasksRunnerStop = true;
5746
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5747
+ }
5748
+ }).finally(() => {
5749
+ runningStopControlPromise = null;
5750
+ });
5751
+ }
5752
+ }
5753
+ if (claimJitterMs > 0) {
5754
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
5755
+ }
5756
+ while (runningPromises.size < maxParallel) {
5757
+ const claimed = await claimNextRunnableTask(
4851
5758
  context,
4852
5759
  tasksTable,
4853
- historyTable,
4854
- claimedStopTask,
5760
+ target,
4855
5761
  registry,
4856
- runningTaskInstances
4857
- ).then(async (outcome) => {
5762
+ scanLimit,
5763
+ allowedTasks,
5764
+ runnerIdentity
5765
+ );
5766
+ if (!claimed) break;
5767
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4858
5768
  if (outcome.stopRunnerRequested && !stopRequested) {
4859
5769
  stopRequested = true;
4860
5770
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4861
- context.__tasksRunnerStop = true;
5771
+ context.tasksRunnerStop = true;
4862
5772
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4863
5773
  }
4864
5774
  }).finally(() => {
4865
- runningStopControlPromise = null;
5775
+ runningPromises.delete(p);
4866
5776
  });
5777
+ runningPromises.add(p);
5778
+ }
5779
+ const wakePromises = [...runningPromises];
5780
+ if (runningStopControlPromise) {
5781
+ wakePromises.push(runningStopControlPromise);
5782
+ }
5783
+ if (wakePromises.length === 0) {
5784
+ await sleepMs(pollMs);
5785
+ } else {
5786
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
5787
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
4867
5788
  }
4868
5789
  }
4869
- while (runningPromises.size < maxParallel) {
4870
- const claimed = await claimNextRunnableTask(
4871
- context,
4872
- tasksTable,
4873
- target,
4874
- registry,
4875
- scanLimit,
4876
- allowedTasks
4877
- );
4878
- if (!claimed) break;
4879
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4880
- if (outcome.stopRunnerRequested && !stopRequested) {
4881
- stopRequested = true;
4882
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4883
- context.__tasksRunnerStop = true;
4884
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4885
- }
4886
- }).finally(() => {
4887
- runningPromises.delete(p);
4888
- });
4889
- runningPromises.add(p);
5790
+ if (context.isStop() && !stopRequested) {
5791
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4890
5792
  }
4891
- await sleepMs(pollMs);
4892
- }
4893
- if (context.isStop() && !stopRequested) {
4894
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4895
- }
4896
- if (runningPromises.size > 0) {
4897
- if (stopRequested) {
4898
- await Promise.race([
4899
- Promise.allSettled(Array.from(runningPromises)),
4900
- sleepMs(stopAllowanceMs).then(() => {
4901
- context.logger.warn?.(
4902
- `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
4903
- );
4904
- })
4905
- ]);
4906
- } else {
4907
- await Promise.allSettled(Array.from(runningPromises));
5793
+ if (runningPromises.size > 0) {
5794
+ if (stopRequested) {
5795
+ await Promise.race([
5796
+ Promise.allSettled(Array.from(runningPromises)),
5797
+ sleepMs(stopAllowanceMs).then(() => {
5798
+ context.logger.warn?.(
5799
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
5800
+ );
5801
+ })
5802
+ ]);
5803
+ } else {
5804
+ await Promise.allSettled(Array.from(runningPromises));
5805
+ }
5806
+ }
5807
+ } finally {
5808
+ if (registryInterval) {
5809
+ clearInterval(registryInterval);
5810
+ registryInterval = null;
5811
+ }
5812
+ if (registryReg) {
5813
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
5814
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
5815
+ });
5816
+ registryReg = null;
5817
+ delete context.servicesRegistry;
5818
+ delete context.runnerHeartbeat;
4908
5819
  }
4909
5820
  }
4910
5821
  }
4911
5822
  var TasksManager = class _TasksManager {
4912
- context;
4913
- queue;
4914
- target;
4915
- recreateTaskTables;
4916
- pollMs;
4917
- maxParallel;
4918
- scanLimit;
4919
- allowedTasks;
4920
- registry;
5823
+ /**
5824
+ * @param {object} context
5825
+ * @param {{
5826
+ * queueName?: string,
5827
+ * target?: string,
5828
+ * recreateTaskTables?: boolean,
5829
+ * pollMs?: number,
5830
+ * claimJitterMs?: number,
5831
+ * maxParallel?: number,
5832
+ * scanLimit?: number,
5833
+ * allowedTasks?: string | string[],
5834
+ * registry?: TasksRegistry | Record<string, Function>,
5835
+ * runnerServiceGroup?: string,
5836
+ * runnerServiceName?: string,
5837
+ * runnerInstanceNumber?: number,
5838
+ * runnerHeartbeatIntervalMs?: number,
5839
+ * runnerHeartbeatStaleMs?: number,
5840
+ * runnerGroupMaxInstances?: number,
5841
+ * runnerEnforceMaxInstances?: boolean,
5842
+ * runnerMetadata?: Record<string, unknown>,
5843
+ * }} [options]
5844
+ */
4921
5845
  constructor(context, options = {}) {
4922
5846
  this.context = context;
4923
- this.queue = options.queue ?? "tasks";
5847
+ this.queueName = options.queueName ?? "tasks";
4924
5848
  this.target = options.target ?? "localRunner";
4925
5849
  this.recreateTaskTables = options.recreateTaskTables ?? false;
4926
5850
  this.pollMs = options.pollMs ?? 1e3;
5851
+ this.claimJitterMs = options.claimJitterMs ?? 0;
4927
5852
  this.maxParallel = options.maxParallel ?? 1;
4928
5853
  this.scanLimit = options.scanLimit ?? 100;
4929
5854
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
4930
5855
  this.registry = normalizeRegistry(options.registry);
5856
+ this.runnerServiceGroup = options.runnerServiceGroup;
5857
+ this.runnerServiceName = options.runnerServiceName;
5858
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
5859
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
5860
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
5861
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
5862
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
5863
+ this.runnerMetadata = options.runnerMetadata;
4931
5864
  }
5865
+ /**
5866
+ * Preferred factory: reads defaults from `context.params` (module namespace
5867
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
5868
+ * and inline options in one consistent resolver.
5869
+ *
5870
+ * @param {object} context
5871
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
5872
+ * @returns {TasksManager}
5873
+ */
4932
5874
  static init(context, options = {}) {
4933
5875
  const defs2 = {
4934
5876
  table: "string default tasks",
4935
5877
  target: "string default localRunner",
4936
5878
  recreateTaskTables: "boolean default false",
4937
5879
  pollMs: "number default 1000",
5880
+ claimJitterMs: "number default 0",
4938
5881
  maxParallel: "number default 1",
4939
5882
  scanLimit: "number default 100",
4940
- allowedTasks: "string"
5883
+ allowedTasks: "string",
5884
+ runnerServiceGroup: "string",
5885
+ runnerServiceName: "string",
5886
+ runnerInstanceNumber: "number",
5887
+ runnerHeartbeatIntervalMs: "number default 10000",
5888
+ runnerHeartbeatStaleMs: "number default 45000",
5889
+ runnerGroupMaxInstances: "number",
5890
+ runnerEnforceMaxInstances: "boolean default true"
4941
5891
  };
4942
- const discovered = context.params.getAllForModule(defs2);
5892
+ const discovered = context.params.getAllForModule("tasks", defs2);
4943
5893
  const resolved = {
4944
- queue: discovered.table,
5894
+ queueName: discovered.table,
4945
5895
  target: discovered.target,
4946
5896
  recreateTaskTables: discovered.recreateTaskTables,
4947
5897
  pollMs: discovered.pollMs,
5898
+ claimJitterMs: discovered.claimJitterMs,
4948
5899
  maxParallel: discovered.maxParallel,
4949
5900
  scanLimit: discovered.scanLimit,
4950
5901
  allowedTasks: discovered.allowedTasks,
5902
+ runnerServiceGroup: discovered.runnerServiceGroup,
5903
+ runnerServiceName: discovered.runnerServiceName,
5904
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
5905
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
5906
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
5907
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
5908
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
4951
5909
  ...options
4952
5910
  };
4953
5911
  return new _TasksManager(context, resolved);
4954
5912
  }
5913
+ /**
5914
+ * Idempotently ensure the three backing tables exist for this queue.
5915
+ *
5916
+ * @param {{ recreate?: boolean }} [options]
5917
+ * @returns {Promise<void>}
5918
+ */
4955
5919
  async ensureTaskTables(options = {}) {
4956
5920
  await ensureTaskTables(this.context, {
4957
- queue: this.queue,
5921
+ queueName: this.queueName,
4958
5922
  recreate: options.recreate ?? this.recreateTaskTables
4959
5923
  });
4960
5924
  }
5925
+ /**
5926
+ * Start the runner loop using this manager's resolved config. Per-call
5927
+ * options override the stored defaults, but `runnerMetadata` still falls
5928
+ * through when omitted.
5929
+ *
5930
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
5931
+ * @returns {Promise<void>}
5932
+ */
4961
5933
  async runTasksLoop(options = {}) {
4962
5934
  await runTasksLoop(this.context, {
4963
- queue: options.queue ?? this.queue,
5935
+ queueName: options.queueName ?? this.queueName,
4964
5936
  target: options.target ?? this.target,
4965
5937
  pollMs: options.pollMs ?? this.pollMs,
5938
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
4966
5939
  maxParallel: options.maxParallel ?? this.maxParallel,
4967
5940
  scanLimit: options.scanLimit ?? this.scanLimit,
4968
5941
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
4969
- registry: options.registry ?? this.registry
5942
+ registry: options.registry ?? this.registry,
5943
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
5944
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
5945
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
5946
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
5947
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
5948
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
5949
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
5950
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
4970
5951
  });
4971
5952
  }
4972
5953
  };
4973
5954
 
4974
- // src/scripts/cli-runner.ts
5955
+ // src/scripts/cli-runner.js
4975
5956
  var defs = {
4976
- dbName: "string default local",
4977
5957
  tasksModule: "string"
4978
5958
  };
4979
5959
  async function loadTasksModule(modulePath) {
4980
- const absolute = import_node_path.default.isAbsolute(modulePath) ? modulePath : import_node_path.default.resolve(process.cwd(), modulePath);
5960
+ const absolute = import_node_path2.default.isAbsolute(modulePath) ? modulePath : import_node_path2.default.resolve(process.cwd(), modulePath);
4981
5961
  const imported = await import((0, import_node_url.pathToFileURL)(absolute).href);
4982
5962
  if (!imported.tasksRegistry || typeof imported.tasksRegistry !== "object") {
4983
5963
  throw new Error(`tasksModule "${modulePath}" must export "tasksRegistry" object`);
@@ -4985,11 +5965,8 @@ async function loadTasksModule(modulePath) {
4985
5965
  return imported.tasksRegistry;
4986
5966
  }
4987
5967
  var flow = async (context) => {
4988
- const {
4989
- dbName,
4990
- tasksModule
4991
- } = context.params.getAll(defs);
4992
- const db = await dbInit(context, dbName);
5968
+ const { tasksModule } = context.params.getAll(defs);
5969
+ const db = await Db.init(context);
4993
5970
  context.db = db;
4994
5971
  const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
4995
5972
  if (tasksModule) {