@nmakarov/cli-toolkit 0.21.0 → 0.23.0

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