@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
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var __esm = (fn, res) => function __init() {
9
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
10
  };
11
11
 
12
- // src/screen/components.ts
12
+ // src/screen/components.js
13
13
  import { createElement as h } from "react";
14
14
  import { Box, Text } from "ink";
15
15
  function getScreenWidth(maxWidth = null) {
@@ -84,13 +84,12 @@ function ScreenFooter({ lines, textStyle }) {
84
84
  );
85
85
  }
86
86
  var init_components = __esm({
87
- "src/screen/components.ts"() {
88
- "use strict";
87
+ "src/screen/components.js"() {
89
88
  }
90
89
  });
91
90
 
92
- // src/screen/list-components.ts
93
- import React2, { useState, useEffect, useRef, createElement } from "react";
91
+ // src/screen/list-components.js
92
+ import React, { useState, useEffect, useRef, createElement } from "react";
94
93
  import { Box as Box2, Text as Text2 } from "ink";
95
94
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
96
95
  const [, forceUpdate] = useState({});
@@ -226,11 +225,11 @@ function MultiColumnListWithPreviewComponent({
226
225
  const previewRows = [];
227
226
  if (typeof previewContent === "string") {
228
227
  previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(Text2, { bold: true }, previewContent) }));
229
- } else if (typeof previewContent === "object" && !React2.isValidElement(previewContent) && previewContent !== null) {
228
+ } else if (typeof previewContent === "object" && !React.isValidElement(previewContent) && previewContent !== null) {
230
229
  Object.entries(previewContent).forEach(([key, value], idx) => {
231
230
  previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(Text2, {}, `${key}: ${value}`) }));
232
231
  });
233
- } else if (React2.isValidElement(previewContent)) {
232
+ } else if (React.isValidElement(previewContent)) {
234
233
  previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
235
234
  }
236
235
  return h2(
@@ -243,11 +242,13 @@ function MultiColumnListWithPreviewComponent({
243
242
  ...previewRows
244
243
  );
245
244
  }
246
- function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
245
+ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " ", onSelectionChange }) {
247
246
  const [, forceUpdate] = useState({});
248
247
  const [sortOrder, setSortOrder] = useState("none");
249
248
  const [scrollOffset, setScrollOffset] = useState(0);
250
249
  const scrollStateRef = useRef({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
250
+ const itemsRef = useRef(items);
251
+ itemsRef.current = items;
251
252
  const defaultGetTitle = (item) => {
252
253
  return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
253
254
  };
@@ -261,18 +262,24 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
261
262
  return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
262
263
  }
263
264
  }) : items;
265
+ const displayItemsRef = useRef(displayItems);
266
+ displayItemsRef.current = displayItems;
264
267
  const effectiveMaxHeight = maxHeight || displayItems.length;
265
- const canScroll = displayItems.length > effectiveMaxHeight;
268
+ const _canScroll = displayItems.length > effectiveMaxHeight;
266
269
  const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
267
270
  const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
268
271
  const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
269
272
  const canScrollUp = clampedScrollOffset > 0;
270
273
  const canScrollDown = clampedScrollOffset < maxScrollOffset;
271
274
  scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
275
+ const onSelectionChangeRef = useRef(onSelectionChange);
276
+ onSelectionChangeRef.current = onSelectionChange;
272
277
  useEffect(() => {
273
278
  ctx.setAction("moveUp", () => {
274
279
  const newIndex = Math.max(0, selectedIndexRef.current - 1);
275
280
  selectedIndexRef.current = newIndex;
281
+ const list = displayItemsRef.current;
282
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
276
283
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
277
284
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
278
285
  const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
@@ -282,18 +289,11 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
282
289
  forceUpdate({});
283
290
  });
284
291
  ctx.setAction("moveDown", () => {
285
- const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
286
- const titleA = titleGetter(a).toLowerCase();
287
- const titleB = titleGetter(b).toLowerCase();
288
- if (sortOrder === "asc") {
289
- return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
290
- } else {
291
- return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
292
- }
293
- }) : items;
294
- const maxIndex = currentItems.length - 1;
292
+ const currentItems = displayItemsRef.current;
293
+ const maxIndex = Math.max(0, currentItems.length - 1);
295
294
  const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
296
295
  selectedIndexRef.current = newIndex;
296
+ onSelectionChangeRef.current?.(newIndex, currentItems[newIndex]);
297
297
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
298
298
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
299
299
  const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
@@ -303,8 +303,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
303
303
  forceUpdate({});
304
304
  });
305
305
  ctx.setAction("scrollUp", () => {
306
- const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
307
- const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
306
+ const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
308
307
  const newScrollOffset = Math.max(0, currentScrollOffset - 1);
309
308
  setScrollOffset(newScrollOffset);
310
309
  forceUpdate({});
@@ -319,9 +318,9 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
319
318
  if (sortable) {
320
319
  ctx.setAction("toggleSort", () => {
321
320
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
322
- const currentSelectedItem = displayItems[selectedIndexRef.current];
321
+ const currentSelectedItem = displayItemsRef.current[selectedIndexRef.current];
323
322
  setSortOrder(nextSort);
324
- const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
323
+ const newSortedItems = nextSort !== "none" ? [...itemsRef.current].sort((a, b) => {
325
324
  const titleA = titleGetter(a).toLowerCase();
326
325
  const titleB = titleGetter(b).toLowerCase();
327
326
  if (nextSort === "asc") {
@@ -329,7 +328,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
329
328
  } else {
330
329
  return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
331
330
  }
332
- }) : items;
331
+ }) : itemsRef.current;
333
332
  const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
334
333
  if (newIndex !== -1) {
335
334
  selectedIndexRef.current = newIndex;
@@ -467,14 +466,13 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
467
466
  }
468
467
  var h2;
469
468
  var init_list_components = __esm({
470
- "src/screen/list-components.ts"() {
471
- "use strict";
469
+ "src/screen/list-components.js"() {
472
470
  init_components();
473
471
  h2 = createElement;
474
472
  }
475
473
  });
476
474
 
477
- // src/screen/screens.ts
475
+ // src/screen/screens.js
478
476
  import { useState as useState2, createElement as h3 } from "react";
479
477
  import { render, useInput, Text as Text3 } from "ink";
480
478
  function groupKeyBindings(bindings) {
@@ -552,7 +550,7 @@ async function showScreen(config2) {
552
550
  let renderResult = null;
553
551
  let initialized = false;
554
552
  const Screen = () => {
555
- const [updateCounter, setUpdateCounter] = useState2(0);
553
+ const [, setUpdateCounter] = useState2(0);
556
554
  if (!initialized) {
557
555
  const defaultBindings = [
558
556
  { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
@@ -671,7 +669,7 @@ async function showScreen(config2) {
671
669
  }
672
670
  }
673
671
  if (matchedBinding && actions[matchedBinding.action]) {
674
- const actionResult = actions[matchedBinding.action]({
672
+ actions[matchedBinding.action]({
675
673
  input,
676
674
  key,
677
675
  binding: matchedBinding
@@ -801,8 +799,7 @@ async function showMultiColumnListWithPreviewScreen(config2) {
801
799
  }
802
800
  var showMenuScreen, showWordGridScreen;
803
801
  var init_screens = __esm({
804
- "src/screen/screens.ts"() {
805
- "use strict";
802
+ "src/screen/screens.js"() {
806
803
  init_components();
807
804
  init_list_components();
808
805
  showMenuScreen = showListScreen;
@@ -810,9 +807,9 @@ var init_screens = __esm({
810
807
  }
811
808
  });
812
809
 
813
- // src/screen/ui-elements.ts
810
+ // src/screen/ui-elements.js
814
811
  import { createElement as h4 } from "react";
815
- import { Box as Box4, Text as Text4 } from "ink";
812
+ import { Box as Box3, Text as Text4 } from "ink";
816
813
  function ListItem({
817
814
  children,
818
815
  isSelected = false,
@@ -822,7 +819,7 @@ function ListItem({
822
819
  dimColor = false
823
820
  }) {
824
821
  return h4(
825
- Box4,
822
+ Box3,
826
823
  {},
827
824
  h4(Text4, {
828
825
  color: isSelected ? backgroundColor || "green" : color,
@@ -837,10 +834,10 @@ function TextBlock({
837
834
  color = "white",
838
835
  dimmed = false,
839
836
  bold = false,
840
- maxWidth
837
+ maxWidth: _maxWidth
841
838
  }) {
842
839
  return h4(
843
- Box4,
840
+ Box3,
844
841
  {},
845
842
  h4(Text4, {
846
843
  color,
@@ -851,7 +848,7 @@ function TextBlock({
851
848
  }
852
849
  function Divider({ character = "\u2500", width = 80 }) {
853
850
  return h4(
854
- Box4,
851
+ Box3,
855
852
  { marginY: 1 },
856
853
  h4(Text4, { dimColor: true }, character.repeat(width))
857
854
  );
@@ -866,7 +863,7 @@ function GridCell({
866
863
  align = "left"
867
864
  }) {
868
865
  return h4(
869
- Box4,
866
+ Box3,
870
867
  { width },
871
868
  h4(Text4, {
872
869
  color,
@@ -877,44 +874,42 @@ function GridCell({
877
874
  }, children)
878
875
  );
879
876
  }
880
- function InputField({ prompt, value, onChange, onSubmit }) {
877
+ function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit }) {
881
878
  return h4(
882
- Box4,
879
+ Box3,
883
880
  { flexDirection: "column" },
884
881
  h4(Text4, {}, prompt),
885
882
  h4(
886
- Box4,
883
+ Box3,
887
884
  { marginTop: 1 },
888
885
  h4(Text4, { color: "cyan" }, " > ", value, "_")
889
886
  )
890
887
  );
891
888
  }
892
889
  var init_ui_elements = __esm({
893
- "src/screen/ui-elements.ts"() {
894
- "use strict";
890
+ "src/screen/ui-elements.js"() {
895
891
  }
896
892
  });
897
893
 
898
- // src/screen/utils.ts
894
+ // src/screen/utils.js
899
895
  function buildBreadcrumb(parts) {
900
896
  if (parts.length === 0) return "";
901
897
  if (parts.length === 1) return parts[0];
902
898
  return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
903
899
  }
904
- function buildDetailBreadcrumb(path4, suffix = "") {
905
- if (path4.length <= 1) {
906
- return suffix ? `\u2190 ${suffix}` : path4[0] || "";
900
+ function buildDetailBreadcrumb(path5, suffix = "") {
901
+ if (path5.length <= 1) {
902
+ return suffix ? `\u2190 ${suffix}` : path5[0] || "";
907
903
  }
908
- const breadcrumb = buildBreadcrumb(path4);
904
+ const breadcrumb = buildBreadcrumb(path5);
909
905
  return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
910
906
  }
911
907
  var init_utils = __esm({
912
- "src/screen/utils.ts"() {
913
- "use strict";
908
+ "src/screen/utils.js"() {
914
909
  }
915
910
  });
916
911
 
917
- // src/screen/footer-builder.ts
912
+ // src/screen/footer-builder.js
918
913
  function buildFooter(config2 = {}) {
919
914
  const {
920
915
  navigation = null,
@@ -965,8 +960,7 @@ function organizeFooterMessages(messages) {
965
960
  }
966
961
  var FooterPresets;
967
962
  var init_footer_builder = __esm({
968
- "src/screen/footer-builder.ts"() {
969
- "use strict";
963
+ "src/screen/footer-builder.js"() {
970
964
  FooterPresets = {
971
965
  /**
972
966
  * Menu screen footer
@@ -1025,9 +1019,9 @@ var init_footer_builder = __esm({
1025
1019
  }
1026
1020
  });
1027
1021
 
1028
- // src/screen/index.ts
1029
- import React5, { useState as useState3, useEffect as useEffect3, useRef as useRef3, useMemo, useCallback, createElement as createElement2 } from "react";
1030
- import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
1022
+ // src/screen/index.js
1023
+ import React2, { useState as useState3, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useMemo, useCallback, memo, createElement as createElement2 } from "react";
1024
+ import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
1031
1025
  async function load() {
1032
1026
  if (loadPromise) return loadPromise;
1033
1027
  loadPromise = Promise.all([
@@ -1039,8 +1033,7 @@ async function load() {
1039
1033
  }
1040
1034
  var loadPromise;
1041
1035
  var init_screen = __esm({
1042
- "src/screen/index.ts"() {
1043
- "use strict";
1036
+ "src/screen/index.js"() {
1044
1037
  init_screens();
1045
1038
  init_list_components();
1046
1039
  init_components();
@@ -1055,7 +1048,7 @@ var init_screen = __esm({
1055
1048
  }
1056
1049
  });
1057
1050
 
1058
- // src/args/index.ts
1051
+ // src/args/index.js
1059
1052
  import { readFileSync, existsSync } from "fs";
1060
1053
  import { resolve, dirname, basename, extname, join, isAbsolute } from "path";
1061
1054
  import { config } from "dotenv";
@@ -1542,10 +1535,10 @@ function getArgsInstance() {
1542
1535
  return instance;
1543
1536
  }
1544
1537
 
1545
- // src/params/index.ts
1538
+ // src/params/index.js
1546
1539
  import Joi from "joi";
1547
1540
 
1548
- // src/errors.ts
1541
+ // src/errors.js
1549
1542
  var FrameworkError = class extends Error {
1550
1543
  constructor(message) {
1551
1544
  super(message);
@@ -1565,7 +1558,7 @@ var FileDatabaseError = class extends FrameworkError {
1565
1558
  }
1566
1559
  };
1567
1560
 
1568
- // src/params/custom-types.ts
1561
+ // src/params/custom-types.js
1569
1562
  var joiEdateType = (value, helpers) => {
1570
1563
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
1571
1564
  const testDate = new Date(value);
@@ -1658,7 +1651,7 @@ function calculateTimeOffset(amount, unit, sign) {
1658
1651
  }
1659
1652
  return sign === "+" ? amount * multiplier : -amount * multiplier;
1660
1653
  }
1661
- var joiStringArrayType = (type) => (value, helpers) => {
1654
+ var joiStringArrayType = (type) => (value, _helpers) => {
1662
1655
  if (value === void 0 || typeof value === "function") {
1663
1656
  return [];
1664
1657
  }
@@ -1684,7 +1677,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
1684
1677
  return arr;
1685
1678
  };
1686
1679
 
1687
- // src/params/index.ts
1680
+ // src/params/index.js
1688
1681
  var Params = class _Params {
1689
1682
  context;
1690
1683
  // Partial context during initialization
@@ -1876,7 +1869,7 @@ var Params = class _Params {
1876
1869
  throw new ParamError(`default value "${defValObj.value}" type mismatch`);
1877
1870
  }
1878
1871
  type = type.default(defValObj.value);
1879
- } else if (str.match(/required/)) {
1872
+ } else if (str.match(/\s*required\s*/)) {
1880
1873
  type = type.required();
1881
1874
  } else {
1882
1875
  type = type.optional();
@@ -1944,7 +1937,7 @@ var Params = class _Params {
1944
1937
  definition = val;
1945
1938
  val = val.value;
1946
1939
  }
1947
- const def = this.assignDefinition(key, definition);
1940
+ this.assignDefinition(key, definition);
1948
1941
  if (!this.runAllRegisteredSetters(key, val)) {
1949
1942
  this.params[key] = val;
1950
1943
  }
@@ -1952,6 +1945,8 @@ var Params = class _Params {
1952
1945
  /**
1953
1946
  * Get all parameters from definitions (main script).
1954
1947
  * Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
1948
+ * Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
1949
+ * around {@link get}) so --showUsedParams groups usage correctly.
1955
1950
  */
1956
1951
  getAll(defs) {
1957
1952
  return this.getAllForModule("script", defs);
@@ -1988,6 +1983,19 @@ var Params = class _Params {
1988
1983
  this._currentModule = prev;
1989
1984
  }
1990
1985
  }
1986
+ /**
1987
+ * Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
1988
+ * under the same module (for --showUsedParams / getFiguredByModule).
1989
+ */
1990
+ runWithModule(moduleName, fn) {
1991
+ const prev = this._currentModule;
1992
+ this._currentModule = moduleName;
1993
+ try {
1994
+ return fn();
1995
+ } finally {
1996
+ this._currentModule = prev;
1997
+ }
1998
+ }
1991
1999
  /**
1992
2000
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
1993
2001
  */
@@ -2001,9 +2009,9 @@ var Params = class _Params {
2001
2009
  if (!parenMatch) continue;
2002
2010
  const parts = parenMatch[1].split(":");
2003
2011
  if (parts.length < 3) continue;
2004
- const path4 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2005
- if (!path4 || path4.includes(paramsIndexPath)) continue;
2006
- const srcMatch = path4.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2012
+ const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2013
+ if (!path5 || path5.includes(paramsIndexPath)) continue;
2014
+ const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2007
2015
  if (srcMatch) return srcMatch[1];
2008
2016
  }
2009
2017
  return "script";
@@ -2048,14 +2056,14 @@ var Params = class _Params {
2048
2056
  }
2049
2057
  };
2050
2058
 
2051
- // src/screen.ts
2059
+ // src/index.js
2052
2060
  init_screen();
2053
2061
 
2054
- // src/filedatabase/index.ts
2062
+ // src/filedatabase/index.js
2055
2063
  import fs3 from "fs";
2056
2064
  import path3 from "path";
2057
2065
 
2058
- // src/utils/os-utils.ts
2066
+ // src/utils/os-utils.js
2059
2067
  import fs from "fs";
2060
2068
  import path from "path";
2061
2069
  import { execSync } from "child_process";
@@ -2084,7 +2092,7 @@ function getFreeDiskSpace(targetPath) {
2084
2092
  }
2085
2093
  }
2086
2094
 
2087
- // src/utils/fs-utils.ts
2095
+ // src/utils/fs-utils.js
2088
2096
  import fs2 from "fs";
2089
2097
  import path2 from "path";
2090
2098
  async function ensurePath(...pathParts) {
@@ -2108,7 +2116,7 @@ function getFileExtension(dataType) {
2108
2116
  }
2109
2117
  }
2110
2118
 
2111
- // src/utils/format-utils.ts
2119
+ // src/utils/format-utils.js
2112
2120
  function bytesToHumanReadable(bytes) {
2113
2121
  if (bytes === 0) return "0 B";
2114
2122
  const k = 1024;
@@ -2117,7 +2125,7 @@ function bytesToHumanReadable(bytes) {
2117
2125
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
2118
2126
  }
2119
2127
 
2120
- // src/utils/date-utils.ts
2128
+ // src/utils/date-utils.js
2121
2129
  function isTimestampFolder(folderName) {
2122
2130
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
2123
2131
  if (!isoRegex.test(folderName)) {
@@ -2127,7 +2135,7 @@ function isTimestampFolder(folderName) {
2127
2135
  return !isNaN(date.getTime()) && date.getTime() > 0;
2128
2136
  }
2129
2137
 
2130
- // src/filedatabase/serializers.ts
2138
+ // src/filedatabase/serializers.js
2131
2139
  function detectDataType(data) {
2132
2140
  if (Array.isArray(data)) {
2133
2141
  return "json-array";
@@ -2159,7 +2167,7 @@ function deserializeData(rawData, dataType) {
2159
2167
  }
2160
2168
  }
2161
2169
 
2162
- // src/filedatabase/synopsis-functions.ts
2170
+ // src/filedatabase/synopsis-functions.js
2163
2171
  function defaultFileSynopsisFunction(fileEntry, data) {
2164
2172
  if (!Array.isArray(data) || data.length === 0) {
2165
2173
  return { ...fileEntry };
@@ -2227,7 +2235,7 @@ function defaultVersionSynopsisFunction(metadata) {
2227
2235
  return result;
2228
2236
  }
2229
2237
 
2230
- // src/filedatabase/index.ts
2238
+ // src/filedatabase/index.js
2231
2239
  var FileDatabase = class _FileDatabase {
2232
2240
  basePath;
2233
2241
  namespace;
@@ -2313,7 +2321,7 @@ var FileDatabase = class _FileDatabase {
2313
2321
  if (errors.length) {
2314
2322
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
2315
2323
  }
2316
- let parts = [this.basePath, this.namespace];
2324
+ const parts = [this.basePath, this.namespace];
2317
2325
  if (this.tableName) {
2318
2326
  parts.push(...this.tableName.split("/"));
2319
2327
  }
@@ -2806,14 +2814,17 @@ var FileDatabase = class _FileDatabase {
2806
2814
  * Prepare the instance for read or write operations
2807
2815
  * This discovers state and sets up internal members based on mode and current data
2808
2816
  */
2809
- async prepare({ write, read, version }) {
2817
+ async prepare(options) {
2818
+ const { write, read, version, deferInitialVersion } = options;
2810
2819
  if (write) {
2811
2820
  if (this.versioned) {
2812
2821
  if (this.currentVersion === null) {
2813
- await this.makeNewVersion();
2814
- this.metadata = this.getDefaultMetadata();
2815
- this.metadata.version = this.currentVersion;
2816
- this.makeNewFile();
2822
+ if (!deferInitialVersion) {
2823
+ await this.makeNewVersion();
2824
+ this.metadata = this.getDefaultMetadata();
2825
+ this.metadata.version = this.currentVersion;
2826
+ this.makeNewFile();
2827
+ }
2817
2828
  } else {
2818
2829
  if (!this.metadata.files.length) {
2819
2830
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -2923,7 +2934,7 @@ var FileDatabase = class _FileDatabase {
2923
2934
  if (options.forceNewVersion && !this.versioned) {
2924
2935
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
2925
2936
  }
2926
- await this.prepare({ write: true });
2937
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
2927
2938
  const incomingDataType = detectDataType(data);
2928
2939
  this.metadata.dataType = incomingDataType;
2929
2940
  if (options.forceNewVersion) {
@@ -3238,22 +3249,57 @@ function listSources(basePath) {
3238
3249
  }
3239
3250
  }
3240
3251
 
3241
- // src/db/index.ts
3252
+ // src/db/index.js
3242
3253
  import knex from "knex";
3254
+ var KNEX_DEFAULTS = {
3255
+ testConnection: true,
3256
+ pool: { min: 2, max: 10 },
3257
+ acquireConnectionTimeout: 1e4,
3258
+ ssl: { rejectUnauthorized: false }
3259
+ };
3243
3260
  var Db = class {
3244
- knexInstance = null;
3245
- config;
3246
- logger;
3247
- queriesLog = [];
3248
- isConnected = false;
3249
- /**
3250
- * Constructor - accepts config object
3251
- * Use dbInit() function to initialize with Context
3252
- */
3261
+ static async init(context, options = {}) {
3262
+ const defs = {
3263
+ dbName: "string",
3264
+ dbConnectionString: "string",
3265
+ dbProfile: "boolean default false"
3266
+ };
3267
+ const discovered = context?.params?.getAllForModule?.("db", defs) ?? {};
3268
+ const merged = { ...discovered, ...options };
3269
+ let { dbName, dbConnectionString } = merged;
3270
+ const { dbProfile } = merged;
3271
+ if (!dbName && !dbConnectionString) {
3272
+ dbName = "local";
3273
+ }
3274
+ if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
3275
+ dbConnectionString = dbName;
3276
+ dbName = void 0;
3277
+ }
3278
+ if (dbName && !dbConnectionString) {
3279
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3280
+ dbConnectionString = await context.params.get(paramName, "string");
3281
+ if (!dbConnectionString) {
3282
+ throw new ParamError(
3283
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3284
+ );
3285
+ }
3286
+ }
3287
+ const config2 = {
3288
+ ...KNEX_DEFAULTS,
3289
+ connectionString: dbConnectionString,
3290
+ name: dbName || merged.name || "default",
3291
+ profile: !!dbProfile,
3292
+ logger: context.logger
3293
+ };
3294
+ return dbConnect(context, config2);
3295
+ }
3253
3296
  constructor(config2) {
3254
- if (!config2.connectionString) {
3297
+ if (!config2 || !config2.connectionString) {
3255
3298
  throw new ParamError("Db: connectionString is required");
3256
3299
  }
3300
+ this.knexInstance = null;
3301
+ this.isConnected = false;
3302
+ this.queriesLog = [];
3257
3303
  this.config = {
3258
3304
  testConnection: true,
3259
3305
  profile: false,
@@ -3266,25 +3312,23 @@ var Db = class {
3266
3312
  };
3267
3313
  this.logger = this.config.logger;
3268
3314
  const instance2 = this;
3269
- const callableWrapper = function(...args) {
3315
+ const callableWrapper = function() {
3270
3316
  throw new Error("This should never be called directly");
3271
3317
  };
3272
3318
  callableWrapper._instance = instance2;
3273
3319
  return new Proxy(callableWrapper, {
3274
- // Intercept function calls: db('table')
3275
- apply: (target, thisArg, argumentsList) => {
3320
+ apply: (target, _thisArg, argumentsList) => {
3276
3321
  const inst = target._instance;
3277
3322
  if (!inst.knexInstance) {
3278
3323
  throw new Error("Db: Not connected. Call connect() first.");
3279
3324
  }
3280
3325
  return inst.knexInstance(...argumentsList);
3281
3326
  },
3282
- // Intercept property access: db.schema, db.raw, etc.
3283
3327
  get: (target, prop) => {
3284
3328
  if (prop === "_instance") {
3285
3329
  return target._instance;
3286
3330
  }
3287
- const instance3 = target._instance;
3331
+ const inst = target._instance;
3288
3332
  const ownMethods = [
3289
3333
  "connect",
3290
3334
  "disconnect",
@@ -3297,26 +3341,26 @@ var Db = class {
3297
3341
  "detectClient",
3298
3342
  "attachProfiler"
3299
3343
  ];
3300
- if (prop in instance3) {
3301
- const value = instance3[prop];
3344
+ if (prop in inst) {
3345
+ const value = inst[prop];
3302
3346
  if (typeof value === "function" && ownMethods.includes(prop)) {
3303
- return value.bind(instance3);
3347
+ return value.bind(inst);
3304
3348
  }
3305
3349
  if (typeof value !== "function") {
3306
3350
  return value;
3307
3351
  }
3308
3352
  }
3309
- if (instance3.knexInstance) {
3310
- const knexProp = instance3.knexInstance[prop];
3353
+ if (inst.knexInstance) {
3354
+ const knexProp = inst.knexInstance[prop];
3311
3355
  if (typeof knexProp === "function") {
3312
- return knexProp.bind(instance3.knexInstance);
3356
+ return knexProp.bind(inst.knexInstance);
3313
3357
  }
3314
3358
  return knexProp;
3315
3359
  }
3316
- if (prop in instance3) {
3317
- const method = instance3[prop];
3360
+ if (prop in inst) {
3361
+ const method = inst[prop];
3318
3362
  if (typeof method === "function") {
3319
- return method.bind(instance3);
3363
+ return method.bind(inst);
3320
3364
  }
3321
3365
  return method;
3322
3366
  }
@@ -3324,9 +3368,6 @@ var Db = class {
3324
3368
  }
3325
3369
  });
3326
3370
  }
3327
- /**
3328
- * Detect database client type from connection string
3329
- */
3330
3371
  detectClient(connectionString) {
3331
3372
  if (connectionString.match(/^postgresql/)) {
3332
3373
  return "pg";
@@ -3336,9 +3377,6 @@ var Db = class {
3336
3377
  }
3337
3378
  return null;
3338
3379
  }
3339
- /**
3340
- * Connect to the database
3341
- */
3342
3380
  async connect() {
3343
3381
  if (this.isConnected && this.knexInstance) {
3344
3382
  this.logger.warn?.("[Db] Already connected");
@@ -3347,14 +3385,13 @@ var Db = class {
3347
3385
  const client = this.detectClient(this.config.connectionString);
3348
3386
  if (!client) {
3349
3387
  throw new ParamError(
3350
- `Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
3388
+ "Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
3351
3389
  );
3352
3390
  }
3353
3391
  try {
3354
3392
  const connectionConfig = {
3355
3393
  connectionString: this.config.connectionString,
3356
3394
  family: 4
3357
- // Force IPv4 only (disable IPv6)
3358
3395
  };
3359
3396
  this.knexInstance = knex({
3360
3397
  client,
@@ -3370,7 +3407,9 @@ var Db = class {
3370
3407
  await this.testConnection();
3371
3408
  }
3372
3409
  this.isConnected = true;
3373
- this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
3410
+ this.logger.debug?.(
3411
+ `[Db] Connected to database "${this.config.name || this.config.connectionString}"`
3412
+ );
3374
3413
  } catch (error) {
3375
3414
  if (error instanceof ParamError) {
3376
3415
  throw error;
@@ -3379,9 +3418,6 @@ var Db = class {
3379
3418
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
3380
3419
  }
3381
3420
  }
3382
- /**
3383
- * Disconnect from the database
3384
- */
3385
3421
  async disconnect() {
3386
3422
  if (!this.knexInstance) {
3387
3423
  return;
@@ -3391,16 +3427,15 @@ var Db = class {
3391
3427
  this.knexInstance = null;
3392
3428
  this.isConnected = false;
3393
3429
  this.queriesLog = [];
3394
- this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
3430
+ this.logger.debug?.(
3431
+ `[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
3432
+ );
3395
3433
  } catch (error) {
3396
3434
  const errorMsg = this.getErrorMessage(error);
3397
3435
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
3398
3436
  throw error;
3399
3437
  }
3400
3438
  }
3401
- /**
3402
- * Extract error message from various error types
3403
- */
3404
3439
  getErrorMessage(error) {
3405
3440
  if (error instanceof AggregateError) {
3406
3441
  const errors = error.errors || [];
@@ -3425,9 +3460,11 @@ var Db = class {
3425
3460
  return `${code} (tried: ${addresses.join(", ")})`;
3426
3461
  }
3427
3462
  }
3428
- const uniqueMessages = [...new Set(errors.map((e) => {
3429
- return e instanceof Error ? e.message : String(e);
3430
- }))];
3463
+ const uniqueMessages = [
3464
+ ...new Set(
3465
+ errors.map((e) => e instanceof Error ? e.message : String(e))
3466
+ )
3467
+ ];
3431
3468
  if (uniqueMessages.length === 1) {
3432
3469
  return uniqueMessages[0];
3433
3470
  }
@@ -3436,28 +3473,25 @@ var Db = class {
3436
3473
  return error.message || "Multiple errors occurred";
3437
3474
  }
3438
3475
  if (error instanceof Error) {
3439
- const errorWithCode = error;
3440
- if (errorWithCode.code) {
3441
- return `${errorWithCode.code}: ${error.message || String(error)}`;
3476
+ const code = error.code;
3477
+ if (code) {
3478
+ return `${code}: ${error.message || String(error)}`;
3442
3479
  }
3443
3480
  return error.message || String(error);
3444
3481
  }
3445
3482
  if (typeof error === "string") {
3446
3483
  return error;
3447
3484
  }
3448
- if (error?.message) {
3485
+ if (error && typeof error === "object" && "message" in error) {
3449
3486
  const msg = String(error.message);
3450
- const errorWithCode = error;
3451
- if (errorWithCode.code) {
3452
- return `${errorWithCode.code}: ${msg}`;
3487
+ const code = error.code;
3488
+ if (code) {
3489
+ return `${code}: ${msg}`;
3453
3490
  }
3454
3491
  return msg;
3455
3492
  }
3456
3493
  return String(error) || "Unknown error";
3457
3494
  }
3458
- /**
3459
- * Test database connection
3460
- */
3461
3495
  async testConnection() {
3462
3496
  if (!this.knexInstance) {
3463
3497
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3473,9 +3507,6 @@ var Db = class {
3473
3507
  throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
3474
3508
  }
3475
3509
  }
3476
- /**
3477
- * Attach query profiler to log all queries
3478
- */
3479
3510
  attachProfiler() {
3480
3511
  if (!this.knexInstance) {
3481
3512
  return;
@@ -3485,7 +3516,7 @@ var Db = class {
3485
3516
  this.knexInstance.on("query", (query) => {
3486
3517
  query.__startTime = process.hrtime();
3487
3518
  });
3488
- this.knexInstance.on("query-response", (response, query) => {
3519
+ this.knexInstance.on("query-response", (_response, query) => {
3489
3520
  const [seconds, nanoseconds] = process.hrtime(query.__startTime);
3490
3521
  const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
3491
3522
  const logEntry = {
@@ -3500,15 +3531,9 @@ var Db = class {
3500
3531
  this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
3501
3532
  });
3502
3533
  }
3503
- /**
3504
- * Get query log (only available if profiling is enabled)
3505
- */
3506
3534
  getQueryLog() {
3507
3535
  return [...this.queriesLog];
3508
3536
  }
3509
- /**
3510
- * Check if a table exists
3511
- */
3512
3537
  async tableExists(tableName) {
3513
3538
  if (!this.knexInstance) {
3514
3539
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3520,65 +3545,28 @@ var Db = class {
3520
3545
  throw error;
3521
3546
  }
3522
3547
  }
3523
- /**
3524
- * Get the underlying Knex instance (for advanced usage)
3525
- */
3526
3548
  getKnex() {
3527
3549
  if (!this.knexInstance) {
3528
3550
  throw new Error("Db: Not connected. Call connect() first.");
3529
3551
  }
3530
3552
  return this.knexInstance;
3531
3553
  }
3532
- /**
3533
- * Get connection status
3534
- */
3535
3554
  isConnectedToDb() {
3536
3555
  return this.isConnected && this.knexInstance !== null;
3537
3556
  }
3538
- /**
3539
- * Initialize Db with context (connects and registers disconnect cleanup).
3540
- * Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
3541
- */
3542
- static async init(context, dbNameOrConnectionString) {
3543
- return dbFindAndConnect(context, dbNameOrConnectionString);
3544
- }
3545
3557
  };
3546
3558
  function capitalizeFirstLetter(str) {
3547
3559
  return str.charAt(0).toUpperCase() + str.slice(1);
3548
3560
  }
3549
- async function dbConnect(context, connectionString, name, dbProfile) {
3550
- const defs = {
3551
- testDbConnection: "boolean default true",
3552
- name: "string",
3553
- poolMin: "number default 2",
3554
- poolMax: "number default 10",
3555
- acquireConnectionTimeout: "number default 10000",
3556
- sslRejectUnauthorized: "boolean default false"
3557
- };
3558
- const paramsConfig = context.params.getAllForModule(defs);
3559
- const config2 = {
3560
- connectionString,
3561
- name: paramsConfig.name || name || "default",
3562
- testConnection: paramsConfig.testDbConnection,
3563
- profile: dbProfile ?? false,
3564
- pool: {
3565
- min: paramsConfig.poolMin,
3566
- max: paramsConfig.poolMax
3567
- },
3568
- acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
3569
- ssl: {
3570
- rejectUnauthorized: paramsConfig.sslRejectUnauthorized
3571
- },
3572
- logger: context.logger
3573
- };
3561
+ async function dbConnect(context, config2) {
3574
3562
  try {
3575
3563
  const db = new Db(config2);
3576
3564
  context.registerCleanup(async () => {
3577
3565
  await db.disconnect();
3578
- context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
3566
+ context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
3579
3567
  });
3580
3568
  await db.connect();
3581
- context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
3569
+ context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
3582
3570
  return db;
3583
3571
  } catch (error) {
3584
3572
  if (error instanceof ParamError) {
@@ -3588,52 +3576,266 @@ async function dbConnect(context, connectionString, name, dbProfile) {
3588
3576
  throw new ParamError(`[Db] connect error: ${errorMsg}`);
3589
3577
  }
3590
3578
  }
3591
- async function dbFindAndConnect(context, dbNameOrConnectionString) {
3592
- let dbName;
3593
- let dbConnectionString;
3594
- let dbProfile;
3595
- if (dbNameOrConnectionString) {
3596
- if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
3597
- dbName = void 0;
3598
- dbConnectionString = dbNameOrConnectionString;
3599
- } else {
3600
- dbName = dbNameOrConnectionString;
3601
- }
3602
- } else {
3603
- const defs = {
3604
- dbName: "string",
3605
- dbConnectionString: "string",
3606
- dbProfile: "boolean default false"
3579
+
3580
+ // src/s3/index.js
3581
+ import {
3582
+ S3Client,
3583
+ HeadBucketCommand,
3584
+ HeadObjectCommand,
3585
+ GetObjectCommand,
3586
+ PutObjectCommand,
3587
+ DeleteObjectCommand,
3588
+ CopyObjectCommand,
3589
+ ListObjectsV2Command,
3590
+ PutObjectTaggingCommand,
3591
+ GetObjectTaggingCommand
3592
+ } from "@aws-sdk/client-s3";
3593
+ var DEFAULT_PROFILE = "local";
3594
+ function capitalize(str) {
3595
+ if (!str) return str;
3596
+ return str.charAt(0).toUpperCase() + str.slice(1);
3597
+ }
3598
+ var S3 = class _S3 {
3599
+ /**
3600
+ * Build an S3 instance. Reads bucket profile from --bucket (default "local"),
3601
+ * then resolves per-profile params, builds the SDK client, and returns the
3602
+ * instance. Optionally pings the bucket once to verify reachability.
3603
+ */
3604
+ static async init(context, options = {}) {
3605
+ const profileDef = { bucket: "string" };
3606
+ const discovered = context?.params?.getAllForModule?.("s3", profileDef) ?? {};
3607
+ const profile = options.bucket ?? discovered.bucket ?? DEFAULT_PROFILE;
3608
+ const cap = capitalize(profile);
3609
+ const config2 = {
3610
+ profile,
3611
+ bucketName: options.bucketName ?? await context.params.get(`s3Bucket${cap}`, "string"),
3612
+ region: options.region ?? await context.params.get(`s3Region${cap}`, "string default us-east-1"),
3613
+ endpoint: options.endpoint ?? await context.params.get(`s3Endpoint${cap}`, "string"),
3614
+ forcePathStyle: options.forcePathStyle ?? await context.params.get(`s3ForcePathStyle${cap}`, "boolean default false"),
3615
+ accessKeyId: options.accessKeyId ?? await context.params.get(`s3AccessKeyId${cap}`, "string"),
3616
+ secretAccessKey: options.secretAccessKey ?? await context.params.get(`s3SecretAccessKey${cap}`, "string")
3607
3617
  };
3608
- const paramsConfig = context.params.getAllForModule(defs);
3609
- dbName = paramsConfig.dbName;
3610
- dbConnectionString = paramsConfig.dbConnectionString;
3611
- dbProfile = paramsConfig.dbProfile;
3612
- }
3613
- if (!dbName && !dbConnectionString) {
3614
- throw new ParamError("Db: either dbName or dbConnectionString must be specified");
3615
- }
3616
- if (dbName) {
3617
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3618
- dbConnectionString = await context.params.get(paramName, "string");
3619
- if (!dbConnectionString) {
3618
+ if (!config2.bucketName) {
3620
3619
  throw new ParamError(
3621
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3620
+ `S3: bucket name not configured for profile "${profile}" (set s3Bucket${cap} or S3_BUCKET_${profile.toUpperCase()})`
3622
3621
  );
3623
3622
  }
3623
+ const s3 = new _S3(context, config2);
3624
+ if (options.testBucket !== false) {
3625
+ try {
3626
+ await s3.bucketExists();
3627
+ context.logger?.debug?.(
3628
+ `[S3] profile="${profile}" bucket="${config2.bucketName}" reachable`
3629
+ );
3630
+ } catch (err) {
3631
+ context.logger?.warn?.(
3632
+ `[S3] profile="${profile}" bucket="${config2.bucketName}" reachability test failed: ${err?.message ?? err}`
3633
+ );
3634
+ }
3635
+ }
3636
+ return s3;
3637
+ }
3638
+ constructor(context, config2) {
3639
+ this.logger = context?.logger ?? console;
3640
+ this.profile = config2.profile;
3641
+ this.bucketName = config2.bucketName;
3642
+ this.region = config2.region;
3643
+ this.endpoint = config2.endpoint || null;
3644
+ const clientConfig = { region: config2.region };
3645
+ if (config2.endpoint) clientConfig.endpoint = config2.endpoint;
3646
+ if (config2.forcePathStyle) clientConfig.forcePathStyle = true;
3647
+ if (config2.accessKeyId && config2.secretAccessKey) {
3648
+ clientConfig.credentials = {
3649
+ accessKeyId: config2.accessKeyId,
3650
+ secretAccessKey: config2.secretAccessKey
3651
+ };
3652
+ }
3653
+ this.client = new S3Client(clientConfig);
3624
3654
  }
3625
- const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
3626
- return db;
3627
- }
3628
- async function dbInit(context, dbNameOrConnectionString) {
3629
- return await dbFindAndConnect(context, dbNameOrConnectionString);
3630
- }
3655
+ // ── info ────────────────────────────────────────────────────────────────
3656
+ getBucketName() {
3657
+ return this.bucketName;
3658
+ }
3659
+ getProfile() {
3660
+ return this.profile;
3661
+ }
3662
+ getRegion() {
3663
+ return this.region;
3664
+ }
3665
+ getEndpoint() {
3666
+ return this.endpoint;
3667
+ }
3668
+ // ── reachability ────────────────────────────────────────────────────────
3669
+ async bucketExists() {
3670
+ await this.client.send(new HeadBucketCommand({ Bucket: this.bucketName }));
3671
+ return true;
3672
+ }
3673
+ // ── HEAD / GET ──────────────────────────────────────────────────────────
3674
+ /** Returns null on 404; never throws for "missing". Other errors throw. */
3675
+ async headObject(key) {
3676
+ try {
3677
+ const out = await this.client.send(new HeadObjectCommand({
3678
+ Bucket: this.bucketName,
3679
+ Key: key
3680
+ }));
3681
+ return {
3682
+ etag: out.ETag,
3683
+ size: out.ContentLength,
3684
+ contentType: out.ContentType,
3685
+ lastModified: out.LastModified,
3686
+ metadata: out.Metadata,
3687
+ storageClass: out.StorageClass
3688
+ };
3689
+ } catch (err) {
3690
+ if (this._isNotFound(err)) return null;
3691
+ throw err;
3692
+ }
3693
+ }
3694
+ /** Returns { body: Readable, contentType, contentLength, etag, ... } or null on 404. */
3695
+ async getObject(key) {
3696
+ try {
3697
+ const out = await this.client.send(new GetObjectCommand({
3698
+ Bucket: this.bucketName,
3699
+ Key: key
3700
+ }));
3701
+ return {
3702
+ body: out.Body,
3703
+ contentType: out.ContentType,
3704
+ contentLength: out.ContentLength,
3705
+ etag: out.ETag,
3706
+ lastModified: out.LastModified,
3707
+ metadata: out.Metadata
3708
+ };
3709
+ } catch (err) {
3710
+ if (this._isNotFound(err)) return null;
3711
+ throw err;
3712
+ }
3713
+ }
3714
+ /** Buffers the whole object. Use only for small objects (manifests, JSON). */
3715
+ async getObjectBytes(key) {
3716
+ const obj = await this.getObject(key);
3717
+ if (!obj) return null;
3718
+ const chunks = [];
3719
+ for await (const chunk of obj.body) chunks.push(chunk);
3720
+ return { ...obj, body: Buffer.concat(chunks) };
3721
+ }
3722
+ /** Convenience for JSON manifests. Returns parsed object or null on 404. */
3723
+ async getJson(key) {
3724
+ const obj = await this.getObjectBytes(key);
3725
+ if (!obj) return null;
3726
+ return JSON.parse(obj.body.toString("utf8"));
3727
+ }
3728
+ // ── PUT ─────────────────────────────────────────────────────────────────
3729
+ async putObject({ key, body, contentType, contentLength, tags, metadata }) {
3730
+ const cmd = new PutObjectCommand({
3731
+ Bucket: this.bucketName,
3732
+ Key: key,
3733
+ Body: body,
3734
+ ...contentType && { ContentType: contentType },
3735
+ ...contentLength != null && { ContentLength: contentLength },
3736
+ ...metadata && { Metadata: metadata },
3737
+ ...tags && { Tagging: this._tagsToQuery(tags) }
3738
+ });
3739
+ return this.client.send(cmd);
3740
+ }
3741
+ /** Convenience for JSON manifests. */
3742
+ async putJson(key, value, opts = {}) {
3743
+ const json = JSON.stringify(value, null, opts.pretty ? 2 : 0);
3744
+ const body = Buffer.from(json, "utf8");
3745
+ return this.putObject({
3746
+ key,
3747
+ body,
3748
+ contentType: "application/json",
3749
+ contentLength: body.length,
3750
+ tags: opts.tags,
3751
+ metadata: opts.metadata
3752
+ });
3753
+ }
3754
+ // ── DELETE ──────────────────────────────────────────────────────────────
3755
+ async deleteObject(key) {
3756
+ return this.client.send(new DeleteObjectCommand({
3757
+ Bucket: this.bucketName,
3758
+ Key: key
3759
+ }));
3760
+ }
3761
+ // ── COPY (for migration: legacy → new bucket, or intra-bucket "rename") ─
3762
+ async copyObject({ sourceBucket, sourceKey, key, contentType, metadata, tags }) {
3763
+ const src = sourceBucket || this.bucketName;
3764
+ const cmd = new CopyObjectCommand({
3765
+ Bucket: this.bucketName,
3766
+ Key: key,
3767
+ CopySource: encodeURIComponent(`${src}/${sourceKey}`),
3768
+ ...contentType && {
3769
+ ContentType: contentType,
3770
+ MetadataDirective: "REPLACE"
3771
+ },
3772
+ ...metadata && {
3773
+ Metadata: metadata,
3774
+ MetadataDirective: "REPLACE"
3775
+ },
3776
+ ...tags && {
3777
+ Tagging: this._tagsToQuery(tags),
3778
+ TaggingDirective: "REPLACE"
3779
+ }
3780
+ });
3781
+ return this.client.send(cmd);
3782
+ }
3783
+ // ── LIST ────────────────────────────────────────────────────────────────
3784
+ async listObjects(prefix, { keysOnly = false, maxKeys = 1e3, continuationToken } = {}) {
3785
+ const out = await this.client.send(new ListObjectsV2Command({
3786
+ Bucket: this.bucketName,
3787
+ Prefix: prefix,
3788
+ MaxKeys: maxKeys,
3789
+ ContinuationToken: continuationToken
3790
+ }));
3791
+ const items = (out.Contents ?? []).map((o) => ({
3792
+ key: o.Key,
3793
+ size: o.Size,
3794
+ etag: o.ETag,
3795
+ lastModified: o.LastModified,
3796
+ storageClass: o.StorageClass
3797
+ }));
3798
+ return {
3799
+ items: keysOnly ? items.map((i) => i.key) : items,
3800
+ isTruncated: !!out.IsTruncated,
3801
+ nextContinuationToken: out.NextContinuationToken
3802
+ };
3803
+ }
3804
+ // ── TAGS (used for lifecycle rules, e.g. status=closed → Glacier IR) ────
3805
+ async putObjectTagging(key, tags) {
3806
+ return this.client.send(new PutObjectTaggingCommand({
3807
+ Bucket: this.bucketName,
3808
+ Key: key,
3809
+ Tagging: { TagSet: this._tagsToTagSet(tags) }
3810
+ }));
3811
+ }
3812
+ async getObjectTagging(key) {
3813
+ const out = await this.client.send(new GetObjectTaggingCommand({
3814
+ Bucket: this.bucketName,
3815
+ Key: key
3816
+ }));
3817
+ const tags = {};
3818
+ for (const t of out.TagSet ?? []) tags[t.Key] = t.Value;
3819
+ return tags;
3820
+ }
3821
+ // ── internals ───────────────────────────────────────────────────────────
3822
+ _isNotFound(err) {
3823
+ const status = err?.$metadata?.httpStatusCode;
3824
+ return status === 404 || err?.name === "NotFound" || err?.name === "NoSuchKey" || err?.Code === "NoSuchKey";
3825
+ }
3826
+ _tagsToQuery(tags) {
3827
+ return Object.entries(tags).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
3828
+ }
3829
+ _tagsToTagSet(tags) {
3830
+ return Object.entries(tags).map(([Key, Value]) => ({ Key, Value: String(Value) }));
3831
+ }
3832
+ };
3631
3833
 
3632
- // src/logger/index.ts
3834
+ // src/logger/index.js
3633
3835
  import chalk from "chalk";
3634
3836
  import util from "util";
3635
3837
 
3636
- // src/logger/transports.ts
3838
+ // src/logger/transports.js
3637
3839
  var ConsoleTransport = class {
3638
3840
  write(payload) {
3639
3841
  console.info(payload);
@@ -3653,7 +3855,7 @@ var ParentProcessTransport = class {
3653
3855
  }
3654
3856
  };
3655
3857
 
3656
- // src/logger/index.ts
3858
+ // src/logger/index.js
3657
3859
  var ALL_LEVELS = [
3658
3860
  "silly",
3659
3861
  "debug",
@@ -3726,6 +3928,7 @@ var Logger = class _Logger {
3726
3928
  }
3727
3929
  /**
3728
3930
  * Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
3931
+ * Params are tracked under the `logger` module for --showUsedParams.
3729
3932
  */
3730
3933
  static init(context, options) {
3731
3934
  const paramDefs = {
@@ -3739,7 +3942,7 @@ var Logger = class _Logger {
3739
3942
  progressWithTimes: "boolean default false",
3740
3943
  progressThrottleMs: "number"
3741
3944
  };
3742
- const discovered = context.params.getAllForModule(paramDefs);
3945
+ const discovered = context.params.getAllForModule("logger", paramDefs);
3743
3946
  const config2 = { ...discovered, ...options };
3744
3947
  const logger = new _Logger(context, config2);
3745
3948
  context.logger = logger;
@@ -3935,9 +4138,9 @@ var Logger = class _Logger {
3935
4138
  }
3936
4139
  };
3937
4140
 
3938
- // src/init/index.ts
4141
+ // src/init/index.js
3939
4142
  import { EventEmitter } from "events";
3940
- function extractComponentOptions(opts, componentName) {
4143
+ function extractComponentOptions(opts, _componentName) {
3941
4144
  const reservedKeys = ["overrides", "defaults", "modules"];
3942
4145
  const componentOptions = {};
3943
4146
  for (const [key, value] of Object.entries(opts)) {
@@ -3982,7 +4185,10 @@ function setupContext(opts = {}) {
3982
4185
  return setup(opts);
3983
4186
  }
3984
4187
 
3985
- // src/utils/core-utils.ts
4188
+ // src/tasks/index.js
4189
+ import os3 from "os";
4190
+
4191
+ // src/utils/core-utils.js
3986
4192
  function sleepMs(ms) {
3987
4193
  return new Promise((resolve2) => setTimeout(resolve2, ms));
3988
4194
  }
@@ -3991,8 +4197,82 @@ function toJsonColumn(value) {
3991
4197
  return JSON.stringify(value);
3992
4198
  }
3993
4199
 
3994
- // src/tasks/taskUtils.ts
4200
+ // src/tasks/servicesRegistry.js
4201
+ import os from "os";
4202
+
4203
+ // src/tasks/taskUtils.js
3995
4204
  import { randomUUID } from "crypto";
4205
+
4206
+ // src/tasks/time-matcher.js
4207
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4208
+ function resolveAsterisks(field, range) {
4209
+ return field.includes("*") ? field.replace("*", range) : field;
4210
+ }
4211
+ function resolveRanges(field) {
4212
+ const regex = /(\d+)-(\d+)/;
4213
+ let current = field;
4214
+ while (true) {
4215
+ const match = regex.exec(current);
4216
+ if (!match) break;
4217
+ const raw = match[0];
4218
+ let first = Number(match[1]);
4219
+ let last = Number(match[2]);
4220
+ if (last < first) {
4221
+ [first, last] = [last, first];
4222
+ }
4223
+ const values = [];
4224
+ for (let i = first; i <= last; i += 1) {
4225
+ values.push(i);
4226
+ }
4227
+ current = current.replace(raw, values.join(","));
4228
+ }
4229
+ return current;
4230
+ }
4231
+ function resolveSteps(field) {
4232
+ const match = /^(.+)\/(\d+)$/.exec(field);
4233
+ if (!match) return field;
4234
+ const base = match[1];
4235
+ const step = Number(match[2]);
4236
+ if (!Number.isFinite(step) || step <= 0) return field;
4237
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4238
+ }
4239
+ function convertPattern(pattern) {
4240
+ const parts = pattern.trim().split(/\s+/);
4241
+ if (parts.length !== 6) {
4242
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4243
+ }
4244
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4245
+ }
4246
+ function fieldMatches(field, value) {
4247
+ const allowed = field.split(",").map((v) => Number(v));
4248
+ return allowed.includes(value);
4249
+ }
4250
+ function matchesParsedPattern(parsed, date) {
4251
+ 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());
4252
+ }
4253
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4254
+ const parsed = convertPattern(pattern);
4255
+ return matchesParsedPattern(parsed, date);
4256
+ }
4257
+ var MS_PER_SECOND = 1e3;
4258
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
4259
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
4260
+ const parsed = convertPattern(pattern);
4261
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
4262
+ const end = t + maxSearchMs;
4263
+ while (t <= end) {
4264
+ const date = new Date(t);
4265
+ if (matchesParsedPattern(parsed, date)) {
4266
+ return date;
4267
+ }
4268
+ t += MS_PER_SECOND;
4269
+ }
4270
+ throw new Error(
4271
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
4272
+ );
4273
+ }
4274
+
4275
+ // src/tasks/taskUtils.js
3996
4276
  function getDb(context) {
3997
4277
  const db = context.db;
3998
4278
  if (!db) {
@@ -4000,105 +4280,117 @@ function getDb(context) {
4000
4280
  }
4001
4281
  return db;
4002
4282
  }
4003
- function queueToTableNames(queue) {
4004
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4005
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4006
- }
4283
+ function queueToTableNames(queueName) {
4284
+ return {
4285
+ tasksTable: queueName,
4286
+ historyTable: `${queueName}_history`,
4287
+ registryTable: `${queueName}_services_registry`
4288
+ };
4289
+ }
4290
+ function defineTasksTable(t, db, tableNameForIndex) {
4291
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4292
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4293
+ t.timestamp("started_at");
4294
+ t.timestamp("completed_at");
4295
+ t.integer("priority").notNullable().defaultTo(50);
4296
+ t.text("schedule");
4297
+ t.timestamp("next_run_at").defaultTo(null);
4298
+ t.timestamp("past_due").defaultTo(null);
4299
+ t.text("name").notNullable();
4300
+ t.text("opid");
4301
+ t.json("params");
4302
+ t.text("service_group");
4303
+ t.integer("instance_number");
4304
+ t.text("service_name");
4305
+ t.text("server_name");
4306
+ t.text("status").notNullable().defaultTo("idle");
4307
+ t.timestamp("status_changed_at").defaultTo(null);
4308
+ t.text("progress");
4309
+ t.boolean("success");
4310
+ t.json("results");
4311
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
4312
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
4313
+ }
4314
+ function taskHistoryInsertFromQueueRow(row, overrides) {
4315
+ const { id, ...snapshot } = row;
4316
+ void id;
4007
4317
  return {
4008
- tasksTable: queue,
4009
- historyTable: `${queue}_history`
4318
+ ...snapshot,
4319
+ ...overrides
4010
4320
  };
4011
4321
  }
4012
4322
  async function ensureTaskTables(context, options = {}) {
4013
- const queue = options.queue ?? "tasks";
4323
+ const queueName = options.queueName ?? "tasks";
4014
4324
  const recreate = options.recreate ?? false;
4015
4325
  const db = getDb(context);
4016
- const { tasksTable, historyTable } = queueToTableNames(queue);
4326
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
4017
4327
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
4018
4328
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
4329
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
4019
4330
  if (recreate) {
4020
4331
  await db.schema.dropTableIfExists(historyTable);
4021
4332
  await db.schema.dropTableIfExists(tasksTable);
4333
+ await db.schema.dropTableIfExists(registryTable);
4022
4334
  }
4023
4335
  if (needsTasks) {
4024
4336
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
4025
4337
  await db.schema.createTable(tasksTable, (t) => {
4026
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4027
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4028
- t.timestamp("started_at");
4029
- t.timestamp("completed_at");
4030
- t.integer("priority").notNullable().defaultTo(0);
4031
- t.text("schedule");
4032
- t.timestamp("past_due").defaultTo(null);
4033
- t.text("target").notNullable();
4034
- t.text("task").notNullable();
4035
- t.json("params");
4036
- t.text("opid");
4037
- t.timestamp("paused_at").defaultTo(null);
4038
- t.text("progress");
4039
- t.boolean("success");
4040
- t.json("results");
4041
- });
4042
- await db.schema.alterTable(tasksTable, (t) => {
4043
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
4044
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
4045
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
4046
- });
4047
- }
4048
- const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
4049
- if (!tasksHasOpid) {
4050
- await db.schema.alterTable(tasksTable, (t) => {
4051
- t.text("opid");
4052
- });
4053
- }
4054
- const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
4055
- if (!tasksHasPausedAt) {
4056
- await db.schema.alterTable(tasksTable, (t) => {
4057
- t.timestamp("paused_at").defaultTo(null);
4338
+ defineTasksTable(t, db, tasksTable);
4058
4339
  });
4059
4340
  }
4060
4341
  if (needsHistory) {
4061
4342
  await db.schema.createTable(historyTable, (t) => {
4062
- t.uuid("id").notNullable();
4063
- t.timestamp("created_at").notNullable();
4064
- t.timestamp("started_at");
4065
- t.timestamp("completed_at");
4066
- t.integer("priority").notNullable().defaultTo(0);
4067
- t.text("schedule");
4068
- t.timestamp("past_due").defaultTo(null);
4069
- t.text("target").notNullable();
4070
- t.text("task").notNullable();
4071
- t.json("params");
4072
- t.text("opid");
4073
- t.text("progress");
4074
- t.boolean("success");
4075
- t.json("results");
4076
- });
4077
- await db.schema.alterTable(historyTable, (t) => {
4078
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
4079
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
4343
+ defineTasksTable(t, db, historyTable);
4080
4344
  });
4081
4345
  }
4082
- const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
4083
- if (!historyHasOpid) {
4084
- await db.schema.alterTable(historyTable, (t) => {
4085
- t.text("opid");
4346
+ if (needsRegistry) {
4347
+ await db.schema.createTable(registryTable, (t) => {
4348
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4349
+ t.text("queue_name").notNullable();
4350
+ t.text("service_group").notNullable();
4351
+ t.integer("instance_number").notNullable().defaultTo(1);
4352
+ t.text("service_name").notNullable();
4353
+ t.text("server_name").notNullable();
4354
+ t.integer("pid");
4355
+ t.json("metadata");
4356
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4357
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
4358
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
4359
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
4360
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
4086
4361
  });
4087
4362
  }
4088
4363
  }
4089
4364
  async function enqueueTask(context, options) {
4090
4365
  const db = getDb(context);
4091
- const queue = options.queue ?? "tasks";
4092
- const { tasksTable } = queueToTableNames(queue);
4366
+ const queueName = options.queueName ?? "tasks";
4367
+ const { tasksTable } = queueToTableNames(queueName);
4093
4368
  const id = randomUUID();
4369
+ const name = options.name ?? options.task;
4370
+ if (!name) {
4371
+ throw new Error("enqueueTask: name (or task) is required");
4372
+ }
4373
+ const schedule = options.schedule?.trim() ? options.schedule : null;
4374
+ let nextRunAt = null;
4375
+ if (options.nextRunAt !== void 0) {
4376
+ nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
4377
+ } else if (schedule) {
4378
+ nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
4379
+ }
4094
4380
  await db(tasksTable).insert({
4095
4381
  id,
4096
- target: options.target,
4097
- task: options.task,
4382
+ name,
4098
4383
  params: toJsonColumn(options.params ?? null),
4099
4384
  opid: options.opid ?? null,
4100
- priority: options.priority ?? 0,
4101
- schedule: options.schedule ?? null
4385
+ priority: options.priority ?? 50,
4386
+ schedule,
4387
+ next_run_at: nextRunAt,
4388
+ service_group: options.serviceGroup ?? null,
4389
+ instance_number: options.instanceNumber ?? null,
4390
+ service_name: options.serviceName ?? null,
4391
+ server_name: options.serverName ?? null,
4392
+ status: "idle",
4393
+ status_changed_at: db.fn.now()
4102
4394
  });
4103
4395
  return id;
4104
4396
  }
@@ -4109,57 +4401,395 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
4109
4401
  });
4110
4402
  }
4111
4403
 
4112
- // src/tasks/taskLogs.ts
4113
- function getLogsState(context) {
4114
- const holder = context;
4115
- if (holder.__tasksLogsState) return holder.__tasksLogsState;
4116
- const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
4117
- const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
4118
- const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
4119
- const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
4120
- const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4121
- const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4122
- const errorDb = new FileDatabase({
4123
- basePath,
4124
- namespace,
4125
- tableName: errorTableName,
4126
- versioned: true,
4127
- useMetadata: true,
4128
- maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4129
- pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4130
- logger: holder.logger
4131
- });
4132
- const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4133
- const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4134
- if (!enabled) {
4135
- const disabledState = {
4136
- db: null,
4137
- errorDb,
4138
- queue: Promise.resolve(),
4139
- initialized: true,
4140
- errorInitialized: false
4141
- };
4142
- holder.__tasksLogsState = disabledState;
4143
- return disabledState;
4404
+ // src/tasks/servicesRegistry.js
4405
+ function getDb2(context) {
4406
+ const db = context.db;
4407
+ if (!db) {
4408
+ throw new Error("Services registry requires context.db");
4144
4409
  }
4145
- const db = new FileDatabase({
4146
- basePath,
4147
- namespace,
4148
- tableName,
4149
- versioned: true,
4150
- useMetadata: true,
4151
- maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4152
- pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4153
- logger: holder.logger
4154
- });
4155
- const state = {
4156
- db,
4157
- errorDb,
4158
- queue: Promise.resolve(),
4159
- initialized: false,
4160
- errorInitialized: false
4161
- };
4162
- holder.__tasksLogsState = state;
4410
+ return db;
4411
+ }
4412
+ function parseMetadataColumn(value) {
4413
+ if (!value) return {};
4414
+ if (typeof value === "object" && !Array.isArray(value)) return value;
4415
+ if (typeof value === "string") {
4416
+ try {
4417
+ const p = JSON.parse(value);
4418
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
4419
+ } catch {
4420
+ return {};
4421
+ }
4422
+ }
4423
+ return {};
4424
+ }
4425
+ var DEFAULT_GROUP_MAX_INSTANCES = {
4426
+ intake: 1,
4427
+ harvest: 1,
4428
+ harvester: 0,
4429
+ loader: 0,
4430
+ photos: 0,
4431
+ photosprocessor: 0,
4432
+ ingest: 0
4433
+ };
4434
+ function sanitizeNamePart(raw) {
4435
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4436
+ return s.slice(0, 80) || "runner";
4437
+ }
4438
+ function resolveMaxInstances(serviceGroup, override) {
4439
+ if (override !== void 0 && Number.isFinite(override)) {
4440
+ return Math.max(0, Math.floor(Number(override)));
4441
+ }
4442
+ const g = serviceGroup.trim().toLowerCase();
4443
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
4444
+ }
4445
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
4446
+ const cutoff = new Date(Date.now() - staleMs);
4447
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
4448
+ if (excludeRowId) {
4449
+ q = q.whereNot("id", excludeRowId);
4450
+ }
4451
+ const row = await q.count("id as count").first();
4452
+ return Number(row?.count ?? 0);
4453
+ }
4454
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
4455
+ const cutoff = new Date(Date.now() - staleMs);
4456
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
4457
+ const set = /* @__PURE__ */ new Set();
4458
+ for (const r of rows) {
4459
+ const n = Number(r.instance_number);
4460
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
4461
+ }
4462
+ return set;
4463
+ }
4464
+ function isUniqueViolation(error) {
4465
+ const code = error?.code ?? error?.errno;
4466
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
4467
+ }
4468
+ function buildMetadata(options) {
4469
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
4470
+ if (options.target) {
4471
+ base.runnerTarget = options.target;
4472
+ }
4473
+ return toJsonColumn(Object.keys(base).length ? base : null);
4474
+ }
4475
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
4476
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
4477
+ const e = Math.max(1, Math.floor(Number(explicit)));
4478
+ if (occupied.has(e)) {
4479
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
4480
+ }
4481
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
4482
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
4483
+ }
4484
+ return e;
4485
+ }
4486
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
4487
+ for (let n = 1; n <= cap; n++) {
4488
+ if (!occupied.has(n)) return n;
4489
+ }
4490
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
4491
+ }
4492
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
4493
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
4494
+ }
4495
+ async function registerInServicesRegistry(context, options) {
4496
+ const db = getDb2(context);
4497
+ const registryTable = queueToTableNames(options.queueName).registryTable;
4498
+ const serviceGroup = options.serviceGroup.trim();
4499
+ if (!serviceGroup) {
4500
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
4501
+ }
4502
+ const serverName = os.hostname();
4503
+ const pid = typeof process.pid === "number" ? process.pid : null;
4504
+ const meta = buildMetadata(options);
4505
+ const groupBase = sanitizeNamePart(serviceGroup);
4506
+ const hostBase = sanitizeNamePart(serverName);
4507
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
4508
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
4509
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
4510
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
4511
+ if (options.enforceMaxInstances) {
4512
+ throw new Error(msg);
4513
+ }
4514
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
4515
+ }
4516
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
4517
+ const cutoff = new Date(Date.now() - options.staleMs);
4518
+ const MAX_ATTEMPTS = 8;
4519
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
4520
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
4521
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
4522
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
4523
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
4524
+ if (existing) {
4525
+ const lastSeen = new Date(existing.last_seen_at);
4526
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
4527
+ if (isAlive) {
4528
+ if (options.serviceName?.trim()) {
4529
+ throw new Error(
4530
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
4531
+ );
4532
+ }
4533
+ context.logger.warn?.(
4534
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
4535
+ );
4536
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
4537
+ throw new Error(
4538
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
4539
+ );
4540
+ }
4541
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
4542
+ continue;
4543
+ }
4544
+ await db(registryTable).where({ id: existing.id }).update({
4545
+ server_name: serverName,
4546
+ pid,
4547
+ metadata: meta,
4548
+ service_group: serviceGroup,
4549
+ instance_number: instanceNumber,
4550
+ last_seen_at: db.fn.now()
4551
+ });
4552
+ const reg = {
4553
+ serviceName: serviceNameRaw,
4554
+ serviceGroup,
4555
+ queueName: options.queueName,
4556
+ target: options.target,
4557
+ rowId: String(existing.id),
4558
+ registryTable,
4559
+ instanceNumber
4560
+ };
4561
+ context.servicesRegistry = reg;
4562
+ context.runnerHeartbeat = reg;
4563
+ context.logger.info?.(
4564
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
4565
+ );
4566
+ return reg;
4567
+ }
4568
+ try {
4569
+ const rows = await db(registryTable).insert({
4570
+ queue_name: options.queueName,
4571
+ service_group: serviceGroup,
4572
+ instance_number: instanceNumber,
4573
+ service_name: serviceNameRaw,
4574
+ server_name: serverName,
4575
+ pid,
4576
+ metadata: meta,
4577
+ last_seen_at: db.fn.now(),
4578
+ created_at: db.fn.now()
4579
+ }).returning(["id", "service_name"]);
4580
+ const row = Array.isArray(rows) ? rows[0] : rows;
4581
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
4582
+ if (!rowId) {
4583
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
4584
+ rowId = again?.id != null ? String(again.id) : "";
4585
+ }
4586
+ if (!rowId) continue;
4587
+ const regNew = {
4588
+ serviceName: String(row?.service_name ?? serviceNameRaw),
4589
+ serviceGroup,
4590
+ queueName: options.queueName,
4591
+ target: options.target,
4592
+ rowId,
4593
+ registryTable,
4594
+ instanceNumber
4595
+ };
4596
+ context.servicesRegistry = regNew;
4597
+ context.runnerHeartbeat = regNew;
4598
+ context.logger.info?.(
4599
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
4600
+ );
4601
+ return regNew;
4602
+ } catch (error) {
4603
+ if (!isUniqueViolation(error)) {
4604
+ throw error;
4605
+ }
4606
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
4607
+ }
4608
+ }
4609
+ throw new Error(
4610
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
4611
+ );
4612
+ }
4613
+ async function touchServicesRegistry(context, registration) {
4614
+ const db = getDb2(context);
4615
+ const serverName = os.hostname();
4616
+ const pid = typeof process.pid === "number" ? process.pid : null;
4617
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4618
+ last_seen_at: db.fn.now(),
4619
+ server_name: serverName,
4620
+ pid
4621
+ });
4622
+ }
4623
+ async function updateServicesRegistryMetadata(context, registration, patch) {
4624
+ const db = getDb2(context);
4625
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
4626
+ const prev = parseMetadataColumn(row?.metadata);
4627
+ const merged = { ...prev, ...patch };
4628
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4629
+ metadata: toJsonColumn(merged),
4630
+ last_seen_at: db.fn.now()
4631
+ });
4632
+ context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
4633
+ }
4634
+ async function unregisterServicesRegistry(context, registration) {
4635
+ const db = getDb2(context);
4636
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
4637
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
4638
+ }
4639
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
4640
+ const db = getDb2(context);
4641
+ const staleMs = options.staleMs ?? 6e4;
4642
+ const cutoff = new Date(Date.now() - staleMs);
4643
+ const table = queueToTableNames(options.queueName).registryTable;
4644
+ let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
4645
+ if (options.serviceGroup?.trim()) {
4646
+ q = q.where({ service_group: options.serviceGroup.trim() });
4647
+ }
4648
+ return await q;
4649
+ }
4650
+
4651
+ // src/tasks/taskLogs.js
4652
+ import path4 from "path";
4653
+ function getLogsState(context) {
4654
+ const holder = context;
4655
+ if (holder.__tasksLogsState) return holder.__tasksLogsState;
4656
+ const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
4657
+ const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
4658
+ const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
4659
+ const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
4660
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4661
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4662
+ const errorDb = new FileDatabase({
4663
+ basePath,
4664
+ namespace,
4665
+ tableName: errorTableName,
4666
+ versioned: true,
4667
+ useMetadata: true,
4668
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4669
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4670
+ logger: holder.logger
4671
+ });
4672
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4673
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4674
+ if (!enabled) {
4675
+ const disabledState = {
4676
+ db: null,
4677
+ errorDb,
4678
+ queue: Promise.resolve(),
4679
+ initialized: true,
4680
+ errorInitialized: false
4681
+ };
4682
+ holder.__tasksLogsState = disabledState;
4683
+ return disabledState;
4684
+ }
4685
+ const db = new FileDatabase({
4686
+ basePath,
4687
+ namespace,
4688
+ tableName,
4689
+ versioned: true,
4690
+ useMetadata: true,
4691
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4692
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4693
+ logger: holder.logger
4694
+ });
4695
+ const state = {
4696
+ db,
4697
+ errorDb,
4698
+ queue: Promise.resolve(),
4699
+ initialized: false,
4700
+ errorInitialized: false
4701
+ };
4702
+ holder.__tasksLogsState = state;
4703
+ return state;
4704
+ }
4705
+ function ipcLogTargetKey(target) {
4706
+ const bp = target.basePath ?? "";
4707
+ const ns = target.namespace ?? "";
4708
+ return `${bp}::${ns}::${target.tableName}`;
4709
+ }
4710
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
4711
+ const seg = (s) => {
4712
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
4713
+ return t.length ? t : "x";
4714
+ };
4715
+ return `${seg(source)}/${seg(resource)}`;
4716
+ }
4717
+ async function readTaskIpcLogsSnapshot(context, options) {
4718
+ const holder = context;
4719
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
4720
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
4721
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
4722
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
4723
+ const fd = new FileDatabase({
4724
+ basePath,
4725
+ namespace,
4726
+ tableName,
4727
+ versioned: true,
4728
+ useMetadata: true,
4729
+ maxVersions: 30,
4730
+ pageSize: 2e3,
4731
+ logger: holder.logger
4732
+ });
4733
+ const versions = await fd.getVersions();
4734
+ if (versions.length === 0) {
4735
+ return { records: [], latestTs: null };
4736
+ }
4737
+ const latest = versions[versions.length - 1];
4738
+ const raw = await fd.read({ version: latest });
4739
+ const arr = Array.isArray(raw) ? raw : [];
4740
+ let filtered = arr;
4741
+ if (options.afterTs && String(options.afterTs).trim()) {
4742
+ const cut = String(options.afterTs).trim();
4743
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
4744
+ }
4745
+ let latestTs = null;
4746
+ for (const r of filtered) {
4747
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
4748
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
4749
+ }
4750
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
4751
+ const maxReturn = incremental ? 1e4 : tail;
4752
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
4753
+ return { records: sliced, latestTs };
4754
+ }
4755
+ function resolveIpcFileLogsDir(context, target) {
4756
+ const holder = context;
4757
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4758
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4759
+ const segments = target.tableName.split("/").filter(Boolean);
4760
+ return path4.resolve(basePath, namespace, ...segments);
4761
+ }
4762
+ function getLogsStateForTarget(context, target) {
4763
+ const holder = context;
4764
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4765
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4766
+ if (!enabled) return null;
4767
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
4768
+ const map = holder.__tasksLogsTargetStates;
4769
+ const key = ipcLogTargetKey(target);
4770
+ if (map.has(key)) return map.get(key);
4771
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4772
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4773
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4774
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4775
+ const db = new FileDatabase({
4776
+ basePath,
4777
+ namespace,
4778
+ tableName: target.tableName,
4779
+ versioned: true,
4780
+ useMetadata: true,
4781
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4782
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4783
+ logger: holder.logger
4784
+ });
4785
+ const state = {
4786
+ db,
4787
+ errorDb: null,
4788
+ queue: Promise.resolve(),
4789
+ initialized: false,
4790
+ errorInitialized: false
4791
+ };
4792
+ map.set(key, state);
4163
4793
  return state;
4164
4794
  }
4165
4795
  function isErrorPayload(payload) {
@@ -4181,14 +4811,26 @@ function buildLogRecord(task, payload) {
4181
4811
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4182
4812
  opid: task.opid ?? null,
4183
4813
  taskId: task.id,
4184
- taskName: task.task,
4185
- target: task.target,
4814
+ taskName: task.name,
4815
+ target: task.service_group,
4186
4816
  source: typeof params.source === "string" ? params.source : null,
4187
4817
  resource: typeof params.resource === "string" ? params.resource : null,
4188
4818
  payload
4189
4819
  };
4190
4820
  }
4191
- function appendTaskIpcLog(context, task, payload) {
4821
+ function appendTaskIpcLog(context, task, payload, target) {
4822
+ if (target) {
4823
+ const state2 = getLogsStateForTarget(context, target);
4824
+ if (!state2?.db) return;
4825
+ const record2 = buildLogRecord(task, payload);
4826
+ state2.queue = state2.queue.then(async () => {
4827
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
4828
+ state2.initialized = true;
4829
+ }).catch((error) => {
4830
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
4831
+ });
4832
+ return;
4833
+ }
4192
4834
  const state = getLogsState(context);
4193
4835
  if (!state.db && !state.errorDb) return;
4194
4836
  const record = buildLogRecord(task, payload);
@@ -4205,84 +4847,293 @@ function appendTaskIpcLog(context, task, payload) {
4205
4847
  context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
4206
4848
  });
4207
4849
  }
4208
-
4209
- // src/tasks/time-matcher.ts
4210
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4211
- function resolveAsterisks(field, range) {
4212
- return field.includes("*") ? field.replace("*", range) : field;
4213
- }
4214
- function resolveRanges(field) {
4215
- const regex = /(\d+)-(\d+)/;
4216
- let current = field;
4217
- while (true) {
4218
- const match = regex.exec(current);
4219
- if (!match) break;
4220
- const raw = match[0];
4221
- let first = Number(match[1]);
4222
- let last = Number(match[2]);
4223
- if (last < first) {
4224
- [first, last] = [last, first];
4225
- }
4226
- const values = [];
4227
- for (let i = first; i <= last; i += 1) {
4228
- values.push(i);
4850
+ async function flushTaskIpcLogs(context) {
4851
+ const holder = context;
4852
+ const promises = [];
4853
+ if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
4854
+ const map = holder.__tasksLogsTargetStates;
4855
+ if (map) {
4856
+ for (const s of map.values()) {
4857
+ if (s.queue) promises.push(s.queue);
4229
4858
  }
4230
- current = current.replace(raw, values.join(","));
4231
4859
  }
4232
- return current;
4233
- }
4234
- function resolveSteps(field) {
4235
- const match = /^(.+)\/(\d+)$/.exec(field);
4236
- if (!match) return field;
4237
- const base = match[1];
4238
- const step = Number(match[2]);
4239
- if (!Number.isFinite(step) || step <= 0) return field;
4240
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4241
- }
4242
- function convertPattern(pattern) {
4243
- const parts = pattern.trim().split(/\s+/);
4244
- if (parts.length !== 6) {
4245
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4246
- }
4247
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4248
- }
4249
- function fieldMatches(field, value) {
4250
- const allowed = field.split(",").map((v) => Number(v));
4251
- return allowed.includes(value);
4252
- }
4253
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4254
- const parsed = convertPattern(pattern);
4255
- 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());
4860
+ await Promise.all(promises);
4256
4861
  }
4257
4862
 
4258
- // src/tasks/TaskMaster.ts
4259
- var TaskMaster = class {
4260
- context;
4261
- task;
4863
+ // src/tasks/AbstractTask.js
4864
+ var AbstractTask = class _AbstractTask {
4865
+ /**
4866
+ * Whether `send-task` should wait for completion (and print a result
4867
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
4868
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
4869
+ *
4870
+ * @type {boolean}
4871
+ */
4872
+ static defaultWaitForResult = false;
4873
+ /**
4874
+ * @param {object} context Runner context (db, logger, params, emitter...).
4875
+ * @param {object} task Task row as claimed from the queue.
4876
+ */
4262
4877
  constructor(context, task) {
4263
4878
  this.context = context;
4264
4879
  this.task = task;
4265
4880
  }
4881
+ /**
4882
+ * Return a short reason string when the task should be deferred (e.g. "locked
4883
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
4884
+ *
4885
+ * @returns {string | false | Promise<string | false>}
4886
+ */
4266
4887
  cantRunReason() {
4267
4888
  return false;
4268
4889
  }
4890
+ /**
4891
+ * Called by the runner when a stop has been requested. Subclasses running
4892
+ * long loops should flip a flag here and check it between iterations.
4893
+ *
4894
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
4895
+ */
4269
4896
  requestStop(_allowanceMs) {
4270
4897
  }
4898
+ /**
4899
+ * Perform the task. Must be implemented by subclasses.
4900
+ *
4901
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
4902
+ * Updates the DB `progress` column. Accepts any serializable value;
4903
+ * strings are stored verbatim, objects are JSON-stringified.
4904
+ * @returns {Promise<{ success: boolean, results: unknown }>}
4905
+ */
4906
+ async run(_reportProgress) {
4907
+ throw new Error("AbstractTask.run must be implemented by subclass");
4908
+ }
4909
+ /**
4910
+ * Resolve a complete row payload for this task — envelope fields (queue,
4911
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
4912
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
4913
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
4914
+ *
4915
+ * const payload = await TaskClass.resolveParams(context, { name });
4916
+ * await enqueueTask(context, payload);
4917
+ *
4918
+ * Validation failures throw {@link ParamError} so the script aborts before
4919
+ * a malformed row hits the DB.
4920
+ *
4921
+ * @param {object} context
4922
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
4923
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
4924
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
4925
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
4926
+ * @returns {Promise<object>}
4927
+ */
4928
+ static async resolveParams(context, overrides = {}) {
4929
+ const main = _AbstractTask._resolveMainFields(context, overrides);
4930
+ const params = await this.resolveCustomParams(context, overrides);
4931
+ return { ...main, params };
4932
+ }
4933
+ /**
4934
+ * Resolve the inner JSON blob stored in the `params` column. Default
4935
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
4936
+ * overlaid with `overrides.params` when supplied; returns `null` when
4937
+ * neither is provided.
4938
+ *
4939
+ * Subclasses with typed fields should override and call
4940
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
4941
+ * resolution, then validate and throw {@link ParamError} on bad input.
4942
+ *
4943
+ * @param {object} context
4944
+ * @param {Record<string, unknown>} [overrides]
4945
+ * @returns {Promise<object|null>}
4946
+ */
4947
+ static async resolveCustomParams(context, overrides = {}) {
4948
+ return _AbstractTask._defaultParamsBlob(context, overrides);
4949
+ }
4950
+ /**
4951
+ * Read main task envelope fields from `context.params` (CLI/env), with
4952
+ * any matching key on `overrides` taking precedence. Internal; called by
4953
+ * {@link AbstractTask.resolveParams}.
4954
+ *
4955
+ * @param {object} context
4956
+ * @param {Record<string, unknown>} [overrides]
4957
+ * @returns {object}
4958
+ */
4959
+ static _resolveMainFields(context, overrides = {}) {
4960
+ const defs = {
4961
+ queueName: "string default tasks",
4962
+ priority: "number default 50",
4963
+ serviceGroup: "string",
4964
+ serviceName: "string",
4965
+ instanceNumber: "number",
4966
+ serverName: "string",
4967
+ opid: "string",
4968
+ schedule: "string"
4969
+ };
4970
+ const cli = context.params.getAllForModule("task-envelope", defs);
4971
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
4972
+ if (!name) {
4973
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
4974
+ }
4975
+ let instanceNumber;
4976
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
4977
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
4978
+ const n = Number(rawInstance);
4979
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
4980
+ throw new ParamError("--instanceNumber must be a positive integer when set");
4981
+ }
4982
+ instanceNumber = n;
4983
+ } else {
4984
+ instanceNumber = null;
4985
+ }
4986
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
4987
+ const priority = Number(priorityRaw);
4988
+ if (!Number.isFinite(priority)) {
4989
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
4990
+ }
4991
+ return {
4992
+ name,
4993
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
4994
+ priority,
4995
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
4996
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
4997
+ instanceNumber,
4998
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
4999
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
5000
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
5001
+ nextRunAt: overrides.nextRunAt ?? null
5002
+ };
5003
+ }
5004
+ /**
5005
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
5006
+ * object), then overlays `overrides.params` on top. Returns `null` when
5007
+ * neither is provided.
5008
+ *
5009
+ * @param {object} context
5010
+ * @param {Record<string, unknown>} [overrides]
5011
+ * @returns {object|null}
5012
+ */
5013
+ static _defaultParamsBlob(context, overrides = {}) {
5014
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
5015
+ const fromJson = parseParamsJson(cli.paramsJson);
5016
+ const fromOverride = pickParamsObject(overrides);
5017
+ if (!fromJson && !fromOverride) return null;
5018
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
5019
+ }
5020
+ /**
5021
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
5022
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
5023
+ * merges them with explicit `overrides.params` in increasing priority:
5024
+ *
5025
+ * typed CLI flags → --paramsJson → overrides.params
5026
+ *
5027
+ * Undefined values are dropped so defaults declared in `defs` aren't
5028
+ * overwritten by missing-flag noise. Returns the merged object; the
5029
+ * caller is responsible for validation and throwing `ParamError`.
5030
+ *
5031
+ * @param {object} context
5032
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
5033
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
5034
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
5035
+ * @returns {Record<string, unknown>}
5036
+ */
5037
+ static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
5038
+ const fullDefs = { ...defs, paramsJson: "string" };
5039
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
5040
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
5041
+ const fromCli = {};
5042
+ for (const [k, v] of Object.entries(cliRaw)) {
5043
+ if (k === "paramsJson") continue;
5044
+ if (v !== void 0 && v !== null) fromCli[k] = v;
5045
+ }
5046
+ const fromOverride = pickParamsObject(overrides) ?? {};
5047
+ return { ...fromCli, ...fromJson, ...fromOverride };
5048
+ }
4271
5049
  };
5050
+ function emptyToUndef(s) {
5051
+ if (s === void 0 || s === null) return void 0;
5052
+ if (typeof s !== "string") return s;
5053
+ const t = s.trim();
5054
+ return t.length ? t : void 0;
5055
+ }
5056
+ function parseParamsJson(raw) {
5057
+ if (raw == null) return null;
5058
+ const t = String(raw).trim();
5059
+ if (!t) return null;
5060
+ let parsed;
5061
+ try {
5062
+ parsed = JSON.parse(t);
5063
+ } catch (e) {
5064
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
5065
+ }
5066
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5067
+ throw new ParamError("--paramsJson must be a JSON object");
5068
+ }
5069
+ return parsed;
5070
+ }
5071
+ function pickParamsObject(overrides) {
5072
+ const p = overrides?.params;
5073
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
5074
+ return void 0;
5075
+ }
4272
5076
 
4273
- // src/tasks/coreTasks/TaskPing.ts
4274
- var TaskPing = class extends TaskMaster {
5077
+ // src/tasks/coreTasks/TaskPing.js
5078
+ var TaskPing = class extends AbstractTask {
5079
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
5080
+ static defaultWaitForResult = true;
5081
+ /** Ping takes no params. */
5082
+ static async resolveCustomParams() {
5083
+ return null;
5084
+ }
5085
+ /**
5086
+ * @returns {Promise<{ success: true, results: "pong" }>}
5087
+ */
4275
5088
  async run() {
4276
5089
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4277
5090
  return { success: true, results: "pong" };
4278
5091
  }
4279
5092
  };
4280
5093
 
4281
- // src/tasks/coreTasks/TaskSampleProcess.ts
4282
- var TaskSampleProcess = class extends TaskMaster {
4283
- stopRequested = false;
4284
- stopAllowanceMs = 0;
4285
- stopDecisionLogged = false;
5094
+ // src/tasks/coreTasks/TaskSampleProcess.js
5095
+ var TaskSampleProcess = class extends AbstractTask {
5096
+ /**
5097
+ * @param {object} context
5098
+ * @param {Record<string, unknown>} [overrides]
5099
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
5100
+ */
5101
+ static async resolveCustomParams(context, overrides = {}) {
5102
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
5103
+ total: "number default 10",
5104
+ delay: "number default 1000",
5105
+ name: "string"
5106
+ }, overrides);
5107
+ const total = Number(merged.total);
5108
+ const delay = Number(merged.delay);
5109
+ if (!Number.isInteger(total) || total <= 0) {
5110
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
5111
+ }
5112
+ if (!Number.isInteger(delay) || delay < 0) {
5113
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
5114
+ }
5115
+ const out = { total, delay };
5116
+ if (typeof merged.name === "string" && merged.name.trim()) {
5117
+ out.name = merged.name.trim();
5118
+ }
5119
+ return out;
5120
+ }
5121
+ /**
5122
+ * @param {object} context
5123
+ * @param {object} task
5124
+ */
5125
+ constructor(context, task) {
5126
+ super(context, task);
5127
+ this.stopRequested = false;
5128
+ this.stopAllowanceMs = 0;
5129
+ this.stopDecisionLogged = false;
5130
+ }
5131
+ /**
5132
+ * Runner-facing stop signal. Records the allowance window so the main loop
5133
+ * can decide per-iteration whether to finish or abort early.
5134
+ *
5135
+ * @param {number} allowanceMs
5136
+ */
4286
5137
  requestStop(allowanceMs) {
4287
5138
  this.stopRequested = true;
4288
5139
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -4290,6 +5141,14 @@ var TaskSampleProcess = class extends TaskMaster {
4290
5141
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4291
5142
  );
4292
5143
  }
5144
+ /**
5145
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
5146
+ * progress every iteration. Validates params up front; invalid values short-
5147
+ * circuit to a structured failure without starting the loop.
5148
+ *
5149
+ * @param {(progress: object) => Promise<void>} reportProgress
5150
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5151
+ */
4293
5152
  async run(reportProgress) {
4294
5153
  const totalRaw = this.task?.params?.total ?? 10;
4295
5154
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -4371,7 +5230,7 @@ var TaskSampleProcess = class extends TaskMaster {
4371
5230
  }
4372
5231
  };
4373
5232
 
4374
- // src/tasks/coreTasks/TaskShellCommand.ts
5233
+ // src/tasks/coreTasks/TaskShellCommand.js
4375
5234
  import { spawn } from "child_process";
4376
5235
  function runShellCommand(command, cwd) {
4377
5236
  return new Promise((resolve2, reject) => {
@@ -4401,7 +5260,27 @@ function runShellCommand(command, cwd) {
4401
5260
  });
4402
5261
  });
4403
5262
  }
4404
- var TaskShellCommand = class extends TaskMaster {
5263
+ var TaskShellCommand = class extends AbstractTask {
5264
+ /**
5265
+ * @param {object} context
5266
+ * @param {Record<string, unknown>} [overrides]
5267
+ * @returns {Promise<{ command: string, cwd?: string }>}
5268
+ */
5269
+ static async resolveCustomParams(context, overrides = {}) {
5270
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
5271
+ command: "string",
5272
+ cwd: "string"
5273
+ }, overrides);
5274
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
5275
+ if (!command) {
5276
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
5277
+ }
5278
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
5279
+ return cwd ? { command, cwd } : { command };
5280
+ }
5281
+ /**
5282
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5283
+ */
4405
5284
  async run() {
4406
5285
  const params = this.task?.params;
4407
5286
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -4450,8 +5329,8 @@ var TaskShellCommand = class extends TaskMaster {
4450
5329
  }
4451
5330
  };
4452
5331
 
4453
- // src/tasks/coreTasks/TaskSystemInfo.ts
4454
- import os from "os";
5332
+ // src/tasks/coreTasks/TaskSystemInfo.js
5333
+ import os2 from "os";
4455
5334
  import fs4 from "fs/promises";
4456
5335
  function toGb(valueBytes) {
4457
5336
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
@@ -4470,13 +5349,22 @@ async function getDiskStats() {
4470
5349
  free: toGb(free)
4471
5350
  };
4472
5351
  }
4473
- var TaskSystemInfo = class extends TaskMaster {
5352
+ var TaskSystemInfo = class extends AbstractTask {
5353
+ /** Same UX expectation as `ping` — short probe, print the result. */
5354
+ static defaultWaitForResult = true;
5355
+ /** systemInfo takes no params. */
5356
+ static async resolveCustomParams() {
5357
+ return null;
5358
+ }
5359
+ /**
5360
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5361
+ */
4474
5362
  async run() {
4475
5363
  try {
4476
- const totalMemory = os.totalmem();
4477
- const freeMemory = os.freemem();
5364
+ const totalMemory = os2.totalmem();
5365
+ const freeMemory = os2.freemem();
4478
5366
  const usedMemory = totalMemory - freeMemory;
4479
- const cpus = os.cpus();
5367
+ const cpus = os2.cpus();
4480
5368
  const cpuUtilization = cpus.map((cpu) => {
4481
5369
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
4482
5370
  const usage = (total - cpu.times.idle) / total * 100;
@@ -4502,10 +5390,10 @@ var TaskSystemInfo = class extends TaskMaster {
4502
5390
  utilization: cpuUtilization
4503
5391
  },
4504
5392
  runtime: {
4505
- platform: os.platform(),
4506
- arch: os.arch(),
4507
- uptimeSec: os.uptime(),
4508
- hostname: os.hostname()
5393
+ platform: os2.platform(),
5394
+ arch: os2.arch(),
5395
+ uptimeSec: os2.uptime(),
5396
+ hostname: os2.hostname()
4509
5397
  }
4510
5398
  };
4511
5399
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -4522,8 +5410,31 @@ var TaskSystemInfo = class extends TaskMaster {
4522
5410
  }
4523
5411
  };
4524
5412
 
4525
- // src/tasks/coreTasks/TaskSumAB.ts
4526
- var TaskSumAB = class extends TaskMaster {
5413
+ // src/tasks/coreTasks/TaskSumAB.js
5414
+ var TaskSumAB = class extends AbstractTask {
5415
+ /** Short, deterministic — wait by default so callers see the sum. */
5416
+ static defaultWaitForResult = true;
5417
+ /**
5418
+ * @param {object} context
5419
+ * @param {Record<string, unknown>} [overrides]
5420
+ * @returns {Promise<{ a: number, b: number }>}
5421
+ */
5422
+ static async resolveCustomParams(context, overrides = {}) {
5423
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
5424
+ a: "number",
5425
+ b: "number"
5426
+ }, overrides);
5427
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
5428
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
5429
+ }
5430
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
5431
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
5432
+ }
5433
+ return { a: merged.a, b: merged.b };
5434
+ }
5435
+ /**
5436
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5437
+ */
4527
5438
  async run() {
4528
5439
  const a = this.task?.params?.a;
4529
5440
  const b = this.task?.params?.b;
@@ -4554,8 +5465,46 @@ var TaskSumAB = class extends TaskMaster {
4554
5465
  }
4555
5466
  };
4556
5467
 
4557
- // src/tasks/coreTasks/TaskStopRunner.ts
4558
- var TaskStopRunner = class extends TaskMaster {
5468
+ // src/tasks/coreTasks/TaskStopRunner.js
5469
+ var TaskStopRunner = class extends AbstractTask {
5470
+ /**
5471
+ * Stop tasks must target a concrete instance — without `serviceName` the
5472
+ * row would race against any worker on the queue. Layered on top of the
5473
+ * envelope built by {@link AbstractTask.resolveParams}.
5474
+ *
5475
+ * @param {object} context
5476
+ * @param {Record<string, unknown>} [overrides]
5477
+ * @returns {Promise<object>}
5478
+ */
5479
+ static async resolveParams(context, overrides = {}) {
5480
+ const main = await super.resolveParams(context, overrides);
5481
+ if (!main.serviceName) {
5482
+ throw new ParamError(
5483
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
5484
+ );
5485
+ }
5486
+ return main;
5487
+ }
5488
+ /**
5489
+ * @param {object} context
5490
+ * @param {Record<string, unknown>} [overrides]
5491
+ * @returns {Promise<{ allowanceMs: number }>}
5492
+ */
5493
+ static async resolveCustomParams(context, overrides = {}) {
5494
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
5495
+ allowanceMs: "number default 5000"
5496
+ }, overrides);
5497
+ const allowanceMs = Number(merged.allowanceMs);
5498
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
5499
+ throw new ParamError(
5500
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
5501
+ );
5502
+ }
5503
+ return { allowanceMs };
5504
+ }
5505
+ /**
5506
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
5507
+ */
4559
5508
  async run() {
4560
5509
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4561
5510
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -4570,175 +5519,399 @@ var TaskStopRunner = class extends TaskMaster {
4570
5519
  }
4571
5520
  };
4572
5521
 
4573
- // src/tasks/TasksRegistry.ts
5522
+ // src/tasks/coreTasks/TaskGetLogs.js
5523
+ var TaskGetLogs = class extends AbstractTask {
5524
+ /**
5525
+ * @param {object} context
5526
+ * @param {Record<string, unknown>} [overrides]
5527
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
5528
+ */
5529
+ static async resolveCustomParams(context, overrides = {}) {
5530
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
5531
+ source: "string",
5532
+ resource: "string",
5533
+ tail: "number default 100",
5534
+ afterTs: "string"
5535
+ }, overrides);
5536
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
5537
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
5538
+ if (!source) throw new ParamError('getLogs: param "source" is required');
5539
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
5540
+ let tail = Number(merged.tail);
5541
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
5542
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
5543
+ const out = { source, resource, tail };
5544
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
5545
+ out.afterTs = merged.afterTs.trim();
5546
+ }
5547
+ return out;
5548
+ }
5549
+ /**
5550
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
5551
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5552
+ */
5553
+ async run(_reportProgress) {
5554
+ const p = this.task.params ?? {};
5555
+ const source = String(p.source ?? "").trim();
5556
+ const resource = String(p.resource ?? "").trim();
5557
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
5558
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
5559
+ if (!source || !resource) {
5560
+ return {
5561
+ success: false,
5562
+ results: { error: 'getLogs requires params "source" and "resource"' }
5563
+ };
5564
+ }
5565
+ try {
5566
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
5567
+ source,
5568
+ resource,
5569
+ tail,
5570
+ afterTs
5571
+ });
5572
+ return {
5573
+ success: true,
5574
+ results: { records, latestTs, source, resource }
5575
+ };
5576
+ } catch (e) {
5577
+ return {
5578
+ success: false,
5579
+ results: { error: e?.message ?? String(e) }
5580
+ };
5581
+ }
5582
+ }
5583
+ };
5584
+
5585
+ // src/tasks/TasksRegistry.js
4574
5586
  var TasksRegistry = class _TasksRegistry {
4575
- map = {};
5587
+ /**
5588
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
5589
+ */
4576
5590
  constructor(initial) {
5591
+ this.map = {};
4577
5592
  if (initial) {
4578
5593
  this.addMany(initial);
4579
5594
  }
4580
5595
  }
5596
+ /**
5597
+ * Build a registry pre-populated with every core task plus legacy aliases.
5598
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
5599
+ *
5600
+ * @returns {TasksRegistry}
5601
+ */
4581
5602
  static withCoreTasks() {
4582
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
5603
+ 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);
4583
5604
  }
5605
+ /**
5606
+ * Register a single task class under a name. Overwrites any previous entry.
5607
+ *
5608
+ * @param {string} taskName
5609
+ * @param {Function} taskClass Subclass of `AbstractTask`.
5610
+ * @returns {this}
5611
+ */
4584
5612
  add(taskName, taskClass) {
4585
5613
  this.map[taskName] = taskClass;
4586
5614
  return this;
4587
5615
  }
5616
+ /**
5617
+ * Bulk-register a name → class map. Later calls override earlier ones.
5618
+ *
5619
+ * @param {Record<string, Function>} entries
5620
+ * @returns {this}
5621
+ */
4588
5622
  addMany(entries) {
4589
5623
  for (const [name, klass] of Object.entries(entries)) {
4590
5624
  this.add(name, klass);
4591
5625
  }
4592
5626
  return this;
4593
5627
  }
5628
+ /**
5629
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
5630
+ * the runner treats that as "some other worker may handle this" and skips.
5631
+ *
5632
+ * @param {string} taskName
5633
+ * @returns {Function | undefined}
5634
+ */
4594
5635
  get(taskName) {
4595
5636
  return this.map[taskName];
4596
5637
  }
5638
+ /**
5639
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
5640
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
5641
+ * paths where an unknown name is a hard CLI/programmer error.
5642
+ *
5643
+ * @param {string} taskName
5644
+ * @returns {Function}
5645
+ */
5646
+ requireClass(taskName) {
5647
+ const TaskClass = taskName ? this.map[taskName] : void 0;
5648
+ if (!TaskClass) {
5649
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
5650
+ throw new ParamError(
5651
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
5652
+ );
5653
+ }
5654
+ return TaskClass;
5655
+ }
5656
+ /**
5657
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
5658
+ * from `overrides` or `context.params`, look up the class, and delegate
5659
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
5660
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
5661
+ *
5662
+ * Validation failures (unknown task, missing required custom params, etc.)
5663
+ * surface as {@link ParamError} so the caller aborts cleanly before any
5664
+ * row is inserted.
5665
+ *
5666
+ * @param {object} context
5667
+ * @param {Record<string, unknown>} [overrides]
5668
+ * @returns {Promise<object>}
5669
+ */
5670
+ async resolveTaskParams(context, overrides = {}) {
5671
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
5672
+ const fromCli = context.params.get("name", "string");
5673
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
5674
+ const name = overrideName || cliName;
5675
+ if (!name) {
5676
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5677
+ }
5678
+ const TaskClass = this.requireClass(name);
5679
+ return TaskClass.resolveParams(context, { ...overrides, name });
5680
+ }
5681
+ /**
5682
+ * Names of every registered task, sorted alphabetically (useful for CLI output
5683
+ * and allowlist sanity checks).
5684
+ *
5685
+ * @returns {string[]}
5686
+ */
4597
5687
  listSupportedTasks() {
4598
5688
  return Object.keys(this.map).sort();
4599
5689
  }
5690
+ /**
5691
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
5692
+ * or for serialization.
5693
+ *
5694
+ * @returns {Record<string, Function>}
5695
+ */
4600
5696
  toObject() {
4601
5697
  return { ...this.map };
4602
5698
  }
4603
5699
  };
4604
5700
 
4605
- // src/tasks/taskScriptRunner.ts
5701
+ // src/tasks/serviceTaskAllowlist.js
5702
+ var SERVICE_TASK_NAMES = [
5703
+ "ping",
5704
+ "stop",
5705
+ "stopRunner",
5706
+ "shellCommand",
5707
+ "systemInfo",
5708
+ "info",
5709
+ "getLogs"
5710
+ ];
5711
+ function normalizeAllowedTasks(value) {
5712
+ if (!value) return void 0;
5713
+ if (Array.isArray(value)) {
5714
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5715
+ return out2.length ? out2 : void 0;
5716
+ }
5717
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5718
+ return out.length ? out : void 0;
5719
+ }
5720
+ function mergeAllowedTasksWithServiceTasks(names) {
5721
+ const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
5722
+ return Array.from(set).sort();
5723
+ }
5724
+
5725
+ // src/tasks/taskScriptRunner.js
4606
5726
  import { spawn as spawn2 } from "child_process";
5727
+ var MAX_PROGRESS_TEXT_LEN = 4e3;
4607
5728
  function toCliArgs(args = []) {
4608
5729
  return args.filter((a) => typeof a === "string" && a.length > 0);
4609
5730
  }
4610
5731
  function formatChildLogPrefix(task) {
4611
- return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
5732
+ return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
4612
5733
  }
4613
- async function runNodeTaskScript(context, options) {
4614
- const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
5734
+ function isProgressPayload(payload) {
5735
+ if (!payload || typeof payload !== "object") return false;
5736
+ if (payload.level !== "progress") return false;
5737
+ const count = Number(payload.count);
5738
+ const total = Number(payload.total);
5739
+ return Number.isFinite(count) && Number.isFinite(total) && total > 0;
5740
+ }
5741
+ function formatProgressText(payload, fallbackPrefix) {
5742
+ const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
5743
+ const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
5744
+ return `${pfx}${label}${payload.count}/${payload.total}`;
5745
+ }
5746
+ function forwardChildLogToParent(context, prefix, message) {
5747
+ if (!message || typeof message !== "object") return;
5748
+ const text = typeof message.message === "string" ? message.message : null;
5749
+ if (!text) return;
5750
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
5751
+ const line = `[child:${prefix}] ${text}`;
5752
+ const logger = context.logger;
5753
+ switch (level) {
5754
+ case "error":
5755
+ case "fatal":
5756
+ logger.error?.(line);
5757
+ return;
5758
+ case "warn":
5759
+ case "warning":
5760
+ logger.warn?.(line);
5761
+ return;
5762
+ case "debug":
5763
+ logger.debug?.(line);
5764
+ return;
5765
+ case "info":
5766
+ default:
5767
+ logger.info?.(line);
5768
+ }
5769
+ }
5770
+ function buildNodeArgs(scriptPath, cliArgs) {
4615
5771
  const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
4616
5772
  const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
4617
- const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
4618
- const child = spawn2(
4619
- process.execPath,
4620
- nodeArgs,
4621
- {
4622
- cwd: options.cwd || process.cwd(),
4623
- stdio: ["ignore", "pipe", "pipe", "ipc"],
4624
- env: {
4625
- ...process.env,
4626
- TASK_ID: options.task.id,
4627
- TASK_NAME: options.task.task,
4628
- TASK_OPID: options.task.opid || ""
4629
- }
4630
- }
5773
+ return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
5774
+ }
5775
+ function resolveTasksTableName(context) {
5776
+ return context.tasksQueueName || context.params?.get?.("table") || "tasks";
5777
+ }
5778
+ function announceIpcFileLogsTarget(context, options) {
5779
+ if (!options.ipcFileLogs) return;
5780
+ const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
5781
+ const enabledRaw = context.params?.get?.("tasksLogsEnabled");
5782
+ const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
5783
+ context.logger.info?.(
5784
+ `[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
4631
5785
  );
4632
- let stdout = "";
4633
- let stderr = "";
4634
- let workerResult = null;
4635
- let hadErrorMessage = false;
4636
- const prefix = formatChildLogPrefix(options.task);
4637
- const db = context.db;
4638
- const tasksTable = context.params?.get?.("table") || "tasks";
4639
- let progressWriteChain = Promise.resolve();
4640
- let progressCallbackChain = Promise.resolve();
4641
- const updateProgress = (text) => {
4642
- if (!db || !text || !text.trim()) return;
4643
- progressWriteChain = progressWriteChain.then(async () => {
4644
- await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
4645
- }).catch((error) => {
4646
- context.logger.warn?.(
4647
- `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
4648
- );
4649
- });
4650
- if (options.onProgress) {
4651
- progressCallbackChain = progressCallbackChain.then(async () => {
4652
- await options.onProgress?.(text.slice(0, 4e3));
4653
- }).catch((error) => {
4654
- context.logger.warn?.(
4655
- `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
4656
- );
5786
+ }
5787
+ function createSerializedQueue() {
5788
+ let chain = Promise.resolve();
5789
+ return {
5790
+ push(fn) {
5791
+ chain = chain.then(fn, () => {
5792
+ }).catch(() => {
5793
+ });
5794
+ return chain;
5795
+ },
5796
+ drain() {
5797
+ return chain.catch(() => {
4657
5798
  });
4658
5799
  }
4659
5800
  };
4660
- const payloadToProgressText = (payload) => {
4661
- if (!payload) return "";
4662
- if (typeof payload === "string") return payload;
4663
- if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4664
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
4665
- return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
4666
- }
4667
- if (typeof payload.message === "string") return payload.message;
4668
- if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4669
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
4670
- return `${pfx}${payload.count}/${payload.total}`;
4671
- }
4672
- return "";
5801
+ }
5802
+ async function runNodeTaskScript(context, options) {
5803
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
5804
+ const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
5805
+ const child = spawn2(process.execPath, nodeArgs, {
5806
+ cwd: options.cwd || process.cwd(),
5807
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
5808
+ env: {
5809
+ ...process.env,
5810
+ TASK_ID: options.task.id,
5811
+ TASK_NAME: options.task.name,
5812
+ TASK_OPID: options.task.opid || ""
5813
+ }
5814
+ });
5815
+ announceIpcFileLogsTarget(context, options);
5816
+ const prefix = formatChildLogPrefix(options.task);
5817
+ const tasksTable = resolveTasksTableName(context);
5818
+ const progressQueue = createSerializedQueue();
5819
+ const state = {
5820
+ stdout: "",
5821
+ stderr: "",
5822
+ workerResult: null,
5823
+ hadErrorMessage: false
5824
+ };
5825
+ const writeProgress = (text) => {
5826
+ const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
5827
+ if (!trimmed) return;
5828
+ progressQueue.push(async () => {
5829
+ const db = context.db;
5830
+ if (db) {
5831
+ try {
5832
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
5833
+ } catch (error) {
5834
+ context.logger.warn?.(
5835
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
5836
+ );
5837
+ }
5838
+ }
5839
+ if (options.onProgress) {
5840
+ try {
5841
+ await options.onProgress(trimmed);
5842
+ } catch (error) {
5843
+ context.logger.warn?.(
5844
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
5845
+ );
5846
+ }
5847
+ }
5848
+ });
4673
5849
  };
4674
- child.stdout.on("data", (chunk) => {
5850
+ child.stdout?.on("data", (chunk) => {
4675
5851
  const text = String(chunk);
4676
- stdout += text;
5852
+ state.stdout += text;
4677
5853
  if (text.trim()) {
4678
5854
  context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
4679
- updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
4680
5855
  }
4681
5856
  });
4682
- child.stderr.on("data", (chunk) => {
5857
+ child.stderr?.on("data", (chunk) => {
4683
5858
  const text = String(chunk);
4684
- stderr += text;
5859
+ state.stderr += text;
4685
5860
  if (text.trim()) {
4686
5861
  context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
4687
5862
  }
4688
5863
  });
4689
5864
  child.on("message", (message) => {
4690
5865
  if (message && typeof message === "object" && "__taskWorkerResult" in message) {
4691
- workerResult = message.__taskWorkerResult;
5866
+ state.workerResult = message.__taskWorkerResult;
4692
5867
  return;
4693
5868
  }
4694
5869
  if (message && typeof message === "object") {
4695
5870
  const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
4696
5871
  if (level === "error" || level === "fatal") {
4697
- hadErrorMessage = true;
5872
+ state.hadErrorMessage = true;
4698
5873
  }
4699
5874
  }
4700
- appendTaskIpcLog(context, options.task, message);
4701
- const progressText = payloadToProgressText(message);
4702
- if (progressText) {
4703
- updateProgress(progressText);
4704
- if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
4705
- const countNum = Number(String(message.count).trim());
4706
- const totalNum = Number(message.total);
4707
- if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
4708
- context.logger.progress(message.message || "progress", {
4709
- prefix: message.prefix || prefix,
4710
- count: countNum,
4711
- total: totalNum
4712
- });
4713
- } else {
4714
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
4715
- }
4716
- } else {
4717
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
4718
- }
5875
+ appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
5876
+ try {
5877
+ options.onChildIpcMessage?.(message);
5878
+ } catch (e) {
5879
+ context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
4719
5880
  }
5881
+ if (isProgressPayload(message)) {
5882
+ context.logger.progress(message.message || "progress", {
5883
+ prefix: message.prefix || prefix,
5884
+ count: Number(message.count),
5885
+ total: Number(message.total)
5886
+ });
5887
+ writeProgress(formatProgressText(message, prefix));
5888
+ return;
5889
+ }
5890
+ forwardChildLogToParent(context, prefix, message);
4720
5891
  });
4721
5892
  return await new Promise((resolve2, reject) => {
4722
5893
  child.on("error", (error) => reject(error));
4723
5894
  child.on("close", (exitCode, signal) => {
4724
- Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
5895
+ void (async () => {
5896
+ await flushTaskIpcLogs(context);
5897
+ await progressQueue.drain();
4725
5898
  resolve2({
4726
5899
  exitCode,
4727
5900
  signal,
4728
- stdout: stdout.trim(),
4729
- stderr: stderr.trim(),
4730
- workerResult,
4731
- hadErrorMessage
5901
+ stdout: state.stdout.trim(),
5902
+ stderr: state.stderr.trim(),
5903
+ workerResult: state.workerResult,
5904
+ hadErrorMessage: state.hadErrorMessage
4732
5905
  });
4733
- });
5906
+ })();
4734
5907
  });
4735
5908
  });
4736
5909
  }
4737
5910
 
4738
- // src/tasks/index.ts
5911
+ // src/tasks/index.js
4739
5912
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4740
5913
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4741
- function getDb2(context) {
5914
+ function getDb3(context) {
4742
5915
  const db = context.db;
4743
5916
  if (!db) {
4744
5917
  throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
@@ -4750,22 +5923,13 @@ function normalizeRegistry(registry) {
4750
5923
  if (registry instanceof TasksRegistry) return registry;
4751
5924
  return new TasksRegistry().addMany(registry);
4752
5925
  }
4753
- function normalizeAllowedTasks(value) {
4754
- if (!value) return void 0;
4755
- if (Array.isArray(value)) {
4756
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
4757
- return out2.length ? out2 : void 0;
4758
- }
4759
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
4760
- return out.length ? out : void 0;
4761
- }
4762
- async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
5926
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
4763
5927
  return enqueueTask(context, {
4764
- queue,
4765
- target,
4766
- task: "stopRunner",
5928
+ queueName,
5929
+ name: "stopRunner",
4767
5930
  params: { allowanceMs },
4768
- priority: 1e6
5931
+ priority: 0,
5932
+ serviceGroup
4769
5933
  });
4770
5934
  }
4771
5935
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
@@ -4782,19 +5946,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
4782
5946
  context.emitter.emit("stop", allowanceMs);
4783
5947
  }
4784
5948
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4785
- const db = getDb2(context);
4786
- const taskName = row.task;
5949
+ const db = getDb3(context);
5950
+ const taskName = row.name;
4787
5951
  const TaskClass = registry.get(taskName);
4788
- const { paused_at: _pausedAt, ...rowForHistory } = row;
4789
5952
  if (!TaskClass) {
4790
5953
  const err = { message: `Unknown task "${taskName}"` };
4791
- await db(historyTable).insert({
4792
- ...rowForHistory,
4793
- completed_at: /* @__PURE__ */ new Date(),
4794
- success: false,
4795
- params: toJsonColumn(row.params),
4796
- results: toJsonColumn(err)
4797
- });
5954
+ await db(historyTable).insert(
5955
+ taskHistoryInsertFromQueueRow(row, {
5956
+ completed_at: /* @__PURE__ */ new Date(),
5957
+ success: false,
5958
+ status: "failed",
5959
+ status_changed_at: db.fn.now(),
5960
+ params: toJsonColumn(row.params),
5961
+ results: toJsonColumn(err)
5962
+ })
5963
+ );
4798
5964
  if (row.schedule) {
4799
5965
  await db(tasksTable).where({ id: row.id }).update({
4800
5966
  started_at: null,
@@ -4802,7 +5968,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4802
5968
  success: false,
4803
5969
  results: toJsonColumn(err),
4804
5970
  past_due: null,
4805
- paused_at: db.fn.now(),
5971
+ status: "paused",
5972
+ status_changed_at: db.fn.now(),
4806
5973
  progress: LOCKED_BY_ERROR_MESSAGE
4807
5974
  });
4808
5975
  } else {
@@ -4829,13 +5996,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4829
5996
  } finally {
4830
5997
  runningTaskInstances.delete(row.id);
4831
5998
  }
4832
- await db(historyTable).insert({
4833
- ...rowForHistory,
4834
- completed_at: /* @__PURE__ */ new Date(),
4835
- success,
4836
- params: toJsonColumn(row.params),
4837
- results: toJsonColumn(results)
4838
- });
5999
+ await db(historyTable).insert(
6000
+ taskHistoryInsertFromQueueRow(row, {
6001
+ completed_at: /* @__PURE__ */ new Date(),
6002
+ success,
6003
+ status: success ? "completed" : "failed",
6004
+ status_changed_at: db.fn.now(),
6005
+ params: toJsonColumn(row.params),
6006
+ results: toJsonColumn(results)
6007
+ })
6008
+ );
4839
6009
  if (!success) {
4840
6010
  const dbName = String(context?.params?.get?.("dbName") || "local");
4841
6011
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -4850,19 +6020,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4850
6020
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
4851
6021
  appendTaskIpcLog(context, row, {
4852
6022
  level: "error",
4853
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
6023
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
4854
6024
  details: results
4855
6025
  });
4856
6026
  }
4857
6027
  if (row.schedule) {
4858
6028
  if (success) {
6029
+ let nextRunAt = null;
6030
+ try {
6031
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
6032
+ } catch (e) {
6033
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
6034
+ }
4859
6035
  await db(tasksTable).where({ id: row.id }).update({
4860
6036
  started_at: null,
4861
6037
  completed_at: /* @__PURE__ */ new Date(),
4862
6038
  success,
4863
6039
  results: toJsonColumn(results),
4864
6040
  progress: null,
4865
- past_due: null
6041
+ past_due: null,
6042
+ status: "idle",
6043
+ status_changed_at: db.fn.now(),
6044
+ next_run_at: nextRunAt,
6045
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
6046
+ service_name: null,
6047
+ server_name: null,
6048
+ instance_number: null
4866
6049
  });
4867
6050
  } else {
4868
6051
  await db(tasksTable).where({ id: row.id }).update({
@@ -4870,7 +6053,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4870
6053
  completed_at: /* @__PURE__ */ new Date(),
4871
6054
  success,
4872
6055
  results: toJsonColumn(results),
4873
- paused_at: db.fn.now(),
6056
+ status: "paused",
6057
+ status_changed_at: db.fn.now(),
4874
6058
  progress: LOCKED_BY_ERROR_MESSAGE,
4875
6059
  past_due: null
4876
6060
  });
@@ -4882,209 +6066,401 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4882
6066
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
4883
6067
  return { stopRunnerRequested, stopAllowanceMs };
4884
6068
  }
4885
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
4886
- const db = getDb2(context);
4887
- 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);
6069
+ function shuffleTaskRowsInPlace(rows) {
6070
+ for (let i = rows.length - 1; i > 0; i--) {
6071
+ const j = Math.floor(Math.random() * (i + 1));
6072
+ const t = rows[i];
6073
+ rows[i] = rows[j];
6074
+ rows[j] = t;
6075
+ }
6076
+ }
6077
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
6078
+ const db = getDb3(context);
6079
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
6080
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
6081
+ }).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);
4888
6082
  if (taskNames && taskNames.length > 0) {
4889
- query = query.whereIn("task", taskNames);
6083
+ query = query.whereIn("name", taskNames);
6084
+ }
6085
+ if (runnerIdentity) {
6086
+ query = query.where(function() {
6087
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
6088
+ }).where(function() {
6089
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
6090
+ }).where(function() {
6091
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
6092
+ });
6093
+ } else {
6094
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
4890
6095
  }
4891
6096
  const candidates = await query;
6097
+ shuffleTaskRowsInPlace(candidates);
4892
6098
  for (const row of candidates) {
4893
6099
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
4894
6100
  continue;
4895
6101
  }
4896
- const TaskClass = registry.get(row.task);
4897
- if (TaskClass) {
4898
- const taskInstance = new TaskClass(context, row);
4899
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
4900
- if (reason) {
4901
- if (!row.past_due) {
4902
- await db(tasksTable).where({ id: row.id }).update({
4903
- past_due: db.fn.now(),
4904
- progress: String(reason)
4905
- });
4906
- }
4907
- continue;
6102
+ const TaskClass = registry.get(row.name);
6103
+ if (!TaskClass) {
6104
+ continue;
6105
+ }
6106
+ const taskInstance = new TaskClass(context, row);
6107
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
6108
+ if (reason) {
6109
+ if (!row.past_due) {
6110
+ await db(tasksTable).where({ id: row.id }).update({
6111
+ past_due: db.fn.now(),
6112
+ progress: String(reason)
6113
+ });
4908
6114
  }
6115
+ continue;
6116
+ }
6117
+ const claimPatch = {
6118
+ started_at: db.fn.now(),
6119
+ status: "running",
6120
+ status_changed_at: db.fn.now()
6121
+ };
6122
+ if (runnerIdentity) {
6123
+ claimPatch.service_name = runnerIdentity.service_name;
6124
+ claimPatch.server_name = runnerIdentity.server_name;
6125
+ claimPatch.instance_number = runnerIdentity.instance_number;
4909
6126
  }
4910
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
6127
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
4911
6128
  const claimed = Array.isArray(updated) ? updated[0] : null;
4912
6129
  if (claimed) return claimed;
4913
6130
  }
4914
6131
  return null;
4915
6132
  }
4916
6133
  async function runTasksLoop(context, options) {
4917
- const queue = options.queue ?? "tasks";
6134
+ const queueName = options.queueName ?? "tasks";
4918
6135
  const target = options.target;
4919
6136
  const pollMs = options.pollMs ?? 1e3;
4920
- const maxParallel = options.maxParallel ?? 1;
6137
+ const claimJitterMs = options.claimJitterMs ?? 0;
6138
+ const maxParallel = options.maxParallel ?? 32;
4921
6139
  const scanLimit = options.scanLimit ?? 100;
4922
6140
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
4923
6141
  const registry = normalizeRegistry(options.registry);
4924
- const { tasksTable, historyTable } = queueToTableNames(queue);
6142
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
4925
6143
  if (!target) throw new Error("runTasksLoop: target is required");
6144
+ context.tasksQueueName = queueName;
4926
6145
  const runningPromises = /* @__PURE__ */ new Set();
4927
6146
  const runningTaskInstances = /* @__PURE__ */ new Map();
4928
6147
  let runningStopControlPromise = null;
4929
6148
  let stopRequested = false;
4930
6149
  let stopAllowanceMs = 5e3;
4931
- context.__tasksRunnerStop = false;
4932
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
4933
- if (!runningStopControlPromise) {
4934
- const claimedStopTask = await claimNextRunnableTask(
4935
- context,
4936
- tasksTable,
4937
- target,
4938
- registry,
4939
- 10,
4940
- ["stopRunner", "stop"]
4941
- );
4942
- if (claimedStopTask) {
4943
- runningStopControlPromise = executeClaimedTask(
6150
+ context.tasksRunnerStop = false;
6151
+ let registryReg = null;
6152
+ let registryInterval = null;
6153
+ let runnerIdentity = null;
6154
+ const hbGroup = options.runnerServiceGroup?.trim();
6155
+ if (hbGroup) {
6156
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
6157
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
6158
+ const defaultMeta = {
6159
+ component: "tasks-runner",
6160
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
6161
+ };
6162
+ registryReg = await registerInServicesRegistry(context, {
6163
+ queueName,
6164
+ target,
6165
+ serviceGroup: hbGroup,
6166
+ serviceName: options.runnerServiceName,
6167
+ instanceNumber: options.runnerInstanceNumber,
6168
+ staleMs,
6169
+ groupMaxInstances: options.runnerGroupMaxInstances,
6170
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
6171
+ metadata: options.runnerMetadata ?? defaultMeta
6172
+ });
6173
+ runnerIdentity = {
6174
+ service_name: registryReg.serviceName,
6175
+ server_name: os3.hostname(),
6176
+ instance_number: registryReg.instanceNumber
6177
+ };
6178
+ registryInterval = setInterval(() => {
6179
+ void touchServicesRegistry(context, registryReg).catch((err) => {
6180
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
6181
+ });
6182
+ }, hbIntervalMs);
6183
+ }
6184
+ try {
6185
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
6186
+ if (!runningStopControlPromise) {
6187
+ const claimedStopTask = await claimNextRunnableTask(
4944
6188
  context,
4945
6189
  tasksTable,
4946
- historyTable,
4947
- claimedStopTask,
6190
+ target,
4948
6191
  registry,
4949
- runningTaskInstances
4950
- ).then(async (outcome) => {
6192
+ 10,
6193
+ ["stopRunner", "stop"],
6194
+ runnerIdentity
6195
+ );
6196
+ if (claimedStopTask) {
6197
+ runningStopControlPromise = executeClaimedTask(
6198
+ context,
6199
+ tasksTable,
6200
+ historyTable,
6201
+ claimedStopTask,
6202
+ registry,
6203
+ runningTaskInstances
6204
+ ).then(async (outcome) => {
6205
+ if (outcome.stopRunnerRequested && !stopRequested) {
6206
+ stopRequested = true;
6207
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
6208
+ context.tasksRunnerStop = true;
6209
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
6210
+ }
6211
+ }).finally(() => {
6212
+ runningStopControlPromise = null;
6213
+ });
6214
+ }
6215
+ }
6216
+ if (claimJitterMs > 0) {
6217
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
6218
+ }
6219
+ while (runningPromises.size < maxParallel) {
6220
+ const claimed = await claimNextRunnableTask(
6221
+ context,
6222
+ tasksTable,
6223
+ target,
6224
+ registry,
6225
+ scanLimit,
6226
+ allowedTasks,
6227
+ runnerIdentity
6228
+ );
6229
+ if (!claimed) break;
6230
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4951
6231
  if (outcome.stopRunnerRequested && !stopRequested) {
4952
6232
  stopRequested = true;
4953
6233
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4954
- context.__tasksRunnerStop = true;
6234
+ context.tasksRunnerStop = true;
4955
6235
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4956
6236
  }
4957
6237
  }).finally(() => {
4958
- runningStopControlPromise = null;
6238
+ runningPromises.delete(p);
4959
6239
  });
6240
+ runningPromises.add(p);
6241
+ }
6242
+ const wakePromises = [...runningPromises];
6243
+ if (runningStopControlPromise) {
6244
+ wakePromises.push(runningStopControlPromise);
6245
+ }
6246
+ if (wakePromises.length === 0) {
6247
+ await sleepMs(pollMs);
6248
+ } else {
6249
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
6250
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
4960
6251
  }
4961
6252
  }
4962
- while (runningPromises.size < maxParallel) {
4963
- const claimed = await claimNextRunnableTask(
4964
- context,
4965
- tasksTable,
4966
- target,
4967
- registry,
4968
- scanLimit,
4969
- allowedTasks
4970
- );
4971
- if (!claimed) break;
4972
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4973
- if (outcome.stopRunnerRequested && !stopRequested) {
4974
- stopRequested = true;
4975
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4976
- context.__tasksRunnerStop = true;
4977
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4978
- }
4979
- }).finally(() => {
4980
- runningPromises.delete(p);
4981
- });
4982
- runningPromises.add(p);
6253
+ if (context.isStop() && !stopRequested) {
6254
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4983
6255
  }
4984
- await sleepMs(pollMs);
4985
- }
4986
- if (context.isStop() && !stopRequested) {
4987
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4988
- }
4989
- if (runningPromises.size > 0) {
4990
- if (stopRequested) {
4991
- await Promise.race([
4992
- Promise.allSettled(Array.from(runningPromises)),
4993
- sleepMs(stopAllowanceMs).then(() => {
4994
- context.logger.warn?.(
4995
- `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
4996
- );
4997
- })
4998
- ]);
4999
- } else {
5000
- await Promise.allSettled(Array.from(runningPromises));
6256
+ if (runningPromises.size > 0) {
6257
+ if (stopRequested) {
6258
+ await Promise.race([
6259
+ Promise.allSettled(Array.from(runningPromises)),
6260
+ sleepMs(stopAllowanceMs).then(() => {
6261
+ context.logger.warn?.(
6262
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
6263
+ );
6264
+ })
6265
+ ]);
6266
+ } else {
6267
+ await Promise.allSettled(Array.from(runningPromises));
6268
+ }
6269
+ }
6270
+ } finally {
6271
+ if (registryInterval) {
6272
+ clearInterval(registryInterval);
6273
+ registryInterval = null;
6274
+ }
6275
+ if (registryReg) {
6276
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
6277
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
6278
+ });
6279
+ registryReg = null;
6280
+ delete context.servicesRegistry;
6281
+ delete context.runnerHeartbeat;
5001
6282
  }
5002
6283
  }
5003
6284
  }
5004
6285
  async function waitForTaskResult(context, taskId, options = {}) {
5005
- const db = getDb2(context);
5006
- const queue = options.queue ?? "tasks";
6286
+ const db = getDb3(context);
6287
+ const queueName = options.queueName ?? "tasks";
5007
6288
  const timeoutMs = options.timeoutMs ?? 6e4;
5008
6289
  const pollMs = options.pollMs ?? 500;
5009
- const { tasksTable, historyTable } = queueToTableNames(queue);
6290
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5010
6291
  const deadline = Date.now() + timeoutMs;
6292
+ const waitStartedAt = /* @__PURE__ */ new Date();
6293
+ let cachedNameOpid = null;
6294
+ async function historySinceWait(name, opid) {
6295
+ let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
6296
+ if (opid == null || opid === "") {
6297
+ q = q.whereNull("opid");
6298
+ } else {
6299
+ q = q.where({ opid });
6300
+ }
6301
+ return await q.orderBy("completed_at", "desc").first();
6302
+ }
5011
6303
  while (Date.now() <= deadline) {
5012
- const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5013
- if (done) return done;
6304
+ const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
6305
+ if (legacy) {
6306
+ return legacy;
6307
+ }
5014
6308
  const pending = await db(tasksTable).where({ id: taskId }).first();
5015
- if (!pending) {
5016
- const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5017
- return maybeDone ?? null;
6309
+ if (pending) {
6310
+ cachedNameOpid = { name: pending.name, opid: pending.opid };
6311
+ const done = await historySinceWait(pending.name, pending.opid);
6312
+ if (done) {
6313
+ return done;
6314
+ }
6315
+ } else if (cachedNameOpid) {
6316
+ const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
6317
+ if (done) {
6318
+ return done;
6319
+ }
6320
+ return null;
6321
+ } else {
6322
+ return null;
5018
6323
  }
5019
6324
  await sleepMs(pollMs);
5020
6325
  }
5021
6326
  return null;
5022
6327
  }
5023
6328
  var TasksManager = class _TasksManager {
5024
- context;
5025
- queue;
5026
- target;
5027
- recreateTaskTables;
5028
- pollMs;
5029
- maxParallel;
5030
- scanLimit;
5031
- allowedTasks;
5032
- registry;
6329
+ /**
6330
+ * @param {object} context
6331
+ * @param {{
6332
+ * queueName?: string,
6333
+ * target?: string,
6334
+ * recreateTaskTables?: boolean,
6335
+ * pollMs?: number,
6336
+ * claimJitterMs?: number,
6337
+ * maxParallel?: number,
6338
+ * scanLimit?: number,
6339
+ * allowedTasks?: string | string[],
6340
+ * registry?: TasksRegistry | Record<string, Function>,
6341
+ * runnerServiceGroup?: string,
6342
+ * runnerServiceName?: string,
6343
+ * runnerInstanceNumber?: number,
6344
+ * runnerHeartbeatIntervalMs?: number,
6345
+ * runnerHeartbeatStaleMs?: number,
6346
+ * runnerGroupMaxInstances?: number,
6347
+ * runnerEnforceMaxInstances?: boolean,
6348
+ * runnerMetadata?: Record<string, unknown>,
6349
+ * }} [options]
6350
+ */
5033
6351
  constructor(context, options = {}) {
5034
6352
  this.context = context;
5035
- this.queue = options.queue ?? "tasks";
6353
+ this.queueName = options.queueName ?? "tasks";
5036
6354
  this.target = options.target ?? "localRunner";
5037
6355
  this.recreateTaskTables = options.recreateTaskTables ?? false;
5038
6356
  this.pollMs = options.pollMs ?? 1e3;
6357
+ this.claimJitterMs = options.claimJitterMs ?? 0;
5039
6358
  this.maxParallel = options.maxParallel ?? 1;
5040
6359
  this.scanLimit = options.scanLimit ?? 100;
5041
6360
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5042
6361
  this.registry = normalizeRegistry(options.registry);
6362
+ this.runnerServiceGroup = options.runnerServiceGroup;
6363
+ this.runnerServiceName = options.runnerServiceName;
6364
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
6365
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
6366
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
6367
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
6368
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
6369
+ this.runnerMetadata = options.runnerMetadata;
5043
6370
  }
6371
+ /**
6372
+ * Preferred factory: reads defaults from `context.params` (module namespace
6373
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
6374
+ * and inline options in one consistent resolver.
6375
+ *
6376
+ * @param {object} context
6377
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
6378
+ * @returns {TasksManager}
6379
+ */
5044
6380
  static init(context, options = {}) {
5045
6381
  const defs = {
5046
6382
  table: "string default tasks",
5047
6383
  target: "string default localRunner",
5048
6384
  recreateTaskTables: "boolean default false",
5049
6385
  pollMs: "number default 1000",
6386
+ claimJitterMs: "number default 0",
5050
6387
  maxParallel: "number default 1",
5051
6388
  scanLimit: "number default 100",
5052
- allowedTasks: "string"
6389
+ allowedTasks: "string",
6390
+ runnerServiceGroup: "string",
6391
+ runnerServiceName: "string",
6392
+ runnerInstanceNumber: "number",
6393
+ runnerHeartbeatIntervalMs: "number default 10000",
6394
+ runnerHeartbeatStaleMs: "number default 45000",
6395
+ runnerGroupMaxInstances: "number",
6396
+ runnerEnforceMaxInstances: "boolean default true"
5053
6397
  };
5054
- const discovered = context.params.getAllForModule(defs);
6398
+ const discovered = context.params.getAllForModule("tasks", defs);
5055
6399
  const resolved = {
5056
- queue: discovered.table,
6400
+ queueName: discovered.table,
5057
6401
  target: discovered.target,
5058
6402
  recreateTaskTables: discovered.recreateTaskTables,
5059
6403
  pollMs: discovered.pollMs,
6404
+ claimJitterMs: discovered.claimJitterMs,
5060
6405
  maxParallel: discovered.maxParallel,
5061
6406
  scanLimit: discovered.scanLimit,
5062
6407
  allowedTasks: discovered.allowedTasks,
6408
+ runnerServiceGroup: discovered.runnerServiceGroup,
6409
+ runnerServiceName: discovered.runnerServiceName,
6410
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
6411
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
6412
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
6413
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
6414
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
5063
6415
  ...options
5064
6416
  };
5065
6417
  return new _TasksManager(context, resolved);
5066
6418
  }
6419
+ /**
6420
+ * Idempotently ensure the three backing tables exist for this queue.
6421
+ *
6422
+ * @param {{ recreate?: boolean }} [options]
6423
+ * @returns {Promise<void>}
6424
+ */
5067
6425
  async ensureTaskTables(options = {}) {
5068
6426
  await ensureTaskTables(this.context, {
5069
- queue: this.queue,
6427
+ queueName: this.queueName,
5070
6428
  recreate: options.recreate ?? this.recreateTaskTables
5071
6429
  });
5072
6430
  }
6431
+ /**
6432
+ * Start the runner loop using this manager's resolved config. Per-call
6433
+ * options override the stored defaults, but `runnerMetadata` still falls
6434
+ * through when omitted.
6435
+ *
6436
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
6437
+ * @returns {Promise<void>}
6438
+ */
5073
6439
  async runTasksLoop(options = {}) {
5074
6440
  await runTasksLoop(this.context, {
5075
- queue: options.queue ?? this.queue,
6441
+ queueName: options.queueName ?? this.queueName,
5076
6442
  target: options.target ?? this.target,
5077
6443
  pollMs: options.pollMs ?? this.pollMs,
6444
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
5078
6445
  maxParallel: options.maxParallel ?? this.maxParallel,
5079
6446
  scanLimit: options.scanLimit ?? this.scanLimit,
5080
6447
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
5081
- registry: options.registry ?? this.registry
6448
+ registry: options.registry ?? this.registry,
6449
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
6450
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
6451
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
6452
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
6453
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
6454
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
6455
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
6456
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
5082
6457
  });
5083
6458
  }
5084
6459
  };
5085
6460
  export {
6461
+ AbstractTask,
5086
6462
  Args,
5087
- Box5 as Box,
6463
+ Box4 as Box,
5088
6464
  Db,
5089
6465
  Divider,
5090
6466
  FileDatabase,
@@ -5097,14 +6473,16 @@ export {
5097
6473
  MultiColumnListComponent,
5098
6474
  MultiColumnListWithPreviewComponent,
5099
6475
  Params,
5100
- React5 as React,
6476
+ React2 as React,
6477
+ S3,
6478
+ SERVICE_TASK_NAMES,
5101
6479
  ScreenBody,
5102
6480
  ScreenContainer,
5103
6481
  ScreenDivider,
5104
6482
  ScreenFooter,
5105
6483
  ScreenRow,
5106
6484
  ScreenTitle,
5107
- TaskMaster,
6485
+ TaskGetLogs,
5108
6486
  TaskPing,
5109
6487
  TaskSampleProcess,
5110
6488
  TaskShellCommand,
@@ -5119,24 +6497,38 @@ export {
5119
6497
  buildBreadcrumb,
5120
6498
  buildDetailBreadcrumb,
5121
6499
  buildFooter,
5122
- dbConnect,
5123
- dbFindAndConnect,
5124
- dbInit,
6500
+ convertPattern,
5125
6501
  defaultFileSynopsisFunction,
5126
6502
  defaultTasksRegistry,
5127
6503
  defaultVersionSynopsisFunction,
5128
6504
  enqueueStopTask,
5129
6505
  enqueueTask,
5130
6506
  ensureTaskTables,
6507
+ flushTaskIpcLogs,
5131
6508
  getArgsInstance,
5132
6509
  createElement2 as h,
6510
+ ipcFileLogsTableNameForSourceResource,
5133
6511
  joiEdateType,
5134
6512
  joiStringArrayType,
6513
+ listServicesRegistry as listAliveRunnerHeartbeats,
6514
+ listServicesRegistry,
5135
6515
  listSources,
5136
6516
  listTables,
5137
6517
  load,
6518
+ matchesParsedPattern,
6519
+ memo,
6520
+ mergeAllowedTasksWithServiceTasks,
6521
+ nextTimeMatch,
6522
+ normalizeAllowedTasks,
5138
6523
  organizeFooterMessages,
5139
6524
  queueToTableNames,
6525
+ readTaskIpcLogsSnapshot,
6526
+ registerInServicesRegistry,
6527
+ registerInServicesRegistry as registerRunnerHeartbeat,
6528
+ resolveAsterisks,
6529
+ resolveIpcFileLogsDir,
6530
+ resolveRanges,
6531
+ resolveSteps,
5140
6532
  runNodeTaskScript,
5141
6533
  runTasksLoop,
5142
6534
  setupContext,
@@ -5146,12 +6538,20 @@ export {
5146
6538
  showMultiColumnListWithPreviewScreen,
5147
6539
  showScreen,
5148
6540
  showWordGridScreen,
6541
+ taskHistoryInsertFromQueueRow,
6542
+ timeMatcher,
6543
+ touchServicesRegistry as touchRunnerHeartbeat,
6544
+ touchServicesRegistry,
6545
+ unregisterServicesRegistry as unregisterRunnerHeartbeat,
6546
+ unregisterServicesRegistry,
6547
+ updateServicesRegistryMetadata,
5149
6548
  updateTaskProgress,
5150
6549
  useCallback,
5151
- useEffect3 as useEffect,
6550
+ useEffect2 as useEffect,
5152
6551
  useInput2 as useInput,
6552
+ useLayoutEffect,
5153
6553
  useMemo,
5154
- useRef3 as useRef,
6554
+ useRef2 as useRef,
5155
6555
  useState3 as useState,
5156
6556
  waitForTaskResult
5157
6557
  };