@nmakarov/cli-toolkit 0.21.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/args.cjs +1 -4
  2. package/dist/args.cjs.map +1 -1
  3. package/dist/args.js +1 -1
  4. package/dist/args.js.map +1 -1
  5. package/dist/cli-runner.cjs +1370 -623
  6. package/dist/cli-runner.cjs.map +1 -1
  7. package/dist/cli-runner.js +1415 -667
  8. package/dist/cli-runner.js.map +1 -1
  9. package/dist/db.cjs +173 -158
  10. package/dist/db.cjs.map +1 -1
  11. package/dist/db.js +172 -151
  12. package/dist/db.js.map +1 -1
  13. package/dist/errors.cjs +2 -2
  14. package/dist/errors.cjs.map +1 -1
  15. package/dist/errors.js +2 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/filedatabase.cjs +19 -19
  18. package/dist/filedatabase.cjs.map +1 -1
  19. package/dist/filedatabase.js +19 -16
  20. package/dist/filedatabase.js.map +1 -1
  21. package/dist/http-client.cjs +9 -11
  22. package/dist/http-client.cjs.map +1 -1
  23. package/dist/http-client.js +10 -9
  24. package/dist/http-client.js.map +1 -1
  25. package/dist/http-client2.cjs +34 -37
  26. package/dist/http-client2.cjs.map +1 -1
  27. package/dist/http-client2.js +34 -34
  28. package/dist/http-client2.js.map +1 -1
  29. package/dist/index.cjs +1831 -713
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +1837 -713
  32. package/dist/index.js.map +1 -1
  33. package/dist/init.cjs +93 -68
  34. package/dist/init.cjs.map +1 -1
  35. package/dist/init.js +108 -82
  36. package/dist/init.js.map +1 -1
  37. package/dist/logger.cjs +5 -5
  38. package/dist/logger.cjs.map +1 -1
  39. package/dist/logger.js +5 -4
  40. package/dist/logger.js.map +1 -1
  41. package/dist/mock-server.cjs +21 -33
  42. package/dist/mock-server.cjs.map +1 -1
  43. package/dist/mock-server.js +21 -28
  44. package/dist/mock-server.js.map +1 -1
  45. package/dist/params.cjs +18 -9
  46. package/dist/params.cjs.map +1 -1
  47. package/dist/params.js +18 -6
  48. package/dist/params.js.map +1 -1
  49. package/dist/s3.cjs +286 -0
  50. package/dist/s3.cjs.map +1 -0
  51. package/dist/s3.js +273 -0
  52. package/dist/s3.js.map +1 -0
  53. package/dist/screen.cjs +34 -39
  54. package/dist/screen.cjs.map +1 -1
  55. package/dist/screen.js +48 -46
  56. package/dist/screen.js.map +1 -1
  57. package/dist/tasks.cjs +1354 -501
  58. package/dist/tasks.cjs.map +1 -1
  59. package/dist/tasks.js +1369 -527
  60. package/dist/tasks.js.map +1 -1
  61. package/dist/utils.cjs +7 -8
  62. package/dist/utils.cjs.map +1 -1
  63. package/dist/utils.js +6 -6
  64. package/dist/utils.js.map +1 -1
  65. package/package.json +32 -47
  66. package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
  67. package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
  68. package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
package/dist/index.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,14 +907,13 @@ 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];
@@ -940,12 +927,11 @@ function buildDetailBreadcrumb(path5, suffix = "") {
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,17 +1109,17 @@ __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,
1138
1125
  listAliveRunnerHeartbeats: () => listServicesRegistry,
@@ -1140,14 +1127,22 @@ __export(src_exports, {
1140
1127
  listSources: () => listSources,
1141
1128
  listTables: () => listTables,
1142
1129
  load: () => load,
1130
+ matchesParsedPattern: () => matchesParsedPattern,
1131
+ memo: () => import_react5.memo,
1132
+ mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
1133
+ nextTimeMatch: () => nextTimeMatch,
1134
+ normalizeAllowedTasks: () => normalizeAllowedTasks,
1143
1135
  organizeFooterMessages: () => organizeFooterMessages,
1144
1136
  queueToTableNames: () => queueToTableNames,
1137
+ readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
1145
1138
  registerInServicesRegistry: () => registerInServicesRegistry,
1146
1139
  registerRunnerHeartbeat: () => registerInServicesRegistry,
1140
+ resolveAsterisks: () => resolveAsterisks,
1141
+ resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
1142
+ resolveRanges: () => resolveRanges,
1143
+ resolveSteps: () => resolveSteps,
1147
1144
  runNodeTaskScript: () => runNodeTaskScript,
1148
1145
  runTasksLoop: () => runTasksLoop,
1149
- runnerHeartbeatsTable: () => servicesRegistryTable,
1150
- servicesRegistryTable: () => servicesRegistryTable,
1151
1146
  setupContext: () => setupContext,
1152
1147
  showListScreen: () => showListScreen,
1153
1148
  showMenuScreen: () => showMenuScreen,
@@ -1155,6 +1150,8 @@ __export(src_exports, {
1155
1150
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1156
1151
  showScreen: () => showScreen,
1157
1152
  showWordGridScreen: () => showWordGridScreen,
1153
+ taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
1154
+ timeMatcher: () => timeMatcher,
1158
1155
  touchRunnerHeartbeat: () => touchServicesRegistry,
1159
1156
  touchServicesRegistry: () => touchServicesRegistry,
1160
1157
  unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
@@ -1164,6 +1161,7 @@ __export(src_exports, {
1164
1161
  useCallback: () => import_react5.useCallback,
1165
1162
  useEffect: () => import_react5.useEffect,
1166
1163
  useInput: () => import_ink5.useInput,
1164
+ useLayoutEffect: () => import_react5.useLayoutEffect,
1167
1165
  useMemo: () => import_react5.useMemo,
1168
1166
  useRef: () => import_react5.useRef,
1169
1167
  useState: () => import_react5.useState,
@@ -1171,7 +1169,7 @@ __export(src_exports, {
1171
1169
  });
1172
1170
  module.exports = __toCommonJS(src_exports);
1173
1171
 
1174
- // src/args/index.ts
1172
+ // src/args/index.js
1175
1173
  var import_fs = require("fs");
1176
1174
  var import_path = require("path");
1177
1175
  var import_dotenv = require("dotenv");
@@ -1658,10 +1656,10 @@ function getArgsInstance() {
1658
1656
  return instance;
1659
1657
  }
1660
1658
 
1661
- // src/params/index.ts
1659
+ // src/params/index.js
1662
1660
  var import_joi = __toESM(require("joi"), 1);
1663
1661
 
1664
- // src/errors.ts
1662
+ // src/errors.js
1665
1663
  var FrameworkError = class extends Error {
1666
1664
  constructor(message) {
1667
1665
  super(message);
@@ -1681,7 +1679,7 @@ var FileDatabaseError = class extends FrameworkError {
1681
1679
  }
1682
1680
  };
1683
1681
 
1684
- // src/params/custom-types.ts
1682
+ // src/params/custom-types.js
1685
1683
  var joiEdateType = (value, helpers) => {
1686
1684
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
1687
1685
  const testDate = new Date(value);
@@ -1774,7 +1772,7 @@ function calculateTimeOffset(amount, unit, sign) {
1774
1772
  }
1775
1773
  return sign === "+" ? amount * multiplier : -amount * multiplier;
1776
1774
  }
1777
- var joiStringArrayType = (type) => (value, helpers) => {
1775
+ var joiStringArrayType = (type) => (value, _helpers) => {
1778
1776
  if (value === void 0 || typeof value === "function") {
1779
1777
  return [];
1780
1778
  }
@@ -1800,7 +1798,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
1800
1798
  return arr;
1801
1799
  };
1802
1800
 
1803
- // src/params/index.ts
1801
+ // src/params/index.js
1804
1802
  var Params = class _Params {
1805
1803
  context;
1806
1804
  // Partial context during initialization
@@ -2060,7 +2058,7 @@ var Params = class _Params {
2060
2058
  definition = val;
2061
2059
  val = val.value;
2062
2060
  }
2063
- const def = this.assignDefinition(key, definition);
2061
+ this.assignDefinition(key, definition);
2064
2062
  if (!this.runAllRegisteredSetters(key, val)) {
2065
2063
  this.params[key] = val;
2066
2064
  }
@@ -2119,6 +2117,18 @@ var Params = class _Params {
2119
2117
  this._currentModule = prev;
2120
2118
  }
2121
2119
  }
2120
+ /**
2121
+ * Async variant of {@link runWithModule} for modules that await params.get().
2122
+ */
2123
+ async runWithModuleAsync(moduleName, fn) {
2124
+ const prev = this._currentModule;
2125
+ this._currentModule = moduleName;
2126
+ try {
2127
+ return await fn();
2128
+ } finally {
2129
+ this._currentModule = prev;
2130
+ }
2131
+ }
2122
2132
  /**
2123
2133
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2124
2134
  */
@@ -2179,14 +2189,14 @@ var Params = class _Params {
2179
2189
  }
2180
2190
  };
2181
2191
 
2182
- // src/screen.ts
2192
+ // src/index.js
2183
2193
  init_screen();
2184
2194
 
2185
- // src/filedatabase/index.ts
2195
+ // src/filedatabase/index.js
2186
2196
  var import_fs4 = __toESM(require("fs"), 1);
2187
2197
  var import_path4 = __toESM(require("path"), 1);
2188
2198
 
2189
- // src/utils/os-utils.ts
2199
+ // src/utils/os-utils.js
2190
2200
  var import_fs2 = __toESM(require("fs"), 1);
2191
2201
  var import_path2 = __toESM(require("path"), 1);
2192
2202
  var import_child_process = require("child_process");
@@ -2215,7 +2225,7 @@ function getFreeDiskSpace(targetPath) {
2215
2225
  }
2216
2226
  }
2217
2227
 
2218
- // src/utils/fs-utils.ts
2228
+ // src/utils/fs-utils.js
2219
2229
  var import_fs3 = __toESM(require("fs"), 1);
2220
2230
  var import_path3 = __toESM(require("path"), 1);
2221
2231
  async function ensurePath(...pathParts) {
@@ -2239,7 +2249,7 @@ function getFileExtension(dataType) {
2239
2249
  }
2240
2250
  }
2241
2251
 
2242
- // src/utils/format-utils.ts
2252
+ // src/utils/format-utils.js
2243
2253
  function bytesToHumanReadable(bytes) {
2244
2254
  if (bytes === 0) return "0 B";
2245
2255
  const k = 1024;
@@ -2248,7 +2258,7 @@ function bytesToHumanReadable(bytes) {
2248
2258
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
2249
2259
  }
2250
2260
 
2251
- // src/utils/date-utils.ts
2261
+ // src/utils/date-utils.js
2252
2262
  function isTimestampFolder(folderName) {
2253
2263
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
2254
2264
  if (!isoRegex.test(folderName)) {
@@ -2258,7 +2268,7 @@ function isTimestampFolder(folderName) {
2258
2268
  return !isNaN(date.getTime()) && date.getTime() > 0;
2259
2269
  }
2260
2270
 
2261
- // src/filedatabase/serializers.ts
2271
+ // src/filedatabase/serializers.js
2262
2272
  function detectDataType(data) {
2263
2273
  if (Array.isArray(data)) {
2264
2274
  return "json-array";
@@ -2290,7 +2300,7 @@ function deserializeData(rawData, dataType) {
2290
2300
  }
2291
2301
  }
2292
2302
 
2293
- // src/filedatabase/synopsis-functions.ts
2303
+ // src/filedatabase/synopsis-functions.js
2294
2304
  function defaultFileSynopsisFunction(fileEntry, data) {
2295
2305
  if (!Array.isArray(data) || data.length === 0) {
2296
2306
  return { ...fileEntry };
@@ -2358,7 +2368,7 @@ function defaultVersionSynopsisFunction(metadata) {
2358
2368
  return result;
2359
2369
  }
2360
2370
 
2361
- // src/filedatabase/index.ts
2371
+ // src/filedatabase/index.js
2362
2372
  var FileDatabase = class _FileDatabase {
2363
2373
  basePath;
2364
2374
  namespace;
@@ -2444,7 +2454,7 @@ var FileDatabase = class _FileDatabase {
2444
2454
  if (errors.length) {
2445
2455
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
2446
2456
  }
2447
- let parts = [this.basePath, this.namespace];
2457
+ const parts = [this.basePath, this.namespace];
2448
2458
  if (this.tableName) {
2449
2459
  parts.push(...this.tableName.split("/"));
2450
2460
  }
@@ -2937,14 +2947,17 @@ var FileDatabase = class _FileDatabase {
2937
2947
  * Prepare the instance for read or write operations
2938
2948
  * This discovers state and sets up internal members based on mode and current data
2939
2949
  */
2940
- async prepare({ write, read, version }) {
2950
+ async prepare(options) {
2951
+ const { write, read, version, deferInitialVersion } = options;
2941
2952
  if (write) {
2942
2953
  if (this.versioned) {
2943
2954
  if (this.currentVersion === null) {
2944
- await this.makeNewVersion();
2945
- this.metadata = this.getDefaultMetadata();
2946
- this.metadata.version = this.currentVersion;
2947
- this.makeNewFile();
2955
+ if (!deferInitialVersion) {
2956
+ await this.makeNewVersion();
2957
+ this.metadata = this.getDefaultMetadata();
2958
+ this.metadata.version = this.currentVersion;
2959
+ this.makeNewFile();
2960
+ }
2948
2961
  } else {
2949
2962
  if (!this.metadata.files.length) {
2950
2963
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -3054,7 +3067,7 @@ var FileDatabase = class _FileDatabase {
3054
3067
  if (options.forceNewVersion && !this.versioned) {
3055
3068
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
3056
3069
  }
3057
- await this.prepare({ write: true });
3070
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
3058
3071
  const incomingDataType = detectDataType(data);
3059
3072
  this.metadata.dataType = incomingDataType;
3060
3073
  if (options.forceNewVersion) {
@@ -3369,22 +3382,90 @@ function listSources(basePath) {
3369
3382
  }
3370
3383
  }
3371
3384
 
3372
- // src/db/index.ts
3385
+ // src/db/index.js
3373
3386
  var import_knex = __toESM(require("knex"), 1);
3387
+ var KNEX_DEFAULTS = {
3388
+ testConnection: true,
3389
+ pool: { min: 2, max: 10 },
3390
+ acquireConnectionTimeout: 1e4,
3391
+ ssl: { rejectUnauthorized: false }
3392
+ };
3374
3393
  var Db = class {
3375
- knexInstance = null;
3376
- config;
3377
- logger;
3378
- queriesLog = [];
3379
- isConnected = false;
3380
- /**
3381
- * Constructor - accepts config object
3382
- * Use dbInit() function to initialize with Context
3383
- */
3394
+ static async init(context, options = {}) {
3395
+ const buildConfig = async () => {
3396
+ const defs = {
3397
+ dbName: "string",
3398
+ dbProfile: "boolean default false"
3399
+ };
3400
+ const discovered = context?.params?.getAllForModule?.("db", defs) ?? {};
3401
+ const merged = { ...discovered, ...options };
3402
+ let { dbName, dbProfile } = merged;
3403
+ let dbConnectionString = options.dbConnectionString ?? options.connectionString;
3404
+ let connectionParam = dbConnectionString ? "options" : null;
3405
+ if (!dbConnectionString) {
3406
+ const src = context?.args?.getSource?.("dbConnectionString");
3407
+ if (src === "cli" || src === "overrides" || src === "config") {
3408
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
3409
+ connectionParam = "dbConnectionString";
3410
+ }
3411
+ }
3412
+ if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
3413
+ dbConnectionString = dbName;
3414
+ dbName = void 0;
3415
+ }
3416
+ if (!dbConnectionString && dbName) {
3417
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3418
+ dbConnectionString = await context.params.get(paramName, "string");
3419
+ connectionParam = paramName;
3420
+ if (!dbConnectionString) {
3421
+ throw new ParamError(
3422
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3423
+ );
3424
+ }
3425
+ }
3426
+ if (!dbConnectionString) {
3427
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
3428
+ if (dbConnectionString) {
3429
+ connectionParam = "dbConnectionString";
3430
+ }
3431
+ }
3432
+ if (!dbConnectionString) {
3433
+ if (!dbName) {
3434
+ dbName = "local";
3435
+ }
3436
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3437
+ dbConnectionString = await context.params.get(paramName, "string");
3438
+ connectionParam = paramName;
3439
+ if (!dbConnectionString) {
3440
+ throw new ParamError(
3441
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3442
+ );
3443
+ }
3444
+ }
3445
+ const displayName = resolveDbDisplayName(
3446
+ dbName,
3447
+ connectionParam,
3448
+ context?.args?.env,
3449
+ merged.name
3450
+ );
3451
+ return {
3452
+ ...KNEX_DEFAULTS,
3453
+ connectionString: dbConnectionString,
3454
+ name: displayName,
3455
+ profile: !!dbProfile,
3456
+ logger: context.logger
3457
+ };
3458
+ };
3459
+ const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
3460
+ return dbConnect(context, config2);
3461
+ }
3384
3462
  constructor(config2) {
3385
- if (!config2.connectionString) {
3463
+ if (!config2 || !config2.connectionString) {
3386
3464
  throw new ParamError("Db: connectionString is required");
3387
3465
  }
3466
+ this.knexInstance = null;
3467
+ this.isConnected = false;
3468
+ this.queriesLog = [];
3388
3469
  this.config = {
3389
3470
  testConnection: true,
3390
3471
  profile: false,
@@ -3392,30 +3473,27 @@ var Db = class {
3392
3473
  acquireConnectionTimeout: 1e4,
3393
3474
  ssl: { rejectUnauthorized: false },
3394
3475
  logger: console,
3395
- name: "default",
3396
3476
  ...config2
3397
3477
  };
3398
3478
  this.logger = this.config.logger;
3399
3479
  const instance2 = this;
3400
- const callableWrapper = function(...args) {
3480
+ const callableWrapper = function() {
3401
3481
  throw new Error("This should never be called directly");
3402
3482
  };
3403
3483
  callableWrapper._instance = instance2;
3404
3484
  return new Proxy(callableWrapper, {
3405
- // Intercept function calls: db('table')
3406
- apply: (target, thisArg, argumentsList) => {
3485
+ apply: (target, _thisArg, argumentsList) => {
3407
3486
  const inst = target._instance;
3408
3487
  if (!inst.knexInstance) {
3409
3488
  throw new Error("Db: Not connected. Call connect() first.");
3410
3489
  }
3411
3490
  return inst.knexInstance(...argumentsList);
3412
3491
  },
3413
- // Intercept property access: db.schema, db.raw, etc.
3414
3492
  get: (target, prop) => {
3415
3493
  if (prop === "_instance") {
3416
3494
  return target._instance;
3417
3495
  }
3418
- const instance3 = target._instance;
3496
+ const inst = target._instance;
3419
3497
  const ownMethods = [
3420
3498
  "connect",
3421
3499
  "disconnect",
@@ -3428,26 +3506,26 @@ var Db = class {
3428
3506
  "detectClient",
3429
3507
  "attachProfiler"
3430
3508
  ];
3431
- if (prop in instance3) {
3432
- const value = instance3[prop];
3509
+ if (prop in inst) {
3510
+ const value = inst[prop];
3433
3511
  if (typeof value === "function" && ownMethods.includes(prop)) {
3434
- return value.bind(instance3);
3512
+ return value.bind(inst);
3435
3513
  }
3436
3514
  if (typeof value !== "function") {
3437
3515
  return value;
3438
3516
  }
3439
3517
  }
3440
- if (instance3.knexInstance) {
3441
- const knexProp = instance3.knexInstance[prop];
3518
+ if (inst.knexInstance) {
3519
+ const knexProp = inst.knexInstance[prop];
3442
3520
  if (typeof knexProp === "function") {
3443
- return knexProp.bind(instance3.knexInstance);
3521
+ return knexProp.bind(inst.knexInstance);
3444
3522
  }
3445
3523
  return knexProp;
3446
3524
  }
3447
- if (prop in instance3) {
3448
- const method = instance3[prop];
3525
+ if (prop in inst) {
3526
+ const method = inst[prop];
3449
3527
  if (typeof method === "function") {
3450
- return method.bind(instance3);
3528
+ return method.bind(inst);
3451
3529
  }
3452
3530
  return method;
3453
3531
  }
@@ -3455,9 +3533,6 @@ var Db = class {
3455
3533
  }
3456
3534
  });
3457
3535
  }
3458
- /**
3459
- * Detect database client type from connection string
3460
- */
3461
3536
  detectClient(connectionString) {
3462
3537
  if (connectionString.match(/^postgresql/)) {
3463
3538
  return "pg";
@@ -3467,9 +3542,6 @@ var Db = class {
3467
3542
  }
3468
3543
  return null;
3469
3544
  }
3470
- /**
3471
- * Connect to the database
3472
- */
3473
3545
  async connect() {
3474
3546
  if (this.isConnected && this.knexInstance) {
3475
3547
  this.logger.warn?.("[Db] Already connected");
@@ -3478,14 +3550,13 @@ var Db = class {
3478
3550
  const client = this.detectClient(this.config.connectionString);
3479
3551
  if (!client) {
3480
3552
  throw new ParamError(
3481
- `Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
3553
+ "Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
3482
3554
  );
3483
3555
  }
3484
3556
  try {
3485
3557
  const connectionConfig = {
3486
3558
  connectionString: this.config.connectionString,
3487
3559
  family: 4
3488
- // Force IPv4 only (disable IPv6)
3489
3560
  };
3490
3561
  this.knexInstance = (0, import_knex.default)({
3491
3562
  client,
@@ -3501,7 +3572,7 @@ var Db = class {
3501
3572
  await this.testConnection();
3502
3573
  }
3503
3574
  this.isConnected = true;
3504
- this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
3575
+ this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
3505
3576
  } catch (error) {
3506
3577
  if (error instanceof ParamError) {
3507
3578
  throw error;
@@ -3510,9 +3581,6 @@ var Db = class {
3510
3581
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
3511
3582
  }
3512
3583
  }
3513
- /**
3514
- * Disconnect from the database
3515
- */
3516
3584
  async disconnect() {
3517
3585
  if (!this.knexInstance) {
3518
3586
  return;
@@ -3522,16 +3590,13 @@ var Db = class {
3522
3590
  this.knexInstance = null;
3523
3591
  this.isConnected = false;
3524
3592
  this.queriesLog = [];
3525
- this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
3593
+ this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
3526
3594
  } catch (error) {
3527
3595
  const errorMsg = this.getErrorMessage(error);
3528
3596
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
3529
3597
  throw error;
3530
3598
  }
3531
3599
  }
3532
- /**
3533
- * Extract error message from various error types
3534
- */
3535
3600
  getErrorMessage(error) {
3536
3601
  if (error instanceof AggregateError) {
3537
3602
  const errors = error.errors || [];
@@ -3556,9 +3621,11 @@ var Db = class {
3556
3621
  return `${code} (tried: ${addresses.join(", ")})`;
3557
3622
  }
3558
3623
  }
3559
- const uniqueMessages = [...new Set(errors.map((e) => {
3560
- return e instanceof Error ? e.message : String(e);
3561
- }))];
3624
+ const uniqueMessages = [
3625
+ ...new Set(
3626
+ errors.map((e) => e instanceof Error ? e.message : String(e))
3627
+ )
3628
+ ];
3562
3629
  if (uniqueMessages.length === 1) {
3563
3630
  return uniqueMessages[0];
3564
3631
  }
@@ -3567,28 +3634,25 @@ var Db = class {
3567
3634
  return error.message || "Multiple errors occurred";
3568
3635
  }
3569
3636
  if (error instanceof Error) {
3570
- const errorWithCode = error;
3571
- if (errorWithCode.code) {
3572
- return `${errorWithCode.code}: ${error.message || String(error)}`;
3637
+ const code = error.code;
3638
+ if (code) {
3639
+ return `${code}: ${error.message || String(error)}`;
3573
3640
  }
3574
3641
  return error.message || String(error);
3575
3642
  }
3576
3643
  if (typeof error === "string") {
3577
3644
  return error;
3578
3645
  }
3579
- if (error?.message) {
3646
+ if (error && typeof error === "object" && "message" in error) {
3580
3647
  const msg = String(error.message);
3581
- const errorWithCode = error;
3582
- if (errorWithCode.code) {
3583
- return `${errorWithCode.code}: ${msg}`;
3648
+ const code = error.code;
3649
+ if (code) {
3650
+ return `${code}: ${msg}`;
3584
3651
  }
3585
3652
  return msg;
3586
3653
  }
3587
3654
  return String(error) || "Unknown error";
3588
3655
  }
3589
- /**
3590
- * Test database connection
3591
- */
3592
3656
  async testConnection() {
3593
3657
  if (!this.knexInstance) {
3594
3658
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3604,9 +3668,6 @@ var Db = class {
3604
3668
  throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
3605
3669
  }
3606
3670
  }
3607
- /**
3608
- * Attach query profiler to log all queries
3609
- */
3610
3671
  attachProfiler() {
3611
3672
  if (!this.knexInstance) {
3612
3673
  return;
@@ -3616,7 +3677,7 @@ var Db = class {
3616
3677
  this.knexInstance.on("query", (query) => {
3617
3678
  query.__startTime = process.hrtime();
3618
3679
  });
3619
- this.knexInstance.on("query-response", (response, query) => {
3680
+ this.knexInstance.on("query-response", (_response, query) => {
3620
3681
  const [seconds, nanoseconds] = process.hrtime(query.__startTime);
3621
3682
  const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
3622
3683
  const logEntry = {
@@ -3631,15 +3692,9 @@ var Db = class {
3631
3692
  this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
3632
3693
  });
3633
3694
  }
3634
- /**
3635
- * Get query log (only available if profiling is enabled)
3636
- */
3637
3695
  getQueryLog() {
3638
3696
  return [...this.queriesLog];
3639
3697
  }
3640
- /**
3641
- * Check if a table exists
3642
- */
3643
3698
  async tableExists(tableName) {
3644
3699
  if (!this.knexInstance) {
3645
3700
  throw new Error("Db: Not connected. Call connect() first.");
@@ -3651,65 +3706,87 @@ var Db = class {
3651
3706
  throw error;
3652
3707
  }
3653
3708
  }
3654
- /**
3655
- * Get the underlying Knex instance (for advanced usage)
3656
- */
3657
3709
  getKnex() {
3658
3710
  if (!this.knexInstance) {
3659
3711
  throw new Error("Db: Not connected. Call connect() first.");
3660
3712
  }
3661
3713
  return this.knexInstance;
3662
3714
  }
3663
- /**
3664
- * Get connection status
3665
- */
3666
3715
  isConnectedToDb() {
3667
3716
  return this.isConnected && this.knexInstance !== null;
3668
3717
  }
3669
- /**
3670
- * Initialize Db with context (connects and registers disconnect cleanup).
3671
- * Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
3672
- */
3673
- static async init(context, dbNameOrConnectionString) {
3674
- return dbFindAndConnect(context, dbNameOrConnectionString);
3675
- }
3676
3718
  };
3677
3719
  function capitalizeFirstLetter(str) {
3678
3720
  return str.charAt(0).toUpperCase() + str.slice(1);
3679
3721
  }
3680
- async function dbConnect(context, connectionString, name, dbProfile) {
3681
- const defs = {
3682
- testDbConnection: "boolean default true",
3683
- name: "string",
3684
- poolMin: "number default 2",
3685
- poolMax: "number default 10",
3686
- acquireConnectionTimeout: "number default 10000",
3687
- sslRejectUnauthorized: "boolean default false"
3688
- };
3689
- const paramsConfig = context.params.getAllForModule(defs);
3690
- const config2 = {
3691
- connectionString,
3692
- name: paramsConfig.name || name || "default",
3693
- testConnection: paramsConfig.testDbConnection,
3694
- profile: dbProfile ?? false,
3695
- pool: {
3696
- min: paramsConfig.poolMin,
3697
- max: paramsConfig.poolMax
3698
- },
3699
- acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
3700
- ssl: {
3701
- rejectUnauthorized: paramsConfig.sslRejectUnauthorized
3702
- },
3703
- logger: context.logger
3704
- };
3722
+ function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
3723
+ if (dbName) {
3724
+ return dbName;
3725
+ }
3726
+ if (mergedName) {
3727
+ return mergedName;
3728
+ }
3729
+ if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
3730
+ return connectionParam.slice("dbConnectionString".length).toLowerCase();
3731
+ }
3732
+ if (connectionParam === "dbConnectionString" && argsEnv) {
3733
+ return argsEnv;
3734
+ }
3735
+ return void 0;
3736
+ }
3737
+ function formatDbConnectMessage(name, connectionString) {
3738
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
3739
+ if (name) {
3740
+ return `[Db] Connected to database "${name}"${endpointSuffix}`;
3741
+ }
3742
+ return `[Db] Connected${endpointSuffix}`;
3743
+ }
3744
+ function formatDbDisconnectMessage(name, connectionString) {
3745
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
3746
+ if (name) {
3747
+ return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
3748
+ }
3749
+ return `[Db] Disconnected${endpointSuffix}`;
3750
+ }
3751
+ function formatDbInstanceMessage(action, name) {
3752
+ if (name) {
3753
+ return `[Db] instance "${name}" ${action}`;
3754
+ }
3755
+ return `[Db] instance ${action}`;
3756
+ }
3757
+ function formatConnectionEndpointSuffix(connectionString) {
3758
+ const endpoint = formatConnectionEndpoint(connectionString);
3759
+ return endpoint ? ` (${endpoint})` : "";
3760
+ }
3761
+ function formatConnectionEndpoint(connectionString) {
3762
+ try {
3763
+ const url = new URL(connectionString);
3764
+ const host = url.hostname;
3765
+ if (!host) {
3766
+ return null;
3767
+ }
3768
+ let port = url.port;
3769
+ if (!port) {
3770
+ if (url.protocol === "postgresql:") {
3771
+ port = "5432";
3772
+ } else if (url.protocol === "mysql:") {
3773
+ port = "3306";
3774
+ }
3775
+ }
3776
+ return port ? `${host}:${port}` : host;
3777
+ } catch {
3778
+ return null;
3779
+ }
3780
+ }
3781
+ async function dbConnect(context, config2) {
3705
3782
  try {
3706
3783
  const db = new Db(config2);
3707
3784
  context.registerCleanup(async () => {
3708
3785
  await db.disconnect();
3709
- context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
3786
+ context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
3710
3787
  });
3711
3788
  await db.connect();
3712
- context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
3789
+ context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
3713
3790
  return db;
3714
3791
  } catch (error) {
3715
3792
  if (error instanceof ParamError) {
@@ -3719,52 +3796,255 @@ async function dbConnect(context, connectionString, name, dbProfile) {
3719
3796
  throw new ParamError(`[Db] connect error: ${errorMsg}`);
3720
3797
  }
3721
3798
  }
3722
- async function dbFindAndConnect(context, dbNameOrConnectionString) {
3723
- let dbName;
3724
- let dbConnectionString;
3725
- let dbProfile;
3726
- if (dbNameOrConnectionString) {
3727
- if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
3728
- dbName = void 0;
3729
- dbConnectionString = dbNameOrConnectionString;
3730
- } else {
3731
- dbName = dbNameOrConnectionString;
3732
- }
3733
- } else {
3734
- const defs = {
3735
- dbName: "string",
3736
- dbConnectionString: "string",
3737
- dbProfile: "boolean default false"
3799
+
3800
+ // src/s3/index.js
3801
+ var import_client_s3 = require("@aws-sdk/client-s3");
3802
+ var DEFAULT_PROFILE = "local";
3803
+ function capitalize(str) {
3804
+ if (!str) return str;
3805
+ return str.charAt(0).toUpperCase() + str.slice(1);
3806
+ }
3807
+ var S3 = class _S3 {
3808
+ /**
3809
+ * Build an S3 instance. Reads bucket profile from --bucket (default "local"),
3810
+ * then resolves per-profile params, builds the SDK client, and returns the
3811
+ * instance. Optionally pings the bucket once to verify reachability.
3812
+ */
3813
+ static async init(context, options = {}) {
3814
+ const profileDef = { bucket: "string" };
3815
+ const discovered = context?.params?.getAllForModule?.("s3", profileDef) ?? {};
3816
+ const profile = options.bucket ?? discovered.bucket ?? DEFAULT_PROFILE;
3817
+ const cap = capitalize(profile);
3818
+ const config2 = {
3819
+ profile,
3820
+ bucketName: options.bucketName ?? await context.params.get(`s3Bucket${cap}`, "string"),
3821
+ region: options.region ?? await context.params.get(`s3Region${cap}`, "string default us-east-1"),
3822
+ endpoint: options.endpoint ?? await context.params.get(`s3Endpoint${cap}`, "string"),
3823
+ forcePathStyle: options.forcePathStyle ?? await context.params.get(`s3ForcePathStyle${cap}`, "boolean default false"),
3824
+ accessKeyId: options.accessKeyId ?? await context.params.get(`s3AccessKeyId${cap}`, "string"),
3825
+ secretAccessKey: options.secretAccessKey ?? await context.params.get(`s3SecretAccessKey${cap}`, "string")
3738
3826
  };
3739
- const paramsConfig = context.params.getAll(defs);
3740
- dbName = paramsConfig.dbName;
3741
- dbConnectionString = paramsConfig.dbConnectionString;
3742
- dbProfile = paramsConfig.dbProfile;
3743
- }
3744
- if (!dbName && !dbConnectionString) {
3745
- throw new ParamError("Db: either dbName or dbConnectionString must be specified");
3746
- }
3747
- if (dbName) {
3748
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
3749
- dbConnectionString = await context.params.get(paramName, "string");
3750
- if (!dbConnectionString) {
3827
+ if (!config2.bucketName) {
3751
3828
  throw new ParamError(
3752
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
3829
+ `S3: bucket name not configured for profile "${profile}" (set s3Bucket${cap} or S3_BUCKET_${profile.toUpperCase()})`
3753
3830
  );
3754
3831
  }
3832
+ const s3 = new _S3(context, config2);
3833
+ if (options.testBucket !== false) {
3834
+ try {
3835
+ await s3.bucketExists();
3836
+ context.logger?.debug?.(
3837
+ `[S3] profile="${profile}" bucket="${config2.bucketName}" reachable`
3838
+ );
3839
+ } catch (err) {
3840
+ context.logger?.warn?.(
3841
+ `[S3] profile="${profile}" bucket="${config2.bucketName}" reachability test failed: ${err?.message ?? err}`
3842
+ );
3843
+ }
3844
+ }
3845
+ return s3;
3846
+ }
3847
+ constructor(context, config2) {
3848
+ this.logger = context?.logger ?? console;
3849
+ this.profile = config2.profile;
3850
+ this.bucketName = config2.bucketName;
3851
+ this.region = config2.region;
3852
+ this.endpoint = config2.endpoint || null;
3853
+ const clientConfig = { region: config2.region };
3854
+ if (config2.endpoint) clientConfig.endpoint = config2.endpoint;
3855
+ if (config2.forcePathStyle) clientConfig.forcePathStyle = true;
3856
+ if (config2.accessKeyId && config2.secretAccessKey) {
3857
+ clientConfig.credentials = {
3858
+ accessKeyId: config2.accessKeyId,
3859
+ secretAccessKey: config2.secretAccessKey
3860
+ };
3861
+ }
3862
+ this.client = new import_client_s3.S3Client(clientConfig);
3755
3863
  }
3756
- const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
3757
- return db;
3758
- }
3759
- async function dbInit(context, dbNameOrConnectionString) {
3760
- return await dbFindAndConnect(context, dbNameOrConnectionString);
3761
- }
3864
+ // ── info ────────────────────────────────────────────────────────────────
3865
+ getBucketName() {
3866
+ return this.bucketName;
3867
+ }
3868
+ getProfile() {
3869
+ return this.profile;
3870
+ }
3871
+ getRegion() {
3872
+ return this.region;
3873
+ }
3874
+ getEndpoint() {
3875
+ return this.endpoint;
3876
+ }
3877
+ // ── reachability ────────────────────────────────────────────────────────
3878
+ async bucketExists() {
3879
+ await this.client.send(new import_client_s3.HeadBucketCommand({ Bucket: this.bucketName }));
3880
+ return true;
3881
+ }
3882
+ // ── HEAD / GET ──────────────────────────────────────────────────────────
3883
+ /** Returns null on 404; never throws for "missing". Other errors throw. */
3884
+ async headObject(key) {
3885
+ try {
3886
+ const out = await this.client.send(new import_client_s3.HeadObjectCommand({
3887
+ Bucket: this.bucketName,
3888
+ Key: key
3889
+ }));
3890
+ return {
3891
+ etag: out.ETag,
3892
+ size: out.ContentLength,
3893
+ contentType: out.ContentType,
3894
+ lastModified: out.LastModified,
3895
+ metadata: out.Metadata,
3896
+ storageClass: out.StorageClass
3897
+ };
3898
+ } catch (err) {
3899
+ if (this._isNotFound(err)) return null;
3900
+ throw err;
3901
+ }
3902
+ }
3903
+ /** Returns { body: Readable, contentType, contentLength, etag, ... } or null on 404. */
3904
+ async getObject(key) {
3905
+ try {
3906
+ const out = await this.client.send(new import_client_s3.GetObjectCommand({
3907
+ Bucket: this.bucketName,
3908
+ Key: key
3909
+ }));
3910
+ return {
3911
+ body: out.Body,
3912
+ contentType: out.ContentType,
3913
+ contentLength: out.ContentLength,
3914
+ etag: out.ETag,
3915
+ lastModified: out.LastModified,
3916
+ metadata: out.Metadata
3917
+ };
3918
+ } catch (err) {
3919
+ if (this._isNotFound(err)) return null;
3920
+ throw err;
3921
+ }
3922
+ }
3923
+ /** Buffers the whole object. Use only for small objects (manifests, JSON). */
3924
+ async getObjectBytes(key) {
3925
+ const obj = await this.getObject(key);
3926
+ if (!obj) return null;
3927
+ const chunks = [];
3928
+ for await (const chunk of obj.body) chunks.push(chunk);
3929
+ return { ...obj, body: Buffer.concat(chunks) };
3930
+ }
3931
+ /** Convenience for JSON manifests. Returns parsed object or null on 404. */
3932
+ async getJson(key) {
3933
+ const obj = await this.getObjectBytes(key);
3934
+ if (!obj) return null;
3935
+ return JSON.parse(obj.body.toString("utf8"));
3936
+ }
3937
+ // ── PUT ─────────────────────────────────────────────────────────────────
3938
+ async putObject({ key, body, contentType, contentLength, tags, metadata }) {
3939
+ const cmd = new import_client_s3.PutObjectCommand({
3940
+ Bucket: this.bucketName,
3941
+ Key: key,
3942
+ Body: body,
3943
+ ...contentType && { ContentType: contentType },
3944
+ ...contentLength != null && { ContentLength: contentLength },
3945
+ ...metadata && { Metadata: metadata },
3946
+ ...tags && { Tagging: this._tagsToQuery(tags) }
3947
+ });
3948
+ return this.client.send(cmd);
3949
+ }
3950
+ /** Convenience for JSON manifests. */
3951
+ async putJson(key, value, opts = {}) {
3952
+ const json = JSON.stringify(value, null, opts.pretty ? 2 : 0);
3953
+ const body = Buffer.from(json, "utf8");
3954
+ return this.putObject({
3955
+ key,
3956
+ body,
3957
+ contentType: "application/json",
3958
+ contentLength: body.length,
3959
+ tags: opts.tags,
3960
+ metadata: opts.metadata
3961
+ });
3962
+ }
3963
+ // ── DELETE ──────────────────────────────────────────────────────────────
3964
+ async deleteObject(key) {
3965
+ return this.client.send(new import_client_s3.DeleteObjectCommand({
3966
+ Bucket: this.bucketName,
3967
+ Key: key
3968
+ }));
3969
+ }
3970
+ // ── COPY (for migration: legacy → new bucket, or intra-bucket "rename") ─
3971
+ async copyObject({ sourceBucket, sourceKey, key, contentType, metadata, tags }) {
3972
+ const src = sourceBucket || this.bucketName;
3973
+ const cmd = new import_client_s3.CopyObjectCommand({
3974
+ Bucket: this.bucketName,
3975
+ Key: key,
3976
+ CopySource: encodeURIComponent(`${src}/${sourceKey}`),
3977
+ ...contentType && {
3978
+ ContentType: contentType,
3979
+ MetadataDirective: "REPLACE"
3980
+ },
3981
+ ...metadata && {
3982
+ Metadata: metadata,
3983
+ MetadataDirective: "REPLACE"
3984
+ },
3985
+ ...tags && {
3986
+ Tagging: this._tagsToQuery(tags),
3987
+ TaggingDirective: "REPLACE"
3988
+ }
3989
+ });
3990
+ return this.client.send(cmd);
3991
+ }
3992
+ // ── LIST ────────────────────────────────────────────────────────────────
3993
+ async listObjects(prefix, { keysOnly = false, maxKeys = 1e3, continuationToken } = {}) {
3994
+ const out = await this.client.send(new import_client_s3.ListObjectsV2Command({
3995
+ Bucket: this.bucketName,
3996
+ Prefix: prefix,
3997
+ MaxKeys: maxKeys,
3998
+ ContinuationToken: continuationToken
3999
+ }));
4000
+ const items = (out.Contents ?? []).map((o) => ({
4001
+ key: o.Key,
4002
+ size: o.Size,
4003
+ etag: o.ETag,
4004
+ lastModified: o.LastModified,
4005
+ storageClass: o.StorageClass
4006
+ }));
4007
+ return {
4008
+ items: keysOnly ? items.map((i) => i.key) : items,
4009
+ isTruncated: !!out.IsTruncated,
4010
+ nextContinuationToken: out.NextContinuationToken
4011
+ };
4012
+ }
4013
+ // ── TAGS (used for lifecycle rules, e.g. status=closed → Glacier IR) ────
4014
+ async putObjectTagging(key, tags) {
4015
+ return this.client.send(new import_client_s3.PutObjectTaggingCommand({
4016
+ Bucket: this.bucketName,
4017
+ Key: key,
4018
+ Tagging: { TagSet: this._tagsToTagSet(tags) }
4019
+ }));
4020
+ }
4021
+ async getObjectTagging(key) {
4022
+ const out = await this.client.send(new import_client_s3.GetObjectTaggingCommand({
4023
+ Bucket: this.bucketName,
4024
+ Key: key
4025
+ }));
4026
+ const tags = {};
4027
+ for (const t of out.TagSet ?? []) tags[t.Key] = t.Value;
4028
+ return tags;
4029
+ }
4030
+ // ── internals ───────────────────────────────────────────────────────────
4031
+ _isNotFound(err) {
4032
+ const status = err?.$metadata?.httpStatusCode;
4033
+ return status === 404 || err?.name === "NotFound" || err?.name === "NoSuchKey" || err?.Code === "NoSuchKey";
4034
+ }
4035
+ _tagsToQuery(tags) {
4036
+ return Object.entries(tags).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
4037
+ }
4038
+ _tagsToTagSet(tags) {
4039
+ return Object.entries(tags).map(([Key, Value]) => ({ Key, Value: String(Value) }));
4040
+ }
4041
+ };
3762
4042
 
3763
- // src/logger/index.ts
4043
+ // src/logger/index.js
3764
4044
  var import_chalk = __toESM(require("chalk"), 1);
3765
4045
  var import_util = __toESM(require("util"), 1);
3766
4046
 
3767
- // src/logger/transports.ts
4047
+ // src/logger/transports.js
3768
4048
  var ConsoleTransport = class {
3769
4049
  write(payload) {
3770
4050
  console.info(payload);
@@ -3784,7 +4064,7 @@ var ParentProcessTransport = class {
3784
4064
  }
3785
4065
  };
3786
4066
 
3787
- // src/logger/index.ts
4067
+ // src/logger/index.js
3788
4068
  var ALL_LEVELS = [
3789
4069
  "silly",
3790
4070
  "debug",
@@ -3857,6 +4137,7 @@ var Logger = class _Logger {
3857
4137
  }
3858
4138
  /**
3859
4139
  * Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
4140
+ * Params are tracked under the `logger` module for --showUsedParams.
3860
4141
  */
3861
4142
  static init(context, options) {
3862
4143
  const paramDefs = {
@@ -3870,7 +4151,7 @@ var Logger = class _Logger {
3870
4151
  progressWithTimes: "boolean default false",
3871
4152
  progressThrottleMs: "number"
3872
4153
  };
3873
- const discovered = context.params.getAllForModule(paramDefs);
4154
+ const discovered = context.params.getAllForModule("logger", paramDefs);
3874
4155
  const config2 = { ...discovered, ...options };
3875
4156
  const logger = new _Logger(context, config2);
3876
4157
  context.logger = logger;
@@ -4066,9 +4347,9 @@ var Logger = class _Logger {
4066
4347
  }
4067
4348
  };
4068
4349
 
4069
- // src/init/index.ts
4350
+ // src/init/index.js
4070
4351
  var import_events = require("events");
4071
- function extractComponentOptions(opts, componentName) {
4352
+ function extractComponentOptions(opts, _componentName) {
4072
4353
  const reservedKeys = ["overrides", "defaults", "modules"];
4073
4354
  const componentOptions = {};
4074
4355
  for (const [key, value] of Object.entries(opts)) {
@@ -4113,7 +4394,10 @@ function setupContext(opts = {}) {
4113
4394
  return setup(opts);
4114
4395
  }
4115
4396
 
4116
- // src/utils/core-utils.ts
4397
+ // src/tasks/index.js
4398
+ var import_node_os3 = __toESM(require("os"), 1);
4399
+
4400
+ // src/utils/core-utils.js
4117
4401
  function sleepMs(ms) {
4118
4402
  return new Promise((resolve2) => setTimeout(resolve2, ms));
4119
4403
  }
@@ -4122,14 +4406,82 @@ function toJsonColumn(value) {
4122
4406
  return JSON.stringify(value);
4123
4407
  }
4124
4408
 
4125
- // src/tasks/servicesRegistry.ts
4126
- var import_node_crypto2 = require("crypto");
4127
- var import_promises = require("fs/promises");
4409
+ // src/tasks/servicesRegistry.js
4128
4410
  var import_node_os = __toESM(require("os"), 1);
4129
- var import_node_path = __toESM(require("path"), 1);
4130
4411
 
4131
- // src/tasks/taskUtils.ts
4412
+ // src/tasks/taskUtils.js
4132
4413
  var import_node_crypto = require("crypto");
4414
+
4415
+ // src/tasks/time-matcher.js
4416
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4417
+ function resolveAsterisks(field, range) {
4418
+ return field.includes("*") ? field.replace("*", range) : field;
4419
+ }
4420
+ function resolveRanges(field) {
4421
+ const regex = /(\d+)-(\d+)/;
4422
+ let current = field;
4423
+ while (true) {
4424
+ const match = regex.exec(current);
4425
+ if (!match) break;
4426
+ const raw = match[0];
4427
+ let first = Number(match[1]);
4428
+ let last = Number(match[2]);
4429
+ if (last < first) {
4430
+ [first, last] = [last, first];
4431
+ }
4432
+ const values = [];
4433
+ for (let i = first; i <= last; i += 1) {
4434
+ values.push(i);
4435
+ }
4436
+ current = current.replace(raw, values.join(","));
4437
+ }
4438
+ return current;
4439
+ }
4440
+ function resolveSteps(field) {
4441
+ const match = /^(.+)\/(\d+)$/.exec(field);
4442
+ if (!match) return field;
4443
+ const base = match[1];
4444
+ const step = Number(match[2]);
4445
+ if (!Number.isFinite(step) || step <= 0) return field;
4446
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4447
+ }
4448
+ function convertPattern(pattern) {
4449
+ const parts = pattern.trim().split(/\s+/);
4450
+ if (parts.length !== 6) {
4451
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4452
+ }
4453
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4454
+ }
4455
+ function fieldMatches(field, value) {
4456
+ const allowed = field.split(",").map((v) => Number(v));
4457
+ return allowed.includes(value);
4458
+ }
4459
+ function matchesParsedPattern(parsed, date) {
4460
+ 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());
4461
+ }
4462
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4463
+ const parsed = convertPattern(pattern);
4464
+ return matchesParsedPattern(parsed, date);
4465
+ }
4466
+ var MS_PER_SECOND = 1e3;
4467
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
4468
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
4469
+ const parsed = convertPattern(pattern);
4470
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
4471
+ const end = t + maxSearchMs;
4472
+ while (t <= end) {
4473
+ const date = new Date(t);
4474
+ if (matchesParsedPattern(parsed, date)) {
4475
+ return date;
4476
+ }
4477
+ t += MS_PER_SECOND;
4478
+ }
4479
+ throw new Error(
4480
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
4481
+ );
4482
+ }
4483
+
4484
+ // src/tasks/taskUtils.js
4133
4485
  function getDb(context) {
4134
4486
  const db = context.db;
4135
4487
  if (!db) {
@@ -4137,113 +4489,117 @@ function getDb(context) {
4137
4489
  }
4138
4490
  return db;
4139
4491
  }
4140
- function queueToTableNames(queue) {
4141
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4142
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4143
- }
4492
+ function queueToTableNames(queueName) {
4144
4493
  return {
4145
- tasksTable: queue,
4146
- historyTable: `${queue}_history`
4494
+ tasksTable: queueName,
4495
+ historyTable: `${queueName}_history`,
4496
+ registryTable: `${queueName}_services_registry`
4147
4497
  };
4148
4498
  }
4149
- function servicesRegistryTable(queue) {
4150
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4151
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4152
- }
4153
- return `${queue}_services_registry`;
4499
+ function defineTasksTable(t, db, tableNameForIndex) {
4500
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4501
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4502
+ t.timestamp("started_at");
4503
+ t.timestamp("completed_at");
4504
+ t.integer("priority").notNullable().defaultTo(50);
4505
+ t.text("schedule");
4506
+ t.timestamp("next_run_at").defaultTo(null);
4507
+ t.timestamp("past_due").defaultTo(null);
4508
+ t.text("name").notNullable();
4509
+ t.text("opid");
4510
+ t.json("params");
4511
+ t.text("service_group");
4512
+ t.integer("instance_number");
4513
+ t.text("service_name");
4514
+ t.text("server_name");
4515
+ t.text("status").notNullable().defaultTo("idle");
4516
+ t.timestamp("status_changed_at").defaultTo(null);
4517
+ t.text("progress");
4518
+ t.boolean("success");
4519
+ t.json("results");
4520
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
4521
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
4522
+ }
4523
+ function taskHistoryInsertFromQueueRow(row, overrides) {
4524
+ const { id, ...snapshot } = row;
4525
+ void id;
4526
+ return {
4527
+ ...snapshot,
4528
+ ...overrides
4529
+ };
4154
4530
  }
4155
4531
  async function ensureTaskTables(context, options = {}) {
4156
- const queue = options.queue ?? "tasks";
4532
+ const queueName = options.queueName ?? "tasks";
4157
4533
  const recreate = options.recreate ?? false;
4158
4534
  const db = getDb(context);
4159
- const { tasksTable, historyTable } = queueToTableNames(queue);
4535
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
4160
4536
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
4161
4537
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
4538
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
4162
4539
  if (recreate) {
4163
4540
  await db.schema.dropTableIfExists(historyTable);
4164
4541
  await db.schema.dropTableIfExists(tasksTable);
4542
+ await db.schema.dropTableIfExists(registryTable);
4165
4543
  }
4166
4544
  if (needsTasks) {
4167
4545
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
4168
4546
  await db.schema.createTable(tasksTable, (t) => {
4169
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4170
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4171
- t.timestamp("started_at");
4172
- t.timestamp("completed_at");
4173
- t.integer("priority").notNullable().defaultTo(0);
4174
- t.text("schedule");
4175
- t.timestamp("past_due").defaultTo(null);
4176
- t.text("target").notNullable();
4177
- t.text("task").notNullable();
4178
- t.json("params");
4179
- t.text("opid");
4180
- t.timestamp("paused_at").defaultTo(null);
4181
- t.text("progress");
4182
- t.boolean("success");
4183
- t.json("results");
4184
- });
4185
- await db.schema.alterTable(tasksTable, (t) => {
4186
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
4187
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
4188
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
4547
+ defineTasksTable(t, db, tasksTable);
4189
4548
  });
4190
4549
  }
4191
4550
  if (needsHistory) {
4192
4551
  await db.schema.createTable(historyTable, (t) => {
4193
- t.uuid("id").notNullable();
4194
- t.timestamp("created_at").notNullable();
4195
- t.timestamp("started_at");
4196
- t.timestamp("completed_at");
4197
- t.integer("priority").notNullable().defaultTo(0);
4198
- t.text("schedule");
4199
- t.timestamp("past_due").defaultTo(null);
4200
- t.text("target").notNullable();
4201
- t.text("task").notNullable();
4202
- t.json("params");
4203
- t.text("opid");
4204
- t.text("progress");
4205
- t.boolean("success");
4206
- t.json("results");
4207
- });
4208
- await db.schema.alterTable(historyTable, (t) => {
4209
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
4210
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
4552
+ defineTasksTable(t, db, historyTable);
4211
4553
  });
4212
4554
  }
4213
- const registryTable = servicesRegistryTable(queue);
4214
- const needsRegistry = !await db.tableExists(registryTable);
4215
4555
  if (needsRegistry) {
4216
4556
  await db.schema.createTable(registryTable, (t) => {
4217
4557
  t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4218
- t.uuid("instance_id").notNullable().unique();
4219
- t.text("queue").notNullable();
4558
+ t.text("queue_name").notNullable();
4220
4559
  t.text("service_group").notNullable();
4560
+ t.integer("instance_number").notNullable().defaultTo(1);
4221
4561
  t.text("service_name").notNullable();
4222
- t.text("target").notNullable();
4223
- t.text("hostname");
4562
+ t.text("server_name").notNullable();
4224
4563
  t.integer("pid");
4225
4564
  t.json("metadata");
4226
4565
  t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4227
4566
  t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
4228
- t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
4229
- t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
4230
- t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
4567
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
4568
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
4569
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
4231
4570
  });
4232
4571
  }
4233
4572
  }
4234
4573
  async function enqueueTask(context, options) {
4235
4574
  const db = getDb(context);
4236
- const queue = options.queue ?? "tasks";
4237
- const { tasksTable } = queueToTableNames(queue);
4575
+ const queueName = options.queueName ?? "tasks";
4576
+ const { tasksTable } = queueToTableNames(queueName);
4238
4577
  const id = (0, import_node_crypto.randomUUID)();
4578
+ const name = options.name ?? options.task;
4579
+ if (!name) {
4580
+ throw new Error("enqueueTask: name (or task) is required");
4581
+ }
4582
+ const schedule = options.schedule?.trim() ? options.schedule : null;
4583
+ let nextRunAt = null;
4584
+ if (options.nextRunAt !== void 0) {
4585
+ nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
4586
+ } else if (schedule) {
4587
+ nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
4588
+ }
4239
4589
  await db(tasksTable).insert({
4240
4590
  id,
4241
- target: options.target,
4242
- task: options.task,
4591
+ name,
4243
4592
  params: toJsonColumn(options.params ?? null),
4244
4593
  opid: options.opid ?? null,
4245
- priority: options.priority ?? 0,
4246
- schedule: options.schedule ?? null
4594
+ priority: options.priority ?? 50,
4595
+ schedule,
4596
+ next_run_at: nextRunAt,
4597
+ service_group: options.serviceGroup ?? null,
4598
+ instance_number: options.instanceNumber ?? null,
4599
+ service_name: options.serviceName ?? null,
4600
+ server_name: options.serverName ?? null,
4601
+ status: "idle",
4602
+ status_changed_at: db.fn.now()
4247
4603
  });
4248
4604
  return id;
4249
4605
  }
@@ -4254,7 +4610,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
4254
4610
  });
4255
4611
  }
4256
4612
 
4257
- // src/tasks/servicesRegistry.ts
4613
+ // src/tasks/servicesRegistry.js
4258
4614
  function getDb2(context) {
4259
4615
  const db = context.db;
4260
4616
  if (!db) {
@@ -4268,7 +4624,7 @@ function parseMetadataColumn(value) {
4268
4624
  if (typeof value === "string") {
4269
4625
  try {
4270
4626
  const p = JSON.parse(value);
4271
- return p && typeof p === "object" && !Array.isArray(p) ? p : {};
4627
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
4272
4628
  } catch {
4273
4629
  return {};
4274
4630
  }
@@ -4278,6 +4634,7 @@ function parseMetadataColumn(value) {
4278
4634
  var DEFAULT_GROUP_MAX_INSTANCES = {
4279
4635
  intake: 1,
4280
4636
  harvest: 1,
4637
+ harvester: 0,
4281
4638
  loader: 0,
4282
4639
  photos: 0,
4283
4640
  photosprocessor: 0,
@@ -4287,25 +4644,6 @@ function sanitizeNamePart(raw) {
4287
4644
  const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4288
4645
  return s.slice(0, 80) || "runner";
4289
4646
  }
4290
- function identityFilePath(identityDir, queue, serviceGroup) {
4291
- const safeQ = sanitizeNamePart(queue);
4292
- const safeG = sanitizeNamePart(serviceGroup);
4293
- return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
4294
- }
4295
- async function readIdentityFile(filePath) {
4296
- try {
4297
- const text = await (0, import_promises.readFile)(filePath, "utf8");
4298
- const parsed = JSON.parse(text);
4299
- return parsed && typeof parsed === "object" ? parsed : {};
4300
- } catch {
4301
- return {};
4302
- }
4303
- }
4304
- async function writeIdentityFile(filePath, data) {
4305
- await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
4306
- await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
4307
- `, "utf8");
4308
- }
4309
4647
  function resolveMaxInstances(serviceGroup, override) {
4310
4648
  if (override !== void 0 && Number.isFinite(override)) {
4311
4649
  return Math.max(0, Math.floor(Number(override)));
@@ -4313,179 +4651,190 @@ function resolveMaxInstances(serviceGroup, override) {
4313
4651
  const g = serviceGroup.trim().toLowerCase();
4314
4652
  return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
4315
4653
  }
4316
- async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
4654
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
4317
4655
  const cutoff = new Date(Date.now() - staleMs);
4318
- let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
4319
- if (excludeInstanceId) {
4320
- q = q.whereNot("instance_id", excludeInstanceId);
4656
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
4657
+ if (excludeRowId) {
4658
+ q = q.whereNot("id", excludeRowId);
4321
4659
  }
4322
4660
  const row = await q.count("id as count").first();
4323
4661
  return Number(row?.count ?? 0);
4324
4662
  }
4663
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
4664
+ const cutoff = new Date(Date.now() - staleMs);
4665
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
4666
+ const set = /* @__PURE__ */ new Set();
4667
+ for (const r of rows) {
4668
+ const n = Number(r.instance_number);
4669
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
4670
+ }
4671
+ return set;
4672
+ }
4325
4673
  function isUniqueViolation(error) {
4326
4674
  const code = error?.code ?? error?.errno;
4327
4675
  return code === "23505" || String(error?.message || "").includes("duplicate key");
4328
4676
  }
4677
+ function buildMetadata(options) {
4678
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
4679
+ if (options.target) {
4680
+ base.runnerTarget = options.target;
4681
+ }
4682
+ return toJsonColumn(Object.keys(base).length ? base : null);
4683
+ }
4684
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
4685
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
4686
+ const e = Math.max(1, Math.floor(Number(explicit)));
4687
+ if (occupied.has(e)) {
4688
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
4689
+ }
4690
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
4691
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
4692
+ }
4693
+ return e;
4694
+ }
4695
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
4696
+ for (let n = 1; n <= cap; n++) {
4697
+ if (!occupied.has(n)) return n;
4698
+ }
4699
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
4700
+ }
4701
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
4702
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
4703
+ }
4329
4704
  async function registerInServicesRegistry(context, options) {
4330
4705
  const db = getDb2(context);
4331
- const registryTable = servicesRegistryTable(options.queue);
4706
+ const registryTable = queueToTableNames(options.queueName).registryTable;
4332
4707
  const serviceGroup = options.serviceGroup.trim();
4333
4708
  if (!serviceGroup) {
4334
4709
  throw new Error("registerInServicesRegistry: serviceGroup is required");
4335
4710
  }
4336
- const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
4337
- let identity = await readIdentityFile(identityPath);
4338
- let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
4339
- identity.instanceId = instanceId;
4340
- await writeIdentityFile(identityPath, identity);
4341
- const hostname = import_node_os.default.hostname();
4711
+ const serverName = import_node_os.default.hostname();
4342
4712
  const pid = typeof process.pid === "number" ? process.pid : null;
4343
- const meta = toJsonColumn(options.metadata ?? null);
4344
- const existing = await db(registryTable).where({ instance_id: instanceId }).first();
4345
- if (existing) {
4346
- await db(registryTable).where({ instance_id: instanceId }).update({
4347
- target: options.target,
4348
- hostname,
4349
- pid,
4350
- metadata: meta,
4351
- last_seen_at: db.fn.now()
4352
- });
4353
- const serviceName = String(existing.service_name);
4354
- identity.serviceName = serviceName;
4355
- await writeIdentityFile(identityPath, identity);
4356
- const reg = {
4357
- instanceId,
4358
- serviceName,
4359
- serviceGroup,
4360
- queue: options.queue,
4361
- target: options.target,
4362
- rowId: String(existing.id)
4363
- };
4364
- context.servicesRegistry = reg;
4365
- context.runnerHeartbeat = reg;
4366
- context.logger.info?.(
4367
- `[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
4368
- );
4369
- return {
4370
- instanceId,
4371
- serviceName,
4372
- serviceGroup,
4373
- queue: options.queue,
4374
- target: options.target,
4375
- rowId: String(existing.id),
4376
- registryTable
4377
- };
4378
- }
4713
+ const meta = buildMetadata(options);
4714
+ const groupBase = sanitizeNamePart(serviceGroup);
4715
+ const hostBase = sanitizeNamePart(serverName);
4379
4716
  const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
4380
- const aliveOthers = await countAliveInGroup(
4381
- db,
4382
- registryTable,
4383
- options.queue,
4384
- serviceGroup,
4385
- options.staleMs,
4386
- instanceId
4387
- );
4388
- if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
4389
- const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
4717
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
4718
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
4719
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
4390
4720
  if (options.enforceMaxInstances) {
4391
4721
  throw new Error(msg);
4392
4722
  }
4393
4723
  context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
4394
4724
  }
4395
- const explicitName = options.serviceName?.trim();
4396
- const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
4397
- const hostBase = sanitizeNamePart(hostname);
4398
- const groupBase = sanitizeNamePart(serviceGroup);
4399
- const baseCandidates = [];
4400
- if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
4401
- if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
4402
- baseCandidates.push(`${groupBase}-${hostBase}`);
4403
- baseCandidates.push(groupBase);
4404
- function* eachServiceNameCandidate(bases) {
4405
- const seen = /* @__PURE__ */ new Set();
4406
- for (const rawBase of bases) {
4407
- const base = sanitizeNamePart(rawBase);
4408
- if (!base) continue;
4409
- const seq = [base];
4410
- for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
4411
- for (const c of seq) {
4412
- if (seen.has(c)) continue;
4413
- seen.add(c);
4414
- yield c;
4415
- }
4416
- }
4417
- }
4418
- let inserted;
4419
- for (const candidate of eachServiceNameCandidate(baseCandidates)) {
4725
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
4726
+ const cutoff = new Date(Date.now() - options.staleMs);
4727
+ const MAX_ATTEMPTS = 8;
4728
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
4729
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
4730
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
4731
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
4732
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
4733
+ if (existing) {
4734
+ const lastSeen = new Date(existing.last_seen_at);
4735
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
4736
+ if (isAlive) {
4737
+ if (options.serviceName?.trim()) {
4738
+ throw new Error(
4739
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
4740
+ );
4741
+ }
4742
+ context.logger.warn?.(
4743
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
4744
+ );
4745
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
4746
+ throw new Error(
4747
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
4748
+ );
4749
+ }
4750
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
4751
+ continue;
4752
+ }
4753
+ await db(registryTable).where({ id: existing.id }).update({
4754
+ server_name: serverName,
4755
+ pid,
4756
+ metadata: meta,
4757
+ service_group: serviceGroup,
4758
+ instance_number: instanceNumber,
4759
+ last_seen_at: db.fn.now()
4760
+ });
4761
+ const reg = {
4762
+ serviceName: serviceNameRaw,
4763
+ serviceGroup,
4764
+ queueName: options.queueName,
4765
+ target: options.target,
4766
+ rowId: String(existing.id),
4767
+ registryTable,
4768
+ instanceNumber
4769
+ };
4770
+ context.servicesRegistry = reg;
4771
+ context.runnerHeartbeat = reg;
4772
+ context.logger.info?.(
4773
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
4774
+ );
4775
+ return reg;
4776
+ }
4420
4777
  try {
4421
4778
  const rows = await db(registryTable).insert({
4422
- instance_id: instanceId,
4423
- queue: options.queue,
4779
+ queue_name: options.queueName,
4424
4780
  service_group: serviceGroup,
4425
- service_name: candidate,
4426
- target: options.target,
4427
- hostname,
4781
+ instance_number: instanceNumber,
4782
+ service_name: serviceNameRaw,
4783
+ server_name: serverName,
4428
4784
  pid,
4429
4785
  metadata: meta,
4430
- last_seen_at: db.fn.now()
4786
+ last_seen_at: db.fn.now(),
4787
+ created_at: db.fn.now()
4431
4788
  }).returning(["id", "service_name"]);
4432
4789
  const row = Array.isArray(rows) ? rows[0] : rows;
4433
- if (row) {
4434
- inserted = { id: String(row.id), service_name: String(row.service_name) };
4435
- break;
4790
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
4791
+ if (!rowId) {
4792
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
4793
+ rowId = again?.id != null ? String(again.id) : "";
4436
4794
  }
4795
+ if (!rowId) continue;
4796
+ const regNew = {
4797
+ serviceName: String(row?.service_name ?? serviceNameRaw),
4798
+ serviceGroup,
4799
+ queueName: options.queueName,
4800
+ target: options.target,
4801
+ rowId,
4802
+ registryTable,
4803
+ instanceNumber
4804
+ };
4805
+ context.servicesRegistry = regNew;
4806
+ context.runnerHeartbeat = regNew;
4807
+ context.logger.info?.(
4808
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
4809
+ );
4810
+ return regNew;
4437
4811
  } catch (error) {
4438
4812
  if (!isUniqueViolation(error)) {
4439
4813
  throw error;
4440
4814
  }
4815
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
4441
4816
  }
4442
4817
  }
4443
- if (!inserted) {
4444
- throw new Error(
4445
- `[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
4446
- );
4447
- }
4448
- identity.serviceName = inserted.service_name;
4449
- await writeIdentityFile(identityPath, identity);
4450
- const regNew = {
4451
- instanceId,
4452
- serviceName: inserted.service_name,
4453
- serviceGroup,
4454
- queue: options.queue,
4455
- target: options.target,
4456
- rowId: inserted.id
4457
- };
4458
- context.servicesRegistry = regNew;
4459
- context.runnerHeartbeat = regNew;
4460
- context.logger.info?.(
4461
- `[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
4818
+ throw new Error(
4819
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
4462
4820
  );
4463
- return {
4464
- instanceId,
4465
- serviceName: inserted.service_name,
4466
- serviceGroup,
4467
- queue: options.queue,
4468
- target: options.target,
4469
- rowId: inserted.id,
4470
- registryTable
4471
- };
4472
4821
  }
4473
4822
  async function touchServicesRegistry(context, registration) {
4474
4823
  const db = getDb2(context);
4475
- const hostname = import_node_os.default.hostname();
4824
+ const serverName = import_node_os.default.hostname();
4476
4825
  const pid = typeof process.pid === "number" ? process.pid : null;
4477
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
4826
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4478
4827
  last_seen_at: db.fn.now(),
4479
- hostname,
4828
+ server_name: serverName,
4480
4829
  pid
4481
4830
  });
4482
4831
  }
4483
4832
  async function updateServicesRegistryMetadata(context, registration, patch) {
4484
4833
  const db = getDb2(context);
4485
- const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
4834
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
4486
4835
  const prev = parseMetadataColumn(row?.metadata);
4487
4836
  const merged = { ...prev, ...patch };
4488
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
4837
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4489
4838
  metadata: toJsonColumn(merged),
4490
4839
  last_seen_at: db.fn.now()
4491
4840
  });
@@ -4493,14 +4842,14 @@ async function updateServicesRegistryMetadata(context, registration, patch) {
4493
4842
  }
4494
4843
  async function unregisterServicesRegistry(context, registration) {
4495
4844
  const db = getDb2(context);
4496
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
4497
- context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
4845
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
4846
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
4498
4847
  }
4499
- async function listServicesRegistry(context, options = { queue: "tasks" }) {
4848
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
4500
4849
  const db = getDb2(context);
4501
4850
  const staleMs = options.staleMs ?? 6e4;
4502
4851
  const cutoff = new Date(Date.now() - staleMs);
4503
- const table = servicesRegistryTable(options.queue);
4852
+ const table = queueToTableNames(options.queueName).registryTable;
4504
4853
  let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
4505
4854
  if (options.serviceGroup?.trim()) {
4506
4855
  q = q.where({ service_group: options.serviceGroup.trim() });
@@ -4508,7 +4857,8 @@ async function listServicesRegistry(context, options = { queue: "tasks" }) {
4508
4857
  return await q;
4509
4858
  }
4510
4859
 
4511
- // src/tasks/taskLogs.ts
4860
+ // src/tasks/taskLogs.js
4861
+ var import_node_path = __toESM(require("path"), 1);
4512
4862
  function getLogsState(context) {
4513
4863
  const holder = context;
4514
4864
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -4561,6 +4911,96 @@ function getLogsState(context) {
4561
4911
  holder.__tasksLogsState = state;
4562
4912
  return state;
4563
4913
  }
4914
+ function ipcLogTargetKey(target) {
4915
+ const bp = target.basePath ?? "";
4916
+ const ns = target.namespace ?? "";
4917
+ return `${bp}::${ns}::${target.tableName}`;
4918
+ }
4919
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
4920
+ const seg = (s) => {
4921
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
4922
+ return t.length ? t : "x";
4923
+ };
4924
+ return `${seg(source)}/${seg(resource)}`;
4925
+ }
4926
+ async function readTaskIpcLogsSnapshot(context, options) {
4927
+ const holder = context;
4928
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
4929
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
4930
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
4931
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
4932
+ const fd = new FileDatabase({
4933
+ basePath,
4934
+ namespace,
4935
+ tableName,
4936
+ versioned: true,
4937
+ useMetadata: true,
4938
+ maxVersions: 30,
4939
+ pageSize: 2e3,
4940
+ logger: holder.logger
4941
+ });
4942
+ const versions = await fd.getVersions();
4943
+ if (versions.length === 0) {
4944
+ return { records: [], latestTs: null };
4945
+ }
4946
+ const latest = versions[versions.length - 1];
4947
+ const raw = await fd.read({ version: latest });
4948
+ const arr = Array.isArray(raw) ? raw : [];
4949
+ let filtered = arr;
4950
+ if (options.afterTs && String(options.afterTs).trim()) {
4951
+ const cut = String(options.afterTs).trim();
4952
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
4953
+ }
4954
+ let latestTs = null;
4955
+ for (const r of filtered) {
4956
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
4957
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
4958
+ }
4959
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
4960
+ const maxReturn = incremental ? 1e4 : tail;
4961
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
4962
+ return { records: sliced, latestTs };
4963
+ }
4964
+ function resolveIpcFileLogsDir(context, target) {
4965
+ const holder = context;
4966
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4967
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4968
+ const segments = target.tableName.split("/").filter(Boolean);
4969
+ return import_node_path.default.resolve(basePath, namespace, ...segments);
4970
+ }
4971
+ function getLogsStateForTarget(context, target) {
4972
+ const holder = context;
4973
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4974
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4975
+ if (!enabled) return null;
4976
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
4977
+ const map = holder.__tasksLogsTargetStates;
4978
+ const key = ipcLogTargetKey(target);
4979
+ if (map.has(key)) return map.get(key);
4980
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
4981
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
4982
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4983
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4984
+ const db = new FileDatabase({
4985
+ basePath,
4986
+ namespace,
4987
+ tableName: target.tableName,
4988
+ versioned: true,
4989
+ useMetadata: true,
4990
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4991
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4992
+ logger: holder.logger
4993
+ });
4994
+ const state = {
4995
+ db,
4996
+ errorDb: null,
4997
+ queue: Promise.resolve(),
4998
+ initialized: false,
4999
+ errorInitialized: false
5000
+ };
5001
+ map.set(key, state);
5002
+ return state;
5003
+ }
4564
5004
  function isErrorPayload(payload) {
4565
5005
  if (!payload) return false;
4566
5006
  if (typeof payload === "object") {
@@ -4580,14 +5020,26 @@ function buildLogRecord(task, payload) {
4580
5020
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4581
5021
  opid: task.opid ?? null,
4582
5022
  taskId: task.id,
4583
- taskName: task.task,
4584
- target: task.target,
5023
+ taskName: task.name,
5024
+ target: task.service_group,
4585
5025
  source: typeof params.source === "string" ? params.source : null,
4586
5026
  resource: typeof params.resource === "string" ? params.resource : null,
4587
5027
  payload
4588
5028
  };
4589
5029
  }
4590
- function appendTaskIpcLog(context, task, payload) {
5030
+ function appendTaskIpcLog(context, task, payload, target) {
5031
+ if (target) {
5032
+ const state2 = getLogsStateForTarget(context, target);
5033
+ if (!state2?.db) return;
5034
+ const record2 = buildLogRecord(task, payload);
5035
+ state2.queue = state2.queue.then(async () => {
5036
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
5037
+ state2.initialized = true;
5038
+ }).catch((error) => {
5039
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
5040
+ });
5041
+ return;
5042
+ }
4591
5043
  const state = getLogsState(context);
4592
5044
  if (!state.db && !state.errorDb) return;
4593
5045
  const record = buildLogRecord(task, payload);
@@ -4604,84 +5056,293 @@ function appendTaskIpcLog(context, task, payload) {
4604
5056
  context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
4605
5057
  });
4606
5058
  }
4607
-
4608
- // src/tasks/time-matcher.ts
4609
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4610
- function resolveAsterisks(field, range) {
4611
- return field.includes("*") ? field.replace("*", range) : field;
4612
- }
4613
- function resolveRanges(field) {
4614
- const regex = /(\d+)-(\d+)/;
4615
- let current = field;
4616
- while (true) {
4617
- const match = regex.exec(current);
4618
- if (!match) break;
4619
- const raw = match[0];
4620
- let first = Number(match[1]);
4621
- let last = Number(match[2]);
4622
- if (last < first) {
4623
- [first, last] = [last, first];
4624
- }
4625
- const values = [];
4626
- for (let i = first; i <= last; i += 1) {
4627
- values.push(i);
5059
+ async function flushTaskIpcLogs(context) {
5060
+ const holder = context;
5061
+ const promises = [];
5062
+ if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
5063
+ const map = holder.__tasksLogsTargetStates;
5064
+ if (map) {
5065
+ for (const s of map.values()) {
5066
+ if (s.queue) promises.push(s.queue);
4628
5067
  }
4629
- current = current.replace(raw, values.join(","));
4630
5068
  }
4631
- return current;
4632
- }
4633
- function resolveSteps(field) {
4634
- const match = /^(.+)\/(\d+)$/.exec(field);
4635
- if (!match) return field;
4636
- const base = match[1];
4637
- const step = Number(match[2]);
4638
- if (!Number.isFinite(step) || step <= 0) return field;
4639
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4640
- }
4641
- function convertPattern(pattern) {
4642
- const parts = pattern.trim().split(/\s+/);
4643
- if (parts.length !== 6) {
4644
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4645
- }
4646
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4647
- }
4648
- function fieldMatches(field, value) {
4649
- const allowed = field.split(",").map((v) => Number(v));
4650
- return allowed.includes(value);
4651
- }
4652
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4653
- const parsed = convertPattern(pattern);
4654
- 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());
5069
+ await Promise.all(promises);
4655
5070
  }
4656
5071
 
4657
- // src/tasks/TaskMaster.ts
4658
- var TaskMaster = class {
4659
- context;
4660
- task;
5072
+ // src/tasks/AbstractTask.js
5073
+ var AbstractTask = class _AbstractTask {
5074
+ /**
5075
+ * Whether `send-task` should wait for completion (and print a result
5076
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
5077
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
5078
+ *
5079
+ * @type {boolean}
5080
+ */
5081
+ static defaultWaitForResult = false;
5082
+ /**
5083
+ * @param {object} context Runner context (db, logger, params, emitter...).
5084
+ * @param {object} task Task row as claimed from the queue.
5085
+ */
4661
5086
  constructor(context, task) {
4662
5087
  this.context = context;
4663
5088
  this.task = task;
4664
5089
  }
5090
+ /**
5091
+ * Return a short reason string when the task should be deferred (e.g. "locked
5092
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
5093
+ *
5094
+ * @returns {string | false | Promise<string | false>}
5095
+ */
4665
5096
  cantRunReason() {
4666
5097
  return false;
4667
5098
  }
5099
+ /**
5100
+ * Called by the runner when a stop has been requested. Subclasses running
5101
+ * long loops should flip a flag here and check it between iterations.
5102
+ *
5103
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
5104
+ */
4668
5105
  requestStop(_allowanceMs) {
4669
5106
  }
5107
+ /**
5108
+ * Perform the task. Must be implemented by subclasses.
5109
+ *
5110
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
5111
+ * Updates the DB `progress` column. Accepts any serializable value;
5112
+ * strings are stored verbatim, objects are JSON-stringified.
5113
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5114
+ */
5115
+ async run(_reportProgress) {
5116
+ throw new Error("AbstractTask.run must be implemented by subclass");
5117
+ }
5118
+ /**
5119
+ * Resolve a complete row payload for this task — envelope fields (queue,
5120
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
5121
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
5122
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
5123
+ *
5124
+ * const payload = await TaskClass.resolveParams(context, { name });
5125
+ * await enqueueTask(context, payload);
5126
+ *
5127
+ * Validation failures throw {@link ParamError} so the script aborts before
5128
+ * a malformed row hits the DB.
5129
+ *
5130
+ * @param {object} context
5131
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
5132
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
5133
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
5134
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
5135
+ * @returns {Promise<object>}
5136
+ */
5137
+ static async resolveParams(context, overrides = {}) {
5138
+ const main = _AbstractTask._resolveMainFields(context, overrides);
5139
+ const params = await this.resolveCustomParams(context, overrides);
5140
+ return { ...main, params };
5141
+ }
5142
+ /**
5143
+ * Resolve the inner JSON blob stored in the `params` column. Default
5144
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
5145
+ * overlaid with `overrides.params` when supplied; returns `null` when
5146
+ * neither is provided.
5147
+ *
5148
+ * Subclasses with typed fields should override and call
5149
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
5150
+ * resolution, then validate and throw {@link ParamError} on bad input.
5151
+ *
5152
+ * @param {object} context
5153
+ * @param {Record<string, unknown>} [overrides]
5154
+ * @returns {Promise<object|null>}
5155
+ */
5156
+ static async resolveCustomParams(context, overrides = {}) {
5157
+ return _AbstractTask._defaultParamsBlob(context, overrides);
5158
+ }
5159
+ /**
5160
+ * Read main task envelope fields from `context.params` (CLI/env), with
5161
+ * any matching key on `overrides` taking precedence. Internal; called by
5162
+ * {@link AbstractTask.resolveParams}.
5163
+ *
5164
+ * @param {object} context
5165
+ * @param {Record<string, unknown>} [overrides]
5166
+ * @returns {object}
5167
+ */
5168
+ static _resolveMainFields(context, overrides = {}) {
5169
+ const defs = {
5170
+ queueName: "string default tasks",
5171
+ priority: "number default 50",
5172
+ serviceGroup: "string",
5173
+ serviceName: "string",
5174
+ instanceNumber: "number",
5175
+ serverName: "string",
5176
+ opid: "string",
5177
+ schedule: "string"
5178
+ };
5179
+ const cli = context.params.getAllForModule("task-envelope", defs);
5180
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
5181
+ if (!name) {
5182
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5183
+ }
5184
+ let instanceNumber;
5185
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
5186
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
5187
+ const n = Number(rawInstance);
5188
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
5189
+ throw new ParamError("--instanceNumber must be a positive integer when set");
5190
+ }
5191
+ instanceNumber = n;
5192
+ } else {
5193
+ instanceNumber = null;
5194
+ }
5195
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
5196
+ const priority = Number(priorityRaw);
5197
+ if (!Number.isFinite(priority)) {
5198
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
5199
+ }
5200
+ return {
5201
+ name,
5202
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
5203
+ priority,
5204
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
5205
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
5206
+ instanceNumber,
5207
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
5208
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
5209
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
5210
+ nextRunAt: overrides.nextRunAt ?? null
5211
+ };
5212
+ }
5213
+ /**
5214
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
5215
+ * object), then overlays `overrides.params` on top. Returns `null` when
5216
+ * neither is provided.
5217
+ *
5218
+ * @param {object} context
5219
+ * @param {Record<string, unknown>} [overrides]
5220
+ * @returns {object|null}
5221
+ */
5222
+ static _defaultParamsBlob(context, overrides = {}) {
5223
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
5224
+ const fromJson = parseParamsJson(cli.paramsJson);
5225
+ const fromOverride = pickParamsObject(overrides);
5226
+ if (!fromJson && !fromOverride) return null;
5227
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
5228
+ }
5229
+ /**
5230
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
5231
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
5232
+ * merges them with explicit `overrides.params` in increasing priority:
5233
+ *
5234
+ * typed CLI flags → --paramsJson → overrides.params
5235
+ *
5236
+ * Undefined values are dropped so defaults declared in `defs` aren't
5237
+ * overwritten by missing-flag noise. Returns the merged object; the
5238
+ * caller is responsible for validation and throwing `ParamError`.
5239
+ *
5240
+ * @param {object} context
5241
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
5242
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
5243
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
5244
+ * @returns {Record<string, unknown>}
5245
+ */
5246
+ static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
5247
+ const fullDefs = { ...defs, paramsJson: "string" };
5248
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
5249
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
5250
+ const fromCli = {};
5251
+ for (const [k, v] of Object.entries(cliRaw)) {
5252
+ if (k === "paramsJson") continue;
5253
+ if (v !== void 0 && v !== null) fromCli[k] = v;
5254
+ }
5255
+ const fromOverride = pickParamsObject(overrides) ?? {};
5256
+ return { ...fromCli, ...fromJson, ...fromOverride };
5257
+ }
4670
5258
  };
5259
+ function emptyToUndef(s) {
5260
+ if (s === void 0 || s === null) return void 0;
5261
+ if (typeof s !== "string") return s;
5262
+ const t = s.trim();
5263
+ return t.length ? t : void 0;
5264
+ }
5265
+ function parseParamsJson(raw) {
5266
+ if (raw == null) return null;
5267
+ const t = String(raw).trim();
5268
+ if (!t) return null;
5269
+ let parsed;
5270
+ try {
5271
+ parsed = JSON.parse(t);
5272
+ } catch (e) {
5273
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
5274
+ }
5275
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5276
+ throw new ParamError("--paramsJson must be a JSON object");
5277
+ }
5278
+ return parsed;
5279
+ }
5280
+ function pickParamsObject(overrides) {
5281
+ const p = overrides?.params;
5282
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
5283
+ return void 0;
5284
+ }
4671
5285
 
4672
- // src/tasks/coreTasks/TaskPing.ts
4673
- var TaskPing = class extends TaskMaster {
5286
+ // src/tasks/coreTasks/TaskPing.js
5287
+ var TaskPing = class extends AbstractTask {
5288
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
5289
+ static defaultWaitForResult = true;
5290
+ /** Ping takes no params. */
5291
+ static async resolveCustomParams() {
5292
+ return null;
5293
+ }
5294
+ /**
5295
+ * @returns {Promise<{ success: true, results: "pong" }>}
5296
+ */
4674
5297
  async run() {
4675
5298
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4676
5299
  return { success: true, results: "pong" };
4677
5300
  }
4678
5301
  };
4679
5302
 
4680
- // src/tasks/coreTasks/TaskSampleProcess.ts
4681
- var TaskSampleProcess = class extends TaskMaster {
4682
- stopRequested = false;
4683
- stopAllowanceMs = 0;
4684
- stopDecisionLogged = false;
5303
+ // src/tasks/coreTasks/TaskSampleProcess.js
5304
+ var TaskSampleProcess = class extends AbstractTask {
5305
+ /**
5306
+ * @param {object} context
5307
+ * @param {Record<string, unknown>} [overrides]
5308
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
5309
+ */
5310
+ static async resolveCustomParams(context, overrides = {}) {
5311
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
5312
+ total: "number default 10",
5313
+ delay: "number default 1000",
5314
+ name: "string"
5315
+ }, overrides);
5316
+ const total = Number(merged.total);
5317
+ const delay = Number(merged.delay);
5318
+ if (!Number.isInteger(total) || total <= 0) {
5319
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
5320
+ }
5321
+ if (!Number.isInteger(delay) || delay < 0) {
5322
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
5323
+ }
5324
+ const out = { total, delay };
5325
+ if (typeof merged.name === "string" && merged.name.trim()) {
5326
+ out.name = merged.name.trim();
5327
+ }
5328
+ return out;
5329
+ }
5330
+ /**
5331
+ * @param {object} context
5332
+ * @param {object} task
5333
+ */
5334
+ constructor(context, task) {
5335
+ super(context, task);
5336
+ this.stopRequested = false;
5337
+ this.stopAllowanceMs = 0;
5338
+ this.stopDecisionLogged = false;
5339
+ }
5340
+ /**
5341
+ * Runner-facing stop signal. Records the allowance window so the main loop
5342
+ * can decide per-iteration whether to finish or abort early.
5343
+ *
5344
+ * @param {number} allowanceMs
5345
+ */
4685
5346
  requestStop(allowanceMs) {
4686
5347
  this.stopRequested = true;
4687
5348
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -4689,6 +5350,14 @@ var TaskSampleProcess = class extends TaskMaster {
4689
5350
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4690
5351
  );
4691
5352
  }
5353
+ /**
5354
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
5355
+ * progress every iteration. Validates params up front; invalid values short-
5356
+ * circuit to a structured failure without starting the loop.
5357
+ *
5358
+ * @param {(progress: object) => Promise<void>} reportProgress
5359
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5360
+ */
4692
5361
  async run(reportProgress) {
4693
5362
  const totalRaw = this.task?.params?.total ?? 10;
4694
5363
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -4770,7 +5439,7 @@ var TaskSampleProcess = class extends TaskMaster {
4770
5439
  }
4771
5440
  };
4772
5441
 
4773
- // src/tasks/coreTasks/TaskShellCommand.ts
5442
+ // src/tasks/coreTasks/TaskShellCommand.js
4774
5443
  var import_node_child_process = require("child_process");
4775
5444
  function runShellCommand(command, cwd) {
4776
5445
  return new Promise((resolve2, reject) => {
@@ -4800,7 +5469,27 @@ function runShellCommand(command, cwd) {
4800
5469
  });
4801
5470
  });
4802
5471
  }
4803
- var TaskShellCommand = class extends TaskMaster {
5472
+ var TaskShellCommand = class extends AbstractTask {
5473
+ /**
5474
+ * @param {object} context
5475
+ * @param {Record<string, unknown>} [overrides]
5476
+ * @returns {Promise<{ command: string, cwd?: string }>}
5477
+ */
5478
+ static async resolveCustomParams(context, overrides = {}) {
5479
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
5480
+ command: "string",
5481
+ cwd: "string"
5482
+ }, overrides);
5483
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
5484
+ if (!command) {
5485
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
5486
+ }
5487
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
5488
+ return cwd ? { command, cwd } : { command };
5489
+ }
5490
+ /**
5491
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5492
+ */
4804
5493
  async run() {
4805
5494
  const params = this.task?.params;
4806
5495
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -4849,9 +5538,9 @@ var TaskShellCommand = class extends TaskMaster {
4849
5538
  }
4850
5539
  };
4851
5540
 
4852
- // src/tasks/coreTasks/TaskSystemInfo.ts
5541
+ // src/tasks/coreTasks/TaskSystemInfo.js
4853
5542
  var import_node_os2 = __toESM(require("os"), 1);
4854
- var import_promises2 = __toESM(require("fs/promises"), 1);
5543
+ var import_promises = __toESM(require("fs/promises"), 1);
4855
5544
  function toGb(valueBytes) {
4856
5545
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
4857
5546
  }
@@ -4859,7 +5548,7 @@ function toMb(valueBytes) {
4859
5548
  return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
4860
5549
  }
4861
5550
  async function getDiskStats() {
4862
- const stats = await import_promises2.default.statfs("/");
5551
+ const stats = await import_promises.default.statfs("/");
4863
5552
  const total = Number(stats.bsize) * Number(stats.blocks);
4864
5553
  const free = Number(stats.bsize) * Number(stats.bavail);
4865
5554
  const used = total - free;
@@ -4869,7 +5558,16 @@ async function getDiskStats() {
4869
5558
  free: toGb(free)
4870
5559
  };
4871
5560
  }
4872
- var TaskSystemInfo = class extends TaskMaster {
5561
+ var TaskSystemInfo = class extends AbstractTask {
5562
+ /** Same UX expectation as `ping` — short probe, print the result. */
5563
+ static defaultWaitForResult = true;
5564
+ /** systemInfo takes no params. */
5565
+ static async resolveCustomParams() {
5566
+ return null;
5567
+ }
5568
+ /**
5569
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5570
+ */
4873
5571
  async run() {
4874
5572
  try {
4875
5573
  const totalMemory = import_node_os2.default.totalmem();
@@ -4921,8 +5619,31 @@ var TaskSystemInfo = class extends TaskMaster {
4921
5619
  }
4922
5620
  };
4923
5621
 
4924
- // src/tasks/coreTasks/TaskSumAB.ts
4925
- var TaskSumAB = class extends TaskMaster {
5622
+ // src/tasks/coreTasks/TaskSumAB.js
5623
+ var TaskSumAB = class extends AbstractTask {
5624
+ /** Short, deterministic — wait by default so callers see the sum. */
5625
+ static defaultWaitForResult = true;
5626
+ /**
5627
+ * @param {object} context
5628
+ * @param {Record<string, unknown>} [overrides]
5629
+ * @returns {Promise<{ a: number, b: number }>}
5630
+ */
5631
+ static async resolveCustomParams(context, overrides = {}) {
5632
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
5633
+ a: "number",
5634
+ b: "number"
5635
+ }, overrides);
5636
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
5637
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
5638
+ }
5639
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
5640
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
5641
+ }
5642
+ return { a: merged.a, b: merged.b };
5643
+ }
5644
+ /**
5645
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5646
+ */
4926
5647
  async run() {
4927
5648
  const a = this.task?.params?.a;
4928
5649
  const b = this.task?.params?.b;
@@ -4953,8 +5674,46 @@ var TaskSumAB = class extends TaskMaster {
4953
5674
  }
4954
5675
  };
4955
5676
 
4956
- // src/tasks/coreTasks/TaskStopRunner.ts
4957
- var TaskStopRunner = class extends TaskMaster {
5677
+ // src/tasks/coreTasks/TaskStopRunner.js
5678
+ var TaskStopRunner = class extends AbstractTask {
5679
+ /**
5680
+ * Stop tasks must target a concrete instance — without `serviceName` the
5681
+ * row would race against any worker on the queue. Layered on top of the
5682
+ * envelope built by {@link AbstractTask.resolveParams}.
5683
+ *
5684
+ * @param {object} context
5685
+ * @param {Record<string, unknown>} [overrides]
5686
+ * @returns {Promise<object>}
5687
+ */
5688
+ static async resolveParams(context, overrides = {}) {
5689
+ const main = await super.resolveParams(context, overrides);
5690
+ if (!main.serviceName) {
5691
+ throw new ParamError(
5692
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
5693
+ );
5694
+ }
5695
+ return main;
5696
+ }
5697
+ /**
5698
+ * @param {object} context
5699
+ * @param {Record<string, unknown>} [overrides]
5700
+ * @returns {Promise<{ allowanceMs: number }>}
5701
+ */
5702
+ static async resolveCustomParams(context, overrides = {}) {
5703
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
5704
+ allowanceMs: "number default 5000"
5705
+ }, overrides);
5706
+ const allowanceMs = Number(merged.allowanceMs);
5707
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
5708
+ throw new ParamError(
5709
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
5710
+ );
5711
+ }
5712
+ return { allowanceMs };
5713
+ }
5714
+ /**
5715
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
5716
+ */
4958
5717
  async run() {
4959
5718
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4960
5719
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -4969,172 +5728,396 @@ var TaskStopRunner = class extends TaskMaster {
4969
5728
  }
4970
5729
  };
4971
5730
 
4972
- // src/tasks/TasksRegistry.ts
5731
+ // src/tasks/coreTasks/TaskGetLogs.js
5732
+ var TaskGetLogs = class extends AbstractTask {
5733
+ /**
5734
+ * @param {object} context
5735
+ * @param {Record<string, unknown>} [overrides]
5736
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
5737
+ */
5738
+ static async resolveCustomParams(context, overrides = {}) {
5739
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
5740
+ source: "string",
5741
+ resource: "string",
5742
+ tail: "number default 100",
5743
+ afterTs: "string"
5744
+ }, overrides);
5745
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
5746
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
5747
+ if (!source) throw new ParamError('getLogs: param "source" is required');
5748
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
5749
+ let tail = Number(merged.tail);
5750
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
5751
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
5752
+ const out = { source, resource, tail };
5753
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
5754
+ out.afterTs = merged.afterTs.trim();
5755
+ }
5756
+ return out;
5757
+ }
5758
+ /**
5759
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
5760
+ * @returns {Promise<{ success: boolean, results: unknown }>}
5761
+ */
5762
+ async run(_reportProgress) {
5763
+ const p = this.task.params ?? {};
5764
+ const source = String(p.source ?? "").trim();
5765
+ const resource = String(p.resource ?? "").trim();
5766
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
5767
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
5768
+ if (!source || !resource) {
5769
+ return {
5770
+ success: false,
5771
+ results: { error: 'getLogs requires params "source" and "resource"' }
5772
+ };
5773
+ }
5774
+ try {
5775
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
5776
+ source,
5777
+ resource,
5778
+ tail,
5779
+ afterTs
5780
+ });
5781
+ return {
5782
+ success: true,
5783
+ results: { records, latestTs, source, resource }
5784
+ };
5785
+ } catch (e) {
5786
+ return {
5787
+ success: false,
5788
+ results: { error: e?.message ?? String(e) }
5789
+ };
5790
+ }
5791
+ }
5792
+ };
5793
+
5794
+ // src/tasks/TasksRegistry.js
4973
5795
  var TasksRegistry = class _TasksRegistry {
4974
- map = {};
5796
+ /**
5797
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
5798
+ */
4975
5799
  constructor(initial) {
5800
+ this.map = {};
4976
5801
  if (initial) {
4977
5802
  this.addMany(initial);
4978
5803
  }
4979
5804
  }
5805
+ /**
5806
+ * Build a registry pre-populated with every core task plus legacy aliases.
5807
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
5808
+ *
5809
+ * @returns {TasksRegistry}
5810
+ */
4980
5811
  static withCoreTasks() {
4981
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
5812
+ 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);
4982
5813
  }
5814
+ /**
5815
+ * Register a single task class under a name. Overwrites any previous entry.
5816
+ *
5817
+ * @param {string} taskName
5818
+ * @param {Function} taskClass Subclass of `AbstractTask`.
5819
+ * @returns {this}
5820
+ */
4983
5821
  add(taskName, taskClass) {
4984
5822
  this.map[taskName] = taskClass;
4985
5823
  return this;
4986
5824
  }
5825
+ /**
5826
+ * Bulk-register a name → class map. Later calls override earlier ones.
5827
+ *
5828
+ * @param {Record<string, Function>} entries
5829
+ * @returns {this}
5830
+ */
4987
5831
  addMany(entries) {
4988
5832
  for (const [name, klass] of Object.entries(entries)) {
4989
5833
  this.add(name, klass);
4990
5834
  }
4991
5835
  return this;
4992
5836
  }
5837
+ /**
5838
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
5839
+ * the runner treats that as "some other worker may handle this" and skips.
5840
+ *
5841
+ * @param {string} taskName
5842
+ * @returns {Function | undefined}
5843
+ */
4993
5844
  get(taskName) {
4994
5845
  return this.map[taskName];
4995
5846
  }
5847
+ /**
5848
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
5849
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
5850
+ * paths where an unknown name is a hard CLI/programmer error.
5851
+ *
5852
+ * @param {string} taskName
5853
+ * @returns {Function}
5854
+ */
5855
+ requireClass(taskName) {
5856
+ const TaskClass = taskName ? this.map[taskName] : void 0;
5857
+ if (!TaskClass) {
5858
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
5859
+ throw new ParamError(
5860
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
5861
+ );
5862
+ }
5863
+ return TaskClass;
5864
+ }
5865
+ /**
5866
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
5867
+ * from `overrides` or `context.params`, look up the class, and delegate
5868
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
5869
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
5870
+ *
5871
+ * Validation failures (unknown task, missing required custom params, etc.)
5872
+ * surface as {@link ParamError} so the caller aborts cleanly before any
5873
+ * row is inserted.
5874
+ *
5875
+ * @param {object} context
5876
+ * @param {Record<string, unknown>} [overrides]
5877
+ * @returns {Promise<object>}
5878
+ */
5879
+ async resolveTaskParams(context, overrides = {}) {
5880
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
5881
+ const fromCli = context.params.get("name", "string");
5882
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
5883
+ const name = overrideName || cliName;
5884
+ if (!name) {
5885
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
5886
+ }
5887
+ const TaskClass = this.requireClass(name);
5888
+ return TaskClass.resolveParams(context, { ...overrides, name });
5889
+ }
5890
+ /**
5891
+ * Names of every registered task, sorted alphabetically (useful for CLI output
5892
+ * and allowlist sanity checks).
5893
+ *
5894
+ * @returns {string[]}
5895
+ */
4996
5896
  listSupportedTasks() {
4997
5897
  return Object.keys(this.map).sort();
4998
5898
  }
5899
+ /**
5900
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
5901
+ * or for serialization.
5902
+ *
5903
+ * @returns {Record<string, Function>}
5904
+ */
4999
5905
  toObject() {
5000
5906
  return { ...this.map };
5001
5907
  }
5002
5908
  };
5003
5909
 
5004
- // src/tasks/taskScriptRunner.ts
5910
+ // src/tasks/serviceTaskAllowlist.js
5911
+ var SERVICE_TASK_NAMES = [
5912
+ "ping",
5913
+ "stop",
5914
+ "stopRunner",
5915
+ "shellCommand",
5916
+ "systemInfo",
5917
+ "info",
5918
+ "getLogs"
5919
+ ];
5920
+ function normalizeAllowedTasks(value) {
5921
+ if (!value) return void 0;
5922
+ if (Array.isArray(value)) {
5923
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5924
+ return out2.length ? out2 : void 0;
5925
+ }
5926
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5927
+ return out.length ? out : void 0;
5928
+ }
5929
+ function mergeAllowedTasksWithServiceTasks(names) {
5930
+ const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
5931
+ return Array.from(set).sort();
5932
+ }
5933
+
5934
+ // src/tasks/taskScriptRunner.js
5005
5935
  var import_node_child_process2 = require("child_process");
5936
+ var MAX_PROGRESS_TEXT_LEN = 4e3;
5006
5937
  function toCliArgs(args = []) {
5007
5938
  return args.filter((a) => typeof a === "string" && a.length > 0);
5008
5939
  }
5009
5940
  function formatChildLogPrefix(task) {
5010
- return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
5941
+ return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
5011
5942
  }
5012
- async function runNodeTaskScript(context, options) {
5013
- const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
5943
+ function isProgressPayload(payload) {
5944
+ if (!payload || typeof payload !== "object") return false;
5945
+ if (payload.level !== "progress") return false;
5946
+ const count = Number(payload.count);
5947
+ const total = Number(payload.total);
5948
+ return Number.isFinite(count) && Number.isFinite(total) && total > 0;
5949
+ }
5950
+ function formatProgressText(payload, fallbackPrefix) {
5951
+ const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
5952
+ const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
5953
+ return `${pfx}${label}${payload.count}/${payload.total}`;
5954
+ }
5955
+ function forwardChildLogToParent(context, prefix, message) {
5956
+ if (!message || typeof message !== "object") return;
5957
+ const text = typeof message.message === "string" ? message.message : null;
5958
+ if (!text) return;
5959
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
5960
+ const line = `[child:${prefix}] ${text}`;
5961
+ const logger = context.logger;
5962
+ switch (level) {
5963
+ case "error":
5964
+ case "fatal":
5965
+ logger.error?.(line);
5966
+ return;
5967
+ case "warn":
5968
+ case "warning":
5969
+ logger.warn?.(line);
5970
+ return;
5971
+ case "debug":
5972
+ logger.debug?.(line);
5973
+ return;
5974
+ case "info":
5975
+ default:
5976
+ logger.info?.(line);
5977
+ }
5978
+ }
5979
+ function buildNodeArgs(scriptPath, cliArgs) {
5014
5980
  const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
5015
5981
  const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
5016
- const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
5017
- const child = (0, import_node_child_process2.spawn)(
5018
- process.execPath,
5019
- nodeArgs,
5020
- {
5021
- cwd: options.cwd || process.cwd(),
5022
- stdio: ["ignore", "pipe", "pipe", "ipc"],
5023
- env: {
5024
- ...process.env,
5025
- TASK_ID: options.task.id,
5026
- TASK_NAME: options.task.task,
5027
- TASK_OPID: options.task.opid || ""
5028
- }
5029
- }
5982
+ return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
5983
+ }
5984
+ function resolveTasksTableName(context) {
5985
+ return context.tasksQueueName || context.params?.get?.("table") || "tasks";
5986
+ }
5987
+ function announceIpcFileLogsTarget(context, options) {
5988
+ if (!options.ipcFileLogs) return;
5989
+ const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
5990
+ const enabledRaw = context.params?.get?.("tasksLogsEnabled");
5991
+ const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
5992
+ context.logger.info?.(
5993
+ `[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
5030
5994
  );
5031
- let stdout = "";
5032
- let stderr = "";
5033
- let workerResult = null;
5034
- let hadErrorMessage = false;
5035
- const prefix = formatChildLogPrefix(options.task);
5036
- const db = context.db;
5037
- const tasksTable = context.params?.get?.("table") || "tasks";
5038
- let progressWriteChain = Promise.resolve();
5039
- let progressCallbackChain = Promise.resolve();
5040
- const updateProgress = (text) => {
5041
- if (!db || !text || !text.trim()) return;
5042
- progressWriteChain = progressWriteChain.then(async () => {
5043
- await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
5044
- }).catch((error) => {
5045
- context.logger.warn?.(
5046
- `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
5047
- );
5048
- });
5049
- if (options.onProgress) {
5050
- progressCallbackChain = progressCallbackChain.then(async () => {
5051
- await options.onProgress?.(text.slice(0, 4e3));
5052
- }).catch((error) => {
5053
- context.logger.warn?.(
5054
- `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
5055
- );
5995
+ }
5996
+ function createSerializedQueue() {
5997
+ let chain = Promise.resolve();
5998
+ return {
5999
+ push(fn) {
6000
+ chain = chain.then(fn, () => {
6001
+ }).catch(() => {
6002
+ });
6003
+ return chain;
6004
+ },
6005
+ drain() {
6006
+ return chain.catch(() => {
5056
6007
  });
5057
6008
  }
5058
6009
  };
5059
- const payloadToProgressText = (payload) => {
5060
- if (!payload) return "";
5061
- if (typeof payload === "string") return payload;
5062
- if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
5063
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
5064
- return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
5065
- }
5066
- if (typeof payload.message === "string") return payload.message;
5067
- if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
5068
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
5069
- return `${pfx}${payload.count}/${payload.total}`;
5070
- }
5071
- return "";
6010
+ }
6011
+ async function runNodeTaskScript(context, options) {
6012
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
6013
+ const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
6014
+ const child = (0, import_node_child_process2.spawn)(process.execPath, nodeArgs, {
6015
+ cwd: options.cwd || process.cwd(),
6016
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
6017
+ env: {
6018
+ ...process.env,
6019
+ TASK_ID: options.task.id,
6020
+ TASK_NAME: options.task.name,
6021
+ TASK_OPID: options.task.opid || ""
6022
+ }
6023
+ });
6024
+ announceIpcFileLogsTarget(context, options);
6025
+ const prefix = formatChildLogPrefix(options.task);
6026
+ const tasksTable = resolveTasksTableName(context);
6027
+ const progressQueue = createSerializedQueue();
6028
+ const state = {
6029
+ stdout: "",
6030
+ stderr: "",
6031
+ workerResult: null,
6032
+ hadErrorMessage: false
6033
+ };
6034
+ const writeProgress = (text) => {
6035
+ const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
6036
+ if (!trimmed) return;
6037
+ progressQueue.push(async () => {
6038
+ const db = context.db;
6039
+ if (db) {
6040
+ try {
6041
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
6042
+ } catch (error) {
6043
+ context.logger.warn?.(
6044
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
6045
+ );
6046
+ }
6047
+ }
6048
+ if (options.onProgress) {
6049
+ try {
6050
+ await options.onProgress(trimmed);
6051
+ } catch (error) {
6052
+ context.logger.warn?.(
6053
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
6054
+ );
6055
+ }
6056
+ }
6057
+ });
5072
6058
  };
5073
- child.stdout.on("data", (chunk) => {
6059
+ child.stdout?.on("data", (chunk) => {
5074
6060
  const text = String(chunk);
5075
- stdout += text;
6061
+ state.stdout += text;
5076
6062
  if (text.trim()) {
5077
6063
  context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
5078
- updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
5079
6064
  }
5080
6065
  });
5081
- child.stderr.on("data", (chunk) => {
6066
+ child.stderr?.on("data", (chunk) => {
5082
6067
  const text = String(chunk);
5083
- stderr += text;
6068
+ state.stderr += text;
5084
6069
  if (text.trim()) {
5085
6070
  context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
5086
6071
  }
5087
6072
  });
5088
6073
  child.on("message", (message) => {
5089
6074
  if (message && typeof message === "object" && "__taskWorkerResult" in message) {
5090
- workerResult = message.__taskWorkerResult;
6075
+ state.workerResult = message.__taskWorkerResult;
5091
6076
  return;
5092
6077
  }
5093
6078
  if (message && typeof message === "object") {
5094
6079
  const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
5095
6080
  if (level === "error" || level === "fatal") {
5096
- hadErrorMessage = true;
6081
+ state.hadErrorMessage = true;
5097
6082
  }
5098
6083
  }
5099
- appendTaskIpcLog(context, options.task, message);
5100
- const progressText = payloadToProgressText(message);
5101
- if (progressText) {
5102
- updateProgress(progressText);
5103
- if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
5104
- const countNum = Number(String(message.count).trim());
5105
- const totalNum = Number(message.total);
5106
- if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
5107
- context.logger.progress(message.message || "progress", {
5108
- prefix: message.prefix || prefix,
5109
- count: countNum,
5110
- total: totalNum
5111
- });
5112
- } else {
5113
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
5114
- }
5115
- } else {
5116
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
5117
- }
6084
+ appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
6085
+ try {
6086
+ options.onChildIpcMessage?.(message);
6087
+ } catch (e) {
6088
+ context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
5118
6089
  }
6090
+ if (isProgressPayload(message)) {
6091
+ context.logger.progress(message.message || "progress", {
6092
+ prefix: message.prefix || prefix,
6093
+ count: Number(message.count),
6094
+ total: Number(message.total)
6095
+ });
6096
+ writeProgress(formatProgressText(message, prefix));
6097
+ return;
6098
+ }
6099
+ forwardChildLogToParent(context, prefix, message);
5119
6100
  });
5120
6101
  return await new Promise((resolve2, reject) => {
5121
6102
  child.on("error", (error) => reject(error));
5122
6103
  child.on("close", (exitCode, signal) => {
5123
- Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
6104
+ void (async () => {
6105
+ await flushTaskIpcLogs(context);
6106
+ await progressQueue.drain();
5124
6107
  resolve2({
5125
6108
  exitCode,
5126
6109
  signal,
5127
- stdout: stdout.trim(),
5128
- stderr: stderr.trim(),
5129
- workerResult,
5130
- hadErrorMessage
6110
+ stdout: state.stdout.trim(),
6111
+ stderr: state.stderr.trim(),
6112
+ workerResult: state.workerResult,
6113
+ hadErrorMessage: state.hadErrorMessage
5131
6114
  });
5132
- });
6115
+ })();
5133
6116
  });
5134
6117
  });
5135
6118
  }
5136
6119
 
5137
- // src/tasks/index.ts
6120
+ // src/tasks/index.js
5138
6121
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
5139
6122
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
5140
6123
  function getDb3(context) {
@@ -5149,22 +6132,13 @@ function normalizeRegistry(registry) {
5149
6132
  if (registry instanceof TasksRegistry) return registry;
5150
6133
  return new TasksRegistry().addMany(registry);
5151
6134
  }
5152
- function normalizeAllowedTasks(value) {
5153
- if (!value) return void 0;
5154
- if (Array.isArray(value)) {
5155
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
5156
- return out2.length ? out2 : void 0;
5157
- }
5158
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
5159
- return out.length ? out : void 0;
5160
- }
5161
- async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
6135
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
5162
6136
  return enqueueTask(context, {
5163
- queue,
5164
- target,
5165
- task: "stopRunner",
6137
+ queueName,
6138
+ name: "stopRunner",
5166
6139
  params: { allowanceMs },
5167
- priority: 1e6
6140
+ priority: 0,
6141
+ serviceGroup
5168
6142
  });
5169
6143
  }
5170
6144
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
@@ -5182,18 +6156,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
5182
6156
  }
5183
6157
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
5184
6158
  const db = getDb3(context);
5185
- const taskName = row.task;
6159
+ const taskName = row.name;
5186
6160
  const TaskClass = registry.get(taskName);
5187
- const { paused_at: _pausedAt, ...rowForHistory } = row;
5188
6161
  if (!TaskClass) {
5189
6162
  const err = { message: `Unknown task "${taskName}"` };
5190
- await db(historyTable).insert({
5191
- ...rowForHistory,
5192
- completed_at: /* @__PURE__ */ new Date(),
5193
- success: false,
5194
- params: toJsonColumn(row.params),
5195
- results: toJsonColumn(err)
5196
- });
6163
+ await db(historyTable).insert(
6164
+ taskHistoryInsertFromQueueRow(row, {
6165
+ completed_at: /* @__PURE__ */ new Date(),
6166
+ success: false,
6167
+ status: "failed",
6168
+ status_changed_at: db.fn.now(),
6169
+ params: toJsonColumn(row.params),
6170
+ results: toJsonColumn(err)
6171
+ })
6172
+ );
5197
6173
  if (row.schedule) {
5198
6174
  await db(tasksTable).where({ id: row.id }).update({
5199
6175
  started_at: null,
@@ -5201,7 +6177,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5201
6177
  success: false,
5202
6178
  results: toJsonColumn(err),
5203
6179
  past_due: null,
5204
- paused_at: db.fn.now(),
6180
+ status: "paused",
6181
+ status_changed_at: db.fn.now(),
5205
6182
  progress: LOCKED_BY_ERROR_MESSAGE
5206
6183
  });
5207
6184
  } else {
@@ -5228,13 +6205,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5228
6205
  } finally {
5229
6206
  runningTaskInstances.delete(row.id);
5230
6207
  }
5231
- await db(historyTable).insert({
5232
- ...rowForHistory,
5233
- completed_at: /* @__PURE__ */ new Date(),
5234
- success,
5235
- params: toJsonColumn(row.params),
5236
- results: toJsonColumn(results)
5237
- });
6208
+ await db(historyTable).insert(
6209
+ taskHistoryInsertFromQueueRow(row, {
6210
+ completed_at: /* @__PURE__ */ new Date(),
6211
+ success,
6212
+ status: success ? "completed" : "failed",
6213
+ status_changed_at: db.fn.now(),
6214
+ params: toJsonColumn(row.params),
6215
+ results: toJsonColumn(results)
6216
+ })
6217
+ );
5238
6218
  if (!success) {
5239
6219
  const dbName = String(context?.params?.get?.("dbName") || "local");
5240
6220
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -5249,19 +6229,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5249
6229
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
5250
6230
  appendTaskIpcLog(context, row, {
5251
6231
  level: "error",
5252
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
6232
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
5253
6233
  details: results
5254
6234
  });
5255
6235
  }
5256
6236
  if (row.schedule) {
5257
6237
  if (success) {
6238
+ let nextRunAt = null;
6239
+ try {
6240
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
6241
+ } catch (e) {
6242
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
6243
+ }
5258
6244
  await db(tasksTable).where({ id: row.id }).update({
5259
6245
  started_at: null,
5260
6246
  completed_at: /* @__PURE__ */ new Date(),
5261
6247
  success,
5262
6248
  results: toJsonColumn(results),
5263
6249
  progress: null,
5264
- past_due: null
6250
+ past_due: null,
6251
+ status: "idle",
6252
+ status_changed_at: db.fn.now(),
6253
+ next_run_at: nextRunAt,
6254
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
6255
+ service_name: null,
6256
+ server_name: null,
6257
+ instance_number: null
5265
6258
  });
5266
6259
  } else {
5267
6260
  await db(tasksTable).where({ id: row.id }).update({
@@ -5269,7 +6262,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5269
6262
  completed_at: /* @__PURE__ */ new Date(),
5270
6263
  success,
5271
6264
  results: toJsonColumn(results),
5272
- paused_at: db.fn.now(),
6265
+ status: "paused",
6266
+ status_changed_at: db.fn.now(),
5273
6267
  progress: LOCKED_BY_ERROR_MESSAGE,
5274
6268
  past_due: null
5275
6269
  });
@@ -5281,58 +6275,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5281
6275
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
5282
6276
  return { stopRunnerRequested, stopAllowanceMs };
5283
6277
  }
5284
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
6278
+ function shuffleTaskRowsInPlace(rows) {
6279
+ for (let i = rows.length - 1; i > 0; i--) {
6280
+ const j = Math.floor(Math.random() * (i + 1));
6281
+ const t = rows[i];
6282
+ rows[i] = rows[j];
6283
+ rows[j] = t;
6284
+ }
6285
+ }
6286
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
5285
6287
  const db = getDb3(context);
5286
- 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);
6288
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
6289
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
6290
+ }).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);
5287
6291
  if (taskNames && taskNames.length > 0) {
5288
- query = query.whereIn("task", taskNames);
6292
+ query = query.whereIn("name", taskNames);
6293
+ }
6294
+ if (runnerIdentity) {
6295
+ query = query.where(function() {
6296
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
6297
+ }).where(function() {
6298
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
6299
+ }).where(function() {
6300
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
6301
+ });
6302
+ } else {
6303
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
5289
6304
  }
5290
6305
  const candidates = await query;
6306
+ shuffleTaskRowsInPlace(candidates);
5291
6307
  for (const row of candidates) {
5292
6308
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
5293
6309
  continue;
5294
6310
  }
5295
- const TaskClass = registry.get(row.task);
5296
- if (TaskClass) {
5297
- const taskInstance = new TaskClass(context, row);
5298
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
5299
- if (reason) {
5300
- if (!row.past_due) {
5301
- await db(tasksTable).where({ id: row.id }).update({
5302
- past_due: db.fn.now(),
5303
- progress: String(reason)
5304
- });
5305
- }
5306
- continue;
6311
+ const TaskClass = registry.get(row.name);
6312
+ if (!TaskClass) {
6313
+ continue;
6314
+ }
6315
+ const taskInstance = new TaskClass(context, row);
6316
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
6317
+ if (reason) {
6318
+ if (!row.past_due) {
6319
+ await db(tasksTable).where({ id: row.id }).update({
6320
+ past_due: db.fn.now(),
6321
+ progress: String(reason)
6322
+ });
5307
6323
  }
6324
+ continue;
6325
+ }
6326
+ const claimPatch = {
6327
+ started_at: db.fn.now(),
6328
+ status: "running",
6329
+ status_changed_at: db.fn.now()
6330
+ };
6331
+ if (runnerIdentity) {
6332
+ claimPatch.service_name = runnerIdentity.service_name;
6333
+ claimPatch.server_name = runnerIdentity.server_name;
6334
+ claimPatch.instance_number = runnerIdentity.instance_number;
5308
6335
  }
5309
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
6336
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
5310
6337
  const claimed = Array.isArray(updated) ? updated[0] : null;
5311
6338
  if (claimed) return claimed;
5312
6339
  }
5313
6340
  return null;
5314
6341
  }
5315
6342
  async function runTasksLoop(context, options) {
5316
- const queue = options.queue ?? "tasks";
6343
+ const queueName = options.queueName ?? "tasks";
5317
6344
  const target = options.target;
5318
6345
  const pollMs = options.pollMs ?? 1e3;
5319
- const maxParallel = options.maxParallel ?? 1;
6346
+ const claimJitterMs = options.claimJitterMs ?? 0;
6347
+ const maxParallel = options.maxParallel ?? 32;
5320
6348
  const scanLimit = options.scanLimit ?? 100;
5321
6349
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5322
6350
  const registry = normalizeRegistry(options.registry);
5323
- const { tasksTable, historyTable } = queueToTableNames(queue);
6351
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5324
6352
  if (!target) throw new Error("runTasksLoop: target is required");
6353
+ context.tasksQueueName = queueName;
5325
6354
  const runningPromises = /* @__PURE__ */ new Set();
5326
6355
  const runningTaskInstances = /* @__PURE__ */ new Map();
5327
6356
  let runningStopControlPromise = null;
5328
6357
  let stopRequested = false;
5329
6358
  let stopAllowanceMs = 5e3;
5330
- context.__tasksRunnerStop = false;
6359
+ context.tasksRunnerStop = false;
5331
6360
  let registryReg = null;
5332
6361
  let registryInterval = null;
6362
+ let runnerIdentity = null;
5333
6363
  const hbGroup = options.runnerServiceGroup?.trim();
5334
6364
  if (hbGroup) {
5335
- const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
5336
6365
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
5337
6366
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
5338
6367
  const defaultMeta = {
@@ -5340,16 +6369,21 @@ async function runTasksLoop(context, options) {
5340
6369
  allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
5341
6370
  };
5342
6371
  registryReg = await registerInServicesRegistry(context, {
5343
- queue,
6372
+ queueName,
5344
6373
  target,
5345
6374
  serviceGroup: hbGroup,
5346
6375
  serviceName: options.runnerServiceName,
5347
- identityDir,
6376
+ instanceNumber: options.runnerInstanceNumber,
5348
6377
  staleMs,
5349
6378
  groupMaxInstances: options.runnerGroupMaxInstances,
5350
6379
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
5351
6380
  metadata: options.runnerMetadata ?? defaultMeta
5352
6381
  });
6382
+ runnerIdentity = {
6383
+ service_name: registryReg.serviceName,
6384
+ server_name: import_node_os3.default.hostname(),
6385
+ instance_number: registryReg.instanceNumber
6386
+ };
5353
6387
  registryInterval = setInterval(() => {
5354
6388
  void touchServicesRegistry(context, registryReg).catch((err) => {
5355
6389
  context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
@@ -5357,7 +6391,7 @@ async function runTasksLoop(context, options) {
5357
6391
  }, hbIntervalMs);
5358
6392
  }
5359
6393
  try {
5360
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
6394
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
5361
6395
  if (!runningStopControlPromise) {
5362
6396
  const claimedStopTask = await claimNextRunnableTask(
5363
6397
  context,
@@ -5365,7 +6399,8 @@ async function runTasksLoop(context, options) {
5365
6399
  target,
5366
6400
  registry,
5367
6401
  10,
5368
- ["stopRunner", "stop"]
6402
+ ["stopRunner", "stop"],
6403
+ runnerIdentity
5369
6404
  );
5370
6405
  if (claimedStopTask) {
5371
6406
  runningStopControlPromise = executeClaimedTask(
@@ -5379,7 +6414,7 @@ async function runTasksLoop(context, options) {
5379
6414
  if (outcome.stopRunnerRequested && !stopRequested) {
5380
6415
  stopRequested = true;
5381
6416
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5382
- context.__tasksRunnerStop = true;
6417
+ context.tasksRunnerStop = true;
5383
6418
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5384
6419
  }
5385
6420
  }).finally(() => {
@@ -5387,6 +6422,9 @@ async function runTasksLoop(context, options) {
5387
6422
  });
5388
6423
  }
5389
6424
  }
6425
+ if (claimJitterMs > 0) {
6426
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
6427
+ }
5390
6428
  while (runningPromises.size < maxParallel) {
5391
6429
  const claimed = await claimNextRunnableTask(
5392
6430
  context,
@@ -5394,14 +6432,15 @@ async function runTasksLoop(context, options) {
5394
6432
  target,
5395
6433
  registry,
5396
6434
  scanLimit,
5397
- allowedTasks
6435
+ allowedTasks,
6436
+ runnerIdentity
5398
6437
  );
5399
6438
  if (!claimed) break;
5400
6439
  const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
5401
6440
  if (outcome.stopRunnerRequested && !stopRequested) {
5402
6441
  stopRequested = true;
5403
6442
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5404
- context.__tasksRunnerStop = true;
6443
+ context.tasksRunnerStop = true;
5405
6444
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5406
6445
  }
5407
6446
  }).finally(() => {
@@ -5409,7 +6448,16 @@ async function runTasksLoop(context, options) {
5409
6448
  });
5410
6449
  runningPromises.add(p);
5411
6450
  }
5412
- await sleepMs(pollMs);
6451
+ const wakePromises = [...runningPromises];
6452
+ if (runningStopControlPromise) {
6453
+ wakePromises.push(runningStopControlPromise);
6454
+ }
6455
+ if (wakePromises.length === 0) {
6456
+ await sleepMs(pollMs);
6457
+ } else {
6458
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
6459
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
6460
+ }
5413
6461
  }
5414
6462
  if (context.isStop() && !stopRequested) {
5415
6463
  await signalRunningTasksStop(context, runningTaskInstances, 5e3);
@@ -5445,89 +6493,130 @@ async function runTasksLoop(context, options) {
5445
6493
  }
5446
6494
  async function waitForTaskResult(context, taskId, options = {}) {
5447
6495
  const db = getDb3(context);
5448
- const queue = options.queue ?? "tasks";
6496
+ const queueName = options.queueName ?? "tasks";
5449
6497
  const timeoutMs = options.timeoutMs ?? 6e4;
5450
6498
  const pollMs = options.pollMs ?? 500;
5451
- const { tasksTable, historyTable } = queueToTableNames(queue);
6499
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
5452
6500
  const deadline = Date.now() + timeoutMs;
6501
+ const waitStartedAt = /* @__PURE__ */ new Date();
6502
+ let cachedNameOpid = null;
6503
+ async function historySinceWait(name, opid) {
6504
+ let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
6505
+ if (opid == null || opid === "") {
6506
+ q = q.whereNull("opid");
6507
+ } else {
6508
+ q = q.where({ opid });
6509
+ }
6510
+ return await q.orderBy("completed_at", "desc").first();
6511
+ }
5453
6512
  while (Date.now() <= deadline) {
5454
- const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5455
- if (done) return done;
6513
+ const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
6514
+ if (legacy) {
6515
+ return legacy;
6516
+ }
5456
6517
  const pending = await db(tasksTable).where({ id: taskId }).first();
5457
- if (!pending) {
5458
- const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5459
- return maybeDone ?? null;
6518
+ if (pending) {
6519
+ cachedNameOpid = { name: pending.name, opid: pending.opid };
6520
+ const done = await historySinceWait(pending.name, pending.opid);
6521
+ if (done) {
6522
+ return done;
6523
+ }
6524
+ } else if (cachedNameOpid) {
6525
+ const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
6526
+ if (done) {
6527
+ return done;
6528
+ }
6529
+ return null;
6530
+ } else {
6531
+ return null;
5460
6532
  }
5461
6533
  await sleepMs(pollMs);
5462
6534
  }
5463
6535
  return null;
5464
6536
  }
5465
6537
  var TasksManager = class _TasksManager {
5466
- context;
5467
- queue;
5468
- target;
5469
- recreateTaskTables;
5470
- pollMs;
5471
- maxParallel;
5472
- scanLimit;
5473
- allowedTasks;
5474
- registry;
5475
- runnerServiceGroup;
5476
- runnerServiceName;
5477
- runnerIdentityDir;
5478
- runnerHeartbeatIntervalMs;
5479
- runnerHeartbeatStaleMs;
5480
- runnerGroupMaxInstances;
5481
- runnerEnforceMaxInstances;
5482
- runnerMetadata;
6538
+ /**
6539
+ * @param {object} context
6540
+ * @param {{
6541
+ * queueName?: string,
6542
+ * target?: string,
6543
+ * recreateTaskTables?: boolean,
6544
+ * pollMs?: number,
6545
+ * claimJitterMs?: number,
6546
+ * maxParallel?: number,
6547
+ * scanLimit?: number,
6548
+ * allowedTasks?: string | string[],
6549
+ * registry?: TasksRegistry | Record<string, Function>,
6550
+ * runnerServiceGroup?: string,
6551
+ * runnerServiceName?: string,
6552
+ * runnerInstanceNumber?: number,
6553
+ * runnerHeartbeatIntervalMs?: number,
6554
+ * runnerHeartbeatStaleMs?: number,
6555
+ * runnerGroupMaxInstances?: number,
6556
+ * runnerEnforceMaxInstances?: boolean,
6557
+ * runnerMetadata?: Record<string, unknown>,
6558
+ * }} [options]
6559
+ */
5483
6560
  constructor(context, options = {}) {
5484
6561
  this.context = context;
5485
- this.queue = options.queue ?? "tasks";
6562
+ this.queueName = options.queueName ?? "tasks";
5486
6563
  this.target = options.target ?? "localRunner";
5487
6564
  this.recreateTaskTables = options.recreateTaskTables ?? false;
5488
6565
  this.pollMs = options.pollMs ?? 1e3;
6566
+ this.claimJitterMs = options.claimJitterMs ?? 0;
5489
6567
  this.maxParallel = options.maxParallel ?? 1;
5490
6568
  this.scanLimit = options.scanLimit ?? 100;
5491
6569
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5492
6570
  this.registry = normalizeRegistry(options.registry);
5493
6571
  this.runnerServiceGroup = options.runnerServiceGroup;
5494
6572
  this.runnerServiceName = options.runnerServiceName;
5495
- this.runnerIdentityDir = options.runnerIdentityDir;
6573
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
5496
6574
  this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
5497
6575
  this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
5498
6576
  this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
5499
6577
  this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
5500
6578
  this.runnerMetadata = options.runnerMetadata;
5501
6579
  }
6580
+ /**
6581
+ * Preferred factory: reads defaults from `context.params` (module namespace
6582
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
6583
+ * and inline options in one consistent resolver.
6584
+ *
6585
+ * @param {object} context
6586
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
6587
+ * @returns {TasksManager}
6588
+ */
5502
6589
  static init(context, options = {}) {
5503
6590
  const defs = {
5504
6591
  table: "string default tasks",
5505
6592
  target: "string default localRunner",
5506
6593
  recreateTaskTables: "boolean default false",
5507
6594
  pollMs: "number default 1000",
6595
+ claimJitterMs: "number default 0",
5508
6596
  maxParallel: "number default 1",
5509
6597
  scanLimit: "number default 100",
5510
6598
  allowedTasks: "string",
5511
6599
  runnerServiceGroup: "string",
5512
6600
  runnerServiceName: "string",
5513
- runnerIdentityDir: "string default ./data/runner-identities",
6601
+ runnerInstanceNumber: "number",
5514
6602
  runnerHeartbeatIntervalMs: "number default 10000",
5515
6603
  runnerHeartbeatStaleMs: "number default 45000",
5516
6604
  runnerGroupMaxInstances: "number",
5517
6605
  runnerEnforceMaxInstances: "boolean default true"
5518
6606
  };
5519
- const discovered = context.params.getAllForModule(defs);
6607
+ const discovered = context.params.getAllForModule("tasks", defs);
5520
6608
  const resolved = {
5521
- queue: discovered.table,
6609
+ queueName: discovered.table,
5522
6610
  target: discovered.target,
5523
6611
  recreateTaskTables: discovered.recreateTaskTables,
5524
6612
  pollMs: discovered.pollMs,
6613
+ claimJitterMs: discovered.claimJitterMs,
5525
6614
  maxParallel: discovered.maxParallel,
5526
6615
  scanLimit: discovered.scanLimit,
5527
6616
  allowedTasks: discovered.allowedTasks,
5528
6617
  runnerServiceGroup: discovered.runnerServiceGroup,
5529
6618
  runnerServiceName: discovered.runnerServiceName,
5530
- runnerIdentityDir: discovered.runnerIdentityDir,
6619
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
5531
6620
  runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
5532
6621
  runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
5533
6622
  runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
@@ -5536,24 +6625,39 @@ var TasksManager = class _TasksManager {
5536
6625
  };
5537
6626
  return new _TasksManager(context, resolved);
5538
6627
  }
6628
+ /**
6629
+ * Idempotently ensure the three backing tables exist for this queue.
6630
+ *
6631
+ * @param {{ recreate?: boolean }} [options]
6632
+ * @returns {Promise<void>}
6633
+ */
5539
6634
  async ensureTaskTables(options = {}) {
5540
6635
  await ensureTaskTables(this.context, {
5541
- queue: this.queue,
6636
+ queueName: this.queueName,
5542
6637
  recreate: options.recreate ?? this.recreateTaskTables
5543
6638
  });
5544
6639
  }
6640
+ /**
6641
+ * Start the runner loop using this manager's resolved config. Per-call
6642
+ * options override the stored defaults, but `runnerMetadata` still falls
6643
+ * through when omitted.
6644
+ *
6645
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
6646
+ * @returns {Promise<void>}
6647
+ */
5545
6648
  async runTasksLoop(options = {}) {
5546
6649
  await runTasksLoop(this.context, {
5547
- queue: options.queue ?? this.queue,
6650
+ queueName: options.queueName ?? this.queueName,
5548
6651
  target: options.target ?? this.target,
5549
6652
  pollMs: options.pollMs ?? this.pollMs,
6653
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
5550
6654
  maxParallel: options.maxParallel ?? this.maxParallel,
5551
6655
  scanLimit: options.scanLimit ?? this.scanLimit,
5552
6656
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
5553
6657
  registry: options.registry ?? this.registry,
5554
6658
  runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
5555
6659
  runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
5556
- runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
6660
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
5557
6661
  runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
5558
6662
  runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
5559
6663
  runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
@@ -5564,6 +6668,7 @@ var TasksManager = class _TasksManager {
5564
6668
  };
5565
6669
  // Annotate the CommonJS export names for ESM import in node:
5566
6670
  0 && (module.exports = {
6671
+ AbstractTask,
5567
6672
  Args,
5568
6673
  Box,
5569
6674
  Db,
@@ -5579,13 +6684,15 @@ var TasksManager = class _TasksManager {
5579
6684
  MultiColumnListWithPreviewComponent,
5580
6685
  Params,
5581
6686
  React,
6687
+ S3,
6688
+ SERVICE_TASK_NAMES,
5582
6689
  ScreenBody,
5583
6690
  ScreenContainer,
5584
6691
  ScreenDivider,
5585
6692
  ScreenFooter,
5586
6693
  ScreenRow,
5587
6694
  ScreenTitle,
5588
- TaskMaster,
6695
+ TaskGetLogs,
5589
6696
  TaskPing,
5590
6697
  TaskSampleProcess,
5591
6698
  TaskShellCommand,
@@ -5600,17 +6707,17 @@ var TasksManager = class _TasksManager {
5600
6707
  buildBreadcrumb,
5601
6708
  buildDetailBreadcrumb,
5602
6709
  buildFooter,
5603
- dbConnect,
5604
- dbFindAndConnect,
5605
- dbInit,
6710
+ convertPattern,
5606
6711
  defaultFileSynopsisFunction,
5607
6712
  defaultTasksRegistry,
5608
6713
  defaultVersionSynopsisFunction,
5609
6714
  enqueueStopTask,
5610
6715
  enqueueTask,
5611
6716
  ensureTaskTables,
6717
+ flushTaskIpcLogs,
5612
6718
  getArgsInstance,
5613
6719
  h,
6720
+ ipcFileLogsTableNameForSourceResource,
5614
6721
  joiEdateType,
5615
6722
  joiStringArrayType,
5616
6723
  listAliveRunnerHeartbeats,
@@ -5618,14 +6725,22 @@ var TasksManager = class _TasksManager {
5618
6725
  listSources,
5619
6726
  listTables,
5620
6727
  load,
6728
+ matchesParsedPattern,
6729
+ memo,
6730
+ mergeAllowedTasksWithServiceTasks,
6731
+ nextTimeMatch,
6732
+ normalizeAllowedTasks,
5621
6733
  organizeFooterMessages,
5622
6734
  queueToTableNames,
6735
+ readTaskIpcLogsSnapshot,
5623
6736
  registerInServicesRegistry,
5624
6737
  registerRunnerHeartbeat,
6738
+ resolveAsterisks,
6739
+ resolveIpcFileLogsDir,
6740
+ resolveRanges,
6741
+ resolveSteps,
5625
6742
  runNodeTaskScript,
5626
6743
  runTasksLoop,
5627
- runnerHeartbeatsTable,
5628
- servicesRegistryTable,
5629
6744
  setupContext,
5630
6745
  showListScreen,
5631
6746
  showMenuScreen,
@@ -5633,6 +6748,8 @@ var TasksManager = class _TasksManager {
5633
6748
  showMultiColumnListWithPreviewScreen,
5634
6749
  showScreen,
5635
6750
  showWordGridScreen,
6751
+ taskHistoryInsertFromQueueRow,
6752
+ timeMatcher,
5636
6753
  touchRunnerHeartbeat,
5637
6754
  touchServicesRegistry,
5638
6755
  unregisterRunnerHeartbeat,
@@ -5642,6 +6759,7 @@ var TasksManager = class _TasksManager {
5642
6759
  useCallback,
5643
6760
  useEffect,
5644
6761
  useInput,
6762
+ useLayoutEffect,
5645
6763
  useMemo,
5646
6764
  useRef,
5647
6765
  useState,