@nmakarov/cli-toolkit 0.21.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 (68) hide show
  1. package/dist/args.cjs +1 -4
  2. package/dist/args.cjs.map +1 -1
  3. package/dist/args.js +1 -1
  4. package/dist/args.js.map +1 -1
  5. package/dist/cli-runner.cjs +1252 -604
  6. package/dist/cli-runner.cjs.map +1 -1
  7. package/dist/cli-runner.js +1299 -650
  8. package/dist/cli-runner.js.map +1 -1
  9. package/dist/db.cjs +85 -157
  10. package/dist/db.cjs.map +1 -1
  11. package/dist/db.js +84 -150
  12. package/dist/db.js.map +1 -1
  13. package/dist/errors.cjs +2 -2
  14. package/dist/errors.cjs.map +1 -1
  15. package/dist/errors.js +2 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/filedatabase.cjs +19 -19
  18. package/dist/filedatabase.cjs.map +1 -1
  19. package/dist/filedatabase.js +19 -16
  20. package/dist/filedatabase.js.map +1 -1
  21. package/dist/http-client.cjs +9 -11
  22. package/dist/http-client.cjs.map +1 -1
  23. package/dist/http-client.js +10 -9
  24. package/dist/http-client.js.map +1 -1
  25. package/dist/http-client2.cjs +34 -37
  26. package/dist/http-client2.cjs.map +1 -1
  27. package/dist/http-client2.js +34 -34
  28. package/dist/http-client2.js.map +1 -1
  29. package/dist/index.cjs +1739 -720
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +1746 -721
  32. package/dist/index.js.map +1 -1
  33. package/dist/init.cjs +81 -68
  34. package/dist/init.cjs.map +1 -1
  35. package/dist/init.js +96 -82
  36. package/dist/init.js.map +1 -1
  37. package/dist/logger.cjs +5 -5
  38. package/dist/logger.cjs.map +1 -1
  39. package/dist/logger.js +5 -4
  40. package/dist/logger.js.map +1 -1
  41. package/dist/mock-server.cjs +21 -33
  42. package/dist/mock-server.cjs.map +1 -1
  43. package/dist/mock-server.js +21 -28
  44. package/dist/mock-server.js.map +1 -1
  45. package/dist/params.cjs +6 -9
  46. package/dist/params.cjs.map +1 -1
  47. package/dist/params.js +6 -6
  48. package/dist/params.js.map +1 -1
  49. package/dist/s3.cjs +286 -0
  50. package/dist/s3.cjs.map +1 -0
  51. package/dist/s3.js +273 -0
  52. package/dist/s3.js.map +1 -0
  53. package/dist/screen.cjs +34 -39
  54. package/dist/screen.cjs.map +1 -1
  55. package/dist/screen.js +48 -46
  56. package/dist/screen.js.map +1 -1
  57. package/dist/tasks.cjs +1354 -501
  58. package/dist/tasks.cjs.map +1 -1
  59. package/dist/tasks.js +1369 -527
  60. package/dist/tasks.js.map +1 -1
  61. package/dist/utils.cjs +7 -8
  62. package/dist/utils.cjs.map +1 -1
  63. package/dist/utils.js +6 -6
  64. package/dist/utils.js.map +1 -1
  65. package/package.json +32 -47
  66. package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
  67. package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
  68. package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
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,25 +874,24 @@ 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];
@@ -909,12 +905,11 @@ function buildDetailBreadcrumb(path5, suffix = "") {
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
@@ -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
  }
@@ -2063,14 +2056,14 @@ var Params = class _Params {
2063
2056
  }
2064
2057
  };
2065
2058
 
2066
- // src/screen.ts
2059
+ // src/index.js
2067
2060
  init_screen();
2068
2061
 
2069
- // src/filedatabase/index.ts
2062
+ // src/filedatabase/index.js
2070
2063
  import fs3 from "fs";
2071
2064
  import path3 from "path";
2072
2065
 
2073
- // src/utils/os-utils.ts
2066
+ // src/utils/os-utils.js
2074
2067
  import fs from "fs";
2075
2068
  import path from "path";
2076
2069
  import { execSync } from "child_process";
@@ -2099,7 +2092,7 @@ function getFreeDiskSpace(targetPath) {
2099
2092
  }
2100
2093
  }
2101
2094
 
2102
- // src/utils/fs-utils.ts
2095
+ // src/utils/fs-utils.js
2103
2096
  import fs2 from "fs";
2104
2097
  import path2 from "path";
2105
2098
  async function ensurePath(...pathParts) {
@@ -2123,7 +2116,7 @@ function getFileExtension(dataType) {
2123
2116
  }
2124
2117
  }
2125
2118
 
2126
- // src/utils/format-utils.ts
2119
+ // src/utils/format-utils.js
2127
2120
  function bytesToHumanReadable(bytes) {
2128
2121
  if (bytes === 0) return "0 B";
2129
2122
  const k = 1024;
@@ -2132,7 +2125,7 @@ function bytesToHumanReadable(bytes) {
2132
2125
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
2133
2126
  }
2134
2127
 
2135
- // src/utils/date-utils.ts
2128
+ // src/utils/date-utils.js
2136
2129
  function isTimestampFolder(folderName) {
2137
2130
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
2138
2131
  if (!isoRegex.test(folderName)) {
@@ -2142,7 +2135,7 @@ function isTimestampFolder(folderName) {
2142
2135
  return !isNaN(date.getTime()) && date.getTime() > 0;
2143
2136
  }
2144
2137
 
2145
- // src/filedatabase/serializers.ts
2138
+ // src/filedatabase/serializers.js
2146
2139
  function detectDataType(data) {
2147
2140
  if (Array.isArray(data)) {
2148
2141
  return "json-array";
@@ -2174,7 +2167,7 @@ function deserializeData(rawData, dataType) {
2174
2167
  }
2175
2168
  }
2176
2169
 
2177
- // src/filedatabase/synopsis-functions.ts
2170
+ // src/filedatabase/synopsis-functions.js
2178
2171
  function defaultFileSynopsisFunction(fileEntry, data) {
2179
2172
  if (!Array.isArray(data) || data.length === 0) {
2180
2173
  return { ...fileEntry };
@@ -2242,7 +2235,7 @@ function defaultVersionSynopsisFunction(metadata) {
2242
2235
  return result;
2243
2236
  }
2244
2237
 
2245
- // src/filedatabase/index.ts
2238
+ // src/filedatabase/index.js
2246
2239
  var FileDatabase = class _FileDatabase {
2247
2240
  basePath;
2248
2241
  namespace;
@@ -2328,7 +2321,7 @@ var FileDatabase = class _FileDatabase {
2328
2321
  if (errors.length) {
2329
2322
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
2330
2323
  }
2331
- let parts = [this.basePath, this.namespace];
2324
+ const parts = [this.basePath, this.namespace];
2332
2325
  if (this.tableName) {
2333
2326
  parts.push(...this.tableName.split("/"));
2334
2327
  }
@@ -2821,14 +2814,17 @@ var FileDatabase = class _FileDatabase {
2821
2814
  * Prepare the instance for read or write operations
2822
2815
  * This discovers state and sets up internal members based on mode and current data
2823
2816
  */
2824
- async prepare({ write, read, version }) {
2817
+ async prepare(options) {
2818
+ const { write, read, version, deferInitialVersion } = options;
2825
2819
  if (write) {
2826
2820
  if (this.versioned) {
2827
2821
  if (this.currentVersion === null) {
2828
- await this.makeNewVersion();
2829
- this.metadata = this.getDefaultMetadata();
2830
- this.metadata.version = this.currentVersion;
2831
- this.makeNewFile();
2822
+ if (!deferInitialVersion) {
2823
+ await this.makeNewVersion();
2824
+ this.metadata = this.getDefaultMetadata();
2825
+ this.metadata.version = this.currentVersion;
2826
+ this.makeNewFile();
2827
+ }
2832
2828
  } else {
2833
2829
  if (!this.metadata.files.length) {
2834
2830
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -2938,7 +2934,7 @@ var FileDatabase = class _FileDatabase {
2938
2934
  if (options.forceNewVersion && !this.versioned) {
2939
2935
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
2940
2936
  }
2941
- await this.prepare({ write: true });
2937
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
2942
2938
  const incomingDataType = detectDataType(data);
2943
2939
  this.metadata.dataType = incomingDataType;
2944
2940
  if (options.forceNewVersion) {
@@ -3253,22 +3249,57 @@ function listSources(basePath) {
3253
3249
  }
3254
3250
  }
3255
3251
 
3256
- // src/db/index.ts
3252
+ // src/db/index.js
3257
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
+ };
3258
3260
  var Db = class {
3259
- knexInstance = null;
3260
- config;
3261
- logger;
3262
- queriesLog = [];
3263
- isConnected = false;
3264
- /**
3265
- * Constructor - accepts config object
3266
- * Use dbInit() function to initialize with Context
3267
- */
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
+ }
3268
3296
  constructor(config2) {
3269
- if (!config2.connectionString) {
3297
+ if (!config2 || !config2.connectionString) {
3270
3298
  throw new ParamError("Db: connectionString is required");
3271
3299
  }
3300
+ this.knexInstance = null;
3301
+ this.isConnected = false;
3302
+ this.queriesLog = [];
3272
3303
  this.config = {
3273
3304
  testConnection: true,
3274
3305
  profile: false,
@@ -3281,25 +3312,23 @@ var Db = class {
3281
3312
  };
3282
3313
  this.logger = this.config.logger;
3283
3314
  const instance2 = this;
3284
- const callableWrapper = function(...args) {
3315
+ const callableWrapper = function() {
3285
3316
  throw new Error("This should never be called directly");
3286
3317
  };
3287
3318
  callableWrapper._instance = instance2;
3288
3319
  return new Proxy(callableWrapper, {
3289
- // Intercept function calls: db('table')
3290
- apply: (target, thisArg, argumentsList) => {
3320
+ apply: (target, _thisArg, argumentsList) => {
3291
3321
  const inst = target._instance;
3292
3322
  if (!inst.knexInstance) {
3293
3323
  throw new Error("Db: Not connected. Call connect() first.");
3294
3324
  }
3295
3325
  return inst.knexInstance(...argumentsList);
3296
3326
  },
3297
- // Intercept property access: db.schema, db.raw, etc.
3298
3327
  get: (target, prop) => {
3299
3328
  if (prop === "_instance") {
3300
3329
  return target._instance;
3301
3330
  }
3302
- const instance3 = target._instance;
3331
+ const inst = target._instance;
3303
3332
  const ownMethods = [
3304
3333
  "connect",
3305
3334
  "disconnect",
@@ -3312,26 +3341,26 @@ var Db = class {
3312
3341
  "detectClient",
3313
3342
  "attachProfiler"
3314
3343
  ];
3315
- if (prop in instance3) {
3316
- const value = instance3[prop];
3344
+ if (prop in inst) {
3345
+ const value = inst[prop];
3317
3346
  if (typeof value === "function" && ownMethods.includes(prop)) {
3318
- return value.bind(instance3);
3347
+ return value.bind(inst);
3319
3348
  }
3320
3349
  if (typeof value !== "function") {
3321
3350
  return value;
3322
3351
  }
3323
3352
  }
3324
- if (instance3.knexInstance) {
3325
- const knexProp = instance3.knexInstance[prop];
3353
+ if (inst.knexInstance) {
3354
+ const knexProp = inst.knexInstance[prop];
3326
3355
  if (typeof knexProp === "function") {
3327
- return knexProp.bind(instance3.knexInstance);
3356
+ return knexProp.bind(inst.knexInstance);
3328
3357
  }
3329
3358
  return knexProp;
3330
3359
  }
3331
- if (prop in instance3) {
3332
- const method = instance3[prop];
3360
+ if (prop in inst) {
3361
+ const method = inst[prop];
3333
3362
  if (typeof method === "function") {
3334
- return method.bind(instance3);
3363
+ return method.bind(inst);
3335
3364
  }
3336
3365
  return method;
3337
3366
  }
@@ -3339,9 +3368,6 @@ var Db = class {
3339
3368
  }
3340
3369
  });
3341
3370
  }
3342
- /**
3343
- * Detect database client type from connection string
3344
- */
3345
3371
  detectClient(connectionString) {
3346
3372
  if (connectionString.match(/^postgresql/)) {
3347
3373
  return "pg";
@@ -3351,9 +3377,6 @@ var Db = class {
3351
3377
  }
3352
3378
  return null;
3353
3379
  }
3354
- /**
3355
- * Connect to the database
3356
- */
3357
3380
  async connect() {
3358
3381
  if (this.isConnected && this.knexInstance) {
3359
3382
  this.logger.warn?.("[Db] Already connected");
@@ -3362,14 +3385,13 @@ var Db = class {
3362
3385
  const client = this.detectClient(this.config.connectionString);
3363
3386
  if (!client) {
3364
3387
  throw new ParamError(
3365
- `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://"
3366
3389
  );
3367
3390
  }
3368
3391
  try {
3369
3392
  const connectionConfig = {
3370
3393
  connectionString: this.config.connectionString,
3371
3394
  family: 4
3372
- // Force IPv4 only (disable IPv6)
3373
3395
  };
3374
3396
  this.knexInstance = knex({
3375
3397
  client,
@@ -3385,7 +3407,9 @@ var Db = class {
3385
3407
  await this.testConnection();
3386
3408
  }
3387
3409
  this.isConnected = true;
3388
- 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
+ );
3389
3413
  } catch (error) {
3390
3414
  if (error instanceof ParamError) {
3391
3415
  throw error;
@@ -3394,9 +3418,6 @@ var Db = class {
3394
3418
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
3395
3419
  }
3396
3420
  }
3397
- /**
3398
- * Disconnect from the database
3399
- */
3400
3421
  async disconnect() {
3401
3422
  if (!this.knexInstance) {
3402
3423
  return;
@@ -3406,16 +3427,15 @@ var Db = class {
3406
3427
  this.knexInstance = null;
3407
3428
  this.isConnected = false;
3408
3429
  this.queriesLog = [];
3409
- 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
+ );
3410
3433
  } catch (error) {
3411
3434
  const errorMsg = this.getErrorMessage(error);
3412
3435
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
3413
3436
  throw error;
3414
3437
  }
3415
3438
  }
3416
- /**
3417
- * Extract error message from various error types
3418
- */
3419
3439
  getErrorMessage(error) {
3420
3440
  if (error instanceof AggregateError) {
3421
3441
  const errors = error.errors || [];
@@ -3440,9 +3460,11 @@ var Db = class {
3440
3460
  return `${code} (tried: ${addresses.join(", ")})`;
3441
3461
  }
3442
3462
  }
3443
- const uniqueMessages = [...new Set(errors.map((e) => {
3444
- return e instanceof Error ? e.message : String(e);
3445
- }))];
3463
+ const uniqueMessages = [
3464
+ ...new Set(
3465
+ errors.map((e) => e instanceof Error ? e.message : String(e))
3466
+ )
3467
+ ];
3446
3468
  if (uniqueMessages.length === 1) {
3447
3469
  return uniqueMessages[0];
3448
3470
  }
@@ -3451,28 +3473,25 @@ var Db = class {
3451
3473
  return error.message || "Multiple errors occurred";
3452
3474
  }
3453
3475
  if (error instanceof Error) {
3454
- const errorWithCode = error;
3455
- if (errorWithCode.code) {
3456
- return `${errorWithCode.code}: ${error.message || String(error)}`;
3476
+ const code = error.code;
3477
+ if (code) {
3478
+ return `${code}: ${error.message || String(error)}`;
3457
3479
  }
3458
3480
  return error.message || String(error);
3459
3481
  }
3460
3482
  if (typeof error === "string") {
3461
3483
  return error;
3462
3484
  }
3463
- if (error?.message) {
3485
+ if (error && typeof error === "object" && "message" in error) {
3464
3486
  const msg = String(error.message);
3465
- const errorWithCode = error;
3466
- if (errorWithCode.code) {
3467
- return `${errorWithCode.code}: ${msg}`;
3487
+ const code = error.code;
3488
+ if (code) {
3489
+ return `${code}: ${msg}`;
3468
3490
  }
3469
3491
  return msg;
3470
3492
  }
3471
3493
  return String(error) || "Unknown error";
3472
3494
  }
3473
- /**
3474
- * Test database connection
3475
- */
3476
3495
  async testConnection() {
3477
3496
  if (!this.knexInstance) {
3478
3497
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3488,9 +3507,6 @@ var Db = class {
3488
3507
  throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
3489
3508
  }
3490
3509
  }
3491
- /**
3492
- * Attach query profiler to log all queries
3493
- */
3494
3510
  attachProfiler() {
3495
3511
  if (!this.knexInstance) {
3496
3512
  return;
@@ -3500,7 +3516,7 @@ var Db = class {
3500
3516
  this.knexInstance.on("query", (query) => {
3501
3517
  query.__startTime = process.hrtime();
3502
3518
  });
3503
- this.knexInstance.on("query-response", (response, query) => {
3519
+ this.knexInstance.on("query-response", (_response, query) => {
3504
3520
  const [seconds, nanoseconds] = process.hrtime(query.__startTime);
3505
3521
  const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
3506
3522
  const logEntry = {
@@ -3515,15 +3531,9 @@ var Db = class {
3515
3531
  this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
3516
3532
  });
3517
3533
  }
3518
- /**
3519
- * Get query log (only available if profiling is enabled)
3520
- */
3521
3534
  getQueryLog() {
3522
3535
  return [...this.queriesLog];
3523
3536
  }
3524
- /**
3525
- * Check if a table exists
3526
- */
3527
3537
  async tableExists(tableName) {
3528
3538
  if (!this.knexInstance) {
3529
3539
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3535,65 +3545,28 @@ var Db = class {
3535
3545
  throw error;
3536
3546
  }
3537
3547
  }
3538
- /**
3539
- * Get the underlying Knex instance (for advanced usage)
3540
- */
3541
3548
  getKnex() {
3542
3549
  if (!this.knexInstance) {
3543
3550
  throw new Error("Db: Not connected. Call connect() first.");
3544
3551
  }
3545
3552
  return this.knexInstance;
3546
3553
  }
3547
- /**
3548
- * Get connection status
3549
- */
3550
3554
  isConnectedToDb() {
3551
3555
  return this.isConnected && this.knexInstance !== null;
3552
3556
  }
3553
- /**
3554
- * Initialize Db with context (connects and registers disconnect cleanup).
3555
- * Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
3556
- */
3557
- static async init(context, dbNameOrConnectionString) {
3558
- return dbFindAndConnect(context, dbNameOrConnectionString);
3559
- }
3560
3557
  };
3561
3558
  function capitalizeFirstLetter(str) {
3562
3559
  return str.charAt(0).toUpperCase() + str.slice(1);
3563
3560
  }
3564
- async function dbConnect(context, connectionString, name, dbProfile) {
3565
- const defs = {
3566
- testDbConnection: "boolean default true",
3567
- name: "string",
3568
- poolMin: "number default 2",
3569
- poolMax: "number default 10",
3570
- acquireConnectionTimeout: "number default 10000",
3571
- sslRejectUnauthorized: "boolean default false"
3572
- };
3573
- const paramsConfig = context.params.getAllForModule(defs);
3574
- const config2 = {
3575
- connectionString,
3576
- name: paramsConfig.name || name || "default",
3577
- testConnection: paramsConfig.testDbConnection,
3578
- profile: dbProfile ?? false,
3579
- pool: {
3580
- min: paramsConfig.poolMin,
3581
- max: paramsConfig.poolMax
3582
- },
3583
- acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
3584
- ssl: {
3585
- rejectUnauthorized: paramsConfig.sslRejectUnauthorized
3586
- },
3587
- logger: context.logger
3588
- };
3561
+ async function dbConnect(context, config2) {
3589
3562
  try {
3590
3563
  const db = new Db(config2);
3591
3564
  context.registerCleanup(async () => {
3592
3565
  await db.disconnect();
3593
- context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
3566
+ context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
3594
3567
  });
3595
3568
  await db.connect();
3596
- context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
3569
+ context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
3597
3570
  return db;
3598
3571
  } catch (error) {
3599
3572
  if (error instanceof ParamError) {
@@ -3603,52 +3576,266 @@ async function dbConnect(context, connectionString, name, dbProfile) {
3603
3576
  throw new ParamError(`[Db] connect error: ${errorMsg}`);
3604
3577
  }
3605
3578
  }
3606
- async function dbFindAndConnect(context, dbNameOrConnectionString) {
3607
- let dbName;
3608
- let dbConnectionString;
3609
- let dbProfile;
3610
- if (dbNameOrConnectionString) {
3611
- if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
3612
- dbName = void 0;
3613
- dbConnectionString = dbNameOrConnectionString;
3614
- } else {
3615
- dbName = dbNameOrConnectionString;
3616
- }
3617
- } else {
3618
- const defs = {
3619
- dbName: "string",
3620
- dbConnectionString: "string",
3621
- 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")
3622
3617
  };
3623
- const paramsConfig = context.params.getAll(defs);
3624
- dbName = paramsConfig.dbName;
3625
- dbConnectionString = paramsConfig.dbConnectionString;
3626
- dbProfile = paramsConfig.dbProfile;
3627
- }
3628
- if (!dbName && !dbConnectionString) {
3629
- throw new ParamError("Db: either dbName or dbConnectionString must be specified");
3630
- }
3631
- if (dbName) {
3632
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3633
- dbConnectionString = await context.params.get(paramName, "string");
3634
- if (!dbConnectionString) {
3618
+ if (!config2.bucketName) {
3635
3619
  throw new ParamError(
3636
- `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()})`
3637
3621
  );
3638
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);
3639
3654
  }
3640
- const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
3641
- return db;
3642
- }
3643
- async function dbInit(context, dbNameOrConnectionString) {
3644
- return await dbFindAndConnect(context, dbNameOrConnectionString);
3645
- }
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
+ };
3646
3833
 
3647
- // src/logger/index.ts
3834
+ // src/logger/index.js
3648
3835
  import chalk from "chalk";
3649
3836
  import util from "util";
3650
3837
 
3651
- // src/logger/transports.ts
3838
+ // src/logger/transports.js
3652
3839
  var ConsoleTransport = class {
3653
3840
  write(payload) {
3654
3841
  console.info(payload);
@@ -3668,7 +3855,7 @@ var ParentProcessTransport = class {
3668
3855
  }
3669
3856
  };
3670
3857
 
3671
- // src/logger/index.ts
3858
+ // src/logger/index.js
3672
3859
  var ALL_LEVELS = [
3673
3860
  "silly",
3674
3861
  "debug",
@@ -3741,6 +3928,7 @@ var Logger = class _Logger {
3741
3928
  }
3742
3929
  /**
3743
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.
3744
3932
  */
3745
3933
  static init(context, options) {
3746
3934
  const paramDefs = {
@@ -3754,7 +3942,7 @@ var Logger = class _Logger {
3754
3942
  progressWithTimes: "boolean default false",
3755
3943
  progressThrottleMs: "number"
3756
3944
  };
3757
- const discovered = context.params.getAllForModule(paramDefs);
3945
+ const discovered = context.params.getAllForModule("logger", paramDefs);
3758
3946
  const config2 = { ...discovered, ...options };
3759
3947
  const logger = new _Logger(context, config2);
3760
3948
  context.logger = logger;
@@ -3950,9 +4138,9 @@ var Logger = class _Logger {
3950
4138
  }
3951
4139
  };
3952
4140
 
3953
- // src/init/index.ts
4141
+ // src/init/index.js
3954
4142
  import { EventEmitter } from "events";
3955
- function extractComponentOptions(opts, componentName) {
4143
+ function extractComponentOptions(opts, _componentName) {
3956
4144
  const reservedKeys = ["overrides", "defaults", "modules"];
3957
4145
  const componentOptions = {};
3958
4146
  for (const [key, value] of Object.entries(opts)) {
@@ -3997,7 +4185,10 @@ function setupContext(opts = {}) {
3997
4185
  return setup(opts);
3998
4186
  }
3999
4187
 
4000
- // src/utils/core-utils.ts
4188
+ // src/tasks/index.js
4189
+ import os3 from "os";
4190
+
4191
+ // src/utils/core-utils.js
4001
4192
  function sleepMs(ms) {
4002
4193
  return new Promise((resolve2) => setTimeout(resolve2, ms));
4003
4194
  }
@@ -4006,14 +4197,82 @@ function toJsonColumn(value) {
4006
4197
  return JSON.stringify(value);
4007
4198
  }
4008
4199
 
4009
- // src/tasks/servicesRegistry.ts
4010
- import { randomUUID as randomUUID2 } from "crypto";
4011
- import { mkdir, readFile, writeFile } from "fs/promises";
4200
+ // src/tasks/servicesRegistry.js
4012
4201
  import os from "os";
4013
- import path4 from "path";
4014
4202
 
4015
- // src/tasks/taskUtils.ts
4203
+ // src/tasks/taskUtils.js
4016
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
4017
4276
  function getDb(context) {
4018
4277
  const db = context.db;
4019
4278
  if (!db) {
@@ -4021,113 +4280,117 @@ function getDb(context) {
4021
4280
  }
4022
4281
  return db;
4023
4282
  }
4024
- function queueToTableNames(queue) {
4025
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4026
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4027
- }
4283
+ function queueToTableNames(queueName) {
4028
4284
  return {
4029
- tasksTable: queue,
4030
- historyTable: `${queue}_history`
4285
+ tasksTable: queueName,
4286
+ historyTable: `${queueName}_history`,
4287
+ registryTable: `${queueName}_services_registry`
4031
4288
  };
4032
4289
  }
4033
- function servicesRegistryTable(queue) {
4034
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4035
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4036
- }
4037
- return `${queue}_services_registry`;
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;
4317
+ return {
4318
+ ...snapshot,
4319
+ ...overrides
4320
+ };
4038
4321
  }
4039
4322
  async function ensureTaskTables(context, options = {}) {
4040
- const queue = options.queue ?? "tasks";
4323
+ const queueName = options.queueName ?? "tasks";
4041
4324
  const recreate = options.recreate ?? false;
4042
4325
  const db = getDb(context);
4043
- const { tasksTable, historyTable } = queueToTableNames(queue);
4326
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
4044
4327
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
4045
4328
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
4329
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
4046
4330
  if (recreate) {
4047
4331
  await db.schema.dropTableIfExists(historyTable);
4048
4332
  await db.schema.dropTableIfExists(tasksTable);
4333
+ await db.schema.dropTableIfExists(registryTable);
4049
4334
  }
4050
4335
  if (needsTasks) {
4051
4336
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
4052
4337
  await db.schema.createTable(tasksTable, (t) => {
4053
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4054
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4055
- t.timestamp("started_at");
4056
- t.timestamp("completed_at");
4057
- t.integer("priority").notNullable().defaultTo(0);
4058
- t.text("schedule");
4059
- t.timestamp("past_due").defaultTo(null);
4060
- t.text("target").notNullable();
4061
- t.text("task").notNullable();
4062
- t.json("params");
4063
- t.text("opid");
4064
- t.timestamp("paused_at").defaultTo(null);
4065
- t.text("progress");
4066
- t.boolean("success");
4067
- t.json("results");
4068
- });
4069
- await db.schema.alterTable(tasksTable, (t) => {
4070
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
4071
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
4072
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
4338
+ defineTasksTable(t, db, tasksTable);
4073
4339
  });
4074
4340
  }
4075
4341
  if (needsHistory) {
4076
4342
  await db.schema.createTable(historyTable, (t) => {
4077
- t.uuid("id").notNullable();
4078
- t.timestamp("created_at").notNullable();
4079
- t.timestamp("started_at");
4080
- t.timestamp("completed_at");
4081
- t.integer("priority").notNullable().defaultTo(0);
4082
- t.text("schedule");
4083
- t.timestamp("past_due").defaultTo(null);
4084
- t.text("target").notNullable();
4085
- t.text("task").notNullable();
4086
- t.json("params");
4087
- t.text("opid");
4088
- t.text("progress");
4089
- t.boolean("success");
4090
- t.json("results");
4091
- });
4092
- await db.schema.alterTable(historyTable, (t) => {
4093
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
4094
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
4343
+ defineTasksTable(t, db, historyTable);
4095
4344
  });
4096
4345
  }
4097
- const registryTable = servicesRegistryTable(queue);
4098
- const needsRegistry = !await db.tableExists(registryTable);
4099
4346
  if (needsRegistry) {
4100
4347
  await db.schema.createTable(registryTable, (t) => {
4101
4348
  t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4102
- t.uuid("instance_id").notNullable().unique();
4103
- t.text("queue").notNullable();
4349
+ t.text("queue_name").notNullable();
4104
4350
  t.text("service_group").notNullable();
4351
+ t.integer("instance_number").notNullable().defaultTo(1);
4105
4352
  t.text("service_name").notNullable();
4106
- t.text("target").notNullable();
4107
- t.text("hostname");
4353
+ t.text("server_name").notNullable();
4108
4354
  t.integer("pid");
4109
4355
  t.json("metadata");
4110
4356
  t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4111
4357
  t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
4112
- t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
4113
- t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
4114
- t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
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`);
4115
4361
  });
4116
4362
  }
4117
4363
  }
4118
4364
  async function enqueueTask(context, options) {
4119
4365
  const db = getDb(context);
4120
- const queue = options.queue ?? "tasks";
4121
- const { tasksTable } = queueToTableNames(queue);
4366
+ const queueName = options.queueName ?? "tasks";
4367
+ const { tasksTable } = queueToTableNames(queueName);
4122
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
+ }
4123
4380
  await db(tasksTable).insert({
4124
4381
  id,
4125
- target: options.target,
4126
- task: options.task,
4382
+ name,
4127
4383
  params: toJsonColumn(options.params ?? null),
4128
4384
  opid: options.opid ?? null,
4129
- priority: options.priority ?? 0,
4130
- 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()
4131
4394
  });
4132
4395
  return id;
4133
4396
  }
@@ -4138,7 +4401,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
4138
4401
  });
4139
4402
  }
4140
4403
 
4141
- // src/tasks/servicesRegistry.ts
4404
+ // src/tasks/servicesRegistry.js
4142
4405
  function getDb2(context) {
4143
4406
  const db = context.db;
4144
4407
  if (!db) {
@@ -4152,7 +4415,7 @@ function parseMetadataColumn(value) {
4152
4415
  if (typeof value === "string") {
4153
4416
  try {
4154
4417
  const p = JSON.parse(value);
4155
- return p && typeof p === "object" && !Array.isArray(p) ? p : {};
4418
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
4156
4419
  } catch {
4157
4420
  return {};
4158
4421
  }
@@ -4162,6 +4425,7 @@ function parseMetadataColumn(value) {
4162
4425
  var DEFAULT_GROUP_MAX_INSTANCES = {
4163
4426
  intake: 1,
4164
4427
  harvest: 1,
4428
+ harvester: 0,
4165
4429
  loader: 0,
4166
4430
  photos: 0,
4167
4431
  photosprocessor: 0,
@@ -4171,25 +4435,6 @@ function sanitizeNamePart(raw) {
4171
4435
  const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4172
4436
  return s.slice(0, 80) || "runner";
4173
4437
  }
4174
- function identityFilePath(identityDir, queue, serviceGroup) {
4175
- const safeQ = sanitizeNamePart(queue);
4176
- const safeG = sanitizeNamePart(serviceGroup);
4177
- return path4.join(identityDir, `${safeQ}_${safeG}.json`);
4178
- }
4179
- async function readIdentityFile(filePath) {
4180
- try {
4181
- const text = await readFile(filePath, "utf8");
4182
- const parsed = JSON.parse(text);
4183
- return parsed && typeof parsed === "object" ? parsed : {};
4184
- } catch {
4185
- return {};
4186
- }
4187
- }
4188
- async function writeIdentityFile(filePath, data) {
4189
- await mkdir(path4.dirname(filePath), { recursive: true });
4190
- await writeFile(filePath, `${JSON.stringify(data, null, 2)}
4191
- `, "utf8");
4192
- }
4193
4438
  function resolveMaxInstances(serviceGroup, override) {
4194
4439
  if (override !== void 0 && Number.isFinite(override)) {
4195
4440
  return Math.max(0, Math.floor(Number(override)));
@@ -4197,179 +4442,190 @@ function resolveMaxInstances(serviceGroup, override) {
4197
4442
  const g = serviceGroup.trim().toLowerCase();
4198
4443
  return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
4199
4444
  }
4200
- async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
4445
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
4201
4446
  const cutoff = new Date(Date.now() - staleMs);
4202
- let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
4203
- if (excludeInstanceId) {
4204
- q = q.whereNot("instance_id", excludeInstanceId);
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);
4205
4450
  }
4206
4451
  const row = await q.count("id as count").first();
4207
4452
  return Number(row?.count ?? 0);
4208
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
+ }
4209
4464
  function isUniqueViolation(error) {
4210
4465
  const code = error?.code ?? error?.errno;
4211
4466
  return code === "23505" || String(error?.message || "").includes("duplicate key");
4212
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
+ }
4213
4495
  async function registerInServicesRegistry(context, options) {
4214
4496
  const db = getDb2(context);
4215
- const registryTable = servicesRegistryTable(options.queue);
4497
+ const registryTable = queueToTableNames(options.queueName).registryTable;
4216
4498
  const serviceGroup = options.serviceGroup.trim();
4217
4499
  if (!serviceGroup) {
4218
4500
  throw new Error("registerInServicesRegistry: serviceGroup is required");
4219
4501
  }
4220
- const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
4221
- let identity = await readIdentityFile(identityPath);
4222
- let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : randomUUID2();
4223
- identity.instanceId = instanceId;
4224
- await writeIdentityFile(identityPath, identity);
4225
- const hostname = os.hostname();
4502
+ const serverName = os.hostname();
4226
4503
  const pid = typeof process.pid === "number" ? process.pid : null;
4227
- const meta = toJsonColumn(options.metadata ?? null);
4228
- const existing = await db(registryTable).where({ instance_id: instanceId }).first();
4229
- if (existing) {
4230
- await db(registryTable).where({ instance_id: instanceId }).update({
4231
- target: options.target,
4232
- hostname,
4233
- pid,
4234
- metadata: meta,
4235
- last_seen_at: db.fn.now()
4236
- });
4237
- const serviceName = String(existing.service_name);
4238
- identity.serviceName = serviceName;
4239
- await writeIdentityFile(identityPath, identity);
4240
- const reg = {
4241
- instanceId,
4242
- serviceName,
4243
- serviceGroup,
4244
- queue: options.queue,
4245
- target: options.target,
4246
- rowId: String(existing.id)
4247
- };
4248
- context.servicesRegistry = reg;
4249
- context.runnerHeartbeat = reg;
4250
- context.logger.info?.(
4251
- `[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
4252
- );
4253
- return {
4254
- instanceId,
4255
- serviceName,
4256
- serviceGroup,
4257
- queue: options.queue,
4258
- target: options.target,
4259
- rowId: String(existing.id),
4260
- registryTable
4261
- };
4262
- }
4504
+ const meta = buildMetadata(options);
4505
+ const groupBase = sanitizeNamePart(serviceGroup);
4506
+ const hostBase = sanitizeNamePart(serverName);
4263
4507
  const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
4264
- const aliveOthers = await countAliveInGroup(
4265
- db,
4266
- registryTable,
4267
- options.queue,
4268
- serviceGroup,
4269
- options.staleMs,
4270
- instanceId
4271
- );
4272
- if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
4273
- const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
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}).`;
4274
4511
  if (options.enforceMaxInstances) {
4275
4512
  throw new Error(msg);
4276
4513
  }
4277
4514
  context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
4278
4515
  }
4279
- const explicitName = options.serviceName?.trim();
4280
- const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
4281
- const hostBase = sanitizeNamePart(hostname);
4282
- const groupBase = sanitizeNamePart(serviceGroup);
4283
- const baseCandidates = [];
4284
- if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
4285
- if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
4286
- baseCandidates.push(`${groupBase}-${hostBase}`);
4287
- baseCandidates.push(groupBase);
4288
- function* eachServiceNameCandidate(bases) {
4289
- const seen = /* @__PURE__ */ new Set();
4290
- for (const rawBase of bases) {
4291
- const base = sanitizeNamePart(rawBase);
4292
- if (!base) continue;
4293
- const seq = [base];
4294
- for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
4295
- for (const c of seq) {
4296
- if (seen.has(c)) continue;
4297
- seen.add(c);
4298
- yield c;
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;
4299
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;
4300
4567
  }
4301
- }
4302
- let inserted;
4303
- for (const candidate of eachServiceNameCandidate(baseCandidates)) {
4304
4568
  try {
4305
4569
  const rows = await db(registryTable).insert({
4306
- instance_id: instanceId,
4307
- queue: options.queue,
4570
+ queue_name: options.queueName,
4308
4571
  service_group: serviceGroup,
4309
- service_name: candidate,
4310
- target: options.target,
4311
- hostname,
4572
+ instance_number: instanceNumber,
4573
+ service_name: serviceNameRaw,
4574
+ server_name: serverName,
4312
4575
  pid,
4313
4576
  metadata: meta,
4314
- last_seen_at: db.fn.now()
4577
+ last_seen_at: db.fn.now(),
4578
+ created_at: db.fn.now()
4315
4579
  }).returning(["id", "service_name"]);
4316
4580
  const row = Array.isArray(rows) ? rows[0] : rows;
4317
- if (row) {
4318
- inserted = { id: String(row.id), service_name: String(row.service_name) };
4319
- break;
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) : "";
4320
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;
4321
4602
  } catch (error) {
4322
4603
  if (!isUniqueViolation(error)) {
4323
4604
  throw error;
4324
4605
  }
4606
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
4325
4607
  }
4326
4608
  }
4327
- if (!inserted) {
4328
- throw new Error(
4329
- `[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
4330
- );
4331
- }
4332
- identity.serviceName = inserted.service_name;
4333
- await writeIdentityFile(identityPath, identity);
4334
- const regNew = {
4335
- instanceId,
4336
- serviceName: inserted.service_name,
4337
- serviceGroup,
4338
- queue: options.queue,
4339
- target: options.target,
4340
- rowId: inserted.id
4341
- };
4342
- context.servicesRegistry = regNew;
4343
- context.runnerHeartbeat = regNew;
4344
- context.logger.info?.(
4345
- `[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
4609
+ throw new Error(
4610
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
4346
4611
  );
4347
- return {
4348
- instanceId,
4349
- serviceName: inserted.service_name,
4350
- serviceGroup,
4351
- queue: options.queue,
4352
- target: options.target,
4353
- rowId: inserted.id,
4354
- registryTable
4355
- };
4356
4612
  }
4357
4613
  async function touchServicesRegistry(context, registration) {
4358
4614
  const db = getDb2(context);
4359
- const hostname = os.hostname();
4615
+ const serverName = os.hostname();
4360
4616
  const pid = typeof process.pid === "number" ? process.pid : null;
4361
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
4617
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4362
4618
  last_seen_at: db.fn.now(),
4363
- hostname,
4619
+ server_name: serverName,
4364
4620
  pid
4365
4621
  });
4366
4622
  }
4367
4623
  async function updateServicesRegistryMetadata(context, registration, patch) {
4368
4624
  const db = getDb2(context);
4369
- const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
4625
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
4370
4626
  const prev = parseMetadataColumn(row?.metadata);
4371
4627
  const merged = { ...prev, ...patch };
4372
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
4628
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4373
4629
  metadata: toJsonColumn(merged),
4374
4630
  last_seen_at: db.fn.now()
4375
4631
  });
@@ -4377,14 +4633,14 @@ async function updateServicesRegistryMetadata(context, registration, patch) {
4377
4633
  }
4378
4634
  async function unregisterServicesRegistry(context, registration) {
4379
4635
  const db = getDb2(context);
4380
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
4381
- context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
4636
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
4637
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
4382
4638
  }
4383
- async function listServicesRegistry(context, options = { queue: "tasks" }) {
4639
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
4384
4640
  const db = getDb2(context);
4385
4641
  const staleMs = options.staleMs ?? 6e4;
4386
4642
  const cutoff = new Date(Date.now() - staleMs);
4387
- const table = servicesRegistryTable(options.queue);
4643
+ const table = queueToTableNames(options.queueName).registryTable;
4388
4644
  let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
4389
4645
  if (options.serviceGroup?.trim()) {
4390
4646
  q = q.where({ service_group: options.serviceGroup.trim() });
@@ -4392,7 +4648,8 @@ async function listServicesRegistry(context, options = { queue: "tasks" }) {
4392
4648
  return await q;
4393
4649
  }
4394
4650
 
4395
- // src/tasks/taskLogs.ts
4651
+ // src/tasks/taskLogs.js
4652
+ import path4 from "path";
4396
4653
  function getLogsState(context) {
4397
4654
  const holder = context;
4398
4655
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -4445,33 +4702,135 @@ function getLogsState(context) {
4445
4702
  holder.__tasksLogsState = state;
4446
4703
  return state;
4447
4704
  }
4448
- function isErrorPayload(payload) {
4449
- if (!payload) return false;
4450
- if (typeof payload === "object") {
4451
- const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
4452
- if (level === "error" || level === "fatal") return true;
4453
- if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
4454
- return false;
4455
- }
4456
- if (typeof payload === "string") {
4457
- return /\berror\b/i.test(payload);
4458
- }
4459
- return false;
4705
+ function ipcLogTargetKey(target) {
4706
+ const bp = target.basePath ?? "";
4707
+ const ns = target.namespace ?? "";
4708
+ return `${bp}::${ns}::${target.tableName}`;
4460
4709
  }
4461
- function buildLogRecord(task, payload) {
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);
4793
+ return state;
4794
+ }
4795
+ function isErrorPayload(payload) {
4796
+ if (!payload) return false;
4797
+ if (typeof payload === "object") {
4798
+ const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
4799
+ if (level === "error" || level === "fatal") return true;
4800
+ if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
4801
+ return false;
4802
+ }
4803
+ if (typeof payload === "string") {
4804
+ return /\berror\b/i.test(payload);
4805
+ }
4806
+ return false;
4807
+ }
4808
+ function buildLogRecord(task, payload) {
4462
4809
  const params = task.params && typeof task.params === "object" ? task.params : {};
4463
4810
  return {
4464
4811
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4465
4812
  opid: task.opid ?? null,
4466
4813
  taskId: task.id,
4467
- taskName: task.task,
4468
- target: task.target,
4814
+ taskName: task.name,
4815
+ target: task.service_group,
4469
4816
  source: typeof params.source === "string" ? params.source : null,
4470
4817
  resource: typeof params.resource === "string" ? params.resource : null,
4471
4818
  payload
4472
4819
  };
4473
4820
  }
4474
- 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
+ }
4475
4834
  const state = getLogsState(context);
4476
4835
  if (!state.db && !state.errorDb) return;
4477
4836
  const record = buildLogRecord(task, payload);
@@ -4488,84 +4847,293 @@ function appendTaskIpcLog(context, task, payload) {
4488
4847
  context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
4489
4848
  });
4490
4849
  }
4491
-
4492
- // src/tasks/time-matcher.ts
4493
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4494
- function resolveAsterisks(field, range) {
4495
- return field.includes("*") ? field.replace("*", range) : field;
4496
- }
4497
- function resolveRanges(field) {
4498
- const regex = /(\d+)-(\d+)/;
4499
- let current = field;
4500
- while (true) {
4501
- const match = regex.exec(current);
4502
- if (!match) break;
4503
- const raw = match[0];
4504
- let first = Number(match[1]);
4505
- let last = Number(match[2]);
4506
- if (last < first) {
4507
- [first, last] = [last, first];
4508
- }
4509
- const values = [];
4510
- for (let i = first; i <= last; i += 1) {
4511
- 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);
4512
4858
  }
4513
- current = current.replace(raw, values.join(","));
4514
- }
4515
- return current;
4516
- }
4517
- function resolveSteps(field) {
4518
- const match = /^(.+)\/(\d+)$/.exec(field);
4519
- if (!match) return field;
4520
- const base = match[1];
4521
- const step = Number(match[2]);
4522
- if (!Number.isFinite(step) || step <= 0) return field;
4523
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4524
- }
4525
- function convertPattern(pattern) {
4526
- const parts = pattern.trim().split(/\s+/);
4527
- if (parts.length !== 6) {
4528
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4529
4859
  }
4530
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4531
- }
4532
- function fieldMatches(field, value) {
4533
- const allowed = field.split(",").map((v) => Number(v));
4534
- return allowed.includes(value);
4535
- }
4536
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4537
- const parsed = convertPattern(pattern);
4538
- 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);
4539
4861
  }
4540
4862
 
4541
- // src/tasks/TaskMaster.ts
4542
- var TaskMaster = class {
4543
- context;
4544
- 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
+ */
4545
4877
  constructor(context, task) {
4546
4878
  this.context = context;
4547
4879
  this.task = task;
4548
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
+ */
4549
4887
  cantRunReason() {
4550
4888
  return false;
4551
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
+ */
4552
4896
  requestStop(_allowanceMs) {
4553
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
+ }
4554
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
+ }
4555
5076
 
4556
- // src/tasks/coreTasks/TaskPing.ts
4557
- 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
+ */
4558
5088
  async run() {
4559
5089
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4560
5090
  return { success: true, results: "pong" };
4561
5091
  }
4562
5092
  };
4563
5093
 
4564
- // src/tasks/coreTasks/TaskSampleProcess.ts
4565
- var TaskSampleProcess = class extends TaskMaster {
4566
- stopRequested = false;
4567
- stopAllowanceMs = 0;
4568
- 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
+ */
4569
5137
  requestStop(allowanceMs) {
4570
5138
  this.stopRequested = true;
4571
5139
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -4573,6 +5141,14 @@ var TaskSampleProcess = class extends TaskMaster {
4573
5141
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4574
5142
  );
4575
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
+ */
4576
5152
  async run(reportProgress) {
4577
5153
  const totalRaw = this.task?.params?.total ?? 10;
4578
5154
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -4654,7 +5230,7 @@ var TaskSampleProcess = class extends TaskMaster {
4654
5230
  }
4655
5231
  };
4656
5232
 
4657
- // src/tasks/coreTasks/TaskShellCommand.ts
5233
+ // src/tasks/coreTasks/TaskShellCommand.js
4658
5234
  import { spawn } from "child_process";
4659
5235
  function runShellCommand(command, cwd) {
4660
5236
  return new Promise((resolve2, reject) => {
@@ -4684,7 +5260,27 @@ function runShellCommand(command, cwd) {
4684
5260
  });
4685
5261
  });
4686
5262
  }
4687
- 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
+ */
4688
5284
  async run() {
4689
5285
  const params = this.task?.params;
4690
5286
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -4733,7 +5329,7 @@ var TaskShellCommand = class extends TaskMaster {
4733
5329
  }
4734
5330
  };
4735
5331
 
4736
- // src/tasks/coreTasks/TaskSystemInfo.ts
5332
+ // src/tasks/coreTasks/TaskSystemInfo.js
4737
5333
  import os2 from "os";
4738
5334
  import fs4 from "fs/promises";
4739
5335
  function toGb(valueBytes) {
@@ -4753,7 +5349,16 @@ async function getDiskStats() {
4753
5349
  free: toGb(free)
4754
5350
  };
4755
5351
  }
4756
- 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
+ */
4757
5362
  async run() {
4758
5363
  try {
4759
5364
  const totalMemory = os2.totalmem();
@@ -4805,8 +5410,31 @@ var TaskSystemInfo = class extends TaskMaster {
4805
5410
  }
4806
5411
  };
4807
5412
 
4808
- // src/tasks/coreTasks/TaskSumAB.ts
4809
- 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
+ */
4810
5438
  async run() {
4811
5439
  const a = this.task?.params?.a;
4812
5440
  const b = this.task?.params?.b;
@@ -4837,8 +5465,46 @@ var TaskSumAB = class extends TaskMaster {
4837
5465
  }
4838
5466
  };
4839
5467
 
4840
- // src/tasks/coreTasks/TaskStopRunner.ts
4841
- 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
+ */
4842
5508
  async run() {
4843
5509
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4844
5510
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -4853,172 +5519,396 @@ var TaskStopRunner = class extends TaskMaster {
4853
5519
  }
4854
5520
  };
4855
5521
 
4856
- // 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
4857
5586
  var TasksRegistry = class _TasksRegistry {
4858
- map = {};
5587
+ /**
5588
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
5589
+ */
4859
5590
  constructor(initial) {
5591
+ this.map = {};
4860
5592
  if (initial) {
4861
5593
  this.addMany(initial);
4862
5594
  }
4863
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
+ */
4864
5602
  static withCoreTasks() {
4865
- 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);
4866
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
+ */
4867
5612
  add(taskName, taskClass) {
4868
5613
  this.map[taskName] = taskClass;
4869
5614
  return this;
4870
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
+ */
4871
5622
  addMany(entries) {
4872
5623
  for (const [name, klass] of Object.entries(entries)) {
4873
5624
  this.add(name, klass);
4874
5625
  }
4875
5626
  return this;
4876
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
+ */
4877
5635
  get(taskName) {
4878
5636
  return this.map[taskName];
4879
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
+ */
4880
5687
  listSupportedTasks() {
4881
5688
  return Object.keys(this.map).sort();
4882
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
+ */
4883
5696
  toObject() {
4884
5697
  return { ...this.map };
4885
5698
  }
4886
5699
  };
4887
5700
 
4888
- // 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
4889
5726
  import { spawn as spawn2 } from "child_process";
5727
+ var MAX_PROGRESS_TEXT_LEN = 4e3;
4890
5728
  function toCliArgs(args = []) {
4891
5729
  return args.filter((a) => typeof a === "string" && a.length > 0);
4892
5730
  }
4893
5731
  function formatChildLogPrefix(task) {
4894
- 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}` : ""}`;
4895
5733
  }
4896
- async function runNodeTaskScript(context, options) {
4897
- 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) {
4898
5771
  const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
4899
5772
  const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
4900
- const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
4901
- const child = spawn2(
4902
- process.execPath,
4903
- nodeArgs,
4904
- {
4905
- cwd: options.cwd || process.cwd(),
4906
- stdio: ["ignore", "pipe", "pipe", "ipc"],
4907
- env: {
4908
- ...process.env,
4909
- TASK_ID: options.task.id,
4910
- TASK_NAME: options.task.task,
4911
- TASK_OPID: options.task.opid || ""
4912
- }
4913
- }
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)")
4914
5785
  );
4915
- let stdout = "";
4916
- let stderr = "";
4917
- let workerResult = null;
4918
- let hadErrorMessage = false;
4919
- const prefix = formatChildLogPrefix(options.task);
4920
- const db = context.db;
4921
- const tasksTable = context.params?.get?.("table") || "tasks";
4922
- let progressWriteChain = Promise.resolve();
4923
- let progressCallbackChain = Promise.resolve();
4924
- const updateProgress = (text) => {
4925
- if (!db || !text || !text.trim()) return;
4926
- progressWriteChain = progressWriteChain.then(async () => {
4927
- await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
4928
- }).catch((error) => {
4929
- context.logger.warn?.(
4930
- `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
4931
- );
4932
- });
4933
- if (options.onProgress) {
4934
- progressCallbackChain = progressCallbackChain.then(async () => {
4935
- await options.onProgress?.(text.slice(0, 4e3));
4936
- }).catch((error) => {
4937
- context.logger.warn?.(
4938
- `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
4939
- );
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(() => {
4940
5798
  });
4941
5799
  }
4942
5800
  };
4943
- const payloadToProgressText = (payload) => {
4944
- if (!payload) return "";
4945
- if (typeof payload === "string") return payload;
4946
- if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4947
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
4948
- return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
4949
- }
4950
- if (typeof payload.message === "string") return payload.message;
4951
- if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4952
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
4953
- return `${pfx}${payload.count}/${payload.total}`;
4954
- }
4955
- 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
4956
5824
  };
4957
- child.stdout.on("data", (chunk) => {
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
+ });
5849
+ };
5850
+ child.stdout?.on("data", (chunk) => {
4958
5851
  const text = String(chunk);
4959
- stdout += text;
5852
+ state.stdout += text;
4960
5853
  if (text.trim()) {
4961
5854
  context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
4962
- updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
4963
5855
  }
4964
5856
  });
4965
- child.stderr.on("data", (chunk) => {
5857
+ child.stderr?.on("data", (chunk) => {
4966
5858
  const text = String(chunk);
4967
- stderr += text;
5859
+ state.stderr += text;
4968
5860
  if (text.trim()) {
4969
5861
  context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
4970
5862
  }
4971
5863
  });
4972
5864
  child.on("message", (message) => {
4973
5865
  if (message && typeof message === "object" && "__taskWorkerResult" in message) {
4974
- workerResult = message.__taskWorkerResult;
5866
+ state.workerResult = message.__taskWorkerResult;
4975
5867
  return;
4976
5868
  }
4977
5869
  if (message && typeof message === "object") {
4978
5870
  const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
4979
5871
  if (level === "error" || level === "fatal") {
4980
- hadErrorMessage = true;
5872
+ state.hadErrorMessage = true;
4981
5873
  }
4982
5874
  }
4983
- appendTaskIpcLog(context, options.task, message);
4984
- const progressText = payloadToProgressText(message);
4985
- if (progressText) {
4986
- updateProgress(progressText);
4987
- if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
4988
- const countNum = Number(String(message.count).trim());
4989
- const totalNum = Number(message.total);
4990
- if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
4991
- context.logger.progress(message.message || "progress", {
4992
- prefix: message.prefix || prefix,
4993
- count: countNum,
4994
- total: totalNum
4995
- });
4996
- } else {
4997
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
4998
- }
4999
- } else {
5000
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
5001
- }
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)}`);
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;
5002
5889
  }
5890
+ forwardChildLogToParent(context, prefix, message);
5003
5891
  });
5004
5892
  return await new Promise((resolve2, reject) => {
5005
5893
  child.on("error", (error) => reject(error));
5006
5894
  child.on("close", (exitCode, signal) => {
5007
- Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
5895
+ void (async () => {
5896
+ await flushTaskIpcLogs(context);
5897
+ await progressQueue.drain();
5008
5898
  resolve2({
5009
5899
  exitCode,
5010
5900
  signal,
5011
- stdout: stdout.trim(),
5012
- stderr: stderr.trim(),
5013
- workerResult,
5014
- hadErrorMessage
5901
+ stdout: state.stdout.trim(),
5902
+ stderr: state.stderr.trim(),
5903
+ workerResult: state.workerResult,
5904
+ hadErrorMessage: state.hadErrorMessage
5015
5905
  });
5016
- });
5906
+ })();
5017
5907
  });
5018
5908
  });
5019
5909
  }
5020
5910
 
5021
- // src/tasks/index.ts
5911
+ // src/tasks/index.js
5022
5912
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
5023
5913
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
5024
5914
  function getDb3(context) {
@@ -5033,22 +5923,13 @@ function normalizeRegistry(registry) {
5033
5923
  if (registry instanceof TasksRegistry) return registry;
5034
5924
  return new TasksRegistry().addMany(registry);
5035
5925
  }
5036
- function normalizeAllowedTasks(value) {
5037
- if (!value) return void 0;
5038
- if (Array.isArray(value)) {
5039
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5040
- return out2.length ? out2 : void 0;
5041
- }
5042
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5043
- return out.length ? out : void 0;
5044
- }
5045
- async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
5926
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
5046
5927
  return enqueueTask(context, {
5047
- queue,
5048
- target,
5049
- task: "stopRunner",
5928
+ queueName,
5929
+ name: "stopRunner",
5050
5930
  params: { allowanceMs },
5051
- priority: 1e6
5931
+ priority: 0,
5932
+ serviceGroup
5052
5933
  });
5053
5934
  }
5054
5935
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
@@ -5066,18 +5947,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
5066
5947
  }
5067
5948
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
5068
5949
  const db = getDb3(context);
5069
- const taskName = row.task;
5950
+ const taskName = row.name;
5070
5951
  const TaskClass = registry.get(taskName);
5071
- const { paused_at: _pausedAt, ...rowForHistory } = row;
5072
5952
  if (!TaskClass) {
5073
5953
  const err = { message: `Unknown task "${taskName}"` };
5074
- await db(historyTable).insert({
5075
- ...rowForHistory,
5076
- completed_at: /* @__PURE__ */ new Date(),
5077
- success: false,
5078
- params: toJsonColumn(row.params),
5079
- results: toJsonColumn(err)
5080
- });
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
+ );
5081
5964
  if (row.schedule) {
5082
5965
  await db(tasksTable).where({ id: row.id }).update({
5083
5966
  started_at: null,
@@ -5085,7 +5968,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5085
5968
  success: false,
5086
5969
  results: toJsonColumn(err),
5087
5970
  past_due: null,
5088
- paused_at: db.fn.now(),
5971
+ status: "paused",
5972
+ status_changed_at: db.fn.now(),
5089
5973
  progress: LOCKED_BY_ERROR_MESSAGE
5090
5974
  });
5091
5975
  } else {
@@ -5112,13 +5996,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5112
5996
  } finally {
5113
5997
  runningTaskInstances.delete(row.id);
5114
5998
  }
5115
- await db(historyTable).insert({
5116
- ...rowForHistory,
5117
- completed_at: /* @__PURE__ */ new Date(),
5118
- success,
5119
- params: toJsonColumn(row.params),
5120
- results: toJsonColumn(results)
5121
- });
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
+ );
5122
6009
  if (!success) {
5123
6010
  const dbName = String(context?.params?.get?.("dbName") || "local");
5124
6011
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -5133,19 +6020,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5133
6020
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
5134
6021
  appendTaskIpcLog(context, row, {
5135
6022
  level: "error",
5136
- 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}`,
5137
6024
  details: results
5138
6025
  });
5139
6026
  }
5140
6027
  if (row.schedule) {
5141
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
+ }
5142
6035
  await db(tasksTable).where({ id: row.id }).update({
5143
6036
  started_at: null,
5144
6037
  completed_at: /* @__PURE__ */ new Date(),
5145
6038
  success,
5146
6039
  results: toJsonColumn(results),
5147
6040
  progress: null,
5148
- 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
5149
6049
  });
5150
6050
  } else {
5151
6051
  await db(tasksTable).where({ id: row.id }).update({
@@ -5153,7 +6053,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5153
6053
  completed_at: /* @__PURE__ */ new Date(),
5154
6054
  success,
5155
6055
  results: toJsonColumn(results),
5156
- paused_at: db.fn.now(),
6056
+ status: "paused",
6057
+ status_changed_at: db.fn.now(),
5157
6058
  progress: LOCKED_BY_ERROR_MESSAGE,
5158
6059
  past_due: null
5159
6060
  });
@@ -5165,58 +6066,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5165
6066
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
5166
6067
  return { stopRunnerRequested, stopAllowanceMs };
5167
6068
  }
5168
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
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) {
5169
6078
  const db = getDb3(context);
5170
- 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);
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);
5171
6082
  if (taskNames && taskNames.length > 0) {
5172
- 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");
5173
6095
  }
5174
6096
  const candidates = await query;
6097
+ shuffleTaskRowsInPlace(candidates);
5175
6098
  for (const row of candidates) {
5176
6099
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
5177
6100
  continue;
5178
6101
  }
5179
- const TaskClass = registry.get(row.task);
5180
- if (TaskClass) {
5181
- const taskInstance = new TaskClass(context, row);
5182
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
5183
- if (reason) {
5184
- if (!row.past_due) {
5185
- await db(tasksTable).where({ id: row.id }).update({
5186
- past_due: db.fn.now(),
5187
- progress: String(reason)
5188
- });
5189
- }
5190
- 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
+ });
5191
6114
  }
6115
+ continue;
5192
6116
  }
5193
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
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;
6126
+ }
6127
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
5194
6128
  const claimed = Array.isArray(updated) ? updated[0] : null;
5195
6129
  if (claimed) return claimed;
5196
6130
  }
5197
6131
  return null;
5198
6132
  }
5199
6133
  async function runTasksLoop(context, options) {
5200
- const queue = options.queue ?? "tasks";
6134
+ const queueName = options.queueName ?? "tasks";
5201
6135
  const target = options.target;
5202
6136
  const pollMs = options.pollMs ?? 1e3;
5203
- const maxParallel = options.maxParallel ?? 1;
6137
+ const claimJitterMs = options.claimJitterMs ?? 0;
6138
+ const maxParallel = options.maxParallel ?? 32;
5204
6139
  const scanLimit = options.scanLimit ?? 100;
5205
6140
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5206
6141
  const registry = normalizeRegistry(options.registry);
5207
- const { tasksTable, historyTable } = queueToTableNames(queue);
6142
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5208
6143
  if (!target) throw new Error("runTasksLoop: target is required");
6144
+ context.tasksQueueName = queueName;
5209
6145
  const runningPromises = /* @__PURE__ */ new Set();
5210
6146
  const runningTaskInstances = /* @__PURE__ */ new Map();
5211
6147
  let runningStopControlPromise = null;
5212
6148
  let stopRequested = false;
5213
6149
  let stopAllowanceMs = 5e3;
5214
- context.__tasksRunnerStop = false;
6150
+ context.tasksRunnerStop = false;
5215
6151
  let registryReg = null;
5216
6152
  let registryInterval = null;
6153
+ let runnerIdentity = null;
5217
6154
  const hbGroup = options.runnerServiceGroup?.trim();
5218
6155
  if (hbGroup) {
5219
- const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
5220
6156
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
5221
6157
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
5222
6158
  const defaultMeta = {
@@ -5224,16 +6160,21 @@ async function runTasksLoop(context, options) {
5224
6160
  allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
5225
6161
  };
5226
6162
  registryReg = await registerInServicesRegistry(context, {
5227
- queue,
6163
+ queueName,
5228
6164
  target,
5229
6165
  serviceGroup: hbGroup,
5230
6166
  serviceName: options.runnerServiceName,
5231
- identityDir,
6167
+ instanceNumber: options.runnerInstanceNumber,
5232
6168
  staleMs,
5233
6169
  groupMaxInstances: options.runnerGroupMaxInstances,
5234
6170
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
5235
6171
  metadata: options.runnerMetadata ?? defaultMeta
5236
6172
  });
6173
+ runnerIdentity = {
6174
+ service_name: registryReg.serviceName,
6175
+ server_name: os3.hostname(),
6176
+ instance_number: registryReg.instanceNumber
6177
+ };
5237
6178
  registryInterval = setInterval(() => {
5238
6179
  void touchServicesRegistry(context, registryReg).catch((err) => {
5239
6180
  context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
@@ -5241,7 +6182,7 @@ async function runTasksLoop(context, options) {
5241
6182
  }, hbIntervalMs);
5242
6183
  }
5243
6184
  try {
5244
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
6185
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
5245
6186
  if (!runningStopControlPromise) {
5246
6187
  const claimedStopTask = await claimNextRunnableTask(
5247
6188
  context,
@@ -5249,7 +6190,8 @@ async function runTasksLoop(context, options) {
5249
6190
  target,
5250
6191
  registry,
5251
6192
  10,
5252
- ["stopRunner", "stop"]
6193
+ ["stopRunner", "stop"],
6194
+ runnerIdentity
5253
6195
  );
5254
6196
  if (claimedStopTask) {
5255
6197
  runningStopControlPromise = executeClaimedTask(
@@ -5263,7 +6205,7 @@ async function runTasksLoop(context, options) {
5263
6205
  if (outcome.stopRunnerRequested && !stopRequested) {
5264
6206
  stopRequested = true;
5265
6207
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5266
- context.__tasksRunnerStop = true;
6208
+ context.tasksRunnerStop = true;
5267
6209
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5268
6210
  }
5269
6211
  }).finally(() => {
@@ -5271,6 +6213,9 @@ async function runTasksLoop(context, options) {
5271
6213
  });
5272
6214
  }
5273
6215
  }
6216
+ if (claimJitterMs > 0) {
6217
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
6218
+ }
5274
6219
  while (runningPromises.size < maxParallel) {
5275
6220
  const claimed = await claimNextRunnableTask(
5276
6221
  context,
@@ -5278,14 +6223,15 @@ async function runTasksLoop(context, options) {
5278
6223
  target,
5279
6224
  registry,
5280
6225
  scanLimit,
5281
- allowedTasks
6226
+ allowedTasks,
6227
+ runnerIdentity
5282
6228
  );
5283
6229
  if (!claimed) break;
5284
6230
  const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
5285
6231
  if (outcome.stopRunnerRequested && !stopRequested) {
5286
6232
  stopRequested = true;
5287
6233
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5288
- context.__tasksRunnerStop = true;
6234
+ context.tasksRunnerStop = true;
5289
6235
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5290
6236
  }
5291
6237
  }).finally(() => {
@@ -5293,7 +6239,16 @@ async function runTasksLoop(context, options) {
5293
6239
  });
5294
6240
  runningPromises.add(p);
5295
6241
  }
5296
- await sleepMs(pollMs);
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)]);
6251
+ }
5297
6252
  }
5298
6253
  if (context.isStop() && !stopRequested) {
5299
6254
  await signalRunningTasksStop(context, runningTaskInstances, 5e3);
@@ -5329,89 +6284,130 @@ async function runTasksLoop(context, options) {
5329
6284
  }
5330
6285
  async function waitForTaskResult(context, taskId, options = {}) {
5331
6286
  const db = getDb3(context);
5332
- const queue = options.queue ?? "tasks";
6287
+ const queueName = options.queueName ?? "tasks";
5333
6288
  const timeoutMs = options.timeoutMs ?? 6e4;
5334
6289
  const pollMs = options.pollMs ?? 500;
5335
- const { tasksTable, historyTable } = queueToTableNames(queue);
6290
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5336
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
+ }
5337
6303
  while (Date.now() <= deadline) {
5338
- const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5339
- 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
+ }
5340
6308
  const pending = await db(tasksTable).where({ id: taskId }).first();
5341
- if (!pending) {
5342
- const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5343
- 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;
5344
6323
  }
5345
6324
  await sleepMs(pollMs);
5346
6325
  }
5347
6326
  return null;
5348
6327
  }
5349
6328
  var TasksManager = class _TasksManager {
5350
- context;
5351
- queue;
5352
- target;
5353
- recreateTaskTables;
5354
- pollMs;
5355
- maxParallel;
5356
- scanLimit;
5357
- allowedTasks;
5358
- registry;
5359
- runnerServiceGroup;
5360
- runnerServiceName;
5361
- runnerIdentityDir;
5362
- runnerHeartbeatIntervalMs;
5363
- runnerHeartbeatStaleMs;
5364
- runnerGroupMaxInstances;
5365
- runnerEnforceMaxInstances;
5366
- runnerMetadata;
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
+ */
5367
6351
  constructor(context, options = {}) {
5368
6352
  this.context = context;
5369
- this.queue = options.queue ?? "tasks";
6353
+ this.queueName = options.queueName ?? "tasks";
5370
6354
  this.target = options.target ?? "localRunner";
5371
6355
  this.recreateTaskTables = options.recreateTaskTables ?? false;
5372
6356
  this.pollMs = options.pollMs ?? 1e3;
6357
+ this.claimJitterMs = options.claimJitterMs ?? 0;
5373
6358
  this.maxParallel = options.maxParallel ?? 1;
5374
6359
  this.scanLimit = options.scanLimit ?? 100;
5375
6360
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5376
6361
  this.registry = normalizeRegistry(options.registry);
5377
6362
  this.runnerServiceGroup = options.runnerServiceGroup;
5378
6363
  this.runnerServiceName = options.runnerServiceName;
5379
- this.runnerIdentityDir = options.runnerIdentityDir;
6364
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
5380
6365
  this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
5381
6366
  this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
5382
6367
  this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
5383
6368
  this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
5384
6369
  this.runnerMetadata = options.runnerMetadata;
5385
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
+ */
5386
6380
  static init(context, options = {}) {
5387
6381
  const defs = {
5388
6382
  table: "string default tasks",
5389
6383
  target: "string default localRunner",
5390
6384
  recreateTaskTables: "boolean default false",
5391
6385
  pollMs: "number default 1000",
6386
+ claimJitterMs: "number default 0",
5392
6387
  maxParallel: "number default 1",
5393
6388
  scanLimit: "number default 100",
5394
6389
  allowedTasks: "string",
5395
6390
  runnerServiceGroup: "string",
5396
6391
  runnerServiceName: "string",
5397
- runnerIdentityDir: "string default ./data/runner-identities",
6392
+ runnerInstanceNumber: "number",
5398
6393
  runnerHeartbeatIntervalMs: "number default 10000",
5399
6394
  runnerHeartbeatStaleMs: "number default 45000",
5400
6395
  runnerGroupMaxInstances: "number",
5401
6396
  runnerEnforceMaxInstances: "boolean default true"
5402
6397
  };
5403
- const discovered = context.params.getAllForModule(defs);
6398
+ const discovered = context.params.getAllForModule("tasks", defs);
5404
6399
  const resolved = {
5405
- queue: discovered.table,
6400
+ queueName: discovered.table,
5406
6401
  target: discovered.target,
5407
6402
  recreateTaskTables: discovered.recreateTaskTables,
5408
6403
  pollMs: discovered.pollMs,
6404
+ claimJitterMs: discovered.claimJitterMs,
5409
6405
  maxParallel: discovered.maxParallel,
5410
6406
  scanLimit: discovered.scanLimit,
5411
6407
  allowedTasks: discovered.allowedTasks,
5412
6408
  runnerServiceGroup: discovered.runnerServiceGroup,
5413
6409
  runnerServiceName: discovered.runnerServiceName,
5414
- runnerIdentityDir: discovered.runnerIdentityDir,
6410
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
5415
6411
  runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
5416
6412
  runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
5417
6413
  runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
@@ -5420,24 +6416,39 @@ var TasksManager = class _TasksManager {
5420
6416
  };
5421
6417
  return new _TasksManager(context, resolved);
5422
6418
  }
6419
+ /**
6420
+ * Idempotently ensure the three backing tables exist for this queue.
6421
+ *
6422
+ * @param {{ recreate?: boolean }} [options]
6423
+ * @returns {Promise<void>}
6424
+ */
5423
6425
  async ensureTaskTables(options = {}) {
5424
6426
  await ensureTaskTables(this.context, {
5425
- queue: this.queue,
6427
+ queueName: this.queueName,
5426
6428
  recreate: options.recreate ?? this.recreateTaskTables
5427
6429
  });
5428
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
+ */
5429
6439
  async runTasksLoop(options = {}) {
5430
6440
  await runTasksLoop(this.context, {
5431
- queue: options.queue ?? this.queue,
6441
+ queueName: options.queueName ?? this.queueName,
5432
6442
  target: options.target ?? this.target,
5433
6443
  pollMs: options.pollMs ?? this.pollMs,
6444
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
5434
6445
  maxParallel: options.maxParallel ?? this.maxParallel,
5435
6446
  scanLimit: options.scanLimit ?? this.scanLimit,
5436
6447
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
5437
6448
  registry: options.registry ?? this.registry,
5438
6449
  runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
5439
6450
  runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
5440
- runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
6451
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
5441
6452
  runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
5442
6453
  runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
5443
6454
  runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
@@ -5447,8 +6458,9 @@ var TasksManager = class _TasksManager {
5447
6458
  }
5448
6459
  };
5449
6460
  export {
6461
+ AbstractTask,
5450
6462
  Args,
5451
- Box5 as Box,
6463
+ Box4 as Box,
5452
6464
  Db,
5453
6465
  Divider,
5454
6466
  FileDatabase,
@@ -5461,14 +6473,16 @@ export {
5461
6473
  MultiColumnListComponent,
5462
6474
  MultiColumnListWithPreviewComponent,
5463
6475
  Params,
5464
- React5 as React,
6476
+ React2 as React,
6477
+ S3,
6478
+ SERVICE_TASK_NAMES,
5465
6479
  ScreenBody,
5466
6480
  ScreenContainer,
5467
6481
  ScreenDivider,
5468
6482
  ScreenFooter,
5469
6483
  ScreenRow,
5470
6484
  ScreenTitle,
5471
- TaskMaster,
6485
+ TaskGetLogs,
5472
6486
  TaskPing,
5473
6487
  TaskSampleProcess,
5474
6488
  TaskShellCommand,
@@ -5483,17 +6497,17 @@ export {
5483
6497
  buildBreadcrumb,
5484
6498
  buildDetailBreadcrumb,
5485
6499
  buildFooter,
5486
- dbConnect,
5487
- dbFindAndConnect,
5488
- dbInit,
6500
+ convertPattern,
5489
6501
  defaultFileSynopsisFunction,
5490
6502
  defaultTasksRegistry,
5491
6503
  defaultVersionSynopsisFunction,
5492
6504
  enqueueStopTask,
5493
6505
  enqueueTask,
5494
6506
  ensureTaskTables,
6507
+ flushTaskIpcLogs,
5495
6508
  getArgsInstance,
5496
6509
  createElement2 as h,
6510
+ ipcFileLogsTableNameForSourceResource,
5497
6511
  joiEdateType,
5498
6512
  joiStringArrayType,
5499
6513
  listServicesRegistry as listAliveRunnerHeartbeats,
@@ -5501,14 +6515,22 @@ export {
5501
6515
  listSources,
5502
6516
  listTables,
5503
6517
  load,
6518
+ matchesParsedPattern,
6519
+ memo,
6520
+ mergeAllowedTasksWithServiceTasks,
6521
+ nextTimeMatch,
6522
+ normalizeAllowedTasks,
5504
6523
  organizeFooterMessages,
5505
6524
  queueToTableNames,
6525
+ readTaskIpcLogsSnapshot,
5506
6526
  registerInServicesRegistry,
5507
6527
  registerInServicesRegistry as registerRunnerHeartbeat,
6528
+ resolveAsterisks,
6529
+ resolveIpcFileLogsDir,
6530
+ resolveRanges,
6531
+ resolveSteps,
5508
6532
  runNodeTaskScript,
5509
6533
  runTasksLoop,
5510
- servicesRegistryTable as runnerHeartbeatsTable,
5511
- servicesRegistryTable,
5512
6534
  setupContext,
5513
6535
  showListScreen,
5514
6536
  showMenuScreen,
@@ -5516,6 +6538,8 @@ export {
5516
6538
  showMultiColumnListWithPreviewScreen,
5517
6539
  showScreen,
5518
6540
  showWordGridScreen,
6541
+ taskHistoryInsertFromQueueRow,
6542
+ timeMatcher,
5519
6543
  touchServicesRegistry as touchRunnerHeartbeat,
5520
6544
  touchServicesRegistry,
5521
6545
  unregisterServicesRegistry as unregisterRunnerHeartbeat,
@@ -5523,10 +6547,11 @@ export {
5523
6547
  updateServicesRegistryMetadata,
5524
6548
  updateTaskProgress,
5525
6549
  useCallback,
5526
- useEffect3 as useEffect,
6550
+ useEffect2 as useEffect,
5527
6551
  useInput2 as useInput,
6552
+ useLayoutEffect,
5528
6553
  useMemo,
5529
- useRef3 as useRef,
6554
+ useRef2 as useRef,
5530
6555
  useState3 as useState,
5531
6556
  waitForTaskResult
5532
6557
  };