@nmakarov/cli-toolkit 0.21.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/args.cjs +1 -4
  2. package/dist/args.cjs.map +1 -1
  3. package/dist/args.js +1 -1
  4. package/dist/args.js.map +1 -1
  5. package/dist/cli-runner.cjs +1370 -623
  6. package/dist/cli-runner.cjs.map +1 -1
  7. package/dist/cli-runner.js +1415 -667
  8. package/dist/cli-runner.js.map +1 -1
  9. package/dist/db.cjs +173 -158
  10. package/dist/db.cjs.map +1 -1
  11. package/dist/db.js +172 -151
  12. package/dist/db.js.map +1 -1
  13. package/dist/errors.cjs +2 -2
  14. package/dist/errors.cjs.map +1 -1
  15. package/dist/errors.js +2 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/filedatabase.cjs +19 -19
  18. package/dist/filedatabase.cjs.map +1 -1
  19. package/dist/filedatabase.js +19 -16
  20. package/dist/filedatabase.js.map +1 -1
  21. package/dist/http-client.cjs +9 -11
  22. package/dist/http-client.cjs.map +1 -1
  23. package/dist/http-client.js +10 -9
  24. package/dist/http-client.js.map +1 -1
  25. package/dist/http-client2.cjs +34 -37
  26. package/dist/http-client2.cjs.map +1 -1
  27. package/dist/http-client2.js +34 -34
  28. package/dist/http-client2.js.map +1 -1
  29. package/dist/index.cjs +1831 -713
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +1837 -713
  32. package/dist/index.js.map +1 -1
  33. package/dist/init.cjs +93 -68
  34. package/dist/init.cjs.map +1 -1
  35. package/dist/init.js +108 -82
  36. package/dist/init.js.map +1 -1
  37. package/dist/logger.cjs +5 -5
  38. package/dist/logger.cjs.map +1 -1
  39. package/dist/logger.js +5 -4
  40. package/dist/logger.js.map +1 -1
  41. package/dist/mock-server.cjs +21 -33
  42. package/dist/mock-server.cjs.map +1 -1
  43. package/dist/mock-server.js +21 -28
  44. package/dist/mock-server.js.map +1 -1
  45. package/dist/params.cjs +18 -9
  46. package/dist/params.cjs.map +1 -1
  47. package/dist/params.js +18 -6
  48. package/dist/params.js.map +1 -1
  49. package/dist/s3.cjs +286 -0
  50. package/dist/s3.cjs.map +1 -0
  51. package/dist/s3.js +273 -0
  52. package/dist/s3.js.map +1 -0
  53. package/dist/screen.cjs +34 -39
  54. package/dist/screen.cjs.map +1 -1
  55. package/dist/screen.js +48 -46
  56. package/dist/screen.js.map +1 -1
  57. package/dist/tasks.cjs +1354 -501
  58. package/dist/tasks.cjs.map +1 -1
  59. package/dist/tasks.js +1369 -527
  60. package/dist/tasks.js.map +1 -1
  61. package/dist/utils.cjs +7 -8
  62. package/dist/utils.cjs.map +1 -1
  63. package/dist/utils.js +6 -6
  64. package/dist/utils.js.map +1 -1
  65. package/package.json +32 -47
  66. package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
  67. package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
  68. package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
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
  }
@@ -2003,6 +1996,18 @@ var Params = class _Params {
2003
1996
  this._currentModule = prev;
2004
1997
  }
2005
1998
  }
1999
+ /**
2000
+ * Async variant of {@link runWithModule} for modules that await params.get().
2001
+ */
2002
+ async runWithModuleAsync(moduleName, fn) {
2003
+ const prev = this._currentModule;
2004
+ this._currentModule = moduleName;
2005
+ try {
2006
+ return await fn();
2007
+ } finally {
2008
+ this._currentModule = prev;
2009
+ }
2010
+ }
2006
2011
  /**
2007
2012
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2008
2013
  */
@@ -2063,14 +2068,14 @@ var Params = class _Params {
2063
2068
  }
2064
2069
  };
2065
2070
 
2066
- // src/screen.ts
2071
+ // src/index.js
2067
2072
  init_screen();
2068
2073
 
2069
- // src/filedatabase/index.ts
2074
+ // src/filedatabase/index.js
2070
2075
  import fs3 from "fs";
2071
2076
  import path3 from "path";
2072
2077
 
2073
- // src/utils/os-utils.ts
2078
+ // src/utils/os-utils.js
2074
2079
  import fs from "fs";
2075
2080
  import path from "path";
2076
2081
  import { execSync } from "child_process";
@@ -2099,7 +2104,7 @@ function getFreeDiskSpace(targetPath) {
2099
2104
  }
2100
2105
  }
2101
2106
 
2102
- // src/utils/fs-utils.ts
2107
+ // src/utils/fs-utils.js
2103
2108
  import fs2 from "fs";
2104
2109
  import path2 from "path";
2105
2110
  async function ensurePath(...pathParts) {
@@ -2123,7 +2128,7 @@ function getFileExtension(dataType) {
2123
2128
  }
2124
2129
  }
2125
2130
 
2126
- // src/utils/format-utils.ts
2131
+ // src/utils/format-utils.js
2127
2132
  function bytesToHumanReadable(bytes) {
2128
2133
  if (bytes === 0) return "0 B";
2129
2134
  const k = 1024;
@@ -2132,7 +2137,7 @@ function bytesToHumanReadable(bytes) {
2132
2137
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
2133
2138
  }
2134
2139
 
2135
- // src/utils/date-utils.ts
2140
+ // src/utils/date-utils.js
2136
2141
  function isTimestampFolder(folderName) {
2137
2142
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
2138
2143
  if (!isoRegex.test(folderName)) {
@@ -2142,7 +2147,7 @@ function isTimestampFolder(folderName) {
2142
2147
  return !isNaN(date.getTime()) && date.getTime() > 0;
2143
2148
  }
2144
2149
 
2145
- // src/filedatabase/serializers.ts
2150
+ // src/filedatabase/serializers.js
2146
2151
  function detectDataType(data) {
2147
2152
  if (Array.isArray(data)) {
2148
2153
  return "json-array";
@@ -2174,7 +2179,7 @@ function deserializeData(rawData, dataType) {
2174
2179
  }
2175
2180
  }
2176
2181
 
2177
- // src/filedatabase/synopsis-functions.ts
2182
+ // src/filedatabase/synopsis-functions.js
2178
2183
  function defaultFileSynopsisFunction(fileEntry, data) {
2179
2184
  if (!Array.isArray(data) || data.length === 0) {
2180
2185
  return { ...fileEntry };
@@ -2242,7 +2247,7 @@ function defaultVersionSynopsisFunction(metadata) {
2242
2247
  return result;
2243
2248
  }
2244
2249
 
2245
- // src/filedatabase/index.ts
2250
+ // src/filedatabase/index.js
2246
2251
  var FileDatabase = class _FileDatabase {
2247
2252
  basePath;
2248
2253
  namespace;
@@ -2328,7 +2333,7 @@ var FileDatabase = class _FileDatabase {
2328
2333
  if (errors.length) {
2329
2334
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
2330
2335
  }
2331
- let parts = [this.basePath, this.namespace];
2336
+ const parts = [this.basePath, this.namespace];
2332
2337
  if (this.tableName) {
2333
2338
  parts.push(...this.tableName.split("/"));
2334
2339
  }
@@ -2821,14 +2826,17 @@ var FileDatabase = class _FileDatabase {
2821
2826
  * Prepare the instance for read or write operations
2822
2827
  * This discovers state and sets up internal members based on mode and current data
2823
2828
  */
2824
- async prepare({ write, read, version }) {
2829
+ async prepare(options) {
2830
+ const { write, read, version, deferInitialVersion } = options;
2825
2831
  if (write) {
2826
2832
  if (this.versioned) {
2827
2833
  if (this.currentVersion === null) {
2828
- await this.makeNewVersion();
2829
- this.metadata = this.getDefaultMetadata();
2830
- this.metadata.version = this.currentVersion;
2831
- this.makeNewFile();
2834
+ if (!deferInitialVersion) {
2835
+ await this.makeNewVersion();
2836
+ this.metadata = this.getDefaultMetadata();
2837
+ this.metadata.version = this.currentVersion;
2838
+ this.makeNewFile();
2839
+ }
2832
2840
  } else {
2833
2841
  if (!this.metadata.files.length) {
2834
2842
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -2938,7 +2946,7 @@ var FileDatabase = class _FileDatabase {
2938
2946
  if (options.forceNewVersion && !this.versioned) {
2939
2947
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
2940
2948
  }
2941
- await this.prepare({ write: true });
2949
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
2942
2950
  const incomingDataType = detectDataType(data);
2943
2951
  this.metadata.dataType = incomingDataType;
2944
2952
  if (options.forceNewVersion) {
@@ -3253,22 +3261,90 @@ function listSources(basePath) {
3253
3261
  }
3254
3262
  }
3255
3263
 
3256
- // src/db/index.ts
3264
+ // src/db/index.js
3257
3265
  import knex from "knex";
3266
+ var KNEX_DEFAULTS = {
3267
+ testConnection: true,
3268
+ pool: { min: 2, max: 10 },
3269
+ acquireConnectionTimeout: 1e4,
3270
+ ssl: { rejectUnauthorized: false }
3271
+ };
3258
3272
  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
- */
3273
+ static async init(context, options = {}) {
3274
+ const buildConfig = async () => {
3275
+ const defs = {
3276
+ dbName: "string",
3277
+ dbProfile: "boolean default false"
3278
+ };
3279
+ const discovered = context?.params?.getAllForModule?.("db", defs) ?? {};
3280
+ const merged = { ...discovered, ...options };
3281
+ let { dbName, dbProfile } = merged;
3282
+ let dbConnectionString = options.dbConnectionString ?? options.connectionString;
3283
+ let connectionParam = dbConnectionString ? "options" : null;
3284
+ if (!dbConnectionString) {
3285
+ const src = context?.args?.getSource?.("dbConnectionString");
3286
+ if (src === "cli" || src === "overrides" || src === "config") {
3287
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
3288
+ connectionParam = "dbConnectionString";
3289
+ }
3290
+ }
3291
+ if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
3292
+ dbConnectionString = dbName;
3293
+ dbName = void 0;
3294
+ }
3295
+ if (!dbConnectionString && dbName) {
3296
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3297
+ dbConnectionString = await context.params.get(paramName, "string");
3298
+ connectionParam = paramName;
3299
+ if (!dbConnectionString) {
3300
+ throw new ParamError(
3301
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3302
+ );
3303
+ }
3304
+ }
3305
+ if (!dbConnectionString) {
3306
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
3307
+ if (dbConnectionString) {
3308
+ connectionParam = "dbConnectionString";
3309
+ }
3310
+ }
3311
+ if (!dbConnectionString) {
3312
+ if (!dbName) {
3313
+ dbName = "local";
3314
+ }
3315
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3316
+ dbConnectionString = await context.params.get(paramName, "string");
3317
+ connectionParam = paramName;
3318
+ if (!dbConnectionString) {
3319
+ throw new ParamError(
3320
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3321
+ );
3322
+ }
3323
+ }
3324
+ const displayName = resolveDbDisplayName(
3325
+ dbName,
3326
+ connectionParam,
3327
+ context?.args?.env,
3328
+ merged.name
3329
+ );
3330
+ return {
3331
+ ...KNEX_DEFAULTS,
3332
+ connectionString: dbConnectionString,
3333
+ name: displayName,
3334
+ profile: !!dbProfile,
3335
+ logger: context.logger
3336
+ };
3337
+ };
3338
+ const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
3339
+ return dbConnect(context, config2);
3340
+ }
3268
3341
  constructor(config2) {
3269
- if (!config2.connectionString) {
3342
+ if (!config2 || !config2.connectionString) {
3270
3343
  throw new ParamError("Db: connectionString is required");
3271
3344
  }
3345
+ this.knexInstance = null;
3346
+ this.isConnected = false;
3347
+ this.queriesLog = [];
3272
3348
  this.config = {
3273
3349
  testConnection: true,
3274
3350
  profile: false,
@@ -3276,30 +3352,27 @@ var Db = class {
3276
3352
  acquireConnectionTimeout: 1e4,
3277
3353
  ssl: { rejectUnauthorized: false },
3278
3354
  logger: console,
3279
- name: "default",
3280
3355
  ...config2
3281
3356
  };
3282
3357
  this.logger = this.config.logger;
3283
3358
  const instance2 = this;
3284
- const callableWrapper = function(...args) {
3359
+ const callableWrapper = function() {
3285
3360
  throw new Error("This should never be called directly");
3286
3361
  };
3287
3362
  callableWrapper._instance = instance2;
3288
3363
  return new Proxy(callableWrapper, {
3289
- // Intercept function calls: db('table')
3290
- apply: (target, thisArg, argumentsList) => {
3364
+ apply: (target, _thisArg, argumentsList) => {
3291
3365
  const inst = target._instance;
3292
3366
  if (!inst.knexInstance) {
3293
3367
  throw new Error("Db: Not connected. Call connect() first.");
3294
3368
  }
3295
3369
  return inst.knexInstance(...argumentsList);
3296
3370
  },
3297
- // Intercept property access: db.schema, db.raw, etc.
3298
3371
  get: (target, prop) => {
3299
3372
  if (prop === "_instance") {
3300
3373
  return target._instance;
3301
3374
  }
3302
- const instance3 = target._instance;
3375
+ const inst = target._instance;
3303
3376
  const ownMethods = [
3304
3377
  "connect",
3305
3378
  "disconnect",
@@ -3312,26 +3385,26 @@ var Db = class {
3312
3385
  "detectClient",
3313
3386
  "attachProfiler"
3314
3387
  ];
3315
- if (prop in instance3) {
3316
- const value = instance3[prop];
3388
+ if (prop in inst) {
3389
+ const value = inst[prop];
3317
3390
  if (typeof value === "function" && ownMethods.includes(prop)) {
3318
- return value.bind(instance3);
3391
+ return value.bind(inst);
3319
3392
  }
3320
3393
  if (typeof value !== "function") {
3321
3394
  return value;
3322
3395
  }
3323
3396
  }
3324
- if (instance3.knexInstance) {
3325
- const knexProp = instance3.knexInstance[prop];
3397
+ if (inst.knexInstance) {
3398
+ const knexProp = inst.knexInstance[prop];
3326
3399
  if (typeof knexProp === "function") {
3327
- return knexProp.bind(instance3.knexInstance);
3400
+ return knexProp.bind(inst.knexInstance);
3328
3401
  }
3329
3402
  return knexProp;
3330
3403
  }
3331
- if (prop in instance3) {
3332
- const method = instance3[prop];
3404
+ if (prop in inst) {
3405
+ const method = inst[prop];
3333
3406
  if (typeof method === "function") {
3334
- return method.bind(instance3);
3407
+ return method.bind(inst);
3335
3408
  }
3336
3409
  return method;
3337
3410
  }
@@ -3339,9 +3412,6 @@ var Db = class {
3339
3412
  }
3340
3413
  });
3341
3414
  }
3342
- /**
3343
- * Detect database client type from connection string
3344
- */
3345
3415
  detectClient(connectionString) {
3346
3416
  if (connectionString.match(/^postgresql/)) {
3347
3417
  return "pg";
@@ -3351,9 +3421,6 @@ var Db = class {
3351
3421
  }
3352
3422
  return null;
3353
3423
  }
3354
- /**
3355
- * Connect to the database
3356
- */
3357
3424
  async connect() {
3358
3425
  if (this.isConnected && this.knexInstance) {
3359
3426
  this.logger.warn?.("[Db] Already connected");
@@ -3362,14 +3429,13 @@ var Db = class {
3362
3429
  const client = this.detectClient(this.config.connectionString);
3363
3430
  if (!client) {
3364
3431
  throw new ParamError(
3365
- `Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
3432
+ "Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
3366
3433
  );
3367
3434
  }
3368
3435
  try {
3369
3436
  const connectionConfig = {
3370
3437
  connectionString: this.config.connectionString,
3371
3438
  family: 4
3372
- // Force IPv4 only (disable IPv6)
3373
3439
  };
3374
3440
  this.knexInstance = knex({
3375
3441
  client,
@@ -3385,7 +3451,7 @@ var Db = class {
3385
3451
  await this.testConnection();
3386
3452
  }
3387
3453
  this.isConnected = true;
3388
- this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
3454
+ this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
3389
3455
  } catch (error) {
3390
3456
  if (error instanceof ParamError) {
3391
3457
  throw error;
@@ -3394,9 +3460,6 @@ var Db = class {
3394
3460
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
3395
3461
  }
3396
3462
  }
3397
- /**
3398
- * Disconnect from the database
3399
- */
3400
3463
  async disconnect() {
3401
3464
  if (!this.knexInstance) {
3402
3465
  return;
@@ -3406,16 +3469,13 @@ var Db = class {
3406
3469
  this.knexInstance = null;
3407
3470
  this.isConnected = false;
3408
3471
  this.queriesLog = [];
3409
- this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
3472
+ this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
3410
3473
  } catch (error) {
3411
3474
  const errorMsg = this.getErrorMessage(error);
3412
3475
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
3413
3476
  throw error;
3414
3477
  }
3415
3478
  }
3416
- /**
3417
- * Extract error message from various error types
3418
- */
3419
3479
  getErrorMessage(error) {
3420
3480
  if (error instanceof AggregateError) {
3421
3481
  const errors = error.errors || [];
@@ -3440,9 +3500,11 @@ var Db = class {
3440
3500
  return `${code} (tried: ${addresses.join(", ")})`;
3441
3501
  }
3442
3502
  }
3443
- const uniqueMessages = [...new Set(errors.map((e) => {
3444
- return e instanceof Error ? e.message : String(e);
3445
- }))];
3503
+ const uniqueMessages = [
3504
+ ...new Set(
3505
+ errors.map((e) => e instanceof Error ? e.message : String(e))
3506
+ )
3507
+ ];
3446
3508
  if (uniqueMessages.length === 1) {
3447
3509
  return uniqueMessages[0];
3448
3510
  }
@@ -3451,28 +3513,25 @@ var Db = class {
3451
3513
  return error.message || "Multiple errors occurred";
3452
3514
  }
3453
3515
  if (error instanceof Error) {
3454
- const errorWithCode = error;
3455
- if (errorWithCode.code) {
3456
- return `${errorWithCode.code}: ${error.message || String(error)}`;
3516
+ const code = error.code;
3517
+ if (code) {
3518
+ return `${code}: ${error.message || String(error)}`;
3457
3519
  }
3458
3520
  return error.message || String(error);
3459
3521
  }
3460
3522
  if (typeof error === "string") {
3461
3523
  return error;
3462
3524
  }
3463
- if (error?.message) {
3525
+ if (error && typeof error === "object" && "message" in error) {
3464
3526
  const msg = String(error.message);
3465
- const errorWithCode = error;
3466
- if (errorWithCode.code) {
3467
- return `${errorWithCode.code}: ${msg}`;
3527
+ const code = error.code;
3528
+ if (code) {
3529
+ return `${code}: ${msg}`;
3468
3530
  }
3469
3531
  return msg;
3470
3532
  }
3471
3533
  return String(error) || "Unknown error";
3472
3534
  }
3473
- /**
3474
- * Test database connection
3475
- */
3476
3535
  async testConnection() {
3477
3536
  if (!this.knexInstance) {
3478
3537
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3488,9 +3547,6 @@ var Db = class {
3488
3547
  throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
3489
3548
  }
3490
3549
  }
3491
- /**
3492
- * Attach query profiler to log all queries
3493
- */
3494
3550
  attachProfiler() {
3495
3551
  if (!this.knexInstance) {
3496
3552
  return;
@@ -3500,7 +3556,7 @@ var Db = class {
3500
3556
  this.knexInstance.on("query", (query) => {
3501
3557
  query.__startTime = process.hrtime();
3502
3558
  });
3503
- this.knexInstance.on("query-response", (response, query) => {
3559
+ this.knexInstance.on("query-response", (_response, query) => {
3504
3560
  const [seconds, nanoseconds] = process.hrtime(query.__startTime);
3505
3561
  const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
3506
3562
  const logEntry = {
@@ -3515,15 +3571,9 @@ var Db = class {
3515
3571
  this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
3516
3572
  });
3517
3573
  }
3518
- /**
3519
- * Get query log (only available if profiling is enabled)
3520
- */
3521
3574
  getQueryLog() {
3522
3575
  return [...this.queriesLog];
3523
3576
  }
3524
- /**
3525
- * Check if a table exists
3526
- */
3527
3577
  async tableExists(tableName) {
3528
3578
  if (!this.knexInstance) {
3529
3579
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3535,65 +3585,87 @@ var Db = class {
3535
3585
  throw error;
3536
3586
  }
3537
3587
  }
3538
- /**
3539
- * Get the underlying Knex instance (for advanced usage)
3540
- */
3541
3588
  getKnex() {
3542
3589
  if (!this.knexInstance) {
3543
3590
  throw new Error("Db: Not connected. Call connect() first.");
3544
3591
  }
3545
3592
  return this.knexInstance;
3546
3593
  }
3547
- /**
3548
- * Get connection status
3549
- */
3550
3594
  isConnectedToDb() {
3551
3595
  return this.isConnected && this.knexInstance !== null;
3552
3596
  }
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
3597
  };
3561
3598
  function capitalizeFirstLetter(str) {
3562
3599
  return str.charAt(0).toUpperCase() + str.slice(1);
3563
3600
  }
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
- };
3601
+ function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
3602
+ if (dbName) {
3603
+ return dbName;
3604
+ }
3605
+ if (mergedName) {
3606
+ return mergedName;
3607
+ }
3608
+ if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
3609
+ return connectionParam.slice("dbConnectionString".length).toLowerCase();
3610
+ }
3611
+ if (connectionParam === "dbConnectionString" && argsEnv) {
3612
+ return argsEnv;
3613
+ }
3614
+ return void 0;
3615
+ }
3616
+ function formatDbConnectMessage(name, connectionString) {
3617
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
3618
+ if (name) {
3619
+ return `[Db] Connected to database "${name}"${endpointSuffix}`;
3620
+ }
3621
+ return `[Db] Connected${endpointSuffix}`;
3622
+ }
3623
+ function formatDbDisconnectMessage(name, connectionString) {
3624
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
3625
+ if (name) {
3626
+ return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
3627
+ }
3628
+ return `[Db] Disconnected${endpointSuffix}`;
3629
+ }
3630
+ function formatDbInstanceMessage(action, name) {
3631
+ if (name) {
3632
+ return `[Db] instance "${name}" ${action}`;
3633
+ }
3634
+ return `[Db] instance ${action}`;
3635
+ }
3636
+ function formatConnectionEndpointSuffix(connectionString) {
3637
+ const endpoint = formatConnectionEndpoint(connectionString);
3638
+ return endpoint ? ` (${endpoint})` : "";
3639
+ }
3640
+ function formatConnectionEndpoint(connectionString) {
3641
+ try {
3642
+ const url = new URL(connectionString);
3643
+ const host = url.hostname;
3644
+ if (!host) {
3645
+ return null;
3646
+ }
3647
+ let port = url.port;
3648
+ if (!port) {
3649
+ if (url.protocol === "postgresql:") {
3650
+ port = "5432";
3651
+ } else if (url.protocol === "mysql:") {
3652
+ port = "3306";
3653
+ }
3654
+ }
3655
+ return port ? `${host}:${port}` : host;
3656
+ } catch {
3657
+ return null;
3658
+ }
3659
+ }
3660
+ async function dbConnect(context, config2) {
3589
3661
  try {
3590
3662
  const db = new Db(config2);
3591
3663
  context.registerCleanup(async () => {
3592
3664
  await db.disconnect();
3593
- context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
3665
+ context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
3594
3666
  });
3595
3667
  await db.connect();
3596
- context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
3668
+ context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
3597
3669
  return db;
3598
3670
  } catch (error) {
3599
3671
  if (error instanceof ParamError) {
@@ -3603,52 +3675,266 @@ async function dbConnect(context, connectionString, name, dbProfile) {
3603
3675
  throw new ParamError(`[Db] connect error: ${errorMsg}`);
3604
3676
  }
3605
3677
  }
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"
3678
+
3679
+ // src/s3/index.js
3680
+ import {
3681
+ S3Client,
3682
+ HeadBucketCommand,
3683
+ HeadObjectCommand,
3684
+ GetObjectCommand,
3685
+ PutObjectCommand,
3686
+ DeleteObjectCommand,
3687
+ CopyObjectCommand,
3688
+ ListObjectsV2Command,
3689
+ PutObjectTaggingCommand,
3690
+ GetObjectTaggingCommand
3691
+ } from "@aws-sdk/client-s3";
3692
+ var DEFAULT_PROFILE = "local";
3693
+ function capitalize(str) {
3694
+ if (!str) return str;
3695
+ return str.charAt(0).toUpperCase() + str.slice(1);
3696
+ }
3697
+ var S3 = class _S3 {
3698
+ /**
3699
+ * Build an S3 instance. Reads bucket profile from --bucket (default "local"),
3700
+ * then resolves per-profile params, builds the SDK client, and returns the
3701
+ * instance. Optionally pings the bucket once to verify reachability.
3702
+ */
3703
+ static async init(context, options = {}) {
3704
+ const profileDef = { bucket: "string" };
3705
+ const discovered = context?.params?.getAllForModule?.("s3", profileDef) ?? {};
3706
+ const profile = options.bucket ?? discovered.bucket ?? DEFAULT_PROFILE;
3707
+ const cap = capitalize(profile);
3708
+ const config2 = {
3709
+ profile,
3710
+ bucketName: options.bucketName ?? await context.params.get(`s3Bucket${cap}`, "string"),
3711
+ region: options.region ?? await context.params.get(`s3Region${cap}`, "string default us-east-1"),
3712
+ endpoint: options.endpoint ?? await context.params.get(`s3Endpoint${cap}`, "string"),
3713
+ forcePathStyle: options.forcePathStyle ?? await context.params.get(`s3ForcePathStyle${cap}`, "boolean default false"),
3714
+ accessKeyId: options.accessKeyId ?? await context.params.get(`s3AccessKeyId${cap}`, "string"),
3715
+ secretAccessKey: options.secretAccessKey ?? await context.params.get(`s3SecretAccessKey${cap}`, "string")
3622
3716
  };
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) {
3717
+ if (!config2.bucketName) {
3635
3718
  throw new ParamError(
3636
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3719
+ `S3: bucket name not configured for profile "${profile}" (set s3Bucket${cap} or S3_BUCKET_${profile.toUpperCase()})`
3637
3720
  );
3638
3721
  }
3722
+ const s3 = new _S3(context, config2);
3723
+ if (options.testBucket !== false) {
3724
+ try {
3725
+ await s3.bucketExists();
3726
+ context.logger?.debug?.(
3727
+ `[S3] profile="${profile}" bucket="${config2.bucketName}" reachable`
3728
+ );
3729
+ } catch (err) {
3730
+ context.logger?.warn?.(
3731
+ `[S3] profile="${profile}" bucket="${config2.bucketName}" reachability test failed: ${err?.message ?? err}`
3732
+ );
3733
+ }
3734
+ }
3735
+ return s3;
3736
+ }
3737
+ constructor(context, config2) {
3738
+ this.logger = context?.logger ?? console;
3739
+ this.profile = config2.profile;
3740
+ this.bucketName = config2.bucketName;
3741
+ this.region = config2.region;
3742
+ this.endpoint = config2.endpoint || null;
3743
+ const clientConfig = { region: config2.region };
3744
+ if (config2.endpoint) clientConfig.endpoint = config2.endpoint;
3745
+ if (config2.forcePathStyle) clientConfig.forcePathStyle = true;
3746
+ if (config2.accessKeyId && config2.secretAccessKey) {
3747
+ clientConfig.credentials = {
3748
+ accessKeyId: config2.accessKeyId,
3749
+ secretAccessKey: config2.secretAccessKey
3750
+ };
3751
+ }
3752
+ this.client = new S3Client(clientConfig);
3639
3753
  }
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
- }
3754
+ // ── info ────────────────────────────────────────────────────────────────
3755
+ getBucketName() {
3756
+ return this.bucketName;
3757
+ }
3758
+ getProfile() {
3759
+ return this.profile;
3760
+ }
3761
+ getRegion() {
3762
+ return this.region;
3763
+ }
3764
+ getEndpoint() {
3765
+ return this.endpoint;
3766
+ }
3767
+ // ── reachability ────────────────────────────────────────────────────────
3768
+ async bucketExists() {
3769
+ await this.client.send(new HeadBucketCommand({ Bucket: this.bucketName }));
3770
+ return true;
3771
+ }
3772
+ // ── HEAD / GET ──────────────────────────────────────────────────────────
3773
+ /** Returns null on 404; never throws for "missing". Other errors throw. */
3774
+ async headObject(key) {
3775
+ try {
3776
+ const out = await this.client.send(new HeadObjectCommand({
3777
+ Bucket: this.bucketName,
3778
+ Key: key
3779
+ }));
3780
+ return {
3781
+ etag: out.ETag,
3782
+ size: out.ContentLength,
3783
+ contentType: out.ContentType,
3784
+ lastModified: out.LastModified,
3785
+ metadata: out.Metadata,
3786
+ storageClass: out.StorageClass
3787
+ };
3788
+ } catch (err) {
3789
+ if (this._isNotFound(err)) return null;
3790
+ throw err;
3791
+ }
3792
+ }
3793
+ /** Returns { body: Readable, contentType, contentLength, etag, ... } or null on 404. */
3794
+ async getObject(key) {
3795
+ try {
3796
+ const out = await this.client.send(new GetObjectCommand({
3797
+ Bucket: this.bucketName,
3798
+ Key: key
3799
+ }));
3800
+ return {
3801
+ body: out.Body,
3802
+ contentType: out.ContentType,
3803
+ contentLength: out.ContentLength,
3804
+ etag: out.ETag,
3805
+ lastModified: out.LastModified,
3806
+ metadata: out.Metadata
3807
+ };
3808
+ } catch (err) {
3809
+ if (this._isNotFound(err)) return null;
3810
+ throw err;
3811
+ }
3812
+ }
3813
+ /** Buffers the whole object. Use only for small objects (manifests, JSON). */
3814
+ async getObjectBytes(key) {
3815
+ const obj = await this.getObject(key);
3816
+ if (!obj) return null;
3817
+ const chunks = [];
3818
+ for await (const chunk of obj.body) chunks.push(chunk);
3819
+ return { ...obj, body: Buffer.concat(chunks) };
3820
+ }
3821
+ /** Convenience for JSON manifests. Returns parsed object or null on 404. */
3822
+ async getJson(key) {
3823
+ const obj = await this.getObjectBytes(key);
3824
+ if (!obj) return null;
3825
+ return JSON.parse(obj.body.toString("utf8"));
3826
+ }
3827
+ // ── PUT ─────────────────────────────────────────────────────────────────
3828
+ async putObject({ key, body, contentType, contentLength, tags, metadata }) {
3829
+ const cmd = new PutObjectCommand({
3830
+ Bucket: this.bucketName,
3831
+ Key: key,
3832
+ Body: body,
3833
+ ...contentType && { ContentType: contentType },
3834
+ ...contentLength != null && { ContentLength: contentLength },
3835
+ ...metadata && { Metadata: metadata },
3836
+ ...tags && { Tagging: this._tagsToQuery(tags) }
3837
+ });
3838
+ return this.client.send(cmd);
3839
+ }
3840
+ /** Convenience for JSON manifests. */
3841
+ async putJson(key, value, opts = {}) {
3842
+ const json = JSON.stringify(value, null, opts.pretty ? 2 : 0);
3843
+ const body = Buffer.from(json, "utf8");
3844
+ return this.putObject({
3845
+ key,
3846
+ body,
3847
+ contentType: "application/json",
3848
+ contentLength: body.length,
3849
+ tags: opts.tags,
3850
+ metadata: opts.metadata
3851
+ });
3852
+ }
3853
+ // ── DELETE ──────────────────────────────────────────────────────────────
3854
+ async deleteObject(key) {
3855
+ return this.client.send(new DeleteObjectCommand({
3856
+ Bucket: this.bucketName,
3857
+ Key: key
3858
+ }));
3859
+ }
3860
+ // ── COPY (for migration: legacy → new bucket, or intra-bucket "rename") ─
3861
+ async copyObject({ sourceBucket, sourceKey, key, contentType, metadata, tags }) {
3862
+ const src = sourceBucket || this.bucketName;
3863
+ const cmd = new CopyObjectCommand({
3864
+ Bucket: this.bucketName,
3865
+ Key: key,
3866
+ CopySource: encodeURIComponent(`${src}/${sourceKey}`),
3867
+ ...contentType && {
3868
+ ContentType: contentType,
3869
+ MetadataDirective: "REPLACE"
3870
+ },
3871
+ ...metadata && {
3872
+ Metadata: metadata,
3873
+ MetadataDirective: "REPLACE"
3874
+ },
3875
+ ...tags && {
3876
+ Tagging: this._tagsToQuery(tags),
3877
+ TaggingDirective: "REPLACE"
3878
+ }
3879
+ });
3880
+ return this.client.send(cmd);
3881
+ }
3882
+ // ── LIST ────────────────────────────────────────────────────────────────
3883
+ async listObjects(prefix, { keysOnly = false, maxKeys = 1e3, continuationToken } = {}) {
3884
+ const out = await this.client.send(new ListObjectsV2Command({
3885
+ Bucket: this.bucketName,
3886
+ Prefix: prefix,
3887
+ MaxKeys: maxKeys,
3888
+ ContinuationToken: continuationToken
3889
+ }));
3890
+ const items = (out.Contents ?? []).map((o) => ({
3891
+ key: o.Key,
3892
+ size: o.Size,
3893
+ etag: o.ETag,
3894
+ lastModified: o.LastModified,
3895
+ storageClass: o.StorageClass
3896
+ }));
3897
+ return {
3898
+ items: keysOnly ? items.map((i) => i.key) : items,
3899
+ isTruncated: !!out.IsTruncated,
3900
+ nextContinuationToken: out.NextContinuationToken
3901
+ };
3902
+ }
3903
+ // ── TAGS (used for lifecycle rules, e.g. status=closed → Glacier IR) ────
3904
+ async putObjectTagging(key, tags) {
3905
+ return this.client.send(new PutObjectTaggingCommand({
3906
+ Bucket: this.bucketName,
3907
+ Key: key,
3908
+ Tagging: { TagSet: this._tagsToTagSet(tags) }
3909
+ }));
3910
+ }
3911
+ async getObjectTagging(key) {
3912
+ const out = await this.client.send(new GetObjectTaggingCommand({
3913
+ Bucket: this.bucketName,
3914
+ Key: key
3915
+ }));
3916
+ const tags = {};
3917
+ for (const t of out.TagSet ?? []) tags[t.Key] = t.Value;
3918
+ return tags;
3919
+ }
3920
+ // ── internals ───────────────────────────────────────────────────────────
3921
+ _isNotFound(err) {
3922
+ const status = err?.$metadata?.httpStatusCode;
3923
+ return status === 404 || err?.name === "NotFound" || err?.name === "NoSuchKey" || err?.Code === "NoSuchKey";
3924
+ }
3925
+ _tagsToQuery(tags) {
3926
+ return Object.entries(tags).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
3927
+ }
3928
+ _tagsToTagSet(tags) {
3929
+ return Object.entries(tags).map(([Key, Value]) => ({ Key, Value: String(Value) }));
3930
+ }
3931
+ };
3646
3932
 
3647
- // src/logger/index.ts
3933
+ // src/logger/index.js
3648
3934
  import chalk from "chalk";
3649
3935
  import util from "util";
3650
3936
 
3651
- // src/logger/transports.ts
3937
+ // src/logger/transports.js
3652
3938
  var ConsoleTransport = class {
3653
3939
  write(payload) {
3654
3940
  console.info(payload);
@@ -3668,7 +3954,7 @@ var ParentProcessTransport = class {
3668
3954
  }
3669
3955
  };
3670
3956
 
3671
- // src/logger/index.ts
3957
+ // src/logger/index.js
3672
3958
  var ALL_LEVELS = [
3673
3959
  "silly",
3674
3960
  "debug",
@@ -3741,6 +4027,7 @@ var Logger = class _Logger {
3741
4027
  }
3742
4028
  /**
3743
4029
  * Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
4030
+ * Params are tracked under the `logger` module for --showUsedParams.
3744
4031
  */
3745
4032
  static init(context, options) {
3746
4033
  const paramDefs = {
@@ -3754,7 +4041,7 @@ var Logger = class _Logger {
3754
4041
  progressWithTimes: "boolean default false",
3755
4042
  progressThrottleMs: "number"
3756
4043
  };
3757
- const discovered = context.params.getAllForModule(paramDefs);
4044
+ const discovered = context.params.getAllForModule("logger", paramDefs);
3758
4045
  const config2 = { ...discovered, ...options };
3759
4046
  const logger = new _Logger(context, config2);
3760
4047
  context.logger = logger;
@@ -3950,9 +4237,9 @@ var Logger = class _Logger {
3950
4237
  }
3951
4238
  };
3952
4239
 
3953
- // src/init/index.ts
4240
+ // src/init/index.js
3954
4241
  import { EventEmitter } from "events";
3955
- function extractComponentOptions(opts, componentName) {
4242
+ function extractComponentOptions(opts, _componentName) {
3956
4243
  const reservedKeys = ["overrides", "defaults", "modules"];
3957
4244
  const componentOptions = {};
3958
4245
  for (const [key, value] of Object.entries(opts)) {
@@ -3997,7 +4284,10 @@ function setupContext(opts = {}) {
3997
4284
  return setup(opts);
3998
4285
  }
3999
4286
 
4000
- // src/utils/core-utils.ts
4287
+ // src/tasks/index.js
4288
+ import os3 from "os";
4289
+
4290
+ // src/utils/core-utils.js
4001
4291
  function sleepMs(ms) {
4002
4292
  return new Promise((resolve2) => setTimeout(resolve2, ms));
4003
4293
  }
@@ -4006,14 +4296,82 @@ function toJsonColumn(value) {
4006
4296
  return JSON.stringify(value);
4007
4297
  }
4008
4298
 
4009
- // src/tasks/servicesRegistry.ts
4010
- import { randomUUID as randomUUID2 } from "crypto";
4011
- import { mkdir, readFile, writeFile } from "fs/promises";
4299
+ // src/tasks/servicesRegistry.js
4012
4300
  import os from "os";
4013
- import path4 from "path";
4014
4301
 
4015
- // src/tasks/taskUtils.ts
4302
+ // src/tasks/taskUtils.js
4016
4303
  import { randomUUID } from "crypto";
4304
+
4305
+ // src/tasks/time-matcher.js
4306
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4307
+ function resolveAsterisks(field, range) {
4308
+ return field.includes("*") ? field.replace("*", range) : field;
4309
+ }
4310
+ function resolveRanges(field) {
4311
+ const regex = /(\d+)-(\d+)/;
4312
+ let current = field;
4313
+ while (true) {
4314
+ const match = regex.exec(current);
4315
+ if (!match) break;
4316
+ const raw = match[0];
4317
+ let first = Number(match[1]);
4318
+ let last = Number(match[2]);
4319
+ if (last < first) {
4320
+ [first, last] = [last, first];
4321
+ }
4322
+ const values = [];
4323
+ for (let i = first; i <= last; i += 1) {
4324
+ values.push(i);
4325
+ }
4326
+ current = current.replace(raw, values.join(","));
4327
+ }
4328
+ return current;
4329
+ }
4330
+ function resolveSteps(field) {
4331
+ const match = /^(.+)\/(\d+)$/.exec(field);
4332
+ if (!match) return field;
4333
+ const base = match[1];
4334
+ const step = Number(match[2]);
4335
+ if (!Number.isFinite(step) || step <= 0) return field;
4336
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4337
+ }
4338
+ function convertPattern(pattern) {
4339
+ const parts = pattern.trim().split(/\s+/);
4340
+ if (parts.length !== 6) {
4341
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4342
+ }
4343
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4344
+ }
4345
+ function fieldMatches(field, value) {
4346
+ const allowed = field.split(",").map((v) => Number(v));
4347
+ return allowed.includes(value);
4348
+ }
4349
+ function matchesParsedPattern(parsed, date) {
4350
+ 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());
4351
+ }
4352
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4353
+ const parsed = convertPattern(pattern);
4354
+ return matchesParsedPattern(parsed, date);
4355
+ }
4356
+ var MS_PER_SECOND = 1e3;
4357
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
4358
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
4359
+ const parsed = convertPattern(pattern);
4360
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
4361
+ const end = t + maxSearchMs;
4362
+ while (t <= end) {
4363
+ const date = new Date(t);
4364
+ if (matchesParsedPattern(parsed, date)) {
4365
+ return date;
4366
+ }
4367
+ t += MS_PER_SECOND;
4368
+ }
4369
+ throw new Error(
4370
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
4371
+ );
4372
+ }
4373
+
4374
+ // src/tasks/taskUtils.js
4017
4375
  function getDb(context) {
4018
4376
  const db = context.db;
4019
4377
  if (!db) {
@@ -4021,113 +4379,117 @@ function getDb(context) {
4021
4379
  }
4022
4380
  return db;
4023
4381
  }
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
- }
4382
+ function queueToTableNames(queueName) {
4028
4383
  return {
4029
- tasksTable: queue,
4030
- historyTable: `${queue}_history`
4384
+ tasksTable: queueName,
4385
+ historyTable: `${queueName}_history`,
4386
+ registryTable: `${queueName}_services_registry`
4031
4387
  };
4032
4388
  }
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`;
4389
+ function defineTasksTable(t, db, tableNameForIndex) {
4390
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4391
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4392
+ t.timestamp("started_at");
4393
+ t.timestamp("completed_at");
4394
+ t.integer("priority").notNullable().defaultTo(50);
4395
+ t.text("schedule");
4396
+ t.timestamp("next_run_at").defaultTo(null);
4397
+ t.timestamp("past_due").defaultTo(null);
4398
+ t.text("name").notNullable();
4399
+ t.text("opid");
4400
+ t.json("params");
4401
+ t.text("service_group");
4402
+ t.integer("instance_number");
4403
+ t.text("service_name");
4404
+ t.text("server_name");
4405
+ t.text("status").notNullable().defaultTo("idle");
4406
+ t.timestamp("status_changed_at").defaultTo(null);
4407
+ t.text("progress");
4408
+ t.boolean("success");
4409
+ t.json("results");
4410
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
4411
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
4412
+ }
4413
+ function taskHistoryInsertFromQueueRow(row, overrides) {
4414
+ const { id, ...snapshot } = row;
4415
+ void id;
4416
+ return {
4417
+ ...snapshot,
4418
+ ...overrides
4419
+ };
4038
4420
  }
4039
4421
  async function ensureTaskTables(context, options = {}) {
4040
- const queue = options.queue ?? "tasks";
4422
+ const queueName = options.queueName ?? "tasks";
4041
4423
  const recreate = options.recreate ?? false;
4042
4424
  const db = getDb(context);
4043
- const { tasksTable, historyTable } = queueToTableNames(queue);
4425
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
4044
4426
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
4045
4427
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
4428
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
4046
4429
  if (recreate) {
4047
4430
  await db.schema.dropTableIfExists(historyTable);
4048
4431
  await db.schema.dropTableIfExists(tasksTable);
4432
+ await db.schema.dropTableIfExists(registryTable);
4049
4433
  }
4050
4434
  if (needsTasks) {
4051
4435
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
4052
4436
  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`);
4437
+ defineTasksTable(t, db, tasksTable);
4073
4438
  });
4074
4439
  }
4075
4440
  if (needsHistory) {
4076
4441
  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`);
4442
+ defineTasksTable(t, db, historyTable);
4095
4443
  });
4096
4444
  }
4097
- const registryTable = servicesRegistryTable(queue);
4098
- const needsRegistry = !await db.tableExists(registryTable);
4099
4445
  if (needsRegistry) {
4100
4446
  await db.schema.createTable(registryTable, (t) => {
4101
4447
  t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4102
- t.uuid("instance_id").notNullable().unique();
4103
- t.text("queue").notNullable();
4448
+ t.text("queue_name").notNullable();
4104
4449
  t.text("service_group").notNullable();
4450
+ t.integer("instance_number").notNullable().defaultTo(1);
4105
4451
  t.text("service_name").notNullable();
4106
- t.text("target").notNullable();
4107
- t.text("hostname");
4452
+ t.text("server_name").notNullable();
4108
4453
  t.integer("pid");
4109
4454
  t.json("metadata");
4110
4455
  t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4111
4456
  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`);
4457
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
4458
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
4459
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
4115
4460
  });
4116
4461
  }
4117
4462
  }
4118
4463
  async function enqueueTask(context, options) {
4119
4464
  const db = getDb(context);
4120
- const queue = options.queue ?? "tasks";
4121
- const { tasksTable } = queueToTableNames(queue);
4465
+ const queueName = options.queueName ?? "tasks";
4466
+ const { tasksTable } = queueToTableNames(queueName);
4122
4467
  const id = randomUUID();
4468
+ const name = options.name ?? options.task;
4469
+ if (!name) {
4470
+ throw new Error("enqueueTask: name (or task) is required");
4471
+ }
4472
+ const schedule = options.schedule?.trim() ? options.schedule : null;
4473
+ let nextRunAt = null;
4474
+ if (options.nextRunAt !== void 0) {
4475
+ nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
4476
+ } else if (schedule) {
4477
+ nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
4478
+ }
4123
4479
  await db(tasksTable).insert({
4124
4480
  id,
4125
- target: options.target,
4126
- task: options.task,
4481
+ name,
4127
4482
  params: toJsonColumn(options.params ?? null),
4128
4483
  opid: options.opid ?? null,
4129
- priority: options.priority ?? 0,
4130
- schedule: options.schedule ?? null
4484
+ priority: options.priority ?? 50,
4485
+ schedule,
4486
+ next_run_at: nextRunAt,
4487
+ service_group: options.serviceGroup ?? null,
4488
+ instance_number: options.instanceNumber ?? null,
4489
+ service_name: options.serviceName ?? null,
4490
+ server_name: options.serverName ?? null,
4491
+ status: "idle",
4492
+ status_changed_at: db.fn.now()
4131
4493
  });
4132
4494
  return id;
4133
4495
  }
@@ -4138,7 +4500,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
4138
4500
  });
4139
4501
  }
4140
4502
 
4141
- // src/tasks/servicesRegistry.ts
4503
+ // src/tasks/servicesRegistry.js
4142
4504
  function getDb2(context) {
4143
4505
  const db = context.db;
4144
4506
  if (!db) {
@@ -4152,7 +4514,7 @@ function parseMetadataColumn(value) {
4152
4514
  if (typeof value === "string") {
4153
4515
  try {
4154
4516
  const p = JSON.parse(value);
4155
- return p && typeof p === "object" && !Array.isArray(p) ? p : {};
4517
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
4156
4518
  } catch {
4157
4519
  return {};
4158
4520
  }
@@ -4162,6 +4524,7 @@ function parseMetadataColumn(value) {
4162
4524
  var DEFAULT_GROUP_MAX_INSTANCES = {
4163
4525
  intake: 1,
4164
4526
  harvest: 1,
4527
+ harvester: 0,
4165
4528
  loader: 0,
4166
4529
  photos: 0,
4167
4530
  photosprocessor: 0,
@@ -4171,25 +4534,6 @@ function sanitizeNamePart(raw) {
4171
4534
  const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4172
4535
  return s.slice(0, 80) || "runner";
4173
4536
  }
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
4537
  function resolveMaxInstances(serviceGroup, override) {
4194
4538
  if (override !== void 0 && Number.isFinite(override)) {
4195
4539
  return Math.max(0, Math.floor(Number(override)));
@@ -4197,179 +4541,190 @@ function resolveMaxInstances(serviceGroup, override) {
4197
4541
  const g = serviceGroup.trim().toLowerCase();
4198
4542
  return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
4199
4543
  }
4200
- async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
4544
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
4201
4545
  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);
4546
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
4547
+ if (excludeRowId) {
4548
+ q = q.whereNot("id", excludeRowId);
4205
4549
  }
4206
4550
  const row = await q.count("id as count").first();
4207
4551
  return Number(row?.count ?? 0);
4208
4552
  }
4553
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
4554
+ const cutoff = new Date(Date.now() - staleMs);
4555
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
4556
+ const set = /* @__PURE__ */ new Set();
4557
+ for (const r of rows) {
4558
+ const n = Number(r.instance_number);
4559
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
4560
+ }
4561
+ return set;
4562
+ }
4209
4563
  function isUniqueViolation(error) {
4210
4564
  const code = error?.code ?? error?.errno;
4211
4565
  return code === "23505" || String(error?.message || "").includes("duplicate key");
4212
4566
  }
4567
+ function buildMetadata(options) {
4568
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
4569
+ if (options.target) {
4570
+ base.runnerTarget = options.target;
4571
+ }
4572
+ return toJsonColumn(Object.keys(base).length ? base : null);
4573
+ }
4574
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
4575
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
4576
+ const e = Math.max(1, Math.floor(Number(explicit)));
4577
+ if (occupied.has(e)) {
4578
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
4579
+ }
4580
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
4581
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
4582
+ }
4583
+ return e;
4584
+ }
4585
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
4586
+ for (let n = 1; n <= cap; n++) {
4587
+ if (!occupied.has(n)) return n;
4588
+ }
4589
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
4590
+ }
4591
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
4592
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
4593
+ }
4213
4594
  async function registerInServicesRegistry(context, options) {
4214
4595
  const db = getDb2(context);
4215
- const registryTable = servicesRegistryTable(options.queue);
4596
+ const registryTable = queueToTableNames(options.queueName).registryTable;
4216
4597
  const serviceGroup = options.serviceGroup.trim();
4217
4598
  if (!serviceGroup) {
4218
4599
  throw new Error("registerInServicesRegistry: serviceGroup is required");
4219
4600
  }
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();
4601
+ const serverName = os.hostname();
4226
4602
  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
- }
4603
+ const meta = buildMetadata(options);
4604
+ const groupBase = sanitizeNamePart(serviceGroup);
4605
+ const hostBase = sanitizeNamePart(serverName);
4263
4606
  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}).`;
4607
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
4608
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
4609
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
4274
4610
  if (options.enforceMaxInstances) {
4275
4611
  throw new Error(msg);
4276
4612
  }
4277
4613
  context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
4278
4614
  }
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;
4299
- }
4300
- }
4301
- }
4302
- let inserted;
4303
- for (const candidate of eachServiceNameCandidate(baseCandidates)) {
4304
- try {
4305
- const rows = await db(registryTable).insert({
4306
- instance_id: instanceId,
4307
- queue: options.queue,
4308
- service_group: serviceGroup,
4309
- service_name: candidate,
4310
- target: options.target,
4311
- hostname,
4615
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
4616
+ const cutoff = new Date(Date.now() - options.staleMs);
4617
+ const MAX_ATTEMPTS = 8;
4618
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
4619
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
4620
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
4621
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
4622
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
4623
+ if (existing) {
4624
+ const lastSeen = new Date(existing.last_seen_at);
4625
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
4626
+ if (isAlive) {
4627
+ if (options.serviceName?.trim()) {
4628
+ throw new Error(
4629
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
4630
+ );
4631
+ }
4632
+ context.logger.warn?.(
4633
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
4634
+ );
4635
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
4636
+ throw new Error(
4637
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
4638
+ );
4639
+ }
4640
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
4641
+ continue;
4642
+ }
4643
+ await db(registryTable).where({ id: existing.id }).update({
4644
+ server_name: serverName,
4312
4645
  pid,
4313
4646
  metadata: meta,
4647
+ service_group: serviceGroup,
4648
+ instance_number: instanceNumber,
4314
4649
  last_seen_at: db.fn.now()
4650
+ });
4651
+ const reg = {
4652
+ serviceName: serviceNameRaw,
4653
+ serviceGroup,
4654
+ queueName: options.queueName,
4655
+ target: options.target,
4656
+ rowId: String(existing.id),
4657
+ registryTable,
4658
+ instanceNumber
4659
+ };
4660
+ context.servicesRegistry = reg;
4661
+ context.runnerHeartbeat = reg;
4662
+ context.logger.info?.(
4663
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
4664
+ );
4665
+ return reg;
4666
+ }
4667
+ try {
4668
+ const rows = await db(registryTable).insert({
4669
+ queue_name: options.queueName,
4670
+ service_group: serviceGroup,
4671
+ instance_number: instanceNumber,
4672
+ service_name: serviceNameRaw,
4673
+ server_name: serverName,
4674
+ pid,
4675
+ metadata: meta,
4676
+ last_seen_at: db.fn.now(),
4677
+ created_at: db.fn.now()
4315
4678
  }).returning(["id", "service_name"]);
4316
4679
  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;
4680
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
4681
+ if (!rowId) {
4682
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
4683
+ rowId = again?.id != null ? String(again.id) : "";
4320
4684
  }
4685
+ if (!rowId) continue;
4686
+ const regNew = {
4687
+ serviceName: String(row?.service_name ?? serviceNameRaw),
4688
+ serviceGroup,
4689
+ queueName: options.queueName,
4690
+ target: options.target,
4691
+ rowId,
4692
+ registryTable,
4693
+ instanceNumber
4694
+ };
4695
+ context.servicesRegistry = regNew;
4696
+ context.runnerHeartbeat = regNew;
4697
+ context.logger.info?.(
4698
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
4699
+ );
4700
+ return regNew;
4321
4701
  } catch (error) {
4322
4702
  if (!isUniqueViolation(error)) {
4323
4703
  throw error;
4324
4704
  }
4705
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
4325
4706
  }
4326
4707
  }
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}`
4708
+ throw new Error(
4709
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
4346
4710
  );
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
4711
  }
4357
4712
  async function touchServicesRegistry(context, registration) {
4358
4713
  const db = getDb2(context);
4359
- const hostname = os.hostname();
4714
+ const serverName = os.hostname();
4360
4715
  const pid = typeof process.pid === "number" ? process.pid : null;
4361
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
4716
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4362
4717
  last_seen_at: db.fn.now(),
4363
- hostname,
4718
+ server_name: serverName,
4364
4719
  pid
4365
4720
  });
4366
4721
  }
4367
4722
  async function updateServicesRegistryMetadata(context, registration, patch) {
4368
4723
  const db = getDb2(context);
4369
- const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
4724
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
4370
4725
  const prev = parseMetadataColumn(row?.metadata);
4371
4726
  const merged = { ...prev, ...patch };
4372
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
4727
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4373
4728
  metadata: toJsonColumn(merged),
4374
4729
  last_seen_at: db.fn.now()
4375
4730
  });
@@ -4377,14 +4732,14 @@ async function updateServicesRegistryMetadata(context, registration, patch) {
4377
4732
  }
4378
4733
  async function unregisterServicesRegistry(context, registration) {
4379
4734
  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}`);
4735
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
4736
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
4382
4737
  }
4383
- async function listServicesRegistry(context, options = { queue: "tasks" }) {
4738
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
4384
4739
  const db = getDb2(context);
4385
4740
  const staleMs = options.staleMs ?? 6e4;
4386
4741
  const cutoff = new Date(Date.now() - staleMs);
4387
- const table = servicesRegistryTable(options.queue);
4742
+ const table = queueToTableNames(options.queueName).registryTable;
4388
4743
  let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
4389
4744
  if (options.serviceGroup?.trim()) {
4390
4745
  q = q.where({ service_group: options.serviceGroup.trim() });
@@ -4392,7 +4747,8 @@ async function listServicesRegistry(context, options = { queue: "tasks" }) {
4392
4747
  return await q;
4393
4748
  }
4394
4749
 
4395
- // src/tasks/taskLogs.ts
4750
+ // src/tasks/taskLogs.js
4751
+ import path4 from "path";
4396
4752
  function getLogsState(context) {
4397
4753
  const holder = context;
4398
4754
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -4445,6 +4801,96 @@ function getLogsState(context) {
4445
4801
  holder.__tasksLogsState = state;
4446
4802
  return state;
4447
4803
  }
4804
+ function ipcLogTargetKey(target) {
4805
+ const bp = target.basePath ?? "";
4806
+ const ns = target.namespace ?? "";
4807
+ return `${bp}::${ns}::${target.tableName}`;
4808
+ }
4809
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
4810
+ const seg = (s) => {
4811
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
4812
+ return t.length ? t : "x";
4813
+ };
4814
+ return `${seg(source)}/${seg(resource)}`;
4815
+ }
4816
+ async function readTaskIpcLogsSnapshot(context, options) {
4817
+ const holder = context;
4818
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
4819
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
4820
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
4821
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
4822
+ const fd = new FileDatabase({
4823
+ basePath,
4824
+ namespace,
4825
+ tableName,
4826
+ versioned: true,
4827
+ useMetadata: true,
4828
+ maxVersions: 30,
4829
+ pageSize: 2e3,
4830
+ logger: holder.logger
4831
+ });
4832
+ const versions = await fd.getVersions();
4833
+ if (versions.length === 0) {
4834
+ return { records: [], latestTs: null };
4835
+ }
4836
+ const latest = versions[versions.length - 1];
4837
+ const raw = await fd.read({ version: latest });
4838
+ const arr = Array.isArray(raw) ? raw : [];
4839
+ let filtered = arr;
4840
+ if (options.afterTs && String(options.afterTs).trim()) {
4841
+ const cut = String(options.afterTs).trim();
4842
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
4843
+ }
4844
+ let latestTs = null;
4845
+ for (const r of filtered) {
4846
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
4847
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
4848
+ }
4849
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
4850
+ const maxReturn = incremental ? 1e4 : tail;
4851
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
4852
+ return { records: sliced, latestTs };
4853
+ }
4854
+ function resolveIpcFileLogsDir(context, target) {
4855
+ const holder = context;
4856
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4857
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4858
+ const segments = target.tableName.split("/").filter(Boolean);
4859
+ return path4.resolve(basePath, namespace, ...segments);
4860
+ }
4861
+ function getLogsStateForTarget(context, target) {
4862
+ const holder = context;
4863
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4864
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4865
+ if (!enabled) return null;
4866
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
4867
+ const map = holder.__tasksLogsTargetStates;
4868
+ const key = ipcLogTargetKey(target);
4869
+ if (map.has(key)) return map.get(key);
4870
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4871
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4872
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4873
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4874
+ const db = new FileDatabase({
4875
+ basePath,
4876
+ namespace,
4877
+ tableName: target.tableName,
4878
+ versioned: true,
4879
+ useMetadata: true,
4880
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4881
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4882
+ logger: holder.logger
4883
+ });
4884
+ const state = {
4885
+ db,
4886
+ errorDb: null,
4887
+ queue: Promise.resolve(),
4888
+ initialized: false,
4889
+ errorInitialized: false
4890
+ };
4891
+ map.set(key, state);
4892
+ return state;
4893
+ }
4448
4894
  function isErrorPayload(payload) {
4449
4895
  if (!payload) return false;
4450
4896
  if (typeof payload === "object") {
@@ -4464,14 +4910,26 @@ function buildLogRecord(task, payload) {
4464
4910
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4465
4911
  opid: task.opid ?? null,
4466
4912
  taskId: task.id,
4467
- taskName: task.task,
4468
- target: task.target,
4913
+ taskName: task.name,
4914
+ target: task.service_group,
4469
4915
  source: typeof params.source === "string" ? params.source : null,
4470
4916
  resource: typeof params.resource === "string" ? params.resource : null,
4471
4917
  payload
4472
4918
  };
4473
4919
  }
4474
- function appendTaskIpcLog(context, task, payload) {
4920
+ function appendTaskIpcLog(context, task, payload, target) {
4921
+ if (target) {
4922
+ const state2 = getLogsStateForTarget(context, target);
4923
+ if (!state2?.db) return;
4924
+ const record2 = buildLogRecord(task, payload);
4925
+ state2.queue = state2.queue.then(async () => {
4926
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
4927
+ state2.initialized = true;
4928
+ }).catch((error) => {
4929
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
4930
+ });
4931
+ return;
4932
+ }
4475
4933
  const state = getLogsState(context);
4476
4934
  if (!state.db && !state.errorDb) return;
4477
4935
  const record = buildLogRecord(task, payload);
@@ -4488,84 +4946,293 @@ function appendTaskIpcLog(context, task, payload) {
4488
4946
  context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
4489
4947
  });
4490
4948
  }
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);
4949
+ async function flushTaskIpcLogs(context) {
4950
+ const holder = context;
4951
+ const promises = [];
4952
+ if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
4953
+ const map = holder.__tasksLogsTargetStates;
4954
+ if (map) {
4955
+ for (const s of map.values()) {
4956
+ if (s.queue) promises.push(s.queue);
4512
4957
  }
4513
- current = current.replace(raw, values.join(","));
4514
4958
  }
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
- }
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());
4959
+ await Promise.all(promises);
4539
4960
  }
4540
4961
 
4541
- // src/tasks/TaskMaster.ts
4542
- var TaskMaster = class {
4543
- context;
4544
- task;
4962
+ // src/tasks/AbstractTask.js
4963
+ var AbstractTask = class _AbstractTask {
4964
+ /**
4965
+ * Whether `send-task` should wait for completion (and print a result
4966
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
4967
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
4968
+ *
4969
+ * @type {boolean}
4970
+ */
4971
+ static defaultWaitForResult = false;
4972
+ /**
4973
+ * @param {object} context Runner context (db, logger, params, emitter...).
4974
+ * @param {object} task Task row as claimed from the queue.
4975
+ */
4545
4976
  constructor(context, task) {
4546
4977
  this.context = context;
4547
4978
  this.task = task;
4548
4979
  }
4980
+ /**
4981
+ * Return a short reason string when the task should be deferred (e.g. "locked
4982
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
4983
+ *
4984
+ * @returns {string | false | Promise<string | false>}
4985
+ */
4549
4986
  cantRunReason() {
4550
4987
  return false;
4551
4988
  }
4989
+ /**
4990
+ * Called by the runner when a stop has been requested. Subclasses running
4991
+ * long loops should flip a flag here and check it between iterations.
4992
+ *
4993
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
4994
+ */
4552
4995
  requestStop(_allowanceMs) {
4553
4996
  }
4997
+ /**
4998
+ * Perform the task. Must be implemented by subclasses.
4999
+ *
5000
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
5001
+ * Updates the DB `progress` column. Accepts any serializable value;
5002
+ * strings are stored verbatim, objects are JSON-stringified.
5003
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5004
+ */
5005
+ async run(_reportProgress) {
5006
+ throw new Error("AbstractTask.run must be implemented by subclass");
5007
+ }
5008
+ /**
5009
+ * Resolve a complete row payload for this task — envelope fields (queue,
5010
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
5011
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
5012
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
5013
+ *
5014
+ * const payload = await TaskClass.resolveParams(context, { name });
5015
+ * await enqueueTask(context, payload);
5016
+ *
5017
+ * Validation failures throw {@link ParamError} so the script aborts before
5018
+ * a malformed row hits the DB.
5019
+ *
5020
+ * @param {object} context
5021
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
5022
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
5023
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
5024
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
5025
+ * @returns {Promise<object>}
5026
+ */
5027
+ static async resolveParams(context, overrides = {}) {
5028
+ const main = _AbstractTask._resolveMainFields(context, overrides);
5029
+ const params = await this.resolveCustomParams(context, overrides);
5030
+ return { ...main, params };
5031
+ }
5032
+ /**
5033
+ * Resolve the inner JSON blob stored in the `params` column. Default
5034
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
5035
+ * overlaid with `overrides.params` when supplied; returns `null` when
5036
+ * neither is provided.
5037
+ *
5038
+ * Subclasses with typed fields should override and call
5039
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
5040
+ * resolution, then validate and throw {@link ParamError} on bad input.
5041
+ *
5042
+ * @param {object} context
5043
+ * @param {Record<string, unknown>} [overrides]
5044
+ * @returns {Promise<object|null>}
5045
+ */
5046
+ static async resolveCustomParams(context, overrides = {}) {
5047
+ return _AbstractTask._defaultParamsBlob(context, overrides);
5048
+ }
5049
+ /**
5050
+ * Read main task envelope fields from `context.params` (CLI/env), with
5051
+ * any matching key on `overrides` taking precedence. Internal; called by
5052
+ * {@link AbstractTask.resolveParams}.
5053
+ *
5054
+ * @param {object} context
5055
+ * @param {Record<string, unknown>} [overrides]
5056
+ * @returns {object}
5057
+ */
5058
+ static _resolveMainFields(context, overrides = {}) {
5059
+ const defs = {
5060
+ queueName: "string default tasks",
5061
+ priority: "number default 50",
5062
+ serviceGroup: "string",
5063
+ serviceName: "string",
5064
+ instanceNumber: "number",
5065
+ serverName: "string",
5066
+ opid: "string",
5067
+ schedule: "string"
5068
+ };
5069
+ const cli = context.params.getAllForModule("task-envelope", defs);
5070
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
5071
+ if (!name) {
5072
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5073
+ }
5074
+ let instanceNumber;
5075
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
5076
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
5077
+ const n = Number(rawInstance);
5078
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
5079
+ throw new ParamError("--instanceNumber must be a positive integer when set");
5080
+ }
5081
+ instanceNumber = n;
5082
+ } else {
5083
+ instanceNumber = null;
5084
+ }
5085
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
5086
+ const priority = Number(priorityRaw);
5087
+ if (!Number.isFinite(priority)) {
5088
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
5089
+ }
5090
+ return {
5091
+ name,
5092
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
5093
+ priority,
5094
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
5095
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
5096
+ instanceNumber,
5097
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
5098
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
5099
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
5100
+ nextRunAt: overrides.nextRunAt ?? null
5101
+ };
5102
+ }
5103
+ /**
5104
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
5105
+ * object), then overlays `overrides.params` on top. Returns `null` when
5106
+ * neither is provided.
5107
+ *
5108
+ * @param {object} context
5109
+ * @param {Record<string, unknown>} [overrides]
5110
+ * @returns {object|null}
5111
+ */
5112
+ static _defaultParamsBlob(context, overrides = {}) {
5113
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
5114
+ const fromJson = parseParamsJson(cli.paramsJson);
5115
+ const fromOverride = pickParamsObject(overrides);
5116
+ if (!fromJson && !fromOverride) return null;
5117
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
5118
+ }
5119
+ /**
5120
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
5121
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
5122
+ * merges them with explicit `overrides.params` in increasing priority:
5123
+ *
5124
+ * typed CLI flags → --paramsJson → overrides.params
5125
+ *
5126
+ * Undefined values are dropped so defaults declared in `defs` aren't
5127
+ * overwritten by missing-flag noise. Returns the merged object; the
5128
+ * caller is responsible for validation and throwing `ParamError`.
5129
+ *
5130
+ * @param {object} context
5131
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
5132
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
5133
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
5134
+ * @returns {Record<string, unknown>}
5135
+ */
5136
+ static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
5137
+ const fullDefs = { ...defs, paramsJson: "string" };
5138
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
5139
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
5140
+ const fromCli = {};
5141
+ for (const [k, v] of Object.entries(cliRaw)) {
5142
+ if (k === "paramsJson") continue;
5143
+ if (v !== void 0 && v !== null) fromCli[k] = v;
5144
+ }
5145
+ const fromOverride = pickParamsObject(overrides) ?? {};
5146
+ return { ...fromCli, ...fromJson, ...fromOverride };
5147
+ }
4554
5148
  };
5149
+ function emptyToUndef(s) {
5150
+ if (s === void 0 || s === null) return void 0;
5151
+ if (typeof s !== "string") return s;
5152
+ const t = s.trim();
5153
+ return t.length ? t : void 0;
5154
+ }
5155
+ function parseParamsJson(raw) {
5156
+ if (raw == null) return null;
5157
+ const t = String(raw).trim();
5158
+ if (!t) return null;
5159
+ let parsed;
5160
+ try {
5161
+ parsed = JSON.parse(t);
5162
+ } catch (e) {
5163
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
5164
+ }
5165
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5166
+ throw new ParamError("--paramsJson must be a JSON object");
5167
+ }
5168
+ return parsed;
5169
+ }
5170
+ function pickParamsObject(overrides) {
5171
+ const p = overrides?.params;
5172
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
5173
+ return void 0;
5174
+ }
4555
5175
 
4556
- // src/tasks/coreTasks/TaskPing.ts
4557
- var TaskPing = class extends TaskMaster {
5176
+ // src/tasks/coreTasks/TaskPing.js
5177
+ var TaskPing = class extends AbstractTask {
5178
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
5179
+ static defaultWaitForResult = true;
5180
+ /** Ping takes no params. */
5181
+ static async resolveCustomParams() {
5182
+ return null;
5183
+ }
5184
+ /**
5185
+ * @returns {Promise<{ success: true, results: "pong" }>}
5186
+ */
4558
5187
  async run() {
4559
5188
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4560
5189
  return { success: true, results: "pong" };
4561
5190
  }
4562
5191
  };
4563
5192
 
4564
- // src/tasks/coreTasks/TaskSampleProcess.ts
4565
- var TaskSampleProcess = class extends TaskMaster {
4566
- stopRequested = false;
4567
- stopAllowanceMs = 0;
4568
- stopDecisionLogged = false;
5193
+ // src/tasks/coreTasks/TaskSampleProcess.js
5194
+ var TaskSampleProcess = class extends AbstractTask {
5195
+ /**
5196
+ * @param {object} context
5197
+ * @param {Record<string, unknown>} [overrides]
5198
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
5199
+ */
5200
+ static async resolveCustomParams(context, overrides = {}) {
5201
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
5202
+ total: "number default 10",
5203
+ delay: "number default 1000",
5204
+ name: "string"
5205
+ }, overrides);
5206
+ const total = Number(merged.total);
5207
+ const delay = Number(merged.delay);
5208
+ if (!Number.isInteger(total) || total <= 0) {
5209
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
5210
+ }
5211
+ if (!Number.isInteger(delay) || delay < 0) {
5212
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
5213
+ }
5214
+ const out = { total, delay };
5215
+ if (typeof merged.name === "string" && merged.name.trim()) {
5216
+ out.name = merged.name.trim();
5217
+ }
5218
+ return out;
5219
+ }
5220
+ /**
5221
+ * @param {object} context
5222
+ * @param {object} task
5223
+ */
5224
+ constructor(context, task) {
5225
+ super(context, task);
5226
+ this.stopRequested = false;
5227
+ this.stopAllowanceMs = 0;
5228
+ this.stopDecisionLogged = false;
5229
+ }
5230
+ /**
5231
+ * Runner-facing stop signal. Records the allowance window so the main loop
5232
+ * can decide per-iteration whether to finish or abort early.
5233
+ *
5234
+ * @param {number} allowanceMs
5235
+ */
4569
5236
  requestStop(allowanceMs) {
4570
5237
  this.stopRequested = true;
4571
5238
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -4573,6 +5240,14 @@ var TaskSampleProcess = class extends TaskMaster {
4573
5240
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4574
5241
  );
4575
5242
  }
5243
+ /**
5244
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
5245
+ * progress every iteration. Validates params up front; invalid values short-
5246
+ * circuit to a structured failure without starting the loop.
5247
+ *
5248
+ * @param {(progress: object) => Promise<void>} reportProgress
5249
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5250
+ */
4576
5251
  async run(reportProgress) {
4577
5252
  const totalRaw = this.task?.params?.total ?? 10;
4578
5253
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -4654,7 +5329,7 @@ var TaskSampleProcess = class extends TaskMaster {
4654
5329
  }
4655
5330
  };
4656
5331
 
4657
- // src/tasks/coreTasks/TaskShellCommand.ts
5332
+ // src/tasks/coreTasks/TaskShellCommand.js
4658
5333
  import { spawn } from "child_process";
4659
5334
  function runShellCommand(command, cwd) {
4660
5335
  return new Promise((resolve2, reject) => {
@@ -4684,7 +5359,27 @@ function runShellCommand(command, cwd) {
4684
5359
  });
4685
5360
  });
4686
5361
  }
4687
- var TaskShellCommand = class extends TaskMaster {
5362
+ var TaskShellCommand = class extends AbstractTask {
5363
+ /**
5364
+ * @param {object} context
5365
+ * @param {Record<string, unknown>} [overrides]
5366
+ * @returns {Promise<{ command: string, cwd?: string }>}
5367
+ */
5368
+ static async resolveCustomParams(context, overrides = {}) {
5369
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
5370
+ command: "string",
5371
+ cwd: "string"
5372
+ }, overrides);
5373
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
5374
+ if (!command) {
5375
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
5376
+ }
5377
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
5378
+ return cwd ? { command, cwd } : { command };
5379
+ }
5380
+ /**
5381
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5382
+ */
4688
5383
  async run() {
4689
5384
  const params = this.task?.params;
4690
5385
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -4733,7 +5428,7 @@ var TaskShellCommand = class extends TaskMaster {
4733
5428
  }
4734
5429
  };
4735
5430
 
4736
- // src/tasks/coreTasks/TaskSystemInfo.ts
5431
+ // src/tasks/coreTasks/TaskSystemInfo.js
4737
5432
  import os2 from "os";
4738
5433
  import fs4 from "fs/promises";
4739
5434
  function toGb(valueBytes) {
@@ -4753,7 +5448,16 @@ async function getDiskStats() {
4753
5448
  free: toGb(free)
4754
5449
  };
4755
5450
  }
4756
- var TaskSystemInfo = class extends TaskMaster {
5451
+ var TaskSystemInfo = class extends AbstractTask {
5452
+ /** Same UX expectation as `ping` — short probe, print the result. */
5453
+ static defaultWaitForResult = true;
5454
+ /** systemInfo takes no params. */
5455
+ static async resolveCustomParams() {
5456
+ return null;
5457
+ }
5458
+ /**
5459
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5460
+ */
4757
5461
  async run() {
4758
5462
  try {
4759
5463
  const totalMemory = os2.totalmem();
@@ -4805,8 +5509,31 @@ var TaskSystemInfo = class extends TaskMaster {
4805
5509
  }
4806
5510
  };
4807
5511
 
4808
- // src/tasks/coreTasks/TaskSumAB.ts
4809
- var TaskSumAB = class extends TaskMaster {
5512
+ // src/tasks/coreTasks/TaskSumAB.js
5513
+ var TaskSumAB = class extends AbstractTask {
5514
+ /** Short, deterministic — wait by default so callers see the sum. */
5515
+ static defaultWaitForResult = true;
5516
+ /**
5517
+ * @param {object} context
5518
+ * @param {Record<string, unknown>} [overrides]
5519
+ * @returns {Promise<{ a: number, b: number }>}
5520
+ */
5521
+ static async resolveCustomParams(context, overrides = {}) {
5522
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
5523
+ a: "number",
5524
+ b: "number"
5525
+ }, overrides);
5526
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
5527
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
5528
+ }
5529
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
5530
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
5531
+ }
5532
+ return { a: merged.a, b: merged.b };
5533
+ }
5534
+ /**
5535
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5536
+ */
4810
5537
  async run() {
4811
5538
  const a = this.task?.params?.a;
4812
5539
  const b = this.task?.params?.b;
@@ -4837,8 +5564,46 @@ var TaskSumAB = class extends TaskMaster {
4837
5564
  }
4838
5565
  };
4839
5566
 
4840
- // src/tasks/coreTasks/TaskStopRunner.ts
4841
- var TaskStopRunner = class extends TaskMaster {
5567
+ // src/tasks/coreTasks/TaskStopRunner.js
5568
+ var TaskStopRunner = class extends AbstractTask {
5569
+ /**
5570
+ * Stop tasks must target a concrete instance — without `serviceName` the
5571
+ * row would race against any worker on the queue. Layered on top of the
5572
+ * envelope built by {@link AbstractTask.resolveParams}.
5573
+ *
5574
+ * @param {object} context
5575
+ * @param {Record<string, unknown>} [overrides]
5576
+ * @returns {Promise<object>}
5577
+ */
5578
+ static async resolveParams(context, overrides = {}) {
5579
+ const main = await super.resolveParams(context, overrides);
5580
+ if (!main.serviceName) {
5581
+ throw new ParamError(
5582
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
5583
+ );
5584
+ }
5585
+ return main;
5586
+ }
5587
+ /**
5588
+ * @param {object} context
5589
+ * @param {Record<string, unknown>} [overrides]
5590
+ * @returns {Promise<{ allowanceMs: number }>}
5591
+ */
5592
+ static async resolveCustomParams(context, overrides = {}) {
5593
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
5594
+ allowanceMs: "number default 5000"
5595
+ }, overrides);
5596
+ const allowanceMs = Number(merged.allowanceMs);
5597
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
5598
+ throw new ParamError(
5599
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
5600
+ );
5601
+ }
5602
+ return { allowanceMs };
5603
+ }
5604
+ /**
5605
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
5606
+ */
4842
5607
  async run() {
4843
5608
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4844
5609
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -4853,172 +5618,396 @@ var TaskStopRunner = class extends TaskMaster {
4853
5618
  }
4854
5619
  };
4855
5620
 
4856
- // src/tasks/TasksRegistry.ts
5621
+ // src/tasks/coreTasks/TaskGetLogs.js
5622
+ var TaskGetLogs = class extends AbstractTask {
5623
+ /**
5624
+ * @param {object} context
5625
+ * @param {Record<string, unknown>} [overrides]
5626
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
5627
+ */
5628
+ static async resolveCustomParams(context, overrides = {}) {
5629
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
5630
+ source: "string",
5631
+ resource: "string",
5632
+ tail: "number default 100",
5633
+ afterTs: "string"
5634
+ }, overrides);
5635
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
5636
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
5637
+ if (!source) throw new ParamError('getLogs: param "source" is required');
5638
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
5639
+ let tail = Number(merged.tail);
5640
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
5641
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
5642
+ const out = { source, resource, tail };
5643
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
5644
+ out.afterTs = merged.afterTs.trim();
5645
+ }
5646
+ return out;
5647
+ }
5648
+ /**
5649
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
5650
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5651
+ */
5652
+ async run(_reportProgress) {
5653
+ const p = this.task.params ?? {};
5654
+ const source = String(p.source ?? "").trim();
5655
+ const resource = String(p.resource ?? "").trim();
5656
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
5657
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
5658
+ if (!source || !resource) {
5659
+ return {
5660
+ success: false,
5661
+ results: { error: 'getLogs requires params "source" and "resource"' }
5662
+ };
5663
+ }
5664
+ try {
5665
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
5666
+ source,
5667
+ resource,
5668
+ tail,
5669
+ afterTs
5670
+ });
5671
+ return {
5672
+ success: true,
5673
+ results: { records, latestTs, source, resource }
5674
+ };
5675
+ } catch (e) {
5676
+ return {
5677
+ success: false,
5678
+ results: { error: e?.message ?? String(e) }
5679
+ };
5680
+ }
5681
+ }
5682
+ };
5683
+
5684
+ // src/tasks/TasksRegistry.js
4857
5685
  var TasksRegistry = class _TasksRegistry {
4858
- map = {};
5686
+ /**
5687
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
5688
+ */
4859
5689
  constructor(initial) {
5690
+ this.map = {};
4860
5691
  if (initial) {
4861
5692
  this.addMany(initial);
4862
5693
  }
4863
5694
  }
5695
+ /**
5696
+ * Build a registry pre-populated with every core task plus legacy aliases.
5697
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
5698
+ *
5699
+ * @returns {TasksRegistry}
5700
+ */
4864
5701
  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);
5702
+ 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
5703
  }
5704
+ /**
5705
+ * Register a single task class under a name. Overwrites any previous entry.
5706
+ *
5707
+ * @param {string} taskName
5708
+ * @param {Function} taskClass Subclass of `AbstractTask`.
5709
+ * @returns {this}
5710
+ */
4867
5711
  add(taskName, taskClass) {
4868
5712
  this.map[taskName] = taskClass;
4869
5713
  return this;
4870
5714
  }
5715
+ /**
5716
+ * Bulk-register a name → class map. Later calls override earlier ones.
5717
+ *
5718
+ * @param {Record<string, Function>} entries
5719
+ * @returns {this}
5720
+ */
4871
5721
  addMany(entries) {
4872
5722
  for (const [name, klass] of Object.entries(entries)) {
4873
5723
  this.add(name, klass);
4874
5724
  }
4875
5725
  return this;
4876
5726
  }
5727
+ /**
5728
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
5729
+ * the runner treats that as "some other worker may handle this" and skips.
5730
+ *
5731
+ * @param {string} taskName
5732
+ * @returns {Function | undefined}
5733
+ */
4877
5734
  get(taskName) {
4878
5735
  return this.map[taskName];
4879
5736
  }
5737
+ /**
5738
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
5739
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
5740
+ * paths where an unknown name is a hard CLI/programmer error.
5741
+ *
5742
+ * @param {string} taskName
5743
+ * @returns {Function}
5744
+ */
5745
+ requireClass(taskName) {
5746
+ const TaskClass = taskName ? this.map[taskName] : void 0;
5747
+ if (!TaskClass) {
5748
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
5749
+ throw new ParamError(
5750
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
5751
+ );
5752
+ }
5753
+ return TaskClass;
5754
+ }
5755
+ /**
5756
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
5757
+ * from `overrides` or `context.params`, look up the class, and delegate
5758
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
5759
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
5760
+ *
5761
+ * Validation failures (unknown task, missing required custom params, etc.)
5762
+ * surface as {@link ParamError} so the caller aborts cleanly before any
5763
+ * row is inserted.
5764
+ *
5765
+ * @param {object} context
5766
+ * @param {Record<string, unknown>} [overrides]
5767
+ * @returns {Promise<object>}
5768
+ */
5769
+ async resolveTaskParams(context, overrides = {}) {
5770
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
5771
+ const fromCli = context.params.get("name", "string");
5772
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
5773
+ const name = overrideName || cliName;
5774
+ if (!name) {
5775
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5776
+ }
5777
+ const TaskClass = this.requireClass(name);
5778
+ return TaskClass.resolveParams(context, { ...overrides, name });
5779
+ }
5780
+ /**
5781
+ * Names of every registered task, sorted alphabetically (useful for CLI output
5782
+ * and allowlist sanity checks).
5783
+ *
5784
+ * @returns {string[]}
5785
+ */
4880
5786
  listSupportedTasks() {
4881
5787
  return Object.keys(this.map).sort();
4882
5788
  }
5789
+ /**
5790
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
5791
+ * or for serialization.
5792
+ *
5793
+ * @returns {Record<string, Function>}
5794
+ */
4883
5795
  toObject() {
4884
5796
  return { ...this.map };
4885
5797
  }
4886
5798
  };
4887
5799
 
4888
- // src/tasks/taskScriptRunner.ts
5800
+ // src/tasks/serviceTaskAllowlist.js
5801
+ var SERVICE_TASK_NAMES = [
5802
+ "ping",
5803
+ "stop",
5804
+ "stopRunner",
5805
+ "shellCommand",
5806
+ "systemInfo",
5807
+ "info",
5808
+ "getLogs"
5809
+ ];
5810
+ function normalizeAllowedTasks(value) {
5811
+ if (!value) return void 0;
5812
+ if (Array.isArray(value)) {
5813
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5814
+ return out2.length ? out2 : void 0;
5815
+ }
5816
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5817
+ return out.length ? out : void 0;
5818
+ }
5819
+ function mergeAllowedTasksWithServiceTasks(names) {
5820
+ const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
5821
+ return Array.from(set).sort();
5822
+ }
5823
+
5824
+ // src/tasks/taskScriptRunner.js
4889
5825
  import { spawn as spawn2 } from "child_process";
5826
+ var MAX_PROGRESS_TEXT_LEN = 4e3;
4890
5827
  function toCliArgs(args = []) {
4891
5828
  return args.filter((a) => typeof a === "string" && a.length > 0);
4892
5829
  }
4893
5830
  function formatChildLogPrefix(task) {
4894
- return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
5831
+ return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
4895
5832
  }
4896
- async function runNodeTaskScript(context, options) {
4897
- const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
5833
+ function isProgressPayload(payload) {
5834
+ if (!payload || typeof payload !== "object") return false;
5835
+ if (payload.level !== "progress") return false;
5836
+ const count = Number(payload.count);
5837
+ const total = Number(payload.total);
5838
+ return Number.isFinite(count) && Number.isFinite(total) && total > 0;
5839
+ }
5840
+ function formatProgressText(payload, fallbackPrefix) {
5841
+ const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
5842
+ const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
5843
+ return `${pfx}${label}${payload.count}/${payload.total}`;
5844
+ }
5845
+ function forwardChildLogToParent(context, prefix, message) {
5846
+ if (!message || typeof message !== "object") return;
5847
+ const text = typeof message.message === "string" ? message.message : null;
5848
+ if (!text) return;
5849
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
5850
+ const line = `[child:${prefix}] ${text}`;
5851
+ const logger = context.logger;
5852
+ switch (level) {
5853
+ case "error":
5854
+ case "fatal":
5855
+ logger.error?.(line);
5856
+ return;
5857
+ case "warn":
5858
+ case "warning":
5859
+ logger.warn?.(line);
5860
+ return;
5861
+ case "debug":
5862
+ logger.debug?.(line);
5863
+ return;
5864
+ case "info":
5865
+ default:
5866
+ logger.info?.(line);
5867
+ }
5868
+ }
5869
+ function buildNodeArgs(scriptPath, cliArgs) {
4898
5870
  const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
4899
5871
  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
- }
5872
+ return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
5873
+ }
5874
+ function resolveTasksTableName(context) {
5875
+ return context.tasksQueueName || context.params?.get?.("table") || "tasks";
5876
+ }
5877
+ function announceIpcFileLogsTarget(context, options) {
5878
+ if (!options.ipcFileLogs) return;
5879
+ const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
5880
+ const enabledRaw = context.params?.get?.("tasksLogsEnabled");
5881
+ const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
5882
+ context.logger.info?.(
5883
+ `[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
4914
5884
  );
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
- );
5885
+ }
5886
+ function createSerializedQueue() {
5887
+ let chain = Promise.resolve();
5888
+ return {
5889
+ push(fn) {
5890
+ chain = chain.then(fn, () => {
5891
+ }).catch(() => {
5892
+ });
5893
+ return chain;
5894
+ },
5895
+ drain() {
5896
+ return chain.catch(() => {
4940
5897
  });
4941
5898
  }
4942
5899
  };
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 "";
5900
+ }
5901
+ async function runNodeTaskScript(context, options) {
5902
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
5903
+ const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
5904
+ const child = spawn2(process.execPath, nodeArgs, {
5905
+ cwd: options.cwd || process.cwd(),
5906
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
5907
+ env: {
5908
+ ...process.env,
5909
+ TASK_ID: options.task.id,
5910
+ TASK_NAME: options.task.name,
5911
+ TASK_OPID: options.task.opid || ""
5912
+ }
5913
+ });
5914
+ announceIpcFileLogsTarget(context, options);
5915
+ const prefix = formatChildLogPrefix(options.task);
5916
+ const tasksTable = resolveTasksTableName(context);
5917
+ const progressQueue = createSerializedQueue();
5918
+ const state = {
5919
+ stdout: "",
5920
+ stderr: "",
5921
+ workerResult: null,
5922
+ hadErrorMessage: false
5923
+ };
5924
+ const writeProgress = (text) => {
5925
+ const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
5926
+ if (!trimmed) return;
5927
+ progressQueue.push(async () => {
5928
+ const db = context.db;
5929
+ if (db) {
5930
+ try {
5931
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
5932
+ } catch (error) {
5933
+ context.logger.warn?.(
5934
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
5935
+ );
5936
+ }
5937
+ }
5938
+ if (options.onProgress) {
5939
+ try {
5940
+ await options.onProgress(trimmed);
5941
+ } catch (error) {
5942
+ context.logger.warn?.(
5943
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
5944
+ );
5945
+ }
5946
+ }
5947
+ });
4956
5948
  };
4957
- child.stdout.on("data", (chunk) => {
5949
+ child.stdout?.on("data", (chunk) => {
4958
5950
  const text = String(chunk);
4959
- stdout += text;
5951
+ state.stdout += text;
4960
5952
  if (text.trim()) {
4961
5953
  context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
4962
- updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
4963
5954
  }
4964
5955
  });
4965
- child.stderr.on("data", (chunk) => {
5956
+ child.stderr?.on("data", (chunk) => {
4966
5957
  const text = String(chunk);
4967
- stderr += text;
5958
+ state.stderr += text;
4968
5959
  if (text.trim()) {
4969
5960
  context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
4970
5961
  }
4971
5962
  });
4972
5963
  child.on("message", (message) => {
4973
5964
  if (message && typeof message === "object" && "__taskWorkerResult" in message) {
4974
- workerResult = message.__taskWorkerResult;
5965
+ state.workerResult = message.__taskWorkerResult;
4975
5966
  return;
4976
5967
  }
4977
5968
  if (message && typeof message === "object") {
4978
5969
  const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
4979
5970
  if (level === "error" || level === "fatal") {
4980
- hadErrorMessage = true;
5971
+ state.hadErrorMessage = true;
4981
5972
  }
4982
5973
  }
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
- }
5974
+ appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
5975
+ try {
5976
+ options.onChildIpcMessage?.(message);
5977
+ } catch (e) {
5978
+ context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
5002
5979
  }
5980
+ if (isProgressPayload(message)) {
5981
+ context.logger.progress(message.message || "progress", {
5982
+ prefix: message.prefix || prefix,
5983
+ count: Number(message.count),
5984
+ total: Number(message.total)
5985
+ });
5986
+ writeProgress(formatProgressText(message, prefix));
5987
+ return;
5988
+ }
5989
+ forwardChildLogToParent(context, prefix, message);
5003
5990
  });
5004
5991
  return await new Promise((resolve2, reject) => {
5005
5992
  child.on("error", (error) => reject(error));
5006
5993
  child.on("close", (exitCode, signal) => {
5007
- Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
5994
+ void (async () => {
5995
+ await flushTaskIpcLogs(context);
5996
+ await progressQueue.drain();
5008
5997
  resolve2({
5009
5998
  exitCode,
5010
5999
  signal,
5011
- stdout: stdout.trim(),
5012
- stderr: stderr.trim(),
5013
- workerResult,
5014
- hadErrorMessage
6000
+ stdout: state.stdout.trim(),
6001
+ stderr: state.stderr.trim(),
6002
+ workerResult: state.workerResult,
6003
+ hadErrorMessage: state.hadErrorMessage
5015
6004
  });
5016
- });
6005
+ })();
5017
6006
  });
5018
6007
  });
5019
6008
  }
5020
6009
 
5021
- // src/tasks/index.ts
6010
+ // src/tasks/index.js
5022
6011
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
5023
6012
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
5024
6013
  function getDb3(context) {
@@ -5033,22 +6022,13 @@ function normalizeRegistry(registry) {
5033
6022
  if (registry instanceof TasksRegistry) return registry;
5034
6023
  return new TasksRegistry().addMany(registry);
5035
6024
  }
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) {
6025
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
5046
6026
  return enqueueTask(context, {
5047
- queue,
5048
- target,
5049
- task: "stopRunner",
6027
+ queueName,
6028
+ name: "stopRunner",
5050
6029
  params: { allowanceMs },
5051
- priority: 1e6
6030
+ priority: 0,
6031
+ serviceGroup
5052
6032
  });
5053
6033
  }
5054
6034
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
@@ -5066,18 +6046,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
5066
6046
  }
5067
6047
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
5068
6048
  const db = getDb3(context);
5069
- const taskName = row.task;
6049
+ const taskName = row.name;
5070
6050
  const TaskClass = registry.get(taskName);
5071
- const { paused_at: _pausedAt, ...rowForHistory } = row;
5072
6051
  if (!TaskClass) {
5073
6052
  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
- });
6053
+ await db(historyTable).insert(
6054
+ taskHistoryInsertFromQueueRow(row, {
6055
+ completed_at: /* @__PURE__ */ new Date(),
6056
+ success: false,
6057
+ status: "failed",
6058
+ status_changed_at: db.fn.now(),
6059
+ params: toJsonColumn(row.params),
6060
+ results: toJsonColumn(err)
6061
+ })
6062
+ );
5081
6063
  if (row.schedule) {
5082
6064
  await db(tasksTable).where({ id: row.id }).update({
5083
6065
  started_at: null,
@@ -5085,7 +6067,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5085
6067
  success: false,
5086
6068
  results: toJsonColumn(err),
5087
6069
  past_due: null,
5088
- paused_at: db.fn.now(),
6070
+ status: "paused",
6071
+ status_changed_at: db.fn.now(),
5089
6072
  progress: LOCKED_BY_ERROR_MESSAGE
5090
6073
  });
5091
6074
  } else {
@@ -5112,13 +6095,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5112
6095
  } finally {
5113
6096
  runningTaskInstances.delete(row.id);
5114
6097
  }
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
- });
6098
+ await db(historyTable).insert(
6099
+ taskHistoryInsertFromQueueRow(row, {
6100
+ completed_at: /* @__PURE__ */ new Date(),
6101
+ success,
6102
+ status: success ? "completed" : "failed",
6103
+ status_changed_at: db.fn.now(),
6104
+ params: toJsonColumn(row.params),
6105
+ results: toJsonColumn(results)
6106
+ })
6107
+ );
5122
6108
  if (!success) {
5123
6109
  const dbName = String(context?.params?.get?.("dbName") || "local");
5124
6110
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -5133,19 +6119,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5133
6119
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
5134
6120
  appendTaskIpcLog(context, row, {
5135
6121
  level: "error",
5136
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
6122
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
5137
6123
  details: results
5138
6124
  });
5139
6125
  }
5140
6126
  if (row.schedule) {
5141
6127
  if (success) {
6128
+ let nextRunAt = null;
6129
+ try {
6130
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
6131
+ } catch (e) {
6132
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
6133
+ }
5142
6134
  await db(tasksTable).where({ id: row.id }).update({
5143
6135
  started_at: null,
5144
6136
  completed_at: /* @__PURE__ */ new Date(),
5145
6137
  success,
5146
6138
  results: toJsonColumn(results),
5147
6139
  progress: null,
5148
- past_due: null
6140
+ past_due: null,
6141
+ status: "idle",
6142
+ status_changed_at: db.fn.now(),
6143
+ next_run_at: nextRunAt,
6144
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
6145
+ service_name: null,
6146
+ server_name: null,
6147
+ instance_number: null
5149
6148
  });
5150
6149
  } else {
5151
6150
  await db(tasksTable).where({ id: row.id }).update({
@@ -5153,7 +6152,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5153
6152
  completed_at: /* @__PURE__ */ new Date(),
5154
6153
  success,
5155
6154
  results: toJsonColumn(results),
5156
- paused_at: db.fn.now(),
6155
+ status: "paused",
6156
+ status_changed_at: db.fn.now(),
5157
6157
  progress: LOCKED_BY_ERROR_MESSAGE,
5158
6158
  past_due: null
5159
6159
  });
@@ -5165,58 +6165,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5165
6165
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
5166
6166
  return { stopRunnerRequested, stopAllowanceMs };
5167
6167
  }
5168
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
6168
+ function shuffleTaskRowsInPlace(rows) {
6169
+ for (let i = rows.length - 1; i > 0; i--) {
6170
+ const j = Math.floor(Math.random() * (i + 1));
6171
+ const t = rows[i];
6172
+ rows[i] = rows[j];
6173
+ rows[j] = t;
6174
+ }
6175
+ }
6176
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
5169
6177
  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);
6178
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
6179
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
6180
+ }).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
6181
  if (taskNames && taskNames.length > 0) {
5172
- query = query.whereIn("task", taskNames);
6182
+ query = query.whereIn("name", taskNames);
6183
+ }
6184
+ if (runnerIdentity) {
6185
+ query = query.where(function() {
6186
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
6187
+ }).where(function() {
6188
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
6189
+ }).where(function() {
6190
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
6191
+ });
6192
+ } else {
6193
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
5173
6194
  }
5174
6195
  const candidates = await query;
6196
+ shuffleTaskRowsInPlace(candidates);
5175
6197
  for (const row of candidates) {
5176
6198
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
5177
6199
  continue;
5178
6200
  }
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;
6201
+ const TaskClass = registry.get(row.name);
6202
+ if (!TaskClass) {
6203
+ continue;
6204
+ }
6205
+ const taskInstance = new TaskClass(context, row);
6206
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
6207
+ if (reason) {
6208
+ if (!row.past_due) {
6209
+ await db(tasksTable).where({ id: row.id }).update({
6210
+ past_due: db.fn.now(),
6211
+ progress: String(reason)
6212
+ });
5191
6213
  }
6214
+ continue;
6215
+ }
6216
+ const claimPatch = {
6217
+ started_at: db.fn.now(),
6218
+ status: "running",
6219
+ status_changed_at: db.fn.now()
6220
+ };
6221
+ if (runnerIdentity) {
6222
+ claimPatch.service_name = runnerIdentity.service_name;
6223
+ claimPatch.server_name = runnerIdentity.server_name;
6224
+ claimPatch.instance_number = runnerIdentity.instance_number;
5192
6225
  }
5193
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
6226
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
5194
6227
  const claimed = Array.isArray(updated) ? updated[0] : null;
5195
6228
  if (claimed) return claimed;
5196
6229
  }
5197
6230
  return null;
5198
6231
  }
5199
6232
  async function runTasksLoop(context, options) {
5200
- const queue = options.queue ?? "tasks";
6233
+ const queueName = options.queueName ?? "tasks";
5201
6234
  const target = options.target;
5202
6235
  const pollMs = options.pollMs ?? 1e3;
5203
- const maxParallel = options.maxParallel ?? 1;
6236
+ const claimJitterMs = options.claimJitterMs ?? 0;
6237
+ const maxParallel = options.maxParallel ?? 32;
5204
6238
  const scanLimit = options.scanLimit ?? 100;
5205
6239
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5206
6240
  const registry = normalizeRegistry(options.registry);
5207
- const { tasksTable, historyTable } = queueToTableNames(queue);
6241
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5208
6242
  if (!target) throw new Error("runTasksLoop: target is required");
6243
+ context.tasksQueueName = queueName;
5209
6244
  const runningPromises = /* @__PURE__ */ new Set();
5210
6245
  const runningTaskInstances = /* @__PURE__ */ new Map();
5211
6246
  let runningStopControlPromise = null;
5212
6247
  let stopRequested = false;
5213
6248
  let stopAllowanceMs = 5e3;
5214
- context.__tasksRunnerStop = false;
6249
+ context.tasksRunnerStop = false;
5215
6250
  let registryReg = null;
5216
6251
  let registryInterval = null;
6252
+ let runnerIdentity = null;
5217
6253
  const hbGroup = options.runnerServiceGroup?.trim();
5218
6254
  if (hbGroup) {
5219
- const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
5220
6255
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
5221
6256
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
5222
6257
  const defaultMeta = {
@@ -5224,16 +6259,21 @@ async function runTasksLoop(context, options) {
5224
6259
  allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
5225
6260
  };
5226
6261
  registryReg = await registerInServicesRegistry(context, {
5227
- queue,
6262
+ queueName,
5228
6263
  target,
5229
6264
  serviceGroup: hbGroup,
5230
6265
  serviceName: options.runnerServiceName,
5231
- identityDir,
6266
+ instanceNumber: options.runnerInstanceNumber,
5232
6267
  staleMs,
5233
6268
  groupMaxInstances: options.runnerGroupMaxInstances,
5234
6269
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
5235
6270
  metadata: options.runnerMetadata ?? defaultMeta
5236
6271
  });
6272
+ runnerIdentity = {
6273
+ service_name: registryReg.serviceName,
6274
+ server_name: os3.hostname(),
6275
+ instance_number: registryReg.instanceNumber
6276
+ };
5237
6277
  registryInterval = setInterval(() => {
5238
6278
  void touchServicesRegistry(context, registryReg).catch((err) => {
5239
6279
  context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
@@ -5241,7 +6281,7 @@ async function runTasksLoop(context, options) {
5241
6281
  }, hbIntervalMs);
5242
6282
  }
5243
6283
  try {
5244
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
6284
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
5245
6285
  if (!runningStopControlPromise) {
5246
6286
  const claimedStopTask = await claimNextRunnableTask(
5247
6287
  context,
@@ -5249,7 +6289,8 @@ async function runTasksLoop(context, options) {
5249
6289
  target,
5250
6290
  registry,
5251
6291
  10,
5252
- ["stopRunner", "stop"]
6292
+ ["stopRunner", "stop"],
6293
+ runnerIdentity
5253
6294
  );
5254
6295
  if (claimedStopTask) {
5255
6296
  runningStopControlPromise = executeClaimedTask(
@@ -5263,7 +6304,7 @@ async function runTasksLoop(context, options) {
5263
6304
  if (outcome.stopRunnerRequested && !stopRequested) {
5264
6305
  stopRequested = true;
5265
6306
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5266
- context.__tasksRunnerStop = true;
6307
+ context.tasksRunnerStop = true;
5267
6308
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5268
6309
  }
5269
6310
  }).finally(() => {
@@ -5271,6 +6312,9 @@ async function runTasksLoop(context, options) {
5271
6312
  });
5272
6313
  }
5273
6314
  }
6315
+ if (claimJitterMs > 0) {
6316
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
6317
+ }
5274
6318
  while (runningPromises.size < maxParallel) {
5275
6319
  const claimed = await claimNextRunnableTask(
5276
6320
  context,
@@ -5278,14 +6322,15 @@ async function runTasksLoop(context, options) {
5278
6322
  target,
5279
6323
  registry,
5280
6324
  scanLimit,
5281
- allowedTasks
6325
+ allowedTasks,
6326
+ runnerIdentity
5282
6327
  );
5283
6328
  if (!claimed) break;
5284
6329
  const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
5285
6330
  if (outcome.stopRunnerRequested && !stopRequested) {
5286
6331
  stopRequested = true;
5287
6332
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5288
- context.__tasksRunnerStop = true;
6333
+ context.tasksRunnerStop = true;
5289
6334
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5290
6335
  }
5291
6336
  }).finally(() => {
@@ -5293,7 +6338,16 @@ async function runTasksLoop(context, options) {
5293
6338
  });
5294
6339
  runningPromises.add(p);
5295
6340
  }
5296
- await sleepMs(pollMs);
6341
+ const wakePromises = [...runningPromises];
6342
+ if (runningStopControlPromise) {
6343
+ wakePromises.push(runningStopControlPromise);
6344
+ }
6345
+ if (wakePromises.length === 0) {
6346
+ await sleepMs(pollMs);
6347
+ } else {
6348
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
6349
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
6350
+ }
5297
6351
  }
5298
6352
  if (context.isStop() && !stopRequested) {
5299
6353
  await signalRunningTasksStop(context, runningTaskInstances, 5e3);
@@ -5329,89 +6383,130 @@ async function runTasksLoop(context, options) {
5329
6383
  }
5330
6384
  async function waitForTaskResult(context, taskId, options = {}) {
5331
6385
  const db = getDb3(context);
5332
- const queue = options.queue ?? "tasks";
6386
+ const queueName = options.queueName ?? "tasks";
5333
6387
  const timeoutMs = options.timeoutMs ?? 6e4;
5334
6388
  const pollMs = options.pollMs ?? 500;
5335
- const { tasksTable, historyTable } = queueToTableNames(queue);
6389
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5336
6390
  const deadline = Date.now() + timeoutMs;
6391
+ const waitStartedAt = /* @__PURE__ */ new Date();
6392
+ let cachedNameOpid = null;
6393
+ async function historySinceWait(name, opid) {
6394
+ let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
6395
+ if (opid == null || opid === "") {
6396
+ q = q.whereNull("opid");
6397
+ } else {
6398
+ q = q.where({ opid });
6399
+ }
6400
+ return await q.orderBy("completed_at", "desc").first();
6401
+ }
5337
6402
  while (Date.now() <= deadline) {
5338
- const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5339
- if (done) return done;
6403
+ const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
6404
+ if (legacy) {
6405
+ return legacy;
6406
+ }
5340
6407
  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;
6408
+ if (pending) {
6409
+ cachedNameOpid = { name: pending.name, opid: pending.opid };
6410
+ const done = await historySinceWait(pending.name, pending.opid);
6411
+ if (done) {
6412
+ return done;
6413
+ }
6414
+ } else if (cachedNameOpid) {
6415
+ const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
6416
+ if (done) {
6417
+ return done;
6418
+ }
6419
+ return null;
6420
+ } else {
6421
+ return null;
5344
6422
  }
5345
6423
  await sleepMs(pollMs);
5346
6424
  }
5347
6425
  return null;
5348
6426
  }
5349
6427
  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;
6428
+ /**
6429
+ * @param {object} context
6430
+ * @param {{
6431
+ * queueName?: string,
6432
+ * target?: string,
6433
+ * recreateTaskTables?: boolean,
6434
+ * pollMs?: number,
6435
+ * claimJitterMs?: number,
6436
+ * maxParallel?: number,
6437
+ * scanLimit?: number,
6438
+ * allowedTasks?: string | string[],
6439
+ * registry?: TasksRegistry | Record<string, Function>,
6440
+ * runnerServiceGroup?: string,
6441
+ * runnerServiceName?: string,
6442
+ * runnerInstanceNumber?: number,
6443
+ * runnerHeartbeatIntervalMs?: number,
6444
+ * runnerHeartbeatStaleMs?: number,
6445
+ * runnerGroupMaxInstances?: number,
6446
+ * runnerEnforceMaxInstances?: boolean,
6447
+ * runnerMetadata?: Record<string, unknown>,
6448
+ * }} [options]
6449
+ */
5367
6450
  constructor(context, options = {}) {
5368
6451
  this.context = context;
5369
- this.queue = options.queue ?? "tasks";
6452
+ this.queueName = options.queueName ?? "tasks";
5370
6453
  this.target = options.target ?? "localRunner";
5371
6454
  this.recreateTaskTables = options.recreateTaskTables ?? false;
5372
6455
  this.pollMs = options.pollMs ?? 1e3;
6456
+ this.claimJitterMs = options.claimJitterMs ?? 0;
5373
6457
  this.maxParallel = options.maxParallel ?? 1;
5374
6458
  this.scanLimit = options.scanLimit ?? 100;
5375
6459
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5376
6460
  this.registry = normalizeRegistry(options.registry);
5377
6461
  this.runnerServiceGroup = options.runnerServiceGroup;
5378
6462
  this.runnerServiceName = options.runnerServiceName;
5379
- this.runnerIdentityDir = options.runnerIdentityDir;
6463
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
5380
6464
  this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
5381
6465
  this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
5382
6466
  this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
5383
6467
  this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
5384
6468
  this.runnerMetadata = options.runnerMetadata;
5385
6469
  }
6470
+ /**
6471
+ * Preferred factory: reads defaults from `context.params` (module namespace
6472
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
6473
+ * and inline options in one consistent resolver.
6474
+ *
6475
+ * @param {object} context
6476
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
6477
+ * @returns {TasksManager}
6478
+ */
5386
6479
  static init(context, options = {}) {
5387
6480
  const defs = {
5388
6481
  table: "string default tasks",
5389
6482
  target: "string default localRunner",
5390
6483
  recreateTaskTables: "boolean default false",
5391
6484
  pollMs: "number default 1000",
6485
+ claimJitterMs: "number default 0",
5392
6486
  maxParallel: "number default 1",
5393
6487
  scanLimit: "number default 100",
5394
6488
  allowedTasks: "string",
5395
6489
  runnerServiceGroup: "string",
5396
6490
  runnerServiceName: "string",
5397
- runnerIdentityDir: "string default ./data/runner-identities",
6491
+ runnerInstanceNumber: "number",
5398
6492
  runnerHeartbeatIntervalMs: "number default 10000",
5399
6493
  runnerHeartbeatStaleMs: "number default 45000",
5400
6494
  runnerGroupMaxInstances: "number",
5401
6495
  runnerEnforceMaxInstances: "boolean default true"
5402
6496
  };
5403
- const discovered = context.params.getAllForModule(defs);
6497
+ const discovered = context.params.getAllForModule("tasks", defs);
5404
6498
  const resolved = {
5405
- queue: discovered.table,
6499
+ queueName: discovered.table,
5406
6500
  target: discovered.target,
5407
6501
  recreateTaskTables: discovered.recreateTaskTables,
5408
6502
  pollMs: discovered.pollMs,
6503
+ claimJitterMs: discovered.claimJitterMs,
5409
6504
  maxParallel: discovered.maxParallel,
5410
6505
  scanLimit: discovered.scanLimit,
5411
6506
  allowedTasks: discovered.allowedTasks,
5412
6507
  runnerServiceGroup: discovered.runnerServiceGroup,
5413
6508
  runnerServiceName: discovered.runnerServiceName,
5414
- runnerIdentityDir: discovered.runnerIdentityDir,
6509
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
5415
6510
  runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
5416
6511
  runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
5417
6512
  runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
@@ -5420,24 +6515,39 @@ var TasksManager = class _TasksManager {
5420
6515
  };
5421
6516
  return new _TasksManager(context, resolved);
5422
6517
  }
6518
+ /**
6519
+ * Idempotently ensure the three backing tables exist for this queue.
6520
+ *
6521
+ * @param {{ recreate?: boolean }} [options]
6522
+ * @returns {Promise<void>}
6523
+ */
5423
6524
  async ensureTaskTables(options = {}) {
5424
6525
  await ensureTaskTables(this.context, {
5425
- queue: this.queue,
6526
+ queueName: this.queueName,
5426
6527
  recreate: options.recreate ?? this.recreateTaskTables
5427
6528
  });
5428
6529
  }
6530
+ /**
6531
+ * Start the runner loop using this manager's resolved config. Per-call
6532
+ * options override the stored defaults, but `runnerMetadata` still falls
6533
+ * through when omitted.
6534
+ *
6535
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
6536
+ * @returns {Promise<void>}
6537
+ */
5429
6538
  async runTasksLoop(options = {}) {
5430
6539
  await runTasksLoop(this.context, {
5431
- queue: options.queue ?? this.queue,
6540
+ queueName: options.queueName ?? this.queueName,
5432
6541
  target: options.target ?? this.target,
5433
6542
  pollMs: options.pollMs ?? this.pollMs,
6543
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
5434
6544
  maxParallel: options.maxParallel ?? this.maxParallel,
5435
6545
  scanLimit: options.scanLimit ?? this.scanLimit,
5436
6546
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
5437
6547
  registry: options.registry ?? this.registry,
5438
6548
  runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
5439
6549
  runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
5440
- runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
6550
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
5441
6551
  runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
5442
6552
  runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
5443
6553
  runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
@@ -5447,8 +6557,9 @@ var TasksManager = class _TasksManager {
5447
6557
  }
5448
6558
  };
5449
6559
  export {
6560
+ AbstractTask,
5450
6561
  Args,
5451
- Box5 as Box,
6562
+ Box4 as Box,
5452
6563
  Db,
5453
6564
  Divider,
5454
6565
  FileDatabase,
@@ -5461,14 +6572,16 @@ export {
5461
6572
  MultiColumnListComponent,
5462
6573
  MultiColumnListWithPreviewComponent,
5463
6574
  Params,
5464
- React5 as React,
6575
+ React2 as React,
6576
+ S3,
6577
+ SERVICE_TASK_NAMES,
5465
6578
  ScreenBody,
5466
6579
  ScreenContainer,
5467
6580
  ScreenDivider,
5468
6581
  ScreenFooter,
5469
6582
  ScreenRow,
5470
6583
  ScreenTitle,
5471
- TaskMaster,
6584
+ TaskGetLogs,
5472
6585
  TaskPing,
5473
6586
  TaskSampleProcess,
5474
6587
  TaskShellCommand,
@@ -5483,17 +6596,17 @@ export {
5483
6596
  buildBreadcrumb,
5484
6597
  buildDetailBreadcrumb,
5485
6598
  buildFooter,
5486
- dbConnect,
5487
- dbFindAndConnect,
5488
- dbInit,
6599
+ convertPattern,
5489
6600
  defaultFileSynopsisFunction,
5490
6601
  defaultTasksRegistry,
5491
6602
  defaultVersionSynopsisFunction,
5492
6603
  enqueueStopTask,
5493
6604
  enqueueTask,
5494
6605
  ensureTaskTables,
6606
+ flushTaskIpcLogs,
5495
6607
  getArgsInstance,
5496
6608
  createElement2 as h,
6609
+ ipcFileLogsTableNameForSourceResource,
5497
6610
  joiEdateType,
5498
6611
  joiStringArrayType,
5499
6612
  listServicesRegistry as listAliveRunnerHeartbeats,
@@ -5501,14 +6614,22 @@ export {
5501
6614
  listSources,
5502
6615
  listTables,
5503
6616
  load,
6617
+ matchesParsedPattern,
6618
+ memo,
6619
+ mergeAllowedTasksWithServiceTasks,
6620
+ nextTimeMatch,
6621
+ normalizeAllowedTasks,
5504
6622
  organizeFooterMessages,
5505
6623
  queueToTableNames,
6624
+ readTaskIpcLogsSnapshot,
5506
6625
  registerInServicesRegistry,
5507
6626
  registerInServicesRegistry as registerRunnerHeartbeat,
6627
+ resolveAsterisks,
6628
+ resolveIpcFileLogsDir,
6629
+ resolveRanges,
6630
+ resolveSteps,
5508
6631
  runNodeTaskScript,
5509
6632
  runTasksLoop,
5510
- servicesRegistryTable as runnerHeartbeatsTable,
5511
- servicesRegistryTable,
5512
6633
  setupContext,
5513
6634
  showListScreen,
5514
6635
  showMenuScreen,
@@ -5516,6 +6637,8 @@ export {
5516
6637
  showMultiColumnListWithPreviewScreen,
5517
6638
  showScreen,
5518
6639
  showWordGridScreen,
6640
+ taskHistoryInsertFromQueueRow,
6641
+ timeMatcher,
5519
6642
  touchServicesRegistry as touchRunnerHeartbeat,
5520
6643
  touchServicesRegistry,
5521
6644
  unregisterServicesRegistry as unregisterRunnerHeartbeat,
@@ -5523,10 +6646,11 @@ export {
5523
6646
  updateServicesRegistryMetadata,
5524
6647
  updateTaskProgress,
5525
6648
  useCallback,
5526
- useEffect3 as useEffect,
6649
+ useEffect2 as useEffect,
5527
6650
  useInput2 as useInput,
6651
+ useLayoutEffect,
5528
6652
  useMemo,
5529
- useRef3 as useRef,
6653
+ useRef2 as useRef,
5530
6654
  useState3 as useState,
5531
6655
  waitForTaskResult
5532
6656
  };