@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
@@ -15,7 +15,7 @@ var __export = (target, all) => {
15
15
  __defProp(target, name, { get: all[name], enumerable: true });
16
16
  };
17
17
 
18
- // src/screen/components.ts
18
+ // src/screen/components.js
19
19
  import { createElement as h } from "react";
20
20
  import { Box, Text } from "ink";
21
21
  function getScreenWidth(maxWidth = null) {
@@ -90,13 +90,12 @@ function ScreenFooter({ lines, textStyle }) {
90
90
  );
91
91
  }
92
92
  var init_components = __esm({
93
- "src/screen/components.ts"() {
94
- "use strict";
93
+ "src/screen/components.js"() {
95
94
  }
96
95
  });
97
96
 
98
- // src/screen/list-components.ts
99
- import React2, { useState, useEffect, useRef, createElement } from "react";
97
+ // src/screen/list-components.js
98
+ import React, { useState, useEffect, useRef, createElement } from "react";
100
99
  import { Box as Box2, Text as Text2 } from "ink";
101
100
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
102
101
  const [, forceUpdate] = useState({});
@@ -232,11 +231,11 @@ function MultiColumnListWithPreviewComponent({
232
231
  const previewRows = [];
233
232
  if (typeof previewContent === "string") {
234
233
  previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(Text2, { bold: true }, previewContent) }));
235
- } else if (typeof previewContent === "object" && !React2.isValidElement(previewContent) && previewContent !== null) {
234
+ } else if (typeof previewContent === "object" && !React.isValidElement(previewContent) && previewContent !== null) {
236
235
  Object.entries(previewContent).forEach(([key, value], idx) => {
237
236
  previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(Text2, {}, `${key}: ${value}`) }));
238
237
  });
239
- } else if (React2.isValidElement(previewContent)) {
238
+ } else if (React.isValidElement(previewContent)) {
240
239
  previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
241
240
  }
242
241
  return h2(
@@ -249,11 +248,13 @@ function MultiColumnListWithPreviewComponent({
249
248
  ...previewRows
250
249
  );
251
250
  }
252
- function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
251
+ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " ", onSelectionChange }) {
253
252
  const [, forceUpdate] = useState({});
254
253
  const [sortOrder, setSortOrder] = useState("none");
255
254
  const [scrollOffset, setScrollOffset] = useState(0);
256
255
  const scrollStateRef = useRef({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
256
+ const itemsRef = useRef(items);
257
+ itemsRef.current = items;
257
258
  const defaultGetTitle = (item) => {
258
259
  return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
259
260
  };
@@ -267,18 +268,24 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
267
268
  return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
268
269
  }
269
270
  }) : items;
271
+ const displayItemsRef = useRef(displayItems);
272
+ displayItemsRef.current = displayItems;
270
273
  const effectiveMaxHeight = maxHeight || displayItems.length;
271
- const canScroll = displayItems.length > effectiveMaxHeight;
274
+ const _canScroll = displayItems.length > effectiveMaxHeight;
272
275
  const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
273
276
  const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
274
277
  const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
275
278
  const canScrollUp = clampedScrollOffset > 0;
276
279
  const canScrollDown = clampedScrollOffset < maxScrollOffset;
277
280
  scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
281
+ const onSelectionChangeRef = useRef(onSelectionChange);
282
+ onSelectionChangeRef.current = onSelectionChange;
278
283
  useEffect(() => {
279
284
  ctx.setAction("moveUp", () => {
280
285
  const newIndex = Math.max(0, selectedIndexRef.current - 1);
281
286
  selectedIndexRef.current = newIndex;
287
+ const list = displayItemsRef.current;
288
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
282
289
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
283
290
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
284
291
  const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
@@ -288,18 +295,11 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
288
295
  forceUpdate({});
289
296
  });
290
297
  ctx.setAction("moveDown", () => {
291
- const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
292
- const titleA = titleGetter(a).toLowerCase();
293
- const titleB = titleGetter(b).toLowerCase();
294
- if (sortOrder === "asc") {
295
- return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
296
- } else {
297
- return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
298
- }
299
- }) : items;
300
- const maxIndex = currentItems.length - 1;
298
+ const currentItems = displayItemsRef.current;
299
+ const maxIndex = Math.max(0, currentItems.length - 1);
301
300
  const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
302
301
  selectedIndexRef.current = newIndex;
302
+ onSelectionChangeRef.current?.(newIndex, currentItems[newIndex]);
303
303
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
304
304
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
305
305
  const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
@@ -309,8 +309,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
309
309
  forceUpdate({});
310
310
  });
311
311
  ctx.setAction("scrollUp", () => {
312
- const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
313
- const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
312
+ const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
314
313
  const newScrollOffset = Math.max(0, currentScrollOffset - 1);
315
314
  setScrollOffset(newScrollOffset);
316
315
  forceUpdate({});
@@ -325,9 +324,9 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
325
324
  if (sortable) {
326
325
  ctx.setAction("toggleSort", () => {
327
326
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
328
- const currentSelectedItem = displayItems[selectedIndexRef.current];
327
+ const currentSelectedItem = displayItemsRef.current[selectedIndexRef.current];
329
328
  setSortOrder(nextSort);
330
- const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
329
+ const newSortedItems = nextSort !== "none" ? [...itemsRef.current].sort((a, b) => {
331
330
  const titleA = titleGetter(a).toLowerCase();
332
331
  const titleB = titleGetter(b).toLowerCase();
333
332
  if (nextSort === "asc") {
@@ -335,7 +334,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
335
334
  } else {
336
335
  return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
337
336
  }
338
- }) : items;
337
+ }) : itemsRef.current;
339
338
  const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
340
339
  if (newIndex !== -1) {
341
340
  selectedIndexRef.current = newIndex;
@@ -473,14 +472,13 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
473
472
  }
474
473
  var h2;
475
474
  var init_list_components = __esm({
476
- "src/screen/list-components.ts"() {
477
- "use strict";
475
+ "src/screen/list-components.js"() {
478
476
  init_components();
479
477
  h2 = createElement;
480
478
  }
481
479
  });
482
480
 
483
- // src/screen/screens.ts
481
+ // src/screen/screens.js
484
482
  import { useState as useState2, createElement as h3 } from "react";
485
483
  import { render, useInput, Text as Text3 } from "ink";
486
484
  function groupKeyBindings(bindings) {
@@ -558,7 +556,7 @@ async function showScreen(config2) {
558
556
  let renderResult = null;
559
557
  let initialized = false;
560
558
  const Screen = () => {
561
- const [updateCounter, setUpdateCounter] = useState2(0);
559
+ const [, setUpdateCounter] = useState2(0);
562
560
  if (!initialized) {
563
561
  const defaultBindings = [
564
562
  { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
@@ -677,7 +675,7 @@ async function showScreen(config2) {
677
675
  }
678
676
  }
679
677
  if (matchedBinding && actions[matchedBinding.action]) {
680
- const actionResult = actions[matchedBinding.action]({
678
+ actions[matchedBinding.action]({
681
679
  input,
682
680
  key,
683
681
  binding: matchedBinding
@@ -807,8 +805,7 @@ async function showMultiColumnListWithPreviewScreen(config2) {
807
805
  }
808
806
  var showMenuScreen, showWordGridScreen;
809
807
  var init_screens = __esm({
810
- "src/screen/screens.ts"() {
811
- "use strict";
808
+ "src/screen/screens.js"() {
812
809
  init_components();
813
810
  init_list_components();
814
811
  showMenuScreen = showListScreen;
@@ -816,9 +813,9 @@ var init_screens = __esm({
816
813
  }
817
814
  });
818
815
 
819
- // src/screen/ui-elements.ts
816
+ // src/screen/ui-elements.js
820
817
  import { createElement as h4 } from "react";
821
- import { Box as Box4, Text as Text4 } from "ink";
818
+ import { Box as Box3, Text as Text4 } from "ink";
822
819
  function ListItem({
823
820
  children,
824
821
  isSelected = false,
@@ -828,7 +825,7 @@ function ListItem({
828
825
  dimColor = false
829
826
  }) {
830
827
  return h4(
831
- Box4,
828
+ Box3,
832
829
  {},
833
830
  h4(Text4, {
834
831
  color: isSelected ? backgroundColor || "green" : color,
@@ -843,10 +840,10 @@ function TextBlock({
843
840
  color = "white",
844
841
  dimmed = false,
845
842
  bold = false,
846
- maxWidth
843
+ maxWidth: _maxWidth
847
844
  }) {
848
845
  return h4(
849
- Box4,
846
+ Box3,
850
847
  {},
851
848
  h4(Text4, {
852
849
  color,
@@ -857,7 +854,7 @@ function TextBlock({
857
854
  }
858
855
  function Divider({ character = "\u2500", width = 80 }) {
859
856
  return h4(
860
- Box4,
857
+ Box3,
861
858
  { marginY: 1 },
862
859
  h4(Text4, { dimColor: true }, character.repeat(width))
863
860
  );
@@ -872,7 +869,7 @@ function GridCell({
872
869
  align = "left"
873
870
  }) {
874
871
  return h4(
875
- Box4,
872
+ Box3,
876
873
  { width },
877
874
  h4(Text4, {
878
875
  color,
@@ -883,44 +880,42 @@ function GridCell({
883
880
  }, children)
884
881
  );
885
882
  }
886
- function InputField({ prompt, value, onChange, onSubmit }) {
883
+ function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit }) {
887
884
  return h4(
888
- Box4,
885
+ Box3,
889
886
  { flexDirection: "column" },
890
887
  h4(Text4, {}, prompt),
891
888
  h4(
892
- Box4,
889
+ Box3,
893
890
  { marginTop: 1 },
894
891
  h4(Text4, { color: "cyan" }, " > ", value, "_")
895
892
  )
896
893
  );
897
894
  }
898
895
  var init_ui_elements = __esm({
899
- "src/screen/ui-elements.ts"() {
900
- "use strict";
896
+ "src/screen/ui-elements.js"() {
901
897
  }
902
898
  });
903
899
 
904
- // src/screen/utils.ts
900
+ // src/screen/utils.js
905
901
  function buildBreadcrumb(parts) {
906
902
  if (parts.length === 0) return "";
907
903
  if (parts.length === 1) return parts[0];
908
904
  return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
909
905
  }
910
- function buildDetailBreadcrumb(path5, suffix = "") {
911
- if (path5.length <= 1) {
912
- return suffix ? `\u2190 ${suffix}` : path5[0] || "";
906
+ function buildDetailBreadcrumb(path6, suffix = "") {
907
+ if (path6.length <= 1) {
908
+ return suffix ? `\u2190 ${suffix}` : path6[0] || "";
913
909
  }
914
- const breadcrumb = buildBreadcrumb(path5);
910
+ const breadcrumb = buildBreadcrumb(path6);
915
911
  return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
916
912
  }
917
913
  var init_utils = __esm({
918
- "src/screen/utils.ts"() {
919
- "use strict";
914
+ "src/screen/utils.js"() {
920
915
  }
921
916
  });
922
917
 
923
- // src/screen/footer-builder.ts
918
+ // src/screen/footer-builder.js
924
919
  function buildFooter(config2 = {}) {
925
920
  const {
926
921
  navigation = null,
@@ -971,8 +966,7 @@ function organizeFooterMessages(messages) {
971
966
  }
972
967
  var FooterPresets;
973
968
  var init_footer_builder = __esm({
974
- "src/screen/footer-builder.ts"() {
975
- "use strict";
969
+ "src/screen/footer-builder.js"() {
976
970
  FooterPresets = {
977
971
  /**
978
972
  * Menu screen footer
@@ -1031,10 +1025,10 @@ var init_footer_builder = __esm({
1031
1025
  }
1032
1026
  });
1033
1027
 
1034
- // src/screen/index.ts
1028
+ // src/screen/index.js
1035
1029
  var screen_exports = {};
1036
1030
  __export(screen_exports, {
1037
- Box: () => Box5,
1031
+ Box: () => Box4,
1038
1032
  Divider: () => Divider,
1039
1033
  FooterPresets: () => FooterPresets,
1040
1034
  GridCell: () => GridCell,
@@ -1043,7 +1037,7 @@ __export(screen_exports, {
1043
1037
  ListItem: () => ListItem,
1044
1038
  MultiColumnListComponent: () => MultiColumnListComponent,
1045
1039
  MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1046
- React: () => React5,
1040
+ React: () => React2,
1047
1041
  ScreenBody: () => ScreenBody,
1048
1042
  ScreenContainer: () => ScreenContainer,
1049
1043
  ScreenDivider: () => ScreenDivider,
@@ -1057,6 +1051,7 @@ __export(screen_exports, {
1057
1051
  buildFooter: () => buildFooter,
1058
1052
  h: () => createElement2,
1059
1053
  load: () => load,
1054
+ memo: () => memo,
1060
1055
  organizeFooterMessages: () => organizeFooterMessages,
1061
1056
  showListScreen: () => showListScreen,
1062
1057
  showMenuScreen: () => showMenuScreen,
@@ -1065,14 +1060,15 @@ __export(screen_exports, {
1065
1060
  showScreen: () => showScreen,
1066
1061
  showWordGridScreen: () => showWordGridScreen,
1067
1062
  useCallback: () => useCallback,
1068
- useEffect: () => useEffect3,
1063
+ useEffect: () => useEffect2,
1069
1064
  useInput: () => useInput2,
1065
+ useLayoutEffect: () => useLayoutEffect,
1070
1066
  useMemo: () => useMemo,
1071
- useRef: () => useRef3,
1067
+ useRef: () => useRef2,
1072
1068
  useState: () => useState3
1073
1069
  });
1074
- import React5, { useState as useState3, useEffect as useEffect3, useRef as useRef3, useMemo, useCallback, createElement as createElement2 } from "react";
1075
- import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
1070
+ import React2, { useState as useState3, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useMemo, useCallback, memo, createElement as createElement2 } from "react";
1071
+ import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
1076
1072
  async function load() {
1077
1073
  if (loadPromise) return loadPromise;
1078
1074
  loadPromise = Promise.all([
@@ -1084,8 +1080,7 @@ async function load() {
1084
1080
  }
1085
1081
  var loadPromise;
1086
1082
  var init_screen = __esm({
1087
- "src/screen/index.ts"() {
1088
- "use strict";
1083
+ "src/screen/index.js"() {
1089
1084
  init_screens();
1090
1085
  init_list_components();
1091
1086
  init_components();
@@ -1100,11 +1095,11 @@ var init_screen = __esm({
1100
1095
  }
1101
1096
  });
1102
1097
 
1103
- // src/scripts/cli-runner.ts
1104
- import path4 from "path";
1098
+ // src/scripts/cli-runner.js
1099
+ import path5 from "path";
1105
1100
  import { pathToFileURL } from "url";
1106
1101
 
1107
- // src/args/index.ts
1102
+ // src/args/index.js
1108
1103
  import { readFileSync, existsSync } from "fs";
1109
1104
  import { resolve, dirname, basename, extname, join, isAbsolute } from "path";
1110
1105
  import { config } from "dotenv";
@@ -1587,10 +1582,10 @@ var Args = class _Args {
1587
1582
  }
1588
1583
  };
1589
1584
 
1590
- // src/params/index.ts
1585
+ // src/params/index.js
1591
1586
  import Joi from "joi";
1592
1587
 
1593
- // src/errors.ts
1588
+ // src/errors.js
1594
1589
  var FrameworkError = class extends Error {
1595
1590
  constructor(message) {
1596
1591
  super(message);
@@ -1616,7 +1611,7 @@ var FileDatabaseError = class extends FrameworkError {
1616
1611
  }
1617
1612
  };
1618
1613
 
1619
- // src/params/custom-types.ts
1614
+ // src/params/custom-types.js
1620
1615
  var joiEdateType = (value, helpers) => {
1621
1616
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
1622
1617
  const testDate = new Date(value);
@@ -1709,7 +1704,7 @@ function calculateTimeOffset(amount, unit, sign) {
1709
1704
  }
1710
1705
  return sign === "+" ? amount * multiplier : -amount * multiplier;
1711
1706
  }
1712
- var joiStringArrayType = (type) => (value, helpers) => {
1707
+ var joiStringArrayType = (type) => (value, _helpers) => {
1713
1708
  if (value === void 0 || typeof value === "function") {
1714
1709
  return [];
1715
1710
  }
@@ -1735,7 +1730,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
1735
1730
  return arr;
1736
1731
  };
1737
1732
 
1738
- // src/params/index.ts
1733
+ // src/params/index.js
1739
1734
  var Params = class _Params {
1740
1735
  context;
1741
1736
  // Partial context during initialization
@@ -1927,7 +1922,7 @@ var Params = class _Params {
1927
1922
  throw new ParamError(`default value "${defValObj.value}" type mismatch`);
1928
1923
  }
1929
1924
  type = type.default(defValObj.value);
1930
- } else if (str.match(/required/)) {
1925
+ } else if (str.match(/\s*required\s*/)) {
1931
1926
  type = type.required();
1932
1927
  } else {
1933
1928
  type = type.optional();
@@ -1995,7 +1990,7 @@ var Params = class _Params {
1995
1990
  definition = val;
1996
1991
  val = val.value;
1997
1992
  }
1998
- const def = this.assignDefinition(key, definition);
1993
+ this.assignDefinition(key, definition);
1999
1994
  if (!this.runAllRegisteredSetters(key, val)) {
2000
1995
  this.params[key] = val;
2001
1996
  }
@@ -2003,6 +1998,8 @@ var Params = class _Params {
2003
1998
  /**
2004
1999
  * Get all parameters from definitions (main script).
2005
2000
  * Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
2001
+ * Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
2002
+ * around {@link get}) so --showUsedParams groups usage correctly.
2006
2003
  */
2007
2004
  getAll(defs2) {
2008
2005
  return this.getAllForModule("script", defs2);
@@ -2039,6 +2036,19 @@ var Params = class _Params {
2039
2036
  this._currentModule = prev;
2040
2037
  }
2041
2038
  }
2039
+ /**
2040
+ * Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
2041
+ * under the same module (for --showUsedParams / getFiguredByModule).
2042
+ */
2043
+ runWithModule(moduleName, fn) {
2044
+ const prev = this._currentModule;
2045
+ this._currentModule = moduleName;
2046
+ try {
2047
+ return fn();
2048
+ } finally {
2049
+ this._currentModule = prev;
2050
+ }
2051
+ }
2042
2052
  /**
2043
2053
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2044
2054
  */
@@ -2052,9 +2062,9 @@ var Params = class _Params {
2052
2062
  if (!parenMatch) continue;
2053
2063
  const parts = parenMatch[1].split(":");
2054
2064
  if (parts.length < 3) continue;
2055
- const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2056
- if (!path5 || path5.includes(paramsIndexPath)) continue;
2057
- const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2065
+ const path6 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2066
+ if (!path6 || path6.includes(paramsIndexPath)) continue;
2067
+ const srcMatch = path6.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2058
2068
  if (srcMatch) return srcMatch[1];
2059
2069
  }
2060
2070
  return "script";
@@ -2099,11 +2109,11 @@ var Params = class _Params {
2099
2109
  }
2100
2110
  };
2101
2111
 
2102
- // src/logger/index.ts
2112
+ // src/logger/index.js
2103
2113
  import chalk from "chalk";
2104
2114
  import util from "util";
2105
2115
 
2106
- // src/logger/transports.ts
2116
+ // src/logger/transports.js
2107
2117
  var ConsoleTransport = class {
2108
2118
  write(payload) {
2109
2119
  console.info(payload);
@@ -2123,7 +2133,7 @@ var ParentProcessTransport = class {
2123
2133
  }
2124
2134
  };
2125
2135
 
2126
- // src/logger/index.ts
2136
+ // src/logger/index.js
2127
2137
  var ALL_LEVELS = [
2128
2138
  "silly",
2129
2139
  "debug",
@@ -2196,6 +2206,7 @@ var Logger = class _Logger {
2196
2206
  }
2197
2207
  /**
2198
2208
  * Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
2209
+ * Params are tracked under the `logger` module for --showUsedParams.
2199
2210
  */
2200
2211
  static init(context, options) {
2201
2212
  const paramDefs = {
@@ -2209,7 +2220,7 @@ var Logger = class _Logger {
2209
2220
  progressWithTimes: "boolean default false",
2210
2221
  progressThrottleMs: "number"
2211
2222
  };
2212
- const discovered = context.params.getAllForModule(paramDefs);
2223
+ const discovered = context.params.getAllForModule("logger", paramDefs);
2213
2224
  const config2 = { ...discovered, ...options };
2214
2225
  const logger = new _Logger(context, config2);
2215
2226
  context.logger = logger;
@@ -2405,9 +2416,9 @@ var Logger = class _Logger {
2405
2416
  }
2406
2417
  };
2407
2418
 
2408
- // src/init/index.ts
2419
+ // src/init/index.js
2409
2420
  import { EventEmitter } from "events";
2410
- function extractComponentOptions(opts, componentName) {
2421
+ function extractComponentOptions(opts, _componentName) {
2411
2422
  const reservedKeys = ["overrides", "defaults", "modules"];
2412
2423
  const componentOptions = {};
2413
2424
  for (const [key, value] of Object.entries(opts)) {
@@ -2474,6 +2485,19 @@ function printAllParameters(context) {
2474
2485
  async function init(flow2, opts = {}) {
2475
2486
  let stop = false;
2476
2487
  let context = null;
2488
+ let cleanupRan = false;
2489
+ const runRegisteredCleanups = async (ctx) => {
2490
+ if (cleanupRan) return;
2491
+ cleanupRan = true;
2492
+ const fns = [...ctx.cleanupFunctions].reverse();
2493
+ for (const fn of fns) {
2494
+ try {
2495
+ await fn(ctx);
2496
+ } catch (error) {
2497
+ ctx.logger.warn("[cleanup] error in cleanup function:", error);
2498
+ }
2499
+ }
2500
+ };
2477
2501
  try {
2478
2502
  try {
2479
2503
  const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
@@ -2496,13 +2520,24 @@ async function init(flow2, opts = {}) {
2496
2520
  printAllParameters(context);
2497
2521
  process.exit(0);
2498
2522
  }
2523
+ let sigintCount = 0;
2499
2524
  process.on("SIGINT", async () => {
2500
- if (stop) {
2501
- context.logger.warn("[process] killed");
2502
- process.exit(2);
2525
+ if (!context) return;
2526
+ sigintCount += 1;
2527
+ if (sigintCount === 1) {
2528
+ stop = true;
2529
+ context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2530
+ context.emitter.emit("stop", stopAllowance);
2531
+ return;
2503
2532
  }
2533
+ context.logger.warn("[process] second SIGINT: running cleanup then exit");
2534
+ await runRegisteredCleanups(context);
2535
+ process.exit(2);
2536
+ });
2537
+ process.on("SIGTERM", () => {
2538
+ if (!context || stop) return;
2504
2539
  stop = true;
2505
- context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2540
+ context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
2506
2541
  context.emitter.emit("stop", stopAllowance);
2507
2542
  });
2508
2543
  await flow2(context);
@@ -2527,33 +2562,62 @@ async function init(flow2, opts = {}) {
2527
2562
  }
2528
2563
  } finally {
2529
2564
  if (context) {
2530
- for (const fn of context.cleanupFunctions.reverse()) {
2531
- try {
2532
- await fn(context);
2533
- } catch (error) {
2534
- context.logger.warn("[cleanup] error in cleanup function:", error);
2535
- }
2536
- }
2565
+ await runRegisteredCleanups(context);
2537
2566
  }
2538
2567
  }
2539
2568
  }
2540
2569
 
2541
- // src/db/index.ts
2570
+ // src/db/index.js
2542
2571
  import knex from "knex";
2572
+ var KNEX_DEFAULTS = {
2573
+ testConnection: true,
2574
+ pool: { min: 2, max: 10 },
2575
+ acquireConnectionTimeout: 1e4,
2576
+ ssl: { rejectUnauthorized: false }
2577
+ };
2543
2578
  var Db = class {
2544
- knexInstance = null;
2545
- config;
2546
- logger;
2547
- queriesLog = [];
2548
- isConnected = false;
2549
- /**
2550
- * Constructor - accepts config object
2551
- * Use dbInit() function to initialize with Context
2552
- */
2579
+ static async init(context, options = {}) {
2580
+ const defs2 = {
2581
+ dbName: "string",
2582
+ dbConnectionString: "string",
2583
+ dbProfile: "boolean default false"
2584
+ };
2585
+ const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
2586
+ const merged = { ...discovered, ...options };
2587
+ let { dbName, dbConnectionString } = merged;
2588
+ const { dbProfile } = merged;
2589
+ if (!dbName && !dbConnectionString) {
2590
+ dbName = "local";
2591
+ }
2592
+ if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
2593
+ dbConnectionString = dbName;
2594
+ dbName = void 0;
2595
+ }
2596
+ if (dbName && !dbConnectionString) {
2597
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2598
+ dbConnectionString = await context.params.get(paramName, "string");
2599
+ if (!dbConnectionString) {
2600
+ throw new ParamError(
2601
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2602
+ );
2603
+ }
2604
+ }
2605
+ const config2 = {
2606
+ ...KNEX_DEFAULTS,
2607
+ connectionString: dbConnectionString,
2608
+ name: dbName || merged.name || "default",
2609
+ profile: !!dbProfile,
2610
+ logger: context.logger
2611
+ };
2612
+ return dbConnect(context, config2);
2613
+ }
2553
2614
  constructor(config2) {
2554
- if (!config2.connectionString) {
2615
+ if (!config2 || !config2.connectionString) {
2555
2616
  throw new ParamError("Db: connectionString is required");
2556
2617
  }
2618
+ this.knexInstance = null;
2619
+ this.isConnected = false;
2620
+ this.queriesLog = [];
2557
2621
  this.config = {
2558
2622
  testConnection: true,
2559
2623
  profile: false,
@@ -2566,25 +2630,23 @@ var Db = class {
2566
2630
  };
2567
2631
  this.logger = this.config.logger;
2568
2632
  const instance = this;
2569
- const callableWrapper = function(...args) {
2633
+ const callableWrapper = function() {
2570
2634
  throw new Error("This should never be called directly");
2571
2635
  };
2572
2636
  callableWrapper._instance = instance;
2573
2637
  return new Proxy(callableWrapper, {
2574
- // Intercept function calls: db('table')
2575
- apply: (target, thisArg, argumentsList) => {
2638
+ apply: (target, _thisArg, argumentsList) => {
2576
2639
  const inst = target._instance;
2577
2640
  if (!inst.knexInstance) {
2578
2641
  throw new Error("Db: Not connected. Call connect() first.");
2579
2642
  }
2580
2643
  return inst.knexInstance(...argumentsList);
2581
2644
  },
2582
- // Intercept property access: db.schema, db.raw, etc.
2583
2645
  get: (target, prop) => {
2584
2646
  if (prop === "_instance") {
2585
2647
  return target._instance;
2586
2648
  }
2587
- const instance2 = target._instance;
2649
+ const inst = target._instance;
2588
2650
  const ownMethods = [
2589
2651
  "connect",
2590
2652
  "disconnect",
@@ -2597,26 +2659,26 @@ var Db = class {
2597
2659
  "detectClient",
2598
2660
  "attachProfiler"
2599
2661
  ];
2600
- if (prop in instance2) {
2601
- const value = instance2[prop];
2662
+ if (prop in inst) {
2663
+ const value = inst[prop];
2602
2664
  if (typeof value === "function" && ownMethods.includes(prop)) {
2603
- return value.bind(instance2);
2665
+ return value.bind(inst);
2604
2666
  }
2605
2667
  if (typeof value !== "function") {
2606
2668
  return value;
2607
2669
  }
2608
2670
  }
2609
- if (instance2.knexInstance) {
2610
- const knexProp = instance2.knexInstance[prop];
2671
+ if (inst.knexInstance) {
2672
+ const knexProp = inst.knexInstance[prop];
2611
2673
  if (typeof knexProp === "function") {
2612
- return knexProp.bind(instance2.knexInstance);
2674
+ return knexProp.bind(inst.knexInstance);
2613
2675
  }
2614
2676
  return knexProp;
2615
2677
  }
2616
- if (prop in instance2) {
2617
- const method = instance2[prop];
2678
+ if (prop in inst) {
2679
+ const method = inst[prop];
2618
2680
  if (typeof method === "function") {
2619
- return method.bind(instance2);
2681
+ return method.bind(inst);
2620
2682
  }
2621
2683
  return method;
2622
2684
  }
@@ -2624,9 +2686,6 @@ var Db = class {
2624
2686
  }
2625
2687
  });
2626
2688
  }
2627
- /**
2628
- * Detect database client type from connection string
2629
- */
2630
2689
  detectClient(connectionString) {
2631
2690
  if (connectionString.match(/^postgresql/)) {
2632
2691
  return "pg";
@@ -2636,9 +2695,6 @@ var Db = class {
2636
2695
  }
2637
2696
  return null;
2638
2697
  }
2639
- /**
2640
- * Connect to the database
2641
- */
2642
2698
  async connect() {
2643
2699
  if (this.isConnected && this.knexInstance) {
2644
2700
  this.logger.warn?.("[Db] Already connected");
@@ -2647,14 +2703,13 @@ var Db = class {
2647
2703
  const client = this.detectClient(this.config.connectionString);
2648
2704
  if (!client) {
2649
2705
  throw new ParamError(
2650
- `Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
2706
+ "Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
2651
2707
  );
2652
2708
  }
2653
2709
  try {
2654
2710
  const connectionConfig = {
2655
2711
  connectionString: this.config.connectionString,
2656
2712
  family: 4
2657
- // Force IPv4 only (disable IPv6)
2658
2713
  };
2659
2714
  this.knexInstance = knex({
2660
2715
  client,
@@ -2670,7 +2725,9 @@ var Db = class {
2670
2725
  await this.testConnection();
2671
2726
  }
2672
2727
  this.isConnected = true;
2673
- this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
2728
+ this.logger.debug?.(
2729
+ `[Db] Connected to database "${this.config.name || this.config.connectionString}"`
2730
+ );
2674
2731
  } catch (error) {
2675
2732
  if (error instanceof ParamError) {
2676
2733
  throw error;
@@ -2679,9 +2736,6 @@ var Db = class {
2679
2736
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
2680
2737
  }
2681
2738
  }
2682
- /**
2683
- * Disconnect from the database
2684
- */
2685
2739
  async disconnect() {
2686
2740
  if (!this.knexInstance) {
2687
2741
  return;
@@ -2691,16 +2745,15 @@ var Db = class {
2691
2745
  this.knexInstance = null;
2692
2746
  this.isConnected = false;
2693
2747
  this.queriesLog = [];
2694
- this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
2748
+ this.logger.debug?.(
2749
+ `[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
2750
+ );
2695
2751
  } catch (error) {
2696
2752
  const errorMsg = this.getErrorMessage(error);
2697
2753
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
2698
2754
  throw error;
2699
2755
  }
2700
2756
  }
2701
- /**
2702
- * Extract error message from various error types
2703
- */
2704
2757
  getErrorMessage(error) {
2705
2758
  if (error instanceof AggregateError) {
2706
2759
  const errors = error.errors || [];
@@ -2725,9 +2778,11 @@ var Db = class {
2725
2778
  return `${code} (tried: ${addresses.join(", ")})`;
2726
2779
  }
2727
2780
  }
2728
- const uniqueMessages = [...new Set(errors.map((e) => {
2729
- return e instanceof Error ? e.message : String(e);
2730
- }))];
2781
+ const uniqueMessages = [
2782
+ ...new Set(
2783
+ errors.map((e) => e instanceof Error ? e.message : String(e))
2784
+ )
2785
+ ];
2731
2786
  if (uniqueMessages.length === 1) {
2732
2787
  return uniqueMessages[0];
2733
2788
  }
@@ -2736,28 +2791,25 @@ var Db = class {
2736
2791
  return error.message || "Multiple errors occurred";
2737
2792
  }
2738
2793
  if (error instanceof Error) {
2739
- const errorWithCode = error;
2740
- if (errorWithCode.code) {
2741
- return `${errorWithCode.code}: ${error.message || String(error)}`;
2794
+ const code = error.code;
2795
+ if (code) {
2796
+ return `${code}: ${error.message || String(error)}`;
2742
2797
  }
2743
2798
  return error.message || String(error);
2744
2799
  }
2745
2800
  if (typeof error === "string") {
2746
2801
  return error;
2747
2802
  }
2748
- if (error?.message) {
2803
+ if (error && typeof error === "object" && "message" in error) {
2749
2804
  const msg = String(error.message);
2750
- const errorWithCode = error;
2751
- if (errorWithCode.code) {
2752
- return `${errorWithCode.code}: ${msg}`;
2805
+ const code = error.code;
2806
+ if (code) {
2807
+ return `${code}: ${msg}`;
2753
2808
  }
2754
2809
  return msg;
2755
2810
  }
2756
2811
  return String(error) || "Unknown error";
2757
2812
  }
2758
- /**
2759
- * Test database connection
2760
- */
2761
2813
  async testConnection() {
2762
2814
  if (!this.knexInstance) {
2763
2815
  throw new Error("Db: Not connected. Call connect() first.");
@@ -2773,9 +2825,6 @@ var Db = class {
2773
2825
  throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
2774
2826
  }
2775
2827
  }
2776
- /**
2777
- * Attach query profiler to log all queries
2778
- */
2779
2828
  attachProfiler() {
2780
2829
  if (!this.knexInstance) {
2781
2830
  return;
@@ -2785,7 +2834,7 @@ var Db = class {
2785
2834
  this.knexInstance.on("query", (query) => {
2786
2835
  query.__startTime = process.hrtime();
2787
2836
  });
2788
- this.knexInstance.on("query-response", (response, query) => {
2837
+ this.knexInstance.on("query-response", (_response, query) => {
2789
2838
  const [seconds, nanoseconds] = process.hrtime(query.__startTime);
2790
2839
  const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
2791
2840
  const logEntry = {
@@ -2800,15 +2849,9 @@ var Db = class {
2800
2849
  this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
2801
2850
  });
2802
2851
  }
2803
- /**
2804
- * Get query log (only available if profiling is enabled)
2805
- */
2806
2852
  getQueryLog() {
2807
2853
  return [...this.queriesLog];
2808
2854
  }
2809
- /**
2810
- * Check if a table exists
2811
- */
2812
2855
  async tableExists(tableName) {
2813
2856
  if (!this.knexInstance) {
2814
2857
  throw new Error("Db: Not connected. Call connect() first.");
@@ -2820,65 +2863,28 @@ var Db = class {
2820
2863
  throw error;
2821
2864
  }
2822
2865
  }
2823
- /**
2824
- * Get the underlying Knex instance (for advanced usage)
2825
- */
2826
2866
  getKnex() {
2827
2867
  if (!this.knexInstance) {
2828
2868
  throw new Error("Db: Not connected. Call connect() first.");
2829
2869
  }
2830
2870
  return this.knexInstance;
2831
2871
  }
2832
- /**
2833
- * Get connection status
2834
- */
2835
2872
  isConnectedToDb() {
2836
2873
  return this.isConnected && this.knexInstance !== null;
2837
2874
  }
2838
- /**
2839
- * Initialize Db with context (connects and registers disconnect cleanup).
2840
- * Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
2841
- */
2842
- static async init(context, dbNameOrConnectionString) {
2843
- return dbFindAndConnect(context, dbNameOrConnectionString);
2844
- }
2845
2875
  };
2846
2876
  function capitalizeFirstLetter(str) {
2847
2877
  return str.charAt(0).toUpperCase() + str.slice(1);
2848
2878
  }
2849
- async function dbConnect(context, connectionString, name, dbProfile) {
2850
- const defs2 = {
2851
- testDbConnection: "boolean default true",
2852
- name: "string",
2853
- poolMin: "number default 2",
2854
- poolMax: "number default 10",
2855
- acquireConnectionTimeout: "number default 10000",
2856
- sslRejectUnauthorized: "boolean default false"
2857
- };
2858
- const paramsConfig = context.params.getAllForModule(defs2);
2859
- const config2 = {
2860
- connectionString,
2861
- name: paramsConfig.name || name || "default",
2862
- testConnection: paramsConfig.testDbConnection,
2863
- profile: dbProfile ?? false,
2864
- pool: {
2865
- min: paramsConfig.poolMin,
2866
- max: paramsConfig.poolMax
2867
- },
2868
- acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
2869
- ssl: {
2870
- rejectUnauthorized: paramsConfig.sslRejectUnauthorized
2871
- },
2872
- logger: context.logger
2873
- };
2879
+ async function dbConnect(context, config2) {
2874
2880
  try {
2875
2881
  const db = new Db(config2);
2876
2882
  context.registerCleanup(async () => {
2877
2883
  await db.disconnect();
2878
- context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
2884
+ context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
2879
2885
  });
2880
2886
  await db.connect();
2881
- context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
2887
+ context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
2882
2888
  return db;
2883
2889
  } catch (error) {
2884
2890
  if (error instanceof ParamError) {
@@ -2888,48 +2894,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
2888
2894
  throw new ParamError(`[Db] connect error: ${errorMsg}`);
2889
2895
  }
2890
2896
  }
2891
- async function dbFindAndConnect(context, dbNameOrConnectionString) {
2892
- let dbName;
2893
- let dbConnectionString;
2894
- let dbProfile;
2895
- if (dbNameOrConnectionString) {
2896
- if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
2897
- dbName = void 0;
2898
- dbConnectionString = dbNameOrConnectionString;
2899
- } else {
2900
- dbName = dbNameOrConnectionString;
2901
- }
2902
- } else {
2903
- const defs2 = {
2904
- dbName: "string",
2905
- dbConnectionString: "string",
2906
- dbProfile: "boolean default false"
2907
- };
2908
- const paramsConfig = context.params.getAllForModule(defs2);
2909
- dbName = paramsConfig.dbName;
2910
- dbConnectionString = paramsConfig.dbConnectionString;
2911
- dbProfile = paramsConfig.dbProfile;
2912
- }
2913
- if (!dbName && !dbConnectionString) {
2914
- throw new ParamError("Db: either dbName or dbConnectionString must be specified");
2915
- }
2916
- if (dbName) {
2917
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2918
- dbConnectionString = await context.params.get(paramName, "string");
2919
- if (!dbConnectionString) {
2920
- throw new ParamError(
2921
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2922
- );
2923
- }
2924
- }
2925
- const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
2926
- return db;
2927
- }
2928
- async function dbInit(context, dbNameOrConnectionString) {
2929
- return await dbFindAndConnect(context, dbNameOrConnectionString);
2930
- }
2931
2897
 
2932
- // src/utils/date-utils.ts
2898
+ // src/tasks/index.js
2899
+ import os3 from "os";
2900
+
2901
+ // src/utils/date-utils.js
2933
2902
  function isTimestampFolder(folderName) {
2934
2903
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
2935
2904
  if (!isoRegex.test(folderName)) {
@@ -2939,7 +2908,7 @@ function isTimestampFolder(folderName) {
2939
2908
  return !isNaN(date.getTime()) && date.getTime() > 0;
2940
2909
  }
2941
2910
 
2942
- // src/utils/fs-utils.ts
2911
+ // src/utils/fs-utils.js
2943
2912
  import fs from "fs";
2944
2913
  import path from "path";
2945
2914
  async function ensurePath(...pathParts) {
@@ -2963,7 +2932,7 @@ function getFileExtension(dataType) {
2963
2932
  }
2964
2933
  }
2965
2934
 
2966
- // src/utils/os-utils.ts
2935
+ // src/utils/os-utils.js
2967
2936
  import fs2 from "fs";
2968
2937
  import path2 from "path";
2969
2938
  import { execSync } from "child_process";
@@ -2992,7 +2961,7 @@ function getFreeDiskSpace(targetPath) {
2992
2961
  }
2993
2962
  }
2994
2963
 
2995
- // src/utils/format-utils.ts
2964
+ // src/utils/format-utils.js
2996
2965
  function bytesToHumanReadable(bytes) {
2997
2966
  if (bytes === 0) return "0 B";
2998
2967
  const k = 1024;
@@ -3001,7 +2970,7 @@ function bytesToHumanReadable(bytes) {
3001
2970
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
3002
2971
  }
3003
2972
 
3004
- // src/utils/core-utils.ts
2973
+ // src/utils/core-utils.js
3005
2974
  function sleepMs(ms) {
3006
2975
  return new Promise((resolve2) => setTimeout(resolve2, ms));
3007
2976
  }
@@ -3010,8 +2979,82 @@ function toJsonColumn(value) {
3010
2979
  return JSON.stringify(value);
3011
2980
  }
3012
2981
 
3013
- // src/tasks/taskUtils.ts
2982
+ // src/tasks/servicesRegistry.js
2983
+ import os from "os";
2984
+
2985
+ // src/tasks/taskUtils.js
3014
2986
  import { randomUUID } from "crypto";
2987
+
2988
+ // src/tasks/time-matcher.js
2989
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
2990
+ function resolveAsterisks(field, range) {
2991
+ return field.includes("*") ? field.replace("*", range) : field;
2992
+ }
2993
+ function resolveRanges(field) {
2994
+ const regex = /(\d+)-(\d+)/;
2995
+ let current = field;
2996
+ while (true) {
2997
+ const match = regex.exec(current);
2998
+ if (!match) break;
2999
+ const raw = match[0];
3000
+ let first = Number(match[1]);
3001
+ let last = Number(match[2]);
3002
+ if (last < first) {
3003
+ [first, last] = [last, first];
3004
+ }
3005
+ const values = [];
3006
+ for (let i = first; i <= last; i += 1) {
3007
+ values.push(i);
3008
+ }
3009
+ current = current.replace(raw, values.join(","));
3010
+ }
3011
+ return current;
3012
+ }
3013
+ function resolveSteps(field) {
3014
+ const match = /^(.+)\/(\d+)$/.exec(field);
3015
+ if (!match) return field;
3016
+ const base = match[1];
3017
+ const step = Number(match[2]);
3018
+ if (!Number.isFinite(step) || step <= 0) return field;
3019
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
3020
+ }
3021
+ function convertPattern(pattern) {
3022
+ const parts = pattern.trim().split(/\s+/);
3023
+ if (parts.length !== 6) {
3024
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
3025
+ }
3026
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
3027
+ }
3028
+ function fieldMatches(field, value) {
3029
+ const allowed = field.split(",").map((v) => Number(v));
3030
+ return allowed.includes(value);
3031
+ }
3032
+ function matchesParsedPattern(parsed, date) {
3033
+ 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());
3034
+ }
3035
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
3036
+ const parsed = convertPattern(pattern);
3037
+ return matchesParsedPattern(parsed, date);
3038
+ }
3039
+ var MS_PER_SECOND = 1e3;
3040
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
3041
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
3042
+ const parsed = convertPattern(pattern);
3043
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
3044
+ const end = t + maxSearchMs;
3045
+ while (t <= end) {
3046
+ const date = new Date(t);
3047
+ if (matchesParsedPattern(parsed, date)) {
3048
+ return date;
3049
+ }
3050
+ t += MS_PER_SECOND;
3051
+ }
3052
+ throw new Error(
3053
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
3054
+ );
3055
+ }
3056
+
3057
+ // src/tasks/taskUtils.js
3015
3058
  function getDb(context) {
3016
3059
  const db = context.db;
3017
3060
  if (!db) {
@@ -3019,89 +3062,84 @@ function getDb(context) {
3019
3062
  }
3020
3063
  return db;
3021
3064
  }
3022
- function queueToTableNames(queue) {
3023
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
3024
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
3025
- }
3065
+ function queueToTableNames(queueName) {
3066
+ return {
3067
+ tasksTable: queueName,
3068
+ historyTable: `${queueName}_history`,
3069
+ registryTable: `${queueName}_services_registry`
3070
+ };
3071
+ }
3072
+ function defineTasksTable(t, db, tableNameForIndex) {
3073
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3074
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3075
+ t.timestamp("started_at");
3076
+ t.timestamp("completed_at");
3077
+ t.integer("priority").notNullable().defaultTo(50);
3078
+ t.text("schedule");
3079
+ t.timestamp("next_run_at").defaultTo(null);
3080
+ t.timestamp("past_due").defaultTo(null);
3081
+ t.text("name").notNullable();
3082
+ t.text("opid");
3083
+ t.json("params");
3084
+ t.text("service_group");
3085
+ t.integer("instance_number");
3086
+ t.text("service_name");
3087
+ t.text("server_name");
3088
+ t.text("status").notNullable().defaultTo("idle");
3089
+ t.timestamp("status_changed_at").defaultTo(null);
3090
+ t.text("progress");
3091
+ t.boolean("success");
3092
+ t.json("results");
3093
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
3094
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
3095
+ }
3096
+ function taskHistoryInsertFromQueueRow(row, overrides) {
3097
+ const { id, ...snapshot } = row;
3098
+ void id;
3026
3099
  return {
3027
- tasksTable: queue,
3028
- historyTable: `${queue}_history`
3100
+ ...snapshot,
3101
+ ...overrides
3029
3102
  };
3030
3103
  }
3031
3104
  async function ensureTaskTables(context, options = {}) {
3032
- const queue = options.queue ?? "tasks";
3105
+ const queueName = options.queueName ?? "tasks";
3033
3106
  const recreate = options.recreate ?? false;
3034
3107
  const db = getDb(context);
3035
- const { tasksTable, historyTable } = queueToTableNames(queue);
3108
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
3036
3109
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
3037
3110
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
3111
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
3038
3112
  if (recreate) {
3039
3113
  await db.schema.dropTableIfExists(historyTable);
3040
3114
  await db.schema.dropTableIfExists(tasksTable);
3115
+ await db.schema.dropTableIfExists(registryTable);
3041
3116
  }
3042
3117
  if (needsTasks) {
3043
3118
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
3044
3119
  await db.schema.createTable(tasksTable, (t) => {
3045
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3046
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3047
- t.timestamp("started_at");
3048
- t.timestamp("completed_at");
3049
- t.integer("priority").notNullable().defaultTo(0);
3050
- t.text("schedule");
3051
- t.timestamp("past_due").defaultTo(null);
3052
- t.text("target").notNullable();
3053
- t.text("task").notNullable();
3054
- t.json("params");
3055
- t.text("opid");
3056
- t.timestamp("paused_at").defaultTo(null);
3057
- t.text("progress");
3058
- t.boolean("success");
3059
- t.json("results");
3060
- });
3061
- await db.schema.alterTable(tasksTable, (t) => {
3062
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
3063
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
3064
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
3065
- });
3066
- }
3067
- const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
3068
- if (!tasksHasOpid) {
3069
- await db.schema.alterTable(tasksTable, (t) => {
3070
- t.text("opid");
3071
- });
3072
- }
3073
- const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
3074
- if (!tasksHasPausedAt) {
3075
- await db.schema.alterTable(tasksTable, (t) => {
3076
- t.timestamp("paused_at").defaultTo(null);
3120
+ defineTasksTable(t, db, tasksTable);
3077
3121
  });
3078
3122
  }
3079
3123
  if (needsHistory) {
3080
3124
  await db.schema.createTable(historyTable, (t) => {
3081
- t.uuid("id").notNullable();
3082
- t.timestamp("created_at").notNullable();
3083
- t.timestamp("started_at");
3084
- t.timestamp("completed_at");
3085
- t.integer("priority").notNullable().defaultTo(0);
3086
- t.text("schedule");
3087
- t.timestamp("past_due").defaultTo(null);
3088
- t.text("target").notNullable();
3089
- t.text("task").notNullable();
3090
- t.json("params");
3091
- t.text("opid");
3092
- t.text("progress");
3093
- t.boolean("success");
3094
- t.json("results");
3095
- });
3096
- await db.schema.alterTable(historyTable, (t) => {
3097
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
3098
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
3125
+ defineTasksTable(t, db, historyTable);
3099
3126
  });
3100
3127
  }
3101
- const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
3102
- if (!historyHasOpid) {
3103
- await db.schema.alterTable(historyTable, (t) => {
3104
- t.text("opid");
3128
+ if (needsRegistry) {
3129
+ await db.schema.createTable(registryTable, (t) => {
3130
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3131
+ t.text("queue_name").notNullable();
3132
+ t.text("service_group").notNullable();
3133
+ t.integer("instance_number").notNullable().defaultTo(1);
3134
+ t.text("service_name").notNullable();
3135
+ t.text("server_name").notNullable();
3136
+ t.integer("pid");
3137
+ t.json("metadata");
3138
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3139
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
3140
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
3141
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
3142
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
3105
3143
  });
3106
3144
  }
3107
3145
  }
@@ -3112,11 +3150,226 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
3112
3150
  });
3113
3151
  }
3114
3152
 
3115
- // src/filedatabase/index.ts
3153
+ // src/tasks/servicesRegistry.js
3154
+ function getDb2(context) {
3155
+ const db = context.db;
3156
+ if (!db) {
3157
+ throw new Error("Services registry requires context.db");
3158
+ }
3159
+ return db;
3160
+ }
3161
+ var DEFAULT_GROUP_MAX_INSTANCES = {
3162
+ intake: 1,
3163
+ harvest: 1,
3164
+ harvester: 0,
3165
+ loader: 0,
3166
+ photos: 0,
3167
+ photosprocessor: 0,
3168
+ ingest: 0
3169
+ };
3170
+ function sanitizeNamePart(raw) {
3171
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3172
+ return s.slice(0, 80) || "runner";
3173
+ }
3174
+ function resolveMaxInstances(serviceGroup, override) {
3175
+ if (override !== void 0 && Number.isFinite(override)) {
3176
+ return Math.max(0, Math.floor(Number(override)));
3177
+ }
3178
+ const g = serviceGroup.trim().toLowerCase();
3179
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
3180
+ }
3181
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
3182
+ const cutoff = new Date(Date.now() - staleMs);
3183
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
3184
+ if (excludeRowId) {
3185
+ q = q.whereNot("id", excludeRowId);
3186
+ }
3187
+ const row = await q.count("id as count").first();
3188
+ return Number(row?.count ?? 0);
3189
+ }
3190
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
3191
+ const cutoff = new Date(Date.now() - staleMs);
3192
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
3193
+ const set = /* @__PURE__ */ new Set();
3194
+ for (const r of rows) {
3195
+ const n = Number(r.instance_number);
3196
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
3197
+ }
3198
+ return set;
3199
+ }
3200
+ function isUniqueViolation(error) {
3201
+ const code = error?.code ?? error?.errno;
3202
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
3203
+ }
3204
+ function buildMetadata(options) {
3205
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
3206
+ if (options.target) {
3207
+ base.runnerTarget = options.target;
3208
+ }
3209
+ return toJsonColumn(Object.keys(base).length ? base : null);
3210
+ }
3211
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
3212
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
3213
+ const e = Math.max(1, Math.floor(Number(explicit)));
3214
+ if (occupied.has(e)) {
3215
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
3216
+ }
3217
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
3218
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
3219
+ }
3220
+ return e;
3221
+ }
3222
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
3223
+ for (let n = 1; n <= cap; n++) {
3224
+ if (!occupied.has(n)) return n;
3225
+ }
3226
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
3227
+ }
3228
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
3229
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
3230
+ }
3231
+ async function registerInServicesRegistry(context, options) {
3232
+ const db = getDb2(context);
3233
+ const registryTable = queueToTableNames(options.queueName).registryTable;
3234
+ const serviceGroup = options.serviceGroup.trim();
3235
+ if (!serviceGroup) {
3236
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
3237
+ }
3238
+ const serverName = os.hostname();
3239
+ const pid = typeof process.pid === "number" ? process.pid : null;
3240
+ const meta = buildMetadata(options);
3241
+ const groupBase = sanitizeNamePart(serviceGroup);
3242
+ const hostBase = sanitizeNamePart(serverName);
3243
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
3244
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
3245
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
3246
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
3247
+ if (options.enforceMaxInstances) {
3248
+ throw new Error(msg);
3249
+ }
3250
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
3251
+ }
3252
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
3253
+ const cutoff = new Date(Date.now() - options.staleMs);
3254
+ const MAX_ATTEMPTS = 8;
3255
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
3256
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
3257
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
3258
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
3259
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
3260
+ if (existing) {
3261
+ const lastSeen = new Date(existing.last_seen_at);
3262
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
3263
+ if (isAlive) {
3264
+ if (options.serviceName?.trim()) {
3265
+ throw new Error(
3266
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
3267
+ );
3268
+ }
3269
+ context.logger.warn?.(
3270
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
3271
+ );
3272
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
3273
+ throw new Error(
3274
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
3275
+ );
3276
+ }
3277
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
3278
+ continue;
3279
+ }
3280
+ await db(registryTable).where({ id: existing.id }).update({
3281
+ server_name: serverName,
3282
+ pid,
3283
+ metadata: meta,
3284
+ service_group: serviceGroup,
3285
+ instance_number: instanceNumber,
3286
+ last_seen_at: db.fn.now()
3287
+ });
3288
+ const reg = {
3289
+ serviceName: serviceNameRaw,
3290
+ serviceGroup,
3291
+ queueName: options.queueName,
3292
+ target: options.target,
3293
+ rowId: String(existing.id),
3294
+ registryTable,
3295
+ instanceNumber
3296
+ };
3297
+ context.servicesRegistry = reg;
3298
+ context.runnerHeartbeat = reg;
3299
+ context.logger.info?.(
3300
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
3301
+ );
3302
+ return reg;
3303
+ }
3304
+ try {
3305
+ const rows = await db(registryTable).insert({
3306
+ queue_name: options.queueName,
3307
+ service_group: serviceGroup,
3308
+ instance_number: instanceNumber,
3309
+ service_name: serviceNameRaw,
3310
+ server_name: serverName,
3311
+ pid,
3312
+ metadata: meta,
3313
+ last_seen_at: db.fn.now(),
3314
+ created_at: db.fn.now()
3315
+ }).returning(["id", "service_name"]);
3316
+ const row = Array.isArray(rows) ? rows[0] : rows;
3317
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
3318
+ if (!rowId) {
3319
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
3320
+ rowId = again?.id != null ? String(again.id) : "";
3321
+ }
3322
+ if (!rowId) continue;
3323
+ const regNew = {
3324
+ serviceName: String(row?.service_name ?? serviceNameRaw),
3325
+ serviceGroup,
3326
+ queueName: options.queueName,
3327
+ target: options.target,
3328
+ rowId,
3329
+ registryTable,
3330
+ instanceNumber
3331
+ };
3332
+ context.servicesRegistry = regNew;
3333
+ context.runnerHeartbeat = regNew;
3334
+ context.logger.info?.(
3335
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
3336
+ );
3337
+ return regNew;
3338
+ } catch (error) {
3339
+ if (!isUniqueViolation(error)) {
3340
+ throw error;
3341
+ }
3342
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
3343
+ }
3344
+ }
3345
+ throw new Error(
3346
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
3347
+ );
3348
+ }
3349
+ async function touchServicesRegistry(context, registration) {
3350
+ const db = getDb2(context);
3351
+ const serverName = os.hostname();
3352
+ const pid = typeof process.pid === "number" ? process.pid : null;
3353
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
3354
+ last_seen_at: db.fn.now(),
3355
+ server_name: serverName,
3356
+ pid
3357
+ });
3358
+ }
3359
+ async function unregisterServicesRegistry(context, registration) {
3360
+ const db = getDb2(context);
3361
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
3362
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
3363
+ }
3364
+
3365
+ // src/tasks/taskLogs.js
3366
+ import path4 from "path";
3367
+
3368
+ // src/filedatabase/index.js
3116
3369
  import fs3 from "fs";
3117
3370
  import path3 from "path";
3118
3371
 
3119
- // src/filedatabase/serializers.ts
3372
+ // src/filedatabase/serializers.js
3120
3373
  function detectDataType(data) {
3121
3374
  if (Array.isArray(data)) {
3122
3375
  return "json-array";
@@ -3148,7 +3401,7 @@ function deserializeData(rawData, dataType) {
3148
3401
  }
3149
3402
  }
3150
3403
 
3151
- // src/filedatabase/index.ts
3404
+ // src/filedatabase/index.js
3152
3405
  var FileDatabase = class _FileDatabase {
3153
3406
  basePath;
3154
3407
  namespace;
@@ -3234,7 +3487,7 @@ var FileDatabase = class _FileDatabase {
3234
3487
  if (errors.length) {
3235
3488
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
3236
3489
  }
3237
- let parts = [this.basePath, this.namespace];
3490
+ const parts = [this.basePath, this.namespace];
3238
3491
  if (this.tableName) {
3239
3492
  parts.push(...this.tableName.split("/"));
3240
3493
  }
@@ -3727,14 +3980,17 @@ var FileDatabase = class _FileDatabase {
3727
3980
  * Prepare the instance for read or write operations
3728
3981
  * This discovers state and sets up internal members based on mode and current data
3729
3982
  */
3730
- async prepare({ write, read, version }) {
3983
+ async prepare(options) {
3984
+ const { write, read, version, deferInitialVersion } = options;
3731
3985
  if (write) {
3732
3986
  if (this.versioned) {
3733
3987
  if (this.currentVersion === null) {
3734
- await this.makeNewVersion();
3735
- this.metadata = this.getDefaultMetadata();
3736
- this.metadata.version = this.currentVersion;
3737
- this.makeNewFile();
3988
+ if (!deferInitialVersion) {
3989
+ await this.makeNewVersion();
3990
+ this.metadata = this.getDefaultMetadata();
3991
+ this.metadata.version = this.currentVersion;
3992
+ this.makeNewFile();
3993
+ }
3738
3994
  } else {
3739
3995
  if (!this.metadata.files.length) {
3740
3996
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -3844,7 +4100,7 @@ var FileDatabase = class _FileDatabase {
3844
4100
  if (options.forceNewVersion && !this.versioned) {
3845
4101
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
3846
4102
  }
3847
- await this.prepare({ write: true });
4103
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
3848
4104
  const incomingDataType = detectDataType(data);
3849
4105
  this.metadata.dataType = incomingDataType;
3850
4106
  if (options.forceNewVersion) {
@@ -4138,7 +4394,7 @@ var FileDatabase = class _FileDatabase {
4138
4394
  }
4139
4395
  };
4140
4396
 
4141
- // src/tasks/taskLogs.ts
4397
+ // src/tasks/taskLogs.js
4142
4398
  function getLogsState(context) {
4143
4399
  const holder = context;
4144
4400
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -4191,6 +4447,89 @@ function getLogsState(context) {
4191
4447
  holder.__tasksLogsState = state;
4192
4448
  return state;
4193
4449
  }
4450
+ function ipcLogTargetKey(target) {
4451
+ const bp = target.basePath ?? "";
4452
+ const ns = target.namespace ?? "";
4453
+ return `${bp}::${ns}::${target.tableName}`;
4454
+ }
4455
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
4456
+ const seg = (s) => {
4457
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
4458
+ return t.length ? t : "x";
4459
+ };
4460
+ return `${seg(source)}/${seg(resource)}`;
4461
+ }
4462
+ async function readTaskIpcLogsSnapshot(context, options) {
4463
+ const holder = context;
4464
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
4465
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
4466
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
4467
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
4468
+ const fd = new FileDatabase({
4469
+ basePath,
4470
+ namespace,
4471
+ tableName,
4472
+ versioned: true,
4473
+ useMetadata: true,
4474
+ maxVersions: 30,
4475
+ pageSize: 2e3,
4476
+ logger: holder.logger
4477
+ });
4478
+ const versions = await fd.getVersions();
4479
+ if (versions.length === 0) {
4480
+ return { records: [], latestTs: null };
4481
+ }
4482
+ const latest = versions[versions.length - 1];
4483
+ const raw = await fd.read({ version: latest });
4484
+ const arr = Array.isArray(raw) ? raw : [];
4485
+ let filtered = arr;
4486
+ if (options.afterTs && String(options.afterTs).trim()) {
4487
+ const cut = String(options.afterTs).trim();
4488
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
4489
+ }
4490
+ let latestTs = null;
4491
+ for (const r of filtered) {
4492
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
4493
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
4494
+ }
4495
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
4496
+ const maxReturn = incremental ? 1e4 : tail;
4497
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
4498
+ return { records: sliced, latestTs };
4499
+ }
4500
+ function getLogsStateForTarget(context, target) {
4501
+ const holder = context;
4502
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4503
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4504
+ if (!enabled) return null;
4505
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
4506
+ const map = holder.__tasksLogsTargetStates;
4507
+ const key = ipcLogTargetKey(target);
4508
+ if (map.has(key)) return map.get(key);
4509
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4510
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4511
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4512
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4513
+ const db = new FileDatabase({
4514
+ basePath,
4515
+ namespace,
4516
+ tableName: target.tableName,
4517
+ versioned: true,
4518
+ useMetadata: true,
4519
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4520
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4521
+ logger: holder.logger
4522
+ });
4523
+ const state = {
4524
+ db,
4525
+ errorDb: null,
4526
+ queue: Promise.resolve(),
4527
+ initialized: false,
4528
+ errorInitialized: false
4529
+ };
4530
+ map.set(key, state);
4531
+ return state;
4532
+ }
4194
4533
  function isErrorPayload(payload) {
4195
4534
  if (!payload) return false;
4196
4535
  if (typeof payload === "object") {
@@ -4210,14 +4549,26 @@ function buildLogRecord(task, payload) {
4210
4549
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4211
4550
  opid: task.opid ?? null,
4212
4551
  taskId: task.id,
4213
- taskName: task.task,
4214
- target: task.target,
4552
+ taskName: task.name,
4553
+ target: task.service_group,
4215
4554
  source: typeof params.source === "string" ? params.source : null,
4216
4555
  resource: typeof params.resource === "string" ? params.resource : null,
4217
4556
  payload
4218
4557
  };
4219
4558
  }
4220
- function appendTaskIpcLog(context, task, payload) {
4559
+ function appendTaskIpcLog(context, task, payload, target) {
4560
+ if (target) {
4561
+ const state2 = getLogsStateForTarget(context, target);
4562
+ if (!state2?.db) return;
4563
+ const record2 = buildLogRecord(task, payload);
4564
+ state2.queue = state2.queue.then(async () => {
4565
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
4566
+ state2.initialized = true;
4567
+ }).catch((error) => {
4568
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
4569
+ });
4570
+ return;
4571
+ }
4221
4572
  const state = getLogsState(context);
4222
4573
  if (!state.db && !state.errorDb) return;
4223
4574
  const record = buildLogRecord(task, payload);
@@ -4235,83 +4586,280 @@ function appendTaskIpcLog(context, task, payload) {
4235
4586
  });
4236
4587
  }
4237
4588
 
4238
- // src/tasks/time-matcher.ts
4239
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4240
- function resolveAsterisks(field, range) {
4241
- return field.includes("*") ? field.replace("*", range) : field;
4242
- }
4243
- function resolveRanges(field) {
4244
- const regex = /(\d+)-(\d+)/;
4245
- let current = field;
4246
- while (true) {
4247
- const match = regex.exec(current);
4248
- if (!match) break;
4249
- const raw = match[0];
4250
- let first = Number(match[1]);
4251
- let last = Number(match[2]);
4252
- if (last < first) {
4253
- [first, last] = [last, first];
4254
- }
4255
- const values = [];
4256
- for (let i = first; i <= last; i += 1) {
4257
- values.push(i);
4258
- }
4259
- current = current.replace(raw, values.join(","));
4260
- }
4261
- return current;
4262
- }
4263
- function resolveSteps(field) {
4264
- const match = /^(.+)\/(\d+)$/.exec(field);
4265
- if (!match) return field;
4266
- const base = match[1];
4267
- const step = Number(match[2]);
4268
- if (!Number.isFinite(step) || step <= 0) return field;
4269
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4270
- }
4271
- function convertPattern(pattern) {
4272
- const parts = pattern.trim().split(/\s+/);
4273
- if (parts.length !== 6) {
4274
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4275
- }
4276
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4277
- }
4278
- function fieldMatches(field, value) {
4279
- const allowed = field.split(",").map((v) => Number(v));
4280
- return allowed.includes(value);
4281
- }
4282
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4283
- const parsed = convertPattern(pattern);
4284
- 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());
4285
- }
4286
-
4287
- // src/tasks/TaskMaster.ts
4288
- var TaskMaster = class {
4289
- context;
4290
- task;
4589
+ // src/tasks/AbstractTask.js
4590
+ var AbstractTask = class _AbstractTask {
4591
+ /**
4592
+ * Whether `send-task` should wait for completion (and print a result
4593
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
4594
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
4595
+ *
4596
+ * @type {boolean}
4597
+ */
4598
+ static defaultWaitForResult = false;
4599
+ /**
4600
+ * @param {object} context Runner context (db, logger, params, emitter...).
4601
+ * @param {object} task Task row as claimed from the queue.
4602
+ */
4291
4603
  constructor(context, task) {
4292
4604
  this.context = context;
4293
4605
  this.task = task;
4294
4606
  }
4607
+ /**
4608
+ * Return a short reason string when the task should be deferred (e.g. "locked
4609
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
4610
+ *
4611
+ * @returns {string | false | Promise<string | false>}
4612
+ */
4295
4613
  cantRunReason() {
4296
4614
  return false;
4297
4615
  }
4616
+ /**
4617
+ * Called by the runner when a stop has been requested. Subclasses running
4618
+ * long loops should flip a flag here and check it between iterations.
4619
+ *
4620
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
4621
+ */
4298
4622
  requestStop(_allowanceMs) {
4299
4623
  }
4624
+ /**
4625
+ * Perform the task. Must be implemented by subclasses.
4626
+ *
4627
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
4628
+ * Updates the DB `progress` column. Accepts any serializable value;
4629
+ * strings are stored verbatim, objects are JSON-stringified.
4630
+ * @returns {Promise<{ success: boolean, results: unknown }>}
4631
+ */
4632
+ async run(_reportProgress) {
4633
+ throw new Error("AbstractTask.run must be implemented by subclass");
4634
+ }
4635
+ /**
4636
+ * Resolve a complete row payload for this task — envelope fields (queue,
4637
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
4638
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
4639
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
4640
+ *
4641
+ * const payload = await TaskClass.resolveParams(context, { name });
4642
+ * await enqueueTask(context, payload);
4643
+ *
4644
+ * Validation failures throw {@link ParamError} so the script aborts before
4645
+ * a malformed row hits the DB.
4646
+ *
4647
+ * @param {object} context
4648
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
4649
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
4650
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
4651
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
4652
+ * @returns {Promise<object>}
4653
+ */
4654
+ static async resolveParams(context, overrides = {}) {
4655
+ const main = _AbstractTask._resolveMainFields(context, overrides);
4656
+ const params = await this.resolveCustomParams(context, overrides);
4657
+ return { ...main, params };
4658
+ }
4659
+ /**
4660
+ * Resolve the inner JSON blob stored in the `params` column. Default
4661
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
4662
+ * overlaid with `overrides.params` when supplied; returns `null` when
4663
+ * neither is provided.
4664
+ *
4665
+ * Subclasses with typed fields should override and call
4666
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
4667
+ * resolution, then validate and throw {@link ParamError} on bad input.
4668
+ *
4669
+ * @param {object} context
4670
+ * @param {Record<string, unknown>} [overrides]
4671
+ * @returns {Promise<object|null>}
4672
+ */
4673
+ static async resolveCustomParams(context, overrides = {}) {
4674
+ return _AbstractTask._defaultParamsBlob(context, overrides);
4675
+ }
4676
+ /**
4677
+ * Read main task envelope fields from `context.params` (CLI/env), with
4678
+ * any matching key on `overrides` taking precedence. Internal; called by
4679
+ * {@link AbstractTask.resolveParams}.
4680
+ *
4681
+ * @param {object} context
4682
+ * @param {Record<string, unknown>} [overrides]
4683
+ * @returns {object}
4684
+ */
4685
+ static _resolveMainFields(context, overrides = {}) {
4686
+ const defs2 = {
4687
+ queueName: "string default tasks",
4688
+ priority: "number default 50",
4689
+ serviceGroup: "string",
4690
+ serviceName: "string",
4691
+ instanceNumber: "number",
4692
+ serverName: "string",
4693
+ opid: "string",
4694
+ schedule: "string"
4695
+ };
4696
+ const cli = context.params.getAllForModule("task-envelope", defs2);
4697
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
4698
+ if (!name) {
4699
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
4700
+ }
4701
+ let instanceNumber;
4702
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
4703
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
4704
+ const n = Number(rawInstance);
4705
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
4706
+ throw new ParamError("--instanceNumber must be a positive integer when set");
4707
+ }
4708
+ instanceNumber = n;
4709
+ } else {
4710
+ instanceNumber = null;
4711
+ }
4712
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
4713
+ const priority = Number(priorityRaw);
4714
+ if (!Number.isFinite(priority)) {
4715
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
4716
+ }
4717
+ return {
4718
+ name,
4719
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
4720
+ priority,
4721
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
4722
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
4723
+ instanceNumber,
4724
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
4725
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
4726
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
4727
+ nextRunAt: overrides.nextRunAt ?? null
4728
+ };
4729
+ }
4730
+ /**
4731
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
4732
+ * object), then overlays `overrides.params` on top. Returns `null` when
4733
+ * neither is provided.
4734
+ *
4735
+ * @param {object} context
4736
+ * @param {Record<string, unknown>} [overrides]
4737
+ * @returns {object|null}
4738
+ */
4739
+ static _defaultParamsBlob(context, overrides = {}) {
4740
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
4741
+ const fromJson = parseParamsJson(cli.paramsJson);
4742
+ const fromOverride = pickParamsObject(overrides);
4743
+ if (!fromJson && !fromOverride) return null;
4744
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
4745
+ }
4746
+ /**
4747
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
4748
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
4749
+ * merges them with explicit `overrides.params` in increasing priority:
4750
+ *
4751
+ * typed CLI flags → --paramsJson → overrides.params
4752
+ *
4753
+ * Undefined values are dropped so defaults declared in `defs` aren't
4754
+ * overwritten by missing-flag noise. Returns the merged object; the
4755
+ * caller is responsible for validation and throwing `ParamError`.
4756
+ *
4757
+ * @param {object} context
4758
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
4759
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
4760
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
4761
+ * @returns {Record<string, unknown>}
4762
+ */
4763
+ static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
4764
+ const fullDefs = { ...defs2, paramsJson: "string" };
4765
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
4766
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
4767
+ const fromCli = {};
4768
+ for (const [k, v] of Object.entries(cliRaw)) {
4769
+ if (k === "paramsJson") continue;
4770
+ if (v !== void 0 && v !== null) fromCli[k] = v;
4771
+ }
4772
+ const fromOverride = pickParamsObject(overrides) ?? {};
4773
+ return { ...fromCli, ...fromJson, ...fromOverride };
4774
+ }
4300
4775
  };
4776
+ function emptyToUndef(s) {
4777
+ if (s === void 0 || s === null) return void 0;
4778
+ if (typeof s !== "string") return s;
4779
+ const t = s.trim();
4780
+ return t.length ? t : void 0;
4781
+ }
4782
+ function parseParamsJson(raw) {
4783
+ if (raw == null) return null;
4784
+ const t = String(raw).trim();
4785
+ if (!t) return null;
4786
+ let parsed;
4787
+ try {
4788
+ parsed = JSON.parse(t);
4789
+ } catch (e) {
4790
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
4791
+ }
4792
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4793
+ throw new ParamError("--paramsJson must be a JSON object");
4794
+ }
4795
+ return parsed;
4796
+ }
4797
+ function pickParamsObject(overrides) {
4798
+ const p = overrides?.params;
4799
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
4800
+ return void 0;
4801
+ }
4301
4802
 
4302
- // src/tasks/coreTasks/TaskPing.ts
4303
- var TaskPing = class extends TaskMaster {
4803
+ // src/tasks/coreTasks/TaskPing.js
4804
+ var TaskPing = class extends AbstractTask {
4805
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
4806
+ static defaultWaitForResult = true;
4807
+ /** Ping takes no params. */
4808
+ static async resolveCustomParams() {
4809
+ return null;
4810
+ }
4811
+ /**
4812
+ * @returns {Promise<{ success: true, results: "pong" }>}
4813
+ */
4304
4814
  async run() {
4305
4815
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4306
4816
  return { success: true, results: "pong" };
4307
4817
  }
4308
4818
  };
4309
4819
 
4310
- // src/tasks/coreTasks/TaskSampleProcess.ts
4311
- var TaskSampleProcess = class extends TaskMaster {
4312
- stopRequested = false;
4313
- stopAllowanceMs = 0;
4314
- stopDecisionLogged = false;
4820
+ // src/tasks/coreTasks/TaskSampleProcess.js
4821
+ var TaskSampleProcess = class extends AbstractTask {
4822
+ /**
4823
+ * @param {object} context
4824
+ * @param {Record<string, unknown>} [overrides]
4825
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
4826
+ */
4827
+ static async resolveCustomParams(context, overrides = {}) {
4828
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
4829
+ total: "number default 10",
4830
+ delay: "number default 1000",
4831
+ name: "string"
4832
+ }, overrides);
4833
+ const total = Number(merged.total);
4834
+ const delay = Number(merged.delay);
4835
+ if (!Number.isInteger(total) || total <= 0) {
4836
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
4837
+ }
4838
+ if (!Number.isInteger(delay) || delay < 0) {
4839
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
4840
+ }
4841
+ const out = { total, delay };
4842
+ if (typeof merged.name === "string" && merged.name.trim()) {
4843
+ out.name = merged.name.trim();
4844
+ }
4845
+ return out;
4846
+ }
4847
+ /**
4848
+ * @param {object} context
4849
+ * @param {object} task
4850
+ */
4851
+ constructor(context, task) {
4852
+ super(context, task);
4853
+ this.stopRequested = false;
4854
+ this.stopAllowanceMs = 0;
4855
+ this.stopDecisionLogged = false;
4856
+ }
4857
+ /**
4858
+ * Runner-facing stop signal. Records the allowance window so the main loop
4859
+ * can decide per-iteration whether to finish or abort early.
4860
+ *
4861
+ * @param {number} allowanceMs
4862
+ */
4315
4863
  requestStop(allowanceMs) {
4316
4864
  this.stopRequested = true;
4317
4865
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -4319,6 +4867,14 @@ var TaskSampleProcess = class extends TaskMaster {
4319
4867
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4320
4868
  );
4321
4869
  }
4870
+ /**
4871
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
4872
+ * progress every iteration. Validates params up front; invalid values short-
4873
+ * circuit to a structured failure without starting the loop.
4874
+ *
4875
+ * @param {(progress: object) => Promise<void>} reportProgress
4876
+ * @returns {Promise<{ success: boolean, results: unknown }>}
4877
+ */
4322
4878
  async run(reportProgress) {
4323
4879
  const totalRaw = this.task?.params?.total ?? 10;
4324
4880
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -4400,7 +4956,7 @@ var TaskSampleProcess = class extends TaskMaster {
4400
4956
  }
4401
4957
  };
4402
4958
 
4403
- // src/tasks/coreTasks/TaskShellCommand.ts
4959
+ // src/tasks/coreTasks/TaskShellCommand.js
4404
4960
  import { spawn } from "child_process";
4405
4961
  function runShellCommand(command, cwd) {
4406
4962
  return new Promise((resolve2, reject) => {
@@ -4430,7 +4986,27 @@ function runShellCommand(command, cwd) {
4430
4986
  });
4431
4987
  });
4432
4988
  }
4433
- var TaskShellCommand = class extends TaskMaster {
4989
+ var TaskShellCommand = class extends AbstractTask {
4990
+ /**
4991
+ * @param {object} context
4992
+ * @param {Record<string, unknown>} [overrides]
4993
+ * @returns {Promise<{ command: string, cwd?: string }>}
4994
+ */
4995
+ static async resolveCustomParams(context, overrides = {}) {
4996
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
4997
+ command: "string",
4998
+ cwd: "string"
4999
+ }, overrides);
5000
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
5001
+ if (!command) {
5002
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
5003
+ }
5004
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
5005
+ return cwd ? { command, cwd } : { command };
5006
+ }
5007
+ /**
5008
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5009
+ */
4434
5010
  async run() {
4435
5011
  const params = this.task?.params;
4436
5012
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -4479,8 +5055,8 @@ var TaskShellCommand = class extends TaskMaster {
4479
5055
  }
4480
5056
  };
4481
5057
 
4482
- // src/tasks/coreTasks/TaskSystemInfo.ts
4483
- import os from "os";
5058
+ // src/tasks/coreTasks/TaskSystemInfo.js
5059
+ import os2 from "os";
4484
5060
  import fs4 from "fs/promises";
4485
5061
  function toGb(valueBytes) {
4486
5062
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
@@ -4499,13 +5075,22 @@ async function getDiskStats() {
4499
5075
  free: toGb(free)
4500
5076
  };
4501
5077
  }
4502
- var TaskSystemInfo = class extends TaskMaster {
5078
+ var TaskSystemInfo = class extends AbstractTask {
5079
+ /** Same UX expectation as `ping` — short probe, print the result. */
5080
+ static defaultWaitForResult = true;
5081
+ /** systemInfo takes no params. */
5082
+ static async resolveCustomParams() {
5083
+ return null;
5084
+ }
5085
+ /**
5086
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5087
+ */
4503
5088
  async run() {
4504
5089
  try {
4505
- const totalMemory = os.totalmem();
4506
- const freeMemory = os.freemem();
5090
+ const totalMemory = os2.totalmem();
5091
+ const freeMemory = os2.freemem();
4507
5092
  const usedMemory = totalMemory - freeMemory;
4508
- const cpus = os.cpus();
5093
+ const cpus = os2.cpus();
4509
5094
  const cpuUtilization = cpus.map((cpu) => {
4510
5095
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
4511
5096
  const usage = (total - cpu.times.idle) / total * 100;
@@ -4531,10 +5116,10 @@ var TaskSystemInfo = class extends TaskMaster {
4531
5116
  utilization: cpuUtilization
4532
5117
  },
4533
5118
  runtime: {
4534
- platform: os.platform(),
4535
- arch: os.arch(),
4536
- uptimeSec: os.uptime(),
4537
- hostname: os.hostname()
5119
+ platform: os2.platform(),
5120
+ arch: os2.arch(),
5121
+ uptimeSec: os2.uptime(),
5122
+ hostname: os2.hostname()
4538
5123
  }
4539
5124
  };
4540
5125
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -4551,8 +5136,31 @@ var TaskSystemInfo = class extends TaskMaster {
4551
5136
  }
4552
5137
  };
4553
5138
 
4554
- // src/tasks/coreTasks/TaskSumAB.ts
4555
- var TaskSumAB = class extends TaskMaster {
5139
+ // src/tasks/coreTasks/TaskSumAB.js
5140
+ var TaskSumAB = class extends AbstractTask {
5141
+ /** Short, deterministic — wait by default so callers see the sum. */
5142
+ static defaultWaitForResult = true;
5143
+ /**
5144
+ * @param {object} context
5145
+ * @param {Record<string, unknown>} [overrides]
5146
+ * @returns {Promise<{ a: number, b: number }>}
5147
+ */
5148
+ static async resolveCustomParams(context, overrides = {}) {
5149
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
5150
+ a: "number",
5151
+ b: "number"
5152
+ }, overrides);
5153
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
5154
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
5155
+ }
5156
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
5157
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
5158
+ }
5159
+ return { a: merged.a, b: merged.b };
5160
+ }
5161
+ /**
5162
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5163
+ */
4556
5164
  async run() {
4557
5165
  const a = this.task?.params?.a;
4558
5166
  const b = this.task?.params?.b;
@@ -4583,8 +5191,46 @@ var TaskSumAB = class extends TaskMaster {
4583
5191
  }
4584
5192
  };
4585
5193
 
4586
- // src/tasks/coreTasks/TaskStopRunner.ts
4587
- var TaskStopRunner = class extends TaskMaster {
5194
+ // src/tasks/coreTasks/TaskStopRunner.js
5195
+ var TaskStopRunner = class extends AbstractTask {
5196
+ /**
5197
+ * Stop tasks must target a concrete instance — without `serviceName` the
5198
+ * row would race against any worker on the queue. Layered on top of the
5199
+ * envelope built by {@link AbstractTask.resolveParams}.
5200
+ *
5201
+ * @param {object} context
5202
+ * @param {Record<string, unknown>} [overrides]
5203
+ * @returns {Promise<object>}
5204
+ */
5205
+ static async resolveParams(context, overrides = {}) {
5206
+ const main = await super.resolveParams(context, overrides);
5207
+ if (!main.serviceName) {
5208
+ throw new ParamError(
5209
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
5210
+ );
5211
+ }
5212
+ return main;
5213
+ }
5214
+ /**
5215
+ * @param {object} context
5216
+ * @param {Record<string, unknown>} [overrides]
5217
+ * @returns {Promise<{ allowanceMs: number }>}
5218
+ */
5219
+ static async resolveCustomParams(context, overrides = {}) {
5220
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
5221
+ allowanceMs: "number default 5000"
5222
+ }, overrides);
5223
+ const allowanceMs = Number(merged.allowanceMs);
5224
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
5225
+ throw new ParamError(
5226
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
5227
+ );
5228
+ }
5229
+ return { allowanceMs };
5230
+ }
5231
+ /**
5232
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
5233
+ */
4588
5234
  async run() {
4589
5235
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4590
5236
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -4599,45 +5245,203 @@ var TaskStopRunner = class extends TaskMaster {
4599
5245
  }
4600
5246
  };
4601
5247
 
4602
- // src/tasks/TasksRegistry.ts
5248
+ // src/tasks/coreTasks/TaskGetLogs.js
5249
+ var TaskGetLogs = class extends AbstractTask {
5250
+ /**
5251
+ * @param {object} context
5252
+ * @param {Record<string, unknown>} [overrides]
5253
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
5254
+ */
5255
+ static async resolveCustomParams(context, overrides = {}) {
5256
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
5257
+ source: "string",
5258
+ resource: "string",
5259
+ tail: "number default 100",
5260
+ afterTs: "string"
5261
+ }, overrides);
5262
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
5263
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
5264
+ if (!source) throw new ParamError('getLogs: param "source" is required');
5265
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
5266
+ let tail = Number(merged.tail);
5267
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
5268
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
5269
+ const out = { source, resource, tail };
5270
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
5271
+ out.afterTs = merged.afterTs.trim();
5272
+ }
5273
+ return out;
5274
+ }
5275
+ /**
5276
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
5277
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5278
+ */
5279
+ async run(_reportProgress) {
5280
+ const p = this.task.params ?? {};
5281
+ const source = String(p.source ?? "").trim();
5282
+ const resource = String(p.resource ?? "").trim();
5283
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
5284
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
5285
+ if (!source || !resource) {
5286
+ return {
5287
+ success: false,
5288
+ results: { error: 'getLogs requires params "source" and "resource"' }
5289
+ };
5290
+ }
5291
+ try {
5292
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
5293
+ source,
5294
+ resource,
5295
+ tail,
5296
+ afterTs
5297
+ });
5298
+ return {
5299
+ success: true,
5300
+ results: { records, latestTs, source, resource }
5301
+ };
5302
+ } catch (e) {
5303
+ return {
5304
+ success: false,
5305
+ results: { error: e?.message ?? String(e) }
5306
+ };
5307
+ }
5308
+ }
5309
+ };
5310
+
5311
+ // src/tasks/TasksRegistry.js
4603
5312
  var TasksRegistry = class _TasksRegistry {
4604
- map = {};
5313
+ /**
5314
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
5315
+ */
4605
5316
  constructor(initial) {
5317
+ this.map = {};
4606
5318
  if (initial) {
4607
5319
  this.addMany(initial);
4608
5320
  }
4609
5321
  }
5322
+ /**
5323
+ * Build a registry pre-populated with every core task plus legacy aliases.
5324
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
5325
+ *
5326
+ * @returns {TasksRegistry}
5327
+ */
4610
5328
  static withCoreTasks() {
4611
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
5329
+ 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);
4612
5330
  }
5331
+ /**
5332
+ * Register a single task class under a name. Overwrites any previous entry.
5333
+ *
5334
+ * @param {string} taskName
5335
+ * @param {Function} taskClass Subclass of `AbstractTask`.
5336
+ * @returns {this}
5337
+ */
4613
5338
  add(taskName, taskClass) {
4614
5339
  this.map[taskName] = taskClass;
4615
5340
  return this;
4616
5341
  }
5342
+ /**
5343
+ * Bulk-register a name → class map. Later calls override earlier ones.
5344
+ *
5345
+ * @param {Record<string, Function>} entries
5346
+ * @returns {this}
5347
+ */
4617
5348
  addMany(entries) {
4618
5349
  for (const [name, klass] of Object.entries(entries)) {
4619
5350
  this.add(name, klass);
4620
5351
  }
4621
5352
  return this;
4622
5353
  }
5354
+ /**
5355
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
5356
+ * the runner treats that as "some other worker may handle this" and skips.
5357
+ *
5358
+ * @param {string} taskName
5359
+ * @returns {Function | undefined}
5360
+ */
4623
5361
  get(taskName) {
4624
5362
  return this.map[taskName];
4625
5363
  }
5364
+ /**
5365
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
5366
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
5367
+ * paths where an unknown name is a hard CLI/programmer error.
5368
+ *
5369
+ * @param {string} taskName
5370
+ * @returns {Function}
5371
+ */
5372
+ requireClass(taskName) {
5373
+ const TaskClass = taskName ? this.map[taskName] : void 0;
5374
+ if (!TaskClass) {
5375
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
5376
+ throw new ParamError(
5377
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
5378
+ );
5379
+ }
5380
+ return TaskClass;
5381
+ }
5382
+ /**
5383
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
5384
+ * from `overrides` or `context.params`, look up the class, and delegate
5385
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
5386
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
5387
+ *
5388
+ * Validation failures (unknown task, missing required custom params, etc.)
5389
+ * surface as {@link ParamError} so the caller aborts cleanly before any
5390
+ * row is inserted.
5391
+ *
5392
+ * @param {object} context
5393
+ * @param {Record<string, unknown>} [overrides]
5394
+ * @returns {Promise<object>}
5395
+ */
5396
+ async resolveTaskParams(context, overrides = {}) {
5397
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
5398
+ const fromCli = context.params.get("name", "string");
5399
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
5400
+ const name = overrideName || cliName;
5401
+ if (!name) {
5402
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5403
+ }
5404
+ const TaskClass = this.requireClass(name);
5405
+ return TaskClass.resolveParams(context, { ...overrides, name });
5406
+ }
5407
+ /**
5408
+ * Names of every registered task, sorted alphabetically (useful for CLI output
5409
+ * and allowlist sanity checks).
5410
+ *
5411
+ * @returns {string[]}
5412
+ */
4626
5413
  listSupportedTasks() {
4627
5414
  return Object.keys(this.map).sort();
4628
5415
  }
5416
+ /**
5417
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
5418
+ * or for serialization.
5419
+ *
5420
+ * @returns {Record<string, Function>}
5421
+ */
4629
5422
  toObject() {
4630
5423
  return { ...this.map };
4631
5424
  }
4632
5425
  };
4633
5426
 
4634
- // src/tasks/taskScriptRunner.ts
5427
+ // src/tasks/serviceTaskAllowlist.js
5428
+ function normalizeAllowedTasks(value) {
5429
+ if (!value) return void 0;
5430
+ if (Array.isArray(value)) {
5431
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5432
+ return out2.length ? out2 : void 0;
5433
+ }
5434
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5435
+ return out.length ? out : void 0;
5436
+ }
5437
+
5438
+ // src/tasks/taskScriptRunner.js
4635
5439
  import { spawn as spawn2 } from "child_process";
4636
5440
 
4637
- // src/tasks/index.ts
5441
+ // src/tasks/index.js
4638
5442
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4639
5443
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4640
- function getDb2(context) {
5444
+ function getDb3(context) {
4641
5445
  const db = context.db;
4642
5446
  if (!db) {
4643
5447
  throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
@@ -4649,15 +5453,6 @@ function normalizeRegistry(registry) {
4649
5453
  if (registry instanceof TasksRegistry) return registry;
4650
5454
  return new TasksRegistry().addMany(registry);
4651
5455
  }
4652
- function normalizeAllowedTasks(value) {
4653
- if (!value) return void 0;
4654
- if (Array.isArray(value)) {
4655
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
4656
- return out2.length ? out2 : void 0;
4657
- }
4658
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
4659
- return out.length ? out : void 0;
4660
- }
4661
5456
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
4662
5457
  context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
4663
5458
  for (const [, taskInstance] of runningTaskInstances) {
@@ -4672,19 +5467,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
4672
5467
  context.emitter.emit("stop", allowanceMs);
4673
5468
  }
4674
5469
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4675
- const db = getDb2(context);
4676
- const taskName = row.task;
5470
+ const db = getDb3(context);
5471
+ const taskName = row.name;
4677
5472
  const TaskClass = registry.get(taskName);
4678
- const { paused_at: _pausedAt, ...rowForHistory } = row;
4679
5473
  if (!TaskClass) {
4680
5474
  const err = { message: `Unknown task "${taskName}"` };
4681
- await db(historyTable).insert({
4682
- ...rowForHistory,
4683
- completed_at: /* @__PURE__ */ new Date(),
4684
- success: false,
4685
- params: toJsonColumn(row.params),
4686
- results: toJsonColumn(err)
4687
- });
5475
+ await db(historyTable).insert(
5476
+ taskHistoryInsertFromQueueRow(row, {
5477
+ completed_at: /* @__PURE__ */ new Date(),
5478
+ success: false,
5479
+ status: "failed",
5480
+ status_changed_at: db.fn.now(),
5481
+ params: toJsonColumn(row.params),
5482
+ results: toJsonColumn(err)
5483
+ })
5484
+ );
4688
5485
  if (row.schedule) {
4689
5486
  await db(tasksTable).where({ id: row.id }).update({
4690
5487
  started_at: null,
@@ -4692,7 +5489,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4692
5489
  success: false,
4693
5490
  results: toJsonColumn(err),
4694
5491
  past_due: null,
4695
- paused_at: db.fn.now(),
5492
+ status: "paused",
5493
+ status_changed_at: db.fn.now(),
4696
5494
  progress: LOCKED_BY_ERROR_MESSAGE
4697
5495
  });
4698
5496
  } else {
@@ -4719,13 +5517,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4719
5517
  } finally {
4720
5518
  runningTaskInstances.delete(row.id);
4721
5519
  }
4722
- await db(historyTable).insert({
4723
- ...rowForHistory,
4724
- completed_at: /* @__PURE__ */ new Date(),
4725
- success,
4726
- params: toJsonColumn(row.params),
4727
- results: toJsonColumn(results)
4728
- });
5520
+ await db(historyTable).insert(
5521
+ taskHistoryInsertFromQueueRow(row, {
5522
+ completed_at: /* @__PURE__ */ new Date(),
5523
+ success,
5524
+ status: success ? "completed" : "failed",
5525
+ status_changed_at: db.fn.now(),
5526
+ params: toJsonColumn(row.params),
5527
+ results: toJsonColumn(results)
5528
+ })
5529
+ );
4729
5530
  if (!success) {
4730
5531
  const dbName = String(context?.params?.get?.("dbName") || "local");
4731
5532
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -4740,19 +5541,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4740
5541
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
4741
5542
  appendTaskIpcLog(context, row, {
4742
5543
  level: "error",
4743
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
5544
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
4744
5545
  details: results
4745
5546
  });
4746
5547
  }
4747
5548
  if (row.schedule) {
4748
5549
  if (success) {
5550
+ let nextRunAt = null;
5551
+ try {
5552
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
5553
+ } catch (e) {
5554
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
5555
+ }
4749
5556
  await db(tasksTable).where({ id: row.id }).update({
4750
5557
  started_at: null,
4751
5558
  completed_at: /* @__PURE__ */ new Date(),
4752
5559
  success,
4753
5560
  results: toJsonColumn(results),
4754
5561
  progress: null,
4755
- past_due: null
5562
+ past_due: null,
5563
+ status: "idle",
5564
+ status_changed_at: db.fn.now(),
5565
+ next_run_at: nextRunAt,
5566
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
5567
+ service_name: null,
5568
+ server_name: null,
5569
+ instance_number: null
4756
5570
  });
4757
5571
  } else {
4758
5572
  await db(tasksTable).where({ id: row.id }).update({
@@ -4760,7 +5574,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4760
5574
  completed_at: /* @__PURE__ */ new Date(),
4761
5575
  success,
4762
5576
  results: toJsonColumn(results),
4763
- paused_at: db.fn.now(),
5577
+ status: "paused",
5578
+ status_changed_at: db.fn.now(),
4764
5579
  progress: LOCKED_BY_ERROR_MESSAGE,
4765
5580
  past_due: null
4766
5581
  });
@@ -4772,195 +5587,361 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4772
5587
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
4773
5588
  return { stopRunnerRequested, stopAllowanceMs };
4774
5589
  }
4775
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
4776
- const db = getDb2(context);
4777
- 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);
5590
+ function shuffleTaskRowsInPlace(rows) {
5591
+ for (let i = rows.length - 1; i > 0; i--) {
5592
+ const j = Math.floor(Math.random() * (i + 1));
5593
+ const t = rows[i];
5594
+ rows[i] = rows[j];
5595
+ rows[j] = t;
5596
+ }
5597
+ }
5598
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
5599
+ const db = getDb3(context);
5600
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
5601
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
5602
+ }).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);
4778
5603
  if (taskNames && taskNames.length > 0) {
4779
- query = query.whereIn("task", taskNames);
5604
+ query = query.whereIn("name", taskNames);
5605
+ }
5606
+ if (runnerIdentity) {
5607
+ query = query.where(function() {
5608
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
5609
+ }).where(function() {
5610
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
5611
+ }).where(function() {
5612
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
5613
+ });
5614
+ } else {
5615
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
4780
5616
  }
4781
5617
  const candidates = await query;
5618
+ shuffleTaskRowsInPlace(candidates);
4782
5619
  for (const row of candidates) {
4783
5620
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
4784
5621
  continue;
4785
5622
  }
4786
- const TaskClass = registry.get(row.task);
4787
- if (TaskClass) {
4788
- const taskInstance = new TaskClass(context, row);
4789
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
4790
- if (reason) {
4791
- if (!row.past_due) {
4792
- await db(tasksTable).where({ id: row.id }).update({
4793
- past_due: db.fn.now(),
4794
- progress: String(reason)
4795
- });
4796
- }
4797
- continue;
5623
+ const TaskClass = registry.get(row.name);
5624
+ if (!TaskClass) {
5625
+ continue;
5626
+ }
5627
+ const taskInstance = new TaskClass(context, row);
5628
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
5629
+ if (reason) {
5630
+ if (!row.past_due) {
5631
+ await db(tasksTable).where({ id: row.id }).update({
5632
+ past_due: db.fn.now(),
5633
+ progress: String(reason)
5634
+ });
4798
5635
  }
5636
+ continue;
4799
5637
  }
4800
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
5638
+ const claimPatch = {
5639
+ started_at: db.fn.now(),
5640
+ status: "running",
5641
+ status_changed_at: db.fn.now()
5642
+ };
5643
+ if (runnerIdentity) {
5644
+ claimPatch.service_name = runnerIdentity.service_name;
5645
+ claimPatch.server_name = runnerIdentity.server_name;
5646
+ claimPatch.instance_number = runnerIdentity.instance_number;
5647
+ }
5648
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
4801
5649
  const claimed = Array.isArray(updated) ? updated[0] : null;
4802
5650
  if (claimed) return claimed;
4803
5651
  }
4804
5652
  return null;
4805
5653
  }
4806
5654
  async function runTasksLoop(context, options) {
4807
- const queue = options.queue ?? "tasks";
5655
+ const queueName = options.queueName ?? "tasks";
4808
5656
  const target = options.target;
4809
5657
  const pollMs = options.pollMs ?? 1e3;
4810
- const maxParallel = options.maxParallel ?? 1;
5658
+ const claimJitterMs = options.claimJitterMs ?? 0;
5659
+ const maxParallel = options.maxParallel ?? 32;
4811
5660
  const scanLimit = options.scanLimit ?? 100;
4812
5661
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
4813
5662
  const registry = normalizeRegistry(options.registry);
4814
- const { tasksTable, historyTable } = queueToTableNames(queue);
5663
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
4815
5664
  if (!target) throw new Error("runTasksLoop: target is required");
5665
+ context.tasksQueueName = queueName;
4816
5666
  const runningPromises = /* @__PURE__ */ new Set();
4817
5667
  const runningTaskInstances = /* @__PURE__ */ new Map();
4818
5668
  let runningStopControlPromise = null;
4819
5669
  let stopRequested = false;
4820
5670
  let stopAllowanceMs = 5e3;
4821
- context.__tasksRunnerStop = false;
4822
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
4823
- if (!runningStopControlPromise) {
4824
- const claimedStopTask = await claimNextRunnableTask(
4825
- context,
4826
- tasksTable,
4827
- target,
4828
- registry,
4829
- 10,
4830
- ["stopRunner", "stop"]
4831
- );
4832
- if (claimedStopTask) {
4833
- runningStopControlPromise = executeClaimedTask(
5671
+ context.tasksRunnerStop = false;
5672
+ let registryReg = null;
5673
+ let registryInterval = null;
5674
+ let runnerIdentity = null;
5675
+ const hbGroup = options.runnerServiceGroup?.trim();
5676
+ if (hbGroup) {
5677
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
5678
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
5679
+ const defaultMeta = {
5680
+ component: "tasks-runner",
5681
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
5682
+ };
5683
+ registryReg = await registerInServicesRegistry(context, {
5684
+ queueName,
5685
+ target,
5686
+ serviceGroup: hbGroup,
5687
+ serviceName: options.runnerServiceName,
5688
+ instanceNumber: options.runnerInstanceNumber,
5689
+ staleMs,
5690
+ groupMaxInstances: options.runnerGroupMaxInstances,
5691
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
5692
+ metadata: options.runnerMetadata ?? defaultMeta
5693
+ });
5694
+ runnerIdentity = {
5695
+ service_name: registryReg.serviceName,
5696
+ server_name: os3.hostname(),
5697
+ instance_number: registryReg.instanceNumber
5698
+ };
5699
+ registryInterval = setInterval(() => {
5700
+ void touchServicesRegistry(context, registryReg).catch((err) => {
5701
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
5702
+ });
5703
+ }, hbIntervalMs);
5704
+ }
5705
+ try {
5706
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
5707
+ if (!runningStopControlPromise) {
5708
+ const claimedStopTask = await claimNextRunnableTask(
5709
+ context,
5710
+ tasksTable,
5711
+ target,
5712
+ registry,
5713
+ 10,
5714
+ ["stopRunner", "stop"],
5715
+ runnerIdentity
5716
+ );
5717
+ if (claimedStopTask) {
5718
+ runningStopControlPromise = executeClaimedTask(
5719
+ context,
5720
+ tasksTable,
5721
+ historyTable,
5722
+ claimedStopTask,
5723
+ registry,
5724
+ runningTaskInstances
5725
+ ).then(async (outcome) => {
5726
+ if (outcome.stopRunnerRequested && !stopRequested) {
5727
+ stopRequested = true;
5728
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5729
+ context.tasksRunnerStop = true;
5730
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5731
+ }
5732
+ }).finally(() => {
5733
+ runningStopControlPromise = null;
5734
+ });
5735
+ }
5736
+ }
5737
+ if (claimJitterMs > 0) {
5738
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
5739
+ }
5740
+ while (runningPromises.size < maxParallel) {
5741
+ const claimed = await claimNextRunnableTask(
4834
5742
  context,
4835
5743
  tasksTable,
4836
- historyTable,
4837
- claimedStopTask,
5744
+ target,
4838
5745
  registry,
4839
- runningTaskInstances
4840
- ).then(async (outcome) => {
5746
+ scanLimit,
5747
+ allowedTasks,
5748
+ runnerIdentity
5749
+ );
5750
+ if (!claimed) break;
5751
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4841
5752
  if (outcome.stopRunnerRequested && !stopRequested) {
4842
5753
  stopRequested = true;
4843
5754
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4844
- context.__tasksRunnerStop = true;
5755
+ context.tasksRunnerStop = true;
4845
5756
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4846
5757
  }
4847
5758
  }).finally(() => {
4848
- runningStopControlPromise = null;
5759
+ runningPromises.delete(p);
4849
5760
  });
5761
+ runningPromises.add(p);
5762
+ }
5763
+ const wakePromises = [...runningPromises];
5764
+ if (runningStopControlPromise) {
5765
+ wakePromises.push(runningStopControlPromise);
5766
+ }
5767
+ if (wakePromises.length === 0) {
5768
+ await sleepMs(pollMs);
5769
+ } else {
5770
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
5771
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
4850
5772
  }
4851
5773
  }
4852
- while (runningPromises.size < maxParallel) {
4853
- const claimed = await claimNextRunnableTask(
4854
- context,
4855
- tasksTable,
4856
- target,
4857
- registry,
4858
- scanLimit,
4859
- allowedTasks
4860
- );
4861
- if (!claimed) break;
4862
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4863
- if (outcome.stopRunnerRequested && !stopRequested) {
4864
- stopRequested = true;
4865
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4866
- context.__tasksRunnerStop = true;
4867
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4868
- }
4869
- }).finally(() => {
4870
- runningPromises.delete(p);
4871
- });
4872
- runningPromises.add(p);
5774
+ if (context.isStop() && !stopRequested) {
5775
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4873
5776
  }
4874
- await sleepMs(pollMs);
4875
- }
4876
- if (context.isStop() && !stopRequested) {
4877
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4878
- }
4879
- if (runningPromises.size > 0) {
4880
- if (stopRequested) {
4881
- await Promise.race([
4882
- Promise.allSettled(Array.from(runningPromises)),
4883
- sleepMs(stopAllowanceMs).then(() => {
4884
- context.logger.warn?.(
4885
- `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
4886
- );
4887
- })
4888
- ]);
4889
- } else {
4890
- await Promise.allSettled(Array.from(runningPromises));
5777
+ if (runningPromises.size > 0) {
5778
+ if (stopRequested) {
5779
+ await Promise.race([
5780
+ Promise.allSettled(Array.from(runningPromises)),
5781
+ sleepMs(stopAllowanceMs).then(() => {
5782
+ context.logger.warn?.(
5783
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
5784
+ );
5785
+ })
5786
+ ]);
5787
+ } else {
5788
+ await Promise.allSettled(Array.from(runningPromises));
5789
+ }
5790
+ }
5791
+ } finally {
5792
+ if (registryInterval) {
5793
+ clearInterval(registryInterval);
5794
+ registryInterval = null;
5795
+ }
5796
+ if (registryReg) {
5797
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
5798
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
5799
+ });
5800
+ registryReg = null;
5801
+ delete context.servicesRegistry;
5802
+ delete context.runnerHeartbeat;
4891
5803
  }
4892
5804
  }
4893
5805
  }
4894
5806
  var TasksManager = class _TasksManager {
4895
- context;
4896
- queue;
4897
- target;
4898
- recreateTaskTables;
4899
- pollMs;
4900
- maxParallel;
4901
- scanLimit;
4902
- allowedTasks;
4903
- registry;
5807
+ /**
5808
+ * @param {object} context
5809
+ * @param {{
5810
+ * queueName?: string,
5811
+ * target?: string,
5812
+ * recreateTaskTables?: boolean,
5813
+ * pollMs?: number,
5814
+ * claimJitterMs?: number,
5815
+ * maxParallel?: number,
5816
+ * scanLimit?: number,
5817
+ * allowedTasks?: string | string[],
5818
+ * registry?: TasksRegistry | Record<string, Function>,
5819
+ * runnerServiceGroup?: string,
5820
+ * runnerServiceName?: string,
5821
+ * runnerInstanceNumber?: number,
5822
+ * runnerHeartbeatIntervalMs?: number,
5823
+ * runnerHeartbeatStaleMs?: number,
5824
+ * runnerGroupMaxInstances?: number,
5825
+ * runnerEnforceMaxInstances?: boolean,
5826
+ * runnerMetadata?: Record<string, unknown>,
5827
+ * }} [options]
5828
+ */
4904
5829
  constructor(context, options = {}) {
4905
5830
  this.context = context;
4906
- this.queue = options.queue ?? "tasks";
5831
+ this.queueName = options.queueName ?? "tasks";
4907
5832
  this.target = options.target ?? "localRunner";
4908
5833
  this.recreateTaskTables = options.recreateTaskTables ?? false;
4909
5834
  this.pollMs = options.pollMs ?? 1e3;
5835
+ this.claimJitterMs = options.claimJitterMs ?? 0;
4910
5836
  this.maxParallel = options.maxParallel ?? 1;
4911
5837
  this.scanLimit = options.scanLimit ?? 100;
4912
5838
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
4913
5839
  this.registry = normalizeRegistry(options.registry);
5840
+ this.runnerServiceGroup = options.runnerServiceGroup;
5841
+ this.runnerServiceName = options.runnerServiceName;
5842
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
5843
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
5844
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
5845
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
5846
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
5847
+ this.runnerMetadata = options.runnerMetadata;
4914
5848
  }
5849
+ /**
5850
+ * Preferred factory: reads defaults from `context.params` (module namespace
5851
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
5852
+ * and inline options in one consistent resolver.
5853
+ *
5854
+ * @param {object} context
5855
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
5856
+ * @returns {TasksManager}
5857
+ */
4915
5858
  static init(context, options = {}) {
4916
5859
  const defs2 = {
4917
5860
  table: "string default tasks",
4918
5861
  target: "string default localRunner",
4919
5862
  recreateTaskTables: "boolean default false",
4920
5863
  pollMs: "number default 1000",
5864
+ claimJitterMs: "number default 0",
4921
5865
  maxParallel: "number default 1",
4922
5866
  scanLimit: "number default 100",
4923
- allowedTasks: "string"
5867
+ allowedTasks: "string",
5868
+ runnerServiceGroup: "string",
5869
+ runnerServiceName: "string",
5870
+ runnerInstanceNumber: "number",
5871
+ runnerHeartbeatIntervalMs: "number default 10000",
5872
+ runnerHeartbeatStaleMs: "number default 45000",
5873
+ runnerGroupMaxInstances: "number",
5874
+ runnerEnforceMaxInstances: "boolean default true"
4924
5875
  };
4925
- const discovered = context.params.getAllForModule(defs2);
5876
+ const discovered = context.params.getAllForModule("tasks", defs2);
4926
5877
  const resolved = {
4927
- queue: discovered.table,
5878
+ queueName: discovered.table,
4928
5879
  target: discovered.target,
4929
5880
  recreateTaskTables: discovered.recreateTaskTables,
4930
5881
  pollMs: discovered.pollMs,
5882
+ claimJitterMs: discovered.claimJitterMs,
4931
5883
  maxParallel: discovered.maxParallel,
4932
5884
  scanLimit: discovered.scanLimit,
4933
5885
  allowedTasks: discovered.allowedTasks,
5886
+ runnerServiceGroup: discovered.runnerServiceGroup,
5887
+ runnerServiceName: discovered.runnerServiceName,
5888
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
5889
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
5890
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
5891
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
5892
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
4934
5893
  ...options
4935
5894
  };
4936
5895
  return new _TasksManager(context, resolved);
4937
5896
  }
5897
+ /**
5898
+ * Idempotently ensure the three backing tables exist for this queue.
5899
+ *
5900
+ * @param {{ recreate?: boolean }} [options]
5901
+ * @returns {Promise<void>}
5902
+ */
4938
5903
  async ensureTaskTables(options = {}) {
4939
5904
  await ensureTaskTables(this.context, {
4940
- queue: this.queue,
5905
+ queueName: this.queueName,
4941
5906
  recreate: options.recreate ?? this.recreateTaskTables
4942
5907
  });
4943
5908
  }
5909
+ /**
5910
+ * Start the runner loop using this manager's resolved config. Per-call
5911
+ * options override the stored defaults, but `runnerMetadata` still falls
5912
+ * through when omitted.
5913
+ *
5914
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
5915
+ * @returns {Promise<void>}
5916
+ */
4944
5917
  async runTasksLoop(options = {}) {
4945
5918
  await runTasksLoop(this.context, {
4946
- queue: options.queue ?? this.queue,
5919
+ queueName: options.queueName ?? this.queueName,
4947
5920
  target: options.target ?? this.target,
4948
5921
  pollMs: options.pollMs ?? this.pollMs,
5922
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
4949
5923
  maxParallel: options.maxParallel ?? this.maxParallel,
4950
5924
  scanLimit: options.scanLimit ?? this.scanLimit,
4951
5925
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
4952
- registry: options.registry ?? this.registry
5926
+ registry: options.registry ?? this.registry,
5927
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
5928
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
5929
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
5930
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
5931
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
5932
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
5933
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
5934
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
4953
5935
  });
4954
5936
  }
4955
5937
  };
4956
5938
 
4957
- // src/scripts/cli-runner.ts
5939
+ // src/scripts/cli-runner.js
4958
5940
  var defs = {
4959
- dbName: "string default local",
4960
5941
  tasksModule: "string"
4961
5942
  };
4962
5943
  async function loadTasksModule(modulePath) {
4963
- const absolute = path4.isAbsolute(modulePath) ? modulePath : path4.resolve(process.cwd(), modulePath);
5944
+ const absolute = path5.isAbsolute(modulePath) ? modulePath : path5.resolve(process.cwd(), modulePath);
4964
5945
  const imported = await import(pathToFileURL(absolute).href);
4965
5946
  if (!imported.tasksRegistry || typeof imported.tasksRegistry !== "object") {
4966
5947
  throw new Error(`tasksModule "${modulePath}" must export "tasksRegistry" object`);
@@ -4968,11 +5949,8 @@ async function loadTasksModule(modulePath) {
4968
5949
  return imported.tasksRegistry;
4969
5950
  }
4970
5951
  var flow = async (context) => {
4971
- const {
4972
- dbName,
4973
- tasksModule
4974
- } = context.params.getAll(defs);
4975
- const db = await dbInit(context, dbName);
5952
+ const { tasksModule } = context.params.getAll(defs);
5953
+ const db = await Db.init(context);
4976
5954
  context.db = db;
4977
5955
  const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
4978
5956
  if (tasksModule) {