@nmakarov/cli-toolkit 0.18.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +9 -0
  2. package/dist/args.cjs +1 -4
  3. package/dist/args.cjs.map +1 -1
  4. package/dist/args.js +1 -1
  5. package/dist/args.js.map +1 -1
  6. package/dist/cli-runner.cjs +1493 -516
  7. package/dist/cli-runner.cjs.map +1 -1
  8. package/dist/cli-runner.js +1509 -531
  9. package/dist/cli-runner.js.map +1 -1
  10. package/dist/db.cjs +85 -157
  11. package/dist/db.cjs.map +1 -1
  12. package/dist/db.js +84 -150
  13. package/dist/db.js.map +1 -1
  14. package/dist/errors.cjs +2 -2
  15. package/dist/errors.cjs.map +1 -1
  16. package/dist/errors.js +2 -1
  17. package/dist/errors.js.map +1 -1
  18. package/dist/filedatabase.cjs +19 -19
  19. package/dist/filedatabase.cjs.map +1 -1
  20. package/dist/filedatabase.js +19 -16
  21. package/dist/filedatabase.js.map +1 -1
  22. package/dist/http-client.cjs +9 -11
  23. package/dist/http-client.cjs.map +1 -1
  24. package/dist/http-client.js +10 -9
  25. package/dist/http-client.js.map +1 -1
  26. package/dist/http-client2.cjs +34 -33
  27. package/dist/http-client2.cjs.map +1 -1
  28. package/dist/http-client2.js +34 -30
  29. package/dist/http-client2.js.map +1 -1
  30. package/dist/index.cjs +2063 -658
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +2063 -663
  33. package/dist/index.js.map +1 -1
  34. package/dist/init.cjs +97 -69
  35. package/dist/init.cjs.map +1 -1
  36. package/dist/init.js +112 -83
  37. package/dist/init.js.map +1 -1
  38. package/dist/logger.cjs +5 -5
  39. package/dist/logger.cjs.map +1 -1
  40. package/dist/logger.js +5 -4
  41. package/dist/logger.js.map +1 -1
  42. package/dist/mock-server.cjs +21 -33
  43. package/dist/mock-server.cjs.map +1 -1
  44. package/dist/mock-server.js +21 -28
  45. package/dist/mock-server.js.map +1 -1
  46. package/dist/params.cjs +22 -10
  47. package/dist/params.cjs.map +1 -1
  48. package/dist/params.js +22 -7
  49. package/dist/params.js.map +1 -1
  50. package/dist/s3.cjs +286 -0
  51. package/dist/s3.cjs.map +1 -0
  52. package/dist/s3.js +273 -0
  53. package/dist/s3.js.map +1 -0
  54. package/dist/screen.cjs +34 -39
  55. package/dist/screen.cjs.map +1 -1
  56. package/dist/screen.js +48 -46
  57. package/dist/screen.js.map +1 -1
  58. package/dist/tasks.cjs +1640 -416
  59. package/dist/tasks.cjs.map +1 -1
  60. package/dist/tasks.js +1614 -412
  61. package/dist/tasks.js.map +1 -1
  62. package/dist/utils.cjs +7 -8
  63. package/dist/utils.cjs.map +1 -1
  64. package/dist/utils.js +6 -6
  65. package/dist/utils.js.map +1 -1
  66. package/package.json +36 -44
  67. package/scripts/ssm/parse-cli.js +35 -0
  68. package/scripts/ssm/ssm-admin.js +151 -0
  69. package/scripts/ssm/ssm-pull.js +147 -0
package/dist/index.cjs CHANGED
@@ -1,12 +1,3 @@
1
- "use strict";
2
-
3
- var __esmCache = {};
4
- var __loadESMSync = function(moduleName) {
5
- if (!__esmCache[moduleName]) {
6
- throw new Error(`ESM module "${moduleName}" not loaded. Please call the load() function first: const toolkit = require("@nmakarov/cli-toolkit"); await toolkit.load();`);
7
- }
8
- return __esmCache[moduleName];
9
- };
10
1
  var __create = Object.create;
11
2
  var __defProp = Object.defineProperty;
12
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -38,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
38
29
  ));
39
30
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
40
31
 
41
- // src/screen/components.ts
32
+ // src/screen/components.js
42
33
  function getScreenWidth(maxWidth = null) {
43
34
  const terminalWidth = process.stdout.columns || 80;
44
35
  const availableWidth = Math.max(20, terminalWidth - 4);
@@ -112,14 +103,13 @@ function ScreenFooter({ lines, textStyle }) {
112
103
  }
113
104
  var import_react, import_ink;
114
105
  var init_components = __esm({
115
- "src/screen/components.ts"() {
116
- "use strict";
106
+ "src/screen/components.js"() {
117
107
  import_react = require("react");
118
108
  import_ink = require("ink");
119
109
  }
120
110
  });
121
111
 
122
- // src/screen/list-components.ts
112
+ // src/screen/list-components.js
123
113
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
124
114
  const [, forceUpdate] = (0, import_react2.useState)({});
125
115
  const termWidth = (process.stdout.columns || 80) - 8;
@@ -271,11 +261,13 @@ function MultiColumnListWithPreviewComponent({
271
261
  ...previewRows
272
262
  );
273
263
  }
274
- function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
264
+ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " ", onSelectionChange }) {
275
265
  const [, forceUpdate] = (0, import_react2.useState)({});
276
266
  const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
277
267
  const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
278
268
  const scrollStateRef = (0, import_react2.useRef)({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
269
+ const itemsRef = (0, import_react2.useRef)(items);
270
+ itemsRef.current = items;
279
271
  const defaultGetTitle = (item) => {
280
272
  return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
281
273
  };
@@ -289,18 +281,24 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
289
281
  return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
290
282
  }
291
283
  }) : items;
284
+ const displayItemsRef = (0, import_react2.useRef)(displayItems);
285
+ displayItemsRef.current = displayItems;
292
286
  const effectiveMaxHeight = maxHeight || displayItems.length;
293
- const canScroll = displayItems.length > effectiveMaxHeight;
287
+ const _canScroll = displayItems.length > effectiveMaxHeight;
294
288
  const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
295
289
  const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
296
290
  const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
297
291
  const canScrollUp = clampedScrollOffset > 0;
298
292
  const canScrollDown = clampedScrollOffset < maxScrollOffset;
299
293
  scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
294
+ const onSelectionChangeRef = (0, import_react2.useRef)(onSelectionChange);
295
+ onSelectionChangeRef.current = onSelectionChange;
300
296
  (0, import_react2.useEffect)(() => {
301
297
  ctx.setAction("moveUp", () => {
302
298
  const newIndex = Math.max(0, selectedIndexRef.current - 1);
303
299
  selectedIndexRef.current = newIndex;
300
+ const list = displayItemsRef.current;
301
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
304
302
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
305
303
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
306
304
  const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
@@ -310,18 +308,11 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
310
308
  forceUpdate({});
311
309
  });
312
310
  ctx.setAction("moveDown", () => {
313
- const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
314
- const titleA = titleGetter(a).toLowerCase();
315
- const titleB = titleGetter(b).toLowerCase();
316
- if (sortOrder === "asc") {
317
- return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
318
- } else {
319
- return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
320
- }
321
- }) : items;
322
- const maxIndex = currentItems.length - 1;
311
+ const currentItems = displayItemsRef.current;
312
+ const maxIndex = Math.max(0, currentItems.length - 1);
323
313
  const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
324
314
  selectedIndexRef.current = newIndex;
315
+ onSelectionChangeRef.current?.(newIndex, currentItems[newIndex]);
325
316
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
326
317
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
327
318
  const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
@@ -331,8 +322,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
331
322
  forceUpdate({});
332
323
  });
333
324
  ctx.setAction("scrollUp", () => {
334
- const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
335
- const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
325
+ const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
336
326
  const newScrollOffset = Math.max(0, currentScrollOffset - 1);
337
327
  setScrollOffset(newScrollOffset);
338
328
  forceUpdate({});
@@ -347,9 +337,9 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
347
337
  if (sortable) {
348
338
  ctx.setAction("toggleSort", () => {
349
339
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
350
- const currentSelectedItem = displayItems[selectedIndexRef.current];
340
+ const currentSelectedItem = displayItemsRef.current[selectedIndexRef.current];
351
341
  setSortOrder(nextSort);
352
- const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
342
+ const newSortedItems = nextSort !== "none" ? [...itemsRef.current].sort((a, b) => {
353
343
  const titleA = titleGetter(a).toLowerCase();
354
344
  const titleB = titleGetter(b).toLowerCase();
355
345
  if (nextSort === "asc") {
@@ -357,7 +347,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
357
347
  } else {
358
348
  return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
359
349
  }
360
- }) : items;
350
+ }) : itemsRef.current;
361
351
  const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
362
352
  if (newIndex !== -1) {
363
353
  selectedIndexRef.current = newIndex;
@@ -495,8 +485,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
495
485
  }
496
486
  var import_react2, import_ink2, h2;
497
487
  var init_list_components = __esm({
498
- "src/screen/list-components.ts"() {
499
- "use strict";
488
+ "src/screen/list-components.js"() {
500
489
  import_react2 = __toESM(require("react"), 1);
501
490
  import_ink2 = require("ink");
502
491
  init_components();
@@ -504,7 +493,7 @@ var init_list_components = __esm({
504
493
  }
505
494
  });
506
495
 
507
- // src/screen/screens.ts
496
+ // src/screen/screens.js
508
497
  function groupKeyBindings(bindings) {
509
498
  const groups = {};
510
499
  const enabledBindings = bindings.filter((b) => b.enabled !== false);
@@ -580,7 +569,7 @@ async function showScreen(config2) {
580
569
  let renderResult = null;
581
570
  let initialized = false;
582
571
  const Screen = () => {
583
- const [updateCounter, setUpdateCounter] = (0, import_react3.useState)(0);
572
+ const [, setUpdateCounter] = (0, import_react3.useState)(0);
584
573
  if (!initialized) {
585
574
  const defaultBindings = [
586
575
  { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
@@ -699,7 +688,7 @@ async function showScreen(config2) {
699
688
  }
700
689
  }
701
690
  if (matchedBinding && actions[matchedBinding.action]) {
702
- const actionResult = actions[matchedBinding.action]({
691
+ actions[matchedBinding.action]({
703
692
  input,
704
693
  key,
705
694
  binding: matchedBinding
@@ -829,8 +818,7 @@ async function showMultiColumnListWithPreviewScreen(config2) {
829
818
  }
830
819
  var import_react3, import_ink3, showMenuScreen, showWordGridScreen;
831
820
  var init_screens = __esm({
832
- "src/screen/screens.ts"() {
833
- "use strict";
821
+ "src/screen/screens.js"() {
834
822
  import_react3 = require("react");
835
823
  import_ink3 = require("ink");
836
824
  init_components();
@@ -840,7 +828,7 @@ var init_screens = __esm({
840
828
  }
841
829
  });
842
830
 
843
- // src/screen/ui-elements.ts
831
+ // src/screen/ui-elements.js
844
832
  function ListItem({
845
833
  children,
846
834
  isSelected = false,
@@ -865,7 +853,7 @@ function TextBlock({
865
853
  color = "white",
866
854
  dimmed = false,
867
855
  bold = false,
868
- maxWidth
856
+ maxWidth: _maxWidth
869
857
  }) {
870
858
  return (0, import_react4.createElement)(
871
859
  import_ink4.Box,
@@ -905,7 +893,7 @@ function GridCell({
905
893
  }, children)
906
894
  );
907
895
  }
908
- function InputField({ prompt, value, onChange, onSubmit }) {
896
+ function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit }) {
909
897
  return (0, import_react4.createElement)(
910
898
  import_ink4.Box,
911
899
  { flexDirection: "column" },
@@ -919,33 +907,31 @@ function InputField({ prompt, value, onChange, onSubmit }) {
919
907
  }
920
908
  var import_react4, import_ink4;
921
909
  var init_ui_elements = __esm({
922
- "src/screen/ui-elements.ts"() {
923
- "use strict";
910
+ "src/screen/ui-elements.js"() {
924
911
  import_react4 = require("react");
925
912
  import_ink4 = require("ink");
926
913
  }
927
914
  });
928
915
 
929
- // src/screen/utils.ts
916
+ // src/screen/utils.js
930
917
  function buildBreadcrumb(parts) {
931
918
  if (parts.length === 0) return "";
932
919
  if (parts.length === 1) return parts[0];
933
920
  return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
934
921
  }
935
- function buildDetailBreadcrumb(path4, suffix = "") {
936
- if (path4.length <= 1) {
937
- return suffix ? `\u2190 ${suffix}` : path4[0] || "";
922
+ function buildDetailBreadcrumb(path5, suffix = "") {
923
+ if (path5.length <= 1) {
924
+ return suffix ? `\u2190 ${suffix}` : path5[0] || "";
938
925
  }
939
- const breadcrumb = buildBreadcrumb(path4);
926
+ const breadcrumb = buildBreadcrumb(path5);
940
927
  return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
941
928
  }
942
929
  var init_utils = __esm({
943
- "src/screen/utils.ts"() {
944
- "use strict";
930
+ "src/screen/utils.js"() {
945
931
  }
946
932
  });
947
933
 
948
- // src/screen/footer-builder.ts
934
+ // src/screen/footer-builder.js
949
935
  function buildFooter(config2 = {}) {
950
936
  const {
951
937
  navigation = null,
@@ -996,8 +982,7 @@ function organizeFooterMessages(messages) {
996
982
  }
997
983
  var FooterPresets;
998
984
  var init_footer_builder = __esm({
999
- "src/screen/footer-builder.ts"() {
1000
- "use strict";
985
+ "src/screen/footer-builder.js"() {
1001
986
  FooterPresets = {
1002
987
  /**
1003
988
  * Menu screen footer
@@ -1056,7 +1041,7 @@ var init_footer_builder = __esm({
1056
1041
  }
1057
1042
  });
1058
1043
 
1059
- // src/screen/index.ts
1044
+ // src/screen/index.js
1060
1045
  async function load() {
1061
1046
  if (loadPromise) return loadPromise;
1062
1047
 
@@ -1065,8 +1050,7 @@ async function load() {
1065
1050
  }
1066
1051
  var import_react5, import_ink5, loadPromise;
1067
1052
  var init_screen = __esm({
1068
- "src/screen/index.ts"() {
1069
- "use strict";
1053
+ "src/screen/index.js"() {
1070
1054
  import_react5 = __toESM(require("react"), 1);
1071
1055
  import_ink5 = require("ink");
1072
1056
  init_screens();
@@ -1083,9 +1067,10 @@ var init_screen = __esm({
1083
1067
  }
1084
1068
  });
1085
1069
 
1086
- // src/index.ts
1070
+ // src/index.js
1087
1071
  var src_exports = {};
1088
1072
  __export(src_exports, {
1073
+ AbstractTask: () => AbstractTask,
1089
1074
  Args: () => Args,
1090
1075
  Box: () => import_ink5.Box,
1091
1076
  Db: () => Db,
@@ -1101,13 +1086,15 @@ __export(src_exports, {
1101
1086
  MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1102
1087
  Params: () => Params,
1103
1088
  React: () => import_react5.default,
1089
+ S3: () => S3,
1090
+ SERVICE_TASK_NAMES: () => SERVICE_TASK_NAMES,
1104
1091
  ScreenBody: () => ScreenBody,
1105
1092
  ScreenContainer: () => ScreenContainer,
1106
1093
  ScreenDivider: () => ScreenDivider,
1107
1094
  ScreenFooter: () => ScreenFooter,
1108
1095
  ScreenRow: () => ScreenRow,
1109
1096
  ScreenTitle: () => ScreenTitle,
1110
- TaskMaster: () => TaskMaster,
1097
+ TaskGetLogs: () => TaskGetLogs,
1111
1098
  TaskPing: () => TaskPing,
1112
1099
  TaskSampleProcess: () => TaskSampleProcess,
1113
1100
  TaskShellCommand: () => TaskShellCommand,
@@ -1122,24 +1109,38 @@ __export(src_exports, {
1122
1109
  buildBreadcrumb: () => buildBreadcrumb,
1123
1110
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1124
1111
  buildFooter: () => buildFooter,
1125
- dbConnect: () => dbConnect,
1126
- dbFindAndConnect: () => dbFindAndConnect,
1127
- dbInit: () => dbInit,
1112
+ convertPattern: () => convertPattern,
1128
1113
  defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
1129
1114
  defaultTasksRegistry: () => defaultTasksRegistry,
1130
1115
  defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
1131
1116
  enqueueStopTask: () => enqueueStopTask,
1132
1117
  enqueueTask: () => enqueueTask,
1133
1118
  ensureTaskTables: () => ensureTaskTables,
1119
+ flushTaskIpcLogs: () => flushTaskIpcLogs,
1134
1120
  getArgsInstance: () => getArgsInstance,
1135
1121
  h: () => import_react5.createElement,
1122
+ ipcFileLogsTableNameForSourceResource: () => ipcFileLogsTableNameForSourceResource,
1136
1123
  joiEdateType: () => joiEdateType,
1137
1124
  joiStringArrayType: () => joiStringArrayType,
1125
+ listAliveRunnerHeartbeats: () => listServicesRegistry,
1126
+ listServicesRegistry: () => listServicesRegistry,
1138
1127
  listSources: () => listSources,
1139
1128
  listTables: () => listTables,
1140
1129
  load: () => load,
1130
+ matchesParsedPattern: () => matchesParsedPattern,
1131
+ memo: () => import_react5.memo,
1132
+ mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
1133
+ nextTimeMatch: () => nextTimeMatch,
1134
+ normalizeAllowedTasks: () => normalizeAllowedTasks,
1141
1135
  organizeFooterMessages: () => organizeFooterMessages,
1142
1136
  queueToTableNames: () => queueToTableNames,
1137
+ readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
1138
+ registerInServicesRegistry: () => registerInServicesRegistry,
1139
+ registerRunnerHeartbeat: () => registerInServicesRegistry,
1140
+ resolveAsterisks: () => resolveAsterisks,
1141
+ resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
1142
+ resolveRanges: () => resolveRanges,
1143
+ resolveSteps: () => resolveSteps,
1143
1144
  runNodeTaskScript: () => runNodeTaskScript,
1144
1145
  runTasksLoop: () => runTasksLoop,
1145
1146
  setupContext: () => setupContext,
@@ -1149,10 +1150,18 @@ __export(src_exports, {
1149
1150
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1150
1151
  showScreen: () => showScreen,
1151
1152
  showWordGridScreen: () => showWordGridScreen,
1153
+ taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
1154
+ timeMatcher: () => timeMatcher,
1155
+ touchRunnerHeartbeat: () => touchServicesRegistry,
1156
+ touchServicesRegistry: () => touchServicesRegistry,
1157
+ unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
1158
+ unregisterServicesRegistry: () => unregisterServicesRegistry,
1159
+ updateServicesRegistryMetadata: () => updateServicesRegistryMetadata,
1152
1160
  updateTaskProgress: () => updateTaskProgress,
1153
1161
  useCallback: () => import_react5.useCallback,
1154
1162
  useEffect: () => import_react5.useEffect,
1155
1163
  useInput: () => import_ink5.useInput,
1164
+ useLayoutEffect: () => import_react5.useLayoutEffect,
1156
1165
  useMemo: () => import_react5.useMemo,
1157
1166
  useRef: () => import_react5.useRef,
1158
1167
  useState: () => import_react5.useState,
@@ -1160,7 +1169,7 @@ __export(src_exports, {
1160
1169
  });
1161
1170
  module.exports = __toCommonJS(src_exports);
1162
1171
 
1163
- // src/args/index.ts
1172
+ // src/args/index.js
1164
1173
  var import_fs = require("fs");
1165
1174
  var import_path = require("path");
1166
1175
  var import_dotenv = require("dotenv");
@@ -1647,10 +1656,10 @@ function getArgsInstance() {
1647
1656
  return instance;
1648
1657
  }
1649
1658
 
1650
- // src/params/index.ts
1659
+ // src/params/index.js
1651
1660
  var import_joi = __toESM(require("joi"), 1);
1652
1661
 
1653
- // src/errors.ts
1662
+ // src/errors.js
1654
1663
  var FrameworkError = class extends Error {
1655
1664
  constructor(message) {
1656
1665
  super(message);
@@ -1670,7 +1679,7 @@ var FileDatabaseError = class extends FrameworkError {
1670
1679
  }
1671
1680
  };
1672
1681
 
1673
- // src/params/custom-types.ts
1682
+ // src/params/custom-types.js
1674
1683
  var joiEdateType = (value, helpers) => {
1675
1684
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
1676
1685
  const testDate = new Date(value);
@@ -1763,7 +1772,7 @@ function calculateTimeOffset(amount, unit, sign) {
1763
1772
  }
1764
1773
  return sign === "+" ? amount * multiplier : -amount * multiplier;
1765
1774
  }
1766
- var joiStringArrayType = (type) => (value, helpers) => {
1775
+ var joiStringArrayType = (type) => (value, _helpers) => {
1767
1776
  if (value === void 0 || typeof value === "function") {
1768
1777
  return [];
1769
1778
  }
@@ -1789,7 +1798,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
1789
1798
  return arr;
1790
1799
  };
1791
1800
 
1792
- // src/params/index.ts
1801
+ // src/params/index.js
1793
1802
  var Params = class _Params {
1794
1803
  context;
1795
1804
  // Partial context during initialization
@@ -1981,7 +1990,7 @@ var Params = class _Params {
1981
1990
  throw new ParamError(`default value "${defValObj.value}" type mismatch`);
1982
1991
  }
1983
1992
  type = type.default(defValObj.value);
1984
- } else if (str.match(/required/)) {
1993
+ } else if (str.match(/\s*required\s*/)) {
1985
1994
  type = type.required();
1986
1995
  } else {
1987
1996
  type = type.optional();
@@ -2049,7 +2058,7 @@ var Params = class _Params {
2049
2058
  definition = val;
2050
2059
  val = val.value;
2051
2060
  }
2052
- const def = this.assignDefinition(key, definition);
2061
+ this.assignDefinition(key, definition);
2053
2062
  if (!this.runAllRegisteredSetters(key, val)) {
2054
2063
  this.params[key] = val;
2055
2064
  }
@@ -2057,6 +2066,8 @@ var Params = class _Params {
2057
2066
  /**
2058
2067
  * Get all parameters from definitions (main script).
2059
2068
  * Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
2069
+ * Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
2070
+ * around {@link get}) so --showUsedParams groups usage correctly.
2060
2071
  */
2061
2072
  getAll(defs) {
2062
2073
  return this.getAllForModule("script", defs);
@@ -2093,6 +2104,19 @@ var Params = class _Params {
2093
2104
  this._currentModule = prev;
2094
2105
  }
2095
2106
  }
2107
+ /**
2108
+ * Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
2109
+ * under the same module (for --showUsedParams / getFiguredByModule).
2110
+ */
2111
+ runWithModule(moduleName, fn) {
2112
+ const prev = this._currentModule;
2113
+ this._currentModule = moduleName;
2114
+ try {
2115
+ return fn();
2116
+ } finally {
2117
+ this._currentModule = prev;
2118
+ }
2119
+ }
2096
2120
  /**
2097
2121
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2098
2122
  */
@@ -2106,9 +2130,9 @@ var Params = class _Params {
2106
2130
  if (!parenMatch) continue;
2107
2131
  const parts = parenMatch[1].split(":");
2108
2132
  if (parts.length < 3) continue;
2109
- const path4 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2110
- if (!path4 || path4.includes(paramsIndexPath)) continue;
2111
- const srcMatch = path4.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2133
+ const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2134
+ if (!path5 || path5.includes(paramsIndexPath)) continue;
2135
+ const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2112
2136
  if (srcMatch) return srcMatch[1];
2113
2137
  }
2114
2138
  return "script";
@@ -2153,14 +2177,14 @@ var Params = class _Params {
2153
2177
  }
2154
2178
  };
2155
2179
 
2156
- // src/screen.ts
2180
+ // src/index.js
2157
2181
  init_screen();
2158
2182
 
2159
- // src/filedatabase/index.ts
2183
+ // src/filedatabase/index.js
2160
2184
  var import_fs4 = __toESM(require("fs"), 1);
2161
2185
  var import_path4 = __toESM(require("path"), 1);
2162
2186
 
2163
- // src/utils/os-utils.ts
2187
+ // src/utils/os-utils.js
2164
2188
  var import_fs2 = __toESM(require("fs"), 1);
2165
2189
  var import_path2 = __toESM(require("path"), 1);
2166
2190
  var import_child_process = require("child_process");
@@ -2189,7 +2213,7 @@ function getFreeDiskSpace(targetPath) {
2189
2213
  }
2190
2214
  }
2191
2215
 
2192
- // src/utils/fs-utils.ts
2216
+ // src/utils/fs-utils.js
2193
2217
  var import_fs3 = __toESM(require("fs"), 1);
2194
2218
  var import_path3 = __toESM(require("path"), 1);
2195
2219
  async function ensurePath(...pathParts) {
@@ -2213,7 +2237,7 @@ function getFileExtension(dataType) {
2213
2237
  }
2214
2238
  }
2215
2239
 
2216
- // src/utils/format-utils.ts
2240
+ // src/utils/format-utils.js
2217
2241
  function bytesToHumanReadable(bytes) {
2218
2242
  if (bytes === 0) return "0 B";
2219
2243
  const k = 1024;
@@ -2222,7 +2246,7 @@ function bytesToHumanReadable(bytes) {
2222
2246
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
2223
2247
  }
2224
2248
 
2225
- // src/utils/date-utils.ts
2249
+ // src/utils/date-utils.js
2226
2250
  function isTimestampFolder(folderName) {
2227
2251
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
2228
2252
  if (!isoRegex.test(folderName)) {
@@ -2232,7 +2256,7 @@ function isTimestampFolder(folderName) {
2232
2256
  return !isNaN(date.getTime()) && date.getTime() > 0;
2233
2257
  }
2234
2258
 
2235
- // src/filedatabase/serializers.ts
2259
+ // src/filedatabase/serializers.js
2236
2260
  function detectDataType(data) {
2237
2261
  if (Array.isArray(data)) {
2238
2262
  return "json-array";
@@ -2264,7 +2288,7 @@ function deserializeData(rawData, dataType) {
2264
2288
  }
2265
2289
  }
2266
2290
 
2267
- // src/filedatabase/synopsis-functions.ts
2291
+ // src/filedatabase/synopsis-functions.js
2268
2292
  function defaultFileSynopsisFunction(fileEntry, data) {
2269
2293
  if (!Array.isArray(data) || data.length === 0) {
2270
2294
  return { ...fileEntry };
@@ -2332,7 +2356,7 @@ function defaultVersionSynopsisFunction(metadata) {
2332
2356
  return result;
2333
2357
  }
2334
2358
 
2335
- // src/filedatabase/index.ts
2359
+ // src/filedatabase/index.js
2336
2360
  var FileDatabase = class _FileDatabase {
2337
2361
  basePath;
2338
2362
  namespace;
@@ -2418,7 +2442,7 @@ var FileDatabase = class _FileDatabase {
2418
2442
  if (errors.length) {
2419
2443
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
2420
2444
  }
2421
- let parts = [this.basePath, this.namespace];
2445
+ const parts = [this.basePath, this.namespace];
2422
2446
  if (this.tableName) {
2423
2447
  parts.push(...this.tableName.split("/"));
2424
2448
  }
@@ -2911,14 +2935,17 @@ var FileDatabase = class _FileDatabase {
2911
2935
  * Prepare the instance for read or write operations
2912
2936
  * This discovers state and sets up internal members based on mode and current data
2913
2937
  */
2914
- async prepare({ write, read, version }) {
2938
+ async prepare(options) {
2939
+ const { write, read, version, deferInitialVersion } = options;
2915
2940
  if (write) {
2916
2941
  if (this.versioned) {
2917
2942
  if (this.currentVersion === null) {
2918
- await this.makeNewVersion();
2919
- this.metadata = this.getDefaultMetadata();
2920
- this.metadata.version = this.currentVersion;
2921
- this.makeNewFile();
2943
+ if (!deferInitialVersion) {
2944
+ await this.makeNewVersion();
2945
+ this.metadata = this.getDefaultMetadata();
2946
+ this.metadata.version = this.currentVersion;
2947
+ this.makeNewFile();
2948
+ }
2922
2949
  } else {
2923
2950
  if (!this.metadata.files.length) {
2924
2951
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -3028,7 +3055,7 @@ var FileDatabase = class _FileDatabase {
3028
3055
  if (options.forceNewVersion && !this.versioned) {
3029
3056
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
3030
3057
  }
3031
- await this.prepare({ write: true });
3058
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
3032
3059
  const incomingDataType = detectDataType(data);
3033
3060
  this.metadata.dataType = incomingDataType;
3034
3061
  if (options.forceNewVersion) {
@@ -3343,22 +3370,57 @@ function listSources(basePath) {
3343
3370
  }
3344
3371
  }
3345
3372
 
3346
- // src/db/index.ts
3373
+ // src/db/index.js
3347
3374
  var import_knex = __toESM(require("knex"), 1);
3375
+ var KNEX_DEFAULTS = {
3376
+ testConnection: true,
3377
+ pool: { min: 2, max: 10 },
3378
+ acquireConnectionTimeout: 1e4,
3379
+ ssl: { rejectUnauthorized: false }
3380
+ };
3348
3381
  var Db = class {
3349
- knexInstance = null;
3350
- config;
3351
- logger;
3352
- queriesLog = [];
3353
- isConnected = false;
3354
- /**
3355
- * Constructor - accepts config object
3356
- * Use dbInit() function to initialize with Context
3357
- */
3382
+ static async init(context, options = {}) {
3383
+ const defs = {
3384
+ dbName: "string",
3385
+ dbConnectionString: "string",
3386
+ dbProfile: "boolean default false"
3387
+ };
3388
+ const discovered = context?.params?.getAllForModule?.("db", defs) ?? {};
3389
+ const merged = { ...discovered, ...options };
3390
+ let { dbName, dbConnectionString } = merged;
3391
+ const { dbProfile } = merged;
3392
+ if (!dbName && !dbConnectionString) {
3393
+ dbName = "local";
3394
+ }
3395
+ if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
3396
+ dbConnectionString = dbName;
3397
+ dbName = void 0;
3398
+ }
3399
+ if (dbName && !dbConnectionString) {
3400
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3401
+ dbConnectionString = await context.params.get(paramName, "string");
3402
+ if (!dbConnectionString) {
3403
+ throw new ParamError(
3404
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3405
+ );
3406
+ }
3407
+ }
3408
+ const config2 = {
3409
+ ...KNEX_DEFAULTS,
3410
+ connectionString: dbConnectionString,
3411
+ name: dbName || merged.name || "default",
3412
+ profile: !!dbProfile,
3413
+ logger: context.logger
3414
+ };
3415
+ return dbConnect(context, config2);
3416
+ }
3358
3417
  constructor(config2) {
3359
- if (!config2.connectionString) {
3418
+ if (!config2 || !config2.connectionString) {
3360
3419
  throw new ParamError("Db: connectionString is required");
3361
3420
  }
3421
+ this.knexInstance = null;
3422
+ this.isConnected = false;
3423
+ this.queriesLog = [];
3362
3424
  this.config = {
3363
3425
  testConnection: true,
3364
3426
  profile: false,
@@ -3371,25 +3433,23 @@ var Db = class {
3371
3433
  };
3372
3434
  this.logger = this.config.logger;
3373
3435
  const instance2 = this;
3374
- const callableWrapper = function(...args) {
3436
+ const callableWrapper = function() {
3375
3437
  throw new Error("This should never be called directly");
3376
3438
  };
3377
3439
  callableWrapper._instance = instance2;
3378
3440
  return new Proxy(callableWrapper, {
3379
- // Intercept function calls: db('table')
3380
- apply: (target, thisArg, argumentsList) => {
3441
+ apply: (target, _thisArg, argumentsList) => {
3381
3442
  const inst = target._instance;
3382
3443
  if (!inst.knexInstance) {
3383
3444
  throw new Error("Db: Not connected. Call connect() first.");
3384
3445
  }
3385
3446
  return inst.knexInstance(...argumentsList);
3386
3447
  },
3387
- // Intercept property access: db.schema, db.raw, etc.
3388
3448
  get: (target, prop) => {
3389
3449
  if (prop === "_instance") {
3390
3450
  return target._instance;
3391
3451
  }
3392
- const instance3 = target._instance;
3452
+ const inst = target._instance;
3393
3453
  const ownMethods = [
3394
3454
  "connect",
3395
3455
  "disconnect",
@@ -3402,26 +3462,26 @@ var Db = class {
3402
3462
  "detectClient",
3403
3463
  "attachProfiler"
3404
3464
  ];
3405
- if (prop in instance3) {
3406
- const value = instance3[prop];
3465
+ if (prop in inst) {
3466
+ const value = inst[prop];
3407
3467
  if (typeof value === "function" && ownMethods.includes(prop)) {
3408
- return value.bind(instance3);
3468
+ return value.bind(inst);
3409
3469
  }
3410
3470
  if (typeof value !== "function") {
3411
3471
  return value;
3412
3472
  }
3413
3473
  }
3414
- if (instance3.knexInstance) {
3415
- const knexProp = instance3.knexInstance[prop];
3474
+ if (inst.knexInstance) {
3475
+ const knexProp = inst.knexInstance[prop];
3416
3476
  if (typeof knexProp === "function") {
3417
- return knexProp.bind(instance3.knexInstance);
3477
+ return knexProp.bind(inst.knexInstance);
3418
3478
  }
3419
3479
  return knexProp;
3420
3480
  }
3421
- if (prop in instance3) {
3422
- const method = instance3[prop];
3481
+ if (prop in inst) {
3482
+ const method = inst[prop];
3423
3483
  if (typeof method === "function") {
3424
- return method.bind(instance3);
3484
+ return method.bind(inst);
3425
3485
  }
3426
3486
  return method;
3427
3487
  }
@@ -3429,9 +3489,6 @@ var Db = class {
3429
3489
  }
3430
3490
  });
3431
3491
  }
3432
- /**
3433
- * Detect database client type from connection string
3434
- */
3435
3492
  detectClient(connectionString) {
3436
3493
  if (connectionString.match(/^postgresql/)) {
3437
3494
  return "pg";
@@ -3441,9 +3498,6 @@ var Db = class {
3441
3498
  }
3442
3499
  return null;
3443
3500
  }
3444
- /**
3445
- * Connect to the database
3446
- */
3447
3501
  async connect() {
3448
3502
  if (this.isConnected && this.knexInstance) {
3449
3503
  this.logger.warn?.("[Db] Already connected");
@@ -3452,14 +3506,13 @@ var Db = class {
3452
3506
  const client = this.detectClient(this.config.connectionString);
3453
3507
  if (!client) {
3454
3508
  throw new ParamError(
3455
- `Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
3509
+ "Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
3456
3510
  );
3457
3511
  }
3458
3512
  try {
3459
3513
  const connectionConfig = {
3460
3514
  connectionString: this.config.connectionString,
3461
3515
  family: 4
3462
- // Force IPv4 only (disable IPv6)
3463
3516
  };
3464
3517
  this.knexInstance = (0, import_knex.default)({
3465
3518
  client,
@@ -3475,7 +3528,9 @@ var Db = class {
3475
3528
  await this.testConnection();
3476
3529
  }
3477
3530
  this.isConnected = true;
3478
- this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
3531
+ this.logger.debug?.(
3532
+ `[Db] Connected to database "${this.config.name || this.config.connectionString}"`
3533
+ );
3479
3534
  } catch (error) {
3480
3535
  if (error instanceof ParamError) {
3481
3536
  throw error;
@@ -3484,9 +3539,6 @@ var Db = class {
3484
3539
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
3485
3540
  }
3486
3541
  }
3487
- /**
3488
- * Disconnect from the database
3489
- */
3490
3542
  async disconnect() {
3491
3543
  if (!this.knexInstance) {
3492
3544
  return;
@@ -3496,16 +3548,15 @@ var Db = class {
3496
3548
  this.knexInstance = null;
3497
3549
  this.isConnected = false;
3498
3550
  this.queriesLog = [];
3499
- this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
3551
+ this.logger.debug?.(
3552
+ `[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
3553
+ );
3500
3554
  } catch (error) {
3501
3555
  const errorMsg = this.getErrorMessage(error);
3502
3556
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
3503
3557
  throw error;
3504
3558
  }
3505
3559
  }
3506
- /**
3507
- * Extract error message from various error types
3508
- */
3509
3560
  getErrorMessage(error) {
3510
3561
  if (error instanceof AggregateError) {
3511
3562
  const errors = error.errors || [];
@@ -3530,9 +3581,11 @@ var Db = class {
3530
3581
  return `${code} (tried: ${addresses.join(", ")})`;
3531
3582
  }
3532
3583
  }
3533
- const uniqueMessages = [...new Set(errors.map((e) => {
3534
- return e instanceof Error ? e.message : String(e);
3535
- }))];
3584
+ const uniqueMessages = [
3585
+ ...new Set(
3586
+ errors.map((e) => e instanceof Error ? e.message : String(e))
3587
+ )
3588
+ ];
3536
3589
  if (uniqueMessages.length === 1) {
3537
3590
  return uniqueMessages[0];
3538
3591
  }
@@ -3541,28 +3594,25 @@ var Db = class {
3541
3594
  return error.message || "Multiple errors occurred";
3542
3595
  }
3543
3596
  if (error instanceof Error) {
3544
- const errorWithCode = error;
3545
- if (errorWithCode.code) {
3546
- return `${errorWithCode.code}: ${error.message || String(error)}`;
3597
+ const code = error.code;
3598
+ if (code) {
3599
+ return `${code}: ${error.message || String(error)}`;
3547
3600
  }
3548
3601
  return error.message || String(error);
3549
3602
  }
3550
3603
  if (typeof error === "string") {
3551
3604
  return error;
3552
3605
  }
3553
- if (error?.message) {
3606
+ if (error && typeof error === "object" && "message" in error) {
3554
3607
  const msg = String(error.message);
3555
- const errorWithCode = error;
3556
- if (errorWithCode.code) {
3557
- return `${errorWithCode.code}: ${msg}`;
3608
+ const code = error.code;
3609
+ if (code) {
3610
+ return `${code}: ${msg}`;
3558
3611
  }
3559
3612
  return msg;
3560
3613
  }
3561
3614
  return String(error) || "Unknown error";
3562
3615
  }
3563
- /**
3564
- * Test database connection
3565
- */
3566
3616
  async testConnection() {
3567
3617
  if (!this.knexInstance) {
3568
3618
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3578,9 +3628,6 @@ var Db = class {
3578
3628
  throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
3579
3629
  }
3580
3630
  }
3581
- /**
3582
- * Attach query profiler to log all queries
3583
- */
3584
3631
  attachProfiler() {
3585
3632
  if (!this.knexInstance) {
3586
3633
  return;
@@ -3590,7 +3637,7 @@ var Db = class {
3590
3637
  this.knexInstance.on("query", (query) => {
3591
3638
  query.__startTime = process.hrtime();
3592
3639
  });
3593
- this.knexInstance.on("query-response", (response, query) => {
3640
+ this.knexInstance.on("query-response", (_response, query) => {
3594
3641
  const [seconds, nanoseconds] = process.hrtime(query.__startTime);
3595
3642
  const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
3596
3643
  const logEntry = {
@@ -3605,15 +3652,9 @@ var Db = class {
3605
3652
  this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
3606
3653
  });
3607
3654
  }
3608
- /**
3609
- * Get query log (only available if profiling is enabled)
3610
- */
3611
3655
  getQueryLog() {
3612
3656
  return [...this.queriesLog];
3613
3657
  }
3614
- /**
3615
- * Check if a table exists
3616
- */
3617
3658
  async tableExists(tableName) {
3618
3659
  if (!this.knexInstance) {
3619
3660
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3625,65 +3666,28 @@ var Db = class {
3625
3666
  throw error;
3626
3667
  }
3627
3668
  }
3628
- /**
3629
- * Get the underlying Knex instance (for advanced usage)
3630
- */
3631
3669
  getKnex() {
3632
3670
  if (!this.knexInstance) {
3633
3671
  throw new Error("Db: Not connected. Call connect() first.");
3634
3672
  }
3635
3673
  return this.knexInstance;
3636
3674
  }
3637
- /**
3638
- * Get connection status
3639
- */
3640
3675
  isConnectedToDb() {
3641
3676
  return this.isConnected && this.knexInstance !== null;
3642
3677
  }
3643
- /**
3644
- * Initialize Db with context (connects and registers disconnect cleanup).
3645
- * Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
3646
- */
3647
- static async init(context, dbNameOrConnectionString) {
3648
- return dbFindAndConnect(context, dbNameOrConnectionString);
3649
- }
3650
3678
  };
3651
3679
  function capitalizeFirstLetter(str) {
3652
3680
  return str.charAt(0).toUpperCase() + str.slice(1);
3653
3681
  }
3654
- async function dbConnect(context, connectionString, name, dbProfile) {
3655
- const defs = {
3656
- testDbConnection: "boolean default true",
3657
- name: "string",
3658
- poolMin: "number default 2",
3659
- poolMax: "number default 10",
3660
- acquireConnectionTimeout: "number default 10000",
3661
- sslRejectUnauthorized: "boolean default false"
3662
- };
3663
- const paramsConfig = context.params.getAllForModule(defs);
3664
- const config2 = {
3665
- connectionString,
3666
- name: paramsConfig.name || name || "default",
3667
- testConnection: paramsConfig.testDbConnection,
3668
- profile: dbProfile ?? false,
3669
- pool: {
3670
- min: paramsConfig.poolMin,
3671
- max: paramsConfig.poolMax
3672
- },
3673
- acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
3674
- ssl: {
3675
- rejectUnauthorized: paramsConfig.sslRejectUnauthorized
3676
- },
3677
- logger: context.logger
3678
- };
3682
+ async function dbConnect(context, config2) {
3679
3683
  try {
3680
3684
  const db = new Db(config2);
3681
3685
  context.registerCleanup(async () => {
3682
3686
  await db.disconnect();
3683
- context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
3687
+ context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
3684
3688
  });
3685
3689
  await db.connect();
3686
- context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
3690
+ context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
3687
3691
  return db;
3688
3692
  } catch (error) {
3689
3693
  if (error instanceof ParamError) {
@@ -3693,52 +3697,255 @@ async function dbConnect(context, connectionString, name, dbProfile) {
3693
3697
  throw new ParamError(`[Db] connect error: ${errorMsg}`);
3694
3698
  }
3695
3699
  }
3696
- async function dbFindAndConnect(context, dbNameOrConnectionString) {
3697
- let dbName;
3698
- let dbConnectionString;
3699
- let dbProfile;
3700
- if (dbNameOrConnectionString) {
3701
- if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
3702
- dbName = void 0;
3703
- dbConnectionString = dbNameOrConnectionString;
3704
- } else {
3705
- dbName = dbNameOrConnectionString;
3706
- }
3707
- } else {
3708
- const defs = {
3709
- dbName: "string",
3710
- dbConnectionString: "string",
3711
- dbProfile: "boolean default false"
3700
+
3701
+ // src/s3/index.js
3702
+ var import_client_s3 = require("@aws-sdk/client-s3");
3703
+ var DEFAULT_PROFILE = "local";
3704
+ function capitalize(str) {
3705
+ if (!str) return str;
3706
+ return str.charAt(0).toUpperCase() + str.slice(1);
3707
+ }
3708
+ var S3 = class _S3 {
3709
+ /**
3710
+ * Build an S3 instance. Reads bucket profile from --bucket (default "local"),
3711
+ * then resolves per-profile params, builds the SDK client, and returns the
3712
+ * instance. Optionally pings the bucket once to verify reachability.
3713
+ */
3714
+ static async init(context, options = {}) {
3715
+ const profileDef = { bucket: "string" };
3716
+ const discovered = context?.params?.getAllForModule?.("s3", profileDef) ?? {};
3717
+ const profile = options.bucket ?? discovered.bucket ?? DEFAULT_PROFILE;
3718
+ const cap = capitalize(profile);
3719
+ const config2 = {
3720
+ profile,
3721
+ bucketName: options.bucketName ?? await context.params.get(`s3Bucket${cap}`, "string"),
3722
+ region: options.region ?? await context.params.get(`s3Region${cap}`, "string default us-east-1"),
3723
+ endpoint: options.endpoint ?? await context.params.get(`s3Endpoint${cap}`, "string"),
3724
+ forcePathStyle: options.forcePathStyle ?? await context.params.get(`s3ForcePathStyle${cap}`, "boolean default false"),
3725
+ accessKeyId: options.accessKeyId ?? await context.params.get(`s3AccessKeyId${cap}`, "string"),
3726
+ secretAccessKey: options.secretAccessKey ?? await context.params.get(`s3SecretAccessKey${cap}`, "string")
3712
3727
  };
3713
- const paramsConfig = context.params.getAllForModule(defs);
3714
- dbName = paramsConfig.dbName;
3715
- dbConnectionString = paramsConfig.dbConnectionString;
3716
- dbProfile = paramsConfig.dbProfile;
3717
- }
3718
- if (!dbName && !dbConnectionString) {
3719
- throw new ParamError("Db: either dbName or dbConnectionString must be specified");
3720
- }
3721
- if (dbName) {
3722
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3723
- dbConnectionString = await context.params.get(paramName, "string");
3724
- if (!dbConnectionString) {
3728
+ if (!config2.bucketName) {
3725
3729
  throw new ParamError(
3726
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3730
+ `S3: bucket name not configured for profile "${profile}" (set s3Bucket${cap} or S3_BUCKET_${profile.toUpperCase()})`
3727
3731
  );
3728
3732
  }
3733
+ const s3 = new _S3(context, config2);
3734
+ if (options.testBucket !== false) {
3735
+ try {
3736
+ await s3.bucketExists();
3737
+ context.logger?.debug?.(
3738
+ `[S3] profile="${profile}" bucket="${config2.bucketName}" reachable`
3739
+ );
3740
+ } catch (err) {
3741
+ context.logger?.warn?.(
3742
+ `[S3] profile="${profile}" bucket="${config2.bucketName}" reachability test failed: ${err?.message ?? err}`
3743
+ );
3744
+ }
3745
+ }
3746
+ return s3;
3747
+ }
3748
+ constructor(context, config2) {
3749
+ this.logger = context?.logger ?? console;
3750
+ this.profile = config2.profile;
3751
+ this.bucketName = config2.bucketName;
3752
+ this.region = config2.region;
3753
+ this.endpoint = config2.endpoint || null;
3754
+ const clientConfig = { region: config2.region };
3755
+ if (config2.endpoint) clientConfig.endpoint = config2.endpoint;
3756
+ if (config2.forcePathStyle) clientConfig.forcePathStyle = true;
3757
+ if (config2.accessKeyId && config2.secretAccessKey) {
3758
+ clientConfig.credentials = {
3759
+ accessKeyId: config2.accessKeyId,
3760
+ secretAccessKey: config2.secretAccessKey
3761
+ };
3762
+ }
3763
+ this.client = new import_client_s3.S3Client(clientConfig);
3729
3764
  }
3730
- const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
3731
- return db;
3732
- }
3733
- async function dbInit(context, dbNameOrConnectionString) {
3734
- return await dbFindAndConnect(context, dbNameOrConnectionString);
3735
- }
3765
+ // ── info ────────────────────────────────────────────────────────────────
3766
+ getBucketName() {
3767
+ return this.bucketName;
3768
+ }
3769
+ getProfile() {
3770
+ return this.profile;
3771
+ }
3772
+ getRegion() {
3773
+ return this.region;
3774
+ }
3775
+ getEndpoint() {
3776
+ return this.endpoint;
3777
+ }
3778
+ // ── reachability ────────────────────────────────────────────────────────
3779
+ async bucketExists() {
3780
+ await this.client.send(new import_client_s3.HeadBucketCommand({ Bucket: this.bucketName }));
3781
+ return true;
3782
+ }
3783
+ // ── HEAD / GET ──────────────────────────────────────────────────────────
3784
+ /** Returns null on 404; never throws for "missing". Other errors throw. */
3785
+ async headObject(key) {
3786
+ try {
3787
+ const out = await this.client.send(new import_client_s3.HeadObjectCommand({
3788
+ Bucket: this.bucketName,
3789
+ Key: key
3790
+ }));
3791
+ return {
3792
+ etag: out.ETag,
3793
+ size: out.ContentLength,
3794
+ contentType: out.ContentType,
3795
+ lastModified: out.LastModified,
3796
+ metadata: out.Metadata,
3797
+ storageClass: out.StorageClass
3798
+ };
3799
+ } catch (err) {
3800
+ if (this._isNotFound(err)) return null;
3801
+ throw err;
3802
+ }
3803
+ }
3804
+ /** Returns { body: Readable, contentType, contentLength, etag, ... } or null on 404. */
3805
+ async getObject(key) {
3806
+ try {
3807
+ const out = await this.client.send(new import_client_s3.GetObjectCommand({
3808
+ Bucket: this.bucketName,
3809
+ Key: key
3810
+ }));
3811
+ return {
3812
+ body: out.Body,
3813
+ contentType: out.ContentType,
3814
+ contentLength: out.ContentLength,
3815
+ etag: out.ETag,
3816
+ lastModified: out.LastModified,
3817
+ metadata: out.Metadata
3818
+ };
3819
+ } catch (err) {
3820
+ if (this._isNotFound(err)) return null;
3821
+ throw err;
3822
+ }
3823
+ }
3824
+ /** Buffers the whole object. Use only for small objects (manifests, JSON). */
3825
+ async getObjectBytes(key) {
3826
+ const obj = await this.getObject(key);
3827
+ if (!obj) return null;
3828
+ const chunks = [];
3829
+ for await (const chunk of obj.body) chunks.push(chunk);
3830
+ return { ...obj, body: Buffer.concat(chunks) };
3831
+ }
3832
+ /** Convenience for JSON manifests. Returns parsed object or null on 404. */
3833
+ async getJson(key) {
3834
+ const obj = await this.getObjectBytes(key);
3835
+ if (!obj) return null;
3836
+ return JSON.parse(obj.body.toString("utf8"));
3837
+ }
3838
+ // ── PUT ─────────────────────────────────────────────────────────────────
3839
+ async putObject({ key, body, contentType, contentLength, tags, metadata }) {
3840
+ const cmd = new import_client_s3.PutObjectCommand({
3841
+ Bucket: this.bucketName,
3842
+ Key: key,
3843
+ Body: body,
3844
+ ...contentType && { ContentType: contentType },
3845
+ ...contentLength != null && { ContentLength: contentLength },
3846
+ ...metadata && { Metadata: metadata },
3847
+ ...tags && { Tagging: this._tagsToQuery(tags) }
3848
+ });
3849
+ return this.client.send(cmd);
3850
+ }
3851
+ /** Convenience for JSON manifests. */
3852
+ async putJson(key, value, opts = {}) {
3853
+ const json = JSON.stringify(value, null, opts.pretty ? 2 : 0);
3854
+ const body = Buffer.from(json, "utf8");
3855
+ return this.putObject({
3856
+ key,
3857
+ body,
3858
+ contentType: "application/json",
3859
+ contentLength: body.length,
3860
+ tags: opts.tags,
3861
+ metadata: opts.metadata
3862
+ });
3863
+ }
3864
+ // ── DELETE ──────────────────────────────────────────────────────────────
3865
+ async deleteObject(key) {
3866
+ return this.client.send(new import_client_s3.DeleteObjectCommand({
3867
+ Bucket: this.bucketName,
3868
+ Key: key
3869
+ }));
3870
+ }
3871
+ // ── COPY (for migration: legacy → new bucket, or intra-bucket "rename") ─
3872
+ async copyObject({ sourceBucket, sourceKey, key, contentType, metadata, tags }) {
3873
+ const src = sourceBucket || this.bucketName;
3874
+ const cmd = new import_client_s3.CopyObjectCommand({
3875
+ Bucket: this.bucketName,
3876
+ Key: key,
3877
+ CopySource: encodeURIComponent(`${src}/${sourceKey}`),
3878
+ ...contentType && {
3879
+ ContentType: contentType,
3880
+ MetadataDirective: "REPLACE"
3881
+ },
3882
+ ...metadata && {
3883
+ Metadata: metadata,
3884
+ MetadataDirective: "REPLACE"
3885
+ },
3886
+ ...tags && {
3887
+ Tagging: this._tagsToQuery(tags),
3888
+ TaggingDirective: "REPLACE"
3889
+ }
3890
+ });
3891
+ return this.client.send(cmd);
3892
+ }
3893
+ // ── LIST ────────────────────────────────────────────────────────────────
3894
+ async listObjects(prefix, { keysOnly = false, maxKeys = 1e3, continuationToken } = {}) {
3895
+ const out = await this.client.send(new import_client_s3.ListObjectsV2Command({
3896
+ Bucket: this.bucketName,
3897
+ Prefix: prefix,
3898
+ MaxKeys: maxKeys,
3899
+ ContinuationToken: continuationToken
3900
+ }));
3901
+ const items = (out.Contents ?? []).map((o) => ({
3902
+ key: o.Key,
3903
+ size: o.Size,
3904
+ etag: o.ETag,
3905
+ lastModified: o.LastModified,
3906
+ storageClass: o.StorageClass
3907
+ }));
3908
+ return {
3909
+ items: keysOnly ? items.map((i) => i.key) : items,
3910
+ isTruncated: !!out.IsTruncated,
3911
+ nextContinuationToken: out.NextContinuationToken
3912
+ };
3913
+ }
3914
+ // ── TAGS (used for lifecycle rules, e.g. status=closed → Glacier IR) ────
3915
+ async putObjectTagging(key, tags) {
3916
+ return this.client.send(new import_client_s3.PutObjectTaggingCommand({
3917
+ Bucket: this.bucketName,
3918
+ Key: key,
3919
+ Tagging: { TagSet: this._tagsToTagSet(tags) }
3920
+ }));
3921
+ }
3922
+ async getObjectTagging(key) {
3923
+ const out = await this.client.send(new import_client_s3.GetObjectTaggingCommand({
3924
+ Bucket: this.bucketName,
3925
+ Key: key
3926
+ }));
3927
+ const tags = {};
3928
+ for (const t of out.TagSet ?? []) tags[t.Key] = t.Value;
3929
+ return tags;
3930
+ }
3931
+ // ── internals ───────────────────────────────────────────────────────────
3932
+ _isNotFound(err) {
3933
+ const status = err?.$metadata?.httpStatusCode;
3934
+ return status === 404 || err?.name === "NotFound" || err?.name === "NoSuchKey" || err?.Code === "NoSuchKey";
3935
+ }
3936
+ _tagsToQuery(tags) {
3937
+ return Object.entries(tags).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
3938
+ }
3939
+ _tagsToTagSet(tags) {
3940
+ return Object.entries(tags).map(([Key, Value]) => ({ Key, Value: String(Value) }));
3941
+ }
3942
+ };
3736
3943
 
3737
- // src/logger/index.ts
3944
+ // src/logger/index.js
3738
3945
  var import_chalk = __toESM(require("chalk"), 1);
3739
3946
  var import_util = __toESM(require("util"), 1);
3740
3947
 
3741
- // src/logger/transports.ts
3948
+ // src/logger/transports.js
3742
3949
  var ConsoleTransport = class {
3743
3950
  write(payload) {
3744
3951
  console.info(payload);
@@ -3758,7 +3965,7 @@ var ParentProcessTransport = class {
3758
3965
  }
3759
3966
  };
3760
3967
 
3761
- // src/logger/index.ts
3968
+ // src/logger/index.js
3762
3969
  var ALL_LEVELS = [
3763
3970
  "silly",
3764
3971
  "debug",
@@ -3831,6 +4038,7 @@ var Logger = class _Logger {
3831
4038
  }
3832
4039
  /**
3833
4040
  * Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
4041
+ * Params are tracked under the `logger` module for --showUsedParams.
3834
4042
  */
3835
4043
  static init(context, options) {
3836
4044
  const paramDefs = {
@@ -3844,7 +4052,7 @@ var Logger = class _Logger {
3844
4052
  progressWithTimes: "boolean default false",
3845
4053
  progressThrottleMs: "number"
3846
4054
  };
3847
- const discovered = context.params.getAllForModule(paramDefs);
4055
+ const discovered = context.params.getAllForModule("logger", paramDefs);
3848
4056
  const config2 = { ...discovered, ...options };
3849
4057
  const logger = new _Logger(context, config2);
3850
4058
  context.logger = logger;
@@ -4040,9 +4248,9 @@ var Logger = class _Logger {
4040
4248
  }
4041
4249
  };
4042
4250
 
4043
- // src/init/index.ts
4251
+ // src/init/index.js
4044
4252
  var import_events = require("events");
4045
- function extractComponentOptions(opts, componentName) {
4253
+ function extractComponentOptions(opts, _componentName) {
4046
4254
  const reservedKeys = ["overrides", "defaults", "modules"];
4047
4255
  const componentOptions = {};
4048
4256
  for (const [key, value] of Object.entries(opts)) {
@@ -4087,7 +4295,10 @@ function setupContext(opts = {}) {
4087
4295
  return setup(opts);
4088
4296
  }
4089
4297
 
4090
- // src/utils/core-utils.ts
4298
+ // src/tasks/index.js
4299
+ var import_node_os3 = __toESM(require("os"), 1);
4300
+
4301
+ // src/utils/core-utils.js
4091
4302
  function sleepMs(ms) {
4092
4303
  return new Promise((resolve2) => setTimeout(resolve2, ms));
4093
4304
  }
@@ -4096,8 +4307,82 @@ function toJsonColumn(value) {
4096
4307
  return JSON.stringify(value);
4097
4308
  }
4098
4309
 
4099
- // src/tasks/taskUtils.ts
4310
+ // src/tasks/servicesRegistry.js
4311
+ var import_node_os = __toESM(require("os"), 1);
4312
+
4313
+ // src/tasks/taskUtils.js
4100
4314
  var import_node_crypto = require("crypto");
4315
+
4316
+ // src/tasks/time-matcher.js
4317
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4318
+ function resolveAsterisks(field, range) {
4319
+ return field.includes("*") ? field.replace("*", range) : field;
4320
+ }
4321
+ function resolveRanges(field) {
4322
+ const regex = /(\d+)-(\d+)/;
4323
+ let current = field;
4324
+ while (true) {
4325
+ const match = regex.exec(current);
4326
+ if (!match) break;
4327
+ const raw = match[0];
4328
+ let first = Number(match[1]);
4329
+ let last = Number(match[2]);
4330
+ if (last < first) {
4331
+ [first, last] = [last, first];
4332
+ }
4333
+ const values = [];
4334
+ for (let i = first; i <= last; i += 1) {
4335
+ values.push(i);
4336
+ }
4337
+ current = current.replace(raw, values.join(","));
4338
+ }
4339
+ return current;
4340
+ }
4341
+ function resolveSteps(field) {
4342
+ const match = /^(.+)\/(\d+)$/.exec(field);
4343
+ if (!match) return field;
4344
+ const base = match[1];
4345
+ const step = Number(match[2]);
4346
+ if (!Number.isFinite(step) || step <= 0) return field;
4347
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4348
+ }
4349
+ function convertPattern(pattern) {
4350
+ const parts = pattern.trim().split(/\s+/);
4351
+ if (parts.length !== 6) {
4352
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4353
+ }
4354
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4355
+ }
4356
+ function fieldMatches(field, value) {
4357
+ const allowed = field.split(",").map((v) => Number(v));
4358
+ return allowed.includes(value);
4359
+ }
4360
+ function matchesParsedPattern(parsed, date) {
4361
+ 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());
4362
+ }
4363
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4364
+ const parsed = convertPattern(pattern);
4365
+ return matchesParsedPattern(parsed, date);
4366
+ }
4367
+ var MS_PER_SECOND = 1e3;
4368
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
4369
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
4370
+ const parsed = convertPattern(pattern);
4371
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
4372
+ const end = t + maxSearchMs;
4373
+ while (t <= end) {
4374
+ const date = new Date(t);
4375
+ if (matchesParsedPattern(parsed, date)) {
4376
+ return date;
4377
+ }
4378
+ t += MS_PER_SECOND;
4379
+ }
4380
+ throw new Error(
4381
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
4382
+ );
4383
+ }
4384
+
4385
+ // src/tasks/taskUtils.js
4101
4386
  function getDb(context) {
4102
4387
  const db = context.db;
4103
4388
  if (!db) {
@@ -4105,105 +4390,117 @@ function getDb(context) {
4105
4390
  }
4106
4391
  return db;
4107
4392
  }
4108
- function queueToTableNames(queue) {
4109
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4110
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4111
- }
4393
+ function queueToTableNames(queueName) {
4394
+ return {
4395
+ tasksTable: queueName,
4396
+ historyTable: `${queueName}_history`,
4397
+ registryTable: `${queueName}_services_registry`
4398
+ };
4399
+ }
4400
+ function defineTasksTable(t, db, tableNameForIndex) {
4401
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4402
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4403
+ t.timestamp("started_at");
4404
+ t.timestamp("completed_at");
4405
+ t.integer("priority").notNullable().defaultTo(50);
4406
+ t.text("schedule");
4407
+ t.timestamp("next_run_at").defaultTo(null);
4408
+ t.timestamp("past_due").defaultTo(null);
4409
+ t.text("name").notNullable();
4410
+ t.text("opid");
4411
+ t.json("params");
4412
+ t.text("service_group");
4413
+ t.integer("instance_number");
4414
+ t.text("service_name");
4415
+ t.text("server_name");
4416
+ t.text("status").notNullable().defaultTo("idle");
4417
+ t.timestamp("status_changed_at").defaultTo(null);
4418
+ t.text("progress");
4419
+ t.boolean("success");
4420
+ t.json("results");
4421
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
4422
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
4423
+ }
4424
+ function taskHistoryInsertFromQueueRow(row, overrides) {
4425
+ const { id, ...snapshot } = row;
4426
+ void id;
4112
4427
  return {
4113
- tasksTable: queue,
4114
- historyTable: `${queue}_history`
4428
+ ...snapshot,
4429
+ ...overrides
4115
4430
  };
4116
4431
  }
4117
4432
  async function ensureTaskTables(context, options = {}) {
4118
- const queue = options.queue ?? "tasks";
4433
+ const queueName = options.queueName ?? "tasks";
4119
4434
  const recreate = options.recreate ?? false;
4120
4435
  const db = getDb(context);
4121
- const { tasksTable, historyTable } = queueToTableNames(queue);
4436
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
4122
4437
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
4123
4438
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
4439
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
4124
4440
  if (recreate) {
4125
4441
  await db.schema.dropTableIfExists(historyTable);
4126
4442
  await db.schema.dropTableIfExists(tasksTable);
4443
+ await db.schema.dropTableIfExists(registryTable);
4127
4444
  }
4128
4445
  if (needsTasks) {
4129
4446
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
4130
4447
  await db.schema.createTable(tasksTable, (t) => {
4131
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4132
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4133
- t.timestamp("started_at");
4134
- t.timestamp("completed_at");
4135
- t.integer("priority").notNullable().defaultTo(0);
4136
- t.text("schedule");
4137
- t.timestamp("past_due").defaultTo(null);
4138
- t.text("target").notNullable();
4139
- t.text("task").notNullable();
4140
- t.json("params");
4141
- t.text("opid");
4142
- t.timestamp("paused_at").defaultTo(null);
4143
- t.text("progress");
4144
- t.boolean("success");
4145
- t.json("results");
4146
- });
4147
- await db.schema.alterTable(tasksTable, (t) => {
4148
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
4149
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
4150
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
4151
- });
4152
- }
4153
- const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
4154
- if (!tasksHasOpid) {
4155
- await db.schema.alterTable(tasksTable, (t) => {
4156
- t.text("opid");
4157
- });
4158
- }
4159
- const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
4160
- if (!tasksHasPausedAt) {
4161
- await db.schema.alterTable(tasksTable, (t) => {
4162
- t.timestamp("paused_at").defaultTo(null);
4448
+ defineTasksTable(t, db, tasksTable);
4163
4449
  });
4164
4450
  }
4165
4451
  if (needsHistory) {
4166
4452
  await db.schema.createTable(historyTable, (t) => {
4167
- t.uuid("id").notNullable();
4168
- t.timestamp("created_at").notNullable();
4169
- t.timestamp("started_at");
4170
- t.timestamp("completed_at");
4171
- t.integer("priority").notNullable().defaultTo(0);
4172
- t.text("schedule");
4173
- t.timestamp("past_due").defaultTo(null);
4174
- t.text("target").notNullable();
4175
- t.text("task").notNullable();
4176
- t.json("params");
4177
- t.text("opid");
4178
- t.text("progress");
4179
- t.boolean("success");
4180
- t.json("results");
4181
- });
4182
- await db.schema.alterTable(historyTable, (t) => {
4183
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
4184
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
4453
+ defineTasksTable(t, db, historyTable);
4185
4454
  });
4186
4455
  }
4187
- const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
4188
- if (!historyHasOpid) {
4189
- await db.schema.alterTable(historyTable, (t) => {
4190
- t.text("opid");
4456
+ if (needsRegistry) {
4457
+ await db.schema.createTable(registryTable, (t) => {
4458
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4459
+ t.text("queue_name").notNullable();
4460
+ t.text("service_group").notNullable();
4461
+ t.integer("instance_number").notNullable().defaultTo(1);
4462
+ t.text("service_name").notNullable();
4463
+ t.text("server_name").notNullable();
4464
+ t.integer("pid");
4465
+ t.json("metadata");
4466
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4467
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
4468
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
4469
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
4470
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
4191
4471
  });
4192
4472
  }
4193
4473
  }
4194
4474
  async function enqueueTask(context, options) {
4195
4475
  const db = getDb(context);
4196
- const queue = options.queue ?? "tasks";
4197
- const { tasksTable } = queueToTableNames(queue);
4476
+ const queueName = options.queueName ?? "tasks";
4477
+ const { tasksTable } = queueToTableNames(queueName);
4198
4478
  const id = (0, import_node_crypto.randomUUID)();
4479
+ const name = options.name ?? options.task;
4480
+ if (!name) {
4481
+ throw new Error("enqueueTask: name (or task) is required");
4482
+ }
4483
+ const schedule = options.schedule?.trim() ? options.schedule : null;
4484
+ let nextRunAt = null;
4485
+ if (options.nextRunAt !== void 0) {
4486
+ nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
4487
+ } else if (schedule) {
4488
+ nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
4489
+ }
4199
4490
  await db(tasksTable).insert({
4200
4491
  id,
4201
- target: options.target,
4202
- task: options.task,
4492
+ name,
4203
4493
  params: toJsonColumn(options.params ?? null),
4204
4494
  opid: options.opid ?? null,
4205
- priority: options.priority ?? 0,
4206
- schedule: options.schedule ?? null
4495
+ priority: options.priority ?? 50,
4496
+ schedule,
4497
+ next_run_at: nextRunAt,
4498
+ service_group: options.serviceGroup ?? null,
4499
+ instance_number: options.instanceNumber ?? null,
4500
+ service_name: options.serviceName ?? null,
4501
+ server_name: options.serverName ?? null,
4502
+ status: "idle",
4503
+ status_changed_at: db.fn.now()
4207
4504
  });
4208
4505
  return id;
4209
4506
  }
@@ -4214,59 +4511,397 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
4214
4511
  });
4215
4512
  }
4216
4513
 
4217
- // src/tasks/taskLogs.ts
4218
- function getLogsState(context) {
4219
- const holder = context;
4220
- if (holder.__tasksLogsState) return holder.__tasksLogsState;
4221
- const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
4222
- const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
4223
- const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
4224
- const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
4225
- const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4226
- const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4227
- const errorDb = new FileDatabase({
4228
- basePath,
4229
- namespace,
4230
- tableName: errorTableName,
4231
- versioned: true,
4232
- useMetadata: true,
4233
- maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4234
- pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4235
- logger: holder.logger
4236
- });
4237
- const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4238
- const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4239
- if (!enabled) {
4240
- const disabledState = {
4241
- db: null,
4242
- errorDb,
4243
- queue: Promise.resolve(),
4244
- initialized: true,
4245
- errorInitialized: false
4246
- };
4247
- holder.__tasksLogsState = disabledState;
4248
- return disabledState;
4514
+ // src/tasks/servicesRegistry.js
4515
+ function getDb2(context) {
4516
+ const db = context.db;
4517
+ if (!db) {
4518
+ throw new Error("Services registry requires context.db");
4249
4519
  }
4250
- const db = new FileDatabase({
4251
- basePath,
4252
- namespace,
4253
- tableName,
4254
- versioned: true,
4255
- useMetadata: true,
4256
- maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4257
- pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4258
- logger: holder.logger
4259
- });
4260
- const state = {
4261
- db,
4262
- errorDb,
4263
- queue: Promise.resolve(),
4264
- initialized: false,
4520
+ return db;
4521
+ }
4522
+ function parseMetadataColumn(value) {
4523
+ if (!value) return {};
4524
+ if (typeof value === "object" && !Array.isArray(value)) return value;
4525
+ if (typeof value === "string") {
4526
+ try {
4527
+ const p = JSON.parse(value);
4528
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
4529
+ } catch {
4530
+ return {};
4531
+ }
4532
+ }
4533
+ return {};
4534
+ }
4535
+ var DEFAULT_GROUP_MAX_INSTANCES = {
4536
+ intake: 1,
4537
+ harvest: 1,
4538
+ harvester: 0,
4539
+ loader: 0,
4540
+ photos: 0,
4541
+ photosprocessor: 0,
4542
+ ingest: 0
4543
+ };
4544
+ function sanitizeNamePart(raw) {
4545
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4546
+ return s.slice(0, 80) || "runner";
4547
+ }
4548
+ function resolveMaxInstances(serviceGroup, override) {
4549
+ if (override !== void 0 && Number.isFinite(override)) {
4550
+ return Math.max(0, Math.floor(Number(override)));
4551
+ }
4552
+ const g = serviceGroup.trim().toLowerCase();
4553
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
4554
+ }
4555
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
4556
+ const cutoff = new Date(Date.now() - staleMs);
4557
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
4558
+ if (excludeRowId) {
4559
+ q = q.whereNot("id", excludeRowId);
4560
+ }
4561
+ const row = await q.count("id as count").first();
4562
+ return Number(row?.count ?? 0);
4563
+ }
4564
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
4565
+ const cutoff = new Date(Date.now() - staleMs);
4566
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
4567
+ const set = /* @__PURE__ */ new Set();
4568
+ for (const r of rows) {
4569
+ const n = Number(r.instance_number);
4570
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
4571
+ }
4572
+ return set;
4573
+ }
4574
+ function isUniqueViolation(error) {
4575
+ const code = error?.code ?? error?.errno;
4576
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
4577
+ }
4578
+ function buildMetadata(options) {
4579
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
4580
+ if (options.target) {
4581
+ base.runnerTarget = options.target;
4582
+ }
4583
+ return toJsonColumn(Object.keys(base).length ? base : null);
4584
+ }
4585
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
4586
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
4587
+ const e = Math.max(1, Math.floor(Number(explicit)));
4588
+ if (occupied.has(e)) {
4589
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
4590
+ }
4591
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
4592
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
4593
+ }
4594
+ return e;
4595
+ }
4596
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
4597
+ for (let n = 1; n <= cap; n++) {
4598
+ if (!occupied.has(n)) return n;
4599
+ }
4600
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
4601
+ }
4602
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
4603
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
4604
+ }
4605
+ async function registerInServicesRegistry(context, options) {
4606
+ const db = getDb2(context);
4607
+ const registryTable = queueToTableNames(options.queueName).registryTable;
4608
+ const serviceGroup = options.serviceGroup.trim();
4609
+ if (!serviceGroup) {
4610
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
4611
+ }
4612
+ const serverName = import_node_os.default.hostname();
4613
+ const pid = typeof process.pid === "number" ? process.pid : null;
4614
+ const meta = buildMetadata(options);
4615
+ const groupBase = sanitizeNamePart(serviceGroup);
4616
+ const hostBase = sanitizeNamePart(serverName);
4617
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
4618
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
4619
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
4620
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
4621
+ if (options.enforceMaxInstances) {
4622
+ throw new Error(msg);
4623
+ }
4624
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
4625
+ }
4626
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
4627
+ const cutoff = new Date(Date.now() - options.staleMs);
4628
+ const MAX_ATTEMPTS = 8;
4629
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
4630
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
4631
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
4632
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
4633
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
4634
+ if (existing) {
4635
+ const lastSeen = new Date(existing.last_seen_at);
4636
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
4637
+ if (isAlive) {
4638
+ if (options.serviceName?.trim()) {
4639
+ throw new Error(
4640
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
4641
+ );
4642
+ }
4643
+ context.logger.warn?.(
4644
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
4645
+ );
4646
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
4647
+ throw new Error(
4648
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
4649
+ );
4650
+ }
4651
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
4652
+ continue;
4653
+ }
4654
+ await db(registryTable).where({ id: existing.id }).update({
4655
+ server_name: serverName,
4656
+ pid,
4657
+ metadata: meta,
4658
+ service_group: serviceGroup,
4659
+ instance_number: instanceNumber,
4660
+ last_seen_at: db.fn.now()
4661
+ });
4662
+ const reg = {
4663
+ serviceName: serviceNameRaw,
4664
+ serviceGroup,
4665
+ queueName: options.queueName,
4666
+ target: options.target,
4667
+ rowId: String(existing.id),
4668
+ registryTable,
4669
+ instanceNumber
4670
+ };
4671
+ context.servicesRegistry = reg;
4672
+ context.runnerHeartbeat = reg;
4673
+ context.logger.info?.(
4674
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
4675
+ );
4676
+ return reg;
4677
+ }
4678
+ try {
4679
+ const rows = await db(registryTable).insert({
4680
+ queue_name: options.queueName,
4681
+ service_group: serviceGroup,
4682
+ instance_number: instanceNumber,
4683
+ service_name: serviceNameRaw,
4684
+ server_name: serverName,
4685
+ pid,
4686
+ metadata: meta,
4687
+ last_seen_at: db.fn.now(),
4688
+ created_at: db.fn.now()
4689
+ }).returning(["id", "service_name"]);
4690
+ const row = Array.isArray(rows) ? rows[0] : rows;
4691
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
4692
+ if (!rowId) {
4693
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
4694
+ rowId = again?.id != null ? String(again.id) : "";
4695
+ }
4696
+ if (!rowId) continue;
4697
+ const regNew = {
4698
+ serviceName: String(row?.service_name ?? serviceNameRaw),
4699
+ serviceGroup,
4700
+ queueName: options.queueName,
4701
+ target: options.target,
4702
+ rowId,
4703
+ registryTable,
4704
+ instanceNumber
4705
+ };
4706
+ context.servicesRegistry = regNew;
4707
+ context.runnerHeartbeat = regNew;
4708
+ context.logger.info?.(
4709
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
4710
+ );
4711
+ return regNew;
4712
+ } catch (error) {
4713
+ if (!isUniqueViolation(error)) {
4714
+ throw error;
4715
+ }
4716
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
4717
+ }
4718
+ }
4719
+ throw new Error(
4720
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
4721
+ );
4722
+ }
4723
+ async function touchServicesRegistry(context, registration) {
4724
+ const db = getDb2(context);
4725
+ const serverName = import_node_os.default.hostname();
4726
+ const pid = typeof process.pid === "number" ? process.pid : null;
4727
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4728
+ last_seen_at: db.fn.now(),
4729
+ server_name: serverName,
4730
+ pid
4731
+ });
4732
+ }
4733
+ async function updateServicesRegistryMetadata(context, registration, patch) {
4734
+ const db = getDb2(context);
4735
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
4736
+ const prev = parseMetadataColumn(row?.metadata);
4737
+ const merged = { ...prev, ...patch };
4738
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4739
+ metadata: toJsonColumn(merged),
4740
+ last_seen_at: db.fn.now()
4741
+ });
4742
+ context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
4743
+ }
4744
+ async function unregisterServicesRegistry(context, registration) {
4745
+ const db = getDb2(context);
4746
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
4747
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
4748
+ }
4749
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
4750
+ const db = getDb2(context);
4751
+ const staleMs = options.staleMs ?? 6e4;
4752
+ const cutoff = new Date(Date.now() - staleMs);
4753
+ const table = queueToTableNames(options.queueName).registryTable;
4754
+ let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
4755
+ if (options.serviceGroup?.trim()) {
4756
+ q = q.where({ service_group: options.serviceGroup.trim() });
4757
+ }
4758
+ return await q;
4759
+ }
4760
+
4761
+ // src/tasks/taskLogs.js
4762
+ var import_node_path = __toESM(require("path"), 1);
4763
+ function getLogsState(context) {
4764
+ const holder = context;
4765
+ if (holder.__tasksLogsState) return holder.__tasksLogsState;
4766
+ const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
4767
+ const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
4768
+ const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
4769
+ const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
4770
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4771
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4772
+ const errorDb = new FileDatabase({
4773
+ basePath,
4774
+ namespace,
4775
+ tableName: errorTableName,
4776
+ versioned: true,
4777
+ useMetadata: true,
4778
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4779
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4780
+ logger: holder.logger
4781
+ });
4782
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4783
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4784
+ if (!enabled) {
4785
+ const disabledState = {
4786
+ db: null,
4787
+ errorDb,
4788
+ queue: Promise.resolve(),
4789
+ initialized: true,
4790
+ errorInitialized: false
4791
+ };
4792
+ holder.__tasksLogsState = disabledState;
4793
+ return disabledState;
4794
+ }
4795
+ const db = new FileDatabase({
4796
+ basePath,
4797
+ namespace,
4798
+ tableName,
4799
+ versioned: true,
4800
+ useMetadata: true,
4801
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4802
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4803
+ logger: holder.logger
4804
+ });
4805
+ const state = {
4806
+ db,
4807
+ errorDb,
4808
+ queue: Promise.resolve(),
4809
+ initialized: false,
4265
4810
  errorInitialized: false
4266
4811
  };
4267
4812
  holder.__tasksLogsState = state;
4268
4813
  return state;
4269
4814
  }
4815
+ function ipcLogTargetKey(target) {
4816
+ const bp = target.basePath ?? "";
4817
+ const ns = target.namespace ?? "";
4818
+ return `${bp}::${ns}::${target.tableName}`;
4819
+ }
4820
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
4821
+ const seg = (s) => {
4822
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
4823
+ return t.length ? t : "x";
4824
+ };
4825
+ return `${seg(source)}/${seg(resource)}`;
4826
+ }
4827
+ async function readTaskIpcLogsSnapshot(context, options) {
4828
+ const holder = context;
4829
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
4830
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
4831
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
4832
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
4833
+ const fd = new FileDatabase({
4834
+ basePath,
4835
+ namespace,
4836
+ tableName,
4837
+ versioned: true,
4838
+ useMetadata: true,
4839
+ maxVersions: 30,
4840
+ pageSize: 2e3,
4841
+ logger: holder.logger
4842
+ });
4843
+ const versions = await fd.getVersions();
4844
+ if (versions.length === 0) {
4845
+ return { records: [], latestTs: null };
4846
+ }
4847
+ const latest = versions[versions.length - 1];
4848
+ const raw = await fd.read({ version: latest });
4849
+ const arr = Array.isArray(raw) ? raw : [];
4850
+ let filtered = arr;
4851
+ if (options.afterTs && String(options.afterTs).trim()) {
4852
+ const cut = String(options.afterTs).trim();
4853
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
4854
+ }
4855
+ let latestTs = null;
4856
+ for (const r of filtered) {
4857
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
4858
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
4859
+ }
4860
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
4861
+ const maxReturn = incremental ? 1e4 : tail;
4862
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
4863
+ return { records: sliced, latestTs };
4864
+ }
4865
+ function resolveIpcFileLogsDir(context, target) {
4866
+ const holder = context;
4867
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4868
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4869
+ const segments = target.tableName.split("/").filter(Boolean);
4870
+ return import_node_path.default.resolve(basePath, namespace, ...segments);
4871
+ }
4872
+ function getLogsStateForTarget(context, target) {
4873
+ const holder = context;
4874
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4875
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4876
+ if (!enabled) return null;
4877
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
4878
+ const map = holder.__tasksLogsTargetStates;
4879
+ const key = ipcLogTargetKey(target);
4880
+ if (map.has(key)) return map.get(key);
4881
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4882
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4883
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4884
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4885
+ const db = new FileDatabase({
4886
+ basePath,
4887
+ namespace,
4888
+ tableName: target.tableName,
4889
+ versioned: true,
4890
+ useMetadata: true,
4891
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4892
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4893
+ logger: holder.logger
4894
+ });
4895
+ const state = {
4896
+ db,
4897
+ errorDb: null,
4898
+ queue: Promise.resolve(),
4899
+ initialized: false,
4900
+ errorInitialized: false
4901
+ };
4902
+ map.set(key, state);
4903
+ return state;
4904
+ }
4270
4905
  function isErrorPayload(payload) {
4271
4906
  if (!payload) return false;
4272
4907
  if (typeof payload === "object") {
@@ -4286,14 +4921,26 @@ function buildLogRecord(task, payload) {
4286
4921
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4287
4922
  opid: task.opid ?? null,
4288
4923
  taskId: task.id,
4289
- taskName: task.task,
4290
- target: task.target,
4924
+ taskName: task.name,
4925
+ target: task.service_group,
4291
4926
  source: typeof params.source === "string" ? params.source : null,
4292
4927
  resource: typeof params.resource === "string" ? params.resource : null,
4293
4928
  payload
4294
4929
  };
4295
4930
  }
4296
- function appendTaskIpcLog(context, task, payload) {
4931
+ function appendTaskIpcLog(context, task, payload, target) {
4932
+ if (target) {
4933
+ const state2 = getLogsStateForTarget(context, target);
4934
+ if (!state2?.db) return;
4935
+ const record2 = buildLogRecord(task, payload);
4936
+ state2.queue = state2.queue.then(async () => {
4937
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
4938
+ state2.initialized = true;
4939
+ }).catch((error) => {
4940
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
4941
+ });
4942
+ return;
4943
+ }
4297
4944
  const state = getLogsState(context);
4298
4945
  if (!state.db && !state.errorDb) return;
4299
4946
  const record = buildLogRecord(task, payload);
@@ -4310,84 +4957,293 @@ function appendTaskIpcLog(context, task, payload) {
4310
4957
  context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
4311
4958
  });
4312
4959
  }
4313
-
4314
- // src/tasks/time-matcher.ts
4315
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4316
- function resolveAsterisks(field, range) {
4317
- return field.includes("*") ? field.replace("*", range) : field;
4318
- }
4319
- function resolveRanges(field) {
4320
- const regex = /(\d+)-(\d+)/;
4321
- let current = field;
4322
- while (true) {
4323
- const match = regex.exec(current);
4324
- if (!match) break;
4325
- const raw = match[0];
4326
- let first = Number(match[1]);
4327
- let last = Number(match[2]);
4328
- if (last < first) {
4329
- [first, last] = [last, first];
4330
- }
4331
- const values = [];
4332
- for (let i = first; i <= last; i += 1) {
4333
- values.push(i);
4960
+ async function flushTaskIpcLogs(context) {
4961
+ const holder = context;
4962
+ const promises = [];
4963
+ if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
4964
+ const map = holder.__tasksLogsTargetStates;
4965
+ if (map) {
4966
+ for (const s of map.values()) {
4967
+ if (s.queue) promises.push(s.queue);
4334
4968
  }
4335
- current = current.replace(raw, values.join(","));
4336
4969
  }
4337
- return current;
4338
- }
4339
- function resolveSteps(field) {
4340
- const match = /^(.+)\/(\d+)$/.exec(field);
4341
- if (!match) return field;
4342
- const base = match[1];
4343
- const step = Number(match[2]);
4344
- if (!Number.isFinite(step) || step <= 0) return field;
4345
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4346
- }
4347
- function convertPattern(pattern) {
4348
- const parts = pattern.trim().split(/\s+/);
4349
- if (parts.length !== 6) {
4350
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4351
- }
4352
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4353
- }
4354
- function fieldMatches(field, value) {
4355
- const allowed = field.split(",").map((v) => Number(v));
4356
- return allowed.includes(value);
4357
- }
4358
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4359
- const parsed = convertPattern(pattern);
4360
- 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());
4970
+ await Promise.all(promises);
4361
4971
  }
4362
4972
 
4363
- // src/tasks/TaskMaster.ts
4364
- var TaskMaster = class {
4365
- context;
4366
- task;
4973
+ // src/tasks/AbstractTask.js
4974
+ var AbstractTask = class _AbstractTask {
4975
+ /**
4976
+ * Whether `send-task` should wait for completion (and print a result
4977
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
4978
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
4979
+ *
4980
+ * @type {boolean}
4981
+ */
4982
+ static defaultWaitForResult = false;
4983
+ /**
4984
+ * @param {object} context Runner context (db, logger, params, emitter...).
4985
+ * @param {object} task Task row as claimed from the queue.
4986
+ */
4367
4987
  constructor(context, task) {
4368
4988
  this.context = context;
4369
4989
  this.task = task;
4370
4990
  }
4991
+ /**
4992
+ * Return a short reason string when the task should be deferred (e.g. "locked
4993
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
4994
+ *
4995
+ * @returns {string | false | Promise<string | false>}
4996
+ */
4371
4997
  cantRunReason() {
4372
4998
  return false;
4373
4999
  }
5000
+ /**
5001
+ * Called by the runner when a stop has been requested. Subclasses running
5002
+ * long loops should flip a flag here and check it between iterations.
5003
+ *
5004
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
5005
+ */
4374
5006
  requestStop(_allowanceMs) {
4375
5007
  }
5008
+ /**
5009
+ * Perform the task. Must be implemented by subclasses.
5010
+ *
5011
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
5012
+ * Updates the DB `progress` column. Accepts any serializable value;
5013
+ * strings are stored verbatim, objects are JSON-stringified.
5014
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5015
+ */
5016
+ async run(_reportProgress) {
5017
+ throw new Error("AbstractTask.run must be implemented by subclass");
5018
+ }
5019
+ /**
5020
+ * Resolve a complete row payload for this task — envelope fields (queue,
5021
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
5022
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
5023
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
5024
+ *
5025
+ * const payload = await TaskClass.resolveParams(context, { name });
5026
+ * await enqueueTask(context, payload);
5027
+ *
5028
+ * Validation failures throw {@link ParamError} so the script aborts before
5029
+ * a malformed row hits the DB.
5030
+ *
5031
+ * @param {object} context
5032
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
5033
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
5034
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
5035
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
5036
+ * @returns {Promise<object>}
5037
+ */
5038
+ static async resolveParams(context, overrides = {}) {
5039
+ const main = _AbstractTask._resolveMainFields(context, overrides);
5040
+ const params = await this.resolveCustomParams(context, overrides);
5041
+ return { ...main, params };
5042
+ }
5043
+ /**
5044
+ * Resolve the inner JSON blob stored in the `params` column. Default
5045
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
5046
+ * overlaid with `overrides.params` when supplied; returns `null` when
5047
+ * neither is provided.
5048
+ *
5049
+ * Subclasses with typed fields should override and call
5050
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
5051
+ * resolution, then validate and throw {@link ParamError} on bad input.
5052
+ *
5053
+ * @param {object} context
5054
+ * @param {Record<string, unknown>} [overrides]
5055
+ * @returns {Promise<object|null>}
5056
+ */
5057
+ static async resolveCustomParams(context, overrides = {}) {
5058
+ return _AbstractTask._defaultParamsBlob(context, overrides);
5059
+ }
5060
+ /**
5061
+ * Read main task envelope fields from `context.params` (CLI/env), with
5062
+ * any matching key on `overrides` taking precedence. Internal; called by
5063
+ * {@link AbstractTask.resolveParams}.
5064
+ *
5065
+ * @param {object} context
5066
+ * @param {Record<string, unknown>} [overrides]
5067
+ * @returns {object}
5068
+ */
5069
+ static _resolveMainFields(context, overrides = {}) {
5070
+ const defs = {
5071
+ queueName: "string default tasks",
5072
+ priority: "number default 50",
5073
+ serviceGroup: "string",
5074
+ serviceName: "string",
5075
+ instanceNumber: "number",
5076
+ serverName: "string",
5077
+ opid: "string",
5078
+ schedule: "string"
5079
+ };
5080
+ const cli = context.params.getAllForModule("task-envelope", defs);
5081
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
5082
+ if (!name) {
5083
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5084
+ }
5085
+ let instanceNumber;
5086
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
5087
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
5088
+ const n = Number(rawInstance);
5089
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
5090
+ throw new ParamError("--instanceNumber must be a positive integer when set");
5091
+ }
5092
+ instanceNumber = n;
5093
+ } else {
5094
+ instanceNumber = null;
5095
+ }
5096
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
5097
+ const priority = Number(priorityRaw);
5098
+ if (!Number.isFinite(priority)) {
5099
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
5100
+ }
5101
+ return {
5102
+ name,
5103
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
5104
+ priority,
5105
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
5106
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
5107
+ instanceNumber,
5108
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
5109
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
5110
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
5111
+ nextRunAt: overrides.nextRunAt ?? null
5112
+ };
5113
+ }
5114
+ /**
5115
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
5116
+ * object), then overlays `overrides.params` on top. Returns `null` when
5117
+ * neither is provided.
5118
+ *
5119
+ * @param {object} context
5120
+ * @param {Record<string, unknown>} [overrides]
5121
+ * @returns {object|null}
5122
+ */
5123
+ static _defaultParamsBlob(context, overrides = {}) {
5124
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
5125
+ const fromJson = parseParamsJson(cli.paramsJson);
5126
+ const fromOverride = pickParamsObject(overrides);
5127
+ if (!fromJson && !fromOverride) return null;
5128
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
5129
+ }
5130
+ /**
5131
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
5132
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
5133
+ * merges them with explicit `overrides.params` in increasing priority:
5134
+ *
5135
+ * typed CLI flags → --paramsJson → overrides.params
5136
+ *
5137
+ * Undefined values are dropped so defaults declared in `defs` aren't
5138
+ * overwritten by missing-flag noise. Returns the merged object; the
5139
+ * caller is responsible for validation and throwing `ParamError`.
5140
+ *
5141
+ * @param {object} context
5142
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
5143
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
5144
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
5145
+ * @returns {Record<string, unknown>}
5146
+ */
5147
+ static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
5148
+ const fullDefs = { ...defs, paramsJson: "string" };
5149
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
5150
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
5151
+ const fromCli = {};
5152
+ for (const [k, v] of Object.entries(cliRaw)) {
5153
+ if (k === "paramsJson") continue;
5154
+ if (v !== void 0 && v !== null) fromCli[k] = v;
5155
+ }
5156
+ const fromOverride = pickParamsObject(overrides) ?? {};
5157
+ return { ...fromCli, ...fromJson, ...fromOverride };
5158
+ }
4376
5159
  };
5160
+ function emptyToUndef(s) {
5161
+ if (s === void 0 || s === null) return void 0;
5162
+ if (typeof s !== "string") return s;
5163
+ const t = s.trim();
5164
+ return t.length ? t : void 0;
5165
+ }
5166
+ function parseParamsJson(raw) {
5167
+ if (raw == null) return null;
5168
+ const t = String(raw).trim();
5169
+ if (!t) return null;
5170
+ let parsed;
5171
+ try {
5172
+ parsed = JSON.parse(t);
5173
+ } catch (e) {
5174
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
5175
+ }
5176
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5177
+ throw new ParamError("--paramsJson must be a JSON object");
5178
+ }
5179
+ return parsed;
5180
+ }
5181
+ function pickParamsObject(overrides) {
5182
+ const p = overrides?.params;
5183
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
5184
+ return void 0;
5185
+ }
4377
5186
 
4378
- // src/tasks/coreTasks/TaskPing.ts
4379
- var TaskPing = class extends TaskMaster {
5187
+ // src/tasks/coreTasks/TaskPing.js
5188
+ var TaskPing = class extends AbstractTask {
5189
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
5190
+ static defaultWaitForResult = true;
5191
+ /** Ping takes no params. */
5192
+ static async resolveCustomParams() {
5193
+ return null;
5194
+ }
5195
+ /**
5196
+ * @returns {Promise<{ success: true, results: "pong" }>}
5197
+ */
4380
5198
  async run() {
4381
5199
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4382
5200
  return { success: true, results: "pong" };
4383
5201
  }
4384
5202
  };
4385
5203
 
4386
- // src/tasks/coreTasks/TaskSampleProcess.ts
4387
- var TaskSampleProcess = class extends TaskMaster {
4388
- stopRequested = false;
4389
- stopAllowanceMs = 0;
4390
- stopDecisionLogged = false;
5204
+ // src/tasks/coreTasks/TaskSampleProcess.js
5205
+ var TaskSampleProcess = class extends AbstractTask {
5206
+ /**
5207
+ * @param {object} context
5208
+ * @param {Record<string, unknown>} [overrides]
5209
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
5210
+ */
5211
+ static async resolveCustomParams(context, overrides = {}) {
5212
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
5213
+ total: "number default 10",
5214
+ delay: "number default 1000",
5215
+ name: "string"
5216
+ }, overrides);
5217
+ const total = Number(merged.total);
5218
+ const delay = Number(merged.delay);
5219
+ if (!Number.isInteger(total) || total <= 0) {
5220
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
5221
+ }
5222
+ if (!Number.isInteger(delay) || delay < 0) {
5223
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
5224
+ }
5225
+ const out = { total, delay };
5226
+ if (typeof merged.name === "string" && merged.name.trim()) {
5227
+ out.name = merged.name.trim();
5228
+ }
5229
+ return out;
5230
+ }
5231
+ /**
5232
+ * @param {object} context
5233
+ * @param {object} task
5234
+ */
5235
+ constructor(context, task) {
5236
+ super(context, task);
5237
+ this.stopRequested = false;
5238
+ this.stopAllowanceMs = 0;
5239
+ this.stopDecisionLogged = false;
5240
+ }
5241
+ /**
5242
+ * Runner-facing stop signal. Records the allowance window so the main loop
5243
+ * can decide per-iteration whether to finish or abort early.
5244
+ *
5245
+ * @param {number} allowanceMs
5246
+ */
4391
5247
  requestStop(allowanceMs) {
4392
5248
  this.stopRequested = true;
4393
5249
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -4395,6 +5251,14 @@ var TaskSampleProcess = class extends TaskMaster {
4395
5251
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4396
5252
  );
4397
5253
  }
5254
+ /**
5255
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
5256
+ * progress every iteration. Validates params up front; invalid values short-
5257
+ * circuit to a structured failure without starting the loop.
5258
+ *
5259
+ * @param {(progress: object) => Promise<void>} reportProgress
5260
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5261
+ */
4398
5262
  async run(reportProgress) {
4399
5263
  const totalRaw = this.task?.params?.total ?? 10;
4400
5264
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -4476,7 +5340,7 @@ var TaskSampleProcess = class extends TaskMaster {
4476
5340
  }
4477
5341
  };
4478
5342
 
4479
- // src/tasks/coreTasks/TaskShellCommand.ts
5343
+ // src/tasks/coreTasks/TaskShellCommand.js
4480
5344
  var import_node_child_process = require("child_process");
4481
5345
  function runShellCommand(command, cwd) {
4482
5346
  return new Promise((resolve2, reject) => {
@@ -4506,7 +5370,27 @@ function runShellCommand(command, cwd) {
4506
5370
  });
4507
5371
  });
4508
5372
  }
4509
- var TaskShellCommand = class extends TaskMaster {
5373
+ var TaskShellCommand = class extends AbstractTask {
5374
+ /**
5375
+ * @param {object} context
5376
+ * @param {Record<string, unknown>} [overrides]
5377
+ * @returns {Promise<{ command: string, cwd?: string }>}
5378
+ */
5379
+ static async resolveCustomParams(context, overrides = {}) {
5380
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
5381
+ command: "string",
5382
+ cwd: "string"
5383
+ }, overrides);
5384
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
5385
+ if (!command) {
5386
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
5387
+ }
5388
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
5389
+ return cwd ? { command, cwd } : { command };
5390
+ }
5391
+ /**
5392
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5393
+ */
4510
5394
  async run() {
4511
5395
  const params = this.task?.params;
4512
5396
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -4555,8 +5439,8 @@ var TaskShellCommand = class extends TaskMaster {
4555
5439
  }
4556
5440
  };
4557
5441
 
4558
- // src/tasks/coreTasks/TaskSystemInfo.ts
4559
- var import_node_os = __toESM(require("os"), 1);
5442
+ // src/tasks/coreTasks/TaskSystemInfo.js
5443
+ var import_node_os2 = __toESM(require("os"), 1);
4560
5444
  var import_promises = __toESM(require("fs/promises"), 1);
4561
5445
  function toGb(valueBytes) {
4562
5446
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
@@ -4575,13 +5459,22 @@ async function getDiskStats() {
4575
5459
  free: toGb(free)
4576
5460
  };
4577
5461
  }
4578
- var TaskSystemInfo = class extends TaskMaster {
5462
+ var TaskSystemInfo = class extends AbstractTask {
5463
+ /** Same UX expectation as `ping` — short probe, print the result. */
5464
+ static defaultWaitForResult = true;
5465
+ /** systemInfo takes no params. */
5466
+ static async resolveCustomParams() {
5467
+ return null;
5468
+ }
5469
+ /**
5470
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5471
+ */
4579
5472
  async run() {
4580
5473
  try {
4581
- const totalMemory = import_node_os.default.totalmem();
4582
- const freeMemory = import_node_os.default.freemem();
5474
+ const totalMemory = import_node_os2.default.totalmem();
5475
+ const freeMemory = import_node_os2.default.freemem();
4583
5476
  const usedMemory = totalMemory - freeMemory;
4584
- const cpus = import_node_os.default.cpus();
5477
+ const cpus = import_node_os2.default.cpus();
4585
5478
  const cpuUtilization = cpus.map((cpu) => {
4586
5479
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
4587
5480
  const usage = (total - cpu.times.idle) / total * 100;
@@ -4607,10 +5500,10 @@ var TaskSystemInfo = class extends TaskMaster {
4607
5500
  utilization: cpuUtilization
4608
5501
  },
4609
5502
  runtime: {
4610
- platform: import_node_os.default.platform(),
4611
- arch: import_node_os.default.arch(),
4612
- uptimeSec: import_node_os.default.uptime(),
4613
- hostname: import_node_os.default.hostname()
5503
+ platform: import_node_os2.default.platform(),
5504
+ arch: import_node_os2.default.arch(),
5505
+ uptimeSec: import_node_os2.default.uptime(),
5506
+ hostname: import_node_os2.default.hostname()
4614
5507
  }
4615
5508
  };
4616
5509
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -4627,8 +5520,31 @@ var TaskSystemInfo = class extends TaskMaster {
4627
5520
  }
4628
5521
  };
4629
5522
 
4630
- // src/tasks/coreTasks/TaskSumAB.ts
4631
- var TaskSumAB = class extends TaskMaster {
5523
+ // src/tasks/coreTasks/TaskSumAB.js
5524
+ var TaskSumAB = class extends AbstractTask {
5525
+ /** Short, deterministic — wait by default so callers see the sum. */
5526
+ static defaultWaitForResult = true;
5527
+ /**
5528
+ * @param {object} context
5529
+ * @param {Record<string, unknown>} [overrides]
5530
+ * @returns {Promise<{ a: number, b: number }>}
5531
+ */
5532
+ static async resolveCustomParams(context, overrides = {}) {
5533
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
5534
+ a: "number",
5535
+ b: "number"
5536
+ }, overrides);
5537
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
5538
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
5539
+ }
5540
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
5541
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
5542
+ }
5543
+ return { a: merged.a, b: merged.b };
5544
+ }
5545
+ /**
5546
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5547
+ */
4632
5548
  async run() {
4633
5549
  const a = this.task?.params?.a;
4634
5550
  const b = this.task?.params?.b;
@@ -4659,8 +5575,46 @@ var TaskSumAB = class extends TaskMaster {
4659
5575
  }
4660
5576
  };
4661
5577
 
4662
- // src/tasks/coreTasks/TaskStopRunner.ts
4663
- var TaskStopRunner = class extends TaskMaster {
5578
+ // src/tasks/coreTasks/TaskStopRunner.js
5579
+ var TaskStopRunner = class extends AbstractTask {
5580
+ /**
5581
+ * Stop tasks must target a concrete instance — without `serviceName` the
5582
+ * row would race against any worker on the queue. Layered on top of the
5583
+ * envelope built by {@link AbstractTask.resolveParams}.
5584
+ *
5585
+ * @param {object} context
5586
+ * @param {Record<string, unknown>} [overrides]
5587
+ * @returns {Promise<object>}
5588
+ */
5589
+ static async resolveParams(context, overrides = {}) {
5590
+ const main = await super.resolveParams(context, overrides);
5591
+ if (!main.serviceName) {
5592
+ throw new ParamError(
5593
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
5594
+ );
5595
+ }
5596
+ return main;
5597
+ }
5598
+ /**
5599
+ * @param {object} context
5600
+ * @param {Record<string, unknown>} [overrides]
5601
+ * @returns {Promise<{ allowanceMs: number }>}
5602
+ */
5603
+ static async resolveCustomParams(context, overrides = {}) {
5604
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
5605
+ allowanceMs: "number default 5000"
5606
+ }, overrides);
5607
+ const allowanceMs = Number(merged.allowanceMs);
5608
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
5609
+ throw new ParamError(
5610
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
5611
+ );
5612
+ }
5613
+ return { allowanceMs };
5614
+ }
5615
+ /**
5616
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
5617
+ */
4664
5618
  async run() {
4665
5619
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4666
5620
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -4675,175 +5629,399 @@ var TaskStopRunner = class extends TaskMaster {
4675
5629
  }
4676
5630
  };
4677
5631
 
4678
- // src/tasks/TasksRegistry.ts
5632
+ // src/tasks/coreTasks/TaskGetLogs.js
5633
+ var TaskGetLogs = class extends AbstractTask {
5634
+ /**
5635
+ * @param {object} context
5636
+ * @param {Record<string, unknown>} [overrides]
5637
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
5638
+ */
5639
+ static async resolveCustomParams(context, overrides = {}) {
5640
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
5641
+ source: "string",
5642
+ resource: "string",
5643
+ tail: "number default 100",
5644
+ afterTs: "string"
5645
+ }, overrides);
5646
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
5647
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
5648
+ if (!source) throw new ParamError('getLogs: param "source" is required');
5649
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
5650
+ let tail = Number(merged.tail);
5651
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
5652
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
5653
+ const out = { source, resource, tail };
5654
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
5655
+ out.afterTs = merged.afterTs.trim();
5656
+ }
5657
+ return out;
5658
+ }
5659
+ /**
5660
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
5661
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5662
+ */
5663
+ async run(_reportProgress) {
5664
+ const p = this.task.params ?? {};
5665
+ const source = String(p.source ?? "").trim();
5666
+ const resource = String(p.resource ?? "").trim();
5667
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
5668
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
5669
+ if (!source || !resource) {
5670
+ return {
5671
+ success: false,
5672
+ results: { error: 'getLogs requires params "source" and "resource"' }
5673
+ };
5674
+ }
5675
+ try {
5676
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
5677
+ source,
5678
+ resource,
5679
+ tail,
5680
+ afterTs
5681
+ });
5682
+ return {
5683
+ success: true,
5684
+ results: { records, latestTs, source, resource }
5685
+ };
5686
+ } catch (e) {
5687
+ return {
5688
+ success: false,
5689
+ results: { error: e?.message ?? String(e) }
5690
+ };
5691
+ }
5692
+ }
5693
+ };
5694
+
5695
+ // src/tasks/TasksRegistry.js
4679
5696
  var TasksRegistry = class _TasksRegistry {
4680
- map = {};
5697
+ /**
5698
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
5699
+ */
4681
5700
  constructor(initial) {
5701
+ this.map = {};
4682
5702
  if (initial) {
4683
5703
  this.addMany(initial);
4684
5704
  }
4685
5705
  }
5706
+ /**
5707
+ * Build a registry pre-populated with every core task plus legacy aliases.
5708
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
5709
+ *
5710
+ * @returns {TasksRegistry}
5711
+ */
4686
5712
  static withCoreTasks() {
4687
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
5713
+ 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);
4688
5714
  }
5715
+ /**
5716
+ * Register a single task class under a name. Overwrites any previous entry.
5717
+ *
5718
+ * @param {string} taskName
5719
+ * @param {Function} taskClass Subclass of `AbstractTask`.
5720
+ * @returns {this}
5721
+ */
4689
5722
  add(taskName, taskClass) {
4690
5723
  this.map[taskName] = taskClass;
4691
5724
  return this;
4692
5725
  }
5726
+ /**
5727
+ * Bulk-register a name → class map. Later calls override earlier ones.
5728
+ *
5729
+ * @param {Record<string, Function>} entries
5730
+ * @returns {this}
5731
+ */
4693
5732
  addMany(entries) {
4694
5733
  for (const [name, klass] of Object.entries(entries)) {
4695
5734
  this.add(name, klass);
4696
5735
  }
4697
5736
  return this;
4698
5737
  }
5738
+ /**
5739
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
5740
+ * the runner treats that as "some other worker may handle this" and skips.
5741
+ *
5742
+ * @param {string} taskName
5743
+ * @returns {Function | undefined}
5744
+ */
4699
5745
  get(taskName) {
4700
5746
  return this.map[taskName];
4701
5747
  }
5748
+ /**
5749
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
5750
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
5751
+ * paths where an unknown name is a hard CLI/programmer error.
5752
+ *
5753
+ * @param {string} taskName
5754
+ * @returns {Function}
5755
+ */
5756
+ requireClass(taskName) {
5757
+ const TaskClass = taskName ? this.map[taskName] : void 0;
5758
+ if (!TaskClass) {
5759
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
5760
+ throw new ParamError(
5761
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
5762
+ );
5763
+ }
5764
+ return TaskClass;
5765
+ }
5766
+ /**
5767
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
5768
+ * from `overrides` or `context.params`, look up the class, and delegate
5769
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
5770
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
5771
+ *
5772
+ * Validation failures (unknown task, missing required custom params, etc.)
5773
+ * surface as {@link ParamError} so the caller aborts cleanly before any
5774
+ * row is inserted.
5775
+ *
5776
+ * @param {object} context
5777
+ * @param {Record<string, unknown>} [overrides]
5778
+ * @returns {Promise<object>}
5779
+ */
5780
+ async resolveTaskParams(context, overrides = {}) {
5781
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
5782
+ const fromCli = context.params.get("name", "string");
5783
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
5784
+ const name = overrideName || cliName;
5785
+ if (!name) {
5786
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5787
+ }
5788
+ const TaskClass = this.requireClass(name);
5789
+ return TaskClass.resolveParams(context, { ...overrides, name });
5790
+ }
5791
+ /**
5792
+ * Names of every registered task, sorted alphabetically (useful for CLI output
5793
+ * and allowlist sanity checks).
5794
+ *
5795
+ * @returns {string[]}
5796
+ */
4702
5797
  listSupportedTasks() {
4703
5798
  return Object.keys(this.map).sort();
4704
5799
  }
5800
+ /**
5801
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
5802
+ * or for serialization.
5803
+ *
5804
+ * @returns {Record<string, Function>}
5805
+ */
4705
5806
  toObject() {
4706
5807
  return { ...this.map };
4707
5808
  }
4708
5809
  };
4709
5810
 
4710
- // src/tasks/taskScriptRunner.ts
5811
+ // src/tasks/serviceTaskAllowlist.js
5812
+ var SERVICE_TASK_NAMES = [
5813
+ "ping",
5814
+ "stop",
5815
+ "stopRunner",
5816
+ "shellCommand",
5817
+ "systemInfo",
5818
+ "info",
5819
+ "getLogs"
5820
+ ];
5821
+ function normalizeAllowedTasks(value) {
5822
+ if (!value) return void 0;
5823
+ if (Array.isArray(value)) {
5824
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5825
+ return out2.length ? out2 : void 0;
5826
+ }
5827
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5828
+ return out.length ? out : void 0;
5829
+ }
5830
+ function mergeAllowedTasksWithServiceTasks(names) {
5831
+ const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
5832
+ return Array.from(set).sort();
5833
+ }
5834
+
5835
+ // src/tasks/taskScriptRunner.js
4711
5836
  var import_node_child_process2 = require("child_process");
5837
+ var MAX_PROGRESS_TEXT_LEN = 4e3;
4712
5838
  function toCliArgs(args = []) {
4713
5839
  return args.filter((a) => typeof a === "string" && a.length > 0);
4714
5840
  }
4715
5841
  function formatChildLogPrefix(task) {
4716
- return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
5842
+ return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
4717
5843
  }
4718
- async function runNodeTaskScript(context, options) {
4719
- const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
5844
+ function isProgressPayload(payload) {
5845
+ if (!payload || typeof payload !== "object") return false;
5846
+ if (payload.level !== "progress") return false;
5847
+ const count = Number(payload.count);
5848
+ const total = Number(payload.total);
5849
+ return Number.isFinite(count) && Number.isFinite(total) && total > 0;
5850
+ }
5851
+ function formatProgressText(payload, fallbackPrefix) {
5852
+ const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
5853
+ const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
5854
+ return `${pfx}${label}${payload.count}/${payload.total}`;
5855
+ }
5856
+ function forwardChildLogToParent(context, prefix, message) {
5857
+ if (!message || typeof message !== "object") return;
5858
+ const text = typeof message.message === "string" ? message.message : null;
5859
+ if (!text) return;
5860
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
5861
+ const line = `[child:${prefix}] ${text}`;
5862
+ const logger = context.logger;
5863
+ switch (level) {
5864
+ case "error":
5865
+ case "fatal":
5866
+ logger.error?.(line);
5867
+ return;
5868
+ case "warn":
5869
+ case "warning":
5870
+ logger.warn?.(line);
5871
+ return;
5872
+ case "debug":
5873
+ logger.debug?.(line);
5874
+ return;
5875
+ case "info":
5876
+ default:
5877
+ logger.info?.(line);
5878
+ }
5879
+ }
5880
+ function buildNodeArgs(scriptPath, cliArgs) {
4720
5881
  const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
4721
5882
  const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
4722
- const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
4723
- const child = (0, import_node_child_process2.spawn)(
4724
- process.execPath,
4725
- nodeArgs,
4726
- {
4727
- cwd: options.cwd || process.cwd(),
4728
- stdio: ["ignore", "pipe", "pipe", "ipc"],
4729
- env: {
4730
- ...process.env,
4731
- TASK_ID: options.task.id,
4732
- TASK_NAME: options.task.task,
4733
- TASK_OPID: options.task.opid || ""
4734
- }
4735
- }
5883
+ return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
5884
+ }
5885
+ function resolveTasksTableName(context) {
5886
+ return context.tasksQueueName || context.params?.get?.("table") || "tasks";
5887
+ }
5888
+ function announceIpcFileLogsTarget(context, options) {
5889
+ if (!options.ipcFileLogs) return;
5890
+ const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
5891
+ const enabledRaw = context.params?.get?.("tasksLogsEnabled");
5892
+ const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
5893
+ context.logger.info?.(
5894
+ `[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
4736
5895
  );
4737
- let stdout = "";
4738
- let stderr = "";
4739
- let workerResult = null;
4740
- let hadErrorMessage = false;
4741
- const prefix = formatChildLogPrefix(options.task);
4742
- const db = context.db;
4743
- const tasksTable = context.params?.get?.("table") || "tasks";
4744
- let progressWriteChain = Promise.resolve();
4745
- let progressCallbackChain = Promise.resolve();
4746
- const updateProgress = (text) => {
4747
- if (!db || !text || !text.trim()) return;
4748
- progressWriteChain = progressWriteChain.then(async () => {
4749
- await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
4750
- }).catch((error) => {
4751
- context.logger.warn?.(
4752
- `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
4753
- );
4754
- });
4755
- if (options.onProgress) {
4756
- progressCallbackChain = progressCallbackChain.then(async () => {
4757
- await options.onProgress?.(text.slice(0, 4e3));
4758
- }).catch((error) => {
4759
- context.logger.warn?.(
4760
- `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
4761
- );
5896
+ }
5897
+ function createSerializedQueue() {
5898
+ let chain = Promise.resolve();
5899
+ return {
5900
+ push(fn) {
5901
+ chain = chain.then(fn, () => {
5902
+ }).catch(() => {
5903
+ });
5904
+ return chain;
5905
+ },
5906
+ drain() {
5907
+ return chain.catch(() => {
4762
5908
  });
4763
5909
  }
4764
5910
  };
4765
- const payloadToProgressText = (payload) => {
4766
- if (!payload) return "";
4767
- if (typeof payload === "string") return payload;
4768
- if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4769
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
4770
- return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
4771
- }
4772
- if (typeof payload.message === "string") return payload.message;
4773
- if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4774
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
4775
- return `${pfx}${payload.count}/${payload.total}`;
4776
- }
4777
- return "";
5911
+ }
5912
+ async function runNodeTaskScript(context, options) {
5913
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
5914
+ const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
5915
+ const child = (0, import_node_child_process2.spawn)(process.execPath, nodeArgs, {
5916
+ cwd: options.cwd || process.cwd(),
5917
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
5918
+ env: {
5919
+ ...process.env,
5920
+ TASK_ID: options.task.id,
5921
+ TASK_NAME: options.task.name,
5922
+ TASK_OPID: options.task.opid || ""
5923
+ }
5924
+ });
5925
+ announceIpcFileLogsTarget(context, options);
5926
+ const prefix = formatChildLogPrefix(options.task);
5927
+ const tasksTable = resolveTasksTableName(context);
5928
+ const progressQueue = createSerializedQueue();
5929
+ const state = {
5930
+ stdout: "",
5931
+ stderr: "",
5932
+ workerResult: null,
5933
+ hadErrorMessage: false
5934
+ };
5935
+ const writeProgress = (text) => {
5936
+ const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
5937
+ if (!trimmed) return;
5938
+ progressQueue.push(async () => {
5939
+ const db = context.db;
5940
+ if (db) {
5941
+ try {
5942
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
5943
+ } catch (error) {
5944
+ context.logger.warn?.(
5945
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
5946
+ );
5947
+ }
5948
+ }
5949
+ if (options.onProgress) {
5950
+ try {
5951
+ await options.onProgress(trimmed);
5952
+ } catch (error) {
5953
+ context.logger.warn?.(
5954
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
5955
+ );
5956
+ }
5957
+ }
5958
+ });
4778
5959
  };
4779
- child.stdout.on("data", (chunk) => {
5960
+ child.stdout?.on("data", (chunk) => {
4780
5961
  const text = String(chunk);
4781
- stdout += text;
5962
+ state.stdout += text;
4782
5963
  if (text.trim()) {
4783
5964
  context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
4784
- updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
4785
5965
  }
4786
5966
  });
4787
- child.stderr.on("data", (chunk) => {
5967
+ child.stderr?.on("data", (chunk) => {
4788
5968
  const text = String(chunk);
4789
- stderr += text;
5969
+ state.stderr += text;
4790
5970
  if (text.trim()) {
4791
5971
  context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
4792
5972
  }
4793
5973
  });
4794
5974
  child.on("message", (message) => {
4795
5975
  if (message && typeof message === "object" && "__taskWorkerResult" in message) {
4796
- workerResult = message.__taskWorkerResult;
5976
+ state.workerResult = message.__taskWorkerResult;
4797
5977
  return;
4798
5978
  }
4799
5979
  if (message && typeof message === "object") {
4800
5980
  const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
4801
5981
  if (level === "error" || level === "fatal") {
4802
- hadErrorMessage = true;
5982
+ state.hadErrorMessage = true;
4803
5983
  }
4804
5984
  }
4805
- appendTaskIpcLog(context, options.task, message);
4806
- const progressText = payloadToProgressText(message);
4807
- if (progressText) {
4808
- updateProgress(progressText);
4809
- if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
4810
- const countNum = Number(String(message.count).trim());
4811
- const totalNum = Number(message.total);
4812
- if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
4813
- context.logger.progress(message.message || "progress", {
4814
- prefix: message.prefix || prefix,
4815
- count: countNum,
4816
- total: totalNum
4817
- });
4818
- } else {
4819
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
4820
- }
4821
- } else {
4822
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
4823
- }
5985
+ appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
5986
+ try {
5987
+ options.onChildIpcMessage?.(message);
5988
+ } catch (e) {
5989
+ context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
4824
5990
  }
5991
+ if (isProgressPayload(message)) {
5992
+ context.logger.progress(message.message || "progress", {
5993
+ prefix: message.prefix || prefix,
5994
+ count: Number(message.count),
5995
+ total: Number(message.total)
5996
+ });
5997
+ writeProgress(formatProgressText(message, prefix));
5998
+ return;
5999
+ }
6000
+ forwardChildLogToParent(context, prefix, message);
4825
6001
  });
4826
6002
  return await new Promise((resolve2, reject) => {
4827
6003
  child.on("error", (error) => reject(error));
4828
6004
  child.on("close", (exitCode, signal) => {
4829
- Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
6005
+ void (async () => {
6006
+ await flushTaskIpcLogs(context);
6007
+ await progressQueue.drain();
4830
6008
  resolve2({
4831
6009
  exitCode,
4832
6010
  signal,
4833
- stdout: stdout.trim(),
4834
- stderr: stderr.trim(),
4835
- workerResult,
4836
- hadErrorMessage
6011
+ stdout: state.stdout.trim(),
6012
+ stderr: state.stderr.trim(),
6013
+ workerResult: state.workerResult,
6014
+ hadErrorMessage: state.hadErrorMessage
4837
6015
  });
4838
- });
6016
+ })();
4839
6017
  });
4840
6018
  });
4841
6019
  }
4842
6020
 
4843
- // src/tasks/index.ts
6021
+ // src/tasks/index.js
4844
6022
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4845
6023
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4846
- function getDb2(context) {
6024
+ function getDb3(context) {
4847
6025
  const db = context.db;
4848
6026
  if (!db) {
4849
6027
  throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
@@ -4855,22 +6033,13 @@ function normalizeRegistry(registry) {
4855
6033
  if (registry instanceof TasksRegistry) return registry;
4856
6034
  return new TasksRegistry().addMany(registry);
4857
6035
  }
4858
- function normalizeAllowedTasks(value) {
4859
- if (!value) return void 0;
4860
- if (Array.isArray(value)) {
4861
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
4862
- return out2.length ? out2 : void 0;
4863
- }
4864
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
4865
- return out.length ? out : void 0;
4866
- }
4867
- async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
6036
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
4868
6037
  return enqueueTask(context, {
4869
- queue,
4870
- target,
4871
- task: "stopRunner",
6038
+ queueName,
6039
+ name: "stopRunner",
4872
6040
  params: { allowanceMs },
4873
- priority: 1e6
6041
+ priority: 0,
6042
+ serviceGroup
4874
6043
  });
4875
6044
  }
4876
6045
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
@@ -4887,19 +6056,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
4887
6056
  context.emitter.emit("stop", allowanceMs);
4888
6057
  }
4889
6058
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4890
- const db = getDb2(context);
4891
- const taskName = row.task;
6059
+ const db = getDb3(context);
6060
+ const taskName = row.name;
4892
6061
  const TaskClass = registry.get(taskName);
4893
- const { paused_at: _pausedAt, ...rowForHistory } = row;
4894
6062
  if (!TaskClass) {
4895
6063
  const err = { message: `Unknown task "${taskName}"` };
4896
- await db(historyTable).insert({
4897
- ...rowForHistory,
4898
- completed_at: /* @__PURE__ */ new Date(),
4899
- success: false,
4900
- params: toJsonColumn(row.params),
4901
- results: toJsonColumn(err)
4902
- });
6064
+ await db(historyTable).insert(
6065
+ taskHistoryInsertFromQueueRow(row, {
6066
+ completed_at: /* @__PURE__ */ new Date(),
6067
+ success: false,
6068
+ status: "failed",
6069
+ status_changed_at: db.fn.now(),
6070
+ params: toJsonColumn(row.params),
6071
+ results: toJsonColumn(err)
6072
+ })
6073
+ );
4903
6074
  if (row.schedule) {
4904
6075
  await db(tasksTable).where({ id: row.id }).update({
4905
6076
  started_at: null,
@@ -4907,7 +6078,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4907
6078
  success: false,
4908
6079
  results: toJsonColumn(err),
4909
6080
  past_due: null,
4910
- paused_at: db.fn.now(),
6081
+ status: "paused",
6082
+ status_changed_at: db.fn.now(),
4911
6083
  progress: LOCKED_BY_ERROR_MESSAGE
4912
6084
  });
4913
6085
  } else {
@@ -4934,13 +6106,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4934
6106
  } finally {
4935
6107
  runningTaskInstances.delete(row.id);
4936
6108
  }
4937
- await db(historyTable).insert({
4938
- ...rowForHistory,
4939
- completed_at: /* @__PURE__ */ new Date(),
4940
- success,
4941
- params: toJsonColumn(row.params),
4942
- results: toJsonColumn(results)
4943
- });
6109
+ await db(historyTable).insert(
6110
+ taskHistoryInsertFromQueueRow(row, {
6111
+ completed_at: /* @__PURE__ */ new Date(),
6112
+ success,
6113
+ status: success ? "completed" : "failed",
6114
+ status_changed_at: db.fn.now(),
6115
+ params: toJsonColumn(row.params),
6116
+ results: toJsonColumn(results)
6117
+ })
6118
+ );
4944
6119
  if (!success) {
4945
6120
  const dbName = String(context?.params?.get?.("dbName") || "local");
4946
6121
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -4955,19 +6130,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4955
6130
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
4956
6131
  appendTaskIpcLog(context, row, {
4957
6132
  level: "error",
4958
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
6133
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
4959
6134
  details: results
4960
6135
  });
4961
6136
  }
4962
6137
  if (row.schedule) {
4963
6138
  if (success) {
6139
+ let nextRunAt = null;
6140
+ try {
6141
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
6142
+ } catch (e) {
6143
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
6144
+ }
4964
6145
  await db(tasksTable).where({ id: row.id }).update({
4965
6146
  started_at: null,
4966
6147
  completed_at: /* @__PURE__ */ new Date(),
4967
6148
  success,
4968
6149
  results: toJsonColumn(results),
4969
6150
  progress: null,
4970
- past_due: null
6151
+ past_due: null,
6152
+ status: "idle",
6153
+ status_changed_at: db.fn.now(),
6154
+ next_run_at: nextRunAt,
6155
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
6156
+ service_name: null,
6157
+ server_name: null,
6158
+ instance_number: null
4971
6159
  });
4972
6160
  } else {
4973
6161
  await db(tasksTable).where({ id: row.id }).update({
@@ -4975,7 +6163,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4975
6163
  completed_at: /* @__PURE__ */ new Date(),
4976
6164
  success,
4977
6165
  results: toJsonColumn(results),
4978
- paused_at: db.fn.now(),
6166
+ status: "paused",
6167
+ status_changed_at: db.fn.now(),
4979
6168
  progress: LOCKED_BY_ERROR_MESSAGE,
4980
6169
  past_due: null
4981
6170
  });
@@ -4987,208 +6176,400 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4987
6176
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
4988
6177
  return { stopRunnerRequested, stopAllowanceMs };
4989
6178
  }
4990
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
4991
- const db = getDb2(context);
4992
- 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);
6179
+ function shuffleTaskRowsInPlace(rows) {
6180
+ for (let i = rows.length - 1; i > 0; i--) {
6181
+ const j = Math.floor(Math.random() * (i + 1));
6182
+ const t = rows[i];
6183
+ rows[i] = rows[j];
6184
+ rows[j] = t;
6185
+ }
6186
+ }
6187
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
6188
+ const db = getDb3(context);
6189
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
6190
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
6191
+ }).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);
4993
6192
  if (taskNames && taskNames.length > 0) {
4994
- query = query.whereIn("task", taskNames);
6193
+ query = query.whereIn("name", taskNames);
6194
+ }
6195
+ if (runnerIdentity) {
6196
+ query = query.where(function() {
6197
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
6198
+ }).where(function() {
6199
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
6200
+ }).where(function() {
6201
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
6202
+ });
6203
+ } else {
6204
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
4995
6205
  }
4996
6206
  const candidates = await query;
6207
+ shuffleTaskRowsInPlace(candidates);
4997
6208
  for (const row of candidates) {
4998
6209
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
4999
6210
  continue;
5000
6211
  }
5001
- const TaskClass = registry.get(row.task);
5002
- if (TaskClass) {
5003
- const taskInstance = new TaskClass(context, row);
5004
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
5005
- if (reason) {
5006
- if (!row.past_due) {
5007
- await db(tasksTable).where({ id: row.id }).update({
5008
- past_due: db.fn.now(),
5009
- progress: String(reason)
5010
- });
5011
- }
5012
- continue;
6212
+ const TaskClass = registry.get(row.name);
6213
+ if (!TaskClass) {
6214
+ continue;
6215
+ }
6216
+ const taskInstance = new TaskClass(context, row);
6217
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
6218
+ if (reason) {
6219
+ if (!row.past_due) {
6220
+ await db(tasksTable).where({ id: row.id }).update({
6221
+ past_due: db.fn.now(),
6222
+ progress: String(reason)
6223
+ });
5013
6224
  }
6225
+ continue;
6226
+ }
6227
+ const claimPatch = {
6228
+ started_at: db.fn.now(),
6229
+ status: "running",
6230
+ status_changed_at: db.fn.now()
6231
+ };
6232
+ if (runnerIdentity) {
6233
+ claimPatch.service_name = runnerIdentity.service_name;
6234
+ claimPatch.server_name = runnerIdentity.server_name;
6235
+ claimPatch.instance_number = runnerIdentity.instance_number;
5014
6236
  }
5015
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
6237
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
5016
6238
  const claimed = Array.isArray(updated) ? updated[0] : null;
5017
6239
  if (claimed) return claimed;
5018
6240
  }
5019
6241
  return null;
5020
6242
  }
5021
6243
  async function runTasksLoop(context, options) {
5022
- const queue = options.queue ?? "tasks";
6244
+ const queueName = options.queueName ?? "tasks";
5023
6245
  const target = options.target;
5024
6246
  const pollMs = options.pollMs ?? 1e3;
5025
- const maxParallel = options.maxParallel ?? 1;
6247
+ const claimJitterMs = options.claimJitterMs ?? 0;
6248
+ const maxParallel = options.maxParallel ?? 32;
5026
6249
  const scanLimit = options.scanLimit ?? 100;
5027
6250
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5028
6251
  const registry = normalizeRegistry(options.registry);
5029
- const { tasksTable, historyTable } = queueToTableNames(queue);
6252
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5030
6253
  if (!target) throw new Error("runTasksLoop: target is required");
6254
+ context.tasksQueueName = queueName;
5031
6255
  const runningPromises = /* @__PURE__ */ new Set();
5032
6256
  const runningTaskInstances = /* @__PURE__ */ new Map();
5033
6257
  let runningStopControlPromise = null;
5034
6258
  let stopRequested = false;
5035
6259
  let stopAllowanceMs = 5e3;
5036
- context.__tasksRunnerStop = false;
5037
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
5038
- if (!runningStopControlPromise) {
5039
- const claimedStopTask = await claimNextRunnableTask(
5040
- context,
5041
- tasksTable,
5042
- target,
5043
- registry,
5044
- 10,
5045
- ["stopRunner", "stop"]
5046
- );
5047
- if (claimedStopTask) {
5048
- runningStopControlPromise = executeClaimedTask(
6260
+ context.tasksRunnerStop = false;
6261
+ let registryReg = null;
6262
+ let registryInterval = null;
6263
+ let runnerIdentity = null;
6264
+ const hbGroup = options.runnerServiceGroup?.trim();
6265
+ if (hbGroup) {
6266
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
6267
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
6268
+ const defaultMeta = {
6269
+ component: "tasks-runner",
6270
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
6271
+ };
6272
+ registryReg = await registerInServicesRegistry(context, {
6273
+ queueName,
6274
+ target,
6275
+ serviceGroup: hbGroup,
6276
+ serviceName: options.runnerServiceName,
6277
+ instanceNumber: options.runnerInstanceNumber,
6278
+ staleMs,
6279
+ groupMaxInstances: options.runnerGroupMaxInstances,
6280
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
6281
+ metadata: options.runnerMetadata ?? defaultMeta
6282
+ });
6283
+ runnerIdentity = {
6284
+ service_name: registryReg.serviceName,
6285
+ server_name: import_node_os3.default.hostname(),
6286
+ instance_number: registryReg.instanceNumber
6287
+ };
6288
+ registryInterval = setInterval(() => {
6289
+ void touchServicesRegistry(context, registryReg).catch((err) => {
6290
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
6291
+ });
6292
+ }, hbIntervalMs);
6293
+ }
6294
+ try {
6295
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
6296
+ if (!runningStopControlPromise) {
6297
+ const claimedStopTask = await claimNextRunnableTask(
5049
6298
  context,
5050
6299
  tasksTable,
5051
- historyTable,
5052
- claimedStopTask,
6300
+ target,
5053
6301
  registry,
5054
- runningTaskInstances
5055
- ).then(async (outcome) => {
6302
+ 10,
6303
+ ["stopRunner", "stop"],
6304
+ runnerIdentity
6305
+ );
6306
+ if (claimedStopTask) {
6307
+ runningStopControlPromise = executeClaimedTask(
6308
+ context,
6309
+ tasksTable,
6310
+ historyTable,
6311
+ claimedStopTask,
6312
+ registry,
6313
+ runningTaskInstances
6314
+ ).then(async (outcome) => {
6315
+ if (outcome.stopRunnerRequested && !stopRequested) {
6316
+ stopRequested = true;
6317
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
6318
+ context.tasksRunnerStop = true;
6319
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
6320
+ }
6321
+ }).finally(() => {
6322
+ runningStopControlPromise = null;
6323
+ });
6324
+ }
6325
+ }
6326
+ if (claimJitterMs > 0) {
6327
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
6328
+ }
6329
+ while (runningPromises.size < maxParallel) {
6330
+ const claimed = await claimNextRunnableTask(
6331
+ context,
6332
+ tasksTable,
6333
+ target,
6334
+ registry,
6335
+ scanLimit,
6336
+ allowedTasks,
6337
+ runnerIdentity
6338
+ );
6339
+ if (!claimed) break;
6340
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
5056
6341
  if (outcome.stopRunnerRequested && !stopRequested) {
5057
6342
  stopRequested = true;
5058
6343
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5059
- context.__tasksRunnerStop = true;
6344
+ context.tasksRunnerStop = true;
5060
6345
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5061
6346
  }
5062
6347
  }).finally(() => {
5063
- runningStopControlPromise = null;
6348
+ runningPromises.delete(p);
5064
6349
  });
6350
+ runningPromises.add(p);
6351
+ }
6352
+ const wakePromises = [...runningPromises];
6353
+ if (runningStopControlPromise) {
6354
+ wakePromises.push(runningStopControlPromise);
6355
+ }
6356
+ if (wakePromises.length === 0) {
6357
+ await sleepMs(pollMs);
6358
+ } else {
6359
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
6360
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
5065
6361
  }
5066
6362
  }
5067
- while (runningPromises.size < maxParallel) {
5068
- const claimed = await claimNextRunnableTask(
5069
- context,
5070
- tasksTable,
5071
- target,
5072
- registry,
5073
- scanLimit,
5074
- allowedTasks
5075
- );
5076
- if (!claimed) break;
5077
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
5078
- if (outcome.stopRunnerRequested && !stopRequested) {
5079
- stopRequested = true;
5080
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5081
- context.__tasksRunnerStop = true;
5082
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5083
- }
5084
- }).finally(() => {
5085
- runningPromises.delete(p);
5086
- });
5087
- runningPromises.add(p);
6363
+ if (context.isStop() && !stopRequested) {
6364
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
5088
6365
  }
5089
- await sleepMs(pollMs);
5090
- }
5091
- if (context.isStop() && !stopRequested) {
5092
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
5093
- }
5094
- if (runningPromises.size > 0) {
5095
- if (stopRequested) {
5096
- await Promise.race([
5097
- Promise.allSettled(Array.from(runningPromises)),
5098
- sleepMs(stopAllowanceMs).then(() => {
5099
- context.logger.warn?.(
5100
- `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
5101
- );
5102
- })
5103
- ]);
5104
- } else {
5105
- await Promise.allSettled(Array.from(runningPromises));
6366
+ if (runningPromises.size > 0) {
6367
+ if (stopRequested) {
6368
+ await Promise.race([
6369
+ Promise.allSettled(Array.from(runningPromises)),
6370
+ sleepMs(stopAllowanceMs).then(() => {
6371
+ context.logger.warn?.(
6372
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
6373
+ );
6374
+ })
6375
+ ]);
6376
+ } else {
6377
+ await Promise.allSettled(Array.from(runningPromises));
6378
+ }
6379
+ }
6380
+ } finally {
6381
+ if (registryInterval) {
6382
+ clearInterval(registryInterval);
6383
+ registryInterval = null;
6384
+ }
6385
+ if (registryReg) {
6386
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
6387
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
6388
+ });
6389
+ registryReg = null;
6390
+ delete context.servicesRegistry;
6391
+ delete context.runnerHeartbeat;
5106
6392
  }
5107
6393
  }
5108
6394
  }
5109
6395
  async function waitForTaskResult(context, taskId, options = {}) {
5110
- const db = getDb2(context);
5111
- const queue = options.queue ?? "tasks";
6396
+ const db = getDb3(context);
6397
+ const queueName = options.queueName ?? "tasks";
5112
6398
  const timeoutMs = options.timeoutMs ?? 6e4;
5113
6399
  const pollMs = options.pollMs ?? 500;
5114
- const { tasksTable, historyTable } = queueToTableNames(queue);
6400
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5115
6401
  const deadline = Date.now() + timeoutMs;
6402
+ const waitStartedAt = /* @__PURE__ */ new Date();
6403
+ let cachedNameOpid = null;
6404
+ async function historySinceWait(name, opid) {
6405
+ let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
6406
+ if (opid == null || opid === "") {
6407
+ q = q.whereNull("opid");
6408
+ } else {
6409
+ q = q.where({ opid });
6410
+ }
6411
+ return await q.orderBy("completed_at", "desc").first();
6412
+ }
5116
6413
  while (Date.now() <= deadline) {
5117
- const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5118
- if (done) return done;
6414
+ const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
6415
+ if (legacy) {
6416
+ return legacy;
6417
+ }
5119
6418
  const pending = await db(tasksTable).where({ id: taskId }).first();
5120
- if (!pending) {
5121
- const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5122
- return maybeDone ?? null;
6419
+ if (pending) {
6420
+ cachedNameOpid = { name: pending.name, opid: pending.opid };
6421
+ const done = await historySinceWait(pending.name, pending.opid);
6422
+ if (done) {
6423
+ return done;
6424
+ }
6425
+ } else if (cachedNameOpid) {
6426
+ const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
6427
+ if (done) {
6428
+ return done;
6429
+ }
6430
+ return null;
6431
+ } else {
6432
+ return null;
5123
6433
  }
5124
6434
  await sleepMs(pollMs);
5125
6435
  }
5126
6436
  return null;
5127
6437
  }
5128
6438
  var TasksManager = class _TasksManager {
5129
- context;
5130
- queue;
5131
- target;
5132
- recreateTaskTables;
5133
- pollMs;
5134
- maxParallel;
5135
- scanLimit;
5136
- allowedTasks;
5137
- registry;
6439
+ /**
6440
+ * @param {object} context
6441
+ * @param {{
6442
+ * queueName?: string,
6443
+ * target?: string,
6444
+ * recreateTaskTables?: boolean,
6445
+ * pollMs?: number,
6446
+ * claimJitterMs?: number,
6447
+ * maxParallel?: number,
6448
+ * scanLimit?: number,
6449
+ * allowedTasks?: string | string[],
6450
+ * registry?: TasksRegistry | Record<string, Function>,
6451
+ * runnerServiceGroup?: string,
6452
+ * runnerServiceName?: string,
6453
+ * runnerInstanceNumber?: number,
6454
+ * runnerHeartbeatIntervalMs?: number,
6455
+ * runnerHeartbeatStaleMs?: number,
6456
+ * runnerGroupMaxInstances?: number,
6457
+ * runnerEnforceMaxInstances?: boolean,
6458
+ * runnerMetadata?: Record<string, unknown>,
6459
+ * }} [options]
6460
+ */
5138
6461
  constructor(context, options = {}) {
5139
6462
  this.context = context;
5140
- this.queue = options.queue ?? "tasks";
6463
+ this.queueName = options.queueName ?? "tasks";
5141
6464
  this.target = options.target ?? "localRunner";
5142
6465
  this.recreateTaskTables = options.recreateTaskTables ?? false;
5143
6466
  this.pollMs = options.pollMs ?? 1e3;
6467
+ this.claimJitterMs = options.claimJitterMs ?? 0;
5144
6468
  this.maxParallel = options.maxParallel ?? 1;
5145
6469
  this.scanLimit = options.scanLimit ?? 100;
5146
6470
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5147
6471
  this.registry = normalizeRegistry(options.registry);
6472
+ this.runnerServiceGroup = options.runnerServiceGroup;
6473
+ this.runnerServiceName = options.runnerServiceName;
6474
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
6475
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
6476
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
6477
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
6478
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
6479
+ this.runnerMetadata = options.runnerMetadata;
5148
6480
  }
6481
+ /**
6482
+ * Preferred factory: reads defaults from `context.params` (module namespace
6483
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
6484
+ * and inline options in one consistent resolver.
6485
+ *
6486
+ * @param {object} context
6487
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
6488
+ * @returns {TasksManager}
6489
+ */
5149
6490
  static init(context, options = {}) {
5150
6491
  const defs = {
5151
6492
  table: "string default tasks",
5152
6493
  target: "string default localRunner",
5153
6494
  recreateTaskTables: "boolean default false",
5154
6495
  pollMs: "number default 1000",
6496
+ claimJitterMs: "number default 0",
5155
6497
  maxParallel: "number default 1",
5156
6498
  scanLimit: "number default 100",
5157
- allowedTasks: "string"
6499
+ allowedTasks: "string",
6500
+ runnerServiceGroup: "string",
6501
+ runnerServiceName: "string",
6502
+ runnerInstanceNumber: "number",
6503
+ runnerHeartbeatIntervalMs: "number default 10000",
6504
+ runnerHeartbeatStaleMs: "number default 45000",
6505
+ runnerGroupMaxInstances: "number",
6506
+ runnerEnforceMaxInstances: "boolean default true"
5158
6507
  };
5159
- const discovered = context.params.getAllForModule(defs);
6508
+ const discovered = context.params.getAllForModule("tasks", defs);
5160
6509
  const resolved = {
5161
- queue: discovered.table,
6510
+ queueName: discovered.table,
5162
6511
  target: discovered.target,
5163
6512
  recreateTaskTables: discovered.recreateTaskTables,
5164
6513
  pollMs: discovered.pollMs,
6514
+ claimJitterMs: discovered.claimJitterMs,
5165
6515
  maxParallel: discovered.maxParallel,
5166
6516
  scanLimit: discovered.scanLimit,
5167
6517
  allowedTasks: discovered.allowedTasks,
6518
+ runnerServiceGroup: discovered.runnerServiceGroup,
6519
+ runnerServiceName: discovered.runnerServiceName,
6520
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
6521
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
6522
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
6523
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
6524
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
5168
6525
  ...options
5169
6526
  };
5170
6527
  return new _TasksManager(context, resolved);
5171
6528
  }
6529
+ /**
6530
+ * Idempotently ensure the three backing tables exist for this queue.
6531
+ *
6532
+ * @param {{ recreate?: boolean }} [options]
6533
+ * @returns {Promise<void>}
6534
+ */
5172
6535
  async ensureTaskTables(options = {}) {
5173
6536
  await ensureTaskTables(this.context, {
5174
- queue: this.queue,
6537
+ queueName: this.queueName,
5175
6538
  recreate: options.recreate ?? this.recreateTaskTables
5176
6539
  });
5177
6540
  }
6541
+ /**
6542
+ * Start the runner loop using this manager's resolved config. Per-call
6543
+ * options override the stored defaults, but `runnerMetadata` still falls
6544
+ * through when omitted.
6545
+ *
6546
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
6547
+ * @returns {Promise<void>}
6548
+ */
5178
6549
  async runTasksLoop(options = {}) {
5179
6550
  await runTasksLoop(this.context, {
5180
- queue: options.queue ?? this.queue,
6551
+ queueName: options.queueName ?? this.queueName,
5181
6552
  target: options.target ?? this.target,
5182
6553
  pollMs: options.pollMs ?? this.pollMs,
6554
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
5183
6555
  maxParallel: options.maxParallel ?? this.maxParallel,
5184
6556
  scanLimit: options.scanLimit ?? this.scanLimit,
5185
6557
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
5186
- registry: options.registry ?? this.registry
6558
+ registry: options.registry ?? this.registry,
6559
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
6560
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
6561
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
6562
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
6563
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
6564
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
6565
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
6566
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
5187
6567
  });
5188
6568
  }
5189
6569
  };
5190
6570
  // Annotate the CommonJS export names for ESM import in node:
5191
6571
  0 && (module.exports = {
6572
+ AbstractTask,
5192
6573
  Args,
5193
6574
  Box,
5194
6575
  Db,
@@ -5204,13 +6585,15 @@ var TasksManager = class _TasksManager {
5204
6585
  MultiColumnListWithPreviewComponent,
5205
6586
  Params,
5206
6587
  React,
6588
+ S3,
6589
+ SERVICE_TASK_NAMES,
5207
6590
  ScreenBody,
5208
6591
  ScreenContainer,
5209
6592
  ScreenDivider,
5210
6593
  ScreenFooter,
5211
6594
  ScreenRow,
5212
6595
  ScreenTitle,
5213
- TaskMaster,
6596
+ TaskGetLogs,
5214
6597
  TaskPing,
5215
6598
  TaskSampleProcess,
5216
6599
  TaskShellCommand,
@@ -5225,24 +6608,38 @@ var TasksManager = class _TasksManager {
5225
6608
  buildBreadcrumb,
5226
6609
  buildDetailBreadcrumb,
5227
6610
  buildFooter,
5228
- dbConnect,
5229
- dbFindAndConnect,
5230
- dbInit,
6611
+ convertPattern,
5231
6612
  defaultFileSynopsisFunction,
5232
6613
  defaultTasksRegistry,
5233
6614
  defaultVersionSynopsisFunction,
5234
6615
  enqueueStopTask,
5235
6616
  enqueueTask,
5236
6617
  ensureTaskTables,
6618
+ flushTaskIpcLogs,
5237
6619
  getArgsInstance,
5238
6620
  h,
6621
+ ipcFileLogsTableNameForSourceResource,
5239
6622
  joiEdateType,
5240
6623
  joiStringArrayType,
6624
+ listAliveRunnerHeartbeats,
6625
+ listServicesRegistry,
5241
6626
  listSources,
5242
6627
  listTables,
5243
6628
  load,
6629
+ matchesParsedPattern,
6630
+ memo,
6631
+ mergeAllowedTasksWithServiceTasks,
6632
+ nextTimeMatch,
6633
+ normalizeAllowedTasks,
5244
6634
  organizeFooterMessages,
5245
6635
  queueToTableNames,
6636
+ readTaskIpcLogsSnapshot,
6637
+ registerInServicesRegistry,
6638
+ registerRunnerHeartbeat,
6639
+ resolveAsterisks,
6640
+ resolveIpcFileLogsDir,
6641
+ resolveRanges,
6642
+ resolveSteps,
5246
6643
  runNodeTaskScript,
5247
6644
  runTasksLoop,
5248
6645
  setupContext,
@@ -5252,10 +6649,18 @@ var TasksManager = class _TasksManager {
5252
6649
  showMultiColumnListWithPreviewScreen,
5253
6650
  showScreen,
5254
6651
  showWordGridScreen,
6652
+ taskHistoryInsertFromQueueRow,
6653
+ timeMatcher,
6654
+ touchRunnerHeartbeat,
6655
+ touchServicesRegistry,
6656
+ unregisterRunnerHeartbeat,
6657
+ unregisterServicesRegistry,
6658
+ updateServicesRegistryMetadata,
5255
6659
  updateTaskProgress,
5256
6660
  useCallback,
5257
6661
  useEffect,
5258
6662
  useInput,
6663
+ useLayoutEffect,
5259
6664
  useMemo,
5260
6665
  useRef,
5261
6666
  useState,